@trading-game/design-intelligence-layer 1.0.5 → 1.0.7
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 +188 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -11
- package/dist/index.d.ts +41 -11
- package/dist/index.js +138 -93
- 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 +10 -6
- package/docs/components/switch.md +1 -1
- package/docs/components/toggle.md +1 -1
- package/docs/foundations/colors.md +2 -2
- package/docs/foundations/typography.md +12 -0
- package/package.json +1 -1
- package/src/styles.css +167 -35
|
@@ -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,16 +12,17 @@ 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 sentence-case `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
|
|
|
19
20
|
| Prop | Type | Default | Notes |
|
|
20
21
|
| --- | --- | --- | --- |
|
|
21
|
-
| `label` | `ReactNode` | — |
|
|
22
|
+
| `label` | `ReactNode` | — | Sentence-case caption above the amount, e.g. `"Stake (USDT)"`. Renders in the shared `caption` type style, so a Stepper sits level with other captioned controls in a form row. |
|
|
22
23
|
| `value` / `defaultValue` | `number` | `0` | Controlled / uncontrolled. |
|
|
23
24
|
| `onValueChange` | `(n: number) => void` | — | |
|
|
24
|
-
| `min` / `max` | `number` | ±∞ | Buttons disable at the bounds; typed values clamp on commit. |
|
|
25
|
+
| `min` / `max` | `number` | ±∞ | Buttons disable at the bounds; typed values clamp on commit. Bounds need not sit on the step grid — `min={0.5} step={1}` is legal and `0.5` is exactly reachable. |
|
|
25
26
|
| `step` | `number` | `1` | Decimal steps set the display precision (0.5 → "2.50"). |
|
|
26
27
|
| `disabled` | `boolean` | `false` | Pointer events off; the `+`/`−` render Button's own disabled state and the label and amount switch to `text-disabled-default`. No wash on the wrapper — stacking one over the Buttons' own dim multiplied to ~6% and made them invisible. |
|
|
27
28
|
| `placeholder`, `id`, `name`, `aria-label` | | — | Pass-through to the amount input. |
|
|
@@ -40,12 +41,13 @@ import { Stepper } from "@trading-game/design-intelligence-layer"
|
|
|
40
41
|
|
|
41
42
|
| Token | Value |
|
|
42
43
|
| --- | --- |
|
|
43
|
-
| `--stepper-stake-label-*` | 12 / medium / 16
|
|
44
|
-
| `--stepper-stake-amount-*` |
|
|
44
|
+
| `--stepper-stake-label-*` | Aliases `--type-caption-*` — 12 / medium / 16 / `tracking-loose`, sentence case |
|
|
45
|
+
| `--stepper-stake-amount-*` | 20 / semibold / 28, tabular — matches [Numpad](./numpad.md)'s digit size |
|
|
45
46
|
|
|
46
47
|
## Behaviour
|
|
47
48
|
|
|
48
|
-
- The amount is directly editable: type, then blur or Enter commits
|
|
49
|
+
- The amount is directly editable: type, then blur or Enter commits; ArrowUp/ArrowDown step the value.
|
|
50
|
+
- **The bounds are exempt from step rounding.** A value at or past a bound commits as exactly that bound; only values strictly inside the range are rounded to the step's precision (then clamped as a backstop). So `min`/`max` are both exactly *reachable* and *stable* even when they sit off the step grid: with `min={0.5} step={1}`, decrementing from `1` lands on `0.5` and stays there across blur and re-commit. Rounding a bound would move the value off it — `Math.round(0.5)` is `1` — and a clamp afterwards can't undo that, since `1` is inside the range; the pre-1.0.7 component rounded after clamping and so returned `1` forever, making `min` unreachable.
|
|
49
51
|
- `onValueChange` fires with the committed number.
|
|
50
52
|
- Full-width by default — constrain with `className` (`max-w-90` in the demo).
|
|
51
53
|
|
|
@@ -53,8 +55,10 @@ import { Stepper } from "@trading-game/design-intelligence-layer"
|
|
|
53
55
|
|
|
54
56
|
**Do**
|
|
55
57
|
- Put the unit in the `label` ("Stake (USDT)").
|
|
58
|
+
- Write `label` copy in sentence case; currency codes stay capitalised as proper nouns.
|
|
56
59
|
- Pair with [Numpad](./numpad.md) on mobile amount screens.
|
|
57
60
|
|
|
58
61
|
**Don't**
|
|
59
62
|
- Don't use it for long-range values — that's [Slider](./slider.md).
|
|
60
63
|
- Don't hide the label on money amounts; the unit matters.
|
|
64
|
+
- Don't hand-roll a caption above a Stepper to dodge the label's case — use `label`.
|
|
@@ -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
|
|
|
@@ -68,6 +68,18 @@ Recurring ladders:
|
|
|
68
68
|
- **Overlay titles** (Dialog, Alert Dialog, Drawer, Sheet, Empty): 18/semibold/24; their descriptions 14/regular/20.
|
|
69
69
|
- **Small labels** (Badge sm, Kbd-like chips, Sidebar labels, Bottom Navigation, menu headings): 12/16, medium.
|
|
70
70
|
- **Helper text** (Field description/error): fixed **12/16 at every field size** — help text never scales with the control.
|
|
71
|
+
- **Standalone figures** (Stepper amount, Input OTP, Numpad keys): **20/28**. Weight varies with the job — the Stepper amount and OTP are semibold, Numpad keys medium — but the size is fixed, so a value typed on the pad reads at the same size in the tray above it. Only the Stepper amount adds `tabular-nums`, since it is the one that changes in place while the user holds `−` / `+`.
|
|
72
|
+
|
|
73
|
+
### Aliasing a foundation style
|
|
74
|
+
|
|
75
|
+
A component token normally points at a **primitive** step. Point it at a **semantic** `--type-*` instead when the component's text does the same job as a foundation style, so the two cannot drift apart:
|
|
76
|
+
|
|
77
|
+
```css
|
|
78
|
+
--stepper-stake-label-font-size: var(--type-caption-size);
|
|
79
|
+
--stepper-stake-label-tracking: var(--type-caption-tracking);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The Stepper's `label` names the control beneath it — a caption's job — so it aliases `--type-caption-*` rather than re-declaring 12/16. Reserve this for a genuine role match: aliasing because two things happen to share a size today just couples them by accident.
|
|
71
83
|
|
|
72
84
|
## The one shared family: `--menu-*`
|
|
73
85
|
|
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. */
|
|
@@ -1141,12 +1165,13 @@
|
|
|
1141
1165
|
--bottom-navigation-label-active-font-weight: var(--primitive-font-weight-semibold);
|
|
1142
1166
|
|
|
1143
1167
|
/* ── Stepper (stake variant) — component type tokens (private) ── */
|
|
1144
|
-
--stepper-stake-label-font-size: var(--
|
|
1145
|
-
--stepper-stake-label-font-weight: var(--
|
|
1146
|
-
--stepper-stake-label-line-height: var(--
|
|
1147
|
-
--stepper-stake-
|
|
1168
|
+
--stepper-stake-label-font-size: var(--type-caption-size);
|
|
1169
|
+
--stepper-stake-label-font-weight: var(--type-caption-weight);
|
|
1170
|
+
--stepper-stake-label-line-height: var(--type-caption-line);
|
|
1171
|
+
--stepper-stake-label-tracking: var(--type-caption-tracking);
|
|
1172
|
+
--stepper-stake-amount-font-size: var(--primitive-font-size-20);
|
|
1148
1173
|
--stepper-stake-amount-font-weight: var(--primitive-font-weight-semibold);
|
|
1149
|
-
--stepper-stake-amount-line-height: var(--primitive-line-height-
|
|
1174
|
+
--stepper-stake-amount-line-height: var(--primitive-line-height-28);
|
|
1150
1175
|
|
|
1151
1176
|
/* ── Progress — component type tokens (private) ── */
|
|
1152
1177
|
--progress-label-font-size: var(--primitive-font-size-12);
|
|
@@ -1346,50 +1371,158 @@
|
|
|
1346
1371
|
}
|
|
1347
1372
|
}
|
|
1348
1373
|
|
|
1349
|
-
/*
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1374
|
+
/* ─────────────────────────────────────────────────────────────────
|
|
1375
|
+
Disabled — Primary and Secondary buttons
|
|
1376
|
+
|
|
1377
|
+
Disabled is picked per variant and per theme, not from one rule. The
|
|
1378
|
+
four decisions currently are:
|
|
1379
|
+
|
|
1380
|
+
Primary · light — 24% opacity
|
|
1381
|
+
Primary · dark — 24% opacity
|
|
1382
|
+
Secondary · light — 24% opacity
|
|
1383
|
+
Secondary · dark — black 24% glaze over the fill
|
|
1384
|
+
|
|
1385
|
+
Primary is the same in both themes, so it takes the opacity utility in
|
|
1386
|
+
the component. Secondary differs by theme, and the package ships no
|
|
1387
|
+
`dark:` variant, so its split lives here where the themes are declared.
|
|
1388
|
+
Changing any one cell is expected; they are not derived from each other.
|
|
1389
|
+
───────────────────────────────────────────────────────────────── */
|
|
1390
|
+
.button-disabled-secondary:disabled {
|
|
1391
|
+
opacity: var(--opacity-disabled);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
.dark .button-disabled-secondary:disabled,
|
|
1395
|
+
[data-theme="dark"] .button-disabled-secondary:disabled {
|
|
1396
|
+
opacity: 1;
|
|
1397
|
+
/* Rides the alpha ladder directly: a fixed recede step, not a themed
|
|
1398
|
+
colour, and it is laid over the fill so the variant keeps its identity. */
|
|
1399
|
+
background-image: linear-gradient(
|
|
1400
|
+
var(--primitive-black-alpha-24),
|
|
1401
|
+
var(--primitive-black-alpha-24)
|
|
1359
1402
|
);
|
|
1360
|
-
|
|
1403
|
+
border-color: var(--border-disabled-default);
|
|
1404
|
+
color: var(--text-disabled-default);
|
|
1361
1405
|
}
|
|
1362
1406
|
|
|
1363
|
-
/*
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1407
|
+
/* ─────────────────────────────────────────────────────────────────
|
|
1408
|
+
Game loading splash
|
|
1409
|
+
|
|
1410
|
+
The backdrop is the tinted canvas, so the splash reads as the product's
|
|
1411
|
+
own surface in both themes and already lands on the page canvas the app
|
|
1412
|
+
is about to paint. The dot grid is a background-image, which Tailwind
|
|
1413
|
+
utilities can't express cleanly, so it lives here as a class.
|
|
1414
|
+
───────────────────────────────────────────────────────────────── */
|
|
1415
|
+
.game-loader-field {
|
|
1416
|
+
position: absolute;
|
|
1417
|
+
inset: 0;
|
|
1418
|
+
background-color: var(--background-primary-canvas);
|
|
1419
|
+
background-image:
|
|
1420
|
+
radial-gradient(
|
|
1421
|
+
color-mix(in oklch, var(--text-prominent-default) 10%, transparent) 1px,
|
|
1422
|
+
transparent 1px
|
|
1423
|
+
),
|
|
1424
|
+
var(--background-tinted-canvas);
|
|
1425
|
+
background-size: 14px 14px, auto, auto;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/* The product mark, subtracted from the field so it is a window rather than
|
|
1429
|
+
ink. `--game-loader-mark` is the logo's url(), set by the component from
|
|
1430
|
+
the same prop that draws the resting mark, so the two always agree. */
|
|
1431
|
+
.game-loader-punch {
|
|
1432
|
+
mask-image: linear-gradient(#000, #000), var(--game-loader-mark);
|
|
1433
|
+
mask-size: 100% 100%, 120px 113px;
|
|
1434
|
+
mask-position: center, center;
|
|
1435
|
+
mask-repeat: no-repeat, no-repeat;
|
|
1436
|
+
mask-composite: exclude;
|
|
1437
|
+
-webkit-mask-image: linear-gradient(#000, #000), var(--game-loader-mark);
|
|
1438
|
+
-webkit-mask-size: 100% 100%, 120px 113px;
|
|
1439
|
+
-webkit-mask-position: center, center;
|
|
1440
|
+
-webkit-mask-repeat: no-repeat, no-repeat;
|
|
1441
|
+
-webkit-mask-composite: xor;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
.game-loader-mark {
|
|
1445
|
+
position: absolute;
|
|
1446
|
+
top: 50%;
|
|
1447
|
+
left: 50%;
|
|
1448
|
+
width: 120px;
|
|
1449
|
+
height: auto;
|
|
1450
|
+
transform: translate(-50%, -50%);
|
|
1451
|
+
transform-origin: center;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/* Exit reveal — ONLY the hole grows. The field keeps its position, its
|
|
1455
|
+
gradient and its dot grid, so nothing behind the hole appears to zoom;
|
|
1456
|
+
scaling the field instead is what made the old reveal read as a camera
|
|
1457
|
+
push. The field fades across the same beat, so the app bleeds through
|
|
1458
|
+
from about a third of the way in rather than arriving all at once.
|
|
1459
|
+
Suppressed under prefers-reduced-motion by the component, which falls
|
|
1460
|
+
back to a plain fade. */
|
|
1461
|
+
@keyframes game-loader-reveal {
|
|
1367
1462
|
0% {
|
|
1368
|
-
|
|
1369
|
-
|
|
1463
|
+
mask-size: 100% 100%, 120px 113px;
|
|
1464
|
+
-webkit-mask-size: 100% 100%, 120px 113px;
|
|
1465
|
+
animation-timing-function: cubic-bezier(0.33, 0, 0.55, 1);
|
|
1370
1466
|
}
|
|
1371
1467
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1468
|
+
26% {
|
|
1469
|
+
mask-size: 100% 100%, 108px 102px;
|
|
1470
|
+
-webkit-mask-size: 100% 100%, 108px 102px;
|
|
1471
|
+
animation-timing-function: cubic-bezier(0.72, 0, 0.86, 0.3);
|
|
1375
1472
|
}
|
|
1376
1473
|
|
|
1377
1474
|
100% {
|
|
1378
|
-
|
|
1475
|
+
mask-size: 100% 100%, 420vmax 396vmax;
|
|
1476
|
+
-webkit-mask-size: 100% 100%, 420vmax 396vmax;
|
|
1379
1477
|
}
|
|
1380
1478
|
}
|
|
1381
1479
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1480
|
+
@keyframes game-loader-field-fade {
|
|
1481
|
+
to {
|
|
1482
|
+
opacity: 0;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
.game-loader-field-exit {
|
|
1487
|
+
will-change: mask-size, opacity;
|
|
1488
|
+
animation:
|
|
1489
|
+
game-loader-reveal 660ms forwards,
|
|
1490
|
+
game-loader-field-fade 480ms 180ms ease-in forwards;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/* The resting mark tracks the same anticipation dip, then hands over to the
|
|
1494
|
+
hole at 27%. The two shapes coincide there, so the handover is invisible. */
|
|
1495
|
+
@keyframes game-loader-plug-dip {
|
|
1496
|
+
0% {
|
|
1497
|
+
transform: translate(-50%, -50%) scale(1);
|
|
1498
|
+
opacity: 1;
|
|
1499
|
+
animation-timing-function: cubic-bezier(0.33, 0, 0.55, 1);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
26% {
|
|
1503
|
+
transform: translate(-50%, -50%) scale(0.9);
|
|
1504
|
+
opacity: 1;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
27% {
|
|
1508
|
+
transform: translate(-50%, -50%) scale(0.9);
|
|
1509
|
+
opacity: 0;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
100% {
|
|
1513
|
+
transform: translate(-50%, -50%) scale(0.9);
|
|
1514
|
+
opacity: 0;
|
|
1515
|
+
}
|
|
1385
1516
|
}
|
|
1386
1517
|
|
|
1387
1518
|
.game-loader-mark-exit {
|
|
1388
|
-
|
|
1519
|
+
will-change: transform, opacity;
|
|
1520
|
+
animation: game-loader-plug-dip 660ms forwards;
|
|
1389
1521
|
}
|
|
1390
1522
|
|
|
1391
1523
|
/* Progress-bar shimmer — sweeps across the filled portion while a stage is
|
|
1392
|
-
still loading, so a held bar still reads as "working".
|
|
1524
|
+
still loading, so a held bar still reads as "working". The fill is now the
|
|
1525
|
+
brand colour on a canvas track, so the sweep is the on-brand white. */
|
|
1393
1526
|
@keyframes game-loader-sweep {
|
|
1394
1527
|
0% {
|
|
1395
1528
|
left: -60%;
|
|
@@ -1404,7 +1537,7 @@
|
|
|
1404
1537
|
background: linear-gradient(
|
|
1405
1538
|
90deg,
|
|
1406
1539
|
transparent,
|
|
1407
|
-
color-mix(in oklch, var(--
|
|
1540
|
+
color-mix(in oklch, var(--icon-on-brand-static) 35%, transparent),
|
|
1408
1541
|
transparent
|
|
1409
1542
|
);
|
|
1410
1543
|
animation: game-loader-sweep 1.4s ease-in-out infinite;
|
|
@@ -1849,8 +1982,7 @@
|
|
|
1849
1982
|
font-size: var(--stepper-stake-label-font-size);
|
|
1850
1983
|
line-height: var(--stepper-stake-label-line-height);
|
|
1851
1984
|
font-weight: var(--stepper-stake-label-font-weight);
|
|
1852
|
-
letter-spacing:
|
|
1853
|
-
text-transform: uppercase;
|
|
1985
|
+
letter-spacing: var(--stepper-stake-label-tracking);
|
|
1854
1986
|
}
|
|
1855
1987
|
|
|
1856
1988
|
.type-stepper-stake-amount {
|