@iloveagents/foundry-web-ui 0.30.0 → 0.32.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.
@@ -137,7 +137,7 @@ export function ChatContent({ centerComposer = false, composerIntro, composerFoo
137
137
  // `overflow-hidden`, not scroll: a surface owns its own layout,
138
138
  // and a full-bleed one (an avatar cropped to the frame) must be
139
139
  // able to reach the edges rather than sit in a scroll box.
140
- _jsx("div", { className: "absolute inset-0 z-10 overflow-hidden bg-background", children: _jsx(ThreadSurface, {}) })), _jsxs(ThreadPrimitive.Viewport, { className: cn("absolute inset-0 overflow-y-auto bg-background", ThreadSurface && "invisible"), "aria-hidden": ThreadSurface ? true : undefined, children: [_jsx(ThreadPrimitive.Empty, { children: emptyState ?? (_jsxs("div", { className: cn("flex min-h-full flex-col", "items-center justify-center", "px-4"), children: [_jsx(Bot, { className: "size-16 mb-6 text-primary/70" }), _jsx("h1", { className: "text-[2rem] font-normal text-foreground text-center mb-6", children: "How can I help you today?" }), _jsx(ChatHomeStartersSlot, {}), starterSuggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-2 max-w-lg", children: starterSuggestions.map((prompt) => (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, asChild: true, children: _jsx(Button, { variant: "outline", className: "text-sm", children: prompt }) }, prompt))) }))] })) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "mx-auto max-w-3xl px-4 pt-8 pb-4", children: [_jsx(ThreadPrimitive.Messages, { components: {
140
+ _jsx("div", { className: "absolute inset-0 z-10 overflow-hidden bg-background", children: _jsx(ThreadSurface, {}) })), _jsxs(ThreadPrimitive.Viewport, { className: cn("absolute inset-0 overflow-y-auto bg-background", ThreadSurface && "invisible"), "aria-hidden": ThreadSurface ? true : undefined, children: [_jsx(ThreadPrimitive.Empty, { children: emptyState ?? (_jsxs("div", { className: cn("flex min-h-full flex-col", "items-center justify-center", "px-4"), children: [_jsx(Bot, { className: "size-16 mb-6 text-primary/70" }), _jsx("h1", { className: "text-[2rem] font-normal text-heading text-center mb-6", children: "How can I help you today?" }), _jsx(ChatHomeStartersSlot, {}), starterSuggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-2 max-w-lg", children: starterSuggestions.map((prompt) => (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, asChild: true, children: _jsx(Button, { variant: "outline", className: "text-sm", children: prompt }) }, prompt))) }))] })) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "mx-auto max-w-3xl px-4 pt-8 pb-4", children: [_jsx(ThreadPrimitive.Messages, { components: {
141
141
  UserMessage,
142
142
  EditComposer,
143
143
  AssistantMessage,
@@ -132,10 +132,12 @@ function extractCodeInfo(children) {
132
132
  * Shared markdown component overrides used by both chat messages and the tool panel.
133
133
  */
134
134
  export const markdownComponents = {
135
- h1: ({ children, ...props }) => (_jsx("h1", { className: "text-2xl font-bold mt-6 mb-4 text-foreground", ...props, children: children })),
136
- h2: ({ children, ...props }) => (_jsx("h2", { className: "text-xl font-bold mt-5 mb-3 text-foreground", ...props, children: children })),
137
- h3: ({ children, ...props }) => (_jsx("h3", { className: "text-lg font-semibold mt-4 mb-2 text-foreground", ...props, children: children })),
138
- h4: ({ children, ...props }) => (_jsx("h4", { className: "text-base font-semibold mt-3 mb-1.5 text-foreground", ...props, children: children })),
135
+ h1: ({ children, ...props }) => (_jsx("h1", { className: "text-2xl font-bold mt-6 mb-4 text-content-heading-1", ...props, children: children })),
136
+ h2: ({ children, ...props }) => (_jsx("h2", { className: "text-xl font-bold mt-5 mb-3 text-content-heading-2", ...props, children: children })),
137
+ h3: ({ children, ...props }) => (_jsx("h3", { className: "text-lg font-semibold mt-4 mb-2 text-content-heading-3", ...props, children: children })),
138
+ h4: ({ children, ...props }) => (_jsx("h4", { className: "text-base font-semibold mt-3 mb-1.5 text-content-heading-4", ...props, children: children })),
139
+ h5: ({ children, ...props }) => (_jsx("h5", { className: "mt-3 mb-1.5 text-sm font-semibold text-content-heading-5", ...props, children: children })),
140
+ h6: ({ children, ...props }) => (_jsx("h6", { className: "mt-3 mb-1.5 text-xs font-semibold text-content-heading-6", ...props, children: children })),
139
141
  p: ({ children, ...props }) => (_jsx("p", { className: "mb-2 leading-relaxed text-foreground", ...props, children: injectCitations(children) })),
140
142
  ul: ({ children, ...props }) => (_jsx("ul", { className: "list-disc list-outside pl-6 mb-4 space-y-1 text-foreground", ...props, children: children })),
141
143
  ol: ({ children, ...props }) => (_jsx("ol", { className: "list-decimal list-outside pl-6 mb-4 space-y-1 text-foreground", ...props, children: children })),
@@ -13,9 +13,9 @@ import { TooltipIconButton } from "./tooltip-icon-button.js";
13
13
  import { Button } from "@iloveagents/foundry-web-primitives";
14
14
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu.js";
15
15
  import { AppBrand } from "./app-brand.js";
16
- const ACTIVE_NAV_ROW_CLASS = "border border-transparent bg-sidebar-selected shadow-none";
17
- const ACTIVE_NAV_TEXT_CLASS = "font-semibold text-sidebar-foreground";
18
- const ACTIVE_NAV_ICON_CLASS = "text-primary";
16
+ const ACTIVE_NAV_ROW_CLASS = "border border-transparent bg-sidebar-selected text-sidebar-selected-foreground shadow-none";
17
+ const ACTIVE_NAV_TEXT_CLASS = "font-semibold text-sidebar-selected-foreground";
18
+ const ACTIVE_NAV_ICON_CLASS = "text-sidebar-selected-foreground";
19
19
  const ACTIVE_NAV_BADGE_CLASS = "border border-sidebar-border bg-background/60 text-sidebar-foreground";
20
20
  // Indent ladder for the nav tree, indexed by depth (0 = top-level item).
21
21
  //
@@ -437,13 +437,15 @@ function ContainerDropZone({ dnd }) {
437
437
  function NavTwisty({ hasChildren, childrenUnloaded = false, isExpanded, onToggle, label, isActive, depth, inTree = true, }) {
438
438
  const cell = cn(NAV_TWISTY_CLASS, getNavDepthOffsets(depth).column);
439
439
  if (!hasChildren) {
440
- return (_jsx("span", { "data-nav-twisty-cell": true, "aria-hidden": "true", className: cn(cell, "pointer-events-none"), children: inTree && !childrenUnloaded && (_jsx("span", { "data-nav-leaf-dot": true, className: cn("block size-1 shrink-0 rounded-full", isActive ? "bg-primary/70" : "bg-sidebar-foreground/30") })) }));
440
+ return (_jsx("span", { "data-nav-twisty-cell": true, "aria-hidden": "true", className: cn(cell, "pointer-events-none"), children: inTree && !childrenUnloaded && (_jsx("span", { "data-nav-leaf-dot": true, className: cn("block size-1 shrink-0 rounded-full", isActive ? "bg-sidebar-selected-foreground/70" : "bg-sidebar-foreground/30") })) }));
441
441
  }
442
442
  return (_jsx("span", { "data-nav-twisty-cell": true, className: cell, children: _jsx("button", { type: "button", "data-nav-twisty": true, "aria-expanded": isExpanded, "aria-label": isExpanded ? `Collapse ${label}` : `Expand ${label}`, onClick: (e) => {
443
443
  e.preventDefault();
444
444
  e.stopPropagation();
445
445
  onToggle?.();
446
- }, className: cn("flex items-center justify-center rounded", NAV_TWISTY_HIT_CLASS, isActive ? "text-primary" : "text-sidebar-foreground/50 hover:text-sidebar-foreground"), children: _jsx(ChevronRight, { className: cn("size-3.5 transition-transform duration-150", isExpanded && "rotate-90") }) }) }));
446
+ }, className: cn("flex items-center justify-center rounded", NAV_TWISTY_HIT_CLASS, isActive
447
+ ? "text-sidebar-selected-foreground"
448
+ : "text-sidebar-foreground/50 hover:text-sidebar-foreground"), children: _jsx(ChevronRight, { className: cn("size-3.5 transition-transform duration-150", isExpanded && "rotate-90") }) }) }));
447
449
  }
448
450
  function NavItemActions({ actions }) {
449
451
  return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7 rounded-md text-muted-foreground hover:bg-muted/80 hover:text-foreground", onClick: (e) => {
@@ -666,7 +668,9 @@ function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader
666
668
  : heading || !underHeader
667
669
  ? NAV_FLAT_BODY_CLASS
668
670
  : getNavDepthOffsets(0).body;
669
- const headingClass = heading && !isDisabled && "font-semibold text-sidebar-foreground";
671
+ const headingClass = heading &&
672
+ !isDisabled &&
673
+ `font-semibold ${isActive ? "text-sidebar-selected-foreground" : "text-sidebar-primary"}`;
670
674
  const { draggableProps, dropTargetProps, isDragOver, isFileDragOver } = useNavItemDnd(item.dnd);
671
675
  const rendersRow = Boolean(hasChildren || hasActions);
672
676
  const activeRowRef = useActiveNavRowScroll(isActive && rendersRow);
@@ -794,7 +798,8 @@ function SidebarResizeHandle() {
794
798
  // group is now the source of truth for "you're in a chat right now".
795
799
  // Kept as a comment so future spelunkers know it was intentional.
796
800
  function CollapsedNavShortcut({ to, label, icon: Icon, isActive, }) {
797
- return (_jsx(NavLink, { to: to, children: _jsx(TooltipIconButton, { tooltip: label, side: "right", className: cn(isActive && "border border-transparent bg-sidebar-selected text-sidebar-foreground"), children: _jsx(Icon, { className: cn("size-4", isActive ? "text-primary" : "text-sidebar-icon") }) }) }, to));
801
+ return (_jsx(NavLink, { to: to, children: _jsx(TooltipIconButton, { tooltip: label, side: "right", className: cn(isActive &&
802
+ "border border-transparent bg-sidebar-selected text-sidebar-selected-foreground"), children: _jsx(Icon, { className: cn("size-4", isActive ? "text-sidebar-selected-foreground" : "text-sidebar-icon") }) }) }, to));
798
803
  }
799
804
  // ---------------------------------------------------------------------------
800
805
  // Collapsible nav-group state — persisted per-label in localStorage so the
@@ -181,10 +181,43 @@ export function ThemeScope({ children, className, style, }) {
181
181
  }
182
182
  export function ThemeDocumentMetadata() {
183
183
  const runtime = useThemeRuntime();
184
+ const { faviconUrl, darkFaviconUrl, iconUrl, darkIconUrl, touchIconUrl } = runtime.branding;
185
+ const favicon = runtime.mode === "dark"
186
+ ? darkFaviconUrl || faviconUrl || darkIconUrl || iconUrl
187
+ : faviconUrl || darkFaviconUrl || iconUrl || darkIconUrl;
184
188
  useEffect(() => {
185
189
  if (typeof document === "undefined")
186
190
  return;
187
191
  document.title = runtime.branding.appTitle || runtime.branding.appName || "Foundry UI";
188
192
  }, [runtime.branding.appName, runtime.branding.appTitle]);
193
+ useEffect(() => {
194
+ if (typeof document === "undefined")
195
+ return;
196
+ const managed = [];
197
+ const addIcon = (rel, href) => {
198
+ if (!href)
199
+ return;
200
+ let resolved;
201
+ try {
202
+ resolved = new URL(href, document.baseURI);
203
+ }
204
+ catch {
205
+ return;
206
+ }
207
+ if (resolved.protocol !== "http:" &&
208
+ resolved.protocol !== "https:" &&
209
+ !(resolved.protocol === "data:" && /^data:image\//i.test(href)))
210
+ return;
211
+ const link = document.createElement("link");
212
+ link.rel = rel;
213
+ link.href = href;
214
+ link.dataset.themeIcon = "true";
215
+ document.head.appendChild(link);
216
+ managed.push(link);
217
+ };
218
+ addIcon("icon", favicon);
219
+ addIcon("apple-touch-icon", touchIconUrl);
220
+ return () => managed.forEach((link) => link.remove());
221
+ }, [favicon, touchIconUrl]);
189
222
  return null;
190
223
  }
@@ -141,5 +141,5 @@ export function ToolPanel({ contained = false }) {
141
141
  else
142
142
  return;
143
143
  event.preventDefault();
144
- }, 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-foreground" : "text-muted-foreground"), "aria-pressed": isEntityPinned, "aria-label": isEntityPinned ? "Unpin document" : "Pin document", title: isEntityPinned ? "Unpin document" : "Pin document", children: _jsx(PinStateIcon, { pinned: isEntityPinned }) }), _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" })) }), !contained && (_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 })] })] }));
144
+ }, 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-heading 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-foreground" : "text-muted-foreground"), "aria-pressed": isEntityPinned, "aria-label": isEntityPinned ? "Unpin document" : "Pin document", title: isEntityPinned ? "Unpin document" : "Pin document", children: _jsx(PinStateIcon, { pinned: isEntityPinned }) }), _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" })) }), !contained && (_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 })] })] }));
145
145
  }
package/dist/index.d.ts CHANGED
@@ -76,8 +76,8 @@ export { submitComposerText } from "./lib/composer-submit-store.js";
76
76
  export { registerSelectionContextResolver, resolveSelectionContext, type SelectionContextInput, type SelectionContextItem, type SelectionContextResolver, type SelectionContextResult, } from "./lib/selection-context.js";
77
77
  export { useThemeStore } from "./lib/theme-store.js";
78
78
  export type { ThemeMode } from "./lib/theme-store.js";
79
- export { mergeThemeDefinitions, resolveThemeRuntime, getEffectiveThemeMode, getThemePreset, } from "./lib/theme-runtime.js";
80
- export type { ThemeDefinition, ThemeLayer, ResolvedThemeRuntime, ThemeColorSlots, ThemeSidebarSlots, ThemeStatusSlots, ThemeChartSlots, ThemeTypographySlots, ThemeRadiusSlots, ThemeAppSlots, ThemeBranding, } from "./lib/theme-runtime.js";
79
+ export { THEME_SLOTS, mergeThemeDefinitions, resolveThemeRuntime, getEffectiveThemeMode, getThemePreset, } from "./lib/theme-runtime.js";
80
+ export type { ThemeSlot, ThemeDefinition, ThemeLayer, ResolvedThemeRuntime, ThemeColorSlots, ThemeSidebarSlots, ThemeStatusSlots, ThemeChartSlots, ThemeTypographySlots, ThemeRadiusSlots, ThemeAppSlots, ThemeBranding, } from "./lib/theme-runtime.js";
81
81
  export { useDevStore } from "./lib/dev-store.js";
82
82
  export { DEFAULT_NAV_CONFIG, findNavItem } from "./lib/nav-config.js";
83
83
  export type { NavItem, NavItemAction, NavItemDnd, NavGroup, NavGroupCreateAction, NavMatch, CollapsedRailItem, CollapsedRailContext, } from "./lib/nav-config.js";
package/dist/index.js CHANGED
@@ -79,7 +79,7 @@ export { useChatLifecycleStore } from "./lib/chat-lifecycle-store.js";
79
79
  export { submitComposerText } from "./lib/composer-submit-store.js";
80
80
  export { registerSelectionContextResolver, resolveSelectionContext, } from "./lib/selection-context.js";
81
81
  export { useThemeStore } from "./lib/theme-store.js";
82
- export { mergeThemeDefinitions, resolveThemeRuntime, getEffectiveThemeMode, getThemePreset, } from "./lib/theme-runtime.js";
82
+ export { THEME_SLOTS, mergeThemeDefinitions, resolveThemeRuntime, getEffectiveThemeMode, getThemePreset, } from "./lib/theme-runtime.js";
83
83
  // Stores moved to @iloveagents/foundry-agent (#95): authStore, citationStore,
84
84
  // streamingStatusStore, clientToolRegistry. Consumers import directly from
85
85
  // `@iloveagents/foundry-agent` / `@iloveagents/foundry-agent/msal` — no re-exports here per
@@ -42,7 +42,7 @@ export const AuthProvider = ({ config, children }) => {
42
42
  if (!ready)
43
43
  return null;
44
44
  if (initError) {
45
- return (_jsx("div", { className: "flex min-h-dvh items-center justify-center bg-background px-6 text-center", children: _jsxs("div", { className: "max-w-md rounded-2xl border border-border bg-card px-6 py-5 shadow-sm", children: [_jsx("h1", { className: "text-lg font-semibold text-foreground", children: "Authentication unavailable" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: initError })] }) }));
45
+ return (_jsx("div", { className: "flex min-h-dvh items-center justify-center bg-background px-6 text-center", children: _jsxs("div", { className: "max-w-md rounded-2xl border border-border bg-card px-6 py-5 shadow-sm", children: [_jsx("h1", { className: "text-lg font-semibold text-heading", children: "Authentication unavailable" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: initError })] }) }));
46
46
  }
47
47
  if (!MsalProvider || !useMsalHook) {
48
48
  return null;
@@ -3,6 +3,15 @@ import type { ThemeMode } from "./theme-store.js";
3
3
  export interface ThemeColorSlots {
4
4
  background?: string;
5
5
  foreground?: string;
6
+ /** Page and document headings, independent of body text and action colours. */
7
+ heading?: string;
8
+ /** Heading colours inside authored content, independent of application chrome. */
9
+ contentHeading1?: string;
10
+ contentHeading2?: string;
11
+ contentHeading3?: string;
12
+ contentHeading4?: string;
13
+ contentHeading5?: string;
14
+ contentHeading6?: string;
6
15
  card?: string;
7
16
  cardForeground?: string;
8
17
  popover?: string;
@@ -109,6 +118,11 @@ export interface ThemeBranding {
109
118
  */
110
119
  iconUrl?: string;
111
120
  darkIconUrl?: string;
121
+ /** Browser tab icon; falls back to the compact brand icon when unset. */
122
+ faviconUrl?: string;
123
+ darkFaviconUrl?: string;
124
+ /** Square PNG recommended for mobile home-screen bookmarks. */
125
+ touchIconUrl?: string;
112
126
  /**
113
127
  * Rendered height of the mark, as a CSS length (`"2rem"`, `"32px"`).
114
128
  * Defaults to the 20px lockup height when unset.
@@ -161,6 +175,20 @@ export interface ResolvedThemeRuntime {
161
175
  variables: CSSProperties;
162
176
  sources: ThemeLayer[];
163
177
  }
178
+ export interface ThemeSlot {
179
+ path: readonly string[];
180
+ label: string;
181
+ group: string;
182
+ kind: "color" | "font" | "length" | "text" | "image";
183
+ cssVariable?: string;
184
+ mode?: "light" | "dark";
185
+ /** Lower values appear first in authoring tools. */
186
+ impact: number;
187
+ /** Human-readable source used when this value is not explicitly set. */
188
+ derivedFrom?: string;
189
+ }
190
+ /** The same allowlists power sanitization and authoring: supported slots cannot disappear from editors. */
191
+ export declare const THEME_SLOTS: readonly ThemeSlot[];
164
192
  export declare function mergeThemeDefinitions(...definitions: (ThemeDefinition | null | undefined)[]): ThemeDefinition;
165
193
  export declare function getEffectiveThemeMode(mode: ThemeMode): "light" | "dark";
166
194
  export declare const FOUNDRY_VIOLET_THEME: ThemeDefinition;
@@ -1,34 +1,41 @@
1
1
  const COLOR_KEYS = [
2
+ "primary",
3
+ "primaryForeground",
4
+ "heading",
5
+ "contentHeading1",
6
+ "contentHeading2",
7
+ "contentHeading3",
8
+ "contentHeading4",
9
+ "contentHeading5",
10
+ "contentHeading6",
2
11
  "background",
3
12
  "foreground",
13
+ "accent",
14
+ "accentForeground",
4
15
  "card",
5
16
  "cardForeground",
6
- "popover",
7
- "popoverForeground",
8
- "primary",
9
- "primaryForeground",
10
- "secondary",
11
- "secondaryForeground",
17
+ "border",
18
+ "ring",
19
+ "input",
12
20
  "muted",
13
21
  "mutedForeground",
14
- "accent",
15
- "accentForeground",
22
+ "secondary",
23
+ "secondaryForeground",
24
+ "popover",
25
+ "popoverForeground",
16
26
  "destructive",
17
27
  "destructiveForeground",
18
- "border",
19
- "input",
20
- "ring",
21
28
  ];
22
29
  const SIDEBAR_KEYS = [
30
+ "sidebarSelected",
31
+ "sidebarSelectedForeground",
23
32
  "sidebar",
24
33
  "sidebarForeground",
25
34
  "sidebarPrimary",
26
35
  "sidebarPrimaryForeground",
36
+ "sidebarIcon",
27
37
  "sidebarAccent",
28
38
  "sidebarAccentForeground",
29
- "sidebarSelected",
30
- "sidebarSelectedForeground",
31
- "sidebarIcon",
32
39
  "sidebarBorder",
33
40
  "sidebarRing",
34
41
  ];
@@ -85,8 +92,105 @@ const BRANDING_KEYS = [
85
92
  "darkLogoUrl",
86
93
  "iconUrl",
87
94
  "darkIconUrl",
95
+ "faviconUrl",
96
+ "darkFaviconUrl",
97
+ "touchIconUrl",
88
98
  "logoHeight",
89
99
  ];
100
+ const SURFACE_IMPACT = Object.fromEntries(COLOR_KEYS.map((key, index) => [key, index]));
101
+ const SURFACE_DERIVATIONS = {
102
+ heading: "foreground",
103
+ contentHeading1: "interface heading",
104
+ contentHeading2: "interface heading + foreground",
105
+ contentHeading3: "foreground",
106
+ contentHeading4: "foreground",
107
+ contentHeading5: "foreground",
108
+ contentHeading6: "foreground",
109
+ card: "background",
110
+ cardForeground: "foreground",
111
+ popover: "card",
112
+ popoverForeground: "foreground",
113
+ secondary: "background + primary",
114
+ secondaryForeground: "foreground",
115
+ muted: "background + foreground",
116
+ mutedForeground: "foreground + background",
117
+ accent: "background + primary",
118
+ accentForeground: "foreground",
119
+ border: "foreground + background",
120
+ input: "border",
121
+ ring: "primary",
122
+ };
123
+ const cssName = (key) => `--${key.replace(/[A-Z0-9]/g, (part) => `-${part.toLowerCase()}`)}`;
124
+ const slotLabel = (key) => ({
125
+ heading: "Interface headings",
126
+ contentHeading1: "H1",
127
+ contentHeading2: "H2",
128
+ contentHeading3: "H3",
129
+ contentHeading4: "H4",
130
+ contentHeading5: "H5",
131
+ contentHeading6: "H6",
132
+ sidebarSelected: "Selected background",
133
+ sidebarSelectedForeground: "Selected text",
134
+ })[key] ?? key.replace(/([a-z])([A-Z0-9])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
135
+ /** The same allowlists power sanitization and authoring: supported slots cannot disappear from editors. */
136
+ export const THEME_SLOTS = [
137
+ ...["light", "dark"].flatMap((mode) => [
138
+ ...COLOR_KEYS.map((key) => ({
139
+ path: [mode, key],
140
+ label: slotLabel(key),
141
+ group: key.startsWith("contentHeading") ? "Content" : "Surfaces",
142
+ kind: "color",
143
+ cssVariable: cssName(key),
144
+ mode,
145
+ impact: SURFACE_IMPACT[key],
146
+ derivedFrom: SURFACE_DERIVATIONS[key],
147
+ })),
148
+ ...[
149
+ ["sidebar", SIDEBAR_KEYS, "Navigation"],
150
+ ["status", STATUS_KEYS, "Signals"],
151
+ ["charts", CHART_KEYS, "Charts"],
152
+ ["app", APP_KEYS, "Code & workflows"],
153
+ ].flatMap(([section, keys, group]) => keys.map((key, impact) => ({
154
+ path: [section, mode, key],
155
+ label: slotLabel(key),
156
+ group,
157
+ kind: "color",
158
+ cssVariable: cssName(key),
159
+ mode,
160
+ impact,
161
+ derivedFrom: group === "Navigation"
162
+ ? "page colours"
163
+ : group === "Signals" && String(key).startsWith("info")
164
+ ? "primary action"
165
+ : group === "Code & workflows"
166
+ ? "page and signal colours"
167
+ : undefined,
168
+ }))),
169
+ ]),
170
+ ...TYPOGRAPHY_KEYS.map((key) => ({
171
+ path: ["typography", key],
172
+ label: slotLabel(key),
173
+ group: "Typography & shape",
174
+ kind: "font",
175
+ cssVariable: `--font-${key.replace("Font", "")}`,
176
+ impact: key === "headingFont" ? 0 : key === "bodyFont" ? 1 : 3,
177
+ })),
178
+ ...RADIUS_KEYS.map((key) => ({
179
+ path: ["radius", key],
180
+ label: "Corner radius",
181
+ group: "Typography & shape",
182
+ kind: "length",
183
+ cssVariable: cssName(key),
184
+ impact: 2,
185
+ })),
186
+ ...BRANDING_KEYS.map((key) => ({
187
+ path: ["branding", key],
188
+ label: slotLabel(key),
189
+ group: "Brand",
190
+ kind: key.endsWith("Url") ? "image" : key === "logoHeight" ? "length" : "text",
191
+ impact: BRANDING_KEYS.indexOf(key),
192
+ })),
193
+ ];
90
194
  function cleanObject(value, keys) {
91
195
  if (!value || typeof value !== "object" || Array.isArray(value))
92
196
  return undefined;
@@ -165,6 +269,13 @@ function toCssVars(colors, sidebar, status, charts, typography, radius, app) {
165
269
  return {
166
270
  ["--background"]: colors.background,
167
271
  ["--foreground"]: colors.foreground,
272
+ ["--heading"]: colors.heading ?? colors.foreground,
273
+ ["--content-heading-1"]: colors.contentHeading1,
274
+ ["--content-heading-2"]: colors.contentHeading2,
275
+ ["--content-heading-3"]: colors.contentHeading3,
276
+ ["--content-heading-4"]: colors.contentHeading4,
277
+ ["--content-heading-5"]: colors.contentHeading5,
278
+ ["--content-heading-6"]: colors.contentHeading6,
168
279
  ["--card"]: colors.card,
169
280
  ["--card-foreground"]: colors.cardForeground,
170
281
  ["--popover"]: colors.popover,
@@ -251,7 +362,7 @@ function defaultSidebar(colors) {
251
362
  sidebarAccentForeground: colors.accentForeground,
252
363
  sidebarSelected: colors.muted,
253
364
  sidebarSelectedForeground: colors.foreground,
254
- sidebarIcon: `color-mix(in srgb, ${colors.foreground} 60%, transparent)`,
365
+ sidebarIcon: "color-mix(in srgb, var(--sidebar-foreground) 60%, transparent)",
255
366
  sidebarBorder: colors.border,
256
367
  sidebarRing: colors.ring,
257
368
  };
@@ -322,8 +433,8 @@ function defaultApp(mode, colors) {
322
433
  workflowDiagramSkippedFill: "color-mix(in srgb, var(--muted) 80%, var(--background) 20%)",
323
434
  workflowDiagramSkippedStroke: "color-mix(in srgb, var(--muted-foreground) 36%, transparent)",
324
435
  workflowDiagramSkippedText: colors.mutedForeground,
325
- workflowDiagramGlow: "rgba(182, 148, 255, 0.18)",
326
- workflowDiagramShell: "rgba(182, 148, 255, 0.06)",
436
+ workflowDiagramGlow: "color-mix(in srgb, var(--primary) 18%, transparent)",
437
+ workflowDiagramShell: "color-mix(in srgb, var(--primary) 6%, transparent)",
327
438
  };
328
439
  }
329
440
  return {
@@ -344,8 +455,8 @@ function defaultApp(mode, colors) {
344
455
  workflowDiagramSkippedFill: "color-mix(in srgb, var(--muted) 65%, var(--background) 35%)",
345
456
  workflowDiagramSkippedStroke: "color-mix(in srgb, var(--muted-foreground) 22%, transparent)",
346
457
  workflowDiagramSkippedText: colors.mutedForeground,
347
- workflowDiagramGlow: "rgba(124, 58, 237, 0.18)",
348
- workflowDiagramShell: "rgba(124, 58, 237, 0.04)",
458
+ workflowDiagramGlow: "color-mix(in srgb, var(--primary) 18%, transparent)",
459
+ workflowDiagramShell: "color-mix(in srgb, var(--primary) 4%, transparent)",
349
460
  };
350
461
  }
351
462
  export const FOUNDRY_VIOLET_THEME = {
@@ -437,42 +548,114 @@ export function getThemePreset(name) {
437
548
  return FOUNDRY_VIOLET_THEME;
438
549
  return FOUNDRY_VIOLET_THEME;
439
550
  }
551
+ function deriveSurfaceColors(resolved, explicit) {
552
+ // The preset remains the safe fallback for an entirely unthemed product.
553
+ // Only values depending on an authored colour should move. A sparse edit
554
+ // to an unrelated slot must not repaint preset surfaces.
555
+ const changed = new Set(Object.keys(explicit ?? {}).filter((key) => Boolean(explicit?.[key])));
556
+ if (!explicit || changed.size === 0)
557
+ return { colors: resolved, changed };
558
+ const next = { ...resolved, ...explicit };
559
+ const setDerived = (key, value, sources) => {
560
+ if (!explicit[key] && value && sources.some((source) => changed.has(source))) {
561
+ next[key] = value;
562
+ changed.add(key);
563
+ }
564
+ };
565
+ setDerived("heading", next.foreground, ["foreground"]);
566
+ setDerived("contentHeading1", next.heading, ["heading"]);
567
+ setDerived("contentHeading2", `color-mix(in srgb, ${next.heading} 78%, ${next.foreground} 22%)`, [
568
+ "heading",
569
+ "foreground",
570
+ ]);
571
+ setDerived("contentHeading3", next.foreground, ["foreground"]);
572
+ setDerived("contentHeading4", next.foreground, ["foreground"]);
573
+ setDerived("contentHeading5", next.foreground, ["foreground"]);
574
+ setDerived("contentHeading6", next.foreground, ["foreground"]);
575
+ setDerived("card", next.background, ["background"]);
576
+ setDerived("cardForeground", next.foreground, ["foreground"]);
577
+ setDerived("popover", next.card, ["card"]);
578
+ setDerived("popoverForeground", next.foreground, ["foreground"]);
579
+ setDerived("secondary", `color-mix(in srgb, ${next.background} 94%, ${next.primary} 6%)`, [
580
+ "background",
581
+ "primary",
582
+ ]);
583
+ setDerived("secondaryForeground", next.foreground, ["foreground"]);
584
+ setDerived("muted", `color-mix(in srgb, ${next.background} 97%, ${next.foreground} 3%)`, [
585
+ "background",
586
+ "foreground",
587
+ ]);
588
+ setDerived("mutedForeground", `color-mix(in srgb, ${next.foreground} 62%, ${next.background} 38%)`, ["foreground", "background"]);
589
+ setDerived("accent", `color-mix(in srgb, ${next.background} 90%, ${next.primary} 10%)`, [
590
+ "background",
591
+ "primary",
592
+ ]);
593
+ setDerived("accentForeground", next.foreground, ["foreground"]);
594
+ setDerived("border", `color-mix(in srgb, ${next.foreground} 14%, ${next.background} 86%)`, [
595
+ "foreground",
596
+ "background",
597
+ ]);
598
+ setDerived("input", next.border, ["border"]);
599
+ setDerived("ring", next.primary, ["primary"]);
600
+ return { colors: next, changed };
601
+ }
440
602
  export function resolveThemeRuntime(layers, mode) {
441
603
  const effectiveMode = getEffectiveThemeMode(mode);
442
604
  const basePreset = layers.find((layer) => layer.basePreset && layer.basePreset.trim())?.basePreset ??
443
605
  "foundry-violet";
444
- const definitions = [getThemePreset(basePreset), ...layers.map((layer) => layer.definition)];
606
+ const customDefinition = mergeThemeDefinitions(...layers.map((layer) => layer.definition));
607
+ const definitions = [getThemePreset(basePreset), customDefinition];
445
608
  const definition = mergeThemeDefinitions(...definitions);
446
- const colors = {
447
- ...(effectiveMode === "dark" ? definition.dark : definition.light),
609
+ const { colors, changed } = deriveSurfaceColors({ ...(effectiveMode === "dark" ? definition.dark : definition.light) }, effectiveMode === "dark" ? customDefinition.dark : customDefinition.light);
610
+ const explicitSidebar = effectiveMode === "dark" ? customDefinition.sidebar?.dark : customDefinition.sidebar?.light;
611
+ const presetSidebar = effectiveMode === "dark"
612
+ ? getThemePreset(basePreset).sidebar?.dark
613
+ : getThemePreset(basePreset).sidebar?.light;
614
+ const sidebarSources = {
615
+ sidebar: "secondary",
616
+ sidebarForeground: "foreground",
617
+ sidebarPrimary: "primary",
618
+ sidebarPrimaryForeground: "primaryForeground",
619
+ sidebarAccent: "accent",
620
+ sidebarAccentForeground: "accentForeground",
621
+ sidebarSelected: "muted",
622
+ sidebarSelectedForeground: "foreground",
623
+ sidebarIcon: null,
624
+ sidebarBorder: "border",
625
+ sidebarRing: "ring",
448
626
  };
627
+ const derivedSidebar = defaultSidebar(colors);
449
628
  const sidebar = {
450
- ...defaultSidebar(colors),
451
- ...(effectiveMode === "dark" ? definition.sidebar?.dark : definition.sidebar?.light),
629
+ ...derivedSidebar,
630
+ ...presetSidebar,
631
+ ...Object.fromEntries(Object.keys(sidebarSources)
632
+ .filter((key) => sidebarSources[key] && changed.has(sidebarSources[key]))
633
+ .map((key) => [key, derivedSidebar[key]])),
634
+ ...explicitSidebar,
452
635
  };
453
636
  const status = {
454
637
  ...defaultStatus(effectiveMode, colors),
455
- ...(effectiveMode === "dark" ? definition.status?.dark : definition.status?.light),
638
+ ...(effectiveMode === "dark" ? customDefinition.status?.dark : customDefinition.status?.light),
456
639
  };
457
640
  const charts = {
458
641
  ...defaultCharts(effectiveMode),
459
- ...(effectiveMode === "dark" ? definition.charts?.dark : definition.charts?.light),
642
+ ...(effectiveMode === "dark" ? customDefinition.charts?.dark : customDefinition.charts?.light),
460
643
  };
461
644
  const typography = {
462
645
  ...FOUNDRY_VIOLET_THEME.typography,
463
- ...definition.typography,
646
+ ...customDefinition.typography,
464
647
  };
465
648
  const radius = {
466
649
  ...FOUNDRY_VIOLET_THEME.radius,
467
- ...definition.radius,
650
+ ...customDefinition.radius,
468
651
  };
469
652
  const app = {
470
653
  ...defaultApp(effectiveMode, colors),
471
- ...(effectiveMode === "dark" ? definition.app?.dark : definition.app?.light),
654
+ ...(effectiveMode === "dark" ? customDefinition.app?.dark : customDefinition.app?.light),
472
655
  };
473
656
  const branding = {
474
657
  ...FOUNDRY_VIOLET_THEME.branding,
475
- ...definition.branding,
658
+ ...customDefinition.branding,
476
659
  };
477
660
  return {
478
661
  mode: effectiveMode,
@@ -7,7 +7,9 @@
7
7
  export type ThemeMode = "light" | "dark" | "system";
8
8
  interface ThemeState {
9
9
  mode: ThemeMode;
10
- setMode: (mode: ThemeMode) => void;
10
+ setMode: (mode: ThemeMode, options?: {
11
+ persist?: boolean;
12
+ }) => void;
11
13
  /** Cycle through light -> dark -> system. */
12
14
  cycle: () => void;
13
15
  }
@@ -38,8 +38,9 @@ export const useThemeStore = create((set, get) => {
38
38
  }
39
39
  return {
40
40
  mode: initial,
41
- setMode: (mode) => {
42
- localStorage.setItem(STORAGE_KEY, mode);
41
+ setMode: (mode, options) => {
42
+ if (options?.persist !== false)
43
+ localStorage.setItem(STORAGE_KEY, mode);
43
44
  applyTheme(mode);
44
45
  set({ mode });
45
46
  },
package/dist/styles.css CHANGED
@@ -1,4 +1,11 @@
1
1
  @theme inline {
2
+ --color-heading: var(--heading, var(--foreground));
3
+ --color-content-heading-1: var(--content-heading-1, var(--heading, var(--foreground)));
4
+ --color-content-heading-2: var(--content-heading-2, var(--heading, var(--foreground)));
5
+ --color-content-heading-3: var(--content-heading-3, var(--foreground));
6
+ --color-content-heading-4: var(--content-heading-4, var(--foreground));
7
+ --color-content-heading-5: var(--content-heading-5, var(--foreground));
8
+ --color-content-heading-6: var(--content-heading-6, var(--foreground));
2
9
  --radius-xs: calc(var(--radius) - 6px);
3
10
  --radius-sm: calc(var(--radius) - 4px);
4
11
  --radius-md: calc(var(--radius) - 2px);
@@ -255,3 +262,10 @@ tr[data-state="selected"] > [data-pinned="cell"] {
255
262
  tr[data-active] > [data-pinned="cell"] {
256
263
  background-color: color-mix(in srgb, var(--primary) 8%, var(--pinned-surface));
257
264
  }
265
+
266
+ @layer base {
267
+ :where(h1, h2, h3, h4, h5, h6) {
268
+ color: var(--heading, var(--foreground));
269
+ font-family: var(--font-heading, inherit);
270
+ }
271
+ }
@@ -60,12 +60,9 @@ export function SelectionBridge({ children }) {
60
60
  .workbench-preview [data-sidebar-surface="desktop"] nav { padding-right: 16px; }
61
61
  .workbench-preview [data-sidebar-surface="desktop"] { border-right-color: transparent; }
62
62
  [data-connected-navigation="true"] [data-sidebar-surface="desktop"] [data-active-nav-row="true"] {
63
- background: var(--background);
64
63
  border-color: transparent; border-top-right-radius: 0; border-bottom-right-radius: 0;
65
64
  }
66
- ` }), children, anchor && (_jsxs("svg", { "aria-hidden": "true", className: "pointer-events-none absolute z-20", style: { inset: 0, width: anchor.width + anchor.surfaceWidth, height: anchor.navHeight }, viewBox: `0 0 ${anchor.width + anchor.surfaceWidth} ${anchor.navHeight}`, children: [_jsx("path", { fill: "var(--background)", d: `M ${anchor.width},${anchor.top - 7} Q ${anchor.width},${anchor.top} ${anchor.width - 8},${anchor.top}
67
- H ${anchor.rowRight} V ${anchor.top + anchor.height} H ${anchor.width - 8}
68
- Q ${anchor.width},${anchor.top + anchor.height} ${anchor.width},${anchor.top + anchor.height + 7} Z` }), _jsx("path", { fill: "none", stroke: "var(--sidebar-ring)", strokeOpacity: 0.28, strokeWidth: 1, d: `M ${anchor.width + 12},0.5
65
+ ` }), children, anchor && (_jsx("svg", { "aria-hidden": "true", className: "pointer-events-none absolute z-20", style: { inset: 0, width: anchor.width + anchor.surfaceWidth, height: anchor.navHeight }, viewBox: `0 0 ${anchor.width + anchor.surfaceWidth} ${anchor.navHeight}`, children: _jsx("path", { fill: "none", stroke: "var(--sidebar-ring)", strokeOpacity: 0.6, strokeWidth: 1, d: `M ${anchor.width + 12},0.5
69
66
  H ${anchor.width + anchor.surfaceWidth - 12}
70
67
  Q ${anchor.width + anchor.surfaceWidth - 0.5},0.5 ${anchor.width + anchor.surfaceWidth - 0.5},12
71
68
  V ${anchor.navHeight - 12}
@@ -80,5 +77,5 @@ export function SelectionBridge({ children }) {
80
77
  Q ${anchor.left},${anchor.top + 0.5} ${anchor.left + 12},${anchor.top + 0.5}
81
78
  H ${anchor.width - 8}
82
79
  Q ${anchor.width - 0.5},${anchor.top + 0.5} ${anchor.width - 0.5},${anchor.top - 7}
83
- V 12 Q ${anchor.width - 0.5},0.5 ${anchor.width + 12},0.5 Z` })] }))] }));
80
+ V 12 Q ${anchor.width - 0.5},0.5 ${anchor.width + 12},0.5 Z` }) }))] }));
84
81
  }
@@ -101,7 +101,6 @@ export function WorkbenchStyles() {
101
101
  }
102
102
  .workbench-preview {
103
103
  --workbench-header-height: 60px;
104
- --sidebar: color-mix(in srgb, var(--background) 98.5%, var(--foreground) 1.5%);
105
104
  background: var(--sidebar);
106
105
  --workbench-outline: color-mix(in srgb, var(--sidebar-ring) 28%, transparent);
107
106
  padding: 8px 8px 8px 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-web-ui",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "license": "MIT",
5
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
6
  "keywords": [
@@ -80,8 +80,8 @@
80
80
  "recharts": "^3.10.1",
81
81
  "remark-gfm": "^4.0.0",
82
82
  "tailwind-merge": "^3.5.0",
83
- "@iloveagents/foundry-agent": "^0.30.0",
84
- "@iloveagents/foundry-web-primitives": "^0.30.0"
83
+ "@iloveagents/foundry-agent": "^0.32.0",
84
+ "@iloveagents/foundry-web-primitives": "^0.32.0"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@ag-ui/client": "^0.0.52",