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