@iloveagents/foundry-web-shell 0.6.0 → 0.7.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.
@@ -4,6 +4,7 @@ import { Outlet, useLocation } from "react-router";
4
4
  import { Sidebar, ToolPanelLayout, ChatHeader, ChatBubble, ThemeDocumentMetadata, ThemeRuntimeProvider, ThemeScope, GlobalSelectionPopover, ChatContent, ContextPins, TooltipIconButton, useAppStore, useNavStore, findNavItem, useChatBubbleStore, useThemeStore, } from "@iloveagents/foundry-web-ui";
5
5
  import { cn } from "@iloveagents/foundry-web-primitives";
6
6
  import { X } from "lucide-react";
7
+ import { usePagePanelWidth, MIN_PAGE_PANEL_WIDTH } from "./use-page-panel-width.js";
7
8
  /**
8
9
  * Generic app layout — rendered as the outlet for the customer's routes.
9
10
  *
@@ -65,13 +66,30 @@ export function ShellLayout({ modules, baseThemeLayers }) {
65
66
  // Page content (Outlet) — always rendered to keep page tools and context alive.
66
67
  const page = _jsx(Outlet, {});
67
68
  // Aggregate module layoutExtras into a single fragment.
69
+ const { width: pagePanelWidth, setWidth: setPagePanelWidth, nudgeWidth: nudgePagePanelWidth, startDrag: startPagePanelDrag, maxWidth: pagePanelMaxWidth, } = usePagePanelWidth();
68
70
  const layoutExtras = [];
69
71
  for (const m of modules) {
70
72
  if (m.layoutExtras) {
71
73
  layoutExtras.push(_jsx(ModuleExtra, { children: m.layoutExtras }, m.name));
72
74
  }
73
75
  }
74
- return (_jsx(ThemeRuntimeProvider, { layers: themeLayers, mode: themeMode, children: _jsxs(ThemeScope, { className: "flex h-dvh flex-col overflow-hidden bg-background text-foreground", children: [_jsx(ThemeDocumentMetadata, {}), layoutExtras, _jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx(Sidebar, {}), isExpanded ? (_jsx(ToolPanelLayout, { children: _jsxs("div", { className: "flex h-full min-w-0 flex-1 overflow-hidden", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: [_jsx(ChatHeader, {}), _jsx(ChatContent, {})] }), showPagePanel && (_jsxs("div", { className: cn("hidden h-full shrink-0 border-l border-border bg-background xl:flex xl:flex-col"), style: { width: 600 }, children: [_jsx(PagePanelActions, {}), _jsx("div", { className: "flex-1 flex flex-col min-h-0 overflow-hidden", children: page })] }))] }) })) : (_jsx(ToolPanelLayout, { children: _jsxs("div", { className: "flex flex-1 flex-col overflow-hidden", children: [_jsx(ChatHeader, {}), page] }) }))] }), _jsx(ChatBubble, {}), _jsx(GlobalSelectionPopover, {})] }) }));
76
+ return (_jsx(ThemeRuntimeProvider, { layers: themeLayers, mode: themeMode, children: _jsxs(ThemeScope, { className: "flex h-dvh flex-col overflow-hidden bg-background text-foreground", children: [_jsx(ThemeDocumentMetadata, {}), layoutExtras, _jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx(Sidebar, {}), isExpanded ? (_jsx(ToolPanelLayout, { children: _jsxs("div", { className: "flex h-full min-w-0 flex-1 overflow-hidden", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: [_jsx(ChatHeader, {}), _jsx(ChatContent, {})] }), showPagePanel && (_jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": "Resize content panel", "aria-valuemin": MIN_PAGE_PANEL_WIDTH, "aria-valuemax": pagePanelMaxWidth, "aria-valuenow": Math.round(pagePanelWidth), tabIndex: 0, onPointerDown: (event) => {
77
+ event.preventDefault();
78
+ startPagePanelDrag(event.pointerId);
79
+ }, onKeyDown: (event) => {
80
+ const step = event.shiftKey ? 80 : 24;
81
+ if (event.key === "ArrowLeft")
82
+ nudgePagePanelWidth(step);
83
+ else if (event.key === "ArrowRight")
84
+ nudgePagePanelWidth(-step);
85
+ else if (event.key === "Home")
86
+ setPagePanelWidth(pagePanelMaxWidth);
87
+ else if (event.key === "End")
88
+ setPagePanelWidth(MIN_PAGE_PANEL_WIDTH);
89
+ else
90
+ return;
91
+ event.preventDefault();
92
+ }, className: cn("hidden w-1.5 shrink-0 cursor-col-resize touch-none xl:block", "bg-transparent hover:bg-primary/30 focus-visible:bg-primary", "focus:outline-none") })), showPagePanel && (_jsxs("div", { className: cn("hidden h-full shrink-0 border-l border-border bg-background xl:flex xl:flex-col"), style: { width: pagePanelWidth }, children: [_jsx(PagePanelActions, {}), _jsx("div", { className: "flex-1 flex flex-col min-h-0 overflow-hidden", children: page })] }))] }) })) : (_jsx(ToolPanelLayout, { children: _jsxs("div", { className: "flex flex-1 flex-col overflow-hidden", children: [_jsx(ChatHeader, {}), page] }) }))] }), _jsx(ChatBubble, {}), _jsx(GlobalSelectionPopover, {})] }) }));
75
93
  }
76
94
  function ModuleExtra({ children }) {
77
95
  return _jsx(_Fragment, { children: children });
@@ -0,0 +1,9 @@
1
+ export declare const MIN_PAGE_PANEL_WIDTH = 360;
2
+ export declare function clampPagePanelWidth(desired: number, viewport: number): number;
3
+ export declare function usePagePanelWidth(): {
4
+ width: number;
5
+ setWidth: (next: number) => void;
6
+ nudgeWidth: (delta: number) => void;
7
+ startDrag: (pointerId: number) => void;
8
+ maxWidth: number;
9
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Width of the page panel beside the chat in expanded ("medium") mode.
3
+ *
4
+ * The navigation sidebar is resizable; this split was a hardcoded 600px, so
5
+ * the chat/content ratio was fixed no matter the screen or the task. Users
6
+ * comparing us to ChatGPT/Claude expect to drag it. Clamped so neither side
7
+ * collapses, and persisted per user.
8
+ */
9
+ import { useCallback, useEffect, useRef, useState } from "react";
10
+ export const MIN_PAGE_PANEL_WIDTH = 360;
11
+ /** The chat column must stay usable too. */
12
+ const MIN_CHAT_WIDTH = 420;
13
+ const DEFAULT_PAGE_PANEL_WIDTH = 600;
14
+ const STORAGE_KEY = "foundry.pagePanel.width";
15
+ export function clampPagePanelWidth(desired, viewport) {
16
+ const max = Math.max(MIN_PAGE_PANEL_WIDTH, viewport - MIN_CHAT_WIDTH);
17
+ return Math.round(Math.min(max, Math.max(MIN_PAGE_PANEL_WIDTH, desired)));
18
+ }
19
+ function readStored() {
20
+ if (typeof window === "undefined")
21
+ return null;
22
+ try {
23
+ const raw = window.localStorage.getItem(STORAGE_KEY);
24
+ const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
25
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
26
+ }
27
+ catch {
28
+ return null; // storage disabled — fall back to the default
29
+ }
30
+ }
31
+ export function usePagePanelWidth() {
32
+ const [width, setWidthState] = useState(() => {
33
+ if (typeof window === "undefined")
34
+ return DEFAULT_PAGE_PANEL_WIDTH;
35
+ return clampPagePanelWidth(readStored() ?? DEFAULT_PAGE_PANEL_WIDTH, window.innerWidth);
36
+ });
37
+ const dragPointerId = useRef(null);
38
+ const applyWidth = useCallback((resolve, persist = true) => {
39
+ if (typeof window === "undefined")
40
+ return;
41
+ setWidthState((prev) => {
42
+ const clamped = clampPagePanelWidth(resolve(prev), window.innerWidth);
43
+ // A drag fires pointermove every frame; writing localStorage each time
44
+ // is a synchronous main-thread write per frame for a value only the
45
+ // final position of which matters. Live moves skip the write and the
46
+ // drag persists once, on release.
47
+ if (persist) {
48
+ try {
49
+ window.localStorage.setItem(STORAGE_KEY, String(clamped));
50
+ }
51
+ catch {
52
+ // best-effort; the change still applies for this session
53
+ }
54
+ }
55
+ return clamped;
56
+ });
57
+ }, []);
58
+ const setWidth = useCallback((next) => applyWidth(() => next), [applyWidth]);
59
+ /** Relative nudge — resolved against the LATEST width, so held/repeated
60
+ * arrow keys accumulate instead of all reading the same stale value. */
61
+ const nudgeWidth = useCallback((delta) => applyWidth((prev) => prev + delta), [applyWidth]);
62
+ // Re-clamp on viewport changes so a narrower window can never strand the
63
+ // chat column off-screen.
64
+ useEffect(() => {
65
+ const onResize = () => setWidthState((w) => clampPagePanelWidth(w, window.innerWidth));
66
+ window.addEventListener("resize", onResize);
67
+ return () => window.removeEventListener("resize", onResize);
68
+ }, []);
69
+ // Drag listeners live in an effect and end on pointerup OR pointercancel,
70
+ // so a cancelled drag can never leave the document unselectable.
71
+ useEffect(() => {
72
+ const onMove = (event) => {
73
+ if (dragPointerId.current === null)
74
+ return;
75
+ applyWidth(() => window.innerWidth - event.clientX, false);
76
+ };
77
+ const onEnd = (event) => {
78
+ if (dragPointerId.current !== null && event.pointerId !== dragPointerId.current)
79
+ return;
80
+ const wasDragging = dragPointerId.current !== null;
81
+ dragPointerId.current = null;
82
+ // Persist the position the drag actually landed on.
83
+ if (wasDragging)
84
+ applyWidth((prev) => prev);
85
+ document.body.style.cursor = "";
86
+ document.body.style.userSelect = "";
87
+ };
88
+ window.addEventListener("pointermove", onMove);
89
+ window.addEventListener("pointerup", onEnd);
90
+ window.addEventListener("pointercancel", onEnd);
91
+ return () => {
92
+ window.removeEventListener("pointermove", onMove);
93
+ window.removeEventListener("pointerup", onEnd);
94
+ window.removeEventListener("pointercancel", onEnd);
95
+ document.body.style.cursor = "";
96
+ document.body.style.userSelect = "";
97
+ };
98
+ }, [applyWidth]);
99
+ const startDrag = useCallback((pointerId) => {
100
+ dragPointerId.current = pointerId;
101
+ document.body.style.cursor = "col-resize";
102
+ document.body.style.userSelect = "none";
103
+ }, []);
104
+ const maxWidth = typeof window === "undefined"
105
+ ? DEFAULT_PAGE_PANEL_WIDTH
106
+ : clampPagePanelWidth(Number.POSITIVE_INFINITY, window.innerWidth);
107
+ return { width, setWidth, nudgeWidth, startDrag, maxWidth };
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-web-shell",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,9 +28,9 @@
28
28
  "zustand": "^5.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@iloveagents/foundry-agent": "^0.6.0",
32
- "@iloveagents/foundry-web-ui": "^0.6.0",
33
- "@iloveagents/foundry-web-primitives": "^0.6.0"
31
+ "@iloveagents/foundry-web-primitives": "^0.7.0",
32
+ "@iloveagents/foundry-web-ui": "^0.7.0",
33
+ "@iloveagents/foundry-agent": "^0.7.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@assistant-ui/react": "^0.12.25",