division-ds 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -4
- package/division-ds-0.3.0.tgz +0 -0
- package/package.json +1 -1
- package/public/docs/components/command-palette.md +4 -1
- package/public/docs/components/date-picker.md +166 -0
- package/public/docs/components/date-presets.md +111 -0
- package/public/docs/components/date-range-picker.md +135 -0
- package/public/docs/components/time-field.md +127 -0
- package/public/manifest/components.json +870 -2
- package/scripts/lib/agent-docs.mjs +21 -0
- package/scripts/lib/mcp-tools.mjs +2 -14
- package/scripts/lib/search-score.mjs +24 -0
- package/skills/division-ds/SKILL.md +2 -2
- package/skills/division-ds/topics/forms.md +1 -1
- package/division-ds-0.2.1.tgz +0 -0
package/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Division DS: instructions for coding agents
|
|
2
2
|
|
|
3
|
-
A React 19 + Tailwind 4 + Radix design system:
|
|
3
|
+
A React 19 + Tailwind 4 + Radix design system: 72 components, tokens in three layers, light and
|
|
4
4
|
dark themes, WCAG 2.2 AA. This file is the entry point. It is short on purpose; everything it
|
|
5
5
|
points to is generated from the code, so it is current.
|
|
6
6
|
|
|
@@ -51,7 +51,7 @@ installs these rules into that project's `AGENTS.md`, `GEMINI.md` and Cursor rul
|
|
|
51
51
|
`.claude/skills/` (Claude Code).
|
|
52
52
|
|
|
53
53
|
An agent that supports MCP can have the docs and the same checks as tools instead:
|
|
54
|
-
`claude mcp add division-ds -- npx -y division-ds@0.
|
|
54
|
+
`claude mcp add division-ds -- npx -y division-ds@0.3.0 mcp` (`list_components`, `get_component`,
|
|
55
55
|
`search`, `get_tokens`, `validate_code`, `check_contrast`, `get_add_command`,
|
|
56
56
|
`get_review_checklist`).
|
|
57
57
|
|
|
@@ -91,7 +91,7 @@ provider. A project that uses the components mounts both itself (step 4 of `inst
|
|
|
91
91
|
`src/demo/states/` files are not part of the system.
|
|
92
92
|
|
|
93
93
|
**What the system does not have.** No Popover, no Combobox (single value: use `Select`; several:
|
|
94
|
-
`MultiSelect`)
|
|
94
|
+
`MultiSelect`). Do not reach into `src/demo/` from a component.
|
|
95
95
|
|
|
96
96
|
**Vocabulary.** Inside this repository, `npm run check:vocabulary` bans a list of words in `src/`,
|
|
97
97
|
`scripts/` and `e2e/` (the list is `scripts/lib/vocabulary.mjs`; it is not published with the
|
|
@@ -154,7 +154,7 @@ renders `ErrorSummary` at the top and focuses it; `useFieldErrors` holds the sta
|
|
|
154
154
|
`public/docs/helpers.md`).
|
|
155
155
|
|
|
156
156
|
A `RadioGroup` in a form sits in a `<fieldset>` whose `<legend>` names it (code on the Radio
|
|
157
|
-
page). A date is
|
|
157
|
+
page). A date is `DatePicker` in a `FormField`; its value is 'YYYY-MM-DD'.
|
|
158
158
|
|
|
159
159
|
**Buttons.** Two or more buttons side by side need a parent with a gap
|
|
160
160
|
(`flex flex-wrap gap-3`); `DialogFooter` and `CardFooter` already are one. A `Button` is
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "division-ds",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Checks a project that uses Division DS components (raw colours, invented tokens, touching buttons, unknown props), and serves the docs and the same checks to agents over MCP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Victor Ilioiu",
|
|
@@ -19,7 +19,7 @@ Paths are from the Division DS repository root. Installed with shadcn they start
|
|
|
19
19
|
|
|
20
20
|
| Prop | Type | Default | Description |
|
|
21
21
|
|---|---|---|---|
|
|
22
|
-
| `commands` | `Command[]` | | Required. Each has id, label, group, keywords, shortcut, icon and onSelect. |
|
|
22
|
+
| `commands` | `Command[]` | | Required. Each has id, label, description (a quieter second line), group, keywords, shortcut, icon and onSelect. |
|
|
23
23
|
| `open` | `boolean` | | Open state when controlled, meaning your code decides whether it is open. |
|
|
24
24
|
| `defaultOpen` | `boolean` | `false` | Starting open state when uncontrolled, meaning the palette keeps track of it itself. |
|
|
25
25
|
| `onOpenChange` | `(open: boolean) => void` | | Called on every open and close, including Escape and a click on the scrim (the dimmed backdrop). If you refuse the close, the chosen command stays ready to run until the user does anything else in the palette. Closing it yourself later counts as accepting it. |
|
|
@@ -28,6 +28,7 @@ Paths are from the Division DS repository root. Installed with shadcn they start
|
|
|
28
28
|
| `searchLabel` | `string` | `'Search commands'` | The search field's accessible name. |
|
|
29
29
|
| `placeholder` | `string` | `'Type a command or search'` | Shown in the empty search field. |
|
|
30
30
|
| `emptyMessage` | `string` | `'No matching commands'` | Shown when nothing matches the search. |
|
|
31
|
+
| `filter` | `(commands: readonly Command[], query: string) => Command[]` | `filterCommands` | Your own matching and ranking. Gets every command and the query, returns the ones to show in order; groups follow that order. The docs site search uses it to rank with the same scorer as the MCP server. |
|
|
31
32
|
| `ref` | `Ref<HTMLInputElement>` | | The search field, while the palette is open. |
|
|
32
33
|
|
|
33
34
|
## Types and helpers
|
|
@@ -38,6 +39,8 @@ Exported beside the component, from the same file. Import a type with `import ty
|
|
|
38
39
|
export type Command = {
|
|
39
40
|
id: string
|
|
40
41
|
label: string
|
|
42
|
+
/** A second, quieter line under the label, e.g. what a search result is for. */
|
|
43
|
+
description?: string
|
|
41
44
|
/** Commands with the same group are drawn together under its heading. */
|
|
42
45
|
group?: string
|
|
43
46
|
/** Extra words a search matches on. */
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# DatePicker
|
|
2
|
+
|
|
3
|
+
> One date, typed or picked. The field has a segment for the day, the month and the year, in the order your language writes them: type the digits and it moves on by itself, or use the arrow keys. The button inside it opens a calendar. The value is a plain 'YYYY-MM-DD' string with no time zone, so it means the same day for everyone. Calendar and DateField are exported too, for building your own.
|
|
4
|
+
|
|
5
|
+
- **Best for:** One date: a delivery day, a deadline, a birthday.
|
|
6
|
+
- **Not for:** A range: DateRangePicker. A time: TimeField.
|
|
7
|
+
- **Also on this page:** `Calendar`, `DateField`
|
|
8
|
+
- Molecule. Source: `src/components/molecules/date-picker/DatePicker.tsx`
|
|
9
|
+
- Rendered docs page: `/components/date-picker`
|
|
10
|
+
|
|
11
|
+
## Import
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Calendar } from 'src/components/molecules/date-picker/Calendar'
|
|
15
|
+
import { DateField } from 'src/components/molecules/date-picker/DateField'
|
|
16
|
+
import { DatePicker } from 'src/components/molecules/date-picker/DatePicker'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
|
|
20
|
+
|
|
21
|
+
## Props
|
|
22
|
+
|
|
23
|
+
| Prop | Type | Default | Description |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| `label` | `string` | | Required. Names the field. Inside a FormField, FormField sets it from its own label. |
|
|
26
|
+
| `value` | `string ('YYYY-MM-DD' \| '')` | | Controlled value (your page holds it). |
|
|
27
|
+
| `defaultValue` | `string` | `''` | Starting value when the picker holds the value itself. |
|
|
28
|
+
| `onValueChange` | `(value: string) => void` | | Fires with a whole, real date, or '' when the field is emptied. A half-typed date does not fire. |
|
|
29
|
+
| `min` | `string` | | Earliest date, 'YYYY-MM-DD'. Earlier days are struck through; typing one marks the field invalid. |
|
|
30
|
+
| `max` | `string` | | Latest date, 'YYYY-MM-DD'. |
|
|
31
|
+
| `isDateDisabled` | `(date: string) => boolean` | | Days that cannot be picked, such as weekends or holidays. |
|
|
32
|
+
| `locale` | `string` | | A language tag such as 'en-GB' or 'de'. Sets the segment order, the separator, the month and day names and the first day of the week. Defaults to the browser's. |
|
|
33
|
+
| `weekStartsOn` | `number` | | 0 Sunday to 6 Saturday. Defaults to the locale's. |
|
|
34
|
+
| `showToday` | `boolean` | `true` | Shows the Today button under the calendar when today can be picked. |
|
|
35
|
+
| `disabled` | `boolean` | `false` | Disables the field and the calendar button. |
|
|
36
|
+
| `name` | `string` | | Submits the 'YYYY-MM-DD' value with a form, through a hidden input. |
|
|
37
|
+
| `invalid` | `boolean` | | Marks the field invalid. FormField sets it when there is an error. |
|
|
38
|
+
| `required` | `boolean` | | Marks the field required. FormField sets it. |
|
|
39
|
+
| `id` | `string` | | The field's id, so a FormField's error text can point at it. FormField sets it. |
|
|
40
|
+
| `aria-describedby` | `string` | | The ids of the help and error text. FormField sets it. |
|
|
41
|
+
| `className` | `string` | | Combined with the field's classes. |
|
|
42
|
+
| `ref` | `Ref<HTMLDivElement>` | | The field's group element. |
|
|
43
|
+
| `trailing (DateField)` | `ReactNode` | | Something inside the field after the segments. DatePicker puts its calendar button here. |
|
|
44
|
+
| `bare (DateField)` | `boolean` | `false` | No border, fill or ring of its own, for two dates inside one border (DateRangePicker draws them). |
|
|
45
|
+
| `onSelect (Calendar)` | `(date: string) => void` | | A day was picked with a click, Enter or Space. |
|
|
46
|
+
| `value (Calendar)` | `string \| null` | | The picked day. Ignored when dayState is given. |
|
|
47
|
+
| `month (Calendar)` | `string` | | Any day in the first month shown. Controlled with onMonthChange. |
|
|
48
|
+
| `defaultMonth (Calendar)` | `string` | | The first month shown when the calendar holds it itself. Defaults to the value's month, or today's. |
|
|
49
|
+
| `onMonthChange (Calendar)` | `(month: string) => void` | | Fires with the new first month's 1st day. |
|
|
50
|
+
| `dayState (Calendar)` | `(date: string) => 'single' \| 'start' \| 'end' \| 'start-end' \| 'middle' \| undefined` | | Draws each day yourself, for a range or any other selection. |
|
|
51
|
+
| `onDayEnter (Calendar)` | `(date: string) => void` | | The keyboard or the pointer moved onto a day. A range uses it to preview the band. |
|
|
52
|
+
| `numberOfMonths (Calendar)` | `1 \| 2` | `1` | Months side by side, stacked on narrow screens. |
|
|
53
|
+
| `autoFocus (Calendar)` | `boolean` | `false` | Moves focus to the picked day, or today, when the calendar appears. |
|
|
54
|
+
|
|
55
|
+
## Types and helpers
|
|
56
|
+
|
|
57
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
/** How a day is drawn. `single` is one picked day; the rest belong to a range. */
|
|
61
|
+
export type DayState = 'single' | 'start' | 'end' | 'start-end' | 'middle'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Examples
|
|
65
|
+
|
|
66
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
67
|
+
|
|
68
|
+
### Default
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
<div className="w-full max-w-xs">
|
|
72
|
+
<DatePicker label="Delivery date" />
|
|
73
|
+
</div>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### In a FormField, with help text
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
<div className="w-full max-w-xs">
|
|
80
|
+
<FormField label="Trip date" help="Type it, or open the calendar.">
|
|
81
|
+
<DatePicker label="Trip date" />
|
|
82
|
+
</FormField>
|
|
83
|
+
</div>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Controlled: the page holds the value
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
function Controlled() {
|
|
90
|
+
const [date, setDate] = useState('2026-11-14')
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex w-full max-w-xs flex-col gap-2">
|
|
93
|
+
<DatePicker label="Dispatch date" value={date} onValueChange={setDate} />
|
|
94
|
+
<span className="font-mono text-xs text-[var(--color-text-secondary)]">value: {date ? `'${date}'` : "''"}</span>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
<Controlled />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Limits: weekdays only, within the next two months
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
const weekend = (date: string) => [0, 6].includes(new Date(`${date}T12:00:00Z`).getUTCDay())
|
|
106
|
+
|
|
107
|
+
<div className="w-full max-w-xs">
|
|
108
|
+
<DatePicker label="Collection day" min="2026-11-02" max="2026-12-31" defaultValue="2026-11-16" isDateDisabled={weekend} />
|
|
109
|
+
</div>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### US order and a Sunday start, from the locale
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
<div className="w-full max-w-xs">
|
|
116
|
+
<DatePicker label="Ship date" locale="en-US" defaultValue="2026-11-14" />
|
|
117
|
+
</div>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### The calendar on its own
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
function InlineCalendar() {
|
|
124
|
+
const [date, setDate] = useState<string | null>(null)
|
|
125
|
+
return (
|
|
126
|
+
<div className="flex flex-col gap-2">
|
|
127
|
+
<Calendar value={date} onSelect={setDate} />
|
|
128
|
+
<span className="font-mono text-xs text-[var(--color-text-secondary)]">{date ? formatLong(date) : 'No day picked'}</span>
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
<InlineCalendar />
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Keyboard
|
|
137
|
+
|
|
138
|
+
| Keys | Result |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `0–9` | Types into the focused segment and moves on once it is full. |
|
|
141
|
+
| `↑` `↓` | In the field: one more or one less in the segment. In the calendar: a week earlier or later. |
|
|
142
|
+
| `←` `→` | In the field: the previous or next segment. In the calendar: the day before or after. |
|
|
143
|
+
| `Backspace` | Clears the segment, or moves back when it is already empty. |
|
|
144
|
+
| `Page Up` `Page Down` | The same day a month earlier or later. With Shift, a year. |
|
|
145
|
+
| `Home` `End` | The first or last day of the week. |
|
|
146
|
+
| `Enter` `Space` | Picks the focused day and closes the calendar. |
|
|
147
|
+
| `Esc` | Closes the calendar without a change. |
|
|
148
|
+
|
|
149
|
+
## Do
|
|
150
|
+
|
|
151
|
+
- Store and send the 'YYYY-MM-DD' value as it is. It has no time zone, so it can't shift by a day.
|
|
152
|
+
- Set min and max to the real limits, so days nobody can book are struck through rather than refused after the fact.
|
|
153
|
+
- Wrap it in a FormField for a visible label, help text and errors.
|
|
154
|
+
|
|
155
|
+
## Don't
|
|
156
|
+
|
|
157
|
+
- Don't convert the value with new Date('2026-11-14'): that reads it as midnight UTC and can show the day before in the Americas.
|
|
158
|
+
- Don't hide unavailable days. Struck through and announced, they explain themselves.
|
|
159
|
+
- Don't use it for a date of birth without setting max to today.
|
|
160
|
+
|
|
161
|
+
## Anatomy
|
|
162
|
+
|
|
163
|
+
- **Field (DateField)**: A group named by the label, holding three spinbuttons (Day, Month, Year). Each says its value or "Empty". A date that does not exist (31 February) or falls outside min and max marks the field invalid.
|
|
164
|
+
- **Calendar button**: A ghost IconButton inside the field. Its name includes the picked date: "Choose date, Saturday, 14 November 2026".
|
|
165
|
+
- **Calendar**: A grid with one tab stop. Each day is a button named with the full date. The picked day is a solid accent square with a cut corner; today has aria-current and a short bar under its number. Days that can't be picked stay visible, struck through and announced as unavailable.
|
|
166
|
+
- **Today**: A secondary Button under the calendar, shown only when today can be picked.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# DatePresets
|
|
2
|
+
|
|
3
|
+
> Named periods such as Last 7 days, one click each. The defaults: Today, Yesterday, Last 7 days, Last 30 days, This month, Last month, This year, or your own. An add-on: put it in a DateRangePicker's aside and it reads and sets the picker's range by itself, then closes it. On its own it is a list you give a value and an onSelect. Installs DateRangePicker and DatePicker with it.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Reports and logs, where people want the usual periods.
|
|
6
|
+
- **Not for:** Exact dates: DateRangePicker alone.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/date-presets/DatePresets.tsx`
|
|
8
|
+
- Rendered docs page: `/components/date-presets`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { DatePresets, defaultPresets } from 'src/components/molecules/date-presets/DatePresets'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
|
|
17
|
+
|
|
18
|
+
## Props
|
|
19
|
+
|
|
20
|
+
| Prop | Type | Default | Description |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `presets` | `DatePreset[]` | `defaultPresets()` | Each { label, range }. Make your own, or start from defaultPresets(today). |
|
|
23
|
+
| `value` | `{ start: string; end: string }` | | The current range. Inside a DateRangePicker it comes from the picker. |
|
|
24
|
+
| `onSelect` | `(range: DateRange) => void` | | A preset was chosen. Inside a DateRangePicker the picker also takes it and closes. |
|
|
25
|
+
| `label` | `string` | `'Presets'` | The list's accessible name. |
|
|
26
|
+
| `className` | `string` | | Combined with the list's classes. |
|
|
27
|
+
| `ref` | `Ref<HTMLDivElement>` | | The list element. |
|
|
28
|
+
| `defaultPresets()` | `(today?: string) => DatePreset[]` | | The seven usual periods counted back from today ('YYYY-MM-DD'; defaults to the reader's today). Every one includes today. |
|
|
29
|
+
|
|
30
|
+
## Types and helpers
|
|
31
|
+
|
|
32
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
export type DatePreset = { label: string; range: DateRange }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
/** The usual periods, counted back from `today` (default: the reader's today). Each includes today. */
|
|
40
|
+
export function defaultPresets(today: IsoDate = todayIso()): DatePreset[]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
46
|
+
|
|
47
|
+
### Inside a DateRangePicker: no wiring
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<div className="w-full max-w-sm">
|
|
51
|
+
<DateRangePicker label="Activity" aside={<DatePresets />} />
|
|
52
|
+
</div>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### On its own
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
function OnItsOwn() {
|
|
59
|
+
const [range, setRange] = useState<DateRange>(defaultPresets()[2].range)
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex flex-col gap-3">
|
|
62
|
+
<DatePresets value={range} onSelect={setRange} />
|
|
63
|
+
<span className="font-mono text-xs text-[var(--color-text-secondary)]">
|
|
64
|
+
{range.start} to {range.end}
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
<OnItsOwn />
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Your own periods
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<div className="w-full max-w-sm">
|
|
77
|
+
<DateRangePicker
|
|
78
|
+
label="Season"
|
|
79
|
+
aside={
|
|
80
|
+
<DatePresets
|
|
81
|
+
label="Seasons"
|
|
82
|
+
presets={[
|
|
83
|
+
{ label: 'Winter 2026', range: { start: '2026-01-01', end: '2026-03-31' } },
|
|
84
|
+
{ label: 'Spring 2026', range: { start: '2026-04-01', end: '2026-06-30' } },
|
|
85
|
+
{ label: 'Summer 2026', range: { start: '2026-07-01', end: '2026-09-30' } },
|
|
86
|
+
]}
|
|
87
|
+
/>
|
|
88
|
+
}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Keyboard
|
|
94
|
+
|
|
95
|
+
| Keys | Result |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `Tab` | Moves through the presets. |
|
|
98
|
+
| `Enter` `Space` | Chooses the focused preset. |
|
|
99
|
+
|
|
100
|
+
## Do
|
|
101
|
+
|
|
102
|
+
- Keep the list short, seven or fewer.
|
|
103
|
+
- Name periods the way your users say them: "This quarter" if they plan in quarters.
|
|
104
|
+
|
|
105
|
+
## Don't
|
|
106
|
+
|
|
107
|
+
- Don't offer a preset nobody can pick, such as a future period when max is today.
|
|
108
|
+
|
|
109
|
+
## Anatomy
|
|
110
|
+
|
|
111
|
+
- **List**: A group named "Presets". Each period is a button; the one matching the current range is pressed (aria-pressed) and filled.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# DateRangePicker
|
|
2
|
+
|
|
3
|
+
> A start and an end date in one field, an add-on to DatePicker. Type both, or open two months side by side: the first day you pick is the start, the band follows you to the second, which sets the end and closes the calendar. Put DatePresets in `aside` for one-click periods like Last 7 days. Installs DatePicker with it.
|
|
4
|
+
|
|
5
|
+
- **Best for:** A booking, a report window, a leave request.
|
|
6
|
+
- **Not for:** One day: DatePicker.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/date-range-picker/DateRangePicker.tsx`
|
|
8
|
+
- Rendered docs page: `/components/date-range-picker`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { DateRangePicker, EMPTY_RANGE, useDateRangePicker } from 'src/components/molecules/date-range-picker/DateRangePicker'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
|
|
17
|
+
|
|
18
|
+
## Props
|
|
19
|
+
|
|
20
|
+
| Prop | Type | Default | Description |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `label` | `string` | | Required. Names the field. Inside a FormField, FormField sets it. |
|
|
23
|
+
| `value` | `{ start: string; end: string }` | | Controlled range. Each end is 'YYYY-MM-DD' or ''. |
|
|
24
|
+
| `defaultValue` | `{ start: string; end: string }` | `{ start: '', end: '' }` | Starting range when the picker holds it itself. |
|
|
25
|
+
| `onValueChange` | `(range: DateRange) => void` | | Fires when either end changes. Days picked in the calendar always arrive in order. |
|
|
26
|
+
| `min` | `string` | | Earliest date, 'YYYY-MM-DD'. |
|
|
27
|
+
| `max` | `string` | | Latest date, 'YYYY-MM-DD'. |
|
|
28
|
+
| `isDateDisabled` | `(date: string) => boolean` | | Days that cannot be picked. |
|
|
29
|
+
| `locale` | `string` | | Segment order, names and first day of the week. Defaults to the browser's. |
|
|
30
|
+
| `weekStartsOn` | `number` | | 0 Sunday to 6 Saturday. Defaults to the locale's. |
|
|
31
|
+
| `aside` | `ReactNode` | | Beside the months, in the popover. DatePresets reads and sets the range by itself here. |
|
|
32
|
+
| `partLabels` | `[string, string]` | `['Start date', 'End date']` | The names of the two dates. |
|
|
33
|
+
| `disabled` | `boolean` | `false` | Disables both dates and the button. |
|
|
34
|
+
| `invalid` | `boolean` | | Marks the field invalid. FormField sets it. |
|
|
35
|
+
| `required` | `boolean` | | Marks both dates required. FormField sets it. |
|
|
36
|
+
| `name` | `string` | | Submits name-start and name-end with a form. |
|
|
37
|
+
| `id` | `string` | | The field's id. FormField sets it. |
|
|
38
|
+
| `aria-describedby` | `string` | | The ids of the help and error text. FormField sets it. |
|
|
39
|
+
| `className` | `string` | | Combined with the field's classes. |
|
|
40
|
+
| `ref` | `Ref<HTMLDivElement>` | | The field's group element. |
|
|
41
|
+
| `useDateRangePicker()` | `() => { value: DateRange; select: (range: DateRange) => void } \| null` | | Inside the popover: the range, and a way to set it and close. For your own aside content. |
|
|
42
|
+
|
|
43
|
+
## Types and helpers
|
|
44
|
+
|
|
45
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
/** A start and an end, each 'YYYY-MM-DD' or '' while not chosen. */
|
|
49
|
+
export type DateRange = { start: IsoDate | ''; end: IsoDate | '' }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
export const EMPTY_RANGE: DateRange = { start: '', end: '' }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
/** The open picker's range, and a way to set it and close. DatePresets reads it, so a preset in
|
|
58
|
+
* `aside` needs no wiring. null outside a DateRangePicker. */
|
|
59
|
+
export const useDateRangePicker = () => useContext(Context)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Examples
|
|
63
|
+
|
|
64
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
65
|
+
|
|
66
|
+
### Default
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<div className="w-full max-w-sm">
|
|
70
|
+
<DateRangePicker label="Stay" />
|
|
71
|
+
</div>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### With DatePresets beside the months
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<div className="w-full max-w-sm">
|
|
78
|
+
<FormField label="Report period" help="Pick a preset, or two days.">
|
|
79
|
+
<DateRangePicker label="Report period" aside={<DatePresets />} />
|
|
80
|
+
</FormField>
|
|
81
|
+
</div>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Controlled: the page holds the range
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
function Controlled() {
|
|
88
|
+
const [range, setRange] = useState<DateRange>({ start: '2026-11-09', end: '2026-11-13' })
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex w-full max-w-sm flex-col gap-2">
|
|
91
|
+
<DateRangePicker label="Leave" value={range} onValueChange={setRange} />
|
|
92
|
+
<span className="font-mono text-xs text-[var(--color-text-secondary)]">
|
|
93
|
+
start: {range.start || '—'}, end: {range.end || '—'}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
<Controlled />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Limits: November and December only
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<div className="w-full max-w-sm">
|
|
106
|
+
<DateRangePicker label="Rental" min="2026-11-01" max="2026-12-30" defaultValue={{ start: '2026-11-20', end: '2026-11-23' }} />
|
|
107
|
+
</div>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Keyboard
|
|
111
|
+
|
|
112
|
+
| Keys | Result |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `0–9` `↑` `↓` | Types or steps the focused segment of either date. |
|
|
115
|
+
| `←` `→` | In the field: the next segment, across both dates. In the calendar: the day before or after, moving the band. |
|
|
116
|
+
| `Page Up` `Page Down` | A month earlier or later. With Shift, a year. |
|
|
117
|
+
| `Enter` `Space` | Picks the start, then the end, which closes the calendar. |
|
|
118
|
+
| `Esc` | Closes the calendar. A start picked without an end stays as the start. |
|
|
119
|
+
|
|
120
|
+
## Do
|
|
121
|
+
|
|
122
|
+
- Add DatePresets in aside when people mostly want the usual periods.
|
|
123
|
+
- Validate the length yourself if a range has a limit (a 30-day report), and say so in help text.
|
|
124
|
+
|
|
125
|
+
## Don't
|
|
126
|
+
|
|
127
|
+
- Don't use two separate DatePickers for a start and an end: nobody sees the band.
|
|
128
|
+
- Don't reorder a typed range for the user; the field marks it invalid so they can see what they typed.
|
|
129
|
+
|
|
130
|
+
## Anatomy
|
|
131
|
+
|
|
132
|
+
- **Field**: A group named by the label, holding two segmented dates (Start date, End date) and the calendar button. A typed end before the start marks it invalid.
|
|
133
|
+
- **Calendar**: Two months, stacked on narrow screens. The ends are solid accent squares, the days between a light accent band.
|
|
134
|
+
- **Hint**: "Pick the first day." or "Pick the last day.", under the months, announced when it changes.
|
|
135
|
+
- **Aside**: An optional column beside the months, for DatePresets.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# TimeField
|
|
2
|
+
|
|
3
|
+
> A time of day, an add-on to the date pickers. Hours and minutes are segments you type into or step with the arrow keys, like DateField; the clock button opens a list of times every step minutes. With a 12-hour clock there is an AM/PM segment. The value is 'HH:MM' on a 24-hour clock whatever is shown. Put it next to a DatePicker for a date and a time.
|
|
4
|
+
|
|
5
|
+
- **Best for:** A pickup slot, a meeting start, a shift.
|
|
6
|
+
- **Not for:** A duration: Input with a unit.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/time-field/TimeField.tsx`
|
|
8
|
+
- Rendered docs page: `/components/time-field`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { TimeField, hourCycleFor } from 'src/components/molecules/time-field/TimeField'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
|
|
17
|
+
|
|
18
|
+
## Props
|
|
19
|
+
|
|
20
|
+
| Prop | Type | Default | Description |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `label` | `string` | | Required. Names the field. Inside a FormField, FormField sets it. |
|
|
23
|
+
| `value` | `string ('HH:MM' \| '')` | | Controlled value, 24-hour. |
|
|
24
|
+
| `defaultValue` | `string` | `''` | Starting value when the field holds it itself. |
|
|
25
|
+
| `onValueChange` | `(value: string) => void` | | Fires with a whole time, or '' when emptied. |
|
|
26
|
+
| `hourCycle` | `12 \| 24` | | 12 adds an AM/PM segment. The value stays 24-hour. Defaults to the locale's clock. |
|
|
27
|
+
| `step` | `number` | `30` | Minutes between the times in the list. |
|
|
28
|
+
| `min` | `string` | | Earliest time, 'HH:MM'. Earlier times are left out of the list; typing one marks the field invalid. |
|
|
29
|
+
| `max` | `string` | | Latest time, 'HH:MM'. |
|
|
30
|
+
| `showSlots` | `boolean` | `true` | Shows the clock button and its list. |
|
|
31
|
+
| `locale` | `string` | | Decides the default clock. Defaults to the browser's. |
|
|
32
|
+
| `disabled` | `boolean` | `false` | Disables the field and the button. |
|
|
33
|
+
| `invalid` | `boolean` | | Marks the field invalid. FormField sets it. |
|
|
34
|
+
| `required` | `boolean` | | Marks the field required. FormField sets it. |
|
|
35
|
+
| `name` | `string` | | Submits the 'HH:MM' value with a form. |
|
|
36
|
+
| `id` | `string` | | The field's id. FormField sets it. |
|
|
37
|
+
| `aria-describedby` | `string` | | The ids of the help and error text. FormField sets it. |
|
|
38
|
+
| `className` | `string` | | Combined with the field's classes. |
|
|
39
|
+
| `ref` | `Ref<HTMLDivElement>` | | The field's group element. |
|
|
40
|
+
| `hourCycleFor()` | `(locale?: string) => 12 \| 24` | | The locale's own clock. |
|
|
41
|
+
|
|
42
|
+
## Types and helpers
|
|
43
|
+
|
|
44
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
/** 'HH:MM', 24-hour, or '' for no time. */
|
|
48
|
+
export type TimeValue = string
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
/** 12 or 24, from the locale's own clock. */
|
|
53
|
+
export function hourCycleFor(locale?: string): 12 | 24
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
59
|
+
|
|
60
|
+
### Default, 24-hour
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<div className="w-36">
|
|
64
|
+
<TimeField label="Start time" hourCycle={24} />
|
|
65
|
+
</div>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 12-hour, with AM and PM
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
<div className="w-40">
|
|
72
|
+
<TimeField label="Call time" hourCycle={12} defaultValue="14:30" />
|
|
73
|
+
</div>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### With a DatePicker: a date and a time
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
function DateAndTime() {
|
|
80
|
+
const [date, setDate] = useState('2026-11-14')
|
|
81
|
+
const [time, setTime] = useState('09:30')
|
|
82
|
+
return (
|
|
83
|
+
<div className="flex w-full max-w-md flex-col gap-3">
|
|
84
|
+
<div className="grid gap-3 sm:grid-cols-[1fr_9rem]">
|
|
85
|
+
<FormField label="Pickup date">
|
|
86
|
+
<DatePicker label="Pickup date" value={date} onValueChange={setDate} />
|
|
87
|
+
</FormField>
|
|
88
|
+
<FormField label="Pickup time">
|
|
89
|
+
<TimeField label="Pickup time" value={time} onValueChange={setTime} step={15} min="08:00" max="18:00" />
|
|
90
|
+
</FormField>
|
|
91
|
+
</div>
|
|
92
|
+
<span className="font-mono text-xs text-[var(--color-text-secondary)]">
|
|
93
|
+
{date && time ? `${date}T${time}` : 'Pick a date and a time'}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
<DateAndTime />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Keyboard
|
|
103
|
+
|
|
104
|
+
| Keys | Result |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `0–9` | Types the hour or minute and moves on once it is full. |
|
|
107
|
+
| `A` `P` | On the AM/PM segment, sets AM or PM. |
|
|
108
|
+
| `↑` `↓` | In the field: one more or one less. In the list: the next or previous time. |
|
|
109
|
+
| `←` `→` | The previous or next segment. |
|
|
110
|
+
| `Page Up` `Page Down` | In the list: six times earlier or later. |
|
|
111
|
+
| `Enter` `Space` | In the list: picks the time and closes it. |
|
|
112
|
+
| `Esc` | Closes the list without a change. |
|
|
113
|
+
|
|
114
|
+
## Do
|
|
115
|
+
|
|
116
|
+
- Set step to the real booking grid, such as 15 for 15-minute slots.
|
|
117
|
+
- Store the value with its date and the time zone it means, separately.
|
|
118
|
+
|
|
119
|
+
## Don't
|
|
120
|
+
|
|
121
|
+
- Don't parse 'HH:MM' with new Date(): keep it a string until you combine it with a date and a zone.
|
|
122
|
+
|
|
123
|
+
## Anatomy
|
|
124
|
+
|
|
125
|
+
- **Field**: A group named by the label: Hour, Minute and, on a 12-hour clock, AM or PM, each a spinbutton.
|
|
126
|
+
- **Clock button**: A ghost IconButton. Its name includes the time: "Choose time, 14:30".
|
|
127
|
+
- **Time list**: A listbox of times between min and max, every step minutes. It opens on the picked time, or the next slot after now.
|