@ratio-ui/core 0.2.0 → 0.4.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 (143) hide show
  1. package/dist/catalogue.json +2690 -749
  2. package/dist/components/accordion/Accordion.d.ts +19 -0
  3. package/dist/components/accordion/Accordion.js +52 -0
  4. package/dist/components/accordion/index.d.ts +2 -0
  5. package/dist/components/accordion/index.js +1 -0
  6. package/dist/components/alert/Alert.d.ts +18 -0
  7. package/dist/components/alert/Alert.js +67 -0
  8. package/dist/components/alert/index.d.ts +2 -0
  9. package/dist/components/alert/index.js +1 -0
  10. package/dist/components/alert-dialog/AlertDialog.d.ts +48 -0
  11. package/dist/components/alert-dialog/AlertDialog.js +76 -0
  12. package/dist/components/alert-dialog/index.d.ts +2 -0
  13. package/dist/components/alert-dialog/index.js +1 -0
  14. package/dist/components/app-shell/AppShell.js +1 -1
  15. package/dist/components/breadcrumb/Breadcrumb.d.ts +19 -0
  16. package/dist/components/breadcrumb/Breadcrumb.js +50 -0
  17. package/dist/components/breadcrumb/index.d.ts +2 -0
  18. package/dist/components/breadcrumb/index.js +1 -0
  19. package/dist/components/calendar/Calendar.d.ts +20 -0
  20. package/dist/components/calendar/Calendar.js +119 -0
  21. package/dist/components/calendar/index.d.ts +2 -0
  22. package/dist/components/calendar/index.js +1 -0
  23. package/dist/components/chart/Chart.d.ts +137 -0
  24. package/dist/components/chart/Chart.js +238 -0
  25. package/dist/components/chart/index.d.ts +2 -0
  26. package/dist/components/chart/index.js +1 -0
  27. package/dist/components/checkbox-list/CheckboxList.d.ts +36 -0
  28. package/dist/components/checkbox-list/CheckboxList.js +93 -0
  29. package/dist/components/checkbox-list/index.d.ts +2 -0
  30. package/dist/components/checkbox-list/index.js +1 -0
  31. package/dist/components/collapsible/Collapsible.d.ts +24 -0
  32. package/dist/components/collapsible/Collapsible.js +58 -0
  33. package/dist/components/collapsible/index.d.ts +2 -0
  34. package/dist/components/collapsible/index.js +1 -0
  35. package/dist/components/combobox/Combobox.d.ts +52 -0
  36. package/dist/components/combobox/Combobox.js +92 -0
  37. package/dist/components/combobox/index.d.ts +2 -0
  38. package/dist/components/combobox/index.js +1 -0
  39. package/dist/components/data-table/DataTable.js +2 -2
  40. package/dist/components/date-picker/DatePicker.d.ts +34 -0
  41. package/dist/components/date-picker/DatePicker.js +108 -0
  42. package/dist/components/date-picker/index.d.ts +2 -0
  43. package/dist/components/date-picker/index.js +1 -0
  44. package/dist/components/date-range-picker/DateRangePicker.d.ts +40 -0
  45. package/dist/components/date-range-picker/DateRangePicker.js +146 -0
  46. package/dist/components/date-range-picker/index.d.ts +2 -0
  47. package/dist/components/date-range-picker/index.js +1 -0
  48. package/dist/components/drawer/Drawer.d.ts +52 -0
  49. package/dist/components/drawer/Drawer.js +120 -0
  50. package/dist/components/drawer/index.d.ts +2 -0
  51. package/dist/components/drawer/index.js +1 -0
  52. package/dist/components/field/Field.d.ts +38 -0
  53. package/dist/components/field/Field.js +73 -0
  54. package/dist/components/field/Label.d.ts +8 -0
  55. package/dist/components/field/Label.js +13 -0
  56. package/dist/components/field/index.d.ts +3 -0
  57. package/dist/components/field/index.js +3 -0
  58. package/dist/components/filter-tabs/FilterTabs.js +1 -1
  59. package/dist/components/global-search/GlobalSearch.js +1 -1
  60. package/dist/components/icon/registry.d.ts +2 -2
  61. package/dist/components/icon/registry.js +5 -1
  62. package/dist/components/input-group/InputGroup.js +1 -1
  63. package/dist/components/input-otp/InputOtp.d.ts +43 -0
  64. package/dist/components/input-otp/InputOtp.js +45 -0
  65. package/dist/components/input-otp/index.d.ts +2 -0
  66. package/dist/components/input-otp/index.js +1 -0
  67. package/dist/components/key-value-list/KeyValueList.d.ts +25 -0
  68. package/dist/components/key-value-list/KeyValueList.js +40 -0
  69. package/dist/components/key-value-list/index.d.ts +2 -0
  70. package/dist/components/key-value-list/index.js +1 -0
  71. package/dist/components/link/Link.d.ts +22 -0
  72. package/dist/components/link/Link.js +31 -0
  73. package/dist/components/link/index.d.ts +2 -0
  74. package/dist/components/link/index.js +1 -0
  75. package/dist/components/media-grid/MediaGrid.d.ts +49 -0
  76. package/dist/components/media-grid/MediaGrid.js +303 -0
  77. package/dist/components/media-grid/index.d.ts +2 -0
  78. package/dist/components/media-grid/index.js +1 -0
  79. package/dist/components/money-input/MoneyInput.d.ts +30 -0
  80. package/dist/components/money-input/MoneyInput.js +72 -0
  81. package/dist/components/money-input/index.d.ts +2 -0
  82. package/dist/components/money-input/index.js +1 -0
  83. package/dist/components/page-header/PageHeader.js +9 -9
  84. package/dist/components/progress/Progress.d.ts +25 -0
  85. package/dist/components/progress/Progress.js +38 -0
  86. package/dist/components/progress/index.d.ts +2 -0
  87. package/dist/components/progress/index.js +1 -0
  88. package/dist/components/radio-group/RadioGroup.d.ts +18 -0
  89. package/dist/components/radio-group/RadioGroup.js +32 -0
  90. package/dist/components/radio-group/index.d.ts +2 -0
  91. package/dist/components/radio-group/index.js +1 -0
  92. package/dist/components/save-bar/SaveBar.d.ts +27 -0
  93. package/dist/components/save-bar/SaveBar.js +37 -0
  94. package/dist/components/save-bar/index.d.ts +2 -0
  95. package/dist/components/save-bar/index.js +1 -0
  96. package/dist/components/scroll-area/ScrollArea.d.ts +12 -0
  97. package/dist/components/scroll-area/ScrollArea.js +34 -0
  98. package/dist/components/scroll-area/index.d.ts +2 -0
  99. package/dist/components/scroll-area/index.js +1 -0
  100. package/dist/components/search-field/SearchField.js +1 -1
  101. package/dist/components/select/Select.d.ts +40 -0
  102. package/dist/components/select/Select.js +84 -0
  103. package/dist/components/select/index.d.ts +2 -0
  104. package/dist/components/select/index.js +1 -0
  105. package/dist/components/separator/Separator.d.ts +15 -0
  106. package/dist/components/separator/Separator.js +25 -0
  107. package/dist/components/separator/index.d.ts +2 -0
  108. package/dist/components/separator/index.js +1 -0
  109. package/dist/components/sheet/Sheet.d.ts +52 -0
  110. package/dist/components/sheet/Sheet.js +89 -0
  111. package/dist/components/sheet/index.d.ts +2 -0
  112. package/dist/components/sheet/index.js +1 -0
  113. package/dist/components/side-nav/SideNav.js +3 -3
  114. package/dist/components/summary-chips/SummaryChips.d.ts +17 -0
  115. package/dist/components/summary-chips/SummaryChips.js +19 -0
  116. package/dist/components/summary-chips/index.d.ts +2 -0
  117. package/dist/components/summary-chips/index.js +1 -0
  118. package/dist/components/switch/Switch.d.ts +10 -0
  119. package/dist/components/switch/Switch.js +20 -0
  120. package/dist/components/switch/index.d.ts +2 -0
  121. package/dist/components/switch/index.js +1 -0
  122. package/dist/components/table/Table.js +1 -1
  123. package/dist/components/tag-input/TagInput.d.ts +44 -0
  124. package/dist/components/tag-input/TagInput.js +102 -0
  125. package/dist/components/tag-input/index.d.ts +2 -0
  126. package/dist/components/tag-input/index.js +1 -0
  127. package/dist/components/textarea/Textarea.d.ts +15 -0
  128. package/dist/components/textarea/Textarea.js +21 -0
  129. package/dist/components/textarea/index.d.ts +2 -0
  130. package/dist/components/textarea/index.js +1 -0
  131. package/dist/components/top-bar/StoreSwitcher.js +1 -1
  132. package/dist/components/top-bar/TopBar.js +2 -2
  133. package/dist/components/unit-input/UnitInput.d.ts +33 -0
  134. package/dist/components/unit-input/UnitInput.js +47 -0
  135. package/dist/components/unit-input/index.d.ts +2 -0
  136. package/dist/components/unit-input/index.js +1 -0
  137. package/dist/index.d.ts +70 -9
  138. package/dist/index.js +38 -7
  139. package/dist/lib/cn.js +9 -2
  140. package/dist/lib/format.d.ts +65 -4
  141. package/dist/lib/format.js +163 -6
  142. package/dist/styles.css +1 -1
  143. package/package.json +129 -1
@@ -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,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";
@@ -1,7 +1,7 @@
1
- import { Button } from "../button/Button.js";
2
1
  import { Icon } from "../icon/Icon.js";
3
- import { RatioLink } from "../link-provider/RatioLink.js";
2
+ import { Button } from "../button/Button.js";
4
3
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../dropdown-menu/DropdownMenu.js";
4
+ import { Link } from "../link/Link.js";
5
5
  import { StatusPill } from "../status-pill/StatusPill.js";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  // src/components/page-header/PageHeader.tsx
@@ -13,13 +13,13 @@ function PageHeader({ title, icon, status, backHref, backLabel = "Back", actions
13
13
  const hasMenu = menuItems !== void 0 && menuItems.length > 0;
14
14
  return /* @__PURE__ */ jsxs("header", {
15
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]
16
+ children: [backHref !== void 0 && /* @__PURE__ */ jsx("div", {
17
+ className: "rt:flex rt:text-b1",
18
+ children: /* @__PURE__ */ jsx(Link, {
19
+ href: backHref,
20
+ back: true,
21
+ children: backLabel
22
+ })
23
23
  }), /* @__PURE__ */ jsxs("div", {
24
24
  className: "rt:flex rt:flex-wrap rt:items-center rt:justify-between rt:gap-x-3 rt:gap-y-2",
25
25
  children: [/* @__PURE__ */ jsxs("div", {
@@ -0,0 +1,25 @@
1
+ import { JSX } from "react";
2
+ /**
3
+ * The props of Progress: a value, or none for indeterminate, with an optional visible label and value.
4
+ * Without a visible label, pass aria-label so the bar has a name.
5
+ */
6
+ export type ProgressProps = {
7
+ /** The current value; leave it out or pass null for an indeterminate bar. */
8
+ value?: number | null;
9
+ /** The value at which the bar is full. Defaults to 100; for steps, the number of steps. */
10
+ max?: number;
11
+ /** A visible label, which also names the bar for screen readers. */
12
+ label?: string;
13
+ /** Shows the value on the right: a percent, or "value/max" with steps. */
14
+ showValue?: boolean;
15
+ /** The step variant: counts completed steps out of max, such as 0/5 for the setup guide. Implies showValue. */
16
+ steps?: boolean;
17
+ /** The accessible name when there is no visible label. */
18
+ 'aria-label'?: string;
19
+ };
20
+ /**
21
+ * The thin progress bar of question 5, 2px high, for page loads and refetches. Determinate with a value, indeterminate
22
+ * without one (a partial bar that moves only when the loading animation token names a motion, question 5). Base UI gives it the
23
+ * progressbar role, its label and its value text.
24
+ */
25
+ export declare function Progress({ value, max, label, showValue, steps, ...rest }: ProgressProps): JSX.Element;
@@ -0,0 +1,38 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Progress } from "@base-ui/react/progress";
3
+ // src/components/progress/Progress.tsx
4
+ /**
5
+ * The thin progress bar of question 5, 2px high, for page loads and refetches. Determinate with a value, indeterminate
6
+ * without one (a partial bar that moves only when the loading animation token names a motion, question 5). Base UI gives it the
7
+ * progressbar role, its label and its value text.
8
+ */
9
+ function Progress$1({ value = null, max = 100, label, showValue, steps, ...rest }) {
10
+ const withValue = showValue || steps;
11
+ return /* @__PURE__ */ jsxs(Progress.Root, {
12
+ "data-slot": "progress",
13
+ value,
14
+ max,
15
+ "aria-label": label ? void 0 : rest["aria-label"],
16
+ getAriaValueText: steps ? (_f, v) => v === null ? "In progress" : `${v} of ${max}` : void 0,
17
+ className: "rt:flex rt:flex-wrap rt:gap-2",
18
+ children: [
19
+ label ? /* @__PURE__ */ jsx(Progress.Label, {
20
+ className: "rt:text-b3 rt:text-foreground",
21
+ children: label
22
+ }) : null,
23
+ withValue ? /* @__PURE__ */ jsx(Progress.Value, {
24
+ className: "rt:ml-auto rt:text-b3 rt:text-muted-foreground rt:tabular-nums",
25
+ children: steps ? (_formatted, v) => v === null ? "" : `${v}/${max}` : void 0
26
+ }) : null,
27
+ /* @__PURE__ */ jsx(Progress.Track, {
28
+ "data-slot": "progress-track",
29
+ className: "rt:h-progress rt:w-full rt:overflow-hidden rt:bg-progress-track",
30
+ children: /* @__PURE__ */ jsx(Progress.Indicator, {
31
+ "data-slot": "progress-indicator",
32
+ className: "rt:h-full rt:bg-progress rt:data-indeterminate:w-1/3 rt:data-indeterminate:loading-motion"
33
+ })
34
+ })
35
+ ]
36
+ });
37
+ }
38
+ export { Progress$1 as Progress };
@@ -0,0 +1,2 @@
1
+ import { Progress, ProgressProps } from "./Progress.js";
2
+ export { Progress, type ProgressProps };
@@ -0,0 +1 @@
1
+ export { Progress } from "./Progress.js";
@@ -0,0 +1,18 @@
1
+ import { JSX } from "react";
2
+ import { Radio } from "@base-ui/react/radio";
3
+ import { RadioGroup } from "@base-ui/react/radio-group";
4
+ /** The props of RadioGroup: Base UI's radio group props, except className, style and render. Give it an aria-label or a label that points to it. */
5
+ export type RadioGroupProps = Omit<RadioGroup.Props, 'className' | 'style' | 'render'>;
6
+ /** The props of RadioGroupItem: Base UI's radio props, except className, style and render. Give it an aria-label or a label that points to it. */
7
+ export type RadioGroupItemProps = Omit<Radio.Root.Props, 'className' | 'style' | 'render'>;
8
+ /**
9
+ * A column of radio buttons where exactly one can be on. The arrow keys move the choice. aria-invalid on the group, as
10
+ * Field sets it, draws the error ring on every item.
11
+ */
12
+ declare function RadioGroup$1({ ...props }: RadioGroupProps): JSX.Element;
13
+ /**
14
+ * One choice in a RadioGroup: a 14px round control like the checkbox, drawn with a hairline ring and filled in the
15
+ * primary colour with a white dot when chosen. An invisible 24px area around it takes the tap.
16
+ */
17
+ export declare function RadioGroupItem({ ...props }: RadioGroupItemProps): JSX.Element;
18
+ export { RadioGroup$1 as RadioGroup };
@@ -0,0 +1,32 @@
1
+ import { cn } from "../../lib/cn.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { Radio } from "@base-ui/react/radio";
4
+ import { RadioGroup } from "@base-ui/react/radio-group";
5
+ // src/components/radio-group/RadioGroup.tsx
6
+ /**
7
+ * A column of radio buttons where exactly one can be on. The arrow keys move the choice. aria-invalid on the group, as
8
+ * Field sets it, draws the error ring on every item.
9
+ */
10
+ function RadioGroup$1({ ...props }) {
11
+ return /* @__PURE__ */ jsx(RadioGroup, {
12
+ "data-slot": "radio-group",
13
+ className: "rt:group/radio-group rt:grid rt:w-full rt:gap-2",
14
+ ...props
15
+ });
16
+ }
17
+ /**
18
+ * One choice in a RadioGroup: a 14px round control like the checkbox, drawn with a hairline ring and filled in the
19
+ * primary colour with a white dot when chosen. An invisible 24px area around it takes the tap.
20
+ */
21
+ function RadioGroupItem({ ...props }) {
22
+ return /* @__PURE__ */ jsx(Radio.Root, {
23
+ "data-slot": "radio-group-item",
24
+ className: cn("rt:relative rt:flex rt:size-3.5 rt:shrink-0 rt:cursor-pointer rt:items-center rt:justify-center rt:rounded-3xl rt:bg-background rt:shadow-ring-hairline rt:transition-colors rt:outline-none", "rt:after:absolute rt:after:-inset-1.25", "rt:hover:bg-primary-tint rt:data-checked:bg-primary rt:data-checked:shadow-none rt:data-checked:hover:bg-primary-hover", "rt:focus-visible:focus-ring rt:forced-outline", "rt:aria-invalid:shadow-ring-invalid rt:group-aria-invalid/radio-group:shadow-ring-invalid", "rt:data-disabled:cursor-not-allowed rt:data-disabled:opacity-50"),
25
+ ...props,
26
+ children: /* @__PURE__ */ jsx(Radio.Indicator, {
27
+ "data-slot": "radio-group-indicator",
28
+ className: "rt:block rt:size-1.5 rt:rounded-3xl rt:bg-primary-foreground"
29
+ })
30
+ });
31
+ }
32
+ export { RadioGroup$1 as RadioGroup, RadioGroupItem };
@@ -0,0 +1,2 @@
1
+ import { RadioGroup, RadioGroupItem, RadioGroupItemProps, RadioGroupProps } from "./RadioGroup.js";
2
+ export { RadioGroup, RadioGroupItem, type RadioGroupItemProps, type RadioGroupProps };
@@ -0,0 +1 @@
1
+ export { RadioGroup, RadioGroupItem } from "./RadioGroup.js";
@@ -0,0 +1,27 @@
1
+ import { HTMLAttributes, JSX, ReactNode, Ref } from "react";
2
+ /** The props of SaveBar: the HTML attributes of its div, except className, style, children and onSave. */
3
+ export type SaveBarProps = Omit<HTMLAttributes<HTMLDivElement>, 'className' | 'style' | 'children'> & {
4
+ /** Whether the form has unsaved changes. The bar renders nothing when false. */
5
+ dirty: boolean;
6
+ /** True while the save is running: Save is disabled and reads the saving label. */
7
+ saving?: boolean;
8
+ /** Called by Save. */
9
+ onSave: () => void;
10
+ /** Called by Discard. The bar asks nothing itself: confirm in the caller if you need to. */
11
+ onDiscard: () => void;
12
+ /** The message. Defaults to "Unsaved changes". */
13
+ message?: ReactNode;
14
+ /** The Save label. Defaults to "Save". */
15
+ saveLabel?: ReactNode;
16
+ /** The Save label while saving. Defaults to "Saving…". */
17
+ savingLabel?: ReactNode;
18
+ /** The Discard label. Defaults to "Discard". */
19
+ discardLabel?: ReactNode;
20
+ /** Receives the rendered div. */
21
+ ref?: Ref<HTMLDivElement>;
22
+ };
23
+ /**
24
+ * The bar that appears under the top bar when a form is dirty: a message, Discard and Save. It renders in flow, so the
25
+ * page (AppShell) decides where it sits. While saving, Save is disabled; Discard only calls onDiscard.
26
+ */
27
+ export declare function SaveBar({ dirty, saving, onSave, onDiscard, message, saveLabel, savingLabel, discardLabel, ref, ...rest }: SaveBarProps): JSX.Element | null;
@@ -0,0 +1,37 @@
1
+ import { cn } from "../../lib/cn.js";
2
+ import { Text } from "../text/Text.js";
3
+ import { Button } from "../button/Button.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ // src/components/save-bar/SaveBar.tsx
6
+ /**
7
+ * The bar that appears under the top bar when a form is dirty: a message, Discard and Save. It renders in flow, so the
8
+ * page (AppShell) decides where it sits. While saving, Save is disabled; Discard only calls onDiscard.
9
+ */
10
+ function SaveBar({ dirty, saving = false, onSave, onDiscard, message = "Unsaved changes", saveLabel = "Save", savingLabel = "Saving…", discardLabel = "Discard", ref, ...rest }) {
11
+ if (!dirty) return null;
12
+ return /* @__PURE__ */ jsxs("div", {
13
+ role: "status",
14
+ "data-slot": "save-bar",
15
+ ...rest,
16
+ ref,
17
+ "aria-busy": saving || void 0,
18
+ className: cn("rt:flex rt:w-full rt:min-w-0 rt:items-center rt:justify-between rt:gap-3 rt:border-b rt:border-border rt:bg-background rt:px-3 rt:py-2"),
19
+ children: [/* @__PURE__ */ jsx(Text, {
20
+ variant: "b4",
21
+ children: message
22
+ }), /* @__PURE__ */ jsxs("div", {
23
+ className: "rt:flex rt:shrink-0 rt:items-center rt:gap-2",
24
+ children: [/* @__PURE__ */ jsx(Button, {
25
+ variant: "secondary",
26
+ onClick: onDiscard,
27
+ children: discardLabel
28
+ }), /* @__PURE__ */ jsx(Button, {
29
+ variant: "main",
30
+ disabled: saving,
31
+ onClick: onSave,
32
+ children: saving ? savingLabel : saveLabel
33
+ })]
34
+ })]
35
+ });
36
+ }
37
+ export { SaveBar };
@@ -0,0 +1,2 @@
1
+ import { SaveBar, SaveBarProps } from "./SaveBar.js";
2
+ export { SaveBar, type SaveBarProps };
@@ -0,0 +1 @@
1
+ export { SaveBar } from "./SaveBar.js";