@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.mjs CHANGED
@@ -25,21 +25,29 @@ var IconButton = forwardRef(
25
25
  "[IconButton]: Multiple variant props passed. Using the highest priority only."
26
26
  );
27
27
  }
28
+ const hasAccessibleName = Boolean(
29
+ props["aria-label"] || props["aria-labelledby"]
30
+ );
31
+ if (!hasAccessibleName) {
32
+ console.warn(
33
+ "[IconButton]: Provide an accessible name with aria-label or aria-labelledby."
34
+ );
35
+ }
28
36
  }
29
37
  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";
30
- const sizes = {
31
- small: "h-[36px] w-[36px] text-sm",
32
- medium: "h-[44px] w-[44px] text-base",
33
- large: "h-[56px] w-[56px] text-lg"
38
+ const sizes2 = {
39
+ small: "size-control-sm text-sm",
40
+ medium: "size-control-md text-base",
41
+ large: "size-control-lg text-lg"
34
42
  };
35
43
  const variants = {
36
- 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",
44
+ 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",
37
45
  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",
38
46
  bare: "bg-bg-base text-fg-primary-emphasis hover:bg-bg-minimal active:bg-bg-minimal-5",
39
47
  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",
40
48
  disabled: "border-2 border-border-subtle bg-bg-inactive text-fg-inactive hover:no-underline cursor-not-allowed"
41
49
  };
42
- const STYLES = cx(defaults, sizes[size], variants[variant], className);
50
+ const STYLES = cx(defaults, sizes2[size], variants[variant], className);
43
51
  return /* @__PURE__ */ jsx(
44
52
  "button",
45
53
  {
@@ -97,21 +105,21 @@ var Button = forwardRef2(
97
105
  }
98
106
  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";
99
107
  const fonts = "text-bold tracking-normal hover:underline hover:underline-offset-(--underline-offset-btn) active:underline active:underline-offset-(--underline-offset-btn)";
100
- const sizes = {
108
+ const sizes2 = {
101
109
  small: "h-control-sm py-8 px-16 rounded-lg text-sm",
102
110
  medium: "h-control-md py-8 px-16 rounded-lg text-base",
103
111
  large: "h-control-lg py-8 px-16 rounded-lg text-lg"
104
112
  };
105
113
  const width = isStretched ? "w-full" : "w-fit-content";
106
114
  const variants = {
107
- 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",
115
+ 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",
108
116
  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",
109
117
  bare: "bg-bg-base text-fg-primary-emphasis hover:bg-bg-minimal active:bg-bg-minimal-5",
110
118
  dangerBare: "bg-bg-base text-fg-danger-emphasis hover:bg-bg-danger-subtle active:bg-bg-danger-subtle",
111
119
  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",
112
120
  disabled: "border-2 border-border-subtle bg-bg-inactive text-fg-inactive hover:no-underline"
113
121
  };
114
- const STYLES = cx2(defaults, sizes[size], width, fonts, variants[variant]);
122
+ const STYLES = cx2(defaults, sizes2[size], width, fonts, variants[variant]);
115
123
  return /* @__PURE__ */ jsxs(
116
124
  "button",
117
125
  {
@@ -133,17 +141,11 @@ var Button = forwardRef2(
133
141
  );
134
142
 
135
143
  // src/elements/MenuButton.jsx
136
- import React4 from "react";
137
-
138
- // src/util/MenuButtonContext.jsx
139
- import React3, { createContext, useContext } from "react";
140
- var MenuButtonContext = createContext();
141
-
142
- // src/elements/MenuButton.jsx
144
+ import React3, { forwardRef as forwardRef3 } from "react";
143
145
  import { cx as cx3 } from "@edux-design/utils";
144
146
 
145
147
  // ../icons/dist/index.mjs
146
- import { jsxs as jsxs2, jsx as jsx3 } from "react/jsx-runtime";
148
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
147
149
  var SvgChevron = (props) => /* @__PURE__ */ jsx3(
148
150
  "svg",
149
151
  {
@@ -168,23 +170,42 @@ var Chevron_default = SvgChevron;
168
170
 
169
171
  // src/elements/MenuButton.jsx
170
172
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
171
- function MenuButton({ children, size }) {
172
- const SPLIT_BUTTON_STYLES = cx3(
173
- "w-[36px] border-l-[1px] border-l-black flex flex-row justify-center items-center",
173
+ var MenuButton = forwardRef3(function MenuButton2({
174
+ children,
175
+ caret,
176
+ size = "medium",
177
+ className,
178
+ type = "button",
179
+ "aria-haspopup": ariaHasPopup = "menu",
180
+ ...props
181
+ }, ref) {
182
+ const styles = cx3(
183
+ "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",
174
184
  {
175
- "h-[32px]": size === "small",
176
- "h-[40px] py-2": size === "medium" || size === void 0
185
+ "min-h-control-sm text-sm": size === "small",
186
+ "min-h-control-md text-base": size === "medium"
187
+ },
188
+ className
189
+ );
190
+ return /* @__PURE__ */ jsxs3(
191
+ "button",
192
+ {
193
+ ...props,
194
+ ref,
195
+ type,
196
+ "aria-haspopup": ariaHasPopup,
197
+ className: styles,
198
+ children: [
199
+ children ? /* @__PURE__ */ jsx4("span", { children }) : null,
200
+ /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", className: "flex items-center text-current", children: caret ?? /* @__PURE__ */ jsx4(Chevron_default, {}) })
201
+ ]
177
202
  }
178
203
  );
179
- return /* @__PURE__ */ jsxs3(MenuButtonContext.Provider, { value: true, children: [
180
- /* @__PURE__ */ jsx4("div", { className: SPLIT_BUTTON_STYLES, children }),
181
- /* @__PURE__ */ jsx4(Chevron_default, {})
182
- ] });
183
- }
204
+ });
184
205
 
185
206
  // src/elements/ButtonGroup.jsx
186
- import React5, {
187
- forwardRef as forwardRef3,
207
+ import React4, {
208
+ forwardRef as forwardRef4,
188
209
  useCallback,
189
210
  useEffect,
190
211
  useLayoutEffect,
@@ -207,7 +228,7 @@ var SIZE_STYLES = {
207
228
  };
208
229
  var isBrowser = typeof window !== "undefined";
209
230
  var useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;
210
- var ButtonGroup = forwardRef3(function ButtonGroup2({
231
+ var ButtonGroup = forwardRef4(function ButtonGroup2({
211
232
  value,
212
233
  defaultValue,
213
234
  onChange,
@@ -365,8 +386,15 @@ var ButtonGroup = forwardRef3(function ButtonGroup2({
365
386
  return -1;
366
387
  }, [normalizedOptions]);
367
388
  const indicatorVisible = indicator.width > 0 && indicator.height > 0;
389
+ const selectedIndex = normalizedOptions.findIndex(
390
+ (option) => option.value === selectedValue && !option.disabled
391
+ );
392
+ const firstEnabledIndex = normalizedOptions.findIndex(
393
+ (option) => !option.disabled
394
+ );
395
+ const activeTabStopIndex = selectedIndex >= 0 ? selectedIndex : firstEnabledIndex;
368
396
  const rootClasses = cx4(
369
- "inline-flex h-[44px] rounded-lg border-2 border-border-subtle bg-bg-base/90 p-2 shadow-inner",
397
+ "inline-flex h-control-md rounded-lg border-2 border-border-subtle bg-bg-base/90 p-2 shadow-inner",
370
398
  isStretched ? "w-full" : "w-auto",
371
399
  className
372
400
  );
@@ -375,7 +403,7 @@ var ButtonGroup = forwardRef3(function ButtonGroup2({
375
403
  !isStretched && "w-auto"
376
404
  );
377
405
  const buttonBaseClasses = cx4(
378
- "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",
406
+ "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",
379
407
  sizeStyles.button,
380
408
  isStretched ? "flex-1" : "w-auto"
381
409
  );
@@ -385,6 +413,7 @@ var ButtonGroup = forwardRef3(function ButtonGroup2({
385
413
  ...rest,
386
414
  ref: setContainerRef,
387
415
  role: "radiogroup",
416
+ "aria-orientation": "horizontal",
388
417
  className: rootClasses,
389
418
  children: /* @__PURE__ */ jsxs4("div", { className: trackClasses, children: [
390
419
  /* @__PURE__ */ jsx5(
@@ -392,13 +421,13 @@ var ButtonGroup = forwardRef3(function ButtonGroup2({
392
421
  {
393
422
  "aria-hidden": "true",
394
423
  className: cx4(
395
- "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",
424
+ "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",
396
425
  !indicatorVisible && "opacity-0"
397
426
  ),
398
427
  style: {
399
428
  width: indicator.width || void 0,
400
429
  height: indicator.height || void 0,
401
- transform: `translate(${indicator.x - 4}px`
430
+ transform: `translate(${indicator.x - 4}px, ${indicator.y - 4}px)`
402
431
  }
403
432
  }
404
433
  ),
@@ -456,6 +485,7 @@ var ButtonGroup = forwardRef3(function ButtonGroup2({
456
485
  type: "button",
457
486
  role: "radio",
458
487
  "aria-checked": isActive,
488
+ tabIndex: option.disabled ? -1 : index === activeTabStopIndex ? 0 : -1,
459
489
  disabled: option.disabled,
460
490
  className: buttonClasses,
461
491
  onClick: () => handleSelect(option),
@@ -490,24 +520,25 @@ var ButtonGroup = forwardRef3(function ButtonGroup2({
490
520
  });
491
521
 
492
522
  // src/elements/ButtonGroupItem.jsx
493
- import React6 from "react";
523
+ import React5 from "react";
494
524
  function ButtonGroupItem(_props) {
495
525
  return null;
496
526
  }
497
527
  ButtonGroupItem.displayName = "ButtonGroupItem";
498
528
 
499
529
  // src/elements/ButtonCounter.jsx
500
- import React7, {
501
- forwardRef as forwardRef4,
530
+ import React6, {
531
+ forwardRef as forwardRef5,
502
532
  useCallback as useCallback2,
503
533
  useEffect as useEffect2,
534
+ useId,
504
535
  useMemo as useMemo2,
505
536
  useRef as useRef2,
506
537
  useState as useState2
507
538
  } from "react";
508
539
  import { cx as cx5 } from "@edux-design/utils";
509
540
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
510
- var ButtonCounter = forwardRef4(function ButtonCounter2({
541
+ var ButtonCounter = forwardRef5(function ButtonCounter2({
511
542
  value,
512
543
  defaultValue = 1,
513
544
  onChange,
@@ -530,6 +561,7 @@ var ButtonCounter = forwardRef4(function ButtonCounter2({
530
561
  decrementButtonProps,
531
562
  incrementButtonProps,
532
563
  className,
564
+ "aria-label": ariaLabel,
533
565
  ...rest
534
566
  }, ref) {
535
567
  const isControlled = value !== void 0;
@@ -543,7 +575,7 @@ var ButtonCounter = forwardRef4(function ButtonCounter2({
543
575
  (next) => {
544
576
  const clamped = Math.min(max, Math.max(min, next));
545
577
  if (!isControlled) setInternalValue(clamped);
546
- onChange == null ? void 0 : onChange(clamped);
578
+ onChange?.(clamped);
547
579
  },
548
580
  [isControlled, max, min, onChange]
549
581
  );
@@ -575,16 +607,16 @@ var ButtonCounter = forwardRef4(function ButtonCounter2({
575
607
  }
576
608
  const CONTROL_SIZES = {
577
609
  small: {
578
- button: "h-[36px] w-[36px] text-sm",
579
- display: "h-[36px] min-w-[48px] px-12 text-sm"
610
+ button: "size-control-sm text-sm",
611
+ display: "h-control-sm min-w-48 px-12 text-sm"
580
612
  },
581
613
  medium: {
582
- button: "h-[44px] w-[44px] text-base",
583
- display: "h-[44px] min-w-[56px] px-12 text-base"
614
+ button: "size-control-md text-base",
615
+ display: "h-control-md min-w-56 px-12 text-base"
584
616
  },
585
617
  large: {
586
- button: "h-[56px] w-[56px] text-lg",
587
- display: "h-[56px] min-w-[72px] px-16 text-lg"
618
+ button: "size-control-lg text-lg",
619
+ display: "h-control-lg min-w-72 px-16 text-lg"
588
620
  }
589
621
  };
590
622
  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";
@@ -592,8 +624,8 @@ var ButtonCounter = forwardRef4(function ButtonCounter2({
592
624
  const width = isStretched ? "w-full" : "w-fit-content";
593
625
  const variants = {
594
626
  primary: {
595
- 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",
596
- display: "border-y-2 border-x-2 border-border-primary-emphasis bg-bg-primary-base text-white"
627
+ 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",
628
+ display: "border-y-2 border-x-2 border-border-primary-emphasis bg-bg-primary-base text-fg-invert"
597
629
  },
598
630
  secondary: {
599
631
  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",
@@ -642,6 +674,9 @@ var ButtonCounter = forwardRef4(function ButtonCounter2({
642
674
  );
643
675
  const effectiveDecrementIcon = decrementIcon ?? startIcon ?? "-";
644
676
  const effectiveIncrementIcon = incrementIcon ?? endIcon ?? "+";
677
+ const generatedValueId = useId();
678
+ const valueElementId = inputProps?.id ?? generatedValueId;
679
+ const valueAriaLabel = inputProps?.["aria-label"] ?? ariaLabel ?? "Value";
645
680
  const [draftValue, setDraftValue] = useState2(String(clampedValue));
646
681
  const [isEditing, setIsEditing] = useState2(false);
647
682
  const inputRef = useRef2(null);
@@ -674,17 +709,17 @@ var ButtonCounter = forwardRef4(function ButtonCounter2({
674
709
  {
675
710
  ...decrementButtonProps,
676
711
  type: "button",
677
- "aria-label": (decrementButtonProps == null ? void 0 : decrementButtonProps["aria-label"]) ?? "Decrease",
712
+ "aria-label": decrementButtonProps?.["aria-label"] ?? "Decrease",
713
+ "aria-controls": valueElementId,
678
714
  "aria-disabled": decrementDisabled,
679
715
  disabled: decrementDisabled,
680
716
  className: cx5(
681
717
  leftButtonStyles,
682
718
  decrementDisabled && variant !== "disabled" ? variants.disabled.button : null,
683
- decrementButtonProps == null ? void 0 : decrementButtonProps.className
719
+ decrementButtonProps?.className
684
720
  ),
685
721
  onClick: (event) => {
686
- var _a;
687
- (_a = decrementButtonProps == null ? void 0 : decrementButtonProps.onClick) == null ? void 0 : _a.call(decrementButtonProps, event);
722
+ decrementButtonProps?.onClick?.(event);
688
723
  if (event.defaultPrevented) return;
689
724
  commitValue(clampedValue - step);
690
725
  },
@@ -698,38 +733,35 @@ var ButtonCounter = forwardRef4(function ButtonCounter2({
698
733
  ref: inputRef,
699
734
  type: "number",
700
735
  inputMode: "numeric",
736
+ id: valueElementId,
701
737
  disabled: !!disabled,
702
- "aria-label": (inputProps == null ? void 0 : inputProps["aria-label"]) ?? "Value",
738
+ "aria-label": valueAriaLabel,
703
739
  className: cx5(
704
740
  displayStyles,
705
- "w-[44px] text-center outline-none bg-transparent text-fg-base",
741
+ "w-control-md text-center outline-none bg-transparent text-fg-base",
706
742
  "[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",
707
743
  "focus:shadow-focus focus-visible:shadow-focus focus:z-15 focus-within:z-15",
708
- inputProps == null ? void 0 : inputProps.className
744
+ inputProps?.className
709
745
  ),
710
746
  value: draftValue,
711
747
  onChange: (event) => {
712
- var _a;
713
- (_a = inputProps == null ? void 0 : inputProps.onChange) == null ? void 0 : _a.call(inputProps, event);
748
+ inputProps?.onChange?.(event);
714
749
  if (event.defaultPrevented) return;
715
750
  setDraftValue(event.target.value);
716
751
  },
717
752
  onFocus: (event) => {
718
- var _a;
719
- (_a = inputProps == null ? void 0 : inputProps.onFocus) == null ? void 0 : _a.call(inputProps, event);
753
+ inputProps?.onFocus?.(event);
720
754
  if (event.defaultPrevented) return;
721
755
  setIsEditing(true);
722
756
  },
723
757
  onBlur: (event) => {
724
- var _a;
725
- (_a = inputProps == null ? void 0 : inputProps.onBlur) == null ? void 0 : _a.call(inputProps, event);
758
+ inputProps?.onBlur?.(event);
726
759
  if (event.defaultPrevented) return;
727
760
  setIsEditing(false);
728
761
  commitDraft();
729
762
  },
730
763
  onKeyDown: (event) => {
731
- var _a;
732
- (_a = inputProps == null ? void 0 : inputProps.onKeyDown) == null ? void 0 : _a.call(inputProps, event);
764
+ inputProps?.onKeyDown?.(event);
733
765
  if (event.defaultPrevented) return;
734
766
  if (event.key === "Enter") {
735
767
  event.currentTarget.blur();
@@ -740,23 +772,33 @@ var ButtonCounter = forwardRef4(function ButtonCounter2({
740
772
  }
741
773
  }
742
774
  }
743
- ) : /* @__PURE__ */ jsx6("div", { className: cx5(displayStyles, "select-none"), children: children ?? clampedValue }),
775
+ ) : /* @__PURE__ */ jsx6(
776
+ "output",
777
+ {
778
+ id: valueElementId,
779
+ "aria-label": valueAriaLabel,
780
+ "aria-live": "polite",
781
+ "aria-atomic": "true",
782
+ className: cx5(displayStyles, "select-none"),
783
+ children: children ?? clampedValue
784
+ }
785
+ ),
744
786
  /* @__PURE__ */ jsx6(
745
787
  "button",
746
788
  {
747
789
  ...incrementButtonProps,
748
790
  type: "button",
749
- "aria-label": (incrementButtonProps == null ? void 0 : incrementButtonProps["aria-label"]) ?? "Increase",
791
+ "aria-label": incrementButtonProps?.["aria-label"] ?? "Increase",
792
+ "aria-controls": valueElementId,
750
793
  "aria-disabled": incrementDisabled,
751
794
  disabled: incrementDisabled,
752
795
  className: cx5(
753
796
  rightButtonStyles,
754
797
  incrementDisabled && variant !== "disabled" ? variants.disabled.button : null,
755
- incrementButtonProps == null ? void 0 : incrementButtonProps.className
798
+ incrementButtonProps?.className
756
799
  ),
757
800
  onClick: (event) => {
758
- var _a;
759
- (_a = incrementButtonProps == null ? void 0 : incrementButtonProps.onClick) == null ? void 0 : _a.call(incrementButtonProps, event);
801
+ incrementButtonProps?.onClick?.(event);
760
802
  if (event.defaultPrevented) return;
761
803
  commitValue(clampedValue + step);
762
804
  },
@@ -767,11 +809,279 @@ var ButtonCounter = forwardRef4(function ButtonCounter2({
767
809
  }
768
810
  );
769
811
  });
812
+
813
+ // src/elements/Toolbar.tsx
814
+ import {
815
+ forwardRef as forwardRef6,
816
+ useEffect as useEffect3,
817
+ useImperativeHandle,
818
+ useRef as useRef3
819
+ } from "react";
820
+ import { cx as cx6 } from "@edux-design/utils";
821
+ import { jsx as jsx7 } from "react/jsx-runtime";
822
+ function toolbarButtons(root) {
823
+ return Array.from(
824
+ root.querySelectorAll("[data-edux-toolbar-button]")
825
+ ).filter((button) => {
826
+ if (button.closest("[data-edux-toolbar]") !== root) return false;
827
+ if (button.disabled || button.getAttribute("aria-disabled") === "true")
828
+ return false;
829
+ for (let node = button; node; node = node.parentElement) {
830
+ if (node.hidden || node.hasAttribute("inert") || node.getAttribute("aria-hidden") === "true")
831
+ return false;
832
+ const styles = node.ownerDocument.defaultView?.getComputedStyle(node);
833
+ if (styles?.display === "none" || styles?.visibility === "hidden")
834
+ return false;
835
+ if (node === root) break;
836
+ }
837
+ return true;
838
+ });
839
+ }
840
+ var Toolbar = forwardRef6(
841
+ function Toolbar2({
842
+ keyboardNavigation = "native",
843
+ orientation = "horizontal",
844
+ loop = true,
845
+ className,
846
+ children,
847
+ onKeyDown,
848
+ onFocusCapture,
849
+ ...props
850
+ }, forwardedRef) {
851
+ const rootRef = useRef3(null);
852
+ useImperativeHandle(forwardedRef, () => rootRef.current);
853
+ const lastFocused = useRef3(null);
854
+ const originalTabIndexes = useRef3(
855
+ /* @__PURE__ */ new Map()
856
+ );
857
+ function syncTabStops() {
858
+ const root = rootRef.current;
859
+ if (!root || keyboardNavigation !== "roving") return;
860
+ const buttons = toolbarButtons(root);
861
+ const active = buttons.includes(lastFocused.current) ? lastFocused.current : buttons[0];
862
+ root.querySelectorAll("[data-edux-toolbar-button]").forEach((button) => {
863
+ const nextTabIndex = button === active ? 0 : -1;
864
+ if (button.closest("[data-edux-toolbar]") !== root) return;
865
+ if (!originalTabIndexes.current.has(button))
866
+ originalTabIndexes.current.set(
867
+ button,
868
+ button.getAttribute("tabindex")
869
+ );
870
+ if (button.tabIndex !== nextTabIndex) button.tabIndex = nextTabIndex;
871
+ });
872
+ lastFocused.current = active ?? null;
873
+ }
874
+ useEffect3(() => {
875
+ const root = rootRef.current;
876
+ if (!root || keyboardNavigation !== "roving") return;
877
+ syncTabStops();
878
+ const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver(syncTabStops);
879
+ observer?.observe(root, {
880
+ subtree: true,
881
+ childList: true,
882
+ attributes: true,
883
+ attributeFilter: [
884
+ "disabled",
885
+ "aria-disabled",
886
+ "hidden",
887
+ "inert",
888
+ "aria-hidden",
889
+ "class",
890
+ "style",
891
+ "tabindex"
892
+ ]
893
+ });
894
+ return () => {
895
+ observer?.disconnect();
896
+ originalTabIndexes.current.forEach((value, button) => {
897
+ if (value === null) button.removeAttribute("tabindex");
898
+ else button.setAttribute("tabindex", value);
899
+ });
900
+ originalTabIndexes.current.clear();
901
+ };
902
+ }, [keyboardNavigation]);
903
+ return /* @__PURE__ */ jsx7(
904
+ "div",
905
+ {
906
+ ...props,
907
+ ref: rootRef,
908
+ role: "toolbar",
909
+ "aria-orientation": orientation,
910
+ "data-edux-toolbar": "",
911
+ className: cx6(
912
+ "flex min-w-0 flex-wrap gap-8",
913
+ orientation === "vertical" ? "flex-col items-start" : "items-center",
914
+ className
915
+ ),
916
+ onFocusCapture: (event) => {
917
+ onFocusCapture?.(event);
918
+ const target = event.target;
919
+ if (target instanceof HTMLButtonElement && target.closest("[data-edux-toolbar]") === event.currentTarget) {
920
+ if (toolbarButtons(event.currentTarget).includes(target))
921
+ lastFocused.current = target;
922
+ syncTabStops();
923
+ }
924
+ },
925
+ onKeyDown: (event) => {
926
+ onKeyDown?.(event);
927
+ if (keyboardNavigation !== "roving" || event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
928
+ return;
929
+ const root = event.currentTarget;
930
+ const target = event.target;
931
+ if (!(target instanceof HTMLButtonElement) || target.closest("[data-edux-toolbar]") !== root)
932
+ return;
933
+ const buttons = toolbarButtons(root);
934
+ const index = buttons.indexOf(target);
935
+ if (index === -1) return;
936
+ let next;
937
+ if (event.key === "Home") next = 0;
938
+ else if (event.key === "End") next = buttons.length - 1;
939
+ else {
940
+ const directionValue = root.ownerDocument.defaultView?.getComputedStyle(root).direction || root.closest("[dir]")?.getAttribute("dir");
941
+ const rtl = directionValue === "rtl";
942
+ const forward = orientation === "vertical" ? "ArrowDown" : rtl ? "ArrowLeft" : "ArrowRight";
943
+ const backward = orientation === "vertical" ? "ArrowUp" : rtl ? "ArrowRight" : "ArrowLeft";
944
+ if (event.key !== forward && event.key !== backward) return;
945
+ const direction = event.key === forward ? 1 : -1;
946
+ next = loop ? (index + direction + buttons.length) % buttons.length : Math.max(0, Math.min(buttons.length - 1, index + direction));
947
+ }
948
+ event.preventDefault();
949
+ buttons[next]?.focus();
950
+ },
951
+ children
952
+ }
953
+ );
954
+ }
955
+ );
956
+ var ToolbarGroup = forwardRef6(
957
+ function ToolbarGroup2({ orientation = "horizontal", className, ...props }, ref) {
958
+ return /* @__PURE__ */ jsx7(
959
+ "div",
960
+ {
961
+ ...props,
962
+ ref,
963
+ role: "group",
964
+ className: cx6(
965
+ "inline-flex gap-4",
966
+ orientation === "vertical" ? "flex-col" : "flex-wrap items-center",
967
+ className
968
+ )
969
+ }
970
+ );
971
+ }
972
+ );
973
+ var ToolbarSeparator = forwardRef6(function ToolbarSeparator2({ orientation = "vertical", className, ...props }, ref) {
974
+ return /* @__PURE__ */ jsx7(
975
+ "div",
976
+ {
977
+ ...props,
978
+ ref,
979
+ role: "separator",
980
+ "aria-orientation": orientation,
981
+ className: cx6(
982
+ "shrink-0 border-border-subtle",
983
+ orientation === "vertical" ? "h-control-sm border-l" : "w-full border-t",
984
+ className
985
+ )
986
+ }
987
+ );
988
+ });
989
+
990
+ // src/elements/ToolbarButton.tsx
991
+ import { forwardRef as forwardRef7, useState as useState3 } from "react";
992
+ import { cx as cx7 } from "@edux-design/utils";
993
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
994
+ var sizes = {
995
+ small: "h-control-sm min-w-control-sm text-sm",
996
+ medium: "h-control-md min-w-control-md text-base",
997
+ large: "h-control-lg min-w-control-lg text-lg"
998
+ };
999
+ var ToolbarButton = forwardRef7(
1000
+ function ToolbarButton2({
1001
+ active = false,
1002
+ size = "small",
1003
+ variant = "neutral",
1004
+ icon,
1005
+ iconOnly = false,
1006
+ children,
1007
+ className,
1008
+ type = "button",
1009
+ onClick,
1010
+ ...props
1011
+ }, ref) {
1012
+ const pressed = active || props["aria-pressed"] === true || props["aria-pressed"] === "true" || props["aria-pressed"] === "mixed";
1013
+ const disabled = props.disabled || props["aria-disabled"] === true || props["aria-disabled"] === "true";
1014
+ const colours = {
1015
+ 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",
1016
+ 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",
1017
+ 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"
1018
+ };
1019
+ return /* @__PURE__ */ jsxs6(
1020
+ "button",
1021
+ {
1022
+ "aria-pressed": active ? true : void 0,
1023
+ ...props,
1024
+ ref,
1025
+ type,
1026
+ "data-edux-toolbar-button": "",
1027
+ "data-state": pressed ? "on" : "off",
1028
+ className: cx7(
1029
+ "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",
1030
+ sizes[size],
1031
+ iconOnly ? "aspect-square p-0" : "px-8",
1032
+ disabled ? "border-border-subtle bg-bg-inactive text-fg-inactive" : colours[variant],
1033
+ className
1034
+ ),
1035
+ onClick: (event) => {
1036
+ if (disabled) {
1037
+ event.preventDefault();
1038
+ return;
1039
+ }
1040
+ onClick?.(event);
1041
+ },
1042
+ children: [
1043
+ icon && /* @__PURE__ */ jsx8(
1044
+ "span",
1045
+ {
1046
+ "aria-hidden": "true",
1047
+ className: "inline-flex shrink-0 items-center",
1048
+ children: icon
1049
+ }
1050
+ ),
1051
+ children
1052
+ ]
1053
+ }
1054
+ );
1055
+ }
1056
+ );
1057
+ var ToolbarToggleButton = forwardRef7(function ToolbarToggleButton2({ pressed, defaultPressed = false, onPressedChange, onClick, ...props }, ref) {
1058
+ const [internalPressed, setInternalPressed] = useState3(defaultPressed);
1059
+ const isPressed = pressed ?? internalPressed;
1060
+ return /* @__PURE__ */ jsx8(
1061
+ ToolbarButton,
1062
+ {
1063
+ ...props,
1064
+ ref,
1065
+ "aria-pressed": isPressed,
1066
+ onClick: (event) => {
1067
+ onClick?.(event);
1068
+ if (event.defaultPrevented) return;
1069
+ if (pressed === void 0) setInternalPressed(!isPressed);
1070
+ onPressedChange?.(!isPressed, event);
1071
+ }
1072
+ }
1073
+ );
1074
+ });
770
1075
  export {
771
1076
  Button,
772
1077
  ButtonCounter,
773
1078
  ButtonGroup,
774
1079
  ButtonGroupItem,
775
1080
  IconButton,
776
- MenuButton
1081
+ MenuButton,
1082
+ Toolbar,
1083
+ ToolbarButton,
1084
+ ToolbarGroup,
1085
+ ToolbarSeparator,
1086
+ ToolbarToggleButton
777
1087
  };