panelui-web 0.9.0 → 0.10.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/dist/components/button-group/index.d.ts +28 -16
- package/dist/components/button-group/index.js +53 -15
- package/dist/components/choice-cards/index.d.ts +167 -0
- package/dist/components/choice-cards/index.js +132 -0
- package/dist/components/inspector/index.d.ts +242 -0
- package/dist/components/inspector/index.js +230 -0
- package/dist/components/sheet/index.d.ts +16 -5
- package/dist/components/sheet/index.js +72 -15
- package/dist/components/slider/index.d.ts +293 -0
- package/dist/components/slider/index.js +342 -0
- package/dist/components/timeline/index.d.ts +328 -0
- package/dist/components/timeline/index.js +613 -0
- package/dist/components/toolbar/index.d.ts +185 -0
- package/dist/components/toolbar/index.js +270 -0
- package/dist/components/workbench/index.d.ts +224 -0
- package/dist/components/workbench/index.js +306 -0
- package/dist/components/xy-pad/index.d.ts +180 -0
- package/dist/components/xy-pad/index.js +278 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ButtonGroup — related actions
|
|
2
|
+
* ButtonGroup — related actions held together in one control.
|
|
3
3
|
*
|
|
4
4
|
* ```tsx
|
|
5
5
|
* <ButtonGroup aria-label="History">
|
|
@@ -8,21 +8,30 @@
|
|
|
8
8
|
* </ButtonGroup>
|
|
9
9
|
* ```
|
|
10
10
|
*
|
|
11
|
-
* ##
|
|
11
|
+
* ## A track, like the segmented Tabs
|
|
12
12
|
*
|
|
13
|
-
* The group
|
|
14
|
-
*
|
|
15
|
-
* so
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* The group is drawn the way the segmented `Tabs` are: a muted track with
|
|
14
|
+
* rounded corners, and the buttons inside it at the radius one step smaller,
|
|
15
|
+
* so every corner follows the track's. Outline and ghost buttons lie flat in
|
|
16
|
+
* the track with muted labels; the one under the pointer rises out of it the
|
|
17
|
+
* way the chosen tab does, and the short dividers on either side of it fade.
|
|
18
|
+
* A field or a filled button keeps its own surface, so it reads as raised
|
|
19
|
+
* from the start.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
+
* It does this with child selectors rather than props, so any `Button`,
|
|
22
|
+
* `Input`, `Menu.Trigger` or `ButtonGroup.Text` placed directly inside takes
|
|
23
|
+
* part without being told to.
|
|
24
|
+
*
|
|
25
|
+
* ## Split buttons
|
|
26
|
+
*
|
|
27
|
+
* A group with a `ButtonGroup.Separator` in it is a split button instead: no
|
|
28
|
+
* track, and its filled halves joined edge to edge, with the separator
|
|
29
|
+
* carrying their fill across the seam.
|
|
21
30
|
*
|
|
22
31
|
* ## Groups of groups
|
|
23
32
|
*
|
|
24
|
-
* A group whose children are groups spaces them apart
|
|
25
|
-
*
|
|
33
|
+
* A group whose children are groups spaces them apart, which is how a
|
|
34
|
+
* toolbar is built: one track per kind of action.
|
|
26
35
|
*/
|
|
27
36
|
import type { ComponentProps } from 'react';
|
|
28
37
|
import { type VariantProps } from 'tailwind-variants';
|
|
@@ -51,7 +60,7 @@ export declare const buttonGroupVariants: import("tailwind-variants").TVReturnTy
|
|
|
51
60
|
};
|
|
52
61
|
}, {
|
|
53
62
|
root: string[];
|
|
54
|
-
separator: string;
|
|
63
|
+
separator: string[];
|
|
55
64
|
text: string[];
|
|
56
65
|
}, undefined, {
|
|
57
66
|
orientation: {
|
|
@@ -78,7 +87,7 @@ export declare const buttonGroupVariants: import("tailwind-variants").TVReturnTy
|
|
|
78
87
|
};
|
|
79
88
|
}, {
|
|
80
89
|
root: string[];
|
|
81
|
-
separator: string;
|
|
90
|
+
separator: string[];
|
|
82
91
|
text: string[];
|
|
83
92
|
}, import("tailwind-variants").TVReturnType<{
|
|
84
93
|
orientation: {
|
|
@@ -105,7 +114,7 @@ export declare const buttonGroupVariants: import("tailwind-variants").TVReturnTy
|
|
|
105
114
|
};
|
|
106
115
|
}, {
|
|
107
116
|
root: string[];
|
|
108
|
-
separator: string;
|
|
117
|
+
separator: string[];
|
|
109
118
|
text: string[];
|
|
110
119
|
}, undefined, unknown, unknown, undefined>>;
|
|
111
120
|
export type ButtonGroupOrientation = NonNullable<VariantProps<typeof buttonGroupVariants>['orientation']>;
|
|
@@ -130,8 +139,11 @@ export interface ButtonGroupSeparatorProps extends ComponentProps<'span'> {
|
|
|
130
139
|
orientation?: 'vertical' | 'horizontal';
|
|
131
140
|
}
|
|
132
141
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
142
|
+
* The divider of a split button. A group with one in it drops its track and
|
|
143
|
+
* joins its children edge to edge. Before a primary, secondary or destructive
|
|
144
|
+
* button the separator takes that button's fill, so the two halves read as
|
|
145
|
+
* one, and draws a short line in the button's text colour. It is decorative
|
|
146
|
+
* and hidden from assistive technology.
|
|
135
147
|
*/
|
|
136
148
|
declare function ButtonGroupSeparator({ orientation, className, ...props }: ButtonGroupSeparatorProps): import("react").JSX.Element;
|
|
137
149
|
declare namespace ButtonGroupSeparator {
|
|
@@ -1,19 +1,53 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { tv } from 'tailwind-variants';
|
|
3
|
+
/*
|
|
4
|
+
* Which of the three a group is depends on what it holds, so the rules below
|
|
5
|
+
* are scoped by selector rather than by prop:
|
|
6
|
+
*
|
|
7
|
+
* track &:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))
|
|
8
|
+
* split &:has(>[data-slot=button-group-separator])
|
|
9
|
+
*
|
|
10
|
+
* Tailwind reads class names whole, so each selector is written out in full
|
|
11
|
+
* in every class that uses it.
|
|
12
|
+
*/
|
|
3
13
|
export const buttonGroupVariants = tv({
|
|
4
14
|
slots: {
|
|
5
15
|
root: [
|
|
6
16
|
'flex w-fit items-stretch',
|
|
7
17
|
// A focused child draws its ring over its neighbours, not under them.
|
|
8
18
|
'*:focus-visible:relative *:focus-visible:z-10',
|
|
9
|
-
// Groups inside a group are spaced, not joined.
|
|
10
|
-
'has-[>[data-slot=button-group]]:gap-2',
|
|
11
19
|
// A field takes the room the buttons leave.
|
|
12
20
|
'[&>[data-slot=input]]:min-w-0 [&>[data-slot=input]]:flex-1',
|
|
21
|
+
// The track, in the segmented Tabs' colour and radius. Its children
|
|
22
|
+
// take the radius one step down, which is the track's less its padding.
|
|
23
|
+
'[&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))]:gap-1 [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))]:rounded-xl [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))]:bg-muted [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))]:p-1',
|
|
24
|
+
'[&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*]:relative [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*]:rounded-lg',
|
|
25
|
+
// Outline and ghost buttons lie flat, with muted labels, and rise out
|
|
26
|
+
// of the track under the pointer like the chosen tab.
|
|
27
|
+
'[&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>:is([data-variant=outline],[data-variant=ghost])]:border-transparent [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>:is([data-variant=outline],[data-variant=ghost])]:bg-transparent [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>:is([data-variant=outline],[data-variant=ghost])]:text-muted-foreground [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>:is([data-variant=outline],[data-variant=ghost])]:shadow-none',
|
|
28
|
+
'[&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>:is([data-variant=outline],[data-variant=ghost]):hover:not(:disabled)]:bg-background [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>:is([data-variant=outline],[data-variant=ghost]):hover:not(:disabled)]:text-foreground [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>:is([data-variant=outline],[data-variant=ghost]):hover:not(:disabled)]:shadow-sm',
|
|
29
|
+
'[&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>:is([data-variant=outline],[data-variant=ghost]):hover:not(:disabled)]:inset-ring [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>:is([data-variant=outline],[data-variant=ghost]):hover:not(:disabled)]:inset-ring-foreground/[0.06]',
|
|
30
|
+
'dark:[&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>:is([data-variant=outline],[data-variant=ghost]):hover:not(:disabled)]:bg-input dark:[&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>:is([data-variant=outline],[data-variant=ghost]):hover:not(:disabled)]:inset-ring-foreground/10',
|
|
31
|
+
// Short dividers in the gaps, drawn by the second child of each pair so
|
|
32
|
+
// the first never gets one. None beside a field, which has its own
|
|
33
|
+
// edge, and none beside the child under the pointer.
|
|
34
|
+
"[&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*]:before:pointer-events-none [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*]:before:absolute [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*]:before:bg-foreground/10 [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*]:before:transition-opacity [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*]:before:duration-150 [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*]:before:content-['']",
|
|
35
|
+
'[&>[data-slot=input]+*]:before:hidden [&>*+[data-slot=input]]:before:hidden',
|
|
36
|
+
'[&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*:hover:not(:disabled)]:before:opacity-0 [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*:hover:not(:disabled)+*]:before:opacity-0',
|
|
37
|
+
// Groups inside a group are spaced, one track each.
|
|
38
|
+
'has-[>[data-slot=button-group]]:gap-2',
|
|
39
|
+
],
|
|
40
|
+
separator: [
|
|
41
|
+
'relative shrink-0 self-stretch bg-border',
|
|
42
|
+
// Between filled buttons the separator carries their fill across the
|
|
43
|
+
// seam, and a short line in their text colour divides it.
|
|
44
|
+
"before:absolute before:content-['']",
|
|
45
|
+
'has-[+[data-variant=default]]:bg-primary has-[+[data-variant=default]]:before:bg-primary-foreground/25',
|
|
46
|
+
'has-[+[data-variant=secondary]]:bg-secondary has-[+[data-variant=secondary]]:before:bg-secondary-foreground/15',
|
|
47
|
+
'has-[+[data-variant=destructive]]:bg-destructive has-[+[data-variant=destructive]]:before:bg-white/30',
|
|
13
48
|
],
|
|
14
|
-
separator: 'shrink-0 self-stretch bg-border',
|
|
15
49
|
text: [
|
|
16
|
-
'flex shrink-0 items-center gap-2 whitespace-nowrap
|
|
50
|
+
'relative flex shrink-0 items-center gap-2 whitespace-nowrap font-medium text-muted-foreground',
|
|
17
51
|
'[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
|
|
18
52
|
],
|
|
19
53
|
},
|
|
@@ -21,25 +55,26 @@ export const buttonGroupVariants = tv({
|
|
|
21
55
|
orientation: {
|
|
22
56
|
horizontal: {
|
|
23
57
|
root: [
|
|
24
|
-
'[
|
|
25
|
-
'
|
|
58
|
+
'[&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*]:before:inset-y-2 [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*]:before:-left-0.5 [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*]:before:w-px',
|
|
59
|
+
// A split button's halves meet edge to edge.
|
|
60
|
+
'[&:has(>[data-slot=button-group-separator])>*:not(:first-child)]:rounded-l-none [&:has(>[data-slot=button-group-separator])>*:not(:last-child)]:rounded-r-none',
|
|
26
61
|
],
|
|
27
|
-
separator: 'w-px',
|
|
62
|
+
separator: 'w-px before:inset-y-2 before:inset-x-0',
|
|
28
63
|
},
|
|
29
64
|
vertical: {
|
|
30
65
|
root: [
|
|
31
66
|
'flex-col',
|
|
32
|
-
'[
|
|
33
|
-
'[
|
|
67
|
+
'[&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*]:before:inset-x-2 [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*]:before:-top-0.5 [&:not(:has(>[data-slot=button-group-separator],>[data-slot=button-group]))>*+*]:before:h-px',
|
|
68
|
+
'[&:has(>[data-slot=button-group-separator])>*:not(:first-child)]:rounded-t-none [&:has(>[data-slot=button-group-separator])>*:not(:last-child)]:rounded-b-none',
|
|
34
69
|
],
|
|
35
|
-
separator: 'h-px',
|
|
70
|
+
separator: 'h-px before:inset-x-2 before:inset-y-0',
|
|
36
71
|
},
|
|
37
72
|
},
|
|
38
73
|
/** Matches the heights of `Button` and `Input` at the same size. */
|
|
39
74
|
size: {
|
|
40
|
-
sm: { text: 'h-8
|
|
41
|
-
md: { text: 'h-9
|
|
42
|
-
lg: { text: 'h-11
|
|
75
|
+
sm: { text: 'h-8 px-2.5 text-[0.8125rem]' },
|
|
76
|
+
md: { text: 'h-9 px-3 text-sm' },
|
|
77
|
+
lg: { text: 'h-11 px-3.5 text-[0.9375rem]' },
|
|
43
78
|
},
|
|
44
79
|
},
|
|
45
80
|
defaultVariants: { orientation: 'horizontal', size: 'md' },
|
|
@@ -53,8 +88,11 @@ function ButtonGroupRoot({ orientation = 'horizontal', className, ...props }) {
|
|
|
53
88
|
}
|
|
54
89
|
ButtonGroupRoot.displayName = 'ButtonGroup';
|
|
55
90
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
91
|
+
* The divider of a split button. A group with one in it drops its track and
|
|
92
|
+
* joins its children edge to edge. Before a primary, secondary or destructive
|
|
93
|
+
* button the separator takes that button's fill, so the two halves read as
|
|
94
|
+
* one, and draws a short line in the button's text colour. It is decorative
|
|
95
|
+
* and hidden from assistive technology.
|
|
58
96
|
*/
|
|
59
97
|
function ButtonGroupSeparator({ orientation = 'vertical', className, ...props }) {
|
|
60
98
|
const slots = buttonGroupVariants({
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChoiceCards — one option chosen from a few, each shown as a picture of
|
|
3
|
+
* what it does.
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <ChoiceCards defaultValue="rounded" aria-label="Pointer">
|
|
7
|
+
* <ChoiceCards.Item value="classic">
|
|
8
|
+
* <ChoiceCards.Media><ClassicPointer /></ChoiceCards.Media>
|
|
9
|
+
* <ChoiceCards.Label>Classic</ChoiceCards.Label>
|
|
10
|
+
* </ChoiceCards.Item>
|
|
11
|
+
* …
|
|
12
|
+
* </ChoiceCards>
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* ## Show the option, not its name
|
|
16
|
+
*
|
|
17
|
+
* A pointer style, an aspect ratio, a background: options that are seen
|
|
18
|
+
* before they are understood. Each card leads with a picture of the option,
|
|
19
|
+
* so the choice is made by looking, and the name under it confirms it.
|
|
20
|
+
*
|
|
21
|
+
* ## The chosen one comes forward
|
|
22
|
+
*
|
|
23
|
+
* The chosen card is ringed in the theme colour, a check settles into its
|
|
24
|
+
* corner, and its picture grows a little, as if lifted towards the reader.
|
|
25
|
+
* The others stay flat, so a row of cards still reads as one choice and one
|
|
26
|
+
* set of alternatives.
|
|
27
|
+
*/
|
|
28
|
+
import { Radio } from '@base-ui/react/radio';
|
|
29
|
+
import { RadioGroup } from '@base-ui/react/radio-group';
|
|
30
|
+
import { type ComponentProps, type CSSProperties, type ReactNode } from 'react';
|
|
31
|
+
export type ChoiceCardsColor = 'primary' | 'info' | 'success' | 'warning' | 'destructive';
|
|
32
|
+
declare const choiceCardsVariants: import("tailwind-variants").TVReturnType<{
|
|
33
|
+
color: {
|
|
34
|
+
primary: {
|
|
35
|
+
root: string;
|
|
36
|
+
};
|
|
37
|
+
info: {
|
|
38
|
+
root: string;
|
|
39
|
+
};
|
|
40
|
+
success: {
|
|
41
|
+
root: string;
|
|
42
|
+
};
|
|
43
|
+
warning: {
|
|
44
|
+
root: string;
|
|
45
|
+
};
|
|
46
|
+
destructive: {
|
|
47
|
+
root: string;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
}, {
|
|
51
|
+
root: string;
|
|
52
|
+
item: string[];
|
|
53
|
+
media: string[];
|
|
54
|
+
check: string[];
|
|
55
|
+
text: string;
|
|
56
|
+
label: string;
|
|
57
|
+
description: string;
|
|
58
|
+
}, undefined, {
|
|
59
|
+
color: {
|
|
60
|
+
primary: {
|
|
61
|
+
root: string;
|
|
62
|
+
};
|
|
63
|
+
info: {
|
|
64
|
+
root: string;
|
|
65
|
+
};
|
|
66
|
+
success: {
|
|
67
|
+
root: string;
|
|
68
|
+
};
|
|
69
|
+
warning: {
|
|
70
|
+
root: string;
|
|
71
|
+
};
|
|
72
|
+
destructive: {
|
|
73
|
+
root: string;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
}, {
|
|
77
|
+
root: string;
|
|
78
|
+
item: string[];
|
|
79
|
+
media: string[];
|
|
80
|
+
check: string[];
|
|
81
|
+
text: string;
|
|
82
|
+
label: string;
|
|
83
|
+
description: string;
|
|
84
|
+
}, import("tailwind-variants").TVReturnType<{
|
|
85
|
+
color: {
|
|
86
|
+
primary: {
|
|
87
|
+
root: string;
|
|
88
|
+
};
|
|
89
|
+
info: {
|
|
90
|
+
root: string;
|
|
91
|
+
};
|
|
92
|
+
success: {
|
|
93
|
+
root: string;
|
|
94
|
+
};
|
|
95
|
+
warning: {
|
|
96
|
+
root: string;
|
|
97
|
+
};
|
|
98
|
+
destructive: {
|
|
99
|
+
root: string;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
}, {
|
|
103
|
+
root: string;
|
|
104
|
+
item: string[];
|
|
105
|
+
media: string[];
|
|
106
|
+
check: string[];
|
|
107
|
+
text: string;
|
|
108
|
+
label: string;
|
|
109
|
+
description: string;
|
|
110
|
+
}, undefined, unknown, unknown, undefined>>;
|
|
111
|
+
export interface ChoiceCardsProps extends Omit<RadioGroup.Props<string>, 'className' | 'style' | 'children'> {
|
|
112
|
+
/** How many cards stand in a row. Default: one per item, up to four. */
|
|
113
|
+
columns?: number;
|
|
114
|
+
/** The shape of every card's picture, as a CSS aspect ratio. Default `4 / 3`. */
|
|
115
|
+
aspect?: string;
|
|
116
|
+
/** The colour of the ring and the check on the chosen card. Default `primary`. */
|
|
117
|
+
color?: ChoiceCardsColor;
|
|
118
|
+
children?: ReactNode;
|
|
119
|
+
className?: string;
|
|
120
|
+
style?: CSSProperties;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The cards, of which one is chosen. `value` and `onValueChange`, or
|
|
124
|
+
* `defaultValue`, hold which; `name` submits it with a form.
|
|
125
|
+
*/
|
|
126
|
+
declare function ChoiceCardsRoot({ columns, aspect, color, className, style, children, ...props }: ChoiceCardsProps): import("react").JSX.Element;
|
|
127
|
+
declare namespace ChoiceCardsRoot {
|
|
128
|
+
var displayName: string;
|
|
129
|
+
}
|
|
130
|
+
export interface ChoiceCardsItemProps extends Omit<Radio.Root.Props, 'className' | 'value'> {
|
|
131
|
+
/** What choosing this card sets the group's value to. */
|
|
132
|
+
value: string;
|
|
133
|
+
className?: string;
|
|
134
|
+
}
|
|
135
|
+
/** One option: a `ChoiceCards.Media` picture, a `Label`, and maybe a `Description`. */
|
|
136
|
+
declare function ChoiceCardsItem({ className, children, ...props }: ChoiceCardsItemProps): import("react").JSX.Element;
|
|
137
|
+
declare namespace ChoiceCardsItem {
|
|
138
|
+
var displayName: string;
|
|
139
|
+
}
|
|
140
|
+
export interface ChoiceCardsMediaProps extends ComponentProps<'span'> {
|
|
141
|
+
}
|
|
142
|
+
/** The picture of the option: a drawing, an icon or an image, centred in a rounded frame. */
|
|
143
|
+
declare function ChoiceCardsMedia({ className, ...props }: ChoiceCardsMediaProps): import("react").JSX.Element;
|
|
144
|
+
declare namespace ChoiceCardsMedia {
|
|
145
|
+
var displayName: string;
|
|
146
|
+
}
|
|
147
|
+
export interface ChoiceCardsLabelProps extends ComponentProps<'span'> {
|
|
148
|
+
}
|
|
149
|
+
/** The option's name, under its picture. It is what a screen reader calls the card. */
|
|
150
|
+
declare function ChoiceCardsLabel({ className, ...props }: ChoiceCardsLabelProps): import("react").JSX.Element;
|
|
151
|
+
declare namespace ChoiceCardsLabel {
|
|
152
|
+
var displayName: string;
|
|
153
|
+
}
|
|
154
|
+
export interface ChoiceCardsDescriptionProps extends ComponentProps<'span'> {
|
|
155
|
+
}
|
|
156
|
+
/** A line under the label: what the option costs or does. */
|
|
157
|
+
declare function ChoiceCardsDescription({ className, ...props }: ChoiceCardsDescriptionProps): import("react").JSX.Element;
|
|
158
|
+
declare namespace ChoiceCardsDescription {
|
|
159
|
+
var displayName: string;
|
|
160
|
+
}
|
|
161
|
+
export declare const ChoiceCards: typeof ChoiceCardsRoot & {
|
|
162
|
+
Item: typeof ChoiceCardsItem;
|
|
163
|
+
Media: typeof ChoiceCardsMedia;
|
|
164
|
+
Label: typeof ChoiceCardsLabel;
|
|
165
|
+
Description: typeof ChoiceCardsDescription;
|
|
166
|
+
};
|
|
167
|
+
export { ChoiceCardsItem, ChoiceCardsMedia, ChoiceCardsLabel, ChoiceCardsDescription, choiceCardsVariants };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* ChoiceCards — one option chosen from a few, each shown as a picture of
|
|
5
|
+
* what it does.
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <ChoiceCards defaultValue="rounded" aria-label="Pointer">
|
|
9
|
+
* <ChoiceCards.Item value="classic">
|
|
10
|
+
* <ChoiceCards.Media><ClassicPointer /></ChoiceCards.Media>
|
|
11
|
+
* <ChoiceCards.Label>Classic</ChoiceCards.Label>
|
|
12
|
+
* </ChoiceCards.Item>
|
|
13
|
+
* …
|
|
14
|
+
* </ChoiceCards>
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ## Show the option, not its name
|
|
18
|
+
*
|
|
19
|
+
* A pointer style, an aspect ratio, a background: options that are seen
|
|
20
|
+
* before they are understood. Each card leads with a picture of the option,
|
|
21
|
+
* so the choice is made by looking, and the name under it confirms it.
|
|
22
|
+
*
|
|
23
|
+
* ## The chosen one comes forward
|
|
24
|
+
*
|
|
25
|
+
* The chosen card is ringed in the theme colour, a check settles into its
|
|
26
|
+
* corner, and its picture grows a little, as if lifted towards the reader.
|
|
27
|
+
* The others stay flat, so a row of cards still reads as one choice and one
|
|
28
|
+
* set of alternatives.
|
|
29
|
+
*/
|
|
30
|
+
import { Radio } from '@base-ui/react/radio';
|
|
31
|
+
import { RadioGroup } from '@base-ui/react/radio-group';
|
|
32
|
+
import { createContext, useContext, useId } from 'react';
|
|
33
|
+
import { tv } from 'tailwind-variants';
|
|
34
|
+
const SPRING = 'ease-[cubic-bezier(0.3,1.35,0.45,1)]';
|
|
35
|
+
const choiceCardsVariants = tv({
|
|
36
|
+
slots: {
|
|
37
|
+
root: 'grid w-full grid-cols-(--choice-columns) gap-2 data-disabled:opacity-50',
|
|
38
|
+
item: [
|
|
39
|
+
'group/choice relative flex min-w-0 cursor-pointer flex-col gap-1.5 rounded-[18px] p-1.5 text-start outline-none select-none',
|
|
40
|
+
'bg-muted/60 transition-[background-color,box-shadow] duration-200 hover:bg-muted',
|
|
41
|
+
// The ring stands clear of the card, so it never sits on the picture. It
|
|
42
|
+
// is an outline, whose gap is see-through on whatever surface holds it.
|
|
43
|
+
'outline-offset-2 data-checked:bg-muted data-checked:outline-2 data-checked:outline-solid data-checked:outline-(--choice)',
|
|
44
|
+
'focus-visible:ring-2 focus-visible:ring-ring',
|
|
45
|
+
'data-disabled:cursor-not-allowed data-disabled:opacity-50',
|
|
46
|
+
],
|
|
47
|
+
media: [
|
|
48
|
+
'relative flex aspect-(--choice-aspect) w-full items-center justify-center overflow-hidden rounded-xl',
|
|
49
|
+
// A frame a step lighter than the card in either theme, with a hairline edge.
|
|
50
|
+
'bg-background/80 ring-1 ring-inset ring-foreground/[0.05] dark:bg-foreground/[0.06]',
|
|
51
|
+
'text-foreground/80 [&>*]:transition-transform [&>*]:duration-[420ms] [&>*]:ease-[cubic-bezier(0.3,1.35,0.45,1)]',
|
|
52
|
+
'group-data-checked/choice:text-foreground group-data-checked/choice:[&>*]:scale-[1.06] motion-reduce:[&>*]:transition-none',
|
|
53
|
+
'[&>img]:size-full [&>img]:object-cover',
|
|
54
|
+
],
|
|
55
|
+
check: [
|
|
56
|
+
'pointer-events-none absolute end-1.5 top-1.5 z-10 flex size-5 items-center justify-center rounded-full bg-(--choice) text-(--choice-foreground)',
|
|
57
|
+
`scale-0 opacity-0 shadow-sm transition-[scale,opacity] duration-300 ${SPRING}`,
|
|
58
|
+
'group-data-checked/choice:scale-100 group-data-checked/choice:opacity-100 motion-reduce:transition-none',
|
|
59
|
+
],
|
|
60
|
+
text: 'flex flex-col gap-0.5 px-1.5 pb-1',
|
|
61
|
+
label: 'truncate text-[13px] font-medium text-foreground/80 group-data-checked/choice:text-foreground',
|
|
62
|
+
description: 'text-xs leading-4 text-muted-foreground',
|
|
63
|
+
},
|
|
64
|
+
variants: {
|
|
65
|
+
color: {
|
|
66
|
+
primary: { root: '[--choice:var(--primary)] [--choice-foreground:var(--primary-foreground)]' },
|
|
67
|
+
info: { root: '[--choice:var(--info)] [--choice-foreground:white]' },
|
|
68
|
+
success: { root: '[--choice:var(--success)] [--choice-foreground:white]' },
|
|
69
|
+
warning: { root: '[--choice:var(--warning)] [--choice-foreground:black]' },
|
|
70
|
+
destructive: { root: '[--choice:var(--destructive)] [--choice-foreground:white]' },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
defaultVariants: { color: 'primary' },
|
|
74
|
+
});
|
|
75
|
+
const ItemContext = createContext(null);
|
|
76
|
+
/**
|
|
77
|
+
* The cards, of which one is chosen. `value` and `onValueChange`, or
|
|
78
|
+
* `defaultValue`, hold which; `name` submits it with a form.
|
|
79
|
+
*/
|
|
80
|
+
function ChoiceCardsRoot({ columns, aspect = '4 / 3', color, className, style, children, ...props }) {
|
|
81
|
+
const slots = choiceCardsVariants({ color });
|
|
82
|
+
const count = Array.isArray(children) ? children.filter(Boolean).length : 1;
|
|
83
|
+
const across = columns ?? Math.min(Math.max(count, 1), 4);
|
|
84
|
+
return (_jsx(RadioGroup, { "data-slot": "choice-cards", className: slots.root({ className }), style: {
|
|
85
|
+
...style,
|
|
86
|
+
'--choice-columns': `repeat(${across}, minmax(0, 1fr))`,
|
|
87
|
+
'--choice-aspect': aspect,
|
|
88
|
+
}, ...props, children: children }));
|
|
89
|
+
}
|
|
90
|
+
ChoiceCardsRoot.displayName = 'ChoiceCards';
|
|
91
|
+
/** One option: a `ChoiceCards.Media` picture, a `Label`, and maybe a `Description`. */
|
|
92
|
+
function ChoiceCardsItem({ className, children, ...props }) {
|
|
93
|
+
const slots = choiceCardsVariants();
|
|
94
|
+
const id = useId();
|
|
95
|
+
const ids = { labelId: `${id}-label`, descriptionId: `${id}-description` };
|
|
96
|
+
return (_jsx(ItemContext.Provider, { value: ids, children: _jsxs(Radio.Root, { "data-slot": "choice-cards-item", "aria-labelledby": ids.labelId, "aria-describedby": ids.descriptionId, className: slots.item({ className }), ...props, children: [_jsx("span", { "aria-hidden": true, "data-slot": "choice-cards-check", className: slots.check(), children: _jsx("svg", { viewBox: "0 0 12 12", className: "size-3", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M2.75 6.25 5 8.5l4.25-5" }) }) }), children] }) }));
|
|
97
|
+
}
|
|
98
|
+
ChoiceCardsItem.displayName = 'ChoiceCards.Item';
|
|
99
|
+
/** The picture of the option: a drawing, an icon or an image, centred in a rounded frame. */
|
|
100
|
+
function ChoiceCardsMedia({ className, ...props }) {
|
|
101
|
+
const slots = choiceCardsVariants();
|
|
102
|
+
return _jsx("span", { "aria-hidden": true, "data-slot": "choice-cards-media", className: slots.media({ className }), ...props });
|
|
103
|
+
}
|
|
104
|
+
ChoiceCardsMedia.displayName = 'ChoiceCards.Media';
|
|
105
|
+
/** Gathers the label and description under the picture, with the card's padding. */
|
|
106
|
+
function useItem(part) {
|
|
107
|
+
const item = useContext(ItemContext);
|
|
108
|
+
if (!item)
|
|
109
|
+
throw new Error(`${part} must be used inside a <ChoiceCards.Item>.`);
|
|
110
|
+
return item;
|
|
111
|
+
}
|
|
112
|
+
/** The option's name, under its picture. It is what a screen reader calls the card. */
|
|
113
|
+
function ChoiceCardsLabel({ className, ...props }) {
|
|
114
|
+
const { labelId } = useItem('ChoiceCards.Label');
|
|
115
|
+
const slots = choiceCardsVariants();
|
|
116
|
+
return (_jsx("span", { "data-slot": "choice-cards-text", className: slots.text(), children: _jsx("span", { id: labelId, "data-slot": "choice-cards-label", className: slots.label({ className }), ...props }) }));
|
|
117
|
+
}
|
|
118
|
+
ChoiceCardsLabel.displayName = 'ChoiceCards.Label';
|
|
119
|
+
/** A line under the label: what the option costs or does. */
|
|
120
|
+
function ChoiceCardsDescription({ className, ...props }) {
|
|
121
|
+
const { descriptionId } = useItem('ChoiceCards.Description');
|
|
122
|
+
const slots = choiceCardsVariants();
|
|
123
|
+
return (_jsx("span", { id: descriptionId, "data-slot": "choice-cards-description", className: slots.description({ className: ['-mt-1.5 px-1.5 pb-1', className] }), ...props }));
|
|
124
|
+
}
|
|
125
|
+
ChoiceCardsDescription.displayName = 'ChoiceCards.Description';
|
|
126
|
+
export const ChoiceCards = Object.assign(ChoiceCardsRoot, {
|
|
127
|
+
Item: ChoiceCardsItem,
|
|
128
|
+
Media: ChoiceCardsMedia,
|
|
129
|
+
Label: ChoiceCardsLabel,
|
|
130
|
+
Description: ChoiceCardsDescription,
|
|
131
|
+
});
|
|
132
|
+
export { ChoiceCardsItem, ChoiceCardsMedia, ChoiceCardsLabel, ChoiceCardsDescription, choiceCardsVariants };
|