canvas-ui-sdk 0.3.21 → 0.3.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +680 -15
- package/dist/index.js +6523 -1771
- package/dist/index.js.map +1 -1
- package/mcp/dist/index.js +14 -4
- package/package.json +1 -1
- package/registry/blocks/confirmation-popup.json +18 -0
- package/registry/blocks/contact-form-popup.json +24 -0
- package/registry/blocks/detail-drawer.json +21 -0
- package/registry/blocks/details-popup.json +17 -0
- package/registry/blocks/feedback-popup.json +19 -0
- package/registry/blocks/form-group.json +2 -2
- package/registry/blocks/hero-fullwidth-image.json +1 -1
- package/registry/blocks/hero-section.json +1 -1
- package/registry/blocks/image-popup.json +17 -0
- package/registry/blocks/invoice-popup.json +20 -0
- package/registry/blocks/monthly-calendar-widget.json +1 -1
- package/registry/blocks/page-previews.json +1 -1
- package/registry/blocks/place-detail-panel.json +22 -0
- package/registry/blocks/pricing-cta.json +1 -1
- package/registry/blocks/pricing-plans-popup.json +18 -0
- package/registry/blocks/profile-image-uploader.json +1 -1
- package/registry/blocks/purchase-confirmation-popup.json +18 -0
- package/registry/blocks/sidebar-profile-card.json +1 -1
- package/registry/blocks/slideshow-popup.json +22 -0
- package/registry/blocks/store-location-map.json +18 -0
- package/registry/blocks/terms-of-service-popup.json +18 -0
- package/registry/blocks/video-popup.json +18 -0
- package/registry/blocks/view-profile-popup.json +23 -0
- package/registry/index.json +76 -1
- package/registry/layout/dashboard-shell.json +1 -1
- package/registry/layout/double-sidebar-shell.json +1 -1
- package/registry/layout/header.json +1 -1
- package/registry/layout/icon-sidebar-shell.json +1 -1
- package/registry/layout/mobile-menu-shell.json +1 -1
- package/registry/layout/sidebar.json +1 -1
- package/registry/ui/checkbox.json +1 -1
- package/registry/ui/date-input.json +1 -1
- package/registry/ui/dialog.json +1 -1
- package/registry/ui/dropdown-menu.json +1 -1
- package/registry/ui/input.json +1 -1
- package/registry/ui/label.json +1 -1
- package/registry/ui/line-tabs.json +1 -1
- package/registry/ui/multiselect-checkbox-field.json +1 -1
- package/registry/ui/multiselect-tags.json +1 -1
- package/registry/ui/popover.json +1 -1
- package/registry/ui/searchbox.json +1 -1
- package/registry/ui/select.json +1 -1
- package/registry/ui/selectable-pills.json +1 -1
- package/registry/ui/sheet.json +1 -1
- package/registry/ui/tabs.json +1 -1
- package/registry/ui/text-input.json +1 -1
- package/registry/ui/textarea.json +1 -1
- package/registry/ui/tooltip.json +1 -1
- package/styles/tokens.reference.css +5 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "video-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/video-popup.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { YouTubePlayer } from \"./youtube-player\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface VideoPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** YouTube video ID (e.g., \"dQw4w9WgXcQ\") */\n videoId?: string;\n /** Accessible title for the video (sr-only) */\n title?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_VIDEO_ID = \"I1V9YWqRIeI\";\nconst DEFAULT_TITLE = \"Video player\";\n\n// ---------------------------------------------------------------------------\n// VideoPopup\n// ---------------------------------------------------------------------------\n\nexport function VideoPopup({\n open,\n onOpenChange,\n videoId = DEFAULT_VIDEO_ID,\n title = DEFAULT_TITLE,\n className,\n}: VideoPopupProps) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[768px]\",\n className\n )}\n showCloseButton\n >\n {/* Visually hidden title for accessibility */}\n <DialogTitle className=\"sr-only\">{title}</DialogTitle>\n <DialogDescription className=\"sr-only\">\n Video player for {title}\n </DialogDescription>\n\n <YouTubePlayer videoId={videoId} className=\"rounded-none border-0\" />\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": [
|
|
14
|
+
"lib/utils",
|
|
15
|
+
"ui/dialog",
|
|
16
|
+
"blocks/youtube-player"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "view-profile-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/view-profile-popup.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { GradientBanner } from \"./gradient-banner\";\nimport { AVATAR_MARCUS_WEBB } from \"./demo-avatars\";\nimport {\n Star,\n MoreHorizontal,\n Instagram,\n Facebook,\n Twitter,\n} from \"lucide-react\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ProfileDetailRow {\n /** Label displayed on the left side of the row */\n label: string;\n /** Text value(s) — string for single line, string[] for multi-line */\n value: string | string[];\n /** Rendering mode: \"text\" (default), \"tags\" for pill badges, \"icons\" for social media icons */\n type?: \"text\" | \"tags\" | \"icons\";\n}\n\nexport interface SocialIcon {\n /** Icon name — maps to a lucide-react icon */\n name: \"instagram\" | \"facebook\" | \"twitter\";\n /** Link URL */\n href?: string;\n}\n\nexport interface ViewProfilePopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Profile name */\n name?: string;\n /** Subtitle text below the name (e.g. role/title) */\n subtitle?: string;\n /** Avatar image URL */\n avatarUrl?: string;\n /** Avatar fallback initials */\n avatarFallback?: string;\n /** Star rating from 0 to 5 */\n rating?: number;\n /** Detail rows to display */\n details?: ProfileDetailRow[];\n /** \"View profile\" link href */\n profileUrl?: string;\n /** Label for the profile link */\n profileLinkLabel?: string;\n /** Callback when the Message button is clicked */\n onMessage?: () => void;\n /** Callback when the more options button is clicked */\n onMoreOptions?: () => void;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_NAME = \"Jeffrey Connor\";\nconst DEFAULT_SUBTITLE = \"Math tutor\";\nconst DEFAULT_AVATAR = AVATAR_MARCUS_WEBB;\nconst DEFAULT_AVATAR_FALLBACK = \"JC\";\nconst DEFAULT_RATING = 5;\n\nconst DEFAULT_DETAILS: ProfileDetailRow[] = [\n {\n label: \"Biography\",\n value:\n \"Known for his patient demeanor and innovative teaching methods, Jeffrey tailors his approach to suit each student's unique learning style, fostering a supportive environment where students feel empowered to explore mathematical ideas with confidence.\",\n },\n {\n label: \"Education\",\n value: [\n \"Harvard University, M.S. in Physics\",\n \"Stanford University, M.S. in Mathematics\",\n ],\n },\n {\n label: \"Concepts\",\n value: [\"Algebra\", \"Geometry\", \"Calculus\"],\n type: \"tags\",\n },\n {\n label: \"Experience\",\n value: [\"High School Math Teacher\", \"College Teacher's Assistant\"],\n },\n {\n label: \"Rate\",\n value: \"$50 per hour\",\n },\n {\n label: \"Location\",\n value: [\"Connor Tutoring\", \"123 Broadway Street\", \"New York, NY\", \"10010\"],\n },\n {\n label: \"Social Media\",\n value: [\"instagram\", \"facebook\", \"twitter\"],\n type: \"icons\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst SOCIAL_ICONS: Record<string, React.ElementType> = {\n instagram: Instagram,\n facebook: Facebook,\n twitter: Twitter,\n};\n\nfunction StarRating({ rating }: { rating: number }) {\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-xxs)\" }}\n >\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n size={20}\n fill={i < rating ? \"var(--canvas-primary)\" : \"none\"}\n stroke={i < rating ? \"var(--canvas-primary)\" : \"var(--canvas-border)\"}\n />\n ))}\n </div>\n );\n}\n\nfunction DetailRowValue({ row }: { row: ProfileDetailRow }) {\n const type = row.type ?? \"text\";\n const values = Array.isArray(row.value) ? row.value : [row.value];\n\n if (type === \"tags\") {\n return (\n <div\n className=\"flex flex-wrap items-center\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {values.map((tag) => (\n <span\n key={tag}\n className=\"flex items-center bg-[var(--canvas-border)] overflow-hidden\"\n style={{\n height: 32,\n paddingLeft: \"var(--spacing-lg)\",\n paddingRight: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-xs)\",\n paddingBottom: \"var(--spacing-xs)\",\n borderRadius: \"var(--radius-xs)\",\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {tag}\n </span>\n ))}\n </div>\n );\n }\n\n if (type === \"icons\") {\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n {values.map((iconName) => {\n const IconComponent = SOCIAL_ICONS[iconName];\n if (!IconComponent) return null;\n return (\n <IconComponent\n key={iconName}\n size={24}\n style={{ color: \"var(--canvas-text)\" }}\n />\n );\n })}\n </div>\n );\n }\n\n // Default: text (single or multi-line)\n return (\n <div\n className=\"flex flex-col\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {values.map((line, i) => (\n <span key={i}>{line}</span>\n ))}\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ViewProfilePopup\n// ---------------------------------------------------------------------------\n\nexport function ViewProfilePopup({\n open,\n onOpenChange,\n name = DEFAULT_NAME,\n subtitle = DEFAULT_SUBTITLE,\n avatarUrl = DEFAULT_AVATAR,\n avatarFallback = DEFAULT_AVATAR_FALLBACK,\n rating = DEFAULT_RATING,\n details = DEFAULT_DETAILS,\n profileUrl,\n profileLinkLabel = \"View profile >\",\n onMessage,\n onMoreOptions,\n className,\n}: ViewProfilePopupProps) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n {/* Banner + Avatar section */}\n <div className=\"relative\">\n <GradientBanner\n height=\"160px\"\n className=\"rounded-t-[var(--radius-xl)]\"\n />\n\n {/* Avatar overlapping banner */}\n <div\n className=\"absolute bottom-0 translate-y-1/2\"\n style={{ left: \"var(--spacing-4xl)\" }}\n >\n <Avatar className=\"size-[125px] border-4 border-[var(--canvas-background)]\">\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback\n className=\"font-semibold bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\"\n style={{ fontSize: \"var(--typo-body-xl-size)\" }}\n >\n {avatarFallback}\n </AvatarFallback>\n </Avatar>\n </div>\n </div>\n\n {/* Action buttons — right-aligned below banner */}\n <div\n className=\"flex items-start justify-end\"\n style={{\n paddingTop: \"var(--spacing-2xl)\",\n paddingRight: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n }}\n >\n <Button\n variant=\"neutral\"\n size=\"icon-sm\"\n onClick={onMoreOptions}\n aria-label=\"More options\"\n >\n <MoreHorizontal size={24} />\n </Button>\n <Button variant=\"neutral\" onClick={onMessage}>\n Message\n </Button>\n </div>\n\n {/* Visually hidden dialog description for accessibility */}\n <DialogDescription className=\"sr-only\">\n Profile details for {name}\n </DialogDescription>\n\n {/* Content */}\n <div\n className=\"flex flex-col overflow-hidden\"\n style={{\n gap: \"var(--spacing-2xl)\",\n paddingLeft: \"var(--spacing-4xl)\",\n paddingRight: \"var(--spacing-4xl)\",\n paddingBottom: \"var(--spacing-4xl)\",\n }}\n >\n {/* Name, subtitle, rating */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-none)\" }}>\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {name}\n </DialogTitle>\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {subtitle}\n </span>\n {rating > 0 && (\n <div style={{ paddingTop: \"var(--spacing-xs)\" }}>\n <StarRating rating={rating} />\n </div>\n )}\n </div>\n\n {/* Detail rows */}\n <div className=\"flex flex-col\">\n {details.map((row, idx) => (\n <div\n key={idx}\n className=\"flex gap-[var(--spacing-xl)] items-start w-full\"\n style={{\n paddingTop: \"var(--spacing-xl)\",\n paddingBottom: \"var(--spacing-xl)\",\n borderTop:\n idx === 0\n ? \"1px solid var(--canvas-border)\"\n : undefined,\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n <span\n className=\"shrink-0 w-[180px]\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: 16,\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {row.label}\n </span>\n <div className=\"flex-1 min-w-0\">\n <DetailRowValue row={row} />\n </div>\n </div>\n ))}\n </div>\n\n {/* Footer link */}\n {profileLinkLabel && (\n <div className=\"flex justify-end w-full\">\n {profileUrl ? (\n <a\n href={profileUrl}\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-primary)\",\n }}\n >\n {profileLinkLabel}\n </a>\n ) : (\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-primary)\",\n }}\n >\n {profileLinkLabel}\n </span>\n )}\n </div>\n )}\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"lib/utils",
|
|
17
|
+
"ui/dialog",
|
|
18
|
+
"ui/button",
|
|
19
|
+
"ui/avatar",
|
|
20
|
+
"blocks/gradient-banner",
|
|
21
|
+
"blocks/demo-avatars"
|
|
22
|
+
]
|
|
23
|
+
}
|
package/registry/index.json
CHANGED
|
@@ -66,6 +66,16 @@
|
|
|
66
66
|
"type": "registry:block",
|
|
67
67
|
"description": ""
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
"name": "confirmation-popup",
|
|
71
|
+
"type": "registry:block",
|
|
72
|
+
"description": ""
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "contact-form-popup",
|
|
76
|
+
"type": "registry:block",
|
|
77
|
+
"description": ""
|
|
78
|
+
},
|
|
69
79
|
{
|
|
70
80
|
"name": "content-dropzone",
|
|
71
81
|
"type": "registry:block",
|
|
@@ -116,6 +126,16 @@
|
|
|
116
126
|
"type": "registry:block",
|
|
117
127
|
"description": "Grid of destination cards with images and titles."
|
|
118
128
|
},
|
|
129
|
+
{
|
|
130
|
+
"name": "detail-drawer",
|
|
131
|
+
"type": "registry:block",
|
|
132
|
+
"description": "Right-side detail drawer with tabbed content. Info tab shows metadata fields (with avatars, badges, links), rich content sections, and file attachments. Comments tab shows a chat-style thread with sender names, timestamps, and a comment input."
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "details-popup",
|
|
136
|
+
"type": "registry:block",
|
|
137
|
+
"description": ""
|
|
138
|
+
},
|
|
119
139
|
{
|
|
120
140
|
"name": "dialog",
|
|
121
141
|
"type": "registry:ui",
|
|
@@ -171,6 +191,11 @@
|
|
|
171
191
|
"type": "registry:block",
|
|
172
192
|
"description": "Feature comparison table across pricing tiers."
|
|
173
193
|
},
|
|
194
|
+
{
|
|
195
|
+
"name": "feedback-popup",
|
|
196
|
+
"type": "registry:block",
|
|
197
|
+
"description": ""
|
|
198
|
+
},
|
|
174
199
|
{
|
|
175
200
|
"name": "file-uploader",
|
|
176
201
|
"type": "registry:ui",
|
|
@@ -199,7 +224,7 @@
|
|
|
199
224
|
{
|
|
200
225
|
"name": "form-group",
|
|
201
226
|
"type": "registry:block",
|
|
202
|
-
"description": "
|
|
227
|
+
"description": "Single-column form layout with header (title, sort, filter, action button), configurable fields, and footer (cancel/save). Supports text inputs, textareas, selects, date pickers, multiselect checkboxes, checkbox groups, radio groups, multiselect tags, image/file uploaders, and sliders."
|
|
203
228
|
},
|
|
204
229
|
{
|
|
205
230
|
"name": "gallery-section",
|
|
@@ -266,6 +291,11 @@
|
|
|
266
291
|
"type": "registry:block",
|
|
267
292
|
"description": "Instagram-style image feed with large images, social interactions (like/comment/share/bookmark), and nested comment threads."
|
|
268
293
|
},
|
|
294
|
+
{
|
|
295
|
+
"name": "image-popup",
|
|
296
|
+
"type": "registry:block",
|
|
297
|
+
"description": ""
|
|
298
|
+
},
|
|
269
299
|
{
|
|
270
300
|
"name": "image-uploader",
|
|
271
301
|
"type": "registry:ui",
|
|
@@ -276,6 +306,11 @@
|
|
|
276
306
|
"type": "registry:ui",
|
|
277
307
|
"description": ""
|
|
278
308
|
},
|
|
309
|
+
{
|
|
310
|
+
"name": "invoice-popup",
|
|
311
|
+
"type": "registry:block",
|
|
312
|
+
"description": ""
|
|
313
|
+
},
|
|
279
314
|
{
|
|
280
315
|
"name": "label",
|
|
281
316
|
"type": "registry:ui",
|
|
@@ -406,6 +441,11 @@
|
|
|
406
441
|
"type": "registry:block",
|
|
407
442
|
"description": "Horizontal pill-style tab navigation."
|
|
408
443
|
},
|
|
444
|
+
{
|
|
445
|
+
"name": "place-detail-panel",
|
|
446
|
+
"type": "registry:block",
|
|
447
|
+
"description": ""
|
|
448
|
+
},
|
|
409
449
|
{
|
|
410
450
|
"name": "popover",
|
|
411
451
|
"type": "registry:ui",
|
|
@@ -421,6 +461,11 @@
|
|
|
421
461
|
"type": "registry:block",
|
|
422
462
|
"description": "CTA section for pricing page (contact sales, etc.)."
|
|
423
463
|
},
|
|
464
|
+
{
|
|
465
|
+
"name": "pricing-plans-popup",
|
|
466
|
+
"type": "registry:block",
|
|
467
|
+
"description": ""
|
|
468
|
+
},
|
|
424
469
|
{
|
|
425
470
|
"name": "profile-card",
|
|
426
471
|
"type": "registry:block",
|
|
@@ -456,6 +501,11 @@
|
|
|
456
501
|
"type": "registry:block",
|
|
457
502
|
"description": ""
|
|
458
503
|
},
|
|
504
|
+
{
|
|
505
|
+
"name": "purchase-confirmation-popup",
|
|
506
|
+
"type": "registry:block",
|
|
507
|
+
"description": ""
|
|
508
|
+
},
|
|
459
509
|
{
|
|
460
510
|
"name": "radio-group",
|
|
461
511
|
"type": "registry:ui",
|
|
@@ -581,6 +631,11 @@
|
|
|
581
631
|
"type": "registry:block",
|
|
582
632
|
"description": "2-column grid of portfolio/slideshow tiles with hover states. Features large images with slideshow navigation, save button, user info with avatar, and engagement stats (likes/views)."
|
|
583
633
|
},
|
|
634
|
+
{
|
|
635
|
+
"name": "slideshow-popup",
|
|
636
|
+
"type": "registry:block",
|
|
637
|
+
"description": ""
|
|
638
|
+
},
|
|
584
639
|
{
|
|
585
640
|
"name": "social-feed",
|
|
586
641
|
"type": "registry:block",
|
|
@@ -611,6 +666,11 @@
|
|
|
611
666
|
"type": "registry:block",
|
|
612
667
|
"description": "Horizontal multi-step progress indicator with numbered circles and connecting lines."
|
|
613
668
|
},
|
|
669
|
+
{
|
|
670
|
+
"name": "store-location-map",
|
|
671
|
+
"type": "registry:block",
|
|
672
|
+
"description": "Single store location card with address info, directions button, and embedded Google Maps iframe. Uses TitleGroup for header."
|
|
673
|
+
},
|
|
614
674
|
{
|
|
615
675
|
"name": "switch",
|
|
616
676
|
"type": "registry:ui",
|
|
@@ -631,6 +691,11 @@
|
|
|
631
691
|
"type": "registry:block",
|
|
632
692
|
"description": "Team grid with circular avatar photos."
|
|
633
693
|
},
|
|
694
|
+
{
|
|
695
|
+
"name": "terms-of-service-popup",
|
|
696
|
+
"type": "registry:block",
|
|
697
|
+
"description": ""
|
|
698
|
+
},
|
|
634
699
|
{
|
|
635
700
|
"name": "testimonial-carousel",
|
|
636
701
|
"type": "registry:block",
|
|
@@ -711,6 +776,16 @@
|
|
|
711
776
|
"type": "registry:block",
|
|
712
777
|
"description": "Sidebar playlist for video content with thumbnails."
|
|
713
778
|
},
|
|
779
|
+
{
|
|
780
|
+
"name": "video-popup",
|
|
781
|
+
"type": "registry:block",
|
|
782
|
+
"description": ""
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
"name": "view-profile-popup",
|
|
786
|
+
"type": "registry:block",
|
|
787
|
+
"description": ""
|
|
788
|
+
},
|
|
714
789
|
{
|
|
715
790
|
"name": "webcam-preview",
|
|
716
791
|
"type": "registry:block",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/dashboard-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { Sidebar, NavSection, NavItem } from \"./sidebar\";\nimport { \n Sheet, \n SheetContent,\n SheetTitle,\n} from \"../ui/sheet\";\nimport { cn } from \"../../lib/utils\";\nimport * as VisuallyHidden from \"@radix-ui/react-visually-hidden\";\n\ninterface DashboardShellProps {\n /** Navigation sections for the sidebar */\n navigation: NavSection[];\n /** Optional page header content (e.g., breadcrumbs, page title) */\n pageHeader?: React.ReactNode;\n /** Main content - the modular blocks */\n children: React.ReactNode;\n /** Callback when a nav item or subtab is clicked */\n onNavItemClick?: (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => void;\n /** Callback when app menu (hamburger) is clicked - for future app-level menu */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Dashboard Shell\n * \n * A composable page layout that provides:\n * - Fixed header (80px)\n * - Fixed dark sidebar on desktop (320px, hidden on mobile)\n * - Floating sidebar toggle button on mobile (left edge)\n * - Mobile sheet navigation for dashboard sidebar\n * - Hamburger menu in header for app-level menu (future)\n * - Main content area with pageHeader slot and children slot for blocks\n * \n * @example\n * ```tsx\n * <DashboardShell \n * navigation={navSections}\n * pageHeader={<Breadcrumbs />}\n * >\n * <StatsBlock />\n * <ChartBlock />\n * <TableBlock />\n * </DashboardShell>\n * ```\n */\nexport function DashboardShell({\n navigation,\n pageHeader,\n children,\n onNavItemClick,\n onAppMenuClick,\n contentClassName,\n}: DashboardShellProps) {\n useCSSVariableSync();\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const handleNavItemClick = (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => {\n onNavItemClick?.(item);\n // Close sidebar when nav item is clicked\n setSidebarOpen(false);\n };\n\n const handleAppMenuClick = () => {\n // Placeholder for future app-level menu\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n return (\n <div className=\"min-h-screen min-w-0 bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top, offset on desktop to not overlap sidebar */}\n <div className=\"fixed top-0 left-0 right-0 lg:left-[var(--sidebar-width)] z-40\">\n <Header onMenuClick={handleAppMenuClick} />\n </div>\n\n {/* Desktop Sidebar - Fixed on left, visible lg+ */}\n <div className=\"hidden lg:block fixed top-0 left-0 bottom-0 z-50 w-[var(--sidebar-width)]\">\n <Sidebar
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { Sidebar, NavSection, NavItem } from \"./sidebar\";\nimport { \n Sheet, \n SheetContent,\n SheetTitle,\n} from \"../ui/sheet\";\nimport { cn } from \"../../lib/utils\";\nimport { useThemeBranding } from \"../../context/theme-context\";\nimport * as VisuallyHidden from \"@radix-ui/react-visually-hidden\";\n\ninterface DashboardShellProps {\n /** Navigation sections for the sidebar */\n navigation: NavSection[];\n /** Optional page header content (e.g., breadcrumbs, page title) */\n pageHeader?: React.ReactNode;\n /** Main content - the modular blocks */\n children: React.ReactNode;\n /** Callback when a nav item or subtab is clicked */\n onNavItemClick?: (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => void;\n /** Callback when app menu (hamburger) is clicked - for future app-level menu */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Dashboard Shell\n * \n * A composable page layout that provides:\n * - Fixed header (80px)\n * - Fixed dark sidebar on desktop (320px, hidden on mobile)\n * - Floating sidebar toggle button on mobile (left edge)\n * - Mobile sheet navigation for dashboard sidebar\n * - Hamburger menu in header for app-level menu (future)\n * - Main content area with pageHeader slot and children slot for blocks\n * \n * @example\n * ```tsx\n * <DashboardShell \n * navigation={navSections}\n * pageHeader={<Breadcrumbs />}\n * >\n * <StatsBlock />\n * <ChartBlock />\n * <TableBlock />\n * </DashboardShell>\n * ```\n */\nexport function DashboardShell({\n navigation,\n pageHeader,\n children,\n onNavItemClick,\n onAppMenuClick,\n contentClassName,\n}: DashboardShellProps) {\n useCSSVariableSync();\n const { branding } = useThemeBranding();\n const sidebarVariant = branding.sidebarMode ?? \"dark\";\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const handleNavItemClick = (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => {\n onNavItemClick?.(item);\n // Close sidebar when nav item is clicked\n setSidebarOpen(false);\n };\n\n const handleAppMenuClick = () => {\n // Placeholder for future app-level menu\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n return (\n <div className=\"min-h-screen min-w-0 bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top, offset on desktop to not overlap sidebar */}\n <div className=\"fixed top-0 left-0 right-0 lg:left-[var(--sidebar-width)] z-40\">\n <Header onMenuClick={handleAppMenuClick} />\n </div>\n\n {/* Desktop Sidebar - Fixed on left, visible lg+ */}\n <div className=\"hidden lg:block fixed top-0 left-0 bottom-0 z-50 w-[var(--sidebar-width)]\">\n <Sidebar\n sections={navigation}\n variant={sidebarVariant}\n onItemClick={handleNavItemClick}\n />\n </div>\n\n {/* Mobile Sidebar Toggle Button - Floating on left edge */}\n <button\n onClick={() => setSidebarOpen(true)}\n className={cn(\n \"lg:hidden fixed left-0 z-30\",\n \"top-[calc(var(--header-height)+4px)]\",\n \"flex items-center justify-center\",\n \"size-11\",\n \"bg-[var(--canvas-background)]\",\n \"border border-l-0 border-[var(--canvas-neutral-border)]\",\n \"rounded-r-[var(--radius-xs)]\",\n \"shadow-[0px_4px_16px_0px_rgba(0,0,0,0.04)]\",\n \"transition-opacity hover:opacity-80\"\n )}\n aria-label=\"Open sidebar\"\n >\n <ChevronRight className=\"size-6 text-[var(--canvas-primary)]\" />\n </button>\n\n {/* Mobile Sidebar Sheet */}\n <Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>\n <SheetContent side=\"left\" className=\"p-0 w-[var(--sidebar-width)]\">\n <VisuallyHidden.Root>\n <SheetTitle>Dashboard Navigation</SheetTitle>\n </VisuallyHidden.Root>\n <Sidebar\n sections={navigation}\n variant={sidebarVariant}\n onItemClick={handleNavItemClick}\n />\n </SheetContent>\n </Sheet>\n\n {/* Main Content Area */}\n <main\n className={cn(\n \"pt-[var(--header-height)]\",\n \"lg:pl-[var(--sidebar-width)]\",\n \"min-h-screen\",\n \"overflow-x-hidden\"\n )}\n >\n <div \n className={cn(\n \"flex flex-col gap-[var(--spacing-xl)]\",\n \"px-[var(--spacing-xl)] lg:px-[var(--spacing-5xl)]\",\n \"pt-10 pb-[var(--spacing-5xl)]\",\n contentClassName\n )}\n >\n {/* Page Header Slot */}\n {pageHeader && (\n <section className=\"pt-0\">\n {pageHeader}\n </section>\n )}\n\n {/* Main Content Slot - Blocks go here */}\n <section className=\"flex flex-col gap-[var(--spacing-4xl)]\">\n {children}\n </section>\n </div>\n </main>\n </div>\n );\n}\n\n// Re-export types for convenience\nexport type { NavSection, NavItem } from \"./sidebar\";\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/double-sidebar-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { DoubleSidebar, DoubleSidebarSection, NavTab, defaultDoubleSidebarSections } from \"./double-sidebar\";\nimport { \n Sheet, \n SheetContent,\n SheetTitle,\n} from \"../ui/sheet\";\nimport { cn } from \"../../lib/utils\";\nimport * as VisuallyHidden from \"@radix-ui/react-visually-hidden\";\n\ninterface DoubleSidebarShellProps {\n /** Navigation sections for the double sidebar */\n sections?: DoubleSidebarSection[];\n /** Visual variant for the icon column */\n iconVariant?: \"dark\" | \"light\";\n /** Visual variant for the navigation column */\n navVariant?: \"dark\" | \"light\";\n /** Optional page header content (e.g., breadcrumbs, page title) */\n pageHeader?: React.ReactNode;\n /** Main content - the modular blocks */\n children: React.ReactNode;\n /** Callback when a tab is clicked */\n onTabClick?: (section: DoubleSidebarSection, tab: NavTab) => void;\n /** Callback when app menu (hamburger) is clicked - for future app-level menu */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Double Sidebar Shell\n * \n * A composable page layout with a two-column sidebar that provides:\n * - Fixed header (80px)\n * - Fixed double sidebar on desktop (96px icons + 280px nav = 376px total)\n * - Floating sidebar toggle button on mobile (left edge)\n * - Mobile sheet navigation with both sidebar columns\n * - Each sidebar column can be independently themed (light/dark)\n * - Main content area with pageHeader slot and children slot for blocks\n * \n * Uses the same styling and spacing as DashboardShell for non-sidebar content.\n * \n * @example\n * ```tsx\n * <DoubleSidebarShell \n * sections={sections}\n * iconVariant=\"light\" \n * navVariant=\"light\"\n * >\n * <ContentDropzone label=\"Main content area\" />\n * </DoubleSidebarShell>\n * ```\n */\nexport function DoubleSidebarShell({\n sections = defaultDoubleSidebarSections,\n iconVariant
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { DoubleSidebar, DoubleSidebarSection, NavTab, defaultDoubleSidebarSections } from \"./double-sidebar\";\nimport { \n Sheet, \n SheetContent,\n SheetTitle,\n} from \"../ui/sheet\";\nimport { cn } from \"../../lib/utils\";\nimport * as VisuallyHidden from \"@radix-ui/react-visually-hidden\";\n\ninterface DoubleSidebarShellProps {\n /** Navigation sections for the double sidebar */\n sections?: DoubleSidebarSection[];\n /** Visual variant for the icon column */\n iconVariant?: \"dark\" | \"light\";\n /** Visual variant for the navigation column */\n navVariant?: \"dark\" | \"light\";\n /** Optional page header content (e.g., breadcrumbs, page title) */\n pageHeader?: React.ReactNode;\n /** Main content - the modular blocks */\n children: React.ReactNode;\n /** Callback when a tab is clicked */\n onTabClick?: (section: DoubleSidebarSection, tab: NavTab) => void;\n /** Callback when app menu (hamburger) is clicked - for future app-level menu */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Double Sidebar Shell\n * \n * A composable page layout with a two-column sidebar that provides:\n * - Fixed header (80px)\n * - Fixed double sidebar on desktop (96px icons + 280px nav = 376px total)\n * - Floating sidebar toggle button on mobile (left edge)\n * - Mobile sheet navigation with both sidebar columns\n * - Each sidebar column can be independently themed (light/dark)\n * - Main content area with pageHeader slot and children slot for blocks\n * \n * Uses the same styling and spacing as DashboardShell for non-sidebar content.\n * \n * @example\n * ```tsx\n * <DoubleSidebarShell \n * sections={sections}\n * iconVariant=\"light\" \n * navVariant=\"light\"\n * >\n * <ContentDropzone label=\"Main content area\" />\n * </DoubleSidebarShell>\n * ```\n */\nexport function DoubleSidebarShell({\n sections = defaultDoubleSidebarSections,\n iconVariant,\n navVariant,\n pageHeader,\n children,\n onTabClick,\n onAppMenuClick,\n contentClassName,\n}: DoubleSidebarShellProps) {\n useCSSVariableSync();\n const effectiveIconVariant = iconVariant ?? \"dark\";\n const effectiveNavVariant = navVariant ?? \"light\";\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const handleTabClick = (section: DoubleSidebarSection, tab: NavTab) => {\n onTabClick?.(section, tab);\n // Close sidebar when tab is clicked\n setSidebarOpen(false);\n };\n\n const handleAppMenuClick = () => {\n // Placeholder for future app-level menu\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top, offset on desktop to not overlap double sidebar */}\n <div className=\"fixed top-0 left-0 right-0 lg:left-[var(--double-sidebar-width)] z-40\">\n <Header onMenuClick={handleAppMenuClick} />\n </div>\n\n {/* Desktop Double Sidebar - Fixed on left, visible lg+ */}\n <div className=\"hidden lg:block fixed top-0 left-0 bottom-0 z-50 w-[var(--double-sidebar-width)]\">\n <DoubleSidebar\n sections={sections}\n iconVariant={effectiveIconVariant}\n navVariant={effectiveNavVariant}\n onTabClick={handleTabClick}\n />\n </div>\n\n {/* Mobile Sidebar Toggle Button - Floating on left edge */}\n <button\n onClick={() => setSidebarOpen(true)}\n className={cn(\n \"lg:hidden fixed left-0 z-30\",\n \"top-[calc(var(--header-height)+4px)]\",\n \"flex items-center justify-center\",\n \"size-11\",\n \"bg-[var(--canvas-background)]\",\n \"border border-l-0 border-[var(--canvas-neutral-border)]\",\n \"rounded-r-[var(--radius-xs)]\",\n \"shadow-[0px_4px_16px_0px_rgba(0,0,0,0.04)]\",\n \"transition-opacity hover:opacity-80\"\n )}\n aria-label=\"Open sidebar\"\n >\n <ChevronRight className=\"size-6 text-[var(--canvas-primary)]\" />\n </button>\n\n {/* Mobile Double Sidebar Sheet */}\n <Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>\n <SheetContent side=\"left\" className=\"p-0 w-[var(--double-sidebar-width)]\">\n <VisuallyHidden.Root>\n <SheetTitle>Navigation</SheetTitle>\n </VisuallyHidden.Root>\n <DoubleSidebar\n sections={sections}\n iconVariant={effectiveIconVariant}\n navVariant={effectiveNavVariant}\n onTabClick={handleTabClick}\n onClose={() => setSidebarOpen(false)}\n />\n </SheetContent>\n </Sheet>\n\n {/* Main Content Area - Same styling as DashboardShell */}\n <main\n className={cn(\n \"pt-[var(--header-height)]\",\n \"lg:pl-[var(--double-sidebar-width)]\",\n \"min-h-screen\"\n )}\n >\n <div \n className={cn(\n \"flex flex-col gap-[var(--spacing-xl)]\",\n \"px-[var(--spacing-xl)] lg:px-[var(--spacing-5xl)]\",\n \"pt-10 pb-[var(--spacing-5xl)]\",\n contentClassName\n )}\n >\n {/* Page Header Slot */}\n {pageHeader && (\n <section className=\"pt-0\">\n {pageHeader}\n </section>\n )}\n\n {/* Main Content Slot - Blocks go here */}\n <section className=\"flex flex-col gap-[var(--spacing-4xl)]\">\n {children}\n </section>\n </div>\n </main>\n </div>\n );\n}\n\n// Re-export types for convenience\nexport type { DoubleSidebarSection, NavTab } from \"./double-sidebar\";\n\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/header.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Search, Bell, ShoppingCart, Menu, User, LogOut, MessageSquare, X, Home, Info, LayoutGrid, type LucideIcon } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { Button } from \"../ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../ui/popover\";\nimport { useThemeBranding } from \"../../context/theme-context\";\nimport { AVATAR_ETHAN_BROOKS, AVATAR_SARAH_CHEN, AVATAR_JASON_MORALES, AVATAR_MARCUS_WEBB, AVATAR_ALEX_REEVES, AVATAR_MAYA_JOHNSON, AVATAR_HANNAH_KIM } from \"../blocks/demo-avatars\";\n\n// ============================================\n// Cart Types\n// ============================================\n\nexport interface CartItem {\n id: string;\n name: string;\n price: number;\n image: string;\n}\n\n// Sample cart items for demo\nconst defaultCartItems: CartItem[] = [\n {\n id: \"1\",\n name: \"Julian Bag\",\n price: 120,\n image: \"https://images.unsplash.com/photo-1591561954557-26941169b49e?w=150&h=150&fit=crop\",\n },\n {\n id: \"2\",\n name: \"Davis Keychain\",\n price: 60,\n image: \"https://images.unsplash.com/photo-1606107557195-0e29a4b5b4aa?w=150&h=150&fit=crop&crop=center\",\n },\n];\n\n// ============================================\n// Message Types\n// ============================================\n\nexport interface Message {\n id: string;\n senderName: string;\n senderAvatar: string;\n timestamp: string;\n}\n\n// Sample messages for demo\nconst defaultMessages: Message[] = [\n {\n id: \"1\",\n senderName: \"Ethan Brooks\",\n senderAvatar: AVATAR_ETHAN_BROOKS,\n timestamp: \"Jun 5, 2023 8:13 AM\",\n },\n {\n id: \"2\",\n senderName: \"Sarah Chen\",\n senderAvatar: AVATAR_SARAH_CHEN,\n timestamp: \"May 2, 2023 11:54 AM\",\n },\n {\n id: \"3\",\n senderName: \"Jason Morales\",\n senderAvatar: AVATAR_JASON_MORALES,\n timestamp: \"Jan 10, 2023 5:22 PM\",\n },\n {\n id: \"4\",\n senderName: \"Marcus Webb\",\n senderAvatar: AVATAR_MARCUS_WEBB,\n timestamp: \"Dec 20, 2022 2:22 PM\",\n },\n];\n\n// ============================================\n// Notification Types\n// ============================================\n\nexport interface Notification {\n id: string;\n userName: string;\n userAvatar: string;\n action: string;\n timestamp: string;\n}\n\n// Sample notifications for demo\nconst defaultNotifications: Notification[] = [\n {\n id: \"1\",\n userName: \"Sarah Chen\",\n userAvatar: AVATAR_SARAH_CHEN,\n action: \"liked your photo\",\n timestamp: \"Apr 15, 2023 6:21 AM\",\n },\n {\n id: \"2\",\n userName: \"Alex Reeves\",\n userAvatar: AVATAR_ALEX_REEVES,\n action: \"liked your photo\",\n timestamp: \"Jun 10, 2023 5:45 PM\",\n },\n {\n id: \"3\",\n userName: \"Maya Johnson\",\n userAvatar: AVATAR_MAYA_JOHNSON,\n action: \"liked your photo\",\n timestamp: \"May 9, 2023 2:00 AM\",\n },\n {\n id: \"4\",\n userName: \"Hannah Kim\",\n userAvatar: AVATAR_HANNAH_KIM,\n action: \"liked your photo\",\n timestamp: \"Apr 8, 2023 8:55 PM\",\n },\n];\n\n// ============================================\n// Navigation Types\n// ============================================\n\nexport interface NavItem {\n id: string;\n label: string;\n icon?: LucideIcon;\n href?: string;\n onClick?: () => void;\n}\n\n// Default navigation items\nconst defaultNavItems: NavItem[] = [\n { id: \"home\", label: \"Home\", icon: Home },\n { id: \"about\", label: \"About\", icon: Info },\n { id: \"dashboard\", label: \"Dashboard\", icon: LayoutGrid },\n];\n\n// Phosphor Icons for Logo\nimport { Buildings, type Icon as PhosphorIcon } from \"@phosphor-icons/react\";\nimport {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Storefront, Handshake, ChartLine,\n Palette as PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n} from \"@phosphor-icons/react\";\n\n// Icon shape renderers - use style attribute for CSS variable support\nconst iconShapes = {\n rounded: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n circle: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n square: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n};\n\n// Map icon names to components\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake,\n ChartLine, Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb,\n Leaf, Tree, Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone,\n Heart, Shield, Trophy, Rocket, Target, Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\ninterface HeaderProps {\n /** Callback when mobile menu button is clicked */\n onMenuClick?: () => void;\n /** Whether to show the logo on desktop (for no-sidebar pages) */\n showDesktopLogo?: boolean;\n /** Visual variant - light (default) or dark mode */\n variant?: \"light\" | \"dark\";\n /** Callback when \"My Account\" is clicked */\n onAccountClick?: () => void;\n /** Callback when \"Logout\" is clicked */\n onLogout?: () => void;\n /** Avatar image URL */\n avatarUrl?: string;\n /** Cart items to display */\n cartItems?: CartItem[];\n /** Callback when checkout button is clicked */\n onCheckout?: () => void;\n /** Callback when remove item is clicked */\n onRemoveCartItem?: (id: string) => void;\n /** Messages to display */\n messages?: Message[];\n /** Callback when \"Mark as read\" is clicked for messages */\n onMarkAsRead?: () => void;\n /** Callback when \"view more\" is clicked for messages */\n onViewMoreMessages?: () => void;\n /** Notifications to display */\n notifications?: Notification[];\n /** Callback when \"Mark as read\" is clicked for notifications */\n onMarkNotificationsAsRead?: () => void;\n /** Callback when \"view more\" is clicked for notifications */\n onViewMoreNotifications?: () => void;\n /** Navigation items for header and mobile menu */\n navItems?: NavItem[];\n /** Callback when Login button is clicked */\n onLogin?: () => void;\n /** Callback when Sign up button is clicked */\n onSignUp?: () => void;\n /** Whether to show auth buttons (Login/Sign up) */\n showAuthButtons?: boolean;\n}\n\n/**\n * Canvas Design System - Header/Navbar Component\n * \n * Desktop (lg+): Full logo with wordmark, icon cluster, avatar\n * Mobile/Tablet: Favicon only, avatar, hamburger menu\n * \n * For pages without a sidebar, set showDesktopLogo={true} to display\n * the logo in the header on desktop.\n * \n * Set variant=\"dark\" for a dark themed header that matches the sidebar.\n */\nexport function Header({ \n onMenuClick, \n showDesktopLogo = false, \n variant = \"light\",\n onAccountClick,\n onLogout,\n avatarUrl = AVATAR_SARAH_CHEN,\n cartItems = defaultCartItems,\n onCheckout,\n onRemoveCartItem,\n messages = defaultMessages,\n onMarkAsRead,\n onViewMoreMessages,\n notifications = defaultNotifications,\n onMarkNotificationsAsRead,\n onViewMoreNotifications,\n navItems = defaultNavItems,\n onLogin,\n onSignUp,\n showAuthButtons = false,\n}: HeaderProps) {\n const { branding, isMounted } = useThemeBranding();\n const isDark = variant === \"dark\";\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n \n // Calculate cart total\n const cartTotal = cartItems.reduce((sum, item) => sum + item.price, 0);\n\n // Cart popover content component\n const CartPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Your cart\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-neutral-text)\",\n }}\n >\n {cartItems.length} {cartItems.length === 1 ? \"item\" : \"items\"}\n </span>\n </div>\n\n {/* Cart Items */}\n <div className=\"py-[var(--spacing-xl)] space-y-[var(--spacing-xl)]\">\n {cartItems.map((item) => (\n <div key={item.id} className=\"flex gap-[var(--spacing-lg)]\">\n {/* Product Image */}\n <div \n className=\"size-16 rounded-[var(--radius-md)] overflow-hidden shrink-0 bg-[var(--canvas-neutral-surface)]\"\n >\n <img \n src={item.image} \n alt={item.name}\n className=\"size-full object-cover\"\n />\n </div>\n \n {/* Product Details */}\n <div className=\"flex flex-col justify-center min-w-0\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n ${item.price}\n </span>\n <button\n onClick={() => onRemoveCartItem?.(item.id)}\n className=\"cursor-pointer text-left mt-[var(--spacing-xs)] hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n Remove\n </button>\n </div>\n </div>\n ))}\n </div>\n\n {/* Total */}\n <div \n className=\"flex items-center justify-between py-[var(--spacing-xl)] border-t border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Total\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n ${cartTotal}\n </span>\n </div>\n\n {/* Checkout Button */}\n <Button\n variant=\"primary\"\n className=\"w-full\"\n size=\"default\"\n onClick={onCheckout}\n >\n Checkout\n </Button>\n </div>\n );\n\n // Messages popover content component\n const MessagesPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Messages\n </span>\n <button\n onClick={onMarkAsRead}\n className=\"cursor-pointer hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n Mark as read\n </button>\n </div>\n\n {/* Messages List */}\n <div className=\"py-[var(--spacing-lg)]\">\n {messages.map((message, index) => (\n <div \n key={message.id} \n className={`flex gap-[var(--spacing-lg)] py-[var(--spacing-lg)] ${\n index < messages.length - 1 ? \"border-b border-[var(--canvas-neutral-border)]\" : \"\"\n }`}\n >\n {/* Sender Avatar */}\n <Avatar className=\"size-10 shrink-0\">\n <AvatarImage src={message.senderAvatar} alt={message.senderName} />\n <AvatarFallback \n className=\"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {message.senderName.split(\" \").map(n => n[0]).join(\"\")}\n </AvatarFallback>\n </Avatar>\n \n {/* Message Details */}\n <div className=\"flex flex-col justify-center min-w-0\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ fontWeight: 600 }}>{message.senderName}</span> sent you a message\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-neutral-text)\",\n }}\n >\n {message.timestamp}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* View More */}\n <div \n className=\"pt-[var(--spacing-lg)] border-t border-[var(--canvas-neutral-border)] text-center\"\n >\n <button\n onClick={onViewMoreMessages}\n className=\"cursor-pointer hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n view more\n </button>\n </div>\n </div>\n );\n\n // Notifications popover content component\n const NotificationsPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Notifications\n </span>\n <button\n onClick={onMarkNotificationsAsRead}\n className=\"cursor-pointer hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n Mark as read\n </button>\n </div>\n\n {/* Notifications List */}\n <div className=\"py-[var(--spacing-lg)]\">\n {notifications.map((notification, index) => (\n <div \n key={notification.id} \n className={`flex gap-[var(--spacing-lg)] py-[var(--spacing-lg)] ${\n index < notifications.length - 1 ? \"border-b border-[var(--canvas-neutral-border)]\" : \"\"\n }`}\n >\n {/* User Avatar */}\n <Avatar className=\"size-10 shrink-0\">\n <AvatarImage src={notification.userAvatar} alt={notification.userName} />\n <AvatarFallback \n className=\"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {notification.userName.split(\" \").map(n => n[0]).join(\"\")}\n </AvatarFallback>\n </Avatar>\n \n {/* Notification Details */}\n <div className=\"flex flex-col justify-center min-w-0\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ fontWeight: 600 }}>{notification.userName}</span> {notification.action}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-neutral-text)\",\n }}\n >\n {notification.timestamp}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* View More */}\n <div \n className=\"pt-[var(--spacing-lg)] border-t border-[var(--canvas-neutral-border)] text-center\"\n >\n <button\n onClick={onViewMoreNotifications}\n className=\"cursor-pointer hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n view more\n </button>\n </div>\n </div>\n );\n\n // Get the icon shape renderer\n const shapeRenderer = iconShapes[branding.iconShape as keyof typeof iconShapes] || iconShapes.rounded;\n\n // Logo component used for both mobile and desktop (when showDesktopLogo is true)\n // Uses CSS variables directly - no JavaScript resolution needed\n const LogoIcon = () => {\n // Use CSS variables directly - the browser handles resolution\n const bgColor = branding.bgColor || \"var(--canvas-primary)\";\n const iconColor = branding.iconColor || \"var(--canvas-primary-foreground)\";\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n \n return (\n <div className=\"relative size-8 shrink-0\">\n {shapeRenderer(bgColor)}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n <IconComponent weight=\"bold\" size={18} color={iconColor} />\n </div>\n </div>\n );\n };\n\n return (\n <header \n className={`h-[var(--header-height)] w-full border-b ${\n isDark \n ? \"bg-[var(--canvas-sidebar-dark-bg)] border-[var(--canvas-sidebar-dark-border)]\" \n : \"bg-[var(--canvas-background)] border-[var(--canvas-neutral-border)]\"\n }`}\n >\n <div className=\"flex items-center h-full px-4 lg:px-[var(--spacing-5xl)]\">\n {/* Logo - Visible on mobile, and on desktop when showDesktopLogo is true */}\n <div className={`flex items-center gap-[var(--spacing-md)] h-8 shrink-0 ${showDesktopLogo ? '' : 'lg:hidden'}`}>\n <LogoIcon />\n {/* Wordmark - only on desktop when showDesktopLogo is true */}\n {showDesktopLogo && (\n <span \n className={`hidden lg:block ${isDark ? \"text-white\" : \"text-[var(--canvas-text)]\"}`}\n style={{\n fontFamily: \"var(--typo-header-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-header-spacing)\",\n lineHeight: \"var(--typo-header-line-height)\",\n }}\n >\n {branding.wordmark || \"canvas\"}\n </span>\n )}\n </div>\n\n {/* Spacer */}\n <div className=\"flex-1\" />\n\n {/* Navigation Links - Desktop Only */}\n <nav className=\"hidden lg:flex items-center gap-[var(--spacing-2xl)] h-full\">\n {navItems.map((item) => (\n <button\n key={item.id}\n onClick={() => {\n item.onClick?.();\n if (item.href) {\n window.location.href = item.href;\n }\n }}\n className={`cursor-pointer transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n style={{\n fontFamily: \"var(--typo-header-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-header-size)\",\n fontWeight: \"var(--typo-header-weight)\",\n letterSpacing: \"var(--typo-header-spacing)\",\n lineHeight: \"var(--typo-header-line-height)\",\n }}\n >\n {item.label}\n </button>\n ))}\n </nav>\n\n {/* Icons - Always Visible */}\n <div className=\"flex items-center gap-[var(--spacing-2xl)] ml-[var(--spacing-2xl)]\">\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-white/60 hover:text-white\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Search\"\n >\n <Search className=\"size-4\" />\n </button>\n \n {isMounted ? (\n <Popover>\n <PopoverTrigger asChild>\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-white/60 hover:text-white\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Notifications\"\n >\n <Bell className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <NotificationsPopoverContent />\n </PopoverContent>\n </Popover>\n ) : (\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-white/60 hover:text-white\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Notifications\"\n >\n <Bell className=\"size-4\" />\n </button>\n )}\n\n {isMounted ? (\n <Popover>\n <PopoverTrigger asChild>\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-white/60 hover:text-white\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Messages\"\n >\n <MessageSquare className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <MessagesPopoverContent />\n </PopoverContent>\n </Popover>\n ) : (\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-white/60 hover:text-white\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Messages\"\n >\n <MessageSquare className=\"size-4\" />\n </button>\n )}\n\n {isMounted ? (\n <Popover>\n <PopoverTrigger asChild>\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-white/60 hover:text-white\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Cart\"\n >\n <ShoppingCart className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <CartPopoverContent />\n </PopoverContent>\n </Popover>\n ) : (\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-white/60 hover:text-white\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Cart\"\n >\n <ShoppingCart className=\"size-4\" />\n </button>\n )}\n \n {/* Auth Buttons - Desktop Only */}\n {showAuthButtons && (\n <div className=\"hidden lg:flex items-center gap-[var(--spacing-lg)]\">\n <Button\n variant=\"primary-outline\"\n size=\"default\"\n onClick={onLogin}\n >\n Log in\n </Button>\n <Button\n variant=\"primary\"\n size=\"default\"\n onClick={onSignUp}\n >\n Sign up\n </Button>\n </div>\n )}\n \n {/* Avatar with Dropdown */}\n {isMounted ? (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button className=\"cursor-pointer rounded-full focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:ring-offset-2\">\n <Avatar className={`size-10 border cursor-pointer ${\n isDark\n ? \"border-[var(--canvas-sidebar-dark-border)]\"\n : \"border-[var(--canvas-neutral-border)]\"\n }`}>\n <AvatarImage src={avatarUrl} alt=\"User avatar\" />\n <AvatarFallback\n className={\n isDark\n ? \"bg-white/10 text-white/60\"\n : \"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n }\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n JC\n </AvatarFallback>\n </Avatar>\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" sideOffset={8}>\n <DropdownMenuItem onClick={onAccountClick}>\n <User className=\"size-4 mr-2\" />\n My Account\n </DropdownMenuItem>\n <DropdownMenuItem onClick={onLogout}>\n <LogOut className=\"size-4 mr-2\" />\n Logout\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n ) : (\n <button className=\"cursor-pointer rounded-full focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:ring-offset-2\">\n <Avatar className={`size-10 border cursor-pointer ${\n isDark\n ? \"border-[var(--canvas-sidebar-dark-border)]\"\n : \"border-[var(--canvas-neutral-border)]\"\n }`}>\n <AvatarImage src={avatarUrl} alt=\"User avatar\" />\n <AvatarFallback\n className={\n isDark\n ? \"bg-white/10 text-white/60\"\n : \"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n }\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n JC\n </AvatarFallback>\n </Avatar>\n </button>\n )}\n\n {/* Mobile Menu Button */}\n <Button \n variant=\"ghost\" \n size=\"icon\" \n onClick={() => {\n setIsMobileMenuOpen(true);\n onMenuClick?.();\n }}\n aria-label=\"Open menu\"\n className={`lg:hidden -ml-[var(--spacing-md)] ${isDark ? \"text-white/60 hover:text-white hover:bg-white/10\" : \"text-[var(--canvas-neutral-text)]\"}`}\n >\n <Menu className=\"size-4\" />\n </Button>\n </div>\n </div>\n\n {/* Mobile Menu Overlay */}\n {isMobileMenuOpen && (\n <div className=\"fixed inset-0 z-50 lg:hidden\">\n {/* Backdrop */}\n <div \n className=\"absolute inset-0 bg-black/50\"\n onClick={() => setIsMobileMenuOpen(false)}\n />\n \n {/* Menu Panel */}\n <div className=\"absolute right-0 top-0 h-full w-full max-w-sm bg-[var(--canvas-background)] shadow-xl\">\n {/* Close Button */}\n <div className=\"flex justify-end p-4\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setIsMobileMenuOpen(false)}\n aria-label=\"Close menu\"\n >\n <X className=\"size-5\" />\n </Button>\n </div>\n \n {/* Navigation Items */}\n <nav className=\"px-6 py-4\">\n <div className=\"space-y-2\">\n {navItems.map((item) => {\n const Icon = item.icon;\n return (\n <button\n key={item.id}\n onClick={() => {\n item.onClick?.();\n if (item.href) {\n window.location.href = item.href;\n }\n setIsMobileMenuOpen(false);\n }}\n className=\"cursor-pointer flex items-center gap-[var(--spacing-lg)] w-full py-[var(--spacing-lg)] text-left hover:bg-[var(--canvas-neutral-surface)] rounded-[var(--radius-md)] transition-colors\"\n >\n {Icon && (\n <div \n className=\"size-12 rounded-[var(--radius-md)] flex items-center justify-center shrink-0\"\n style={{\n backgroundColor: \"color-mix(in srgb, var(--canvas-primary) 10%, transparent)\",\n }}\n >\n <Icon \n className=\"size-5\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n </div>\n )}\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.label}\n </span>\n </button>\n );\n })}\n </div>\n </nav>\n \n {/* Auth Buttons */}\n {showAuthButtons && (\n <div className=\"absolute bottom-0 left-0 right-0 p-6 space-y-3 border-t border-[var(--canvas-neutral-border)]\">\n <Button\n variant=\"primary-outline\"\n className=\"w-full\"\n size=\"lg\"\n onClick={() => {\n onLogin?.();\n setIsMobileMenuOpen(false);\n }}\n >\n Log in\n </Button>\n <Button\n variant=\"primary\"\n className=\"w-full\"\n size=\"lg\"\n onClick={() => {\n onSignUp?.();\n setIsMobileMenuOpen(false);\n }}\n >\n Sign up\n </Button>\n </div>\n )}\n </div>\n </div>\n )}\n </header>\n );\n}\n\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Search, Bell, ShoppingCart, Menu, User, LogOut, MessageSquare, X, Home, Info, LayoutGrid, type LucideIcon } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { Button } from \"../ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../ui/popover\";\nimport { useThemeBranding } from \"../../context/theme-context\";\nimport { AVATAR_ETHAN_BROOKS, AVATAR_SARAH_CHEN, AVATAR_JASON_MORALES, AVATAR_MARCUS_WEBB, AVATAR_ALEX_REEVES, AVATAR_MAYA_JOHNSON, AVATAR_HANNAH_KIM } from \"../blocks/demo-avatars\";\n\n// ============================================\n// Cart Types\n// ============================================\n\nexport interface CartItem {\n id: string;\n name: string;\n price: number;\n image: string;\n}\n\n// Sample cart items for demo\nconst defaultCartItems: CartItem[] = [\n {\n id: \"1\",\n name: \"Julian Bag\",\n price: 120,\n image: \"https://images.unsplash.com/photo-1591561954557-26941169b49e?w=150&h=150&fit=crop\",\n },\n {\n id: \"2\",\n name: \"Davis Keychain\",\n price: 60,\n image: \"https://images.unsplash.com/photo-1606107557195-0e29a4b5b4aa?w=150&h=150&fit=crop&crop=center\",\n },\n];\n\n// ============================================\n// Message Types\n// ============================================\n\nexport interface Message {\n id: string;\n senderName: string;\n senderAvatar: string;\n timestamp: string;\n}\n\n// Sample messages for demo\nconst defaultMessages: Message[] = [\n {\n id: \"1\",\n senderName: \"Ethan Brooks\",\n senderAvatar: AVATAR_ETHAN_BROOKS,\n timestamp: \"Jun 5, 2023 8:13 AM\",\n },\n {\n id: \"2\",\n senderName: \"Sarah Chen\",\n senderAvatar: AVATAR_SARAH_CHEN,\n timestamp: \"May 2, 2023 11:54 AM\",\n },\n {\n id: \"3\",\n senderName: \"Jason Morales\",\n senderAvatar: AVATAR_JASON_MORALES,\n timestamp: \"Jan 10, 2023 5:22 PM\",\n },\n {\n id: \"4\",\n senderName: \"Marcus Webb\",\n senderAvatar: AVATAR_MARCUS_WEBB,\n timestamp: \"Dec 20, 2022 2:22 PM\",\n },\n];\n\n// ============================================\n// Notification Types\n// ============================================\n\nexport interface Notification {\n id: string;\n userName: string;\n userAvatar: string;\n action: string;\n timestamp: string;\n}\n\n// Sample notifications for demo\nconst defaultNotifications: Notification[] = [\n {\n id: \"1\",\n userName: \"Sarah Chen\",\n userAvatar: AVATAR_SARAH_CHEN,\n action: \"liked your photo\",\n timestamp: \"Apr 15, 2023 6:21 AM\",\n },\n {\n id: \"2\",\n userName: \"Alex Reeves\",\n userAvatar: AVATAR_ALEX_REEVES,\n action: \"liked your photo\",\n timestamp: \"Jun 10, 2023 5:45 PM\",\n },\n {\n id: \"3\",\n userName: \"Maya Johnson\",\n userAvatar: AVATAR_MAYA_JOHNSON,\n action: \"liked your photo\",\n timestamp: \"May 9, 2023 2:00 AM\",\n },\n {\n id: \"4\",\n userName: \"Hannah Kim\",\n userAvatar: AVATAR_HANNAH_KIM,\n action: \"liked your photo\",\n timestamp: \"Apr 8, 2023 8:55 PM\",\n },\n];\n\n// ============================================\n// Navigation Types\n// ============================================\n\nexport interface NavItem {\n id: string;\n label: string;\n icon?: LucideIcon;\n href?: string;\n onClick?: () => void;\n}\n\n// Default navigation items\nconst defaultNavItems: NavItem[] = [\n { id: \"home\", label: \"Home\", icon: Home },\n { id: \"about\", label: \"About\", icon: Info },\n { id: \"dashboard\", label: \"Dashboard\", icon: LayoutGrid },\n];\n\n// Phosphor Icons for Logo\nimport { Buildings, type Icon as PhosphorIcon } from \"@phosphor-icons/react\";\nimport {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Storefront, Handshake, ChartLine,\n Palette as PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n} from \"@phosphor-icons/react\";\n\n// Icon shape renderers - use style attribute for CSS variable support\nconst iconShapes = {\n rounded: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n circle: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n square: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n};\n\n// Map icon names to components\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake,\n ChartLine, Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb,\n Leaf, Tree, Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone,\n Heart, Shield, Trophy, Rocket, Target, Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\ninterface HeaderProps {\n /** Callback when mobile menu button is clicked */\n onMenuClick?: () => void;\n /** Whether to show the logo on desktop (for no-sidebar pages) */\n showDesktopLogo?: boolean;\n /** Visual variant - light (default) or dark mode */\n variant?: \"light\" | \"dark\";\n /** Callback when \"My Account\" is clicked */\n onAccountClick?: () => void;\n /** Callback when \"Logout\" is clicked */\n onLogout?: () => void;\n /** Avatar image URL */\n avatarUrl?: string;\n /** Cart items to display */\n cartItems?: CartItem[];\n /** Callback when checkout button is clicked */\n onCheckout?: () => void;\n /** Callback when remove item is clicked */\n onRemoveCartItem?: (id: string) => void;\n /** Messages to display */\n messages?: Message[];\n /** Callback when \"Mark as read\" is clicked for messages */\n onMarkAsRead?: () => void;\n /** Callback when \"view more\" is clicked for messages */\n onViewMoreMessages?: () => void;\n /** Notifications to display */\n notifications?: Notification[];\n /** Callback when \"Mark as read\" is clicked for notifications */\n onMarkNotificationsAsRead?: () => void;\n /** Callback when \"view more\" is clicked for notifications */\n onViewMoreNotifications?: () => void;\n /** Navigation items for header and mobile menu */\n navItems?: NavItem[];\n /** Callback when Login button is clicked */\n onLogin?: () => void;\n /** Callback when Sign up button is clicked */\n onSignUp?: () => void;\n /** Whether to show auth buttons (Login/Sign up) */\n showAuthButtons?: boolean;\n}\n\n/**\n * Canvas Design System - Header/Navbar Component\n * \n * Desktop (lg+): Full logo with wordmark, icon cluster, avatar\n * Mobile/Tablet: Favicon only, avatar, hamburger menu\n * \n * For pages without a sidebar, set showDesktopLogo={true} to display\n * the logo in the header on desktop.\n * \n * Set variant=\"dark\" for a dark themed header that matches the sidebar.\n */\nexport function Header({ \n onMenuClick, \n showDesktopLogo = false, \n variant = \"light\",\n onAccountClick,\n onLogout,\n avatarUrl = AVATAR_SARAH_CHEN,\n cartItems = defaultCartItems,\n onCheckout,\n onRemoveCartItem,\n messages = defaultMessages,\n onMarkAsRead,\n onViewMoreMessages,\n notifications = defaultNotifications,\n onMarkNotificationsAsRead,\n onViewMoreNotifications,\n navItems = defaultNavItems,\n onLogin,\n onSignUp,\n showAuthButtons = false,\n}: HeaderProps) {\n const { branding, isMounted } = useThemeBranding();\n const isDark = variant === \"dark\";\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n \n // Calculate cart total\n const cartTotal = cartItems.reduce((sum, item) => sum + item.price, 0);\n\n // Cart popover content component\n const CartPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Your cart\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-neutral-text)\",\n }}\n >\n {cartItems.length} {cartItems.length === 1 ? \"item\" : \"items\"}\n </span>\n </div>\n\n {/* Cart Items */}\n <div className=\"py-[var(--spacing-xl)] space-y-[var(--spacing-xl)]\">\n {cartItems.map((item) => (\n <div key={item.id} className=\"flex gap-[var(--spacing-lg)]\">\n {/* Product Image */}\n <div \n className=\"size-16 rounded-[var(--radius-md)] overflow-hidden shrink-0 bg-[var(--canvas-neutral-surface)]\"\n >\n <img \n src={item.image} \n alt={item.name}\n className=\"size-full object-cover\"\n />\n </div>\n \n {/* Product Details */}\n <div className=\"flex flex-col justify-center min-w-0\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n ${item.price}\n </span>\n <button\n onClick={() => onRemoveCartItem?.(item.id)}\n className=\"cursor-pointer text-left mt-[var(--spacing-xs)] hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n Remove\n </button>\n </div>\n </div>\n ))}\n </div>\n\n {/* Total */}\n <div \n className=\"flex items-center justify-between py-[var(--spacing-xl)] border-t border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Total\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n ${cartTotal}\n </span>\n </div>\n\n {/* Checkout Button */}\n <Button\n variant=\"primary\"\n className=\"w-full\"\n size=\"default\"\n onClick={onCheckout}\n >\n Checkout\n </Button>\n </div>\n );\n\n // Messages popover content component\n const MessagesPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Messages\n </span>\n <button\n onClick={onMarkAsRead}\n className=\"cursor-pointer hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n Mark as read\n </button>\n </div>\n\n {/* Messages List */}\n <div className=\"py-[var(--spacing-lg)]\">\n {messages.map((message, index) => (\n <div \n key={message.id} \n className={`flex gap-[var(--spacing-lg)] py-[var(--spacing-lg)] ${\n index < messages.length - 1 ? \"border-b border-[var(--canvas-neutral-border)]\" : \"\"\n }`}\n >\n {/* Sender Avatar */}\n <Avatar className=\"size-10 shrink-0\">\n <AvatarImage src={message.senderAvatar} alt={message.senderName} />\n <AvatarFallback \n className=\"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {message.senderName.split(\" \").map(n => n[0]).join(\"\")}\n </AvatarFallback>\n </Avatar>\n \n {/* Message Details */}\n <div className=\"flex flex-col justify-center min-w-0\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ fontWeight: 600 }}>{message.senderName}</span> sent you a message\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-neutral-text)\",\n }}\n >\n {message.timestamp}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* View More */}\n <div \n className=\"pt-[var(--spacing-lg)] border-t border-[var(--canvas-neutral-border)] text-center\"\n >\n <button\n onClick={onViewMoreMessages}\n className=\"cursor-pointer hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n view more\n </button>\n </div>\n </div>\n );\n\n // Notifications popover content component\n const NotificationsPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Notifications\n </span>\n <button\n onClick={onMarkNotificationsAsRead}\n className=\"cursor-pointer hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n Mark as read\n </button>\n </div>\n\n {/* Notifications List */}\n <div className=\"py-[var(--spacing-lg)]\">\n {notifications.map((notification, index) => (\n <div \n key={notification.id} \n className={`flex gap-[var(--spacing-lg)] py-[var(--spacing-lg)] ${\n index < notifications.length - 1 ? \"border-b border-[var(--canvas-neutral-border)]\" : \"\"\n }`}\n >\n {/* User Avatar */}\n <Avatar className=\"size-10 shrink-0\">\n <AvatarImage src={notification.userAvatar} alt={notification.userName} />\n <AvatarFallback \n className=\"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {notification.userName.split(\" \").map(n => n[0]).join(\"\")}\n </AvatarFallback>\n </Avatar>\n \n {/* Notification Details */}\n <div className=\"flex flex-col justify-center min-w-0\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ fontWeight: 600 }}>{notification.userName}</span> {notification.action}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-neutral-text)\",\n }}\n >\n {notification.timestamp}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* View More */}\n <div \n className=\"pt-[var(--spacing-lg)] border-t border-[var(--canvas-neutral-border)] text-center\"\n >\n <button\n onClick={onViewMoreNotifications}\n className=\"cursor-pointer hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n view more\n </button>\n </div>\n </div>\n );\n\n // Get the icon shape renderer\n const shapeRenderer = iconShapes[branding.iconShape as keyof typeof iconShapes] || iconShapes.rounded;\n\n // Logo component used for both mobile and desktop (when showDesktopLogo is true)\n // Uses CSS variables directly - no JavaScript resolution needed\n const LogoIcon = () => {\n // Use CSS variables directly - the browser handles resolution\n const bgColor = branding.bgColor || \"var(--canvas-primary)\";\n const iconColor = branding.iconColor || \"var(--canvas-primary-foreground)\";\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n \n return (\n <div className=\"relative size-8 shrink-0\">\n {shapeRenderer(bgColor)}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n <IconComponent weight=\"bold\" size={18} color={iconColor} />\n </div>\n </div>\n );\n };\n\n return (\n <header \n className={`h-[var(--header-height)] w-full border-b ${\n isDark \n ? \"bg-[var(--canvas-sidebar-dark-bg)] border-[var(--canvas-sidebar-dark-border)]\" \n : \"bg-[var(--canvas-background)] border-[var(--canvas-neutral-border)]\"\n }`}\n >\n <div className=\"flex items-center h-full px-4 lg:px-[var(--spacing-5xl)]\">\n {/* Logo - Visible on mobile, and on desktop when showDesktopLogo is true */}\n <div className={`flex items-center gap-[var(--spacing-md)] h-8 shrink-0 ${showDesktopLogo ? '' : 'lg:hidden'}`}>\n <LogoIcon />\n {/* Wordmark - only on desktop when showDesktopLogo is true */}\n {showDesktopLogo && (\n <span \n className={`hidden lg:block ${isDark ? \"text-[var(--canvas-sidebar-dark-active-text)]\" : \"text-[var(--canvas-text)]\"}`}\n style={{\n fontFamily: \"var(--typo-header-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-header-spacing)\",\n lineHeight: \"var(--typo-header-line-height)\",\n }}\n >\n {branding.wordmark || \"canvas\"}\n </span>\n )}\n </div>\n\n {/* Spacer */}\n <div className=\"flex-1\" />\n\n {/* Navigation Links - Desktop Only */}\n <nav className=\"hidden lg:flex items-center gap-[var(--spacing-2xl)] h-full\">\n {navItems.map((item) => (\n <button\n key={item.id}\n onClick={() => {\n item.onClick?.();\n if (item.href) {\n window.location.href = item.href;\n }\n }}\n className={`cursor-pointer transition-colors ${\n isDark \n ? \"text-[var(--canvas-sidebar-dark-text)] hover:text-[var(--canvas-sidebar-dark-active-text)]\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n style={{\n fontFamily: \"var(--typo-header-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-header-size)\",\n fontWeight: \"var(--typo-header-weight)\",\n letterSpacing: \"var(--typo-header-spacing)\",\n lineHeight: \"var(--typo-header-line-height)\",\n }}\n >\n {item.label}\n </button>\n ))}\n </nav>\n\n {/* Icons - Always Visible */}\n <div className=\"flex items-center gap-[var(--spacing-2xl)] ml-[var(--spacing-2xl)]\">\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-[var(--canvas-sidebar-dark-text)] hover:text-[var(--canvas-sidebar-dark-active-text)]\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Search\"\n >\n <Search className=\"size-4\" />\n </button>\n \n {isMounted ? (\n <Popover>\n <PopoverTrigger asChild>\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-[var(--canvas-sidebar-dark-text)] hover:text-[var(--canvas-sidebar-dark-active-text)]\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Notifications\"\n >\n <Bell className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <NotificationsPopoverContent />\n </PopoverContent>\n </Popover>\n ) : (\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-[var(--canvas-sidebar-dark-text)] hover:text-[var(--canvas-sidebar-dark-active-text)]\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Notifications\"\n >\n <Bell className=\"size-4\" />\n </button>\n )}\n\n {isMounted ? (\n <Popover>\n <PopoverTrigger asChild>\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-[var(--canvas-sidebar-dark-text)] hover:text-[var(--canvas-sidebar-dark-active-text)]\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Messages\"\n >\n <MessageSquare className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <MessagesPopoverContent />\n </PopoverContent>\n </Popover>\n ) : (\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-[var(--canvas-sidebar-dark-text)] hover:text-[var(--canvas-sidebar-dark-active-text)]\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Messages\"\n >\n <MessageSquare className=\"size-4\" />\n </button>\n )}\n\n {isMounted ? (\n <Popover>\n <PopoverTrigger asChild>\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-[var(--canvas-sidebar-dark-text)] hover:text-[var(--canvas-sidebar-dark-active-text)]\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Cart\"\n >\n <ShoppingCart className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <CartPopoverContent />\n </PopoverContent>\n </Popover>\n ) : (\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-[var(--canvas-sidebar-dark-text)] hover:text-[var(--canvas-sidebar-dark-active-text)]\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Cart\"\n >\n <ShoppingCart className=\"size-4\" />\n </button>\n )}\n \n {/* Auth Buttons - Desktop Only */}\n {showAuthButtons && (\n <div className=\"hidden lg:flex items-center gap-[var(--spacing-lg)]\">\n <Button\n variant=\"primary-outline\"\n size=\"default\"\n onClick={onLogin}\n >\n Log in\n </Button>\n <Button\n variant=\"primary\"\n size=\"default\"\n onClick={onSignUp}\n >\n Sign up\n </Button>\n </div>\n )}\n \n {/* Avatar with Dropdown */}\n {isMounted ? (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button className=\"cursor-pointer rounded-full focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:ring-offset-2\">\n <Avatar className={`size-10 border cursor-pointer ${\n isDark\n ? \"border-[var(--canvas-sidebar-dark-border)]\"\n : \"border-[var(--canvas-neutral-border)]\"\n }`}>\n <AvatarImage src={avatarUrl} alt=\"User avatar\" />\n <AvatarFallback\n className={\n isDark\n ? \"bg-[var(--canvas-sidebar-dark-active-bg)] text-[var(--canvas-sidebar-dark-text)]\"\n : \"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n }\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n JC\n </AvatarFallback>\n </Avatar>\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" sideOffset={8}>\n <DropdownMenuItem onClick={onAccountClick}>\n <User className=\"size-4 mr-2\" />\n My Account\n </DropdownMenuItem>\n <DropdownMenuItem onClick={onLogout}>\n <LogOut className=\"size-4 mr-2\" />\n Logout\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n ) : (\n <button className=\"cursor-pointer rounded-full focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:ring-offset-2\">\n <Avatar className={`size-10 border cursor-pointer ${\n isDark\n ? \"border-[var(--canvas-sidebar-dark-border)]\"\n : \"border-[var(--canvas-neutral-border)]\"\n }`}>\n <AvatarImage src={avatarUrl} alt=\"User avatar\" />\n <AvatarFallback\n className={\n isDark\n ? \"bg-[var(--canvas-sidebar-dark-active-bg)] text-[var(--canvas-sidebar-dark-text)]\"\n : \"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n }\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n JC\n </AvatarFallback>\n </Avatar>\n </button>\n )}\n\n {/* Mobile Menu Button */}\n <Button \n variant=\"ghost\" \n size=\"icon\" \n onClick={() => {\n setIsMobileMenuOpen(true);\n onMenuClick?.();\n }}\n aria-label=\"Open menu\"\n className={`lg:hidden -ml-[var(--spacing-md)] ${isDark ? \"text-[var(--canvas-sidebar-dark-text)] hover:text-[var(--canvas-sidebar-dark-active-text)] hover:bg-[var(--canvas-sidebar-dark-active-bg)]\" : \"text-[var(--canvas-neutral-text)]\"}`}\n >\n <Menu className=\"size-4\" />\n </Button>\n </div>\n </div>\n\n {/* Mobile Menu Overlay */}\n {isMobileMenuOpen && (\n <div className=\"fixed inset-0 z-50 lg:hidden\">\n {/* Backdrop */}\n <div \n className=\"absolute inset-0 bg-[var(--canvas-overlay-bg)]\"\n onClick={() => setIsMobileMenuOpen(false)}\n />\n \n {/* Menu Panel */}\n <div className=\"absolute right-0 top-0 h-full w-full max-w-sm bg-[var(--canvas-background)] shadow-xl\">\n {/* Close Button */}\n <div className=\"flex justify-end p-4\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setIsMobileMenuOpen(false)}\n aria-label=\"Close menu\"\n >\n <X className=\"size-5\" />\n </Button>\n </div>\n \n {/* Navigation Items */}\n <nav className=\"px-6 py-4\">\n <div className=\"space-y-2\">\n {navItems.map((item) => {\n const Icon = item.icon;\n return (\n <button\n key={item.id}\n onClick={() => {\n item.onClick?.();\n if (item.href) {\n window.location.href = item.href;\n }\n setIsMobileMenuOpen(false);\n }}\n className=\"cursor-pointer flex items-center gap-[var(--spacing-lg)] w-full py-[var(--spacing-lg)] text-left hover:bg-[var(--canvas-neutral-surface)] rounded-[var(--radius-md)] transition-colors\"\n >\n {Icon && (\n <div \n className=\"size-12 rounded-[var(--radius-md)] flex items-center justify-center shrink-0\"\n style={{\n backgroundColor: \"color-mix(in srgb, var(--canvas-primary) 10%, transparent)\",\n }}\n >\n <Icon \n className=\"size-5\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n </div>\n )}\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.label}\n </span>\n </button>\n );\n })}\n </div>\n </nav>\n \n {/* Auth Buttons */}\n {showAuthButtons && (\n <div className=\"absolute bottom-0 left-0 right-0 p-6 space-y-3 border-t border-[var(--canvas-neutral-border)]\">\n <Button\n variant=\"primary-outline\"\n className=\"w-full\"\n size=\"lg\"\n onClick={() => {\n onLogin?.();\n setIsMobileMenuOpen(false);\n }}\n >\n Log in\n </Button>\n <Button\n variant=\"primary\"\n className=\"w-full\"\n size=\"lg\"\n onClick={() => {\n onSignUp?.();\n setIsMobileMenuOpen(false);\n }}\n >\n Sign up\n </Button>\n </div>\n )}\n </div>\n </div>\n )}\n </header>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/icon-sidebar-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { IconSidebar, IconNavItemConfig, defaultIconNavItems } from \"./icon-sidebar\";\nimport { \n Sheet, \n SheetContent,\n SheetTitle,\n} from \"../ui/sheet\";\nimport { cn } from \"../../lib/utils\";\nimport * as VisuallyHidden from \"@radix-ui/react-visually-hidden\";\n\ninterface IconSidebarShellProps {\n /** Navigation items for the icon sidebar */\n navigation?: IconNavItemConfig[];\n /** Optional page header content (e.g., breadcrumbs, page title) */\n pageHeader?: React.ReactNode;\n /** Main content - the modular blocks */\n children: React.ReactNode;\n /** Callback when a nav item is clicked */\n onNavItemClick?: (item: IconNavItemConfig) => void;\n /** Callback when app menu (hamburger) is clicked - for future app-level menu */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Icon Sidebar Shell\n * \n * A composable page layout with a narrow icon sidebar that provides:\n * - Fixed header (80px)\n * - Fixed narrow dark icon sidebar on desktop (96px, hidden on mobile)\n * - Floating sidebar toggle button on mobile (left edge)\n * - Mobile sheet navigation for icon sidebar\n * - Hamburger menu in header for app-level menu (future)\n * - Main content area with pageHeader slot and children slot for blocks\n * \n * Uses the same styling and spacing as DashboardShell for non-sidebar content.\n * \n * @example\n * ```tsx\n * <IconSidebarShell navigation={iconNavItems}>\n * <ContentDropzone label=\"Main content area\" />\n * </IconSidebarShell>\n * ```\n */\nexport function IconSidebarShell({\n navigation = defaultIconNavItems,\n pageHeader,\n children,\n onNavItemClick,\n onAppMenuClick,\n contentClassName,\n}: IconSidebarShellProps) {\n useCSSVariableSync();\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const handleNavItemClick = (item: IconNavItemConfig) => {\n onNavItemClick?.(item);\n // Close sidebar when nav item is clicked\n setSidebarOpen(false);\n };\n\n const handleAppMenuClick = () => {\n // Placeholder for future app-level menu\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top, offset on desktop to not overlap icon sidebar */}\n <div className=\"fixed top-0 left-0 right-0 lg:left-[var(--icon-sidebar-width)] z-40\">\n <Header onMenuClick={handleAppMenuClick} />\n </div>\n\n {/* Desktop Icon Sidebar - Fixed on left, visible lg+ */}\n <div className=\"hidden lg:block fixed top-0 left-0 bottom-0 z-50 w-[var(--icon-sidebar-width)]\">\n <IconSidebar
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { IconSidebar, IconNavItemConfig, defaultIconNavItems } from \"./icon-sidebar\";\nimport { \n Sheet, \n SheetContent,\n SheetTitle,\n} from \"../ui/sheet\";\nimport { cn } from \"../../lib/utils\";\nimport { useThemeBranding } from \"../../context/theme-context\";\nimport * as VisuallyHidden from \"@radix-ui/react-visually-hidden\";\n\ninterface IconSidebarShellProps {\n /** Navigation items for the icon sidebar */\n navigation?: IconNavItemConfig[];\n /** Optional page header content (e.g., breadcrumbs, page title) */\n pageHeader?: React.ReactNode;\n /** Main content - the modular blocks */\n children: React.ReactNode;\n /** Callback when a nav item is clicked */\n onNavItemClick?: (item: IconNavItemConfig) => void;\n /** Callback when app menu (hamburger) is clicked - for future app-level menu */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Icon Sidebar Shell\n * \n * A composable page layout with a narrow icon sidebar that provides:\n * - Fixed header (80px)\n * - Fixed narrow dark icon sidebar on desktop (96px, hidden on mobile)\n * - Floating sidebar toggle button on mobile (left edge)\n * - Mobile sheet navigation for icon sidebar\n * - Hamburger menu in header for app-level menu (future)\n * - Main content area with pageHeader slot and children slot for blocks\n * \n * Uses the same styling and spacing as DashboardShell for non-sidebar content.\n * \n * @example\n * ```tsx\n * <IconSidebarShell navigation={iconNavItems}>\n * <ContentDropzone label=\"Main content area\" />\n * </IconSidebarShell>\n * ```\n */\nexport function IconSidebarShell({\n navigation = defaultIconNavItems,\n pageHeader,\n children,\n onNavItemClick,\n onAppMenuClick,\n contentClassName,\n}: IconSidebarShellProps) {\n useCSSVariableSync();\n const { branding } = useThemeBranding();\n const sidebarVariant = branding.sidebarMode ?? \"dark\";\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const handleNavItemClick = (item: IconNavItemConfig) => {\n onNavItemClick?.(item);\n // Close sidebar when nav item is clicked\n setSidebarOpen(false);\n };\n\n const handleAppMenuClick = () => {\n // Placeholder for future app-level menu\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top, offset on desktop to not overlap icon sidebar */}\n <div className=\"fixed top-0 left-0 right-0 lg:left-[var(--icon-sidebar-width)] z-40\">\n <Header onMenuClick={handleAppMenuClick} />\n </div>\n\n {/* Desktop Icon Sidebar - Fixed on left, visible lg+ */}\n <div className=\"hidden lg:block fixed top-0 left-0 bottom-0 z-50 w-[var(--icon-sidebar-width)]\">\n <IconSidebar\n items={navigation}\n variant={sidebarVariant}\n onItemClick={handleNavItemClick}\n />\n </div>\n\n {/* Mobile Sidebar Toggle Button - Floating on left edge */}\n <button\n onClick={() => setSidebarOpen(true)}\n className={cn(\n \"lg:hidden fixed left-0 z-30\",\n \"top-[calc(var(--header-height)+4px)]\",\n \"flex items-center justify-center\",\n \"size-11\",\n \"bg-[var(--canvas-background)]\",\n \"border border-l-0 border-[var(--canvas-neutral-border)]\",\n \"rounded-r-[var(--radius-xs)]\",\n \"shadow-[0px_4px_16px_0px_rgba(0,0,0,0.04)]\",\n \"transition-opacity hover:opacity-80\"\n )}\n aria-label=\"Open sidebar\"\n >\n <ChevronRight className=\"size-6 text-[var(--canvas-primary)]\" />\n </button>\n\n {/* Mobile Icon Sidebar Sheet */}\n <Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>\n <SheetContent side=\"left\" className=\"p-0 w-[var(--icon-sidebar-width)]\">\n <VisuallyHidden.Root>\n <SheetTitle>Navigation</SheetTitle>\n </VisuallyHidden.Root>\n <IconSidebar\n items={navigation}\n variant={sidebarVariant}\n onItemClick={handleNavItemClick}\n />\n </SheetContent>\n </Sheet>\n\n {/* Main Content Area - Same styling as DashboardShell */}\n <main\n className={cn(\n \"pt-[var(--header-height)]\",\n \"lg:pl-[var(--icon-sidebar-width)]\",\n \"min-h-screen\"\n )}\n >\n <div \n className={cn(\n \"flex flex-col gap-[var(--spacing-xl)]\",\n \"px-[var(--spacing-xl)] lg:px-[var(--spacing-5xl)]\",\n \"pt-10 pb-[var(--spacing-5xl)]\",\n contentClassName\n )}\n >\n {/* Page Header Slot */}\n {pageHeader && (\n <section className=\"pt-0\">\n {pageHeader}\n </section>\n )}\n\n {/* Main Content Slot - Blocks go here */}\n <section className=\"flex flex-col gap-[var(--spacing-4xl)]\">\n {children}\n </section>\n </div>\n </main>\n </div>\n );\n}\n\n// Re-export types for convenience\nexport type { IconNavItemConfig } from \"./icon-sidebar\";\n\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/mobile-menu-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { MobileBottomNav, MobileNavTabConfig, defaultMobileNavTabs } from \"../blocks/mobile-bottom-nav\";\n\ninterface MobileMenuShellProps {\n /** Navigation tabs for the bottom nav */\n tabs?: MobileNavTabConfig[];\n /** Visual variant for the bottom nav - dark or light theme */\n variant?: \"dark\" | \"light\";\n /** ID of the currently active tab */\n activeTab?: string;\n /** Callback when a tab is clicked */\n onTabChange?: (tabId: string) => void;\n /** Main content */\n children: React.ReactNode;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Mobile Menu Shell\n * \n * A layout with:\n * - Fixed header with logo (no sidebar)\n * - Main scrollable content area\n * - Sticky bottom navigation bar (supports dark/light themes)\n * \n * @example\n * ```tsx\n * <MobileMenuShell variant=\"light\">\n * <ContentDropzone />\n * </MobileMenuShell>\n * ```\n */\nexport function MobileMenuShell({\n tabs = defaultMobileNavTabs,\n variant
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { useThemeBranding } from \"../../context/theme-context\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { MobileBottomNav, MobileNavTabConfig, defaultMobileNavTabs } from \"../blocks/mobile-bottom-nav\";\n\ninterface MobileMenuShellProps {\n /** Navigation tabs for the bottom nav */\n tabs?: MobileNavTabConfig[];\n /** Visual variant for the bottom nav - dark or light theme */\n variant?: \"dark\" | \"light\";\n /** ID of the currently active tab */\n activeTab?: string;\n /** Callback when a tab is clicked */\n onTabChange?: (tabId: string) => void;\n /** Main content */\n children: React.ReactNode;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Mobile Menu Shell\n * \n * A layout with:\n * - Fixed header with logo (no sidebar)\n * - Main scrollable content area\n * - Sticky bottom navigation bar (supports dark/light themes)\n * \n * @example\n * ```tsx\n * <MobileMenuShell variant=\"light\">\n * <ContentDropzone />\n * </MobileMenuShell>\n * ```\n */\nexport function MobileMenuShell({\n tabs = defaultMobileNavTabs,\n variant,\n activeTab,\n onTabChange,\n children,\n contentClassName,\n}: MobileMenuShellProps) {\n useCSSVariableSync();\n const { branding } = useThemeBranding();\n const effectiveVariant = variant ?? branding.sidebarMode ?? \"dark\";\n // Internal state for active tab if not controlled\n const [internalActiveTab, setInternalActiveTab] = useState(\n activeTab || tabs[0]?.id || \"home\"\n );\n\n const currentActiveTab = activeTab || internalActiveTab;\n\n // Apply active state to tabs\n const tabsWithActiveState = tabs.map((tab) => ({\n ...tab,\n isActive: tab.id === currentActiveTab,\n }));\n\n const handleTabClick = (tab: MobileNavTabConfig) => {\n if (onTabChange) {\n onTabChange(tab.id);\n } else {\n setInternalActiveTab(tab.id);\n }\n };\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top with logo visible (no sidebar) */}\n <header className=\"sticky top-0 z-40\">\n <Header showDesktopLogo />\n </header>\n\n {/* Main Content Area */}\n <main className=\"w-full\">\n <div\n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-[204px]\",\n \"py-[var(--spacing-6xl)]\",\n // Add bottom padding to account for fixed bottom nav (88px)\n \"pb-28\",\n contentClassName\n )}\n >\n {children}\n </div>\n </main>\n\n {/* Sticky Bottom Navigation */}\n <MobileBottomNav\n tabs={tabsWithActiveState}\n variant={effectiveVariant}\n onTabClick={handleTabClick}\n />\n </div>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|