canvas-ui-sdk 4.0.2 → 4.0.3
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/README.md +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +38 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/registry/blocks/category-grid.json +1 -1
- package/registry/blocks/confirmation-popup.json +1 -1
- package/registry/blocks/contact-form-popup.json +1 -1
- package/registry/blocks/details-popup.json +1 -1
- package/registry/blocks/feedback-popup.json +1 -1
- package/registry/blocks/form-popup.json +1 -1
- package/registry/blocks/image-popup.json +1 -1
- package/registry/blocks/invoice-popup.json +1 -1
- package/registry/blocks/list-popup.json +1 -1
- package/registry/blocks/multistep-form-popup.json +1 -1
- package/registry/blocks/nps-survey-popup.json +1 -1
- package/registry/blocks/page-previews.json +1 -1
- package/registry/blocks/persona-card.json +1 -1
- package/registry/blocks/personalize-feed-popup.json +1 -1
- package/registry/blocks/pricing-plans-popup.json +1 -1
- package/registry/blocks/purchase-confirmation-popup.json +1 -1
- package/registry/blocks/share-project-popup.json +1 -1
- package/registry/blocks/small-edit-popup.json +1 -1
- package/registry/blocks/terms-of-service-popup.json +1 -1
- package/registry/blocks/video-playlist.json +1 -1
- package/registry/blocks/video-popup.json +1 -1
- package/registry/blocks/view-profile-popup.json +1 -1
- package/registry/layout/dashboard-shell.json +1 -1
- package/registry/layout/double-sidebar-shell.json +1 -1
- package/registry/layout/double-sidebar.json +1 -1
- package/registry/layout/icon-sidebar-shell.json +1 -1
- package/registry/ui/dropdown-menu.json +1 -1
- package/registry/ui/popover.json +1 -1
- package/registry/ui/select.json +1 -1
- package/styles/tokens.reference.css +7 -0
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
{
|
|
16
16
|
"path": "components/blocks/view-profile-popup.tsx",
|
|
17
17
|
"type": "registry:block",
|
|
18
|
-
"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"
|
|
18
|
+
"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-[var(--canvas-shadow-modal)]\",\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"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"dependencies": [
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
{
|
|
18
18
|
"path": "components/layout/dashboard-shell.tsx",
|
|
19
19
|
"type": "registry:layout",
|
|
20
|
-
"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-[
|
|
20
|
+
"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-[var(--canvas-shadow-nav)]\",\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"
|
|
21
21
|
}
|
|
22
22
|
],
|
|
23
23
|
"dependencies": [
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
{
|
|
16
16
|
"path": "components/layout/double-sidebar-shell.tsx",
|
|
17
17
|
"type": "registry:layout",
|
|
18
|
-
"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-[
|
|
18
|
+
"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-[var(--canvas-shadow-nav)]\",\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"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"dependencies": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
{
|
|
15
15
|
"path": "components/layout/double-sidebar.tsx",
|
|
16
16
|
"type": "registry:layout",
|
|
17
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, Users, Calendar, MessageSquare, PieChart, FileText, ShoppingBag } from \"lucide-react\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\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// ============================================\n// Icon Shape Presets for Logo Creator\n// ============================================\n\ntype IconShapeId = \"rounded\" | \"circle\" | \"square\";\n\ninterface IconShape {\n id: IconShapeId;\n renderBackground: (bgColor: string) => React.ReactNode;\n}\n\nconst iconShapes: IconShape[] = [\n {\n id: \"rounded\",\n renderBackground: (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 },\n {\n id: \"circle\",\n renderBackground: (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 },\n {\n id: \"square\",\n renderBackground: (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\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\n// ============================================\n// Types\n// ============================================\n\nexport interface NavTab {\n id: string;\n label: string;\n isActive?: boolean;\n}\n\nexport interface DoubleSidebarSection {\n id: string;\n icon: LucideIcon;\n label: string;\n badge?: boolean;\n tabs: NavTab[];\n}\n\n// ============================================\n// Default Sections\n// ============================================\n\nexport const defaultDoubleSidebarSections: DoubleSidebarSection[] = [\n {\n id: \"home\",\n icon: Home,\n label: \"Home\",\n tabs: [\n { id: \"tab1\", label: \"Tab 1\", isActive: true },\n { id: \"tab2\", label: \"Tab 2\" },\n { id: \"tab3\", label: \"Tab 3\" },\n { id: \"tab4\", label: \"Tab 4\" },\n { id: \"tab5\", label: \"Tab 5\" },\n { id: \"tab6\", label: \"Tab 6\" },\n ],\n },\n {\n id: \"teams\",\n icon: Users,\n label: \"Teams\",\n tabs: [\n { id: \"all-teams\", label: \"All Teams\" },\n { id: \"my-team\", label: \"My Team\" },\n { id: \"invites\", label: \"Invites\" },\n ],\n },\n {\n id: \"calendar\",\n icon: Calendar,\n label: \"Calendar\",\n tabs: [\n { id: \"schedule\", label: \"Schedule\" },\n { id: \"events\", label: \"Events\" },\n { id: \"reminders\", label: \"Reminders\" },\n ],\n },\n {\n id: \"messages\",\n icon: MessageSquare,\n label: \"Messages\",\n badge: true,\n tabs: [\n { id: \"inbox\", label: \"Inbox\" },\n { id: \"sent\", label: \"Sent\" },\n { id: \"drafts\", label: \"Drafts\" },\n { id: \"archived\", label: \"Archived\" },\n ],\n },\n {\n id: \"reports\",\n icon: PieChart,\n label: \"Reports\",\n tabs: [\n { id: \"overview\", label: \"Overview\" },\n { id: \"analytics\", label: \"Analytics\" },\n { id: \"exports\", label: \"Exports\" },\n ],\n },\n {\n id: \"docs\",\n icon: FileText,\n label: \"Docs\",\n tabs: [\n { id: \"recent\", label: \"Recent\" },\n { id: \"shared\", label: \"Shared\" },\n { id: \"favorites\", label: \"Favorites\" },\n ],\n },\n {\n id: \"orders\",\n icon: ShoppingBag,\n label: \"Orders\",\n tabs: [\n { id: \"pending\", label: \"Pending\" },\n { id: \"completed\", label: \"Completed\" },\n { id: \"cancelled\", label: \"Cancelled\" },\n ],\n },\n];\n\n// ============================================\n// Icon Column Item\n// ============================================\n\ninterface IconColumnItemProps {\n section: DoubleSidebarSection;\n isActive: boolean;\n variant: \"dark\" | \"light\";\n onClick: () => void;\n}\n\nfunction IconColumnItem({ section, isActive, variant, onClick }: IconColumnItemProps) {\n const Icon = section.icon;\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n \"cursor-pointer relative flex flex-col items-center justify-center gap-1 w-11 h-11 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n {/* Notification Badge */}\n {section.badge && (\n <div className=\"absolute top-1.5 right-1.5 size-1.5 rounded-full bg-[var(--canvas-destructive)]\" />\n )}\n </button>\n );\n}\n\n// ============================================\n// Nav Column Tab Item\n// ============================================\n\ninterface NavTabItemProps {\n tab: NavTab;\n isActive: boolean;\n variant: \"dark\" | \"light\";\n onClick: () => void;\n}\n\nfunction NavTabItem({ tab, isActive, variant, onClick }: NavTabItemProps) {\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n \"cursor-pointer flex items-center gap-[var(--spacing-md)] h-11 px-[var(--spacing-xl)] rounded-[var(--radius-nav)] w-full text-left transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)] text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)] hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)] text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)] hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <span \n className=\"flex-1 truncate\"\n style={{\n fontFamily: \"var(--typo-sidebar-tab-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-tab-size)\",\n fontWeight: \"var(--typo-sidebar-tab-weight)\",\n letterSpacing: \"var(--typo-sidebar-tab-spacing)\",\n lineHeight: \"var(--typo-sidebar-tab-line-height)\",\n }}\n >\n {tab.label}\n </span>\n </button>\n );\n}\n\n// ============================================\n// Double Sidebar Component\n// ============================================\n\ninterface DoubleSidebarProps {\n /** Navigation sections - each section maps to an icon and has tabs */\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 /** Callback when a tab is clicked */\n onTabClick?: (section: DoubleSidebarSection, tab: NavTab) => void;\n /** Callback when closing (for mobile sheet) */\n onClose?: () => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Double Sidebar Component\n * \n * A two-column sidebar where:\n * - Left column (96px): Icon buttons that switch the active section\n * - Right column (280px): Text-based navigation tabs for the active section\n * \n * Each column can be independently themed (light/dark).\n */\nexport function DoubleSidebar({\n sections = defaultDoubleSidebarSections,\n iconVariant = \"light\",\n navVariant = \"light\",\n onTabClick,\n onClose,\n className\n}: DoubleSidebarProps) {\n const [activeSectionId, setActiveSectionId] = useState(sections[0]?.id || \"\");\n const [activeTabId, setActiveTabId] = useState(sections[0]?.tabs[0]?.id || \"\");\n \n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n\n const activeSection = sections.find(s => s.id === activeSectionId);\n const isIconDark = iconVariant === \"dark\";\n const isNavDark = navVariant === \"dark\";\n\n // Get the appropriate logo based on icon column variant\n const logoUrl = isIconDark ? themeImages.logoDark : themeImages.logoLight;\n\n // Get the icon shape renderer\n const iconShape = iconShapes.find(s => s.id === branding.iconShape) || iconShapes[0];\n\n const handleSectionClick = (section: DoubleSidebarSection) => {\n setActiveSectionId(section.id);\n // Set first tab of the section as active\n if (section.tabs.length > 0) {\n setActiveTabId(section.tabs[0].id);\n }\n };\n\n const handleTabClick = (tab: NavTab) => {\n setActiveTabId(tab.id);\n if (activeSection) {\n onTabClick?.(activeSection, tab);\n onClose?.();\n }\n };\n\n return (\n <div className={cn(\"flex h-full\", className)}>\n {/* Icon Column (96px) */}\n <div\n className={cn(\n \"flex flex-col items-center w-[var(--icon-sidebar-width)] shrink-0\",\n isIconDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isIconDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)]\"\n )}\n >\n {/* Logo - Hidden until mounted to prevent hydration flash */}\n <div className={`flex items-center justify-center shrink-0 py-5 ${isMounted ? 'opacity-100' : 'opacity-0'}`}>\n {logoUrl ? (\n <img\n src={logoUrl}\n alt=\"Logo\"\n className=\"size-8 object-contain\"\n />\n ) : (\n // Uses CSS variables directly - no JavaScript resolution needed\n <div className=\"relative size-8 shrink-0\">\n {iconShape.renderBackground(branding.bgColor || \"var(--canvas-primary)\")}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n {(() => {\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n return <IconComponent weight=\"bold\" size={18} color={branding.iconColor || \"var(--canvas-primary-foreground)\"} />;\n })()}\n </div>\n </div>\n )}\n </div>\n\n {/* Icon Navigation */}\n <nav className=\"flex flex-col items-center gap-1 flex-1 px-4 pb-5 mt-2\">\n {sections.map((section) => (\n <IconColumnItem\n key={section.id}\n section={section}\n isActive={section.id === activeSectionId}\n variant={iconVariant}\n onClick={() => handleSectionClick(section)}\n />\n ))}\n </nav>\n </div>\n\n {/* Navigation Column (280px) */}\n <div\n className={cn(\n \"flex flex-col w-[var(--nav-sidebar-width)]\",\n isNavDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isNavDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)] shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]\"\n )}\n >\n {/* Navigation Content */}\n <ScrollArea className=\"flex-1 pt-[var(--header-height)] px-[var(--spacing-2xl)] pb-[var(--spacing-5xl)]\">\n <nav className=\"flex flex-col gap-0\">\n {activeSection?.tabs.map((tab) => (\n <NavTabItem\n key={tab.id}\n tab={tab}\n isActive={tab.id === activeTabId}\n variant={navVariant}\n onClick={() => handleTabClick(tab)}\n />\n ))}\n </nav>\n </ScrollArea>\n </div>\n </div>\n );\n}\n\n"
|
|
17
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, Users, Calendar, MessageSquare, PieChart, FileText, ShoppingBag } from \"lucide-react\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\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// ============================================\n// Icon Shape Presets for Logo Creator\n// ============================================\n\ntype IconShapeId = \"rounded\" | \"circle\" | \"square\";\n\ninterface IconShape {\n id: IconShapeId;\n renderBackground: (bgColor: string) => React.ReactNode;\n}\n\nconst iconShapes: IconShape[] = [\n {\n id: \"rounded\",\n renderBackground: (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 },\n {\n id: \"circle\",\n renderBackground: (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 },\n {\n id: \"square\",\n renderBackground: (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\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\n// ============================================\n// Types\n// ============================================\n\nexport interface NavTab {\n id: string;\n label: string;\n isActive?: boolean;\n}\n\nexport interface DoubleSidebarSection {\n id: string;\n icon: LucideIcon;\n label: string;\n badge?: boolean;\n tabs: NavTab[];\n}\n\n// ============================================\n// Default Sections\n// ============================================\n\nexport const defaultDoubleSidebarSections: DoubleSidebarSection[] = [\n {\n id: \"home\",\n icon: Home,\n label: \"Home\",\n tabs: [\n { id: \"tab1\", label: \"Tab 1\", isActive: true },\n { id: \"tab2\", label: \"Tab 2\" },\n { id: \"tab3\", label: \"Tab 3\" },\n { id: \"tab4\", label: \"Tab 4\" },\n { id: \"tab5\", label: \"Tab 5\" },\n { id: \"tab6\", label: \"Tab 6\" },\n ],\n },\n {\n id: \"teams\",\n icon: Users,\n label: \"Teams\",\n tabs: [\n { id: \"all-teams\", label: \"All Teams\" },\n { id: \"my-team\", label: \"My Team\" },\n { id: \"invites\", label: \"Invites\" },\n ],\n },\n {\n id: \"calendar\",\n icon: Calendar,\n label: \"Calendar\",\n tabs: [\n { id: \"schedule\", label: \"Schedule\" },\n { id: \"events\", label: \"Events\" },\n { id: \"reminders\", label: \"Reminders\" },\n ],\n },\n {\n id: \"messages\",\n icon: MessageSquare,\n label: \"Messages\",\n badge: true,\n tabs: [\n { id: \"inbox\", label: \"Inbox\" },\n { id: \"sent\", label: \"Sent\" },\n { id: \"drafts\", label: \"Drafts\" },\n { id: \"archived\", label: \"Archived\" },\n ],\n },\n {\n id: \"reports\",\n icon: PieChart,\n label: \"Reports\",\n tabs: [\n { id: \"overview\", label: \"Overview\" },\n { id: \"analytics\", label: \"Analytics\" },\n { id: \"exports\", label: \"Exports\" },\n ],\n },\n {\n id: \"docs\",\n icon: FileText,\n label: \"Docs\",\n tabs: [\n { id: \"recent\", label: \"Recent\" },\n { id: \"shared\", label: \"Shared\" },\n { id: \"favorites\", label: \"Favorites\" },\n ],\n },\n {\n id: \"orders\",\n icon: ShoppingBag,\n label: \"Orders\",\n tabs: [\n { id: \"pending\", label: \"Pending\" },\n { id: \"completed\", label: \"Completed\" },\n { id: \"cancelled\", label: \"Cancelled\" },\n ],\n },\n];\n\n// ============================================\n// Icon Column Item\n// ============================================\n\ninterface IconColumnItemProps {\n section: DoubleSidebarSection;\n isActive: boolean;\n variant: \"dark\" | \"light\";\n onClick: () => void;\n}\n\nfunction IconColumnItem({ section, isActive, variant, onClick }: IconColumnItemProps) {\n const Icon = section.icon;\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n \"cursor-pointer relative flex flex-col items-center justify-center gap-1 w-11 h-11 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n {/* Notification Badge */}\n {section.badge && (\n <div className=\"absolute top-1.5 right-1.5 size-1.5 rounded-full bg-[var(--canvas-destructive)]\" />\n )}\n </button>\n );\n}\n\n// ============================================\n// Nav Column Tab Item\n// ============================================\n\ninterface NavTabItemProps {\n tab: NavTab;\n isActive: boolean;\n variant: \"dark\" | \"light\";\n onClick: () => void;\n}\n\nfunction NavTabItem({ tab, isActive, variant, onClick }: NavTabItemProps) {\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n \"cursor-pointer flex items-center gap-[var(--spacing-md)] h-11 px-[var(--spacing-xl)] rounded-[var(--radius-nav)] w-full text-left transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)] text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)] hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)] text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)] hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <span \n className=\"flex-1 truncate\"\n style={{\n fontFamily: \"var(--typo-sidebar-tab-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-tab-size)\",\n fontWeight: \"var(--typo-sidebar-tab-weight)\",\n letterSpacing: \"var(--typo-sidebar-tab-spacing)\",\n lineHeight: \"var(--typo-sidebar-tab-line-height)\",\n }}\n >\n {tab.label}\n </span>\n </button>\n );\n}\n\n// ============================================\n// Double Sidebar Component\n// ============================================\n\ninterface DoubleSidebarProps {\n /** Navigation sections - each section maps to an icon and has tabs */\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 /** Callback when a tab is clicked */\n onTabClick?: (section: DoubleSidebarSection, tab: NavTab) => void;\n /** Callback when closing (for mobile sheet) */\n onClose?: () => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Double Sidebar Component\n * \n * A two-column sidebar where:\n * - Left column (96px): Icon buttons that switch the active section\n * - Right column (280px): Text-based navigation tabs for the active section\n * \n * Each column can be independently themed (light/dark).\n */\nexport function DoubleSidebar({\n sections = defaultDoubleSidebarSections,\n iconVariant = \"light\",\n navVariant = \"light\",\n onTabClick,\n onClose,\n className\n}: DoubleSidebarProps) {\n const [activeSectionId, setActiveSectionId] = useState(sections[0]?.id || \"\");\n const [activeTabId, setActiveTabId] = useState(sections[0]?.tabs[0]?.id || \"\");\n \n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n\n const activeSection = sections.find(s => s.id === activeSectionId);\n const isIconDark = iconVariant === \"dark\";\n const isNavDark = navVariant === \"dark\";\n\n // Get the appropriate logo based on icon column variant\n const logoUrl = isIconDark ? themeImages.logoDark : themeImages.logoLight;\n\n // Get the icon shape renderer\n const iconShape = iconShapes.find(s => s.id === branding.iconShape) || iconShapes[0];\n\n const handleSectionClick = (section: DoubleSidebarSection) => {\n setActiveSectionId(section.id);\n // Set first tab of the section as active\n if (section.tabs.length > 0) {\n setActiveTabId(section.tabs[0].id);\n }\n };\n\n const handleTabClick = (tab: NavTab) => {\n setActiveTabId(tab.id);\n if (activeSection) {\n onTabClick?.(activeSection, tab);\n onClose?.();\n }\n };\n\n return (\n <div className={cn(\"flex h-full\", className)}>\n {/* Icon Column (96px) */}\n <div\n className={cn(\n \"flex flex-col items-center w-[var(--icon-sidebar-width)] shrink-0\",\n isIconDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isIconDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)]\"\n )}\n >\n {/* Logo - Hidden until mounted to prevent hydration flash */}\n <div className={`flex items-center justify-center shrink-0 py-5 ${isMounted ? 'opacity-100' : 'opacity-0'}`}>\n {logoUrl ? (\n <img\n src={logoUrl}\n alt=\"Logo\"\n className=\"size-8 object-contain\"\n />\n ) : (\n // Uses CSS variables directly - no JavaScript resolution needed\n <div className=\"relative size-8 shrink-0\">\n {iconShape.renderBackground(branding.bgColor || \"var(--canvas-primary)\")}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n {(() => {\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n return <IconComponent weight=\"bold\" size={18} color={branding.iconColor || \"var(--canvas-primary-foreground)\"} />;\n })()}\n </div>\n </div>\n )}\n </div>\n\n {/* Icon Navigation */}\n <nav className=\"flex flex-col items-center gap-1 flex-1 px-4 pb-5 mt-2\">\n {sections.map((section) => (\n <IconColumnItem\n key={section.id}\n section={section}\n isActive={section.id === activeSectionId}\n variant={iconVariant}\n onClick={() => handleSectionClick(section)}\n />\n ))}\n </nav>\n </div>\n\n {/* Navigation Column (280px) */}\n <div\n className={cn(\n \"flex flex-col w-[var(--nav-sidebar-width)]\",\n isNavDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isNavDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)] shadow-[var(--canvas-shadow-nav)]\"\n )}\n >\n {/* Navigation Content */}\n <ScrollArea className=\"flex-1 pt-[var(--header-height)] px-[var(--spacing-2xl)] pb-[var(--spacing-5xl)]\">\n <nav className=\"flex flex-col gap-0\">\n {activeSection?.tabs.map((tab) => (\n <NavTabItem\n key={tab.id}\n tab={tab}\n isActive={tab.id === activeTabId}\n variant={navVariant}\n onClick={() => handleTabClick(tab)}\n />\n ))}\n </nav>\n </ScrollArea>\n </div>\n </div>\n );\n}\n\n"
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
20
|
"dependencies": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
{
|
|
15
15
|
"path": "components/layout/icon-sidebar-shell.tsx",
|
|
16
16
|
"type": "registry:layout",
|
|
17
|
-
"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-[
|
|
17
|
+
"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-[var(--canvas-shadow-nav)]\",\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"
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
20
|
"dependencies": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"path": "components/ui/dropdown-menu.tsx",
|
|
15
15
|
"type": "registry:ui",
|
|
16
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\"\n\nimport { cn } from \"../../lib/utils\"\n\nfunction DropdownMenu({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />\n}\n\nfunction DropdownMenuPortal({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n return (\n <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n )\n}\n\nfunction DropdownMenuTrigger({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n return (\n <DropdownMenuPrimitive.Trigger\n data-slot=\"dropdown-menu-trigger\"\n {...props}\n />\n )\n}\n\nfunction DropdownMenuContent({\n className,\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n data-slot=\"dropdown-menu-content\"\n sideOffset={sideOffset}\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-[var(--canvas-border)] p-1 shadow-
|
|
16
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\"\n\nimport { cn } from \"../../lib/utils\"\n\nfunction DropdownMenu({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />\n}\n\nfunction DropdownMenuPortal({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n return (\n <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n )\n}\n\nfunction DropdownMenuTrigger({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n return (\n <DropdownMenuPrimitive.Trigger\n data-slot=\"dropdown-menu-trigger\"\n {...props}\n />\n )\n}\n\nfunction DropdownMenuContent({\n className,\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n data-slot=\"dropdown-menu-content\"\n sideOffset={sideOffset}\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-[var(--canvas-border)] p-1 shadow-[var(--canvas-shadow-dropdown)] font-[family-name:var(--typo-global-font)]\",\n className\n )}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n )\n}\n\nfunction DropdownMenuGroup({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n return (\n <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n )\n}\n\nfunction DropdownMenuItem({\n className,\n inset,\n variant = \"default\",\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n inset?: boolean\n variant?: \"default\" | \"destructive\"\n}) {\n return (\n <DropdownMenuPrimitive.Item\n data-slot=\"dropdown-menu-item\"\n data-inset={inset}\n data-variant={variant}\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground focus:[&_svg:not([class*='text-'])]:text-accent-foreground relative flex cursor-pointer items-center gap-1.5 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuCheckboxItem({\n className,\n children,\n checked,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n return (\n <DropdownMenuPrimitive.CheckboxItem\n data-slot=\"dropdown-menu-checkbox-item\"\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n checked={checked}\n {...props}\n >\n <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <CheckIcon className=\"size-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n )\n}\n\nfunction DropdownMenuRadioGroup({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n return (\n <DropdownMenuPrimitive.RadioGroup\n data-slot=\"dropdown-menu-radio-group\"\n {...props}\n />\n )\n}\n\nfunction DropdownMenuRadioItem({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n return (\n <DropdownMenuPrimitive.RadioItem\n data-slot=\"dropdown-menu-radio-item\"\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n >\n <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <CircleIcon className=\"size-2 fill-current\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.RadioItem>\n )\n}\n\nfunction DropdownMenuLabel({\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n inset?: boolean\n}) {\n return (\n <DropdownMenuPrimitive.Label\n data-slot=\"dropdown-menu-label\"\n data-inset={inset}\n className={cn(\n \"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n return (\n <DropdownMenuPrimitive.Separator\n data-slot=\"dropdown-menu-separator\"\n className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuShortcut({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"dropdown-menu-shortcut\"\n className={cn(\n \"text-muted-foreground ml-auto text-xs tracking-widest\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuSub({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />\n}\n\nfunction DropdownMenuSubTrigger({\n className,\n inset,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n inset?: boolean\n}) {\n return (\n <DropdownMenuPrimitive.SubTrigger\n data-slot=\"dropdown-menu-sub-trigger\"\n data-inset={inset}\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronRightIcon className=\"ml-auto size-4\" />\n </DropdownMenuPrimitive.SubTrigger>\n )\n}\n\nfunction DropdownMenuSubContent({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n return (\n <DropdownMenuPrimitive.SubContent\n data-slot=\"dropdown-menu-sub-content\"\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border border-[var(--canvas-border)] p-1 shadow-lg font-[family-name:var(--typo-global-font)]\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n DropdownMenu,\n DropdownMenuPortal,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuLabel,\n DropdownMenuItem,\n DropdownMenuCheckboxItem,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubTrigger,\n DropdownMenuSubContent,\n}\n"
|
|
17
17
|
}
|
|
18
18
|
],
|
|
19
19
|
"dependencies": [
|
package/registry/ui/popover.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"path": "components/ui/popover.tsx",
|
|
15
15
|
"type": "registry:ui",
|
|
16
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } from \"../../lib/utils\"\n\nfunction Popover({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />\n}\n\nfunction PopoverTrigger({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />\n}\n\nfunction PopoverContent({\n className,\n align = \"center\",\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n data-slot=\"popover-content\"\n align={align}\n sideOffset={sideOffset}\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border border-[var(--canvas-border)] p-4 shadow-
|
|
16
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } from \"../../lib/utils\"\n\nfunction Popover({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />\n}\n\nfunction PopoverTrigger({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />\n}\n\nfunction PopoverContent({\n className,\n align = \"center\",\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n data-slot=\"popover-content\"\n align={align}\n sideOffset={sideOffset}\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border border-[var(--canvas-border)] p-4 shadow-[var(--canvas-shadow-dropdown)] outline-hidden font-[family-name:var(--typo-global-font)]\",\n className\n )}\n {...props}\n />\n </PopoverPrimitive.Portal>\n )\n}\n\nfunction PopoverAnchor({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />\n}\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }\n"
|
|
17
17
|
}
|
|
18
18
|
],
|
|
19
19
|
"dependencies": [
|
package/registry/ui/select.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
{
|
|
15
15
|
"path": "components/ui/select.tsx",
|
|
16
16
|
"type": "registry:ui",
|
|
17
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from \"lucide-react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"../../lib/utils\"\n\nconst selectTriggerVariants = cva(\n // Base styles using CSS variables\n \"cursor-pointer flex w-full items-center justify-between gap-2 bg-[var(--canvas-background)] border border-[var(--canvas-border-input)] text-[var(--canvas-text)] whitespace-nowrap transition-colors outline-none focus:border-[var(--canvas-border-input-focus)] focus:ring-2 focus:ring-[var(--canvas-border-input-focus)] disabled:cursor-not-allowed disabled:bg-[var(--canvas-input-disabled-bg)] disabled:border-[var(--canvas-input-disabled-border)] disabled:text-[var(--canvas-input-disabled-text)] aria-invalid:border-[var(--canvas-border-input-invalid)] data-[placeholder]:text-[var(--canvas-text-placeholder)] [&_svg:not([class*='text-'])]:text-[var(--canvas-text-muted)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 font-[family-name:var(--typo-global-font)]\",\n {\n variants: {\n inputSize: {\n sm: \"h-[var(--input-small-height)] px-[var(--input-small-px)] text-[length:var(--input-small-font-size)] rounded-[var(--input-small-radius)]\",\n default: \"h-[var(--input-standard-height)] px-[var(--input-standard-px)] text-[length:var(--input-standard-font-size)] rounded-[var(--input-standard-radius)]\",\n lg: \"h-[var(--input-large-height)] px-[var(--input-large-px)] text-[length:var(--input-large-font-size)] rounded-[var(--input-large-radius)]\",\n },\n },\n defaultVariants: {\n inputSize: \"default\",\n },\n }\n)\n\nfunction Select({\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Root>) {\n return <SelectPrimitive.Root data-slot=\"select\" {...props} />\n}\n\nfunction SelectGroup({\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Group>) {\n return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />\n}\n\nfunction SelectValue({\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Value>) {\n return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />\n}\n\nfunction SelectTrigger({\n className,\n inputSize,\n children,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> &\n VariantProps<typeof selectTriggerVariants>) {\n return (\n <SelectPrimitive.Trigger\n data-slot=\"select-trigger\"\n data-size={inputSize}\n className={cn(selectTriggerVariants({ inputSize, className }))}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <ChevronDownIcon className=\"size-4 opacity-50\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n )\n}\n\nfunction SelectContent({\n className,\n children,\n position = \"popper\",\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n data-slot=\"select-content\"\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-[var(--canvas-border)] shadow-
|
|
17
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from \"lucide-react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"../../lib/utils\"\n\nconst selectTriggerVariants = cva(\n // Base styles using CSS variables\n \"cursor-pointer flex w-full items-center justify-between gap-2 bg-[var(--canvas-background)] border border-[var(--canvas-border-input)] text-[var(--canvas-text)] whitespace-nowrap transition-colors outline-none focus:border-[var(--canvas-border-input-focus)] focus:ring-2 focus:ring-[var(--canvas-border-input-focus)] disabled:cursor-not-allowed disabled:bg-[var(--canvas-input-disabled-bg)] disabled:border-[var(--canvas-input-disabled-border)] disabled:text-[var(--canvas-input-disabled-text)] aria-invalid:border-[var(--canvas-border-input-invalid)] data-[placeholder]:text-[var(--canvas-text-placeholder)] [&_svg:not([class*='text-'])]:text-[var(--canvas-text-muted)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 font-[family-name:var(--typo-global-font)]\",\n {\n variants: {\n inputSize: {\n sm: \"h-[var(--input-small-height)] px-[var(--input-small-px)] text-[length:var(--input-small-font-size)] rounded-[var(--input-small-radius)]\",\n default: \"h-[var(--input-standard-height)] px-[var(--input-standard-px)] text-[length:var(--input-standard-font-size)] rounded-[var(--input-standard-radius)]\",\n lg: \"h-[var(--input-large-height)] px-[var(--input-large-px)] text-[length:var(--input-large-font-size)] rounded-[var(--input-large-radius)]\",\n },\n },\n defaultVariants: {\n inputSize: \"default\",\n },\n }\n)\n\nfunction Select({\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Root>) {\n return <SelectPrimitive.Root data-slot=\"select\" {...props} />\n}\n\nfunction SelectGroup({\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Group>) {\n return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />\n}\n\nfunction SelectValue({\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Value>) {\n return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />\n}\n\nfunction SelectTrigger({\n className,\n inputSize,\n children,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> &\n VariantProps<typeof selectTriggerVariants>) {\n return (\n <SelectPrimitive.Trigger\n data-slot=\"select-trigger\"\n data-size={inputSize}\n className={cn(selectTriggerVariants({ inputSize, className }))}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <ChevronDownIcon className=\"size-4 opacity-50\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n )\n}\n\nfunction SelectContent({\n className,\n children,\n position = \"popper\",\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n data-slot=\"select-content\"\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-[var(--canvas-border)] shadow-[var(--canvas-shadow-dropdown)] font-[family-name:var(--typo-global-font)]\",\n position === \"popper\" &&\n \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n className\n )}\n position={position}\n sideOffset={sideOffset}\n {...props}\n >\n <SelectScrollUpButton />\n <SelectPrimitive.Viewport\n className={cn(\n \"p-1\",\n position === \"popper\" &&\n \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1\"\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n <SelectScrollDownButton />\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n )\n}\n\nfunction SelectLabel({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Label>) {\n return (\n <SelectPrimitive.Label\n data-slot=\"select-label\"\n className={cn(\"text-muted-foreground px-2 py-1.5 text-xs\", className)}\n {...props}\n />\n )\n}\n\nfunction SelectItem({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Item>) {\n return (\n <SelectPrimitive.Item\n data-slot=\"select-item\"\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground data-[state=checked]:text-[var(--canvas-primary)] [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 font-[family-name:var(--typo-global-font)]\",\n className\n )}\n {...props}\n >\n <span\n data-slot=\"select-item-indicator\"\n className=\"absolute right-2 flex size-3.5 items-center justify-center\"\n >\n <SelectPrimitive.ItemIndicator>\n <CheckIcon className=\"size-4\" />\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n )\n}\n\nfunction SelectSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n return (\n <SelectPrimitive.Separator\n data-slot=\"select-separator\"\n className={cn(\"bg-border pointer-events-none -mx-1 my-1 h-px\", className)}\n {...props}\n />\n )\n}\n\nfunction SelectScrollUpButton({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {\n return (\n <SelectPrimitive.ScrollUpButton\n data-slot=\"select-scroll-up-button\"\n className={cn(\n \"flex cursor-default items-center justify-center py-1\",\n className\n )}\n {...props}\n >\n <ChevronUpIcon className=\"size-4\" />\n </SelectPrimitive.ScrollUpButton>\n )\n}\n\nfunction SelectScrollDownButton({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {\n return (\n <SelectPrimitive.ScrollDownButton\n data-slot=\"select-scroll-down-button\"\n className={cn(\n \"flex cursor-default items-center justify-center py-1\",\n className\n )}\n {...props}\n >\n <ChevronDownIcon className=\"size-4\" />\n </SelectPrimitive.ScrollDownButton>\n )\n}\n\nexport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectScrollDownButton,\n SelectScrollUpButton,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n selectTriggerVariants,\n}\n"
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
20
|
"dependencies": [
|
|
@@ -97,6 +97,13 @@
|
|
|
97
97
|
--radius-nav: 8px;
|
|
98
98
|
--radius-full: 9999px;
|
|
99
99
|
|
|
100
|
+
/* Shadows */
|
|
101
|
+
--canvas-shadow-card: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
|
|
102
|
+
--canvas-shadow-dropdown: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
|
|
103
|
+
--canvas-shadow-modal: 0px 4px 24px 0px rgba(0,0,0,0.1);
|
|
104
|
+
--canvas-shadow-nav: 0px 4px 16px 0px rgba(0,0,0,0.04);
|
|
105
|
+
--canvas-shadow-button: none;
|
|
106
|
+
|
|
100
107
|
/* Neutral palette */
|
|
101
108
|
--canvas-neutral-border: #e9eef3;
|
|
102
109
|
--canvas-neutral-surface: #f8fafc;
|