@sixthshift/design-system 0.7.0 → 0.9.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 +5 -4
- package/dist/components/Badge/Badge.d.ts +17 -35
- package/dist/components/Badge/Badge.d.ts.map +1 -1
- package/dist/components/Badge/Badge.js +19 -22
- package/dist/components/Badge/Badge.js.map +1 -1
- package/dist/components/Badge/index.d.ts +1 -1
- package/dist/components/Badge/index.d.ts.map +1 -1
- package/dist/components/Badge/index.js +1 -1
- package/dist/components/Badge/index.js.map +1 -1
- package/dist/components/Button/Button.d.ts +23 -41
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Button/Button.js +28 -35
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Button/index.d.ts +1 -1
- package/dist/components/Button/index.d.ts.map +1 -1
- package/dist/components/Button/index.js +1 -1
- package/dist/components/Button/index.js.map +1 -1
- package/dist/components/Card/Card.d.ts +2 -2
- package/dist/components/Card/Card.js +1 -1
- package/dist/components/Card/Card.js.map +1 -1
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts +20 -16
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -1
- package/dist/components/CheckboxGroup/CheckboxGroup.js +12 -10
- package/dist/components/CheckboxGroup/CheckboxGroup.js.map +1 -1
- package/dist/components/Code/Workspace/Toolbar.d.ts +2 -1
- package/dist/components/Code/Workspace/Toolbar.d.ts.map +1 -1
- package/dist/components/Code/Workspace/Toolbar.js.map +1 -1
- package/dist/components/DataTable/DataTable.d.ts +89 -0
- package/dist/components/DataTable/DataTable.d.ts.map +1 -0
- package/dist/components/DataTable/DataTable.js +107 -0
- package/dist/components/DataTable/DataTable.js.map +1 -0
- package/dist/components/DataTable/index.d.ts +2 -0
- package/dist/components/DataTable/index.d.ts.map +1 -0
- package/dist/components/DataTable/index.js +2 -0
- package/dist/components/DataTable/index.js.map +1 -0
- package/dist/components/FileDropzone/FileDropzone.d.ts +87 -0
- package/dist/components/FileDropzone/FileDropzone.d.ts.map +1 -0
- package/dist/components/FileDropzone/FileDropzone.js +146 -0
- package/dist/components/FileDropzone/FileDropzone.js.map +1 -0
- package/dist/components/FileDropzone/index.d.ts +2 -0
- package/dist/components/FileDropzone/index.d.ts.map +1 -0
- package/dist/components/FileDropzone/index.js +2 -0
- package/dist/components/FileDropzone/index.js.map +1 -0
- package/dist/components/FormField/FormField.d.ts +3 -1
- package/dist/components/FormField/FormField.d.ts.map +1 -1
- package/dist/components/FormField/FormField.js.map +1 -1
- package/dist/components/ImageGallery/ImageGallery.d.ts +93 -0
- package/dist/components/ImageGallery/ImageGallery.d.ts.map +1 -0
- package/dist/components/ImageGallery/ImageGallery.js +121 -0
- package/dist/components/ImageGallery/ImageGallery.js.map +1 -0
- package/dist/components/ImageGallery/index.d.ts +2 -0
- package/dist/components/ImageGallery/index.d.ts.map +1 -0
- package/dist/components/ImageGallery/index.js +2 -0
- package/dist/components/ImageGallery/index.js.map +1 -0
- package/dist/components/Input/Input.d.ts +3 -3
- package/dist/components/Input/Input.js +3 -3
- package/dist/components/Markdown/Markdown.js +1 -1
- package/dist/components/Markdown/Markdown.js.map +1 -1
- package/dist/components/NumberStepper/NumberStepper.d.ts +78 -0
- package/dist/components/NumberStepper/NumberStepper.d.ts.map +1 -0
- package/dist/components/NumberStepper/NumberStepper.js +129 -0
- package/dist/components/NumberStepper/NumberStepper.js.map +1 -0
- package/dist/components/NumberStepper/index.d.ts +2 -0
- package/dist/components/NumberStepper/index.d.ts.map +1 -0
- package/dist/components/NumberStepper/index.js +2 -0
- package/dist/components/NumberStepper/index.js.map +1 -0
- package/dist/components/ProgressBar/ProgressBar.d.ts +11 -0
- package/dist/components/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/components/ProgressBar/ProgressBar.js +6 -2
- package/dist/components/ProgressBar/ProgressBar.js.map +1 -1
- package/dist/components/ProgressBar/index.d.ts +1 -1
- package/dist/components/ProgressBar/index.d.ts.map +1 -1
- package/dist/components/ProgressBar/index.js.map +1 -1
- package/dist/components/RadioButtonGroup/RadioButtonGroup.d.ts +19 -16
- package/dist/components/RadioButtonGroup/RadioButtonGroup.d.ts.map +1 -1
- package/dist/components/RadioButtonGroup/RadioButtonGroup.js +13 -12
- package/dist/components/RadioButtonGroup/RadioButtonGroup.js.map +1 -1
- package/dist/components/SearchInput/SearchInput.d.ts +3 -2
- package/dist/components/SearchInput/SearchInput.d.ts.map +1 -1
- package/dist/components/SearchInput/SearchInput.js +13 -8
- package/dist/components/SearchInput/SearchInput.js.map +1 -1
- package/dist/components/Sparkline/Sparkline.d.ts +8 -4
- package/dist/components/Sparkline/Sparkline.d.ts.map +1 -1
- package/dist/components/Sparkline/Sparkline.js +1 -1
- package/dist/components/Sparkline/Sparkline.js.map +1 -1
- package/dist/components/Sparkline/index.d.ts +1 -1
- package/dist/components/Sparkline/index.d.ts.map +1 -1
- package/dist/components/Sparkline/index.js.map +1 -1
- package/dist/components/Steps/Steps.d.ts +65 -0
- package/dist/components/Steps/Steps.d.ts.map +1 -0
- package/dist/components/Steps/Steps.js +62 -0
- package/dist/components/Steps/Steps.js.map +1 -0
- package/dist/components/Steps/index.d.ts +2 -0
- package/dist/components/Steps/index.d.ts.map +1 -0
- package/dist/components/Steps/index.js +2 -0
- package/dist/components/Steps/index.js.map +1 -0
- package/dist/components/TagChip/TagChip.d.ts +0 -13
- package/dist/components/TagChip/TagChip.d.ts.map +1 -1
- package/dist/components/TagChip/TagChip.js +10 -4
- package/dist/components/TagChip/TagChip.js.map +1 -1
- package/dist/components/TextInline/TextInline.d.ts +2 -2
- package/dist/components/Toast/Toast.js +1 -1
- package/dist/components/Toast/Toast.js.map +1 -1
- package/dist/components/Toggle/Toggle.d.ts +29 -18
- package/dist/components/Toggle/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle/Toggle.js +48 -11
- package/dist/components/Toggle/Toggle.js.map +1 -1
- package/dist/components/Toggle/index.d.ts +1 -1
- package/dist/components/Toggle/index.d.ts.map +1 -1
- package/dist/components/Toggle/index.js +1 -1
- package/dist/components/Toggle/index.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +4 -4
- package/dist/components/ToggleGroup/ToggleGroup.js +4 -4
- package/dist/components/ToggleGroup/ToggleGroupItem.d.ts +1 -1
- package/dist/components/ToggleGroup/ToggleGroupItem.d.ts.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroupItem.js +37 -7
- package/dist/components/ToggleGroup/ToggleGroupItem.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroupMultiple.d.ts +3 -3
- package/dist/components/ToggleGroup/ToggleGroupSingle.d.ts +3 -3
- package/dist/components/ToggleGroup/index.d.ts +1 -1
- package/dist/components/ToggleGroup/index.d.ts.map +1 -1
- package/dist/components/ToggleGroup/toggleGroup.types.d.ts +18 -16
- package/dist/components/ToggleGroup/toggleGroup.types.d.ts.map +1 -1
- package/dist/typography/Code.js +1 -1
- package/dist/typography/Code.js.map +1 -1
- package/package.json +22 -1
- package/src/components/Badge/Badge.tsx +38 -45
- package/src/components/Badge/badge.recipe.css +20 -101
- package/src/components/Badge/index.ts +0 -1
- package/src/components/Button/Button.tsx +57 -51
- package/src/components/Button/button.recipe.css +39 -122
- package/src/components/Button/index.ts +0 -1
- package/src/components/Card/Card.tsx +1 -1
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +17 -15
- package/src/components/Code/Workspace/Toolbar.tsx +2 -2
- package/src/components/DataTable/DataTable.tsx +261 -0
- package/src/components/DataTable/data-table.recipe.css +28 -0
- package/src/components/DataTable/index.ts +10 -0
- package/src/components/FileDropzone/FileDropzone.tsx +239 -0
- package/src/components/FileDropzone/file-dropzone.recipe.css +38 -0
- package/src/components/FileDropzone/index.ts +1 -0
- package/src/components/FormField/FormField.tsx +3 -2
- package/src/components/ImageGallery/ImageGallery.tsx +285 -0
- package/src/components/ImageGallery/image-gallery.recipe.css +50 -0
- package/src/components/ImageGallery/index.ts +7 -0
- package/src/components/Input/Input.tsx +3 -3
- package/src/components/Markdown/Markdown.tsx +1 -1
- package/src/components/Message/message.recipe.css +13 -28
- package/src/components/NumberStepper/NumberStepper.tsx +227 -0
- package/src/components/NumberStepper/index.ts +1 -0
- package/src/components/NumberStepper/number-stepper.recipe.css +31 -0
- package/src/components/ProgressBar/ProgressBar.tsx +16 -4
- package/src/components/ProgressBar/index.ts +1 -1
- package/src/components/ProgressBar/progress-bar.recipe.css +29 -0
- package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +18 -17
- package/src/components/SearchInput/SearchInput.tsx +34 -20
- package/src/components/SearchInput/search-input.recipe.css +14 -6
- package/src/components/Sparkline/Sparkline.tsx +9 -4
- package/src/components/Sparkline/index.ts +1 -1
- package/src/components/Steps/Steps.tsx +149 -0
- package/src/components/Steps/index.ts +1 -0
- package/src/components/Steps/steps.recipe.css +51 -0
- package/src/components/TagChip/TagChip.tsx +17 -8
- package/src/components/TagChip/tag-chip.recipe.css +12 -12
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/components/Toggle/Toggle.tsx +67 -21
- package/src/components/Toggle/index.ts +9 -1
- package/src/components/Toggle/toggle.recipe.css +45 -73
- package/src/components/ToggleGroup/ToggleGroup.tsx +4 -4
- package/src/components/ToggleGroup/ToggleGroupItem.tsx +46 -11
- package/src/components/ToggleGroup/index.ts +9 -1
- package/src/components/ToggleGroup/toggle-group.recipe.css +62 -0
- package/src/components/ToggleGroup/toggleGroup.types.ts +18 -16
- package/src/theme/linen/theme.css +21 -0
- package/src/theming/intents.css +112 -0
- package/src/theming/tailwind.css +22 -1
- package/src/typography/Code.tsx +1 -1
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Button recipe — the component-token layer for Button.
|
|
3
3
|
*
|
|
4
|
-
* This is the mapping from `(variant,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* This is the mapping from `(variant, state)` to a colour. It used to live in
|
|
5
|
+
* `compoundVariants` inside Button.tsx, where it was compiled into class-name
|
|
6
|
+
* literals and no consumer could reach it. Here it is data, so a consumer can
|
|
7
|
+
* re-point a cell, scope a change to a subtree, or add a variant the library
|
|
8
|
+
* never shipped.
|
|
9
|
+
*
|
|
10
|
+
* Intent does not appear here at all. Each cell below paints with the intent
|
|
11
|
+
* slots (`--intent-solid-bg`, `--intent-fg`, …) that src/theming/intents.css
|
|
12
|
+
* declares per `data-intent`, so every variant works in every intent — including
|
|
13
|
+
* one a consumer adds — and this file has one cell per variant, not one per
|
|
14
|
+
* `(variant, intent)` pair.
|
|
9
15
|
*
|
|
10
16
|
* The grammar is `--button-{context}[-{state}]` — the same `context` and
|
|
11
17
|
* `state` words the semantic layer uses (src/theming/vocabulary.ts). Note what is
|
|
@@ -37,131 +43,42 @@
|
|
|
37
43
|
--button-ring: var(--focus-ring);
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
/* solid — a filled surface, with text read on top of it. The
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
--button-bg
|
|
46
|
-
--button-bg-
|
|
47
|
-
--button-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/* `neutral` means *no colour family* — the grey ramp. It used to resolve to
|
|
51
|
-
the brand fill, which made `intent="neutral"` mean one thing on Button and
|
|
52
|
-
another on Badge and Message, where it was already grey. Button
|
|
53
|
-
was the outlier; this is the cell that ends that. A brand-coloured button
|
|
54
|
-
is now `intent="brand"`, which is also Button's default, so a bare
|
|
55
|
-
`<Button>` is unchanged. */
|
|
56
|
-
.btn[data-variant="solid"][data-intent="neutral"] {
|
|
57
|
-
--button-bg: var(--bg-strong);
|
|
58
|
-
--button-bg-hovered: var(--bg-strong-hovered);
|
|
59
|
-
--button-bg-pressed: var(--bg-strong-pressed);
|
|
60
|
-
--button-fg: var(--fg-on-strong);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.btn[data-variant="solid"][data-intent="danger"] {
|
|
64
|
-
--button-bg: var(--bg-danger);
|
|
65
|
-
--button-bg-hovered: var(--bg-danger-hovered);
|
|
66
|
-
--button-bg-pressed: var(--bg-danger-pressed);
|
|
67
|
-
--button-fg: var(--fg-on-danger);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.btn[data-variant="solid"][data-intent="success"] {
|
|
71
|
-
--button-bg: var(--bg-success);
|
|
72
|
-
--button-bg-hovered: var(--bg-success-hovered);
|
|
73
|
-
--button-bg-pressed: var(--bg-success-pressed);
|
|
74
|
-
--button-fg: var(--fg-on-success);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.btn[data-variant="solid"][data-intent="warning"] {
|
|
78
|
-
--button-bg: var(--bg-warning);
|
|
79
|
-
--button-bg-hovered: var(--bg-warning-hovered);
|
|
80
|
-
--button-bg-pressed: var(--bg-warning-pressed);
|
|
81
|
-
--button-fg: var(--fg-on-warning);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/* outline carries a resting surface; ghost does not. Beyond that their
|
|
85
|
-
colour recipes are identical, so the text colour and the hover/pressed wash
|
|
86
|
-
are shared per intent and only the two lines below differ. */
|
|
87
|
-
.btn[data-variant="outline"] {
|
|
88
|
-
--button-bg: var(--bg-normal);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/* `neutral` here needed no change: outline and ghost were already grey — it
|
|
92
|
-
was only the solid and link cells that read as brand. */
|
|
93
|
-
.btn[data-variant="outline"][data-intent="neutral"],
|
|
94
|
-
.btn[data-variant="ghost"][data-intent="neutral"] {
|
|
95
|
-
--button-bg-hovered: var(--bg-subtle-hovered);
|
|
96
|
-
--button-bg-pressed: var(--bg-subtle-pressed);
|
|
97
|
-
--button-fg: var(--fg-normal);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.btn[data-variant="outline"][data-intent="brand"],
|
|
101
|
-
.btn[data-variant="ghost"][data-intent="brand"] {
|
|
102
|
-
--button-bg-hovered: var(--bg-brand-subtle-hovered);
|
|
103
|
-
--button-bg-pressed: var(--bg-brand-subtle-pressed);
|
|
104
|
-
--button-fg: var(--fg-brand);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.btn[data-variant="outline"][data-intent="danger"],
|
|
108
|
-
.btn[data-variant="ghost"][data-intent="danger"] {
|
|
109
|
-
--button-bg-hovered: var(--bg-danger-subtle-hovered);
|
|
110
|
-
--button-bg-pressed: var(--bg-danger-subtle-pressed);
|
|
111
|
-
--button-fg: var(--fg-danger);
|
|
46
|
+
/* solid — a filled surface, with text read on top of it. The slot pair
|
|
47
|
+
resolves to a `bg-*` / `fg-on-*` partnership, which is the pairing
|
|
48
|
+
check-contrast.ts verifies, so this cell inherits that guarantee. For
|
|
49
|
+
`neutral`, the default, that is the grey `bg-strong` fill. */
|
|
50
|
+
.btn[data-variant="solid"] {
|
|
51
|
+
--button-bg: var(--intent-solid-bg);
|
|
52
|
+
--button-bg-hovered: var(--intent-solid-bg-hovered);
|
|
53
|
+
--button-bg-pressed: var(--intent-solid-bg-pressed);
|
|
54
|
+
--button-fg: var(--intent-solid-fg);
|
|
112
55
|
}
|
|
113
56
|
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
--button-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
.btn[data-variant="outline"][data-intent="warning"],
|
|
122
|
-
.btn[data-variant="ghost"][data-intent="warning"] {
|
|
123
|
-
--button-bg-hovered: var(--bg-warning-subtle-hovered);
|
|
124
|
-
--button-bg-pressed: var(--bg-warning-subtle-pressed);
|
|
125
|
-
--button-fg: var(--fg-warning);
|
|
57
|
+
/* outline carries a resting surface; ghost does not. Beyond that they share
|
|
58
|
+
the family's text colour and its tint wash on hover and press. */
|
|
59
|
+
.btn[data-variant="outline"],
|
|
60
|
+
.btn[data-variant="ghost"] {
|
|
61
|
+
--button-bg-hovered: var(--intent-tint-bg-hovered);
|
|
62
|
+
--button-bg-pressed: var(--intent-tint-bg-pressed);
|
|
63
|
+
--button-fg: var(--intent-fg);
|
|
126
64
|
}
|
|
127
65
|
|
|
128
66
|
/* The border belongs to outline alone. Ghost has no border width, so folding
|
|
129
|
-
|
|
67
|
+
this into the block above would be invisible today — and a silent
|
|
130
68
|
semantic change the moment anyone gave ghost a border. A consumer who wants
|
|
131
69
|
that can still say so in one line; it should be their decision, not a
|
|
132
|
-
side-effect of how this file was factored.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
--button-border: var(--border
|
|
136
|
-
}
|
|
137
|
-
.btn[data-variant="outline"][data-intent="danger"] {
|
|
138
|
-
--button-border: var(--border-danger);
|
|
139
|
-
}
|
|
140
|
-
.btn[data-variant="outline"][data-intent="success"] {
|
|
141
|
-
--button-border: var(--border-success);
|
|
142
|
-
}
|
|
143
|
-
.btn[data-variant="outline"][data-intent="warning"] {
|
|
144
|
-
--button-border: var(--border-warning);
|
|
70
|
+
side-effect of how this file was factored. */
|
|
71
|
+
.btn[data-variant="outline"] {
|
|
72
|
+
--button-bg: var(--bg-normal);
|
|
73
|
+
--button-border: var(--intent-border);
|
|
145
74
|
}
|
|
146
75
|
|
|
147
|
-
/*
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
.btn[data-
|
|
153
|
-
--button-fg: var(--fg
|
|
154
|
-
}
|
|
155
|
-
.btn[data-variant="link"][data-intent="neutral"] {
|
|
156
|
-
--button-fg: var(--fg-normal);
|
|
157
|
-
}
|
|
158
|
-
.btn[data-variant="link"][data-intent="danger"] {
|
|
159
|
-
--button-fg: var(--fg-danger);
|
|
160
|
-
}
|
|
161
|
-
.btn[data-variant="link"][data-intent="success"] {
|
|
162
|
-
--button-fg: var(--fg-success);
|
|
163
|
-
}
|
|
164
|
-
.btn[data-variant="link"][data-intent="warning"] {
|
|
165
|
-
--button-fg: var(--fg-warning);
|
|
76
|
+
/* inline — not a variant: inline text with no box (Button drops
|
|
77
|
+
`data-variant` for it, so no cell above can match). Only the text carries
|
|
78
|
+
colour, so the floor's transparent background and its no-shift-on-hover
|
|
79
|
+
chain both stand. It is brand-coloured only with `intent="brand"`;
|
|
80
|
+
`neutral`, the default, is plain text colour. */
|
|
81
|
+
.btn[data-inline="true"] {
|
|
82
|
+
--button-fg: var(--intent-fg);
|
|
166
83
|
}
|
|
167
84
|
}
|
|
@@ -49,7 +49,7 @@ export const cardVariants = cva(
|
|
|
49
49
|
sm: "rounded-lg p-3",
|
|
50
50
|
md: "rounded-xl p-4",
|
|
51
51
|
lg: "rounded-xl p-6",
|
|
52
|
-
xl: "rounded-
|
|
52
|
+
xl: "rounded-xl p-8",
|
|
53
53
|
},
|
|
54
54
|
interactive: {
|
|
55
55
|
true: "hover:border-(color:--card-border-hovered) focus-visible:outline-(color:--card-ring) cursor-pointer transition-colors focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
@@ -26,10 +26,11 @@ export type CheckboxGroupProps = Omit<React.HTMLAttributes<HTMLDivElement>, "onC
|
|
|
26
26
|
onValueChange?: (value: string[]) => void;
|
|
27
27
|
/** Layout orientation */
|
|
28
28
|
orientation?: "vertical" | "horizontal";
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
/**
|
|
30
|
+
* How the options render: `control` (the default) is one `Checkbox` per option;
|
|
31
|
+
* `segmented` and `separate` are a row or column of buttons, joined or apart.
|
|
32
|
+
*/
|
|
33
|
+
appearance?: "control" | "segmented" | "separate";
|
|
33
34
|
/** Input name for form submission */
|
|
34
35
|
name?: string;
|
|
35
36
|
/** Disable all options */
|
|
@@ -43,17 +44,19 @@ export type CheckboxGroupProps = Omit<React.HTMLAttributes<HTMLDivElement>, "onC
|
|
|
43
44
|
* selection, via the `value`/`defaultValue`/`onValueChange` triad
|
|
44
45
|
* (`useControllableState`).
|
|
45
46
|
*
|
|
46
|
-
* `
|
|
47
|
+
* `appearance="control"` (the default) renders one `Checkbox` per option, each
|
|
47
48
|
* with its own visible label, laid out in a column or row per
|
|
48
|
-
* `orientation`. `
|
|
49
|
-
* re-skinned `Checkbox` — a row/column of plain
|
|
50
|
-
* `role="checkbox"` and no separate label; contiguous buttons
|
|
51
|
-
* share borders (`
|
|
52
|
-
*
|
|
49
|
+
* `orientation`. `appearance="segmented"` or `"separate"` is a separate
|
|
50
|
+
* rendering path — not a re-skinned `Checkbox` — a row/column of plain
|
|
51
|
+
* buttons with `role="checkbox"` and no separate label; contiguous buttons
|
|
52
|
+
* touch and share borders (`segmented`) or sit apart with their own rounded
|
|
53
|
+
* corners (`separate`). One prop rather than a
|
|
54
|
+
* `variant` + `appearance` pair, so "appearance without buttons" — which
|
|
55
|
+
* used to mean nothing — cannot be written.
|
|
53
56
|
*
|
|
54
|
-
* Form submission only happens when `name` is set.
|
|
57
|
+
* Form submission only happens when `name` is set. With `control`
|
|
55
58
|
* that means each underlying `Checkbox` gets `name`, so one hidden native
|
|
56
|
-
* checkbox input is rendered per option;
|
|
59
|
+
* checkbox input is rendered per option; with the button appearances the group
|
|
57
60
|
* itself renders one hidden `<input type="checkbox">` per *currently
|
|
58
61
|
* selected* value.
|
|
59
62
|
*
|
|
@@ -68,8 +71,7 @@ const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
|
|
|
68
71
|
options,
|
|
69
72
|
onValueChange,
|
|
70
73
|
orientation = "vertical",
|
|
71
|
-
|
|
72
|
-
appearance = "segmented",
|
|
74
|
+
appearance = "control",
|
|
73
75
|
name,
|
|
74
76
|
disabled,
|
|
75
77
|
onBlur,
|
|
@@ -111,7 +113,7 @@ const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
|
|
|
111
113
|
}
|
|
112
114
|
};
|
|
113
115
|
|
|
114
|
-
if (
|
|
116
|
+
if (appearance === "segmented" || appearance === "separate") {
|
|
115
117
|
const isVertical = orientation === "vertical";
|
|
116
118
|
const isSegmented = appearance === "segmented";
|
|
117
119
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { HTMLAttributes, ReactNode } from "react";
|
|
4
|
-
import { Button } from "../../Button";
|
|
4
|
+
import { Button, type ButtonVariant } from "../../Button";
|
|
5
5
|
|
|
6
6
|
export type Action = {
|
|
7
7
|
id: string;
|
|
@@ -9,7 +9,7 @@ export type Action = {
|
|
|
9
9
|
icon?: ReactNode;
|
|
10
10
|
onClick: () => void;
|
|
11
11
|
disabled?: boolean;
|
|
12
|
-
variant?:
|
|
12
|
+
variant?: ButtonVariant;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export type ToolbarProps = HTMLAttributes<HTMLDivElement> & {
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
4
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react";
|
|
7
|
+
import { forwardRef, type HTMLAttributes, type KeyboardEvent, type ReactElement, type ReactNode, type Ref, useMemo } from "react";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Geometry only. Every colour reads a `--data-table-*` component token whose
|
|
11
|
+
* value is decided by src/components/DataTable/data-table.recipe.css.
|
|
12
|
+
*
|
|
13
|
+
* `size` is cell density: `sm` for back-office lists scanned dozens of rows at
|
|
14
|
+
* a time, `md` for tables that are the page's main content.
|
|
15
|
+
*/
|
|
16
|
+
const cellVariants = cva("px-3 align-middle", {
|
|
17
|
+
variants: {
|
|
18
|
+
size: {
|
|
19
|
+
sm: "h-9 py-1.5 text-xs",
|
|
20
|
+
md: "h-11 py-2 text-sm",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: {
|
|
24
|
+
size: "md",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type DataTableSize = NonNullable<VariantProps<typeof cellVariants>["size"]>;
|
|
29
|
+
|
|
30
|
+
export type DataTableAlign = "start" | "center" | "end";
|
|
31
|
+
|
|
32
|
+
export type DataTableColumn<Row> = {
|
|
33
|
+
/** Stable key — also what `sort.columnId` refers to. */
|
|
34
|
+
id: string;
|
|
35
|
+
/** Header content. */
|
|
36
|
+
header: ReactNode;
|
|
37
|
+
/** Cell content for one row. */
|
|
38
|
+
cell: (row: Row) => ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Makes the column sortable. The returned value is what rows are compared
|
|
41
|
+
* by — return cents, not a formatted price, and an ISO date, not "Tue 3rd".
|
|
42
|
+
*/
|
|
43
|
+
sortValue?: (row: Row) => string | number;
|
|
44
|
+
/** Text alignment for header and cells. Numbers read best at `end`. */
|
|
45
|
+
align?: DataTableAlign;
|
|
46
|
+
/** A CSS width for the column, e.g. `"8rem"` or `"20%"`. */
|
|
47
|
+
width?: string;
|
|
48
|
+
/** Extra classes for this column's `<td>`s. */
|
|
49
|
+
className?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type DataTableSortDirection = "asc" | "desc";
|
|
53
|
+
|
|
54
|
+
export type DataTableSort = {
|
|
55
|
+
columnId: string;
|
|
56
|
+
direction: DataTableSortDirection;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type DataTableProps<Row> = Omit<HTMLAttributes<HTMLDivElement>, "children"> &
|
|
60
|
+
VariantProps<typeof cellVariants> & {
|
|
61
|
+
columns: readonly DataTableColumn<Row>[];
|
|
62
|
+
rows: readonly Row[];
|
|
63
|
+
/** Stable id per row, used as the React key. */
|
|
64
|
+
getRowId: (row: Row) => string;
|
|
65
|
+
/** Controlled sort. `null` is unsorted — rows in the order given. */
|
|
66
|
+
sort?: DataTableSort | null;
|
|
67
|
+
/** Initial sort in uncontrolled mode. */
|
|
68
|
+
defaultSort?: DataTableSort | null;
|
|
69
|
+
/** Called when a header is clicked: ascending, then descending, then unsorted. */
|
|
70
|
+
onSortChange?: (sort: DataTableSort | null) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Report sort changes without reordering `rows`, for data sorted where it
|
|
73
|
+
* is fetched. The headers still show the sort state.
|
|
74
|
+
*/
|
|
75
|
+
manualSorting?: boolean;
|
|
76
|
+
/** Makes every row activatable by pointer, `Enter` or `Space`. */
|
|
77
|
+
onRowClick?: (row: Row) => void;
|
|
78
|
+
/** Shown in place of the body when `rows` is empty. */
|
|
79
|
+
empty?: ReactNode;
|
|
80
|
+
/** Names the table for assistive tech. Visually hidden unless `showCaption`. */
|
|
81
|
+
caption?: ReactNode;
|
|
82
|
+
showCaption?: boolean;
|
|
83
|
+
/** Keep the header row visible while the wrapper scrolls vertically. Give the wrapper a height via `className`. */
|
|
84
|
+
stickyHeader?: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const alignClass: Record<DataTableAlign, string> = {
|
|
88
|
+
start: "text-start",
|
|
89
|
+
center: "text-center",
|
|
90
|
+
end: "text-end",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const ariaSort = { asc: "ascending", desc: "descending" } as const;
|
|
94
|
+
|
|
95
|
+
function nextSort(current: DataTableSort | null, columnId: string): DataTableSort | null {
|
|
96
|
+
if (current?.columnId !== columnId) return { columnId, direction: "asc" };
|
|
97
|
+
return current.direction === "asc" ? { columnId, direction: "desc" } : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function compare(a: string | number, b: string | number): number {
|
|
101
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
102
|
+
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `forwardRef` erases the generic, so the implementation takes the ref as an
|
|
107
|
+
* argument and the export below restores `<Row>` — the same arrangement
|
|
108
|
+
* Select.tsx uses for `<T>`.
|
|
109
|
+
*/
|
|
110
|
+
const DataTableRoot = forwardRef(function DataTableRoot<Row>(
|
|
111
|
+
{
|
|
112
|
+
className,
|
|
113
|
+
columns,
|
|
114
|
+
rows,
|
|
115
|
+
getRowId,
|
|
116
|
+
size = "md",
|
|
117
|
+
sort: controlledSort,
|
|
118
|
+
defaultSort = null,
|
|
119
|
+
onSortChange,
|
|
120
|
+
manualSorting = false,
|
|
121
|
+
onRowClick,
|
|
122
|
+
empty,
|
|
123
|
+
caption,
|
|
124
|
+
showCaption = false,
|
|
125
|
+
stickyHeader = false,
|
|
126
|
+
...props
|
|
127
|
+
}: DataTableProps<Row>,
|
|
128
|
+
ref: Ref<HTMLDivElement>
|
|
129
|
+
) {
|
|
130
|
+
const [sort, setSort] = useControllableState<DataTableSort | null>({
|
|
131
|
+
value: controlledSort,
|
|
132
|
+
defaultValue: defaultSort,
|
|
133
|
+
onChange: onSortChange,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const sortedRows = useMemo(() => {
|
|
137
|
+
if (manualSorting || sort === null) return rows;
|
|
138
|
+
const sortValue = columns.find((column) => column.id === sort.columnId)?.sortValue;
|
|
139
|
+
if (!sortValue) return rows;
|
|
140
|
+
const factor = sort.direction === "asc" ? 1 : -1;
|
|
141
|
+
// Array.prototype.sort is stable, so equal keys keep the caller's order.
|
|
142
|
+
return [...rows].sort((a, b) => factor * compare(sortValue(a), sortValue(b)));
|
|
143
|
+
}, [rows, columns, sort, manualSorting]);
|
|
144
|
+
|
|
145
|
+
const interactive = onRowClick !== undefined;
|
|
146
|
+
|
|
147
|
+
const handleRowKeyDown = (event: KeyboardEvent<HTMLTableRowElement>, row: Row) => {
|
|
148
|
+
if (event.target !== event.currentTarget) return; // a button inside the row keeps its own keys
|
|
149
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
onRowClick?.(row);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div
|
|
157
|
+
ref={ref}
|
|
158
|
+
className={cn(
|
|
159
|
+
"data-table border-(color:--data-table-border) w-full overflow-auto rounded-lg border bg-(--data-table-bg) text-(--data-table-fg)",
|
|
160
|
+
className
|
|
161
|
+
)}
|
|
162
|
+
{...props}
|
|
163
|
+
>
|
|
164
|
+
<table className="w-full border-collapse">
|
|
165
|
+
{caption !== undefined && <caption className={cn(showCaption ? "px-3 py-2 text-start font-medium text-sm" : "sr-only")}>{caption}</caption>}
|
|
166
|
+
<thead className={cn("bg-(--data-table-header-bg)", stickyHeader && "sticky top-0 z-10")}>
|
|
167
|
+
<tr className="border-(color:--data-table-border) border-b">
|
|
168
|
+
{columns.map((column) => {
|
|
169
|
+
const align = column.align ?? "start";
|
|
170
|
+
const sorted = sort?.columnId === column.id ? sort.direction : undefined;
|
|
171
|
+
const Icon = sorted === "asc" ? ArrowUp : sorted === "desc" ? ArrowDown : ChevronsUpDown;
|
|
172
|
+
return (
|
|
173
|
+
<th
|
|
174
|
+
key={column.id}
|
|
175
|
+
scope="col"
|
|
176
|
+
aria-sort={column.sortValue ? (sorted ? ariaSort[sorted] : "none") : undefined}
|
|
177
|
+
style={column.width ? { width: column.width } : undefined}
|
|
178
|
+
className={cn(cellVariants({ size }), alignClass[align], "whitespace-nowrap font-medium text-(--data-table-header-fg)")}
|
|
179
|
+
>
|
|
180
|
+
{column.sortValue ? (
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
onClick={() => setSort(nextSort(sort, column.id))}
|
|
184
|
+
className={cn(
|
|
185
|
+
"focus-visible:ring-(color:--data-table-ring) -mx-1 inline-flex cursor-pointer items-center gap-1 rounded-sm px-1 hover:text-(--data-table-fg) focus-visible:outline-hidden focus-visible:ring-2",
|
|
186
|
+
align === "end" && "flex-row-reverse"
|
|
187
|
+
)}
|
|
188
|
+
>
|
|
189
|
+
{column.header}
|
|
190
|
+
<Icon aria-hidden="true" className={cn("size-3.5 shrink-0", sorted === undefined && "opacity-50")} />
|
|
191
|
+
</button>
|
|
192
|
+
) : (
|
|
193
|
+
column.header
|
|
194
|
+
)}
|
|
195
|
+
</th>
|
|
196
|
+
);
|
|
197
|
+
})}
|
|
198
|
+
</tr>
|
|
199
|
+
</thead>
|
|
200
|
+
<tbody>
|
|
201
|
+
{sortedRows.length === 0 ? (
|
|
202
|
+
<tr>
|
|
203
|
+
<td colSpan={columns.length} className="px-3 py-10 text-center text-(--data-table-empty-fg) text-sm">
|
|
204
|
+
{empty ?? "Nothing to show"}
|
|
205
|
+
</td>
|
|
206
|
+
</tr>
|
|
207
|
+
) : (
|
|
208
|
+
sortedRows.map((row) => (
|
|
209
|
+
<tr
|
|
210
|
+
key={getRowId(row)}
|
|
211
|
+
data-interactive={interactive || undefined}
|
|
212
|
+
onClick={interactive ? () => onRowClick(row) : undefined}
|
|
213
|
+
onKeyDown={interactive ? (event) => handleRowKeyDown(event, row) : undefined}
|
|
214
|
+
// A row is not a native control, but a clickable table row is the
|
|
215
|
+
// established back-office pattern. It stays a `row` so the table
|
|
216
|
+
// semantics survive, and gains a tab stop and Enter/Space like
|
|
217
|
+
// Card does when it is clickable.
|
|
218
|
+
tabIndex={interactive ? 0 : undefined}
|
|
219
|
+
className={cn(
|
|
220
|
+
"border-(color:--data-table-border) border-b last:border-b-0",
|
|
221
|
+
interactive &&
|
|
222
|
+
"focus-visible:outline-(color:--data-table-ring) cursor-pointer hover:bg-(--data-table-row-bg-hovered) focus-visible:outline-2 focus-visible:-outline-offset-2"
|
|
223
|
+
)}
|
|
224
|
+
>
|
|
225
|
+
{columns.map((column) => (
|
|
226
|
+
<td key={column.id} className={cn(cellVariants({ size }), alignClass[column.align ?? "start"], column.className)}>
|
|
227
|
+
{column.cell(row)}
|
|
228
|
+
</td>
|
|
229
|
+
))}
|
|
230
|
+
</tr>
|
|
231
|
+
))
|
|
232
|
+
)}
|
|
233
|
+
</tbody>
|
|
234
|
+
</table>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A data table for back-office lists — orders, products, customers.
|
|
241
|
+
*
|
|
242
|
+
* Props-based, per docs/component-api-design.md: rows are uniform, so they
|
|
243
|
+
* arrive as a `rows` array and a `columns` config, and each column's `cell`
|
|
244
|
+
* renders one row's content. Anything interactive in a cell — a status
|
|
245
|
+
* Badge, an action Button — is just what `cell` returns.
|
|
246
|
+
*
|
|
247
|
+
* A column with `sortValue` gets a header button cycling ascending →
|
|
248
|
+
* descending → unsorted, and `aria-sort` on its `<th>`. Sorting is
|
|
249
|
+
* client-side and stable; pass `manualSorting` to only report the change when
|
|
250
|
+
* the data is sorted where it is fetched. Sort state is controlled or
|
|
251
|
+
* uncontrolled through `sort`/`defaultSort`/`onSortChange`.
|
|
252
|
+
*
|
|
253
|
+
* `onRowClick` makes every row a tab stop activated by pointer, `Enter` or
|
|
254
|
+
* `Space` — keys pressed on a control inside the row are left to that control.
|
|
255
|
+
*
|
|
256
|
+
* The wrapper scrolls, so a wide table on a phone scrolls sideways inside its
|
|
257
|
+
* border rather than widening the page.
|
|
258
|
+
*/
|
|
259
|
+
export const DataTable = DataTableRoot as <Row>(props: DataTableProps<Row> & { ref?: Ref<HTMLDivElement> }) => ReactElement;
|
|
260
|
+
|
|
261
|
+
export { cellVariants as dataTableCellVariants };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataTable recipe — the component-token layer for DataTable.
|
|
3
|
+
*
|
|
4
|
+
* No variant/intent axis: status colour inside a table belongs to what the
|
|
5
|
+
* cells render (a Badge), not to the table. The table itself is the Elevated
|
|
6
|
+
* tier's container — the same surface and rule colours as card.css — through
|
|
7
|
+
* its own tokens, so a consumer can re-point one without the other.
|
|
8
|
+
*
|
|
9
|
+
* The grammar is `--data-table-[{part}-]{context}[-{state}]`. Parts are the
|
|
10
|
+
* `header` row, a `row` (only its hover, which applies only when rows are
|
|
11
|
+
* clickable), and the `empty` state's text. `--data-table-border` draws the
|
|
12
|
+
* outer frame and the rules between rows alike.
|
|
13
|
+
*
|
|
14
|
+
* `@layer components` is load-bearing, not tidiness — see button.css.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
@layer components {
|
|
18
|
+
.data-table {
|
|
19
|
+
--data-table-bg: var(--bg-normal);
|
|
20
|
+
--data-table-fg: var(--fg-normal);
|
|
21
|
+
--data-table-border: var(--border-normal);
|
|
22
|
+
--data-table-ring: var(--focus-ring);
|
|
23
|
+
--data-table-header-bg: var(--bg-subtle);
|
|
24
|
+
--data-table-header-fg: var(--fg-subtle);
|
|
25
|
+
--data-table-row-bg-hovered: var(--bg-normal-hovered);
|
|
26
|
+
--data-table-empty-fg: var(--fg-subtle);
|
|
27
|
+
}
|
|
28
|
+
}
|