@trading-game/design-intelligence-layer 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ Date and date-range picker built on react-day-picker, restyled with circular day
6
6
 
7
7
  - Picking expiry dates, statement ranges, or tournament windows — single dates or ranges.
8
8
 
9
- **When not:** For choosing from a short list of preset periods use [Chip](./chip.md) rows or a Select. Embed the calendar in a Popover for input-triggered pickers.
9
+ **When not:** For choosing from a short list of preset periods use [Chip](./chip.md) rows or a Select. Embed the calendar in a Popover for input-triggered pickers. On phones, use [Date Wheel Picker](./date-wheel-picker.md) in a Drawer instead. The month grid is cramped at phone width and slow for dates years away.
10
10
 
11
11
  ## Anatomy
12
12
 
@@ -0,0 +1,106 @@
1
+ # Date Wheel Picker
2
+
3
+ The phone's date picker: three scroll-snap wheels (day / month / year) behind one selection band. The band is the only place the date shows.
4
+
5
+ ## When to use
6
+
7
+ - Picking a date on a phone, especially far from today: date of birth, document expiry, an order's end date.
8
+ - Inside a [Drawer](./drawer.md): the phone pattern. The picker owns no surface, header, or button, so the Drawer supplies the title and the footer supplies Save.
9
+
10
+ **When not:** On desktop, or for picking near dates where seeing the week matters, use [Calendar](./calendar.md) (in a [Popover](./popover.md)). For a date range use Calendar `mode="range"`. Don't pair the wheels with a text field showing the same date. The band already shows it, and two readouts of one value is the redundancy this component exists to remove.
11
+
12
+ ## Anatomy
13
+
14
+ - `DateWheelPicker`: `role="group"`, `aria-label="Date"`, a 200px row of three wheels with a 4px gap. `data-slot="date-wheel-picker"`.
15
+ - Band: a 40px `rounded-md` row in `bg-background-brand-selected`, fixed behind the middle row. `data-slot="date-wheel-picker-band"`.
16
+ - Wheels: one per part, `role="spinbutton"` with `aria-label` Day / Month / Year and `aria-valuetext` the visible label. `data-slot="date-wheel-picker-wheel"`. Five 40px rows show; edges fade with a mask.
17
+ - Rows: `type-date-wheel-picker-item`, `data-selected` on the row under the band, `data-disabled` on rows outside the range.
18
+
19
+ ## API
20
+
21
+ ### DateWheelPicker
22
+
23
+ | Prop | Type | Default | Notes |
24
+ | --- | --- | --- | --- |
25
+ | `value` | `Date` | — | Controlled date. The wheels always show a date. |
26
+ | `defaultValue` | `Date` | today | Uncontrolled start. Clamped to the range. |
27
+ | `onValueChange` | `(date: Date) => void` | — | Fires once a wheel settles (~120ms after scrolling stops). It never fires mid-spin. |
28
+ | `min` | `Date` | 1 Jan 1900 | Earliest selectable day (time ignored). |
29
+ | `max` | `Date` | 31 Dec, 10 years ahead | Latest selectable day (time ignored). |
30
+ | `order` | `"dmy" \| "mdy" \| "ymd"` | `"dmy"` | Wheel order, left to right. |
31
+ | `monthFormat` | `"long" \| "short" \| "numeric"` | `"long"` | `January` / `Jan` / `01`. Long months get a wider wheel. |
32
+ | `locale` | `string` | runtime locale | BCP 47 tag for month names. |
33
+
34
+ `defaultValue` and `onChange` are omitted from the div props; everything else passes through.
35
+
36
+ ## Variants & sizes
37
+
38
+ No variants and no sizes. The picker always uses the 40px row rail, five rows tall (200px), and fills its container's width.
39
+
40
+ ```tsx
41
+ import {
42
+ Button,
43
+ DateWheelPicker,
44
+ Drawer,
45
+ DrawerContent,
46
+ DrawerFooter,
47
+ DrawerHeader,
48
+ DrawerTitle,
49
+ } from "@trading-game/design-intelligence-layer"
50
+
51
+ // The profile pattern: "Add" opens a Drawer holding only the wheels and Save.
52
+ const today = new Date()
53
+ const adultCutoff = new Date(today.getFullYear() - 18, today.getMonth(), today.getDate())
54
+
55
+ const [draft, setDraft] = React.useState<Date>(dob ?? adultCutoff)
56
+
57
+ <Drawer open={open} onOpenChange={setOpen}>
58
+ <DrawerContent dismiss="close">
59
+ <DrawerHeader>
60
+ <DrawerTitle>{dob ? "Edit" : "Add"} date of birth</DrawerTitle>
61
+ </DrawerHeader>
62
+ <div className="px-4">
63
+ <DateWheelPicker value={draft} onValueChange={setDraft} max={adultCutoff} />
64
+ </div>
65
+ <DrawerFooter>
66
+ <Button onClick={() => save(draft)}>Save</Button>
67
+ </DrawerFooter>
68
+ </DrawerContent>
69
+ </Drawer>
70
+
71
+ // A future range, year first, numeric months:
72
+ <DateWheelPicker value={expiry} onValueChange={setExpiry} min={new Date()} order="ymd" monthFormat="numeric" />
73
+ ```
74
+
75
+ **Responsive:** render the wheels in a Drawer on phones and [Calendar](./calendar.md) in a Popover on desktop, switching on `useIsMobile()`. The demo app's Date Wheel Picker page shows both behind the screen-mode toggle.
76
+
77
+ ## Tokens
78
+
79
+ **Colour (semantic):**
80
+ - band: `bg-background-brand-selected`
81
+ - selected row: `text-text-brand-selected`. Other rows: `text-text-subtle-default`. Out of range: `text-text-disabled-default`
82
+ - focus: 3px `ring-ring-focus-strong` around the focused wheel
83
+
84
+ **Type (private):** `--date-wheel-picker-item-*`: 16 / 24, regular, with the selected row stepping to semibold (`--date-wheel-picker-item-selected-font-weight`) through a CSS data-attribute rule on `.type-date-wheel-picker-item[data-selected]`.
85
+
86
+ ## Behaviour
87
+
88
+ - **Settling.** Rows snap to the band. The highlight follows the finger live, and `onValueChange` fires only after the wheel has rested for ~120ms.
89
+ - **Range.** Dates outside `min`/`max` stay visible but greyed. A wheel that settles on one springs back to the nearest allowed row, and the highlight moves straight away rather than waiting for the animation. A month is disabled only when none of its days are in range. The year wheel lists only years in range.
90
+ - **Clamping.** Changing month or year clamps the day to the month's length (31 → 30, Feb 28/29 in leap years) and then to the range.
91
+ - **Always a value.** The wheels always show a date. To let people save without spinning, start the controlled value at the date the picker opens on, as the profile does with the 18-years-ago cutoff.
92
+ - **Keyboard.** Each wheel is a spinbutton: Arrow Up/Down step one allowed row, Page Up/Down step five, Home/End jump to the first or last allowed row.
93
+ - **Motion.** Smooth scrolling and the selected row's 4% scale both turn off under `prefers-reduced-motion`. On Android, each row passed gives a light vibration tick. iOS ignores it.
94
+
95
+ ## Do / Don't
96
+
97
+ **Do**
98
+ - Put it in a Drawer with a clear title ("Add date of birth") and a Save button in the footer.
99
+ - Set `max` for age rules and `min` for future-only dates, so impossible dates never reach validation.
100
+ - Open on a sensible date (the age cutoff, today, or the saved value) so most people spin a few rows, not decades.
101
+
102
+ **Don't**
103
+ - Don't add a text field or a readout that repeats the selected date.
104
+ - Don't use it on desktop. The calendar grid is faster with a mouse.
105
+ - Don't wrap it in its own card or border inside a Drawer. The Drawer is the surface.
106
+ - Don't restyle the band with brand washes (`bg-brand-default/10`). The selected family is what keeps it legible in dark.
@@ -21,7 +21,7 @@ Transient notification system — a themed wrapper around `sonner` on the invers
21
21
  | --- | --- | --- | --- |
22
22
  | ...props | sonner `ToasterProps` | — | All pass through; the wrapper presets the values below (overridable). |
23
23
 
24
- Preset defaults: `theme` from `next-themes` (`useTheme`, fallback `"system"`); `position` `"top-center"` on mobile / `"bottom-right"` on desktop (`useIsMobile`); `visibleToasts` 1 mobile / 3 desktop; `duration` 4000ms; status icons CircleCheck / Info / TriangleAlert / OctagonX / spinning Loader2, all `size-4`.
24
+ Preset defaults: `theme` `"system"` (no theme-provider dependency); `position` `"top-center"` on mobile / `"bottom-right"` on desktop (`useIsMobile`); `visibleToasts` 1 mobile / 3 desktop; `duration` 4000ms; status icons CircleCheck / Info / TriangleAlert / OctagonX / spinning Loader2, all `size-4`.
25
25
 
26
26
  ## Variants & sizes
27
27
 
@@ -49,7 +49,7 @@ toast.error("Deposit failed", {
49
49
 
50
50
  - Renders on the inverse surface: dark card on the light theme, light card on dark — with no border and no shadow class of its own.
51
51
  - Position and stack depth adapt to viewport: mobile shows a single toast top-center; desktop stacks up to three bottom-right.
52
- - Theme sync comes from `next-themes`, so the app must be inside a `ThemeProvider` for correct light/dark toasts.
52
+ - No theme provider needed: the toast paints from inverse tokens, which already follow the app's `dark` class. Sonner's `theme` prop only reaches its cancel and close buttons, so apps that use those can pass `theme` explicitly.
53
53
  - Every toast auto-dismisses after 4 seconds unless overridden per call.
54
54
  - All sonner options (promise toasts, custom duration, `richColors` off by default) remain available through the `toast` API and `Toaster` props.
55
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trading-game/design-intelligence-layer",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Trading Game Design System — shadcn/ui components with Tailwind CSS v4",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -25,12 +25,13 @@
25
25
  "scripts": {
26
26
  "dev": "next dev -H 127.0.0.1 -p 4000",
27
27
  "generate:registry": "node scripts/generate-component-registry.mjs",
28
- "build": "tsup",
28
+ "build": "tsup && npm run check:dist",
29
29
  "build:next": "next build",
30
30
  "start": "next start",
31
31
  "lint": "eslint",
32
32
  "prepublishOnly": "npm run build",
33
33
  "check:tokens": "node scripts/check-token-docs.mjs",
34
+ "check:dist": "node scripts/check-dist-imports.mjs",
34
35
  "predev": "rm -rf .next"
35
36
  },
36
37
  "files": [
package/src/styles.css CHANGED
@@ -1245,6 +1245,10 @@
1245
1245
  --numpad-font-size: var(--primitive-font-size-20);
1246
1246
  --numpad-font-weight: var(--primitive-font-weight-medium);
1247
1247
  --numpad-line-height: var(--primitive-line-height-28);
1248
+ --date-wheel-picker-item-font-size: var(--primitive-font-size-16);
1249
+ --date-wheel-picker-item-line-height: var(--primitive-line-height-24);
1250
+ --date-wheel-picker-item-font-weight: var(--primitive-font-weight-regular);
1251
+ --date-wheel-picker-item-selected-font-weight: var(--primitive-font-weight-semibold);
1248
1252
  /* ── Pagination — component type tokens (private) ── */
1249
1253
  --pagination-font-size: var(--primitive-font-size-14);
1250
1254
  --pagination-font-weight: var(--primitive-font-weight-semibold);
@@ -1820,6 +1824,17 @@
1820
1824
  font-weight: var(--numpad-font-weight);
1821
1825
  }
1822
1826
 
1827
+ /* Date Wheel Picker rows — the selected row steps to semibold. A data-attribute
1828
+ rule, not a variant: .type-* classes can't be gated by utilities. */
1829
+ .type-date-wheel-picker-item {
1830
+ font-size: var(--date-wheel-picker-item-font-size);
1831
+ line-height: var(--date-wheel-picker-item-line-height);
1832
+ font-weight: var(--date-wheel-picker-item-font-weight);
1833
+ }
1834
+ .type-date-wheel-picker-item[data-selected] {
1835
+ font-weight: var(--date-wheel-picker-item-selected-font-weight);
1836
+ }
1837
+
1823
1838
  .type-pagination {
1824
1839
  font-size: var(--pagination-font-size);
1825
1840
  line-height: var(--pagination-line-height);