@bsuite/theme 0.9.0 → 0.10.2

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.
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /*
3
3
  * Role-bound, not palette-bound. Until 0.7.0 these were raw Tailwind palette
4
- * classes (`bg-emerald-50`, `bg-red-50`, …), which meant the design-system
4
+ * theme-audit-ok: prose. classes (`bg-emerald-50`, `bg-red-50`, …), which meant the design-system
5
5
  * component every app imports was invisible to tenant white-labelling —
6
6
  * BrandingProvider mutates the role layer, and these bound below it.
7
7
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsuite/theme",
3
- "version": "0.9.0",
3
+ "version": "0.10.2",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -11,7 +11,7 @@
11
11
  "url": "https://github.com/GaryOcean428/bsuite.git",
12
12
  "directory": "packages/theme"
13
13
  },
14
- "description": "BSuite Universal theme D2C Neon Electric + Braden Corporate baselines. Tailwind v4 preset, CSS vars (5-layer), role aliases, colourblind-safe tokens, eye-strain-safe text scale, BrandingProvider (runtime white-label). Used by BSU, CRM7, conduit, R80.3, throughput (D2C) and braden.com.au (Corporate).",
14
+ "description": "BSuite Universal theme \u2014 D2C Neon Electric + Braden Corporate baselines. Tailwind v4 preset, CSS vars (5-layer), role aliases, colourblind-safe tokens, eye-strain-safe text scale, BrandingProvider (runtime white-label). Used by BSU, CRM7, conduit, R80.3, throughput (D2C) and braden.com.au (Corporate).",
15
15
  "files": [
16
16
  "dist",
17
17
  "src/css",
@@ -193,11 +193,23 @@
193
193
  --text-on-warning: var(--text-on-dark-fill);
194
194
  --text-on-success: var(--text-on-dark-fill);
195
195
  --text-on-info: var(--text-on-light-fill);
196
+ /* Was MISSING and preset-v4.css references it, so --color-overlay-foreground
197
+ resolved to nothing and any text on a scrim fell back to whatever it
198
+ inherited. Measured undefined in the browser on 2026-08-03. The scrim is
199
+ dark in both modes, so this is light in both. */
200
+ --text-on-overlay: var(--text-on-light-fill);
196
201
 
197
202
  /* ============================================================
198
203
  DECORATION — shadow, glow, dot and grid
199
204
  ============================================================ */
200
205
  --gradient-accent: linear-gradient(135deg, var(--braden-red), var(--braden-gold));
206
+ /* Same gradient, re-stopped for a DARK backdrop. --gradient-accent is built
207
+ for fills on a light surface: against the hero scrim (L~0.36) its red stop
208
+ measures ~1.7:1 and the text half-disappears. This keeps the red hue and
209
+ chroma exactly (17.6 / 0.17) and lifts only lightness to a value the
210
+ contract already ships, so both stops clear the scrim and G2 still passes. */
211
+ --braden-red-on-dark: oklch(0.704 0.170 17.6);
212
+ --gradient-accent-on-dark: linear-gradient(135deg, var(--braden-red-on-dark), var(--braden-gold));
201
213
  --shadow-xs: 0 1px 2px oklch(0.30 0.03 250 / 0.06);
202
214
  --shadow-sm: 0 1px 3px oklch(0.30 0.03 250 / 0.08), 0 1px 2px oklch(0.30 0.03 250 / 0.04);
203
215
  --shadow-md: 0 4px 10px -3px oklch(0.30 0.03 250 / 0.10), 0 2px 4px -2px oklch(0.30 0.03 250 / 0.06);
@@ -281,3 +293,95 @@
281
293
  --dot-color: oklch(0.72 0.060 90 / 0.16);
282
294
  --grid-color: oklch(0.82 0.020 250 / 0.055);
283
295
  }
296
+
297
+ /* ============================================================
298
+ BASE — Corporate sets the heading FACE only.
299
+ ============================================================
300
+ Not colour, not size, not weight, not transform. Reverted to this after
301
+ operator review ("well that looks worse") and a measurement that showed
302
+ why: on the live site the hero H1 picked up the ramp colour while the 25
303
+ headings below it kept their explicit `text-braden-navy`. One heading
304
+ stopped matching the rest. That is not a hierarchy.
305
+
306
+ A base rule only reaches elements that do not style themselves. In a
307
+ dense app UI that is most headings, which is the case the ramp was
308
+ measured against. On a marketing site it is almost none — 59 headings
309
+ here carry an explicit brand colour and their own size utility — so a
310
+ base ramp cannot create hierarchy there, only a mismatch on whichever
311
+ few it happens to reach.
312
+
313
+ font-family is the exception: nothing on the page overrides it, so it
314
+ lands uniformly and is exactly what a base rule is for.
315
+
316
+ D2C keeps the full ramp (see vars.css) — those apps ARE the dense-UI
317
+ case, and the ramp is verified there in a real browser by G5. */
318
+ @layer base {
319
+ h1 { font-family: var(--font-display); }
320
+ h2, h3, h4, h5, h6 { font-family: var(--font-heading); }
321
+
322
+ /* The root element's colour was the UA default — measured pure black (L=0)
323
+ in BOTH modes on 2026-08-03, i.e. the one banned value, sitting at the top
324
+ of the inheritance chain. Nothing renders text at <html> so it was
325
+ invisible to review, but every element that does not set a colour and is
326
+ not under a styled ancestor lands on it. */
327
+ html { color: var(--role-text-body); background-color: var(--role-bg-base); }
328
+
329
+ /* The contract names a family and the package shipped the token, but NOTHING
330
+ bound it to an element — so every app fell back to whatever it happened
331
+ to set itself, which is how crm7 ran Inter against a Geist contract and
332
+ the estate ran four families at once. Same defect as the heading ramp:
333
+ a token nobody applies is not a font, it is a variable. */
334
+ body { font-family: var(--font-body); }
335
+ code, kbd, samp, pre { font-family: var(--font-mono); }
336
+ }
337
+
338
+ /* ============================================================
339
+ DARK SURFACES INSIDE LIGHT MODE
340
+ ============================================================
341
+ A navy or red panel on a light page is a dark surface, and the mode-sensitive
342
+ role tokens are still resolving their LIGHT values inside it. That is not a
343
+ theoretical problem — measured on the contact section 2026-08-03:
344
+
345
+ --role-accent-text (light) on bg-braden-navy ....... 2.20:1 FAIL
346
+ --braden-gold on bg-braden-navy ..... 5.07:1 AA
347
+
348
+ and the mirror holds on a light background, where the brand gold is 2.05:1
349
+ and the light accent is 4.71:1. No single value passes on both, so this
350
+ cannot be fixed by picking a better gold — the token has to follow the
351
+ SURFACE, not just the mode. Mark a dark panel and its accent text flips. */
352
+ [data-surface='dark'] {
353
+ --role-accent-text: var(--braden-gold);
354
+ --role-text-body: var(--text-on-light-fill);
355
+ --role-text-secondary: var(--text-on-light-fill);
356
+ /* D2C's heading ramp reads this; Corporate has no colour ramp, but a shared
357
+ surface contract should behave the same in both brands. */
358
+ --heading-color: currentColor;
359
+ }
360
+
361
+ /* ============================================================
362
+ GRADIENT TEXT — the one place the accent gradient paints glyphs.
363
+ ============================================================
364
+ background-clip:text needs `color: transparent` to show through, which means
365
+ a failed gradient produces INVISIBLE TEXT rather than an ugly one. So both
366
+ stops resolve from tokens defined above in this same file, never from a
367
+ token an app might or might not have. */
368
+ @layer components {
369
+ .text-gradient-accent {
370
+ background-image: var(--gradient-accent);
371
+ -webkit-background-clip: text;
372
+ background-clip: text;
373
+ color: transparent;
374
+ }
375
+ .text-gradient-accent-on-dark {
376
+ background-image: var(--gradient-accent-on-dark);
377
+ -webkit-background-clip: text;
378
+ background-clip: text;
379
+ color: transparent;
380
+ }
381
+ /* Forced-colours mode drops background-image entirely, which would leave
382
+ transparent glyphs on screen. Hand the text back to the system palette. */
383
+ @media (forced-colors: active) {
384
+ .text-gradient-accent,
385
+ .text-gradient-accent-on-dark { background-image: none; color: CanvasText; }
386
+ }
387
+ }
package/src/css/vars.css CHANGED
@@ -482,3 +482,65 @@
482
482
 
483
483
  /* Shadcn bridge picks up rebindings automatically via var() chain */
484
484
  }
485
+
486
+ /* ============================================================
487
+ BASE — the heading ramp, APPLIED.
488
+ ============================================================
489
+ Added 2026-08-03 after operator review: "borders and heading levels
490
+ unstyled" (conduit) and "Document Management looks all black" (BSU).
491
+
492
+ Both were the same omission. --role-h1..h6 existed as tokens and NOTHING
493
+ bound them to an actual h1..h6, so every heading fell through to
494
+ --role-text-body — one flat near-black for all six levels. A ramp nobody
495
+ applies is not a ramp, it is six unused variables, and that is exactly what
496
+ an operator sees as "unstyled".
497
+
498
+ Values are the contract's, verbatim (d2c-theme-source-of-truth.html and the
499
+ Corporate equivalent). Each level shifts HUE as well as lightness, so the
500
+ hierarchy survives greyscale and both dichromacies rather than relying on
501
+ size alone.
502
+
503
+ Scoped to @layer base so any component class ON THE HEADING ITSELF wins.
504
+ That is a narrower guarantee than it looks, and the earlier version of this
505
+ comment ("it does not fight Tailwind utilities") was simply wrong.
506
+
507
+ WHAT IT DOES OVERRIDE: an INHERITED colour. Inheritance is the last resort in
508
+ the cascade, so a rule targeting `h1` beats `text-primary-foreground` sitting
509
+ on an ancestor — specificity never enters into it. Measured on braden
510
+ 2026-08-03: a hero `<h1>` inheriting a near-white foreground from its wrapper
511
+ rendered dark navy against a dark scrim, i.e. all but invisible, and it was
512
+ the ONLY heading on the page that moved. That is the signature of this bug —
513
+ one heading changing is not a small problem, it is the coloured-container
514
+ case, and coloured containers are where text goes invisible.
515
+
516
+ --heading-color is the escape hatch. Any container that colours its own text
517
+ sets it once and the whole heading subtree defers:
518
+ <div class="text-primary-foreground [--heading-color:currentColor]">
519
+ currentColor there resolves to the container's colour, so headings inherit
520
+ again instead of being repainted. */
521
+ @layer base {
522
+ /* See braden.css: the root element inherits the UA default pure black when
523
+ nothing sets it, which is the one banned value at the top of the chain. */
524
+ html { color: var(--role-text-body); background-color: var(--role-bg-base); }
525
+
526
+ /* The contract names a family and the package shipped the token, but NOTHING
527
+ bound it to an element — so every app fell back to whatever it happened
528
+ to set itself, which is how crm7 ran Inter against a Geist contract and
529
+ the estate ran four families at once. Same defect as the heading ramp:
530
+ a token nobody applies is not a font, it is a variable. */
531
+ body { font-family: var(--font-body); }
532
+ code, kbd, samp, pre { font-family: var(--font-mono); }
533
+
534
+ h1 { color: var(--heading-color, var(--role-h1)); font-size: 25px; font-weight: 700; letter-spacing: var(--track-display); }
535
+ h2 { color: var(--heading-color, var(--role-h2)); font-size: 19px; }
536
+ h3 { color: var(--heading-color, var(--role-h3)); font-size: 16px; }
537
+ h4 { color: var(--heading-color, var(--role-h4)); font-size: 14px; }
538
+ h5 { color: var(--heading-color, var(--role-h5)); font-size: 13px; }
539
+ h6 {
540
+ color: var(--heading-color, var(--role-h6));
541
+ font-size: 11px;
542
+ text-transform: uppercase;
543
+ letter-spacing: var(--track-eyebrow);
544
+ font-weight: 700;
545
+ }
546
+ }