canvas-ui-sdk 4.0.1 → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +41 -3
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.js +38 -30
  4. package/dist/index.js.map +1 -1
  5. package/mcp/dist/index.js +20 -13
  6. package/package.json +3 -2
  7. package/registry/blocks/category-grid.json +1 -1
  8. package/registry/blocks/confirmation-popup.json +1 -1
  9. package/registry/blocks/contact-form-popup.json +1 -1
  10. package/registry/blocks/details-popup.json +1 -1
  11. package/registry/blocks/feedback-popup.json +1 -1
  12. package/registry/blocks/form-popup.json +1 -1
  13. package/registry/blocks/image-popup.json +1 -1
  14. package/registry/blocks/invoice-popup.json +1 -1
  15. package/registry/blocks/list-popup.json +1 -1
  16. package/registry/blocks/multistep-form-popup.json +1 -1
  17. package/registry/blocks/nps-survey-popup.json +1 -1
  18. package/registry/blocks/page-previews.json +1 -1
  19. package/registry/blocks/persona-card.json +1 -1
  20. package/registry/blocks/personalize-feed-popup.json +1 -1
  21. package/registry/blocks/pricing-plans-popup.json +1 -1
  22. package/registry/blocks/purchase-confirmation-popup.json +1 -1
  23. package/registry/blocks/share-project-popup.json +1 -1
  24. package/registry/blocks/small-edit-popup.json +1 -1
  25. package/registry/blocks/terms-of-service-popup.json +1 -1
  26. package/registry/blocks/video-playlist.json +1 -1
  27. package/registry/blocks/video-popup.json +1 -1
  28. package/registry/blocks/view-profile-popup.json +1 -1
  29. package/registry/layout/dashboard-shell.json +1 -1
  30. package/registry/layout/double-sidebar-shell.json +1 -1
  31. package/registry/layout/double-sidebar.json +1 -1
  32. package/registry/layout/icon-sidebar-shell.json +1 -1
  33. package/registry/ui/dropdown-menu.json +1 -1
  34. package/registry/ui/popover.json +1 -1
  35. package/registry/ui/select.json +1 -1
  36. package/styles/tokens.reference.css +7 -0
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/blocks/multistep-form-popup.tsx",
17
17
  "type": "registry:block",
18
- "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Dialog, DialogContent } from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { Label } from \"../ui/label\";\nimport { Input } from \"../ui/input\";\nimport { Textarea } from \"../ui/textarea\";\nimport { DateInput } from \"../ui/date-input\";\nimport {\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n} from \"../ui/select\";\nimport { MultiselectTags } from \"../ui/multiselect-tags\";\nimport { Loader } from \"./loader\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MultistepField {\n /** Unique field identifier */\n id: string;\n /** Field label */\n label: string;\n /** Field input type */\n type:\n | \"text\"\n | \"select\"\n | \"date\"\n | \"textarea\"\n | \"multiselect-tags\";\n /** Placeholder text */\n placeholder?: string;\n /** When true, the field takes 50% width and pairs with the next half field */\n half?: boolean;\n /** Options for select or multiselect-tags */\n options?: { id: string; label: string }[];\n /** Default value */\n value?: string | string[];\n}\n\nexport interface MultistepFormStep {\n /** Step title */\n title: string;\n /** Step description shown below the title */\n description?: string;\n /** Form fields for this step */\n fields: MultistepField[];\n}\n\nexport interface BillingPlan {\n /** Unique plan identifier */\n id: string;\n /** Plan name */\n name: string;\n /** Price display text */\n price: string;\n /** Description text */\n description: string;\n /** Optional badge text */\n badge?: string;\n}\n\nexport interface MultistepFormPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Step configurations — each has title, description, and fields */\n steps?: MultistepFormStep[];\n /** Billing plan options for the final step */\n billingPlans?: BillingPlan[];\n /** Loading state title */\n loadingTitle?: string;\n /** Loading state description */\n loadingDescription?: string;\n /** Success state title */\n successTitle?: string;\n /** Success state description */\n successDescription?: string;\n /** Success action button text */\n successButtonText?: string;\n /** Callback when the form is completed (after all steps) */\n onComplete?: () => void;\n /** Callback when the success action button is clicked */\n onSuccessButtonClick?: () => void;\n /** Additional class names for the dialog content */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_STEPS: MultistepFormStep[] = [\n {\n title: \"Enter your personal information to get started\",\n fields: [\n { id: \"email\", label: \"Email\", type: \"text\", placeholder: \"jane@example.com\" },\n { id: \"company\", label: \"Company name\", type: \"text\", placeholder: \"Acme Inc.\" },\n { id: \"firstName\", label: \"First name\", type: \"text\", placeholder: \"Jane\", half: true },\n { id: \"lastName\", label: \"Last name\", type: \"text\", placeholder: \"Doe\", half: true },\n { id: \"city\", label: \"City\", type: \"text\", placeholder: \"San Francisco\" },\n {\n id: \"country\",\n label: \"Country\",\n type: \"select\",\n half: true,\n options: [\n { id: \"us\", label: \"United States\" },\n { id: \"ca\", label: \"Canada\" },\n { id: \"uk\", label: \"United Kingdom\" },\n { id: \"au\", label: \"Australia\" },\n ],\n },\n {\n id: \"state\",\n label: \"State\",\n type: \"select\",\n half: true,\n options: [\n { id: \"ca\", label: \"California\" },\n { id: \"ny\", label: \"New York\" },\n { id: \"tx\", label: \"Texas\" },\n { id: \"wa\", label: \"Washington\" },\n ],\n },\n { id: \"zip\", label: \"Zip / Postal code\", type: \"text\", placeholder: \"94102\" },\n { id: \"profession\", label: \"Profession\", type: \"text\", placeholder: \"Software Engineer\", half: true },\n { id: \"startDate\", label: \"Start date\", type: \"date\", half: true },\n ],\n },\n {\n title: \"Enter additional information below\",\n fields: [\n { id: \"address\", label: \"Address\", type: \"text\", placeholder: \"123 Main St\" },\n { id: \"stateProvince\", label: \"State / Province\", type: \"text\", placeholder: \"California\" },\n {\n id: \"occupation\",\n label: \"Occupation\",\n type: \"select\",\n half: true,\n options: [\n { id: \"eng\", label: \"Engineering\" },\n { id: \"design\", label: \"Design\" },\n { id: \"marketing\", label: \"Marketing\" },\n { id: \"sales\", label: \"Sales\" },\n ],\n },\n {\n id: \"status\",\n label: \"Status\",\n type: \"select\",\n half: true,\n options: [\n { id: \"active\", label: \"Active\" },\n { id: \"inactive\", label: \"Inactive\" },\n { id: \"pending\", label: \"Pending\" },\n ],\n },\n { id: \"phone\", label: \"Phone\", type: \"text\", placeholder: \"(555) 123-4567\", half: true },\n { id: \"fax\", label: \"Fax\", type: \"text\", placeholder: \"(555) 987-6543\", half: true },\n { id: \"step2StartDate\", label: \"Start date\", type: \"date\", half: true },\n { id: \"step2EndDate\", label: \"End date\", type: \"date\", half: true },\n { id: \"address2\", label: \"Address line 2\", type: \"text\", placeholder: \"Suite 200\" },\n {\n id: \"tags\",\n label: \"Tags\",\n type: \"multiselect-tags\",\n options: [\n { id: \"finance\", label: \"Finance\" },\n { id: \"technology\", label: \"Technology\" },\n { id: \"healthcare\", label: \"Healthcare\" },\n { id: \"education\", label: \"Education\" },\n { id: \"retail\", label: \"Retail\" },\n ],\n value: [],\n },\n ],\n },\n {\n title: \"Complete your account setup\",\n fields: [\n { id: \"step3Phone\", label: \"Phone\", type: \"text\", placeholder: \"(555) 123-4567\", half: true },\n { id: \"extension\", label: \"Extension\", type: \"text\", placeholder: \"1234\", half: true },\n { id: \"step3StartDate\", label: \"Start date\", type: \"date\", half: true },\n { id: \"step3EndDate\", label: \"End date\", type: \"date\", half: true },\n { id: \"copyrightYear\", label: \"Copyright year\", type: \"text\", placeholder: \"2026\", half: true },\n { id: \"licenseType\", label: \"License type\", type: \"text\", placeholder: \"Standard\", half: true },\n { id: \"notes\", label: \"Notes\", type: \"textarea\", placeholder: \"Add any additional notes here...\" },\n ],\n },\n];\n\nconst DEFAULT_BILLING_PLANS: BillingPlan[] = [\n {\n id: \"annual\",\n name: \"Annual billing\",\n price: \"$120/month\",\n description: \"Billed annually\",\n badge: \"Save 5%!\",\n },\n {\n id: \"monthly\",\n name: \"Monthly billing\",\n price: \"$150/month\",\n description: \"after your 30-day free trial\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// Inline Sub-Components\n// ---------------------------------------------------------------------------\n\nconst fontBase = \"var(--typo-global-font)\";\n\n/** Step indicator badge + title + description */\nfunction StepHeader({\n step,\n title,\n description,\n}: {\n step: number;\n title: string;\n description?: string;\n}) {\n return (\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-xl)\" }}>\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: 40,\n height: 40,\n borderRadius: \"var(--spacing-3xl)\",\n backgroundColor: \"var(--canvas-neutral-surface)\",\n }}\n >\n <span\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"24px\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {step}\n </span>\n </div>\n <h2\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"30px\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n {description && (\n <p\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"20px\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n paddingLeft: 56,\n }}\n >\n {description}\n </p>\n )}\n </div>\n );\n}\n\n/** Renders form fields with half-width pairing */\nfunction StepFormFields({\n fields,\n values,\n onChange,\n}: {\n fields: MultistepField[];\n values: Record<string, unknown>;\n onChange: (id: string, value: unknown) => void;\n}) {\n // Group fields into rows: half-width fields paired together\n const rows: MultistepField[][] = [];\n let i = 0;\n while (i < fields.length) {\n const field = fields[i];\n if (field.half && i + 1 < fields.length && fields[i + 1].half) {\n rows.push([field, fields[i + 1]]);\n i += 2;\n } else {\n rows.push([field]);\n i += 1;\n }\n }\n\n return (\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-3xl)\" }}>\n {rows.map((row, rowIdx) => (\n <div\n key={rowIdx}\n className={cn(\n \"flex gap-[var(--spacing-3xl)]\",\n row.length > 1 ? \"flex-col md:flex-row\" : \"flex-col\"\n )}\n >\n {row.map((field) => (\n <div\n key={field.id}\n className={cn(\n \"flex flex-col\",\n row.length > 1 ? \"flex-1\" : \"w-full\"\n )}\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <Label>{field.label}</Label>\n {renderField(field, values, onChange)}\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n}\n\nfunction renderField(\n field: MultistepField,\n values: Record<string, unknown>,\n onChange: (id: string, value: unknown) => void\n) {\n switch (field.type) {\n case \"textarea\":\n return (\n <Textarea\n inputSize=\"sm\"\n value={(values[field.id] as string) ?? \"\"}\n onChange={(e) => onChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n className=\"resize-none\"\n />\n );\n\n case \"select\":\n return (\n <Select\n value={(values[field.id] as string) ?? \"\"}\n onValueChange={(v) => onChange(field.id, v)}\n >\n <SelectTrigger style={{ height: \"var(--input-standard-height)\" }}>\n <SelectValue placeholder={field.placeholder ?? \"Select\"} />\n </SelectTrigger>\n <SelectContent>\n {field.options?.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n );\n\n case \"date\":\n return (\n <DateInput\n value={(values[field.id] as string) ?? \"\"}\n onChange={(v) => onChange(field.id, v)}\n />\n );\n\n case \"multiselect-tags\":\n return (\n <MultiselectTags\n tags={(values[field.id] as string[]) ?? (field.value as string[]) ?? []}\n placeholder={field.placeholder ?? \"Add...\"}\n onAdd={(tag: string) => {\n const current = (values[field.id] as string[]) ?? (field.value as string[]) ?? [];\n onChange(field.id, [...current, tag]);\n }}\n onRemove={(tag: string) => {\n const current = (values[field.id] as string[]) ?? (field.value as string[]) ?? [];\n onChange(field.id, current.filter((t: string) => t !== tag));\n }}\n />\n );\n\n default:\n return (\n <Input\n type=\"text\"\n value={(values[field.id] as string) ?? \"\"}\n onChange={(e) => onChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n />\n );\n }\n}\n\n/** Billing plan radio card */\nfunction BillingPlanCard({\n plan,\n selected,\n onClick,\n}: {\n plan: BillingPlan;\n selected: boolean;\n onClick: () => void;\n}) {\n return (\n <button\n className=\"flex items-start w-full text-left cursor-pointer\"\n onClick={onClick}\n style={{\n padding: \"var(--spacing-xl)\",\n borderRadius: \"var(--radius-md)\",\n border: selected\n ? \"2px solid var(--canvas-primary)\"\n : \"1px solid var(--canvas-border)\",\n backgroundColor: selected\n ? \"color-mix(in srgb, var(--canvas-primary) 4%, var(--canvas-background))\"\n : \"var(--canvas-background)\",\n gap: \"var(--spacing-lg)\",\n }}\n >\n {/* Radio dot */}\n <div\n className=\"shrink-0 flex items-center justify-center\"\n style={{\n width: 20,\n height: 20,\n borderRadius: \"50%\",\n marginTop: 2,\n border: selected\n ? \"2px solid var(--canvas-primary)\"\n : \"2px solid var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {selected && (\n <div\n style={{\n width: 10,\n height: 10,\n borderRadius: \"50%\",\n backgroundColor: \"var(--canvas-primary)\",\n }}\n />\n )}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <span\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.name}\n </span>\n {plan.badge && (\n <span\n className=\"shrink-0\"\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-xs-size)\",\n fontWeight: 600,\n lineHeight: \"16px\",\n padding: \"2px 8px\",\n borderRadius: \"var(--radius-sm)\",\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n {plan.badge}\n </span>\n )}\n </div>\n <p\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"20px\",\n color: \"var(--canvas-text-placeholder)\",\n margin: 0,\n }}\n >\n {plan.description}\n </p>\n </div>\n\n {/* Price */}\n <span\n className=\"shrink-0\"\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.price}\n </span>\n </button>\n );\n}\n\n/** Back / Continue navigation buttons */\nfunction StepNav({\n onBack,\n onNext,\n nextLabel = \"Continue\",\n showBack = true,\n}: {\n onBack?: () => void;\n onNext: () => void;\n nextLabel?: string;\n showBack?: boolean;\n}) {\n return (\n <div\n className=\"flex items-center justify-end\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {showBack && (\n <Button variant=\"neutral\" onClick={onBack}>\n Back\n </Button>\n )}\n <Button variant=\"primary\" onClick={onNext}>\n {nextLabel}\n </Button>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// MultistepFormPopup\n// ---------------------------------------------------------------------------\n\nexport function MultistepFormPopup({\n open,\n onOpenChange,\n steps = DEFAULT_STEPS,\n billingPlans = DEFAULT_BILLING_PLANS,\n loadingTitle = \"Please wait...\",\n loadingDescription = \"We are currently processing your submission\",\n successTitle = \"Success!\",\n successDescription = \"Your account has been set up successfully\",\n successButtonText = \"Go to portal\",\n onComplete,\n onSuccessButtonClick,\n className,\n}: MultistepFormPopupProps) {\n const [currentStep, setCurrentStep] = useState(0);\n const [phase, setPhase] = useState<\"form\" | \"loading\" | \"success\">(\"form\");\n const [values, setValues] = useState<Record<string, unknown>>({});\n const [selectedPlan, setSelectedPlan] = useState(\n billingPlans[0]?.id ?? \"\"\n );\n\n // Reset state when dialog closes\n useEffect(() => {\n if (!open) {\n setCurrentStep(0);\n setPhase(\"form\");\n setValues({});\n setSelectedPlan(billingPlans[0]?.id ?? \"\");\n }\n }, [open, billingPlans]);\n\n const totalFormSteps = steps.length;\n const totalStepsWithBilling = totalFormSteps + 1; // +1 for billing step\n\n const handleFieldChange = (id: string, value: unknown) => {\n setValues((prev) => ({ ...prev, [id]: value }));\n };\n\n const goBack = () => {\n if (currentStep > 0) {\n setCurrentStep((s) => s - 1);\n }\n };\n\n const goNext = () => {\n if (currentStep < totalStepsWithBilling - 1) {\n setCurrentStep((s) => s + 1);\n } else {\n // Final step — trigger loading → success\n setPhase(\"loading\");\n onComplete?.();\n setTimeout(() => {\n setPhase(\"success\");\n }, 2000);\n }\n };\n\n const isBillingStep = currentStep === totalFormSteps;\n const stepConfig = !isBillingStep ? steps[currentStep] : null;\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[640px]\",\n className\n )}\n showCloseButton={phase === \"form\"}\n >\n {phase === \"form\" && (\n <div\n className=\"flex flex-col overflow-y-auto\"\n style={{\n padding: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n maxHeight: \"85vh\",\n }}\n >\n {/* Step Header */}\n <StepHeader\n step={currentStep + 1}\n title={\n isBillingStep\n ? \"Select a billing plan for your organization\"\n : stepConfig?.title ?? \"\"\n }\n description={\n isBillingStep ? undefined : stepConfig?.description\n }\n />\n\n {/* Step Content */}\n {isBillingStep ? (\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n {billingPlans.map((plan) => (\n <BillingPlanCard\n key={plan.id}\n plan={plan}\n selected={selectedPlan === plan.id}\n onClick={() => setSelectedPlan(plan.id)}\n />\n ))}\n </div>\n ) : (\n <StepFormFields\n fields={stepConfig?.fields ?? []}\n values={values}\n onChange={handleFieldChange}\n />\n )}\n\n {/* Navigation */}\n <StepNav\n onBack={goBack}\n onNext={goNext}\n showBack={currentStep > 0}\n nextLabel={\n isBillingStep ? \"Complete\" : \"Continue\"\n }\n />\n </div>\n )}\n\n {phase === \"loading\" && (\n <div\n className=\"flex items-center justify-center\"\n style={{\n padding: \"var(--spacing-4xl)\",\n minHeight: 320,\n }}\n >\n <Loader\n state=\"loading\"\n title={loadingTitle}\n description={loadingDescription}\n />\n </div>\n )}\n\n {phase === \"success\" && (\n <div\n className=\"flex items-center justify-center\"\n style={{\n padding: \"var(--spacing-4xl)\",\n minHeight: 320,\n }}\n >\n <Loader\n state=\"success\"\n title={successTitle}\n description={successDescription}\n buttonText={successButtonText}\n onButtonClick={() => {\n onSuccessButtonClick?.();\n onOpenChange?.(false);\n }}\n />\n </div>\n )}\n </DialogContent>\n </Dialog>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Dialog, DialogContent } from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { Label } from \"../ui/label\";\nimport { Input } from \"../ui/input\";\nimport { Textarea } from \"../ui/textarea\";\nimport { DateInput } from \"../ui/date-input\";\nimport {\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n} from \"../ui/select\";\nimport { MultiselectTags } from \"../ui/multiselect-tags\";\nimport { Loader } from \"./loader\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MultistepField {\n /** Unique field identifier */\n id: string;\n /** Field label */\n label: string;\n /** Field input type */\n type:\n | \"text\"\n | \"select\"\n | \"date\"\n | \"textarea\"\n | \"multiselect-tags\";\n /** Placeholder text */\n placeholder?: string;\n /** When true, the field takes 50% width and pairs with the next half field */\n half?: boolean;\n /** Options for select or multiselect-tags */\n options?: { id: string; label: string }[];\n /** Default value */\n value?: string | string[];\n}\n\nexport interface MultistepFormStep {\n /** Step title */\n title: string;\n /** Step description shown below the title */\n description?: string;\n /** Form fields for this step */\n fields: MultistepField[];\n}\n\nexport interface BillingPlan {\n /** Unique plan identifier */\n id: string;\n /** Plan name */\n name: string;\n /** Price display text */\n price: string;\n /** Description text */\n description: string;\n /** Optional badge text */\n badge?: string;\n}\n\nexport interface MultistepFormPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Step configurations — each has title, description, and fields */\n steps?: MultistepFormStep[];\n /** Billing plan options for the final step */\n billingPlans?: BillingPlan[];\n /** Loading state title */\n loadingTitle?: string;\n /** Loading state description */\n loadingDescription?: string;\n /** Success state title */\n successTitle?: string;\n /** Success state description */\n successDescription?: string;\n /** Success action button text */\n successButtonText?: string;\n /** Callback when the form is completed (after all steps) */\n onComplete?: () => void;\n /** Callback when the success action button is clicked */\n onSuccessButtonClick?: () => void;\n /** Additional class names for the dialog content */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_STEPS: MultistepFormStep[] = [\n {\n title: \"Enter your personal information to get started\",\n fields: [\n { id: \"email\", label: \"Email\", type: \"text\", placeholder: \"jane@example.com\" },\n { id: \"company\", label: \"Company name\", type: \"text\", placeholder: \"Acme Inc.\" },\n { id: \"firstName\", label: \"First name\", type: \"text\", placeholder: \"Jane\", half: true },\n { id: \"lastName\", label: \"Last name\", type: \"text\", placeholder: \"Doe\", half: true },\n { id: \"city\", label: \"City\", type: \"text\", placeholder: \"San Francisco\" },\n {\n id: \"country\",\n label: \"Country\",\n type: \"select\",\n half: true,\n options: [\n { id: \"us\", label: \"United States\" },\n { id: \"ca\", label: \"Canada\" },\n { id: \"uk\", label: \"United Kingdom\" },\n { id: \"au\", label: \"Australia\" },\n ],\n },\n {\n id: \"state\",\n label: \"State\",\n type: \"select\",\n half: true,\n options: [\n { id: \"ca\", label: \"California\" },\n { id: \"ny\", label: \"New York\" },\n { id: \"tx\", label: \"Texas\" },\n { id: \"wa\", label: \"Washington\" },\n ],\n },\n { id: \"zip\", label: \"Zip / Postal code\", type: \"text\", placeholder: \"94102\" },\n { id: \"profession\", label: \"Profession\", type: \"text\", placeholder: \"Software Engineer\", half: true },\n { id: \"startDate\", label: \"Start date\", type: \"date\", half: true },\n ],\n },\n {\n title: \"Enter additional information below\",\n fields: [\n { id: \"address\", label: \"Address\", type: \"text\", placeholder: \"123 Main St\" },\n { id: \"stateProvince\", label: \"State / Province\", type: \"text\", placeholder: \"California\" },\n {\n id: \"occupation\",\n label: \"Occupation\",\n type: \"select\",\n half: true,\n options: [\n { id: \"eng\", label: \"Engineering\" },\n { id: \"design\", label: \"Design\" },\n { id: \"marketing\", label: \"Marketing\" },\n { id: \"sales\", label: \"Sales\" },\n ],\n },\n {\n id: \"status\",\n label: \"Status\",\n type: \"select\",\n half: true,\n options: [\n { id: \"active\", label: \"Active\" },\n { id: \"inactive\", label: \"Inactive\" },\n { id: \"pending\", label: \"Pending\" },\n ],\n },\n { id: \"phone\", label: \"Phone\", type: \"text\", placeholder: \"(555) 123-4567\", half: true },\n { id: \"fax\", label: \"Fax\", type: \"text\", placeholder: \"(555) 987-6543\", half: true },\n { id: \"step2StartDate\", label: \"Start date\", type: \"date\", half: true },\n { id: \"step2EndDate\", label: \"End date\", type: \"date\", half: true },\n { id: \"address2\", label: \"Address line 2\", type: \"text\", placeholder: \"Suite 200\" },\n {\n id: \"tags\",\n label: \"Tags\",\n type: \"multiselect-tags\",\n options: [\n { id: \"finance\", label: \"Finance\" },\n { id: \"technology\", label: \"Technology\" },\n { id: \"healthcare\", label: \"Healthcare\" },\n { id: \"education\", label: \"Education\" },\n { id: \"retail\", label: \"Retail\" },\n ],\n value: [],\n },\n ],\n },\n {\n title: \"Complete your account setup\",\n fields: [\n { id: \"step3Phone\", label: \"Phone\", type: \"text\", placeholder: \"(555) 123-4567\", half: true },\n { id: \"extension\", label: \"Extension\", type: \"text\", placeholder: \"1234\", half: true },\n { id: \"step3StartDate\", label: \"Start date\", type: \"date\", half: true },\n { id: \"step3EndDate\", label: \"End date\", type: \"date\", half: true },\n { id: \"copyrightYear\", label: \"Copyright year\", type: \"text\", placeholder: \"2026\", half: true },\n { id: \"licenseType\", label: \"License type\", type: \"text\", placeholder: \"Standard\", half: true },\n { id: \"notes\", label: \"Notes\", type: \"textarea\", placeholder: \"Add any additional notes here...\" },\n ],\n },\n];\n\nconst DEFAULT_BILLING_PLANS: BillingPlan[] = [\n {\n id: \"annual\",\n name: \"Annual billing\",\n price: \"$120/month\",\n description: \"Billed annually\",\n badge: \"Save 5%!\",\n },\n {\n id: \"monthly\",\n name: \"Monthly billing\",\n price: \"$150/month\",\n description: \"after your 30-day free trial\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// Inline Sub-Components\n// ---------------------------------------------------------------------------\n\nconst fontBase = \"var(--typo-global-font)\";\n\n/** Step indicator badge + title + description */\nfunction StepHeader({\n step,\n title,\n description,\n}: {\n step: number;\n title: string;\n description?: string;\n}) {\n return (\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-xl)\" }}>\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: 40,\n height: 40,\n borderRadius: \"var(--spacing-3xl)\",\n backgroundColor: \"var(--canvas-neutral-surface)\",\n }}\n >\n <span\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"24px\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {step}\n </span>\n </div>\n <h2\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"30px\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n {description && (\n <p\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"20px\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n paddingLeft: 56,\n }}\n >\n {description}\n </p>\n )}\n </div>\n );\n}\n\n/** Renders form fields with half-width pairing */\nfunction StepFormFields({\n fields,\n values,\n onChange,\n}: {\n fields: MultistepField[];\n values: Record<string, unknown>;\n onChange: (id: string, value: unknown) => void;\n}) {\n // Group fields into rows: half-width fields paired together\n const rows: MultistepField[][] = [];\n let i = 0;\n while (i < fields.length) {\n const field = fields[i];\n if (field.half && i + 1 < fields.length && fields[i + 1].half) {\n rows.push([field, fields[i + 1]]);\n i += 2;\n } else {\n rows.push([field]);\n i += 1;\n }\n }\n\n return (\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-3xl)\" }}>\n {rows.map((row, rowIdx) => (\n <div\n key={rowIdx}\n className={cn(\n \"flex gap-[var(--spacing-3xl)]\",\n row.length > 1 ? \"flex-col md:flex-row\" : \"flex-col\"\n )}\n >\n {row.map((field) => (\n <div\n key={field.id}\n className={cn(\n \"flex flex-col\",\n row.length > 1 ? \"flex-1\" : \"w-full\"\n )}\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <Label>{field.label}</Label>\n {renderField(field, values, onChange)}\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n}\n\nfunction renderField(\n field: MultistepField,\n values: Record<string, unknown>,\n onChange: (id: string, value: unknown) => void\n) {\n switch (field.type) {\n case \"textarea\":\n return (\n <Textarea\n inputSize=\"sm\"\n value={(values[field.id] as string) ?? \"\"}\n onChange={(e) => onChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n className=\"resize-none\"\n />\n );\n\n case \"select\":\n return (\n <Select\n value={(values[field.id] as string) ?? \"\"}\n onValueChange={(v) => onChange(field.id, v)}\n >\n <SelectTrigger style={{ height: \"var(--input-standard-height)\" }}>\n <SelectValue placeholder={field.placeholder ?? \"Select\"} />\n </SelectTrigger>\n <SelectContent>\n {field.options?.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n );\n\n case \"date\":\n return (\n <DateInput\n value={(values[field.id] as string) ?? \"\"}\n onChange={(v) => onChange(field.id, v)}\n />\n );\n\n case \"multiselect-tags\":\n return (\n <MultiselectTags\n tags={(values[field.id] as string[]) ?? (field.value as string[]) ?? []}\n placeholder={field.placeholder ?? \"Add...\"}\n onAdd={(tag: string) => {\n const current = (values[field.id] as string[]) ?? (field.value as string[]) ?? [];\n onChange(field.id, [...current, tag]);\n }}\n onRemove={(tag: string) => {\n const current = (values[field.id] as string[]) ?? (field.value as string[]) ?? [];\n onChange(field.id, current.filter((t: string) => t !== tag));\n }}\n />\n );\n\n default:\n return (\n <Input\n type=\"text\"\n value={(values[field.id] as string) ?? \"\"}\n onChange={(e) => onChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n />\n );\n }\n}\n\n/** Billing plan radio card */\nfunction BillingPlanCard({\n plan,\n selected,\n onClick,\n}: {\n plan: BillingPlan;\n selected: boolean;\n onClick: () => void;\n}) {\n return (\n <button\n className=\"flex items-start w-full text-left cursor-pointer\"\n onClick={onClick}\n style={{\n padding: \"var(--spacing-xl)\",\n borderRadius: \"var(--radius-md)\",\n border: selected\n ? \"2px solid var(--canvas-primary)\"\n : \"1px solid var(--canvas-border)\",\n backgroundColor: selected\n ? \"color-mix(in srgb, var(--canvas-primary) 4%, var(--canvas-background))\"\n : \"var(--canvas-background)\",\n gap: \"var(--spacing-lg)\",\n }}\n >\n {/* Radio dot */}\n <div\n className=\"shrink-0 flex items-center justify-center\"\n style={{\n width: 20,\n height: 20,\n borderRadius: \"50%\",\n marginTop: 2,\n border: selected\n ? \"2px solid var(--canvas-primary)\"\n : \"2px solid var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {selected && (\n <div\n style={{\n width: 10,\n height: 10,\n borderRadius: \"50%\",\n backgroundColor: \"var(--canvas-primary)\",\n }}\n />\n )}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <span\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.name}\n </span>\n {plan.badge && (\n <span\n className=\"shrink-0\"\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-xs-size)\",\n fontWeight: 600,\n lineHeight: \"16px\",\n padding: \"2px 8px\",\n borderRadius: \"var(--radius-sm)\",\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n {plan.badge}\n </span>\n )}\n </div>\n <p\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"20px\",\n color: \"var(--canvas-text-placeholder)\",\n margin: 0,\n }}\n >\n {plan.description}\n </p>\n </div>\n\n {/* Price */}\n <span\n className=\"shrink-0\"\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.price}\n </span>\n </button>\n );\n}\n\n/** Back / Continue navigation buttons */\nfunction StepNav({\n onBack,\n onNext,\n nextLabel = \"Continue\",\n showBack = true,\n}: {\n onBack?: () => void;\n onNext: () => void;\n nextLabel?: string;\n showBack?: boolean;\n}) {\n return (\n <div\n className=\"flex items-center justify-end\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {showBack && (\n <Button variant=\"neutral\" onClick={onBack}>\n Back\n </Button>\n )}\n <Button variant=\"primary\" onClick={onNext}>\n {nextLabel}\n </Button>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// MultistepFormPopup\n// ---------------------------------------------------------------------------\n\nexport function MultistepFormPopup({\n open,\n onOpenChange,\n steps = DEFAULT_STEPS,\n billingPlans = DEFAULT_BILLING_PLANS,\n loadingTitle = \"Please wait...\",\n loadingDescription = \"We are currently processing your submission\",\n successTitle = \"Success!\",\n successDescription = \"Your account has been set up successfully\",\n successButtonText = \"Go to portal\",\n onComplete,\n onSuccessButtonClick,\n className,\n}: MultistepFormPopupProps) {\n const [currentStep, setCurrentStep] = useState(0);\n const [phase, setPhase] = useState<\"form\" | \"loading\" | \"success\">(\"form\");\n const [values, setValues] = useState<Record<string, unknown>>({});\n const [selectedPlan, setSelectedPlan] = useState(\n billingPlans[0]?.id ?? \"\"\n );\n\n // Reset state when dialog closes\n useEffect(() => {\n if (!open) {\n setCurrentStep(0);\n setPhase(\"form\");\n setValues({});\n setSelectedPlan(billingPlans[0]?.id ?? \"\");\n }\n }, [open, billingPlans]);\n\n const totalFormSteps = steps.length;\n const totalStepsWithBilling = totalFormSteps + 1; // +1 for billing step\n\n const handleFieldChange = (id: string, value: unknown) => {\n setValues((prev) => ({ ...prev, [id]: value }));\n };\n\n const goBack = () => {\n if (currentStep > 0) {\n setCurrentStep((s) => s - 1);\n }\n };\n\n const goNext = () => {\n if (currentStep < totalStepsWithBilling - 1) {\n setCurrentStep((s) => s + 1);\n } else {\n // Final step — trigger loading → success\n setPhase(\"loading\");\n onComplete?.();\n setTimeout(() => {\n setPhase(\"success\");\n }, 2000);\n }\n };\n\n const isBillingStep = currentStep === totalFormSteps;\n const stepConfig = !isBillingStep ? steps[currentStep] : null;\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[640px]\",\n className\n )}\n showCloseButton={phase === \"form\"}\n >\n {phase === \"form\" && (\n <div\n className=\"flex flex-col overflow-y-auto\"\n style={{\n padding: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n maxHeight: \"85vh\",\n }}\n >\n {/* Step Header */}\n <StepHeader\n step={currentStep + 1}\n title={\n isBillingStep\n ? \"Select a billing plan for your organization\"\n : stepConfig?.title ?? \"\"\n }\n description={\n isBillingStep ? undefined : stepConfig?.description\n }\n />\n\n {/* Step Content */}\n {isBillingStep ? (\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n {billingPlans.map((plan) => (\n <BillingPlanCard\n key={plan.id}\n plan={plan}\n selected={selectedPlan === plan.id}\n onClick={() => setSelectedPlan(plan.id)}\n />\n ))}\n </div>\n ) : (\n <StepFormFields\n fields={stepConfig?.fields ?? []}\n values={values}\n onChange={handleFieldChange}\n />\n )}\n\n {/* Navigation */}\n <StepNav\n onBack={goBack}\n onNext={goNext}\n showBack={currentStep > 0}\n nextLabel={\n isBillingStep ? \"Complete\" : \"Continue\"\n }\n />\n </div>\n )}\n\n {phase === \"loading\" && (\n <div\n className=\"flex items-center justify-center\"\n style={{\n padding: \"var(--spacing-4xl)\",\n minHeight: 320,\n }}\n >\n <Loader\n state=\"loading\"\n title={loadingTitle}\n description={loadingDescription}\n />\n </div>\n )}\n\n {phase === \"success\" && (\n <div\n className=\"flex items-center justify-center\"\n style={{\n padding: \"var(--spacing-4xl)\",\n minHeight: 320,\n }}\n >\n <Loader\n state=\"success\"\n title={successTitle}\n description={successDescription}\n buttonText={successButtonText}\n onButtonClick={() => {\n onSuccessButtonClick?.();\n onOpenChange?.(false);\n }}\n />\n </div>\n )}\n </DialogContent>\n </Dialog>\n );\n}\n"
19
19
  }
20
20
  ],
21
21
  "dependencies": [],
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/blocks/nps-survey-popup.tsx",
17
17
  "type": "registry:block",
18
- "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface NpsSurveyPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text */\n description?: string;\n /** Currently selected score (controlled) */\n value?: number | null;\n /** Callback when submit is clicked — receives the selected score */\n onSubmit?: (score: number) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Label for the low end of the scale */\n minLabel?: string;\n /** Label for the high end of the scale */\n maxLabel?: string;\n /** Minimum score value */\n min?: number;\n /** Maximum score value */\n max?: number;\n /** Cancel button label */\n cancelLabel?: string;\n /** Submit button label */\n submitLabel?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Your input is valuable to us\";\nconst DEFAULT_DESCRIPTION =\n \"How likely are you to recommend Sample App to a friend or colleague?\";\n\n// ---------------------------------------------------------------------------\n// NpsSurveyPopup\n// ---------------------------------------------------------------------------\n\nexport function NpsSurveyPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n value: controlledValue,\n onSubmit,\n onCancel,\n minLabel = \"Not likely\",\n maxLabel = \"Very likely\",\n min = 1,\n max = 10,\n cancelLabel = \"Cancel\",\n submitLabel = \"Submit\",\n className,\n}: NpsSurveyPopupProps) {\n const [selected, setSelected] = useState<number | null>(controlledValue ?? null);\n\n // Reset when dialog opens\n useEffect(() => {\n if (open) {\n setSelected(controlledValue ?? null);\n }\n }, [open, controlledValue]);\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSubmit = () => {\n if (selected !== null) {\n onSubmit?.(selected);\n onOpenChange?.(false);\n }\n };\n\n const scores = Array.from({ length: max - min + 1 }, (_, i) => min + i);\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Description */}\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n\n {/* Score buttons */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <div\n className=\"flex flex-wrap justify-center\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {scores.map((score) => {\n const isSelected = selected === score;\n return (\n <button\n key={score}\n type=\"button\"\n onClick={() => setSelected(score)}\n className={cn(\n \"flex items-center justify-center border rounded-[var(--radius-xs)] transition-colors cursor-pointer\",\n isSelected\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] border-[var(--canvas-primary)]\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text)] border-[var(--canvas-border)] hover:bg-[var(--canvas-surface-hover)]\"\n )}\n style={{\n width: \"var(--spacing-5xl)\",\n height: \"var(--spacing-5xl)\",\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n }}\n >\n {score}\n </button>\n );\n })}\n </div>\n\n {/* End labels */}\n <div\n className=\"flex justify-between w-full\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n <span>{minLabel}</span>\n <span>{maxLabel}</span>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] justify-end\">\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleSubmit}\n disabled={selected === null}\n >\n {submitLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface NpsSurveyPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text */\n description?: string;\n /** Currently selected score (controlled) */\n value?: number | null;\n /** Callback when submit is clicked — receives the selected score */\n onSubmit?: (score: number) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Label for the low end of the scale */\n minLabel?: string;\n /** Label for the high end of the scale */\n maxLabel?: string;\n /** Minimum score value */\n min?: number;\n /** Maximum score value */\n max?: number;\n /** Cancel button label */\n cancelLabel?: string;\n /** Submit button label */\n submitLabel?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Your input is valuable to us\";\nconst DEFAULT_DESCRIPTION =\n \"How likely are you to recommend Sample App to a friend or colleague?\";\n\n// ---------------------------------------------------------------------------\n// NpsSurveyPopup\n// ---------------------------------------------------------------------------\n\nexport function NpsSurveyPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n value: controlledValue,\n onSubmit,\n onCancel,\n minLabel = \"Not likely\",\n maxLabel = \"Very likely\",\n min = 1,\n max = 10,\n cancelLabel = \"Cancel\",\n submitLabel = \"Submit\",\n className,\n}: NpsSurveyPopupProps) {\n const [selected, setSelected] = useState<number | null>(controlledValue ?? null);\n\n // Reset when dialog opens\n useEffect(() => {\n if (open) {\n setSelected(controlledValue ?? null);\n }\n }, [open, controlledValue]);\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSubmit = () => {\n if (selected !== null) {\n onSubmit?.(selected);\n onOpenChange?.(false);\n }\n };\n\n const scores = Array.from({ length: max - min + 1 }, (_, i) => min + i);\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Description */}\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n\n {/* Score buttons */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <div\n className=\"flex flex-wrap justify-center\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {scores.map((score) => {\n const isSelected = selected === score;\n return (\n <button\n key={score}\n type=\"button\"\n onClick={() => setSelected(score)}\n className={cn(\n \"flex items-center justify-center border rounded-[var(--radius-xs)] transition-colors cursor-pointer\",\n isSelected\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] border-[var(--canvas-primary)]\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text)] border-[var(--canvas-border)] hover:bg-[var(--canvas-surface-hover)]\"\n )}\n style={{\n width: \"var(--spacing-5xl)\",\n height: \"var(--spacing-5xl)\",\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n }}\n >\n {score}\n </button>\n );\n })}\n </div>\n\n {/* End labels */}\n <div\n className=\"flex justify-between w-full\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n <span>{minLabel}</span>\n <span>{maxLabel}</span>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] justify-end\">\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleSubmit}\n disabled={selected === null}\n >\n {submitLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
19
19
  }
20
20
  ],
21
21
  "dependencies": [],
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/page-previews.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { Header } from \"../layout\";\nimport { ContentDropzone } from \"./content-dropzone\";\nimport { LoginBrandingPanel } from \"./login-branding-panel\";\nimport { MessengerSidebar } from \"./messenger-sidebar\";\nimport { VideoChatControls } from \"./video-chat-controls\";\nimport {\n DashboardShell,\n IconSidebarShell,\n DoubleSidebarShell,\n StandardPageShell,\n MobileMenuShell,\n SearchBarShell,\n MultistepShell,\n MultistepSidebarShell,\n MultistepProgressBarShell,\n VerticalMultistepShell,\n AccountSettingsShell,\n} from \"../layout\";\n\n/**\n * Scaled Preview Wrapper\n * Renders content at a scaled-down size for canvas thumbnails\n */\ninterface ScaledPreviewProps {\n children: React.ReactNode;\n width?: number;\n height?: number;\n scale?: number;\n}\n\nexport function ScaledPreview({\n children,\n width = 400,\n height = 280,\n scale = 0.25\n}: ScaledPreviewProps) {\n const innerWidth = width / scale;\n const innerHeight = height / scale;\n\n return (\n <div\n className=\"overflow-hidden rounded-lg border border-[var(--canvas-border)] shadow-md bg-[var(--canvas-background)]\"\n style={{ width, height }}\n >\n <div\n style={{\n width: innerWidth,\n height: innerHeight,\n transform: `scale(${scale})`,\n transformOrigin: \"top left\",\n }}\n >\n {children}\n </div>\n </div>\n );\n}\n\n// Sample navigation for previews\nconst previewNav = [\n { id: \"home\", label: \"Home\", href: \"#\" },\n { id: \"about\", label: \"About\", href: \"#\" },\n];\n\nconst sampleSidebarSections = [\n {\n items: [\n { id: \"dashboard\", label: \"Dashboard\", icon: \"home\" as const, href: \"#\" },\n { id: \"analytics\", label: \"Analytics\", icon: \"chart\" as const, href: \"#\" },\n { id: \"settings\", label: \"Settings\", icon: \"settings\" as const, href: \"#\" },\n ],\n },\n];\n\n// =====================\n// PAGE TEMPLATE PREVIEWS\n// =====================\n\nexport function PageAboutPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <ContentDropzone />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageAccountPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <AccountSettingsShell>\n <ContentDropzone />\n </AccountSettingsShell>\n </ScaledPreview>\n );\n}\n\nexport function PageAdminPortalPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <ContentDropzone />\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageCenteredProfilePreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <StandardPageShell showBanner={false} showPageHeader={false}>\n <ContentDropzone />\n </StandardPageShell>\n </ScaledPreview>\n );\n}\n\nexport function PageDoubleSidebarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DoubleSidebarShell>\n <ContentDropzone />\n </DoubleSidebarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageIconSidebarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <IconSidebarShell>\n <ContentDropzone />\n </IconSidebarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageLoginPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen\">\n <div className=\"flex-1 flex items-center justify-center p-8\">\n <ContentDropzone height=\"320px\" className=\"w-[360px]\" />\n </div>\n <LoginBrandingPanel className=\"hidden lg:flex w-1/2\" />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageMenuSectionsPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <ContentDropzone />\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageMessengerPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} className=\"absolute top-0 left-0 right-0\" />\n <div className=\"flex flex-1 pt-16\">\n <MessengerSidebar className=\"w-[320px]\" />\n <div className=\"flex-1 flex flex-col\">\n <ContentDropzone />\n </div>\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageMobileMenuPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <MobileMenuShell>\n <ContentDropzone />\n </MobileMenuShell>\n </ScaledPreview>\n );\n}\n\nexport function PageMultistepProgressbarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <MultistepProgressBarShell>\n <ContentDropzone />\n </MultistepProgressBarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageMultistepSidebarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <MultistepSidebarShell>\n <ContentDropzone />\n </MultistepSidebarShell>\n </ScaledPreview>\n );\n}\n\nexport function PagePricingPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <ContentDropzone />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageProductHomepagePreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <ContentDropzone />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageResetPasswordPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen\">\n <div className=\"flex-1 flex items-center justify-center p-8\">\n <ContentDropzone height=\"320px\" className=\"w-[360px]\" />\n </div>\n <LoginBrandingPanel className=\"hidden lg:flex w-1/2\" />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageSearchBarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <SearchBarShell>\n <ContentDropzone />\n </SearchBarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageSidebarProfilePreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <ContentDropzone />\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageStandardPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <StandardPageShell>\n <ContentDropzone />\n </StandardPageShell>\n </ScaledPreview>\n );\n}\n\nexport function PageStandardMultistepPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <MultistepShell>\n <ContentDropzone />\n </MultistepShell>\n </ScaledPreview>\n );\n}\n\nexport function PageStandardSearchPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <ContentDropzone />\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageVerticalMultistepPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <VerticalMultistepShell>\n <ContentDropzone />\n </VerticalMultistepShell>\n </ScaledPreview>\n );\n}\n\nexport function PageVideoChatPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"h-screen bg-[var(--canvas-sidebar-dark-bg)] flex flex-col\">\n <div className=\"flex-1 p-4\">\n <ContentDropzone />\n </div>\n <div className=\"p-4\">\n <VideoChatControls />\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageVideoListPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <ContentDropzone />\n </div>\n </ScaledPreview>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { Header } from \"../layout\";\nimport { ContentDropzone } from \"./content-dropzone\";\nimport { LoginBrandingPanel } from \"./login-branding-panel\";\nimport { MessengerSidebar } from \"./messenger-sidebar\";\nimport { VideoChatControls } from \"./video-chat-controls\";\nimport {\n DashboardShell,\n IconSidebarShell,\n DoubleSidebarShell,\n StandardPageShell,\n MobileMenuShell,\n SearchBarShell,\n MultistepShell,\n MultistepSidebarShell,\n MultistepProgressBarShell,\n VerticalMultistepShell,\n AccountSettingsShell,\n} from \"../layout\";\n\n/**\n * Scaled Preview Wrapper\n * Renders content at a scaled-down size for canvas thumbnails\n */\ninterface ScaledPreviewProps {\n children: React.ReactNode;\n width?: number;\n height?: number;\n scale?: number;\n}\n\nexport function ScaledPreview({\n children,\n width = 400,\n height = 280,\n scale = 0.25\n}: ScaledPreviewProps) {\n const innerWidth = width / scale;\n const innerHeight = height / scale;\n\n return (\n <div\n className=\"overflow-hidden rounded-lg border border-[var(--canvas-border)] shadow-[var(--canvas-shadow-card)] bg-[var(--canvas-background)]\"\n style={{ width, height }}\n >\n <div\n style={{\n width: innerWidth,\n height: innerHeight,\n transform: `scale(${scale})`,\n transformOrigin: \"top left\",\n }}\n >\n {children}\n </div>\n </div>\n );\n}\n\n// Sample navigation for previews\nconst previewNav = [\n { id: \"home\", label: \"Home\", href: \"#\" },\n { id: \"about\", label: \"About\", href: \"#\" },\n];\n\nconst sampleSidebarSections = [\n {\n items: [\n { id: \"dashboard\", label: \"Dashboard\", icon: \"home\" as const, href: \"#\" },\n { id: \"analytics\", label: \"Analytics\", icon: \"chart\" as const, href: \"#\" },\n { id: \"settings\", label: \"Settings\", icon: \"settings\" as const, href: \"#\" },\n ],\n },\n];\n\n// =====================\n// PAGE TEMPLATE PREVIEWS\n// =====================\n\nexport function PageAboutPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <ContentDropzone />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageAccountPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <AccountSettingsShell>\n <ContentDropzone />\n </AccountSettingsShell>\n </ScaledPreview>\n );\n}\n\nexport function PageAdminPortalPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <ContentDropzone />\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageCenteredProfilePreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <StandardPageShell showBanner={false} showPageHeader={false}>\n <ContentDropzone />\n </StandardPageShell>\n </ScaledPreview>\n );\n}\n\nexport function PageDoubleSidebarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DoubleSidebarShell>\n <ContentDropzone />\n </DoubleSidebarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageIconSidebarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <IconSidebarShell>\n <ContentDropzone />\n </IconSidebarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageLoginPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen\">\n <div className=\"flex-1 flex items-center justify-center p-8\">\n <ContentDropzone height=\"320px\" className=\"w-[360px]\" />\n </div>\n <LoginBrandingPanel className=\"hidden lg:flex w-1/2\" />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageMenuSectionsPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <ContentDropzone />\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageMessengerPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} className=\"absolute top-0 left-0 right-0\" />\n <div className=\"flex flex-1 pt-16\">\n <MessengerSidebar className=\"w-[320px]\" />\n <div className=\"flex-1 flex flex-col\">\n <ContentDropzone />\n </div>\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageMobileMenuPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <MobileMenuShell>\n <ContentDropzone />\n </MobileMenuShell>\n </ScaledPreview>\n );\n}\n\nexport function PageMultistepProgressbarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <MultistepProgressBarShell>\n <ContentDropzone />\n </MultistepProgressBarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageMultistepSidebarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <MultistepSidebarShell>\n <ContentDropzone />\n </MultistepSidebarShell>\n </ScaledPreview>\n );\n}\n\nexport function PagePricingPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <ContentDropzone />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageProductHomepagePreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <ContentDropzone />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageResetPasswordPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen\">\n <div className=\"flex-1 flex items-center justify-center p-8\">\n <ContentDropzone height=\"320px\" className=\"w-[360px]\" />\n </div>\n <LoginBrandingPanel className=\"hidden lg:flex w-1/2\" />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageSearchBarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <SearchBarShell>\n <ContentDropzone />\n </SearchBarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageSidebarProfilePreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <ContentDropzone />\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageStandardPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <StandardPageShell>\n <ContentDropzone />\n </StandardPageShell>\n </ScaledPreview>\n );\n}\n\nexport function PageStandardMultistepPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <MultistepShell>\n <ContentDropzone />\n </MultistepShell>\n </ScaledPreview>\n );\n}\n\nexport function PageStandardSearchPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <ContentDropzone />\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageVerticalMultistepPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <VerticalMultistepShell>\n <ContentDropzone />\n </VerticalMultistepShell>\n </ScaledPreview>\n );\n}\n\nexport function PageVideoChatPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"h-screen bg-[var(--canvas-sidebar-dark-bg)] flex flex-col\">\n <div className=\"flex-1 p-4\">\n <ContentDropzone />\n </div>\n <div className=\"p-4\">\n <VideoChatControls />\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageVideoListPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <ContentDropzone />\n </div>\n </ScaledPreview>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [],
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/blocks/persona-card.tsx",
17
17
  "type": "registry:block",
18
- "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport type { Persona } from \"../../types/project\";\nimport { Target, AlertCircle, Quote } from \"lucide-react\";\n\ninterface PersonaCardProps {\n persona: Persona;\n className?: string;\n}\n\nexport function PersonaCard({ persona, className }: PersonaCardProps) {\n return (\n <div\n className={cn(\n \"rounded-xl border border-[var(--canvas-border)] bg-[var(--canvas-background)]\",\n \"p-5 hover:shadow-md transition-shadow\",\n className\n )}\n >\n {/* Header */}\n <div className=\"flex items-start gap-3 mb-4\">\n <div className=\"text-3xl\">{persona.avatar || \"👤\"}</div>\n <div>\n <h3 className=\"font-semibold text-[var(--canvas-text)]\">\n {persona.name}\n </h3>\n <p className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {persona.role}\n </p>\n </div>\n </div>\n\n {/* Goals */}\n <div className=\"mb-4\">\n <div className=\"flex items-center gap-1.5 font-medium text-[var(--canvas-text-muted)] mb-2\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n <Target className=\"size-3\" />\n Goals\n </div>\n <ul className=\"space-y-1\">\n {persona.goals.map((goal, i) => (\n <li\n key={i}\n className=\"text-[var(--canvas-text)] flex items-start gap-2\" style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <span className=\"text-[var(--canvas-primary)] mt-1.5\">•</span>\n {goal}\n </li>\n ))}\n </ul>\n </div>\n\n {/* Pain Points */}\n <div className=\"mb-4\">\n <div className=\"flex items-center gap-1.5 font-medium text-[var(--canvas-text-muted)] mb-2\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n <AlertCircle className=\"size-3\" />\n Pain Points\n </div>\n <ul className=\"space-y-1\">\n {persona.painPoints.map((point, i) => (\n <li\n key={i}\n className=\"text-[var(--canvas-text)] flex items-start gap-2\" style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <span className=\"text-[var(--canvas-destructive)] mt-1.5\">•</span>\n {point}\n </li>\n ))}\n </ul>\n </div>\n\n {/* Quote */}\n <div className=\"border-t border-[var(--canvas-border)] pt-4 mt-4\">\n <div className=\"flex items-start gap-2\">\n <Quote className=\"size-4 text-[var(--canvas-text-muted)] shrink-0 mt-0.5\" />\n <p className=\"italic text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n \"{persona.quote}\"\n </p>\n </div>\n </div>\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// PERSONA GRID\n// ═══════════════════════════════════════════════════════════\n\ninterface PersonaGridProps {\n personas: Persona[];\n}\n\nexport function PersonaGrid({ personas }: PersonaGridProps) {\n if (personas.length === 0) {\n return null;\n }\n\n return (\n <div className=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n {personas.map((persona) => (\n <PersonaCard key={persona.id} persona={persona} />\n ))}\n </div>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport type { Persona } from \"../../types/project\";\nimport { Target, AlertCircle, Quote } from \"lucide-react\";\n\ninterface PersonaCardProps {\n persona: Persona;\n className?: string;\n}\n\nexport function PersonaCard({ persona, className }: PersonaCardProps) {\n return (\n <div\n className={cn(\n \"rounded-xl border border-[var(--canvas-border)] bg-[var(--canvas-background)]\",\n \"p-5 hover:shadow-[var(--canvas-shadow-card)] transition-shadow\",\n className\n )}\n >\n {/* Header */}\n <div className=\"flex items-start gap-3 mb-4\">\n <div className=\"text-3xl\">{persona.avatar || \"👤\"}</div>\n <div>\n <h3 className=\"font-semibold text-[var(--canvas-text)]\">\n {persona.name}\n </h3>\n <p className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {persona.role}\n </p>\n </div>\n </div>\n\n {/* Goals */}\n <div className=\"mb-4\">\n <div className=\"flex items-center gap-1.5 font-medium text-[var(--canvas-text-muted)] mb-2\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n <Target className=\"size-3\" />\n Goals\n </div>\n <ul className=\"space-y-1\">\n {persona.goals.map((goal, i) => (\n <li\n key={i}\n className=\"text-[var(--canvas-text)] flex items-start gap-2\" style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <span className=\"text-[var(--canvas-primary)] mt-1.5\">•</span>\n {goal}\n </li>\n ))}\n </ul>\n </div>\n\n {/* Pain Points */}\n <div className=\"mb-4\">\n <div className=\"flex items-center gap-1.5 font-medium text-[var(--canvas-text-muted)] mb-2\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n <AlertCircle className=\"size-3\" />\n Pain Points\n </div>\n <ul className=\"space-y-1\">\n {persona.painPoints.map((point, i) => (\n <li\n key={i}\n className=\"text-[var(--canvas-text)] flex items-start gap-2\" style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <span className=\"text-[var(--canvas-destructive)] mt-1.5\">•</span>\n {point}\n </li>\n ))}\n </ul>\n </div>\n\n {/* Quote */}\n <div className=\"border-t border-[var(--canvas-border)] pt-4 mt-4\">\n <div className=\"flex items-start gap-2\">\n <Quote className=\"size-4 text-[var(--canvas-text-muted)] shrink-0 mt-0.5\" />\n <p className=\"italic text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n \"{persona.quote}\"\n </p>\n </div>\n </div>\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// PERSONA GRID\n// ═══════════════════════════════════════════════════════════\n\ninterface PersonaGridProps {\n personas: Persona[];\n}\n\nexport function PersonaGrid({ personas }: PersonaGridProps) {\n if (personas.length === 0) {\n return null;\n }\n\n return (\n <div className=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n {personas.map((persona) => (\n <PersonaCard key={persona.id} persona={persona} />\n ))}\n </div>\n );\n}\n"
19
19
  }
20
20
  ],
21
21
  "dependencies": [
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/blocks/personalize-feed-popup.tsx",
17
17
  "type": "registry:block",
18
- "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FeedOption {\n /** Unique identifier */\n id: string;\n /** Display label */\n label: string;\n}\n\nexport interface PersonalizeFeedPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text */\n description?: string;\n /** Available topic options */\n options?: FeedOption[];\n /** IDs of initially selected options */\n defaultSelected?: string[];\n /** Callback when save is clicked — receives selected option IDs */\n onSave?: (selectedIds: string[]) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Cancel button label */\n cancelLabel?: string;\n /** Save button label */\n saveLabel?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Personalize your feed\";\nconst DEFAULT_DESCRIPTION =\n \"Select the topics that you are interested in below. You can change these preferences at any time from your account page.\";\n\nconst DEFAULT_OPTIONS: FeedOption[] = [\n { id: \"photography\", label: \"Photography\" },\n { id: \"film\", label: \"Film\" },\n { id: \"music\", label: \"Music\" },\n { id: \"ceramics\", label: \"Ceramics\" },\n { id: \"fitness\", label: \"Fitness\" },\n { id: \"nature\", label: \"Nature\" },\n { id: \"bars\", label: \"Bars\" },\n { id: \"restaurants\", label: \"Restaurants\" },\n];\n\nconst DEFAULT_SELECTED = [\"photography\", \"fitness\"];\n\n// ---------------------------------------------------------------------------\n// PersonalizeFeedPopup\n// ---------------------------------------------------------------------------\n\nexport function PersonalizeFeedPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n options = DEFAULT_OPTIONS,\n defaultSelected = DEFAULT_SELECTED,\n onSave,\n onCancel,\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n className,\n}: PersonalizeFeedPopupProps) {\n const [selectedIds, setSelectedIds] = useState<Set<string>>(\n new Set(defaultSelected)\n );\n\n // Reset selection when dialog opens\n useEffect(() => {\n if (open) {\n setSelectedIds(new Set(defaultSelected));\n }\n }, [open, defaultSelected]);\n\n const toggleOption = (id: string) => {\n setSelectedIds((prev) => {\n const next = new Set(prev);\n if (next.has(id)) {\n next.delete(id);\n } else {\n next.add(id);\n }\n return next;\n });\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.(Array.from(selectedIds));\n onOpenChange?.(false);\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[420px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Description */}\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n\n {/* Chip Grid */}\n <div\n className=\"flex flex-wrap\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {options.map((option) => {\n const isSelected = selectedIds.has(option.id);\n return (\n <button\n key={option.id}\n type=\"button\"\n onClick={() => toggleOption(option.id)}\n className={cn(\n \"rounded-full border transition-colors cursor-pointer\",\n isSelected\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] border-[var(--canvas-primary)]\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text)] border-[var(--canvas-border)] hover:bg-[var(--canvas-surface-hover)]\"\n )}\n style={{\n padding: \"var(--spacing-md) var(--spacing-xl)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {option.label}\n </button>\n );\n })}\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] justify-end\">\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button variant=\"primary\" onClick={handleSave}>\n {saveLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FeedOption {\n /** Unique identifier */\n id: string;\n /** Display label */\n label: string;\n}\n\nexport interface PersonalizeFeedPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text */\n description?: string;\n /** Available topic options */\n options?: FeedOption[];\n /** IDs of initially selected options */\n defaultSelected?: string[];\n /** Callback when save is clicked — receives selected option IDs */\n onSave?: (selectedIds: string[]) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Cancel button label */\n cancelLabel?: string;\n /** Save button label */\n saveLabel?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Personalize your feed\";\nconst DEFAULT_DESCRIPTION =\n \"Select the topics that you are interested in below. You can change these preferences at any time from your account page.\";\n\nconst DEFAULT_OPTIONS: FeedOption[] = [\n { id: \"photography\", label: \"Photography\" },\n { id: \"film\", label: \"Film\" },\n { id: \"music\", label: \"Music\" },\n { id: \"ceramics\", label: \"Ceramics\" },\n { id: \"fitness\", label: \"Fitness\" },\n { id: \"nature\", label: \"Nature\" },\n { id: \"bars\", label: \"Bars\" },\n { id: \"restaurants\", label: \"Restaurants\" },\n];\n\nconst DEFAULT_SELECTED = [\"photography\", \"fitness\"];\n\n// ---------------------------------------------------------------------------\n// PersonalizeFeedPopup\n// ---------------------------------------------------------------------------\n\nexport function PersonalizeFeedPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n options = DEFAULT_OPTIONS,\n defaultSelected = DEFAULT_SELECTED,\n onSave,\n onCancel,\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n className,\n}: PersonalizeFeedPopupProps) {\n const [selectedIds, setSelectedIds] = useState<Set<string>>(\n new Set(defaultSelected)\n );\n\n // Reset selection when dialog opens\n useEffect(() => {\n if (open) {\n setSelectedIds(new Set(defaultSelected));\n }\n }, [open, defaultSelected]);\n\n const toggleOption = (id: string) => {\n setSelectedIds((prev) => {\n const next = new Set(prev);\n if (next.has(id)) {\n next.delete(id);\n } else {\n next.add(id);\n }\n return next;\n });\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.(Array.from(selectedIds));\n onOpenChange?.(false);\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[420px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Description */}\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n\n {/* Chip Grid */}\n <div\n className=\"flex flex-wrap\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {options.map((option) => {\n const isSelected = selectedIds.has(option.id);\n return (\n <button\n key={option.id}\n type=\"button\"\n onClick={() => toggleOption(option.id)}\n className={cn(\n \"rounded-full border transition-colors cursor-pointer\",\n isSelected\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] border-[var(--canvas-primary)]\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text)] border-[var(--canvas-border)] hover:bg-[var(--canvas-surface-hover)]\"\n )}\n style={{\n padding: \"var(--spacing-md) var(--spacing-xl)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {option.label}\n </button>\n );\n })}\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] justify-end\">\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button variant=\"primary\" onClick={handleSave}>\n {saveLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
19
19
  }
20
20
  ],
21
21
  "dependencies": [],
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/blocks/pricing-plans-popup.tsx",
17
17
  "type": "registry:block",
18
- "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type BillingPeriod = \"monthly\" | \"annually\";\n\nexport interface PricingPlan {\n /** Unique plan identifier */\n id: string;\n /** Display name (e.g. \"Basic\", \"Professional\") */\n name: string;\n /** Monthly price in dollars */\n monthlyPrice: number;\n /** Annual price in dollars — defaults to monthlyPrice * 12 if omitted */\n annualPrice?: number;\n /** Short description of what the plan includes */\n description: string;\n}\n\nexport interface PricingPlansPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text below the title */\n description?: string;\n /** Array of plan options to display as radio cards */\n plans?: PricingPlan[];\n /** Currently selected billing period (controlled) */\n billingPeriod?: BillingPeriod;\n /** Default billing period when uncontrolled */\n defaultBillingPeriod?: BillingPeriod;\n /** Callback when billing period changes */\n onBillingPeriodChange?: (period: BillingPeriod) => void;\n /** Currently selected plan ID (controlled) */\n selectedPlanId?: string;\n /** Default selected plan ID when uncontrolled */\n defaultSelectedPlanId?: string;\n /** Callback when selected plan changes */\n onPlanChange?: (planId: string) => void;\n /** Save/confirm button label */\n confirmLabel?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Callback when save button is clicked — receives the selected plan ID and billing period */\n onConfirm?: (planId: string, billingPeriod: BillingPeriod) => void;\n /** Callback when cancel button is clicked */\n onCancel?: () => void;\n /** Disables the confirm button and shows a loading state */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Pricing plans\";\nconst DEFAULT_DESCRIPTION =\n \"Choose the plan that's right for your company. All plans will include a 30-day trial.\";\n\nconst DEFAULT_PLANS: PricingPlan[] = [\n {\n id: \"basic\",\n name: \"Basic\",\n monthlyPrice: 5,\n annualPrice: 50,\n description: \"For hobbyists\",\n },\n {\n id: \"professional\",\n name: \"Professional\",\n monthlyPrice: 10,\n annualPrice: 100,\n description: \"For teams up to 30 people\",\n },\n {\n id: \"enterprise\",\n name: \"Enterprise\",\n monthlyPrice: 30,\n annualPrice: 300,\n description: \"For large teams\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// PricingPlansPopup\n// ---------------------------------------------------------------------------\n\nexport function PricingPlansPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n plans = DEFAULT_PLANS,\n billingPeriod,\n defaultBillingPeriod = \"monthly\",\n onBillingPeriodChange,\n selectedPlanId,\n defaultSelectedPlanId,\n onPlanChange,\n confirmLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n loading = false,\n className,\n}: PricingPlansPopupProps) {\n // Controlled vs uncontrolled billing period\n const isBillingControlled = billingPeriod !== undefined;\n const [internalBilling, setInternalBilling] =\n useState<BillingPeriod>(defaultBillingPeriod);\n const currentBilling = isBillingControlled ? billingPeriod : internalBilling;\n\n // Controlled vs uncontrolled plan selection\n const isPlanControlled = selectedPlanId !== undefined;\n const [internalPlanId, setInternalPlanId] = useState<string | undefined>(\n defaultSelectedPlanId\n );\n const currentPlanId = isPlanControlled ? selectedPlanId : internalPlanId;\n\n // Reset internal state when dialog closes\n useEffect(() => {\n if (!open) {\n if (!isBillingControlled) setInternalBilling(defaultBillingPeriod);\n if (!isPlanControlled) setInternalPlanId(defaultSelectedPlanId);\n }\n }, [open, isBillingControlled, isPlanControlled, defaultBillingPeriod, defaultSelectedPlanId]);\n\n const handleBillingChange = (period: BillingPeriod) => {\n if (!isBillingControlled) {\n setInternalBilling(period);\n }\n onBillingPeriodChange?.(period);\n };\n\n const handlePlanSelect = (planId: string) => {\n if (!isPlanControlled) {\n setInternalPlanId(planId);\n }\n onPlanChange?.(planId);\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleConfirm = () => {\n if (currentPlanId) {\n onConfirm?.(currentPlanId, currentBilling);\n }\n };\n\n const getDisplayPrice = (plan: PricingPlan): number => {\n if (currentBilling === \"annually\") {\n return plan.annualPrice ?? plan.monthlyPrice * 12;\n }\n return plan.monthlyPrice;\n };\n\n const priceSuffix = currentBilling === \"annually\" ? \"/ year\" : \"/ month\";\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[480px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Description */}\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n\n {/* Billing Period Toggle */}\n <div\n className=\"flex w-full overflow-hidden self-center\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--spacing-xs)\",\n height: 40,\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n width: \"fit-content\",\n }}\n >\n {([\"monthly\", \"annually\"] as const).map((period, idx) => {\n const isActive = currentBilling === period;\n return (\n <button\n key={period}\n type=\"button\"\n onClick={() => handleBillingChange(period)}\n className=\"cursor-pointer\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"24px\",\n background: \"var(--canvas-background)\",\n border: \"none\",\n borderLeft:\n isActive\n ? \"2px solid var(--canvas-primary)\"\n : idx > 0\n ? \"1px solid var(--canvas-border)\"\n : \"none\",\n color: isActive\n ? \"var(--canvas-primary)\"\n : \"var(--canvas-text-placeholder)\",\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n {period === \"monthly\" ? \"Monthly\" : \"Annually\"}\n </button>\n );\n })}\n </div>\n\n {/* Plan Radio Cards */}\n <div className=\"flex w-full flex-col\" style={{ gap: \"var(--spacing-2xl)\" }}>\n {plans.map((plan) => {\n const isSelected = currentPlanId === plan.id;\n return (\n <button\n key={plan.id}\n type=\"button\"\n onClick={() => handlePlanSelect(plan.id)}\n className={cn(\n \"flex w-full items-center text-left cursor-pointer transition-colors\"\n )}\n style={{\n background: isSelected ? \"var(--canvas-background)\" : \"var(--canvas-background)\",\n border: isSelected\n ? \"2px solid var(--canvas-primary)\"\n : \"1px solid var(--canvas-border)\",\n padding: isSelected ? 15 : \"var(--spacing-xl)\",\n borderRadius: \"var(--spacing-xs)\",\n gap: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n {/* Radio Circle */}\n <div\n className=\"shrink-0 flex items-center justify-center rounded-full\"\n style={{\n width: 20,\n height: 20,\n border: `1px solid var(--canvas-border)`,\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n background: \"var(--canvas-background)\",\n }}\n >\n {isSelected && (\n <div\n className=\"rounded-full\"\n style={{\n width: 12,\n height: 12,\n backgroundColor: \"var(--canvas-text-subtitle)\",\n }}\n />\n )}\n </div>\n\n {/* Plan Content */}\n <div className=\"flex flex-1 flex-col min-w-0\">\n <div className=\"flex w-full items-start justify-between\" style={{ gap: \"var(--spacing-md)\" }}>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: 16,\n fontWeight: 500,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.name}\n </span>\n <span style={{ lineHeight: \"24px\", whiteSpace: \"nowrap\" }}>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: 16,\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n ${getDisplayPrice(plan)}\n </span>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 400,\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {\" \"}\n {priceSuffix}\n </span>\n </span>\n </div>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.description}\n </span>\n </div>\n </button>\n );\n })}\n </div>\n\n {/* Actions */}\n <div\n className=\"flex w-full justify-end\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleConfirm}\n disabled={loading || !currentPlanId}\n >\n {confirmLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type BillingPeriod = \"monthly\" | \"annually\";\n\nexport interface PricingPlan {\n /** Unique plan identifier */\n id: string;\n /** Display name (e.g. \"Basic\", \"Professional\") */\n name: string;\n /** Monthly price in dollars */\n monthlyPrice: number;\n /** Annual price in dollars — defaults to monthlyPrice * 12 if omitted */\n annualPrice?: number;\n /** Short description of what the plan includes */\n description: string;\n}\n\nexport interface PricingPlansPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text below the title */\n description?: string;\n /** Array of plan options to display as radio cards */\n plans?: PricingPlan[];\n /** Currently selected billing period (controlled) */\n billingPeriod?: BillingPeriod;\n /** Default billing period when uncontrolled */\n defaultBillingPeriod?: BillingPeriod;\n /** Callback when billing period changes */\n onBillingPeriodChange?: (period: BillingPeriod) => void;\n /** Currently selected plan ID (controlled) */\n selectedPlanId?: string;\n /** Default selected plan ID when uncontrolled */\n defaultSelectedPlanId?: string;\n /** Callback when selected plan changes */\n onPlanChange?: (planId: string) => void;\n /** Save/confirm button label */\n confirmLabel?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Callback when save button is clicked — receives the selected plan ID and billing period */\n onConfirm?: (planId: string, billingPeriod: BillingPeriod) => void;\n /** Callback when cancel button is clicked */\n onCancel?: () => void;\n /** Disables the confirm button and shows a loading state */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Pricing plans\";\nconst DEFAULT_DESCRIPTION =\n \"Choose the plan that's right for your company. All plans will include a 30-day trial.\";\n\nconst DEFAULT_PLANS: PricingPlan[] = [\n {\n id: \"basic\",\n name: \"Basic\",\n monthlyPrice: 5,\n annualPrice: 50,\n description: \"For hobbyists\",\n },\n {\n id: \"professional\",\n name: \"Professional\",\n monthlyPrice: 10,\n annualPrice: 100,\n description: \"For teams up to 30 people\",\n },\n {\n id: \"enterprise\",\n name: \"Enterprise\",\n monthlyPrice: 30,\n annualPrice: 300,\n description: \"For large teams\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// PricingPlansPopup\n// ---------------------------------------------------------------------------\n\nexport function PricingPlansPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n plans = DEFAULT_PLANS,\n billingPeriod,\n defaultBillingPeriod = \"monthly\",\n onBillingPeriodChange,\n selectedPlanId,\n defaultSelectedPlanId,\n onPlanChange,\n confirmLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n loading = false,\n className,\n}: PricingPlansPopupProps) {\n // Controlled vs uncontrolled billing period\n const isBillingControlled = billingPeriod !== undefined;\n const [internalBilling, setInternalBilling] =\n useState<BillingPeriod>(defaultBillingPeriod);\n const currentBilling = isBillingControlled ? billingPeriod : internalBilling;\n\n // Controlled vs uncontrolled plan selection\n const isPlanControlled = selectedPlanId !== undefined;\n const [internalPlanId, setInternalPlanId] = useState<string | undefined>(\n defaultSelectedPlanId\n );\n const currentPlanId = isPlanControlled ? selectedPlanId : internalPlanId;\n\n // Reset internal state when dialog closes\n useEffect(() => {\n if (!open) {\n if (!isBillingControlled) setInternalBilling(defaultBillingPeriod);\n if (!isPlanControlled) setInternalPlanId(defaultSelectedPlanId);\n }\n }, [open, isBillingControlled, isPlanControlled, defaultBillingPeriod, defaultSelectedPlanId]);\n\n const handleBillingChange = (period: BillingPeriod) => {\n if (!isBillingControlled) {\n setInternalBilling(period);\n }\n onBillingPeriodChange?.(period);\n };\n\n const handlePlanSelect = (planId: string) => {\n if (!isPlanControlled) {\n setInternalPlanId(planId);\n }\n onPlanChange?.(planId);\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleConfirm = () => {\n if (currentPlanId) {\n onConfirm?.(currentPlanId, currentBilling);\n }\n };\n\n const getDisplayPrice = (plan: PricingPlan): number => {\n if (currentBilling === \"annually\") {\n return plan.annualPrice ?? plan.monthlyPrice * 12;\n }\n return plan.monthlyPrice;\n };\n\n const priceSuffix = currentBilling === \"annually\" ? \"/ year\" : \"/ month\";\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[480px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Description */}\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n\n {/* Billing Period Toggle */}\n <div\n className=\"flex w-full overflow-hidden self-center\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--spacing-xs)\",\n height: 40,\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n width: \"fit-content\",\n }}\n >\n {([\"monthly\", \"annually\"] as const).map((period, idx) => {\n const isActive = currentBilling === period;\n return (\n <button\n key={period}\n type=\"button\"\n onClick={() => handleBillingChange(period)}\n className=\"cursor-pointer\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"24px\",\n background: \"var(--canvas-background)\",\n border: \"none\",\n borderLeft:\n isActive\n ? \"2px solid var(--canvas-primary)\"\n : idx > 0\n ? \"1px solid var(--canvas-border)\"\n : \"none\",\n color: isActive\n ? \"var(--canvas-primary)\"\n : \"var(--canvas-text-placeholder)\",\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n {period === \"monthly\" ? \"Monthly\" : \"Annually\"}\n </button>\n );\n })}\n </div>\n\n {/* Plan Radio Cards */}\n <div className=\"flex w-full flex-col\" style={{ gap: \"var(--spacing-2xl)\" }}>\n {plans.map((plan) => {\n const isSelected = currentPlanId === plan.id;\n return (\n <button\n key={plan.id}\n type=\"button\"\n onClick={() => handlePlanSelect(plan.id)}\n className={cn(\n \"flex w-full items-center text-left cursor-pointer transition-colors\"\n )}\n style={{\n background: isSelected ? \"var(--canvas-background)\" : \"var(--canvas-background)\",\n border: isSelected\n ? \"2px solid var(--canvas-primary)\"\n : \"1px solid var(--canvas-border)\",\n padding: isSelected ? 15 : \"var(--spacing-xl)\",\n borderRadius: \"var(--spacing-xs)\",\n gap: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n {/* Radio Circle */}\n <div\n className=\"shrink-0 flex items-center justify-center rounded-full\"\n style={{\n width: 20,\n height: 20,\n border: `1px solid var(--canvas-border)`,\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n background: \"var(--canvas-background)\",\n }}\n >\n {isSelected && (\n <div\n className=\"rounded-full\"\n style={{\n width: 12,\n height: 12,\n backgroundColor: \"var(--canvas-text-subtitle)\",\n }}\n />\n )}\n </div>\n\n {/* Plan Content */}\n <div className=\"flex flex-1 flex-col min-w-0\">\n <div className=\"flex w-full items-start justify-between\" style={{ gap: \"var(--spacing-md)\" }}>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: 16,\n fontWeight: 500,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.name}\n </span>\n <span style={{ lineHeight: \"24px\", whiteSpace: \"nowrap\" }}>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: 16,\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n ${getDisplayPrice(plan)}\n </span>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 400,\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {\" \"}\n {priceSuffix}\n </span>\n </span>\n </div>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.description}\n </span>\n </div>\n </button>\n );\n })}\n </div>\n\n {/* Actions */}\n <div\n className=\"flex w-full justify-end\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleConfirm}\n disabled={loading || !currentPlanId}\n >\n {confirmLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
19
19
  }
20
20
  ],
21
21
  "dependencies": [],
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/blocks/purchase-confirmation-popup.tsx",
17
17
  "type": "registry:block",
18
- "content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PurchaseConfirmationPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text — supports React nodes for inline bold/emphasis */\n description?: React.ReactNode;\n /** Label for the card field */\n cardLabel?: string;\n /** Masked card number to display (e.g. \"**** **** **** 8274\") */\n cardLastFour?: string;\n /** Label for the change-card button */\n changeCardLabel?: string;\n /** Callback when the \"Change\" card button is clicked */\n onChangeCard?: () => void;\n /** Confirm button label */\n confirmLabel?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Callback when the confirm button is clicked */\n onConfirm?: () => void;\n /** Callback when the cancel button is clicked */\n onCancel?: () => void;\n /** Disables the confirm button and shows a loading state */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Confirm your plan\";\nconst DEFAULT_DESCRIPTION = (\n <>\n You are about to reserve a spot for{\" \"}\n <strong style={{ color: \"var(--canvas-text)\" }}>AirDev Academy</strong> on{\" \"}\n <strong style={{ color: \"var(--canvas-text)\" }}>\n Tuesday, Dec 25 at 4:00pm ET\n </strong>\n .{\"\\n\\n\"}Click below to confirm your credit card and authorize a charge of{\" \"}\n <strong\n style={{\n color: \"var(--canvas-text)\",\n fontSize: \"var(--typo-body-l-size)\",\n fontWeight: 600,\n }}\n >\n $59\n </strong>{\" \"}\n for the class.\n </>\n);\n\n// ---------------------------------------------------------------------------\n// PurchaseConfirmationPopup\n// ---------------------------------------------------------------------------\n\nexport function PurchaseConfirmationPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n cardLabel = \"Selected credit card\",\n cardLastFour = \"**** **** **** 8274\",\n changeCardLabel = \"Change\",\n onChangeCard,\n confirmLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n loading = false,\n className,\n}: PurchaseConfirmationPopupProps) {\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleConfirm = () => {\n onConfirm?.();\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[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 asChild>\n <div\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </div>\n </DialogDescription>\n\n {/* Credit card field */}\n <div className=\"flex flex-col gap-[var(--spacing-xs)] w-full\">\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n fontWeight: 500,\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {cardLabel}\n </span>\n <div\n className={cn(\n \"flex items-center gap-[var(--spacing-3xl)]\",\n \"h-[44px] w-full\",\n \"rounded-[var(--radius-xs)]\",\n \"border border-[var(--canvas-border)]\",\n \"bg-[var(--canvas-background)]\",\n \"pl-[var(--spacing-xl)] pr-[var(--spacing-md)]\",\n \"shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)]\"\n )}\n >\n <span\n className=\"flex-1\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {cardLastFour}\n </span>\n <button\n type=\"button\"\n onClick={onChangeCard}\n className=\"shrink-0 cursor-pointer rounded-[var(--radius-xs)] px-[var(--spacing-lg)] py-[var(--spacing-xs)]\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"16px\",\n lineHeight: \"24px\",\n fontWeight: 600,\n color: \"var(--canvas-primary)\",\n background: \"transparent\",\n border: \"none\",\n }}\n >\n {changeCardLabel}\n </button>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] flex-col-reverse sm:flex-row justify-end\">\n <Button\n variant=\"neutral\"\n className=\"sm:w-[96px]\"\n onClick={handleCancel}\n >\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleConfirm}\n disabled={loading}\n >\n {confirmLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PurchaseConfirmationPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text — supports React nodes for inline bold/emphasis */\n description?: React.ReactNode;\n /** Label for the card field */\n cardLabel?: string;\n /** Masked card number to display (e.g. \"**** **** **** 8274\") */\n cardLastFour?: string;\n /** Label for the change-card button */\n changeCardLabel?: string;\n /** Callback when the \"Change\" card button is clicked */\n onChangeCard?: () => void;\n /** Confirm button label */\n confirmLabel?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Callback when the confirm button is clicked */\n onConfirm?: () => void;\n /** Callback when the cancel button is clicked */\n onCancel?: () => void;\n /** Disables the confirm button and shows a loading state */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Default data\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Confirm your plan\";\nconst DEFAULT_DESCRIPTION = (\n <>\n You are about to reserve a spot for{\" \"}\n <strong style={{ color: \"var(--canvas-text)\" }}>AirDev Academy</strong> on{\" \"}\n <strong style={{ color: \"var(--canvas-text)\" }}>\n Tuesday, Dec 25 at 4:00pm ET\n </strong>\n .{\"\\n\\n\"}Click below to confirm your credit card and authorize a charge of{\" \"}\n <strong\n style={{\n color: \"var(--canvas-text)\",\n fontSize: \"var(--typo-body-l-size)\",\n fontWeight: 600,\n }}\n >\n $59\n </strong>{\" \"}\n for the class.\n </>\n);\n\n// ---------------------------------------------------------------------------\n// PurchaseConfirmationPopup\n// ---------------------------------------------------------------------------\n\nexport function PurchaseConfirmationPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n cardLabel = \"Selected credit card\",\n cardLastFour = \"**** **** **** 8274\",\n changeCardLabel = \"Change\",\n onChangeCard,\n confirmLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n loading = false,\n className,\n}: PurchaseConfirmationPopupProps) {\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleConfirm = () => {\n onConfirm?.();\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[375px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Description */}\n <DialogDescription asChild>\n <div\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </div>\n </DialogDescription>\n\n {/* Credit card field */}\n <div className=\"flex flex-col gap-[var(--spacing-xs)] w-full\">\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n fontWeight: 500,\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {cardLabel}\n </span>\n <div\n className={cn(\n \"flex items-center gap-[var(--spacing-3xl)]\",\n \"h-[44px] w-full\",\n \"rounded-[var(--radius-xs)]\",\n \"border border-[var(--canvas-border)]\",\n \"bg-[var(--canvas-background)]\",\n \"pl-[var(--spacing-xl)] pr-[var(--spacing-md)]\",\n \"shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)]\"\n )}\n >\n <span\n className=\"flex-1\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {cardLastFour}\n </span>\n <button\n type=\"button\"\n onClick={onChangeCard}\n className=\"shrink-0 cursor-pointer rounded-[var(--radius-xs)] px-[var(--spacing-lg)] py-[var(--spacing-xs)]\"\n style={{\n fontFamily:\n \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"16px\",\n lineHeight: \"24px\",\n fontWeight: 600,\n color: \"var(--canvas-primary)\",\n background: \"transparent\",\n border: \"none\",\n }}\n >\n {changeCardLabel}\n </button>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] flex-col-reverse sm:flex-row justify-end\">\n <Button\n variant=\"neutral\"\n className=\"sm:w-[96px]\"\n onClick={handleCancel}\n >\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleConfirm}\n disabled={loading}\n >\n {confirmLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
19
19
  }
20
20
  ],
21
21
  "dependencies": [],
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/blocks/share-project-popup.tsx",
17
17
  "type": "registry:block",
18
- "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { Label } from \"../ui/label\";\nimport { Input } from \"../ui/input\";\nimport {\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n} from \"../ui/select\";\nimport {\n Popover,\n PopoverTrigger,\n PopoverContent,\n} from \"../ui/popover\";\nimport { Switch } from \"../ui/switch\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Copy, Check, ChevronDown, Globe } from \"lucide-react\";\nimport {\n AVATAR_MARCUS_WEBB,\n AVATAR_SARAH_CHEN,\n AVATAR_MAYA_JOHNSON,\n AVATAR_LILY_TRAN,\n} from \"./demo-avatars\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ShareRole {\n /** Unique role identifier */\n id: string;\n /** Display label */\n label: string;\n /** Description shown in the role popover */\n description: string;\n}\n\nexport interface SharedPerson {\n /** Unique person identifier */\n id: string;\n /** Display name */\n name: string;\n /** Avatar image URL or base64 */\n avatarUrl?: string;\n /** Avatar fallback initials */\n avatarFallback?: string;\n /** Current role id — must match a ShareRole.id */\n role: string;\n /** Whether access is enabled */\n enabled: boolean;\n}\n\nexport interface ShareProjectPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Popup title */\n title?: string;\n /** Subtitle description */\n description?: string;\n /** Visibility options for the dropdown */\n visibilityOptions?: { id: string; label: string }[];\n /** Default selected visibility option id */\n defaultVisibility?: string;\n /** The shareable URL */\n shareUrl?: string;\n /** Invite user options for the select */\n inviteOptions?: { id: string; label: string }[];\n /** Invite button label */\n inviteLabel?: string;\n /** Role definitions for the dropdown */\n roles?: ShareRole[];\n /** People with access */\n people?: SharedPerson[];\n /** Callback when invite button is clicked */\n onInvite?: (userId: string) => void;\n /** Callback when a person's role changes */\n onRoleChange?: (personId: string, roleId: string) => void;\n /** Callback when a person's toggle changes */\n onToggle?: (personId: string, enabled: boolean) => void;\n /** Callback when the URL is copied */\n onCopyLink?: () => void;\n /** Additional class names for the dialog content */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_ROLES: ShareRole[] = [\n {\n id: \"full\",\n label: \"Full\",\n description: \"Can view, edit, and manage all aspects of the project.\",\n },\n {\n id: \"edit\",\n label: \"Edit\",\n description: \"Can view and edit content, but cannot manage settings.\",\n },\n {\n id: \"comment\",\n label: \"Comment\",\n description: \"Can view and leave comments, but cannot edit content.\",\n },\n {\n id: \"view\",\n label: \"View only\",\n description: \"Can view content only. No editing or commenting.\",\n },\n];\n\nconst DEFAULT_VISIBILITY_OPTIONS = [\n { id: \"public\", label: \"Public\" },\n { id: \"private\", label: \"Private\" },\n { id: \"team\", label: \"Team only\" },\n];\n\nconst DEFAULT_INVITE_OPTIONS = [\n { id: \"user-1\", label: \"Alex Rivera\" },\n { id: \"user-2\", label: \"Morgan Lee\" },\n { id: \"user-3\", label: \"Casey Jordan\" },\n];\n\nconst DEFAULT_PEOPLE: SharedPerson[] = [\n {\n id: \"p1\",\n name: \"John Connor\",\n avatarUrl: AVATAR_MARCUS_WEBB,\n avatarFallback: \"JC\",\n role: \"full\",\n enabled: true,\n },\n {\n id: \"p2\",\n name: \"Raj Mishra\",\n avatarUrl: AVATAR_SARAH_CHEN,\n avatarFallback: \"RM\",\n role: \"edit\",\n enabled: true,\n },\n {\n id: \"p3\",\n name: \"Mary Trott\",\n avatarUrl: AVATAR_MAYA_JOHNSON,\n avatarFallback: \"MT\",\n role: \"comment\",\n enabled: true,\n },\n {\n id: \"p4\",\n name: \"Lily Sun\",\n avatarUrl: AVATAR_LILY_TRAN,\n avatarFallback: \"LS\",\n role: \"view\",\n enabled: false,\n },\n];\n\n// ---------------------------------------------------------------------------\n// Role Dropdown (uses Popover for rich options)\n// ---------------------------------------------------------------------------\n\nfunction RoleDropdown({\n roles,\n currentRole,\n onRoleChange,\n}: {\n roles: ShareRole[];\n currentRole: string;\n onRoleChange: (roleId: string) => void;\n}) {\n const [open, setOpen] = useState(false);\n const current = roles.find((r) => r.id === currentRole);\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n className=\"flex items-center cursor-pointer shrink-0\"\n style={{\n gap: \"var(--spacing-xs)\",\n padding: \"var(--spacing-xs) var(--spacing-sm)\",\n borderRadius: \"var(--radius-sm)\",\n border: \"1px solid var(--canvas-border)\",\n background: \"var(--canvas-background)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {current?.label ?? currentRole}\n <ChevronDown\n className=\"shrink-0\"\n style={{\n width: 14,\n height: 14,\n color: \"var(--canvas-text-muted)\",\n }}\n />\n </button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n sideOffset={4}\n className=\"p-0 w-[260px]\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"flex flex-col\">\n {roles.map((role, idx) => {\n const isSelected = role.id === currentRole;\n return (\n <button\n key={role.id}\n className=\"flex items-start text-left cursor-pointer w-full\"\n style={{\n padding: \"var(--spacing-lg) var(--spacing-xl)\",\n gap: \"var(--spacing-lg)\",\n background: \"none\",\n border: \"none\",\n borderBottom:\n idx < roles.length - 1\n ? \"1px solid var(--canvas-border)\"\n : \"none\",\n }}\n onClick={() => {\n onRoleChange(role.id);\n setOpen(false);\n }}\n >\n {/* Checkmark area */}\n <div\n className=\"shrink-0 flex items-center justify-center\"\n style={{ width: 16, height: 20 }}\n >\n {isSelected && (\n <Check\n style={{\n width: 16,\n height: 16,\n color: \"var(--canvas-primary)\",\n }}\n />\n )}\n </div>\n\n {/* Label & description */}\n <div className=\"flex flex-col min-w-0\">\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"20px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {role.label}\n </span>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xs-size)\",\n lineHeight: \"18px\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {role.description}\n </span>\n </div>\n </button>\n );\n })}\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Person Row\n// ---------------------------------------------------------------------------\n\nfunction PersonRow({\n person,\n roles,\n onRoleChange,\n onToggle,\n}: {\n person: SharedPerson;\n roles: ShareRole[];\n onRoleChange: (roleId: string) => void;\n onToggle: (enabled: boolean) => void;\n}) {\n return (\n <div\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-lg)\",\n paddingBottom: \"var(--spacing-lg)\",\n }}\n >\n {/* Avatar + Name */}\n <Avatar className=\"size-8 shrink-0\">\n {person.avatarUrl && (\n <AvatarImage src={person.avatarUrl} alt={person.name} />\n )}\n <AvatarFallback\n className=\"text-xs font-medium bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\"\n >\n {person.avatarFallback ?? person.name.slice(0, 2).toUpperCase()}\n </AvatarFallback>\n </Avatar>\n\n <span\n className=\"flex-1 min-w-0 truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"20px\",\n fontWeight: 500,\n color: \"var(--canvas-text)\",\n }}\n >\n {person.name}\n </span>\n\n {/* Role dropdown */}\n <RoleDropdown\n roles={roles}\n currentRole={person.role}\n onRoleChange={onRoleChange}\n />\n\n {/* Toggle switch */}\n <Switch\n checked={person.enabled}\n onCheckedChange={onToggle}\n />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ShareProjectPopup\n// ---------------------------------------------------------------------------\n\nexport function ShareProjectPopup({\n open,\n onOpenChange,\n title = \"Share this project\",\n description = \"Manage access and permissions for your project.\",\n visibilityOptions = DEFAULT_VISIBILITY_OPTIONS,\n defaultVisibility = \"public\",\n shareUrl = \"https://airdev.co/project/23945\",\n inviteOptions = DEFAULT_INVITE_OPTIONS,\n inviteLabel = \"Invite user\",\n roles = DEFAULT_ROLES,\n people: peopleProp = DEFAULT_PEOPLE,\n onInvite,\n onRoleChange,\n onToggle,\n onCopyLink,\n className,\n}: ShareProjectPopupProps) {\n const [copied, setCopied] = useState(false);\n const [people, setPeople] = useState(peopleProp);\n const [visibility, setVisibility] = useState(defaultVisibility);\n const [selectedInvite, setSelectedInvite] = useState(\"\");\n\n // Sync when prop changes\n useEffect(() => {\n setPeople(peopleProp);\n }, [peopleProp]);\n\n // Reset copied state on close\n useEffect(() => {\n if (!open) {\n setCopied(false);\n setSelectedInvite(\"\");\n }\n }, [open]);\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(shareUrl);\n setCopied(true);\n onCopyLink?.();\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleRoleChange = (personId: string, roleId: string) => {\n setPeople((prev) =>\n prev.map((p) => (p.id === personId ? { ...p, role: roleId } : p))\n );\n onRoleChange?.(personId, roleId);\n };\n\n const handleToggle = (personId: string, enabled: boolean) => {\n setPeople((prev) =>\n prev.map((p) => (p.id === personId ? { ...p, enabled } : p))\n );\n onToggle?.(personId, enabled);\n };\n\n const handleInvite = () => {\n if (selectedInvite) {\n onInvite?.(selectedInvite);\n setSelectedInvite(\"\");\n }\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n <div\n className=\"flex flex-col\"\n style={{\n padding: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n }}\n >\n {/* Title & Description */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-xs)\" }}>\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n </div>\n\n {/* Visibility Section */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <Label>Visibility</Label>\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"shrink-0\">\n <Select value={visibility} onValueChange={setVisibility}>\n <SelectTrigger\n className=\"w-[140px]\"\n style={{ height: \"var(--input-standard-height)\" }}\n >\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <Globe\n style={{\n width: 14,\n height: 14,\n color: \"var(--canvas-text-muted)\",\n }}\n />\n <SelectValue />\n </div>\n </SelectTrigger>\n <SelectContent>\n {visibilityOptions.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* URL + copy */}\n <div\n className=\"flex items-center flex-1 min-w-0\"\n style={{\n height: \"var(--input-standard-height)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--input-standard-radius)\",\n backgroundColor: \"var(--canvas-background)\",\n overflow: \"hidden\",\n }}\n >\n <Input\n readOnly\n value={shareUrl}\n className=\"border-0 shadow-none flex-1 min-w-0\"\n style={{\n height: \"100%\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n />\n <button\n onClick={handleCopy}\n className=\"shrink-0 flex items-center justify-center cursor-pointer\"\n style={{\n width: 36,\n height: \"100%\",\n background: \"none\",\n border: \"none\",\n borderLeft: \"1px solid var(--canvas-border)\",\n color: copied\n ? \"var(--canvas-primary)\"\n : \"var(--canvas-text-muted)\",\n }}\n >\n {copied ? (\n <Check style={{ width: 14, height: 14 }} />\n ) : (\n <Copy style={{ width: 14, height: 14 }} />\n )}\n </button>\n </div>\n </div>\n </div>\n\n {/* Invite Section */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <Label>Invite a user</Label>\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"flex-1\">\n <Select\n value={selectedInvite}\n onValueChange={setSelectedInvite}\n >\n <SelectTrigger\n style={{ height: \"var(--input-standard-height)\" }}\n >\n <SelectValue placeholder=\"Search for a user...\" />\n </SelectTrigger>\n <SelectContent>\n {inviteOptions.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <Button\n variant=\"primary\"\n onClick={handleInvite}\n disabled={!selectedInvite}\n >\n {inviteLabel}\n </Button>\n </div>\n </div>\n\n {/* People with access */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"20px\",\n color: \"var(--canvas-text)\",\n }}\n >\n People with access ({people.length})\n </span>\n\n <div\n className=\"flex flex-col\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n }}\n >\n {people.map((person) => (\n <div\n key={person.id}\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n <PersonRow\n person={person}\n roles={roles}\n onRoleChange={(roleId) =>\n handleRoleChange(person.id, roleId)\n }\n onToggle={(enabled) =>\n handleToggle(person.id, enabled)\n }\n />\n </div>\n ))}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { Label } from \"../ui/label\";\nimport { Input } from \"../ui/input\";\nimport {\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n} from \"../ui/select\";\nimport {\n Popover,\n PopoverTrigger,\n PopoverContent,\n} from \"../ui/popover\";\nimport { Switch } from \"../ui/switch\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Copy, Check, ChevronDown, Globe } from \"lucide-react\";\nimport {\n AVATAR_MARCUS_WEBB,\n AVATAR_SARAH_CHEN,\n AVATAR_MAYA_JOHNSON,\n AVATAR_LILY_TRAN,\n} from \"./demo-avatars\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ShareRole {\n /** Unique role identifier */\n id: string;\n /** Display label */\n label: string;\n /** Description shown in the role popover */\n description: string;\n}\n\nexport interface SharedPerson {\n /** Unique person identifier */\n id: string;\n /** Display name */\n name: string;\n /** Avatar image URL or base64 */\n avatarUrl?: string;\n /** Avatar fallback initials */\n avatarFallback?: string;\n /** Current role id — must match a ShareRole.id */\n role: string;\n /** Whether access is enabled */\n enabled: boolean;\n}\n\nexport interface ShareProjectPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Popup title */\n title?: string;\n /** Subtitle description */\n description?: string;\n /** Visibility options for the dropdown */\n visibilityOptions?: { id: string; label: string }[];\n /** Default selected visibility option id */\n defaultVisibility?: string;\n /** The shareable URL */\n shareUrl?: string;\n /** Invite user options for the select */\n inviteOptions?: { id: string; label: string }[];\n /** Invite button label */\n inviteLabel?: string;\n /** Role definitions for the dropdown */\n roles?: ShareRole[];\n /** People with access */\n people?: SharedPerson[];\n /** Callback when invite button is clicked */\n onInvite?: (userId: string) => void;\n /** Callback when a person's role changes */\n onRoleChange?: (personId: string, roleId: string) => void;\n /** Callback when a person's toggle changes */\n onToggle?: (personId: string, enabled: boolean) => void;\n /** Callback when the URL is copied */\n onCopyLink?: () => void;\n /** Additional class names for the dialog content */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_ROLES: ShareRole[] = [\n {\n id: \"full\",\n label: \"Full\",\n description: \"Can view, edit, and manage all aspects of the project.\",\n },\n {\n id: \"edit\",\n label: \"Edit\",\n description: \"Can view and edit content, but cannot manage settings.\",\n },\n {\n id: \"comment\",\n label: \"Comment\",\n description: \"Can view and leave comments, but cannot edit content.\",\n },\n {\n id: \"view\",\n label: \"View only\",\n description: \"Can view content only. No editing or commenting.\",\n },\n];\n\nconst DEFAULT_VISIBILITY_OPTIONS = [\n { id: \"public\", label: \"Public\" },\n { id: \"private\", label: \"Private\" },\n { id: \"team\", label: \"Team only\" },\n];\n\nconst DEFAULT_INVITE_OPTIONS = [\n { id: \"user-1\", label: \"Alex Rivera\" },\n { id: \"user-2\", label: \"Morgan Lee\" },\n { id: \"user-3\", label: \"Casey Jordan\" },\n];\n\nconst DEFAULT_PEOPLE: SharedPerson[] = [\n {\n id: \"p1\",\n name: \"John Connor\",\n avatarUrl: AVATAR_MARCUS_WEBB,\n avatarFallback: \"JC\",\n role: \"full\",\n enabled: true,\n },\n {\n id: \"p2\",\n name: \"Raj Mishra\",\n avatarUrl: AVATAR_SARAH_CHEN,\n avatarFallback: \"RM\",\n role: \"edit\",\n enabled: true,\n },\n {\n id: \"p3\",\n name: \"Mary Trott\",\n avatarUrl: AVATAR_MAYA_JOHNSON,\n avatarFallback: \"MT\",\n role: \"comment\",\n enabled: true,\n },\n {\n id: \"p4\",\n name: \"Lily Sun\",\n avatarUrl: AVATAR_LILY_TRAN,\n avatarFallback: \"LS\",\n role: \"view\",\n enabled: false,\n },\n];\n\n// ---------------------------------------------------------------------------\n// Role Dropdown (uses Popover for rich options)\n// ---------------------------------------------------------------------------\n\nfunction RoleDropdown({\n roles,\n currentRole,\n onRoleChange,\n}: {\n roles: ShareRole[];\n currentRole: string;\n onRoleChange: (roleId: string) => void;\n}) {\n const [open, setOpen] = useState(false);\n const current = roles.find((r) => r.id === currentRole);\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n className=\"flex items-center cursor-pointer shrink-0\"\n style={{\n gap: \"var(--spacing-xs)\",\n padding: \"var(--spacing-xs) var(--spacing-sm)\",\n borderRadius: \"var(--radius-sm)\",\n border: \"1px solid var(--canvas-border)\",\n background: \"var(--canvas-background)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {current?.label ?? currentRole}\n <ChevronDown\n className=\"shrink-0\"\n style={{\n width: 14,\n height: 14,\n color: \"var(--canvas-text-muted)\",\n }}\n />\n </button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n sideOffset={4}\n className=\"p-0 w-[260px]\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"flex flex-col\">\n {roles.map((role, idx) => {\n const isSelected = role.id === currentRole;\n return (\n <button\n key={role.id}\n className=\"flex items-start text-left cursor-pointer w-full\"\n style={{\n padding: \"var(--spacing-lg) var(--spacing-xl)\",\n gap: \"var(--spacing-lg)\",\n background: \"none\",\n border: \"none\",\n borderBottom:\n idx < roles.length - 1\n ? \"1px solid var(--canvas-border)\"\n : \"none\",\n }}\n onClick={() => {\n onRoleChange(role.id);\n setOpen(false);\n }}\n >\n {/* Checkmark area */}\n <div\n className=\"shrink-0 flex items-center justify-center\"\n style={{ width: 16, height: 20 }}\n >\n {isSelected && (\n <Check\n style={{\n width: 16,\n height: 16,\n color: \"var(--canvas-primary)\",\n }}\n />\n )}\n </div>\n\n {/* Label & description */}\n <div className=\"flex flex-col min-w-0\">\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"20px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {role.label}\n </span>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xs-size)\",\n lineHeight: \"18px\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {role.description}\n </span>\n </div>\n </button>\n );\n })}\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Person Row\n// ---------------------------------------------------------------------------\n\nfunction PersonRow({\n person,\n roles,\n onRoleChange,\n onToggle,\n}: {\n person: SharedPerson;\n roles: ShareRole[];\n onRoleChange: (roleId: string) => void;\n onToggle: (enabled: boolean) => void;\n}) {\n return (\n <div\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-lg)\",\n paddingBottom: \"var(--spacing-lg)\",\n }}\n >\n {/* Avatar + Name */}\n <Avatar className=\"size-8 shrink-0\">\n {person.avatarUrl && (\n <AvatarImage src={person.avatarUrl} alt={person.name} />\n )}\n <AvatarFallback\n className=\"text-xs font-medium bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\"\n >\n {person.avatarFallback ?? person.name.slice(0, 2).toUpperCase()}\n </AvatarFallback>\n </Avatar>\n\n <span\n className=\"flex-1 min-w-0 truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"20px\",\n fontWeight: 500,\n color: \"var(--canvas-text)\",\n }}\n >\n {person.name}\n </span>\n\n {/* Role dropdown */}\n <RoleDropdown\n roles={roles}\n currentRole={person.role}\n onRoleChange={onRoleChange}\n />\n\n {/* Toggle switch */}\n <Switch\n checked={person.enabled}\n onCheckedChange={onToggle}\n />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ShareProjectPopup\n// ---------------------------------------------------------------------------\n\nexport function ShareProjectPopup({\n open,\n onOpenChange,\n title = \"Share this project\",\n description = \"Manage access and permissions for your project.\",\n visibilityOptions = DEFAULT_VISIBILITY_OPTIONS,\n defaultVisibility = \"public\",\n shareUrl = \"https://airdev.co/project/23945\",\n inviteOptions = DEFAULT_INVITE_OPTIONS,\n inviteLabel = \"Invite user\",\n roles = DEFAULT_ROLES,\n people: peopleProp = DEFAULT_PEOPLE,\n onInvite,\n onRoleChange,\n onToggle,\n onCopyLink,\n className,\n}: ShareProjectPopupProps) {\n const [copied, setCopied] = useState(false);\n const [people, setPeople] = useState(peopleProp);\n const [visibility, setVisibility] = useState(defaultVisibility);\n const [selectedInvite, setSelectedInvite] = useState(\"\");\n\n // Sync when prop changes\n useEffect(() => {\n setPeople(peopleProp);\n }, [peopleProp]);\n\n // Reset copied state on close\n useEffect(() => {\n if (!open) {\n setCopied(false);\n setSelectedInvite(\"\");\n }\n }, [open]);\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(shareUrl);\n setCopied(true);\n onCopyLink?.();\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleRoleChange = (personId: string, roleId: string) => {\n setPeople((prev) =>\n prev.map((p) => (p.id === personId ? { ...p, role: roleId } : p))\n );\n onRoleChange?.(personId, roleId);\n };\n\n const handleToggle = (personId: string, enabled: boolean) => {\n setPeople((prev) =>\n prev.map((p) => (p.id === personId ? { ...p, enabled } : p))\n );\n onToggle?.(personId, enabled);\n };\n\n const handleInvite = () => {\n if (selectedInvite) {\n onInvite?.(selectedInvite);\n setSelectedInvite(\"\");\n }\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n <div\n className=\"flex flex-col\"\n style={{\n padding: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n }}\n >\n {/* Title & Description */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-xs)\" }}>\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n </div>\n\n {/* Visibility Section */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <Label>Visibility</Label>\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"shrink-0\">\n <Select value={visibility} onValueChange={setVisibility}>\n <SelectTrigger\n className=\"w-[140px]\"\n style={{ height: \"var(--input-standard-height)\" }}\n >\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <Globe\n style={{\n width: 14,\n height: 14,\n color: \"var(--canvas-text-muted)\",\n }}\n />\n <SelectValue />\n </div>\n </SelectTrigger>\n <SelectContent>\n {visibilityOptions.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* URL + copy */}\n <div\n className=\"flex items-center flex-1 min-w-0\"\n style={{\n height: \"var(--input-standard-height)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--input-standard-radius)\",\n backgroundColor: \"var(--canvas-background)\",\n overflow: \"hidden\",\n }}\n >\n <Input\n readOnly\n value={shareUrl}\n className=\"border-0 shadow-none flex-1 min-w-0\"\n style={{\n height: \"100%\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n />\n <button\n onClick={handleCopy}\n className=\"shrink-0 flex items-center justify-center cursor-pointer\"\n style={{\n width: 36,\n height: \"100%\",\n background: \"none\",\n border: \"none\",\n borderLeft: \"1px solid var(--canvas-border)\",\n color: copied\n ? \"var(--canvas-primary)\"\n : \"var(--canvas-text-muted)\",\n }}\n >\n {copied ? (\n <Check style={{ width: 14, height: 14 }} />\n ) : (\n <Copy style={{ width: 14, height: 14 }} />\n )}\n </button>\n </div>\n </div>\n </div>\n\n {/* Invite Section */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <Label>Invite a user</Label>\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"flex-1\">\n <Select\n value={selectedInvite}\n onValueChange={setSelectedInvite}\n >\n <SelectTrigger\n style={{ height: \"var(--input-standard-height)\" }}\n >\n <SelectValue placeholder=\"Search for a user...\" />\n </SelectTrigger>\n <SelectContent>\n {inviteOptions.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <Button\n variant=\"primary\"\n onClick={handleInvite}\n disabled={!selectedInvite}\n >\n {inviteLabel}\n </Button>\n </div>\n </div>\n\n {/* People with access */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"20px\",\n color: \"var(--canvas-text)\",\n }}\n >\n People with access ({people.length})\n </span>\n\n <div\n className=\"flex flex-col\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n }}\n >\n {people.map((person) => (\n <div\n key={person.id}\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n <PersonRow\n person={person}\n roles={roles}\n onRoleChange={(roleId) =>\n handleRoleChange(person.id, roleId)\n }\n onToggle={(enabled) =>\n handleToggle(person.id, enabled)\n }\n />\n </div>\n ))}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
19
19
  }
20
20
  ],
21
21
  "dependencies": [
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/blocks/small-edit-popup.tsx",
17
17
  "type": "registry:block",
18
- "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { TextInput } from \"../ui/text-input\";\nimport { Label } from \"../ui/label\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SmallEditField {\n /** Unique field identifier */\n id: string;\n /** Label text displayed above the field */\n label: string;\n /** Placeholder text */\n placeholder?: string;\n /** When true, field takes 50% width and pairs with next half field */\n half?: boolean;\n}\n\nexport interface SmallEditPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text */\n description?: string;\n /** Form field configuration */\n fields?: SmallEditField[];\n /** Cancel button label */\n cancelLabel?: string;\n /** Save button label */\n saveLabel?: string;\n /** Callback when save is clicked — receives field values */\n onSave?: (values: Record<string, string>) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Disables the save button */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Edit profile\";\nconst DEFAULT_DESCRIPTION = \"Update your personal information below.\";\n\nconst DEFAULT_FIELDS: SmallEditField[] = [\n { id: \"field-1\", label: \"First name\", half: true },\n { id: \"field-2\", label: \"Last name\", half: true },\n { id: \"field-3\", label: \"Phone\", half: true },\n { id: \"field-4\", label: \"Location\", half: true },\n { id: \"field-5\", label: \"Email address\" },\n { id: \"field-6\", label: \"Bio\" },\n];\n\n// ---------------------------------------------------------------------------\n// SmallEditPopup\n// ---------------------------------------------------------------------------\n\nexport function SmallEditPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n fields = DEFAULT_FIELDS,\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n onSave,\n onCancel,\n loading = false,\n className,\n}: SmallEditPopupProps) {\n const [values, setValues] = useState<Record<string, string>>({});\n\n // Reset form values when dialog closes\n useEffect(() => {\n if (!open) {\n setValues({});\n }\n }, [open]);\n\n const handleChange = (id: string, value: string) => {\n setValues((prev) => ({ ...prev, [id]: value }));\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.(values);\n };\n\n // Group fields into rows: half-width fields paired together\n const rows: SmallEditField[][] = [];\n let i = 0;\n while (i < fields.length) {\n const field = fields[i];\n if (field.half && i + 1 < fields.length && fields[i + 1].half) {\n rows.push([field, fields[i + 1]]);\n i += 2;\n } else {\n rows.push([field]);\n i += 1;\n }\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n {/* Title & Description */}\n <div className=\"flex flex-col\">\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n </div>\n\n {/* Form fields */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-2xl)\" }}>\n {rows.map((row, rowIdx) => (\n <div\n key={rowIdx}\n className={cn(\n \"flex gap-[var(--spacing-3xl)]\",\n row.length > 1 ? \"flex-col md:flex-row\" : \"flex-col\"\n )}\n >\n {row.map((field) => (\n <div\n key={field.id}\n className={cn(\n \"flex flex-col\",\n row.length > 1 ? \"flex-1\" : \"w-full\"\n )}\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <Label>{field.label}</Label>\n <TextInput\n value={values[field.id] ?? \"\"}\n onChange={(e) => handleChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n />\n </div>\n ))}\n </div>\n ))}\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] justify-end\">\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleSave}\n disabled={loading}\n >\n {saveLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { TextInput } from \"../ui/text-input\";\nimport { Label } from \"../ui/label\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SmallEditField {\n /** Unique field identifier */\n id: string;\n /** Label text displayed above the field */\n label: string;\n /** Placeholder text */\n placeholder?: string;\n /** When true, field takes 50% width and pairs with next half field */\n half?: boolean;\n}\n\nexport interface SmallEditPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text */\n description?: string;\n /** Form field configuration */\n fields?: SmallEditField[];\n /** Cancel button label */\n cancelLabel?: string;\n /** Save button label */\n saveLabel?: string;\n /** Callback when save is clicked — receives field values */\n onSave?: (values: Record<string, string>) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Disables the save button */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Edit profile\";\nconst DEFAULT_DESCRIPTION = \"Update your personal information below.\";\n\nconst DEFAULT_FIELDS: SmallEditField[] = [\n { id: \"field-1\", label: \"First name\", half: true },\n { id: \"field-2\", label: \"Last name\", half: true },\n { id: \"field-3\", label: \"Phone\", half: true },\n { id: \"field-4\", label: \"Location\", half: true },\n { id: \"field-5\", label: \"Email address\" },\n { id: \"field-6\", label: \"Bio\" },\n];\n\n// ---------------------------------------------------------------------------\n// SmallEditPopup\n// ---------------------------------------------------------------------------\n\nexport function SmallEditPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n fields = DEFAULT_FIELDS,\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n onSave,\n onCancel,\n loading = false,\n className,\n}: SmallEditPopupProps) {\n const [values, setValues] = useState<Record<string, string>>({});\n\n // Reset form values when dialog closes\n useEffect(() => {\n if (!open) {\n setValues({});\n }\n }, [open]);\n\n const handleChange = (id: string, value: string) => {\n setValues((prev) => ({ ...prev, [id]: value }));\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.(values);\n };\n\n // Group fields into rows: half-width fields paired together\n const rows: SmallEditField[][] = [];\n let i = 0;\n while (i < fields.length) {\n const field = fields[i];\n if (field.half && i + 1 < fields.length && fields[i + 1].half) {\n rows.push([field, fields[i + 1]]);\n i += 2;\n } else {\n rows.push([field]);\n i += 1;\n }\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[var(--canvas-shadow-modal)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n {/* Title & Description */}\n <div className=\"flex flex-col\">\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n </div>\n\n {/* Form fields */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-2xl)\" }}>\n {rows.map((row, rowIdx) => (\n <div\n key={rowIdx}\n className={cn(\n \"flex gap-[var(--spacing-3xl)]\",\n row.length > 1 ? \"flex-col md:flex-row\" : \"flex-col\"\n )}\n >\n {row.map((field) => (\n <div\n key={field.id}\n className={cn(\n \"flex flex-col\",\n row.length > 1 ? \"flex-1\" : \"w-full\"\n )}\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <Label>{field.label}</Label>\n <TextInput\n value={values[field.id] ?? \"\"}\n onChange={(e) => handleChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n />\n </div>\n ))}\n </div>\n ))}\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] justify-end\">\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleSave}\n disabled={loading}\n >\n {saveLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
19
19
  }
20
20
  ],
21
21
  "dependencies": [],