@bsuite/theme 0.13.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsuite/theme",
3
- "version": "0.13.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -142,3 +142,48 @@
142
142
  @media (forced-colors: active) {
143
143
  .text-gradient-accent { background-image: none; color: CanvasText; }
144
144
  }
145
+
146
+ /* ── NAV / SWITCHER GRADIENT UNDERLINE ───────────────────────────────────
147
+ TH-6. Promoted from crm7/src/index.css, 2026-08-17.
148
+
149
+ The operator asked for the nav gradient underline in every app. It was
150
+ unimplementable anywhere but crm7, because the treatment existed only in
151
+ crm7's own stylesheet — 3 files in one app (the two declarations here plus
152
+ two call sites), and ZERO occurrences in this package or in the other five
153
+ apps. A request that needs a copy-paste to satisfy is a packaging defect,
154
+ not a feature request, and copying it would have produced six declarations
155
+ drifting apart the way `--bg-shell-elevated` did.
156
+
157
+ It paints from --gradient-accent, which this package already owns, so
158
+ nothing app-specific came with it.
159
+
160
+ TWO FORMS, ONE PAINT SOURCE — that pairing is the point. Keeping them
161
+ adjacent is what makes "the switcher and the active nav item match" a fact
162
+ about this file rather than two declarations someone has to remember to
163
+ keep in step:
164
+
165
+ `-span` the caller already renders its own absolutely-positioned
166
+ element for the rule (crm7's tenant switcher does).
167
+ `::after` the caller does not, so the rule is generated for it (the
168
+ navigation items). REQUIRES `position: relative` on the host —
169
+ without it the rule anchors to the nearest positioned ancestor
170
+ and lands somewhere else on the page.
171
+
172
+ The `-span` form carries no geometry on purpose: the caller owns its own
173
+ inset/height/opacity, and crm7's switcher sets `opacity-70` with a
174
+ `group-hover:opacity-100` transition. Only the paint is shared. */
175
+ .bsuite-gradient-underline-span {
176
+ background: var(--gradient-accent);
177
+ }
178
+
179
+ .bsuite-gradient-underline::after {
180
+ content: '';
181
+ position: absolute;
182
+ inset-inline: 0.5rem;
183
+ bottom: 0.125rem;
184
+ height: 0.125rem;
185
+ border-radius: 9999px;
186
+ background: var(--gradient-accent);
187
+ opacity: 0.7;
188
+ pointer-events: none;
189
+ }
package/src/css/vars.css CHANGED
@@ -33,7 +33,8 @@
33
33
  * --light-bg-accent ship as oklch(1 0 0.5) — theme-audit-ok: naming the
34
34
  * value that was REMOVED — and render every shadcn card
35
35
  * and popover pure white. "White" here means oklch(0.982 0.002 248)
36
- * (#f8f9fa); the lightest surface is bg-panel at L=0.994.
36
+ * theme-audit-ok: naming the equivalent hex for a reader's reference, not
37
+ * consuming it — (#f8f9fa); the lightest surface is bg-panel at L=0.994.
37
38
  *
38
39
  * OKLCH format is MANDATORY for all new tokens. No hex/rgb/hsl.
39
40
  * Syntax: oklch(L C H) — L=lightness 0–1, C=chroma 0–0.4, H=hue 0–360.
@@ -68,8 +69,9 @@
68
69
  --neon-electric-deep: oklch(0.380 0.140 270); /* secondary action */
69
70
 
70
71
  /* The two words the estate uses loosely, pinned to values. Operator ruling
71
- 2026-08-02 ("white is primary: #f8f9fa") in terms: "define --white and
72
- --black once in the token source and reference them everywhere; never
72
+ 2026-08-02 ("white is primary: #f8f9fa" theme-audit-ok: naming the
73
+ ruling's literal, not consuming it; the tokens below are oklch) in terms:
74
+ "define --white and --black once in the token source and reference them everywhere; never
73
75
  write either literal inline." This is that definition — it did not exist
74
76
  until 2026-08-13, which is why the sanctioned replacement values named in
75
77
  packages/eslint-config/rules/no-hardcoded-colours.js had no token to point
@@ -294,6 +296,43 @@
294
296
  --role-border: var(--light-border);
295
297
  --role-border-strong: oklch(0.880 0.008 250);
296
298
 
299
+ /* --role-border-interactive — TH-5 / bsuite#1958, the operator's
300
+ "unstyled button".
301
+ ------------------------------------------------------------------
302
+ WCAG 1.4.11 (non-text contrast) requires 3:1 for the visual boundary
303
+ that IDENTIFIES a user-interface component — the outline of a text
304
+ input, a select, an `outline`-variant button. It does NOT require 3:1
305
+ of a decorative hairline separating two filled surfaces, which is what
306
+ `--role-border` is for. Those are two different jobs and they had one
307
+ token, so the stricter job inherited the looser value.
308
+
309
+ MEASURED, light mode, OKLCH -> linear-sRGB -> WCAG relative luminance
310
+ (the same pipeline the text scale above is measured with, positive-
311
+ controlled by reproducing this file's own recorded dark figures of
312
+ 3.36:1 and 3.61:1 for --role-border-strong to two decimals):
313
+
314
+ --role-border vs --role-bg-panel 1.12:1 <- the filed defect
315
+ --role-border vs --role-bg-surface 1.13:1
316
+ --role-border vs --role-bg-sunken 1.01:1 <- worst live case
317
+ --role-border-strong vs --role-bg-panel 1.35:1
318
+
319
+ So re-pointing at the strongest EXISTING light token does not fix it —
320
+ 1.35:1 is still less than half the floor. This role needs a value that
321
+ does not exist in the light scale yet, and here it is.
322
+
323
+ oklch(0.56 0.015 260) is from the D2C source-of-truth document (the
324
+ palette whitelist accepts it) and clears 3:1 on every light surface
325
+ role with margin, so no call site has to know which surface it landed on:
326
+
327
+ vs --role-bg-surface 4.42:1 vs --role-bg-panel 4.39:1
328
+ vs --role-bg-input 4.18:1 vs --role-bg-body 4.15:1
329
+ vs --role-bg-sunken 3.88:1 <- worst case, still +29% over the floor
330
+
331
+ Bound to shadcn's `--input` below, which is what `border-input` on every
332
+ Input/Select/Textarea and the Button `outline` variant resolves through.
333
+ That binding is the whole point: a token nothing invokes is TH-8. */
334
+ --role-border-interactive: oklch(0.56 0.015 260);
335
+
297
336
  /* Scrim — the veil behind a modal, and the reason `bg-black/N` survived theme-audit-ok: prose
298
337
  everywhere else. It was the single most duplicated pure endpoint in the
299
338
  tree (9 sites across nav-core, page-builder and schema-builder) for one
@@ -357,7 +396,14 @@
357
396
  --destructive: var(--role-destructive);
358
397
  --destructive-foreground: var(--text-on-error);
359
398
  --border: var(--role-border);
360
- --input: var(--role-border);
399
+ /* `--input` is shadcn's INTERACTIVE boundary — `border-input` is on every
400
+ Input, Select, Textarea and the Button `outline` variant. It pointed at
401
+ --role-border, the decorative hairline, which measures 1.12:1 against the
402
+ panel: WCAG 1.4.11 wants 3:1 and the operator simply called it "an
403
+ unstyled button" (bsuite#1958). Now bound to the role built for the job.
404
+ `--border` deliberately stays on --role-border: separating two filled
405
+ surfaces is not a component boundary and does not carry the 3:1 duty. */
406
+ --input: var(--role-border-interactive);
361
407
  --ring: var(--role-primary);
362
408
 
363
409
  /* ============================================================
@@ -583,13 +629,45 @@
583
629
  light, 0.320/0.420 dark). Only the D2C theme was half-defined, which is
584
630
  why nobody reading the corporate theme would spot it.
585
631
 
586
- Value is oklch(0.52 0.02 248) from the D2C source-of-truth document, so
587
- the palette whitelist accepts it. It sits +0.09 L above `--dark-border`
588
- (0.428), mirroring the light pair's step, and measures 3.36:1 against
589
- `--role-bg-panel` and 3.61:1 against `--role-bg-sunken` clearing the
590
- WCAG 1.4.11 non-text-contrast floor of 3:1 on both dark surfaces.
591
- `--role-border` itself is 2.26:1, which is why "strong" has to exist. */
592
- --role-border-strong: oklch(0.52 0.02 248);
632
+ Value WAS oklch(0.52 0.02 248) from the D2C source-of-truth document,
633
+ recorded here as 3.36:1 against `--role-bg-panel` and 3.61:1 against
634
+ `--role-bg-sunken`. Both figures re-measured exactly on 2026-08-17
635
+ which is how the instrument for TH-5 was positive-controlled.
636
+
637
+ WHAT THAT PAIR OF FIGURES MISSED (TH-5, 2026-08-17): only two of the five
638
+ dark surface roles were ever checked. Against the third —
639
+ `--role-bg-input` oklch(0.240 0.020 260), the LIGHTEST dark surface and
640
+ the one an interactive boundary actually sits on — 0.52 measures 2.99:1.
641
+ Under the 3:1 floor, by a rounding margin, on precisely the surface the
642
+ rule exists for. Nothing was wrong with the arithmetic; the surface list
643
+ was short, so the worst case was never in it.
644
+
645
+ Raised to oklch(0.55 0.015 250) — also from the source-of-truth document,
646
+ +0.03 L, visually indistinguishable, and decisive:
647
+
648
+ vs --role-bg-body 3.98:1 vs --role-bg-surface 3.65:1
649
+ vs --role-bg-panel 3.81:1 vs --role-bg-sunken 4.09:1
650
+ vs --role-bg-input 3.40:1 <- the case that used to be 2.99:1
651
+
652
+ `--role-border` itself is 2.26:1, which is why "strong" has to exist.
653
+ The contract test now measures all five surfaces in both modes, so a
654
+ short surface list cannot hide a sub-3:1 pair again. */
655
+ --role-border-strong: oklch(0.55 0.015 250);
656
+
657
+ /* Dark half of --role-border-interactive (see the light block for the full
658
+ rationale). oklch(0.66 0.018 250), source-of-truth palette, measured
659
+ against every dark surface role:
660
+
661
+ vs --role-bg-sunken 6.38:1 vs --role-bg-body 6.20:1
662
+ vs --role-bg-panel 5.95:1 vs --role-bg-surface 5.69:1
663
+ vs --role-bg-input 5.30:1 <- worst case
664
+
665
+ Deliberately a wider margin than the light half's 3.88:1: a hairline on
666
+ navy is the case this estate has already got wrong twice (the near-white
667
+ `border-strong` above, and the 0.09-alpha `--border-shell`), and the cost
668
+ of a boundary that is slightly too visible is nothing next to one that
669
+ disappears. */
670
+ --role-border-interactive: oklch(0.66 0.018 250);
593
671
 
594
672
  /* Role text variants rebind for the dark surface — all AAA on navy. */
595
673
  --role-primary-text: oklch(0.721 0.215 262.9); /* 7.01:1 ✓ AAA */