@robr0/design-system 0.1.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 (91) hide show
  1. package/README.md +13 -9
  2. package/components/Accordion/Accordion.js +0 -1
  3. package/components/Avatar/Avatar.js +1 -0
  4. package/components/Breadcrumb/Breadcrumb.css +9 -1
  5. package/components/Breadcrumb/Breadcrumb.d.ts +7 -1
  6. package/components/Breadcrumb/Breadcrumb.js +3 -2
  7. package/components/Button/Button.css +18 -0
  8. package/components/Button/Button.d.ts +38 -16
  9. package/components/Button/Button.js +76 -58
  10. package/components/Card/Card.css +6 -0
  11. package/components/Card/Card.d.ts +17 -5
  12. package/components/Card/Card.js +101 -80
  13. package/components/Checkbox/Checkbox.d.ts +24 -12
  14. package/components/Checkbox/Checkbox.js +99 -85
  15. package/components/CircularButton/CircularButton.css +11 -0
  16. package/components/CircularButton/CircularButton.d.ts +7 -1
  17. package/components/CircularButton/CircularButton.js +15 -4
  18. package/components/CodeBlock/CodeBlock.js +10 -1
  19. package/components/ColorPicker/ColorPicker.css +278 -0
  20. package/components/ColorPicker/ColorPicker.d.ts +50 -0
  21. package/components/ColorPicker/ColorPicker.js +338 -0
  22. package/components/Combobox/Combobox.css +0 -36
  23. package/components/Combobox/Combobox.d.ts +19 -7
  24. package/components/Combobox/Combobox.js +106 -89
  25. package/components/CommandPalette/CommandPalette.css +1 -15
  26. package/components/CommandPalette/CommandPalette.js +6 -5
  27. package/components/ContextMenu/ContextMenu.css +262 -0
  28. package/components/ContextMenu/ContextMenu.d.ts +26 -0
  29. package/components/ContextMenu/ContextMenu.js +350 -0
  30. package/components/DateInput/DateInput.css +0 -36
  31. package/components/DateInput/DateInput.d.ts +18 -21
  32. package/components/DateInput/DateInput.js +76 -64
  33. package/components/DatePicker/DatePicker.d.ts +9 -2
  34. package/components/DatePicker/DatePicker.js +137 -128
  35. package/components/Dialog/Dialog.d.ts +15 -4
  36. package/components/Dialog/Dialog.js +136 -118
  37. package/components/Drawer/Drawer.d.ts +15 -4
  38. package/components/Drawer/Drawer.js +138 -119
  39. package/components/Dropdown/Dropdown.css +0 -36
  40. package/components/Dropdown/Dropdown.d.ts +24 -8
  41. package/components/Dropdown/Dropdown.js +219 -169
  42. package/components/DropdownMenu/DropdownMenu.js +10 -7
  43. package/components/Field/Field.css +80 -0
  44. package/components/Field/Field.d.ts +50 -0
  45. package/components/Field/Field.js +58 -0
  46. package/components/Field/FieldContext.d.ts +42 -0
  47. package/components/Field/FieldContext.js +7 -0
  48. package/components/FileInput/FileInput.css +0 -36
  49. package/components/FileInput/FileInput.d.ts +13 -17
  50. package/components/FileInput/FileInput.js +168 -141
  51. package/components/Input/Input.css +0 -43
  52. package/components/Input/Input.d.ts +23 -22
  53. package/components/Input/Input.js +93 -66
  54. package/components/Kbd/Kbd.css +28 -0
  55. package/components/Kbd/Kbd.d.ts +19 -0
  56. package/components/Kbd/Kbd.js +14 -0
  57. package/components/Popover/Popover.d.ts +0 -6
  58. package/components/Popover/Popover.js +22 -16
  59. package/components/ProgressBar/ProgressBar.js +1 -1
  60. package/components/RadioButton/RadioButton.d.ts +26 -13
  61. package/components/RadioButton/RadioButton.js +95 -70
  62. package/components/SelectionCard/SelectionCard.css +6 -2
  63. package/components/SelectionCard/SelectionCard.d.ts +10 -4
  64. package/components/SelectionCard/SelectionCard.js +72 -56
  65. package/components/Skeleton/Skeleton.css +2 -0
  66. package/components/Slider/Slider.d.ts +19 -10
  67. package/components/Slider/Slider.js +50 -40
  68. package/components/Spinner/Spinner.css +11 -0
  69. package/components/Spinner/Spinner.d.ts +2 -2
  70. package/components/Swatch/Swatch.css +52 -0
  71. package/components/Swatch/Swatch.d.ts +34 -0
  72. package/components/Swatch/Swatch.js +44 -0
  73. package/components/Table/Table.css +19 -0
  74. package/components/Table/Table.d.ts +10 -2
  75. package/components/Table/Table.js +58 -58
  76. package/components/Textarea/Textarea.css +0 -43
  77. package/components/Textarea/Textarea.d.ts +21 -22
  78. package/components/Textarea/Textarea.js +78 -67
  79. package/components/ToggleGroup/ToggleGroup.d.ts +18 -8
  80. package/components/ToggleGroup/ToggleGroup.js +61 -44
  81. package/components/ToggleSwitch/ToggleSwitch.css +7 -2
  82. package/components/ToggleSwitch/ToggleSwitch.d.ts +17 -9
  83. package/components/ToggleSwitch/ToggleSwitch.js +57 -44
  84. package/components/registry.d.ts +47 -10
  85. package/components/registry.js +5 -1
  86. package/components/registry.json +498 -56
  87. package/components/registry.json.d.ts +498 -56
  88. package/components/registry.json.js +4 -1
  89. package/index.d.ts +8 -1
  90. package/index.js +16 -1
  91. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
2
+ import React, { useMemo, useState } from "react";
3
3
  import "./DatePicker.css";
4
4
  import "../../fonts/material-symbols.css";
5
5
  const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
@@ -31,140 +31,149 @@ function formatDate(date) {
31
31
  function isSameDay(a, b) {
32
32
  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
33
33
  }
34
- const DatePicker = ({
35
- value,
36
- size = "default",
37
- disabled = false,
38
- min,
39
- max,
40
- onDateSelect,
41
- className = ""
42
- }) => {
43
- const selectedDate = parseDate(value);
44
- const minDate = parseDate(min);
45
- const maxDate = parseDate(max);
46
- const today = useMemo(() => /* @__PURE__ */ new Date(), []);
47
- const [viewMonth, setViewMonth] = useState(
48
- selectedDate ? selectedDate.getMonth() : today.getMonth()
49
- );
50
- const [viewYear, setViewYear] = useState(
51
- selectedDate ? selectedDate.getFullYear() : today.getFullYear()
52
- );
53
- const baseClass = "ds-date-picker";
54
- const sizeClass = `${baseClass}--${size}`;
55
- const disabledClass = disabled ? `${baseClass}--disabled` : "";
56
- const classes = [baseClass, sizeClass, disabledClass, className].filter(Boolean).join(" ");
57
- const firstDay = new Date(viewYear, viewMonth, 1).getDay();
58
- const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
59
- const daysInPrevMonth = new Date(viewYear, viewMonth, 0).getDate();
60
- const cells = [];
61
- for (let i = firstDay - 1; i >= 0; i--) {
62
- const d = daysInPrevMonth - i;
63
- cells.push({ day: d, inMonth: false, date: new Date(viewYear, viewMonth - 1, d) });
64
- }
65
- for (let d = 1; d <= daysInMonth; d++) {
66
- cells.push({ day: d, inMonth: true, date: new Date(viewYear, viewMonth, d) });
67
- }
68
- const remaining = 42 - cells.length;
69
- for (let d = 1; d <= remaining; d++) {
70
- cells.push({ day: d, inMonth: false, date: new Date(viewYear, viewMonth + 1, d) });
71
- }
72
- const goToPrevMonth = () => {
73
- if (viewMonth === 0) {
74
- setViewMonth(11);
75
- setViewYear(viewYear - 1);
76
- } else {
77
- setViewMonth(viewMonth - 1);
34
+ const DatePicker = React.forwardRef(
35
+ ({ value, size = "default", disabled = false, min, max, onDateSelect, className = "", ...rest }, ref) => {
36
+ const selectedDate = parseDate(value);
37
+ const minDate = parseDate(min);
38
+ const maxDate = parseDate(max);
39
+ const today = useMemo(() => /* @__PURE__ */ new Date(), []);
40
+ const [viewMonth, setViewMonth] = useState(
41
+ selectedDate ? selectedDate.getMonth() : today.getMonth()
42
+ );
43
+ const [viewYear, setViewYear] = useState(
44
+ selectedDate ? selectedDate.getFullYear() : today.getFullYear()
45
+ );
46
+ const baseClass = "ds-date-picker";
47
+ const classes = [
48
+ baseClass,
49
+ `${baseClass}--${size}`,
50
+ disabled ? `${baseClass}--disabled` : "",
51
+ className
52
+ ].filter(Boolean).join(" ");
53
+ const firstDay = new Date(viewYear, viewMonth, 1).getDay();
54
+ const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
55
+ const daysInPrevMonth = new Date(viewYear, viewMonth, 0).getDate();
56
+ const cells = [];
57
+ for (let i = firstDay - 1; i >= 0; i--) {
58
+ const d = daysInPrevMonth - i;
59
+ cells.push({ day: d, inMonth: false, date: new Date(viewYear, viewMonth - 1, d) });
78
60
  }
79
- };
80
- const goToNextMonth = () => {
81
- if (viewMonth === 11) {
82
- setViewMonth(0);
83
- setViewYear(viewYear + 1);
84
- } else {
85
- setViewMonth(viewMonth + 1);
61
+ for (let d = 1; d <= daysInMonth; d++) {
62
+ cells.push({ day: d, inMonth: true, date: new Date(viewYear, viewMonth, d) });
86
63
  }
87
- };
88
- const isDateDisabled = (date) => {
89
- if (disabled) return true;
90
- if (minDate && date < minDate) return true;
91
- if (maxDate && date > maxDate) return true;
92
- return false;
93
- };
94
- const handleSelect = (cell) => {
95
- if (isDateDisabled(cell.date)) return;
96
- if (!cell.inMonth) {
97
- setViewMonth(cell.date.getMonth());
98
- setViewYear(cell.date.getFullYear());
64
+ const remaining = 42 - cells.length;
65
+ for (let d = 1; d <= remaining; d++) {
66
+ cells.push({ day: d, inMonth: false, date: new Date(viewYear, viewMonth + 1, d) });
99
67
  }
100
- onDateSelect?.(formatDate(cell.date));
101
- };
102
- return /* @__PURE__ */ jsxs("div", { className: classes, children: [
103
- /* @__PURE__ */ jsxs("div", { className: `${baseClass}__header`, children: [
104
- /* @__PURE__ */ jsx(
105
- "button",
106
- {
107
- type: "button",
108
- className: `${baseClass}__nav-btn material-symbols-rounded`,
109
- onClick: goToPrevMonth,
110
- disabled,
111
- "aria-label": "Previous month",
112
- children: "chevron_left"
113
- }
114
- ),
115
- /* @__PURE__ */ jsxs("span", { className: `${baseClass}__month-year`, children: [
116
- MONTHS[viewMonth],
117
- " ",
118
- viewYear
68
+ const goToPrevMonth = () => {
69
+ if (viewMonth === 0) {
70
+ setViewMonth(11);
71
+ setViewYear(viewYear - 1);
72
+ } else {
73
+ setViewMonth(viewMonth - 1);
74
+ }
75
+ };
76
+ const goToNextMonth = () => {
77
+ if (viewMonth === 11) {
78
+ setViewMonth(0);
79
+ setViewYear(viewYear + 1);
80
+ } else {
81
+ setViewMonth(viewMonth + 1);
82
+ }
83
+ };
84
+ const isDateDisabled = (date) => {
85
+ if (disabled) return true;
86
+ if (minDate && date < minDate) return true;
87
+ if (maxDate && date > maxDate) return true;
88
+ return false;
89
+ };
90
+ const handleSelect = (cell) => {
91
+ if (isDateDisabled(cell.date)) return;
92
+ if (!cell.inMonth) {
93
+ setViewMonth(cell.date.getMonth());
94
+ setViewYear(cell.date.getFullYear());
95
+ }
96
+ onDateSelect?.(formatDate(cell.date));
97
+ };
98
+ return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
99
+ /* @__PURE__ */ jsxs("div", { className: `${baseClass}__header`, children: [
100
+ /* @__PURE__ */ jsx(
101
+ "button",
102
+ {
103
+ type: "button",
104
+ className: `${baseClass}__nav-btn material-symbols-rounded`,
105
+ onClick: goToPrevMonth,
106
+ disabled,
107
+ "aria-label": "Previous month",
108
+ children: "chevron_left"
109
+ }
110
+ ),
111
+ /* @__PURE__ */ jsxs("span", { className: `${baseClass}__month-year`, children: [
112
+ MONTHS[viewMonth],
113
+ " ",
114
+ viewYear
115
+ ] }),
116
+ /* @__PURE__ */ jsx(
117
+ "button",
118
+ {
119
+ type: "button",
120
+ className: `${baseClass}__nav-btn material-symbols-rounded`,
121
+ onClick: goToNextMonth,
122
+ disabled,
123
+ "aria-label": "Next month",
124
+ children: "chevron_right"
125
+ }
126
+ )
119
127
  ] }),
120
- /* @__PURE__ */ jsx(
121
- "button",
128
+ /* @__PURE__ */ jsxs(
129
+ "div",
122
130
  {
123
- type: "button",
124
- className: `${baseClass}__nav-btn material-symbols-rounded`,
125
- onClick: goToNextMonth,
126
- disabled,
127
- "aria-label": "Next month",
128
- children: "chevron_right"
131
+ className: `${baseClass}__grid`,
132
+ role: "group",
133
+ "aria-label": `${MONTHS[viewMonth]} ${viewYear}`,
134
+ children: [
135
+ DAYS.map((day) => (
136
+ // Each cell's aria-label already carries the full weekday name, so
137
+ // the visual column headers are redundant noise for a screen reader.
138
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__day-header`, "aria-hidden": "true", children: day }, day)
139
+ )),
140
+ cells.map((cell, idx) => {
141
+ const isSelected = selectedDate && isSameDay(cell.date, selectedDate);
142
+ const isToday = isSameDay(cell.date, today);
143
+ const isCellDisabled = isDateDisabled(cell.date);
144
+ const cellClasses = [
145
+ `${baseClass}__cell`,
146
+ !cell.inMonth ? `${baseClass}__cell--outside` : "",
147
+ isSelected ? `${baseClass}__cell--selected` : "",
148
+ isToday && !isSelected ? `${baseClass}__cell--today` : "",
149
+ isCellDisabled ? `${baseClass}__cell--disabled` : ""
150
+ ].filter(Boolean).join(" ");
151
+ return /* @__PURE__ */ jsx(
152
+ "button",
153
+ {
154
+ type: "button",
155
+ className: cellClasses,
156
+ onClick: () => handleSelect(cell),
157
+ disabled: isCellDisabled,
158
+ "aria-label": `${cell.date.toLocaleDateString(void 0, {
159
+ weekday: "long",
160
+ year: "numeric",
161
+ month: "long",
162
+ day: "numeric"
163
+ })}${isSelected ? ", selected" : ""}`,
164
+ "aria-current": isToday ? "date" : void 0,
165
+ children: cell.day
166
+ },
167
+ idx
168
+ );
169
+ })
170
+ ]
129
171
  }
130
172
  )
131
- ] }),
132
- /* @__PURE__ */ jsxs("div", { className: `${baseClass}__grid`, role: "grid", children: [
133
- DAYS.map((day) => /* @__PURE__ */ jsx("div", { className: `${baseClass}__day-header`, role: "columnheader", children: day }, day)),
134
- cells.map((cell, idx) => {
135
- const isSelected = selectedDate && isSameDay(cell.date, selectedDate);
136
- const isToday = isSameDay(cell.date, today);
137
- const isCellDisabled = isDateDisabled(cell.date);
138
- const cellClasses = [
139
- `${baseClass}__cell`,
140
- !cell.inMonth ? `${baseClass}__cell--outside` : "",
141
- isSelected ? `${baseClass}__cell--selected` : "",
142
- isToday && !isSelected ? `${baseClass}__cell--today` : "",
143
- isCellDisabled ? `${baseClass}__cell--disabled` : ""
144
- ].filter(Boolean).join(" ");
145
- return /* @__PURE__ */ jsx(
146
- "button",
147
- {
148
- type: "button",
149
- className: cellClasses,
150
- onClick: () => handleSelect(cell),
151
- disabled: isCellDisabled,
152
- "aria-label": cell.date.toLocaleDateString(void 0, {
153
- weekday: "long",
154
- year: "numeric",
155
- month: "long",
156
- day: "numeric"
157
- }),
158
- "aria-selected": isSelected || void 0,
159
- role: "gridcell",
160
- children: cell.day
161
- },
162
- idx
163
- );
164
- })
165
- ] })
166
- ] });
167
- };
173
+ ] });
174
+ }
175
+ );
176
+ DatePicker.displayName = "DatePicker";
168
177
  export {
169
178
  DatePicker
170
179
  };
@@ -1,10 +1,14 @@
1
1
  import { default as React } from 'react';
2
- export interface DialogProps {
2
+ /** Props owned by Dialog itself — everything else falls through to the panel. */
3
+ type DialogOwnProps = {
3
4
  /** Whether the dialog is open */
4
5
  open: boolean;
5
6
  /** Callback when dialog requests to close */
6
7
  onOpenChange: (open: boolean) => void;
7
- /** Dialog title */
8
+ /**
9
+ * Dialog title.
10
+ * Note: this shadows the native `title` tooltip attribute, which Dialog does not expose.
11
+ */
8
12
  title: string;
9
13
  /** Optional subtitle under the title */
10
14
  description?: string;
@@ -16,13 +20,20 @@ export interface DialogProps {
16
20
  size?: 'sm' | 'md' | 'lg';
17
21
  /** Whether ESC, backdrop click, and the close button can dismiss */
18
22
  dismissible?: boolean;
19
- /** Additional CSS classes */
23
+ /** Additional CSS classes — applied to the portal container, not the panel */
20
24
  className?: string;
25
+ };
26
+ export interface DialogProps extends DialogOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof DialogOwnProps> {
21
27
  }
22
28
  /**
23
29
  * Dialog component — a general-purpose modal with arbitrary
24
30
  * content and an optional footer. Traps focus, restores it on
25
31
  * close, locks body scroll, and supports ESC / backdrop / close
26
32
  * button dismissal. For confirm/cancel prompts use AlertDialog.
33
+ *
34
+ * Renders through a portal into `document.body`. The forwarded ref and any
35
+ * unrecognised props target the **panel** (the `role="dialog"` element), not the
36
+ * portal container — that is the element worth focusing or measuring.
27
37
  */
28
- export declare const Dialog: ({ open, onOpenChange, title, description, children, footer, size, dismissible, className, }: DialogProps) => React.ReactPortal | null;
38
+ export declare const Dialog: React.ForwardRefExoticComponent<DialogProps & React.RefAttributes<HTMLDivElement>>;
39
+ export {};
@@ -1,132 +1,150 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
- import { useRef, useId, useSyncExternalStore, useEffect } from "react";
2
+ import React, { useRef, useId, useSyncExternalStore, useEffect } from "react";
3
3
  import ReactDOM from "react-dom";
4
4
  import "./Dialog.css";
5
5
  import "../../fonts/material-symbols.css";
6
6
  const emptySubscribe = () => () => {
7
7
  };
8
- const Dialog = ({
9
- open,
10
- onOpenChange,
11
- title,
12
- description,
13
- children,
14
- footer,
15
- size = "md",
16
- dismissible = true,
17
- className = ""
18
- }) => {
19
- const panelRef = useRef(null);
20
- const previousFocusRef = useRef(null);
21
- const titleId = useId();
22
- const descId = useId();
23
- const baseClass = "ds-dialog";
24
- const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false);
25
- useEffect(() => {
26
- if (open) {
27
- previousFocusRef.current = document.activeElement;
28
- }
29
- }, [open]);
30
- useEffect(() => {
31
- if (!open) return;
32
- const panel = panelRef.current;
33
- if (panel) {
34
- const first = panel.querySelector(
35
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
36
- );
37
- (first ?? panel).focus();
38
- }
39
- const handleKeyDown = (e) => {
40
- if (e.key === "Escape" && dismissible) {
41
- onOpenChange(false);
42
- return;
8
+ const Dialog = React.forwardRef(
9
+ ({
10
+ open,
11
+ onOpenChange,
12
+ title,
13
+ description,
14
+ children,
15
+ footer,
16
+ size = "md",
17
+ dismissible = true,
18
+ className = "",
19
+ ...rest
20
+ }, ref) => {
21
+ const panelRef = useRef(null);
22
+ const previousFocusRef = useRef(null);
23
+ const titleId = useId();
24
+ const descId = useId();
25
+ const baseClass = "ds-dialog";
26
+ const setPanelRef = (node) => {
27
+ panelRef.current = node;
28
+ if (typeof ref === "function") ref(node);
29
+ else if (ref) ref.current = node;
30
+ };
31
+ const mounted = useSyncExternalStore(
32
+ emptySubscribe,
33
+ () => true,
34
+ () => false
35
+ );
36
+ useEffect(() => {
37
+ if (open) {
38
+ previousFocusRef.current = document.activeElement;
39
+ }
40
+ }, [open]);
41
+ useEffect(() => {
42
+ if (!open) return;
43
+ const panel = panelRef.current;
44
+ if (panel) {
45
+ const first = panel.querySelector("button, [href], input, select, textarea") ?? panel.querySelector('[tabindex]:not([tabindex="-1"])');
46
+ (first ?? panel).focus();
43
47
  }
44
- if (e.key === "Tab" && panel) {
45
- const focusable = panel.querySelectorAll(
46
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
47
- );
48
- if (focusable.length === 0) return;
49
- const first = focusable[0];
50
- const last = focusable[focusable.length - 1];
51
- if (e.shiftKey && document.activeElement === first) {
52
- e.preventDefault();
53
- last.focus();
54
- } else if (!e.shiftKey && document.activeElement === last) {
55
- e.preventDefault();
56
- first.focus();
48
+ const handleKeyDown = (e) => {
49
+ if (e.key === "Escape" && dismissible) {
50
+ onOpenChange(false);
51
+ return;
57
52
  }
53
+ if (e.key === "Tab" && panel) {
54
+ const focusable = panel.querySelectorAll(
55
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
56
+ );
57
+ if (focusable.length === 0) return;
58
+ const first = focusable[0];
59
+ const last = focusable[focusable.length - 1];
60
+ if (e.shiftKey && document.activeElement === first) {
61
+ e.preventDefault();
62
+ last.focus();
63
+ } else if (!e.shiftKey && document.activeElement === last) {
64
+ e.preventDefault();
65
+ first.focus();
66
+ }
67
+ }
68
+ };
69
+ document.addEventListener("keydown", handleKeyDown);
70
+ return () => document.removeEventListener("keydown", handleKeyDown);
71
+ }, [open, dismissible, onOpenChange]);
72
+ useEffect(() => {
73
+ if (!open && previousFocusRef.current) {
74
+ previousFocusRef.current.focus();
75
+ previousFocusRef.current = null;
58
76
  }
77
+ }, [open]);
78
+ useEffect(() => {
79
+ if (open) {
80
+ document.body.style.overflow = "hidden";
81
+ } else {
82
+ document.body.style.overflow = "";
83
+ }
84
+ return () => {
85
+ document.body.style.overflow = "";
86
+ };
87
+ }, [open]);
88
+ const handleBackdropClick = () => {
89
+ if (dismissible) onOpenChange(false);
59
90
  };
60
- document.addEventListener("keydown", handleKeyDown);
61
- return () => document.removeEventListener("keydown", handleKeyDown);
62
- }, [open, dismissible, onOpenChange]);
63
- useEffect(() => {
64
- if (!open && previousFocusRef.current) {
65
- previousFocusRef.current.focus();
66
- previousFocusRef.current = null;
67
- }
68
- }, [open]);
69
- useEffect(() => {
70
- if (open) {
71
- document.body.style.overflow = "hidden";
72
- } else {
73
- document.body.style.overflow = "";
74
- }
75
- return () => {
76
- document.body.style.overflow = "";
77
- };
78
- }, [open]);
79
- const handleBackdropClick = () => {
80
- if (dismissible) onOpenChange(false);
81
- };
82
- const openClass = open ? `${baseClass}--open` : "";
83
- const sizeClass = `${baseClass}--${size}`;
84
- const containerClasses = [baseClass, sizeClass, openClass, className].filter(Boolean).join(" ");
85
- const dialog = /* @__PURE__ */ jsxs("div", { className: containerClasses, children: [
86
- /* @__PURE__ */ jsx("div", { className: `${baseClass}__backdrop`, onClick: handleBackdropClick }),
87
- /* @__PURE__ */ jsxs(
88
- "div",
89
- {
90
- className: `${baseClass}__panel`,
91
- ref: panelRef,
92
- role: "dialog",
93
- "aria-modal": "true",
94
- "aria-labelledby": titleId,
95
- "aria-describedby": description ? descId : void 0,
96
- tabIndex: -1,
97
- children: [
98
- /* @__PURE__ */ jsxs("div", { className: `${baseClass}__header`, children: [
99
- /* @__PURE__ */ jsxs("div", { className: `${baseClass}__heading`, children: [
100
- /* @__PURE__ */ jsx("h2", { className: `${baseClass}__title`, id: titleId, children: title }),
101
- description && /* @__PURE__ */ jsx("p", { className: `${baseClass}__description`, id: descId, children: description })
91
+ const containerClasses = [
92
+ baseClass,
93
+ `${baseClass}--${size}`,
94
+ open ? `${baseClass}--open` : "",
95
+ className
96
+ ].filter(Boolean).join(" ");
97
+ const dialog = /* @__PURE__ */ jsxs("div", { className: containerClasses, children: [
98
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__backdrop`, onClick: handleBackdropClick }),
99
+ /* @__PURE__ */ jsxs(
100
+ "div",
101
+ {
102
+ ...rest,
103
+ className: `${baseClass}__panel`,
104
+ ref: setPanelRef,
105
+ role: "dialog",
106
+ "aria-modal": "true",
107
+ "aria-labelledby": titleId,
108
+ "aria-describedby": description ? descId : rest["aria-describedby"],
109
+ tabIndex: -1,
110
+ children: [
111
+ /* @__PURE__ */ jsxs("div", { className: `${baseClass}__header`, children: [
112
+ /* @__PURE__ */ jsxs("div", { className: `${baseClass}__heading`, children: [
113
+ /* @__PURE__ */ jsx("h2", { className: `${baseClass}__title`, id: titleId, children: title }),
114
+ description && /* @__PURE__ */ jsx("p", { className: `${baseClass}__description`, id: descId, children: description })
115
+ ] }),
116
+ dismissible && /* @__PURE__ */ jsx(
117
+ "button",
118
+ {
119
+ type: "button",
120
+ className: `${baseClass}__close`,
121
+ onClick: () => onOpenChange(false),
122
+ "aria-label": "Close dialog",
123
+ children: /* @__PURE__ */ jsx(
124
+ "span",
125
+ {
126
+ className: `${baseClass}__close-icon material-symbols-rounded`,
127
+ "aria-hidden": "true",
128
+ children: "close"
129
+ }
130
+ )
131
+ }
132
+ )
102
133
  ] }),
103
- dismissible && /* @__PURE__ */ jsx(
104
- "button",
105
- {
106
- type: "button",
107
- className: `${baseClass}__close`,
108
- onClick: () => onOpenChange(false),
109
- "aria-label": "Close dialog",
110
- children: /* @__PURE__ */ jsx(
111
- "span",
112
- {
113
- className: `${baseClass}__close-icon material-symbols-rounded`,
114
- "aria-hidden": "true",
115
- children: "close"
116
- }
117
- )
118
- }
119
- )
120
- ] }),
121
- children && /* @__PURE__ */ jsx("div", { className: `${baseClass}__body`, children }),
122
- footer && /* @__PURE__ */ jsx("div", { className: `${baseClass}__footer`, children: footer })
123
- ]
124
- }
125
- )
126
- ] });
127
- if (!mounted) return null;
128
- return ReactDOM.createPortal(dialog, document.body);
129
- };
134
+ children && // Scrollable when content is long; without a tab stop a keyboard
135
+ // user cannot reach that scroll. The initial-focus query below
136
+ // still prefers a real control, so this does not steal focus.
137
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__body`, tabIndex: 0, children }),
138
+ footer && /* @__PURE__ */ jsx("div", { className: `${baseClass}__footer`, children: footer })
139
+ ]
140
+ }
141
+ )
142
+ ] });
143
+ if (!mounted) return null;
144
+ return ReactDOM.createPortal(dialog, document.body);
145
+ }
146
+ );
147
+ Dialog.displayName = "Dialog";
130
148
  export {
131
149
  Dialog
132
150
  };
@@ -1,10 +1,14 @@
1
1
  import { default as React } from 'react';
2
- export interface DrawerProps {
2
+ /** Props owned by Drawer itself — everything else falls through to the panel. */
3
+ type DrawerOwnProps = {
3
4
  /** Whether the drawer is open */
4
5
  open: boolean;
5
6
  /** Callback when the drawer requests to close */
6
7
  onOpenChange: (open: boolean) => void;
7
- /** Drawer title */
8
+ /**
9
+ * Drawer title.
10
+ * Note: this shadows the native `title` tooltip attribute, which Drawer does not expose.
11
+ */
8
12
  title: string;
9
13
  /** Optional subtitle under the title */
10
14
  description?: string;
@@ -18,13 +22,20 @@ export interface DrawerProps {
18
22
  size?: 'sm' | 'md' | 'lg';
19
23
  /** Whether ESC, scrim click, and the close button can dismiss */
20
24
  dismissible?: boolean;
21
- /** Additional CSS classes */
25
+ /** Additional CSS classes — applied to the portal container, not the panel */
22
26
  className?: string;
27
+ };
28
+ export interface DrawerProps extends DrawerOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof DrawerOwnProps> {
23
29
  }
24
30
  /**
25
31
  * Drawer component — an edge-anchored panel that slides in over the page.
26
32
  * Shares Dialog's modal semantics (focus trap, focus restore, scroll lock,
27
33
  * ESC/scrim dismissal) but is anchored to a viewport edge, which suits
28
34
  * filter panels, detail views, and mobile navigation.
35
+ *
36
+ * Renders through a portal into `document.body`. The forwarded ref and any
37
+ * unrecognised props target the **panel** (the `role="dialog"` element), not the
38
+ * portal container.
29
39
  */
30
- export declare const Drawer: ({ open, onOpenChange, title, description, children, footer, side, size, dismissible, className, }: DrawerProps) => React.ReactPortal | null;
40
+ export declare const Drawer: React.ForwardRefExoticComponent<DrawerProps & React.RefAttributes<HTMLDivElement>>;
41
+ export {};