@edux-design/buttons 0.1.4 → 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.
package/dist/index.js CHANGED
@@ -34,7 +34,12 @@ __export(index_exports, {
34
34
  ButtonGroup: () => ButtonGroup,
35
35
  ButtonGroupItem: () => ButtonGroupItem,
36
36
  IconButton: () => IconButton,
37
- MenuButton: () => MenuButton
37
+ MenuButton: () => MenuButton,
38
+ Toolbar: () => Toolbar,
39
+ ToolbarButton: () => ToolbarButton,
40
+ ToolbarGroup: () => ToolbarGroup,
41
+ ToolbarSeparator: () => ToolbarSeparator,
42
+ ToolbarToggleButton: () => ToolbarToggleButton
38
43
  });
39
44
  module.exports = __toCommonJS(index_exports);
40
45
 
@@ -65,21 +70,29 @@ var IconButton = (0, import_react.forwardRef)(
65
70
  "[IconButton]: Multiple variant props passed. Using the highest priority only."
66
71
  );
67
72
  }
73
+ const hasAccessibleName = Boolean(
74
+ props["aria-label"] || props["aria-labelledby"]
75
+ );
76
+ if (!hasAccessibleName) {
77
+ console.warn(
78
+ "[IconButton]: Provide an accessible name with aria-label or aria-labelledby."
79
+ );
80
+ }
68
81
  }
69
82
  const defaults = "flex justify-center items-center focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out rounded-full cursor-pointer";
70
- const sizes = {
71
- small: "h-[36px] w-[36px] text-sm",
72
- medium: "h-[44px] w-[44px] text-base",
73
- large: "h-[56px] w-[56px] text-lg"
83
+ const sizes2 = {
84
+ small: "size-control-sm text-sm",
85
+ medium: "size-control-md text-base",
86
+ large: "size-control-lg text-lg"
74
87
  };
75
88
  const variants = {
76
- primary: "border-2 border-border-primary-emphasis bg-bg-primary-base text-white hover:bg-border-focus hover:text-fg-focus hover:border-border-emphasis hover:shadow-md active:bg-border-focus active:bg-bg-focus-overlay active:text-fg-focus",
89
+ primary: "border-2 border-border-primary-emphasis bg-bg-primary-base text-fg-invert hover:bg-border-focus hover:text-fg-focus hover:border-border-emphasis hover:shadow-md active:bg-border-focus active:bg-bg-focus-overlay active:text-fg-focus",
77
90
  secondary: "border-2 border-border-primary-base bg-bg-base text-fg-primary-emphasis hover:shadow-md hover:bg-bg-primary-subtle active:bg-bg-primary-subtle-90",
78
91
  bare: "bg-bg-base text-fg-primary-emphasis hover:bg-bg-minimal active:bg-bg-minimal-5",
79
92
  danger: "bg-bg-danger-base border-2 border-border-danger-emphasis text-white hover:bg-bg-danger-base-5 active:bg-bg-danger-base-10",
80
93
  disabled: "border-2 border-border-subtle bg-bg-inactive text-fg-inactive hover:no-underline cursor-not-allowed"
81
94
  };
82
- const STYLES = (0, import_utils.cx)(defaults, sizes[size], variants[variant], className);
95
+ const STYLES = (0, import_utils.cx)(defaults, sizes2[size], variants[variant], className);
83
96
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
84
97
  "button",
85
98
  {
@@ -137,21 +150,21 @@ var Button = (0, import_react2.forwardRef)(
137
150
  }
138
151
  const defaults = "flex justify-center items-center gap-8 focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out cursor-pointer";
139
152
  const fonts = "text-bold tracking-normal hover:underline hover:underline-offset-(--underline-offset-btn) active:underline active:underline-offset-(--underline-offset-btn)";
140
- const sizes = {
153
+ const sizes2 = {
141
154
  small: "h-control-sm py-8 px-16 rounded-lg text-sm",
142
155
  medium: "h-control-md py-8 px-16 rounded-lg text-base",
143
156
  large: "h-control-lg py-8 px-16 rounded-lg text-lg"
144
157
  };
145
158
  const width = isStretched ? "w-full" : "w-fit-content";
146
159
  const variants = {
147
- primary: "border-2 border-border-primary-emphasis bg-bg-primary-base text-white hover:bg-border-focus hover:text-fg-focus hover:border-border-emphasis hover:shadow-md active:bg-border-focus active:bg-bg-focus-overlay active:text-fg-focus",
160
+ primary: "border-2 border-border-primary-emphasis bg-bg-primary-base text-fg-invert hover:bg-border-focus hover:text-fg-focus hover:border-border-emphasis hover:shadow-md active:bg-border-focus active:bg-bg-focus-overlay active:text-fg-focus",
148
161
  secondary: "border-2 border-border-primary-base bg-bg-base text-fg-primary-emphasis hover:shadow-md hover:bg-bg-primary-subtle active:bg-bg-primary-subtle-90",
149
162
  bare: "bg-bg-base text-fg-primary-emphasis hover:bg-bg-minimal active:bg-bg-minimal-5",
150
163
  dangerBare: "bg-bg-base text-fg-danger-emphasis hover:bg-bg-danger-subtle active:bg-bg-danger-subtle",
151
164
  danger: "bg-bg-danger-base border-2 border-border-danger-emphasis text-white hover:bg-bg-danger-base-5 active:bg-bg-danger-base-10",
152
165
  disabled: "border-2 border-border-subtle bg-bg-inactive text-fg-inactive hover:no-underline"
153
166
  };
154
- const STYLES = (0, import_utils2.cx)(defaults, sizes[size], width, fonts, variants[variant]);
167
+ const STYLES = (0, import_utils2.cx)(defaults, sizes2[size], width, fonts, variants[variant]);
155
168
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
156
169
  "button",
157
170
  {
@@ -173,13 +186,7 @@ var Button = (0, import_react2.forwardRef)(
173
186
  );
174
187
 
175
188
  // src/elements/MenuButton.jsx
176
- var import_react4 = __toESM(require("react"));
177
-
178
- // src/util/MenuButtonContext.jsx
179
189
  var import_react3 = __toESM(require("react"));
180
- var MenuButtonContext = (0, import_react3.createContext)();
181
-
182
- // src/elements/MenuButton.jsx
183
190
  var import_utils3 = require("@edux-design/utils");
184
191
 
185
192
  // ../icons/dist/index.mjs
@@ -208,22 +215,41 @@ var Chevron_default = SvgChevron;
208
215
 
209
216
  // src/elements/MenuButton.jsx
210
217
  var import_jsx_runtime4 = require("react/jsx-runtime");
211
- function MenuButton({ children, size }) {
212
- const SPLIT_BUTTON_STYLES = (0, import_utils3.cx)(
213
- "w-[36px] border-l-[1px] border-l-black flex flex-row justify-center items-center",
218
+ var MenuButton = (0, import_react3.forwardRef)(function MenuButton2({
219
+ children,
220
+ caret,
221
+ size = "medium",
222
+ className,
223
+ type = "button",
224
+ "aria-haspopup": ariaHasPopup = "menu",
225
+ ...props
226
+ }, ref) {
227
+ const styles = (0, import_utils3.cx)(
228
+ "inline-flex items-center justify-center gap-8 rounded-lg border-2 border-border-primary-base bg-bg-base px-16 text-fg-primary-emphasis transition-all duration-300 ease-in-out outline-none focus:shadow-focus focus-visible:shadow-focus disabled:cursor-not-allowed disabled:border-border-subtle disabled:bg-bg-inactive disabled:text-fg-inactive",
214
229
  {
215
- "h-[32px]": size === "small",
216
- "h-[40px] py-2": size === "medium" || size === void 0
230
+ "min-h-control-sm text-sm": size === "small",
231
+ "min-h-control-md text-base": size === "medium"
232
+ },
233
+ className
234
+ );
235
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
236
+ "button",
237
+ {
238
+ ...props,
239
+ ref,
240
+ type,
241
+ "aria-haspopup": ariaHasPopup,
242
+ className: styles,
243
+ children: [
244
+ children ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children }) : null,
245
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "aria-hidden": "true", className: "flex items-center text-current", children: caret ?? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Chevron_default, {}) })
246
+ ]
217
247
  }
218
248
  );
219
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(MenuButtonContext.Provider, { value: true, children: [
220
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: SPLIT_BUTTON_STYLES, children }),
221
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Chevron_default, {})
222
- ] });
223
- }
249
+ });
224
250
 
225
251
  // src/elements/ButtonGroup.jsx
226
- var import_react5 = __toESM(require("react"));
252
+ var import_react4 = __toESM(require("react"));
227
253
  var import_utils4 = require("@edux-design/utils");
228
254
  var import_jsx_runtime5 = require("react/jsx-runtime");
229
255
  var SIZE_STYLES = {
@@ -238,8 +264,8 @@ var SIZE_STYLES = {
238
264
  }
239
265
  };
240
266
  var isBrowser = typeof window !== "undefined";
241
- var useIsomorphicLayoutEffect = isBrowser ? import_react5.useLayoutEffect : import_react5.useEffect;
242
- var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
267
+ var useIsomorphicLayoutEffect = isBrowser ? import_react4.useLayoutEffect : import_react4.useEffect;
268
+ var ButtonGroup = (0, import_react4.forwardRef)(function ButtonGroup2({
243
269
  value,
244
270
  defaultValue,
245
271
  onChange,
@@ -249,7 +275,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
249
275
  children,
250
276
  ...rest
251
277
  }, forwardedRef) {
252
- const normalizedOptions = (0, import_react5.useMemo)(() => {
278
+ const normalizedOptions = (0, import_react4.useMemo)(() => {
253
279
  const childItems = (0, import_utils4.filterChildrenByDisplayName)(children, "ButtonGroupItem");
254
280
  return childItems.map((child, index) => {
255
281
  const props = child.props || {};
@@ -264,30 +290,30 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
264
290
  }, [children]);
265
291
  const sizeStyles = SIZE_STYLES[size] ?? SIZE_STYLES.medium;
266
292
  const initialValue = value ?? defaultValue ?? (normalizedOptions.length ? normalizedOptions[0].value : void 0);
267
- const [internalValue, setInternalValue] = (0, import_react5.useState)(initialValue);
293
+ const [internalValue, setInternalValue] = (0, import_react4.useState)(initialValue);
268
294
  const isControlled = value !== void 0;
269
295
  const selectedValue = isControlled ? value : internalValue;
270
- (0, import_react5.useEffect)(() => {
296
+ (0, import_react4.useEffect)(() => {
271
297
  if (!isControlled && defaultValue !== void 0) {
272
298
  setInternalValue(defaultValue);
273
299
  }
274
300
  }, [defaultValue, isControlled]);
275
- (0, import_react5.useEffect)(() => {
301
+ (0, import_react4.useEffect)(() => {
276
302
  if (isControlled) return;
277
303
  if (normalizedOptions.length && !normalizedOptions.some((option) => option.value === internalValue)) {
278
304
  setInternalValue(normalizedOptions[0].value);
279
305
  }
280
306
  }, [internalValue, isControlled, normalizedOptions]);
281
- const buttonRefs = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
282
- const containerRef = (0, import_react5.useRef)(null);
283
- const indicatorRef = (0, import_react5.useRef)({ width: 0, height: 0, x: 0, y: 0 });
284
- const [indicator, setIndicator] = (0, import_react5.useState)({
307
+ const buttonRefs = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
308
+ const containerRef = (0, import_react4.useRef)(null);
309
+ const indicatorRef = (0, import_react4.useRef)({ width: 0, height: 0, x: 0, y: 0 });
310
+ const [indicator, setIndicator] = (0, import_react4.useState)({
285
311
  width: 0,
286
312
  height: 0,
287
313
  x: 0,
288
314
  y: 0
289
315
  });
290
- const setContainerRef = (0, import_react5.useCallback)(
316
+ const setContainerRef = (0, import_react4.useCallback)(
291
317
  (node) => {
292
318
  containerRef.current = node;
293
319
  if (typeof forwardedRef === "function") {
@@ -298,7 +324,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
298
324
  },
299
325
  [forwardedRef]
300
326
  );
301
- const registerButtonRef = (0, import_react5.useCallback)((optionValue) => {
327
+ const registerButtonRef = (0, import_react4.useCallback)((optionValue) => {
302
328
  return (node) => {
303
329
  if (!buttonRefs.current) return;
304
330
  if (node) {
@@ -308,7 +334,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
308
334
  }
309
335
  };
310
336
  }, []);
311
- const updateIndicator = (0, import_react5.useCallback)(() => {
337
+ const updateIndicator = (0, import_react4.useCallback)(() => {
312
338
  const container = containerRef.current;
313
339
  const target = buttonRefs.current.get(selectedValue);
314
340
  if (!container || !target) {
@@ -335,13 +361,13 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
335
361
  useIsomorphicLayoutEffect(() => {
336
362
  updateIndicator();
337
363
  }, [updateIndicator, normalizedOptions.length, size]);
338
- (0, import_react5.useEffect)(() => {
364
+ (0, import_react4.useEffect)(() => {
339
365
  if (!isBrowser) return;
340
366
  const handleResize = () => updateIndicator();
341
367
  window.addEventListener("resize", handleResize);
342
368
  return () => window.removeEventListener("resize", handleResize);
343
369
  }, [updateIndicator]);
344
- (0, import_react5.useEffect)(() => {
370
+ (0, import_react4.useEffect)(() => {
345
371
  if (!isBrowser) return;
346
372
  const node = containerRef.current;
347
373
  if (!node || typeof ResizeObserver === "undefined") return;
@@ -351,13 +377,13 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
351
377
  observer.observe(node);
352
378
  return () => observer.disconnect();
353
379
  }, [updateIndicator]);
354
- const focusOption = (0, import_react5.useCallback)((option) => {
380
+ const focusOption = (0, import_react4.useCallback)((option) => {
355
381
  const node = buttonRefs.current.get(option.value);
356
382
  if (node) {
357
383
  node.focus();
358
384
  }
359
385
  }, []);
360
- const handleSelect = (0, import_react5.useCallback)(
386
+ const handleSelect = (0, import_react4.useCallback)(
361
387
  (option) => {
362
388
  if (option.disabled || option.value === selectedValue) {
363
389
  return;
@@ -371,7 +397,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
371
397
  },
372
398
  [isControlled, onChange, selectedValue]
373
399
  );
374
- const getNextEnabledIndex = (0, import_react5.useCallback)(
400
+ const getNextEnabledIndex = (0, import_react4.useCallback)(
375
401
  (startIndex, direction) => {
376
402
  if (!normalizedOptions.length) return -1;
377
403
  let nextIndex = startIndex;
@@ -385,10 +411,10 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
385
411
  },
386
412
  [normalizedOptions]
387
413
  );
388
- const findFirstEnabledIndex = (0, import_react5.useCallback)(() => {
414
+ const findFirstEnabledIndex = (0, import_react4.useCallback)(() => {
389
415
  return normalizedOptions.findIndex((option) => !option.disabled);
390
416
  }, [normalizedOptions]);
391
- const findLastEnabledIndex = (0, import_react5.useCallback)(() => {
417
+ const findLastEnabledIndex = (0, import_react4.useCallback)(() => {
392
418
  for (let i = normalizedOptions.length - 1; i >= 0; i -= 1) {
393
419
  if (!normalizedOptions[i].disabled) {
394
420
  return i;
@@ -397,8 +423,15 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
397
423
  return -1;
398
424
  }, [normalizedOptions]);
399
425
  const indicatorVisible = indicator.width > 0 && indicator.height > 0;
426
+ const selectedIndex = normalizedOptions.findIndex(
427
+ (option) => option.value === selectedValue && !option.disabled
428
+ );
429
+ const firstEnabledIndex = normalizedOptions.findIndex(
430
+ (option) => !option.disabled
431
+ );
432
+ const activeTabStopIndex = selectedIndex >= 0 ? selectedIndex : firstEnabledIndex;
400
433
  const rootClasses = (0, import_utils4.cx)(
401
- "inline-flex h-[44px] rounded-lg border-2 border-border-subtle bg-bg-base/90 p-2 shadow-inner",
434
+ "inline-flex h-control-md rounded-lg border-2 border-border-subtle bg-bg-base/90 p-2 shadow-inner",
402
435
  isStretched ? "w-full" : "w-auto",
403
436
  className
404
437
  );
@@ -407,7 +440,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
407
440
  !isStretched && "w-auto"
408
441
  );
409
442
  const buttonBaseClasses = (0, import_utils4.cx)(
410
- "relative z-10 h-[36px] inline-flex items-center justify-center gap-2 whitespace-nowrap font-semibold tracking-tight transition-colors duration-200 ease-out focus-visible:outline-none focus-visible:shadow-focus motion-reduce:duration-0",
443
+ "relative z-10 h-control-sm inline-flex items-center justify-center gap-2 whitespace-nowrap font-semibold tracking-tight transition-colors duration-200 ease-out focus-visible:outline-none focus-visible:shadow-focus motion-reduce:duration-0",
411
444
  sizeStyles.button,
412
445
  isStretched ? "flex-1" : "w-auto"
413
446
  );
@@ -417,6 +450,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
417
450
  ...rest,
418
451
  ref: setContainerRef,
419
452
  role: "radiogroup",
453
+ "aria-orientation": "horizontal",
420
454
  className: rootClasses,
421
455
  children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: trackClasses, children: [
422
456
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
@@ -424,13 +458,13 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
424
458
  {
425
459
  "aria-hidden": "true",
426
460
  className: (0, import_utils4.cx)(
427
- "pointer-events-none h-[44px] absolute rounded-md border-2 border-border-primary-emphasis bg-bg-base shadow-[0_8px_20px_rgb(22_92_131/0.18)] transition-[transform,width,height] duration-300 ease-out motion-reduce:duration-0",
461
+ "pointer-events-none h-control-md absolute rounded-md border-2 border-border-primary-emphasis bg-bg-base shadow-[0_8px_20px_rgb(22_92_131/0.18)] transition-[transform,width,height] duration-300 ease-out motion-reduce:duration-0",
428
462
  !indicatorVisible && "opacity-0"
429
463
  ),
430
464
  style: {
431
465
  width: indicator.width || void 0,
432
466
  height: indicator.height || void 0,
433
- transform: `translate(${indicator.x - 4}px`
467
+ transform: `translate(${indicator.x - 4}px, ${indicator.y - 4}px)`
434
468
  }
435
469
  }
436
470
  ),
@@ -488,6 +522,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
488
522
  type: "button",
489
523
  role: "radio",
490
524
  "aria-checked": isActive,
525
+ tabIndex: option.disabled ? -1 : index === activeTabStopIndex ? 0 : -1,
491
526
  disabled: option.disabled,
492
527
  className: buttonClasses,
493
528
  onClick: () => handleSelect(option),
@@ -522,17 +557,17 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
522
557
  });
523
558
 
524
559
  // src/elements/ButtonGroupItem.jsx
525
- var import_react6 = __toESM(require("react"));
560
+ var import_react5 = __toESM(require("react"));
526
561
  function ButtonGroupItem(_props) {
527
562
  return null;
528
563
  }
529
564
  ButtonGroupItem.displayName = "ButtonGroupItem";
530
565
 
531
566
  // src/elements/ButtonCounter.jsx
532
- var import_react7 = __toESM(require("react"));
567
+ var import_react6 = __toESM(require("react"));
533
568
  var import_utils5 = require("@edux-design/utils");
534
569
  var import_jsx_runtime6 = require("react/jsx-runtime");
535
- var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
570
+ var ButtonCounter = (0, import_react6.forwardRef)(function ButtonCounter2({
536
571
  value,
537
572
  defaultValue = 1,
538
573
  onChange,
@@ -555,20 +590,21 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
555
590
  decrementButtonProps,
556
591
  incrementButtonProps,
557
592
  className,
593
+ "aria-label": ariaLabel,
558
594
  ...rest
559
595
  }, ref) {
560
596
  const isControlled = value !== void 0;
561
- const [internalValue, setInternalValue] = (0, import_react7.useState)(defaultValue);
597
+ const [internalValue, setInternalValue] = (0, import_react6.useState)(defaultValue);
562
598
  const currentValue = isControlled ? value : internalValue;
563
- const clampedValue = (0, import_react7.useMemo)(() => {
599
+ const clampedValue = (0, import_react6.useMemo)(() => {
564
600
  const safe = Number.isFinite(currentValue) ? currentValue : defaultValue;
565
601
  return Math.min(max, Math.max(min, safe));
566
602
  }, [currentValue, defaultValue, max, min]);
567
- const commitValue = (0, import_react7.useCallback)(
603
+ const commitValue = (0, import_react6.useCallback)(
568
604
  (next) => {
569
605
  const clamped = Math.min(max, Math.max(min, next));
570
606
  if (!isControlled) setInternalValue(clamped);
571
- onChange == null ? void 0 : onChange(clamped);
607
+ onChange?.(clamped);
572
608
  },
573
609
  [isControlled, max, min, onChange]
574
610
  );
@@ -600,16 +636,16 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
600
636
  }
601
637
  const CONTROL_SIZES = {
602
638
  small: {
603
- button: "h-[36px] w-[36px] text-sm",
604
- display: "h-[36px] min-w-[48px] px-12 text-sm"
639
+ button: "size-control-sm text-sm",
640
+ display: "h-control-sm min-w-48 px-12 text-sm"
605
641
  },
606
642
  medium: {
607
- button: "h-[44px] w-[44px] text-base",
608
- display: "h-[44px] min-w-[56px] px-12 text-base"
643
+ button: "size-control-md text-base",
644
+ display: "h-control-md min-w-56 px-12 text-base"
609
645
  },
610
646
  large: {
611
- button: "h-[56px] w-[56px] text-lg",
612
- display: "h-[56px] min-w-[72px] px-16 text-lg"
647
+ button: "size-control-lg text-lg",
648
+ display: "h-control-lg min-w-72 px-16 text-lg"
613
649
  }
614
650
  };
615
651
  const defaultsButton = "flex justify-center items-center focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out cursor-pointer";
@@ -617,8 +653,8 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
617
653
  const width = isStretched ? "w-full" : "w-fit-content";
618
654
  const variants = {
619
655
  primary: {
620
- button: "border-2 border-border-primary-emphasis bg-bg-primary-base text-white hover:bg-border-focus hover:text-fg-focus hover:border-border-emphasis hover:shadow-md active:bg-border-focus active:bg-bg-focus-overlay active:text-fg-focus",
621
- display: "border-y-2 border-x-2 border-border-primary-emphasis bg-bg-primary-base text-white"
656
+ button: "border-2 border-border-primary-emphasis bg-bg-primary-base text-fg-invert hover:bg-border-focus hover:text-fg-focus hover:border-border-emphasis hover:shadow-md active:bg-border-focus active:bg-bg-focus-overlay active:text-fg-focus",
657
+ display: "border-y-2 border-x-2 border-border-primary-emphasis bg-bg-primary-base text-fg-invert"
622
658
  },
623
659
  secondary: {
624
660
  button: "border-2 border-border-primary-base bg-bg-base text-fg-primary-emphasis hover:shadow-md hover:bg-bg-primary-subtle active:bg-bg-primary-subtle-90",
@@ -667,14 +703,17 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
667
703
  );
668
704
  const effectiveDecrementIcon = decrementIcon ?? startIcon ?? "-";
669
705
  const effectiveIncrementIcon = incrementIcon ?? endIcon ?? "+";
670
- const [draftValue, setDraftValue] = (0, import_react7.useState)(String(clampedValue));
671
- const [isEditing, setIsEditing] = (0, import_react7.useState)(false);
672
- const inputRef = (0, import_react7.useRef)(null);
673
- (0, import_react7.useEffect)(() => {
706
+ const generatedValueId = (0, import_react6.useId)();
707
+ const valueElementId = inputProps?.id ?? generatedValueId;
708
+ const valueAriaLabel = inputProps?.["aria-label"] ?? ariaLabel ?? "Value";
709
+ const [draftValue, setDraftValue] = (0, import_react6.useState)(String(clampedValue));
710
+ const [isEditing, setIsEditing] = (0, import_react6.useState)(false);
711
+ const inputRef = (0, import_react6.useRef)(null);
712
+ (0, import_react6.useEffect)(() => {
674
713
  if (isEditing) return;
675
714
  setDraftValue(String(clampedValue));
676
715
  }, [clampedValue, isEditing]);
677
- const commitDraft = (0, import_react7.useCallback)(() => {
716
+ const commitDraft = (0, import_react6.useCallback)(() => {
678
717
  const trimmed = String(draftValue).trim();
679
718
  if (trimmed === "") {
680
719
  setDraftValue(String(clampedValue));
@@ -699,17 +738,17 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
699
738
  {
700
739
  ...decrementButtonProps,
701
740
  type: "button",
702
- "aria-label": (decrementButtonProps == null ? void 0 : decrementButtonProps["aria-label"]) ?? "Decrease",
741
+ "aria-label": decrementButtonProps?.["aria-label"] ?? "Decrease",
742
+ "aria-controls": valueElementId,
703
743
  "aria-disabled": decrementDisabled,
704
744
  disabled: decrementDisabled,
705
745
  className: (0, import_utils5.cx)(
706
746
  leftButtonStyles,
707
747
  decrementDisabled && variant !== "disabled" ? variants.disabled.button : null,
708
- decrementButtonProps == null ? void 0 : decrementButtonProps.className
748
+ decrementButtonProps?.className
709
749
  ),
710
750
  onClick: (event) => {
711
- var _a;
712
- (_a = decrementButtonProps == null ? void 0 : decrementButtonProps.onClick) == null ? void 0 : _a.call(decrementButtonProps, event);
751
+ decrementButtonProps?.onClick?.(event);
713
752
  if (event.defaultPrevented) return;
714
753
  commitValue(clampedValue - step);
715
754
  },
@@ -723,38 +762,35 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
723
762
  ref: inputRef,
724
763
  type: "number",
725
764
  inputMode: "numeric",
765
+ id: valueElementId,
726
766
  disabled: !!disabled,
727
- "aria-label": (inputProps == null ? void 0 : inputProps["aria-label"]) ?? "Value",
767
+ "aria-label": valueAriaLabel,
728
768
  className: (0, import_utils5.cx)(
729
769
  displayStyles,
730
- "w-[44px] text-center outline-none bg-transparent text-fg-base",
770
+ "w-control-md text-center outline-none bg-transparent text-fg-base",
731
771
  "[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-inner-spin-button]:m-0",
732
772
  "focus:shadow-focus focus-visible:shadow-focus focus:z-15 focus-within:z-15",
733
- inputProps == null ? void 0 : inputProps.className
773
+ inputProps?.className
734
774
  ),
735
775
  value: draftValue,
736
776
  onChange: (event) => {
737
- var _a;
738
- (_a = inputProps == null ? void 0 : inputProps.onChange) == null ? void 0 : _a.call(inputProps, event);
777
+ inputProps?.onChange?.(event);
739
778
  if (event.defaultPrevented) return;
740
779
  setDraftValue(event.target.value);
741
780
  },
742
781
  onFocus: (event) => {
743
- var _a;
744
- (_a = inputProps == null ? void 0 : inputProps.onFocus) == null ? void 0 : _a.call(inputProps, event);
782
+ inputProps?.onFocus?.(event);
745
783
  if (event.defaultPrevented) return;
746
784
  setIsEditing(true);
747
785
  },
748
786
  onBlur: (event) => {
749
- var _a;
750
- (_a = inputProps == null ? void 0 : inputProps.onBlur) == null ? void 0 : _a.call(inputProps, event);
787
+ inputProps?.onBlur?.(event);
751
788
  if (event.defaultPrevented) return;
752
789
  setIsEditing(false);
753
790
  commitDraft();
754
791
  },
755
792
  onKeyDown: (event) => {
756
- var _a;
757
- (_a = inputProps == null ? void 0 : inputProps.onKeyDown) == null ? void 0 : _a.call(inputProps, event);
793
+ inputProps?.onKeyDown?.(event);
758
794
  if (event.defaultPrevented) return;
759
795
  if (event.key === "Enter") {
760
796
  event.currentTarget.blur();
@@ -765,23 +801,33 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
765
801
  }
766
802
  }
767
803
  }
768
- ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: (0, import_utils5.cx)(displayStyles, "select-none"), children: children ?? clampedValue }),
804
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
805
+ "output",
806
+ {
807
+ id: valueElementId,
808
+ "aria-label": valueAriaLabel,
809
+ "aria-live": "polite",
810
+ "aria-atomic": "true",
811
+ className: (0, import_utils5.cx)(displayStyles, "select-none"),
812
+ children: children ?? clampedValue
813
+ }
814
+ ),
769
815
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
770
816
  "button",
771
817
  {
772
818
  ...incrementButtonProps,
773
819
  type: "button",
774
- "aria-label": (incrementButtonProps == null ? void 0 : incrementButtonProps["aria-label"]) ?? "Increase",
820
+ "aria-label": incrementButtonProps?.["aria-label"] ?? "Increase",
821
+ "aria-controls": valueElementId,
775
822
  "aria-disabled": incrementDisabled,
776
823
  disabled: incrementDisabled,
777
824
  className: (0, import_utils5.cx)(
778
825
  rightButtonStyles,
779
826
  incrementDisabled && variant !== "disabled" ? variants.disabled.button : null,
780
- incrementButtonProps == null ? void 0 : incrementButtonProps.className
827
+ incrementButtonProps?.className
781
828
  ),
782
829
  onClick: (event) => {
783
- var _a;
784
- (_a = incrementButtonProps == null ? void 0 : incrementButtonProps.onClick) == null ? void 0 : _a.call(incrementButtonProps, event);
830
+ incrementButtonProps?.onClick?.(event);
785
831
  if (event.defaultPrevented) return;
786
832
  commitValue(clampedValue + step);
787
833
  },
@@ -792,6 +838,264 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
792
838
  }
793
839
  );
794
840
  });
841
+
842
+ // src/elements/Toolbar.tsx
843
+ var import_react7 = require("react");
844
+ var import_utils6 = require("@edux-design/utils");
845
+ var import_jsx_runtime7 = require("react/jsx-runtime");
846
+ function toolbarButtons(root) {
847
+ return Array.from(
848
+ root.querySelectorAll("[data-edux-toolbar-button]")
849
+ ).filter((button) => {
850
+ if (button.closest("[data-edux-toolbar]") !== root) return false;
851
+ if (button.disabled || button.getAttribute("aria-disabled") === "true")
852
+ return false;
853
+ for (let node = button; node; node = node.parentElement) {
854
+ if (node.hidden || node.hasAttribute("inert") || node.getAttribute("aria-hidden") === "true")
855
+ return false;
856
+ const styles = node.ownerDocument.defaultView?.getComputedStyle(node);
857
+ if (styles?.display === "none" || styles?.visibility === "hidden")
858
+ return false;
859
+ if (node === root) break;
860
+ }
861
+ return true;
862
+ });
863
+ }
864
+ var Toolbar = (0, import_react7.forwardRef)(
865
+ function Toolbar2({
866
+ keyboardNavigation = "native",
867
+ orientation = "horizontal",
868
+ loop = true,
869
+ className,
870
+ children,
871
+ onKeyDown,
872
+ onFocusCapture,
873
+ ...props
874
+ }, forwardedRef) {
875
+ const rootRef = (0, import_react7.useRef)(null);
876
+ (0, import_react7.useImperativeHandle)(forwardedRef, () => rootRef.current);
877
+ const lastFocused = (0, import_react7.useRef)(null);
878
+ const originalTabIndexes = (0, import_react7.useRef)(
879
+ /* @__PURE__ */ new Map()
880
+ );
881
+ function syncTabStops() {
882
+ const root = rootRef.current;
883
+ if (!root || keyboardNavigation !== "roving") return;
884
+ const buttons = toolbarButtons(root);
885
+ const active = buttons.includes(lastFocused.current) ? lastFocused.current : buttons[0];
886
+ root.querySelectorAll("[data-edux-toolbar-button]").forEach((button) => {
887
+ const nextTabIndex = button === active ? 0 : -1;
888
+ if (button.closest("[data-edux-toolbar]") !== root) return;
889
+ if (!originalTabIndexes.current.has(button))
890
+ originalTabIndexes.current.set(
891
+ button,
892
+ button.getAttribute("tabindex")
893
+ );
894
+ if (button.tabIndex !== nextTabIndex) button.tabIndex = nextTabIndex;
895
+ });
896
+ lastFocused.current = active ?? null;
897
+ }
898
+ (0, import_react7.useEffect)(() => {
899
+ const root = rootRef.current;
900
+ if (!root || keyboardNavigation !== "roving") return;
901
+ syncTabStops();
902
+ const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver(syncTabStops);
903
+ observer?.observe(root, {
904
+ subtree: true,
905
+ childList: true,
906
+ attributes: true,
907
+ attributeFilter: [
908
+ "disabled",
909
+ "aria-disabled",
910
+ "hidden",
911
+ "inert",
912
+ "aria-hidden",
913
+ "class",
914
+ "style",
915
+ "tabindex"
916
+ ]
917
+ });
918
+ return () => {
919
+ observer?.disconnect();
920
+ originalTabIndexes.current.forEach((value, button) => {
921
+ if (value === null) button.removeAttribute("tabindex");
922
+ else button.setAttribute("tabindex", value);
923
+ });
924
+ originalTabIndexes.current.clear();
925
+ };
926
+ }, [keyboardNavigation]);
927
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
928
+ "div",
929
+ {
930
+ ...props,
931
+ ref: rootRef,
932
+ role: "toolbar",
933
+ "aria-orientation": orientation,
934
+ "data-edux-toolbar": "",
935
+ className: (0, import_utils6.cx)(
936
+ "flex min-w-0 flex-wrap gap-8",
937
+ orientation === "vertical" ? "flex-col items-start" : "items-center",
938
+ className
939
+ ),
940
+ onFocusCapture: (event) => {
941
+ onFocusCapture?.(event);
942
+ const target = event.target;
943
+ if (target instanceof HTMLButtonElement && target.closest("[data-edux-toolbar]") === event.currentTarget) {
944
+ if (toolbarButtons(event.currentTarget).includes(target))
945
+ lastFocused.current = target;
946
+ syncTabStops();
947
+ }
948
+ },
949
+ onKeyDown: (event) => {
950
+ onKeyDown?.(event);
951
+ if (keyboardNavigation !== "roving" || event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
952
+ return;
953
+ const root = event.currentTarget;
954
+ const target = event.target;
955
+ if (!(target instanceof HTMLButtonElement) || target.closest("[data-edux-toolbar]") !== root)
956
+ return;
957
+ const buttons = toolbarButtons(root);
958
+ const index = buttons.indexOf(target);
959
+ if (index === -1) return;
960
+ let next;
961
+ if (event.key === "Home") next = 0;
962
+ else if (event.key === "End") next = buttons.length - 1;
963
+ else {
964
+ const directionValue = root.ownerDocument.defaultView?.getComputedStyle(root).direction || root.closest("[dir]")?.getAttribute("dir");
965
+ const rtl = directionValue === "rtl";
966
+ const forward = orientation === "vertical" ? "ArrowDown" : rtl ? "ArrowLeft" : "ArrowRight";
967
+ const backward = orientation === "vertical" ? "ArrowUp" : rtl ? "ArrowRight" : "ArrowLeft";
968
+ if (event.key !== forward && event.key !== backward) return;
969
+ const direction = event.key === forward ? 1 : -1;
970
+ next = loop ? (index + direction + buttons.length) % buttons.length : Math.max(0, Math.min(buttons.length - 1, index + direction));
971
+ }
972
+ event.preventDefault();
973
+ buttons[next]?.focus();
974
+ },
975
+ children
976
+ }
977
+ );
978
+ }
979
+ );
980
+ var ToolbarGroup = (0, import_react7.forwardRef)(
981
+ function ToolbarGroup2({ orientation = "horizontal", className, ...props }, ref) {
982
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
983
+ "div",
984
+ {
985
+ ...props,
986
+ ref,
987
+ role: "group",
988
+ className: (0, import_utils6.cx)(
989
+ "inline-flex gap-4",
990
+ orientation === "vertical" ? "flex-col" : "flex-wrap items-center",
991
+ className
992
+ )
993
+ }
994
+ );
995
+ }
996
+ );
997
+ var ToolbarSeparator = (0, import_react7.forwardRef)(function ToolbarSeparator2({ orientation = "vertical", className, ...props }, ref) {
998
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
999
+ "div",
1000
+ {
1001
+ ...props,
1002
+ ref,
1003
+ role: "separator",
1004
+ "aria-orientation": orientation,
1005
+ className: (0, import_utils6.cx)(
1006
+ "shrink-0 border-border-subtle",
1007
+ orientation === "vertical" ? "h-control-sm border-l" : "w-full border-t",
1008
+ className
1009
+ )
1010
+ }
1011
+ );
1012
+ });
1013
+
1014
+ // src/elements/ToolbarButton.tsx
1015
+ var import_react8 = require("react");
1016
+ var import_utils7 = require("@edux-design/utils");
1017
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1018
+ var sizes = {
1019
+ small: "h-control-sm min-w-control-sm text-sm",
1020
+ medium: "h-control-md min-w-control-md text-base",
1021
+ large: "h-control-lg min-w-control-lg text-lg"
1022
+ };
1023
+ var ToolbarButton = (0, import_react8.forwardRef)(
1024
+ function ToolbarButton2({
1025
+ active = false,
1026
+ size = "small",
1027
+ variant = "neutral",
1028
+ icon,
1029
+ iconOnly = false,
1030
+ children,
1031
+ className,
1032
+ type = "button",
1033
+ onClick,
1034
+ ...props
1035
+ }, ref) {
1036
+ const pressed = active || props["aria-pressed"] === true || props["aria-pressed"] === "true" || props["aria-pressed"] === "mixed";
1037
+ const disabled = props.disabled || props["aria-disabled"] === true || props["aria-disabled"] === "true";
1038
+ const colours = {
1039
+ neutral: pressed ? "border-border-primary-base bg-bg-primary-subtle text-fg-primary-emphasis" : "border-transparent bg-transparent text-fg-base hover:bg-bg-minimal active:bg-bg-inactive",
1040
+ primary: pressed ? "border-border-primary-emphasis bg-bg-primary-subtle text-fg-primary-emphasis" : "border-transparent bg-transparent text-fg-primary-emphasis hover:bg-bg-primary-subtle active:bg-bg-primary-subtle-90",
1041
+ danger: pressed ? "border-border-danger-emphasis bg-bg-danger-subtle text-fg-danger-emphasis" : "border-transparent bg-transparent text-fg-danger-emphasis hover:bg-bg-danger-subtle active:bg-bg-danger-subtle"
1042
+ };
1043
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1044
+ "button",
1045
+ {
1046
+ "aria-pressed": active ? true : void 0,
1047
+ ...props,
1048
+ ref,
1049
+ type,
1050
+ "data-edux-toolbar-button": "",
1051
+ "data-state": pressed ? "on" : "off",
1052
+ className: (0, import_utils7.cx)(
1053
+ "inline-flex shrink-0 items-center justify-center gap-8 whitespace-nowrap rounded-md border-2 font-semibold cursor-pointer outline-none transition-colors duration-150 motion-reduce:transition-none focus-visible:shadow-focus disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50",
1054
+ sizes[size],
1055
+ iconOnly ? "aspect-square p-0" : "px-8",
1056
+ disabled ? "border-border-subtle bg-bg-inactive text-fg-inactive" : colours[variant],
1057
+ className
1058
+ ),
1059
+ onClick: (event) => {
1060
+ if (disabled) {
1061
+ event.preventDefault();
1062
+ return;
1063
+ }
1064
+ onClick?.(event);
1065
+ },
1066
+ children: [
1067
+ icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1068
+ "span",
1069
+ {
1070
+ "aria-hidden": "true",
1071
+ className: "inline-flex shrink-0 items-center",
1072
+ children: icon
1073
+ }
1074
+ ),
1075
+ children
1076
+ ]
1077
+ }
1078
+ );
1079
+ }
1080
+ );
1081
+ var ToolbarToggleButton = (0, import_react8.forwardRef)(function ToolbarToggleButton2({ pressed, defaultPressed = false, onPressedChange, onClick, ...props }, ref) {
1082
+ const [internalPressed, setInternalPressed] = (0, import_react8.useState)(defaultPressed);
1083
+ const isPressed = pressed ?? internalPressed;
1084
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1085
+ ToolbarButton,
1086
+ {
1087
+ ...props,
1088
+ ref,
1089
+ "aria-pressed": isPressed,
1090
+ onClick: (event) => {
1091
+ onClick?.(event);
1092
+ if (event.defaultPrevented) return;
1093
+ if (pressed === void 0) setInternalPressed(!isPressed);
1094
+ onPressedChange?.(!isPressed, event);
1095
+ }
1096
+ }
1097
+ );
1098
+ });
795
1099
  // Annotate the CommonJS export names for ESM import in node:
796
1100
  0 && (module.exports = {
797
1101
  Button,
@@ -799,5 +1103,10 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
799
1103
  ButtonGroup,
800
1104
  ButtonGroupItem,
801
1105
  IconButton,
802
- MenuButton
1106
+ MenuButton,
1107
+ Toolbar,
1108
+ ToolbarButton,
1109
+ ToolbarGroup,
1110
+ ToolbarSeparator,
1111
+ ToolbarToggleButton
803
1112
  });