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.
- package/README.md +41 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +38 -30
- package/dist/index.js.map +1 -1
- package/mcp/dist/index.js +20 -13
- package/package.json +3 -2
- package/registry/blocks/category-grid.json +1 -1
- package/registry/blocks/confirmation-popup.json +1 -1
- package/registry/blocks/contact-form-popup.json +1 -1
- package/registry/blocks/details-popup.json +1 -1
- package/registry/blocks/feedback-popup.json +1 -1
- package/registry/blocks/form-popup.json +1 -1
- package/registry/blocks/image-popup.json +1 -1
- package/registry/blocks/invoice-popup.json +1 -1
- package/registry/blocks/list-popup.json +1 -1
- package/registry/blocks/multistep-form-popup.json +1 -1
- package/registry/blocks/nps-survey-popup.json +1 -1
- package/registry/blocks/page-previews.json +1 -1
- package/registry/blocks/persona-card.json +1 -1
- package/registry/blocks/personalize-feed-popup.json +1 -1
- package/registry/blocks/pricing-plans-popup.json +1 -1
- package/registry/blocks/purchase-confirmation-popup.json +1 -1
- package/registry/blocks/share-project-popup.json +1 -1
- package/registry/blocks/small-edit-popup.json +1 -1
- package/registry/blocks/terms-of-service-popup.json +1 -1
- package/registry/blocks/video-playlist.json +1 -1
- package/registry/blocks/video-popup.json +1 -1
- package/registry/blocks/view-profile-popup.json +1 -1
- package/registry/layout/dashboard-shell.json +1 -1
- package/registry/layout/double-sidebar-shell.json +1 -1
- package/registry/layout/double-sidebar.json +1 -1
- package/registry/layout/icon-sidebar-shell.json +1 -1
- package/registry/ui/dropdown-menu.json +1 -1
- package/registry/ui/popover.json +1 -1
- package/registry/ui/select.json +1 -1
- package/styles/tokens.reference.css +7 -0
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
|
|
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
|
-
|
|
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
|
-
...
|
|
18304
|
+
...main2,
|
|
18305
18305
|
[refs.definitionPath]: definitions
|
|
18306
|
-
} :
|
|
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]:
|
|
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(
|
|
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 =
|
|
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(
|
|
19994
|
-
return await this.server.connect(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
23313
|
-
|
|
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.
|
|
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-
|
|
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-[
|
|
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-[
|
|
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-[
|
|
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-[
|
|
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-[
|
|
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-[
|
|
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-[
|
|
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": [],
|