@kennycorrea/ui 0.11.4 → 1.6.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/CHANGELOG.md +182 -0
- package/README.md +251 -1
- package/dist/atoms/ActionMenu/ActionMenu.d.ts +37 -0
- package/dist/atoms/ActionMenu/ActionMenu.js +28 -0
- package/dist/atoms/ActionMenu/index.d.ts +1 -0
- package/dist/atoms/ActionMenu/index.js +1 -0
- package/dist/atoms/ActionSheet/ActionSheet.d.ts +10 -4
- package/dist/atoms/ActionSheet/ActionSheet.js +6 -15
- package/dist/atoms/ActionSheet/confirm.d.ts +1 -1
- package/dist/atoms/ActionSheet/confirm.js +1 -1
- package/dist/atoms/Avatar/Avatar.d.ts +1 -1
- package/dist/atoms/Badge/Badge.js +4 -4
- package/dist/atoms/Button/Button.js +2 -3
- package/dist/atoms/Button/Button.types.d.ts +5 -8
- package/dist/atoms/ContextMenu/ContextMenu.d.ts +1 -1
- package/dist/atoms/ContextMenu/ContextMenu.js +1 -1
- package/dist/atoms/Icon/Icon.d.ts +7 -9
- package/dist/atoms/Icon/Icon.js +23 -8
- package/dist/atoms/Pill/Pill.d.ts +17 -0
- package/dist/atoms/Pill/Pill.js +12 -0
- package/dist/atoms/Pill/index.d.ts +1 -0
- package/dist/atoms/Pill/index.js +1 -0
- package/dist/atoms/ResponsiveDialog/ResponsiveDialog.d.ts +9 -2
- package/dist/atoms/ResponsiveDialog/ResponsiveDialog.js +6 -15
- package/dist/atoms/StatusIndicator/StatusIndicator.d.ts +14 -0
- package/dist/atoms/StatusIndicator/StatusIndicator.js +8 -0
- package/dist/atoms/StatusIndicator/index.d.ts +1 -0
- package/dist/atoms/StatusIndicator/index.js +1 -0
- package/dist/atoms/TextLink/TextLink.d.ts +36 -0
- package/dist/atoms/TextLink/TextLink.js +17 -0
- package/dist/atoms/TextLink/index.d.ts +1 -0
- package/dist/atoms/TextLink/index.js +1 -0
- package/dist/atoms/Toast/Toast.d.ts +1 -1
- package/dist/atoms/Toast/Toast.js +1 -1
- package/dist/atoms/index.d.ts +4 -0
- package/dist/atoms/index.js +4 -0
- package/dist/hooks/breakpoints.d.ts +20 -0
- package/dist/hooks/breakpoints.js +13 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/useControllableState.d.ts +17 -0
- package/dist/hooks/useControllableState.js +22 -0
- package/dist/hooks/useElementWidth.d.ts +7 -0
- package/dist/hooks/useElementWidth.js +22 -0
- package/dist/hooks/useMediaQuery.d.ts +18 -0
- package/dist/hooks/useMediaQuery.js +51 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/molecules/AccentPicker/AccentPicker.d.ts +30 -0
- package/dist/molecules/AccentPicker/AccentPicker.js +27 -0
- package/dist/molecules/AccentPicker/index.d.ts +1 -0
- package/dist/molecules/AccentPicker/index.js +1 -0
- package/dist/molecules/Breadcrumbs/Breadcrumbs.d.ts +27 -0
- package/dist/molecules/Breadcrumbs/Breadcrumbs.js +40 -0
- package/dist/molecules/Breadcrumbs/index.d.ts +1 -0
- package/dist/molecules/Breadcrumbs/index.js +1 -0
- package/dist/molecules/Calendar/Calendar.d.ts +42 -0
- package/dist/molecules/Calendar/Calendar.js +111 -0
- package/dist/molecules/Calendar/index.d.ts +1 -0
- package/dist/molecules/Calendar/index.js +1 -0
- package/dist/molecules/DateInput/DateInput.d.ts +35 -0
- package/dist/molecules/DateInput/DateInput.js +41 -0
- package/dist/molecules/DateInput/index.d.ts +1 -0
- package/dist/molecules/DateInput/index.js +1 -0
- package/dist/molecules/EmptyState/EmptyState.d.ts +19 -0
- package/dist/molecules/EmptyState/EmptyState.js +7 -0
- package/dist/molecules/EmptyState/index.d.ts +1 -0
- package/dist/molecules/EmptyState/index.js +1 -0
- package/dist/molecules/ErrorState/ErrorState.d.ts +18 -0
- package/dist/molecules/ErrorState/ErrorState.js +12 -0
- package/dist/molecules/ErrorState/index.d.ts +1 -0
- package/dist/molecules/ErrorState/index.js +1 -0
- package/dist/molecules/ErrorSummary/ErrorSummary.d.ts +25 -0
- package/dist/molecules/ErrorSummary/ErrorSummary.js +32 -0
- package/dist/molecules/ErrorSummary/index.d.ts +1 -0
- package/dist/molecules/ErrorSummary/index.js +1 -0
- package/dist/molecules/FilterChips/FilterChips.d.ts +10 -0
- package/dist/molecules/FilterChips/FilterChips.js +13 -0
- package/dist/molecules/FilterChips/index.d.ts +1 -0
- package/dist/molecules/FilterChips/index.js +1 -0
- package/dist/molecules/FilterPopover/FilterPopover.d.ts +27 -0
- package/dist/molecules/FilterPopover/FilterPopover.js +32 -0
- package/dist/molecules/FilterPopover/index.d.ts +1 -0
- package/dist/molecules/FilterPopover/index.js +1 -0
- package/dist/molecules/FormActions/FormActions.d.ts +16 -0
- package/dist/molecules/FormActions/FormActions.js +5 -0
- package/dist/molecules/FormActions/index.d.ts +1 -0
- package/dist/molecules/FormActions/index.js +1 -0
- package/dist/molecules/FormField/FormField.d.ts +52 -0
- package/dist/molecules/FormField/FormField.js +63 -0
- package/dist/molecules/FormField/index.d.ts +1 -0
- package/dist/molecules/FormField/index.js +1 -0
- package/dist/molecules/FormSection/FormSection.d.ts +14 -0
- package/dist/molecules/FormSection/FormSection.js +10 -0
- package/dist/molecules/FormSection/index.d.ts +1 -0
- package/dist/molecules/FormSection/index.js +1 -0
- package/dist/molecules/KeyValueList/KeyValueList.d.ts +22 -0
- package/dist/molecules/KeyValueList/KeyValueList.js +9 -0
- package/dist/molecules/KeyValueList/index.d.ts +1 -0
- package/dist/molecules/KeyValueList/index.js +1 -0
- package/dist/molecules/LoadingState/LoadingState.d.ts +20 -0
- package/dist/molecules/LoadingState/LoadingState.js +22 -0
- package/dist/molecules/LoadingState/index.d.ts +1 -0
- package/dist/molecules/LoadingState/index.js +1 -0
- package/dist/molecules/NavItem/NavItem.d.ts +31 -0
- package/dist/molecules/NavItem/NavItem.js +72 -0
- package/dist/molecules/NavItem/context.d.ts +14 -0
- package/dist/molecules/NavItem/context.js +5 -0
- package/dist/molecules/NavItem/index.d.ts +1 -0
- package/dist/molecules/NavItem/index.js +1 -0
- package/dist/molecules/Pagination/Pagination.d.ts +32 -0
- package/dist/molecules/Pagination/Pagination.js +57 -0
- package/dist/molecules/Pagination/index.d.ts +1 -0
- package/dist/molecules/Pagination/index.js +1 -0
- package/dist/molecules/PasswordInput/PasswordInput.d.ts +14 -0
- package/dist/molecules/PasswordInput/PasswordInput.js +16 -0
- package/dist/molecules/PasswordInput/index.d.ts +1 -0
- package/dist/molecules/PasswordInput/index.js +1 -0
- package/dist/molecules/SearchField/SearchField.d.ts +24 -0
- package/dist/molecules/SearchField/SearchField.js +58 -0
- package/dist/molecules/SearchField/index.d.ts +1 -0
- package/dist/molecules/SearchField/index.js +1 -0
- package/dist/molecules/StatGroup/StatGroup.d.ts +21 -0
- package/dist/molecules/StatGroup/StatGroup.js +10 -0
- package/dist/molecules/StatGroup/index.d.ts +1 -0
- package/dist/molecules/StatGroup/index.js +1 -0
- package/dist/molecules/ThemeToggle/ThemeToggle.d.ts +23 -0
- package/dist/molecules/ThemeToggle/ThemeToggle.js +26 -0
- package/dist/molecules/ThemeToggle/index.d.ts +1 -0
- package/dist/molecules/ThemeToggle/index.js +1 -0
- package/dist/molecules/UserMenu/UserMenu.d.ts +20 -0
- package/dist/molecules/UserMenu/UserMenu.js +14 -0
- package/dist/molecules/UserMenu/index.d.ts +1 -0
- package/dist/molecules/UserMenu/index.js +1 -0
- package/dist/molecules/index.d.ts +21 -0
- package/dist/molecules/index.js +21 -0
- package/dist/organisms/AppShell/AppShell.d.ts +29 -0
- package/dist/organisms/AppShell/AppShell.js +34 -0
- package/dist/organisms/AppShell/context.d.ts +14 -0
- package/dist/organisms/AppShell/context.js +6 -0
- package/dist/organisms/AppShell/index.d.ts +1 -0
- package/dist/organisms/AppShell/index.js +1 -0
- package/dist/organisms/AuthForms/AuthForms.d.ts +83 -0
- package/dist/organisms/AuthForms/AuthForms.js +57 -0
- package/dist/organisms/AuthForms/index.d.ts +2 -0
- package/dist/organisms/AuthForms/index.js +1 -0
- package/dist/organisms/AuthForms/shared.d.ts +36 -0
- package/dist/organisms/AuthForms/shared.js +36 -0
- package/dist/organisms/BulkActionBar/BulkActionBar.d.ts +25 -0
- package/dist/organisms/BulkActionBar/BulkActionBar.js +18 -0
- package/dist/organisms/BulkActionBar/index.d.ts +1 -0
- package/dist/organisms/BulkActionBar/index.js +1 -0
- package/dist/organisms/Chart/CategoryCharts.d.ts +34 -0
- package/dist/organisms/Chart/CategoryCharts.js +93 -0
- package/dist/organisms/Chart/ChartFrame.d.ts +26 -0
- package/dist/organisms/Chart/ChartFrame.js +18 -0
- package/dist/organisms/Chart/DonutChart.d.ts +25 -0
- package/dist/organisms/Chart/DonutChart.js +53 -0
- package/dist/organisms/Chart/Sparkline.d.ts +15 -0
- package/dist/organisms/Chart/Sparkline.js +18 -0
- package/dist/organisms/Chart/index.d.ts +5 -0
- package/dist/organisms/Chart/index.js +5 -0
- package/dist/organisms/Chart/shared.d.ts +51 -0
- package/dist/organisms/Chart/shared.js +113 -0
- package/dist/organisms/DataTable/DataTable.d.ts +63 -0
- package/dist/organisms/DataTable/DataTable.js +119 -0
- package/dist/organisms/DataTable/index.d.ts +3 -0
- package/dist/organisms/DataTable/index.js +3 -0
- package/dist/organisms/DataTable/types.d.ts +39 -0
- package/dist/organisms/DataTable/types.js +1 -0
- package/dist/organisms/DataTable/useClientTable.d.ts +27 -0
- package/dist/organisms/DataTable/useClientTable.js +51 -0
- package/dist/organisms/DateRangePicker/DateRangePicker.d.ts +43 -0
- package/dist/organisms/DateRangePicker/DateRangePicker.js +54 -0
- package/dist/organisms/DateRangePicker/index.d.ts +1 -0
- package/dist/organisms/DateRangePicker/index.js +1 -0
- package/dist/organisms/FilterBar/FilterBar.d.ts +28 -0
- package/dist/organisms/FilterBar/FilterBar.js +16 -0
- package/dist/organisms/FilterBar/index.d.ts +1 -0
- package/dist/organisms/FilterBar/index.js +1 -0
- package/dist/organisms/PageHeader/PageHeader.d.ts +31 -0
- package/dist/organisms/PageHeader/PageHeader.js +19 -0
- package/dist/organisms/PageHeader/index.d.ts +1 -0
- package/dist/organisms/PageHeader/index.js +1 -0
- package/dist/organisms/PageTabs/PageTabs.d.ts +27 -0
- package/dist/organisms/PageTabs/PageTabs.js +31 -0
- package/dist/organisms/PageTabs/index.d.ts +1 -0
- package/dist/organisms/PageTabs/index.js +1 -0
- package/dist/organisms/Sidebar/Sidebar.d.ts +48 -0
- package/dist/organisms/Sidebar/Sidebar.js +61 -0
- package/dist/organisms/Sidebar/index.d.ts +1 -0
- package/dist/organisms/Sidebar/index.js +1 -0
- package/dist/organisms/Topbar/Topbar.d.ts +24 -0
- package/dist/organisms/Topbar/Topbar.js +18 -0
- package/dist/organisms/Topbar/index.d.ts +1 -0
- package/dist/organisms/Topbar/index.js +1 -0
- package/dist/organisms/index.d.ts +11 -0
- package/dist/organisms/index.js +11 -0
- package/dist/strings.d.ts +108 -15
- package/dist/strings.js +73 -7
- package/dist/templates/AdminLayout/AdminLayout.d.ts +14 -0
- package/dist/templates/AdminLayout/AdminLayout.js +9 -0
- package/dist/templates/AdminLayout/index.d.ts +1 -0
- package/dist/templates/AdminLayout/index.js +1 -0
- package/dist/templates/AuthLayout/AuthLayout.d.ts +28 -0
- package/dist/templates/AuthLayout/AuthLayout.js +11 -0
- package/dist/templates/AuthLayout/index.d.ts +1 -0
- package/dist/templates/AuthLayout/index.js +1 -0
- package/dist/templates/DashboardPage/DashboardPage.d.ts +25 -0
- package/dist/templates/DashboardPage/DashboardPage.js +12 -0
- package/dist/templates/DashboardPage/index.d.ts +1 -0
- package/dist/templates/DashboardPage/index.js +1 -0
- package/dist/templates/DetailPage/DetailPage.d.ts +20 -0
- package/dist/templates/DetailPage/DetailPage.js +11 -0
- package/dist/templates/DetailPage/index.d.ts +1 -0
- package/dist/templates/DetailPage/index.js +1 -0
- package/dist/templates/FormPage/FormPage.d.ts +26 -0
- package/dist/templates/FormPage/FormPage.js +11 -0
- package/dist/templates/FormPage/index.d.ts +1 -0
- package/dist/templates/FormPage/index.js +1 -0
- package/dist/templates/ListPage/ListPage.d.ts +17 -0
- package/dist/templates/ListPage/ListPage.js +8 -0
- package/dist/templates/ListPage/index.d.ts +1 -0
- package/dist/templates/ListPage/index.js +1 -0
- package/dist/templates/SettingsPage/SettingsPage.d.ts +32 -0
- package/dist/templates/SettingsPage/SettingsPage.js +13 -0
- package/dist/templates/SettingsPage/index.d.ts +1 -0
- package/dist/templates/SettingsPage/index.js +1 -0
- package/dist/templates/StatusPage/StatusPage.d.ts +18 -0
- package/dist/templates/StatusPage/StatusPage.js +8 -0
- package/dist/templates/StatusPage/index.d.ts +1 -0
- package/dist/templates/StatusPage/index.js +1 -0
- package/dist/templates/index.d.ts +8 -0
- package/dist/templates/index.js +8 -0
- package/dist/utils/dates.d.ts +67 -0
- package/dist/utils/dates.js +154 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/theme.d.ts +4 -0
- package/dist/utils/theme.js +7 -3
- package/package.json +4 -1
- package/tokens.css +494 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,187 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.1 — 2026-10-23
|
|
4
|
+
|
|
5
|
+
Fixes found by running the example app in a real browser at phone and desktop widths, in light and dark. Each has a test in `test/browser-fixes.test.tsx`; the cascade problems are pinned by computing the selectors' specificity.
|
|
6
|
+
|
|
7
|
+
- **`Card` padding never applied** (since 0.11.4). Card rewrites a caller's `p-4` into `!p-4` so it beats Themes' own padding, but Tailwind only generates classes it finds in source, and `!p-4` is never written anywhere. `tokens.css` now lists the important padding utilities with `@source inline(...)`: every side, the common steps from 0 to 12, unprefixed and at `sm`–`xl`. **Needs Tailwind 4.1 or later.**
|
|
8
|
+
- **Invalid fields had no red edge until focused.** Themes' `color="red"` only reaches the focus ring. `Input`, `Textarea`, the `Select` trigger and `DateInput` now show a red edge whenever `aria-invalid="true"`, alongside the message that carries the meaning.
|
|
9
|
+
- **`DataTable`: the sticky first column ignored the row tint.** A selected or hovered row was tinted everywhere except the sticky cell, which stayed the solid panel colour. The tint is now a `--row-tint` variable, painted over the panel colour in the sticky cell. The selected tint is `--accent-a3` (was `a2`, too faint to see).
|
|
10
|
+
- **`DataTable` stacked cards ran label and value together** ("CustomerCasey Diaz"). The `display: flex` for stacked cells lost to the `display: block` reset above it on specificity, so the label sat inline against the value. The same cause meant **`priority` columns never hid in the stacked layout**; both rules are now specific enough to win.
|
|
11
|
+
- **`Sidebar collapsedHeader`** — new optional prop: what the header shows in the 64px rail (a logo mark), instead of the full header wrapping onto several lines. Defaults to `header`.
|
|
12
|
+
- **Checked in the browser and working:** sign-in (errors, focus), the off-canvas panel (scrim, focus on its close button, closes on navigation), stacked cards, the bulk bar, the order detail page with its side column below on phones, sticky form actions with buttons sharing the width, the error summary, dark mode throughout, and the rail.
|
|
13
|
+
- **Known, not changed:** the stacked layout hides the table header, so phones have per-row checkboxes but no select-all.
|
|
14
|
+
|
|
15
|
+
## 1.6.0 — 2026-10-23
|
|
16
|
+
|
|
17
|
+
Auth and polish (phase 6, the last, of [docs/admin-template-plan.md](docs/admin-template-plan.md)). Additive only.
|
|
18
|
+
|
|
19
|
+
- **`AuthLayout`** — sign-in pages outside the shell: the page is `main`, the title its `h1`.
|
|
20
|
+
- A centred card on the canvas, with an optional `aside` panel beside it from 1024px.
|
|
21
|
+
- Below 520px the card fills the screen without a border.
|
|
22
|
+
- `toolbar` slot for a ThemeToggle or language switch.
|
|
23
|
+
- **`LoginForm`**, **`SignupForm`**, **`ForgotPasswordForm`**, **`ResetPasswordForm`**:
|
|
24
|
+
- **Copy:** every visible word is a prop, since this is page copy rather than library strings.
|
|
25
|
+
- **Autocomplete:** each field has the right token (`username`, `current-password`, `new-password`, `email`, `name`), and the forms are `noValidate`.
|
|
26
|
+
- **Errors:** `errors` per field moves focus to the first one after a submit, and never in answer to errors that arrive while typing. `formError` is an alert.
|
|
27
|
+
- **Submitting:** `pending` blocks submits and shows the spinner.
|
|
28
|
+
- **Forgot password:** `sent` shows a status message that should not reveal whether an address has an account.
|
|
29
|
+
- Submit buttons are 44px.
|
|
30
|
+
- **`PasswordInput`** — a show/hide toggle (`aria-pressed`, one name). `autoComplete` is required. Spellcheck and autocapitalisation are off. `FormField` gives it the red invalid state like `Input`.
|
|
31
|
+
- **README** — "Admin kit: from sign-in to a form", a walkthrough of providers → sign-in → shell → list → record → form, with router wiring through `render`.
|
|
32
|
+
- **Example** — [examples/admin/App.tsx](examples/admin/App.tsx) puts it all together, and `test/auth.test.tsx` drives it end to end: sign in, open a list, open a record, go back by breadcrumb, open the account form, sign out. The orders example gained an `onOpen` prop and exports its data for reuse.
|
|
33
|
+
- **Strings** — `showPassword`.
|
|
34
|
+
- **Not done** — the optional Playwright visual-regression job. It needs browsers in CI, which is a separate decision. Until then, real-layout behaviour (container queries, sticky bars, the off-canvas slide) is covered only by CSS-rule tests.
|
|
35
|
+
|
|
36
|
+
## 1.5.0 — 2026-10-23
|
|
37
|
+
|
|
38
|
+
Dates and charts (phase 5 of [docs/admin-template-plan.md](docs/admin-template-plan.md)). Additive only; no new dependencies.
|
|
39
|
+
|
|
40
|
+
- **Date utilities** (`utils/dates`) — calendar dates as ISO `YYYY-MM-DD` strings, with arithmetic in UTC so nothing shifts across time zones or DST.
|
|
41
|
+
- **Functions:** `parseIsoDate`, `isIsoDate`, `toIsoDate`, `daysInMonth`, `addDays`, `addMonths` (clamps the day), `weekdayOf`, `startOfMonth` / `endOfMonth`, `startOfWeek` / `endOfWeek`, `isSameMonth`, `clampDate`, `isInRange`, `monthGrid` (always 6×7), `todayIso` (local), `firstDayOfWeek` (CLDR via `Intl.Locale`, ISO Monday fallback), `formatIsoDate`, `formatDateRange`, `weekdayNames`.
|
|
42
|
+
- **Types:** `IsoDate`, `DateRange`, `Weekday`.
|
|
43
|
+
- **`Calendar`** — the WAI-ARIA date picker grid.
|
|
44
|
+
- **Keyboard:** one Tab stop; arrows, Home/End, PageUp/PageDown, Shift for a year; focus follows into the next month.
|
|
45
|
+
- **Labels:** days are named with the full date, and today is `aria-current="date"`; the month heading is a polite live region.
|
|
46
|
+
- **Selection:** `mode="range"` takes two clicks in either order.
|
|
47
|
+
- **Limits:** `min`, `max` and `isDateDisabled` (those dates stay focusable and are never chosen); `weekStartsOn`; `numberOfMonths`.
|
|
48
|
+
- **`DateInput`** — a button showing the date, opening a Calendar in a popover, or a Sheet below `sheetBelow`.
|
|
49
|
+
- The chosen date is the button's description, so a `FormField` label stays its name.
|
|
50
|
+
- `native` uses `<input type="date">`.
|
|
51
|
+
- **`DateRangePicker`** — two months with presets, or one month with preset chips in a sheet. It commits on the second date or a preset, and Escape leaves the value alone.
|
|
52
|
+
- **Charts** — `BarChart` (grouped/stacked), `LineChart` (gaps break the line), `DonutChart` (legend with value and share) and `Sparkline`, in plain SVG sized to the container (`useElementWidth`).
|
|
53
|
+
- Each is a single `role="img"` named by a required `label` sentence.
|
|
54
|
+
- Series are distinguished by pattern or marker shape as well as colour, repeated in `ChartLegend`.
|
|
55
|
+
- Also exported: `niceTicks`, `chartTable`, `donutTable`.
|
|
56
|
+
- **`ChartFrame`** — a Card section named by its heading, with a "View as table" toggle (`aria-pressed`) showing the same data as a captioned table. Works around any chart.
|
|
57
|
+
- **Tokens** — `--chart-1`…`--chart-8` (blue, orange, jade, violet, crimson, cyan, gold, pink), each ≥ 3:1 against the panel in light and dark (WCAG 1.4.11), checked in `test/charts.test.tsx`. Amber, yellow, lime, mint and sky are excluded for failing it.
|
|
58
|
+
- **`DashboardPage`** and **`DashboardWidget`** — a container-query grid: 1 column, 2 from 640px, `columns` (3 or 4) from 1024px; widgets can span 2 or the full row.
|
|
59
|
+
- **Hooks** — `useElementWidth`.
|
|
60
|
+
- **Strings** — `previousMonth`, `nextMonth`, `chooseDate`, `chooseDates`, `datePresets`, `viewAsTable`, `viewAsChart`.
|
|
61
|
+
- **Differences from the plan:**
|
|
62
|
+
- `Calendar` is a molecule, not an organism, so that `DateInput` (a molecule) can use it without breaking the layer rule.
|
|
63
|
+
- `CommandMenu`, marked optional, is not built.
|
|
64
|
+
- In a `StatTile`, a `Sparkline` stays decorative, as the tile already hid its slot.
|
|
65
|
+
- **Example** — [examples/admin/Dashboard.tsx](examples/admin/Dashboard.tsx).
|
|
66
|
+
- **Tests** — `test/dates.test.tsx` covers date math (leap years, DST weeks, month clamping, six locales' week starts and formatting), calendar keyboard and range behaviour, and both picker layouts. `test/charts.test.tsx` covers ticks, rendering, tables, palette contrast in both appearances, and axe on the frame and the example.
|
|
67
|
+
|
|
68
|
+
## 1.4.0 — 2026-10-23
|
|
69
|
+
|
|
70
|
+
Forms and filters (phase 4 of [docs/admin-template-plan.md](docs/admin-template-plan.md)).
|
|
71
|
+
|
|
72
|
+
- **`FormField`** — label, hint, error, `required` and invalid state wired into one control (`id`, `aria-describedby`, `required`).
|
|
73
|
+
- `Input`, `Textarea` and `Select` get `invalid`, which gives them their red state.
|
|
74
|
+
- Any other control gets `aria-invalid`, or the ids through a render function.
|
|
75
|
+
- `layout="inline"` for checkboxes and switches.
|
|
76
|
+
- Errors are text with an icon, never colour alone.
|
|
77
|
+
- **`FormFieldset`** — a `fieldset` and legend, with hint and error describing the group.
|
|
78
|
+
- **`ErrorSummary`** — after a failed submit it takes focus (whenever `submitCount` changes and there are errors).
|
|
79
|
+
- It is a region named by its heading ("There are 4 problems with this form").
|
|
80
|
+
- Each link moves focus into its field.
|
|
81
|
+
- **`FormSection`** (fields beside the title from 720px of its own width), **`FormActions`** (sticky at the bottom below 768px, buttons sharing the width), **`FormPage`** (a `form` with `noValidate`, default prevented).
|
|
82
|
+
- **`SearchField`** — magnifier, clear button, Escape to clear, `debounceMs`, and a page-wide `shortcut` key.
|
|
83
|
+
- The shortcut is ignored while typing elsewhere and its hint is hidden on touch.
|
|
84
|
+
- `aria-keyshortcuts` is set.
|
|
85
|
+
- **`FilterPopover`** — a "Filters" button with an active count (read as "2 active"). It opens a popover, or a bottom Sheet below `sheetBelow`.
|
|
86
|
+
- With `onApply` the controls are a draft; `onReset` adds a Reset button.
|
|
87
|
+
- **`FilterChips`** — `ChipGroup` with a clear-all while anything is selected.
|
|
88
|
+
- **`FilterBar`** — search, filters and actions in one row; the search takes its own row below 640px. Active filters show as a named list of removable `Pill`s, with clear-all.
|
|
89
|
+
- **`SettingsPage`** — one named `nav`: a sticky section list from 768px of page width, a select below it.
|
|
90
|
+
- **`AppShell`** sets `data-tabbar` while its TabBar shows, and `tokens.css` turns that into `--bottom-bar-offset`.
|
|
91
|
+
- **Behaviour change in `BulkActionBar`:** it used to lift itself by the TabBar height on every screen under 1024px. Now it uses `--bottom-bar-offset`, so it lifts only when the TabBar is actually showing. `FormActions` uses the same offset.
|
|
92
|
+
- **Strings** — `search`, `clearSearch`, `filters`, `apply`, `reset`, `clearFilters`, `activeFilters`, `activeCount`, `fixErrors`.
|
|
93
|
+
- **Example** — [examples/admin/AccountForm.tsx](examples/admin/AccountForm.tsx): a validated form with an error summary.
|
|
94
|
+
- **Tests** — `test/forms.test.tsx` covers wiring, focus management, debouncing, the shortcut key, popover against sheet, the CSS rules, and axe on the example form with errors showing and on `SettingsPage`.
|
|
95
|
+
|
|
96
|
+
## 1.3.0 — 2026-10-23
|
|
97
|
+
|
|
98
|
+
Data display (phase 3 of [docs/admin-template-plan.md](docs/admin-template-plan.md)). Additive only.
|
|
99
|
+
|
|
100
|
+
- **`DataTable`** — controlled sort, selection, row actions, row click, `loading` / `empty` / `error` states and pagination.
|
|
101
|
+
- **Columns:** `id`, `header`, `cell`, `sortValue`, `sortable`, `align`, `width`, `priority`, `isRowHeader`, `mobileLabel`.
|
|
102
|
+
- **Responsive** to its own width (container queries): `priority: 'low'` hides below 640px, `'medium'` below 480px, and below `stackBelow` (`sm` 480 / `md` 640 default / `lg` 800 / `false`) rows become labelled cards.
|
|
103
|
+
- **Explicit table roles** on every part, so the stacked layout's `display: block` cannot strip the table semantics.
|
|
104
|
+
- **Keyboard:** a scrollable, named region that is a tab stop; `aria-sort` on sorted headers; sort cycles ascending → descending → off.
|
|
105
|
+
- **Selection:** select-all on the current page with an indeterminate state, and row checkboxes named after the row.
|
|
106
|
+
- **Row actions:** each row's actions menu is named after the row.
|
|
107
|
+
- **Row click:** with `onRowClick`, the row-header cell becomes a button for keyboard users, and clicks on the row's own controls are ignored.
|
|
108
|
+
- **Options:** `stickyFirstColumn`, `maxHeight` (sticky header), `density`.
|
|
109
|
+
- **`useClientTable`** and **`sortRows`** — in-browser sorting (`Intl.Collator`, numeric, in the provider's locale; empty values last both ways; stable) and paging, with the page clamped.
|
|
110
|
+
- **`Pagination`** and **`paginationRange`** — controlled; `total`/`pageSize` or `pageCount`.
|
|
111
|
+
- Shows a "1–25 of 500" range and an optional labelled rows-per-page select.
|
|
112
|
+
- Numbered pages, or "Page 2 of 10" below 520px of its own width.
|
|
113
|
+
- Short runs show every page.
|
|
114
|
+
- **`BulkActionBar`** — a Radix Toolbar shown while anything is selected, sticky above the TabBar on small screens; the count is in a polite live region.
|
|
115
|
+
- **States** — `EmptyState`, `ErrorState` (`role="alert"`, retry, details) and `LoadingState` (`spinner` / `rows` / `cards`, announced once).
|
|
116
|
+
- **`StatGroup`**, **`KeyValueList`**, **`StatusIndicator`**, **`Pill`**.
|
|
117
|
+
- **Templates** — `ListPage`, `DetailPage` (aside beside the main content from 960px of page width, always after it in the DOM, a named `complementary` landmark), `StatusPage`.
|
|
118
|
+
- **Strings** — `remove`, `noResults`, `somethingWentWrong`, `retry`, `showDetails`, `pagination`, `previousPage`, `nextPage`, `rowsPerPage`, `goToPage`, `pageOf`, `rangeOf`, `selectAll`, `selectRow`, `actionsFor`, `bulkActions`, `selectedCount`, `clearSelection`. Those that carry a value are functions.
|
|
119
|
+
- **Differences from the plan:**
|
|
120
|
+
- Column `header` is a string, because it is also the sort button's name and the stacked label.
|
|
121
|
+
- `density` is one value, not per-breakpoint.
|
|
122
|
+
- **Example** — [examples/admin/Orders.tsx](examples/admin/Orders.tsx): 500 rows that sort, page, select and stack.
|
|
123
|
+
- **Tests** — `test/data.test.tsx` covers pagination ranges, sorting edge cases, table behaviour, axe in four table states, the CSS thresholds, and the 500-row example end to end.
|
|
124
|
+
|
|
125
|
+
## 1.2.0 — 2026-10-23
|
|
126
|
+
|
|
127
|
+
The admin shell and navigation (phase 2 of [docs/admin-template-plan.md](docs/admin-template-plan.md)). Additive only.
|
|
128
|
+
|
|
129
|
+
- **Templates** — `AdminLayout`: an `AppShell` with the page header and content in one column capped at `--content-max-width` (`width="full"` removes the cap).
|
|
130
|
+
- **Organisms**
|
|
131
|
+
- `AppShell`: sidebar + topbar + `<main>`, with a skip link.
|
|
132
|
+
- From `breakpoint` (default `md`, 1024px) it is a two-column grid with a sticky sidebar.
|
|
133
|
+
- Below that it is one column, with the sidebar off-canvas and an optional bottom `TabBar` (`mobileNav`).
|
|
134
|
+
- Widening the window with the panel open closes the panel.
|
|
135
|
+
- `Sidebar`: sections of `NavItem`s.
|
|
136
|
+
- On wide screens it is a persistent column that collapses to an icon rail (`collapsed` / `defaultCollapsed` / `onCollapsedChange`).
|
|
137
|
+
- On narrow screens it is a Radix Dialog panel from the start edge, with a scrim, focus trap and Escape. It closes when an item is chosen.
|
|
138
|
+
- It also works outside an `AppShell` (`breakpoint`, `open`, `onOpenChange`).
|
|
139
|
+
- `Topbar`: a sticky `header`.
|
|
140
|
+
- Inside a shell, a menu button opens the off-canvas sidebar.
|
|
141
|
+
- The search moves to its own row below 768px.
|
|
142
|
+
- `PageHeader`: breadcrumbs, the `h1`, a description, `meta`, a primary action and secondary actions.
|
|
143
|
+
- The secondary actions are ghost buttons when the header is 640px or wider, and a "more" menu below that (a container query).
|
|
144
|
+
- `PageTabs`: sub-page navigation as a `nav` of links with `aria-current`, scrolling sideways when crowded.
|
|
145
|
+
- **Molecules**
|
|
146
|
+
- `NavItem`: a link, button or one-level group. It carries a badge, with `badgeLabel` for screen readers.
|
|
147
|
+
- `Breadcrumbs`: folds the middle into a menu past `maxItems`, and shows only the parent as a back link below 768px.
|
|
148
|
+
- `UserMenu`: avatar + name opening an `ActionMenu`. The name stays in the accessible name on mobile.
|
|
149
|
+
- `ThemeToggle` and `AccentPicker`: read and store through a `createTheming` instance; the app applies the choice.
|
|
150
|
+
- **Atoms**
|
|
151
|
+
- `ActionMenu`: a dropdown menu that becomes an `ActionSheet` below `sheetBelow` (default `sm`; `false` keeps the menu).
|
|
152
|
+
- `TextLink`: the theme's link style, router-agnostic, with `external`.
|
|
153
|
+
- Both are named to avoid shadowing the `DropdownMenu` and `Link` primitives.
|
|
154
|
+
- **Routing** — every navigating component takes `href` + `render(props)`, with one shared `LinkRenderProps` / `RenderLink` type.
|
|
155
|
+
- **Strings** — `skipToContent`, `mainNavigation`, `openNavigation`, `closeNavigation`, `collapseSidebar`, `expandSidebar`, `breadcrumbs`, `showMoreBreadcrumbs`, `opensInNewTab`, `userMenu`, `appearance`, `light`, `dark`, `system`, `accent`. English defaults; type dictionaries as `Partial<UiStrings>`.
|
|
156
|
+
- **CSS** — `.ui-shell`, `.ui-sidebar*`, `.ui-nav-item*`, `.ui-topbar*`, `.ui-breadcrumbs*`, `.ui-page-header*`, `.ui-page-tabs*`, `.ui-user-menu*`, `.ui-accent-picker*`, `.ui-admin-content` and a `.ui-sr-only` utility in `tokens.css`.
|
|
157
|
+
- The shell uses `100dvh`.
|
|
158
|
+
- Touch targets are 44px on coarse pointers.
|
|
159
|
+
- Motion is off under `prefers-reduced-motion`.
|
|
160
|
+
- The off-canvas panel slides from the correct side in RTL.
|
|
161
|
+
- **Example** — [examples/admin/Shell.tsx](examples/admin/Shell.tsx).
|
|
162
|
+
- **Tests** — `test/shell.test.tsx` covers behaviour, wide and narrow viewports, server rendering, axe on the shell (both widths), Breadcrumbs and AccentPicker, and the CSS rules above. Shared viewport mock in `test/viewport.ts`.
|
|
163
|
+
|
|
164
|
+
## 1.1.0 — 2026-10-23
|
|
165
|
+
|
|
166
|
+
Foundations for the admin kit (phase 1 of [docs/admin-template-plan.md](docs/admin-template-plan.md)). Nothing removed, renamed or changed in default behaviour.
|
|
167
|
+
|
|
168
|
+
- **Hooks** — `useMediaQuery(query, serverValue = true)`, `useBreakpoint(serverValue = 'lg')`, `useControllableState({ value, defaultValue, onChange })`. `useMediaQuery` also works without `matchMedia`: it returns `serverValue` instead of throwing.
|
|
169
|
+
- **`BREAKPOINTS`** and **`minWidth(bp)`** — Radix Themes' scale (xs 520, sm 768, md 1024, lg 1280, xl 1640) as one constant for JavaScript.
|
|
170
|
+
- **`ResponsiveDialog`, `ActionSheet`** — new optional `breakpoint` prop (default `sm`, the same 768px as before). Both now use `useMediaQuery` instead of their own copies.
|
|
171
|
+
- **`UiStringsProvider locale`** — optional BCP 47 tag for `Intl` formatting, read with the new `useUiLocale()`. It inherits from an outer provider.
|
|
172
|
+
- **Layers** — empty `molecules`, `organisms` and `templates` entry points, re-exported from the root.
|
|
173
|
+
- **`Icon`** — neutral names `home`, `list`, `calendar`, `users`, `card`, `document`. The old names still work as aliases.
|
|
174
|
+
- **Tokens** — `--sidebar-width`, `--sidebar-width-collapsed`, `--topbar-height`, `--content-max-width`, `--table-row-height-comfortable`, `--table-row-height-compact`. Not read by anything yet.
|
|
175
|
+
- **`package.json`** — `"sideEffects": ["*.css"]`, so bundlers can drop unused exports from the root entry.
|
|
176
|
+
- **Deprecated** — the free `readTheme`, `storeTheme`, `readAccent` and `storeAccent`. Use `createTheming({ storagePrefix })`. Removal is planned for 2.0.
|
|
177
|
+
- **Comments, examples and test data** — reworded to be app-neutral.
|
|
178
|
+
- **New tests** — `test/hooks.test.tsx`, plus `test/foundations.test.tsx`, which checks layer import boundaries, that every component folder is exported, that no export shadows a re-exported Radix primitive, and app-neutral wording. Each check was confirmed to fail when its rule is broken.
|
|
179
|
+
|
|
180
|
+
## 1.0.0 — 2026-10-23
|
|
181
|
+
|
|
182
|
+
- **Stable baseline.** The public API of 0.11.4 is now 1.0.0. There are no code changes, and nothing is removed or renamed.
|
|
183
|
+
- **Admin template kit plan** — [docs/admin-template-plan.md](docs/admin-template-plan.md). It covers an audit of every atom on desktop and mobile, the layered breakdown (molecules, organisms, templates), ADRs, the testing strategy and a phased 1.x roadmap.
|
|
184
|
+
|
|
3
185
|
## 0.11.4 — 2026-10-20
|
|
4
186
|
|
|
5
187
|
- **`Card`** — Themes' unlayered `.rt-Card` padding (16px at size 2) beat the layered `p-0`, so every card was padded on top of `CardHeader`/`CardBody` (double padding; header dividers short of the edges). The zero is now `!p-0` plus an unlayered `.ui-card.rt-Card { padding: 0 }` in `tokens.css`. Padding utilities a caller passes (`p-*`, `px-*`, `md:py-*`…) are promoted to `!` so they still win.
|
package/README.md
CHANGED
|
@@ -64,6 +64,9 @@ export const theming = createTheming({
|
|
|
64
64
|
theming.readAccent(); // 'teal' | 'crimson'
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
The free `readTheme`, `storeTheme`, `readAccent` and `storeAccent` functions are deprecated:
|
|
68
|
+
they are bound to a fixed legacy storage prefix. Use an instance from `createTheming` instead.
|
|
69
|
+
|
|
67
70
|
Every accent you offer needs its Radix colour scale imported.
|
|
68
71
|
|
|
69
72
|
### Dark mode and portals
|
|
@@ -88,6 +91,253 @@ Components never contain literal text. The handful they need — a spinner's lab
|
|
|
88
91
|
placeholder, close/expand/collapse names, the `ScrollStrip` region name (`scrollableRegion`) — come from `UiStringsProvider`, which takes a partial
|
|
89
92
|
`UiStrings` object. Defaults are English. Most components also accept the label as a prop.
|
|
90
93
|
|
|
94
|
+
Type your dictionary as `Partial<UiStrings>`: new keys are added in minor releases, with
|
|
95
|
+
English defaults, and a complete `UiStrings` object would stop type-checking when they arrive.
|
|
96
|
+
|
|
97
|
+
`UiStringsProvider` also takes an optional `locale` (a BCP 47 tag such as `pt-PT`). Components
|
|
98
|
+
that format dates, numbers or money pass it to `Intl`; `useUiLocale()` reads it. Left out, it
|
|
99
|
+
inherits from an outer provider, and with none `Intl` uses the runtime's default.
|
|
100
|
+
|
|
101
|
+
## Admin kit: from sign-in to a form
|
|
102
|
+
|
|
103
|
+
The kit is layered: **atoms → molecules → organisms → templates**. Templates are slot layouts:
|
|
104
|
+
they place what you give them and fetch nothing, so routing, data and auth stay in your app.
|
|
105
|
+
Here is a whole app, in the order a person meets it. The runnable version is
|
|
106
|
+
[examples/admin/App.tsx](examples/admin/App.tsx); `test/auth.test.tsx` drives it from sign-in
|
|
107
|
+
to sign-out.
|
|
108
|
+
|
|
109
|
+
**1. Providers, once, at the root.** Theme from Radix; your words and locale for the kit.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
const theming = createTheming({ accents: ['indigo'], defaultAccent: 'indigo', storagePrefix: 'myapp' });
|
|
113
|
+
|
|
114
|
+
<Theme accentColor={theming.readAccent()}>
|
|
115
|
+
<UiStringsProvider strings={myDictionary.ui} locale="en-GB">
|
|
116
|
+
<App />
|
|
117
|
+
</UiStringsProvider>
|
|
118
|
+
</Theme>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**2. Sign in.** `AuthLayout` is the page; the form is one of `LoginForm`, `SignupForm`,
|
|
122
|
+
`ForgotPasswordForm`, `ResetPasswordForm`. Every label is a prop, because it is page copy.
|
|
123
|
+
Validation and the request are yours: pass back `errors` (focus moves to the first one after
|
|
124
|
+
a submit), `formError` and `pending`.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
<AuthLayout title="Sign in" footer={<TextLink href="/signup">Create an account</TextLink>}>
|
|
128
|
+
<LoginForm
|
|
129
|
+
labels={{ form: 'Sign in', identifier: 'Email', password: 'Password', submit: 'Sign in' }}
|
|
130
|
+
forgotPassword={<TextLink href="/forgot">Forgot password?</TextLink>}
|
|
131
|
+
errors={errors} formError={formError} pending={pending}
|
|
132
|
+
onSubmit={(values) => signIn(values)}
|
|
133
|
+
/>
|
|
134
|
+
</AuthLayout>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**3. The shell.** One `AdminLayout` around every signed-in page. Give NavItems an `href` and a
|
|
138
|
+
`render` for your router's link, and mark the current one `active`.
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
const link: RenderLink = (p) => <RouterLink to={p.href} {...p} />;
|
|
142
|
+
|
|
143
|
+
<AdminLayout
|
|
144
|
+
sidebar={<Sidebar sections={[{ items: [
|
|
145
|
+
{ label: 'Orders', icon: <CubeIcon />, href: '/orders', render: link, active: path.startsWith('/orders') },
|
|
146
|
+
] }]} />}
|
|
147
|
+
topbar={<Topbar actions={<UserMenu name={user.name} items={menu} onSelect={onMenu} />} />}
|
|
148
|
+
>
|
|
149
|
+
<Outlet />
|
|
150
|
+
</AdminLayout>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**4. A list.** `ListPage` with a `PageHeader`, a `FilterBar` and a `DataTable`. With all the
|
|
154
|
+
data in the browser, `useClientTable` sorts and pages; otherwise pass your server's page.
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
<ListPage
|
|
158
|
+
header={<PageHeader title="Orders" primaryAction={<Button>New order</Button>} />}
|
|
159
|
+
toolbar={<FilterBar search={<SearchField onValueChange={setQuery} debounceMs={250} />} />}
|
|
160
|
+
>
|
|
161
|
+
<DataTable caption="Orders" columns={columns} rows={page.rows} getRowId={(o) => o.id}
|
|
162
|
+
sort={sort} onSortChange={setSort} onRowClick={(o) => navigate(`/orders/${o.id}`)}
|
|
163
|
+
pagination={{ page, onPageChange: setPage, total: page.total, pageSize: 25 }} />
|
|
164
|
+
</ListPage>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**5. A record.** `DetailPage`: header with breadcrumbs and tabs, main content, a side column.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
<DetailPage
|
|
171
|
+
header={<PageHeader title={`Order #${o.number}`} breadcrumbs={[{ label: 'Orders', href: '/orders', render: link }, { label: `#${o.number}` }]} />}
|
|
172
|
+
aside={<Card><KeyValueList items={[{ label: 'Customer', value: o.customer }]} /></Card>}
|
|
173
|
+
asideLabel="Order summary"
|
|
174
|
+
>
|
|
175
|
+
…
|
|
176
|
+
</DetailPage>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**6. A form.** `FormPage` with `FormSection`s of `FormField`s, an `ErrorSummary` and
|
|
180
|
+
`FormActions`, as in [Forms and filters](#forms-and-filters).
|
|
181
|
+
|
|
182
|
+
**Also there for the pages in between:** `DashboardPage` with `ChartFrame`s, `SettingsPage`,
|
|
183
|
+
and `StatusPage` for 404 and 500.
|
|
184
|
+
|
|
185
|
+
## Admin shell
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import { AdminLayout, PageHeader, Sidebar, Topbar, UserMenu } from '@kennycorrea/ui';
|
|
189
|
+
|
|
190
|
+
<AdminLayout
|
|
191
|
+
sidebar={<Sidebar header={<Logo />} sections={[{ items: [
|
|
192
|
+
{ label: 'Dashboard', icon: <HomeIcon />, href: '/', active: true },
|
|
193
|
+
{ label: 'Orders', icon: <CubeIcon />, href: '/orders', badge: 3, badgeLabel: '3 new' },
|
|
194
|
+
] }]} />}
|
|
195
|
+
topbar={<Topbar search={<Input type="search" aria-label="Search" />}
|
|
196
|
+
actions={<UserMenu name="Alex Morgan" items={menu} onSelect={onMenu} />} />}
|
|
197
|
+
header={<PageHeader title="Dashboard" primaryAction={<Button>New</Button>} />}
|
|
198
|
+
>
|
|
199
|
+
…
|
|
200
|
+
</AdminLayout>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
- **`AppShell`** / **`AdminLayout`**: from `md` (1024px, the `breakpoint` prop) the sidebar is a
|
|
204
|
+
sticky column that collapses to an icon rail. Below that it is an off-canvas panel opened by
|
|
205
|
+
the Topbar's menu button, with an optional bottom `TabBar` (`mobileNav`). Includes a skip link
|
|
206
|
+
to `<main>`.
|
|
207
|
+
- **`Sidebar`** sections of **`NavItem`**s: links, buttons, or one level of grouped links. In
|
|
208
|
+
the rail, labels become tooltips and groups open as a menu, and the header shows
|
|
209
|
+
`collapsedHeader` (a logo mark) if you give one.
|
|
210
|
+
- **`Topbar`**, **`PageHeader`** (the page's `h1`, breadcrumbs, a primary action, and secondary
|
|
211
|
+
actions that fold into a menu below 640px of header width), **`PageTabs`** (a `nav` of
|
|
212
|
+
links), **`Breadcrumbs`** (folds long trails; only the parent shows below 768px).
|
|
213
|
+
- **`UserMenu`**, **`ThemeToggle`**, **`AccentPicker`**: the last two take a `createTheming`
|
|
214
|
+
instance to read and store the choice. The app applies it to `<Theme>`.
|
|
215
|
+
- **`ActionMenu`**: a dropdown menu that becomes an `ActionSheet` below `sm`. **`TextLink`**:
|
|
216
|
+
an inline link, with `external` for new-tab links.
|
|
217
|
+
- **Routing**: everything that navigates takes `href` plus an optional `render` that receives
|
|
218
|
+
the props to spread onto your router's link:
|
|
219
|
+
`render={(p) => <RouterLink to={p.href} {...p} />}`.
|
|
220
|
+
|
|
221
|
+
Full example: [examples/admin/Shell.tsx](examples/admin/Shell.tsx).
|
|
222
|
+
|
|
223
|
+
## Data display
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
const columns: DataTableColumn<Order>[] = [
|
|
227
|
+
{ id: 'number', header: 'Order', isRowHeader: true, cell: (o) => `#${o.number}`, sortValue: (o) => o.number },
|
|
228
|
+
{ id: 'total', header: 'Total', align: 'end', cell: (o) => money.format(o.total), sortValue: (o) => o.total },
|
|
229
|
+
{ id: 'placed', header: 'Placed', priority: 'low', cell: (o) => day.format(o.placed), sortValue: (o) => o.placed },
|
|
230
|
+
];
|
|
231
|
+
const table = useClientTable(orders, columns, { sort, page, pageSize }); // or your server's page
|
|
232
|
+
|
|
233
|
+
<DataTable caption="Orders" columns={columns} rows={table.rows} getRowId={(o) => o.id}
|
|
234
|
+
sort={sort} onSortChange={setSort} selection={selected} onSelectionChange={setSelected}
|
|
235
|
+
rowActions={() => [{ id: 'refund', label: 'Refund', destructive: true }]} onRowAction={act}
|
|
236
|
+
pagination={{ page, onPageChange: setPage, total: table.total, pageSize }} />
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- **`DataTable`** is fully controlled: it never sorts, pages or selects by itself. It responds to
|
|
240
|
+
its own width. `low`-priority columns hide below 640px of table width and `medium` ones below
|
|
241
|
+
480px. Below `stackBelow` (`sm` 480 / `md` 640, the default / `lg` 800 / `false`) each row
|
|
242
|
+
becomes a labelled card. `state` covers `loading`, `empty` and `error` inside the table.
|
|
243
|
+
- **`useClientTable`** / **`sortRows`**: sorting with `Intl.Collator` (numeric, in the provider's
|
|
244
|
+
`locale`, empty values last) and paging, for data already in the browser.
|
|
245
|
+
- **`Pagination`**: numbered pages, or "Page 2 of 10" when narrow; range text and rows-per-page.
|
|
246
|
+
`paginationRange` is exported for custom pagers.
|
|
247
|
+
- **`BulkActionBar`**: a sticky toolbar while rows are selected, with the count in a live region.
|
|
248
|
+
- **`EmptyState`**, **`ErrorState`** (`role="alert"`, retry, details) and **`LoadingState`**
|
|
249
|
+
(`spinner`, `rows`, `cards`).
|
|
250
|
+
- **`StatGroup`** (StatTiles in a ScrollStrip), **`KeyValueList`** (Radix `DataList`),
|
|
251
|
+
**`StatusIndicator`** (a dot and a word) and **`Pill`** (a removable token).
|
|
252
|
+
- **Templates:** `ListPage`, `DetailPage` (the aside sits beside the main content from 960px and
|
|
253
|
+
comes after it in the DOM) and `StatusPage` (404/500, outside the shell).
|
|
254
|
+
|
|
255
|
+
UI strings with a value in them are functions, e.g.
|
|
256
|
+
`selectedCount: (n) => \`${n} seleccionados\``.
|
|
257
|
+
Full example with 500 rows: [examples/admin/Orders.tsx](examples/admin/Orders.tsx).
|
|
258
|
+
|
|
259
|
+
## Forms and filters
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
<FormPage onSubmit={submit}
|
|
263
|
+
errorSummary={<ErrorSummary errors={summary} submitCount={submitCount} />}
|
|
264
|
+
actions={<FormActions><Button type="submit">Save</Button></FormActions>}>
|
|
265
|
+
<FormSection title="Profile" description="Shown on your profile.">
|
|
266
|
+
<FormField id="email" label="Email" hint="We never share it." error={errors.email} required>
|
|
267
|
+
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
268
|
+
</FormField>
|
|
269
|
+
</FormSection>
|
|
270
|
+
</FormPage>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
- **`FormField`** wires the label, hint, error, `required` and invalid state into one control.
|
|
274
|
+
`Input`, `Textarea` and `Select` get their red state, and anything else gets `aria-invalid`.
|
|
275
|
+
Pass a function as the child to place the ids yourself. Use `layout="inline"` for a Checkbox
|
|
276
|
+
or Switch. **`FormFieldset`** groups controls under a legend.
|
|
277
|
+
- **`ErrorSummary`**: after a failed submit it takes focus, says how many problems there are,
|
|
278
|
+
and links each one to its field. Increment `submitCount` on each submit so it refocuses.
|
|
279
|
+
- **`FormSection`** (title beside the fields from 720px of its own width), **`FormActions`**
|
|
280
|
+
(sticky at the bottom below 768px, above an AppShell's TabBar), **`FormPage`**
|
|
281
|
+
(`noValidate`, so only your messages show).
|
|
282
|
+
- **`SearchField`**: magnifier, clear button, Escape, optional `debounceMs` and a page-wide
|
|
283
|
+
`shortcut` key.
|
|
284
|
+
- **`FilterPopover`**: a "Filters (n)" button opening a popover, or a bottom sheet below `sm`.
|
|
285
|
+
With `onApply` the controls are a draft. **`FilterChips`** is a ChipGroup plus clear-all.
|
|
286
|
+
**`FilterBar`** lays out search, filters and actions, with active filters as removable pills.
|
|
287
|
+
- **`SettingsPage`**: section list beside the content from 768px; a select above it when
|
|
288
|
+
narrower.
|
|
289
|
+
|
|
290
|
+
Full example: [examples/admin/AccountForm.tsx](examples/admin/AccountForm.tsx).
|
|
291
|
+
|
|
292
|
+
## Dates and charts
|
|
293
|
+
|
|
294
|
+
Dates are **calendar dates as ISO strings** (`'2026-09-21'`), not `Date` objects. A due date is
|
|
295
|
+
the same day everywhere, so nothing shifts by a day across time zones or clock changes. The
|
|
296
|
+
helpers in `utils/dates` (`addDays`, `addMonths`, `monthGrid`, `firstDayOfWeek`,
|
|
297
|
+
`formatIsoDate`, `formatDateRange`…) do the arithmetic in UTC and format through `Intl` with
|
|
298
|
+
the `UiStringsProvider` locale.
|
|
299
|
+
|
|
300
|
+
- **`Calendar`** follows the WAI-ARIA date picker grid. Arrows, Home/End, PageUp/PageDown (with
|
|
301
|
+
Shift for a year) and Enter work; days are named by their full date, and today is
|
|
302
|
+
`aria-current="date"`. `mode="range"`, `min`/`max`/`isDateDisabled`, and the week starting on
|
|
303
|
+
the locale's own first day.
|
|
304
|
+
- **`DateInput`**: a field showing the date that opens a Calendar, in a popover or (below
|
|
305
|
+
`sm`) a sheet. `native` uses `<input type="date">` instead. Works inside `FormField`.
|
|
306
|
+
- **`DateRangePicker`**: two months beside presets, or one month and preset chips in a
|
|
307
|
+
sheet. The second date or a preset commits.
|
|
308
|
+
- **Charts**: `BarChart` (grouped or stacked), `LineChart`, `DonutChart` and `Sparkline`, as
|
|
309
|
+
plain SVG with no dependency. Each is one image named by your `label` sentence. Series are
|
|
310
|
+
told apart by pattern (bars, donut) or marker shape (lines) as well as colour, and the
|
|
311
|
+
legend repeats them. The width follows the container.
|
|
312
|
+
- **`ChartFrame`**: the card around any chart, yours or another library's, with a heading and
|
|
313
|
+
a "View as table" switch fed by `chartTable` / `donutTable`.
|
|
314
|
+
- **`--chart-1` … `--chart-8`**: a palette where every colour reaches 3:1 against the panel in
|
|
315
|
+
light and dark, held there by a test.
|
|
316
|
+
- **`DashboardPage`** + **`DashboardWidget span`**: one, two, then `columns` columns by the
|
|
317
|
+
page's own width.
|
|
318
|
+
|
|
319
|
+
Full example: [examples/admin/Dashboard.tsx](examples/admin/Dashboard.tsx).
|
|
320
|
+
|
|
321
|
+
## Hooks and breakpoints
|
|
322
|
+
|
|
323
|
+
- `BREAKPOINTS` — Radix Themes' scale in pixels (`xs` 520, `sm` 768, `md` 1024, `lg` 1280,
|
|
324
|
+
`xl` 1640), the same scale `Responsive<T>` props and `tokens.css` use. `minWidth(bp)` turns
|
|
325
|
+
a name into a media query.
|
|
326
|
+
- `useMediaQuery(query, serverValue = true)` — for switching *structure*; visual changes belong
|
|
327
|
+
in CSS. Returns `serverValue` on the server and without `matchMedia`.
|
|
328
|
+
- `useBreakpoint(serverValue = 'lg')` — the largest breakpoint reached, or `'initial'`.
|
|
329
|
+
- `useControllableState({ value, defaultValue, onChange })` — the controlled/uncontrolled pair.
|
|
330
|
+
|
|
331
|
+
`ResponsiveDialog` and `ActionSheet` take a `breakpoint` (default `sm`, 768px, unchanged).
|
|
332
|
+
|
|
333
|
+
## Library structure
|
|
334
|
+
|
|
335
|
+
`src/` is layered: `atoms` → `molecules` → `organisms` → `templates`, with `hooks`, `utils`,
|
|
336
|
+
`strings`, `primitives` and `icons` underneath. A layer imports only from layers below it, and
|
|
337
|
+
`test/foundations.test.tsx` fails the build when one does not. Everything ships from the root
|
|
338
|
+
entry. The plan for the upper layers is in
|
|
339
|
+
[docs/admin-template-plan.md](docs/admin-template-plan.md).
|
|
340
|
+
|
|
91
341
|
## Mobile presentation layer
|
|
92
342
|
|
|
93
343
|
An opt-in "grouped" surface style — rows in rounded cards on a tinted canvas, as in native
|
|
@@ -239,7 +489,7 @@ content, capped at 90%). Pass `detents` to let the user move between several; dr
|
|
|
239
489
|
the lowest dismisses.
|
|
240
490
|
|
|
241
491
|
```tsx
|
|
242
|
-
<Sheet open={open} onOpenChange={setOpen} title="New
|
|
492
|
+
<Sheet open={open} onOpenChange={setOpen} title="New item" detents={['medium', 'large']}
|
|
243
493
|
defaultDetent="medium" scaleBackground footer={<Button>Save</Button>}>
|
|
244
494
|
<form>…</form>
|
|
245
495
|
</Sheet>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import { type Breakpoint } from '../../hooks/breakpoints.js';
|
|
3
|
+
export interface ActionMenuItem {
|
|
4
|
+
/** Stable across renders; also what `onSelect` is given back. */
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
/** Red and separated from the rest, for the one that undoes something. */
|
|
9
|
+
destructive?: boolean;
|
|
10
|
+
/** Shown greyed in the menu. Left out of the sheet, which has no disabled state. */
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface ActionMenuProps {
|
|
14
|
+
/** The button that opens it. Must accept a ref and `onClick` (any button does). */
|
|
15
|
+
trigger: ReactElement;
|
|
16
|
+
items: ActionMenuItem[];
|
|
17
|
+
onSelect: (id: string) => void;
|
|
18
|
+
/** Alignment of the menu against the trigger. Default `end`. */
|
|
19
|
+
align?: 'start' | 'center' | 'end';
|
|
20
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
21
|
+
/**
|
|
22
|
+
* Below this breakpoint the menu is an `ActionSheet` instead, which is easier to hit with a
|
|
23
|
+
* thumb. Default `sm` (768px), the same switch `ActionSheet` makes. `false` keeps the menu
|
|
24
|
+
* everywhere.
|
|
25
|
+
*/
|
|
26
|
+
sheetBelow?: Breakpoint | false;
|
|
27
|
+
/** The sheet's heading. */
|
|
28
|
+
title?: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A button that opens a short list of actions: a dropdown menu on wide screens, an action
|
|
32
|
+
* sheet on narrow ones. Items are data, in the same shape as `ContextMenu`'s.
|
|
33
|
+
*
|
|
34
|
+
* Named `ActionMenu` because `DropdownMenu` is Radix Themes' own component, re-exported from
|
|
35
|
+
* this package as a primitive for anything this does not cover.
|
|
36
|
+
*/
|
|
37
|
+
export declare function ActionMenu({ trigger, items, onSelect, align, side, sheetBelow, title, }: ActionMenuProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Slot, DropdownMenu as ThemedDropdownMenu } from '@radix-ui/themes';
|
|
3
|
+
import { Fragment, useState } from 'react';
|
|
4
|
+
import { minWidth } from '../../hooks/breakpoints.js';
|
|
5
|
+
import { useMediaQuery } from '../../hooks/useMediaQuery.js';
|
|
6
|
+
import { ActionSheet } from '../ActionSheet/ActionSheet.js';
|
|
7
|
+
/**
|
|
8
|
+
* A button that opens a short list of actions: a dropdown menu on wide screens, an action
|
|
9
|
+
* sheet on narrow ones. Items are data, in the same shape as `ContextMenu`'s.
|
|
10
|
+
*
|
|
11
|
+
* Named `ActionMenu` because `DropdownMenu` is Radix Themes' own component, re-exported from
|
|
12
|
+
* this package as a primitive for anything this does not cover.
|
|
13
|
+
*/
|
|
14
|
+
export function ActionMenu({ trigger, items, onSelect, align = 'end', side, sheetBelow = 'sm', title, }) {
|
|
15
|
+
const wide = useMediaQuery(minWidth(sheetBelow || 'xs'));
|
|
16
|
+
const [sheetOpen, setSheetOpen] = useState(false);
|
|
17
|
+
if (sheetBelow !== false && !wide) {
|
|
18
|
+
return (_jsxs(_Fragment, { children: [_jsx(Slot, { "aria-haspopup": "dialog", onClick: () => setSheetOpen(true), children: trigger }), _jsx(ActionSheet, { open: sheetOpen, onOpenChange: setSheetOpen, title: title, breakpoint: sheetBelow, actions: items
|
|
19
|
+
.filter((item) => !item.disabled)
|
|
20
|
+
.map((item) => ({
|
|
21
|
+
label: item.label,
|
|
22
|
+
icon: item.icon,
|
|
23
|
+
tone: item.destructive ? 'danger' : 'neutral',
|
|
24
|
+
onSelect: () => onSelect(item.id),
|
|
25
|
+
})) })] }));
|
|
26
|
+
}
|
|
27
|
+
return (_jsxs(ThemedDropdownMenu.Root, { children: [_jsx(ThemedDropdownMenu.Trigger, { children: trigger }), _jsx(ThemedDropdownMenu.Content, { size: "2", align: align, side: side, children: items.map((item, index) => (_jsxs(Fragment, { children: [item.destructive && index > 0 && !items[index - 1].destructive ? (_jsx(ThemedDropdownMenu.Separator, {})) : null, _jsxs(ThemedDropdownMenu.Item, { color: item.destructive ? 'red' : undefined, disabled: item.disabled, onSelect: () => onSelect(item.id), children: [item.icon ? (_jsx("span", { "aria-hidden": "true", className: "inline-flex", children: item.icon })) : null, item.label] })] }, item.id))) })] }));
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ActionMenu.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ActionMenu.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type Breakpoint } from '../../hooks/breakpoints.js';
|
|
2
3
|
export type ActionSheetTone = 'neutral' | 'brand' | 'danger';
|
|
3
4
|
export interface ActionSheetAction {
|
|
4
5
|
label: string;
|
|
@@ -18,13 +19,18 @@ export interface ActionSheetProps {
|
|
|
18
19
|
cancelLabel?: string;
|
|
19
20
|
/** Where the portal mounts. Default: the nearest `<Theme>`, else rendered in place. */
|
|
20
21
|
container?: HTMLElement | null;
|
|
22
|
+
/**
|
|
23
|
+
* From this breakpoint up, the sheet is a small centred dialog. Default `sm` (768px), which
|
|
24
|
+
* is what Tailwind calls `md`.
|
|
25
|
+
*/
|
|
26
|
+
breakpoint?: Breakpoint;
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
|
-
* A short list of actions to choose between, on Radix `AlertDialog`: below `
|
|
24
|
-
* bottom as a grouped list with a separated Cancel;
|
|
29
|
+
* A short list of actions to choose between, on Radix `AlertDialog`: below `breakpoint` anchored
|
|
30
|
+
* to the bottom as a grouped list with a separated Cancel; from it up a small centred dialog.
|
|
25
31
|
*
|
|
26
32
|
* AlertDialog rather than Dialog because the user is being asked to decide — it cannot be
|
|
27
33
|
* dismissed by clicking outside, and focus lands on Cancel, so Enter never fires a destructive
|
|
28
34
|
* action by accident. Escape and Cancel both close it without running anything.
|
|
29
35
|
*/
|
|
30
|
-
export declare function ActionSheet({ open, onOpenChange, actions, title, message, cancelLabel, container, }: ActionSheetProps): import("react").JSX.Element;
|
|
36
|
+
export declare function ActionSheet({ open, onOpenChange, actions, title, message, cancelLabel, container, breakpoint, }: ActionSheetProps): import("react").JSX.Element;
|
|
@@ -1,36 +1,27 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import * as AlertDialog from '@radix-ui/react-alert-dialog';
|
|
3
3
|
import { Theme } from '@radix-ui/themes';
|
|
4
|
-
import {
|
|
4
|
+
import { minWidth } from '../../hooks/breakpoints.js';
|
|
5
|
+
import { useMediaQuery } from '../../hooks/useMediaQuery.js';
|
|
5
6
|
import { useUiStrings } from '../../strings.js';
|
|
6
7
|
import { cn } from '../../utils/cn.js';
|
|
7
8
|
import { useThemePortalTarget } from '../../utils/themePortal.js';
|
|
8
|
-
/** Tailwind's `md`. At and above it the sheet becomes a small centred dialog. */
|
|
9
|
-
const MD = '(min-width: 768px)';
|
|
10
|
-
function subscribe(onChange) {
|
|
11
|
-
const query = window.matchMedia(MD);
|
|
12
|
-
query.addEventListener('change', onChange);
|
|
13
|
-
return () => query.removeEventListener('change', onChange);
|
|
14
|
-
}
|
|
15
|
-
function useIsMdUp() {
|
|
16
|
-
return useSyncExternalStore(subscribe, () => window.matchMedia(MD).matches, () => true);
|
|
17
|
-
}
|
|
18
9
|
const TONE = {
|
|
19
10
|
neutral: 'text-ink',
|
|
20
11
|
brand: 'text-brand-700',
|
|
21
12
|
danger: 'text-[var(--red-11)]',
|
|
22
13
|
};
|
|
23
14
|
/**
|
|
24
|
-
* A short list of actions to choose between, on Radix `AlertDialog`: below `
|
|
25
|
-
* bottom as a grouped list with a separated Cancel;
|
|
15
|
+
* A short list of actions to choose between, on Radix `AlertDialog`: below `breakpoint` anchored
|
|
16
|
+
* to the bottom as a grouped list with a separated Cancel; from it up a small centred dialog.
|
|
26
17
|
*
|
|
27
18
|
* AlertDialog rather than Dialog because the user is being asked to decide — it cannot be
|
|
28
19
|
* dismissed by clicking outside, and focus lands on Cancel, so Enter never fires a destructive
|
|
29
20
|
* action by accident. Escape and Cancel both close it without running anything.
|
|
30
21
|
*/
|
|
31
|
-
export function ActionSheet({ open, onOpenChange, actions, title, message, cancelLabel, container, }) {
|
|
22
|
+
export function ActionSheet({ open, onOpenChange, actions, title, message, cancelLabel, container, breakpoint = 'sm', }) {
|
|
32
23
|
const strings = useUiStrings();
|
|
33
|
-
const mdUp =
|
|
24
|
+
const mdUp = useMediaQuery(minWidth(breakpoint));
|
|
34
25
|
const { anchorRef, target, resolved } = useThemePortalTarget(container);
|
|
35
26
|
const cancel = cancelLabel ?? strings.cancel;
|
|
36
27
|
const hasHeader = title != null || message != null;
|
|
@@ -14,7 +14,7 @@ export interface ConfirmOptions {
|
|
|
14
14
|
* Asks and resolves `true` on accept, `false` on Cancel, Escape or any other dismissal.
|
|
15
15
|
* Never rejects.
|
|
16
16
|
*
|
|
17
|
-
* if (await confirm({ title: 'Delete
|
|
17
|
+
* if (await confirm({ title: 'Delete item?', confirmLabel: 'Delete', tone: 'danger' })) …
|
|
18
18
|
*/
|
|
19
19
|
export declare function confirm(options: ConfirmOptions): Promise<boolean>;
|
|
20
20
|
/** Renders the pending confirmation. Mount once, inside `<Theme>`, next to `<Toaster>`. */
|
|
@@ -25,7 +25,7 @@ function subscribe(l) {
|
|
|
25
25
|
* Asks and resolves `true` on accept, `false` on Cancel, Escape or any other dismissal.
|
|
26
26
|
* Never rejects.
|
|
27
27
|
*
|
|
28
|
-
* if (await confirm({ title: 'Delete
|
|
28
|
+
* if (await confirm({ title: 'Delete item?', confirmLabel: 'Delete', tone: 'danger' })) …
|
|
29
29
|
*/
|
|
30
30
|
export function confirm(options) {
|
|
31
31
|
return new Promise((resolve) => emit([...queue, { ...options, resolve }]));
|