@zentauri-ui/zentauri-components 2.1.6 → 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 (152) hide show
  1. package/README.md +12 -8
  2. package/cli/cli.integration.test.ts +36 -0
  3. package/cli/index.mjs +43 -7
  4. package/cli/props.json +462 -3
  5. package/cli/registry.json +29 -0
  6. package/cli/rewrite-imports.mjs +29 -4
  7. package/cli/rewrite-imports.test.ts +35 -0
  8. package/dist/{chunk-WWKAJHIV.mjs → chunk-4PAHLHYF.mjs} +3 -3
  9. package/dist/{chunk-WWKAJHIV.mjs.map → chunk-4PAHLHYF.mjs.map} +1 -1
  10. package/dist/chunk-4SLVTSHM.js +241 -0
  11. package/dist/chunk-4SLVTSHM.js.map +1 -0
  12. package/dist/chunk-6OVDBAMI.js +19 -0
  13. package/dist/{chunk-3W2UUKWP.js.map → chunk-6OVDBAMI.js.map} +1 -1
  14. package/dist/chunk-74SKXGTM.js +4 -0
  15. package/dist/chunk-74SKXGTM.js.map +1 -0
  16. package/dist/{chunk-QE7OJW4J.js → chunk-BAAXQPZ7.js} +6 -6
  17. package/dist/{chunk-QE7OJW4J.js.map → chunk-BAAXQPZ7.js.map} +1 -1
  18. package/dist/chunk-CYKSS5S5.mjs +128 -0
  19. package/dist/chunk-CYKSS5S5.mjs.map +1 -0
  20. package/dist/chunk-D7ZTSAA6.mjs +221 -0
  21. package/dist/chunk-D7ZTSAA6.mjs.map +1 -0
  22. package/dist/{chunk-VA6SB6NN.js → chunk-DPNTQ4AK.js} +73 -6
  23. package/dist/chunk-DPNTQ4AK.js.map +1 -0
  24. package/dist/chunk-HMDH4BQJ.js +123 -0
  25. package/dist/chunk-HMDH4BQJ.js.map +1 -0
  26. package/dist/chunk-I7EBE7BD.js +98 -0
  27. package/dist/chunk-I7EBE7BD.js.map +1 -0
  28. package/dist/chunk-IHDM7AHY.mjs +233 -0
  29. package/dist/chunk-IHDM7AHY.mjs.map +1 -0
  30. package/dist/chunk-L5QORCUO.js +225 -0
  31. package/dist/chunk-L5QORCUO.js.map +1 -0
  32. package/dist/chunk-LHBJD57K.mjs +143 -0
  33. package/dist/chunk-LHBJD57K.mjs.map +1 -0
  34. package/dist/{chunk-CHI6MBTI.mjs → chunk-OWVQVAOY.mjs} +3 -3
  35. package/dist/{chunk-CHI6MBTI.mjs.map → chunk-OWVQVAOY.mjs.map} +1 -1
  36. package/dist/chunk-OYAJG2BO.js +83 -0
  37. package/dist/chunk-OYAJG2BO.js.map +1 -0
  38. package/dist/chunk-PTU5ZAYX.js +145 -0
  39. package/dist/chunk-PTU5ZAYX.js.map +1 -0
  40. package/dist/chunk-QKO5DA4N.mjs +81 -0
  41. package/dist/chunk-QKO5DA4N.mjs.map +1 -0
  42. package/dist/chunk-T7PIKDUZ.js +130 -0
  43. package/dist/chunk-T7PIKDUZ.js.map +1 -0
  44. package/dist/chunk-TDK5TVJE.mjs +3 -0
  45. package/dist/chunk-TDK5TVJE.mjs.map +1 -0
  46. package/dist/{chunk-A4IB3C23.mjs → chunk-UVP3MUBU.mjs} +58 -7
  47. package/dist/chunk-UVP3MUBU.mjs.map +1 -0
  48. package/dist/chunk-VBNW2B4D.mjs +3 -0
  49. package/dist/chunk-VBNW2B4D.mjs.map +1 -0
  50. package/dist/chunk-W6DO36XD.mjs +96 -0
  51. package/dist/chunk-W6DO36XD.mjs.map +1 -0
  52. package/dist/chunk-XR3J46TZ.js +4 -0
  53. package/dist/chunk-XR3J46TZ.js.map +1 -0
  54. package/dist/chunk-ZOHCADDL.mjs +121 -0
  55. package/dist/chunk-ZOHCADDL.mjs.map +1 -0
  56. package/dist/design-system/data-table.d.ts +8 -0
  57. package/dist/design-system/data-table.d.ts.map +1 -0
  58. package/dist/design-system/facade.js +6 -6
  59. package/dist/design-system/facade.mjs +5 -5
  60. package/dist/design-system/index.d.ts +2 -0
  61. package/dist/design-system/index.d.ts.map +1 -1
  62. package/dist/design-system/split-button.d.ts +25 -0
  63. package/dist/design-system/split-button.d.ts.map +1 -0
  64. package/dist/hooks/useTableFilter.js +6 -116
  65. package/dist/hooks/useTableFilter.js.map +1 -1
  66. package/dist/hooks/useTableFilter.mjs +1 -118
  67. package/dist/hooks/useTableFilter.mjs.map +1 -1
  68. package/dist/hooks/useTableSort.js +6 -91
  69. package/dist/hooks/useTableSort.js.map +1 -1
  70. package/dist/hooks/useTableSort.mjs +1 -93
  71. package/dist/hooks/useTableSort.mjs.map +1 -1
  72. package/dist/hooks/useVirtualList.js +6 -76
  73. package/dist/hooks/useVirtualList.js.map +1 -1
  74. package/dist/hooks/useVirtualList.mjs +1 -78
  75. package/dist/hooks/useVirtualList.mjs.map +1 -1
  76. package/dist/ui/buttons/animated.js +8 -8
  77. package/dist/ui/buttons/animated.mjs +6 -6
  78. package/dist/ui/buttons.js +10 -9
  79. package/dist/ui/buttons.mjs +8 -7
  80. package/dist/ui/checkbox.js +7 -123
  81. package/dist/ui/checkbox.js.map +1 -1
  82. package/dist/ui/checkbox.mjs +2 -126
  83. package/dist/ui/checkbox.mjs.map +1 -1
  84. package/dist/ui/data-table/data-table-base.d.ts +6 -0
  85. package/dist/ui/data-table/data-table-base.d.ts.map +1 -0
  86. package/dist/ui/data-table/data-table.d.ts +6 -0
  87. package/dist/ui/data-table/data-table.d.ts.map +1 -0
  88. package/dist/ui/data-table/index.d.ts +4 -0
  89. package/dist/ui/data-table/index.d.ts.map +1 -0
  90. package/dist/ui/data-table/types.d.ts +92 -0
  91. package/dist/ui/data-table/types.d.ts.map +1 -0
  92. package/dist/ui/data-table/variants.d.ts +8 -0
  93. package/dist/ui/data-table/variants.d.ts.map +1 -0
  94. package/dist/ui/data-table.js +620 -0
  95. package/dist/ui/data-table.js.map +1 -0
  96. package/dist/ui/data-table.mjs +611 -0
  97. package/dist/ui/data-table.mjs.map +1 -0
  98. package/dist/ui/dropdown/dropdown.d.ts +1 -1
  99. package/dist/ui/dropdown/dropdown.d.ts.map +1 -1
  100. package/dist/ui/dropdown/types.d.ts +2 -2
  101. package/dist/ui/dropdown/types.d.ts.map +1 -1
  102. package/dist/ui/dropdown.js +31 -231
  103. package/dist/ui/dropdown.js.map +1 -1
  104. package/dist/ui/dropdown.mjs +4 -229
  105. package/dist/ui/dropdown.mjs.map +1 -1
  106. package/dist/ui/dynamic-stepper.js +18 -18
  107. package/dist/ui/dynamic-stepper.mjs +7 -7
  108. package/dist/ui/inputs.js +7 -138
  109. package/dist/ui/inputs.js.map +1 -1
  110. package/dist/ui/inputs.mjs +2 -141
  111. package/dist/ui/inputs.mjs.map +1 -1
  112. package/dist/ui/pagination.js +20 -221
  113. package/dist/ui/pagination.js.map +1 -1
  114. package/dist/ui/pagination.mjs +8 -223
  115. package/dist/ui/pagination.mjs.map +1 -1
  116. package/dist/ui/split-button/index.d.ts +4 -0
  117. package/dist/ui/split-button/index.d.ts.map +1 -0
  118. package/dist/ui/split-button/split-button-base.d.ts +6 -0
  119. package/dist/ui/split-button/split-button-base.d.ts.map +1 -0
  120. package/dist/ui/split-button/split-button.d.ts +6 -0
  121. package/dist/ui/split-button/split-button.d.ts.map +1 -0
  122. package/dist/ui/split-button/types.d.ts +30 -0
  123. package/dist/ui/split-button/types.d.ts.map +1 -0
  124. package/dist/ui/split-button/variants.d.ts +16 -0
  125. package/dist/ui/split-button/variants.d.ts.map +1 -0
  126. package/dist/ui/split-button.js +287 -0
  127. package/dist/ui/split-button.js.map +1 -0
  128. package/dist/ui/split-button.mjs +278 -0
  129. package/dist/ui/split-button.mjs.map +1 -0
  130. package/dist/ui/table.js +1 -0
  131. package/dist/ui/table.mjs +1 -0
  132. package/package.json +1 -1
  133. package/src/design-system/data-table.ts +20 -0
  134. package/src/design-system/index.ts +2 -0
  135. package/src/design-system/split-button.ts +38 -0
  136. package/src/ui/data-table/data-table-base.tsx +701 -0
  137. package/src/ui/data-table/data-table.test.tsx +389 -0
  138. package/src/ui/data-table/data-table.tsx +11 -0
  139. package/src/ui/data-table/index.ts +24 -0
  140. package/src/ui/data-table/types.ts +121 -0
  141. package/src/ui/data-table/variants.ts +21 -0
  142. package/src/ui/dropdown/dropdown.tsx +7 -3
  143. package/src/ui/dropdown/types.ts +2 -2
  144. package/src/ui/split-button/index.ts +19 -0
  145. package/src/ui/split-button/split-button-base.tsx +232 -0
  146. package/src/ui/split-button/split-button.test.tsx +208 -0
  147. package/src/ui/split-button/split-button.tsx +9 -0
  148. package/src/ui/split-button/types.ts +46 -0
  149. package/src/ui/split-button/variants.ts +46 -0
  150. package/dist/chunk-3W2UUKWP.js +0 -19
  151. package/dist/chunk-A4IB3C23.mjs.map +0 -1
  152. package/dist/chunk-VA6SB6NN.js.map +0 -1
@@ -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 chunkVA6SB6NN_js = require('./chunk-VA6SB6NN.js');
4
- var classVarianceAuthority = require('class-variance-authority');
5
-
6
- var buttonVariants = classVarianceAuthority.cva(chunkVA6SB6NN_js.zuiButtonBase, {
7
- variants: {
8
- appearance: chunkVA6SB6NN_js.zuiButtonAppearances,
9
- size: chunkVA6SB6NN_js.zuiButtonSizes
10
- },
11
- defaultVariants: {
12
- appearance: "default",
13
- size: "md"
14
- }
15
- });
16
-
17
- exports.buttonVariants = buttonVariants;
18
- //# sourceMappingURL=chunk-3W2UUKWP.js.map
19
- //# sourceMappingURL=chunk-3W2UUKWP.js.map