@iloveagents/foundry-web-ui 0.6.0 → 0.7.1

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 CHANGED
@@ -47,4 +47,4 @@ Feature modules (like SPACES) plug into `@iloveagents/foundry-web-ui` via regist
47
47
  - **Citation handler** — handles `[n]` citation clicks in markdown
48
48
  - **Tool UI** — custom rendering for agent tools via `makeAssistantToolUI`
49
49
 
50
- See [AGENTS.md](./AGENTS.md) for architecture details and import boundary rules.
50
+ See [AGENTS.md](https://github.com/iLoveAgents/foundry-ui/blob/main/packages/web-ui/AGENTS.md) for architecture details and import boundary rules.
@@ -1,3 +1,26 @@
1
+ import type { ThemeBranding } from "../lib/theme-runtime.js";
2
+ export interface BrandLockup {
3
+ /** Image to render, or undefined to fall back to the generic glyph. */
4
+ markUrl?: string;
5
+ /** True when `markUrl` is a compact icon rather than a wordmark. */
6
+ isIcon: boolean;
7
+ name: string;
8
+ showName: boolean;
9
+ }
10
+ /**
11
+ * Decide what the brand lockup renders. Exported (underscore-prefixed) so the
12
+ * branching is unit-testable without a DOM renderer — same approach as
13
+ * ``tool-fallback.tsx``.
14
+ *
15
+ * The rule worth stating: whether the name is printed depends on the *shape*
16
+ * of the brand image. A wordmark already spells the brand out, so pairing it
17
+ * with `appName` stutters ("Acme Acme"). An icon says nothing on its
18
+ * own, so it needs the name to complete the lockup. Hence an icon wins over a
19
+ * wordmark when both are set — supplying an icon is an explicit request for
20
+ * "<mark> Product Name", which is what a tenant whose product name differs
21
+ * from their company name is asking for.
22
+ */
23
+ export declare function _resolveBrandLockup(branding: ThemeBranding, mode: "light" | "dark", showName: boolean): BrandLockup;
1
24
  export declare function AppBrand({ showName, className, iconClassName, labelClassName, }: {
2
25
  showName?: boolean;
3
26
  className?: string;
@@ -2,12 +2,42 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Layers3 } from "lucide-react";
3
3
  import { cn } from "@iloveagents/foundry-web-primitives";
4
4
  import { useThemeRuntime } from "./theme-runtime-provider.js";
5
+ /**
6
+ * Decide what the brand lockup renders. Exported (underscore-prefixed) so the
7
+ * branching is unit-testable without a DOM renderer — same approach as
8
+ * ``tool-fallback.tsx``.
9
+ *
10
+ * The rule worth stating: whether the name is printed depends on the *shape*
11
+ * of the brand image. A wordmark already spells the brand out, so pairing it
12
+ * with `appName` stutters ("Acme Acme"). An icon says nothing on its
13
+ * own, so it needs the name to complete the lockup. Hence an icon wins over a
14
+ * wordmark when both are set — supplying an icon is an explicit request for
15
+ * "<mark> Product Name", which is what a tenant whose product name differs
16
+ * from their company name is asking for.
17
+ */
18
+ export function _resolveBrandLockup(branding, mode, showName) {
19
+ const isDark = mode === "dark";
20
+ const pick = (dark, light) => (isDark ? dark || light : light || dark);
21
+ const iconUrl = pick(branding.darkIconUrl, branding.iconUrl);
22
+ const logoUrl = pick(branding.darkLogoUrl, branding.logoUrl);
23
+ const markUrl = iconUrl || logoUrl;
24
+ return {
25
+ markUrl,
26
+ isIcon: Boolean(iconUrl),
27
+ name: branding.appName || "Foundry UI",
28
+ showName: showName && (Boolean(iconUrl) || !markUrl),
29
+ };
30
+ }
5
31
  export function AppBrand({ showName = true, className, iconClassName, labelClassName, }) {
6
32
  const runtime = useThemeRuntime();
7
- const brandName = runtime.branding.appName || "Foundry UI";
8
- const logoUrl = runtime.mode === "dark"
9
- ? runtime.branding.darkLogoUrl || runtime.branding.logoUrl
10
- : runtime.branding.logoUrl || runtime.branding.darkLogoUrl;
11
- const shouldShowName = showName && !logoUrl;
12
- return (_jsxs("span", { className: cn("flex items-center gap-2", className), children: [logoUrl ? (_jsx("img", { src: logoUrl, alt: brandName, className: cn("h-5 w-auto max-w-[7rem] shrink-0 object-contain", iconClassName) })) : (_jsx(Layers3, { className: cn("size-5 shrink-0 text-primary", iconClassName) })), shouldShowName && _jsx("span", { className: cn("truncate", labelClassName), children: brandName })] }));
33
+ const lockup = _resolveBrandLockup(runtime.branding, runtime.mode, showName);
34
+ return (_jsxs("span", { className: cn("flex items-center gap-2", className), children: [lockup.markUrl ? (_jsx("img", { src: lockup.markUrl, alt: lockup.name, className: cn("shrink-0 object-contain",
35
+ // Both lock HEIGHT and let width follow the aspect ratio. Sizing an
36
+ // icon into a square box (`size-5`) silently shrinks any mark that
37
+ // is not 1:1 `object-contain` letterboxes a 4:3 glyph to 20x15,
38
+ // so it lands visibly smaller and thinner than the wordmark it
39
+ // replaced. Marks are rarely square, so height is the only
40
+ // dimension worth fixing. The max-width is just a runaway guard:
41
+ // tighter for an icon, which has to leave room for the name.
42
+ lockup.isIcon ? "h-5 w-auto max-w-8" : "h-5 w-auto max-w-[7rem]", iconClassName) })) : (_jsx(Layers3, { className: cn("size-5 shrink-0 text-primary", iconClassName) })), lockup.showName && _jsx("span", { className: cn("truncate", labelClassName), children: lockup.name })] }));
13
43
  }
@@ -8,8 +8,8 @@ export interface ChatContentProps {
8
8
  /**
9
9
  * Starter prompts shown when the thread is empty. Defaults to
10
10
  * `DEFAULT_STARTER_SUGGESTIONS`. Pass an empty array to hide the
11
- * suggestion buttons entirely. Customer apps (e.g. ANDRITZ Contracts)
12
- * can pass a workspace-aware list derived from app context.
11
+ * suggestion buttons entirely. Downstream apps can pass a
12
+ * workspace-aware list derived from app context.
13
13
  */
14
14
  starterSuggestions?: string[];
15
15
  /**
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState, Suspense } from "react";
3
3
  import { X, Maximize2, Minimize2, Copy, Check, Pin, PinOff, FolderOpen } from "lucide-react";
4
4
  import { useNavigate } from "react-router";
5
5
  import { cn } from "@iloveagents/foundry-web-primitives";
6
- import { useToolPanelStore } from "../lib/tool-panel-store.js";
6
+ import { clampPanelWidth, MIN_PANEL_WIDTH, useToolPanelStore, } from "../lib/tool-panel-store.js";
7
7
  import { useAppStore } from "../lib/app-store.js";
8
8
  import ReactMarkdown from "react-markdown";
9
9
  import remarkGfm from "remark-gfm";
@@ -26,6 +26,40 @@ export function ToolPanel() {
26
26
  const content = useToolPanelStore((state) => state.content);
27
27
  const closePanel = useToolPanelStore((state) => state.closePanel);
28
28
  const panelWidth = useToolPanelStore((state) => state.panelWidth);
29
+ const setPanelWidth = useToolPanelStore((state) => state.setPanelWidth);
30
+ const useOverlay = useToolPanelStore((state) => state.useOverlay);
31
+ /** Pointer id of an in-flight resize drag (null when idle). */
32
+ const dragPointerId = useRef(null);
33
+ const maxPanelWidth = typeof window !== "undefined"
34
+ ? clampPanelWidth(Number.POSITIVE_INFINITY, window.innerWidth)
35
+ : MIN_PANEL_WIDTH;
36
+ useEffect(() => {
37
+ const onPointerMove = (event) => {
38
+ if (dragPointerId.current === null)
39
+ return;
40
+ setPanelWidth(window.innerWidth - event.clientX);
41
+ };
42
+ // pointercancel matters: a cancelled drag must not leave the document
43
+ // unselectable with a col-resize cursor (review catch).
44
+ const onPointerEnd = (event) => {
45
+ if (dragPointerId.current !== null && event.pointerId !== dragPointerId.current)
46
+ return;
47
+ dragPointerId.current = null;
48
+ document.body.style.cursor = "";
49
+ document.body.style.userSelect = "";
50
+ };
51
+ window.addEventListener("pointermove", onPointerMove);
52
+ window.addEventListener("pointerup", onPointerEnd);
53
+ window.addEventListener("pointercancel", onPointerEnd);
54
+ return () => {
55
+ window.removeEventListener("pointermove", onPointerMove);
56
+ window.removeEventListener("pointerup", onPointerEnd);
57
+ window.removeEventListener("pointercancel", onPointerEnd);
58
+ // Unmounting mid-drag must not strand the styles either.
59
+ document.body.style.cursor = "";
60
+ document.body.style.userSelect = "";
61
+ };
62
+ }, [setPanelWidth]);
29
63
  const isFullscreen = useToolPanelStore((state) => state.isFullscreen);
30
64
  const toggleFullscreen = useToolPanelStore((state) => state.toggleFullscreen);
31
65
  const navigate = useNavigate();
@@ -85,5 +119,23 @@ export function ToolPanel() {
85
119
  }, [isOpen, closePanel]);
86
120
  if (!content)
87
121
  return null;
88
- return (_jsxs("div", { "data-tool-panel": true, className: cn("fixed top-0 right-0 z-40", "h-dvh", "bg-background border-l border-border shadow-2xl", "transition-transform duration-300 ease-in-out", "flex flex-col", isOpen ? "translate-x-0" : "translate-x-full"), style: { width: panelWidth }, children: [_jsx("div", { className: "flex-shrink-0 border-b border-border px-6 py-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "text-lg font-semibold text-foreground truncate pr-4", children: content.title }), _jsxs("div", { className: "flex items-center gap-1", children: [entityId && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: handleToggleEntityPin, className: cn("flex items-center justify-center", "size-8 rounded-md shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", isEntityPinned && "text-primary"), "aria-label": isEntityPinned ? "Unpin document" : "Pin document", title: isEntityPinned ? "Unpin document" : "Pin document", children: isEntityPinned ? (_jsx(PinOff, { className: "size-4" })) : (_jsx(Pin, { className: "size-4 text-muted-foreground" })) }), _jsx("button", { type: "button", onClick: handleNavigateToEntity, className: cn("flex items-center justify-center", "size-8 rounded-md shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": "Open in navigation", title: "Open in navigation", children: _jsx(FolderOpen, { className: "size-4 text-muted-foreground" }) })] })), _jsx("button", { type: "button", onClick: handleCopyAll, className: cn("flex items-center justify-center", "size-8 rounded-md flex-shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": "Copy content", children: copied ? (_jsx(Check, { className: "size-4 text-success" })) : (_jsx(Copy, { className: "size-4 text-muted-foreground" })) }), _jsx("button", { type: "button", onClick: toggleFullscreen, className: cn("flex items-center justify-center", "size-8 rounded-md flex-shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": isFullscreen ? "Exit fullscreen" : "Fullscreen", children: isFullscreen ? (_jsx(Minimize2, { className: "size-4 text-muted-foreground" })) : (_jsx(Maximize2, { className: "size-4 text-muted-foreground" })) }), _jsx("button", { type: "button", onClick: closePanel, className: cn("flex items-center justify-center", "size-8 rounded-md flex-shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": "Close panel", children: _jsx(X, { className: "size-5 text-muted-foreground" }) })] })] }) }), _jsxs("div", { className: "relative flex-1 overflow-y-auto", ref: contentRef, "data-testid": "tool-panel-content", children: [_jsx(PanelContentRenderer, { content: content }), _jsx(SelectionPopover, { containerRef: contentRef })] })] }));
122
+ return (_jsxs("div", { "data-tool-panel": true, className: cn("fixed top-0 right-0 z-40", "h-dvh", "bg-background border-l border-border shadow-2xl", "transition-transform duration-300 ease-in-out", "flex flex-col", isOpen ? "translate-x-0" : "translate-x-full"), style: { width: panelWidth }, children: [isOpen && !useOverlay && !isFullscreen ? (_jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": "Resize panel", "aria-valuemin": MIN_PANEL_WIDTH, "aria-valuemax": maxPanelWidth, "aria-valuenow": Math.round(panelWidth), tabIndex: 0, onPointerDown: (event) => {
123
+ event.preventDefault();
124
+ dragPointerId.current = event.pointerId;
125
+ document.body.style.cursor = "col-resize";
126
+ document.body.style.userSelect = "none";
127
+ }, onKeyDown: (event) => {
128
+ const step = event.shiftKey ? 80 : 24;
129
+ if (event.key === "ArrowLeft")
130
+ setPanelWidth(panelWidth + step);
131
+ else if (event.key === "ArrowRight")
132
+ setPanelWidth(panelWidth - step);
133
+ else if (event.key === "Home")
134
+ setPanelWidth(maxPanelWidth);
135
+ else if (event.key === "End")
136
+ setPanelWidth(MIN_PANEL_WIDTH);
137
+ else
138
+ return;
139
+ event.preventDefault();
140
+ }, className: cn("absolute left-0 top-0 z-10 h-full w-2 -translate-x-1/2", "cursor-col-resize touch-none", "before:absolute before:inset-y-0 before:left-1/2 before:w-px before:-translate-x-1/2", "before:bg-transparent hover:before:bg-primary/40 focus-visible:before:bg-primary", "focus:outline-none") })) : null, _jsx("div", { className: "flex-shrink-0 border-b border-border px-6 py-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "text-lg font-semibold text-foreground truncate pr-4", children: content.title }), _jsxs("div", { className: "flex items-center gap-1", children: [entityId && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: handleToggleEntityPin, className: cn("flex items-center justify-center", "size-8 rounded-md shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", isEntityPinned && "text-primary"), "aria-label": isEntityPinned ? "Unpin document" : "Pin document", title: isEntityPinned ? "Unpin document" : "Pin document", children: isEntityPinned ? (_jsx(PinOff, { className: "size-4" })) : (_jsx(Pin, { className: "size-4 text-muted-foreground" })) }), _jsx("button", { type: "button", onClick: handleNavigateToEntity, className: cn("flex items-center justify-center", "size-8 rounded-md shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": "Open in navigation", title: "Open in navigation", children: _jsx(FolderOpen, { className: "size-4 text-muted-foreground" }) })] })), _jsx("button", { type: "button", onClick: handleCopyAll, className: cn("flex items-center justify-center", "size-8 rounded-md flex-shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": "Copy content", children: copied ? (_jsx(Check, { className: "size-4 text-success" })) : (_jsx(Copy, { className: "size-4 text-muted-foreground" })) }), _jsx("button", { type: "button", onClick: toggleFullscreen, className: cn("flex items-center justify-center", "size-8 rounded-md flex-shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": isFullscreen ? "Exit fullscreen" : "Fullscreen", children: isFullscreen ? (_jsx(Minimize2, { className: "size-4 text-muted-foreground" })) : (_jsx(Maximize2, { className: "size-4 text-muted-foreground" })) }), _jsx("button", { type: "button", onClick: closePanel, className: cn("flex items-center justify-center", "size-8 rounded-md flex-shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": "Close panel", children: _jsx(X, { className: "size-5 text-muted-foreground" }) })] })] }) }), _jsxs("div", { className: "relative flex-1 overflow-y-auto", ref: contentRef, "data-testid": "tool-panel-content", children: [_jsx(PanelContentRenderer, { content: content }), _jsx(SelectionPopover, { containerRef: contentRef })] })] }));
89
141
  }
@@ -84,8 +84,21 @@ export interface ThemeAppSlots {
84
84
  export interface ThemeBranding {
85
85
  appName?: string;
86
86
  appTitle?: string;
87
+ /**
88
+ * Full wordmark — an image that already spells the brand out. Rendered on
89
+ * its own, because setting it beside `appName` prints the brand twice.
90
+ */
87
91
  logoUrl?: string;
88
92
  darkLogoUrl?: string;
93
+ /**
94
+ * Compact mark (square-ish glyph, no words). Rendered *with* `appName`
95
+ * beside it, so a tenant can show "<mark> Product Name" — the usual answer
96
+ * when the product name differs from the company name. Takes precedence
97
+ * over `logoUrl` when both are set, since supplying an icon is an explicit
98
+ * request for the lockup.
99
+ */
100
+ iconUrl?: string;
101
+ darkIconUrl?: string;
89
102
  }
90
103
  export interface ThemeDefinition {
91
104
  light?: ThemeColorSlots;
@@ -82,6 +82,8 @@ const BRANDING_KEYS = [
82
82
  "appTitle",
83
83
  "logoUrl",
84
84
  "darkLogoUrl",
85
+ "iconUrl",
86
+ "darkIconUrl",
85
87
  ];
86
88
  function cleanObject(value, keys) {
87
89
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -412,9 +414,9 @@ export const FOUNDRY_VIOLET_THEME = {
412
414
  ring: "oklch(0.72 0.18 286)",
413
415
  },
414
416
  typography: {
415
- bodyFont: "\"Segoe UI\", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif",
416
- headingFont: "\"Segoe UI\", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif",
417
- monoFont: "\"SFMono-Regular\", \"Cascadia Code\", \"JetBrains Mono\", ui-monospace, monospace",
417
+ bodyFont: '"Segoe UI", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
418
+ headingFont: '"Segoe UI", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
419
+ monoFont: '"SFMono-Regular", "Cascadia Code", "JetBrains Mono", ui-monospace, monospace',
418
420
  },
419
421
  radius: {
420
422
  radius: "0.875rem",
@@ -433,10 +435,7 @@ export function resolveThemeRuntime(layers, mode) {
433
435
  const effectiveMode = getEffectiveThemeMode(mode);
434
436
  const basePreset = layers.find((layer) => layer.basePreset && layer.basePreset.trim())?.basePreset ??
435
437
  "foundry-violet";
436
- const definitions = [
437
- getThemePreset(basePreset),
438
- ...layers.map((layer) => layer.definition),
439
- ];
438
+ const definitions = [getThemePreset(basePreset), ...layers.map((layer) => layer.definition)];
440
439
  const definition = mergeThemeDefinitions(...definitions);
441
440
  const colors = {
442
441
  ...(effectiveMode === "dark" ? definition.dark : definition.light),
@@ -29,6 +29,10 @@ export interface PanelRendererEntry {
29
29
  content: ToolPanelContent;
30
30
  }>>;
31
31
  }
32
+ /** Smallest useful panel, and the space the chat/content beside it must keep. */
33
+ export declare const MIN_PANEL_WIDTH = 360;
34
+ /** Clamp a desired width so neither side can be squeezed out of usefulness. */
35
+ export declare const clampPanelWidth: (desired: number, viewport: number) => number;
32
36
  interface ToolPanelState {
33
37
  isOpen: boolean;
34
38
  content: ToolPanelContent | null;
@@ -41,6 +45,8 @@ interface ToolPanelState {
41
45
  setContent: (content: ToolPanelContent) => void;
42
46
  updateLayout: () => void;
43
47
  toggleFullscreen: () => void;
48
+ /** Drag-to-resize the split; clamped and persisted per user. */
49
+ setPanelWidth: (width: number) => void;
44
50
  /** Register a custom content renderer (e.g., PDF viewer, entity pages). */
45
51
  registerRenderer: (entry: PanelRendererEntry) => void;
46
52
  }
@@ -6,12 +6,40 @@
6
6
  */
7
7
  import { create } from "zustand";
8
8
  const SIDE_BY_SIDE_BREAKPOINT = 1280;
9
+ /** Smallest useful panel, and the space the chat/content beside it must keep. */
10
+ export const MIN_PANEL_WIDTH = 360;
11
+ const MIN_SIBLING_WIDTH = 420;
12
+ const PANEL_WIDTH_STORAGE_KEY = "foundry.toolPanel.width";
13
+ /** Clamp a desired width so neither side can be squeezed out of usefulness. */
14
+ export const clampPanelWidth = (desired, viewport) => {
15
+ if (viewport < SIDE_BY_SIDE_BREAKPOINT)
16
+ return viewport;
17
+ const max = Math.max(MIN_PANEL_WIDTH, viewport - MIN_SIBLING_WIDTH);
18
+ return Math.round(Math.min(max, Math.max(MIN_PANEL_WIDTH, desired)));
19
+ };
20
+ const readStoredPanelWidth = () => {
21
+ if (typeof window === "undefined")
22
+ return null;
23
+ try {
24
+ const raw = window.localStorage.getItem(PANEL_WIDTH_STORAGE_KEY);
25
+ const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
26
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
27
+ }
28
+ catch {
29
+ return null; // private mode / storage disabled — fall back to the default
30
+ }
31
+ };
9
32
  const getPanelWidth = () => {
10
33
  if (typeof window === "undefined")
11
34
  return 800;
12
35
  const screenWidth = window.innerWidth;
13
36
  if (screenWidth < SIDE_BY_SIDE_BREAKPOINT)
14
37
  return screenWidth;
38
+ // A width the user dragged to wins over the computed default, re-clamped
39
+ // to the current viewport so a narrow window can't strand the content.
40
+ const stored = readStoredPanelWidth();
41
+ if (stored !== null)
42
+ return clampPanelWidth(stored, screenWidth);
15
43
  return Math.min(1000, Math.max(600, screenWidth * 0.5));
16
44
  };
17
45
  const shouldUseOverlay = () => {
@@ -30,6 +58,20 @@ export const useToolPanelStore = create((set, get) => ({
30
58
  registerRenderer: (entry) => set((state) => ({ renderers: [...state.renderers, entry] })),
31
59
  closePanel: () => set({ isOpen: false, isFullscreen: false }),
32
60
  setContent: (content) => set({ content }),
61
+ setPanelWidth: (width) => {
62
+ if (typeof window === "undefined")
63
+ return;
64
+ if (get().isFullscreen)
65
+ return;
66
+ const next = clampPanelWidth(width, window.innerWidth);
67
+ try {
68
+ window.localStorage.setItem(PANEL_WIDTH_STORAGE_KEY, String(next));
69
+ }
70
+ catch {
71
+ // Persisting is best-effort; the drag still applies for this session.
72
+ }
73
+ set({ panelWidth: next });
74
+ },
33
75
  updateLayout: () => {
34
76
  const { isFullscreen } = get();
35
77
  if (!isFullscreen) {
package/package.json CHANGED
@@ -1,7 +1,24 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-web-ui",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "license": "MIT",
5
+ "description": "React agent UI core for Foundry UI — chat, composer, AG-UI adapter for assistant-ui, tool cards, panels, sidebar, theme runtime, and UI stores.",
6
+ "keywords": [
7
+ "react",
8
+ "ag-ui",
9
+ "assistant-ui",
10
+ "chat",
11
+ "agent",
12
+ "foundry-ui",
13
+ "agent-framework"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/iLoveAgents/foundry-ui.git",
18
+ "directory": "packages/web-ui"
19
+ },
20
+ "homepage": "https://github.com/iLoveAgents/foundry-ui/tree/main/packages/web-ui#readme",
21
+ "bugs": "https://github.com/iLoveAgents/foundry-ui/issues",
5
22
  "type": "module",
6
23
  "main": "./dist/index.js",
7
24
  "types": "./dist/index.d.ts",
@@ -53,8 +70,8 @@
53
70
  "tailwind-merge": "^3.5.0",
54
71
  "react-markdown": "^10.0.0",
55
72
  "remark-gfm": "^4.0.0",
56
- "@iloveagents/foundry-agent": "^0.6.0",
57
- "@iloveagents/foundry-web-primitives": "^0.6.0"
73
+ "@iloveagents/foundry-agent": "^0.7.1",
74
+ "@iloveagents/foundry-web-primitives": "^0.7.1"
58
75
  },
59
76
  "devDependencies": {
60
77
  "@ag-ui/client": "^0.0.52",