@iloveagents/foundry-web-ui 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/assistant-chat.d.ts +12 -1
- package/dist/components/assistant-chat.js +18 -13
- package/dist/components/chat-bubble.js +9 -6
- package/dist/components/chat-context.d.ts +2 -0
- package/dist/components/chat-context.js +70 -0
- package/dist/components/chat-slots.d.ts +10 -0
- package/dist/components/chat-slots.js +7 -0
- package/dist/components/composer-add-menu.js +3 -2
- package/dist/components/composer-input.d.ts +4 -0
- package/dist/components/composer-input.js +23 -0
- package/dist/components/context-badges.d.ts +4 -1
- package/dist/components/context-badges.js +7 -4
- package/dist/components/context-bar.d.ts +1 -0
- package/dist/components/context-bar.js +11 -3
- package/dist/components/data-table/data-table-frame.d.ts +1 -22
- package/dist/components/data-table/data-table-frame.js +21 -48
- package/dist/components/data-table/data-table-overflow.d.ts +5 -0
- package/dist/components/data-table/data-table-overflow.js +42 -0
- package/dist/components/data-table/data-table.js +88 -87
- package/dist/components/data-table/focus-pane-resize.d.ts +8 -0
- package/dist/components/data-table/focus-pane-resize.js +88 -0
- package/dist/components/focus-chat-pane.d.ts +8 -0
- package/dist/components/focus-chat-pane.js +9 -0
- package/dist/components/focus-dialog.d.ts +8 -0
- package/dist/components/focus-dialog.js +103 -0
- package/dist/components/focus-source-pane.d.ts +7 -0
- package/dist/components/focus-source-pane.js +15 -0
- package/dist/components/loading-indicator.js +1 -1
- package/dist/components/message-timestamp.js +3 -2
- package/dist/components/pin-state-icon.d.ts +5 -0
- package/dist/components/pin-state-icon.js +7 -0
- package/dist/components/pin-toggle.d.ts +7 -0
- package/dist/components/pin-toggle.js +10 -0
- package/dist/components/sidebar.d.ts +18 -1
- package/dist/components/sidebar.js +124 -74
- package/dist/components/tool-panel-layout.d.ts +3 -1
- package/dist/components/tool-panel-layout.js +5 -5
- package/dist/components/tool-panel.d.ts +8 -1
- package/dist/components/tool-panel.js +12 -8
- package/dist/components/user-menu.js +19 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/app-store.d.ts +0 -8
- package/dist/lib/app-store.js +14 -4
- package/dist/lib/chat-context-summary.d.ts +11 -0
- package/dist/lib/chat-context-summary.js +37 -0
- package/dist/lib/chat-lifecycle-store.d.ts +2 -0
- package/dist/lib/chat-lifecycle-store.js +6 -1
- package/dist/lib/client-tools.js +7 -0
- package/dist/lib/create-ui-id.d.ts +2 -0
- package/dist/lib/create-ui-id.js +9 -0
- package/dist/lib/focus-context.d.ts +2 -0
- package/dist/lib/focus-context.js +78 -0
- package/dist/lib/nav-config.d.ts +7 -0
- package/dist/lib/tool-panel-store.d.ts +7 -6
- package/dist/lib/tool-panel-store.js +29 -3
- package/dist/lib/use-new-conversation.js +1 -1
- package/dist/workbench/chat-header.d.ts +14 -0
- package/dist/workbench/chat-header.js +12 -0
- package/dist/workbench/chat-resize-handle.d.ts +4 -0
- package/dist/workbench/chat-resize-handle.js +40 -0
- package/dist/workbench/content-pane.d.ts +14 -0
- package/dist/workbench/content-pane.js +14 -0
- package/dist/workbench/conversation-history.d.ts +11 -0
- package/dist/workbench/conversation-history.js +83 -0
- package/dist/workbench/navigation.d.ts +6 -0
- package/dist/workbench/navigation.js +6 -0
- package/dist/workbench/selection-bridge.d.ts +5 -0
- package/dist/workbench/selection-bridge.js +84 -0
- package/dist/workbench/use-chat-width.d.ts +14 -0
- package/dist/workbench/use-chat-width.js +56 -0
- package/dist/workbench/use-workbench-state.d.ts +50 -0
- package/dist/workbench/use-workbench-state.js +145 -0
- package/dist/workbench/welcome-intro.d.ts +6 -0
- package/dist/workbench/welcome-intro.js +10 -0
- package/dist/workbench/workbench-styles.d.ts +1 -0
- package/dist/workbench/workbench-styles.js +290 -0
- package/dist/workbench/workbench.d.ts +5 -0
- package/dist/workbench/workbench.js +32 -0
- package/dist/workbench.d.ts +2 -0
- package/dist/workbench.js +2 -0
- package/package.json +8 -4
|
@@ -1,61 +1,34 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
* The frame around a list: normally a plain container, in focus mode a
|
|
4
|
-
* fullscreen layer (Escape leaves it). `DataTableToolbar` shows the focus
|
|
5
|
-
* button whenever it renders inside a frame — users with wide lists get a
|
|
6
|
-
* way out of a cramped page without the host doing anything.
|
|
7
|
-
*
|
|
8
|
-
* The focus layer stays where it is in the tree — only its classes change —
|
|
9
|
-
* so entering and leaving focus never remounts the list (its rows, drawer,
|
|
10
|
-
* selection and in-flight edits survive) and it keeps inheriting the host's
|
|
11
|
-
* theme scope. It never scrolls itself: it is a fixed flex column whose
|
|
12
|
-
* growing child — the table — owns the scrolling, which keeps the toolbar in
|
|
13
|
-
* place while the rows move under a sticky header (see `DataTable`, which
|
|
14
|
-
* reads this context).
|
|
15
|
-
*
|
|
16
|
-
* A `position: fixed` layer is relative to the viewport unless an ancestor
|
|
17
|
-
* establishes a containing block (`transform`, `filter`, `perspective`,
|
|
18
|
-
* `contain: paint`); a host that does that on a list's ancestor has to opt
|
|
19
|
-
* out of focus mode.
|
|
20
|
-
*/
|
|
21
|
-
import { createContext, forwardRef, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, forwardRef, useContext, useMemo, useRef, useState, } from "react";
|
|
22
3
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
4
|
+
import { TooltipProvider } from "../../ui/tooltip.js";
|
|
5
|
+
import { Dialog, DialogPortal } from "../../ui/dialog.js";
|
|
6
|
+
import { FocusDialog } from "../focus-dialog.js";
|
|
23
7
|
const DataTableFrameContext = createContext(null);
|
|
24
|
-
/** The enclosing frame's focus state, or `null` outside a frame. */
|
|
25
8
|
export function useDataTableFrame() {
|
|
26
9
|
return useContext(DataTableFrameContext);
|
|
27
10
|
}
|
|
28
11
|
export const DataTableFrame = forwardRef(function DataTableFrame({ children, className, focusClassName, defaultFocused = false, onFocusChange, ...rest }, ref) {
|
|
29
12
|
const [focused, setFocused] = useState(defaultFocused);
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (!focused)
|
|
34
|
-
return;
|
|
35
|
-
const onKeyDown = (event) => {
|
|
36
|
-
// Through the same door as the button: a host told about focus mode
|
|
37
|
-
// must hear about it however it ended.
|
|
38
|
-
if (event.key === "Escape") {
|
|
39
|
-
setFocused(false);
|
|
40
|
-
changeRef.current?.(false);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
document.addEventListener("keydown", onKeyDown);
|
|
44
|
-
const previousOverflow = document.body.style.overflow;
|
|
45
|
-
document.body.style.overflow = "hidden";
|
|
46
|
-
return () => {
|
|
47
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
48
|
-
document.body.style.overflow = previousOverflow;
|
|
49
|
-
};
|
|
50
|
-
}, [focused]);
|
|
13
|
+
const [host] = useState(() => typeof document === "undefined" ? null : document.createElement("div"));
|
|
14
|
+
const returnFocus = useRef(null);
|
|
51
15
|
const value = useMemo(() => ({
|
|
52
16
|
focused,
|
|
53
17
|
setFocused: (next) => {
|
|
18
|
+
if (next && !focused)
|
|
19
|
+
returnFocus.current = document.activeElement;
|
|
54
20
|
setFocused(next);
|
|
55
|
-
|
|
21
|
+
onFocusChange?.(next);
|
|
56
22
|
},
|
|
57
|
-
}), [focused]);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
23
|
+
}), [focused, onFocusChange]);
|
|
24
|
+
const attach = (element) => {
|
|
25
|
+
if (element && host && host.parentElement !== element) {
|
|
26
|
+
host.style.display = "contents";
|
|
27
|
+
element.appendChild(host);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const content = (_jsx("div", { ...rest, ref: ref, "data-focused": focused ? "" : undefined, "data-focus-title": rest["aria-label"] || "Focused content", className: focused
|
|
31
|
+
? cn("flex min-h-0 flex-1 flex-col gap-3 overflow-hidden bg-background p-4 sm:p-6", focusClassName)
|
|
32
|
+
: className, children: children }));
|
|
33
|
+
return (_jsx(DataTableFrameContext.Provider, { value: value, children: _jsxs(TooltipProvider, { children: [_jsx("div", { ref: focused ? undefined : attach, className: "contents" }), _jsxs(Dialog, { open: focused, onOpenChange: value.setFocused, children: [_jsx(FocusDialog, { focused: focused, setFocused: value.setFocused, host: host, attach: attach, title: rest["aria-label"] || "Table focus", onReturnFocus: () => returnFocus.current?.focus() }), host ? (_jsx(DialogPortal, { container: host, forceMount: true, children: content })) : (content)] })] }) }));
|
|
61
34
|
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
/** Discoverable, keyboard-accessible navigation for a table wider than its pane. */
|
|
3
|
+
export declare function DataTableOverflow({ scroller }: {
|
|
4
|
+
scroller: RefObject<HTMLDivElement | null>;
|
|
5
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
+
import { Button } from "@iloveagents/foundry-web-primitives";
|
|
5
|
+
/** Discoverable, keyboard-accessible navigation for a table wider than its pane. */
|
|
6
|
+
export function DataTableOverflow({ scroller }) {
|
|
7
|
+
const [edges, setEdges] = useState({ previous: false, next: false });
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const element = scroller.current;
|
|
10
|
+
if (!element)
|
|
11
|
+
return;
|
|
12
|
+
const read = () => {
|
|
13
|
+
const position = Math.abs(element.scrollLeft);
|
|
14
|
+
const maximum = Math.max(0, element.scrollWidth - element.clientWidth);
|
|
15
|
+
const next = { previous: position > 1, next: position < maximum - 1 };
|
|
16
|
+
setEdges((current) => current.previous === next.previous && current.next === next.next ? current : next);
|
|
17
|
+
};
|
|
18
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(read);
|
|
19
|
+
observer?.observe(element);
|
|
20
|
+
if (element.firstElementChild)
|
|
21
|
+
observer?.observe(element.firstElementChild);
|
|
22
|
+
element.addEventListener("scroll", read, { passive: true });
|
|
23
|
+
read();
|
|
24
|
+
return () => {
|
|
25
|
+
observer?.disconnect();
|
|
26
|
+
element.removeEventListener("scroll", read);
|
|
27
|
+
};
|
|
28
|
+
}, [scroller]);
|
|
29
|
+
if (!edges.previous && !edges.next)
|
|
30
|
+
return null;
|
|
31
|
+
const move = (direction) => {
|
|
32
|
+
const element = scroller.current;
|
|
33
|
+
if (!element)
|
|
34
|
+
return;
|
|
35
|
+
const sign = getComputedStyle(element).direction === "rtl" ? -1 : 1;
|
|
36
|
+
element.scrollBy({
|
|
37
|
+
left: direction * sign * Math.max(160, element.clientWidth * 0.7),
|
|
38
|
+
behavior: "auto",
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
return (_jsxs("div", { className: "flex shrink-0 items-center justify-between gap-3 border-t border-border px-3 py-1 text-xs text-muted-foreground", children: [_jsx("span", { children: edges.next ? "More columns to explore" : "You’re at the last column" }), _jsxs("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Scroll table columns", children: [_jsx(Button, { variant: "ghost", size: "icon", className: "size-8 [@media(pointer:coarse)]:size-11", "aria-label": "Previous columns", disabled: !edges.previous, onClick: () => move(-1), children: _jsx(ChevronLeft, { className: "size-4 rtl:rotate-180" }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "size-8 [@media(pointer:coarse)]:size-11", "aria-label": "Next columns", disabled: !edges.next, onClick: () => move(1), children: _jsx(ChevronRight, { className: "size-4 rtl:rotate-180" }) })] })] }));
|
|
42
|
+
}
|
|
@@ -11,6 +11,7 @@ import { useCallback, useEffect, useRef, useState, } from "react";
|
|
|
11
11
|
import { DATA_TABLE_MIN_WIDTH } from "./facets.js";
|
|
12
12
|
import { useDataTableFrame } from "./data-table-frame.js";
|
|
13
13
|
import { usePinnedOffsets } from "./use-pinned-offsets.js";
|
|
14
|
+
import { DataTableOverflow } from "./data-table-overflow.js";
|
|
14
15
|
import { useElementWidth } from "./use-element-width.js";
|
|
15
16
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
16
17
|
// Native controls, the ARIA widgets that own the same keys (a slider or a
|
|
@@ -207,97 +208,97 @@ export function DataTable({ table, onRowClick, onRowOpen, activeRowId, onActiveR
|
|
|
207
208
|
return;
|
|
208
209
|
handler(row, event);
|
|
209
210
|
};
|
|
210
|
-
return (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return;
|
|
218
|
-
if (event.key === "ArrowDown")
|
|
219
|
-
moveActive(1);
|
|
220
|
-
else if (event.key === "ArrowUp")
|
|
221
|
-
moveActive(-1);
|
|
222
|
-
else if (event.key === "Home")
|
|
223
|
-
moveActive("first");
|
|
224
|
-
else if (event.key === "End")
|
|
225
|
-
moveActive("last");
|
|
226
|
-
else if (event.key === "Escape") {
|
|
227
|
-
// Only when there IS a row to let go of. One Escape, one
|
|
228
|
-
// effect: it clears the active row without also leaving
|
|
229
|
-
// focus mode — but with no active row it belongs to the
|
|
230
|
-
// frame, so it is left alone to bubble there.
|
|
231
|
-
if (!activeRowId)
|
|
211
|
+
return (_jsxs("div", { ref: rootRef, "data-scrolled-x": scrolledX ? "" : undefined, className: cn("overflow-hidden rounded-2xl border border-border/45 bg-card/60", filling && "flex min-h-0 flex-1 flex-col", className), children: [_jsx("div", { ref: scrollerRef, onScroll: (event) => {
|
|
212
|
+
const next = Math.abs(event.currentTarget.scrollLeft) > 0;
|
|
213
|
+
setScrolledX((current) => (current === next ? current : next));
|
|
214
|
+
}, "data-list-scroller": "", role: "region", "aria-label": `${ariaLabel ?? "Table"} scroll area`, tabIndex: 0, style: bounded ? { maxHeight } : undefined, className: cn(filling ? "min-h-0 flex-1 overflow-auto" : bounded ? "overflow-auto" : "overflow-x-auto"), children: _jsxs("table", { className: "w-full caption-bottom text-sm", "aria-label": ariaLabel, onKeyDown: onActiveRowChange
|
|
215
|
+
? (event) => {
|
|
216
|
+
// Let a control that owns these keys keep them.
|
|
217
|
+
if (event.target.closest(INTERACTIVE))
|
|
232
218
|
return;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
event.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (span === 0)
|
|
248
|
-
return null;
|
|
249
|
-
// `getSize()` sums EVERY descendant, including the ones the
|
|
250
|
-
// width gate just dropped, so a half-gated group would hold
|
|
251
|
-
// space for a column that is not there and push the table
|
|
252
|
-
// wider than its own columns.
|
|
253
|
-
const sized = header.subHeaders.length
|
|
254
|
-
? fittingLeafWidth(header)
|
|
255
|
-
: header.getSize();
|
|
256
|
-
return (_jsx("th", { colSpan: span, scope: "col",
|
|
257
|
-
// Every column in the run, not only the marked ones: the
|
|
258
|
-
// run can start with an unmarked leader (a selection
|
|
259
|
-
// checkbox), and an unmeasured leader leaves the column
|
|
260
|
-
// after it stacked at zero, overlapping it.
|
|
261
|
-
ref: pinnedIds.includes(header.column.id) ? measure(header.column.id) : undefined, ...pinnedProps(header.column.id, "header"), style: {
|
|
262
|
-
...(sized !== undefined && sized !== 150 ? { width: sized } : {}),
|
|
263
|
-
...pinnedStyle(header.column.id),
|
|
264
|
-
}, className: cn("h-10 px-3 text-left align-middle text-xs font-medium text-muted-foreground", meta?.align === "right" && "text-right", meta?.align === "center" && "text-center", meta?.headerClassName), children: header.isPlaceholder
|
|
265
|
-
? null
|
|
266
|
-
: flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
|
|
267
|
-
}), spacer ? _jsx("th", { "aria-hidden": "true", className: "w-full p-0" }) : null] }, headerGroup.id))) }), _jsx("tbody", { ref: bodyRef, children: rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: columnCount, className: "h-24 text-center text-sm text-muted-foreground", children: emptyState }) })) : (rows.map((row) => row.getIsGrouped() ? (_jsx("tr", { "data-group-row": "", className: "border-t border-border/45 bg-muted/35 text-sm font-medium", children: _jsx("td", { colSpan: columnCount, className: "p-0", children: _jsxs("div", { "data-group-content": "", style: width ? { width } : undefined, className: "flex items-center gap-2 px-3 py-1.5", children: [_jsx("button", { type: "button", onClick: row.getToggleExpandedHandler(), "aria-expanded": row.getIsExpanded(), "aria-label": row.getIsExpanded() ? "Collapse group" : "Expand group", className: "rounded-md p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground", children: row.getIsExpanded() ? (_jsx(ChevronDown, { className: "size-4" })) : (_jsx(ChevronRight, { className: "size-4" })) }), _jsx("div", { className: "min-w-0 flex-1", children: renderGroup ? renderGroup(row) : _jsx(DefaultGroupLabel, { row: row }) })] }) }) }, row.id)) : (_jsxs("tr", { "data-row-id": row.id, "data-state": row.getIsSelected() ? "selected" : undefined, "data-active": activeRowId != null && row.id === activeRowId ? "" : undefined, "aria-current": activeRowId != null && row.id === activeRowId ? "true" : undefined, "aria-selected": row.getIsSelected() || undefined, "data-clickable": onRowClick || onRowOpen ? "" : undefined, "data-focusable": onRowClick || onRowOpen || onActiveRowChange ? "" : undefined, tabIndex: onRowClick || onRowOpen || onActiveRowChange ? 0 : undefined,
|
|
268
|
-
// `detail` counts the clicks in the sequence: the second
|
|
269
|
-
// one belongs to the double-click that follows, and
|
|
270
|
-
// letting it through would toggle a selection off again
|
|
271
|
-
// right before the row opens.
|
|
272
|
-
onClick: (event) => {
|
|
273
|
-
if (event.detail > 1)
|
|
219
|
+
if (event.key === "ArrowDown")
|
|
220
|
+
moveActive(1);
|
|
221
|
+
else if (event.key === "ArrowUp")
|
|
222
|
+
moveActive(-1);
|
|
223
|
+
else if (event.key === "Home")
|
|
224
|
+
moveActive("first");
|
|
225
|
+
else if (event.key === "End")
|
|
226
|
+
moveActive("last");
|
|
227
|
+
else if (event.key === "Escape") {
|
|
228
|
+
// Only when there IS a row to let go of. One Escape, one
|
|
229
|
+
// effect: it clears the active row without also leaving
|
|
230
|
+
// focus mode — but with no active row it belongs to the
|
|
231
|
+
// frame, so it is left alone to bubble there.
|
|
232
|
+
if (!activeRowId)
|
|
274
233
|
return;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
234
|
+
onActiveRowChange(null);
|
|
235
|
+
event.stopPropagation();
|
|
236
|
+
event.nativeEvent.stopImmediatePropagation();
|
|
237
|
+
}
|
|
238
|
+
else
|
|
239
|
+
return;
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
}
|
|
242
|
+
: undefined, children: [_jsx("thead", { className: cn(sticky && "sticky top-0 z-10 bg-card"), children: table.getHeaderGroups().map((headerGroup) => (_jsxs("tr", { className: "border-b border-border/45", children: [headerGroup.headers.map((header) => {
|
|
243
|
+
const meta = header.column.columnDef.meta;
|
|
244
|
+
// A grouping header spans its leaves; when the width gate
|
|
245
|
+
// drops some of them it has to narrow, and when it drops all
|
|
246
|
+
// of them the group header goes too.
|
|
247
|
+
const span = fittingLeafCount(header);
|
|
248
|
+
if (span === 0)
|
|
249
|
+
return null;
|
|
250
|
+
// `getSize()` sums EVERY descendant, including the ones the
|
|
251
|
+
// width gate just dropped, so a half-gated group would hold
|
|
252
|
+
// space for a column that is not there and push the table
|
|
253
|
+
// wider than its own columns.
|
|
254
|
+
const sized = header.subHeaders.length
|
|
255
|
+
? fittingLeafWidth(header)
|
|
256
|
+
: header.getSize();
|
|
257
|
+
return (_jsx("th", { colSpan: span, scope: "col",
|
|
258
|
+
// Every column in the run, not only the marked ones: the
|
|
259
|
+
// run can start with an unmarked leader (a selection
|
|
260
|
+
// checkbox), and an unmeasured leader leaves the column
|
|
261
|
+
// after it stacked at zero, overlapping it.
|
|
262
|
+
ref: pinnedIds.includes(header.column.id) ? measure(header.column.id) : undefined, ...pinnedProps(header.column.id, "header"), style: {
|
|
263
|
+
...(sized !== undefined && sized !== 150 ? { width: sized } : {}),
|
|
264
|
+
...pinnedStyle(header.column.id),
|
|
265
|
+
}, className: cn("h-10 px-3 text-left align-middle text-xs font-medium text-muted-foreground", meta?.align === "right" && "text-right", meta?.align === "center" && "text-center", meta?.headerClassName), children: header.isPlaceholder
|
|
266
|
+
? null
|
|
267
|
+
: flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
|
|
268
|
+
}), spacer ? _jsx("th", { "aria-hidden": "true", className: "w-full p-0" }) : null] }, headerGroup.id))) }), _jsx("tbody", { ref: bodyRef, children: rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: columnCount, className: "h-24 text-center text-sm text-muted-foreground", children: emptyState }) })) : (rows.map((row) => row.getIsGrouped() ? (_jsx("tr", { "data-group-row": "", className: "border-t border-border/45 bg-muted/35 text-sm font-medium", children: _jsx("td", { colSpan: columnCount, className: "p-0", children: _jsxs("div", { "data-group-content": "", style: width ? { width } : undefined, className: "flex items-center gap-2 px-3 py-1.5", children: [_jsx("button", { type: "button", onClick: row.getToggleExpandedHandler(), "aria-expanded": row.getIsExpanded(), "aria-label": row.getIsExpanded() ? "Collapse group" : "Expand group", className: "rounded-md p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground", children: row.getIsExpanded() ? (_jsx(ChevronDown, { className: "size-4" })) : (_jsx(ChevronRight, { className: "size-4" })) }), _jsx("div", { className: "min-w-0 flex-1", children: renderGroup ? renderGroup(row) : _jsx(DefaultGroupLabel, { row: row }) })] }) }) }, row.id)) : (_jsxs("tr", { "data-row-id": row.id, "data-state": row.getIsSelected() ? "selected" : undefined, "data-active": activeRowId != null && row.id === activeRowId ? "" : undefined, "aria-current": activeRowId != null && row.id === activeRowId ? "true" : undefined, "aria-selected": row.getIsSelected() || undefined, "data-clickable": onRowClick || onRowOpen ? "" : undefined, "data-focusable": onRowClick || onRowOpen || onActiveRowChange ? "" : undefined, tabIndex: onRowClick || onRowOpen || onActiveRowChange ? 0 : undefined,
|
|
269
|
+
// `detail` counts the clicks in the sequence: the second
|
|
270
|
+
// one belongs to the double-click that follows, and
|
|
271
|
+
// letting it through would toggle a selection off again
|
|
272
|
+
// right before the row opens.
|
|
273
|
+
onClick: (event) => {
|
|
274
|
+
if (event.detail > 1)
|
|
285
275
|
return;
|
|
286
|
-
event.preventDefault();
|
|
287
276
|
activate(row, event, onRowClick);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
277
|
+
}, onDoubleClick: (event) => activate(row, event, onRowOpen), onKeyDown: (event) => {
|
|
278
|
+
if (event.key === "Enter") {
|
|
279
|
+
if (event.target.closest(INTERACTIVE))
|
|
280
|
+
return;
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
activate(row, event, onRowOpen ?? onRowClick);
|
|
283
|
+
}
|
|
284
|
+
else if (event.key === " ") {
|
|
285
|
+
if (event.target.closest(INTERACTIVE))
|
|
286
|
+
return;
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
activate(row, event, onRowClick);
|
|
289
|
+
}
|
|
290
|
+
}, className: cn("group/row border-t border-border/35 transition-colors", (onRowClick || onRowOpen) && "cursor-pointer hover:bg-muted/30",
|
|
291
|
+
// Anything focusable has to SHOW where the keyboard is —
|
|
292
|
+
// a list navigated with the arrows is focusable without
|
|
293
|
+
// being clickable, and silent focus leaves a keyboard
|
|
294
|
+
// user guessing which row the next key belongs to.
|
|
295
|
+
(onRowClick || onRowOpen || onActiveRowChange) &&
|
|
296
|
+
"focus-visible:bg-muted/30 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring", "data-[state=selected]:bg-primary/6 data-[active]:bg-primary/8", rowClassName?.(row)), children: [row.getVisibleCells().map((cell) => {
|
|
297
|
+
const meta = cell.column.columnDef.meta;
|
|
298
|
+
if (!fits(meta))
|
|
299
|
+
return null;
|
|
300
|
+
return (_jsx("td", { ...pinnedProps(cell.column.id, "cell"), style: pinnedStyle(cell.column.id), className: cn(cellPad, "align-middle", meta?.align === "right" && "text-right", meta?.align === "center" && "text-center", meta?.className), children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
|
|
301
|
+
}), spacer ? _jsx("td", { "aria-hidden": "true", className: "w-full p-0" }) : null] }, row.id)))) })] }) }), _jsx(DataTableOverflow, { scroller: scrollerRef })] }));
|
|
301
302
|
}
|
|
302
303
|
function DefaultGroupLabel({ row }) {
|
|
303
304
|
const grouping = row
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Resize a focus-view side pane without remounting its chat or source. */
|
|
2
|
+
export declare function FocusPaneResize({ label, side, width, onResize, breakpoint, }: {
|
|
3
|
+
breakpoint?: "md" | "xl";
|
|
4
|
+
label: string;
|
|
5
|
+
side: "left" | "right";
|
|
6
|
+
width?: number;
|
|
7
|
+
onResize: (width?: number) => void;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
/** Resize a focus-view side pane without remounting its chat or source. */
|
|
4
|
+
export function FocusPaneResize({ label, side, width, onResize, breakpoint = "md", }) {
|
|
5
|
+
const start = useRef(null);
|
|
6
|
+
const elementRef = useRef(null);
|
|
7
|
+
const [maximum, setMaximum] = useState(880);
|
|
8
|
+
const [renderedWidth, setRenderedWidth] = useState(240);
|
|
9
|
+
const getMaximum = (element) => {
|
|
10
|
+
const pane = element.parentElement;
|
|
11
|
+
const dialog = pane.closest('[role="dialog"]');
|
|
12
|
+
const other = [...(pane.parentElement?.querySelectorAll(":scope > aside") ?? [])]
|
|
13
|
+
.filter((n) => n !== pane && !["absolute", "fixed"].includes(getComputedStyle(n).position))
|
|
14
|
+
.reduce((sum, n) => sum + n.getBoundingClientRect().width, 0);
|
|
15
|
+
return Math.max(240, (dialog?.getBoundingClientRect().width ?? 1200) - other - 320);
|
|
16
|
+
};
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const element = elementRef.current;
|
|
19
|
+
if (!element)
|
|
20
|
+
return;
|
|
21
|
+
const update = () => {
|
|
22
|
+
const maximum = getMaximum(element);
|
|
23
|
+
setMaximum(maximum);
|
|
24
|
+
setRenderedWidth(Math.round(element.parentElement.getBoundingClientRect().width));
|
|
25
|
+
// Automatic percentage widths obey the same content reserve as a drag.
|
|
26
|
+
if (getComputedStyle(element).display !== "none" &&
|
|
27
|
+
element.parentElement.getBoundingClientRect().width > maximum)
|
|
28
|
+
onResize(maximum);
|
|
29
|
+
};
|
|
30
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
|
|
31
|
+
const dialog = element.closest('[role="dialog"]');
|
|
32
|
+
const layout = element.parentElement?.parentElement;
|
|
33
|
+
const observePanes = () => {
|
|
34
|
+
observer?.disconnect();
|
|
35
|
+
if (dialog) {
|
|
36
|
+
observer?.observe(dialog);
|
|
37
|
+
layout?.querySelectorAll(":scope > aside").forEach((pane) => observer?.observe(pane));
|
|
38
|
+
}
|
|
39
|
+
update();
|
|
40
|
+
};
|
|
41
|
+
observePanes();
|
|
42
|
+
const mutations = typeof MutationObserver === "undefined" ? null : new MutationObserver(observePanes);
|
|
43
|
+
if (layout)
|
|
44
|
+
mutations?.observe(layout, { childList: true });
|
|
45
|
+
return () => {
|
|
46
|
+
observer?.disconnect();
|
|
47
|
+
mutations?.disconnect();
|
|
48
|
+
};
|
|
49
|
+
}, [onResize]);
|
|
50
|
+
const adjust = (element, next) => {
|
|
51
|
+
const maximum = getMaximum(element);
|
|
52
|
+
setMaximum(maximum);
|
|
53
|
+
onResize(Math.round(Math.max(240, Math.min(maximum, next))));
|
|
54
|
+
};
|
|
55
|
+
return (_jsx("div", { ref: elementRef, role: "separator", "aria-orientation": "vertical", "aria-label": `Resize ${label}`, tabIndex: 0, "aria-valuemin": 240, "aria-valuemax": maximum, "aria-valuenow": width ?? renderedWidth, "aria-valuetext": width ? `${width} pixels` : `${renderedWidth} pixels (automatic)`, title: "Drag to resize. Double-click to reset.", className: `absolute inset-y-0 z-30 hidden w-2 cursor-col-resize touch-none hover:bg-border/60 focus-visible:bg-border focus-visible:outline-none ${breakpoint === "xl" ? "xl:block" : "md:block"} ${side === "right" ? "right-0" : "left-0"}`, onPointerDown: (event) => {
|
|
56
|
+
if (event.button !== 0)
|
|
57
|
+
return;
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
start.current = {
|
|
60
|
+
x: event.clientX,
|
|
61
|
+
width: event.currentTarget.parentElement.getBoundingClientRect().width,
|
|
62
|
+
original: width,
|
|
63
|
+
};
|
|
64
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
65
|
+
}, onPointerMove: (event) => {
|
|
66
|
+
if (start.current)
|
|
67
|
+
adjust(event.currentTarget, start.current.width + (event.clientX - start.current.x) * (side === "right" ? 1 : -1));
|
|
68
|
+
}, onPointerUp: () => {
|
|
69
|
+
start.current = null;
|
|
70
|
+
}, onPointerCancel: () => {
|
|
71
|
+
if (start.current)
|
|
72
|
+
onResize(start.current.original);
|
|
73
|
+
start.current = null;
|
|
74
|
+
}, onLostPointerCapture: () => {
|
|
75
|
+
start.current = null;
|
|
76
|
+
}, onDoubleClick: () => onResize(), onKeyDown: (event) => {
|
|
77
|
+
if (event.key === "Home") {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
onResize();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight")
|
|
83
|
+
return;
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
adjust(event.currentTarget, event.currentTarget.parentElement.getBoundingClientRect().width +
|
|
86
|
+
(event.key === "ArrowRight" ? 32 : -32) * (side === "right" ? 1 : -1));
|
|
87
|
+
} }));
|
|
88
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function FocusChatPane({ chatOpen, setChatOpen, chatWidth, setChatWidth, source, onClose, }: {
|
|
2
|
+
chatOpen: boolean;
|
|
3
|
+
setChatOpen: (v: boolean) => void;
|
|
4
|
+
chatWidth?: number;
|
|
5
|
+
setChatWidth: (v?: number) => void;
|
|
6
|
+
source: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, cn } from "@iloveagents/foundry-web-primitives";
|
|
3
|
+
import { X, PanelLeft, MessageSquare } from "lucide-react";
|
|
4
|
+
import { TooltipIconButton } from "./tooltip-icon-button.js";
|
|
5
|
+
import { FocusPaneResize } from "./data-table/focus-pane-resize.js";
|
|
6
|
+
import { ChatContent } from "./assistant-chat.js";
|
|
7
|
+
export function FocusChatPane({ chatOpen, setChatOpen, chatWidth, setChatWidth, source, onClose, }) {
|
|
8
|
+
return (_jsxs("aside", { "aria-label": "Focused content chat", style: { "--focus-chat-width": `${chatWidth ?? 384}px` }, className: cn("relative flex min-h-0 shrink-0 flex-col border-r border-border bg-background", chatOpen ? "w-full md:w-[var(--focus-chat-width)]" : "w-11", source && "hidden xl:flex"), children: [_jsx("div", { className: cn("flex h-12 shrink-0 items-center", chatOpen ? "gap-1 border-b border-border px-2" : "justify-center"), children: chatOpen ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "min-w-0 flex-1 px-2 text-sm font-semibold", children: "Chat" }), _jsx(TooltipIconButton, { tooltip: "Collapse chat", "aria-label": "Collapse chat", className: "size-8", onClick: () => setChatOpen(false), children: _jsx(PanelLeft, { size: 18 }) }), _jsx(Button, { variant: "ghost", size: "icon", title: "Close", "aria-label": "Close", className: "size-8 md:hidden", onClick: () => onClose(), children: _jsx(X, { size: 18 }) })] })) : (_jsx(TooltipIconButton, { tooltip: "Expand chat", "aria-label": "Expand chat", "aria-expanded": false, className: "size-8", onClick: () => setChatOpen(true), children: _jsx(MessageSquare, { size: 18 }) })) }), chatOpen && (_jsx(FocusPaneResize, { label: "chat", side: "right", width: chatWidth, onResize: setChatWidth })), chatOpen && (_jsx(ChatContent, { compactComposer: true, showPinButton: false, hideCurrentPageBadge: true, starterSuggestions: [] }))] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function FocusDialog({ focused, setFocused, host, attach, title, onReturnFocus, }: {
|
|
2
|
+
focused: boolean;
|
|
3
|
+
setFocused: (v: boolean) => void;
|
|
4
|
+
host: HTMLElement | null;
|
|
5
|
+
attach: (el: HTMLDivElement | null) => void;
|
|
6
|
+
title: string;
|
|
7
|
+
onReturnFocus: () => void;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useAuiState } from "@assistant-ui/react";
|
|
4
|
+
import { Button, cn } from "@iloveagents/foundry-web-primitives";
|
|
5
|
+
import { X } from "lucide-react";
|
|
6
|
+
import { DialogContent, DialogTitle } from "../ui/dialog.js";
|
|
7
|
+
import { useToolPanelStore } from "../lib/tool-panel-store.js";
|
|
8
|
+
import { FocusChatPane } from "./focus-chat-pane.js";
|
|
9
|
+
import { FocusSourcePane } from "./focus-source-pane.js";
|
|
10
|
+
export function FocusDialog({ focused, setFocused, host, attach, title, onReturnFocus, }) {
|
|
11
|
+
const [chatOpen, setChatOpen] = useState(false);
|
|
12
|
+
const [chatWidth, setChatWidth] = useState();
|
|
13
|
+
const [sourceWidth, setSourceWidth] = useState();
|
|
14
|
+
const [source, setSource] = useState(null);
|
|
15
|
+
const hasChat = useAuiState((state) => Boolean(state.optional.thread));
|
|
16
|
+
const dialogRef = useRef(null);
|
|
17
|
+
const sourceTrigger = useRef(null);
|
|
18
|
+
const ownedSource = useRef(undefined);
|
|
19
|
+
const previousPanel = useRef(null);
|
|
20
|
+
const releaseSource = () => {
|
|
21
|
+
if (previousPanel.current &&
|
|
22
|
+
useToolPanelStore.getState().isWindowHosted &&
|
|
23
|
+
useToolPanelStore.getState().isOpen &&
|
|
24
|
+
useToolPanelStore.getState().content?.sourceId === ownedSource.current)
|
|
25
|
+
useToolPanelStore.setState(previousPanel.current);
|
|
26
|
+
previousPanel.current = null;
|
|
27
|
+
ownedSource.current = undefined;
|
|
28
|
+
};
|
|
29
|
+
const closeSource = () => {
|
|
30
|
+
releaseSource();
|
|
31
|
+
setSource(null);
|
|
32
|
+
requestAnimationFrame(() => sourceTrigger.current?.focus());
|
|
33
|
+
};
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!focused) {
|
|
36
|
+
releaseSource();
|
|
37
|
+
setChatOpen(false);
|
|
38
|
+
setSource(null);
|
|
39
|
+
}
|
|
40
|
+
}, [focused]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!focused)
|
|
43
|
+
return;
|
|
44
|
+
const openSource = (event) => {
|
|
45
|
+
const request = event;
|
|
46
|
+
if (request.defaultPrevented || typeof request.detail?.content !== "string")
|
|
47
|
+
return;
|
|
48
|
+
const dialogs = Array.from(document.querySelectorAll('[role="dialog"]:not([aria-modal="false"])'));
|
|
49
|
+
if (host?.closest('[role="dialog"]') !== dialogs[dialogs.length - 1])
|
|
50
|
+
return;
|
|
51
|
+
request.preventDefault();
|
|
52
|
+
if (event.type === "preview:open-source") {
|
|
53
|
+
const { content, isOpen, isWindowHosted, hostedOwnerContent } = useToolPanelStore.getState();
|
|
54
|
+
previousPanel.current ?? (previousPanel.current = { content, isOpen, isWindowHosted, hostedOwnerContent });
|
|
55
|
+
ownedSource.current = request.detail.sourceId;
|
|
56
|
+
}
|
|
57
|
+
const active = document.activeElement;
|
|
58
|
+
if (active instanceof HTMLElement && !active.closest('[aria-label="Citation source"]'))
|
|
59
|
+
sourceTrigger.current = active;
|
|
60
|
+
setSource(request.detail);
|
|
61
|
+
};
|
|
62
|
+
const updateSource = (event) => {
|
|
63
|
+
const content = event.detail;
|
|
64
|
+
if (ownedSource.current && content.sourceId === ownedSource.current)
|
|
65
|
+
setSource(content);
|
|
66
|
+
};
|
|
67
|
+
const unsubscribe = useToolPanelStore.subscribe((state) => {
|
|
68
|
+
const dialogs = Array.from(document.querySelectorAll('[role="dialog"]:not([aria-modal="false"])'));
|
|
69
|
+
const isActiveHost = host?.closest('[role="dialog"]') === dialogs[dialogs.length - 1];
|
|
70
|
+
if (ownedSource.current &&
|
|
71
|
+
(!state.isOpen ||
|
|
72
|
+
!state.isWindowHosted ||
|
|
73
|
+
(isActiveHost && state.content?.sourceId !== ownedSource.current))) {
|
|
74
|
+
previousPanel.current = null;
|
|
75
|
+
ownedSource.current = undefined;
|
|
76
|
+
setSource(null);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
window.addEventListener("preview:update-source", updateSource);
|
|
80
|
+
window.addEventListener("focus:open-source", openSource);
|
|
81
|
+
window.addEventListener("preview:open-source", openSource, true);
|
|
82
|
+
return () => {
|
|
83
|
+
unsubscribe();
|
|
84
|
+
releaseSource();
|
|
85
|
+
window.removeEventListener("preview:update-source", updateSource);
|
|
86
|
+
window.removeEventListener("focus:open-source", openSource);
|
|
87
|
+
window.removeEventListener("preview:open-source", openSource, true);
|
|
88
|
+
};
|
|
89
|
+
}, [focused, host]);
|
|
90
|
+
return (_jsxs(DialogContent, { ref: dialogRef, tabIndex: -1, onOpenAutoFocus: (event) => {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
dialogRef.current?.focus();
|
|
93
|
+
}, showCloseButton: false, "aria-describedby": undefined, onEscapeKeyDown: (event) => {
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
if (source)
|
|
96
|
+
closeSource();
|
|
97
|
+
else
|
|
98
|
+
setFocused(false);
|
|
99
|
+
}, onCloseAutoFocus: (event) => {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
onReturnFocus();
|
|
102
|
+
}, className: "inset-0 left-0 top-0 flex h-dvh w-screen max-w-none translate-x-0 translate-y-0 flex-row gap-0 rounded-none border-0 bg-background p-0 sm:max-w-none", children: [hasChat && (_jsx(FocusChatPane, { chatOpen: chatOpen, setChatOpen: setChatOpen, chatWidth: chatWidth, setChatWidth: setChatWidth, source: !!source, onClose: () => setFocused(false) })), _jsxs("div", { className: cn("min-h-0 min-w-0 flex-1 flex-col", source ? "hidden xl:flex" : chatOpen ? "hidden md:flex" : "flex"), children: [_jsxs("div", { className: "flex h-12 shrink-0 items-center gap-3 border-b border-border px-4", children: [_jsx(DialogTitle, { className: "min-w-0 flex-1 truncate text-sm", children: title }), _jsx(Button, { variant: "ghost", size: "icon", title: "Close", "aria-label": "Close", className: "size-8", onClick: () => setFocused(false), children: _jsx(X, { size: 18 }) })] }), _jsx("div", { ref: focused ? attach : undefined, className: "flex min-h-0 min-w-0 flex-1 flex-col" })] }), source && (_jsx(FocusSourcePane, { source: source, sourceWidth: sourceWidth, setSourceWidth: setSourceWidth, closeSource: closeSource }))] }));
|
|
103
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ToolPanelContent } from "../lib/tool-panel-store.js";
|
|
2
|
+
export declare function FocusSourcePane({ source, sourceWidth, setSourceWidth, closeSource, }: {
|
|
3
|
+
source: ToolPanelContent;
|
|
4
|
+
sourceWidth?: number;
|
|
5
|
+
setSourceWidth: (v?: number) => void;
|
|
6
|
+
closeSource: () => void;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { Button } from "@iloveagents/foundry-web-primitives";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
import { FocusPaneResize } from "./data-table/focus-pane-resize.js";
|
|
6
|
+
import { PanelContentRenderer } from "./tool-panel.js";
|
|
7
|
+
export function FocusSourcePane({ source, sourceWidth, setSourceWidth, closeSource, }) {
|
|
8
|
+
const closeRef = useRef(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
closeRef.current?.focus();
|
|
11
|
+
}, []);
|
|
12
|
+
return (_jsxs("aside", { "aria-label": "Citation source", style: {
|
|
13
|
+
"--focus-source-width": sourceWidth ? `${sourceWidth}px` : "40%",
|
|
14
|
+
}, "data-focus-source-entity": source.entityId, "data-focus-source-title": source.title, className: "absolute inset-0 z-20 flex min-h-0 flex-col border-l border-border bg-background xl:relative xl:inset-auto xl:w-[var(--focus-source-width)] xl:min-w-60 xl:shrink-0", children: [_jsx(FocusPaneResize, { breakpoint: "xl", label: "source", side: "left", width: sourceWidth, onResize: setSourceWidth }), _jsxs("div", { className: "flex h-12 shrink-0 items-center gap-2 border-b border-border px-4", children: [_jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-medium", title: source.title, children: source.title }), _jsx(Button, { ref: closeRef, variant: "ghost", size: "icon", "aria-label": "Close citation", title: "Close citation", className: "size-8", onClick: closeSource, children: _jsx(X, { size: 18 }) })] }), _jsx("div", { className: "relative min-h-0 flex-1 overflow-auto", children: _jsx(PanelContentRenderer, { content: source }) })] }));
|
|
15
|
+
}
|
|
@@ -36,5 +36,5 @@ export function LoadingIndicator() {
|
|
|
36
36
|
// above it — measured, not guessed: the rows sit at x=469 and so does
|
|
37
37
|
// this. The old `h-10` did NOT belong: a fixed 40px block on top of the
|
|
38
38
|
// parts' own gap-4 left a visible hole under the last tool row.
|
|
39
|
-
_jsx("div", { className: "min-h-6 px-4", children: _jsxs("div", { className: "flex items-center gap-2 py-0.5 text-foreground leading-relaxed animate-in fade-in duration-300", children: [_jsx("span", { className: stalled ? "text-
|
|
39
|
+
_jsx("div", { role: "status", "aria-live": "polite", className: "min-h-6 px-4", children: _jsxs("div", { className: "flex items-center gap-2 py-0.5 text-foreground leading-relaxed animate-in fade-in duration-300", children: [_jsx("span", { className: stalled ? "text-xs text-warning" : "text-xs text-muted-foreground", children: label }), _jsx("span", { "aria-hidden": "true", className: `size-1.5 rounded-full ${stalled ? "bg-warning/70" : "bg-primary/60"} animate-[pulse_1.4s_ease-in-out_infinite]` })] }) }));
|
|
40
40
|
}
|