@brokenrubik/ui 0.0.0-stage → 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 +264 -2
- package/dist/components/accordion.d.ts +13 -0
- package/dist/components/accordion.d.ts.map +1 -0
- package/dist/components/accordion.js +25 -0
- package/dist/components/accordion.js.map +1 -0
- package/dist/components/alert-dialog.d.ts +21 -0
- package/dist/components/alert-dialog.d.ts.map +1 -0
- package/dist/components/alert-dialog.js +46 -0
- package/dist/components/alert-dialog.js.map +1 -0
- package/dist/components/alert.d.ts +24 -0
- package/dist/components/alert.d.ts.map +1 -0
- package/dist/components/alert.js +42 -0
- package/dist/components/alert.js.map +1 -0
- package/dist/components/avatar.d.ts +27 -0
- package/dist/components/avatar.d.ts.map +1 -0
- package/dist/components/avatar.js +53 -0
- package/dist/components/avatar.js.map +1 -0
- package/dist/components/badge.d.ts +21 -0
- package/dist/components/badge.d.ts.map +1 -0
- package/dist/components/badge.js +40 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/button.d.ts +18 -0
- package/dist/components/button.d.ts.map +1 -0
- package/dist/components/button.js +39 -0
- package/dist/components/button.js.map +1 -0
- package/dist/components/calendar.d.ts +13 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +68 -0
- package/dist/components/calendar.js.map +1 -0
- package/dist/components/card.d.ts +15 -0
- package/dist/components/card.d.ts.map +1 -0
- package/dist/components/card.js +31 -0
- package/dist/components/card.js.map +1 -0
- package/dist/components/checkbox.d.ts +6 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox.js +12 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/command.d.ts +24 -0
- package/dist/components/command.d.ts.map +1 -0
- package/dist/components/command.js +41 -0
- package/dist/components/command.js.map +1 -0
- package/dist/components/date-picker.d.ts +24 -0
- package/dist/components/date-picker.d.ts.map +1 -0
- package/dist/components/date-picker.js +18 -0
- package/dist/components/date-picker.js.map +1 -0
- package/dist/components/dialog.d.ts +23 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.js +45 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/dropdown-menu.d.ts +34 -0
- package/dist/components/dropdown-menu.d.ts.map +1 -0
- package/dist/components/dropdown-menu.js +61 -0
- package/dist/components/dropdown-menu.js.map +1 -0
- package/dist/components/empty-state.d.ts +13 -0
- package/dist/components/empty-state.d.ts.map +1 -0
- package/dist/components/empty-state.js +25 -0
- package/dist/components/empty-state.js.map +1 -0
- package/dist/components/field.d.ts +24 -0
- package/dist/components/field.d.ts.map +1 -0
- package/dist/components/field.js +34 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/input.d.ts +17 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +18 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/kbd.d.ts +14 -0
- package/dist/components/kbd.d.ts.map +1 -0
- package/dist/components/kbd.js +31 -0
- package/dist/components/kbd.js.map +1 -0
- package/dist/components/label.d.ts +6 -0
- package/dist/components/label.d.ts.map +1 -0
- package/dist/components/label.js +11 -0
- package/dist/components/label.js.map +1 -0
- package/dist/components/pagination.d.ts +18 -0
- package/dist/components/pagination.d.ts.map +1 -0
- package/dist/components/pagination.js +33 -0
- package/dist/components/pagination.js.map +1 -0
- package/dist/components/popover.d.ts +12 -0
- package/dist/components/popover.d.ts.map +1 -0
- package/dist/components/popover.js +29 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/progress.d.ts +17 -0
- package/dist/components/progress.d.ts.map +1 -0
- package/dist/components/progress.js +26 -0
- package/dist/components/progress.js.map +1 -0
- package/dist/components/radio-group.d.ts +7 -0
- package/dist/components/radio-group.d.ts.map +1 -0
- package/dist/components/radio-group.js +14 -0
- package/dist/components/radio-group.js.map +1 -0
- package/dist/components/select.d.ts +20 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.js +46 -0
- package/dist/components/select.js.map +1 -0
- package/dist/components/separator.d.ts +6 -0
- package/dist/components/separator.d.ts.map +1 -0
- package/dist/components/separator.js +11 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/sheet.d.ts +19 -0
- package/dist/components/sheet.d.ts.map +1 -0
- package/dist/components/sheet.js +47 -0
- package/dist/components/sheet.js.map +1 -0
- package/dist/components/skeleton.d.ts +5 -0
- package/dist/components/skeleton.d.ts.map +1 -0
- package/dist/components/skeleton.js +9 -0
- package/dist/components/skeleton.js.map +1 -0
- package/dist/components/sonner.d.ts +13 -0
- package/dist/components/sonner.d.ts.map +1 -0
- package/dist/components/sonner.js +40 -0
- package/dist/components/sonner.js.map +1 -0
- package/dist/components/switch.d.ts +6 -0
- package/dist/components/switch.d.ts.map +1 -0
- package/dist/components/switch.js +11 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/tabs.d.ts +13 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.js +24 -0
- package/dist/components/tabs.js.map +1 -0
- package/dist/components/textarea.d.ts +5 -0
- package/dist/components/textarea.d.ts.map +1 -0
- package/dist/components/textarea.js +10 -0
- package/dist/components/textarea.js.map +1 -0
- package/dist/components/toggle-group.d.ts +11 -0
- package/dist/components/toggle-group.d.ts.map +1 -0
- package/dist/components/toggle-group.js +18 -0
- package/dist/components/toggle-group.js.map +1 -0
- package/dist/components/tooltip.d.ts +13 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.js +24 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/tone.d.ts +8 -0
- package/dist/lib/tone.d.ts.map +1 -0
- package/dist/lib/tone.js +14 -0
- package/dist/lib/tone.js.map +1 -0
- package/dist/lib/utils.d.ts +20 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +40 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/styles/base.css +37 -0
- package/dist/styles/fonts.css +16 -0
- package/dist/styles/styles.css +14 -0
- package/dist/styles/theme.css +170 -0
- package/dist/styles/tokens.css +228 -0
- package/docs/design-handoff.md +112 -0
- package/docs/figma-vs-tailwind.md +135 -0
- package/docs/visual-testing.md +156 -0
- package/package.json +96 -4
- package/src/components/accordion.tsx +66 -0
- package/src/components/alert-dialog.tsx +139 -0
- package/src/components/alert.tsx +87 -0
- package/src/components/avatar.tsx +105 -0
- package/src/components/badge.tsx +63 -0
- package/src/components/button.tsx +70 -0
- package/src/components/calendar.tsx +131 -0
- package/src/components/card.tsx +72 -0
- package/src/components/checkbox.tsx +31 -0
- package/src/components/command.tsx +149 -0
- package/src/components/date-picker.tsx +86 -0
- package/src/components/dialog.tsx +126 -0
- package/src/components/dropdown-menu.tsx +225 -0
- package/src/components/empty-state.tsx +63 -0
- package/src/components/field.tsx +79 -0
- package/src/components/input.tsx +49 -0
- package/src/components/kbd.tsx +48 -0
- package/src/components/label.tsx +22 -0
- package/src/components/pagination.tsx +94 -0
- package/src/components/popover.tsx +65 -0
- package/src/components/progress.tsx +52 -0
- package/src/components/radio-group.tsx +42 -0
- package/src/components/select.tsx +167 -0
- package/src/components/separator.tsx +29 -0
- package/src/components/sheet.tsx +112 -0
- package/src/components/skeleton.tsx +16 -0
- package/src/components/sonner.tsx +58 -0
- package/src/components/switch.tsx +27 -0
- package/src/components/tabs.tsx +56 -0
- package/src/components/textarea.tsx +17 -0
- package/src/components/toggle-group.tsx +42 -0
- package/src/components/tooltip.tsx +55 -0
- package/src/index.ts +35 -0
- package/src/lib/tone.ts +16 -0
- package/src/lib/utils.ts +46 -0
- package/src/styles/base.css +37 -0
- package/src/styles/fonts.css +16 -0
- package/src/styles/styles.css +14 -0
- package/src/styles/theme.css +170 -0
- package/src/styles/tokens.css +228 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { CircleAlertIcon, CircleCheckIcon, InfoIcon, TriangleAlertIcon } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { resolveTone } from '../lib/tone.js';
|
|
6
|
+
import { cn } from '../lib/utils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Figma: Components · Base › Alert (2085:172). 8 px radius, 12/16 px padding, 12 px gap,
|
|
10
|
+
* 16 px icon. info = bordered on background (violet icon, muted text); the states are a
|
|
11
|
+
* tint with the -strong shade for icon, title and text.
|
|
12
|
+
*/
|
|
13
|
+
const alertVariants = cva(
|
|
14
|
+
"relative grid w-full grid-cols-[auto_1fr] items-start gap-x-3 gap-y-0.5 rounded-lg px-4 py-3 text-body [&>svg]:size-4",
|
|
15
|
+
{
|
|
16
|
+
variants: {
|
|
17
|
+
tone: {
|
|
18
|
+
info: 'border border-border bg-background text-foreground [&>svg]:text-primary-strong *:data-[slot=alert-description]:text-muted-foreground',
|
|
19
|
+
success: 'bg-success-tint text-success-strong',
|
|
20
|
+
warning: 'bg-warning-tint text-warning-strong',
|
|
21
|
+
destructive: 'bg-destructive-tint text-destructive-strong',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: { tone: 'info' },
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const defaultIcons = {
|
|
29
|
+
info: InfoIcon,
|
|
30
|
+
success: CircleCheckIcon,
|
|
31
|
+
warning: TriangleAlertIcon,
|
|
32
|
+
destructive: CircleAlertIcon,
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
type AlertTone = NonNullable<VariantProps<typeof alertVariants>['tone']>;
|
|
36
|
+
|
|
37
|
+
type AlertProps = React.ComponentProps<'div'> & {
|
|
38
|
+
/** Figma "Tone". Default `info`. */
|
|
39
|
+
tone?: AlertTone | null;
|
|
40
|
+
/** shadcn name for `tone`. Also accepts `default` (→ `info`). */
|
|
41
|
+
variant?: AlertTone | 'default' | null;
|
|
42
|
+
/** The icon. Default: the tone's lucide icon from Figma. `null` = no icon. */
|
|
43
|
+
icon?: React.ReactNode;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function Alert({ className, tone: toneProp, variant, icon, children, ...props }: AlertProps) {
|
|
47
|
+
const tone = resolveTone(toneProp, variant, { default: 'info' }, 'info');
|
|
48
|
+
const DefaultIcon = defaultIcons[tone];
|
|
49
|
+
const shown = icon === undefined ? <DefaultIcon aria-hidden /> : icon;
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
data-slot="alert"
|
|
53
|
+
data-tone={tone}
|
|
54
|
+
role="alert"
|
|
55
|
+
className={cn(alertVariants({ tone }), !shown && 'grid-cols-1', className)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{shown}
|
|
59
|
+
{children}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
data-slot="alert-title"
|
|
68
|
+
className={cn('col-start-2 font-medium in-[[data-slot=alert].grid-cols-1]:col-start-1', className)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-slot="alert-description"
|
|
78
|
+
className={cn(
|
|
79
|
+
'col-start-2 text-body-sm in-[[data-slot=alert].grid-cols-1]:col-start-1 [&_p]:leading-relaxed',
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { Alert, AlertTitle, AlertDescription, alertVariants, type AlertProps, type AlertTone };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
5
|
+
import { Avatar as AvatarPrimitive } from 'radix-ui';
|
|
6
|
+
|
|
7
|
+
import { resolveTone } from '../lib/tone.js';
|
|
8
|
+
import { cn } from '../lib/utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Figma: Components · Base › Avatar (2006:62). 20 or 28 px, full radius. Initials use a
|
|
12
|
+
* soft tint per person with the -strong shade, never a solid saturated fill.
|
|
13
|
+
*/
|
|
14
|
+
const avatarVariants = cva('group/avatar relative flex shrink-0 overflow-hidden rounded-full', {
|
|
15
|
+
variants: {
|
|
16
|
+
size: {
|
|
17
|
+
20: 'size-5',
|
|
18
|
+
28: 'size-7',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: { size: 28 },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const avatarFallbackVariants = cva(
|
|
25
|
+
'flex size-full items-center justify-center rounded-full text-initials group-data-[size=20]/avatar:text-initials-sm',
|
|
26
|
+
{
|
|
27
|
+
variants: {
|
|
28
|
+
tone: {
|
|
29
|
+
purple: 'bg-primary-tint text-primary-strong',
|
|
30
|
+
amber: 'bg-warning-tint text-warning-strong',
|
|
31
|
+
red: 'bg-destructive-tint text-destructive-strong',
|
|
32
|
+
emerald: 'bg-success-tint text-success-strong',
|
|
33
|
+
coral: 'bg-secondary text-secondary-foreground',
|
|
34
|
+
slate: 'bg-secondary text-secondary-foreground',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
defaultVariants: { tone: 'slate' },
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
type AvatarTone = NonNullable<VariantProps<typeof avatarFallbackVariants>['tone']>;
|
|
42
|
+
|
|
43
|
+
const avatarTones: readonly AvatarTone[] = ['purple', 'amber', 'red', 'emerald', 'coral', 'slate'];
|
|
44
|
+
|
|
45
|
+
/** A stable tone for a person (same name → same tint), for lists without a stored tone. */
|
|
46
|
+
function avatarToneFor(key: string): AvatarTone {
|
|
47
|
+
let hash = 0;
|
|
48
|
+
for (let i = 0; i < key.length; i++) hash = (hash * 31 + key.charCodeAt(i)) | 0;
|
|
49
|
+
return avatarTones[Math.abs(hash) % avatarTones.length] ?? 'slate';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function Avatar({
|
|
53
|
+
className,
|
|
54
|
+
size = 28,
|
|
55
|
+
...props
|
|
56
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Root> & VariantProps<typeof avatarVariants>) {
|
|
57
|
+
return (
|
|
58
|
+
<AvatarPrimitive.Root
|
|
59
|
+
data-slot="avatar"
|
|
60
|
+
data-size={size}
|
|
61
|
+
className={cn(avatarVariants({ size }), className)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
68
|
+
return (
|
|
69
|
+
<AvatarPrimitive.Image
|
|
70
|
+
data-slot="avatar-image"
|
|
71
|
+
className={cn('aspect-square size-full object-cover', className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type AvatarFallbackProps = React.ComponentProps<typeof AvatarPrimitive.Fallback> & {
|
|
78
|
+
/** Figma "Tone". Default `slate`. `avatarToneFor(name)` picks a stable one per person. */
|
|
79
|
+
tone?: AvatarTone | null;
|
|
80
|
+
/** Same as `tone` (shadcn-style name). */
|
|
81
|
+
variant?: AvatarTone | null;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function AvatarFallback({ className, tone: toneProp, variant, ...props }: AvatarFallbackProps) {
|
|
85
|
+
const tone = resolveTone(toneProp, variant, {}, 'slate');
|
|
86
|
+
return (
|
|
87
|
+
<AvatarPrimitive.Fallback
|
|
88
|
+
data-slot="avatar-fallback"
|
|
89
|
+
data-tone={tone}
|
|
90
|
+
className={cn(avatarFallbackVariants({ tone }), className)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
Avatar,
|
|
98
|
+
AvatarImage,
|
|
99
|
+
AvatarFallback,
|
|
100
|
+
avatarVariants,
|
|
101
|
+
avatarFallbackVariants,
|
|
102
|
+
avatarToneFor,
|
|
103
|
+
type AvatarTone,
|
|
104
|
+
type AvatarFallbackProps,
|
|
105
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { Slot } from 'radix-ui';
|
|
4
|
+
|
|
5
|
+
import { resolveTone } from '../lib/tone.js';
|
|
6
|
+
import { cn } from '../lib/utils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Figma: Components · Base › Badge (2006:37). 20 px high, 6 px radius, Label/Caption
|
|
10
|
+
* Medium. Every tone is a soft tint with the -strong text shade ("one color, one job").
|
|
11
|
+
* Only status fields get a tone: categorical tags (country, owner…) stay neutral.
|
|
12
|
+
*/
|
|
13
|
+
const badgeVariants = cva(
|
|
14
|
+
'inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md px-2 py-0.5 text-caption font-medium [&>svg]:pointer-events-none [&>svg]:size-3',
|
|
15
|
+
{
|
|
16
|
+
variants: {
|
|
17
|
+
tone: {
|
|
18
|
+
neutral: 'bg-muted text-muted-foreground',
|
|
19
|
+
primary: 'bg-primary-tint text-primary-strong',
|
|
20
|
+
success: 'bg-success-tint text-success-strong',
|
|
21
|
+
warning: 'bg-warning-tint text-warning-strong',
|
|
22
|
+
destructive: 'bg-destructive-tint text-destructive-strong',
|
|
23
|
+
highlight: 'bg-highlight text-highlight-foreground',
|
|
24
|
+
info: 'bg-info-tint text-info-strong',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
tone: 'neutral',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
type BadgeTone = NonNullable<VariantProps<typeof badgeVariants>['tone']>;
|
|
34
|
+
|
|
35
|
+
/** shadcn Badge variants that have no BR tone of the same name. */
|
|
36
|
+
const badgeVariantAliases: Record<string, BadgeTone> = {
|
|
37
|
+
default: 'neutral',
|
|
38
|
+
secondary: 'neutral',
|
|
39
|
+
outline: 'neutral',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type BadgeProps = React.ComponentProps<'span'> & {
|
|
43
|
+
/** Figma "Tone". Default `neutral`. */
|
|
44
|
+
tone?: BadgeTone | null;
|
|
45
|
+
/** shadcn name for `tone`. Also accepts `default`, `secondary`, `outline` (→ `neutral`). */
|
|
46
|
+
variant?: BadgeTone | 'default' | 'secondary' | 'outline' | null;
|
|
47
|
+
asChild?: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function Badge({ className, tone, variant, asChild = false, ...props }: BadgeProps) {
|
|
51
|
+
const Comp = asChild ? Slot.Root : 'span';
|
|
52
|
+
const resolved = resolveTone(tone, variant, badgeVariantAliases, 'neutral');
|
|
53
|
+
return (
|
|
54
|
+
<Comp
|
|
55
|
+
data-slot="badge"
|
|
56
|
+
data-tone={resolved}
|
|
57
|
+
className={cn(badgeVariants({ tone: resolved }), className)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { Badge, badgeVariants, type BadgeProps, type BadgeTone };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
5
|
+
import { Slot } from 'radix-ui';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/utils.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Figma: Components · Base › Button (2005:49) and Icon Button (2006:24).
|
|
11
|
+
* md 32 px / sm 28 px high, 8 px radius (radius/lg), Body/Default Medium (md) or
|
|
12
|
+
* Body/Small Medium (sm). Icon buttons are 28 / 24 px with 6 px radius (radius/md).
|
|
13
|
+
*/
|
|
14
|
+
const buttonVariants = cva(
|
|
15
|
+
"inline-flex shrink-0 items-center justify-center whitespace-nowrap rounded-lg font-medium transition-[color,background-color,border-color,box-shadow] outline-none select-none focus-visible:ring-3 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 aria-invalid:ring-3 aria-invalid:ring-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
16
|
+
{
|
|
17
|
+
variants: {
|
|
18
|
+
variant: {
|
|
19
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
20
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
21
|
+
outline:
|
|
22
|
+
'border border-border bg-background text-foreground shadow-xs hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent',
|
|
23
|
+
ghost:
|
|
24
|
+
'text-foreground hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent',
|
|
25
|
+
destructive:
|
|
26
|
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/30',
|
|
27
|
+
link: 'text-primary-strong underline-offset-4 hover:underline',
|
|
28
|
+
},
|
|
29
|
+
size: {
|
|
30
|
+
default: 'h-8 gap-1.5 px-3 text-body',
|
|
31
|
+
sm: "h-7 gap-1 px-2.5 text-body-sm [&_svg:not([class*='size-'])]:size-3.5",
|
|
32
|
+
icon: 'size-7 rounded-md',
|
|
33
|
+
'icon-sm': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3.5",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: {
|
|
37
|
+
variant: 'default',
|
|
38
|
+
size: 'default',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
type ButtonProps = React.ComponentProps<'button'> &
|
|
44
|
+
VariantProps<typeof buttonVariants> & {
|
|
45
|
+
/** Render the child element (a link, for example) with the button styles. */
|
|
46
|
+
asChild?: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function Button({
|
|
50
|
+
className,
|
|
51
|
+
variant = 'default',
|
|
52
|
+
size = 'default',
|
|
53
|
+
asChild = false,
|
|
54
|
+
type,
|
|
55
|
+
...props
|
|
56
|
+
}: ButtonProps) {
|
|
57
|
+
const Comp = asChild ? Slot.Root : 'button';
|
|
58
|
+
return (
|
|
59
|
+
<Comp
|
|
60
|
+
data-slot="button"
|
|
61
|
+
data-variant={variant}
|
|
62
|
+
data-size={size}
|
|
63
|
+
type={asChild ? type : (type ?? 'button')}
|
|
64
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { Button, buttonVariants, type ButtonProps };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { DayPicker, getDefaultClassNames, type DayButton } from 'react-day-picker';
|
|
5
|
+
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/utils.js';
|
|
8
|
+
import { buttonVariants } from './button.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Figma: Components · Base › Calendar (2083:95) and Calendar Day (2083:94).
|
|
12
|
+
* 12 px padding; header 28 px with bordered 28 px prev/next (6 px radius) and the month
|
|
13
|
+
* in Body/Default Medium; weekdays Label/Caption muted; days 32 px, 6 px radius:
|
|
14
|
+
* today = muted + Medium, selected = primary + Medium, outside = muted text, disabled = 50 %.
|
|
15
|
+
* No border or shadow of its own: inside a Popover (DatePicker) or a Card it gets them.
|
|
16
|
+
*/
|
|
17
|
+
function Calendar({
|
|
18
|
+
className,
|
|
19
|
+
classNames,
|
|
20
|
+
showOutsideDays = true,
|
|
21
|
+
captionLayout = 'label',
|
|
22
|
+
components,
|
|
23
|
+
...props
|
|
24
|
+
}: React.ComponentProps<typeof DayPicker>) {
|
|
25
|
+
const d = getDefaultClassNames();
|
|
26
|
+
return (
|
|
27
|
+
<DayPicker
|
|
28
|
+
showOutsideDays={showOutsideDays}
|
|
29
|
+
captionLayout={captionLayout}
|
|
30
|
+
className={cn('group/calendar bg-transparent p-3 [--cell-size:32px]', className)}
|
|
31
|
+
classNames={{
|
|
32
|
+
root: cn('w-fit', d.root),
|
|
33
|
+
months: cn('relative flex flex-col gap-4 sm:flex-row', d.months),
|
|
34
|
+
month: cn('flex w-full flex-col gap-2', d.month),
|
|
35
|
+
nav: cn('absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1', d.nav),
|
|
36
|
+
button_previous: cn(
|
|
37
|
+
buttonVariants({ variant: 'outline', size: 'icon' }),
|
|
38
|
+
'size-7 select-none shadow-none aria-disabled:opacity-50',
|
|
39
|
+
d.button_previous,
|
|
40
|
+
),
|
|
41
|
+
button_next: cn(
|
|
42
|
+
buttonVariants({ variant: 'outline', size: 'icon' }),
|
|
43
|
+
'size-7 select-none shadow-none aria-disabled:opacity-50',
|
|
44
|
+
d.button_next,
|
|
45
|
+
),
|
|
46
|
+
month_caption: cn('flex h-7 w-full items-center justify-center px-8', d.month_caption),
|
|
47
|
+
dropdowns: cn('flex h-7 w-full items-center justify-center gap-1.5 text-body font-medium', d.dropdowns),
|
|
48
|
+
dropdown_root: cn('relative rounded-md border border-input shadow-xs has-focus:border-ring', d.dropdown_root),
|
|
49
|
+
dropdown: cn('absolute inset-0 bg-popover opacity-0', d.dropdown),
|
|
50
|
+
caption_label: cn(
|
|
51
|
+
'select-none text-body font-medium',
|
|
52
|
+
captionLayout !== 'label' &&
|
|
53
|
+
'flex h-7 items-center gap-1 rounded-md pr-1 pl-2 [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
|
|
54
|
+
d.caption_label,
|
|
55
|
+
),
|
|
56
|
+
month_grid: 'w-full border-collapse',
|
|
57
|
+
weekdays: cn('flex pb-1.5', d.weekdays),
|
|
58
|
+
weekday: cn(
|
|
59
|
+
'flex h-6 w-(--cell-size) items-center justify-center text-caption font-normal text-muted-foreground select-none',
|
|
60
|
+
d.weekday,
|
|
61
|
+
),
|
|
62
|
+
week: cn('flex w-full pt-0.5', d.week),
|
|
63
|
+
week_number_header: cn('w-(--cell-size) select-none', d.week_number_header),
|
|
64
|
+
week_number: cn('text-caption text-muted-foreground select-none', d.week_number),
|
|
65
|
+
day: cn('relative size-(--cell-size) p-0 text-center select-none', d.day),
|
|
66
|
+
range_start: cn('rounded-l-md bg-muted', d.range_start),
|
|
67
|
+
range_middle: cn('rounded-none', d.range_middle),
|
|
68
|
+
range_end: cn('rounded-r-md bg-muted', d.range_end),
|
|
69
|
+
today: cn(d.today),
|
|
70
|
+
outside: cn('text-muted-foreground', d.outside),
|
|
71
|
+
disabled: cn('text-muted-foreground opacity-50', d.disabled),
|
|
72
|
+
hidden: cn('invisible', d.hidden),
|
|
73
|
+
...classNames,
|
|
74
|
+
}}
|
|
75
|
+
components={{
|
|
76
|
+
Chevron: ({ className: chevronClassName, orientation, ...chevronProps }) => {
|
|
77
|
+
const Icon =
|
|
78
|
+
orientation === 'left'
|
|
79
|
+
? ChevronLeftIcon
|
|
80
|
+
: orientation === 'right'
|
|
81
|
+
? ChevronRightIcon
|
|
82
|
+
: ChevronDownIcon;
|
|
83
|
+
return <Icon className={cn('size-4', chevronClassName)} {...chevronProps} />;
|
|
84
|
+
},
|
|
85
|
+
DayButton: CalendarDayButton,
|
|
86
|
+
...components,
|
|
87
|
+
}}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function CalendarDayButton({
|
|
94
|
+
className,
|
|
95
|
+
day,
|
|
96
|
+
modifiers,
|
|
97
|
+
...props
|
|
98
|
+
}: React.ComponentProps<typeof DayButton>) {
|
|
99
|
+
const ref = React.useRef<HTMLButtonElement>(null);
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
if (modifiers.focused) ref.current?.focus();
|
|
102
|
+
}, [modifiers.focused]);
|
|
103
|
+
|
|
104
|
+
const selectedSingle =
|
|
105
|
+
modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<button
|
|
109
|
+
ref={ref}
|
|
110
|
+
type="button"
|
|
111
|
+
data-day={day.date.toLocaleDateString()}
|
|
112
|
+
data-today={modifiers.today || undefined}
|
|
113
|
+
data-selected-single={selectedSingle || undefined}
|
|
114
|
+
data-range-start={modifiers.range_start || undefined}
|
|
115
|
+
data-range-end={modifiers.range_end || undefined}
|
|
116
|
+
data-range-middle={modifiers.range_middle || undefined}
|
|
117
|
+
className={cn(
|
|
118
|
+
'inline-flex size-(--cell-size) items-center justify-center rounded-md text-body text-inherit transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-3 focus-visible:ring-ring/40 disabled:pointer-events-none',
|
|
119
|
+
'data-[today=true]:bg-muted data-[today=true]:font-medium',
|
|
120
|
+
'data-[selected-single=true]:bg-primary data-[selected-single=true]:font-medium data-[selected-single=true]:text-primary-foreground',
|
|
121
|
+
'data-[range-start=true]:bg-primary data-[range-start=true]:font-medium data-[range-start=true]:text-primary-foreground',
|
|
122
|
+
'data-[range-end=true]:bg-primary data-[range-end=true]:font-medium data-[range-end=true]:text-primary-foreground',
|
|
123
|
+
'data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground',
|
|
124
|
+
className,
|
|
125
|
+
)}
|
|
126
|
+
{...props}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { Calendar, CalendarDayButton };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Figma: Components · Base › Card (2008:30). "Cards are flat: 8px radius, slate-200
|
|
7
|
+
* border, no shadow." 16 px padding; title (Heading/Card) and description (Body/Default,
|
|
8
|
+
* muted) 4 px apart.
|
|
9
|
+
*/
|
|
10
|
+
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
data-slot="card"
|
|
14
|
+
className={cn(
|
|
15
|
+
'flex flex-col gap-4 rounded-lg border border-border bg-card p-4 text-card-foreground',
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
data-slot="card-header"
|
|
27
|
+
className={cn(
|
|
28
|
+
'grid auto-rows-min grid-rows-[auto_auto] items-start gap-1 has-data-[slot=card-action]:grid-cols-[1fr_auto]',
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
37
|
+
return (
|
|
38
|
+
<div data-slot="card-title" className={cn('text-card-title text-foreground', className)} {...props} />
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
data-slot="card-description"
|
|
46
|
+
className={cn('text-body text-muted-foreground', className)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
data-slot="card-action"
|
|
56
|
+
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
63
|
+
return <div data-slot="card-content" className={cn(className)} {...props} />;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
67
|
+
return (
|
|
68
|
+
<div data-slot="card-footer" className={cn('flex items-center gap-2', className)} {...props} />
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Checkbox as CheckboxPrimitive } from 'radix-ui';
|
|
5
|
+
import { CheckIcon, MinusIcon } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/utils.js';
|
|
8
|
+
|
|
9
|
+
/** Figma: Components · Base › Checkbox (2008:29). 16 px, 4 px radius, `input` border. */
|
|
10
|
+
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
11
|
+
return (
|
|
12
|
+
<CheckboxPrimitive.Root
|
|
13
|
+
data-slot="checkbox"
|
|
14
|
+
className={cn(
|
|
15
|
+
'peer size-4 shrink-0 rounded-xs border border-input bg-background shadow-xs transition-[background-color,border-color,box-shadow] outline-none focus-visible:ring-3 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground',
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<CheckboxPrimitive.Indicator
|
|
21
|
+
data-slot="checkbox-indicator"
|
|
22
|
+
className="group/indicator flex items-center justify-center text-current"
|
|
23
|
+
>
|
|
24
|
+
<CheckIcon className="size-3 group-data-[state=indeterminate]/indicator:hidden" strokeWidth={3} />
|
|
25
|
+
<MinusIcon className="hidden size-3 group-data-[state=indeterminate]/indicator:block" strokeWidth={3} />
|
|
26
|
+
</CheckboxPrimitive.Indicator>
|
|
27
|
+
</CheckboxPrimitive.Root>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { Checkbox };
|