canvas-ui-sdk 4.0.2 → 4.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +38 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/registry/blocks/category-grid.json +1 -1
- package/registry/blocks/confirmation-popup.json +1 -1
- package/registry/blocks/contact-form-popup.json +1 -1
- package/registry/blocks/details-popup.json +1 -1
- package/registry/blocks/feedback-popup.json +1 -1
- package/registry/blocks/form-popup.json +1 -1
- package/registry/blocks/image-popup.json +1 -1
- package/registry/blocks/invoice-popup.json +1 -1
- package/registry/blocks/list-popup.json +1 -1
- package/registry/blocks/multistep-form-popup.json +1 -1
- package/registry/blocks/nps-survey-popup.json +1 -1
- package/registry/blocks/page-previews.json +1 -1
- package/registry/blocks/persona-card.json +1 -1
- package/registry/blocks/personalize-feed-popup.json +1 -1
- package/registry/blocks/pricing-plans-popup.json +1 -1
- package/registry/blocks/purchase-confirmation-popup.json +1 -1
- package/registry/blocks/share-project-popup.json +1 -1
- package/registry/blocks/small-edit-popup.json +1 -1
- package/registry/blocks/terms-of-service-popup.json +1 -1
- package/registry/blocks/video-playlist.json +1 -1
- package/registry/blocks/video-popup.json +1 -1
- package/registry/blocks/view-profile-popup.json +1 -1
- package/registry/layout/dashboard-shell.json +1 -1
- package/registry/layout/double-sidebar-shell.json +1 -1
- package/registry/layout/double-sidebar.json +1 -1
- package/registry/layout/icon-sidebar-shell.json +1 -1
- package/registry/ui/dropdown-menu.json +1 -1
- package/registry/ui/popover.json +1 -1
- package/registry/ui/select.json +1 -1
- package/styles/tokens.reference.css +7 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/page-previews.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { Header } from \"../layout\";\nimport { ContentDropzone } from \"./content-dropzone\";\nimport { LoginBrandingPanel } from \"./login-branding-panel\";\nimport { MessengerSidebar } from \"./messenger-sidebar\";\nimport { VideoChatControls } from \"./video-chat-controls\";\nimport {\n DashboardShell,\n IconSidebarShell,\n DoubleSidebarShell,\n StandardPageShell,\n MobileMenuShell,\n SearchBarShell,\n MultistepShell,\n MultistepSidebarShell,\n MultistepProgressBarShell,\n VerticalMultistepShell,\n AccountSettingsShell,\n} from \"../layout\";\n\n/**\n * Scaled Preview Wrapper\n * Renders content at a scaled-down size for canvas thumbnails\n */\ninterface ScaledPreviewProps {\n children: React.ReactNode;\n width?: number;\n height?: number;\n scale?: number;\n}\n\nexport function ScaledPreview({\n children,\n width = 400,\n height = 280,\n scale = 0.25\n}: ScaledPreviewProps) {\n const innerWidth = width / scale;\n const innerHeight = height / scale;\n\n return (\n <div\n className=\"overflow-hidden rounded-lg border border-[var(--canvas-border)] shadow-
|
|
9
|
+
"content": "\"use client\";\n\nimport { Header } from \"../layout\";\nimport { ContentDropzone } from \"./content-dropzone\";\nimport { LoginBrandingPanel } from \"./login-branding-panel\";\nimport { MessengerSidebar } from \"./messenger-sidebar\";\nimport { VideoChatControls } from \"./video-chat-controls\";\nimport {\n DashboardShell,\n IconSidebarShell,\n DoubleSidebarShell,\n StandardPageShell,\n MobileMenuShell,\n SearchBarShell,\n MultistepShell,\n MultistepSidebarShell,\n MultistepProgressBarShell,\n VerticalMultistepShell,\n AccountSettingsShell,\n} from \"../layout\";\n\n/**\n * Scaled Preview Wrapper\n * Renders content at a scaled-down size for canvas thumbnails\n */\ninterface ScaledPreviewProps {\n children: React.ReactNode;\n width?: number;\n height?: number;\n scale?: number;\n}\n\nexport function ScaledPreview({\n children,\n width = 400,\n height = 280,\n scale = 0.25\n}: ScaledPreviewProps) {\n const innerWidth = width / scale;\n const innerHeight = height / scale;\n\n return (\n <div\n className=\"overflow-hidden rounded-lg border border-[var(--canvas-border)] shadow-[var(--canvas-shadow-card)] bg-[var(--canvas-background)]\"\n style={{ width, height }}\n >\n <div\n style={{\n width: innerWidth,\n height: innerHeight,\n transform: `scale(${scale})`,\n transformOrigin: \"top left\",\n }}\n >\n {children}\n </div>\n </div>\n );\n}\n\n// Sample navigation for previews\nconst previewNav = [\n { id: \"home\", label: \"Home\", href: \"#\" },\n { id: \"about\", label: \"About\", href: \"#\" },\n];\n\nconst sampleSidebarSections = [\n {\n items: [\n { id: \"dashboard\", label: \"Dashboard\", icon: \"home\" as const, href: \"#\" },\n { id: \"analytics\", label: \"Analytics\", icon: \"chart\" as const, href: \"#\" },\n { id: \"settings\", label: \"Settings\", icon: \"settings\" as const, href: \"#\" },\n ],\n },\n];\n\n// =====================\n// PAGE TEMPLATE PREVIEWS\n// =====================\n\nexport function PageAboutPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <ContentDropzone />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageAccountPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <AccountSettingsShell>\n <ContentDropzone />\n </AccountSettingsShell>\n </ScaledPreview>\n );\n}\n\nexport function PageAdminPortalPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <ContentDropzone />\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageCenteredProfilePreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <StandardPageShell showBanner={false} showPageHeader={false}>\n <ContentDropzone />\n </StandardPageShell>\n </ScaledPreview>\n );\n}\n\nexport function PageDoubleSidebarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DoubleSidebarShell>\n <ContentDropzone />\n </DoubleSidebarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageIconSidebarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <IconSidebarShell>\n <ContentDropzone />\n </IconSidebarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageLoginPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen\">\n <div className=\"flex-1 flex items-center justify-center p-8\">\n <ContentDropzone height=\"320px\" className=\"w-[360px]\" />\n </div>\n <LoginBrandingPanel className=\"hidden lg:flex w-1/2\" />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageMenuSectionsPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <ContentDropzone />\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageMessengerPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} className=\"absolute top-0 left-0 right-0\" />\n <div className=\"flex flex-1 pt-16\">\n <MessengerSidebar className=\"w-[320px]\" />\n <div className=\"flex-1 flex flex-col\">\n <ContentDropzone />\n </div>\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageMobileMenuPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <MobileMenuShell>\n <ContentDropzone />\n </MobileMenuShell>\n </ScaledPreview>\n );\n}\n\nexport function PageMultistepProgressbarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <MultistepProgressBarShell>\n <ContentDropzone />\n </MultistepProgressBarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageMultistepSidebarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <MultistepSidebarShell>\n <ContentDropzone />\n </MultistepSidebarShell>\n </ScaledPreview>\n );\n}\n\nexport function PagePricingPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <ContentDropzone />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageProductHomepagePreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <ContentDropzone />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageResetPasswordPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen\">\n <div className=\"flex-1 flex items-center justify-center p-8\">\n <ContentDropzone height=\"320px\" className=\"w-[360px]\" />\n </div>\n <LoginBrandingPanel className=\"hidden lg:flex w-1/2\" />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageSearchBarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <SearchBarShell>\n <ContentDropzone />\n </SearchBarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageSidebarProfilePreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <ContentDropzone />\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageStandardPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <StandardPageShell>\n <ContentDropzone />\n </StandardPageShell>\n </ScaledPreview>\n );\n}\n\nexport function PageStandardMultistepPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <MultistepShell>\n <ContentDropzone />\n </MultistepShell>\n </ScaledPreview>\n );\n}\n\nexport function PageStandardSearchPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <ContentDropzone />\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageVerticalMultistepPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <VerticalMultistepShell>\n <ContentDropzone />\n </VerticalMultistepShell>\n </ScaledPreview>\n );\n}\n\nexport function PageVideoChatPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"h-screen bg-[var(--canvas-sidebar-dark-bg)] flex flex-col\">\n <div className=\"flex-1 p-4\">\n <ContentDropzone />\n </div>\n <div className=\"p-4\">\n <VideoChatControls />\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageVideoListPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <ContentDropzone />\n </div>\n </ScaledPreview>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
{
|
|
16
16
|
"path": "components/blocks/persona-card.tsx",
|
|
17
17
|
"type": "registry:block",
|
|
18
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport type { Persona } from \"../../types/project\";\nimport { Target, AlertCircle, Quote } from \"lucide-react\";\n\ninterface PersonaCardProps {\n persona: Persona;\n className?: string;\n}\n\nexport function PersonaCard({ persona, className }: PersonaCardProps) {\n return (\n <div\n className={cn(\n \"rounded-xl border border-[var(--canvas-border)] bg-[var(--canvas-background)]\",\n \"p-5 hover:shadow-
|
|
18
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport type { Persona } from \"../../types/project\";\nimport { Target, AlertCircle, Quote } from \"lucide-react\";\n\ninterface PersonaCardProps {\n persona: Persona;\n className?: string;\n}\n\nexport function PersonaCard({ persona, className }: PersonaCardProps) {\n return (\n <div\n className={cn(\n \"rounded-xl border border-[var(--canvas-border)] bg-[var(--canvas-background)]\",\n \"p-5 hover:shadow-[var(--canvas-shadow-card)] transition-shadow\",\n className\n )}\n >\n {/* Header */}\n <div className=\"flex items-start gap-3 mb-4\">\n <div className=\"text-3xl\">{persona.avatar || \"👤\"}</div>\n <div>\n <h3 className=\"font-semibold text-[var(--canvas-text)]\">\n {persona.name}\n </h3>\n <p className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {persona.role}\n </p>\n </div>\n </div>\n\n {/* Goals */}\n <div className=\"mb-4\">\n <div className=\"flex items-center gap-1.5 font-medium text-[var(--canvas-text-muted)] mb-2\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n <Target className=\"size-3\" />\n Goals\n </div>\n <ul className=\"space-y-1\">\n {persona.goals.map((goal, i) => (\n <li\n key={i}\n className=\"text-[var(--canvas-text)] flex items-start gap-2\" style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <span className=\"text-[var(--canvas-primary)] mt-1.5\">•</span>\n {goal}\n </li>\n ))}\n </ul>\n </div>\n\n {/* Pain Points */}\n <div className=\"mb-4\">\n <div className=\"flex items-center gap-1.5 font-medium text-[var(--canvas-text-muted)] mb-2\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n <AlertCircle className=\"size-3\" />\n Pain Points\n </div>\n <ul className=\"space-y-1\">\n {persona.painPoints.map((point, i) => (\n <li\n key={i}\n className=\"text-[var(--canvas-text)] flex items-start gap-2\" style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <span className=\"text-[var(--canvas-destructive)] mt-1.5\">•</span>\n {point}\n </li>\n ))}\n </ul>\n </div>\n\n {/* Quote */}\n <div className=\"border-t border-[var(--canvas-border)] pt-4 mt-4\">\n <div className=\"flex items-start gap-2\">\n <Quote className=\"size-4 text-[var(--canvas-text-muted)] shrink-0 mt-0.5\" />\n <p className=\"italic text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n \"{persona.quote}\"\n </p>\n </div>\n </div>\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// PERSONA GRID\n// ═══════════════════════════════════════════════════════════\n\ninterface PersonaGridProps {\n personas: Persona[];\n}\n\nexport function PersonaGrid({ personas }: PersonaGridProps) {\n if (personas.length === 0) {\n return null;\n }\n\n return (\n <div className=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n {personas.map((persona) => (\n <PersonaCard key={persona.id} persona={persona} />\n ))}\n </div>\n );\n}\n"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"dependencies": [
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
{
|
|
16
16
|
"path": "components/blocks/personalize-feed-popup.tsx",
|
|
17
17
|
"type": "registry:block",
|
|
18
|
-
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FeedOption {\n /** Unique identifier */\n id: string;\n /** Display label */\n label: string;\n}\n\nexport interface PersonalizeFeedPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text */\n description?: string;\n /** Available topic options */\n options?: FeedOption[];\n /** IDs of initially selected options */\n defaultSelected?: string[];\n /** Callback when save is clicked — receives selected option IDs */\n onSave?: (selectedIds: string[]) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Cancel button label */\n cancelLabel?: string;\n /** Save button label */\n saveLabel?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Personalize your feed\";\nconst DEFAULT_DESCRIPTION =\n \"Select the topics that you are interested in below. You can change these preferences at any time from your account page.\";\n\nconst DEFAULT_OPTIONS: FeedOption[] = [\n { id: \"photography\", label: \"Photography\" },\n { id: \"film\", label: \"Film\" },\n { id: \"music\", label: \"Music\" },\n { id: \"ceramics\", label: \"Ceramics\" },\n { id: \"fitness\", label: \"Fitness\" },\n { id: \"nature\", label: \"Nature\" },\n { id: \"bars\", label: \"Bars\" },\n { id: \"restaurants\", label: \"Restaurants\" },\n];\n\nconst DEFAULT_SELECTED = [\"photography\", \"fitness\"];\n\n// ---------------------------------------------------------------------------\n// PersonalizeFeedPopup\n// ---------------------------------------------------------------------------\n\nexport function PersonalizeFeedPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n options = DEFAULT_OPTIONS,\n defaultSelected = DEFAULT_SELECTED,\n onSave,\n onCancel,\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n className,\n}: PersonalizeFeedPopupProps) {\n const [selectedIds, setSelectedIds] = useState<Set<string>>(\n new Set(defaultSelected)\n );\n\n // Reset selection when dialog opens\n useEffect(() => {\n if (open) {\n setSelectedIds(new Set(defaultSelected));\n }\n }, [open, defaultSelected]);\n\n const toggleOption = (id: string) => {\n setSelectedIds((prev) => {\n const next = new Set(prev);\n if (next.has(id)) {\n next.delete(id);\n } else {\n next.add(id);\n }\n return next;\n });\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.(Array.from(selectedIds));\n onOpenChange?.(false);\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[
|
|
18
|
+
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FeedOption {\n /** Unique identifier */\n id: string;\n /** Display label */\n label: string;\n}\n\nexport interface PersonalizeFeedPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text */\n description?: string;\n /** Available topic options */\n options?: FeedOption[];\n /** IDs of initially selected options */\n defaultSelected?: string[];\n /** Callback when save is clicked — receives selected option IDs */\n onSave?: (selectedIds: string[]) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Cancel button label */\n cancelLabel?: string;\n /** Save button label */\n saveLabel?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Personalize your feed\";\nconst DEFAULT_DESCRIPTION =\n \"Select the topics that you are interested in below. You can change these preferences at any time from your account page.\";\n\nconst DEFAULT_OPTIONS: FeedOption[] = [\n { id: \"photography\", label: \"Photography\" },\n { id: \"film\", label: \"Film\" },\n { id: \"music\", label: \"Music\" },\n { id: \"ceramics\", label: \"Ceramics\" },\n { id: \"fitness\", label: \"Fitness\" },\n { id: \"nature\", label: \"Nature\" },\n { id: \"bars\", label: \"Bars\" },\n { id: \"restaurants\", label: \"Restaurants\" },\n];\n\nconst DEFAULT_SELECTED = [\"photography\", \"fitness\"];\n\n// ---------------------------------------------------------------------------\n// PersonalizeFeedPopup\n// ---------------------------------------------------------------------------\n\nexport function PersonalizeFeedPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n options = DEFAULT_OPTIONS,\n defaultSelected = DEFAULT_SELECTED,\n onSave,\n onCancel,\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n className,\n}: PersonalizeFeedPopupProps) {\n const [selectedIds, setSelectedIds] = useState<Set<string>>(\n new Set(defaultSelected)\n );\n\n // Reset selection when dialog opens\n useEffect(() => {\n if (open) {\n setSelectedIds(new Set(defaultSelected));\n }\n }, [open, defaultSelected]);\n\n const toggleOption = (id: string) => {\n setSelectedIds((prev) => {\n const next = new Set(prev);\n if (next.has(id)) {\n next.delete(id);\n } else {\n next.add(id);\n }\n return next;\n });\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.(Array.from(selectedIds));\n onOpenChange?.(false);\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[420px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Description */}\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n\n {/* Chip Grid */}\n <div\n className=\"flex flex-wrap\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {options.map((option) => {\n const isSelected = selectedIds.has(option.id);\n return (\n <button\n key={option.id}\n type=\"button\"\n onClick={() => toggleOption(option.id)}\n className={cn(\n \"rounded-full border transition-colors cursor-pointer\",\n isSelected\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] border-[var(--canvas-primary)]\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text)] border-[var(--canvas-border)] hover:bg-[var(--canvas-surface-hover)]\"\n )}\n style={{\n padding: \"var(--spacing-md) var(--spacing-xl)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {option.label}\n </button>\n );\n })}\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] justify-end\">\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button variant=\"primary\" onClick={handleSave}>\n {saveLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"dependencies": [],
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
{
|
|
16
16
|
"path": "components/blocks/pricing-plans-popup.tsx",
|
|
17
17
|
"type": "registry:block",
|
|
18
|
-
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type BillingPeriod = \"monthly\" | \"annually\";\n\nexport interface PricingPlan {\n /** Unique plan identifier */\n id: string;\n /** Display name (e.g. \"Basic\", \"Professional\") */\n name: string;\n /** Monthly price in dollars */\n monthlyPrice: number;\n /** Annual price in dollars — defaults to monthlyPrice * 12 if omitted */\n annualPrice?: number;\n /** Short description of what the plan includes */\n description: string;\n}\n\nexport interface PricingPlansPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text below the title */\n description?: string;\n /** Array of plan options to display as radio cards */\n plans?: PricingPlan[];\n /** Currently selected billing period (controlled) */\n billingPeriod?: BillingPeriod;\n /** Default billing period when uncontrolled */\n defaultBillingPeriod?: BillingPeriod;\n /** Callback when billing period changes */\n onBillingPeriodChange?: (period: BillingPeriod) => void;\n /** Currently selected plan ID (controlled) */\n selectedPlanId?: string;\n /** Default selected plan ID when uncontrolled */\n defaultSelectedPlanId?: string;\n /** Callback when selected plan changes */\n onPlanChange?: (planId: string) => void;\n /** Save/confirm button label */\n confirmLabel?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Callback when save button is clicked — receives the selected plan ID and billing period */\n onConfirm?: (planId: string, billingPeriod: BillingPeriod) => void;\n /** Callback when cancel button is clicked */\n onCancel?: () => void;\n /** Disables the confirm button and shows a loading state */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Pricing plans\";\nconst DEFAULT_DESCRIPTION =\n \"Choose the plan that's right for your company. All plans will include a 30-day trial.\";\n\nconst DEFAULT_PLANS: PricingPlan[] = [\n {\n id: \"basic\",\n name: \"Basic\",\n monthlyPrice: 5,\n annualPrice: 50,\n description: \"For hobbyists\",\n },\n {\n id: \"professional\",\n name: \"Professional\",\n monthlyPrice: 10,\n annualPrice: 100,\n description: \"For teams up to 30 people\",\n },\n {\n id: \"enterprise\",\n name: \"Enterprise\",\n monthlyPrice: 30,\n annualPrice: 300,\n description: \"For large teams\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// PricingPlansPopup\n// ---------------------------------------------------------------------------\n\nexport function PricingPlansPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n plans = DEFAULT_PLANS,\n billingPeriod,\n defaultBillingPeriod = \"monthly\",\n onBillingPeriodChange,\n selectedPlanId,\n defaultSelectedPlanId,\n onPlanChange,\n confirmLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n loading = false,\n className,\n}: PricingPlansPopupProps) {\n // Controlled vs uncontrolled billing period\n const isBillingControlled = billingPeriod !== undefined;\n const [internalBilling, setInternalBilling] =\n useState<BillingPeriod>(defaultBillingPeriod);\n const currentBilling = isBillingControlled ? billingPeriod : internalBilling;\n\n // Controlled vs uncontrolled plan selection\n const isPlanControlled = selectedPlanId !== undefined;\n const [internalPlanId, setInternalPlanId] = useState<string | undefined>(\n defaultSelectedPlanId\n );\n const currentPlanId = isPlanControlled ? selectedPlanId : internalPlanId;\n\n // Reset internal state when dialog closes\n useEffect(() => {\n if (!open) {\n if (!isBillingControlled) setInternalBilling(defaultBillingPeriod);\n if (!isPlanControlled) setInternalPlanId(defaultSelectedPlanId);\n }\n }, [open, isBillingControlled, isPlanControlled, defaultBillingPeriod, defaultSelectedPlanId]);\n\n const handleBillingChange = (period: BillingPeriod) => {\n if (!isBillingControlled) {\n setInternalBilling(period);\n }\n onBillingPeriodChange?.(period);\n };\n\n const handlePlanSelect = (planId: string) => {\n if (!isPlanControlled) {\n setInternalPlanId(planId);\n }\n onPlanChange?.(planId);\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleConfirm = () => {\n if (currentPlanId) {\n onConfirm?.(currentPlanId, currentBilling);\n }\n };\n\n const getDisplayPrice = (plan: PricingPlan): number => {\n if (currentBilling === \"annually\") {\n return plan.annualPrice ?? plan.monthlyPrice * 12;\n }\n return plan.monthlyPrice;\n };\n\n const priceSuffix = currentBilling === \"annually\" ? \"/ year\" : \"/ month\";\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[480px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Description */}\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n\n {/* Billing Period Toggle */}\n <div\n className=\"flex w-full overflow-hidden self-center\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--spacing-xs)\",\n height: 40,\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n width: \"fit-content\",\n }}\n >\n {([\"monthly\", \"annually\"] as const).map((period, idx) => {\n const isActive = currentBilling === period;\n return (\n <button\n key={period}\n type=\"button\"\n onClick={() => handleBillingChange(period)}\n className=\"cursor-pointer\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"24px\",\n background: \"var(--canvas-background)\",\n border: \"none\",\n borderLeft:\n isActive\n ? \"2px solid var(--canvas-primary)\"\n : idx > 0\n ? \"1px solid var(--canvas-border)\"\n : \"none\",\n color: isActive\n ? \"var(--canvas-primary)\"\n : \"var(--canvas-text-placeholder)\",\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n {period === \"monthly\" ? \"Monthly\" : \"Annually\"}\n </button>\n );\n })}\n </div>\n\n {/* Plan Radio Cards */}\n <div className=\"flex w-full flex-col\" style={{ gap: \"var(--spacing-2xl)\" }}>\n {plans.map((plan) => {\n const isSelected = currentPlanId === plan.id;\n return (\n <button\n key={plan.id}\n type=\"button\"\n onClick={() => handlePlanSelect(plan.id)}\n className={cn(\n \"flex w-full items-center text-left cursor-pointer transition-colors\"\n )}\n style={{\n background: isSelected ? \"var(--canvas-background)\" : \"var(--canvas-background)\",\n border: isSelected\n ? \"2px solid var(--canvas-primary)\"\n : \"1px solid var(--canvas-border)\",\n padding: isSelected ? 15 : \"var(--spacing-xl)\",\n borderRadius: \"var(--spacing-xs)\",\n gap: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n {/* Radio Circle */}\n <div\n className=\"shrink-0 flex items-center justify-center rounded-full\"\n style={{\n width: 20,\n height: 20,\n border: `1px solid var(--canvas-border)`,\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n background: \"var(--canvas-background)\",\n }}\n >\n {isSelected && (\n <div\n className=\"rounded-full\"\n style={{\n width: 12,\n height: 12,\n backgroundColor: \"var(--canvas-text-subtitle)\",\n }}\n />\n )}\n </div>\n\n {/* Plan Content */}\n <div className=\"flex flex-1 flex-col min-w-0\">\n <div className=\"flex w-full items-start justify-between\" style={{ gap: \"var(--spacing-md)\" }}>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: 16,\n fontWeight: 500,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.name}\n </span>\n <span style={{ lineHeight: \"24px\", whiteSpace: \"nowrap\" }}>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: 16,\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n ${getDisplayPrice(plan)}\n </span>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 400,\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {\" \"}\n {priceSuffix}\n </span>\n </span>\n </div>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.description}\n </span>\n </div>\n </button>\n );\n })}\n </div>\n\n {/* Actions */}\n <div\n className=\"flex w-full justify-end\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleConfirm}\n disabled={loading || !currentPlanId}\n >\n {confirmLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
18
|
+
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type BillingPeriod = \"monthly\" | \"annually\";\n\nexport interface PricingPlan {\n /** Unique plan identifier */\n id: string;\n /** Display name (e.g. \"Basic\", \"Professional\") */\n name: string;\n /** Monthly price in dollars */\n monthlyPrice: number;\n /** Annual price in dollars — defaults to monthlyPrice * 12 if omitted */\n annualPrice?: number;\n /** Short description of what the plan includes */\n description: string;\n}\n\nexport interface PricingPlansPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text below the title */\n description?: string;\n /** Array of plan options to display as radio cards */\n plans?: PricingPlan[];\n /** Currently selected billing period (controlled) */\n billingPeriod?: BillingPeriod;\n /** Default billing period when uncontrolled */\n defaultBillingPeriod?: BillingPeriod;\n /** Callback when billing period changes */\n onBillingPeriodChange?: (period: BillingPeriod) => void;\n /** Currently selected plan ID (controlled) */\n selectedPlanId?: string;\n /** Default selected plan ID when uncontrolled */\n defaultSelectedPlanId?: string;\n /** Callback when selected plan changes */\n onPlanChange?: (planId: string) => void;\n /** Save/confirm button label */\n confirmLabel?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Callback when save button is clicked — receives the selected plan ID and billing period */\n onConfirm?: (planId: string, billingPeriod: BillingPeriod) => void;\n /** Callback when cancel button is clicked */\n onCancel?: () => void;\n /** Disables the confirm button and shows a loading state */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Pricing plans\";\nconst DEFAULT_DESCRIPTION =\n \"Choose the plan that's right for your company. All plans will include a 30-day trial.\";\n\nconst DEFAULT_PLANS: PricingPlan[] = [\n {\n id: \"basic\",\n name: \"Basic\",\n monthlyPrice: 5,\n annualPrice: 50,\n description: \"For hobbyists\",\n },\n {\n id: \"professional\",\n name: \"Professional\",\n monthlyPrice: 10,\n annualPrice: 100,\n description: \"For teams up to 30 people\",\n },\n {\n id: \"enterprise\",\n name: \"Enterprise\",\n monthlyPrice: 30,\n annualPrice: 300,\n description: \"For large teams\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// PricingPlansPopup\n// ---------------------------------------------------------------------------\n\nexport function PricingPlansPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n plans = DEFAULT_PLANS,\n billingPeriod,\n defaultBillingPeriod = \"monthly\",\n onBillingPeriodChange,\n selectedPlanId,\n defaultSelectedPlanId,\n onPlanChange,\n confirmLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n loading = false,\n className,\n}: PricingPlansPopupProps) {\n // Controlled vs uncontrolled billing period\n const isBillingControlled = billingPeriod !== undefined;\n const [internalBilling, setInternalBilling] =\n useState<BillingPeriod>(defaultBillingPeriod);\n const currentBilling = isBillingControlled ? billingPeriod : internalBilling;\n\n // Controlled vs uncontrolled plan selection\n const isPlanControlled = selectedPlanId !== undefined;\n const [internalPlanId, setInternalPlanId] = useState<string | undefined>(\n defaultSelectedPlanId\n );\n const currentPlanId = isPlanControlled ? selectedPlanId : internalPlanId;\n\n // Reset internal state when dialog closes\n useEffect(() => {\n if (!open) {\n if (!isBillingControlled) setInternalBilling(defaultBillingPeriod);\n if (!isPlanControlled) setInternalPlanId(defaultSelectedPlanId);\n }\n }, [open, isBillingControlled, isPlanControlled, defaultBillingPeriod, defaultSelectedPlanId]);\n\n const handleBillingChange = (period: BillingPeriod) => {\n if (!isBillingControlled) {\n setInternalBilling(period);\n }\n onBillingPeriodChange?.(period);\n };\n\n const handlePlanSelect = (planId: string) => {\n if (!isPlanControlled) {\n setInternalPlanId(planId);\n }\n onPlanChange?.(planId);\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleConfirm = () => {\n if (currentPlanId) {\n onConfirm?.(currentPlanId, currentBilling);\n }\n };\n\n const getDisplayPrice = (plan: PricingPlan): number => {\n if (currentBilling === \"annually\") {\n return plan.annualPrice ?? plan.monthlyPrice * 12;\n }\n return plan.monthlyPrice;\n };\n\n const priceSuffix = currentBilling === \"annually\" ? \"/ year\" : \"/ month\";\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[480px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Description */}\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n\n {/* Billing Period Toggle */}\n <div\n className=\"flex w-full overflow-hidden self-center\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--spacing-xs)\",\n height: 40,\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n width: \"fit-content\",\n }}\n >\n {([\"monthly\", \"annually\"] as const).map((period, idx) => {\n const isActive = currentBilling === period;\n return (\n <button\n key={period}\n type=\"button\"\n onClick={() => handleBillingChange(period)}\n className=\"cursor-pointer\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"24px\",\n background: \"var(--canvas-background)\",\n border: \"none\",\n borderLeft:\n isActive\n ? \"2px solid var(--canvas-primary)\"\n : idx > 0\n ? \"1px solid var(--canvas-border)\"\n : \"none\",\n color: isActive\n ? \"var(--canvas-primary)\"\n : \"var(--canvas-text-placeholder)\",\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n {period === \"monthly\" ? \"Monthly\" : \"Annually\"}\n </button>\n );\n })}\n </div>\n\n {/* Plan Radio Cards */}\n <div className=\"flex w-full flex-col\" style={{ gap: \"var(--spacing-2xl)\" }}>\n {plans.map((plan) => {\n const isSelected = currentPlanId === plan.id;\n return (\n <button\n key={plan.id}\n type=\"button\"\n onClick={() => handlePlanSelect(plan.id)}\n className={cn(\n \"flex w-full items-center text-left cursor-pointer transition-colors\"\n )}\n style={{\n background: isSelected ? \"var(--canvas-background)\" : \"var(--canvas-background)\",\n border: isSelected\n ? \"2px solid var(--canvas-primary)\"\n : \"1px solid var(--canvas-border)\",\n padding: isSelected ? 15 : \"var(--spacing-xl)\",\n borderRadius: \"var(--spacing-xs)\",\n gap: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n {/* Radio Circle */}\n <div\n className=\"shrink-0 flex items-center justify-center rounded-full\"\n style={{\n width: 20,\n height: 20,\n border: `1px solid var(--canvas-border)`,\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n background: \"var(--canvas-background)\",\n }}\n >\n {isSelected && (\n <div\n className=\"rounded-full\"\n style={{\n width: 12,\n height: 12,\n backgroundColor: \"var(--canvas-text-subtitle)\",\n }}\n />\n )}\n </div>\n\n {/* Plan Content */}\n <div className=\"flex flex-1 flex-col min-w-0\">\n <div className=\"flex w-full items-start justify-between\" style={{ gap: \"var(--spacing-md)\" }}>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: 16,\n fontWeight: 500,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.name}\n </span>\n <span style={{ lineHeight: \"24px\", whiteSpace: \"nowrap\" }}>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: 16,\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n ${getDisplayPrice(plan)}\n </span>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 400,\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {\" \"}\n {priceSuffix}\n </span>\n </span>\n </div>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.description}\n </span>\n </div>\n </button>\n );\n })}\n </div>\n\n {/* Actions */}\n <div\n className=\"flex w-full justify-end\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleConfirm}\n disabled={loading || !currentPlanId}\n >\n {confirmLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"dependencies": [],
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
{
|
|
16
16
|
"path": "components/blocks/purchase-confirmation-popup.tsx",
|
|
17
17
|
"type": "registry:block",
|
|
18
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PurchaseConfirmationPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text — supports React nodes for inline bold/emphasis */\n description?: React.ReactNode;\n /** Label for the card field */\n cardLabel?: string;\n /** Masked card number to display (e.g. \"**** **** **** 8274\") */\n cardLastFour?: string;\n /** Label for the change-card button */\n changeCardLabel?: string;\n /** Callback when the \"Change\" card button is clicked */\n onChangeCard?: () => void;\n /** Confirm button label */\n confirmLabel?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Callback when the confirm button is clicked */\n onConfirm?: () => void;\n /** Callback when the cancel button is clicked */\n onCancel?: () => void;\n /** Disables the confirm button and shows a loading state */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Confirm your plan\";\nconst DEFAULT_DESCRIPTION = (\n <>\n You are about to reserve a spot for{\" \"}\n <strong style={{ color: \"var(--canvas-text)\" }}>AirDev Academy</strong> on{\" \"}\n <strong style={{ color: \"var(--canvas-text)\" }}>\n Tuesday, Dec 25 at 4:00pm ET\n </strong>\n .{\"\\n\\n\"}Click below to confirm your credit card and authorize a charge of{\" \"}\n <strong\n style={{\n color: \"var(--canvas-text)\",\n fontSize: \"var(--typo-body-l-size)\",\n fontWeight: 600,\n }}\n >\n $59\n </strong>{\" \"}\n for the class.\n </>\n);\n\n// ---------------------------------------------------------------------------\n// PurchaseConfirmationPopup\n// ---------------------------------------------------------------------------\n\nexport function PurchaseConfirmationPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n cardLabel = \"Selected credit card\",\n cardLastFour = \"**** **** **** 8274\",\n changeCardLabel = \"Change\",\n onChangeCard,\n confirmLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n loading = false,\n className,\n}: PurchaseConfirmationPopupProps) {\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleConfirm = () => {\n onConfirm?.();\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[
|
|
18
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PurchaseConfirmationPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text — supports React nodes for inline bold/emphasis */\n description?: React.ReactNode;\n /** Label for the card field */\n cardLabel?: string;\n /** Masked card number to display (e.g. \"**** **** **** 8274\") */\n cardLastFour?: string;\n /** Label for the change-card button */\n changeCardLabel?: string;\n /** Callback when the \"Change\" card button is clicked */\n onChangeCard?: () => void;\n /** Confirm button label */\n confirmLabel?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Callback when the confirm button is clicked */\n onConfirm?: () => void;\n /** Callback when the cancel button is clicked */\n onCancel?: () => void;\n /** Disables the confirm button and shows a loading state */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Confirm your plan\";\nconst DEFAULT_DESCRIPTION = (\n <>\n You are about to reserve a spot for{\" \"}\n <strong style={{ color: \"var(--canvas-text)\" }}>AirDev Academy</strong> on{\" \"}\n <strong style={{ color: \"var(--canvas-text)\" }}>\n Tuesday, Dec 25 at 4:00pm ET\n </strong>\n .{\"\\n\\n\"}Click below to confirm your credit card and authorize a charge of{\" \"}\n <strong\n style={{\n color: \"var(--canvas-text)\",\n fontSize: \"var(--typo-body-l-size)\",\n fontWeight: 600,\n }}\n >\n $59\n </strong>{\" \"}\n for the class.\n </>\n);\n\n// ---------------------------------------------------------------------------\n// PurchaseConfirmationPopup\n// ---------------------------------------------------------------------------\n\nexport function PurchaseConfirmationPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n cardLabel = \"Selected credit card\",\n cardLastFour = \"**** **** **** 8274\",\n changeCardLabel = \"Change\",\n onChangeCard,\n confirmLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n loading = false,\n className,\n}: PurchaseConfirmationPopupProps) {\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleConfirm = () => {\n onConfirm?.();\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[375px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Description */}\n <DialogDescription asChild>\n <div\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </div>\n </DialogDescription>\n\n {/* Credit card field */}\n <div className=\"flex flex-col gap-[var(--spacing-xs)] w-full\">\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n fontWeight: 500,\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {cardLabel}\n </span>\n <div\n className={cn(\n \"flex items-center gap-[var(--spacing-3xl)]\",\n \"h-[44px] w-full\",\n \"rounded-[var(--radius-xs)]\",\n \"border border-[var(--canvas-border)]\",\n \"bg-[var(--canvas-background)]\",\n \"pl-[var(--spacing-xl)] pr-[var(--spacing-md)]\",\n \"shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)]\"\n )}\n >\n <span\n className=\"flex-1\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {cardLastFour}\n </span>\n <button\n type=\"button\"\n onClick={onChangeCard}\n className=\"shrink-0 cursor-pointer rounded-[var(--radius-xs)] px-[var(--spacing-lg)] py-[var(--spacing-xs)]\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"16px\",\n lineHeight: \"24px\",\n fontWeight: 600,\n color: \"var(--canvas-primary)\",\n background: \"transparent\",\n border: \"none\",\n }}\n >\n {changeCardLabel}\n </button>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] flex-col-reverse sm:flex-row justify-end\">\n <Button\n variant=\"neutral\"\n className=\"sm:w-[96px]\"\n onClick={handleCancel}\n >\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleConfirm}\n disabled={loading}\n >\n {confirmLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"dependencies": [],
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
{
|
|
16
16
|
"path": "components/blocks/share-project-popup.tsx",
|
|
17
17
|
"type": "registry:block",
|
|
18
|
-
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { Label } from \"../ui/label\";\nimport { Input } from \"../ui/input\";\nimport {\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n} from \"../ui/select\";\nimport {\n Popover,\n PopoverTrigger,\n PopoverContent,\n} from \"../ui/popover\";\nimport { Switch } from \"../ui/switch\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Copy, Check, ChevronDown, Globe } from \"lucide-react\";\nimport {\n AVATAR_MARCUS_WEBB,\n AVATAR_SARAH_CHEN,\n AVATAR_MAYA_JOHNSON,\n AVATAR_LILY_TRAN,\n} from \"./demo-avatars\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ShareRole {\n /** Unique role identifier */\n id: string;\n /** Display label */\n label: string;\n /** Description shown in the role popover */\n description: string;\n}\n\nexport interface SharedPerson {\n /** Unique person identifier */\n id: string;\n /** Display name */\n name: string;\n /** Avatar image URL or base64 */\n avatarUrl?: string;\n /** Avatar fallback initials */\n avatarFallback?: string;\n /** Current role id — must match a ShareRole.id */\n role: string;\n /** Whether access is enabled */\n enabled: boolean;\n}\n\nexport interface ShareProjectPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Popup title */\n title?: string;\n /** Subtitle description */\n description?: string;\n /** Visibility options for the dropdown */\n visibilityOptions?: { id: string; label: string }[];\n /** Default selected visibility option id */\n defaultVisibility?: string;\n /** The shareable URL */\n shareUrl?: string;\n /** Invite user options for the select */\n inviteOptions?: { id: string; label: string }[];\n /** Invite button label */\n inviteLabel?: string;\n /** Role definitions for the dropdown */\n roles?: ShareRole[];\n /** People with access */\n people?: SharedPerson[];\n /** Callback when invite button is clicked */\n onInvite?: (userId: string) => void;\n /** Callback when a person's role changes */\n onRoleChange?: (personId: string, roleId: string) => void;\n /** Callback when a person's toggle changes */\n onToggle?: (personId: string, enabled: boolean) => void;\n /** Callback when the URL is copied */\n onCopyLink?: () => void;\n /** Additional class names for the dialog content */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_ROLES: ShareRole[] = [\n {\n id: \"full\",\n label: \"Full\",\n description: \"Can view, edit, and manage all aspects of the project.\",\n },\n {\n id: \"edit\",\n label: \"Edit\",\n description: \"Can view and edit content, but cannot manage settings.\",\n },\n {\n id: \"comment\",\n label: \"Comment\",\n description: \"Can view and leave comments, but cannot edit content.\",\n },\n {\n id: \"view\",\n label: \"View only\",\n description: \"Can view content only. No editing or commenting.\",\n },\n];\n\nconst DEFAULT_VISIBILITY_OPTIONS = [\n { id: \"public\", label: \"Public\" },\n { id: \"private\", label: \"Private\" },\n { id: \"team\", label: \"Team only\" },\n];\n\nconst DEFAULT_INVITE_OPTIONS = [\n { id: \"user-1\", label: \"Alex Rivera\" },\n { id: \"user-2\", label: \"Morgan Lee\" },\n { id: \"user-3\", label: \"Casey Jordan\" },\n];\n\nconst DEFAULT_PEOPLE: SharedPerson[] = [\n {\n id: \"p1\",\n name: \"John Connor\",\n avatarUrl: AVATAR_MARCUS_WEBB,\n avatarFallback: \"JC\",\n role: \"full\",\n enabled: true,\n },\n {\n id: \"p2\",\n name: \"Raj Mishra\",\n avatarUrl: AVATAR_SARAH_CHEN,\n avatarFallback: \"RM\",\n role: \"edit\",\n enabled: true,\n },\n {\n id: \"p3\",\n name: \"Mary Trott\",\n avatarUrl: AVATAR_MAYA_JOHNSON,\n avatarFallback: \"MT\",\n role: \"comment\",\n enabled: true,\n },\n {\n id: \"p4\",\n name: \"Lily Sun\",\n avatarUrl: AVATAR_LILY_TRAN,\n avatarFallback: \"LS\",\n role: \"view\",\n enabled: false,\n },\n];\n\n// ---------------------------------------------------------------------------\n// Role Dropdown (uses Popover for rich options)\n// ---------------------------------------------------------------------------\n\nfunction RoleDropdown({\n roles,\n currentRole,\n onRoleChange,\n}: {\n roles: ShareRole[];\n currentRole: string;\n onRoleChange: (roleId: string) => void;\n}) {\n const [open, setOpen] = useState(false);\n const current = roles.find((r) => r.id === currentRole);\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n className=\"flex items-center cursor-pointer shrink-0\"\n style={{\n gap: \"var(--spacing-xs)\",\n padding: \"var(--spacing-xs) var(--spacing-sm)\",\n borderRadius: \"var(--radius-sm)\",\n border: \"1px solid var(--canvas-border)\",\n background: \"var(--canvas-background)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {current?.label ?? currentRole}\n <ChevronDown\n className=\"shrink-0\"\n style={{\n width: 14,\n height: 14,\n color: \"var(--canvas-text-muted)\",\n }}\n />\n </button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n sideOffset={4}\n className=\"p-0 w-[260px]\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"flex flex-col\">\n {roles.map((role, idx) => {\n const isSelected = role.id === currentRole;\n return (\n <button\n key={role.id}\n className=\"flex items-start text-left cursor-pointer w-full\"\n style={{\n padding: \"var(--spacing-lg) var(--spacing-xl)\",\n gap: \"var(--spacing-lg)\",\n background: \"none\",\n border: \"none\",\n borderBottom:\n idx < roles.length - 1\n ? \"1px solid var(--canvas-border)\"\n : \"none\",\n }}\n onClick={() => {\n onRoleChange(role.id);\n setOpen(false);\n }}\n >\n {/* Checkmark area */}\n <div\n className=\"shrink-0 flex items-center justify-center\"\n style={{ width: 16, height: 20 }}\n >\n {isSelected && (\n <Check\n style={{\n width: 16,\n height: 16,\n color: \"var(--canvas-primary)\",\n }}\n />\n )}\n </div>\n\n {/* Label & description */}\n <div className=\"flex flex-col min-w-0\">\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"20px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {role.label}\n </span>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xs-size)\",\n lineHeight: \"18px\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {role.description}\n </span>\n </div>\n </button>\n );\n })}\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Person Row\n// ---------------------------------------------------------------------------\n\nfunction PersonRow({\n person,\n roles,\n onRoleChange,\n onToggle,\n}: {\n person: SharedPerson;\n roles: ShareRole[];\n onRoleChange: (roleId: string) => void;\n onToggle: (enabled: boolean) => void;\n}) {\n return (\n <div\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-lg)\",\n paddingBottom: \"var(--spacing-lg)\",\n }}\n >\n {/* Avatar + Name */}\n <Avatar className=\"size-8 shrink-0\">\n {person.avatarUrl && (\n <AvatarImage src={person.avatarUrl} alt={person.name} />\n )}\n <AvatarFallback\n className=\"text-xs font-medium bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\"\n >\n {person.avatarFallback ?? person.name.slice(0, 2).toUpperCase()}\n </AvatarFallback>\n </Avatar>\n\n <span\n className=\"flex-1 min-w-0 truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"20px\",\n fontWeight: 500,\n color: \"var(--canvas-text)\",\n }}\n >\n {person.name}\n </span>\n\n {/* Role dropdown */}\n <RoleDropdown\n roles={roles}\n currentRole={person.role}\n onRoleChange={onRoleChange}\n />\n\n {/* Toggle switch */}\n <Switch\n checked={person.enabled}\n onCheckedChange={onToggle}\n />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ShareProjectPopup\n// ---------------------------------------------------------------------------\n\nexport function ShareProjectPopup({\n open,\n onOpenChange,\n title = \"Share this project\",\n description = \"Manage access and permissions for your project.\",\n visibilityOptions = DEFAULT_VISIBILITY_OPTIONS,\n defaultVisibility = \"public\",\n shareUrl = \"https://airdev.co/project/23945\",\n inviteOptions = DEFAULT_INVITE_OPTIONS,\n inviteLabel = \"Invite user\",\n roles = DEFAULT_ROLES,\n people: peopleProp = DEFAULT_PEOPLE,\n onInvite,\n onRoleChange,\n onToggle,\n onCopyLink,\n className,\n}: ShareProjectPopupProps) {\n const [copied, setCopied] = useState(false);\n const [people, setPeople] = useState(peopleProp);\n const [visibility, setVisibility] = useState(defaultVisibility);\n const [selectedInvite, setSelectedInvite] = useState(\"\");\n\n // Sync when prop changes\n useEffect(() => {\n setPeople(peopleProp);\n }, [peopleProp]);\n\n // Reset copied state on close\n useEffect(() => {\n if (!open) {\n setCopied(false);\n setSelectedInvite(\"\");\n }\n }, [open]);\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(shareUrl);\n setCopied(true);\n onCopyLink?.();\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleRoleChange = (personId: string, roleId: string) => {\n setPeople((prev) =>\n prev.map((p) => (p.id === personId ? { ...p, role: roleId } : p))\n );\n onRoleChange?.(personId, roleId);\n };\n\n const handleToggle = (personId: string, enabled: boolean) => {\n setPeople((prev) =>\n prev.map((p) => (p.id === personId ? { ...p, enabled } : p))\n );\n onToggle?.(personId, enabled);\n };\n\n const handleInvite = () => {\n if (selectedInvite) {\n onInvite?.(selectedInvite);\n setSelectedInvite(\"\");\n }\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n <div\n className=\"flex flex-col\"\n style={{\n padding: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n }}\n >\n {/* Title & Description */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-xs)\" }}>\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n </div>\n\n {/* Visibility Section */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <Label>Visibility</Label>\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"shrink-0\">\n <Select value={visibility} onValueChange={setVisibility}>\n <SelectTrigger\n className=\"w-[140px]\"\n style={{ height: \"var(--input-standard-height)\" }}\n >\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <Globe\n style={{\n width: 14,\n height: 14,\n color: \"var(--canvas-text-muted)\",\n }}\n />\n <SelectValue />\n </div>\n </SelectTrigger>\n <SelectContent>\n {visibilityOptions.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* URL + copy */}\n <div\n className=\"flex items-center flex-1 min-w-0\"\n style={{\n height: \"var(--input-standard-height)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--input-standard-radius)\",\n backgroundColor: \"var(--canvas-background)\",\n overflow: \"hidden\",\n }}\n >\n <Input\n readOnly\n value={shareUrl}\n className=\"border-0 shadow-none flex-1 min-w-0\"\n style={{\n height: \"100%\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n />\n <button\n onClick={handleCopy}\n className=\"shrink-0 flex items-center justify-center cursor-pointer\"\n style={{\n width: 36,\n height: \"100%\",\n background: \"none\",\n border: \"none\",\n borderLeft: \"1px solid var(--canvas-border)\",\n color: copied\n ? \"var(--canvas-primary)\"\n : \"var(--canvas-text-muted)\",\n }}\n >\n {copied ? (\n <Check style={{ width: 14, height: 14 }} />\n ) : (\n <Copy style={{ width: 14, height: 14 }} />\n )}\n </button>\n </div>\n </div>\n </div>\n\n {/* Invite Section */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <Label>Invite a user</Label>\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"flex-1\">\n <Select\n value={selectedInvite}\n onValueChange={setSelectedInvite}\n >\n <SelectTrigger\n style={{ height: \"var(--input-standard-height)\" }}\n >\n <SelectValue placeholder=\"Search for a user...\" />\n </SelectTrigger>\n <SelectContent>\n {inviteOptions.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <Button\n variant=\"primary\"\n onClick={handleInvite}\n disabled={!selectedInvite}\n >\n {inviteLabel}\n </Button>\n </div>\n </div>\n\n {/* People with access */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"20px\",\n color: \"var(--canvas-text)\",\n }}\n >\n People with access ({people.length})\n </span>\n\n <div\n className=\"flex flex-col\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n }}\n >\n {people.map((person) => (\n <div\n key={person.id}\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n <PersonRow\n person={person}\n roles={roles}\n onRoleChange={(roleId) =>\n handleRoleChange(person.id, roleId)\n }\n onToggle={(enabled) =>\n handleToggle(person.id, enabled)\n }\n />\n </div>\n ))}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
18
|
+
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { Label } from \"../ui/label\";\nimport { Input } from \"../ui/input\";\nimport {\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n} from \"../ui/select\";\nimport {\n Popover,\n PopoverTrigger,\n PopoverContent,\n} from \"../ui/popover\";\nimport { Switch } from \"../ui/switch\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Copy, Check, ChevronDown, Globe } from \"lucide-react\";\nimport {\n AVATAR_MARCUS_WEBB,\n AVATAR_SARAH_CHEN,\n AVATAR_MAYA_JOHNSON,\n AVATAR_LILY_TRAN,\n} from \"./demo-avatars\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ShareRole {\n /** Unique role identifier */\n id: string;\n /** Display label */\n label: string;\n /** Description shown in the role popover */\n description: string;\n}\n\nexport interface SharedPerson {\n /** Unique person identifier */\n id: string;\n /** Display name */\n name: string;\n /** Avatar image URL or base64 */\n avatarUrl?: string;\n /** Avatar fallback initials */\n avatarFallback?: string;\n /** Current role id — must match a ShareRole.id */\n role: string;\n /** Whether access is enabled */\n enabled: boolean;\n}\n\nexport interface ShareProjectPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Popup title */\n title?: string;\n /** Subtitle description */\n description?: string;\n /** Visibility options for the dropdown */\n visibilityOptions?: { id: string; label: string }[];\n /** Default selected visibility option id */\n defaultVisibility?: string;\n /** The shareable URL */\n shareUrl?: string;\n /** Invite user options for the select */\n inviteOptions?: { id: string; label: string }[];\n /** Invite button label */\n inviteLabel?: string;\n /** Role definitions for the dropdown */\n roles?: ShareRole[];\n /** People with access */\n people?: SharedPerson[];\n /** Callback when invite button is clicked */\n onInvite?: (userId: string) => void;\n /** Callback when a person's role changes */\n onRoleChange?: (personId: string, roleId: string) => void;\n /** Callback when a person's toggle changes */\n onToggle?: (personId: string, enabled: boolean) => void;\n /** Callback when the URL is copied */\n onCopyLink?: () => void;\n /** Additional class names for the dialog content */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_ROLES: ShareRole[] = [\n {\n id: \"full\",\n label: \"Full\",\n description: \"Can view, edit, and manage all aspects of the project.\",\n },\n {\n id: \"edit\",\n label: \"Edit\",\n description: \"Can view and edit content, but cannot manage settings.\",\n },\n {\n id: \"comment\",\n label: \"Comment\",\n description: \"Can view and leave comments, but cannot edit content.\",\n },\n {\n id: \"view\",\n label: \"View only\",\n description: \"Can view content only. No editing or commenting.\",\n },\n];\n\nconst DEFAULT_VISIBILITY_OPTIONS = [\n { id: \"public\", label: \"Public\" },\n { id: \"private\", label: \"Private\" },\n { id: \"team\", label: \"Team only\" },\n];\n\nconst DEFAULT_INVITE_OPTIONS = [\n { id: \"user-1\", label: \"Alex Rivera\" },\n { id: \"user-2\", label: \"Morgan Lee\" },\n { id: \"user-3\", label: \"Casey Jordan\" },\n];\n\nconst DEFAULT_PEOPLE: SharedPerson[] = [\n {\n id: \"p1\",\n name: \"John Connor\",\n avatarUrl: AVATAR_MARCUS_WEBB,\n avatarFallback: \"JC\",\n role: \"full\",\n enabled: true,\n },\n {\n id: \"p2\",\n name: \"Raj Mishra\",\n avatarUrl: AVATAR_SARAH_CHEN,\n avatarFallback: \"RM\",\n role: \"edit\",\n enabled: true,\n },\n {\n id: \"p3\",\n name: \"Mary Trott\",\n avatarUrl: AVATAR_MAYA_JOHNSON,\n avatarFallback: \"MT\",\n role: \"comment\",\n enabled: true,\n },\n {\n id: \"p4\",\n name: \"Lily Sun\",\n avatarUrl: AVATAR_LILY_TRAN,\n avatarFallback: \"LS\",\n role: \"view\",\n enabled: false,\n },\n];\n\n// ---------------------------------------------------------------------------\n// Role Dropdown (uses Popover for rich options)\n// ---------------------------------------------------------------------------\n\nfunction RoleDropdown({\n roles,\n currentRole,\n onRoleChange,\n}: {\n roles: ShareRole[];\n currentRole: string;\n onRoleChange: (roleId: string) => void;\n}) {\n const [open, setOpen] = useState(false);\n const current = roles.find((r) => r.id === currentRole);\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n className=\"flex items-center cursor-pointer shrink-0\"\n style={{\n gap: \"var(--spacing-xs)\",\n padding: \"var(--spacing-xs) var(--spacing-sm)\",\n borderRadius: \"var(--radius-sm)\",\n border: \"1px solid var(--canvas-border)\",\n background: \"var(--canvas-background)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {current?.label ?? currentRole}\n <ChevronDown\n className=\"shrink-0\"\n style={{\n width: 14,\n height: 14,\n color: \"var(--canvas-text-muted)\",\n }}\n />\n </button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n sideOffset={4}\n className=\"p-0 w-[260px]\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"flex flex-col\">\n {roles.map((role, idx) => {\n const isSelected = role.id === currentRole;\n return (\n <button\n key={role.id}\n className=\"flex items-start text-left cursor-pointer w-full\"\n style={{\n padding: \"var(--spacing-lg) var(--spacing-xl)\",\n gap: \"var(--spacing-lg)\",\n background: \"none\",\n border: \"none\",\n borderBottom:\n idx < roles.length - 1\n ? \"1px solid var(--canvas-border)\"\n : \"none\",\n }}\n onClick={() => {\n onRoleChange(role.id);\n setOpen(false);\n }}\n >\n {/* Checkmark area */}\n <div\n className=\"shrink-0 flex items-center justify-center\"\n style={{ width: 16, height: 20 }}\n >\n {isSelected && (\n <Check\n style={{\n width: 16,\n height: 16,\n color: \"var(--canvas-primary)\",\n }}\n />\n )}\n </div>\n\n {/* Label & description */}\n <div className=\"flex flex-col min-w-0\">\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"20px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {role.label}\n </span>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xs-size)\",\n lineHeight: \"18px\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {role.description}\n </span>\n </div>\n </button>\n );\n })}\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Person Row\n// ---------------------------------------------------------------------------\n\nfunction PersonRow({\n person,\n roles,\n onRoleChange,\n onToggle,\n}: {\n person: SharedPerson;\n roles: ShareRole[];\n onRoleChange: (roleId: string) => void;\n onToggle: (enabled: boolean) => void;\n}) {\n return (\n <div\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-lg)\",\n paddingBottom: \"var(--spacing-lg)\",\n }}\n >\n {/* Avatar + Name */}\n <Avatar className=\"size-8 shrink-0\">\n {person.avatarUrl && (\n <AvatarImage src={person.avatarUrl} alt={person.name} />\n )}\n <AvatarFallback\n className=\"text-xs font-medium bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\"\n >\n {person.avatarFallback ?? person.name.slice(0, 2).toUpperCase()}\n </AvatarFallback>\n </Avatar>\n\n <span\n className=\"flex-1 min-w-0 truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"20px\",\n fontWeight: 500,\n color: \"var(--canvas-text)\",\n }}\n >\n {person.name}\n </span>\n\n {/* Role dropdown */}\n <RoleDropdown\n roles={roles}\n currentRole={person.role}\n onRoleChange={onRoleChange}\n />\n\n {/* Toggle switch */}\n <Switch\n checked={person.enabled}\n onCheckedChange={onToggle}\n />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ShareProjectPopup\n// ---------------------------------------------------------------------------\n\nexport function ShareProjectPopup({\n open,\n onOpenChange,\n title = \"Share this project\",\n description = \"Manage access and permissions for your project.\",\n visibilityOptions = DEFAULT_VISIBILITY_OPTIONS,\n defaultVisibility = \"public\",\n shareUrl = \"https://airdev.co/project/23945\",\n inviteOptions = DEFAULT_INVITE_OPTIONS,\n inviteLabel = \"Invite user\",\n roles = DEFAULT_ROLES,\n people: peopleProp = DEFAULT_PEOPLE,\n onInvite,\n onRoleChange,\n onToggle,\n onCopyLink,\n className,\n}: ShareProjectPopupProps) {\n const [copied, setCopied] = useState(false);\n const [people, setPeople] = useState(peopleProp);\n const [visibility, setVisibility] = useState(defaultVisibility);\n const [selectedInvite, setSelectedInvite] = useState(\"\");\n\n // Sync when prop changes\n useEffect(() => {\n setPeople(peopleProp);\n }, [peopleProp]);\n\n // Reset copied state on close\n useEffect(() => {\n if (!open) {\n setCopied(false);\n setSelectedInvite(\"\");\n }\n }, [open]);\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(shareUrl);\n setCopied(true);\n onCopyLink?.();\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleRoleChange = (personId: string, roleId: string) => {\n setPeople((prev) =>\n prev.map((p) => (p.id === personId ? { ...p, role: roleId } : p))\n );\n onRoleChange?.(personId, roleId);\n };\n\n const handleToggle = (personId: string, enabled: boolean) => {\n setPeople((prev) =>\n prev.map((p) => (p.id === personId ? { ...p, enabled } : p))\n );\n onToggle?.(personId, enabled);\n };\n\n const handleInvite = () => {\n if (selectedInvite) {\n onInvite?.(selectedInvite);\n setSelectedInvite(\"\");\n }\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n <div\n className=\"flex flex-col\"\n style={{\n padding: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n }}\n >\n {/* Title & Description */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-xs)\" }}>\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n </div>\n\n {/* Visibility Section */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <Label>Visibility</Label>\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"shrink-0\">\n <Select value={visibility} onValueChange={setVisibility}>\n <SelectTrigger\n className=\"w-[140px]\"\n style={{ height: \"var(--input-standard-height)\" }}\n >\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <Globe\n style={{\n width: 14,\n height: 14,\n color: \"var(--canvas-text-muted)\",\n }}\n />\n <SelectValue />\n </div>\n </SelectTrigger>\n <SelectContent>\n {visibilityOptions.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* URL + copy */}\n <div\n className=\"flex items-center flex-1 min-w-0\"\n style={{\n height: \"var(--input-standard-height)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--input-standard-radius)\",\n backgroundColor: \"var(--canvas-background)\",\n overflow: \"hidden\",\n }}\n >\n <Input\n readOnly\n value={shareUrl}\n className=\"border-0 shadow-none flex-1 min-w-0\"\n style={{\n height: \"100%\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n />\n <button\n onClick={handleCopy}\n className=\"shrink-0 flex items-center justify-center cursor-pointer\"\n style={{\n width: 36,\n height: \"100%\",\n background: \"none\",\n border: \"none\",\n borderLeft: \"1px solid var(--canvas-border)\",\n color: copied\n ? \"var(--canvas-primary)\"\n : \"var(--canvas-text-muted)\",\n }}\n >\n {copied ? (\n <Check style={{ width: 14, height: 14 }} />\n ) : (\n <Copy style={{ width: 14, height: 14 }} />\n )}\n </button>\n </div>\n </div>\n </div>\n\n {/* Invite Section */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <Label>Invite a user</Label>\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"flex-1\">\n <Select\n value={selectedInvite}\n onValueChange={setSelectedInvite}\n >\n <SelectTrigger\n style={{ height: \"var(--input-standard-height)\" }}\n >\n <SelectValue placeholder=\"Search for a user...\" />\n </SelectTrigger>\n <SelectContent>\n {inviteOptions.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <Button\n variant=\"primary\"\n onClick={handleInvite}\n disabled={!selectedInvite}\n >\n {inviteLabel}\n </Button>\n </div>\n </div>\n\n {/* People with access */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"20px\",\n color: \"var(--canvas-text)\",\n }}\n >\n People with access ({people.length})\n </span>\n\n <div\n className=\"flex flex-col\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n }}\n >\n {people.map((person) => (\n <div\n key={person.id}\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n <PersonRow\n person={person}\n roles={roles}\n onRoleChange={(roleId) =>\n handleRoleChange(person.id, roleId)\n }\n onToggle={(enabled) =>\n handleToggle(person.id, enabled)\n }\n />\n </div>\n ))}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"dependencies": [
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
{
|
|
16
16
|
"path": "components/blocks/small-edit-popup.tsx",
|
|
17
17
|
"type": "registry:block",
|
|
18
|
-
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { TextInput } from \"../ui/text-input\";\nimport { Label } from \"../ui/label\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SmallEditField {\n /** Unique field identifier */\n id: string;\n /** Label text displayed above the field */\n label: string;\n /** Placeholder text */\n placeholder?: string;\n /** When true, field takes 50% width and pairs with next half field */\n half?: boolean;\n}\n\nexport interface SmallEditPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text */\n description?: string;\n /** Form field configuration */\n fields?: SmallEditField[];\n /** Cancel button label */\n cancelLabel?: string;\n /** Save button label */\n saveLabel?: string;\n /** Callback when save is clicked — receives field values */\n onSave?: (values: Record<string, string>) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Disables the save button */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Edit profile\";\nconst DEFAULT_DESCRIPTION = \"Update your personal information below.\";\n\nconst DEFAULT_FIELDS: SmallEditField[] = [\n { id: \"field-1\", label: \"First name\", half: true },\n { id: \"field-2\", label: \"Last name\", half: true },\n { id: \"field-3\", label: \"Phone\", half: true },\n { id: \"field-4\", label: \"Location\", half: true },\n { id: \"field-5\", label: \"Email address\" },\n { id: \"field-6\", label: \"Bio\" },\n];\n\n// ---------------------------------------------------------------------------\n// SmallEditPopup\n// ---------------------------------------------------------------------------\n\nexport function SmallEditPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n fields = DEFAULT_FIELDS,\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n onSave,\n onCancel,\n loading = false,\n className,\n}: SmallEditPopupProps) {\n const [values, setValues] = useState<Record<string, string>>({});\n\n // Reset form values when dialog closes\n useEffect(() => {\n if (!open) {\n setValues({});\n }\n }, [open]);\n\n const handleChange = (id: string, value: string) => {\n setValues((prev) => ({ ...prev, [id]: value }));\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.(values);\n };\n\n // Group fields into rows: half-width fields paired together\n const rows: SmallEditField[][] = [];\n let i = 0;\n while (i < fields.length) {\n const field = fields[i];\n if (field.half && i + 1 < fields.length && fields[i + 1].half) {\n rows.push([field, fields[i + 1]]);\n i += 2;\n } else {\n rows.push([field]);\n i += 1;\n }\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[
|
|
18
|
+
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { TextInput } from \"../ui/text-input\";\nimport { Label } from \"../ui/label\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SmallEditField {\n /** Unique field identifier */\n id: string;\n /** Label text displayed above the field */\n label: string;\n /** Placeholder text */\n placeholder?: string;\n /** When true, field takes 50% width and pairs with next half field */\n half?: boolean;\n}\n\nexport interface SmallEditPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text */\n description?: string;\n /** Form field configuration */\n fields?: SmallEditField[];\n /** Cancel button label */\n cancelLabel?: string;\n /** Save button label */\n saveLabel?: string;\n /** Callback when save is clicked — receives field values */\n onSave?: (values: Record<string, string>) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Disables the save button */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Edit profile\";\nconst DEFAULT_DESCRIPTION = \"Update your personal information below.\";\n\nconst DEFAULT_FIELDS: SmallEditField[] = [\n { id: \"field-1\", label: \"First name\", half: true },\n { id: \"field-2\", label: \"Last name\", half: true },\n { id: \"field-3\", label: \"Phone\", half: true },\n { id: \"field-4\", label: \"Location\", half: true },\n { id: \"field-5\", label: \"Email address\" },\n { id: \"field-6\", label: \"Bio\" },\n];\n\n// ---------------------------------------------------------------------------\n// SmallEditPopup\n// ---------------------------------------------------------------------------\n\nexport function SmallEditPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n fields = DEFAULT_FIELDS,\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n onSave,\n onCancel,\n loading = false,\n className,\n}: SmallEditPopupProps) {\n const [values, setValues] = useState<Record<string, string>>({});\n\n // Reset form values when dialog closes\n useEffect(() => {\n if (!open) {\n setValues({});\n }\n }, [open]);\n\n const handleChange = (id: string, value: string) => {\n setValues((prev) => ({ ...prev, [id]: value }));\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.(values);\n };\n\n // Group fields into rows: half-width fields paired together\n const rows: SmallEditField[][] = [];\n let i = 0;\n while (i < fields.length) {\n const field = fields[i];\n if (field.half && i + 1 < fields.length && fields[i + 1].half) {\n rows.push([field, fields[i + 1]]);\n i += 2;\n } else {\n rows.push([field]);\n i += 1;\n }\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n {/* Title & Description */}\n <div className=\"flex flex-col\">\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n </div>\n\n {/* Form fields */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-2xl)\" }}>\n {rows.map((row, rowIdx) => (\n <div\n key={rowIdx}\n className={cn(\n \"flex gap-[var(--spacing-3xl)]\",\n row.length > 1 ? \"flex-col md:flex-row\" : \"flex-col\"\n )}\n >\n {row.map((field) => (\n <div\n key={field.id}\n className={cn(\n \"flex flex-col\",\n row.length > 1 ? \"flex-1\" : \"w-full\"\n )}\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <Label>{field.label}</Label>\n <TextInput\n value={values[field.id] ?? \"\"}\n onChange={(e) => handleChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n />\n </div>\n ))}\n </div>\n ))}\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] justify-end\">\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleSave}\n disabled={loading}\n >\n {saveLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"dependencies": [],
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
{
|
|
16
16
|
"path": "components/blocks/terms-of-service-popup.tsx",
|
|
17
17
|
"type": "registry:block",
|
|
18
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TermsOfServicePopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** The scrollable terms content — accepts a string or React nodes */\n children?: React.ReactNode;\n /** Confirm / accept button label */\n confirmLabel?: string;\n /** Cancel / decline button label */\n cancelLabel?: string;\n /** Callback when the confirm button is clicked */\n onConfirm?: () => void;\n /** Callback when the cancel button is clicked */\n onCancel?: () => void;\n /** Disables the confirm button and shows a loading state */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default content\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Terms of Service\";\n\nconst DEFAULT_CONTENT = (\n <>\n <p>1. Terms</p>\n <br />\n <p>\n By accessing the website at http://sample.io, you are agreeing to be bound\n by these terms of service, all applicable laws and regulations, and agree\n that you are responsible for compliance with any applicable local laws. If\n you do not agree with any of these terms, you are prohibited from using or\n accessing this site. The materials contained in this website are protected\n by applicable copyright and trademark law.\n </p>\n <br />\n <p>2. Use License</p>\n <br />\n <p>\n Permission is granted to temporarily download one copy of the materials\n (information or software) on sample's website for personal,\n non-commercial transitory viewing only. This is the grant of a license,\n not a transfer of title, and under this license you may not: modify or\n copy the materials;\n </p>\n <p>\n use the materials for any commercial purpose, or for any public display\n (commercial or non-commercial);\n </p>\n </>\n);\n\n// ---------------------------------------------------------------------------\n// TermsOfServicePopup\n// ---------------------------------------------------------------------------\n\nexport function TermsOfServicePopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n children = DEFAULT_CONTENT,\n confirmLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n loading = false,\n className,\n}: TermsOfServicePopupProps) {\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleConfirm = () => {\n onConfirm?.();\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[
|
|
18
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TermsOfServicePopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** The scrollable terms content — accepts a string or React nodes */\n children?: React.ReactNode;\n /** Confirm / accept button label */\n confirmLabel?: string;\n /** Cancel / decline button label */\n cancelLabel?: string;\n /** Callback when the confirm button is clicked */\n onConfirm?: () => void;\n /** Callback when the cancel button is clicked */\n onCancel?: () => void;\n /** Disables the confirm button and shows a loading state */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default content\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Terms of Service\";\n\nconst DEFAULT_CONTENT = (\n <>\n <p>1. Terms</p>\n <br />\n <p>\n By accessing the website at http://sample.io, you are agreeing to be bound\n by these terms of service, all applicable laws and regulations, and agree\n that you are responsible for compliance with any applicable local laws. If\n you do not agree with any of these terms, you are prohibited from using or\n accessing this site. The materials contained in this website are protected\n by applicable copyright and trademark law.\n </p>\n <br />\n <p>2. Use License</p>\n <br />\n <p>\n Permission is granted to temporarily download one copy of the materials\n (information or software) on sample's website for personal,\n non-commercial transitory viewing only. This is the grant of a license,\n not a transfer of title, and under this license you may not: modify or\n copy the materials;\n </p>\n <p>\n use the materials for any commercial purpose, or for any public display\n (commercial or non-commercial);\n </p>\n </>\n);\n\n// ---------------------------------------------------------------------------\n// TermsOfServicePopup\n// ---------------------------------------------------------------------------\n\nexport function TermsOfServicePopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n children = DEFAULT_CONTENT,\n confirmLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n loading = false,\n className,\n}: TermsOfServicePopupProps) {\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleConfirm = () => {\n onConfirm?.();\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Scrollable content area */}\n <div\n className={cn(\n \"w-full rounded-[var(--radius-md)]\",\n \"bg-[var(--canvas-surface)]\",\n \"max-h-[360px] overflow-y-auto\",\n \"[&::-webkit-scrollbar]:w-[6px]\",\n \"[&::-webkit-scrollbar-track]:bg-[var(--canvas-border)]\",\n \"[&::-webkit-scrollbar-track]:rounded-full\",\n \"[&::-webkit-scrollbar-thumb]:bg-[var(--canvas-text-placeholder)]\",\n \"[&::-webkit-scrollbar-thumb]:rounded-full\"\n )}\n >\n <div\n className=\"p-[var(--spacing-xl)]\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n fontWeight: 400,\n color: \"var(--canvas-text)\",\n }}\n >\n {children}\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] justify-end\">\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleConfirm}\n disabled={loading}\n >\n {confirmLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"dependencies": [],
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
{
|
|
15
15
|
"path": "components/blocks/video-playlist.tsx",
|
|
16
16
|
"type": "registry:block",
|
|
17
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Minus, Plus, Play } from \"lucide-react\";\nimport { useState } from \"react\";\n\ninterface VideoPlaylistItemData {\n id: string;\n title: string;\n duration: string;\n thumbnail?: string;\n}\n\ninterface VideoPlaylistItemProps {\n /** Item index (1-based for display) */\n index: number;\n /** Item data */\n item: VideoPlaylistItemData;\n /** Whether this item is currently active/playing */\n isActive?: boolean;\n /** Click handler */\n onClick?: () => void;\n}\n\n/**\n * Individual lesson row in the playlist\n */\nexport function VideoPlaylistItem({\n index,\n item,\n isActive = false,\n onClick,\n}: VideoPlaylistItemProps) {\n return (\n <div\n className={cn(\n \"flex items-center gap-3 px-6 py-3 cursor-pointer transition-colors\",\n isActive\n ? \"bg-[var(--canvas-surface-brand)]\"\n : \"hover:bg-[var(--canvas-surface)]\"\n )}\n onClick={onClick}\n >\n {/* Index or Play Icon */}\n <div className=\"w-[26px] shrink-0 flex items-center justify-center\">\n {isActive ? (\n <Play\n className=\"w-5 h-5 text-[var(--canvas-primary)] fill-[var(--canvas-primary)]\"\n />\n ) : (\n <span \n className=\"text-[var(--canvas-text-placeholder)]\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-body-xl-spacing)\",\n lineHeight: \"var(--typo-body-xl-line-height)\",\n }}\n >\n {index}\n </span>\n )}\n </div>\n\n {/* Title and Duration */}\n <div className=\"flex-1 min-w-0 flex flex-col gap-1\">\n <p\n className={cn(\n isActive\n ? \"text-[var(--canvas-primary)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-body-s-spacing)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {item.title}\n </p>\n <p \n className=\"text-[var(--canvas-text-muted)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n letterSpacing: \"var(--typo-body-s-spacing)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {item.duration}\n </p>\n </div>\n </div>\n );\n}\n\ninterface VideoPlaylistCardProps {\n /** Card title */\n title?: string;\n /** Playlist items */\n items: VideoPlaylistItemData[];\n /** Currently active item ID */\n activeId?: string;\n /** Callback when an item is clicked */\n onItemClick?: (id: string) => void;\n /** Whether the playlist starts collapsed (mobile only) */\n defaultCollapsed?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Video Playlist Card Component\n * \n * A card container showing lesson list with collapsible behavior.\n * Features shadow, border, and hover states.\n * \n * @example\n * ```tsx\n * <VideoPlaylistCard\n * title=\"Lessons in this course\"\n * items={lessons}\n * activeId=\"lesson-2\"\n * onItemClick={(id) => setActiveLesson(id)}\n * />\n * ```\n */\nexport function VideoPlaylistCard({\n title = \"Lessons in this course\",\n items,\n activeId,\n onItemClick,\n defaultCollapsed = false,\n className,\n}: VideoPlaylistCardProps) {\n const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);\n\n return (\n <div\n className={cn(\n \"flex flex-col rounded-lg border border-[var(--canvas-border)]\",\n \"shadow-[
|
|
17
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Minus, Plus, Play } from \"lucide-react\";\nimport { useState } from \"react\";\n\ninterface VideoPlaylistItemData {\n id: string;\n title: string;\n duration: string;\n thumbnail?: string;\n}\n\ninterface VideoPlaylistItemProps {\n /** Item index (1-based for display) */\n index: number;\n /** Item data */\n item: VideoPlaylistItemData;\n /** Whether this item is currently active/playing */\n isActive?: boolean;\n /** Click handler */\n onClick?: () => void;\n}\n\n/**\n * Individual lesson row in the playlist\n */\nexport function VideoPlaylistItem({\n index,\n item,\n isActive = false,\n onClick,\n}: VideoPlaylistItemProps) {\n return (\n <div\n className={cn(\n \"flex items-center gap-3 px-6 py-3 cursor-pointer transition-colors\",\n isActive\n ? \"bg-[var(--canvas-surface-brand)]\"\n : \"hover:bg-[var(--canvas-surface)]\"\n )}\n onClick={onClick}\n >\n {/* Index or Play Icon */}\n <div className=\"w-[26px] shrink-0 flex items-center justify-center\">\n {isActive ? (\n <Play\n className=\"w-5 h-5 text-[var(--canvas-primary)] fill-[var(--canvas-primary)]\"\n />\n ) : (\n <span \n className=\"text-[var(--canvas-text-placeholder)]\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-body-xl-spacing)\",\n lineHeight: \"var(--typo-body-xl-line-height)\",\n }}\n >\n {index}\n </span>\n )}\n </div>\n\n {/* Title and Duration */}\n <div className=\"flex-1 min-w-0 flex flex-col gap-1\">\n <p\n className={cn(\n isActive\n ? \"text-[var(--canvas-primary)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-body-s-spacing)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {item.title}\n </p>\n <p \n className=\"text-[var(--canvas-text-muted)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n letterSpacing: \"var(--typo-body-s-spacing)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {item.duration}\n </p>\n </div>\n </div>\n );\n}\n\ninterface VideoPlaylistCardProps {\n /** Card title */\n title?: string;\n /** Playlist items */\n items: VideoPlaylistItemData[];\n /** Currently active item ID */\n activeId?: string;\n /** Callback when an item is clicked */\n onItemClick?: (id: string) => void;\n /** Whether the playlist starts collapsed (mobile only) */\n defaultCollapsed?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Video Playlist Card Component\n * \n * A card container showing lesson list with collapsible behavior.\n * Features shadow, border, and hover states.\n * \n * @example\n * ```tsx\n * <VideoPlaylistCard\n * title=\"Lessons in this course\"\n * items={lessons}\n * activeId=\"lesson-2\"\n * onItemClick={(id) => setActiveLesson(id)}\n * />\n * ```\n */\nexport function VideoPlaylistCard({\n title = \"Lessons in this course\",\n items,\n activeId,\n onItemClick,\n defaultCollapsed = false,\n className,\n}: VideoPlaylistCardProps) {\n const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);\n\n return (\n <div\n className={cn(\n \"flex flex-col rounded-lg border border-[var(--canvas-border)]\",\n \"shadow-[var(--canvas-shadow-nav)]\",\n \"bg-[var(--canvas-bg)]\",\n className\n )}\n >\n {/* Header */}\n <button\n type=\"button\"\n className=\"cursor-pointer flex items-center gap-2 p-4 border-b border-[var(--canvas-border)] text-left w-full\"\n onClick={() => setIsCollapsed(!isCollapsed)}\n >\n <div className=\"w-4 h-4 shrink-0 flex items-center justify-center\">\n {isCollapsed ? (\n <Plus className=\"w-4 h-4 text-[var(--canvas-text)]\" />\n ) : (\n <Minus className=\"w-4 h-4 text-[var(--canvas-text)]\" />\n )}\n </div>\n <h4 \n className=\"text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-body-xl-spacing)\",\n lineHeight: \"var(--typo-body-xl-line-height)\",\n }}\n >\n {title}\n </h4>\n </button>\n\n {/* List */}\n <div\n className={cn(\n \"overflow-hidden transition-all duration-200 ease-in-out\",\n isCollapsed ? \"max-h-0\" : \"max-h-[2000px]\"\n )}\n >\n <div className=\"py-4\">\n {items.map((item, index) => (\n <VideoPlaylistItem\n key={item.id}\n index={index + 1}\n item={item}\n isActive={item.id === activeId}\n onClick={() => onItemClick?.(item.id)}\n />\n ))}\n </div>\n </div>\n </div>\n );\n}\n\n"
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
20
|
"dependencies": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
{
|
|
15
15
|
"path": "components/blocks/video-popup.tsx",
|
|
16
16
|
"type": "registry:block",
|
|
17
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { YouTubePlayer } from \"./youtube-player\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface VideoPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** YouTube video ID (e.g., \"dQw4w9WgXcQ\") */\n videoId?: string;\n /** Accessible title for the video (sr-only) */\n title?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_VIDEO_ID = \"I1V9YWqRIeI\";\nconst DEFAULT_TITLE = \"Video player\";\n\n// ---------------------------------------------------------------------------\n// VideoPopup\n// ---------------------------------------------------------------------------\n\nexport function VideoPopup({\n open,\n onOpenChange,\n videoId = DEFAULT_VIDEO_ID,\n title = DEFAULT_TITLE,\n className,\n}: VideoPopupProps) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[
|
|
17
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { YouTubePlayer } from \"./youtube-player\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface VideoPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** YouTube video ID (e.g., \"dQw4w9WgXcQ\") */\n videoId?: string;\n /** Accessible title for the video (sr-only) */\n title?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_VIDEO_ID = \"I1V9YWqRIeI\";\nconst DEFAULT_TITLE = \"Video player\";\n\n// ---------------------------------------------------------------------------\n// VideoPopup\n// ---------------------------------------------------------------------------\n\nexport function VideoPopup({\n open,\n onOpenChange,\n videoId = DEFAULT_VIDEO_ID,\n title = DEFAULT_TITLE,\n className,\n}: VideoPopupProps) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[768px]\",\n className\n )}\n showCloseButton\n >\n {/* Visually hidden title for accessibility */}\n <DialogTitle className=\"sr-only\">{title}</DialogTitle>\n <DialogDescription className=\"sr-only\">\n Video player for {title}\n </DialogDescription>\n\n <YouTubePlayer videoId={videoId} className=\"rounded-none border-0\" />\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
20
|
"dependencies": [],
|