@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,451 @@
|
|
|
1
|
+
// ─── Nova glass recipes (UX v2) ──────────────────────────────────────────────
|
|
2
|
+
// Stage 1 task S1.2 of the UX-refresh program. Canon: `skills/desktop-design-
|
|
3
|
+
// language.md` §4, which cites `.workflow/plans/ux-refresh/analysis/
|
|
4
|
+
// D1-home-desktop.md` §4 and `D3-styleguide-tokens.md` §1.6.
|
|
5
|
+
|
|
6
|
+
// Mixins only — this partial emits NO CSS when merely `@use`d, so components can
|
|
7
|
+
// `@use` it freely without duplicating rules. That is also why `_fly-theme.scss`
|
|
8
|
+
// does NOT `@use` it: there would be nothing to emit.
|
|
9
|
+
|
|
10
|
+
// ── Canonical import ─────────────────────────────────────────────────────────
|
|
11
|
+
// @use 'nova-glass' as glass;
|
|
12
|
+
// .fly-menu { @include glass.popover; }
|
|
13
|
+
// .fly-drawer { @include glass.popover($shadow: drawer, $surface: panel); }
|
|
14
|
+
// .app-panel { @include glass.panel; }
|
|
15
|
+
// .shell-header { @include glass.chrome; }
|
|
16
|
+
// Note the argumentless form has no parentheses — stylelint's
|
|
17
|
+
// `scss/at-mixin-argumentless-call-parentheses` rejects `popover()`.
|
|
18
|
+
|
|
19
|
+
// Members are namespaced rather than `nova-`-prefixed (the sibling
|
|
20
|
+
// `_nova-motion.scss` prefixes because `@keyframes` names are GLOBAL and can
|
|
21
|
+
// genuinely collide; Sass mixin names are module-scoped and cannot). Read the
|
|
22
|
+
// call site, not the definition: `glass.popover` already says everything
|
|
23
|
+
// `nova-glass-popover` would, and the namespace is what a reviewer sees.
|
|
24
|
+
|
|
25
|
+
// ── The one rule this file exists to enforce ─────────────────────────────────
|
|
26
|
+
// NEVER hand-compose glass (skill §9 rule 10). Per-component gradient stacks are
|
|
27
|
+
// the drift this program exists to kill. Every sanctioned surface is one of the
|
|
28
|
+
// four public mixins below; there is deliberately NO public way to pass a raw
|
|
29
|
+
// blur radius, a raw shadow, or a fill of your own.
|
|
30
|
+
|
|
31
|
+
// ── backdrop-filter is not a paint-only property (READ THIS) ─────────────────
|
|
32
|
+
// A non-`none` `backdrop-filter` does three structural things to the element,
|
|
33
|
+
// only the first of which is obvious:
|
|
34
|
+
// 1. it creates a STACKING CONTEXT (hence the shell's z-ladder: the header
|
|
35
|
+
// must out-rank content, skill §6);
|
|
36
|
+
// 2. it makes the element a CONTAINING BLOCK for every descendant, including
|
|
37
|
+
// `position: fixed` ones. A descendant that uses `fixed` to escape to the
|
|
38
|
+
// viewport is instead trapped inside the glass surface, and any coordinates
|
|
39
|
+
// computed from `getBoundingClientRect()` land in the wrong place;
|
|
40
|
+
// 3. it makes the element a BACKDROP ROOT. A nested `backdrop-filter` then
|
|
41
|
+
// samples only what is painted INSIDE this element — not the wallpaper — so
|
|
42
|
+
// glass-inside-glass reads flat and muddy. (The shell already knew this:
|
|
43
|
+
// `window/_nova-vibrancy.scss` invented `--surface-overlay-strong` for
|
|
44
|
+
// exactly this case. `$nested: true` below is that knowledge, generalised.)
|
|
45
|
+
|
|
46
|
+
// (2) is load-bearing exactly once — the `material()` rim and sheen are absolute
|
|
47
|
+
// pseudo-elements that resolve against the glass surface without it needing
|
|
48
|
+
// `position: relative`. Everywhere else it is a hazard: see the handoff note
|
|
49
|
+
// `.workflow/plans/ux-refresh/notes/S1.2-glass-mixins.md` §"Containing block"
|
|
50
|
+
// for the full inventory of what may and may not be nested under glass.
|
|
51
|
+
|
|
52
|
+
// ── Vocabulary shared by the recipes ─────────────────────────────────────────
|
|
53
|
+
// Private (Sass makes a leading `_` member inaccessible from other modules), so
|
|
54
|
+
// no consumer can reach a half-recipe: the ONLY way to get `backdrop-filter` out
|
|
55
|
+
// of this file is through a public mixin, and every public mixin carries the
|
|
56
|
+
// full degradation ladder. That is the mechanism, not a convention.
|
|
57
|
+
|
|
58
|
+
// The leading underscore IS that mechanism — it is the only privacy marker Sass
|
|
59
|
+
// has — and it is what the kebab-case rules below reject. Scoped off for the
|
|
60
|
+
// three private members only, then back on for the public API.
|
|
61
|
+
// These are Sass line comments, NOT `/* */`: a CSS comment would survive into the
|
|
62
|
+
// compiled output, and this partial's whole contract is that it emits nothing.
|
|
63
|
+
// stylelint-disable scss/at-mixin-pattern, scss/at-function-pattern
|
|
64
|
+
|
|
65
|
+
// The three sanctioned blur tiers (skill §4) — chrome 36 / content panel 72 /
|
|
66
|
+
// thin 16. Selected BY NAME; a fourth tier would have to be added here, in the
|
|
67
|
+
// open, where `nova-glass.spec.ts` counts them.
|
|
68
|
+
@mixin _backdrop($tier) {
|
|
69
|
+
@if $tier == chrome {
|
|
70
|
+
-webkit-backdrop-filter: blur(36px) saturate(180%);
|
|
71
|
+
backdrop-filter: blur(36px) saturate(180%);
|
|
72
|
+
} @else if $tier == panel {
|
|
73
|
+
-webkit-backdrop-filter: blur(72px) saturate(180%);
|
|
74
|
+
backdrop-filter: blur(72px) saturate(180%);
|
|
75
|
+
} @else if $tier == thin {
|
|
76
|
+
// The ratified quartet value: blur(16px) saturate(180%) brightness(.95).
|
|
77
|
+
-webkit-backdrop-filter: var(--glass2-blur);
|
|
78
|
+
backdrop-filter: var(--glass2-blur);
|
|
79
|
+
} @else {
|
|
80
|
+
@error 'nova-glass: unknown blur tier `#{$tier}`. The three sanctioned tiers are `chrome` (36px), `panel` (72px), `thin` (16px) — skill §4 says do not invent a fourth.';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The surface-context seam (skill §2 ⚠, §10).
|
|
85
|
+
// The ratified light `--glass2-bg` carries the DARK near-opaque fill, so a light
|
|
86
|
+
// popover anchored in the header renders as dark glass. In the mock only ONE
|
|
87
|
+
// region escaped that, via a `#dc-root > div > div > div:nth-child(2) > section`
|
|
88
|
+
// structural selector — which is exactly the fragile thing this argument
|
|
89
|
+
// replaces: a caller DECLARES which surface it is and gets the right fill.
|
|
90
|
+
// This is the seam the open UX ruling lands on. If UX rules the light fill is a
|
|
91
|
+
// bug, the fix is one value in `_nova-tokens.scss` and `chrome` inherits it with
|
|
92
|
+
// no change here. Nothing in this file second-guesses the ratified token.
|
|
93
|
+
|
|
94
|
+
// READ THIS BEFORE PICKING `chrome`. As of the S1 review fixes NO shipping
|
|
95
|
+
// component passes it: `fly-context-menu`, `fly-action-menu` and the
|
|
96
|
+
// `fly-select` listbox moved to `menu`, and `fly-drawer` / `fly-modal` /
|
|
97
|
+
// `fly-confirm-dialog` were already on `panel`. Only `/design-lab`'s Glass
|
|
98
|
+
// Recipes preview and `nova-glass.spec.ts` still compile it. It is kept as the
|
|
99
|
+
// DEFAULT on purpose — it is the canon's popover fill (skill §4) and the branch
|
|
100
|
+
// the light-`--glass2-bg` ruling lands on — so the next genuine chrome-anchored
|
|
101
|
+
// popover is the first caller that will feel that ruling, and it should. What it
|
|
102
|
+
// must NOT be used for again is a floating menu in light theme: that pairing put
|
|
103
|
+
// 95 % black ink on a near-opaque #38363C plate at 1.66–1.99:1.
|
|
104
|
+
@function _fill($surface) {
|
|
105
|
+
@if $surface == chrome {
|
|
106
|
+
@return var(--glass2-bg);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// `panel`, `menu` and `agent` keep the quartet's two upper layers (bottom
|
|
110
|
+
// radial glow + top sheen, both composed from ramp steps) and swap ONLY the
|
|
111
|
+
// fill layer.
|
|
112
|
+
$glow: radial-gradient(70% 60% at 50% 100%, var(--w07), transparent 70%);
|
|
113
|
+
$sheen: linear-gradient(180deg, var(--w08), transparent 20%);
|
|
114
|
+
|
|
115
|
+
@if $surface == panel {
|
|
116
|
+
// Theme-aware translucency — the fill the light theme's `--mat-panel` was
|
|
117
|
+
// picked for and, today, the only place those values are used at all.
|
|
118
|
+
@return $glow, $sheen, linear-gradient(var(--mat-panel), var(--mat-panel));
|
|
119
|
+
} @else if $surface == menu {
|
|
120
|
+
// The design's own MENU material, ratified in both themes (skill §2) and
|
|
121
|
+
// consumed by nothing until the S1 review fixes — `_nova-tokens.scss` says
|
|
122
|
+
// as much in its ⚠, where the unused `--mat-menu-*` values are listed as
|
|
123
|
+
// collateral of the light `--glass2-bg` anomaly. A two-stop vertical
|
|
124
|
+
// gradient because the pair IS a gradient (`-a` top, `-b` bottom), the same
|
|
125
|
+
// shape `--mat-tip-a`/`-b` carry for tooltips.
|
|
126
|
+
|
|
127
|
+
// This branch exists because a floating menu is neither chrome nor a panel.
|
|
128
|
+
// `chrome` is unreadable in light theme (above). `panel` fixes light but is
|
|
129
|
+
// translucent in BOTH themes, so it inherits its backdrop: a dark-theme menu
|
|
130
|
+
// over a light wallpaper falls to 3.23:1 — AA failure for primary ink on the
|
|
131
|
+
// three most-used overlays in the system. `--mat-menu-*` is ALPHA-1 in dark,
|
|
132
|
+
// so this fill does not vary with the wallpaper there at all (14.85:1), and
|
|
133
|
+
// light lands at 17.44:1. Measured table:
|
|
134
|
+
// `.workflow/plans/ux-refresh/notes/S1-review-fixes-surfaces.md`.
|
|
135
|
+
@return $glow, $sheen, linear-gradient(180deg, var(--mat-menu-a), var(--mat-menu-b));
|
|
136
|
+
} @else if $surface == agent {
|
|
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));
|
|
148
|
+
}
|
|
149
|
+
|
|
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).';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── The degradation ladder (skill §4 — non-negotiable) ───────────────────────
|
|
154
|
+
// Every public recipe ends with this. A component must never have to remember an
|
|
155
|
+
// accessibility fallback, so the ladder ships WITH the glass or the glass does
|
|
156
|
+
// not ship. `nova-glass.spec.ts` fails the build if a recipe is added without
|
|
157
|
+
// it — that is the rule that stops the ladder rotting as recipes multiply.
|
|
158
|
+
// The reduced-transparency flat fills are theme-dependent (`#2c2c2e` dark /
|
|
159
|
+
// `#eef2f8` light) and are resolved with `light-dark()`, NOT a theme selector.
|
|
160
|
+
// `html.light-theme` / `html.dark-theme` each set `color-scheme` (see
|
|
161
|
+
// `_theme-light.scss` / `_theme-dark.scss`, and `native-select.spec.ts` which
|
|
162
|
+
// forbids re-pinning it locally), so `light-dark()` follows the APP theme rather
|
|
163
|
+
// than the OS. It is also the only mechanism that survives Angular's emulated
|
|
164
|
+
// encapsulation: `html.dark-theme &` compiles to `html.dark-theme[_ngcontent-x]`
|
|
165
|
+
// and is dead, and a component-local theme branch is banned outright (skill §9
|
|
166
|
+
// rule 4). One value, no selector, correct in both themes.
|
|
167
|
+
@mixin _degrade {
|
|
168
|
+
// Transparency off: a flat, fully opaque plate. Killing only the blur would
|
|
169
|
+
// leave the translucent fill, which is the thing that hurts.
|
|
170
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
171
|
+
& {
|
|
172
|
+
border-color: transparent;
|
|
173
|
+
background-color: light-dark(#eef2f8, #2c2c2e);
|
|
174
|
+
background-image: none;
|
|
175
|
+
box-shadow: none;
|
|
176
|
+
-webkit-backdrop-filter: none;
|
|
177
|
+
backdrop-filter: none;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// The refractive rim and specular sheen ARE the transparency effect.
|
|
181
|
+
&::before,
|
|
182
|
+
&::after {
|
|
183
|
+
display: none;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// The glass hairline is decorative-contrast at best (`--w18` measures 1.79:1
|
|
188
|
+
// against the panel — D3 §contrast). This is the ratified bump.
|
|
189
|
+
@media (prefers-contrast: more) {
|
|
190
|
+
& {
|
|
191
|
+
border-color: var(--w6);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The sheen is the only animation this file starts, and `::after` is the only
|
|
196
|
+
// pseudo it animates. `_nova-motion.scss` already zeroes every animation
|
|
197
|
+
// globally under this query; this block makes the recipe self-sufficient for
|
|
198
|
+
// any consumer that pulls the mixin without the motion partial.
|
|
199
|
+
@media (prefers-reduced-motion: reduce) {
|
|
200
|
+
&::after {
|
|
201
|
+
animation: none;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// stylelint-enable scss/at-mixin-pattern, scss/at-function-pattern
|
|
206
|
+
|
|
207
|
+
// ── Popover composite ────────────────────────────────────────────────────────
|
|
208
|
+
// Menus, listboxes, dialogs, drawers, filter panels — the four-token quartet
|
|
209
|
+
// (skill §4). Thin blur tier, always: a popover's tier is a property of the
|
|
210
|
+
// recipe, not a caller's choice.
|
|
211
|
+
|
|
212
|
+
// $shadow `menu` (default) — menus/listboxes/dialogs/content-panel/agent-aside
|
|
213
|
+
// `drawer` — the directional, direction-AWARE drawer shadow
|
|
214
|
+
// $surface `chrome` (default) / `menu` / `panel` / `agent` — see `_fill()`.
|
|
215
|
+
// A FLOATING MENU takes `menu`, not the default: `chrome` is the
|
|
216
|
+
// ratified quartet fill and carries the dark plate in BOTH themes.
|
|
217
|
+
// $nested `true` when this surface sits INSIDE another glass surface. Drops
|
|
218
|
+
// `backdrop-filter` entirely, because a nested backdrop root samples
|
|
219
|
+
// its parent instead of the wallpaper and reads as flat mud (hazard 3
|
|
220
|
+
// in the header). The near-opaque `chrome` fill then carries the
|
|
221
|
+
// surface on its own — which is why `$nested` and a translucent
|
|
222
|
+
// `$surface` are refused rather than silently rendered see-through.
|
|
223
|
+
@mixin popover($shadow: menu, $surface: chrome, $nested: false) {
|
|
224
|
+
@if $nested and $surface != chrome {
|
|
225
|
+
@error 'nova-glass: popover($nested: true) needs the near-opaque `chrome` fill — a nested surface has no backdrop-filter to separate it from the glass beneath, so a translucent `#{$surface}` fill renders see-through. Drop $surface, or drop $nested and accept the muddy double-blur.';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
border: 1px solid var(--glass2-border);
|
|
229
|
+
background-image: _fill($surface);
|
|
230
|
+
|
|
231
|
+
@if $shadow == menu {
|
|
232
|
+
box-shadow:
|
|
233
|
+
0 16px 40px rgb(0 0 0 / 20%),
|
|
234
|
+
0 4px 10px rgb(0 0 0 / 10%),
|
|
235
|
+
var(--glass2-inset);
|
|
236
|
+
} @else if $shadow == drawer {
|
|
237
|
+
// A drawer's shadow travels AWAY from the edge it is anchored to, so it
|
|
238
|
+
// mirrors under RTL. `box-shadow` has no logical-offset form, so the offset
|
|
239
|
+
// rides a custom property flipped by `:dir(rtl)` — a compound on the element
|
|
240
|
+
// ITSELF, which is why it survives emulated encapsulation where the
|
|
241
|
+
// `[dir='rtl'] &` descendant combinator is dead (skill §9 rule 1). On an
|
|
242
|
+
// engine without `:dir()` the rule drops whole and the shadow stays on the
|
|
243
|
+
// LTR side: cosmetic, never a layout break.
|
|
244
|
+
--nova-glass-drawer-shadow-x: -30px;
|
|
245
|
+
|
|
246
|
+
box-shadow:
|
|
247
|
+
var(--nova-glass-drawer-shadow-x) 0 70px rgb(0 0 0 / 34%),
|
|
248
|
+
0 4px 10px rgb(0 0 0 / 10%),
|
|
249
|
+
var(--glass2-inset);
|
|
250
|
+
} @else {
|
|
251
|
+
@error 'nova-glass: unknown $shadow `#{$shadow}`. Use `menu` (menus/listboxes/dialogs/content panel/agent aside) or `drawer`.';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@if not $nested {
|
|
255
|
+
@include _backdrop(thin);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@if $shadow == drawer {
|
|
259
|
+
&:dir(rtl) {
|
|
260
|
+
--nova-glass-drawer-shadow-x: 30px;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@include _degrade;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Chrome ───────────────────────────────────────────────────────────────────
|
|
268
|
+
// Header + nav rail. Explicitly NOT the quartet (skill §4): a flat `--mat-rail`
|
|
269
|
+
// fill, a `--w16` hairline, its own three-part shadow, and the 36px tier.
|
|
270
|
+
// `--mat-header` exists and is deliberately unused here — the design's own
|
|
271
|
+
// header draws `--mat-rail` too, and the skill says not to "clean that up".
|
|
272
|
+
// Note the inset step is `--w2` (0.20), NOT `--w22` (0.22) as the quartet's
|
|
273
|
+
// `--glass2-inset` uses. The digits are the decimal fraction (skill §3.3) and
|
|
274
|
+
// these are two different ratified steps, not a typo.
|
|
275
|
+
@mixin chrome {
|
|
276
|
+
border: 1px solid var(--w16);
|
|
277
|
+
background: var(--mat-rail);
|
|
278
|
+
box-shadow:
|
|
279
|
+
0 6px 16px rgb(0 0 0 / 16%),
|
|
280
|
+
0 1px 4px rgb(0 0 0 / 10%),
|
|
281
|
+
inset 0 0.5px 0 var(--w2);
|
|
282
|
+
|
|
283
|
+
@include _backdrop(chrome);
|
|
284
|
+
@include _degrade;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Content panel ────────────────────────────────────────────────────────────
|
|
288
|
+
// The surface holding app content — the strongest blur in the language (72px),
|
|
289
|
+
// on the quartet's layer stack with the `--mat-panel` fill.
|
|
290
|
+
@mixin panel {
|
|
291
|
+
border: 1px solid var(--w18);
|
|
292
|
+
background-image: _fill(panel);
|
|
293
|
+
box-shadow:
|
|
294
|
+
0 16px 40px rgb(0 0 0 / 20%),
|
|
295
|
+
0 4px 10px rgb(0 0 0 / 10%),
|
|
296
|
+
var(--glass2-inset);
|
|
297
|
+
|
|
298
|
+
@include _backdrop(panel);
|
|
299
|
+
@include _degrade;
|
|
300
|
+
}
|
|
301
|
+
|
|
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:
|
|
326
|
+
// · `$surface` has NO DEFAULT and is checked against a closed six-name list of
|
|
327
|
+
// REAL shell surfaces. `@include glass.material()` is a compile error, and
|
|
328
|
+
// there is no honest value for a feature-app component to pass.
|
|
329
|
+
// · The light/dark gate is the CALLER's selector scope, and the only scope
|
|
330
|
+
// where a theme gate actually works is a GLOBAL stylesheet:
|
|
331
|
+
// html.light-theme .fly-header { @include glass.material($surface: header); }
|
|
332
|
+
// Including this from a component stylesheet is a review-blocker: under
|
|
333
|
+
// emulated encapsulation `html.light-theme &` is dead (skill §9 rule 1), so
|
|
334
|
+
// the material would apply in DARK theme too and paint white gradients over
|
|
335
|
+
// dark chrome.
|
|
336
|
+
// The variant follows from the surface; a caller cannot mismatch them. The four
|
|
337
|
+
// `regular`/`chrome` names are the design's four `[data-material]` surfaces; the
|
|
338
|
+
// two `thin` names are the light thin controls §4 lists alongside them.
|
|
339
|
+
@mixin material($surface) {
|
|
340
|
+
$variant: null;
|
|
341
|
+
|
|
342
|
+
@if $surface == header or $surface == rail {
|
|
343
|
+
$variant: chrome;
|
|
344
|
+
} @else if $surface == content-panel or $surface == agent-aside {
|
|
345
|
+
$variant: regular;
|
|
346
|
+
} @else if $surface == toolbar or $surface == tablist {
|
|
347
|
+
$variant: thin;
|
|
348
|
+
} @else {
|
|
349
|
+
@error 'nova-glass: material($surface: #{$surface}) is not a shell surface. This layer is opt-in for shell chrome ONLY — pass `header`, `rail`, `content-panel`, `agent-aside` (the four [data-material] surfaces), or `toolbar` / `tablist` (the thin light controls). A feature-app component must not apply this layer at all; use popover() / panel() instead.';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// The rim and sheen are `z-index: -1` pseudo-elements: they paint above this
|
|
353
|
+
// element's own background but below its content. `isolation` keeps them from
|
|
354
|
+
// falling through to an ancestor's background. No `position` is set on
|
|
355
|
+
// purpose — the `backdrop-filter` below already makes this element a
|
|
356
|
+
// containing block for absolutely positioned descendants (hazard 2 in the
|
|
357
|
+
// header, load-bearing exactly here), so forcing `position: relative` would
|
|
358
|
+
// buy nothing and would silently break a `fixed` or `sticky` caller.
|
|
359
|
+
isolation: isolate;
|
|
360
|
+
border-color: transparent;
|
|
361
|
+
|
|
362
|
+
@if $variant == regular {
|
|
363
|
+
background-image: linear-gradient(
|
|
364
|
+
180deg,
|
|
365
|
+
rgb(255 255 255 / 50%),
|
|
366
|
+
rgb(255 255 255 / 10%) 46%,
|
|
367
|
+
rgb(255 255 255 / 22%) 100%
|
|
368
|
+
);
|
|
369
|
+
box-shadow:
|
|
370
|
+
inset 0 0.5px 0 rgb(255 255 255 / 70%),
|
|
371
|
+
inset 0 -0.5px 0 rgb(148 170 200 / 20%),
|
|
372
|
+
inset 0 0 26px rgb(255 255 255 / 16%),
|
|
373
|
+
0 10px 26px rgb(71 90 120 / 8%),
|
|
374
|
+
0 2px 5px rgb(71 90 120 / 5%);
|
|
375
|
+
-webkit-backdrop-filter: blur(34px) saturate(165%) brightness(1.04);
|
|
376
|
+
backdrop-filter: blur(34px) saturate(165%) brightness(1.04);
|
|
377
|
+
} @else if $variant == chrome {
|
|
378
|
+
background-image: linear-gradient(
|
|
379
|
+
168deg,
|
|
380
|
+
rgb(255 255 255 / 72%) 0%,
|
|
381
|
+
rgb(248 251 255 / 36%) 34%,
|
|
382
|
+
rgb(232 240 252 / 24%) 72%,
|
|
383
|
+
rgb(255 255 255 / 46%) 100%
|
|
384
|
+
);
|
|
385
|
+
box-shadow:
|
|
386
|
+
inset 0 1px 0 rgb(255 255 255 / 98%),
|
|
387
|
+
inset 0 -0.5px 0 rgb(120 145 180 / 22%),
|
|
388
|
+
inset 0 0 0 0.5px rgb(255 255 255 / 50%),
|
|
389
|
+
0 8px 22px rgb(60 80 115 / 10%),
|
|
390
|
+
0 2px 5px rgb(60 80 115 / 6%);
|
|
391
|
+
-webkit-backdrop-filter: blur(30px) saturate(180%) brightness(1.07);
|
|
392
|
+
backdrop-filter: blur(30px) saturate(180%) brightness(1.07);
|
|
393
|
+
} @else {
|
|
394
|
+
background-image: linear-gradient(180deg, rgb(255 255 255 / 66%), rgb(240 246 255 / 24%));
|
|
395
|
+
box-shadow:
|
|
396
|
+
inset 0 0.5px 0 rgb(255 255 255 / 80%),
|
|
397
|
+
inset 0 -0.5px 0 rgb(148 170 200 / 16%),
|
|
398
|
+
inset 0 0 0 0.5px rgb(255 255 255 / 34%),
|
|
399
|
+
0 1px 3px rgb(71 90 120 / 5%);
|
|
400
|
+
-webkit-backdrop-filter: blur(16px) saturate(160%) brightness(1.05);
|
|
401
|
+
backdrop-filter: blur(16px) saturate(160%) brightness(1.05);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Refractive rim — light bending through the edge of the glass. A 1px padding
|
|
405
|
+
// box masked against its own content box leaves only the ring.
|
|
406
|
+
&::before {
|
|
407
|
+
content: '';
|
|
408
|
+
position: absolute;
|
|
409
|
+
z-index: -1;
|
|
410
|
+
inset: 0;
|
|
411
|
+
padding: 1px;
|
|
412
|
+
border-radius: inherit;
|
|
413
|
+
background: linear-gradient(
|
|
414
|
+
140deg,
|
|
415
|
+
rgb(255 255 255 / 60%),
|
|
416
|
+
rgb(255 255 255 / 8%) 34%,
|
|
417
|
+
rgb(190 210 240 / 16%) 64%,
|
|
418
|
+
rgb(255 255 255 / 42%)
|
|
419
|
+
);
|
|
420
|
+
pointer-events: none;
|
|
421
|
+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
422
|
+
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
423
|
+
-webkit-mask-composite: xor;
|
|
424
|
+
mask-composite: exclude;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Specular sheen drifting across the surface. `background-size` is
|
|
428
|
+
// load-bearing, not decoration: `glassSheen` animates background-POSITION, and
|
|
429
|
+
// a background no wider than its box has nowhere to travel — at 100% the
|
|
430
|
+
// animation runs and nothing moves.
|
|
431
|
+
&::after {
|
|
432
|
+
content: '';
|
|
433
|
+
position: absolute;
|
|
434
|
+
z-index: -1;
|
|
435
|
+
inset: 0;
|
|
436
|
+
border-radius: inherit;
|
|
437
|
+
background: linear-gradient(
|
|
438
|
+
100deg,
|
|
439
|
+
transparent 22%,
|
|
440
|
+
rgb(255 255 255 / 22%) 46%,
|
|
441
|
+
rgb(255 255 255 / 7%) 54%,
|
|
442
|
+
transparent 76%
|
|
443
|
+
);
|
|
444
|
+
background-size: 260% 100%;
|
|
445
|
+
opacity: 0.5;
|
|
446
|
+
animation: glassSheen 22s ease-in-out infinite;
|
|
447
|
+
pointer-events: none;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
@include _degrade;
|
|
451
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// ─── Nova motion — MIXINS ONLY ───────────────────────────────────────────────
|
|
2
|
+
// The companion to `_nova-motion.scss`, split out at the S1 review (F4).
|
|
3
|
+
// `_nova-motion.scss` is the EMISSION entry: `@use`-ing it writes the `:root`
|
|
4
|
+
// token block, the `[dir='rtl']` flip, all 16 `@keyframes` and the global
|
|
5
|
+
// `prefers-reduced-motion` block into whatever compiled it. Because every
|
|
6
|
+
// Angular component stylesheet compiles as its own Sass entry, that made the
|
|
7
|
+
// partial's one useful Sass-level construct — the `nova-stagger` mixin —
|
|
8
|
+
// unreachable from a component without paying for a second copy of ~200 lines
|
|
9
|
+
// of global CSS. The result was predictable and did happen: two callers
|
|
10
|
+
// hand-copied the mixin's nth-child math rather than `@use` it, and a third
|
|
11
|
+
// `@use`d it anyway and re-emitted everything.
|
|
12
|
+
// This file is the fix, in the shape `_nova-glass.scss` already proved: it
|
|
13
|
+
// declares mixins and emits NOTHING, so any component may `@use` it freely. It
|
|
14
|
+
// is the single home of the stagger math; `_nova-motion.scss` re-exports it via
|
|
15
|
+
// `@forward` so an existing `@use 'nova-motion'` caller keeps working.
|
|
16
|
+
// Referencing a keyframe NAME (`animation: itemIn …`) or a `var(--nova-*)` token
|
|
17
|
+
// needs no `@use` at all — both are plain global CSS, emitted once through
|
|
18
|
+
// `_fly-theme.scss`. Only the nth-child loop is a Sass construct, which is why
|
|
19
|
+
// it is the only thing here.
|
|
20
|
+
|
|
21
|
+
// ── Stagger utility ──────────────────────────────────────────────────────────
|
|
22
|
+
// Emits an `:nth-child` animation-delay chain so no component hand-writes one.
|
|
23
|
+
// $step matches the context: 40ms toolbar actions/items, 55ms cards, 20ms app
|
|
24
|
+
// tiles (skill §5). Capped at $max (default 12 — every shipped stagger set in
|
|
25
|
+
// the design is well under this) because emitting hundreds of nth-child rules
|
|
26
|
+
// for an unbounded list is worse than the duplication this mixin replaces. Items
|
|
27
|
+
// past the cap hold at the LAST step's delay rather than snapping to a bare 0,
|
|
28
|
+
// so a long list's tail doesn't visibly un-stagger.
|
|
29
|
+
// `:nth-child` counts ALL element siblings, not just the ones this selector
|
|
30
|
+
// matches — a caller must keep the staggered element's parent homogeneous, or
|
|
31
|
+
// insert non-staggered siblings only AFTER the last staggered one.
|
|
32
|
+
@mixin nova-stagger($step: 40ms, $max: 12) {
|
|
33
|
+
@for $i from 0 through ($max - 1) {
|
|
34
|
+
&:nth-child(#{$i + 1}) {
|
|
35
|
+
animation-delay: $i * $step;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&:nth-child(n + #{$max + 1}) {
|
|
40
|
+
animation-delay: ($max - 1) * $step;
|
|
41
|
+
}
|
|
42
|
+
}
|