@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.
- package/README.md +14 -5
- package/dist/components/ag-ui-runtime-provider.d.ts +64 -0
- package/dist/components/ag-ui-runtime-provider.js +71 -0
- package/dist/components/app-brand.d.ts +6 -0
- package/dist/components/app-brand.js +13 -0
- package/dist/components/assistant-chat.d.ts +26 -0
- package/dist/components/assistant-chat.js +93 -0
- package/dist/components/chat-attachments.d.ts +25 -0
- package/dist/components/chat-attachments.js +20 -0
- package/dist/components/chat-bubble.d.ts +1 -0
- package/dist/components/chat-bubble.js +103 -0
- package/dist/components/chat-header.d.ts +2 -0
- package/dist/components/chat-header.js +16 -0
- package/dist/components/client-tool-executor.d.ts +12 -0
- package/dist/components/client-tool-executor.js +108 -0
- package/dist/components/collection-empty-state.d.ts +9 -0
- package/dist/components/collection-empty-state.js +6 -0
- package/dist/components/collection-filter-bar.d.ts +19 -0
- package/dist/components/collection-filter-bar.js +28 -0
- package/dist/components/collection-search-input.d.ts +6 -0
- package/dist/components/collection-search-input.js +8 -0
- package/dist/components/collection-skeleton.d.ts +6 -0
- package/dist/components/collection-skeleton.js +10 -0
- package/dist/components/collection-sort-menu.d.ts +10 -0
- package/dist/components/collection-sort-menu.js +9 -0
- package/dist/components/collection-surface.d.ts +8 -0
- package/dist/components/collection-surface.js +5 -0
- package/dist/components/collection-toolbar.d.ts +9 -0
- package/dist/components/collection-toolbar.js +5 -0
- package/dist/components/collection-view-toggle.d.ts +7 -0
- package/dist/components/collection-view-toggle.js +17 -0
- package/dist/components/composer-submit-bridge.d.ts +1 -0
- package/dist/components/composer-submit-bridge.js +15 -0
- package/dist/components/confirm-dialog.d.ts +11 -0
- package/dist/components/confirm-dialog.js +6 -0
- package/dist/components/confirmation-card.d.ts +14 -0
- package/dist/components/confirmation-card.js +12 -0
- package/dist/components/context-badges.d.ts +16 -0
- package/dist/components/context-badges.js +56 -0
- package/dist/components/context-bar.d.ts +41 -0
- package/dist/components/context-bar.js +123 -0
- package/dist/components/form-dialog.d.ts +12 -0
- package/dist/components/form-dialog.js +13 -0
- package/dist/components/global-selection-popover.d.ts +7 -0
- package/dist/components/global-selection-popover.js +167 -0
- package/dist/components/infinite-scroll-sentinel.d.ts +6 -0
- package/dist/components/infinite-scroll-sentinel.js +25 -0
- package/dist/components/json-viewer.d.ts +10 -0
- package/dist/components/json-viewer.js +80 -0
- package/dist/components/loading-indicator.d.ts +4 -0
- package/dist/components/loading-indicator.js +22 -0
- package/dist/components/markdown-text.d.ts +13 -0
- package/dist/components/markdown-text.js +221 -0
- package/dist/components/name-dialog.d.ts +16 -0
- package/dist/components/name-dialog.js +23 -0
- package/dist/components/selection-popover.d.ts +12 -0
- package/dist/components/selection-popover.js +111 -0
- package/dist/components/show-document-tool-ui.d.ts +1 -0
- package/dist/components/show-document-tool-ui.js +53 -0
- package/dist/components/sidebar.d.ts +4 -0
- package/dist/components/sidebar.js +904 -0
- package/dist/components/surface-card.d.ts +9 -0
- package/dist/components/surface-card.js +33 -0
- package/dist/components/theme-runtime-provider.d.ts +15 -0
- package/dist/components/theme-runtime-provider.js +48 -0
- package/dist/components/theme-toggle.d.ts +2 -0
- package/dist/components/theme-toggle.js +19 -0
- package/dist/components/tool-call-card.d.ts +27 -0
- package/dist/components/tool-call-card.js +13 -0
- package/dist/components/tool-fallback.d.ts +26 -0
- package/dist/components/tool-fallback.js +129 -0
- package/dist/components/tool-panel-layout.d.ts +10 -0
- package/dist/components/tool-panel-layout.js +12 -0
- package/dist/components/tool-panel.d.ts +1 -0
- package/dist/components/tool-panel.js +89 -0
- package/dist/components/tooltip-icon-button.d.ts +7 -0
- package/dist/components/tooltip-icon-button.js +9 -0
- package/dist/components/user-menu.d.ts +2 -0
- package/dist/components/user-menu.js +14 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +83 -0
- package/dist/lib/ag-ui-adapter.d.ts +29 -0
- package/dist/lib/ag-ui-adapter.js +373 -0
- package/dist/lib/app-store.d.ts +115 -0
- package/dist/lib/app-store.js +199 -0
- package/dist/lib/attachment-adapter.d.ts +20 -0
- package/dist/lib/attachment-adapter.js +99 -0
- package/dist/lib/auth-provider.d.ts +16 -0
- package/dist/lib/auth-provider.js +88 -0
- package/dist/lib/chat-bubble-store.d.ts +30 -0
- package/dist/lib/chat-bubble-store.js +23 -0
- package/dist/lib/chat-lifecycle-store.d.ts +10 -0
- package/{src/lib/chat-lifecycle-store.ts → dist/lib/chat-lifecycle-store.js} +3 -13
- package/dist/lib/client-tools.d.ts +11 -0
- package/dist/lib/client-tools.js +274 -0
- package/dist/lib/composer-submit-store.d.ts +12 -0
- package/dist/lib/composer-submit-store.js +17 -0
- package/dist/lib/dev-store.d.ts +38 -0
- package/dist/lib/dev-store.js +32 -0
- package/dist/lib/nav-config.d.ts +159 -0
- package/dist/lib/nav-config.js +162 -0
- package/dist/lib/nav-dnd.d.ts +8 -0
- package/dist/lib/nav-dnd.js +9 -0
- package/dist/lib/nav-store.d.ts +45 -0
- package/dist/lib/nav-store.js +49 -0
- package/dist/lib/selection-context.d.ts +21 -0
- package/dist/lib/selection-context.js +34 -0
- package/dist/lib/sidebar-store.d.ts +23 -0
- package/dist/lib/sidebar-store.js +98 -0
- package/dist/lib/theme-runtime.d.ts +130 -0
- package/dist/lib/theme-runtime.js +479 -0
- package/dist/lib/theme-store.d.ts +15 -0
- package/dist/lib/theme-store.js +53 -0
- package/dist/lib/tool-panel-store.d.ts +48 -0
- package/dist/lib/tool-panel-store.js +69 -0
- package/dist/lib/use-new-conversation.d.ts +8 -0
- package/dist/lib/use-new-conversation.js +62 -0
- package/dist/lib/use-page-tools.d.ts +27 -0
- package/{src/lib/use-page-tools.ts → dist/lib/use-page-tools.js} +11 -13
- package/dist/styles.css +142 -0
- package/dist/ui/collapsible.d.ts +5 -0
- package/{src/ui/collapsible.tsx → dist/ui/collapsible.js} +0 -2
- package/dist/ui/dialog.d.ts +15 -0
- package/dist/ui/dialog.js +25 -0
- package/dist/ui/dropdown-menu.d.ts +7 -0
- package/dist/ui/dropdown-menu.js +19 -0
- package/dist/ui/popover.d.ts +7 -0
- package/dist/ui/popover.js +10 -0
- package/dist/ui/select.d.ts +4 -0
- package/dist/ui/select.js +9 -0
- package/dist/ui/tooltip.d.ts +7 -0
- package/dist/ui/tooltip.js +10 -0
- package/package.json +31 -12
- package/AGENTS.md +0 -59
- package/CHANGELOG.md +0 -203
- package/CLAUDE.md +0 -1
- package/src/__tests__/no-spaces-imports.test.ts +0 -59
- package/src/__tests__/open-core-guards.test.ts +0 -307
- package/src/__tests__/theme-runtime.test.ts +0 -81
- package/src/__tests__/tool-fallback.test.tsx +0 -109
- package/src/components/__tests__/context-bar.test.ts +0 -112
- package/src/components/ag-ui-runtime-provider.tsx +0 -170
- package/src/components/app-brand.tsx +0 -38
- package/src/components/assistant-chat.tsx +0 -461
- package/src/components/chat-attachments.tsx +0 -68
- package/src/components/chat-bubble.tsx +0 -373
- package/src/components/chat-header.tsx +0 -44
- package/src/components/client-tool-executor.tsx +0 -230
- package/src/components/collection-empty-state.tsx +0 -37
- package/src/components/collection-filter-bar.tsx +0 -168
- package/src/components/collection-search-input.tsx +0 -41
- package/src/components/collection-skeleton.tsx +0 -55
- package/src/components/collection-sort-menu.tsx +0 -62
- package/src/components/collection-surface.tsx +0 -46
- package/src/components/collection-toolbar.tsx +0 -33
- package/src/components/collection-view-toggle.tsx +0 -61
- package/src/components/composer-submit-bridge.tsx +0 -19
- package/src/components/confirm-dialog.tsx +0 -49
- package/src/components/confirmation-card.tsx +0 -32
- package/src/components/context-badges.tsx +0 -136
- package/src/components/context-bar.tsx +0 -238
- package/src/components/form-dialog.tsx +0 -56
- package/src/components/global-selection-popover.tsx +0 -266
- package/src/components/infinite-scroll-sentinel.tsx +0 -48
- package/src/components/json-viewer.tsx +0 -232
- package/src/components/loading-indicator.tsx +0 -40
- package/src/components/markdown-text-citations.test.tsx +0 -108
- package/src/components/markdown-text.tsx +0 -508
- package/src/components/name-dialog.tsx +0 -87
- package/src/components/selection-popover.tsx +0 -156
- package/src/components/show-document-tool-ui.tsx +0 -90
- package/src/components/sidebar.test.tsx +0 -611
- package/src/components/sidebar.tsx +0 -1689
- package/src/components/surface-card.tsx +0 -45
- package/src/components/theme-runtime-provider.tsx +0 -93
- package/src/components/theme-toggle.tsx +0 -27
- package/src/components/tool-call-card.tsx +0 -126
- package/src/components/tool-fallback.tsx +0 -169
- package/src/components/tool-panel-layout.tsx +0 -36
- package/src/components/tool-panel.tsx +0 -233
- package/src/components/tooltip-icon-button.tsx +0 -28
- package/src/components/user-menu.tsx +0 -60
- package/src/index.ts +0 -185
- package/src/lib/__tests__/ag-ui-adapter.test.ts +0 -273
- package/src/lib/__tests__/app-store.test.ts +0 -405
- package/src/lib/__tests__/attachment-adapter.test.ts +0 -48
- package/src/lib/__tests__/chat-bubble-store.test.ts +0 -67
- package/src/lib/__tests__/client-tools.test.ts +0 -124
- package/src/lib/__tests__/dev-store.test.ts +0 -78
- package/src/lib/__tests__/nav-config.test.ts +0 -135
- package/src/lib/__tests__/nav-dnd.test.ts +0 -24
- package/src/lib/__tests__/nav-store.test.ts +0 -59
- package/src/lib/__tests__/selection-context.test.ts +0 -114
- package/src/lib/__tests__/sidebar-store.test.ts +0 -144
- package/src/lib/__tests__/theme-store.test.ts +0 -83
- package/src/lib/__tests__/tool-panel-store.test.ts +0 -61
- package/src/lib/__tests__/use-page-context.test.ts +0 -58
- package/src/lib/ag-ui-adapter.ts +0 -412
- package/src/lib/app-store.ts +0 -361
- package/src/lib/attachment-adapter.ts +0 -92
- package/src/lib/auth-provider.tsx +0 -144
- package/src/lib/chat-bubble-store.ts +0 -46
- package/src/lib/client-tools.ts +0 -293
- package/src/lib/composer-submit-store.ts +0 -32
- package/src/lib/dev-store.ts +0 -69
- package/src/lib/nav-config.ts +0 -329
- package/src/lib/nav-dnd.ts +0 -14
- package/src/lib/nav-store.ts +0 -89
- package/src/lib/selection-context.ts +0 -65
- package/src/lib/sidebar-store.ts +0 -113
- package/src/lib/theme-runtime.ts +0 -660
- package/src/lib/theme-store.ts +0 -67
- package/src/lib/tool-panel-store.ts +0 -121
- package/src/lib/use-new-conversation.test.tsx +0 -168
- package/src/lib/use-new-conversation.ts +0 -64
- package/src/ui/dialog.tsx +0 -109
- package/src/ui/dropdown-menu.tsx +0 -75
- package/src/ui/popover.tsx +0 -34
- package/src/ui/select.tsx +0 -15
- package/src/ui/tooltip.tsx +0 -30
- package/src/vite-env.d.ts +0 -1
- package/tsconfig.json +0 -16
- 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
|
-
}
|