@trading-game/design-intelligence-layer 0.17.4 → 1.0.1
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 +105 -231
- package/README.md +50 -746
- package/dist/index.cjs +2753 -2986
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +247 -261
- package/dist/index.d.ts +247 -261
- package/dist/index.js +2648 -2856
- package/dist/index.js.map +1 -1
- package/docs/components/accordion.md +85 -0
- package/docs/components/alert-dialog.md +98 -0
- package/docs/components/aspect-ratio.md +55 -0
- package/docs/components/avatar.md +88 -0
- package/docs/components/badge.md +75 -0
- package/docs/components/banner.md +84 -0
- package/docs/components/bottom-navigation.md +90 -0
- package/docs/components/breadcrumb.md +85 -0
- package/docs/components/button.md +94 -0
- package/docs/components/calendar.md +74 -0
- package/docs/components/card.md +79 -0
- package/docs/components/carousel.md +82 -0
- package/docs/components/checkbox.md +66 -0
- package/docs/components/chip.md +72 -0
- package/docs/components/command.md +86 -0
- package/docs/components/context-menu.md +90 -0
- package/docs/components/dialog.md +95 -0
- package/docs/components/drawer.md +97 -0
- package/docs/components/dropdown-menu.md +92 -0
- package/docs/components/empty.md +87 -0
- package/docs/components/field.md +117 -0
- package/docs/components/hover-card.md +77 -0
- package/docs/components/input-group.md +105 -0
- package/docs/components/input-otp.md +87 -0
- package/docs/components/input.md +71 -0
- package/docs/components/item.md +105 -0
- package/docs/components/label.md +56 -0
- package/docs/components/link.md +66 -0
- package/docs/components/menubar.md +102 -0
- package/docs/components/native-select.md +71 -0
- package/docs/components/navigation-button.md +68 -0
- package/docs/components/navigation-menu.md +99 -0
- package/docs/components/numpad.md +78 -0
- package/docs/components/pagination.md +84 -0
- package/docs/components/popover.md +89 -0
- package/docs/components/profile-photo.md +81 -0
- package/docs/components/progress.md +60 -0
- package/docs/components/radio-group.md +82 -0
- package/docs/components/resizable.md +79 -0
- package/docs/components/scroll-area.md +66 -0
- package/docs/components/section-message.md +99 -0
- package/docs/components/select.md +105 -0
- package/docs/components/separator.md +55 -0
- package/docs/components/sheet.md +91 -0
- package/docs/components/sidebar.md +125 -0
- package/docs/components/skeleton.md +51 -0
- package/docs/components/slider.md +61 -0
- package/docs/components/spinner.md +52 -0
- package/docs/components/stepper.md +68 -0
- package/docs/components/switch.md +57 -0
- package/docs/components/table.md +86 -0
- package/docs/components/tabs.md +95 -0
- package/docs/components/textarea.md +58 -0
- package/docs/components/toast.md +66 -0
- package/docs/components/toggle-group.md +77 -0
- package/docs/components/toggle.md +60 -0
- package/docs/components/tooltip.md +83 -0
- package/docs/foundations/colors.md +110 -0
- package/docs/foundations/motion.md +63 -0
- package/docs/foundations/shape-layout.md +56 -0
- package/docs/foundations/typography.md +83 -0
- package/docs/patterns/forms.md +70 -0
- package/docs/patterns/menus.md +50 -0
- package/docs/patterns/on-brand.md +43 -0
- package/guides/audits/design-system-audit-2026-07.md +135 -0
- package/guides/rules/design-system-consuming-project.mdc +56 -0
- package/package.json +5 -6
- package/src/styles.css +1634 -252
- package/guides/design-system-guide/trading-game-ds-guide.md +0 -933
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Navigation Menu
|
|
2
|
+
|
|
3
|
+
Site navigation with dropdown panels — triggers in a row, rich link panels beneath.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
- Top-level site/app navigation where sections expand into panels of links.
|
|
8
|
+
|
|
9
|
+
**When not:** For application command menus use [Menubar](./menubar.md). For a single circular back/forward control use [Navigation Button](./navigation-button.md). For page number navigation use [Pagination](./pagination.md).
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
- `NavigationMenu` — Radix root; renders the shared `NavigationMenuViewport` unless `viewport={false}`. `data-slot="navigation-menu"`, `data-viewport`.
|
|
14
|
+
- `NavigationMenuList` — horizontal `<ul>`, `gap-1`. `data-slot="navigation-menu-list"`.
|
|
15
|
+
- `NavigationMenuItem` — one entry. `data-slot="navigation-menu-item"`.
|
|
16
|
+
- `NavigationMenuTrigger` — `h-10 rounded-md px-4` button with an auto-appended `ChevronDownIcon` that rotates 180° while open. `data-slot="navigation-menu-trigger"`.
|
|
17
|
+
- `NavigationMenuContent` — the panel content; when `viewport={false}` it becomes its own `rounded-lg` bordered dropdown. `data-slot="navigation-menu-content"`.
|
|
18
|
+
- `NavigationMenuViewport` — shared panel frame below the bar: `rounded-lg`, hairline border, solid surface, animated height/width. `data-slot="navigation-menu-viewport"`.
|
|
19
|
+
- `NavigationMenuLink` — link row, `rounded-md p-2`. `data-slot="navigation-menu-link"`.
|
|
20
|
+
- `NavigationMenuIndicator` — small rotated-square arrow under the active trigger. `data-slot="navigation-menu-indicator"`.
|
|
21
|
+
- `navigationMenuTriggerStyle` — exported cva to give plain links the trigger look.
|
|
22
|
+
|
|
23
|
+
## API
|
|
24
|
+
|
|
25
|
+
### NavigationMenu
|
|
26
|
+
|
|
27
|
+
| Prop | Type | Default | Notes |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `viewport` | `boolean` | `true` | `true` = panels render in the single shared viewport; `false` = each content is its own dropdown under its item. |
|
|
30
|
+
|
|
31
|
+
### NavigationMenuLink
|
|
32
|
+
|
|
33
|
+
Radix `Link` props pass through; set `active` (renders `data-active=true`) to mark the current page, and use `asChild` for router links.
|
|
34
|
+
|
|
35
|
+
Other parts take Radix/native props plus `className` only.
|
|
36
|
+
|
|
37
|
+
## Variants & sizes
|
|
38
|
+
|
|
39
|
+
No size rail — fixed density from the shared menu type family.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import {
|
|
43
|
+
NavigationMenu,
|
|
44
|
+
NavigationMenuList,
|
|
45
|
+
NavigationMenuItem,
|
|
46
|
+
NavigationMenuTrigger,
|
|
47
|
+
NavigationMenuContent,
|
|
48
|
+
NavigationMenuLink,
|
|
49
|
+
navigationMenuTriggerStyle,
|
|
50
|
+
} from "@trading-game/design-intelligence-layer"
|
|
51
|
+
|
|
52
|
+
<NavigationMenu>
|
|
53
|
+
<NavigationMenuList>
|
|
54
|
+
<NavigationMenuItem>
|
|
55
|
+
<NavigationMenuTrigger>Markets</NavigationMenuTrigger>
|
|
56
|
+
<NavigationMenuContent>
|
|
57
|
+
<NavigationMenuLink active href="/markets/forex">Forex</NavigationMenuLink>
|
|
58
|
+
<NavigationMenuLink href="/markets/synthetics">Synthetics</NavigationMenuLink>
|
|
59
|
+
</NavigationMenuContent>
|
|
60
|
+
</NavigationMenuItem>
|
|
61
|
+
<NavigationMenuItem>
|
|
62
|
+
<NavigationMenuLink className={navigationMenuTriggerStyle()} href="/pricing">
|
|
63
|
+
Pricing
|
|
64
|
+
</NavigationMenuLink>
|
|
65
|
+
</NavigationMenuItem>
|
|
66
|
+
</NavigationMenuList>
|
|
67
|
+
</NavigationMenu>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Tokens
|
|
71
|
+
|
|
72
|
+
**Colour (semantic):**
|
|
73
|
+
- trigger/viewport/panels: `bg-background-primary-surface`, `border-border-default-default`, ink `text-text-prominent-default`
|
|
74
|
+
- hover/focus/open: `bg-background-hover-default` + `text-text-prominent-default`
|
|
75
|
+
- active link (`data-active=true`): `bg-background-brand-selected` + `text-text-brand-selected` — held on hover and focus
|
|
76
|
+
- link icons `text-icon-subtle-default` (16px default)
|
|
77
|
+
- focus: 3px `ring-ring-focus-strong`; indicator arrow `bg-border-default-default`
|
|
78
|
+
|
|
79
|
+
**Type (private):** shared `--menu-*` family — trigger `type-menu-trigger` 14/medium/20, links `type-menu-item` 14/regular/20.
|
|
80
|
+
|
|
81
|
+
## Behaviour
|
|
82
|
+
|
|
83
|
+
- With the default shared viewport, panels cross-fade and the viewport animates to each panel's size; content slides in from the direction of travel (`data-motion`).
|
|
84
|
+
- `viewport={false}` gives per-item dropdowns: `top-full mt-1.5`, own `rounded-lg` border and surface.
|
|
85
|
+
- The trigger chevron rotates 180° while open (`duration-slow`).
|
|
86
|
+
- The current page keeps the brand-selected fill and ink even when hovered or focused — selection outranks hover.
|
|
87
|
+
- On small screens content is full-width static (`md:absolute md:w-auto` gates the dropdown positioning).
|
|
88
|
+
|
|
89
|
+
## Do / Don't
|
|
90
|
+
|
|
91
|
+
**Do**
|
|
92
|
+
- Mark the current route with `active` on `NavigationMenuLink` so it takes the brand-selected pair.
|
|
93
|
+
- Use `navigationMenuTriggerStyle()` for panel-less top-level links so heights and hover states match triggers.
|
|
94
|
+
- Keep panels `rounded-lg` (10px) with `rounded-md` (8px) link rows.
|
|
95
|
+
|
|
96
|
+
**Don't**
|
|
97
|
+
- Don't repaint the active state — `bg-background-brand-selected`/`text-text-brand-selected` are the selection contract, and dark mode has its own values (solid #121A55 fill + blue-200 ink), not a mirror of light.
|
|
98
|
+
- Don't add shadows to the viewport or dropdowns; hairline on solid surface.
|
|
99
|
+
- Don't nest a `Link` component inside `NavigationMenuLink`; use `asChild`/`href` on the menu link itself.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Numpad
|
|
2
|
+
|
|
3
|
+
The mobile amount-entry tray — a bare 3×4 digit grid with no key backgrounds; only glazes mark a touch.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
- Mobile amount entry: stake, deposit, withdrawal amounts, paired with an amount display the consumer owns.
|
|
8
|
+
|
|
9
|
+
**When not:** For general text/number typing with a system keyboard use [Input](./input.md) (`inputMode="decimal"`). For code entry use [Input OTP](./input-otp.md).
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
- `Numpad` — the whole grid: `role="group"`, `aria-label="Number pad"`, `grid-cols-3 gap-1`. `data-slot="numpad"`.
|
|
14
|
+
- Keys — internal `<button>`s, `h-12 rounded-lg`, one per digit plus "." and backspace (a 22px `Delete` icon). `data-slot="numpad-key"`, `data-key`.
|
|
15
|
+
- With `decimal={false}` the "." cell is an empty `aria-hidden` span — the slot stays, the grid never reflows.
|
|
16
|
+
|
|
17
|
+
## API
|
|
18
|
+
|
|
19
|
+
### Numpad
|
|
20
|
+
|
|
21
|
+
| Prop | Type | Default | Notes |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| `onKeyPress` | `(key: NumpadKey) => void` | — | Fires for every tap. `NumpadKey = "0"–"9" \| "." \| "backspace"`. |
|
|
24
|
+
| `onClear` | `() => void` | — | Fires when backspace is long-pressed (~600ms). The release tap after a fired long-press is swallowed — no `onKeyPress("backspace")` follows. |
|
|
25
|
+
| `decimal` | `boolean` | `true` | `false` hides the "." key; its grid slot stays empty. |
|
|
26
|
+
| `disabled` | `boolean` | `false` | Dims the tray to 50% and blocks pointer events on grid and keys. |
|
|
27
|
+
|
|
28
|
+
`onKeyDown`/`onKeyPress` DOM handlers are omitted from the div props; everything else passes through.
|
|
29
|
+
|
|
30
|
+
## Variants & sizes
|
|
31
|
+
|
|
32
|
+
No variants; keys are fixed at 48px rows (`h-12`) in a full-width 3-column grid.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { Numpad, type NumpadKey } from "@trading-game/design-intelligence-layer"
|
|
36
|
+
|
|
37
|
+
const [amount, setAmount] = React.useState("")
|
|
38
|
+
|
|
39
|
+
<Numpad
|
|
40
|
+
onKeyPress={(key: NumpadKey) => {
|
|
41
|
+
if (key === "backspace") setAmount((a) => a.slice(0, -1))
|
|
42
|
+
else setAmount((a) => a + key)
|
|
43
|
+
}}
|
|
44
|
+
onClear={() => setAmount("")}
|
|
45
|
+
/>
|
|
46
|
+
|
|
47
|
+
// Integer-only entry:
|
|
48
|
+
<Numpad decimal={false} onKeyPress={handleKey} onClear={handleClear} />
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Tokens
|
|
52
|
+
|
|
53
|
+
**Colour (semantic):**
|
|
54
|
+
- keys: transparent at rest, `text-text-prominent-default`; backspace icon `text-icon-subtle-default`
|
|
55
|
+
- hover glaze `bg-background-hover-default`, press glaze `bg-background-pressed-default` — the only fills the keys ever get
|
|
56
|
+
- focus: 3px `ring-ring-focus-strong`
|
|
57
|
+
|
|
58
|
+
**Type (private):** `--numpad-*`: 20 / medium / 28 (`type-numpad`).
|
|
59
|
+
|
|
60
|
+
## Behaviour
|
|
61
|
+
|
|
62
|
+
- Fully controlled: the component owns no amount — it only emits keys; the consumer owns the string.
|
|
63
|
+
- Long-press clear: pointer-down on backspace starts a ~600ms timer; if it fires, `onClear` runs and the subsequent click is swallowed. Pointer-up or leaving the key cancels the timer. Context menu on backspace is suppressed so the long-press isn't hijacked on touch.
|
|
64
|
+
- The hold timer is cleaned up on unmount.
|
|
65
|
+
- Key order is 1–9 then [".", "0", backspace]; `decimal={false}` replaces "." with an empty `aria-hidden` span.
|
|
66
|
+
- Backspace's accessible name is "Backspace — hold to clear".
|
|
67
|
+
|
|
68
|
+
## Do / Don't
|
|
69
|
+
|
|
70
|
+
**Do**
|
|
71
|
+
- Own the amount string and its validation (decimal places, max) in the consumer — the tray is dumb by design.
|
|
72
|
+
- Wire `onClear`; users expect hold-to-clear on trading numpads.
|
|
73
|
+
- Keep keys at 48px height — the touch-target floor.
|
|
74
|
+
|
|
75
|
+
**Don't**
|
|
76
|
+
- Don't give keys resting backgrounds, borders, or shadows; per the design file, only the hover/press glazes mark a touch.
|
|
77
|
+
- Don't emit your own backspace on long-press release — the component already swallows it; double handling double-deletes.
|
|
78
|
+
- Don't collapse the empty "." slot when `decimal={false}`; the 0 key must stay centred under 8.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Pagination
|
|
2
|
+
|
|
3
|
+
Page navigation as a row of 40px circles — the active page lit with the brand-selected tokens.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
- Paged tables and lists: trade history, statements, leaderboards.
|
|
8
|
+
|
|
9
|
+
**When not:** For prev/next-only stepping without page numbers use two [Navigation Buttons](./navigation-button.md). For loading more inline, use a "Load more" Button.
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
- `Pagination` — `<nav role="navigation" aria-label="pagination">`, centred. `data-slot="pagination"`.
|
|
14
|
+
- `PaginationContent` — `<ul>` row, `gap-1`. `data-slot="pagination-content"`.
|
|
15
|
+
- `PaginationItem` — `<li>` wrapper. `data-slot="pagination-item"`.
|
|
16
|
+
- `PaginationLink` — a page number: `size-10` circle `<a>`. `data-slot="pagination-link"`, `data-active`.
|
|
17
|
+
- `PaginationPrevious` / `PaginationNext` — chevron links rendered as tertiary icon Buttons (`variant="tertiary" size="icon-md"`, 20px chevrons). `data-slot="pagination-link"`.
|
|
18
|
+
- `PaginationEllipsis` — non-interactive `size-10` cell with `MoreHorizontalIcon` + sr-only "More pages". `data-slot="pagination-ellipsis"`.
|
|
19
|
+
|
|
20
|
+
## API
|
|
21
|
+
|
|
22
|
+
### PaginationLink
|
|
23
|
+
|
|
24
|
+
| Prop | Type | Default | Notes |
|
|
25
|
+
| --- | --- | --- | --- |
|
|
26
|
+
| `isActive` | `boolean` | — | Sets `aria-current="page"` and the brand-selected fill/border/ink. |
|
|
27
|
+
|
|
28
|
+
`PaginationPrevious`/`PaginationNext` accept `<a>` props (`href`, etc.); the aria-labels "Go to previous page"/"Go to next page" are built in.
|
|
29
|
+
|
|
30
|
+
## Variants & sizes
|
|
31
|
+
|
|
32
|
+
One size: 40px circular cells.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import {
|
|
36
|
+
Pagination,
|
|
37
|
+
PaginationContent,
|
|
38
|
+
PaginationItem,
|
|
39
|
+
PaginationLink,
|
|
40
|
+
PaginationPrevious,
|
|
41
|
+
PaginationNext,
|
|
42
|
+
PaginationEllipsis,
|
|
43
|
+
} from "@trading-game/design-intelligence-layer"
|
|
44
|
+
|
|
45
|
+
<Pagination>
|
|
46
|
+
<PaginationContent>
|
|
47
|
+
<PaginationItem><PaginationPrevious href="?page=1" /></PaginationItem>
|
|
48
|
+
<PaginationItem><PaginationLink href="?page=1">1</PaginationLink></PaginationItem>
|
|
49
|
+
<PaginationItem><PaginationLink href="?page=2" isActive>2</PaginationLink></PaginationItem>
|
|
50
|
+
<PaginationItem><PaginationEllipsis /></PaginationItem>
|
|
51
|
+
<PaginationItem><PaginationLink href="?page=9">9</PaginationLink></PaginationItem>
|
|
52
|
+
<PaginationItem><PaginationNext href="?page=3" /></PaginationItem>
|
|
53
|
+
</PaginationContent>
|
|
54
|
+
</Pagination>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Tokens
|
|
58
|
+
|
|
59
|
+
**Colour (semantic):**
|
|
60
|
+
- rest page: transparent fill and border, `text-text-prominent-default`, hover `bg-background-hover-default`
|
|
61
|
+
- active page: `bg-background-brand-selected` + `border-border-brand-selected` + `text-text-brand-selected` (light: 10% brand tint + blue-600 ink; dark: solid #121A55 + blue-200 ink + blue-300 border)
|
|
62
|
+
- focus: 3px `ring-ring-focus-strong`
|
|
63
|
+
- prev/next inherit the tertiary Button recipe
|
|
64
|
+
|
|
65
|
+
**Type (private):** `--pagination-*`: 14 / semibold / 20 (`type-pagination`, `font-display`).
|
|
66
|
+
|
|
67
|
+
## Behaviour
|
|
68
|
+
|
|
69
|
+
- Pure links — no internal state; the consumer renders the right `isActive` page and hrefs.
|
|
70
|
+
- `isActive` drives both `aria-current="page"` and `data-active` plus the selected paint.
|
|
71
|
+
- Every cell carries a border (transparent at rest) so the active border-color change never shifts layout.
|
|
72
|
+
- Prev/next are real Buttons via `asChild`, so they inherit Button's hover/press/disabled behaviour.
|
|
73
|
+
|
|
74
|
+
## Do / Don't
|
|
75
|
+
|
|
76
|
+
**Do**
|
|
77
|
+
- Keep cells 40px circles (`size-10 rounded-full`) — buttons/chips round fully in this system.
|
|
78
|
+
- Use `PaginationEllipsis` for gaps; it stays non-interactive and announces "More pages" to screen readers.
|
|
79
|
+
- Render page numbers at the token type: 14/semibold/20 — semibold is the ceiling.
|
|
80
|
+
|
|
81
|
+
**Don't**
|
|
82
|
+
- Don't restyle the active cell with raw brand fills; the brand-selected trio is the selection contract, and dark theme is not a mirror.
|
|
83
|
+
- Don't turn page links into `<button>`s when they change the URL — they are navigation.
|
|
84
|
+
- Don't drop the transparent rest border; you'll get a 1px jump when the active ring appears.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Popover
|
|
2
|
+
|
|
3
|
+
Click-opened floating card for small interactive surfaces — filters, quick settings, confirm-lite content.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
- Small interactive panels anchored to a trigger: column pickers, mini forms, share sheets.
|
|
8
|
+
|
|
9
|
+
**When not:** For hover-only previews use [Hover Card](./hover-card.md). For command lists use a dropdown menu. For flows needing full attention use a Dialog.
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
- `Popover` — Radix root. `data-slot="popover"`.
|
|
14
|
+
- `PopoverTrigger` — the opening control. `data-slot="popover-trigger"`.
|
|
15
|
+
- `PopoverContent` — portalled card: 288px (`w-72`), `rounded-2xl`, `p-4`, hairline border, no shadow. `data-slot="popover-content"`.
|
|
16
|
+
- `PopoverAnchor` — optional custom anchor. `data-slot="popover-anchor"`.
|
|
17
|
+
- `PopoverHeader` — title + description stack, `gap-1`. `data-slot="popover-header"`.
|
|
18
|
+
- `PopoverTitle` — 14/medium/20 heading. `data-slot="popover-title"`.
|
|
19
|
+
- `PopoverDescription` — 14/regular/20 subtle copy. `data-slot="popover-description"`.
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
|
|
23
|
+
### Popover (root)
|
|
24
|
+
|
|
25
|
+
Radix `Popover.Root` props: `open`, `defaultOpen`, `onOpenChange`, `modal`.
|
|
26
|
+
|
|
27
|
+
### PopoverContent
|
|
28
|
+
|
|
29
|
+
| Prop | Type | Default | Notes |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `align` | `"start" \| "center" \| "end"` | `"center"` | |
|
|
32
|
+
| `sideOffset` | `number` | `4` | Gap to the anchor in px. |
|
|
33
|
+
|
|
34
|
+
Other Radix `Popover.Content` props (`side`, `collisionPadding`, …) pass through. Header/Title/Description take only element props plus `className`.
|
|
35
|
+
|
|
36
|
+
## Variants & sizes
|
|
37
|
+
|
|
38
|
+
No variants; one `w-72` card — widen via `className` when the content demands it.
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import {
|
|
42
|
+
Popover,
|
|
43
|
+
PopoverTrigger,
|
|
44
|
+
PopoverContent,
|
|
45
|
+
PopoverHeader,
|
|
46
|
+
PopoverTitle,
|
|
47
|
+
PopoverDescription,
|
|
48
|
+
Button,
|
|
49
|
+
} from "@trading-game/design-intelligence-layer"
|
|
50
|
+
|
|
51
|
+
<Popover>
|
|
52
|
+
<PopoverTrigger asChild>
|
|
53
|
+
<Button variant="secondary">Chart settings</Button>
|
|
54
|
+
</PopoverTrigger>
|
|
55
|
+
<PopoverContent align="end">
|
|
56
|
+
<PopoverHeader>
|
|
57
|
+
<PopoverTitle>Chart settings</PopoverTitle>
|
|
58
|
+
<PopoverDescription>Applies to this chart only.</PopoverDescription>
|
|
59
|
+
</PopoverHeader>
|
|
60
|
+
{/* controls */}
|
|
61
|
+
</PopoverContent>
|
|
62
|
+
</Popover>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Tokens
|
|
66
|
+
|
|
67
|
+
**Colour (semantic):**
|
|
68
|
+
- card: `bg-background-primary-surface` (solid), `border-border-default-default`, ink `text-text-prominent-default`
|
|
69
|
+
- title `text-text-prominent-default`; description `text-text-subtle-default`
|
|
70
|
+
|
|
71
|
+
**Type (private):** `--popover-title-*`: 14 / medium / 20; `--popover-description-*`: 14 / regular / 20. (The base `--popover`/`--popover-foreground` aliases map to primary surface / prominent ink.)
|
|
72
|
+
|
|
73
|
+
## Behaviour
|
|
74
|
+
|
|
75
|
+
- Radix popover semantics: opens on trigger click, closes on outside click and Escape, focus moves into the content.
|
|
76
|
+
- Content is portalled and animated (fade + 95% zoom + side-aware slide) from the trigger's transform origin.
|
|
77
|
+
- The card shell is the system overlay-card recipe: `rounded-2xl` (18px), hairline border, solid surface, **no shadow**.
|
|
78
|
+
|
|
79
|
+
## Do / Don't
|
|
80
|
+
|
|
81
|
+
**Do**
|
|
82
|
+
- Use `PopoverTitle`/`PopoverDescription` for headings — 14/medium and 14/regular subtle are the fixed pair.
|
|
83
|
+
- Keep the trigger a real focusable control (`asChild` with a Button).
|
|
84
|
+
- Use `align`/`sideOffset` rather than margin hacks to position the card.
|
|
85
|
+
|
|
86
|
+
**Don't**
|
|
87
|
+
- Don't add `shadow-*` classes — surfaces are solid and shadowless by law; the hairline is the depth cue.
|
|
88
|
+
- Don't shrink the 18px radius to menu radius; popovers are cards, not menus.
|
|
89
|
+
- Don't stack a popover inside a hover card; pick the interaction model that fits.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Profile Photo
|
|
2
|
+
|
|
3
|
+
Page-level profile photo with a verification progress ring, photo-change button, and status badge slot.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
- Account/profile pages where the photo carries verification progress and a change affordance.
|
|
8
|
+
|
|
9
|
+
**When not:** For identity chips in lists, nav, and comments use Avatar (24–48px) — ProfilePhoto is a fixed 112px page element, not a list glyph.
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
- `ProfilePhoto` — the 112px composite. `data-slot="profile-photo"`, `data-progress`, `data-complete`.
|
|
14
|
+
- Ring — SVG track + progress circle, 5px stroke flush to the outer edge, 3px gap to the photo. Progress arc: `data-slot="profile-photo-ring"`.
|
|
15
|
+
- Photo — 96px circle (`data-slot="profile-photo-image"`); placeholder is a secondary-surface tile with a 40px `User` glyph.
|
|
16
|
+
- Camera button — 24px primary icon Button (`data-slot="profile-photo-camera"`), hit area extended to ~44px via `after:-inset-2.5`.
|
|
17
|
+
- Status slot — centred over the bottom edge (`data-slot="profile-photo-status"`); pass a `Badge`.
|
|
18
|
+
|
|
19
|
+
## API
|
|
20
|
+
|
|
21
|
+
### ProfilePhoto
|
|
22
|
+
|
|
23
|
+
| Prop | Type | Default | Notes |
|
|
24
|
+
| --- | --- | --- | --- |
|
|
25
|
+
| `src` | `string` | — | Photo URL; omit for the placeholder glyph. |
|
|
26
|
+
| `alt` | `string` | `""` | Ignored when there is no `src`. |
|
|
27
|
+
| `progress` | `number` | `0` | Verification progress 0–100, clamped; drives the ring arc. |
|
|
28
|
+
| `onPhotoChange` | `() => void` | — | Shows the camera button; omit to hide it entirely (read-only contexts). |
|
|
29
|
+
| `photoChangeLabel` | `string` | `"Change photo"` | aria-label for the camera button. |
|
|
30
|
+
| `status` | `ReactNode` | — | Rendered overlapping the bottom edge; positioned, not styled — pass a `Badge`. |
|
|
31
|
+
|
|
32
|
+
`children` is excluded; other `div` props pass through.
|
|
33
|
+
|
|
34
|
+
## Variants & sizes
|
|
35
|
+
|
|
36
|
+
No variants; fixed geometry — 112px box, 96px photo, 5px ring, 3px gap.
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { ProfilePhoto, Badge } from "@trading-game/design-intelligence-layer"
|
|
40
|
+
|
|
41
|
+
<ProfilePhoto
|
|
42
|
+
src="/players/fahad.jpg"
|
|
43
|
+
alt="Fahad"
|
|
44
|
+
progress={60}
|
|
45
|
+
onPhotoChange={openUploader}
|
|
46
|
+
status={<Badge variant="warning">Verifying</Badge>}
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
// Read-only, unverified placeholder:
|
|
50
|
+
<ProfilePhoto progress={0} />
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Tokens
|
|
54
|
+
|
|
55
|
+
**Colour (semantic):**
|
|
56
|
+
- ring track: `var(--border-default-default)` (SVG stroke)
|
|
57
|
+
- progress arc: `var(--background-brand-default)`; at 100%: `var(--text-success-default)`
|
|
58
|
+
- placeholder tile: `bg-background-secondary-surface` + `text-icon-subtle-default` glyph
|
|
59
|
+
- camera chip: primary Button recipe with a 2px `border-prominent` ring against the photo
|
|
60
|
+
|
|
61
|
+
**Type (private):** none — no text of its own.
|
|
62
|
+
|
|
63
|
+
## Behaviour
|
|
64
|
+
|
|
65
|
+
- `progress` is clamped to 0–100; `data-complete` appears at ≥100 and swaps the arc stroke to the win colour.
|
|
66
|
+
- The arc starts and ends at 6 o'clock (SVG rotated 90°) so its seam hides behind the status badge, and animates between values (`transition-[stroke-dashoffset,stroke] duration-open ease-enter`, disabled under `motion-reduce`).
|
|
67
|
+
- The stroke path radius is inset by half the stroke (r = 53.5) so the 5px ring stays inside the 112px box.
|
|
68
|
+
- The camera button renders only when `onPhotoChange` is provided; at 24px it stays subordinate to the photo while the `::after` hit area meets the ~44px touch minimum.
|
|
69
|
+
- The status node is positioned (`-bottom-1`, centred) but never styled — content is the consumer's Badge.
|
|
70
|
+
|
|
71
|
+
## Do / Don't
|
|
72
|
+
|
|
73
|
+
**Do**
|
|
74
|
+
- Pass a system `Badge` into `status`; the component only owns its position.
|
|
75
|
+
- Omit `onPhotoChange` for viewers who cannot change the photo — hiding the button is the API, not CSS.
|
|
76
|
+
- Provide `alt` whenever `src` is set.
|
|
77
|
+
|
|
78
|
+
**Don't**
|
|
79
|
+
- Don't resize the composite; 112/96/5/3 is fixed geometry, and Avatar covers the small sizes.
|
|
80
|
+
- Don't recolour the ring track or arc — border-default track, brand arc, win at 100% is the verification vocabulary.
|
|
81
|
+
- Don't put arbitrary content in `status`; a long node will collide with the ring seam it is meant to cover.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Progress
|
|
2
|
+
|
|
3
|
+
Determinate progress bar — a brand wash track with a solid brand indicator.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
- Showing completion of a known-length process: upload progress, profile completeness, step progress.
|
|
8
|
+
|
|
9
|
+
**When not:** For unknown-length waits use Spinner or Skeleton. For circular verification progress on a profile use [Profile Photo](./profile-photo.md).
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
- `Progress` — Radix `Progress.Root` track: `h-2` (8px), `rounded-2xs`, overflow hidden. `data-slot="progress"`.
|
|
14
|
+
- Indicator — full-size bar translated left by the remaining percentage. `data-slot="progress-indicator"`.
|
|
15
|
+
|
|
16
|
+
## API
|
|
17
|
+
|
|
18
|
+
### Progress
|
|
19
|
+
|
|
20
|
+
| Prop | Type | Default | Notes |
|
|
21
|
+
| --- | --- | --- | --- |
|
|
22
|
+
| `value` | `number \| null` | — | 0–100. `null`/`undefined` renders as 0 (indicator fully translated out). |
|
|
23
|
+
|
|
24
|
+
Other Radix `Progress.Root` props (`max`, `getValueLabel`, …) pass through; note the fill math reads `value` directly against 100.
|
|
25
|
+
|
|
26
|
+
## Variants & sizes
|
|
27
|
+
|
|
28
|
+
No variants; one 8px bar. Width is fluid (`w-full`).
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { Progress } from "@trading-game/design-intelligence-layer"
|
|
32
|
+
|
|
33
|
+
<Progress value={66} />
|
|
34
|
+
<Progress value={verifiedSteps * 25} className="max-w-xs" />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Tokens
|
|
38
|
+
|
|
39
|
+
**Colour (semantic):**
|
|
40
|
+
- track: `bg-background-secondary-surface` — the shared track spec (h-2, rounded-full), same as Slider
|
|
41
|
+
- indicator: `bg-background-brand-default`, solid
|
|
42
|
+
|
|
43
|
+
**Type (private):** none.
|
|
44
|
+
|
|
45
|
+
## Behaviour
|
|
46
|
+
|
|
47
|
+
- The indicator is always full-width and is moved with `translateX(-(100 - value)%)`, so value changes animate smoothly via `transition-all`.
|
|
48
|
+
- `value || 0` means null/undefined/0 all show an empty bar — there is no indeterminate animation in this component.
|
|
49
|
+
- Radix provides the `progressbar` role and aria value attributes.
|
|
50
|
+
|
|
51
|
+
## Do / Don't
|
|
52
|
+
|
|
53
|
+
**Do**
|
|
54
|
+
- Keep the track as the 20% brand wash — track and fill are the same hue by design, separated by alpha.
|
|
55
|
+
- Constrain width with layout (`max-w-*`) rather than restyling the bar.
|
|
56
|
+
|
|
57
|
+
**Don't**
|
|
58
|
+
- Don't repaint the pair with grey tracks or gradient fills; brand wash + solid brand is the recipe.
|
|
59
|
+
- Don't fake indeterminate state by animating `value`; use Spinner for unknown durations.
|
|
60
|
+
- Don't change the 8px height or `rounded-2xs` radius per screen; it is a fixed primitive.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Radio Group
|
|
2
|
+
|
|
3
|
+
Pick-one control set — 16px hairline circles that take a brand border and dot when checked.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
- Mutually exclusive choices, 2–6 options, all visible at once (trade type, duration unit).
|
|
8
|
+
- As the control inside [Field](./field.md) choice cards.
|
|
9
|
+
|
|
10
|
+
**When not:** For many options use [Native Select](./native-select.md) or Select. For independent on/off choices use Checkbox; for a single toggle use Switch.
|
|
11
|
+
|
|
12
|
+
## Anatomy
|
|
13
|
+
|
|
14
|
+
- `RadioGroup` — Radix root, `grid gap-3`; owns value and roving focus. `data-slot="radio-group"`.
|
|
15
|
+
- `RadioGroupItem` — one radio: 16px (`size-4`) circle, hairline border on primary surface. `data-slot="radio-group-item"`.
|
|
16
|
+
- Indicator — centred 8px (`size-2`) brand dot, rendered only when checked. `data-slot="radio-group-indicator"`.
|
|
17
|
+
|
|
18
|
+
## API
|
|
19
|
+
|
|
20
|
+
### RadioGroup
|
|
21
|
+
|
|
22
|
+
Radix `RadioGroup.Root` props: `value`, `defaultValue`, `onValueChange`, `disabled`, `orientation`, `name`, `required`, `loop`.
|
|
23
|
+
|
|
24
|
+
### RadioGroupItem
|
|
25
|
+
|
|
26
|
+
| Prop | Type | Default | Notes |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `value` | `string` | required | The option's value. |
|
|
29
|
+
| …rest | Radix `Item` props | — | `disabled`, `id`, `aria-invalid`, etc. |
|
|
30
|
+
|
|
31
|
+
## Variants & sizes
|
|
32
|
+
|
|
33
|
+
No variants; one 16px size.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import {
|
|
37
|
+
RadioGroup,
|
|
38
|
+
RadioGroupItem,
|
|
39
|
+
Label,
|
|
40
|
+
} from "@trading-game/design-intelligence-layer"
|
|
41
|
+
|
|
42
|
+
<RadioGroup defaultValue="rise" onValueChange={setSide}>
|
|
43
|
+
<div className="flex items-center gap-2">
|
|
44
|
+
<RadioGroupItem value="rise" id="rise" />
|
|
45
|
+
<Label htmlFor="rise">Rise</Label>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="flex items-center gap-2">
|
|
48
|
+
<RadioGroupItem value="fall" id="fall" />
|
|
49
|
+
<Label htmlFor="fall">Fall</Label>
|
|
50
|
+
</div>
|
|
51
|
+
</RadioGroup>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Tokens
|
|
55
|
+
|
|
56
|
+
**Colour (semantic):**
|
|
57
|
+
- rest: `border-border-default-default` hairline on `bg-background-primary-surface`
|
|
58
|
+
- checked: `border-background-brand-default` ring + `fill-background-brand-default` dot (surface stays primary — no filled disc)
|
|
59
|
+
- focus: `border-ring-focus-default` + 3px `ring-ring-focus-strong` (control ring)
|
|
60
|
+
- error (`aria-invalid`): `border-border-error-default` + `ring-ring-error-soft`
|
|
61
|
+
|
|
62
|
+
**Type (private):** none — pair with [Label](./label.md) (14/medium/20).
|
|
63
|
+
|
|
64
|
+
## Behaviour
|
|
65
|
+
|
|
66
|
+
- Single-select with roving tabindex: Tab reaches the group once, arrows move and select within it (Radix).
|
|
67
|
+
- Controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
|
|
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` + 50% opacity.
|
|
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
|
+
|
|
72
|
+
## Do / Don't
|
|
73
|
+
|
|
74
|
+
**Do**
|
|
75
|
+
- Give every item an `id` and a `Label htmlFor` — the label is the touch target that makes 16px circles usable.
|
|
76
|
+
- Put `aria-invalid` on the items and `data-invalid` on the enclosing Field so ring and label ink agree.
|
|
77
|
+
- Use `gap-3` stacking as rendered; wrap each item + label row in a flex row.
|
|
78
|
+
|
|
79
|
+
**Don't**
|
|
80
|
+
- Don't fill the checked circle's background; brand border + brand dot on primary surface is the checked recipe.
|
|
81
|
+
- Don't add shadows or inner glows to states — hairline, ring, and dot carry everything.
|
|
82
|
+
- Don't manage checked state per-item; the group owns the single value.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Resizable
|
|
2
|
+
|
|
3
|
+
Draggable panel groups (wrapping `react-resizable-panels`) for splitting a region into user-adjustable panes.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
- Side-by-side work areas the user should be able to re-proportion — chart vs. order panel, list vs. detail.
|
|
8
|
+
- Nested horizontal/vertical splits in dense desktop layouts.
|
|
9
|
+
|
|
10
|
+
**When not:** For a fixed navigation column use [Sidebar](./sidebar.md). For a static divider with no drag affordance use [Separator](./separator.md).
|
|
11
|
+
|
|
12
|
+
## Anatomy
|
|
13
|
+
|
|
14
|
+
- `ResizablePanelGroup` — flex container, flips to `flex-col` when `aria-orientation="vertical"`; `data-slot="resizable-panel-group"`.
|
|
15
|
+
- `ResizablePanel` — pass-through panel from `react-resizable-panels`; `data-slot="resizable-panel"`.
|
|
16
|
+
- `ResizableHandle` — 1px drag separator (`w-px` / `h-px` horizontal) with an invisible 4px hit area (`after:w-1`); optional grip pill; `data-slot="resizable-handle"`.
|
|
17
|
+
|
|
18
|
+
## API
|
|
19
|
+
|
|
20
|
+
### ResizablePanelGroup
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Notes |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| `direction` | `"horizontal" \| "vertical"` | — | Required by `react-resizable-panels`. |
|
|
25
|
+
| ...props | `GroupProps` | — | All library props pass through. |
|
|
26
|
+
|
|
27
|
+
### ResizablePanel
|
|
28
|
+
|
|
29
|
+
| Prop | Type | Default | Notes |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `defaultSize` / `minSize` / `maxSize` | `number` | — | Percentages, from the library. |
|
|
32
|
+
| ...props | `PanelProps` | — | Pass-through. |
|
|
33
|
+
|
|
34
|
+
### ResizableHandle
|
|
35
|
+
|
|
36
|
+
| Prop | Type | Default | Notes |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| `withHandle` | `boolean` | `false` | Renders the grip pill (`h-4 w-3 rounded-xs`, `GripVerticalIcon` at `size-2.5`, rotated 90° when horizontal). |
|
|
39
|
+
|
|
40
|
+
## Variants & sizes
|
|
41
|
+
|
|
42
|
+
No cva variants and no size prop — one hairline handle, with or without the grip.
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import {
|
|
46
|
+
ResizablePanelGroup,
|
|
47
|
+
ResizablePanel,
|
|
48
|
+
ResizableHandle,
|
|
49
|
+
} from "@trading-game/design-intelligence-layer"
|
|
50
|
+
|
|
51
|
+
<ResizablePanelGroup direction="horizontal">
|
|
52
|
+
<ResizablePanel defaultSize={70}>Chart</ResizablePanel>
|
|
53
|
+
<ResizableHandle withHandle />
|
|
54
|
+
<ResizablePanel defaultSize={30}>Positions</ResizablePanel>
|
|
55
|
+
</ResizablePanelGroup>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Tokens
|
|
59
|
+
|
|
60
|
+
**Colour (semantic):** `bg-border-default-default` (handle line and grip fill), `border-border-default-default` (grip border), `text-icon-subtle-default` (grip icon), `ring-ring-focus-default` (keyboard focus, `ring-1` + `ring-offset-1`).
|
|
61
|
+
**Type (private):** none — the component renders no text.
|
|
62
|
+
|
|
63
|
+
## Behaviour
|
|
64
|
+
|
|
65
|
+
- Orientation is driven by the library's `aria-orientation` attribute, not a prop on the handle: horizontal groups get a `h-px w-full` handle and the grip rotates 90°.
|
|
66
|
+
- The visible line is 1px; the `after:` pseudo-element widens the pointer target to 4px on the drag axis.
|
|
67
|
+
- Keyboard resize focus uses `ring-1 ring-ring-focus-default ring-offset-1` — a thinner ring than the standard 3px control ring, because the handle itself is only 1px.
|
|
68
|
+
|
|
69
|
+
## Do / Don't
|
|
70
|
+
|
|
71
|
+
**Do**
|
|
72
|
+
- Keep the handle at the default 1px hairline — it matches every other divider in the system.
|
|
73
|
+
- Use `withHandle` when the split is a primary interaction; the bare hairline alone is easy to miss.
|
|
74
|
+
- Set `minSize` on panels holding controls so they cannot collapse below usability.
|
|
75
|
+
|
|
76
|
+
**Don't**
|
|
77
|
+
- Don't thicken or recolour the handle to make it "visible" — the 4px hit area already handles discoverability.
|
|
78
|
+
- Don't add shadows to panels — surfaces are solid and flat everywhere in the system.
|
|
79
|
+
- Don't use Resizable for mobile layouts; drag-resize is a desktop pattern.
|