@clicktap/ui 2.3.2 → 2.3.3

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 (53) hide show
  1. package/components/Accordion/Accordion.js +1 -1
  2. package/components/Badge/Badge.d.ts +2 -2
  3. package/components/Badge/Badge.js +1 -1
  4. package/components/Button/Button.js +1 -1
  5. package/components/Checkbox/Checkbox.js +1 -1
  6. package/components/Collection/CollectionFilterChips/CollectionFilterChips.js +1 -1
  7. package/components/Collection/CollectionFilters/CollectionFilters.js +1 -1
  8. package/components/Collection/CollectionToolbar/CollectionToolbar.js +1 -1
  9. package/components/Collection/EmptyResults/EmptyResults.js +1 -1
  10. package/components/Collection/FilterRenderer/Renderer/Swatch/SwatchText.js +1 -1
  11. package/components/Collection/ProductCardSwatches/ProductCardSwatches.js +1 -1
  12. package/components/Collection/ProductPrice/ProductPrice.js +1 -1
  13. package/components/ContextMenu/ContextMenu.js +1 -1
  14. package/components/CreditCardExpirationInput/CreditCardExpirationInput.js +1 -1
  15. package/components/CreditCardInput/CreditCardInput.js +1 -1
  16. package/components/DateInput/DateInput.js +1 -1
  17. package/components/Dialog/Dialog.js +1 -1
  18. package/components/Divider/Divider.js +1 -1
  19. package/components/DobInput/DobInput.js +1 -1
  20. package/components/Drawer/Drawer.js +1 -1
  21. package/components/FeatureGate/AccessDenied.js +1 -1
  22. package/components/FeatureGate/PermissionRequired.js +1 -1
  23. package/components/FeatureGate/UpgradePrompt.js +1 -1
  24. package/components/Input/Input.js +1 -1
  25. package/components/Link/Link.js +1 -1
  26. package/components/LinkAsButton/LinkAsButton.js +1 -1
  27. package/components/Loader/Pulse.js +1 -1
  28. package/components/Menu/ContextMenu.js +1 -1
  29. package/components/Meter/Meter.js +1 -1
  30. package/components/NumberInput/NumberInput.js +1 -1
  31. package/components/Pagination/Pagination.js +1 -1
  32. package/components/PasswordCheck/PasswordCheck.js +1 -1
  33. package/components/PhoneInput/PhoneInput.js +1 -1
  34. package/components/PinInput/PinInput.js +1 -1
  35. package/components/Progressbar/CircularProgressbar.js +1 -1
  36. package/components/Progressbar/LinearProgressbar.js +1 -1
  37. package/components/Select/ButtonSelect.js +1 -1
  38. package/components/Select/ComboBoxSelect.js +1 -1
  39. package/components/Select/MultiButtonSelect.js +1 -1
  40. package/components/Select/parts.js +1 -1
  41. package/components/Skeleton/Skeleton.js +1 -1
  42. package/components/Slider/Slider.js +1 -1
  43. package/components/Switch/Switch.js +1 -1
  44. package/components/Tabs/Tab.js +1 -1
  45. package/components/Tabs/TabList.js +1 -1
  46. package/components/TimeInput/TimeInput.js +1 -1
  47. package/components/ToggleButton/ToggleButton.js +1 -1
  48. package/components/Tooltip/Tooltip.js +1 -1
  49. package/components/UploadImage/UploadImage.js +1 -1
  50. package/docs/migrations/2026/09/ui-2026-09-24-dialog-semantic-surface-tokens.md +81 -0
  51. package/docs/migrations/2026/09/ui-2026-09-24-semantic-color-token-lint-rule.md +283 -0
  52. package/docs/migrations/index.json +20 -0
  53. package/package.json +1 -1
@@ -0,0 +1,283 @@
1
+ # IMPROVEMENT: literal palette colors are a lint error in `@clicktap/ui`, and the 136 existing ones are resolved
2
+
3
+ **Date:** 2026-09-24
4
+ **Package:** `@clicktap/ui`
5
+ **Affects:** 54 files under `src/components/**` — every component listed in "What changed" below — plus the new root-level `eslint.rules.no-literal-palette-color.mjs` and the two eslint configs that reference it.
6
+ **Classification:** IMPROVEMENT
7
+ **Breaking:** No API change, and no prop, export or class-name contract moves. **Visual:** 76 of the 118 swaps resolve to the exact same light-theme value and render identically. The other 42 do not — they are listed component by component under "Rendering changes" and are all one step along a neutral ramp, except `UploadImage`, whose hover background **appears for the first time** because the class it had was a typo.
8
+ **Ticket:** CTAP-1904
9
+
10
+ ## Problem
11
+
12
+ Four tickets described the same defect: a component styled with a literal Tailwind palette utility
13
+ instead of a semantic token. It is invisible in a light-themed app and wrong — sometimes illegible —
14
+ in a re-themed one. CTAP-1123 did the original sweep, CTAP-1304 fixed `BreadcrumbLink`, CTAP-1834
15
+ asked for the token set, CTAP-1902 fixed `Dialog`'s `bg-white`.
16
+
17
+ Review missed it three times running, so the convention needed a machine check. And a rule without
18
+ the sweep would fail 54 files on day one; a sweep without the rule would regress, which is exactly
19
+ the history above. Both land here.
20
+
21
+ ## The survey
22
+
23
+ Measured on this branch's merge-base with `master`, over `src/**/*.{ts,tsx}`, excluding
24
+ `*.stories.*` / `*.test.*` / `*.spec.*`:
25
+
26
+ | | Count |
27
+ |---|---|
28
+ | Textual matches | 140 across 55 files |
29
+ | …of which sit inside a **comment** (a docblock `@example`, a note naming a class in prose) | 4 |
30
+ | **Literals in code — what the rule reports** | **136 across 54 files** |
31
+ | Matches in stories / tests / specs, which the rule excludes on purpose | 28 across 6 files |
32
+
33
+ The ticket's survey said 139 across 54 files. The difference is accounted for: it was measured after
34
+ CTAP-1902's `Dialog` fix, which is on PR #3030 and not yet in `master`, so `Dialog`'s `bg-white` is
35
+ the 140th match here; and the ticket counted textually, where an AST rule does not see the four
36
+ matches that live in comments. The ticket's "34 hits in stories/tests" is 28 by this regex — the
37
+ gap is arbitrary-value classes such as `bg-[#222]`, which are deliberately out of scope.
38
+
39
+ Resolution of the 136:
40
+
41
+ | | Count |
42
+ |---|---|
43
+ | Swapped to a semantic token | 118 |
44
+ | Annotated as deliberate, with a reason | 17 |
45
+ | Deleted as dead code (`shadow-slate-200` with no `shadow` utility beside it, so it painted nothing) | 1 |
46
+
47
+ ## The rule
48
+
49
+ `clicktap/no-literal-palette-color`, defined in `eslint.rules.no-literal-palette-color.mjs` at the
50
+ workspace root and turned on for `libs/frontend-modules/ui/src/**` only.
51
+
52
+ **Why a custom rule.** `no-restricted-syntax` — the mechanism the repo already uses for the
53
+ `z.string().trim()` convention — can match a string against a regex, but its message is a fixed
54
+ string: it can say the line has a literal palette color and not *which* one. A `cn()` call in this
55
+ package routinely carries a dozen classes over several lines, so naming the offender is the
56
+ difference between a usable error and a hunt. An AST rule also skips comments for free, which is
57
+ what keeps a docblock explaining this very convention from being an error.
58
+ `eslint-plugin-tailwindcss` is not a dependency of this workspace and has no "these colors are off
59
+ limits" rule, so it would have meant a new dependency *and* this rule.
60
+
61
+ **What it flags:** `bg-`, `text-`, `border-`, `ring-`, `ring-offset-`, `fill-`, `stroke-`, `from-`,
62
+ `via-`, `to-`, `divide-`, `outline-`, `decoration-`, `shadow-`, `caret-`, `accent-`, `placeholder-`
63
+ — with an optional side/axis segment (`border-t-`, `divide-y-`) — followed by `white`, `black`, or
64
+ one of the 22 named ramps with a numeric step. Variant prefixes and the important modifier pass
65
+ through (`dark:hover:!bg-white` is reported as `bg-white`), and the opacity modifier is reported as
66
+ part of the class (`bg-black/30`).
67
+
68
+ **What it does not flag, deliberately:**
69
+
70
+ - **`transparent`, `current`, `inherit` are not colors.** They carry no theme decision and repaint
71
+ themselves, so `bg-transparent` and `text-current` stay legal. The ticket asked for this call to
72
+ be stated: it is stated.
73
+ - **Arbitrary values** — `shadow-[0_8px_24px_rgba(0,0,0,0.1)]`, `bg-[#222]`. `theme.css` publishes
74
+ no `--shadow-*` token, so flagging them would produce hits with nowhere to go. Minting those
75
+ tokens is CTAP-1834's business.
76
+ - **Stories, tests, specs and fixtures.** Excluded twice over: by the config block's `ignores`, and
77
+ by a filename check inside the rule itself, so it holds however the rule is wired up.
78
+ - **Comments.** The rule visits string and template nodes only.
79
+
80
+ **Where it runs.** In both `libs/frontend-modules/ui/eslint.config.mjs` and the root
81
+ `eslint.config.mjs`, for the same reason `eslint.rules.trim-form-strings.mjs` lives in both:
82
+ `lint-staged` resolves the **root** config on commit while `nx run frontend-module-ui:lint` resolves
83
+ the package's, and a rule known to only one passes one and fails the other. Both wirings were
84
+ verified live. The `files` glob is what keeps the rule inside the library: `nx run frontend:lint`
85
+ and `nx run admin:lint` report zero hits from it, which is intended — the rule is about what the
86
+ library hardcodes, not about what an app writes for its own pages.
87
+
88
+ ## The annotation convention
89
+
90
+ Some literals are right, so the rule has an opt-out that requires a reason:
91
+
92
+ ```tsx
93
+ // theme-literal: a scrim is black in every theme; its opacity is the lever, not its hue
94
+ 'bg-black/30 backdrop-blur-sm',
95
+ ```
96
+
97
+ The marker covers its own line and the next line of **code** — it resolves forward past a second
98
+ comment line and past the `}` that closes a `{/* … */}` JSX comment, so a reason may run to two or
99
+ three lines and survives prettier reflowing a ternary. The regex requires text after the colon: a
100
+ bare `// theme-literal:` does not suppress anything (proven below). This is the shape the repo
101
+ already uses for `// no-trim:`, `// blank-ok:` and `// no-persist:` rather than a third one, and it
102
+ has one practical advantage over `// eslint-disable-next-line`: `eslint --fix` from the root deletes
103
+ a disable directive naming a rule that run does not know, and a plain comment survives that.
104
+ `// eslint-disable-next-line clicktap/no-literal-palette-color -- reason` still works.
105
+
106
+ There is **no** file-level or package-level suppression anywhere in this change. Every one of the 17
107
+ exemptions is a single line carrying its own reason.
108
+
109
+ ### The 17 deliberate literals
110
+
111
+ | Component | Class | Why it stays |
112
+ |---|---|---|
113
+ | `ModalOverlay` | `bg-black/30` | A scrim is black in every theme; its opacity is the lever. |
114
+ | `Checkbox` | `stroke-white`, `fill-white` | The tick and the indeterminate dash are drawn **on the filled control**, so they take their contrast from that fill, not from the page. |
115
+ | `ProductCardSwatches` | `ring-white`, `bg-white` ×2, `bg-slate-400` ×2, `text-white` | Every one is drawn on the swatch's **own** color, which comes from product data. `needsBorder` is the component's own light/dark test for that color. A re-themed `--color-fg-inverse` would make the check vanish on half a catalog. |
116
+ | `SwatchColor` | `ring-white`, `text-white` | The same marks, in the filter-sidebar rendering of the same swatch. |
117
+ | `GemIcon` | `text-purple-500` | Purple *is* the premium/upgrade signal the glyph carries. |
118
+ | `LockIcon` | `text-yellow-500` | Amber *is* the restricted/permission signal. |
119
+ | `StarIcon` | `text-amber-400` | A filled rating star is gold in every theme. (Its empty state already used `text-fg-subtle` and still does.) |
120
+ | `PermissionRequired` | `bg-yellow-100` | The chip tint pairs with `LockIcon`'s deliberately literal amber. |
121
+ | `UpgradePrompt` | `bg-purple-100` | The chip tint pairs with `GemIcon`'s deliberately literal purple. |
122
+ | `Radio` | `text-red-300` | A faded danger for the disabled-invalid state. |
123
+
124
+ The ticket named four that must survive — `ModalOverlay`, `Checkbox`, `ProductCardSwatches`, `Icon`.
125
+ Each was read in context and confirmed. `SwatchColor` is the same swatch mark as
126
+ `ProductCardSwatches` rendered in the filter sidebar and is annotated on the same grounds.
127
+
128
+ ### Deferred to CTAP-1834
129
+
130
+ Three of the annotations are annotations only because the token set has no role for them. They are
131
+ not judgments that a literal is right forever; no token is invented here:
132
+
133
+ - **A warning surface** and **a brand/premium surface** — `bg-yellow-100` in `PermissionRequired`,
134
+ `bg-purple-100` in `UpgradePrompt`. `--color-danger-surface` exists; there is no `warning`/`info`
135
+ equivalent, and nothing for a brand tint at all.
136
+ - **A muted danger** — `text-red-300` in `Radio`. `--color-danger-ring` is red-200 and is
137
+ documented as the invalid focus ring's value; using it as disabled text would be role abuse.
138
+ - **Shadows.** `shadow-[0_8px_24px_rgba(0,0,0,0.1)]` in `Dialog` and `shadow-[0_0_0_2px_#222]` in
139
+ the two swatch components are out of the rule's scope entirely, as the ticket directed.
140
+
141
+ ## What changed
142
+
143
+ Every swap and its role token. `theme.css` defines each token as the literal Tailwind value the
144
+ components used before the CTAP-1123 sweep, so "same value" below means byte-identical in the
145
+ default theme.
146
+
147
+ **Same light-theme value (76 occurrences) — nothing renders differently:**
148
+
149
+ | Literal | Token | n |
150
+ |---|---|---|
151
+ | `bg-white` | `bg-surface` | 25 |
152
+ | `text-white` | `text-fg-inverse` | 17 |
153
+ | `bg-slate-200` | `bg-border-subtle` | 9 |
154
+ | `bg-slate-300` | `bg-border` | 8 |
155
+ | `outline-slate-500` | `outline-fg-muted` | 5 |
156
+ | `bg-slate-500` | `bg-fg-muted` | 3 |
157
+ | `bg-slate-400` | `bg-border-emphasis` | 2 |
158
+ | `border-slate-500` | `border-fg-muted` | 2 |
159
+ | `outline-white` | `outline-fg-inverse` | 2 |
160
+ | `border-white` | `border-surface` | 1 |
161
+ | `fill-slate-300` | `fill-border` | 1 |
162
+ | `stroke-slate-300` | `stroke-border` | 1 |
163
+
164
+ **Different light-theme value (42 occurrences):**
165
+
166
+ | Literal | Token | Direction | n |
167
+ |---|---|---|---|
168
+ | `text-slate-600` | `text-fg-muted` (slate-500) | one step lighter | 7 |
169
+ | `text-slate-800` | `text-fg` (slate-900) | one step darker | 6 |
170
+ | `text-slate-700` | `text-fg` (slate-900) | two steps darker | 4 |
171
+ | `text-slate-700` | `text-fg-muted` (slate-500) | two steps lighter | 2 |
172
+ | `text-slate-300` | `text-fg-subtle` (slate-400) | one step darker | 4 |
173
+ | `bg-slate-600` | `bg-fg-muted` (slate-500) | one step lighter | 3 |
174
+ | `text-black` | `text-fg` (slate-900) | a hair lighter | 3 |
175
+ | `border-gray-200` | `border-border-subtle` (slate-200) | same lightness, faint hue shift | 2 |
176
+ | `bg-slate-50` / `fill-slate-50` / `bg-gray-50` | `*-surface-subtle` (slate-100) | one step darker | 3 |
177
+ | `border-slate-100` / `outline-slate-100` | `*-border-subtle` (slate-200) | one step darker | 2 |
178
+ | `border-gray-100` | `border-border-subtle` (slate-200) | one step darker | 1 |
179
+ | `text-slate-100` | `text-fg-inverse` (`#fff`) | a hair brighter | 1 |
180
+ | `text-gray-600` | `text-fg-muted` (slate-500) | one step lighter | 1 |
181
+ | `stroke-slate-800` / `fill-slate-800` | `stroke-fg` / `fill-fg` (slate-900) | one step darker | 2 |
182
+ | `text-green-700` | `text-success` (green-600) | one step lighter | 1 |
183
+
184
+ Where the same literal maps to two tokens, the role decided it. `text-slate-700` is the label on an
185
+ unselected swatch pill whose selected twin is `text-fg-inverse` on `bg-surface-inverse` — that is
186
+ the full-emphasis foreground, `text-fg`. The same class on `EmptyResults`' action links, whose hover
187
+ was already `text-fg`, is the muted end of a ladder, so `text-fg-muted`.
188
+
189
+ ### Rendering changes, component by component
190
+
191
+ A consumer who overrode one of these through `className` may have been working around the bug;
192
+ `cn` is tailwind-merge, so their override still wins and they lose nothing by dropping it.
193
+
194
+ | Component | What visibly changes in the default (light) theme |
195
+ |---|---|
196
+ | `Accordion` | Trigger focus ring slate-100 → slate-200. |
197
+ | `Badge` (`faded`) | Label slate-800 → slate-900. |
198
+ | `CollectionFilterChips` | Top rule gray-100 → slate-200 (a step darker). |
199
+ | `CollectionFilters` | Group heading slate-800 → slate-900; the two "clear" links' hover slate-700 → slate-900; the trigger and footer rules gray-200 → slate-200 (same lightness, a faintly cooler hue). |
200
+ | `CollectionToolbar` | Sort label gray-600 → slate-500 (lighter). |
201
+ | `EmptyResults` | Empty-state icon and the `\|` separator slate-300 → slate-400 (darker); heading slate-800 → slate-900; the two action links slate-700 → slate-500 (lighter at rest; their hover was already `text-fg`). |
202
+ | `FeatureGate` — `AccessDenied`, `PermissionRequired`, `UpgradePrompt` | Body copy slate-600 → slate-500 (lighter). |
203
+ | `Input` | Password-reveal toggle hover slate-600 → slate-500 (lighter). |
204
+ | `Link` | Hover slate-800 → slate-900; disabled and disabled-hover slate-300 → slate-400 (darker, so more legible). |
205
+ | `Menu`'s `ContextMenu` | Panel gray-50 → slate-100 and its arrow slate-50 → slate-100 (both a step darker). |
206
+ | `Pagination` | Control hover slate-50 → slate-100; page-item border slate-100 → slate-200; the `<nav>` text slate-600 → slate-500. |
207
+ | `PasswordCheck` | A met requirement's label green-700 → green-600 — which also makes it agree with its own check icon, already `text-success`; the strength segments past the current strength slate-600 → slate-500. |
208
+ | `PhoneInput` | Country-dropdown text and the dial-code-only input state slate-600 → slate-500. |
209
+ | `CircularProgressbar` | Progress arc and the centered value slate-800 → slate-900. |
210
+ | `ProductPrice` | Price and label `#000` → slate-900. |
211
+ | `ProductCardSwatches`, `SwatchText` | Unselected pill label slate-700 → slate-900. |
212
+ | `Tab` | Labels slate-800 → slate-900; the selected label on `solid`/`enclosed` slate-100 → `#fff`; the `solid` overlay slate-600 → slate-500 — under `mix-blend-color`, which takes only hue and chroma, and those two are within 0.003 of each other, so this one is not observable. |
213
+ | `UploadImage` | **The only new rendering.** The class was `hover:bg-slate-100s` — a typo, not a utility, so it never painted. It is now `hover:bg-surface-subtle`, so the control gains the hover background it was always meant to have. Its `hover:text-black` also becomes slate-900. |
214
+ | `Accordion` (`shadow` variant) | Nothing. `shadow-slate-200` set a shadow *color* with no `shadow` utility beside it, so it painted nothing; it is deleted. |
215
+
216
+ Everything not in that table renders exactly as before: `Button`, `Checkbox`, `ContextMenu`,
217
+ `CreditCardInput`, `CreditCardExpirationInput`, `DateInput`, `Dialog`, `Divider`, `DobInput`,
218
+ `Drawer`, `LinkAsButton`, `Loader`'s `Pulse`, `Meter`, `ModalOverlay`, `NumberInput`, `PinInput`,
219
+ `LinearProgressbar`, `Radio`, `Select` (all four files), `Skeleton`, `Slider`, `Switch`, `TabList`,
220
+ `TimeInput`, `ToggleButton`, `Tooltip`, and the three `Icon` glyphs.
221
+
222
+ ## On `Dialog.spec.tsx`
223
+
224
+ CTAP-1902 put a rendered-DOM assertion into `Dialog.spec.tsx` that fails on any literal palette
225
+ utility reaching the panel. That file arrives with PR #3030 and is not in `master`, so nothing here
226
+ touches it — and it should **stay**. The two checks do not overlap: the lint rule is static and sees
227
+ string literals in that one file, while the spec asserts on what actually reached the DOM, so it
228
+ still catches a literal arriving through a `tv()` variant assembled elsewhere, a HeroUI default, or
229
+ a consumer's `className`. The rule can also be switched off on a line by an annotation; the spec
230
+ cannot. Belt and braces, on purpose.
231
+
232
+ ## Consumer Impact
233
+
234
+ **No action needed to upgrade.** No prop, export or type changes. Class names on the rendered DOM do
235
+ change (`bg-white` becomes `bg-surface`), so a consumer asserting on a literal class name in a test —
236
+ or targeting one in a CSS selector — needs to update that assertion; nothing else does.
237
+
238
+ **You can now re-theme.** The point of the sweep: overriding `--color-surface`, `--color-fg` and the
239
+ rest under a class, an attribute or a media query now repaints these components, where before it
240
+ repainted only the ones CTAP-1123 had already converted.
241
+
242
+ ```css
243
+ :root[data-theme='dark'] {
244
+ --color-fg: oklch(98% 0 0);
245
+ --color-surface: oklch(20% 0.02 260);
246
+ --color-surface-subtle: oklch(25% 0.02 260);
247
+ }
248
+ ```
249
+
250
+ **Drop workaround overrides.** If you were passing `className="bg-slate-900 text-white"` to a
251
+ component purely because it hardcoded a light surface, remove it and set the token instead.
252
+
253
+ **Contributing to the package.** Write the token, not the literal. If a literal is genuinely right,
254
+ say why on the line above with `// theme-literal: <reason>` — the reason is required, and a bare
255
+ marker does not suppress anything.
256
+
257
+ ## Verification
258
+
259
+ ```
260
+ nx run frontend-module-ui:lint 0 errors (1 pre-existing react-hooks warning in ModalOverlay)
261
+ nx run frontend-module-ui:typecheck passed
262
+ nx run frontend-module-ui:test 24 suites, 136 tests, all passed
263
+ nx run frontend:lint 0 errors, 0 hits from this rule
264
+ nx run admin:lint 0 errors, 0 hits from this rule
265
+ ```
266
+
267
+ The rule was proven to fail before it was trusted to pass. Putting `bg-white` back into `Dialog`:
268
+
269
+ ```
270
+ libs/frontend-modules/ui/src/components/Dialog/Dialog.tsx
271
+ 64:7 error `bg-white` is a literal palette color: a re-themed app cannot repaint it. …
272
+ clicktap/no-literal-palette-color
273
+ ✖ 2 problems (1 error, 1 warning)
274
+ ```
275
+
276
+ and adding a reason-less `// theme-literal:` above it did **not** silence it:
277
+
278
+ ```
279
+ 65:7 error `bg-white` is a literal palette color: … clicktap/no-literal-palette-color
280
+ ✖ 2 problems (1 error, 1 warning)
281
+ ```
282
+
283
+ Both were reverted.
@@ -1,4 +1,24 @@
1
1
  [
2
+ {
3
+ "path": "2026/09/ui-2026-09-24-semantic-color-token-lint-rule.md",
4
+ "module": "libs/frontend-modules/ui",
5
+ "package": "@clicktap/ui",
6
+ "date": "2026-09-24",
7
+ "title": "IMPROVEMENT: literal palette colors are a lint error in `@clicktap/ui`, and the 136 existing ones are resolved",
8
+ "classification": "IMPROVEMENT",
9
+ "breaking": "No API change, and no prop, export or class-name contract moves. **Visual:** 76 of the 118 swaps resolve to the exact same light-theme value and render identically. The other 42 do not — they are listed component by component under \"Rendering changes\" and are all one step along a neutral ramp, except `UploadImage`, whose hover background **appears for the first time** because the class it had was a typo.",
10
+ "affects": "54 files under `src/components/**` — every component listed in \"What changed\" below — plus the new root-level `eslint.rules.no-literal-palette-color.mjs` and the two eslint configs that reference it."
11
+ },
12
+ {
13
+ "path": "2026/09/ui-2026-09-24-dialog-semantic-surface-tokens.md",
14
+ "module": "libs/frontend-modules/ui",
15
+ "package": "@clicktap/ui",
16
+ "date": "2026-09-24",
17
+ "title": "BUGFIX: `Dialog` paints from semantic tokens instead of literal `bg-white`",
18
+ "classification": "BUGFIX",
19
+ "breaking": "No. It is a change of default, and the default only moves where a consumer has re-themed the tokens: an app on the shipped light theme sees **no visual change at all** (`--color-surface` is `#fff`, the exact value `bg-white` produced, and `--color-fg` is the slate-900 such an app already inherited). A dark-themed app stops getting a white panel and gets its own surface. No API change, and the `className` escape hatch is untouched.",
20
+ "affects": "`src/components/Dialog/Dialog.tsx` — every `<Dialog>` that does not already pass its own background in `className`."
21
+ },
2
22
  {
3
23
  "path": "2026/09/ui-2026-09-23-interactive-elements-cursor-pointer.md",
4
24
  "module": "libs/frontend-modules/ui",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clicktap/ui",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "private": false,
5
5
  "author": "Clicktap",
6
6
  "description": "A library of React UI components and low-level hooks.",