beez-ui 2.3.1 → 2.4.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.
@@ -0,0 +1,210 @@
1
+ "use client";
2
+ import { c as _c } from "react/compiler-runtime";
3
+ /**
4
+ * Letter-by-letter label morph adapted from Arc (https://uiarc.dev), Copyright (c) 2026 Elia Kuratli, MIT License.
5
+ * The full license text ships in THIRD_PARTY_NOTICES.md. Internal: shared by `ActionButton` and `SplitButton`.
6
+ */
7
+ import { useState } from "react";
8
+ import { AnimatePresence, motion } from "motion/react";
9
+ import { MORPH_TEXT_GLYPH_OFFSET_PX, MORPH_TEXT_MAX_GLYPH_DELAY_S, MORPH_TEXT_MIN_SHARED_RUN } from "../constants/morph-text.js";
10
+ import { ARC_BLUR, ARC_DURATION, ARC_EASE, ARC_SPRING_MORPH, ARC_STAGGER } from "../motion/arc-tokens.js";
11
+ import { jsx as _jsx } from "react/jsx-runtime";
12
+ const restState = {
13
+ opacity: 1,
14
+ y: 0,
15
+ filter: "blur(0px)"
16
+ };
17
+ /** Letters rise in from a soft blur and lift away a little faster. */
18
+ const glyphEnter = {
19
+ opacity: 0,
20
+ y: MORPH_TEXT_GLYPH_OFFSET_PX.enter,
21
+ filter: `blur(${ARC_BLUR.soft}px)`
22
+ };
23
+ const glyphExit = {
24
+ opacity: 0,
25
+ y: MORPH_TEXT_GLYPH_OFFSET_PX.exit,
26
+ filter: `blur(${ARC_BLUR.subtle}px)`,
27
+ transition: {
28
+ duration: ARC_DURATION.fast,
29
+ ease: [...ARC_EASE.standard]
30
+ }
31
+ };
32
+ const reducedTransition = { duration: ARC_DURATION.instant };
33
+ const reducedEnter = {
34
+ ...restState,
35
+ opacity: 0
36
+ };
37
+ const reducedExit = {
38
+ opacity: 0,
39
+ transition: reducedTransition
40
+ };
41
+ /** Grapheme segmenter created once; absent on runtimes without `Intl.Segmenter`. */
42
+ const graphemeSegmenter = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) : undefined;
43
+ /**
44
+ * Splits text into user-perceived characters, so emoji sequences, flags and combining accents stay in one glyph.
45
+ * Falls back to code points when `Intl.Segmenter` is unavailable.
46
+ * @param text - Text to split.
47
+ * @returns Graphemes in reading order.
48
+ */
49
+ function splitGraphemes(text) {
50
+ if (!graphemeSegmenter) return [...text];
51
+ return Array.from(graphemeSegmenter.segment(text), (segment) => segment.segment);
52
+ }
53
+ /**
54
+ * Turns characters into glyphs of one generation.
55
+ * @param characters - Characters to wrap.
56
+ * @param generation - Label generation, part of every glyph id.
57
+ * @returns Glyphs in reading order.
58
+ */
59
+ function toGlyphs(characters, generation) {
60
+ return characters.map((character, order) => ({
61
+ id: `${generation}:${order}`,
62
+ character,
63
+ order
64
+ }));
65
+ }
66
+ /**
67
+ * Splits a label into glyphs, keeping the identity of the shared leading and trailing characters of the previous
68
+ * label, so `Copy page` → `Copied` only replaces the changed letters.
69
+ * @param text - Current label.
70
+ * @returns Glyphs of the label.
71
+ */
72
+ function useGlyphs(text) {
73
+ const $ = _c(10);
74
+ let t0;
75
+ if ($[0] !== text) {
76
+ t0 = () => ({
77
+ text,
78
+ generation: 0,
79
+ glyphs: toGlyphs(splitGraphemes(text), 0)
80
+ });
81
+ $[0] = text;
82
+ $[1] = t0;
83
+ } else {
84
+ t0 = $[1];
85
+ }
86
+ const [glyphState, setGlyphState] = useState(t0);
87
+ if (glyphState.text === text) {
88
+ return glyphState.glyphs;
89
+ }
90
+ let t1;
91
+ if ($[2] !== glyphState.text) {
92
+ t1 = splitGraphemes(glyphState.text);
93
+ $[2] = glyphState.text;
94
+ $[3] = t1;
95
+ } else {
96
+ t1 = $[3];
97
+ }
98
+ const previous = t1;
99
+ let generation;
100
+ let t2;
101
+ if ($[4] !== glyphState.generation || $[5] !== glyphState.glyphs || $[6] !== previous || $[7] !== text) {
102
+ const next = splitGraphemes(text);
103
+ let sharedStart = 0;
104
+ let sharedEnd = 0;
105
+ while (sharedStart < previous.length && sharedStart < next.length && previous[sharedStart] === next[sharedStart]) {
106
+ sharedStart++;
107
+ }
108
+ while (sharedEnd < previous.length - sharedStart && sharedEnd < next.length - sharedStart && previous[previous.length - 1 - sharedEnd] === next[next.length - 1 - sharedEnd]) {
109
+ sharedEnd++;
110
+ }
111
+ if (sharedStart < MORPH_TEXT_MIN_SHARED_RUN) {
112
+ sharedStart = 0;
113
+ }
114
+ if (sharedEnd < MORPH_TEXT_MIN_SHARED_RUN) {
115
+ sharedEnd = 0;
116
+ }
117
+ generation = glyphState.generation + 1;
118
+ t2 = [
119
+ ...glyphState.glyphs.slice(0, sharedStart),
120
+ ...toGlyphs(next.slice(sharedStart, next.length - sharedEnd), generation),
121
+ ...glyphState.glyphs.slice(glyphState.glyphs.length - sharedEnd)
122
+ ];
123
+ $[4] = glyphState.generation;
124
+ $[5] = glyphState.glyphs;
125
+ $[6] = previous;
126
+ $[7] = text;
127
+ $[8] = generation;
128
+ $[9] = t2;
129
+ } else {
130
+ generation = $[8];
131
+ t2 = $[9];
132
+ }
133
+ const glyphs = t2;
134
+ setGlyphState({
135
+ text,
136
+ generation,
137
+ glyphs
138
+ });
139
+ return glyphs;
140
+ }
141
+ /**
142
+ * Morphs one label into the next: kept letters glide into place and new ones rise in from a soft blur. The row
143
+ * keeps its natural width; callers spring the slot around it.
144
+ * @param props - Label text, whether the user prefers reduced motion and an optional ref to the row.
145
+ * @returns The row of animated letters. Callers hide it from assistive tech and name their control otherwise.
146
+ */
147
+ export function MorphText(t0) {
148
+ const $ = _c(12);
149
+ const { text, reduceMotion, ref } = t0;
150
+ const glyphs = useGlyphs(text);
151
+ let t1;
152
+ if ($[0] !== glyphs || $[1] !== reduceMotion || $[2] !== text) {
153
+ let t2;
154
+ if ($[4] !== reduceMotion || $[5] !== text) {
155
+ t2 = (glyph) => /* @__PURE__ */ _jsx(motion.span, {
156
+ className: "inline-block",
157
+ layout: reduceMotion ? false : "position",
158
+ layoutDependency: text,
159
+ initial: reduceMotion ? reducedEnter : glyphEnter,
160
+ animate: restState,
161
+ exit: reduceMotion ? reducedExit : glyphExit,
162
+ transition: reduceMotion ? reducedTransition : {
163
+ duration: ARC_DURATION.standard,
164
+ ease: [...ARC_EASE.enter],
165
+ delay: Math.min(glyph.order * ARC_STAGGER.char, MORPH_TEXT_MAX_GLYPH_DELAY_S),
166
+ layout: ARC_SPRING_MORPH
167
+ },
168
+ children: glyph.character
169
+ }, glyph.id);
170
+ $[4] = reduceMotion;
171
+ $[5] = text;
172
+ $[6] = t2;
173
+ } else {
174
+ t2 = $[6];
175
+ }
176
+ t1 = glyphs.map(t2);
177
+ $[0] = glyphs;
178
+ $[1] = reduceMotion;
179
+ $[2] = text;
180
+ $[3] = t1;
181
+ } else {
182
+ t1 = $[3];
183
+ }
184
+ let t2;
185
+ if ($[7] !== t1) {
186
+ t2 = /* @__PURE__ */ _jsx(AnimatePresence, {
187
+ mode: "popLayout",
188
+ initial: false,
189
+ children: t1
190
+ });
191
+ $[7] = t1;
192
+ $[8] = t2;
193
+ } else {
194
+ t2 = $[8];
195
+ }
196
+ let t3;
197
+ if ($[9] !== ref || $[10] !== t2) {
198
+ t3 = /* @__PURE__ */ _jsx("span", {
199
+ ref,
200
+ className: "relative inline-flex flex-none whitespace-pre",
201
+ children: t2
202
+ });
203
+ $[9] = ref;
204
+ $[10] = t2;
205
+ $[11] = t3;
206
+ } else {
207
+ t3 = $[11];
208
+ }
209
+ return t3;
210
+ }
@@ -0,0 +1,36 @@
1
+ import type { ReactNode } from "react";
2
+ import type { SplitButtonVariant } from "../constants/split-button.js";
3
+ export type { SplitButtonVariant } from "../constants/split-button.js";
4
+ /** One entry of the menu half. */
5
+ export type SplitButtonAction = {
6
+ label: string;
7
+ onSelect?: () => void;
8
+ disabled?: boolean;
9
+ /** Marks an action that deletes or discards, drawn in the danger color. */
10
+ destructive?: boolean;
11
+ icon?: ReactNode;
12
+ };
13
+ export type SplitButtonProps = {
14
+ /** Label of the main action and its accessible name; changing it morphs the letters in place. */
15
+ label: string;
16
+ /** Actions of the menu half, in order. */
17
+ actions: SplitButtonAction[];
18
+ /** Runs the main action. */
19
+ onClick?: () => void;
20
+ disabled?: boolean;
21
+ /** Icon before the label; swapping it for another element (such as Copy for Check) crossfades it. */
22
+ icon?: ReactNode;
23
+ /** `primary` for the main action of a surface, `secondary` for a quieter, smaller one. */
24
+ variant?: SplitButtonVariant;
25
+ /** Accessible name of the menu half; defaults to "Más acciones de {label}". */
26
+ menuLabel?: string;
27
+ className?: string;
28
+ };
29
+ /**
30
+ * A main action joined to a menu of related actions, such as Copy page with Copy as Markdown and Open in a new tab.
31
+ * The main half announces label changes politely; the menu half opens a `DropdownMenu` aligned to its end.
32
+ * @param props - Main action, its icon, the menu actions, the variant and accessible names.
33
+ * @returns The joined pair of buttons and their menu.
34
+ */
35
+ declare function SplitButton({ label, actions, onClick, disabled, icon, variant, menuLabel, className, }: SplitButtonProps): import("react").JSX.Element;
36
+ export { SplitButton };
@@ -0,0 +1,306 @@
1
+ "use client";
2
+ import { c as _c } from "react/compiler-runtime";
3
+ /**
4
+ * Arc's SplitButton (https://uiarc.dev, MIT License, see THIRD_PARTY_NOTICES.md) built from beez-ui's `Button` and
5
+ * `DropdownMenu`: a main action whose icon and label morph in place, joined to a menu of related actions. The menu
6
+ * half never scales on press, so the menu opens from a still anchor.
7
+ */
8
+ import { isValidElement, useRef } from "react";
9
+ import { AnimatePresence, motion } from "motion/react";
10
+ import { ChevronDown } from "lucide-react";
11
+ import { SPLIT_BUTTON_SIZE_BY_VARIANT } from "../constants/split-button.js";
12
+ import { usePrefersReducedMotion } from "../hooks/use-prefers-reduced-motion.js";
13
+ import { cn } from "../lib/utils.js";
14
+ import { ARC_ICON_ENTER as iconEnter, ARC_ICON_EXIT as iconExit, ARC_ICON_TRANSITION as iconTransition, ARC_REDUCED_ENTER as reducedEnter, ARC_REDUCED_EXIT as reducedExit, ARC_REDUCED_TRANSITION as reducedTransition, ARC_SWAP_REST as restState } from "../motion/arc-swap.js";
15
+ import { useMorphWidth } from "../motion/use-morph-width.js";
16
+ import { Button } from "./button.js";
17
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./dropdown-menu.js";
18
+ import { MorphText } from "./morph-text.js";
19
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
+ /**
21
+ * Names an icon element, so swapping Copy for Check morphs while a re-render of the same icon stays still.
22
+ * @param icon - Icon node.
23
+ * @returns Element name, the text of a text node, or an empty string.
24
+ */
25
+ function getIconKey(icon) {
26
+ if (!isValidElement(icon)) return icon == null || typeof icon === "boolean" ? "" : String(icon);
27
+ const elementType = icon.type;
28
+ return typeof elementType === "string" ? elementType : elementType?.displayName ?? elementType?.name ?? "icon";
29
+ }
30
+ /**
31
+ * Visible content of the main action: the icon and the morphing label, inside a slot whose width follows on a
32
+ * spring when either changes.
33
+ * @param props - Label, icon and whether the user prefers reduced motion.
34
+ * @returns Content hidden from assistive tech; the button names itself through `aria-label`.
35
+ */
36
+ function SplitButtonMainContent(t0) {
37
+ const $ = _c(17);
38
+ const { label, icon, reduceMotion } = t0;
39
+ const contentRef = useRef(null);
40
+ let t1;
41
+ if ($[0] !== icon) {
42
+ t1 = getIconKey(icon);
43
+ $[0] = icon;
44
+ $[1] = t1;
45
+ } else {
46
+ t1 = $[1];
47
+ }
48
+ const iconKey = t1;
49
+ const width = useMorphWidth(contentRef, `${iconKey}|${label}`, reduceMotion);
50
+ let t2;
51
+ if ($[2] !== width) {
52
+ t2 = { width };
53
+ $[2] = width;
54
+ $[3] = t2;
55
+ } else {
56
+ t2 = $[3];
57
+ }
58
+ let t3;
59
+ if ($[4] !== icon || $[5] !== iconKey || $[6] !== reduceMotion) {
60
+ t3 = icon ? /* @__PURE__ */ _jsx("span", {
61
+ className: "inline-grid flex-none place-items-center",
62
+ children: /* @__PURE__ */ _jsx(AnimatePresence, {
63
+ initial: false,
64
+ children: /* @__PURE__ */ _jsx(motion.span, {
65
+ className: "col-start-1 row-start-1 inline-flex items-center justify-center",
66
+ initial: reduceMotion ? reducedEnter : iconEnter,
67
+ animate: restState,
68
+ exit: reduceMotion ? reducedExit : iconExit,
69
+ transition: reduceMotion ? reducedTransition : iconTransition,
70
+ children: icon
71
+ }, iconKey)
72
+ })
73
+ }) : null;
74
+ $[4] = icon;
75
+ $[5] = iconKey;
76
+ $[6] = reduceMotion;
77
+ $[7] = t3;
78
+ } else {
79
+ t3 = $[7];
80
+ }
81
+ let t4;
82
+ if ($[8] !== label || $[9] !== reduceMotion) {
83
+ t4 = /* @__PURE__ */ _jsx(MorphText, {
84
+ text: label,
85
+ reduceMotion
86
+ });
87
+ $[8] = label;
88
+ $[9] = reduceMotion;
89
+ $[10] = t4;
90
+ } else {
91
+ t4 = $[10];
92
+ }
93
+ let t5;
94
+ if ($[11] !== t3 || $[12] !== t4) {
95
+ t5 = /* @__PURE__ */ _jsxs("span", {
96
+ ref: contentRef,
97
+ className: "inline-flex flex-none items-center gap-2 whitespace-nowrap",
98
+ children: [t3, t4]
99
+ });
100
+ $[11] = t3;
101
+ $[12] = t4;
102
+ $[13] = t5;
103
+ } else {
104
+ t5 = $[13];
105
+ }
106
+ let t6;
107
+ if ($[14] !== t2 || $[15] !== t5) {
108
+ t6 = /* @__PURE__ */ _jsx(motion.span, {
109
+ "data-slot": "split-button-label",
110
+ className: "inline-flex min-w-0 items-center data-morphing:[clip-path:inset(-50%_calc(var(--spacing)*-2))]",
111
+ style: t2,
112
+ "aria-hidden": "true",
113
+ children: t5
114
+ });
115
+ $[14] = t2;
116
+ $[15] = t5;
117
+ $[16] = t6;
118
+ } else {
119
+ t6 = $[16];
120
+ }
121
+ return t6;
122
+ }
123
+ /**
124
+ * A main action joined to a menu of related actions, such as Copy page with Copy as Markdown and Open in a new tab.
125
+ * The main half announces label changes politely; the menu half opens a `DropdownMenu` aligned to its end.
126
+ * @param props - Main action, its icon, the menu actions, the variant and accessible names.
127
+ * @returns The joined pair of buttons and their menu.
128
+ */
129
+ function SplitButton(t0) {
130
+ const $ = _c(39);
131
+ const { label, actions, onClick, disabled, icon, variant: t1, menuLabel, className } = t0;
132
+ const variant = t1 === undefined ? "primary" : t1;
133
+ const reduceMotion = usePrefersReducedMotion();
134
+ const size = SPLIT_BUTTON_SIZE_BY_VARIANT[variant];
135
+ let t2;
136
+ if ($[0] !== className) {
137
+ t2 = cn("inline-flex items-stretch", className);
138
+ $[0] = className;
139
+ $[1] = t2;
140
+ } else {
141
+ t2 = $[1];
142
+ }
143
+ let t3;
144
+ if ($[2] !== icon || $[3] !== label || $[4] !== reduceMotion) {
145
+ t3 = /* @__PURE__ */ _jsx(SplitButtonMainContent, {
146
+ label,
147
+ icon,
148
+ reduceMotion
149
+ });
150
+ $[2] = icon;
151
+ $[3] = label;
152
+ $[4] = reduceMotion;
153
+ $[5] = t3;
154
+ } else {
155
+ t3 = $[5];
156
+ }
157
+ let t4;
158
+ if ($[6] !== disabled || $[7] !== label || $[8] !== onClick || $[9] !== size || $[10] !== t3 || $[11] !== variant) {
159
+ t4 = /* @__PURE__ */ _jsx(Button, {
160
+ variant,
161
+ size,
162
+ "aria-label": label,
163
+ className: "rounded-r-none border-r-0",
164
+ disabled,
165
+ onClick,
166
+ children: t3
167
+ });
168
+ $[6] = disabled;
169
+ $[7] = label;
170
+ $[8] = onClick;
171
+ $[9] = size;
172
+ $[10] = t3;
173
+ $[11] = variant;
174
+ $[12] = t4;
175
+ } else {
176
+ t4 = $[12];
177
+ }
178
+ const t5 = menuLabel ?? `Más acciones de ${label}`;
179
+ const t6 = variant === "primary" ? "border-l-[color-mix(in_oklab,var(--background)_18%,var(--foreground))]" : "w-(--control-height-sm) border-l-border";
180
+ let t7;
181
+ if ($[13] !== t6) {
182
+ t7 = cn("w-(--control-height-md) rounded-l-none px-0 [&_svg]:transition-transform [&_svg]:duration-(--duration-fast) data-[state=open]:[&_svg]:rotate-180 motion-reduce:[&_svg]:transition-none", t6);
183
+ $[13] = t6;
184
+ $[14] = t7;
185
+ } else {
186
+ t7 = $[14];
187
+ }
188
+ let t8;
189
+ if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
190
+ t8 = /* @__PURE__ */ _jsx(ChevronDown, {
191
+ strokeWidth: 1.75,
192
+ "aria-hidden": "true"
193
+ });
194
+ $[15] = t8;
195
+ } else {
196
+ t8 = $[15];
197
+ }
198
+ let t9;
199
+ if ($[16] !== size || $[17] !== t5 || $[18] !== t7 || $[19] !== variant) {
200
+ t9 = /* @__PURE__ */ _jsx(Button, {
201
+ variant,
202
+ size,
203
+ "aria-label": t5,
204
+ className: t7,
205
+ children: t8
206
+ });
207
+ $[16] = size;
208
+ $[17] = t5;
209
+ $[18] = t7;
210
+ $[19] = variant;
211
+ $[20] = t9;
212
+ } else {
213
+ t9 = $[20];
214
+ }
215
+ let t10;
216
+ if ($[21] !== disabled || $[22] !== t9) {
217
+ t10 = /* @__PURE__ */ _jsx(DropdownMenuTrigger, {
218
+ asChild: true,
219
+ disabled,
220
+ children: t9
221
+ });
222
+ $[21] = disabled;
223
+ $[22] = t9;
224
+ $[23] = t10;
225
+ } else {
226
+ t10 = $[23];
227
+ }
228
+ let t11;
229
+ if ($[24] !== label) {
230
+ t11 = /* @__PURE__ */ _jsx("span", {
231
+ className: "sr-only",
232
+ "aria-live": "polite",
233
+ children: label
234
+ });
235
+ $[24] = label;
236
+ $[25] = t11;
237
+ } else {
238
+ t11 = $[25];
239
+ }
240
+ let t12;
241
+ if ($[26] !== t10 || $[27] !== t11 || $[28] !== t2 || $[29] !== t4 || $[30] !== variant) {
242
+ t12 = /* @__PURE__ */ _jsxs("div", {
243
+ role: "group",
244
+ "data-slot": "split-button",
245
+ "data-variant": variant,
246
+ className: t2,
247
+ children: [
248
+ t4,
249
+ t10,
250
+ t11
251
+ ]
252
+ });
253
+ $[26] = t10;
254
+ $[27] = t11;
255
+ $[28] = t2;
256
+ $[29] = t4;
257
+ $[30] = variant;
258
+ $[31] = t12;
259
+ } else {
260
+ t12 = $[31];
261
+ }
262
+ let t13;
263
+ if ($[32] !== actions) {
264
+ t13 = actions.map(_temp);
265
+ $[32] = actions;
266
+ $[33] = t13;
267
+ } else {
268
+ t13 = $[33];
269
+ }
270
+ let t14;
271
+ if ($[34] !== t13) {
272
+ t14 = /* @__PURE__ */ _jsx(DropdownMenuContent, {
273
+ align: "end",
274
+ sideOffset: 4,
275
+ collisionPadding: 12,
276
+ loop: true,
277
+ children: t13
278
+ });
279
+ $[34] = t13;
280
+ $[35] = t14;
281
+ } else {
282
+ t14 = $[35];
283
+ }
284
+ let t15;
285
+ if ($[36] !== t12 || $[37] !== t14) {
286
+ t15 = /* @__PURE__ */ _jsxs(DropdownMenu, { children: [t12, t14] });
287
+ $[36] = t12;
288
+ $[37] = t14;
289
+ $[38] = t15;
290
+ } else {
291
+ t15 = $[38];
292
+ }
293
+ return t15;
294
+ }
295
+ function _temp(action) {
296
+ return /* @__PURE__ */ _jsxs(DropdownMenuItem, {
297
+ variant: action.destructive ? "destructive" : "default",
298
+ disabled: action.disabled,
299
+ onSelect: action.onSelect,
300
+ children: [action.icon ? /* @__PURE__ */ _jsx("span", {
301
+ "aria-hidden": "true",
302
+ children: action.icon
303
+ }) : null, action.label]
304
+ }, action.label);
305
+ }
306
+ export { SplitButton };
@@ -0,0 +1,20 @@
1
+ /** Tuning of `ActionButton`: its async states, reset delay and the icon that follows each state. */
2
+ /** Phases an action goes through: ready to run, running, and finished successfully. */
3
+ export type ActionButtonState = "idle" | "pending" | "success";
4
+ /** Value of each phase, exposed on the button as `data-action-state`, apart from `data-state`, which marks popup triggers that never press. */
5
+ export declare const ACTION_BUTTON_STATE: {
6
+ readonly idle: "idle";
7
+ readonly pending: "pending";
8
+ readonly success: "success";
9
+ };
10
+ /** Time the success state stays on the button before it returns to idle on its own. */
11
+ export declare const ACTION_BUTTON_DEFAULT_RESET_AFTER_MS = 2400;
12
+ /** Side, in pixels, of the slot that swaps the arrow, the spinner and the check. */
13
+ export declare const ACTION_BUTTON_ICON_SIZE_PX = 17;
14
+ /** Horizontal offsets, in pixels, of the arrow returning from behind and leaving forward. */
15
+ export declare const ACTION_BUTTON_ARROW_OFFSET_PX: {
16
+ readonly enter: -6;
17
+ readonly exit: 8;
18
+ };
19
+ /** Delay, in seconds, before the success check starts drawing itself. */
20
+ export declare const ACTION_BUTTON_CHECK_DRAW_DELAY_S = 0.05;
@@ -0,0 +1,17 @@
1
+ /** Value of each phase, exposed on the button as `data-action-state`, apart from `data-state`, which marks popup triggers that never press. */
2
+ export const ACTION_BUTTON_STATE = {
3
+ idle: "idle",
4
+ pending: "pending",
5
+ success: "success"
6
+ };
7
+ /** Time the success state stays on the button before it returns to idle on its own. */
8
+ export const ACTION_BUTTON_DEFAULT_RESET_AFTER_MS = 2400;
9
+ /** Side, in pixels, of the slot that swaps the arrow, the spinner and the check. */
10
+ export const ACTION_BUTTON_ICON_SIZE_PX = 17;
11
+ /** Horizontal offsets, in pixels, of the arrow returning from behind and leaving forward. */
12
+ export const ACTION_BUTTON_ARROW_OFFSET_PX = {
13
+ enter: -6,
14
+ exit: 8
15
+ };
16
+ /** Delay, in seconds, before the success check starts drawing itself. */
17
+ export const ACTION_BUTTON_CHECK_DRAW_DELAY_S = .05;
@@ -0,0 +1,13 @@
1
+ /** Tuning of the internal letter-by-letter label morph shared by `ActionButton` and `SplitButton`. */
2
+ /** Longest entrance delay a single letter gets, so long labels still finish their morph quickly. */
3
+ export declare const MORPH_TEXT_MAX_GLYPH_DELAY_S = 0.1;
4
+ /**
5
+ * Shared leading or trailing runs shorter than this are replaced instead of kept, so a one-letter coincidence does
6
+ * not leave a lonely letter standing while the rest of the label morphs.
7
+ */
8
+ export declare const MORPH_TEXT_MIN_SHARED_RUN = 2;
9
+ /** Vertical offsets, in pixels, of letters rising in and lifting out of the label. */
10
+ export declare const MORPH_TEXT_GLYPH_OFFSET_PX: {
11
+ readonly enter: 5;
12
+ readonly exit: -4;
13
+ };
@@ -0,0 +1,13 @@
1
+ /** Tuning of the internal letter-by-letter label morph shared by `ActionButton` and `SplitButton`. */
2
+ /** Longest entrance delay a single letter gets, so long labels still finish their morph quickly. */
3
+ export const MORPH_TEXT_MAX_GLYPH_DELAY_S = .1;
4
+ /**
5
+ * Shared leading or trailing runs shorter than this are replaced instead of kept, so a one-letter coincidence does
6
+ * not leave a lonely letter standing while the rest of the label morphs.
7
+ */
8
+ export const MORPH_TEXT_MIN_SHARED_RUN = 2;
9
+ /** Vertical offsets, in pixels, of letters rising in and lifting out of the label. */
10
+ export const MORPH_TEXT_GLYPH_OFFSET_PX = {
11
+ enter: 5,
12
+ exit: -4
13
+ };
@@ -0,0 +1,7 @@
1
+ /** `primary` for the main action of a surface, `secondary` for a quieter, smaller one. */
2
+ export type SplitButtonVariant = "primary" | "secondary";
3
+ /** Height of both halves for each variant: the secondary split button is compact, as in Arc. */
4
+ export declare const SPLIT_BUTTON_SIZE_BY_VARIANT: {
5
+ readonly primary: "md";
6
+ readonly secondary: "sm";
7
+ };
@@ -0,0 +1,5 @@
1
+ /** Height of both halves for each variant: the secondary split button is compact, as in Arc. */
2
+ export const SPLIT_BUTTON_SIZE_BY_VARIANT = {
3
+ primary: "md",
4
+ secondary: "sm"
5
+ };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { useTheme } from "next-themes";
3
3
  export { Link, type LinkProps } from "./components/link.js";
4
4
  export { BeezUIProvider, type BeezUIProviderProps } from "./providers/native-provider.js";
5
5
  export type { BeezUIComponents, BeezImageProps, BeezLinkProps } from "./providers/beez-ui-provider.js";
6
+ export * from "./components/action-button.js";
6
7
  export * from "./components/alert-dialog.js";
7
8
  export * from "./components/avatar.js";
8
9
  export * from "./components/badge.js";
@@ -21,6 +22,7 @@ export * from "./components/separator.js";
21
22
  export * from "./components/sheet.js";
22
23
  export * from "./components/sidebar.js";
23
24
  export * from "./components/skeleton.js";
25
+ export * from "./components/split-button.js";
24
26
  export * from "./components/switch.js";
25
27
  export * from "./components/table.js";
26
28
  export * from "./components/tabs.js";
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  export { useTheme } from "next-themes";
3
3
  export { Link } from "./components/link.js";
4
4
  export { BeezUIProvider } from "./providers/native-provider.js";
5
+ export * from "./components/action-button.js";
5
6
  export * from "./components/alert-dialog.js";
6
7
  export * from "./components/avatar.js";
7
8
  export * from "./components/badge.js";
@@ -20,6 +21,7 @@ export * from "./components/separator.js";
20
21
  export * from "./components/sheet.js";
21
22
  export * from "./components/sidebar.js";
22
23
  export * from "./components/skeleton.js";
24
+ export * from "./components/split-button.js";
23
25
  export * from "./components/switch.js";
24
26
  export * from "./components/table.js";
25
27
  export * from "./components/tabs.js";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Swap presets adapted from Arc (https://uiarc.dev), Copyright (c) 2026 Elia Kuratli, MIT License.
3
+ * The full license text ships in THIRD_PARTY_NOTICES.md. Internal: shared by the Arc-derived buttons to swap icons
4
+ * and labels in place.
5
+ */
6
+ import type { TargetAndTransition, Transition } from "motion/react";
7
+ /** Resting pose every swapped element settles into. */
8
+ export declare const ARC_SWAP_REST: TargetAndTransition;
9
+ /** Outgoing elements leave faster than incoming ones arrive. */
10
+ export declare const ARC_SWAP_EXIT_TRANSITION: Transition;
11
+ /** An incoming icon grows out of a slight blur. */
12
+ export declare const ARC_ICON_ENTER: TargetAndTransition;
13
+ /** An outgoing icon shrinks back into the same blur. */
14
+ export declare const ARC_ICON_EXIT: TargetAndTransition;
15
+ /** Scale rides the spring; opacity and blur tween so blur never overshoots below zero. */
16
+ export declare const ARC_ICON_TRANSITION: Transition;
17
+ /** With reduced motion, every swap is an instant fade. */
18
+ export declare const ARC_REDUCED_TRANSITION: Transition;
19
+ /** Reduced-motion entrance: the resting pose, only faded out. */
20
+ export declare const ARC_REDUCED_ENTER: TargetAndTransition;
21
+ /** Reduced-motion exit: an instant fade. */
22
+ export declare const ARC_REDUCED_EXIT: TargetAndTransition;