@edux-design/buttons 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -35,19 +35,19 @@ var IconButton = forwardRef(
35
35
  }
36
36
  }
37
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";
38
- const sizes = {
38
+ const sizes2 = {
39
39
  small: "size-control-sm text-sm",
40
40
  medium: "size-control-md text-base",
41
41
  large: "size-control-lg text-lg"
42
42
  };
43
43
  const variants = {
44
- 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",
45
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",
46
46
  bare: "bg-bg-base text-fg-primary-emphasis hover:bg-bg-minimal active:bg-bg-minimal-5",
47
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",
48
48
  disabled: "border-2 border-border-subtle bg-bg-inactive text-fg-inactive hover:no-underline cursor-not-allowed"
49
49
  };
50
- const STYLES = cx(defaults, sizes[size], variants[variant], className);
50
+ const STYLES = cx(defaults, sizes2[size], variants[variant], className);
51
51
  return /* @__PURE__ */ jsx(
52
52
  "button",
53
53
  {
@@ -105,21 +105,21 @@ var Button = forwardRef2(
105
105
  }
106
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";
107
107
  const fonts = "text-bold tracking-normal hover:underline hover:underline-offset-(--underline-offset-btn) active:underline active:underline-offset-(--underline-offset-btn)";
108
- const sizes = {
108
+ const sizes2 = {
109
109
  small: "h-control-sm py-8 px-16 rounded-lg text-sm",
110
110
  medium: "h-control-md py-8 px-16 rounded-lg text-base",
111
111
  large: "h-control-lg py-8 px-16 rounded-lg text-lg"
112
112
  };
113
113
  const width = isStretched ? "w-full" : "w-fit-content";
114
114
  const variants = {
115
- 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",
116
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",
117
117
  bare: "bg-bg-base text-fg-primary-emphasis hover:bg-bg-minimal active:bg-bg-minimal-5",
118
118
  dangerBare: "bg-bg-base text-fg-danger-emphasis hover:bg-bg-danger-subtle active:bg-bg-danger-subtle",
119
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",
120
120
  disabled: "border-2 border-border-subtle bg-bg-inactive text-fg-inactive hover:no-underline"
121
121
  };
122
- const STYLES = cx2(defaults, sizes[size], width, fonts, variants[variant]);
122
+ const STYLES = cx2(defaults, sizes2[size], width, fonts, variants[variant]);
123
123
  return /* @__PURE__ */ jsxs(
124
124
  "button",
125
125
  {
@@ -575,7 +575,7 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
575
575
  (next) => {
576
576
  const clamped = Math.min(max, Math.max(min, next));
577
577
  if (!isControlled) setInternalValue(clamped);
578
- onChange == null ? void 0 : onChange(clamped);
578
+ onChange?.(clamped);
579
579
  },
580
580
  [isControlled, max, min, onChange]
581
581
  );
@@ -608,15 +608,15 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
608
608
  const CONTROL_SIZES = {
609
609
  small: {
610
610
  button: "size-control-sm text-sm",
611
- display: "h-control-sm min-w-[48px] px-12 text-sm"
611
+ display: "h-control-sm min-w-48 px-12 text-sm"
612
612
  },
613
613
  medium: {
614
614
  button: "size-control-md text-base",
615
- display: "h-control-md min-w-[56px] px-12 text-base"
615
+ display: "h-control-md min-w-56 px-12 text-base"
616
616
  },
617
617
  large: {
618
618
  button: "size-control-lg text-lg",
619
- display: "h-control-lg min-w-[72px] px-16 text-lg"
619
+ display: "h-control-lg min-w-72 px-16 text-lg"
620
620
  }
621
621
  };
622
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";
@@ -624,8 +624,8 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
624
624
  const width = isStretched ? "w-full" : "w-fit-content";
625
625
  const variants = {
626
626
  primary: {
627
- 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",
628
- 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"
629
629
  },
630
630
  secondary: {
631
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",
@@ -675,8 +675,8 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
675
675
  const effectiveDecrementIcon = decrementIcon ?? startIcon ?? "-";
676
676
  const effectiveIncrementIcon = incrementIcon ?? endIcon ?? "+";
677
677
  const generatedValueId = useId();
678
- const valueElementId = (inputProps == null ? void 0 : inputProps.id) ?? generatedValueId;
679
- const valueAriaLabel = (inputProps == null ? void 0 : inputProps["aria-label"]) ?? ariaLabel ?? "Value";
678
+ const valueElementId = inputProps?.id ?? generatedValueId;
679
+ const valueAriaLabel = inputProps?.["aria-label"] ?? ariaLabel ?? "Value";
680
680
  const [draftValue, setDraftValue] = useState2(String(clampedValue));
681
681
  const [isEditing, setIsEditing] = useState2(false);
682
682
  const inputRef = useRef2(null);
@@ -709,18 +709,17 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
709
709
  {
710
710
  ...decrementButtonProps,
711
711
  type: "button",
712
- "aria-label": (decrementButtonProps == null ? void 0 : decrementButtonProps["aria-label"]) ?? "Decrease",
712
+ "aria-label": decrementButtonProps?.["aria-label"] ?? "Decrease",
713
713
  "aria-controls": valueElementId,
714
714
  "aria-disabled": decrementDisabled,
715
715
  disabled: decrementDisabled,
716
716
  className: cx5(
717
717
  leftButtonStyles,
718
718
  decrementDisabled && variant !== "disabled" ? variants.disabled.button : null,
719
- decrementButtonProps == null ? void 0 : decrementButtonProps.className
719
+ decrementButtonProps?.className
720
720
  ),
721
721
  onClick: (event) => {
722
- var _a;
723
- (_a = decrementButtonProps == null ? void 0 : decrementButtonProps.onClick) == null ? void 0 : _a.call(decrementButtonProps, event);
722
+ decrementButtonProps?.onClick?.(event);
724
723
  if (event.defaultPrevented) return;
725
724
  commitValue(clampedValue - step);
726
725
  },
@@ -739,34 +738,30 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
739
738
  "aria-label": valueAriaLabel,
740
739
  className: cx5(
741
740
  displayStyles,
742
- "w-[44px] text-center outline-none bg-transparent text-fg-base",
741
+ "w-control-md text-center outline-none bg-transparent text-fg-base",
743
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",
744
743
  "focus:shadow-focus focus-visible:shadow-focus focus:z-15 focus-within:z-15",
745
- inputProps == null ? void 0 : inputProps.className
744
+ inputProps?.className
746
745
  ),
747
746
  value: draftValue,
748
747
  onChange: (event) => {
749
- var _a;
750
- (_a = inputProps == null ? void 0 : inputProps.onChange) == null ? void 0 : _a.call(inputProps, event);
748
+ inputProps?.onChange?.(event);
751
749
  if (event.defaultPrevented) return;
752
750
  setDraftValue(event.target.value);
753
751
  },
754
752
  onFocus: (event) => {
755
- var _a;
756
- (_a = inputProps == null ? void 0 : inputProps.onFocus) == null ? void 0 : _a.call(inputProps, event);
753
+ inputProps?.onFocus?.(event);
757
754
  if (event.defaultPrevented) return;
758
755
  setIsEditing(true);
759
756
  },
760
757
  onBlur: (event) => {
761
- var _a;
762
- (_a = inputProps == null ? void 0 : inputProps.onBlur) == null ? void 0 : _a.call(inputProps, event);
758
+ inputProps?.onBlur?.(event);
763
759
  if (event.defaultPrevented) return;
764
760
  setIsEditing(false);
765
761
  commitDraft();
766
762
  },
767
763
  onKeyDown: (event) => {
768
- var _a;
769
- (_a = inputProps == null ? void 0 : inputProps.onKeyDown) == null ? void 0 : _a.call(inputProps, event);
764
+ inputProps?.onKeyDown?.(event);
770
765
  if (event.defaultPrevented) return;
771
766
  if (event.key === "Enter") {
772
767
  event.currentTarget.blur();
@@ -793,18 +788,17 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
793
788
  {
794
789
  ...incrementButtonProps,
795
790
  type: "button",
796
- "aria-label": (incrementButtonProps == null ? void 0 : incrementButtonProps["aria-label"]) ?? "Increase",
791
+ "aria-label": incrementButtonProps?.["aria-label"] ?? "Increase",
797
792
  "aria-controls": valueElementId,
798
793
  "aria-disabled": incrementDisabled,
799
794
  disabled: incrementDisabled,
800
795
  className: cx5(
801
796
  rightButtonStyles,
802
797
  incrementDisabled && variant !== "disabled" ? variants.disabled.button : null,
803
- incrementButtonProps == null ? void 0 : incrementButtonProps.className
798
+ incrementButtonProps?.className
804
799
  ),
805
800
  onClick: (event) => {
806
- var _a;
807
- (_a = incrementButtonProps == null ? void 0 : incrementButtonProps.onClick) == null ? void 0 : _a.call(incrementButtonProps, event);
801
+ incrementButtonProps?.onClick?.(event);
808
802
  if (event.defaultPrevented) return;
809
803
  commitValue(clampedValue + step);
810
804
  },
@@ -815,11 +809,279 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
815
809
  }
816
810
  );
817
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
+ });
818
1075
  export {
819
1076
  Button,
820
1077
  ButtonCounter,
821
1078
  ButtonGroup,
822
1079
  ButtonGroupItem,
823
1080
  IconButton,
824
- MenuButton
1081
+ MenuButton,
1082
+ Toolbar,
1083
+ ToolbarButton,
1084
+ ToolbarGroup,
1085
+ ToolbarSeparator,
1086
+ ToolbarToggleButton
825
1087
  };
@@ -0,0 +1,146 @@
1
+ # Toolbar controls
2
+
3
+ `ToolbarButton`, `ToolbarToggleButton`, `Toolbar`, `ToolbarGroup` and
4
+ `ToolbarSeparator` extend the Buttons package without changing existing Button,
5
+ IconButton or ButtonGroup APIs. Use them for compact editor, preview and page
6
+ actions. Commands, editor selection, persistence and menu state belong to the host.
7
+
8
+ ```tsx
9
+ import {
10
+ Toolbar,
11
+ ToolbarGroup,
12
+ ToolbarSeparator,
13
+ ToolbarButton,
14
+ ToolbarToggleButton,
15
+ } from "@edux-design/buttons";
16
+
17
+ <Toolbar aria-label="Document formatting">
18
+ <ToolbarGroup aria-label="Text style">
19
+ <ToolbarToggleButton pressed={bold} onPressedChange={setBold}>
20
+ Bold
21
+ </ToolbarToggleButton>
22
+ <ToolbarToggleButton defaultPressed>Italic</ToolbarToggleButton>
23
+ </ToolbarGroup>
24
+ <ToolbarSeparator />
25
+ <ToolbarButton onClick={save}>Save draft</ToolbarButton>
26
+ </Toolbar>;
27
+ ```
28
+
29
+ ## Buttons and state
30
+
31
+ All buttons render a single native `button`, forward an `HTMLButtonElement` ref,
32
+ and accept native attributes, events, ARIA, data attributes, `className`, `style`
33
+ and arbitrary children. Their default `type` is `button`; `type="submit"` is
34
+ supported. Handlers receive the native React event with the actual button as
35
+ `event.currentTarget`, suitable for menu anchors and measurement. No wrapper is
36
+ inserted around the button. An optional `icon` slot is decorative (`aria-hidden`).
37
+
38
+ | Prop | Default | Contract |
39
+ | --------------- | --------- | ----------------------------------------------------------- |
40
+ | `size` | `small` | `small`, `medium`, `large`: 36, 44, 56px semantic controls |
41
+ | `variant` | `neutral` | `neutral`, `primary`, `danger`; existing semantic tokens |
42
+ | `iconOnly` | `false` | Square geometry; caller supplies an accessible name |
43
+ | `icon` | — | Decorative icon before children |
44
+ | `active` | `false` | ToolbarButton compatibility visual state, no internal state |
45
+ | `disabled` | `false` | Native disabled behaviour; blocks activation |
46
+ | `aria-disabled` | — | Also blocks activation; roving focus skips this action |
47
+
48
+ An ordinary action has no `aria-pressed` attribute by default. Native
49
+ `aria-pressed` true/false/mixed is forwarded and controls selected styling.
50
+ `active` is a compatibility flag: true selects the visual and defaults
51
+ `aria-pressed` to true. Explicit native `aria-pressed`, including false or
52
+ undefined, takes precedence for semantics. A question selector can use
53
+ `active={current}` and `aria-pressed={undefined}` with the host's appropriate
54
+ `aria-current` semantics. It need not become a toggle merely to show selection.
55
+
56
+ `ToolbarToggleButton` is an independent toggle with boolean `pressed`,
57
+ `defaultPressed` (false by default), and
58
+ `onPressedChange(nextBoolean, clickEvent)`. It always exposes true or false
59
+ `aria-pressed`. Controlled state changes only when its host updates `pressed`.
60
+ The native `onClick` runs first; `event.preventDefault()` cancels state changes
61
+ and the change callback. Disabled actions cannot change state. Enter and Space
62
+ use native button activation. Keep a toggle's accessible label stable between
63
+ states. `active` is reserved for ToolbarButton, not the dedicated toggle.
64
+
65
+ The selected border reserves its space in every state, keeping dimensions stable.
66
+ Transitions honour reduced motion. Disabled controls keep their disabled colours
67
+ on hover. There are no new palette or semantic tokens.
68
+
69
+ ## Grouping and keyboard policy
70
+
71
+ `Toolbar` renders a `div` with `role="toolbar"`; supply `aria-label` or
72
+ `aria-labelledby`. `ToolbarGroup` is a native `div` with `role="group"`, not a
73
+ radiogroup. Give meaningful groups accessible names. Root and group support
74
+ `orientation="horizontal"` (default) or `vertical`, refs and native div props.
75
+ `ToolbarSeparator` renders a non-focusable separator; its `orientation` refers
76
+ to the line, and defaults to `vertical` for a horizontal toolbar.
77
+
78
+ **Native Tab order is the default.** Grouping does not change button tab stops,
79
+ activate toggles, intercept arrows or manage menu state. Buttons and groups also
80
+ work independently without a Toolbar. This preserves existing footer/editor
81
+ keyboard behaviour when replacing presentation.
82
+
83
+ Opt into `keyboardNavigation="roving"` deliberately to use a single tab stop
84
+ across ToolbarButton and ToolbarToggleButton descendants. After hydration:
85
+
86
+ - Horizontal Left/Right arrows move focus in the layout direction, including RTL.
87
+ - Vertical Up/Down arrows move focus; the perpendicular axis remains available.
88
+ - Home/End focus the first/last enabled visible action.
89
+ - `loop` defaults to true; false stops at either end.
90
+ - Tab/Shift+Tab leave normally; returning remembers the last enabled action.
91
+ - Arrow navigation moves focus without activating an action or toggle.
92
+ - Native disabled, aria-disabled, hidden/inert and CSS-hidden actions are skipped.
93
+ - Nested toolbars keep independent focus. Inputs and other widgets retain their
94
+ own keys and tab order; place complex widgets in separate labelled groups when
95
+ a strict one-stop toolbar is required.
96
+ - Modified keys and caller-prevented key events are left alone, including table
97
+ Alt+Arrow shortcuts. The host's root `onKeyDown` runs before navigation.
98
+
99
+ Roving mode owns action tab stops while enabled, updates them when actions change,
100
+ and restores the original native values when disabled/unmounted. It does not
101
+ capture focus, auto-focus on mount, intercept Escape or handle editor commands.
102
+
103
+ ## Payload compact and selection adapters
104
+
105
+ Existing Payload toolbars use 32px controls. The package's standard small control
106
+ is 36px. Preserve 32px explicitly during local acceptance rather than silently
107
+ resizing the client. This is component/host geometry, not a new global token:
108
+
109
+ ```tsx
110
+ const compact = { height: 32, minWidth: 32 };
111
+
112
+ <ToolbarButton
113
+ style={compact}
114
+ onMouseDown={(event) => {
115
+ event.preventDefault(); // Host preserves editor selection.
116
+ onMouseDown?.(event); // Existing handler observes defaultPrevented.
117
+ }}
118
+ onClick={runCommand}
119
+ >
120
+ Insert
121
+ </ToolbarButton>;
122
+ ```
123
+
124
+ Mouse prevention is optional host behaviour. The package does not prevent pointer
125
+ focus or focus the editor. Keyboard navigation and activation remain available.
126
+ Table handles can retain their class/style/data hooks and specialised geometry,
127
+ pointer handlers, currentTarget anchors and Alt+Arrow reordering in local adapters.
128
+
129
+ ## Tooltip, menu and theme composition
130
+
131
+ Use TooltipTrigger's `asChild` API around the same button, or pass that button
132
+ as the existing Menu's `button` prop. Provide TooltipProvider as usual.
133
+ Tooltips describe an action; icon-only actions
134
+ still need `aria-label`/`aria-labelledby`. Injected refs/handlers/ARIA are forwarded.
135
+ Menus own their `aria-expanded`, popup keyboard behaviour and focus restoration;
136
+ use ToolbarButton, not an independent toggle, for a menu trigger.
137
+
138
+ Import `@edux-design/tokens/theme` once, and include installed packages in Tailwind
139
+ source detection. Use existing `theme--light`/`theme--dark` and `brand--blue`/
140
+ `brand--teal` classes on the same scope. The controls use existing semantic
141
+ foreground, background, border and focus tokens. They do not own a theme provider.
142
+
143
+ Storybook **Components/Toolbar** includes native actions, explicit roving focus,
144
+ all sizes/states, light/dark × blue/teal, narrow wrapping, vertical/RTL, tooltip/menu
145
+ composition, a measured Payload32px recipe and the editor selection adapter. The package includes interaction,
146
+ structural accessibility, public consumer type and CJS/SSR fixtures.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edux-design/buttons",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css"
@@ -13,21 +13,6 @@
13
13
  "require": "./dist/index.js"
14
14
  }
15
15
  },
16
- "scripts": {
17
- "lint": "eslint . --max-warnings 0",
18
- "build": "tsup src/index.js --format esm,cjs --dts",
19
- "dev": "tsup src/index.js --watch --format esm,cjs --dts",
20
- "generate:component": "turbo gen react-component",
21
- "check-types": "tsc --noEmit"
22
- },
23
- "devDependencies": {
24
- "typescript": "^5.9.2",
25
- "tailwindcss": "^4.1.12",
26
- "postcss": "^8.5.6",
27
- "tsup": "^8.5.0",
28
- "@tailwindcss/cli": "^4.1.12",
29
- "@tailwindcss/postcss": "^4.1.12"
30
- },
31
16
  "dependencies": {
32
17
  "@edux-design/utils": "*"
33
18
  },
@@ -35,5 +20,13 @@
35
20
  "@edux-design/tokens": ">=0.2.0 <0.3.0",
36
21
  "react": "^19.1.0",
37
22
  "react-dom": "^19.1.0"
38
- }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "docs",
27
+ "README.md",
28
+ "CHANGELOG.md",
29
+ "LICENSE",
30
+ "LICENSE.md"
31
+ ]
39
32
  }
@@ -1,4 +0,0 @@
1
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none"
2
- xmlns="http://www.w3.org/2000/svg">
3
- <path fill-rule="evenodd" clip-rule="evenodd" d="M13.5858 12L9.29289 7.70711C8.90237 7.31658 8.90237 6.68342 9.29289 6.29289C9.68342 5.90237 10.3166 5.90237 10.7071 6.29289L15.7071 11.2929C16.0976 11.6834 16.0976 12.3166 15.7071 12.7071L10.7071 17.7071C10.3166 18.0976 9.68342 18.0976 9.29289 17.7071C8.90237 17.3166 8.90237 16.6834 9.29289 16.2929L13.5858 12Z" fill="currentColor"/>
4
- </svg>
@@ -1,10 +0,0 @@
1
- // src/elements/Button.jsx
2
- import React from "react";
3
- function Button() {
4
- return /* @__PURE__ */ React.createElement("div", { className: "border-2 border-primary-700" }, "Button");
5
- }
6
- var Button_default = Button;
7
-
8
- export {
9
- Button_default
10
- };