@flyos/design-system 1.8.0 → 2.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": "@flyos/design-system",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "description": "FlyOS design system — shared components, directives, pipes, services, and models for Business App developers.",
5
5
  "keywords": [
6
6
  "flyos",
@@ -23,11 +23,11 @@
23
23
  // ADDITIVE ONLY. `tools/ds-compat` classifies re-valuing an existing theme
24
24
  // token as MAJOR (it forks the Native Federation shared singleton for every
25
25
  // remote), so nothing here may redefine a token another DS partial owns.
26
- //
26
+
27
27
  // Three tokens are deliberately ABSENT — they are the consuming app's BRAND and
28
28
  // every app already sets them in its own theme file:
29
29
  // --accent --accent-soft --focus-ring
30
- //
30
+
31
31
  // The other nine platform tokens (--danger* --warning* --purple --scrim
32
32
  // --glass-blur) USED to be absent too, on the assumption that the platform layer
33
33
  // supplies them. That assumption only holds when an app runs FEDERATED, where
@@ -36,7 +36,7 @@
36
36
  // commented out in both Circles and Thoughts — so `var(--danger)` resolved to
37
37
  // nothing and error text rendered colourless. Confirmed in a live browser, not
38
38
  // inferred: 9 of the 12 computed to the empty string on Thoughts standalone.
39
- //
39
+
40
40
  // They are now mirrored below at the SAME values `_theme-light/dark.scss` uses.
41
41
  // Mirroring (rather than picking new values) is what makes this safe under the
42
42
  // cascade: this file's `:root, html.light-theme` selector can tie with the
@@ -161,7 +161,7 @@ html.light-theme {
161
161
  // Label for a fill of `--ink` itself (the primary button). Distinct from
162
162
  // `--ink-inverse`, which is a FIXED near-white for coloured/gradient fills and
163
163
  // therefore wrong here: `--ink` flips per theme, so its label must flip too.
164
- //
164
+
165
165
  // CONTRACT — anything that redefines `--ink` MUST redefine `--on-ink`.
166
166
  // It must also be OPAQUE. The shell-embed glass remaps in Circles and Thoughts
167
167
  // set `--ink` to white and every surface token to a translucent veil; a
@@ -206,14 +206,22 @@ html.light-theme {
206
206
  --on-accent-soft: var(--ink);
207
207
 
208
208
  // 2. --accent-fill is a SOLID plate we own outright, so its ink is knowable
209
- // regardless of backdrop — always white. Badges/chips use this pair, so
210
- // their contrast cannot be broken by a host token override.
209
+ // regardless of backdrop — knowable FROM THE PLATE, not "always white".
210
+ // Badges/chips use this pair, so their contrast cannot be broken by a
211
+ // host token override.
211
212
  --accent-fill: var(--accent);
212
213
 
213
- // Aliased, not a second copy of the same white: "ink on a solid accent plate"
214
- // and "ink on a dark/gradient fill" are the same requirement, so they must
215
- // move together if the inverse ink is ever re-tuned.
216
- --on-accent-fill: var(--ink-inverse);
214
+ // FALLBACK RUNG ONLY. The accent-aware derivation at the foot of this file
215
+ // overrides this wherever relative colour syntax parses; this is what an
216
+ // older engine gets, and Nova's theme-invariant `--on-accent` (#fff) is the
217
+ // right answer for the platform violet in both themes.
218
+
219
+ // It is deliberately NOT an alias of `--ink-inverse` any more. Two tokens that
220
+ // happen to agree today are not the same requirement: "ink on a dark/gradient
221
+ // fill" is a constant, "ink on the accent plate" is a function of the accent.
222
+ // Aliasing them made the second one's contrast depend on a token that is not
223
+ // the plate. See the foot of the file for what it depends on now.
224
+ --on-accent-fill: var(--on-accent);
217
225
 
218
226
  // ── Interaction state layers ─────────────────────────────────────────────
219
227
  // Translucent veils layered OVER a surface for hover/press/selected instead
@@ -364,6 +372,7 @@ html.dark-theme {
364
372
  --ink-3: oklch(62% 0.005 250deg);
365
373
  --ink-4: oklch(45% 0.005 250deg);
366
374
  --ink-hover: oklch(85% 0.005 250deg);
375
+
367
376
  // Flips with --ink above: the primary button is a near-white pill here, so its
368
377
  // label goes near-black. Aliased to --bg (the darkest opaque surface in this
369
378
  // theme) rather than repeating its literal — see the contract on the :root decl.
@@ -425,17 +434,17 @@ html.dark-theme {
425
434
  // and each reference carries a hardcoded literal fallback, e.g.
426
435
  // `var(--label-primary, #1d1d1f)` or `var(--surface-card, #fff)`. 211 such
427
436
  // fallbacks exist across the library.
428
- //
437
+
429
438
  // Those literals are theme-BLIND: a single constant picked by whoever wrote the
430
439
  // component. Federated that never shows, because the desktop shell defines the
431
440
  // whole chrome family. Standalone, no one does — so a business app got whatever
432
441
  // constant each component happened to hardcode, in BOTH light and dark.
433
- //
442
+
434
443
  // Aliasing the chrome names onto the app-surface family fixes all 211 sites at
435
444
  // once, without touching a single component: the var() resolves through the
436
445
  // token and never reaches its literal. The targets are theme-aware, so these
437
446
  // follow light/dark automatically.
438
- //
447
+
439
448
  // SELECTOR IS LOAD-BEARING: this block is `:root` ALONE — deliberately not
440
449
  // `:root, html.light-theme` like the blocks above. The shell declares every one
441
450
  // of these under `html.light-theme` / `html.dark-theme` (specificity 0,1,1),
@@ -465,3 +474,108 @@ html.dark-theme {
465
474
  --surface-hover: var(--bg-hover);
466
475
  --surface-active: var(--state-selected);
467
476
  }
477
+
478
+ // ── Plate ink, derived from the plate (UX v2, S7.1a) ─────────────────────────
479
+ // `--on-accent-fill` labels a SOLID `--accent-fill` plate: `fly-detail-shell`'s
480
+ // active tab, `badge()`'s default, `fly-pagination`'s current-page pill.
481
+
482
+ // THE FAILURE IS THE EXTERNAL-APP ACCENT, NOT THE EMBED. A fixed near-white is
483
+ // right for the platform violet (5.25:1) and for the 2.0 `--accent-hi`
484
+ // (8.33:1), and catastrophic for an External App that remaps `--accent` to a
485
+ // light brand colour: white on a pale gold #F5C542 measures 1.58:1, on a cyan
486
+ // #22D3EE 1.76:1. Three tokens are deliberately absent from this file precisely
487
+ // because the accent is the consuming app's to choose (see the header), so this
488
+ // is not a hypothetical — it is what the seam is for. The label has to follow
489
+ // the plate's LUMINANCE, which means the plate is what it is computed from.
490
+
491
+ // WHAT S1.11 RECORDED HERE WAS WRONG, and the correction is the reason this is
492
+ // a derivation rather than the one-line repoint that note proposed. The claim
493
+ // was that `app-surface-on-glass` pinning `--ink-inverse` to a dark literal
494
+ // dragged `--on-accent-fill` (then an alias of it) down to ~2.5:1 on #9333EA.
495
+ // It never did. A custom property is substituted at the element that DECLARES
496
+ // it: `--on-accent-fill: var(--ink-inverse)` resolves at `:root`, and the
497
+ // resolved value is what inherits. Every shipped `app-surface-on-glass` call
498
+ // site is a component host far below `:root`, so it re-values `--ink-inverse`
499
+ // for its own subtree and cannot reach an `--on-accent-fill` that was computed
500
+ // above it. Measured in Chromium against the real compiled mixin: inside the
501
+ // mixin `--ink-inverse` is `oklch(18% 0.005 250deg)` while `--on-accent-fill`
502
+ // is still `oklch(99% 0 0deg)`, rendering 5.25:1 — identical to outside it.
503
+ // The same rule is why there is no generic `--on-plate` token; see below.
504
+
505
+ // WHY A LIVE `var()` CHAIN AND NOT A BUILD-TIME COMPUTATION. Sass cannot see
506
+ // the input. `--accent` is a runtime knob: `FlyThemeService.applyAccent` writes
507
+ // it as an INLINE style on `<html>` from tenant/user branding, and
508
+ // `isAccentColor` admits anything `CSS.supports('color', …)` does. Any value
509
+ // resolved at compile time is answering a question about a colour that does not
510
+ // exist yet. Same reason `--shadow-accent` is a `color-mix` and not a hex.
511
+
512
+ // MECHANISMS REJECTED.
513
+ // · `contrast-color(var(--accent-fill))` is CSS Color 5 and is literally this
514
+ // feature, but it is not yet carried widely enough to be the only rung — and
515
+ // it splits black/white on the same WCAG luminance the expression below
516
+ // computes, so it would buy a dependency rather than an answer.
517
+ // · `light-dark()` pairs on the THEME. An accent's lightness is independent of
518
+ // the theme (an app may set a pale accent in dark mode), so it answers a
519
+ // different question and fails on exactly the case that motivates this.
520
+ // · `color-mix(… var(--accent) N%, white)` scales lightness monotonically; it
521
+ // cannot invert polarity, so it cannot cross the light/dark accent boundary.
522
+ // · "External Apps override the token" moves the defect into every integrator,
523
+ // silently — nothing can gate an app that forgets. The token stays
524
+ // overridable as an escape hatch; it is not the contract.
525
+
526
+ // THE EXPRESSION. Relative colour syntax in `srgb-linear` exposes `r`/`g`/`b`
527
+ // as exactly the linearised channels WCAG's relative-luminance formula takes,
528
+ // so `0.2126r + 0.7152g + 0.0722b` IS that luminance — no approximation. An
529
+ // oklch-`l` split is the tempting one-channel version and it is not safe: `l`
530
+ // and WCAG luminance disagree by up to ~0.04 on saturated hues, which near the
531
+ // boundary picks the wrong pole and ships a ~3.7:1 label. `(0.1791 - Y) *
532
+ // infinity` clamped to 0..1 is a step function: 1 (white) below the threshold,
533
+ // 0 (black) above. 0.1791 is where white and black contrast a background
534
+ // EQUALLY, so the worst case over the entire sRGB cube is 4.58:1 — the AA floor
535
+ // here is arithmetic, not a review. The explicit `/ 100%` is load-bearing:
536
+ // relative colour syntax inherits the ORIGIN's alpha when the slot is omitted,
537
+ // and this file's header requires a counter-ink to be OPAQUE or it composites
538
+ // into its own fill. A tenant accent may legitimately carry one — `isAccentColor`
539
+ // admits any value `CSS.supports('color', …)` accepts.
540
+
541
+ // TWO RUNGS. Relative colour syntax is Baseline since Firefox 128 (2024-07);
542
+ // anything older keeps the `--on-accent` white declared above, i.e. today's
543
+ // platform-correct behaviour. `@supports` is what makes that a fallback at all:
544
+ // a custom property accepts almost any token sequence at PARSE time, so the
545
+ // usual declare-it-twice ladder does not work here — the later declaration
546
+ // always wins and the failure surfaces at the consumer as an unset `color`.
547
+ // The `infinity` keyword needs no rung of its own; every engine with relative
548
+ // colour syntax predates its own support for it.
549
+
550
+ // THERE IS NO GENERIC `--on-plate` TOKEN, and the substitution rule above is
551
+ // why. A plate that is not the accent — `-board`'s presence chips carry a
552
+ // per-user colour from `presenceColorFor`, bound inline per element — cannot be
553
+ // served by a `:root` token, because a `:root` declaration substitutes `:root`'s
554
+ // input, not the chip's. Tried and measured: with an `--on-plate` token declared
555
+ // here and its input set on the chip, every chip resolved to the `:root`
556
+ // fallback and nothing changed. A plate the DS cannot know has to be derived in the
557
+ // CONSUMING declaration, on the element that paints it, which is what
558
+ // `canvas-diagram` / `canvas-fishbone` now do. `$_plate-ink` is the one
559
+ // statement of the formula; `plate-ink.spec.ts` holds those copies to it, and
560
+ // evaluates this one at both accent poles rather than pattern-matching it.
561
+
562
+ // Both theme classes are listed rather than leaning on `:root` alone: a future
563
+ // redeclaration of `--on-accent-fill` inside `html.dark-theme` (0,1,1) would
564
+ // otherwise silently outrank a bare `:root` (0,1,0) override.
565
+
566
+ // A Sass-private string, interpolated once per channel: all three take the SAME
567
+ // switch, and written inline Sass would try to evaluate `0.2126 * r` as
568
+ // arithmetic and fail. So this is both the escape hatch and the single place
569
+ // the formula exists — it cannot drift between the channels or the tokens.
570
+ $_plate-ink: 'clamp(0, (0.1791 - (0.2126 * r + 0.7152 * g + 0.0722 * b)) * infinity, 1)';
571
+
572
+ // The probe colour is `rgb(0 0 0)`, not a hex, so the identical prelude can be
573
+ // copied verbatim into `design-system-board` — whose own gate bans bare colour
574
+ // literals anywhere in a component stylesheet, prelude and comments included.
575
+ @supports (color: color(from rgb(0 0 0) srgb-linear r g b)) {
576
+ :root,
577
+ html.light-theme,
578
+ html.dark-theme {
579
+ --on-accent-fill: color(from var(--accent-fill) srgb-linear #{$_plate-ink} #{$_plate-ink} #{$_plate-ink} / 100%);
580
+ }
581
+ }
@@ -1,5 +1,5 @@
1
1
  // ─── Inherited-ink baseline ──────────────────────────────────────────────────
2
- //
2
+
3
3
  // Establishes the document's inherited `color`. Until this existed, NOTHING in
4
4
  // the shell ever set `color` on `html` or `body` — `_theme-light.scss` and
5
5
  // `_theme-dark.scss` declare custom properties only. So the inherited colour was
@@ -7,18 +7,18 @@
7
7
  // `color: inherit` (fly-tree-nav rows, fly-breadcrumb's current crumb,
8
8
  // fly-search-input's typed text, fly-card's meta slot, and any consumer markup
9
9
  // that simply doesn't mention colour) rendered black ink on the smoked glass.
10
- //
10
+
11
11
  // That is why the "text is invisible in light theme" bug kept coming back app
12
12
  // after app: it was never really an app's mistake. There was no baseline to
13
13
  // inherit, so *forgetting* to set a colour was the failure mode, and the fix
14
14
  // kept being applied one component at a time. `--text-color` is white in BOTH
15
15
  // themes since the vibrancy flip, so one declaration settles it everywhere.
16
- //
16
+
17
17
  // Scoped to the two theme classes on purpose — NOT `:root`. A business app
18
18
  // running STANDALONE renders on its own opaque paper (`--bg`, near-black
19
19
  // `--ink`) and does not load `fly-theme` at all; keying on the class means a
20
20
  // surface that never opted into a shell theme is never repainted white.
21
- //
21
+
22
22
  // A deliberate paper surface inside the shell (the documents editor) still
23
23
  // brings its own dark ink locally — a component rule beats this baseline, which
24
24
  // is exactly the intended precedence.
@@ -134,12 +134,20 @@
134
134
  // `.workflow/plans/ux-refresh/notes/S1-review-fixes-surfaces.md`.
135
135
  @return $glow, $sheen, linear-gradient(180deg, var(--mat-menu-a), var(--mat-menu-b));
136
136
  } @else if $surface == agent {
137
- // The agent aside keeps its own lighter fill (skill §2 ⚠). No token exists
138
- // for this pair; it is a ratified literal, allow-listed in the spec.
139
- @return $glow, $sheen, linear-gradient(rgb(56 54 60 / 52%), rgb(56 54 60 / 65%));
137
+ // The agent aside's own fill the panel hue on a slightly wider alpha ramp,
138
+ // top-to-bottom. Same two-stop shape as `menu`, for the same reason: the pair
139
+ // IS a gradient.
140
+
141
+ // This used to be the literal `rgb(56 54 60 / 52%) → 65%`, described as "a
142
+ // ratified literal, allow-listed in the spec". That colour is byte-identical to
143
+ // the DARK `--mat-panel`, so the aside painted the dark panel in BOTH themes and
144
+ // was the one surface here that could not flip — while `panel` and `menu` right
145
+ // above it both resolved through theme-aware tokens. The dark values are carried
146
+ // over unchanged, so this is a light-theme fix with no dark-theme delta.
147
+ @return $glow, $sheen, linear-gradient(180deg, var(--mat-agent-a), var(--mat-agent-b));
140
148
  }
141
149
 
142
- @error 'nova-glass: unknown surface `#{$surface}`. Declare one of `chrome` (ratified --glass2-bg fill; the canon default, but unreadable under a light-theme FLOATING menu — see the note on the branch), `menu` (--mat-menu-a/-b, the design\'s menu material, for menus/listboxes/action menus), `panel` (--mat-panel fill, for a surface inside the content panel), `agent` (the agent aside\'s lighter fill).';
150
+ @error 'nova-glass: unknown surface `#{$surface}`. Declare one of `chrome` (ratified --glass2-bg fill; the canon default, but unreadable under a light-theme FLOATING menu — see the note on the branch), `menu` (--mat-menu-a/-b, the design\'s menu material, for menus/listboxes/action menus), `panel` (--mat-panel fill, for a surface inside the content panel), `agent` (--mat-agent-a/-b, the agent aside\'s own fill).';
143
151
  }
144
152
 
145
153
  // ── The degradation ladder (skill §4 — non-negotiable) ───────────────────────
@@ -291,12 +299,30 @@
291
299
  @include _degrade;
292
300
  }
293
301
 
294
- // ── Light "vibrancy" material — OPT-IN, shell chrome only ────────────────
295
- // The light-theme refractive layer (skill §4): a 1px gradient rim ring
296
- // (`::before`, mask-composite exclude) and a drifting specular sheen (`::after`,
297
- // `glassSheen` 22s, opacity .5) over a white-gradient fill.
298
- // This is NOT the default glass, and it must be hard to reach by accident. Two
299
- // locks, both mechanical:
302
+ // ── Light "vibrancy" material — RETIRED IN PLACE, no production callers ──────
303
+ // DO NOT ADOPT THIS. S6.-1 reversed the ruling that motivated it: the light
304
+ // theme is conventional dark-ink-on-pale-plate, and the white-ink-on-smoke
305
+ // vibrancy model is gone. `window/_nova-vibrancy.scss` which this docblock
306
+ // used to name as the live precedent for how to include it WAS DELETED, along
307
+ // with its `styles.scss` include. The only remaining callers anywhere are the
308
+ // design-lab `glass-recipes-panel` exhibit and this file's own spec.
309
+ // ---
310
+ // It is kept rather than deleted at 2.0.0 because removing it is not the
311
+ // one-line edit it looks like, and none of what it costs is worth buying here:
312
+ // · `glassSheen` is a RATIFIED motion-canon keyframe (`nova-motion.spec.ts`)
313
+ // with no other consumer, so deleting the material either strands a canon
314
+ // animation or drags a second ratified removal along with it.
315
+ // · The design lab's reduced-motion simulation uses this material as its only
316
+ // vehicle — the sheen is the one thing on any recipe that reduced-motion has
317
+ // to switch off — so it would need re-vehicling, not just deleting.
318
+ // · ds-compat does NOT extract Sass mixins (it covers TS exports, custom
319
+ // properties, `@layer` names, peer floors and, since S6.1c, CSS classes), so
320
+ // removing a published mixin is a SILENT break with no gate — the same blind
321
+ // spot S6.1c closed for classes, still open for this surface.
322
+ // Leaving it costs nothing behavioural: it is unreachable by accident (below),
323
+ // and no shipped stylesheet includes it.
324
+ // ---
325
+ // It must stay hard to reach by accident. Two locks, both mechanical:
300
326
  // · `$surface` has NO DEFAULT and is checked against a closed six-name list of
301
327
  // REAL shell surfaces. `@include glass.material()` is a compile error, and
302
328
  // there is no honest value for a feature-app component to pass.
@@ -306,9 +332,7 @@
306
332
  // Including this from a component stylesheet is a review-blocker: under
307
333
  // emulated encapsulation `html.light-theme &` is dead (skill §9 rule 1), so
308
334
  // the material would apply in DARK theme too and paint white gradients over
309
- // dark chrome. The shell already sets this precedent — `window/
310
- // _nova-vibrancy.scss` is included once from `styles.scss` under
311
- // `html.light-theme`, never from a component.
335
+ // dark chrome.
312
336
  // The variant follows from the surface; a caller cannot mismatch them. The four
313
337
  // `regular`/`chrome` names are the design's four `[data-material]` surfaces; the
314
338
  // two `thin` names are the light thin controls §4 lists alongside them.
@@ -61,6 +61,32 @@ html.light-theme {
61
61
  // `--sys-red-txt` exists because `--sys-red` fails AA as dark-theme text
62
62
  // (4.16:1). No `-txt` variant exists yet for blue/orange/teal; those fixes
63
63
  // land centrally at S7.1, never improvised per component.
64
+ // ---
65
+ // S6.1w re-measured the family after making dark's chrome plate adaptive, to
66
+ // decide whether BLUE needed a `-txt` variant. It does not, for two separate
67
+ // reasons — the second is the load-bearing one.
68
+ // ---
69
+ // 1. Blue is never TEXT. Its only path to the DOM is `nova-priority-tone
70
+ // ('normal')` -> `.status-dot--normal`, a filled circle painted with
71
+ // `background: currentcolor` — a non-text graphical object (WCAG 1.4.11,
72
+ // 3:1), not body copy. The hues that ARE painted as text (red, green,
73
+ // orange, and `--sys-red-txt`) live on the alpha-1 dark `--mat-menu-*`
74
+ // (action-menu, magic-actions) and the opaque `--surface-overlay`, where
75
+ // nothing varies with the wallpaper: red 4.80 / 4.82, green 8.96 / 8.99.
76
+ // 2. A `-txt` variant is not the mechanism anyway. Where these colours DO
77
+ // measure badly is on TRANSLUCENT plates over a bright wallpaper —
78
+ // `--glass-bg-elevated` at 82% gives red 2.83, blue 2.64, purple 3.40 over
79
+ // the brightest backdrop — and that is a PLATE defect of exactly the kind
80
+ // S6.1w fixed for the chrome, not a palette one. The proof: the EXISTING
81
+ // `--sys-red-txt` measures 3.42 on that same plate and fails too. Clearing
82
+ // 4.5 there needs an ink of luminance >= 0.44 — near-pastel, no longer
83
+ // reading as the system colour, and washed out over the 22-of-26 dark
84
+ // wallpapers where the plain token already sits at 4.74–5.07.
85
+ // ---
86
+ // So the real fix is to bring `--glass-bg-elevated` (`_theme-dark.scss`, NOT
87
+ // this file) into the adaptive set. Minting `--sys-blue-txt` would spend a
88
+ // token to improve one number while every other hue on that plate still
89
+ // failed — the "improvised per component" move this comment already forbids.
64
90
  --sys-red: #C2263A;
65
91
  --sys-red-txt: #B32436;
66
92
  --sys-orange: #9A5A0F;
@@ -158,6 +184,25 @@ html.light-theme {
158
184
  --mat-menu-a: rgb(238 242 247 / calc(var(--mat-menu-a-alpha) * 100%));
159
185
  --mat-menu-b-alpha: 0.64;
160
186
  --mat-menu-b: rgb(222 229 238 / calc(var(--mat-menu-b-alpha) * 100%));
187
+
188
+ // The agent aside — the PANEL hue on its own alpha ramp, top-to-bottom. It was a
189
+ // hardcoded literal inside `_nova-glass.scss`'s `agent` branch, and that literal
190
+ // was `rgb(56 54 60)` — byte-identical to the DARK `--mat-panel`, so the aside
191
+ // painted the dark panel colour in both themes and could never flip. Every sibling
192
+ // branch (`panel`, `menu`) already routed through a theme-aware token; this pair is
193
+ // that same shape, so the aside stops being the one surface outside the system.
194
+ // 0.52 -> 0.60 (S6.-1 fan-in ruling). The aside is painted straight against the
195
+ // WALLPAPER, exactly like `--window-bg`, and 22 of the 26 shipped wallpapers are
196
+ // dark: at 0.52 the top stop composited to 4.06:1 for body ink over the darkest
197
+ // one — sub-AA, and measured as such by `theme-contrast.spec`. The window plate
198
+ // solves this with the adaptive `--fly-glass-alpha` knob; this pair is not in that
199
+ // set, so it takes the static equivalent instead. 0.60 is not arbitrary: it leaves
200
+ // 40% transmission, the same as the window plate's ratified floor, so the aside
201
+ // and the window beside it now let the wallpaper through by the same amount.
202
+ --mat-agent-a-alpha: 0.6;
203
+ --mat-agent-a: rgb(230 236 243 / calc(var(--mat-agent-a-alpha) * 100%));
204
+ --mat-agent-b-alpha: 0.65;
205
+ --mat-agent-b: rgb(230 236 243 / calc(var(--mat-agent-b-alpha) * 100%));
161
206
  --mat-tip-a-alpha: 0.8;
162
207
  --mat-tip-a: rgb(238 241 245 / calc(var(--mat-tip-a-alpha) * 100%));
163
208
  --mat-tip-b-alpha: 0.86;
@@ -274,6 +319,22 @@ html.dark-theme {
274
319
  --mat-menu-a: rgb(32 31 37 / calc(var(--mat-menu-a-alpha) * 100%));
275
320
  --mat-menu-b-alpha: 1;
276
321
  --mat-menu-b: rgb(26 25 30 / calc(var(--mat-menu-b-alpha) * 100%));
322
+
323
+ // Re-valued by S6W (agent-panel sweep). The carried-over literal was
324
+ // rgb(56 54 60) at 0.52→0.65 — warm-hued AND translucent, so over a warm
325
+ // wallpaper the aside composited into muddy brown while the account-menu
326
+ // popover beside it stayed crisp on the alpha-1 `--mat-menu-*` pair. These
327
+ // are now that same menu pair's stops at alpha 1: crisp, wallpaper-immune,
328
+ // and cool-neutral like every other dark floating surface.
329
+ // Alternatives weighed (final call is perceptual, at fan-in):
330
+ // · alpha→1 on the old hue rgb(56 54 60) — crisp but noticeably lighter
331
+ // and warmer than the menu material next to it;
332
+ // · ~0.85 alpha on either hue — still lets a warm wallpaper tint the
333
+ // plate, i.e. a smaller dose of the same defect.
334
+ --mat-agent-a-alpha: 1;
335
+ --mat-agent-a: rgb(32 31 37 / calc(var(--mat-agent-a-alpha) * 100%));
336
+ --mat-agent-b-alpha: 1;
337
+ --mat-agent-b: rgb(26 25 30 / calc(var(--mat-agent-b-alpha) * 100%));
277
338
  --mat-tip-a-alpha: 0.82;
278
339
  --mat-tip-a: rgb(46 44 52 / calc(var(--mat-tip-a-alpha) * 100%));
279
340
  --mat-tip-b-alpha: 0.86;
@@ -1,31 +1,31 @@
1
1
  // ─── Shell-embed bridge — app-surface tokens re-tinted onto shell glass ──────
2
2
  // MIXIN ONLY. This partial emits no CSS on import; it exists so the mapping
3
3
  // below lives in ONE place instead of being retyped per feature app.
4
- //
4
+
5
5
  // WHY IT IS NEEDED
6
6
  // The DS carries two token families on purpose (see `_app-surface-tokens.scss`'s
7
7
  // header). `--ink-*`/`--bg-*`/`--line-*` dress BUSINESS-APP CONTENT: an opaque
8
8
  // reading surface, dark ink on white in light mode. `--label-*`/`--surface-*`
9
9
  // dress SHELL CHROME: white ink on translucent glass over the user's wallpaper.
10
- //
10
+
11
11
  // A desktop-shell feature app renders inside a glass window, so its chrome is
12
12
  // necessarily the second family — but the moment it mounts a component from the
13
13
  // `fly-*` app-surface kit (or `@flyos/design-system-board`), that component
14
14
  // themes itself from the FIRST family and paints an opaque white slab with
15
15
  // near-black ink. One region, two families: the app's own header ink is white,
16
16
  // the slab beneath it is white, and the header goes invisible.
17
- //
17
+
18
18
  // That is not hypothetical. It shipped in canvas-boards and was measured live:
19
19
  // `.cbe__export-btn` computed `color: rgb(255,255,255)` sitting on a board whose
20
20
  // `--_surface` resolved to `oklch(100% 0 0deg)`. `core/theme/feature-app-surface-tokens.ts`
21
21
  // documents the same class of trap ("a dark-mode-only review passes while light
22
22
  // mode renders a white slab") — and note the failure is LIGHT-mode-only, because
23
23
  // in dark mode both families happen to agree on light-ink-on-dark.
24
- //
24
+
25
25
  // THE FIX is the seam the tokens file already names: an app that embeds in the
26
26
  // shell re-tints the content family onto glass. help-center (`.hc-shell`) and
27
27
  // dashboard-app hand-rolled identical copies of this map before it lived here.
28
- //
28
+
29
29
  // ALIASES ONLY — every value is another token, never a literal, so the whole map
30
30
  // follows the shell's theme automatically. The ONE exception is `--on-ink`, and
31
31
  // its reason is written at the declaration.
@@ -57,7 +57,7 @@
57
57
  // both. It cannot alias a shell token: the shell is glass and owns no
58
58
  // dark-opaque ink. This literal is the same value `--ink` carries in the
59
59
  // app-surface light theme, i.e. the colour a white pill is designed to label.
60
- //
60
+
61
61
  // `app-surface-conventions.spec.ts` can only police the DS token file itself;
62
62
  // its doc says consumers that remap `--ink` "carry the same obligation".
63
63
  // Routing every consumer through this mixin is how that obligation is met.
@@ -81,33 +81,40 @@
81
81
  --on-ink: oklch(18% 0.005 250deg);
82
82
 
83
83
  // `--ink-inverse` carries the SAME obligation as `--on-ink` and was missed.
84
- //
84
+
85
85
  // The app-surface family defines it once, as "text on dark / gradient fills" — near-white, which
86
86
  // is correct while `--ink` is near-black. This mixin flips `--ink` to the shell's WHITE label ink
87
87
  // and updated `--on-ink` accordingly, but left `--ink-inverse` at its near-white resting value.
88
88
  // Every `background: var(--ink); color: var(--ink-inverse)` pair inside an embedded app therefore
89
89
  // became WHITE ON WHITE at a contrast ratio of about 1:1.
90
- //
90
+
91
91
  // That is not hypothetical: it is what made `[data-tooltip]` (the CSS tooltip behind every
92
92
  // `fly-icon-button`'s `tooltipKey`) invisible across the board editor — measured live at
93
93
  // background rgb(255,255,255) with colour oklch(0.99 0 0). The same pair is used by the button,
94
94
  // icon-button, checkbox and card components, so this was never a Boards-only bug.
95
- //
95
+
96
96
  // Pinned to `--on-ink` rather than restated, so the two counter-inks cannot drift apart again.
97
97
 
98
- // KNOWN COLLATERAL, recorded not fixed (S1.11 the fix is a token re-value, and DS 1.x is
99
- // additive-only). `_app-surface-tokens.scss` aliases `--on-accent-fill: var(--ink-inverse)`,
100
- // so this pin also re-points the label on a SOLID ACCENT plate from near-white to near-black
101
- // wherever the mixin applies. That contradicts the guarantee its header gives "--accent-fill
102
- // is a SOLID plate we own outright, so its ink is knowable regardless of backdrop — always
103
- // white their contrast cannot be broken by a host token override" and this mixin is such
104
- // an override. Live example inside the DS itself: `detail-shell.component.scss` paints
105
- // `.ds__tab--active` as `background: var(--accent-fill); color: var(--on-accent-fill)`, which
106
- // under the bridge is oklch(18%) ink on `#9333EA`, roughly 2.5:1. Also reached by
107
- // `_mixins.scss`'s `badge()` default and `-board`'s canvas-diagram / canvas-fishbone.
108
-
109
- // The 2.0 fix is one line in `_app-surface-tokens.scss` `--on-accent-fill: var(--on-accent)`,
110
- // Nova's theme-invariant `#fff` which also makes the pair immune to any future host remap of
111
- // `--ink-inverse`, rather than immune only to the remaps that exist today.
98
+ // THE "KNOWN COLLATERAL" THIS NOTE USED TO RECORD DOES NOT EXIST (corrected at 2.0.0). S1.11
99
+ // claimed that because `_app-surface-tokens.scss` aliased `--on-accent-fill: var(--ink-inverse)`,
100
+ // the pin below also dragged the label on a SOLID ACCENT plate to near-black `.ds__tab--active`
101
+ // cited at roughly 2.5:1 on `#9333EA`. It never could. A custom property is substituted at the
102
+ // element that DECLARES it: that alias resolved at `:root`, against `:root`'s `--ink-inverse`,
103
+ // and it is the RESOLVED value that inherits. Re-valuing `--ink-inverse` for a subtree cannot
104
+ // reach a property already computed above it.
105
+
106
+ // That is a structural fact, not a browser quirk, and it holds for every shipped call site
107
+ // because all of them are component-level `.hc-shell`, `.fly-app-surface-on-glass` on a
108
+ // `<div>`, and the strategies / dashboard / canvas-boards feature hosts. None is `:root` or
109
+ // `html`. Measured in Chromium inside the real compiled mixin: `--ink-inverse` is
110
+ // `oklch(18% 0.005 250deg)` there while `--on-accent-fill` is still `oklch(99% 0 0deg)`,
111
+ // rendering 5.25:1 identical to outside it. Applying this mixin AT `:root` would break that
112
+ // reasoning, which is one more reason it belongs on an embedded app's own subtree.
113
+
114
+ // The token was still wrong, for an unrelated reason S1.11 half-saw: a FIXED ink on a plate the
115
+ // consuming app may remap. `--on-accent-fill` is now derived from `--accent-fill`'s own
116
+ // luminance (see the foot of `_app-surface-tokens.scss`), so it is immune both to a host
117
+ // re-value of `--ink-inverse` and to an External App choosing a pale accent — white on
118
+ // `#F5C542` measured 1.58:1 before that change.
112
119
  --ink-inverse: var(--on-ink);
113
120
  }
@@ -64,6 +64,15 @@ html.dark-theme {
64
64
  // fully legible — popovers, menus, tooltips. Unlike the translucent glass
65
65
  // --surface-card (which reads whatever is behind it), this is solid.
66
66
  --surface-overlay: #1f1f24;
67
+
68
+ // A surface nested INSIDE another overlay — a listbox in a drawer, an emoji grid
69
+ // in a popover. `window/_nova-vibrancy.scss` invented this token for the light
70
+ // vibrancy theme (where stacking two translucent glasses reads as see-through)
71
+ // but never defined it here, so every consumer painting it had NO background in
72
+ // dark theme. It must resolve in every theme or the contract "opt in by painting
73
+ // this instead of `--surface-overlay`" is a trap. Dark's overlays are already
74
+ // opaque, so this is simply one step lifted, to separate the nested plate.
75
+ --surface-overlay-strong: #26262c;
67
76
  --surface-border: rgb(255 255 255 / 14%);
68
77
  --surface-hover: rgb(255 255 255 / 8%);
69
78
 
@@ -187,6 +196,7 @@ html.dark-theme {
187
196
  --surface-section: #161618;
188
197
  --surface-card: #1c1c1f;
189
198
  --surface-overlay: #1c1c1f;
199
+ --surface-overlay-strong: #232327;
190
200
  --glass-bg: #161618;
191
201
  --glass-bg-elevated: #1e1e22;
192
202
  --glass-blur: 0px;
@@ -70,17 +70,32 @@ html.light-theme {
70
70
  // theme's dark secondary ink disappeared into it.
71
71
 
72
72
  // They now carry real opacity, in line with the dark theme's long-standing 72%/55%. The
73
- // window still reads as glass — 72% white lets the wallpaper tint through and the 50px
74
- // backdrop blur still does its work — but text no longer sits on a bare photo.
73
+ // window still reads as glass — 60% white lets the wallpaper tint through and the backdrop
74
+ // blur still does its work — but text no longer sits on a bare photo.
75
75
  // Contract pinned by `src/app/core/theme/theme-contrast.spec.ts`.
76
76
  // `--surface-card` / `--surface-section` are a LIFT on top of the window plate, not a second
77
77
  // full substrate — stacking two thick layers is what reads as frosted plastic.
78
78
  --surface-section: rgb(255 255 255 / 35%);
79
79
  --surface-card: rgb(255 255 255 / 20%);
80
80
 
81
+ // The substrate plate alpha — ONE knob, three consumers (`--window-bg`, `--glass-bg`,
82
+ // `--dock-bg`). 0.6 is the ratified static plate; `WallpaperGlassService` overrides it at
83
+ // runtime with the thinnest alpha in [0.6, 0.7] that keeps the WEAKEST substrate body ink
84
+ // (`--text-color-secondary`, 78%) at AA over the ACTIVE wallpaper. Over bright and mid rooms
85
+ // that is 0.6 itself; over a starfield-dark room it steps to 0.62 (0.6 measures 4.51:1 there —
86
+ // above the 4.5 line but with no margin at all, which is why the knob exists). The service and
87
+ // `theme-contrast.ts` reconstruct the same scan; their specs fail if the two ever disagree.
88
+ --fly-glass-alpha: 0.6;
89
+
81
90
  // Opaque surface for floating overlays (popovers, menus, tooltips) that must
82
91
  // stay fully legible above content — see the dark theme for the rationale.
83
92
  --surface-overlay: #fff;
93
+
94
+ // Nested-overlay surface — see the dark theme for why this must exist in every
95
+ // theme. It equals `--surface-overlay` here on purpose: the problem it solves is
96
+ // two TRANSLUCENT glasses stacking into transparency, and this theme's overlays
97
+ // are already opaque white, where separation is the border's job, not the fill's.
98
+ --surface-overlay-strong: #fff;
84
99
  --surface-border: rgb(0 0 0 / 6%);
85
100
  --surface-hover: rgb(0 0 0 / 4%);
86
101
 
@@ -94,11 +109,14 @@ html.light-theme {
94
109
  // and for opposite reasons. Both now follow the `--glass-ink-elevated` model below.
95
110
  --text-color: rgb(17 24 39 / 92%);
96
111
  --text-color-secondary: rgb(17 24 39 / 78%);
97
- --glass-bg: rgb(255 255 255 / 60%);
112
+
113
+ // Composes the adaptive knob above over a WHITE base — the same two-part shape as the
114
+ // `--mat-*` materials, so the service adapts a NUMBER and never rewrites a colour string.
115
+ --glass-bg: rgb(255 255 255 / calc(var(--fly-glass-alpha) * 100%));
98
116
  --glass-bg-elevated: rgb(255 255 255 / 88%);
99
117
 
100
118
  // Tooltip surface. OPAQUE and deliberately NOT a glass token.
101
- //
119
+
102
120
  // A tooltip is body-parented and floats over whatever happens to be underneath — a light app
103
121
  // surface, a dark board canvas, the wallpaper. The glass family is TRANSLUCENT and, at runtime,
104
122
  // wallpaper-adaptive: the shell recomputes `--glass-bg-elevated` from the wallpaper and writes it
@@ -106,7 +124,7 @@ html.light-theme {
106
124
  // is in `light-theme`. Composited over a light page that resolves to roughly rgb(143,141,140),
107
125
  // and the matching white ink lands at about 2.8:1 — the "tooltips are white on white" report.
108
126
  // An overlay that can appear anywhere cannot inherit a surface tuned for one backdrop.
109
- //
127
+
110
128
  // Dark plate + white ink in BOTH themes is the classic tooltip treatment (macOS, Material,
111
129
  // Bootstrap) and is exactly what this directive's own comment always claimed it fell back to —
112
130
  // it just never actually did, because the `var()`s carried no fallback.
@@ -149,8 +167,9 @@ html.light-theme {
149
167
  // Window chrome — light frosted glass similar to Figma spec (soft white plate,
150
168
  // strong background blur, subtle luminous border).
151
169
  // The app window plate. At 6% this was the single biggest cause of the reported failure:
152
- // every app's body text was effectively painted on the wallpaper. Raised to match `--glass-bg`.
153
- --window-bg: rgb(255 255 255 / 60%);
170
+ // every app's body text was effectively painted on the wallpaper. Matches `--glass-bg`,
171
+ // including its adaptive-knob composition.
172
+ --window-bg: rgb(255 255 255 / calc(var(--fly-glass-alpha) * 100%));
154
173
  --window-radius: 24px;
155
174
  --window-blur: 24px;
156
175
  --window-backdrop-saturate: 175%;
@@ -166,7 +185,8 @@ html.light-theme {
166
185
 
167
186
  // Backs BOTH the dock and the top Finder bar (`.topbar-glass`), which is why it needs a real
168
187
  // substrate: at 15% the bar's labels and icons sat on the wallpaper like everything else.
169
- --dock-bg: rgb(255 255 255 / 60%);
188
+ // Same plate as the window, same adaptive knob.
189
+ --dock-bg: rgb(255 255 255 / calc(var(--fly-glass-alpha) * 100%));
170
190
  --dock-border: rgb(17 24 39 / 12%);
171
191
  --dock-blur:50px;
172
192
  --menubar-bg: rgb(255 255 255 / 60%);
@@ -231,6 +251,7 @@ html.light-theme {
231
251
  --surface-section: #fff;
232
252
  --surface-card: #fff;
233
253
  --surface-overlay: #fff;
254
+ --surface-overlay-strong: #fff;
234
255
  --glass-bg: #f7f8fa;
235
256
  --glass-bg-elevated: #fff;
236
257
  --glass-blur: 0px;