@ratio-ui/core 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +2 -0
  2. package/dist/catalogue.json +2829 -72
  3. package/dist/components/alert/Alert.d.ts +18 -0
  4. package/dist/components/alert/Alert.js +67 -0
  5. package/dist/components/alert/index.d.ts +2 -0
  6. package/dist/components/alert/index.js +1 -0
  7. package/dist/components/alert-dialog/AlertDialog.d.ts +48 -0
  8. package/dist/components/alert-dialog/AlertDialog.js +76 -0
  9. package/dist/components/alert-dialog/index.d.ts +2 -0
  10. package/dist/components/alert-dialog/index.js +1 -0
  11. package/dist/components/app-shell/AppShell.d.ts +22 -0
  12. package/dist/components/app-shell/AppShell.js +39 -0
  13. package/dist/components/app-shell/context.js +5 -0
  14. package/dist/components/app-shell/index.d.ts +2 -0
  15. package/dist/components/app-shell/index.js +1 -0
  16. package/dist/components/avatar/Avatar.d.ts +22 -0
  17. package/dist/components/avatar/Avatar.js +38 -0
  18. package/dist/components/avatar/index.d.ts +2 -0
  19. package/dist/components/avatar/index.js +1 -0
  20. package/dist/components/button/Button.d.ts +37 -0
  21. package/dist/components/button/Button.js +41 -0
  22. package/dist/components/button/index.d.ts +2 -0
  23. package/dist/components/button/index.js +1 -0
  24. package/dist/components/calendar/Calendar.d.ts +20 -0
  25. package/dist/components/calendar/Calendar.js +119 -0
  26. package/dist/components/calendar/index.d.ts +2 -0
  27. package/dist/components/calendar/index.js +1 -0
  28. package/dist/components/card/Card.d.ts +33 -0
  29. package/dist/components/card/Card.js +53 -0
  30. package/dist/components/card/index.d.ts +2 -0
  31. package/dist/components/card/index.js +1 -0
  32. package/dist/components/checkbox/Checkbox.d.ts +10 -0
  33. package/dist/components/checkbox/Checkbox.js +28 -0
  34. package/dist/components/checkbox/index.d.ts +2 -0
  35. package/dist/components/checkbox/index.js +1 -0
  36. package/dist/components/checkbox-list/CheckboxList.d.ts +36 -0
  37. package/dist/components/checkbox-list/CheckboxList.js +93 -0
  38. package/dist/components/checkbox-list/index.d.ts +2 -0
  39. package/dist/components/checkbox-list/index.js +1 -0
  40. package/dist/components/collapsible/Collapsible.d.ts +24 -0
  41. package/dist/components/collapsible/Collapsible.js +58 -0
  42. package/dist/components/collapsible/index.d.ts +2 -0
  43. package/dist/components/collapsible/index.js +1 -0
  44. package/dist/components/data-table/DataTable.d.ts +65 -0
  45. package/dist/components/data-table/DataTable.js +141 -0
  46. package/dist/components/data-table/index.d.ts +2 -0
  47. package/dist/components/data-table/index.js +1 -0
  48. package/dist/components/date-picker/DatePicker.d.ts +33 -0
  49. package/dist/components/date-picker/DatePicker.js +104 -0
  50. package/dist/components/date-picker/index.d.ts +2 -0
  51. package/dist/components/date-picker/index.js +1 -0
  52. package/dist/components/date-range-picker/DateRangePicker.d.ts +39 -0
  53. package/dist/components/date-range-picker/DateRangePicker.js +142 -0
  54. package/dist/components/date-range-picker/index.d.ts +2 -0
  55. package/dist/components/date-range-picker/index.js +1 -0
  56. package/dist/components/definition/Definition.d.ts +14 -0
  57. package/dist/components/definition/Definition.js +29 -0
  58. package/dist/components/definition/index.d.ts +2 -0
  59. package/dist/components/definition/index.js +1 -0
  60. package/dist/components/dialog/Dialog.d.ts +44 -0
  61. package/dist/components/dialog/Dialog.js +79 -0
  62. package/dist/components/dialog/index.d.ts +2 -0
  63. package/dist/components/dialog/index.js +1 -0
  64. package/dist/components/dropdown-menu/DropdownMenu.d.ts +73 -0
  65. package/dist/components/dropdown-menu/DropdownMenu.js +116 -0
  66. package/dist/components/dropdown-menu/index.d.ts +2 -0
  67. package/dist/components/dropdown-menu/index.js +1 -0
  68. package/dist/components/empty-state/EmptyState.d.ts +15 -0
  69. package/dist/components/empty-state/EmptyState.js +30 -0
  70. package/dist/components/empty-state/index.d.ts +2 -0
  71. package/dist/components/empty-state/index.js +1 -0
  72. package/dist/components/field/Field.d.ts +35 -0
  73. package/dist/components/field/Field.js +67 -0
  74. package/dist/components/field/Label.d.ts +8 -0
  75. package/dist/components/field/Label.js +13 -0
  76. package/dist/components/field/index.d.ts +3 -0
  77. package/dist/components/field/index.js +3 -0
  78. package/dist/components/filter-tabs/FilterTabs.d.ts +28 -0
  79. package/dist/components/filter-tabs/FilterTabs.js +37 -0
  80. package/dist/components/filter-tabs/index.d.ts +2 -0
  81. package/dist/components/filter-tabs/index.js +1 -0
  82. package/dist/components/global-search/GlobalSearch.d.ts +51 -0
  83. package/dist/components/global-search/GlobalSearch.js +144 -0
  84. package/dist/components/global-search/index.d.ts +2 -0
  85. package/dist/components/global-search/index.js +1 -0
  86. package/dist/components/icon/registry.d.ts +3 -2
  87. package/dist/components/icon/registry.js +57 -1
  88. package/dist/components/input/Input.d.ts +19 -0
  89. package/dist/components/input/Input.js +26 -0
  90. package/dist/components/input/index.d.ts +2 -0
  91. package/dist/components/input/index.js +1 -0
  92. package/dist/components/input-group/InputGroup.d.ts +42 -0
  93. package/dist/components/input-group/InputGroup.js +83 -0
  94. package/dist/components/input-group/index.d.ts +2 -0
  95. package/dist/components/input-group/index.js +1 -0
  96. package/dist/components/kbd/Kbd.d.ts +17 -0
  97. package/dist/components/kbd/Kbd.js +21 -0
  98. package/dist/components/kbd/index.d.ts +2 -0
  99. package/dist/components/kbd/index.js +1 -0
  100. package/dist/components/link/Link.d.ts +22 -0
  101. package/dist/components/link/Link.js +31 -0
  102. package/dist/components/link/index.d.ts +2 -0
  103. package/dist/components/link/index.js +1 -0
  104. package/dist/components/link-provider/LinkProvider.d.ts +29 -0
  105. package/dist/components/link-provider/LinkProvider.js +15 -0
  106. package/dist/components/link-provider/RatioLink.js +18 -0
  107. package/dist/components/link-provider/context.js +4 -0
  108. package/dist/components/link-provider/index.d.ts +2 -0
  109. package/dist/components/link-provider/index.js +1 -0
  110. package/dist/components/media-grid/MediaGrid.d.ts +49 -0
  111. package/dist/components/media-grid/MediaGrid.js +303 -0
  112. package/dist/components/media-grid/index.d.ts +2 -0
  113. package/dist/components/media-grid/index.js +1 -0
  114. package/dist/components/metric-strip/MetricStrip.d.ts +23 -0
  115. package/dist/components/metric-strip/MetricStrip.js +39 -0
  116. package/dist/components/metric-strip/index.d.ts +2 -0
  117. package/dist/components/metric-strip/index.js +1 -0
  118. package/dist/components/money-input/MoneyInput.d.ts +30 -0
  119. package/dist/components/money-input/MoneyInput.js +72 -0
  120. package/dist/components/money-input/index.d.ts +2 -0
  121. package/dist/components/money-input/index.js +1 -0
  122. package/dist/components/page-header/PageHeader.d.ts +37 -0
  123. package/dist/components/page-header/PageHeader.js +62 -0
  124. package/dist/components/page-header/index.d.ts +2 -0
  125. package/dist/components/page-header/index.js +1 -0
  126. package/dist/components/pagination/Pagination.d.ts +18 -0
  127. package/dist/components/pagination/Pagination.js +59 -0
  128. package/dist/components/pagination/index.d.ts +2 -0
  129. package/dist/components/pagination/index.js +1 -0
  130. package/dist/components/popover/Popover.d.ts +27 -0
  131. package/dist/components/popover/Popover.js +47 -0
  132. package/dist/components/popover/index.d.ts +2 -0
  133. package/dist/components/popover/index.js +1 -0
  134. package/dist/components/save-bar/SaveBar.d.ts +27 -0
  135. package/dist/components/save-bar/SaveBar.js +37 -0
  136. package/dist/components/save-bar/index.d.ts +2 -0
  137. package/dist/components/save-bar/index.js +1 -0
  138. package/dist/components/search-field/SearchField.d.ts +23 -0
  139. package/dist/components/search-field/SearchField.js +66 -0
  140. package/dist/components/search-field/index.d.ts +2 -0
  141. package/dist/components/search-field/index.js +1 -0
  142. package/dist/components/select/Select.d.ts +40 -0
  143. package/dist/components/select/Select.js +84 -0
  144. package/dist/components/select/index.d.ts +2 -0
  145. package/dist/components/select/index.js +1 -0
  146. package/dist/components/side-nav/SideNav.d.ts +64 -0
  147. package/dist/components/side-nav/SideNav.js +194 -0
  148. package/dist/components/side-nav/index.d.ts +2 -0
  149. package/dist/components/side-nav/index.js +1 -0
  150. package/dist/components/skeleton/Skeleton.d.ts +13 -0
  151. package/dist/components/skeleton/Skeleton.js +21 -0
  152. package/dist/components/skeleton/index.d.ts +2 -0
  153. package/dist/components/skeleton/index.js +1 -0
  154. package/dist/components/spinner/Spinner.d.ts +11 -0
  155. package/dist/components/spinner/Spinner.js +16 -0
  156. package/dist/components/spinner/index.d.ts +2 -0
  157. package/dist/components/spinner/index.js +1 -0
  158. package/dist/components/status-pill/StatusPill.d.ts +14 -0
  159. package/dist/components/status-pill/StatusPill.js +19 -0
  160. package/dist/components/status-pill/index.d.ts +2 -0
  161. package/dist/components/status-pill/index.js +1 -0
  162. package/dist/components/summary-chips/SummaryChips.d.ts +17 -0
  163. package/dist/components/summary-chips/SummaryChips.js +19 -0
  164. package/dist/components/summary-chips/index.d.ts +2 -0
  165. package/dist/components/summary-chips/index.js +1 -0
  166. package/dist/components/switch/Switch.d.ts +10 -0
  167. package/dist/components/switch/Switch.js +20 -0
  168. package/dist/components/switch/index.d.ts +2 -0
  169. package/dist/components/switch/index.js +1 -0
  170. package/dist/components/table/Table.d.ts +46 -0
  171. package/dist/components/table/Table.js +87 -0
  172. package/dist/components/table/index.d.ts +2 -0
  173. package/dist/components/table/index.js +1 -0
  174. package/dist/components/table-loader/TableLoader.d.ts +31 -0
  175. package/dist/components/table-loader/TableLoader.js +35 -0
  176. package/dist/components/table-loader/index.d.ts +2 -0
  177. package/dist/components/table-loader/index.js +1 -0
  178. package/dist/components/tabs/Tabs.d.ts +23 -0
  179. package/dist/components/tabs/Tabs.js +41 -0
  180. package/dist/components/tabs/index.d.ts +2 -0
  181. package/dist/components/tabs/index.js +1 -0
  182. package/dist/components/tag-input/TagInput.d.ts +44 -0
  183. package/dist/components/tag-input/TagInput.js +102 -0
  184. package/dist/components/tag-input/index.d.ts +2 -0
  185. package/dist/components/tag-input/index.js +1 -0
  186. package/dist/components/textarea/Textarea.d.ts +15 -0
  187. package/dist/components/textarea/Textarea.js +21 -0
  188. package/dist/components/textarea/index.d.ts +2 -0
  189. package/dist/components/textarea/index.js +1 -0
  190. package/dist/components/toaster/Toaster.d.ts +32 -0
  191. package/dist/components/toaster/Toaster.js +89 -0
  192. package/dist/components/toaster/index.d.ts +2 -0
  193. package/dist/components/toaster/index.js +1 -0
  194. package/dist/components/tooltip/Tooltip.d.ts +18 -0
  195. package/dist/components/tooltip/Tooltip.js +35 -0
  196. package/dist/components/tooltip/index.d.ts +2 -0
  197. package/dist/components/tooltip/index.js +1 -0
  198. package/dist/components/top-bar/StoreSwitcher.d.ts +26 -0
  199. package/dist/components/top-bar/StoreSwitcher.js +48 -0
  200. package/dist/components/top-bar/TopBar.d.ts +24 -0
  201. package/dist/components/top-bar/TopBar.js +53 -0
  202. package/dist/components/top-bar/index.d.ts +3 -0
  203. package/dist/components/top-bar/index.js +3 -0
  204. package/dist/components/unit-input/UnitInput.d.ts +33 -0
  205. package/dist/components/unit-input/UnitInput.js +47 -0
  206. package/dist/components/unit-input/index.d.ts +2 -0
  207. package/dist/components/unit-input/index.js +1 -0
  208. package/dist/index.d.ts +102 -1
  209. package/dist/index.js +54 -2
  210. package/dist/lib/cn.js +38 -27
  211. package/dist/lib/format.d.ts +92 -0
  212. package/dist/lib/format.js +222 -0
  213. package/dist/styles.css +1 -1
  214. package/package.json +205 -3
@@ -0,0 +1,303 @@
1
+ import { Icon } from "../icon/Icon.js";
2
+ import { Button } from "../button/Button.js";
3
+ import { formatCount } from "../../lib/format.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { useEffect, useId, useRef, useState } from "react";
6
+ import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
7
+ import { SortableContext, arrayMove, rectSortingStrategy, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
8
+ import { Progress } from "@base-ui/react/progress";
9
+ import { useDropzone } from "react-dropzone";
10
+ // src/components/media-grid/MediaGrid.tsx
11
+ const orderKey = (ids) => ids.join("\n");
12
+ const IMAGES = { "image/*": [] };
13
+ const INSTRUCTIONS = { draggable: "To move an image, press space or enter to pick it up, the arrow keys to move it, space or enter to drop it and escape to cancel. The Move left, Move right and Make cover buttons move it too" };
14
+ function MediaTile({ item, index, last, disabled, onAction }) {
15
+ const { attributes, listeners, setNodeRef, isDragging } = useSortable({
16
+ id: item.id,
17
+ disabled
18
+ });
19
+ const id = useId();
20
+ const isCover = index === 0;
21
+ const uploading = item.progress !== void 0 && item.progress < 100 && !item.error;
22
+ const describedBy = [
23
+ attributes["aria-describedby"],
24
+ isCover ? `${id}-cover` : null,
25
+ item.error ? `${id}-error` : null
26
+ ].filter(Boolean).join(" ");
27
+ return /* @__PURE__ */ jsxs("li", {
28
+ "data-slot": "media-grid-item",
29
+ "data-media-id": item.id,
30
+ className: "rt:flex rt:w-24 rt:flex-col rt:gap-1",
31
+ children: [
32
+ /* @__PURE__ */ jsxs("div", {
33
+ className: "rt:relative rt:size-24",
34
+ children: [
35
+ /* @__PURE__ */ jsx("div", {
36
+ ref: setNodeRef,
37
+ ...attributes,
38
+ ...listeners,
39
+ "aria-describedby": describedBy,
40
+ "aria-invalid": item.error ? true : void 0,
41
+ "data-dragging": isDragging || void 0,
42
+ className: "rt:size-full rt:cursor-grab rt:overflow-hidden rt:rounded-lg rt:bg-muted rt:p-px rt:shadow-ring-subtle rt:forced-outline rt:select-none rt:transition-colors rt:focus-visible:focus-ring rt:aria-invalid:shadow-ring-invalid rt:aria-invalid:focus-visible:shadow-ring-invalid rt:aria-disabled:cursor-default rt:data-dragging:opacity-50",
43
+ children: /* @__PURE__ */ jsx("img", {
44
+ src: item.src,
45
+ alt: item.alt,
46
+ draggable: false,
47
+ className: "rt:size-full rt:rounded-md rt:object-cover"
48
+ })
49
+ }),
50
+ isCover ? /* @__PURE__ */ jsx("span", {
51
+ id: `${id}-cover`,
52
+ className: "rt:pointer-events-none rt:absolute rt:top-1 rt:left-1 rt:rounded-sm rt:bg-background rt:px-1.5 rt:text-c2 rt:text-foreground",
53
+ children: "Cover"
54
+ }) : null,
55
+ uploading ? /* @__PURE__ */ jsx(Progress.Root, {
56
+ value: item.progress ?? 0,
57
+ "aria-label": `Uploading ${item.alt}`,
58
+ className: "rt:pointer-events-none rt:absolute rt:inset-x-2 rt:bottom-2",
59
+ children: /* @__PURE__ */ jsx(Progress.Track, {
60
+ "data-slot": "media-grid-progress-track",
61
+ className: "rt:h-progress rt:overflow-hidden rt:bg-progress-track",
62
+ children: /* @__PURE__ */ jsx(Progress.Indicator, { className: "rt:h-full rt:bg-progress" })
63
+ })
64
+ }) : null
65
+ ]
66
+ }),
67
+ item.error ? /* @__PURE__ */ jsxs("span", {
68
+ className: "rt:flex rt:items-start rt:gap-1 rt:text-error-icon",
69
+ children: [/* @__PURE__ */ jsx("span", {
70
+ className: "rt:mt-0.5 rt:flex",
71
+ children: /* @__PURE__ */ jsx(Icon, { name: "alert-triangle" })
72
+ }), /* @__PURE__ */ jsx("span", {
73
+ id: `${id}-error`,
74
+ className: "rt:text-c1 rt:text-error",
75
+ children: item.error
76
+ })]
77
+ }) : null,
78
+ /* @__PURE__ */ jsxs("div", {
79
+ role: "group",
80
+ "aria-label": item.alt,
81
+ className: "rt:flex rt:flex-col rt:items-start",
82
+ children: [/* @__PURE__ */ jsxs("div", {
83
+ className: "rt:flex rt:w-full rt:justify-between",
84
+ children: [
85
+ /* @__PURE__ */ jsx(Button, {
86
+ variant: "ghost",
87
+ iconOnly: true,
88
+ "aria-label": "Move left",
89
+ "data-media-action": "left",
90
+ disabled: disabled || isCover,
91
+ onClick: () => onAction("left", index),
92
+ children: /* @__PURE__ */ jsx(Icon, { name: "chevron-left" })
93
+ }),
94
+ /* @__PURE__ */ jsx(Button, {
95
+ variant: "ghost",
96
+ iconOnly: true,
97
+ "aria-label": "Move right",
98
+ "data-media-action": "right",
99
+ disabled: disabled || last,
100
+ onClick: () => onAction("right", index),
101
+ children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right" })
102
+ }),
103
+ /* @__PURE__ */ jsx(Button, {
104
+ variant: "ghost",
105
+ iconOnly: true,
106
+ "aria-label": "Remove",
107
+ "data-media-action": "remove",
108
+ disabled,
109
+ onClick: () => onAction("remove", index),
110
+ children: /* @__PURE__ */ jsx(Icon, { name: "x" })
111
+ })
112
+ ]
113
+ }), isCover ? null : /* @__PURE__ */ jsx(Button, {
114
+ variant: "link",
115
+ "data-media-action": "cover",
116
+ disabled,
117
+ onClick: () => onAction("cover", index),
118
+ children: "Make cover"
119
+ })]
120
+ })
121
+ ]
122
+ });
123
+ }
124
+ /**
125
+ * The media of a product: a row of square thumbs with an add tile that opens the file picker and takes dropped files.
126
+ * Thumbs reorder by drag (pointer, touch or keyboard) and by the Move left, Move right and Make cover buttons, so no
127
+ * drag is needed. The first image is the cover and carries a badge. Uploads show a thin progress bar and failures an
128
+ * error under the thumb. Every move, cover change and removal is announced to screen readers.
129
+ */
130
+ function MediaGrid({ items, onAdd, onReorder, onRemove, onMakeCover, accept = IMAGES, disabled = false, addLabel = "Add media", "aria-label": ariaLabel, "aria-labelledby": labelledBy, ...rest }) {
131
+ const ids = items.map((i) => i.id);
132
+ const total = formatCount(items.length);
133
+ const [activeId, setActiveId] = useState(null);
134
+ const pendingRef = useRef(null);
135
+ const statusRef = useRef(null);
136
+ const { getRootProps, getInputProps, open, isDragActive, rootRef } = useDropzone({
137
+ accept,
138
+ disabled,
139
+ multiple: true,
140
+ noClick: true,
141
+ noKeyboard: true,
142
+ onDrop: (files) => {
143
+ if (files.length > 0) onAdd(files);
144
+ }
145
+ });
146
+ const sensors = useSensors(useSensor(MouseSensor, { activationConstraint: { distance: 4 } }), useSensor(TouchSensor, { activationConstraint: {
147
+ delay: 200,
148
+ tolerance: 6
149
+ } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
150
+ useEffect(() => {
151
+ const want = pendingRef.current;
152
+ const root = rootRef.current;
153
+ if (!want || !root) return;
154
+ const now = orderKey(items.map((i) => i.id));
155
+ if (now === want.before) return;
156
+ pendingRef.current = null;
157
+ if (now !== want.expected) return;
158
+ if (statusRef.current) statusRef.current.textContent = want.message;
159
+ const focused = document.activeElement;
160
+ if (focused && focused !== document.body && !root.contains(focused)) return;
161
+ const tile = Array.from(root.querySelectorAll("[data-slot=\"media-grid-item\"]")).find((el) => el.dataset.mediaId === want.id);
162
+ for (const action of want.actions) {
163
+ const target = tile?.querySelector(`button[data-media-action="${action}"]`);
164
+ if (target && !target.disabled) {
165
+ target.focus();
166
+ return;
167
+ }
168
+ }
169
+ root.querySelector("button[data-media-action=\"add\"]")?.focus();
170
+ }, [items, rootRef]);
171
+ const altOf = (id) => items.find((i) => i.id === id)?.alt ?? "";
172
+ const positionOf = (id) => formatCount(ids.indexOf(String(id)) + 1);
173
+ const announcements = {
174
+ onDragStart: ({ active }) => `Picked up ${altOf(active.id)}, position ${positionOf(active.id)} of ${total}`,
175
+ onDragOver: ({ active, over }) => over ? `${altOf(active.id)} is over position ${positionOf(over.id)} of ${total}` : `${altOf(active.id)} is not over a position`,
176
+ onDragEnd: ({ active, over }) => over ? `${altOf(active.id)} dropped at position ${positionOf(over.id)} of ${total}` : `${altOf(active.id)} dropped`,
177
+ onDragCancel: ({ active }) => `Moving ${altOf(active.id)} cancelled`
178
+ };
179
+ const onAction = (action, index) => {
180
+ const item = items[index];
181
+ if (!item) return;
182
+ const before = orderKey(ids);
183
+ if (action === "remove") {
184
+ const next = ids.filter((id) => id !== item.id);
185
+ pendingRef.current = {
186
+ before,
187
+ expected: orderKey(next),
188
+ message: `${item.alt} removed`,
189
+ id: (items[index + 1] ?? items[index - 1])?.id ?? null,
190
+ actions: ["remove"]
191
+ };
192
+ onRemove(item.id);
193
+ return;
194
+ }
195
+ if (action === "cover") {
196
+ const next = arrayMove(ids, index, 0);
197
+ pendingRef.current = {
198
+ before,
199
+ expected: orderKey(next),
200
+ message: `${item.alt} is now the cover`,
201
+ id: item.id,
202
+ actions: ["right", "remove"]
203
+ };
204
+ onReorder(next);
205
+ onMakeCover?.(item.id);
206
+ return;
207
+ }
208
+ const to = action === "left" ? index - 1 : index + 1;
209
+ if (to < 0 || to >= items.length) return;
210
+ const next = arrayMove(ids, index, to);
211
+ pendingRef.current = {
212
+ before,
213
+ expected: orderKey(next),
214
+ message: `${item.alt} moved to position ${formatCount(to + 1)} of ${total}`,
215
+ id: item.id,
216
+ actions: action === "left" ? [
217
+ "left",
218
+ "right",
219
+ "remove"
220
+ ] : [
221
+ "right",
222
+ "left",
223
+ "remove"
224
+ ]
225
+ };
226
+ onReorder(next);
227
+ };
228
+ const onDragEnd = ({ active, over }) => {
229
+ setActiveId(null);
230
+ if (!over || active.id === over.id) return;
231
+ const from = ids.indexOf(String(active.id));
232
+ const to = ids.indexOf(String(over.id));
233
+ if (from < 0 || to < 0) return;
234
+ onReorder(arrayMove(ids, from, to));
235
+ };
236
+ const active = items.find((i) => i.id === activeId);
237
+ return /* @__PURE__ */ jsxs("div", {
238
+ ...getRootProps({ role: "group" }),
239
+ ...rest,
240
+ "aria-label": labelledBy ? ariaLabel : ariaLabel ?? "Media",
241
+ "aria-labelledby": labelledBy,
242
+ "data-slot": "media-grid",
243
+ className: "rt:flex rt:flex-col",
244
+ children: [
245
+ /* @__PURE__ */ jsx("input", { ...getInputProps() }),
246
+ /* @__PURE__ */ jsxs(DndContext, {
247
+ sensors,
248
+ collisionDetection: closestCenter,
249
+ accessibility: {
250
+ announcements,
251
+ screenReaderInstructions: INSTRUCTIONS
252
+ },
253
+ onDragStart: ({ active: dragged }) => setActiveId(String(dragged.id)),
254
+ onDragEnd,
255
+ onDragCancel: () => setActiveId(null),
256
+ children: [/* @__PURE__ */ jsx(SortableContext, {
257
+ items: ids,
258
+ strategy: rectSortingStrategy,
259
+ children: /* @__PURE__ */ jsxs("ul", {
260
+ className: "rt:flex rt:flex-wrap rt:gap-2",
261
+ children: [items.map((item, index) => /* @__PURE__ */ jsx(MediaTile, {
262
+ item,
263
+ index,
264
+ last: index === items.length - 1,
265
+ disabled,
266
+ onAction
267
+ }, item.id)), /* @__PURE__ */ jsx("li", {
268
+ className: "rt:flex",
269
+ children: /* @__PURE__ */ jsx("button", {
270
+ type: "button",
271
+ "aria-label": addLabel,
272
+ "data-media-action": "add",
273
+ "data-drag-active": isDragActive || void 0,
274
+ disabled,
275
+ onClick: () => open(),
276
+ className: "rt:flex rt:size-24 rt:cursor-pointer rt:items-center rt:justify-center rt:rounded-lg rt:border rt:border-dashed rt:border-border-strong rt:bg-background rt:text-icon rt:transition-colors rt:hover:bg-muted rt:focus-visible:focus-ring rt:data-drag-active:border-primary rt:data-drag-active:bg-primary-tint rt:disabled:pointer-events-none rt:disabled:cursor-not-allowed rt:disabled:opacity-50",
277
+ children: /* @__PURE__ */ jsx(Icon, {
278
+ name: "plus",
279
+ size: 16
280
+ })
281
+ })
282
+ })]
283
+ })
284
+ }), /* @__PURE__ */ jsx(DragOverlay, {
285
+ dropAnimation: null,
286
+ children: active ? /* @__PURE__ */ jsx("img", {
287
+ src: active.src,
288
+ alt: "",
289
+ draggable: false,
290
+ className: "rt:size-24 rt:cursor-grabbing rt:rounded-lg rt:object-cover rt:shadow-ring-subtle"
291
+ }) : null
292
+ })]
293
+ }),
294
+ /* @__PURE__ */ jsx("div", {
295
+ ref: statusRef,
296
+ "data-slot": "media-grid-status",
297
+ role: "status",
298
+ className: "rt:sr-only"
299
+ })
300
+ ]
301
+ });
302
+ }
303
+ export { MediaGrid };
@@ -0,0 +1,2 @@
1
+ import { MediaGrid, MediaGridItem, MediaGridProps } from "./MediaGrid.js";
2
+ export { MediaGrid, type MediaGridItem, type MediaGridProps };
@@ -0,0 +1 @@
1
+ export { MediaGrid } from "./MediaGrid.js";
@@ -0,0 +1,23 @@
1
+ import { JSX, ReactNode } from "react";
2
+ /** One metric of the strip. */
3
+ export type MetricSegment = {
4
+ /** A stable key. */
5
+ id: string;
6
+ /** The metric's name. */
7
+ title: string;
8
+ /** A sentence that explains the title; the title gets a dotted underline and opens it. */
9
+ definition?: ReactNode;
10
+ /** The value. A number is formatted by format; a string is shown as it is; null or undefined shows No data. */
11
+ value?: number | string | null;
12
+ /** How a number is written: money (rupees, Indian grouping) or count (123456). Defaults to count. */
13
+ format?: 'money' | 'count';
14
+ };
15
+ /** Props of MetricStrip. */
16
+ export type MetricStripProps = {
17
+ /** The metrics, left to right. */
18
+ segments: MetricSegment[];
19
+ /** The accessible name of the strip. */
20
+ label?: string;
21
+ };
22
+ /** A row of metrics in one hairline container: each segment is a title, optionally with a definition, over its value. */
23
+ export declare function MetricStrip({ segments, label }: MetricStripProps): JSX.Element;
@@ -0,0 +1,39 @@
1
+ import { formatCount, formatMoney } from "../../lib/format.js";
2
+ import { Definition } from "../definition/Definition.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ // src/components/metric-strip/MetricStrip.tsx
5
+ function show(segment) {
6
+ const { value, format = "count" } = segment;
7
+ if (value === null || value === void 0) return "No data";
8
+ if (typeof value === "string") return value;
9
+ return format === "money" ? formatMoney(value) : formatCount(value);
10
+ }
11
+ /** A row of metrics in one hairline container: each segment is a title, optionally with a definition, over its value. */
12
+ function MetricStrip({ segments, label }) {
13
+ return /* @__PURE__ */ jsx("div", {
14
+ role: "group",
15
+ "aria-label": label,
16
+ className: "rt:overflow-hidden rt:rounded-md rt:border rt:border-border rt:bg-background",
17
+ children: /* @__PURE__ */ jsx("div", {
18
+ className: "rt:-mr-px rt:-mb-px rt:flex rt:flex-wrap",
19
+ children: segments.map((segment) => {
20
+ const empty = segment.value === null || segment.value === void 0;
21
+ return /* @__PURE__ */ jsxs("div", {
22
+ "data-segment": "",
23
+ className: "rt:flex rt:min-w-0 rt:flex-auto rt:flex-col rt:gap-1 rt:border-r rt:border-b rt:border-divider rt:p-6",
24
+ children: [/* @__PURE__ */ jsx("span", {
25
+ className: "rt:text-b1 rt:text-muted-foreground",
26
+ children: segment.definition !== void 0 ? /* @__PURE__ */ jsx(Definition, {
27
+ definition: segment.definition,
28
+ children: segment.title
29
+ }) : segment.title
30
+ }), /* @__PURE__ */ jsx("span", {
31
+ className: empty ? "rt:text-b2 rt:text-muted-foreground" : "rt:text-s1 rt:text-foreground-strong",
32
+ children: show(segment)
33
+ })]
34
+ }, segment.id);
35
+ })
36
+ })
37
+ });
38
+ }
39
+ export { MetricStrip };
@@ -0,0 +1,2 @@
1
+ import { MetricSegment, MetricStrip, MetricStripProps } from "./MetricStrip.js";
2
+ export { type MetricSegment, MetricStrip, type MetricStripProps };
@@ -0,0 +1 @@
1
+ export { MetricStrip } from "./MetricStrip.js";
@@ -0,0 +1,30 @@
1
+ import { InputGroupSize } from "../input-group/InputGroup.js";
2
+ import "../input-group/index.js";
3
+ import { InputHTMLAttributes, JSX, Ref } from "react";
4
+ /**
5
+ * The props of MoneyInput: the HTML attributes of an input, except className, style, size and the ones the field sets
6
+ * itself (type, inputMode, step, onChange), with the amount in integer paise in and out. Mark an invalid field with
7
+ * aria-invalid, which draws the error ring around the whole group.
8
+ */
9
+ export type MoneyInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'style' | 'size' | 'type' | 'inputMode' | 'step' | 'value' | 'defaultValue' | 'onChange' | 'min' | 'max'> & {
10
+ /** The amount in integer paise (12345650 is 1,23,456 rupees and 50 paise), or null for an empty field. Pass it to control the field. */
11
+ value?: number | null;
12
+ /** The starting amount in integer paise when the field is not controlled. Defaults to empty. */
13
+ defaultValue?: number | null;
14
+ /** Called with the amount in integer paise as it is typed, pasted or cleared (null when empty). */
15
+ onValueChange?: (paise: number | null) => void;
16
+ /** The smallest amount in integer paise; the field moves a smaller one up to it on blur. Leave it out to allow negatives. */
17
+ min?: number;
18
+ /** The largest amount in integer paise; the field moves a larger one down to it on blur. */
19
+ max?: number;
20
+ /** The height in px: 36 for forms, 32 for prices inside a table. Defaults to 36. */
21
+ size?: InputGroupSize;
22
+ /** Receives the input element. */
23
+ ref?: Ref<HTMLInputElement>;
24
+ };
25
+ /**
26
+ * A money field: the currency symbol before the amount, inside one ring. It takes and gives integer paise, shows the
27
+ * amount grouped as India groups it with two decimals (1,23,456.50) once the field loses focus, and reads a pasted
28
+ * amount written with the currency symbol or with either grouping. Under a name, a form submits paise.
29
+ */
30
+ export declare function MoneyInput({ value, defaultValue, onValueChange, min, max, size, id, name, disabled, readOnly, required, onPaste, 'aria-describedby': describedBy, ref, ...rest }: MoneyInputProps): JSX.Element;
@@ -0,0 +1,72 @@
1
+ import { FORMAT, MONEY_NUMBER_FORMAT, amountToPaise, currencySymbol, formatPaiseForEditing, paiseToAmount, parseMoney } from "../../lib/format.js";
2
+ import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "../input-group/InputGroup.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { useId, useState } from "react";
5
+ import { NumberField } from "@base-ui/react/number-field";
6
+ // src/components/money-input/MoneyInput.tsx
7
+ const amountOf = (paise) => paise === void 0 ? void 0 : paiseToAmount(paise);
8
+ /**
9
+ * Writes text into the input the way typing does (the value setter React tracks, then an input event), so NumberField
10
+ * reads it with its own change handler and keeps its text and value in step.
11
+ */
12
+ function typeInto(input, text) {
13
+ Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set?.call(input, text);
14
+ input.dispatchEvent(new Event("input", { bubbles: true }));
15
+ }
16
+ /**
17
+ * A money field: the currency symbol before the amount, inside one ring. It takes and gives integer paise, shows the
18
+ * amount grouped as India groups it with two decimals (1,23,456.50) once the field loses focus, and reads a pasted
19
+ * amount written with the currency symbol or with either grouping. Under a name, a form submits paise.
20
+ */
21
+ function MoneyInput({ value, defaultValue = null, onValueChange, min, max, size = 36, id, name, disabled, readOnly, required, onPaste, "aria-describedby": describedBy, ref, ...rest }) {
22
+ const [ownPaise, setOwnPaise] = useState(defaultValue);
23
+ const paise = value === void 0 ? ownPaise : value;
24
+ const symbolId = useId();
25
+ const change = (amount) => {
26
+ const next = amount === null ? null : amountToPaise(amount);
27
+ if (value === void 0) setOwnPaise(next);
28
+ onValueChange?.(next);
29
+ };
30
+ const paste = (event) => {
31
+ onPaste?.(event);
32
+ const input = event.currentTarget;
33
+ if (event.defaultPrevented || input.readOnly) return;
34
+ event.preventDefault();
35
+ const { value: text, selectionStart, selectionEnd } = input;
36
+ const pasted = event.clipboardData.getData("text/plain");
37
+ const next = parseMoney(text.slice(0, selectionStart ?? text.length) + pasted + text.slice(selectionEnd ?? text.length));
38
+ if (next !== null) typeInto(input, formatPaiseForEditing(next));
39
+ };
40
+ return /* @__PURE__ */ jsxs(NumberField.Root, {
41
+ id,
42
+ value: paise === null ? null : paiseToAmount(paise),
43
+ onValueChange: change,
44
+ locale: FORMAT.locale,
45
+ format: MONEY_NUMBER_FORMAT,
46
+ min: amountOf(min),
47
+ max: amountOf(max),
48
+ disabled,
49
+ readOnly,
50
+ required,
51
+ children: [/* @__PURE__ */ jsxs(InputGroup, {
52
+ size,
53
+ "aria-disabled": disabled || void 0,
54
+ children: [/* @__PURE__ */ jsx(InputGroupAddon, { children: /* @__PURE__ */ jsx(InputGroupText, {
55
+ id: symbolId,
56
+ children: currencySymbol()
57
+ }) }), /* @__PURE__ */ jsx(NumberField.Input, {
58
+ render: /* @__PURE__ */ jsx(InputGroupInput, {}),
59
+ ref,
60
+ "aria-describedby": describedBy === void 0 ? symbolId : `${symbolId} ${describedBy}`,
61
+ onPaste: paste,
62
+ ...rest
63
+ })]
64
+ }), name !== void 0 && /* @__PURE__ */ jsx("input", {
65
+ type: "hidden",
66
+ name,
67
+ value: paise ?? "",
68
+ disabled
69
+ })]
70
+ });
71
+ }
72
+ export { MoneyInput };
@@ -0,0 +1,2 @@
1
+ import { MoneyInput, MoneyInputProps } from "./MoneyInput.js";
2
+ export { MoneyInput, type MoneyInputProps };
@@ -0,0 +1 @@
1
+ export { MoneyInput } from "./MoneyInput.js";
@@ -0,0 +1,37 @@
1
+ import { StatusPillTone } from "../status-pill/StatusPill.js";
2
+ import "../status-pill/index.js";
3
+ import { JSX, ReactNode } from "react";
4
+ /** One entry of the overflow menu. */
5
+ export type PageHeaderMenuItem = {
6
+ /** The text of the item. */
7
+ label: string;
8
+ /** Runs when the item is chosen. */
9
+ onSelect: () => void;
10
+ /** Draws the item in the danger colour, for an action that deletes. */
11
+ destructive?: boolean;
12
+ };
13
+ /** Props of PageHeader. */
14
+ export type PageHeaderProps = {
15
+ /** The page title, shown as the level 1 heading. */
16
+ title: string;
17
+ /** An icon or logo shown before the title. */
18
+ icon?: ReactNode;
19
+ /** The state of the record, one word, beside the title. */
20
+ status?: {
21
+ label: string;
22
+ tone?: StatusPillTone;
23
+ };
24
+ /** Where the back link goes. The link shows only with this. On a router, LinkProvider draws it with the app's own link. */
25
+ backHref?: string;
26
+ /** The text of the back link, such as the parent page's name. Defaults to Back. */
27
+ backLabel?: string;
28
+ /** Buttons on the right, such as Export and Add product. */
29
+ actions?: ReactNode;
30
+ /** Entries of the overflow menu. The More actions button shows only when there is at least one. */
31
+ menuItems?: PageHeaderMenuItem[];
32
+ };
33
+ /**
34
+ * The top of a page: an optional back link, then an icon, the title and a status pill on the left, and the actions with
35
+ * an overflow menu on the right. It wraps on a phone.
36
+ */
37
+ export declare function PageHeader({ title, icon, status, backHref, backLabel, actions, menuItems }: PageHeaderProps): JSX.Element;
@@ -0,0 +1,62 @@
1
+ import { Icon } from "../icon/Icon.js";
2
+ import { Button } from "../button/Button.js";
3
+ import { RatioLink } from "../link-provider/RatioLink.js";
4
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../dropdown-menu/DropdownMenu.js";
5
+ import { StatusPill } from "../status-pill/StatusPill.js";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ // src/components/page-header/PageHeader.tsx
8
+ /**
9
+ * The top of a page: an optional back link, then an icon, the title and a status pill on the left, and the actions with
10
+ * an overflow menu on the right. It wraps on a phone.
11
+ */
12
+ function PageHeader({ title, icon, status, backHref, backLabel = "Back", actions, menuItems }) {
13
+ const hasMenu = menuItems !== void 0 && menuItems.length > 0;
14
+ return /* @__PURE__ */ jsxs("header", {
15
+ className: "rt:flex rt:flex-col rt:gap-2",
16
+ children: [backHref !== void 0 && /* @__PURE__ */ jsxs(RatioLink, {
17
+ href: backHref,
18
+ className: "rt:inline-flex rt:w-fit rt:items-center rt:gap-1 rt:rounded-sm rt:text-b1 rt:text-muted-foreground rt:transition-colors rt:hover:text-foreground rt:focus-visible:focus-ring",
19
+ children: [/* @__PURE__ */ jsx(Icon, {
20
+ name: "chevron-left",
21
+ size: 8
22
+ }), backLabel]
23
+ }), /* @__PURE__ */ jsxs("div", {
24
+ className: "rt:flex rt:flex-wrap rt:items-center rt:justify-between rt:gap-x-3 rt:gap-y-2",
25
+ children: [/* @__PURE__ */ jsxs("div", {
26
+ className: "rt:flex rt:min-w-0 rt:items-center rt:gap-3",
27
+ children: [
28
+ icon !== void 0 && /* @__PURE__ */ jsx("span", {
29
+ className: "rt:flex rt:shrink-0 rt:items-center",
30
+ children: icon
31
+ }),
32
+ /* @__PURE__ */ jsx("h1", {
33
+ className: "rt:m-0 rt:min-w-0 rt:truncate rt:text-h5 rt:text-foreground-strong",
34
+ children: title
35
+ }),
36
+ status !== void 0 && /* @__PURE__ */ jsx(StatusPill, {
37
+ tone: status.tone,
38
+ children: status.label
39
+ })
40
+ ]
41
+ }), (actions !== void 0 || hasMenu) && /* @__PURE__ */ jsxs("div", {
42
+ className: "rt:flex rt:flex-wrap rt:items-center rt:gap-2",
43
+ children: [actions, hasMenu && /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsxs(DropdownMenuTrigger, {
44
+ render: /* @__PURE__ */ jsx(Button, { variant: "secondary" }),
45
+ children: ["More actions", /* @__PURE__ */ jsx(Icon, {
46
+ name: "nav-caret-down",
47
+ size: 8
48
+ })]
49
+ }), /* @__PURE__ */ jsx(DropdownMenuContent, {
50
+ side: "bottom",
51
+ align: "end",
52
+ children: menuItems.map((item) => /* @__PURE__ */ jsx(DropdownMenuItem, {
53
+ variant: item.destructive ? "destructive" : void 0,
54
+ onClick: item.onSelect,
55
+ children: item.label
56
+ }, item.label))
57
+ })] })]
58
+ })]
59
+ })]
60
+ });
61
+ }
62
+ export { PageHeader };
@@ -0,0 +1,2 @@
1
+ import { PageHeader, PageHeaderMenuItem, PageHeaderProps } from "./PageHeader.js";
2
+ export { PageHeader, type PageHeaderMenuItem, type PageHeaderProps };
@@ -0,0 +1 @@
1
+ export { PageHeader } from "./PageHeader.js";
@@ -0,0 +1,18 @@
1
+ import { ComponentProps, JSX } from "react";
2
+ /** The props of Pagination: the attributes of a nav element, except className, style and children, and the page being shown. */
3
+ export type PaginationProps = Omit<ComponentProps<'nav'>, 'className' | 'style' | 'children'> & {
4
+ /** The page being shown, counting from 1. */
5
+ page: number;
6
+ /** How many results one page holds. */
7
+ pageSize: number;
8
+ /** How many results there are in all, across every page. */
9
+ total: number;
10
+ /** Called with the page to show when Previous or Next is pressed. */
11
+ onPageChange: (page: number) => void;
12
+ };
13
+ /**
14
+ * The footer under a table: Previous and Next arrows and the range on show, written "1 – 20 of 84 results". An arrow is
15
+ * dimmed to 50% and disabled (aria-disabled, still focusable) when there is no page that way. The parent owns the page: it passes `page` and updates it in
16
+ * `onPageChange`.
17
+ */
18
+ export declare function Pagination({ page, pageSize, total, onPageChange, 'aria-label': label, ...rest }: PaginationProps): JSX.Element;