canvas-ui-sdk 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +326 -277
- package/dist/index.js.map +1 -1
- package/mcp/dist/index.js +14 -2
- package/package.json +1 -1
- package/registry/blocks/canvas-item.json +1 -1
- package/registry/blocks/chat-message.json +1 -1
- package/registry/blocks/component-palette.json +1 -1
- package/registry/blocks/component-search.json +1 -1
- package/registry/blocks/content-dropzone.json +1 -1
- package/registry/blocks/credit-card-display.json +1 -1
- package/registry/blocks/custom-component-helper.json +1 -1
- package/registry/blocks/empty-state.json +1 -1
- package/registry/blocks/faqs-table.json +1 -1
- package/registry/blocks/filter-popover.json +1 -1
- package/registry/blocks/fixed-column-data-table.json +1 -1
- package/registry/blocks/infinity-canvas.json +1 -1
- package/registry/blocks/menu-section.json +1 -1
- package/registry/blocks/messenger-sidebar.json +1 -1
- package/registry/blocks/mobile-bottom-nav.json +1 -1
- package/registry/blocks/monthly-calendar-widget.json +1 -1
- package/registry/blocks/page-header-section.json +1 -1
- package/registry/blocks/page-previews.json +1 -1
- package/registry/blocks/pagination.json +1 -1
- package/registry/blocks/persona-card.json +1 -1
- package/registry/blocks/pill-tabs.json +1 -1
- package/registry/blocks/pricing-cards.json +1 -1
- package/registry/blocks/profile-card.json +1 -1
- package/registry/blocks/profile-info-cards.json +1 -1
- package/registry/blocks/prompt-template.json +1 -1
- package/registry/blocks/screen-flowchart.json +1 -1
- package/registry/blocks/screen-prompt-builder.json +1 -1
- package/registry/blocks/screen-prompt-template.json +1 -1
- package/registry/blocks/search-bar.json +1 -1
- package/registry/blocks/sidebar-cards.json +1 -1
- package/registry/blocks/sidebar-profile-card.json +1 -1
- package/registry/blocks/slideshow-grid-tiles.json +1 -1
- package/registry/blocks/social-feed.json +1 -1
- package/registry/blocks/step-tracker.json +1 -1
- package/registry/blocks/upvoting-posts-table.json +1 -1
- package/registry/blocks/vertical-step-tracker.json +1 -1
- package/registry/blocks/video-chat-controls.json +1 -1
- package/registry/layout/account-settings-shell.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/header.json +1 -1
- package/registry/layout/icon-sidebar-shell.json +1 -1
- package/registry/layout/icon-sidebar.json +1 -1
- package/registry/layout/mobile-menu-shell.json +1 -1
- package/registry/layout/multistep-progressbar-shell.json +1 -1
- package/registry/layout/multistep-shell.json +1 -1
- package/registry/layout/multistep-sidebar-shell.json +1 -1
- package/registry/layout/project-context-shell.json +1 -1
- package/registry/layout/search-bar-shell.json +1 -1
- package/registry/layout/sidebar-nav.json +1 -1
- package/registry/layout/sidebar.json +1 -1
- package/registry/layout/standard-page-shell.json +1 -1
- package/registry/layout/vertical-multistep-shell.json +1 -1
- package/registry/ui/avatar.json +1 -1
- package/registry/ui/button.json +1 -1
- package/registry/ui/checkbox.json +1 -1
- package/registry/ui/date-input.json +1 -1
- package/registry/ui/image-uploader.json +1 -1
- package/registry/ui/line-tabs.json +1 -1
- package/registry/ui/multiselect-checkbox-field.json +1 -1
- package/registry/ui/multiselect-tags.json +1 -1
- package/registry/ui/radio-group.json +1 -1
- package/registry/ui/searchbox.json +1 -1
- package/registry/ui/select.json +1 -1
- package/registry/ui/selectable-pills.json +1 -1
- package/registry/ui/slider.json +1 -1
- package/registry/ui/switch.json +1 -1
- package/registry/ui/tabs.json +1 -1
- package/registry/ui/text-input.json +1 -1
- package/registry/ui/textarea.json +1 -1
- package/styles/tokens.reference.css +9 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/multistep-progressbar-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { FlairBanner } from \"../blocks/flair-banner\";\nimport { PageHeaderSection } from \"../blocks/page-header-section\";\nimport { ProgressBar } from \"../blocks/progress-bar\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\nexport interface MultistepProgressBarStep {\n id: string;\n title: string;\n description?: string;\n}\n\nexport interface MultistepProgressBarShellProps {\n /** Flair banner title */\n bannerTitle?: string;\n /** Flair banner description */\n bannerDescription?: string;\n /** Page title shown below the flair banner */\n pageTitle?: string;\n /** Page description shown below the flair banner */\n pageDescription?: string;\n /** Array of step objects */\n steps?: MultistepProgressBarStep[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel is clicked */\n onCancel?: () => void;\n /** Callback when Continue is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the content area */\n contentClassName?: string;\n}\n\n/** Default steps for demo/placeholder purposes */\nexport const defaultProgressBarSteps: MultistepProgressBarStep[] = [\n { id: \"step-1\", title: \"Step 1\", description: \"Enter your basic information\" },\n { id: \"step-2\", title: \"Step 2\", description: \"Provide additional details\" },\n { id: \"step-3\", title: \"Step 3\", description: \"Review and submit\" },\n];\n\n/**\n * Canvas Design System - Multistep Progress Bar Shell\n * \n * A layout for multi-step processes/wizards featuring:\n * - Fixed header with logo\n * - Page title and description section\n * - Horizontal progress bar showing completion\n * - Step-specific title and description\n * - Centered content area (max-width 992px)\n * - Navigation buttons (Cancel, Continue)\n * \n * @example\n * ```tsx\n * <MultistepProgressBarShell\n * pageTitle=\"Page title\"\n * pageDescription=\"Description\"\n * steps={[\n * { id: \"step1\", title: \"Step 1\", description: \"First step\" },\n * { id: \"step2\", title: \"Step 2\", description: \"Second step\" },\n * ]}\n * currentStep={0}\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </MultistepProgressBarShell>\n * ```\n */\nexport function MultistepProgressBarShell({\n bannerTitle = \"Large title\",\n bannerDescription = \"Description\",\n pageTitle = \"Page title\",\n pageDescription = \"Description\",\n steps = defaultProgressBarSteps,\n currentStep = 0,\n children,\n onCancel,\n onContinue,\n cancelText = \"Cancel\",\n continueText = \"Continue\",\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: MultistepProgressBarShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep] || { title: `Step ${currentStep + 1}`, description: \"Description\" };\n const totalSteps = steps.length;\n\n return (\n <div className=\"min-h-screen bg-[var(--background)]\">\n {/* Header - Fixed at top with logo visible */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Flair Banner */}\n <FlairBanner title={bannerTitle} />\n\n {/* Page Header Section */}\n <PageHeaderSection \n title={pageTitle}\n description={pageDescription}\n showTabs={false}\n />\n\n {/* Content Section */}\n <div className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[992px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-5xl)]\",\n contentClassName\n )}\n >\n {/* Progress Bar */}\n <ProgressBar \n currentStep={currentStep}\n totalSteps={totalSteps}\n />\n\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)] pt-[var(--spacing-5xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData.title}\n </Typography>\n {currentStepData.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText}\n </Button>\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { FlairBanner } from \"../blocks/flair-banner\";\nimport { PageHeaderSection } from \"../blocks/page-header-section\";\nimport { ProgressBar } from \"../blocks/progress-bar\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\nexport interface MultistepProgressBarStep {\n id: string;\n title: string;\n description?: string;\n}\n\nexport interface MultistepProgressBarShellProps {\n /** Flair banner title */\n bannerTitle?: string;\n /** Flair banner description */\n bannerDescription?: string;\n /** Page title shown below the flair banner */\n pageTitle?: string;\n /** Page description shown below the flair banner */\n pageDescription?: string;\n /** Array of step objects */\n steps?: MultistepProgressBarStep[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel is clicked */\n onCancel?: () => void;\n /** Callback when Continue is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the content area */\n contentClassName?: string;\n}\n\n/** Default steps for demo/placeholder purposes */\nexport const defaultProgressBarSteps: MultistepProgressBarStep[] = [\n { id: \"step-1\", title: \"Step 1\", description: \"Enter your basic information\" },\n { id: \"step-2\", title: \"Step 2\", description: \"Provide additional details\" },\n { id: \"step-3\", title: \"Step 3\", description: \"Review and submit\" },\n];\n\n/**\n * Canvas Design System - Multistep Progress Bar Shell\n * \n * A layout for multi-step processes/wizards featuring:\n * - Fixed header with logo\n * - Page title and description section\n * - Horizontal progress bar showing completion\n * - Step-specific title and description\n * - Centered content area (max-width 992px)\n * - Navigation buttons (Cancel, Continue)\n * \n * @example\n * ```tsx\n * <MultistepProgressBarShell\n * pageTitle=\"Page title\"\n * pageDescription=\"Description\"\n * steps={[\n * { id: \"step1\", title: \"Step 1\", description: \"First step\" },\n * { id: \"step2\", title: \"Step 2\", description: \"Second step\" },\n * ]}\n * currentStep={0}\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </MultistepProgressBarShell>\n * ```\n */\nexport function MultistepProgressBarShell({\n bannerTitle = \"Large title\",\n bannerDescription = \"Description\",\n pageTitle = \"Page title\",\n pageDescription = \"Description\",\n steps = defaultProgressBarSteps,\n currentStep = 0,\n children,\n onCancel,\n onContinue,\n cancelText = \"Cancel\",\n continueText = \"Continue\",\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: MultistepProgressBarShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep] || { title: `Step ${currentStep + 1}`, description: \"Description\" };\n const totalSteps = steps.length;\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top with logo visible */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Flair Banner */}\n <FlairBanner title={bannerTitle} />\n\n {/* Page Header Section */}\n <PageHeaderSection \n title={pageTitle}\n description={pageDescription}\n showTabs={false}\n />\n\n {/* Content Section */}\n <div className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[992px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-5xl)]\",\n contentClassName\n )}\n >\n {/* Progress Bar */}\n <ProgressBar \n currentStep={currentStep}\n totalSteps={totalSteps}\n />\n\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)] pt-[var(--spacing-5xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData.title}\n </Typography>\n {currentStepData.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText}\n </Button>\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/multistep-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { StepTracker, Step, defaultSteps } from \"../blocks/step-tracker\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\ninterface MultistepShellProps {\n /** Array of step objects */\n steps?: Step[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Callback when a step is clicked */\n onStepClick?: (stepIndex: number) => void;\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel/Back is clicked */\n onCancel?: () => void;\n /** Callback when Continue/Submit is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Multistep Shell\n * \n * A layout for multi-step processes/wizards with:\n * - Fixed header with logo\n * - Horizontal step tracker below header\n * - Step title and description\n * - Centered content area (max-width 768px)\n * - Navigation buttons (Cancel, Continue)\n * \n * @example\n * ```tsx\n * <MultistepShell\n * steps={[\n * { id: \"step1\", label: \"Step 1\", description: \"First step\" },\n * { id: \"step2\", label: \"Step 2\", description: \"Second step\" },\n * ]}\n * currentStep={0}\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </MultistepShell>\n * ```\n */\nexport function MultistepShell({\n steps = defaultSteps,\n currentStep = 0,\n onStepClick,\n children,\n onCancel,\n onContinue,\n cancelText,\n continueText,\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: MultistepShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep];\n const isFirstStep = currentStep === 0;\n const isLastStep = currentStep === steps.length - 1;\n\n return (\n <div className=\"min-h-screen bg-[var(--background)]\">\n {/* Header - Fixed at top with logo visible */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Step Tracker Section */}\n <div className=\"w-full\">\n <StepTracker\n steps={steps}\n currentStep={currentStep}\n onStepClick={onStepClick}\n />\n </div>\n\n {/* Main Content Area - Centered */}\n <main className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[768px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-5xl)]\",\n contentClassName\n )}\n >\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData?.label || `Step ${currentStep + 1}`}\n </Typography>\n {currentStepData?.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText || (isFirstStep ? \"Cancel\" : \"Back\")}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText || (isLastStep ? \"Submit\" : \"Continue\")}\n </Button>\n </div>\n </div>\n </div>\n </main>\n </div>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { StepTracker, Step, defaultSteps } from \"../blocks/step-tracker\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\ninterface MultistepShellProps {\n /** Array of step objects */\n steps?: Step[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Callback when a step is clicked */\n onStepClick?: (stepIndex: number) => void;\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel/Back is clicked */\n onCancel?: () => void;\n /** Callback when Continue/Submit is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Multistep Shell\n * \n * A layout for multi-step processes/wizards with:\n * - Fixed header with logo\n * - Horizontal step tracker below header\n * - Step title and description\n * - Centered content area (max-width 768px)\n * - Navigation buttons (Cancel, Continue)\n * \n * @example\n * ```tsx\n * <MultistepShell\n * steps={[\n * { id: \"step1\", label: \"Step 1\", description: \"First step\" },\n * { id: \"step2\", label: \"Step 2\", description: \"Second step\" },\n * ]}\n * currentStep={0}\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </MultistepShell>\n * ```\n */\nexport function MultistepShell({\n steps = defaultSteps,\n currentStep = 0,\n onStepClick,\n children,\n onCancel,\n onContinue,\n cancelText,\n continueText,\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: MultistepShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep];\n const isFirstStep = currentStep === 0;\n const isLastStep = currentStep === steps.length - 1;\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top with logo visible */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Step Tracker Section */}\n <div className=\"w-full\">\n <StepTracker\n steps={steps}\n currentStep={currentStep}\n onStepClick={onStepClick}\n />\n </div>\n\n {/* Main Content Area - Centered */}\n <main className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[768px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-5xl)]\",\n contentClassName\n )}\n >\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData?.label || `Step ${currentStep + 1}`}\n </Typography>\n {currentStepData?.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText || (isFirstStep ? \"Cancel\" : \"Back\")}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText || (isLastStep ? \"Submit\" : \"Continue\")}\n </Button>\n </div>\n </div>\n </div>\n </main>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/multistep-sidebar-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { StepTracker, Step, defaultSteps } from \"../blocks/step-tracker\";\nimport { InfoCard, LinksCard, LinkItem, defaultSupportLinks } from \"../blocks/sidebar-cards\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\ninterface MultistepSidebarShellProps {\n /** Array of step objects */\n steps?: Step[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Callback when a step is clicked */\n onStepClick?: (stepIndex: number) => void;\n /** Info card title */\n infoTitle?: string;\n /** Info card description */\n infoDescription?: string;\n /** Links card title */\n linksTitle?: string;\n /** Links card items */\n links?: LinkItem[];\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel/Back is clicked */\n onCancel?: () => void;\n /** Callback when Continue/Submit is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Multistep Sidebar Shell\n * \n * A layout for multi-step processes with a sidebar:\n * - Fixed header with logo\n * - Horizontal step tracker below header\n * - Step title and description\n * - Two-column layout:\n * - Left: Main content with navigation buttons\n * - Right: Sidebar (320px) with Info and Links cards\n * \n * @example\n * ```tsx\n * <MultistepSidebarShell\n * steps={defaultSteps}\n * currentStep={0}\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </MultistepSidebarShell>\n * ```\n */\nexport function MultistepSidebarShell({\n steps = defaultSteps,\n currentStep = 0,\n onStepClick,\n infoTitle,\n infoDescription,\n linksTitle,\n links = defaultSupportLinks,\n children,\n onCancel,\n onContinue,\n cancelText,\n continueText,\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: MultistepSidebarShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep];\n const isFirstStep = currentStep === 0;\n const isLastStep = currentStep === steps.length - 1;\n\n return (\n <div className=\"min-h-screen bg-[var(--background)]\">\n {/* Header - Fixed at top with logo visible */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Step Tracker Section */}\n <div className=\"w-full\">\n <StepTracker\n steps={steps}\n currentStep={currentStep}\n onStepClick={onStepClick}\n />\n </div>\n\n {/* Main Content Area - Two Column Layout */}\n <main className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[1200px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-10\",\n \"flex gap-10\"\n )}\n >\n {/* Left: Main Content */}\n <div className={cn(\"flex-1 min-w-0\", contentClassName)}>\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData?.label || `Step ${currentStep + 1}`}\n </Typography>\n {currentStepData?.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText || (isFirstStep ? \"Cancel\" : \"Back\")}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText || (isLastStep ? \"Submit\" : \"Continue\")}\n </Button>\n </div>\n </div>\n </div>\n\n {/* Right: Sidebar */}\n <aside className=\"hidden lg:flex flex-col gap-10 shrink-0\">\n <InfoCard \n title={infoTitle} \n description={infoDescription} \n />\n <LinksCard \n title={linksTitle} \n links={links} \n />\n </aside>\n </div>\n </main>\n </div>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { StepTracker, Step, defaultSteps } from \"../blocks/step-tracker\";\nimport { InfoCard, LinksCard, LinkItem, defaultSupportLinks } from \"../blocks/sidebar-cards\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\ninterface MultistepSidebarShellProps {\n /** Array of step objects */\n steps?: Step[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Callback when a step is clicked */\n onStepClick?: (stepIndex: number) => void;\n /** Info card title */\n infoTitle?: string;\n /** Info card description */\n infoDescription?: string;\n /** Links card title */\n linksTitle?: string;\n /** Links card items */\n links?: LinkItem[];\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel/Back is clicked */\n onCancel?: () => void;\n /** Callback when Continue/Submit is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Multistep Sidebar Shell\n * \n * A layout for multi-step processes with a sidebar:\n * - Fixed header with logo\n * - Horizontal step tracker below header\n * - Step title and description\n * - Two-column layout:\n * - Left: Main content with navigation buttons\n * - Right: Sidebar (320px) with Info and Links cards\n * \n * @example\n * ```tsx\n * <MultistepSidebarShell\n * steps={defaultSteps}\n * currentStep={0}\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </MultistepSidebarShell>\n * ```\n */\nexport function MultistepSidebarShell({\n steps = defaultSteps,\n currentStep = 0,\n onStepClick,\n infoTitle,\n infoDescription,\n linksTitle,\n links = defaultSupportLinks,\n children,\n onCancel,\n onContinue,\n cancelText,\n continueText,\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: MultistepSidebarShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep];\n const isFirstStep = currentStep === 0;\n const isLastStep = currentStep === steps.length - 1;\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top with logo visible */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Step Tracker Section */}\n <div className=\"w-full\">\n <StepTracker\n steps={steps}\n currentStep={currentStep}\n onStepClick={onStepClick}\n />\n </div>\n\n {/* Main Content Area - Two Column Layout */}\n <main className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[1200px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-10\",\n \"flex gap-10\"\n )}\n >\n {/* Left: Main Content */}\n <div className={cn(\"flex-1 min-w-0\", contentClassName)}>\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData?.label || `Step ${currentStep + 1}`}\n </Typography>\n {currentStepData?.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText || (isFirstStep ? \"Cancel\" : \"Back\")}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText || (isLastStep ? \"Submit\" : \"Continue\")}\n </Button>\n </div>\n </div>\n </div>\n\n {/* Right: Sidebar */}\n <aside className=\"hidden lg:flex flex-col gap-10 shrink-0\">\n <InfoCard \n title={infoTitle} \n description={infoDescription} \n />\n <LinksCard \n title={linksTitle} \n links={links} \n />\n </aside>\n </div>\n </main>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/project-context-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { ReactNode } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport {\n FileText,\n Users,\n LayoutGrid,\n Wand2,\n} from \"lucide-react\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\nimport {\n Diamond,\n Hexagon,\n Star,\n Lightning,\n Sparkle,\n Infinity,\n Code,\n Terminal,\n Cpu,\n Database,\n Globe,\n Cloud,\n WifiHigh,\n Briefcase,\n Buildings,\n Storefront,\n Handshake,\n ChartLine,\n Palette as PaletteIcon,\n PencilSimple,\n Camera,\n MusicNote,\n Lightbulb,\n Leaf,\n Tree,\n Sun,\n Moon,\n Fire,\n Drop,\n ChatCircle,\n Envelope,\n Phone,\n Megaphone,\n Heart,\n Shield,\n Trophy,\n Rocket,\n Target,\n Flag,\n type Icon as PhosphorIcon,\n} from \"@phosphor-icons/react\";\n\n// ═══════════════════════════════════════════════════════════\n// LOGO ICON SHAPES\n// ═══════════════════════════════════════════════════════════\n\ntype IconShapeId = \"rounded\" | \"circle\" | \"square\";\n\nconst iconShapes: { id: IconShapeId; renderBackground: (bgColor: string) => React.ReactNode }[] = [\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\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu, Database,\n Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake, ChartLine,\n Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n};\n\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// TAB TYPES\n// ═══════════════════════════════════════════════════════════\n\nexport type ProjectContextTab = \"scope\" | \"personas\" | \"screens\" | \"prompts\";\n\ninterface TabConfig {\n id: ProjectContextTab;\n label: string;\n icon: typeof FileText;\n description: string;\n}\n\nconst tabs: TabConfig[] = [\n {\n id: \"scope\",\n label: \"Scope\",\n icon: FileText,\n description: \"Upload your project scope document\",\n },\n {\n id: \"personas\",\n label: \"Personas\",\n icon: Users,\n description: \"Define who you're building for\",\n },\n {\n id: \"screens\",\n label: \"Screens\",\n icon: LayoutGrid,\n description: \"Map out your product's screens and flows\",\n },\n {\n id: \"prompts\",\n label: \"Prompt Helpers\",\n icon: Wand2,\n description: \"Build prompts with existing components\",\n },\n];\n\n// ═══════════════════════════════════════════════════════════\n// SHELL COMPONENT\n// ═══════════════════════════════════════════════════════════\n\ninterface ProjectContextShellProps {\n children: ReactNode;\n activeTab: ProjectContextTab;\n onTabChange: (tab: ProjectContextTab) => void;\n}\n\nexport function ProjectContextShell({\n children,\n activeTab,\n onTabChange,\n}: ProjectContextShellProps) {\n const activeTabConfig = tabs.find((t) => t.id === activeTab);\n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n \n // Get logo (use light variant for this light sidebar)\n const logoUrl = themeImages.logoLight;\n const iconShape = iconShapes.find(s => s.id === branding.iconShape) || iconShapes[0];\n\n return (\n <div className=\"min-h-screen flex bg-[var(--canvas-background)]\">\n {/* Sidebar */}\n <aside className=\"w-64 border-r border-[var(--canvas-border)] bg-[var(--canvas-background)] flex flex-col shrink-0\">\n {/* Logo + Title Header - matches main header height */}\n <div className=\"px-4 border-b border-[var(--canvas-border)] flex items-center h-[97px]\">\n {/* Logo */}\n <div className={`flex items-center ${isMounted ? 'opacity-100' : 'opacity-0'}`}>\n {logoUrl ? (\n <img \n src={logoUrl} \n alt=\"Logo\" \n className=\"h-8 w-auto object-contain\"\n />\n ) : (\n // Uses CSS variables directly - no JavaScript resolution needed\n <div className=\"flex items-center\">\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 <span className=\"
|
|
9
|
+
"content": "\"use client\";\n\nimport { ReactNode } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport {\n FileText,\n Users,\n LayoutGrid,\n Wand2,\n} from \"lucide-react\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\nimport {\n Diamond,\n Hexagon,\n Star,\n Lightning,\n Sparkle,\n Infinity,\n Code,\n Terminal,\n Cpu,\n Database,\n Globe,\n Cloud,\n WifiHigh,\n Briefcase,\n Buildings,\n Storefront,\n Handshake,\n ChartLine,\n Palette as PaletteIcon,\n PencilSimple,\n Camera,\n MusicNote,\n Lightbulb,\n Leaf,\n Tree,\n Sun,\n Moon,\n Fire,\n Drop,\n ChatCircle,\n Envelope,\n Phone,\n Megaphone,\n Heart,\n Shield,\n Trophy,\n Rocket,\n Target,\n Flag,\n type Icon as PhosphorIcon,\n} from \"@phosphor-icons/react\";\n\n// ═══════════════════════════════════════════════════════════\n// LOGO ICON SHAPES\n// ═══════════════════════════════════════════════════════════\n\ntype IconShapeId = \"rounded\" | \"circle\" | \"square\";\n\nconst iconShapes: { id: IconShapeId; renderBackground: (bgColor: string) => React.ReactNode }[] = [\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\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu, Database,\n Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake, ChartLine,\n Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n};\n\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// TAB TYPES\n// ═══════════════════════════════════════════════════════════\n\nexport type ProjectContextTab = \"scope\" | \"personas\" | \"screens\" | \"prompts\";\n\ninterface TabConfig {\n id: ProjectContextTab;\n label: string;\n icon: typeof FileText;\n description: string;\n}\n\nconst tabs: TabConfig[] = [\n {\n id: \"scope\",\n label: \"Scope\",\n icon: FileText,\n description: \"Upload your project scope document\",\n },\n {\n id: \"personas\",\n label: \"Personas\",\n icon: Users,\n description: \"Define who you're building for\",\n },\n {\n id: \"screens\",\n label: \"Screens\",\n icon: LayoutGrid,\n description: \"Map out your product's screens and flows\",\n },\n {\n id: \"prompts\",\n label: \"Prompt Helpers\",\n icon: Wand2,\n description: \"Build prompts with existing components\",\n },\n];\n\n// ═══════════════════════════════════════════════════════════\n// SHELL COMPONENT\n// ═══════════════════════════════════════════════════════════\n\ninterface ProjectContextShellProps {\n children: ReactNode;\n activeTab: ProjectContextTab;\n onTabChange: (tab: ProjectContextTab) => void;\n}\n\nexport function ProjectContextShell({\n children,\n activeTab,\n onTabChange,\n}: ProjectContextShellProps) {\n const activeTabConfig = tabs.find((t) => t.id === activeTab);\n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n \n // Get logo (use light variant for this light sidebar)\n const logoUrl = themeImages.logoLight;\n const iconShape = iconShapes.find(s => s.id === branding.iconShape) || iconShapes[0];\n\n return (\n <div className=\"min-h-screen flex bg-[var(--canvas-background)]\">\n {/* Sidebar */}\n <aside className=\"w-64 border-r border-[var(--canvas-border)] bg-[var(--canvas-background)] flex flex-col shrink-0\">\n {/* Logo + Title Header - matches main header height */}\n <div className=\"px-4 border-b border-[var(--canvas-border)] flex items-center h-[97px]\">\n {/* Logo */}\n <div className={`flex items-center ${isMounted ? 'opacity-100' : 'opacity-0'}`}>\n {logoUrl ? (\n <img \n src={logoUrl} \n alt=\"Logo\" \n className=\"h-8 w-auto object-contain\"\n />\n ) : (\n // Uses CSS variables directly - no JavaScript resolution needed\n <div className=\"flex items-center\">\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 <span className=\"font-semibold ml-2.5 text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-xl-size)\" }}>\n {branding.wordmark || \"canvas\"}\n </span>\n </div>\n )}\n </div>\n </div>\n\n <nav className=\"flex-1 p-2\">\n {tabs.map((tab) => {\n const Icon = tab.icon;\n const isActive = activeTab === tab.id;\n\n return (\n <button\n key={tab.id}\n onClick={() => onTabChange(tab.id)}\n className={cn(\n \"cursor-pointer w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors mb-1\",\n isActive\n ? \"bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n : \"text-[var(--canvas-text-muted)] hover:bg-[var(--canvas-surface)] hover:text-[var(--canvas-text)]\"\n )}\n >\n <Icon className=\"size-4 shrink-0\" />\n <span className=\"font-medium\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>{tab.label}</span>\n </button>\n );\n })}\n </nav>\n\n {/* Help section */}\n <div className=\"p-4 border-t border-[var(--canvas-border)]\">\n <div className=\"rounded-lg bg-[var(--canvas-surface)] p-3\">\n <p className=\"text-[var(--canvas-text-muted)] leading-relaxed\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n 💡 Use the prompt templates to generate content with Cursor AI\n </p>\n </div>\n </div>\n </aside>\n\n {/* Content Area */}\n <main className=\"flex-1 flex flex-col min-w-0 overflow-hidden\">\n {/* Tab Header */}\n <div className=\"border-b border-[var(--canvas-border)] px-8 py-6 bg-[var(--canvas-background)]\">\n {activeTabConfig && (\n <div>\n <h2 className=\"font-semibold text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-xl-size)\" }}>\n {activeTabConfig.label}\n </h2>\n <p className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {activeTabConfig.description}\n </p>\n </div>\n )}\n </div>\n\n {/* Scrollable Content */}\n <ScrollArea className=\"flex-1\">\n <div className=\"p-8\">{children}</div>\n </ScrollArea>\n </main>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/search-bar-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { FlairBanner } from \"../blocks/flair-banner\";\nimport { PageHeaderSection } from \"../blocks/page-header-section\";\nimport { SearchBar } from \"../blocks/search-bar\";\nimport { FilterPopover, FilterState } from \"../blocks/filter-popover\";\n\ninterface SearchBarShellProps {\n /** Flair banner title */\n bannerTitle?: string;\n /** Flair banner description */\n bannerDescription?: string;\n /** Page title */\n pageTitle?: string;\n /** Page description */\n pageDescription?: string;\n /** Search bar placeholder */\n searchPlaceholder?: string;\n /** Callback when search is triggered */\n onSearch?: (query: string) => void;\n /** Callback when filters are applied */\n onFilterApply?: (filters: FilterState) => void;\n /** Main content */\n children: React.ReactNode;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Search Bar Shell\n * \n * A layout with:\n * - Fixed header with logo (no sidebar)\n * - Page header section with title and description\n * - Search bar with filter button\n * - Content area for search results\n * \n * @example\n * ```tsx\n * <SearchBarShell\n * pageTitle=\"Page title\"\n * pageDescription=\"Description\"\n * onSearch={(query) => console.log(query)}\n * >\n * <ContentDropzone />\n * </SearchBarShell>\n * ```\n */\nexport function SearchBarShell({\n bannerTitle = \"Large title\",\n bannerDescription = \"Description\",\n pageTitle = \"Page title\",\n pageDescription = \"Description\",\n searchPlaceholder = \"Search\",\n onSearch,\n onFilterApply,\n children,\n contentClassName,\n}: SearchBarShellProps) {\n useCSSVariableSync();\n const [searchValue, setSearchValue] = useState(\"\");\n const [filterState, setFilterState] = useState<FilterState>({\n dropdowns: {},\n checkboxes: {},\n dateRange: { start: \"\", end: \"\" },\n });\n\n const handleSearch = () => {\n onSearch?.(searchValue);\n };\n\n const handleFilterApply = (newFilters: FilterState) => {\n setFilterState(newFilters);\n onFilterApply?.(newFilters);\n };\n\n const handleFilterClear = () => {\n const clearedState: FilterState = {\n dropdowns: {},\n checkboxes: {},\n dateRange: { start: \"\", end: \"\" },\n };\n setFilterState(clearedState);\n onFilterApply?.(clearedState);\n };\n\n return (\n <div className=\"min-h-screen bg-[var(--background)]\">\n {/* Header - Fixed at top with logo visible (no sidebar) */}\n <header className=\"sticky top-0 z-40\">\n <Header showDesktopLogo />\n </header>\n\n {/* Flair Banner */}\n <FlairBanner title={bannerTitle} />\n\n {/* Page Header Section - Title and Description only, no tabs */}\n <PageHeaderSection\n title={pageTitle}\n description={pageDescription}\n showTabs={false}\n />\n\n {/* Search Bar Section */}\n <div className=\"w-full\">\n <div\n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"pt-10 pb-10\",\n \"border-b border-[var(--canvas-border)]\"\n )}\n >\n <div className=\"flex items-center gap-6\">\n {/* Search Bar */}\n <SearchBar\n placeholder={searchPlaceholder}\n value={searchValue}\n onChange={setSearchValue}\n onSearch={handleSearch}\n className=\"flex-1\"\n />\n\n {/* Filter Button */}\n <FilterPopover\n filterState={filterState}\n onApply={handleFilterApply}\n onClear={handleFilterClear}\n />\n </div>\n </div>\n </div>\n\n {/* Main Content Area */}\n <main className=\"w-full\">\n <div\n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-6xl)]\",\n contentClassName\n )}\n >\n {children}\n </div>\n </main>\n </div>\n );\n}\n\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { FlairBanner } from \"../blocks/flair-banner\";\nimport { PageHeaderSection } from \"../blocks/page-header-section\";\nimport { SearchBar } from \"../blocks/search-bar\";\nimport { FilterPopover, FilterState } from \"../blocks/filter-popover\";\n\ninterface SearchBarShellProps {\n /** Flair banner title */\n bannerTitle?: string;\n /** Flair banner description */\n bannerDescription?: string;\n /** Page title */\n pageTitle?: string;\n /** Page description */\n pageDescription?: string;\n /** Search bar placeholder */\n searchPlaceholder?: string;\n /** Callback when search is triggered */\n onSearch?: (query: string) => void;\n /** Callback when filters are applied */\n onFilterApply?: (filters: FilterState) => void;\n /** Main content */\n children: React.ReactNode;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Search Bar Shell\n * \n * A layout with:\n * - Fixed header with logo (no sidebar)\n * - Page header section with title and description\n * - Search bar with filter button\n * - Content area for search results\n * \n * @example\n * ```tsx\n * <SearchBarShell\n * pageTitle=\"Page title\"\n * pageDescription=\"Description\"\n * onSearch={(query) => console.log(query)}\n * >\n * <ContentDropzone />\n * </SearchBarShell>\n * ```\n */\nexport function SearchBarShell({\n bannerTitle = \"Large title\",\n bannerDescription = \"Description\",\n pageTitle = \"Page title\",\n pageDescription = \"Description\",\n searchPlaceholder = \"Search\",\n onSearch,\n onFilterApply,\n children,\n contentClassName,\n}: SearchBarShellProps) {\n useCSSVariableSync();\n const [searchValue, setSearchValue] = useState(\"\");\n const [filterState, setFilterState] = useState<FilterState>({\n dropdowns: {},\n checkboxes: {},\n dateRange: { start: \"\", end: \"\" },\n });\n\n const handleSearch = () => {\n onSearch?.(searchValue);\n };\n\n const handleFilterApply = (newFilters: FilterState) => {\n setFilterState(newFilters);\n onFilterApply?.(newFilters);\n };\n\n const handleFilterClear = () => {\n const clearedState: FilterState = {\n dropdowns: {},\n checkboxes: {},\n dateRange: { start: \"\", end: \"\" },\n };\n setFilterState(clearedState);\n onFilterApply?.(clearedState);\n };\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top with logo visible (no sidebar) */}\n <header className=\"sticky top-0 z-40\">\n <Header showDesktopLogo />\n </header>\n\n {/* Flair Banner */}\n <FlairBanner title={bannerTitle} />\n\n {/* Page Header Section - Title and Description only, no tabs */}\n <PageHeaderSection\n title={pageTitle}\n description={pageDescription}\n showTabs={false}\n />\n\n {/* Search Bar Section */}\n <div className=\"w-full\">\n <div\n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"pt-10 pb-10\",\n \"border-b border-[var(--canvas-border)]\"\n )}\n >\n <div className=\"flex items-center gap-6\">\n {/* Search Bar */}\n <SearchBar\n placeholder={searchPlaceholder}\n value={searchValue}\n onChange={setSearchValue}\n onSearch={handleSearch}\n className=\"flex-1\"\n />\n\n {/* Filter Button */}\n <FilterPopover\n filterState={filterState}\n onApply={handleFilterApply}\n onClear={handleFilterClear}\n />\n </div>\n </div>\n </div>\n\n {/* Main Content Area */}\n <main className=\"w-full\">\n <div\n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-6xl)]\",\n contentClassName\n )}\n >\n {children}\n </div>\n </main>\n </div>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/sidebar-nav.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, ChevronDown } from \"lucide-react\";\n\nexport interface NavItem {\n id: string;\n label: string;\n icon: LucideIcon;\n href?: string;\n isActive?: boolean;\n /** Nested subtabs - when present, the item becomes expandable */\n children?: Omit<NavItem, \"children\" | \"icon\">[];\n}\n\nexport interface NavSection {\n id: string;\n title: string;\n items: NavItem[];\n}\n\ninterface SidebarNavProps {\n sections: NavSection[];\n variant?: \"dark\" | \"light\";\n onItemClick?: (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => void;\n}\n\n/**\n * Canvas Design System - Sidebar Navigation\n * \n * Renders navigation sections with collapsible groups and nav items.\n * Supports dark (desktop) and light (mobile sheet) variants.\n * Items with children become expandable with nested subtabs.\n */\nexport function SidebarNav({ sections, variant = \"dark\", onItemClick }: SidebarNavProps) {\n const isDark = variant === \"dark\";\n \n // Track which items are expanded (by item id)\n const [expandedItems, setExpandedItems] = useState<Set<string>>(() => {\n // Auto-expand items that have active children\n const initialExpanded = new Set<string>();\n sections.forEach(section => {\n section.items.forEach(item => {\n if (item.children?.some(child => child.isActive)) {\n initialExpanded.add(item.id);\n }\n });\n });\n return initialExpanded;\n });\n\n const toggleExpanded = (itemId: string) => {\n setExpandedItems(prev => {\n const next = new Set(prev);\n if (next.has(itemId)) {\n next.delete(itemId);\n } else {\n next.add(itemId);\n }\n return next;\n });\n };\n \n return (\n <nav className=\"flex flex-col gap-[var(--spacing-xs)] w-full\">\n {sections.map((section) => (\n <div key={section.id} className=\"flex flex-col w-full\">\n {/* Section Title */}\n <div className=\"flex items-center pt-11 pb-[var(--spacing-md)] pl-[var(--spacing-2xl)] pr-0\">\n <span\n className={cn(\n \"uppercase\",\n isDark \n ? \"text-[var(--canvas-sidebar-dark-text)]\" \n : \"text-[var(--canvas-neutral-placeholder)]\"\n )}\n style={{\n fontFamily: \"var(--typo-sidebar-label-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-label-size)\",\n fontWeight: \"var(--typo-sidebar-label-weight)\",\n letterSpacing: \"var(--typo-sidebar-label-spacing)\",\n lineHeight: \"var(--typo-sidebar-label-line-height)\",\n }}\n >\n {section.title}\n </span>\n </div>\n \n {/* Nav Items */}\n <div className=\"flex flex-col gap-0\">\n {section.items.map((item) => {\n const Icon = item.icon;\n const isActive = item.isActive;\n const hasChildren = item.children && item.children.length > 0;\n const isExpanded = expandedItems.has(item.id);\n \n return (\n <div key={item.id} className=\"flex flex-col\">\n {/* Parent Nav Item */}\n <button\n onClick={() => {\n if (hasChildren) {\n toggleExpanded(item.id);\n } else {\n onItemClick?.(item);\n }\n }}\n className={cn(\n \"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 (desktop sidebar)\n isDark && isActive && !hasChildren && \"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 isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n // Light variant (mobile sheet)\n !isDark && isActive && !hasChildren && \"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 !isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-light-active-text)]\"\n )}\n >\n <Icon \n className={cn(\n \"size-4 shrink-0\",\n isDark && isActive && !hasChildren && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n !isDark && isActive && !hasChildren && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\",\n !isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-light-active-text)]\"\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 {item.label}\n </span>\n {hasChildren && (\n <ChevronDown \n className={cn(\n \"size-4 shrink-0 transition-transform duration-200\",\n isExpanded && \"rotate-180\",\n isDark ? \"text-[var(--canvas-sidebar-dark-text)]\" : \"text-[var(--canvas-neutral-text)]\"\n )}\n />\n )}\n </button>\n\n {/* Nested Subtabs */}\n {hasChildren && isExpanded && (\n <div className=\"flex flex-col mt-1 mb-1\">\n {item.children!.map((child) => {\n const isChildActive = child.isActive;\n \n return (\n <button\n key={child.id}\n onClick={() => onItemClick?.(child)}\n className={cn(\n \"flex items-center h-9 pl-12 pr-[var(--spacing-xl)] rounded-[var(--radius-nav)] w-full text-left transition-colors\",\n // Dark variant\n isDark && isChildActive && \"bg-[var(--canvas-sidebar-dark-active-bg)] text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isChildActive && \"text-[var(--canvas-sidebar-dark-text)] hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isChildActive && \"bg-[var(--canvas-sidebar-light-active-bg)] text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isChildActive && \"text-[var(--canvas-sidebar-light-text)] hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <span \n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-sidebar-subtab-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-subtab-size)\",\n fontWeight: \"var(--typo-sidebar-subtab-weight)\",\n letterSpacing: \"var(--typo-sidebar-subtab-spacing)\",\n lineHeight: \"var(--typo-sidebar-subtab-line-height)\",\n }}\n >\n {child.label}\n </span>\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n ))}\n </nav>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, ChevronDown } from \"lucide-react\";\n\nexport interface NavItem {\n id: string;\n label: string;\n icon: LucideIcon;\n href?: string;\n isActive?: boolean;\n /** Nested subtabs - when present, the item becomes expandable */\n children?: Omit<NavItem, \"children\" | \"icon\">[];\n}\n\nexport interface NavSection {\n id: string;\n title: string;\n items: NavItem[];\n}\n\ninterface SidebarNavProps {\n sections: NavSection[];\n variant?: \"dark\" | \"light\";\n onItemClick?: (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => void;\n}\n\n/**\n * Canvas Design System - Sidebar Navigation\n * \n * Renders navigation sections with collapsible groups and nav items.\n * Supports dark (desktop) and light (mobile sheet) variants.\n * Items with children become expandable with nested subtabs.\n */\nexport function SidebarNav({ sections, variant = \"dark\", onItemClick }: SidebarNavProps) {\n const isDark = variant === \"dark\";\n \n // Track which items are expanded (by item id)\n const [expandedItems, setExpandedItems] = useState<Set<string>>(() => {\n // Auto-expand items that have active children\n const initialExpanded = new Set<string>();\n sections.forEach(section => {\n section.items.forEach(item => {\n if (item.children?.some(child => child.isActive)) {\n initialExpanded.add(item.id);\n }\n });\n });\n return initialExpanded;\n });\n\n const toggleExpanded = (itemId: string) => {\n setExpandedItems(prev => {\n const next = new Set(prev);\n if (next.has(itemId)) {\n next.delete(itemId);\n } else {\n next.add(itemId);\n }\n return next;\n });\n };\n \n return (\n <nav className=\"flex flex-col gap-[var(--spacing-xs)] w-full\">\n {sections.map((section) => (\n <div key={section.id} className=\"flex flex-col w-full\">\n {/* Section Title */}\n <div className=\"flex items-center pt-11 pb-[var(--spacing-md)] pl-[var(--spacing-2xl)] pr-0\">\n <span\n className={cn(\n \"uppercase\",\n isDark \n ? \"text-[var(--canvas-sidebar-dark-text)]\" \n : \"text-[var(--canvas-neutral-placeholder)]\"\n )}\n style={{\n fontFamily: \"var(--typo-sidebar-label-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-label-size)\",\n fontWeight: \"var(--typo-sidebar-label-weight)\",\n letterSpacing: \"var(--typo-sidebar-label-spacing)\",\n lineHeight: \"var(--typo-sidebar-label-line-height)\",\n }}\n >\n {section.title}\n </span>\n </div>\n \n {/* Nav Items */}\n <div className=\"flex flex-col gap-0\">\n {section.items.map((item) => {\n const Icon = item.icon;\n const isActive = item.isActive;\n const hasChildren = item.children && item.children.length > 0;\n const isExpanded = expandedItems.has(item.id);\n \n return (\n <div key={item.id} className=\"flex flex-col\">\n {/* Parent Nav Item */}\n <button\n onClick={() => {\n if (hasChildren) {\n toggleExpanded(item.id);\n } else {\n onItemClick?.(item);\n }\n }}\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 (desktop sidebar)\n isDark && isActive && !hasChildren && \"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 isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n // Light variant (mobile sheet)\n !isDark && isActive && !hasChildren && \"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 !isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-light-active-text)]\"\n )}\n >\n <Icon \n className={cn(\n \"size-4 shrink-0\",\n isDark && isActive && !hasChildren && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n !isDark && isActive && !hasChildren && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\",\n !isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-light-active-text)]\"\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 {item.label}\n </span>\n {hasChildren && (\n <ChevronDown \n className={cn(\n \"size-4 shrink-0 transition-transform duration-200\",\n isExpanded && \"rotate-180\",\n isDark ? \"text-[var(--canvas-sidebar-dark-text)]\" : \"text-[var(--canvas-neutral-text)]\"\n )}\n />\n )}\n </button>\n\n {/* Nested Subtabs */}\n {hasChildren && isExpanded && (\n <div className=\"flex flex-col mt-1 mb-1\">\n {item.children!.map((child) => {\n const isChildActive = child.isActive;\n \n return (\n <button\n key={child.id}\n onClick={() => onItemClick?.(child)}\n className={cn(\n \"cursor-pointer flex items-center h-9 pl-12 pr-[var(--spacing-xl)] rounded-[var(--radius-nav)] w-full text-left transition-colors\",\n // Dark variant\n isDark && isChildActive && \"bg-[var(--canvas-sidebar-dark-active-bg)] text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isChildActive && \"text-[var(--canvas-sidebar-dark-text)] hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isChildActive && \"bg-[var(--canvas-sidebar-light-active-bg)] text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isChildActive && \"text-[var(--canvas-sidebar-light-text)] hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <span \n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-sidebar-subtab-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-subtab-size)\",\n fontWeight: \"var(--typo-sidebar-subtab-weight)\",\n letterSpacing: \"var(--typo-sidebar-subtab-spacing)\",\n lineHeight: \"var(--typo-sidebar-subtab-line-height)\",\n }}\n >\n {child.label}\n </span>\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n ))}\n </nav>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/sidebar.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { SidebarNav, NavSection, NavItem } from \"./sidebar-nav\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\n\n// Phosphor Icons (Bold variant) - same curated set as variables-modal\nimport {\n Diamond,\n Hexagon,\n Star,\n Lightning,\n Sparkle,\n Infinity,\n Code,\n Terminal,\n Cpu,\n Database,\n Globe,\n Cloud,\n WifiHigh,\n Briefcase,\n Buildings,\n Storefront,\n Handshake,\n ChartLine,\n Palette as PaletteIcon,\n PencilSimple,\n Camera,\n MusicNote,\n Lightbulb,\n Leaf,\n Tree,\n Sun,\n Moon,\n Fire,\n Drop,\n ChatCircle,\n Envelope,\n Phone,\n Megaphone,\n Heart,\n Shield,\n Trophy,\n Rocket,\n Target,\n Flag,\n type Icon as PhosphorIcon,\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,\n Hexagon,\n Star,\n Lightning,\n Sparkle,\n Infinity,\n Code,\n Terminal,\n Cpu,\n Database,\n Globe,\n Cloud,\n WifiHigh,\n Briefcase,\n Buildings,\n Storefront,\n Handshake,\n ChartLine,\n Palette: PaletteIcon,\n PencilSimple,\n Camera,\n MusicNote,\n Lightbulb,\n Leaf,\n Tree,\n Sun,\n Moon,\n Fire,\n Drop,\n ChatCircle,\n Envelope,\n Phone,\n Megaphone,\n Heart,\n Shield,\n Trophy,\n Rocket,\n Target,\n Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\ninterface SidebarProps {\n /** Navigation sections to display */\n sections: NavSection[];\n /** Visual variant - dark for desktop, light for mobile sheet */\n variant?: \"dark\" | \"light\";\n /** Callback when a nav item or subtab is clicked */\n onItemClick?: (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Sidebar Component\n * \n * Desktop: Fixed dark sidebar (320px width) on the left\n * Mobile: Light theme sidebar rendered inside a Sheet\n */\nexport function Sidebar({ \n sections, \n variant = \"dark\", \n onItemClick,\n className \n}: SidebarProps) {\n const isDark = variant === \"dark\";\n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n \n // Get the appropriate logo based on variant\n const logoUrl = isDark ? 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 return (\n <aside\n className={cn(\n \"flex flex-col h-full w-[var(--sidebar-width)]\",\n isDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isDark && \"bg-
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { SidebarNav, NavSection, NavItem } from \"./sidebar-nav\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\n\n// Phosphor Icons (Bold variant) - same curated set as variables-modal\nimport {\n Diamond,\n Hexagon,\n Star,\n Lightning,\n Sparkle,\n Infinity,\n Code,\n Terminal,\n Cpu,\n Database,\n Globe,\n Cloud,\n WifiHigh,\n Briefcase,\n Buildings,\n Storefront,\n Handshake,\n ChartLine,\n Palette as PaletteIcon,\n PencilSimple,\n Camera,\n MusicNote,\n Lightbulb,\n Leaf,\n Tree,\n Sun,\n Moon,\n Fire,\n Drop,\n ChatCircle,\n Envelope,\n Phone,\n Megaphone,\n Heart,\n Shield,\n Trophy,\n Rocket,\n Target,\n Flag,\n type Icon as PhosphorIcon,\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,\n Hexagon,\n Star,\n Lightning,\n Sparkle,\n Infinity,\n Code,\n Terminal,\n Cpu,\n Database,\n Globe,\n Cloud,\n WifiHigh,\n Briefcase,\n Buildings,\n Storefront,\n Handshake,\n ChartLine,\n Palette: PaletteIcon,\n PencilSimple,\n Camera,\n MusicNote,\n Lightbulb,\n Leaf,\n Tree,\n Sun,\n Moon,\n Fire,\n Drop,\n ChatCircle,\n Envelope,\n Phone,\n Megaphone,\n Heart,\n Shield,\n Trophy,\n Rocket,\n Target,\n Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\ninterface SidebarProps {\n /** Navigation sections to display */\n sections: NavSection[];\n /** Visual variant - dark for desktop, light for mobile sheet */\n variant?: \"dark\" | \"light\";\n /** Callback when a nav item or subtab is clicked */\n onItemClick?: (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Sidebar Component\n * \n * Desktop: Fixed dark sidebar (320px width) on the left\n * Mobile: Light theme sidebar rendered inside a Sheet\n */\nexport function Sidebar({ \n sections, \n variant = \"dark\", \n onItemClick,\n className \n}: SidebarProps) {\n const isDark = variant === \"dark\";\n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n \n // Get the appropriate logo based on variant\n const logoUrl = isDark ? 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 return (\n <aside\n className={cn(\n \"flex flex-col h-full w-[var(--sidebar-width)]\",\n isDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)]\",\n className\n )}\n >\n {/* Logo Section */}\n <div className={cn(\n \"flex items-center h-8 shrink-0\",\n \"pt-11 pb-0 pr-[var(--spacing-2xl)] pl-9\"\n )}>\n {logoUrl ? (\n // Custom logo replaces entire lockup (icon + wordmark)\n <img \n src={logoUrl} \n alt=\"Logo\" \n className={`h-8 w-auto object-contain ${isMounted ? 'opacity-100' : 'opacity-0'}`}\n />\n ) : (\n // Logo creator: dynamic icon shape + Phosphor icon + wordmark\n // Uses CSS variables directly - no JavaScript resolution needed\n <div className=\"flex items-center\">\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 <span className={cn(\n \"font-semibold ml-[var(--spacing-md)]\",\n isDark ? \"text-white\" : \"text-[var(--canvas-text)]\"\n )} style={{ fontSize: \"var(--typo-body-xl-size)\" }}>\n {branding.wordmark || \"canvas\"}\n </span>\n </div>\n )}\n </div>\n\n {/* Navigation */}\n <ScrollArea className=\"flex-1 px-[var(--spacing-2xl)] pb-[var(--spacing-5xl)]\">\n <SidebarNav \n sections={sections} \n variant={variant} \n onItemClick={onItemClick}\n />\n </ScrollArea>\n </aside>\n );\n}\n\n// Re-export types for convenience\nexport type { NavSection, NavItem } from \"./sidebar-nav\";\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/standard-page-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { FlairBanner } from \"../blocks/flair-banner\";\nimport { PageHeaderSection } from \"../blocks/page-header-section\";\nimport { LineTab } from \"../ui/line-tabs\";\n\ninterface StandardPageShellProps {\n /** Flair banner title */\n bannerTitle?: string;\n /** Flair banner description */\n bannerDescription?: string;\n /** Whether to show the flair banner */\n showBanner?: boolean;\n /** Page title */\n pageTitle?: string;\n /** Page description */\n pageDescription?: string;\n /** Array of tab items */\n tabs?: LineTab[];\n /** ID of the currently active tab */\n activeTab?: string;\n /** Callback when a tab is clicked */\n onTabChange?: (tabId: string) => void;\n /** Whether to show the tabs */\n showTabs?: boolean;\n /** Whether to show the page header section */\n showPageHeader?: boolean;\n /** Main content - the modular blocks */\n children: React.ReactNode;\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 - Standard Page Shell\n * \n * A no-sidebar layout with:\n * - Fixed header with logo (no sidebar)\n * - Optional flair banner (dark blue hero section)\n * - Optional page header section with title, description, and tabs\n * - Centered content area (max-width 992px)\n * \n * @example\n * ```tsx\n * <StandardPageShell\n * bannerTitle=\"Large title\"\n * bannerDescription=\"Description\"\n * pageTitle=\"Page title\"\n * pageDescription=\"Description\"\n * tabs={[{ id: \"tab1\", label: \"Tab 1\" }]}\n * >\n * <ContentDropzone />\n * </StandardPageShell>\n * ```\n */\nexport function StandardPageShell({\n bannerTitle = \"Large title\",\n bannerDescription = \"Description\",\n showBanner = true,\n pageTitle = \"Page title\",\n pageDescription = \"Description\",\n tabs = [\n { id: \"tab1\", label: \"Tab 1\" },\n { id: \"tab2\", label: \"Tab 2\" },\n { id: \"tab3\", label: \"Tab 3\" },\n { id: \"tab4\", label: \"Tab 4\" },\n ],\n activeTab,\n onTabChange,\n showTabs = true,\n showPageHeader = true,\n children,\n onAppMenuClick,\n contentClassName,\n}: StandardPageShellProps) {\n // Sync CSS variables when rendered in iframes\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\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(--background)]\">\n {/* Header - Fixed at top with logo visible (no sidebar) */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Page Header Section - Banner + Title/Tabs */}\n {(showBanner || showPageHeader) && (\n <div className=\"flex flex-col w-full\">\n {/* Flair Banner */}\n {showBanner && (\n <FlairBanner title={bannerTitle} />\n )}\n\n {/* Page Title, Description, and Tabs */}\n {showPageHeader && (\n <PageHeaderSection\n title={pageTitle}\n description={pageDescription}\n tabs={tabs}\n activeTab={activeTab}\n onTabChange={onTabChange}\n showTabs={showTabs}\n />\n )}\n </div>\n )}\n\n {/* Main Content Area - Centered */}\n <main className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-6xl)]\",\n contentClassName\n )}\n >\n {children}\n </div>\n </main>\n </div>\n );\n}\n\n// Re-export types for convenience\nexport type { LineTab } from \"../ui/line-tabs\";\n\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { FlairBanner } from \"../blocks/flair-banner\";\nimport { PageHeaderSection } from \"../blocks/page-header-section\";\nimport { LineTab } from \"../ui/line-tabs\";\n\ninterface StandardPageShellProps {\n /** Flair banner title */\n bannerTitle?: string;\n /** Flair banner description */\n bannerDescription?: string;\n /** Whether to show the flair banner */\n showBanner?: boolean;\n /** Page title */\n pageTitle?: string;\n /** Page description */\n pageDescription?: string;\n /** Array of tab items */\n tabs?: LineTab[];\n /** ID of the currently active tab */\n activeTab?: string;\n /** Callback when a tab is clicked */\n onTabChange?: (tabId: string) => void;\n /** Whether to show the tabs */\n showTabs?: boolean;\n /** Whether to show the page header section */\n showPageHeader?: boolean;\n /** Main content - the modular blocks */\n children: React.ReactNode;\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 - Standard Page Shell\n * \n * A no-sidebar layout with:\n * - Fixed header with logo (no sidebar)\n * - Optional flair banner (dark blue hero section)\n * - Optional page header section with title, description, and tabs\n * - Centered content area (max-width 992px)\n * \n * @example\n * ```tsx\n * <StandardPageShell\n * bannerTitle=\"Large title\"\n * bannerDescription=\"Description\"\n * pageTitle=\"Page title\"\n * pageDescription=\"Description\"\n * tabs={[{ id: \"tab1\", label: \"Tab 1\" }]}\n * >\n * <ContentDropzone />\n * </StandardPageShell>\n * ```\n */\nexport function StandardPageShell({\n bannerTitle = \"Large title\",\n bannerDescription = \"Description\",\n showBanner = true,\n pageTitle = \"Page title\",\n pageDescription = \"Description\",\n tabs = [\n { id: \"tab1\", label: \"Tab 1\" },\n { id: \"tab2\", label: \"Tab 2\" },\n { id: \"tab3\", label: \"Tab 3\" },\n { id: \"tab4\", label: \"Tab 4\" },\n ],\n activeTab,\n onTabChange,\n showTabs = true,\n showPageHeader = true,\n children,\n onAppMenuClick,\n contentClassName,\n}: StandardPageShellProps) {\n // Sync CSS variables when rendered in iframes\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\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 with logo visible (no sidebar) */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Page Header Section - Banner + Title/Tabs */}\n {(showBanner || showPageHeader) && (\n <div className=\"flex flex-col w-full\">\n {/* Flair Banner */}\n {showBanner && (\n <FlairBanner title={bannerTitle} />\n )}\n\n {/* Page Title, Description, and Tabs */}\n {showPageHeader && (\n <PageHeaderSection\n title={pageTitle}\n description={pageDescription}\n tabs={tabs}\n activeTab={activeTab}\n onTabChange={onTabChange}\n showTabs={showTabs}\n />\n )}\n </div>\n )}\n\n {/* Main Content Area - Centered */}\n <main className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-6xl)]\",\n contentClassName\n )}\n >\n {children}\n </div>\n </main>\n </div>\n );\n}\n\n// Re-export types for convenience\nexport type { LineTab } from \"../ui/line-tabs\";\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/vertical-multistep-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { FlairBanner } from \"../blocks/flair-banner\";\nimport { VerticalStepTracker, defaultVerticalSteps } from \"../blocks/vertical-step-tracker\";\nimport { Step } from \"../blocks/step-tracker\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\ninterface VerticalMultistepShellProps {\n /** Array of step objects */\n steps?: Step[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Callback when a step is clicked */\n onStepClick?: (stepIndex: number) => void;\n /** Section title for the step tracker (e.g., \"REGISTRATION\") */\n sectionTitle?: string;\n /** Flair banner title */\n bannerTitle?: string;\n /** Flair banner description */\n bannerDescription?: string;\n /** Page title */\n pageTitle?: string;\n /** Page description */\n pageDescription?: string;\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel/Back is clicked */\n onCancel?: () => void;\n /** Callback when Continue/Submit is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Vertical Multistep Shell\n * \n * A layout for multi-step processes with vertical step tracker:\n * \n * **Desktop**: Two-column layout\n * - Left: Sidebar card (320px) with vertical step tracker\n * - Right: Content area with step title, description, content, and navigation\n * \n * **Mobile**: Stacked layout\n * - Header with hamburger menu\n * - Page title and description\n * - Vertical step list (no card)\n * - Content area with navigation\n * \n * @example\n * ```tsx\n * <VerticalMultistepShell\n * steps={defaultVerticalSteps}\n * currentStep={0}\n * sectionTitle=\"REGISTRATION\"\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </VerticalMultistepShell>\n * ```\n */\nexport function VerticalMultistepShell({\n steps = defaultVerticalSteps,\n currentStep = 0,\n onStepClick,\n sectionTitle = \"REGISTRATION\",\n bannerTitle = \"Large title\",\n bannerDescription = \"Description\",\n pageTitle = \"Page title\",\n pageDescription = \"Description\",\n children,\n onCancel,\n onContinue,\n cancelText,\n continueText,\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: VerticalMultistepShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep];\n const isFirstStep = currentStep === 0;\n const isLastStep = currentStep === steps.length - 1;\n\n return (\n <div className=\"min-h-screen bg-[var(--background)]\">\n {/* Header */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Flair Banner */}\n <FlairBanner title={bannerTitle} />\n\n {/* Page Header Section - Custom aligned to match sidebar content */}\n <div className=\"w-full border-b border-[var(--canvas-border)]\">\n <div\n className={cn(\n \"w-full max-w-[1200px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-6\",\n \"py-[var(--spacing-6xl)]\",\n \"flex flex-col gap-1\"\n )}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { FlairBanner } from \"../blocks/flair-banner\";\nimport { VerticalStepTracker, defaultVerticalSteps } from \"../blocks/vertical-step-tracker\";\nimport { Step } from \"../blocks/step-tracker\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\ninterface VerticalMultistepShellProps {\n /** Array of step objects */\n steps?: Step[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Callback when a step is clicked */\n onStepClick?: (stepIndex: number) => void;\n /** Section title for the step tracker (e.g., \"REGISTRATION\") */\n sectionTitle?: string;\n /** Flair banner title */\n bannerTitle?: string;\n /** Flair banner description */\n bannerDescription?: string;\n /** Page title */\n pageTitle?: string;\n /** Page description */\n pageDescription?: string;\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel/Back is clicked */\n onCancel?: () => void;\n /** Callback when Continue/Submit is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Vertical Multistep Shell\n * \n * A layout for multi-step processes with vertical step tracker:\n * \n * **Desktop**: Two-column layout\n * - Left: Sidebar card (320px) with vertical step tracker\n * - Right: Content area with step title, description, content, and navigation\n * \n * **Mobile**: Stacked layout\n * - Header with hamburger menu\n * - Page title and description\n * - Vertical step list (no card)\n * - Content area with navigation\n * \n * @example\n * ```tsx\n * <VerticalMultistepShell\n * steps={defaultVerticalSteps}\n * currentStep={0}\n * sectionTitle=\"REGISTRATION\"\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </VerticalMultistepShell>\n * ```\n */\nexport function VerticalMultistepShell({\n steps = defaultVerticalSteps,\n currentStep = 0,\n onStepClick,\n sectionTitle = \"REGISTRATION\",\n bannerTitle = \"Large title\",\n bannerDescription = \"Description\",\n pageTitle = \"Page title\",\n pageDescription = \"Description\",\n children,\n onCancel,\n onContinue,\n cancelText,\n continueText,\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: VerticalMultistepShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep];\n const isFirstStep = currentStep === 0;\n const isLastStep = currentStep === steps.length - 1;\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Flair Banner */}\n <FlairBanner title={bannerTitle} />\n\n {/* Page Header Section - Custom aligned to match sidebar content */}\n <div className=\"w-full border-b border-[var(--canvas-border)]\">\n <div\n className={cn(\n \"w-full max-w-[1200px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-6\",\n \"py-[var(--spacing-6xl)]\",\n \"flex flex-col gap-1\"\n )}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h4-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h4-size)\",\n fontWeight: \"var(--typo-h4-weight)\",\n letterSpacing: \"var(--typo-h4-spacing)\",\n lineHeight: \"var(--typo-h4-line-height)\",\n color: \"var(--typo-h4-color)\",\n }}\n >\n {pageTitle}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: \"var(--typo-body-m-weight)\",\n letterSpacing: \"var(--typo-body-m-spacing)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--typo-body-m-color-muted)\",\n }}\n >\n {pageDescription}\n </p>\n </div>\n </div>\n\n {/* Main Container */}\n <div \n className={cn(\n \"w-full max-w-[1200px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-5xl)]\",\n \"flex flex-col lg:flex-row gap-[var(--spacing-5xl)]\"\n )}\n >\n {/* Sidebar - Desktop: Card, Mobile: No card styling */}\n <div className=\"lg:shrink-0\">\n {/* Desktop: Card container */}\n <div \n className={cn(\n \"lg:w-[320px]\",\n // Desktop card styling\n \"lg:bg-[var(--canvas-background)] lg:border lg:border-[var(--canvas-border)]\",\n \"lg:rounded-[var(--radius-xl)]\",\n \"lg:p-6\"\n )}\n >\n <VerticalStepTracker\n steps={steps}\n currentStep={currentStep}\n onStepClick={onStepClick}\n title={sectionTitle}\n />\n </div>\n </div>\n\n {/* Content Area */}\n <div className={cn(\"flex-1 min-w-0\", contentClassName)}>\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData?.label || `Step ${currentStep + 1}`}\n </Typography>\n {currentStepData?.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText || (isFirstStep ? \"Cancel\" : \"Back\")}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText || (isLastStep ? \"Submit\" : \"Continue\")}\n </Button>\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
package/registry/ui/avatar.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/ui/avatar.tsx",
|
|
8
8
|
"type": "registry:ui",
|
|
9
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"../../lib/utils\"\n\nfunction Avatar({\n className,\n ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Root>) {\n return (\n <AvatarPrimitive.Root\n data-slot=\"avatar\"\n className={cn(\n \"relative flex size-8 shrink-0 overflow-hidden rounded-full\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction AvatarImage({\n className,\n ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Image>) {\n return (\n <AvatarPrimitive.Image\n data-slot=\"avatar-image\"\n className={cn(\"aspect-square size-full\", className)}\n {...props}\n />\n )\n}\n\nfunction AvatarFallback({\n className,\n ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {\n return (\n <AvatarPrimitive.Fallback\n data-slot=\"avatar-fallback\"\n className={cn(\n \"bg-
|
|
9
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"../../lib/utils\"\n\nfunction Avatar({\n className,\n ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Root>) {\n return (\n <AvatarPrimitive.Root\n data-slot=\"avatar\"\n className={cn(\n \"relative flex size-8 shrink-0 overflow-hidden rounded-full\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction AvatarImage({\n className,\n ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Image>) {\n return (\n <AvatarPrimitive.Image\n data-slot=\"avatar-image\"\n className={cn(\"aspect-square size-full\", className)}\n {...props}\n />\n )\n}\n\nfunction AvatarFallback({\n className,\n ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {\n return (\n <AvatarPrimitive.Fallback\n data-slot=\"avatar-fallback\"\n className={cn(\n \"bg-[var(--canvas-surface)] flex size-full items-center justify-center rounded-full\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Avatar, AvatarImage, AvatarFallback }\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
package/registry/ui/button.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/ui/button.tsx",
|
|
8
8
|
"type": "registry:ui",
|
|
9
|
-
"content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"../../lib/utils\"\n\n// Base button styles (layout and structure only, colors applied via CSS variables)\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n {\n variants: {\n variant: {\n // Custom Canvas variants (styled via CSS variables in component)\n primary: \"\",\n \"primary-outline\": \"border\",\n \"primary-neutral\": \"\",\n neutral: \"border\",\n \"neutral-delete\": \"border\",\n delete: \"\",\n // Legacy shadcn variants (for backwards compatibility)\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive: \"bg-destructive text-white hover:bg-destructive/90\",\n outline: \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n // Custom Canvas sizes (styled via CSS variables in component)\n mini: \"\",\n sm: \"\",\n default: \"\",\n lg: \"\",\n // Legacy shadcn sizes\n icon: \"size-9\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\n// CSS variable mappings for Canvas design system variants\nconst variantStyles: Record<string, React.CSSProperties> = {\n primary: {\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n },\n \"primary-outline\": {\n backgroundColor: \"var(--btn-primary-outline-bg)\",\n color: \"var(--btn-primary-outline-text)\",\n borderColor: \"var(--btn-primary-outline-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n },\n \"primary-neutral\": {\n backgroundColor: \"var(--btn-primary-neutral-bg)\",\n color: \"var(--btn-primary-neutral-text)\",\n borderColor: \"var(--btn-primary-neutral-border)\",\n },\n neutral: {\n backgroundColor: \"var(--btn-neutral-bg)\",\n color: \"var(--btn-neutral-text)\",\n borderColor: \"var(--btn-neutral-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n },\n \"neutral-delete\": {\n backgroundColor: \"var(--btn-neutral-delete-bg)\",\n color: \"var(--btn-neutral-delete-text)\",\n borderColor: \"var(--btn-neutral-delete-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n },\n delete: {\n backgroundColor: \"var(--btn-delete-bg)\",\n color: \"var(--btn-delete-text)\",\n borderColor: \"var(--btn-delete-border)\",\n },\n}\n\n// CSS variable mappings for Canvas design system sizes\nconst sizeStyles: Record<string, React.CSSProperties> = {\n mini: {\n height: \"var(--btn-mini-height)\",\n paddingLeft: \"var(--btn-mini-px)\",\n paddingRight: \"var(--btn-mini-px)\",\n fontSize: \"var(--btn-mini-font-size)\",\n borderRadius: \"var(--btn-mini-radius)\",\n fontWeight: \"var(--btn-mini-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-mini-letter-spacing)\",\n fontFamily: \"var(--btn-mini-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n sm: {\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n fontWeight: \"var(--btn-small-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-small-letter-spacing)\",\n fontFamily: \"var(--btn-small-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n default: {\n height: \"var(--btn-standard-height)\",\n paddingLeft: \"var(--btn-standard-px)\",\n paddingRight: \"var(--btn-standard-px)\",\n fontSize: \"var(--btn-standard-font-size)\",\n borderRadius: \"var(--btn-standard-radius)\",\n fontWeight: \"var(--btn-standard-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-standard-letter-spacing)\",\n fontFamily: \"var(--btn-standard-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n lg: {\n height: \"var(--btn-large-height)\",\n paddingLeft: \"var(--btn-large-px)\",\n paddingRight: \"var(--btn-large-px)\",\n fontSize: \"var(--btn-large-font-size)\",\n borderRadius: \"var(--btn-large-radius)\",\n fontWeight: \"var(--btn-large-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-large-letter-spacing)\",\n fontFamily: \"var(--btn-large-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n}\n\n// Custom Canvas variants that use CSS variables\nconst canvasVariants = [\"primary\", \"primary-outline\", \"primary-neutral\", \"neutral\", \"neutral-delete\", \"delete\"]\nconst canvasSizes = [\"mini\", \"sm\", \"default\", \"lg\"]\n\nfunction Button({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n style,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean\n }) {\n const Comp = asChild ? Slot : \"button\"\n\n // Build style object with CSS variables for Canvas variants/sizes\n const computedStyle: React.CSSProperties = { ...style }\n \n if (variant && canvasVariants.includes(variant)) {\n Object.assign(computedStyle, variantStyles[variant])\n }\n \n if (size && canvasSizes.includes(size)) {\n Object.assign(computedStyle, sizeStyles[size])\n }\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size, className }))}\n style={Object.keys(computedStyle).length > 0 ? computedStyle : undefined}\n {...props}\n />\n )\n}\n\nexport { Button, buttonVariants }\n"
|
|
9
|
+
"content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"../../lib/utils\"\n\n// Base button styles (layout and structure only, colors applied via CSS variables)\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n {\n variants: {\n variant: {\n // Custom Canvas variants (styled via CSS variables in component)\n primary: \"\",\n \"primary-outline\": \"border\",\n \"primary-neutral\": \"\",\n neutral: \"border\",\n \"neutral-delete\": \"border\",\n delete: \"\",\n // Legacy shadcn variants (for backwards compatibility)\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive: \"bg-destructive text-white hover:bg-destructive/90\",\n outline: \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n // Custom Canvas sizes (styled via CSS variables in component)\n mini: \"\",\n sm: \"\",\n default: \"\",\n lg: \"\",\n // Legacy shadcn sizes\n icon: \"size-9\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\n// CSS variable mappings for Canvas design system variants\nconst variantStyles: Record<string, React.CSSProperties> = {\n primary: {\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n },\n \"primary-outline\": {\n backgroundColor: \"var(--btn-primary-outline-bg)\",\n color: \"var(--btn-primary-outline-text)\",\n borderColor: \"var(--btn-primary-outline-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n },\n \"primary-neutral\": {\n backgroundColor: \"var(--btn-primary-neutral-bg)\",\n color: \"var(--btn-primary-neutral-text)\",\n borderColor: \"var(--btn-primary-neutral-border)\",\n },\n neutral: {\n backgroundColor: \"var(--btn-neutral-bg)\",\n color: \"var(--btn-neutral-text)\",\n borderColor: \"var(--btn-neutral-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n },\n \"neutral-delete\": {\n backgroundColor: \"var(--btn-neutral-delete-bg)\",\n color: \"var(--btn-neutral-delete-text)\",\n borderColor: \"var(--btn-neutral-delete-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n },\n delete: {\n backgroundColor: \"var(--btn-delete-bg)\",\n color: \"var(--btn-delete-text)\",\n borderColor: \"var(--btn-delete-border)\",\n },\n}\n\n// CSS variable mappings for Canvas design system sizes\nconst sizeStyles: Record<string, React.CSSProperties> = {\n mini: {\n height: \"var(--btn-mini-height)\",\n paddingLeft: \"var(--btn-mini-px)\",\n paddingRight: \"var(--btn-mini-px)\",\n fontSize: \"var(--btn-mini-font-size)\",\n borderRadius: \"var(--btn-mini-radius)\",\n fontWeight: \"var(--btn-mini-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-mini-letter-spacing)\",\n fontFamily: \"var(--btn-mini-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n sm: {\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n fontWeight: \"var(--btn-small-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-small-letter-spacing)\",\n fontFamily: \"var(--btn-small-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n default: {\n height: \"var(--btn-standard-height)\",\n paddingLeft: \"var(--btn-standard-px)\",\n paddingRight: \"var(--btn-standard-px)\",\n fontSize: \"var(--btn-standard-font-size)\",\n borderRadius: \"var(--btn-standard-radius)\",\n fontWeight: \"var(--btn-standard-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-standard-letter-spacing)\",\n fontFamily: \"var(--btn-standard-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n lg: {\n height: \"var(--btn-large-height)\",\n paddingLeft: \"var(--btn-large-px)\",\n paddingRight: \"var(--btn-large-px)\",\n fontSize: \"var(--btn-large-font-size)\",\n borderRadius: \"var(--btn-large-radius)\",\n fontWeight: \"var(--btn-large-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-large-letter-spacing)\",\n fontFamily: \"var(--btn-large-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n}\n\n// Custom Canvas variants that use CSS variables\nconst canvasVariants = [\"primary\", \"primary-outline\", \"primary-neutral\", \"neutral\", \"neutral-delete\", \"delete\"]\nconst canvasSizes = [\"mini\", \"sm\", \"default\", \"lg\"]\n\nfunction Button({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n style,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean\n }) {\n const Comp = asChild ? Slot : \"button\"\n\n // Build style object with CSS variables for Canvas variants/sizes\n const computedStyle: React.CSSProperties = { ...style }\n \n if (variant && canvasVariants.includes(variant)) {\n Object.assign(computedStyle, variantStyles[variant])\n }\n \n if (size && canvasSizes.includes(size)) {\n Object.assign(computedStyle, sizeStyles[size])\n }\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size, className }))}\n style={Object.keys(computedStyle).length > 0 ? computedStyle : undefined}\n {...props}\n />\n )\n}\n\nexport { Button, buttonVariants }\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/ui/checkbox.tsx",
|
|
8
8
|
"type": "registry:ui",
|
|
9
|
-
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { Check } from \"lucide-react\";\n\nimport { cn } from \"../../lib/utils\";\n\nconst Checkbox = React.forwardRef<\n React.ComponentRef<typeof CheckboxPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <CheckboxPrimitive.Root\n ref={ref}\n className={cn(\n \"peer size-4 shrink-0 rounded-[var(--radius-xs)] border border-[var(--canvas-border-input)]\",\n \"bg-
|
|
9
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { Check } from \"lucide-react\";\n\nimport { cn } from \"../../lib/utils\";\n\nconst Checkbox = React.forwardRef<\n React.ComponentRef<typeof CheckboxPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <CheckboxPrimitive.Root\n ref={ref}\n className={cn(\n \"peer size-4 shrink-0 rounded-[var(--radius-xs)] border border-[var(--canvas-border-input)]\",\n \"bg-[var(--canvas-background)] transition-colors\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--canvas-border-input-focus)] focus-visible:ring-offset-2\",\n \"disabled:cursor-not-allowed disabled:bg-[var(--canvas-input-disabled-bg)] disabled:border-[var(--canvas-input-disabled-border)]\",\n \"data-[state=checked]:bg-[var(--canvas-primary)] data-[state=checked]:border-[var(--canvas-primary)]\",\n \"data-[state=checked]:disabled:bg-[var(--canvas-input-disabled-text)] data-[state=checked]:disabled:border-[var(--canvas-input-disabled-border)]\",\n className\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n className={cn(\"flex items-center justify-center text-[var(--canvas-primary-foreground)]\")}\n >\n <Check className=\"size-3\" strokeWidth={2.5} />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n));\nCheckbox.displayName = CheckboxPrimitive.Root.displayName;\n\ninterface CheckboxWithLabelProps\n extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> {\n children: React.ReactNode;\n size?: \"default\" | \"sm\";\n}\n\nconst CheckboxWithLabel = React.forwardRef<\n React.ComponentRef<typeof CheckboxPrimitive.Root>,\n CheckboxWithLabelProps\n>(({ children, className, id, size = \"default\", ...props }, ref) => {\n const generatedId = React.useId();\n const checkboxId = id || generatedId;\n\n const isSmall = size === \"sm\";\n\n return (\n <div className={cn(\"flex items-center\", isSmall ? \"gap-1.5\" : \"gap-2\")}>\n <Checkbox \n ref={ref} \n id={checkboxId} \n className={cn(isSmall && \"size-3.5\", className)} \n {...props} \n />\n <label\n htmlFor={checkboxId}\n className={cn(\n \"cursor-pointer select-none\",\n isSmall ? \"text-xs text-[var(--canvas-text-muted)]\" : \"text-[var(--canvas-text)]\"\n )}\n style={!isSmall ? { fontSize: \"var(--input-standard-font-size)\" } : undefined}\n >\n {children}\n </label>\n </div>\n );\n});\nCheckboxWithLabel.displayName = \"CheckboxWithLabel\";\n\nexport { Checkbox, CheckboxWithLabel };\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/ui/date-input.tsx",
|
|
8
8
|
"type": "registry:ui",
|
|
9
|
-
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Calendar } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface DateInputProps\n extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\" | \"onChange\"> {\n inputSize?: \"sm\" | \"default\" | \"lg\";\n value?: string;\n onChange?: (value: string) => void;\n}\n\nconst DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(\n ({ className, inputSize = \"default\", placeholder = \"mm/dd/yyyy\", disabled, value, onChange, ...props }, ref) => {\n const sizeVar = inputSize === \"default\" ? \"standard\" : inputSize === \"sm\" ? \"small\" : \"large\";\n\n return (\n <div\n className={cn(\n \"flex items-center gap-2 bg-
|
|
9
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Calendar } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface DateInputProps\n extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\" | \"onChange\"> {\n inputSize?: \"sm\" | \"default\" | \"lg\";\n value?: string;\n onChange?: (value: string) => void;\n}\n\nconst DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(\n ({ className, inputSize = \"default\", placeholder = \"mm/dd/yyyy\", disabled, value, onChange, ...props }, ref) => {\n const sizeVar = inputSize === \"default\" ? \"standard\" : inputSize === \"sm\" ? \"small\" : \"large\";\n\n return (\n <div\n className={cn(\n \"flex items-center gap-2 bg-[var(--canvas-background)] border border-[var(--canvas-border-input)]\",\n \"focus-within:border-[var(--canvas-border-input-focus)] focus-within:ring-2 focus-within:ring-[var(--canvas-border-input-focus)] focus-within:ring-offset-2\",\n \"has-[:disabled]:cursor-not-allowed has-[:disabled]:bg-[var(--canvas-input-disabled-bg)] has-[:disabled]:border-[var(--canvas-input-disabled-border)]\",\n className\n )}\n style={{\n height: `var(--input-${sizeVar}-height)`,\n paddingLeft: `var(--input-${sizeVar}-px)`,\n paddingRight: `var(--input-${sizeVar}-px)`,\n borderRadius: `var(--input-${sizeVar}-radius)`,\n }}\n >\n <Calendar className=\"size-4 shrink-0 text-[var(--canvas-text-muted)]\" />\n <input\n type=\"text\"\n ref={ref}\n placeholder={placeholder}\n disabled={disabled}\n value={value}\n onChange={(e) => onChange?.(e.target.value)}\n className={cn(\n \"flex-1 bg-transparent outline-none\",\n \"text-[var(--canvas-text)] placeholder:text-[var(--canvas-text-placeholder)]\",\n \"disabled:text-[var(--canvas-input-disabled-text)] disabled:cursor-not-allowed\"\n )}\n style={{\n fontSize: `var(--input-${sizeVar}-font-size)`,\n }}\n {...props}\n />\n </div>\n );\n }\n);\nDateInput.displayName = \"DateInput\";\n\nexport { DateInput };\n\n\n\n\n\n\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/ui/image-uploader.tsx",
|
|
8
8
|
"type": "registry:ui",
|
|
9
|
-
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Upload, ChevronLeft, ChevronRight, Trash2 } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface UploadedImage {\n id: string;\n url: string;\n name?: string;\n}\n\nexport interface ImageUploaderProps {\n images?: UploadedImage[];\n selectedImageId?: string;\n onImagesChange?: (images: UploadedImage[]) => void;\n onSelectImage?: (imageId: string) => void;\n placeholder?: string;\n disabled?: boolean;\n className?: string;\n}\n\nconst ImageUploader = React.forwardRef<HTMLDivElement, ImageUploaderProps>(\n (\n {\n images = [],\n selectedImageId,\n onImagesChange,\n onSelectImage,\n placeholder = \"Drop image here\",\n disabled,\n className,\n },\n ref\n ) => {\n const fileInputRef = React.useRef<HTMLInputElement>(null);\n const [isDragging, setIsDragging] = React.useState(false);\n\n const handleDragOver = (e: React.DragEvent) => {\n e.preventDefault();\n if (!disabled) setIsDragging(true);\n };\n\n const handleDragLeave = (e: React.DragEvent) => {\n e.preventDefault();\n setIsDragging(false);\n };\n\n const handleDrop = (e: React.DragEvent) => {\n e.preventDefault();\n setIsDragging(false);\n if (disabled) return;\n\n const files = Array.from(e.dataTransfer.files).filter((file) =>\n file.type.startsWith(\"image/\")\n );\n handleFiles(files);\n };\n\n const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (disabled || !e.target.files) return;\n const files = Array.from(e.target.files);\n handleFiles(files);\n e.target.value = \"\";\n };\n\n const handleFiles = (files: File[]) => {\n const newImages: UploadedImage[] = files.map((file) => ({\n id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n url: URL.createObjectURL(file),\n name: file.name,\n }));\n\n onImagesChange?.([...images, ...newImages]);\n };\n\n const handleDelete = (imageId: string) => {\n if (disabled) return;\n const newImages = images.filter((img) => img.id !== imageId);\n onImagesChange?.(newImages);\n };\n\n const handleMoveLeft = (imageId: string) => {\n if (disabled) return;\n const index = images.findIndex((img) => img.id === imageId);\n if (index > 0) {\n const newImages = [...images];\n [newImages[index - 1], newImages[index]] = [\n newImages[index],\n newImages[index - 1],\n ];\n onImagesChange?.(newImages);\n }\n };\n\n const handleMoveRight = (imageId: string) => {\n if (disabled) return;\n const index = images.findIndex((img) => img.id === imageId);\n if (index < images.length - 1) {\n const newImages = [...images];\n [newImages[index], newImages[index + 1]] = [\n newImages[index + 1],\n newImages[index],\n ];\n onImagesChange?.(newImages);\n }\n };\n\n return (\n <div\n ref={ref}\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n {/* Dropzone */}\n <div\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n onClick={() => !disabled && fileInputRef.current?.click()}\n onKeyDown={(e) =>\n e.key === \"Enter\" && !disabled && fileInputRef.current?.click()\n }\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n className={cn(\n \"flex flex-col items-center justify-center w-full border border-dashed rounded-[var(--radius-xs)] cursor-pointer transition-colors\",\n isDragging\n ? \"border-[var(--canvas-primary)] bg-[var(--canvas-surface-brand)]\"\n : \"border-[var(--canvas-border-input)] bg-[var(--canvas-surface)]\",\n disabled &&\n \"cursor-not-allowed bg-[var(--canvas-input-disabled-bg)] border-[var(--canvas-input-disabled-border)] opacity-60\"\n )}\n style={{\n height: \"var(--input-expandable-height)\",\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-2xl)\",\n paddingBottom: \"var(--spacing-2xl)\",\n gap: \"var(--spacing-xs)\",\n }}\n >\n <Upload\n className=\"text-[var(--canvas-text-muted)]\"\n style={{ width: \"32px\", height: \"32px\" }}\n />\n <p\n className=\"text-[var(--canvas-text-placeholder)] text-center\"\n style={{\n fontFamily: \"var(--typo-body-m-font)\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n }}\n >\n {placeholder}\n </p>\n </div>\n\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n onChange={handleFileSelect}\n disabled={disabled}\n className=\"hidden\"\n />\n\n {/* Uploaded images gallery */}\n {images.length > 0 && (\n <div\n className=\"flex items-start w-full overflow-x-auto\"\n style={{\n gap: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-xl)\",\n }}\n >\n {images.map((image, index) => {\n const isSelected = selectedImageId === image.id;\n const isFirst = index === 0;\n const isLast = index === images.length - 1;\n\n return (\n <div\n key={image.id}\n className={cn(\n \"flex flex-col shrink-0 rounded-[var(--radius-xs)] border overflow-hidden\",\n isSelected\n ? \"border-[var(--canvas-primary)]\"\n : \"border-[var(--canvas-border)]\"\n )}\n onClick={() => onSelectImage?.(image.id)}\n >\n {/* Image preview */}\n <div\n className=\"relative overflow-hidden\"\n style={{\n width: \"80px\",\n height: \"92px\",\n padding: \"var(--spacing-md)\",\n }}\n >\n <img\n src={image.url}\n alt={image.name || \"Uploaded image\"}\n className=\"w-full h-full object-cover rounded-sm\"\n />\n </div>\n\n {/* Controls */}\n <div\n className=\"flex items-center justify-end border-t border-[var(--canvas-border)]\"\n style={{\n gap: \"var(--spacing-xs)\",\n paddingLeft: \"var(--spacing-md)\",\n paddingRight: \"var(--spacing-md)\",\n paddingTop: \"var(--spacing-sm)\",\n paddingBottom: \"var(--spacing-sm)\",\n }}\n >\n {/* Move left */}\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n handleMoveLeft(image.id);\n }}\n disabled={disabled || isFirst}\n className={cn(\n \"flex items-center justify-center rounded-full transition-colors\",\n isFirst || disabled\n ? \"bg-[var(--canvas-surface)] border border-[var(--canvas-border)] text-[var(--canvas-text-muted)]\"\n : \"bg-[var(--canvas-text)] text-white\"\n )}\n style={{ width: \"24px\", height: \"24px\" }}\n >\n <ChevronLeft style={{ width: \"16px\", height: \"16px\" }} />\n </button>\n\n {/* Move right */}\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n handleMoveRight(image.id);\n }}\n disabled={disabled || isLast}\n className={cn(\n \"flex items-center justify-center rounded-full transition-colors\",\n isLast || disabled\n ? \"bg-[var(--canvas-surface)] border border-[var(--canvas-border)] text-[var(--canvas-text-muted)]\"\n : \"bg-[var(--canvas-text)] text-white\"\n )}\n style={{ width: \"24px\", height: \"24px\" }}\n >\n <ChevronRight style={{ width: \"16px\", height: \"16px\" }} />\n </button>\n\n {/* Delete */}\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n handleDelete(image.id);\n }}\n disabled={disabled}\n className=\"flex items-center justify-center rounded-full bg-[var(--canvas-text)] text-white transition-colors\"\n style={{ width: \"24px\", height: \"24px\" }}\n >\n <Trash2 style={{ width: \"16px\", height: \"16px\" }} />\n </button>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n }\n);\nImageUploader.displayName = \"ImageUploader\";\n\nexport { ImageUploader };\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Upload, ChevronLeft, ChevronRight, Trash2 } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface UploadedImage {\n id: string;\n url: string;\n name?: string;\n}\n\nexport interface ImageUploaderProps {\n images?: UploadedImage[];\n selectedImageId?: string;\n onImagesChange?: (images: UploadedImage[]) => void;\n onSelectImage?: (imageId: string) => void;\n placeholder?: string;\n disabled?: boolean;\n className?: string;\n}\n\nconst ImageUploader = React.forwardRef<HTMLDivElement, ImageUploaderProps>(\n (\n {\n images = [],\n selectedImageId,\n onImagesChange,\n onSelectImage,\n placeholder = \"Drop image here\",\n disabled,\n className,\n },\n ref\n ) => {\n const fileInputRef = React.useRef<HTMLInputElement>(null);\n const [isDragging, setIsDragging] = React.useState(false);\n\n const handleDragOver = (e: React.DragEvent) => {\n e.preventDefault();\n if (!disabled) setIsDragging(true);\n };\n\n const handleDragLeave = (e: React.DragEvent) => {\n e.preventDefault();\n setIsDragging(false);\n };\n\n const handleDrop = (e: React.DragEvent) => {\n e.preventDefault();\n setIsDragging(false);\n if (disabled) return;\n\n const files = Array.from(e.dataTransfer.files).filter((file) =>\n file.type.startsWith(\"image/\")\n );\n handleFiles(files);\n };\n\n const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (disabled || !e.target.files) return;\n const files = Array.from(e.target.files);\n handleFiles(files);\n e.target.value = \"\";\n };\n\n const handleFiles = (files: File[]) => {\n const newImages: UploadedImage[] = files.map((file) => ({\n id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n url: URL.createObjectURL(file),\n name: file.name,\n }));\n\n onImagesChange?.([...images, ...newImages]);\n };\n\n const handleDelete = (imageId: string) => {\n if (disabled) return;\n const newImages = images.filter((img) => img.id !== imageId);\n onImagesChange?.(newImages);\n };\n\n const handleMoveLeft = (imageId: string) => {\n if (disabled) return;\n const index = images.findIndex((img) => img.id === imageId);\n if (index > 0) {\n const newImages = [...images];\n [newImages[index - 1], newImages[index]] = [\n newImages[index],\n newImages[index - 1],\n ];\n onImagesChange?.(newImages);\n }\n };\n\n const handleMoveRight = (imageId: string) => {\n if (disabled) return;\n const index = images.findIndex((img) => img.id === imageId);\n if (index < images.length - 1) {\n const newImages = [...images];\n [newImages[index], newImages[index + 1]] = [\n newImages[index + 1],\n newImages[index],\n ];\n onImagesChange?.(newImages);\n }\n };\n\n return (\n <div\n ref={ref}\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n {/* Dropzone */}\n <div\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n onClick={() => !disabled && fileInputRef.current?.click()}\n onKeyDown={(e) =>\n e.key === \"Enter\" && !disabled && fileInputRef.current?.click()\n }\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n className={cn(\n \"flex flex-col items-center justify-center w-full border border-dashed rounded-[var(--radius-xs)] cursor-pointer transition-colors\",\n isDragging\n ? \"border-[var(--canvas-primary)] bg-[var(--canvas-surface-brand)]\"\n : \"border-[var(--canvas-border-input)] bg-[var(--canvas-surface)]\",\n disabled &&\n \"cursor-not-allowed bg-[var(--canvas-input-disabled-bg)] border-[var(--canvas-input-disabled-border)] opacity-60\"\n )}\n style={{\n height: \"var(--input-expandable-height)\",\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-2xl)\",\n paddingBottom: \"var(--spacing-2xl)\",\n gap: \"var(--spacing-xs)\",\n }}\n >\n <Upload\n className=\"text-[var(--canvas-text-muted)]\"\n style={{ width: \"32px\", height: \"32px\" }}\n />\n <p\n className=\"text-[var(--canvas-text-placeholder)] text-center\"\n style={{\n fontFamily: \"var(--typo-body-m-font)\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n }}\n >\n {placeholder}\n </p>\n </div>\n\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n onChange={handleFileSelect}\n disabled={disabled}\n className=\"hidden\"\n />\n\n {/* Uploaded images gallery */}\n {images.length > 0 && (\n <div\n className=\"flex items-start w-full overflow-x-auto\"\n style={{\n gap: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-xl)\",\n }}\n >\n {images.map((image, index) => {\n const isSelected = selectedImageId === image.id;\n const isFirst = index === 0;\n const isLast = index === images.length - 1;\n\n return (\n <div\n key={image.id}\n className={cn(\n \"flex flex-col shrink-0 rounded-[var(--radius-xs)] border overflow-hidden\",\n isSelected\n ? \"border-[var(--canvas-primary)]\"\n : \"border-[var(--canvas-border)]\"\n )}\n onClick={() => onSelectImage?.(image.id)}\n >\n {/* Image preview */}\n <div\n className=\"relative overflow-hidden\"\n style={{\n width: \"80px\",\n height: \"92px\",\n padding: \"var(--spacing-md)\",\n }}\n >\n <img\n src={image.url}\n alt={image.name || \"Uploaded image\"}\n className=\"w-full h-full object-cover rounded-sm\"\n />\n </div>\n\n {/* Controls */}\n <div\n className=\"flex items-center justify-end border-t border-[var(--canvas-border)]\"\n style={{\n gap: \"var(--spacing-xs)\",\n paddingLeft: \"var(--spacing-md)\",\n paddingRight: \"var(--spacing-md)\",\n paddingTop: \"var(--spacing-sm)\",\n paddingBottom: \"var(--spacing-sm)\",\n }}\n >\n {/* Move left */}\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n handleMoveLeft(image.id);\n }}\n disabled={disabled || isFirst}\n className={cn(\n \"flex items-center justify-center rounded-full transition-colors\",\n isFirst || disabled\n ? \"bg-[var(--canvas-surface)] border border-[var(--canvas-border)] text-[var(--canvas-text-muted)]\"\n : \"bg-[var(--canvas-text)] text-[var(--canvas-primary-foreground)]\"\n )}\n style={{ width: \"24px\", height: \"24px\" }}\n >\n <ChevronLeft style={{ width: \"16px\", height: \"16px\" }} />\n </button>\n\n {/* Move right */}\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n handleMoveRight(image.id);\n }}\n disabled={disabled || isLast}\n className={cn(\n \"flex items-center justify-center rounded-full transition-colors\",\n isLast || disabled\n ? \"bg-[var(--canvas-surface)] border border-[var(--canvas-border)] text-[var(--canvas-text-muted)]\"\n : \"bg-[var(--canvas-text)] text-[var(--canvas-primary-foreground)]\"\n )}\n style={{ width: \"24px\", height: \"24px\" }}\n >\n <ChevronRight style={{ width: \"16px\", height: \"16px\" }} />\n </button>\n\n {/* Delete */}\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n handleDelete(image.id);\n }}\n disabled={disabled}\n className=\"flex items-center justify-center rounded-full bg-[var(--canvas-text)] text-[var(--canvas-primary-foreground)] transition-colors\"\n style={{ width: \"24px\", height: \"24px\" }}\n >\n <Trash2 style={{ width: \"16px\", height: \"16px\" }} />\n </button>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n }\n);\nImageUploader.displayName = \"ImageUploader\";\n\nexport { ImageUploader };\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/ui/line-tabs.tsx",
|
|
8
8
|
"type": "registry:ui",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface LineTab {\n id: string;\n label: string;\n}\n\ninterface LineTabsProps {\n /** Array of tab items */\n tabs: LineTab[];\n /** ID of the currently active tab */\n activeTab?: string;\n /** Callback when a tab is clicked */\n onTabChange?: (tabId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Line Tabs Component\n * \n * Horizontal underline-style tabs for page navigation.\n * Active tab has blue text with 2px blue bottom border.\n * Inactive tabs have gray text with 1px gray bottom border.\n * \n * @example\n * ```tsx\n * <LineTabs \n * tabs={[\n * { id: \"tab1\", label: \"Tab 1\" },\n * { id: \"tab2\", label: \"Tab 2\" },\n * ]} \n * activeTab=\"tab1\"\n * onTabChange={(id) => console.log(id)}\n * />\n * ```\n */\nexport function LineTabs({\n tabs,\n activeTab: controlledActiveTab,\n onTabChange,\n className,\n}: LineTabsProps) {\n // Use internal state if not controlled\n const [internalActiveTab, setInternalActiveTab] = useState(tabs[0]?.id || \"\");\n const activeTab = controlledActiveTab ?? internalActiveTab;\n\n const handleTabClick = (tabId: string) => {\n if (!controlledActiveTab) {\n setInternalActiveTab(tabId);\n }\n onTabChange?.(tabId);\n };\n\n return (\n <div className={cn(\"flex items-end w-full\", className)}>\n {tabs.map((tab, index) => {\n const isActive = tab.id === activeTab;\n const isLast = index === tabs.length - 1;\n\n return (\n <div key={tab.id} className=\"flex items-end h-12\">\n {/* Tab Button */}\n <button\n onClick={() => handleTabClick(tab.id)}\n className={cn(\n \"flex items-center justify-center h-full px-0 transition-colors\",\n isActive\n ? \"border-b-2 border-[var(--canvas-primary)] text-[var(--canvas-primary)]\"\n : \"border-b border-[var(--canvas-border)] text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)]\"\n )}\n >\n <span className=\"text-base font-medium leading-6\">\n {tab.label}\n </span>\n </button>\n\n {/* Divider - 24px wide line */}\n {!isLast && (\n <div className=\"w-6 h-0 border-b border-[var(--canvas-border)]\" />\n )}\n </div>\n );\n })}\n\n {/* Trailing line to fill remaining space */}\n <div className=\"flex-1 h-0 border-b border-[var(--canvas-border)]\" />\n </div>\n );\n}\n\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface LineTab {\n id: string;\n label: string;\n}\n\ninterface LineTabsProps {\n /** Array of tab items */\n tabs: LineTab[];\n /** ID of the currently active tab */\n activeTab?: string;\n /** Callback when a tab is clicked */\n onTabChange?: (tabId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Line Tabs Component\n * \n * Horizontal underline-style tabs for page navigation.\n * Active tab has blue text with 2px blue bottom border.\n * Inactive tabs have gray text with 1px gray bottom border.\n * \n * @example\n * ```tsx\n * <LineTabs \n * tabs={[\n * { id: \"tab1\", label: \"Tab 1\" },\n * { id: \"tab2\", label: \"Tab 2\" },\n * ]} \n * activeTab=\"tab1\"\n * onTabChange={(id) => console.log(id)}\n * />\n * ```\n */\nexport function LineTabs({\n tabs,\n activeTab: controlledActiveTab,\n onTabChange,\n className,\n}: LineTabsProps) {\n // Use internal state if not controlled\n const [internalActiveTab, setInternalActiveTab] = useState(tabs[0]?.id || \"\");\n const activeTab = controlledActiveTab ?? internalActiveTab;\n\n const handleTabClick = (tabId: string) => {\n if (!controlledActiveTab) {\n setInternalActiveTab(tabId);\n }\n onTabChange?.(tabId);\n };\n\n return (\n <div className={cn(\"flex items-end w-full\", className)}>\n {tabs.map((tab, index) => {\n const isActive = tab.id === activeTab;\n const isLast = index === tabs.length - 1;\n\n return (\n <div key={tab.id} className=\"flex items-end h-12\">\n {/* Tab Button */}\n <button\n onClick={() => handleTabClick(tab.id)}\n className={cn(\n \"flex items-center justify-center h-full px-0 transition-colors cursor-pointer\",\n isActive\n ? \"border-b-2 border-[var(--canvas-primary)] text-[var(--canvas-primary)]\"\n : \"border-b border-[var(--canvas-border)] text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)]\"\n )}\n >\n <span className=\"text-base font-medium leading-6\">\n {tab.label}\n </span>\n </button>\n\n {/* Divider - 24px wide line */}\n {!isLast && (\n <div className=\"w-6 h-0 border-b border-[var(--canvas-border)]\" />\n )}\n </div>\n );\n })}\n\n {/* Trailing line to fill remaining space */}\n <div className=\"flex-1 h-0 border-b border-[var(--canvas-border)]\" />\n </div>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|