bubble-toolbar 0.1.0 → 1.1.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
@@ -1,8 +1,51 @@
1
- // src/BubbleToolbar.tsx
2
- import { forwardRef as forwardRef2, useMemo } from "react";
1
+ // src/activity.ts
2
+ import {
3
+ createContext,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState
10
+ } from "react";
11
+ var BubbleActivityContext = createContext(null);
12
+ var useBubbleActivity = () => {
13
+ const activity = useContext(BubbleActivityContext);
14
+ if (!activity) {
15
+ throw new Error("useBubbleActivity must be used inside a BubbleGroup or BubbleToolbar.Group.");
16
+ }
17
+ return activity;
18
+ };
19
+ var useGroupActivity = (externalBusy) => {
20
+ const [pending, setPending] = useState(0);
21
+ const mounted = useRef(true);
22
+ useEffect(() => {
23
+ mounted.current = true;
24
+ return () => {
25
+ mounted.current = false;
26
+ };
27
+ }, []);
28
+ const run = useCallback(async (task) => {
29
+ if (mounted.current) {
30
+ setPending((count) => count + 1);
31
+ }
32
+ try {
33
+ return await task();
34
+ } finally {
35
+ if (mounted.current) {
36
+ setPending((count) => count - 1);
37
+ }
38
+ }
39
+ }, []);
40
+ const isBusy = externalBusy || pending > 0;
41
+ return useMemo(() => ({ isBusy, run }), [isBusy, run]);
42
+ };
43
+
44
+ // src/BubbleGroup.tsx
45
+ import { forwardRef } from "react";
3
46
 
4
47
  // src/context.ts
5
- import { createContext, useContext } from "react";
48
+ import { createContext as createContext2, useContext as useContext2 } from "react";
6
49
  var defaultContextValue = {
7
50
  size: "md",
8
51
  theme: "glass",
@@ -12,26 +55,21 @@ var defaultContextValue = {
12
55
  bordered: true,
13
56
  shadow: true
14
57
  };
15
- var BubbleToolbarContext = createContext(defaultContextValue);
58
+ var BubbleToolbarContext = createContext2(defaultContextValue);
16
59
  var useBubbleToolbar = () => {
17
- return useContext(BubbleToolbarContext);
60
+ return useContext2(BubbleToolbarContext);
18
61
  };
19
62
 
20
- // src/BubbleGroup.tsx
21
- import { forwardRef } from "react";
22
-
23
63
  // src/utils/children.ts
24
- import React from "react";
64
+ import { Children, Fragment, isValidElement } from "react";
25
65
  function getValidChildren(children) {
26
66
  const result = [];
27
- React.Children.forEach(children, (child) => {
67
+ Children.forEach(children, (child) => {
28
68
  if (child === null || child === void 0 || typeof child === "boolean") {
29
69
  return;
30
70
  }
31
- if (React.isValidElement(child) && child.type === React.Fragment) {
32
- const fragmentChildren = getValidChildren(
33
- child.props.children
34
- );
71
+ if (isValidElement(child) && child.type === Fragment) {
72
+ const fragmentChildren = getValidChildren(child.props.children);
35
73
  result.push(...fragmentChildren);
36
74
  } else {
37
75
  result.push(child);
@@ -42,13 +80,14 @@ function getValidChildren(children) {
42
80
 
43
81
  // src/utils/styles.ts
44
82
  function resolveSize(size) {
45
- if (typeof size === "number") return size;
83
+ if (typeof size === "number") {
84
+ return size;
85
+ }
46
86
  switch (size) {
47
87
  case "sm":
48
88
  return 40;
49
89
  case "lg":
50
90
  return 56;
51
- case "md":
52
91
  default:
53
92
  return 48;
54
93
  }
@@ -63,28 +102,27 @@ function getGlassStyles(theme, blurred, blurAmount, bordered, shadow) {
63
102
  let color;
64
103
  switch (theme) {
65
104
  case "dark":
66
- background = blurred ? "rgba(24, 24, 27, 0.72)" : "rgba(24, 24, 27, 0.95)";
67
- borderColor = "rgba(255, 255, 255, 0.12)";
68
- color = "#f4f4f5";
105
+ background = blurred ? "var(--bubble-dark-bg, rgba(24, 24, 27, 0.72))" : "var(--bubble-dark-bg-solid, rgba(24, 24, 27, 0.95))";
106
+ borderColor = "var(--bubble-dark-border, rgba(255, 255, 255, 0.12))";
107
+ color = "var(--bubble-dark-fg, #f4f4f5)";
69
108
  break;
70
109
  case "light":
71
- background = blurred ? "rgba(255, 255, 255, 0.75)" : "rgba(255, 255, 255, 0.95)";
72
- borderColor = "rgba(0, 0, 0, 0.08)";
73
- color = "#18181b";
110
+ background = blurred ? "var(--bubble-light-bg, rgba(255, 255, 255, 0.75))" : "var(--bubble-light-bg-solid, rgba(255, 255, 255, 0.95))";
111
+ borderColor = "var(--bubble-light-border, rgba(0, 0, 0, 0.08))";
112
+ color = "var(--bubble-light-fg, #18181b)";
74
113
  break;
75
114
  case "auto":
76
- background = "var(--bubble-bg, rgba(255, 255, 255, 0.75))";
77
- borderColor = "var(--bubble-border, rgba(0, 0, 0, 0.08))";
78
- color = "var(--bubble-fg, #18181b)";
115
+ background = "var(--bubble-bg, var(--bubble-light-bg, rgba(255, 255, 255, 0.75)))";
116
+ borderColor = "var(--bubble-border, var(--bubble-light-border, rgba(0, 0, 0, 0.08)))";
117
+ color = "var(--bubble-fg, var(--bubble-light-fg, #18181b))";
79
118
  break;
80
- case "glass":
81
119
  default:
82
- background = blurred ? "rgba(255, 255, 255, 0.65)" : "rgba(255, 255, 255, 0.92)";
83
- borderColor = "rgba(255, 255, 255, 0.4)";
84
- color = "#09090b";
120
+ background = blurred ? "var(--bubble-glass-bg, rgba(255, 255, 255, 0.65))" : "var(--bubble-glass-bg-solid, rgba(255, 255, 255, 0.92))";
121
+ borderColor = "var(--bubble-glass-border, rgba(255, 255, 255, 0.4))";
122
+ color = "var(--bubble-glass-fg, #09090b)";
85
123
  break;
86
124
  }
87
- const boxShadow = typeof shadow === "string" ? shadow : shadow ? "0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.06)" : "none";
125
+ const boxShadow = typeof shadow === "string" ? shadow : shadow ? "var(--bubble-glass-shadow, 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.06))" : "none";
88
126
  return {
89
127
  backgroundColor: background,
90
128
  backdropFilter: blurred ? `blur(${blurVal}) saturate(180%)` : void 0,
@@ -104,11 +142,17 @@ var BubbleGroup = forwardRef(
104
142
  shape: explicitShape,
105
143
  size: explicitSize,
106
144
  itemGap: explicitItemGap,
145
+ busy = false,
146
+ activityColors,
147
+ activityDuration,
107
148
  className = "",
108
149
  style,
109
150
  ...rest
110
151
  }, ref) => {
111
152
  const context = useBubbleToolbar();
153
+ const activity = useGroupActivity(Boolean(context.busy || busy));
154
+ const colors = activityColors ?? context.activityColors ?? ["#22d3ee", "#8b5cf6", "#ec4899"];
155
+ const duration = activityDuration ?? context.activityDuration ?? 2.4;
112
156
  const validChildren = getValidChildren(children);
113
157
  const count = validChildren.length;
114
158
  const computedShape = explicitShape || (count <= 1 ? "circle" : "pill");
@@ -123,6 +167,10 @@ var BubbleGroup = forwardRef(
123
167
  );
124
168
  const isCircle = computedShape === "circle";
125
169
  const groupStyles = {
170
+ "--bubble-activity-color-1": colors[0],
171
+ "--bubble-activity-color-2": colors[1],
172
+ "--bubble-activity-color-3": colors[2],
173
+ "--bubble-activity-duration": `${Number.isFinite(duration) && duration > 0 ? duration : 2.4}s`,
126
174
  ...glassStyles,
127
175
  height: `${effectiveSize}px`,
128
176
  boxSizing: "border-box",
@@ -151,9 +199,10 @@ var BubbleGroup = forwardRef(
151
199
  const combinedClassName = [
152
200
  "bubble-group",
153
201
  `bubble-group--${computedShape}`,
202
+ activity.isBusy && "bubble-group--busy",
154
203
  className
155
204
  ].filter(Boolean).join(" ");
156
- return /* @__PURE__ */ jsx(
205
+ return /* @__PURE__ */ jsx(BubbleActivityContext.Provider, { value: activity, children: /* @__PURE__ */ jsx(
157
206
  "div",
158
207
  {
159
208
  ref,
@@ -163,151 +212,145 @@ var BubbleGroup = forwardRef(
163
212
  "data-shape": computedShape,
164
213
  "data-items-count": count,
165
214
  ...rest,
215
+ "aria-busy": activity.isBusy,
166
216
  children: validChildren
167
217
  }
168
- );
218
+ ) });
169
219
  }
170
220
  );
171
221
  BubbleGroup.displayName = "BubbleGroup";
172
222
 
173
223
  // src/BubbleToolbar.tsx
174
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
175
- var BubbleToolbar = forwardRef2(
176
- ({
177
- groups,
178
- children,
179
- position = "bottom",
180
- theme = "glass",
181
- size = "md",
182
- blurred = true,
183
- blurAmount = 16,
184
- groupGap = 8,
185
- itemGap = 8,
186
- bordered = true,
187
- shadow = true,
188
- className = "",
189
- style,
190
- ariaLabel = "Toolbar",
191
- ...rest
192
- }, ref) => {
193
- const contextValue = useMemo(
194
- () => ({
195
- size,
196
- theme,
197
- blurred,
198
- blurAmount,
199
- itemGap,
200
- bordered,
201
- shadow
202
- }),
203
- [size, theme, blurred, blurAmount, itemGap, bordered, shadow]
204
- );
205
- const getPositionStyles = () => {
206
- switch (position) {
207
- case "bottom":
208
- return {
209
- position: "fixed",
210
- bottom: "24px",
211
- left: "50%",
212
- transform: "translateX(-50%)",
213
- zIndex: 1e3
214
- };
215
- case "top":
216
- return {
217
- position: "fixed",
218
- top: "24px",
219
- left: "50%",
220
- transform: "translateX(-50%)",
221
- zIndex: 1e3
222
- };
223
- case "static":
224
- return {
225
- position: "relative"
226
- };
227
- case "custom":
228
- default:
229
- return {};
230
- }
231
- };
232
- const containerStyles = {
233
- ...getPositionStyles(),
234
- display: "inline-flex",
235
- alignItems: "center",
236
- gap: formatPx(groupGap),
237
- maxWidth: "calc(100vw - 32px)",
238
- overflowX: "auto",
239
- overflowY: "hidden",
240
- padding: "4px",
241
- boxSizing: "border-box",
242
- scrollbarWidth: "none",
243
- // Firefox
244
- msOverflowStyle: "none",
245
- // IE/Edge
246
- WebkitOverflowScrolling: "touch",
247
- pointerEvents: "auto",
248
- ...style
249
- };
250
- const combinedClassName = [
251
- "bubble-toolbar",
252
- `bubble-toolbar--${position}`,
253
- `bubble-toolbar--${theme}`,
254
- className
255
- ].filter(Boolean).join(" ");
256
- return /* @__PURE__ */ jsx2(BubbleToolbarContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs(
257
- "div",
258
- {
259
- ref,
260
- role: "toolbar",
261
- "aria-label": ariaLabel,
262
- className: combinedClassName,
263
- style: containerStyles,
264
- ...rest,
265
- children: [
266
- groups && groups.length > 0 ? groups.map((groupItems, index) => /* @__PURE__ */ jsx2(BubbleGroup, { children: groupItems }, index)) : null,
267
- children
268
- ]
269
- }
270
- ) });
271
- }
272
- );
273
- BubbleToolbar.displayName = "BubbleToolbar";
274
- BubbleToolbar.Group = BubbleGroup;
224
+ import { forwardRef as forwardRef6, useCallback as useCallback4, useMemo as useMemo3, useRef as useRef5 } from "react";
225
+
226
+ // src/live/BubbleLiveActivity.tsx
227
+ import { useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useState as useState4, useSyncExternalStore } from "react";
275
228
 
276
229
  // src/components/BubbleIconButton.tsx
277
- import { forwardRef as forwardRef3, useState } from "react";
278
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
230
+ import { forwardRef as forwardRef2, useState as useState2 } from "react";
231
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
279
232
  var ICONS = {
280
- search: /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
281
- /* @__PURE__ */ jsx3("circle", { cx: "11", cy: "11", r: "8" }),
282
- /* @__PURE__ */ jsx3("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })
283
- ] }),
284
- theme: /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
285
- /* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "5" }),
286
- /* @__PURE__ */ jsx3("line", { x1: "12", y1: "1", x2: "12", y2: "3" }),
287
- /* @__PURE__ */ jsx3("line", { x1: "12", y1: "21", x2: "12", y2: "23" }),
288
- /* @__PURE__ */ jsx3("line", { x1: "4.22", y1: "4.22", x2: "5.64", y2: "5.64" }),
289
- /* @__PURE__ */ jsx3("line", { x1: "18.36", y1: "18.36", x2: "19.78", y2: "19.78" }),
290
- /* @__PURE__ */ jsx3("line", { x1: "1", y1: "12", x2: "3", y2: "12" }),
291
- /* @__PURE__ */ jsx3("line", { x1: "21", y1: "12", x2: "23", y2: "12" }),
292
- /* @__PURE__ */ jsx3("line", { x1: "4.22", y1: "19.78", x2: "5.64", y2: "18.36" }),
293
- /* @__PURE__ */ jsx3("line", { x1: "18.36", y1: "5.64", x2: "19.78", y2: "4.22" })
294
- ] }),
295
- chat: /* @__PURE__ */ jsx3("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx3("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) }),
296
- notifications: /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
297
- /* @__PURE__ */ jsx3("path", { d: "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" }),
298
- /* @__PURE__ */ jsx3("path", { d: "M13.73 21a2 2 0 0 1-3.46 0" })
299
- ] }),
300
- logo: /* @__PURE__ */ jsxs2("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
301
- /* @__PURE__ */ jsx3("polygon", { points: "12 2 2 7 12 12 22 7 12 2" }),
302
- /* @__PURE__ */ jsx3("polyline", { points: "2 17 12 22 22 17" }),
303
- /* @__PURE__ */ jsx3("polyline", { points: "2 12 12 17 22 12" })
304
- ] }),
305
- close: /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
306
- /* @__PURE__ */ jsx3("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
307
- /* @__PURE__ */ jsx3("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
308
- ] })
233
+ search: /* @__PURE__ */ jsxs(
234
+ "svg",
235
+ {
236
+ "aria-hidden": "true",
237
+ focusable: "false",
238
+ width: "18",
239
+ height: "18",
240
+ viewBox: "0 0 24 24",
241
+ fill: "none",
242
+ stroke: "currentColor",
243
+ strokeWidth: "2",
244
+ strokeLinecap: "round",
245
+ strokeLinejoin: "round",
246
+ children: [
247
+ /* @__PURE__ */ jsx2("circle", { cx: "11", cy: "11", r: "8" }),
248
+ /* @__PURE__ */ jsx2("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })
249
+ ]
250
+ }
251
+ ),
252
+ theme: /* @__PURE__ */ jsxs(
253
+ "svg",
254
+ {
255
+ "aria-hidden": "true",
256
+ focusable: "false",
257
+ width: "18",
258
+ height: "18",
259
+ viewBox: "0 0 24 24",
260
+ fill: "none",
261
+ stroke: "currentColor",
262
+ strokeWidth: "2",
263
+ strokeLinecap: "round",
264
+ strokeLinejoin: "round",
265
+ children: [
266
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "5" }),
267
+ /* @__PURE__ */ jsx2("line", { x1: "12", y1: "1", x2: "12", y2: "3" }),
268
+ /* @__PURE__ */ jsx2("line", { x1: "12", y1: "21", x2: "12", y2: "23" }),
269
+ /* @__PURE__ */ jsx2("line", { x1: "4.22", y1: "4.22", x2: "5.64", y2: "5.64" }),
270
+ /* @__PURE__ */ jsx2("line", { x1: "18.36", y1: "18.36", x2: "19.78", y2: "19.78" }),
271
+ /* @__PURE__ */ jsx2("line", { x1: "1", y1: "12", x2: "3", y2: "12" }),
272
+ /* @__PURE__ */ jsx2("line", { x1: "21", y1: "12", x2: "23", y2: "12" }),
273
+ /* @__PURE__ */ jsx2("line", { x1: "4.22", y1: "19.78", x2: "5.64", y2: "18.36" }),
274
+ /* @__PURE__ */ jsx2("line", { x1: "18.36", y1: "5.64", x2: "19.78", y2: "4.22" })
275
+ ]
276
+ }
277
+ ),
278
+ chat: /* @__PURE__ */ jsx2(
279
+ "svg",
280
+ {
281
+ "aria-hidden": "true",
282
+ focusable: "false",
283
+ width: "18",
284
+ height: "18",
285
+ viewBox: "0 0 24 24",
286
+ fill: "none",
287
+ stroke: "currentColor",
288
+ strokeWidth: "2",
289
+ strokeLinecap: "round",
290
+ strokeLinejoin: "round",
291
+ children: /* @__PURE__ */ jsx2("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
292
+ }
293
+ ),
294
+ notifications: /* @__PURE__ */ jsxs(
295
+ "svg",
296
+ {
297
+ "aria-hidden": "true",
298
+ focusable: "false",
299
+ width: "18",
300
+ height: "18",
301
+ viewBox: "0 0 24 24",
302
+ fill: "none",
303
+ stroke: "currentColor",
304
+ strokeWidth: "2",
305
+ strokeLinecap: "round",
306
+ strokeLinejoin: "round",
307
+ children: [
308
+ /* @__PURE__ */ jsx2("path", { d: "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" }),
309
+ /* @__PURE__ */ jsx2("path", { d: "M13.73 21a2 2 0 0 1-3.46 0" })
310
+ ]
311
+ }
312
+ ),
313
+ logo: /* @__PURE__ */ jsxs(
314
+ "svg",
315
+ {
316
+ "aria-hidden": "true",
317
+ focusable: "false",
318
+ width: "20",
319
+ height: "20",
320
+ viewBox: "0 0 24 24",
321
+ fill: "none",
322
+ stroke: "currentColor",
323
+ strokeWidth: "2",
324
+ strokeLinecap: "round",
325
+ strokeLinejoin: "round",
326
+ children: [
327
+ /* @__PURE__ */ jsx2("polygon", { points: "12 2 2 7 12 12 22 7 12 2" }),
328
+ /* @__PURE__ */ jsx2("polyline", { points: "2 17 12 22 22 17" }),
329
+ /* @__PURE__ */ jsx2("polyline", { points: "2 12 12 17 22 12" })
330
+ ]
331
+ }
332
+ ),
333
+ close: /* @__PURE__ */ jsxs(
334
+ "svg",
335
+ {
336
+ "aria-hidden": "true",
337
+ focusable: "false",
338
+ width: "18",
339
+ height: "18",
340
+ viewBox: "0 0 24 24",
341
+ fill: "none",
342
+ stroke: "currentColor",
343
+ strokeWidth: "2",
344
+ strokeLinecap: "round",
345
+ strokeLinejoin: "round",
346
+ children: [
347
+ /* @__PURE__ */ jsx2("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
348
+ /* @__PURE__ */ jsx2("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
349
+ ]
350
+ }
351
+ )
309
352
  };
310
- var BubbleIconButton = forwardRef3(
353
+ var BubbleIconButton = forwardRef2(
311
354
  ({
312
355
  name,
313
356
  icon,
@@ -322,7 +365,7 @@ var BubbleIconButton = forwardRef3(
322
365
  ariaLabel,
323
366
  ...rest
324
367
  }, ref) => {
325
- const [isHovered, setIsHovered] = useState(false);
368
+ const [isHovered, setIsHovered] = useState2(false);
326
369
  const iconContent = icon || (name ? ICONS[name] : null) || children;
327
370
  const computedAriaLabel = ariaLabel || tooltip || (name ? `${name} button` : "button");
328
371
  const buttonStyles = {
@@ -334,12 +377,11 @@ var BubbleIconButton = forwardRef3(
334
377
  height: "36px",
335
378
  borderRadius: "50%",
336
379
  border: "none",
337
- background: active ? "rgba(0, 0, 0, 0.08)" : isHovered ? "rgba(0, 0, 0, 0.05)" : "transparent",
380
+ background: active ? "var(--bubble-button-active-bg, rgba(0, 0, 0, 0.08))" : isHovered ? "var(--bubble-button-hover-bg, rgba(0, 0, 0, 0.05))" : "transparent",
338
381
  color: "inherit",
339
382
  cursor: "pointer",
340
383
  padding: 0,
341
384
  margin: 0,
342
- outline: "none",
343
385
  transition: "background-color 0.15s ease, transform 0.15s ease",
344
386
  transform: isHovered ? "scale(1.06)" : "scale(1)",
345
387
  flexShrink: 0,
@@ -363,7 +405,7 @@ var BubbleIconButton = forwardRef3(
363
405
  justifyContent: "center",
364
406
  boxShadow: "0 0 0 1.5px rgba(255, 255, 255, 0.9)"
365
407
  };
366
- return /* @__PURE__ */ jsxs2(
408
+ return /* @__PURE__ */ jsxs(
367
409
  "button",
368
410
  {
369
411
  ref,
@@ -383,7 +425,7 @@ var BubbleIconButton = forwardRef3(
383
425
  ...rest,
384
426
  children: [
385
427
  iconContent,
386
- badge ? /* @__PURE__ */ jsx3("span", { className: "bubble-icon-button__badge", style: badgeStyles, children: typeof badge !== "boolean" ? badge : null }) : null
428
+ badge ? /* @__PURE__ */ jsx2("span", { className: "bubble-icon-button__badge", style: badgeStyles, children: typeof badge !== "boolean" ? badge : null }) : null
387
429
  ]
388
430
  }
389
431
  );
@@ -391,18 +433,534 @@ var BubbleIconButton = forwardRef3(
391
433
  );
392
434
  BubbleIconButton.displayName = "BubbleIconButton";
393
435
 
394
- // src/components/BubbleArea.tsx
436
+ // src/utils/tokens.ts
437
+ var createTokenStyle = (map) => (tokens) => {
438
+ const style = {};
439
+ if (!tokens) {
440
+ return style;
441
+ }
442
+ for (const [key, value] of Object.entries(tokens)) {
443
+ if (!Object.hasOwn(map, key) || value === void 0 || value === null) {
444
+ continue;
445
+ }
446
+ const [name, unit] = map[key];
447
+ style[name] = typeof value === "number" && unit ? `${value}${unit}` : String(value);
448
+ }
449
+ return style;
450
+ };
451
+
452
+ // src/live/useBubbleLiveActivity.ts
453
+ import { useContext as useContext3, useState as useState3 } from "react";
454
+
455
+ // src/live/controller.ts
456
+ var IDLE = {
457
+ state: "idle",
458
+ id: 0,
459
+ outcome: "neutral",
460
+ duration: 0,
461
+ paused: false,
462
+ queued: 0
463
+ };
464
+ var isDevelopment = () => globalThis.process?.env?.NODE_ENV !== "production";
465
+ var createLiveController = (initialOptions = {}) => {
466
+ const listeners = /* @__PURE__ */ new Set();
467
+ const queue = [];
468
+ let snapshot = IDLE;
469
+ let current = null;
470
+ let connections = 0;
471
+ let warned = false;
472
+ let compactSince = 0;
473
+ let expandTimer;
474
+ let closeTimer;
475
+ let remaining = 0;
476
+ let closeStartedAt = 0;
477
+ const update = (next2) => {
478
+ snapshot = { ...snapshot, ...next2, queued: queue.length };
479
+ for (const listener of listeners) {
480
+ listener();
481
+ }
482
+ };
483
+ const clearTimers = () => {
484
+ clearTimeout(expandTimer);
485
+ clearTimeout(closeTimer);
486
+ expandTimer = void 0;
487
+ closeTimer = void 0;
488
+ };
489
+ const startCloseTimer = (ms) => {
490
+ remaining = ms;
491
+ closeStartedAt = Date.now();
492
+ closeTimer = setTimeout(controller.dismiss, ms);
493
+ };
494
+ const expand = (job) => {
495
+ const { settled, options } = job;
496
+ let { content } = job;
497
+ let outcome = "neutral";
498
+ if (settled?.ok && !job.content) {
499
+ outcome = "success";
500
+ content = options.success?.(settled.value);
501
+ } else if (settled && !settled.ok) {
502
+ outcome = "error";
503
+ content = options.error?.(settled.error);
504
+ }
505
+ const duration = Math.max(0, options.duration ?? controller.options.duration ?? 4e3);
506
+ update({ state: "expanded", content, outcome, duration, paused: false });
507
+ if (duration > 0) {
508
+ startCloseTimer(duration);
509
+ }
510
+ };
511
+ const next = () => {
512
+ clearTimers();
513
+ current = queue.shift() ?? null;
514
+ if (!current) {
515
+ update({ ...IDLE, id: snapshot.id });
516
+ return;
517
+ }
518
+ if (current.settled) {
519
+ update({ id: snapshot.id + 1 });
520
+ expand(current);
521
+ return;
522
+ }
523
+ compactSince = Date.now();
524
+ update({
525
+ state: "compact",
526
+ id: snapshot.id + 1,
527
+ content: current.options.pending,
528
+ outcome: "pending",
529
+ duration: 0,
530
+ paused: false
531
+ });
532
+ };
533
+ const enqueue = (job) => {
534
+ queue.push(job);
535
+ if (!current) {
536
+ next();
537
+ } else {
538
+ update({});
539
+ }
540
+ };
541
+ const onSettled = (job) => {
542
+ if (job !== current || snapshot.state !== "compact") {
543
+ return;
544
+ }
545
+ const minPending = controller.options.minPendingMs ?? 600;
546
+ const wait = Math.max(0, minPending - (Date.now() - compactSince));
547
+ expandTimer = setTimeout(() => expand(job), wait);
548
+ };
549
+ const controller = {
550
+ options: initialOptions,
551
+ run(task, options = {}) {
552
+ if (connections === 0 && !warned && isDevelopment()) {
553
+ warned = true;
554
+ console.warn(
555
+ "useBubbleLiveActivity: the controller is not passed to any <BubbleToolbar liveActivity>; activities will not be shown."
556
+ );
557
+ }
558
+ let promise;
559
+ try {
560
+ promise = Promise.resolve(task());
561
+ } catch (error) {
562
+ promise = Promise.reject(error);
563
+ }
564
+ const job = { options, settled: null };
565
+ void (async () => {
566
+ try {
567
+ job.settled = { ok: true, value: await promise };
568
+ } catch (error) {
569
+ job.settled = { ok: false, error };
570
+ }
571
+ onSettled(job);
572
+ })();
573
+ enqueue(job);
574
+ return promise;
575
+ },
576
+ show(content, options = {}) {
577
+ enqueue({ options, settled: { ok: true, value: void 0 }, content });
578
+ },
579
+ dismiss() {
580
+ if (current) {
581
+ next();
582
+ }
583
+ },
584
+ get state() {
585
+ return snapshot.state;
586
+ },
587
+ subscribe(listener) {
588
+ listeners.add(listener);
589
+ return () => {
590
+ listeners.delete(listener);
591
+ };
592
+ },
593
+ getSnapshot: () => snapshot,
594
+ connect() {
595
+ connections += 1;
596
+ return () => {
597
+ connections -= 1;
598
+ };
599
+ },
600
+ pause() {
601
+ if (snapshot.state !== "expanded" || snapshot.paused || !closeTimer) {
602
+ return;
603
+ }
604
+ clearTimeout(closeTimer);
605
+ closeTimer = void 0;
606
+ remaining = Math.max(0, remaining - (Date.now() - closeStartedAt));
607
+ update({ paused: true });
608
+ },
609
+ resume() {
610
+ if (snapshot.state !== "expanded" || !snapshot.paused) {
611
+ return;
612
+ }
613
+ update({ paused: false });
614
+ startCloseTimer(remaining);
615
+ }
616
+ };
617
+ return controller;
618
+ };
619
+
620
+ // src/live/useBubbleLiveActivity.ts
621
+ var useBubbleLiveActivity = (options = {}) => {
622
+ const [controller] = useState3(() => createLiveController(options));
623
+ controller.options = options;
624
+ return controller;
625
+ };
626
+ var useBubbleToolbarLiveActivity = () => {
627
+ const controller = useContext3(BubbleToolbarContext).liveActivity;
628
+ if (!controller) {
629
+ throw new Error(
630
+ "useBubbleToolbarLiveActivity must be used inside a <BubbleToolbar liveActivity={\u2026}>."
631
+ );
632
+ }
633
+ return controller;
634
+ };
635
+ var asInternals = (controller) => controller;
636
+
637
+ // src/live/BubbleLiveActivity.tsx
638
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
639
+ var LIVE_TOKENS = {
640
+ compactWidth: ["--bubble-live-compact-width", "px"],
641
+ width: ["--bubble-live-width", "px"],
642
+ radius: ["--bubble-live-radius", "px"],
643
+ progressColor: ["--bubble-live-progress-color", ""],
644
+ successColor: ["--bubble-live-success-color", ""],
645
+ errorColor: ["--bubble-live-error-color", ""],
646
+ titleSize: ["--bubble-live-title-size", "px"],
647
+ metaSize: ["--bubble-live-meta-size", "px"],
648
+ duration: ["--bubble-live-duration", "ms"]
649
+ };
650
+ var liveTokensToStyle = createTokenStyle(LIVE_TOKENS);
651
+ var defaultLiveLabels = {
652
+ pending: "Working\u2026",
653
+ success: "Done",
654
+ error: "Something went wrong",
655
+ close: "Dismiss",
656
+ group: "Live activity"
657
+ };
658
+ var LEAVE_FALLBACK_MS = 650;
659
+ var MORPH = "clamp(0ms, var(--bubble-live-duration, 320ms), 600ms) cubic-bezier(0.16, 1, 0.3, 1)";
660
+ var VISUALLY_HIDDEN = {
661
+ position: "absolute",
662
+ width: 1,
663
+ height: 1,
664
+ margin: -1,
665
+ padding: 0,
666
+ overflow: "hidden",
667
+ clip: "rect(0 0 0 0)",
668
+ whiteSpace: "nowrap",
669
+ border: 0
670
+ };
671
+ var svg = (children, className) => /* @__PURE__ */ jsx3(
672
+ "svg",
673
+ {
674
+ "aria-hidden": "true",
675
+ focusable: "false",
676
+ className,
677
+ width: "18",
678
+ height: "18",
679
+ viewBox: "0 0 24 24",
680
+ fill: "none",
681
+ stroke: "currentColor",
682
+ strokeWidth: "2",
683
+ strokeLinecap: "round",
684
+ strokeLinejoin: "round",
685
+ children
686
+ }
687
+ );
688
+ var ICONS2 = {
689
+ pending: svg(/* @__PURE__ */ jsx3("path", { d: "M21 12a9 9 0 1 1-6.22-8.56" }), "bubble-live__spinner"),
690
+ success: svg(/* @__PURE__ */ jsx3("polyline", { points: "20 6 9 17 4 12" })),
691
+ error: svg(
692
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
693
+ /* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "10" }),
694
+ /* @__PURE__ */ jsx3("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
695
+ /* @__PURE__ */ jsx3("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
696
+ ] })
697
+ ),
698
+ neutral: null
699
+ };
700
+ var prefersReducedMotion = () => typeof window !== "undefined" && Boolean(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches);
701
+ var LiveSlot = ({ controller, toolbarRef, children }) => {
702
+ const live = asInternals(controller);
703
+ const context = useBubbleToolbar();
704
+ const snapshot = useSyncExternalStore(live.subscribe, live.getSnapshot, live.getSnapshot);
705
+ const [visual, setVisual] = useState4("idle");
706
+ const fromWidth = useRef2(0);
707
+ const savedFocus = useRef2(null);
708
+ const groupsRef = useRef2(null);
709
+ const liveRef = useRef2(null);
710
+ const interaction = useRef2({ hover: false, focus: false });
711
+ const active = snapshot.state !== "idle";
712
+ const labels = { ...defaultLiveLabels, ...live.options.labels };
713
+ useEffect2(() => live.connect(), [live]);
714
+ useLayoutEffect(() => {
715
+ if (active && (visual === "idle" || visual === "leave")) {
716
+ if (visual === "idle") {
717
+ const toolbar = toolbarRef.current;
718
+ fromWidth.current = toolbar ? Math.max(0, toolbar.getBoundingClientRect().width - 8) : 0;
719
+ const focused = document.activeElement;
720
+ savedFocus.current = focused instanceof HTMLElement && groupsRef.current?.contains(focused) ? focused : null;
721
+ }
722
+ setVisual(prefersReducedMotion() || !fromWidth.current ? "live" : "enter");
723
+ } else if (!active && (visual === "enter" || visual === "live")) {
724
+ setVisual(prefersReducedMotion() || !fromWidth.current ? "idle" : "leave");
725
+ }
726
+ }, [active, visual, toolbarRef]);
727
+ useLayoutEffect(() => {
728
+ if (groupsRef.current) {
729
+ groupsRef.current.inert = visual !== "idle";
730
+ }
731
+ }, [visual]);
732
+ useEffect2(() => {
733
+ if (visual !== "enter") {
734
+ return;
735
+ }
736
+ let second = 0;
737
+ const first = requestAnimationFrame(() => {
738
+ second = requestAnimationFrame(() => setVisual("live"));
739
+ });
740
+ return () => {
741
+ cancelAnimationFrame(first);
742
+ cancelAnimationFrame(second);
743
+ };
744
+ }, [visual]);
745
+ useEffect2(() => {
746
+ if (visual !== "leave") {
747
+ return;
748
+ }
749
+ const timer = setTimeout(() => setVisual("idle"), LEAVE_FALLBACK_MS);
750
+ return () => clearTimeout(timer);
751
+ }, [visual]);
752
+ useEffect2(() => {
753
+ if (visual === "idle") {
754
+ interaction.current = { hover: false, focus: false };
755
+ if (savedFocus.current?.isConnected) {
756
+ savedFocus.current.focus();
757
+ }
758
+ savedFocus.current = null;
759
+ } else if (savedFocus.current && !liveRef.current?.contains(document.activeElement)) {
760
+ liveRef.current?.focus();
761
+ }
762
+ }, [visual]);
763
+ const syncPause = () => {
764
+ const { hover, focus } = interaction.current;
765
+ if (hover || focus) {
766
+ live.pause();
767
+ } else {
768
+ live.resume();
769
+ }
770
+ };
771
+ const size = resolveSize(context.size);
772
+ const expanded = snapshot.state === "expanded" && visual === "live";
773
+ const { outcome } = snapshot;
774
+ const tone = snapshot.content?.tone ?? (outcome === "pending" ? "neutral" : outcome);
775
+ const title = snapshot.content?.title ?? (outcome === "pending" ? labels.pending : outcome === "error" ? labels.error : outcome === "success" ? labels.success : "");
776
+ const description = snapshot.content?.description;
777
+ const icon = snapshot.content?.icon ?? ICONS2[outcome === "pending" ? "pending" : tone];
778
+ const announcement = active ? [title, expanded ? description : ""].filter(Boolean).join(". ") : "";
779
+ const width = visual === "enter" || visual === "leave" ? `${fromWidth.current}px` : snapshot.state === "expanded" ? "min(var(--bubble-live-width, 360px), calc(100vw - 32px))" : "min(var(--bubble-live-compact-width, 200px), calc(100vw - 32px))";
780
+ const groupStyle = {
781
+ ...liveTokensToStyle(live.options.tokens),
782
+ width,
783
+ minWidth: `${size}px`,
784
+ height: snapshot.state === "expanded" ? "auto" : `${size}px`,
785
+ minHeight: `${size}px`,
786
+ padding: 0,
787
+ gap: 0,
788
+ flexDirection: "column",
789
+ alignItems: "stretch",
790
+ justifyContent: "center",
791
+ position: "relative",
792
+ overflow: "hidden",
793
+ borderRadius: snapshot.state === "expanded" ? "var(--bubble-live-radius, 24px)" : "9999px",
794
+ transition: `width ${MORPH}, border-radius ${MORPH}`
795
+ };
796
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
797
+ /* @__PURE__ */ jsx3("div", { ref: groupsRef, style: { display: visual === "idle" ? "contents" : "none" }, children }),
798
+ visual === "idle" ? null : /* @__PURE__ */ jsxs2(
799
+ BubbleGroup,
800
+ {
801
+ ref: liveRef,
802
+ className: "bubble-live",
803
+ shape: "pill",
804
+ busy: snapshot.state === "compact",
805
+ "aria-label": labels.group,
806
+ "data-state": visual === "leave" ? "leaving" : snapshot.state,
807
+ "data-tone": tone,
808
+ tabIndex: -1,
809
+ style: groupStyle,
810
+ onTransitionEnd: (event) => {
811
+ if (visual === "leave" && event.target === liveRef.current && event.propertyName === "width") {
812
+ setVisual("idle");
813
+ }
814
+ },
815
+ onMouseEnter: () => {
816
+ interaction.current.hover = true;
817
+ syncPause();
818
+ },
819
+ onMouseLeave: () => {
820
+ interaction.current.hover = false;
821
+ syncPause();
822
+ },
823
+ onFocus: () => {
824
+ interaction.current.focus = true;
825
+ syncPause();
826
+ },
827
+ onBlur: (event) => {
828
+ if (!event.currentTarget.contains(event.relatedTarget)) {
829
+ interaction.current.focus = false;
830
+ syncPause();
831
+ }
832
+ },
833
+ onKeyDown: (event) => {
834
+ if (event.key === "Escape" && snapshot.state === "expanded") {
835
+ event.preventDefault();
836
+ live.dismiss();
837
+ }
838
+ },
839
+ children: [
840
+ active ? /* @__PURE__ */ jsxs2(
841
+ "div",
842
+ {
843
+ className: "bubble-live__body",
844
+ style: {
845
+ display: "flex",
846
+ alignItems: "center",
847
+ gap: 10,
848
+ minWidth: 0,
849
+ minHeight: `${size - (context.bordered ? 2 : 0)}px`,
850
+ padding: expanded ? "10px 6px 10px 14px" : "0 14px",
851
+ boxSizing: "border-box"
852
+ },
853
+ children: [
854
+ icon ? /* @__PURE__ */ jsx3(
855
+ "span",
856
+ {
857
+ className: "bubble-live__icon",
858
+ "aria-hidden": "true",
859
+ style: {
860
+ display: "inline-flex",
861
+ flexShrink: 0,
862
+ color: tone === "success" ? "var(--bubble-live-success-color, #22c55e)" : tone === "error" ? "var(--bubble-live-error-color, #ef4444)" : "inherit"
863
+ },
864
+ children: icon
865
+ }
866
+ ) : null,
867
+ /* @__PURE__ */ jsxs2(
868
+ "span",
869
+ {
870
+ className: "bubble-live__text",
871
+ style: { display: "flex", flexDirection: "column", flex: "1 1 auto", minWidth: 0 },
872
+ children: [
873
+ /* @__PURE__ */ jsx3(
874
+ "span",
875
+ {
876
+ className: "bubble-live__title",
877
+ style: {
878
+ overflow: "hidden",
879
+ textOverflow: "ellipsis",
880
+ whiteSpace: "nowrap",
881
+ fontSize: "clamp(11px, var(--bubble-live-title-size, 13px), 18px)",
882
+ fontWeight: 600
883
+ },
884
+ children: title
885
+ }
886
+ ),
887
+ expanded && description ? /* @__PURE__ */ jsx3(
888
+ "span",
889
+ {
890
+ className: "bubble-live__description",
891
+ style: {
892
+ fontSize: "clamp(9px, var(--bubble-live-meta-size, 11px), 14px)",
893
+ lineHeight: 1.4,
894
+ opacity: 0.75,
895
+ overflowWrap: "anywhere"
896
+ },
897
+ children: description
898
+ }
899
+ ) : null
900
+ ]
901
+ }
902
+ ),
903
+ snapshot.state === "compact" && snapshot.queued > 0 ? /* @__PURE__ */ jsxs2(
904
+ "span",
905
+ {
906
+ className: "bubble-live__queued",
907
+ style: { flexShrink: 0, fontSize: 11, opacity: 0.7 },
908
+ children: [
909
+ "+",
910
+ snapshot.queued
911
+ ]
912
+ }
913
+ ) : null,
914
+ expanded ? /* @__PURE__ */ jsx3(BubbleIconButton, { name: "close", ariaLabel: labels.close, onClick: live.dismiss }) : null
915
+ ]
916
+ },
917
+ snapshot.id
918
+ ) : null,
919
+ expanded && snapshot.duration > 0 ? /* @__PURE__ */ jsx3(
920
+ "span",
921
+ {
922
+ className: "bubble-live__progress",
923
+ "aria-hidden": "true",
924
+ style: {
925
+ position: "absolute",
926
+ left: 0,
927
+ right: 0,
928
+ bottom: 0,
929
+ height: 2,
930
+ animationDuration: `${snapshot.duration}ms`,
931
+ animationPlayState: snapshot.paused ? "paused" : "running"
932
+ }
933
+ },
934
+ `progress-${snapshot.id}`
935
+ ) : null
936
+ ]
937
+ }
938
+ ),
939
+ /* @__PURE__ */ jsx3(
940
+ "span",
941
+ {
942
+ role: "status",
943
+ "aria-live": "polite",
944
+ className: "bubble-live__announcer",
945
+ style: VISUALLY_HIDDEN,
946
+ children: tone === "error" ? "" : announcement
947
+ }
948
+ ),
949
+ /* @__PURE__ */ jsx3("span", { role: "alert", className: "bubble-live__announcer", style: VISUALLY_HIDDEN, children: tone === "error" ? announcement : "" })
950
+ ] });
951
+ };
952
+
953
+ // src/search/BubbleBreadcrumbSearch.tsx
395
954
  import { forwardRef as forwardRef4 } from "react";
955
+
956
+ // src/search/SearchBase.tsx
957
+ import { useCallback as useCallback3, useMemo as useMemo2, useRef as useRef4, useState as useState6 } from "react";
958
+
959
+ // src/components/BubbleArea.tsx
960
+ import { forwardRef as forwardRef3 } from "react";
396
961
  import { jsx as jsx4 } from "react/jsx-runtime";
397
- var BubbleArea = forwardRef4(
398
- ({
399
- children,
400
- minWidth = 120,
401
- maxWidth,
402
- className = "",
403
- style,
404
- ...rest
405
- }, ref) => {
962
+ var BubbleArea = forwardRef3(
963
+ ({ children, minWidth = 120, maxWidth, className = "", style, ...rest }, ref) => {
406
964
  const areaStyles = {
407
965
  display: "inline-flex",
408
966
  alignItems: "center",
@@ -413,29 +971,635 @@ var BubbleArea = forwardRef4(
413
971
  color: "inherit",
414
972
  ...style
415
973
  };
416
- return /* @__PURE__ */ jsx4(
974
+ return /* @__PURE__ */ jsx4("div", { ref, className: `bubble-area ${className}`.trim(), style: areaStyles, ...rest, children });
975
+ }
976
+ );
977
+ BubbleArea.displayName = "BubbleArea";
978
+
979
+ // src/search/tokens.ts
980
+ var TOKENS = {
981
+ width: ["--bubble-search-width", "px"],
982
+ radius: ["--bubble-search-radius", "px"],
983
+ divider: ["--bubble-search-divider", ""],
984
+ focusRing: ["--bubble-search-focus-ring", ""],
985
+ optionActiveBg: ["--bubble-search-option-active-bg", ""],
986
+ optionRadius: ["--bubble-search-option-radius", "px"],
987
+ titleSize: ["--bubble-search-title-size", "px"],
988
+ titleWeight: ["--bubble-search-title-weight", ""],
989
+ metaSize: ["--bubble-search-meta-size", "px"],
990
+ mutedOpacity: ["--bubble-search-muted-opacity", ""],
991
+ resultsMaxHeight: ["--bubble-search-results-max-height", "px"],
992
+ panelPadding: ["--bubble-search-panel-padding", "px"],
993
+ duration: ["--bubble-search-duration", "ms"],
994
+ easing: ["--bubble-search-easing", ""],
995
+ breadcrumbSize: ["--bubble-breadcrumb-size", "px"],
996
+ breadcrumbSeparatorColor: ["--bubble-breadcrumb-separator-color", ""],
997
+ breadcrumbCurrentWeight: ["--bubble-breadcrumb-current-weight", ""],
998
+ breadcrumbMutedOpacity: ["--bubble-breadcrumb-muted-opacity", ""]
999
+ };
1000
+ var tokensToStyle = createTokenStyle(TOKENS);
1001
+
1002
+ // src/search/useBubbleSearch.ts
1003
+ import { useCallback as useCallback2, useEffect as useEffect3, useId, useRef as useRef3, useState as useState5 } from "react";
1004
+ var normalizeQuery = (query) => query.trim().toLowerCase();
1005
+ var useLatest = (value) => {
1006
+ const ref = useRef3(value);
1007
+ ref.current = value;
1008
+ return ref;
1009
+ };
1010
+ function useBubbleSearch({
1011
+ open,
1012
+ setOpen,
1013
+ groupRef,
1014
+ onSearch,
1015
+ onSelect,
1016
+ onError,
1017
+ suggestions = [],
1018
+ debounceMs
1019
+ }) {
1020
+ const { run } = useBubbleActivity();
1021
+ const [query, setQuery] = useState5("");
1022
+ const [result, setResult] = useState5(null);
1023
+ const [activeIndex, setActiveIndex] = useState5(-1);
1024
+ const inputRef = useRef3(null);
1025
+ const buttonRef = useRef3(null);
1026
+ const listId = useId();
1027
+ const hintId = useId();
1028
+ const onSearchRef = useLatest(onSearch);
1029
+ const onSelectRef = useLatest(onSelect);
1030
+ const onErrorRef = useLatest(onError);
1031
+ const normalizedQuery = normalizeQuery(query);
1032
+ const pending = Boolean(normalizedQuery && result?.query !== normalizedQuery);
1033
+ const noResults = Boolean(normalizedQuery && !pending && result?.items.length === 0);
1034
+ const options = pending ? [] : normalizedQuery && !noResults ? result?.items ?? [] : suggestions;
1035
+ const optionId = (index) => `${listId}-option-${index}`;
1036
+ const close = useCallback2(
1037
+ (restoreFocus = false) => {
1038
+ setOpen(false);
1039
+ if (restoreFocus) {
1040
+ buttonRef.current?.focus();
1041
+ }
1042
+ },
1043
+ [setOpen]
1044
+ );
1045
+ useEffect3(() => {
1046
+ if (open) {
1047
+ inputRef.current?.focus();
1048
+ } else {
1049
+ setQuery("");
1050
+ setResult(null);
1051
+ setActiveIndex(-1);
1052
+ }
1053
+ }, [open]);
1054
+ useEffect3(() => {
1055
+ if (!open) {
1056
+ return;
1057
+ }
1058
+ const handlePointerDown = (event) => {
1059
+ if (event.target instanceof Node && !groupRef.current?.contains(event.target)) {
1060
+ close();
1061
+ }
1062
+ };
1063
+ document.addEventListener("pointerdown", handlePointerDown);
1064
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
1065
+ }, [open, close, groupRef]);
1066
+ useEffect3(() => {
1067
+ if (!open || !normalizedQuery) {
1068
+ return;
1069
+ }
1070
+ const controller = new AbortController();
1071
+ const search = async () => {
1072
+ try {
1073
+ const items = await run(() => onSearchRef.current(normalizedQuery, controller.signal));
1074
+ if (!controller.signal.aborted) {
1075
+ setResult({ query: normalizedQuery, items });
1076
+ }
1077
+ } catch (error) {
1078
+ if (!controller.signal.aborted) {
1079
+ onErrorRef.current?.(error);
1080
+ setResult({ query: normalizedQuery, items: [] });
1081
+ }
1082
+ }
1083
+ };
1084
+ if (debounceMs > 0) {
1085
+ const timer = setTimeout(() => void search(), debounceMs);
1086
+ return () => {
1087
+ clearTimeout(timer);
1088
+ controller.abort();
1089
+ };
1090
+ }
1091
+ void search();
1092
+ return () => controller.abort();
1093
+ }, [open, normalizedQuery, run, debounceMs, onSearchRef, onErrorRef]);
1094
+ useEffect3(() => {
1095
+ if (activeIndex >= 0) {
1096
+ document.getElementById(listId)?.children[activeIndex]?.scrollIntoView?.({ block: "nearest" });
1097
+ }
1098
+ }, [activeIndex, listId]);
1099
+ const select = (item) => {
1100
+ onSelectRef.current(item);
1101
+ close(true);
1102
+ };
1103
+ const changeQuery = (value) => {
1104
+ setQuery(value);
1105
+ if (normalizeQuery(value) !== normalizedQuery) {
1106
+ setResult(null);
1107
+ }
1108
+ setActiveIndex(-1);
1109
+ };
1110
+ const handleInputKeyDown = (event) => {
1111
+ if (event.key === "Escape") {
1112
+ event.preventDefault();
1113
+ event.stopPropagation();
1114
+ close(true);
1115
+ } else if ((event.key === "ArrowDown" || event.key === "ArrowUp") && options.length) {
1116
+ event.preventDefault();
1117
+ const direction = event.key === "ArrowDown" ? 1 : -1;
1118
+ setActiveIndex(
1119
+ (index) => index < 0 ? direction === 1 ? 0 : options.length - 1 : (index + direction + options.length) % options.length
1120
+ );
1121
+ } else if (event.key === "Enter" && options[activeIndex]) {
1122
+ event.preventDefault();
1123
+ select(options[activeIndex]);
1124
+ }
1125
+ };
1126
+ return {
1127
+ query,
1128
+ changeQuery,
1129
+ normalizedQuery,
1130
+ pending,
1131
+ noResults,
1132
+ options,
1133
+ activeIndex,
1134
+ setActiveIndex,
1135
+ activeDescendant: activeIndex >= 0 && options[activeIndex] ? optionId(activeIndex) : void 0,
1136
+ optionId,
1137
+ listId,
1138
+ hintId,
1139
+ inputRef,
1140
+ buttonRef,
1141
+ close,
1142
+ select,
1143
+ handleInputKeyDown
1144
+ };
1145
+ }
1146
+
1147
+ // src/search/SearchBase.tsx
1148
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1149
+ var defaultSearchLabels = {
1150
+ group: "Search",
1151
+ button: "Search",
1152
+ input: "Search",
1153
+ listbox: "Results",
1154
+ placeholder: "Search\u2026",
1155
+ searching: "Searching\u2026",
1156
+ noResults: "No results. Try another term or explore a suggestion.",
1157
+ suggestions: "Suggestions",
1158
+ results: (count) => `${count} ${count === 1 ? "result" : "results"}`,
1159
+ hint: "\u2191 \u2193 to explore \xB7 Enter to select \xB7 Esc to close"
1160
+ };
1161
+ var mergeRefs = (...refs) => (value) => {
1162
+ for (const ref of refs) {
1163
+ if (typeof ref === "function") {
1164
+ ref(value);
1165
+ } else if (ref) {
1166
+ ref.current = value;
1167
+ }
1168
+ }
1169
+ };
1170
+ var SearchBody = ({
1171
+ open,
1172
+ setOpen,
1173
+ groupRef,
1174
+ headerHeight,
1175
+ upward,
1176
+ labels,
1177
+ idleContent,
1178
+ onSearch,
1179
+ onSelect,
1180
+ onError,
1181
+ suggestions,
1182
+ debounceMs = 0
1183
+ }) => {
1184
+ const search = useBubbleSearch({
1185
+ open,
1186
+ setOpen,
1187
+ groupRef,
1188
+ onSearch,
1189
+ onSelect,
1190
+ onError,
1191
+ suggestions,
1192
+ debounceMs
1193
+ });
1194
+ const { options, activeIndex, listId, hintId } = search;
1195
+ const compact = !open && idleContent === void 0;
1196
+ return /* @__PURE__ */ jsxs3(
1197
+ "div",
1198
+ {
1199
+ className: "bubble-search__body",
1200
+ "data-placement": upward ? "top" : "bottom",
1201
+ style: { display: "flex", flexDirection: upward ? "column-reverse" : "column", minWidth: 0 },
1202
+ onBlurCapture: (event) => {
1203
+ if (!event.currentTarget.contains(event.relatedTarget)) {
1204
+ search.close();
1205
+ }
1206
+ },
1207
+ children: [
1208
+ /* @__PURE__ */ jsxs3(
1209
+ "div",
1210
+ {
1211
+ className: "bubble-search__header",
1212
+ "data-compact": compact,
1213
+ style: { height: `${headerHeight}px` },
1214
+ children: [
1215
+ /* @__PURE__ */ jsx5(
1216
+ BubbleIconButton,
1217
+ {
1218
+ ref: search.buttonRef,
1219
+ name: "search",
1220
+ ariaLabel: labels.button,
1221
+ "aria-expanded": open,
1222
+ "aria-controls": open ? listId : void 0,
1223
+ active: open,
1224
+ onKeyDown: (event) => {
1225
+ if (event.key === "Escape" && open) {
1226
+ event.preventDefault();
1227
+ search.close(true);
1228
+ }
1229
+ },
1230
+ onClick: () => {
1231
+ if (!open) {
1232
+ setOpen(true);
1233
+ } else {
1234
+ search.inputRef.current?.focus();
1235
+ }
1236
+ }
1237
+ }
1238
+ ),
1239
+ compact ? null : /* @__PURE__ */ jsx5(BubbleArea, { minWidth: 0, className: "bubble-search__area", children: open ? /* @__PURE__ */ jsx5(
1240
+ "input",
1241
+ {
1242
+ ref: search.inputRef,
1243
+ className: "bubble-search__input",
1244
+ type: "text",
1245
+ role: "combobox",
1246
+ "aria-label": labels.input,
1247
+ "aria-autocomplete": "list",
1248
+ "aria-expanded": open,
1249
+ "aria-controls": listId,
1250
+ "aria-describedby": hintId,
1251
+ "aria-activedescendant": search.activeDescendant,
1252
+ autoComplete: "off",
1253
+ placeholder: labels.placeholder,
1254
+ value: search.query,
1255
+ onChange: (event) => search.changeQuery(event.target.value),
1256
+ onKeyDown: search.handleInputKeyDown
1257
+ }
1258
+ ) : idleContent })
1259
+ ]
1260
+ }
1261
+ ),
1262
+ /* @__PURE__ */ jsx5(
1263
+ "div",
1264
+ {
1265
+ className: "bubble-search__expansion",
1266
+ "data-open": open,
1267
+ "aria-hidden": !open,
1268
+ style: {
1269
+ display: "grid",
1270
+ gridTemplateRows: open ? "1fr" : "0fr",
1271
+ visibility: open ? "visible" : "hidden",
1272
+ opacity: open ? 1 : 0
1273
+ },
1274
+ children: /* @__PURE__ */ jsx5(
1275
+ "div",
1276
+ {
1277
+ className: "bubble-search__expansion-inner",
1278
+ style: { minHeight: 0, overflow: "hidden" },
1279
+ children: /* @__PURE__ */ jsxs3("div", { className: "bubble-search__panel", children: [
1280
+ /* @__PURE__ */ jsx5("div", { className: "bubble-search__status", role: "status", "aria-live": "polite", children: search.pending ? labels.searching : search.noResults ? labels.noResults : search.normalizedQuery ? labels.results(options.length) : labels.suggestions }),
1281
+ /* @__PURE__ */ jsx5(
1282
+ "div",
1283
+ {
1284
+ id: listId,
1285
+ role: "listbox",
1286
+ "aria-label": labels.listbox,
1287
+ className: "bubble-search__results",
1288
+ children: options.map((item, index) => /* @__PURE__ */ jsxs3(
1289
+ "div",
1290
+ {
1291
+ id: search.optionId(index),
1292
+ role: "option",
1293
+ tabIndex: -1,
1294
+ "aria-selected": activeIndex === index,
1295
+ className: "bubble-search__option",
1296
+ onPointerDown: (event) => event.preventDefault(),
1297
+ onMouseMove: () => search.setActiveIndex(index),
1298
+ onClick: () => search.select(item),
1299
+ onKeyDown: (event) => {
1300
+ if (event.key === "Enter" || event.key === " ") {
1301
+ event.preventDefault();
1302
+ search.select(item);
1303
+ }
1304
+ },
1305
+ children: [
1306
+ /* @__PURE__ */ jsx5("span", { className: "bubble-search__option-title", children: item.title }),
1307
+ item.description ? /* @__PURE__ */ jsx5("span", { className: "bubble-search__option-description", children: item.description }) : null
1308
+ ]
1309
+ },
1310
+ item.id
1311
+ ))
1312
+ }
1313
+ ),
1314
+ /* @__PURE__ */ jsx5("p", { id: hintId, className: "bubble-search__hint", children: labels.hint })
1315
+ ] })
1316
+ }
1317
+ )
1318
+ }
1319
+ )
1320
+ ]
1321
+ }
1322
+ );
1323
+ };
1324
+ var SearchBase = ({
1325
+ open: openProp,
1326
+ defaultOpen = false,
1327
+ onOpenChange,
1328
+ tokens,
1329
+ className = "",
1330
+ forwardedRef,
1331
+ ...bodyProps
1332
+ }) => {
1333
+ const context = useBubbleToolbar();
1334
+ const groupRef = useRef4(null);
1335
+ const [innerOpen, setInnerOpen] = useState6(defaultOpen);
1336
+ const controlled = openProp !== void 0;
1337
+ const open = controlled ? openProp : innerOpen;
1338
+ const onOpenChangeRef = useRef4(onOpenChange);
1339
+ onOpenChangeRef.current = onOpenChange;
1340
+ const setOpen = useCallback3(
1341
+ (next) => {
1342
+ if (!controlled) {
1343
+ setInnerOpen(next);
1344
+ }
1345
+ onOpenChangeRef.current?.(next);
1346
+ },
1347
+ [controlled]
1348
+ );
1349
+ const ref = useMemo2(() => mergeRefs(groupRef, forwardedRef), [forwardedRef]);
1350
+ const size = resolveSize(context.size);
1351
+ const compact = !open && bodyProps.idleContent === void 0;
1352
+ const style = {
1353
+ ...tokensToStyle(tokens),
1354
+ height: "auto",
1355
+ width: compact ? `${size}px` : "min(var(--bubble-search-width, 360px), calc(100vw - 96px))",
1356
+ minWidth: 0,
1357
+ padding: 0,
1358
+ gap: 0,
1359
+ flexDirection: "column",
1360
+ alignItems: "stretch",
1361
+ borderRadius: "var(--bubble-search-radius, 24px)"
1362
+ };
1363
+ return /* @__PURE__ */ jsx5(
1364
+ BubbleGroup,
1365
+ {
1366
+ ref,
1367
+ "aria-label": bodyProps.labels.group,
1368
+ shape: "pill",
1369
+ className: `bubble-search ${className}`.trim(),
1370
+ "data-expanded": open,
1371
+ "data-compact": compact,
1372
+ style,
1373
+ children: /* @__PURE__ */ jsx5(
1374
+ SearchBody,
1375
+ {
1376
+ ...bodyProps,
1377
+ open,
1378
+ setOpen,
1379
+ groupRef,
1380
+ headerHeight: size - (context.bordered ? 2 : 0),
1381
+ upward: context.position === "bottom"
1382
+ }
1383
+ )
1384
+ }
1385
+ );
1386
+ };
1387
+
1388
+ // src/search/BubbleBreadcrumbSearch.tsx
1389
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1390
+ var BreadcrumbLevel = ({ item, current }) => item.href ? /* @__PURE__ */ jsx6("a", { href: item.href, "aria-current": current ? "page" : void 0, children: item.label }) : /* @__PURE__ */ jsx6("span", { "aria-current": current ? "page" : void 0, children: item.label });
1391
+ var Breadcrumb = ({
1392
+ items,
1393
+ separator,
1394
+ label
1395
+ }) => {
1396
+ const parents = items.slice(0, -1);
1397
+ const current = items[items.length - 1];
1398
+ const divider = /* @__PURE__ */ jsx6("span", { className: "bubble-breadcrumb__separator", "aria-hidden": "true", children: ` ${separator} ` });
1399
+ return /* @__PURE__ */ jsxs4(
1400
+ "nav",
1401
+ {
1402
+ "aria-label": label,
1403
+ className: "bubble-breadcrumb",
1404
+ title: items.map((item) => item.label).join(` ${separator} `),
1405
+ children: [
1406
+ parents.length > 0 ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1407
+ /* @__PURE__ */ jsx6("span", { className: "bubble-breadcrumb__parents", children: parents.map((item, index) => (
1408
+ // biome-ignore lint/suspicious/noArrayIndexKey: Breadcrumb levels are identified by position.
1409
+ /* @__PURE__ */ jsxs4("span", { children: [
1410
+ /* @__PURE__ */ jsx6(BreadcrumbLevel, { item, current: false }),
1411
+ index < parents.length - 1 ? divider : null
1412
+ ] }, index)
1413
+ )) }),
1414
+ divider
1415
+ ] }) : null,
1416
+ current ? /* @__PURE__ */ jsx6("span", { className: "bubble-breadcrumb__current", children: /* @__PURE__ */ jsx6(BreadcrumbLevel, { item: current, current: true }) }) : null
1417
+ ]
1418
+ }
1419
+ );
1420
+ };
1421
+ var BubbleBreadcrumbSearchInner = forwardRef4(
1422
+ ({ items, separator = "/", labels, ...props }, ref) => {
1423
+ const { location = "Breadcrumb", ...searchLabels } = labels ?? {};
1424
+ return /* @__PURE__ */ jsx6(
1425
+ SearchBase,
1426
+ {
1427
+ ...props,
1428
+ labels: { ...defaultSearchLabels, ...searchLabels },
1429
+ idleContent: /* @__PURE__ */ jsx6(Breadcrumb, { items, separator, label: location }),
1430
+ forwardedRef: ref
1431
+ }
1432
+ );
1433
+ }
1434
+ );
1435
+ BubbleBreadcrumbSearchInner.displayName = "BubbleBreadcrumbSearch";
1436
+ var BubbleBreadcrumbSearch = BubbleBreadcrumbSearchInner;
1437
+
1438
+ // src/search/BubbleSearch.tsx
1439
+ import { forwardRef as forwardRef5 } from "react";
1440
+ import { jsx as jsx7 } from "react/jsx-runtime";
1441
+ var BubbleSearchInner = forwardRef5(
1442
+ ({ labels, ...props }, ref) => /* @__PURE__ */ jsx7(SearchBase, { ...props, labels: { ...defaultSearchLabels, ...labels }, forwardedRef: ref })
1443
+ );
1444
+ BubbleSearchInner.displayName = "BubbleSearch";
1445
+ var BubbleSearch = BubbleSearchInner;
1446
+
1447
+ // src/BubbleToolbar.tsx
1448
+ import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1449
+ var BubbleToolbar = forwardRef6(
1450
+ ({
1451
+ groups,
1452
+ children,
1453
+ position = "bottom",
1454
+ theme = "glass",
1455
+ size = "md",
1456
+ blurred = true,
1457
+ blurAmount = 16,
1458
+ groupGap = 8,
1459
+ itemGap = 8,
1460
+ bordered = true,
1461
+ shadow = true,
1462
+ busy = false,
1463
+ activityColors,
1464
+ activityDuration,
1465
+ className = "",
1466
+ style,
1467
+ ariaLabel = "Toolbar",
1468
+ liveActivity,
1469
+ ...rest
1470
+ }, ref) => {
1471
+ const contextValue = useMemo3(
1472
+ () => ({
1473
+ size,
1474
+ theme,
1475
+ blurred,
1476
+ blurAmount,
1477
+ itemGap,
1478
+ bordered,
1479
+ shadow,
1480
+ position,
1481
+ liveActivity,
1482
+ busy,
1483
+ activityColors,
1484
+ activityDuration
1485
+ }),
1486
+ [
1487
+ size,
1488
+ theme,
1489
+ blurred,
1490
+ blurAmount,
1491
+ itemGap,
1492
+ bordered,
1493
+ shadow,
1494
+ position,
1495
+ liveActivity,
1496
+ busy,
1497
+ activityColors,
1498
+ activityDuration
1499
+ ]
1500
+ );
1501
+ const toolbarRef = useRef5(null);
1502
+ const setRef = useCallback4(
1503
+ (node) => {
1504
+ toolbarRef.current = node;
1505
+ if (typeof ref === "function") {
1506
+ ref(node);
1507
+ } else if (ref) {
1508
+ ref.current = node;
1509
+ }
1510
+ },
1511
+ [ref]
1512
+ );
1513
+ const getPositionStyles = () => {
1514
+ switch (position) {
1515
+ case "bottom":
1516
+ return {
1517
+ position: "fixed",
1518
+ bottom: "24px",
1519
+ left: "50%",
1520
+ transform: "translateX(-50%)",
1521
+ zIndex: 1e3
1522
+ };
1523
+ case "top":
1524
+ return {
1525
+ position: "fixed",
1526
+ top: "24px",
1527
+ left: "50%",
1528
+ transform: "translateX(-50%)",
1529
+ zIndex: 1e3
1530
+ };
1531
+ case "static":
1532
+ return {
1533
+ position: "relative"
1534
+ };
1535
+ default:
1536
+ return {};
1537
+ }
1538
+ };
1539
+ const containerStyles = {
1540
+ ...getPositionStyles(),
1541
+ display: "inline-flex",
1542
+ alignItems: "center",
1543
+ gap: formatPx(groupGap),
1544
+ maxWidth: "calc(100vw - 32px)",
1545
+ overflowX: "auto",
1546
+ overflowY: "hidden",
1547
+ padding: "4px",
1548
+ boxSizing: "border-box",
1549
+ scrollbarWidth: "none",
1550
+ // Firefox
1551
+ msOverflowStyle: "none",
1552
+ // IE/Edge
1553
+ WebkitOverflowScrolling: "touch",
1554
+ pointerEvents: "auto",
1555
+ ...style
1556
+ };
1557
+ const combinedClassName = [
1558
+ "bubble-toolbar",
1559
+ `bubble-toolbar--${position}`,
1560
+ `bubble-toolbar--${theme}`,
1561
+ className
1562
+ ].filter(Boolean).join(" ");
1563
+ const content = /* @__PURE__ */ jsxs5(Fragment4, { children: [
1564
+ groups && groups.length > 0 ? groups.map((groupItems, index) => (
1565
+ // biome-ignore lint/suspicious/noArrayIndexKey: The groups array API identifies groups by position; use compound children for keyed groups.
1566
+ /* @__PURE__ */ jsx8(BubbleGroup, { children: groupItems }, index)
1567
+ )) : null,
1568
+ children
1569
+ ] });
1570
+ return /* @__PURE__ */ jsx8(BubbleToolbarContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx8(
417
1571
  "div",
418
1572
  {
419
- ref,
420
- className: `bubble-area ${className}`.trim(),
421
- style: areaStyles,
1573
+ ref: setRef,
1574
+ role: "toolbar",
1575
+ "aria-label": ariaLabel,
1576
+ className: combinedClassName,
1577
+ style: containerStyles,
422
1578
  ...rest,
423
- children
1579
+ children: liveActivity ? /* @__PURE__ */ jsx8(LiveSlot, { controller: liveActivity, toolbarRef, children: content }) : content
424
1580
  }
425
- );
1581
+ ) });
426
1582
  }
427
1583
  );
428
- BubbleArea.displayName = "BubbleArea";
1584
+ BubbleToolbar.displayName = "BubbleToolbar";
1585
+ BubbleToolbar.Group = BubbleGroup;
1586
+ BubbleToolbar.Search = BubbleSearch;
1587
+ BubbleToolbar.BreadcrumbSearch = BubbleBreadcrumbSearch;
429
1588
  export {
430
1589
  BubbleArea,
1590
+ BubbleBreadcrumbSearch,
431
1591
  BubbleGroup,
432
1592
  BubbleIconButton,
1593
+ BubbleSearch,
433
1594
  BubbleToolbar,
434
1595
  BubbleToolbarContext,
435
1596
  formatPx,
436
1597
  getGlassStyles,
437
1598
  getValidChildren,
438
1599
  resolveSize,
439
- useBubbleToolbar
1600
+ useBubbleActivity,
1601
+ useBubbleLiveActivity,
1602
+ useBubbleToolbar,
1603
+ useBubbleToolbarLiveActivity
440
1604
  };
441
1605
  //# sourceMappingURL=index.mjs.map