@ratio-ui/core 0.2.0 → 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 (95) hide show
  1. package/dist/catalogue.json +1091 -10
  2. package/dist/components/alert/Alert.d.ts +18 -0
  3. package/dist/components/alert/Alert.js +67 -0
  4. package/dist/components/alert/index.d.ts +2 -0
  5. package/dist/components/alert/index.js +1 -0
  6. package/dist/components/alert-dialog/AlertDialog.d.ts +48 -0
  7. package/dist/components/alert-dialog/AlertDialog.js +76 -0
  8. package/dist/components/alert-dialog/index.d.ts +2 -0
  9. package/dist/components/alert-dialog/index.js +1 -0
  10. package/dist/components/app-shell/AppShell.js +1 -1
  11. package/dist/components/calendar/Calendar.d.ts +20 -0
  12. package/dist/components/calendar/Calendar.js +119 -0
  13. package/dist/components/calendar/index.d.ts +2 -0
  14. package/dist/components/calendar/index.js +1 -0
  15. package/dist/components/checkbox-list/CheckboxList.d.ts +36 -0
  16. package/dist/components/checkbox-list/CheckboxList.js +93 -0
  17. package/dist/components/checkbox-list/index.d.ts +2 -0
  18. package/dist/components/checkbox-list/index.js +1 -0
  19. package/dist/components/collapsible/Collapsible.d.ts +24 -0
  20. package/dist/components/collapsible/Collapsible.js +58 -0
  21. package/dist/components/collapsible/index.d.ts +2 -0
  22. package/dist/components/collapsible/index.js +1 -0
  23. package/dist/components/data-table/DataTable.js +1 -1
  24. package/dist/components/date-picker/DatePicker.d.ts +33 -0
  25. package/dist/components/date-picker/DatePicker.js +104 -0
  26. package/dist/components/date-picker/index.d.ts +2 -0
  27. package/dist/components/date-picker/index.js +1 -0
  28. package/dist/components/date-range-picker/DateRangePicker.d.ts +39 -0
  29. package/dist/components/date-range-picker/DateRangePicker.js +142 -0
  30. package/dist/components/date-range-picker/index.d.ts +2 -0
  31. package/dist/components/date-range-picker/index.js +1 -0
  32. package/dist/components/field/Field.d.ts +35 -0
  33. package/dist/components/field/Field.js +67 -0
  34. package/dist/components/field/Label.d.ts +8 -0
  35. package/dist/components/field/Label.js +13 -0
  36. package/dist/components/field/index.d.ts +3 -0
  37. package/dist/components/field/index.js +3 -0
  38. package/dist/components/filter-tabs/FilterTabs.js +1 -1
  39. package/dist/components/global-search/GlobalSearch.js +1 -1
  40. package/dist/components/icon/registry.d.ts +2 -2
  41. package/dist/components/icon/registry.js +5 -1
  42. package/dist/components/input-group/InputGroup.js +1 -1
  43. package/dist/components/link/Link.d.ts +22 -0
  44. package/dist/components/link/Link.js +31 -0
  45. package/dist/components/link/index.d.ts +2 -0
  46. package/dist/components/link/index.js +1 -0
  47. package/dist/components/media-grid/MediaGrid.d.ts +49 -0
  48. package/dist/components/media-grid/MediaGrid.js +303 -0
  49. package/dist/components/media-grid/index.d.ts +2 -0
  50. package/dist/components/media-grid/index.js +1 -0
  51. package/dist/components/money-input/MoneyInput.d.ts +30 -0
  52. package/dist/components/money-input/MoneyInput.js +72 -0
  53. package/dist/components/money-input/index.d.ts +2 -0
  54. package/dist/components/money-input/index.js +1 -0
  55. package/dist/components/page-header/PageHeader.js +1 -1
  56. package/dist/components/save-bar/SaveBar.d.ts +27 -0
  57. package/dist/components/save-bar/SaveBar.js +37 -0
  58. package/dist/components/save-bar/index.d.ts +2 -0
  59. package/dist/components/save-bar/index.js +1 -0
  60. package/dist/components/search-field/SearchField.js +1 -1
  61. package/dist/components/select/Select.d.ts +40 -0
  62. package/dist/components/select/Select.js +84 -0
  63. package/dist/components/select/index.d.ts +2 -0
  64. package/dist/components/select/index.js +1 -0
  65. package/dist/components/side-nav/SideNav.js +3 -3
  66. package/dist/components/summary-chips/SummaryChips.d.ts +17 -0
  67. package/dist/components/summary-chips/SummaryChips.js +19 -0
  68. package/dist/components/summary-chips/index.d.ts +2 -0
  69. package/dist/components/summary-chips/index.js +1 -0
  70. package/dist/components/switch/Switch.d.ts +10 -0
  71. package/dist/components/switch/Switch.js +20 -0
  72. package/dist/components/switch/index.d.ts +2 -0
  73. package/dist/components/switch/index.js +1 -0
  74. package/dist/components/table/Table.js +1 -1
  75. package/dist/components/tag-input/TagInput.d.ts +44 -0
  76. package/dist/components/tag-input/TagInput.js +102 -0
  77. package/dist/components/tag-input/index.d.ts +2 -0
  78. package/dist/components/tag-input/index.js +1 -0
  79. package/dist/components/textarea/Textarea.d.ts +15 -0
  80. package/dist/components/textarea/Textarea.js +21 -0
  81. package/dist/components/textarea/index.d.ts +2 -0
  82. package/dist/components/textarea/index.js +1 -0
  83. package/dist/components/top-bar/StoreSwitcher.js +1 -1
  84. package/dist/components/top-bar/TopBar.js +2 -2
  85. package/dist/components/unit-input/UnitInput.d.ts +33 -0
  86. package/dist/components/unit-input/UnitInput.js +47 -0
  87. package/dist/components/unit-input/index.d.ts +2 -0
  88. package/dist/components/unit-input/index.js +1 -0
  89. package/dist/index.d.ts +43 -6
  90. package/dist/index.js +26 -7
  91. package/dist/lib/cn.js +7 -1
  92. package/dist/lib/format.d.ts +65 -4
  93. package/dist/lib/format.js +163 -6
  94. package/dist/styles.css +1 -1
  95. package/package.json +78 -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,5 +1,5 @@
1
- import { Button } from "../button/Button.js";
2
1
  import { Icon } from "../icon/Icon.js";
2
+ import { Button } from "../button/Button.js";
3
3
  import { RatioLink } from "../link-provider/RatioLink.js";
4
4
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../dropdown-menu/DropdownMenu.js";
5
5
  import { StatusPill } from "../status-pill/StatusPill.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";
@@ -1,7 +1,7 @@
1
1
  import { Icon } from "../icon/Icon.js";
2
2
  import { InputGroup, InputGroupAddon, InputGroupInput } from "../input-group/InputGroup.js";
3
- import { useRef, useState } from "react";
4
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { useRef, useState } from "react";
5
5
  // src/components/search-field/SearchField.tsx
6
6
  /**
7
7
  * The search field above a listing: a search icon, the input and a clear button that appears once there is text.
@@ -0,0 +1,40 @@
1
+ import { JSX } from "react";
2
+ import { Select } from "@base-ui/react/select";
3
+ /** The props of Select: Base UI's root props, such as value, defaultValue, onValueChange, items, name and disabled. */
4
+ export type SelectProps<Value = string> = Select.Root.Props<Value>;
5
+ /** A choice from a fixed list. Pass items ({ label, value }[]) so the trigger shows the label of the chosen value. Put a SelectTrigger and a SelectContent inside it. */
6
+ declare function Select$1<Value = string>(props: SelectProps<Value>): JSX.Element;
7
+ /** The height of the trigger in px: 36 for forms, 32 for fields inside a table. */
8
+ export type SelectSize = 36 | 32;
9
+ /** The props of SelectTrigger: Base UI's trigger props except className and style, with the height. Mark an invalid field with aria-invalid. */
10
+ export type SelectTriggerProps = Omit<Select.Trigger.Props, 'className' | 'style'> & {
11
+ /** The height in px: 36 for forms, 32 inside a table. Defaults to 36. */
12
+ size?: SelectSize;
13
+ };
14
+ /** The closed field: the chosen value (or the placeholder) and a chevron at the right. Put a SelectValue inside it. */
15
+ export declare function SelectTrigger({ size, children, ...props }: SelectTriggerProps): JSX.Element;
16
+ /** The props of SelectValue: Base UI's value props except className and style. */
17
+ export type SelectValueProps = Omit<Select.Value.Props, 'className' | 'style'>;
18
+ /** Shows the label of the chosen option inside the trigger, or the placeholder while nothing is chosen. */
19
+ export declare function SelectValue(props: SelectValueProps): JSX.Element;
20
+ /** The props of SelectContent: the popup props plus where the list sits. */
21
+ export type SelectContentProps = Omit<Select.Popup.Props, 'className' | 'style'> & Pick<Select.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'>;
22
+ /** The list: the popover look (8px radius, outline, no shadow, 6px 0 padding), as wide as the trigger. It opens below the trigger. */
23
+ export declare function SelectContent({ align, alignOffset, side, sideOffset, children, ...props }: SelectContentProps): JSX.Element;
24
+ /** The props of SelectGroup: Base UI's group props except className and style. */
25
+ export type SelectGroupProps = Omit<Select.Group.Props, 'className' | 'style'>;
26
+ /** Groups related options under a SelectLabel. */
27
+ export declare function SelectGroup(props: SelectGroupProps): JSX.Element;
28
+ /** The props of SelectLabel: Base UI's group label props except className and style. */
29
+ export type SelectLabelProps = Omit<Select.GroupLabel.Props, 'className' | 'style'>;
30
+ /** The heading of a group, in the muted colour. */
31
+ export declare function SelectLabel(props: SelectLabelProps): JSX.Element;
32
+ /** The props of SelectItem: Base UI's item props except className and style. The value is the one the select reports. */
33
+ export type SelectItemProps = Omit<Select.Item.Props, 'className' | 'style'>;
34
+ /** One option. The highlighted one has the accent background, the chosen one a check at its end, a disabled one is dimmed and cannot be chosen. */
35
+ export declare function SelectItem({ children, ...props }: SelectItemProps): JSX.Element;
36
+ /** The props of SelectSeparator: Base UI's separator props except className and style. */
37
+ export type SelectSeparatorProps = Omit<Select.Separator.Props, 'className' | 'style'>;
38
+ /** A hairline between groups. */
39
+ export declare function SelectSeparator(props: SelectSeparatorProps): JSX.Element;
40
+ export { Select$1 as Select };
@@ -0,0 +1,84 @@
1
+ import { cn } from "../../lib/cn.js";
2
+ import { Icon } from "../icon/Icon.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { cva } from "class-variance-authority";
5
+ import { Select } from "@base-ui/react/select";
6
+ // src/components/select/Select.tsx
7
+ /** A choice from a fixed list. Pass items ({ label, value }[]) so the trigger shows the label of the chosen value. Put a SelectTrigger and a SelectContent inside it. */
8
+ function Select$1(props) {
9
+ return /* @__PURE__ */ jsx(Select.Root, { ...props });
10
+ }
11
+ const triggerVariants = cva("rt:flex rt:w-full rt:min-w-0 rt:items-center rt:justify-between rt:gap-2 rt:rounded-lg rt:bg-background rt:text-b3 rt:text-foreground rt:whitespace-nowrap rt:shadow-ring-subtle rt:outline-none rt:select-none rt:transition-colors rt:forced-outline rt:focus-visible:shadow-ring-focus rt:aria-invalid:shadow-ring-invalid rt:aria-invalid:focus-visible:shadow-ring-invalid rt:data-placeholder:text-muted-foreground rt:disabled:pointer-events-none rt:disabled:cursor-not-allowed rt:disabled:opacity-50", {
12
+ variants: { size: {
13
+ 36: "rt:h-9 rt:pr-8 rt:pl-3",
14
+ 32: "rt:h-8 rt:pr-8 rt:pl-2.75"
15
+ } },
16
+ defaultVariants: { size: 36 }
17
+ });
18
+ /** The closed field: the chosen value (or the placeholder) and a chevron at the right. Put a SelectValue inside it. */
19
+ function SelectTrigger({ size = 36, children, ...props }) {
20
+ return /* @__PURE__ */ jsxs(Select.Trigger, {
21
+ className: cn(triggerVariants({ size }), "rt:relative"),
22
+ ...props,
23
+ children: [children, /* @__PURE__ */ jsx(Select.Icon, {
24
+ render: /* @__PURE__ */ jsx("span", { className: "rt:pointer-events-none rt:absolute rt:right-3 rt:flex rt:text-muted-foreground" }),
25
+ children: /* @__PURE__ */ jsx(Icon, { name: "nav-caret-down" })
26
+ })]
27
+ });
28
+ }
29
+ /** Shows the label of the chosen option inside the trigger, or the placeholder while nothing is chosen. */
30
+ function SelectValue(props) {
31
+ return /* @__PURE__ */ jsx(Select.Value, {
32
+ className: "rt:min-w-0 rt:flex-1 rt:truncate rt:text-left",
33
+ ...props
34
+ });
35
+ }
36
+ /** The list: the popover look (8px radius, outline, no shadow, 6px 0 padding), as wide as the trigger. It opens below the trigger. */
37
+ function SelectContent({ align = "start", alignOffset = 0, side = "bottom", sideOffset = 4, children, ...props }) {
38
+ return /* @__PURE__ */ jsx(Select.Portal, { children: /* @__PURE__ */ jsx(Select.Positioner, {
39
+ align,
40
+ alignOffset,
41
+ side,
42
+ sideOffset,
43
+ alignItemWithTrigger: false,
44
+ className: "rt:isolate rt:z-50",
45
+ children: /* @__PURE__ */ jsx(Select.Popup, {
46
+ className: "rt:max-h-available rt:min-w-anchor rt:overflow-y-auto rt:rounded-lg rt:border rt:border-border rt:bg-popover rt:py-1.5 rt:text-b3 rt:text-popover-foreground rt:outline-hidden rt:transition-colors",
47
+ ...props,
48
+ children: /* @__PURE__ */ jsx(Select.List, { children })
49
+ })
50
+ }) });
51
+ }
52
+ /** Groups related options under a SelectLabel. */
53
+ function SelectGroup(props) {
54
+ return /* @__PURE__ */ jsx(Select.Group, { ...props });
55
+ }
56
+ /** The heading of a group, in the muted colour. */
57
+ function SelectLabel(props) {
58
+ return /* @__PURE__ */ jsx(Select.GroupLabel, {
59
+ className: "rt:px-3 rt:py-1 rt:text-label rt:uppercase rt:text-foreground-subdued",
60
+ ...props
61
+ });
62
+ }
63
+ /** One option. The highlighted one has the accent background, the chosen one a check at its end, a disabled one is dimmed and cannot be chosen. */
64
+ function SelectItem({ children, ...props }) {
65
+ return /* @__PURE__ */ jsxs(Select.Item, {
66
+ className: "rt:relative rt:flex rt:h-7 rt:cursor-default rt:items-center rt:gap-2 rt:pr-8 rt:pl-3 rt:text-b3 rt:text-foreground rt:outline-none rt:select-none rt:transition-colors rt:data-highlighted:bg-accent rt:data-highlighted:text-accent-foreground rt:data-disabled:pointer-events-none rt:data-disabled:opacity-50",
67
+ ...props,
68
+ children: [/* @__PURE__ */ jsx(Select.ItemText, {
69
+ className: "rt:flex-1 rt:truncate",
70
+ children
71
+ }), /* @__PURE__ */ jsx(Select.ItemIndicator, {
72
+ render: /* @__PURE__ */ jsx("span", { className: "rt:pointer-events-none rt:absolute rt:right-3 rt:flex" }),
73
+ children: /* @__PURE__ */ jsx(Icon, { name: "check" })
74
+ })]
75
+ });
76
+ }
77
+ /** A hairline between groups. */
78
+ function SelectSeparator(props) {
79
+ return /* @__PURE__ */ jsx(Select.Separator, {
80
+ className: "rt:my-1.5 rt:h-px rt:bg-divider",
81
+ ...props
82
+ });
83
+ }
84
+ export { Select$1 as Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue };
@@ -0,0 +1,2 @@
1
+ import { Select, SelectContent, SelectContentProps, SelectGroup, SelectGroupProps, SelectItem, SelectItemProps, SelectLabel, SelectLabelProps, SelectProps, SelectSeparator, SelectSeparatorProps, SelectSize, SelectTrigger, SelectTriggerProps, SelectValue, SelectValueProps } from "./Select.js";
2
+ export { Select, SelectContent, type SelectContentProps, SelectGroup, type SelectGroupProps, SelectItem, type SelectItemProps, SelectLabel, type SelectLabelProps, type SelectProps, SelectSeparator, type SelectSeparatorProps, type SelectSize, SelectTrigger, type SelectTriggerProps, SelectValue, type SelectValueProps };
@@ -0,0 +1 @@
1
+ export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from "./Select.js";