@botiverse/raft-ui 0.21.2 → 0.22.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.
@@ -1,21 +1,23 @@
1
1
  import { cn } from "./cn.mjs";
2
2
  import { Dialog } from "@base-ui/react/dialog";
3
- import { Check, Minus, X } from "lucide-react";
3
+ import { ArrowLeft, Check, Minus, X } from "lucide-react";
4
+ import { Children, Fragment, createContext, createElement, isValidElement, use, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
5
  import { tv } from "tailwind-variants/lite";
5
- import { Children, Fragment, createContext, isValidElement, use, useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
6
6
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { Button } from "@base-ui/react/button";
8
8
  import { mergeProps } from "@base-ui/react/merge-props";
9
9
  import { useRender } from "@base-ui/react/use-render";
10
+ import { createPortal, flushSync } from "react-dom";
10
11
  import { Tooltip } from "@base-ui/react/tooltip";
11
12
  import { Checkbox } from "@base-ui/react/checkbox";
12
13
  import { ScrollArea } from "@base-ui/react/scroll-area";
13
14
  import { Avatar } from "@base-ui/react";
14
15
  import { Menu } from "@base-ui/react/menu";
15
- import { flushSync } from "react-dom";
16
+ import { Popover } from "@base-ui/react/popover";
17
+ import { PointerSensor, TouchSensor, useSensor } from "@dnd-kit/core";
18
+ import { PreviewCard } from "@base-ui/react/preview-card";
16
19
  import { Toggle } from "@base-ui/react/toggle";
17
20
  import { Toolbar } from "@base-ui/react/toolbar";
18
- import { PointerSensor, TouchSensor, useSensor } from "@dnd-kit/core";
19
21
  //#region src/lib/decision-width.ts
20
22
  /**
21
23
  * The width of every decision dialog (dialog.md#variants): AlertDialog and a
@@ -54,6 +56,103 @@ const headEdges = ["px-5 max-md:px-4", ...headEdgePull];
54
56
  * as the arithmetic so it follows the control tokens wherever they switch.
55
57
  */
56
58
  const controlIconInset = "[--icon-inset:calc((var(--control-height)-var(--control-icon-size))/2)]";
59
+ /**
60
+ * The row of buttons while `data-stacked` is set: a full-width column. A
61
+ * button alone in a wrapper (a tooltip's trigger) fills the width too.
62
+ */
63
+ const stackedActions = [
64
+ "data-stacked:flex-col-reverse",
65
+ "data-stacked:items-stretch",
66
+ "data-stacked:*:data-[slot=button]:w-full data-stacked:[&>*>[data-slot=button]:only-child]:w-full",
67
+ ...[].map((value) => `data-stacked:${value}`)
68
+ ];
69
+ function assignRef$2(ref, value) {
70
+ if (typeof ref === "function") ref(value);
71
+ else if (ref) ref.current = value;
72
+ }
73
+ function contentWidth(element) {
74
+ const style = getComputedStyle(element);
75
+ return element.getBoundingClientRect().width - (Number.parseFloat(style.paddingLeft) || 0) - (Number.parseFloat(style.paddingRight) || 0) - (Number.parseFloat(style.borderLeftWidth) || 0) - (Number.parseFloat(style.borderRightWidth) || 0);
76
+ }
77
+ /**
78
+ * Whether a row of buttons must stack: laid out as a row (`data-stacked` off
79
+ * for the measure and put back at once, inside one frame, so nothing is
80
+ * painted in between and the layouts never flip back and forth), its children
81
+ * run past the content box of `frame`. Children that may shrink (a label that
82
+ * truncates or wraps) shrink first, so only content that can't, the buttons,
83
+ * counts as not fitting. Undefined while the frame is hidden (a sheet hides
84
+ * its footer), so nothing is decided then.
85
+ */
86
+ function rowOverflows(row, frame) {
87
+ if (!(frame.getBoundingClientRect().width > 0)) return void 0;
88
+ const wasStacked = row.hasAttribute("data-stacked");
89
+ if (wasStacked) row.removeAttribute("data-stacked");
90
+ const available = contentWidth(frame);
91
+ let start = Number.POSITIVE_INFINITY;
92
+ let end = Number.NEGATIVE_INFINITY;
93
+ for (const child of row.children) {
94
+ const box = child.getBoundingClientRect();
95
+ if (box.width === 0 && box.height === 0) continue;
96
+ const style = getComputedStyle(child);
97
+ start = Math.min(start, box.left - (Number.parseFloat(style.marginLeft) || 0));
98
+ end = Math.max(end, box.right + (Number.parseFloat(style.marginRight) || 0));
99
+ }
100
+ if (wasStacked) row.setAttribute("data-stacked", "");
101
+ return end - start > available + .5;
102
+ }
103
+ /**
104
+ * Sets `data-stacked` on a footer's row of buttons when it doesn't fit on one
105
+ * line (rowOverflows). Pass the returned ref to the row. It must fit its own
106
+ * content box, or with `inParent` (the group of buttons beside a note) its
107
+ * parent's. As BannerAction's `data-wrap`, it measures again when the footer
108
+ * resizes, the buttons change or a web font arrives.
109
+ */
110
+ function useStackedActions(ref, inParent = false) {
111
+ const [row, setRow] = useState(null);
112
+ const [stacked, setStacked] = useState(false);
113
+ const callbackRef = useCallback((node) => {
114
+ setRow(node);
115
+ assignRef$2(ref, node);
116
+ }, [ref]);
117
+ useLayoutEffect(() => {
118
+ const frame = inParent ? row?.parentElement : row;
119
+ if (!row || !frame || typeof ResizeObserver === "undefined") return;
120
+ const measure = () => {
121
+ const overflows = rowOverflows(row, frame);
122
+ if (overflows !== void 0) setStacked(overflows);
123
+ };
124
+ let raf = 0;
125
+ const schedule = () => {
126
+ cancelAnimationFrame(raf);
127
+ raf = requestAnimationFrame(measure);
128
+ };
129
+ measure();
130
+ const resize = new ResizeObserver(schedule);
131
+ resize.observe(frame);
132
+ resize.observe(row);
133
+ for (const child of row.children) resize.observe(child);
134
+ const mutation = new MutationObserver(schedule);
135
+ mutation.observe(row, {
136
+ childList: true,
137
+ characterData: true,
138
+ subtree: true
139
+ });
140
+ const fonts = typeof document.fonts === "undefined" ? void 0 : document.fonts;
141
+ let live = true;
142
+ fonts?.ready.then(() => {
143
+ if (live) schedule();
144
+ });
145
+ fonts?.addEventListener("loadingdone", schedule);
146
+ return () => {
147
+ live = false;
148
+ cancelAnimationFrame(raf);
149
+ resize.disconnect();
150
+ mutation.disconnect();
151
+ fonts?.removeEventListener("loadingdone", schedule);
152
+ };
153
+ }, [row, inParent]);
154
+ return [stacked, callbackRef];
155
+ }
57
156
  //#endregion
58
157
  //#region src/lib/tv.ts
59
158
  const tv$1 = ((options) => {
@@ -97,6 +196,16 @@ function useThemeScopeProps(props) {
97
196
  //#endregion
98
197
  //#region src/lib/overlay-depth.tsx
99
198
  const OverlayStackContext = createContext([]);
199
+ /**
200
+ * Tells everything under it that a modal surface is open, so a raft-ui
201
+ * Dialog, AlertDialog or Drawer opened from inside draws the lighter nested
202
+ * scrim (design/foundations/elevation.md#stacked-overlays). raft-ui's own
203
+ * roots add one by themselves; an app's own modal (its own backdrop, Esc and
204
+ * focus) wraps its scrim and its content in one to join the stack, and reads
205
+ * `useParentOverlayKind` inside it to pick its own scrim. Modal overlays only:
206
+ * a non-modal one passes `modal={false}`, adds nothing, and must not call
207
+ * `useParentOverlayKind` (it would read the layer below as its own).
208
+ */
100
209
  function OverlayLayerProvider({ kind, modal, children }) {
101
210
  const stack = useContext(OverlayStackContext);
102
211
  const value = useMemo(() => modal ? [...stack, kind] : stack, [
@@ -111,8 +220,14 @@ function OverlayLayerProvider({ kind, modal, children }) {
111
220
  }
112
221
  /**
113
222
  * The modal surface this overlay's own surface was opened from, or null when
114
- * it is the first. Called inside a surface, so the last entry is the surface
115
- * itself and the one before it is its parent.
223
+ * it is the first. Call it inside the surface's own `OverlayLayerProvider`
224
+ * (for raft-ui's overlays, inside their root): the last entry is the surface
225
+ * itself and the one before it is its parent. Not null means the surface is
226
+ * stacked over another and draws `layer-backdrop-nested` instead of
227
+ * `layer-backdrop`. Modal overlays only: under `modal={false}` nothing is
228
+ * added, so the last entry is the parent and this returns the wrong layer. It reads React context, so it sees the surfaces whose
229
+ * tree this one is rendered in, portals included; one opened from outside
230
+ * that tree is not stacked on it here.
116
231
  */
117
232
  function useParentOverlayKind() {
118
233
  const stack = useContext(OverlayStackContext);
@@ -410,6 +525,141 @@ function Button$1({ variant, size, loading = false, loadingLabel = "Loading", di
410
525
  });
411
526
  }
412
527
  //#endregion
528
+ //#region src/components/dialog/dialog-sheet-context.ts
529
+ const NO_SHEET = {
530
+ sheet: false,
531
+ commitSlot: null,
532
+ setCommitSlot: () => void 0,
533
+ bodyEndSlot: null,
534
+ setBodyEndSlot: () => void 0
535
+ };
536
+ const DialogSheetContext = createContext(NO_SHEET);
537
+ function useDialogSheet() {
538
+ return useContext(DialogSheetContext);
539
+ }
540
+ /** Inside the sheet's head: its × is a pushed page's leading action. */
541
+ const DialogSheetHeadContext = createContext(false);
542
+ function useInDialogSheetHead() {
543
+ return useContext(DialogSheetHeadContext);
544
+ }
545
+ /**
546
+ * Every overlay surface starts outside any sheet: a confirm, a dialog or a
547
+ * drawer opened from inside a sheet keeps its own head, footer and commit
548
+ * button instead of taking the sheet's.
549
+ */
550
+ function DialogSheetReset({ children }) {
551
+ return createElement(DialogSheetContext.Provider, { value: NO_SHEET }, createElement(DialogSheetHeadContext.Provider, { value: false }, children));
552
+ }
553
+ /**
554
+ * A dialog drawn as a phone sheet (design: dialog.md "On a phone"). The
555
+ * surface is DialogContent's frame docked to the bottom, 90% of the screen
556
+ * tall, rounded on its top corners, sliding up as a bottom sheet does; its
557
+ * head is a pushed page's head (× at the start, the title, the commit button
558
+ * at the end); its body keeps the phone's 16px margin and the home indicator's
559
+ * inset. Merged over DialogContent's classes, so these win.
560
+ */
561
+ const dialogSheet = tv$1({ slots: {
562
+ content: [
563
+ "fixed inset-x-0 top-auto bottom-0 left-0 h-[90dvh] max-h-none w-full max-w-none translate-none",
564
+ "rounded-none rounded-t-dialog",
565
+ "pr-[env(safe-area-inset-right,0px)] pl-[env(safe-area-inset-left,0px)]",
566
+ "transition-transform duration-240 ease-slide data-ending-style:duration-180",
567
+ "data-starting-style:translate-y-full data-ending-style:translate-y-full",
568
+ "motion-reduce:transition-none"
569
+ ],
570
+ overlay: [
571
+ "transition-opacity duration-240 ease-slide data-ending-style:duration-180",
572
+ "data-starting-style:opacity-0 data-ending-style:opacity-0",
573
+ "motion-reduce:transition-none"
574
+ ],
575
+ header: ["flex h-14 shrink-0 items-center gap-2 px-4 pt-0", "border-b border-line-seam"],
576
+ title: ["min-w-0 flex-1 truncate"],
577
+ commitSlot: ["flex shrink-0 items-center empty:hidden"],
578
+ body: ["min-h-0 content-start", "px-4 pt-4 first:pt-4 pb-[max(16px,env(safe-area-inset-bottom,0px))]"],
579
+ barBody: ["block overflow-y-auto"],
580
+ bodyEnd: ["mt-4 empty:hidden", "text-start font-sans text-footnote text-foreground-muted *:data-[slot=text-link]:text-footnote"]
581
+ } });
582
+ //#endregion
583
+ //#region src/components/text-link/text-link.tsx
584
+ const textLinkVariants = tv$1({
585
+ base: [
586
+ "rounded-[2px]",
587
+ "decoration-[1.5px] underline-offset-[3px] not-disabled:hover:underline",
588
+ "outline-none focus-visible:shadow-focus",
589
+ "disabled:cursor-not-allowed disabled:text-foreground-disabled"
590
+ ],
591
+ variants: { variant: {
592
+ muted: ["font-medium text-foreground-muted", "hover:text-foreground"],
593
+ strong: ["font-semibold text-foreground"]
594
+ } },
595
+ defaultVariants: { variant: "muted" }
596
+ });
597
+ function TextLink({ variant, className, render, ...props }) {
598
+ const defaultProps = {
599
+ className: textLinkVariants({
600
+ variant,
601
+ className
602
+ }),
603
+ ...render ? {} : { type: "button" }
604
+ };
605
+ return useRender({
606
+ defaultTagName: "button",
607
+ render,
608
+ props: mergeProps(defaultProps, props),
609
+ state: { slot: "text-link" }
610
+ });
611
+ }
612
+ //#endregion
613
+ //#region src/components/panel/panel-action.recipe.ts
614
+ const panelActionBase = tv$1({ base: [
615
+ "relative inline-flex w-auto shrink-0 items-center justify-center",
616
+ "overflow-visible font-sans leading-none tabular-nums select-none outline-none",
617
+ "transition-all duration-100",
618
+ "[&>svg]:pointer-events-none [&>svg]:shrink-0",
619
+ "h-control min-w-control rounded-control border-[1.5px] border-transparent bg-transparent py-0",
620
+ "px-2 md:pointer-fine:px-[5px]",
621
+ "after:absolute after:-inset-control-hit",
622
+ "text-foreground",
623
+ "hover:bg-fill-medium active:bg-fill-medium",
624
+ "focus-visible:shadow-focus",
625
+ "[&>svg]:size-control-icon",
626
+ controlIconInset
627
+ ] });
628
+ const panelAction = tv$1({
629
+ extend: panelActionBase,
630
+ base: [
631
+ "whitespace-nowrap",
632
+ "disabled:pointer-events-none disabled:transform-none",
633
+ "gap-1.5 text-control font-bold",
634
+ "disabled:bg-transparent disabled:text-foreground-disabled disabled:opacity-100",
635
+ "aria-pressed:border-sticker-ink aria-pressed:bg-brand aria-pressed:text-sticker-ink aria-pressed:ring-[1.5px] aria-pressed:ring-sticker-edge",
636
+ "aria-pressed:active:overlay-sticker-medium"
637
+ ]
638
+ });
639
+ const panelToggleAction = tv$1({
640
+ extend: panelActionBase,
641
+ base: [
642
+ "data-disabled:pointer-events-none data-disabled:transform-none",
643
+ "gap-1.5 text-control font-bold",
644
+ "data-disabled:bg-transparent data-disabled:text-foreground-disabled data-disabled:opacity-100",
645
+ "data-pressed:border-sticker-ink data-pressed:bg-brand data-pressed:text-sticker-ink data-pressed:ring-[1.5px] data-pressed:ring-sticker-edge",
646
+ "data-pressed:active:overlay-sticker-medium"
647
+ ]
648
+ });
649
+ //#endregion
650
+ //#region src/components/panel/panel-action.tsx
651
+ function PanelAction({ className, render, nativeButton, ...props }) {
652
+ return /* @__PURE__ */ jsx(Button, {
653
+ ...props,
654
+ render,
655
+ nativeButton: nativeButton ?? defaultNativeButton(render),
656
+ type: "button",
657
+ "data-slot": "panel-action",
658
+ "data-icon-inset": "",
659
+ className: panelAction({ className })
660
+ });
661
+ }
662
+ //#endregion
413
663
  //#region src/components/dialog/dialog.tsx
414
664
  const dialog = tv$1({
415
665
  slots: {
@@ -430,7 +680,18 @@ const dialog = tv$1({
430
680
  "px-5 pt-1.5 pb-5 first:pt-5",
431
681
  "font-sans text-subhead text-foreground-muted"
432
682
  ],
433
- footer: ["flex shrink-0 items-center justify-end", "gap-2 px-5 pb-5 first:pt-5 [[data-slot=dialog-header]+&]:pt-5"],
683
+ footer: [
684
+ "flex shrink-0 items-center justify-end",
685
+ ...stackedActions,
686
+ "gap-2 px-5 pb-5 first:pt-5 [[data-slot=dialog-header]+&]:pt-5",
687
+ "*:data-[slot=text-link]:text-footnote"
688
+ ],
689
+ footerNote: ["m-0 md:me-2 md:flex-1", "text-start font-sans text-footnote text-pretty text-foreground-muted md:whitespace-nowrap"],
690
+ footerActions: [
691
+ "ms-auto flex shrink-0 items-center justify-end gap-2",
692
+ ...stackedActions,
693
+ "data-stacked:ms-0 data-stacked:w-full"
694
+ ],
434
695
  title: ["m-0 min-w-0 text-foreground", "font-sans text-headline font-bold"],
435
696
  description: ["m-0", "font-sans text-subhead text-foreground-muted"]
436
697
  },
@@ -462,6 +723,10 @@ const dialog = tv$1({
462
723
  nested: {
463
724
  false: {},
464
725
  true: { overlay: ["bg-layer-backdrop-nested"] }
726
+ },
727
+ start: {
728
+ false: {},
729
+ true: { footer: ["flex-wrap justify-between"] }
465
730
  }
466
731
  },
467
732
  defaultVariants: {
@@ -502,6 +767,7 @@ function DialogOverlay({ className, layer, ...props }) {
502
767
  ...props
503
768
  });
504
769
  }
770
+ const DialogSizeContext = createContext(void 0);
505
771
  function DialogContent({ className, children, portalProps, overlay, size, layer, ...props }) {
506
772
  const { content } = dialog({
507
773
  size,
@@ -517,11 +783,15 @@ function DialogContent({ className, children, portalProps, overlay, size, layer,
517
783
  "data-slot": "dialog-content",
518
784
  className: content({ className }),
519
785
  ...props,
520
- children
786
+ children: /* @__PURE__ */ jsx(DialogSizeContext.Provider, {
787
+ value: size ?? (/(^|\s)max-w-/.test(className ?? "") ? void 0 : "md"),
788
+ children: /* @__PURE__ */ jsx(DialogSheetReset, { children })
789
+ })
521
790
  })]
522
791
  });
523
792
  }
524
793
  function DialogClose({ className, variant, size, children, render, "aria-label": ariaLabel, ...props }) {
794
+ const inSheetHead = useInDialogSheetHead();
525
795
  if (render) return /* @__PURE__ */ jsx(Dialog.Close, {
526
796
  "data-slot": "dialog-close",
527
797
  render,
@@ -529,6 +799,15 @@ function DialogClose({ className, variant, size, children, render, "aria-label":
529
799
  ...props,
530
800
  children
531
801
  });
802
+ if (inSheetHead) return /* @__PURE__ */ jsx(Dialog.Close, {
803
+ "data-slot": "dialog-close",
804
+ render: /* @__PURE__ */ jsx(PanelAction, {
805
+ "aria-label": ariaLabel ?? "Close",
806
+ className
807
+ }),
808
+ ...props,
809
+ children: children ?? /* @__PURE__ */ jsx(X, {})
810
+ });
532
811
  return /* @__PURE__ */ jsx(Dialog.Close, {
533
812
  "data-slot": "dialog-close",
534
813
  render: /* @__PURE__ */ jsx(Button$1, {
@@ -541,35 +820,149 @@ function DialogClose({ className, variant, size, children, render, "aria-label":
541
820
  children: children ?? /* @__PURE__ */ jsx(X, { className: "size-4.5" })
542
821
  });
543
822
  }
544
- function DialogHeader({ className, bar = false, ...props }) {
823
+ /**
824
+ * Back, in a later step of a flow drawn as a phone sheet: it takes the head's
825
+ * start in place of × (stdrc, 2026-10-22). The sheet is a stack of steps, and
826
+ * Back undoes one, as on a pushed page; Esc still closes. Put it in
827
+ * `DialogHeader` beside the `DialogClose`, from the second step on; it draws
828
+ * nothing outside a sheet, where Back stays in the footer. Its icon is
829
+ * `arrow-left`, as every Back (Alignment, "The ends of a header").
830
+ */
831
+ function DialogSheetBack({ className, children, "aria-label": ariaLabel, ...props }) {
832
+ if (!useInDialogSheetHead()) return null;
833
+ return /* @__PURE__ */ jsx(PanelAction, {
834
+ "data-slot": "dialog-sheet-back",
835
+ "aria-label": ariaLabel ?? "Back",
836
+ className,
837
+ ...props,
838
+ children: children ?? /* @__PURE__ */ jsx(ArrowLeft, {})
839
+ });
840
+ }
841
+ function DialogHeader({ className, bar = false, children, ...props }) {
842
+ const { sheet, setCommitSlot } = useDialogSheet();
843
+ const items = sheet ? Children.toArray(children) : [];
844
+ const backs = items.filter((item) => isValidElement(item) && item.type === DialogSheetBack);
845
+ const closes = items.filter((item) => isValidElement(item) && item.type === DialogClose);
846
+ const missingLeading = sheet && backs.length === 0 && closes.length === 0;
847
+ useEffect(() => {
848
+ if (process.env.NODE_ENV === "production" || !missingLeading) return;
849
+ console.warn("raft-ui: a DialogHeader drawn as a phone sheet has no DialogClose (or DialogSheetBack) as a direct child, so the sheet has no × at the start of its head. Put DialogClose directly in DialogHeader, not inside a Tooltip, a Fragment or a component of your own.");
850
+ }, [missingLeading]);
851
+ if (sheet) {
852
+ const rest = items.filter((item) => !backs.includes(item) && !closes.includes(item));
853
+ const head = dialogSheet();
854
+ return /* @__PURE__ */ jsxs("div", {
855
+ "data-slot": "dialog-header",
856
+ "data-sheet": "",
857
+ className: dialog({ bar: false }).header({ className: cn(head.header(), className) }),
858
+ ...props,
859
+ children: [/* @__PURE__ */ jsxs(DialogSheetHeadContext.Provider, {
860
+ value: true,
861
+ children: [backs.length > 0 ? backs : closes, rest]
862
+ }), /* @__PURE__ */ jsx("div", {
863
+ ref: setCommitSlot,
864
+ "data-slot": "dialog-commit-slot",
865
+ className: head.commitSlot()
866
+ })]
867
+ });
868
+ }
545
869
  const { header } = dialog({ bar });
546
870
  return /* @__PURE__ */ jsx("div", {
547
871
  "data-slot": "dialog-header",
548
872
  className: header({ className }),
549
- ...props
873
+ ...props,
874
+ children
550
875
  });
551
876
  }
552
- function DialogBody({ className, bar = false, ...props }) {
877
+ function DialogBody({ className, bar = false, children, ...props }) {
878
+ const { sheet, setBodyEndSlot } = useDialogSheet();
553
879
  const { body } = dialog({ bar });
554
- return /* @__PURE__ */ jsx("div", {
880
+ const sheetBody = sheet ? cn(bar && dialogSheet().barBody(), dialogSheet().body()) : void 0;
881
+ return /* @__PURE__ */ jsxs("div", {
555
882
  "data-slot": "dialog-body",
556
- className: body({ className }),
557
- ...props
883
+ className: body({ className: sheetBody ? cn(sheetBody, className) : className }),
884
+ ...props,
885
+ children: [children, sheet ? /* @__PURE__ */ jsx("div", {
886
+ ref: setBodyEndSlot,
887
+ "data-slot": "dialog-sheet-body-end",
888
+ className: dialogSheet().bodyEnd()
889
+ }) : null]
558
890
  });
559
891
  }
560
- function DialogFooter({ className, bar = false, ...props }) {
561
- const { footer } = dialog({ bar });
562
- return /* @__PURE__ */ jsx("div", {
892
+ function DialogFooter({ className, bar = false, hidden, children, ref, ...props }) {
893
+ const { sheet, bodyEndSlot } = useDialogSheet();
894
+ const items = Children.toArray(children);
895
+ const start = items.find((item) => isValidElement(item) && item.type === DialogFooterNote && hasNoteContent(item.props.children)) ?? (isValidElement(items[0]) && items[0].type === TextLink ? items[0] : void 0);
896
+ const [stacked, rowRef] = useStackedActions(start == null ? ref : void 0, start != null);
897
+ const { footer, footerActions } = dialog({
898
+ bar,
899
+ start: start != null
900
+ });
901
+ const stackedProps = stacked ? { "data-stacked": "" } : {};
902
+ if (start == null) return /* @__PURE__ */ jsx("div", {
563
903
  "data-slot": "dialog-footer",
904
+ ref: rowRef,
564
905
  className: footer({ className }),
565
- ...props
906
+ hidden: sheet || hidden,
907
+ ...stackedProps,
908
+ ...props,
909
+ children
910
+ });
911
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [sheet && bodyEndSlot ? createPortal(start, bodyEndSlot) : null, /* @__PURE__ */ jsxs("div", {
912
+ "data-slot": "dialog-footer",
913
+ ref,
914
+ className: footer({ className }),
915
+ hidden: sheet || hidden,
916
+ ...props,
917
+ children: [sheet ? null : start, /* @__PURE__ */ jsx("div", {
918
+ "data-slot": "dialog-footer-actions",
919
+ ref: rowRef,
920
+ className: footerActions(),
921
+ ...stackedProps,
922
+ children: items.filter((item) => item !== start)
923
+ })]
924
+ })] });
925
+ }
926
+ /**
927
+ * Whether a note has anything to show: not only null, false or "", but also
928
+ * whitespace and an empty array (`{items.map(...)}` of nothing). Numbers,
929
+ * 0 included, and elements count.
930
+ */
931
+ function hasNoteContent(children) {
932
+ return Children.toArray(children).some((child) => typeof child !== "string" || child.trim() !== "");
933
+ }
934
+ /**
935
+ * One short line at the start of `DialogFooter` that says what the commit
936
+ * button will do ("You'll be billed $8 per member each month"). Muted footnote
937
+ * text, start-aligned and never cut. Only in a `lg` or `wide` dialog, and at
938
+ * most 40 characters: on a desktop it is one line beside the buttons and never
939
+ * wraps; on a phone it goes above the buttons when they don't fit beside it,
940
+ * and may wrap to 2 lines. In a phone sheet, which has no footer, it moves
941
+ * to the end of the body. Longer copy belongs in the body. A footer holds a note or a `TextLink`,
942
+ * never both.
943
+ */
944
+ function DialogFooterNote({ className, children, ...props }) {
945
+ const { footerNote } = dialog();
946
+ const size = useContext(DialogSizeContext);
947
+ useEffect(() => {
948
+ if (process.env.NODE_ENV === "production") return;
949
+ if (size === "sm" || size === "md") console.warn(`raft-ui: a DialogFooterNote is in a size="${size}" dialog. A footer note fits beside the buttons only in a size="lg" or size="wide" dialog; put the sentence in DialogBody instead.`);
950
+ }, [size]);
951
+ if (!hasNoteContent(children)) return null;
952
+ return /* @__PURE__ */ jsx("p", {
953
+ "data-slot": "dialog-footer-note",
954
+ className: footerNote({ className }),
955
+ ...props,
956
+ children
566
957
  });
567
958
  }
568
959
  function DialogTitle({ className, ...props }) {
960
+ const inSheetHead = useInDialogSheetHead();
569
961
  const { title } = dialog();
962
+ const classes = inSheetHead ? cn(dialogSheet().title(), className) : className;
570
963
  return /* @__PURE__ */ jsx(Dialog.Title, {
571
964
  "data-slot": "dialog-title",
572
- className: title({ className }),
965
+ className: title({ className: classes }),
573
966
  ...props
574
967
  });
575
968
  }
@@ -593,7 +986,7 @@ const dialogSurface = tv$1({ slots: { root: [
593
986
  * animates itself: the host centres it, and a centred modal has no motion.
594
987
  * DialogContent draws the same frame for a Base UI dialog.
595
988
  */
596
- function DialogSurface({ className, render, ...props }) {
989
+ function DialogSurface({ className, render, children, ...props }) {
597
990
  const { root } = dialogSurface();
598
991
  return useRender({
599
992
  defaultTagName: "div",
@@ -601,7 +994,7 @@ function DialogSurface({ className, render, ...props }) {
601
994
  props: mergeProps({
602
995
  "data-slot": "dialog-surface",
603
996
  className: root({ className })
604
- }, props)
997
+ }, props, { children: /* @__PURE__ */ jsx(DialogSheetReset, { children }) })
605
998
  });
606
999
  }
607
1000
  const dialogNav = tv$1({ slots: { root: ["flex shrink-0 flex-col overflow-y-auto", "w-52 gap-0.5 border-r border-line-seam p-2 max-md:hidden"] } });
@@ -618,6 +1011,206 @@ function DialogNav({ className, render, ...props }) {
618
1011
  });
619
1012
  }
620
1013
  //#endregion
1014
+ //#region src/lib/use-mobile-viewport.ts
1015
+ const MOBILE_QUERY = "(max-width: 767px)";
1016
+ function subscribeMobileViewport(onStoreChange) {
1017
+ if (typeof window === "undefined") return () => void 0;
1018
+ const media = window.matchMedia(MOBILE_QUERY);
1019
+ media.addEventListener("change", onStoreChange);
1020
+ return () => media.removeEventListener("change", onStoreChange);
1021
+ }
1022
+ function getMobileViewportSnapshot() {
1023
+ return typeof window !== "undefined" && window.matchMedia(MOBILE_QUERY).matches;
1024
+ }
1025
+ function useMobileViewport() {
1026
+ return useSyncExternalStore(subscribeMobileViewport, getMobileViewportSnapshot, () => false);
1027
+ }
1028
+ //#endregion
1029
+ //#region src/components/dialog/dialog-sheet.tsx
1030
+ /**
1031
+ * DialogContent for a dialog with several fields, a long list or steps. On a
1032
+ * desktop and a tablet it is DialogContent, the same DOM and classes. Below
1033
+ * 768px it is a sheet: docked to the bottom, 90% of the screen tall, sliding
1034
+ * up over its scrim; `DialogHeader` becomes a pushed page's head (× at the
1035
+ * start, the title, the `DialogCommit` button at the end), `DialogFooter` is
1036
+ * hidden (× is Cancel) and `DialogBody` scrolls on the phone's 16px margin
1037
+ * above the home indicator. Base UI keeps Esc, focus and close.
1038
+ */
1039
+ function DialogSheetContent({ className, overlay, style, ref, children, ...props }) {
1040
+ const phone = useMobileViewport();
1041
+ const [commitSlot, setCommitSlot] = useState(null);
1042
+ const [bodyEndSlot, setBodyEndSlot] = useState(null);
1043
+ const [popup, setPopup] = useState(null);
1044
+ const keyboardBox = useSheetKeyboardBox(phone);
1045
+ useRevealNewProblems(phone ? popup : null);
1046
+ const state = useMemo(() => ({
1047
+ sheet: phone,
1048
+ commitSlot: phone ? commitSlot : null,
1049
+ setCommitSlot,
1050
+ bodyEndSlot: phone ? bodyEndSlot : null,
1051
+ setBodyEndSlot
1052
+ }), [
1053
+ phone,
1054
+ commitSlot,
1055
+ bodyEndSlot
1056
+ ]);
1057
+ const setRef = useCallback((element) => {
1058
+ setPopup(element);
1059
+ assignRef$1(ref, element);
1060
+ }, [ref]);
1061
+ const sheet = dialogSheet();
1062
+ const sheetOverlay = useMemo(() => overlay === false ? false : {
1063
+ ...typeof overlay === "object" ? overlay : null,
1064
+ className: cn(dialogSheet().overlay(), typeof overlay === "object" ? overlay.className : ""),
1065
+ children: /* @__PURE__ */ jsx("span", {
1066
+ "aria-hidden": true,
1067
+ className: "absolute inset-0"
1068
+ })
1069
+ }, [overlay]);
1070
+ const sheetStyle = useMemo(() => keyboardBox ? {
1071
+ ...style,
1072
+ ...keyboardBox
1073
+ } : style, [style, keyboardBox]);
1074
+ return /* @__PURE__ */ jsx(DialogContent, {
1075
+ ...props,
1076
+ ref: setRef,
1077
+ overlay: phone ? sheetOverlay : overlay,
1078
+ className: phone ? cn(className, sheet.content()) : className,
1079
+ style: phone ? sheetStyle : style,
1080
+ "data-sheet": phone ? "" : void 0,
1081
+ children: /* @__PURE__ */ jsx(DialogSheetContext.Provider, {
1082
+ value: state,
1083
+ children
1084
+ })
1085
+ });
1086
+ }
1087
+ function assignRef$1(ref, value) {
1088
+ if (typeof ref === "function") ref(value);
1089
+ else if (ref) ref.current = value;
1090
+ }
1091
+ /**
1092
+ * The dialog's one commit button: a `Button`, written where it goes on a
1093
+ * desktop (the end of `DialogFooter`). In a `DialogSheetContent` below 768px
1094
+ * it is drawn at the end of the sheet's head instead, the same button (color,
1095
+ * words, disabled and loading state) at `size="md"`. In a flow of steps, write
1096
+ * each step's button as one (Next, then the last step's action).
1097
+ *
1098
+ * A submit button stays its form's default button in the head: it is tied to
1099
+ * the form it was written in by the form's `id` (the `form` attribute), so a
1100
+ * click and Enter in a field submit, and neither does while it is disabled.
1101
+ * Give that form an `id`, or pass `form`. Without one a click still submits
1102
+ * the form, but Enter in a field doesn't.
1103
+ */
1104
+ function DialogCommit({ size, form, onClick, ...props }) {
1105
+ const { commitSlot } = useDialogSheet();
1106
+ const anchorRef = useRef(null);
1107
+ const [writtenIn, setWrittenIn] = useState(null);
1108
+ useLayoutEffect(() => {
1109
+ if (!commitSlot || form) return;
1110
+ const found = anchorRef.current?.closest("form") ?? null;
1111
+ setWrittenIn(found);
1112
+ if (process.env.NODE_ENV === "production" || props.type !== "submit" || found?.id) return;
1113
+ console.warn(found ? "raft-ui: a submit DialogCommit is drawn in a phone sheet's head, outside its form, and the form has no id, so Enter in a field doesn't submit it (a click still does). Give the form an id, or pass `form` to DialogCommit." : "raft-ui: a submit DialogCommit in a phone sheet isn't inside a form and has no `form`, so it submits nothing. Pass `form` with the form's id.");
1114
+ }, [
1115
+ commitSlot,
1116
+ form,
1117
+ props.type
1118
+ ]);
1119
+ const handleClick = useCallback((event) => {
1120
+ onClick?.(event);
1121
+ if (!event.defaultPrevented && props.type === "submit" && writtenIn && !writtenIn.id) {
1122
+ event.preventDefault();
1123
+ writtenIn.requestSubmit();
1124
+ }
1125
+ }, [
1126
+ onClick,
1127
+ props.type,
1128
+ writtenIn
1129
+ ]);
1130
+ if (!commitSlot) return /* @__PURE__ */ jsx(Button$1, {
1131
+ size,
1132
+ form,
1133
+ onClick,
1134
+ ...props
1135
+ });
1136
+ const linkedForm = form ?? (writtenIn?.id || void 0);
1137
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
1138
+ ref: anchorRef,
1139
+ hidden: true
1140
+ }), createPortal(/* @__PURE__ */ jsx(Button$1, {
1141
+ size: "md",
1142
+ form: linkedForm,
1143
+ onClick: handleClick,
1144
+ ...props
1145
+ }), commitSlot)] });
1146
+ }
1147
+ /**
1148
+ * The sheet's box while the on-screen keyboard is up: docked to the top of the
1149
+ * keyboard, as tall as the visual viewport allows up to its usual 90% of the
1150
+ * window. With the keyboard up the visual viewport is shorter than that, so
1151
+ * the sheet fills it: the head stays in view and the body scrolls above the
1152
+ * keyboard. `undefined` otherwise (the CSS box).
1153
+ */
1154
+ function useSheetKeyboardBox(active) {
1155
+ const [box, setBox] = useState(void 0);
1156
+ useEffect(() => {
1157
+ const viewport = typeof window === "undefined" ? void 0 : window.visualViewport;
1158
+ if (!active || !viewport) return;
1159
+ const update = () => {
1160
+ const focused = document.activeElement;
1161
+ const keyboardUp = (focused instanceof HTMLInputElement || focused instanceof HTMLTextAreaElement || focused instanceof HTMLElement && focused.isContentEditable) && window.innerHeight - viewport.height > 100;
1162
+ const height = Math.round(Math.min(viewport.height, window.innerHeight * .9));
1163
+ const top = Math.round(Math.max(viewport.offsetTop, 0) + viewport.height - height);
1164
+ setBox((previous) => {
1165
+ if (!keyboardUp) return void 0;
1166
+ return previous?.top === `${top}px` && previous?.height === `${height}px` ? previous : {
1167
+ top: `${top}px`,
1168
+ bottom: "auto",
1169
+ height: `${height}px`
1170
+ };
1171
+ });
1172
+ };
1173
+ viewport.addEventListener("resize", update);
1174
+ viewport.addEventListener("scroll", update, { passive: true });
1175
+ document.addEventListener("focusin", update);
1176
+ document.addEventListener("focusout", update);
1177
+ return () => {
1178
+ viewport.removeEventListener("resize", update);
1179
+ viewport.removeEventListener("scroll", update);
1180
+ document.removeEventListener("focusin", update);
1181
+ document.removeEventListener("focusout", update);
1182
+ };
1183
+ }, [active]);
1184
+ return active ? box : void 0;
1185
+ }
1186
+ /**
1187
+ * The commit is at the top of a sheet, so an error a failed commit adds near
1188
+ * the bottom of a long form would appear out of sight. A problem added after
1189
+ * the sheet opens (an alert, or a warning or destructive Banner) is scrolled
1190
+ * into view.
1191
+ */
1192
+ const PROBLEM_SELECTOR = "[role=alert], [data-slot=banner][data-status=warning], [data-slot=banner][data-status=destructive]";
1193
+ function useRevealNewProblems(popup) {
1194
+ useEffect(() => {
1195
+ if (!popup || typeof MutationObserver === "undefined") return;
1196
+ const observer = new MutationObserver((records) => {
1197
+ for (const record of records) for (const node of record.addedNodes) {
1198
+ if (!(node instanceof Element)) continue;
1199
+ const problem = node.matches(PROBLEM_SELECTOR) ? node : node.querySelector(PROBLEM_SELECTOR);
1200
+ if (problem) {
1201
+ problem.scrollIntoView?.({ block: "nearest" });
1202
+ return;
1203
+ }
1204
+ }
1205
+ });
1206
+ observer.observe(popup, {
1207
+ childList: true,
1208
+ subtree: true
1209
+ });
1210
+ return () => observer.disconnect();
1211
+ }, [popup]);
1212
+ }
1213
+ //#endregion
621
1214
  //#region src/components/badge/badge.tsx
622
1215
  const badge = tv$1({
623
1216
  slots: { root: [
@@ -799,36 +1392,6 @@ function Experimental(props) {
799
1392
  }
800
1393
  const Badge = Object.assign(BadgeRoot, { Experimental });
801
1394
  //#endregion
802
- //#region src/components/text-link/text-link.tsx
803
- const textLinkVariants = tv$1({
804
- base: [
805
- "rounded-[2px]",
806
- "decoration-[1.5px] underline-offset-[3px] not-disabled:hover:underline",
807
- "outline-none focus-visible:shadow-focus",
808
- "disabled:cursor-not-allowed disabled:text-foreground-disabled"
809
- ],
810
- variants: { variant: {
811
- muted: ["font-medium text-foreground-muted", "hover:text-foreground"],
812
- strong: ["font-semibold text-foreground"]
813
- } },
814
- defaultVariants: { variant: "muted" }
815
- });
816
- function TextLink({ variant, className, render, ...props }) {
817
- const defaultProps = {
818
- className: textLinkVariants({
819
- variant,
820
- className
821
- }),
822
- ...render ? {} : { type: "button" }
823
- };
824
- return useRender({
825
- defaultTagName: "button",
826
- render,
827
- props: mergeProps(defaultProps, props),
828
- state: { slot: "text-link" }
829
- });
830
- }
831
- //#endregion
832
1395
  //#region src/components/tooltip/tooltip.tsx
833
1396
  const tooltip = tv$1({
834
1397
  slots: {
@@ -1110,21 +1673,6 @@ function Checkbox$1({ shape = "square", className, ...props }) {
1110
1673
  });
1111
1674
  }
1112
1675
  //#endregion
1113
- //#region src/lib/use-mobile-viewport.ts
1114
- const MOBILE_QUERY = "(max-width: 767px)";
1115
- function subscribeMobileViewport(onStoreChange) {
1116
- if (typeof window === "undefined") return () => void 0;
1117
- const media = window.matchMedia(MOBILE_QUERY);
1118
- media.addEventListener("change", onStoreChange);
1119
- return () => media.removeEventListener("change", onStoreChange);
1120
- }
1121
- function getMobileViewportSnapshot() {
1122
- return typeof window !== "undefined" && window.matchMedia(MOBILE_QUERY).matches;
1123
- }
1124
- function useMobileViewport() {
1125
- return useSyncExternalStore(subscribeMobileViewport, getMobileViewportSnapshot, () => false);
1126
- }
1127
- //#endregion
1128
1676
  //#region src/components/scroll-area/scroll-area.recipe.ts
1129
1677
  const scrollArea = tv$1({ slots: {
1130
1678
  root: ["relative min-h-0"],
@@ -1508,138 +2056,927 @@ function DropdownMenuLabel({ className, ...props }) {
1508
2056
  ...props
1509
2057
  });
1510
2058
  }
1511
- function DropdownMenuShortcut({ className, ...props }) {
1512
- const { shortcut } = dropdownMenu();
1513
- return /* @__PURE__ */ jsx(Kbd, {
1514
- "data-slot": "dropdown-menu-shortcut",
1515
- className: shortcut({ className }),
1516
- ...props
2059
+ function DropdownMenuShortcut({ className, ...props }) {
2060
+ const { shortcut } = dropdownMenu();
2061
+ return /* @__PURE__ */ jsx(Kbd, {
2062
+ "data-slot": "dropdown-menu-shortcut",
2063
+ className: shortcut({ className }),
2064
+ ...props
2065
+ });
2066
+ }
2067
+ function DropdownMenuItemCount({ className, ...props }) {
2068
+ const { itemCount } = dropdownMenu();
2069
+ return /* @__PURE__ */ jsx(Badge, {
2070
+ "data-slot": "dropdown-menu-item-count",
2071
+ className: itemCount({ className }),
2072
+ ...props
2073
+ });
2074
+ }
2075
+ function DropdownMenuSeparator({ className, ...props }) {
2076
+ const { separator } = dropdownMenu();
2077
+ return /* @__PURE__ */ jsx(Menu.Separator, {
2078
+ "data-slot": "dropdown-menu-separator",
2079
+ className: separator({ className }),
2080
+ ...props
2081
+ });
2082
+ }
2083
+ function DropdownMenuSubmenuChevronIcon() {
2084
+ return /* @__PURE__ */ jsx("svg", {
2085
+ width: "12",
2086
+ height: "12",
2087
+ viewBox: "0 0 12 12",
2088
+ "aria-hidden": true,
2089
+ className: "size-3 shrink-0",
2090
+ children: /* @__PURE__ */ jsx("g", {
2091
+ fill: "none",
2092
+ strokeLinecap: "round",
2093
+ strokeLinejoin: "round",
2094
+ strokeWidth: "1",
2095
+ stroke: "currentColor",
2096
+ children: /* @__PURE__ */ jsx("path", { d: "M4.25 2.5L7.75 6l-3.5 3.5" })
2097
+ })
2098
+ });
2099
+ }
2100
+ function DropdownMenuCheckIcon() {
2101
+ return /* @__PURE__ */ jsx("svg", {
2102
+ width: "12",
2103
+ height: "12",
2104
+ viewBox: "0 0 12 12",
2105
+ "aria-hidden": true,
2106
+ className: "size-3 shrink-0",
2107
+ children: /* @__PURE__ */ jsx("g", {
2108
+ fill: "none",
2109
+ strokeLinecap: "round",
2110
+ strokeLinejoin: "round",
2111
+ strokeWidth: "1",
2112
+ stroke: "currentColor",
2113
+ children: /* @__PURE__ */ jsx("path", { d: "M1.75 6.41L4.4 9.25L10.25 2.99" })
2114
+ })
2115
+ });
2116
+ }
2117
+ function DropdownMenuRadioGroup({ ...props }) {
2118
+ return /* @__PURE__ */ jsx(Menu.RadioGroup, {
2119
+ "data-slot": "dropdown-menu-radio-group",
2120
+ ...props
2121
+ });
2122
+ }
2123
+ function DropdownMenuRadioItem({ className, children, ...props }) {
2124
+ const { radioItem, radioLabel, indicator } = dropdownMenu();
2125
+ return /* @__PURE__ */ jsxs(Menu.RadioItem, {
2126
+ "data-slot": "dropdown-menu-radio-item",
2127
+ className: radioItem({ className }),
2128
+ ...props,
2129
+ children: [renderRadioItemChildren(children, radioLabel()), /* @__PURE__ */ jsx("span", {
2130
+ className: indicator(),
2131
+ children: /* @__PURE__ */ jsx(Menu.RadioItemIndicator, { children: /* @__PURE__ */ jsx(DropdownMenuCheckIcon, {}) })
2132
+ })]
2133
+ });
2134
+ }
2135
+ function renderRadioItemChildren(children, className) {
2136
+ if (typeof children === "string" || typeof children === "number") return /* @__PURE__ */ jsx("span", {
2137
+ "data-slot": "dropdown-menu-radio-item-label",
2138
+ className,
2139
+ children
2140
+ });
2141
+ if (Array.isArray(children) && children.every((child) => typeof child === "string")) return /* @__PURE__ */ jsx("span", {
2142
+ "data-slot": "dropdown-menu-radio-item-label",
2143
+ className,
2144
+ children
2145
+ });
2146
+ return children;
2147
+ }
2148
+ function DropdownMenuRadioItemIndicator({ className, ...props }) {
2149
+ return /* @__PURE__ */ jsx(Menu.RadioItemIndicator, {
2150
+ "data-slot": "dropdown-menu-radio-item-indicator",
2151
+ className,
2152
+ ...props
2153
+ });
2154
+ }
2155
+ function DropdownMenuCheckboxItem({ className, children, ...props }) {
2156
+ const { item, indicator } = dropdownMenu();
2157
+ return /* @__PURE__ */ jsxs(Menu.CheckboxItem, {
2158
+ "data-slot": "dropdown-menu-checkbox-item",
2159
+ className: item({ className }),
2160
+ ...props,
2161
+ children: [children, /* @__PURE__ */ jsx("span", {
2162
+ className: indicator(),
2163
+ children: /* @__PURE__ */ jsx(Menu.CheckboxItemIndicator, { children: /* @__PURE__ */ jsx(DropdownMenuCheckIcon, {}) })
2164
+ })]
2165
+ });
2166
+ }
2167
+ function DropdownMenuCheckboxItemIndicator({ className, ...props }) {
2168
+ return /* @__PURE__ */ jsx(Menu.CheckboxItemIndicator, {
2169
+ "data-slot": "dropdown-menu-checkbox-item-indicator",
2170
+ className,
2171
+ ...props
2172
+ });
2173
+ }
2174
+ /**
2175
+ * One choice that is another page, such as the same page in another
2176
+ * language: a link, so it opens in a new tab and can be copied. Its
2177
+ * `href` goes on the item; `current` marks the page being shown.
2178
+ */
2179
+ function DropdownMenuLinkItem({ current = false, className, children, ...props }) {
2180
+ const { radioItem, radioLabel, indicator } = dropdownMenu();
2181
+ return /* @__PURE__ */ jsxs(Menu.LinkItem, {
2182
+ "data-slot": "dropdown-menu-link-item",
2183
+ "aria-current": current ? "page" : void 0,
2184
+ "data-checked": current ? "" : void 0,
2185
+ className: radioItem({ className }),
2186
+ ...props,
2187
+ children: [renderRadioItemChildren(children, radioLabel()), current ? /* @__PURE__ */ jsx("span", {
2188
+ className: indicator(),
2189
+ children: /* @__PURE__ */ jsx(DropdownMenuCheckIcon, {})
2190
+ }) : null]
2191
+ });
2192
+ }
2193
+ //#endregion
2194
+ //#region src/components/mobile-nav/mobile-nav.tsx
2195
+ const mobileNav = tv$1({ slots: {
2196
+ root: [
2197
+ "grid w-full flex-none grid-flow-col auto-cols-fr items-start gap-0",
2198
+ "border-t border-line-seam bg-layer-card px-2 pt-1.5 text-foreground-hint",
2199
+ "pb-[max(8px,min(env(safe-area-inset-bottom,0px),34px))]"
2200
+ ],
2201
+ item: [
2202
+ "relative inline-flex shrink-0 items-center outline-none",
2203
+ "disabled:pointer-events-none disabled:opacity-45",
2204
+ "[&_svg]:shrink-0",
2205
+ "isolate min-w-0 flex-col justify-start gap-[7px] border-0 px-0 pt-1 pb-0",
2206
+ "font-sans text-[10px]/3 font-semibold text-foreground-hint",
2207
+ "*:relative *:z-10 [&_svg]:size-[22px] [&_svg]:stroke-2",
2208
+ "before:pointer-events-none before:absolute before:top-0 before:left-1/2 before:z-0 before:-translate-x-1/2",
2209
+ "before:h-[30px] before:w-14 before:rounded-control before:border-[1.5px] before:border-transparent before:content-['']",
2210
+ "active:before:bg-fill-medium",
2211
+ "data-[selected=true]:font-bold data-[selected=true]:text-foreground",
2212
+ "data-[selected=true]:[&_svg]:text-sticker-ink",
2213
+ "data-[selected=true]:before:border-sticker-ink data-[selected=true]:before:bg-brand data-[selected=true]:before:ring-[1.5px] data-[selected=true]:before:ring-sticker-edge",
2214
+ "data-[selected=true]:active:before:overlay-sticker-medium",
2215
+ "rounded-control focus-visible:shadow-focus"
2216
+ ],
2217
+ itemLabel: ["min-w-0 truncate", "max-w-full"]
2218
+ } });
2219
+ function MobileNavRoot({ className, render, ...props }) {
2220
+ const { root } = mobileNav();
2221
+ const defaultProps = {
2222
+ "data-slot": "mobile-nav-root",
2223
+ className: root({ className })
2224
+ };
2225
+ return useRender({
2226
+ defaultTagName: "nav",
2227
+ render,
2228
+ props: mergeProps(defaultProps, props)
2229
+ });
2230
+ }
2231
+ function MobileNavItem({ nativeButton = true, selected, className, render, ...props }) {
2232
+ const { item } = mobileNav();
2233
+ const defaultProps = {
2234
+ type: nativeButton ? "button" : void 0,
2235
+ "aria-pressed": nativeButton ? selected : void 0,
2236
+ "data-slot": "mobile-nav-item",
2237
+ "data-selected": selected ? "true" : void 0,
2238
+ className: item({ className })
2239
+ };
2240
+ return useRender({
2241
+ render: render ?? /* @__PURE__ */ jsx(Button, {}),
2242
+ props: mergeProps(defaultProps, props)
2243
+ });
2244
+ }
2245
+ function MobileNavLabel({ className, render, ...props }) {
2246
+ const { itemLabel } = mobileNav();
2247
+ const defaultProps = {
2248
+ "data-slot": "mobile-nav-item-label",
2249
+ className: itemLabel({ className })
2250
+ };
2251
+ return useRender({
2252
+ defaultTagName: "span",
2253
+ render,
2254
+ props: mergeProps(defaultProps, props)
2255
+ });
2256
+ }
2257
+ const holds = /* @__PURE__ */ new WeakMap();
2258
+ function touchPoint(event) {
2259
+ const touch = event.touches?.[0] ?? event.changedTouches?.[0];
2260
+ return touch ? {
2261
+ x: touch.clientX,
2262
+ y: touch.clientY
2263
+ } : null;
2264
+ }
2265
+ function farther(from, to, limit) {
2266
+ return Math.hypot(to.x - from.x, to.y - from.y) > limit;
2267
+ }
2268
+ var HoldThenMoveTouchSensorImpl = class extends TouchSensor {
2269
+ static activators = TouchSensor.activators;
2270
+ static setup() {
2271
+ return TouchSensor.setup();
2272
+ }
2273
+ attach() {
2274
+ super.attach();
2275
+ const self = this;
2276
+ const { holdMs = 500 } = self.props.options;
2277
+ const state = {
2278
+ heldAt: null,
2279
+ last: null,
2280
+ timer: setTimeout(() => {
2281
+ state.heldAt = state.last ?? self.initialCoordinates;
2282
+ }, holdMs)
2283
+ };
2284
+ holds.set(this, state);
2285
+ }
2286
+ handleMove(event) {
2287
+ const self = this;
2288
+ const state = holds.get(this);
2289
+ if (self.activated || !state) {
2290
+ super.handleMove(event);
2291
+ return;
2292
+ }
2293
+ if ((event.touches?.length ?? 1) > 1) {
2294
+ self.handleCancel();
2295
+ return;
2296
+ }
2297
+ const point = touchPoint(event);
2298
+ const start = self.initialCoordinates;
2299
+ if (!point || !start) return;
2300
+ state.last = point;
2301
+ const { distance = 10, tolerance = 10 } = self.props.options;
2302
+ if (!state.heldAt) {
2303
+ if (farther(start, point, tolerance)) self.handleCancel();
2304
+ return;
2305
+ }
2306
+ if (event.cancelable) event.preventDefault();
2307
+ if (farther(state.heldAt, point, distance)) self.handleStart();
2308
+ }
2309
+ detach() {
2310
+ const state = holds.get(this);
2311
+ if (state) clearTimeout(state.timer);
2312
+ holds.delete(this);
2313
+ super.detach();
2314
+ }
2315
+ };
2316
+ /** A TouchSensor that starts a drag on the first move after a still hold (see above). */
2317
+ const HoldThenMoveTouchSensor = HoldThenMoveTouchSensorImpl;
2318
+ /**
2319
+ * dnd-kit's PointerSensor for a mouse and a pen only: a finger's pointerdown is
2320
+ * left to `HoldThenMoveTouchSensor`.
2321
+ */
2322
+ var MouseOnlyPointerSensor = class extends PointerSensor {
2323
+ static activators = [{
2324
+ eventName: "onPointerDown",
2325
+ handler: (event, options) => event.nativeEvent.pointerType === "touch" ? false : PointerSensor.activators[0].handler(event, options)
2326
+ }];
2327
+ };
2328
+ /**
2329
+ * `useSensor(HoldThenMoveTouchSensor, …)` with the activation the sensor relies
2330
+ * on: no constraint dnd-kit would act on by itself, so only a move after the
2331
+ * hold starts the drag.
2332
+ */
2333
+ function useHoldThenMoveTouchSensor({ holdMs = 500, distance = 10, tolerance = 10 } = {}) {
2334
+ const options = useMemo(() => ({
2335
+ holdMs,
2336
+ distance,
2337
+ tolerance,
2338
+ activationConstraint: { distance: Number.POSITIVE_INFINITY }
2339
+ }), [
2340
+ holdMs,
2341
+ distance,
2342
+ tolerance
2343
+ ]);
2344
+ return useSensor(HoldThenMoveTouchSensor, options);
2345
+ }
2346
+ /** The mouse / pen sensor that goes with it: picks up after `distance` px. */
2347
+ function useMouseOnlyPointerSensor(distance) {
2348
+ const options = useMemo(() => ({ activationConstraint: { distance } }), [distance]);
2349
+ return useSensor(MouseOnlyPointerSensor, options);
2350
+ }
2351
+ //#endregion
2352
+ //#region src/components/popover/popover.tsx
2353
+ const popover = tv$1({ slots: {
2354
+ trigger: ["select-none"],
2355
+ positioner: ["isolate z-50 outline-none"],
2356
+ content: [
2357
+ "max-h-(--available-height) min-w-[220px] overflow-hidden",
2358
+ "outline-none",
2359
+ "rounded-container bg-layer-popover text-foreground",
2360
+ "shadow-float ring-1 ring-line-seam"
2361
+ ],
2362
+ header: ["flex items-center justify-between px-3", "min-h-11 gap-2 py-1.5 pr-1.5 pl-4"],
2363
+ body: ["flex min-w-0 flex-col gap-1.5", "px-4 pt-4 pb-3 last:pb-4"],
2364
+ actions: ["flex flex-wrap items-center justify-end gap-2", "px-4 pb-3 last:pb-4"],
2365
+ separator: ["border-t", "border-line-hairline"],
2366
+ title: ["m-0", "font-sans text-headline font-bold text-foreground"],
2367
+ description: ["m-0", "font-sans text-footnote text-foreground-muted"],
2368
+ close: [
2369
+ "inline-flex items-center justify-center",
2370
+ "outline-none transition-colors",
2371
+ "h-8 min-w-8 rounded-chip px-1.5",
2372
+ "font-sans text-footnote font-bold text-foreground",
2373
+ "hover:bg-fill-medium active:bg-fill-medium",
2374
+ "focus-visible:shadow-focus",
2375
+ "[&_svg]:size-4.5"
2376
+ ],
2377
+ arrow: ["size-2 rotate-45", "border-l border-t border-line-seam bg-layer-popover"],
2378
+ backdrop: ["fixed inset-0", "bg-transparent"],
2379
+ viewport: ["block"]
2380
+ } });
2381
+ function Popover$1(props) {
2382
+ return /* @__PURE__ */ jsx(Popover.Root, {
2383
+ "data-slot": "popover",
2384
+ ...props
2385
+ });
2386
+ }
2387
+ function PopoverTrigger({ className, ...props }) {
2388
+ const { trigger } = popover();
2389
+ return /* @__PURE__ */ jsx(Popover.Trigger, {
2390
+ "data-slot": "popover-trigger",
2391
+ className: trigger({ className }),
2392
+ ...props
2393
+ });
2394
+ }
2395
+ function PopoverPortal(props) {
2396
+ return /* @__PURE__ */ jsx(Popover.Portal, {
2397
+ "data-slot": "popover-portal",
2398
+ ...useThemeScopeProps(props)
2399
+ });
2400
+ }
2401
+ function PopoverPopup({ className, ...props }) {
2402
+ const { content } = popover();
2403
+ return /* @__PURE__ */ jsx("div", {
2404
+ "data-slot": "popover-content",
2405
+ className: content({ className }),
2406
+ ...props
2407
+ });
2408
+ }
2409
+ function PopoverContent({ align = "start", alignOffset = 0, anchor, arrowPadding, collisionAvoidance, collisionBoundary, collisionPadding, disableAnchorTracking, positionMethod = "fixed", side = "bottom", sideOffset = 4, sticky, container, portalProps, className, ...props }) {
2410
+ const { positioner, content } = popover();
2411
+ const portalContainer = container !== void 0 ? container : portalProps?.container;
2412
+ return /* @__PURE__ */ jsx(PopoverPortal, {
2413
+ ...portalProps,
2414
+ container: portalContainer,
2415
+ children: /* @__PURE__ */ jsx(Popover.Positioner, {
2416
+ align,
2417
+ alignOffset,
2418
+ anchor,
2419
+ arrowPadding,
2420
+ collisionAvoidance,
2421
+ collisionBoundary,
2422
+ collisionPadding,
2423
+ disableAnchorTracking,
2424
+ positionMethod,
2425
+ side,
2426
+ sideOffset,
2427
+ sticky,
2428
+ className: positioner(),
2429
+ children: /* @__PURE__ */ jsx(Popover.Popup, {
2430
+ "data-slot": "popover-content",
2431
+ className: content({ className }),
2432
+ ...props
2433
+ })
2434
+ })
2435
+ });
2436
+ }
2437
+ function PopoverViewport({ className, ...props }) {
2438
+ const { viewport } = popover();
2439
+ return /* @__PURE__ */ jsx(Popover.Viewport, {
2440
+ "data-slot": "popover-viewport",
2441
+ className: viewport({ className }),
2442
+ ...props
2443
+ });
2444
+ }
2445
+ function PopoverHeader({ className, ...props }) {
2446
+ const { header } = popover();
2447
+ return /* @__PURE__ */ jsx("div", {
2448
+ "data-slot": "popover-header",
2449
+ className: header({ className }),
2450
+ ...props
2451
+ });
2452
+ }
2453
+ function PopoverSeparator({ className, ...props }) {
2454
+ const { separator } = popover();
2455
+ return /* @__PURE__ */ jsx("div", {
2456
+ role: "separator",
2457
+ "aria-orientation": "horizontal",
2458
+ "data-slot": "popover-separator",
2459
+ className: separator({ className }),
2460
+ ...props
2461
+ });
2462
+ }
2463
+ function PopoverTitle({ className, ...props }) {
2464
+ const { title } = popover();
2465
+ return /* @__PURE__ */ jsx(Popover.Title, {
2466
+ "data-slot": "popover-title",
2467
+ className: title({ className }),
2468
+ ...props
2469
+ });
2470
+ }
2471
+ function PopoverDescription({ className, ...props }) {
2472
+ const { description } = popover();
2473
+ return /* @__PURE__ */ jsx(Popover.Description, {
2474
+ "data-slot": "popover-description",
2475
+ className: description({ className }),
2476
+ ...props
2477
+ });
2478
+ }
2479
+ function PopoverClose({ className, ...props }) {
2480
+ const { close } = popover();
2481
+ return /* @__PURE__ */ jsx(Popover.Close, {
2482
+ "data-slot": "popover-close",
2483
+ className: close({ className }),
2484
+ ...props
2485
+ });
2486
+ }
2487
+ function PopoverArrow({ className, ...props }) {
2488
+ const { arrow } = popover();
2489
+ return /* @__PURE__ */ jsx(Popover.Arrow, {
2490
+ "data-slot": "popover-arrow",
2491
+ className: arrow({ className }),
2492
+ ...props
2493
+ });
2494
+ }
2495
+ function PopoverBackdrop({ className, ...props }) {
2496
+ const { backdrop } = popover();
2497
+ return /* @__PURE__ */ jsx(Popover.Backdrop, {
2498
+ "data-slot": "popover-backdrop",
2499
+ className: backdrop({ className }),
2500
+ ...props
2501
+ });
2502
+ }
2503
+ /** The panel's text: a `PopoverTitle` and a `PopoverDescription`, 16px in from its edges. */
2504
+ function PopoverBody({ className, ...props }) {
2505
+ const { body } = popover();
2506
+ return /* @__PURE__ */ jsx("div", {
2507
+ "data-slot": "popover-body",
2508
+ className: body({ className }),
2509
+ ...props
2510
+ });
2511
+ }
2512
+ /** The panel's buttons, at its end, under `PopoverBody`: the way out first, then the confirm button. */
2513
+ function PopoverActions({ className, ...props }) {
2514
+ const { actions } = popover();
2515
+ return /* @__PURE__ */ jsx("div", {
2516
+ "data-slot": "popover-actions",
2517
+ className: actions({ className }),
2518
+ ...props
2519
+ });
2520
+ }
2521
+ //#endregion
2522
+ //#region src/components/mobile-nav/mobile-nav-server.tsx
2523
+ const mobileNavServer = tv$1({ slots: {
2524
+ item: [
2525
+ "select-none [-webkit-touch-callout:none]",
2526
+ "*:data-[slot=avatar]:size-[22px]",
2527
+ "data-popup-open:before:bg-fill-medium"
2528
+ ],
2529
+ content: [
2530
+ "flex flex-col",
2531
+ "w-[calc(var(--anchor-width)-16px)] min-w-0",
2532
+ "max-h-[min(60dvh,calc(var(--available-height)-env(safe-area-inset-top,0px)))]"
2533
+ ],
2534
+ list: ["flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain p-1"],
2535
+ group: ["flex shrink-0 flex-col p-1"],
2536
+ row: [
2537
+ "flex w-full shrink-0 cursor-default select-none items-center",
2538
+ "text-left outline-none transition-colors",
2539
+ "min-h-option gap-2.5 rounded-chip px-2.5 py-option-inset",
2540
+ "font-sans text-callout font-medium text-foreground",
2541
+ "hover:bg-brand-soft active:bg-brand-soft focus-visible:bg-brand-soft",
2542
+ "aria-[current=true]:font-bold",
2543
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&>svg:not([class*='size-'])]:size-4",
2544
+ "[&>svg]:text-foreground-muted",
2545
+ "*:data-[slot=avatar]:size-5 *:data-[slot=avatar]:shrink-0"
2546
+ ],
2547
+ rowLabel: ["min-w-0 flex-1 truncate"],
2548
+ rowBody: ["flex min-w-0 flex-1 flex-col"],
2549
+ rowDescription: ["min-w-0 truncate", "font-mono text-mono-meta font-normal text-foreground-hint"],
2550
+ destinations: ["grid shrink-0 grid-flow-col auto-cols-fr gap-1 p-1"],
2551
+ destination: [
2552
+ "relative flex min-w-0 cursor-default select-none flex-col items-center justify-center",
2553
+ "outline-none transition-colors",
2554
+ "gap-1 rounded-chip px-1 py-2",
2555
+ "font-sans text-footnote font-medium text-foreground",
2556
+ "hover:bg-brand-soft active:bg-brand-soft focus-visible:bg-brand-soft",
2557
+ "[&_svg]:pointer-events-none [&_svg]:size-5 [&_svg]:shrink-0 [&_svg]:text-foreground-muted",
2558
+ "[&>span]:max-w-full [&>span]:truncate",
2559
+ "*:data-[slot=status]:absolute *:data-[slot=status]:top-1 *:data-[slot=status]:left-[calc(50%+6px)]"
2560
+ ]
2561
+ } });
2562
+ const MobileNavServerContext = createContext(null);
2563
+ function useMobileNavServerContext(part) {
2564
+ const context = use(MobileNavServerContext);
2565
+ if (!context) throw new Error(`${part} must be inside MobileNavServerPopover`);
2566
+ return context;
2567
+ }
2568
+ /** The root: owns the popover's open state, like `Popover` (`open`, `onOpenChange`). */
2569
+ function MobileNavServerPopover(props) {
2570
+ const [handle] = useState(() => Popover.createHandle());
2571
+ const triggerRef = useRef(null);
2572
+ const context = useMemo(() => ({
2573
+ handle,
2574
+ triggerRef
2575
+ }), [handle]);
2576
+ return /* @__PURE__ */ jsx(MobileNavServerContext, {
2577
+ value: context,
2578
+ children: /* @__PURE__ */ jsx(Popover.Root, {
2579
+ "data-slot": "mobile-nav-server-popover",
2580
+ handle,
2581
+ ...props
2582
+ })
2583
+ });
2584
+ }
2585
+ /**
2586
+ * The tab: the server's avatar and its name in `MobileNavLabel`, as children.
2587
+ * A press opens the popover, and so does holding it for 500ms (the long press
2588
+ * of raft-ui's menus). Lifting the finger after a hold never closes it: the
2589
+ * click that ends the hold is not a press, whether the hold opened the
2590
+ * popover or found it open.
2591
+ * It is never `selected`: it holds no page.
2592
+ */
2593
+ function MobileNavServerItem({ className, id, ref, onClick, onContextMenu, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onPointerLeave, ...props }) {
2594
+ const { handle, triggerRef } = useMobileNavServerContext("MobileNavServerItem");
2595
+ const { item } = mobileNavServer();
2596
+ const generatedId = useId();
2597
+ const triggerId = id ?? generatedId;
2598
+ const holdTimer = useRef(void 0);
2599
+ const holdStart = useRef(null);
2600
+ const held = useRef(false);
2601
+ const cancelHold = useCallback(() => {
2602
+ clearTimeout(holdTimer.current);
2603
+ holdTimer.current = void 0;
2604
+ holdStart.current = null;
2605
+ }, []);
2606
+ useEffect(() => cancelHold, [cancelHold]);
2607
+ const setRef = useCallback((node) => {
2608
+ triggerRef.current = node;
2609
+ assignRef(ref, node);
2610
+ }, [ref, triggerRef]);
2611
+ const handlePointerDown = useCallback((event) => {
2612
+ onPointerDown?.(event);
2613
+ cancelHold();
2614
+ held.current = false;
2615
+ if (event.button !== 0) return;
2616
+ holdStart.current = {
2617
+ x: event.clientX,
2618
+ y: event.clientY
2619
+ };
2620
+ holdTimer.current = setTimeout(() => {
2621
+ holdTimer.current = void 0;
2622
+ held.current = true;
2623
+ if (!handle.isOpen) handle.open(triggerId);
2624
+ }, 500);
2625
+ }, [
2626
+ cancelHold,
2627
+ handle,
2628
+ onPointerDown,
2629
+ triggerId
2630
+ ]);
2631
+ const handlePointerMove = useCallback((event) => {
2632
+ onPointerMove?.(event);
2633
+ const start = holdStart.current;
2634
+ if (!start) return;
2635
+ if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > 10) cancelHold();
2636
+ }, [cancelHold, onPointerMove]);
2637
+ const handlePointerUp = useCallback((event) => {
2638
+ onPointerUp?.(event);
2639
+ cancelHold();
2640
+ }, [cancelHold, onPointerUp]);
2641
+ const handlePointerCancel = useCallback((event) => {
2642
+ onPointerCancel?.(event);
2643
+ cancelHold();
2644
+ }, [cancelHold, onPointerCancel]);
2645
+ const handlePointerLeave = useCallback((event) => {
2646
+ onPointerLeave?.(event);
2647
+ cancelHold();
2648
+ }, [cancelHold, onPointerLeave]);
2649
+ const handleClick = useCallback((event) => {
2650
+ onClick?.(event);
2651
+ if (!held.current) return;
2652
+ held.current = false;
2653
+ event.preventBaseUIHandler?.();
2654
+ }, [onClick]);
2655
+ const handleContextMenu = useCallback((event) => {
2656
+ onContextMenu?.(event);
2657
+ event.preventDefault();
2658
+ }, [onContextMenu]);
2659
+ return /* @__PURE__ */ jsx(Popover.Trigger, {
2660
+ handle,
2661
+ id: triggerId,
2662
+ ref: setRef,
2663
+ render: /* @__PURE__ */ jsx(MobileNavItem, {}),
2664
+ "data-slot": "mobile-nav-server-item",
2665
+ className: item({ className }),
2666
+ onPointerDown: handlePointerDown,
2667
+ onPointerMove: handlePointerMove,
2668
+ onPointerUp: handlePointerUp,
2669
+ onPointerCancel: handlePointerCancel,
2670
+ onPointerLeave: handlePointerLeave,
2671
+ onClick: handleClick,
2672
+ onContextMenu: handleContextMenu,
2673
+ ...props
2674
+ });
2675
+ }
2676
+ /**
2677
+ * The popover above the tab bar, 8px from each side of it and 8px above it,
2678
+ * as tall as its content and at most 60% of the screen. It stops below the
2679
+ * top safe area (a notch in a standalone web app): Base UI's collision padding
2680
+ * takes only pixels, so the height cap subtracts `env(safe-area-inset-top)`.
2681
+ * The bottom needs none: the tab bar under the popover already pads the
2682
+ * bottom safe area. A column:
2683
+ * `MobileNavServerPopoverList` takes the room left and scrolls, and the
2684
+ * destinations and the footer after it stay in view.
2685
+ */
2686
+ function MobileNavServerPopoverContent({ anchor, side = "top", align = "center", sideOffset = 8, collisionPadding = 8, className, ...props }) {
2687
+ const { triggerRef } = useMobileNavServerContext("MobileNavServerPopoverContent");
2688
+ const { content } = mobileNavServer();
2689
+ const tabBar = useCallback(() => {
2690
+ const trigger = triggerRef.current;
2691
+ return trigger?.closest("[data-slot=mobile-nav-root]") ?? trigger;
2692
+ }, [triggerRef]);
2693
+ return /* @__PURE__ */ jsx(PopoverContent, {
2694
+ "data-slot": "mobile-nav-server-popover-content",
2695
+ anchor: anchor ?? tabBar,
2696
+ side,
2697
+ align,
2698
+ sideOffset,
2699
+ collisionPadding,
2700
+ className: content({ className }),
2701
+ ...props
2702
+ });
2703
+ }
2704
+ /** The server list: takes the room left in the popover and scrolls inside it. */
2705
+ function MobileNavServerPopoverList({ className, ...props }) {
2706
+ const { list } = mobileNavServer();
2707
+ return /* @__PURE__ */ jsx("div", {
2708
+ "data-slot": "mobile-nav-server-popover-list",
2709
+ className: list({ className }),
2710
+ ...props
2711
+ });
2712
+ }
2713
+ /** Rows that always stay in view, such as the footer actions. */
2714
+ function MobileNavServerPopoverGroup({ className, ...props }) {
2715
+ const { group } = mobileNavServer();
2716
+ return /* @__PURE__ */ jsx("div", {
2717
+ "data-slot": "mobile-nav-server-popover-group",
2718
+ className: group({ className }),
2719
+ ...props
2720
+ });
2721
+ }
2722
+ /**
2723
+ * One row at the option height of every popup list (`py-option-inset`, 36px
2724
+ * for one line; a second line adds its own height and nothing else): a 20px
2725
+ * avatar or a 16px icon, `MobileNavServerPopoverRowLabel` (or a
2726
+ * `MobileNavServerPopoverRowBody` with a description under the label), then
2727
+ * anything trailing (a check, a count), centered on the row.
2728
+ * `aria-current="true"` marks the current server in bold.
2729
+ */
2730
+ function MobileNavServerPopoverRow({ className, type = "button", ...props }) {
2731
+ const { row } = mobileNavServer();
2732
+ return /* @__PURE__ */ jsx("button", {
2733
+ type,
2734
+ "data-slot": "mobile-nav-server-popover-row",
2735
+ className: row({ className }),
2736
+ ...props
2737
+ });
2738
+ }
2739
+ /** A row's label: takes the room and truncates. */
2740
+ function MobileNavServerPopoverRowLabel({ className, ...props }) {
2741
+ const { rowLabel } = mobileNavServer();
2742
+ return /* @__PURE__ */ jsx("span", {
2743
+ "data-slot": "mobile-nav-server-popover-row-label",
2744
+ className: rowLabel({ className }),
2745
+ ...props
2746
+ });
2747
+ }
2748
+ /** The server's own pages, side by side in equal cells; a `group`, named with `aria-label`. */
2749
+ function MobileNavServerPopoverDestinations({ className, ...props }) {
2750
+ const { destinations } = mobileNavServer();
2751
+ return /* @__PURE__ */ jsx("div", {
2752
+ role: "group",
2753
+ "data-slot": "mobile-nav-server-popover-destinations",
2754
+ className: destinations({ className }),
2755
+ ...props
2756
+ });
2757
+ }
2758
+ /**
2759
+ * One of the server's pages: a 20px icon over a short label in a `span`, which
2760
+ * truncates. Something new on that page (a feedback reply, a computer that
2761
+ * needs attention) is an `accent` `Status` child, the attention dot at the
2762
+ * icon's top-right corner: the server tab's avatar never carries it.
2763
+ */
2764
+ function MobileNavServerPopoverDestination({ className, type = "button", ...props }) {
2765
+ const { destination } = mobileNavServer();
2766
+ return /* @__PURE__ */ jsx("button", {
2767
+ type,
2768
+ "data-slot": "mobile-nav-server-popover-destination",
2769
+ className: destination({ className }),
2770
+ ...props
2771
+ });
2772
+ }
2773
+ /**
2774
+ * The text of a two-line row: `MobileNavServerPopoverRowLabel`, then
2775
+ * `MobileNavServerPopoverRowDescription` under it. The second line adds its
2776
+ * own height and nothing else; the avatar and the trailing check or count
2777
+ * center on both lines.
2778
+ */
2779
+ function MobileNavServerPopoverRowBody({ className, ...props }) {
2780
+ const { rowBody } = mobileNavServer();
2781
+ return /* @__PURE__ */ jsx("span", {
2782
+ "data-slot": "mobile-nav-server-popover-row-body",
2783
+ className: rowBody({ className }),
2784
+ ...props
2785
+ });
2786
+ }
2787
+ /** A row's second line, such as a server's address: muted Space Mono; truncates. */
2788
+ function MobileNavServerPopoverRowDescription({ className, ...props }) {
2789
+ const { rowDescription } = mobileNavServer();
2790
+ return /* @__PURE__ */ jsx("span", {
2791
+ "data-slot": "mobile-nav-server-popover-row-description",
2792
+ className: rowDescription({ className }),
2793
+ ...props
2794
+ });
2795
+ }
2796
+ function assignRef(ref, value) {
2797
+ if (typeof ref === "function") ref(value);
2798
+ else if (ref) ref.current = value;
2799
+ }
2800
+ //#endregion
2801
+ //#region src/components/preview-card/preview-card.tsx
2802
+ const previewCard = tv$1({ slots: {
2803
+ trigger: [],
2804
+ positioner: ["isolate z-50 outline-none"],
2805
+ popup: [
2806
+ "max-h-(--available-height) min-w-[220px] overflow-hidden",
2807
+ "outline-none",
2808
+ "origin-(--transform-origin) transform-gpu opacity-100 will-change-[transform,opacity]",
2809
+ "transition-[opacity,transform] duration-250 ease-[cubic-bezier(0.22,1,0.36,1)]",
2810
+ "data-starting-style:scale-[0.97] data-starting-style:opacity-0",
2811
+ "data-ending-style:scale-[0.99] data-ending-style:opacity-0 data-ending-style:duration-150",
2812
+ "data-instant:transition-none motion-reduce:transition-none",
2813
+ "w-[300px] rounded-container bg-layer-popover text-foreground",
2814
+ "shadow-float ring-1 ring-line-seam"
2815
+ ],
2816
+ header: ["flex items-start gap-3", "px-4 pt-4 pb-3 last:pb-4"],
2817
+ separator: ["border-t", "border-line-hairline"],
2818
+ title: ["m-0 truncate", "font-sans text-headline font-bold text-foreground"],
2819
+ description: ["m-0 truncate", "px-4 py-3 last:pb-4 font-sans text-callout text-foreground-muted"],
2820
+ arrow: ["size-2 rotate-45", "border-l border-t border-line-seam bg-layer-popover"],
2821
+ body: [
2822
+ "flex min-w-0 flex-col gap-1.5",
2823
+ "px-4 pt-4 pb-3 last:pb-4",
2824
+ "*:data-[slot=preview-card-title]:line-clamp-3 *:data-[slot=preview-card-title]:whitespace-normal"
2825
+ ],
2826
+ eyebrow: ["m-0 truncate", "font-mono text-mono-meta text-foreground-muted"],
2827
+ meta: ["flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", "m-0 font-sans text-callout text-foreground-muted"],
2828
+ actions: ["flex flex-wrap items-center gap-2", "px-4 pb-3 last:pb-4"],
2829
+ footer: [
2830
+ "m-0 truncate",
2831
+ "border-t border-line-hairline px-4 py-2",
2832
+ "font-sans text-caption text-foreground-muted"
2833
+ ]
2834
+ } });
2835
+ function PreviewCard$1(props) {
2836
+ return /* @__PURE__ */ jsx(PreviewCard.Root, {
2837
+ "data-slot": "preview-card",
2838
+ ...props
2839
+ });
2840
+ }
2841
+ function PreviewCardTrigger({ delay = 80, closeDelay = 40, className, ...props }) {
2842
+ const { trigger } = previewCard();
2843
+ return /* @__PURE__ */ jsx(PreviewCard.Trigger, {
2844
+ "data-slot": "preview-card-trigger",
2845
+ delay,
2846
+ closeDelay,
2847
+ className: trigger({ className }),
2848
+ ...props
2849
+ });
2850
+ }
2851
+ function PreviewCardPortal(props) {
2852
+ return /* @__PURE__ */ jsx(PreviewCard.Portal, {
2853
+ "data-slot": "preview-card-portal",
2854
+ ...useThemeScopeProps(props)
2855
+ });
2856
+ }
2857
+ function PreviewCardPopup({ className, ...props }) {
2858
+ const themeState = useTheme();
2859
+ const { popup } = previewCard();
2860
+ const scopeProps = resolveThemeScopeProps({
2861
+ ...props,
2862
+ className: popup({ className })
2863
+ }, themeState);
2864
+ return /* @__PURE__ */ jsx("div", {
2865
+ "data-slot": "preview-card-content",
2866
+ ...scopeProps
2867
+ });
2868
+ }
2869
+ function PreviewCardContent({ align = "start", alignOffset = 0, anchor, arrowPadding, collisionAvoidance, collisionBoundary, collisionPadding, disableAnchorTracking, positionMethod = "fixed", side = "bottom", sideOffset = 6, sticky, portalProps, className, ...props }) {
2870
+ const { positioner, popup } = previewCard();
2871
+ return /* @__PURE__ */ jsx(PreviewCardPortal, {
2872
+ ...portalProps,
2873
+ children: /* @__PURE__ */ jsx(PreviewCard.Positioner, {
2874
+ align,
2875
+ alignOffset,
2876
+ anchor,
2877
+ arrowPadding,
2878
+ collisionAvoidance,
2879
+ collisionBoundary,
2880
+ collisionPadding,
2881
+ disableAnchorTracking,
2882
+ positionMethod,
2883
+ side,
2884
+ sideOffset,
2885
+ sticky,
2886
+ className: positioner(),
2887
+ children: /* @__PURE__ */ jsx(PreviewCard.Popup, {
2888
+ "data-slot": "preview-card-content",
2889
+ className: popup({ className }),
2890
+ ...props
2891
+ })
2892
+ })
1517
2893
  });
1518
2894
  }
1519
- function DropdownMenuItemCount({ className, ...props }) {
1520
- const { itemCount } = dropdownMenu();
1521
- return /* @__PURE__ */ jsx(Badge, {
1522
- "data-slot": "dropdown-menu-item-count",
1523
- className: itemCount({ className }),
2895
+ function PreviewCardHeader({ className, ...props }) {
2896
+ const { header } = previewCard();
2897
+ return /* @__PURE__ */ jsx("div", {
2898
+ "data-slot": "preview-card-header",
2899
+ className: header({ className }),
1524
2900
  ...props
1525
2901
  });
1526
2902
  }
1527
- function DropdownMenuSeparator({ className, ...props }) {
1528
- const { separator } = dropdownMenu();
1529
- return /* @__PURE__ */ jsx(Menu.Separator, {
1530
- "data-slot": "dropdown-menu-separator",
2903
+ function PreviewCardSeparator({ className, ...props }) {
2904
+ const { separator } = previewCard();
2905
+ return /* @__PURE__ */ jsx("div", {
2906
+ role: "separator",
2907
+ "aria-orientation": "horizontal",
2908
+ "data-slot": "preview-card-separator",
1531
2909
  className: separator({ className }),
1532
2910
  ...props
1533
2911
  });
1534
2912
  }
1535
- function DropdownMenuSubmenuChevronIcon() {
1536
- return /* @__PURE__ */ jsx("svg", {
1537
- width: "12",
1538
- height: "12",
1539
- viewBox: "0 0 12 12",
1540
- "aria-hidden": true,
1541
- className: "size-3 shrink-0",
1542
- children: /* @__PURE__ */ jsx("g", {
1543
- fill: "none",
1544
- strokeLinecap: "round",
1545
- strokeLinejoin: "round",
1546
- strokeWidth: "1",
1547
- stroke: "currentColor",
1548
- children: /* @__PURE__ */ jsx("path", { d: "M4.25 2.5L7.75 6l-3.5 3.5" })
1549
- })
1550
- });
1551
- }
1552
- function DropdownMenuCheckIcon() {
1553
- return /* @__PURE__ */ jsx("svg", {
1554
- width: "12",
1555
- height: "12",
1556
- viewBox: "0 0 12 12",
1557
- "aria-hidden": true,
1558
- className: "size-3 shrink-0",
1559
- children: /* @__PURE__ */ jsx("g", {
1560
- fill: "none",
1561
- strokeLinecap: "round",
1562
- strokeLinejoin: "round",
1563
- strokeWidth: "1",
1564
- stroke: "currentColor",
1565
- children: /* @__PURE__ */ jsx("path", { d: "M1.75 6.41L4.4 9.25L10.25 2.99" })
1566
- })
2913
+ function PreviewCardTitle({ className, ...props }) {
2914
+ const { title } = previewCard();
2915
+ return /* @__PURE__ */ jsx("p", {
2916
+ "data-slot": "preview-card-title",
2917
+ className: title({ className }),
2918
+ ...props
1567
2919
  });
1568
2920
  }
1569
- function DropdownMenuRadioGroup({ ...props }) {
1570
- return /* @__PURE__ */ jsx(Menu.RadioGroup, {
1571
- "data-slot": "dropdown-menu-radio-group",
2921
+ function PreviewCardDescription({ className, ...props }) {
2922
+ const { description } = previewCard();
2923
+ return /* @__PURE__ */ jsx("p", {
2924
+ "data-slot": "preview-card-description",
2925
+ className: description({ className }),
1572
2926
  ...props
1573
2927
  });
1574
2928
  }
1575
- function DropdownMenuRadioItem({ className, children, ...props }) {
1576
- const { radioItem, radioLabel, indicator } = dropdownMenu();
1577
- return /* @__PURE__ */ jsxs(Menu.RadioItem, {
1578
- "data-slot": "dropdown-menu-radio-item",
1579
- className: radioItem({ className }),
1580
- ...props,
1581
- children: [renderRadioItemChildren(children, radioLabel()), /* @__PURE__ */ jsx("span", {
1582
- className: indicator(),
1583
- children: /* @__PURE__ */ jsx(Menu.RadioItemIndicator, { children: /* @__PURE__ */ jsx(DropdownMenuCheckIcon, {}) })
1584
- })]
2929
+ function PreviewCardArrow({ className, ...props }) {
2930
+ const { arrow } = previewCard();
2931
+ return /* @__PURE__ */ jsx(PreviewCard.Arrow, {
2932
+ "data-slot": "preview-card-arrow",
2933
+ className: arrow({ className }),
2934
+ ...props
1585
2935
  });
1586
2936
  }
1587
- function renderRadioItemChildren(children, className) {
1588
- if (typeof children === "string" || typeof children === "number") return /* @__PURE__ */ jsx("span", {
1589
- "data-slot": "dropdown-menu-radio-item-label",
1590
- className,
1591
- children
1592
- });
1593
- if (Array.isArray(children) && children.every((child) => typeof child === "string")) return /* @__PURE__ */ jsx("span", {
1594
- "data-slot": "dropdown-menu-radio-item-label",
1595
- className,
1596
- children
2937
+ /** The item card's block: the id line, the title and the details row, in that order. */
2938
+ function PreviewCardBody({ className, ...props }) {
2939
+ const { body } = previewCard();
2940
+ return /* @__PURE__ */ jsx("div", {
2941
+ "data-slot": "preview-card-body",
2942
+ className: body({ className }),
2943
+ ...props
1597
2944
  });
1598
- return children;
1599
2945
  }
1600
- function DropdownMenuRadioItemIndicator({ className, ...props }) {
1601
- return /* @__PURE__ */ jsx(Menu.RadioItemIndicator, {
1602
- "data-slot": "dropdown-menu-radio-item-indicator",
1603
- className,
2946
+ /** The muted line above the title that identifies the item, such as its id. One line, truncated. */
2947
+ function PreviewCardEyebrow({ className, ...props }) {
2948
+ const { eyebrow } = previewCard();
2949
+ return /* @__PURE__ */ jsx("p", {
2950
+ "data-slot": "preview-card-eyebrow",
2951
+ className: eyebrow({ className }),
1604
2952
  ...props
1605
2953
  });
1606
2954
  }
1607
- function DropdownMenuCheckboxItem({ className, children, ...props }) {
1608
- const { item, indicator } = dropdownMenu();
1609
- return /* @__PURE__ */ jsxs(Menu.CheckboxItem, {
1610
- "data-slot": "dropdown-menu-checkbox-item",
1611
- className: item({ className }),
1612
- ...props,
1613
- children: [children, /* @__PURE__ */ jsx("span", {
1614
- className: indicator(),
1615
- children: /* @__PURE__ */ jsx(Menu.CheckboxItemIndicator, { children: /* @__PURE__ */ jsx(DropdownMenuCheckIcon, {}) })
1616
- })]
2955
+ /** The row under the title: one state chip, then muted details. Wraps when it runs out of room. */
2956
+ function PreviewCardMeta({ className, ...props }) {
2957
+ const { meta } = previewCard();
2958
+ return /* @__PURE__ */ jsx("div", {
2959
+ "data-slot": "preview-card-meta",
2960
+ className: meta({ className }),
2961
+ ...props
1617
2962
  });
1618
2963
  }
1619
- function DropdownMenuCheckboxItemIndicator({ className, ...props }) {
1620
- return /* @__PURE__ */ jsx(Menu.CheckboxItemIndicator, {
1621
- "data-slot": "dropdown-menu-checkbox-item-indicator",
1622
- className,
2964
+ /** The row of at most two buttons. */
2965
+ function PreviewCardActions({ className, ...props }) {
2966
+ const { actions } = previewCard();
2967
+ return /* @__PURE__ */ jsx("div", {
2968
+ "data-slot": "preview-card-actions",
2969
+ className: actions({ className }),
1623
2970
  ...props
1624
2971
  });
1625
2972
  }
1626
- /**
1627
- * One choice that is another page, such as the same page in another
1628
- * language: a link, so it opens in a new tab and can be copied. Its
1629
- * `href` goes on the item; `current` marks the page being shown.
1630
- */
1631
- function DropdownMenuLinkItem({ current = false, className, children, ...props }) {
1632
- const { radioItem, radioLabel, indicator } = dropdownMenu();
1633
- return /* @__PURE__ */ jsxs(Menu.LinkItem, {
1634
- "data-slot": "dropdown-menu-link-item",
1635
- "aria-current": current ? "page" : void 0,
1636
- "data-checked": current ? "" : void 0,
1637
- className: radioItem({ className }),
1638
- ...props,
1639
- children: [renderRadioItemChildren(children, radioLabel()), current ? /* @__PURE__ */ jsx("span", {
1640
- className: indicator(),
1641
- children: /* @__PURE__ */ jsx(DropdownMenuCheckIcon, {})
1642
- }) : null]
2973
+ /** The caption under a hairline at the bottom of the card: where the item comes from. */
2974
+ function PreviewCardFooter({ className, ...props }) {
2975
+ const { footer } = previewCard();
2976
+ return /* @__PURE__ */ jsx("p", {
2977
+ "data-slot": "preview-card-footer",
2978
+ className: footer({ className }),
2979
+ ...props
1643
2980
  });
1644
2981
  }
1645
2982
  function contentBox(element) {
@@ -2882,10 +4219,38 @@ function MessageReactionTooltipMeta({ className, render, ...props }) {
2882
4219
  });
2883
4220
  }
2884
4221
  //#endregion
4222
+ //#region src/lib/inline-chip.ts
4223
+ /**
4224
+ * The one box every chip in message text shares: a reference chip (any
4225
+ * variant, loading or unavailable too), the yellow sticker of a mention of the
4226
+ * person reading, and an inline task chip. The rule and its numbers are in
4227
+ * message-item.md, Chips in running text.
4228
+ *
4229
+ * - Type: 0.875em of the sentence, semibold.
4230
+ * - Height: the line box of its text, the body line less 2px, with no padding
4231
+ * above or below: 20px on a desktop (body 14/22), 22px on a phone (16/24). A
4232
+ * chip is always 2px shorter than its line, so it never makes the line taller.
4233
+ * `--chip-line-height` overrides it for text that isn't on the body line.
4234
+ * - Alignment: the chip's text on the sentence's baseline, a leading mark
4235
+ * centred on the chip (alignment.md, Marks in running text).
4236
+ * - Sides: 6px at each end. A chip with a leading mark (a 14px icon, an app's
4237
+ * image or avatar in `MessageReferenceIcon`'s 14px box, the loading spinner,
4238
+ * a task's 9px status pixel, each in its own box) takes
4239
+ * 4.5px at each end, so the ink at its two ends sits equally far in
4240
+ * (alignment.md, Chips), and the mark is 4px from the text. A note after the
4241
+ * label keeps the same 4px.
4242
+ * - Corners: `rounded-sticker`.
4243
+ */
4244
+ const inlineChip = [
4245
+ "items-baseline gap-1 px-1.5 py-0 align-baseline",
4246
+ "has-[>svg,>[data-slot=spinner],>[data-slot=message-reference-icon]]:px-[4.5px]",
4247
+ "[font-size:0.875em] leading-[var(--chip-line-height,calc(var(--text-body--line-height)-2px))] font-semibold",
4248
+ "rounded-sticker"
4249
+ ];
4250
+ //#endregion
2885
4251
  //#region src/components/message-item/message-reference.recipe.ts
2886
4252
  const chip = (fill) => [
2887
- "items-baseline gap-0.5 rounded-sticker px-1.5 py-px has-[>svg]:px-[4.5px]",
2888
- "[font-size:0.875em] leading-5 font-semibold",
4253
+ ...inlineChip,
2889
4254
  "[--message-reference-em:1em]",
2890
4255
  "[&_svg]:size-3.5 [&_svg]:text-foreground-muted",
2891
4256
  fill
@@ -2915,23 +4280,29 @@ const messageReference = tv$1({
2915
4280
  SUFFIX_LIFT,
2916
4281
  "font-mono text-label leading-none font-normal text-foreground-hint"
2917
4282
  ],
4283
+ icon: ["inline-flex size-3.5 shrink-0 items-center justify-center self-center", "*:size-full *:shrink-0 [&>img]:rounded-[2px] [&>img]:object-cover"],
2918
4284
  indicator: ["pointer-events-none absolute -right-1.5 top-0 inline-flex size-2 shrink-0 items-center justify-center", "*:data-[slot=status]:size-full"]
2919
4285
  },
2920
- variants: { variant: {
2921
- primary: { root: [
2922
- "inline-flex cursor-pointer rounded-sticker px-[3px]",
2923
- "font-semibold text-sticker-ink no-underline",
2924
- "bg-brand inset-ring-[1.5px] inset-ring-sticker-ink ring-[1.5px] ring-sticker-edge",
2925
- "hover:overlay-sticker-medium active:overlay-sticker-medium"
2926
- ] },
2927
- secondary: { root: ["inline cursor-pointer font-bold text-foreground", "decoration-foreground decoration-[1.5px] underline-offset-[3px] hover:underline"] },
2928
- accent: { root: linkChip("bg-attention-soft") },
2929
- muted: { root: linkChip("bg-inactive-soft") },
2930
- info: { root: linkChip("bg-info-soft") },
2931
- link: { root: linkChip("bg-info-soft") },
2932
- pending: { root: chip("cursor-wait bg-inactive-soft text-foreground-muted") },
2933
- unavailable: { root: chip("cursor-default bg-inactive-soft text-foreground-hint") }
2934
- } },
4286
+ variants: {
4287
+ variant: {
4288
+ primary: { root: [
4289
+ "inline-flex cursor-pointer",
4290
+ ...inlineChip,
4291
+ "[--message-reference-em:1em]",
4292
+ "text-sticker-ink no-underline",
4293
+ "bg-brand inset-ring-[1.5px] inset-ring-sticker-ink",
4294
+ "hover:overlay-sticker-medium active:overlay-sticker-medium"
4295
+ ] },
4296
+ secondary: { root: ["inline cursor-pointer font-bold text-foreground", "decoration-foreground decoration-[1.5px] underline-offset-[3px] hover:underline"] },
4297
+ accent: { root: linkChip("bg-attention-soft") },
4298
+ muted: { root: linkChip("bg-inactive-soft") },
4299
+ info: { root: linkChip("bg-info-soft") },
4300
+ link: { root: linkChip("bg-info-soft") },
4301
+ pending: { root: chip("cursor-wait bg-inactive-soft text-foreground-muted") },
4302
+ unavailable: { root: chip("cursor-default bg-inactive-soft text-foreground-hint") }
4303
+ },
4304
+ capped: { true: { root: "max-w-[min(100%,24em)]" } }
4305
+ },
2935
4306
  defaultVariants: { variant: "muted" }
2936
4307
  });
2937
4308
  //#endregion
@@ -2958,8 +4329,11 @@ function MessageReferenceIndicator({ className, render, ...props }) {
2958
4329
  state: { slot: "message-reference-indicator" }
2959
4330
  });
2960
4331
  }
2961
- function MessageReferenceChip({ className, render, variant = "muted", ...props }) {
2962
- const { root } = messageReference({ variant });
4332
+ function MessageReferenceChip({ className, render, variant = "muted", capped, ...props }) {
4333
+ const { root } = messageReference({
4334
+ variant,
4335
+ capped
4336
+ });
2963
4337
  return useReferenceRender(root({ className }), render, props);
2964
4338
  }
2965
4339
  function MessageReferenceLabel({ className, render, ...props }) {
@@ -2984,8 +4358,26 @@ function MessageReferenceSuffix({ className, render, ...props }) {
2984
4358
  state: { slot: "message-reference-suffix" }
2985
4359
  });
2986
4360
  }
2987
- function MessageReferencePending({ className, render, ...props }) {
2988
- const { root } = messageReference({ variant: "pending" });
4361
+ /**
4362
+ * The chip's leading icon when it is not a plain svg: an image or a `3xs`
4363
+ * Avatar, such as an app's registered icon. It holds one child at 14px,
4364
+ * centred on the chip. Put it first in the chip.
4365
+ */
4366
+ function MessageReferenceIcon({ className, render, ...props }) {
4367
+ const { icon } = messageReference();
4368
+ const defaultProps = { className: icon({ className }) };
4369
+ return useRender({
4370
+ defaultTagName: "span",
4371
+ render,
4372
+ props: mergeProps(defaultProps, props),
4373
+ state: { slot: "message-reference-icon" }
4374
+ });
4375
+ }
4376
+ function MessageReferencePending({ className, render, capped, ...props }) {
4377
+ const { root } = messageReference({
4378
+ variant: "pending",
4379
+ capped
4380
+ });
2989
4381
  return useReferenceRender(root({ className }), render, props);
2990
4382
  }
2991
4383
  function MessageReferenceUnavailable({ className, render, ...props }) {
@@ -3551,100 +4943,6 @@ function MessageListJumpPillDismiss({ className, children, render, nativeButton,
3551
4943
  })
3552
4944
  });
3553
4945
  }
3554
- const holds = /* @__PURE__ */ new WeakMap();
3555
- function touchPoint(event) {
3556
- const touch = event.touches?.[0] ?? event.changedTouches?.[0];
3557
- return touch ? {
3558
- x: touch.clientX,
3559
- y: touch.clientY
3560
- } : null;
3561
- }
3562
- function farther(from, to, limit) {
3563
- return Math.hypot(to.x - from.x, to.y - from.y) > limit;
3564
- }
3565
- var HoldThenMoveTouchSensorImpl = class extends TouchSensor {
3566
- static activators = TouchSensor.activators;
3567
- static setup() {
3568
- return TouchSensor.setup();
3569
- }
3570
- attach() {
3571
- super.attach();
3572
- const self = this;
3573
- const { holdMs = 500 } = self.props.options;
3574
- const state = {
3575
- heldAt: null,
3576
- last: null,
3577
- timer: setTimeout(() => {
3578
- state.heldAt = state.last ?? self.initialCoordinates;
3579
- }, holdMs)
3580
- };
3581
- holds.set(this, state);
3582
- }
3583
- handleMove(event) {
3584
- const self = this;
3585
- const state = holds.get(this);
3586
- if (self.activated || !state) {
3587
- super.handleMove(event);
3588
- return;
3589
- }
3590
- if ((event.touches?.length ?? 1) > 1) {
3591
- self.handleCancel();
3592
- return;
3593
- }
3594
- const point = touchPoint(event);
3595
- const start = self.initialCoordinates;
3596
- if (!point || !start) return;
3597
- state.last = point;
3598
- const { distance = 10, tolerance = 10 } = self.props.options;
3599
- if (!state.heldAt) {
3600
- if (farther(start, point, tolerance)) self.handleCancel();
3601
- return;
3602
- }
3603
- if (event.cancelable) event.preventDefault();
3604
- if (farther(state.heldAt, point, distance)) self.handleStart();
3605
- }
3606
- detach() {
3607
- const state = holds.get(this);
3608
- if (state) clearTimeout(state.timer);
3609
- holds.delete(this);
3610
- super.detach();
3611
- }
3612
- };
3613
- /** A TouchSensor that starts a drag on the first move after a still hold (see above). */
3614
- const HoldThenMoveTouchSensor = HoldThenMoveTouchSensorImpl;
3615
- /**
3616
- * dnd-kit's PointerSensor for a mouse and a pen only: a finger's pointerdown is
3617
- * left to `HoldThenMoveTouchSensor`.
3618
- */
3619
- var MouseOnlyPointerSensor = class extends PointerSensor {
3620
- static activators = [{
3621
- eventName: "onPointerDown",
3622
- handler: (event, options) => event.nativeEvent.pointerType === "touch" ? false : PointerSensor.activators[0].handler(event, options)
3623
- }];
3624
- };
3625
- /**
3626
- * `useSensor(HoldThenMoveTouchSensor, …)` with the activation the sensor relies
3627
- * on: no constraint dnd-kit would act on by itself, so only a move after the
3628
- * hold starts the drag.
3629
- */
3630
- function useHoldThenMoveTouchSensor({ holdMs = 500, distance = 10, tolerance = 10 } = {}) {
3631
- const options = useMemo(() => ({
3632
- holdMs,
3633
- distance,
3634
- tolerance,
3635
- activationConstraint: { distance: Number.POSITIVE_INFINITY }
3636
- }), [
3637
- holdMs,
3638
- distance,
3639
- tolerance
3640
- ]);
3641
- return useSensor(HoldThenMoveTouchSensor, options);
3642
- }
3643
- /** The mouse / pen sensor that goes with it: picks up after `distance` px. */
3644
- function useMouseOnlyPointerSensor(distance) {
3645
- const options = useMemo(() => ({ activationConstraint: { distance } }), [distance]);
3646
- return useSensor(MouseOnlyPointerSensor, options);
3647
- }
3648
4946
  //#endregion
3649
4947
  //#region src/components/inbox-item/inbox-item.recipe.ts
3650
4948
  const inboxItem = tv$1({
@@ -3785,4 +5083,4 @@ const inboxItem = tv$1({
3785
5083
  }]
3786
5084
  });
3787
5085
  //#endregion
3788
- export { MessageReactionPickerStatic as $, DialogTitle as $n, DropdownMenuFooter as $t, MessageEmbedFooter as A, useMobileViewport as An, MessageItemEmbedded as At, MessageReferenceChip as B, textLinkVariants as Bn, AvatarFallback as Bt, MessageQuotedPreviewLoading as C, PanelSection as Cn, MessageItemSenderButton as Ct, MessageQuotedPreviewUnavailable as D, ScrollAreaScrollbar as Dn, MessageItemBody as Dt, MessageQuotedPreviewTimestamp as E, ScrollAreaCorner as En, MessageItemAttachments as Et, MessageTextSelectionToolbarSeparator as F, TooltipContent as Fn, MessageItemGutter as Ft, MessageReferenceText as G, DialogContent as Gn, measureContent as Gt, MessageReferenceLabel as H, Dialog$1 as Hn, AvatarGroupCount as Ht, MessageMultiSelect as I, TooltipPortal as In, MessageItem as It, MessageReactionChip as J, DialogHeader as Jn, readNow as Jt, MessageReferenceUnavailable as K, DialogDescription as Kn, measureElement as Kt, MessageMultiSelectCheckbox as L, TooltipProvider as Ln, searchHit as Lt, messageEmbed as M, CheckboxIndicator as Mn, MessageItemSearchMatch as Mt, MessageTextSelectionToolbar as N, PRESENCE_COLOR as Nn, MessageItemAvatarSlot as Nt, MessageEmbed as O, ScrollAreaThumb as On, MessageItemContent as Ot, MessageTextSelectionToolbarButton as P, Tooltip$1 as Pn, MessageItemContinuationTime as Pt, MessageReactionPickerContent as Q, DialogSurface as Qn, DropdownMenuContent as Qt, MessageMultiSelectToolbar as R, TooltipTrigger as Rn, Avatar$1 as Rt, MessageQuotedPreviewHeader as S, PanelScrollViewport as Sn, MessageItemSenderBadges as St, MessageQuotedPreviewThread as T, ScrollAreaContent as Tn, MessageItemTime as Tt, MessageReferencePending as U, DialogBody as Un, AvatarImage as Ut, MessageReferenceIndicator as V, Badge as Vn, AvatarGroup as Vt, MessageReferenceSuffix as W, DialogClose as Wn, useOverflowTitle as Wt, MessageReactionErrorIndicator as X, DialogOverlay as Xn, DropdownMenuCheckboxItem as Xt, MessageReactionCount as Y, DialogNav as Yn, DropdownMenu as Yt, MessageReactionGlyph as Z, DialogPortal as Zn, DropdownMenuCheckboxItemIndicator as Zt, MessageQuotedPreviewAuthorDelegatedBy as _, Panel as _n, MessageItemDelegatedByName as _t, MessageListJumpPill as a, DropdownMenuPopup as an, useParentOverlayKind as ar, MessageReactionTooltipNameText as at, MessageQuotedPreviewChannel as b, PanelHeaderRoot as bn, MessageItemMeta as bt, MessageListDateDivider as c, DropdownMenuRadioItem as cn, useThemeScopeProps as cr, MessageItemBadge as ct, MessageListNotice as d, DropdownMenuShortcut as dn, tv$1 as dr, MessageItemSavedIndicator as dt, DropdownMenuItem as en, DialogTrigger as er, MessageReactionQuickButton as et, MessageList as f, DropdownMenuSubmenu as fn, controlIconInset as fr, MessageItemToolbar as ft, MessageQuotedPreviewAuthor as g, KbdGroup as gn, MessageItemDelegatedByButton as gt, MessageQuotedPreviewAttachments as h, Kbd as hn, decisionWidth as hr, MessageItemDelegatedBy as ht, MessageListJumpControls as i, DropdownMenuLinkItem as in, defaultNativeButton as ir, MessageReactionTooltipName as it, MessageEmbedHeader as j, Checkbox$1 as jn, MessageItemFooter as jt, MessageEmbedContent as k, ScrollAreaViewport as kn, MessageItemEdited as kt, MessageListEndFade as l, DropdownMenuRadioItemIndicator as ln, useTheme as lr, MessageItemBadgeSeparator as lt, MessageQuotedPreviewAttachment as m, DropdownMenuTrigger as mn, headEdges as mr, MessageItemDelegatedAvatar as mt, useHoldThenMoveTouchSensor as n, DropdownMenuItemLabel as nn, buttonVariants as nr, MessageReactionTooltipContent as nt, MessageListJumpPillDismiss as o, DropdownMenuPortal as on, withOverlayLayer as or, MessageReactionTooltipNames as ot, MessageQuotedPreview as p, DropdownMenuSubmenuTrigger as pn, headEdgePull as pr, MessageItemToolbarButton as pt, MessageReactionAddButton as q, DialogFooter as qn, observeTruncation as qt, useMouseOnlyPointerSensor as r, DropdownMenuLabel as rn, Spinner as rr, MessageReactionTooltipMeta as rt, MessageListJumpPillGroup as s, DropdownMenuRadioGroup as sn, resolveThemeScopeProps as sr, MessageReactionTooltipSeparator as st, inboxItem as t, DropdownMenuItemCount as tn, Button$1 as tr, MessageReactionQuickRow as tt, MessageListHistoryLimited as u, DropdownMenuSeparator as un, ThemeStateContext as ur, MessageItemReadReceipt as ut, MessageQuotedPreviewAuthorMeta as v, PanelBody as vn, MessageItemHeader as vt, MessageQuotedPreviewText as w, ScrollArea$1 as wn, MessageItemSenderGroup as wt, MessageQuotedPreviewContent as x, PanelScrollContent as xn, MessageItemSender as xt, MessageQuotedPreviewAuthorName as y, PanelFooter as yn, MessageItemInlineStatus as yt, MessageMultiSelectToolbarActions as z, TextLink as zn, AvatarBadge as zt };
5086
+ export { MessageReactionPickerStatic as $, DropdownMenuRadioGroup as $n, DialogSheetReset as $r, PreviewCardContent as $t, messageEmbed as A, PopoverPopup as An, Badge as Ar, MessageItemEmbedded as At, MessageReferenceIndicator as B, MobileNavRoot as Bn, DialogFooterNote as Br, AvatarFallback as Bt, MessageQuotedPreviewThread as C, PopoverArrow as Cn, CheckboxIndicator as Cr, MessageItemSenderButton as Ct, MessageEmbedContent as D, PopoverContent as Dn, TooltipPortal as Dr, MessageItemBody as Dt, MessageEmbed as E, PopoverClose as En, TooltipContent as Er, MessageItemAttachments as Et, MessageMultiSelectCheckbox as F, PopoverViewport as Fn, DialogBody as Fr, MessageItemGutter as Ft, MessageReferenceUnavailable as G, DropdownMenuFooter as Gn, DialogSheetBack as Gr, measureContent as Gt, MessageReferencePending as H, DropdownMenuCheckboxItem as Hn, DialogNav as Hr, AvatarGroupCount as Ht, MessageMultiSelectToolbar as I, useHoldThenMoveTouchSensor as In, DialogClose as Ir, MessageItem as It, MessageReactionChip as J, DropdownMenuItemLabel as Jn, DialogTrigger as Jr, readNow as Jt, inlineChip as K, DropdownMenuItem as Kn, DialogSurface as Kr, measureElement as Kt, MessageMultiSelectToolbarActions as L, useMouseOnlyPointerSensor as Ln, DialogContent as Lr, searchHit as Lt, MessageTextSelectionToolbarButton as M, PopoverSeparator as Mn, DialogSheetContent as Mr, MessageItemSearchMatch as Mt, MessageTextSelectionToolbarSeparator as N, PopoverTitle as Nn, useMobileViewport as Nr, MessageItemAvatarSlot as Nt, MessageEmbedFooter as O, PopoverDescription as On, TooltipProvider as Or, MessageItemContent as Ot, MessageMultiSelect as P, PopoverTrigger as Pn, Dialog$1 as Pr, MessageItemContinuationTime as Pt, MessageReactionPickerContent as Q, DropdownMenuPortal as Qn, textLinkVariants as Qr, PreviewCardBody as Qt, MessageReferenceChip as R, MobileNavItem as Rn, DialogDescription as Rr, Avatar$1 as Rt, MessageQuotedPreviewText as S, PopoverActions as Sn, Checkbox$1 as Sr, MessageItemSenderBadges as St, MessageQuotedPreviewUnavailable as T, PopoverBody as Tn, Tooltip$1 as Tr, MessageItemTime as Tt, MessageReferenceSuffix as U, DropdownMenuCheckboxItemIndicator as Un, DialogOverlay as Ur, AvatarImage as Ut, MessageReferenceLabel as V, DropdownMenu as Vn, DialogHeader as Vr, AvatarGroup as Vt, MessageReferenceText as W, DropdownMenuContent as Wn, DialogPortal as Wr, useOverflowTitle as Wt, MessageReactionErrorIndicator as X, DropdownMenuLinkItem as Xn, panelToggleAction as Xr, PreviewCardActions as Xt, MessageReactionCount as Y, DropdownMenuLabel as Yn, PanelAction as Yr, PreviewCard$1 as Yt, MessageReactionGlyph as Z, DropdownMenuPopup as Zn, TextLink as Zr, PreviewCardArrow as Zt, MessageQuotedPreviewAuthorName as _, MobileNavServerPopoverRow as _n, ScrollAreaContent as _r, MessageItemDelegatedByName as _t, MessageListJumpPillGroup as a, useParentOverlayKind as ai, PreviewCardPopup as an, DropdownMenuSubmenuTrigger as ar, MessageReactionTooltipNameText as at, MessageQuotedPreviewHeader as b, MobileNavServerPopoverRowLabel as bn, ScrollAreaThumb as br, MessageItemMeta as bt, MessageListHistoryLimited as c, useTheme as ci, PreviewCardTitle as cn, KbdGroup as cr, MessageItemBadge as ct, MessageQuotedPreview as d, stackedActions as di, MobileNavServerPopover as dn, PanelFooter as dr, MessageItemSavedIndicator as dt, Button$1 as ei, PreviewCardDescription as en, DropdownMenuRadioItem as er, MessageReactionQuickButton as et, MessageQuotedPreviewAttachment as f, useStackedActions as fi, MobileNavServerPopoverContent as fn, PanelHeaderRoot as fr, MessageItemToolbar as ft, MessageQuotedPreviewAuthorMeta as g, decisionWidth as gi, MobileNavServerPopoverList as gn, ScrollArea$1 as gr, MessageItemDelegatedByButton as gt, MessageQuotedPreviewAuthorDelegatedBy as h, headEdges as hi, MobileNavServerPopoverGroup as hn, PanelSection as hr, MessageItemDelegatedBy as ht, MessageListJumpPillDismiss as i, OverlayLayerProvider as ii, PreviewCardMeta as in, DropdownMenuSubmenu as ir, MessageReactionTooltipName as it, MessageTextSelectionToolbar as j, PopoverPortal as jn, DialogCommit as jr, MessageItemFooter as jt, MessageEmbedHeader as k, PopoverHeader as kn, TooltipTrigger as kr, MessageItemEdited as kt, MessageListNotice as l, ThemeStateContext as li, PreviewCardTrigger as ln, Panel as lr, MessageItemBadgeSeparator as lt, MessageQuotedPreviewAuthor as m, headEdgePull as mi, MobileNavServerPopoverDestinations as mn, PanelScrollViewport as mr, MessageItemDelegatedAvatar as mt, MessageListJumpControls as n, Spinner as ni, PreviewCardFooter as nn, DropdownMenuSeparator as nr, MessageReactionTooltipContent as nt, MessageListDateDivider as o, withOverlayLayer as oi, PreviewCardPortal as on, DropdownMenuTrigger as or, MessageReactionTooltipNames as ot, MessageQuotedPreviewAttachments as p, controlIconInset as pi, MobileNavServerPopoverDestination as pn, PanelScrollContent as pr, MessageItemToolbarButton as pt, MessageReactionAddButton as q, DropdownMenuItemCount as qn, DialogTitle as qr, observeTruncation as qt, MessageListJumpPill as r, defaultNativeButton as ri, PreviewCardHeader as rn, DropdownMenuShortcut as rr, MessageReactionTooltipMeta as rt, MessageListEndFade as s, useThemeScopeProps as si, PreviewCardSeparator as sn, Kbd as sr, MessageReactionTooltipSeparator as st, inboxItem as t, buttonVariants as ti, PreviewCardEyebrow as tn, DropdownMenuRadioItemIndicator as tr, MessageReactionQuickRow as tt, MessageList as u, tv$1 as ui, MobileNavServerItem as un, PanelBody as ur, MessageItemReadReceipt as ut, MessageQuotedPreviewChannel as v, MobileNavServerPopoverRowBody as vn, ScrollAreaCorner as vr, MessageItemHeader as vt, MessageQuotedPreviewTimestamp as w, PopoverBackdrop as wn, PRESENCE_COLOR as wr, MessageItemSenderGroup as wt, MessageQuotedPreviewLoading as x, Popover$1 as xn, ScrollAreaViewport as xr, MessageItemSender as xt, MessageQuotedPreviewContent as y, MobileNavServerPopoverRowDescription as yn, ScrollAreaScrollbar as yr, MessageItemInlineStatus as yt, MessageReferenceIcon as z, MobileNavLabel as zn, DialogFooter as zr, AvatarBadge as zt };