@dododog/ui 0.16.0 → 0.17.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.
@@ -46,26 +46,55 @@ var textareaVariants = classVarianceAuthority.cva(
46
46
  }
47
47
  );
48
48
  var Textarea = React__namespace.forwardRef(
49
- ({ className, size, variant, label, error, required, rows = 4, ...props }, ref) => {
49
+ ({
50
+ className,
51
+ size,
52
+ variant,
53
+ label,
54
+ error,
55
+ required,
56
+ rows = 4,
57
+ id,
58
+ "aria-describedby": ariaDescribedBy,
59
+ "aria-invalid": ariaInvalid,
60
+ "aria-required": ariaRequired,
61
+ ...props
62
+ }, ref) => {
63
+ const generatedId = React__namespace.useId();
64
+ const textareaId = id ?? generatedId;
65
+ const errorId = `${textareaId}-error`;
50
66
  const resolvedVariant = error ? "error" : variant ?? "default";
67
+ const describedBy = [ariaDescribedBy, error ? errorId : null].filter(Boolean).join(" ") || void 0;
51
68
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", children: [
52
- label && /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block text-sm font-medium text-primary mb-1.5", children: [
53
- label,
54
- required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-secondary", children: " *" })
55
- ] }),
69
+ label && /* @__PURE__ */ jsxRuntime.jsxs(
70
+ "label",
71
+ {
72
+ htmlFor: textareaId,
73
+ className: "block text-sm font-medium text-primary mb-1.5",
74
+ children: [
75
+ label,
76
+ required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-secondary", "aria-hidden": "true", children: " *" })
77
+ ]
78
+ }
79
+ ),
56
80
  /* @__PURE__ */ jsxRuntime.jsx(
57
81
  "textarea",
58
82
  {
83
+ ...props,
59
84
  ref,
85
+ id: textareaId,
60
86
  rows,
87
+ required,
88
+ "aria-required": ariaRequired ?? (required ? true : void 0),
89
+ "aria-invalid": ariaInvalid ?? (error ? true : void 0),
90
+ "aria-describedby": describedBy,
61
91
  className: chunkADIDI7AJ_js.cn(
62
92
  textareaVariants({ size, variant: resolvedVariant }),
63
93
  className
64
- ),
65
- ...props
94
+ )
66
95
  }
67
96
  ),
68
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 text-sm text-red-600", children: error })
97
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { id: errorId, role: "alert", className: "mt-1 text-sm text-red-600", children: error })
69
98
  ] });
70
99
  }
71
100
  );
@@ -24,26 +24,55 @@ var textareaVariants = cva(
24
24
  }
25
25
  );
26
26
  var Textarea = React.forwardRef(
27
- ({ className, size, variant, label, error, required, rows = 4, ...props }, ref) => {
27
+ ({
28
+ className,
29
+ size,
30
+ variant,
31
+ label,
32
+ error,
33
+ required,
34
+ rows = 4,
35
+ id,
36
+ "aria-describedby": ariaDescribedBy,
37
+ "aria-invalid": ariaInvalid,
38
+ "aria-required": ariaRequired,
39
+ ...props
40
+ }, ref) => {
41
+ const generatedId = React.useId();
42
+ const textareaId = id ?? generatedId;
43
+ const errorId = `${textareaId}-error`;
28
44
  const resolvedVariant = error ? "error" : variant ?? "default";
45
+ const describedBy = [ariaDescribedBy, error ? errorId : null].filter(Boolean).join(" ") || void 0;
29
46
  return /* @__PURE__ */ jsxs("div", { className: "w-full", children: [
30
- label && /* @__PURE__ */ jsxs("label", { className: "block text-sm font-medium text-primary mb-1.5", children: [
31
- label,
32
- required && /* @__PURE__ */ jsx("span", { className: "text-secondary", children: " *" })
33
- ] }),
47
+ label && /* @__PURE__ */ jsxs(
48
+ "label",
49
+ {
50
+ htmlFor: textareaId,
51
+ className: "block text-sm font-medium text-primary mb-1.5",
52
+ children: [
53
+ label,
54
+ required && /* @__PURE__ */ jsx("span", { className: "text-secondary", "aria-hidden": "true", children: " *" })
55
+ ]
56
+ }
57
+ ),
34
58
  /* @__PURE__ */ jsx(
35
59
  "textarea",
36
60
  {
61
+ ...props,
37
62
  ref,
63
+ id: textareaId,
38
64
  rows,
65
+ required,
66
+ "aria-required": ariaRequired ?? (required ? true : void 0),
67
+ "aria-invalid": ariaInvalid ?? (error ? true : void 0),
68
+ "aria-describedby": describedBy,
39
69
  className: cn(
40
70
  textareaVariants({ size, variant: resolvedVariant }),
41
71
  className
42
- ),
43
- ...props
72
+ )
44
73
  }
45
74
  ),
46
- error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-sm text-red-600", children: error })
75
+ error && /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "mt-1 text-sm text-red-600", children: error })
47
76
  ] });
48
77
  }
49
78
  );
@@ -0,0 +1,78 @@
1
+ import { cn } from './chunk-IMKLN273.mjs';
2
+ import * as React from 'react';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ var SegmentedControl = React.forwardRef(
6
+ ({
7
+ options,
8
+ value,
9
+ onChange,
10
+ className,
11
+ "aria-label": ariaLabel,
12
+ "aria-labelledby": ariaLabelledBy
13
+ }, ref) => {
14
+ const optionRefs = React.useRef([]);
15
+ const selectedIndex = options.findIndex((option) => option.value === value);
16
+ const rovingIndex = selectedIndex === -1 ? 0 : selectedIndex;
17
+ const handleKeyDown = (event, index) => {
18
+ const lastIndex = options.length - 1;
19
+ let nextIndex;
20
+ switch (event.key) {
21
+ case "ArrowRight":
22
+ case "ArrowDown":
23
+ nextIndex = index === lastIndex ? 0 : index + 1;
24
+ break;
25
+ case "ArrowLeft":
26
+ case "ArrowUp":
27
+ nextIndex = index === 0 ? lastIndex : index - 1;
28
+ break;
29
+ case "Home":
30
+ nextIndex = 0;
31
+ break;
32
+ case "End":
33
+ nextIndex = lastIndex;
34
+ break;
35
+ default:
36
+ return;
37
+ }
38
+ event.preventDefault();
39
+ const nextOption = options[nextIndex];
40
+ if (!nextOption) return;
41
+ onChange(nextOption.value);
42
+ optionRefs.current[nextIndex]?.focus();
43
+ };
44
+ return /* @__PURE__ */ jsx(
45
+ "div",
46
+ {
47
+ ref,
48
+ className: cn("flex gap-1 bg-gray-100 rounded-full p-1", className),
49
+ role: "radiogroup",
50
+ "aria-label": ariaLabelledBy ? void 0 : ariaLabel,
51
+ "aria-labelledby": ariaLabelledBy,
52
+ children: options.map((option, index) => /* @__PURE__ */ jsx(
53
+ "button",
54
+ {
55
+ type: "button",
56
+ role: "radio",
57
+ "aria-checked": value === option.value,
58
+ tabIndex: index === rovingIndex ? 0 : -1,
59
+ ref: (node) => {
60
+ optionRefs.current[index] = node;
61
+ },
62
+ onKeyDown: (event) => handleKeyDown(event, index),
63
+ onClick: () => onChange(option.value),
64
+ className: cn(
65
+ "px-3 py-1.5 text-sm rounded-full transition-all",
66
+ value === option.value ? "bg-white text-gray-900 shadow-sm font-medium" : "text-gray-600 hover:text-gray-900"
67
+ ),
68
+ children: option.label
69
+ },
70
+ option.value
71
+ ))
72
+ }
73
+ );
74
+ }
75
+ );
76
+ SegmentedControl.displayName = "SegmentedControl";
77
+
78
+ export { SegmentedControl };
@@ -37,6 +37,7 @@ var FOCUSABLE_SELECTOR = [
37
37
  "[contenteditable]:not([contenteditable='false'])",
38
38
  '[tabindex]:not([tabindex="-1"])'
39
39
  ].join(", ");
40
+ var DEFAULT_Z_INDEX_CLASS = "z-50";
40
41
  function getFocusableElements(panel) {
41
42
  return Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
42
43
  (element) => !element.closest("[hidden], [aria-hidden='true'], details:not([open])")
@@ -54,6 +55,11 @@ var Modal = React__namespace.forwardRef(
54
55
  children,
55
56
  title,
56
57
  className,
58
+ variant = "centered",
59
+ overlayClassName,
60
+ zIndexClassName = DEFAULT_Z_INDEX_CLASS,
61
+ initialFocusRef,
62
+ returnFocusTo,
57
63
  "aria-label": ariaLabel,
58
64
  "aria-labelledby": ariaLabelledBy,
59
65
  "aria-describedby": ariaDescribedBy
@@ -62,10 +68,20 @@ var Modal = React__namespace.forwardRef(
62
68
  const generatedTitleId = React__namespace.useId();
63
69
  const titleId = title ? `${generatedTitleId}-title` : void 0;
64
70
  const labelledBy = ariaLabelledBy ?? titleId;
71
+ const isFullBleed = variant === "fullBleed";
72
+ const initialFocusRefLatest = React__namespace.useRef(initialFocusRef);
73
+ initialFocusRefLatest.current = initialFocusRef;
74
+ const returnFocusToLatest = React__namespace.useRef(returnFocusTo);
75
+ returnFocusToLatest.current = returnFocusTo;
65
76
  React__namespace.useEffect(() => {
66
77
  if (!isOpen) return;
67
78
  const trigger = document.activeElement instanceof HTMLElement ? document.activeElement : null;
68
79
  return () => {
80
+ const designated = returnFocusToLatest.current?.current ?? null;
81
+ if (designated && designated.isConnected) {
82
+ designated.focus();
83
+ return;
84
+ }
69
85
  if (trigger && trigger.isConnected) trigger.focus();
70
86
  };
71
87
  }, [isOpen]);
@@ -73,6 +89,11 @@ var Modal = React__namespace.forwardRef(
73
89
  if (!isOpen) return;
74
90
  const panel = panelRef.current;
75
91
  if (!panel) return;
92
+ const preferred = initialFocusRefLatest.current?.current ?? null;
93
+ if (preferred) {
94
+ preferred.focus();
95
+ return;
96
+ }
76
97
  const focusable = getFocusableElements(panel);
77
98
  (focusable[0] ?? panel).focus();
78
99
  }, [isOpen]);
@@ -124,14 +145,21 @@ var Modal = React__namespace.forwardRef(
124
145
  "div",
125
146
  {
126
147
  ref,
127
- className: "fixed inset-0 z-50 flex items-center justify-center p-4",
148
+ className: chunkADIDI7AJ_js.cn(
149
+ "fixed inset-0",
150
+ zIndexClassName,
151
+ // La mise en page centree (et sa marge de 16 px) n'existe que dans la
152
+ // variante `centered` : une surface bord a bord n'en veut pas.
153
+ !isFullBleed && "flex items-center justify-center p-4",
154
+ overlayClassName
155
+ ),
128
156
  role: "dialog",
129
157
  "aria-modal": "true",
130
158
  "aria-labelledby": labelledBy,
131
159
  "aria-label": labelledBy ? void 0 : ariaLabel,
132
160
  "aria-describedby": ariaDescribedBy,
133
161
  children: [
134
- /* @__PURE__ */ jsxRuntime.jsx(
162
+ !isFullBleed && /* @__PURE__ */ jsxRuntime.jsx(
135
163
  "div",
136
164
  {
137
165
  className: "absolute inset-0 bg-black/50",
@@ -145,7 +173,7 @@ var Modal = React__namespace.forwardRef(
145
173
  ref: panelRef,
146
174
  tabIndex: -1,
147
175
  className: chunkADIDI7AJ_js.cn(
148
- "relative bg-white rounded-2xl shadow-xl max-w-md w-full max-h-[90vh] overflow-auto outline-none",
176
+ isFullBleed ? "relative h-full w-full outline-none" : "relative bg-white rounded-2xl shadow-xl max-w-md w-full max-h-[90vh] overflow-auto outline-none",
149
177
  className
150
178
  ),
151
179
  children: [
@@ -186,7 +214,7 @@ var Modal = React__namespace.forwardRef(
186
214
  }
187
215
  )
188
216
  ] }),
189
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: chunkADIDI7AJ_js.cn(title ? "px-6 pb-6" : "p-6"), children })
217
+ isFullBleed ? children : /* @__PURE__ */ jsxRuntime.jsx("div", { className: chunkADIDI7AJ_js.cn(title ? "px-6 pb-6" : "p-6"), children })
190
218
  ]
191
219
  }
192
220
  )
@@ -25,19 +25,63 @@ function _interopNamespace(e) {
25
25
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
26
26
 
27
27
  var SegmentedControl = React__namespace.forwardRef(
28
- ({ options, value, onChange, className }, ref) => {
28
+ ({
29
+ options,
30
+ value,
31
+ onChange,
32
+ className,
33
+ "aria-label": ariaLabel,
34
+ "aria-labelledby": ariaLabelledBy
35
+ }, ref) => {
36
+ const optionRefs = React__namespace.useRef([]);
37
+ const selectedIndex = options.findIndex((option) => option.value === value);
38
+ const rovingIndex = selectedIndex === -1 ? 0 : selectedIndex;
39
+ const handleKeyDown = (event, index) => {
40
+ const lastIndex = options.length - 1;
41
+ let nextIndex;
42
+ switch (event.key) {
43
+ case "ArrowRight":
44
+ case "ArrowDown":
45
+ nextIndex = index === lastIndex ? 0 : index + 1;
46
+ break;
47
+ case "ArrowLeft":
48
+ case "ArrowUp":
49
+ nextIndex = index === 0 ? lastIndex : index - 1;
50
+ break;
51
+ case "Home":
52
+ nextIndex = 0;
53
+ break;
54
+ case "End":
55
+ nextIndex = lastIndex;
56
+ break;
57
+ default:
58
+ return;
59
+ }
60
+ event.preventDefault();
61
+ const nextOption = options[nextIndex];
62
+ if (!nextOption) return;
63
+ onChange(nextOption.value);
64
+ optionRefs.current[nextIndex]?.focus();
65
+ };
29
66
  return /* @__PURE__ */ jsxRuntime.jsx(
30
67
  "div",
31
68
  {
32
69
  ref,
33
70
  className: chunkADIDI7AJ_js.cn("flex gap-1 bg-gray-100 rounded-full p-1", className),
34
71
  role: "radiogroup",
35
- children: options.map((option) => /* @__PURE__ */ jsxRuntime.jsx(
72
+ "aria-label": ariaLabelledBy ? void 0 : ariaLabel,
73
+ "aria-labelledby": ariaLabelledBy,
74
+ children: options.map((option, index) => /* @__PURE__ */ jsxRuntime.jsx(
36
75
  "button",
37
76
  {
38
77
  type: "button",
39
78
  role: "radio",
40
79
  "aria-checked": value === option.value,
80
+ tabIndex: index === rovingIndex ? 0 : -1,
81
+ ref: (node) => {
82
+ optionRefs.current[index] = node;
83
+ },
84
+ onKeyDown: (event) => handleKeyDown(event, index),
41
85
  onClick: () => onChange(option.value),
42
86
  className: chunkADIDI7AJ_js.cn(
43
87
  "px-3 py-1.5 text-sm rounded-full transition-all",
@@ -25,17 +25,57 @@ function _interopNamespace(e) {
25
25
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
26
26
 
27
27
  var Tabs = React__namespace.forwardRef(
28
- ({ tabs, defaultTab, value, onChange, className }, ref) => {
28
+ ({
29
+ tabs,
30
+ defaultTab,
31
+ value,
32
+ onChange,
33
+ className,
34
+ "aria-label": ariaLabel,
35
+ "aria-labelledby": ariaLabelledBy
36
+ }, ref) => {
29
37
  const [internalValue, setInternalValue] = React__namespace.useState(
30
38
  defaultTab || tabs[0]?.key || ""
31
39
  );
32
40
  const activeKey = value ?? internalValue;
33
- const handleChange = (key) => {
34
- if (onChange) {
35
- onChange(key);
36
- } else {
37
- setInternalValue(key);
41
+ const idPrefix = React__namespace.useId();
42
+ const tabId = (key) => `${idPrefix}-tab-${key}`;
43
+ const panelId = (key) => `${idPrefix}-panel-${key}`;
44
+ const tabRefs = React__namespace.useRef([]);
45
+ const handleChange = React__namespace.useCallback(
46
+ (key) => {
47
+ if (onChange) {
48
+ onChange(key);
49
+ } else {
50
+ setInternalValue(key);
51
+ }
52
+ },
53
+ [onChange]
54
+ );
55
+ const handleKeyDown = (event, index) => {
56
+ const lastIndex = tabs.length - 1;
57
+ let nextIndex;
58
+ switch (event.key) {
59
+ case "ArrowRight":
60
+ nextIndex = index === lastIndex ? 0 : index + 1;
61
+ break;
62
+ case "ArrowLeft":
63
+ nextIndex = index === 0 ? lastIndex : index - 1;
64
+ break;
65
+ case "Home":
66
+ nextIndex = 0;
67
+ break;
68
+ case "End":
69
+ nextIndex = lastIndex;
70
+ break;
71
+ default:
72
+ return;
38
73
  }
74
+ event.preventDefault();
75
+ const nextTab = tabs[nextIndex];
76
+ if (!nextTab) return;
77
+ handleChange(nextTab.key);
78
+ tabRefs.current[nextIndex]?.focus();
39
79
  };
40
80
  const activeTab = tabs.find((t) => t.key === activeKey);
41
81
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: chunkADIDI7AJ_js.cn("w-full", className), children: [
@@ -43,13 +83,22 @@ var Tabs = React__namespace.forwardRef(
43
83
  "div",
44
84
  {
45
85
  role: "tablist",
86
+ "aria-label": ariaLabelledBy ? void 0 : ariaLabel,
87
+ "aria-labelledby": ariaLabelledBy,
46
88
  className: "flex border-b border-gray-200 gap-1 overflow-x-auto",
47
- children: tabs.map((tab) => /* @__PURE__ */ jsxRuntime.jsxs(
89
+ children: tabs.map((tab, index) => /* @__PURE__ */ jsxRuntime.jsxs(
48
90
  "button",
49
91
  {
92
+ id: tabId(tab.key),
50
93
  role: "tab",
51
94
  type: "button",
52
95
  "aria-selected": tab.key === activeKey,
96
+ "aria-controls": panelId(tab.key),
97
+ tabIndex: tab.key === activeKey ? 0 : -1,
98
+ ref: (node) => {
99
+ tabRefs.current[index] = node;
100
+ },
101
+ onKeyDown: (event) => handleKeyDown(event, index),
53
102
  onClick: () => handleChange(tab.key),
54
103
  className: chunkADIDI7AJ_js.cn(
55
104
  "flex items-center gap-2 px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 transition-colors -mb-px",
@@ -64,7 +113,16 @@ var Tabs = React__namespace.forwardRef(
64
113
  ))
65
114
  }
66
115
  ),
67
- /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tabpanel", className: "py-4", children: activeTab?.content })
116
+ activeTab && /* @__PURE__ */ jsxRuntime.jsx(
117
+ "div",
118
+ {
119
+ role: "tabpanel",
120
+ id: panelId(activeTab.key),
121
+ "aria-labelledby": tabId(activeTab.key),
122
+ className: "py-4",
123
+ children: activeTab.content
124
+ }
125
+ )
68
126
  ] });
69
127
  }
70
128
  );
@@ -15,6 +15,7 @@ var FOCUSABLE_SELECTOR = [
15
15
  "[contenteditable]:not([contenteditable='false'])",
16
16
  '[tabindex]:not([tabindex="-1"])'
17
17
  ].join(", ");
18
+ var DEFAULT_Z_INDEX_CLASS = "z-50";
18
19
  function getFocusableElements(panel) {
19
20
  return Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
20
21
  (element) => !element.closest("[hidden], [aria-hidden='true'], details:not([open])")
@@ -32,6 +33,11 @@ var Modal = React.forwardRef(
32
33
  children,
33
34
  title,
34
35
  className,
36
+ variant = "centered",
37
+ overlayClassName,
38
+ zIndexClassName = DEFAULT_Z_INDEX_CLASS,
39
+ initialFocusRef,
40
+ returnFocusTo,
35
41
  "aria-label": ariaLabel,
36
42
  "aria-labelledby": ariaLabelledBy,
37
43
  "aria-describedby": ariaDescribedBy
@@ -40,10 +46,20 @@ var Modal = React.forwardRef(
40
46
  const generatedTitleId = React.useId();
41
47
  const titleId = title ? `${generatedTitleId}-title` : void 0;
42
48
  const labelledBy = ariaLabelledBy ?? titleId;
49
+ const isFullBleed = variant === "fullBleed";
50
+ const initialFocusRefLatest = React.useRef(initialFocusRef);
51
+ initialFocusRefLatest.current = initialFocusRef;
52
+ const returnFocusToLatest = React.useRef(returnFocusTo);
53
+ returnFocusToLatest.current = returnFocusTo;
43
54
  React.useEffect(() => {
44
55
  if (!isOpen) return;
45
56
  const trigger = document.activeElement instanceof HTMLElement ? document.activeElement : null;
46
57
  return () => {
58
+ const designated = returnFocusToLatest.current?.current ?? null;
59
+ if (designated && designated.isConnected) {
60
+ designated.focus();
61
+ return;
62
+ }
47
63
  if (trigger && trigger.isConnected) trigger.focus();
48
64
  };
49
65
  }, [isOpen]);
@@ -51,6 +67,11 @@ var Modal = React.forwardRef(
51
67
  if (!isOpen) return;
52
68
  const panel = panelRef.current;
53
69
  if (!panel) return;
70
+ const preferred = initialFocusRefLatest.current?.current ?? null;
71
+ if (preferred) {
72
+ preferred.focus();
73
+ return;
74
+ }
54
75
  const focusable = getFocusableElements(panel);
55
76
  (focusable[0] ?? panel).focus();
56
77
  }, [isOpen]);
@@ -102,14 +123,21 @@ var Modal = React.forwardRef(
102
123
  "div",
103
124
  {
104
125
  ref,
105
- className: "fixed inset-0 z-50 flex items-center justify-center p-4",
126
+ className: cn(
127
+ "fixed inset-0",
128
+ zIndexClassName,
129
+ // La mise en page centree (et sa marge de 16 px) n'existe que dans la
130
+ // variante `centered` : une surface bord a bord n'en veut pas.
131
+ !isFullBleed && "flex items-center justify-center p-4",
132
+ overlayClassName
133
+ ),
106
134
  role: "dialog",
107
135
  "aria-modal": "true",
108
136
  "aria-labelledby": labelledBy,
109
137
  "aria-label": labelledBy ? void 0 : ariaLabel,
110
138
  "aria-describedby": ariaDescribedBy,
111
139
  children: [
112
- /* @__PURE__ */ jsx(
140
+ !isFullBleed && /* @__PURE__ */ jsx(
113
141
  "div",
114
142
  {
115
143
  className: "absolute inset-0 bg-black/50",
@@ -123,7 +151,7 @@ var Modal = React.forwardRef(
123
151
  ref: panelRef,
124
152
  tabIndex: -1,
125
153
  className: cn(
126
- "relative bg-white rounded-2xl shadow-xl max-w-md w-full max-h-[90vh] overflow-auto outline-none",
154
+ isFullBleed ? "relative h-full w-full outline-none" : "relative bg-white rounded-2xl shadow-xl max-w-md w-full max-h-[90vh] overflow-auto outline-none",
127
155
  className
128
156
  ),
129
157
  children: [
@@ -164,7 +192,7 @@ var Modal = React.forwardRef(
164
192
  }
165
193
  )
166
194
  ] }),
167
- /* @__PURE__ */ jsx("div", { className: cn(title ? "px-6 pb-6" : "p-6"), children })
195
+ isFullBleed ? children : /* @__PURE__ */ jsx("div", { className: cn(title ? "px-6 pb-6" : "p-6"), children })
168
196
  ]
169
197
  }
170
198
  )
@@ -0,0 +1,109 @@
1
+ import { cn } from './chunk-IMKLN273.mjs';
2
+ import * as React from 'react';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ var Tabs = React.forwardRef(
6
+ ({
7
+ tabs,
8
+ defaultTab,
9
+ value,
10
+ onChange,
11
+ className,
12
+ "aria-label": ariaLabel,
13
+ "aria-labelledby": ariaLabelledBy
14
+ }, ref) => {
15
+ const [internalValue, setInternalValue] = React.useState(
16
+ defaultTab || tabs[0]?.key || ""
17
+ );
18
+ const activeKey = value ?? internalValue;
19
+ const idPrefix = React.useId();
20
+ const tabId = (key) => `${idPrefix}-tab-${key}`;
21
+ const panelId = (key) => `${idPrefix}-panel-${key}`;
22
+ const tabRefs = React.useRef([]);
23
+ const handleChange = React.useCallback(
24
+ (key) => {
25
+ if (onChange) {
26
+ onChange(key);
27
+ } else {
28
+ setInternalValue(key);
29
+ }
30
+ },
31
+ [onChange]
32
+ );
33
+ const handleKeyDown = (event, index) => {
34
+ const lastIndex = tabs.length - 1;
35
+ let nextIndex;
36
+ switch (event.key) {
37
+ case "ArrowRight":
38
+ nextIndex = index === lastIndex ? 0 : index + 1;
39
+ break;
40
+ case "ArrowLeft":
41
+ nextIndex = index === 0 ? lastIndex : index - 1;
42
+ break;
43
+ case "Home":
44
+ nextIndex = 0;
45
+ break;
46
+ case "End":
47
+ nextIndex = lastIndex;
48
+ break;
49
+ default:
50
+ return;
51
+ }
52
+ event.preventDefault();
53
+ const nextTab = tabs[nextIndex];
54
+ if (!nextTab) return;
55
+ handleChange(nextTab.key);
56
+ tabRefs.current[nextIndex]?.focus();
57
+ };
58
+ const activeTab = tabs.find((t) => t.key === activeKey);
59
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("w-full", className), children: [
60
+ /* @__PURE__ */ jsx(
61
+ "div",
62
+ {
63
+ role: "tablist",
64
+ "aria-label": ariaLabelledBy ? void 0 : ariaLabel,
65
+ "aria-labelledby": ariaLabelledBy,
66
+ className: "flex border-b border-gray-200 gap-1 overflow-x-auto",
67
+ children: tabs.map((tab, index) => /* @__PURE__ */ jsxs(
68
+ "button",
69
+ {
70
+ id: tabId(tab.key),
71
+ role: "tab",
72
+ type: "button",
73
+ "aria-selected": tab.key === activeKey,
74
+ "aria-controls": panelId(tab.key),
75
+ tabIndex: tab.key === activeKey ? 0 : -1,
76
+ ref: (node) => {
77
+ tabRefs.current[index] = node;
78
+ },
79
+ onKeyDown: (event) => handleKeyDown(event, index),
80
+ onClick: () => handleChange(tab.key),
81
+ className: cn(
82
+ "flex items-center gap-2 px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 transition-colors -mb-px",
83
+ tab.key === activeKey ? "border-secondary text-secondary" : "border-transparent text-text-secondary hover:text-primary hover:border-gray-300"
84
+ ),
85
+ children: [
86
+ tab.icon && /* @__PURE__ */ jsx("span", { className: "shrink-0", children: tab.icon }),
87
+ tab.label
88
+ ]
89
+ },
90
+ tab.key
91
+ ))
92
+ }
93
+ ),
94
+ activeTab && /* @__PURE__ */ jsx(
95
+ "div",
96
+ {
97
+ role: "tabpanel",
98
+ id: panelId(activeTab.key),
99
+ "aria-labelledby": tabId(activeTab.key),
100
+ className: "py-4",
101
+ children: activeTab.content
102
+ }
103
+ )
104
+ ] });
105
+ }
106
+ );
107
+ Tabs.displayName = "Tabs";
108
+
109
+ export { Tabs };