@iloveagents/foundry-web-ui 0.30.0 → 0.31.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
@@ -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;
@@ -161,6 +170,20 @@ export interface ResolvedThemeRuntime {
161
170
  variables: CSSProperties;
162
171
  sources: ThemeLayer[];
163
172
  }
173
+ export interface ThemeSlot {
174
+ path: readonly string[];
175
+ label: string;
176
+ group: string;
177
+ kind: "color" | "font" | "length" | "text" | "image";
178
+ cssVariable?: string;
179
+ mode?: "light" | "dark";
180
+ /** Lower values appear first in authoring tools. */
181
+ impact: number;
182
+ /** Human-readable source used when this value is not explicitly set. */
183
+ derivedFrom?: string;
184
+ }
185
+ /** The same allowlists power sanitization and authoring: supported slots cannot disappear from editors. */
186
+ export declare const THEME_SLOTS: readonly ThemeSlot[];
164
187
  export declare function mergeThemeDefinitions(...definitions: (ThemeDefinition | null | undefined)[]): ThemeDefinition;
165
188
  export declare function getEffectiveThemeMode(mode: ThemeMode): "light" | "dark";
166
189
  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
  ];
@@ -87,6 +94,100 @@ const BRANDING_KEYS = [
87
94
  "darkIconUrl",
88
95
  "logoHeight",
89
96
  ];
97
+ const SURFACE_IMPACT = Object.fromEntries(COLOR_KEYS.map((key, index) => [key, index]));
98
+ const SURFACE_DERIVATIONS = {
99
+ heading: "foreground",
100
+ contentHeading1: "interface heading",
101
+ contentHeading2: "interface heading + foreground",
102
+ contentHeading3: "foreground",
103
+ contentHeading4: "foreground",
104
+ contentHeading5: "foreground",
105
+ contentHeading6: "foreground",
106
+ card: "background",
107
+ cardForeground: "foreground",
108
+ popover: "card",
109
+ popoverForeground: "foreground",
110
+ secondary: "background + primary",
111
+ secondaryForeground: "foreground",
112
+ muted: "background + foreground",
113
+ mutedForeground: "foreground + background",
114
+ accent: "background + primary",
115
+ accentForeground: "foreground",
116
+ border: "foreground + background",
117
+ input: "border",
118
+ ring: "primary",
119
+ };
120
+ const cssName = (key) => `--${key.replace(/[A-Z0-9]/g, (part) => `-${part.toLowerCase()}`)}`;
121
+ const slotLabel = (key) => ({
122
+ heading: "Interface headings",
123
+ contentHeading1: "H1",
124
+ contentHeading2: "H2",
125
+ contentHeading3: "H3",
126
+ contentHeading4: "H4",
127
+ contentHeading5: "H5",
128
+ contentHeading6: "H6",
129
+ sidebarSelected: "Selected background",
130
+ sidebarSelectedForeground: "Selected text",
131
+ })[key] ?? key.replace(/([a-z])([A-Z0-9])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
132
+ /** The same allowlists power sanitization and authoring: supported slots cannot disappear from editors. */
133
+ export const THEME_SLOTS = [
134
+ ...["light", "dark"].flatMap((mode) => [
135
+ ...COLOR_KEYS.map((key) => ({
136
+ path: [mode, key],
137
+ label: slotLabel(key),
138
+ group: key.startsWith("contentHeading") ? "Content" : "Surfaces",
139
+ kind: "color",
140
+ cssVariable: cssName(key),
141
+ mode,
142
+ impact: SURFACE_IMPACT[key],
143
+ derivedFrom: SURFACE_DERIVATIONS[key],
144
+ })),
145
+ ...[
146
+ ["sidebar", SIDEBAR_KEYS, "Navigation"],
147
+ ["status", STATUS_KEYS, "Signals"],
148
+ ["charts", CHART_KEYS, "Charts"],
149
+ ["app", APP_KEYS, "Code & workflows"],
150
+ ].flatMap(([section, keys, group]) => keys.map((key, impact) => ({
151
+ path: [section, mode, key],
152
+ label: slotLabel(key),
153
+ group,
154
+ kind: "color",
155
+ cssVariable: cssName(key),
156
+ mode,
157
+ impact,
158
+ derivedFrom: group === "Navigation"
159
+ ? "page colours"
160
+ : group === "Signals" && String(key).startsWith("info")
161
+ ? "primary action"
162
+ : group === "Code & workflows"
163
+ ? "page and signal colours"
164
+ : undefined,
165
+ }))),
166
+ ]),
167
+ ...TYPOGRAPHY_KEYS.map((key) => ({
168
+ path: ["typography", key],
169
+ label: slotLabel(key),
170
+ group: "Typography & shape",
171
+ kind: "font",
172
+ cssVariable: `--font-${key.replace("Font", "")}`,
173
+ impact: key === "headingFont" ? 0 : key === "bodyFont" ? 1 : 3,
174
+ })),
175
+ ...RADIUS_KEYS.map((key) => ({
176
+ path: ["radius", key],
177
+ label: "Corner radius",
178
+ group: "Typography & shape",
179
+ kind: "length",
180
+ cssVariable: cssName(key),
181
+ impact: 2,
182
+ })),
183
+ ...BRANDING_KEYS.map((key) => ({
184
+ path: ["branding", key],
185
+ label: slotLabel(key),
186
+ group: "Brand",
187
+ kind: key.endsWith("Url") ? "image" : key === "logoHeight" ? "length" : "text",
188
+ impact: BRANDING_KEYS.indexOf(key),
189
+ })),
190
+ ];
90
191
  function cleanObject(value, keys) {
91
192
  if (!value || typeof value !== "object" || Array.isArray(value))
92
193
  return undefined;
@@ -165,6 +266,13 @@ function toCssVars(colors, sidebar, status, charts, typography, radius, app) {
165
266
  return {
166
267
  ["--background"]: colors.background,
167
268
  ["--foreground"]: colors.foreground,
269
+ ["--heading"]: colors.heading ?? colors.foreground,
270
+ ["--content-heading-1"]: colors.contentHeading1,
271
+ ["--content-heading-2"]: colors.contentHeading2,
272
+ ["--content-heading-3"]: colors.contentHeading3,
273
+ ["--content-heading-4"]: colors.contentHeading4,
274
+ ["--content-heading-5"]: colors.contentHeading5,
275
+ ["--content-heading-6"]: colors.contentHeading6,
168
276
  ["--card"]: colors.card,
169
277
  ["--card-foreground"]: colors.cardForeground,
170
278
  ["--popover"]: colors.popover,
@@ -251,7 +359,7 @@ function defaultSidebar(colors) {
251
359
  sidebarAccentForeground: colors.accentForeground,
252
360
  sidebarSelected: colors.muted,
253
361
  sidebarSelectedForeground: colors.foreground,
254
- sidebarIcon: `color-mix(in srgb, ${colors.foreground} 60%, transparent)`,
362
+ sidebarIcon: "color-mix(in srgb, var(--sidebar-foreground) 60%, transparent)",
255
363
  sidebarBorder: colors.border,
256
364
  sidebarRing: colors.ring,
257
365
  };
@@ -322,8 +430,8 @@ function defaultApp(mode, colors) {
322
430
  workflowDiagramSkippedFill: "color-mix(in srgb, var(--muted) 80%, var(--background) 20%)",
323
431
  workflowDiagramSkippedStroke: "color-mix(in srgb, var(--muted-foreground) 36%, transparent)",
324
432
  workflowDiagramSkippedText: colors.mutedForeground,
325
- workflowDiagramGlow: "rgba(182, 148, 255, 0.18)",
326
- workflowDiagramShell: "rgba(182, 148, 255, 0.06)",
433
+ workflowDiagramGlow: "color-mix(in srgb, var(--primary) 18%, transparent)",
434
+ workflowDiagramShell: "color-mix(in srgb, var(--primary) 6%, transparent)",
327
435
  };
328
436
  }
329
437
  return {
@@ -344,8 +452,8 @@ function defaultApp(mode, colors) {
344
452
  workflowDiagramSkippedFill: "color-mix(in srgb, var(--muted) 65%, var(--background) 35%)",
345
453
  workflowDiagramSkippedStroke: "color-mix(in srgb, var(--muted-foreground) 22%, transparent)",
346
454
  workflowDiagramSkippedText: colors.mutedForeground,
347
- workflowDiagramGlow: "rgba(124, 58, 237, 0.18)",
348
- workflowDiagramShell: "rgba(124, 58, 237, 0.04)",
455
+ workflowDiagramGlow: "color-mix(in srgb, var(--primary) 18%, transparent)",
456
+ workflowDiagramShell: "color-mix(in srgb, var(--primary) 4%, transparent)",
349
457
  };
350
458
  }
351
459
  export const FOUNDRY_VIOLET_THEME = {
@@ -437,42 +545,114 @@ export function getThemePreset(name) {
437
545
  return FOUNDRY_VIOLET_THEME;
438
546
  return FOUNDRY_VIOLET_THEME;
439
547
  }
548
+ function deriveSurfaceColors(resolved, explicit) {
549
+ // The preset remains the safe fallback for an entirely unthemed product.
550
+ // Only values depending on an authored colour should move. A sparse edit
551
+ // to an unrelated slot must not repaint preset surfaces.
552
+ const changed = new Set(Object.keys(explicit ?? {}).filter((key) => Boolean(explicit?.[key])));
553
+ if (!explicit || changed.size === 0)
554
+ return { colors: resolved, changed };
555
+ const next = { ...resolved, ...explicit };
556
+ const setDerived = (key, value, sources) => {
557
+ if (!explicit[key] && value && sources.some((source) => changed.has(source))) {
558
+ next[key] = value;
559
+ changed.add(key);
560
+ }
561
+ };
562
+ setDerived("heading", next.foreground, ["foreground"]);
563
+ setDerived("contentHeading1", next.heading, ["heading"]);
564
+ setDerived("contentHeading2", `color-mix(in srgb, ${next.heading} 78%, ${next.foreground} 22%)`, [
565
+ "heading",
566
+ "foreground",
567
+ ]);
568
+ setDerived("contentHeading3", next.foreground, ["foreground"]);
569
+ setDerived("contentHeading4", next.foreground, ["foreground"]);
570
+ setDerived("contentHeading5", next.foreground, ["foreground"]);
571
+ setDerived("contentHeading6", next.foreground, ["foreground"]);
572
+ setDerived("card", next.background, ["background"]);
573
+ setDerived("cardForeground", next.foreground, ["foreground"]);
574
+ setDerived("popover", next.card, ["card"]);
575
+ setDerived("popoverForeground", next.foreground, ["foreground"]);
576
+ setDerived("secondary", `color-mix(in srgb, ${next.background} 94%, ${next.primary} 6%)`, [
577
+ "background",
578
+ "primary",
579
+ ]);
580
+ setDerived("secondaryForeground", next.foreground, ["foreground"]);
581
+ setDerived("muted", `color-mix(in srgb, ${next.background} 97%, ${next.foreground} 3%)`, [
582
+ "background",
583
+ "foreground",
584
+ ]);
585
+ setDerived("mutedForeground", `color-mix(in srgb, ${next.foreground} 62%, ${next.background} 38%)`, ["foreground", "background"]);
586
+ setDerived("accent", `color-mix(in srgb, ${next.background} 90%, ${next.primary} 10%)`, [
587
+ "background",
588
+ "primary",
589
+ ]);
590
+ setDerived("accentForeground", next.foreground, ["foreground"]);
591
+ setDerived("border", `color-mix(in srgb, ${next.foreground} 14%, ${next.background} 86%)`, [
592
+ "foreground",
593
+ "background",
594
+ ]);
595
+ setDerived("input", next.border, ["border"]);
596
+ setDerived("ring", next.primary, ["primary"]);
597
+ return { colors: next, changed };
598
+ }
440
599
  export function resolveThemeRuntime(layers, mode) {
441
600
  const effectiveMode = getEffectiveThemeMode(mode);
442
601
  const basePreset = layers.find((layer) => layer.basePreset && layer.basePreset.trim())?.basePreset ??
443
602
  "foundry-violet";
444
- const definitions = [getThemePreset(basePreset), ...layers.map((layer) => layer.definition)];
603
+ const customDefinition = mergeThemeDefinitions(...layers.map((layer) => layer.definition));
604
+ const definitions = [getThemePreset(basePreset), customDefinition];
445
605
  const definition = mergeThemeDefinitions(...definitions);
446
- const colors = {
447
- ...(effectiveMode === "dark" ? definition.dark : definition.light),
606
+ const { colors, changed } = deriveSurfaceColors({ ...(effectiveMode === "dark" ? definition.dark : definition.light) }, effectiveMode === "dark" ? customDefinition.dark : customDefinition.light);
607
+ const explicitSidebar = effectiveMode === "dark" ? customDefinition.sidebar?.dark : customDefinition.sidebar?.light;
608
+ const presetSidebar = effectiveMode === "dark"
609
+ ? getThemePreset(basePreset).sidebar?.dark
610
+ : getThemePreset(basePreset).sidebar?.light;
611
+ const sidebarSources = {
612
+ sidebar: "secondary",
613
+ sidebarForeground: "foreground",
614
+ sidebarPrimary: "primary",
615
+ sidebarPrimaryForeground: "primaryForeground",
616
+ sidebarAccent: "accent",
617
+ sidebarAccentForeground: "accentForeground",
618
+ sidebarSelected: "muted",
619
+ sidebarSelectedForeground: "foreground",
620
+ sidebarIcon: null,
621
+ sidebarBorder: "border",
622
+ sidebarRing: "ring",
448
623
  };
624
+ const derivedSidebar = defaultSidebar(colors);
449
625
  const sidebar = {
450
- ...defaultSidebar(colors),
451
- ...(effectiveMode === "dark" ? definition.sidebar?.dark : definition.sidebar?.light),
626
+ ...derivedSidebar,
627
+ ...presetSidebar,
628
+ ...Object.fromEntries(Object.keys(sidebarSources)
629
+ .filter((key) => sidebarSources[key] && changed.has(sidebarSources[key]))
630
+ .map((key) => [key, derivedSidebar[key]])),
631
+ ...explicitSidebar,
452
632
  };
453
633
  const status = {
454
634
  ...defaultStatus(effectiveMode, colors),
455
- ...(effectiveMode === "dark" ? definition.status?.dark : definition.status?.light),
635
+ ...(effectiveMode === "dark" ? customDefinition.status?.dark : customDefinition.status?.light),
456
636
  };
457
637
  const charts = {
458
638
  ...defaultCharts(effectiveMode),
459
- ...(effectiveMode === "dark" ? definition.charts?.dark : definition.charts?.light),
639
+ ...(effectiveMode === "dark" ? customDefinition.charts?.dark : customDefinition.charts?.light),
460
640
  };
461
641
  const typography = {
462
642
  ...FOUNDRY_VIOLET_THEME.typography,
463
- ...definition.typography,
643
+ ...customDefinition.typography,
464
644
  };
465
645
  const radius = {
466
646
  ...FOUNDRY_VIOLET_THEME.radius,
467
- ...definition.radius,
647
+ ...customDefinition.radius,
468
648
  };
469
649
  const app = {
470
650
  ...defaultApp(effectiveMode, colors),
471
- ...(effectiveMode === "dark" ? definition.app?.dark : definition.app?.light),
651
+ ...(effectiveMode === "dark" ? customDefinition.app?.dark : customDefinition.app?.light),
472
652
  };
473
653
  const branding = {
474
654
  ...FOUNDRY_VIOLET_THEME.branding,
475
- ...definition.branding,
655
+ ...customDefinition.branding,
476
656
  };
477
657
  return {
478
658
  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.31.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.31.0",
84
+ "@iloveagents/foundry-web-primitives": "^0.31.0"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@ag-ui/client": "^0.0.52",