canvas-ui-sdk 0.3.21 → 0.3.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +680 -15
- package/dist/index.js +6523 -1771
- package/dist/index.js.map +1 -1
- package/mcp/dist/index.js +14 -4
- package/package.json +1 -1
- package/registry/blocks/confirmation-popup.json +18 -0
- package/registry/blocks/contact-form-popup.json +24 -0
- package/registry/blocks/detail-drawer.json +21 -0
- package/registry/blocks/details-popup.json +17 -0
- package/registry/blocks/feedback-popup.json +19 -0
- package/registry/blocks/form-group.json +2 -2
- package/registry/blocks/hero-fullwidth-image.json +1 -1
- package/registry/blocks/hero-section.json +1 -1
- package/registry/blocks/image-popup.json +17 -0
- package/registry/blocks/invoice-popup.json +20 -0
- package/registry/blocks/monthly-calendar-widget.json +1 -1
- package/registry/blocks/page-previews.json +1 -1
- package/registry/blocks/place-detail-panel.json +22 -0
- package/registry/blocks/pricing-cta.json +1 -1
- package/registry/blocks/pricing-plans-popup.json +18 -0
- package/registry/blocks/profile-image-uploader.json +1 -1
- package/registry/blocks/purchase-confirmation-popup.json +18 -0
- package/registry/blocks/sidebar-profile-card.json +1 -1
- package/registry/blocks/slideshow-popup.json +22 -0
- package/registry/blocks/store-location-map.json +18 -0
- package/registry/blocks/terms-of-service-popup.json +18 -0
- package/registry/blocks/video-popup.json +18 -0
- package/registry/blocks/view-profile-popup.json +23 -0
- package/registry/index.json +76 -1
- package/registry/layout/dashboard-shell.json +1 -1
- package/registry/layout/double-sidebar-shell.json +1 -1
- package/registry/layout/header.json +1 -1
- package/registry/layout/icon-sidebar-shell.json +1 -1
- package/registry/layout/mobile-menu-shell.json +1 -1
- package/registry/layout/sidebar.json +1 -1
- package/registry/ui/checkbox.json +1 -1
- package/registry/ui/date-input.json +1 -1
- package/registry/ui/dialog.json +1 -1
- package/registry/ui/dropdown-menu.json +1 -1
- package/registry/ui/input.json +1 -1
- package/registry/ui/label.json +1 -1
- package/registry/ui/line-tabs.json +1 -1
- package/registry/ui/multiselect-checkbox-field.json +1 -1
- package/registry/ui/multiselect-tags.json +1 -1
- package/registry/ui/popover.json +1 -1
- package/registry/ui/searchbox.json +1 -1
- package/registry/ui/select.json +1 -1
- package/registry/ui/selectable-pills.json +1 -1
- package/registry/ui/sheet.json +1 -1
- package/registry/ui/tabs.json +1 -1
- package/registry/ui/text-input.json +1 -1
- package/registry/ui/textarea.json +1 -1
- package/registry/ui/tooltip.json +1 -1
- package/styles/tokens.reference.css +5 -2
package/mcp/dist/index.js
CHANGED
|
@@ -21066,6 +21066,11 @@ var blocks = {
|
|
|
21066
21066
|
description: "Avatar upload component with preview and edit button.",
|
|
21067
21067
|
props: ["currentImage?", "onUpload?"]
|
|
21068
21068
|
},
|
|
21069
|
+
StoreLocationMap: {
|
|
21070
|
+
path: "@/components/blocks",
|
|
21071
|
+
description: "Single store location card with address info, directions button, and embedded Google Maps iframe. Uses TitleGroup for header.",
|
|
21072
|
+
props: ["title?", "subtitle?", "storeLabel?", "storeName?", "addressLines?", "buttonText?", "onDirectionsClick?", "mapEmbedUrl?", "className?"]
|
|
21073
|
+
},
|
|
21069
21074
|
SettingsListRow: {
|
|
21070
21075
|
path: "@/components/blocks",
|
|
21071
21076
|
description: "Row item for settings lists with label, value, and edit action.",
|
|
@@ -21206,8 +21211,13 @@ var blocks = {
|
|
|
21206
21211
|
var groupModalDrawerBlocks = {
|
|
21207
21212
|
FormGroup: {
|
|
21208
21213
|
path: "@/components/blocks",
|
|
21209
|
-
description: "
|
|
21210
|
-
props: ["title?", "description?", "
|
|
21214
|
+
description: "Single-column form layout with header (title, sort, filter, action button), configurable fields, and footer (cancel/save). Supports text inputs, textareas, selects, date pickers, multiselect checkboxes, checkbox groups, radio groups, multiselect tags, image/file uploaders, and sliders.",
|
|
21215
|
+
props: ["title?", "description?", "fields?: FormFieldConfig[]", "sortOptions?", "filterOptions?", "inputSize?: 'sm' | 'default' | 'lg'", "onAddNew?", "onCancel?", "onSave?", "onFieldChange?", "showHeader?", "showFooter?"]
|
|
21216
|
+
},
|
|
21217
|
+
DetailDrawer: {
|
|
21218
|
+
path: "@/components/blocks",
|
|
21219
|
+
description: "Right-side detail drawer with tabbed content. Info tab shows metadata fields (with avatars, badges, links), rich content sections, and file attachments. Comments tab shows a chat-style thread with sender names, timestamps, and a comment input.",
|
|
21220
|
+
props: ["open?", "onOpenChange?", "title?", "subtitle?", "tabs?: DrawerTab[]", "activeTab?", "onTabChange?", "detailFields?: DetailField[][]", "richContent?", "attachments?: DrawerAttachment[]", "comments?: DrawerComment[]", "commentPlaceholder?", "onComment?", "onAttachmentDownload?"]
|
|
21211
21221
|
}
|
|
21212
21222
|
};
|
|
21213
21223
|
var videoBlocks = {
|
|
@@ -21663,8 +21673,8 @@ var defaultTypography = {
|
|
|
21663
21673
|
"--typo-body-l-color": "var(--canvas-text)",
|
|
21664
21674
|
"--typo-body-l-color-muted": "var(--canvas-text-muted)",
|
|
21665
21675
|
// Body M - Standard body text
|
|
21666
|
-
"--typo-body-m-size": "
|
|
21667
|
-
"--typo-body-m-size-mobile": "
|
|
21676
|
+
"--typo-body-m-size": "14px",
|
|
21677
|
+
"--typo-body-m-size-mobile": "13px",
|
|
21668
21678
|
"--typo-body-m-weight": "400",
|
|
21669
21679
|
"--typo-body-m-spacing": "0em",
|
|
21670
21680
|
"--typo-body-m-line-height": "1.5",
|
package/package.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "confirmation-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/confirmation-popup.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"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"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": [
|
|
14
|
+
"lib/utils",
|
|
15
|
+
"ui/dialog",
|
|
16
|
+
"ui/button"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "contact-form-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/contact-form-popup.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"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"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": [
|
|
14
|
+
"lib/utils",
|
|
15
|
+
"ui/dialog",
|
|
16
|
+
"ui/button",
|
|
17
|
+
"ui/input",
|
|
18
|
+
"ui/textarea",
|
|
19
|
+
"ui/label",
|
|
20
|
+
"ui/avatar",
|
|
21
|
+
"blocks/gradient-banner",
|
|
22
|
+
"blocks/demo-avatars"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "detail-drawer",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Right-side detail drawer with tabbed content. Info tab shows metadata fields (with avatars, badges, links), rich content sections, and file attachments. Comments tab shows a chat-style thread with sender names, timestamps, and a comment input.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/detail-drawer.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport React, { useState, useRef } from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { Download, Paperclip } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Sheet, SheetContent, SheetTitle } from \"../ui/sheet\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DetailField {\n /** Field label */\n label: string;\n /** Plain text value */\n value?: string;\n /** Avatar to show next to the value */\n avatar?: { src: string; fallback: string };\n /** Status badge */\n badge?: {\n text: string;\n variant: \"default\" | \"success\" | \"warning\" | \"danger\";\n };\n /** Render the value as a link */\n isLink?: boolean;\n href?: string;\n}\n\nexport interface DrawerAttachment {\n id: string;\n /** File name */\n name: string;\n /** File type label (e.g. \"DOC\", \"PDF\", \"IMG\") */\n type: string;\n /** Who uploaded the file */\n uploadedBy: string;\n url?: string;\n}\n\nexport interface DrawerComment {\n id: string;\n senderName: string;\n senderAvatar?: string;\n content: string;\n timestamp: string;\n /** true = current user's message (right-aligned, primary bg) */\n isSent: boolean;\n}\n\nexport interface DrawerTab {\n id: string;\n label: string;\n}\n\nexport interface DetailDrawerProps {\n /** Controls drawer visibility */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Drawer title */\n title?: string;\n /** Subtitle shown below the title */\n subtitle?: string;\n /** Tab definitions – defaults to Info / Comments */\n tabs?: DrawerTab[];\n /** Controlled active tab */\n activeTab?: string;\n onTabChange?: (tabId: string) => void;\n /** Detail fields displayed in the Info tab – each inner array is a row of 1-2 fields */\n detailFields?: DetailField[][];\n /** Rich content sections below the fields (e.g., \"Scope of work\") */\n richContent?: { title: string; content: React.ReactNode }[];\n /** Attachments shown at the bottom of the Info tab */\n attachments?: DrawerAttachment[];\n /** Comments shown in the Comments tab */\n comments?: DrawerComment[];\n /** Placeholder text for the comment input */\n commentPlaceholder?: string;\n /** Callback when a comment is submitted */\n onComment?: (text: string) => void;\n /** Callback when an attachment download is clicked */\n onAttachmentDownload?: (attachment: DrawerAttachment) => void;\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default demo data\n// ---------------------------------------------------------------------------\n\nconst defaultTabs: DrawerTab[] = [\n { id: \"info\", label: \"Info\" },\n { id: \"comments\", label: \"Comments\" },\n];\n\nconst defaultDetailFields: DetailField[][] = [\n [\n {\n label: \"Description\",\n value:\n \"Redesign and update the Acme Corporation\\u2019s website to improve user experience\",\n },\n ],\n [\n {\n label: \"Client\",\n value: \"Raj Mishra\",\n avatar: { src: \"\", fallback: \"RM\" },\n },\n {\n label: \"Assigned\",\n value: \"Mary Trott\",\n avatar: { src: \"\", fallback: \"MT\" },\n },\n ],\n [\n { label: \"Timeline\", value: \"3/12/24 - 5/21/24\" },\n {\n label: \"Status\",\n badge: { text: \"Need review\", variant: \"danger\" },\n },\n ],\n [\n { label: \"Budget\", value: \"$50,000\" },\n {\n label: \"Invoice\",\n value: \"AG3329351Z3\",\n isLink: true,\n href: \"#\",\n },\n ],\n];\n\nconst defaultRichContent: { title: string; content: React.ReactNode }[] = [\n {\n title: \"Scope of work\",\n content: (\n <ol\n className=\"list-decimal ml-6\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <li>\n Scope of Work:\n <ul className=\"list-disc ml-6\">\n <li>Redesign website layout</li>\n <li>Update website content</li>\n <li>Implement responsive design</li>\n <li>Improve site performance</li>\n </ul>\n </li>\n <li>\n Team Members:\n <ul className=\"list-disc ml-6\">\n <li>Mary Trott (Project Manager)</li>\n <li>Tracy Tannar (Designer)</li>\n <li>Jeffrey Connor (Developer)</li>\n <li>David Clark (QA Tester)</li>\n </ul>\n </li>\n <li>\n Project Timeline:\n <ul className=\"list-disc ml-6\">\n <li>Phase 1: Planning and Research</li>\n <li>Phase 2: Design and Prototyping</li>\n <li>Phase 3: Development and Content Update</li>\n <li>Phase 4: Testing and Quality Assurance</li>\n <li>Phase 5: Launch and Post-Launch Support</li>\n </ul>\n </li>\n </ol>\n ),\n },\n];\n\nconst defaultAttachments: DrawerAttachment[] = [\n {\n id: \"1\",\n name: \"Photography_Masterclass\",\n type: \"DOC\",\n uploadedBy: \"Mary Trott\",\n },\n {\n id: \"2\",\n name: \"Figma_mockups_drafts\",\n type: \"PDF\",\n uploadedBy: \"Tracy Tanner\",\n },\n];\n\nconst defaultComments: DrawerComment[] = [\n {\n id: \"1\",\n senderName: \"Mary Trott\",\n senderAvatar: \"\",\n content:\n \"Hi Jeff, Thank you for sending the materials over. I\\u2019ll review and get back to you in a bit!\",\n timestamp: \"Oct 15, 3:05pm\",\n isSent: false,\n },\n {\n id: \"2\",\n senderName: \"Jeffrey Connor\",\n senderAvatar: \"\",\n content: \"Of course, will update that now!\",\n timestamp: \"2 hours ago\",\n isSent: true,\n },\n];\n\n// ---------------------------------------------------------------------------\n// Sub-components\n// ---------------------------------------------------------------------------\n\nconst badgeVariants: Record<\n string,\n { bg: string; text: string }\n> = {\n default: {\n bg: \"var(--canvas-surface)\",\n text: \"var(--canvas-text-muted)\",\n },\n success: {\n bg: \"var(--canvas-success-surface)\",\n text: \"var(--canvas-success)\",\n },\n warning: {\n bg: \"var(--canvas-warning-surface)\",\n text: \"var(--canvas-warning)\",\n },\n danger: {\n bg: \"var(--canvas-destructive-surface)\",\n text: \"var(--canvas-destructive)\",\n },\n};\n\nfunction StatusBadge({\n text,\n variant = \"default\",\n}: {\n text: string;\n variant?: string;\n}) {\n const colors = badgeVariants[variant] ?? badgeVariants.default;\n return (\n <span\n className=\"inline-flex items-center w-fit h-8 px-[var(--spacing-xl)] rounded-full\"\n style={{\n backgroundColor: colors.bg,\n color: colors.text,\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--btn-standard-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {text}\n </span>\n );\n}\n\nfunction DetailFieldCell({ field }: { field: DetailField }) {\n return (\n <div className=\"flex flex-1 flex-col gap-[var(--spacing-xxs)]\">\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: \"var(--typo-h6-weight)\" as React.CSSProperties[\"fontWeight\"],\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {field.label}\n </span>\n\n {field.badge ? (\n <StatusBadge text={field.badge.text} variant={field.badge.variant} />\n ) : field.avatar ? (\n <div className=\"flex items-center gap-[var(--spacing-md)]\">\n <Avatar className=\"size-8 shrink-0 border border-[var(--canvas-border)]\">\n <AvatarImage src={field.avatar.src} alt={field.value} />\n <AvatarFallback\n style={{\n fontSize: \"10px\",\n backgroundColor: \"var(--canvas-surface)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {field.avatar.fallback}\n </AvatarFallback>\n </Avatar>\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {field.value}\n </span>\n </div>\n ) : field.isLink ? (\n <a\n href={field.href ?? \"#\"}\n className=\"hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n {field.value}\n </a>\n ) : (\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {field.value}\n </span>\n )}\n </div>\n );\n}\n\nfunction AttachmentCard({\n attachment,\n onDownload,\n}: {\n attachment: DrawerAttachment;\n onDownload?: (a: DrawerAttachment) => void;\n}) {\n return (\n <div\n className=\"flex items-center gap-[var(--spacing-3xl)] p-[var(--spacing-4xl)] rounded-[var(--radius-md)] border\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n {/* File type icon */}\n <div\n className=\"flex items-center justify-center shrink-0 size-12 rounded-[var(--radius-md)] border\"\n style={{\n backgroundColor: \"var(--canvas-surface-brand)\",\n borderColor: \"var(--canvas-border)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-h6-weight)\" as React.CSSProperties[\"fontWeight\"],\n color: \"var(--canvas-primary)\",\n }}\n >\n {attachment.type}\n </span>\n </div>\n\n {/* Name + uploader */}\n <div className=\"flex flex-1 flex-col gap-[var(--spacing-xs)]\">\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: \"var(--btn-standard-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {attachment.name}\n </span>\n <span\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-placeholder)\",\n }}\n >\n Uploaded by {attachment.uploadedBy}\n </span>\n </div>\n\n {/* Download button */}\n <button\n className=\"shrink-0 p-[var(--spacing-md)] rounded-full hover:opacity-70 transition-opacity\"\n onClick={() => onDownload?.(attachment)}\n aria-label={`Download ${attachment.name}`}\n >\n <Download\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n </div>\n );\n}\n\nfunction CommentBubble({ comment }: { comment: DrawerComment }) {\n if (comment.isSent) {\n return (\n <div className=\"flex items-end justify-end gap-[var(--spacing-xs)] pl-[var(--spacing-5xl)]\">\n <div\n className=\"flex flex-1 flex-col gap-[var(--spacing-md)] p-[var(--spacing-xl)] rounded-tl-[var(--radius-xl)] rounded-tr-[var(--radius-xl)] rounded-bl-[var(--radius-xl)] overflow-hidden\"\n style={{\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n <div className=\"flex items-center justify-between\">\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: \"var(--typo-h6-weight)\" as React.CSSProperties[\"fontWeight\"],\n lineHeight: \"var(--typo-body-m-line-height)\",\n }}\n >\n {comment.senderName}\n </span>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n }}\n >\n {comment.timestamp}\n </span>\n </div>\n <p\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 }}\n >\n {comment.content}\n </p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex items-end gap-[var(--spacing-md)] pr-[var(--spacing-5xl)]\">\n <Avatar className=\"size-8 shrink-0\">\n <AvatarImage src={comment.senderAvatar} alt={comment.senderName} />\n <AvatarFallback\n style={{\n fontSize: \"10px\",\n backgroundColor: \"var(--canvas-surface)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {comment.senderName\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n <div\n className=\"flex flex-1 flex-col gap-[var(--spacing-md)] p-[var(--spacing-xl)] rounded-tl-[var(--radius-xl)] rounded-tr-[var(--radius-xl)] rounded-br-[var(--radius-xl)] overflow-hidden\"\n style={{\n backgroundColor: \"var(--canvas-border)\",\n }}\n >\n <div className=\"flex items-center justify-between\">\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: \"var(--typo-h6-weight)\" as React.CSSProperties[\"fontWeight\"],\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {comment.senderName}\n </span>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {comment.timestamp}\n </span>\n </div>\n <p\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)\",\n }}\n >\n {comment.content}\n </p>\n </div>\n </div>\n );\n}\n\nfunction CommentInput({\n placeholder = \"Write a comment\",\n onSubmit,\n}: {\n placeholder?: string;\n onSubmit?: (text: string) => void;\n}) {\n const [text, setText] = useState(\"\");\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const handleSubmit = () => {\n if (!text.trim()) return;\n onSubmit?.(text.trim());\n setText(\"\");\n };\n\n return (\n <div\n className=\"border rounded-[var(--radius-lg)] overflow-hidden\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n >\n <textarea\n value={text}\n onChange={(e) => setText(e.target.value)}\n placeholder={placeholder}\n rows={2}\n className=\"w-full resize-none p-[var(--spacing-xl)] focus:outline-none\"\n style={{\n backgroundColor: \"transparent\",\n color: \"var(--canvas-text)\",\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n }}\n />\n <div\n className=\"flex items-center justify-between px-[var(--spacing-xl)] py-[var(--spacing-xl)] border-t\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n <div className=\"flex items-center gap-[var(--spacing-lg)]\">\n <input\n ref={fileInputRef}\n type=\"file\"\n className=\"hidden\"\n aria-hidden=\"true\"\n />\n <button\n onClick={() => fileInputRef.current?.click()}\n className=\"hover:opacity-70 transition-opacity\"\n aria-label=\"Add attachment\"\n >\n <Paperclip\n className=\"size-5\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n </div>\n <button\n onClick={handleSubmit}\n className=\"h-[var(--btn-standard-height)] px-[var(--spacing-lg)] rounded-[var(--radius-xs)] transition-colors hover:opacity-90\"\n style={{\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--btn-standard-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n }}\n >\n Comment\n </button>\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Main component\n// ---------------------------------------------------------------------------\n\n/**\n * A right-side detail drawer with tabbed content — an Info tab for metadata,\n * rich content, and attachments, and a Comments tab with a chat-style thread.\n *\n * @example\n * ```tsx\n * <DetailDrawer\n * open={open}\n * onOpenChange={setOpen}\n * title=\"Acme Website Redesign\"\n * subtitle=\"Project ID: PRJ2024-1121\"\n * />\n * ```\n */\nexport function DetailDrawer({\n open,\n onOpenChange,\n title = \"Acme Website Redesign\",\n subtitle = \"Project ID: PRJ2024-1121\",\n tabs = defaultTabs,\n activeTab,\n onTabChange,\n detailFields = defaultDetailFields,\n richContent = defaultRichContent,\n attachments = defaultAttachments,\n comments = defaultComments,\n commentPlaceholder = \"Write a comment\",\n onComment,\n onAttachmentDownload,\n className,\n}: DetailDrawerProps) {\n const defaultTab = tabs[0]?.id ?? \"info\";\n\n return (\n <Sheet open={open} onOpenChange={onOpenChange}>\n <SheetContent\n side=\"right\"\n className={cn(\n \"w-[480px] sm:max-w-[480px] p-0 gap-0 overflow-hidden\",\n className\n )}\n >\n <TabsPrimitive.Root\n defaultValue={activeTab ?? defaultTab}\n value={activeTab}\n onValueChange={onTabChange}\n className=\"flex flex-col h-full\"\n >\n {/* ---- Header ---- */}\n <div\n className=\"flex flex-col gap-[var(--spacing-xxs)] px-[var(--spacing-4xl)] pt-[var(--spacing-3xl)]\"\n >\n <SheetTitle\n className=\"pr-8\"\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\" as React.CSSProperties[\"fontWeight\"],\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </SheetTitle>\n {subtitle && (\n <p\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 {subtitle}\n </p>\n )}\n </div>\n\n {/* ---- Line Tabs ---- */}\n <TabsPrimitive.List\n className=\"flex items-end gap-[var(--spacing-3xl)] px-[var(--spacing-4xl)] border-b-2 shrink-0 mt-[var(--spacing-3xl)]\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n {tabs.map((tab) => (\n <TabsPrimitive.Trigger\n key={tab.id}\n value={tab.id}\n className={cn(\n \"relative h-12 flex items-center justify-center cursor-pointer transition-colors\",\n \"border-b-2 -mb-[2px]\",\n \"data-[state=active]:border-[var(--canvas-primary)] data-[state=active]:text-[var(--canvas-primary)]\",\n \"data-[state=inactive]:border-transparent data-[state=inactive]:text-[var(--canvas-text-placeholder)]\",\n \"hover:text-[var(--canvas-text)]\"\n )}\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: \"var(--btn-standard-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n lineHeight: \"var(--typo-body-m-line-height)\",\n }}\n >\n {tab.label}\n </TabsPrimitive.Trigger>\n ))}\n </TabsPrimitive.List>\n\n {/* ---- Info Tab ---- */}\n <TabsPrimitive.Content\n value={tabs[0]?.id ?? \"info\"}\n className=\"flex-1 overflow-y-auto outline-none\"\n >\n <div className=\"flex flex-col gap-[var(--spacing-3xl)] py-[var(--spacing-3xl)]\">\n {/* Detail fields */}\n {detailFields.length > 0 && (\n <div className=\"flex flex-col gap-[var(--spacing-xl)] px-[var(--spacing-4xl)]\">\n {detailFields.map((row, rowIdx) => (\n <div\n key={rowIdx}\n className=\"flex gap-[var(--spacing-2xl)]\"\n >\n {row.map((field, fieldIdx) => (\n <DetailFieldCell\n key={fieldIdx}\n field={field}\n />\n ))}\n </div>\n ))}\n </div>\n )}\n\n {/* Divider */}\n {(richContent.length > 0 || attachments.length > 0) && (\n <div\n className=\"h-px w-full\"\n style={{ backgroundColor: \"var(--canvas-border)\" }}\n />\n )}\n\n {/* Rich content sections + attachments */}\n {(richContent.length > 0 || attachments.length > 0) && (\n <div className=\"flex flex-col gap-[var(--spacing-3xl)] px-[var(--spacing-4xl)]\">\n {richContent.map((section, idx) => (\n <div\n key={idx}\n className=\"flex flex-col\"\n >\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: \"var(--typo-h6-weight)\" as React.CSSProperties[\"fontWeight\"],\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {section.title}\n </span>\n <div>{section.content}</div>\n </div>\n ))}\n\n {/* Attachments */}\n {attachments.length > 0 && (\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: \"var(--typo-h6-weight)\" as React.CSSProperties[\"fontWeight\"],\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Attachments\n </span>\n {attachments.map((attachment) => (\n <AttachmentCard\n key={attachment.id}\n attachment={attachment}\n onDownload={onAttachmentDownload}\n />\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n </TabsPrimitive.Content>\n\n {/* ---- Comments Tab ---- */}\n <TabsPrimitive.Content\n value={tabs[1]?.id ?? \"comments\"}\n className=\"flex-1 flex flex-col overflow-hidden outline-none\"\n >\n <div className=\"flex flex-1 flex-col px-[var(--spacing-4xl)] py-[var(--spacing-3xl)] overflow-hidden\">\n {/* Comments heading + messages */}\n <div className=\"flex flex-1 flex-col gap-[var(--spacing-xl)] overflow-y-auto\">\n <span\n className=\"shrink-0\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: \"var(--typo-h6-weight)\" as React.CSSProperties[\"fontWeight\"],\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Comments\n </span>\n {comments.map((comment) => (\n <CommentBubble key={comment.id} comment={comment} />\n ))}\n </div>\n\n {/* Comment input */}\n <div className=\"shrink-0 pt-[var(--spacing-xl)]\">\n <CommentInput\n placeholder={commentPlaceholder}\n onSubmit={onComment}\n />\n </div>\n </div>\n </TabsPrimitive.Content>\n </TabsPrimitive.Root>\n </SheetContent>\n </Sheet>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"@radix-ui/react-tabs",
|
|
14
|
+
"lucide-react"
|
|
15
|
+
],
|
|
16
|
+
"registryDependencies": [
|
|
17
|
+
"lib/utils",
|
|
18
|
+
"ui/avatar",
|
|
19
|
+
"ui/sheet"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "details-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/details-popup.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"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"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": [
|
|
14
|
+
"lib/utils",
|
|
15
|
+
"ui/dialog"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "feedback-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/feedback-popup.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"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"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": [
|
|
14
|
+
"lib/utils",
|
|
15
|
+
"ui/dialog",
|
|
16
|
+
"ui/button",
|
|
17
|
+
"ui/textarea"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "form-group",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Single-column form layout with header (title, sort, filter, action button), configurable fields, and footer (cancel/save). Supports text inputs, textareas, selects, date pickers, multiselect checkboxes, checkbox groups, radio groups, multiselect tags, image/file uploaders, and sliders.",
|
|
5
5
|
"files": [
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/form-group.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { Label } from \"../ui/label\";\nimport { TextInput } from \"../ui/text-input\";\nimport { Textarea } from \"../ui/textarea\";\nimport { DateInput } from \"../ui/date-input\";\nimport { Slider } from \"../ui/slider\";\nimport { RadioGroup, RadioGroupItem } from \"../ui/radio-group\";\nimport { CheckboxWithLabel } from \"../ui/checkbox\";\nimport { MultiselectTags } from \"../ui/multiselect-tags\";\nimport { MultiselectCheckboxField, type CheckboxOption } from \"../ui/multiselect-checkbox-field\";\nimport { ImageUploader, type UploadedImage } from \"../ui/image-uploader\";\nimport { FileUploader, type UploadedFile } from \"../ui/file-uploader\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { TitleGroup } from \"./title-group\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface FormFieldConfig {\n id: string;\n label: string;\n type:\n | \"text\"\n | \"textarea\"\n | \"select\"\n | \"date\"\n | \"multiselect-checkbox\"\n | \"checkbox-group\"\n | \"radio-group\"\n | \"multiselect-tags\"\n | \"image-uploader\"\n | \"file-uploader\"\n | \"slider\";\n placeholder?: string;\n options?: { id: string; label: string }[];\n value?: string | string[] | number[] | UploadedImage[] | UploadedFile[];\n min?: number;\n max?: number;\n step?: number;\n fullWidth?: boolean;\n disabled?: boolean;\n}\n\nexport interface FormRowConfig {\n id: string;\n fields: FormFieldConfig[];\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FormGroupProps {\n /** Form title */\n title?: string;\n /** Form description */\n description?: string;\n /** Row configurations containing field definitions */\n rows?: FormRowConfig[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: { id: string; label: string }[];\n /** Primary action button text */\n actionButtonText?: string;\n /** Cancel button text */\n cancelButtonText?: string;\n /** Save button text */\n saveButtonText?: string;\n /** Input size variant */\n inputSize?: \"sm\" | \"default\" | \"lg\";\n /** Callback when action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when cancel button is clicked */\n onCancel?: () => void;\n /** Callback when save button is clicked */\n onSave?: () => void;\n /** Callback when a field value changes */\n onFieldChange?: (fieldId: string, value: unknown) => void;\n /** Show header section */\n showHeader?: boolean;\n /** Show footer section */\n showFooter?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"name-asc\", label: \"Name (A-Z)\" },\n { id: \"name-desc\", label: \"Name (Z-A)\" },\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n];\n\nconst defaultCheckboxOptions: CheckboxOption[] = [\n { id: \"option-1\", label: \"Option 1\" },\n { id: \"option-2\", label: \"Option 1\" },\n { id: \"option-3\", label: \"Option 1\" },\n { id: \"option-4\", label: \"Option 1\" },\n { id: \"option-5\", label: \"Option 1\" },\n];\n\nconst defaultRadioOptions = [\n { id: \"option-a\", label: \"Option\" },\n { id: \"option-b\", label: \"Option\" },\n { id: \"option-c\", label: \"Option\" },\n { id: \"option-d\", label: \"Option\" },\n];\n\nconst defaultSelectOptions = [\n { id: \"placeholder\", label: \"Placeholder\" },\n { id: \"option-1\", label: \"Option 1\" },\n { id: \"option-2\", label: \"Option 2\" },\n { id: \"option-3\", label: \"Option 3\" },\n];\n\nconst defaultRows: FormRowConfig[] = [\n {\n id: \"row-1\",\n fields: [\n { id: \"field-1\", label: \"Label\", type: \"text\", placeholder: \"Placeholder\" },\n { id: \"field-2\", label: \"Label\", type: \"text\", placeholder: \"Placeholder\" },\n ],\n },\n {\n id: \"row-2\",\n fields: [\n { id: \"field-3\", label: \"Label\", type: \"text\", placeholder: \"Placeholder\" },\n { id: \"field-4\", label: \"Label\", type: \"text\", placeholder: \"Placeholder\" },\n ],\n },\n {\n id: \"row-3\",\n fields: [\n { id: \"field-5\", label: \"Label\", type: \"text\", placeholder: \"Placeholder\" },\n { id: \"field-6\", label: \"Label\", type: \"text\", placeholder: \"Placeholder\" },\n ],\n },\n {\n id: \"row-4\",\n fields: [\n { id: \"field-7\", label: \"Label\", type: \"text\", placeholder: \"Placeholder\", fullWidth: true },\n ],\n },\n {\n id: \"row-5\",\n fields: [\n { id: \"field-8\", label: \"Label\", type: \"text\", placeholder: \"Placeholder\", fullWidth: true },\n ],\n },\n {\n id: \"row-6\",\n fields: [\n {\n id: \"field-9\",\n label: \"Label\",\n type: \"select\",\n placeholder: \"Placeholder\",\n options: defaultSelectOptions,\n fullWidth: true,\n },\n ],\n },\n {\n id: \"row-7\",\n fields: [\n {\n id: \"field-10\",\n label: \"Label\",\n type: \"multiselect-checkbox\",\n options: defaultCheckboxOptions,\n value: [\"option-1\"],\n fullWidth: true,\n },\n ],\n },\n {\n id: \"row-8\",\n fields: [\n { id: \"field-11\", label: \"Label\", type: \"date\", placeholder: \"2/21/2024\", fullWidth: true },\n ],\n },\n {\n id: \"row-9\",\n fields: [\n {\n id: \"field-12\",\n label: \"Label\",\n type: \"checkbox-group\",\n options: [\n { id: \"cb-1\", label: \"Label\" },\n { id: \"cb-2\", label: \"Label\" },\n { id: \"cb-3\", label: \"Label\" },\n { id: \"cb-4\", label: \"Label\" },\n { id: \"cb-5\", label: \"Label\" },\n { id: \"cb-6\", label: \"Label\" },\n ],\n fullWidth: true,\n },\n ],\n },\n {\n id: \"row-10\",\n fields: [\n {\n id: \"field-13\",\n label: \"Label\",\n type: \"radio-group\",\n options: defaultRadioOptions,\n fullWidth: true,\n },\n ],\n },\n {\n id: \"row-11\",\n fields: [\n { id: \"field-14\", label: \"Label\", type: \"text\", placeholder: \"Placeholder\", fullWidth: true },\n ],\n },\n {\n id: \"row-12\",\n fields: [\n {\n id: \"field-15\",\n label: \"Label\",\n type: \"multiselect-tags\",\n value: [\"Choice A\", \"Choice B\"],\n fullWidth: true,\n },\n ],\n },\n {\n id: \"row-13\",\n fields: [\n { id: \"field-16\", label: \"Label\", type: \"image-uploader\", fullWidth: true },\n ],\n },\n {\n id: \"row-14\",\n fields: [\n { id: \"field-17\", label: \"Label\", type: \"file-uploader\", fullWidth: true },\n ],\n },\n {\n id: \"row-15\",\n fields: [\n {\n id: \"field-18\",\n label: \"Label\",\n type: \"slider\",\n value: [0],\n min: 0,\n max: 1000,\n fullWidth: true,\n },\n ],\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface FormFieldProps {\n field: FormFieldConfig;\n inputSize?: \"sm\" | \"default\" | \"lg\";\n onChange?: (fieldId: string, value: unknown) => void;\n}\n\nfunction FormField({ field, inputSize = \"default\", onChange }: FormFieldProps) {\n const [localValue, setLocalValue] = React.useState<unknown>(field.value);\n \n // State for multiselect tags\n const [tags, setTags] = React.useState<string[]>(\n Array.isArray(field.value) && field.type === \"multiselect-tags\"\n ? (field.value as string[])\n : []\n );\n\n // State for checkbox group\n const [checkedItems, setCheckedItems] = React.useState<string[]>(\n Array.isArray(field.value) && field.type === \"checkbox-group\"\n ? (field.value as string[])\n : []\n );\n\n // State for images and files\n const [images, setImages] = React.useState<UploadedImage[]>(\n Array.isArray(field.value) && field.type === \"image-uploader\"\n ? (field.value as UploadedImage[])\n : []\n );\n const [files, setFiles] = React.useState<UploadedFile[]>(\n Array.isArray(field.value) && field.type === \"file-uploader\"\n ? (field.value as UploadedFile[])\n : []\n );\n\n // State for slider\n const [sliderValue, setSliderValue] = React.useState<number[]>(\n Array.isArray(field.value) && field.type === \"slider\"\n ? (field.value as number[])\n : [0]\n );\n\n const handleChange = (value: unknown) => {\n setLocalValue(value);\n onChange?.(field.id, value);\n };\n\n const renderField = () => {\n switch (field.type) {\n case \"text\":\n return (\n <TextInput\n inputSize={inputSize}\n placeholder={field.placeholder}\n value={typeof localValue === \"string\" ? localValue : \"\"}\n onChange={(e) => handleChange(e.target.value)}\n disabled={field.disabled}\n />\n );\n\n case \"textarea\":\n return (\n <Textarea\n inputSize={inputSize}\n placeholder={field.placeholder}\n value={typeof localValue === \"string\" ? localValue : \"\"}\n onChange={(e) => handleChange(e.target.value)}\n disabled={field.disabled}\n />\n );\n\n case \"select\":\n return (\n <Select\n value={typeof localValue === \"string\" ? localValue : undefined}\n onValueChange={(val) => handleChange(val)}\n disabled={field.disabled}\n >\n <SelectTrigger inputSize={inputSize}>\n <SelectValue placeholder={field.placeholder || \"Select...\"} />\n </SelectTrigger>\n <SelectContent>\n {field.options?.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n );\n\n case \"date\":\n return (\n <DateInput\n inputSize={inputSize}\n placeholder={field.placeholder}\n value={typeof localValue === \"string\" ? localValue : \"\"}\n onChange={(val) => handleChange(val)}\n disabled={field.disabled}\n />\n );\n\n case \"multiselect-checkbox\":\n return (\n <MultiselectCheckboxField\n options={field.options}\n selectedValues={\n Array.isArray(localValue) ? (localValue as string[]) : []\n }\n onChange={(vals) => handleChange(vals)}\n inputSize={inputSize}\n disabled={field.disabled}\n />\n );\n\n case \"checkbox-group\":\n return (\n <div\n className=\"flex flex-wrap items-center\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {field.options?.map((option) => (\n <CheckboxWithLabel\n key={option.id}\n checked={checkedItems.includes(option.id)}\n onCheckedChange={(checked) => {\n const newChecked = checked\n ? [...checkedItems, option.id]\n : checkedItems.filter((id) => id !== option.id);\n setCheckedItems(newChecked);\n onChange?.(field.id, newChecked);\n }}\n disabled={field.disabled}\n >\n {option.label}\n </CheckboxWithLabel>\n ))}\n </div>\n );\n\n case \"radio-group\":\n return (\n <RadioGroup\n value={typeof localValue === \"string\" ? localValue : undefined}\n onValueChange={(val) => handleChange(val)}\n disabled={field.disabled}\n className=\"flex flex-wrap items-center\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {field.options?.map((option) => (\n <div\n key={option.id}\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n <RadioGroupItem value={option.id} id={`${field.id}-${option.id}`} />\n <label\n htmlFor={`${field.id}-${option.id}`}\n className=\"text-[var(--canvas-text-muted)] cursor-pointer\"\n style={{\n fontFamily: \"var(--typo-body-s-font)\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {option.label}\n </label>\n </div>\n ))}\n </RadioGroup>\n );\n\n case \"multiselect-tags\":\n return (\n <MultiselectTags\n tags={tags}\n inputSize={inputSize}\n onAdd={(tag) => {\n const newTags = [...tags, tag];\n setTags(newTags);\n onChange?.(field.id, newTags);\n }}\n onRemove={(tag) => {\n const newTags = tags.filter((t) => t !== tag);\n setTags(newTags);\n onChange?.(field.id, newTags);\n }}\n disabled={field.disabled}\n />\n );\n\n case \"image-uploader\":\n return (\n <ImageUploader\n images={images}\n onImagesChange={(newImages) => {\n setImages(newImages);\n onChange?.(field.id, newImages);\n }}\n disabled={field.disabled}\n />\n );\n\n case \"file-uploader\":\n return (\n <FileUploader\n files={files}\n onFilesChange={(newFiles) => {\n setFiles(newFiles);\n onChange?.(field.id, newFiles);\n }}\n disabled={field.disabled}\n />\n );\n\n case \"slider\":\n return (\n <Slider\n inputSize={inputSize}\n value={sliderValue}\n onValueChange={(vals) => {\n setSliderValue(vals);\n onChange?.(field.id, vals);\n }}\n min={field.min ?? 0}\n max={field.max ?? 100}\n step={field.step ?? 1}\n showLabel\n labelFormatter={(vals) => `$${vals[0]}`}\n disabled={field.disabled}\n />\n );\n\n default:\n return null;\n }\n };\n\n return (\n <div\n className={cn(\n \"flex flex-col w-full\",\n field.fullWidth ? \"col-span-full\" : \"\"\n )}\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <Label>{field.label}</Label>\n {renderField()}\n </div>\n );\n}\n\ninterface FormRowProps {\n row: FormRowConfig;\n inputSize?: \"sm\" | \"default\" | \"lg\";\n onChange?: (fieldId: string, value: unknown) => void;\n}\n\nfunction FormRow({ row, inputSize, onChange }: FormRowProps) {\n // Check if all fields in the row are full width\n const allFullWidth = row.fields.every((f) => f.fullWidth);\n \n if (allFullWidth || row.fields.length === 1) {\n // Render as single column\n return (\n <div className=\"flex flex-col w-full\" style={{ gap: \"var(--spacing-3xl)\" }}>\n {row.fields.map((field) => (\n <FormField\n key={field.id}\n field={field}\n inputSize={inputSize}\n onChange={onChange}\n />\n ))}\n </div>\n );\n }\n\n // Render as responsive 2-column grid\n return (\n <div\n className=\"grid grid-cols-1 sm:grid-cols-2 w-full\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {row.fields.map((field) => (\n <FormField\n key={field.id}\n field={field}\n inputSize={inputSize}\n onChange={onChange}\n />\n ))}\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Form Group Block\n *\n * A comprehensive form layout block with various input types arranged\n * in responsive rows. Includes header section with title, description,\n * sort/filter controls, and footer with action buttons.\n *\n * @example\n * ```tsx\n * <FormGroup\n * title=\"Create Entry\"\n * description=\"Fill in the details below\"\n * onSave={() => console.log(\"Save\")}\n * onCancel={() => console.log(\"Cancel\")}\n * />\n * ```\n */\nexport function FormGroup({\n title = \"Title\",\n description = \"Description\",\n rows = defaultRows,\n sortOptions = defaultSortOptions,\n actionButtonText = \"Add new\",\n cancelButtonText = \"Cancel\",\n saveButtonText = \"Save changes\",\n inputSize = \"default\",\n onAddNew,\n onSort,\n onCancel,\n onSave,\n onFieldChange,\n showHeader = true,\n showFooter = true,\n className,\n}: FormGroupProps) {\n return (\n <div\n className={cn(\"flex flex-col w-full bg-[var(--canvas-background)]\", className)}\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Header Section */}\n {showHeader && (\n <TitleGroup title={title} subtitle={description} sortOptions={sortOptions} onSort={onSort} actionButtonText={actionButtonText} onAction={onAddNew} />\n )}\n\n {/* Form Rows */}\n {rows.map((row) => (\n <FormRow\n key={row.id}\n row={row}\n inputSize={inputSize}\n onChange={onFieldChange}\n />\n ))}\n\n {/* Footer Section */}\n {showFooter && (\n <div\n className=\"flex items-center justify-end w-full overflow-hidden\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n <Button variant=\"outline\" size=\"default\" onClick={onCancel}>\n {cancelButtonText}\n </Button>\n <Button variant=\"primary\" size=\"default\" onClick={onSave}>\n {saveButtonText}\n </Button>\n </div>\n )}\n </div>\n );\n}\n\n// Export sub-components for advanced usage\nexport { FormField, FormRow };\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { Label } from \"../ui/label\";\nimport { TextInput } from \"../ui/text-input\";\nimport { Textarea } from \"../ui/textarea\";\nimport { DateInput } from \"../ui/date-input\";\nimport { Slider } from \"../ui/slider\";\nimport { RadioGroup, RadioGroupItem } from \"../ui/radio-group\";\nimport { CheckboxWithLabel } from \"../ui/checkbox\";\nimport { MultiselectTags } from \"../ui/multiselect-tags\";\nimport { MultiselectCheckboxField, type CheckboxOption } from \"../ui/multiselect-checkbox-field\";\nimport { ImageUploader, type UploadedImage } from \"../ui/image-uploader\";\nimport { FileUploader, type UploadedFile } from \"../ui/file-uploader\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { TitleGroup } from \"./title-group\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface FormFieldConfig {\n id: string;\n label: string;\n type:\n | \"text\"\n | \"textarea\"\n | \"select\"\n | \"date\"\n | \"multiselect-checkbox\"\n | \"checkbox-group\"\n | \"radio-group\"\n | \"multiselect-tags\"\n | \"image-uploader\"\n | \"file-uploader\"\n | \"slider\";\n placeholder?: string;\n options?: { id: string; label: string }[];\n value?: string | string[] | number[] | UploadedImage[] | UploadedFile[];\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FormGroupProps {\n /** Form title */\n title?: string;\n /** Form description */\n description?: string;\n /** Flat array of field configurations */\n fields?: FormFieldConfig[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: { id: string; label: string }[];\n /** Primary action button text */\n actionButtonText?: string;\n /** Cancel button text */\n cancelButtonText?: string;\n /** Save button text */\n saveButtonText?: string;\n /** Input size variant */\n inputSize?: \"sm\" | \"default\" | \"lg\";\n /** Callback when action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when cancel button is clicked */\n onCancel?: () => void;\n /** Callback when save button is clicked */\n onSave?: () => void;\n /** Callback when a field value changes */\n onFieldChange?: (fieldId: string, value: unknown) => void;\n /** Show header section */\n showHeader?: boolean;\n /** Show footer section */\n showFooter?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"name-asc\", label: \"Name (A-Z)\" },\n { id: \"name-desc\", label: \"Name (Z-A)\" },\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n];\n\nconst defaultCheckboxOptions: CheckboxOption[] = [\n { id: \"option-1\", label: \"Option 1\" },\n { id: \"option-2\", label: \"Option 1\" },\n { id: \"option-3\", label: \"Option 1\" },\n { id: \"option-4\", label: \"Option 1\" },\n { id: \"option-5\", label: \"Option 1\" },\n];\n\nconst defaultSelectOptions = [\n { id: \"placeholder\", label: \"Placeholder\" },\n { id: \"option-1\", label: \"Option 1\" },\n { id: \"option-2\", label: \"Option 2\" },\n { id: \"option-3\", label: \"Option 3\" },\n];\n\nconst defaultFields: FormFieldConfig[] = [\n { id: \"field-1\", label: \"Label\", type: \"text\" },\n { id: \"field-2\", label: \"Label\", type: \"text\" },\n {\n id: \"field-3\",\n label: \"Label\",\n type: \"select\",\n options: defaultSelectOptions,\n },\n {\n id: \"field-4\",\n label: \"Label\",\n type: \"multiselect-checkbox\",\n options: defaultCheckboxOptions,\n value: [\"option-1\"],\n },\n { id: \"field-5\", label: \"Label\", type: \"date\", placeholder: \"2/21/2024\" },\n {\n id: \"field-6\",\n label: \"Label\",\n type: \"checkbox-group\",\n options: [\n { id: \"cb-1\", label: \"Label\" },\n { id: \"cb-2\", label: \"Label\" },\n { id: \"cb-3\", label: \"Label\" },\n { id: \"cb-4\", label: \"Label\" },\n { id: \"cb-5\", label: \"Label\" },\n { id: \"cb-6\", label: \"Label\" },\n ],\n },\n {\n id: \"field-7\",\n label: \"Label\",\n type: \"radio-group\",\n options: [\n { id: \"option-a\", label: \"Option\" },\n { id: \"option-b\", label: \"Option\" },\n { id: \"option-c\", label: \"Option\" },\n { id: \"option-d\", label: \"Option\" },\n ],\n },\n { id: \"field-8\", label: \"Label\", type: \"text\" },\n {\n id: \"field-9\",\n label: \"Label\",\n type: \"multiselect-tags\",\n value: [\"Choice A\", \"Choice B\"],\n },\n { id: \"field-10\", label: \"Label\", type: \"image-uploader\", placeholder: \"Drop image here\" },\n { id: \"field-11\", label: \"Label\", type: \"image-uploader\", placeholder: \"Drop images here\" },\n { id: \"field-12\", label: \"Label\", type: \"file-uploader\" },\n {\n id: \"field-13\",\n label: \"Label\",\n type: \"slider\",\n value: [0],\n min: 0,\n max: 1000,\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface FormFieldProps {\n field: FormFieldConfig;\n inputSize?: \"sm\" | \"default\" | \"lg\";\n onChange?: (fieldId: string, value: unknown) => void;\n}\n\nfunction FormField({ field, inputSize = \"default\", onChange }: FormFieldProps) {\n const [localValue, setLocalValue] = React.useState<unknown>(field.value);\n\n const [tags, setTags] = React.useState<string[]>(\n Array.isArray(field.value) && field.type === \"multiselect-tags\"\n ? (field.value as string[])\n : []\n );\n\n const [checkedItems, setCheckedItems] = React.useState<string[]>(\n Array.isArray(field.value) && field.type === \"checkbox-group\"\n ? (field.value as string[])\n : []\n );\n\n const [images, setImages] = React.useState<UploadedImage[]>(\n Array.isArray(field.value) && field.type === \"image-uploader\"\n ? (field.value as UploadedImage[])\n : []\n );\n const [files, setFiles] = React.useState<UploadedFile[]>(\n Array.isArray(field.value) && field.type === \"file-uploader\"\n ? (field.value as UploadedFile[])\n : []\n );\n\n const [sliderValue, setSliderValue] = React.useState<number[]>(\n Array.isArray(field.value) && field.type === \"slider\"\n ? (field.value as number[])\n : [0]\n );\n\n const handleChange = (value: unknown) => {\n setLocalValue(value);\n onChange?.(field.id, value);\n };\n\n const renderField = () => {\n switch (field.type) {\n case \"text\":\n return (\n <TextInput\n inputSize={inputSize}\n placeholder={field.placeholder}\n value={typeof localValue === \"string\" ? localValue : \"\"}\n onChange={(e) => handleChange(e.target.value)}\n disabled={field.disabled}\n />\n );\n\n case \"textarea\":\n return (\n <Textarea\n inputSize={inputSize}\n placeholder={field.placeholder}\n value={typeof localValue === \"string\" ? localValue : \"\"}\n onChange={(e) => handleChange(e.target.value)}\n disabled={field.disabled}\n />\n );\n\n case \"select\":\n return (\n <Select\n value={typeof localValue === \"string\" ? localValue : undefined}\n onValueChange={(val) => handleChange(val)}\n disabled={field.disabled}\n >\n <SelectTrigger inputSize={inputSize}>\n <SelectValue placeholder={field.placeholder || \"Select...\"} />\n </SelectTrigger>\n <SelectContent>\n {field.options?.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n );\n\n case \"date\":\n return (\n <DateInput\n inputSize={inputSize}\n placeholder={field.placeholder}\n value={typeof localValue === \"string\" ? localValue : \"\"}\n onChange={(val) => handleChange(val)}\n disabled={field.disabled}\n />\n );\n\n case \"multiselect-checkbox\":\n return (\n <MultiselectCheckboxField\n options={field.options}\n selectedValues={\n Array.isArray(localValue) ? (localValue as string[]) : []\n }\n onChange={(vals) => handleChange(vals)}\n inputSize={inputSize}\n disabled={field.disabled}\n />\n );\n\n case \"checkbox-group\":\n return (\n <div\n className=\"flex flex-wrap items-center\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {field.options?.map((option) => (\n <CheckboxWithLabel\n key={option.id}\n checked={checkedItems.includes(option.id)}\n onCheckedChange={(checked) => {\n const newChecked = checked\n ? [...checkedItems, option.id]\n : checkedItems.filter((id) => id !== option.id);\n setCheckedItems(newChecked);\n onChange?.(field.id, newChecked);\n }}\n disabled={field.disabled}\n >\n {option.label}\n </CheckboxWithLabel>\n ))}\n </div>\n );\n\n case \"radio-group\":\n return (\n <RadioGroup\n value={typeof localValue === \"string\" ? localValue : undefined}\n onValueChange={(val) => handleChange(val)}\n disabled={field.disabled}\n className=\"flex flex-wrap items-center\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {field.options?.map((option) => (\n <div\n key={option.id}\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n <RadioGroupItem value={option.id} id={`${field.id}-${option.id}`} />\n <label\n htmlFor={`${field.id}-${option.id}`}\n className=\"text-[var(--canvas-text-muted)] cursor-pointer\"\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 }}\n >\n {option.label}\n </label>\n </div>\n ))}\n </RadioGroup>\n );\n\n case \"multiselect-tags\":\n return (\n <MultiselectTags\n tags={tags}\n inputSize={inputSize}\n onAdd={(tag) => {\n const newTags = [...tags, tag];\n setTags(newTags);\n onChange?.(field.id, newTags);\n }}\n onRemove={(tag) => {\n const newTags = tags.filter((t) => t !== tag);\n setTags(newTags);\n onChange?.(field.id, newTags);\n }}\n disabled={field.disabled}\n />\n );\n\n case \"image-uploader\":\n return (\n <ImageUploader\n images={images}\n placeholder={field.placeholder}\n onImagesChange={(newImages) => {\n setImages(newImages);\n onChange?.(field.id, newImages);\n }}\n disabled={field.disabled}\n />\n );\n\n case \"file-uploader\":\n return (\n <FileUploader\n files={files}\n placeholder={field.placeholder}\n onFilesChange={(newFiles) => {\n setFiles(newFiles);\n onChange?.(field.id, newFiles);\n }}\n disabled={field.disabled}\n />\n );\n\n case \"slider\":\n return (\n <Slider\n inputSize={inputSize}\n value={sliderValue}\n onValueChange={(vals) => {\n setSliderValue(vals);\n onChange?.(field.id, vals);\n }}\n min={field.min ?? 0}\n max={field.max ?? 100}\n step={field.step ?? 1}\n showLabel\n labelFormatter={(vals) => `$${vals[0]}`}\n disabled={field.disabled}\n />\n );\n\n default:\n return null;\n }\n };\n\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <Label>{field.label}</Label>\n {renderField()}\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Form Group Block\n *\n * A single-column form layout block with configurable fields, header section\n * (title, description, sort/filter, action button), and footer with action buttons.\n * Supports text inputs, textareas, selects, date pickers, multiselect checkboxes,\n * checkbox groups, radio groups, multiselect tags, image/file uploaders, and sliders.\n *\n * @example\n * ```tsx\n * <FormGroup\n * title=\"Create Entry\"\n * description=\"Fill in the details below\"\n * fields={[\n * { id: \"name\", label: \"Name\", type: \"text\" },\n * { id: \"bio\", label: \"Bio\", type: \"textarea\" },\n * ]}\n * onSave={() => console.log(\"Save\")}\n * onCancel={() => console.log(\"Cancel\")}\n * />\n * ```\n */\nexport function FormGroup({\n title = \"Title\",\n description = \"Description\",\n fields = defaultFields,\n sortOptions = defaultSortOptions,\n actionButtonText = \"Add new\",\n cancelButtonText = \"Cancel\",\n saveButtonText = \"Save changes\",\n inputSize = \"default\",\n onAddNew,\n onSort,\n onCancel,\n onSave,\n onFieldChange,\n showHeader = true,\n showFooter = true,\n className,\n}: FormGroupProps) {\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n {/* Header Section - outside the form border */}\n {showHeader && (\n <TitleGroup title={title} subtitle={description} sortOptions={sortOptions} onSort={onSort} actionButtonText={actionButtonText} onAction={onAddNew} />\n )}\n\n {/* Form Body - fields + footer inside the border */}\n <div\n className=\"flex flex-col w-full rounded-[var(--radius-sm)] border border-[var(--canvas-border)] bg-[var(--canvas-background)]\"\n style={{ padding: \"var(--spacing-3xl)\", gap: \"var(--spacing-3xl)\" }}\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 {/* Footer Section */}\n {showFooter && (\n <div\n className=\"flex items-center justify-end w-full overflow-hidden\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n <Button variant=\"neutral\" size=\"default\" onClick={onCancel}>\n {cancelButtonText}\n </Button>\n <Button variant=\"primary\" size=\"default\" onClick={onSave}>\n {saveButtonText}\n </Button>\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// Export sub-component for advanced usage\nexport { FormField };\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/marketing/hero-fullwidth-image.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { MapPin, Calendar, Users } from \"@phosphor-icons/react\";\nimport { Button } from \"../../ui/button\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface HeroFullwidthImageProps {\n title?: string;\n subtitle?: string;\n backgroundImage?: string;\n}\n\nexport function HeroFullwidthImage({ \n title = \"Plan your next adventure\",\n subtitle = \"Live like locals from anywhere in the world\",\n backgroundImage = \"https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1400&h=500&fit=crop\"\n}: HeroFullwidthImageProps) {\n return (\n <section \n className=\"w-full pb-24 md:pb-28\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {/* Hero Image with Overlay */}\n <div \n className=\"relative w-full flex flex-col items-center justify-center px-4 md:px-10\"\n style={{\n height: \"420px\",\n }}\n >\n {/* Background Image */}\n <div \n className=\"absolute inset-0 bg-cover bg-center\"\n style={{ backgroundImage: `url(${backgroundImage})` }}\n />\n {/* Overlay */}\n <div className=\"absolute inset-0 bg-
|
|
9
|
+
"content": "\"use client\";\n\nimport { MapPin, Calendar, Users } from \"@phosphor-icons/react\";\nimport { Button } from \"../../ui/button\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface HeroFullwidthImageProps {\n title?: string;\n subtitle?: string;\n backgroundImage?: string;\n}\n\nexport function HeroFullwidthImage({ \n title = \"Plan your next adventure\",\n subtitle = \"Live like locals from anywhere in the world\",\n backgroundImage = \"https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1400&h=500&fit=crop\"\n}: HeroFullwidthImageProps) {\n return (\n <section \n className=\"w-full pb-24 md:pb-28\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {/* Hero Image with Overlay */}\n <div \n className=\"relative w-full flex flex-col items-center justify-center px-4 md:px-10\"\n style={{\n height: \"420px\",\n }}\n >\n {/* Background Image */}\n <div \n className=\"absolute inset-0 bg-cover bg-center\"\n style={{ backgroundImage: `url(${backgroundImage})` }}\n />\n {/* Overlay */}\n <div className=\"absolute inset-0 bg-[var(--canvas-overlay-bg)]\" />\n \n {/* Content */}\n <div className=\"relative z-10 flex flex-col items-center text-center gap-3 max-w-[992px] w-full\">\n <Typography\n variant=\"h2\"\n as=\"h1\"\n style={{ color: \"white\" }}\n >\n {title}\n </Typography>\n <Typography\n variant=\"body-xl\"\n style={{ color: \"white\" }}\n >\n {subtitle}\n </Typography>\n </div>\n\n {/* Search Bar - positioned at bottom, overlapping */}\n <div \n className=\"absolute left-1/2 -translate-x-1/2 flex flex-col md:flex-row items-center w-[calc(100%-32px)] max-w-[900px]\"\n style={{\n bottom: \"-40px\",\n backgroundColor: \"var(--canvas-background)\",\n borderRadius: \"var(--spacing-md)\",\n border: \"1px solid var(--canvas-border)\",\n padding: \"var(--spacing-md)\",\n boxShadow: \"0px 4px 16px 0px rgba(0, 0, 0, 0.04)\",\n }}\n >\n {/* Destination */}\n <div \n className=\"flex items-center gap-2 px-3 py-2 border-b md:border-b-0 md:border-r w-full md:flex-1 min-w-0\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n <MapPin size={20} className=\"shrink-0\" style={{ color: \"var(--canvas-text-placeholder)\" }} />\n <input\n type=\"text\"\n placeholder=\"Destination\"\n className=\"flex-1 bg-transparent outline-none min-w-0\"\n style={{\n fontFamily: \"var(--typo-global-font)\",\n fontSize: \"var(--typo-body-l-size)\",\n color: \"var(--canvas-text)\",\n }}\n />\n </div>\n\n {/* Check-in */}\n <div \n className=\"flex items-center gap-2 px-3 py-2 border-b md:border-b-0 md:border-r w-full md:flex-1 min-w-0\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n <Calendar size={20} className=\"shrink-0\" style={{ color: \"var(--canvas-text-placeholder)\" }} />\n <input\n type=\"text\"\n placeholder=\"Check-in\"\n className=\"flex-1 bg-transparent outline-none min-w-0\"\n style={{\n fontFamily: \"var(--typo-global-font)\",\n fontSize: \"var(--typo-body-l-size)\",\n color: \"var(--canvas-text)\",\n }}\n />\n </div>\n\n {/* Check-out */}\n <div \n className=\"flex items-center gap-2 px-3 py-2 border-b md:border-b-0 md:border-r w-full md:flex-1 min-w-0\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n <Calendar size={20} className=\"shrink-0\" style={{ color: \"var(--canvas-text-placeholder)\" }} />\n <input\n type=\"text\"\n placeholder=\"Check out\"\n className=\"flex-1 bg-transparent outline-none min-w-0\"\n style={{\n fontFamily: \"var(--typo-global-font)\",\n fontSize: \"var(--typo-body-l-size)\",\n color: \"var(--canvas-text)\",\n }}\n />\n </div>\n\n {/* Guests */}\n <div className=\"flex items-center gap-2 px-3 py-2 w-full md:flex-1 min-w-0\">\n <Users size={20} className=\"shrink-0\" style={{ color: \"var(--canvas-text-placeholder)\" }} />\n <input\n type=\"text\"\n placeholder=\"Guests\"\n className=\"flex-1 bg-transparent outline-none min-w-0\"\n style={{\n fontFamily: \"var(--typo-global-font)\",\n fontSize: \"var(--typo-body-l-size)\",\n color: \"var(--canvas-text)\",\n }}\n />\n </div>\n\n {/* Search Button */}\n <Button variant=\"primary\" size=\"lg\" className=\"shrink-0 w-full md:w-auto mt-2 md:mt-0 md:ml-2\">\n Search\n </Button>\n </div>\n </div>\n </section>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/marketing/hero-section.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { MagnifyingGlass } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface HeroSectionProps {\n title: string;\n subtitle: string;\n searchPlaceholder?: string;\n backgroundImage?: string;\n}\n\nexport function HeroSection({ \n title, \n subtitle, \n searchPlaceholder = \"Search for a place\",\n backgroundImage = \"https://images.unsplash.com/photo-1506929562872-bb421503ef21?w=1400&h=600&fit=crop\"\n}: HeroSectionProps) {\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-20 pt-5 pb-8 md:pb-12\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n <div \n className=\"relative w-full overflow-hidden flex items-center min-h-[320px] md:min-h-[420px] p-6 md:p-10\"\n style={{\n borderRadius: \"var(--spacing-2xl)\",\n }}\n >\n {/* Background Image */}\n <div \n className=\"absolute inset-0 bg-cover bg-center\"\n style={{ backgroundImage: `url(${backgroundImage})` }}\n />\n {/* Overlay */}\n <div className=\"absolute inset-0 bg-
|
|
9
|
+
"content": "\"use client\";\n\nimport { MagnifyingGlass } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface HeroSectionProps {\n title: string;\n subtitle: string;\n searchPlaceholder?: string;\n backgroundImage?: string;\n}\n\nexport function HeroSection({ \n title, \n subtitle, \n searchPlaceholder = \"Search for a place\",\n backgroundImage = \"https://images.unsplash.com/photo-1506929562872-bb421503ef21?w=1400&h=600&fit=crop\"\n}: HeroSectionProps) {\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-20 pt-5 pb-8 md:pb-12\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n <div \n className=\"relative w-full overflow-hidden flex items-center min-h-[320px] md:min-h-[420px] p-6 md:p-10\"\n style={{\n borderRadius: \"var(--spacing-2xl)\",\n }}\n >\n {/* Background Image */}\n <div \n className=\"absolute inset-0 bg-cover bg-center\"\n style={{ backgroundImage: `url(${backgroundImage})` }}\n />\n {/* Overlay */}\n <div className=\"absolute inset-0 bg-[var(--canvas-overlay-bg)]\" />\n \n {/* Content */}\n <div \n className=\"relative z-10 flex flex-col max-w-[480px]\"\n style={{ gap: \"var(--spacing-5xl)\" }}\n >\n <div className=\"flex flex-col text-white\" style={{ gap: \"var(--spacing-md)\" }}>\n <Typography \n variant=\"h2\" \n as=\"h1\" \n className=\"text-white\"\n style={{ color: \"white\" }}\n >\n {title}\n </Typography>\n <Typography \n variant=\"body-xl\" \n className=\"text-white/90\"\n style={{ color: \"rgba(255,255,255,0.9)\" }}\n >\n {subtitle}\n </Typography>\n </div>\n \n {/* Search Bar */}\n <div \n className=\"flex items-center w-full shadow-lg\"\n style={{\n height: \"64px\",\n backgroundColor: \"var(--canvas-background)\",\n borderRadius: \"var(--spacing-md)\",\n border: \"1px solid var(--canvas-border)\",\n padding: \"var(--spacing-md) var(--spacing-md) var(--spacing-md) var(--spacing-xl)\",\n gap: \"var(--spacing-3xl)\",\n }}\n >\n <input\n type=\"text\"\n placeholder={searchPlaceholder}\n className=\"flex-1 bg-transparent outline-none\"\n style={{\n fontFamily: \"var(--typo-global-font)\",\n fontSize: \"var(--typo-body-l-size)\",\n color: \"var(--canvas-text)\",\n }}\n />\n <button \n className=\"shrink-0 flex items-center justify-center\"\n style={{\n width: \"44px\",\n height: \"44px\",\n backgroundColor: \"var(--canvas-primary)\",\n borderRadius: \"var(--spacing-md)\",\n }}\n >\n <MagnifyingGlass size={20} color=\"white\" weight=\"bold\" />\n </button>\n </div>\n </div>\n </div>\n </section>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "image-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/image-popup.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"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"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": [
|
|
14
|
+
"lib/utils",
|
|
15
|
+
"ui/dialog"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "invoice-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/invoice-popup.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"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"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"lib/utils",
|
|
17
|
+
"ui/dialog",
|
|
18
|
+
"ui/button"
|
|
19
|
+
]
|
|
20
|
+
}
|