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