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