@protolabsai/ui 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin-kit.css +2763 -0
- package/package.json +8 -3
- package/src/AppShell.full.stories.tsx +18 -3
- package/src/ThemePanel.stories.tsx +4 -1
- package/src/app-shell.tsx +66 -21
- package/src/plugin-kit.js +70 -0
- package/src/styles/app-shell.css +7 -0
- package/src/styles/theming.css +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protolabsai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
"./menu": "./src/menu.tsx",
|
|
23
23
|
"./app-shell": "./src/app-shell.tsx",
|
|
24
24
|
"./theming": "./src/theming.tsx",
|
|
25
|
-
"./styles.css": "./src/styles.css"
|
|
25
|
+
"./styles.css": "./src/styles.css",
|
|
26
|
+
"./plugin-kit.css": "./dist/plugin-kit.css",
|
|
27
|
+
"./plugin-kit.js": "./src/plugin-kit.js"
|
|
26
28
|
},
|
|
27
29
|
"files": [
|
|
28
|
-
"src"
|
|
30
|
+
"src",
|
|
31
|
+
"dist"
|
|
29
32
|
],
|
|
30
33
|
"dependencies": {
|
|
31
34
|
"@dnd-kit/core": "^6.3.1",
|
|
@@ -54,6 +57,8 @@
|
|
|
54
57
|
"vite": "^6.0.0"
|
|
55
58
|
},
|
|
56
59
|
"scripts": {
|
|
60
|
+
"build": "node scripts/build-plugin-kit.mjs",
|
|
61
|
+
"build:plugin-kit": "node scripts/build-plugin-kit.mjs",
|
|
57
62
|
"storybook": "storybook dev -p 6006 --no-open",
|
|
58
63
|
"build-storybook": "storybook build -o storybook-static",
|
|
59
64
|
"typecheck": "tsc --noEmit"
|
|
@@ -46,6 +46,7 @@ export const Full: Story = {
|
|
|
46
46
|
const [activeRight, setActiveRight] = useState("inbox");
|
|
47
47
|
const [rightWidth, setRightWidth] = useState(360);
|
|
48
48
|
const [collapsed, setCollapsed] = useState(false);
|
|
49
|
+
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
|
49
50
|
const leftItems = order.left.map((id) => BY_ID.get(id)!);
|
|
50
51
|
const rightItems = order.right.map((id) => BY_ID.get(id)!);
|
|
51
52
|
const mobile: MobileItem[] = [...LEFT, ...RIGHT].map((i) => ({ id: i.id, label: i.label, icon: i.icon }));
|
|
@@ -58,8 +59,10 @@ export const Full: Story = {
|
|
|
58
59
|
activeLeft={activeLeft}
|
|
59
60
|
activeRight={activeRight}
|
|
60
61
|
onSelect={(side, id) => {
|
|
61
|
-
if (side === "left")
|
|
62
|
-
|
|
62
|
+
if (side === "left") {
|
|
63
|
+
setActiveLeft(id);
|
|
64
|
+
setLeftCollapsed(false);
|
|
65
|
+
} else {
|
|
63
66
|
setActiveRight(id);
|
|
64
67
|
setCollapsed(false);
|
|
65
68
|
}
|
|
@@ -68,9 +71,21 @@ export const Full: Story = {
|
|
|
68
71
|
onRightWidthChange={setRightWidth}
|
|
69
72
|
rightCollapsed={collapsed}
|
|
70
73
|
onCollapse={setCollapsed}
|
|
74
|
+
leftCollapsed={leftCollapsed}
|
|
75
|
+
onLeftCollapse={setLeftCollapsed}
|
|
71
76
|
leftContent={
|
|
72
77
|
<>
|
|
73
|
-
<PanelHeader
|
|
78
|
+
<PanelHeader
|
|
79
|
+
title={activeLeft}
|
|
80
|
+
actions={
|
|
81
|
+
<>
|
|
82
|
+
<Button size="sm">Action</Button>
|
|
83
|
+
<Button size="sm" variant="ghost" onClick={() => setLeftCollapsed(true)}>
|
|
84
|
+
Close
|
|
85
|
+
</Button>
|
|
86
|
+
</>
|
|
87
|
+
}
|
|
88
|
+
/>
|
|
74
89
|
{surfaceBody(activeLeft)}
|
|
75
90
|
</>
|
|
76
91
|
}
|
|
@@ -47,7 +47,10 @@ function Sample() {
|
|
|
47
47
|
export const LiveEditor: Story = {
|
|
48
48
|
render: () => (
|
|
49
49
|
<div style={{ display: "flex", gap: 24, alignItems: "flex-start", padding: 24, flexWrap: "wrap" }}>
|
|
50
|
-
|
|
50
|
+
{/* The panel fills its container — the host sizes it (here a 400×560 box). */}
|
|
51
|
+
<div style={{ width: 400, height: 560 }}>
|
|
52
|
+
<ThemePanel />
|
|
53
|
+
</div>
|
|
51
54
|
<div style={{ flex: "1 1 360px" }}>
|
|
52
55
|
<Sample />
|
|
53
56
|
</div>
|
package/src/app-shell.tsx
CHANGED
|
@@ -274,6 +274,14 @@ export type AppShellProps = {
|
|
|
274
274
|
onCollapse?: (collapsed: boolean) => void;
|
|
275
275
|
minRightWidth?: number;
|
|
276
276
|
maxRightWidth?: number;
|
|
277
|
+
/** Controlled left-column collapse — drives the left close button and the
|
|
278
|
+
* overdrag-left snap. Mirrors `rightCollapsed`/`onCollapse`. */
|
|
279
|
+
leftCollapsed?: boolean;
|
|
280
|
+
onLeftCollapse?: (collapsed: boolean) => void;
|
|
281
|
+
/** Min left-column width (px). The single divider holds the left column here
|
|
282
|
+
* on the way in; dragging `OVERDRAG` px further snaps the left column closed
|
|
283
|
+
* (it stays `leftCollapsed` until the host restores it). */
|
|
284
|
+
minLeftWidth?: number;
|
|
277
285
|
/** Mobile (<breakpoint) config. Omit to disable the mobile shell. */
|
|
278
286
|
mobileItems?: MobileItem[];
|
|
279
287
|
mobileActiveId?: string;
|
|
@@ -309,6 +317,9 @@ export function AppShell({
|
|
|
309
317
|
onCollapse,
|
|
310
318
|
minRightWidth = 280,
|
|
311
319
|
maxRightWidth = 720,
|
|
320
|
+
leftCollapsed = false,
|
|
321
|
+
onLeftCollapse,
|
|
322
|
+
minLeftWidth = 280,
|
|
312
323
|
mobileItems,
|
|
313
324
|
mobileActiveId,
|
|
314
325
|
onMobileSelect,
|
|
@@ -321,25 +332,54 @@ export function AppShell({
|
|
|
321
332
|
const isMobile = useIsMobile(mobileBreakpoint);
|
|
322
333
|
|
|
323
334
|
// ── resize handle ──
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
335
|
+
// One divider, zero-sum: the right column is width-controlled and the left
|
|
336
|
+
// flexes to fill, so dragging grows one column as it shrinks the other. Each
|
|
337
|
+
// side is held at its min on the way in; OVERDRAG px past that min snaps that
|
|
338
|
+
// side closed (collapse is controlled — the host restores it).
|
|
339
|
+
const OVERDRAG = 64;
|
|
340
|
+
const leftColRef = useRef<HTMLElement | null>(null);
|
|
341
|
+
const drag = useRef<{ startX: number; startW: number; startLeftW: number } | null>(null);
|
|
342
|
+
/** Resize `rightWidth` to `raw` px, holding both columns at their minimums. */
|
|
343
|
+
const commit = useCallback(
|
|
344
|
+
(raw: number, startLeftW: number, startW: number) => {
|
|
345
|
+
const upper = Math.max(minRightWidth, Math.min(maxRightWidth, startLeftW + startW - minLeftWidth));
|
|
346
|
+
onRightWidthChange(Math.min(upper, Math.max(minRightWidth, raw)));
|
|
347
|
+
},
|
|
348
|
+
[minRightWidth, maxRightWidth, minLeftWidth, onRightWidthChange],
|
|
328
349
|
);
|
|
329
350
|
const onPointerDown = useCallback(
|
|
330
351
|
(e: ReactPointerEvent<HTMLDivElement>) => {
|
|
331
352
|
e.preventDefault();
|
|
332
|
-
drag.current = { startX: e.clientX, startW: rightWidth };
|
|
353
|
+
drag.current = { startX: e.clientX, startW: rightWidth, startLeftW: leftColRef.current?.clientWidth ?? 0 };
|
|
333
354
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
334
355
|
},
|
|
335
356
|
[rightWidth],
|
|
336
357
|
);
|
|
337
358
|
const onPointerMove = useCallback(
|
|
338
359
|
(e: ReactPointerEvent<HTMLDivElement>) => {
|
|
339
|
-
|
|
340
|
-
|
|
360
|
+
const d = drag.current;
|
|
361
|
+
if (!d) return;
|
|
362
|
+
const raw = d.startW + (d.startX - e.clientX);
|
|
363
|
+
const predictedLeft = d.startLeftW - (raw - d.startW);
|
|
364
|
+
const endDrag = () => {
|
|
365
|
+
drag.current = null;
|
|
366
|
+
e.currentTarget.releasePointerCapture?.(e.pointerId);
|
|
367
|
+
};
|
|
368
|
+
// Overdrag toward the right rail → snap the right column closed.
|
|
369
|
+
if (onCollapse && raw < minRightWidth - OVERDRAG) {
|
|
370
|
+
endDrag();
|
|
371
|
+
onCollapse(true);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
// Overdrag toward the left rail → snap the left column closed.
|
|
375
|
+
if (onLeftCollapse && predictedLeft < minLeftWidth - OVERDRAG) {
|
|
376
|
+
endDrag();
|
|
377
|
+
onLeftCollapse(true);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
commit(raw, d.startLeftW, d.startW);
|
|
341
381
|
},
|
|
342
|
-
[
|
|
382
|
+
[minRightWidth, minLeftWidth, onCollapse, onLeftCollapse, commit],
|
|
343
383
|
);
|
|
344
384
|
const onPointerUp = useCallback((e: ReactPointerEvent<HTMLDivElement>) => {
|
|
345
385
|
drag.current = null;
|
|
@@ -347,16 +387,14 @@ export function AppShell({
|
|
|
347
387
|
}, []);
|
|
348
388
|
const onKeyDown = useCallback(
|
|
349
389
|
(e: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
390
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
391
|
+
e.preventDefault();
|
|
350
392
|
const step = e.shiftKey ? 48 : 16;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
} else if (e.key === "ArrowRight") {
|
|
355
|
-
e.preventDefault();
|
|
356
|
-
onRightWidthChange(clamp(rightWidth - step));
|
|
357
|
-
}
|
|
393
|
+
const startLeftW = leftColRef.current?.clientWidth ?? 0;
|
|
394
|
+
// ArrowLeft grows the right column (shrinks the left); ArrowRight reverses.
|
|
395
|
+
commit(rightWidth + (e.key === "ArrowLeft" ? step : -step), startLeftW, rightWidth);
|
|
358
396
|
},
|
|
359
|
-
[rightWidth,
|
|
397
|
+
[rightWidth, commit],
|
|
360
398
|
);
|
|
361
399
|
|
|
362
400
|
// ── rail drag-and-drop (only when onRailReorder is provided) ──
|
|
@@ -450,6 +488,8 @@ export function AppShell({
|
|
|
450
488
|
}
|
|
451
489
|
|
|
452
490
|
const showRight = !rightCollapsed && rightItems.length > 0;
|
|
491
|
+
const showLeft = !leftCollapsed;
|
|
492
|
+
const showHandle = showLeft && showRight;
|
|
453
493
|
const ctxLeft = onRailContextMenu ? (e: ReactMouseEvent, id: string) => onRailContextMenu("left", e, id) : undefined;
|
|
454
494
|
const ctxRight = onRailContextMenu ? (e: ReactMouseEvent, id: string) => onRailContextMenu("right", e, id) : undefined;
|
|
455
495
|
|
|
@@ -458,16 +498,21 @@ export function AppShell({
|
|
|
458
498
|
{header != null && <div className="pl-appshell__header">{header}</div>}
|
|
459
499
|
<div className="pl-appshell">
|
|
460
500
|
{leftRail}
|
|
461
|
-
|
|
462
|
-
|
|
501
|
+
{showLeft && (
|
|
502
|
+
<main ref={leftColRef} className="pl-appshell__col pl-appshell__col--left">
|
|
503
|
+
{leftContent}
|
|
504
|
+
</main>
|
|
505
|
+
)}
|
|
506
|
+
{showHandle && (
|
|
463
507
|
<div
|
|
464
508
|
className="pl-appshell__handle"
|
|
465
509
|
role="separator"
|
|
466
510
|
aria-orientation="vertical"
|
|
467
|
-
aria-label="Resize
|
|
511
|
+
aria-label="Resize panels"
|
|
468
512
|
aria-valuenow={rightWidth}
|
|
469
513
|
aria-valuemin={minRightWidth}
|
|
470
514
|
aria-valuemax={maxRightWidth}
|
|
515
|
+
aria-valuetext={`Right panel ${rightWidth}px`}
|
|
471
516
|
tabIndex={0}
|
|
472
517
|
onPointerDown={onPointerDown}
|
|
473
518
|
onPointerMove={onPointerMove}
|
|
@@ -478,8 +523,8 @@ export function AppShell({
|
|
|
478
523
|
)}
|
|
479
524
|
{showRight && (
|
|
480
525
|
<aside
|
|
481
|
-
className="pl-appshell__col pl-appshell__col--right"
|
|
482
|
-
style={{ flex: `0 0 ${rightWidth}px`, width: rightWidth }}
|
|
526
|
+
className={cx("pl-appshell__col pl-appshell__col--right", !showLeft && "pl-appshell__col--fill")}
|
|
527
|
+
style={showLeft ? { flex: `0 0 ${rightWidth}px`, width: rightWidth } : undefined}
|
|
483
528
|
>
|
|
484
529
|
{rightContent}
|
|
485
530
|
</aside>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// @protolabsai/ui/plugin-kit — the ADR 0026 console handshake for protoAgent
|
|
2
|
+
// plugin iframe pages. Replaces the broken substring stub that shipped copy-pasted
|
|
3
|
+
// across plugins (it only mapped bg/accent onto private vars and dropped every other
|
|
4
|
+
// token). This applies the console's FULL --pl-* theme bundle to :root and captures
|
|
5
|
+
// the bearer token for same-origin API calls.
|
|
6
|
+
//
|
|
7
|
+
// Two ways to use it (no build step required for either):
|
|
8
|
+
// 1. <script type="module">: import { initPluginView } from ".../plugin-kit.js"
|
|
9
|
+
// 2. classic <script src=".../plugin-kit.js">: use the window.protoPluginView global
|
|
10
|
+
// Or just inline the snippet from docs/how-to/build-a-plugin-view.md.
|
|
11
|
+
|
|
12
|
+
let token = null;
|
|
13
|
+
const listeners = new Set();
|
|
14
|
+
|
|
15
|
+
/** Apply only DS custom properties from a theme map to :root — a stray key can
|
|
16
|
+
* never set an arbitrary CSS property. */
|
|
17
|
+
function applyTheme(theme) {
|
|
18
|
+
if (!theme || typeof theme !== "object") return;
|
|
19
|
+
const root = document.documentElement;
|
|
20
|
+
for (const [k, v] of Object.entries(theme)) {
|
|
21
|
+
if (k.startsWith("--pl-")) root.style.setProperty(k, String(v));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function onMessage(e) {
|
|
26
|
+
const d = e && e.data;
|
|
27
|
+
if (!d || d.type !== "protoagent:init") return;
|
|
28
|
+
if (d.token) token = d.token;
|
|
29
|
+
applyTheme(d.theme);
|
|
30
|
+
for (const fn of listeners) {
|
|
31
|
+
try {
|
|
32
|
+
fn({ token, theme: d.theme });
|
|
33
|
+
} catch (_) {
|
|
34
|
+
/* a listener throwing must not break the handshake */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Start listening for the console handshake. Call once on load. `onInit` (optional)
|
|
40
|
+
* fires every time the console (re)sends tokens/theme — e.g. on a live theme switch.
|
|
41
|
+
* Returns a small handle with the current token. */
|
|
42
|
+
export function initPluginView(onInit) {
|
|
43
|
+
if (typeof onInit === "function") listeners.add(onInit);
|
|
44
|
+
window.addEventListener("message", onMessage);
|
|
45
|
+
// Announce readiness so a console that waits for it sends the init eagerly.
|
|
46
|
+
try {
|
|
47
|
+
window.parent && window.parent.postMessage({ type: "protoagent:ready" }, "*");
|
|
48
|
+
} catch (_) {
|
|
49
|
+
/* sandboxed / no parent */
|
|
50
|
+
}
|
|
51
|
+
return { getToken };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The captured bearer token (null until the handshake delivers one). */
|
|
55
|
+
export function getToken() {
|
|
56
|
+
return token;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Same-origin fetch with the bearer token attached when present. */
|
|
60
|
+
export function apiFetch(input, init) {
|
|
61
|
+
const opts = init || {};
|
|
62
|
+
const headers = new Headers(opts.headers || {});
|
|
63
|
+
if (token) headers.set("Authorization", "Bearer " + token);
|
|
64
|
+
return fetch(input, { ...opts, headers });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Expose a global for classic <script src> (no-build) pages that can't use imports.
|
|
68
|
+
if (typeof window !== "undefined") {
|
|
69
|
+
window.protoPluginView = { initPluginView, getToken, apiFetch };
|
|
70
|
+
}
|
package/src/styles/app-shell.css
CHANGED
|
@@ -222,6 +222,13 @@
|
|
|
222
222
|
border-left: var(--pl-border-width) solid var(--pl-color-border);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/* When the left column is collapsed, the surviving column fills the stage
|
|
226
|
+
(no fixed width, no divider) instead of sitting at its controlled width. */
|
|
227
|
+
.pl-appshell__col--fill {
|
|
228
|
+
flex: 1 1 auto;
|
|
229
|
+
border-left: none;
|
|
230
|
+
}
|
|
231
|
+
|
|
225
232
|
.pl-appshell__handle {
|
|
226
233
|
flex: 0 0 auto;
|
|
227
234
|
width: 5px;
|
package/src/styles/theming.css
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
display: flex;
|
|
6
6
|
flex-direction: column;
|
|
7
7
|
width: 100%;
|
|
8
|
-
|
|
8
|
+
height: 100%;
|
|
9
9
|
color: var(--pl-color-fg);
|
|
10
10
|
font-size: 13px;
|
|
11
11
|
background: var(--pl-color-bg-raised);
|
|
@@ -71,8 +71,9 @@
|
|
|
71
71
|
.pl-theme-panel__groups {
|
|
72
72
|
display: flex;
|
|
73
73
|
flex-direction: column;
|
|
74
|
+
flex: 1;
|
|
75
|
+
min-height: 0;
|
|
74
76
|
gap: var(--pl-space-4);
|
|
75
|
-
max-height: 460px;
|
|
76
77
|
padding: var(--pl-space-3);
|
|
77
78
|
overflow-y: auto;
|
|
78
79
|
}
|