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