division-ds 0.1.1 → 0.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.
Files changed (97) hide show
  1. package/AGENTS.md +16 -2
  2. package/README.md +11 -0
  3. package/division-ds-0.2.0.tgz +0 -0
  4. package/package.json +2 -2
  5. package/public/docs/components/accordion.md +205 -0
  6. package/public/docs/components/ai-label.md +88 -0
  7. package/public/docs/components/alert-dialog.md +290 -0
  8. package/public/docs/components/alert.md +173 -0
  9. package/public/docs/components/approval-request.md +146 -0
  10. package/public/docs/components/auth-form.md +130 -0
  11. package/public/docs/components/avatar-group.md +150 -0
  12. package/public/docs/components/avatar.md +177 -0
  13. package/public/docs/components/badge.md +176 -0
  14. package/public/docs/components/breadcrumbs.md +110 -0
  15. package/public/docs/components/button.md +202 -0
  16. package/public/docs/components/card.md +255 -0
  17. package/public/docs/components/carousel.md +128 -0
  18. package/public/docs/components/cart.md +170 -0
  19. package/public/docs/components/chart-panel.md +356 -0
  20. package/public/docs/components/chat-message.md +154 -0
  21. package/public/docs/components/chat-thread.md +242 -0
  22. package/public/docs/components/checkbox.md +147 -0
  23. package/public/docs/components/checkout-form.md +126 -0
  24. package/public/docs/components/command-palette.md +145 -0
  25. package/public/docs/components/data-table.md +399 -0
  26. package/public/docs/components/description-list.md +101 -0
  27. package/public/docs/components/dialog.md +195 -0
  28. package/public/docs/components/divider.md +95 -0
  29. package/public/docs/components/dropdown-menu.md +220 -0
  30. package/public/docs/components/empty-state.md +143 -0
  31. package/public/docs/components/footer.md +395 -0
  32. package/public/docs/components/form-field.md +213 -0
  33. package/public/docs/components/icon-button.md +125 -0
  34. package/public/docs/components/input.md +198 -0
  35. package/public/docs/components/kanban-board.md +298 -0
  36. package/public/docs/components/kbd.md +70 -0
  37. package/public/docs/components/link.md +96 -0
  38. package/public/docs/components/list-item.md +238 -0
  39. package/public/docs/components/loading-animation.md +70 -0
  40. package/public/docs/components/multi-select.md +200 -0
  41. package/public/docs/components/navbar.md +142 -0
  42. package/public/docs/components/newsletter-form.md +110 -0
  43. package/public/docs/components/page-header.md +149 -0
  44. package/public/docs/components/pagination.md +210 -0
  45. package/public/docs/components/product-card.md +169 -0
  46. package/public/docs/components/progress.md +99 -0
  47. package/public/docs/components/prompt-input.md +149 -0
  48. package/public/docs/components/quantity-stepper.md +138 -0
  49. package/public/docs/components/radio.md +140 -0
  50. package/public/docs/components/rating.md +104 -0
  51. package/public/docs/components/score-ring.md +93 -0
  52. package/public/docs/components/search.md +114 -0
  53. package/public/docs/components/section-heading.md +111 -0
  54. package/public/docs/components/segmented-control.md +124 -0
  55. package/public/docs/components/select.md +222 -0
  56. package/public/docs/components/sheet.md +143 -0
  57. package/public/docs/components/sidebar.md +191 -0
  58. package/public/docs/components/skeleton.md +133 -0
  59. package/public/docs/components/slider.md +112 -0
  60. package/public/docs/components/sparkline.md +126 -0
  61. package/public/docs/components/spinner.md +96 -0
  62. package/public/docs/components/stat.md +104 -0
  63. package/public/docs/components/status-tracker.md +143 -0
  64. package/public/docs/components/stepper.md +145 -0
  65. package/public/docs/components/switch.md +151 -0
  66. package/public/docs/components/tabs.md +178 -0
  67. package/public/docs/components/tag.md +103 -0
  68. package/public/docs/components/textarea.md +146 -0
  69. package/public/docs/components/timeline.md +150 -0
  70. package/public/docs/components/toast.md +192 -0
  71. package/public/docs/components/tool-call.md +105 -0
  72. package/public/docs/components/tooltip.md +106 -0
  73. package/public/manifest/components.json +189 -7
  74. package/public/manifest/tokens.json +39 -0
  75. package/scripts/division-ds.mjs +76 -37
  76. package/scripts/lib/agent-docs.mjs +59 -2
  77. package/scripts/lib/ai-copy.mjs +106 -0
  78. package/scripts/lib/button-spacing.mjs +5 -5
  79. package/scripts/lib/check-text.mjs +44 -0
  80. package/scripts/lib/contrast.mjs +111 -0
  81. package/scripts/lib/host-inline.mjs +59 -0
  82. package/scripts/lib/mcp-server.mjs +115 -0
  83. package/scripts/lib/mcp-tools.mjs +241 -0
  84. package/scripts/lib/props-validator.mjs +1 -1
  85. package/scripts/lib/rules-installer.mjs +42 -4
  86. package/skills/division-ds/SKILL.md +90 -0
  87. package/skills/division-ds/commands/add.md +25 -0
  88. package/skills/division-ds/commands/build.md +46 -0
  89. package/skills/division-ds/commands/fix.md +28 -0
  90. package/skills/division-ds/commands/review.md +25 -0
  91. package/skills/division-ds/topics/ai.md +7 -0
  92. package/skills/division-ds/topics/colour.md +11 -0
  93. package/skills/division-ds/topics/feedback.md +9 -0
  94. package/skills/division-ds/topics/forms.md +19 -0
  95. package/skills/division-ds/topics/layout.md +34 -0
  96. package/skills/division-ds/topics/motion.md +10 -0
  97. package/skills/division-ds/topics/overlays.md +9 -0
@@ -0,0 +1,142 @@
1
+ # Navbar
2
+
3
+ > The top of a page: the brand, the main links and a few actions. When its container gets narrow, the links move into a menu sheet. Nothing gets cut off, and nothing is hidden for good.
4
+
5
+ - **Best for:** The top of a site: the brand, up to about six main links and a few actions.
6
+ - **Not for:** App navigation across many grouped pages: use Sidebar.
7
+ - Organism. Source: `src/components/organisms/navbar/Navbar.tsx`
8
+ - Rendered docs page: `/components/navbar`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Navbar } from 'src/components/organisms/navbar/Navbar'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `label` | `string` | | Required. The name screen readers give the navigation region. |
21
+ | `brand` | `ReactNode` | | Required. The wordmark or logo, usually a link to the home page. |
22
+ | `items` | `NavbarItem[]` | | Required. Each item has label, href and current. |
23
+ | `actions` | `ReactNode` | | Controls on the right: icon buttons, a cart link, an account menu. Keep them small, because they stay visible at every width. |
24
+ | `renderLink` | `(props: LinkRenderProps) => ReactNode` | `a plain anchor` | Pass your router's link component here. Map href to its own prop, and pass className, aria-current and onClick through. |
25
+ | `menuLabel` | `string` | `'Menu'` | The menu button's name for screen readers, and the sheet's title. |
26
+ | `menuFooter` | `ReactNode` | | Shown in the menu sheet under the links. Use it for an action that doesn't fit in actions on a narrow bar. Hide the matching copy in actions below the breakpoint by wrapping it in an element with the classes hidden @2xl:flex. |
27
+ | `className` | `string` | | Added to the root classes. Where two Tailwind utility classes clash, the later one wins. |
28
+ | `ref` | `Ref<HTMLElement>` | | The header element. |
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 NavbarItem = { label: string; href: string; current?: boolean }
36
+ ```
37
+
38
+ ## Examples
39
+
40
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
41
+
42
+ ### Store navigation
43
+
44
+ ```tsx
45
+ const ITEMS = [
46
+ { label: 'Replicas', href: '#replicas' },
47
+ { label: 'Optics', href: '#optics', current: true },
48
+ { label: 'Parts', href: '#parts' },
49
+ { label: 'Care', href: '#care' },
50
+ { label: 'Field notes', href: '#field-notes' },
51
+ ]
52
+
53
+ const BRAND = (
54
+ <a
55
+ href="#home"
56
+ className="inline-flex items-center min-h-10 font-display font-bold uppercase tracking-[0.14em] text-[var(--color-text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
57
+ >
58
+ Hexline
59
+ </a>
60
+ )
61
+
62
+ const ACTIONS = (
63
+ <>
64
+ <IconButton label="Search" variant="ghost">
65
+ <Search aria-hidden="true" className="w-5 h-5" />
66
+ </IconButton>
67
+ <IconButton label="Cart, 2 items" variant="ghost" asChild>
68
+ <a href="#cart">
69
+ <ShoppingBag aria-hidden="true" className="w-5 h-5" />
70
+ </a>
71
+ </IconButton>
72
+ </>
73
+ )
74
+
75
+ <Navbar label="Store navigation" brand={BRAND} items={ITEMS} actions={ACTIONS} />
76
+ ```
77
+
78
+ ### A narrow container
79
+
80
+ ```tsx
81
+ const ITEMS = [
82
+ { label: 'Replicas', href: '#replicas' },
83
+ { label: 'Optics', href: '#optics', current: true },
84
+ { label: 'Parts', href: '#parts' },
85
+ { label: 'Care', href: '#care' },
86
+ { label: 'Field notes', href: '#field-notes' },
87
+ ]
88
+
89
+ const BRAND = (
90
+ <a
91
+ href="#home"
92
+ className="inline-flex items-center min-h-10 font-display font-bold uppercase tracking-[0.14em] text-[var(--color-text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
93
+ >
94
+ Hexline
95
+ </a>
96
+ )
97
+
98
+ const ACTIONS = (
99
+ <>
100
+ <IconButton label="Search" variant="ghost">
101
+ <Search aria-hidden="true" className="w-5 h-5" />
102
+ </IconButton>
103
+ <IconButton label="Cart, 2 items" variant="ghost" asChild>
104
+ <a href="#cart">
105
+ <ShoppingBag aria-hidden="true" className="w-5 h-5" />
106
+ </a>
107
+ </IconButton>
108
+ </>
109
+ )
110
+
111
+ <div className="max-w-sm">
112
+ <Navbar label="Store navigation, narrow" brand={BRAND} items={ITEMS} actions={ACTIONS} />
113
+ </div>
114
+ ```
115
+
116
+ ## Keyboard
117
+
118
+ | Keys | Result |
119
+ |---|---|
120
+ | `Tab` | Moves through the brand, each link and the actions, then the menu button when it shows. |
121
+ | `Enter` | Follows a link, or opens the menu. |
122
+ | `Esc` | Closes the menu and sends focus back to its button. |
123
+
124
+ ## Do
125
+
126
+ - Mark exactly one item as current.
127
+ - Keep actions to icon buttons with names, or one short button.
128
+ - If a page has more than one navbar, give each its own label.
129
+
130
+ ## Don't
131
+
132
+ - Don't put more than about six links in the bar. Group the rest under a page.
133
+ - Don't hide an action inside the menu that the bar shows at wide widths only.
134
+ - Don't mark a link as current with colour alone. The fill and aria-current both move with it.
135
+
136
+ ## Anatomy
137
+
138
+ - **Header**: The page's banner landmark, a region screen readers can jump straight to, at the top level of the page.
139
+ - **Brand**: The wordmark or logo, usually a link to the home page.
140
+ - **Links**: A named navigation region. The current page gets the signal fill, plus aria-current, an ARIA attribute (Accessible Rich Internet Applications, the labels screen readers rely on) that tells screen readers this is the page you are on.
141
+ - **Actions**: They stay visible at every width.
142
+ - **Menu**: When the container is narrower than 42rem (rem is a unit based on the root text size, usually 16 pixels), an icon button opens the same links in a Sheet. If the container widens past that breakpoint while the sheet is open, the sheet stays open. It doesn't close itself when the links reappear in the bar.
@@ -0,0 +1,110 @@
1
+ # NewsletterForm
2
+
3
+ > An email field, a line saying what you're agreeing to, and a button. It checks input the same way every form here does, is honest about waiting while it sends, and tells you when it worked.
4
+
5
+ - **Best for:** Collecting one email address for a newsletter, with a consent line.
6
+ - **Not for:** Creating an account: use AuthForm.
7
+ - Organism. Source: `src/components/organisms/newsletter-form/NewsletterForm.tsx`
8
+ - Rendered docs page: `/components/newsletter-form`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { NewsletterForm } from 'src/components/organisms/newsletter-form/NewsletterForm'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `label` | `string` | | Required. The form's accessible name, the name screen readers say, e.g. "Newsletter sign-up". Leaving it out is a compile error. |
21
+ | `onSubmit` | `(email: string) => void \| Promise<void>` | | Required. Called with the trimmed address once it passes validation. Resolve (finish successfully) to show the success state. Reject (fail) to show the alert, and what was typed stays in the field. |
22
+ | `consent` | `ReactNode` | | Required. The consent line. Leaving it out is a compile error. |
23
+ | `fieldLabel` | `string` | `'Email'` | The field's label and the name screen readers say for it, as one string. Say what the address is for when the page might have another email field, such as a footer under a sign-in form. |
24
+ | `submitLabel` | `string` | `'Subscribe'` | The button text. |
25
+ | `busyLabel` | `string` | `'Subscribing'` | What a screen reader hears while onSubmit is pending. |
26
+ | `successTitle` | `string` | `'You are subscribed'` | The success heading. |
27
+ | `successBody` | `(email: string) => ReactNode` | `email => `Updates go to ${email}.`` | The line under the success heading. It receives the subscribed address. |
28
+ | `headingLevel` | `2 \| 3 \| 4` | `3` | The level of the error summary heading and the success heading. Make it one below the heading of the section the form sits in. |
29
+ | `className` | `string` | | Added to the root element. |
30
+ | `ref` | `Ref<HTMLDivElement>` | | Passed to the root element, which stays on the page when the success message replaces the form. |
31
+
32
+ ## Examples
33
+
34
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
35
+
36
+ ### A sign-up that succeeds
37
+
38
+ ```tsx
39
+ const wait = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms))
40
+
41
+ const CONSENT = 'One email a month. Every one has an unsubscribe link.'
42
+
43
+ async function accept() {
44
+ await wait(700)
45
+ }
46
+
47
+ <div className="flex w-full flex-col gap-3">
48
+ <p className="font-body text-sm text-[var(--color-text-secondary)]">
49
+ Demo: any address is accepted after a short wait. Nothing is sent.
50
+ </p>
51
+ <NewsletterForm label="Field notes by email" consent={CONSENT} onSubmit={accept} headingLevel={4} />
52
+ </div>
53
+ ```
54
+
55
+ ### A sign-up that fails
56
+
57
+ ```tsx
58
+ const wait = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms))
59
+
60
+ async function refuse() {
61
+ await wait(700)
62
+ throw new Error('The demo server is switched off.')
63
+ }
64
+
65
+ <div className="flex w-full flex-col gap-3">
66
+ <p className="font-body text-sm text-[var(--color-text-secondary)]">
67
+ Demo: this one rejects every address, so you can see the alert, and the address you typed stays in the field.
68
+ </p>
69
+ <NewsletterForm
70
+ label="Restock alerts by email"
71
+ consent="An email when a sold-out replica is back. Nothing else."
72
+ submitLabel="Notify me"
73
+ busyLabel="Signing you up"
74
+ onSubmit={refuse}
75
+ headingLevel={4}
76
+ />
77
+ </div>
78
+ ```
79
+
80
+ ## Keyboard
81
+
82
+ | Keys | Result |
83
+ |---|---|
84
+ | `Tab` | Moves to the field, then the button. Screen readers read the consent line with the button. |
85
+ | `Enter` | In the field or on the button, it submits, and it does nothing while a submit is pending. On the error summary entry, it moves focus to the field. |
86
+
87
+ ## Do
88
+
89
+ - Say in the consent line how often you write and how to stop.
90
+ - Start the section it sits in with a heading that says what the emails are about.
91
+ - Reject onSubmit when the request fails. The form shows the alert and keeps the address.
92
+ - Set headingLevel to one below the section heading above it.
93
+
94
+ ## Don't
95
+
96
+ - Don't pre-tick anything, or sign people up as a side effect of another form.
97
+ - Don't put network code in it. onSubmit belongs to you; the form handles busy, failure and success.
98
+ - Don't place it on a raised panel and remove its fill. Field errors sit on surface.base.
99
+ - Don't ask for a name. Having just one field is the point.
100
+
101
+ ## Anatomy
102
+
103
+ - **Form**: Screen readers name it by the label prop. It sits on opaque surface.base, in a hairline frame with the corner crosshair.
104
+ - **Error summary**: Appears when a submit fails and takes focus. Its entry moves focus to the field. It waits for someone to try submitting: leaving the field (a blur) never brings it up on its own.
105
+ - **Email**: A FormField wrapped around an Input, set to type email, autocomplete email and required.
106
+ - **Consent**: Required. What subscribing means and how to stop. Screen readers read it as the submit button's description.
107
+ - **Submit**: A Button that keeps focus and shows it's busy while onSubmit is pending. aria-busy and aria-disabled, two ARIA attributes (Accessible Rich Internet Applications, the labels screen readers rely on), tell screen readers the same.
108
+ - **Submit error**: An alert region that is always there and gets its text when onSubmit fails. The visible message only appears then. It clears before the next try.
109
+ - **Success**: Takes the place of the form: a check icon, a heading that takes focus, and the address that was subscribed.
110
+ - **Status**: Hidden from view and present from the start. It announces the success only if focus had already left the form.
@@ -0,0 +1,149 @@
1
+ # PageHeader
2
+
3
+ > The top of a screen. It holds the page’s one h1 (the main, level-1 heading), and can add an eyebrow, a description, a breadcrumb trail and the page’s own actions at the far edge.
4
+
5
+ - **Best for:** The top of every screen: its one h1, with eyebrow, description, breadcrumbs and page actions.
6
+ - **Not for:** A section inside a page: use SectionHeading.
7
+ - Molecule. Source: `src/components/molecules/page-header/PageHeader.tsx`
8
+ - Rendered docs page: `/components/page-header`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { PageHeader } from 'src/components/molecules/page-header/PageHeader'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `title` | `string` | | Required. The page’s name, shown as its h1. |
21
+ | `eyebrow` | `ReactNode` | | A short label above the title: the area the page belongs to. |
22
+ | `description` | `ReactNode` | | One or two sentences under the title. |
23
+ | `actions` | `ReactNode` | | The page’s own actions, usually one or two Buttons. The component lays out the row and its gap. |
24
+ | `breadcrumbs` | `ReactNode` | | A Breadcrumbs element, drawn above the eyebrow. |
25
+ | `id` | `string` | | Goes on the h1, not the wrapper, so main can point aria-labelledby at it. That ARIA (Accessible Rich Internet Applications) attribute lets the main area take its name from the heading. |
26
+ | `level` | `1 \| 2` | `1` | Use 2 only for a page-style header inside a page that already has its h1 (a pane, a tab panel). It looks the same either way. |
27
+ | `className` | `string` | | Extra CSS classes, merged onto the outer element. When two Tailwind classes clash, the later one wins. |
28
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the outer element. |
29
+
30
+ ## Examples
31
+
32
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
33
+
34
+ ### Title and description
35
+
36
+ ```tsx
37
+ <PageHeader level={2} title="Orders" description="Every order from the last 90 days, newest first." />
38
+ ```
39
+
40
+ ### With an eyebrow and actions
41
+
42
+ ```tsx
43
+ <PageHeader
44
+ level={2}
45
+ eyebrow="Operations"
46
+ title="Inventory"
47
+ description="Stock across both depots, counted at close of day."
48
+ actions={
49
+ <>
50
+ <Button variant="secondary">Export</Button>
51
+ <Button>Add replica</Button>
52
+ </>
53
+ }
54
+ />
55
+ ```
56
+
57
+ ### Under a breadcrumb trail
58
+
59
+ ```tsx
60
+ <PageHeader
61
+ level={2}
62
+ breadcrumbs={
63
+ <Breadcrumbs
64
+ items={[
65
+ { label: 'Account', href: '#account' },
66
+ { label: 'Settings', href: '#settings' },
67
+ { label: 'Notifications' },
68
+ ]}
69
+ />
70
+ }
71
+ title="Notifications"
72
+ description="What we send, and where."
73
+ actions={<Button variant="secondary">Reset to defaults</Button>}
74
+ />
75
+ ```
76
+
77
+ ## Keyboard
78
+
79
+ | Keys | Result |
80
+ |---|---|
81
+ | `Tab` | Moves through the breadcrumb links, then the actions. The header itself never takes keyboard focus. |
82
+
83
+ ## Do
84
+
85
+ - Use one per screen, at the top, inside main.
86
+ - Keep the eyebrow to the area’s name (“Operations”), and the title to the page’s.
87
+ - Put the page’s primary action last in the actions row, so it lands at the far edge.
88
+ - When the page has more than one landmark (a named region screen readers can jump to), give the h1 an id and use it to name main.
89
+
90
+ ## Don't
91
+
92
+ - Don’t use it for a section inside a page. That’s a SectionHeading.
93
+ - Don’t put the page’s filters or tabs in the actions slot. They belong to the content under it.
94
+ - Don’t repeat the title in the eyebrow. The eyebrow names where the page sits, and the title names the page.
95
+ - Don’t add a second h1 inside the description or the actions.
96
+
97
+ ## Common mistakes
98
+
99
+ Wrong:
100
+
101
+ ```tsx
102
+ <PageHeader level={2} title="Orders" />
103
+ ```
104
+
105
+ Right:
106
+
107
+ ```tsx
108
+ <PageHeader title="Orders" />
109
+ ```
110
+
111
+ The examples on this page pass level={2} only because they sit inside this docs page, which has its own h1. A screen’s own header is its h1, so leave level out.
112
+
113
+ Wrong:
114
+
115
+ ```tsx
116
+ <PageHeader title="Inventory" />
117
+ <PageHeader title="Low stock" />
118
+ ```
119
+
120
+ Right:
121
+
122
+ ```tsx
123
+ <PageHeader title="Inventory" />
124
+ <SectionHeading level={2} title="Low stock" />
125
+ ```
126
+
127
+ A page has one h1. Every heading under it is a SectionHeading, at the level the page’s outline of headings needs.
128
+
129
+ Wrong:
130
+
131
+ ```tsx
132
+ <PageHeader title="Inventory" breadcrumbs={[{ label: "Operations" }]} />
133
+ ```
134
+
135
+ Right:
136
+
137
+ ```tsx
138
+ <PageHeader title="Inventory" breadcrumbs={<Breadcrumbs items={[{ label: "Operations", href: "/ops" }, { label: "Inventory" }]} />} />
139
+ ```
140
+
141
+ breadcrumbs is a slot, not an array. Pass a Breadcrumbs element, which keeps its own renderLink and maxItems.
142
+
143
+ ## Anatomy
144
+
145
+ - **Breadcrumbs**: Optional. A Breadcrumbs trail above everything else, first in reading order.
146
+ - **Eyebrow**: Optional. A small label: JetBrains Mono, 12px (pixels), semibold, uppercase, in text.muted. It's a paragraph above the h1 and isn't part of the heading's name.
147
+ - **Title**: The page’s h1. Oxanium, semibold, 24px, growing to 30px when the header gets 48rem or more of width (a rem is the base text size, usually 16px).
148
+ - **Description**: Optional. 16px body text in text.secondary, with lines no longer than 64 characters.
149
+ - **Actions**: Optional. The page’s own controls at the far edge, in a row with a 12px gap. The row drops under the title when it runs out of room.
@@ -0,0 +1,210 @@
1
+ # Pagination
2
+
3
+ > Moves through a long list one page at a time. The current page gets the signal fill. Where there are too many pages to fit, a gap stands in for the run that's hidden.
4
+
5
+ - **Best for:** Moving through a long list or table one page at a time.
6
+ - **Not for:** Steps in a form: use Stepper. Fewer than two pages: don't render anything.
7
+ - Molecule. Source: `src/components/molecules/pagination/Pagination.tsx`
8
+ - Rendered docs page: `/components/pagination`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Pagination, paginationRange } from 'src/components/molecules/pagination/Pagination'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `page` | `number` | | Counts from 1. A number outside the range is pulled back into it. |
21
+ | `pageCount` | `number` | | Below 1, nothing renders. |
22
+ | `onPageChange` | `(page: number) => void` | | It's never called with the page you're already on. |
23
+ | `variant` | `'numbered' \| 'compact' \| 'select'` | `'numbered'` | What sits between Previous and Next: the page buttons, a 'Page 3 of 20' readout (which screen readers announce politely, without interrupting, when it changes), or a page Select. Only use 'select' up to a few dozen pages. |
24
+ | `siblings` | `number` | `1` | How many pages show on each side of the current page, for 'numbered'. |
25
+ | `showFirstLast` | `boolean` | `false` | Adds First page and Last page icon buttons at the ends. Like Previous and Next, they go aria-disabled at the first and last page. |
26
+ | `showLabels` | `boolean` | `false` | Shows Previous and Next as text buttons ('Previous', 'Next') with their chevrons, instead of icons alone. |
27
+ | `showJump` | `boolean` | `false` | Adds a 'Go to page' field. Enter or Go jumps there, and a number past either end goes to that end. |
28
+ | `totalItems` | `number` | | Together with pageSize, it shows the range on view ('21–40 of 400') at the start and lays the control out as a table footer. |
29
+ | `pageSize` | `number` | | Items per page, used for the range text and the rows-per-page Select. |
30
+ | `onPageSizeChange` | `(pageSize: number) => void` | | Together with pageSize, it shows a rows-per-page Select. When it fires, reset page to 1: the old page may not exist any more. |
31
+ | `pageSizeOptions` | `number[]` | `[10, 20, 50, 100]` | The rows-per-page choices. |
32
+ | `label` | `string` | `'Pagination'` | The name of the nav landmark (a region screen readers can jump to). |
33
+ | `className` | `string` | | Added to the nav element. |
34
+ | `ref` | `Ref<HTMLElement>` | | Passed through to the nav DOM node (the element in the page). |
35
+
36
+ ## Types and helpers
37
+
38
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
39
+
40
+ ```ts
41
+ /** Pure. 1-based. Always includes page 1, the last page, the current page and `siblings` pages
42
+ * either side of it. A gap stands for two or more hidden pages; a single hidden page is shown
43
+ * instead, because a gap that hides one number costs the same space as the number. */
44
+ export function paginationRange(page: number, pageCount: number, siblings = 1): (number | 'gap')[]
45
+ ```
46
+
47
+ ## Examples
48
+
49
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
50
+
51
+ ### Few pages
52
+
53
+ ```tsx
54
+ <Pagination page={2} pageCount={5} onPageChange={() => {}} label="Pagination, few pages" />
55
+ ```
56
+
57
+ ### Many pages
58
+
59
+ ```tsx
60
+ function ManyPagesExample() {
61
+ const [page, setPage] = useState(1)
62
+ const perPage = 20
63
+ const total = 400
64
+ const from = (page - 1) * perPage + 1
65
+ const to = Math.min(page * perPage, total)
66
+
67
+ return (
68
+ <div>
69
+ <Pagination page={page} pageCount={20} onPageChange={setPage} label="Pagination, many pages" />
70
+ <p className="font-mono text-xs text-[var(--color-text-muted)] mt-3">
71
+ Showing {from} to {to} of {total} depots
72
+ </p>
73
+ </div>
74
+ )
75
+ }
76
+
77
+ <ManyPagesExample />
78
+ ```
79
+
80
+ ### Wider context: two page numbers on each side of the current page
81
+
82
+ ```tsx
83
+ <Pagination
84
+ page={10}
85
+ pageCount={20}
86
+ siblings={2}
87
+ onPageChange={() => {}}
88
+ label="Pagination, wider context"
89
+ />
90
+ ```
91
+
92
+ ### First and last
93
+
94
+ ```tsx
95
+ function Live(props: Omit<Parameters<typeof Pagination>[0], 'page' | 'onPageChange'>) {
96
+ const [page, setPage] = useState(4)
97
+ return <Pagination {...props} page={page} onPageChange={setPage} />
98
+ }
99
+
100
+ <Live pageCount={20} showFirstLast label="Pagination, first and last" />
101
+ ```
102
+
103
+ ### With labels
104
+
105
+ ```tsx
106
+ function Live(props: Omit<Parameters<typeof Pagination>[0], 'page' | 'onPageChange'>) {
107
+ const [page, setPage] = useState(4)
108
+ return <Pagination {...props} page={page} onPageChange={setPage} />
109
+ }
110
+
111
+ <Live pageCount={20} showLabels label="Pagination, with labels" />
112
+ ```
113
+
114
+ ### Compact
115
+
116
+ ```tsx
117
+ function Live(props: Omit<Parameters<typeof Pagination>[0], 'page' | 'onPageChange'>) {
118
+ const [page, setPage] = useState(4)
119
+ return <Pagination {...props} page={page} onPageChange={setPage} />
120
+ }
121
+
122
+ <Live variant="compact" pageCount={20} showFirstLast label="Pagination, compact" />
123
+ ```
124
+
125
+ ### Page select
126
+
127
+ ```tsx
128
+ function Live(props: Omit<Parameters<typeof Pagination>[0], 'page' | 'onPageChange'>) {
129
+ const [page, setPage] = useState(4)
130
+ return <Pagination {...props} page={page} onPageChange={setPage} />
131
+ }
132
+
133
+ <Live variant="select" pageCount={12} label="Pagination, page select" />
134
+ ```
135
+
136
+ ### Go to page
137
+
138
+ ```tsx
139
+ function Live(props: Omit<Parameters<typeof Pagination>[0], 'page' | 'onPageChange'>) {
140
+ const [page, setPage] = useState(4)
141
+ return <Pagination {...props} page={page} onPageChange={setPage} />
142
+ }
143
+
144
+ <Live pageCount={48} showJump label="Pagination, go to page" />
145
+ ```
146
+
147
+ ### Table footer
148
+
149
+ ```tsx
150
+ function TableFooterExample() {
151
+ const total = 397
152
+ const [page, setPage] = useState(2)
153
+ const [pageSize, setPageSize] = useState(20)
154
+ return (
155
+ <div className="w-full">
156
+ <Pagination
157
+ page={page}
158
+ pageCount={Math.ceil(total / pageSize)}
159
+ onPageChange={setPage}
160
+ totalItems={total}
161
+ pageSize={pageSize}
162
+ onPageSizeChange={size => {
163
+ setPageSize(size)
164
+ setPage(1)
165
+ }}
166
+ showFirstLast
167
+ label="Pagination, table footer"
168
+ />
169
+ </div>
170
+ )
171
+ }
172
+
173
+ <TableFooterExample />
174
+ ```
175
+
176
+ ## Keyboard
177
+
178
+ | Keys | Result |
179
+ |---|---|
180
+ | `Tab` | Moves across Previous, each page, and Next. |
181
+ | `Enter` `Space` | Goes to that page. |
182
+ | `Enter` | In the Go to page field, jumps to the page you typed. |
183
+
184
+ ## Do
185
+
186
+ - Say how many results there are somewhere near it: 'Showing 41 to 60 of 312'.
187
+ - After a change, move focus to the top of the new page of results.
188
+ - Keep siblings at 1 on narrow layouts.
189
+ - Under a data table, pass totalItems and pageSize. The footer then says what's on view, so you don't need a separate line of text.
190
+ - Use variant 'compact' where space is tight, such as a card or a phone layout.
191
+
192
+ ## Don't
193
+
194
+ - Don't paginate fewer than two pages; render nothing.
195
+ - Don't disable the current page; Tab would skip it.
196
+ - Don't use pagination for steps in a form.
197
+ - Don't use variant 'select' for hundreds of pages. A list that long needs showJump instead.
198
+ - Don't round the buttons into circles or pills; the square and the cut corner are this system's shape.
199
+
200
+ ## Anatomy
201
+
202
+ - **nav**: The navigation region, named 'Pagination'.
203
+ - **Previous / Next**: Ghost IconButtons (icon-only buttons with no fill). At the first and last page they're aria-disabled rather than disabled. ARIA (Accessible Rich Internet Applications) attributes tell screen readers about a control, and aria-disabled only marks the button as unavailable. It stays a real stop when you press Tab, so keyboard focus never drops to the page body when one stops working.
204
+ - **Page**: A small Button named 'Page n', with tabular figures (digits that all take the same width).
205
+ - **Current page**: The primary fill, with aria-current=page so screen readers say it's the current one. It isn't disabled, so Tab still reaches it.
206
+ - **Gap**: An ellipsis that stands for two or more hidden pages. It's aria-hidden, so screen readers skip it.
207
+ - **First / Last**: Optional (showFirstLast). Ghost IconButtons, aria-disabled at the first and last page.
208
+ - **Readout or page Select**: Takes the place of the page buttons when variant is 'compact' or 'select'.
209
+ - **Range and rows per page**: Optional (totalItems, pageSize, onPageSizeChange). These turn the control into a table footer: the range at the start, the controls at the end.
210
+ - **Go to page**: Optional (showJump). A small number Input with a visible label, and a Go button.