sapient-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/bin/sapient-ai.js +623 -0
- package/local-registry/README.md +59 -0
- package/local-registry/r/accordion.json +65 -0
- package/local-registry/r/alert.json +64 -0
- package/local-registry/r/badge.json +64 -0
- package/local-registry/r/button.json +66 -0
- package/local-registry/r/checkbox.json +65 -0
- package/local-registry/r/customer-satisfaction.json +61 -0
- package/local-registry/r/input.json +61 -0
- package/local-registry/r/label.json +64 -0
- package/local-registry/r/multiple-choice-card.json +66 -0
- package/local-registry/r/multiple-choice-grid.json +64 -0
- package/local-registry/r/multiple-choice-list.json +64 -0
- package/local-registry/r/news-card.json +61 -0
- package/local-registry/r/privacy-consent.json +61 -0
- package/local-registry/r/product-card.json +64 -0
- package/local-registry/r/profile-card.json +64 -0
- package/local-registry/r/progress.json +64 -0
- package/local-registry/r/promo-card.json +64 -0
- package/local-registry/r/radio-group.json +65 -0
- package/local-registry/r/separator.json +64 -0
- package/local-registry/r/switch.json +64 -0
- package/local-registry/r/tabs.json +64 -0
- package/local-registry/r/textarea.json +61 -0
- package/local-registry/r/video-card.json +69 -0
- package/local-registry/scripts/build-registry.mjs +283 -0
- package/local-registry/scripts/sync-to-design-system-public.mjs +43 -0
- package/local-registry/src/components/ui/sapient-accordion.tsx +89 -0
- package/local-registry/src/components/ui/sapient-alert.tsx +68 -0
- package/local-registry/src/components/ui/sapient-badge.tsx +28 -0
- package/local-registry/src/components/ui/sapient-button.tsx +31 -0
- package/local-registry/src/components/ui/sapient-checkbox.tsx +35 -0
- package/local-registry/src/components/ui/sapient-customer-satisfaction.tsx +189 -0
- package/local-registry/src/components/ui/sapient-icon.tsx +40 -0
- package/local-registry/src/components/ui/sapient-input.tsx +23 -0
- package/local-registry/src/components/ui/sapient-label.tsx +25 -0
- package/local-registry/src/components/ui/sapient-multiple-choice-card.tsx +172 -0
- package/local-registry/src/components/ui/sapient-multiple-choice-grid.tsx +94 -0
- package/local-registry/src/components/ui/sapient-multiple-choice-list.tsx +74 -0
- package/local-registry/src/components/ui/sapient-news-card.tsx +227 -0
- package/local-registry/src/components/ui/sapient-privacy-consent.tsx +197 -0
- package/local-registry/src/components/ui/sapient-product-card.tsx +468 -0
- package/local-registry/src/components/ui/sapient-profile-card.tsx +193 -0
- package/local-registry/src/components/ui/sapient-progress.tsx +32 -0
- package/local-registry/src/components/ui/sapient-promo-card.tsx +247 -0
- package/local-registry/src/components/ui/sapient-radio-button.tsx +82 -0
- package/local-registry/src/components/ui/sapient-radio-group.tsx +54 -0
- package/local-registry/src/components/ui/sapient-separator.tsx +28 -0
- package/local-registry/src/components/ui/sapient-switch.tsx +36 -0
- package/local-registry/src/components/ui/sapient-tabs.tsx +82 -0
- package/local-registry/src/components/ui/sapient-textarea.tsx +23 -0
- package/local-registry/src/components/ui/sapient-video-card.tsx +159 -0
- package/local-registry/src/components/ui/sapient-video-controller.tsx +214 -0
- package/package.json +25 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "multiple-choice-card",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Sapient Multiple Choice Card",
|
|
6
|
+
"description": "Selectable option card for image- and text-based answer states.",
|
|
7
|
+
"cssVars": {
|
|
8
|
+
"theme": {
|
|
9
|
+
"radius": "0.5rem"
|
|
10
|
+
},
|
|
11
|
+
"light": {
|
|
12
|
+
"background": "0 0% 98%",
|
|
13
|
+
"foreground": "0 0% 12%",
|
|
14
|
+
"card": "0 0% 98%",
|
|
15
|
+
"card-foreground": "0 0% 12%",
|
|
16
|
+
"popover": "0 0% 100%",
|
|
17
|
+
"popover-foreground": "0 0% 12%",
|
|
18
|
+
"primary": "161 89% 53%",
|
|
19
|
+
"primary-foreground": "0 0% 0%",
|
|
20
|
+
"secondary": "249 89% 63%",
|
|
21
|
+
"secondary-foreground": "0 0% 100%",
|
|
22
|
+
"muted": "0 0% 88%",
|
|
23
|
+
"muted-foreground": "0 0% 44%",
|
|
24
|
+
"accent": "161 89% 53%",
|
|
25
|
+
"accent-foreground": "0 0% 0%",
|
|
26
|
+
"destructive": "349 81% 50%",
|
|
27
|
+
"destructive-foreground": "0 0% 100%",
|
|
28
|
+
"border": "0 0% 78%",
|
|
29
|
+
"input": "0 0% 78%",
|
|
30
|
+
"ring": "161 89% 53%"
|
|
31
|
+
},
|
|
32
|
+
"dark": {
|
|
33
|
+
"background": "0 0% 0%",
|
|
34
|
+
"foreground": "0 0% 98%",
|
|
35
|
+
"card": "0 0% 12%",
|
|
36
|
+
"card-foreground": "0 0% 98%",
|
|
37
|
+
"popover": "0 0% 12%",
|
|
38
|
+
"popover-foreground": "0 0% 98%",
|
|
39
|
+
"primary": "161 89% 67%",
|
|
40
|
+
"primary-foreground": "0 0% 0%",
|
|
41
|
+
"secondary": "249 100% 73%",
|
|
42
|
+
"secondary-foreground": "0 0% 100%",
|
|
43
|
+
"muted": "0 0% 32%",
|
|
44
|
+
"muted-foreground": "0 0% 78%",
|
|
45
|
+
"accent": "161 89% 67%",
|
|
46
|
+
"accent-foreground": "0 0% 0%",
|
|
47
|
+
"destructive": "350 90% 57%",
|
|
48
|
+
"destructive-foreground": "0 0% 100%",
|
|
49
|
+
"border": "0 0% 44%",
|
|
50
|
+
"input": "0 0% 44%",
|
|
51
|
+
"ring": "161 89% 67%"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
{
|
|
56
|
+
"path": "components/ui/sapient-radio-button.tsx",
|
|
57
|
+
"type": "registry:ui",
|
|
58
|
+
"content": "'use client';\n\nimport React from 'react';\nimport { cn } from '@/lib/utils';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type RadioButtonState = 'default' | 'selected';\n\nexport interface RadioButtonProps {\n className?: string;\n state?: RadioButtonState;\n label?: string;\n showLabel?: boolean;\n /** Whether to apply backdrop-blur on default state (used when overlaid on images) */\n blur?: boolean;\n onClick?: () => void;\n}\n\n// ─── Radio dot indicator ──────────────────────────────────────────────────────\n// 24×24 container — outer ring + inner fill dot (only when selected)\nfunction RadioDot({ selected }: { selected: boolean }) {\n return (\n <span className=\"relative inline-flex items-center justify-center size-6 shrink-0\">\n {/* Outer ring */}\n <span\n className={cn(\n 'absolute size-5 rounded-full border-2 transition-colors',\n selected\n ? 'border-[hsl(var(--secondary-600))]'\n : 'border-[hsl(var(--neutral-400))]'\n )}\n />\n {/* Inner fill — only when selected */}\n {selected && (\n <span className=\"absolute size-[10px] rounded-full bg-[hsl(var(--secondary-600))]\" />\n )}\n </span>\n );\n}\n\n// ─── RadioButton ──────────────────────────────────────────────────────────────\n\nexport function RadioButton({\n className,\n state = 'default',\n label = 'LABEL',\n showLabel = true,\n blur = false,\n onClick,\n}: RadioButtonProps) {\n const isSelected = state === 'selected';\n\n return (\n <button\n type=\"button\"\n aria-pressed={isSelected}\n onClick={onClick}\n className={cn(\n 'inline-flex items-center gap-[var(--spacing-s,12px)] h-8 pl-3 pr-1 py-1 rounded-[16px] bg-white transition-colors shrink-0',\n blur && !isSelected && 'backdrop-blur-[18.5px]',\n className\n )}\n >\n {showLabel && (\n <span\n className={cn(\n 'text-[length:var(--text-label,12px)] leading-[var(--leading-label,18px)] font-[var(--font-weight-medium,500)] whitespace-nowrap shrink-0 tracking-[0.02em] uppercase',\n isSelected\n ? 'text-[hsl(var(--secondary-600))]'\n : 'text-[hsl(var(--neutral-950))]'\n )}\n >\n {label}\n </span>\n )}\n <RadioDot selected={isSelected} />\n </button>\n );\n}\n\nexport default RadioButton;\n"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"path": "components/ui/sapient-multiple-choice-card.tsx",
|
|
62
|
+
"type": "registry:ui",
|
|
63
|
+
"content": "'use client';\n\nimport React from 'react';\nimport { cn } from '@/lib/utils';\nimport { RadioButton } from './sapient-radio-button';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type MultipleChoiceCardStyle = 'image' | 'text';\nexport type MultipleChoiceCardSize = 'large' | 'small';\n\nexport interface MultipleChoiceCardProps {\n className?: string;\n style?: MultipleChoiceCardStyle;\n size?: MultipleChoiceCardSize;\n selected?: boolean;\n label?: string;\n /** Image URL — used when style=\"image\" */\n imageSrc?: string;\n imageAlt?: string;\n /** Card title — used when style=\"text\" */\n title?: string;\n /** Card subtitle — used when style=\"text\" */\n subtitle?: string;\n onSelect?: () => void;\n}\n\n// ─── Placeholder image ────────────────────────────────────────────────────────\nconst PLACEHOLDER_IMAGE =\n 'https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&q=80';\n\n// ─── MultipleChoiceCard ───────────────────────────────────────────────────────\n\nexport function MultipleChoiceCard({\n className,\n style = 'image',\n size = 'large',\n selected = false,\n label = 'LABEL',\n imageSrc = PLACEHOLDER_IMAGE,\n imageAlt = '',\n title = 'Card Title',\n subtitle,\n onSelect,\n}: MultipleChoiceCardProps) {\n const isLarge = size === 'large';\n const isSmall = size === 'small';\n const isImage = style === 'image';\n const isText = style === 'text';\n\n const defaultSubtitle = isLarge\n ? 'Subtitle on a single line of text'\n : 'Subtitle on two\\nlines of text';\n\n const resolvedSubtitle = subtitle ?? defaultSubtitle;\n\n // ── Image Large: 353×141, landscape full-bleed, radio bottom-right ────────\n if (isImage && isLarge) {\n return (\n <div\n className={cn(\n 'relative flex flex-col items-end justify-end overflow-hidden rounded-[var(--radius-token-lg,32px)] w-[353px] h-[141px] p-[var(--spacing-m,16px)] cursor-pointer',\n className\n )}\n onClick={onSelect}\n >\n {/* Full-bleed image */}\n <img\n src={imageSrc}\n alt={imageAlt}\n className=\"absolute inset-0 w-full h-full object-cover pointer-events-none rounded-[var(--radius-token-lg,32px)]\"\n />\n {/* Dark overlay */}\n <div className=\"absolute inset-0 bg-black/15 rounded-[var(--radius-token-lg,32px)] pointer-events-none\" />\n {/* Radio pill — bottom-right */}\n <RadioButton\n state={selected ? 'selected' : 'default'}\n label={label}\n blur={!selected}\n className=\"relative z-10\"\n />\n </div>\n );\n }\n\n // ── Image Small: 170×209, portrait full-bleed, radio bottom full-width ────\n if (isImage && isSmall) {\n return (\n <div\n className={cn(\n 'relative flex flex-col items-start justify-end overflow-hidden rounded-[var(--radius-token-lg,32px)] w-[170px] h-[209px] p-[var(--spacing-m,16px)] cursor-pointer',\n className\n )}\n onClick={onSelect}\n >\n {/* Full-bleed image */}\n <img\n src={imageSrc}\n alt={imageAlt}\n className=\"absolute inset-0 w-full h-full object-cover pointer-events-none rounded-[var(--radius-token-lg,32px)]\"\n />\n {/* Dark overlay */}\n <div className=\"absolute inset-0 bg-black/15 rounded-[var(--radius-token-lg,32px)] pointer-events-none\" />\n {/* Radio pill — bottom, full width */}\n <RadioButton\n state={selected ? 'selected' : 'default'}\n label={label}\n blur={!selected}\n className=\"relative z-10 w-full justify-between\"\n />\n </div>\n );\n }\n\n // ── Text Large: 353×141, white bg, radio top-right, title+subtitle bottom ─\n if (isText && isLarge) {\n return (\n <div\n className={cn(\n 'relative flex flex-col items-end justify-between overflow-hidden rounded-[var(--radius-token-lg,32px)] w-[353px] h-[141px] p-[var(--spacing-m,16px)] bg-[hsl(var(--neutral-50))] cursor-pointer',\n className\n )}\n onClick={onSelect}\n >\n {/* Radio dot only — top-right, no label */}\n <RadioButton\n state={selected ? 'selected' : 'default'}\n showLabel={false}\n className=\"bg-transparent p-0 h-auto\"\n />\n {/* Title + subtitle — bottom-left */}\n <div className=\"flex flex-col gap-[var(--spacing-xs4,4px)] items-start w-full p-[var(--spacing-xs2,8px)]\">\n <p className=\"text-[length:var(--text-subheading,20px)] leading-[var(--leading-subheading,24px)] font-[var(--font-weight-medium,500)] text-[hsl(var(--neutral-950))] w-full whitespace-pre-wrap\">\n {title}\n </p>\n <p className=\"text-[16px] leading-[var(--leading-body,24px)] font-[var(--font-weight-regular,400)] text-[hsl(var(--neutral-500))] w-full overflow-hidden text-ellipsis whitespace-nowrap\">\n {resolvedSubtitle}\n </p>\n </div>\n </div>\n );\n }\n\n // ── Text Small: 170×209, white bg, radio top-right, title+subtitle bottom ─\n return (\n <div\n className={cn(\n 'relative flex flex-col items-end justify-between overflow-hidden rounded-[var(--radius-token-lg,32px)] w-[170px] h-[209px] p-[var(--spacing-m,16px)] bg-[hsl(var(--neutral-50))] cursor-pointer',\n className\n )}\n onClick={onSelect}\n >\n {/* Radio dot only — top-right */}\n <RadioButton\n state={selected ? 'selected' : 'default'}\n showLabel={false}\n className=\"bg-transparent p-0 h-auto\"\n />\n {/* Title + subtitle — bottom-left */}\n <div className=\"flex flex-col gap-[var(--spacing-xs4,4px)] items-start w-full p-[var(--spacing-xs2,8px)]\">\n <p className=\"text-[length:var(--text-subheading,20px)] leading-[var(--leading-subheading,24px)] font-[var(--font-weight-medium,500)] text-[hsl(var(--neutral-950))] w-full whitespace-pre-wrap\">\n {title}\n </p>\n <p className=\"text-[16px] leading-[var(--leading-body,24px)] font-[var(--font-weight-regular,400)] text-[hsl(var(--neutral-500))] w-full whitespace-pre-wrap\">\n {resolvedSubtitle}\n </p>\n </div>\n </div>\n );\n}\n\nexport default MultipleChoiceCard;\n"
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "multiple-choice-grid",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Sapient Multiple Choice Grid",
|
|
6
|
+
"description": "Two-column staggered grid of selectable answer cards.",
|
|
7
|
+
"cssVars": {
|
|
8
|
+
"theme": {
|
|
9
|
+
"radius": "0.5rem"
|
|
10
|
+
},
|
|
11
|
+
"light": {
|
|
12
|
+
"background": "0 0% 98%",
|
|
13
|
+
"foreground": "0 0% 12%",
|
|
14
|
+
"card": "0 0% 98%",
|
|
15
|
+
"card-foreground": "0 0% 12%",
|
|
16
|
+
"popover": "0 0% 100%",
|
|
17
|
+
"popover-foreground": "0 0% 12%",
|
|
18
|
+
"primary": "161 89% 53%",
|
|
19
|
+
"primary-foreground": "0 0% 0%",
|
|
20
|
+
"secondary": "249 89% 63%",
|
|
21
|
+
"secondary-foreground": "0 0% 100%",
|
|
22
|
+
"muted": "0 0% 88%",
|
|
23
|
+
"muted-foreground": "0 0% 44%",
|
|
24
|
+
"accent": "161 89% 53%",
|
|
25
|
+
"accent-foreground": "0 0% 0%",
|
|
26
|
+
"destructive": "349 81% 50%",
|
|
27
|
+
"destructive-foreground": "0 0% 100%",
|
|
28
|
+
"border": "0 0% 78%",
|
|
29
|
+
"input": "0 0% 78%",
|
|
30
|
+
"ring": "161 89% 53%"
|
|
31
|
+
},
|
|
32
|
+
"dark": {
|
|
33
|
+
"background": "0 0% 0%",
|
|
34
|
+
"foreground": "0 0% 98%",
|
|
35
|
+
"card": "0 0% 12%",
|
|
36
|
+
"card-foreground": "0 0% 98%",
|
|
37
|
+
"popover": "0 0% 12%",
|
|
38
|
+
"popover-foreground": "0 0% 98%",
|
|
39
|
+
"primary": "161 89% 67%",
|
|
40
|
+
"primary-foreground": "0 0% 0%",
|
|
41
|
+
"secondary": "249 100% 73%",
|
|
42
|
+
"secondary-foreground": "0 0% 100%",
|
|
43
|
+
"muted": "0 0% 32%",
|
|
44
|
+
"muted-foreground": "0 0% 78%",
|
|
45
|
+
"accent": "161 89% 67%",
|
|
46
|
+
"accent-foreground": "0 0% 0%",
|
|
47
|
+
"destructive": "350 90% 57%",
|
|
48
|
+
"destructive-foreground": "0 0% 100%",
|
|
49
|
+
"border": "0 0% 44%",
|
|
50
|
+
"input": "0 0% 44%",
|
|
51
|
+
"ring": "161 89% 67%"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
{
|
|
56
|
+
"path": "components/ui/sapient-multiple-choice-grid.tsx",
|
|
57
|
+
"type": "registry:ui",
|
|
58
|
+
"content": "'use client';\n\nimport React, { useState } from 'react';\nimport { cn } from '@/lib/utils';\nimport { MultipleChoiceCard } from './sapient-multiple-choice-card';\nimport type { MultipleChoiceOption } from './sapient-multiple-choice-list';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface MultipleChoiceGridProps {\n className?: string;\n /** Options for left column (top-aligned) */\n leftOptions?: MultipleChoiceOption[];\n /** Options for right column (offset down by 40px) */\n rightOptions?: MultipleChoiceOption[];\n /** Initially selected option id */\n defaultSelected?: string;\n onChange?: (selectedId: string) => void;\n}\n\n// ─── Default options ──────────────────────────────────────────────────────────\nconst DEFAULT_LEFT: MultipleChoiceOption[] = [\n { id: '1', label: 'LABEL' },\n { id: '3', label: 'LABEL' },\n];\nconst DEFAULT_RIGHT: MultipleChoiceOption[] = [\n { id: '2', label: 'LABEL' },\n { id: '4', label: 'LABEL' },\n];\n\n// ─── MultipleChoiceGrid ───────────────────────────────────────────────────────\n\nexport function MultipleChoiceGrid({\n className,\n leftOptions = DEFAULT_LEFT,\n rightOptions = DEFAULT_RIGHT,\n defaultSelected,\n onChange,\n}: MultipleChoiceGridProps) {\n // Default selects the first right-column item (matches Figma — right col first card selected)\n const [selected, setSelected] = useState<string | undefined>(\n defaultSelected ?? rightOptions[0]?.id\n );\n\n const handleSelect = (id: string) => {\n setSelected(id);\n onChange?.(id);\n };\n\n return (\n <div\n className={cn(\n 'flex gap-[var(--spacing-m,16px)] items-start',\n className\n )}\n >\n {/* Left column — starts at top */}\n <div className=\"flex flex-col gap-[var(--spacing-m,16px)] items-start w-[170px]\">\n {leftOptions.map((option) => (\n <MultipleChoiceCard\n key={option.id}\n style=\"image\"\n size=\"small\"\n selected={selected === option.id}\n label={option.label}\n imageSrc={option.imageSrc}\n imageAlt={option.imageAlt}\n onSelect={() => handleSelect(option.id)}\n className=\"w-full\"\n />\n ))}\n </div>\n\n {/* Right column — offset down by 40px (masonry effect from Figma) */}\n <div className=\"flex flex-col gap-[var(--spacing-m,16px)] items-start w-[170px] pt-10\">\n {rightOptions.map((option) => (\n <MultipleChoiceCard\n key={option.id}\n style=\"image\"\n size=\"small\"\n selected={selected === option.id}\n label={option.label}\n imageSrc={option.imageSrc}\n imageAlt={option.imageAlt}\n onSelect={() => handleSelect(option.id)}\n className=\"w-full\"\n />\n ))}\n </div>\n </div>\n );\n}\n\nexport default MultipleChoiceGrid;\n"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"registryDependencies": [
|
|
62
|
+
"@sapient/multiple-choice-card"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "multiple-choice-list",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Sapient Multiple Choice List",
|
|
6
|
+
"description": "Single-column group of multiple-choice cards with managed selection state.",
|
|
7
|
+
"cssVars": {
|
|
8
|
+
"theme": {
|
|
9
|
+
"radius": "0.5rem"
|
|
10
|
+
},
|
|
11
|
+
"light": {
|
|
12
|
+
"background": "0 0% 98%",
|
|
13
|
+
"foreground": "0 0% 12%",
|
|
14
|
+
"card": "0 0% 98%",
|
|
15
|
+
"card-foreground": "0 0% 12%",
|
|
16
|
+
"popover": "0 0% 100%",
|
|
17
|
+
"popover-foreground": "0 0% 12%",
|
|
18
|
+
"primary": "161 89% 53%",
|
|
19
|
+
"primary-foreground": "0 0% 0%",
|
|
20
|
+
"secondary": "249 89% 63%",
|
|
21
|
+
"secondary-foreground": "0 0% 100%",
|
|
22
|
+
"muted": "0 0% 88%",
|
|
23
|
+
"muted-foreground": "0 0% 44%",
|
|
24
|
+
"accent": "161 89% 53%",
|
|
25
|
+
"accent-foreground": "0 0% 0%",
|
|
26
|
+
"destructive": "349 81% 50%",
|
|
27
|
+
"destructive-foreground": "0 0% 100%",
|
|
28
|
+
"border": "0 0% 78%",
|
|
29
|
+
"input": "0 0% 78%",
|
|
30
|
+
"ring": "161 89% 53%"
|
|
31
|
+
},
|
|
32
|
+
"dark": {
|
|
33
|
+
"background": "0 0% 0%",
|
|
34
|
+
"foreground": "0 0% 98%",
|
|
35
|
+
"card": "0 0% 12%",
|
|
36
|
+
"card-foreground": "0 0% 98%",
|
|
37
|
+
"popover": "0 0% 12%",
|
|
38
|
+
"popover-foreground": "0 0% 98%",
|
|
39
|
+
"primary": "161 89% 67%",
|
|
40
|
+
"primary-foreground": "0 0% 0%",
|
|
41
|
+
"secondary": "249 100% 73%",
|
|
42
|
+
"secondary-foreground": "0 0% 100%",
|
|
43
|
+
"muted": "0 0% 32%",
|
|
44
|
+
"muted-foreground": "0 0% 78%",
|
|
45
|
+
"accent": "161 89% 67%",
|
|
46
|
+
"accent-foreground": "0 0% 0%",
|
|
47
|
+
"destructive": "350 90% 57%",
|
|
48
|
+
"destructive-foreground": "0 0% 100%",
|
|
49
|
+
"border": "0 0% 44%",
|
|
50
|
+
"input": "0 0% 44%",
|
|
51
|
+
"ring": "161 89% 67%"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
{
|
|
56
|
+
"path": "components/ui/sapient-multiple-choice-list.tsx",
|
|
57
|
+
"type": "registry:ui",
|
|
58
|
+
"content": "'use client';\n\nimport React, { useState } from 'react';\nimport { cn } from '@/lib/utils';\nimport { MultipleChoiceCard } from './sapient-multiple-choice-card';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface MultipleChoiceOption {\n id: string;\n label?: string;\n imageSrc?: string;\n imageAlt?: string;\n title?: string;\n subtitle?: string;\n}\n\nexport interface MultipleChoiceListProps {\n className?: string;\n options?: MultipleChoiceOption[];\n /** Initially selected option id */\n defaultSelected?: string;\n onChange?: (selectedId: string) => void;\n}\n\n// ─── Default options ──────────────────────────────────────────────────────────\nconst DEFAULT_OPTIONS: MultipleChoiceOption[] = [\n { id: '1', label: 'LABEL' },\n { id: '2', label: 'LABEL' },\n { id: '3', label: 'LABEL' },\n];\n\n// ─── MultipleChoiceList ───────────────────────────────────────────────────────\n\nexport function MultipleChoiceList({\n className,\n options = DEFAULT_OPTIONS,\n defaultSelected,\n onChange,\n}: MultipleChoiceListProps) {\n const [selected, setSelected] = useState<string | undefined>(\n defaultSelected ?? options[0]?.id\n );\n\n const handleSelect = (id: string) => {\n setSelected(id);\n onChange?.(id);\n };\n\n return (\n <div\n className={cn(\n 'flex flex-col gap-[var(--spacing-m,16px)] items-start w-[353px]',\n className\n )}\n >\n {options.map((option) => (\n <MultipleChoiceCard\n key={option.id}\n style=\"image\"\n size=\"large\"\n selected={selected === option.id}\n label={option.label}\n imageSrc={option.imageSrc}\n imageAlt={option.imageAlt}\n onSelect={() => handleSelect(option.id)}\n className=\"w-full\"\n />\n ))}\n </div>\n );\n}\n\nexport default MultipleChoiceList;\n"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"registryDependencies": [
|
|
62
|
+
"@sapient/multiple-choice-card"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "news-card",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Sapient News Card",
|
|
6
|
+
"description": "Editorial news card in vertical and horizontal layouts.",
|
|
7
|
+
"cssVars": {
|
|
8
|
+
"theme": {
|
|
9
|
+
"radius": "0.5rem"
|
|
10
|
+
},
|
|
11
|
+
"light": {
|
|
12
|
+
"background": "0 0% 98%",
|
|
13
|
+
"foreground": "0 0% 12%",
|
|
14
|
+
"card": "0 0% 98%",
|
|
15
|
+
"card-foreground": "0 0% 12%",
|
|
16
|
+
"popover": "0 0% 100%",
|
|
17
|
+
"popover-foreground": "0 0% 12%",
|
|
18
|
+
"primary": "161 89% 53%",
|
|
19
|
+
"primary-foreground": "0 0% 0%",
|
|
20
|
+
"secondary": "249 89% 63%",
|
|
21
|
+
"secondary-foreground": "0 0% 100%",
|
|
22
|
+
"muted": "0 0% 88%",
|
|
23
|
+
"muted-foreground": "0 0% 44%",
|
|
24
|
+
"accent": "161 89% 53%",
|
|
25
|
+
"accent-foreground": "0 0% 0%",
|
|
26
|
+
"destructive": "349 81% 50%",
|
|
27
|
+
"destructive-foreground": "0 0% 100%",
|
|
28
|
+
"border": "0 0% 78%",
|
|
29
|
+
"input": "0 0% 78%",
|
|
30
|
+
"ring": "161 89% 53%"
|
|
31
|
+
},
|
|
32
|
+
"dark": {
|
|
33
|
+
"background": "0 0% 0%",
|
|
34
|
+
"foreground": "0 0% 98%",
|
|
35
|
+
"card": "0 0% 12%",
|
|
36
|
+
"card-foreground": "0 0% 98%",
|
|
37
|
+
"popover": "0 0% 12%",
|
|
38
|
+
"popover-foreground": "0 0% 98%",
|
|
39
|
+
"primary": "161 89% 67%",
|
|
40
|
+
"primary-foreground": "0 0% 0%",
|
|
41
|
+
"secondary": "249 100% 73%",
|
|
42
|
+
"secondary-foreground": "0 0% 100%",
|
|
43
|
+
"muted": "0 0% 32%",
|
|
44
|
+
"muted-foreground": "0 0% 78%",
|
|
45
|
+
"accent": "161 89% 67%",
|
|
46
|
+
"accent-foreground": "0 0% 0%",
|
|
47
|
+
"destructive": "350 90% 57%",
|
|
48
|
+
"destructive-foreground": "0 0% 100%",
|
|
49
|
+
"border": "0 0% 44%",
|
|
50
|
+
"input": "0 0% 44%",
|
|
51
|
+
"ring": "161 89% 67%"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
{
|
|
56
|
+
"path": "components/ui/sapient-news-card.tsx",
|
|
57
|
+
"type": "registry:ui",
|
|
58
|
+
"content": "'use client';\n\nimport React from 'react';\nimport { cn } from '@/lib/utils';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type NewsCardOrientation = 'vertical' | 'horizontal';\nexport type NewsCardState = 'enabled' | 'hover';\n\nexport interface NewsCardProps {\n className?: string;\n /** Image URL for the card thumbnail */\n imageSrc?: string;\n imageAlt?: string;\n /** Small badge shown above roofline. Violet pill with a live \"blink\" dot. */\n showBadge?: boolean;\n badgeLabel?: string;\n /** Category / section label above the headline */\n roofline?: string;\n headline?: string;\n showBody?: boolean;\n body?: string;\n datePosted?: string;\n /** Wraps content in a white card surface with rounded corners */\n background?: boolean;\n orientation?: NewsCardOrientation;\n state?: NewsCardState;\n}\n\n// ─── Blink Badge ──────────────────────────────────────────────────────────────\n// Matches the Figma \"Badge\" – violet pill with animated dot + label text.\nfunction BlinkBadge({ label = 'Label' }: { label?: string }) {\n return (\n <div className=\"inline-flex items-center gap-[var(--spacing-xs4,4px)] h-5 px-[var(--spacing-xs2,8px)] rounded-[var(--radius-token-full,100px)] bg-[hsl(var(--secondary-600))] shrink-0\">\n {/* Blink dot */}\n <span className=\"relative flex items-center justify-center size-3 shrink-0\">\n <span className=\"absolute inline-flex size-[9.6px] rounded-full bg-[hsl(var(--secondary-300))] opacity-75 animate-ping\" />\n <span className=\"relative inline-flex size-[4px] rounded-full bg-white\" />\n </span>\n <span className=\"text-[length:var(--text-label,12px)] leading-[var(--leading-label,18px)] font-[var(--font-weight-regular,400)] text-white whitespace-nowrap\">\n {label}\n </span>\n </div>\n );\n}\n\n// ─── Card Image ───────────────────────────────────────────────────────────────\nfunction CardImage({\n src,\n alt = '',\n className,\n}: {\n src: string;\n alt?: string;\n className?: string;\n}) {\n return (\n <div className={cn('relative overflow-hidden shrink-0', className)}>\n <img\n src={src}\n alt={alt}\n className=\"absolute inset-0 w-full h-full object-cover\"\n />\n </div>\n );\n}\n\n// ─── NewsCard ─────────────────────────────────────────────────────────────────\n\nconst PLACEHOLDER_IMAGE =\n 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=600&q=80';\n\nexport function NewsCard({\n className,\n imageSrc = PLACEHOLDER_IMAGE,\n imageAlt = '',\n showBadge = true,\n badgeLabel = 'Label',\n roofline = 'Roofline',\n headline = 'Headline lorem ipsum dolor sit amet, consectetur adipiscing elit',\n showBody = true,\n body = 'Body lorem ipsum dolor sit amet, consectetur adipiscing elit',\n datePosted = '1 Jan 2026',\n background = false,\n orientation = 'vertical',\n state = 'enabled',\n}: NewsCardProps) {\n const isHover = state === 'hover';\n const isVertical = orientation === 'vertical';\n const isHorizontal = orientation === 'horizontal';\n\n // Headline color: teal accent on hover, neutral-950 on enabled\n const headlineColor = isHover\n ? 'text-[hsl(var(--foreground-strong))]'\n : 'text-[hsl(var(--neutral-950))]';\n\n // ── VERTICAL ─────────────────────────────────────────────────────────────\n if (isVertical) {\n return (\n <div\n className={cn(\n 'flex flex-col items-start w-[256px]',\n background && 'overflow-hidden rounded-[var(--radius-token-lg,32px)]',\n className\n )}\n >\n {/* Image – full width, rounded top if no background, fully clipped if background */}\n <CardImage\n src={imageSrc}\n alt={imageAlt}\n className={cn(\n 'w-full h-[192px]',\n !background && 'rounded-[var(--radius-token-lg,32px)]'\n )}\n />\n\n {/* Text container */}\n <div\n className={cn(\n 'flex flex-col items-start w-full shrink-0',\n background\n ? 'bg-white gap-[var(--spacing-s,12px)] p-[var(--spacing-m,16px)]'\n : 'gap-[var(--spacing-s,12px)] py-[var(--spacing-m,16px)]'\n )}\n >\n {/* Subtitle row: badge + roofline */}\n <div className=\"flex flex-col items-start gap-[var(--spacing-xs2,8px)] w-full shrink-0\">\n {showBadge && <BlinkBadge label={badgeLabel} />}\n <p\n className={cn(\n 'text-[length:var(--text-label,12px)] leading-[var(--leading-label,18px)] w-full shrink-0 whitespace-pre-wrap',\n background\n ? 'font-[var(--font-weight-medium,500)] text-[hsl(var(--neutral-950))]'\n : 'font-[var(--font-weight-medium,500)] text-[hsl(var(--neutral-950))]'\n )}\n >\n {roofline}\n </p>\n </div>\n\n {/* Headline + body */}\n <div className=\"flex flex-col gap-[var(--spacing-xs2,8px)] items-start w-full shrink-0\">\n <p\n className={cn(\n 'text-[length:var(--text-subheading,20px)] leading-[var(--leading-subheading,24px)] font-[var(--font-weight-medium,500)] w-full overflow-hidden',\n headlineColor\n )}\n >\n {headline}\n </p>\n {showBody && (\n <p className=\"text-[length:var(--text-body-small,14px)] leading-[var(--leading-body-small,22px)] font-[var(--font-weight-regular,400)] text-[hsl(var(--neutral-600))] max-h-[44px] overflow-hidden w-full\">\n {body}\n </p>\n )}\n </div>\n\n {/* Date */}\n <p className=\"text-[length:var(--text-label,12px)] leading-[var(--leading-label,18px)] font-[var(--font-weight-regular,400)] text-[hsl(var(--neutral-950))] w-full whitespace-pre-wrap shrink-0\">\n {datePosted}\n </p>\n </div>\n </div>\n );\n }\n\n // ── HORIZONTAL ───────────────────────────────────────────────────────────\n return (\n <div\n className={cn(\n 'flex items-stretch w-[441px]',\n background\n ? 'overflow-hidden rounded-[var(--radius-token-md,16px)]'\n : 'flex-row',\n // For non-background enabled: image on right, row layout\n // For hover / background: always row\n className\n )}\n >\n {/* Text container – left side */}\n <div\n className={cn(\n 'flex flex-col flex-1 min-w-0 gap-[var(--spacing-xs2,8px)] justify-center py-[var(--spacing-xs2,8px)]',\n background\n ? 'bg-white px-[var(--spacing-m,16px)]'\n : 'pr-[var(--spacing-m,16px)]'\n )}\n >\n {/* Subtitle row: badge + roofline inline */}\n <div className=\"flex items-center gap-[var(--spacing-xs2,8px)] w-full shrink-0\">\n {showBadge && <BlinkBadge label={badgeLabel} />}\n <p className=\"flex-1 min-w-0 text-[length:var(--text-label,12px)] leading-[var(--leading-label,18px)] font-[var(--font-weight-medium,500)] text-[hsl(var(--neutral-950))] overflow-hidden text-ellipsis whitespace-pre-wrap max-h-5\">\n {roofline}\n </p>\n </div>\n\n {/* Headline */}\n <p\n className={cn(\n 'text-[length:var(--text-body-small,14px)] leading-[var(--leading-body-small,22px)] font-[var(--font-weight-medium,500)] overflow-hidden text-ellipsis w-full whitespace-pre-wrap shrink-0',\n headlineColor\n )}\n >\n {headline}\n </p>\n\n {/* Date */}\n <p className=\"text-[length:var(--text-label,12px)] leading-[var(--leading-label,18px)] font-[var(--font-weight-regular,400)] text-[hsl(var(--neutral-950))] w-full whitespace-pre-wrap shrink-0\">\n {datePosted}\n </p>\n </div>\n\n {/* Image – right side, fixed size */}\n <CardImage\n src={imageSrc}\n alt={imageAlt}\n className={cn(\n 'w-[160px] h-[116px] shrink-0',\n !background && 'rounded-[var(--radius-token-sm,8px)]'\n )}\n />\n </div>\n );\n}\n\nexport default NewsCard;\n"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "privacy-consent",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Sapient Privacy Consent",
|
|
6
|
+
"description": "Consent capture card with agreement toggles and confirm or cancel actions.",
|
|
7
|
+
"cssVars": {
|
|
8
|
+
"theme": {
|
|
9
|
+
"radius": "0.5rem"
|
|
10
|
+
},
|
|
11
|
+
"light": {
|
|
12
|
+
"background": "0 0% 98%",
|
|
13
|
+
"foreground": "0 0% 12%",
|
|
14
|
+
"card": "0 0% 98%",
|
|
15
|
+
"card-foreground": "0 0% 12%",
|
|
16
|
+
"popover": "0 0% 100%",
|
|
17
|
+
"popover-foreground": "0 0% 12%",
|
|
18
|
+
"primary": "161 89% 53%",
|
|
19
|
+
"primary-foreground": "0 0% 0%",
|
|
20
|
+
"secondary": "249 89% 63%",
|
|
21
|
+
"secondary-foreground": "0 0% 100%",
|
|
22
|
+
"muted": "0 0% 88%",
|
|
23
|
+
"muted-foreground": "0 0% 44%",
|
|
24
|
+
"accent": "161 89% 53%",
|
|
25
|
+
"accent-foreground": "0 0% 0%",
|
|
26
|
+
"destructive": "349 81% 50%",
|
|
27
|
+
"destructive-foreground": "0 0% 100%",
|
|
28
|
+
"border": "0 0% 78%",
|
|
29
|
+
"input": "0 0% 78%",
|
|
30
|
+
"ring": "161 89% 53%"
|
|
31
|
+
},
|
|
32
|
+
"dark": {
|
|
33
|
+
"background": "0 0% 0%",
|
|
34
|
+
"foreground": "0 0% 98%",
|
|
35
|
+
"card": "0 0% 12%",
|
|
36
|
+
"card-foreground": "0 0% 98%",
|
|
37
|
+
"popover": "0 0% 12%",
|
|
38
|
+
"popover-foreground": "0 0% 98%",
|
|
39
|
+
"primary": "161 89% 67%",
|
|
40
|
+
"primary-foreground": "0 0% 0%",
|
|
41
|
+
"secondary": "249 100% 73%",
|
|
42
|
+
"secondary-foreground": "0 0% 100%",
|
|
43
|
+
"muted": "0 0% 32%",
|
|
44
|
+
"muted-foreground": "0 0% 78%",
|
|
45
|
+
"accent": "161 89% 67%",
|
|
46
|
+
"accent-foreground": "0 0% 0%",
|
|
47
|
+
"destructive": "350 90% 57%",
|
|
48
|
+
"destructive-foreground": "0 0% 100%",
|
|
49
|
+
"border": "0 0% 44%",
|
|
50
|
+
"input": "0 0% 44%",
|
|
51
|
+
"ring": "161 89% 67%"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
{
|
|
56
|
+
"path": "components/ui/sapient-privacy-consent.tsx",
|
|
57
|
+
"type": "registry:ui",
|
|
58
|
+
"content": "'use client';\n\nimport React, { useState } from 'react';\nimport { cn } from '@/lib/utils';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type ConsentValue = 'agree' | 'disagree' | null;\n\nexport interface ConsentQuestion {\n id: string;\n text: string;\n /** Label for the \"yes\" option — defaults to \"I agree\" */\n agreeLabel?: string;\n /** Label for the \"no\" option — defaults to \"I do not agree\" */\n disagreeLabel?: string;\n}\n\nexport interface PrivacyConsentProps {\n className?: string;\n /** Card heading */\n title?: string;\n /** The three (or more) consent questions */\n questions?: ConsentQuestion[];\n /** Called when \"Confirm\" is clicked — receives map of questionId → value */\n onConfirm?: (values: Record<string, ConsentValue>) => void;\n /** Called when \"Cancel\" is clicked */\n onCancel?: () => void;\n confirmLabel?: string;\n cancelLabel?: string;\n}\n\n// ─── Default questions matching Figma ────────────────────────────────────────\nconst DEFAULT_QUESTIONS: ConsentQuestion[] = [\n {\n id: 'q1',\n text: 'I agree to receive news and promotions from [Company Name]. Read more',\n },\n {\n id: 'q2',\n text: 'I agree that my data may be used for personalised promotions. Read more',\n },\n {\n id: 'q3',\n text: 'I agree that my data may be shared with [Company Name] partners. Read more',\n },\n];\n\n// ─── Inline radio option (circle + label) ────────────────────────────────────\nfunction ConsentRadio({\n label,\n checked,\n onChange,\n}: {\n label: string;\n checked: boolean;\n onChange: () => void;\n}) {\n return (\n <button\n type=\"button\"\n role=\"radio\"\n aria-checked={checked}\n onClick={onChange}\n className=\"inline-flex items-center gap-[var(--spacing-m,16px)] shrink-0 group\"\n >\n {/* 16px radio circle */}\n <span className=\"relative inline-flex items-center justify-center size-4 shrink-0\">\n <span\n className={cn(\n 'absolute size-4 rounded-full border-2 transition-colors',\n checked\n ? 'border-[hsl(var(--secondary-600))]'\n : 'border-[hsl(var(--neutral-400))] group-hover:border-[hsl(var(--neutral-600))]'\n )}\n />\n {checked && (\n <span className=\"absolute size-[8px] rounded-full bg-[hsl(var(--secondary-600))]\" />\n )}\n </span>\n <span\n className={cn(\n 'text-[length:var(--text-body,16px)] leading-[var(--leading-body,24px)] font-[var(--font-weight-regular,400)] whitespace-nowrap transition-colors',\n checked\n ? 'text-[hsl(var(--neutral-950))]'\n : 'text-[hsl(var(--neutral-950))]'\n )}\n >\n {label}\n </span>\n </button>\n );\n}\n\n// ─── Single consent question row ─────────────────────────────────────────────\nfunction ConsentRow({\n question,\n value,\n onChange,\n}: {\n question: ConsentQuestion;\n value: ConsentValue;\n onChange: (v: ConsentValue) => void;\n}) {\n return (\n <div className=\"flex flex-col gap-[var(--spacing-xs2,8px)] items-start w-full shrink-0\">\n {/* Question text */}\n <p className=\"text-[length:var(--text-body,16px)] leading-[var(--leading-body,24px)] font-[var(--font-weight-regular,400)] text-[hsl(var(--neutral-950))] w-full whitespace-pre-wrap\">\n {question.text}\n </p>\n {/* Radio pair */}\n <div className=\"flex gap-[var(--spacing-xl,32px)] items-center\">\n <ConsentRadio\n label={question.agreeLabel ?? 'I agree'}\n checked={value === 'agree'}\n onChange={() => onChange(value === 'agree' ? null : 'agree')}\n />\n <ConsentRadio\n label={question.disagreeLabel ?? 'I do not agree'}\n checked={value === 'disagree'}\n onChange={() => onChange(value === 'disagree' ? null : 'disagree')}\n />\n </div>\n </div>\n );\n}\n\n// ─── PrivacyConsent ───────────────────────────────────────────────────────────\n\nexport function PrivacyConsent({\n className,\n title = 'Consent for marketing purposes',\n questions = DEFAULT_QUESTIONS,\n onConfirm,\n onCancel,\n confirmLabel = 'Confirm',\n cancelLabel = 'Cancel',\n}: PrivacyConsentProps) {\n // Initialise all answers to null\n const [values, setValues] = useState<Record<string, ConsentValue>>(\n () => Object.fromEntries(questions.map((q) => [q.id, null]))\n );\n\n const handleChange = (id: string, v: ConsentValue) => {\n setValues((prev) => ({ ...prev, [id]: v }));\n };\n\n return (\n <div\n className={cn(\n 'flex flex-col gap-[var(--spacing-xl2,40px)] items-center overflow-hidden rounded-[var(--radius-token-lg,32px)] p-[var(--spacing-l,24px)] w-[353px] bg-white',\n className\n )}\n >\n {/* ── Content area ── */}\n <div className=\"flex flex-col gap-[24px] items-start w-full shrink-0\">\n {/* Heading */}\n <p className=\"text-[length:var(--text-body,16px)] leading-[var(--leading-body,24px)] font-[var(--font-weight-medium,500)] text-[hsl(var(--neutral-950))] w-full whitespace-pre-wrap\">\n {title}\n </p>\n\n {/* Question rows */}\n {questions.map((q) => (\n <ConsentRow\n key={q.id}\n question={q}\n value={values[q.id] ?? null}\n onChange={(v) => handleChange(q.id, v)}\n />\n ))}\n </div>\n\n {/* ── Button row ── */}\n <div className=\"flex gap-[var(--spacing-m,16px)] items-center w-full shrink-0\">\n {/* Cancel — secondary outlined pill */}\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"flex-1 inline-flex items-center justify-center px-[var(--spacing-l,24px)] py-[var(--spacing-s,12px)] rounded-[var(--radius-token-full,100px)] border border-[hsl(var(--neutral-600))] bg-transparent text-[length:var(--text-body,16px)] leading-[var(--leading-body,24px)] font-[var(--font-weight-medium,500)] text-[hsl(var(--neutral-950))] whitespace-nowrap transition-colors hover:bg-[hsl(var(--state-hover))] active:bg-[hsl(var(--state-pressed))]\"\n >\n {cancelLabel}\n </button>\n\n {/* Confirm — primary teal pill */}\n <button\n type=\"button\"\n onClick={() => onConfirm?.(values)}\n className=\"flex-1 inline-flex items-center justify-center px-[var(--spacing-l,24px)] py-[var(--spacing-s,12px)] rounded-[var(--radius-token-full,100px)] bg-[hsl(var(--primary-500))] text-[length:var(--text-body,16px)] leading-[var(--leading-body,24px)] font-[var(--font-weight-medium,500)] text-[hsl(var(--neutral-950))] whitespace-nowrap transition-colors hover:bg-[hsl(var(--primary-500)/0.9)] active:bg-[hsl(var(--primary-500)/0.8)]\"\n >\n {confirmLabel}\n </button>\n </div>\n </div>\n );\n}\n\nexport default PrivacyConsent;\n"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "product-card",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Sapient Product Card",
|
|
6
|
+
"description": "Commerce-focused product cards with no-image, full-image, and split layouts.",
|
|
7
|
+
"cssVars": {
|
|
8
|
+
"theme": {
|
|
9
|
+
"radius": "0.5rem"
|
|
10
|
+
},
|
|
11
|
+
"light": {
|
|
12
|
+
"background": "0 0% 98%",
|
|
13
|
+
"foreground": "0 0% 12%",
|
|
14
|
+
"card": "0 0% 98%",
|
|
15
|
+
"card-foreground": "0 0% 12%",
|
|
16
|
+
"popover": "0 0% 100%",
|
|
17
|
+
"popover-foreground": "0 0% 12%",
|
|
18
|
+
"primary": "161 89% 53%",
|
|
19
|
+
"primary-foreground": "0 0% 0%",
|
|
20
|
+
"secondary": "249 89% 63%",
|
|
21
|
+
"secondary-foreground": "0 0% 100%",
|
|
22
|
+
"muted": "0 0% 88%",
|
|
23
|
+
"muted-foreground": "0 0% 44%",
|
|
24
|
+
"accent": "161 89% 53%",
|
|
25
|
+
"accent-foreground": "0 0% 0%",
|
|
26
|
+
"destructive": "349 81% 50%",
|
|
27
|
+
"destructive-foreground": "0 0% 100%",
|
|
28
|
+
"border": "0 0% 78%",
|
|
29
|
+
"input": "0 0% 78%",
|
|
30
|
+
"ring": "161 89% 53%"
|
|
31
|
+
},
|
|
32
|
+
"dark": {
|
|
33
|
+
"background": "0 0% 0%",
|
|
34
|
+
"foreground": "0 0% 98%",
|
|
35
|
+
"card": "0 0% 12%",
|
|
36
|
+
"card-foreground": "0 0% 98%",
|
|
37
|
+
"popover": "0 0% 12%",
|
|
38
|
+
"popover-foreground": "0 0% 98%",
|
|
39
|
+
"primary": "161 89% 67%",
|
|
40
|
+
"primary-foreground": "0 0% 0%",
|
|
41
|
+
"secondary": "249 100% 73%",
|
|
42
|
+
"secondary-foreground": "0 0% 100%",
|
|
43
|
+
"muted": "0 0% 32%",
|
|
44
|
+
"muted-foreground": "0 0% 78%",
|
|
45
|
+
"accent": "161 89% 67%",
|
|
46
|
+
"accent-foreground": "0 0% 0%",
|
|
47
|
+
"destructive": "350 90% 57%",
|
|
48
|
+
"destructive-foreground": "0 0% 100%",
|
|
49
|
+
"border": "0 0% 44%",
|
|
50
|
+
"input": "0 0% 44%",
|
|
51
|
+
"ring": "161 89% 67%"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
{
|
|
56
|
+
"path": "components/ui/sapient-product-card.tsx",
|
|
57
|
+
"type": "registry:ui",
|
|
58
|
+
"content": "'use client';\n\nimport React from 'react';\nimport { cn } from '@/lib/utils';\nimport { SapientIcon } from '@/components/ui/sapient-icon';\n\n// ─── Icon helper ─────────────────────────────────────────────────────────────\nfunction Icon({\n name,\n size = 24,\n className,\n alt = '',\n}: {\n name: string;\n size?: number;\n className?: string;\n alt?: string;\n}) {\n return (\n <SapientIcon\n name={name}\n label={alt}\n size={size}\n className={className}\n />\n );\n}\n\n// ─── Badge ────────────────────────────────────────────────────────────────────\nfunction Badge({ label = 'Label' }: { label?: string }) {\n return (\n <div className=\"inline-flex items-center gap-[var(--spacing-xs4,4px)] h-5 px-[var(--spacing-xs2,8px)] rounded-[var(--radius-token-full,100px)] bg-[hsl(var(--secondary-600))]\">\n <span className=\"text-[length:var(--text-label,12px)] leading-[var(--leading-label,18px)] font-[var(--font-weight-regular,400)] text-white whitespace-nowrap\">\n {label}\n </span>\n </div>\n );\n}\n\n// ─── Price row ────────────────────────────────────────────────────────────────\nfunction PriceRow({\n price = '1.234€',\n showInfo = true,\n inverted = false,\n}: {\n price?: string;\n showInfo?: boolean;\n inverted?: boolean;\n}) {\n return (\n <div className=\"flex items-center gap-[var(--spacing-xs3,6px)] w-full\">\n <span\n className={cn(\n 'text-[length:var(--text-subheading,20px)] leading-[var(--leading-subheading,24px)] font-[var(--font-weight-medium,500)] whitespace-nowrap',\n inverted ? 'text-white' : 'text-[hsl(var(--neutral-950))]'\n )}\n >\n {price}\n </span>\n {showInfo && (\n <Icon\n name=\"info-circle\"\n size={24}\n alt=\"Price info\"\n className={cn(\n 'opacity-70',\n inverted ? 'brightness-0 invert' : ''\n )}\n />\n )}\n </div>\n );\n}\n\n// ─── Expand/Zoom icon button ──────────────────────────────────────────────────\nfunction ZoomButton({ inverted = false }: { inverted?: boolean }) {\n return (\n <button\n type=\"button\"\n aria-label=\"Expand\"\n className={cn(\n 'inline-flex items-center justify-center shrink-0 size-[var(--sizing-s,40px)] rounded-[var(--radius-token-full,100px)] border backdrop-blur-[12px] transition-colors',\n inverted\n ? 'border-white bg-white/10 text-white hover:bg-white/20'\n : 'border-[hsl(var(--neutral-800))] bg-white/10 text-[hsl(var(--neutral-950))] hover:bg-[hsl(var(--state-hover))]'\n )}\n >\n <Icon name=\"expand-05\" size={24} alt=\"Expand\" className={cn(inverted ? 'brightness-0 invert' : '')} />\n </button>\n );\n}\n\n// ─── Card image ───────────────────────────────────────────────────────────────\nfunction CardImage({\n src,\n alt = '',\n aspectClass,\n className,\n}: {\n src: string;\n alt?: string;\n aspectClass?: string;\n className?: string;\n}) {\n return (\n <div className={cn('relative w-full overflow-hidden', aspectClass, className)}>\n <img\n src={src}\n alt={alt}\n className=\"absolute inset-0 w-full h-full object-cover\"\n />\n </div>\n );\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type ImageRatio = '4:5' | '4:3' | '16:9';\n\n/** Aspect-ratio utility classes keyed by ratio */\nconst ASPECT_CLASS: Record<ImageRatio, string> = {\n '16:9': 'aspect-video', // 16/9\n '4:3': 'aspect-[4/3]',\n '4:5': 'aspect-[4/5]',\n};\n\n/** Card width per ratio (mirrors Figma) */\nconst CARD_WIDTH: Record<ImageRatio, string> = {\n '16:9': 'w-[353px]',\n '4:3': 'w-[353px]',\n '4:5': 'w-[353px]',\n};\n\n// ─── Variant A: No image (info card) ─────────────────────────────────────────\n\nexport interface ProductCardNoImageProps {\n className?: string;\n headline?: string;\n body?: string;\n price?: string;\n badgeLabel?: string;\n imageRatio?: ImageRatio;\n showBadge?: boolean;\n showBody?: boolean;\n showPrice?: boolean;\n showPriceInfo?: boolean;\n /** When false renders without background (ghost) */\n withBackground?: boolean;\n}\n\n/**\n * **Product Card – No Image**\n *\n * Displays product info text and price without a hero image.\n * Supports 4:5, 4:3, and 16:9 layouts.\n */\nexport function ProductCardNoImage({\n className,\n headline = 'Headline lorem ipsum',\n body = 'Body lorem ipsum dolor sit amet, consectetur adipiscing',\n price = '1.234€',\n badgeLabel = 'Label',\n imageRatio = '4:3',\n showBadge = true,\n showBody = true,\n showPrice = true,\n showPriceInfo = true,\n withBackground = true,\n}: ProductCardNoImageProps) {\n const is45 = imageRatio === '4:5';\n\n return (\n <div\n className={cn(\n 'flex flex-col items-start p-[var(--spacing-l,24px)] rounded-[var(--radius-token-lg,32px)] relative',\n CARD_WIDTH[imageRatio],\n withBackground && 'bg-white',\n // height\n is45 ? 'h-[442px] justify-between' : 'gap-[var(--spacing-l,24px)]',\n imageRatio === '4:3' && 'h-[283px] justify-between',\n className\n )}\n >\n {/* Content up */}\n <div\n className={cn(\n 'flex flex-col items-start w-full',\n is45 ? 'gap-[var(--spacing-m,16px)] shrink-0' : 'gap-[var(--spacing-l,24px)] flex-1 min-h-0'\n )}\n >\n {showBadge && <Badge label={badgeLabel} />}\n\n {/* Heading + body – only for 4:3 and 16:9 */}\n {!is45 && (\n <div className=\"flex flex-col gap-[var(--spacing-xs2,8px)] items-start w-full\">\n <p className=\"text-[length:var(--text-subheading,20px)] leading-[var(--leading-subheading,24px)] font-[var(--font-weight-medium,500)] text-[hsl(var(--neutral-950))] w-full overflow-hidden text-ellipsis whitespace-pre-wrap\">\n {headline}\n </p>\n {showBody && (\n <p className=\"text-[length:var(--text-body-small,14px)] leading-[var(--leading-body-small,22px)] font-[var(--font-weight-regular,400)] text-[hsl(var(--neutral-600))] w-full overflow-hidden text-ellipsis whitespace-pre-wrap\">\n {body}\n </p>\n )}\n </div>\n )}\n\n {/* Headline only for 4:5 */}\n {is45 && (\n <p className=\"text-[length:var(--text-subheading,20px)] leading-[var(--leading-subheading,24px)] font-[var(--font-weight-medium,500)] text-[hsl(var(--neutral-950))] w-full overflow-hidden text-ellipsis whitespace-pre-wrap\">\n {headline}\n </p>\n )}\n </div>\n\n {/* Price */}\n {showPrice && (\n <PriceRow price={price} showInfo={showPriceInfo} />\n )}\n\n {/* Icon button – top-right (only 4:3, 16:9) */}\n {!is45 && (\n <div className=\"absolute top-4 right-4\">\n <ZoomButton />\n </div>\n )}\n </div>\n );\n}\n\n// ─── Variant B: Full image (image fills the whole card) ───────────────────────\n\nexport interface ProductCardFullImageProps {\n className?: string;\n imageSrc: string;\n imageAlt?: string;\n headline?: string;\n body?: string;\n price?: string;\n badgeLabel?: string;\n imageRatio?: ImageRatio;\n showBadge?: boolean;\n showBody?: boolean;\n showPrice?: boolean;\n showPriceInfo?: boolean;\n showZoomButton?: boolean;\n state?: 'enabled' | 'hover';\n}\n\n/**\n * **Product Card – Full Image**\n *\n * Hero image fills the entire card. Text is overlaid at the bottom.\n * Supports 4:5, 4:3, and 16:9 ratios.\n */\nexport function ProductCardFullImage({\n className,\n imageSrc,\n imageAlt = '',\n headline = 'Headline lorem ipsum',\n body = 'Body lorem ipsum dolor sit amet, consectetur',\n price = '1.234€',\n badgeLabel = 'Label',\n imageRatio = '4:5',\n showBadge = true,\n showBody = true,\n showPrice = true,\n showPriceInfo = true,\n showZoomButton = true,\n state = 'enabled',\n}: ProductCardFullImageProps) {\n const is169 = imageRatio === '16:9';\n const is45 = imageRatio === '4:5';\n const showTextOverlay = is45 || imageRatio === '4:3';\n\n return (\n <div\n className={cn(\n 'relative flex flex-col items-start overflow-hidden rounded-[var(--radius-token-lg,32px)]',\n CARD_WIDTH[imageRatio],\n ASPECT_CLASS[imageRatio],\n className\n )}\n >\n {/* Full-bleed image */}\n <img\n src={imageSrc}\n alt={imageAlt}\n className=\"absolute inset-0 w-full h-full object-cover\"\n />\n\n {/* Top gradient + badge/zoom overlay */}\n <div\n className={cn(\n 'absolute inset-x-0 top-0 flex items-start justify-between px-[var(--spacing-l,24px)] pt-[var(--spacing-l,24px)] pb-[var(--spacing-xl,32px)]',\n !is169 && 'bg-gradient-to-b from-black/15 to-transparent'\n )}\n >\n {showBadge && <Badge label={badgeLabel} />}\n {(is45 || imageRatio === '4:3') && showZoomButton && (\n <ZoomButton inverted />\n )}\n </div>\n\n {/* Bottom text overlay – 4:5 and 4:3 */}\n {showTextOverlay && (\n <div className=\"absolute inset-x-0 bottom-0 bg-gradient-to-b from-transparent to-black/30 px-[var(--spacing-l,24px)] pt-[var(--spacing-xl2,40px)] pb-[var(--spacing-l,24px)] flex flex-col gap-[var(--spacing-xs2,8px)]\">\n <p\n className={cn(\n 'text-[length:var(--text-subheading,20px)] leading-[var(--leading-subheading,24px)] font-[var(--font-weight-medium,500)] text-white w-full overflow-hidden text-ellipsis whitespace-pre-wrap',\n state === 'hover' && 'text-[hsl(var(--foreground-strong))]'\n )}\n >\n {headline}\n </p>\n {showBody && (\n <p className=\"text-[length:var(--text-body-small,14px)] leading-[var(--leading-body-small,22px)] font-[var(--font-weight-regular,400)] text-white max-h-[44px] overflow-hidden text-ellipsis whitespace-pre-wrap w-full\">\n {body}\n </p>\n )}\n {showPrice && (\n <PriceRow price={price} showInfo={showPriceInfo} inverted />\n )}\n </div>\n )}\n\n {/* Bottom text overlay – 16:9 */}\n {is169 && (\n <div className=\"absolute inset-x-0 bottom-0 bg-gradient-to-b from-transparent to-black/30 px-[var(--spacing-l,24px)] pt-[var(--spacing-xl2,40px)] pb-[var(--spacing-l,24px)] flex flex-col gap-[var(--spacing-xs2,8px)]\">\n <p\n className={cn(\n 'text-[length:var(--text-subheading,20px)] leading-[var(--leading-subheading,24px)] font-[var(--font-weight-medium,500)] text-white w-full overflow-hidden text-ellipsis whitespace-pre-wrap',\n state === 'hover' && 'text-[hsl(var(--foreground-strong))]'\n )}\n >\n {headline}\n </p>\n {showPrice && (\n <PriceRow price={price} showInfo={showPriceInfo} inverted />\n )}\n </div>\n )}\n </div>\n );\n}\n\n// ─── Variant C: Split content (image top + text bottom) ───────────────────────\n\nexport interface ProductCardSplitProps {\n className?: string;\n imageSrc: string;\n imageAlt?: string;\n headline?: string;\n body?: string;\n price?: string;\n badgeLabel?: string;\n showBadge?: boolean;\n showBody?: boolean;\n showImage?: boolean;\n showPrice?: boolean;\n showPriceInfo?: boolean;\n showZoomButton?: boolean;\n /** Wraps content in a white card background */\n withBackground?: boolean;\n state?: 'enabled' | 'hover';\n}\n\n/**\n * **Product Card – Split Content**\n *\n * Image on top, text panel below. Comes in two layouts:\n * - `withBackground=true` → rounded card with white text panel\n * - `withBackground=false` → transparent, text directly below image\n */\nexport function ProductCardSplit({\n className,\n imageSrc,\n imageAlt = '',\n headline = 'Headline lorem ipsum dolor sit amet, consectetur adipiscing elit',\n body = 'Body lorem ipsum dolor sit amet, consectetur adipiscing elit',\n price = '1.234€',\n badgeLabel = 'Label',\n showBadge = true,\n showBody = true,\n showImage = true,\n showPrice = true,\n showPriceInfo = true,\n showZoomButton = true,\n withBackground = true,\n state = 'enabled',\n}: ProductCardSplitProps) {\n const isHover = state === 'hover';\n\n return (\n <div\n className={cn(\n 'flex flex-col items-start w-[353px] relative',\n withBackground && 'overflow-hidden rounded-[var(--radius-token-lg,32px)]',\n className\n )}\n >\n {/* Image section */}\n {showImage && (\n <div\n className={cn(\n 'relative w-full overflow-hidden shrink-0',\n withBackground\n ? 'rounded-tl-[var(--radius-token-lg,32px)] rounded-tr-[var(--radius-token-lg,32px)]'\n : 'rounded-[var(--radius-token-lg,32px)]',\n 'aspect-[4/3]'\n )}\n >\n <img\n src={imageSrc}\n alt={imageAlt}\n className=\"absolute inset-0 w-full h-full object-cover\"\n />\n\n {/* Top overlay: badge + zoom */}\n <div className=\"absolute inset-x-0 top-0 flex items-start justify-between px-[var(--spacing-l,24px)] pt-[var(--spacing-l,24px)] pb-[var(--spacing-xl,32px)] bg-gradient-to-b from-black/15 to-transparent\">\n {showBadge && <Badge label={badgeLabel} />}\n {showZoomButton && <ZoomButton inverted />}\n </div>\n </div>\n )}\n\n {/* Text section */}\n <div\n className={cn(\n 'flex flex-col gap-[var(--spacing-xs2,8px)] items-start w-full shrink-0',\n withBackground\n ? 'bg-white p-[var(--spacing-l,24px)]'\n : 'py-[var(--spacing-l,24px)]'\n )}\n >\n <p\n className={cn(\n 'text-[length:var(--text-subheading,20px)] leading-[var(--leading-subheading,24px)] font-[var(--font-weight-medium,500)] w-full overflow-hidden text-ellipsis whitespace-pre-wrap',\n isHover\n ? 'text-[hsl(var(--foreground-strong))]'\n : 'text-[hsl(var(--neutral-950))]'\n )}\n >\n {headline}\n </p>\n\n {showBody && (\n <p className=\"text-[length:var(--text-body-small,14px)] leading-[var(--leading-body-small,22px)] font-[var(--font-weight-regular,400)] text-[hsl(var(--neutral-600))] max-h-[44px] overflow-hidden text-ellipsis whitespace-pre-wrap w-full\">\n {body}\n </p>\n )}\n\n {showPrice && (\n <PriceRow price={price} showInfo={showPriceInfo} />\n )}\n </div>\n </div>\n );\n}\n\n// ─── Combined export (convenience) ────────────────────────────────────────────\n\nexport const ProductCard = {\n NoImage: ProductCardNoImage,\n FullImage: ProductCardFullImage,\n Split: ProductCardSplit,\n};\n\nexport default ProductCard;\n"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"registryDependencies": [
|
|
62
|
+
"@sapient/button"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "profile-card",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Sapient Profile Card",
|
|
6
|
+
"description": "Profile and contact card in large, medium, and compact variants.",
|
|
7
|
+
"cssVars": {
|
|
8
|
+
"theme": {
|
|
9
|
+
"radius": "0.5rem"
|
|
10
|
+
},
|
|
11
|
+
"light": {
|
|
12
|
+
"background": "0 0% 98%",
|
|
13
|
+
"foreground": "0 0% 12%",
|
|
14
|
+
"card": "0 0% 98%",
|
|
15
|
+
"card-foreground": "0 0% 12%",
|
|
16
|
+
"popover": "0 0% 100%",
|
|
17
|
+
"popover-foreground": "0 0% 12%",
|
|
18
|
+
"primary": "161 89% 53%",
|
|
19
|
+
"primary-foreground": "0 0% 0%",
|
|
20
|
+
"secondary": "249 89% 63%",
|
|
21
|
+
"secondary-foreground": "0 0% 100%",
|
|
22
|
+
"muted": "0 0% 88%",
|
|
23
|
+
"muted-foreground": "0 0% 44%",
|
|
24
|
+
"accent": "161 89% 53%",
|
|
25
|
+
"accent-foreground": "0 0% 0%",
|
|
26
|
+
"destructive": "349 81% 50%",
|
|
27
|
+
"destructive-foreground": "0 0% 100%",
|
|
28
|
+
"border": "0 0% 78%",
|
|
29
|
+
"input": "0 0% 78%",
|
|
30
|
+
"ring": "161 89% 53%"
|
|
31
|
+
},
|
|
32
|
+
"dark": {
|
|
33
|
+
"background": "0 0% 0%",
|
|
34
|
+
"foreground": "0 0% 98%",
|
|
35
|
+
"card": "0 0% 12%",
|
|
36
|
+
"card-foreground": "0 0% 98%",
|
|
37
|
+
"popover": "0 0% 12%",
|
|
38
|
+
"popover-foreground": "0 0% 98%",
|
|
39
|
+
"primary": "161 89% 67%",
|
|
40
|
+
"primary-foreground": "0 0% 0%",
|
|
41
|
+
"secondary": "249 100% 73%",
|
|
42
|
+
"secondary-foreground": "0 0% 100%",
|
|
43
|
+
"muted": "0 0% 32%",
|
|
44
|
+
"muted-foreground": "0 0% 78%",
|
|
45
|
+
"accent": "161 89% 67%",
|
|
46
|
+
"accent-foreground": "0 0% 0%",
|
|
47
|
+
"destructive": "350 90% 57%",
|
|
48
|
+
"destructive-foreground": "0 0% 100%",
|
|
49
|
+
"border": "0 0% 44%",
|
|
50
|
+
"input": "0 0% 44%",
|
|
51
|
+
"ring": "161 89% 67%"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
{
|
|
56
|
+
"path": "components/ui/sapient-profile-card.tsx",
|
|
57
|
+
"type": "registry:ui",
|
|
58
|
+
"content": "'use client';\n\nimport React from 'react';\nimport { cn } from '@/lib/utils';\nimport { SapientIcon } from '@/components/ui/sapient-icon';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/**\n * L – Full-bleed portrait photo card (353 × 441 px), white text overlaid at bottom.\n * M – White card with circular avatar, dark text name + contacts below.\n * S – White card, no photo, name + contacts only (compact).\n */\nexport type ProfileCardSize = 'L' | 'M' | 'S';\n\nexport interface ProfileCardProps {\n className?: string;\n size?: ProfileCardSize;\n /** First name (L/M show on separate line from surname) */\n firstName?: string;\n /** Last name */\n lastName?: string;\n /** Full name string – used by S (single line) */\n fullName?: string;\n email?: string;\n phone?: string;\n address?: string;\n /** Photo URL. Used in L (full-bleed) and M (circular avatar). */\n imageSrc?: string;\n imageAlt?: string;\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nconst PLACEHOLDER_PHOTO =\n 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=800&q=80';\n\nfunction ContactRow({\n iconName,\n label,\n inverted = false,\n alignTop = false,\n}: {\n iconName: string;\n label: string;\n inverted?: boolean;\n alignTop?: boolean;\n}) {\n return (\n <div\n className={cn(\n 'flex gap-[var(--spacing-xs2,8px)] w-full shrink-0',\n alignTop ? 'items-start' : 'items-center'\n )}\n >\n <SapientIcon\n name={iconName}\n label=\"\"\n size={24}\n className={cn(\n 'size-6 shrink-0',\n inverted && 'brightness-0 invert'\n )}\n />\n <p\n className={cn(\n 'text-[length:var(--text-body,16px)] leading-[1.4] font-[var(--font-weight-regular,400)] shrink-0',\n inverted\n ? 'text-white'\n : 'text-[hsl(var(--neutral-950))]'\n )}\n >\n {label}\n </p>\n </div>\n );\n}\n\n// ─── ProfileCard ──────────────────────────────────────────────────────────────\n\nexport function ProfileCard({\n className,\n size = 'L',\n firstName = 'Name',\n lastName = 'Surname',\n fullName = 'Name Surname',\n email = 'name.surname@email.com',\n phone = '+00 123 456 7890',\n address = 'Lorem Ipsum Street, 20154, Milan, Italy',\n imageSrc = PLACEHOLDER_PHOTO,\n imageAlt = '',\n}: ProfileCardProps) {\n\n // ── Size L ───────────────────────────────────────────────────────────────\n if (size === 'L') {\n return (\n <div\n className={cn(\n 'relative w-[353px] h-[441px] overflow-hidden rounded-[var(--radius-token-lg,32px)] flex flex-col justify-end',\n className\n )}\n >\n {/* Full-bleed photo */}\n <img\n src={imageSrc}\n alt={imageAlt}\n className=\"absolute inset-0 w-full h-full object-cover\"\n />\n\n {/* Bottom gradient scrim */}\n <div className=\"absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-black/70 pointer-events-none\" />\n\n {/* Content */}\n <div className=\"relative z-10 flex flex-col gap-[var(--spacing-l,24px)] p-[var(--spacing-l,24px)]\">\n {/* Name */}\n <p className=\"text-[length:var(--text-display,32px)] leading-[1.2] font-[var(--font-weight-medium,500)] tracking-[0.32px] text-white whitespace-pre-wrap\">\n {firstName}{'\\n'}{lastName}\n </p>\n\n {/* Contact rows */}\n <div className=\"flex flex-col gap-[var(--spacing-m,16px)]\">\n <ContactRow iconName=\"mail-01\" label={email} inverted />\n <ContactRow iconName=\"phone\" label={phone} inverted alignTop />\n <ContactRow iconName=\"home-02\" label={address} inverted alignTop />\n </div>\n </div>\n </div>\n );\n }\n\n // ── Size M ───────────────────────────────────────────────────────────────\n if (size === 'M') {\n return (\n <div\n className={cn(\n 'flex flex-col items-start gap-[var(--spacing-l,24px)] p-[var(--spacing-l,24px)] w-[353px] bg-[hsl(var(--neutral-50))] rounded-[var(--radius-token-lg,32px)]',\n className\n )}\n >\n {/* Circular avatar */}\n <div className=\"relative size-[124px] rounded-full overflow-hidden shrink-0\">\n <img\n src={imageSrc}\n alt={imageAlt}\n className=\"absolute inset-0 w-full h-full object-cover\"\n />\n </div>\n\n {/* Name */}\n <p className=\"text-[length:var(--text-display,32px)] leading-[1.2] font-[var(--font-weight-medium,500)] text-[hsl(var(--neutral-950))] whitespace-pre-wrap min-w-full\">\n {firstName}{'\\n'}{lastName}\n </p>\n\n {/* Contact rows */}\n <div className=\"flex flex-col gap-[var(--spacing-m,16px)] w-full\">\n <ContactRow iconName=\"mail-01\" label={email} />\n <ContactRow iconName=\"phone\" label={phone} alignTop />\n <ContactRow iconName=\"home-02\" label={address} alignTop />\n </div>\n </div>\n );\n }\n\n // ── Size S ───────────────────────────────────────────────────────────────\n return (\n <div\n className={cn(\n 'flex flex-col items-start gap-[var(--spacing-l,24px)] p-[var(--spacing-l,24px)] w-[353px] bg-[hsl(var(--neutral-50))] rounded-[var(--radius-token-lg,32px)]',\n className\n )}\n >\n {/* Name — single line for S */}\n <p className=\"text-[length:var(--text-heading,24px)] leading-[1.2] font-[var(--font-weight-medium,500)] tracking-[-0.48px] text-[hsl(var(--neutral-950))] w-[305px] whitespace-pre-wrap\">\n {fullName}\n </p>\n\n {/* Contact rows */}\n <div className=\"flex flex-col gap-[12px] w-full\">\n <ContactRow iconName=\"mail-01\" label={email} />\n <ContactRow iconName=\"phone\" label={phone} alignTop />\n <ContactRow iconName=\"home-02\" label={address} alignTop />\n </div>\n </div>\n );\n}\n\nexport const ProfileCardComponent = {\n L: (props: Omit<ProfileCardProps, 'size'>) => <ProfileCard {...props} size=\"L\" />,\n M: (props: Omit<ProfileCardProps, 'size'>) => <ProfileCard {...props} size=\"M\" />,\n S: (props: Omit<ProfileCardProps, 'size'>) => <ProfileCard {...props} size=\"S\" />,\n};\n\nexport default ProfileCard;\n"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"registryDependencies": [
|
|
62
|
+
"@sapient/button"
|
|
63
|
+
]
|
|
64
|
+
}
|