@trading-game/design-intelligence-layer 1.0.5 → 1.0.6
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/AGENTS.md +3 -3
- package/README.md +1 -1
- package/dist/index.cjs +176 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -10
- package/dist/index.d.ts +40 -10
- package/dist/index.js +126 -88
- package/dist/index.js.map +1 -1
- package/docs/components/button.md +4 -2
- package/docs/components/checkbox.md +1 -1
- package/docs/components/drawer.md +13 -3
- package/docs/components/native-select.md +1 -1
- package/docs/components/radio-group.md +1 -1
- package/docs/components/stepper.md +2 -1
- package/docs/components/switch.md +1 -1
- package/docs/components/toggle.md +1 -1
- package/docs/foundations/colors.md +2 -2
- package/package.json +1 -1
- package/src/styles.css +160 -28
|
@@ -74,8 +74,10 @@ Padding-x = height/2 − 4; gap fixed at 8px and padding does not change when ic
|
|
|
74
74
|
|
|
75
75
|
- All shapes are `rounded-full`; icon sizes are perfect circles.
|
|
76
76
|
- Focus: `focus-visible:ring-[3px] ring-ring-focus-strong` + focus border.
|
|
77
|
-
- Disabled is **per variant
|
|
78
|
-
-
|
|
77
|
+
- Disabled is **per variant and per theme**. Each cell is a deliberate choice, not a rule the others follow. Current decisions:
|
|
78
|
+
- `primary` — 24% opacity in both themes, via `disabled:opacity-(--opacity-disabled)`. The whole control drains together, so the label goes with the fill: it lands at 1.14:1 in light and 2.13:1 in dark against its own washed fill and is effectively unreadable. WCAG 1.4.3 exempts inactive controls, so this is an accepted legibility trade rather than a compliance break
|
|
79
|
+
- `secondary` — 24% opacity in light; in dark it holds full opacity and takes a black-24% glaze plus `border-disabled-default` and `text-disabled-default`, so it recedes with its label still legible. The theme split lives in the stylesheet (`.button-disabled-secondary`) because the package ships no `dark:` variant
|
|
80
|
+
- `frosted` — 40% opacity via `disabled:opacity-(--opacity-disabled-frosted)`, in both themes, keeping `text-brand-selected`. It has its own strength because its fill is only a 10–24% brand tint, so the shared 24% faded it to near-invisible; the previous glaze plus `text-disabled-default` was worse still, since the ink left the brand family for neutral grey while the fill stayed blue
|
|
79
81
|
- fill-less variants (`tertiary`, `tertiary-on-brand`, `secondary-on-brand`) take the ink only — a glaze would paint a surface the variant deliberately doesn't have
|
|
80
82
|
- on-brand variants stay on the static white ladder: `primary-on-brand`'s white pill becomes `white-alpha-24`, `frosted-on-brand` drops from `alpha-24` to `alpha-8`, and the ink is `text-on-brand-disabled-default`
|
|
81
83
|
- `loading` still uses `opacity-24` — a different state, deliberately.
|
|
@@ -22,7 +22,7 @@ Binary (or indeterminate) form control that fills brand blue when checked.
|
|
|
22
22
|
| `checked` | `boolean \| "indeterminate"` | — | Controlled state. |
|
|
23
23
|
| `defaultChecked` | `boolean` | — | Uncontrolled initial state. |
|
|
24
24
|
| `onCheckedChange` | `(checked) => void` | — | Radix change callback. |
|
|
25
|
-
| `disabled` | `boolean` | `false` | `cursor-not-allowed
|
|
25
|
+
| `disabled` | `boolean` | `false` | `cursor-not-allowed` + 24% opacity (`--opacity-disabled`). The whole control fades, including a checked box's brand fill — previously the fill was untreated, so disabled-and-checked was indistinguishable from enabled. |
|
|
26
26
|
| `required`, `name`, `value` | form props | — | Radix renders a hidden input inside forms. |
|
|
27
27
|
|
|
28
28
|
## Variants & sizes
|
|
@@ -13,13 +13,20 @@ Edge-anchored panel built on vaul — the mobile-first modal that slides from th
|
|
|
13
13
|
|
|
14
14
|
- `Drawer` / `DrawerTrigger` / `DrawerPortal` / `DrawerClose` — vaul plumbing with `data-slot` names (`drawer`, `drawer-trigger`, `drawer-portal`, `drawer-close`).
|
|
15
15
|
- `DrawerOverlay` — scrim in `background-overlay-default`; `data-slot="drawer-overlay"`.
|
|
16
|
-
- `DrawerContent` — the panel; direction-aware chrome via `data-vaul-drawer-direction`; `data-slot="drawer-content"`.
|
|
16
|
+
- `DrawerContent` — the panel; direction-aware chrome via `data-vaul-drawer-direction`; `data-slot="drawer-content"`. Takes `dismiss`: `"handle"` (default) shows the grab handle, a 100×8px `rounded-full` bar in `bg-border-default-default`, on bottom drawers only; `"close"` shows a close button instead, in every direction.
|
|
17
17
|
- `DrawerHeader` — `p-4`, centred for top/bottom, left from `md`; `data-slot="drawer-header"`.
|
|
18
18
|
- `DrawerTitle` / `DrawerDescription` — `type-drawer-title` / `-description`.
|
|
19
19
|
- `DrawerFooter` — bottom-pinned (`mt-auto`), stacked full-width children; `data-slot="drawer-footer"`.
|
|
20
20
|
|
|
21
21
|
## API
|
|
22
22
|
|
|
23
|
+
### DrawerContent
|
|
24
|
+
|
|
25
|
+
| Prop | Type | Default | Notes |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| `dismiss` | `"handle" \| "close"` | `"handle"` | `"handle"` is the drag pill, unchanged, bottom direction only. `"close"` swaps it for the same control Dialog and Sheet use — `NavigationButton size="sm"` around a `size-4` icon, a 32px circle on a 16px glyph — wrapped in `Drawer.Close` and shown in every direction. It sits in flow on its own row rather than absolutely, so nothing shares its line and the header always starts below it; `p-4 pb-0` puts it 16px from the top and right edges, matching the header's own padding. `data-slot="drawer-close"`. |
|
|
28
|
+
| …rest | vaul `Content` props | — | Pass through untouched. |
|
|
29
|
+
|
|
23
30
|
### Drawer (root — vaul props)
|
|
24
31
|
|
|
25
32
|
| Prop | Type | Default | Notes |
|
|
@@ -61,8 +68,9 @@ import {
|
|
|
61
68
|
|
|
62
69
|
| Direction | Chrome |
|
|
63
70
|
| --- | --- |
|
|
64
|
-
| `bottom` | `rounded-t-2xl`, top hairline, `max-h-[80vh]`, grab handle |
|
|
71
|
+
| `bottom` | `rounded-t-2xl`, top hairline, `max-h-[80vh]`, grab handle under `dismiss="handle"` |
|
|
65
72
|
| `top` | `rounded-b-2xl`, bottom hairline, `max-h-[80vh]`, no handle |
|
|
73
|
+
| any | `dismiss="close"` shows the close button regardless of direction |
|
|
66
74
|
| `left` / `right` | square corners, side hairline, `w-3/4` capped at `sm:max-w-sm` |
|
|
67
75
|
|
|
68
76
|
## Tokens
|
|
@@ -79,7 +87,8 @@ import {
|
|
|
79
87
|
|
|
80
88
|
- vaul provides the drag/velocity dismissal, snap points, and background scaling; direction is read from `data-vaul-drawer-direction` at render.
|
|
81
89
|
- Only the detached edge is rounded (`rounded-t-2xl` bottom, `rounded-b-2xl` top — the 18px card radius); side drawers stay square because they meet two screen edges.
|
|
82
|
-
- The grab handle renders only on bottom drawers — the direction users can actually drag from comfortably.
|
|
90
|
+
- The grab handle renders only on bottom drawers — the direction users can actually drag from comfortably. The `dismiss="close"` button has no such limit, since a tap target works from any edge.
|
|
91
|
+
- Pick `dismiss="close"` when the drawer is not swipe-dismissible, or on a side drawer where there is no handle at all.
|
|
83
92
|
- Top/bottom drawers cap at 80vh and reserve 96px (`mb-24`/`mt-24`) of visible page.
|
|
84
93
|
- `DrawerFooter` forces every child full-width (`[&>*]:w-full`) — the mobile button stack.
|
|
85
94
|
- Header text centres on top/bottom drawers and left-aligns from `md`.
|
|
@@ -94,4 +103,5 @@ import {
|
|
|
94
103
|
**Don't**
|
|
95
104
|
- Don't add your own rounding to side drawers — square is deliberate.
|
|
96
105
|
- Don't recolour the grab handle; it is `bg-border-default-default` by contract.
|
|
106
|
+
- Don't hand-roll a close button in `DrawerContent`'s children; `dismiss="close"` already wires `Drawer.Close` and matches Dialog and Sheet.
|
|
97
107
|
- Don't exceed 80vh with content — let the body scroll inside the panel instead.
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
|
|
56
56
|
- Joins the [Field](./field.md) cascade: with no `size` prop it takes the Field's size; an explicit prop wins. `size="default"` always resolves to `md` regardless of context.
|
|
57
57
|
- `appearance-none` + the injected chevron replace the platform arrow; right padding (`pr-9`, lg `pr-10`) reserves its space.
|
|
58
|
-
- Disabled
|
|
58
|
+
- Disabled lays the disabled glaze (`background-disabled-default` on `background-image`, so the fill stays underneath) on the select and kills pointer events. The wrapper is no longer dimmed as a group. The select carries `disabled:text-disabled-default` **itself** — the wrapper's `has-[select:disabled]:` colour was being overridden by the select's own `text-prominent-default`, so the value stayed at full strength. The chevron takes `group-has-[select:disabled]/native-select:text-icon-disabled-default`, since it is a sibling with its own colour and would not inherit either.
|
|
59
59
|
- The wrapper is `w-fit` and `className` lands on the inner `<select>` — for full-width layouts you must widen the wrapper (e.g. a `w-full` parent with `[&>[data-slot=native-select-wrapper]]:w-full`), not just the select.
|
|
60
60
|
|
|
61
61
|
## Do / Don't
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
- Single-select with roving tabindex: Tab reaches the group once, arrows move and select within it (Radix).
|
|
67
67
|
- Controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
|
|
68
68
|
- Checked state swaps only border and dot — the background never fills; there is no shadow in any state.
|
|
69
|
-
- Disabled items get `cursor-not-allowed` +
|
|
69
|
+
- Disabled items get `cursor-not-allowed` + 24% opacity (`--opacity-disabled`), which fades the selected indicator with the control. Previously the indicator was untreated.
|
|
70
70
|
- Inside a Field choice card, the item's `data-state=checked` is what flips the surrounding `FieldLabel` to the brand-selected card treatment.
|
|
71
71
|
|
|
72
72
|
## Do / Don't
|
|
@@ -12,7 +12,8 @@ The mobile amount tray — a centred amount with frosted − / + circles either
|
|
|
12
12
|
|
|
13
13
|
- Container — full-width row, `data-slot="stepper"`, `data-disabled`.
|
|
14
14
|
- − / + — composed `<Button variant="frosted" size="icon-lg">` (48px brand-selected tint circles); each disables at its bound.
|
|
15
|
-
-
|
|
15
|
+
- Two rows — the optional uppercase `label` spans the whole control, then a 4px gap, then the `−` / amount / `+` row. The label is deliberately **not** nested in the value column: doing so made the buttons centre against the label-plus-amount stack, which dropped them off the amount's centre line.
|
|
16
|
+
- Centre of the row — the editable amount (`role="spinbutton"`, tabular semibold figure), flanked by the two circles.
|
|
16
17
|
|
|
17
18
|
## API
|
|
18
19
|
|
|
@@ -20,7 +20,7 @@ Binary on/off toggle (Radix Switch) with a sliding white thumb.
|
|
|
20
20
|
| `size` | `"sm" \| "default"` | `"default"` | default: 24px tall (`h-6 w-11`, 18px thumb); sm: 20px tall (`h-5 w-9`, 14px thumb). |
|
|
21
21
|
| `checked` / `defaultChecked` | `boolean` | — | Controlled / uncontrolled. |
|
|
22
22
|
| `onCheckedChange` | `(checked: boolean) => void` | — | Radix callback. |
|
|
23
|
-
| `disabled` | `boolean` | — | `cursor-not-allowed` +
|
|
23
|
+
| `disabled` | `boolean` | — | `cursor-not-allowed` + 24% opacity (`--opacity-disabled`). Track and thumb fade together; previously both were untreated, so a disabled-checked switch was indistinguishable from enabled. |
|
|
24
24
|
|
|
25
25
|
## Variants & sizes
|
|
26
26
|
|
|
@@ -77,7 +77,7 @@ import { Star } from "lucide-react"
|
|
|
77
77
|
- Radius is `rounded-md` (8px, the row radius); icons default to `size-4`.
|
|
78
78
|
- `spacing={0}` (default) joins segments: items lose their own radius (`rounded-none`), the first/last regain `rounded-l-md`/`rounded-r-md`, and segments share hairlines by dropping `border-l` on all but the first; any positive `spacing` separates them and each keeps `rounded-md`.
|
|
79
79
|
- Items are `w-auto min-w-0 shrink-0 px-3` and raise `z-10` on focus so the ring is never clipped by neighbours.
|
|
80
|
-
- Disabled: `pointer-events-none` +
|
|
80
|
+
- Disabled: `pointer-events-none` + 24% opacity (`--opacity-disabled`). The pressed fill fades with the control; previously it was untreated, so a disabled-pressed toggle read as pressed-and-active.
|
|
81
81
|
|
|
82
82
|
## Do / Don't
|
|
83
83
|
|
|
@@ -46,7 +46,7 @@ All scales run 50→1000 (mono 50→1200; blue adds 25 and 1000 as the two canva
|
|
|
46
46
|
| `background-tinted-canvas` | blue-600 12%→4% mixed into the canvas + a blue-400 aurora | blue-400 14% into the canvas + aurora | The Refined screen background — a layered gradient, consume via `background: var(--background-tinted-canvas)` on the page root only; ends on `primary-canvas` in both themes so long pages melt in seamlessly |
|
|
47
47
|
| `background-primary-surface` | mono-50 `#FFFFFF` | white 6% over canvas ≈ `#0F0F2C` | Cards, panels, menus, inputs, sheets — the default solid surface |
|
|
48
48
|
| `background-secondary-surface` | mono-100 `#F1F1F1` | white 12% over canvas ≈ `#1F1F3A` | One step up: muted fills, tracks, icon tiles, read-only fields |
|
|
49
|
-
| `background-disabled-default` | white-alpha-24 | black-alpha-
|
|
49
|
+
| `background-disabled-default` | white-alpha-24 | black-alpha-24 | **The disabled glaze** — an alpha layer laid *over* whatever fill a control already has, so every variant keeps its identity while draining. Consume it on `background-image` (`[background-image:linear-gradient(var(--background-disabled-default),var(--background-disabled-default))]`), never `background-color`, or it replaces the fill instead of washing it. Polarity is the inverse of hover — hover advances (black in light, white in dark), disabled recedes (white in light, black in dark) — and the strengths differ because the ground does: white moves fast against a near-white page, black slowly against a near-black canvas. Button's `primary` and `secondary` no longer consume it — they were moved to `opacity`/black-24% by explicit decision, see AGENTS rule 13 |
|
|
50
50
|
| `background-hover-default` | black-alpha-4 | white-alpha-12 | The hover glaze — laid over any solid surface |
|
|
51
51
|
| `background-pressed-default` | black-alpha-8 | white-alpha-20 | The pressed glaze |
|
|
52
52
|
| `background-brand-default` | blue-600 `#2323FF` | blue-400 `#4D6BFF` | Primary CTAs, checked controls, solid selection chips |
|
|
@@ -85,7 +85,7 @@ All scales run 50→1000 (mono 50→1200; blue adds 25 and 1000 as the two canva
|
|
|
85
85
|
| `text-success-default` / `-error-` / `-warning-` / `-information-` | status 600 | **static — same in both themes** (deliberate call) | Status text |
|
|
86
86
|
| `text-on-status-static` | mono-50 | static | White on solid status fills (below AA on green/amber — deliberate style call) |
|
|
87
87
|
| `text-demo-default` | ember-500 | static | Demo badge ink |
|
|
88
|
-
| `icon-prominent-default` … `icon-information-default` | aliases of the matching text tokens | — | Separate namespace so icons (3:1 requirement) can diverge later |
|
|
88
|
+
| `icon-prominent-default` … `icon-information-default` | aliases of the matching text tokens | — | Separate namespace so icons (3:1 requirement) can diverge later. `icon-prominent/subtle/disabled-default` are **re-declared in the dark block**: a custom property resolves its `var()` where it is declared, so the `:root` aliases froze at the light values and every `text-icon-*` utility painted a light-theme colour in dark. The status and on-brand members need no override — the text tokens they alias are static |
|
|
89
89
|
|
|
90
90
|
## Semantic tokens — Border & Ring
|
|
91
91
|
|
package/package.json
CHANGED
package/src/styles.css
CHANGED
|
@@ -554,6 +554,16 @@
|
|
|
554
554
|
differ because the ground does — white moves fast against a near-white
|
|
555
555
|
page, black moves slowly against a near-black canvas. */
|
|
556
556
|
--background-disabled-default: var(--primitive-white-alpha-24);
|
|
557
|
+
/* Disabled fade — the 24% opacity treatment. Not a colour, so it sits
|
|
558
|
+
outside the semantic colour namespaces. Which variants use it, and in
|
|
559
|
+
which theme, is a per-case decision (see the button rules below), not a
|
|
560
|
+
blanket rule. */
|
|
561
|
+
--opacity-disabled: 0.24;
|
|
562
|
+
/* Frosted drains to 40%, not 24%. Its fill is already a 10%/24% brand
|
|
563
|
+
tint, so a 24% fade left almost nothing on screen. Static in both
|
|
564
|
+
themes. Separate token, not a shared one — disabled strength is a
|
|
565
|
+
per-variant decision. */
|
|
566
|
+
--opacity-disabled-frosted: 0.4;
|
|
557
567
|
--background-hover-default: var(--primitive-black-alpha-4);
|
|
558
568
|
--background-pressed-default: var(--primitive-black-alpha-8);
|
|
559
569
|
--background-brand-default: var(--primitive-blue-600);
|
|
@@ -812,7 +822,10 @@
|
|
|
812
822
|
--background-primary-canvas: var(--primitive-blue-1000);
|
|
813
823
|
--background-primary-surface: color-mix(in srgb, white 6%, var(--primitive-blue-1000));
|
|
814
824
|
--background-secondary-surface: color-mix(in srgb, white 12%, var(--primitive-blue-1000));
|
|
815
|
-
|
|
825
|
+
/* 24%, not 50%. At 50% the glaze stripped most of the blue out of a navy
|
|
826
|
+
surface, so a disabled field landed darker and greyer than the canvas
|
|
827
|
+
and read as a hole punched in the page rather than a drained control. */
|
|
828
|
+
--background-disabled-default: var(--primitive-black-alpha-24);
|
|
816
829
|
/* solid #1F1F3A — one glaze step above the card (6%) */
|
|
817
830
|
/* solid #0F0F2C — base + white glaze, flattened */
|
|
818
831
|
--background-hover-default: var(--primitive-white-alpha-12);
|
|
@@ -846,6 +859,17 @@
|
|
|
846
859
|
--text-prominent-default: var(--primitive-mono-50);
|
|
847
860
|
--text-subtle-default: var(--primitive-mono-600);
|
|
848
861
|
--text-disabled-default: var(--primitive-mono-900);
|
|
862
|
+
/* The icon namespace aliases the text ladder, and a custom property
|
|
863
|
+
resolves its var() where it is DECLARED, not where it is used — so the
|
|
864
|
+
aliases declared in :root froze at the light values and never flipped
|
|
865
|
+
here. Every text-icon-* utility in dark was painting a light-theme
|
|
866
|
+
colour: subtle icons at mono-800 instead of mono-600, prominent at
|
|
867
|
+
mono-1100 on a near-black canvas. Re-declaring them re-resolves against
|
|
868
|
+
the dark ink above. Status and on-brand icons need no override — the
|
|
869
|
+
text tokens they alias are static. */
|
|
870
|
+
--icon-prominent-default: var(--text-prominent-default);
|
|
871
|
+
--icon-subtle-default: var(--text-subtle-default);
|
|
872
|
+
--icon-disabled-default: var(--text-disabled-default);
|
|
849
873
|
--text-on-brand-disabled-default: var(--primitive-white-alpha-24);
|
|
850
874
|
/* Inverse mirrors the light page ladder here — see the note in the light
|
|
851
875
|
block. mono 1100 · 800 · 400, same values light's PAGE row carries. */
|
|
@@ -1346,50 +1370,158 @@
|
|
|
1346
1370
|
}
|
|
1347
1371
|
}
|
|
1348
1372
|
|
|
1349
|
-
/*
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1373
|
+
/* ─────────────────────────────────────────────────────────────────
|
|
1374
|
+
Disabled — Primary and Secondary buttons
|
|
1375
|
+
|
|
1376
|
+
Disabled is picked per variant and per theme, not from one rule. The
|
|
1377
|
+
four decisions currently are:
|
|
1378
|
+
|
|
1379
|
+
Primary · light — 24% opacity
|
|
1380
|
+
Primary · dark — 24% opacity
|
|
1381
|
+
Secondary · light — 24% opacity
|
|
1382
|
+
Secondary · dark — black 24% glaze over the fill
|
|
1383
|
+
|
|
1384
|
+
Primary is the same in both themes, so it takes the opacity utility in
|
|
1385
|
+
the component. Secondary differs by theme, and the package ships no
|
|
1386
|
+
`dark:` variant, so its split lives here where the themes are declared.
|
|
1387
|
+
Changing any one cell is expected; they are not derived from each other.
|
|
1388
|
+
───────────────────────────────────────────────────────────────── */
|
|
1389
|
+
.button-disabled-secondary:disabled {
|
|
1390
|
+
opacity: var(--opacity-disabled);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
.dark .button-disabled-secondary:disabled,
|
|
1394
|
+
[data-theme="dark"] .button-disabled-secondary:disabled {
|
|
1395
|
+
opacity: 1;
|
|
1396
|
+
/* Rides the alpha ladder directly: a fixed recede step, not a themed
|
|
1397
|
+
colour, and it is laid over the fill so the variant keeps its identity. */
|
|
1398
|
+
background-image: linear-gradient(
|
|
1399
|
+
var(--primitive-black-alpha-24),
|
|
1400
|
+
var(--primitive-black-alpha-24)
|
|
1359
1401
|
);
|
|
1360
|
-
|
|
1402
|
+
border-color: var(--border-disabled-default);
|
|
1403
|
+
color: var(--text-disabled-default);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/* ─────────────────────────────────────────────────────────────────
|
|
1407
|
+
Game loading splash
|
|
1408
|
+
|
|
1409
|
+
The backdrop is the tinted canvas, so the splash reads as the product's
|
|
1410
|
+
own surface in both themes and already lands on the page canvas the app
|
|
1411
|
+
is about to paint. The dot grid is a background-image, which Tailwind
|
|
1412
|
+
utilities can't express cleanly, so it lives here as a class.
|
|
1413
|
+
───────────────────────────────────────────────────────────────── */
|
|
1414
|
+
.game-loader-field {
|
|
1415
|
+
position: absolute;
|
|
1416
|
+
inset: 0;
|
|
1417
|
+
background-color: var(--background-primary-canvas);
|
|
1418
|
+
background-image:
|
|
1419
|
+
radial-gradient(
|
|
1420
|
+
color-mix(in oklch, var(--text-prominent-default) 10%, transparent) 1px,
|
|
1421
|
+
transparent 1px
|
|
1422
|
+
),
|
|
1423
|
+
var(--background-tinted-canvas);
|
|
1424
|
+
background-size: 14px 14px, auto, auto;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/* The product mark, subtracted from the field so it is a window rather than
|
|
1428
|
+
ink. `--game-loader-mark` is the logo's url(), set by the component from
|
|
1429
|
+
the same prop that draws the resting mark, so the two always agree. */
|
|
1430
|
+
.game-loader-punch {
|
|
1431
|
+
mask-image: linear-gradient(#000, #000), var(--game-loader-mark);
|
|
1432
|
+
mask-size: 100% 100%, 120px 113px;
|
|
1433
|
+
mask-position: center, center;
|
|
1434
|
+
mask-repeat: no-repeat, no-repeat;
|
|
1435
|
+
mask-composite: exclude;
|
|
1436
|
+
-webkit-mask-image: linear-gradient(#000, #000), var(--game-loader-mark);
|
|
1437
|
+
-webkit-mask-size: 100% 100%, 120px 113px;
|
|
1438
|
+
-webkit-mask-position: center, center;
|
|
1439
|
+
-webkit-mask-repeat: no-repeat, no-repeat;
|
|
1440
|
+
-webkit-mask-composite: xor;
|
|
1361
1441
|
}
|
|
1362
1442
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1443
|
+
.game-loader-mark {
|
|
1444
|
+
position: absolute;
|
|
1445
|
+
top: 50%;
|
|
1446
|
+
left: 50%;
|
|
1447
|
+
width: 120px;
|
|
1448
|
+
height: auto;
|
|
1449
|
+
transform: translate(-50%, -50%);
|
|
1450
|
+
transform-origin: center;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/* Exit reveal — ONLY the hole grows. The field keeps its position, its
|
|
1454
|
+
gradient and its dot grid, so nothing behind the hole appears to zoom;
|
|
1455
|
+
scaling the field instead is what made the old reveal read as a camera
|
|
1456
|
+
push. The field fades across the same beat, so the app bleeds through
|
|
1457
|
+
from about a third of the way in rather than arriving all at once.
|
|
1458
|
+
Suppressed under prefers-reduced-motion by the component, which falls
|
|
1459
|
+
back to a plain fade. */
|
|
1460
|
+
@keyframes game-loader-reveal {
|
|
1367
1461
|
0% {
|
|
1368
|
-
|
|
1369
|
-
|
|
1462
|
+
mask-size: 100% 100%, 120px 113px;
|
|
1463
|
+
-webkit-mask-size: 100% 100%, 120px 113px;
|
|
1464
|
+
animation-timing-function: cubic-bezier(0.33, 0, 0.55, 1);
|
|
1370
1465
|
}
|
|
1371
1466
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1467
|
+
26% {
|
|
1468
|
+
mask-size: 100% 100%, 108px 102px;
|
|
1469
|
+
-webkit-mask-size: 100% 100%, 108px 102px;
|
|
1470
|
+
animation-timing-function: cubic-bezier(0.72, 0, 0.86, 0.3);
|
|
1375
1471
|
}
|
|
1376
1472
|
|
|
1377
1473
|
100% {
|
|
1378
|
-
|
|
1474
|
+
mask-size: 100% 100%, 420vmax 396vmax;
|
|
1475
|
+
-webkit-mask-size: 100% 100%, 420vmax 396vmax;
|
|
1379
1476
|
}
|
|
1380
1477
|
}
|
|
1381
1478
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1479
|
+
@keyframes game-loader-field-fade {
|
|
1480
|
+
to {
|
|
1481
|
+
opacity: 0;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
.game-loader-field-exit {
|
|
1486
|
+
will-change: mask-size, opacity;
|
|
1487
|
+
animation:
|
|
1488
|
+
game-loader-reveal 660ms forwards,
|
|
1489
|
+
game-loader-field-fade 480ms 180ms ease-in forwards;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/* The resting mark tracks the same anticipation dip, then hands over to the
|
|
1493
|
+
hole at 27%. The two shapes coincide there, so the handover is invisible. */
|
|
1494
|
+
@keyframes game-loader-plug-dip {
|
|
1495
|
+
0% {
|
|
1496
|
+
transform: translate(-50%, -50%) scale(1);
|
|
1497
|
+
opacity: 1;
|
|
1498
|
+
animation-timing-function: cubic-bezier(0.33, 0, 0.55, 1);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
26% {
|
|
1502
|
+
transform: translate(-50%, -50%) scale(0.9);
|
|
1503
|
+
opacity: 1;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
27% {
|
|
1507
|
+
transform: translate(-50%, -50%) scale(0.9);
|
|
1508
|
+
opacity: 0;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
100% {
|
|
1512
|
+
transform: translate(-50%, -50%) scale(0.9);
|
|
1513
|
+
opacity: 0;
|
|
1514
|
+
}
|
|
1385
1515
|
}
|
|
1386
1516
|
|
|
1387
1517
|
.game-loader-mark-exit {
|
|
1388
|
-
|
|
1518
|
+
will-change: transform, opacity;
|
|
1519
|
+
animation: game-loader-plug-dip 660ms forwards;
|
|
1389
1520
|
}
|
|
1390
1521
|
|
|
1391
1522
|
/* Progress-bar shimmer — sweeps across the filled portion while a stage is
|
|
1392
|
-
still loading, so a held bar still reads as "working".
|
|
1523
|
+
still loading, so a held bar still reads as "working". The fill is now the
|
|
1524
|
+
brand colour on a canvas track, so the sweep is the on-brand white. */
|
|
1393
1525
|
@keyframes game-loader-sweep {
|
|
1394
1526
|
0% {
|
|
1395
1527
|
left: -60%;
|
|
@@ -1404,7 +1536,7 @@
|
|
|
1404
1536
|
background: linear-gradient(
|
|
1405
1537
|
90deg,
|
|
1406
1538
|
transparent,
|
|
1407
|
-
color-mix(in oklch, var(--
|
|
1539
|
+
color-mix(in oklch, var(--icon-on-brand-static) 35%, transparent),
|
|
1408
1540
|
transparent
|
|
1409
1541
|
);
|
|
1410
1542
|
animation: game-loader-sweep 1.4s ease-in-out infinite;
|