@iloveagents/foundry-web-ui 0.3.0 → 0.4.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.
Files changed (223) hide show
  1. package/README.md +14 -5
  2. package/dist/components/ag-ui-runtime-provider.d.ts +64 -0
  3. package/dist/components/ag-ui-runtime-provider.js +71 -0
  4. package/dist/components/app-brand.d.ts +6 -0
  5. package/dist/components/app-brand.js +13 -0
  6. package/dist/components/assistant-chat.d.ts +26 -0
  7. package/dist/components/assistant-chat.js +93 -0
  8. package/dist/components/chat-attachments.d.ts +25 -0
  9. package/dist/components/chat-attachments.js +20 -0
  10. package/dist/components/chat-bubble.d.ts +1 -0
  11. package/dist/components/chat-bubble.js +103 -0
  12. package/dist/components/chat-header.d.ts +2 -0
  13. package/dist/components/chat-header.js +16 -0
  14. package/dist/components/client-tool-executor.d.ts +12 -0
  15. package/dist/components/client-tool-executor.js +108 -0
  16. package/dist/components/collection-empty-state.d.ts +9 -0
  17. package/dist/components/collection-empty-state.js +6 -0
  18. package/dist/components/collection-filter-bar.d.ts +19 -0
  19. package/dist/components/collection-filter-bar.js +28 -0
  20. package/dist/components/collection-search-input.d.ts +6 -0
  21. package/dist/components/collection-search-input.js +8 -0
  22. package/dist/components/collection-skeleton.d.ts +6 -0
  23. package/dist/components/collection-skeleton.js +10 -0
  24. package/dist/components/collection-sort-menu.d.ts +10 -0
  25. package/dist/components/collection-sort-menu.js +9 -0
  26. package/dist/components/collection-surface.d.ts +8 -0
  27. package/dist/components/collection-surface.js +5 -0
  28. package/dist/components/collection-toolbar.d.ts +9 -0
  29. package/dist/components/collection-toolbar.js +5 -0
  30. package/dist/components/collection-view-toggle.d.ts +7 -0
  31. package/dist/components/collection-view-toggle.js +17 -0
  32. package/dist/components/composer-submit-bridge.d.ts +1 -0
  33. package/dist/components/composer-submit-bridge.js +15 -0
  34. package/dist/components/confirm-dialog.d.ts +11 -0
  35. package/dist/components/confirm-dialog.js +6 -0
  36. package/dist/components/confirmation-card.d.ts +14 -0
  37. package/dist/components/confirmation-card.js +12 -0
  38. package/dist/components/context-badges.d.ts +16 -0
  39. package/dist/components/context-badges.js +56 -0
  40. package/dist/components/context-bar.d.ts +41 -0
  41. package/dist/components/context-bar.js +123 -0
  42. package/dist/components/form-dialog.d.ts +12 -0
  43. package/dist/components/form-dialog.js +13 -0
  44. package/dist/components/global-selection-popover.d.ts +7 -0
  45. package/dist/components/global-selection-popover.js +167 -0
  46. package/dist/components/infinite-scroll-sentinel.d.ts +6 -0
  47. package/dist/components/infinite-scroll-sentinel.js +25 -0
  48. package/dist/components/json-viewer.d.ts +10 -0
  49. package/dist/components/json-viewer.js +80 -0
  50. package/dist/components/loading-indicator.d.ts +4 -0
  51. package/dist/components/loading-indicator.js +22 -0
  52. package/dist/components/markdown-text.d.ts +13 -0
  53. package/dist/components/markdown-text.js +221 -0
  54. package/dist/components/name-dialog.d.ts +16 -0
  55. package/dist/components/name-dialog.js +23 -0
  56. package/dist/components/selection-popover.d.ts +12 -0
  57. package/dist/components/selection-popover.js +111 -0
  58. package/dist/components/show-document-tool-ui.d.ts +1 -0
  59. package/dist/components/show-document-tool-ui.js +53 -0
  60. package/dist/components/sidebar.d.ts +4 -0
  61. package/dist/components/sidebar.js +904 -0
  62. package/dist/components/surface-card.d.ts +9 -0
  63. package/dist/components/surface-card.js +33 -0
  64. package/dist/components/theme-runtime-provider.d.ts +15 -0
  65. package/dist/components/theme-runtime-provider.js +48 -0
  66. package/dist/components/theme-toggle.d.ts +2 -0
  67. package/dist/components/theme-toggle.js +19 -0
  68. package/dist/components/tool-call-card.d.ts +27 -0
  69. package/dist/components/tool-call-card.js +13 -0
  70. package/dist/components/tool-fallback.d.ts +26 -0
  71. package/dist/components/tool-fallback.js +129 -0
  72. package/dist/components/tool-panel-layout.d.ts +10 -0
  73. package/dist/components/tool-panel-layout.js +12 -0
  74. package/dist/components/tool-panel.d.ts +1 -0
  75. package/dist/components/tool-panel.js +89 -0
  76. package/dist/components/tooltip-icon-button.d.ts +7 -0
  77. package/dist/components/tooltip-icon-button.js +9 -0
  78. package/dist/components/user-menu.d.ts +2 -0
  79. package/dist/components/user-menu.js +14 -0
  80. package/dist/index.d.ts +68 -0
  81. package/dist/index.js +83 -0
  82. package/dist/lib/ag-ui-adapter.d.ts +29 -0
  83. package/dist/lib/ag-ui-adapter.js +373 -0
  84. package/dist/lib/app-store.d.ts +115 -0
  85. package/dist/lib/app-store.js +199 -0
  86. package/dist/lib/attachment-adapter.d.ts +20 -0
  87. package/dist/lib/attachment-adapter.js +99 -0
  88. package/dist/lib/auth-provider.d.ts +16 -0
  89. package/dist/lib/auth-provider.js +88 -0
  90. package/dist/lib/chat-bubble-store.d.ts +30 -0
  91. package/dist/lib/chat-bubble-store.js +23 -0
  92. package/dist/lib/chat-lifecycle-store.d.ts +10 -0
  93. package/{src/lib/chat-lifecycle-store.ts → dist/lib/chat-lifecycle-store.js} +3 -13
  94. package/dist/lib/client-tools.d.ts +11 -0
  95. package/dist/lib/client-tools.js +274 -0
  96. package/dist/lib/composer-submit-store.d.ts +12 -0
  97. package/dist/lib/composer-submit-store.js +17 -0
  98. package/dist/lib/dev-store.d.ts +38 -0
  99. package/dist/lib/dev-store.js +32 -0
  100. package/dist/lib/nav-config.d.ts +159 -0
  101. package/dist/lib/nav-config.js +162 -0
  102. package/dist/lib/nav-dnd.d.ts +8 -0
  103. package/dist/lib/nav-dnd.js +9 -0
  104. package/dist/lib/nav-store.d.ts +45 -0
  105. package/dist/lib/nav-store.js +49 -0
  106. package/dist/lib/selection-context.d.ts +21 -0
  107. package/dist/lib/selection-context.js +34 -0
  108. package/dist/lib/sidebar-store.d.ts +23 -0
  109. package/dist/lib/sidebar-store.js +98 -0
  110. package/dist/lib/theme-runtime.d.ts +130 -0
  111. package/dist/lib/theme-runtime.js +479 -0
  112. package/dist/lib/theme-store.d.ts +15 -0
  113. package/dist/lib/theme-store.js +53 -0
  114. package/dist/lib/tool-panel-store.d.ts +48 -0
  115. package/dist/lib/tool-panel-store.js +69 -0
  116. package/dist/lib/use-new-conversation.d.ts +8 -0
  117. package/dist/lib/use-new-conversation.js +62 -0
  118. package/dist/lib/use-page-tools.d.ts +27 -0
  119. package/{src/lib/use-page-tools.ts → dist/lib/use-page-tools.js} +11 -13
  120. package/dist/styles.css +142 -0
  121. package/dist/ui/collapsible.d.ts +5 -0
  122. package/{src/ui/collapsible.tsx → dist/ui/collapsible.js} +0 -2
  123. package/dist/ui/dialog.d.ts +15 -0
  124. package/dist/ui/dialog.js +25 -0
  125. package/dist/ui/dropdown-menu.d.ts +7 -0
  126. package/dist/ui/dropdown-menu.js +19 -0
  127. package/dist/ui/popover.d.ts +7 -0
  128. package/dist/ui/popover.js +10 -0
  129. package/dist/ui/select.d.ts +4 -0
  130. package/dist/ui/select.js +9 -0
  131. package/dist/ui/tooltip.d.ts +7 -0
  132. package/dist/ui/tooltip.js +10 -0
  133. package/package.json +31 -12
  134. package/AGENTS.md +0 -59
  135. package/CHANGELOG.md +0 -203
  136. package/CLAUDE.md +0 -1
  137. package/src/__tests__/no-spaces-imports.test.ts +0 -59
  138. package/src/__tests__/open-core-guards.test.ts +0 -307
  139. package/src/__tests__/theme-runtime.test.ts +0 -81
  140. package/src/__tests__/tool-fallback.test.tsx +0 -109
  141. package/src/components/__tests__/context-bar.test.ts +0 -112
  142. package/src/components/ag-ui-runtime-provider.tsx +0 -170
  143. package/src/components/app-brand.tsx +0 -38
  144. package/src/components/assistant-chat.tsx +0 -461
  145. package/src/components/chat-attachments.tsx +0 -68
  146. package/src/components/chat-bubble.tsx +0 -373
  147. package/src/components/chat-header.tsx +0 -44
  148. package/src/components/client-tool-executor.tsx +0 -230
  149. package/src/components/collection-empty-state.tsx +0 -37
  150. package/src/components/collection-filter-bar.tsx +0 -168
  151. package/src/components/collection-search-input.tsx +0 -41
  152. package/src/components/collection-skeleton.tsx +0 -55
  153. package/src/components/collection-sort-menu.tsx +0 -62
  154. package/src/components/collection-surface.tsx +0 -46
  155. package/src/components/collection-toolbar.tsx +0 -33
  156. package/src/components/collection-view-toggle.tsx +0 -61
  157. package/src/components/composer-submit-bridge.tsx +0 -19
  158. package/src/components/confirm-dialog.tsx +0 -49
  159. package/src/components/confirmation-card.tsx +0 -32
  160. package/src/components/context-badges.tsx +0 -136
  161. package/src/components/context-bar.tsx +0 -238
  162. package/src/components/form-dialog.tsx +0 -56
  163. package/src/components/global-selection-popover.tsx +0 -266
  164. package/src/components/infinite-scroll-sentinel.tsx +0 -48
  165. package/src/components/json-viewer.tsx +0 -232
  166. package/src/components/loading-indicator.tsx +0 -40
  167. package/src/components/markdown-text-citations.test.tsx +0 -108
  168. package/src/components/markdown-text.tsx +0 -508
  169. package/src/components/name-dialog.tsx +0 -87
  170. package/src/components/selection-popover.tsx +0 -156
  171. package/src/components/show-document-tool-ui.tsx +0 -90
  172. package/src/components/sidebar.test.tsx +0 -611
  173. package/src/components/sidebar.tsx +0 -1689
  174. package/src/components/surface-card.tsx +0 -45
  175. package/src/components/theme-runtime-provider.tsx +0 -93
  176. package/src/components/theme-toggle.tsx +0 -27
  177. package/src/components/tool-call-card.tsx +0 -126
  178. package/src/components/tool-fallback.tsx +0 -169
  179. package/src/components/tool-panel-layout.tsx +0 -36
  180. package/src/components/tool-panel.tsx +0 -233
  181. package/src/components/tooltip-icon-button.tsx +0 -28
  182. package/src/components/user-menu.tsx +0 -60
  183. package/src/index.ts +0 -185
  184. package/src/lib/__tests__/ag-ui-adapter.test.ts +0 -273
  185. package/src/lib/__tests__/app-store.test.ts +0 -405
  186. package/src/lib/__tests__/attachment-adapter.test.ts +0 -48
  187. package/src/lib/__tests__/chat-bubble-store.test.ts +0 -67
  188. package/src/lib/__tests__/client-tools.test.ts +0 -124
  189. package/src/lib/__tests__/dev-store.test.ts +0 -78
  190. package/src/lib/__tests__/nav-config.test.ts +0 -135
  191. package/src/lib/__tests__/nav-dnd.test.ts +0 -24
  192. package/src/lib/__tests__/nav-store.test.ts +0 -59
  193. package/src/lib/__tests__/selection-context.test.ts +0 -114
  194. package/src/lib/__tests__/sidebar-store.test.ts +0 -144
  195. package/src/lib/__tests__/theme-store.test.ts +0 -83
  196. package/src/lib/__tests__/tool-panel-store.test.ts +0 -61
  197. package/src/lib/__tests__/use-page-context.test.ts +0 -58
  198. package/src/lib/ag-ui-adapter.ts +0 -412
  199. package/src/lib/app-store.ts +0 -361
  200. package/src/lib/attachment-adapter.ts +0 -92
  201. package/src/lib/auth-provider.tsx +0 -144
  202. package/src/lib/chat-bubble-store.ts +0 -46
  203. package/src/lib/client-tools.ts +0 -293
  204. package/src/lib/composer-submit-store.ts +0 -32
  205. package/src/lib/dev-store.ts +0 -69
  206. package/src/lib/nav-config.ts +0 -329
  207. package/src/lib/nav-dnd.ts +0 -14
  208. package/src/lib/nav-store.ts +0 -89
  209. package/src/lib/selection-context.ts +0 -65
  210. package/src/lib/sidebar-store.ts +0 -113
  211. package/src/lib/theme-runtime.ts +0 -660
  212. package/src/lib/theme-store.ts +0 -67
  213. package/src/lib/tool-panel-store.ts +0 -121
  214. package/src/lib/use-new-conversation.test.tsx +0 -168
  215. package/src/lib/use-new-conversation.ts +0 -64
  216. package/src/ui/dialog.tsx +0 -109
  217. package/src/ui/dropdown-menu.tsx +0 -75
  218. package/src/ui/popover.tsx +0 -34
  219. package/src/ui/select.tsx +0 -15
  220. package/src/ui/tooltip.tsx +0 -30
  221. package/src/vite-env.d.ts +0 -1
  222. package/tsconfig.json +0 -16
  223. package/vitest.config.ts +0 -8
@@ -0,0 +1,108 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Client Tool Executor — registers custom UIs for client-side tools.
4
+ *
5
+ * Tool execution happens in the AGUIRunner (client-side tools intercepted at
6
+ * `tool-call-end`). This component only registers the UI rendering and
7
+ * injects the navigate function (which requires React Router context).
8
+ *
9
+ * Modern assistant-ui v0.12 pattern: each tool UI is a top-level
10
+ * `makeAssistantToolUI(...)` component. We mount them as children of
11
+ * `<ClientToolExecutor>` inside the AssistantRuntimeProvider tree.
12
+ */
13
+ import { makeAssistantToolUI } from "@assistant-ui/react";
14
+ import { useNavigate } from "react-router";
15
+ import { useEffect } from "react";
16
+ import { setNavigateFunction } from "../lib/client-tools.js";
17
+ import { useToolPanelStore } from "../lib/tool-panel-store.js";
18
+ import { ConfirmationCard } from "./confirmation-card.js";
19
+ import { ToolCallCard } from "./tool-call-card.js";
20
+ import { Navigation, Palette, PanelRight, MousePointerClick, PenLine, Highlighter, ShieldCheck, Info, } from "lucide-react";
21
+ const NavigateUI = makeAssistantToolUI({
22
+ toolName: "ui_navigate",
23
+ render: ({ args, result, status }) => {
24
+ const navigate = useNavigate();
25
+ return (_jsx(ToolCallCard, { icon: _jsx(Navigation, { className: "size-3.5" }), title: "Navigate", description: status.type === "running"
26
+ ? "Navigating..."
27
+ : result?.status === "success"
28
+ ? `Navigated to ${args?.path}`
29
+ : "Navigation failed", status: status, action: status.type === "complete" && args?.path
30
+ ? { label: "Go to page", onClick: () => navigate(args.path) }
31
+ : undefined, className: "w-full" }));
32
+ },
33
+ });
34
+ const SetThemeUI = makeAssistantToolUI({
35
+ toolName: "ui_set_theme",
36
+ render: ({ args, result, status }) => (_jsx(ToolCallCard, { icon: _jsx(Palette, { className: "size-3.5" }), title: "Set Theme", description: status.type === "running"
37
+ ? "Changing theme..."
38
+ : result?.status === "success"
39
+ ? `Theme set to ${args?.mode}`
40
+ : "Theme change failed", status: status, className: "w-full" })),
41
+ });
42
+ const OpenPanelUI = makeAssistantToolUI({
43
+ toolName: "ui_open_panel",
44
+ render: ({ args, result, status }) => {
45
+ const handleReopen = () => {
46
+ if (args?.path) {
47
+ useToolPanelStore.getState().openPanel({
48
+ title: args.title,
49
+ content: args.path,
50
+ type: "page",
51
+ });
52
+ }
53
+ else if (args?.content) {
54
+ useToolPanelStore.getState().openPanel({
55
+ title: args.title,
56
+ content: args.content,
57
+ type: args.type || "markdown",
58
+ });
59
+ }
60
+ };
61
+ return (_jsx(ToolCallCard, { icon: _jsx(PanelRight, { className: "size-3.5" }), title: "Open Panel", description: status.type === "running"
62
+ ? "Opening panel..."
63
+ : result?.status === "success"
64
+ ? `Panel: ${args?.title}`
65
+ : "Panel failed", status: status, action: status.type === "complete" && result?.status === "success"
66
+ ? { label: "View", onClick: handleReopen }
67
+ : undefined, className: "w-full" }));
68
+ },
69
+ });
70
+ const SelectItemUI = makeAssistantToolUI({
71
+ toolName: "ui_select_item",
72
+ render: ({ args, status }) => (_jsx(ToolCallCard, { icon: _jsx(MousePointerClick, { className: "size-3.5" }), title: "Select Item", description: status.type === "running"
73
+ ? "Selecting..."
74
+ : args?.itemId
75
+ ? `Selected: ${args.itemId}`
76
+ : "Selection cleared", status: status, className: "w-full" })),
77
+ });
78
+ const UpdateContentUI = makeAssistantToolUI({
79
+ toolName: "ui_update_content",
80
+ render: ({ args, status }) => {
81
+ const fieldNames = args?.fields ? Object.keys(args.fields).join(", ") : "";
82
+ return (_jsx(ToolCallCard, { icon: _jsx(PenLine, { className: "size-3.5" }), title: "Update Content", description: status.type === "running" ? "Updating..." : `Updated: ${fieldNames}`, status: status, className: "w-full" }));
83
+ },
84
+ });
85
+ const HighlightUI = makeAssistantToolUI({
86
+ toolName: "ui_highlight",
87
+ render: ({ args, status }) => (_jsx(ToolCallCard, { icon: _jsx(Highlighter, { className: "size-3.5" }), title: "Highlight", description: status.type === "running" ? "Highlighting..." : `Highlighted: ${args?.entityId}`, status: status, className: "w-full" })),
88
+ });
89
+ const ConfirmUI = makeAssistantToolUI({
90
+ toolName: "ui_confirm",
91
+ render: ({ args, result, status }) => {
92
+ if (result?.status === "pending") {
93
+ return (_jsx(ConfirmationCard, { action: args?.action ?? "Unknown action", details: args?.details, status: status }));
94
+ }
95
+ return (_jsx(ToolCallCard, { icon: _jsx(ShieldCheck, { className: "size-3.5" }), title: "Confirmation Required", description: args?.action ?? "Confirmation requested", status: status, className: "w-full" }));
96
+ },
97
+ });
98
+ const GetContextUI = makeAssistantToolUI({
99
+ toolName: "ui_get_context",
100
+ render: ({ status }) => (_jsx(ToolCallCard, { icon: _jsx(Info, { className: "size-3.5" }), title: "Get Context", description: status.type === "running" ? "Reading context..." : "Context loaded", status: status, className: "w-full" })),
101
+ });
102
+ export function ClientToolExecutor() {
103
+ const navigate = useNavigate();
104
+ useEffect(() => {
105
+ setNavigateFunction(navigate);
106
+ }, [navigate]);
107
+ return (_jsxs(_Fragment, { children: [_jsx(NavigateUI, {}), _jsx(SetThemeUI, {}), _jsx(OpenPanelUI, {}), _jsx(SelectItemUI, {}), _jsx(UpdateContentUI, {}), _jsx(HighlightUI, {}), _jsx(ConfirmUI, {}), _jsx(GetContextUI, {})] }));
108
+ }
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from "react";
2
+ import type { LucideIcon } from "lucide-react";
3
+ export declare function CollectionEmptyState({ icon: Icon, title, description, action, className, }: {
4
+ icon?: LucideIcon | null;
5
+ title: string;
6
+ description?: string;
7
+ action?: ReactNode;
8
+ className?: string;
9
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SearchX } from "lucide-react";
3
+ import { cn } from "@iloveagents/foundry-web-primitives";
4
+ export function CollectionEmptyState({ icon: Icon = SearchX, title, description, action, className, }) {
5
+ return (_jsxs("div", { className: cn("flex min-h-[18rem] flex-col items-center justify-center rounded-[1.75rem] bg-muted/18 px-6 py-10 text-center ring-1 ring-border/35", className), children: [Icon ? (_jsx("div", { className: "mb-4 flex size-14 items-center justify-center rounded-2xl bg-background/85 text-primary shadow-[0_8px_24px_-18px_rgb(15_23_42/0.35)]", children: _jsx(Icon, { className: "size-6" }) })) : null, _jsx("div", { className: "text-base font-semibold text-foreground", children: title }), description ? _jsx("p", { className: "mt-2 max-w-xl text-sm leading-6 text-muted-foreground", children: description }) : null, action ? _jsx("div", { className: "mt-5", children: action }) : null] }));
6
+ }
@@ -0,0 +1,19 @@
1
+ export type CollectionFilterValue = string | string[] | boolean | undefined;
2
+ export interface CollectionFilterOption {
3
+ value: string;
4
+ label: string;
5
+ }
6
+ export interface CollectionFilterDefinition {
7
+ key: string;
8
+ label: string;
9
+ type: "single" | "multi" | "boolean";
10
+ options?: CollectionFilterOption[];
11
+ trueLabel?: string;
12
+ }
13
+ export declare function CollectionFilterBar({ filters, values, onValueChange, onClearAll, className, }: {
14
+ filters: CollectionFilterDefinition[];
15
+ values: Record<string, CollectionFilterValue>;
16
+ onValueChange: (key: string, value: CollectionFilterValue) => void;
17
+ onClearAll?: () => void;
18
+ className?: string;
19
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Check, Filter, X } from "lucide-react";
3
+ import { Button } from "@iloveagents/foundry-web-primitives";
4
+ import { Checkbox } from "@iloveagents/foundry-web-primitives";
5
+ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
6
+ import { cn } from "@iloveagents/foundry-web-primitives";
7
+ function activeCount(value, type) {
8
+ if (type === "boolean")
9
+ return value ? 1 : 0;
10
+ if (Array.isArray(value))
11
+ return value.length;
12
+ return value ? 1 : 0;
13
+ }
14
+ export function CollectionFilterBar({ filters, values, onValueChange, onClearAll, className, }) {
15
+ const hasActiveFilters = filters.some((filter) => activeCount(values[filter.key], filter.type) > 0);
16
+ return (_jsxs("div", { className: cn("flex flex-wrap items-center gap-2", className), children: [filters.map((filter) => {
17
+ const count = activeCount(values[filter.key], filter.type);
18
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: cn("rounded-xl border-border/45 bg-background/65 shadow-none hover:bg-muted/26", count > 0 && "border-primary/18 bg-primary/7 text-foreground hover:bg-primary/10"), children: [_jsx(Filter, { className: "size-4" }), _jsx("span", { children: filter.label }), count > 0 ? (_jsx("span", { className: "rounded-full bg-primary/10 px-1.5 py-0.5 text-[11px] font-semibold text-primary", children: count })) : null] }) }), _jsxs(PopoverContent, { align: "start", className: "w-64 rounded-2xl border-border/45 bg-popover/95 p-2 shadow-lg", children: [_jsx("div", { className: "px-2 pb-2 pt-1 text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground", children: filter.label }), filter.type === "boolean" ? (_jsxs("button", { type: "button", onClick: () => onValueChange(filter.key, values[filter.key] ? undefined : true), className: cn("flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-sm transition-colors", values[filter.key] ? "bg-primary/8 text-foreground" : "hover:bg-accent"), children: [_jsx(Checkbox, { checked: Boolean(values[filter.key]) }), _jsx("span", { children: filter.trueLabel ?? `Only ${filter.label.toLowerCase()}` })] })) : null, filter.type === "single" ? (_jsxs("div", { className: "space-y-1", children: [_jsxs("button", { type: "button", onClick: () => onValueChange(filter.key, undefined), className: "flex w-full items-center justify-between rounded-xl px-3 py-2 text-left text-sm transition-colors hover:bg-accent", children: [_jsx("span", { children: "Any" }), _jsx(Check, { className: cn("size-4 text-primary", values[filter.key] == null ? "opacity-100" : "opacity-0") })] }), filter.options?.map((option) => (_jsxs("button", { type: "button", onClick: () => onValueChange(filter.key, option.value), className: "flex w-full items-center justify-between rounded-xl px-3 py-2 text-left text-sm transition-colors hover:bg-accent", children: [_jsx("span", { children: option.label }), _jsx(Check, { className: cn("size-4 text-primary", values[filter.key] === option.value ? "opacity-100" : "opacity-0") })] }, option.value)))] })) : null, filter.type === "multi" ? (_jsx("div", { className: "space-y-1", children: filter.options?.map((option) => {
19
+ const current = Array.isArray(values[filter.key])
20
+ ? values[filter.key]
21
+ : [];
22
+ const checked = current.includes(option.value);
23
+ return (_jsxs("button", { type: "button", onClick: () => onValueChange(filter.key, checked
24
+ ? current.filter((item) => item !== option.value)
25
+ : [...current, option.value]), className: cn("flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-sm transition-colors", checked ? "bg-primary/8 text-foreground" : "hover:bg-accent"), children: [_jsx(Checkbox, { checked: checked }), _jsx("span", { children: option.label })] }, option.value));
26
+ }) })) : null] })] }, filter.key));
27
+ }), hasActiveFilters && onClearAll ? (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: onClearAll, className: "rounded-xl text-muted-foreground hover:bg-muted/26", children: [_jsx(X, { className: "size-4" }), _jsx("span", { children: "Clear" })] })) : null] }));
28
+ }
@@ -0,0 +1,6 @@
1
+ export declare function CollectionSearchInput({ value, onValueChange, placeholder, className, }: {
2
+ value: string;
3
+ onValueChange: (value: string) => void;
4
+ placeholder?: string;
5
+ className?: string;
6
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Search, X } from "lucide-react";
3
+ import { Button } from "@iloveagents/foundry-web-primitives";
4
+ import { Input } from "@iloveagents/foundry-web-primitives";
5
+ import { cn } from "@iloveagents/foundry-web-primitives";
6
+ export function CollectionSearchInput({ value, onValueChange, placeholder = "Search...", className, }) {
7
+ return (_jsxs("div", { className: cn("relative min-w-[16rem] flex-1", className), children: [_jsx(Search, { className: "pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: value, onChange: (event) => onValueChange(event.target.value), placeholder: placeholder, className: "h-10 rounded-xl border-border/45 bg-background/72 pl-9 pr-10 shadow-none hover:bg-background/82 focus:border-primary/20 focus:ring-primary/10" }), value ? (_jsxs(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => onValueChange(""), className: "absolute right-1 top-1/2 size-8 -translate-y-1/2 rounded-lg text-muted-foreground", children: [_jsx(X, { className: "size-4" }), _jsx("span", { className: "sr-only", children: "Clear search" })] })) : null] }));
8
+ }
@@ -0,0 +1,6 @@
1
+ import type { CollectionViewMode } from "./collection-view-toggle.js";
2
+ export declare function CollectionSkeleton({ view, rows, className, }: {
3
+ view: CollectionViewMode;
4
+ rows?: number;
5
+ className?: string;
6
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "@iloveagents/foundry-web-primitives";
3
+ export function CollectionSkeleton({ view, rows = 8, className, }) {
4
+ if (view === "table") {
5
+ return (_jsxs("div", { className: cn("overflow-hidden rounded-[1.5rem] bg-background/65 ring-1 ring-border/35", className), children: [_jsx("div", { className: "grid grid-cols-4 gap-4 border-b border-border/40 bg-muted/28 px-5 py-4", children: Array.from({ length: 4 }).map((_, index) => (_jsx("div", { className: "h-3.5 animate-pulse rounded bg-muted" }, index))) }), _jsx("div", { className: "divide-y divide-border/35", children: Array.from({ length: rows }).map((_, index) => (_jsx("div", { className: "grid grid-cols-4 gap-4 px-5 py-4", children: Array.from({ length: 4 }).map((_, cellIndex) => (_jsx("div", { className: "h-4 animate-pulse rounded bg-muted/80" }, cellIndex))) }, index))) })] }));
6
+ }
7
+ return (_jsx("div", { className: cn(view === "tiles"
8
+ ? "grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr))]"
9
+ : "grid gap-4 [grid-template-columns:repeat(auto-fit,minmax(min(100%,18rem),1fr))]", className), children: Array.from({ length: rows }).map((_, index) => (_jsx("div", { className: cn("animate-pulse rounded-[1.5rem] bg-card/80 ring-1 ring-border/35", view === "tiles" ? "h-28" : "h-40") }, index))) }));
10
+ }
@@ -0,0 +1,10 @@
1
+ export interface CollectionSortOption {
2
+ value: string;
3
+ label: string;
4
+ }
5
+ export declare function CollectionSortMenu({ options, value, onValueChange, className, }: {
6
+ options: CollectionSortOption[];
7
+ value: string;
8
+ onValueChange: (value: string) => void;
9
+ className?: string;
10
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ArrowUpDown, Check } from "lucide-react";
3
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "../ui/dropdown-menu.js";
4
+ import { Button } from "@iloveagents/foundry-web-primitives";
5
+ import { cn } from "@iloveagents/foundry-web-primitives";
6
+ export function CollectionSortMenu({ options, value, onValueChange, className, }) {
7
+ const active = options.find((option) => option.value === value) ?? options[0];
8
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: cn("rounded-xl border-border/45 bg-background/65 shadow-none hover:bg-muted/26", className), children: [_jsx(ArrowUpDown, { className: "size-4" }), _jsx("span", { children: active?.label ?? "Sort" })] }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-56 rounded-xl border-border/45 bg-popover/95 shadow-lg", children: options.map((option) => (_jsxs(DropdownMenuItem, { onClick: () => onValueChange(option.value), className: "flex items-center justify-between gap-3", children: [_jsx("span", { children: option.label }), _jsx(Check, { className: cn("size-4 text-primary", value === option.value ? "opacity-100" : "opacity-0") })] }, option.value))) })] }));
9
+ }
@@ -0,0 +1,8 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ export declare function CollectionSurface({ toolbar, bordered, backgrounded, className, toolbarClassName, contentClassName, children, ...props }: ComponentPropsWithoutRef<"section"> & {
3
+ toolbar?: ReactNode;
4
+ bordered?: boolean;
5
+ backgrounded?: boolean;
6
+ toolbarClassName?: string;
7
+ contentClassName?: string;
8
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "@iloveagents/foundry-web-primitives";
3
+ export function CollectionSurface({ toolbar, bordered = false, backgrounded = true, className, toolbarClassName, contentClassName, children, ...props }) {
4
+ return (_jsxs("section", { className: cn("w-full min-w-0 overflow-hidden rounded-[1.75rem]", backgrounded && "bg-card/60 shadow-[0_12px_36px_-24px_rgb(15_23_42/0.18)] backdrop-blur-sm", bordered && "ring-1 ring-border/45", className), ...props, children: [toolbar ? (_jsx("div", { className: cn("sticky top-0 z-10 px-4 py-4 sm:px-5", backgrounded && "bg-muted/18 backdrop-blur", bordered && "border-b border-border/45", toolbarClassName), children: toolbar })) : null, _jsx("div", { className: cn("p-4 sm:p-5", contentClassName), children: children })] }));
5
+ }
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from "react";
2
+ export declare function CollectionToolbar({ search, filters, sort, viewToggle, actions, className, }: {
3
+ search?: ReactNode;
4
+ filters?: ReactNode;
5
+ sort?: ReactNode;
6
+ viewToggle?: ReactNode;
7
+ actions?: ReactNode;
8
+ className?: string;
9
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "@iloveagents/foundry-web-primitives";
3
+ export function CollectionToolbar({ search, filters, sort, viewToggle, actions, className, }) {
4
+ return (_jsxs("div", { className: cn("flex flex-col gap-2.5", className), children: [_jsxs("div", { className: "flex flex-col gap-2.5 lg:flex-row lg:items-center", children: [_jsx("div", { className: "flex min-w-0 flex-1 items-center gap-3", children: search }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [sort, viewToggle, actions] })] }), filters ? _jsx("div", { className: "flex flex-wrap items-center gap-2", children: filters }) : null] }));
5
+ }
@@ -0,0 +1,7 @@
1
+ export type CollectionViewMode = "table" | "grid" | "tiles";
2
+ export declare function CollectionViewToggle({ value, onValueChange, views, className, }: {
3
+ value: CollectionViewMode;
4
+ onValueChange: (value: CollectionViewMode) => void;
5
+ views?: CollectionViewMode[];
6
+ className?: string;
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { LayoutGrid, Rows3, TableProperties } from "lucide-react";
3
+ import { Button } from "@iloveagents/foundry-web-primitives";
4
+ import { cn } from "@iloveagents/foundry-web-primitives";
5
+ const VIEW_OPTIONS = [
6
+ { value: "table", label: "Table", icon: TableProperties },
7
+ { value: "grid", label: "Grid", icon: LayoutGrid },
8
+ { value: "tiles", label: "Tiles", icon: Rows3 },
9
+ ];
10
+ export function CollectionViewToggle({ value, onValueChange, views = ["table", "grid"], className, }) {
11
+ return (_jsx("div", { className: cn("inline-flex items-center rounded-xl bg-muted/26 p-1 ring-1 ring-border/40", className), children: VIEW_OPTIONS.filter((option) => views.includes(option.value)).map((option) => {
12
+ const Icon = option.icon;
13
+ const active = value === option.value;
14
+ return (_jsxs(Button, { type: "button", "aria-label": option.label, variant: active ? "secondary" : "ghost", size: "sm", onClick: () => onValueChange(option.value), className: cn("rounded-lg px-2.5 text-muted-foreground hover:bg-background/70 hover:text-foreground", active &&
15
+ "bg-background text-foreground ring-1 ring-primary/35 shadow-[0_1px_2px_rgb(15_23_42/0.08)] hover:bg-background"), children: [_jsx(Icon, { className: "size-4" }), _jsx("span", { className: "hidden sm:inline", children: option.label }), _jsx("span", { className: "sr-only sm:hidden", children: option.label })] }, option.value));
16
+ }) }));
17
+ }
@@ -0,0 +1 @@
1
+ export declare function ComposerSubmitBridge(): null;
@@ -0,0 +1,15 @@
1
+ import { useEffect } from "react";
2
+ import { useComposerRuntime } from "@assistant-ui/react";
3
+ import { useComposerSubmitStore } from "../lib/composer-submit-store.js";
4
+ export function ComposerSubmitBridge() {
5
+ const composerRuntime = useComposerRuntime();
6
+ const pending = useComposerSubmitStore((s) => s.pending);
7
+ useEffect(() => {
8
+ if (!pending)
9
+ return;
10
+ composerRuntime.setText(pending.text);
11
+ composerRuntime.send({ startRun: true });
12
+ useComposerSubmitStore.getState().consume(pending.id);
13
+ }, [composerRuntime, pending]);
14
+ return null;
15
+ }
@@ -0,0 +1,11 @@
1
+ export declare function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, cancelLabel, destructive, onConfirm, confirming, }: {
2
+ open: boolean;
3
+ onOpenChange: (open: boolean) => void;
4
+ title: string;
5
+ description?: string;
6
+ confirmLabel?: string;
7
+ cancelLabel?: string;
8
+ destructive?: boolean;
9
+ onConfirm: () => void;
10
+ confirming?: boolean;
11
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from "@iloveagents/foundry-web-primitives";
3
+ import { FormDialog } from "./form-dialog.js";
4
+ export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel = "Confirm", cancelLabel = "Cancel", destructive = false, onConfirm, confirming = false, }) {
5
+ return (_jsx(FormDialog, { open: open, onOpenChange: onOpenChange, title: title, description: description, size: "sm", footer: _jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: cancelLabel }), _jsx(Button, { type: "button", variant: destructive ? "destructive" : "default", onClick: onConfirm, disabled: confirming, children: confirmLabel })] }) }));
6
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Confirmation Card — informational UI for the ui_confirm tool.
3
+ *
4
+ * Renders a card with action description and optional details.
5
+ * The user responds by typing their confirmation in the chat.
6
+ */
7
+ import { type ToolCallStatus } from "./tool-call-card.js";
8
+ interface ConfirmationCardProps {
9
+ action: string;
10
+ details?: string;
11
+ status: ToolCallStatus;
12
+ }
13
+ export declare function ConfirmationCard({ action, details, status }: ConfirmationCardProps): import("react/jsx-runtime").JSX.Element;
14
+ export {};
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Confirmation Card — informational UI for the ui_confirm tool.
4
+ *
5
+ * Renders a card with action description and optional details.
6
+ * The user responds by typing their confirmation in the chat.
7
+ */
8
+ import { ShieldCheck } from "lucide-react";
9
+ import { ToolCallCard } from "./tool-call-card.js";
10
+ export function ConfirmationCard({ action, details, status }) {
11
+ return (_jsxs(ToolCallCard, { icon: _jsx(ShieldCheck, { className: "size-3.5" }), title: "Confirmation Required", description: action, status: status, className: "w-full", children: [details && _jsx("p", { className: "text-xs text-muted-foreground mt-1", children: details }), _jsx("p", { className: "text-xs text-muted-foreground mt-2 italic", children: "Reply to confirm or reject this action." })] }));
12
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Context badge components for the composer and sent messages.
3
+ *
4
+ * Renders user-selected context items as violet-tinted badges,
5
+ * visually distinct from gray file attachment badges.
6
+ *
7
+ * - ComposerContextBadges: editable badges in the composer (with remove button)
8
+ * - SentContextBadges: read-only badges on sent user messages
9
+ */
10
+ import { type FC } from "react";
11
+ /** Renders ephemeral context badges in the composer (persistent items shown in ContextPins). */
12
+ export declare const ComposerContextBadges: FC;
13
+ /** Renders sent context badges on a user message (read-only). */
14
+ export declare const SentContextBadges: FC<{
15
+ messageId: string;
16
+ }>;
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Context badge components for the composer and sent messages.
4
+ *
5
+ * Renders user-selected context items as violet-tinted badges,
6
+ * visually distinct from gray file attachment badges.
7
+ *
8
+ * - ComposerContextBadges: editable badges in the composer (with remove button)
9
+ * - SentContextBadges: read-only badges on sent user messages
10
+ */
11
+ import { createElement } from "react";
12
+ import { Pin, TextSelect, X, FileText, Link2, StickyNote } from "lucide-react";
13
+ import { cn } from "@iloveagents/foundry-web-primitives";
14
+ import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip.js";
15
+ import { useAppStore } from "../lib/app-store.js";
16
+ const CONTEXT_ICONS = {
17
+ selection: TextSelect,
18
+ page: FileText,
19
+ ref: Link2,
20
+ note: StickyNote,
21
+ };
22
+ function ContextIcon({ type, className }) {
23
+ return createElement(CONTEXT_ICONS[type] ?? TextSelect, { className });
24
+ }
25
+ /** Get tooltip text for a context item (source page or path). */
26
+ function getBadgeTooltip(item) {
27
+ const source = item.type === "page" && item.payload.kind === "page" ? item.payload.path : item.sourcePage;
28
+ return `${item.label}\n${source}`;
29
+ }
30
+ /** Editable badge in the composer with a remove button. */
31
+ const ComposerBadge = ({ item }) => {
32
+ const removeItem = useAppStore((s) => s.removeContextItem);
33
+ const promoteItem = useAppStore((s) => s.promoteContextItem);
34
+ const isEphemeral = item.persistence === "ephemeral";
35
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: cn("inline-flex items-center gap-1.5 max-w-70", "rounded-lg bg-primary/5", "px-2 py-1", isEphemeral ? "border border-dashed border-primary/40" : "border border-primary/20"), children: [_jsx(ContextIcon, { type: item.type, className: "size-3 text-primary shrink-0" }), _jsx("span", { className: "text-xs text-foreground truncate", children: item.label }), isEphemeral && (_jsx("button", { type: "button", onClick: () => promoteItem(item.id), className: "p-0.5 rounded hover:bg-primary/10 shrink-0", "aria-label": "Pin context", children: _jsx(Pin, { className: "size-3 text-muted-foreground" }) })), _jsx("button", { type: "button", onClick: () => removeItem(item.id), className: "p-0.5 rounded hover:bg-primary/10 shrink-0", "aria-label": "Remove context", children: _jsx(X, { className: "size-3" }) })] }) }), _jsx(TooltipContent, { side: "top", className: "whitespace-pre-line", children: getBadgeTooltip(item) })] }));
36
+ };
37
+ /** Read-only badge on a sent user message. Mirrors composer context chips. */
38
+ const SentBadge = ({ item }) => {
39
+ const isEphemeral = item.persistence === "ephemeral";
40
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: cn("inline-flex items-center gap-1.5 max-w-70", "rounded-lg bg-primary/5", "px-2 py-1", isEphemeral ? "border border-dashed border-primary/40" : "border border-primary/20"), children: [_jsx(ContextIcon, { type: item.type, className: "size-3 text-primary shrink-0" }), _jsx("span", { className: "text-[11px] leading-tight text-foreground truncate", children: item.label })] }) }), _jsx(TooltipContent, { side: "top", className: "whitespace-pre-line", children: getBadgeTooltip(item) })] }));
41
+ };
42
+ /** Renders ephemeral context badges in the composer (persistent items shown in ContextPins). */
43
+ export const ComposerContextBadges = () => {
44
+ const items = useAppStore((s) => s.contextItems);
45
+ const ephemeral = items.filter((i) => i.persistence === "ephemeral");
46
+ if (ephemeral.length === 0)
47
+ return null;
48
+ return (_jsx("div", { className: "flex flex-wrap gap-1.5", children: ephemeral.map((item) => (_jsx(ComposerBadge, { item: item }, item.id))) }));
49
+ };
50
+ /** Renders sent context badges on a user message (read-only). */
51
+ export const SentContextBadges = ({ messageId }) => {
52
+ const items = useAppStore((s) => s.sentContext[messageId]);
53
+ if (!items || items.length === 0)
54
+ return null;
55
+ return (_jsx("div", { className: "flex flex-wrap gap-1 mb-1", children: items.map((item) => (_jsx(SentBadge, { item: item }, item.id))) }));
56
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Context Pins — pin button with count badge and hover flyout.
3
+ *
4
+ * Click: toggles pin/unpin current page.
5
+ * Hover (when pins exist): shows flyout to view, navigate, and remove pins.
6
+ */
7
+ import { type FC } from "react";
8
+ import { type ContextItem } from "../lib/app-store.js";
9
+ interface ContextPinsProps {
10
+ /** Show the pin button to add current page */
11
+ showPinButton?: boolean;
12
+ /** Compact mode for small containers (mini chat bubble header) */
13
+ compact?: boolean;
14
+ /** Direction flyout opens: up (composer) or down (header) */
15
+ flyoutDirection?: "up" | "down";
16
+ }
17
+ /** Resolve the URL a context item navigates to when clicked.
18
+ *
19
+ * Two shapes the framework knows about:
20
+ *
21
+ * 1. **Page** (``type: "page"``) — the canonical case the OSS shell
22
+ * creates for "Pin this page". The payload's ``path`` IS the
23
+ * navigation target.
24
+ * 2. **Ref** (``type: "ref"``) — created by feature modules when the
25
+ * user pins a structured reference (Spaces blocks, code symbols,
26
+ * DB rows, …). The framework stays agnostic about the ref shape;
27
+ * the source module places the canonical deep-link URL — including
28
+ * any anchor hash — in the chip's ``sourcePage`` field, which is
29
+ * already the conventional "where the user pinned from" breadcrumb.
30
+ * Surfacing it here makes every feature module's ref chips clickable
31
+ * without an extension-point registry, and without the framework
32
+ * knowing module-specific URL shapes.
33
+ *
34
+ * Other item types (``selection``, ``note``) intentionally stay
35
+ * non-clickable — they are excerpts / annotations, not anchors.
36
+ *
37
+ * @internal Exported for unit tests; not part of the public API.
38
+ */
39
+ export declare function getNavigablePath(item: ContextItem): string | null;
40
+ export declare const ContextPins: FC<ContextPinsProps>;
41
+ export {};
@@ -0,0 +1,123 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Context Pins — pin button with count badge and hover flyout.
4
+ *
5
+ * Click: toggles pin/unpin current page.
6
+ * Hover (when pins exist): shows flyout to view, navigate, and remove pins.
7
+ */
8
+ import { useState, useRef, useEffect, createElement } from "react";
9
+ import { useNavigate } from "react-router";
10
+ import { X, Pin, PinOff, TextSelect, FileText, Link2, StickyNote, } from "lucide-react";
11
+ import { cn } from "@iloveagents/foundry-web-primitives";
12
+ import { TooltipIconButton } from "./tooltip-icon-button.js";
13
+ import { useAppStore } from "../lib/app-store.js";
14
+ const CONTEXT_ICONS = {
15
+ selection: TextSelect,
16
+ page: FileText,
17
+ ref: Link2,
18
+ note: StickyNote,
19
+ };
20
+ /** Resolve the URL a context item navigates to when clicked.
21
+ *
22
+ * Two shapes the framework knows about:
23
+ *
24
+ * 1. **Page** (``type: "page"``) — the canonical case the OSS shell
25
+ * creates for "Pin this page". The payload's ``path`` IS the
26
+ * navigation target.
27
+ * 2. **Ref** (``type: "ref"``) — created by feature modules when the
28
+ * user pins a structured reference (Spaces blocks, code symbols,
29
+ * DB rows, …). The framework stays agnostic about the ref shape;
30
+ * the source module places the canonical deep-link URL — including
31
+ * any anchor hash — in the chip's ``sourcePage`` field, which is
32
+ * already the conventional "where the user pinned from" breadcrumb.
33
+ * Surfacing it here makes every feature module's ref chips clickable
34
+ * without an extension-point registry, and without the framework
35
+ * knowing module-specific URL shapes.
36
+ *
37
+ * Other item types (``selection``, ``note``) intentionally stay
38
+ * non-clickable — they are excerpts / annotations, not anchors.
39
+ *
40
+ * @internal Exported for unit tests; not part of the public API.
41
+ */
42
+ export function getNavigablePath(item) {
43
+ if (item.type === "page" && item.payload.kind === "page")
44
+ return item.payload.path;
45
+ if (item.type === "ref" &&
46
+ item.payload.kind === "ref" &&
47
+ typeof item.sourcePage === "string" &&
48
+ // Guard: ``sourcePage`` is also used as a free-form breadcrumb
49
+ // label by some callers (e.g. "page-label" instead of "/page").
50
+ // Treat it as a navigation target only when it looks like an
51
+ // in-app path. Hash-only links (``#anchor``) are also valid —
52
+ // they keep the user on the current entity but scroll to the
53
+ // pinned atom (e.g. Spaces ``#block-{id}`` deep-link from the
54
+ // current page).
55
+ (item.sourcePage.startsWith("/") || item.sourcePage.startsWith("#"))) {
56
+ return item.sourcePage;
57
+ }
58
+ return null;
59
+ }
60
+ export const ContextPins = ({ showPinButton = false, compact = false, flyoutDirection = "up", }) => {
61
+ const items = useAppStore((s) => s.contextItems);
62
+ const removeItem = useAppStore((s) => s.removeContextItem);
63
+ const addContextItem = useAppStore((s) => s.addContextItem);
64
+ const currentPage = useAppStore((s) => s.currentPage);
65
+ const pageLabel = useAppStore((s) => s.navContext.label);
66
+ const navMeta = useAppStore((s) => s.navContext.meta);
67
+ const navigate = useNavigate();
68
+ const [flyoutOpen, setFlyoutOpen] = useState(false);
69
+ const containerRef = useRef(null);
70
+ const hoverTimeout = useRef(undefined);
71
+ const pinned = items.filter((i) => i.persistence === "persistent");
72
+ // Check if current page is already pinned
73
+ const isCurrentPagePinned = pinned.some((i) => i.type === "page" && i.payload.kind === "page" && i.payload.path === currentPage);
74
+ // Find the pin item for current page (to unpin)
75
+ const currentPagePin = pinned.find((i) => i.type === "page" && i.payload.kind === "page" && i.payload.path === currentPage);
76
+ // Close flyout on outside click
77
+ useEffect(() => {
78
+ if (!flyoutOpen)
79
+ return;
80
+ const handleClick = (e) => {
81
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
82
+ setFlyoutOpen(false);
83
+ }
84
+ };
85
+ document.addEventListener("mousedown", handleClick);
86
+ return () => document.removeEventListener("mousedown", handleClick);
87
+ }, [flyoutOpen]);
88
+ const handleTogglePin = () => {
89
+ if (isCurrentPagePinned && currentPagePin) {
90
+ removeItem(currentPagePin.id);
91
+ }
92
+ else {
93
+ addContextItem({
94
+ type: "page",
95
+ label: pageLabel,
96
+ payload: { kind: "page", path: currentPage, label: pageLabel, meta: navMeta },
97
+ sourcePage: currentPage,
98
+ persistence: "persistent",
99
+ });
100
+ }
101
+ };
102
+ const handleMouseEnter = () => {
103
+ clearTimeout(hoverTimeout.current);
104
+ if (pinned.length > 0)
105
+ setFlyoutOpen(true);
106
+ };
107
+ const handleMouseLeave = () => {
108
+ hoverTimeout.current = setTimeout(() => setFlyoutOpen(false), 200);
109
+ };
110
+ if (pinned.length === 0 && !showPinButton)
111
+ return null;
112
+ return (_jsxs("div", { className: "relative", ref: containerRef, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [_jsxs(TooltipIconButton, { tooltip: isCurrentPagePinned ? "Unpin this page" : "Pin this page", size: "icon", className: cn("shrink-0 relative", compact ? "size-7" : "size-8", isCurrentPagePinned && "text-primary"), onClick: handleTogglePin, children: [isCurrentPagePinned ? (_jsx(PinOff, { className: compact ? "size-3.5" : "size-4" })) : (_jsx(Pin, { className: compact ? "size-3.5" : "size-4" })), pinned.length > 0 && (_jsx("span", { className: cn("absolute rounded-full bg-primary font-medium text-primary-foreground flex items-center justify-center", compact
113
+ ? "-top-0.5 -right-0.5 size-3.5 text-[9px]"
114
+ : "top-0 right-0 size-3.5 text-[9px]"), children: pinned.length }))] }), flyoutOpen && pinned.length > 0 && (_jsxs("div", { className: cn("absolute z-50 w-64 rounded-lg border border-border bg-background shadow-xl p-2 space-y-1", flyoutDirection === "up" ? "bottom-full mb-2 left-0" : "top-9 right-0"), children: [_jsx("div", { className: "px-1 pb-1 border-b border-border/50", children: _jsx("span", { className: "text-[10px] font-medium text-muted-foreground uppercase tracking-wide", children: "Pinned Context" }) }), pinned.map((item) => {
115
+ const navPath = getNavigablePath(item);
116
+ return (_jsxs("div", { className: "flex items-center gap-1.5 px-1 py-1 rounded hover:bg-muted group/pin", children: [createElement(CONTEXT_ICONS[item.type] ?? TextSelect, {
117
+ className: "size-3 text-primary shrink-0",
118
+ }), navPath ? (_jsx("button", { type: "button", onClick: () => {
119
+ navigate(navPath);
120
+ setFlyoutOpen(false);
121
+ }, className: "text-xs text-foreground truncate flex-1 text-left hover:underline", children: item.label })) : (_jsx("span", { className: "text-xs text-foreground truncate flex-1", children: item.label })), _jsx("button", { type: "button", onClick: () => removeItem(item.id), className: cn("p-0.5 rounded hover:bg-primary/10 transition-opacity shrink-0", "opacity-0 group-hover/pin:opacity-100"), "aria-label": `Remove ${item.label}`, children: _jsx(X, { className: "size-3 text-muted-foreground" }) })] }, item.id));
122
+ })] }))] }));
123
+ };