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,468 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { SapientIcon } from '@/components/ui/sapient-icon';
|
|
6
|
+
|
|
7
|
+
// ─── Icon helper ─────────────────────────────────────────────────────────────
|
|
8
|
+
function Icon({
|
|
9
|
+
name,
|
|
10
|
+
size = 24,
|
|
11
|
+
className,
|
|
12
|
+
alt = '',
|
|
13
|
+
}: {
|
|
14
|
+
name: string;
|
|
15
|
+
size?: number;
|
|
16
|
+
className?: string;
|
|
17
|
+
alt?: string;
|
|
18
|
+
}) {
|
|
19
|
+
return (
|
|
20
|
+
<SapientIcon
|
|
21
|
+
name={name}
|
|
22
|
+
label={alt}
|
|
23
|
+
size={size}
|
|
24
|
+
className={className}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Badge ────────────────────────────────────────────────────────────────────
|
|
30
|
+
function Badge({ label = 'Label' }: { label?: string }) {
|
|
31
|
+
return (
|
|
32
|
+
<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))]">
|
|
33
|
+
<span className="text-[length:var(--text-label,12px)] leading-[var(--leading-label,18px)] font-[var(--font-weight-regular,400)] text-white whitespace-nowrap">
|
|
34
|
+
{label}
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Price row ────────────────────────────────────────────────────────────────
|
|
41
|
+
function PriceRow({
|
|
42
|
+
price = '1.234€',
|
|
43
|
+
showInfo = true,
|
|
44
|
+
inverted = false,
|
|
45
|
+
}: {
|
|
46
|
+
price?: string;
|
|
47
|
+
showInfo?: boolean;
|
|
48
|
+
inverted?: boolean;
|
|
49
|
+
}) {
|
|
50
|
+
return (
|
|
51
|
+
<div className="flex items-center gap-[var(--spacing-xs3,6px)] w-full">
|
|
52
|
+
<span
|
|
53
|
+
className={cn(
|
|
54
|
+
'text-[length:var(--text-subheading,20px)] leading-[var(--leading-subheading,24px)] font-[var(--font-weight-medium,500)] whitespace-nowrap',
|
|
55
|
+
inverted ? 'text-white' : 'text-[hsl(var(--neutral-950))]'
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{price}
|
|
59
|
+
</span>
|
|
60
|
+
{showInfo && (
|
|
61
|
+
<Icon
|
|
62
|
+
name="info-circle"
|
|
63
|
+
size={24}
|
|
64
|
+
alt="Price info"
|
|
65
|
+
className={cn(
|
|
66
|
+
'opacity-70',
|
|
67
|
+
inverted ? 'brightness-0 invert' : ''
|
|
68
|
+
)}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Expand/Zoom icon button ──────────────────────────────────────────────────
|
|
76
|
+
function ZoomButton({ inverted = false }: { inverted?: boolean }) {
|
|
77
|
+
return (
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
aria-label="Expand"
|
|
81
|
+
className={cn(
|
|
82
|
+
'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',
|
|
83
|
+
inverted
|
|
84
|
+
? 'border-white bg-white/10 text-white hover:bg-white/20'
|
|
85
|
+
: 'border-[hsl(var(--neutral-800))] bg-white/10 text-[hsl(var(--neutral-950))] hover:bg-[hsl(var(--state-hover))]'
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
<Icon name="expand-05" size={24} alt="Expand" className={cn(inverted ? 'brightness-0 invert' : '')} />
|
|
89
|
+
</button>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Card image ───────────────────────────────────────────────────────────────
|
|
94
|
+
function CardImage({
|
|
95
|
+
src,
|
|
96
|
+
alt = '',
|
|
97
|
+
aspectClass,
|
|
98
|
+
className,
|
|
99
|
+
}: {
|
|
100
|
+
src: string;
|
|
101
|
+
alt?: string;
|
|
102
|
+
aspectClass?: string;
|
|
103
|
+
className?: string;
|
|
104
|
+
}) {
|
|
105
|
+
return (
|
|
106
|
+
<div className={cn('relative w-full overflow-hidden', aspectClass, className)}>
|
|
107
|
+
<img
|
|
108
|
+
src={src}
|
|
109
|
+
alt={alt}
|
|
110
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export type ImageRatio = '4:5' | '4:3' | '16:9';
|
|
119
|
+
|
|
120
|
+
/** Aspect-ratio utility classes keyed by ratio */
|
|
121
|
+
const ASPECT_CLASS: Record<ImageRatio, string> = {
|
|
122
|
+
'16:9': 'aspect-video', // 16/9
|
|
123
|
+
'4:3': 'aspect-[4/3]',
|
|
124
|
+
'4:5': 'aspect-[4/5]',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Card width per ratio (mirrors Figma) */
|
|
128
|
+
const CARD_WIDTH: Record<ImageRatio, string> = {
|
|
129
|
+
'16:9': 'w-[353px]',
|
|
130
|
+
'4:3': 'w-[353px]',
|
|
131
|
+
'4:5': 'w-[353px]',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// ─── Variant A: No image (info card) ─────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
export interface ProductCardNoImageProps {
|
|
137
|
+
className?: string;
|
|
138
|
+
headline?: string;
|
|
139
|
+
body?: string;
|
|
140
|
+
price?: string;
|
|
141
|
+
badgeLabel?: string;
|
|
142
|
+
imageRatio?: ImageRatio;
|
|
143
|
+
showBadge?: boolean;
|
|
144
|
+
showBody?: boolean;
|
|
145
|
+
showPrice?: boolean;
|
|
146
|
+
showPriceInfo?: boolean;
|
|
147
|
+
/** When false renders without background (ghost) */
|
|
148
|
+
withBackground?: boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* **Product Card – No Image**
|
|
153
|
+
*
|
|
154
|
+
* Displays product info text and price without a hero image.
|
|
155
|
+
* Supports 4:5, 4:3, and 16:9 layouts.
|
|
156
|
+
*/
|
|
157
|
+
export function ProductCardNoImage({
|
|
158
|
+
className,
|
|
159
|
+
headline = 'Headline lorem ipsum',
|
|
160
|
+
body = 'Body lorem ipsum dolor sit amet, consectetur adipiscing',
|
|
161
|
+
price = '1.234€',
|
|
162
|
+
badgeLabel = 'Label',
|
|
163
|
+
imageRatio = '4:3',
|
|
164
|
+
showBadge = true,
|
|
165
|
+
showBody = true,
|
|
166
|
+
showPrice = true,
|
|
167
|
+
showPriceInfo = true,
|
|
168
|
+
withBackground = true,
|
|
169
|
+
}: ProductCardNoImageProps) {
|
|
170
|
+
const is45 = imageRatio === '4:5';
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div
|
|
174
|
+
className={cn(
|
|
175
|
+
'flex flex-col items-start p-[var(--spacing-l,24px)] rounded-[var(--radius-token-lg,32px)] relative',
|
|
176
|
+
CARD_WIDTH[imageRatio],
|
|
177
|
+
withBackground && 'bg-white',
|
|
178
|
+
// height
|
|
179
|
+
is45 ? 'h-[442px] justify-between' : 'gap-[var(--spacing-l,24px)]',
|
|
180
|
+
imageRatio === '4:3' && 'h-[283px] justify-between',
|
|
181
|
+
className
|
|
182
|
+
)}
|
|
183
|
+
>
|
|
184
|
+
{/* Content up */}
|
|
185
|
+
<div
|
|
186
|
+
className={cn(
|
|
187
|
+
'flex flex-col items-start w-full',
|
|
188
|
+
is45 ? 'gap-[var(--spacing-m,16px)] shrink-0' : 'gap-[var(--spacing-l,24px)] flex-1 min-h-0'
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
{showBadge && <Badge label={badgeLabel} />}
|
|
192
|
+
|
|
193
|
+
{/* Heading + body – only for 4:3 and 16:9 */}
|
|
194
|
+
{!is45 && (
|
|
195
|
+
<div className="flex flex-col gap-[var(--spacing-xs2,8px)] items-start w-full">
|
|
196
|
+
<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">
|
|
197
|
+
{headline}
|
|
198
|
+
</p>
|
|
199
|
+
{showBody && (
|
|
200
|
+
<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">
|
|
201
|
+
{body}
|
|
202
|
+
</p>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{/* Headline only for 4:5 */}
|
|
208
|
+
{is45 && (
|
|
209
|
+
<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">
|
|
210
|
+
{headline}
|
|
211
|
+
</p>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Price */}
|
|
216
|
+
{showPrice && (
|
|
217
|
+
<PriceRow price={price} showInfo={showPriceInfo} />
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Icon button – top-right (only 4:3, 16:9) */}
|
|
221
|
+
{!is45 && (
|
|
222
|
+
<div className="absolute top-4 right-4">
|
|
223
|
+
<ZoomButton />
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Variant B: Full image (image fills the whole card) ───────────────────────
|
|
231
|
+
|
|
232
|
+
export interface ProductCardFullImageProps {
|
|
233
|
+
className?: string;
|
|
234
|
+
imageSrc: string;
|
|
235
|
+
imageAlt?: string;
|
|
236
|
+
headline?: string;
|
|
237
|
+
body?: string;
|
|
238
|
+
price?: string;
|
|
239
|
+
badgeLabel?: string;
|
|
240
|
+
imageRatio?: ImageRatio;
|
|
241
|
+
showBadge?: boolean;
|
|
242
|
+
showBody?: boolean;
|
|
243
|
+
showPrice?: boolean;
|
|
244
|
+
showPriceInfo?: boolean;
|
|
245
|
+
showZoomButton?: boolean;
|
|
246
|
+
state?: 'enabled' | 'hover';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* **Product Card – Full Image**
|
|
251
|
+
*
|
|
252
|
+
* Hero image fills the entire card. Text is overlaid at the bottom.
|
|
253
|
+
* Supports 4:5, 4:3, and 16:9 ratios.
|
|
254
|
+
*/
|
|
255
|
+
export function ProductCardFullImage({
|
|
256
|
+
className,
|
|
257
|
+
imageSrc,
|
|
258
|
+
imageAlt = '',
|
|
259
|
+
headline = 'Headline lorem ipsum',
|
|
260
|
+
body = 'Body lorem ipsum dolor sit amet, consectetur',
|
|
261
|
+
price = '1.234€',
|
|
262
|
+
badgeLabel = 'Label',
|
|
263
|
+
imageRatio = '4:5',
|
|
264
|
+
showBadge = true,
|
|
265
|
+
showBody = true,
|
|
266
|
+
showPrice = true,
|
|
267
|
+
showPriceInfo = true,
|
|
268
|
+
showZoomButton = true,
|
|
269
|
+
state = 'enabled',
|
|
270
|
+
}: ProductCardFullImageProps) {
|
|
271
|
+
const is169 = imageRatio === '16:9';
|
|
272
|
+
const is45 = imageRatio === '4:5';
|
|
273
|
+
const showTextOverlay = is45 || imageRatio === '4:3';
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<div
|
|
277
|
+
className={cn(
|
|
278
|
+
'relative flex flex-col items-start overflow-hidden rounded-[var(--radius-token-lg,32px)]',
|
|
279
|
+
CARD_WIDTH[imageRatio],
|
|
280
|
+
ASPECT_CLASS[imageRatio],
|
|
281
|
+
className
|
|
282
|
+
)}
|
|
283
|
+
>
|
|
284
|
+
{/* Full-bleed image */}
|
|
285
|
+
<img
|
|
286
|
+
src={imageSrc}
|
|
287
|
+
alt={imageAlt}
|
|
288
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
289
|
+
/>
|
|
290
|
+
|
|
291
|
+
{/* Top gradient + badge/zoom overlay */}
|
|
292
|
+
<div
|
|
293
|
+
className={cn(
|
|
294
|
+
'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)]',
|
|
295
|
+
!is169 && 'bg-gradient-to-b from-black/15 to-transparent'
|
|
296
|
+
)}
|
|
297
|
+
>
|
|
298
|
+
{showBadge && <Badge label={badgeLabel} />}
|
|
299
|
+
{(is45 || imageRatio === '4:3') && showZoomButton && (
|
|
300
|
+
<ZoomButton inverted />
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{/* Bottom text overlay – 4:5 and 4:3 */}
|
|
305
|
+
{showTextOverlay && (
|
|
306
|
+
<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)]">
|
|
307
|
+
<p
|
|
308
|
+
className={cn(
|
|
309
|
+
'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',
|
|
310
|
+
state === 'hover' && 'text-[hsl(var(--foreground-strong))]'
|
|
311
|
+
)}
|
|
312
|
+
>
|
|
313
|
+
{headline}
|
|
314
|
+
</p>
|
|
315
|
+
{showBody && (
|
|
316
|
+
<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">
|
|
317
|
+
{body}
|
|
318
|
+
</p>
|
|
319
|
+
)}
|
|
320
|
+
{showPrice && (
|
|
321
|
+
<PriceRow price={price} showInfo={showPriceInfo} inverted />
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
325
|
+
|
|
326
|
+
{/* Bottom text overlay – 16:9 */}
|
|
327
|
+
{is169 && (
|
|
328
|
+
<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)]">
|
|
329
|
+
<p
|
|
330
|
+
className={cn(
|
|
331
|
+
'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',
|
|
332
|
+
state === 'hover' && 'text-[hsl(var(--foreground-strong))]'
|
|
333
|
+
)}
|
|
334
|
+
>
|
|
335
|
+
{headline}
|
|
336
|
+
</p>
|
|
337
|
+
{showPrice && (
|
|
338
|
+
<PriceRow price={price} showInfo={showPriceInfo} inverted />
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── Variant C: Split content (image top + text bottom) ───────────────────────
|
|
347
|
+
|
|
348
|
+
export interface ProductCardSplitProps {
|
|
349
|
+
className?: string;
|
|
350
|
+
imageSrc: string;
|
|
351
|
+
imageAlt?: string;
|
|
352
|
+
headline?: string;
|
|
353
|
+
body?: string;
|
|
354
|
+
price?: string;
|
|
355
|
+
badgeLabel?: string;
|
|
356
|
+
showBadge?: boolean;
|
|
357
|
+
showBody?: boolean;
|
|
358
|
+
showImage?: boolean;
|
|
359
|
+
showPrice?: boolean;
|
|
360
|
+
showPriceInfo?: boolean;
|
|
361
|
+
showZoomButton?: boolean;
|
|
362
|
+
/** Wraps content in a white card background */
|
|
363
|
+
withBackground?: boolean;
|
|
364
|
+
state?: 'enabled' | 'hover';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* **Product Card – Split Content**
|
|
369
|
+
*
|
|
370
|
+
* Image on top, text panel below. Comes in two layouts:
|
|
371
|
+
* - `withBackground=true` → rounded card with white text panel
|
|
372
|
+
* - `withBackground=false` → transparent, text directly below image
|
|
373
|
+
*/
|
|
374
|
+
export function ProductCardSplit({
|
|
375
|
+
className,
|
|
376
|
+
imageSrc,
|
|
377
|
+
imageAlt = '',
|
|
378
|
+
headline = 'Headline lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
|
379
|
+
body = 'Body lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
|
380
|
+
price = '1.234€',
|
|
381
|
+
badgeLabel = 'Label',
|
|
382
|
+
showBadge = true,
|
|
383
|
+
showBody = true,
|
|
384
|
+
showImage = true,
|
|
385
|
+
showPrice = true,
|
|
386
|
+
showPriceInfo = true,
|
|
387
|
+
showZoomButton = true,
|
|
388
|
+
withBackground = true,
|
|
389
|
+
state = 'enabled',
|
|
390
|
+
}: ProductCardSplitProps) {
|
|
391
|
+
const isHover = state === 'hover';
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<div
|
|
395
|
+
className={cn(
|
|
396
|
+
'flex flex-col items-start w-[353px] relative',
|
|
397
|
+
withBackground && 'overflow-hidden rounded-[var(--radius-token-lg,32px)]',
|
|
398
|
+
className
|
|
399
|
+
)}
|
|
400
|
+
>
|
|
401
|
+
{/* Image section */}
|
|
402
|
+
{showImage && (
|
|
403
|
+
<div
|
|
404
|
+
className={cn(
|
|
405
|
+
'relative w-full overflow-hidden shrink-0',
|
|
406
|
+
withBackground
|
|
407
|
+
? 'rounded-tl-[var(--radius-token-lg,32px)] rounded-tr-[var(--radius-token-lg,32px)]'
|
|
408
|
+
: 'rounded-[var(--radius-token-lg,32px)]',
|
|
409
|
+
'aspect-[4/3]'
|
|
410
|
+
)}
|
|
411
|
+
>
|
|
412
|
+
<img
|
|
413
|
+
src={imageSrc}
|
|
414
|
+
alt={imageAlt}
|
|
415
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
416
|
+
/>
|
|
417
|
+
|
|
418
|
+
{/* Top overlay: badge + zoom */}
|
|
419
|
+
<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">
|
|
420
|
+
{showBadge && <Badge label={badgeLabel} />}
|
|
421
|
+
{showZoomButton && <ZoomButton inverted />}
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
|
|
426
|
+
{/* Text section */}
|
|
427
|
+
<div
|
|
428
|
+
className={cn(
|
|
429
|
+
'flex flex-col gap-[var(--spacing-xs2,8px)] items-start w-full shrink-0',
|
|
430
|
+
withBackground
|
|
431
|
+
? 'bg-white p-[var(--spacing-l,24px)]'
|
|
432
|
+
: 'py-[var(--spacing-l,24px)]'
|
|
433
|
+
)}
|
|
434
|
+
>
|
|
435
|
+
<p
|
|
436
|
+
className={cn(
|
|
437
|
+
'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',
|
|
438
|
+
isHover
|
|
439
|
+
? 'text-[hsl(var(--foreground-strong))]'
|
|
440
|
+
: 'text-[hsl(var(--neutral-950))]'
|
|
441
|
+
)}
|
|
442
|
+
>
|
|
443
|
+
{headline}
|
|
444
|
+
</p>
|
|
445
|
+
|
|
446
|
+
{showBody && (
|
|
447
|
+
<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">
|
|
448
|
+
{body}
|
|
449
|
+
</p>
|
|
450
|
+
)}
|
|
451
|
+
|
|
452
|
+
{showPrice && (
|
|
453
|
+
<PriceRow price={price} showInfo={showPriceInfo} />
|
|
454
|
+
)}
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ─── Combined export (convenience) ────────────────────────────────────────────
|
|
461
|
+
|
|
462
|
+
export const ProductCard = {
|
|
463
|
+
NoImage: ProductCardNoImage,
|
|
464
|
+
FullImage: ProductCardFullImage,
|
|
465
|
+
Split: ProductCardSplit,
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
export default ProductCard;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { SapientIcon } from '@/components/ui/sapient-icon';
|
|
6
|
+
|
|
7
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* L – Full-bleed portrait photo card (353 × 441 px), white text overlaid at bottom.
|
|
11
|
+
* M – White card with circular avatar, dark text name + contacts below.
|
|
12
|
+
* S – White card, no photo, name + contacts only (compact).
|
|
13
|
+
*/
|
|
14
|
+
export type ProfileCardSize = 'L' | 'M' | 'S';
|
|
15
|
+
|
|
16
|
+
export interface ProfileCardProps {
|
|
17
|
+
className?: string;
|
|
18
|
+
size?: ProfileCardSize;
|
|
19
|
+
/** First name (L/M show on separate line from surname) */
|
|
20
|
+
firstName?: string;
|
|
21
|
+
/** Last name */
|
|
22
|
+
lastName?: string;
|
|
23
|
+
/** Full name string – used by S (single line) */
|
|
24
|
+
fullName?: string;
|
|
25
|
+
email?: string;
|
|
26
|
+
phone?: string;
|
|
27
|
+
address?: string;
|
|
28
|
+
/** Photo URL. Used in L (full-bleed) and M (circular avatar). */
|
|
29
|
+
imageSrc?: string;
|
|
30
|
+
imageAlt?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const PLACEHOLDER_PHOTO =
|
|
36
|
+
'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=800&q=80';
|
|
37
|
+
|
|
38
|
+
function ContactRow({
|
|
39
|
+
iconName,
|
|
40
|
+
label,
|
|
41
|
+
inverted = false,
|
|
42
|
+
alignTop = false,
|
|
43
|
+
}: {
|
|
44
|
+
iconName: string;
|
|
45
|
+
label: string;
|
|
46
|
+
inverted?: boolean;
|
|
47
|
+
alignTop?: boolean;
|
|
48
|
+
}) {
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
className={cn(
|
|
52
|
+
'flex gap-[var(--spacing-xs2,8px)] w-full shrink-0',
|
|
53
|
+
alignTop ? 'items-start' : 'items-center'
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
<SapientIcon
|
|
57
|
+
name={iconName}
|
|
58
|
+
label=""
|
|
59
|
+
size={24}
|
|
60
|
+
className={cn(
|
|
61
|
+
'size-6 shrink-0',
|
|
62
|
+
inverted && 'brightness-0 invert'
|
|
63
|
+
)}
|
|
64
|
+
/>
|
|
65
|
+
<p
|
|
66
|
+
className={cn(
|
|
67
|
+
'text-[length:var(--text-body,16px)] leading-[1.4] font-[var(--font-weight-regular,400)] shrink-0',
|
|
68
|
+
inverted
|
|
69
|
+
? 'text-white'
|
|
70
|
+
: 'text-[hsl(var(--neutral-950))]'
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{label}
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── ProfileCard ──────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export function ProfileCard({
|
|
82
|
+
className,
|
|
83
|
+
size = 'L',
|
|
84
|
+
firstName = 'Name',
|
|
85
|
+
lastName = 'Surname',
|
|
86
|
+
fullName = 'Name Surname',
|
|
87
|
+
email = 'name.surname@email.com',
|
|
88
|
+
phone = '+00 123 456 7890',
|
|
89
|
+
address = 'Lorem Ipsum Street, 20154, Milan, Italy',
|
|
90
|
+
imageSrc = PLACEHOLDER_PHOTO,
|
|
91
|
+
imageAlt = '',
|
|
92
|
+
}: ProfileCardProps) {
|
|
93
|
+
|
|
94
|
+
// ── Size L ───────────────────────────────────────────────────────────────
|
|
95
|
+
if (size === 'L') {
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
className={cn(
|
|
99
|
+
'relative w-[353px] h-[441px] overflow-hidden rounded-[var(--radius-token-lg,32px)] flex flex-col justify-end',
|
|
100
|
+
className
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
{/* Full-bleed photo */}
|
|
104
|
+
<img
|
|
105
|
+
src={imageSrc}
|
|
106
|
+
alt={imageAlt}
|
|
107
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
{/* Bottom gradient scrim */}
|
|
111
|
+
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-black/70 pointer-events-none" />
|
|
112
|
+
|
|
113
|
+
{/* Content */}
|
|
114
|
+
<div className="relative z-10 flex flex-col gap-[var(--spacing-l,24px)] p-[var(--spacing-l,24px)]">
|
|
115
|
+
{/* Name */}
|
|
116
|
+
<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">
|
|
117
|
+
{firstName}{'\n'}{lastName}
|
|
118
|
+
</p>
|
|
119
|
+
|
|
120
|
+
{/* Contact rows */}
|
|
121
|
+
<div className="flex flex-col gap-[var(--spacing-m,16px)]">
|
|
122
|
+
<ContactRow iconName="mail-01" label={email} inverted />
|
|
123
|
+
<ContactRow iconName="phone" label={phone} inverted alignTop />
|
|
124
|
+
<ContactRow iconName="home-02" label={address} inverted alignTop />
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Size M ───────────────────────────────────────────────────────────────
|
|
132
|
+
if (size === 'M') {
|
|
133
|
+
return (
|
|
134
|
+
<div
|
|
135
|
+
className={cn(
|
|
136
|
+
'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)]',
|
|
137
|
+
className
|
|
138
|
+
)}
|
|
139
|
+
>
|
|
140
|
+
{/* Circular avatar */}
|
|
141
|
+
<div className="relative size-[124px] rounded-full overflow-hidden shrink-0">
|
|
142
|
+
<img
|
|
143
|
+
src={imageSrc}
|
|
144
|
+
alt={imageAlt}
|
|
145
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Name */}
|
|
150
|
+
<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">
|
|
151
|
+
{firstName}{'\n'}{lastName}
|
|
152
|
+
</p>
|
|
153
|
+
|
|
154
|
+
{/* Contact rows */}
|
|
155
|
+
<div className="flex flex-col gap-[var(--spacing-m,16px)] w-full">
|
|
156
|
+
<ContactRow iconName="mail-01" label={email} />
|
|
157
|
+
<ContactRow iconName="phone" label={phone} alignTop />
|
|
158
|
+
<ContactRow iconName="home-02" label={address} alignTop />
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Size S ───────────────────────────────────────────────────────────────
|
|
165
|
+
return (
|
|
166
|
+
<div
|
|
167
|
+
className={cn(
|
|
168
|
+
'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)]',
|
|
169
|
+
className
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
{/* Name — single line for S */}
|
|
173
|
+
<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">
|
|
174
|
+
{fullName}
|
|
175
|
+
</p>
|
|
176
|
+
|
|
177
|
+
{/* Contact rows */}
|
|
178
|
+
<div className="flex flex-col gap-[12px] w-full">
|
|
179
|
+
<ContactRow iconName="mail-01" label={email} />
|
|
180
|
+
<ContactRow iconName="phone" label={phone} alignTop />
|
|
181
|
+
<ContactRow iconName="home-02" label={address} alignTop />
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const ProfileCardComponent = {
|
|
188
|
+
L: (props: Omit<ProfileCardProps, 'size'>) => <ProfileCard {...props} size="L" />,
|
|
189
|
+
M: (props: Omit<ProfileCardProps, 'size'>) => <ProfileCard {...props} size="M" />,
|
|
190
|
+
S: (props: Omit<ProfileCardProps, 'size'>) => <ProfileCard {...props} size="S" />,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export default ProfileCard;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export interface SapientProgressProps
|
|
6
|
+
extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
|
|
7
|
+
value?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const SapientProgress = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof ProgressPrimitive.Root>,
|
|
12
|
+
SapientProgressProps
|
|
13
|
+
>(({ className, value = 0, ...props }, ref) => {
|
|
14
|
+
return (
|
|
15
|
+
<ProgressPrimitive.Root
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn(
|
|
18
|
+
"relative h-2 w-full overflow-hidden rounded-full bg-secondary",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
value={value}
|
|
22
|
+
{...props}
|
|
23
|
+
>
|
|
24
|
+
<ProgressPrimitive.Indicator
|
|
25
|
+
className="h-full w-full flex-1 bg-primary transition-all"
|
|
26
|
+
style={{ transform: `translateX(-${100 - value}%)` }}
|
|
27
|
+
/>
|
|
28
|
+
</ProgressPrimitive.Root>
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
SapientProgress.displayName = "SapientProgress";
|