@opengeni/react 0.6.1 → 0.6.3
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/{chunk-DEW2ZNF2.js → chunk-5C7RGAWA.js} +70 -37
- package/dist/chunk-5C7RGAWA.js.map +1 -0
- package/dist/index.d.ts +29 -5
- package/dist/index.js +1061 -618
- package/dist/index.js.map +1 -1
- package/dist/{machines-BD6h9P_s.d.ts → machines-Bv9tG7qZ.d.ts} +1 -1
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +1 -1
- package/package.json +2 -2
- package/src/components/chat-composer.tsx +127 -60
- package/src/components/code-editor.tsx +15 -15
- package/src/components/desktop-viewer.tsx +40 -36
- package/src/components/diff-view.tsx +22 -22
- package/src/components/enrollment-consent.tsx +13 -13
- package/src/components/file-browser.tsx +74 -67
- package/src/components/fleet-tile.tsx +3 -3
- package/src/components/machine-card.tsx +6 -6
- package/src/components/machine-status-pill.tsx +3 -3
- package/src/components/machines-dashboard.tsx +28 -14
- package/src/components/markdown.tsx +6 -6
- package/src/components/message-timeline.tsx +75 -4
- package/src/components/pierre-diff.tsx +9 -10
- package/src/components/pierre-file.tsx +8 -8
- package/src/components/sandbox-files.tsx +37 -40
- package/src/components/sandbox-terminal.tsx +9 -11
- package/src/components/session-status.tsx +2 -2
- package/src/components/workspace-dock.tsx +153 -49
- package/src/hooks/use-file-attachments.ts +45 -15
- package/src/index.ts +2 -2
- package/src/lib/format.ts +32 -0
- package/src/lib/xterm-theme.ts +8 -11
- package/src/timeline/activity-rail.tsx +1 -1
- package/src/timeline/parsers.ts +104 -0
- package/src/timeline/projection.ts +135 -3
- package/src/timeline/screenshot-lightbox.tsx +1 -1
- package/src/timeline/shared.tsx +4 -1
- package/src/timeline/tool-diff.tsx +1 -1
- package/src/timeline/tool-renderers.tsx +46 -7
- package/src/timeline/turn-summary.tsx +21 -3
- package/src/timeline/types.ts +2 -0
- package/styles/tokens.css +8 -0
- package/dist/chunk-DEW2ZNF2.js.map +0 -1
|
@@ -99,12 +99,12 @@ export function PierreFile({
|
|
|
99
99
|
// Same shadow-DOM override vars as PierreDiff: pin the base background to the
|
|
100
100
|
// dock surface so the viewer reads as part of the panel, not a black seam.
|
|
101
101
|
const pierreVars = {
|
|
102
|
-
"--diffs-dark-bg": "var(--og-color-bg
|
|
103
|
-
"--diffs-light-bg": "var(--og-color-bg
|
|
104
|
-
"--diffs-bg-buffer-override": "var(--og-color-surface-1
|
|
105
|
-
"--diffs-bg-separator-override": "var(--og-color-surface-1
|
|
106
|
-
"--diffs-font-size": "
|
|
107
|
-
"--diffs-line-height": "
|
|
102
|
+
"--diffs-dark-bg": "var(--og-color-bg)",
|
|
103
|
+
"--diffs-light-bg": "var(--og-color-bg)",
|
|
104
|
+
"--diffs-bg-buffer-override": "var(--og-color-surface-1)",
|
|
105
|
+
"--diffs-bg-separator-override": "var(--og-color-surface-1)",
|
|
106
|
+
"--diffs-font-size": "var(--og-code-font-size)",
|
|
107
|
+
"--diffs-line-height": "var(--og-code-line-height)",
|
|
108
108
|
} as CSSProperties;
|
|
109
109
|
|
|
110
110
|
return (
|
|
@@ -125,7 +125,7 @@ export function PierreFile({
|
|
|
125
125
|
function PlainFile({ name, contents }: { name: string; contents: string }) {
|
|
126
126
|
return (
|
|
127
127
|
<pre
|
|
128
|
-
className="overflow-auto whitespace-pre p-2 font-
|
|
128
|
+
className="overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg"
|
|
129
129
|
data-file={name}
|
|
130
130
|
>
|
|
131
131
|
{contents}
|
|
@@ -135,7 +135,7 @@ function PlainFile({ name, contents }: { name: string; contents: string }) {
|
|
|
135
135
|
|
|
136
136
|
function FileSkeleton() {
|
|
137
137
|
return (
|
|
138
|
-
<div className="p-3 text-
|
|
138
|
+
<div className="p-3 text-og-sm text-og-fg-subtle">
|
|
139
139
|
Loading file…
|
|
140
140
|
</div>
|
|
141
141
|
);
|
|
@@ -30,15 +30,15 @@ export type SandboxFilesProps = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
const STATUS_TINT: Record<GitFileDiff["status"], string> = {
|
|
33
|
-
added: "text-
|
|
34
|
-
modified: "text-
|
|
35
|
-
deleted: "text-
|
|
36
|
-
renamed: "text-
|
|
37
|
-
copied: "text-
|
|
38
|
-
untracked: "text-
|
|
39
|
-
ignored: "text-
|
|
40
|
-
conflicted: "text-
|
|
41
|
-
typechange: "text-
|
|
33
|
+
added: "text-og-status-idle",
|
|
34
|
+
modified: "text-og-status-running",
|
|
35
|
+
deleted: "text-og-status-failed",
|
|
36
|
+
renamed: "text-og-accent",
|
|
37
|
+
copied: "text-og-accent",
|
|
38
|
+
untracked: "text-og-fg-subtle",
|
|
39
|
+
ignored: "text-og-fg-subtle",
|
|
40
|
+
conflicted: "text-og-status-failed",
|
|
41
|
+
typechange: "text-og-status-running",
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
const STATUS_LETTER: Record<GitFileDiff["status"], string> = {
|
|
@@ -158,13 +158,13 @@ export function SandboxFiles({
|
|
|
158
158
|
className={cn(
|
|
159
159
|
"min-h-0 overflow-auto",
|
|
160
160
|
wide
|
|
161
|
-
? "w-[280px] shrink-0 border-r border-
|
|
161
|
+
? "w-[280px] shrink-0 border-r border-og-border"
|
|
162
162
|
: "flex-1",
|
|
163
163
|
)}
|
|
164
164
|
>
|
|
165
165
|
{changed.length > 0 && (
|
|
166
|
-
<div className="border-b border-
|
|
167
|
-
<div className="px-2 py-1 text-
|
|
166
|
+
<div className="border-b border-og-border">
|
|
167
|
+
<div className="px-2 py-1 text-og-xs font-medium uppercase tracking-wide text-og-fg-subtle">
|
|
168
168
|
Changes · {changed.length}
|
|
169
169
|
</div>
|
|
170
170
|
<ul className="pb-1">
|
|
@@ -174,14 +174,13 @@ export function SandboxFiles({
|
|
|
174
174
|
type="button"
|
|
175
175
|
onClick={() => selectFile(file.path)}
|
|
176
176
|
className={cn(
|
|
177
|
-
"flex w-full items-center gap-1.5 truncate px-2 py-0.5 text-left text-
|
|
178
|
-
file.path === effectiveSelected &&
|
|
179
|
-
"bg-[color:var(--og-color-surface-2,var(--color-bg-subtle,#1c1c1c))]",
|
|
177
|
+
"flex 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-10",
|
|
178
|
+
file.path === effectiveSelected && "bg-og-surface-2",
|
|
180
179
|
)}
|
|
181
180
|
>
|
|
182
181
|
<span
|
|
183
182
|
className={cn(
|
|
184
|
-
"w-3 shrink-0 text-center font-
|
|
183
|
+
"w-3 shrink-0 text-center font-og-mono text-og-xs",
|
|
185
184
|
STATUS_TINT[file.status],
|
|
186
185
|
)}
|
|
187
186
|
>
|
|
@@ -189,11 +188,11 @@ export function SandboxFiles({
|
|
|
189
188
|
</span>
|
|
190
189
|
<FileIcon className="size-3.5 shrink-0 opacity-70" />
|
|
191
190
|
<span className="truncate">{file.path}</span>
|
|
192
|
-
<span className="ml-auto flex shrink-0 items-center gap-1.5 pl-2 text-
|
|
193
|
-
<span className="text-
|
|
191
|
+
<span className="ml-auto flex shrink-0 items-center gap-1.5 pl-2 text-og-xs">
|
|
192
|
+
<span className="text-og-status-idle">
|
|
194
193
|
+{file.additions}
|
|
195
194
|
</span>
|
|
196
|
-
<span className="text-
|
|
195
|
+
<span className="text-og-status-failed">
|
|
197
196
|
−{file.deletions}
|
|
198
197
|
</span>
|
|
199
198
|
</span>
|
|
@@ -204,14 +203,14 @@ export function SandboxFiles({
|
|
|
204
203
|
</div>
|
|
205
204
|
)}
|
|
206
205
|
|
|
207
|
-
<div className="px-2 py-1 text-
|
|
206
|
+
<div className="px-2 py-1 text-og-xs font-medium uppercase tracking-wide text-og-fg-subtle">
|
|
208
207
|
Files
|
|
209
208
|
</div>
|
|
210
209
|
<FileBrowser
|
|
211
210
|
result={files}
|
|
212
211
|
selectedPath={effectiveSelected ?? undefined}
|
|
213
212
|
onSelectFile={selectFile}
|
|
214
|
-
emptyState="
|
|
213
|
+
emptyState="This directory is empty"
|
|
215
214
|
className="min-w-0"
|
|
216
215
|
/>
|
|
217
216
|
</div>
|
|
@@ -223,14 +222,14 @@ export function SandboxFiles({
|
|
|
223
222
|
"flex min-h-0 min-w-0 flex-col",
|
|
224
223
|
wide
|
|
225
224
|
? "flex-1"
|
|
226
|
-
: "flex-[1.4] border-t border-
|
|
225
|
+
: "flex-[1.4] border-t border-og-border",
|
|
227
226
|
)}
|
|
228
227
|
>
|
|
229
|
-
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-
|
|
230
|
-
<span className="truncate font-
|
|
228
|
+
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1">
|
|
229
|
+
<span className="min-w-0 truncate font-og-mono text-og-xs text-og-fg-muted">
|
|
231
230
|
{effectiveSelected ?? "No file selected"}
|
|
232
231
|
</span>
|
|
233
|
-
<div className="flex shrink-0 items-center gap-1">
|
|
232
|
+
<div className="flex min-w-0 shrink-0 flex-wrap items-center justify-end gap-1">
|
|
234
233
|
{/* The Working/Staged + Unified/Split toggles only apply to a diff.
|
|
235
234
|
Hide them in the read-only viewer (a non-changed tree file). */}
|
|
236
235
|
{selectedDiff && stagedGit && (
|
|
@@ -306,7 +305,7 @@ export function SandboxFiles({
|
|
|
306
305
|
) : fileView.content !== null ? (
|
|
307
306
|
<>
|
|
308
307
|
{fileView.truncated && (
|
|
309
|
-
<div className="border-b border-
|
|
308
|
+
<div className="border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-xs text-og-status-running">
|
|
310
309
|
Large file — showing a truncated preview ({fileView.sizeBytes ?? 0} bytes loaded). Editing is disabled to avoid corrupting the file.
|
|
311
310
|
</div>
|
|
312
311
|
)}
|
|
@@ -316,14 +315,14 @@ export function SandboxFiles({
|
|
|
316
315
|
contents={fileView.content}
|
|
317
316
|
themeType={resolvedTheme}
|
|
318
317
|
fallback={
|
|
319
|
-
<pre className="overflow-auto whitespace-pre p-2 font-
|
|
318
|
+
<pre className="overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg">
|
|
320
319
|
{fileView.content}
|
|
321
320
|
</pre>
|
|
322
321
|
}
|
|
323
322
|
className="p-1"
|
|
324
323
|
/>
|
|
325
324
|
) : (
|
|
326
|
-
<pre className="overflow-auto whitespace-pre p-2 font-
|
|
325
|
+
<pre className="overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg">
|
|
327
326
|
{fileView.content}
|
|
328
327
|
</pre>
|
|
329
328
|
)}
|
|
@@ -350,27 +349,25 @@ export function SandboxFiles({
|
|
|
350
349
|
function GitHeader({ git, dirtyCount }: { git: UseSandboxGitResult; dirtyCount: number }) {
|
|
351
350
|
const dirty = dirtyCount > 0;
|
|
352
351
|
return (
|
|
353
|
-
<div className="flex shrink-0 items-center gap-2 border-b border-
|
|
352
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-sm">
|
|
354
353
|
<span
|
|
355
354
|
className={cn(
|
|
356
355
|
"size-2 shrink-0 rounded-full",
|
|
357
|
-
dirty
|
|
358
|
-
? "bg-[color:var(--og-color-status-running,var(--color-warning,#d29922))]"
|
|
359
|
-
: "bg-[color:var(--og-color-status-idle,var(--color-success,#3fb950))]",
|
|
356
|
+
dirty ? "bg-og-status-running" : "bg-og-status-idle",
|
|
360
357
|
)}
|
|
361
358
|
title={dirty ? `${dirtyCount} changed` : "clean"}
|
|
362
359
|
/>
|
|
363
|
-
<span className="truncate font-
|
|
360
|
+
<span className="truncate font-og-mono text-og-fg">
|
|
364
361
|
{git.branch ?? (git.isRepo ? "(detached)" : "no repo")}
|
|
365
362
|
</span>
|
|
366
363
|
{(git.ahead > 0 || git.behind > 0) && (
|
|
367
|
-
<span className="flex shrink-0 items-center gap-1.5 text-
|
|
364
|
+
<span className="flex shrink-0 items-center gap-1.5 text-og-xs text-og-fg-subtle">
|
|
368
365
|
{git.ahead > 0 && <span>↑{git.ahead}</span>}
|
|
369
366
|
{git.behind > 0 && <span>↓{git.behind}</span>}
|
|
370
367
|
</span>
|
|
371
368
|
)}
|
|
372
369
|
{dirty && (
|
|
373
|
-
<span className="ml-auto shrink-0 text-
|
|
370
|
+
<span className="ml-auto shrink-0 text-og-xs text-og-fg-subtle">
|
|
374
371
|
{dirtyCount} changed
|
|
375
372
|
</span>
|
|
376
373
|
)}
|
|
@@ -388,17 +385,17 @@ function Segmented({
|
|
|
388
385
|
onChange: (value: string) => void;
|
|
389
386
|
}) {
|
|
390
387
|
return (
|
|
391
|
-
<div className="flex items-center rounded-
|
|
388
|
+
<div className="flex flex-wrap items-center rounded-og-sm border border-og-border p-0.5">
|
|
392
389
|
{options.map((opt) => (
|
|
393
390
|
<button
|
|
394
391
|
key={opt.value}
|
|
395
392
|
type="button"
|
|
396
393
|
onClick={() => onChange(opt.value)}
|
|
397
394
|
className={cn(
|
|
398
|
-
"rounded-
|
|
395
|
+
"rounded-og-xs px-1.5 py-0.5 text-og-xs pointer-coarse:min-h-10",
|
|
399
396
|
opt.value === value
|
|
400
|
-
? "bg-
|
|
401
|
-
: "text-
|
|
397
|
+
? "bg-og-accent-soft text-og-fg"
|
|
398
|
+
: "text-og-fg-subtle hover:text-og-fg",
|
|
402
399
|
)}
|
|
403
400
|
>
|
|
404
401
|
{opt.label}
|
|
@@ -499,7 +496,7 @@ function Notice({ children, className }: { children: ReactNode; className?: stri
|
|
|
499
496
|
return (
|
|
500
497
|
<div
|
|
501
498
|
className={cn(
|
|
502
|
-
"flex h-full items-center justify-center p-4 text-center text-
|
|
499
|
+
"flex h-full items-center justify-center p-4 text-center text-og-sm text-og-fg-subtle",
|
|
503
500
|
className,
|
|
504
501
|
)}
|
|
505
502
|
>
|
|
@@ -181,7 +181,7 @@ export function SandboxTerminal({
|
|
|
181
181
|
convertEol: true,
|
|
182
182
|
disableStdin: !interactive,
|
|
183
183
|
cursorBlink: interactive,
|
|
184
|
-
fontFamily: fontFamily ?? "var(--og-font-mono
|
|
184
|
+
fontFamily: fontFamily ?? "var(--og-font-mono)",
|
|
185
185
|
fontSize: fontSize ?? 13,
|
|
186
186
|
lineHeight: 1.25,
|
|
187
187
|
// A little breathing room from the panel edge so output isn't flush to
|
|
@@ -318,23 +318,21 @@ export function SandboxTerminal({
|
|
|
318
318
|
return (
|
|
319
319
|
<div className={cn("relative flex h-full w-full flex-col overflow-hidden", className)}>
|
|
320
320
|
{showHeader && (
|
|
321
|
-
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-
|
|
321
|
+
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-og-border px-2 py-1 text-og-xs text-og-fg-subtle">
|
|
322
322
|
<span className="flex min-w-0 items-center gap-1.5">
|
|
323
323
|
<span
|
|
324
324
|
className={cn(
|
|
325
325
|
"size-1.5 shrink-0 rounded-full",
|
|
326
|
-
result.running
|
|
327
|
-
? "bg-[color:var(--og-color-status-running,var(--color-status-running,#d29922))]"
|
|
328
|
-
: "bg-[color:var(--og-color-fg-subtle,var(--color-fg-subtle,#888))]",
|
|
326
|
+
result.running ? "bg-og-status-running" : "bg-og-fg-subtle",
|
|
329
327
|
)}
|
|
330
328
|
/>
|
|
331
|
-
<span className="truncate font-
|
|
329
|
+
<span className="truncate font-og-mono">
|
|
332
330
|
pty: {shell ?? "shell"}
|
|
333
331
|
</span>
|
|
334
332
|
</span>
|
|
335
333
|
<span className="flex shrink-0 items-center gap-2">
|
|
336
334
|
{!interactive && (
|
|
337
|
-
<span className="rounded-
|
|
335
|
+
<span className="rounded-og-sm bg-og-surface-2 px-1.5 py-0.5 text-og-xs uppercase tracking-wide">
|
|
338
336
|
read-only
|
|
339
337
|
</span>
|
|
340
338
|
)}
|
|
@@ -344,7 +342,7 @@ export function SandboxTerminal({
|
|
|
344
342
|
termRef.current?.clear();
|
|
345
343
|
writtenRef.current = new Set();
|
|
346
344
|
}}
|
|
347
|
-
className="rounded-
|
|
345
|
+
className="rounded-og-sm px-1.5 py-0.5 text-og-xs hover:text-og-fg pointer-coarse:min-h-10"
|
|
348
346
|
>
|
|
349
347
|
Clear
|
|
350
348
|
</button>
|
|
@@ -355,7 +353,7 @@ export function SandboxTerminal({
|
|
|
355
353
|
with the terminal surface (capture so they win even though xterm's own
|
|
356
354
|
listeners stop propagation). The host warms the box for pty-ws here. */}
|
|
357
355
|
<div
|
|
358
|
-
className="relative min-h-0 flex-1 bg-
|
|
356
|
+
className="relative min-h-0 flex-1 bg-og-bg px-2 py-1.5"
|
|
359
357
|
onPointerDownCapture={onActivate}
|
|
360
358
|
onFocusCapture={onActivate}
|
|
361
359
|
>
|
|
@@ -379,7 +377,7 @@ const XTERM_BASE_CSS = `
|
|
|
379
377
|
.xterm.focus,.xterm:focus{outline:none}
|
|
380
378
|
.xterm .xterm-helpers{position:absolute;top:0;z-index:5}
|
|
381
379
|
.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}
|
|
382
|
-
.xterm .composition-view{background
|
|
380
|
+
.xterm .composition-view{background:var(--og-color-bg);color:var(--og-color-fg);display:none;position:absolute;white-space:nowrap;z-index:1}
|
|
383
381
|
.xterm .composition-view.active{display:block}
|
|
384
382
|
.xterm .xterm-viewport{background-color:transparent;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}
|
|
385
383
|
.xterm .xterm-screen{position:relative}
|
|
@@ -418,7 +416,7 @@ function ensureXtermBaseCss(): void {
|
|
|
418
416
|
|
|
419
417
|
function TerminalPlaceholder() {
|
|
420
418
|
return (
|
|
421
|
-
<div className="absolute inset-0 flex items-center justify-center text-
|
|
419
|
+
<div className="absolute inset-0 flex items-center justify-center text-og-sm text-og-fg-subtle">
|
|
422
420
|
Loading terminal…
|
|
423
421
|
</div>
|
|
424
422
|
);
|
|
@@ -30,7 +30,7 @@ export const SESSION_STATUS_META: Record<SessionStatusValue, SessionStatusMeta>
|
|
|
30
30
|
pulse: false,
|
|
31
31
|
},
|
|
32
32
|
requires_action: {
|
|
33
|
-
label: "
|
|
33
|
+
label: "Waiting on you",
|
|
34
34
|
dotClassName: "bg-og-status-waiting",
|
|
35
35
|
badgeClassName: "text-og-status-waiting border-og-status-waiting/35 bg-og-status-waiting/10",
|
|
36
36
|
pulse: true,
|
|
@@ -65,7 +65,7 @@ export function SessionStatus({ status, label, size = "md", className }: Session
|
|
|
65
65
|
data-status={status}
|
|
66
66
|
className={cn(
|
|
67
67
|
"og-root inline-flex shrink-0 items-center rounded-full border font-medium",
|
|
68
|
-
size === "sm" ? "gap-1 px-1.5 py-px text-
|
|
68
|
+
size === "sm" ? "gap-1 px-1.5 py-px text-og-xs" : "gap-1.5 px-2 py-0.5 text-xs",
|
|
69
69
|
meta.badgeClassName,
|
|
70
70
|
className,
|
|
71
71
|
)}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Maximize2Icon,
|
|
5
5
|
Minimize2Icon,
|
|
6
6
|
PanelRightCloseIcon,
|
|
7
|
+
XIcon,
|
|
7
8
|
} from "lucide-react";
|
|
8
9
|
import {
|
|
9
10
|
Group,
|
|
@@ -49,9 +50,43 @@ export type WorkspaceDockProps = {
|
|
|
49
50
|
* restore button returns). Layout persists via `useDefaultLayout` keyed on
|
|
50
51
|
* `autoSaveId`. Maximize is a mode ABOVE the Group (a `fixed inset-0` overlay) —
|
|
51
52
|
* pushing a Panel to ~100% still fights min sizes and leaves a chat sliver.
|
|
53
|
+
*
|
|
54
|
+
* Below {@link DOCK_OVERLAY_BREAKPOINT} the side-by-side split can't work on a
|
|
55
|
+
* phone-width viewport, so the resizable panels are dropped entirely: the
|
|
56
|
+
* primary pane goes full-width and the dock becomes a full-screen overlay driven
|
|
57
|
+
* by the same `collapsed` / `onCollapsedChange` contract (collapsed → hidden).
|
|
58
|
+
* No drag splitter renders below the breakpoint.
|
|
52
59
|
*/
|
|
53
60
|
const useDockLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
54
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The dock stops being a side column at this width and becomes a full-screen
|
|
64
|
+
* overlay — matches the app's rail-drawer breakpoint (the single `isMobile`
|
|
65
|
+
* source). The package can't read app context, so it detects the width itself.
|
|
66
|
+
*/
|
|
67
|
+
const DOCK_OVERLAY_BREAKPOINT = 1024;
|
|
68
|
+
|
|
69
|
+
/** SSR-safe `(max-width: …)` match; false until mounted, then live. */
|
|
70
|
+
function useIsNarrow(maxWidth: number): boolean {
|
|
71
|
+
const [narrow, setNarrow] = useState(false);
|
|
72
|
+
useDockLayoutEffect(() => {
|
|
73
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const mql = window.matchMedia(`(max-width: ${maxWidth - 1}px)`);
|
|
77
|
+
const update = () => setNarrow(mql.matches);
|
|
78
|
+
update();
|
|
79
|
+
if (typeof mql.addEventListener === "function") {
|
|
80
|
+
mql.addEventListener("change", update);
|
|
81
|
+
return () => mql.removeEventListener("change", update);
|
|
82
|
+
}
|
|
83
|
+
// Legacy Safari.
|
|
84
|
+
mql.addListener(update);
|
|
85
|
+
return () => mql.removeListener(update);
|
|
86
|
+
}, [maxWidth]);
|
|
87
|
+
return narrow;
|
|
88
|
+
}
|
|
89
|
+
|
|
55
90
|
export function WorkspaceDock({
|
|
56
91
|
primary,
|
|
57
92
|
tabs,
|
|
@@ -65,11 +100,18 @@ export function WorkspaceDock({
|
|
|
65
100
|
maxSize = 70,
|
|
66
101
|
className,
|
|
67
102
|
}: WorkspaceDockProps) {
|
|
103
|
+
const narrow = useIsNarrow(DOCK_OVERLAY_BREAKPOINT);
|
|
68
104
|
const dockPanelRef = usePanelRef();
|
|
69
105
|
const [internalCollapsed, setInternalCollapsed] = useState(false);
|
|
70
106
|
const [maximized, setMaximized] = useState(false);
|
|
71
107
|
const [internalTab, setInternalTab] = useState(tabs[0]?.id ?? "");
|
|
72
108
|
const collapsed = collapsedProp ?? internalCollapsed;
|
|
109
|
+
// When the host supplies `collapsed` it owns the open/close affordance (e.g.
|
|
110
|
+
// the app header's panel toggle) — the dock must not offer a SECOND
|
|
111
|
+
// open/close control (duplicate buttons with near-identical icons read as a
|
|
112
|
+
// bug). Standalone (uncontrolled) usage keeps the built-in collapse button
|
|
113
|
+
// and the thin re-open rail as its only affordances.
|
|
114
|
+
const hostControlled = collapsedProp !== undefined;
|
|
73
115
|
|
|
74
116
|
// Persisted layout (width split) keyed by autoSaveId.
|
|
75
117
|
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
|
@@ -117,16 +159,6 @@ export function WorkspaceDock({
|
|
|
117
159
|
// rebuild tab JSX frequently; only id changes can invalidate the active tab.
|
|
118
160
|
}, [tabIds, firstTabId, current, setTab]);
|
|
119
161
|
|
|
120
|
-
// Esc restores from maximize.
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
if (!maximized) return;
|
|
123
|
-
const onKey = (event: KeyboardEvent) => {
|
|
124
|
-
if (event.key === "Escape") setMaximized(false);
|
|
125
|
-
};
|
|
126
|
-
window.addEventListener("keydown", onKey);
|
|
127
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
128
|
-
}, [maximized]);
|
|
129
|
-
|
|
130
162
|
const collapse = useCallback(() => {
|
|
131
163
|
dockPanelRef.current?.collapse();
|
|
132
164
|
setCollapsed(true);
|
|
@@ -136,14 +168,74 @@ export function WorkspaceDock({
|
|
|
136
168
|
setCollapsed(false);
|
|
137
169
|
}, [dockPanelRef, setCollapsed]);
|
|
138
170
|
|
|
171
|
+
// Esc restores from maximize (desktop) and closes the mobile overlay.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
const overlayOpen = maximized || (narrow && !collapsed);
|
|
174
|
+
if (!overlayOpen) return;
|
|
175
|
+
const onKey = (event: KeyboardEvent) => {
|
|
176
|
+
if (event.key !== "Escape") return;
|
|
177
|
+
if (maximized) setMaximized(false);
|
|
178
|
+
else collapse();
|
|
179
|
+
};
|
|
180
|
+
window.addEventListener("keydown", onKey);
|
|
181
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
182
|
+
}, [maximized, narrow, collapsed, collapse]);
|
|
183
|
+
|
|
184
|
+
// Below the breakpoint the dock is a full-screen overlay, not a resizable
|
|
185
|
+
// column: primary goes full-width and no splitter ever mounts. The overlay is
|
|
186
|
+
// driven by the same `collapsed` contract (collapsed → hidden).
|
|
187
|
+
if (narrow) {
|
|
188
|
+
return (
|
|
189
|
+
<div className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
190
|
+
<div className="min-h-0 min-w-0 flex-1">{primary}</div>
|
|
191
|
+
{!collapsed && (
|
|
192
|
+
<div
|
|
193
|
+
role="dialog"
|
|
194
|
+
aria-modal="true"
|
|
195
|
+
aria-label="Workspace"
|
|
196
|
+
className="fixed inset-0 z-40 flex flex-col bg-og-bg"
|
|
197
|
+
style={{
|
|
198
|
+
paddingTop: "env(safe-area-inset-top)",
|
|
199
|
+
paddingBottom: "env(safe-area-inset-bottom)",
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<DockChrome
|
|
203
|
+
tabs={tabs}
|
|
204
|
+
current={current}
|
|
205
|
+
onTab={setTab}
|
|
206
|
+
controls={
|
|
207
|
+
<ChromeButton onClick={collapse} title="Close workspace" label="Close workspace">
|
|
208
|
+
<XIcon className="size-4" />
|
|
209
|
+
</ChromeButton>
|
|
210
|
+
}
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
139
218
|
const dockChrome = (
|
|
140
219
|
<DockChrome
|
|
141
220
|
tabs={tabs}
|
|
142
221
|
current={current}
|
|
143
222
|
onTab={setTab}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
223
|
+
controls={
|
|
224
|
+
<>
|
|
225
|
+
<ChromeButton
|
|
226
|
+
onClick={() => setMaximized((m) => !m)}
|
|
227
|
+
title={maximized ? "Restore (Esc)" : "Maximize"}
|
|
228
|
+
label={maximized ? "Restore dock" : "Maximize dock"}
|
|
229
|
+
>
|
|
230
|
+
{maximized ? <Minimize2Icon className="size-3.5" /> : <Maximize2Icon className="size-3.5" />}
|
|
231
|
+
</ChromeButton>
|
|
232
|
+
{hostControlled ? null : (
|
|
233
|
+
<ChromeButton onClick={collapse} title="Collapse" label="Collapse dock">
|
|
234
|
+
<PanelRightCloseIcon className="size-3.5" />
|
|
235
|
+
</ChromeButton>
|
|
236
|
+
)}
|
|
237
|
+
</>
|
|
238
|
+
}
|
|
147
239
|
/>
|
|
148
240
|
);
|
|
149
241
|
|
|
@@ -161,7 +253,7 @@ export function WorkspaceDock({
|
|
|
161
253
|
|
|
162
254
|
{!collapsed && (
|
|
163
255
|
<Separator className="group relative w-1.5 shrink-0 outline-none">
|
|
164
|
-
<span className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-
|
|
256
|
+
<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" />
|
|
165
257
|
</Separator>
|
|
166
258
|
)}
|
|
167
259
|
|
|
@@ -186,20 +278,21 @@ export function WorkspaceDock({
|
|
|
186
278
|
{/* Hidden behind the overlay while maximized (avoids double-mounting
|
|
187
279
|
the surfaces). */}
|
|
188
280
|
{!collapsed && !maximized && (
|
|
189
|
-
<div className="flex h-full min-h-0 min-w-0 flex-col border-l border-
|
|
281
|
+
<div className="flex h-full min-h-0 min-w-0 flex-col border-l border-og-border bg-og-bg">
|
|
190
282
|
{dockChrome}
|
|
191
283
|
</div>
|
|
192
284
|
)}
|
|
193
285
|
</Panel>
|
|
194
286
|
</Group>
|
|
195
287
|
|
|
196
|
-
{/* Collapsed rail:
|
|
197
|
-
|
|
288
|
+
{/* Collapsed rail: the standalone fallback re-open affordance. Hidden
|
|
289
|
+
when the host controls collapse — its own toggle is the one way in. */}
|
|
290
|
+
{collapsed && !maximized && !hostControlled && (
|
|
198
291
|
<button
|
|
199
292
|
type="button"
|
|
200
293
|
onClick={expand}
|
|
201
294
|
title="Open workspace"
|
|
202
|
-
className="absolute inset-y-0 right-0 flex w-6 shrink-0 items-center justify-center border-l border-
|
|
295
|
+
className="absolute inset-y-0 right-0 flex w-6 shrink-0 items-center justify-center border-l border-og-border bg-og-surface-1 text-og-fg-subtle hover:text-og-fg"
|
|
203
296
|
>
|
|
204
297
|
<ChevronsLeftRightIcon className="size-3.5" />
|
|
205
298
|
</button>
|
|
@@ -207,7 +300,7 @@ export function WorkspaceDock({
|
|
|
207
300
|
|
|
208
301
|
{/* Maximize overlay: full-workspace surface above everything. */}
|
|
209
302
|
{maximized && (
|
|
210
|
-
<div className="fixed inset-0 z-40 flex flex-col bg-
|
|
303
|
+
<div className="fixed inset-0 z-40 flex flex-col bg-og-bg">
|
|
211
304
|
{dockChrome}
|
|
212
305
|
</div>
|
|
213
306
|
)}
|
|
@@ -215,26 +308,54 @@ export function WorkspaceDock({
|
|
|
215
308
|
);
|
|
216
309
|
}
|
|
217
310
|
|
|
311
|
+
/** A dock-chrome control button — compact on fine pointers, ≥40px on coarse. */
|
|
312
|
+
function ChromeButton({
|
|
313
|
+
onClick,
|
|
314
|
+
title,
|
|
315
|
+
label,
|
|
316
|
+
children,
|
|
317
|
+
}: {
|
|
318
|
+
onClick: () => void;
|
|
319
|
+
title: string;
|
|
320
|
+
label: string;
|
|
321
|
+
children: ReactNode;
|
|
322
|
+
}) {
|
|
323
|
+
return (
|
|
324
|
+
<button
|
|
325
|
+
type="button"
|
|
326
|
+
onClick={onClick}
|
|
327
|
+
title={title}
|
|
328
|
+
aria-label={label}
|
|
329
|
+
className="inline-flex items-center justify-center rounded-og-sm p-1 transition-colors hover:bg-og-surface-2 hover:text-og-fg pointer-coarse:size-10"
|
|
330
|
+
>
|
|
331
|
+
{children}
|
|
332
|
+
</button>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
218
336
|
function DockChrome({
|
|
219
337
|
tabs,
|
|
220
338
|
current,
|
|
221
339
|
onTab,
|
|
222
|
-
|
|
223
|
-
onToggleMaximize,
|
|
224
|
-
onCollapse,
|
|
340
|
+
controls,
|
|
225
341
|
}: {
|
|
226
342
|
tabs: WorkspaceTab[];
|
|
227
343
|
current: string;
|
|
228
344
|
onTab: (id: string) => void;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
onCollapse: () => void;
|
|
345
|
+
/** Right-aligned chrome controls (maximize / collapse, or the overlay close). */
|
|
346
|
+
controls: ReactNode;
|
|
232
347
|
}) {
|
|
233
348
|
const active = tabs.find((t) => t.id === current) ?? tabs[0];
|
|
234
349
|
return (
|
|
235
350
|
<>
|
|
236
|
-
<div className="flex shrink-0 items-center
|
|
237
|
-
|
|
351
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-og-border px-1.5 py-1">
|
|
352
|
+
{/* The tab list scrolls horizontally when it can't fit — it must never
|
|
353
|
+
grow into or overlap the chrome controls (they stay shrink-0). The
|
|
354
|
+
scrollbar is hidden to keep the strip calm. */}
|
|
355
|
+
<div
|
|
356
|
+
className="flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
357
|
+
role="tablist"
|
|
358
|
+
>
|
|
238
359
|
{tabs.map((tab) => (
|
|
239
360
|
<button
|
|
240
361
|
key={tab.id}
|
|
@@ -243,35 +364,18 @@ function DockChrome({
|
|
|
243
364
|
aria-selected={tab.id === current}
|
|
244
365
|
onClick={() => onTab(tab.id)}
|
|
245
366
|
className={cn(
|
|
246
|
-
"flex items-center gap-1 rounded-
|
|
367
|
+
"flex shrink-0 items-center gap-1 rounded-og-sm px-2 py-1 text-og-xs font-medium transition-colors pointer-coarse:min-h-10",
|
|
247
368
|
tab.id === current
|
|
248
|
-
? "bg-
|
|
249
|
-
: "text-
|
|
369
|
+
? "bg-og-accent-soft text-og-fg"
|
|
370
|
+
: "text-og-fg-subtle hover:text-og-fg",
|
|
250
371
|
)}
|
|
251
372
|
>
|
|
252
|
-
<span
|
|
373
|
+
<span>{tab.label}</span>
|
|
253
374
|
{tab.badge}
|
|
254
375
|
</button>
|
|
255
376
|
))}
|
|
256
377
|
</div>
|
|
257
|
-
<div className="flex shrink-0 items-center gap-0.5 text-
|
|
258
|
-
<button
|
|
259
|
-
type="button"
|
|
260
|
-
onClick={onToggleMaximize}
|
|
261
|
-
title={maximized ? "Restore (Esc)" : "Maximize"}
|
|
262
|
-
className="rounded-[var(--og-radius-sm,4px)] p-1 hover:bg-[color:var(--og-color-surface-2,var(--color-surface-2,#222))] hover:text-[color:var(--og-color-fg,var(--color-fg,#e6e6e6))]"
|
|
263
|
-
>
|
|
264
|
-
{maximized ? <Minimize2Icon className="size-3.5" /> : <Maximize2Icon className="size-3.5" />}
|
|
265
|
-
</button>
|
|
266
|
-
<button
|
|
267
|
-
type="button"
|
|
268
|
-
onClick={onCollapse}
|
|
269
|
-
title={maximized ? "Restore" : "Collapse"}
|
|
270
|
-
className="rounded-[var(--og-radius-sm,4px)] p-1 hover:bg-[color:var(--og-color-surface-2,var(--color-surface-2,#222))] hover:text-[color:var(--og-color-fg,var(--color-fg,#e6e6e6))]"
|
|
271
|
-
>
|
|
272
|
-
<PanelRightCloseIcon className="size-3.5" />
|
|
273
|
-
</button>
|
|
274
|
-
</div>
|
|
378
|
+
<div className="flex shrink-0 items-center gap-0.5 text-og-fg-subtle">{controls}</div>
|
|
275
379
|
</div>
|
|
276
380
|
<div className="min-h-0 min-w-0 flex-1 overflow-hidden" role="tabpanel">
|
|
277
381
|
{active?.content}
|