@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/fesm2022/flyos-design-system.mjs +231 -150
- package/fesm2022/flyos-design-system.mjs.map +1 -1
- package/package.json +1 -1
- package/scss/_app-surface-tokens.scss +127 -13
- package/scss/_ink-baseline.scss +4 -4
- package/scss/_nova-glass.scss +37 -13
- package/scss/_nova-tokens.scss +61 -0
- package/scss/_shell-embed-bridge.scss +30 -23
- package/scss/_theme-dark.scss +10 -0
- package/scss/_theme-light.scss +29 -8
- package/scss/_tokens.scss +12 -4
- package/types/flyos-design-system.d.ts +94 -12
- package/types/flyos-design-system.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -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 —
|
|
210
|
-
// their contrast cannot be broken by a
|
|
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
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
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
|
+
}
|
package/scss/_ink-baseline.scss
CHANGED
|
@@ -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.
|
package/scss/_nova-glass.scss
CHANGED
|
@@ -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
|
|
138
|
-
//
|
|
139
|
-
|
|
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
|
|
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 —
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
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.
|
|
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.
|
package/scss/_nova-tokens.scss
CHANGED
|
@@ -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
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
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
|
}
|
package/scss/_theme-dark.scss
CHANGED
|
@@ -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;
|
package/scss/_theme-light.scss
CHANGED
|
@@ -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 —
|
|
74
|
-
//
|
|
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
|
-
|
|
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.
|
|
153
|
-
|
|
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
|
-
|
|
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;
|