@botiverse/raft-ui 0.21.1 → 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,23 +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, useState, 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 { Radio } from "@base-ui/react/radio";
17
- import { RadioGroup } from "@base-ui/react/radio-group";
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";
18
19
  import { Toggle } from "@base-ui/react/toggle";
19
20
  import { Toolbar } from "@base-ui/react/toolbar";
20
- import { PointerSensor, TouchSensor, useSensor } from "@dnd-kit/core";
21
21
  //#region src/lib/decision-width.ts
22
22
  /**
23
23
  * The width of every decision dialog (dialog.md#variants): AlertDialog and a
@@ -56,6 +56,103 @@ const headEdges = ["px-5 max-md:px-4", ...headEdgePull];
56
56
  * as the arithmetic so it follows the control tokens wherever they switch.
57
57
  */
58
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
+ }
59
156
  //#endregion
60
157
  //#region src/lib/tv.ts
61
158
  const tv$1 = ((options) => {
@@ -99,6 +196,16 @@ function useThemeScopeProps(props) {
99
196
  //#endregion
100
197
  //#region src/lib/overlay-depth.tsx
101
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
+ */
102
209
  function OverlayLayerProvider({ kind, modal, children }) {
103
210
  const stack = useContext(OverlayStackContext);
104
211
  const value = useMemo(() => modal ? [...stack, kind] : stack, [
@@ -113,8 +220,14 @@ function OverlayLayerProvider({ kind, modal, children }) {
113
220
  }
114
221
  /**
115
222
  * The modal surface this overlay's own surface was opened from, or null when
116
- * it is the first. Called inside a surface, so the last entry is the surface
117
- * 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.
118
231
  */
119
232
  function useParentOverlayKind() {
120
233
  const stack = useContext(OverlayStackContext);
@@ -412,6 +525,141 @@ function Button$1({ variant, size, loading = false, loadingLabel = "Loading", di
412
525
  });
413
526
  }
414
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
415
663
  //#region src/components/dialog/dialog.tsx
416
664
  const dialog = tv$1({
417
665
  slots: {
@@ -432,7 +680,18 @@ const dialog = tv$1({
432
680
  "px-5 pt-1.5 pb-5 first:pt-5",
433
681
  "font-sans text-subhead text-foreground-muted"
434
682
  ],
435
- 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
+ ],
436
695
  title: ["m-0 min-w-0 text-foreground", "font-sans text-headline font-bold"],
437
696
  description: ["m-0", "font-sans text-subhead text-foreground-muted"]
438
697
  },
@@ -464,6 +723,10 @@ const dialog = tv$1({
464
723
  nested: {
465
724
  false: {},
466
725
  true: { overlay: ["bg-layer-backdrop-nested"] }
726
+ },
727
+ start: {
728
+ false: {},
729
+ true: { footer: ["flex-wrap justify-between"] }
467
730
  }
468
731
  },
469
732
  defaultVariants: {
@@ -504,6 +767,7 @@ function DialogOverlay({ className, layer, ...props }) {
504
767
  ...props
505
768
  });
506
769
  }
770
+ const DialogSizeContext = createContext(void 0);
507
771
  function DialogContent({ className, children, portalProps, overlay, size, layer, ...props }) {
508
772
  const { content } = dialog({
509
773
  size,
@@ -519,11 +783,15 @@ function DialogContent({ className, children, portalProps, overlay, size, layer,
519
783
  "data-slot": "dialog-content",
520
784
  className: content({ className }),
521
785
  ...props,
522
- 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
+ })
523
790
  })]
524
791
  });
525
792
  }
526
793
  function DialogClose({ className, variant, size, children, render, "aria-label": ariaLabel, ...props }) {
794
+ const inSheetHead = useInDialogSheetHead();
527
795
  if (render) return /* @__PURE__ */ jsx(Dialog.Close, {
528
796
  "data-slot": "dialog-close",
529
797
  render,
@@ -531,6 +799,15 @@ function DialogClose({ className, variant, size, children, render, "aria-label":
531
799
  ...props,
532
800
  children
533
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
+ });
534
811
  return /* @__PURE__ */ jsx(Dialog.Close, {
535
812
  "data-slot": "dialog-close",
536
813
  render: /* @__PURE__ */ jsx(Button$1, {
@@ -543,35 +820,149 @@ function DialogClose({ className, variant, size, children, render, "aria-label":
543
820
  children: children ?? /* @__PURE__ */ jsx(X, { className: "size-4.5" })
544
821
  });
545
822
  }
546
- 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
+ }
547
869
  const { header } = dialog({ bar });
548
870
  return /* @__PURE__ */ jsx("div", {
549
871
  "data-slot": "dialog-header",
550
872
  className: header({ className }),
551
- ...props
873
+ ...props,
874
+ children
552
875
  });
553
876
  }
554
- function DialogBody({ className, bar = false, ...props }) {
877
+ function DialogBody({ className, bar = false, children, ...props }) {
878
+ const { sheet, setBodyEndSlot } = useDialogSheet();
555
879
  const { body } = dialog({ bar });
556
- return /* @__PURE__ */ jsx("div", {
880
+ const sheetBody = sheet ? cn(bar && dialogSheet().barBody(), dialogSheet().body()) : void 0;
881
+ return /* @__PURE__ */ jsxs("div", {
557
882
  "data-slot": "dialog-body",
558
- className: body({ className }),
559
- ...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]
890
+ });
891
+ }
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", {
903
+ "data-slot": "dialog-footer",
904
+ ref: rowRef,
905
+ className: footer({ className }),
906
+ hidden: sheet || hidden,
907
+ ...stackedProps,
908
+ ...props,
909
+ children
560
910
  });
561
- }
562
- function DialogFooter({ className, bar = false, ...props }) {
563
- const { footer } = dialog({ bar });
564
- return /* @__PURE__ */ jsx("div", {
911
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [sheet && bodyEndSlot ? createPortal(start, bodyEndSlot) : null, /* @__PURE__ */ jsxs("div", {
565
912
  "data-slot": "dialog-footer",
913
+ ref,
566
914
  className: footer({ className }),
567
- ...props
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
568
957
  });
569
958
  }
570
959
  function DialogTitle({ className, ...props }) {
960
+ const inSheetHead = useInDialogSheetHead();
571
961
  const { title } = dialog();
962
+ const classes = inSheetHead ? cn(dialogSheet().title(), className) : className;
572
963
  return /* @__PURE__ */ jsx(Dialog.Title, {
573
964
  "data-slot": "dialog-title",
574
- className: title({ className }),
965
+ className: title({ className: classes }),
575
966
  ...props
576
967
  });
577
968
  }
@@ -595,7 +986,7 @@ const dialogSurface = tv$1({ slots: { root: [
595
986
  * animates itself: the host centres it, and a centred modal has no motion.
596
987
  * DialogContent draws the same frame for a Base UI dialog.
597
988
  */
598
- function DialogSurface({ className, render, ...props }) {
989
+ function DialogSurface({ className, render, children, ...props }) {
599
990
  const { root } = dialogSurface();
600
991
  return useRender({
601
992
  defaultTagName: "div",
@@ -603,7 +994,7 @@ function DialogSurface({ className, render, ...props }) {
603
994
  props: mergeProps({
604
995
  "data-slot": "dialog-surface",
605
996
  className: root({ className })
606
- }, props)
997
+ }, props, { children: /* @__PURE__ */ jsx(DialogSheetReset, { children }) })
607
998
  });
608
999
  }
609
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"] } });
@@ -620,6 +1011,206 @@ function DialogNav({ className, render, ...props }) {
620
1011
  });
621
1012
  }
622
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
623
1214
  //#region src/components/badge/badge.tsx
624
1215
  const badge = tv$1({
625
1216
  slots: { root: [
@@ -801,36 +1392,6 @@ function Experimental(props) {
801
1392
  }
802
1393
  const Badge = Object.assign(BadgeRoot, { Experimental });
803
1394
  //#endregion
804
- //#region src/components/text-link/text-link.tsx
805
- const textLinkVariants = tv$1({
806
- base: [
807
- "rounded-[2px]",
808
- "decoration-[1.5px] underline-offset-[3px] not-disabled:hover:underline",
809
- "outline-none focus-visible:shadow-focus",
810
- "disabled:cursor-not-allowed disabled:text-foreground-disabled"
811
- ],
812
- variants: { variant: {
813
- muted: ["font-medium text-foreground-muted", "hover:text-foreground"],
814
- strong: ["font-semibold text-foreground"]
815
- } },
816
- defaultVariants: { variant: "muted" }
817
- });
818
- function TextLink({ variant, className, render, ...props }) {
819
- const defaultProps = {
820
- className: textLinkVariants({
821
- variant,
822
- className
823
- }),
824
- ...render ? {} : { type: "button" }
825
- };
826
- return useRender({
827
- defaultTagName: "button",
828
- render,
829
- props: mergeProps(defaultProps, props),
830
- state: { slot: "text-link" }
831
- });
832
- }
833
- //#endregion
834
1395
  //#region src/components/tooltip/tooltip.tsx
835
1396
  const tooltip = tv$1({
836
1397
  slots: {
@@ -1112,21 +1673,6 @@ function Checkbox$1({ shape = "square", className, ...props }) {
1112
1673
  });
1113
1674
  }
1114
1675
  //#endregion
1115
- //#region src/lib/use-mobile-viewport.ts
1116
- const MOBILE_QUERY = "(max-width: 767px)";
1117
- function subscribeMobileViewport(onStoreChange) {
1118
- if (typeof window === "undefined") return () => void 0;
1119
- const media = window.matchMedia(MOBILE_QUERY);
1120
- media.addEventListener("change", onStoreChange);
1121
- return () => media.removeEventListener("change", onStoreChange);
1122
- }
1123
- function getMobileViewportSnapshot() {
1124
- return typeof window !== "undefined" && window.matchMedia(MOBILE_QUERY).matches;
1125
- }
1126
- function useMobileViewport() {
1127
- return useSyncExternalStore(subscribeMobileViewport, getMobileViewportSnapshot, () => false);
1128
- }
1129
- //#endregion
1130
1676
  //#region src/components/scroll-area/scroll-area.recipe.ts
1131
1677
  const scrollArea = tv$1({ slots: {
1132
1678
  root: ["relative min-h-0"],
@@ -1534,94 +2080,902 @@ function DropdownMenuSeparator({ className, ...props }) {
1534
2080
  ...props
1535
2081
  });
1536
2082
  }
1537
- function DropdownMenuSubmenuChevronIcon() {
1538
- return /* @__PURE__ */ jsx("svg", {
1539
- width: "12",
1540
- height: "12",
1541
- viewBox: "0 0 12 12",
1542
- "aria-hidden": true,
1543
- className: "size-3 shrink-0",
1544
- children: /* @__PURE__ */ jsx("g", {
1545
- fill: "none",
1546
- strokeLinecap: "round",
1547
- strokeLinejoin: "round",
1548
- strokeWidth: "1",
1549
- stroke: "currentColor",
1550
- children: /* @__PURE__ */ jsx("path", { d: "M4.25 2.5L7.75 6l-3.5 3.5" })
1551
- })
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
+ })
2893
+ });
2894
+ }
2895
+ function PreviewCardHeader({ className, ...props }) {
2896
+ const { header } = previewCard();
2897
+ return /* @__PURE__ */ jsx("div", {
2898
+ "data-slot": "preview-card-header",
2899
+ className: header({ className }),
2900
+ ...props
2901
+ });
2902
+ }
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",
2909
+ className: separator({ className }),
2910
+ ...props
2911
+ });
2912
+ }
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
1552
2919
  });
1553
2920
  }
1554
- function DropdownMenuCheckIcon() {
1555
- return /* @__PURE__ */ jsx("svg", {
1556
- width: "12",
1557
- height: "12",
1558
- viewBox: "0 0 12 12",
1559
- "aria-hidden": true,
1560
- className: "size-3 shrink-0",
1561
- children: /* @__PURE__ */ jsx("g", {
1562
- fill: "none",
1563
- strokeLinecap: "round",
1564
- strokeLinejoin: "round",
1565
- strokeWidth: "1",
1566
- stroke: "currentColor",
1567
- children: /* @__PURE__ */ jsx("path", { d: "M1.75 6.41L4.4 9.25L10.25 2.99" })
1568
- })
2921
+ function PreviewCardDescription({ className, ...props }) {
2922
+ const { description } = previewCard();
2923
+ return /* @__PURE__ */ jsx("p", {
2924
+ "data-slot": "preview-card-description",
2925
+ className: description({ className }),
2926
+ ...props
1569
2927
  });
1570
2928
  }
1571
- function DropdownMenuRadioGroup({ ...props }) {
1572
- return /* @__PURE__ */ jsx(Menu.RadioGroup, {
1573
- "data-slot": "dropdown-menu-radio-group",
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 }),
1574
2934
  ...props
1575
2935
  });
1576
2936
  }
1577
- function DropdownMenuRadioItem({ className, children, ...props }) {
1578
- const { radioItem, radioLabel, indicator } = dropdownMenu();
1579
- return /* @__PURE__ */ jsxs(Menu.RadioItem, {
1580
- "data-slot": "dropdown-menu-radio-item",
1581
- className: radioItem({ className }),
1582
- ...props,
1583
- children: [renderRadioItemChildren(children, radioLabel()), /* @__PURE__ */ jsx("span", {
1584
- className: indicator(),
1585
- children: /* @__PURE__ */ jsx(Menu.RadioItemIndicator, { children: /* @__PURE__ */ jsx(DropdownMenuCheckIcon, {}) })
1586
- })]
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
1587
2944
  });
1588
2945
  }
1589
- function renderRadioItemChildren(children, className) {
1590
- if (typeof children === "string" || typeof children === "number") return /* @__PURE__ */ jsx("span", {
1591
- "data-slot": "dropdown-menu-radio-item-label",
1592
- className,
1593
- children
1594
- });
1595
- if (Array.isArray(children) && children.every((child) => typeof child === "string")) return /* @__PURE__ */ jsx("span", {
1596
- "data-slot": "dropdown-menu-radio-item-label",
1597
- className,
1598
- children
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 }),
2952
+ ...props
1599
2953
  });
1600
- return children;
1601
2954
  }
1602
- function DropdownMenuRadioItemIndicator({ className, ...props }) {
1603
- return /* @__PURE__ */ jsx(Menu.RadioItemIndicator, {
1604
- "data-slot": "dropdown-menu-radio-item-indicator",
1605
- className,
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 }),
1606
2961
  ...props
1607
2962
  });
1608
2963
  }
1609
- function DropdownMenuCheckboxItem({ className, children, ...props }) {
1610
- const { item, indicator } = dropdownMenu();
1611
- return /* @__PURE__ */ jsxs(Menu.CheckboxItem, {
1612
- "data-slot": "dropdown-menu-checkbox-item",
1613
- className: item({ className }),
1614
- ...props,
1615
- children: [children, /* @__PURE__ */ jsx("span", {
1616
- className: indicator(),
1617
- children: /* @__PURE__ */ jsx(Menu.CheckboxItemIndicator, { children: /* @__PURE__ */ jsx(DropdownMenuCheckIcon, {}) })
1618
- })]
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 }),
2970
+ ...props
1619
2971
  });
1620
2972
  }
1621
- function DropdownMenuCheckboxItemIndicator({ className, ...props }) {
1622
- return /* @__PURE__ */ jsx(Menu.CheckboxItemIndicator, {
1623
- "data-slot": "dropdown-menu-checkbox-item-indicator",
1624
- className,
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 }),
1625
2979
  ...props
1626
2980
  });
1627
2981
  }
@@ -1819,138 +3173,6 @@ function useOverflowTitle(ref, title) {
1819
3173
  }, [own, ref]);
1820
3174
  }
1821
3175
  //#endregion
1822
- //#region src/components/segmented-control/segmented-control.tsx
1823
- const segmentedControl = tv$1({
1824
- slots: {
1825
- root: ["inline-flex shrink-0 items-center", "w-fit flex-nowrap gap-[3px] rounded-[11px] bg-fill-muted p-[3px]"],
1826
- item: [
1827
- "inline-flex min-w-0 shrink-0 items-center gap-1.5",
1828
- "outline-none disabled:pointer-events-none",
1829
- "[&_svg]:pointer-events-none [&_svg]:shrink-0",
1830
- "h-8 justify-center rounded-control border-[1.5px] border-transparent bg-transparent px-3",
1831
- "font-sans text-callout font-semibold text-foreground-muted",
1832
- "[&_svg:not([class*='size-'])]:size-4",
1833
- "hover:bg-fill-medium active:bg-fill-medium",
1834
- "data-checked:border-sticker-ink data-checked:bg-brand data-checked:font-bold data-checked:text-sticker-ink data-checked:ring-[1.5px] data-checked:ring-sticker-edge",
1835
- "data-checked:hover:bg-brand data-checked:active:bg-brand data-checked:active:overlay-sticker-medium",
1836
- "focus-visible:shadow-focus",
1837
- "data-disabled:cursor-not-allowed data-disabled:text-foreground-disabled data-disabled:hover:bg-transparent"
1838
- ],
1839
- label: ["truncate"],
1840
- count: ["font-mono", "text-mono-meta font-normal"]
1841
- },
1842
- variants: {
1843
- variant: {
1844
- default: {},
1845
- "on-frame": { root: [
1846
- "bg-fill-frame-medium",
1847
- "**:data-[slot=segmented-control-item]:text-foreground-on-frame-muted **:data-[slot=segmented-control-item]:hover:text-foreground-on-frame",
1848
- "**:data-[slot=segmented-control-item]:hover:bg-transparent **:data-[slot=segmented-control-item]:hover:overlay-frame-medium **:data-[slot=segmented-control-item]:active:bg-transparent **:data-[slot=segmented-control-item]:active:overlay-frame-medium",
1849
- "**:data-[slot=segmented-control-item]:data-checked:border-line-frame-active **:data-[slot=segmented-control-item]:data-checked:bg-layer-frame-active **:data-[slot=segmented-control-item]:data-checked:text-foreground-on-frame-active",
1850
- "**:data-[slot=segmented-control-item]:data-checked:hover:bg-layer-frame-active **:data-[slot=segmented-control-item]:data-checked:hover:bg-none **:data-[slot=segmented-control-item]:data-checked:active:bg-layer-frame-active"
1851
- ] }
1852
- },
1853
- iconsOnlyBelow: {
1854
- xs: { root: ["@max-xs:**:data-[slot=segmented-control-item]:relative @max-xs:**:data-[slot=segmented-control-item]:w-8 @max-xs:**:data-[slot=segmented-control-item]:px-0", "@max-xs:**:data-[slot=segmented-control-label]:sr-only @max-xs:**:data-[slot=segmented-control-count]:sr-only"] },
1855
- sm: { root: ["@max-sm:**:data-[slot=segmented-control-item]:relative @max-sm:**:data-[slot=segmented-control-item]:w-8 @max-sm:**:data-[slot=segmented-control-item]:px-0", "@max-sm:**:data-[slot=segmented-control-label]:sr-only @max-sm:**:data-[slot=segmented-control-count]:sr-only"] },
1856
- md: { root: ["@max-md:**:data-[slot=segmented-control-item]:relative @max-md:**:data-[slot=segmented-control-item]:w-8 @max-md:**:data-[slot=segmented-control-item]:px-0", "@max-md:**:data-[slot=segmented-control-label]:sr-only @max-md:**:data-[slot=segmented-control-count]:sr-only"] },
1857
- lg: { root: ["@max-lg:**:data-[slot=segmented-control-item]:relative @max-lg:**:data-[slot=segmented-control-item]:w-8 @max-lg:**:data-[slot=segmented-control-item]:px-0", "@max-lg:**:data-[slot=segmented-control-label]:sr-only @max-lg:**:data-[slot=segmented-control-count]:sr-only"] },
1858
- xl: { root: ["@max-xl:**:data-[slot=segmented-control-item]:relative @max-xl:**:data-[slot=segmented-control-item]:w-8 @max-xl:**:data-[slot=segmented-control-item]:px-0", "@max-xl:**:data-[slot=segmented-control-label]:sr-only @max-xl:**:data-[slot=segmented-control-count]:sr-only"] },
1859
- "2xl": { root: ["@max-2xl:**:data-[slot=segmented-control-item]:relative @max-2xl:**:data-[slot=segmented-control-item]:w-8 @max-2xl:**:data-[slot=segmented-control-item]:px-0", "@max-2xl:**:data-[slot=segmented-control-label]:sr-only @max-2xl:**:data-[slot=segmented-control-count]:sr-only"] }
1860
- }
1861
- }
1862
- });
1863
- const SegmentedControlIconsOnlyContext = createContext(false);
1864
- function SegmentedControl({ disabled = false, iconsOnlyBelow, variant = "default", className, ...props }) {
1865
- const { root } = segmentedControl({
1866
- iconsOnlyBelow,
1867
- variant
1868
- });
1869
- return /* @__PURE__ */ jsx(SegmentedControlIconsOnlyContext, {
1870
- value: iconsOnlyBelow !== void 0,
1871
- children: /* @__PURE__ */ jsx(RadioGroup, {
1872
- disabled,
1873
- "data-slot": "segmented-control",
1874
- className: root({ className }),
1875
- ...props
1876
- })
1877
- });
1878
- }
1879
- function SegmentedControlItem({ className, ...props }) {
1880
- const { item } = segmentedControl();
1881
- const iconsOnly = use(SegmentedControlIconsOnlyContext);
1882
- const radio = /* @__PURE__ */ jsx(Radio.Root, {
1883
- render: /* @__PURE__ */ jsx("button", { type: "button" }),
1884
- nativeButton: true,
1885
- "data-slot": "segmented-control-item",
1886
- className: item({ className: `group/segmented-control-item ${className ?? ""}`.trim() }),
1887
- ...props
1888
- });
1889
- return iconsOnly ? /* @__PURE__ */ jsx(SegmentedControlItemTooltip, { children: radio }) : radio;
1890
- }
1891
- function SegmentedControlItemTooltip({ children }) {
1892
- const [text, setText] = useState("");
1893
- const triggerRef = useRef(null);
1894
- const handleOpenChange = useCallback((open, details) => {
1895
- if (!open) return;
1896
- const label = triggerRef.current?.querySelector("[data-slot=segmented-control-label]");
1897
- if (!label || label.getBoundingClientRect().width > 1) {
1898
- details.cancel();
1899
- return;
1900
- }
1901
- setText(label.textContent ?? "");
1902
- }, []);
1903
- return /* @__PURE__ */ jsxs(Tooltip$1, {
1904
- onOpenChange: handleOpenChange,
1905
- children: [/* @__PURE__ */ jsx(TooltipTrigger, {
1906
- ref: triggerRef,
1907
- render: children
1908
- }), /* @__PURE__ */ jsx(TooltipContent, { children: text })]
1909
- });
1910
- }
1911
- function SegmentedControlLabel({ className, ...props }) {
1912
- const { label } = segmentedControl();
1913
- return /* @__PURE__ */ jsx("span", {
1914
- "data-slot": "segmented-control-label",
1915
- className: label({ className }),
1916
- ...props
1917
- });
1918
- }
1919
- function SegmentedControlCount({ className, ...props }) {
1920
- const { count } = segmentedControl();
1921
- return /* @__PURE__ */ jsx("span", {
1922
- "data-slot": "segmented-control-count",
1923
- className: count({ className }),
1924
- ...props
1925
- });
1926
- }
1927
- /**
1928
- * The same track for choices that are places, not values: each segment is a
1929
- * link to another URL (a language, a version of the page), so it can be
1930
- * opened in a new tab and copied. Name it with `aria-label`.
1931
- */
1932
- function SegmentedControlNav({ variant = "default", className, ...props }) {
1933
- const { root } = segmentedControl({ variant });
1934
- return /* @__PURE__ */ jsx("nav", {
1935
- "data-slot": "segmented-control",
1936
- className: root({ className }),
1937
- ...props
1938
- });
1939
- }
1940
- /** One segment of a `SegmentedControlNav`: a link, `<a>` unless `render` gives a router link. */
1941
- function SegmentedControlLink({ current = false, className, render, ...props }) {
1942
- const { item } = segmentedControl();
1943
- return useRender({
1944
- render: render ?? /* @__PURE__ */ jsx("a", {}),
1945
- props: mergeProps({
1946
- "data-slot": "segmented-control-item",
1947
- "aria-current": current ? "page" : void 0,
1948
- ...current ? { "data-checked": "" } : {},
1949
- className: item({ className: `group/segmented-control-item ${className ?? ""}`.trim() })
1950
- }, props)
1951
- });
1952
- }
1953
- //#endregion
1954
3176
  //#region src/components/avatar/avatar.tsx
1955
3177
  const avatar = tv$1({
1956
3178
  slots: {
@@ -2997,10 +4219,38 @@ function MessageReactionTooltipMeta({ className, render, ...props }) {
2997
4219
  });
2998
4220
  }
2999
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
3000
4251
  //#region src/components/message-item/message-reference.recipe.ts
3001
4252
  const chip = (fill) => [
3002
- "items-baseline gap-0.5 rounded-sticker px-1.5 py-px has-[>svg]:px-[4.5px]",
3003
- "[font-size:0.875em] leading-5 font-semibold",
4253
+ ...inlineChip,
3004
4254
  "[--message-reference-em:1em]",
3005
4255
  "[&_svg]:size-3.5 [&_svg]:text-foreground-muted",
3006
4256
  fill
@@ -3030,23 +4280,29 @@ const messageReference = tv$1({
3030
4280
  SUFFIX_LIFT,
3031
4281
  "font-mono text-label leading-none font-normal text-foreground-hint"
3032
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"],
3033
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"]
3034
4285
  },
3035
- variants: { variant: {
3036
- primary: { root: [
3037
- "inline-flex cursor-pointer rounded-sticker px-[3px]",
3038
- "font-semibold text-sticker-ink no-underline",
3039
- "bg-brand inset-ring-[1.5px] inset-ring-sticker-ink ring-[1.5px] ring-sticker-edge",
3040
- "hover:overlay-sticker-medium active:overlay-sticker-medium"
3041
- ] },
3042
- secondary: { root: ["inline cursor-pointer font-bold text-foreground", "decoration-foreground decoration-[1.5px] underline-offset-[3px] hover:underline"] },
3043
- accent: { root: linkChip("bg-attention-soft") },
3044
- muted: { root: linkChip("bg-inactive-soft") },
3045
- info: { root: linkChip("bg-info-soft") },
3046
- link: { root: linkChip("bg-info-soft") },
3047
- pending: { root: chip("cursor-wait bg-inactive-soft text-foreground-muted") },
3048
- unavailable: { root: chip("cursor-default bg-inactive-soft text-foreground-hint") }
3049
- } },
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
+ },
3050
4306
  defaultVariants: { variant: "muted" }
3051
4307
  });
3052
4308
  //#endregion
@@ -3073,8 +4329,11 @@ function MessageReferenceIndicator({ className, render, ...props }) {
3073
4329
  state: { slot: "message-reference-indicator" }
3074
4330
  });
3075
4331
  }
3076
- function MessageReferenceChip({ className, render, variant = "muted", ...props }) {
3077
- const { root } = messageReference({ variant });
4332
+ function MessageReferenceChip({ className, render, variant = "muted", capped, ...props }) {
4333
+ const { root } = messageReference({
4334
+ variant,
4335
+ capped
4336
+ });
3078
4337
  return useReferenceRender(root({ className }), render, props);
3079
4338
  }
3080
4339
  function MessageReferenceLabel({ className, render, ...props }) {
@@ -3099,8 +4358,26 @@ function MessageReferenceSuffix({ className, render, ...props }) {
3099
4358
  state: { slot: "message-reference-suffix" }
3100
4359
  });
3101
4360
  }
3102
- function MessageReferencePending({ className, render, ...props }) {
3103
- 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
+ });
3104
4381
  return useReferenceRender(root({ className }), render, props);
3105
4382
  }
3106
4383
  function MessageReferenceUnavailable({ className, render, ...props }) {
@@ -3666,100 +4943,6 @@ function MessageListJumpPillDismiss({ className, children, render, nativeButton,
3666
4943
  })
3667
4944
  });
3668
4945
  }
3669
- const holds = /* @__PURE__ */ new WeakMap();
3670
- function touchPoint(event) {
3671
- const touch = event.touches?.[0] ?? event.changedTouches?.[0];
3672
- return touch ? {
3673
- x: touch.clientX,
3674
- y: touch.clientY
3675
- } : null;
3676
- }
3677
- function farther(from, to, limit) {
3678
- return Math.hypot(to.x - from.x, to.y - from.y) > limit;
3679
- }
3680
- var HoldThenMoveTouchSensorImpl = class extends TouchSensor {
3681
- static activators = TouchSensor.activators;
3682
- static setup() {
3683
- return TouchSensor.setup();
3684
- }
3685
- attach() {
3686
- super.attach();
3687
- const self = this;
3688
- const { holdMs = 500 } = self.props.options;
3689
- const state = {
3690
- heldAt: null,
3691
- last: null,
3692
- timer: setTimeout(() => {
3693
- state.heldAt = state.last ?? self.initialCoordinates;
3694
- }, holdMs)
3695
- };
3696
- holds.set(this, state);
3697
- }
3698
- handleMove(event) {
3699
- const self = this;
3700
- const state = holds.get(this);
3701
- if (self.activated || !state) {
3702
- super.handleMove(event);
3703
- return;
3704
- }
3705
- if ((event.touches?.length ?? 1) > 1) {
3706
- self.handleCancel();
3707
- return;
3708
- }
3709
- const point = touchPoint(event);
3710
- const start = self.initialCoordinates;
3711
- if (!point || !start) return;
3712
- state.last = point;
3713
- const { distance = 10, tolerance = 10 } = self.props.options;
3714
- if (!state.heldAt) {
3715
- if (farther(start, point, tolerance)) self.handleCancel();
3716
- return;
3717
- }
3718
- if (event.cancelable) event.preventDefault();
3719
- if (farther(state.heldAt, point, distance)) self.handleStart();
3720
- }
3721
- detach() {
3722
- const state = holds.get(this);
3723
- if (state) clearTimeout(state.timer);
3724
- holds.delete(this);
3725
- super.detach();
3726
- }
3727
- };
3728
- /** A TouchSensor that starts a drag on the first move after a still hold (see above). */
3729
- const HoldThenMoveTouchSensor = HoldThenMoveTouchSensorImpl;
3730
- /**
3731
- * dnd-kit's PointerSensor for a mouse and a pen only: a finger's pointerdown is
3732
- * left to `HoldThenMoveTouchSensor`.
3733
- */
3734
- var MouseOnlyPointerSensor = class extends PointerSensor {
3735
- static activators = [{
3736
- eventName: "onPointerDown",
3737
- handler: (event, options) => event.nativeEvent.pointerType === "touch" ? false : PointerSensor.activators[0].handler(event, options)
3738
- }];
3739
- };
3740
- /**
3741
- * `useSensor(HoldThenMoveTouchSensor, …)` with the activation the sensor relies
3742
- * on: no constraint dnd-kit would act on by itself, so only a move after the
3743
- * hold starts the drag.
3744
- */
3745
- function useHoldThenMoveTouchSensor({ holdMs = 500, distance = 10, tolerance = 10 } = {}) {
3746
- const options = useMemo(() => ({
3747
- holdMs,
3748
- distance,
3749
- tolerance,
3750
- activationConstraint: { distance: Number.POSITIVE_INFINITY }
3751
- }), [
3752
- holdMs,
3753
- distance,
3754
- tolerance
3755
- ]);
3756
- return useSensor(HoldThenMoveTouchSensor, options);
3757
- }
3758
- /** The mouse / pen sensor that goes with it: picks up after `distance` px. */
3759
- function useMouseOnlyPointerSensor(distance) {
3760
- const options = useMemo(() => ({ activationConstraint: { distance } }), [distance]);
3761
- return useSensor(MouseOnlyPointerSensor, options);
3762
- }
3763
4946
  //#endregion
3764
4947
  //#region src/components/inbox-item/inbox-item.recipe.ts
3765
4948
  const inboxItem = tv$1({
@@ -3900,4 +5083,4 @@ const inboxItem = tv$1({
3900
5083
  }]
3901
5084
  });
3902
5085
  //#endregion
3903
- export { MessageReactionPickerStatic as $, DialogHeader as $n, observeTruncation as $t, MessageEmbedFooter as A, ScrollAreaContent as An, MessageItemEmbedded as At, MessageReferenceChip as B, TooltipContent as Bn, AvatarFallback as Bt, MessageQuotedPreviewLoading as C, PanelBody as Cn, MessageItemSenderButton as Ct, MessageQuotedPreviewUnavailable as D, PanelScrollViewport as Dn, MessageItemBody as Dt, MessageQuotedPreviewTimestamp as E, PanelScrollContent as En, MessageItemAttachments as Et, MessageTextSelectionToolbarSeparator as F, useMobileViewport as Fn, MessageItemGutter as Ft, MessageReferenceText as G, textLinkVariants as Gn, SegmentedControlCount as Gt, MessageReferenceLabel as H, TooltipProvider as Hn, AvatarGroupCount as Ht, MessageMultiSelect as I, Checkbox$1 as In, MessageItem as It, MessageReactionChip as J, DialogBody as Jn, SegmentedControlLink as Jt, MessageReferenceUnavailable as K, Badge as Kn, SegmentedControlItem as Kt, MessageMultiSelectCheckbox as L, CheckboxIndicator as Ln, searchHit as Lt, messageEmbed as M, ScrollAreaScrollbar as Mn, MessageItemSearchMatch as Mt, MessageTextSelectionToolbar as N, ScrollAreaThumb as Nn, MessageItemAvatarSlot as Nt, MessageEmbed as O, PanelSection as On, MessageItemContent as Ot, MessageTextSelectionToolbarButton as P, ScrollAreaViewport as Pn, MessageItemContinuationTime as Pt, MessageReactionPickerContent as Q, DialogFooter as Qn, measureElement as Qt, MessageMultiSelectToolbar as R, PRESENCE_COLOR as Rn, Avatar$1 as Rt, MessageQuotedPreviewHeader as S, Panel as Sn, MessageItemSenderBadges as St, MessageQuotedPreviewThread as T, PanelHeaderRoot as Tn, MessageItemTime as Tt, MessageReferencePending as U, TooltipTrigger as Un, AvatarImage as Ut, MessageReferenceIndicator as V, TooltipPortal as Vn, AvatarGroup as Vt, MessageReferenceSuffix as W, TextLink as Wn, SegmentedControl as Wt, MessageReactionErrorIndicator as X, DialogContent as Xn, useOverflowTitle as Xt, MessageReactionCount as Y, DialogClose as Yn, SegmentedControlNav as Yt, MessageReactionGlyph as Z, DialogDescription as Zn, measureContent as Zt, MessageQuotedPreviewAuthorDelegatedBy as _, DropdownMenuSubmenu as _n, controlIconInset as _r, MessageItemDelegatedByName as _t, MessageListJumpPill as a, DropdownMenuFooter as an, DialogTrigger as ar, MessageReactionTooltipNameText as at, MessageQuotedPreviewChannel as b, Kbd as bn, decisionWidth as br, MessageItemMeta as bt, MessageListDateDivider as c, DropdownMenuItemLabel as cn, Spinner as cr, MessageItemBadge as ct, MessageListNotice as d, DropdownMenuPortal as dn, withOverlayLayer as dr, MessageItemSavedIndicator as dt, readNow as en, DialogNav as er, MessageReactionQuickButton as et, MessageList as f, DropdownMenuRadioGroup as fn, resolveThemeScopeProps as fr, MessageItemToolbar as ft, MessageQuotedPreviewAuthor as g, DropdownMenuShortcut as gn, tv$1 as gr, MessageItemDelegatedByButton as gt, MessageQuotedPreviewAttachments as h, DropdownMenuSeparator as hn, ThemeStateContext as hr, MessageItemDelegatedBy as ht, MessageListJumpControls as i, DropdownMenuContent as in, DialogTitle as ir, MessageReactionTooltipName as it, MessageEmbedHeader as j, ScrollAreaCorner as jn, MessageItemFooter as jt, MessageEmbedContent as k, ScrollArea$1 as kn, MessageItemEdited as kt, MessageListEndFade as l, DropdownMenuLabel as ln, defaultNativeButton as lr, MessageItemBadgeSeparator as lt, MessageQuotedPreviewAttachment as m, DropdownMenuRadioItemIndicator as mn, useTheme as mr, MessageItemDelegatedAvatar as mt, useHoldThenMoveTouchSensor as n, DropdownMenuCheckboxItem as nn, DialogPortal as nr, MessageReactionTooltipContent as nt, MessageListJumpPillDismiss as o, DropdownMenuItem as on, Button$1 as or, MessageReactionTooltipNames as ot, MessageQuotedPreview as p, DropdownMenuRadioItem as pn, useThemeScopeProps as pr, MessageItemToolbarButton as pt, MessageReactionAddButton as q, Dialog$1 as qn, SegmentedControlLabel as qt, useMouseOnlyPointerSensor as r, DropdownMenuCheckboxItemIndicator as rn, DialogSurface as rr, MessageReactionTooltipMeta as rt, MessageListJumpPillGroup as s, DropdownMenuItemCount as sn, buttonVariants as sr, MessageReactionTooltipSeparator as st, inboxItem as t, DropdownMenu as tn, DialogOverlay as tr, MessageReactionQuickRow as tt, MessageListHistoryLimited as u, DropdownMenuPopup as un, useParentOverlayKind as ur, MessageItemReadReceipt as ut, MessageQuotedPreviewAuthorMeta as v, DropdownMenuSubmenuTrigger as vn, headEdgePull as vr, MessageItemHeader as vt, MessageQuotedPreviewText as w, PanelFooter as wn, MessageItemSenderGroup as wt, MessageQuotedPreviewContent as x, KbdGroup as xn, MessageItemSender as xt, MessageQuotedPreviewAuthorName as y, DropdownMenuTrigger as yn, headEdges as yr, MessageItemInlineStatus as yt, MessageMultiSelectToolbarActions as z, Tooltip$1 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 };