@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,92 @@
1
+ import { Icon } from "../icon/Icon.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useMemo } from "react";
4
+ import { Combobox } from "@base-ui/react/combobox";
5
+ // src/components/combobox/Combobox.tsx
6
+ const toOption = (item) => typeof item === "string" ? {
7
+ value: item,
8
+ label: item
9
+ } : item;
10
+ const ROOT_CLASSES = {
11
+ field: "rt:flex rt:h-9 rt:w-full rt:min-w-0 rt:items-center rt:gap-0.5 rt:rounded-lg rt:bg-background rt:pr-1.5 rt:shadow-ring-subtle rt:transition-colors rt:forced-outline rt:focus-within:shadow-ring-focus rt:has-aria-invalid:shadow-ring-invalid rt:has-aria-invalid:focus-within:shadow-ring-invalid rt:has-disabled:opacity-50",
12
+ input: "rt:h-full rt:min-w-0 rt:flex-1 rt:bg-transparent rt:pl-3 rt:text-b3 rt:text-foreground rt:placeholder:text-muted-foreground rt:outline-none rt:disabled:cursor-not-allowed",
13
+ button: "rt:flex rt:size-6 rt:items-center rt:justify-center rt:rounded-md rt:text-muted-foreground rt:outline-none rt:hover:text-foreground rt:focus-visible:focus-ring rt:disabled:pointer-events-none",
14
+ popup: "rt:group/combobox-content rt:max-h-available rt:w-anchor rt:max-w-available rt:overflow-hidden rt:rounded-lg rt:border rt:border-border rt:bg-popover rt:text-b3 rt:text-popover-foreground rt:outline-hidden rt:transition-colors",
15
+ item: "rt:relative rt:flex rt:h-7 rt:w-full 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"
16
+ };
17
+ /**
18
+ * A field to choose one option from a list, with a search: type to narrow the list, pick with the mouse or Enter.
19
+ * The field looks like an Input and the list like a Popover panel. For several choices shown as chips, use TagInput.
20
+ * Mark it invalid with aria-invalid; give the list an empty text with emptyText. Inside a Field, the label, hint, error
21
+ * and required mark reach the input.
22
+ */
23
+ function Combobox$1({ items, value, defaultValue, onValueChange, placeholder, emptyText = "No results found", "aria-invalid": ariaInvalid, invalid = false, clearable = true, disabled = false, name, id, ref, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-required": ariaRequired }) {
24
+ const isInvalid = invalid || ariaInvalid === true || ariaInvalid === "true";
25
+ const options = items.map(toOption);
26
+ const key = JSON.stringify(options);
27
+ const labels = useMemo(() => new Map(JSON.parse(key).map((o) => [o.value, o.label])), [key]);
28
+ const root = {
29
+ items: useMemo(() => [...labels.keys()], [labels]),
30
+ disabled,
31
+ name,
32
+ itemToStringLabel: (v) => labels.get(v) ?? v,
33
+ onValueChange: (v, details) => onValueChange?.(v, details)
34
+ };
35
+ const controlled = value !== void 0 ? { value } : {};
36
+ const initial = defaultValue !== void 0 ? { defaultValue } : {};
37
+ return /* @__PURE__ */ jsxs(Combobox.Root, {
38
+ ...root,
39
+ ...controlled,
40
+ ...initial,
41
+ children: [/* @__PURE__ */ jsxs("div", {
42
+ "data-slot": "combobox",
43
+ className: ROOT_CLASSES.field,
44
+ children: [/* @__PURE__ */ jsx(Combobox.Input, {
45
+ ref,
46
+ id,
47
+ "aria-label": ariaLabel,
48
+ "aria-labelledby": ariaLabelledBy,
49
+ "aria-describedby": ariaDescribedBy,
50
+ "aria-required": ariaRequired,
51
+ "aria-invalid": isInvalid || void 0,
52
+ placeholder,
53
+ className: ROOT_CLASSES.input
54
+ }), /* @__PURE__ */ jsxs("div", {
55
+ className: "rt:flex rt:items-center rt:gap-0.5",
56
+ children: [clearable ? /* @__PURE__ */ jsx(Combobox.Clear, {
57
+ "aria-label": "Clear",
58
+ className: ROOT_CLASSES.button,
59
+ children: /* @__PURE__ */ jsx(Icon, { name: "x" })
60
+ }) : null, /* @__PURE__ */ jsx(Combobox.Trigger, {
61
+ "aria-label": "Show options",
62
+ className: ROOT_CLASSES.button,
63
+ children: /* @__PURE__ */ jsx(Icon, { name: "nav-caret-down" })
64
+ })]
65
+ })]
66
+ }), /* @__PURE__ */ jsx(Combobox.Portal, { children: /* @__PURE__ */ jsx(Combobox.Positioner, {
67
+ side: "bottom",
68
+ align: "start",
69
+ sideOffset: 4,
70
+ className: "rt:isolate rt:z-50",
71
+ children: /* @__PURE__ */ jsxs(Combobox.Popup, {
72
+ "data-slot": "combobox-content",
73
+ className: ROOT_CLASSES.popup,
74
+ children: [/* @__PURE__ */ jsx(Combobox.Empty, {
75
+ className: "rt:hidden rt:justify-center rt:py-3 rt:text-center rt:text-muted-foreground rt:group-data-empty/combobox-content:flex",
76
+ children: emptyText
77
+ }), /* @__PURE__ */ jsx(Combobox.List, {
78
+ className: "rt:max-h-72 rt:overflow-y-auto rt:overscroll-contain rt:py-1.5 rt:data-empty:py-0",
79
+ children: (v) => /* @__PURE__ */ jsxs(Combobox.Item, {
80
+ value: v,
81
+ className: ROOT_CLASSES.item,
82
+ children: [labels.get(v), /* @__PURE__ */ jsx(Combobox.ItemIndicator, {
83
+ className: "rt:pointer-events-none rt:absolute rt:right-3 rt:flex",
84
+ children: /* @__PURE__ */ jsx(Icon, { name: "check" })
85
+ })]
86
+ }, v)
87
+ })]
88
+ })
89
+ }) })]
90
+ });
91
+ }
92
+ export { Combobox$1 as Combobox };
@@ -0,0 +1,2 @@
1
+ import { Combobox, ComboboxItem, ComboboxProps } from "./Combobox.js";
2
+ export { Combobox, type ComboboxItem, type ComboboxProps };
@@ -0,0 +1 @@
1
+ export { Combobox } from "./Combobox.js";
@@ -1,9 +1,9 @@
1
- import { Checkbox } from "../checkbox/Checkbox.js";
2
1
  import { RatioLink } from "../link-provider/RatioLink.js";
2
+ import { Checkbox } from "../checkbox/Checkbox.js";
3
3
  import { Pagination } from "../pagination/Pagination.js";
4
4
  import { Table, TableBody, TableBox, TableCell, TableHead, TableHeader, TableRow } from "../table/Table.js";
5
- import { useMemo, useSyncExternalStore } from "react";
6
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { useMemo, useSyncExternalStore } from "react";
7
7
  import { FlexRender, rowSelectionFeature, rowSortingFeature, tableFeatures, useTable } from "@tanstack/react-table";
8
8
  // src/components/data-table/DataTable.tsx
9
9
  const features = tableFeatures({
@@ -0,0 +1,34 @@
1
+ import { CalendarProps } from "../calendar/Calendar.js";
2
+ import "../calendar/index.js";
3
+ import { InputGroupSize } from "../input-group/InputGroup.js";
4
+ import "../input-group/index.js";
5
+ import { InputHTMLAttributes, JSX, Ref } from "react";
6
+ /**
7
+ * The props of DatePicker: the HTML attributes of an input, except className, style, size and the ones the field sets itself,
8
+ * with the day as a Date in and out. Mark an invalid field with aria-invalid, which draws the error ring around the group.
9
+ */
10
+ export type DatePickerProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'style' | 'size' | 'type' | 'value' | 'defaultValue' | 'onChange' | 'min' | 'max'> & {
11
+ /** The day, or null for an empty field. Pass it to control the field. */
12
+ value?: Date | null;
13
+ /** The starting day when the field is not controlled. Defaults to empty. */
14
+ defaultValue?: Date | null;
15
+ /** Called with the day when a typed date is complete or a day is picked, and with null when the text is cleared or is not a date. */
16
+ onValueChange?: (value: Date | null) => void;
17
+ /** The days that cannot be picked in the calendar, for example { before: new Date() }. */
18
+ disabledDays?: CalendarProps['disabled'];
19
+ /** The first month the calendar can show. */
20
+ startMonth?: Date;
21
+ /** The last month the calendar can show. */
22
+ endMonth?: Date;
23
+ /** The height in px: 36 for forms, 32 for fields inside a table. Defaults to 36. */
24
+ size?: InputGroupSize;
25
+ /** Receives the input element. */
26
+ ref?: Ref<HTMLInputElement>;
27
+ };
28
+ /**
29
+ * A field that shows a day as DD/MM/YYYY, reads a typed date through the format module and opens a calendar in a popover
30
+ * from the button at its end. Text that is not a date reports null (as soon as it is typed over a day, and when the field
31
+ * loses focus) and is marked invalid, aria-invalid, when the field loses focus; the app shows Field's error from the null.
32
+ * Under a name, a form submits the day as YYYY-MM-DD (empty when there is none), never the text on screen.
33
+ */
34
+ export declare function DatePicker({ value, defaultValue, onValueChange, disabledDays, startMonth, endMonth, size, placeholder, readOnly, disabled, onBlur, ref, name, 'aria-invalid': ariaInvalid, ...rest }: DatePickerProps): JSX.Element;
@@ -0,0 +1,108 @@
1
+ import { Icon } from "../icon/Icon.js";
2
+ import { Button } from "../button/Button.js";
3
+ import { FORMAT, formatDate, formatIsoDate, parseDate } from "../../lib/format.js";
4
+ import { Calendar } from "../calendar/Calendar.js";
5
+ import { InputGroup, InputGroupAddon, InputGroupInput } from "../input-group/InputGroup.js";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "../popover/Popover.js";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { useState } from "react";
9
+ // src/components/date-picker/DatePicker.tsx
10
+ /**
11
+ * A field that shows a day as DD/MM/YYYY, reads a typed date through the format module and opens a calendar in a popover
12
+ * from the button at its end. Text that is not a date reports null (as soon as it is typed over a day, and when the field
13
+ * loses focus) and is marked invalid, aria-invalid, when the field loses focus; the app shows Field's error from the null.
14
+ * Under a name, a form submits the day as YYYY-MM-DD (empty when there is none), never the text on screen.
15
+ */
16
+ function DatePicker({ value, defaultValue = null, onValueChange, disabledDays, startMonth, endMonth, size = 36, placeholder = FORMAT.datePattern, readOnly, disabled, onBlur, ref, name, "aria-invalid": ariaInvalid, ...rest }) {
17
+ const controlled = value !== void 0;
18
+ const [inner, setInner] = useState(defaultValue);
19
+ const day = controlled ? value : inner;
20
+ const [text, setText] = useState(() => day === null ? "" : formatDate(day));
21
+ const [seen, setSeen] = useState(day);
22
+ const [malformed, setMalformed] = useState(false);
23
+ const [open, setOpen] = useState(false);
24
+ if ((seen?.getTime() ?? null) !== (day?.getTime() ?? null)) {
25
+ setSeen(day);
26
+ if ((day?.getTime() ?? null) !== (parseDate(text)?.getTime() ?? null)) setText(day === null ? "" : formatDate(day));
27
+ setMalformed(false);
28
+ }
29
+ const commit = (next) => {
30
+ setSeen(next);
31
+ if (!controlled) setInner(next);
32
+ onValueChange?.(next);
33
+ };
34
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(InputGroup, {
35
+ size,
36
+ children: [/* @__PURE__ */ jsx(InputGroupInput, {
37
+ ...rest,
38
+ ref,
39
+ type: "text",
40
+ autoComplete: "off",
41
+ placeholder,
42
+ readOnly,
43
+ disabled,
44
+ "aria-invalid": ariaInvalid ?? (malformed ? true : void 0),
45
+ value: text,
46
+ onChange: (event) => {
47
+ const next = event.target.value;
48
+ setText(next);
49
+ setMalformed(false);
50
+ if (next.trim() === "") return commit(null);
51
+ const parsed = parseDate(next);
52
+ if (parsed !== null) commit(parsed);
53
+ else if (day !== null) commit(null);
54
+ },
55
+ onBlur: (event) => {
56
+ onBlur?.(event);
57
+ const parsed = parseDate(text);
58
+ if (text.trim() === "") setMalformed(false);
59
+ else if (parsed === null) {
60
+ setMalformed(true);
61
+ commit(null);
62
+ } else setText(formatDate(parsed));
63
+ }
64
+ }), readOnly ? null : /* @__PURE__ */ jsx(InputGroupAddon, {
65
+ align: "end",
66
+ children: /* @__PURE__ */ jsxs(Popover, {
67
+ open,
68
+ onOpenChange: setOpen,
69
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
70
+ disabled,
71
+ render: /* @__PURE__ */ jsx(Button, {
72
+ variant: "ghost",
73
+ iconOnly: true,
74
+ "aria-label": "Open calendar",
75
+ children: /* @__PURE__ */ jsx(Icon, {
76
+ name: "calendar",
77
+ size: 12
78
+ })
79
+ })
80
+ }), /* @__PURE__ */ jsx(PopoverContent, {
81
+ align: "end",
82
+ children: /* @__PURE__ */ jsx(Calendar, {
83
+ mode: "single",
84
+ required: true,
85
+ selected: day ?? void 0,
86
+ defaultMonth: day ?? void 0,
87
+ onSelect: (picked) => {
88
+ setText(formatDate(picked));
89
+ setMalformed(false);
90
+ commit(picked);
91
+ setOpen(false);
92
+ },
93
+ disabled: disabledDays,
94
+ startMonth,
95
+ endMonth
96
+ })
97
+ })]
98
+ })
99
+ })]
100
+ }), name !== void 0 && /* @__PURE__ */ jsx("input", {
101
+ type: "hidden",
102
+ name,
103
+ form: rest.form,
104
+ value: day === null ? "" : formatIsoDate(day),
105
+ disabled
106
+ })] });
107
+ }
108
+ export { DatePicker };
@@ -0,0 +1,2 @@
1
+ import { DatePicker, DatePickerProps } from "./DatePicker.js";
2
+ export { DatePicker, type DatePickerProps };
@@ -0,0 +1 @@
1
+ export { DatePicker } from "./DatePicker.js";
@@ -0,0 +1,40 @@
1
+ import { CalendarProps } from "../calendar/Calendar.js";
2
+ import "../calendar/index.js";
3
+ import { InputGroupSize } from "../input-group/InputGroup.js";
4
+ import "../input-group/index.js";
5
+ import { InputHTMLAttributes, JSX, Ref } from "react";
6
+ /** A complete range of days, the first day and the last day (the same day for a range of one). */
7
+ export type DateRangeValue = {
8
+ from: Date;
9
+ to: Date;
10
+ };
11
+ /**
12
+ * The props of DateRangePicker: the HTML attributes of an input, except className, style, size and the ones the field sets
13
+ * itself, with the range as a first and a last day in and out. Mark an invalid field with aria-invalid.
14
+ */
15
+ export type DateRangePickerProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'style' | 'size' | 'type' | 'value' | 'defaultValue' | 'onChange' | 'min' | 'max'> & {
16
+ /** The range, or null for an empty field. Pass it to control the field. */
17
+ value?: DateRangeValue | null;
18
+ /** The starting range when the field is not controlled. Defaults to empty. */
19
+ defaultValue?: DateRangeValue | null;
20
+ /** Called with the range when a typed range is complete or two days are picked, and with null when the text is cleared or is not a range. */
21
+ onValueChange?: (value: DateRangeValue | null) => void;
22
+ /** The days that cannot be picked in the calendar, for example { after: new Date() } for an analytics period. */
23
+ disabledDays?: CalendarProps['disabled'];
24
+ /** The first month the calendar can show. */
25
+ startMonth?: Date;
26
+ /** The last month the calendar can show. */
27
+ endMonth?: Date;
28
+ /** The height in px: 36 for forms, 32 for fields inside a table. Defaults to 36. */
29
+ size?: InputGroupSize;
30
+ /** Receives the input element. */
31
+ ref?: Ref<HTMLInputElement>;
32
+ };
33
+ /**
34
+ * A field that shows a range as two DD/MM/YYYY days, reads a typed range through the format module and opens a calendar in
35
+ * a popover from the button at its end, for analytics periods. Pick the first day, then the last. Text that is not a range
36
+ * in order reports null (as soon as it is typed over a range, and when the field loses focus) and is marked invalid, aria-invalid,
37
+ * when the field loses focus. Under a name, a form submits two fields,
38
+ * `name.from` and `name.to`, each as YYYY-MM-DD (empty when there is no range), never the text on screen.
39
+ */
40
+ export declare function DateRangePicker({ value, defaultValue, onValueChange, disabledDays, startMonth, endMonth, size, placeholder, readOnly, disabled, onBlur, ref, name, 'aria-invalid': ariaInvalid, ...rest }: DateRangePickerProps): JSX.Element;
@@ -0,0 +1,146 @@
1
+ import { Icon } from "../icon/Icon.js";
2
+ import { Button } from "../button/Button.js";
3
+ import { FORMAT, formatDate, formatIsoDate, parseDate } from "../../lib/format.js";
4
+ import { Calendar } from "../calendar/Calendar.js";
5
+ import { InputGroup, InputGroupAddon, InputGroupInput } from "../input-group/InputGroup.js";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "../popover/Popover.js";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { useState } from "react";
9
+ // src/components/date-range-picker/DateRangePicker.tsx
10
+ const SEPARATOR = " – ";
11
+ const SPLIT = /\s+[-–—]\s+|\s*[–—]\s*/;
12
+ const same = (a, b) => a === null || b === null ? a === b : a.from.getTime() === b.from.getTime() && a.to.getTime() === b.to.getTime();
13
+ const write = (range) => range === null ? "" : `${formatDate(range.from)}${SEPARATOR}${formatDate(range.to)}`;
14
+ /** Reads "28/10/2026 – 03/11/2026" (a dash, en dash or em dash between) as a range, or null when it is not a range in order. */
15
+ function parseRange(text) {
16
+ const parts = text.trim().split(SPLIT);
17
+ if (parts.length !== 2) return null;
18
+ const from = parseDate(parts[0] ?? "");
19
+ const to = parseDate(parts[1] ?? "");
20
+ return from === null || to === null || to < from ? null : {
21
+ from,
22
+ to
23
+ };
24
+ }
25
+ /**
26
+ * A field that shows a range as two DD/MM/YYYY days, reads a typed range through the format module and opens a calendar in
27
+ * a popover from the button at its end, for analytics periods. Pick the first day, then the last. Text that is not a range
28
+ * in order reports null (as soon as it is typed over a range, and when the field loses focus) and is marked invalid, aria-invalid,
29
+ * when the field loses focus. Under a name, a form submits two fields,
30
+ * `name.from` and `name.to`, each as YYYY-MM-DD (empty when there is no range), never the text on screen.
31
+ */
32
+ function DateRangePicker({ value, defaultValue = null, onValueChange, disabledDays, startMonth, endMonth, size = 36, placeholder = `${FORMAT.datePattern}${SEPARATOR}${FORMAT.datePattern}`, readOnly, disabled, onBlur, ref, name, "aria-invalid": ariaInvalid, ...rest }) {
33
+ const controlled = value !== void 0;
34
+ const [inner, setInner] = useState(defaultValue);
35
+ const range = controlled ? value : inner;
36
+ const [text, setText] = useState(() => write(range));
37
+ const [seen, setSeen] = useState(range);
38
+ const [malformed, setMalformed] = useState(false);
39
+ const [open, setOpen] = useState(false);
40
+ const [draft, setDraft] = useState();
41
+ if (!same(seen, range)) {
42
+ setSeen(range);
43
+ if (!same(range, parseRange(text))) setText(write(range));
44
+ setMalformed(false);
45
+ }
46
+ const commit = (next) => {
47
+ setSeen(next);
48
+ if (!controlled) setInner(next);
49
+ onValueChange?.(next);
50
+ };
51
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(InputGroup, {
52
+ size,
53
+ children: [/* @__PURE__ */ jsx(InputGroupInput, {
54
+ ...rest,
55
+ ref,
56
+ type: "text",
57
+ autoComplete: "off",
58
+ placeholder,
59
+ readOnly,
60
+ disabled,
61
+ "aria-invalid": ariaInvalid ?? (malformed ? true : void 0),
62
+ value: text,
63
+ onChange: (event) => {
64
+ const next = event.target.value;
65
+ setText(next);
66
+ setMalformed(false);
67
+ if (next.trim() === "") return commit(null);
68
+ const parsed = parseRange(next);
69
+ if (parsed !== null) commit(parsed);
70
+ else if (range !== null) commit(null);
71
+ },
72
+ onBlur: (event) => {
73
+ onBlur?.(event);
74
+ const parsed = parseRange(text);
75
+ if (text.trim() === "") setMalformed(false);
76
+ else if (parsed === null) {
77
+ setMalformed(true);
78
+ commit(null);
79
+ } else setText(write(parsed));
80
+ }
81
+ }), readOnly ? null : /* @__PURE__ */ jsx(InputGroupAddon, {
82
+ align: "end",
83
+ children: /* @__PURE__ */ jsxs(Popover, {
84
+ open,
85
+ onOpenChange: (next) => {
86
+ setDraft(void 0);
87
+ setOpen(next);
88
+ },
89
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
90
+ disabled,
91
+ render: /* @__PURE__ */ jsx(Button, {
92
+ variant: "ghost",
93
+ iconOnly: true,
94
+ "aria-label": "Open calendar",
95
+ children: /* @__PURE__ */ jsx(Icon, {
96
+ name: "calendar",
97
+ size: 12
98
+ })
99
+ })
100
+ }), /* @__PURE__ */ jsx(PopoverContent, {
101
+ align: "end",
102
+ children: /* @__PURE__ */ jsx(Calendar, {
103
+ mode: "range",
104
+ selected: draft ?? range ?? void 0,
105
+ defaultMonth: range?.from,
106
+ onSelect: (_picked, triggerDate) => {
107
+ if (draft?.from === void 0) {
108
+ setDraft({
109
+ from: triggerDate,
110
+ to: void 0
111
+ });
112
+ return;
113
+ }
114
+ const [from, to] = triggerDate < draft.from ? [triggerDate, draft.from] : [draft.from, triggerDate];
115
+ const next = {
116
+ from,
117
+ to
118
+ };
119
+ setText(write(next));
120
+ setMalformed(false);
121
+ commit(next);
122
+ setDraft(void 0);
123
+ setOpen(false);
124
+ },
125
+ disabled: disabledDays,
126
+ startMonth,
127
+ endMonth
128
+ })
129
+ })]
130
+ })
131
+ })]
132
+ }), name !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
133
+ type: "hidden",
134
+ name: `${name}.from`,
135
+ form: rest.form,
136
+ value: range === null ? "" : formatIsoDate(range.from),
137
+ disabled
138
+ }), /* @__PURE__ */ jsx("input", {
139
+ type: "hidden",
140
+ name: `${name}.to`,
141
+ form: rest.form,
142
+ value: range === null ? "" : formatIsoDate(range.to),
143
+ disabled
144
+ })] })] });
145
+ }
146
+ export { DateRangePicker };
@@ -0,0 +1,2 @@
1
+ import { DateRangePicker, DateRangePickerProps, DateRangeValue } from "./DateRangePicker.js";
2
+ export { DateRangePicker, type DateRangePickerProps, type DateRangeValue };
@@ -0,0 +1 @@
1
+ export { DateRangePicker } from "./DateRangePicker.js";
@@ -0,0 +1,52 @@
1
+ import { ComponentProps, JSX } from "react";
2
+ import { Drawer } from "@base-ui/react/drawer";
3
+ /** Props of Drawer. */
4
+ export interface DrawerProps extends Drawer.Root.Props {
5
+ /** Whether a short bar shows on the edge the drawer is swiped from, as a hint on phones. Defaults to false. */
6
+ showSwipeHandle?: boolean;
7
+ }
8
+ /**
9
+ * The root of a drawer: a panel people swipe away on a phone. It holds the open state and renders nothing itself.
10
+ * `swipeDirection` picks the edge it leaves by (down, the default, makes a bottom drawer). Pass `open` and
11
+ * `onOpenChange` to control it, or `defaultOpen`. The page behind is inert while it is open, with no scrim over it.
12
+ */
13
+ declare function Drawer$1({ modal, showSwipeHandle, swipeDirection, ...props }: DrawerProps): JSX.Element;
14
+ /** Props of DrawerTrigger. */
15
+ export type DrawerTriggerProps = Omit<Drawer.Trigger.Props, 'className' | 'style'>;
16
+ /** The button that opens the drawer. */
17
+ export declare function DrawerTrigger(props: DrawerTriggerProps): JSX.Element;
18
+ /** Props of DrawerClose. */
19
+ export type DrawerCloseProps = Omit<Drawer.Close.Props, 'className' | 'style'>;
20
+ /** A button that closes the drawer, such as Done in the footer. */
21
+ export declare function DrawerClose(props: DrawerCloseProps): JSX.Element;
22
+ /** Props of DrawerContent. */
23
+ export interface DrawerContentProps extends Omit<Drawer.Popup.Props, 'className' | 'style'> {
24
+ /** Whether the corner close button shows. It is the way to close without swiping. Defaults to true. */
25
+ showCloseButton?: boolean;
26
+ /** The accessible name of the corner close button. Defaults to Close. */
27
+ closeLabel?: string;
28
+ }
29
+ /**
30
+ * The panel, white with a hairline and 24px corners on its inner edge. A bottom drawer spans the screen and sizes to its
31
+ * content; a side drawer is full height and three quarters of a phone screen, 384px from the small breakpoint up. It
32
+ * follows the finger while it is swiped and otherwise does not move: no slide in or out, no scrim, blur or shadow.
33
+ * Put DrawerHeader, the content and DrawerFooter inside.
34
+ */
35
+ export declare function DrawerContent({ children, showCloseButton, closeLabel, ...props }: DrawerContentProps): JSX.Element;
36
+ /** Props of DrawerHeader. */
37
+ export type DrawerHeaderProps = Omit<ComponentProps<'div'>, 'className' | 'style'>;
38
+ /** The title and description, stacked, with room on the right for the close button. */
39
+ export declare function DrawerHeader(props: DrawerHeaderProps): JSX.Element;
40
+ /** Props of DrawerFooter. */
41
+ export type DrawerFooterProps = Omit<ComponentProps<'div'>, 'className' | 'style'>;
42
+ /** The actions at the bottom of the drawer, stacked full width for a thumb. */
43
+ export declare function DrawerFooter(props: DrawerFooterProps): JSX.Element;
44
+ /** Props of DrawerTitle. */
45
+ export type DrawerTitleProps = Omit<Drawer.Title.Props, 'className' | 'style'>;
46
+ /** The drawer's name, in the b4 style. It names the drawer for assistive technology. */
47
+ export declare function DrawerTitle(props: DrawerTitleProps): JSX.Element;
48
+ /** Props of DrawerDescription. */
49
+ export type DrawerDescriptionProps = Omit<Drawer.Description.Props, 'className' | 'style'>;
50
+ /** One line that says what the drawer is for, in subdued body text. */
51
+ export declare function DrawerDescription(props: DrawerDescriptionProps): JSX.Element;
52
+ export { Drawer$1 as Drawer };
@@ -0,0 +1,120 @@
1
+ import { cn } from "../../lib/cn.js";
2
+ import { Icon } from "../icon/Icon.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { createContext, use, useMemo } from "react";
5
+ import { Drawer } from "@base-ui/react/drawer";
6
+ // src/components/drawer/Drawer.tsx
7
+ const DrawerContext = createContext({
8
+ modal: true,
9
+ showSwipeHandle: false
10
+ });
11
+ /**
12
+ * The root of a drawer: a panel people swipe away on a phone. It holds the open state and renders nothing itself.
13
+ * `swipeDirection` picks the edge it leaves by (down, the default, makes a bottom drawer). Pass `open` and
14
+ * `onOpenChange` to control it, or `defaultOpen`. The page behind is inert while it is open, with no scrim over it.
15
+ */
16
+ function Drawer$1({ modal = true, showSwipeHandle = false, swipeDirection = "down", ...props }) {
17
+ const value = useMemo(() => ({
18
+ modal,
19
+ showSwipeHandle
20
+ }), [modal, showSwipeHandle]);
21
+ return /* @__PURE__ */ jsx(DrawerContext, {
22
+ value,
23
+ children: /* @__PURE__ */ jsx(Drawer.Root, {
24
+ modal,
25
+ swipeDirection,
26
+ ...props
27
+ })
28
+ });
29
+ }
30
+ /** The button that opens the drawer. */
31
+ function DrawerTrigger(props) {
32
+ return /* @__PURE__ */ jsx(Drawer.Trigger, {
33
+ "data-slot": "drawer-trigger",
34
+ ...props
35
+ });
36
+ }
37
+ /** A button that closes the drawer, such as Done in the footer. */
38
+ function DrawerClose(props) {
39
+ return /* @__PURE__ */ jsx(Drawer.Close, {
40
+ "data-slot": "drawer-close",
41
+ ...props
42
+ });
43
+ }
44
+ /**
45
+ * The panel, white with a hairline and 24px corners on its inner edge. A bottom drawer spans the screen and sizes to its
46
+ * content; a side drawer is full height and three quarters of a phone screen, 384px from the small breakpoint up. It
47
+ * follows the finger while it is swiped and otherwise does not move: no slide in or out, no scrim, blur or shadow.
48
+ * Put DrawerHeader, the content and DrawerFooter inside.
49
+ */
50
+ function DrawerContent({ children, showCloseButton = true, closeLabel = "Close", ...props }) {
51
+ const { modal, showSwipeHandle } = use(DrawerContext);
52
+ return /* @__PURE__ */ jsx(Drawer.Portal, { children: /* @__PURE__ */ jsx(Drawer.Viewport, {
53
+ "data-slot": "drawer-viewport",
54
+ className: cn("rt:pointer-events-none rt:fixed rt:inset-0 rt:z-50", modal === true && "rt:pointer-events-auto"),
55
+ children: /* @__PURE__ */ jsxs(Drawer.Popup, {
56
+ "data-slot": "drawer-popup",
57
+ className: cn("rt:group/drawer-popup rt:pointer-events-auto rt:fixed rt:z-50 rt:flex rt:max-h-full rt:min-h-0 rt:flex-col rt:border-border rt:bg-popover rt:text-b3 rt:text-popover-foreground rt:select-none rt:drawer-swipe rt:focus-visible:focus-ring", "rt:swipe-y:inset-x-0 rt:swipe-x:inset-y-0 rt:swipe-x:w-3/4 rt:swipe-x:flex-row rt:swipe-x:sm:w-96", "rt:swipe-down:bottom-0 rt:swipe-down:rounded-t-3xl rt:swipe-down:border-x rt:swipe-down:border-t", "rt:swipe-up:top-0 rt:swipe-up:rounded-b-3xl rt:swipe-up:border-x rt:swipe-up:border-b", "rt:swipe-left:left-0 rt:swipe-left:rounded-r-3xl rt:swipe-left:border-y rt:swipe-left:border-r", "rt:swipe-right:right-0 rt:swipe-right:rounded-l-3xl rt:swipe-right:border-y rt:swipe-right:border-l"),
58
+ ...props,
59
+ children: [
60
+ showSwipeHandle && /* @__PURE__ */ jsx(DrawerSwipeHandle, {}),
61
+ /* @__PURE__ */ jsx(Drawer.Content, {
62
+ "data-slot": "drawer-content",
63
+ className: "rt:flex rt:min-h-0 rt:flex-1 rt:flex-col rt:overflow-y-auto rt:overscroll-contain rt:select-text rt:group-data-swiping/drawer-popup:select-none",
64
+ children
65
+ }),
66
+ showCloseButton && /* @__PURE__ */ jsx(Drawer.Close, {
67
+ "data-slot": "drawer-close",
68
+ "aria-label": closeLabel,
69
+ className: "rt:absolute rt:top-5 rt:right-5 rt:flex rt:size-7 rt:items-center rt:justify-center rt:rounded-md rt:text-icon rt:transition-colors rt:hover:bg-primary-tint rt:focus-visible:focus-ring",
70
+ children: /* @__PURE__ */ jsx(Icon, {
71
+ name: "x",
72
+ size: 12
73
+ })
74
+ })
75
+ ]
76
+ })
77
+ }) });
78
+ }
79
+ /** A short bar on the edge the drawer is swiped from. It is decorative: the whole panel takes the swipe. */
80
+ function DrawerSwipeHandle() {
81
+ return /* @__PURE__ */ jsx("div", {
82
+ "data-slot": "drawer-swipe-handle",
83
+ "aria-hidden": "true",
84
+ className: "rt:flex rt:shrink-0 rt:items-center rt:justify-center rt:cursor-grab rt:group-swipe-y/drawer-popup:h-5 rt:group-swipe-x/drawer-popup:w-5 rt:group-swipe-up/drawer-popup:order-last rt:group-swipe-left/drawer-popup:order-last rt:active:cursor-grabbing",
85
+ children: /* @__PURE__ */ jsx("span", { className: "rt:block rt:rounded-sm rt:bg-muted rt:group-swipe-y/drawer-popup:h-1 rt:group-swipe-y/drawer-popup:w-12 rt:group-swipe-x/drawer-popup:h-12 rt:group-swipe-x/drawer-popup:w-1" })
86
+ });
87
+ }
88
+ /** The title and description, stacked, with room on the right for the close button. */
89
+ function DrawerHeader(props) {
90
+ return /* @__PURE__ */ jsx("div", {
91
+ "data-slot": "drawer-header",
92
+ className: "rt:flex rt:shrink-0 rt:flex-col rt:gap-1 rt:p-6 rt:pr-20",
93
+ ...props
94
+ });
95
+ }
96
+ /** The actions at the bottom of the drawer, stacked full width for a thumb. */
97
+ function DrawerFooter(props) {
98
+ return /* @__PURE__ */ jsx("div", {
99
+ "data-slot": "drawer-footer",
100
+ className: "rt:mt-auto rt:flex rt:shrink-0 rt:flex-col rt:gap-2 rt:px-6 rt:pb-6",
101
+ ...props
102
+ });
103
+ }
104
+ /** The drawer's name, in the b4 style. It names the drawer for assistive technology. */
105
+ function DrawerTitle(props) {
106
+ return /* @__PURE__ */ jsx(Drawer.Title, {
107
+ "data-slot": "drawer-title",
108
+ className: "rt:text-b4 rt:text-foreground-strong",
109
+ ...props
110
+ });
111
+ }
112
+ /** One line that says what the drawer is for, in subdued body text. */
113
+ function DrawerDescription(props) {
114
+ return /* @__PURE__ */ jsx(Drawer.Description, {
115
+ "data-slot": "drawer-description",
116
+ className: "rt:text-b3 rt:text-foreground-subdued",
117
+ ...props
118
+ });
119
+ }
120
+ export { Drawer$1 as Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger };
@@ -0,0 +1,2 @@
1
+ import { Drawer, DrawerClose, DrawerCloseProps, DrawerContent, DrawerContentProps, DrawerDescription, DrawerDescriptionProps, DrawerFooter, DrawerFooterProps, DrawerHeader, DrawerHeaderProps, DrawerProps, DrawerTitle, DrawerTitleProps, DrawerTrigger, DrawerTriggerProps } from "./Drawer.js";
2
+ export { Drawer, DrawerClose, type DrawerCloseProps, DrawerContent, type DrawerContentProps, DrawerDescription, type DrawerDescriptionProps, DrawerFooter, type DrawerFooterProps, DrawerHeader, type DrawerHeaderProps, type DrawerProps, DrawerTitle, type DrawerTitleProps, DrawerTrigger, type DrawerTriggerProps };
@@ -0,0 +1 @@
1
+ export { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "./Drawer.js";