@milktop/react-ui 0.0.0-stage → 0.1.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 (250) hide show
  1. package/README.md +173 -2
  2. package/dist/accordion/Accordion.d.ts +65 -0
  3. package/dist/accordion/Accordion.js +61 -0
  4. package/dist/accordion/accordion.css +17 -0
  5. package/dist/action-bar/ActionBar.d.ts +33 -0
  6. package/dist/action-bar/ActionBar.js +48 -0
  7. package/dist/action-bar/action-bar.css +22 -0
  8. package/dist/alert/Alert.d.ts +28 -0
  9. package/dist/alert/Alert.js +12 -0
  10. package/dist/alert/alert.css +26 -0
  11. package/dist/app-shell/AppShell.d.ts +38 -0
  12. package/dist/app-shell/AppShell.js +92 -0
  13. package/dist/app-shell/Topbar.d.ts +20 -0
  14. package/dist/app-shell/Topbar.js +13 -0
  15. package/dist/app-shell/app-shell.css +41 -0
  16. package/dist/app-shell/shell.d.ts +9 -0
  17. package/dist/app-shell/shell.js +3 -0
  18. package/dist/attachments/Attachments.d.ts +84 -0
  19. package/dist/attachments/Attachments.js +235 -0
  20. package/dist/attachments/attachments.css +86 -0
  21. package/dist/avatar/Avatar.d.ts +31 -0
  22. package/dist/avatar/Avatar.js +41 -0
  23. package/dist/avatar/AvatarGroup.d.ts +41 -0
  24. package/dist/avatar/AvatarGroup.js +17 -0
  25. package/dist/avatar/avatar.css +73 -0
  26. package/dist/badge/Badge.d.ts +26 -0
  27. package/dist/badge/Badge.js +10 -0
  28. package/dist/badge/badge.css +24 -0
  29. package/dist/banner/Banner.d.ts +33 -0
  30. package/dist/banner/Banner.js +14 -0
  31. package/dist/banner/banner.css +25 -0
  32. package/dist/breadcrumbs/Breadcrumbs.d.ts +39 -0
  33. package/dist/breadcrumbs/Breadcrumbs.js +28 -0
  34. package/dist/breadcrumbs/breadcrumbs.css +18 -0
  35. package/dist/button/Button.d.ts +40 -0
  36. package/dist/button/Button.js +19 -0
  37. package/dist/button/button.css +63 -0
  38. package/dist/card/Card.d.ts +355 -0
  39. package/dist/card/Card.js +56 -0
  40. package/dist/card/card.css +43 -0
  41. package/dist/chart/AreaChart.d.ts +19 -0
  42. package/dist/chart/AreaChart.js +61 -0
  43. package/dist/chart/BarChart.d.ts +3 -0
  44. package/dist/chart/BarChart.js +60 -0
  45. package/dist/chart/ChartFrame.d.ts +25 -0
  46. package/dist/chart/ChartFrame.js +22 -0
  47. package/dist/chart/DonutChart.d.ts +20 -0
  48. package/dist/chart/DonutChart.js +41 -0
  49. package/dist/chart/Sparkline.d.ts +26 -0
  50. package/dist/chart/Sparkline.js +36 -0
  51. package/dist/chart/chart.css +51 -0
  52. package/dist/chart/chart.d.ts +23 -0
  53. package/dist/chart/chart.js +19 -0
  54. package/dist/checkbox/Checkbox.d.ts +50 -0
  55. package/dist/checkbox/Checkbox.js +19 -0
  56. package/dist/checkbox/CheckboxGroup.d.ts +44 -0
  57. package/dist/checkbox/CheckboxGroup.js +80 -0
  58. package/dist/checkbox/checkbox.css +49 -0
  59. package/dist/collapsible/Collapsible.d.ts +41 -0
  60. package/dist/collapsible/Collapsible.js +15 -0
  61. package/dist/collapsible/collapsible.css +20 -0
  62. package/dist/color-scheme.d.ts +18 -0
  63. package/dist/color-scheme.js +84 -0
  64. package/dist/command/Command.d.ts +64 -0
  65. package/dist/command/Command.js +162 -0
  66. package/dist/command/command.css +47 -0
  67. package/dist/confirm/confirm.css +5 -0
  68. package/dist/confirm/confirm.d.ts +42 -0
  69. package/dist/confirm/confirm.js +115 -0
  70. package/dist/copy-button/CopyButton.d.ts +30 -0
  71. package/dist/copy-button/CopyButton.js +23 -0
  72. package/dist/copy-button/copy-button.css +12 -0
  73. package/dist/css.d.ts +2 -0
  74. package/dist/data-list/DataList.d.ts +55 -0
  75. package/dist/data-list/DataList.js +43 -0
  76. package/dist/data-list/data-list.css +19 -0
  77. package/dist/date-picker/DatePicker.d.ts +95 -0
  78. package/dist/date-picker/DatePicker.js +237 -0
  79. package/dist/date-picker/date-picker.css +53 -0
  80. package/dist/dialog/Dialog.d.ts +98 -0
  81. package/dist/dialog/Dialog.js +92 -0
  82. package/dist/dialog/dialog.css +31 -0
  83. package/dist/divider/Divider.d.ts +27 -0
  84. package/dist/divider/Divider.js +8 -0
  85. package/dist/divider/divider.css +12 -0
  86. package/dist/editable/Editable.d.ts +47 -0
  87. package/dist/editable/Editable.js +51 -0
  88. package/dist/editable/editable.css +25 -0
  89. package/dist/editor/Editor.d.ts +59 -0
  90. package/dist/editor/Editor.js +220 -0
  91. package/dist/editor/editor.css +42 -0
  92. package/dist/editor/image.d.ts +1 -0
  93. package/dist/editor/image.js +23 -0
  94. package/dist/empty-state/EmptyState.d.ts +22 -0
  95. package/dist/empty-state/EmptyState.js +9 -0
  96. package/dist/empty-state/empty-state.css +14 -0
  97. package/dist/event-calendar/EventCalendar.d.ts +213 -0
  98. package/dist/event-calendar/EventCalendar.js +686 -0
  99. package/dist/event-calendar/dates.d.ts +8 -0
  100. package/dist/event-calendar/dates.js +21 -0
  101. package/dist/event-calendar/event-calendar.css +157 -0
  102. package/dist/field/Field.d.ts +66 -0
  103. package/dist/field/Field.js +23 -0
  104. package/dist/field/field.css +26 -0
  105. package/dist/file-upload/FileUpload.d.ts +60 -0
  106. package/dist/file-upload/FileUpload.js +102 -0
  107. package/dist/file-upload/file-upload.css +32 -0
  108. package/dist/format/FormatNumber.d.ts +42 -0
  109. package/dist/format/FormatNumber.js +59 -0
  110. package/dist/format/LocaleProvider.d.ts +33 -0
  111. package/dist/format/LocaleProvider.js +48 -0
  112. package/dist/format/format.d.ts +37 -0
  113. package/dist/format/format.js +74 -0
  114. package/dist/format/messages.d.ts +211 -0
  115. package/dist/format/messages.js +202 -0
  116. package/dist/highlight/Highlight.d.ts +11 -0
  117. package/dist/highlight/Highlight.js +14 -0
  118. package/dist/highlight/highlight.css +17 -0
  119. package/dist/hover-card/HoverCard.d.ts +47 -0
  120. package/dist/hover-card/HoverCard.js +18 -0
  121. package/dist/hover-card/hover-card.css +19 -0
  122. package/dist/index.d.ts +84 -0
  123. package/dist/index.js +85 -0
  124. package/dist/input/Input.d.ts +41 -0
  125. package/dist/input/Input.js +48 -0
  126. package/dist/input/input.css +25 -0
  127. package/dist/kbd/Kbd.d.ts +19 -0
  128. package/dist/kbd/Kbd.js +20 -0
  129. package/dist/kbd/kbd.css +8 -0
  130. package/dist/menu/Menu.d.ts +223 -0
  131. package/dist/menu/Menu.js +275 -0
  132. package/dist/menu/menu.css +38 -0
  133. package/dist/native-select/NativeSelect.d.ts +36 -0
  134. package/dist/native-select/NativeSelect.js +51 -0
  135. package/dist/native-select/native-select.css +16 -0
  136. package/dist/number-input/NumberInput.d.ts +56 -0
  137. package/dist/number-input/NumberInput.js +67 -0
  138. package/dist/number-input/number-input.css +21 -0
  139. package/dist/page/Page.d.ts +30 -0
  140. package/dist/page/Page.js +9 -0
  141. package/dist/page/page.css +17 -0
  142. package/dist/pagination/Pagination.d.ts +31 -0
  143. package/dist/pagination/Pagination.js +58 -0
  144. package/dist/pagination/pagination.css +15 -0
  145. package/dist/password-input/PasswordInput.d.ts +47 -0
  146. package/dist/password-input/PasswordInput.js +35 -0
  147. package/dist/password-input/password-input.css +18 -0
  148. package/dist/pin-input/PinInput.d.ts +45 -0
  149. package/dist/pin-input/PinInput.js +22 -0
  150. package/dist/pin-input/pin-input.css +13 -0
  151. package/dist/popover/Popover.d.ts +65 -0
  152. package/dist/popover/Popover.js +26 -0
  153. package/dist/popover/popover.css +16 -0
  154. package/dist/progress/Progress.d.ts +56 -0
  155. package/dist/progress/Progress.js +28 -0
  156. package/dist/progress/progress.css +24 -0
  157. package/dist/prose/Prose.d.ts +23 -0
  158. package/dist/prose/Prose.js +10 -0
  159. package/dist/prose/prose.css +29 -0
  160. package/dist/radio-group/RadioGroup.d.ts +59 -0
  161. package/dist/radio-group/RadioGroup.js +37 -0
  162. package/dist/radio-group/radio-group.css +30 -0
  163. package/dist/rolling-number/RollingNumber.d.ts +27 -0
  164. package/dist/rolling-number/RollingNumber.js +18 -0
  165. package/dist/rolling-number/rolling-number.css +10 -0
  166. package/dist/segment-group/Segmented.d.ts +68 -0
  167. package/dist/segment-group/Segmented.js +79 -0
  168. package/dist/segment-group/segment-group.css +25 -0
  169. package/dist/select/ListSelect.d.ts +5 -0
  170. package/dist/select/ListSelect.js +82 -0
  171. package/dist/select/SearchSelect.d.ts +43 -0
  172. package/dist/select/SearchSelect.js +219 -0
  173. package/dist/select/Select.d.ts +147 -0
  174. package/dist/select/Select.js +32 -0
  175. package/dist/select/items.d.ts +17 -0
  176. package/dist/select/items.js +14 -0
  177. package/dist/select/search-select.css +45 -0
  178. package/dist/select/select.css +52 -0
  179. package/dist/sheet/Sheet.d.ts +88 -0
  180. package/dist/sheet/Sheet.js +66 -0
  181. package/dist/sheet/sheet.css +44 -0
  182. package/dist/sidebar/NavGroup.d.ts +29 -0
  183. package/dist/sidebar/NavGroup.js +78 -0
  184. package/dist/sidebar/NavItem.d.ts +33 -0
  185. package/dist/sidebar/NavItem.js +50 -0
  186. package/dist/sidebar/NavSection.d.ts +20 -0
  187. package/dist/sidebar/NavSection.js +44 -0
  188. package/dist/sidebar/Sidebar.d.ts +65 -0
  189. package/dist/sidebar/Sidebar.js +59 -0
  190. package/dist/sidebar/SidebarUser.d.ts +31 -0
  191. package/dist/sidebar/SidebarUser.js +14 -0
  192. package/dist/sidebar/context.d.ts +13 -0
  193. package/dist/sidebar/context.js +6 -0
  194. package/dist/sidebar/sidebar.css +109 -0
  195. package/dist/skeleton/Skeleton.d.ts +25 -0
  196. package/dist/skeleton/Skeleton.js +8 -0
  197. package/dist/skeleton/skeleton.css +13 -0
  198. package/dist/slider/Slider.d.ts +70 -0
  199. package/dist/slider/Slider.js +65 -0
  200. package/dist/slider/slider.css +17 -0
  201. package/dist/sortable-list/SortableList.d.ts +46 -0
  202. package/dist/sortable-list/SortableList.js +85 -0
  203. package/dist/sortable-list/sortable-list.css +25 -0
  204. package/dist/spinner/Spinner.d.ts +18 -0
  205. package/dist/spinner/Spinner.js +11 -0
  206. package/dist/spinner/spinner.css +9 -0
  207. package/dist/stat/Stat.d.ts +42 -0
  208. package/dist/stat/Stat.js +21 -0
  209. package/dist/stat/Stats.d.ts +23 -0
  210. package/dist/stat/Stats.js +8 -0
  211. package/dist/stat/stat.css +34 -0
  212. package/dist/swatch-picker/SwatchPicker.d.ts +45 -0
  213. package/dist/swatch-picker/SwatchPicker.js +54 -0
  214. package/dist/swatch-picker/swatch-picker.css +38 -0
  215. package/dist/switch/Switch.d.ts +39 -0
  216. package/dist/switch/Switch.js +17 -0
  217. package/dist/switch/switch.css +13 -0
  218. package/dist/table/Table.d.ts +90 -0
  219. package/dist/table/Table.js +145 -0
  220. package/dist/table/table.css +44 -0
  221. package/dist/tabs/Tabs.d.ts +87 -0
  222. package/dist/tabs/Tabs.js +90 -0
  223. package/dist/tabs/tabs.css +53 -0
  224. package/dist/tags-input/TagsInput.d.ts +69 -0
  225. package/dist/tags-input/TagsInput.js +95 -0
  226. package/dist/tags-input/tags-input.css +37 -0
  227. package/dist/textarea/Textarea.d.ts +24 -0
  228. package/dist/textarea/Textarea.js +51 -0
  229. package/dist/textarea/textarea.css +14 -0
  230. package/dist/theme-toggle/ThemeToggle.d.ts +49 -0
  231. package/dist/theme-toggle/ThemeToggle.js +71 -0
  232. package/dist/theme-toggle/theme-toggle.css +23 -0
  233. package/dist/theme.css +119 -0
  234. package/dist/time-input/TimeInput.d.ts +25 -0
  235. package/dist/time-input/TimeInput.js +20 -0
  236. package/dist/timeline/Timeline.d.ts +50 -0
  237. package/dist/timeline/Timeline.js +15 -0
  238. package/dist/timeline/timeline.css +29 -0
  239. package/dist/toast/Toaster.d.ts +36 -0
  240. package/dist/toast/Toaster.js +25 -0
  241. package/dist/toast/toast.css +33 -0
  242. package/dist/tooltip/Tooltip.d.ts +55 -0
  243. package/dist/tooltip/Tooltip.js +48 -0
  244. package/dist/tooltip/tooltip.css +18 -0
  245. package/dist/tree-view/TreeView.d.ts +86 -0
  246. package/dist/tree-view/TreeView.js +74 -0
  247. package/dist/tree-view/tree-view.css +29 -0
  248. package/dist/utils.d.ts +19 -0
  249. package/dist/utils.js +59 -0
  250. package/package.json +99 -4
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import 'iconify-icon';
3
+ import { formatNumber } from '../format/format.js';
4
+ import { useLocale, useMessages } from '../format/LocaleProvider.js';
5
+ import { RollingNumber } from '../rolling-number/RollingNumber.js';
6
+ import { cx, partsOf } from '../utils.js';
7
+ import '../theme.css';
8
+ import './stat.css';
9
+ export function Stat({ label = null, value = null, format, animate = false, unit = null, change = null, changeUnit = '%', changeLabel = null, invert = false, help = null, icon = null, variant = 'plain', size = 'md', className, classes, children, ...rest }) {
10
+ const t = useMessages();
11
+ const part = partsOf('stat', classes);
12
+ const direction = change == null || change === 0 ? 'flat' : change > 0 ? 'up' : 'down';
13
+ const good = direction === 'flat' ? null : (direction === 'up') !== invert;
14
+ const locale = useLocale();
15
+ const changeText = `${change != null && change > 0 ? '+' : ''}${change == null ? '' : formatNumber(change, { locale, maximumFractionDigits: 2 })}${changeUnit}`;
16
+ // A number: formatted, or rolling.
17
+ const shown = typeof value === 'number'
18
+ ? animate ? _jsx(RollingNumber, { value: value, format: format, locale: locale }) : formatNumber(value, { ...format, locale })
19
+ : value;
20
+ return (_jsxs("div", { ...rest, ...part('root'), className: cx(className, classes?.root), "data-variant": variant, "data-size": size, children: [_jsxs("div", { ...part('header'), children: [_jsx("div", { ...part('label'), children: label }), icon && _jsx("span", { ...part('icon'), "aria-hidden": "true", children: _jsx("iconify-icon", { icon: icon }) })] }), _jsxs("div", { ...part('value'), children: [_jsx("span", { children: shown }), unit && _jsx("span", { ...part('unit'), children: unit })] }), (change != null || changeLabel) && (_jsxs("div", { ...part('footer'), children: [change != null && (_jsxs("span", { ...part('change'), "data-good": good === true || undefined, "data-bad": good === false || undefined, "aria-label": `${direction === 'up' ? t.stat.up : direction === 'down' ? t.stat.down : t.stat.same} ${changeText}`, children: [direction !== 'flat' && _jsx("iconify-icon", { icon: direction === 'down' ? 'lucide:trending-down' : 'lucide:trending-up' }), changeText] })), changeLabel && _jsx("span", { ...part('change-label'), children: changeLabel })] })), help && _jsx("div", { ...part('help'), children: help }), children] }));
21
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A responsive grid of stats: one column, then two, then `columns`, as its
3
+ * own width (not the screen's) allows.
4
+ *
5
+ * <Stats variant="cards">
6
+ * <Stat label="Students" value={24} />
7
+ * <Stat label="Lessons" value={52} />
8
+ * </Stats>
9
+ *
10
+ * - `columns`: the most per row (4)
11
+ * - `variant`: 'plain' (default), 'cards' (each stat a card) or 'divided'
12
+ * (one card split into cells)
13
+ */
14
+ import type { HTMLAttributes } from 'react';
15
+ import { type Classes } from '../utils.js';
16
+ import '../theme.css';
17
+ import './stat.css';
18
+ export type StatsProps = HTMLAttributes<HTMLDivElement> & {
19
+ columns?: number;
20
+ variant?: 'plain' | 'cards' | 'divided';
21
+ classes?: Classes<'root' | 'grid'>;
22
+ };
23
+ export declare function Stats({ columns, variant, className, classes, style, children, ...rest }: StatsProps): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx, partsOf } from '../utils.js';
3
+ import '../theme.css';
4
+ import './stat.css';
5
+ export function Stats({ columns = 4, variant = 'plain', className, classes, style, children, ...rest }) {
6
+ const part = partsOf('stats', classes);
7
+ return (_jsx("div", { ...rest, ...part('root'), className: cx(className, classes?.root), "data-variant": variant, style: { ...style, '--columns': columns }, children: _jsx("div", { ...part('grid'), children: children }) }));
8
+ }
@@ -0,0 +1,34 @@
1
+ @layer ui {
2
+ [data-scope=stat][data-part=root] {
3
+ display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; color: var(--ui-text); font-family: var(--ui-font); font-size: 14px;
4
+ &[data-variant=card] { padding: 1.25rem; border: 1px solid var(--ui-border); border-radius: calc(var(--ui-radius) + 4px);
5
+ background: var(--ui-surface); box-shadow: var(--ui-card-shadow); }
6
+ }
7
+ [data-scope=stat][data-part=header] { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; min-height: 1.5rem; }
8
+ [data-scope=stat][data-part=label] { color: var(--ui-muted); font-weight: 500; min-width: 0; overflow-wrap: anywhere; }
9
+ /* Small enough to sit in the label's row without making it taller. */
10
+ [data-scope=stat][data-part=icon] { display: grid; place-items: center; width: 2rem; height: 2rem; margin: -0.25rem 0; flex-shrink: 0;
11
+ border-radius: var(--ui-radius); background: var(--ui-accent-soft); color: var(--ui-accent-soft-text); font-size: 16px; }
12
+ [data-scope=stat][data-part=value] { display: flex; align-items: baseline; gap: 0.25rem; font-size: 24px; font-weight: 700; line-height: 1.2; letter-spacing: -0.01em;
13
+ [data-size=sm] > & { font-size: 20px; }
14
+ [data-size=lg] > & { font-size: 30px; } }
15
+ [data-scope=stat][data-part=unit] { font-size: 14px; font-weight: 500; line-height: 1; color: var(--ui-muted); letter-spacing: 0; }
16
+ [data-scope=stat][data-part=footer] { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; font-size: 12px; margin-top: 0.125rem; }
17
+ [data-scope=stat][data-part=change] { display: inline-flex; align-items: center; gap: 0.25rem; font-weight: 600; color: var(--ui-muted);
18
+ &[data-good] { color: var(--ui-success); }
19
+ &[data-bad] { color: var(--ui-danger); } }
20
+ [data-scope=stat][data-part=change-label], [data-scope=stat][data-part=help] { color: var(--ui-muted); }
21
+ [data-scope=stat][data-part=help] { font-size: 12px; }
22
+
23
+ /* Stats respond to their own width: one column, then two, then --columns. */
24
+ [data-scope=stats][data-part=root] { display: block; width: 100%; container-type: inline-size; }
25
+ [data-scope=stats][data-part=grid] { display: grid; gap: 1rem; grid-template-columns: minmax(0, 1fr);
26
+ [data-variant=cards] > & > [data-scope=stat] { padding: 1.25rem; border: 1px solid var(--ui-border); border-radius: calc(var(--ui-radius) + 4px);
27
+ background: var(--ui-surface); box-shadow: var(--ui-card-shadow); }
28
+ /* One card split into cells by 1px gaps over the border colour. */
29
+ [data-variant=divided] > & { gap: 1px; background: var(--ui-border); border: 1px solid var(--ui-border);
30
+ border-radius: calc(var(--ui-radius) + 4px); box-shadow: var(--ui-card-shadow); overflow: hidden; }
31
+ [data-variant=divided] > & > [data-scope=stat] { padding: 1.25rem; background: var(--ui-surface); } }
32
+ @container (min-width: 26rem) { [data-scope=stats][data-part=grid] { grid-template-columns: repeat(min(2, var(--columns)), minmax(0, 1fr)); } }
33
+ @container (min-width: 46rem) { [data-scope=stats][data-part=grid] { grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); } }
34
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A choice of colour from a few swatches, e.g. a vet's colour on the
3
+ * calendar. The swatches are Tailwind's colours by default; their values are
4
+ * the names ('teal'), or give your own.
5
+ *
6
+ * <SwatchPicker label="Colour" value={color} onValueChange={setColor} />
7
+ * <SwatchPicker items={[{ value: 'brand', color: '#e11d48', label: 'Brand' }]} … />
8
+ *
9
+ * Arrow keys move between them, like radio buttons (which they are).
10
+ *
11
+ * - `items`: colour names from `swatches`, or `{ value, color, label }`
12
+ * (the default: every colour in `swatches`)
13
+ * - `value`, `defaultValue`; `onValueChange`: called with the chosen value
14
+ * - `label`: a label above it (skipped inside a Field); `size`: 'sm', 'md' or 'lg'
15
+ * - `variant`: 'inline' (default: the swatches in the page) or 'popover': a
16
+ * field showing the chosen colour and its name, opening the swatches
17
+ * (closed once one's chosen); `placeholder` shows while none is
18
+ *
19
+ * `swatches` is the palette by name (Tailwind's 500s), e.g. for the
20
+ * CSS colour of a chosen name: `swatches.teal`.
21
+ */
22
+ import { type ReactNode } from 'react';
23
+ import { RadioGroup as Ark } from '@ark-ui/react/radio-group';
24
+ import 'iconify-icon';
25
+ import { type Classes } from '../utils.js';
26
+ import '../theme.css';
27
+ import './swatch-picker.css';
28
+ export declare const swatches: Record<string, string>;
29
+ export type Swatch = {
30
+ value: string;
31
+ color: string;
32
+ label?: string;
33
+ };
34
+ export type SwatchPickerProps = Omit<Ark.RootProps, 'value' | 'defaultValue' | 'onValueChange' | 'label'> & {
35
+ items?: (string | Swatch)[];
36
+ value?: string | null;
37
+ defaultValue?: string | null;
38
+ onValueChange?: (value: string | null) => void;
39
+ label?: ReactNode;
40
+ size?: 'sm' | 'md' | 'lg';
41
+ variant?: 'inline' | 'popover';
42
+ placeholder?: string;
43
+ classes?: Classes<'root' | 'label' | 'items' | 'item' | 'swatch' | 'trigger' | 'value' | 'indicator' | 'field' | 'chip'>;
44
+ };
45
+ export declare function SwatchPicker({ items, value, defaultValue, onValueChange, label, size, variant, placeholder: placeholderProp, className, classes, ...rest }: SwatchPickerProps): import("react").JSX.Element;
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A choice of colour from a few swatches, e.g. a vet's colour on the
4
+ * calendar. The swatches are Tailwind's colours by default; their values are
5
+ * the names ('teal'), or give your own.
6
+ *
7
+ * <SwatchPicker label="Colour" value={color} onValueChange={setColor} />
8
+ * <SwatchPicker items={[{ value: 'brand', color: '#e11d48', label: 'Brand' }]} … />
9
+ *
10
+ * Arrow keys move between them, like radio buttons (which they are).
11
+ *
12
+ * - `items`: colour names from `swatches`, or `{ value, color, label }`
13
+ * (the default: every colour in `swatches`)
14
+ * - `value`, `defaultValue`; `onValueChange`: called with the chosen value
15
+ * - `label`: a label above it (skipped inside a Field); `size`: 'sm', 'md' or 'lg'
16
+ * - `variant`: 'inline' (default: the swatches in the page) or 'popover': a
17
+ * field showing the chosen colour and its name, opening the swatches
18
+ * (closed once one's chosen); `placeholder` shows while none is
19
+ *
20
+ * `swatches` is the palette by name (Tailwind's 500s), e.g. for the
21
+ * CSS colour of a chosen name: `swatches.teal`.
22
+ */
23
+ import { useId, useState } from 'react';
24
+ import { Popover } from '../popover/Popover.js';
25
+ import { RadioGroup as Ark } from '@ark-ui/react/radio-group';
26
+ import { useFieldContext } from '@ark-ui/react/field';
27
+ import 'iconify-icon';
28
+ import { cx, partsOf, useControllable } from '../utils.js';
29
+ import { useMessages } from '../format/LocaleProvider.js';
30
+ import '../theme.css';
31
+ import './swatch-picker.css';
32
+ export const swatches = {
33
+ slate: '#64748b', red: '#ef4444', orange: '#f97316', amber: '#f59e0b', yellow: '#eab308', lime: '#84cc16', green: '#22c55e',
34
+ emerald: '#10b981', teal: '#14b8a6', cyan: '#06b6d4', sky: '#0ea5e9', blue: '#3b82f6', indigo: '#6366f1', violet: '#8b5cf6',
35
+ purple: '#a855f7', fuchsia: '#d946ef', pink: '#ec4899', rose: '#f43f5e',
36
+ };
37
+ const capital = (name) => name.charAt(0).toUpperCase() + name.slice(1);
38
+ export function SwatchPicker({ items = Object.keys(swatches), value, defaultValue = null, onValueChange, label = null, size = 'md', variant = 'inline', placeholder: placeholderProp, className, classes, ...rest }) {
39
+ const t = useMessages();
40
+ const placeholder = placeholderProp ?? t.swatchPicker.placeholder;
41
+ const id = useId();
42
+ const [open, setOpen] = useState(false);
43
+ const field = useFieldContext();
44
+ const part = partsOf('swatch-picker', classes);
45
+ const [current, setCurrent] = useControllable(value, defaultValue, onValueChange);
46
+ const list = items.map((item) => (typeof item === 'string' ? { value: item, color: swatches[item] ?? item, label: t.swatchPicker.colors[item] ?? capital(item) } : item));
47
+ const chosen = list.find((swatch) => swatch.value === current);
48
+ const popover = variant === 'popover';
49
+ const grid = (_jsxs(Ark.Root, { ...(!popover && rest), ...part('root'), className: popover ? classes?.root : cx(className, classes?.root), orientation: "horizontal", "data-size": size, "data-variant": variant, value: current, onValueChange: (d) => { setCurrent(d.value); setOpen(false); }, "aria-describedby": field?.ariaDescribedby, ...(popover && { 'aria-labelledby': field ? field.ids.label : label ? `${id}-label` : undefined }), ...(field && { ids: { label: field.ids.label }, invalid: field.invalid, disabled: field.disabled }), children: [label && !field && !popover && _jsx(Ark.Label, { ...part('label'), children: label }), _jsx("div", { ...part('items'), children: list.map((swatch) => (_jsxs(Ark.Item, { value: swatch.value, ...part('item'), title: swatch.label ?? swatch.value, style: { '--swatch': swatch.color }, children: [_jsx(Ark.ItemControl, { ...part('swatch'), children: _jsx("iconify-icon", { icon: "lucide:check", "aria-hidden": "true" }) }), _jsx(Ark.ItemText, { ...part('item-text'), children: swatch.label ?? swatch.value }), _jsx(Ark.ItemHiddenInput, {})] }, swatch.value))) })] }));
50
+ if (!popover)
51
+ return grid;
52
+ // A field (like an input) showing the colour, opening the swatches.
53
+ return (_jsxs("div", { ...rest, ...part('field'), className: cx(className, classes?.field), "data-size": size, children: [label && !field && _jsx("span", { ...part('label'), id: `${id}-label`, children: label }), _jsx(Popover, { placement: "bottom-start", open: open, onOpenChange: setOpen, "data-swatches": "", trigger: _jsxs("button", { type: "button", ...part('trigger'), "aria-haspopup": "dialog", "aria-labelledby": field || label ? `${field ? field.ids.label : `${id}-label`} ${id}-value` : undefined, "aria-invalid": field?.invalid || undefined, disabled: field?.disabled, children: [_jsx("span", { ...part('chip'), style: { '--swatch': chosen?.color ?? 'transparent' }, "data-empty": !chosen || undefined }), _jsx("span", { ...part('value'), id: `${id}-value`, "data-placeholder": !chosen || undefined, children: chosen ? chosen.label ?? chosen.value : placeholder }), _jsx("iconify-icon", { ...part('indicator'), icon: "lucide:chevron-down", "aria-hidden": "true" })] }), children: grid })] }));
54
+ }
@@ -0,0 +1,38 @@
1
+ @layer ui {
2
+ [data-scope=swatch-picker][data-part=root] { display: block; color: var(--ui-text); font-family: var(--ui-font); --size: 1.75rem;
3
+ &[data-size=sm] { --size: 1.375rem; }
4
+ &[data-size=lg] { --size: 2.25rem; } }
5
+ [data-scope=swatch-picker][data-part=label] { display: block; margin-bottom: 0.375rem; font-size: 14px; font-weight: 500; }
6
+ [data-scope=swatch-picker][data-part=items] { display: flex; flex-wrap: wrap; gap: 0.375rem; }
7
+ [data-scope=swatch-picker][data-part=item] { display: inline-flex; cursor: pointer;
8
+ &[data-disabled] { opacity: 0.4; cursor: not-allowed; } }
9
+ /* A round swatch; chosen: a ring round it and a tick in it. */
10
+ [data-scope=swatch-picker][data-part=swatch] { display: grid; place-items: center; width: var(--size); height: var(--size); box-sizing: border-box;
11
+ border-radius: 9999px; background: var(--swatch); color: white; font-size: calc(var(--size) * 0.5);
12
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, black 12%, transparent); transition: transform 100ms;
13
+ & iconify-icon { opacity: 0; }
14
+ [data-part=item]:hover > & { transform: scale(1.08); }
15
+ &[data-state=checked] { box-shadow: 0 0 0 2px var(--ui-surface), 0 0 0 4px var(--swatch);
16
+ & iconify-icon { opacity: 1; } }
17
+ &[data-focus-visible] { outline: 2px solid var(--ui-ring); outline-offset: 3px; } }
18
+ /* The colour's name, for assistive tech (and the title on hover). */
19
+ [data-scope=swatch-picker][data-part=item-text] { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
20
+ /* The popover variant: a field showing the colour, like our inputs. */
21
+ [data-scope=swatch-picker][data-part=field] { display: block; min-width: 0; color: var(--ui-text); font-family: var(--ui-font); }
22
+ [data-scope=swatch-picker][data-part=trigger] { display: flex; align-items: center; gap: 0.5rem; width: 100%; min-width: 0; box-sizing: border-box;
23
+ height: var(--ui-control-height); padding: 0 0.625rem; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);
24
+ background: var(--ui-surface); color: var(--ui-text); font: inherit; font-size: 14px; text-align: left; cursor: pointer;
25
+ &:focus-visible, &[aria-expanded=true] { border-color: var(--ui-ring); outline: 2px solid var(--ui-ring-soft); }
26
+ &[aria-invalid=true] { border-color: var(--ui-danger); }
27
+ &:disabled { opacity: 0.5; cursor: not-allowed; }
28
+ [data-size=sm] > & { height: var(--ui-control-height-sm); font-size: 13px; } }
29
+ [data-scope=swatch-picker][data-part=chip] { flex-shrink: 0; width: 1.125rem; height: 1.125rem; border-radius: 9999px; background: var(--swatch);
30
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, black 12%, transparent);
31
+ &[data-empty] { background: repeating-linear-gradient(-45deg, var(--ui-border) 0 2px, transparent 2px 5px); } }
32
+ [data-scope=swatch-picker][data-part=value] { flex-grow: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
33
+ &[data-placeholder] { color: var(--ui-muted); } }
34
+ [data-scope=swatch-picker][data-part=indicator] { flex-shrink: 0; color: var(--ui-muted); font-size: 16px; }
35
+ /* In the popover: as wide as its swatches, a few to a row, room around the rings. */
36
+ [data-scope=popover][data-part=content][data-swatches] { width: auto; padding: 0.75rem; }
37
+ [data-scope=swatch-picker][data-part=root][data-variant=popover] > [data-part=items] { max-width: 16rem; padding: 0.25rem; }
38
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * An on/off switch with its label.
3
+ *
4
+ * <Switch checked={reminders} onCheckedChange={setReminders}>Email reminders</Switch>
5
+ *
6
+ * - `checked`, `defaultChecked`
7
+ * - `onCheckedChange`: called with the new checked state
8
+ * - `label`: its text (or children)
9
+ * - `name`, `value`: for plain form posts
10
+ * - `disabled`, `invalid`, `required`
11
+ *
12
+ * By hand, with Ark's parts:
13
+ *
14
+ * <Switch.Root>
15
+ * <Switch.Control><Switch.Thumb /></Switch.Control>
16
+ * <Switch.Label>Reminders</Switch.Label>
17
+ * <Switch.HiddenInput />
18
+ * </Switch.Root>
19
+ */
20
+ import type { ReactNode } from 'react';
21
+ import { Switch as Ark, type SwitchCheckedChangeDetails } from '@ark-ui/react/switch';
22
+ import { type Classes } from '../utils.js';
23
+ import '../theme.css';
24
+ import './switch.css';
25
+ export type SwitchProps = Omit<Ark.RootProps, 'onCheckedChange' | 'label'> & {
26
+ onCheckedChange?: (checked: boolean, details: SwitchCheckedChangeDetails) => void;
27
+ label?: ReactNode;
28
+ classes?: Classes<'root' | 'control' | 'thumb' | 'label'>;
29
+ };
30
+ declare function SwitchShortcut({ onCheckedChange, label, className, classes, children, ...rest }: SwitchProps): import("react").JSX.Element;
31
+ export declare const Switch: typeof SwitchShortcut & {
32
+ Root: import("react").ForwardRefExoticComponent<Ark.RootProps & import("react").RefAttributes<HTMLLabelElement>>;
33
+ Control: import("react").ForwardRefExoticComponent<Ark.ControlProps & import("react").RefAttributes<HTMLSpanElement>>;
34
+ Thumb: import("react").ForwardRefExoticComponent<Ark.ThumbProps & import("react").RefAttributes<HTMLSpanElement>>;
35
+ Label: import("react").ForwardRefExoticComponent<Ark.LabelProps & import("react").RefAttributes<HTMLSpanElement>>;
36
+ HiddenInput: import("react").ForwardRefExoticComponent<Ark.HiddenInputProps & import("react").RefAttributes<HTMLInputElement>>;
37
+ Context: (props: Ark.ContextProps) => ReactNode;
38
+ };
39
+ export {};
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Switch as Ark } from '@ark-ui/react/switch';
3
+ import { cx, on, partsOf, withParts } from '../utils.js';
4
+ import '../theme.css';
5
+ import './switch.css';
6
+ function SwitchShortcut({ onCheckedChange, label = null, className, classes, children, ...rest }) {
7
+ const part = partsOf('switch', classes);
8
+ return (_jsxs(Ark.Root, { ...rest, ...part('root'), className: cx(className, classes?.root), ...on('onCheckedChange', onCheckedChange, (details) => details.checked), children: [_jsx(Ark.Control, { ...part('control'), children: _jsx(Ark.Thumb, { ...part('thumb') }) }), (label || children) && _jsx(Ark.Label, { ...part('label'), children: children ?? label }), _jsx(Ark.HiddenInput, {})] }));
9
+ }
10
+ export const Switch = withParts(SwitchShortcut, {
11
+ Root: Ark.Root,
12
+ Control: Ark.Control,
13
+ Thumb: Ark.Thumb,
14
+ Label: Ark.Label,
15
+ HiddenInput: Ark.HiddenInput,
16
+ Context: Ark.Context,
17
+ });
@@ -0,0 +1,13 @@
1
+ @layer ui {
2
+ [data-scope=switch][data-part=root] { display: inline-flex; align-items: center; gap: 0.5rem; vertical-align: top; cursor: pointer;
3
+ color: var(--ui-text); font-family: var(--ui-font); font-size: 14px;
4
+ &[data-disabled] { opacity: 0.5; cursor: not-allowed; } }
5
+ [data-scope=switch][data-part=control] { display: inline-flex; align-items: center; flex-shrink: 0; width: 2.25rem; height: 1.25rem;
6
+ padding: 0.125rem; box-sizing: border-box; background: var(--ui-border); border-radius: 9999px; transition: background-color 150ms;
7
+ &[data-state=checked] { background: var(--ui-accent); }
8
+ &[data-focus-visible] { outline: 2px solid var(--ui-ring-soft); outline-offset: 1px; }
9
+ &[data-invalid] { outline: 1px solid var(--ui-danger); } }
10
+ [data-scope=switch][data-part=thumb] { width: 1rem; height: 1rem; background: white; border-radius: 9999px;
11
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); transition: translate 150ms;
12
+ &[data-state=checked] { translate: 1rem 0; } }
13
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * A data table: rows of objects, laid out by `columns`.
3
+ *
4
+ * <Table columns={columns} rows={students} selectable selected={picked} onSelectedChange={setPicked} />
5
+ *
6
+ * const columns: TableColumn<Student>[] = [
7
+ * { key: 'name', label: 'Name', sortable: true, cell: ({ row }) => <strong>{row.name}</strong> },
8
+ * { key: 'year', label: 'Year', align: 'end', sortable: true },
9
+ * { key: 'fee', label: 'Fee', format: (v) => `£${v}` },
10
+ * ]
11
+ *
12
+ * - `rows`: plain objects, one per row
13
+ * - `columns`: what each column shows. `key`: the row property shown (and
14
+ * sorted by); `label`: the heading; `sortable`; `align`: 'start', 'center'
15
+ * or 'end'; `width`: any CSS width; `format(value, row)`: turns the value
16
+ * into text; `value(row)`: computes it instead of `row[key]`; `cell`:
17
+ * renders the cell, given `{ row, column, value }`
18
+ * - `rowKey`: the property that identifies a row ('id')
19
+ * - `sort`, `defaultSort`: `{ key, dir }` or null; `onSortChange`. Sortable
20
+ * columns sort when their heading is clicked: ascending, descending, then
21
+ * off. With `manualSort` rows aren't sorted here: sort them yourself
22
+ * (`sortRows(list, columns, sort)` before paging, or on a server)
23
+ * - `selectable`: a checkbox per row and a select-all; `selected`,
24
+ * `defaultSelected`: the selected rows' keys; `onSelectedChange`. Shift-click
25
+ * a row's checkbox to tick (or clear) the rows between it and the last one
26
+ * clicked
27
+ * - `loading`: skeleton rows while there are none yet (`loadingRows`, 5),
28
+ * else dims the rows
29
+ * - `emptyText`, or `empty` (markup), for no rows
30
+ * - `maxHeight`: scrolls the rows under a sticky header
31
+ * - `label`: a caption for assistive tech
32
+ * - `size`: 'sm', 'md' (default) or 'lg' (roomier rows)
33
+ * - `flush`: no border or rounding, to sit edge to edge in a card
34
+ * - `onRowClick`: called with the row when a row is clicked (outside its controls)
35
+ *
36
+ * Or write the <table> yourself as its content, for the look alone (no
37
+ * sorting, selection or loading states); `data-align` on cells works too.
38
+ */
39
+ import { type HTMLAttributes, type ReactNode } from 'react';
40
+ import 'iconify-icon';
41
+ import { type Classes } from '../utils.js';
42
+ import '../theme.css';
43
+ import './table.css';
44
+ type Row = Record<string, unknown>;
45
+ type Key = string | number;
46
+ export type TableColumn<T extends object = Row> = {
47
+ key: string;
48
+ label?: ReactNode;
49
+ sortable?: boolean;
50
+ align?: 'start' | 'center' | 'end';
51
+ width?: string;
52
+ format?: (value: unknown, row: T) => ReactNode;
53
+ value?: (row: T) => unknown;
54
+ cell?: (cell: {
55
+ row: T;
56
+ column: TableColumn<T>;
57
+ value: unknown;
58
+ }) => ReactNode;
59
+ };
60
+ export type TableSort = {
61
+ key: string;
62
+ dir: 'asc' | 'desc';
63
+ } | null;
64
+ export type TableProps<T extends object = Row> = Omit<HTMLAttributes<HTMLDivElement>, 'onSelect'> & {
65
+ columns?: TableColumn<T>[];
66
+ rows?: T[];
67
+ rowKey?: string;
68
+ selectable?: boolean;
69
+ selected?: Key[];
70
+ defaultSelected?: Key[];
71
+ onSelectedChange?: (keys: Key[]) => void;
72
+ sort?: TableSort;
73
+ defaultSort?: TableSort;
74
+ onSortChange?: (sort: TableSort) => void;
75
+ manualSort?: boolean;
76
+ loading?: boolean;
77
+ loadingRows?: number;
78
+ emptyText?: ReactNode;
79
+ empty?: ReactNode;
80
+ maxHeight?: string | null;
81
+ label?: string | null;
82
+ size?: 'sm' | 'md' | 'lg';
83
+ flush?: boolean;
84
+ onRowClick?: (row: T) => void;
85
+ classes?: Classes<'root' | 'scroll' | 'table' | 'caption' | 'sort' | 'sortIcon' | 'row' | 'selectCell' | 'empty'>;
86
+ };
87
+ export declare const cellValue: <T extends object>(row: T, column: TableColumn<T>) => unknown;
88
+ export declare function sortRows<T extends object>(list: T[] | null | undefined, columns: TableColumn<T>[], sort: TableSort): T[];
89
+ export declare function Table<T extends object = Row>({ columns, rows, rowKey, selectable, selected: selectedProp, defaultSelected, onSelectedChange, sort: sortProp, defaultSort, onSortChange, manualSort, loading, loadingRows, emptyText: emptyTextProp, empty, maxHeight, label, size, flush, onRowClick, className, classes, children, ...rest }: TableProps<T>): import("react").JSX.Element;
90
+ export {};
@@ -0,0 +1,145 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * A data table: rows of objects, laid out by `columns`.
4
+ *
5
+ * <Table columns={columns} rows={students} selectable selected={picked} onSelectedChange={setPicked} />
6
+ *
7
+ * const columns: TableColumn<Student>[] = [
8
+ * { key: 'name', label: 'Name', sortable: true, cell: ({ row }) => <strong>{row.name}</strong> },
9
+ * { key: 'year', label: 'Year', align: 'end', sortable: true },
10
+ * { key: 'fee', label: 'Fee', format: (v) => `£${v}` },
11
+ * ]
12
+ *
13
+ * - `rows`: plain objects, one per row
14
+ * - `columns`: what each column shows. `key`: the row property shown (and
15
+ * sorted by); `label`: the heading; `sortable`; `align`: 'start', 'center'
16
+ * or 'end'; `width`: any CSS width; `format(value, row)`: turns the value
17
+ * into text; `value(row)`: computes it instead of `row[key]`; `cell`:
18
+ * renders the cell, given `{ row, column, value }`
19
+ * - `rowKey`: the property that identifies a row ('id')
20
+ * - `sort`, `defaultSort`: `{ key, dir }` or null; `onSortChange`. Sortable
21
+ * columns sort when their heading is clicked: ascending, descending, then
22
+ * off. With `manualSort` rows aren't sorted here: sort them yourself
23
+ * (`sortRows(list, columns, sort)` before paging, or on a server)
24
+ * - `selectable`: a checkbox per row and a select-all; `selected`,
25
+ * `defaultSelected`: the selected rows' keys; `onSelectedChange`. Shift-click
26
+ * a row's checkbox to tick (or clear) the rows between it and the last one
27
+ * clicked
28
+ * - `loading`: skeleton rows while there are none yet (`loadingRows`, 5),
29
+ * else dims the rows
30
+ * - `emptyText`, or `empty` (markup), for no rows
31
+ * - `maxHeight`: scrolls the rows under a sticky header
32
+ * - `label`: a caption for assistive tech
33
+ * - `size`: 'sm', 'md' (default) or 'lg' (roomier rows)
34
+ * - `flush`: no border or rounding, to sit edge to edge in a card
35
+ * - `onRowClick`: called with the row when a row is clicked (outside its controls)
36
+ *
37
+ * Or write the <table> yourself as its content, for the look alone (no
38
+ * sorting, selection or loading states); `data-align` on cells works too.
39
+ */
40
+ import { useEffect, useRef } from 'react';
41
+ import 'iconify-icon';
42
+ import { Checkbox } from '../checkbox/Checkbox.js';
43
+ import { Skeleton } from '../skeleton/Skeleton.js';
44
+ import { cx, partsOf, useControllable } from '../utils.js';
45
+ import { useMessages } from '../format/LocaleProvider.js';
46
+ import '../theme.css';
47
+ import './table.css';
48
+ // Numbers by value, everything else as text with numbers in order
49
+ // ("Year 2" before "Year 10"); empty values last.
50
+ function compare(a, b) {
51
+ if (a == b)
52
+ return 0;
53
+ if (a == null || a === '')
54
+ return 1;
55
+ if (b == null || b === '')
56
+ return -1;
57
+ if (typeof a === 'number' && typeof b === 'number')
58
+ return a - b;
59
+ return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' });
60
+ }
61
+ export const cellValue = (row, column) => column.value ? column.value(row) : row[column.key];
62
+ // A copy of `list` in `sort`'s order. With paging, sort the whole list with
63
+ // this (and set `manualSort`), then hand the table one page.
64
+ export function sortRows(list, columns, sort) {
65
+ const column = sort && columns.find((c) => c.key === sort.key);
66
+ if (!column)
67
+ return list ?? [];
68
+ const dir = sort.dir === 'desc' ? -1 : 1;
69
+ return [...(list ?? [])].sort((a, b) => compare(cellValue(a, column), cellValue(b, column)) * dir);
70
+ }
71
+ export function Table({ columns = [], rows = [], rowKey = 'id', selectable = false, selected: selectedProp, defaultSelected = [], onSelectedChange, sort: sortProp, defaultSort = null, onSortChange, manualSort = false, loading = false, loadingRows = 5, emptyText: emptyTextProp, empty = null, maxHeight = null, label = null, size = 'md', flush = false, onRowClick, className, classes, children, ...rest }) {
72
+ const t = useMessages();
73
+ const emptyText = emptyTextProp ?? t.table.empty;
74
+ const part = partsOf('table', classes);
75
+ const [sort, setSort] = useControllable(sortProp, defaultSort, onSortChange);
76
+ const [selected, setSelected] = useControllable(selectedProp, defaultSelected, onSelectedChange);
77
+ // The latest selection, so two changes before a render stack.
78
+ const latest = useRef(selected);
79
+ useEffect(() => { latest.current = selected; });
80
+ const keyOf = (row) => row[rowKey];
81
+ const display = (row, column) => {
82
+ const value = cellValue(row, column);
83
+ return column.format ? column.format(value, row) : (value ?? '');
84
+ };
85
+ const sortedRows = manualSort ? rows : sortRows(rows, columns, sort);
86
+ const sortDir = (column) => (sort && sort.key === column.key ? sort.dir : null);
87
+ function toggleSort(column) {
88
+ const current = sortDir(column);
89
+ setSort(current == null ? { key: column.key, dir: 'asc' } : current === 'asc' ? { key: column.key, dir: 'desc' } : null);
90
+ }
91
+ const ariaSort = (column) => {
92
+ if (!column.sortable)
93
+ return undefined;
94
+ const dir = sortDir(column);
95
+ return dir === 'asc' ? 'ascending' : dir === 'desc' ? 'descending' : 'none';
96
+ };
97
+ const isSelected = (row) => selected.includes(keyOf(row));
98
+ const selectedCount = rows.filter(isSelected).length;
99
+ const allState = selectedCount === 0 ? false : selectedCount === rows.length ? true : 'indeterminate';
100
+ // Change events don't carry the Shift key, so it's noted on the way in;
101
+ // Shift+mousedown would also select the text between.
102
+ const shift = useRef(false);
103
+ const anchorKey = useRef(null);
104
+ function noteShift(e) {
105
+ const event = e.nativeEvent;
106
+ shift.current = event.shiftKey;
107
+ if (event.shiftKey && e.type === 'mousedown' && e.target.closest('[data-part=select-cell]'))
108
+ e.preventDefault();
109
+ }
110
+ function toggleRow(row, on) {
111
+ const index = sortedRows.indexOf(row);
112
+ const anchor = sortedRows.findIndex((r) => keyOf(r) === anchorKey.current);
113
+ anchorKey.current = keyOf(row);
114
+ // With Shift, every row from the last one clicked to this one.
115
+ const range = shift.current && anchor >= 0 && anchor !== index
116
+ ? sortedRows.slice(Math.min(anchor, index), Math.max(anchor, index) + 1) : [row];
117
+ const keys = range.map(keyOf);
118
+ const others = latest.current.filter((key) => !keys.includes(key));
119
+ latest.current = on ? [...others, ...keys] : others;
120
+ setSelected(latest.current);
121
+ }
122
+ function toggleAll(on) {
123
+ const keys = rows.map(keyOf);
124
+ const others = latest.current.filter((key) => !keys.includes(key));
125
+ latest.current = on ? [...others, ...keys] : others;
126
+ setSelected(latest.current);
127
+ }
128
+ // Not when the click was on a control in the row (a link, a menu…); only
129
+ // the row's own contents count, not what the table sits in (a Card).
130
+ function rowClicked(e, row) {
131
+ const control = e.target.closest('a, button, input, label, select, textarea, [data-scope]:not([data-scope=table])');
132
+ if (control && e.currentTarget.contains(control))
133
+ return;
134
+ onRowClick?.(row);
135
+ }
136
+ const columnCount = columns.length + (selectable ? 1 : 0);
137
+ return (_jsx("div", { ...rest, ...part('root'), className: cx(className, classes?.root), "data-size": size, "data-flush": flush || undefined, "data-loading": (loading && rows.length > 0) || undefined, children: _jsx("div", { ...part('scroll'), "data-sticky": maxHeight ? '' : undefined, style: maxHeight ? { maxHeight } : undefined, children: !columns.length ? children : (_jsxs("table", { ...part('table'), "aria-busy": loading, children: [label && _jsx("caption", { ...part('caption'), children: label }), _jsx("thead", { children: _jsxs("tr", { children: [selectable && (_jsx("th", { ...part('select-cell'), scope: "col", children: _jsx(Checkbox, { label: t.table.selectAll, labelHidden: true, disabled: !rows.length, checked: allState, onCheckedChange: (on) => toggleAll(on) }) })), columns.map((column) => (_jsx("th", { scope: "col", "data-align": column.align ?? 'start', "aria-sort": ariaSort(column), style: column.width ? { width: column.width } : undefined, children: column.sortable
138
+ ? _jsxs("button", { ...part('sort'), type: "button", "data-dir": sortDir(column) ?? undefined, onClick: () => toggleSort(column), children: [_jsx("span", { children: column.label }), _jsx("span", { ...part('sort-icon'), children: _jsx("iconify-icon", { icon: sortDir(column) === 'asc' ? 'lucide:chevron-up'
139
+ : sortDir(column) === 'desc' ? 'lucide:chevron-down' : 'lucide:chevrons-up-down' }) })] })
140
+ : column.label }, column.key)))] }) }), _jsx("tbody", { onPointerDownCapture: noteShift, onMouseDownCapture: noteShift, onKeyDownCapture: noteShift, children: loading && !rows.length
141
+ ? Array.from({ length: loadingRows }, (_, i) => (_jsxs("tr", { children: [selectable && _jsx("td", { ...part('select-cell') }), columns.map((column) => _jsx("td", { children: _jsx(Skeleton, { height: "0.75rem", width: "60%" }) }, column.key))] }, i)))
142
+ : !rows.length
143
+ ? _jsx("tr", { children: _jsx("td", { ...part('empty'), colSpan: columnCount, children: empty ?? emptyText }) })
144
+ : sortedRows.map((row) => (_jsxs("tr", { ...part('row'), "data-selected": isSelected(row) || undefined, "aria-selected": selectable ? isSelected(row) : undefined, onClick: (e) => rowClicked(e, row), children: [selectable && (_jsx("td", { ...part('select-cell'), children: _jsx(Checkbox, { label: _jsxs(_Fragment, { children: [t.table.selectRow, " ", display(row, columns[0])] }), labelHidden: true, checked: isSelected(row), onCheckedChange: (on) => toggleRow(row, on) }) })), columns.map((column) => (_jsx("td", { "data-align": column.align ?? 'start', children: column.cell ? column.cell({ row, column, value: cellValue(row, column) }) : display(row, column) }, column.key)))] }, keyOf(row)))) })] })) }) }));
145
+ }
@@ -0,0 +1,44 @@
1
+ /* The table's cells are styled by element, so a <table> written inside
2
+ (without columns) looks the same as the generated one. */
3
+ @layer ui {
4
+ [data-scope=table][data-part=root] { display: block; width: 100%; min-width: 0; color: var(--ui-text); font-family: var(--ui-font); font-size: 14px;
5
+ --cell-x: 0.75rem; --cell-y: 0.75rem; --cell-h: 3rem; --head-h: 2.25rem;
6
+ &[data-size=sm] { --cell-x: 0.625rem; --cell-y: 0.375rem; --cell-h: 2.25rem; --head-h: 2rem; }
7
+ &[data-size=lg] { --cell-x: 1rem; --cell-y: 1rem; --cell-h: 3.5rem; --head-h: 2.5rem; } }
8
+ /* Scrolls sideways on narrow screens; with maxHeight, down too, under a sticky header. */
9
+ [data-scope=table][data-part=scroll] { width: 100%; overflow-x: auto; box-sizing: border-box;
10
+ background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: calc(var(--ui-radius) + 2px); box-shadow: var(--ui-card-shadow);
11
+ &[data-sticky] { overflow-y: auto; }
12
+ [data-flush] > & { border: none; border-radius: 0; background: transparent; box-shadow: none; }
13
+ & table { width: 100%; border-collapse: separate; border-spacing: 0; }
14
+ & caption { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
15
+ /* Rows get room from padding (so two-line cells breathe too), with a minimum height for one-line ones. */
16
+ & :is(th, td) { padding: var(--cell-y) var(--cell-x); height: var(--cell-h); text-align: left; vertical-align: middle; border-bottom: 1px solid var(--ui-border); }
17
+ /* A slim, quiet header: small muted labels on a faint band. */
18
+ & th { height: var(--head-h); padding-block: 0; font-weight: 500; font-size: 12px; color: var(--ui-muted);
19
+ background: color-mix(in srgb, var(--ui-hover) 60%, var(--ui-surface)); }
20
+ &[data-sticky] th { position: sticky; top: 0; z-index: 1; }
21
+ & :is(th, td)[data-align=center] { text-align: center; }
22
+ & :is(th, td)[data-align=end] { text-align: right; }
23
+ & tbody tr:last-child td { border-bottom: none; }
24
+ & tbody tr:hover { background: color-mix(in srgb, var(--ui-hover) 60%, transparent); }
25
+ & tbody tr[data-selected] { background: color-mix(in srgb, var(--ui-accent-soft) 45%, transparent); }
26
+ /* Outer cells line up with a card's padding. */
27
+ [data-flush] > & :is(th, td):first-child { padding-left: 1.25rem; }
28
+ [data-flush] > & :is(th, td):last-child { padding-right: 1.25rem; } }
29
+ /* Generated cells stay on one line (the table scrolls sideways instead). */
30
+ [data-scope=table][data-part=table] :is(th, td) { white-space: nowrap; }
31
+ /* Inside the scroll part, so they outrank its cell rule. */
32
+ [data-scope=table][data-part=scroll] [data-part=select-cell] { width: 2.25rem; padding-right: 0; }
33
+ [data-scope=table][data-part=scroll] [data-part=empty] { height: auto; padding: 2.5rem 0.75rem; text-align: center; color: var(--ui-muted); white-space: normal; }
34
+ [data-scope=table][data-part=sort] { display: inline-flex; align-items: center; gap: 0.25rem; margin: 0 -0.375rem; padding: 0 0.375rem; height: 1.5rem;
35
+ border: none; border-radius: 4px; background: transparent; color: inherit; font: inherit; cursor: pointer;
36
+ &:hover, &[data-dir] { color: var(--ui-text); }
37
+ &:focus-visible { outline: 2px solid var(--ui-ring-soft); }
38
+ th[data-align=end] > & { flex-direction: row-reverse; } }
39
+ /* Sort icons show on hover or focus, and on the sorted column; their space is kept, so labels don't shift. */
40
+ [data-scope=table][data-part=sort-icon] { display: inline-flex; font-size: 14px; opacity: 0; transition: opacity 120ms;
41
+ [data-part=sort]:is(:hover, :focus-visible) > & { opacity: 0.5; }
42
+ [data-part=sort][data-dir] > & { opacity: 1; } }
43
+ [data-scope=table][data-part=root][data-loading] tbody { opacity: 0.5; transition: opacity 150ms; }
44
+ }