@opengeni/react 0.57.0 → 2.2.0-canary.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 +36 -11
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
- package/dist/chunk-5RZN3DD5.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
- package/dist/chunk-UOLJWT5Y.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
- package/dist/chunk-XSA2QAKP.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +10 -3
- package/dist/realtime.js +138 -29
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +48 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +91 -55
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +162 -17
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +200 -37
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +476 -23
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +75 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
Separator,
|
|
22
22
|
useDefaultLayout,
|
|
23
23
|
usePanelRef,
|
|
24
|
+
type Layout,
|
|
25
|
+
type LayoutChangedMeta,
|
|
24
26
|
type LayoutStorage,
|
|
25
27
|
} from "react-resizable-panels";
|
|
26
28
|
import { cn } from "../lib/cn";
|
|
@@ -28,6 +30,10 @@ import { cn } from "../lib/cn";
|
|
|
28
30
|
export type WorkspaceTab = {
|
|
29
31
|
id: string;
|
|
30
32
|
label: ReactNode;
|
|
33
|
+
/** Optional icon used by the compact activity rail. */
|
|
34
|
+
icon?: ReactNode | undefined;
|
|
35
|
+
/** Accessible name when `label` is not plain text. */
|
|
36
|
+
ariaLabel?: string | undefined;
|
|
31
37
|
/** Rendered as the active surface. */
|
|
32
38
|
content: ReactNode;
|
|
33
39
|
/** A small badge after the label (e.g. dirty count, live pill). */
|
|
@@ -44,9 +50,9 @@ export type WorkspaceDockProps = {
|
|
|
44
50
|
/** Controlled collapsed state for hosts that expose their own dock toggle. */
|
|
45
51
|
collapsed?: boolean | undefined;
|
|
46
52
|
onCollapsedChange?: ((collapsed: boolean) => void) | undefined;
|
|
47
|
-
/** Keep the
|
|
53
|
+
/** Keep the panel-local Hide workspace control when the host controls collapsed state. */
|
|
48
54
|
showCollapseControl?: boolean | undefined;
|
|
49
|
-
/** A status accessory pinned to the right of the
|
|
55
|
+
/** A status accessory pinned to the right of the workspace header, left of the
|
|
50
56
|
* maximize/collapse controls (e.g. the machine-state chip). Renders in both
|
|
51
57
|
* the docked header and the full-screen overlay header. */
|
|
52
58
|
headerAccessory?: ReactNode | undefined;
|
|
@@ -131,7 +137,7 @@ export function WorkspaceDock({
|
|
|
131
137
|
onActiveTabChange,
|
|
132
138
|
collapsed: collapsedProp,
|
|
133
139
|
onCollapsedChange,
|
|
134
|
-
showCollapseControl =
|
|
140
|
+
showCollapseControl = true,
|
|
135
141
|
headerAccessory,
|
|
136
142
|
mobileLeadingControl,
|
|
137
143
|
autoSaveId = "og.session.dock",
|
|
@@ -150,11 +156,9 @@ export function WorkspaceDock({
|
|
|
150
156
|
const [maximized, setMaximized] = useState(false);
|
|
151
157
|
const [internalTab, setInternalTab] = useState(tabs[0]?.id ?? "");
|
|
152
158
|
const collapsed = collapsedProp ?? internalCollapsed;
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
// bug). Standalone (uncontrolled) usage keeps the built-in collapse button
|
|
157
|
-
// and the thin re-open rail as its only affordances.
|
|
159
|
+
// The host header remains the stable reopen action. The open panel also owns
|
|
160
|
+
// a local Hide workspace action so a user never has to hunt elsewhere to
|
|
161
|
+
// dismiss it; standalone usage keeps the thin reopen rail as well.
|
|
158
162
|
const hostControlled = collapsedProp !== undefined;
|
|
159
163
|
const previousCollapsedRef = useRef(collapsed);
|
|
160
164
|
|
|
@@ -164,6 +168,25 @@ export function WorkspaceDock({
|
|
|
164
168
|
storage: getLayoutStorage(),
|
|
165
169
|
id: autoSaveId,
|
|
166
170
|
});
|
|
171
|
+
const expandedDefaultLayout =
|
|
172
|
+
defaultLayout && (defaultLayout.dock ?? 0) > 1 ? defaultLayout : undefined;
|
|
173
|
+
const lastExpandedSizeRef = useRef(expandedDefaultLayout?.dock ?? defaultSize);
|
|
174
|
+
const layoutIdentityRef = useRef(autoSaveId);
|
|
175
|
+
if (layoutIdentityRef.current !== autoSaveId) {
|
|
176
|
+
layoutIdentityRef.current = autoSaveId;
|
|
177
|
+
lastExpandedSizeRef.current = expandedDefaultLayout?.dock ?? defaultSize;
|
|
178
|
+
}
|
|
179
|
+
const persistExpandedLayout = useCallback(
|
|
180
|
+
(layout: Layout, meta: LayoutChangedMeta) => {
|
|
181
|
+
// Both supported panel-library versions report 100/0 when the host
|
|
182
|
+
// collapses the dock. Do not replace the last user-selected width with it.
|
|
183
|
+
const dockSize = layout.dock ?? 0;
|
|
184
|
+
if (dockSize <= 1) return;
|
|
185
|
+
lastExpandedSizeRef.current = dockSize;
|
|
186
|
+
onLayoutChanged(layout, meta);
|
|
187
|
+
},
|
|
188
|
+
[onLayoutChanged],
|
|
189
|
+
);
|
|
167
190
|
|
|
168
191
|
const requestedTab = activeTab ?? internalTab;
|
|
169
192
|
const tabIds = tabs.map((tab) => tab.id);
|
|
@@ -193,7 +216,7 @@ export function WorkspaceDock({
|
|
|
193
216
|
dockPanelRef.current?.collapse();
|
|
194
217
|
setMaximized(false);
|
|
195
218
|
} else {
|
|
196
|
-
dockPanelRef.current?.
|
|
219
|
+
dockPanelRef.current?.resize(`${lastExpandedSizeRef.current}%`);
|
|
197
220
|
}
|
|
198
221
|
}, [collapsedProp, dockPanelRef]);
|
|
199
222
|
|
|
@@ -230,7 +253,18 @@ export function WorkspaceDock({
|
|
|
230
253
|
return;
|
|
231
254
|
}
|
|
232
255
|
const target = returnFocusRef.current;
|
|
233
|
-
if (target?.isConnected)
|
|
256
|
+
if (target?.isConnected) {
|
|
257
|
+
target.focus();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// A controlled desktop dock can be restored open on the initial render,
|
|
261
|
+
// so there may be no opener to remember. Never leave focus parked on the
|
|
262
|
+
// now-hidden workspace chrome; fall back to the host's primary surface.
|
|
263
|
+
rootRef.current
|
|
264
|
+
?.querySelector<HTMLElement>(
|
|
265
|
+
'[data-workspace-primary] button:not([disabled]), [data-workspace-primary] a[href], [data-workspace-primary] input:not([disabled]), [data-workspace-primary] textarea:not([disabled]), [data-workspace-primary] select:not([disabled]), [data-workspace-primary] [tabindex]:not([tabindex="-1"])',
|
|
266
|
+
)
|
|
267
|
+
?.focus();
|
|
234
268
|
});
|
|
235
269
|
return () => cancelAnimationFrame(frame);
|
|
236
270
|
}, [collapsed, hostControlled, narrow]);
|
|
@@ -362,7 +396,7 @@ export function WorkspaceDock({
|
|
|
362
396
|
leading={mobileLeadingControl}
|
|
363
397
|
accessory={headerAccessory}
|
|
364
398
|
controls={
|
|
365
|
-
<ChromeButton onClick={collapse} title="
|
|
399
|
+
<ChromeButton onClick={collapse} title="Hide workspace" label="Hide workspace">
|
|
366
400
|
<XIcon className="size-4" />
|
|
367
401
|
</ChromeButton>
|
|
368
402
|
}
|
|
@@ -405,7 +439,7 @@ export function WorkspaceDock({
|
|
|
405
439
|
)}
|
|
406
440
|
</ChromeButton>
|
|
407
441
|
{hostControlled && !showCollapseControl ? null : (
|
|
408
|
-
<ChromeButton onClick={collapse} title="
|
|
442
|
+
<ChromeButton onClick={collapse} title="Hide workspace" label="Hide workspace">
|
|
409
443
|
<PanelRightCloseIcon className="size-3.5" />
|
|
410
444
|
</ChromeButton>
|
|
411
445
|
)}
|
|
@@ -419,8 +453,8 @@ export function WorkspaceDock({
|
|
|
419
453
|
<Group
|
|
420
454
|
orientation="horizontal"
|
|
421
455
|
className="min-h-0 flex-1"
|
|
422
|
-
{...(
|
|
423
|
-
onLayoutChanged={
|
|
456
|
+
{...(expandedDefaultLayout ? { defaultLayout: expandedDefaultLayout } : {})}
|
|
457
|
+
onLayoutChanged={persistExpandedLayout}
|
|
424
458
|
>
|
|
425
459
|
<Panel id="primary" minSize="30%" className="min-h-0 min-w-0">
|
|
426
460
|
<div
|
|
@@ -477,7 +511,7 @@ export function WorkspaceDock({
|
|
|
477
511
|
</Group>
|
|
478
512
|
|
|
479
513
|
{/* Collapsed rail: the standalone fallback re-open affordance. Hidden
|
|
480
|
-
when the host controls collapse — its
|
|
514
|
+
when the host controls collapse — its header remains the stable way in. */}
|
|
481
515
|
{collapsed && !maximized && !hostControlled && (
|
|
482
516
|
<button
|
|
483
517
|
ref={reopenRef}
|
|
@@ -541,24 +575,19 @@ function DockChrome({
|
|
|
541
575
|
const active = tabs.find((t) => t.id === current) ?? tabs[0];
|
|
542
576
|
const tabsetId = useId();
|
|
543
577
|
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
|
544
|
-
const tabListRef = useRef<HTMLDivElement | null>(null);
|
|
545
578
|
const [visitedTabs, setVisitedTabs] = useState<Set<string>>(() => new Set());
|
|
546
|
-
const [tabOverflow, setTabOverflow] = useState({ start: false, end: false });
|
|
547
579
|
const activeId = active?.id ?? "";
|
|
548
|
-
const activeTabIndex = tabs.findIndex((tab) => tab.id === activeId);
|
|
549
580
|
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
// unclickable ARIA tabs. At narrow *dock* widths, give status/controls their
|
|
554
|
-
// own row and the horizontally scrollable tabs the complete second row.
|
|
581
|
+
// Dock width, not viewport width, chooses rail density. A narrow split keeps
|
|
582
|
+
// the icon-first activity rail usable without stealing the panel's content
|
|
583
|
+
// width; overlays and maximized views retain readable labels.
|
|
555
584
|
useDockLayoutEffect(() => {
|
|
556
585
|
const node = chromeRef.current;
|
|
557
586
|
if (!node) return;
|
|
558
587
|
const update = () => {
|
|
559
588
|
const desktop =
|
|
560
589
|
typeof window.matchMedia !== "function" || window.matchMedia("(min-width: 1024px)").matches;
|
|
561
|
-
const next = desktop && node.getBoundingClientRect().width <
|
|
590
|
+
const next = desktop && node.getBoundingClientRect().width < 440;
|
|
562
591
|
setCompact((previous) => (previous === next ? previous : next));
|
|
563
592
|
};
|
|
564
593
|
update();
|
|
@@ -571,11 +600,6 @@ function DockChrome({
|
|
|
571
600
|
return () => observer.disconnect();
|
|
572
601
|
}, []);
|
|
573
602
|
|
|
574
|
-
const keepActiveTabVisible = useCallback(() => {
|
|
575
|
-
const selected = activeTabIndex >= 0 ? tabRefs.current[activeTabIndex] : null;
|
|
576
|
-
selected?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
577
|
-
}, [activeTabIndex]);
|
|
578
|
-
|
|
579
603
|
useEffect(() => {
|
|
580
604
|
if (!activeId) return;
|
|
581
605
|
setVisitedTabs((previous) => {
|
|
@@ -586,55 +610,6 @@ function DockChrome({
|
|
|
586
610
|
});
|
|
587
611
|
}, [activeId]);
|
|
588
612
|
|
|
589
|
-
// A controlled/initial tab can become active without receiving focus (for
|
|
590
|
-
// example, a host routing a guarded diff into Files). Native scrolling only
|
|
591
|
-
// follows focus, so keep the selected tab inside the horizontally scrollable
|
|
592
|
-
// strip even when state — rather than a click or arrow key — selected it.
|
|
593
|
-
useDockLayoutEffect(() => {
|
|
594
|
-
keepActiveTabVisible();
|
|
595
|
-
}, [activeId, keepActiveTabVisible]);
|
|
596
|
-
|
|
597
|
-
// Hidden scrollbars keep the chrome quiet, but without an edge cue a clipped
|
|
598
|
-
// host label reads like corrupted copy. Fade only the edge(s) with more tabs,
|
|
599
|
-
// and recompute after scrolling, resizing, or late font metrics.
|
|
600
|
-
useDockLayoutEffect(() => {
|
|
601
|
-
const list = tabListRef.current;
|
|
602
|
-
if (!list) return;
|
|
603
|
-
const update = () => {
|
|
604
|
-
const max = Math.max(0, list.scrollWidth - list.clientWidth);
|
|
605
|
-
const next = { start: list.scrollLeft > 1, end: list.scrollLeft < max - 1 };
|
|
606
|
-
setTabOverflow((previous) =>
|
|
607
|
-
previous.start === next.start && previous.end === next.end ? previous : next,
|
|
608
|
-
);
|
|
609
|
-
};
|
|
610
|
-
update();
|
|
611
|
-
list.addEventListener("scroll", update, { passive: true });
|
|
612
|
-
const observer =
|
|
613
|
-
typeof ResizeObserver === "undefined"
|
|
614
|
-
? null
|
|
615
|
-
: new ResizeObserver(() => {
|
|
616
|
-
keepActiveTabVisible();
|
|
617
|
-
update();
|
|
618
|
-
});
|
|
619
|
-
observer?.observe(list);
|
|
620
|
-
for (const tab of tabRefs.current) {
|
|
621
|
-
if (tab) observer?.observe(tab);
|
|
622
|
-
}
|
|
623
|
-
return () => {
|
|
624
|
-
list.removeEventListener("scroll", update);
|
|
625
|
-
observer?.disconnect();
|
|
626
|
-
};
|
|
627
|
-
}, [activeId, keepActiveTabVisible, tabs.length]);
|
|
628
|
-
|
|
629
|
-
const tabMask =
|
|
630
|
-
tabOverflow.start && tabOverflow.end
|
|
631
|
-
? "linear-gradient(to right, transparent 0, black 14px, black calc(100% - 14px), transparent 100%)"
|
|
632
|
-
: tabOverflow.start
|
|
633
|
-
? "linear-gradient(to right, transparent 0, black 14px, black 100%)"
|
|
634
|
-
: tabOverflow.end
|
|
635
|
-
? "linear-gradient(to right, black 0, black calc(100% - 14px), transparent 100%)"
|
|
636
|
-
: undefined;
|
|
637
|
-
|
|
638
613
|
const activateTab = (index: number) => {
|
|
639
614
|
const tab = tabs[index];
|
|
640
615
|
if (!tab) {
|
|
@@ -647,8 +622,8 @@ function DockChrome({
|
|
|
647
622
|
const onTabKeyDown = (event: ReactKeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
648
623
|
if (tabs.length === 0) return;
|
|
649
624
|
let nextIndex: number | null = null;
|
|
650
|
-
if (event.key === "
|
|
651
|
-
else if (event.key === "
|
|
625
|
+
if (event.key === "ArrowDown") nextIndex = (index + 1) % tabs.length;
|
|
626
|
+
else if (event.key === "ArrowUp") nextIndex = (index - 1 + tabs.length) % tabs.length;
|
|
652
627
|
else if (event.key === "Home") nextIndex = 0;
|
|
653
628
|
else if (event.key === "End") nextIndex = tabs.length - 1;
|
|
654
629
|
if (nextIndex === null) return;
|
|
@@ -657,115 +632,106 @@ function DockChrome({
|
|
|
657
632
|
};
|
|
658
633
|
|
|
659
634
|
return (
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
)}
|
|
669
|
-
style={compact ? { gridTemplateColumns: "minmax(0, 1fr) auto" } : undefined}
|
|
670
|
-
>
|
|
671
|
-
<div
|
|
672
|
-
className={cn(
|
|
673
|
-
"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",
|
|
674
|
-
compact && "hidden",
|
|
675
|
-
)}
|
|
676
|
-
>
|
|
635
|
+
<div
|
|
636
|
+
ref={chromeRef}
|
|
637
|
+
data-dock-chrome
|
|
638
|
+
data-compact={compact ? "true" : undefined}
|
|
639
|
+
className="flex h-full min-h-0 min-w-0 flex-col"
|
|
640
|
+
>
|
|
641
|
+
<div className="flex min-h-10 shrink-0 items-center gap-2 border-b border-og-border px-2 max-[1023px]:min-h-12">
|
|
642
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
677
643
|
{leading ?? (
|
|
678
|
-
<span className="hidden truncate
|
|
644
|
+
<span className="hidden truncate text-og-sm font-semibold text-og-fg max-[1023px]:inline">
|
|
679
645
|
Workspace
|
|
680
646
|
</span>
|
|
681
647
|
)}
|
|
682
648
|
</div>
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
canonical workspace navigation out of view. Tablets have enough
|
|
688
|
-
width to retain the denser single-row chrome, with this same strip
|
|
689
|
-
absorbing any unusually long host-injected labels. */}
|
|
649
|
+
<div className="min-w-0 shrink text-og-fg-muted">{accessory}</div>
|
|
650
|
+
<div className="flex shrink-0 items-center gap-0.5 text-og-fg-subtle">{controls}</div>
|
|
651
|
+
</div>
|
|
652
|
+
<div className="flex min-h-0 min-w-0 flex-1">
|
|
690
653
|
<div
|
|
691
|
-
ref={tabListRef}
|
|
692
654
|
className={cn(
|
|
693
|
-
"flex
|
|
694
|
-
compact
|
|
655
|
+
"flex shrink-0 flex-col gap-1 overflow-x-hidden overflow-y-auto border-r border-og-border bg-og-surface-1/45 p-1.5",
|
|
656
|
+
compact ? "w-12" : "w-32 max-[1023px]:w-36",
|
|
695
657
|
)}
|
|
696
658
|
role="tablist"
|
|
697
|
-
aria-
|
|
698
|
-
|
|
699
|
-
maskImage: tabMask,
|
|
700
|
-
WebkitMaskImage: tabMask,
|
|
701
|
-
scrollPaddingInline: "16px",
|
|
702
|
-
}}
|
|
703
|
-
>
|
|
704
|
-
{tabs.map((tab, index) => (
|
|
705
|
-
<button
|
|
706
|
-
key={tab.id}
|
|
707
|
-
ref={(node) => {
|
|
708
|
-
tabRefs.current[index] = node;
|
|
709
|
-
}}
|
|
710
|
-
id={`${tabsetId}-tab-${index}`}
|
|
711
|
-
type="button"
|
|
712
|
-
role="tab"
|
|
713
|
-
aria-selected={tab.id === current}
|
|
714
|
-
aria-controls={`${tabsetId}-panel-${index}`}
|
|
715
|
-
tabIndex={tab.id === current ? 0 : -1}
|
|
716
|
-
onClick={() => onTab(tab.id)}
|
|
717
|
-
onKeyDown={(event) => onTabKeyDown(event, index)}
|
|
718
|
-
className={cn(
|
|
719
|
-
"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-hidden 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",
|
|
720
|
-
tab.id === current
|
|
721
|
-
? "bg-og-accent-soft text-og-fg"
|
|
722
|
-
: "text-og-fg-subtle hover:text-og-fg",
|
|
723
|
-
)}
|
|
724
|
-
>
|
|
725
|
-
<span data-contrast-audited>{tab.label}</span>
|
|
726
|
-
{tab.badge}
|
|
727
|
-
</button>
|
|
728
|
-
))}
|
|
729
|
-
</div>
|
|
730
|
-
<div
|
|
731
|
-
className={cn(
|
|
732
|
-
"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",
|
|
733
|
-
compact && "col-start-1 row-start-1 justify-self-start",
|
|
734
|
-
)}
|
|
735
|
-
>
|
|
736
|
-
{accessory}
|
|
737
|
-
</div>
|
|
738
|
-
<div
|
|
739
|
-
className={cn(
|
|
740
|
-
"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",
|
|
741
|
-
compact && "col-start-2 row-start-1",
|
|
742
|
-
)}
|
|
659
|
+
aria-label="Workspace panels"
|
|
660
|
+
aria-orientation="vertical"
|
|
743
661
|
>
|
|
744
|
-
{
|
|
745
|
-
|
|
746
|
-
</div>
|
|
747
|
-
<div className="min-h-0 min-w-0 flex-1 overflow-hidden">
|
|
748
|
-
{tabs.length > 0 ? (
|
|
749
|
-
tabs.map((tab, index) => {
|
|
750
|
-
const selected = tab.id === activeId;
|
|
751
|
-
const shouldMount = selected || visitedTabs.has(tab.id);
|
|
662
|
+
{tabs.map((tab, index) => {
|
|
663
|
+
const tabName = tab.ariaLabel ?? (typeof tab.label === "string" ? tab.label : tab.id);
|
|
752
664
|
return (
|
|
753
|
-
<
|
|
665
|
+
<button
|
|
754
666
|
key={tab.id}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
667
|
+
ref={(node) => {
|
|
668
|
+
tabRefs.current[index] = node;
|
|
669
|
+
}}
|
|
670
|
+
id={`${tabsetId}-tab-${index}`}
|
|
671
|
+
type="button"
|
|
672
|
+
role="tab"
|
|
673
|
+
aria-label={compact && tab.icon ? tabName : undefined}
|
|
674
|
+
title={compact && tab.icon ? tabName : undefined}
|
|
675
|
+
aria-selected={tab.id === current}
|
|
676
|
+
aria-controls={`${tabsetId}-panel-${index}`}
|
|
677
|
+
tabIndex={tab.id === current ? 0 : -1}
|
|
678
|
+
onClick={() => onTab(tab.id)}
|
|
679
|
+
onKeyDown={(event) => onTabKeyDown(event, index)}
|
|
680
|
+
className={cn(
|
|
681
|
+
"group relative flex min-h-9 w-full items-center gap-2 rounded-og-md px-2 text-left text-og-xs font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 pointer-coarse:min-h-11",
|
|
682
|
+
compact && "justify-center px-1",
|
|
683
|
+
tab.id === current
|
|
684
|
+
? "bg-og-accent-soft text-og-fg shadow-[inset_2px_0_0_var(--og-color-accent)]"
|
|
685
|
+
: "text-og-fg-subtle hover:bg-og-surface-2 hover:text-og-fg",
|
|
686
|
+
)}
|
|
760
687
|
>
|
|
761
|
-
{
|
|
762
|
-
|
|
688
|
+
{tab.icon ? (
|
|
689
|
+
<span
|
|
690
|
+
className="flex size-4 shrink-0 items-center justify-center [&>svg]:size-4"
|
|
691
|
+
aria-hidden
|
|
692
|
+
>
|
|
693
|
+
{tab.icon}
|
|
694
|
+
</span>
|
|
695
|
+
) : null}
|
|
696
|
+
{!compact || !tab.icon ? (
|
|
697
|
+
<span data-contrast-audited className="min-w-0 flex-1 truncate">
|
|
698
|
+
{tab.label}
|
|
699
|
+
</span>
|
|
700
|
+
) : null}
|
|
701
|
+
{tab.badge ? (
|
|
702
|
+
<span
|
|
703
|
+
className={cn("shrink-0", compact && "absolute -right-0.5 -top-0.5 scale-90")}
|
|
704
|
+
>
|
|
705
|
+
{tab.badge}
|
|
706
|
+
</span>
|
|
707
|
+
) : null}
|
|
708
|
+
</button>
|
|
763
709
|
);
|
|
764
|
-
})
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
710
|
+
})}
|
|
711
|
+
</div>
|
|
712
|
+
<div className="min-h-0 min-w-0 flex-1 overflow-hidden">
|
|
713
|
+
{tabs.length > 0 ? (
|
|
714
|
+
tabs.map((tab, index) => {
|
|
715
|
+
const selected = tab.id === activeId;
|
|
716
|
+
const shouldMount = selected || visitedTabs.has(tab.id);
|
|
717
|
+
return (
|
|
718
|
+
<div
|
|
719
|
+
key={tab.id}
|
|
720
|
+
id={`${tabsetId}-panel-${index}`}
|
|
721
|
+
aria-labelledby={`${tabsetId}-tab-${index}`}
|
|
722
|
+
className="h-full min-h-0 min-w-0 overflow-hidden"
|
|
723
|
+
hidden={!selected}
|
|
724
|
+
role="tabpanel"
|
|
725
|
+
>
|
|
726
|
+
{shouldMount ? tab.content : null}
|
|
727
|
+
</div>
|
|
728
|
+
);
|
|
729
|
+
})
|
|
730
|
+
) : (
|
|
731
|
+
<div className="h-full min-h-0 min-w-0" aria-label="Workspace" role="tabpanel" />
|
|
732
|
+
)}
|
|
733
|
+
</div>
|
|
768
734
|
</div>
|
|
769
|
-
|
|
735
|
+
</div>
|
|
770
736
|
);
|
|
771
737
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Channel } from "@opengeni/sdk";
|
|
2
|
+
import type { SessionClientLike } from "../client";
|
|
3
|
+
|
|
4
|
+
type PendingChannel = { channel: Channel; committedAfterLoad: number };
|
|
5
|
+
|
|
6
|
+
export type ChannelCacheSnapshot = Readonly<{
|
|
7
|
+
channels: readonly Channel[];
|
|
8
|
+
ready: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
export type ChannelCacheStore = Readonly<{
|
|
12
|
+
getSnapshot: () => ChannelCacheSnapshot;
|
|
13
|
+
subscribe: (listener: () => void) => () => void;
|
|
14
|
+
beginLoad: () => number;
|
|
15
|
+
acceptLoad: (ticket: number, channels: readonly Channel[]) => void;
|
|
16
|
+
publishCreated: (channel: Channel) => void;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
const EMPTY_CHANNELS: readonly Channel[] = [];
|
|
20
|
+
const EMPTY_SNAPSHOT: ChannelCacheSnapshot = {
|
|
21
|
+
channels: EMPTY_CHANNELS,
|
|
22
|
+
ready: false,
|
|
23
|
+
};
|
|
24
|
+
const channelCaches = new WeakMap<object, Map<string, ChannelCacheStore>>();
|
|
25
|
+
|
|
26
|
+
function compareChannels(left: Channel, right: Channel): number {
|
|
27
|
+
return (
|
|
28
|
+
Number(right.pinned) - Number(left.pinned) ||
|
|
29
|
+
left.sortOrder - right.sortOrder ||
|
|
30
|
+
left.id.localeCompare(right.id)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function orderedChannels(channels: readonly Channel[]): Channel[] {
|
|
35
|
+
return [...channels].sort(compareChannels);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function upsertChannel(channels: readonly Channel[], channel: Channel): Channel[] {
|
|
39
|
+
const next = channels.filter((candidate) => candidate.id !== channel.id);
|
|
40
|
+
next.push(channel);
|
|
41
|
+
return orderedChannels(next);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One client/workspace cache shared by every useChannels consumer.
|
|
46
|
+
*
|
|
47
|
+
* List reads are ticketed so an older request cannot erase a newer result.
|
|
48
|
+
* A successful create remains layered over the latest server snapshot until a
|
|
49
|
+
* list request that started after that success reconciles it. Requests already
|
|
50
|
+
* in flight cannot make the project disappear between create and session start.
|
|
51
|
+
*/
|
|
52
|
+
export function createChannelCacheStore(): ChannelCacheStore {
|
|
53
|
+
let base: readonly Channel[] | null = null;
|
|
54
|
+
let snapshot = EMPTY_SNAPSHOT;
|
|
55
|
+
let nextLoadTicket = 0;
|
|
56
|
+
let acceptedLoadTicket = 0;
|
|
57
|
+
const pending = new Map<string, PendingChannel>();
|
|
58
|
+
const listeners = new Set<() => void>();
|
|
59
|
+
|
|
60
|
+
const publish = () => {
|
|
61
|
+
let channels = base ?? EMPTY_CHANNELS;
|
|
62
|
+
for (const { channel } of pending.values()) {
|
|
63
|
+
channels = upsertChannel(channels, channel);
|
|
64
|
+
}
|
|
65
|
+
snapshot = { channels, ready: base !== null || pending.size > 0 };
|
|
66
|
+
for (const listener of listeners) listener();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
getSnapshot: () => snapshot,
|
|
71
|
+
subscribe: (listener) => {
|
|
72
|
+
listeners.add(listener);
|
|
73
|
+
return () => listeners.delete(listener);
|
|
74
|
+
},
|
|
75
|
+
beginLoad: () => ++nextLoadTicket,
|
|
76
|
+
acceptLoad: (ticket, channels) => {
|
|
77
|
+
if (ticket < acceptedLoadTicket) return;
|
|
78
|
+
acceptedLoadTicket = ticket;
|
|
79
|
+
base = orderedChannels(channels);
|
|
80
|
+
for (const [id, created] of pending) {
|
|
81
|
+
if (ticket > created.committedAfterLoad) pending.delete(id);
|
|
82
|
+
}
|
|
83
|
+
publish();
|
|
84
|
+
},
|
|
85
|
+
publishCreated: (channel) => {
|
|
86
|
+
pending.set(channel.id, { channel, committedAfterLoad: nextLoadTicket });
|
|
87
|
+
publish();
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function channelCacheFor(client: SessionClientLike, workspaceId: string): ChannelCacheStore {
|
|
93
|
+
let byWorkspace = channelCaches.get(client);
|
|
94
|
+
if (!byWorkspace) {
|
|
95
|
+
byWorkspace = new Map();
|
|
96
|
+
channelCaches.set(client, byWorkspace);
|
|
97
|
+
}
|
|
98
|
+
let cache = byWorkspace.get(workspaceId);
|
|
99
|
+
if (!cache) {
|
|
100
|
+
cache = createChannelCacheStore();
|
|
101
|
+
byWorkspace.set(workspaceId, cache);
|
|
102
|
+
}
|
|
103
|
+
return cache;
|
|
104
|
+
}
|