canvas-ui-sdk 4.0.1 → 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 (36) hide show
  1. package/README.md +41 -3
  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/mcp/dist/index.js +20 -13
  6. package/package.json +3 -2
  7. package/registry/blocks/category-grid.json +1 -1
  8. package/registry/blocks/confirmation-popup.json +1 -1
  9. package/registry/blocks/contact-form-popup.json +1 -1
  10. package/registry/blocks/details-popup.json +1 -1
  11. package/registry/blocks/feedback-popup.json +1 -1
  12. package/registry/blocks/form-popup.json +1 -1
  13. package/registry/blocks/image-popup.json +1 -1
  14. package/registry/blocks/invoice-popup.json +1 -1
  15. package/registry/blocks/list-popup.json +1 -1
  16. package/registry/blocks/multistep-form-popup.json +1 -1
  17. package/registry/blocks/nps-survey-popup.json +1 -1
  18. package/registry/blocks/page-previews.json +1 -1
  19. package/registry/blocks/persona-card.json +1 -1
  20. package/registry/blocks/personalize-feed-popup.json +1 -1
  21. package/registry/blocks/pricing-plans-popup.json +1 -1
  22. package/registry/blocks/purchase-confirmation-popup.json +1 -1
  23. package/registry/blocks/share-project-popup.json +1 -1
  24. package/registry/blocks/small-edit-popup.json +1 -1
  25. package/registry/blocks/terms-of-service-popup.json +1 -1
  26. package/registry/blocks/video-playlist.json +1 -1
  27. package/registry/blocks/video-popup.json +1 -1
  28. package/registry/blocks/view-profile-popup.json +1 -1
  29. package/registry/layout/dashboard-shell.json +1 -1
  30. package/registry/layout/double-sidebar-shell.json +1 -1
  31. package/registry/layout/double-sidebar.json +1 -1
  32. package/registry/layout/icon-sidebar-shell.json +1 -1
  33. package/registry/ui/dropdown-menu.json +1 -1
  34. package/registry/ui/popover.json +1 -1
  35. package/registry/ui/select.json +1 -1
  36. package/styles/tokens.reference.css +7 -0
package/mcp/dist/index.js CHANGED
@@ -18274,13 +18274,13 @@ var zodToJsonSchema = (schema, options) => {
18274
18274
  }, true) ?? parseAnyDef(refs)
18275
18275
  }), {}) : void 0;
18276
18276
  const name = typeof options === "string" ? options : options?.nameStrategy === "title" ? void 0 : options?.name;
18277
- const main = parseDef(schema._def, name === void 0 ? refs : {
18277
+ const main2 = parseDef(schema._def, name === void 0 ? refs : {
18278
18278
  ...refs,
18279
18279
  currentPath: [...refs.basePath, refs.definitionPath, name]
18280
18280
  }, false) ?? parseAnyDef(refs);
18281
18281
  const title = typeof options === "object" && options.name !== void 0 && options.nameStrategy === "title" ? options.name : void 0;
18282
18282
  if (title !== void 0) {
18283
- main.title = title;
18283
+ main2.title = title;
18284
18284
  }
18285
18285
  if (refs.flags.hasReferencedOpenAiAnyType) {
18286
18286
  if (!definitions) {
@@ -18301,9 +18301,9 @@ var zodToJsonSchema = (schema, options) => {
18301
18301
  }
18302
18302
  }
18303
18303
  const combined = name === void 0 ? definitions ? {
18304
- ...main,
18304
+ ...main2,
18305
18305
  [refs.definitionPath]: definitions
18306
- } : main : {
18306
+ } : main2 : {
18307
18307
  $ref: [
18308
18308
  ...refs.$refStrategy === "relative" ? [] : refs.basePath,
18309
18309
  refs.definitionPath,
@@ -18311,7 +18311,7 @@ var zodToJsonSchema = (schema, options) => {
18311
18311
  ].join("/"),
18312
18312
  [refs.definitionPath]: {
18313
18313
  ...definitions,
18314
- [name]: main
18314
+ [name]: main2
18315
18315
  }
18316
18316
  };
18317
18317
  if (refs.target === "jsonSchema7") {
@@ -18543,11 +18543,11 @@ var Protocol = class {
18543
18543
  *
18544
18544
  * The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
18545
18545
  */
18546
- async connect(transport2) {
18546
+ async connect(transport) {
18547
18547
  if (this._transport) {
18548
18548
  throw new Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.");
18549
18549
  }
18550
- this._transport = transport2;
18550
+ this._transport = transport;
18551
18551
  const _onclose = this.transport?.onclose;
18552
18552
  this._transport.onclose = () => {
18553
18553
  _onclose?.();
@@ -19990,8 +19990,8 @@ var McpServer = class {
19990
19990
  *
19991
19991
  * The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
19992
19992
  */
19993
- async connect(transport2) {
19994
- return await this.server.connect(transport2);
19993
+ async connect(transport) {
19994
+ return await this.server.connect(transport);
19995
19995
  }
19996
19996
  /**
19997
19997
  * Closes the connection.
@@ -20744,7 +20744,7 @@ var EMPTY_COMPLETION_RESULT = {
20744
20744
  };
20745
20745
 
20746
20746
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
20747
- import process from "process";
20747
+ import process2 from "process";
20748
20748
 
20749
20749
  // node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
20750
20750
  var ReadBuffer = class {
@@ -20776,7 +20776,7 @@ function serializeMessage(message) {
20776
20776
 
20777
20777
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
20778
20778
  var StdioServerTransport = class {
20779
- constructor(_stdin = process.stdin, _stdout = process.stdout) {
20779
+ constructor(_stdin = process2.stdin, _stdout = process2.stdout) {
20780
20780
  this._stdin = _stdin;
20781
20781
  this._stdout = _stdout;
20782
20782
  this._readBuffer = new ReadBuffer();
@@ -23309,5 +23309,12 @@ ${resultSections.join("\n---\n\n")}`
23309
23309
  };
23310
23310
  }
23311
23311
  );
23312
- var transport = new StdioServerTransport();
23313
- await server.connect(transport);
23312
+ async function main() {
23313
+ const transport = new StdioServerTransport();
23314
+ await server.connect(transport);
23315
+ }
23316
+ main().catch((error2) => {
23317
+ process.stderr.write(`canvas-ui-sdk MCP server failed to start: ${error2}
23318
+ `);
23319
+ process.exit(1);
23320
+ });
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "canvas-ui-sdk",
3
- "version": "4.0.1",
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": {
7
- "canvas-ui": "./dist/cli/index.js"
7
+ "canvas-ui": "./dist/cli/index.js",
8
+ "canvas-ui-mcp": "./mcp/dist/index.js"
8
9
  },
9
10
  "main": "./dist/index.js",
10
11
  "module": "./dist/index.js",
@@ -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": [],