@clicktap/ui 2.3.2 → 2.3.4

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 (56) 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/PasswordCheck/PasswordCheck.utils.d.ts +15 -0
  34. package/components/PasswordCheck/PasswordCheck.utils.js +1 -1
  35. package/components/PhoneInput/PhoneInput.js +1 -1
  36. package/components/PinInput/PinInput.js +1 -1
  37. package/components/Progressbar/CircularProgressbar.js +1 -1
  38. package/components/Progressbar/LinearProgressbar.js +1 -1
  39. package/components/Select/ButtonSelect.js +1 -1
  40. package/components/Select/ComboBoxSelect.js +1 -1
  41. package/components/Select/MultiButtonSelect.js +1 -1
  42. package/components/Select/parts.js +1 -1
  43. package/components/Skeleton/Skeleton.js +1 -1
  44. package/components/Slider/Slider.js +1 -1
  45. package/components/Switch/Switch.js +1 -1
  46. package/components/Tabs/Tab.js +1 -1
  47. package/components/Tabs/TabList.js +1 -1
  48. package/components/TimeInput/TimeInput.js +1 -1
  49. package/components/ToggleButton/ToggleButton.js +1 -1
  50. package/components/Tooltip/Tooltip.js +1 -1
  51. package/components/UploadImage/UploadImage.js +1 -1
  52. package/docs/migrations/2026/09/ui-2026-09-24-dialog-semantic-surface-tokens.md +81 -0
  53. package/docs/migrations/2026/09/ui-2026-09-24-password-check-lowercase-and-trim.md +81 -0
  54. package/docs/migrations/2026/09/ui-2026-09-24-semantic-color-token-lint-rule.md +283 -0
  55. package/docs/migrations/index.json +30 -0
  56. package/package.json +1 -1
@@ -0,0 +1,81 @@
1
+ # BUGFIX: `PasswordCheck` states the password policy the backend actually enforces
2
+
3
+ **Date:** 2026-09-24
4
+ **Package:** `@clicktap/ui`
5
+ **Affects:** `src/components/PasswordCheck/PasswordCheck.tsx`, `PasswordCheck.utils.ts` — every `<PasswordCheck variant="requirements">` that relies on the default `requirements`.
6
+ **Classification:** BUGFIX
7
+ **Breaking:** No. No API change: `requirements`, `value` and `variant` are untouched, and a consumer passing its own `requirements` is unaffected. **Displayed:** the default list grows from four rules to five, and all five are now evaluated against the edge-trimmed value.
8
+ **Ticket:** CTAP-1906
9
+
10
+ ## Problem
11
+
12
+ `PasswordCheck` restated the password policy in its own words, and the words were wrong.
13
+
14
+ The policy lives in PHP — `PasswordService::getPasswordSchema()`,
15
+ `libs/mezzio-modules/user/src/Service/PasswordService.php`:
16
+
17
+ ```php
18
+ $sb->string()->trim()->min(8)
19
+ ->withValidator(new AtLeastOneUppercaseValidator()) // /[A-Z]/
20
+ ->withValidator(new AtLeastOneLowercaseValidator()) // /[a-z]/
21
+ ->withValidator(new AtLeastOneNumberValidator()) // /\d/
22
+ ->withValidator(new AtLeastOneSpecialCharacterValidator()); // /[\W_]/
23
+ ```
24
+
25
+ Five rules. The component displayed four — the lowercase rule was missing. So `PASSWORD1!` turned
26
+ **every displayed tick green** and the server refused the save, and because the rule that rejected
27
+ the visitor was never on screen, the form could not tell them what to change.
28
+
29
+ Two narrower mismatches came with it:
30
+
31
+ - **Length was measured on the raw value** while the schema measures the trimmed one. `" abcdef1A! "`
32
+ showed as 13 characters and was validated as 9. The schema's `->trim()` is
33
+ `Rmg\Validator\EdgeWhitespace`, which is wider than PHP's `trim()`: NUL, U+200B and U+FEFF too.
34
+ - **The special-character rule was narrower than the validator.** The component tested a fixed
35
+ punctuation list; `AtLeastOneSpecialCharacterValidator` is `/[\W_]/` — anything outside
36
+ `[A-Za-z0-9]`. A password whose only special character was `£`, a non-ASCII letter or an interior
37
+ space was refused a tick the server would have granted.
38
+
39
+ ## What changed
40
+
41
+ `defaultRequirements` is now the five rules above, in schema order, each one carrying a comment
42
+ naming the validator it mirrors:
43
+
44
+ | # | Displayed rule | Backend rule | Before |
45
+ | --- | --- | --- | --- |
46
+ | 1 | At least 8 characters | `->min(8)` (post-trim) | measured untrimmed |
47
+ | 2 | At least one uppercase letter | `AtLeastOneUppercaseValidator` | unchanged |
48
+ | 3 | At least one lowercase letter | `AtLeastOneLowercaseValidator` | **missing** |
49
+ | 4 | At least one digit | `AtLeastOneNumberValidator` | unchanged |
50
+ | 5 | At least one special character | `AtLeastOneSpecialCharacterValidator` (`/[\W_]/`) | fixed punctuation list |
51
+
52
+ The component trims once and hands the trimmed value to every rule and to `checkStrength`, because
53
+ the schema's `->trim()` runs before `->min(8)` and before every validator. The new
54
+ `trimEdgeWhitespace` export in `PasswordCheck.utils.ts` mirrors `Rmg\Validator\EdgeWhitespace`:
55
+ JavaScript's `\s` (which already covers NBSP, the `Zs` category and U+FEFF) plus NUL and U+200B.
56
+ Interior whitespace is untouched, on both sides.
57
+
58
+ `PasswordCheck.requirements.test.tsx` pins the rendered rule set and names
59
+ `PasswordService::getPasswordSchema()` as the source of truth, so the next change to the policy on
60
+ either side lands as a failing test pointing at the other half.
61
+
62
+ This is the hand-mirroring fix. Generating the list from the serialized schema, so the two cannot
63
+ drift again, is **CTAP-1908**.
64
+
65
+ ## Consumer Impact
66
+
67
+ **No action needed.** No prop, export or type changed, and the extra rule appears on its own.
68
+
69
+ **A consumer that passes its own `requirements` keeps exactly what it passes** — the prop still
70
+ overrides the default outright. But that list has the same hole this fix closed, so check it against
71
+ the table above. One such copy existed in the storefront and was fixed in the same change:
72
+ `apps/frontend/components/Account/Auth/Authenticate/constanst.ts` (`passwordValidation`, the zod gate
73
+ behind signup, reset-password and update-password) was missing the lowercase rule and carried the
74
+ same narrow special-character list.
75
+
76
+ **Rules now receive the trimmed value.** If you pass custom `requirements`, your `test` callbacks
77
+ are called with the edge-trimmed string, not the raw one. A rule that deliberately inspected
78
+ leading/trailing whitespace would stop seeing it — nothing in this repo did.
79
+
80
+ **Visually, the block is one line taller** in the `requirements` variant. Nothing else about its
81
+ layout, the strength meter or the hide-until-typed behavior moved.
@@ -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,34 @@
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-password-check-lowercase-and-trim.md",
14
+ "module": "libs/frontend-modules/ui",
15
+ "package": "@clicktap/ui",
16
+ "date": "2026-09-24",
17
+ "title": "BUGFIX: `PasswordCheck` states the password policy the backend actually enforces",
18
+ "classification": "BUGFIX",
19
+ "breaking": "No. No API change: `requirements`, `value` and `variant` are untouched, and a consumer passing its own `requirements` is unaffected. **Displayed:** the default list grows from four rules to five, and all five are now evaluated against the edge-trimmed value.",
20
+ "affects": "`src/components/PasswordCheck/PasswordCheck.tsx`, `PasswordCheck.utils.ts` — every `<PasswordCheck variant=\"requirements\">` that relies on the default `requirements`."
21
+ },
22
+ {
23
+ "path": "2026/09/ui-2026-09-24-dialog-semantic-surface-tokens.md",
24
+ "module": "libs/frontend-modules/ui",
25
+ "package": "@clicktap/ui",
26
+ "date": "2026-09-24",
27
+ "title": "BUGFIX: `Dialog` paints from semantic tokens instead of literal `bg-white`",
28
+ "classification": "BUGFIX",
29
+ "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.",
30
+ "affects": "`src/components/Dialog/Dialog.tsx` — every `<Dialog>` that does not already pass its own background in `className`."
31
+ },
2
32
  {
3
33
  "path": "2026/09/ui-2026-09-23-interactive-elements-cursor-pointer.md",
4
34
  "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.4",
4
4
  "private": false,
5
5
  "author": "Clicktap",
6
6
  "description": "A library of React UI components and low-level hooks.",