@charcuterie/ui 2.18.0 → 3.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 +10 -0
- package/dist/Badge/Badge.d.ts +33 -3
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +20 -2
- package/dist/Badge/Badge.js.map +1 -1
- package/dist/Board/Board.d.ts +119 -0
- package/dist/Board/Board.d.ts.map +1 -0
- package/dist/Board/Board.js +165 -0
- package/dist/Board/Board.js.map +1 -0
- package/dist/Board/BoardCard.d.ts +120 -0
- package/dist/Board/BoardCard.d.ts.map +1 -0
- package/dist/Board/BoardCard.js +77 -0
- package/dist/Board/BoardCard.js.map +1 -0
- package/dist/Board/BoardLaneList.d.ts +103 -0
- package/dist/Board/BoardLaneList.d.ts.map +1 -0
- package/dist/Board/BoardLaneList.js +69 -0
- package/dist/Board/BoardLaneList.js.map +1 -0
- package/dist/Board/boardMove.d.ts +116 -0
- package/dist/Board/boardMove.d.ts.map +1 -0
- package/dist/Board/boardMove.js +116 -0
- package/dist/Board/boardMove.js.map +1 -0
- package/dist/Board/useBoardDrag.d.ts +44 -0
- package/dist/Board/useBoardDrag.d.ts.map +1 -0
- package/dist/Board/useBoardDrag.js +237 -0
- package/dist/Board/useBoardDrag.js.map +1 -0
- package/dist/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/Checkbox/Checkbox.js +15 -4
- package/dist/Checkbox/Checkbox.js.map +1 -1
- package/dist/DataTable/DataTable.d.ts +159 -0
- package/dist/DataTable/DataTable.d.ts.map +1 -0
- package/dist/DataTable/DataTable.js +197 -0
- package/dist/DataTable/DataTable.js.map +1 -0
- package/dist/DatePicker/DateGrid.d.ts +79 -0
- package/dist/DatePicker/DateGrid.d.ts.map +1 -0
- package/dist/DatePicker/DateGrid.js +220 -0
- package/dist/DatePicker/DateGrid.js.map +1 -0
- package/dist/DatePicker/DatePicker.d.ts +138 -0
- package/dist/DatePicker/DatePicker.d.ts.map +1 -0
- package/dist/DatePicker/DatePicker.js +384 -0
- package/dist/DatePicker/DatePicker.js.map +1 -0
- package/dist/DatePicker/datePresets.d.ts +29 -0
- package/dist/DatePicker/datePresets.d.ts.map +1 -0
- package/dist/DatePicker/datePresets.js +28 -0
- package/dist/DatePicker/datePresets.js.map +1 -0
- package/dist/DatePicker/parseDateInput.d.ts +106 -0
- package/dist/DatePicker/parseDateInput.d.ts.map +1 -0
- package/dist/DatePicker/parseDateInput.js +419 -0
- package/dist/DatePicker/parseDateInput.js.map +1 -0
- package/dist/DatePicker/plainDate.d.ts +167 -0
- package/dist/DatePicker/plainDate.d.ts.map +1 -0
- package/dist/DatePicker/plainDate.js +272 -0
- package/dist/DatePicker/plainDate.js.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts +147 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
- package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.js +379 -0
- package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.js +303 -0
- package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
- package/dist/Select/Select.d.ts +36 -1
- package/dist/Select/Select.d.ts.map +1 -1
- package/dist/Select/Select.js +38 -2
- package/dist/Select/Select.js.map +1 -1
- package/dist/categoricalStyles.d.ts +53 -0
- package/dist/categoricalStyles.d.ts.map +1 -0
- package/dist/categoricalStyles.js +195 -0
- package/dist/categoricalStyles.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Badge/Badge.mdx +46 -0
- package/src/Badge/Badge.stories.tsx +98 -4
- package/src/Badge/Badge.test.tsx +157 -2
- package/src/Badge/Badge.tsx +49 -3
- package/src/Board/Board.mdx +199 -0
- package/src/Board/Board.stories.tsx +707 -0
- package/src/Board/Board.test.tsx +310 -0
- package/src/Board/Board.tsx +333 -0
- package/src/Board/BoardCard.tsx +290 -0
- package/src/Board/BoardLaneList.tsx +275 -0
- package/src/Board/boardMove.test.ts +213 -0
- package/src/Board/boardMove.ts +225 -0
- package/src/Board/useBoardDrag.ts +413 -0
- package/src/Checkbox/Checkbox.tsx +34 -3
- package/src/DataTable/DataTable.mdx +123 -0
- package/src/DataTable/DataTable.stories.tsx +411 -0
- package/src/DataTable/DataTable.test.tsx +386 -0
- package/src/DataTable/DataTable.tsx +497 -0
- package/src/DatePicker/DateGrid.tsx +467 -0
- package/src/DatePicker/DatePicker.mdx +170 -0
- package/src/DatePicker/DatePicker.stories.tsx +268 -0
- package/src/DatePicker/DatePicker.test.tsx +455 -0
- package/src/DatePicker/DatePicker.tsx +852 -0
- package/src/DatePicker/datePresets.ts +34 -0
- package/src/DatePicker/parseDateInput.test.ts +187 -0
- package/src/DatePicker/parseDateInput.ts +629 -0
- package/src/DatePicker/plainDate.test.ts +375 -0
- package/src/DatePicker/plainDate.ts +465 -0
- package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
- package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
- package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
- package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
- package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
- package/src/MarkdownEditor/markdownCommands.ts +566 -0
- package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
- package/src/MarkdownEditor/markdownSpans.ts +458 -0
- package/src/Select/Select.mdx +44 -0
- package/src/Select/Select.stories.tsx +55 -0
- package/src/Select/Select.test.tsx +89 -1
- package/src/Select/Select.tsx +58 -2
- package/src/argTypes.storyHelpers.ts +39 -0
- package/src/categoricalStyles.ts +244 -0
- package/src/index.ts +89 -0
- package/src/sourceRules.test.ts +52 -1
package/src/Select/Select.mdx
CHANGED
|
@@ -17,6 +17,50 @@ see below.
|
|
|
17
17
|
|
|
18
18
|
<Controls />
|
|
19
19
|
|
|
20
|
+
## Sizing: `className` is the wrapper, `controlClassName` is the select
|
|
21
|
+
|
|
22
|
+
The chevron has to live somewhere, so this renders a wrapper around the native
|
|
23
|
+
`<select>` and positions the chevron against **that wrapper**. `className` therefore
|
|
24
|
+
lands on the wrapper and the `<select>` stays `w-full` inside it:
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
<Select className="w-44" controlClassName="font-mono" label="Operation" options={OPS} />
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
means "this control is 176px" — chevron included — which is what a caller writing a
|
|
31
|
+
width already assumes.
|
|
32
|
+
|
|
33
|
+
It did not used to. `className` reached only the inner element while the wrapper stayed
|
|
34
|
+
`w-full`, so the control shrank and the chevron stayed pinned to the row's right edge:
|
|
35
|
+
|
|
36
|
+
| | left | right | width |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| wrapper | 55 | 1128.7 | 1073.6 |
|
|
39
|
+
| select | 55 | 231 | 176 |
|
|
40
|
+
| chevron | 1100.7 | 1116.7 | — |
|
|
41
|
+
|
|
42
|
+
**869.6px** between the control and its own chevron, measured in mux-magic's DSL rules
|
|
43
|
+
builder, with the control's own text clipping at the same time (`setScriptInfo` →
|
|
44
|
+
`setScriptInfc`). The width never worked; `ui@2.16.0`'s `tailwind-merge` made half of it
|
|
45
|
+
work, which is what made the other half visible
|
|
46
|
+
([#112](https://github.com/Sawtaytoes/charcuterie/issues/112)).
|
|
47
|
+
|
|
48
|
+
`Select.test.tsx` asserts this as a **measurement** rather than a class-name check —
|
|
49
|
+
every class-name assertion passed while the chevron was 869.6px away, because `w-44`
|
|
50
|
+
really was present, on the element nobody was looking at.
|
|
51
|
+
|
|
52
|
+
<Canvas of={SelectStories.Sized} />
|
|
53
|
+
|
|
54
|
+
### What goes where
|
|
55
|
+
|
|
56
|
+
| | Goes to | For |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `className` | the wrapper | width, margin (`ms-auto`), display, position — the outer box |
|
|
59
|
+
| `controlClassName` | the `<select>` | `font-mono` on the option text, a `text-*`, a `bg-*` |
|
|
60
|
+
|
|
61
|
+
A width in `controlClassName` is the bug again by hand: it desynchronises the inner
|
|
62
|
+
element from the wrapper the chevron is measured against.
|
|
63
|
+
|
|
20
64
|
## There is no state kind here, and that is the finding
|
|
21
65
|
|
|
22
66
|
Every other interactive component in this package owns its state through
|
|
@@ -132,3 +132,58 @@ export const AllStates: Story = {
|
|
|
132
132
|
</StoryGrid>
|
|
133
133
|
),
|
|
134
134
|
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* `className` sizes the **control**, chevron included, because it
|
|
138
|
+
* lands on the wrapper the chevron is positioned against.
|
|
139
|
+
*
|
|
140
|
+
* The first cell is the shape that broke: before this, `w-44`
|
|
141
|
+
* reached only the inner `<select>` while the wrapper stayed
|
|
142
|
+
* `w-full`, so the chevron sat at the *row's* right edge — 869.6px
|
|
143
|
+
* away in mux-magic's rules builder. `controlClassName` is the
|
|
144
|
+
* inner element, for the things that are not the outer box, and
|
|
145
|
+
* `font-mono` is the one every caller reaches for.
|
|
146
|
+
*
|
|
147
|
+
* The width is still the caller's to get right: `w-44` is narrower
|
|
148
|
+
* than the longest option here, and a `<select>` clips rather than
|
|
149
|
+
* wraps. That is why mux-magic measured its widths rather than
|
|
150
|
+
* guessing them.
|
|
151
|
+
*/
|
|
152
|
+
export const Sized: Story = {
|
|
153
|
+
args: { label: "Rip profile", options: PROFILES },
|
|
154
|
+
render: () => (
|
|
155
|
+
<StoryGrid columns={1}>
|
|
156
|
+
<StoryCell label="w-44, in a wide row">
|
|
157
|
+
<Select
|
|
158
|
+
className="w-44"
|
|
159
|
+
label="Profile sized"
|
|
160
|
+
options={PROFILES}
|
|
161
|
+
/>
|
|
162
|
+
</StoryCell>
|
|
163
|
+
|
|
164
|
+
<StoryCell label="w-64, monospaced option text">
|
|
165
|
+
<Select
|
|
166
|
+
className="w-64"
|
|
167
|
+
controlClassName="font-mono"
|
|
168
|
+
label="Profile monospaced"
|
|
169
|
+
options={PROFILES}
|
|
170
|
+
/>
|
|
171
|
+
</StoryCell>
|
|
172
|
+
|
|
173
|
+
<StoryCell label="ms-auto, pushed to the end">
|
|
174
|
+
<Select
|
|
175
|
+
className="ms-auto w-56"
|
|
176
|
+
label="Profile pushed"
|
|
177
|
+
options={PROFILES}
|
|
178
|
+
/>
|
|
179
|
+
</StoryCell>
|
|
180
|
+
|
|
181
|
+
<StoryCell label="default: fills its parent">
|
|
182
|
+
<Select
|
|
183
|
+
label="Profile full width"
|
|
184
|
+
options={PROFILES}
|
|
185
|
+
/>
|
|
186
|
+
</StoryCell>
|
|
187
|
+
</StoryGrid>
|
|
188
|
+
),
|
|
189
|
+
}
|
|
@@ -7,7 +7,7 @@ import { mountStory } from "../mountStory.testHelpers.ts"
|
|
|
7
7
|
import { expectAgentDrivable } from "../testing/index.ts"
|
|
8
8
|
import * as stories from "./Select.stories.tsx"
|
|
9
9
|
|
|
10
|
-
const { AllStates, AllVariants, Default } =
|
|
10
|
+
const { AllStates, AllVariants, Default, Sized } =
|
|
11
11
|
composeStories(stories)
|
|
12
12
|
|
|
13
13
|
test("a select is named, and therefore findable", async () => {
|
|
@@ -127,3 +127,91 @@ test("inside a Field, the label does the naming", async () => {
|
|
|
127
127
|
|
|
128
128
|
await expect(select).toHaveAttribute("aria-describedby")
|
|
129
129
|
})
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The #112 regression test, and it has to be a *measurement*. Every
|
|
133
|
+
* class-name assertion this could have been passed while the control
|
|
134
|
+
* was 869.6px away from its own chevron: `w-44` was present, on the
|
|
135
|
+
* element nobody was looking at.
|
|
136
|
+
*/
|
|
137
|
+
test("a width on className sizes the control, chevron included", async () => {
|
|
138
|
+
const { canvas } = await mountStory(Sized)
|
|
139
|
+
|
|
140
|
+
const select = expectAgentDrivable(canvas, {
|
|
141
|
+
name: "Profile sized",
|
|
142
|
+
role: "combobox",
|
|
143
|
+
}) as HTMLSelectElement
|
|
144
|
+
|
|
145
|
+
const wrapper = select.parentElement as HTMLElement
|
|
146
|
+
const chevron = wrapper.querySelector("svg") as SVGElement
|
|
147
|
+
|
|
148
|
+
const selectBox = select.getBoundingClientRect()
|
|
149
|
+
const wrapperBox = wrapper.getBoundingClientRect()
|
|
150
|
+
const chevronBox = chevron.getBoundingClientRect()
|
|
151
|
+
|
|
152
|
+
// 11rem. The caller asked for one box, not two of different sizes.
|
|
153
|
+
await expect(wrapperBox.width).toBe(176)
|
|
154
|
+
|
|
155
|
+
await expect(selectBox.width).toBe(wrapperBox.width)
|
|
156
|
+
|
|
157
|
+
// The chevron is `end-3` against the wrapper, so this is the whole
|
|
158
|
+
// bug in one number: it used to read 869.6.
|
|
159
|
+
await expect(
|
|
160
|
+
selectBox.right - chevronBox.right,
|
|
161
|
+
).toBeCloseTo(12, 1)
|
|
162
|
+
|
|
163
|
+
// …and it is inside the control on the other side too, rather than
|
|
164
|
+
// merely near it.
|
|
165
|
+
await expect(chevronBox.left).toBeGreaterThan(
|
|
166
|
+
selectBox.left,
|
|
167
|
+
)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The other half of the split: an inner-element class still reaches
|
|
172
|
+
* the inner element, and does not become the wrapper's problem.
|
|
173
|
+
*/
|
|
174
|
+
test("controlClassName reaches the select, not the wrapper", async () => {
|
|
175
|
+
const { canvas } = await mountStory(Sized)
|
|
176
|
+
|
|
177
|
+
const select = expectAgentDrivable(canvas, {
|
|
178
|
+
name: "Profile monospaced",
|
|
179
|
+
role: "combobox",
|
|
180
|
+
}) as HTMLSelectElement
|
|
181
|
+
|
|
182
|
+
const wrapper = select.parentElement as HTMLElement
|
|
183
|
+
|
|
184
|
+
await expect(select.className).toContain("font-mono")
|
|
185
|
+
|
|
186
|
+
await expect(wrapper.className).not.toContain("font-mono")
|
|
187
|
+
|
|
188
|
+
// …and the wrapper is still the one carrying the width.
|
|
189
|
+
await expect(wrapper.getBoundingClientRect().width).toBe(
|
|
190
|
+
256,
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The default is unchanged, which is the other half of the fix being
|
|
196
|
+
* safe: every existing caller passes no width and expects the
|
|
197
|
+
* control to fill its parent.
|
|
198
|
+
*/
|
|
199
|
+
test("with no className, the control still fills its parent", async () => {
|
|
200
|
+
const { canvas } = await mountStory(Sized)
|
|
201
|
+
|
|
202
|
+
const select = expectAgentDrivable(canvas, {
|
|
203
|
+
name: "Profile full width",
|
|
204
|
+
role: "combobox",
|
|
205
|
+
}) as HTMLSelectElement
|
|
206
|
+
|
|
207
|
+
const wrapper = select.parentElement as HTMLElement
|
|
208
|
+
const parent = wrapper.parentElement as HTMLElement
|
|
209
|
+
|
|
210
|
+
await expect(
|
|
211
|
+
wrapper.getBoundingClientRect().width,
|
|
212
|
+
).toBeCloseTo(parent.getBoundingClientRect().width, 1)
|
|
213
|
+
|
|
214
|
+
await expect(
|
|
215
|
+
select.getBoundingClientRect().width,
|
|
216
|
+
).toBeCloseTo(wrapper.getBoundingClientRect().width, 1)
|
|
217
|
+
})
|
package/src/Select/Select.tsx
CHANGED
|
@@ -42,7 +42,23 @@ export type SelectProps = Omit<
|
|
|
42
42
|
ComponentPropsWithRef<"select">,
|
|
43
43
|
"disabled" | "onChange" | "size" | "value"
|
|
44
44
|
> & {
|
|
45
|
+
/**
|
|
46
|
+
* Goes on the **wrapper**, which is the box a caller means when
|
|
47
|
+
* they size a control. See the component docblock — the chevron
|
|
48
|
+
* is positioned against that wrapper, so a width anywhere else
|
|
49
|
+
* leaves it behind.
|
|
50
|
+
*/
|
|
45
51
|
className?: string
|
|
52
|
+
/**
|
|
53
|
+
* The escape hatch for the inner `<select>` itself, for the
|
|
54
|
+
* things that genuinely are not the outer box: `font-mono` on the
|
|
55
|
+
* option text, a `text-*` override, a `bg-*`.
|
|
56
|
+
*
|
|
57
|
+
* Sizing does **not** belong here. The inner element is `w-full`
|
|
58
|
+
* and a width on it desynchronises it from the wrapper the
|
|
59
|
+
* chevron is pinned to, which is the whole of #112.
|
|
60
|
+
*/
|
|
61
|
+
controlClassName?: string
|
|
46
62
|
isDisabled?: boolean
|
|
47
63
|
/**
|
|
48
64
|
* The accessible name, when this is **not** inside a `Field`. A
|
|
@@ -82,6 +98,25 @@ const getIsGroup = (
|
|
|
82
98
|
* A styled native `<select>`, and the fleet's fourteen hand-rolled
|
|
83
99
|
* ones collapse onto it.
|
|
84
100
|
*
|
|
101
|
+
* ### Sizing: `className` is the wrapper, `controlClassName` is the select
|
|
102
|
+
*
|
|
103
|
+
* This renders a wrapper around the native `<select>` because the
|
|
104
|
+
* chevron has to live somewhere, and the chevron is positioned
|
|
105
|
+
* against that wrapper. `className` therefore lands on the wrapper
|
|
106
|
+
* and the `<select>` stays `w-full` inside it, so
|
|
107
|
+
* `className="w-44"` means "this control is 176px" — chevron
|
|
108
|
+
* included — which is what a caller writing a width already assumes.
|
|
109
|
+
*
|
|
110
|
+
* Before this, `className` went to the inner element only. The
|
|
111
|
+
* wrapper stayed `w-full`, the chevron stayed at *its* edge, and
|
|
112
|
+
* mux-magic's rules builder measured the gap at **869.6px** with the
|
|
113
|
+
* control's own text clipping beside it. The width never worked;
|
|
114
|
+
* `ui@2.16.0`'s `tailwind-merge` only made half of it work and made
|
|
115
|
+
* the other half visible.
|
|
116
|
+
*
|
|
117
|
+
* For the genuinely-inner things — `font-mono` on the option text, a
|
|
118
|
+
* `bg-*` — there is `controlClassName`.
|
|
119
|
+
*
|
|
85
120
|
* ### There is no state kind here, and that is the finding
|
|
86
121
|
*
|
|
87
122
|
* Every other interactive component in this package owns its state
|
|
@@ -116,6 +151,7 @@ const getIsGroup = (
|
|
|
116
151
|
*/
|
|
117
152
|
export const Select = ({
|
|
118
153
|
className,
|
|
154
|
+
controlClassName,
|
|
119
155
|
isDisabled = false,
|
|
120
156
|
label,
|
|
121
157
|
onChange,
|
|
@@ -125,7 +161,24 @@ export const Select = ({
|
|
|
125
161
|
value,
|
|
126
162
|
...selectProps
|
|
127
163
|
}: SelectProps): ReactNode => (
|
|
128
|
-
<div
|
|
164
|
+
<div
|
|
165
|
+
className={toClassName(
|
|
166
|
+
"relative inline-grid w-full items-center",
|
|
167
|
+
// `className` lands HERE, not on the `<select>`, and that is
|
|
168
|
+
// the fix for #112 rather than a stylistic choice. The chevron
|
|
169
|
+
// below is `absolute end-3` against this element, so a width
|
|
170
|
+
// that reaches only the inner control moves the control and
|
|
171
|
+
// leaves the chevron pinned to the old edge — measured at
|
|
172
|
+
// 869.6px adrift in mux-magic's rules builder, with the
|
|
173
|
+
// control's own text clipping at the same time.
|
|
174
|
+
//
|
|
175
|
+
// It also matches every other component in this package:
|
|
176
|
+
// `className` is the outermost box. `ms-auto` — which mux-magic
|
|
177
|
+
// passes and which did nothing on a `w-full` inner select — is
|
|
178
|
+
// the tell that callers already believed this.
|
|
179
|
+
className,
|
|
180
|
+
)}
|
|
181
|
+
>
|
|
129
182
|
<select
|
|
130
183
|
{...selectProps}
|
|
131
184
|
aria-label={label}
|
|
@@ -145,7 +198,10 @@ export const Select = ({
|
|
|
145
198
|
"hover:border-border-strong",
|
|
146
199
|
FOCUS_RING_CLASS,
|
|
147
200
|
DISABLED_CLASS,
|
|
148
|
-
className
|
|
201
|
+
// Inner-only, and deliberately not `className`. `w-full`
|
|
202
|
+
// above keeps this element the same width as the wrapper the
|
|
203
|
+
// chevron is measured from, so the two can never drift.
|
|
204
|
+
controlClassName,
|
|
149
205
|
)}
|
|
150
206
|
defaultValue={
|
|
151
207
|
value ??
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { connectionTransitions } from "@charcuterie/logic"
|
|
2
|
+
import {
|
|
3
|
+
CATEGORICAL_HUES,
|
|
4
|
+
CATEGORICAL_INDEXES,
|
|
5
|
+
} from "@charcuterie/tokens"
|
|
2
6
|
import type { InputType } from "storybook/internal/types"
|
|
3
7
|
|
|
4
8
|
import { CONTROL_SIZE_CLASS } from "./controlStyles.ts"
|
|
@@ -95,6 +99,41 @@ export const PLACEMENT_OPTIONS = [
|
|
|
95
99
|
|
|
96
100
|
export const intentArgType = toChoiceArgType(INTENT_OPTIONS)
|
|
97
101
|
|
|
102
|
+
/**
|
|
103
|
+
* `categorical` is the one enumerable prop in the package whose
|
|
104
|
+
* values are **numbers**, so it cannot go through `toOptions` —
|
|
105
|
+
* `Object.keys` on a numerically-keyed record returns
|
|
106
|
+
* `["1", "2", …]`, and a control handing the component `"3"` where
|
|
107
|
+
* it wants `3` indexes a `Record<CategoricalIndex, …>` with a
|
|
108
|
+
* string and paints nothing. Reading `CATEGORICAL_INDEXES` keeps
|
|
109
|
+
* the type and the control agreeing.
|
|
110
|
+
*
|
|
111
|
+
* `undefined` leads the list because it is the real default: a
|
|
112
|
+
* badge with no `categorical` is an `intent` badge, and a control
|
|
113
|
+
* with no way back to that would make the two props look like a
|
|
114
|
+
* choice the reader cannot undo.
|
|
115
|
+
*/
|
|
116
|
+
export const CATEGORICAL_OPTIONS = [...CATEGORICAL_INDEXES]
|
|
117
|
+
|
|
118
|
+
export const categoricalArgType: InputType = {
|
|
119
|
+
control: { type: "select" },
|
|
120
|
+
options: [undefined, ...CATEGORICAL_OPTIONS],
|
|
121
|
+
// The names are `CATEGORICAL_HUES`', not invented here — a picker
|
|
122
|
+
// that says "3" tells a reader nothing, and `Swatch` already
|
|
123
|
+
// settled that a colour needs a word attached.
|
|
124
|
+
mapping: Object.fromEntries(
|
|
125
|
+
CATEGORICAL_OPTIONS.map((index) => [index, index]),
|
|
126
|
+
),
|
|
127
|
+
table: {
|
|
128
|
+
type: {
|
|
129
|
+
summary: CATEGORICAL_OPTIONS.map(
|
|
130
|
+
(index) =>
|
|
131
|
+
`${index} (${CATEGORICAL_HUES[index].label})`,
|
|
132
|
+
).join(" | "),
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
|
|
98
137
|
export const controlSizeArgType = toChoiceArgType(
|
|
99
138
|
CONTROL_SIZE_OPTIONS,
|
|
100
139
|
)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One categorical index → class map, the twin of
|
|
3
|
+
* `intentStyles.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Its own file rather than ten more entries in that one, because
|
|
6
|
+
* the two families are keyed by different things and mean different
|
|
7
|
+
* things. `intentStyles.ts` opens by explaining that it exists to
|
|
8
|
+
* delete rip-deck's duplicated `TONE_CLASS` — a *status* map, where
|
|
9
|
+
* every key is a claim about state. Nothing here is a claim about
|
|
10
|
+
* anything: an index is a user's pick for a label, and the only
|
|
11
|
+
* thing a component may do with it is paint it. Keeping them apart
|
|
12
|
+
* is also what keeps `IntentName` an exhaustive union that
|
|
13
|
+
* `getAsyncIntent` can switch over.
|
|
14
|
+
*
|
|
15
|
+
* **Every class name is written out in full, and that is not
|
|
16
|
+
* negotiable.** Tailwind v4 scans source text for *complete* class
|
|
17
|
+
* strings, so `` `bg-categorical-${index}-solid` `` generates
|
|
18
|
+
* nothing at all — the badge renders unstyled with no error, no
|
|
19
|
+
* warning, and no failing test unless something checks. Something
|
|
20
|
+
* does: `tailwindCandidates.test.ts` compiles every literal in this
|
|
21
|
+
* package through the real Tailwind and fails on any candidate it
|
|
22
|
+
* cannot generate. Forty lines of near-duplication is the price,
|
|
23
|
+
* and the alternative is the `--color-danger-9` that shipped in
|
|
24
|
+
* Docket: a token name that had never existed, resolving to
|
|
25
|
+
* nothing, painting transparent, and passing every assertion about
|
|
26
|
+
* whether the element was rendered.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { CategoricalIndex } from "@charcuterie/tokens"
|
|
30
|
+
|
|
31
|
+
import type { IntentAppearance } from "./intentStyles.ts"
|
|
32
|
+
|
|
33
|
+
export const CATEGORICAL_APPEARANCE_CLASS: Record<
|
|
34
|
+
CategoricalIndex,
|
|
35
|
+
Record<IntentAppearance, string>
|
|
36
|
+
> = {
|
|
37
|
+
1: {
|
|
38
|
+
ghost:
|
|
39
|
+
"border-transparent bg-transparent text-categorical-1-content",
|
|
40
|
+
outline:
|
|
41
|
+
"border-categorical-1-border bg-transparent text-categorical-1-content",
|
|
42
|
+
soft: "border-categorical-1-border bg-categorical-1-surface text-categorical-1-content",
|
|
43
|
+
solid:
|
|
44
|
+
"border-transparent bg-categorical-1-solid text-categorical-1-on-solid",
|
|
45
|
+
},
|
|
46
|
+
2: {
|
|
47
|
+
ghost:
|
|
48
|
+
"border-transparent bg-transparent text-categorical-2-content",
|
|
49
|
+
outline:
|
|
50
|
+
"border-categorical-2-border bg-transparent text-categorical-2-content",
|
|
51
|
+
soft: "border-categorical-2-border bg-categorical-2-surface text-categorical-2-content",
|
|
52
|
+
solid:
|
|
53
|
+
"border-transparent bg-categorical-2-solid text-categorical-2-on-solid",
|
|
54
|
+
},
|
|
55
|
+
3: {
|
|
56
|
+
ghost:
|
|
57
|
+
"border-transparent bg-transparent text-categorical-3-content",
|
|
58
|
+
outline:
|
|
59
|
+
"border-categorical-3-border bg-transparent text-categorical-3-content",
|
|
60
|
+
soft: "border-categorical-3-border bg-categorical-3-surface text-categorical-3-content",
|
|
61
|
+
solid:
|
|
62
|
+
"border-transparent bg-categorical-3-solid text-categorical-3-on-solid",
|
|
63
|
+
},
|
|
64
|
+
4: {
|
|
65
|
+
ghost:
|
|
66
|
+
"border-transparent bg-transparent text-categorical-4-content",
|
|
67
|
+
outline:
|
|
68
|
+
"border-categorical-4-border bg-transparent text-categorical-4-content",
|
|
69
|
+
soft: "border-categorical-4-border bg-categorical-4-surface text-categorical-4-content",
|
|
70
|
+
solid:
|
|
71
|
+
"border-transparent bg-categorical-4-solid text-categorical-4-on-solid",
|
|
72
|
+
},
|
|
73
|
+
5: {
|
|
74
|
+
ghost:
|
|
75
|
+
"border-transparent bg-transparent text-categorical-5-content",
|
|
76
|
+
outline:
|
|
77
|
+
"border-categorical-5-border bg-transparent text-categorical-5-content",
|
|
78
|
+
soft: "border-categorical-5-border bg-categorical-5-surface text-categorical-5-content",
|
|
79
|
+
solid:
|
|
80
|
+
"border-transparent bg-categorical-5-solid text-categorical-5-on-solid",
|
|
81
|
+
},
|
|
82
|
+
6: {
|
|
83
|
+
ghost:
|
|
84
|
+
"border-transparent bg-transparent text-categorical-6-content",
|
|
85
|
+
outline:
|
|
86
|
+
"border-categorical-6-border bg-transparent text-categorical-6-content",
|
|
87
|
+
soft: "border-categorical-6-border bg-categorical-6-surface text-categorical-6-content",
|
|
88
|
+
solid:
|
|
89
|
+
"border-transparent bg-categorical-6-solid text-categorical-6-on-solid",
|
|
90
|
+
},
|
|
91
|
+
7: {
|
|
92
|
+
ghost:
|
|
93
|
+
"border-transparent bg-transparent text-categorical-7-content",
|
|
94
|
+
outline:
|
|
95
|
+
"border-categorical-7-border bg-transparent text-categorical-7-content",
|
|
96
|
+
soft: "border-categorical-7-border bg-categorical-7-surface text-categorical-7-content",
|
|
97
|
+
solid:
|
|
98
|
+
"border-transparent bg-categorical-7-solid text-categorical-7-on-solid",
|
|
99
|
+
},
|
|
100
|
+
8: {
|
|
101
|
+
ghost:
|
|
102
|
+
"border-transparent bg-transparent text-categorical-8-content",
|
|
103
|
+
outline:
|
|
104
|
+
"border-categorical-8-border bg-transparent text-categorical-8-content",
|
|
105
|
+
soft: "border-categorical-8-border bg-categorical-8-surface text-categorical-8-content",
|
|
106
|
+
solid:
|
|
107
|
+
"border-transparent bg-categorical-8-solid text-categorical-8-on-solid",
|
|
108
|
+
},
|
|
109
|
+
9: {
|
|
110
|
+
ghost:
|
|
111
|
+
"border-transparent bg-transparent text-categorical-9-content",
|
|
112
|
+
outline:
|
|
113
|
+
"border-categorical-9-border bg-transparent text-categorical-9-content",
|
|
114
|
+
soft: "border-categorical-9-border bg-categorical-9-surface text-categorical-9-content",
|
|
115
|
+
solid:
|
|
116
|
+
"border-transparent bg-categorical-9-solid text-categorical-9-on-solid",
|
|
117
|
+
},
|
|
118
|
+
10: {
|
|
119
|
+
ghost:
|
|
120
|
+
"border-transparent bg-transparent text-categorical-10-content",
|
|
121
|
+
outline:
|
|
122
|
+
"border-categorical-10-border bg-transparent text-categorical-10-content",
|
|
123
|
+
soft: "border-categorical-10-border bg-categorical-10-surface text-categorical-10-content",
|
|
124
|
+
solid:
|
|
125
|
+
"border-transparent bg-categorical-10-solid text-categorical-10-on-solid",
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Hover, split out for exactly the reason `INTENT_HOVER_CLASS` is:
|
|
131
|
+
* most things wearing a colour are not interactive. A label pill in
|
|
132
|
+
* a task list that lightens under the cursor is telling the user it
|
|
133
|
+
* can be clicked, and a `Badge` cannot be. So an interactive
|
|
134
|
+
* component opts in, and a static one cannot accidentally opt in.
|
|
135
|
+
*
|
|
136
|
+
* `solid` hovers toward `solidHover`, which for this family is
|
|
137
|
+
* **brighter** rather than deeper — the fill carries a near-black
|
|
138
|
+
* label, so it moves away from it in the opposite direction to an
|
|
139
|
+
* intent's white-labelled fill.
|
|
140
|
+
*/
|
|
141
|
+
export const CATEGORICAL_HOVER_CLASS: Record<
|
|
142
|
+
CategoricalIndex,
|
|
143
|
+
Record<IntentAppearance, string>
|
|
144
|
+
> = {
|
|
145
|
+
1: {
|
|
146
|
+
ghost: "hover:bg-categorical-1-surface",
|
|
147
|
+
outline: "hover:bg-categorical-1-surface",
|
|
148
|
+
soft: "hover:bg-categorical-1-surface-hover",
|
|
149
|
+
solid: "hover:bg-categorical-1-solid-hover",
|
|
150
|
+
},
|
|
151
|
+
2: {
|
|
152
|
+
ghost: "hover:bg-categorical-2-surface",
|
|
153
|
+
outline: "hover:bg-categorical-2-surface",
|
|
154
|
+
soft: "hover:bg-categorical-2-surface-hover",
|
|
155
|
+
solid: "hover:bg-categorical-2-solid-hover",
|
|
156
|
+
},
|
|
157
|
+
3: {
|
|
158
|
+
ghost: "hover:bg-categorical-3-surface",
|
|
159
|
+
outline: "hover:bg-categorical-3-surface",
|
|
160
|
+
soft: "hover:bg-categorical-3-surface-hover",
|
|
161
|
+
solid: "hover:bg-categorical-3-solid-hover",
|
|
162
|
+
},
|
|
163
|
+
4: {
|
|
164
|
+
ghost: "hover:bg-categorical-4-surface",
|
|
165
|
+
outline: "hover:bg-categorical-4-surface",
|
|
166
|
+
soft: "hover:bg-categorical-4-surface-hover",
|
|
167
|
+
solid: "hover:bg-categorical-4-solid-hover",
|
|
168
|
+
},
|
|
169
|
+
5: {
|
|
170
|
+
ghost: "hover:bg-categorical-5-surface",
|
|
171
|
+
outline: "hover:bg-categorical-5-surface",
|
|
172
|
+
soft: "hover:bg-categorical-5-surface-hover",
|
|
173
|
+
solid: "hover:bg-categorical-5-solid-hover",
|
|
174
|
+
},
|
|
175
|
+
6: {
|
|
176
|
+
ghost: "hover:bg-categorical-6-surface",
|
|
177
|
+
outline: "hover:bg-categorical-6-surface",
|
|
178
|
+
soft: "hover:bg-categorical-6-surface-hover",
|
|
179
|
+
solid: "hover:bg-categorical-6-solid-hover",
|
|
180
|
+
},
|
|
181
|
+
7: {
|
|
182
|
+
ghost: "hover:bg-categorical-7-surface",
|
|
183
|
+
outline: "hover:bg-categorical-7-surface",
|
|
184
|
+
soft: "hover:bg-categorical-7-surface-hover",
|
|
185
|
+
solid: "hover:bg-categorical-7-solid-hover",
|
|
186
|
+
},
|
|
187
|
+
8: {
|
|
188
|
+
ghost: "hover:bg-categorical-8-surface",
|
|
189
|
+
outline: "hover:bg-categorical-8-surface",
|
|
190
|
+
soft: "hover:bg-categorical-8-surface-hover",
|
|
191
|
+
solid: "hover:bg-categorical-8-solid-hover",
|
|
192
|
+
},
|
|
193
|
+
9: {
|
|
194
|
+
ghost: "hover:bg-categorical-9-surface",
|
|
195
|
+
outline: "hover:bg-categorical-9-surface",
|
|
196
|
+
soft: "hover:bg-categorical-9-surface-hover",
|
|
197
|
+
solid: "hover:bg-categorical-9-solid-hover",
|
|
198
|
+
},
|
|
199
|
+
10: {
|
|
200
|
+
ghost: "hover:bg-categorical-10-surface",
|
|
201
|
+
outline: "hover:bg-categorical-10-surface",
|
|
202
|
+
soft: "hover:bg-categorical-10-surface-hover",
|
|
203
|
+
solid: "hover:bg-categorical-10-solid-hover",
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The fill on its own, for the parts of a component that are a dot
|
|
209
|
+
* or a bar rather than a box — the swatch beside a label in a
|
|
210
|
+
* picker, a series marker on a chart. Same values, no border and no
|
|
211
|
+
* hover, because neither of those is a surface anyone can point at.
|
|
212
|
+
*/
|
|
213
|
+
export const CATEGORICAL_SOLID_FILL_CLASS: Record<
|
|
214
|
+
CategoricalIndex,
|
|
215
|
+
string
|
|
216
|
+
> = {
|
|
217
|
+
1: "bg-categorical-1-solid",
|
|
218
|
+
2: "bg-categorical-2-solid",
|
|
219
|
+
3: "bg-categorical-3-solid",
|
|
220
|
+
4: "bg-categorical-4-solid",
|
|
221
|
+
5: "bg-categorical-5-solid",
|
|
222
|
+
6: "bg-categorical-6-solid",
|
|
223
|
+
7: "bg-categorical-7-solid",
|
|
224
|
+
8: "bg-categorical-8-solid",
|
|
225
|
+
9: "bg-categorical-9-solid",
|
|
226
|
+
10: "bg-categorical-10-solid",
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The same ten as text, for a glyph or a dot's ring. */
|
|
230
|
+
export const CATEGORICAL_CONTENT_CLASS: Record<
|
|
231
|
+
CategoricalIndex,
|
|
232
|
+
string
|
|
233
|
+
> = {
|
|
234
|
+
1: "text-categorical-1-content",
|
|
235
|
+
2: "text-categorical-2-content",
|
|
236
|
+
3: "text-categorical-3-content",
|
|
237
|
+
4: "text-categorical-4-content",
|
|
238
|
+
5: "text-categorical-5-content",
|
|
239
|
+
6: "text-categorical-6-content",
|
|
240
|
+
7: "text-categorical-7-content",
|
|
241
|
+
8: "text-categorical-8-content",
|
|
242
|
+
9: "text-categorical-9-content",
|
|
243
|
+
10: "text-categorical-10-content",
|
|
244
|
+
}
|