@flyos/design-system 1.7.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 +2948 -575
- package/fesm2022/flyos-design-system.mjs.map +1 -1
- package/package.json +1 -1
- package/scss/_app-surface-tokens.scss +228 -13
- package/scss/_app-surface-utilities.scss +7 -2
- package/scss/_business-app-buttons.scss +35 -8
- package/scss/_fly-theme.scss +9 -0
- package/scss/{_vos-button-mixins.scss → _fos-button-mixins.scss} +28 -28
- package/scss/_ink-baseline.scss +29 -29
- package/scss/_nova-glass.scss +451 -0
- package/scss/_nova-motion-mixins.scss +42 -0
- package/scss/_nova-motion.scss +324 -0
- package/scss/_nova-tokens.scss +356 -0
- package/scss/_nova-type.scss +188 -0
- package/scss/_shell-embed-bridge.scss +120 -81
- package/scss/_theme-dark.scss +11 -1
- package/scss/_theme-light.scss +31 -10
- package/scss/_tokens.scss +17 -9
- package/types/flyos-design-system.d.ts +1642 -36
- package/types/flyos-design-system.d.ts.map +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// ─── Nova type roles (UX v2) ─────────────────────────────────────────────────
|
|
2
|
+
// Stage 1 task S1.10 of the UX-refresh program.
|
|
3
|
+
// Canon: `skills/desktop-design-language.md` §7, which cites
|
|
4
|
+
// `.workflow/plans/ux-refresh/analysis/D1-home-desktop.md` §6 for the exact
|
|
5
|
+
// numbers below. Never re-derive a value from the `UX/**/*.dc.html`
|
|
6
|
+
// prototypes — the prototypes drift, the skill is the contract.
|
|
7
|
+
|
|
8
|
+
// This partial is `@use`d exactly once, from `_fly-theme.scss` — it emits
|
|
9
|
+
// GLOBAL CSS (custom properties + two utility classes) and defines NO mixins.
|
|
10
|
+
// That is deliberate, not an omission: `_nova-motion.scss` already hit this
|
|
11
|
+
// exact wall (its own header warns "never `@use` it a second time" because a
|
|
12
|
+
// component-level `@use` re-emits its `:root` block and all 16 `@keyframes`
|
|
13
|
+
// into that component's own compiled stylesheet — harmless in content,
|
|
14
|
+
// wasteful in bytes) and `motion-panel.component.ts` had to replicate
|
|
15
|
+
// `nova-stagger`'s math in TypeScript rather than pay that cost. A role here
|
|
16
|
+
// is therefore exposed ONLY as `var(--nova-font-*)` / `var(--nova-tracking-*)`
|
|
17
|
+
// — plain global custom properties, read directly by any component's own
|
|
18
|
+
// `.scss` exactly the way `--w95` or `--font-title1` already are, with no
|
|
19
|
+
// `@use` of this file required or permitted a second time. Full reasoning for
|
|
20
|
+
// choosing tokens over mixins: `.workflow/plans/ux-refresh/notes/
|
|
21
|
+
// S1.10-typography.md` § "Token vs mixin".
|
|
22
|
+
|
|
23
|
+
// ── The Display/Text split ───────────────────────────────────────────────────
|
|
24
|
+
// The design distinguishes a Display face (headings ≥ ~13.5px bold) from Text
|
|
25
|
+
// (everything else), but D8 (skill §7, ratified) forbids ever shipping a
|
|
26
|
+
// second face for it — no SF fonts, ever. Geist is ONE family for both; the
|
|
27
|
+
// split rides WEIGHT and LETTER-SPACING only, never `font-family`. Proof
|
|
28
|
+
// point: `--nova-font-menu-item` and `--nova-font-section-title` share the
|
|
29
|
+
// exact same 13.5px SIZE and sit on opposite sides of the split (500/normal
|
|
30
|
+
// Text vs 700/-0.01em Display) — that pairing IS the split in action.
|
|
31
|
+
|
|
32
|
+
// ── Snap vs mint ─────────────────────────────────────────────────────────────
|
|
33
|
+
// Each role is annotated SNAP (aliases an existing `--font-*` ramp step from
|
|
34
|
+
// `_tokens.scss` verbatim, via `var()`, so a future ramp change propagates) or
|
|
35
|
+
// MINT (a new literal — no ramp step is close enough on size, line-height
|
|
36
|
+
// ratio, weight or letter-spacing to reuse without a visible fidelity loss).
|
|
37
|
+
// The full per-role reasoning lives in the handoff note; the short form
|
|
38
|
+
// travels here as a comment next to each declaration so the two can't drift
|
|
39
|
+
// apart silently.
|
|
40
|
+
|
|
41
|
+
// ── Letter-spacing rides a SEPARATE token, never the `font` shorthand ───────
|
|
42
|
+
// CSS `font:` cannot carry `letter-spacing` — it is not one of the shorthand's
|
|
43
|
+
// component properties (weight/size/line-height/family only). A role that
|
|
44
|
+
// needs tracking therefore gets a companion `--nova-tracking-<role>` token,
|
|
45
|
+
// and EVERY role gets one — even the ones the design leaves untracked
|
|
46
|
+
// (`normal`) — so a caller always writes the same two-line pattern regardless
|
|
47
|
+
// of role, instead of having to remember which roles carry tracking.
|
|
48
|
+
|
|
49
|
+
:root {
|
|
50
|
+
// ── Hero / marquee — bespoke, animated, never reused elsewhere ────────────
|
|
51
|
+
// MINT. 28px sits between --font-title1 (24px) and --font-large-title
|
|
52
|
+
// (29px); the closer neighbour (large-title) carries a 38/29 ≈ 1.31 line-
|
|
53
|
+
// height ratio against this role's specified 1.1 — snapping would make a
|
|
54
|
+
// compact hero headline read visibly looser than the design. Pair with the
|
|
55
|
+
// `greetShift` keyframe (`_nova-motion.scss`) for the animated gradient
|
|
56
|
+
// fill; the fill itself is the caller's concern, not baked in here.
|
|
57
|
+
--nova-font-page-headline: 700 28px/1.1 var(--font-family);
|
|
58
|
+
--nova-tracking-page-headline: -0.022em;
|
|
59
|
+
|
|
60
|
+
// MINT. Size coincides with --font-title1 (24px), but that step's 32/24 ≈
|
|
61
|
+
// 1.333 line-height ratio is a third looser than this role's 1.15 — the
|
|
62
|
+
// assistant's hero greeting would visibly loosen on snap.
|
|
63
|
+
--nova-font-hero-greeting: 700 24px/1.15 var(--font-family);
|
|
64
|
+
--nova-tracking-hero-greeting: -0.018em;
|
|
65
|
+
|
|
66
|
+
// MINT. No ramp step sits at 26px, and none pairs a semibold (600) weight
|
|
67
|
+
// with a sub-30px size. Colour/text-shadow (the D1 extraction notes
|
|
68
|
+
// "#fff + text-shadow") are the caller's concern, not part of the type role.
|
|
69
|
+
--nova-font-hero-card-name: 600 26px/1.14 var(--font-family);
|
|
70
|
+
--nova-tracking-hero-card-name: -0.01em;
|
|
71
|
+
|
|
72
|
+
// ── Chrome / structural headings (Display) ────────────────────────────────
|
|
73
|
+
// MINT. --font-headline (17px/700) is the nearest ramp step; its 22/17 ≈
|
|
74
|
+
// 1.294 line-height ratio is far looser than this role's 1.1, and the ramp
|
|
75
|
+
// carries no letter-spacing vocabulary at all.
|
|
76
|
+
--nova-font-drawer-title: 700 16px/1.1 var(--font-family);
|
|
77
|
+
--nova-tracking-drawer-title: -0.016em;
|
|
78
|
+
|
|
79
|
+
// MINT. Nothing in the ramp sits near 14.5px at weight 700.
|
|
80
|
+
--nova-font-dialog-title: 700 14.5px/1.1 var(--font-family);
|
|
81
|
+
--nova-tracking-dialog-title: -0.014em;
|
|
82
|
+
|
|
83
|
+
// MINT. The design gives a 500-600 WEIGHT RANGE at 14px; --font-footnote
|
|
84
|
+
// (13px/510) and --font-callout (15px/590) are each 1px off in opposite
|
|
85
|
+
// directions with no clear winner either way. Minted rather than snapped
|
|
86
|
+
// because this role lives in the always-visible header chrome — a singular,
|
|
87
|
+
// high-scrutiny use, not a repeated list primitive — so the vocabulary cost
|
|
88
|
+
// of one more token buys exact fidelity where it is most seen. Picked 600,
|
|
89
|
+
// the range's upper bound, for legibility over the translucent header
|
|
90
|
+
// material.
|
|
91
|
+
--nova-font-header-context: 600 14px/1.3 var(--font-family);
|
|
92
|
+
--nova-tracking-header-context: normal;
|
|
93
|
+
|
|
94
|
+
// MINT. Explicitly tagged Display in the source, with a tracking value the
|
|
95
|
+
// ramp has no vocabulary for at all. Shares its 13.5px SIZE with
|
|
96
|
+
// --nova-font-menu-item below on the opposite side of the Display/Text
|
|
97
|
+
// split — see the file header.
|
|
98
|
+
--nova-font-section-title: 700 13.5px/1.2 var(--font-family);
|
|
99
|
+
--nova-tracking-section-title: -0.01em;
|
|
100
|
+
|
|
101
|
+
// ── Workhorse text — repeated everywhere, snapped where fidelity allows ───
|
|
102
|
+
// SNAP → --font-footnote (510 13px/18px). Δsize 0.5px, Δweight 10 (510 vs
|
|
103
|
+
// 500) — both imperceptible. No Display tag, no tracking in the source:
|
|
104
|
+
// plain Text-family, unlike section-title above despite the shared 13.5px
|
|
105
|
+
// size.
|
|
106
|
+
--nova-font-menu-item: var(--font-footnote);
|
|
107
|
+
--nova-tracking-menu-item: normal;
|
|
108
|
+
|
|
109
|
+
// SNAP → --font-footnote (510 13px/18px, ratio 1.385). Exact size match.
|
|
110
|
+
// The source spec asks for weight 400, but every other body-ish ramp step
|
|
111
|
+
// already uses ~510 as a deliberate on-screen optical correction
|
|
112
|
+
// (--font-body, --font-caption1/2 all do the same) — reusing --font-footnote
|
|
113
|
+
// keeps body/chat/input text consistent with that platform-wide convention
|
|
114
|
+
// instead of introducing a lone literal-400 outlier.
|
|
115
|
+
--nova-font-body: var(--font-footnote);
|
|
116
|
+
--nova-tracking-body: normal;
|
|
117
|
+
|
|
118
|
+
// MINT. Weight 600 is a real, perceptible step up from every ramp option
|
|
119
|
+
// near this size (nearest is --font-caption1 at 510) — row labels / tile
|
|
120
|
+
// names are scannable list-and-grid primitives where that weight jump IS
|
|
121
|
+
// the point of the role, so snapping would flatten the exact distinction
|
|
122
|
+
// the design is making.
|
|
123
|
+
--nova-font-row-label: 600 12.5px/1.3 var(--font-family);
|
|
124
|
+
--nova-tracking-row-label: normal;
|
|
125
|
+
|
|
126
|
+
// MINT. Same weight-fidelity argument as row-label, reinforced by the
|
|
127
|
+
// skill's own field-kit section (§6) independently ratifying this exact
|
|
128
|
+
// 600/11.5px/`--w6` pairing as a load-bearing forms convention. Colour is
|
|
129
|
+
// NOT baked into this token — apply `color: var(--w6)` alongside it, the
|
|
130
|
+
// same as every other ramp-step colour decision.
|
|
131
|
+
--nova-font-field-label: 600 11.5px/1.2 var(--font-family);
|
|
132
|
+
--nova-tracking-field-label: normal;
|
|
133
|
+
|
|
134
|
+
// SNAP → --font-caption1 (510 12px/16px, ratio 1.333). Δsize 0.5px, Δweight
|
|
135
|
+
// 10, Δline-height-ratio ~6.6% against the target 1.25 — all within
|
|
136
|
+
// tolerance for an ancillary role.
|
|
137
|
+
--nova-font-tooltip: var(--font-caption1);
|
|
138
|
+
--nova-tracking-tooltip: normal;
|
|
139
|
+
|
|
140
|
+
// MINT. The smallest heading-adjacent role, and the ONLY one with a
|
|
141
|
+
// POSITIVE tracking direction in the whole table (the ramp has no
|
|
142
|
+
// letter-spacing vocabulary to snap to regardless). Snapped UP to 11px
|
|
143
|
+
// rather than the source's 10.5px floor: skill §7's de-facto size floor
|
|
144
|
+
// reserves 10.5px for the caption-tier exception (timestamps, below), not
|
|
145
|
+
// eyebrows — 11px is the honest reading of "de-facto floor 11px". Tracking
|
|
146
|
+
// is the midpoint of the source's 0.02-0.04em range. Colour defaults to
|
|
147
|
+
// `--w42`; the source also names `--w4` as a context-dependent alternate
|
|
148
|
+
// with no rule for choosing between them, so that choice stays with the
|
|
149
|
+
// caller rather than being baked in here.
|
|
150
|
+
--nova-font-eyebrow: 600 11px/1.3 var(--font-family);
|
|
151
|
+
--nova-tracking-eyebrow: 0.03em;
|
|
152
|
+
|
|
153
|
+
// MINT. The one sanctioned exception to the 11px floor (skill §7:
|
|
154
|
+
// "10.5px caption-only"). Base weight 400; the source's 400-700 range
|
|
155
|
+
// covers a bold badge-count variant, applied by the caller with a local
|
|
156
|
+
// `font-weight: 700` override rather than a second minted role, since size,
|
|
157
|
+
// tracking and family are identical either way.
|
|
158
|
+
--nova-font-timestamp: 400 10.5px/1.3 var(--font-family);
|
|
159
|
+
--nova-tracking-timestamp: normal;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Rule A: tabular numerals (skill §7, mechanical) ─────────────────────────
|
|
163
|
+
// "font-variant-numeric: tabular-nums on every count, timestamp and pagination
|
|
164
|
+
// number." `_app-surface-utilities.scss` already ships `.mono` bundling this
|
|
165
|
+
// same property with a switch to the monospace family; `.nova-tabular-nums`
|
|
166
|
+
// is the tabular-nums-ONLY half, for the proportional-font contexts these
|
|
167
|
+
// roles live in (a row count sitting inside --nova-font-row-label, a
|
|
168
|
+
// timestamp inside --nova-font-timestamp) where switching to Geist Mono is
|
|
169
|
+
// not wanted. One class is the one-liner the rule calls for.
|
|
170
|
+
// Reviewer check: grep the diff for a numeric interpolation — a count binding
|
|
171
|
+
// (`{{ …Count }}`), a date/time pipe, a "page N of M" string — with no
|
|
172
|
+
// `nova-tabular-nums` class alongside it in the same template. That gap IS
|
|
173
|
+
// the violation; there is no way to detect it from the SCSS side alone, since
|
|
174
|
+
// the rule is about which ELEMENTS carry the class, not whether the class
|
|
175
|
+
// itself exists (`nova-type.spec.ts` only guards the latter).
|
|
176
|
+
.nova-tabular-nums {
|
|
177
|
+
font-variant-numeric: tabular-nums;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Rule B: RTL runs keep embedded Latin its own direction (skill §7) ──────
|
|
181
|
+
// "RTL text runs carry unicode-bidi: plaintext so embedded English keeps its
|
|
182
|
+
// own direction." Same one-class mechanism and the same reviewer check: grep
|
|
183
|
+
// the diff for a translated or mixed-script string (a product name, an email
|
|
184
|
+
// address, a file extension embedded in Arabic/Urdu copy) with no
|
|
185
|
+
// `nova-bidi-plaintext` class in the same template.
|
|
186
|
+
.nova-bidi-plaintext {
|
|
187
|
+
unicode-bidi: plaintext;
|
|
188
|
+
}
|
|
@@ -1,81 +1,120 @@
|
|
|
1
|
-
// ─── Shell-embed bridge — app-surface tokens re-tinted onto shell glass ──────
|
|
2
|
-
// MIXIN ONLY. This partial emits no CSS on import; it exists so the mapping
|
|
3
|
-
// below lives in ONE place instead of being retyped per feature app.
|
|
4
|
-
|
|
5
|
-
// WHY IT IS NEEDED
|
|
6
|
-
// The DS carries two token families on purpose (see `_app-surface-tokens.scss`'s
|
|
7
|
-
// header). `--ink-*`/`--bg-*`/`--line-*` dress BUSINESS-APP CONTENT: an opaque
|
|
8
|
-
// reading surface, dark ink on white in light mode. `--label-*`/`--surface-*`
|
|
9
|
-
// dress SHELL CHROME: white ink on translucent glass over the user's wallpaper.
|
|
10
|
-
|
|
11
|
-
// A desktop-shell feature app renders inside a glass window, so its chrome is
|
|
12
|
-
// necessarily the second family — but the moment it mounts a component from the
|
|
13
|
-
// `fly-*` app-surface kit (or `@flyos/design-system-board`), that component
|
|
14
|
-
// themes itself from the FIRST family and paints an opaque white slab with
|
|
15
|
-
// near-black ink. One region, two families: the app's own header ink is white,
|
|
16
|
-
// the slab beneath it is white, and the header goes invisible.
|
|
17
|
-
|
|
18
|
-
// That is not hypothetical. It shipped in canvas-boards and was measured live:
|
|
19
|
-
// `.cbe__export-btn` computed `color: rgb(255,255,255)` sitting on a board whose
|
|
20
|
-
// `--_surface` resolved to `oklch(100% 0 0deg)`. `core/theme/feature-app-surface-tokens.ts`
|
|
21
|
-
// documents the same class of trap ("a dark-mode-only review passes while light
|
|
22
|
-
// mode renders a white slab") — and note the failure is LIGHT-mode-only, because
|
|
23
|
-
// in dark mode both families happen to agree on light-ink-on-dark.
|
|
24
|
-
|
|
25
|
-
// THE FIX is the seam the tokens file already names: an app that embeds in the
|
|
26
|
-
// shell re-tints the content family onto glass. help-center (`.hc-shell`) and
|
|
27
|
-
// dashboard-app hand-rolled identical copies of this map before it lived here.
|
|
28
|
-
|
|
29
|
-
// ALIASES ONLY — every value is another token, never a literal, so the whole map
|
|
30
|
-
// follows the shell's theme automatically. The ONE exception is `--on-ink`, and
|
|
31
|
-
// its reason is written at the declaration.
|
|
32
|
-
@mixin app-surface-on-glass {
|
|
33
|
-
// Surfaces: the window chrome supplies the material, so the app's own page
|
|
34
|
-
// background is nothing at all; cards/tracks are lifts ON that plate.
|
|
35
|
-
--bg: transparent;
|
|
36
|
-
--bg-2: var(--surface-card);
|
|
37
|
-
--bg-3: var(--fill-tertiary);
|
|
38
|
-
--bg-hover: var(--surface-active);
|
|
39
|
-
|
|
40
|
-
// Hairlines.
|
|
41
|
-
--line: var(--separator);
|
|
42
|
-
--line-2: var(--surface-border);
|
|
43
|
-
--line-3: var(--separator);
|
|
44
|
-
|
|
45
|
-
// Ink — opaque label tokens, never a surface token. Aliasing a translucent
|
|
46
|
-
// surface onto an ink slot is what turns a control into an empty coloured
|
|
47
|
-
// square (see the note in help-center.component.scss).
|
|
48
|
-
--ink: var(--label-primary);
|
|
49
|
-
--ink-2: var(--label-secondary);
|
|
50
|
-
--ink-3: var(--label-secondary);
|
|
51
|
-
--ink-4: var(--label-tertiary);
|
|
52
|
-
|
|
53
|
-
// CONTRACT (`_app-surface-tokens.scss`): anything that redefines `--ink` MUST
|
|
54
|
-
// redefine `--on-ink`, and it must be OPAQUE. `--on-ink` labels a fill of
|
|
55
|
-
// `--ink` itself — the primary button. Above we pin `--ink` to the shell's
|
|
56
|
-
// white label ink in BOTH themes, so its counter-ink is a fixed DARK value in
|
|
57
|
-
// both. It cannot alias a shell token: the shell is glass and owns no
|
|
58
|
-
// dark-opaque ink. This literal is the same value `--ink` carries in the
|
|
59
|
-
// app-surface light theme, i.e. the colour a white pill is designed to label.
|
|
60
|
-
|
|
61
|
-
// `app-surface-conventions.spec.ts` can only police the DS token file itself;
|
|
62
|
-
// its doc says consumers that remap `--ink` "carry the same obligation".
|
|
63
|
-
// Routing every consumer through this mixin is how that obligation is met.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
1
|
+
// ─── Shell-embed bridge — app-surface tokens re-tinted onto shell glass ──────
|
|
2
|
+
// MIXIN ONLY. This partial emits no CSS on import; it exists so the mapping
|
|
3
|
+
// below lives in ONE place instead of being retyped per feature app.
|
|
4
|
+
|
|
5
|
+
// WHY IT IS NEEDED
|
|
6
|
+
// The DS carries two token families on purpose (see `_app-surface-tokens.scss`'s
|
|
7
|
+
// header). `--ink-*`/`--bg-*`/`--line-*` dress BUSINESS-APP CONTENT: an opaque
|
|
8
|
+
// reading surface, dark ink on white in light mode. `--label-*`/`--surface-*`
|
|
9
|
+
// dress SHELL CHROME: white ink on translucent glass over the user's wallpaper.
|
|
10
|
+
|
|
11
|
+
// A desktop-shell feature app renders inside a glass window, so its chrome is
|
|
12
|
+
// necessarily the second family — but the moment it mounts a component from the
|
|
13
|
+
// `fly-*` app-surface kit (or `@flyos/design-system-board`), that component
|
|
14
|
+
// themes itself from the FIRST family and paints an opaque white slab with
|
|
15
|
+
// near-black ink. One region, two families: the app's own header ink is white,
|
|
16
|
+
// the slab beneath it is white, and the header goes invisible.
|
|
17
|
+
|
|
18
|
+
// That is not hypothetical. It shipped in canvas-boards and was measured live:
|
|
19
|
+
// `.cbe__export-btn` computed `color: rgb(255,255,255)` sitting on a board whose
|
|
20
|
+
// `--_surface` resolved to `oklch(100% 0 0deg)`. `core/theme/feature-app-surface-tokens.ts`
|
|
21
|
+
// documents the same class of trap ("a dark-mode-only review passes while light
|
|
22
|
+
// mode renders a white slab") — and note the failure is LIGHT-mode-only, because
|
|
23
|
+
// in dark mode both families happen to agree on light-ink-on-dark.
|
|
24
|
+
|
|
25
|
+
// THE FIX is the seam the tokens file already names: an app that embeds in the
|
|
26
|
+
// shell re-tints the content family onto glass. help-center (`.hc-shell`) and
|
|
27
|
+
// dashboard-app hand-rolled identical copies of this map before it lived here.
|
|
28
|
+
|
|
29
|
+
// ALIASES ONLY — every value is another token, never a literal, so the whole map
|
|
30
|
+
// follows the shell's theme automatically. The ONE exception is `--on-ink`, and
|
|
31
|
+
// its reason is written at the declaration.
|
|
32
|
+
@mixin app-surface-on-glass {
|
|
33
|
+
// Surfaces: the window chrome supplies the material, so the app's own page
|
|
34
|
+
// background is nothing at all; cards/tracks are lifts ON that plate.
|
|
35
|
+
--bg: transparent;
|
|
36
|
+
--bg-2: var(--surface-card);
|
|
37
|
+
--bg-3: var(--fill-tertiary);
|
|
38
|
+
--bg-hover: var(--surface-active);
|
|
39
|
+
|
|
40
|
+
// Hairlines.
|
|
41
|
+
--line: var(--separator);
|
|
42
|
+
--line-2: var(--surface-border);
|
|
43
|
+
--line-3: var(--separator);
|
|
44
|
+
|
|
45
|
+
// Ink — opaque label tokens, never a surface token. Aliasing a translucent
|
|
46
|
+
// surface onto an ink slot is what turns a control into an empty coloured
|
|
47
|
+
// square (see the note in help-center.component.scss).
|
|
48
|
+
--ink: var(--label-primary);
|
|
49
|
+
--ink-2: var(--label-secondary);
|
|
50
|
+
--ink-3: var(--label-secondary);
|
|
51
|
+
--ink-4: var(--label-tertiary);
|
|
52
|
+
|
|
53
|
+
// CONTRACT (`_app-surface-tokens.scss`): anything that redefines `--ink` MUST
|
|
54
|
+
// redefine `--on-ink`, and it must be OPAQUE. `--on-ink` labels a fill of
|
|
55
|
+
// `--ink` itself — the primary button. Above we pin `--ink` to the shell's
|
|
56
|
+
// white label ink in BOTH themes, so its counter-ink is a fixed DARK value in
|
|
57
|
+
// both. It cannot alias a shell token: the shell is glass and owns no
|
|
58
|
+
// dark-opaque ink. This literal is the same value `--ink` carries in the
|
|
59
|
+
// app-surface light theme, i.e. the colour a white pill is designed to label.
|
|
60
|
+
|
|
61
|
+
// `app-surface-conventions.spec.ts` can only police the DS token file itself;
|
|
62
|
+
// its doc says consumers that remap `--ink` "carry the same obligation".
|
|
63
|
+
// Routing every consumer through this mixin is how that obligation is met.
|
|
64
|
+
// (`app-surface-nova-bridge.spec.ts` extends that gate to THIS file.)
|
|
65
|
+
|
|
66
|
+
// AT THE DS 2.0 FLIP THIS LINE MUST CHANGE — it is the one value in this mixin
|
|
67
|
+
// that does not follow the shell automatically. The premise above ("`--ink` is
|
|
68
|
+
// the shell's white label ink in BOTH themes") holds only while light chrome is
|
|
69
|
+
// the vibrancy smoked plate. Nova's light chrome is genuinely light with
|
|
70
|
+
// DARK ink (`--chrome-ink: #353537`; design `--ink` light `#000`, folded into
|
|
71
|
+
// `--label-primary` per D1), so post-flip `--ink` flips per theme here too and
|
|
72
|
+
// a single dark literal is wrong in light mode — the same white-slab failure,
|
|
73
|
+
// arriving from the opposite side.
|
|
74
|
+
|
|
75
|
+
// The flip value is `var(--check-ink)`: Nova's only counter-ink that flips the
|
|
76
|
+
// right way (`#fff` light, `#0f1013` dark), which is exactly "the ink on a
|
|
77
|
+
// plate that carries the theme's own primary ink". Do NOT reach for a `--w*`
|
|
78
|
+
// step — the ramp flips polarity (`--w95` is 95% black in light, 95% WHITE in
|
|
79
|
+
// dark), so no step is a counter-ink in both themes; and do NOT reach for
|
|
80
|
+
// `--on-accent`, which is `#fff` in both and therefore right only in light.
|
|
81
|
+
--on-ink: oklch(18% 0.005 250deg);
|
|
82
|
+
|
|
83
|
+
// `--ink-inverse` carries the SAME obligation as `--on-ink` and was missed.
|
|
84
|
+
|
|
85
|
+
// The app-surface family defines it once, as "text on dark / gradient fills" — near-white, which
|
|
86
|
+
// is correct while `--ink` is near-black. This mixin flips `--ink` to the shell's WHITE label ink
|
|
87
|
+
// and updated `--on-ink` accordingly, but left `--ink-inverse` at its near-white resting value.
|
|
88
|
+
// Every `background: var(--ink); color: var(--ink-inverse)` pair inside an embedded app therefore
|
|
89
|
+
// became WHITE ON WHITE at a contrast ratio of about 1:1.
|
|
90
|
+
|
|
91
|
+
// That is not hypothetical: it is what made `[data-tooltip]` (the CSS tooltip behind every
|
|
92
|
+
// `fly-icon-button`'s `tooltipKey`) invisible across the board editor — measured live at
|
|
93
|
+
// background rgb(255,255,255) with colour oklch(0.99 0 0). The same pair is used by the button,
|
|
94
|
+
// icon-button, checkbox and card components, so this was never a Boards-only bug.
|
|
95
|
+
|
|
96
|
+
// Pinned to `--on-ink` rather than restated, so the two counter-inks cannot drift apart again.
|
|
97
|
+
|
|
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.
|
|
119
|
+
--ink-inverse: var(--on-ink);
|
|
120
|
+
}
|
package/scss/_theme-dark.scss
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ─── Dark theme (`html.dark-theme`, persisted `dark`) ────────────────────────
|
|
2
2
|
// Shell Appearance label "Dark" / i18n `settings.theme.dark`.
|
|
3
|
-
//
|
|
3
|
+
// Translucent dark glass: semi-transparent surfaces + backdrop blur.
|
|
4
4
|
html.dark-theme {
|
|
5
5
|
/* Native selects / scrollbars follow OS form appearance; keep in sync with light-on-glass labels. */
|
|
6
6
|
color-scheme: dark;
|
|
@@ -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%);
|
|
@@ -181,7 +201,7 @@ html.light-theme {
|
|
|
181
201
|
--sidebar-radius: 28px;
|
|
182
202
|
--focus-ring: color-mix(in srgb, var(--accent) 50%, transparent);
|
|
183
203
|
|
|
184
|
-
// Segmented `.
|
|
204
|
+
// Segmented `.fos-btn.selected` / `.active` (Settings theme row, clock, weather, etc.): dark pill on light UI.
|
|
185
205
|
--btn-selected-bg: #111827;
|
|
186
206
|
--btn-selected-text: #fff;
|
|
187
207
|
|
|
@@ -207,7 +227,7 @@ html.light-theme {
|
|
|
207
227
|
--material-glass-specular: inset 0 1px 0 rgb(255 255 255 / 60%),
|
|
208
228
|
inset 0 -1px 0 rgb(17 24 39 / 4%);
|
|
209
229
|
|
|
210
|
-
// Aliases used by some Business App SCSS (not part of core
|
|
230
|
+
// Aliases used by some Business App SCSS (not part of core FlyOS tokens).
|
|
211
231
|
--fly-color-surface: #fff;
|
|
212
232
|
--fly-color-surface-alt: #f3f4f6;
|
|
213
233
|
--fly-color-border: #e5e7eb;
|
|
@@ -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;
|
package/scss/_tokens.scss
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// ───
|
|
1
|
+
// ─── FlyOS System Colors & Design Tokens ─────────────────────────────────
|
|
2
2
|
:root {
|
|
3
3
|
--system-red: #FF453A;
|
|
4
4
|
--system-orange: #FF9F0A;
|
|
@@ -21,11 +21,19 @@
|
|
|
21
21
|
--status-info: var(--system-blue);
|
|
22
22
|
--status-pending: var(--system-yellow);
|
|
23
23
|
|
|
24
|
-
// ──
|
|
25
|
-
// Geist is the primary face (self-hosted, see styles/_geist-fonts.scss).
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
24
|
+
// ── FlyOS Typography ──
|
|
25
|
+
// Geist is the primary face (self-hosted, see styles/_geist-fonts.scss). Inter and the
|
|
26
|
+
// system stack are the fallbacks, and the cascade also picks them up for scripts Geist
|
|
27
|
+
// doesn't cover (ar / ur).
|
|
28
|
+
// ---
|
|
29
|
+
// 2.0.0 removed 'SF Pro Display' / 'SF Pro' — D8 (no SF fonts, ever, for licensing). The
|
|
30
|
+
// repo had been SHIPPING them: 9 self-hosted woff2 faces, preloaded from index.html.
|
|
31
|
+
// Removing them changes NOTHING for ar/ur, contrary to the comment that used to sit here
|
|
32
|
+
// and to the one in `_geist-fonts.scss`: measured in the live shell, Arabic and Urdu
|
|
33
|
+
// render byte-identically with and without SF Pro in the stack (its @font-face rules carry
|
|
34
|
+
// no unicode-range, and it supplies no Arabic glyphs at all — the apparent width delta was
|
|
35
|
+
// one SPACE character). Those scripts have always resolved to the system Arabic face.
|
|
36
|
+
--font-family: 'Geist', 'Inter', -apple-system, blinkmacsystemfont, 'Segoe UI', sans-serif;
|
|
29
37
|
--font-family-mono: 'Geist Mono', ui-monospace, 'SF Mono', menlo, monospace;
|
|
30
38
|
--font-xl-title1: 700 48px/56px var(--font-family);
|
|
31
39
|
--font-xl-title2: 700 38px/46px var(--font-family);
|
|
@@ -41,12 +49,12 @@
|
|
|
41
49
|
--font-caption1: 510 12px/16px var(--font-family);
|
|
42
50
|
--font-caption2: 510 12px/16px var(--font-family);
|
|
43
51
|
|
|
44
|
-
// ──
|
|
52
|
+
// ── FlyOS Label Colors ──
|
|
45
53
|
--label-primary: rgb(255 255 255 / 96%);
|
|
46
54
|
--label-secondary: rgb(255 255 255 / 56%);
|
|
47
55
|
--label-tertiary: rgb(255 255 255 / 36%);
|
|
48
56
|
|
|
49
|
-
// ──
|
|
57
|
+
// ── FlyOS Materials ──
|
|
50
58
|
--material-glass: rgb(128 128 128 / 30%);
|
|
51
59
|
--material-glass-blur: 50px;
|
|
52
60
|
--material-glass-stroke: rgb(255 255 255 / 40%);
|
|
@@ -70,7 +78,7 @@
|
|
|
70
78
|
--separator: rgb(255 255 255 / 12%);
|
|
71
79
|
--blur-shadow-small: 0 2px 4px rgb(0 0 0 / 10%);
|
|
72
80
|
|
|
73
|
-
// ──
|
|
81
|
+
// ── FlyOS Button Tokens ──
|
|
74
82
|
--btn-font-weight: 590;
|
|
75
83
|
--btn-symbol-font-weight: 510;
|
|
76
84
|
--btn-radius-capsule: 500px;
|