@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.
- package/components/Accordion/Accordion.js +1 -1
- package/components/Badge/Badge.d.ts +2 -2
- package/components/Badge/Badge.js +1 -1
- package/components/Button/Button.js +1 -1
- package/components/Checkbox/Checkbox.js +1 -1
- package/components/Collection/CollectionFilterChips/CollectionFilterChips.js +1 -1
- package/components/Collection/CollectionFilters/CollectionFilters.js +1 -1
- package/components/Collection/CollectionToolbar/CollectionToolbar.js +1 -1
- package/components/Collection/EmptyResults/EmptyResults.js +1 -1
- package/components/Collection/FilterRenderer/Renderer/Swatch/SwatchText.js +1 -1
- package/components/Collection/ProductCardSwatches/ProductCardSwatches.js +1 -1
- package/components/Collection/ProductPrice/ProductPrice.js +1 -1
- package/components/ContextMenu/ContextMenu.js +1 -1
- package/components/CreditCardExpirationInput/CreditCardExpirationInput.js +1 -1
- package/components/CreditCardInput/CreditCardInput.js +1 -1
- package/components/DateInput/DateInput.js +1 -1
- package/components/Dialog/Dialog.js +1 -1
- package/components/Divider/Divider.js +1 -1
- package/components/DobInput/DobInput.js +1 -1
- package/components/Drawer/Drawer.js +1 -1
- package/components/FeatureGate/AccessDenied.js +1 -1
- package/components/FeatureGate/PermissionRequired.js +1 -1
- package/components/FeatureGate/UpgradePrompt.js +1 -1
- package/components/Input/Input.js +1 -1
- package/components/Link/Link.js +1 -1
- package/components/LinkAsButton/LinkAsButton.js +1 -1
- package/components/Loader/Pulse.js +1 -1
- package/components/Menu/ContextMenu.js +1 -1
- package/components/Meter/Meter.js +1 -1
- package/components/NumberInput/NumberInput.js +1 -1
- package/components/Pagination/Pagination.js +1 -1
- package/components/PasswordCheck/PasswordCheck.js +1 -1
- package/components/PhoneInput/PhoneInput.js +1 -1
- package/components/PinInput/PinInput.js +1 -1
- package/components/Progressbar/CircularProgressbar.js +1 -1
- package/components/Progressbar/LinearProgressbar.js +1 -1
- package/components/Select/ButtonSelect.js +1 -1
- package/components/Select/ComboBoxSelect.js +1 -1
- package/components/Select/MultiButtonSelect.js +1 -1
- package/components/Select/parts.js +1 -1
- package/components/Skeleton/Skeleton.js +1 -1
- package/components/Slider/Slider.js +1 -1
- package/components/Switch/Switch.js +1 -1
- package/components/Tabs/Tab.js +1 -1
- package/components/Tabs/TabList.js +1 -1
- package/components/TimeInput/TimeInput.js +1 -1
- package/components/ToggleButton/ToggleButton.js +1 -1
- package/components/Tooltip/Tooltip.js +1 -1
- package/components/UploadImage/UploadImage.js +1 -1
- package/docs/migrations/2026/09/ui-2026-09-24-dialog-semantic-surface-tokens.md +81 -0
- package/docs/migrations/2026/09/ui-2026-09-24-semantic-color-token-lint-rule.md +283 -0
- package/docs/migrations/index.json +20 -0
- 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",
|