@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
@@ -1,136 +0,0 @@
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
-
11
- import { type FC, createElement } from "react";
12
- import { Pin, TextSelect, X, FileText, Link2, StickyNote, type LucideIcon } from "lucide-react";
13
- import { cn } from "@iloveagents/foundry-web-primitives";
14
- import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip.tsx";
15
- import { useAppStore, type ContextItem, type ContextItemType } from "../lib/app-store.ts";
16
-
17
- const CONTEXT_ICONS: Record<ContextItemType, LucideIcon> = {
18
- selection: TextSelect,
19
- page: FileText,
20
- ref: Link2,
21
- note: StickyNote,
22
- };
23
-
24
- function ContextIcon({ type, className }: { type: ContextItemType; className?: string }) {
25
- return createElement(CONTEXT_ICONS[type] ?? TextSelect, { className });
26
- }
27
-
28
- /** Get tooltip text for a context item (source page or path). */
29
- function getBadgeTooltip(item: ContextItem): string {
30
- const source =
31
- item.type === "page" && item.payload.kind === "page" ? item.payload.path : item.sourcePage;
32
- return `${item.label}\n${source}`;
33
- }
34
-
35
- /** Editable badge in the composer with a remove button. */
36
- const ComposerBadge: FC<{ item: ContextItem }> = ({ item }) => {
37
- const removeItem = useAppStore((s) => s.removeContextItem);
38
- const promoteItem = useAppStore((s) => s.promoteContextItem);
39
- const isEphemeral = item.persistence === "ephemeral";
40
-
41
- return (
42
- <Tooltip>
43
- <TooltipTrigger asChild>
44
- <div
45
- className={cn(
46
- "inline-flex items-center gap-1.5 max-w-70",
47
- "rounded-lg bg-primary/5",
48
- "px-2 py-1",
49
- isEphemeral ? "border border-dashed border-primary/40" : "border border-primary/20",
50
- )}
51
- >
52
- <ContextIcon type={item.type} className="size-3 text-primary shrink-0" />
53
- <span className="text-xs text-foreground truncate">{item.label}</span>
54
- {isEphemeral && (
55
- <button
56
- type="button"
57
- onClick={() => promoteItem(item.id)}
58
- className="p-0.5 rounded hover:bg-primary/10 shrink-0"
59
- aria-label="Pin context"
60
- >
61
- <Pin className="size-3 text-muted-foreground" />
62
- </button>
63
- )}
64
- <button
65
- type="button"
66
- onClick={() => removeItem(item.id)}
67
- className="p-0.5 rounded hover:bg-primary/10 shrink-0"
68
- aria-label="Remove context"
69
- >
70
- <X className="size-3" />
71
- </button>
72
- </div>
73
- </TooltipTrigger>
74
- <TooltipContent side="top" className="whitespace-pre-line">
75
- {getBadgeTooltip(item)}
76
- </TooltipContent>
77
- </Tooltip>
78
- );
79
- };
80
-
81
- /** Read-only badge on a sent user message. Mirrors composer context chips. */
82
- const SentBadge: FC<{ item: ContextItem }> = ({ item }) => {
83
- const isEphemeral = item.persistence === "ephemeral";
84
-
85
- return (
86
- <Tooltip>
87
- <TooltipTrigger asChild>
88
- <div
89
- className={cn(
90
- "inline-flex items-center gap-1.5 max-w-70",
91
- "rounded-lg bg-primary/5",
92
- "px-2 py-1",
93
- isEphemeral ? "border border-dashed border-primary/40" : "border border-primary/20",
94
- )}
95
- >
96
- <ContextIcon type={item.type} className="size-3 text-primary shrink-0" />
97
- <span className="text-[11px] leading-tight text-foreground truncate">{item.label}</span>
98
- </div>
99
- </TooltipTrigger>
100
- <TooltipContent side="top" className="whitespace-pre-line">
101
- {getBadgeTooltip(item)}
102
- </TooltipContent>
103
- </Tooltip>
104
- );
105
- };
106
-
107
- /** Renders ephemeral context badges in the composer (persistent items shown in ContextPins). */
108
- export const ComposerContextBadges: FC = () => {
109
- const items = useAppStore((s) => s.contextItems);
110
- const ephemeral = items.filter((i) => i.persistence === "ephemeral");
111
-
112
- if (ephemeral.length === 0) return null;
113
-
114
- return (
115
- <div className="flex flex-wrap gap-1.5">
116
- {ephemeral.map((item) => (
117
- <ComposerBadge key={item.id} item={item} />
118
- ))}
119
- </div>
120
- );
121
- };
122
-
123
- /** Renders sent context badges on a user message (read-only). */
124
- export const SentContextBadges: FC<{ messageId: string }> = ({ messageId }) => {
125
- const items = useAppStore((s) => s.sentContext[messageId]);
126
-
127
- if (!items || items.length === 0) return null;
128
-
129
- return (
130
- <div className="flex flex-wrap gap-1 mb-1">
131
- {items.map((item) => (
132
- <SentBadge key={item.id} item={item} />
133
- ))}
134
- </div>
135
- );
136
- };
@@ -1,238 +0,0 @@
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
-
8
- import { type FC, useState, useRef, useEffect, createElement } from "react";
9
- import { useNavigate } from "react-router";
10
- import {
11
- X,
12
- Pin,
13
- PinOff,
14
- TextSelect,
15
- FileText,
16
- Link2,
17
- StickyNote,
18
- type LucideIcon,
19
- } from "lucide-react";
20
- import { cn } from "@iloveagents/foundry-web-primitives";
21
- import { TooltipIconButton } from "./tooltip-icon-button.tsx";
22
- import { useAppStore, type ContextItem, type ContextItemType } from "../lib/app-store.ts";
23
-
24
- const CONTEXT_ICONS: Record<ContextItemType, LucideIcon> = {
25
- selection: TextSelect,
26
- page: FileText,
27
- ref: Link2,
28
- note: StickyNote,
29
- };
30
-
31
- interface ContextPinsProps {
32
- /** Show the pin button to add current page */
33
- showPinButton?: boolean;
34
- /** Compact mode for small containers (mini chat bubble header) */
35
- compact?: boolean;
36
- /** Direction flyout opens: up (composer) or down (header) */
37
- flyoutDirection?: "up" | "down";
38
- }
39
-
40
- /** Resolve the URL a context item navigates to when clicked.
41
- *
42
- * Two shapes the framework knows about:
43
- *
44
- * 1. **Page** (``type: "page"``) — the canonical case the OSS shell
45
- * creates for "Pin this page". The payload's ``path`` IS the
46
- * navigation target.
47
- * 2. **Ref** (``type: "ref"``) — created by feature modules when the
48
- * user pins a structured reference (Spaces blocks, code symbols,
49
- * DB rows, …). The framework stays agnostic about the ref shape;
50
- * the source module places the canonical deep-link URL — including
51
- * any anchor hash — in the chip's ``sourcePage`` field, which is
52
- * already the conventional "where the user pinned from" breadcrumb.
53
- * Surfacing it here makes every feature module's ref chips clickable
54
- * without an extension-point registry, and without the framework
55
- * knowing module-specific URL shapes.
56
- *
57
- * Other item types (``selection``, ``note``) intentionally stay
58
- * non-clickable — they are excerpts / annotations, not anchors.
59
- *
60
- * @internal Exported for unit tests; not part of the public API.
61
- */
62
- export function getNavigablePath(item: ContextItem): string | null {
63
- if (item.type === "page" && item.payload.kind === "page") return item.payload.path;
64
- if (
65
- item.type === "ref" &&
66
- item.payload.kind === "ref" &&
67
- typeof item.sourcePage === "string" &&
68
- // Guard: ``sourcePage`` is also used as a free-form breadcrumb
69
- // label by some callers (e.g. "page-label" instead of "/page").
70
- // Treat it as a navigation target only when it looks like an
71
- // in-app path. Hash-only links (``#anchor``) are also valid —
72
- // they keep the user on the current entity but scroll to the
73
- // pinned atom (e.g. Spaces ``#block-{id}`` deep-link from the
74
- // current page).
75
- (item.sourcePage.startsWith("/") || item.sourcePage.startsWith("#"))
76
- ) {
77
- return item.sourcePage;
78
- }
79
- return null;
80
- }
81
-
82
- export const ContextPins: FC<ContextPinsProps> = ({
83
- showPinButton = true,
84
- compact = false,
85
- flyoutDirection = "up",
86
- }) => {
87
- const items = useAppStore((s) => s.contextItems);
88
- const removeItem = useAppStore((s) => s.removeContextItem);
89
- const addContextItem = useAppStore((s) => s.addContextItem);
90
- const currentPage = useAppStore((s) => s.currentPage);
91
- const pageLabel = useAppStore((s) => s.navContext.label);
92
- const navMeta = useAppStore((s) => s.navContext.meta);
93
- const navigate = useNavigate();
94
- const [flyoutOpen, setFlyoutOpen] = useState(false);
95
- const containerRef = useRef<HTMLDivElement>(null);
96
- const hoverTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
97
-
98
- const pinned = items.filter((i) => i.persistence === "persistent");
99
-
100
- // Check if current page is already pinned
101
- const isCurrentPagePinned = pinned.some(
102
- (i) => i.type === "page" && i.payload.kind === "page" && i.payload.path === currentPage,
103
- );
104
-
105
- // Find the pin item for current page (to unpin)
106
- const currentPagePin = pinned.find(
107
- (i) => i.type === "page" && i.payload.kind === "page" && i.payload.path === currentPage,
108
- );
109
-
110
- // Close flyout on outside click
111
- useEffect(() => {
112
- if (!flyoutOpen) return;
113
- const handleClick = (e: MouseEvent) => {
114
- if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
115
- setFlyoutOpen(false);
116
- }
117
- };
118
- document.addEventListener("mousedown", handleClick);
119
- return () => document.removeEventListener("mousedown", handleClick);
120
- }, [flyoutOpen]);
121
-
122
- const handleTogglePin = () => {
123
- if (isCurrentPagePinned && currentPagePin) {
124
- removeItem(currentPagePin.id);
125
- } else {
126
- addContextItem({
127
- type: "page",
128
- label: pageLabel,
129
- payload: { kind: "page", path: currentPage, label: pageLabel, meta: navMeta },
130
- sourcePage: currentPage,
131
- persistence: "persistent",
132
- });
133
- }
134
- };
135
-
136
- const handleMouseEnter = () => {
137
- clearTimeout(hoverTimeout.current);
138
- if (pinned.length > 0) setFlyoutOpen(true);
139
- };
140
-
141
- const handleMouseLeave = () => {
142
- hoverTimeout.current = setTimeout(() => setFlyoutOpen(false), 200);
143
- };
144
-
145
- if (pinned.length === 0 && !showPinButton) return null;
146
-
147
- return (
148
- <div
149
- className="relative"
150
- ref={containerRef}
151
- onMouseEnter={handleMouseEnter}
152
- onMouseLeave={handleMouseLeave}
153
- >
154
- <TooltipIconButton
155
- tooltip={isCurrentPagePinned ? "Unpin this page" : "Pin this page"}
156
- size="icon"
157
- className={cn(
158
- "shrink-0 relative",
159
- compact ? "size-7" : "size-8",
160
- isCurrentPagePinned && "text-primary",
161
- )}
162
- onClick={handleTogglePin}
163
- >
164
- {isCurrentPagePinned ? (
165
- <PinOff className={compact ? "size-3.5" : "size-4"} />
166
- ) : (
167
- <Pin className={compact ? "size-3.5" : "size-4"} />
168
- )}
169
- {pinned.length > 0 && (
170
- <span
171
- className={cn(
172
- "absolute rounded-full bg-primary font-medium text-primary-foreground flex items-center justify-center",
173
- compact
174
- ? "-top-0.5 -right-0.5 size-3.5 text-[9px]"
175
- : "top-0 right-0 size-3.5 text-[9px]",
176
- )}
177
- >
178
- {pinned.length}
179
- </span>
180
- )}
181
- </TooltipIconButton>
182
-
183
- {/* Hover flyout */}
184
- {flyoutOpen && pinned.length > 0 && (
185
- <div
186
- className={cn(
187
- "absolute z-50 w-64 rounded-lg border border-border bg-background shadow-xl p-2 space-y-1",
188
- flyoutDirection === "up" ? "bottom-full mb-2 left-0" : "top-9 right-0",
189
- )}
190
- >
191
- <div className="px-1 pb-1 border-b border-border/50">
192
- <span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wide">
193
- Pinned Context
194
- </span>
195
- </div>
196
- {pinned.map((item) => {
197
- const navPath = getNavigablePath(item);
198
- return (
199
- <div
200
- key={item.id}
201
- className="flex items-center gap-1.5 px-1 py-1 rounded hover:bg-muted group/pin"
202
- >
203
- {createElement(CONTEXT_ICONS[item.type] ?? TextSelect, {
204
- className: "size-3 text-primary shrink-0",
205
- })}
206
- {navPath ? (
207
- <button
208
- type="button"
209
- onClick={() => {
210
- navigate(navPath);
211
- setFlyoutOpen(false);
212
- }}
213
- className="text-xs text-foreground truncate flex-1 text-left hover:underline"
214
- >
215
- {item.label}
216
- </button>
217
- ) : (
218
- <span className="text-xs text-foreground truncate flex-1">{item.label}</span>
219
- )}
220
- <button
221
- type="button"
222
- onClick={() => removeItem(item.id)}
223
- className={cn(
224
- "p-0.5 rounded hover:bg-primary/10 transition-opacity shrink-0",
225
- "opacity-0 group-hover/pin:opacity-100",
226
- )}
227
- aria-label={`Remove ${item.label}`}
228
- >
229
- <X className="size-3 text-muted-foreground" />
230
- </button>
231
- </div>
232
- );
233
- })}
234
- </div>
235
- )}
236
- </div>
237
- );
238
- };
@@ -1,56 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- import {
4
- Dialog,
5
- DialogContent,
6
- DialogDescription,
7
- DialogFooter,
8
- DialogHeader,
9
- DialogTitle,
10
- } from "../ui/dialog.tsx";
11
- import { cn } from "@iloveagents/foundry-web-primitives";
12
-
13
- export function FormDialog({
14
- open,
15
- onOpenChange,
16
- title,
17
- description,
18
- children,
19
- footer,
20
- contentClassName,
21
- bodyClassName,
22
- size = "md",
23
- }: {
24
- open: boolean;
25
- onOpenChange: (open: boolean) => void;
26
- title: ReactNode;
27
- description?: ReactNode;
28
- children?: ReactNode;
29
- footer?: ReactNode;
30
- contentClassName?: string;
31
- bodyClassName?: string;
32
- size?: "sm" | "md" | "lg" | "xl" | "2xl";
33
- }) {
34
- const sizeClassName = {
35
- sm: "max-w-sm",
36
- md: "max-w-lg",
37
- lg: "max-w-xl",
38
- xl: "max-w-2xl",
39
- "2xl": "max-w-3xl",
40
- }[size];
41
-
42
- return (
43
- <Dialog open={open} onOpenChange={onOpenChange}>
44
- <DialogContent className={cn(sizeClassName, contentClassName)}>
45
- <DialogHeader>
46
- <DialogTitle>{title}</DialogTitle>
47
- <DialogDescription className={description ? undefined : "sr-only"}>
48
- {description ?? "Complete the form below."}
49
- </DialogDescription>
50
- </DialogHeader>
51
- {children != null ? <div className={bodyClassName}>{children}</div> : null}
52
- {footer ? <DialogFooter>{footer}</DialogFooter> : null}
53
- </DialogContent>
54
- </Dialog>
55
- );
56
- }
@@ -1,266 +0,0 @@
1
- /**
2
- * Global selection command bar for text selection anywhere on the page.
3
- *
4
- * Appears near selected text when the user makes a selection outside of
5
- * input elements and the tool panel (which has its own SelectionPopover).
6
- */
7
-
8
- import { type FormEvent, useState, useEffect, useCallback, useRef } from "react";
9
- import { PencilLine, Send, TextSelect, X } from "lucide-react";
10
- import { cn } from "@iloveagents/foundry-web-primitives";
11
- import { useAppStore } from "../lib/app-store.ts";
12
- import { useChatBubbleStore } from "../lib/chat-bubble-store.ts";
13
- import { submitComposerText } from "../lib/composer-submit-store.ts";
14
- import {
15
- resolveSelectionContext,
16
- type SelectionContextResult,
17
- } from "../lib/selection-context.ts";
18
-
19
- const ACTION_POPOVER_HEIGHT = 38;
20
- const SIMPLE_POPOVER_WIDTH = 126;
21
- const ACTION_POPOVER_WIDTH = 236;
22
- const MODIFY_POPOVER_WIDTH = 340;
23
- const OFFSET = 8;
24
- const DEFAULT_MODIFY_PROMPT_PREFIX = "Modify the selected content:";
25
-
26
- /** Elements where selection should NOT trigger the popover */
27
- function isEditableTarget(node: Node): boolean {
28
- const el = node instanceof HTMLElement ? node : node.parentElement;
29
- if (!el) return false;
30
- const tag = el.tagName;
31
- if (tag === "INPUT" || tag === "TEXTAREA") return true;
32
- if (el.isContentEditable) return true;
33
- return false;
34
- }
35
-
36
- export function GlobalSelectionPopover() {
37
- const [position, setPosition] = useState<{ top: number; left: number } | null>(null);
38
- const [selectedText, setSelectedText] = useState("");
39
- const [selectionContext, setSelectionContext] = useState<SelectionContextResult | null>(null);
40
- const [mode, setMode] = useState<"actions" | "modify">("actions");
41
- const [modifyPrompt, setModifyPrompt] = useState("");
42
- const addContextItem = useAppStore((s) => s.addContextItem);
43
- const popoverRef = useRef<HTMLElement | null>(null);
44
- const inputRef = useRef<HTMLInputElement>(null);
45
-
46
- const setPopoverNode = useCallback((node: HTMLElement | null) => {
47
- popoverRef.current = node;
48
- }, []);
49
-
50
- const hidePopover = useCallback(() => {
51
- setPosition(null);
52
- setSelectedText("");
53
- setSelectionContext(null);
54
- setMode("actions");
55
- setModifyPrompt("");
56
- }, []);
57
-
58
- const handleMouseUp = useCallback((event: MouseEvent) => {
59
- if (popoverRef.current?.contains(event.target as Node)) return;
60
-
61
- requestAnimationFrame(() => {
62
- const selection = window.getSelection();
63
- if (!selection || selection.isCollapsed || !selection.toString().trim()) return;
64
-
65
- const anchorNode = selection.anchorNode;
66
- if (!anchorNode) return;
67
-
68
- // Skip if inside editable element
69
- if (isEditableTarget(anchorNode)) return;
70
-
71
- // Skip if inside tool panel (it has its own SelectionPopover)
72
- const el = anchorNode instanceof HTMLElement ? anchorNode : anchorNode.parentElement;
73
- if (el?.closest("[data-tool-panel]")) return;
74
-
75
- const text = selection.toString().trim();
76
- if (!text) return;
77
-
78
- const range = selection.getRangeAt(0);
79
- const rect = range.getBoundingClientRect();
80
- const sourcePage = useAppStore.getState().navContext.label;
81
- const resolved = resolveSelectionContext({
82
- text,
83
- anchorNode,
84
- focusNode: selection.focusNode,
85
- range,
86
- sourcePage,
87
- });
88
- const width = resolved.supportsModify ? ACTION_POPOVER_WIDTH : SIMPLE_POPOVER_WIDTH;
89
-
90
- // Position above selection, fixed to viewport
91
- let top = rect.top - ACTION_POPOVER_HEIGHT - OFFSET;
92
- let left = rect.left + rect.width / 2 - width / 2;
93
-
94
- // If would go above viewport, place below
95
- if (top < OFFSET) {
96
- top = rect.bottom + OFFSET;
97
- }
98
-
99
- // Clamp horizontal
100
- left = Math.max(OFFSET, Math.min(left, window.innerWidth - width - OFFSET));
101
-
102
- setSelectedText(text);
103
- setSelectionContext(resolved);
104
- setMode("actions");
105
- setModifyPrompt("");
106
- setPosition({ top, left });
107
- });
108
- }, []);
109
-
110
- const addResolvedContext = useCallback(() => {
111
- if (!selectionContext) return false;
112
- addContextItem(selectionContext.item);
113
- return true;
114
- }, [addContextItem, selectionContext]);
115
-
116
- const handleAddToContext = useCallback(() => {
117
- if (!selectedText || !addResolvedContext()) return;
118
-
119
- window.getSelection()?.removeAllRanges();
120
- hidePopover();
121
- }, [selectedText, addResolvedContext, hidePopover]);
122
-
123
- const handleStartModify = useCallback(() => {
124
- setMode("modify");
125
- setPosition((current) => {
126
- if (!current) return current;
127
- const centeredLeft = current.left - (MODIFY_POPOVER_WIDTH - ACTION_POPOVER_WIDTH) / 2;
128
- return {
129
- ...current,
130
- left: Math.max(
131
- OFFSET,
132
- Math.min(centeredLeft, window.innerWidth - MODIFY_POPOVER_WIDTH - OFFSET),
133
- ),
134
- };
135
- });
136
- }, []);
137
-
138
- const handleModifySubmit = useCallback(
139
- (event: FormEvent<HTMLFormElement>) => {
140
- event.preventDefault();
141
- const prompt = modifyPrompt.trim();
142
- if (!prompt || !selectionContext || !addResolvedContext()) return;
143
-
144
- const text =
145
- selectionContext.formatModifyPrompt?.(prompt) ??
146
- `${DEFAULT_MODIFY_PROMPT_PREFIX} ${prompt}`;
147
- useChatBubbleStore.getState().open();
148
- submitComposerText(text);
149
- window.getSelection()?.removeAllRanges();
150
- hidePopover();
151
- },
152
- [addResolvedContext, hidePopover, modifyPrompt, selectionContext],
153
- );
154
-
155
- useEffect(() => {
156
- const handleMouseDown = (e: MouseEvent) => {
157
- if (popoverRef.current?.contains(e.target as Node)) return;
158
- hidePopover();
159
- };
160
- const handleKeyDown = (e: KeyboardEvent) => {
161
- if (e.key === "Escape") hidePopover();
162
- };
163
-
164
- document.addEventListener("mouseup", handleMouseUp);
165
- document.addEventListener("mousedown", handleMouseDown);
166
- window.addEventListener("keydown", handleKeyDown);
167
-
168
- return () => {
169
- document.removeEventListener("mouseup", handleMouseUp);
170
- document.removeEventListener("mousedown", handleMouseDown);
171
- window.removeEventListener("keydown", handleKeyDown);
172
- };
173
- }, [handleMouseUp, hidePopover]);
174
-
175
- useEffect(() => {
176
- if (mode === "modify") {
177
- inputRef.current?.focus();
178
- }
179
- }, [mode]);
180
-
181
- if (!position || !selectionContext) return null;
182
-
183
- if (mode === "modify") {
184
- return (
185
- <form
186
- ref={setPopoverNode}
187
- className={cn(
188
- "fixed z-50",
189
- "flex items-center gap-1.5",
190
- "rounded-2xl border border-border/70 bg-background/95 p-1.5 shadow-xl shadow-black/10 backdrop-blur-md",
191
- "animate-in fade-in-0 zoom-in-95 duration-150",
192
- )}
193
- style={{ top: position.top, left: position.left, width: MODIFY_POPOVER_WIDTH }}
194
- onSubmit={handleModifySubmit}
195
- >
196
- <PencilLine className="ml-1 size-3.5 shrink-0 text-primary" />
197
- <input
198
- ref={inputRef}
199
- value={modifyPrompt}
200
- onChange={(event) => setModifyPrompt(event.target.value)}
201
- placeholder="Describe change..."
202
- className={cn(
203
- "min-w-0 flex-1 bg-transparent px-1 py-1.5 text-xs text-foreground outline-none",
204
- "placeholder:text-muted-foreground",
205
- )}
206
- />
207
- <button
208
- type="button"
209
- aria-label="Cancel modify"
210
- onClick={hidePopover}
211
- className="rounded-full p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
212
- >
213
- <X className="size-3.5" />
214
- </button>
215
- <button
216
- type="submit"
217
- aria-label="Send modify prompt"
218
- disabled={!modifyPrompt.trim()}
219
- className={cn(
220
- "rounded-full p-1.5 transition-colors",
221
- modifyPrompt.trim()
222
- ? "bg-primary text-primary-foreground hover:bg-primary/90"
223
- : "text-muted-foreground opacity-50",
224
- )}
225
- >
226
- <Send className="size-3.5" />
227
- </button>
228
- </form>
229
- );
230
- }
231
-
232
- return (
233
- <div
234
- ref={setPopoverNode}
235
- role="toolbar"
236
- aria-label="Selection actions"
237
- className={cn(
238
- "fixed z-50",
239
- "inline-flex items-center gap-1",
240
- "rounded-full border border-border/70 bg-background/95 p-1 shadow-xl shadow-black/10 backdrop-blur-md",
241
- "animate-in fade-in-0 zoom-in-95 duration-150",
242
- )}
243
- style={{ top: position.top, left: position.left }}
244
- onMouseDown={(e) => e.preventDefault()}
245
- >
246
- <button
247
- type="button"
248
- className="inline-flex h-7 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium text-foreground transition-colors hover:bg-muted"
249
- onClick={handleAddToContext}
250
- >
251
- <TextSelect className="size-3.5 text-primary" />
252
- Add to chat
253
- </button>
254
- {selectionContext.supportsModify && (
255
- <button
256
- type="button"
257
- className="inline-flex h-7 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium text-foreground transition-colors hover:bg-primary/10 hover:text-primary"
258
- onClick={handleStartModify}
259
- >
260
- <PencilLine className="size-3.5 text-primary" />
261
- Modify
262
- </button>
263
- )}
264
- </div>
265
- );
266
- }