@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
|
@@ -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,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,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
|
+
};
|