@opengeni/react 0.13.0 → 0.15.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 +19 -13
- package/dist/chunk-TOJR776I.js +2280 -0
- package/dist/chunk-TOJR776I.js.map +1 -0
- package/dist/index.d.ts +314 -255
- package/dist/index.js +5862 -4404
- package/dist/index.js.map +1 -1
- package/dist/{machines-CnlMb7E-.d.ts → machines-BpdwuQcD.d.ts} +130 -10
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +23 -1
- package/package.json +5 -2
- package/src/client.ts +10 -1
- package/src/components/chat-composer.tsx +309 -57
- package/src/components/machine-card.tsx +81 -15
- 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 +13 -1
- package/src/components/queue-surface.tsx +578 -0
- package/src/components/sandbox-files.tsx +94 -9
- package/src/components/sandbox-workspace.tsx +186 -52
- package/src/components/session-status.tsx +0 -6
- package/src/components/workbench-changes.tsx +64 -20
- package/src/components/workspace-dock.tsx +146 -55
- package/src/hooks/use-composer.ts +369 -39
- package/src/hooks/use-session-control.ts +6 -7
- package/src/hooks/use-session-events.ts +3 -2
- package/src/hooks/use-session-lineage.ts +15 -6
- package/src/hooks/use-session.ts +10 -2
- package/src/hooks/use-turn-queue.ts +175 -47
- package/src/index.ts +13 -7
- package/src/machines.ts +16 -0
- package/src/provider.tsx +192 -5
- package/src/timeline/parsers.ts +43 -6
- package/src/timeline/projection.ts +24 -2
- package/styles/index.css +22 -0
- package/dist/chunk-NFYVQWIB.js +0 -1377
- package/dist/chunk-NFYVQWIB.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GitFileDiff } from "@opengeni/sdk";
|
|
2
|
-
import { FileWarningIcon, HistoryIcon } from "lucide-react";
|
|
2
|
+
import { ArrowUpRightIcon, FileWarningIcon, HistoryIcon } from "lucide-react";
|
|
3
3
|
import {
|
|
4
4
|
type ReactNode,
|
|
5
5
|
useCallback,
|
|
@@ -77,6 +77,8 @@ export type WorkbenchChangesProps = {
|
|
|
77
77
|
/** The capture's turn revision, for the "as of turn N" badge. */
|
|
78
78
|
captureRevision?: number | null | undefined;
|
|
79
79
|
themeType?: "dark" | "light" | undefined;
|
|
80
|
+
/** Route a guarded binary/oversize file into the live Files surface. */
|
|
81
|
+
onOpenFile?: ((path: string) => void) | undefined;
|
|
80
82
|
className?: string | undefined;
|
|
81
83
|
};
|
|
82
84
|
|
|
@@ -137,6 +139,7 @@ export function WorkbenchChanges({
|
|
|
137
139
|
capturedAt,
|
|
138
140
|
captureRevision,
|
|
139
141
|
themeType,
|
|
142
|
+
onOpenFile,
|
|
140
143
|
className,
|
|
141
144
|
}: WorkbenchChangesProps) {
|
|
142
145
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -211,7 +214,9 @@ export function WorkbenchChanges({
|
|
|
211
214
|
// Track which section is at the top of the pane so the rail highlights it.
|
|
212
215
|
const onPaneScroll = useCallback(() => {
|
|
213
216
|
const el = windowed.scrollRef.current;
|
|
214
|
-
if (!el)
|
|
217
|
+
if (!el) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
215
220
|
const top = el.scrollTop;
|
|
216
221
|
const offsets = windowed.offsets;
|
|
217
222
|
let idx = 0;
|
|
@@ -240,13 +245,23 @@ export function WorkbenchChanges({
|
|
|
240
245
|
data-workbench-changes-layout={compact ? "compact" : "rail"}
|
|
241
246
|
>
|
|
242
247
|
{/* Summary + source badge + layout toggle. */}
|
|
243
|
-
<div
|
|
244
|
-
|
|
248
|
+
<div
|
|
249
|
+
className={cn(
|
|
250
|
+
"flex shrink-0 items-center justify-between gap-2 border-b border-og-border px-3 py-1.5",
|
|
251
|
+
compact && source === "capture" && "flex-col items-stretch gap-1.5 py-2",
|
|
252
|
+
)}
|
|
253
|
+
>
|
|
254
|
+
<span data-contrast-audited className="min-w-0 text-og-xs text-og-fg-muted">
|
|
245
255
|
{diff.length} {diff.length === 1 ? "file" : "files"} changed
|
|
246
256
|
<span className="ml-2 text-og-status-idle">+{additions}</span>
|
|
247
257
|
<span className="ml-1 text-og-status-failed">−{deletions}</span>
|
|
248
258
|
</span>
|
|
249
|
-
<div
|
|
259
|
+
<div
|
|
260
|
+
className={cn(
|
|
261
|
+
"flex shrink-0 items-center gap-2",
|
|
262
|
+
compact && source === "capture" && "self-start",
|
|
263
|
+
)}
|
|
264
|
+
>
|
|
250
265
|
{compact ? null : <LayoutToggle layout={layout} onChange={setLayout} />}
|
|
251
266
|
<SourceBadge source={source} capturedAt={capturedAt} label={sourceBadge} />
|
|
252
267
|
</div>
|
|
@@ -271,21 +286,26 @@ export function WorkbenchChanges({
|
|
|
271
286
|
>
|
|
272
287
|
{orderedFiles.map((file, index) => (
|
|
273
288
|
<option key={file.path} value={index}>
|
|
274
|
-
{STATUS_LETTER[file.status]} · {file.path}
|
|
289
|
+
{STATUS_LETTER[file.status]} · {file.path}
|
|
275
290
|
</option>
|
|
276
291
|
))}
|
|
277
292
|
</select>
|
|
278
293
|
</div>
|
|
279
294
|
<div className="min-h-0 min-w-0 flex-1 overflow-auto" data-opengeni-changes-pane>
|
|
280
295
|
{activeFile ? (
|
|
281
|
-
<DiffSection
|
|
296
|
+
<DiffSection
|
|
297
|
+
file={activeFile}
|
|
298
|
+
layout="unified"
|
|
299
|
+
themeType={resolvedTheme}
|
|
300
|
+
{...(onOpenFile ? { onOpenFile } : {})}
|
|
301
|
+
/>
|
|
282
302
|
) : null}
|
|
283
303
|
</div>
|
|
284
304
|
</div>
|
|
285
305
|
) : (
|
|
286
306
|
<div className="flex min-h-0 flex-1">
|
|
287
307
|
{/* File rail — virtualized (virtua): a dense change set is fine. */}
|
|
288
|
-
<div className="w-[
|
|
308
|
+
<div className="w-[clamp(12rem,28%,15rem)] shrink-0 border-r border-og-border bg-og-surface-1/35">
|
|
289
309
|
<VList
|
|
290
310
|
className="h-full"
|
|
291
311
|
itemSize={coarsePointer ? 44 : 28}
|
|
@@ -332,7 +352,12 @@ export function WorkbenchChanges({
|
|
|
332
352
|
top={windowed.offsets[index] ?? 0}
|
|
333
353
|
onMeasure={windowed.measure}
|
|
334
354
|
>
|
|
335
|
-
<DiffSection
|
|
355
|
+
<DiffSection
|
|
356
|
+
file={file}
|
|
357
|
+
layout={layout}
|
|
358
|
+
themeType={resolvedTheme}
|
|
359
|
+
{...(onOpenFile ? { onOpenFile } : {})}
|
|
360
|
+
/>
|
|
336
361
|
</MeasuredSection>
|
|
337
362
|
);
|
|
338
363
|
})}
|
|
@@ -377,13 +402,13 @@ function SourceBadge({
|
|
|
377
402
|
);
|
|
378
403
|
}
|
|
379
404
|
|
|
380
|
-
/** "
|
|
405
|
+
/** "Live diff" · "as of turn 7 · 14:32" · "as of 14:32" (no revision). */
|
|
381
406
|
function describeSource(
|
|
382
407
|
source: "live" | "capture" | null,
|
|
383
408
|
capturedAt: string | null,
|
|
384
409
|
revision: number | null,
|
|
385
410
|
): string {
|
|
386
|
-
if (source !== "capture" || !capturedAt) return "
|
|
411
|
+
if (source !== "capture" || !capturedAt) return "Live diff";
|
|
387
412
|
const time = formatAsOf(capturedAt, Date.now());
|
|
388
413
|
return revision !== null ? `as of turn ${revision} · ${time}` : `as of ${time}`;
|
|
389
414
|
}
|
|
@@ -409,9 +434,10 @@ function RailFileRow({
|
|
|
409
434
|
title={file.path}
|
|
410
435
|
data-rail-file
|
|
411
436
|
className={cn(
|
|
412
|
-
"flex min-h-7 w-full items-center gap-1.5 truncate px-2 py-0.5 text-left text-og-sm hover:bg-og-surface-2 pointer-coarse:min-h-11",
|
|
437
|
+
"relative flex min-h-7 w-full items-center gap-1.5 truncate px-2 py-0.5 text-left text-og-sm transition-colors hover:bg-og-surface-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent pointer-coarse:min-h-11",
|
|
413
438
|
grouped && "pl-3",
|
|
414
|
-
active &&
|
|
439
|
+
active &&
|
|
440
|
+
"bg-og-accent-soft text-og-fg before:absolute before:inset-y-1 before:left-0 before:w-0.5 before:rounded-r-full before:bg-og-accent",
|
|
415
441
|
)}
|
|
416
442
|
>
|
|
417
443
|
<span
|
|
@@ -468,10 +494,12 @@ function DiffSection({
|
|
|
468
494
|
file,
|
|
469
495
|
layout,
|
|
470
496
|
themeType,
|
|
497
|
+
onOpenFile,
|
|
471
498
|
}: {
|
|
472
499
|
file: GitFileDiff;
|
|
473
500
|
layout: "unified" | "split";
|
|
474
501
|
themeType: "dark" | "light";
|
|
502
|
+
onOpenFile?: ((path: string) => void) | undefined;
|
|
475
503
|
}) {
|
|
476
504
|
// The guard files (binary / over-cap) get a minimal header + "open live" body
|
|
477
505
|
// since Pierre has nothing to render; real diffs use Pierre's own sticky file
|
|
@@ -489,12 +517,12 @@ function DiffSection({
|
|
|
489
517
|
<span className="text-og-status-failed">−{file.deletions}</span>
|
|
490
518
|
</span>
|
|
491
519
|
</header>
|
|
492
|
-
<GuardBody file={file} />
|
|
520
|
+
<GuardBody file={file} {...(onOpenFile ? { onOpenFile } : {})} />
|
|
493
521
|
</section>
|
|
494
522
|
);
|
|
495
523
|
}
|
|
496
524
|
return (
|
|
497
|
-
<section className="border-b border-og-border pb-2">
|
|
525
|
+
<section data-pierre-section className="border-b border-og-border pb-2">
|
|
498
526
|
<PierreDiff diff={[file]} layout={layout} themeType={themeType} className="px-1" />
|
|
499
527
|
</section>
|
|
500
528
|
);
|
|
@@ -502,12 +530,28 @@ function DiffSection({
|
|
|
502
530
|
|
|
503
531
|
/** The per-file guard: a binary file or an over-cap diff opens live rather than
|
|
504
532
|
* inlining a body we don't (fully) have. */
|
|
505
|
-
function GuardBody({
|
|
533
|
+
function GuardBody({
|
|
534
|
+
file,
|
|
535
|
+
onOpenFile,
|
|
536
|
+
}: {
|
|
537
|
+
file: GitFileDiff;
|
|
538
|
+
onOpenFile?: ((path: string) => void) | undefined;
|
|
539
|
+
}) {
|
|
506
540
|
const reason = file.isBinary ? "Binary file" : "Diff too large to show here";
|
|
507
541
|
return (
|
|
508
|
-
<div className="flex items-center gap-2 px-3 py-3 text-og-sm text-og-fg-subtle">
|
|
509
|
-
<FileWarningIcon className="size-3.5 shrink-0" />
|
|
510
|
-
<span className="min-w-0">{reason}
|
|
542
|
+
<div className="flex flex-wrap items-center gap-2 px-3 py-3 text-og-sm text-og-fg-subtle">
|
|
543
|
+
<FileWarningIcon className="size-3.5 shrink-0" aria-hidden />
|
|
544
|
+
<span className="min-w-0 flex-1">{reason}.</span>
|
|
545
|
+
{onOpenFile ? (
|
|
546
|
+
<button
|
|
547
|
+
type="button"
|
|
548
|
+
onClick={() => onOpenFile(file.path)}
|
|
549
|
+
className="inline-flex min-h-8 shrink-0 items-center gap-1.5 rounded-og-sm border border-og-border px-2 py-1 text-og-xs font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:bg-og-surface-2 hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:min-h-11"
|
|
550
|
+
>
|
|
551
|
+
Open in Files
|
|
552
|
+
<ArrowUpRightIcon className="size-3.5" aria-hidden />
|
|
553
|
+
</button>
|
|
554
|
+
) : null}
|
|
511
555
|
</div>
|
|
512
556
|
);
|
|
513
557
|
}
|
|
@@ -527,7 +571,7 @@ function LayoutToggle({
|
|
|
527
571
|
type="button"
|
|
528
572
|
onClick={() => onChange(value)}
|
|
529
573
|
className={cn(
|
|
530
|
-
"min-h-7 rounded-og-xs px-1.5 py-0.5 text-2xs capitalize pointer-coarse:min-h-11",
|
|
574
|
+
"min-h-7 rounded-og-xs px-1.5 py-0.5 text-2xs capitalize focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:min-h-11",
|
|
531
575
|
layout === value
|
|
532
576
|
? "bg-og-accent-soft text-og-fg"
|
|
533
577
|
: "text-og-fg-subtle hover:text-og-fg",
|
|
@@ -113,6 +113,9 @@ export function WorkspaceDock({
|
|
|
113
113
|
className,
|
|
114
114
|
}: WorkspaceDockProps) {
|
|
115
115
|
const narrow = useIsNarrow(DOCK_OVERLAY_BREAKPOINT);
|
|
116
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
117
|
+
const reopenRef = useRef<HTMLButtonElement | null>(null);
|
|
118
|
+
const returnFocusRef = useRef<HTMLElement | null>(null);
|
|
116
119
|
const dockPanelRef = usePanelRef();
|
|
117
120
|
const [internalCollapsed, setInternalCollapsed] = useState(false);
|
|
118
121
|
const [maximized, setMaximized] = useState(false);
|
|
@@ -124,6 +127,7 @@ export function WorkspaceDock({
|
|
|
124
127
|
// bug). Standalone (uncontrolled) usage keeps the built-in collapse button
|
|
125
128
|
// and the thin re-open rail as its only affordances.
|
|
126
129
|
const hostControlled = collapsedProp !== undefined;
|
|
130
|
+
const previousCollapsedRef = useRef(collapsed);
|
|
127
131
|
|
|
128
132
|
// Persisted layout (width split) keyed by autoSaveId.
|
|
129
133
|
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
|
@@ -164,6 +168,44 @@ export function WorkspaceDock({
|
|
|
164
168
|
}
|
|
165
169
|
}, [collapsedProp, dockPanelRef]);
|
|
166
170
|
|
|
171
|
+
// Treat the narrow overlay like a real modal: remember the external opener,
|
|
172
|
+
// move focus into the selected tab, and restore it on close. The standalone
|
|
173
|
+
// desktop dock instead focuses its newly-visible reopen rail after collapse.
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const previous = previousCollapsedRef.current;
|
|
176
|
+
previousCollapsedRef.current = collapsed;
|
|
177
|
+
if (previous === collapsed) return;
|
|
178
|
+
|
|
179
|
+
if (!collapsed) {
|
|
180
|
+
const active = document.activeElement;
|
|
181
|
+
const workspaceSurface = rootRef.current?.querySelector<HTMLElement>(
|
|
182
|
+
narrow ? '[role="dialog"]' : "[data-workspace-surface]",
|
|
183
|
+
);
|
|
184
|
+
if (active instanceof HTMLElement && !workspaceSurface?.contains(active)) {
|
|
185
|
+
returnFocusRef.current = active;
|
|
186
|
+
}
|
|
187
|
+
if (narrow) {
|
|
188
|
+
const frame = requestAnimationFrame(() => {
|
|
189
|
+
rootRef.current
|
|
190
|
+
?.querySelector<HTMLElement>('[role="dialog"] [role="tab"][aria-selected="true"]')
|
|
191
|
+
?.focus();
|
|
192
|
+
});
|
|
193
|
+
return () => cancelAnimationFrame(frame);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const frame = requestAnimationFrame(() => {
|
|
199
|
+
if (!hostControlled) {
|
|
200
|
+
reopenRef.current?.focus();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const target = returnFocusRef.current;
|
|
204
|
+
if (target?.isConnected) target.focus();
|
|
205
|
+
});
|
|
206
|
+
return () => cancelAnimationFrame(frame);
|
|
207
|
+
}, [collapsed, hostControlled, narrow]);
|
|
208
|
+
|
|
167
209
|
// Keep the active tab valid if the available tabs change.
|
|
168
210
|
useEffect(() => {
|
|
169
211
|
if (firstTabId && !requestedTabIsValid) {
|
|
@@ -200,33 +242,45 @@ export function WorkspaceDock({
|
|
|
200
242
|
// driven by the same `collapsed` contract (collapsed → hidden).
|
|
201
243
|
if (narrow) {
|
|
202
244
|
return (
|
|
203
|
-
<div className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
245
|
+
<div ref={rootRef} className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
204
246
|
<div className="min-h-0 min-w-0 flex-1">{primary}</div>
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
247
|
+
<div
|
|
248
|
+
role="dialog"
|
|
249
|
+
aria-modal="true"
|
|
250
|
+
aria-label="Workspace"
|
|
251
|
+
className="fixed inset-0 z-40 flex flex-col bg-og-bg"
|
|
252
|
+
hidden={collapsed}
|
|
253
|
+
style={{
|
|
254
|
+
paddingTop: "env(safe-area-inset-top)",
|
|
255
|
+
paddingBottom: "env(safe-area-inset-bottom)",
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<DockChrome
|
|
259
|
+
tabs={tabs}
|
|
260
|
+
current={current}
|
|
261
|
+
onTab={setTab}
|
|
262
|
+
leading={mobileLeadingControl}
|
|
263
|
+
accessory={headerAccessory}
|
|
264
|
+
controls={
|
|
265
|
+
<ChromeButton onClick={collapse} title="Close workspace" label="Close workspace">
|
|
266
|
+
<XIcon className="size-4" />
|
|
267
|
+
</ChromeButton>
|
|
268
|
+
}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
{collapsed && !hostControlled ? (
|
|
272
|
+
<button
|
|
273
|
+
ref={reopenRef}
|
|
274
|
+
type="button"
|
|
275
|
+
onClick={expand}
|
|
276
|
+
title="Open workspace"
|
|
277
|
+
aria-label="Open workspace"
|
|
278
|
+
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"
|
|
279
|
+
style={{ marginTop: "env(safe-area-inset-top)" }}
|
|
215
280
|
>
|
|
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
|
-
)}
|
|
281
|
+
<ChevronsLeftRightIcon className="size-4" aria-hidden />
|
|
282
|
+
</button>
|
|
283
|
+
) : null}
|
|
230
284
|
</div>
|
|
231
285
|
);
|
|
232
286
|
}
|
|
@@ -261,7 +315,7 @@ export function WorkspaceDock({
|
|
|
261
315
|
);
|
|
262
316
|
|
|
263
317
|
return (
|
|
264
|
-
<div className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
318
|
+
<div ref={rootRef} className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
265
319
|
<Group
|
|
266
320
|
orientation="horizontal"
|
|
267
321
|
className="min-h-0 flex-1"
|
|
@@ -296,13 +350,19 @@ export function WorkspaceDock({
|
|
|
296
350
|
}}
|
|
297
351
|
className="min-h-0 min-w-0"
|
|
298
352
|
>
|
|
299
|
-
{/*
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
353
|
+
{/* One persistent mount across normal, collapsed, and maximized modes:
|
|
354
|
+
layout changes must never destroy an editor buffer or terminal view. */}
|
|
355
|
+
<div
|
|
356
|
+
data-workspace-surface
|
|
357
|
+
aria-hidden={collapsed ? true : undefined}
|
|
358
|
+
className={cn(
|
|
359
|
+
"flex h-full min-h-0 min-w-0 flex-col bg-og-bg",
|
|
360
|
+
maximized ? "fixed inset-0 z-40" : "border-l border-og-border",
|
|
361
|
+
collapsed && "invisible pointer-events-none",
|
|
362
|
+
)}
|
|
363
|
+
>
|
|
364
|
+
{dockChrome}
|
|
365
|
+
</div>
|
|
306
366
|
</Panel>
|
|
307
367
|
</Group>
|
|
308
368
|
|
|
@@ -310,6 +370,7 @@ export function WorkspaceDock({
|
|
|
310
370
|
when the host controls collapse — its own toggle is the one way in. */}
|
|
311
371
|
{collapsed && !maximized && !hostControlled && (
|
|
312
372
|
<button
|
|
373
|
+
ref={reopenRef}
|
|
313
374
|
type="button"
|
|
314
375
|
onClick={expand}
|
|
315
376
|
title="Open workspace"
|
|
@@ -318,9 +379,6 @@ export function WorkspaceDock({
|
|
|
318
379
|
<ChevronsLeftRightIcon className="size-3.5" />
|
|
319
380
|
</button>
|
|
320
381
|
)}
|
|
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
382
|
</div>
|
|
325
383
|
);
|
|
326
384
|
}
|
|
@@ -343,7 +401,7 @@ function ChromeButton({
|
|
|
343
401
|
onClick={onClick}
|
|
344
402
|
title={title}
|
|
345
403
|
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"
|
|
404
|
+
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
405
|
>
|
|
348
406
|
{children}
|
|
349
407
|
</button>
|
|
@@ -369,14 +427,26 @@ function DockChrome({
|
|
|
369
427
|
controls: ReactNode;
|
|
370
428
|
}) {
|
|
371
429
|
const active = tabs.find((t) => t.id === current) ?? tabs[0];
|
|
372
|
-
const activeIndex = tabs.findIndex((tab) => tab.id === active?.id);
|
|
373
430
|
const tabsetId = useId();
|
|
374
431
|
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
|
375
|
-
const
|
|
432
|
+
const [visitedTabs, setVisitedTabs] = useState<Set<string>>(() => new Set());
|
|
433
|
+
const activeId = active?.id ?? "";
|
|
434
|
+
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
if (!activeId) return;
|
|
437
|
+
setVisitedTabs((previous) => {
|
|
438
|
+
if (previous.has(activeId)) return previous;
|
|
439
|
+
const next = new Set(previous);
|
|
440
|
+
next.add(activeId);
|
|
441
|
+
return next;
|
|
442
|
+
});
|
|
443
|
+
}, [activeId]);
|
|
376
444
|
|
|
377
445
|
const activateTab = (index: number) => {
|
|
378
446
|
const tab = tabs[index];
|
|
379
|
-
if (!tab)
|
|
447
|
+
if (!tab) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
380
450
|
onTab(tab.id);
|
|
381
451
|
tabRefs.current[index]?.focus();
|
|
382
452
|
};
|
|
@@ -395,13 +465,21 @@ function DockChrome({
|
|
|
395
465
|
|
|
396
466
|
return (
|
|
397
467
|
<>
|
|
398
|
-
<div className="
|
|
399
|
-
|
|
468
|
+
<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">
|
|
469
|
+
<div className="flex min-w-0 shrink-0 items-center max-[1023px]:min-h-11">
|
|
470
|
+
{leading ?? (
|
|
471
|
+
<span className="hidden truncate px-1 text-og-sm font-semibold text-og-fg max-[1023px]:inline">
|
|
472
|
+
Workspace
|
|
473
|
+
</span>
|
|
474
|
+
)}
|
|
475
|
+
</div>
|
|
400
476
|
{/* The tab list scrolls horizontally when it can't fit — it must never
|
|
401
477
|
grow into or overlap the chrome controls (they stay shrink-0). The
|
|
402
|
-
scrollbar is hidden to keep the strip calm.
|
|
478
|
+
scrollbar is hidden to keep the strip calm. On the narrow overlay it
|
|
479
|
+
owns a full second row, so status/close chrome can never squeeze the
|
|
480
|
+
canonical workspace navigation out of view. */}
|
|
403
481
|
<div
|
|
404
|
-
className="flex min-w-0
|
|
482
|
+
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"
|
|
405
483
|
role="tablist"
|
|
406
484
|
aria-orientation="horizontal"
|
|
407
485
|
>
|
|
@@ -415,12 +493,12 @@ function DockChrome({
|
|
|
415
493
|
type="button"
|
|
416
494
|
role="tab"
|
|
417
495
|
aria-selected={tab.id === current}
|
|
418
|
-
aria-controls={
|
|
496
|
+
aria-controls={`${tabsetId}-panel-${index}`}
|
|
419
497
|
tabIndex={tab.id === current ? 0 : -1}
|
|
420
498
|
onClick={() => onTab(tab.id)}
|
|
421
499
|
onKeyDown={(event) => onTabKeyDown(event, index)}
|
|
422
500
|
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",
|
|
501
|
+
"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
502
|
tab.id === current
|
|
425
503
|
? "bg-og-accent-soft text-og-fg"
|
|
426
504
|
: "text-og-fg-subtle hover:text-og-fg",
|
|
@@ -431,17 +509,30 @@ function DockChrome({
|
|
|
431
509
|
</button>
|
|
432
510
|
))}
|
|
433
511
|
</div>
|
|
434
|
-
|
|
512
|
+
<div className="flex min-w-0 shrink-0 items-center justify-self-end">{accessory}</div>
|
|
435
513
|
<div className="flex shrink-0 items-center gap-0.5 text-og-fg-subtle">{controls}</div>
|
|
436
514
|
</div>
|
|
437
|
-
<div
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
515
|
+
<div className="min-h-0 min-w-0 flex-1 overflow-hidden">
|
|
516
|
+
{tabs.length > 0 ? (
|
|
517
|
+
tabs.map((tab, index) => {
|
|
518
|
+
const selected = tab.id === activeId;
|
|
519
|
+
const shouldMount = selected || visitedTabs.has(tab.id);
|
|
520
|
+
return (
|
|
521
|
+
<div
|
|
522
|
+
key={tab.id}
|
|
523
|
+
id={`${tabsetId}-panel-${index}`}
|
|
524
|
+
aria-labelledby={`${tabsetId}-tab-${index}`}
|
|
525
|
+
className="h-full min-h-0 min-w-0 overflow-hidden"
|
|
526
|
+
hidden={!selected}
|
|
527
|
+
role="tabpanel"
|
|
528
|
+
>
|
|
529
|
+
{shouldMount ? tab.content : null}
|
|
530
|
+
</div>
|
|
531
|
+
);
|
|
532
|
+
})
|
|
533
|
+
) : (
|
|
534
|
+
<div className="h-full min-h-0 min-w-0" aria-label="Workspace" role="tabpanel" />
|
|
535
|
+
)}
|
|
445
536
|
</div>
|
|
446
537
|
</>
|
|
447
538
|
);
|