@optilogic/core 1.0.0-beta.1 → 1.0.0-beta.10
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 +10 -7
- package/dist/index.cjs +1244 -284
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +412 -6
- package/dist/index.d.ts +412 -6
- package/dist/index.js +1179 -234
- package/dist/index.js.map +1 -1
- package/dist/styles.css +61 -0
- package/package.json +20 -56
- package/src/components/board.tsx +251 -0
- package/src/components/card.tsx +656 -12
- package/src/components/context-menu.tsx +1 -1
- package/src/components/data-grid/hooks/useKeyboardNavigation.ts +1 -1
- package/src/components/data-table.tsx +735 -0
- package/src/index.ts +40 -0
- package/src/styles.css +61 -0
package/src/components/card.tsx
CHANGED
|
@@ -1,28 +1,173 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
3
|
|
|
3
4
|
import { cn } from "../utils/cn";
|
|
5
|
+
import { Checkbox } from "./checkbox";
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Card Variants
|
|
9
|
+
// =============================================================================
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
|
-
* Card
|
|
12
|
+
* Card variant styles using class-variance-authority.
|
|
13
|
+
* Provides size, hover effects, and interactive styling options.
|
|
14
|
+
*/
|
|
15
|
+
const cardVariants = cva(
|
|
16
|
+
"rounded-xl border bg-card text-card-foreground shadow",
|
|
17
|
+
{
|
|
18
|
+
variants: {
|
|
19
|
+
/**
|
|
20
|
+
* Card width size presets
|
|
21
|
+
*/
|
|
22
|
+
size: {
|
|
23
|
+
auto: "",
|
|
24
|
+
sm: "w-64",
|
|
25
|
+
md: "w-80",
|
|
26
|
+
lg: "w-96",
|
|
27
|
+
xl: "w-[28rem]",
|
|
28
|
+
full: "w-full",
|
|
29
|
+
},
|
|
30
|
+
/**
|
|
31
|
+
* Hover effect styles
|
|
32
|
+
*/
|
|
33
|
+
hover: {
|
|
34
|
+
none: "",
|
|
35
|
+
lift: "transition-all duration-200 hover:-translate-y-1 hover:shadow-lg",
|
|
36
|
+
glow: "transition-shadow duration-200 hover:shadow-lg hover:shadow-accent/20",
|
|
37
|
+
border: "transition-colors duration-200 hover:border-accent",
|
|
38
|
+
"border-success": "transition-colors duration-200 hover:border-success",
|
|
39
|
+
"border-warning": "transition-colors duration-200 hover:border-warning",
|
|
40
|
+
"border-destructive": "transition-colors duration-200 hover:border-destructive",
|
|
41
|
+
"border-muted": "transition-colors duration-200 hover:border-muted-foreground",
|
|
42
|
+
scale: "transition-transform duration-200 hover:scale-[1.02]",
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Whether the card is interactive (clickable)
|
|
46
|
+
*/
|
|
47
|
+
interactive: {
|
|
48
|
+
true: "cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
49
|
+
false: "",
|
|
50
|
+
},
|
|
51
|
+
/**
|
|
52
|
+
* Card padding density
|
|
53
|
+
*/
|
|
54
|
+
padding: {
|
|
55
|
+
none: "",
|
|
56
|
+
sm: "[&>*:not(img)]:p-3 [&>*:not(img)]:pt-3",
|
|
57
|
+
md: "",
|
|
58
|
+
lg: "[&>*:not(img)]:p-8 [&>*:not(img)]:pt-8",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
defaultVariants: {
|
|
62
|
+
size: "auto",
|
|
63
|
+
hover: "none",
|
|
64
|
+
interactive: false,
|
|
65
|
+
padding: "md",
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// Base Card Component
|
|
72
|
+
// =============================================================================
|
|
73
|
+
|
|
74
|
+
export interface CardProps
|
|
75
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
76
|
+
VariantProps<typeof cardVariants> {
|
|
77
|
+
/**
|
|
78
|
+
* If true, the card acts as a button and can be clicked.
|
|
79
|
+
* Adds keyboard accessibility and focus states.
|
|
80
|
+
*/
|
|
81
|
+
asButton?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Custom CSS class for hover border color.
|
|
84
|
+
* Use Tailwind classes like "hover:border-blue-500" or custom CSS variables.
|
|
85
|
+
* This overrides the hover variant's border color if specified.
|
|
86
|
+
*/
|
|
87
|
+
hoverBorderClass?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Card - Versatile container component for grouped content
|
|
92
|
+
*
|
|
93
|
+
* A flexible card component supporting multiple sizes, hover effects,
|
|
94
|
+
* and interactive states. Use with CardHeader, CardContent, CardFooter,
|
|
95
|
+
* and other sub-components.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* <Card size="md" hover="lift">
|
|
99
|
+
* <CardHeader>
|
|
100
|
+
* <CardTitle>Title</CardTitle>
|
|
101
|
+
* </CardHeader>
|
|
102
|
+
* <CardContent>Content here</CardContent>
|
|
103
|
+
* </Card>
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* <Card interactive hover="border" onClick={() => navigate('/item')}>
|
|
107
|
+
* <CardContent>Clickable card</CardContent>
|
|
108
|
+
* </Card>
|
|
9
109
|
*/
|
|
10
110
|
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
11
|
-
(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
111
|
+
(
|
|
112
|
+
{
|
|
113
|
+
className,
|
|
114
|
+
size,
|
|
115
|
+
hover,
|
|
116
|
+
interactive,
|
|
117
|
+
padding,
|
|
118
|
+
asButton,
|
|
119
|
+
hoverBorderClass,
|
|
120
|
+
onClick,
|
|
121
|
+
onKeyDown,
|
|
122
|
+
...props
|
|
123
|
+
},
|
|
124
|
+
ref
|
|
125
|
+
) => {
|
|
126
|
+
// If asButton is true, make the card interactive
|
|
127
|
+
const isInteractive = interactive || asButton || !!onClick;
|
|
128
|
+
|
|
129
|
+
const handleKeyDown = React.useCallback(
|
|
130
|
+
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
131
|
+
if (isInteractive && (e.key === "Enter" || e.key === " ")) {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
onClick?.(e as unknown as React.MouseEvent<HTMLDivElement>);
|
|
134
|
+
}
|
|
135
|
+
onKeyDown?.(e);
|
|
136
|
+
},
|
|
137
|
+
[isInteractive, onClick, onKeyDown]
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
ref={ref}
|
|
143
|
+
role={isInteractive ? "button" : undefined}
|
|
144
|
+
tabIndex={isInteractive ? 0 : undefined}
|
|
145
|
+
className={cn(
|
|
146
|
+
cardVariants({ size, hover, interactive: isInteractive, padding }),
|
|
147
|
+
"group relative",
|
|
148
|
+
// Custom hover border color overrides variant if provided
|
|
149
|
+
hoverBorderClass && "transition-colors duration-200",
|
|
150
|
+
hoverBorderClass,
|
|
151
|
+
className
|
|
152
|
+
)}
|
|
153
|
+
onClick={onClick}
|
|
154
|
+
onKeyDown={handleKeyDown}
|
|
155
|
+
{...props}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
21
159
|
);
|
|
22
160
|
Card.displayName = "Card";
|
|
23
161
|
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// Card Sub-Components
|
|
164
|
+
// =============================================================================
|
|
165
|
+
|
|
24
166
|
export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
25
167
|
|
|
168
|
+
/**
|
|
169
|
+
* CardHeader - Header section of a card
|
|
170
|
+
*/
|
|
26
171
|
const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>(
|
|
27
172
|
({ className, ...props }, ref) => (
|
|
28
173
|
<div
|
|
@@ -36,6 +181,9 @@ CardHeader.displayName = "CardHeader";
|
|
|
36
181
|
|
|
37
182
|
export interface CardTitleProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
38
183
|
|
|
184
|
+
/**
|
|
185
|
+
* CardTitle - Title text for card header
|
|
186
|
+
*/
|
|
39
187
|
const CardTitle = React.forwardRef<HTMLDivElement, CardTitleProps>(
|
|
40
188
|
({ className, ...props }, ref) => (
|
|
41
189
|
<div
|
|
@@ -50,6 +198,9 @@ CardTitle.displayName = "CardTitle";
|
|
|
50
198
|
export interface CardDescriptionProps
|
|
51
199
|
extends React.HTMLAttributes<HTMLDivElement> {}
|
|
52
200
|
|
|
201
|
+
/**
|
|
202
|
+
* CardDescription - Descriptive text for card header
|
|
203
|
+
*/
|
|
53
204
|
const CardDescription = React.forwardRef<HTMLDivElement, CardDescriptionProps>(
|
|
54
205
|
({ className, ...props }, ref) => (
|
|
55
206
|
<div
|
|
@@ -64,6 +215,9 @@ CardDescription.displayName = "CardDescription";
|
|
|
64
215
|
export interface CardContentProps
|
|
65
216
|
extends React.HTMLAttributes<HTMLDivElement> {}
|
|
66
217
|
|
|
218
|
+
/**
|
|
219
|
+
* CardContent - Main content area of a card
|
|
220
|
+
*/
|
|
67
221
|
const CardContent = React.forwardRef<HTMLDivElement, CardContentProps>(
|
|
68
222
|
({ className, ...props }, ref) => (
|
|
69
223
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
@@ -73,6 +227,9 @@ CardContent.displayName = "CardContent";
|
|
|
73
227
|
|
|
74
228
|
export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
75
229
|
|
|
230
|
+
/**
|
|
231
|
+
* CardFooter - Footer section of a card
|
|
232
|
+
*/
|
|
76
233
|
const CardFooter = React.forwardRef<HTMLDivElement, CardFooterProps>(
|
|
77
234
|
({ className, ...props }, ref) => (
|
|
78
235
|
<div
|
|
@@ -84,11 +241,498 @@ const CardFooter = React.forwardRef<HTMLDivElement, CardFooterProps>(
|
|
|
84
241
|
);
|
|
85
242
|
CardFooter.displayName = "CardFooter";
|
|
86
243
|
|
|
244
|
+
// =============================================================================
|
|
245
|
+
// CardImage Component
|
|
246
|
+
// =============================================================================
|
|
247
|
+
|
|
248
|
+
const cardImageVariants = cva("w-full object-cover", {
|
|
249
|
+
variants: {
|
|
250
|
+
/**
|
|
251
|
+
* Image aspect ratio
|
|
252
|
+
*/
|
|
253
|
+
aspectRatio: {
|
|
254
|
+
auto: "",
|
|
255
|
+
square: "aspect-square",
|
|
256
|
+
video: "aspect-video",
|
|
257
|
+
wide: "aspect-[2/1]",
|
|
258
|
+
portrait: "aspect-[3/4]",
|
|
259
|
+
},
|
|
260
|
+
/**
|
|
261
|
+
* Image position in the card
|
|
262
|
+
*/
|
|
263
|
+
position: {
|
|
264
|
+
top: "rounded-t-xl",
|
|
265
|
+
bottom: "rounded-b-xl",
|
|
266
|
+
fill: "rounded-xl",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
defaultVariants: {
|
|
270
|
+
aspectRatio: "video",
|
|
271
|
+
position: "top",
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
export interface CardImageProps
|
|
276
|
+
extends React.ImgHTMLAttributes<HTMLImageElement>,
|
|
277
|
+
VariantProps<typeof cardImageVariants> {
|
|
278
|
+
/**
|
|
279
|
+
* Fallback content to display if image fails to load
|
|
280
|
+
*/
|
|
281
|
+
fallback?: React.ReactNode;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* CardImage - Image/media component for cards
|
|
286
|
+
*
|
|
287
|
+
* Displays images with configurable aspect ratios and positions.
|
|
288
|
+
* Handles loading states and fallbacks.
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* <Card>
|
|
292
|
+
* <CardImage src="/photo.jpg" alt="Photo" aspectRatio="video" />
|
|
293
|
+
* <CardContent>Caption</CardContent>
|
|
294
|
+
* </Card>
|
|
295
|
+
*/
|
|
296
|
+
const CardImage = React.forwardRef<HTMLImageElement, CardImageProps>(
|
|
297
|
+
(
|
|
298
|
+
{ className, aspectRatio, position, fallback, alt, src, onError, ...props },
|
|
299
|
+
ref
|
|
300
|
+
) => {
|
|
301
|
+
const [hasError, setHasError] = React.useState(false);
|
|
302
|
+
|
|
303
|
+
const handleError = React.useCallback(
|
|
304
|
+
(e: React.SyntheticEvent<HTMLImageElement>) => {
|
|
305
|
+
setHasError(true);
|
|
306
|
+
onError?.(e);
|
|
307
|
+
},
|
|
308
|
+
[onError]
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Reset error state when src changes
|
|
312
|
+
React.useEffect(() => {
|
|
313
|
+
setHasError(false);
|
|
314
|
+
}, [src]);
|
|
315
|
+
|
|
316
|
+
if (hasError && fallback) {
|
|
317
|
+
return (
|
|
318
|
+
<div
|
|
319
|
+
className={cn(
|
|
320
|
+
"flex items-center justify-center bg-muted",
|
|
321
|
+
cardImageVariants({ aspectRatio, position }),
|
|
322
|
+
className
|
|
323
|
+
)}
|
|
324
|
+
>
|
|
325
|
+
{fallback}
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<img
|
|
332
|
+
ref={ref}
|
|
333
|
+
src={src}
|
|
334
|
+
alt={alt}
|
|
335
|
+
className={cn(cardImageVariants({ aspectRatio, position }), className)}
|
|
336
|
+
onError={handleError}
|
|
337
|
+
{...props}
|
|
338
|
+
/>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
CardImage.displayName = "CardImage";
|
|
343
|
+
|
|
344
|
+
// =============================================================================
|
|
345
|
+
// CardActions Component
|
|
346
|
+
// =============================================================================
|
|
347
|
+
|
|
348
|
+
const cardActionsVariants = cva(
|
|
349
|
+
"absolute flex items-center gap-1 transition-opacity duration-200 z-10",
|
|
350
|
+
{
|
|
351
|
+
variants: {
|
|
352
|
+
/**
|
|
353
|
+
* When to show the actions
|
|
354
|
+
*/
|
|
355
|
+
showOn: {
|
|
356
|
+
hover: "opacity-0 group-hover:opacity-100",
|
|
357
|
+
always: "opacity-100",
|
|
358
|
+
},
|
|
359
|
+
/**
|
|
360
|
+
* Position of the actions overlay
|
|
361
|
+
*/
|
|
362
|
+
position: {
|
|
363
|
+
"top-right": "top-2 right-2",
|
|
364
|
+
"top-left": "top-2 left-2",
|
|
365
|
+
"bottom-right": "bottom-2 right-2",
|
|
366
|
+
"bottom-left": "bottom-2 left-2",
|
|
367
|
+
},
|
|
368
|
+
/**
|
|
369
|
+
* Visual style of the actions container
|
|
370
|
+
*/
|
|
371
|
+
variant: {
|
|
372
|
+
/** Solid background with backdrop blur */
|
|
373
|
+
floating: "bg-background/90 backdrop-blur-sm rounded-md shadow-sm p-1",
|
|
374
|
+
/** No background, just spacing */
|
|
375
|
+
ghost: "p-1",
|
|
376
|
+
/** Muted bar background */
|
|
377
|
+
bar: "bg-muted/80 backdrop-blur-sm rounded-md p-1.5",
|
|
378
|
+
/** No background on container, icons get background on hover */
|
|
379
|
+
"icon-hover":
|
|
380
|
+
"p-0 [&>button]:bg-transparent [&>button]:hover:bg-background/90 [&>button]:hover:shadow-sm [&>button]:transition-all",
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
defaultVariants: {
|
|
384
|
+
showOn: "hover",
|
|
385
|
+
position: "top-right",
|
|
386
|
+
variant: "floating",
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
export interface CardActionsProps
|
|
392
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
393
|
+
VariantProps<typeof cardActionsVariants> {}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* CardActions - Hover-reveal action buttons for cards
|
|
397
|
+
*
|
|
398
|
+
* Place action buttons that appear on hover. Position can be customized.
|
|
399
|
+
* The parent Card automatically gets the 'group' class for hover detection.
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* <Card hover="lift">
|
|
403
|
+
* <CardActions>
|
|
404
|
+
* <IconButton size="sm" variant="ghost"><Edit /></IconButton>
|
|
405
|
+
* <IconButton size="sm" variant="ghost"><Trash /></IconButton>
|
|
406
|
+
* </CardActions>
|
|
407
|
+
* <CardContent>Content</CardContent>
|
|
408
|
+
* </Card>
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* // With floating style (default)
|
|
412
|
+
* <CardActions variant="floating">...</CardActions>
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* // With bar style for image overlays
|
|
416
|
+
* <CardActions variant="bar" position="bottom-right">...</CardActions>
|
|
417
|
+
*/
|
|
418
|
+
const CardActions = React.forwardRef<HTMLDivElement, CardActionsProps>(
|
|
419
|
+
({ className, showOn, position, variant, ...props }, ref) => (
|
|
420
|
+
<div
|
|
421
|
+
ref={ref}
|
|
422
|
+
className={cn(cardActionsVariants({ showOn, position, variant }), className)}
|
|
423
|
+
// Prevent clicks from bubbling to parent card
|
|
424
|
+
onClick={(e) => e.stopPropagation()}
|
|
425
|
+
{...props}
|
|
426
|
+
/>
|
|
427
|
+
)
|
|
428
|
+
);
|
|
429
|
+
CardActions.displayName = "CardActions";
|
|
430
|
+
|
|
431
|
+
// =============================================================================
|
|
432
|
+
// SelectableCard Component
|
|
433
|
+
// =============================================================================
|
|
434
|
+
|
|
435
|
+
export interface SelectableCardProps extends CardProps {
|
|
436
|
+
/**
|
|
437
|
+
* Whether the card is selected (controlled)
|
|
438
|
+
*/
|
|
439
|
+
selected?: boolean;
|
|
440
|
+
/**
|
|
441
|
+
* Default selected state (uncontrolled)
|
|
442
|
+
*/
|
|
443
|
+
defaultSelected?: boolean;
|
|
444
|
+
/**
|
|
445
|
+
* Callback when selection changes
|
|
446
|
+
*/
|
|
447
|
+
onSelectedChange?: (selected: boolean) => void;
|
|
448
|
+
/**
|
|
449
|
+
* Whether to show a visible checkbox
|
|
450
|
+
*/
|
|
451
|
+
showCheckbox?: boolean;
|
|
452
|
+
/**
|
|
453
|
+
* Position of the checkbox
|
|
454
|
+
*/
|
|
455
|
+
checkboxPosition?: "top-left" | "top-right" | "inline-left";
|
|
456
|
+
/**
|
|
457
|
+
* Whether the card is disabled
|
|
458
|
+
*/
|
|
459
|
+
disabled?: boolean;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* SelectableCard - Card with selection state
|
|
464
|
+
*
|
|
465
|
+
* A card that can be selected/deselected. Supports controlled and uncontrolled modes.
|
|
466
|
+
* Can optionally display a checkbox indicator.
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* <SelectableCard
|
|
470
|
+
* selected={isSelected}
|
|
471
|
+
* onSelectedChange={setIsSelected}
|
|
472
|
+
* showCheckbox
|
|
473
|
+
* >
|
|
474
|
+
* <CardContent>Selectable content</CardContent>
|
|
475
|
+
* </SelectableCard>
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* // Multi-select list with inline checkbox
|
|
479
|
+
* {items.map(item => (
|
|
480
|
+
* <SelectableCard
|
|
481
|
+
* key={item.id}
|
|
482
|
+
* selected={selectedIds.includes(item.id)}
|
|
483
|
+
* onSelectedChange={(sel) => toggleSelection(item.id, sel)}
|
|
484
|
+
* showCheckbox
|
|
485
|
+
* checkboxPosition="inline-left"
|
|
486
|
+
* >
|
|
487
|
+
* <CardContent>{item.name}</CardContent>
|
|
488
|
+
* </SelectableCard>
|
|
489
|
+
* ))}
|
|
490
|
+
*/
|
|
491
|
+
const SelectableCard = React.forwardRef<HTMLDivElement, SelectableCardProps>(
|
|
492
|
+
(
|
|
493
|
+
{
|
|
494
|
+
className,
|
|
495
|
+
selected: controlledSelected,
|
|
496
|
+
defaultSelected = false,
|
|
497
|
+
onSelectedChange,
|
|
498
|
+
showCheckbox = false,
|
|
499
|
+
checkboxPosition = "top-right",
|
|
500
|
+
disabled = false,
|
|
501
|
+
children,
|
|
502
|
+
onClick,
|
|
503
|
+
hover = "border",
|
|
504
|
+
...props
|
|
505
|
+
},
|
|
506
|
+
ref
|
|
507
|
+
) => {
|
|
508
|
+
// Support both controlled and uncontrolled modes
|
|
509
|
+
const [uncontrolledSelected, setUncontrolledSelected] =
|
|
510
|
+
React.useState(defaultSelected);
|
|
511
|
+
const isControlled = controlledSelected !== undefined;
|
|
512
|
+
const isSelected = isControlled ? controlledSelected : uncontrolledSelected;
|
|
513
|
+
|
|
514
|
+
const handleClick = React.useCallback(
|
|
515
|
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
516
|
+
if (disabled) return;
|
|
517
|
+
|
|
518
|
+
const newSelected = !isSelected;
|
|
519
|
+
if (!isControlled) {
|
|
520
|
+
setUncontrolledSelected(newSelected);
|
|
521
|
+
}
|
|
522
|
+
onSelectedChange?.(newSelected);
|
|
523
|
+
onClick?.(e);
|
|
524
|
+
},
|
|
525
|
+
[disabled, isSelected, isControlled, onSelectedChange, onClick]
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const handleCheckboxChange = React.useCallback(
|
|
529
|
+
(checked: boolean | "indeterminate") => {
|
|
530
|
+
if (disabled) return;
|
|
531
|
+
|
|
532
|
+
const newSelected = checked === true;
|
|
533
|
+
if (!isControlled) {
|
|
534
|
+
setUncontrolledSelected(newSelected);
|
|
535
|
+
}
|
|
536
|
+
onSelectedChange?.(newSelected);
|
|
537
|
+
},
|
|
538
|
+
[disabled, isControlled, onSelectedChange]
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
// Inline checkbox renders in a dedicated control bar
|
|
542
|
+
const isInline = checkboxPosition === "inline-left";
|
|
543
|
+
|
|
544
|
+
return (
|
|
545
|
+
<Card
|
|
546
|
+
ref={ref}
|
|
547
|
+
className={cn(
|
|
548
|
+
// Selection styling
|
|
549
|
+
isSelected && "ring-2 ring-accent border-accent",
|
|
550
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
551
|
+
className
|
|
552
|
+
)}
|
|
553
|
+
interactive={!disabled}
|
|
554
|
+
hover={disabled ? "none" : hover}
|
|
555
|
+
onClick={handleClick}
|
|
556
|
+
aria-selected={isSelected}
|
|
557
|
+
aria-disabled={disabled}
|
|
558
|
+
{...props}
|
|
559
|
+
>
|
|
560
|
+
{showCheckbox && isInline && (
|
|
561
|
+
<div
|
|
562
|
+
className="flex items-center gap-3 px-4 py-3 border-b border-border/50"
|
|
563
|
+
onClick={(e) => e.stopPropagation()}
|
|
564
|
+
>
|
|
565
|
+
<Checkbox
|
|
566
|
+
checked={isSelected}
|
|
567
|
+
onCheckedChange={handleCheckboxChange}
|
|
568
|
+
disabled={disabled}
|
|
569
|
+
/>
|
|
570
|
+
<span className="text-xs text-muted-foreground">
|
|
571
|
+
{isSelected ? "Selected" : "Click to select"}
|
|
572
|
+
</span>
|
|
573
|
+
</div>
|
|
574
|
+
)}
|
|
575
|
+
{showCheckbox && !isInline && (
|
|
576
|
+
<div
|
|
577
|
+
className={cn(
|
|
578
|
+
"absolute z-10 p-1 bg-background/90 backdrop-blur-sm rounded-md",
|
|
579
|
+
checkboxPosition === "top-left" ? "top-2 left-2" : "top-2 right-2"
|
|
580
|
+
)}
|
|
581
|
+
onClick={(e) => e.stopPropagation()}
|
|
582
|
+
>
|
|
583
|
+
<Checkbox
|
|
584
|
+
checked={isSelected}
|
|
585
|
+
onCheckedChange={handleCheckboxChange}
|
|
586
|
+
disabled={disabled}
|
|
587
|
+
/>
|
|
588
|
+
</div>
|
|
589
|
+
)}
|
|
590
|
+
{children}
|
|
591
|
+
</Card>
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
);
|
|
595
|
+
SelectableCard.displayName = "SelectableCard";
|
|
596
|
+
|
|
597
|
+
// =============================================================================
|
|
598
|
+
// Layout Components
|
|
599
|
+
// =============================================================================
|
|
600
|
+
|
|
601
|
+
const cardGridVariants = cva("grid", {
|
|
602
|
+
variants: {
|
|
603
|
+
/**
|
|
604
|
+
* Number of columns
|
|
605
|
+
*/
|
|
606
|
+
columns: {
|
|
607
|
+
1: "grid-cols-1",
|
|
608
|
+
2: "grid-cols-1 sm:grid-cols-2",
|
|
609
|
+
3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
|
|
610
|
+
4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
|
|
611
|
+
auto: "grid-cols-[repeat(auto-fill,minmax(280px,1fr))]",
|
|
612
|
+
},
|
|
613
|
+
/**
|
|
614
|
+
* Gap between cards
|
|
615
|
+
*/
|
|
616
|
+
gap: {
|
|
617
|
+
none: "gap-0",
|
|
618
|
+
sm: "gap-3",
|
|
619
|
+
md: "gap-4",
|
|
620
|
+
lg: "gap-6",
|
|
621
|
+
xl: "gap-8",
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
defaultVariants: {
|
|
625
|
+
columns: "auto",
|
|
626
|
+
gap: "md",
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
export interface CardGridProps
|
|
631
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
632
|
+
VariantProps<typeof cardGridVariants> {}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* CardGrid - Responsive grid layout for cards
|
|
636
|
+
*
|
|
637
|
+
* Arranges cards in a responsive grid with configurable columns and gaps.
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* <CardGrid columns={3} gap="lg">
|
|
641
|
+
* <Card>...</Card>
|
|
642
|
+
* <Card>...</Card>
|
|
643
|
+
* <Card>...</Card>
|
|
644
|
+
* </CardGrid>
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* // Auto-fill columns based on available space
|
|
648
|
+
* <CardGrid columns="auto">
|
|
649
|
+
* {items.map(item => <Card key={item.id}>...</Card>)}
|
|
650
|
+
* </CardGrid>
|
|
651
|
+
*/
|
|
652
|
+
const CardGrid = React.forwardRef<HTMLDivElement, CardGridProps>(
|
|
653
|
+
({ className, columns, gap, ...props }, ref) => (
|
|
654
|
+
<div
|
|
655
|
+
ref={ref}
|
|
656
|
+
className={cn(cardGridVariants({ columns, gap }), className)}
|
|
657
|
+
{...props}
|
|
658
|
+
/>
|
|
659
|
+
)
|
|
660
|
+
);
|
|
661
|
+
CardGrid.displayName = "CardGrid";
|
|
662
|
+
|
|
663
|
+
const cardListVariants = cva("flex flex-col", {
|
|
664
|
+
variants: {
|
|
665
|
+
/**
|
|
666
|
+
* Gap between cards
|
|
667
|
+
*/
|
|
668
|
+
gap: {
|
|
669
|
+
none: "gap-0",
|
|
670
|
+
sm: "gap-2",
|
|
671
|
+
md: "gap-4",
|
|
672
|
+
lg: "gap-6",
|
|
673
|
+
},
|
|
674
|
+
/**
|
|
675
|
+
* Whether to show dividers between cards
|
|
676
|
+
*/
|
|
677
|
+
divided: {
|
|
678
|
+
true: "[&>*:not(:last-child)]:border-b [&>*:not(:last-child)]:pb-4 [&>*:not(:last-child)]:rounded-b-none",
|
|
679
|
+
false: "",
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
defaultVariants: {
|
|
683
|
+
gap: "md",
|
|
684
|
+
divided: false,
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
export interface CardListProps
|
|
689
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
690
|
+
VariantProps<typeof cardListVariants> {}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* CardList - Vertical list layout for cards
|
|
694
|
+
*
|
|
695
|
+
* Arranges cards in a vertical list with optional dividers.
|
|
696
|
+
*
|
|
697
|
+
* @example
|
|
698
|
+
* <CardList gap="sm" divided>
|
|
699
|
+
* <Card>...</Card>
|
|
700
|
+
* <Card>...</Card>
|
|
701
|
+
* </CardList>
|
|
702
|
+
*/
|
|
703
|
+
const CardList = React.forwardRef<HTMLDivElement, CardListProps>(
|
|
704
|
+
({ className, gap, divided, ...props }, ref) => (
|
|
705
|
+
<div
|
|
706
|
+
ref={ref}
|
|
707
|
+
className={cn(cardListVariants({ gap, divided }), className)}
|
|
708
|
+
{...props}
|
|
709
|
+
/>
|
|
710
|
+
)
|
|
711
|
+
);
|
|
712
|
+
CardList.displayName = "CardList";
|
|
713
|
+
|
|
714
|
+
// =============================================================================
|
|
715
|
+
// Exports
|
|
716
|
+
// =============================================================================
|
|
717
|
+
|
|
87
718
|
export {
|
|
719
|
+
// Core components
|
|
88
720
|
Card,
|
|
89
721
|
CardHeader,
|
|
90
722
|
CardFooter,
|
|
91
723
|
CardTitle,
|
|
92
724
|
CardDescription,
|
|
93
725
|
CardContent,
|
|
726
|
+
// New components
|
|
727
|
+
CardImage,
|
|
728
|
+
CardActions,
|
|
729
|
+
SelectableCard,
|
|
730
|
+
CardGrid,
|
|
731
|
+
CardList,
|
|
732
|
+
// Variants (for advanced customization)
|
|
733
|
+
cardVariants,
|
|
734
|
+
cardImageVariants,
|
|
735
|
+
cardActionsVariants,
|
|
736
|
+
cardGridVariants,
|
|
737
|
+
cardListVariants,
|
|
94
738
|
};
|