@opengeni/react 0.13.0 → 0.20.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 +129 -23
- package/dist/chunk-6BXBGJ3B.js +1956 -0
- package/dist/chunk-6BXBGJ3B.js.map +1 -0
- package/dist/chunk-6UT5OC3P.js +2133 -0
- package/dist/chunk-6UT5OC3P.js.map +1 -0
- package/dist/chunk-EBTT3MYN.js +92 -0
- package/dist/chunk-EBTT3MYN.js.map +1 -0
- package/dist/chunk-HHFA4AFX.js +425 -0
- package/dist/chunk-HHFA4AFX.js.map +1 -0
- package/dist/chunk-LAZ2A743.js +2393 -0
- package/dist/chunk-LAZ2A743.js.map +1 -0
- package/dist/chunk-MRSECXRP.js +563 -0
- package/dist/chunk-MRSECXRP.js.map +1 -0
- package/dist/chunk-TK7G6XLT.js +18 -0
- package/dist/chunk-TK7G6XLT.js.map +1 -0
- package/dist/composer-DvUaa5ki.d.ts +585 -0
- package/dist/composer.d.ts +6 -0
- package/dist/composer.js +56 -0
- package/dist/composer.js.map +1 -0
- package/dist/index.d.ts +642 -1080
- package/dist/index.js +7672 -8005
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +412 -3
- package/dist/machines.js +25 -1
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
- package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
- package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
- package/dist/session-Gd4iVPYw.d.ts +425 -0
- package/dist/session-context-DIFFlXKc.d.ts +52 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/use-composer-BCfm4mzX.d.ts +112 -0
- package/package.json +23 -4
- package/src/approvals.ts +5 -3
- package/src/client.ts +50 -1
- package/src/components/approval-surface.tsx +198 -0
- package/src/components/chat-composer.tsx +104 -751
- package/src/components/code-editor.tsx +104 -26
- package/src/components/command-palette.tsx +8 -2
- package/src/components/composer-transcription-control.tsx +211 -0
- package/src/components/composer.tsx +1536 -0
- package/src/components/desktop-viewer.tsx +3 -3
- package/src/components/enrollment-consent.tsx +2 -2
- package/src/components/file-browser.tsx +367 -29
- package/src/components/human-input-form.tsx +417 -0
- package/src/components/machine-card.tsx +82 -16
- package/src/components/machine-health-pill.tsx +68 -0
- package/src/components/machine-metrics.tsx +10 -24
- package/src/components/machines/health.ts +146 -0
- package/src/components/machines/machine-detail.tsx +220 -0
- package/src/components/machines/metric-history-chart.tsx +298 -0
- package/src/components/machines/metric-sparkline.tsx +76 -0
- package/src/components/machines/series.ts +113 -0
- package/src/components/machines-dashboard.tsx +16 -4
- package/src/components/message-timeline.tsx +15 -7
- package/src/components/model-picker.tsx +12 -3
- package/src/components/pierre-diff.tsx +32 -6
- package/src/components/queue-surface-implementation.tsx +1041 -0
- package/src/components/queue-surface-state.tsx +94 -0
- package/src/components/queue-surface.tsx +60 -0
- package/src/components/sandbox-files.tsx +230 -21
- package/src/components/sandbox-terminal.tsx +8 -2
- package/src/components/sandbox-workspace.tsx +489 -135
- package/src/components/session-status.tsx +0 -6
- package/src/components/workbench-changes.tsx +145 -50
- package/src/components/workspace-dock.tsx +329 -68
- package/src/composer.ts +60 -0
- package/src/hooks/internal.ts +115 -34
- package/src/hooks/use-composer.ts +635 -74
- package/src/hooks/use-human-input.ts +131 -0
- package/src/hooks/use-machine-chip.ts +2 -2
- package/src/hooks/use-machines.ts +27 -11
- package/src/hooks/use-sandbox-files.ts +310 -62
- package/src/hooks/use-sandbox-git.ts +154 -40
- package/src/hooks/use-sandbox-terminal.ts +28 -6
- package/src/hooks/use-session-capabilities.ts +38 -9
- package/src/hooks/use-session-control.ts +43 -15
- package/src/hooks/use-session-events.ts +523 -48
- package/src/hooks/use-session-lineage.ts +15 -6
- package/src/hooks/use-session.ts +10 -2
- package/src/hooks/use-slash-commands.ts +38 -38
- package/src/hooks/use-transcription.ts +757 -0
- package/src/hooks/use-turn-queue.ts +273 -77
- package/src/hooks/use-windowed-sections.ts +2 -2
- package/src/hooks/use-workspace-capture.ts +146 -40
- package/src/hooks/use-workspace-edit.ts +50 -14
- package/src/hooks/use-workspace-sessions.ts +3 -0
- package/src/human-input.ts +72 -0
- package/src/index.ts +88 -12
- package/src/lib/noto-sans-arabic-loader.ts +21 -0
- package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
- package/src/lib/use-portal-token-style.ts +51 -0
- package/src/lib/use-unicode-fonts.ts +57 -0
- package/src/machines.ts +16 -0
- package/src/provider.tsx +163 -41
- package/src/session-context.ts +135 -0
- package/src/session.ts +79 -0
- package/src/timeline/parsers.ts +78 -6
- package/src/timeline/projection.ts +36 -6
- package/src/timeline/tool-renderers.tsx +41 -0
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +16 -8
- package/styles/index.css +78 -0
- package/styles/index.d.ts +2 -0
- package/styles/tokens.css +11 -6
- package/styles/tokens.d.ts +2 -0
- package/dist/chunk-NFYVQWIB.js +0 -1377
- package/dist/chunk-NFYVQWIB.js.map +0 -1
- package/dist/machines-CnlMb7E-.d.ts +0 -329
|
@@ -15,7 +15,14 @@ import {
|
|
|
15
15
|
PanelRightCloseIcon,
|
|
16
16
|
XIcon,
|
|
17
17
|
} from "lucide-react";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
Group,
|
|
20
|
+
Panel,
|
|
21
|
+
Separator,
|
|
22
|
+
useDefaultLayout,
|
|
23
|
+
usePanelRef,
|
|
24
|
+
type LayoutStorage,
|
|
25
|
+
} from "react-resizable-panels";
|
|
19
26
|
import { cn } from "../lib/cn";
|
|
20
27
|
|
|
21
28
|
export type WorkspaceTab = {
|
|
@@ -53,6 +60,24 @@ export type WorkspaceDockProps = {
|
|
|
53
60
|
className?: string | undefined;
|
|
54
61
|
};
|
|
55
62
|
|
|
63
|
+
const SERVER_LAYOUT_STORAGE: LayoutStorage = {
|
|
64
|
+
getItem: () => null,
|
|
65
|
+
setItem: () => {},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** `useDefaultLayout` defaults to the ambient `localStorage` identifier when
|
|
69
|
+
* storage is undefined, which throws during SSR. Access can also throw in a
|
|
70
|
+
* browser that blocks storage (sandboxed frames/private policy), so both cases
|
|
71
|
+
* use a stable, non-persisting implementation instead. */
|
|
72
|
+
function getLayoutStorage(): LayoutStorage {
|
|
73
|
+
if (typeof window === "undefined") return SERVER_LAYOUT_STORAGE;
|
|
74
|
+
try {
|
|
75
|
+
return window.localStorage;
|
|
76
|
+
} catch {
|
|
77
|
+
return SERVER_LAYOUT_STORAGE;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
56
81
|
/**
|
|
57
82
|
* The resizable / collapsible / maximizable right-hand Workspace dock. Replaces
|
|
58
83
|
* a fixed grid column: drag the separator to set width, collapse to a thin rail
|
|
@@ -113,6 +138,10 @@ export function WorkspaceDock({
|
|
|
113
138
|
className,
|
|
114
139
|
}: WorkspaceDockProps) {
|
|
115
140
|
const narrow = useIsNarrow(DOCK_OVERLAY_BREAKPOINT);
|
|
141
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
142
|
+
const reopenRef = useRef<HTMLButtonElement | null>(null);
|
|
143
|
+
const returnFocusRef = useRef<HTMLElement | null>(null);
|
|
144
|
+
const initialNarrowFocusRef = useRef(false);
|
|
116
145
|
const dockPanelRef = usePanelRef();
|
|
117
146
|
const [internalCollapsed, setInternalCollapsed] = useState(false);
|
|
118
147
|
const [maximized, setMaximized] = useState(false);
|
|
@@ -124,13 +153,14 @@ export function WorkspaceDock({
|
|
|
124
153
|
// bug). Standalone (uncontrolled) usage keeps the built-in collapse button
|
|
125
154
|
// and the thin re-open rail as its only affordances.
|
|
126
155
|
const hostControlled = collapsedProp !== undefined;
|
|
156
|
+
const previousCollapsedRef = useRef(collapsed);
|
|
127
157
|
|
|
128
158
|
// Persisted layout (width split) keyed by autoSaveId.
|
|
129
159
|
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
|
130
160
|
panelIds: ["primary", "dock"],
|
|
131
|
-
storage:
|
|
161
|
+
storage: getLayoutStorage(),
|
|
132
162
|
id: autoSaveId,
|
|
133
|
-
}
|
|
163
|
+
});
|
|
134
164
|
|
|
135
165
|
const requestedTab = activeTab ?? internalTab;
|
|
136
166
|
const tabIds = tabs.map((tab) => tab.id);
|
|
@@ -164,6 +194,61 @@ export function WorkspaceDock({
|
|
|
164
194
|
}
|
|
165
195
|
}, [collapsedProp, dockPanelRef]);
|
|
166
196
|
|
|
197
|
+
// Treat the narrow overlay like a real modal: remember the external opener,
|
|
198
|
+
// move focus into the selected tab, and restore it on close. The standalone
|
|
199
|
+
// desktop dock instead focuses its newly-visible reopen rail after collapse.
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
const previous = previousCollapsedRef.current;
|
|
202
|
+
previousCollapsedRef.current = collapsed;
|
|
203
|
+
if (previous === collapsed) return;
|
|
204
|
+
|
|
205
|
+
if (!collapsed) {
|
|
206
|
+
const active = document.activeElement;
|
|
207
|
+
const workspaceSurface = rootRef.current?.querySelector<HTMLElement>(
|
|
208
|
+
narrow ? '[role="dialog"]' : "[data-workspace-surface]",
|
|
209
|
+
);
|
|
210
|
+
if (active instanceof HTMLElement && !workspaceSurface?.contains(active)) {
|
|
211
|
+
returnFocusRef.current = active;
|
|
212
|
+
}
|
|
213
|
+
if (narrow) {
|
|
214
|
+
const frame = requestAnimationFrame(() => {
|
|
215
|
+
rootRef.current
|
|
216
|
+
?.querySelector<HTMLElement>('[role="dialog"] [role="tab"][aria-selected="true"]')
|
|
217
|
+
?.focus();
|
|
218
|
+
});
|
|
219
|
+
return () => cancelAnimationFrame(frame);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const frame = requestAnimationFrame(() => {
|
|
225
|
+
if (!hostControlled) {
|
|
226
|
+
reopenRef.current?.focus();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const target = returnFocusRef.current;
|
|
230
|
+
if (target?.isConnected) target.focus();
|
|
231
|
+
});
|
|
232
|
+
return () => cancelAnimationFrame(frame);
|
|
233
|
+
}, [collapsed, hostControlled, narrow]);
|
|
234
|
+
|
|
235
|
+
// On the first narrow render there is no collapsed-state transition to drive
|
|
236
|
+
// focus into the modal: the overlay can already be open at mount. Place focus
|
|
237
|
+
// on its selected tab once, just as we do when an existing overlay is opened.
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (!narrow || collapsed || initialNarrowFocusRef.current) return;
|
|
240
|
+
initialNarrowFocusRef.current = true;
|
|
241
|
+
const active = document.activeElement;
|
|
242
|
+
const workspaceSurface = rootRef.current?.querySelector<HTMLElement>('[role="dialog"]');
|
|
243
|
+
if (active instanceof HTMLElement && !workspaceSurface?.contains(active)) {
|
|
244
|
+
returnFocusRef.current = active;
|
|
245
|
+
}
|
|
246
|
+
const frame = requestAnimationFrame(() => {
|
|
247
|
+
workspaceSurface?.querySelector<HTMLElement>('[role="tab"][aria-selected="true"]')?.focus();
|
|
248
|
+
});
|
|
249
|
+
return () => cancelAnimationFrame(frame);
|
|
250
|
+
}, [collapsed, narrow]);
|
|
251
|
+
|
|
167
252
|
// Keep the active tab valid if the available tabs change.
|
|
168
253
|
useEffect(() => {
|
|
169
254
|
if (firstTabId && !requestedTabIsValid) {
|
|
@@ -182,14 +267,55 @@ export function WorkspaceDock({
|
|
|
182
267
|
setCollapsed(false);
|
|
183
268
|
}, [dockPanelRef, setCollapsed]);
|
|
184
269
|
|
|
185
|
-
// Esc restores from maximize (desktop) and closes the mobile overlay.
|
|
270
|
+
// Esc restores from maximize (desktop) and closes the mobile overlay. Tab is
|
|
271
|
+
// fenced inside either full-screen surface: both visually cover the host, so
|
|
272
|
+
// keyboard focus must never move into invisible content behind them.
|
|
186
273
|
useEffect(() => {
|
|
187
274
|
const overlayOpen = maximized || (narrow && !collapsed);
|
|
188
275
|
if (!overlayOpen) return;
|
|
189
276
|
const onKey = (event: KeyboardEvent) => {
|
|
190
|
-
if (event.key
|
|
191
|
-
|
|
192
|
-
|
|
277
|
+
if (event.key === "Escape") {
|
|
278
|
+
if (maximized) setMaximized(false);
|
|
279
|
+
else collapse();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (event.key !== "Tab") return;
|
|
283
|
+
const surface = rootRef.current?.querySelector<HTMLElement>(
|
|
284
|
+
narrow ? '[role="dialog"]' : "[data-workspace-surface]",
|
|
285
|
+
);
|
|
286
|
+
if (!surface) return;
|
|
287
|
+
const focusable = Array.from(surface.querySelectorAll<HTMLElement>("*")).filter(
|
|
288
|
+
(element) =>
|
|
289
|
+
element.tabIndex >= 0 &&
|
|
290
|
+
!element.hasAttribute("disabled") &&
|
|
291
|
+
element.getClientRects().length > 0 &&
|
|
292
|
+
getComputedStyle(element).visibility !== "hidden",
|
|
293
|
+
);
|
|
294
|
+
const first = focusable[0];
|
|
295
|
+
const last = focusable.at(-1);
|
|
296
|
+
const active = document.activeElement;
|
|
297
|
+
if (!first || !last) {
|
|
298
|
+
event.preventDefault();
|
|
299
|
+
surface.focus();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!surface.contains(active)) {
|
|
303
|
+
event.preventDefault();
|
|
304
|
+
(event.shiftKey ? last : first).focus();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if ((event.shiftKey && active === first) || (!event.shiftKey && active === last)) {
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
(event.shiftKey ? last : first).focus();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Focus can emerge from a shadow-DOM editor/highlighter host at a point
|
|
313
|
+
// the light-DOM order cannot predict. Repair any escape after the browser
|
|
314
|
+
// performs its default Tab move.
|
|
315
|
+
requestAnimationFrame(() => {
|
|
316
|
+
const next = document.activeElement;
|
|
317
|
+
if (!surface.contains(next)) (event.shiftKey ? last : first).focus();
|
|
318
|
+
});
|
|
193
319
|
};
|
|
194
320
|
window.addEventListener("keydown", onKey);
|
|
195
321
|
return () => window.removeEventListener("keydown", onKey);
|
|
@@ -200,33 +326,53 @@ export function WorkspaceDock({
|
|
|
200
326
|
// driven by the same `collapsed` contract (collapsed → hidden).
|
|
201
327
|
if (narrow) {
|
|
202
328
|
return (
|
|
203
|
-
<div className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
204
|
-
<div
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
329
|
+
<div ref={rootRef} className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
330
|
+
<div
|
|
331
|
+
data-workspace-primary
|
|
332
|
+
aria-hidden={!collapsed ? true : undefined}
|
|
333
|
+
className="min-h-0 min-w-0 flex-1"
|
|
334
|
+
inert={!collapsed ? true : undefined}
|
|
335
|
+
>
|
|
336
|
+
{primary}
|
|
337
|
+
</div>
|
|
338
|
+
<div
|
|
339
|
+
data-workspace-surface
|
|
340
|
+
role="dialog"
|
|
341
|
+
aria-modal="true"
|
|
342
|
+
aria-label="Workspace"
|
|
343
|
+
className="fixed inset-0 z-40 flex flex-col bg-og-bg"
|
|
344
|
+
hidden={collapsed}
|
|
345
|
+
style={{
|
|
346
|
+
paddingTop: "env(safe-area-inset-top)",
|
|
347
|
+
paddingBottom: "env(safe-area-inset-bottom)",
|
|
348
|
+
}}
|
|
349
|
+
>
|
|
350
|
+
<DockChrome
|
|
351
|
+
tabs={tabs}
|
|
352
|
+
current={current}
|
|
353
|
+
onTab={setTab}
|
|
354
|
+
leading={mobileLeadingControl}
|
|
355
|
+
accessory={headerAccessory}
|
|
356
|
+
controls={
|
|
357
|
+
<ChromeButton onClick={collapse} title="Close workspace" label="Close workspace">
|
|
358
|
+
<XIcon className="size-4" />
|
|
359
|
+
</ChromeButton>
|
|
360
|
+
}
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
363
|
+
{collapsed && !hostControlled ? (
|
|
364
|
+
<button
|
|
365
|
+
ref={reopenRef}
|
|
366
|
+
type="button"
|
|
367
|
+
onClick={expand}
|
|
368
|
+
title="Open workspace"
|
|
369
|
+
aria-label="Open workspace"
|
|
370
|
+
className="absolute right-3 top-3 z-30 inline-flex size-11 items-center justify-center rounded-og-md border border-og-border bg-og-surface-1 text-og-fg-muted shadow-lg transition-colors hover:border-og-border-strong hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent"
|
|
371
|
+
style={{ marginTop: "env(safe-area-inset-top)" }}
|
|
215
372
|
>
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
onTab={setTab}
|
|
220
|
-
leading={mobileLeadingControl}
|
|
221
|
-
accessory={headerAccessory}
|
|
222
|
-
controls={
|
|
223
|
-
<ChromeButton onClick={collapse} title="Close workspace" label="Close workspace">
|
|
224
|
-
<XIcon className="size-4" />
|
|
225
|
-
</ChromeButton>
|
|
226
|
-
}
|
|
227
|
-
/>
|
|
228
|
-
</div>
|
|
229
|
-
)}
|
|
373
|
+
<ChevronsLeftRightIcon className="size-4" aria-hidden />
|
|
374
|
+
</button>
|
|
375
|
+
) : null}
|
|
230
376
|
</div>
|
|
231
377
|
);
|
|
232
378
|
}
|
|
@@ -261,7 +407,7 @@ export function WorkspaceDock({
|
|
|
261
407
|
);
|
|
262
408
|
|
|
263
409
|
return (
|
|
264
|
-
<div className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
410
|
+
<div ref={rootRef} className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
265
411
|
<Group
|
|
266
412
|
orientation="horizontal"
|
|
267
413
|
className="min-h-0 flex-1"
|
|
@@ -269,12 +415,19 @@ export function WorkspaceDock({
|
|
|
269
415
|
onLayoutChanged={onLayoutChanged}
|
|
270
416
|
>
|
|
271
417
|
<Panel id="primary" minSize="30%" className="min-h-0 min-w-0">
|
|
272
|
-
|
|
418
|
+
<div
|
|
419
|
+
data-workspace-primary
|
|
420
|
+
aria-hidden={maximized ? true : undefined}
|
|
421
|
+
className="h-full min-h-0 min-w-0"
|
|
422
|
+
inert={maximized ? true : undefined}
|
|
423
|
+
>
|
|
424
|
+
{primary}
|
|
425
|
+
</div>
|
|
273
426
|
</Panel>
|
|
274
427
|
|
|
275
428
|
{!collapsed && (
|
|
276
|
-
<Separator className="group relative w-1.5 shrink-0 outline-none">
|
|
277
|
-
<span className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-og-border transition-colors group-hover:bg-og-accent group-data-[separator-state=dragging]:bg-og-accent" />
|
|
429
|
+
<Separator className="group relative z-10 w-1.5 shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent max-[1023px]:-mx-[19px] max-[1023px]:w-11 pointer-coarse:-mx-[19px] pointer-coarse:w-11">
|
|
430
|
+
<span className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-og-border transition-colors group-hover:bg-og-accent group-focus-visible:bg-og-accent group-data-[separator-state=dragging]:bg-og-accent" />
|
|
278
431
|
</Separator>
|
|
279
432
|
)}
|
|
280
433
|
|
|
@@ -296,13 +449,22 @@ export function WorkspaceDock({
|
|
|
296
449
|
}}
|
|
297
450
|
className="min-h-0 min-w-0"
|
|
298
451
|
>
|
|
299
|
-
{/*
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
452
|
+
{/* One persistent mount across normal, collapsed, and maximized modes:
|
|
453
|
+
layout changes must never destroy an editor buffer or terminal view. */}
|
|
454
|
+
<div
|
|
455
|
+
data-workspace-surface
|
|
456
|
+
role={maximized ? "dialog" : undefined}
|
|
457
|
+
aria-modal={maximized ? true : undefined}
|
|
458
|
+
aria-label={maximized ? "Workspace" : undefined}
|
|
459
|
+
aria-hidden={collapsed ? true : undefined}
|
|
460
|
+
className={cn(
|
|
461
|
+
"flex h-full min-h-0 min-w-0 flex-col bg-og-bg",
|
|
462
|
+
maximized ? "fixed inset-0 z-40" : "border-l border-og-border",
|
|
463
|
+
collapsed && "invisible pointer-events-none",
|
|
464
|
+
)}
|
|
465
|
+
>
|
|
466
|
+
{dockChrome}
|
|
467
|
+
</div>
|
|
306
468
|
</Panel>
|
|
307
469
|
</Group>
|
|
308
470
|
|
|
@@ -310,6 +472,7 @@ export function WorkspaceDock({
|
|
|
310
472
|
when the host controls collapse — its own toggle is the one way in. */}
|
|
311
473
|
{collapsed && !maximized && !hostControlled && (
|
|
312
474
|
<button
|
|
475
|
+
ref={reopenRef}
|
|
313
476
|
type="button"
|
|
314
477
|
onClick={expand}
|
|
315
478
|
title="Open workspace"
|
|
@@ -318,9 +481,6 @@ export function WorkspaceDock({
|
|
|
318
481
|
<ChevronsLeftRightIcon className="size-3.5" />
|
|
319
482
|
</button>
|
|
320
483
|
)}
|
|
321
|
-
|
|
322
|
-
{/* Maximize overlay: full-workspace surface above everything. */}
|
|
323
|
-
{maximized && <div className="fixed inset-0 z-40 flex flex-col bg-og-bg">{dockChrome}</div>}
|
|
324
484
|
</div>
|
|
325
485
|
);
|
|
326
486
|
}
|
|
@@ -343,7 +503,7 @@ function ChromeButton({
|
|
|
343
503
|
onClick={onClick}
|
|
344
504
|
title={title}
|
|
345
505
|
aria-label={label}
|
|
346
|
-
className="inline-flex size-7 items-center justify-center rounded-og-sm p-1 transition-colors hover:bg-og-surface-2 hover:text-og-fg max-[1023px]:size-11 pointer-coarse:size-11"
|
|
506
|
+
className="inline-flex size-7 items-center justify-center rounded-og-sm p-1 transition-colors hover:bg-og-surface-2 hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:size-11 pointer-coarse:size-11"
|
|
347
507
|
>
|
|
348
508
|
{children}
|
|
349
509
|
</button>
|
|
@@ -369,14 +529,82 @@ function DockChrome({
|
|
|
369
529
|
controls: ReactNode;
|
|
370
530
|
}) {
|
|
371
531
|
const active = tabs.find((t) => t.id === current) ?? tabs[0];
|
|
372
|
-
const activeIndex = tabs.findIndex((tab) => tab.id === active?.id);
|
|
373
532
|
const tabsetId = useId();
|
|
374
533
|
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
|
375
|
-
const
|
|
534
|
+
const tabListRef = useRef<HTMLDivElement | null>(null);
|
|
535
|
+
const [visitedTabs, setVisitedTabs] = useState<Set<string>>(() => new Set());
|
|
536
|
+
const [tabOverflow, setTabOverflow] = useState({ start: false, end: false });
|
|
537
|
+
const activeId = active?.id ?? "";
|
|
538
|
+
const activeTabIndex = tabs.findIndex((tab) => tab.id === activeId);
|
|
539
|
+
const keepActiveTabVisible = useCallback(() => {
|
|
540
|
+
const selected = activeTabIndex >= 0 ? tabRefs.current[activeTabIndex] : null;
|
|
541
|
+
selected?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
542
|
+
}, [activeTabIndex]);
|
|
543
|
+
|
|
544
|
+
useEffect(() => {
|
|
545
|
+
if (!activeId) return;
|
|
546
|
+
setVisitedTabs((previous) => {
|
|
547
|
+
if (previous.has(activeId)) return previous;
|
|
548
|
+
const next = new Set(previous);
|
|
549
|
+
next.add(activeId);
|
|
550
|
+
return next;
|
|
551
|
+
});
|
|
552
|
+
}, [activeId]);
|
|
553
|
+
|
|
554
|
+
// A controlled/initial tab can become active without receiving focus (for
|
|
555
|
+
// example, a host routing a guarded diff into Files). Native scrolling only
|
|
556
|
+
// follows focus, so keep the selected tab inside the horizontally scrollable
|
|
557
|
+
// strip even when state — rather than a click or arrow key — selected it.
|
|
558
|
+
useDockLayoutEffect(() => {
|
|
559
|
+
keepActiveTabVisible();
|
|
560
|
+
}, [activeId, keepActiveTabVisible]);
|
|
561
|
+
|
|
562
|
+
// Hidden scrollbars keep the chrome quiet, but without an edge cue a clipped
|
|
563
|
+
// host label reads like corrupted copy. Fade only the edge(s) with more tabs,
|
|
564
|
+
// and recompute after scrolling, resizing, or late font metrics.
|
|
565
|
+
useDockLayoutEffect(() => {
|
|
566
|
+
const list = tabListRef.current;
|
|
567
|
+
if (!list) return;
|
|
568
|
+
const update = () => {
|
|
569
|
+
const max = Math.max(0, list.scrollWidth - list.clientWidth);
|
|
570
|
+
const next = { start: list.scrollLeft > 1, end: list.scrollLeft < max - 1 };
|
|
571
|
+
setTabOverflow((previous) =>
|
|
572
|
+
previous.start === next.start && previous.end === next.end ? previous : next,
|
|
573
|
+
);
|
|
574
|
+
};
|
|
575
|
+
update();
|
|
576
|
+
list.addEventListener("scroll", update, { passive: true });
|
|
577
|
+
const observer =
|
|
578
|
+
typeof ResizeObserver === "undefined"
|
|
579
|
+
? null
|
|
580
|
+
: new ResizeObserver(() => {
|
|
581
|
+
keepActiveTabVisible();
|
|
582
|
+
update();
|
|
583
|
+
});
|
|
584
|
+
observer?.observe(list);
|
|
585
|
+
for (const tab of tabRefs.current) {
|
|
586
|
+
if (tab) observer?.observe(tab);
|
|
587
|
+
}
|
|
588
|
+
return () => {
|
|
589
|
+
list.removeEventListener("scroll", update);
|
|
590
|
+
observer?.disconnect();
|
|
591
|
+
};
|
|
592
|
+
}, [activeId, keepActiveTabVisible, tabs.length]);
|
|
593
|
+
|
|
594
|
+
const tabMask =
|
|
595
|
+
tabOverflow.start && tabOverflow.end
|
|
596
|
+
? "linear-gradient(to right, transparent 0, black 14px, black calc(100% - 14px), transparent 100%)"
|
|
597
|
+
: tabOverflow.start
|
|
598
|
+
? "linear-gradient(to right, transparent 0, black 14px, black 100%)"
|
|
599
|
+
: tabOverflow.end
|
|
600
|
+
? "linear-gradient(to right, black 0, black calc(100% - 14px), transparent 100%)"
|
|
601
|
+
: undefined;
|
|
376
602
|
|
|
377
603
|
const activateTab = (index: number) => {
|
|
378
604
|
const tab = tabs[index];
|
|
379
|
-
if (!tab)
|
|
605
|
+
if (!tab) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
380
608
|
onTab(tab.id);
|
|
381
609
|
tabRefs.current[index]?.focus();
|
|
382
610
|
};
|
|
@@ -395,15 +623,31 @@ function DockChrome({
|
|
|
395
623
|
|
|
396
624
|
return (
|
|
397
625
|
<>
|
|
398
|
-
<div className="
|
|
399
|
-
|
|
626
|
+
<div className="grid shrink-0 grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center gap-x-2 border-b border-og-border px-1.5 py-1 max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto] max-[1023px]:gap-y-0 max-[1023px]:px-2 max-[1023px]:pb-1 max-[1023px]:pt-0 min-[640px]:max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto_auto]">
|
|
627
|
+
<div className="flex min-w-0 shrink-0 items-center max-[1023px]:min-h-11 min-[640px]:max-[1023px]:col-start-1 min-[640px]:max-[1023px]:row-start-1">
|
|
628
|
+
{leading ?? (
|
|
629
|
+
<span className="hidden truncate px-1 text-og-sm font-semibold text-og-fg max-[1023px]:inline">
|
|
630
|
+
Workspace
|
|
631
|
+
</span>
|
|
632
|
+
)}
|
|
633
|
+
</div>
|
|
400
634
|
{/* The tab list scrolls horizontally when it can't fit — it must never
|
|
401
635
|
grow into or overlap the chrome controls (they stay shrink-0). The
|
|
402
|
-
scrollbar is hidden to keep the strip calm.
|
|
636
|
+
scrollbar is hidden to keep the strip calm. On the narrow overlay it
|
|
637
|
+
owns a full second row, so status/close chrome can never squeeze the
|
|
638
|
+
canonical workspace navigation out of view. Tablets have enough
|
|
639
|
+
width to retain the denser single-row chrome, with this same strip
|
|
640
|
+
absorbing any unusually long host-injected labels. */}
|
|
403
641
|
<div
|
|
404
|
-
|
|
642
|
+
ref={tabListRef}
|
|
643
|
+
className="flex min-w-0 items-center gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden max-[1023px]:col-span-3 max-[1023px]:row-start-2 max-[1023px]:w-full min-[640px]:max-[1023px]:col-span-1 min-[640px]:max-[1023px]:col-start-2 min-[640px]:max-[1023px]:row-start-1"
|
|
405
644
|
role="tablist"
|
|
406
645
|
aria-orientation="horizontal"
|
|
646
|
+
style={{
|
|
647
|
+
maskImage: tabMask,
|
|
648
|
+
WebkitMaskImage: tabMask,
|
|
649
|
+
scrollPaddingInline: "16px",
|
|
650
|
+
}}
|
|
407
651
|
>
|
|
408
652
|
{tabs.map((tab, index) => (
|
|
409
653
|
<button
|
|
@@ -415,33 +659,50 @@ function DockChrome({
|
|
|
415
659
|
type="button"
|
|
416
660
|
role="tab"
|
|
417
661
|
aria-selected={tab.id === current}
|
|
418
|
-
aria-controls={
|
|
662
|
+
aria-controls={`${tabsetId}-panel-${index}`}
|
|
419
663
|
tabIndex={tab.id === current ? 0 : -1}
|
|
420
664
|
onClick={() => onTab(tab.id)}
|
|
421
665
|
onKeyDown={(event) => onTabKeyDown(event, index)}
|
|
422
666
|
className={cn(
|
|
423
|
-
"flex min-h-7 shrink-0 items-center gap-1 rounded-og-sm px-2 py-1 text-og-xs font-medium transition-colors max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
|
|
667
|
+
"flex min-h-7 shrink-0 items-center justify-center gap-1 rounded-og-sm px-2 py-1 text-og-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
|
|
424
668
|
tab.id === current
|
|
425
669
|
? "bg-og-accent-soft text-og-fg"
|
|
426
670
|
: "text-og-fg-subtle hover:text-og-fg",
|
|
427
671
|
)}
|
|
428
672
|
>
|
|
429
|
-
<span>{tab.label}</span>
|
|
673
|
+
<span data-contrast-audited>{tab.label}</span>
|
|
430
674
|
{tab.badge}
|
|
431
675
|
</button>
|
|
432
676
|
))}
|
|
433
677
|
</div>
|
|
434
|
-
|
|
435
|
-
|
|
678
|
+
<div className="flex min-w-0 shrink-0 items-center justify-self-end min-[640px]:max-[1023px]:col-start-3 min-[640px]:max-[1023px]:row-start-1">
|
|
679
|
+
{accessory}
|
|
680
|
+
</div>
|
|
681
|
+
<div className="flex shrink-0 items-center gap-0.5 text-og-fg-subtle min-[640px]:max-[1023px]:col-start-4 min-[640px]:max-[1023px]:row-start-1">
|
|
682
|
+
{controls}
|
|
683
|
+
</div>
|
|
436
684
|
</div>
|
|
437
|
-
<div
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
685
|
+
<div className="min-h-0 min-w-0 flex-1 overflow-hidden">
|
|
686
|
+
{tabs.length > 0 ? (
|
|
687
|
+
tabs.map((tab, index) => {
|
|
688
|
+
const selected = tab.id === activeId;
|
|
689
|
+
const shouldMount = selected || visitedTabs.has(tab.id);
|
|
690
|
+
return (
|
|
691
|
+
<div
|
|
692
|
+
key={tab.id}
|
|
693
|
+
id={`${tabsetId}-panel-${index}`}
|
|
694
|
+
aria-labelledby={`${tabsetId}-tab-${index}`}
|
|
695
|
+
className="h-full min-h-0 min-w-0 overflow-hidden"
|
|
696
|
+
hidden={!selected}
|
|
697
|
+
role="tabpanel"
|
|
698
|
+
>
|
|
699
|
+
{shouldMount ? tab.content : null}
|
|
700
|
+
</div>
|
|
701
|
+
);
|
|
702
|
+
})
|
|
703
|
+
) : (
|
|
704
|
+
<div className="h-full min-h-0 min-w-0" aria-label="Workspace" role="tabpanel" />
|
|
705
|
+
)}
|
|
445
706
|
</div>
|
|
446
707
|
</>
|
|
447
708
|
);
|
package/src/composer.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced chat-composer framework. Import this subpath as a namespace:
|
|
3
|
+
*
|
|
4
|
+
* `import * as Composer from "@opengeni/react/composer"`.
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
Actions,
|
|
8
|
+
AttachButton,
|
|
9
|
+
Attachments,
|
|
10
|
+
CommandPalette,
|
|
11
|
+
Confirmation,
|
|
12
|
+
Controls,
|
|
13
|
+
Footer,
|
|
14
|
+
Frame,
|
|
15
|
+
Help,
|
|
16
|
+
Hint,
|
|
17
|
+
Input,
|
|
18
|
+
ModelPicker,
|
|
19
|
+
OPEN_WORKSTREAM_CONTROL_EVENT,
|
|
20
|
+
PauseButton,
|
|
21
|
+
PausedState,
|
|
22
|
+
RestoredResources,
|
|
23
|
+
Root,
|
|
24
|
+
SendButton,
|
|
25
|
+
Status,
|
|
26
|
+
Surface,
|
|
27
|
+
defaultChatComposerMessages,
|
|
28
|
+
useChatComposer,
|
|
29
|
+
useChatComposerController,
|
|
30
|
+
} from "./components/composer";
|
|
31
|
+
export type {
|
|
32
|
+
ChatComposerController,
|
|
33
|
+
ChatComposerContextValue,
|
|
34
|
+
ChatComposerMessages,
|
|
35
|
+
ComposerActionsProps,
|
|
36
|
+
ComposerAttachButtonProps,
|
|
37
|
+
ComposerCommandPaletteProps,
|
|
38
|
+
ComposerControlLinks,
|
|
39
|
+
ComposerControlState,
|
|
40
|
+
ComposerControlsProps,
|
|
41
|
+
ComposerDelivery,
|
|
42
|
+
ComposerDraftState,
|
|
43
|
+
ComposerFooterProps,
|
|
44
|
+
ComposerFrameProps,
|
|
45
|
+
ComposerHintProps,
|
|
46
|
+
ComposerInputProps,
|
|
47
|
+
ComposerModelPickerProps,
|
|
48
|
+
ComposerPauseButtonProps,
|
|
49
|
+
ComposerRootProps,
|
|
50
|
+
ComposerSendButtonProps,
|
|
51
|
+
ComposerSubmitBlocker,
|
|
52
|
+
ComposerSubmitMode,
|
|
53
|
+
ComposerSurfaceProps,
|
|
54
|
+
UseChatComposerControllerOptions,
|
|
55
|
+
} from "./components/composer";
|
|
56
|
+
export { ComposerTranscriptionControl } from "./components/composer-transcription-control";
|
|
57
|
+
export type {
|
|
58
|
+
ComposerTranscriptionControlProps,
|
|
59
|
+
ComposerTranscriptionMessages,
|
|
60
|
+
} from "./components/composer-transcription-control";
|