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.
Files changed (35) hide show
  1. package/README.md +2 -2
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.js +38 -30
  4. package/dist/index.js.map +1 -1
  5. package/package.json +1 -1
  6. package/registry/blocks/category-grid.json +1 -1
  7. package/registry/blocks/confirmation-popup.json +1 -1
  8. package/registry/blocks/contact-form-popup.json +1 -1
  9. package/registry/blocks/details-popup.json +1 -1
  10. package/registry/blocks/feedback-popup.json +1 -1
  11. package/registry/blocks/form-popup.json +1 -1
  12. package/registry/blocks/image-popup.json +1 -1
  13. package/registry/blocks/invoice-popup.json +1 -1
  14. package/registry/blocks/list-popup.json +1 -1
  15. package/registry/blocks/multistep-form-popup.json +1 -1
  16. package/registry/blocks/nps-survey-popup.json +1 -1
  17. package/registry/blocks/page-previews.json +1 -1
  18. package/registry/blocks/persona-card.json +1 -1
  19. package/registry/blocks/personalize-feed-popup.json +1 -1
  20. package/registry/blocks/pricing-plans-popup.json +1 -1
  21. package/registry/blocks/purchase-confirmation-popup.json +1 -1
  22. package/registry/blocks/share-project-popup.json +1 -1
  23. package/registry/blocks/small-edit-popup.json +1 -1
  24. package/registry/blocks/terms-of-service-popup.json +1 -1
  25. package/registry/blocks/video-playlist.json +1 -1
  26. package/registry/blocks/video-popup.json +1 -1
  27. package/registry/blocks/view-profile-popup.json +1 -1
  28. package/registry/layout/dashboard-shell.json +1 -1
  29. package/registry/layout/double-sidebar-shell.json +1 -1
  30. package/registry/layout/double-sidebar.json +1 -1
  31. package/registry/layout/icon-sidebar-shell.json +1 -1
  32. package/registry/ui/dropdown-menu.json +1 -1
  33. package/registry/ui/popover.json +1 -1
  34. package/registry/ui/select.json +1 -1
  35. package/styles/tokens.reference.css +7 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvas-ui-sdk",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "type": "module",
5
5
  "description": "A comprehensive UI component library with design tokens for building beautiful interfaces",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  {
15
15
  "path": "components/blocks/marketing/category-grid.tsx",
16
16
  "type": "registry:block",
17
- "content": "\"use client\";\n\nimport { \n Heart, Star, Sun, CurrencyDollar, Smiley, \n Image, Coffee, Moon, Clock, MapPin \n} from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface CategoryItem {\n id: string;\n title: string;\n count: string;\n icon: React.ReactNode;\n}\n\nconst defaultCategories: CategoryItem[] = [\n { id: \"1\", title: \"Most popular\", count: \"5,000 homes\", icon: <Heart size={48} /> },\n { id: \"2\", title: \"Top rated\", count: \"5,000 homes\", icon: <Star size={48} /> },\n { id: \"3\", title: \"Unique stays\", count: \"5,000 homes\", icon: <Sun size={48} /> },\n { id: \"4\", title: \"Affordable\", count: \"5,000 homes\", icon: <CurrencyDollar size={48} /> },\n { id: \"5\", title: \"Friendly staff\", count: \"5,000 homes\", icon: <Smiley size={48} /> },\n { id: \"6\", title: \"Best views\", count: \"5,000 homes\", icon: <Image size={48} /> },\n { id: \"7\", title: \"Cafes\", count: \"5,000 homes\", icon: <Coffee size={48} /> },\n { id: \"8\", title: \"Night life\", count: \"5,000 homes\", icon: <Moon size={48} /> },\n { id: \"9\", title: \"Open 24 hours\", count: \"5,000 homes\", icon: <Clock size={48} /> },\n { id: \"10\", title: \"Best locations\", count: \"5,000 homes\", icon: <MapPin size={48} /> },\n];\n\ninterface CategoryGridProps {\n title?: string;\n categories?: CategoryItem[];\n}\n\nexport function CategoryGrid({ \n title = \"Browse by category\", \n categories = defaultCategories \n}: CategoryGridProps) {\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-10 py-10 md:py-16\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n <div className=\"w-full max-w-[1240px] mx-auto\">\n {/* Header */}\n <Typography variant=\"h3\" as=\"h2\" style={{ marginBottom: \"var(--spacing-6xl)\" }}>\n {title}\n </Typography>\n\n {/* Categories Grid */}\n <div className=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-8\">\n {categories.map((category) => (\n <div \n key={category.id}\n className=\"flex flex-col items-center justify-center text-center cursor-pointer hover:shadow-md transition-shadow\"\n style={{\n height: \"158px\",\n padding: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--spacing-md)\",\n gap: \"var(--spacing-md)\",\n boxShadow: \"0px 1px 8px 0px rgba(0, 0, 0, 0.03)\",\n }}\n >\n <div style={{ color: \"var(--canvas-text)\" }}>\n {category.icon}\n </div>\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-xxs)\" }}>\n <Typography variant=\"body-xl\" className=\"font-semibold\">\n {category.title}\n </Typography>\n <Typography variant=\"body-m\" color=\"muted\">\n {category.count}\n </Typography>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n\n"
17
+ "content": "\"use client\";\n\nimport { \n Heart, Star, Sun, CurrencyDollar, Smiley, \n Image, Coffee, Moon, Clock, MapPin \n} from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface CategoryItem {\n id: string;\n title: string;\n count: string;\n icon: React.ReactNode;\n}\n\nconst defaultCategories: CategoryItem[] = [\n { id: \"1\", title: \"Most popular\", count: \"5,000 homes\", icon: <Heart size={48} /> },\n { id: \"2\", title: \"Top rated\", count: \"5,000 homes\", icon: <Star size={48} /> },\n { id: \"3\", title: \"Unique stays\", count: \"5,000 homes\", icon: <Sun size={48} /> },\n { id: \"4\", title: \"Affordable\", count: \"5,000 homes\", icon: <CurrencyDollar size={48} /> },\n { id: \"5\", title: \"Friendly staff\", count: \"5,000 homes\", icon: <Smiley size={48} /> },\n { id: \"6\", title: \"Best views\", count: \"5,000 homes\", icon: <Image size={48} /> },\n { id: \"7\", title: \"Cafes\", count: \"5,000 homes\", icon: <Coffee size={48} /> },\n { id: \"8\", title: \"Night life\", count: \"5,000 homes\", icon: <Moon size={48} /> },\n { id: \"9\", title: \"Open 24 hours\", count: \"5,000 homes\", icon: <Clock size={48} /> },\n { id: \"10\", title: \"Best locations\", count: \"5,000 homes\", icon: <MapPin size={48} /> },\n];\n\ninterface CategoryGridProps {\n title?: string;\n categories?: CategoryItem[];\n}\n\nexport function CategoryGrid({ \n title = \"Browse by category\", \n categories = defaultCategories \n}: CategoryGridProps) {\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-10 py-10 md:py-16\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n <div className=\"w-full max-w-[1240px] mx-auto\">\n {/* Header */}\n <Typography variant=\"h3\" as=\"h2\" style={{ marginBottom: \"var(--spacing-6xl)\" }}>\n {title}\n </Typography>\n\n {/* Categories Grid */}\n <div className=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-8\">\n {categories.map((category) => (\n <div \n key={category.id}\n className=\"flex flex-col items-center justify-center text-center cursor-pointer hover:shadow-[var(--canvas-shadow-card)] transition-shadow\"\n style={{\n height: \"158px\",\n padding: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--spacing-md)\",\n gap: \"var(--spacing-md)\",\n boxShadow: \"0px 1px 8px 0px rgba(0, 0, 0, 0.03)\",\n }}\n >\n <div style={{ color: \"var(--canvas-text)\" }}>\n {category.icon}\n </div>\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-xxs)\" }}>\n <Typography variant=\"body-xl\" className=\"font-semibold\">\n {category.title}\n </Typography>\n <Typography variant=\"body-m\" color=\"muted\">\n {category.count}\n </Typography>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n\n"
18
18
  }
19
19
  ],
20
20
  "dependencies": [
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/blocks/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 ConfirmationPopupProps {\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 /** Confirm button label — when omitted the dialog renders a single dismiss button (message mode) */\n confirmLabel?: string;\n /** Cancel / dismiss button label */\n cancelLabel?: string;\n /** Controls the confirm button style — \"destructive\" uses the delete variant, \"default\" uses primary */\n variant?: \"destructive\" | \"default\";\n /** Callback when the confirm button is clicked */\n onConfirm?: () => void;\n /** Callback when the cancel / dismiss 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 = \"Congratulations\";\nconst DEFAULT_DESCRIPTION =\n \"You have registered for our new service, and can now navigate to your portal to manage your account.\";\n\n// ---------------------------------------------------------------------------\n// ConfirmationPopup\n// ---------------------------------------------------------------------------\n\nexport function ConfirmationPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n confirmLabel,\n cancelLabel = \"Close\",\n variant = \"default\",\n onConfirm,\n onCancel,\n loading = false,\n className,\n}: ConfirmationPopupProps) {\n const isMessageMode = !confirmLabel;\n\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-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\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\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-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n\n {/* Actions */}\n <div\n className={cn(\n \"flex w-full gap-[var(--spacing-3xl)]\",\n isMessageMode\n ? \"justify-end\"\n : \"flex-col-reverse sm:flex-row\"\n )}\n >\n <Button\n variant=\"neutral\"\n className={isMessageMode ? undefined : \"flex-1\"}\n onClick={handleCancel}\n >\n {cancelLabel}\n </Button>\n {confirmLabel && (\n <Button\n variant={variant === \"destructive\" ? \"delete\" : \"primary\"}\n className=\"flex-1\"\n onClick={handleConfirm}\n disabled={loading}\n >\n {confirmLabel}\n </Button>\n )}\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ConfirmationPopupProps {\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 /** Confirm button label — when omitted the dialog renders a single dismiss button (message mode) */\n confirmLabel?: string;\n /** Cancel / dismiss button label */\n cancelLabel?: string;\n /** Controls the confirm button style — \"destructive\" uses the delete variant, \"default\" uses primary */\n variant?: \"destructive\" | \"default\";\n /** Callback when the confirm button is clicked */\n onConfirm?: () => void;\n /** Callback when the cancel / dismiss 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 = \"Congratulations\";\nconst DEFAULT_DESCRIPTION =\n \"You have registered for our new service, and can now navigate to your portal to manage your account.\";\n\n// ---------------------------------------------------------------------------\n// ConfirmationPopup\n// ---------------------------------------------------------------------------\n\nexport function ConfirmationPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n confirmLabel,\n cancelLabel = \"Close\",\n variant = \"default\",\n onConfirm,\n onCancel,\n loading = false,\n className,\n}: ConfirmationPopupProps) {\n const isMessageMode = !confirmLabel;\n\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\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-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n\n {/* Actions */}\n <div\n className={cn(\n \"flex w-full gap-[var(--spacing-3xl)]\",\n isMessageMode\n ? \"justify-end\"\n : \"flex-col-reverse sm:flex-row\"\n )}\n >\n <Button\n variant=\"neutral\"\n className={isMessageMode ? undefined : \"flex-1\"}\n onClick={handleCancel}\n >\n {cancelLabel}\n </Button>\n {confirmLabel && (\n <Button\n variant={variant === \"destructive\" ? \"delete\" : \"primary\"}\n className=\"flex-1\"\n onClick={handleConfirm}\n disabled={loading}\n >\n {confirmLabel}\n </Button>\n )}\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/contact-form-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 { Input } from \"../ui/input\";\nimport { Textarea } from \"../ui/textarea\";\nimport { Label } from \"../ui/label\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { GradientBanner } from \"./gradient-banner\";\nimport { AVATAR_MARCUS_WEBB } from \"./demo-avatars\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ContactFormField {\n /** Unique field identifier, used as key in the submitted values record */\n id: string;\n /** Label text displayed above the field */\n label: string;\n /** Input type — \"textarea\" renders a Textarea, all others render an Input */\n type?: \"text\" | \"email\" | \"tel\" | \"textarea\";\n /** Placeholder text */\n placeholder?: string;\n /** Whether the field is required */\n required?: boolean;\n /** When true the field takes 50% width and sits side-by-side with the next half field */\n half?: boolean;\n}\n\nexport interface ContactFormPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Contact name displayed in the title */\n name?: string;\n /** Descriptive text below the title */\n description?: string;\n /** Avatar image URL */\n avatarUrl?: string;\n /** Avatar fallback initials */\n avatarFallback?: string;\n /** Form field configuration */\n fields?: ContactFormField[];\n /** Submit button label */\n submitLabel?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Callback when the form is submitted — receives a record of field id → value */\n onSubmit?: (values: Record<string, string>) => void;\n /** Callback when the cancel button is clicked */\n onCancel?: () => void;\n /** Disables the submit button */\n loading?: boolean;\n /** Additional class names for the dialog content */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_NAME = \"Jeffrey Connor\";\nconst DEFAULT_DESCRIPTION =\n \"Send a message to Jeffrey and he will contact you within 24 hours.\";\nconst DEFAULT_AVATAR = AVATAR_MARCUS_WEBB;\nconst DEFAULT_AVATAR_FALLBACK = \"JC\";\n\nconst DEFAULT_FIELDS: ContactFormField[] = [\n { id: \"firstName\", label: \"First name\", half: true },\n { id: \"lastName\", label: \"Last name\", half: true },\n { id: \"email\", label: \"Email\", type: \"email\" },\n { id: \"message\", label: \"Message\", type: \"textarea\" },\n];\n\n// ---------------------------------------------------------------------------\n// ContactFormPopup\n// ---------------------------------------------------------------------------\n\nexport function ContactFormPopup({\n open,\n onOpenChange,\n name = DEFAULT_NAME,\n description = DEFAULT_DESCRIPTION,\n avatarUrl = DEFAULT_AVATAR,\n avatarFallback = DEFAULT_AVATAR_FALLBACK,\n fields = DEFAULT_FIELDS,\n submitLabel = \"Send message\",\n cancelLabel = \"Cancel\",\n onSubmit,\n onCancel,\n loading = false,\n className,\n}: ContactFormPopupProps) {\n const [values, setValues] = useState<Record<string, string>>({});\n\n // Reset form values when the 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 handleSubmit = () => {\n onSubmit?.(values);\n };\n\n // Group fields into rows: half-width fields are paired together\n const rows: ContactFormField[][] = [];\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-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n {/* Banner + Avatar section */}\n <div className=\"relative\">\n <GradientBanner\n height=\"160px\"\n className=\"rounded-t-[var(--radius-xl)]\"\n />\n\n {/* Avatar overlapping banner */}\n <div\n className=\"absolute bottom-0 translate-y-1/2\"\n style={{ left: \"var(--spacing-4xl)\" }}\n >\n <Avatar className=\"size-[125px] border-4 border-[var(--canvas-background)]\">\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback\n className=\"font-semibold bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\"\n style={{ fontSize: \"var(--typo-body-xl-size)\" }}\n >\n {avatarFallback}\n </AvatarFallback>\n </Avatar>\n </div>\n </div>\n\n {/* Spacer for avatar overflow */}\n <div className=\"h-[65px]\" />\n\n {/* Content */}\n <div\n className=\"flex flex-col\"\n style={{\n gap: \"var(--spacing-2xl)\",\n paddingLeft: \"var(--spacing-4xl)\",\n paddingRight: \"var(--spacing-4xl)\",\n paddingBottom: \"var(--spacing-4xl)\",\n }}\n >\n {/* 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 Contact {name}\n </DialogTitle>\n <DialogDescription\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 </DialogDescription>\n </div>\n\n {/* Form fields */}\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 {field.type === \"textarea\" ? (\n <Textarea\n inputSize=\"sm\"\n value={values[field.id] ?? \"\"}\n onChange={(e) => handleChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n required={field.required}\n className=\"resize-none\"\n />\n ) : (\n <Input\n type={field.type ?? \"text\"}\n value={values[field.id] ?? \"\"}\n onChange={(e) => handleChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n required={field.required}\n />\n )}\n </div>\n ))}\n </div>\n ))}\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={handleSubmit}\n disabled={loading}\n >\n {submitLabel}\n </Button>\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 { Input } from \"../ui/input\";\nimport { Textarea } from \"../ui/textarea\";\nimport { Label } from \"../ui/label\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { GradientBanner } from \"./gradient-banner\";\nimport { AVATAR_MARCUS_WEBB } from \"./demo-avatars\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ContactFormField {\n /** Unique field identifier, used as key in the submitted values record */\n id: string;\n /** Label text displayed above the field */\n label: string;\n /** Input type — \"textarea\" renders a Textarea, all others render an Input */\n type?: \"text\" | \"email\" | \"tel\" | \"textarea\";\n /** Placeholder text */\n placeholder?: string;\n /** Whether the field is required */\n required?: boolean;\n /** When true the field takes 50% width and sits side-by-side with the next half field */\n half?: boolean;\n}\n\nexport interface ContactFormPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Contact name displayed in the title */\n name?: string;\n /** Descriptive text below the title */\n description?: string;\n /** Avatar image URL */\n avatarUrl?: string;\n /** Avatar fallback initials */\n avatarFallback?: string;\n /** Form field configuration */\n fields?: ContactFormField[];\n /** Submit button label */\n submitLabel?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Callback when the form is submitted — receives a record of field id → value */\n onSubmit?: (values: Record<string, string>) => void;\n /** Callback when the cancel button is clicked */\n onCancel?: () => void;\n /** Disables the submit button */\n loading?: boolean;\n /** Additional class names for the dialog content */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_NAME = \"Jeffrey Connor\";\nconst DEFAULT_DESCRIPTION =\n \"Send a message to Jeffrey and he will contact you within 24 hours.\";\nconst DEFAULT_AVATAR = AVATAR_MARCUS_WEBB;\nconst DEFAULT_AVATAR_FALLBACK = \"JC\";\n\nconst DEFAULT_FIELDS: ContactFormField[] = [\n { id: \"firstName\", label: \"First name\", half: true },\n { id: \"lastName\", label: \"Last name\", half: true },\n { id: \"email\", label: \"Email\", type: \"email\" },\n { id: \"message\", label: \"Message\", type: \"textarea\" },\n];\n\n// ---------------------------------------------------------------------------\n// ContactFormPopup\n// ---------------------------------------------------------------------------\n\nexport function ContactFormPopup({\n open,\n onOpenChange,\n name = DEFAULT_NAME,\n description = DEFAULT_DESCRIPTION,\n avatarUrl = DEFAULT_AVATAR,\n avatarFallback = DEFAULT_AVATAR_FALLBACK,\n fields = DEFAULT_FIELDS,\n submitLabel = \"Send message\",\n cancelLabel = \"Cancel\",\n onSubmit,\n onCancel,\n loading = false,\n className,\n}: ContactFormPopupProps) {\n const [values, setValues] = useState<Record<string, string>>({});\n\n // Reset form values when the 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 handleSubmit = () => {\n onSubmit?.(values);\n };\n\n // Group fields into rows: half-width fields are paired together\n const rows: ContactFormField[][] = [];\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-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n {/* Banner + Avatar section */}\n <div className=\"relative\">\n <GradientBanner\n height=\"160px\"\n className=\"rounded-t-[var(--radius-xl)]\"\n />\n\n {/* Avatar overlapping banner */}\n <div\n className=\"absolute bottom-0 translate-y-1/2\"\n style={{ left: \"var(--spacing-4xl)\" }}\n >\n <Avatar className=\"size-[125px] border-4 border-[var(--canvas-background)]\">\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback\n className=\"font-semibold bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\"\n style={{ fontSize: \"var(--typo-body-xl-size)\" }}\n >\n {avatarFallback}\n </AvatarFallback>\n </Avatar>\n </div>\n </div>\n\n {/* Spacer for avatar overflow */}\n <div className=\"h-[65px]\" />\n\n {/* Content */}\n <div\n className=\"flex flex-col\"\n style={{\n gap: \"var(--spacing-2xl)\",\n paddingLeft: \"var(--spacing-4xl)\",\n paddingRight: \"var(--spacing-4xl)\",\n paddingBottom: \"var(--spacing-4xl)\",\n }}\n >\n {/* 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 Contact {name}\n </DialogTitle>\n <DialogDescription\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 </DialogDescription>\n </div>\n\n {/* Form fields */}\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 {field.type === \"textarea\" ? (\n <Textarea\n inputSize=\"sm\"\n value={values[field.id] ?? \"\"}\n onChange={(e) => handleChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n required={field.required}\n className=\"resize-none\"\n />\n ) : (\n <Input\n type={field.type ?? \"text\"}\n value={values[field.id] ?? \"\"}\n onChange={(e) => handleChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n required={field.required}\n />\n )}\n </div>\n ))}\n </div>\n ))}\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={handleSubmit}\n disabled={loading}\n >\n {submitLabel}\n </Button>\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/details-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\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DetailItem {\n /** Label displayed on the left side of the row */\n label: string;\n /** Value displayed on the right side — string for single-line, string[] for multi-line */\n value: string | string[];\n}\n\nexport interface DetailsPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Popup title displayed at the top */\n title?: string;\n /** Detail rows to display */\n details?: DetailItem[];\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Sony Alpha A7R Camera\";\n\nconst DEFAULT_DETAILS: DetailItem[] = [\n { label: \"Lens Mount\", value: \"Sony E\" },\n { label: \"Camera Format\", value: \"Full-Frame (1x Crop Factor)\" },\n {\n label: \"Pixels\",\n value: [\"Actual: 62.5 Megapixel\", \"Effective: 61 Megapixel\"],\n },\n { label: \"Aspect Ratio\", value: \"1:1, 3:2, 4:3, 16:9\" },\n { label: \"Sensor Type\", value: \"CMOS\" },\n { label: \"Sensor Size\", value: \"35.7 x 23.8 mm\" },\n { label: \"Image Format\", value: \"JPEG, RAW\" },\n {\n label: \"ISO Sensitivity\",\n value: \"Auto, 100 to 32000 (Extended: 50 to 102400)\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// DetailsPopup\n// ---------------------------------------------------------------------------\n\nexport function DetailsPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n details = DEFAULT_DETAILS,\n className,\n}: DetailsPopupProps) {\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 {/* Title */}\n <div\n className=\"flex flex-col\"\n style={{\n padding: \"var(--spacing-4xl)\",\n paddingBottom: 0,\n gap: \"var(--spacing-2xl)\",\n }}\n >\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 </div>\n\n {/* Visually hidden description for accessibility */}\n <DialogDescription className=\"sr-only\">\n Details for {title}\n </DialogDescription>\n\n {/* Detail rows */}\n <div\n className=\"flex flex-col\"\n style={{\n paddingLeft: \"var(--spacing-4xl)\",\n paddingRight: \"var(--spacing-4xl)\",\n paddingBottom: \"var(--spacing-4xl)\",\n paddingTop: \"var(--spacing-2xl)\",\n }}\n >\n {details.map((item, idx) => {\n const values = Array.isArray(item.value)\n ? item.value\n : [item.value];\n\n return (\n <div\n key={idx}\n className=\"flex gap-[var(--spacing-xl)] items-start w-full\"\n style={{\n paddingTop: \"var(--spacing-xl)\",\n paddingBottom: \"var(--spacing-xl)\",\n borderTop:\n idx === 0\n ? \"1px solid var(--canvas-border)\"\n : undefined,\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Label */}\n <span\n className=\"shrink-0 w-[160px]\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: 16,\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.label}\n </span>\n\n {/* Value */}\n <div\n className=\"flex-1 min-w-0 flex flex-col\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"24px\",\n fontWeight: 400,\n color: \"var(--canvas-text)\",\n }}\n >\n {values.map((line, i) => (\n <span key={i}>{line}</span>\n ))}\n </div>\n </div>\n );\n })}\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DetailItem {\n /** Label displayed on the left side of the row */\n label: string;\n /** Value displayed on the right side — string for single-line, string[] for multi-line */\n value: string | string[];\n}\n\nexport interface DetailsPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Popup title displayed at the top */\n title?: string;\n /** Detail rows to display */\n details?: DetailItem[];\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Sony Alpha A7R Camera\";\n\nconst DEFAULT_DETAILS: DetailItem[] = [\n { label: \"Lens Mount\", value: \"Sony E\" },\n { label: \"Camera Format\", value: \"Full-Frame (1x Crop Factor)\" },\n {\n label: \"Pixels\",\n value: [\"Actual: 62.5 Megapixel\", \"Effective: 61 Megapixel\"],\n },\n { label: \"Aspect Ratio\", value: \"1:1, 3:2, 4:3, 16:9\" },\n { label: \"Sensor Type\", value: \"CMOS\" },\n { label: \"Sensor Size\", value: \"35.7 x 23.8 mm\" },\n { label: \"Image Format\", value: \"JPEG, RAW\" },\n {\n label: \"ISO Sensitivity\",\n value: \"Auto, 100 to 32000 (Extended: 50 to 102400)\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// DetailsPopup\n// ---------------------------------------------------------------------------\n\nexport function DetailsPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n details = DEFAULT_DETAILS,\n className,\n}: DetailsPopupProps) {\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 {/* Title */}\n <div\n className=\"flex flex-col\"\n style={{\n padding: \"var(--spacing-4xl)\",\n paddingBottom: 0,\n gap: \"var(--spacing-2xl)\",\n }}\n >\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 </div>\n\n {/* Visually hidden description for accessibility */}\n <DialogDescription className=\"sr-only\">\n Details for {title}\n </DialogDescription>\n\n {/* Detail rows */}\n <div\n className=\"flex flex-col\"\n style={{\n paddingLeft: \"var(--spacing-4xl)\",\n paddingRight: \"var(--spacing-4xl)\",\n paddingBottom: \"var(--spacing-4xl)\",\n paddingTop: \"var(--spacing-2xl)\",\n }}\n >\n {details.map((item, idx) => {\n const values = Array.isArray(item.value)\n ? item.value\n : [item.value];\n\n return (\n <div\n key={idx}\n className=\"flex gap-[var(--spacing-xl)] items-start w-full\"\n style={{\n paddingTop: \"var(--spacing-xl)\",\n paddingBottom: \"var(--spacing-xl)\",\n borderTop:\n idx === 0\n ? \"1px solid var(--canvas-border)\"\n : undefined,\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Label */}\n <span\n className=\"shrink-0 w-[160px]\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: 16,\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.label}\n </span>\n\n {/* Value */}\n <div\n className=\"flex-1 min-w-0 flex flex-col\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"24px\",\n fontWeight: 400,\n color: \"var(--canvas-text)\",\n }}\n >\n {values.map((line, i) => (\n <span key={i}>{line}</span>\n ))}\n </div>\n </div>\n );\n })}\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/feedback-popup.tsx",
16
16
  "type": "registry:block",
17
- "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 { Textarea } from \"../ui/textarea\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackPopupProps {\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 /** Submit button label */\n submitLabel?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Textarea placeholder text */\n placeholder?: string;\n /** Callback when the submit button is clicked — receives the textarea value */\n onSubmit?: (value: string) => void;\n /** Callback when the cancel button is clicked */\n onCancel?: () => void;\n /** Disables the submit button and indicates a loading state */\n loading?: boolean;\n /** Controlled textarea value */\n value?: string;\n /** Controlled textarea change handler */\n onChange?: (value: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Help us improve\";\nconst DEFAULT_DESCRIPTION =\n \"Thank you for rating your experience! We welcome any comments or suggestions that you may have.\";\n\n// ---------------------------------------------------------------------------\n// FeedbackPopup\n// ---------------------------------------------------------------------------\n\n/**\n * A centered modal popup for collecting free-text feedback from users.\n *\n * @example\n * ```tsx\n * const [open, setOpen] = useState(false);\n *\n * <FeedbackPopup\n * open={open}\n * onOpenChange={setOpen}\n * onSubmit={(value) => console.log(\"Feedback:\", value)}\n * />\n * ```\n */\nexport function FeedbackPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n submitLabel = \"Submit\",\n cancelLabel = \"Cancel\",\n placeholder,\n onSubmit,\n onCancel,\n loading = false,\n value,\n onChange,\n className,\n}: FeedbackPopupProps) {\n const isControlled = value !== undefined;\n const [internalValue, setInternalValue] = useState(\"\");\n\n const textValue = isControlled ? value : internalValue;\n\n // Clear internal value when the dialog closes\n useEffect(() => {\n if (!open && !isControlled) {\n setInternalValue(\"\");\n }\n }, [open, isControlled]);\n\n const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n const next = e.target.value;\n if (isControlled) {\n onChange?.(next);\n } else {\n setInternalValue(next);\n }\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSubmit = () => {\n onSubmit?.(textValue);\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-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\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\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 </DialogDescription>\n\n {/* Textarea */}\n <Textarea\n inputSize=\"sm\"\n value={textValue}\n onChange={handleTextChange}\n placeholder={placeholder}\n className=\"resize-none\"\n />\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={handleSubmit}\n disabled={loading}\n >\n {submitLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
17
+ "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 { Textarea } from \"../ui/textarea\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackPopupProps {\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 /** Submit button label */\n submitLabel?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Textarea placeholder text */\n placeholder?: string;\n /** Callback when the submit button is clicked — receives the textarea value */\n onSubmit?: (value: string) => void;\n /** Callback when the cancel button is clicked */\n onCancel?: () => void;\n /** Disables the submit button and indicates a loading state */\n loading?: boolean;\n /** Controlled textarea value */\n value?: string;\n /** Controlled textarea change handler */\n onChange?: (value: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Help us improve\";\nconst DEFAULT_DESCRIPTION =\n \"Thank you for rating your experience! We welcome any comments or suggestions that you may have.\";\n\n// ---------------------------------------------------------------------------\n// FeedbackPopup\n// ---------------------------------------------------------------------------\n\n/**\n * A centered modal popup for collecting free-text feedback from users.\n *\n * @example\n * ```tsx\n * const [open, setOpen] = useState(false);\n *\n * <FeedbackPopup\n * open={open}\n * onOpenChange={setOpen}\n * onSubmit={(value) => console.log(\"Feedback:\", value)}\n * />\n * ```\n */\nexport function FeedbackPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n submitLabel = \"Submit\",\n cancelLabel = \"Cancel\",\n placeholder,\n onSubmit,\n onCancel,\n loading = false,\n value,\n onChange,\n className,\n}: FeedbackPopupProps) {\n const isControlled = value !== undefined;\n const [internalValue, setInternalValue] = useState(\"\");\n\n const textValue = isControlled ? value : internalValue;\n\n // Clear internal value when the dialog closes\n useEffect(() => {\n if (!open && !isControlled) {\n setInternalValue(\"\");\n }\n }, [open, isControlled]);\n\n const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n const next = e.target.value;\n if (isControlled) {\n onChange?.(next);\n } else {\n setInternalValue(next);\n }\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSubmit = () => {\n onSubmit?.(textValue);\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\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 </DialogDescription>\n\n {/* Textarea */}\n <Textarea\n inputSize=\"sm\"\n value={textValue}\n onChange={handleTextChange}\n placeholder={placeholder}\n className=\"resize-none\"\n />\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={handleSubmit}\n disabled={loading}\n >\n {submitLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
18
18
  }
19
19
  ],
20
20
  "dependencies": [],
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "path": "components/blocks/form-popup.tsx",
18
18
  "type": "registry:block",
19
- "content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { FormField, type FormFieldConfig } from \"./form-group\";\nimport type { UploadedImage } from \"../ui/image-uploader\";\nimport type { UploadedFile } from \"../ui/file-uploader\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FormPopupProps {\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 configurations */\n fields?: FormFieldConfig[];\n /** Cancel button label */\n cancelLabel?: string;\n /** Save button label */\n saveLabel?: string;\n /** Input size variant */\n inputSize?: \"sm\" | \"default\" | \"lg\";\n /** Callback when save is clicked */\n onSave?: () => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Callback when a field value changes */\n onFieldChange?: (fieldId: string, value: unknown) => 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 = \"Submit a request\";\nconst DEFAULT_DESCRIPTION = \"Fill out the form below and we'll get back to you within 24 hours.\";\n\nconst defaultSelectOptions = [\n { id: \"placeholder\", label: \"Select a category\" },\n { id: \"option-1\", label: \"General inquiry\" },\n { id: \"option-2\", label: \"Technical support\" },\n { id: \"option-3\", label: \"Billing question\" },\n];\n\nconst DEFAULT_FIELDS: FormFieldConfig[] = [\n { id: \"fp-1\", label: \"Full name\", type: \"text\", placeholder: \"Jane Doe\" },\n { id: \"fp-2\", label: \"Message\", type: \"textarea\", placeholder: \"Describe your request...\" },\n {\n id: \"fp-3\",\n label: \"Category\",\n type: \"select\",\n options: defaultSelectOptions,\n placeholder: \"Select a category\",\n },\n { id: \"fp-4\", label: \"Preferred date\", type: \"date\", placeholder: \"2/21/2024\" },\n { id: \"fp-5\", label: \"Phone number\", type: \"text\", placeholder: \"+1 (555) 123-4567\" },\n {\n id: \"fp-6\",\n label: \"Priority\",\n type: \"radio-group\",\n options: [\n { id: \"option-a\", label: \"Low\" },\n { id: \"option-b\", label: \"Medium\" },\n { id: \"option-c\", label: \"High\" },\n { id: \"option-d\", label: \"Urgent\" },\n ],\n },\n {\n id: \"fp-7\",\n label: \"Tags\",\n type: \"multiselect-tags\",\n value: [\"Bug report\", \"Feature request\"] as string[],\n },\n { id: \"fp-8\", label: \"Screenshot\", type: \"image-uploader\", placeholder: \"Drop image here\" },\n { id: \"fp-9\", label: \"Attachment\", type: \"file-uploader\", placeholder: \"Drop file here\" },\n {\n id: \"fp-10\",\n label: \"Agreement\",\n type: \"checkbox-group\",\n options: [{ id: \"cb-1\", label: \"I agree to the terms and conditions\" }],\n },\n {\n id: \"fp-11\",\n label: \"Satisfaction rating\",\n type: \"slider\",\n value: [0] as number[],\n min: 0,\n max: 1000,\n },\n {\n id: \"fp-12\",\n label: \"Related links\",\n type: \"list\",\n listItems: [\"https://example.com/issue-1\", \"https://example.com/docs\"],\n addPlaceholder: \"Add URL\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// FormPopup\n// ---------------------------------------------------------------------------\n\nexport function FormPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n fields = DEFAULT_FIELDS,\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n inputSize = \"default\",\n onSave,\n onCancel,\n onFieldChange,\n loading = false,\n className,\n}: FormPopupProps) {\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.();\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 \"max-h-[85vh] flex flex-col\",\n className\n )}\n showCloseButton\n >\n {/* Scrollable content */}\n <div\n className=\"flex flex-col overflow-y-auto flex-1\"\n style={{\n padding: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n }}\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 {fields.map((field) => (\n <FormField\n key={field.id}\n field={field}\n inputSize={inputSize}\n onChange={onFieldChange}\n />\n ))}\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 </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
19
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { FormField, type FormFieldConfig } from \"./form-group\";\nimport type { UploadedImage } from \"../ui/image-uploader\";\nimport type { UploadedFile } from \"../ui/file-uploader\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FormPopupProps {\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 configurations */\n fields?: FormFieldConfig[];\n /** Cancel button label */\n cancelLabel?: string;\n /** Save button label */\n saveLabel?: string;\n /** Input size variant */\n inputSize?: \"sm\" | \"default\" | \"lg\";\n /** Callback when save is clicked */\n onSave?: () => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Callback when a field value changes */\n onFieldChange?: (fieldId: string, value: unknown) => 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 = \"Submit a request\";\nconst DEFAULT_DESCRIPTION = \"Fill out the form below and we'll get back to you within 24 hours.\";\n\nconst defaultSelectOptions = [\n { id: \"placeholder\", label: \"Select a category\" },\n { id: \"option-1\", label: \"General inquiry\" },\n { id: \"option-2\", label: \"Technical support\" },\n { id: \"option-3\", label: \"Billing question\" },\n];\n\nconst DEFAULT_FIELDS: FormFieldConfig[] = [\n { id: \"fp-1\", label: \"Full name\", type: \"text\", placeholder: \"Jane Doe\" },\n { id: \"fp-2\", label: \"Message\", type: \"textarea\", placeholder: \"Describe your request...\" },\n {\n id: \"fp-3\",\n label: \"Category\",\n type: \"select\",\n options: defaultSelectOptions,\n placeholder: \"Select a category\",\n },\n { id: \"fp-4\", label: \"Preferred date\", type: \"date\", placeholder: \"2/21/2024\" },\n { id: \"fp-5\", label: \"Phone number\", type: \"text\", placeholder: \"+1 (555) 123-4567\" },\n {\n id: \"fp-6\",\n label: \"Priority\",\n type: \"radio-group\",\n options: [\n { id: \"option-a\", label: \"Low\" },\n { id: \"option-b\", label: \"Medium\" },\n { id: \"option-c\", label: \"High\" },\n { id: \"option-d\", label: \"Urgent\" },\n ],\n },\n {\n id: \"fp-7\",\n label: \"Tags\",\n type: \"multiselect-tags\",\n value: [\"Bug report\", \"Feature request\"] as string[],\n },\n { id: \"fp-8\", label: \"Screenshot\", type: \"image-uploader\", placeholder: \"Drop image here\" },\n { id: \"fp-9\", label: \"Attachment\", type: \"file-uploader\", placeholder: \"Drop file here\" },\n {\n id: \"fp-10\",\n label: \"Agreement\",\n type: \"checkbox-group\",\n options: [{ id: \"cb-1\", label: \"I agree to the terms and conditions\" }],\n },\n {\n id: \"fp-11\",\n label: \"Satisfaction rating\",\n type: \"slider\",\n value: [0] as number[],\n min: 0,\n max: 1000,\n },\n {\n id: \"fp-12\",\n label: \"Related links\",\n type: \"list\",\n listItems: [\"https://example.com/issue-1\", \"https://example.com/docs\"],\n addPlaceholder: \"Add URL\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// FormPopup\n// ---------------------------------------------------------------------------\n\nexport function FormPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n fields = DEFAULT_FIELDS,\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n inputSize = \"default\",\n onSave,\n onCancel,\n onFieldChange,\n loading = false,\n className,\n}: FormPopupProps) {\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.();\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 \"max-h-[85vh] flex flex-col\",\n className\n )}\n showCloseButton\n >\n {/* Scrollable content */}\n <div\n className=\"flex flex-col overflow-y-auto flex-1\"\n style={{\n padding: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n }}\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 {fields.map((field) => (\n <FormField\n key={field.id}\n field={field}\n inputSize={inputSize}\n onChange={onFieldChange}\n />\n ))}\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 </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
20
20
  }
21
21
  ],
22
22
  "dependencies": [],
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/blocks/image-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\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ImagePopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Image source URL */\n src?: string;\n /** Alt text for the image */\n alt?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SRC =\n \"https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=1200&q=80\";\nconst DEFAULT_ALT = \"Luxury bedroom interior\";\n\n// ---------------------------------------------------------------------------\n// ImagePopup\n// ---------------------------------------------------------------------------\n\nexport function ImagePopup({\n open,\n onOpenChange,\n src = DEFAULT_SRC,\n alt = DEFAULT_ALT,\n className,\n}: ImagePopupProps) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[768px]\",\n className\n )}\n showCloseButton\n >\n {/* Visually hidden title for accessibility */}\n <DialogTitle className=\"sr-only\">{alt}</DialogTitle>\n <DialogDescription className=\"sr-only\">\n Enlarged view of {alt}\n </DialogDescription>\n\n <img\n src={src}\n alt={alt}\n className=\"w-full h-auto object-cover rounded-[var(--radius-xl)]\"\n />\n </DialogContent>\n </Dialog>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ImagePopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Image source URL */\n src?: string;\n /** Alt text for the image */\n alt?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SRC =\n \"https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=1200&q=80\";\nconst DEFAULT_ALT = \"Luxury bedroom interior\";\n\n// ---------------------------------------------------------------------------\n// ImagePopup\n// ---------------------------------------------------------------------------\n\nexport function ImagePopup({\n open,\n onOpenChange,\n src = DEFAULT_SRC,\n alt = DEFAULT_ALT,\n className,\n}: ImagePopupProps) {\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\">{alt}</DialogTitle>\n <DialogDescription className=\"sr-only\">\n Enlarged view of {alt}\n </DialogDescription>\n\n <img\n src={src}\n alt={alt}\n className=\"w-full h-auto object-cover rounded-[var(--radius-xl)]\"\n />\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/invoice-popup.tsx",
17
17
  "type": "registry:block",
18
- "content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { LayoutGrid } from \"lucide-react\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface InvoiceLineItem {\n description: string;\n quantity: number;\n amount: string;\n}\n\nexport interface InvoicePopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Custom logo icon — defaults to a grid icon */\n logoIcon?: React.ReactNode;\n /** Invoice title */\n title?: string;\n /** Invoice subtitle / description */\n subtitle?: string;\n /** Invoice number */\n invoiceNumber?: string;\n /** Invoice date */\n invoiceDate?: string;\n /** Recipient name */\n recipientName?: string;\n /** Recipient address */\n recipientAddress?: string;\n /** Line items to display in the table */\n lineItems?: InvoiceLineItem[];\n /** Formatted subtotal */\n subtotal?: string;\n /** Formatted discount (e.g. \"-$300\") — rendered in destructive color */\n discount?: string;\n /** Formatted total */\n total?: string;\n /** Action button label */\n actionLabel?: string;\n /** Callback when the action button is clicked */\n onAction?: () => void;\n /** Disables the action button */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Invoice\";\nconst DEFAULT_SUBTITLE = \"App development milestone #4\";\nconst DEFAULT_INVOICE_NUMBER = \"#0023\";\nconst DEFAULT_INVOICE_DATE = \"April 24, 2024\";\nconst DEFAULT_RECIPIENT_NAME = \"Raj Mishra\";\nconst DEFAULT_RECIPIENT_ADDRESS = \"123 Market St. SF, CA 94102\";\n\nconst DEFAULT_LINE_ITEMS: InvoiceLineItem[] = [\n { description: \"Scope wireframes\", quantity: 1, amount: \"$300\" },\n { description: \"Milestone #1\", quantity: 1, amount: \"$2,000\" },\n { description: \"Milestone #2\", quantity: 1, amount: \"$2,000\" },\n];\n\nconst DEFAULT_SUBTOTAL = \"$4,300\";\nconst DEFAULT_DISCOUNT = \"-$300\";\nconst DEFAULT_TOTAL = \"$4,000\";\nconst DEFAULT_ACTION_LABEL = \"Pay invoice now\";\n\n// ---------------------------------------------------------------------------\n// Shared typography styles\n// ---------------------------------------------------------------------------\n\nconst labelStyle: React.CSSProperties = {\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 fontWeight: 500,\n color: \"var(--canvas-text-muted)\",\n};\n\nconst valueStyle: React.CSSProperties = {\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"24px\",\n fontWeight: 400,\n color: \"var(--canvas-text)\",\n};\n\nconst summaryLabelStyle: React.CSSProperties = {\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"16px\",\n lineHeight: \"24px\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n};\n\nconst summaryValueStyle: React.CSSProperties = {\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"24px\",\n fontWeight: 400,\n color: \"var(--canvas-text-muted)\",\n};\n\n// ---------------------------------------------------------------------------\n// InvoicePopup\n// ---------------------------------------------------------------------------\n\nexport function InvoicePopup({\n open,\n onOpenChange,\n logoIcon,\n title = DEFAULT_TITLE,\n subtitle = DEFAULT_SUBTITLE,\n invoiceNumber = DEFAULT_INVOICE_NUMBER,\n invoiceDate = DEFAULT_INVOICE_DATE,\n recipientName = DEFAULT_RECIPIENT_NAME,\n recipientAddress = DEFAULT_RECIPIENT_ADDRESS,\n lineItems = DEFAULT_LINE_ITEMS,\n subtotal = DEFAULT_SUBTOTAL,\n discount = DEFAULT_DISCOUNT,\n total = DEFAULT_TOTAL,\n actionLabel = DEFAULT_ACTION_LABEL,\n onAction,\n loading = false,\n className,\n}: InvoicePopupProps) {\n const defaultLogo = (\n <div\n className=\"flex items-center justify-center rounded-[var(--radius-md)]\"\n style={{\n width: 48,\n height: 48,\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n <LayoutGrid size={24} />\n </div>\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 {/* ---- Header zone ---- */}\n <div\n className=\"flex flex-col gap-[var(--spacing-4xl)] p-[var(--spacing-4xl)]\"\n style={{ backgroundColor: \"var(--canvas-border)\" }}\n >\n {/* Logo + Title row */}\n <div className=\"flex gap-[var(--spacing-xl)] items-start\">\n <div className=\"shrink-0\">{logoIcon ?? defaultLogo}</div>\n <div className=\"flex flex-col min-w-0\">\n <DialogTitle\n style={{\n fontFamily:\n \"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:\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: 400,\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {subtitle}\n </DialogDescription>\n </div>\n </div>\n\n {/* Metadata grid */}\n <div className=\"flex flex-col gap-[var(--spacing-md)]\">\n {/* Row 1: Invoice no. / Recipient */}\n <div className=\"flex flex-col sm:flex-row gap-[var(--spacing-3xl)]\">\n <div className=\"flex-1 flex flex-col gap-[var(--spacing-xxs)]\">\n <span style={labelStyle}>Invoice no.</span>\n <span style={valueStyle}>{invoiceNumber}</span>\n </div>\n <div className=\"flex-1 flex flex-col gap-[var(--spacing-xxs)]\">\n <span style={labelStyle}>Recipient</span>\n <span style={valueStyle}>{recipientName}</span>\n </div>\n </div>\n {/* Row 2: Invoice date / Address */}\n <div className=\"flex flex-col sm:flex-row gap-[var(--spacing-3xl)]\">\n <div className=\"flex-1 flex flex-col gap-[var(--spacing-xxs)]\">\n <span style={labelStyle}>Invoice date</span>\n <span style={valueStyle}>{invoiceDate}</span>\n </div>\n <div className=\"flex-1 flex flex-col gap-[var(--spacing-xxs)]\">\n <span style={labelStyle}>Address</span>\n <span style={valueStyle}>{recipientAddress}</span>\n </div>\n </div>\n </div>\n </div>\n\n {/* ---- Line items zone ---- */}\n <div className=\"flex flex-col gap-[var(--spacing-2xl)] items-end p-[var(--spacing-4xl)]\">\n {/* Table */}\n <div className=\"w-full flex flex-col\">\n {/* Table header */}\n <div\n className=\"flex gap-[var(--spacing-md)] pb-[var(--spacing-md)]\"\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)\",\n }}\n >\n <div className=\"flex-1\">Description</div>\n <div className=\"w-[80px]\">Quantity</div>\n <div className=\"w-[100px] sm:w-[140px] text-right\">Amount</div>\n </div>\n\n {/* Line item rows */}\n {lineItems.map((item, i) => (\n <div\n key={i}\n className={cn(\n \"flex gap-[var(--spacing-md)] items-center h-[44px]\",\n \"border-t border-b border-[var(--canvas-border)]\",\n \"py-[var(--spacing-lg)]\"\n )}\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"24px\",\n fontWeight: 400,\n color: \"var(--canvas-text-muted)\",\n }}\n >\n <div className=\"flex-1 truncate\">{item.description}</div>\n <div className=\"w-[80px]\">{item.quantity}</div>\n <div className=\"w-[100px] sm:w-[140px] text-right\">\n {item.amount}\n </div>\n </div>\n ))}\n\n {/* Summary rows — right-aligned */}\n {subtotal && (\n <div\n className={cn(\n \"flex gap-[var(--spacing-md)] items-center h-[44px]\",\n \"border-b border-[var(--canvas-border)]\",\n \"py-[var(--spacing-lg)] self-end\"\n )}\n style={{ width: \"fit-content\" }}\n >\n <div className=\"w-[80px]\" style={summaryLabelStyle}>\n Subtotal\n </div>\n <div\n className=\"w-[100px] sm:w-[140px] text-right\"\n style={summaryValueStyle}\n >\n {subtotal}\n </div>\n </div>\n )}\n\n {discount && (\n <div\n className={cn(\n \"flex gap-[var(--spacing-md)] items-center h-[44px]\",\n \"border-b border-[var(--canvas-border)]\",\n \"py-[var(--spacing-lg)] self-end\"\n )}\n style={{ width: \"fit-content\" }}\n >\n <div className=\"w-[80px]\" style={summaryLabelStyle}>\n Discount\n </div>\n <div\n className=\"w-[100px] sm:w-[140px] text-right\"\n style={{\n ...summaryValueStyle,\n color: \"var(--canvas-destructive)\",\n }}\n >\n {discount}\n </div>\n </div>\n )}\n\n {total && (\n <div\n className={cn(\n \"flex gap-[var(--spacing-md)] items-center h-[44px]\",\n \"border-b border-[var(--canvas-border)]\",\n \"py-[var(--spacing-lg)] self-end\"\n )}\n style={{ width: \"fit-content\" }}\n >\n <div className=\"w-[80px]\" style={summaryLabelStyle}>\n Total\n </div>\n <div\n className=\"w-[100px] sm:w-[140px] text-right\"\n style={summaryLabelStyle}\n >\n {total}\n </div>\n </div>\n )}\n </div>\n\n {/* Action button */}\n <Button\n variant=\"primary\"\n onClick={onAction}\n disabled={loading}\n >\n {actionLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { LayoutGrid } from \"lucide-react\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface InvoiceLineItem {\n description: string;\n quantity: number;\n amount: string;\n}\n\nexport interface InvoicePopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Custom logo icon — defaults to a grid icon */\n logoIcon?: React.ReactNode;\n /** Invoice title */\n title?: string;\n /** Invoice subtitle / description */\n subtitle?: string;\n /** Invoice number */\n invoiceNumber?: string;\n /** Invoice date */\n invoiceDate?: string;\n /** Recipient name */\n recipientName?: string;\n /** Recipient address */\n recipientAddress?: string;\n /** Line items to display in the table */\n lineItems?: InvoiceLineItem[];\n /** Formatted subtotal */\n subtotal?: string;\n /** Formatted discount (e.g. \"-$300\") — rendered in destructive color */\n discount?: string;\n /** Formatted total */\n total?: string;\n /** Action button label */\n actionLabel?: string;\n /** Callback when the action button is clicked */\n onAction?: () => void;\n /** Disables the action button */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Invoice\";\nconst DEFAULT_SUBTITLE = \"App development milestone #4\";\nconst DEFAULT_INVOICE_NUMBER = \"#0023\";\nconst DEFAULT_INVOICE_DATE = \"April 24, 2024\";\nconst DEFAULT_RECIPIENT_NAME = \"Raj Mishra\";\nconst DEFAULT_RECIPIENT_ADDRESS = \"123 Market St. SF, CA 94102\";\n\nconst DEFAULT_LINE_ITEMS: InvoiceLineItem[] = [\n { description: \"Scope wireframes\", quantity: 1, amount: \"$300\" },\n { description: \"Milestone #1\", quantity: 1, amount: \"$2,000\" },\n { description: \"Milestone #2\", quantity: 1, amount: \"$2,000\" },\n];\n\nconst DEFAULT_SUBTOTAL = \"$4,300\";\nconst DEFAULT_DISCOUNT = \"-$300\";\nconst DEFAULT_TOTAL = \"$4,000\";\nconst DEFAULT_ACTION_LABEL = \"Pay invoice now\";\n\n// ---------------------------------------------------------------------------\n// Shared typography styles\n// ---------------------------------------------------------------------------\n\nconst labelStyle: React.CSSProperties = {\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 fontWeight: 500,\n color: \"var(--canvas-text-muted)\",\n};\n\nconst valueStyle: React.CSSProperties = {\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"24px\",\n fontWeight: 400,\n color: \"var(--canvas-text)\",\n};\n\nconst summaryLabelStyle: React.CSSProperties = {\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"16px\",\n lineHeight: \"24px\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n};\n\nconst summaryValueStyle: React.CSSProperties = {\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"24px\",\n fontWeight: 400,\n color: \"var(--canvas-text-muted)\",\n};\n\n// ---------------------------------------------------------------------------\n// InvoicePopup\n// ---------------------------------------------------------------------------\n\nexport function InvoicePopup({\n open,\n onOpenChange,\n logoIcon,\n title = DEFAULT_TITLE,\n subtitle = DEFAULT_SUBTITLE,\n invoiceNumber = DEFAULT_INVOICE_NUMBER,\n invoiceDate = DEFAULT_INVOICE_DATE,\n recipientName = DEFAULT_RECIPIENT_NAME,\n recipientAddress = DEFAULT_RECIPIENT_ADDRESS,\n lineItems = DEFAULT_LINE_ITEMS,\n subtotal = DEFAULT_SUBTOTAL,\n discount = DEFAULT_DISCOUNT,\n total = DEFAULT_TOTAL,\n actionLabel = DEFAULT_ACTION_LABEL,\n onAction,\n loading = false,\n className,\n}: InvoicePopupProps) {\n const defaultLogo = (\n <div\n className=\"flex items-center justify-center rounded-[var(--radius-md)]\"\n style={{\n width: 48,\n height: 48,\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n <LayoutGrid size={24} />\n </div>\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 {/* ---- Header zone ---- */}\n <div\n className=\"flex flex-col gap-[var(--spacing-4xl)] p-[var(--spacing-4xl)]\"\n style={{ backgroundColor: \"var(--canvas-border)\" }}\n >\n {/* Logo + Title row */}\n <div className=\"flex gap-[var(--spacing-xl)] items-start\">\n <div className=\"shrink-0\">{logoIcon ?? defaultLogo}</div>\n <div className=\"flex flex-col min-w-0\">\n <DialogTitle\n style={{\n fontFamily:\n \"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:\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: 400,\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {subtitle}\n </DialogDescription>\n </div>\n </div>\n\n {/* Metadata grid */}\n <div className=\"flex flex-col gap-[var(--spacing-md)]\">\n {/* Row 1: Invoice no. / Recipient */}\n <div className=\"flex flex-col sm:flex-row gap-[var(--spacing-3xl)]\">\n <div className=\"flex-1 flex flex-col gap-[var(--spacing-xxs)]\">\n <span style={labelStyle}>Invoice no.</span>\n <span style={valueStyle}>{invoiceNumber}</span>\n </div>\n <div className=\"flex-1 flex flex-col gap-[var(--spacing-xxs)]\">\n <span style={labelStyle}>Recipient</span>\n <span style={valueStyle}>{recipientName}</span>\n </div>\n </div>\n {/* Row 2: Invoice date / Address */}\n <div className=\"flex flex-col sm:flex-row gap-[var(--spacing-3xl)]\">\n <div className=\"flex-1 flex flex-col gap-[var(--spacing-xxs)]\">\n <span style={labelStyle}>Invoice date</span>\n <span style={valueStyle}>{invoiceDate}</span>\n </div>\n <div className=\"flex-1 flex flex-col gap-[var(--spacing-xxs)]\">\n <span style={labelStyle}>Address</span>\n <span style={valueStyle}>{recipientAddress}</span>\n </div>\n </div>\n </div>\n </div>\n\n {/* ---- Line items zone ---- */}\n <div className=\"flex flex-col gap-[var(--spacing-2xl)] items-end p-[var(--spacing-4xl)]\">\n {/* Table */}\n <div className=\"w-full flex flex-col\">\n {/* Table header */}\n <div\n className=\"flex gap-[var(--spacing-md)] pb-[var(--spacing-md)]\"\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)\",\n }}\n >\n <div className=\"flex-1\">Description</div>\n <div className=\"w-[80px]\">Quantity</div>\n <div className=\"w-[100px] sm:w-[140px] text-right\">Amount</div>\n </div>\n\n {/* Line item rows */}\n {lineItems.map((item, i) => (\n <div\n key={i}\n className={cn(\n \"flex gap-[var(--spacing-md)] items-center h-[44px]\",\n \"border-t border-b border-[var(--canvas-border)]\",\n \"py-[var(--spacing-lg)]\"\n )}\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"24px\",\n fontWeight: 400,\n color: \"var(--canvas-text-muted)\",\n }}\n >\n <div className=\"flex-1 truncate\">{item.description}</div>\n <div className=\"w-[80px]\">{item.quantity}</div>\n <div className=\"w-[100px] sm:w-[140px] text-right\">\n {item.amount}\n </div>\n </div>\n ))}\n\n {/* Summary rows — right-aligned */}\n {subtotal && (\n <div\n className={cn(\n \"flex gap-[var(--spacing-md)] items-center h-[44px]\",\n \"border-b border-[var(--canvas-border)]\",\n \"py-[var(--spacing-lg)] self-end\"\n )}\n style={{ width: \"fit-content\" }}\n >\n <div className=\"w-[80px]\" style={summaryLabelStyle}>\n Subtotal\n </div>\n <div\n className=\"w-[100px] sm:w-[140px] text-right\"\n style={summaryValueStyle}\n >\n {subtotal}\n </div>\n </div>\n )}\n\n {discount && (\n <div\n className={cn(\n \"flex gap-[var(--spacing-md)] items-center h-[44px]\",\n \"border-b border-[var(--canvas-border)]\",\n \"py-[var(--spacing-lg)] self-end\"\n )}\n style={{ width: \"fit-content\" }}\n >\n <div className=\"w-[80px]\" style={summaryLabelStyle}>\n Discount\n </div>\n <div\n className=\"w-[100px] sm:w-[140px] text-right\"\n style={{\n ...summaryValueStyle,\n color: \"var(--canvas-destructive)\",\n }}\n >\n {discount}\n </div>\n </div>\n )}\n\n {total && (\n <div\n className={cn(\n \"flex gap-[var(--spacing-md)] items-center h-[44px]\",\n \"border-b border-[var(--canvas-border)]\",\n \"py-[var(--spacing-lg)] self-end\"\n )}\n style={{ width: \"fit-content\" }}\n >\n <div className=\"w-[80px]\" style={summaryLabelStyle}>\n Total\n </div>\n <div\n className=\"w-[100px] sm:w-[140px] text-right\"\n style={summaryLabelStyle}\n >\n {total}\n </div>\n </div>\n )}\n </div>\n\n {/* Action button */}\n <Button\n variant=\"primary\"\n onClick={onAction}\n disabled={loading}\n >\n {actionLabel}\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/list-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} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { EditableList } from \"./editable-list\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ListPopupProps {\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 /** Label above the list */\n listLabel?: string;\n /** Initial list items */\n items?: string[];\n /** Callback when save is clicked — receives the current list items */\n onSave?: (items: string[]) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Placeholder text for the add input */\n addPlaceholder?: string;\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 = \"Make a list\";\nconst DEFAULT_LIST_LABEL = \"List\";\nconst DEFAULT_ITEMS = [\"Finance\", \"Technology\", \"Retail\", \"Real Estate\"];\n\n// ---------------------------------------------------------------------------\n// ListPopup\n// ---------------------------------------------------------------------------\n\nexport function ListPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n listLabel = DEFAULT_LIST_LABEL,\n items: initialItems = DEFAULT_ITEMS,\n onSave,\n onCancel,\n addPlaceholder = \"Enter category\",\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n className,\n}: ListPopupProps) {\n const [currentItems, setCurrentItems] = useState<string[]>(initialItems);\n\n // Reset items when dialog opens\n useEffect(() => {\n if (open) {\n setCurrentItems(initialItems);\n }\n }, [open, initialItems]);\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.(currentItems);\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-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\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 {/* Editable List */}\n <EditableList\n label={listLabel}\n items={currentItems}\n onItemsChange={setCurrentItems}\n addPlaceholder={addPlaceholder}\n />\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"
18
+ "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { EditableList } from \"./editable-list\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ListPopupProps {\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 /** Label above the list */\n listLabel?: string;\n /** Initial list items */\n items?: string[];\n /** Callback when save is clicked — receives the current list items */\n onSave?: (items: string[]) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Placeholder text for the add input */\n addPlaceholder?: string;\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 = \"Make a list\";\nconst DEFAULT_LIST_LABEL = \"List\";\nconst DEFAULT_ITEMS = [\"Finance\", \"Technology\", \"Retail\", \"Real Estate\"];\n\n// ---------------------------------------------------------------------------\n// ListPopup\n// ---------------------------------------------------------------------------\n\nexport function ListPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n listLabel = DEFAULT_LIST_LABEL,\n items: initialItems = DEFAULT_ITEMS,\n onSave,\n onCancel,\n addPlaceholder = \"Enter category\",\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n className,\n}: ListPopupProps) {\n const [currentItems, setCurrentItems] = useState<string[]>(initialItems);\n\n // Reset items when dialog opens\n useEffect(() => {\n if (open) {\n setCurrentItems(initialItems);\n }\n }, [open, initialItems]);\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.(currentItems);\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-[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 {/* Editable List */}\n <EditableList\n label={listLabel}\n items={currentItems}\n onItemsChange={setCurrentItems}\n addPlaceholder={addPlaceholder}\n />\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/multistep-form-popup.tsx",
17
17
  "type": "registry:block",
18
- "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Dialog, DialogContent } from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { Label } from \"../ui/label\";\nimport { Input } from \"../ui/input\";\nimport { Textarea } from \"../ui/textarea\";\nimport { DateInput } from \"../ui/date-input\";\nimport {\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n} from \"../ui/select\";\nimport { MultiselectTags } from \"../ui/multiselect-tags\";\nimport { Loader } from \"./loader\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MultistepField {\n /** Unique field identifier */\n id: string;\n /** Field label */\n label: string;\n /** Field input type */\n type:\n | \"text\"\n | \"select\"\n | \"date\"\n | \"textarea\"\n | \"multiselect-tags\";\n /** Placeholder text */\n placeholder?: string;\n /** When true, the field takes 50% width and pairs with the next half field */\n half?: boolean;\n /** Options for select or multiselect-tags */\n options?: { id: string; label: string }[];\n /** Default value */\n value?: string | string[];\n}\n\nexport interface MultistepFormStep {\n /** Step title */\n title: string;\n /** Step description shown below the title */\n description?: string;\n /** Form fields for this step */\n fields: MultistepField[];\n}\n\nexport interface BillingPlan {\n /** Unique plan identifier */\n id: string;\n /** Plan name */\n name: string;\n /** Price display text */\n price: string;\n /** Description text */\n description: string;\n /** Optional badge text */\n badge?: string;\n}\n\nexport interface MultistepFormPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Step configurations — each has title, description, and fields */\n steps?: MultistepFormStep[];\n /** Billing plan options for the final step */\n billingPlans?: BillingPlan[];\n /** Loading state title */\n loadingTitle?: string;\n /** Loading state description */\n loadingDescription?: string;\n /** Success state title */\n successTitle?: string;\n /** Success state description */\n successDescription?: string;\n /** Success action button text */\n successButtonText?: string;\n /** Callback when the form is completed (after all steps) */\n onComplete?: () => void;\n /** Callback when the success action button is clicked */\n onSuccessButtonClick?: () => void;\n /** Additional class names for the dialog content */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_STEPS: MultistepFormStep[] = [\n {\n title: \"Enter your personal information to get started\",\n fields: [\n { id: \"email\", label: \"Email\", type: \"text\", placeholder: \"jane@example.com\" },\n { id: \"company\", label: \"Company name\", type: \"text\", placeholder: \"Acme Inc.\" },\n { id: \"firstName\", label: \"First name\", type: \"text\", placeholder: \"Jane\", half: true },\n { id: \"lastName\", label: \"Last name\", type: \"text\", placeholder: \"Doe\", half: true },\n { id: \"city\", label: \"City\", type: \"text\", placeholder: \"San Francisco\" },\n {\n id: \"country\",\n label: \"Country\",\n type: \"select\",\n half: true,\n options: [\n { id: \"us\", label: \"United States\" },\n { id: \"ca\", label: \"Canada\" },\n { id: \"uk\", label: \"United Kingdom\" },\n { id: \"au\", label: \"Australia\" },\n ],\n },\n {\n id: \"state\",\n label: \"State\",\n type: \"select\",\n half: true,\n options: [\n { id: \"ca\", label: \"California\" },\n { id: \"ny\", label: \"New York\" },\n { id: \"tx\", label: \"Texas\" },\n { id: \"wa\", label: \"Washington\" },\n ],\n },\n { id: \"zip\", label: \"Zip / Postal code\", type: \"text\", placeholder: \"94102\" },\n { id: \"profession\", label: \"Profession\", type: \"text\", placeholder: \"Software Engineer\", half: true },\n { id: \"startDate\", label: \"Start date\", type: \"date\", half: true },\n ],\n },\n {\n title: \"Enter additional information below\",\n fields: [\n { id: \"address\", label: \"Address\", type: \"text\", placeholder: \"123 Main St\" },\n { id: \"stateProvince\", label: \"State / Province\", type: \"text\", placeholder: \"California\" },\n {\n id: \"occupation\",\n label: \"Occupation\",\n type: \"select\",\n half: true,\n options: [\n { id: \"eng\", label: \"Engineering\" },\n { id: \"design\", label: \"Design\" },\n { id: \"marketing\", label: \"Marketing\" },\n { id: \"sales\", label: \"Sales\" },\n ],\n },\n {\n id: \"status\",\n label: \"Status\",\n type: \"select\",\n half: true,\n options: [\n { id: \"active\", label: \"Active\" },\n { id: \"inactive\", label: \"Inactive\" },\n { id: \"pending\", label: \"Pending\" },\n ],\n },\n { id: \"phone\", label: \"Phone\", type: \"text\", placeholder: \"(555) 123-4567\", half: true },\n { id: \"fax\", label: \"Fax\", type: \"text\", placeholder: \"(555) 987-6543\", half: true },\n { id: \"step2StartDate\", label: \"Start date\", type: \"date\", half: true },\n { id: \"step2EndDate\", label: \"End date\", type: \"date\", half: true },\n { id: \"address2\", label: \"Address line 2\", type: \"text\", placeholder: \"Suite 200\" },\n {\n id: \"tags\",\n label: \"Tags\",\n type: \"multiselect-tags\",\n options: [\n { id: \"finance\", label: \"Finance\" },\n { id: \"technology\", label: \"Technology\" },\n { id: \"healthcare\", label: \"Healthcare\" },\n { id: \"education\", label: \"Education\" },\n { id: \"retail\", label: \"Retail\" },\n ],\n value: [],\n },\n ],\n },\n {\n title: \"Complete your account setup\",\n fields: [\n { id: \"step3Phone\", label: \"Phone\", type: \"text\", placeholder: \"(555) 123-4567\", half: true },\n { id: \"extension\", label: \"Extension\", type: \"text\", placeholder: \"1234\", half: true },\n { id: \"step3StartDate\", label: \"Start date\", type: \"date\", half: true },\n { id: \"step3EndDate\", label: \"End date\", type: \"date\", half: true },\n { id: \"copyrightYear\", label: \"Copyright year\", type: \"text\", placeholder: \"2026\", half: true },\n { id: \"licenseType\", label: \"License type\", type: \"text\", placeholder: \"Standard\", half: true },\n { id: \"notes\", label: \"Notes\", type: \"textarea\", placeholder: \"Add any additional notes here...\" },\n ],\n },\n];\n\nconst DEFAULT_BILLING_PLANS: BillingPlan[] = [\n {\n id: \"annual\",\n name: \"Annual billing\",\n price: \"$120/month\",\n description: \"Billed annually\",\n badge: \"Save 5%!\",\n },\n {\n id: \"monthly\",\n name: \"Monthly billing\",\n price: \"$150/month\",\n description: \"after your 30-day free trial\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// Inline Sub-Components\n// ---------------------------------------------------------------------------\n\nconst fontBase = \"var(--typo-global-font)\";\n\n/** Step indicator badge + title + description */\nfunction StepHeader({\n step,\n title,\n description,\n}: {\n step: number;\n title: string;\n description?: string;\n}) {\n return (\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-xl)\" }}>\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: 40,\n height: 40,\n borderRadius: \"var(--spacing-3xl)\",\n backgroundColor: \"var(--canvas-neutral-surface)\",\n }}\n >\n <span\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"24px\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {step}\n </span>\n </div>\n <h2\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"30px\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n {description && (\n <p\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"20px\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n paddingLeft: 56,\n }}\n >\n {description}\n </p>\n )}\n </div>\n );\n}\n\n/** Renders form fields with half-width pairing */\nfunction StepFormFields({\n fields,\n values,\n onChange,\n}: {\n fields: MultistepField[];\n values: Record<string, unknown>;\n onChange: (id: string, value: unknown) => void;\n}) {\n // Group fields into rows: half-width fields paired together\n const rows: MultistepField[][] = [];\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 <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-3xl)\" }}>\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 {renderField(field, values, onChange)}\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n}\n\nfunction renderField(\n field: MultistepField,\n values: Record<string, unknown>,\n onChange: (id: string, value: unknown) => void\n) {\n switch (field.type) {\n case \"textarea\":\n return (\n <Textarea\n inputSize=\"sm\"\n value={(values[field.id] as string) ?? \"\"}\n onChange={(e) => onChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n className=\"resize-none\"\n />\n );\n\n case \"select\":\n return (\n <Select\n value={(values[field.id] as string) ?? \"\"}\n onValueChange={(v) => onChange(field.id, v)}\n >\n <SelectTrigger style={{ height: \"var(--input-standard-height)\" }}>\n <SelectValue placeholder={field.placeholder ?? \"Select\"} />\n </SelectTrigger>\n <SelectContent>\n {field.options?.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n );\n\n case \"date\":\n return (\n <DateInput\n value={(values[field.id] as string) ?? \"\"}\n onChange={(v) => onChange(field.id, v)}\n />\n );\n\n case \"multiselect-tags\":\n return (\n <MultiselectTags\n tags={(values[field.id] as string[]) ?? (field.value as string[]) ?? []}\n placeholder={field.placeholder ?? \"Add...\"}\n onAdd={(tag: string) => {\n const current = (values[field.id] as string[]) ?? (field.value as string[]) ?? [];\n onChange(field.id, [...current, tag]);\n }}\n onRemove={(tag: string) => {\n const current = (values[field.id] as string[]) ?? (field.value as string[]) ?? [];\n onChange(field.id, current.filter((t: string) => t !== tag));\n }}\n />\n );\n\n default:\n return (\n <Input\n type=\"text\"\n value={(values[field.id] as string) ?? \"\"}\n onChange={(e) => onChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n />\n );\n }\n}\n\n/** Billing plan radio card */\nfunction BillingPlanCard({\n plan,\n selected,\n onClick,\n}: {\n plan: BillingPlan;\n selected: boolean;\n onClick: () => void;\n}) {\n return (\n <button\n className=\"flex items-start w-full text-left cursor-pointer\"\n onClick={onClick}\n style={{\n padding: \"var(--spacing-xl)\",\n borderRadius: \"var(--radius-md)\",\n border: selected\n ? \"2px solid var(--canvas-primary)\"\n : \"1px solid var(--canvas-border)\",\n backgroundColor: selected\n ? \"color-mix(in srgb, var(--canvas-primary) 4%, var(--canvas-background))\"\n : \"var(--canvas-background)\",\n gap: \"var(--spacing-lg)\",\n }}\n >\n {/* Radio dot */}\n <div\n className=\"shrink-0 flex items-center justify-center\"\n style={{\n width: 20,\n height: 20,\n borderRadius: \"50%\",\n marginTop: 2,\n border: selected\n ? \"2px solid var(--canvas-primary)\"\n : \"2px solid var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {selected && (\n <div\n style={{\n width: 10,\n height: 10,\n borderRadius: \"50%\",\n backgroundColor: \"var(--canvas-primary)\",\n }}\n />\n )}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <span\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.name}\n </span>\n {plan.badge && (\n <span\n className=\"shrink-0\"\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-xs-size)\",\n fontWeight: 600,\n lineHeight: \"16px\",\n padding: \"2px 8px\",\n borderRadius: \"var(--radius-sm)\",\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n {plan.badge}\n </span>\n )}\n </div>\n <p\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"20px\",\n color: \"var(--canvas-text-placeholder)\",\n margin: 0,\n }}\n >\n {plan.description}\n </p>\n </div>\n\n {/* Price */}\n <span\n className=\"shrink-0\"\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.price}\n </span>\n </button>\n );\n}\n\n/** Back / Continue navigation buttons */\nfunction StepNav({\n onBack,\n onNext,\n nextLabel = \"Continue\",\n showBack = true,\n}: {\n onBack?: () => void;\n onNext: () => void;\n nextLabel?: string;\n showBack?: boolean;\n}) {\n return (\n <div\n className=\"flex items-center justify-end\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {showBack && (\n <Button variant=\"neutral\" onClick={onBack}>\n Back\n </Button>\n )}\n <Button variant=\"primary\" onClick={onNext}>\n {nextLabel}\n </Button>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// MultistepFormPopup\n// ---------------------------------------------------------------------------\n\nexport function MultistepFormPopup({\n open,\n onOpenChange,\n steps = DEFAULT_STEPS,\n billingPlans = DEFAULT_BILLING_PLANS,\n loadingTitle = \"Please wait...\",\n loadingDescription = \"We are currently processing your submission\",\n successTitle = \"Success!\",\n successDescription = \"Your account has been set up successfully\",\n successButtonText = \"Go to portal\",\n onComplete,\n onSuccessButtonClick,\n className,\n}: MultistepFormPopupProps) {\n const [currentStep, setCurrentStep] = useState(0);\n const [phase, setPhase] = useState<\"form\" | \"loading\" | \"success\">(\"form\");\n const [values, setValues] = useState<Record<string, unknown>>({});\n const [selectedPlan, setSelectedPlan] = useState(\n billingPlans[0]?.id ?? \"\"\n );\n\n // Reset state when dialog closes\n useEffect(() => {\n if (!open) {\n setCurrentStep(0);\n setPhase(\"form\");\n setValues({});\n setSelectedPlan(billingPlans[0]?.id ?? \"\");\n }\n }, [open, billingPlans]);\n\n const totalFormSteps = steps.length;\n const totalStepsWithBilling = totalFormSteps + 1; // +1 for billing step\n\n const handleFieldChange = (id: string, value: unknown) => {\n setValues((prev) => ({ ...prev, [id]: value }));\n };\n\n const goBack = () => {\n if (currentStep > 0) {\n setCurrentStep((s) => s - 1);\n }\n };\n\n const goNext = () => {\n if (currentStep < totalStepsWithBilling - 1) {\n setCurrentStep((s) => s + 1);\n } else {\n // Final step — trigger loading → success\n setPhase(\"loading\");\n onComplete?.();\n setTimeout(() => {\n setPhase(\"success\");\n }, 2000);\n }\n };\n\n const isBillingStep = currentStep === totalFormSteps;\n const stepConfig = !isBillingStep ? steps[currentStep] : null;\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-[640px]\",\n className\n )}\n showCloseButton={phase === \"form\"}\n >\n {phase === \"form\" && (\n <div\n className=\"flex flex-col overflow-y-auto\"\n style={{\n padding: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n maxHeight: \"85vh\",\n }}\n >\n {/* Step Header */}\n <StepHeader\n step={currentStep + 1}\n title={\n isBillingStep\n ? \"Select a billing plan for your organization\"\n : stepConfig?.title ?? \"\"\n }\n description={\n isBillingStep ? undefined : stepConfig?.description\n }\n />\n\n {/* Step Content */}\n {isBillingStep ? (\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n {billingPlans.map((plan) => (\n <BillingPlanCard\n key={plan.id}\n plan={plan}\n selected={selectedPlan === plan.id}\n onClick={() => setSelectedPlan(plan.id)}\n />\n ))}\n </div>\n ) : (\n <StepFormFields\n fields={stepConfig?.fields ?? []}\n values={values}\n onChange={handleFieldChange}\n />\n )}\n\n {/* Navigation */}\n <StepNav\n onBack={goBack}\n onNext={goNext}\n showBack={currentStep > 0}\n nextLabel={\n isBillingStep ? \"Complete\" : \"Continue\"\n }\n />\n </div>\n )}\n\n {phase === \"loading\" && (\n <div\n className=\"flex items-center justify-center\"\n style={{\n padding: \"var(--spacing-4xl)\",\n minHeight: 320,\n }}\n >\n <Loader\n state=\"loading\"\n title={loadingTitle}\n description={loadingDescription}\n />\n </div>\n )}\n\n {phase === \"success\" && (\n <div\n className=\"flex items-center justify-center\"\n style={{\n padding: \"var(--spacing-4xl)\",\n minHeight: 320,\n }}\n >\n <Loader\n state=\"success\"\n title={successTitle}\n description={successDescription}\n buttonText={successButtonText}\n onButtonClick={() => {\n onSuccessButtonClick?.();\n onOpenChange?.(false);\n }}\n />\n </div>\n )}\n </DialogContent>\n </Dialog>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Dialog, DialogContent } from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { Label } from \"../ui/label\";\nimport { Input } from \"../ui/input\";\nimport { Textarea } from \"../ui/textarea\";\nimport { DateInput } from \"../ui/date-input\";\nimport {\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n} from \"../ui/select\";\nimport { MultiselectTags } from \"../ui/multiselect-tags\";\nimport { Loader } from \"./loader\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MultistepField {\n /** Unique field identifier */\n id: string;\n /** Field label */\n label: string;\n /** Field input type */\n type:\n | \"text\"\n | \"select\"\n | \"date\"\n | \"textarea\"\n | \"multiselect-tags\";\n /** Placeholder text */\n placeholder?: string;\n /** When true, the field takes 50% width and pairs with the next half field */\n half?: boolean;\n /** Options for select or multiselect-tags */\n options?: { id: string; label: string }[];\n /** Default value */\n value?: string | string[];\n}\n\nexport interface MultistepFormStep {\n /** Step title */\n title: string;\n /** Step description shown below the title */\n description?: string;\n /** Form fields for this step */\n fields: MultistepField[];\n}\n\nexport interface BillingPlan {\n /** Unique plan identifier */\n id: string;\n /** Plan name */\n name: string;\n /** Price display text */\n price: string;\n /** Description text */\n description: string;\n /** Optional badge text */\n badge?: string;\n}\n\nexport interface MultistepFormPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Step configurations — each has title, description, and fields */\n steps?: MultistepFormStep[];\n /** Billing plan options for the final step */\n billingPlans?: BillingPlan[];\n /** Loading state title */\n loadingTitle?: string;\n /** Loading state description */\n loadingDescription?: string;\n /** Success state title */\n successTitle?: string;\n /** Success state description */\n successDescription?: string;\n /** Success action button text */\n successButtonText?: string;\n /** Callback when the form is completed (after all steps) */\n onComplete?: () => void;\n /** Callback when the success action button is clicked */\n onSuccessButtonClick?: () => void;\n /** Additional class names for the dialog content */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_STEPS: MultistepFormStep[] = [\n {\n title: \"Enter your personal information to get started\",\n fields: [\n { id: \"email\", label: \"Email\", type: \"text\", placeholder: \"jane@example.com\" },\n { id: \"company\", label: \"Company name\", type: \"text\", placeholder: \"Acme Inc.\" },\n { id: \"firstName\", label: \"First name\", type: \"text\", placeholder: \"Jane\", half: true },\n { id: \"lastName\", label: \"Last name\", type: \"text\", placeholder: \"Doe\", half: true },\n { id: \"city\", label: \"City\", type: \"text\", placeholder: \"San Francisco\" },\n {\n id: \"country\",\n label: \"Country\",\n type: \"select\",\n half: true,\n options: [\n { id: \"us\", label: \"United States\" },\n { id: \"ca\", label: \"Canada\" },\n { id: \"uk\", label: \"United Kingdom\" },\n { id: \"au\", label: \"Australia\" },\n ],\n },\n {\n id: \"state\",\n label: \"State\",\n type: \"select\",\n half: true,\n options: [\n { id: \"ca\", label: \"California\" },\n { id: \"ny\", label: \"New York\" },\n { id: \"tx\", label: \"Texas\" },\n { id: \"wa\", label: \"Washington\" },\n ],\n },\n { id: \"zip\", label: \"Zip / Postal code\", type: \"text\", placeholder: \"94102\" },\n { id: \"profession\", label: \"Profession\", type: \"text\", placeholder: \"Software Engineer\", half: true },\n { id: \"startDate\", label: \"Start date\", type: \"date\", half: true },\n ],\n },\n {\n title: \"Enter additional information below\",\n fields: [\n { id: \"address\", label: \"Address\", type: \"text\", placeholder: \"123 Main St\" },\n { id: \"stateProvince\", label: \"State / Province\", type: \"text\", placeholder: \"California\" },\n {\n id: \"occupation\",\n label: \"Occupation\",\n type: \"select\",\n half: true,\n options: [\n { id: \"eng\", label: \"Engineering\" },\n { id: \"design\", label: \"Design\" },\n { id: \"marketing\", label: \"Marketing\" },\n { id: \"sales\", label: \"Sales\" },\n ],\n },\n {\n id: \"status\",\n label: \"Status\",\n type: \"select\",\n half: true,\n options: [\n { id: \"active\", label: \"Active\" },\n { id: \"inactive\", label: \"Inactive\" },\n { id: \"pending\", label: \"Pending\" },\n ],\n },\n { id: \"phone\", label: \"Phone\", type: \"text\", placeholder: \"(555) 123-4567\", half: true },\n { id: \"fax\", label: \"Fax\", type: \"text\", placeholder: \"(555) 987-6543\", half: true },\n { id: \"step2StartDate\", label: \"Start date\", type: \"date\", half: true },\n { id: \"step2EndDate\", label: \"End date\", type: \"date\", half: true },\n { id: \"address2\", label: \"Address line 2\", type: \"text\", placeholder: \"Suite 200\" },\n {\n id: \"tags\",\n label: \"Tags\",\n type: \"multiselect-tags\",\n options: [\n { id: \"finance\", label: \"Finance\" },\n { id: \"technology\", label: \"Technology\" },\n { id: \"healthcare\", label: \"Healthcare\" },\n { id: \"education\", label: \"Education\" },\n { id: \"retail\", label: \"Retail\" },\n ],\n value: [],\n },\n ],\n },\n {\n title: \"Complete your account setup\",\n fields: [\n { id: \"step3Phone\", label: \"Phone\", type: \"text\", placeholder: \"(555) 123-4567\", half: true },\n { id: \"extension\", label: \"Extension\", type: \"text\", placeholder: \"1234\", half: true },\n { id: \"step3StartDate\", label: \"Start date\", type: \"date\", half: true },\n { id: \"step3EndDate\", label: \"End date\", type: \"date\", half: true },\n { id: \"copyrightYear\", label: \"Copyright year\", type: \"text\", placeholder: \"2026\", half: true },\n { id: \"licenseType\", label: \"License type\", type: \"text\", placeholder: \"Standard\", half: true },\n { id: \"notes\", label: \"Notes\", type: \"textarea\", placeholder: \"Add any additional notes here...\" },\n ],\n },\n];\n\nconst DEFAULT_BILLING_PLANS: BillingPlan[] = [\n {\n id: \"annual\",\n name: \"Annual billing\",\n price: \"$120/month\",\n description: \"Billed annually\",\n badge: \"Save 5%!\",\n },\n {\n id: \"monthly\",\n name: \"Monthly billing\",\n price: \"$150/month\",\n description: \"after your 30-day free trial\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// Inline Sub-Components\n// ---------------------------------------------------------------------------\n\nconst fontBase = \"var(--typo-global-font)\";\n\n/** Step indicator badge + title + description */\nfunction StepHeader({\n step,\n title,\n description,\n}: {\n step: number;\n title: string;\n description?: string;\n}) {\n return (\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-xl)\" }}>\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: 40,\n height: 40,\n borderRadius: \"var(--spacing-3xl)\",\n backgroundColor: \"var(--canvas-neutral-surface)\",\n }}\n >\n <span\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"24px\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {step}\n </span>\n </div>\n <h2\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"30px\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n {description && (\n <p\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"20px\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n paddingLeft: 56,\n }}\n >\n {description}\n </p>\n )}\n </div>\n );\n}\n\n/** Renders form fields with half-width pairing */\nfunction StepFormFields({\n fields,\n values,\n onChange,\n}: {\n fields: MultistepField[];\n values: Record<string, unknown>;\n onChange: (id: string, value: unknown) => void;\n}) {\n // Group fields into rows: half-width fields paired together\n const rows: MultistepField[][] = [];\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 <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-3xl)\" }}>\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 {renderField(field, values, onChange)}\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n}\n\nfunction renderField(\n field: MultistepField,\n values: Record<string, unknown>,\n onChange: (id: string, value: unknown) => void\n) {\n switch (field.type) {\n case \"textarea\":\n return (\n <Textarea\n inputSize=\"sm\"\n value={(values[field.id] as string) ?? \"\"}\n onChange={(e) => onChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n className=\"resize-none\"\n />\n );\n\n case \"select\":\n return (\n <Select\n value={(values[field.id] as string) ?? \"\"}\n onValueChange={(v) => onChange(field.id, v)}\n >\n <SelectTrigger style={{ height: \"var(--input-standard-height)\" }}>\n <SelectValue placeholder={field.placeholder ?? \"Select\"} />\n </SelectTrigger>\n <SelectContent>\n {field.options?.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n );\n\n case \"date\":\n return (\n <DateInput\n value={(values[field.id] as string) ?? \"\"}\n onChange={(v) => onChange(field.id, v)}\n />\n );\n\n case \"multiselect-tags\":\n return (\n <MultiselectTags\n tags={(values[field.id] as string[]) ?? (field.value as string[]) ?? []}\n placeholder={field.placeholder ?? \"Add...\"}\n onAdd={(tag: string) => {\n const current = (values[field.id] as string[]) ?? (field.value as string[]) ?? [];\n onChange(field.id, [...current, tag]);\n }}\n onRemove={(tag: string) => {\n const current = (values[field.id] as string[]) ?? (field.value as string[]) ?? [];\n onChange(field.id, current.filter((t: string) => t !== tag));\n }}\n />\n );\n\n default:\n return (\n <Input\n type=\"text\"\n value={(values[field.id] as string) ?? \"\"}\n onChange={(e) => onChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n />\n );\n }\n}\n\n/** Billing plan radio card */\nfunction BillingPlanCard({\n plan,\n selected,\n onClick,\n}: {\n plan: BillingPlan;\n selected: boolean;\n onClick: () => void;\n}) {\n return (\n <button\n className=\"flex items-start w-full text-left cursor-pointer\"\n onClick={onClick}\n style={{\n padding: \"var(--spacing-xl)\",\n borderRadius: \"var(--radius-md)\",\n border: selected\n ? \"2px solid var(--canvas-primary)\"\n : \"1px solid var(--canvas-border)\",\n backgroundColor: selected\n ? \"color-mix(in srgb, var(--canvas-primary) 4%, var(--canvas-background))\"\n : \"var(--canvas-background)\",\n gap: \"var(--spacing-lg)\",\n }}\n >\n {/* Radio dot */}\n <div\n className=\"shrink-0 flex items-center justify-center\"\n style={{\n width: 20,\n height: 20,\n borderRadius: \"50%\",\n marginTop: 2,\n border: selected\n ? \"2px solid var(--canvas-primary)\"\n : \"2px solid var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {selected && (\n <div\n style={{\n width: 10,\n height: 10,\n borderRadius: \"50%\",\n backgroundColor: \"var(--canvas-primary)\",\n }}\n />\n )}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <span\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.name}\n </span>\n {plan.badge && (\n <span\n className=\"shrink-0\"\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-xs-size)\",\n fontWeight: 600,\n lineHeight: \"16px\",\n padding: \"2px 8px\",\n borderRadius: \"var(--radius-sm)\",\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n {plan.badge}\n </span>\n )}\n </div>\n <p\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"20px\",\n color: \"var(--canvas-text-placeholder)\",\n margin: 0,\n }}\n >\n {plan.description}\n </p>\n </div>\n\n {/* Price */}\n <span\n className=\"shrink-0\"\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.price}\n </span>\n </button>\n );\n}\n\n/** Back / Continue navigation buttons */\nfunction StepNav({\n onBack,\n onNext,\n nextLabel = \"Continue\",\n showBack = true,\n}: {\n onBack?: () => void;\n onNext: () => void;\n nextLabel?: string;\n showBack?: boolean;\n}) {\n return (\n <div\n className=\"flex items-center justify-end\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {showBack && (\n <Button variant=\"neutral\" onClick={onBack}>\n Back\n </Button>\n )}\n <Button variant=\"primary\" onClick={onNext}>\n {nextLabel}\n </Button>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// MultistepFormPopup\n// ---------------------------------------------------------------------------\n\nexport function MultistepFormPopup({\n open,\n onOpenChange,\n steps = DEFAULT_STEPS,\n billingPlans = DEFAULT_BILLING_PLANS,\n loadingTitle = \"Please wait...\",\n loadingDescription = \"We are currently processing your submission\",\n successTitle = \"Success!\",\n successDescription = \"Your account has been set up successfully\",\n successButtonText = \"Go to portal\",\n onComplete,\n onSuccessButtonClick,\n className,\n}: MultistepFormPopupProps) {\n const [currentStep, setCurrentStep] = useState(0);\n const [phase, setPhase] = useState<\"form\" | \"loading\" | \"success\">(\"form\");\n const [values, setValues] = useState<Record<string, unknown>>({});\n const [selectedPlan, setSelectedPlan] = useState(\n billingPlans[0]?.id ?? \"\"\n );\n\n // Reset state when dialog closes\n useEffect(() => {\n if (!open) {\n setCurrentStep(0);\n setPhase(\"form\");\n setValues({});\n setSelectedPlan(billingPlans[0]?.id ?? \"\");\n }\n }, [open, billingPlans]);\n\n const totalFormSteps = steps.length;\n const totalStepsWithBilling = totalFormSteps + 1; // +1 for billing step\n\n const handleFieldChange = (id: string, value: unknown) => {\n setValues((prev) => ({ ...prev, [id]: value }));\n };\n\n const goBack = () => {\n if (currentStep > 0) {\n setCurrentStep((s) => s - 1);\n }\n };\n\n const goNext = () => {\n if (currentStep < totalStepsWithBilling - 1) {\n setCurrentStep((s) => s + 1);\n } else {\n // Final step — trigger loading → success\n setPhase(\"loading\");\n onComplete?.();\n setTimeout(() => {\n setPhase(\"success\");\n }, 2000);\n }\n };\n\n const isBillingStep = currentStep === totalFormSteps;\n const stepConfig = !isBillingStep ? steps[currentStep] : null;\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-[640px]\",\n className\n )}\n showCloseButton={phase === \"form\"}\n >\n {phase === \"form\" && (\n <div\n className=\"flex flex-col overflow-y-auto\"\n style={{\n padding: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n maxHeight: \"85vh\",\n }}\n >\n {/* Step Header */}\n <StepHeader\n step={currentStep + 1}\n title={\n isBillingStep\n ? \"Select a billing plan for your organization\"\n : stepConfig?.title ?? \"\"\n }\n description={\n isBillingStep ? undefined : stepConfig?.description\n }\n />\n\n {/* Step Content */}\n {isBillingStep ? (\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n {billingPlans.map((plan) => (\n <BillingPlanCard\n key={plan.id}\n plan={plan}\n selected={selectedPlan === plan.id}\n onClick={() => setSelectedPlan(plan.id)}\n />\n ))}\n </div>\n ) : (\n <StepFormFields\n fields={stepConfig?.fields ?? []}\n values={values}\n onChange={handleFieldChange}\n />\n )}\n\n {/* Navigation */}\n <StepNav\n onBack={goBack}\n onNext={goNext}\n showBack={currentStep > 0}\n nextLabel={\n isBillingStep ? \"Complete\" : \"Continue\"\n }\n />\n </div>\n )}\n\n {phase === \"loading\" && (\n <div\n className=\"flex items-center justify-center\"\n style={{\n padding: \"var(--spacing-4xl)\",\n minHeight: 320,\n }}\n >\n <Loader\n state=\"loading\"\n title={loadingTitle}\n description={loadingDescription}\n />\n </div>\n )}\n\n {phase === \"success\" && (\n <div\n className=\"flex items-center justify-center\"\n style={{\n padding: \"var(--spacing-4xl)\",\n minHeight: 320,\n }}\n >\n <Loader\n state=\"success\"\n title={successTitle}\n description={successDescription}\n buttonText={successButtonText}\n onButtonClick={() => {\n onSuccessButtonClick?.();\n onOpenChange?.(false);\n }}\n />\n </div>\n )}\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/nps-survey-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 NpsSurveyPopupProps {\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 /** Currently selected score (controlled) */\n value?: number | null;\n /** Callback when submit is clicked — receives the selected score */\n onSubmit?: (score: number) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Label for the low end of the scale */\n minLabel?: string;\n /** Label for the high end of the scale */\n maxLabel?: string;\n /** Minimum score value */\n min?: number;\n /** Maximum score value */\n max?: number;\n /** Cancel button label */\n cancelLabel?: string;\n /** Submit button label */\n submitLabel?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Your input is valuable to us\";\nconst DEFAULT_DESCRIPTION =\n \"How likely are you to recommend Sample App to a friend or colleague?\";\n\n// ---------------------------------------------------------------------------\n// NpsSurveyPopup\n// ---------------------------------------------------------------------------\n\nexport function NpsSurveyPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n value: controlledValue,\n onSubmit,\n onCancel,\n minLabel = \"Not likely\",\n maxLabel = \"Very likely\",\n min = 1,\n max = 10,\n cancelLabel = \"Cancel\",\n submitLabel = \"Submit\",\n className,\n}: NpsSurveyPopupProps) {\n const [selected, setSelected] = useState<number | null>(controlledValue ?? null);\n\n // Reset when dialog opens\n useEffect(() => {\n if (open) {\n setSelected(controlledValue ?? null);\n }\n }, [open, controlledValue]);\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSubmit = () => {\n if (selected !== null) {\n onSubmit?.(selected);\n onOpenChange?.(false);\n }\n };\n\n const scores = Array.from({ length: max - min + 1 }, (_, i) => min + i);\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-[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 {/* 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 {/* Score buttons */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <div\n className=\"flex flex-wrap justify-center\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {scores.map((score) => {\n const isSelected = selected === score;\n return (\n <button\n key={score}\n type=\"button\"\n onClick={() => setSelected(score)}\n className={cn(\n \"flex items-center justify-center border rounded-[var(--radius-xs)] 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 width: \"var(--spacing-5xl)\",\n height: \"var(--spacing-5xl)\",\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n }}\n >\n {score}\n </button>\n );\n })}\n </div>\n\n {/* End labels */}\n <div\n className=\"flex justify-between w-full\"\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 <span>{minLabel}</span>\n <span>{maxLabel}</span>\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={handleSubmit}\n disabled={selected === null}\n >\n {submitLabel}\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 interface NpsSurveyPopupProps {\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 /** Currently selected score (controlled) */\n value?: number | null;\n /** Callback when submit is clicked — receives the selected score */\n onSubmit?: (score: number) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Label for the low end of the scale */\n minLabel?: string;\n /** Label for the high end of the scale */\n maxLabel?: string;\n /** Minimum score value */\n min?: number;\n /** Maximum score value */\n max?: number;\n /** Cancel button label */\n cancelLabel?: string;\n /** Submit button label */\n submitLabel?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Your input is valuable to us\";\nconst DEFAULT_DESCRIPTION =\n \"How likely are you to recommend Sample App to a friend or colleague?\";\n\n// ---------------------------------------------------------------------------\n// NpsSurveyPopup\n// ---------------------------------------------------------------------------\n\nexport function NpsSurveyPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n value: controlledValue,\n onSubmit,\n onCancel,\n minLabel = \"Not likely\",\n maxLabel = \"Very likely\",\n min = 1,\n max = 10,\n cancelLabel = \"Cancel\",\n submitLabel = \"Submit\",\n className,\n}: NpsSurveyPopupProps) {\n const [selected, setSelected] = useState<number | null>(controlledValue ?? null);\n\n // Reset when dialog opens\n useEffect(() => {\n if (open) {\n setSelected(controlledValue ?? null);\n }\n }, [open, controlledValue]);\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSubmit = () => {\n if (selected !== null) {\n onSubmit?.(selected);\n onOpenChange?.(false);\n }\n };\n\n const scores = Array.from({ length: max - min + 1 }, (_, i) => min + i);\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 {/* 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 {/* Score buttons */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <div\n className=\"flex flex-wrap justify-center\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {scores.map((score) => {\n const isSelected = selected === score;\n return (\n <button\n key={score}\n type=\"button\"\n onClick={() => setSelected(score)}\n className={cn(\n \"flex items-center justify-center border rounded-[var(--radius-xs)] 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 width: \"var(--spacing-5xl)\",\n height: \"var(--spacing-5xl)\",\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n }}\n >\n {score}\n </button>\n );\n })}\n </div>\n\n {/* End labels */}\n <div\n className=\"flex justify-between w-full\"\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 <span>{minLabel}</span>\n <span>{maxLabel}</span>\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={handleSubmit}\n disabled={selected === null}\n >\n {submitLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
19
19
  }
20
20
  ],
21
21
  "dependencies": [],