@zentauri-ui/zentauri-components 2.1.7 → 2.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +7 -5
  2. package/cli/props.json +189 -3
  3. package/cli/registry.json +9 -0
  4. package/dist/{chunk-KVSRUAXP.mjs → chunk-4PAHLHYF.mjs} +3 -3
  5. package/dist/{chunk-KVSRUAXP.mjs.map → chunk-4PAHLHYF.mjs.map} +1 -1
  6. package/dist/chunk-4SLVTSHM.js +241 -0
  7. package/dist/chunk-4SLVTSHM.js.map +1 -0
  8. package/dist/chunk-6OVDBAMI.js +19 -0
  9. package/dist/{chunk-5FU57ZVQ.js.map → chunk-6OVDBAMI.js.map} +1 -1
  10. package/dist/{chunk-5ELR6MIN.js → chunk-BAAXQPZ7.js} +6 -6
  11. package/dist/{chunk-5ELR6MIN.js.map → chunk-BAAXQPZ7.js.map} +1 -1
  12. package/dist/{chunk-DBNGLT5U.mjs → chunk-D7ZTSAA6.mjs} +4 -4
  13. package/dist/{chunk-DBNGLT5U.mjs.map → chunk-D7ZTSAA6.mjs.map} +1 -1
  14. package/dist/{chunk-TJ2EWPER.js → chunk-DPNTQ4AK.js} +47 -3
  15. package/dist/chunk-DPNTQ4AK.js.map +1 -0
  16. package/dist/chunk-IHDM7AHY.mjs +233 -0
  17. package/dist/chunk-IHDM7AHY.mjs.map +1 -0
  18. package/dist/{chunk-G7FVHZRB.js → chunk-L5QORCUO.js} +12 -12
  19. package/dist/{chunk-G7FVHZRB.js.map → chunk-L5QORCUO.js.map} +1 -1
  20. package/dist/{chunk-7UXPXCKV.mjs → chunk-OWVQVAOY.mjs} +3 -3
  21. package/dist/{chunk-7UXPXCKV.mjs.map → chunk-OWVQVAOY.mjs.map} +1 -1
  22. package/dist/{chunk-FUCW5GPE.mjs → chunk-UVP3MUBU.mjs} +39 -4
  23. package/dist/chunk-UVP3MUBU.mjs.map +1 -0
  24. package/dist/design-system/facade.js +3 -3
  25. package/dist/design-system/facade.mjs +2 -2
  26. package/dist/design-system/index.d.ts +1 -0
  27. package/dist/design-system/index.d.ts.map +1 -1
  28. package/dist/design-system/split-button.d.ts +25 -0
  29. package/dist/design-system/split-button.d.ts.map +1 -0
  30. package/dist/ui/buttons/animated.js +5 -5
  31. package/dist/ui/buttons/animated.mjs +3 -3
  32. package/dist/ui/buttons.js +6 -6
  33. package/dist/ui/buttons.mjs +4 -4
  34. package/dist/ui/data-table.js +16 -16
  35. package/dist/ui/data-table.mjs +6 -6
  36. package/dist/ui/dropdown/dropdown.d.ts +1 -1
  37. package/dist/ui/dropdown/dropdown.d.ts.map +1 -1
  38. package/dist/ui/dropdown/types.d.ts +2 -2
  39. package/dist/ui/dropdown/types.d.ts.map +1 -1
  40. package/dist/ui/dropdown.js +31 -231
  41. package/dist/ui/dropdown.js.map +1 -1
  42. package/dist/ui/dropdown.mjs +4 -229
  43. package/dist/ui/dropdown.mjs.map +1 -1
  44. package/dist/ui/dynamic-stepper.js +15 -15
  45. package/dist/ui/dynamic-stepper.mjs +4 -4
  46. package/dist/ui/pagination.js +7 -7
  47. package/dist/ui/pagination.mjs +4 -4
  48. package/dist/ui/split-button/index.d.ts +4 -0
  49. package/dist/ui/split-button/index.d.ts.map +1 -0
  50. package/dist/ui/split-button/split-button-base.d.ts +6 -0
  51. package/dist/ui/split-button/split-button-base.d.ts.map +1 -0
  52. package/dist/ui/split-button/split-button.d.ts +6 -0
  53. package/dist/ui/split-button/split-button.d.ts.map +1 -0
  54. package/dist/ui/split-button/types.d.ts +30 -0
  55. package/dist/ui/split-button/types.d.ts.map +1 -0
  56. package/dist/ui/split-button/variants.d.ts +16 -0
  57. package/dist/ui/split-button/variants.d.ts.map +1 -0
  58. package/dist/ui/split-button.js +287 -0
  59. package/dist/ui/split-button.js.map +1 -0
  60. package/dist/ui/split-button.mjs +278 -0
  61. package/dist/ui/split-button.mjs.map +1 -0
  62. package/package.json +1 -1
  63. package/src/design-system/index.ts +1 -0
  64. package/src/design-system/split-button.ts +38 -0
  65. package/src/ui/dropdown/dropdown.tsx +7 -3
  66. package/src/ui/dropdown/types.ts +2 -2
  67. package/src/ui/split-button/index.ts +19 -0
  68. package/src/ui/split-button/split-button-base.tsx +232 -0
  69. package/src/ui/split-button/split-button.test.tsx +208 -0
  70. package/src/ui/split-button/split-button.tsx +9 -0
  71. package/src/ui/split-button/types.ts +46 -0
  72. package/src/ui/split-button/variants.ts +46 -0
  73. package/dist/chunk-5FU57ZVQ.js +0 -19
  74. package/dist/chunk-FUCW5GPE.mjs.map +0 -1
  75. package/dist/chunk-TJ2EWPER.js.map +0 -1
@@ -0,0 +1,38 @@
1
+ export const zuiSplitButtonRoot =
2
+ "inline-flex align-middle data-[full-width=true]:w-full";
3
+
4
+ export const zuiSplitButtonFullWidth = "w-full";
5
+
6
+ export const zuiSplitButtonDropdown =
7
+ "data-[full-width=true]:block data-[full-width=true]:w-full";
8
+
9
+ export const zuiSplitButtonGroup =
10
+ "inline-flex items-stretch data-[full-width=true]:w-full";
11
+
12
+ export const zuiSplitButtonPrimary =
13
+ "rounded-r-none data-[full-width=true]:min-w-0 data-[full-width=true]:flex-1";
14
+
15
+ export const zuiSplitButtonTrigger =
16
+ "rounded-l-none border-l border-[color:var(--zui-split-button-separator,var(--zui-border,#ffffff33))] dark:border-[color:var(--zui-split-button-separator-dark,var(--zui-border-dark,#00000033))] px-2.5";
17
+
18
+ export const zuiSplitButtonTriggerSizes = {
19
+ sm: "min-w-8 px-2",
20
+ md: "min-w-10 px-2.5",
21
+ lg: "min-w-11 px-3",
22
+ xl: "min-w-12 px-3.5",
23
+ "2xl": "min-w-14 px-4",
24
+ "3xl": "min-w-16 px-4",
25
+ "4xl": "min-w-18 px-5",
26
+ "5xl": "min-w-20 px-5",
27
+ "6xl": "min-w-22 px-6",
28
+ "7xl": "min-w-24 px-6",
29
+ "8xl": "min-w-26 px-7",
30
+ "9xl": "min-w-28 px-7",
31
+ "10xl": "min-w-30 px-8",
32
+ icon: "min-w-10 px-0",
33
+ } as const;
34
+
35
+ export const zuiSplitButtonContent =
36
+ "min-w-[var(--zui-split-button-menu-min-width,12rem)]";
37
+
38
+ export const zuiSplitButtonItemDisabled = "pointer-events-none opacity-50";
@@ -37,10 +37,12 @@ const useDropdown = () => {
37
37
  ========================= */
38
38
  export const Dropdown = ({
39
39
  children,
40
+ className,
40
41
  defaultOpen = false,
41
42
  open: controlledOpen,
42
43
  onOpenChange,
43
44
  multiSelect = false,
45
+ ...props
44
46
  }: DropdownProps) => {
45
47
  const menuId = `${useId()}-menu`;
46
48
  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
@@ -82,7 +84,9 @@ export const Dropdown = ({
82
84
  menuId,
83
85
  }}
84
86
  >
85
- <div className="relative inline-block">{children}</div>
87
+ <div className={cn("relative inline-block", className)} {...props}>
88
+ {children}
89
+ </div>
86
90
  </DropdownContext.Provider>
87
91
  );
88
92
  };
@@ -180,10 +184,10 @@ export const DropdownItem = ({
180
184
  ...props
181
185
  }: DropdownItemProps) => {
182
186
  const { toggleSelect, selectedValues } = useDropdown();
183
- const isSelected = selectedValues.includes(value);
187
+ const isSelected = value !== undefined && selectedValues.includes(value);
184
188
 
185
189
  const handleClick = () => {
186
- toggleSelect(value);
190
+ if (value !== undefined) toggleSelect(value);
187
191
  onSelect?.();
188
192
  };
189
193
 
@@ -14,7 +14,7 @@ export type DropdownContextType = {
14
14
 
15
15
  type Variant = keyof typeof zuiDropdownTriggerVariants;
16
16
 
17
- export type DropdownProps = {
17
+ export type DropdownProps = HTMLAttributes<HTMLDivElement> & {
18
18
  children: ReactNode;
19
19
  defaultOpen?: boolean;
20
20
  open?: boolean;
@@ -37,7 +37,7 @@ export type DropdownContentProps = HTMLAttributes<HTMLDivElement> & {
37
37
 
38
38
  export type DropdownItemProps = HTMLAttributes<HTMLDivElement> & {
39
39
  children: ReactNode;
40
- value: string;
40
+ value?: string;
41
41
  onSelect?: () => void;
42
42
  leftIcon?: ReactNode;
43
43
  rightIcon?: ReactNode;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ export { SplitButton } from "./split-button";
4
+ export type {
5
+ SplitButtonAppearance,
6
+ SplitButtonItem,
7
+ SplitButtonProps,
8
+ SplitButtonSize,
9
+ SplitButtonVariant,
10
+ } from "./types";
11
+ export {
12
+ splitButtonContentVariants,
13
+ splitButtonDropdownVariants,
14
+ splitButtonGroupVariants,
15
+ splitButtonItemDisabledVariants,
16
+ splitButtonPrimaryVariants,
17
+ splitButtonRootVariants,
18
+ splitButtonTriggerVariants,
19
+ } from "./variants";
@@ -0,0 +1,232 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { FiChevronDown, FiLoader } from "react-icons/fi";
5
+
6
+ import {
7
+ Dropdown,
8
+ DropdownContent,
9
+ DropdownItem,
10
+ DropdownTrigger,
11
+ } from "../dropdown";
12
+ import { buttonVariants } from "../buttons";
13
+ import { cn } from "../../lib/utils";
14
+
15
+ import type {
16
+ SplitButtonAppearance,
17
+ SplitButtonProps,
18
+ SplitButtonVariant,
19
+ } from "./types";
20
+ import {
21
+ splitButtonContentVariants,
22
+ splitButtonDropdownVariants,
23
+ splitButtonGroupVariants,
24
+ splitButtonItemDisabledVariants,
25
+ splitButtonPrimaryVariants,
26
+ splitButtonRootVariants,
27
+ splitButtonTriggerVariants,
28
+ } from "./variants";
29
+
30
+ const variantAppearanceMap = {
31
+ primary: "default",
32
+ secondary: "secondary",
33
+ outline: "outline",
34
+ ghost: "ghost",
35
+ danger: "destructive",
36
+ success: "green",
37
+ } as const satisfies Record<SplitButtonVariant, SplitButtonAppearance>;
38
+
39
+ function resolveAppearance({
40
+ appearance,
41
+ variant,
42
+ }: {
43
+ appearance?: SplitButtonAppearance;
44
+ variant?: SplitButtonVariant;
45
+ }) {
46
+ return appearance ?? (variant ? variantAppearanceMap[variant] : "default");
47
+ }
48
+
49
+ export function SplitButtonBase({
50
+ label,
51
+ onClick,
52
+ items,
53
+ disabled = false,
54
+ loading = false,
55
+ appearance,
56
+ variant,
57
+ size = "md",
58
+ startIcon,
59
+ fullWidth = false,
60
+ open: controlledOpen,
61
+ defaultOpen = false,
62
+ onOpenChange,
63
+ triggerLabel,
64
+ triggerOn = "click",
65
+ className,
66
+ ref,
67
+ ...rest
68
+ }: SplitButtonProps) {
69
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
70
+ const isUnavailable = disabled || loading;
71
+ const resolvedAppearance = resolveAppearance({ appearance, variant });
72
+ const isControlled = controlledOpen !== undefined;
73
+ const open = isUnavailable
74
+ ? false
75
+ : isControlled
76
+ ? controlledOpen
77
+ : uncontrolledOpen;
78
+
79
+ const setOpen = (nextOpen: boolean) => {
80
+ if (isUnavailable && nextOpen) {
81
+ return;
82
+ }
83
+ if (!isControlled) {
84
+ setUncontrolledOpen(nextOpen);
85
+ }
86
+ onOpenChange?.(nextOpen);
87
+ };
88
+
89
+ const fullWidthFlag = fullWidth ? "true" : undefined;
90
+ const menuLabel = triggerLabel ?? `More ${label} actions`;
91
+
92
+ // Shared timeout ref for hover mode: delays close so the cursor can
93
+ // travel through the mt-2 gap between the button and the menu panel
94
+ // without dismissing the menu.
95
+ const hoverCloseRef = useRef<ReturnType<typeof setTimeout> | null>(null);
96
+
97
+ const scheduleClose = () => {
98
+ cancelClose();
99
+ hoverCloseRef.current = setTimeout(() => setOpen(false), 120);
100
+ };
101
+
102
+ const cancelClose = () => {
103
+ if (hoverCloseRef.current !== null) {
104
+ clearTimeout(hoverCloseRef.current);
105
+ hoverCloseRef.current = null;
106
+ }
107
+ };
108
+
109
+ useEffect(() => cancelClose, []);
110
+
111
+ useEffect(() => {
112
+ if (isUnavailable && !isControlled) {
113
+ setUncontrolledOpen(false);
114
+ onOpenChange?.(false);
115
+ }
116
+ }, [isUnavailable, onOpenChange, isControlled]);
117
+
118
+ const dropdownHoverHandlers =
119
+ triggerOn === "hover"
120
+ ? {
121
+ onMouseEnter: () => {
122
+ cancelClose();
123
+ setOpen(true);
124
+ },
125
+ onMouseLeave: scheduleClose,
126
+ }
127
+ : undefined;
128
+
129
+ const contentHoverHandlers =
130
+ triggerOn === "hover"
131
+ ? { onMouseEnter: cancelClose, onMouseLeave: scheduleClose }
132
+ : undefined;
133
+
134
+ return (
135
+ <div
136
+ ref={ref}
137
+ data-slot="split-button"
138
+ data-full-width={fullWidthFlag}
139
+ className={cn(splitButtonRootVariants({ fullWidth }), className)}
140
+ {...rest}
141
+ >
142
+ <Dropdown
143
+ open={open}
144
+ defaultOpen={defaultOpen}
145
+ onOpenChange={setOpen}
146
+ data-full-width={fullWidthFlag}
147
+ className={splitButtonDropdownVariants({ fullWidth })}
148
+ {...dropdownHoverHandlers}
149
+ >
150
+ <div
151
+ data-slot="split-button-group"
152
+ data-full-width={fullWidthFlag}
153
+ className={splitButtonGroupVariants({ fullWidth })}
154
+ >
155
+ <button
156
+ type="button"
157
+ data-slot="split-button-primary"
158
+ data-full-width={fullWidthFlag}
159
+ disabled={isUnavailable}
160
+ onClick={onClick}
161
+ className={cn(
162
+ buttonVariants({ appearance: resolvedAppearance, size }),
163
+ splitButtonPrimaryVariants(),
164
+ )}
165
+ >
166
+ {loading ? (
167
+ <FiLoader
168
+ aria-hidden
169
+ className="animate-spin"
170
+ data-slot="split-button-spinner-icon"
171
+ />
172
+ ) : (
173
+ startIcon
174
+ )}
175
+ <span>{label}</span>
176
+ {loading ? (
177
+ <span className="sr-only" role="status" aria-label="Loading" />
178
+ ) : null}
179
+ </button>
180
+
181
+ <DropdownTrigger
182
+ aria-label={menuLabel}
183
+ disabled={isUnavailable}
184
+ className={cn(
185
+ buttonVariants({ appearance: resolvedAppearance, size }),
186
+ splitButtonTriggerVariants({ size }),
187
+ )}
188
+ >
189
+ <FiChevronDown aria-hidden />
190
+ </DropdownTrigger>
191
+ </div>
192
+
193
+ <DropdownContent
194
+ className={splitButtonContentVariants()}
195
+ placement="bottom"
196
+ {...contentHoverHandlers}
197
+ >
198
+ {items.map((item) => (
199
+ <DropdownItem
200
+ key={item.id}
201
+ leftIcon={item.icon}
202
+ aria-disabled={item.disabled ? "true" : undefined}
203
+ className={
204
+ item.disabled ? splitButtonItemDisabledVariants() : undefined
205
+ }
206
+ onClick={(event) => {
207
+ if (item.disabled) {
208
+ event.preventDefault();
209
+ event.stopPropagation();
210
+ }
211
+ }}
212
+ onKeyDown={(event) => {
213
+ if (item.disabled) {
214
+ event.preventDefault();
215
+ event.stopPropagation();
216
+ }
217
+ }}
218
+ onSelect={() => {
219
+ setOpen(false);
220
+ item.onSelect?.();
221
+ }}
222
+ >
223
+ {item.label}
224
+ </DropdownItem>
225
+ ))}
226
+ </DropdownContent>
227
+ </Dropdown>
228
+ </div>
229
+ );
230
+ }
231
+
232
+ SplitButtonBase.displayName = "SplitButton";
@@ -0,0 +1,208 @@
1
+ import { createRef } from "react";
2
+ import { render, screen, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { describe, expect, it, vi } from "vitest";
5
+
6
+ import { SplitButton } from "./split-button";
7
+
8
+ const items = [
9
+ { id: "save-as", label: "Save As", onSelect: vi.fn() },
10
+ { id: "export", label: "Export", onSelect: vi.fn() },
11
+ ];
12
+
13
+ describe("SplitButton", () => {
14
+ it("should render the primary label and menu trigger", () => {
15
+ render(<SplitButton label="Save" items={items} />);
16
+
17
+ expect(screen.getByRole("button", { name: "Save" })).toBeInTheDocument();
18
+ expect(
19
+ screen.getByRole("button", { name: /more save actions/i }),
20
+ ).toHaveAttribute("aria-haspopup", "menu");
21
+ });
22
+
23
+ it("should call onClick from the primary button", async () => {
24
+ const user = userEvent.setup();
25
+ const onClick = vi.fn();
26
+ render(<SplitButton label="Save" items={items} onClick={onClick} />);
27
+
28
+ await user.click(screen.getByRole("button", { name: "Save" }));
29
+
30
+ expect(onClick).toHaveBeenCalledTimes(1);
31
+ });
32
+
33
+ it("should open the menu and call an item onSelect", async () => {
34
+ const user = userEvent.setup();
35
+ const handleExport = vi.fn();
36
+ render(
37
+ <SplitButton
38
+ label="Save"
39
+ items={[
40
+ { id: "save-as", label: "Save As" },
41
+ { id: "export", label: "Export", onSelect: handleExport },
42
+ ]}
43
+ />,
44
+ );
45
+
46
+ await user.click(
47
+ screen.getByRole("button", { name: /more save actions/i }),
48
+ );
49
+ await user.click(await screen.findByRole("menuitem", { name: "Export" }));
50
+
51
+ expect(handleExport).toHaveBeenCalledTimes(1);
52
+ await waitFor(() => {
53
+ expect(screen.queryByRole("menuitem", { name: "Export" })).toBeNull();
54
+ });
55
+ });
56
+
57
+ it("should not open or call actions while disabled", async () => {
58
+ const user = userEvent.setup();
59
+ const onClick = vi.fn();
60
+ render(
61
+ <SplitButton
62
+ disabled
63
+ label="Save"
64
+ items={[{ id: "export", label: "Export" }]}
65
+ onClick={onClick}
66
+ />,
67
+ );
68
+
69
+ const buttons = screen.getAllByRole("button");
70
+ expect(buttons).toHaveLength(2);
71
+ const [primary, trigger] = buttons as [HTMLElement, HTMLElement];
72
+
73
+ expect(primary).toBeDisabled();
74
+ expect(trigger).toBeDisabled();
75
+ await user.click(primary);
76
+ await user.click(trigger);
77
+
78
+ expect(onClick).not.toHaveBeenCalled();
79
+ expect(screen.queryByRole("menu")).toBeNull();
80
+ });
81
+
82
+ it("should show a spinner and disable both buttons while loading", async () => {
83
+ const user = userEvent.setup();
84
+ render(
85
+ <SplitButton
86
+ loading
87
+ label="Saving"
88
+ items={[{ id: "export", label: "Export" }]}
89
+ />,
90
+ );
91
+
92
+ expect(
93
+ screen.getByRole("status", { name: /loading/i }),
94
+ ).toBeInTheDocument();
95
+ for (const button of screen.getAllByRole("button")) {
96
+ expect(button).toBeDisabled();
97
+ }
98
+
99
+ await user.click(
100
+ screen.getByRole("button", { name: /more saving actions/i }),
101
+ );
102
+ expect(screen.queryByRole("menu")).toBeNull();
103
+ });
104
+
105
+ it("should support controlled open state", () => {
106
+ const onOpenChange = vi.fn();
107
+ render(
108
+ <SplitButton
109
+ open
110
+ onOpenChange={onOpenChange}
111
+ label="Save"
112
+ items={[{ id: "export", label: "Export" }]}
113
+ />,
114
+ );
115
+
116
+ expect(screen.getByRole("menuitem", { name: "Export" })).toBeVisible();
117
+ expect(
118
+ screen.getByRole("button", { name: /more save actions/i }),
119
+ ).toHaveAttribute("aria-expanded", "true");
120
+ });
121
+
122
+ it("should render start and item icons", async () => {
123
+ const user = userEvent.setup();
124
+ render(
125
+ <SplitButton
126
+ label="Save"
127
+ startIcon={<span data-testid="start-icon" />}
128
+ items={[
129
+ {
130
+ id: "export",
131
+ label: "Export",
132
+ icon: <span data-testid="item-icon" />,
133
+ },
134
+ ]}
135
+ />,
136
+ );
137
+
138
+ expect(screen.getByTestId("start-icon")).toBeInTheDocument();
139
+ await user.click(
140
+ screen.getByRole("button", { name: /more save actions/i }),
141
+ );
142
+ expect(screen.getByTestId("item-icon")).toBeInTheDocument();
143
+ });
144
+
145
+ it("should apply variant aliases and fullWidth layout", () => {
146
+ render(
147
+ <SplitButton
148
+ fullWidth
149
+ label="Save"
150
+ variant="danger"
151
+ items={[{ id: "export", label: "Export" }]}
152
+ />,
153
+ );
154
+
155
+ expect(document.querySelector('[data-slot="split-button"]')).toHaveClass(
156
+ "w-full",
157
+ );
158
+ expect(screen.getByRole("button", { name: "Save" }).className).toMatch(
159
+ /destructive/,
160
+ );
161
+ });
162
+
163
+ it("should forward ref to the root element", () => {
164
+ const ref = createRef<HTMLDivElement>();
165
+ render(<SplitButton ref={ref} label="Save" items={items} />);
166
+
167
+ expect(ref.current?.getAttribute("data-slot")).toBe("split-button");
168
+ });
169
+
170
+ it("should open the menu on hover and close on mouse leave when triggerOn is hover", async () => {
171
+ const user = userEvent.setup();
172
+ render(
173
+ <SplitButton
174
+ triggerOn="hover"
175
+ label="Save"
176
+ items={[{ id: "export", label: "Export" }]}
177
+ />,
178
+ );
179
+
180
+ const trigger = screen.getByRole("button", { name: /more save actions/i });
181
+ await user.hover(trigger);
182
+ expect(
183
+ await screen.findByRole("menuitem", { name: "Export" }),
184
+ ).toBeVisible();
185
+
186
+ await user.unhover(trigger);
187
+ await waitFor(() => {
188
+ expect(screen.queryByRole("menuitem", { name: "Export" })).toBeNull();
189
+ });
190
+ });
191
+
192
+ it("should not open on hover when disabled with triggerOn hover", async () => {
193
+ const user = userEvent.setup();
194
+ render(
195
+ <SplitButton
196
+ triggerOn="hover"
197
+ disabled
198
+ label="Save"
199
+ items={[{ id: "export", label: "Export" }]}
200
+ />,
201
+ );
202
+
203
+ await user.hover(
204
+ screen.getByRole("button", { name: /more save actions/i }),
205
+ );
206
+ expect(screen.queryByRole("menu")).toBeNull();
207
+ });
208
+ });
@@ -0,0 +1,9 @@
1
+ // split-button.tsx — default static entry (no framer-motion)
2
+ import { SplitButtonBase } from "./split-button-base";
3
+ import type { SplitButtonProps } from "./types";
4
+
5
+ export const SplitButton = (props: SplitButtonProps) => {
6
+ return <SplitButtonBase {...props} />;
7
+ };
8
+
9
+ SplitButton.displayName = "SplitButton";
@@ -0,0 +1,46 @@
1
+ import type {
2
+ ComponentPropsWithRef,
3
+ MouseEventHandler,
4
+ ReactNode,
5
+ } from "react";
6
+
7
+ import type { ButtonSharedStatic } from "../buttons";
8
+
9
+ export type SplitButtonAppearance = ButtonSharedStatic["appearance"];
10
+ export type SplitButtonSize = Extract<ButtonSharedStatic["size"], string>;
11
+ export type SplitButtonVariant =
12
+ | "primary"
13
+ | "secondary"
14
+ | "outline"
15
+ | "ghost"
16
+ | "danger"
17
+ | "success";
18
+
19
+ export interface SplitButtonItem {
20
+ id: string;
21
+ label: string;
22
+ icon?: ReactNode;
23
+ disabled?: boolean;
24
+ onSelect?: () => void;
25
+ }
26
+
27
+ export interface SplitButtonProps extends Omit<
28
+ ComponentPropsWithRef<"div">,
29
+ "children" | "onClick"
30
+ > {
31
+ label: string;
32
+ onClick?: MouseEventHandler<HTMLButtonElement>;
33
+ items: SplitButtonItem[];
34
+ disabled?: boolean;
35
+ loading?: boolean;
36
+ appearance?: SplitButtonAppearance;
37
+ variant?: SplitButtonVariant;
38
+ size?: SplitButtonSize;
39
+ startIcon?: ReactNode;
40
+ fullWidth?: boolean;
41
+ open?: boolean;
42
+ defaultOpen?: boolean;
43
+ onOpenChange?: (open: boolean) => void;
44
+ triggerLabel?: string;
45
+ triggerOn?: "click" | "hover";
46
+ }
@@ -0,0 +1,46 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ import {
4
+ zuiSplitButtonContent,
5
+ zuiSplitButtonDropdown,
6
+ zuiSplitButtonFullWidth,
7
+ zuiSplitButtonGroup,
8
+ zuiSplitButtonItemDisabled,
9
+ zuiSplitButtonPrimary,
10
+ zuiSplitButtonRoot,
11
+ zuiSplitButtonTrigger,
12
+ zuiSplitButtonTriggerSizes,
13
+ } from "../../design-system";
14
+
15
+ export const splitButtonRootVariants = cva(zuiSplitButtonRoot, {
16
+ variants: {
17
+ fullWidth: {
18
+ true: zuiSplitButtonFullWidth,
19
+ },
20
+ },
21
+ });
22
+ export const splitButtonDropdownVariants = cva(zuiSplitButtonDropdown, {
23
+ variants: {
24
+ fullWidth: {
25
+ true: zuiSplitButtonFullWidth,
26
+ },
27
+ },
28
+ });
29
+ export const splitButtonGroupVariants = cva(zuiSplitButtonGroup, {
30
+ variants: {
31
+ fullWidth: {
32
+ true: zuiSplitButtonFullWidth,
33
+ },
34
+ },
35
+ });
36
+ export const splitButtonPrimaryVariants = cva(zuiSplitButtonPrimary);
37
+ export const splitButtonTriggerVariants = cva(zuiSplitButtonTrigger, {
38
+ variants: {
39
+ size: zuiSplitButtonTriggerSizes,
40
+ },
41
+ defaultVariants: {
42
+ size: "md",
43
+ },
44
+ });
45
+ export const splitButtonContentVariants = cva(zuiSplitButtonContent);
46
+ export const splitButtonItemDisabledVariants = cva(zuiSplitButtonItemDisabled);
@@ -1,19 +0,0 @@
1
- 'use strict';
2
-
3
- var chunkTJ2EWPER_js = require('./chunk-TJ2EWPER.js');
4
- var classVarianceAuthority = require('class-variance-authority');
5
-
6
- var buttonVariants = classVarianceAuthority.cva(chunkTJ2EWPER_js.zuiButtonBase, {
7
- variants: {
8
- appearance: chunkTJ2EWPER_js.zuiButtonAppearances,
9
- size: chunkTJ2EWPER_js.zuiButtonSizes
10
- },
11
- defaultVariants: {
12
- appearance: "default",
13
- size: "md"
14
- }
15
- });
16
-
17
- exports.buttonVariants = buttonVariants;
18
- //# sourceMappingURL=chunk-5FU57ZVQ.js.map
19
- //# sourceMappingURL=chunk-5FU57ZVQ.js.map