@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,324 @@
|
|
|
1
|
+
// ─── Nova motion library (UX v2) ─────────────────────────────────────────────
|
|
2
|
+
// Stage 1 task S1.3 of the UX-refresh program.
|
|
3
|
+
// Canon: `skills/desktop-design-language.md` §5, which cites
|
|
4
|
+
// `.workflow/plans/ux-refresh/analysis/D1-home-desktop.md` §5 and
|
|
5
|
+
// `D2-appwindow-circles.md` §6 for the exact numbers below.
|
|
6
|
+
// This partial is the EMISSION entry, `@use`d exactly once — from
|
|
7
|
+
// `_fly-theme.scss` (and transitively by the standalone `_app-surface-tokens
|
|
8
|
+
// .scss` bundle). It emits GLOBAL CSS: custom properties, `@keyframes`, and one
|
|
9
|
+
// `prefers-reduced-motion` block. Never `@use` it from a component: `@keyframes`
|
|
10
|
+
// and custom-property declarations are not scoped by Sass module boundaries, so
|
|
11
|
+
// a second `@use` emits every rule again into that component's stylesheet.
|
|
12
|
+
// **Need `nova-stagger` in a component? `@use 'nova-motion-mixins'.**
|
|
13
|
+
// The mixin moved to that sibling partial at the S1 review (F4) precisely
|
|
14
|
+
// because bundling a mixin with global emission made it unusable: the two
|
|
15
|
+
// callers who honoured this header hand-copied the mixin's math instead, and the
|
|
16
|
+
// one who ignored it re-emitted this whole file. The mixins-only partial emits
|
|
17
|
+
// nothing, so it is safe to `@use` anywhere. It is `@forward`ed below, so an
|
|
18
|
+
// existing `@use 'nova-motion'` caller still resolves `nova-stagger` — but pay
|
|
19
|
+
// the global emission for nothing if you do.
|
|
20
|
+
// Contract for the rest of Stage 1 (full detail in
|
|
21
|
+
// `.workflow/plans/ux-refresh/notes/S1.3-motion-library.md`):
|
|
22
|
+
// - Easing tokens `--nova-ease-*` and duration tokens `--nova-duration-*` /
|
|
23
|
+
// `--nova-scale-press` / `--nova-filter-press` — a component transition or
|
|
24
|
+
// animation NEVER writes a raw `cubic-bezier(...)`, `scale(.985)` or
|
|
25
|
+
// `brightness(.96)` literal again.
|
|
26
|
+
// - The 16 ratified `@keyframes` by exact name — components reference them
|
|
27
|
+
// by name in their own `animation:` shorthand (duration/easing/iteration
|
|
28
|
+
// are the CALLER's concern; a keyframe only defines the shape).
|
|
29
|
+
// - The single `prefers-reduced-motion` block for the whole app. Components
|
|
30
|
+
// never write their own.
|
|
31
|
+
|
|
32
|
+
// Compatibility re-export — emits nothing (the target is mixins-only).
|
|
33
|
+
@forward 'nova-motion-mixins';
|
|
34
|
+
|
|
35
|
+
:root {
|
|
36
|
+
// ── Easing families (§5 table — use these and nothing else) ───────────────
|
|
37
|
+
--nova-ease-structural: cubic-bezier(0.32, 0.72, 0, 1); // menus, drawers, panel/aside width, search morph, view entry, flex-basis, theme bg fade
|
|
38
|
+
--nova-ease-micro: cubic-bezier(0.2, 0.8, 0.2, 1); // hover/colour/scrim, fadeIn, tooltips
|
|
39
|
+
--nova-ease-soft: cubic-bezier(0.4, 0.6, 0.2, 1); // buttons/menuitems base transition (the interaction default)
|
|
40
|
+
--nova-ease-overshoot: cubic-bezier(0.34, 1.56, 0.64, 1); // icon press bounce only — playful overshoot
|
|
41
|
+
|
|
42
|
+
// ── Interaction timing (§5) ────────────────────────────────────────────────
|
|
43
|
+
--nova-duration-settle: 0.34s; // default settle
|
|
44
|
+
--nova-duration-hover: 0.14s; // hover snaps in
|
|
45
|
+
--nova-duration-press: 0.06s; // press — pairs with --nova-scale-press / --nova-filter-press below
|
|
46
|
+
|
|
47
|
+
// Bundled with --nova-duration-press because the canon always cites them
|
|
48
|
+
// together ("press .06s + scale(.985) + filter:brightness(.96)") — without
|
|
49
|
+
// these two, every pressable component would still hand-roll the literal.
|
|
50
|
+
--nova-scale-press: 0.985;
|
|
51
|
+
--nova-filter-press: brightness(0.96);
|
|
52
|
+
|
|
53
|
+
// ── drawerIn direction ──────────────────────────────────────────────────────
|
|
54
|
+
// The design is LTR-authored (drawer travels in from +28px, i.e. the inline-
|
|
55
|
+
// end edge). A `@keyframes` block cannot itself be direction-aware — CSS
|
|
56
|
+
// keyframes carry no selector — so the keyframe below animates this custom
|
|
57
|
+
// property instead of a literal offset, and ONLY this property flips under
|
|
58
|
+
// RTL (see the `[dir='rtl']` override immediately after this block). Custom
|
|
59
|
+
// properties inherit through Angular's emulated-encapsulation boundary even
|
|
60
|
+
// though selectors don't (that boundary is what makes the `[dir=rtl] &`
|
|
61
|
+
// combinator dead per the program's RTL invariant) — so a consumer gets the
|
|
62
|
+
// correct edge for free. See the handoff note for the full rationale.
|
|
63
|
+
--nova-drawer-in-x: 28px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The ONLY direction-aware rule in this file. It lives here — global,
|
|
67
|
+
// unencapsulated CSS — specifically so no consumer of `drawerIn` ever needs
|
|
68
|
+
// its own `[dir=rtl]`/`:host-context([dir=rtl])` selector. `[dir='rtl']`
|
|
69
|
+
// matches `<html dir="rtl">` (the shell/design-lab convention) and anything
|
|
70
|
+
// beneath it inherits the flipped value.
|
|
71
|
+
[dir="rtl"] {
|
|
72
|
+
--nova-drawer-in-x: -28px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Keyframe inventory (§5 table, 16 names) ──────────────────────────────────
|
|
76
|
+
// Each keyframe defines ONLY the shape (from/to transform + opacity). Duration,
|
|
77
|
+
// easing and iteration-count are the caller's `animation:` shorthand — reach
|
|
78
|
+
// for the tokens above, never a raw cubic-bezier.
|
|
79
|
+
|
|
80
|
+
// Dropdowns/dialogs. `.24-.28s` structural, `both`.
|
|
81
|
+
@keyframes menuIn {
|
|
82
|
+
from {
|
|
83
|
+
opacity: 0;
|
|
84
|
+
transform: translate3d(0, -5px, 0) scale(0.965);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
to {
|
|
88
|
+
opacity: 1;
|
|
89
|
+
transform: translate3d(0, 0, 0) scale(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Centered popovers — same motion as menuIn, composed with the static
|
|
94
|
+
// `translateX(-50%)` centering offset so the animation doesn't fight it.
|
|
95
|
+
@keyframes menuInCentered {
|
|
96
|
+
from {
|
|
97
|
+
opacity: 0;
|
|
98
|
+
transform: translate3d(-50%, -5px, 0) scale(0.965);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
to {
|
|
102
|
+
opacity: 1;
|
|
103
|
+
transform: translate3d(-50%, 0, 0) scale(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Right/end-edge drawers. `.34s` structural. See --nova-drawer-in-x above.
|
|
108
|
+
@keyframes drawerIn {
|
|
109
|
+
from {
|
|
110
|
+
opacity: 0;
|
|
111
|
+
transform: translate3d(var(--nova-drawer-in-x), 0, 0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
to {
|
|
115
|
+
opacity: 1;
|
|
116
|
+
transform: translate3d(0, 0, 0);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Scrims. `.2s` micro.
|
|
121
|
+
@keyframes fadeIn {
|
|
122
|
+
from {
|
|
123
|
+
opacity: 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
to {
|
|
127
|
+
opacity: 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Staggered content reveal (cards) — 10px rise. `.3-.44s`, cards stagger
|
|
132
|
+
// `i*55ms` via nova-stagger below.
|
|
133
|
+
@keyframes riseIn {
|
|
134
|
+
from {
|
|
135
|
+
opacity: 0;
|
|
136
|
+
transform: translate3d(0, 10px, 0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
to {
|
|
140
|
+
opacity: 1;
|
|
141
|
+
transform: translate3d(0, 0, 0);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// View-level entry (whole panes/routes) — 8px rise, `.4s`, no stagger (one
|
|
146
|
+
// view enters at a time).
|
|
147
|
+
@keyframes viewIn {
|
|
148
|
+
from {
|
|
149
|
+
opacity: 0;
|
|
150
|
+
transform: translate3d(0, 8px, 0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
to {
|
|
154
|
+
opacity: 1;
|
|
155
|
+
transform: translate3d(0, 0, 0);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// List/row items — 6px rise, `.4s`, items stagger `i*40ms` via nova-stagger.
|
|
160
|
+
@keyframes itemIn {
|
|
161
|
+
from {
|
|
162
|
+
opacity: 0;
|
|
163
|
+
transform: translate3d(0, 6px, 0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
to {
|
|
167
|
+
opacity: 1;
|
|
168
|
+
transform: translate3d(0, 0, 0);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Contextual toolbar actions. `.3s` structural, 40ms stagger per action.
|
|
173
|
+
@keyframes toolbarPopIn {
|
|
174
|
+
from {
|
|
175
|
+
opacity: 0;
|
|
176
|
+
transform: scale(0.8) translateX(6px);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
to {
|
|
180
|
+
opacity: 1;
|
|
181
|
+
transform: scale(1) translateX(0);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// App-launcher tiles — 6px rise, `.3s` micro, `i*20ms` stagger.
|
|
186
|
+
@keyframes appTileIn {
|
|
187
|
+
from {
|
|
188
|
+
opacity: 0;
|
|
189
|
+
transform: translate3d(0, 6px, 0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
to {
|
|
193
|
+
opacity: 1;
|
|
194
|
+
transform: translate3d(0, 0, 0);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Icon-button hover. `.5s ease` (not one of the four families — a wiggle, not
|
|
199
|
+
// a settle).
|
|
200
|
+
@keyframes iconWiggle {
|
|
201
|
+
0%,
|
|
202
|
+
100% {
|
|
203
|
+
transform: rotate(0deg);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
25% {
|
|
207
|
+
transform: rotate(-8deg);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
75% {
|
|
211
|
+
transform: rotate(8deg);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Icon-button press. `.38s` overshoot (--nova-ease-overshoot).
|
|
216
|
+
@keyframes iconBounce {
|
|
217
|
+
0% {
|
|
218
|
+
transform: scale(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
35% {
|
|
222
|
+
transform: scale(0.78);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
70% {
|
|
226
|
+
transform: scale(1.16);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
100% {
|
|
230
|
+
transform: scale(1);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// AI quick-command chips. 4s linear infinite. The design file hardcodes the
|
|
235
|
+
// third stop as the dark-only literal `#BF5AF2` (skill §10 known bug) — this
|
|
236
|
+
// substitutes the ratified `--sys-purple` token (S1.1's `_nova-tokens.scss`;
|
|
237
|
+
// dark `#a48ce0` / light `#6A3FC0`) so the cycle stays correct in light theme
|
|
238
|
+
// too. See the handoff note.
|
|
239
|
+
@keyframes strokeCycle {
|
|
240
|
+
0%,
|
|
241
|
+
100% {
|
|
242
|
+
border-color: var(--accent);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
33% {
|
|
246
|
+
border-color: var(--sys-teal);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
66% {
|
|
250
|
+
border-color: var(--sys-purple);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// AI orb conic-gradient rotation. 6s (bar orb) / 14s (aside orb), both linear
|
|
255
|
+
// — duration is the caller's, this is just one full turn.
|
|
256
|
+
@keyframes novaSpin {
|
|
257
|
+
to {
|
|
258
|
+
transform: rotate(360deg);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// AI orb glow pulse. 7s ease-in-out. The design file hardcodes the two glow
|
|
263
|
+
// stops as dark-only literals (`rgb(80 150 255 / 40%)` → `rgb(140 120 255 /
|
|
264
|
+
// 75%)`) — skill §10 lists the orb literals as "tokenize at port time", the same
|
|
265
|
+
// known bug `strokeCycle` above already resolves. Substituted here for the
|
|
266
|
+
// nearest ratified tokens on the orb's own identity gradient (`--accent` →
|
|
267
|
+
// `--sys-teal` → `--sys-purple`): the cool stop is `--sys-blue`, the warm stop
|
|
268
|
+
// `--sys-purple`. `color-mix` carries the authored 40%/75% alphas forward
|
|
269
|
+
// unchanged, so the pulse keeps its shape while both stops now flip with theme
|
|
270
|
+
// instead of glowing dark-blue over light paper.
|
|
271
|
+
@keyframes novaBreathe {
|
|
272
|
+
0%,
|
|
273
|
+
100% {
|
|
274
|
+
box-shadow: 0 0 16px color-mix(in oklab, var(--sys-blue) 40%, transparent);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
50% {
|
|
278
|
+
box-shadow: 0 0 30px color-mix(in oklab, var(--sys-purple) 75%, transparent);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Light-mode material sheen. 22s.
|
|
283
|
+
@keyframes glassSheen {
|
|
284
|
+
0%,
|
|
285
|
+
100% {
|
|
286
|
+
background-position: 0% 0%;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
50% {
|
|
290
|
+
background-position: 100% 0%;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Gradient greeting/headline text. 11s.
|
|
295
|
+
@keyframes greetShift {
|
|
296
|
+
0%,
|
|
297
|
+
100% {
|
|
298
|
+
background-position: 0% 0%;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
50% {
|
|
302
|
+
background-position: 100% 0%;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── prefers-reduced-motion ───────────────────────────────────────────────────
|
|
307
|
+
// THE single block for the whole design language — every animation/transition
|
|
308
|
+
// in the app, Nova or otherwise, is covered here. Components never repeat
|
|
309
|
+
// this. (The shell's `src/styles/_accessibility.scss` carries an identical
|
|
310
|
+
// block for the shell's own bundle; this one makes the design-system package
|
|
311
|
+
// self-sufficient for any consumer — federated remote, standalone Business
|
|
312
|
+
// App, External App — that pulls in `_fly-theme.scss` without the shell's
|
|
313
|
+
// stylesheet. Both are idempotent; there is no conflict in applying both.)
|
|
314
|
+
@media (prefers-reduced-motion: reduce) {
|
|
315
|
+
*,
|
|
316
|
+
*::before,
|
|
317
|
+
*::after {
|
|
318
|
+
/* stylelint-disable declaration-no-important */
|
|
319
|
+
animation-duration: 0.01ms !important;
|
|
320
|
+
animation-iteration-count: 1 !important;
|
|
321
|
+
transition-duration: 0.01ms !important;
|
|
322
|
+
/* stylelint-enable declaration-no-important */
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// ─── Nova token layer (UX v2) ────────────────────────────────────────────────
|
|
2
|
+
// The Nova design language's colour vocabulary, both themes. Landed by Stage-1
|
|
3
|
+
// task S1.1 of the UX-refresh program.
|
|
4
|
+
|
|
5
|
+
// Canon: `skills/desktop-design-language.md` §2 (ratified table) and §3 (the
|
|
6
|
+
// ramp law), which in turn cite `.workflow/plans/ux-refresh/analysis/
|
|
7
|
+
// D3-styleguide-tokens.md`. NEVER re-derive a value from the `UX/**/*.dc.html`
|
|
8
|
+
// prototypes — the prototypes drift, the skill is the contract.
|
|
9
|
+
|
|
10
|
+
// ── Additive only ────────────────────────────────────────────────────────────
|
|
11
|
+
// Nothing here re-values a token the shipped DS already defines; every name
|
|
12
|
+
// below is greenfield. `nova-tokens.spec.ts` proves it by parsing the other
|
|
13
|
+
// theme partials, so this stays true as the DS evolves. Two consequences of the
|
|
14
|
+
// D1 collision ruling that look like omissions and are not:
|
|
15
|
+
// · The design's `--glass-bg/-border/-inset/-blur` land as `--glass2-*`. The
|
|
16
|
+
// shipped `--glass-*` family has a different value SHAPE (simple fill vs a
|
|
17
|
+
// 3-layer background-image), so loading design values into those names
|
|
18
|
+
// would silently restyle every current component.
|
|
19
|
+
// · The design's `--ink` gets NO token at all. It folds into the existing
|
|
20
|
+
// `--text-color` / `--label-primary` semantics at the 2.0 flip.
|
|
21
|
+
// · `--accent` is NOT declared here. It is the existing runtime knob
|
|
22
|
+
// (`#9333EA`, re-skinnable via `FlyThemeService.applyAccent`) and is
|
|
23
|
+
// re-valued only at the DS 2.0 flip. `--accent-hi` IS new vocabulary and
|
|
24
|
+
// ships now, pinned to its 2.0 partner — so in 1.x it is deliberately
|
|
25
|
+
// consumed by nothing. `/design-lab` → "Tokens & Ramp" previews the pair
|
|
26
|
+
// together by scoping both to the panel.
|
|
27
|
+
|
|
28
|
+
// ── Notation ─────────────────────────────────────────────────────────────────
|
|
29
|
+
// The ratified table is written in the design files' legacy `rgba(r,g,b,.a)`
|
|
30
|
+
// form; this file carries the same NUMBERS in the repo's modern `rgb(r g b /
|
|
31
|
+
// n%)` form, because stylelint (`color-function-notation` + `alpha-value-
|
|
32
|
+
// notation`, via stylelint-config-standard-scss) rejects the legacy form.
|
|
33
|
+
// `nova-tokens.spec.ts` compares notation-normalised, so a number that drifts
|
|
34
|
+
// still fails even though the spelling differs.
|
|
35
|
+
|
|
36
|
+
// ── Light ─────────────────────────────────────────────────────────────────────
|
|
37
|
+
// Selector pair matches `_theme-light.scss`: FlyOS keys themes on
|
|
38
|
+
// `html.light-theme` / `html.dark-theme`, NOT on the design files'
|
|
39
|
+
// `[data-theme]`. Do not introduce `[data-theme]` here.
|
|
40
|
+
:root,
|
|
41
|
+
html.light-theme {
|
|
42
|
+
// ── Chrome + nav rail ──────────────────────────────────────────────────────
|
|
43
|
+
--chrome-ink: #353537;
|
|
44
|
+
--chrome-ink-hover: #000;
|
|
45
|
+
--rail-ink: #353537;
|
|
46
|
+
--rail-stroke: rgb(0 0 0 / 7%);
|
|
47
|
+
--rail-on-bg: rgb(133 88 224 / 16%);
|
|
48
|
+
--rail-on-ink: #6A3FC0;
|
|
49
|
+
--rail-on-stroke: rgb(133 88 224 / 26%);
|
|
50
|
+
--rail-on-shadow: 0 2px 6px rgb(106 63 192 / 12%);
|
|
51
|
+
|
|
52
|
+
// Hover/selection are a TINT, never an inversion (skill §9 rule 11).
|
|
53
|
+
--tint-hover: rgb(133 88 224 / 10%);
|
|
54
|
+
--tint-sel: rgb(133 88 224 / 15%);
|
|
55
|
+
|
|
56
|
+
// The 2.0 accent's hover/pressed partner. Its `--accent` sibling stays the
|
|
57
|
+
// shipped runtime knob until the flip — see the header.
|
|
58
|
+
--accent-hi: #5A32A8;
|
|
59
|
+
|
|
60
|
+
// ── System semantics ───────────────────────────────────────────────────────
|
|
61
|
+
// `--sys-red-txt` exists because `--sys-red` fails AA as dark-theme text
|
|
62
|
+
// (4.16:1). No `-txt` variant exists yet for blue/orange/teal; those fixes
|
|
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.
|
|
90
|
+
--sys-red: #C2263A;
|
|
91
|
+
--sys-red-txt: #B32436;
|
|
92
|
+
--sys-orange: #9A5A0F;
|
|
93
|
+
--sys-blue: #1667B8;
|
|
94
|
+
--sys-teal: #1F6E7C;
|
|
95
|
+
--sys-green: #1B7355;
|
|
96
|
+
--sys-purple: #6A3FC0;
|
|
97
|
+
|
|
98
|
+
// Brushed-steel wash. Light-only in the design; the dark block declares it
|
|
99
|
+
// `none` so a consumer never needs `var(--steel-sheen, …)` — a hex fallback
|
|
100
|
+
// is banned outright (skill §9 rule 3).
|
|
101
|
+
--steel-sheen: linear-gradient(
|
|
102
|
+
112deg,
|
|
103
|
+
rgb(203 213 225 / 30%) 0%,
|
|
104
|
+
rgb(241 245 249 / 40%) 22%,
|
|
105
|
+
rgb(191 203 217 / 28%) 44%,
|
|
106
|
+
rgb(236 241 247 / 38%) 66%,
|
|
107
|
+
rgb(198 209 223 / 30%) 100%
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// ── Ink ramp — TEXT tier ───────────────────────────────────────────────────
|
|
111
|
+
// The light ramp is a two-tier PERCEPTUAL REMAP, not an alpha mirror of dark.
|
|
112
|
+
// This tier is compressed upward into 65–95% black so every text step keeps
|
|
113
|
+
// AA on light paper. Never lerp it back toward the dark alphas.
|
|
114
|
+
|
|
115
|
+
// Naming hazard (skill §3.3): the digits after `w` are the decimal fraction —
|
|
116
|
+
// `--w1` is .10, `--w15` is .15, `--w015` is .015. Pick a step BY NAME; never
|
|
117
|
+
// mint a new rgba.
|
|
118
|
+
--w95: rgb(0 0 0 / 95%);
|
|
119
|
+
--w9: rgb(0 0 0 / 92%);
|
|
120
|
+
--w85: rgb(0 0 0 / 90%);
|
|
121
|
+
--w8: rgb(0 0 0 / 88%);
|
|
122
|
+
--w78: rgb(0 0 0 / 87%);
|
|
123
|
+
--w75: rgb(0 0 0 / 85%);
|
|
124
|
+
--w72: rgb(0 0 0 / 84%);
|
|
125
|
+
--w7: rgb(0 0 0 / 83%);
|
|
126
|
+
--w65: rgb(0 0 0 / 80%);
|
|
127
|
+
--w6: rgb(0 0 0 / 78%);
|
|
128
|
+
--w55: rgb(0 0 0 / 76%);
|
|
129
|
+
--w5: rgb(0 0 0 / 74%);
|
|
130
|
+
--w45: rgb(0 0 0 / 72%);
|
|
131
|
+
|
|
132
|
+
// `--w42` ≡ `--w4` here (both 70%) — an intentional closest-step collision on
|
|
133
|
+
// the light side, not a copy-paste bug. Same for `--w14` ≡ `--w15` below.
|
|
134
|
+
--w42: rgb(0 0 0 / 70%);
|
|
135
|
+
--w4: rgb(0 0 0 / 70%);
|
|
136
|
+
--w35: rgb(0 0 0 / 68%);
|
|
137
|
+
--w3: rgb(0 0 0 / 66%);
|
|
138
|
+
--w28: rgb(0 0 0 / 65%);
|
|
139
|
+
|
|
140
|
+
// ── Ink ramp — STRUCTURE tier ──────────────────────────────────────────────
|
|
141
|
+
// The hard discontinuity between `--w28` (65%) and `--w22` (15%) IS the
|
|
142
|
+
// accessibility mechanism: text stays legible, hairlines stay faint. Do not
|
|
143
|
+
// "smooth" it.
|
|
144
|
+
--w22: rgb(0 0 0 / 15%);
|
|
145
|
+
--w2: rgb(0 0 0 / 14%);
|
|
146
|
+
--w18: rgb(0 0 0 / 13%);
|
|
147
|
+
--w16: rgb(0 0 0 / 12%);
|
|
148
|
+
--w15: rgb(0 0 0 / 11%);
|
|
149
|
+
--w14: rgb(0 0 0 / 11%);
|
|
150
|
+
--w12: rgb(0 0 0 / 10%);
|
|
151
|
+
--w1: rgb(0 0 0 / 9%);
|
|
152
|
+
--w09: rgb(0 0 0 / 8%);
|
|
153
|
+
--w08: rgb(0 0 0 / 7.5%);
|
|
154
|
+
--w075: rgb(0 0 0 / 7%);
|
|
155
|
+
--w07: rgb(0 0 0 / 6.5%);
|
|
156
|
+
--w06: rgb(0 0 0 / 6%);
|
|
157
|
+
--w055: rgb(0 0 0 / 5.5%);
|
|
158
|
+
--w05: rgb(0 0 0 / 5%);
|
|
159
|
+
--w045: rgb(0 0 0 / 4.5%);
|
|
160
|
+
--w04: rgb(0 0 0 / 4%);
|
|
161
|
+
--w035: rgb(0 0 0 / 3.5%);
|
|
162
|
+
--w03: rgb(0 0 0 / 3%);
|
|
163
|
+
--w02: rgb(0 0 0 / 2.2%);
|
|
164
|
+
--w015: rgb(0 0 0 / 1.8%);
|
|
165
|
+
|
|
166
|
+
// ── Materials ──────────────────────────────────────────────────────────────
|
|
167
|
+
// Each material is a two-part token on purpose: a per-material alpha knob and
|
|
168
|
+
// a colour composed from it. `WallpaperGlassService` already samples the
|
|
169
|
+
// wallpaper's luminance and writes `--fly-glass-alpha` at the document root;
|
|
170
|
+
// S2.10 re-anchors these materials the same way by writing ONE number per
|
|
171
|
+
// material, with no colour string to rewrite. The composed values are
|
|
172
|
+
// numerically identical to the ratified literals.
|
|
173
|
+
|
|
174
|
+
// `--mat-header` is not consumed by the design's own header (header and rail
|
|
175
|
+
// both use `--mat-rail`; the values are identical today). Keep both — do not
|
|
176
|
+
// "clean up" the unused one.
|
|
177
|
+
--mat-header-alpha: 0.38;
|
|
178
|
+
--mat-header: rgb(226 232 240 / calc(var(--mat-header-alpha) * 100%));
|
|
179
|
+
--mat-rail-alpha: 0.38;
|
|
180
|
+
--mat-rail: rgb(226 232 240 / calc(var(--mat-rail-alpha) * 100%));
|
|
181
|
+
--mat-panel-alpha: 0.46;
|
|
182
|
+
--mat-panel: rgb(230 236 243 / calc(var(--mat-panel-alpha) * 100%));
|
|
183
|
+
--mat-menu-a-alpha: 0.58;
|
|
184
|
+
--mat-menu-a: rgb(238 242 247 / calc(var(--mat-menu-a-alpha) * 100%));
|
|
185
|
+
--mat-menu-b-alpha: 0.64;
|
|
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%));
|
|
206
|
+
--mat-tip-a-alpha: 0.8;
|
|
207
|
+
--mat-tip-a: rgb(238 241 245 / calc(var(--mat-tip-a-alpha) * 100%));
|
|
208
|
+
--mat-tip-b-alpha: 0.86;
|
|
209
|
+
--mat-tip-b: rgb(222 227 234 / calc(var(--mat-tip-b-alpha) * 100%));
|
|
210
|
+
|
|
211
|
+
// ── Accent ink ─────────────────────────────────────────────────────────────
|
|
212
|
+
--on-accent: #fff;
|
|
213
|
+
--check-ink: #fff;
|
|
214
|
+
|
|
215
|
+
// ── Glass quartet (design `--glass-*`, renamed per D1) ─────────────────────
|
|
216
|
+
// ⚠ The light `--glass2-bg` carries the DARK near-opaque fill
|
|
217
|
+
// (`rgb(56 54 60 / 95%)`) — reproduced verbatim from the canon. In the mock,
|
|
218
|
+
// header/rail-anchored popovers therefore render dark glass even in light
|
|
219
|
+
// mode, and the carefully-picked light `--mat-menu-*` values go unused.
|
|
220
|
+
// Intent-vs-bug is an OPEN question with UX (S0.2 memo; skill §2 ⚠ and §10).
|
|
221
|
+
// Do NOT silently "fix" it here — the ruling changes this one value, and the
|
|
222
|
+
// port replaces the mock's fragile `nth-child` fill override with an explicit
|
|
223
|
+
// surface-context class.
|
|
224
|
+
--glass2-bg:
|
|
225
|
+
radial-gradient(70% 60% at 50% 100%, var(--w07), transparent 70%),
|
|
226
|
+
linear-gradient(180deg, var(--w08), transparent 20%),
|
|
227
|
+
linear-gradient(rgb(56 54 60 / 95%), rgb(56 54 60 / 95%));
|
|
228
|
+
--glass2-border: var(--w18);
|
|
229
|
+
--glass2-inset: inset 0 0.5px 0 var(--w22);
|
|
230
|
+
--glass2-blur: blur(16px) saturate(180%) brightness(.95);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Dark ──────────────────────────────────────────────────────────────────────
|
|
234
|
+
// Selector matches `_theme-dark.scss`.
|
|
235
|
+
html.dark-theme {
|
|
236
|
+
// ── Chrome + nav rail ──────────────────────────────────────────────────────
|
|
237
|
+
--chrome-ink: rgb(255 255 255 / 65%);
|
|
238
|
+
--chrome-ink-hover: #fff;
|
|
239
|
+
--rail-ink: rgb(255 255 255 / 65%);
|
|
240
|
+
--rail-stroke: rgb(255 255 255 / 12%);
|
|
241
|
+
--rail-on-bg: #836aca;
|
|
242
|
+
--rail-on-ink: #fff;
|
|
243
|
+
--rail-on-stroke: rgb(255 255 255 / 28%);
|
|
244
|
+
--rail-on-shadow: 0 8px 22px rgb(131 106 202 / 45%);
|
|
245
|
+
--tint-hover: rgb(131 106 202 / 26%);
|
|
246
|
+
--tint-sel: rgb(131 106 202 / 30%);
|
|
247
|
+
--accent-hi: #7259bd;
|
|
248
|
+
|
|
249
|
+
// ── System semantics ───────────────────────────────────────────────────────
|
|
250
|
+
--sys-red: #FF453A;
|
|
251
|
+
--sys-red-txt: #FF6961;
|
|
252
|
+
--sys-orange: #FF9F0A;
|
|
253
|
+
--sys-blue: #0A84FF;
|
|
254
|
+
--sys-teal: #64D2FF;
|
|
255
|
+
--sys-green: #4dd6a8;
|
|
256
|
+
--sys-purple: #a48ce0;
|
|
257
|
+
|
|
258
|
+
// Light-only in the design — declared as a no-op here so consumers can read
|
|
259
|
+
// it unconditionally in either theme.
|
|
260
|
+
--steel-sheen: none;
|
|
261
|
+
|
|
262
|
+
// ── Ink ramp — TEXT tier ───────────────────────────────────────────────────
|
|
263
|
+
// White-over-dark. This is the tier the light block remaps; the two are NOT
|
|
264
|
+
// mirrors of each other and must never be generated from one another.
|
|
265
|
+
--w95: rgb(255 255 255 / 95%);
|
|
266
|
+
--w9: rgb(255 255 255 / 90%);
|
|
267
|
+
--w85: rgb(255 255 255 / 85%);
|
|
268
|
+
--w8: rgb(255 255 255 / 80%);
|
|
269
|
+
--w78: rgb(255 255 255 / 78%);
|
|
270
|
+
--w75: rgb(255 255 255 / 75%);
|
|
271
|
+
--w72: rgb(255 255 255 / 72%);
|
|
272
|
+
--w7: rgb(255 255 255 / 70%);
|
|
273
|
+
--w65: rgb(255 255 255 / 65%);
|
|
274
|
+
--w6: rgb(255 255 255 / 60%);
|
|
275
|
+
--w55: rgb(255 255 255 / 55%);
|
|
276
|
+
--w5: rgb(255 255 255 / 50%);
|
|
277
|
+
--w45: rgb(255 255 255 / 45%);
|
|
278
|
+
--w42: rgb(255 255 255 / 42%);
|
|
279
|
+
--w4: rgb(255 255 255 / 40%);
|
|
280
|
+
--w35: rgb(255 255 255 / 35%);
|
|
281
|
+
--w3: rgb(255 255 255 / 30%);
|
|
282
|
+
--w28: rgb(255 255 255 / 28%);
|
|
283
|
+
|
|
284
|
+
// ── Ink ramp — STRUCTURE tier ──────────────────────────────────────────────
|
|
285
|
+
// Dark's own tiers are continuous (28% → 22%); only the light ramp breaks.
|
|
286
|
+
--w22: rgb(255 255 255 / 22%);
|
|
287
|
+
--w2: rgb(255 255 255 / 20%);
|
|
288
|
+
--w18: rgb(255 255 255 / 18%);
|
|
289
|
+
--w16: rgb(255 255 255 / 16%);
|
|
290
|
+
--w15: rgb(255 255 255 / 15%);
|
|
291
|
+
--w14: rgb(255 255 255 / 14%);
|
|
292
|
+
--w12: rgb(255 255 255 / 12%);
|
|
293
|
+
--w1: rgb(255 255 255 / 10%);
|
|
294
|
+
--w09: rgb(255 255 255 / 9%);
|
|
295
|
+
--w08: rgb(255 255 255 / 8%);
|
|
296
|
+
--w075: rgb(255 255 255 / 7.5%);
|
|
297
|
+
--w07: rgb(255 255 255 / 7%);
|
|
298
|
+
--w06: rgb(255 255 255 / 6%);
|
|
299
|
+
--w055: rgb(255 255 255 / 5.5%);
|
|
300
|
+
--w05: rgb(255 255 255 / 5%);
|
|
301
|
+
--w045: rgb(255 255 255 / 4.5%);
|
|
302
|
+
--w04: rgb(255 255 255 / 4%);
|
|
303
|
+
--w035: rgb(255 255 255 / 3.5%);
|
|
304
|
+
--w03: rgb(255 255 255 / 3%);
|
|
305
|
+
--w02: rgb(255 255 255 / 2%);
|
|
306
|
+
--w015: rgb(255 255 255 / 1.5%);
|
|
307
|
+
|
|
308
|
+
// ── Materials ──────────────────────────────────────────────────────────────
|
|
309
|
+
// Same two-part shape as the light block. The menu gradient stops are opaque
|
|
310
|
+
// in this theme (`#201f25` / `#1a191e`); they are expressed as alpha-1 rgb so
|
|
311
|
+
// S2.10 can re-anchor every material through one uniform knob.
|
|
312
|
+
--mat-header-alpha: 0.38;
|
|
313
|
+
--mat-header: rgb(48 46 52 / calc(var(--mat-header-alpha) * 100%));
|
|
314
|
+
--mat-rail-alpha: 0.38;
|
|
315
|
+
--mat-rail: rgb(48 46 52 / calc(var(--mat-rail-alpha) * 100%));
|
|
316
|
+
--mat-panel-alpha: 0.55;
|
|
317
|
+
--mat-panel: rgb(56 54 60 / calc(var(--mat-panel-alpha) * 100%));
|
|
318
|
+
--mat-menu-a-alpha: 1;
|
|
319
|
+
--mat-menu-a: rgb(32 31 37 / calc(var(--mat-menu-a-alpha) * 100%));
|
|
320
|
+
--mat-menu-b-alpha: 1;
|
|
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%));
|
|
338
|
+
--mat-tip-a-alpha: 0.82;
|
|
339
|
+
--mat-tip-a: rgb(46 44 52 / calc(var(--mat-tip-a-alpha) * 100%));
|
|
340
|
+
--mat-tip-b-alpha: 0.86;
|
|
341
|
+
--mat-tip-b: rgb(40 38 46 / calc(var(--mat-tip-b-alpha) * 100%));
|
|
342
|
+
|
|
343
|
+
// ── Accent ink ─────────────────────────────────────────────────────────────
|
|
344
|
+
--on-accent: #fff;
|
|
345
|
+
--check-ink: #0f1013;
|
|
346
|
+
|
|
347
|
+
// ── Glass quartet ──────────────────────────────────────────────────────────
|
|
348
|
+
// Identical to the light block by design — see the ⚠ there.
|
|
349
|
+
--glass2-bg:
|
|
350
|
+
radial-gradient(70% 60% at 50% 100%, var(--w07), transparent 70%),
|
|
351
|
+
linear-gradient(180deg, var(--w08), transparent 20%),
|
|
352
|
+
linear-gradient(rgb(56 54 60 / 95%), rgb(56 54 60 / 95%));
|
|
353
|
+
--glass2-border: var(--w18);
|
|
354
|
+
--glass2-inset: inset 0 0.5px 0 var(--w22);
|
|
355
|
+
--glass2-blur: blur(16px) saturate(180%) brightness(.95);
|
|
356
|
+
}
|