@epicbagel/saas-kit 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -440,6 +440,25 @@ export function Appearance() {
440
440
  MIT. Some components and styles are adapted from shadcn/ui, and the file
441
441
  drop handling from AI SDK Elements (Apache-2.0); see `THIRD_PARTY_NOTICES.md`.
442
442
 
443
+ ## Scroll navigation
444
+
445
+ `primitives/scroll-navigator` exports `ScrollNavigator`: pass an `items` array
446
+ with stable `id`, accessible `label`, optional `imageSrc`/`fallback`, and
447
+ `content`. It renders the list and a clickable avatar index while the list is
448
+ visible. The item at the top of the scroll viewport is highlighted; the index
449
+ scrolls independently for long lists. With insufficient left gutter, it uses
450
+ a compact horizontal row above the content. Single-item lists have no index.
451
+
452
+ It detects the nearest scrolling ancestor, supports an explicit `scrollRoot`
453
+ and `topOffset` for sticky headers, and offers `onNavigate` for custom scroll
454
+ controllers. Keyboard users Tab into the index, use arrows or Home/End, then
455
+ Enter to jump. Reduced-motion preferences are respected. No data or selected
456
+ record state changes when scrolling or navigating.
457
+
458
+ Inside a `MessageScrollerProvider`, use `MessageScrollerNavigator` from
459
+ `primitives/message-scroller` with the same props. It uses the thread's scroll
460
+ controller so incoming messages do not undo a jump into a long list.
461
+
443
462
  ## Notes
444
463
 
445
464
  `primitives/notes` provides a reusable `Notes` area and `NoteComposer` for any
@@ -0,0 +1,256 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
2
+
3
+
4
+
5
+
6
+ var _chunkWABCLRBRcjs = require('./chunk-WABCLRBR.cjs');
7
+
8
+
9
+ var _chunkKWLSBTB2cjs = require('./chunk-KWLSBTB2.cjs');
10
+
11
+ // src/primitives/scroll-navigator.tsx
12
+ var _react = require('react');
13
+ var _reactdom = require('react-dom');
14
+ var _jsxruntime = require('react/jsx-runtime');
15
+ var GAP = 12;
16
+ var RAIL_WIDTH = 56;
17
+ var ROW_HEIGHT = 64;
18
+ var initialPlacement = { mode: "row", top: 0, left: 0, width: 0, maxHeight: 0, active: 0, visible: false };
19
+ function findScrollRoot(element) {
20
+ for (let parent = element.parentElement; parent; parent = parent.parentElement) {
21
+ if (/(auto|scroll|overlay)/.test(getComputedStyle(parent).overflowY) && parent.scrollHeight > parent.clientHeight) return parent;
22
+ }
23
+ return null;
24
+ }
25
+ function viewportBounds(root) {
26
+ const viewport = window.visualViewport;
27
+ const top = _nullishCoalesce(_optionalChain([viewport, 'optionalAccess', _ => _.offsetTop]), () => ( 0));
28
+ const left = _nullishCoalesce(_optionalChain([viewport, 'optionalAccess', _2 => _2.offsetLeft]), () => ( 0));
29
+ const bounds = { top, left, right: left + (_nullishCoalesce(_optionalChain([viewport, 'optionalAccess', _3 => _3.width]), () => ( innerWidth))), bottom: top + (_nullishCoalesce(_optionalChain([viewport, 'optionalAccess', _4 => _4.height]), () => ( innerHeight))) };
30
+ for (let parent = root; parent; parent = parent.parentElement) {
31
+ const style = getComputedStyle(parent);
32
+ if (parent !== root && !/(auto|scroll|hidden|clip)/.test(`${style.overflowX} ${style.overflowY}`)) continue;
33
+ const rect = parent.getBoundingClientRect();
34
+ bounds.top = Math.max(bounds.top, rect.top + parent.clientTop);
35
+ bounds.left = Math.max(bounds.left, rect.left + parent.clientLeft);
36
+ bounds.right = Math.min(bounds.right, rect.left + parent.clientLeft + parent.clientWidth);
37
+ bounds.bottom = Math.min(bounds.bottom, rect.top + parent.clientTop + parent.clientHeight);
38
+ }
39
+ return bounds;
40
+ }
41
+ var ScrollNavigator = _react.forwardRef.call(void 0, function ScrollNavigator2({
42
+ items,
43
+ label = "Browse items",
44
+ topOffset = 0,
45
+ scrollRoot,
46
+ onNavigate,
47
+ className,
48
+ ...props
49
+ }, forwardedRef) {
50
+ const rootRef = _react.useRef.call(void 0, null);
51
+ const targets = _react.useRef.call(void 0, /* @__PURE__ */ new Map());
52
+ const controls = _react.useRef.call(void 0, /* @__PURE__ */ new Map());
53
+ const indexRef = _react.useRef.call(void 0, null);
54
+ const scroller = _react.useRef.call(void 0, null);
55
+ const [placement, setPlacement] = _react.useState.call(void 0, initialPlacement);
56
+ const [focusId, setFocusId] = _react.useState.call(void 0, null);
57
+ const id = _react.useId.call(void 0, );
58
+ const keys = JSON.stringify(items.map((item) => item.id));
59
+ const navigable = items.length > 1;
60
+ _react.useLayoutEffect.call(void 0, () => {
61
+ const element = rootRef.current;
62
+ if (!element || !navigable) return;
63
+ let frame = 0;
64
+ let intersects = true;
65
+ const measure = () => {
66
+ frame = 0;
67
+ if (!intersects) {
68
+ setPlacement((previous) => previous.visible ? { ...previous, visible: false } : previous);
69
+ return;
70
+ }
71
+ const scrollElement = scrollRoot === void 0 ? findScrollRoot(element) : scrollRoot;
72
+ scroller.current = scrollElement;
73
+ const view = viewportBounds(scrollElement);
74
+ const box = element.getBoundingClientRect();
75
+ const mode = box.left - view.left >= RAIL_WIDTH + GAP ? "rail" : "row";
76
+ const top = Math.max(view.top + topOffset + GAP, box.top);
77
+ const bottom = Math.min(view.bottom, box.bottom) - GAP;
78
+ const visible = box.width > 0 && box.right > view.left && box.left < view.right && bottom - top >= ROW_HEIGHT;
79
+ const readingEdge = view.top + topOffset + GAP + (mode === "row" ? ROW_HEIGHT : 0);
80
+ const active = visible ? Math.max(0, items.findIndex((item) => (_nullishCoalesce(_optionalChain([targets, 'access', _5 => _5.current, 'access', _6 => _6.get, 'call', _7 => _7(item.id), 'optionalAccess', _8 => _8.getBoundingClientRect, 'call', _9 => _9(), 'access', _10 => _10.bottom]), () => ( 0))) > readingEdge + 1)) : 0;
81
+ const next = {
82
+ mode,
83
+ top,
84
+ left: mode === "rail" ? box.left - RAIL_WIDTH - GAP : Math.max(box.left, view.left),
85
+ width: mode === "rail" ? RAIL_WIDTH : Math.max(0, Math.min(box.right, view.right) - Math.max(box.left, view.left)),
86
+ maxHeight: Math.max(0, bottom - top),
87
+ active,
88
+ visible
89
+ };
90
+ setPlacement((previous) => Object.keys(next).every((key) => previous[key] === next[key]) ? previous : next);
91
+ };
92
+ const schedule = () => {
93
+ if (!frame) frame = requestAnimationFrame(measure);
94
+ };
95
+ const visibility = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver((entries) => {
96
+ intersects = _nullishCoalesce(_optionalChain([entries, 'access', _11 => _11[entries.length - 1], 'optionalAccess', _12 => _12.isIntersecting]), () => ( intersects));
97
+ schedule();
98
+ });
99
+ _optionalChain([visibility, 'optionalAccess', _13 => _13.observe, 'call', _14 => _14(element)]);
100
+ const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(schedule);
101
+ for (let parent = element; parent; parent = parent.parentElement) {
102
+ _optionalChain([resize, 'optionalAccess', _15 => _15.observe, 'call', _16 => _16(parent)]);
103
+ }
104
+ targets.current.forEach((target) => _optionalChain([resize, 'optionalAccess', _17 => _17.observe, 'call', _18 => _18(target)]));
105
+ document.addEventListener("scroll", schedule, { passive: true, capture: true });
106
+ window.addEventListener("resize", schedule);
107
+ _optionalChain([window, 'access', _19 => _19.visualViewport, 'optionalAccess', _20 => _20.addEventListener, 'call', _21 => _21("resize", schedule)]);
108
+ _optionalChain([window, 'access', _22 => _22.visualViewport, 'optionalAccess', _23 => _23.addEventListener, 'call', _24 => _24("scroll", schedule)]);
109
+ measure();
110
+ return () => {
111
+ cancelAnimationFrame(frame);
112
+ _optionalChain([resize, 'optionalAccess', _25 => _25.disconnect, 'call', _26 => _26()]);
113
+ _optionalChain([visibility, 'optionalAccess', _27 => _27.disconnect, 'call', _28 => _28()]);
114
+ document.removeEventListener("scroll", schedule, true);
115
+ window.removeEventListener("resize", schedule);
116
+ _optionalChain([window, 'access', _29 => _29.visualViewport, 'optionalAccess', _30 => _30.removeEventListener, 'call', _31 => _31("resize", schedule)]);
117
+ _optionalChain([window, 'access', _32 => _32.visualViewport, 'optionalAccess', _33 => _33.removeEventListener, 'call', _34 => _34("scroll", schedule)]);
118
+ };
119
+ }, [keys, navigable, topOffset, scrollRoot]);
120
+ _react.useEffect.call(void 0, () => {
121
+ const index = indexRef.current;
122
+ const button = controls.current.get(_nullishCoalesce(_nullishCoalesce(focusId, () => ( _optionalChain([items, 'access', _35 => _35[placement.active], 'optionalAccess', _36 => _36.id]))), () => ( "")));
123
+ if (!index || !button || !placement.visible) return;
124
+ const horizontal = placement.mode === "row";
125
+ const start = horizontal ? button.offsetLeft : button.offsetTop;
126
+ const end = start + (horizontal ? button.offsetWidth : button.offsetHeight);
127
+ const position = horizontal ? index.scrollLeft : index.scrollTop;
128
+ const size = horizontal ? index.clientWidth : index.clientHeight;
129
+ const next = start < position ? start : end > position + size ? end - size : position;
130
+ if (horizontal) index.scrollLeft = next;
131
+ else index.scrollTop = next;
132
+ }, [placement.active, placement.mode, placement.visible, focusId, keys]);
133
+ function navigate(item) {
134
+ const target = targets.current.get(item.id);
135
+ if (!target) return;
136
+ const options = {
137
+ offset: topOffset + GAP + (placement.mode === "row" ? ROW_HEIGHT : 0),
138
+ behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth"
139
+ };
140
+ if (_optionalChain([onNavigate, 'optionalCall', _37 => _37(item.id, target, options)]) === true) return;
141
+ const scrollElement = scroller.current;
142
+ const view = viewportBounds(scrollElement);
143
+ const top = (_nullishCoalesce(_optionalChain([scrollElement, 'optionalAccess', _38 => _38.scrollTop]), () => ( window.scrollY))) + target.getBoundingClientRect().top - view.top - options.offset;
144
+ if (scrollElement) scrollElement.scrollTo({ top, behavior: options.behavior });
145
+ else window.scrollTo({ top, behavior: options.behavior });
146
+ }
147
+ const activeId = _optionalChain([items, 'access', _39 => _39[placement.active], 'optionalAccess', _40 => _40.id]);
148
+ const tabId = focusId && items.some((item) => item.id === focusId) ? focusId : activeId;
149
+ const row = placement.mode === "row";
150
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
151
+ "div",
152
+ {
153
+ ...props,
154
+ "data-slot": "scroll-navigator",
155
+ "data-orientation": row ? "horizontal" : "vertical",
156
+ ref: (element) => {
157
+ rootRef.current = element;
158
+ if (typeof forwardedRef === "function") forwardedRef(element);
159
+ else if (forwardedRef) forwardedRef.current = element;
160
+ },
161
+ className: _chunkKWLSBTB2cjs.cn.call(void 0, "relative min-w-0", navigable && row && "pt-16", className),
162
+ children: [
163
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "flex min-w-0 flex-col gap-4", children: items.map((item, index) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
164
+ "div",
165
+ {
166
+ id: `${id}-${index}`,
167
+ "data-slot": "scroll-navigator-item",
168
+ "data-item-id": item.id,
169
+ ref: (element) => {
170
+ if (element) targets.current.set(item.id, element);
171
+ else targets.current.delete(item.id);
172
+ },
173
+ className: "min-w-0",
174
+ children: item.content
175
+ },
176
+ item.id
177
+ )) }),
178
+ navigable && placement.visible && _reactdom.createPortal.call(void 0,
179
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
180
+ "nav",
181
+ {
182
+ "aria-label": label,
183
+ "data-slot": "scroll-navigator-controls",
184
+ "data-orientation": row ? "horizontal" : "vertical",
185
+ className: _chunkKWLSBTB2cjs.cn.call(void 0, "fixed z-20 flex rounded-full border border-border/60 bg-background/95 p-1 shadow-sm backdrop-blur-sm", row ? "items-center gap-1" : "flex-col items-center gap-1"),
186
+ style: { top: placement.top, left: placement.left, width: placement.width, maxHeight: placement.maxHeight },
187
+ onBlur: (event) => {
188
+ if (!event.currentTarget.contains(event.relatedTarget)) setFocusId(null);
189
+ },
190
+ children: [
191
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
192
+ "div",
193
+ {
194
+ ref: indexRef,
195
+ "data-slot": "scroll-navigator-index",
196
+ className: _chunkKWLSBTB2cjs.cn.call(void 0, "relative flex min-h-0 min-w-0 gap-1 overscroll-contain p-0.5 scrollbar-none", row ? "flex-1 overflow-x-auto" : "flex-col overflow-y-auto"),
197
+ onKeyDown: (event) => {
198
+ const current = items.findIndex((item) => controls.current.get(item.id) === document.activeElement);
199
+ let next = current;
200
+ if (event.key === "Home") next = 0;
201
+ else if (event.key === "End") next = items.length - 1;
202
+ else if (event.key === (row ? "ArrowRight" : "ArrowDown")) next = Math.min(items.length - 1, current + 1);
203
+ else if (event.key === (row ? "ArrowLeft" : "ArrowUp")) next = Math.max(0, current - 1);
204
+ else return;
205
+ event.preventDefault();
206
+ const button = controls.current.get(items[next].id);
207
+ _optionalChain([button, 'optionalAccess', _41 => _41.focus, 'call', _42 => _42({ preventScroll: true })]);
208
+ const index = indexRef.current;
209
+ if (index && button) {
210
+ if (row) index.scrollLeft = button.offsetLeft - (index.clientWidth - button.offsetWidth) / 2;
211
+ else index.scrollTop = button.offsetTop - (index.clientHeight - button.offsetHeight) / 2;
212
+ }
213
+ },
214
+ children: items.map((item, index) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
215
+ "button",
216
+ {
217
+ type: "button",
218
+ ref: (element) => {
219
+ if (element) controls.current.set(item.id, element);
220
+ else controls.current.delete(item.id);
221
+ },
222
+ "aria-label": `${item.label}, ${index + 1} / ${items.length}`,
223
+ title: item.label,
224
+ "aria-controls": `${id}-${index}`,
225
+ "aria-current": item.id === activeId ? "location" : void 0,
226
+ tabIndex: item.id === tabId ? 0 : -1,
227
+ onFocus: () => setFocusId(item.id),
228
+ onClick: () => navigate(item),
229
+ className: _chunkKWLSBTB2cjs.cn.call(void 0, "flex size-11 shrink-0 items-center justify-center rounded-full border-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", item.id === activeId ? "border-primary bg-primary/10" : "border-transparent opacity-60 hover:bg-muted hover:opacity-100 focus-visible:opacity-100"),
230
+ children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkWABCLRBRcjs.Avatar, { size: "xs", "aria-hidden": "true", children: [
231
+ item.imageSrc ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkWABCLRBRcjs.AvatarImage, { src: item.imageSrc, alt: "" }) : null,
232
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkWABCLRBRcjs.AvatarFallback, { textSize: "micro", children: _nullishCoalesce(item.fallback, () => ( item.label.trim().split(/\s+/).map((word) => word[0]).slice(0, 2).join(""))) })
233
+ ] })
234
+ },
235
+ item.id
236
+ ))
237
+ }
238
+ ),
239
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { "aria-hidden": "true", className: _chunkKWLSBTB2cjs.cn.call(void 0, "shrink-0 text-center text-[10px] tabular-nums text-muted-foreground", row ? "px-2" : "pb-1"), children: [
240
+ placement.active + 1,
241
+ " / ",
242
+ items.length
243
+ ] })
244
+ ]
245
+ }
246
+ ),
247
+ document.body
248
+ )
249
+ ]
250
+ }
251
+ );
252
+ });
253
+
254
+
255
+
256
+ exports.ScrollNavigator = ScrollNavigator;
@@ -1,4 +1,7 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }"use client";
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
2
+
3
+
4
+ var _chunk34AS3OCCcjs = require('./chunk-34AS3OCC.cjs');
2
5
 
3
6
 
4
7
  var _chunkJDEXZGIYcjs = require('./chunk-JDEXZGIY.cjs');
@@ -18,6 +21,21 @@ var _jsxruntime = require('react/jsx-runtime');
18
21
  function MessageScrollerProvider(props) {
19
22
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _messagescroller.MessageScroller.Provider, { ...props });
20
23
  }
24
+ function MessageScrollerNavigator(props) {
25
+ const { scrollToMessage } = _messagescroller.useMessageScroller.call(void 0, );
26
+ return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk34AS3OCCcjs.ScrollNavigator, { ...props, onNavigate: (id, target, options) => {
27
+ if (_optionalChain([props, 'access', _ => _.onNavigate, 'optionalCall', _2 => _2(id, target, options)]) === true) return true;
28
+ const message = target.closest("[data-message-id]");
29
+ const messageId = _optionalChain([message, 'optionalAccess', _3 => _3.dataset, 'access', _4 => _4.messageId]);
30
+ if (!messageId || !_optionalChain([message, 'optionalAccess', _5 => _5.parentElement])) return false;
31
+ const padding = Number.parseFloat(getComputedStyle(message.parentElement).paddingTop) || 0;
32
+ return scrollToMessage(messageId, {
33
+ align: "start",
34
+ behavior: options.behavior,
35
+ scrollMargin: message.getBoundingClientRect().top - target.getBoundingClientRect().top + options.offset - padding
36
+ });
37
+ } });
38
+ }
21
39
  function MessageScroller({
22
40
  className,
23
41
  ...props
@@ -129,4 +147,5 @@ function MessageScrollerButton({
129
147
 
130
148
 
131
149
 
132
- exports.useMessageScroller = _messagescroller.useMessageScroller; exports.useMessageScrollerScrollable = _messagescroller.useMessageScrollerScrollable; exports.useMessageScrollerVisibility = _messagescroller.useMessageScrollerVisibility; exports.MessageScrollerProvider = MessageScrollerProvider; exports.MessageScroller = MessageScroller; exports.MessageScrollerViewport = MessageScrollerViewport; exports.MessageScrollerContent = MessageScrollerContent; exports.MessageScrollerItem = MessageScrollerItem; exports.MessageScrollerButton = MessageScrollerButton;
150
+
151
+ exports.useMessageScroller = _messagescroller.useMessageScroller; exports.useMessageScrollerScrollable = _messagescroller.useMessageScrollerScrollable; exports.useMessageScrollerVisibility = _messagescroller.useMessageScrollerVisibility; exports.MessageScrollerProvider = MessageScrollerProvider; exports.MessageScrollerNavigator = MessageScrollerNavigator; exports.MessageScroller = MessageScroller; exports.MessageScrollerViewport = MessageScrollerViewport; exports.MessageScrollerContent = MessageScrollerContent; exports.MessageScrollerItem = MessageScrollerItem; exports.MessageScrollerButton = MessageScrollerButton;
@@ -1,4 +1,7 @@
1
1
  "use client";
2
+ import {
3
+ ScrollNavigator
4
+ } from "./chunk-VT2WDFSU.js";
2
5
  import {
3
6
  Button
4
7
  } from "./chunk-W326GKML.js";
@@ -18,6 +21,21 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
18
21
  function MessageScrollerProvider(props) {
19
22
  return /* @__PURE__ */ jsx(MessageScrollerPrimitive.Provider, { ...props });
20
23
  }
24
+ function MessageScrollerNavigator(props) {
25
+ const { scrollToMessage } = useMessageScroller();
26
+ return /* @__PURE__ */ jsx(ScrollNavigator, { ...props, onNavigate: (id, target, options) => {
27
+ if (props.onNavigate?.(id, target, options) === true) return true;
28
+ const message = target.closest("[data-message-id]");
29
+ const messageId = message?.dataset.messageId;
30
+ if (!messageId || !message?.parentElement) return false;
31
+ const padding = Number.parseFloat(getComputedStyle(message.parentElement).paddingTop) || 0;
32
+ return scrollToMessage(messageId, {
33
+ align: "start",
34
+ behavior: options.behavior,
35
+ scrollMargin: message.getBoundingClientRect().top - target.getBoundingClientRect().top + options.offset - padding
36
+ });
37
+ } });
38
+ }
21
39
  function MessageScroller({
22
40
  className,
23
41
  ...props
@@ -124,6 +142,7 @@ export {
124
142
  useMessageScrollerScrollable,
125
143
  useMessageScrollerVisibility,
126
144
  MessageScrollerProvider,
145
+ MessageScrollerNavigator,
127
146
  MessageScroller,
128
147
  MessageScrollerViewport,
129
148
  MessageScrollerContent,
@@ -0,0 +1,256 @@
1
+ "use client";
2
+ import {
3
+ Avatar,
4
+ AvatarFallback,
5
+ AvatarImage
6
+ } from "./chunk-3OWGL4Q2.js";
7
+ import {
8
+ cn
9
+ } from "./chunk-G6SRSWXB.js";
10
+
11
+ // src/primitives/scroll-navigator.tsx
12
+ import { forwardRef, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
13
+ import { createPortal } from "react-dom";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ var GAP = 12;
16
+ var RAIL_WIDTH = 56;
17
+ var ROW_HEIGHT = 64;
18
+ var initialPlacement = { mode: "row", top: 0, left: 0, width: 0, maxHeight: 0, active: 0, visible: false };
19
+ function findScrollRoot(element) {
20
+ for (let parent = element.parentElement; parent; parent = parent.parentElement) {
21
+ if (/(auto|scroll|overlay)/.test(getComputedStyle(parent).overflowY) && parent.scrollHeight > parent.clientHeight) return parent;
22
+ }
23
+ return null;
24
+ }
25
+ function viewportBounds(root) {
26
+ const viewport = window.visualViewport;
27
+ const top = viewport?.offsetTop ?? 0;
28
+ const left = viewport?.offsetLeft ?? 0;
29
+ const bounds = { top, left, right: left + (viewport?.width ?? innerWidth), bottom: top + (viewport?.height ?? innerHeight) };
30
+ for (let parent = root; parent; parent = parent.parentElement) {
31
+ const style = getComputedStyle(parent);
32
+ if (parent !== root && !/(auto|scroll|hidden|clip)/.test(`${style.overflowX} ${style.overflowY}`)) continue;
33
+ const rect = parent.getBoundingClientRect();
34
+ bounds.top = Math.max(bounds.top, rect.top + parent.clientTop);
35
+ bounds.left = Math.max(bounds.left, rect.left + parent.clientLeft);
36
+ bounds.right = Math.min(bounds.right, rect.left + parent.clientLeft + parent.clientWidth);
37
+ bounds.bottom = Math.min(bounds.bottom, rect.top + parent.clientTop + parent.clientHeight);
38
+ }
39
+ return bounds;
40
+ }
41
+ var ScrollNavigator = forwardRef(function ScrollNavigator2({
42
+ items,
43
+ label = "Browse items",
44
+ topOffset = 0,
45
+ scrollRoot,
46
+ onNavigate,
47
+ className,
48
+ ...props
49
+ }, forwardedRef) {
50
+ const rootRef = useRef(null);
51
+ const targets = useRef(/* @__PURE__ */ new Map());
52
+ const controls = useRef(/* @__PURE__ */ new Map());
53
+ const indexRef = useRef(null);
54
+ const scroller = useRef(null);
55
+ const [placement, setPlacement] = useState(initialPlacement);
56
+ const [focusId, setFocusId] = useState(null);
57
+ const id = useId();
58
+ const keys = JSON.stringify(items.map((item) => item.id));
59
+ const navigable = items.length > 1;
60
+ useLayoutEffect(() => {
61
+ const element = rootRef.current;
62
+ if (!element || !navigable) return;
63
+ let frame = 0;
64
+ let intersects = true;
65
+ const measure = () => {
66
+ frame = 0;
67
+ if (!intersects) {
68
+ setPlacement((previous) => previous.visible ? { ...previous, visible: false } : previous);
69
+ return;
70
+ }
71
+ const scrollElement = scrollRoot === void 0 ? findScrollRoot(element) : scrollRoot;
72
+ scroller.current = scrollElement;
73
+ const view = viewportBounds(scrollElement);
74
+ const box = element.getBoundingClientRect();
75
+ const mode = box.left - view.left >= RAIL_WIDTH + GAP ? "rail" : "row";
76
+ const top = Math.max(view.top + topOffset + GAP, box.top);
77
+ const bottom = Math.min(view.bottom, box.bottom) - GAP;
78
+ const visible = box.width > 0 && box.right > view.left && box.left < view.right && bottom - top >= ROW_HEIGHT;
79
+ const readingEdge = view.top + topOffset + GAP + (mode === "row" ? ROW_HEIGHT : 0);
80
+ const active = visible ? Math.max(0, items.findIndex((item) => (targets.current.get(item.id)?.getBoundingClientRect().bottom ?? 0) > readingEdge + 1)) : 0;
81
+ const next = {
82
+ mode,
83
+ top,
84
+ left: mode === "rail" ? box.left - RAIL_WIDTH - GAP : Math.max(box.left, view.left),
85
+ width: mode === "rail" ? RAIL_WIDTH : Math.max(0, Math.min(box.right, view.right) - Math.max(box.left, view.left)),
86
+ maxHeight: Math.max(0, bottom - top),
87
+ active,
88
+ visible
89
+ };
90
+ setPlacement((previous) => Object.keys(next).every((key) => previous[key] === next[key]) ? previous : next);
91
+ };
92
+ const schedule = () => {
93
+ if (!frame) frame = requestAnimationFrame(measure);
94
+ };
95
+ const visibility = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver((entries) => {
96
+ intersects = entries[entries.length - 1]?.isIntersecting ?? intersects;
97
+ schedule();
98
+ });
99
+ visibility?.observe(element);
100
+ const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(schedule);
101
+ for (let parent = element; parent; parent = parent.parentElement) {
102
+ resize?.observe(parent);
103
+ }
104
+ targets.current.forEach((target) => resize?.observe(target));
105
+ document.addEventListener("scroll", schedule, { passive: true, capture: true });
106
+ window.addEventListener("resize", schedule);
107
+ window.visualViewport?.addEventListener("resize", schedule);
108
+ window.visualViewport?.addEventListener("scroll", schedule);
109
+ measure();
110
+ return () => {
111
+ cancelAnimationFrame(frame);
112
+ resize?.disconnect();
113
+ visibility?.disconnect();
114
+ document.removeEventListener("scroll", schedule, true);
115
+ window.removeEventListener("resize", schedule);
116
+ window.visualViewport?.removeEventListener("resize", schedule);
117
+ window.visualViewport?.removeEventListener("scroll", schedule);
118
+ };
119
+ }, [keys, navigable, topOffset, scrollRoot]);
120
+ useEffect(() => {
121
+ const index = indexRef.current;
122
+ const button = controls.current.get(focusId ?? items[placement.active]?.id ?? "");
123
+ if (!index || !button || !placement.visible) return;
124
+ const horizontal = placement.mode === "row";
125
+ const start = horizontal ? button.offsetLeft : button.offsetTop;
126
+ const end = start + (horizontal ? button.offsetWidth : button.offsetHeight);
127
+ const position = horizontal ? index.scrollLeft : index.scrollTop;
128
+ const size = horizontal ? index.clientWidth : index.clientHeight;
129
+ const next = start < position ? start : end > position + size ? end - size : position;
130
+ if (horizontal) index.scrollLeft = next;
131
+ else index.scrollTop = next;
132
+ }, [placement.active, placement.mode, placement.visible, focusId, keys]);
133
+ function navigate(item) {
134
+ const target = targets.current.get(item.id);
135
+ if (!target) return;
136
+ const options = {
137
+ offset: topOffset + GAP + (placement.mode === "row" ? ROW_HEIGHT : 0),
138
+ behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth"
139
+ };
140
+ if (onNavigate?.(item.id, target, options) === true) return;
141
+ const scrollElement = scroller.current;
142
+ const view = viewportBounds(scrollElement);
143
+ const top = (scrollElement?.scrollTop ?? window.scrollY) + target.getBoundingClientRect().top - view.top - options.offset;
144
+ if (scrollElement) scrollElement.scrollTo({ top, behavior: options.behavior });
145
+ else window.scrollTo({ top, behavior: options.behavior });
146
+ }
147
+ const activeId = items[placement.active]?.id;
148
+ const tabId = focusId && items.some((item) => item.id === focusId) ? focusId : activeId;
149
+ const row = placement.mode === "row";
150
+ return /* @__PURE__ */ jsxs(
151
+ "div",
152
+ {
153
+ ...props,
154
+ "data-slot": "scroll-navigator",
155
+ "data-orientation": row ? "horizontal" : "vertical",
156
+ ref: (element) => {
157
+ rootRef.current = element;
158
+ if (typeof forwardedRef === "function") forwardedRef(element);
159
+ else if (forwardedRef) forwardedRef.current = element;
160
+ },
161
+ className: cn("relative min-w-0", navigable && row && "pt-16", className),
162
+ children: [
163
+ /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-col gap-4", children: items.map((item, index) => /* @__PURE__ */ jsx(
164
+ "div",
165
+ {
166
+ id: `${id}-${index}`,
167
+ "data-slot": "scroll-navigator-item",
168
+ "data-item-id": item.id,
169
+ ref: (element) => {
170
+ if (element) targets.current.set(item.id, element);
171
+ else targets.current.delete(item.id);
172
+ },
173
+ className: "min-w-0",
174
+ children: item.content
175
+ },
176
+ item.id
177
+ )) }),
178
+ navigable && placement.visible && createPortal(
179
+ /* @__PURE__ */ jsxs(
180
+ "nav",
181
+ {
182
+ "aria-label": label,
183
+ "data-slot": "scroll-navigator-controls",
184
+ "data-orientation": row ? "horizontal" : "vertical",
185
+ className: cn("fixed z-20 flex rounded-full border border-border/60 bg-background/95 p-1 shadow-sm backdrop-blur-sm", row ? "items-center gap-1" : "flex-col items-center gap-1"),
186
+ style: { top: placement.top, left: placement.left, width: placement.width, maxHeight: placement.maxHeight },
187
+ onBlur: (event) => {
188
+ if (!event.currentTarget.contains(event.relatedTarget)) setFocusId(null);
189
+ },
190
+ children: [
191
+ /* @__PURE__ */ jsx(
192
+ "div",
193
+ {
194
+ ref: indexRef,
195
+ "data-slot": "scroll-navigator-index",
196
+ className: cn("relative flex min-h-0 min-w-0 gap-1 overscroll-contain p-0.5 scrollbar-none", row ? "flex-1 overflow-x-auto" : "flex-col overflow-y-auto"),
197
+ onKeyDown: (event) => {
198
+ const current = items.findIndex((item) => controls.current.get(item.id) === document.activeElement);
199
+ let next = current;
200
+ if (event.key === "Home") next = 0;
201
+ else if (event.key === "End") next = items.length - 1;
202
+ else if (event.key === (row ? "ArrowRight" : "ArrowDown")) next = Math.min(items.length - 1, current + 1);
203
+ else if (event.key === (row ? "ArrowLeft" : "ArrowUp")) next = Math.max(0, current - 1);
204
+ else return;
205
+ event.preventDefault();
206
+ const button = controls.current.get(items[next].id);
207
+ button?.focus({ preventScroll: true });
208
+ const index = indexRef.current;
209
+ if (index && button) {
210
+ if (row) index.scrollLeft = button.offsetLeft - (index.clientWidth - button.offsetWidth) / 2;
211
+ else index.scrollTop = button.offsetTop - (index.clientHeight - button.offsetHeight) / 2;
212
+ }
213
+ },
214
+ children: items.map((item, index) => /* @__PURE__ */ jsx(
215
+ "button",
216
+ {
217
+ type: "button",
218
+ ref: (element) => {
219
+ if (element) controls.current.set(item.id, element);
220
+ else controls.current.delete(item.id);
221
+ },
222
+ "aria-label": `${item.label}, ${index + 1} / ${items.length}`,
223
+ title: item.label,
224
+ "aria-controls": `${id}-${index}`,
225
+ "aria-current": item.id === activeId ? "location" : void 0,
226
+ tabIndex: item.id === tabId ? 0 : -1,
227
+ onFocus: () => setFocusId(item.id),
228
+ onClick: () => navigate(item),
229
+ className: cn("flex size-11 shrink-0 items-center justify-center rounded-full border-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", item.id === activeId ? "border-primary bg-primary/10" : "border-transparent opacity-60 hover:bg-muted hover:opacity-100 focus-visible:opacity-100"),
230
+ children: /* @__PURE__ */ jsxs(Avatar, { size: "xs", "aria-hidden": "true", children: [
231
+ item.imageSrc ? /* @__PURE__ */ jsx(AvatarImage, { src: item.imageSrc, alt: "" }) : null,
232
+ /* @__PURE__ */ jsx(AvatarFallback, { textSize: "micro", children: item.fallback ?? item.label.trim().split(/\s+/).map((word) => word[0]).slice(0, 2).join("") })
233
+ ] })
234
+ },
235
+ item.id
236
+ ))
237
+ }
238
+ ),
239
+ /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: cn("shrink-0 text-center text-[10px] tabular-nums text-muted-foreground", row ? "px-2" : "pb-1"), children: [
240
+ placement.active + 1,
241
+ " / ",
242
+ items.length
243
+ ] })
244
+ ]
245
+ }
246
+ ),
247
+ document.body
248
+ )
249
+ ]
250
+ }
251
+ );
252
+ });
253
+
254
+ export {
255
+ ScrollNavigator
256
+ };
@@ -2,15 +2,15 @@
2
2
  "use client";
3
3
 
4
4
 
5
- var _chunkQWUMIAYDcjs = require('../chunk-QWUMIAYD.cjs');
6
5
 
7
6
 
8
- var _chunkYMI3X2YLcjs = require('../chunk-YMI3X2YL.cjs');
7
+ var _chunk2BTDBZYNcjs = require('../chunk-2BTDBZYN.cjs');
9
8
 
10
9
 
10
+ var _chunkQWUMIAYDcjs = require('../chunk-QWUMIAYD.cjs');
11
11
 
12
12
 
13
- var _chunk2BTDBZYNcjs = require('../chunk-2BTDBZYN.cjs');
13
+ var _chunkYMI3X2YLcjs = require('../chunk-YMI3X2YL.cjs');
14
14
 
15
15
 
16
16
 
@@ -26,7 +26,8 @@ var _chunkB765DSRGcjs = require('../chunk-B765DSRG.cjs');
26
26
 
27
27
 
28
28
 
29
- var _chunk5VMKDVB6cjs = require('../chunk-5VMKDVB6.cjs');
29
+ var _chunkPD5UQEKYcjs = require('../chunk-PD5UQEKY.cjs');
30
+ require('../chunk-34AS3OCC.cjs');
30
31
 
31
32
 
32
33
 
@@ -75,6 +76,7 @@ var _chunkFYRQFEU4cjs = require('../chunk-FYRQFEU4.cjs');
75
76
 
76
77
  var _chunk2KFZQJH5cjs = require('../chunk-2KFZQJH5.cjs');
77
78
  require('../chunk-PQUSJEXL.cjs');
79
+ require('../chunk-WABCLRBR.cjs');
78
80
  require('../chunk-NKWHDI4C.cjs');
79
81
 
80
82
 
@@ -176,15 +178,15 @@ var ChatThread = _react.forwardRef.call(void 0,
176
178
  const markers = timelineMarkers(messages, /* @__PURE__ */ new Date());
177
179
  const anchorId = useArrivingUserTurnId(messages);
178
180
  const workspace = appearance === "workspace";
179
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5VMKDVB6cjs.MessageScrollerProvider, { autoScroll: true, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
180
- _chunk5VMKDVB6cjs.MessageScroller,
181
+ return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkPD5UQEKYcjs.MessageScrollerProvider, { autoScroll: true, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
182
+ _chunkPD5UQEKYcjs.MessageScroller,
181
183
  {
182
184
  ref,
183
185
  style: { maxHeight },
184
186
  className: _chunkKWLSBTB2cjs.cn.call(void 0, "min-h-0", className),
185
187
  children: [
186
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5VMKDVB6cjs.MessageScrollerViewport, { children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
187
- _chunk5VMKDVB6cjs.MessageScrollerContent,
188
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkPD5UQEKYcjs.MessageScrollerViewport, { children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
189
+ _chunkPD5UQEKYcjs.MessageScrollerContent,
188
190
  {
189
191
  className: _chunkKWLSBTB2cjs.cn.call(void 0,
190
192
  workspace ? "mx-auto w-full max-w-3xl gap-10 px-4 py-6 md:px-6 md:py-8" : "gap-2.5 px-px py-px"
@@ -193,7 +195,7 @@ var ChatThread = _react.forwardRef.call(void 0,
193
195
  messages.map((m, index) => {
194
196
  const isUser = m.role === "user";
195
197
  if (m.status === "running") {
196
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5VMKDVB6cjs.MessageScrollerItem, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
198
+ return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkPD5UQEKYcjs.MessageScrollerItem, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
197
199
  ChatRunningLine,
198
200
  {
199
201
  label: m.text || pendingLabel,
@@ -206,7 +208,7 @@ var ChatThread = _react.forwardRef.call(void 0,
206
208
  const marker = markers[index];
207
209
  const actions = messageActions && offersActions(m) ? messageActions(m) : null;
208
210
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
209
- _chunk5VMKDVB6cjs.MessageScrollerItem,
211
+ _chunkPD5UQEKYcjs.MessageScrollerItem,
210
212
  {
211
213
  messageId: m.id,
212
214
  scrollAnchor: m.id === anchorId,
@@ -262,15 +264,15 @@ var ChatThread = _react.forwardRef.call(void 0,
262
264
  m.id
263
265
  );
264
266
  }),
265
- pending && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5VMKDVB6cjs.MessageScrollerItem, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, ChatRunningLine, { label: pendingLabel, workspace }) }),
267
+ pending && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkPD5UQEKYcjs.MessageScrollerItem, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, ChatRunningLine, { label: pendingLabel, workspace }) }),
266
268
  footer && // Always painted: content-visibility:auto would contain the
267
269
  // footer's paint and layout, clipping a ring or shadow at its
268
270
  // edge and re-parenting anything fixed inside it.
269
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5VMKDVB6cjs.MessageScrollerItem, { className: "[content-visibility:visible]", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkNWDPH2Z7cjs.Message, { align: "start", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkNWDPH2Z7cjs.MessageContent, { children: footer }) }) })
271
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkPD5UQEKYcjs.MessageScrollerItem, { className: "[content-visibility:visible]", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkNWDPH2Z7cjs.Message, { align: "start", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkNWDPH2Z7cjs.MessageContent, { children: footer }) }) })
270
272
  ]
271
273
  }
272
274
  ) }),
273
- /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunk5VMKDVB6cjs.MessageScrollerButton, { variant: "outline", className: "rounded-full shadow-sm", children: [
275
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkPD5UQEKYcjs.MessageScrollerButton, { variant: "outline", className: "rounded-full shadow-sm", children: [
274
276
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.ArrowDown, { className: "h-4 w-4" }),
275
277
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "sr-only", children: scrollButtonLabel })
276
278
  ] })
@@ -1,16 +1,16 @@
1
1
  "use client";
2
2
  "use client";
3
+ import {
4
+ Trace,
5
+ TraceLine,
6
+ TraceTitle
7
+ } from "../chunk-Z45YKVAM.js";
3
8
  import {
4
9
  VoiceDictationButton
5
10
  } from "../chunk-JKVVTFAP.js";
6
11
  import {
7
12
  VoiceSessionControls
8
13
  } from "../chunk-KF4X37DP.js";
9
- import {
10
- Trace,
11
- TraceLine,
12
- TraceTitle
13
- } from "../chunk-Z45YKVAM.js";
14
14
  import {
15
15
  Suggestion,
16
16
  Suggestions
@@ -26,7 +26,8 @@ import {
26
26
  MessageScrollerItem,
27
27
  MessageScrollerProvider,
28
28
  MessageScrollerViewport
29
- } from "../chunk-2V7AVB2U.js";
29
+ } from "../chunk-UBTLWSKK.js";
30
+ import "../chunk-VT2WDFSU.js";
30
31
  import {
31
32
  Message,
32
33
  MessageAction,
@@ -75,6 +76,7 @@ import {
75
76
  ActionDock
76
77
  } from "../chunk-42RTTHND.js";
77
78
  import "../chunk-QMA3WVB7.js";
79
+ import "../chunk-3OWGL4Q2.js";
78
80
  import "../chunk-5OF6D3GH.js";
79
81
  import {
80
82
  Button
@@ -10,7 +10,10 @@
10
10
 
11
11
 
12
12
 
13
- var _chunk5VMKDVB6cjs = require('../chunk-5VMKDVB6.cjs');
13
+
14
+ var _chunkPD5UQEKYcjs = require('../chunk-PD5UQEKY.cjs');
15
+ require('../chunk-34AS3OCC.cjs');
16
+ require('../chunk-WABCLRBR.cjs');
14
17
  require('../chunk-JDEXZGIY.cjs');
15
18
  require('../chunk-KWLSBTB2.cjs');
16
19
 
@@ -23,4 +26,5 @@ require('../chunk-KWLSBTB2.cjs');
23
26
 
24
27
 
25
28
 
26
- exports.MessageScroller = _chunk5VMKDVB6cjs.MessageScroller; exports.MessageScrollerButton = _chunk5VMKDVB6cjs.MessageScrollerButton; exports.MessageScrollerContent = _chunk5VMKDVB6cjs.MessageScrollerContent; exports.MessageScrollerItem = _chunk5VMKDVB6cjs.MessageScrollerItem; exports.MessageScrollerProvider = _chunk5VMKDVB6cjs.MessageScrollerProvider; exports.MessageScrollerViewport = _chunk5VMKDVB6cjs.MessageScrollerViewport; exports.useMessageScroller = _chunk5VMKDVB6cjs.useMessageScroller; exports.useMessageScrollerScrollable = _chunk5VMKDVB6cjs.useMessageScrollerScrollable; exports.useMessageScrollerVisibility = _chunk5VMKDVB6cjs.useMessageScrollerVisibility;
29
+
30
+ exports.MessageScroller = _chunkPD5UQEKYcjs.MessageScroller; exports.MessageScrollerButton = _chunkPD5UQEKYcjs.MessageScrollerButton; exports.MessageScrollerContent = _chunkPD5UQEKYcjs.MessageScrollerContent; exports.MessageScrollerItem = _chunkPD5UQEKYcjs.MessageScrollerItem; exports.MessageScrollerNavigator = _chunkPD5UQEKYcjs.MessageScrollerNavigator; exports.MessageScrollerProvider = _chunkPD5UQEKYcjs.MessageScrollerProvider; exports.MessageScrollerViewport = _chunkPD5UQEKYcjs.MessageScrollerViewport; exports.useMessageScroller = _chunkPD5UQEKYcjs.useMessageScroller; exports.useMessageScrollerScrollable = _chunkPD5UQEKYcjs.useMessageScrollerScrollable; exports.useMessageScrollerVisibility = _chunkPD5UQEKYcjs.useMessageScrollerVisibility;
@@ -3,14 +3,18 @@ import * as React from 'react';
3
3
  import { MessageScroller as MessageScroller$1 } from '@shadcn/react/message-scroller';
4
4
  export { useMessageScroller, useMessageScrollerScrollable, useMessageScrollerVisibility } from '@shadcn/react/message-scroller';
5
5
  import { Button } from './button.cjs';
6
+ import { ScrollNavigatorProps } from './scroll-navigator.cjs';
6
7
  import 'class-variance-authority/types';
7
8
  import 'class-variance-authority';
8
9
 
9
10
  declare function MessageScrollerProvider(props: React.ComponentProps<typeof MessageScroller$1.Provider>): react_jsx_runtime.JSX.Element;
11
+ /** A list index inside a message. Jumps use the thread's own anchoring API,
12
+ * so incoming content cannot snap the reader back to the end of the thread. */
13
+ declare function MessageScrollerNavigator(props: ScrollNavigatorProps): react_jsx_runtime.JSX.Element;
10
14
  declare function MessageScroller({ className, ...props }: React.ComponentProps<typeof MessageScroller$1.Root>): react_jsx_runtime.JSX.Element;
11
15
  declare function MessageScrollerViewport({ className, ...props }: React.ComponentProps<typeof MessageScroller$1.Viewport>): react_jsx_runtime.JSX.Element;
12
16
  declare function MessageScrollerContent({ className, ...props }: React.ComponentProps<typeof MessageScroller$1.Content>): react_jsx_runtime.JSX.Element;
13
17
  declare function MessageScrollerItem({ className, scrollAnchor, ...props }: React.ComponentProps<typeof MessageScroller$1.Item>): react_jsx_runtime.JSX.Element;
14
18
  declare function MessageScrollerButton({ direction, className, children, render, variant, size, ...props }: React.ComponentProps<typeof MessageScroller$1.Button> & Pick<React.ComponentProps<typeof Button>, "variant" | "size">): react_jsx_runtime.JSX.Element;
15
19
 
16
- export { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport };
20
+ export { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerNavigator, MessageScrollerProvider, MessageScrollerViewport };
@@ -3,14 +3,18 @@ import * as React from 'react';
3
3
  import { MessageScroller as MessageScroller$1 } from '@shadcn/react/message-scroller';
4
4
  export { useMessageScroller, useMessageScrollerScrollable, useMessageScrollerVisibility } from '@shadcn/react/message-scroller';
5
5
  import { Button } from './button.js';
6
+ import { ScrollNavigatorProps } from './scroll-navigator.js';
6
7
  import 'class-variance-authority/types';
7
8
  import 'class-variance-authority';
8
9
 
9
10
  declare function MessageScrollerProvider(props: React.ComponentProps<typeof MessageScroller$1.Provider>): react_jsx_runtime.JSX.Element;
11
+ /** A list index inside a message. Jumps use the thread's own anchoring API,
12
+ * so incoming content cannot snap the reader back to the end of the thread. */
13
+ declare function MessageScrollerNavigator(props: ScrollNavigatorProps): react_jsx_runtime.JSX.Element;
10
14
  declare function MessageScroller({ className, ...props }: React.ComponentProps<typeof MessageScroller$1.Root>): react_jsx_runtime.JSX.Element;
11
15
  declare function MessageScrollerViewport({ className, ...props }: React.ComponentProps<typeof MessageScroller$1.Viewport>): react_jsx_runtime.JSX.Element;
12
16
  declare function MessageScrollerContent({ className, ...props }: React.ComponentProps<typeof MessageScroller$1.Content>): react_jsx_runtime.JSX.Element;
13
17
  declare function MessageScrollerItem({ className, scrollAnchor, ...props }: React.ComponentProps<typeof MessageScroller$1.Item>): react_jsx_runtime.JSX.Element;
14
18
  declare function MessageScrollerButton({ direction, className, children, render, variant, size, ...props }: React.ComponentProps<typeof MessageScroller$1.Button> & Pick<React.ComponentProps<typeof Button>, "variant" | "size">): react_jsx_runtime.JSX.Element;
15
19
 
16
- export { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport };
20
+ export { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerNavigator, MessageScrollerProvider, MessageScrollerViewport };
@@ -5,12 +5,15 @@ import {
5
5
  MessageScrollerButton,
6
6
  MessageScrollerContent,
7
7
  MessageScrollerItem,
8
+ MessageScrollerNavigator,
8
9
  MessageScrollerProvider,
9
10
  MessageScrollerViewport,
10
11
  useMessageScroller,
11
12
  useMessageScrollerScrollable,
12
13
  useMessageScrollerVisibility
13
- } from "../chunk-2V7AVB2U.js";
14
+ } from "../chunk-UBTLWSKK.js";
15
+ import "../chunk-VT2WDFSU.js";
16
+ import "../chunk-3OWGL4Q2.js";
14
17
  import "../chunk-W326GKML.js";
15
18
  import "../chunk-G6SRSWXB.js";
16
19
  export {
@@ -18,6 +21,7 @@ export {
18
21
  MessageScrollerButton,
19
22
  MessageScrollerContent,
20
23
  MessageScrollerItem,
24
+ MessageScrollerNavigator,
21
25
  MessageScrollerProvider,
22
26
  MessageScrollerViewport,
23
27
  useMessageScroller,
@@ -0,0 +1,10 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});"use client";
2
+ "use client";
3
+
4
+
5
+ var _chunk34AS3OCCcjs = require('../chunk-34AS3OCC.cjs');
6
+ require('../chunk-WABCLRBR.cjs');
7
+ require('../chunk-KWLSBTB2.cjs');
8
+
9
+
10
+ exports.ScrollNavigator = _chunk34AS3OCCcjs.ScrollNavigator;
@@ -0,0 +1,40 @@
1
+ import * as React from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
+
4
+ interface ScrollNavigatorItem {
5
+ /** Stable within this list. Separate lists can contain the same IDs. */
6
+ id: string;
7
+ label: string;
8
+ imageSrc?: string | null;
9
+ /** Initials or an icon; defaults to the label's initials. */
10
+ fallback?: ReactNode;
11
+ content: ReactNode;
12
+ }
13
+ interface ScrollNavigatorScrollOptions {
14
+ /** Space above the target, including the horizontal navigator when present. */
15
+ offset: number;
16
+ behavior: ScrollBehavior;
17
+ }
18
+ interface ScrollNavigatorProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
19
+ items: readonly ScrollNavigatorItem[];
20
+ label?: string;
21
+ /** Space below a sticky header inside the scroll viewport, in pixels. */
22
+ topOffset?: number;
23
+ /** The nearest vertically scrolling ancestor is detected by default. */
24
+ scrollRoot?: HTMLElement | null;
25
+ /** Return true when a host scroller has handled the jump. */
26
+ onNavigate?: (id: string, target: HTMLElement, options: ScrollNavigatorScrollOptions) => boolean | void;
27
+ }
28
+ /**
29
+ * A list with an avatar index which follows the first item at the viewport's
30
+ * reading edge. The index lives only beside this list: it disappears before
31
+ * and after it. With no room in the left gutter it becomes a horizontal row.
32
+ * The row reserves space above the first item; later items scroll beneath it.
33
+ *
34
+ * Portalling the controls avoids clipping by cards, message bubbles and
35
+ * content-visibility containers. Their bounds still follow the list and its
36
+ * actual scroll viewport, never the full browser height of a nested pane.
37
+ */
38
+ declare const ScrollNavigator: React.ForwardRefExoticComponent<ScrollNavigatorProps & React.RefAttributes<HTMLDivElement>>;
39
+
40
+ export { ScrollNavigator, type ScrollNavigatorItem, type ScrollNavigatorProps, type ScrollNavigatorScrollOptions };
@@ -0,0 +1,40 @@
1
+ import * as React from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
+
4
+ interface ScrollNavigatorItem {
5
+ /** Stable within this list. Separate lists can contain the same IDs. */
6
+ id: string;
7
+ label: string;
8
+ imageSrc?: string | null;
9
+ /** Initials or an icon; defaults to the label's initials. */
10
+ fallback?: ReactNode;
11
+ content: ReactNode;
12
+ }
13
+ interface ScrollNavigatorScrollOptions {
14
+ /** Space above the target, including the horizontal navigator when present. */
15
+ offset: number;
16
+ behavior: ScrollBehavior;
17
+ }
18
+ interface ScrollNavigatorProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
19
+ items: readonly ScrollNavigatorItem[];
20
+ label?: string;
21
+ /** Space below a sticky header inside the scroll viewport, in pixels. */
22
+ topOffset?: number;
23
+ /** The nearest vertically scrolling ancestor is detected by default. */
24
+ scrollRoot?: HTMLElement | null;
25
+ /** Return true when a host scroller has handled the jump. */
26
+ onNavigate?: (id: string, target: HTMLElement, options: ScrollNavigatorScrollOptions) => boolean | void;
27
+ }
28
+ /**
29
+ * A list with an avatar index which follows the first item at the viewport's
30
+ * reading edge. The index lives only beside this list: it disappears before
31
+ * and after it. With no room in the left gutter it becomes a horizontal row.
32
+ * The row reserves space above the first item; later items scroll beneath it.
33
+ *
34
+ * Portalling the controls avoids clipping by cards, message bubbles and
35
+ * content-visibility containers. Their bounds still follow the list and its
36
+ * actual scroll viewport, never the full browser height of a nested pane.
37
+ */
38
+ declare const ScrollNavigator: React.ForwardRefExoticComponent<ScrollNavigatorProps & React.RefAttributes<HTMLDivElement>>;
39
+
40
+ export { ScrollNavigator, type ScrollNavigatorItem, type ScrollNavigatorProps, type ScrollNavigatorScrollOptions };
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ "use client";
3
+ import {
4
+ ScrollNavigator
5
+ } from "../chunk-VT2WDFSU.js";
6
+ import "../chunk-3OWGL4Q2.js";
7
+ import "../chunk-G6SRSWXB.js";
8
+ export {
9
+ ScrollNavigator
10
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicbagel/saas-kit",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "Accessible React primitives, layout components and design tokens for SaaS apps, built on Radix UI and Tailwind CSS v4.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -950,6 +950,16 @@
950
950
  "default": "./dist/primitives/scroll-area.cjs"
951
951
  }
952
952
  },
953
+ "./primitives/scroll-navigator": {
954
+ "import": {
955
+ "types": "./dist/primitives/scroll-navigator.d.ts",
956
+ "default": "./dist/primitives/scroll-navigator.js"
957
+ },
958
+ "require": {
959
+ "types": "./dist/primitives/scroll-navigator.d.cts",
960
+ "default": "./dist/primitives/scroll-navigator.cjs"
961
+ }
962
+ },
953
963
  "./primitives/search-input": {
954
964
  "import": {
955
965
  "types": "./dist/primitives/search-input.d.ts",