@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.
Files changed (242) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/README.md +251 -1
  3. package/dist/atoms/ActionMenu/ActionMenu.d.ts +37 -0
  4. package/dist/atoms/ActionMenu/ActionMenu.js +28 -0
  5. package/dist/atoms/ActionMenu/index.d.ts +1 -0
  6. package/dist/atoms/ActionMenu/index.js +1 -0
  7. package/dist/atoms/ActionSheet/ActionSheet.d.ts +10 -4
  8. package/dist/atoms/ActionSheet/ActionSheet.js +6 -15
  9. package/dist/atoms/ActionSheet/confirm.d.ts +1 -1
  10. package/dist/atoms/ActionSheet/confirm.js +1 -1
  11. package/dist/atoms/Avatar/Avatar.d.ts +1 -1
  12. package/dist/atoms/Badge/Badge.js +4 -4
  13. package/dist/atoms/Button/Button.js +2 -3
  14. package/dist/atoms/Button/Button.types.d.ts +5 -8
  15. package/dist/atoms/ContextMenu/ContextMenu.d.ts +1 -1
  16. package/dist/atoms/ContextMenu/ContextMenu.js +1 -1
  17. package/dist/atoms/Icon/Icon.d.ts +7 -9
  18. package/dist/atoms/Icon/Icon.js +23 -8
  19. package/dist/atoms/Pill/Pill.d.ts +17 -0
  20. package/dist/atoms/Pill/Pill.js +12 -0
  21. package/dist/atoms/Pill/index.d.ts +1 -0
  22. package/dist/atoms/Pill/index.js +1 -0
  23. package/dist/atoms/ResponsiveDialog/ResponsiveDialog.d.ts +9 -2
  24. package/dist/atoms/ResponsiveDialog/ResponsiveDialog.js +6 -15
  25. package/dist/atoms/StatusIndicator/StatusIndicator.d.ts +14 -0
  26. package/dist/atoms/StatusIndicator/StatusIndicator.js +8 -0
  27. package/dist/atoms/StatusIndicator/index.d.ts +1 -0
  28. package/dist/atoms/StatusIndicator/index.js +1 -0
  29. package/dist/atoms/TextLink/TextLink.d.ts +36 -0
  30. package/dist/atoms/TextLink/TextLink.js +17 -0
  31. package/dist/atoms/TextLink/index.d.ts +1 -0
  32. package/dist/atoms/TextLink/index.js +1 -0
  33. package/dist/atoms/Toast/Toast.d.ts +1 -1
  34. package/dist/atoms/Toast/Toast.js +1 -1
  35. package/dist/atoms/index.d.ts +4 -0
  36. package/dist/atoms/index.js +4 -0
  37. package/dist/hooks/breakpoints.d.ts +20 -0
  38. package/dist/hooks/breakpoints.js +13 -0
  39. package/dist/hooks/index.d.ts +4 -0
  40. package/dist/hooks/index.js +4 -0
  41. package/dist/hooks/useControllableState.d.ts +17 -0
  42. package/dist/hooks/useControllableState.js +22 -0
  43. package/dist/hooks/useElementWidth.d.ts +7 -0
  44. package/dist/hooks/useElementWidth.js +22 -0
  45. package/dist/hooks/useMediaQuery.d.ts +18 -0
  46. package/dist/hooks/useMediaQuery.js +51 -0
  47. package/dist/index.d.ts +4 -0
  48. package/dist/index.js +4 -0
  49. package/dist/molecules/AccentPicker/AccentPicker.d.ts +30 -0
  50. package/dist/molecules/AccentPicker/AccentPicker.js +27 -0
  51. package/dist/molecules/AccentPicker/index.d.ts +1 -0
  52. package/dist/molecules/AccentPicker/index.js +1 -0
  53. package/dist/molecules/Breadcrumbs/Breadcrumbs.d.ts +27 -0
  54. package/dist/molecules/Breadcrumbs/Breadcrumbs.js +40 -0
  55. package/dist/molecules/Breadcrumbs/index.d.ts +1 -0
  56. package/dist/molecules/Breadcrumbs/index.js +1 -0
  57. package/dist/molecules/Calendar/Calendar.d.ts +42 -0
  58. package/dist/molecules/Calendar/Calendar.js +111 -0
  59. package/dist/molecules/Calendar/index.d.ts +1 -0
  60. package/dist/molecules/Calendar/index.js +1 -0
  61. package/dist/molecules/DateInput/DateInput.d.ts +35 -0
  62. package/dist/molecules/DateInput/DateInput.js +41 -0
  63. package/dist/molecules/DateInput/index.d.ts +1 -0
  64. package/dist/molecules/DateInput/index.js +1 -0
  65. package/dist/molecules/EmptyState/EmptyState.d.ts +19 -0
  66. package/dist/molecules/EmptyState/EmptyState.js +7 -0
  67. package/dist/molecules/EmptyState/index.d.ts +1 -0
  68. package/dist/molecules/EmptyState/index.js +1 -0
  69. package/dist/molecules/ErrorState/ErrorState.d.ts +18 -0
  70. package/dist/molecules/ErrorState/ErrorState.js +12 -0
  71. package/dist/molecules/ErrorState/index.d.ts +1 -0
  72. package/dist/molecules/ErrorState/index.js +1 -0
  73. package/dist/molecules/ErrorSummary/ErrorSummary.d.ts +25 -0
  74. package/dist/molecules/ErrorSummary/ErrorSummary.js +32 -0
  75. package/dist/molecules/ErrorSummary/index.d.ts +1 -0
  76. package/dist/molecules/ErrorSummary/index.js +1 -0
  77. package/dist/molecules/FilterChips/FilterChips.d.ts +10 -0
  78. package/dist/molecules/FilterChips/FilterChips.js +13 -0
  79. package/dist/molecules/FilterChips/index.d.ts +1 -0
  80. package/dist/molecules/FilterChips/index.js +1 -0
  81. package/dist/molecules/FilterPopover/FilterPopover.d.ts +27 -0
  82. package/dist/molecules/FilterPopover/FilterPopover.js +32 -0
  83. package/dist/molecules/FilterPopover/index.d.ts +1 -0
  84. package/dist/molecules/FilterPopover/index.js +1 -0
  85. package/dist/molecules/FormActions/FormActions.d.ts +16 -0
  86. package/dist/molecules/FormActions/FormActions.js +5 -0
  87. package/dist/molecules/FormActions/index.d.ts +1 -0
  88. package/dist/molecules/FormActions/index.js +1 -0
  89. package/dist/molecules/FormField/FormField.d.ts +52 -0
  90. package/dist/molecules/FormField/FormField.js +63 -0
  91. package/dist/molecules/FormField/index.d.ts +1 -0
  92. package/dist/molecules/FormField/index.js +1 -0
  93. package/dist/molecules/FormSection/FormSection.d.ts +14 -0
  94. package/dist/molecules/FormSection/FormSection.js +10 -0
  95. package/dist/molecules/FormSection/index.d.ts +1 -0
  96. package/dist/molecules/FormSection/index.js +1 -0
  97. package/dist/molecules/KeyValueList/KeyValueList.d.ts +22 -0
  98. package/dist/molecules/KeyValueList/KeyValueList.js +9 -0
  99. package/dist/molecules/KeyValueList/index.d.ts +1 -0
  100. package/dist/molecules/KeyValueList/index.js +1 -0
  101. package/dist/molecules/LoadingState/LoadingState.d.ts +20 -0
  102. package/dist/molecules/LoadingState/LoadingState.js +22 -0
  103. package/dist/molecules/LoadingState/index.d.ts +1 -0
  104. package/dist/molecules/LoadingState/index.js +1 -0
  105. package/dist/molecules/NavItem/NavItem.d.ts +31 -0
  106. package/dist/molecules/NavItem/NavItem.js +72 -0
  107. package/dist/molecules/NavItem/context.d.ts +14 -0
  108. package/dist/molecules/NavItem/context.js +5 -0
  109. package/dist/molecules/NavItem/index.d.ts +1 -0
  110. package/dist/molecules/NavItem/index.js +1 -0
  111. package/dist/molecules/Pagination/Pagination.d.ts +32 -0
  112. package/dist/molecules/Pagination/Pagination.js +57 -0
  113. package/dist/molecules/Pagination/index.d.ts +1 -0
  114. package/dist/molecules/Pagination/index.js +1 -0
  115. package/dist/molecules/PasswordInput/PasswordInput.d.ts +14 -0
  116. package/dist/molecules/PasswordInput/PasswordInput.js +16 -0
  117. package/dist/molecules/PasswordInput/index.d.ts +1 -0
  118. package/dist/molecules/PasswordInput/index.js +1 -0
  119. package/dist/molecules/SearchField/SearchField.d.ts +24 -0
  120. package/dist/molecules/SearchField/SearchField.js +58 -0
  121. package/dist/molecules/SearchField/index.d.ts +1 -0
  122. package/dist/molecules/SearchField/index.js +1 -0
  123. package/dist/molecules/StatGroup/StatGroup.d.ts +21 -0
  124. package/dist/molecules/StatGroup/StatGroup.js +10 -0
  125. package/dist/molecules/StatGroup/index.d.ts +1 -0
  126. package/dist/molecules/StatGroup/index.js +1 -0
  127. package/dist/molecules/ThemeToggle/ThemeToggle.d.ts +23 -0
  128. package/dist/molecules/ThemeToggle/ThemeToggle.js +26 -0
  129. package/dist/molecules/ThemeToggle/index.d.ts +1 -0
  130. package/dist/molecules/ThemeToggle/index.js +1 -0
  131. package/dist/molecules/UserMenu/UserMenu.d.ts +20 -0
  132. package/dist/molecules/UserMenu/UserMenu.js +14 -0
  133. package/dist/molecules/UserMenu/index.d.ts +1 -0
  134. package/dist/molecules/UserMenu/index.js +1 -0
  135. package/dist/molecules/index.d.ts +21 -0
  136. package/dist/molecules/index.js +21 -0
  137. package/dist/organisms/AppShell/AppShell.d.ts +29 -0
  138. package/dist/organisms/AppShell/AppShell.js +34 -0
  139. package/dist/organisms/AppShell/context.d.ts +14 -0
  140. package/dist/organisms/AppShell/context.js +6 -0
  141. package/dist/organisms/AppShell/index.d.ts +1 -0
  142. package/dist/organisms/AppShell/index.js +1 -0
  143. package/dist/organisms/AuthForms/AuthForms.d.ts +83 -0
  144. package/dist/organisms/AuthForms/AuthForms.js +57 -0
  145. package/dist/organisms/AuthForms/index.d.ts +2 -0
  146. package/dist/organisms/AuthForms/index.js +1 -0
  147. package/dist/organisms/AuthForms/shared.d.ts +36 -0
  148. package/dist/organisms/AuthForms/shared.js +36 -0
  149. package/dist/organisms/BulkActionBar/BulkActionBar.d.ts +25 -0
  150. package/dist/organisms/BulkActionBar/BulkActionBar.js +18 -0
  151. package/dist/organisms/BulkActionBar/index.d.ts +1 -0
  152. package/dist/organisms/BulkActionBar/index.js +1 -0
  153. package/dist/organisms/Chart/CategoryCharts.d.ts +34 -0
  154. package/dist/organisms/Chart/CategoryCharts.js +93 -0
  155. package/dist/organisms/Chart/ChartFrame.d.ts +26 -0
  156. package/dist/organisms/Chart/ChartFrame.js +18 -0
  157. package/dist/organisms/Chart/DonutChart.d.ts +25 -0
  158. package/dist/organisms/Chart/DonutChart.js +53 -0
  159. package/dist/organisms/Chart/Sparkline.d.ts +15 -0
  160. package/dist/organisms/Chart/Sparkline.js +18 -0
  161. package/dist/organisms/Chart/index.d.ts +5 -0
  162. package/dist/organisms/Chart/index.js +5 -0
  163. package/dist/organisms/Chart/shared.d.ts +51 -0
  164. package/dist/organisms/Chart/shared.js +113 -0
  165. package/dist/organisms/DataTable/DataTable.d.ts +63 -0
  166. package/dist/organisms/DataTable/DataTable.js +119 -0
  167. package/dist/organisms/DataTable/index.d.ts +3 -0
  168. package/dist/organisms/DataTable/index.js +3 -0
  169. package/dist/organisms/DataTable/types.d.ts +39 -0
  170. package/dist/organisms/DataTable/types.js +1 -0
  171. package/dist/organisms/DataTable/useClientTable.d.ts +27 -0
  172. package/dist/organisms/DataTable/useClientTable.js +51 -0
  173. package/dist/organisms/DateRangePicker/DateRangePicker.d.ts +43 -0
  174. package/dist/organisms/DateRangePicker/DateRangePicker.js +54 -0
  175. package/dist/organisms/DateRangePicker/index.d.ts +1 -0
  176. package/dist/organisms/DateRangePicker/index.js +1 -0
  177. package/dist/organisms/FilterBar/FilterBar.d.ts +28 -0
  178. package/dist/organisms/FilterBar/FilterBar.js +16 -0
  179. package/dist/organisms/FilterBar/index.d.ts +1 -0
  180. package/dist/organisms/FilterBar/index.js +1 -0
  181. package/dist/organisms/PageHeader/PageHeader.d.ts +31 -0
  182. package/dist/organisms/PageHeader/PageHeader.js +19 -0
  183. package/dist/organisms/PageHeader/index.d.ts +1 -0
  184. package/dist/organisms/PageHeader/index.js +1 -0
  185. package/dist/organisms/PageTabs/PageTabs.d.ts +27 -0
  186. package/dist/organisms/PageTabs/PageTabs.js +31 -0
  187. package/dist/organisms/PageTabs/index.d.ts +1 -0
  188. package/dist/organisms/PageTabs/index.js +1 -0
  189. package/dist/organisms/Sidebar/Sidebar.d.ts +48 -0
  190. package/dist/organisms/Sidebar/Sidebar.js +61 -0
  191. package/dist/organisms/Sidebar/index.d.ts +1 -0
  192. package/dist/organisms/Sidebar/index.js +1 -0
  193. package/dist/organisms/Topbar/Topbar.d.ts +24 -0
  194. package/dist/organisms/Topbar/Topbar.js +18 -0
  195. package/dist/organisms/Topbar/index.d.ts +1 -0
  196. package/dist/organisms/Topbar/index.js +1 -0
  197. package/dist/organisms/index.d.ts +11 -0
  198. package/dist/organisms/index.js +11 -0
  199. package/dist/strings.d.ts +108 -15
  200. package/dist/strings.js +73 -7
  201. package/dist/templates/AdminLayout/AdminLayout.d.ts +14 -0
  202. package/dist/templates/AdminLayout/AdminLayout.js +9 -0
  203. package/dist/templates/AdminLayout/index.d.ts +1 -0
  204. package/dist/templates/AdminLayout/index.js +1 -0
  205. package/dist/templates/AuthLayout/AuthLayout.d.ts +28 -0
  206. package/dist/templates/AuthLayout/AuthLayout.js +11 -0
  207. package/dist/templates/AuthLayout/index.d.ts +1 -0
  208. package/dist/templates/AuthLayout/index.js +1 -0
  209. package/dist/templates/DashboardPage/DashboardPage.d.ts +25 -0
  210. package/dist/templates/DashboardPage/DashboardPage.js +12 -0
  211. package/dist/templates/DashboardPage/index.d.ts +1 -0
  212. package/dist/templates/DashboardPage/index.js +1 -0
  213. package/dist/templates/DetailPage/DetailPage.d.ts +20 -0
  214. package/dist/templates/DetailPage/DetailPage.js +11 -0
  215. package/dist/templates/DetailPage/index.d.ts +1 -0
  216. package/dist/templates/DetailPage/index.js +1 -0
  217. package/dist/templates/FormPage/FormPage.d.ts +26 -0
  218. package/dist/templates/FormPage/FormPage.js +11 -0
  219. package/dist/templates/FormPage/index.d.ts +1 -0
  220. package/dist/templates/FormPage/index.js +1 -0
  221. package/dist/templates/ListPage/ListPage.d.ts +17 -0
  222. package/dist/templates/ListPage/ListPage.js +8 -0
  223. package/dist/templates/ListPage/index.d.ts +1 -0
  224. package/dist/templates/ListPage/index.js +1 -0
  225. package/dist/templates/SettingsPage/SettingsPage.d.ts +32 -0
  226. package/dist/templates/SettingsPage/SettingsPage.js +13 -0
  227. package/dist/templates/SettingsPage/index.d.ts +1 -0
  228. package/dist/templates/SettingsPage/index.js +1 -0
  229. package/dist/templates/StatusPage/StatusPage.d.ts +18 -0
  230. package/dist/templates/StatusPage/StatusPage.js +8 -0
  231. package/dist/templates/StatusPage/index.d.ts +1 -0
  232. package/dist/templates/StatusPage/index.js +1 -0
  233. package/dist/templates/index.d.ts +8 -0
  234. package/dist/templates/index.js +8 -0
  235. package/dist/utils/dates.d.ts +67 -0
  236. package/dist/utils/dates.js +154 -0
  237. package/dist/utils/index.d.ts +1 -0
  238. package/dist/utils/index.js +1 -0
  239. package/dist/utils/theme.d.ts +4 -0
  240. package/dist/utils/theme.js +7 -3
  241. package/package.json +4 -1
  242. package/tokens.css +494 -21
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Cross2Icon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
3
+ import { IconButton, Kbd, TextField } from '@radix-ui/themes';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import { Input } from '../../atoms/Input/Input.js';
6
+ import { useUiStrings } from '../../strings.js';
7
+ function isEditable(el) {
8
+ if (!(el instanceof HTMLElement))
9
+ return false;
10
+ return el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
11
+ }
12
+ /**
13
+ * A search input with a magnifier, a clear button once there is text, Escape to clear, and an
14
+ * optional page-wide shortcut key. The shortcut hint is hidden on touch screens, which have no
15
+ * keyboard to press it on, and once the field has text.
16
+ */
17
+ export function SearchField({ value, defaultValue = '', onValueChange, debounceMs = 0, placeholder, 'aria-label': ariaLabel, shortcut, size, id, className, }) {
18
+ const strings = useUiStrings();
19
+ const [text, setText] = useState(value ?? defaultValue);
20
+ const inputRef = useRef(null);
21
+ const timer = useRef(undefined);
22
+ const report = useRef(onValueChange);
23
+ report.current = onValueChange;
24
+ useEffect(() => {
25
+ if (value !== undefined)
26
+ setText(value);
27
+ }, [value]);
28
+ useEffect(() => () => clearTimeout(timer.current), []);
29
+ useEffect(() => {
30
+ if (!shortcut)
31
+ return;
32
+ const onKey = (e) => {
33
+ if (e.key !== shortcut || e.metaKey || e.ctrlKey || e.altKey || isEditable(e.target))
34
+ return;
35
+ e.preventDefault();
36
+ inputRef.current?.focus();
37
+ };
38
+ document.addEventListener('keydown', onKey);
39
+ return () => document.removeEventListener('keydown', onKey);
40
+ }, [shortcut]);
41
+ const change = (next, immediate = false) => {
42
+ setText(next);
43
+ clearTimeout(timer.current);
44
+ if (immediate || debounceMs <= 0)
45
+ report.current(next);
46
+ else
47
+ timer.current = setTimeout(() => report.current(next), debounceMs);
48
+ };
49
+ return (_jsxs(Input, { ref: inputRef, id: id, type: "search", size: size, value: text, placeholder: placeholder, "aria-label": ariaLabel ?? strings.search, "aria-keyshortcuts": shortcut, className: className, onChange: (e) => change(e.target.value), onKeyDown: (e) => {
50
+ if (e.key === 'Escape' && text) {
51
+ e.preventDefault();
52
+ change('', true);
53
+ }
54
+ }, children: [_jsx(TextField.Slot, { children: _jsx(MagnifyingGlassIcon, { "aria-hidden": "true" }) }), _jsx(TextField.Slot, { children: text ? (_jsx(IconButton, { type: "button", size: "1", variant: "ghost", color: "gray", "aria-label": strings.clearSearch, onClick: () => {
55
+ change('', true);
56
+ inputRef.current?.focus();
57
+ }, children: _jsx(Cross2Icon, {}) })) : shortcut ? (_jsx(Kbd, { className: "ui-search__kbd", "aria-hidden": "true", children: shortcut })) : null })] }));
58
+ }
@@ -0,0 +1 @@
1
+ export * from './SearchField.js';
@@ -0,0 +1 @@
1
+ export * from './SearchField.js';
@@ -0,0 +1,21 @@
1
+ import { type ScrollStripBreakpoint } from '../../atoms/ScrollStrip/ScrollStrip.js';
2
+ import { type StatTileProps } from '../../atoms/StatTile/StatTile.js';
3
+ export interface StatGroupItem extends StatTileProps {
4
+ /** React key. Defaults to the label when it is a string, else the index. */
5
+ id?: string;
6
+ }
7
+ export interface StatGroupProps {
8
+ items: StatGroupItem[];
9
+ /** Grid columns from `gridFrom`. Default: the number of items, at most 4. */
10
+ columns?: number;
11
+ /** Below this the tiles scroll sideways; from it they are a grid. Default `md`. */
12
+ gridFrom?: ScrollStripBreakpoint;
13
+ /** Names the scrollable region (otherwise the `scrollableRegion` UI string). */
14
+ 'aria-label'?: string;
15
+ className?: string;
16
+ }
17
+ /**
18
+ * A row of KPI tiles: a swipeable strip on a phone, a grid from `gridFrom`. A `ScrollStrip` of
19
+ * `StatTile`s, so both keep their own behaviour and accessibility.
20
+ */
21
+ export declare function StatGroup({ items, columns, gridFrom, 'aria-label': ariaLabel, className, }: StatGroupProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ScrollStrip } from '../../atoms/ScrollStrip/ScrollStrip.js';
3
+ import { StatTile } from '../../atoms/StatTile/StatTile.js';
4
+ /**
5
+ * A row of KPI tiles: a swipeable strip on a phone, a grid from `gridFrom`. A `ScrollStrip` of
6
+ * `StatTile`s, so both keep their own behaviour and accessibility.
7
+ */
8
+ export function StatGroup({ items, columns, gridFrom = 'md', 'aria-label': ariaLabel, className, }) {
9
+ return (_jsx(ScrollStrip, { gridFrom: gridFrom, columns: columns ?? Math.min(items.length, 4), "aria-label": ariaLabel, className: className, children: items.map(({ id, ...tile }, i) => (_jsx(StatTile, { ...tile }, id ?? (typeof tile.label === 'string' ? tile.label : `stat-${i}`)))) }));
10
+ }
@@ -0,0 +1 @@
1
+ export * from './StatGroup.js';
@@ -0,0 +1 @@
1
+ export * from './StatGroup.js';
@@ -0,0 +1,23 @@
1
+ import type { Theme } from '../../utils/theme.js';
2
+ /** The part of a `createTheming` instance the toggle uses. */
3
+ export interface ThemeStore {
4
+ readTheme(): Theme;
5
+ storeTheme(theme: Theme): void;
6
+ }
7
+ export interface ThemeToggleProps {
8
+ value?: Theme;
9
+ /** Initial value when uncontrolled. Default: `theming.readTheme()`, else `system`. */
10
+ defaultValue?: Theme;
11
+ onValueChange?: (theme: Theme) => void;
12
+ /** A `createTheming` instance: reads the stored choice and stores each new one. */
13
+ theming?: ThemeStore;
14
+ size?: 'sm' | 'md';
15
+ fullWidth?: boolean;
16
+ }
17
+ /**
18
+ * System / Light / Dark, as a segmented control.
19
+ *
20
+ * It chooses and remembers; it does not apply. Pass the value on to Radix's `<Theme
21
+ * appearance>` (or `applyTheme`) in `onValueChange`, because the app owns its `<Theme>`.
22
+ */
23
+ export declare function ThemeToggle({ value, defaultValue, onValueChange, theming, size, fullWidth, }: ThemeToggleProps): import("react").JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { SegmentedControl } from '../../atoms/SegmentedControl/SegmentedControl.js';
3
+ import { useControllableState } from '../../hooks/useControllableState.js';
4
+ import { useUiStrings } from '../../strings.js';
5
+ /**
6
+ * System / Light / Dark, as a segmented control.
7
+ *
8
+ * It chooses and remembers; it does not apply. Pass the value on to Radix's `<Theme
9
+ * appearance>` (or `applyTheme`) in `onValueChange`, because the app owns its `<Theme>`.
10
+ */
11
+ export function ThemeToggle({ value, defaultValue, onValueChange, theming, size, fullWidth, }) {
12
+ const strings = useUiStrings();
13
+ const [current, setCurrent] = useControllableState({
14
+ value,
15
+ defaultValue: defaultValue ?? theming?.readTheme() ?? 'system',
16
+ onChange: (next) => {
17
+ theming?.storeTheme(next);
18
+ onValueChange?.(next);
19
+ },
20
+ });
21
+ return (_jsx(SegmentedControl, { "aria-label": strings.appearance, size: size, fullWidth: fullWidth, value: current, onValueChange: (v) => setCurrent(v), options: [
22
+ { value: 'system', label: strings.system },
23
+ { value: 'light', label: strings.light },
24
+ { value: 'dark', label: strings.dark },
25
+ ] }));
26
+ }
@@ -0,0 +1 @@
1
+ export * from './ThemeToggle.js';
@@ -0,0 +1 @@
1
+ export * from './ThemeToggle.js';
@@ -0,0 +1,20 @@
1
+ import { type ActionMenuItem } from '../../atoms/ActionMenu/ActionMenu.js';
2
+ import type { Breakpoint } from '../../hooks/breakpoints.js';
3
+ export interface UserMenuProps {
4
+ name: string;
5
+ /** Shown under the name in the sheet heading on small screens. */
6
+ email?: string;
7
+ avatarSrc?: string;
8
+ /** Profile, settings, sign out… Same shape as `ActionMenu`'s items. */
9
+ items: ActionMenuItem[];
10
+ onSelect: (id: string) => void;
11
+ /** Passed to ActionMenu. Default `sm`. */
12
+ sheetBelow?: Breakpoint | false;
13
+ }
14
+ /**
15
+ * The signed-in person's avatar and name, opening their account actions.
16
+ *
17
+ * Below 768px the name is visually hidden (still read out) and only the avatar shows, and the
18
+ * menu becomes a sheet headed by the name and email.
19
+ */
20
+ export declare function UserMenu({ name, email, avatarSrc, items, onSelect, sheetBelow }: UserMenuProps): import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ActionMenu } from '../../atoms/ActionMenu/ActionMenu.js';
3
+ import { Avatar } from '../../atoms/Avatar/Avatar.js';
4
+ import { useUiStrings } from '../../strings.js';
5
+ /**
6
+ * The signed-in person's avatar and name, opening their account actions.
7
+ *
8
+ * Below 768px the name is visually hidden (still read out) and only the avatar shows, and the
9
+ * menu becomes a sheet headed by the name and email.
10
+ */
11
+ export function UserMenu({ name, email, avatarSrc, items, onSelect, sheetBelow }) {
12
+ const strings = useUiStrings();
13
+ return (_jsx(ActionMenu, { items: items, onSelect: onSelect, sheetBelow: sheetBelow, title: _jsxs(_Fragment, { children: [name, email ? _jsx("span", { className: "block text-sm font-normal text-ink-muted", children: email }) : null] }), trigger: _jsxs("button", { type: "button", className: "ui-user-menu__trigger", children: [_jsx("span", { "aria-hidden": "true", className: "inline-flex", children: _jsx(Avatar, { src: avatarSrc, name: name }) }), _jsx("span", { className: "ui-user-menu__name", children: name }), _jsxs("span", { className: "ui-sr-only", children: [", ", strings.userMenu] })] }) }));
14
+ }
@@ -0,0 +1 @@
1
+ export * from './UserMenu.js';
@@ -0,0 +1 @@
1
+ export * from './UserMenu.js';
@@ -0,0 +1,21 @@
1
+ export * from './AccentPicker/index.js';
2
+ export * from './Breadcrumbs/index.js';
3
+ export * from './Calendar/index.js';
4
+ export * from './DateInput/index.js';
5
+ export * from './EmptyState/index.js';
6
+ export * from './ErrorState/index.js';
7
+ export * from './ErrorSummary/index.js';
8
+ export * from './FilterChips/index.js';
9
+ export * from './FilterPopover/index.js';
10
+ export * from './FormActions/index.js';
11
+ export * from './FormField/index.js';
12
+ export * from './FormSection/index.js';
13
+ export * from './KeyValueList/index.js';
14
+ export * from './LoadingState/index.js';
15
+ export * from './NavItem/index.js';
16
+ export * from './Pagination/index.js';
17
+ export * from './PasswordInput/index.js';
18
+ export * from './SearchField/index.js';
19
+ export * from './StatGroup/index.js';
20
+ export * from './ThemeToggle/index.js';
21
+ export * from './UserMenu/index.js';
@@ -0,0 +1,21 @@
1
+ export * from './AccentPicker/index.js';
2
+ export * from './Breadcrumbs/index.js';
3
+ export * from './Calendar/index.js';
4
+ export * from './DateInput/index.js';
5
+ export * from './EmptyState/index.js';
6
+ export * from './ErrorState/index.js';
7
+ export * from './ErrorSummary/index.js';
8
+ export * from './FilterChips/index.js';
9
+ export * from './FilterPopover/index.js';
10
+ export * from './FormActions/index.js';
11
+ export * from './FormField/index.js';
12
+ export * from './FormSection/index.js';
13
+ export * from './KeyValueList/index.js';
14
+ export * from './LoadingState/index.js';
15
+ export * from './NavItem/index.js';
16
+ export * from './Pagination/index.js';
17
+ export * from './PasswordInput/index.js';
18
+ export * from './SearchField/index.js';
19
+ export * from './StatGroup/index.js';
20
+ export * from './ThemeToggle/index.js';
21
+ export * from './UserMenu/index.js';
@@ -0,0 +1,29 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type TabBarProps } from '../../atoms/TabBar/TabBar.js';
3
+ import { type Breakpoint } from '../../hooks/breakpoints.js';
4
+ export interface AppShellProps {
5
+ /** A `Sidebar`. */
6
+ sidebar: ReactNode;
7
+ /** A `Topbar`, above the content. */
8
+ topbar?: ReactNode;
9
+ /**
10
+ * A bottom TabBar for small screens, hidden from `breakpoint` up. Optional: the off-canvas
11
+ * sidebar is always available through the Topbar's menu button.
12
+ */
13
+ mobileNav?: Omit<TabBarProps, 'hideFrom'>;
14
+ children: ReactNode;
15
+ /** From here up the sidebar is a persistent column. Default `md` (1024px). */
16
+ breakpoint?: Breakpoint;
17
+ /** Controlled open state of the off-canvas sidebar. */
18
+ navOpen?: boolean;
19
+ onNavOpenChange?: (open: boolean) => void;
20
+ className?: string;
21
+ }
22
+ /**
23
+ * The admin frame: sidebar, topbar and content, with a skip link to the content.
24
+ *
25
+ * Wide screens get a two-column grid with a sticky sidebar. Narrow ones get one column, the
26
+ * sidebar off-canvas behind the Topbar's menu button, and optionally a bottom TabBar. The
27
+ * shell is at least the dynamic viewport's height, so the browser's own bars do not cut it off.
28
+ */
29
+ export declare function AppShell({ sidebar, topbar, mobileNav, children, breakpoint, navOpen: navOpenProp, onNavOpenChange, className, }: AppShellProps): import("react").JSX.Element;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useState } from 'react';
3
+ import { TabBar, TabBarSpacer } from '../../atoms/TabBar/TabBar.js';
4
+ import { minWidth } from '../../hooks/breakpoints.js';
5
+ import { useMediaQuery } from '../../hooks/useMediaQuery.js';
6
+ import { useUiStrings } from '../../strings.js';
7
+ import { ShellContext } from './context.js';
8
+ /**
9
+ * The admin frame: sidebar, topbar and content, with a skip link to the content.
10
+ *
11
+ * Wide screens get a two-column grid with a sticky sidebar. Narrow ones get one column, the
12
+ * sidebar off-canvas behind the Topbar's menu button, and optionally a bottom TabBar. The
13
+ * shell is at least the dynamic viewport's height, so the browser's own bars do not cut it off.
14
+ */
15
+ export function AppShell({ sidebar, topbar, mobileNav, children, breakpoint = 'md', navOpen: navOpenProp, onNavOpenChange, className, }) {
16
+ const strings = useUiStrings();
17
+ const wide = useMediaQuery(minWidth(breakpoint));
18
+ const [ownOpen, setOwnOpen] = useState(false);
19
+ const mainId = useId();
20
+ const navOpen = navOpenProp ?? ownOpen;
21
+ const setNavOpen = (open) => {
22
+ if (navOpenProp === undefined)
23
+ setOwnOpen(open);
24
+ onNavOpenChange?.(open);
25
+ };
26
+ // Widening the window with the panel open closes it for good, rather than leaving it open
27
+ // behind the persistent sidebar to reappear the next time the window narrows.
28
+ // biome-ignore lint/correctness/useExhaustiveDependencies: setNavOpen is rebuilt every render; only these two values decide.
29
+ useEffect(() => {
30
+ if (wide && navOpen)
31
+ setNavOpen(false);
32
+ }, [wide, navOpen]);
33
+ return (_jsx(ShellContext.Provider, { value: { breakpoint, wide, navOpen: navOpen && !wide, setNavOpen }, children: _jsxs("div", { className: ['ui-shell', className].filter(Boolean).join(' '), "data-wide": wide ? '' : undefined, "data-tabbar": mobileNav && !wide ? '' : undefined, children: [_jsx("a", { className: "ui-skip-link", href: `#${mainId}`, children: strings.skipToContent }), sidebar, _jsxs("div", { className: "ui-shell__main", children: [topbar, _jsx("main", { id: mainId, tabIndex: -1, className: "ui-shell__content", children: children }), mobileNav ? (_jsxs(_Fragment, { children: [_jsx(TabBarSpacer, { hideFrom: breakpoint }), _jsx(TabBar, { ...mobileNav, hideFrom: breakpoint })] })) : null] })] }) }));
34
+ }
@@ -0,0 +1,14 @@
1
+ import type { Breakpoint } from '../../hooks/breakpoints.js';
2
+ /**
3
+ * What AppShell shares with the Sidebar and Topbar inside it: whether the screen is wide enough
4
+ * for a persistent sidebar, and the open state of the off-canvas one when it is not.
5
+ */
6
+ export interface ShellContextValue {
7
+ breakpoint: Breakpoint;
8
+ wide: boolean;
9
+ navOpen: boolean;
10
+ setNavOpen: (open: boolean) => void;
11
+ }
12
+ export declare const ShellContext: import("react").Context<ShellContextValue | null>;
13
+ /** `null` outside an AppShell; Sidebar and Topbar then manage themselves. */
14
+ export declare function useShell(): ShellContextValue | null;
@@ -0,0 +1,6 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const ShellContext = createContext(null);
3
+ /** `null` outside an AppShell; Sidebar and Topbar then manage themselves. */
4
+ export function useShell() {
5
+ return useContext(ShellContext);
6
+ }
@@ -0,0 +1 @@
1
+ export * from './AppShell.js';
@@ -0,0 +1 @@
1
+ export * from './AppShell.js';
@@ -0,0 +1,83 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type AuthFormBaseProps } from './shared.js';
3
+ export interface LoginValues {
4
+ identifier: string;
5
+ password: string;
6
+ remember: boolean;
7
+ }
8
+ export interface LoginFormProps extends AuthFormBaseProps<'identifier' | 'password'> {
9
+ onSubmit: (values: LoginValues) => void;
10
+ labels: {
11
+ /** Names the form. */
12
+ form: string;
13
+ identifier: string;
14
+ password: string;
15
+ submit: string;
16
+ /** With it, a "remember me" checkbox. */
17
+ remember?: string;
18
+ };
19
+ /** `email` (the default) or `username`. Both use `autocomplete="username"`. */
20
+ identifierType?: 'email' | 'username';
21
+ /** A "Forgot password?" link, shown under the password field. */
22
+ forgotPassword?: ReactNode;
23
+ defaultValues?: Partial<LoginValues>;
24
+ }
25
+ export declare function LoginForm({ onSubmit, labels, identifierType, forgotPassword, defaultValues, errors, formError, pending, footer, className, }: LoginFormProps): import("react").JSX.Element;
26
+ export interface SignupValues {
27
+ name: string;
28
+ email: string;
29
+ password: string;
30
+ acceptedTerms: boolean;
31
+ }
32
+ export interface SignupFormProps extends AuthFormBaseProps<'name' | 'email' | 'password' | 'acceptedTerms'> {
33
+ onSubmit: (values: SignupValues) => void;
34
+ labels: {
35
+ form: string;
36
+ /** With it, a name field. */
37
+ name?: string;
38
+ email: string;
39
+ password: string;
40
+ /** The rules, shown under the password field and linked to it. */
41
+ passwordHint?: string;
42
+ /** With it, a required checkbox; may contain links to the terms. */
43
+ terms?: ReactNode;
44
+ submit: string;
45
+ };
46
+ defaultValues?: Partial<SignupValues>;
47
+ }
48
+ export declare function SignupForm({ onSubmit, labels, defaultValues, errors, formError, pending, footer, className, }: SignupFormProps): import("react").JSX.Element;
49
+ export interface ForgotPasswordFormProps extends AuthFormBaseProps<'email'> {
50
+ onSubmit: (values: {
51
+ email: string;
52
+ }) => void;
53
+ labels: {
54
+ form: string;
55
+ email: string;
56
+ submit: string;
57
+ /** Shown once `sent`: "If that address has an account, a link is on its way." */
58
+ sent: string;
59
+ };
60
+ /**
61
+ * The request went out. Shows `labels.sent` as a status message and keeps the form, so the
62
+ * person can correct the address or ask again. Word `labels.sent` so it does not reveal
63
+ * whether the address has an account.
64
+ */
65
+ sent?: boolean;
66
+ defaultEmail?: string;
67
+ }
68
+ export declare function ForgotPasswordForm({ onSubmit, labels, sent, defaultEmail, errors, formError, pending, footer, className, }: ForgotPasswordFormProps): import("react").JSX.Element;
69
+ export interface ResetPasswordValues {
70
+ password: string;
71
+ confirmPassword: string;
72
+ }
73
+ export interface ResetPasswordFormProps extends AuthFormBaseProps<'password' | 'confirmPassword'> {
74
+ onSubmit: (values: ResetPasswordValues) => void;
75
+ labels: {
76
+ form: string;
77
+ password: string;
78
+ confirmPassword: string;
79
+ passwordHint?: string;
80
+ submit: string;
81
+ };
82
+ }
83
+ export declare function ResetPasswordForm({ onSubmit, labels, errors, formError, pending, footer, className, }: ResetPasswordFormProps): import("react").JSX.Element;
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Alert } from '../../atoms/Alert/Alert.js';
4
+ import { Checkbox } from '../../atoms/Checkbox/Checkbox.js';
5
+ import { Input } from '../../atoms/Input/Input.js';
6
+ import { FormField } from '../../molecules/FormField/FormField.js';
7
+ import { PasswordInput } from '../../molecules/PasswordInput/PasswordInput.js';
8
+ import { AuthFormShell, useAuthForm } from './shared.js';
9
+ const LOGIN_FIELDS = ['identifier', 'password'];
10
+ export function LoginForm({ onSubmit, labels, identifierType = 'email', forgotPassword, defaultValues, errors, formError, pending, footer, className, }) {
11
+ const { ids, markSubmitted } = useAuthForm(LOGIN_FIELDS, errors);
12
+ const [values, setValues] = useState({
13
+ identifier: '',
14
+ password: '',
15
+ remember: false,
16
+ ...defaultValues,
17
+ });
18
+ const set = (k, v) => setValues((s) => ({ ...s, [k]: v }));
19
+ return (_jsxs(AuthFormShell, { label: labels.form, submitLabel: labels.submit, formError: formError, pending: pending, footer: footer, className: className, onSubmit: () => {
20
+ markSubmitted();
21
+ onSubmit(values);
22
+ }, children: [_jsx(FormField, { id: ids.identifier, label: labels.identifier, required: true, error: errors?.identifier, children: _jsx(Input, { type: identifierType === 'email' ? 'email' : 'text', inputMode: identifierType === 'email' ? 'email' : undefined, autoComplete: "username", autoCapitalize: "none", spellCheck: false, value: values.identifier, onChange: (e) => set('identifier', e.target.value) }) }), _jsx(FormField, { id: ids.password, label: labels.password, required: true, error: errors?.password, children: _jsx(PasswordInput, { autoComplete: "current-password", value: values.password, onChange: (e) => set('password', e.target.value) }) }), forgotPassword || labels.remember ? (_jsxs("div", { className: "ui-auth-form__row", children: [labels.remember ? (_jsx(FormField, { label: labels.remember, layout: "inline", children: _jsx(Checkbox, { checked: values.remember, onCheckedChange: (c) => set('remember', c === true) }) })) : (_jsx("span", {})), forgotPassword] })) : null] }));
23
+ }
24
+ const SIGNUP_FIELDS = ['name', 'email', 'password', 'acceptedTerms'];
25
+ export function SignupForm({ onSubmit, labels, defaultValues, errors, formError, pending, footer, className, }) {
26
+ const { ids, markSubmitted } = useAuthForm(SIGNUP_FIELDS, errors);
27
+ const [values, setValues] = useState({
28
+ name: '',
29
+ email: '',
30
+ password: '',
31
+ acceptedTerms: false,
32
+ ...defaultValues,
33
+ });
34
+ const set = (k, v) => setValues((s) => ({ ...s, [k]: v }));
35
+ return (_jsxs(AuthFormShell, { label: labels.form, submitLabel: labels.submit, formError: formError, pending: pending, footer: footer, className: className, onSubmit: () => {
36
+ markSubmitted();
37
+ onSubmit(values);
38
+ }, children: [labels.name ? (_jsx(FormField, { id: ids.name, label: labels.name, required: true, error: errors?.name, children: _jsx(Input, { autoComplete: "name", value: values.name, onChange: (e) => set('name', e.target.value) }) })) : null, _jsx(FormField, { id: ids.email, label: labels.email, required: true, error: errors?.email, children: _jsx(Input, { type: "email", inputMode: "email", autoComplete: "email", autoCapitalize: "none", spellCheck: false, value: values.email, onChange: (e) => set('email', e.target.value) }) }), _jsx(FormField, { id: ids.password, label: labels.password, hint: labels.passwordHint, required: true, error: errors?.password, children: _jsx(PasswordInput, { autoComplete: "new-password", value: values.password, onChange: (e) => set('password', e.target.value) }) }), labels.terms ? (_jsx(FormField, { id: ids.acceptedTerms, label: labels.terms, layout: "inline", required: true, error: errors?.acceptedTerms, children: _jsx(Checkbox, { checked: values.acceptedTerms, onCheckedChange: (c) => set('acceptedTerms', c === true) }) })) : null] }));
39
+ }
40
+ const FORGOT_FIELDS = ['email'];
41
+ export function ForgotPasswordForm({ onSubmit, labels, sent = false, defaultEmail = '', errors, formError, pending, footer, className, }) {
42
+ const { ids, markSubmitted } = useAuthForm(FORGOT_FIELDS, errors);
43
+ const [email, setEmail] = useState(defaultEmail);
44
+ return (_jsxs(AuthFormShell, { label: labels.form, submitLabel: labels.submit, formError: formError, pending: pending, footer: footer, className: className, onSubmit: () => {
45
+ markSubmitted();
46
+ onSubmit({ email });
47
+ }, children: [sent ? _jsx(Alert, { type: "success", body: labels.sent }) : null, _jsx(FormField, { id: ids.email, label: labels.email, required: true, error: errors?.email, children: _jsx(Input, { type: "email", inputMode: "email", autoComplete: "email", autoCapitalize: "none", spellCheck: false, value: email, onChange: (e) => setEmail(e.target.value) }) })] }));
48
+ }
49
+ const RESET_FIELDS = ['password', 'confirmPassword'];
50
+ export function ResetPasswordForm({ onSubmit, labels, errors, formError, pending, footer, className, }) {
51
+ const { ids, markSubmitted } = useAuthForm(RESET_FIELDS, errors);
52
+ const [values, setValues] = useState({ password: '', confirmPassword: '' });
53
+ return (_jsxs(AuthFormShell, { label: labels.form, submitLabel: labels.submit, formError: formError, pending: pending, footer: footer, className: className, onSubmit: () => {
54
+ markSubmitted();
55
+ onSubmit(values);
56
+ }, children: [_jsx(FormField, { id: ids.password, label: labels.password, hint: labels.passwordHint, required: true, error: errors?.password, children: _jsx(PasswordInput, { autoComplete: "new-password", value: values.password, onChange: (e) => setValues((v) => ({ ...v, password: e.target.value })) }) }), _jsx(FormField, { id: ids.confirmPassword, label: labels.confirmPassword, required: true, error: errors?.confirmPassword, children: _jsx(PasswordInput, { autoComplete: "new-password", value: values.confirmPassword, onChange: (e) => setValues((v) => ({ ...v, confirmPassword: e.target.value })) }) })] }));
57
+ }
@@ -0,0 +1,2 @@
1
+ export * from './AuthForms.js';
2
+ export type { AuthFormBaseProps } from './shared.js';
@@ -0,0 +1 @@
1
+ export * from './AuthForms.js';
@@ -0,0 +1,36 @@
1
+ import { type FormEvent, type ReactNode } from 'react';
2
+ /** What every auth form takes besides its own labels. */
3
+ export interface AuthFormBaseProps<Field extends string> {
4
+ /**
5
+ * Field messages from your validation or your server, by field. After a submit, focus moves
6
+ * to the first field that has one.
7
+ */
8
+ errors?: Partial<Record<Field, string>>;
9
+ /** A message about the whole attempt ("Those details did not match"). Announced as an alert. */
10
+ formError?: string;
11
+ /** Disables the submit button and shows its spinner while a request is in flight. */
12
+ pending?: boolean;
13
+ /** Under the form: "No account? Sign up", legal links. */
14
+ footer?: ReactNode;
15
+ className?: string;
16
+ }
17
+ /**
18
+ * The ids for a form's fields, and focus on the first invalid one after each submit.
19
+ *
20
+ * Focus moves only in answer to a submit, never when `errors` changes for another reason, so a
21
+ * field validated as the person types does not pull the cursor away from where they are.
22
+ */
23
+ export declare function useAuthForm<Field extends string>(fields: readonly Field[], errors: Partial<Record<Field, string>> | undefined): {
24
+ ids: Record<Field, string>;
25
+ markSubmitted: () => void;
26
+ };
27
+ export declare function AuthFormShell({ label, onSubmit, formError, submitLabel, pending, footer, className, children, }: {
28
+ label: string;
29
+ onSubmit: (e: FormEvent<HTMLFormElement>) => void;
30
+ formError?: string;
31
+ submitLabel: string;
32
+ pending?: boolean;
33
+ footer?: ReactNode;
34
+ className?: string;
35
+ children: ReactNode;
36
+ }): import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef } from 'react';
3
+ import { Alert } from '../../atoms/Alert/Alert.js';
4
+ import { Button } from '../../atoms/Button/Button.js';
5
+ /**
6
+ * The ids for a form's fields, and focus on the first invalid one after each submit.
7
+ *
8
+ * Focus moves only in answer to a submit, never when `errors` changes for another reason, so a
9
+ * field validated as the person types does not pull the cursor away from where they are.
10
+ */
11
+ export function useAuthForm(fields, errors) {
12
+ const base = useId();
13
+ const ids = Object.fromEntries(fields.map((f) => [f, `${base}-${f}`]));
14
+ const submitted = useRef(false);
15
+ // biome-ignore lint/correctness/useExhaustiveDependencies: ids and fields are stable for the form's lifetime.
16
+ useEffect(() => {
17
+ if (!submitted.current || !errors)
18
+ return;
19
+ const first = fields.find((f) => errors[f]);
20
+ if (!first)
21
+ return;
22
+ submitted.current = false;
23
+ document.getElementById(ids[first])?.focus();
24
+ }, [errors]);
25
+ const markSubmitted = () => {
26
+ submitted.current = true;
27
+ };
28
+ return { ids, markSubmitted };
29
+ }
30
+ export function AuthFormShell({ label, onSubmit, formError, submitLabel, pending, footer, className, children, }) {
31
+ return (_jsxs("form", { noValidate: true, "aria-label": label, className: ['ui-auth-form', className].filter(Boolean).join(' '), onSubmit: (e) => {
32
+ e.preventDefault();
33
+ if (!pending)
34
+ onSubmit(e);
35
+ }, children: [formError ? _jsx(Alert, { type: "error", body: formError }) : null, children, _jsx(Button, { type: "submit", size: "touch", fullWidth: true, loading: pending, children: submitLabel }), footer ? _jsx("div", { className: "ui-auth-form__footer", children: footer }) : null] }));
36
+ }
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface BulkAction {
3
+ id: string;
4
+ label: string;
5
+ icon?: ReactNode;
6
+ /** Red, for the one that deletes or cancels. */
7
+ destructive?: boolean;
8
+ disabled?: boolean;
9
+ }
10
+ export interface BulkActionBarProps {
11
+ selectedCount: number;
12
+ actions: BulkAction[];
13
+ onAction: (id: string) => void;
14
+ onClear: () => void;
15
+ className?: string;
16
+ }
17
+ /**
18
+ * What to do with the selected rows: a floating toolbar that appears while anything is
19
+ * selected and sticks to the bottom of the content, above the TabBar on small screens.
20
+ *
21
+ * A Radix `Toolbar`, so its buttons are one Tab stop with arrow keys between them. The count
22
+ * sits in a polite live region that stays in the page while the bar is hidden, so a screen
23
+ * reader hears the selection change.
24
+ */
25
+ export declare function BulkActionBar({ selectedCount, actions, onAction, onClear, className, }: BulkActionBarProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Cross2Icon } from '@radix-ui/react-icons';
3
+ import * as Toolbar from '@radix-ui/react-toolbar';
4
+ import { Button as ThemedButton } from '@radix-ui/themes';
5
+ import { useUiStrings } from '../../strings.js';
6
+ /**
7
+ * What to do with the selected rows: a floating toolbar that appears while anything is
8
+ * selected and sticks to the bottom of the content, above the TabBar on small screens.
9
+ *
10
+ * A Radix `Toolbar`, so its buttons are one Tab stop with arrow keys between them. The count
11
+ * sits in a polite live region that stays in the page while the bar is hidden, so a screen
12
+ * reader hears the selection change.
13
+ */
14
+ export function BulkActionBar({ selectedCount, actions, onAction, onClear, className, }) {
15
+ const strings = useUiStrings();
16
+ const count = strings.selectedCount(selectedCount);
17
+ return (_jsxs(_Fragment, { children: [_jsx("span", { className: "ui-sr-only", "aria-live": "polite", children: selectedCount > 0 ? count : '' }), selectedCount > 0 ? (_jsxs(Toolbar.Root, { "aria-label": strings.bulkActions, className: ['ui-bulk-bar', className].filter(Boolean).join(' '), children: [_jsx("span", { className: "ui-bulk-bar__count", "aria-hidden": "true", children: count }), actions.map((action) => (_jsx(Toolbar.Button, { asChild: true, disabled: action.disabled, children: _jsxs(ThemedButton, { size: "2", variant: action.destructive ? 'solid' : 'soft', color: action.destructive ? 'red' : undefined, onClick: () => onAction(action.id), children: [action.icon, action.label] }) }, action.id))), _jsx(Toolbar.Button, { asChild: true, children: _jsxs(ThemedButton, { size: "2", variant: "ghost", color: "gray", onClick: onClear, children: [_jsx(Cross2Icon, { "aria-hidden": "true" }), strings.clearSelection] }) })] })) : null] }));
18
+ }
@@ -0,0 +1 @@
1
+ export * from './BulkActionBar.js';
@@ -0,0 +1 @@
1
+ export * from './BulkActionBar.js';