@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.
Files changed (121) hide show
  1. package/README.md +10 -0
  2. package/dist/Badge/Badge.d.ts +33 -3
  3. package/dist/Badge/Badge.d.ts.map +1 -1
  4. package/dist/Badge/Badge.js +20 -2
  5. package/dist/Badge/Badge.js.map +1 -1
  6. package/dist/Board/Board.d.ts +119 -0
  7. package/dist/Board/Board.d.ts.map +1 -0
  8. package/dist/Board/Board.js +165 -0
  9. package/dist/Board/Board.js.map +1 -0
  10. package/dist/Board/BoardCard.d.ts +120 -0
  11. package/dist/Board/BoardCard.d.ts.map +1 -0
  12. package/dist/Board/BoardCard.js +77 -0
  13. package/dist/Board/BoardCard.js.map +1 -0
  14. package/dist/Board/BoardLaneList.d.ts +103 -0
  15. package/dist/Board/BoardLaneList.d.ts.map +1 -0
  16. package/dist/Board/BoardLaneList.js +69 -0
  17. package/dist/Board/BoardLaneList.js.map +1 -0
  18. package/dist/Board/boardMove.d.ts +116 -0
  19. package/dist/Board/boardMove.d.ts.map +1 -0
  20. package/dist/Board/boardMove.js +116 -0
  21. package/dist/Board/boardMove.js.map +1 -0
  22. package/dist/Board/useBoardDrag.d.ts +44 -0
  23. package/dist/Board/useBoardDrag.d.ts.map +1 -0
  24. package/dist/Board/useBoardDrag.js +237 -0
  25. package/dist/Board/useBoardDrag.js.map +1 -0
  26. package/dist/Checkbox/Checkbox.d.ts.map +1 -1
  27. package/dist/Checkbox/Checkbox.js +15 -4
  28. package/dist/Checkbox/Checkbox.js.map +1 -1
  29. package/dist/DataTable/DataTable.d.ts +159 -0
  30. package/dist/DataTable/DataTable.d.ts.map +1 -0
  31. package/dist/DataTable/DataTable.js +197 -0
  32. package/dist/DataTable/DataTable.js.map +1 -0
  33. package/dist/DatePicker/DateGrid.d.ts +79 -0
  34. package/dist/DatePicker/DateGrid.d.ts.map +1 -0
  35. package/dist/DatePicker/DateGrid.js +220 -0
  36. package/dist/DatePicker/DateGrid.js.map +1 -0
  37. package/dist/DatePicker/DatePicker.d.ts +138 -0
  38. package/dist/DatePicker/DatePicker.d.ts.map +1 -0
  39. package/dist/DatePicker/DatePicker.js +384 -0
  40. package/dist/DatePicker/DatePicker.js.map +1 -0
  41. package/dist/DatePicker/datePresets.d.ts +29 -0
  42. package/dist/DatePicker/datePresets.d.ts.map +1 -0
  43. package/dist/DatePicker/datePresets.js +28 -0
  44. package/dist/DatePicker/datePresets.js.map +1 -0
  45. package/dist/DatePicker/parseDateInput.d.ts +106 -0
  46. package/dist/DatePicker/parseDateInput.d.ts.map +1 -0
  47. package/dist/DatePicker/parseDateInput.js +419 -0
  48. package/dist/DatePicker/parseDateInput.js.map +1 -0
  49. package/dist/DatePicker/plainDate.d.ts +167 -0
  50. package/dist/DatePicker/plainDate.d.ts.map +1 -0
  51. package/dist/DatePicker/plainDate.js +272 -0
  52. package/dist/DatePicker/plainDate.js.map +1 -0
  53. package/dist/MarkdownEditor/MarkdownEditor.d.ts +147 -0
  54. package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
  55. package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
  56. package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
  57. package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
  58. package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
  59. package/dist/MarkdownEditor/markdownCommands.js +379 -0
  60. package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
  61. package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
  62. package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
  63. package/dist/MarkdownEditor/markdownSpans.js +303 -0
  64. package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
  65. package/dist/Select/Select.d.ts +36 -1
  66. package/dist/Select/Select.d.ts.map +1 -1
  67. package/dist/Select/Select.js +38 -2
  68. package/dist/Select/Select.js.map +1 -1
  69. package/dist/categoricalStyles.d.ts +53 -0
  70. package/dist/categoricalStyles.d.ts.map +1 -0
  71. package/dist/categoricalStyles.js +195 -0
  72. package/dist/categoricalStyles.js.map +1 -0
  73. package/dist/index.d.ts +22 -0
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +11 -0
  76. package/dist/index.js.map +1 -1
  77. package/package.json +2 -2
  78. package/src/Badge/Badge.mdx +46 -0
  79. package/src/Badge/Badge.stories.tsx +98 -4
  80. package/src/Badge/Badge.test.tsx +157 -2
  81. package/src/Badge/Badge.tsx +49 -3
  82. package/src/Board/Board.mdx +199 -0
  83. package/src/Board/Board.stories.tsx +707 -0
  84. package/src/Board/Board.test.tsx +310 -0
  85. package/src/Board/Board.tsx +333 -0
  86. package/src/Board/BoardCard.tsx +290 -0
  87. package/src/Board/BoardLaneList.tsx +275 -0
  88. package/src/Board/boardMove.test.ts +213 -0
  89. package/src/Board/boardMove.ts +225 -0
  90. package/src/Board/useBoardDrag.ts +413 -0
  91. package/src/Checkbox/Checkbox.tsx +34 -3
  92. package/src/DataTable/DataTable.mdx +123 -0
  93. package/src/DataTable/DataTable.stories.tsx +411 -0
  94. package/src/DataTable/DataTable.test.tsx +386 -0
  95. package/src/DataTable/DataTable.tsx +497 -0
  96. package/src/DatePicker/DateGrid.tsx +467 -0
  97. package/src/DatePicker/DatePicker.mdx +170 -0
  98. package/src/DatePicker/DatePicker.stories.tsx +268 -0
  99. package/src/DatePicker/DatePicker.test.tsx +455 -0
  100. package/src/DatePicker/DatePicker.tsx +852 -0
  101. package/src/DatePicker/datePresets.ts +34 -0
  102. package/src/DatePicker/parseDateInput.test.ts +187 -0
  103. package/src/DatePicker/parseDateInput.ts +629 -0
  104. package/src/DatePicker/plainDate.test.ts +375 -0
  105. package/src/DatePicker/plainDate.ts +465 -0
  106. package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
  107. package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
  108. package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
  109. package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
  110. package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
  111. package/src/MarkdownEditor/markdownCommands.ts +566 -0
  112. package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
  113. package/src/MarkdownEditor/markdownSpans.ts +458 -0
  114. package/src/Select/Select.mdx +44 -0
  115. package/src/Select/Select.stories.tsx +55 -0
  116. package/src/Select/Select.test.tsx +89 -1
  117. package/src/Select/Select.tsx +58 -2
  118. package/src/argTypes.storyHelpers.ts +39 -0
  119. package/src/categoricalStyles.ts +244 -0
  120. package/src/index.ts +89 -0
  121. package/src/sourceRules.test.ts +52 -1
@@ -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
+ })
@@ -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 className="relative inline-grid w-full items-center">
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
+ }