@plannotator/ui 0.22.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 +60 -0
- package/assets/diff-options.png +0 -0
- package/assets/icon-codex.png +0 -0
- package/assets/look-flat.png +0 -0
- package/assets/look-grid.png +0 -0
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/assets/workspaces.webp +0 -0
- package/components/AISettingsTab.tsx +145 -0
- package/components/ActionMenu.tsx +101 -0
- package/components/AgentControls.tsx +216 -0
- package/components/AgentsTab.tsx +1294 -0
- package/components/AnnotationPanel.tsx +731 -0
- package/components/AnnotationSidebar.tsx +86 -0
- package/components/AnnotationToolbar.tsx +323 -0
- package/components/AnnotationToolstrip.tsx +355 -0
- package/components/ApproveDropdown.tsx +170 -0
- package/components/AttachmentsButton.tsx +410 -0
- package/components/BlockRenderer.tsx +163 -0
- package/components/BorderTrail.tsx +38 -0
- package/components/CodeFilePicker.tsx +64 -0
- package/components/CodeFilePopout.tsx +602 -0
- package/components/CodePathValidationContext.tsx +14 -0
- package/components/CommentPopover.tsx +512 -0
- package/components/CompletionOverlay.tsx +99 -0
- package/components/ConfirmDialog.tsx +128 -0
- package/components/DocBadges.tsx +231 -0
- package/components/EditorAnnotationCard.tsx +65 -0
- package/components/ExportModal.tsx +565 -0
- package/components/FloatingQuickLabelPicker.tsx +144 -0
- package/components/GitHubIcon.tsx +16 -0
- package/components/GitLabIcon.tsx +30 -0
- package/components/GraphvizBlock.tsx +511 -0
- package/components/ImageAnnotator/Canvas.tsx +118 -0
- package/components/ImageAnnotator/Toolbar.tsx +218 -0
- package/components/ImageAnnotator/index.tsx +272 -0
- package/components/ImageAnnotator/types.ts +39 -0
- package/components/ImageAnnotator/utils.ts +147 -0
- package/components/ImageThumbnail.tsx +126 -0
- package/components/ImportModal.tsx +146 -0
- package/components/InlineMarkdown.tsx +1094 -0
- package/components/KeyboardShortcuts.tsx +208 -0
- package/components/Landing.tsx +496 -0
- package/components/ListItemBody.tsx +62 -0
- package/components/ListMarker.tsx +73 -0
- package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
- package/components/MarkdownEditor.tsx +44 -0
- package/components/MenuVersionSection.tsx +91 -0
- package/components/MermaidBlock.tsx +579 -0
- package/components/ModeToggle.tsx +68 -0
- package/components/OpenInAppButton.tsx +299 -0
- package/components/OverlayScrollArea.tsx +82 -0
- package/components/PermissionModeSetup.tsx +92 -0
- package/components/PinpointOverlay.tsx +99 -0
- package/components/PlanAIAnnouncementDialog.tsx +187 -0
- package/components/PlanHeaderMenu.tsx +299 -0
- package/components/PopoutDialog.tsx +91 -0
- package/components/Popover.tsx +27 -0
- package/components/ProviderIcons.tsx +51 -0
- package/components/PullRequestIcon.tsx +12 -0
- package/components/QuickLabelDropdown.tsx +63 -0
- package/components/RenderedMarkdown.tsx +54 -0
- package/components/RepoIcon.tsx +12 -0
- package/components/ResizeHandle.tsx +86 -0
- package/components/ReviewAgentsIcon.tsx +18 -0
- package/components/SearchableSelect.tsx +157 -0
- package/components/Settings.tsx +2220 -0
- package/components/SparklesIcon.tsx +45 -0
- package/components/StickyHeaderLane.tsx +260 -0
- package/components/TableOfContents.tsx +157 -0
- package/components/TaterSpritePullup.tsx +34 -0
- package/components/TaterSpriteRunning.tsx +54 -0
- package/components/TaterSpriteSitting.tsx +37 -0
- package/components/TextShimmer.tsx +57 -0
- package/components/ThemeProvider.tsx +162 -0
- package/components/ThemeTab.tsx +139 -0
- package/components/ToolbarButtons.tsx +116 -0
- package/components/Tooltip.tsx +45 -0
- package/components/Viewer.tsx +977 -0
- package/components/ai/AIProviderBar.tsx +95 -0
- package/components/ai/DocumentAIChatPanel.tsx +313 -0
- package/components/blocks/AlertBlock.tsx +58 -0
- package/components/blocks/Callout.tsx +60 -0
- package/components/blocks/CodeBlock.tsx +75 -0
- package/components/blocks/HtmlBlock.tsx +122 -0
- package/components/blocks/MathBlock.tsx +36 -0
- package/components/blocks/TableBlock.tsx +146 -0
- package/components/blocks/TablePopout.tsx +279 -0
- package/components/blocks/TableToolbar.tsx +153 -0
- package/components/blocks/proseBody.tsx +102 -0
- package/components/core/button.tsx +44 -0
- package/components/core/textarea.tsx +26 -0
- package/components/diagramLanguages.ts +14 -0
- package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
- package/components/html-viewer/HtmlViewer.tsx +386 -0
- package/components/html-viewer/bridge-script.ts +505 -0
- package/components/html-viewer/index.ts +1 -0
- package/components/html-viewer/useHtmlAnnotation.ts +400 -0
- package/components/icons/AgentIcons.tsx +66 -0
- package/components/icons/AppIcon.tsx +56 -0
- package/components/icons/MessagesIcon.tsx +11 -0
- package/components/icons/ObsidianIcons.tsx +208 -0
- package/components/icons/app/android-studio.svg +369 -0
- package/components/icons/app/antigravity.svg +97 -0
- package/components/icons/app/cursor.svg +16 -0
- package/components/icons/app/file-explorer.svg +20 -0
- package/components/icons/app/finder.png +0 -0
- package/components/icons/app/ghostty.svg +13 -0
- package/components/icons/app/iterm2.svg +13 -0
- package/components/icons/app/powershell.svg +14 -0
- package/components/icons/app/sublime-text.svg +17 -0
- package/components/icons/app/terminal.png +0 -0
- package/components/icons/app/textmate.png +0 -0
- package/components/icons/app/vscode.svg +39 -0
- package/components/icons/app/warp.png +0 -0
- package/components/icons/app/xcode.png +0 -0
- package/components/icons/app/zed-dark.svg +15 -0
- package/components/icons/app/zed.svg +15 -0
- package/components/icons/themeIcons.tsx +47 -0
- package/components/mermaidSvg.ts +33 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
- package/components/plan-diff/PlanDiffBadge.tsx +48 -0
- package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
- package/components/plan-diff/PlanDiffViewer.tsx +214 -0
- package/components/plan-diff/PlanRawDiffView.tsx +102 -0
- package/components/plan-diff/VSCodeIcon.tsx +132 -0
- package/components/settings/HooksTab.tsx +208 -0
- package/components/sidebar/ArchiveBrowser.tsx +98 -0
- package/components/sidebar/CountBadge.tsx +12 -0
- package/components/sidebar/FileBrowser.tsx +498 -0
- package/components/sidebar/MessagesBrowser.tsx +109 -0
- package/components/sidebar/SidebarContainer.tsx +353 -0
- package/components/sidebar/SidebarTabs.tsx +155 -0
- package/components/sidebar/VersionBrowser.tsx +141 -0
- package/components/types.d.ts +7 -0
- package/components/ui/badge.tsx +44 -0
- package/components/ui/button.tsx +83 -0
- package/components/ui/card.tsx +58 -0
- package/components/ui/dialog.tsx +106 -0
- package/components/ui/dropdown-menu.tsx +240 -0
- package/components/ui/state-pill.tsx +45 -0
- package/components/ui/tabs.tsx +46 -0
- package/components/ui/textarea.tsx +25 -0
- package/config/configStore.ts +214 -0
- package/config/index.ts +4 -0
- package/config/reviewView.ts +42 -0
- package/config/settings.ts +310 -0
- package/config/useConfig.ts +20 -0
- package/configure.ts +69 -0
- package/globals.d.ts +14 -0
- package/hooks/pfm/useCodeFilePopout.ts +111 -0
- package/hooks/useAIChat.ts +532 -0
- package/hooks/useAIProviderConfig.ts +115 -0
- package/hooks/useActiveSection.ts +78 -0
- package/hooks/useAgentJobs.ts +306 -0
- package/hooks/useAgentSettings.ts +579 -0
- package/hooks/useAgents.ts +90 -0
- package/hooks/useAnnotationDraft.ts +523 -0
- package/hooks/useAnnotationHighlighter.ts +1170 -0
- package/hooks/useArchive.ts +171 -0
- package/hooks/useAutoClose.ts +104 -0
- package/hooks/useCodeAnnotationDraft.ts +194 -0
- package/hooks/useCodeFilePopout.ts +1 -0
- package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
- package/hooks/useDraggable.ts +108 -0
- package/hooks/useEditorAnnotations.ts +64 -0
- package/hooks/useExternalAnnotationHighlights.ts +105 -0
- package/hooks/useExternalAnnotations.ts +268 -0
- package/hooks/useFileBrowser.ts +402 -0
- package/hooks/useInputMethodSwitch.ts +89 -0
- package/hooks/useIsMobile.ts +17 -0
- package/hooks/useLinkedDoc.ts +494 -0
- package/hooks/useOverlayViewport.ts +36 -0
- package/hooks/usePinpoint.ts +182 -0
- package/hooks/usePlanDiff.ts +176 -0
- package/hooks/usePrintMode.ts +27 -0
- package/hooks/useResizablePanel.ts +173 -0
- package/hooks/useScrollViewport.ts +38 -0
- package/hooks/useSharing.ts +441 -0
- package/hooks/useSidebar.ts +55 -0
- package/hooks/useUpdateCheck.ts +132 -0
- package/hooks/useValidatedCodePaths.ts +94 -0
- package/icons/GitUser.tsx +17 -0
- package/lib/utils.ts +11 -0
- package/package.json +117 -0
- package/plannotator.webp +0 -0
- package/print.css +427 -0
- package/shortcuts/code-review/ai.shortcuts.ts +25 -0
- package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
- package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
- package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
- package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
- package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
- package/shortcuts/core.ts +399 -0
- package/shortcuts/index.ts +21 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
- package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
- package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
- package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
- package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
- package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
- package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
- package/shortcuts/runtime.ts +259 -0
- package/sprite_package_additional/index.html +34 -0
- package/sprite_package_additional/sprite.png +0 -0
- package/sprite_package_new/index.html +34 -0
- package/sprite_package_new/sprite.png +0 -0
- package/sprite_package_pulluphang/index.html +34 -0
- package/sprite_package_pulluphang/sprite.png +0 -0
- package/styles.css +1 -0
- package/theme.css +893 -0
- package/themes/adwaita.css +62 -0
- package/themes/andromeeda.css +66 -0
- package/themes/aurora-x.css +66 -0
- package/themes/ayu-dark.css +66 -0
- package/themes/caffeine.css +113 -0
- package/themes/catppuccin.css +62 -0
- package/themes/claude-plus.css +60 -0
- package/themes/cursor-hc.css +34 -0
- package/themes/cursor-midnight.css +34 -0
- package/themes/cursor.css +62 -0
- package/themes/dark-plus.css +66 -0
- package/themes/doom-64.css +109 -0
- package/themes/dracula.css +33 -0
- package/themes/everforest-hard.css +62 -0
- package/themes/everforest-soft.css +62 -0
- package/themes/everforest.css +62 -0
- package/themes/github.css +66 -0
- package/themes/gruvbox.css +62 -0
- package/themes/houston.css +66 -0
- package/themes/kanagawa-dragon.css +34 -0
- package/themes/kanagawa-lotus.css +34 -0
- package/themes/kanagawa-wave.css +34 -0
- package/themes/laserwave.css +66 -0
- package/themes/material.css +62 -0
- package/themes/min.css +66 -0
- package/themes/monokai-pro.css +34 -0
- package/themes/neutral.css +59 -0
- package/themes/night-owl.css +66 -0
- package/themes/nord.css +66 -0
- package/themes/one-dark-pro.css +66 -0
- package/themes/one-light.css +66 -0
- package/themes/paulmillr.css +34 -0
- package/themes/plannotator.css +60 -0
- package/themes/plastic.css +66 -0
- package/themes/poimandres.css +66 -0
- package/themes/quantum-rose.css +109 -0
- package/themes/red.css +66 -0
- package/themes/rose-pine.css +62 -0
- package/themes/simple.css +124 -0
- package/themes/slack.css +66 -0
- package/themes/snazzy-light.css +66 -0
- package/themes/soft-pop.css +60 -0
- package/themes/solar-dusk.css +109 -0
- package/themes/solarized.css +66 -0
- package/themes/synthwave-84.css +34 -0
- package/themes/terminal.css +62 -0
- package/themes/tinacious.css +57 -0
- package/themes/tokyo-night.css +62 -0
- package/themes/vesper.css +62 -0
- package/themes/vitesse-black.css +66 -0
- package/themes/vitesse.css +62 -0
- package/types.ts +264 -0
- package/utils/agentSwitch.ts +71 -0
- package/utils/aiChatFormat.ts +33 -0
- package/utils/aiPrompt.ts +29 -0
- package/utils/aiProvider.ts +217 -0
- package/utils/anchors.ts +11 -0
- package/utils/annotateAgentTerminal.ts +29 -0
- package/utils/annotationHelpers.ts +101 -0
- package/utils/bear.ts +59 -0
- package/utils/blockTargeting.ts +240 -0
- package/utils/callback.ts +99 -0
- package/utils/commentContent.ts +8 -0
- package/utils/defaultNotesApp.ts +20 -0
- package/utils/diffFonts.ts +33 -0
- package/utils/editorMode.ts +31 -0
- package/utils/fileBrowser.ts +40 -0
- package/utils/generateId.ts +7 -0
- package/utils/generateIdentity.ts +29 -0
- package/utils/identity.ts +112 -0
- package/utils/inlineTransforms.ts +41 -0
- package/utils/inputMethod.ts +17 -0
- package/utils/lookAndFeelAnnouncement.ts +18 -0
- package/utils/obsidian.ts +201 -0
- package/utils/octarine.ts +53 -0
- package/utils/parser.ts +991 -0
- package/utils/permissionMode.ts +77 -0
- package/utils/planAIAnnouncement.ts +17 -0
- package/utils/planAgentInstructions.ts +137 -0
- package/utils/planDiffEngine.ts +590 -0
- package/utils/planSave.ts +49 -0
- package/utils/platform.ts +11 -0
- package/utils/quickLabels.ts +77 -0
- package/utils/reviewAgentInstructions.ts +182 -0
- package/utils/sanitizeHtml.ts +45 -0
- package/utils/sharing.ts +350 -0
- package/utils/slugify.ts +37 -0
- package/utils/storage.ts +141 -0
- package/utils/themeRegistry.ts +567 -0
- package/utils/uiPreferences.ts +34 -0
- package/utils/upload.ts +56 -0
- package/utils/wideMode.ts +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @plannotator/ui
|
|
2
|
+
|
|
3
|
+
Plannotator's document UI — markdown rendering, themes, the annotation editor, settings, comments, and layout — as installable building blocks. Published so a separate app (the commercial Workspaces app) can reuse the exact same experience, while Plannotator itself stays unchanged.
|
|
4
|
+
|
|
5
|
+
Ships with **`@plannotator/core`**: a small, browser-safe, zero-dependency package of the pure utilities and types `ui` builds on (carved out so `ui` can be installed standalone without Plannotator's server code).
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
Workspaces needs the same document experience Plannotator has — render docs, annotate, comment, theme, edit — but backed by its own infrastructure (its own storage, auth, realtime, AI). Rather than fork or rebuild, it **installs these packages and plugs in its own backend.** Plannotator passes nothing and behaves exactly as before.
|
|
10
|
+
|
|
11
|
+
## How it works: host-override seams
|
|
12
|
+
|
|
13
|
+
Every place the UI talks to a backend (loading a doc preview, saving settings, persisting drafts, streaming comments, listing files, calling AI, etc.) is an **optional seam** that defaults to Plannotator's behavior. A host swaps in its own implementations through **one call at startup**:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { configurePlannotatorUI } from "@plannotator/ui/configure";
|
|
17
|
+
|
|
18
|
+
configurePlannotatorUI({
|
|
19
|
+
storageBackend, // where settings persist
|
|
20
|
+
identityProvider, // who the current user is
|
|
21
|
+
imageSrcResolver, // how image paths resolve to URLs
|
|
22
|
+
docPreviewFetcher,
|
|
23
|
+
fileTreeBackend,
|
|
24
|
+
draftTransport,
|
|
25
|
+
externalAnnotationTransport, // live/agent comments
|
|
26
|
+
aiTransport,
|
|
27
|
+
serverSync,
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Anything you don't pass keeps Plannotator's default. A few component-specific overrides (e.g. an "open in editor" diff action) are passed as props where you render that component.
|
|
32
|
+
|
|
33
|
+
## Consuming it (e.g. from Workspaces)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @plannotator/ui @plannotator/core
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
1. Call `configurePlannotatorUI({ ... })` once at startup with your backend.
|
|
40
|
+
2. Import the stylesheet: `import "@plannotator/ui/styles.css";` (precompiled — no Tailwind wiring needed; if you'd rather run your own Tailwind over the package source, add `@source` globs for `@plannotator/ui`'s `components/`, `hooks/`, and `utils/` dirs in your own CSS — the package doesn't ship its build entry).
|
|
41
|
+
3. **Load the fonts in your app entry** — the stylesheet references `--font-sans` / `--font-mono` but does not ship font binaries (standard for a shared UI package; your app owns font loading). Plannotator uses Inter + Geist Mono:
|
|
42
|
+
```ts
|
|
43
|
+
import "@fontsource-variable/inter";
|
|
44
|
+
import "@fontsource-variable/geist-mono";
|
|
45
|
+
```
|
|
46
|
+
Or provide your own fonts and set `--font-sans` / `--font-mono` to match.
|
|
47
|
+
The same policy covers math: KaTeX's stylesheet + fonts are deliberately not in `styles.css` — if you render math, load `katex/dist/katex.min.css` yourself (import, CDN tag, or self-hosted copy; see HANDOFF.md "Math rendering").
|
|
48
|
+
4. Import components: `import { Viewer } from "@plannotator/ui/components/Viewer";`
|
|
49
|
+
5. Build with a bundler that compiles TS/TSX (Vite + React 19 + Tailwind v4). The packages ship **source**, so your bundler compiles them — set `moduleResolution: "bundler"`, `allowImportingTsExtensions`, `jsx: "react-jsx"`.
|
|
50
|
+
|
|
51
|
+
## Packages & publishing
|
|
52
|
+
|
|
53
|
+
- `@plannotator/core` — pure utils + types, zero deps, browser-safe (CI enforces no `node:` imports). Published.
|
|
54
|
+
- `@plannotator/ui` — React components/hooks + theme + `configure()`. Depends on `@plannotator/core` (exact-version lockstep). Published.
|
|
55
|
+
- `@plannotator/shared`, `@plannotator/ai` — stay private to the monorepo; `shared` re-exports `core`'s modules via shims so Plannotator's internals are untouched.
|
|
56
|
+
- Versioned in lockstep with the repo. Publish `core` then `ui`: build each tarball with **`bun pm pack`** (resolves `workspace:*` to the exact version at pack time), then **`npm publish *.tgz --provenance --access public`** — the repo's existing flow.
|
|
57
|
+
|
|
58
|
+
## The one rule
|
|
59
|
+
|
|
60
|
+
**Do not reimplement the document UI from scratch.** A prior from-scratch rewrite broke the app and was reverted. The supported path is always: keep these components as-is and add a seam where a host needs different backend behavior. Never delete working Plannotator code until a human has confirmed parity in the browser.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { getProviderMeta } from './ProviderIcons';
|
|
3
|
+
import {
|
|
4
|
+
getAIProviderSettings,
|
|
5
|
+
resolveAIModelForProvider,
|
|
6
|
+
resolveAIProviderSelection,
|
|
7
|
+
saveAIProviderSelection,
|
|
8
|
+
savePreferredModel,
|
|
9
|
+
type AIProviderOption,
|
|
10
|
+
} from '../utils/aiProvider';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import type { Origin } from '@plannotator/core/agents';
|
|
13
|
+
|
|
14
|
+
interface AIProvider extends AIProviderOption {
|
|
15
|
+
capabilities: Record<string, boolean>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface AISettingsTabProps {
|
|
19
|
+
providers: AIProvider[];
|
|
20
|
+
selectedProviderId: string | null;
|
|
21
|
+
origin?: Origin | null;
|
|
22
|
+
onProviderChange: (providerId: string | null) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const AISettingsTab: React.FC<AISettingsTabProps> = ({
|
|
26
|
+
providers,
|
|
27
|
+
selectedProviderId,
|
|
28
|
+
origin,
|
|
29
|
+
onProviderChange,
|
|
30
|
+
}) => {
|
|
31
|
+
const settings = getAIProviderSettings();
|
|
32
|
+
const originDefault = resolveAIProviderSelection({ providers, origin, settings }).providerId;
|
|
33
|
+
const effectiveSelection = selectedProviderId ?? originDefault ?? providers[0]?.id ?? null;
|
|
34
|
+
const [preferredModels, setPreferredModels] = useState<Record<string, string>>(() =>
|
|
35
|
+
getAIProviderSettings().preferredModels
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const handleSelectProvider = (providerId: string) => {
|
|
39
|
+
onProviderChange(providerId);
|
|
40
|
+
const provider = providers.find(p => p.id === providerId) ?? null;
|
|
41
|
+
const model = resolveAIModelForProvider(provider, getAIProviderSettings().preferredModels);
|
|
42
|
+
saveAIProviderSelection({ providerId, model, origin });
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleModelChange = (providerId: string, modelId: string) => {
|
|
46
|
+
savePreferredModel(providerId, modelId);
|
|
47
|
+
setPreferredModels(prev => ({ ...prev, [providerId]: modelId }));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (providers.length === 0) {
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
<div>
|
|
54
|
+
<div className="text-sm font-medium">AI Provider</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="flex items-start gap-2 p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg text-xs text-amber-600 dark:text-amber-400">
|
|
57
|
+
<svg className="w-4 h-4 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
58
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
59
|
+
</svg>
|
|
60
|
+
<span>No AI providers detected. Install the <strong>claude</strong> or <strong>codex</strong> CLI and make sure you're authenticated. <a href="https://plannotator.ai/docs/guides/ai-features/" target="_blank" rel="noopener noreferrer" className="underline hover:text-amber-700 dark:hover:text-amber-300">Setup guide</a></span>
|
|
61
|
+
</div>
|
|
62
|
+
</>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<div>
|
|
69
|
+
<div className="text-sm font-medium">AI Provider</div>
|
|
70
|
+
<div className="text-xs text-muted-foreground">
|
|
71
|
+
Choose which AI provider and model to use for inline chat
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
{providers.map((p) => {
|
|
77
|
+
const meta = getProviderMeta(p.name);
|
|
78
|
+
const Icon = meta.icon;
|
|
79
|
+
const isSelected = effectiveSelection === p.id;
|
|
80
|
+
const models = p.models ?? [];
|
|
81
|
+
const defaultModel = models.find(m => m.default) ?? models[0];
|
|
82
|
+
const selectedModel = preferredModels[p.id] ?? defaultModel?.id ?? null;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div key={p.id}>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={() => handleSelectProvider(p.id)}
|
|
89
|
+
className={`w-full flex items-center gap-3 p-3 rounded-lg border transition-colors text-left ${
|
|
90
|
+
isSelected
|
|
91
|
+
? 'border-primary bg-primary/5'
|
|
92
|
+
: 'border-border hover:border-muted-foreground/30 hover:bg-muted/50'
|
|
93
|
+
} ${models.length > 1 && isSelected ? 'rounded-b-none border-b-0' : ''}`}
|
|
94
|
+
>
|
|
95
|
+
<div className="w-6 h-6 rounded-md bg-muted flex items-center justify-center text-muted-foreground">
|
|
96
|
+
<Icon className="w-4 h-4" />
|
|
97
|
+
</div>
|
|
98
|
+
<div className="flex-1 min-w-0">
|
|
99
|
+
<div className="text-sm font-medium">{meta.label}</div>
|
|
100
|
+
</div>
|
|
101
|
+
{isSelected && (
|
|
102
|
+
<svg className="w-4 h-4 text-primary flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
103
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
104
|
+
</svg>
|
|
105
|
+
)}
|
|
106
|
+
</button>
|
|
107
|
+
|
|
108
|
+
{/* Model selector — shown for selected provider when models available */}
|
|
109
|
+
{isSelected && models.length > 1 && (
|
|
110
|
+
<div className="border border-t-0 border-primary bg-primary/5 rounded-b-lg px-3 pb-3 pt-1">
|
|
111
|
+
<label className="flex items-center gap-2">
|
|
112
|
+
<span className="text-[11px] text-muted-foreground">Model</span>
|
|
113
|
+
<select
|
|
114
|
+
value={selectedModel ?? ''}
|
|
115
|
+
onChange={(e) => handleModelChange(p.id, e.target.value)}
|
|
116
|
+
className="flex-1 text-xs bg-muted rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-primary/50 cursor-pointer"
|
|
117
|
+
>
|
|
118
|
+
{models.map(m => (
|
|
119
|
+
<option key={m.id} value={m.id}>{m.label}</option>
|
|
120
|
+
))}
|
|
121
|
+
</select>
|
|
122
|
+
</label>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{/* Show current model as muted text for unselected providers */}
|
|
127
|
+
{!isSelected && models.length > 0 && (
|
|
128
|
+
<div className="px-3 -mt-0.5">
|
|
129
|
+
<span className="text-[10px] text-muted-foreground/50">
|
|
130
|
+
{models.find(m => m.id === (preferredModels[p.id] ?? defaultModel?.id))?.label ?? defaultModel?.label}
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="text-[10px] text-muted-foreground/70">
|
|
140
|
+
Providers are detected from installed CLI tools. No API keys are managed by Plannotator — you must be authenticated with each CLI independently.{' '}
|
|
141
|
+
<a href="https://plannotator.ai/docs/guides/ai-features/" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Learn more</a>
|
|
142
|
+
</div>
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ActionMenuProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
panelClassName?: string;
|
|
6
|
+
renderTrigger: (props: {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
toggleMenu: () => void;
|
|
9
|
+
}) => React.ReactNode;
|
|
10
|
+
children: (props: { closeMenu: () => void }) => React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ActionMenu: React.FC<ActionMenuProps> = ({
|
|
14
|
+
className,
|
|
15
|
+
panelClassName,
|
|
16
|
+
renderTrigger,
|
|
17
|
+
children,
|
|
18
|
+
}) => {
|
|
19
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
20
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!isOpen) return;
|
|
24
|
+
|
|
25
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
26
|
+
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
|
27
|
+
setIsOpen(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
32
|
+
if (event.key === 'Escape') {
|
|
33
|
+
setIsOpen(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
document.addEventListener('pointerdown', handlePointerDown);
|
|
38
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
39
|
+
return () => {
|
|
40
|
+
document.removeEventListener('pointerdown', handlePointerDown);
|
|
41
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
42
|
+
};
|
|
43
|
+
}, [isOpen]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div ref={menuRef} className={className ? `relative ${className}` : 'relative'}>
|
|
47
|
+
{renderTrigger({
|
|
48
|
+
isOpen,
|
|
49
|
+
toggleMenu: () => setIsOpen(open => !open),
|
|
50
|
+
})}
|
|
51
|
+
|
|
52
|
+
{isOpen && (
|
|
53
|
+
<div className={panelClassName ?? 'absolute top-full right-0 mt-1 w-56 rounded-lg border border-border bg-popover py-1 shadow-xl z-[70]'}>
|
|
54
|
+
{children({ closeMenu: () => setIsOpen(false) })}
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
interface ActionMenuItemProps {
|
|
62
|
+
onClick: () => void;
|
|
63
|
+
icon: React.ReactNode;
|
|
64
|
+
label: string;
|
|
65
|
+
subtitle?: string;
|
|
66
|
+
badge?: React.ReactNode;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const ActionMenuItem: React.FC<ActionMenuItemProps> = ({
|
|
70
|
+
onClick,
|
|
71
|
+
icon,
|
|
72
|
+
label,
|
|
73
|
+
subtitle,
|
|
74
|
+
badge,
|
|
75
|
+
}) => (
|
|
76
|
+
<button
|
|
77
|
+
onClick={onClick}
|
|
78
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs transition-colors hover:bg-muted"
|
|
79
|
+
>
|
|
80
|
+
<span className="text-muted-foreground">{icon}</span>
|
|
81
|
+
{subtitle ? (
|
|
82
|
+
<span className="flex flex-1 flex-col gap-0.5">
|
|
83
|
+
<span>{label}</span>
|
|
84
|
+
<span className="text-[10px] text-muted-foreground">{subtitle}</span>
|
|
85
|
+
</span>
|
|
86
|
+
) : (
|
|
87
|
+
<span className="flex-1">{label}</span>
|
|
88
|
+
)}
|
|
89
|
+
{badge}
|
|
90
|
+
</button>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
export const ActionMenuDivider: React.FC = () => (
|
|
94
|
+
<div className="my-1 border-t border-border" />
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
export const ActionMenuSectionLabel: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
98
|
+
<div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
99
|
+
{children}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { ChevronDown, Plus } from 'lucide-react';
|
|
3
|
+
import { cn } from '../lib/utils';
|
|
4
|
+
|
|
5
|
+
// --- Launch-control primitives (ported from the prototype's sidebar) ---
|
|
6
|
+
|
|
7
|
+
// A labelled config row. Inline (label left, control right) by default; pass
|
|
8
|
+
// `stacked` to put a full-width control under the label — used for the model
|
|
9
|
+
// dropdown and the effort/reasoning segmented pickers, which need the room.
|
|
10
|
+
export function ConfigRow({ label, stacked, children }: { label: string; stacked?: boolean; children: React.ReactNode }) {
|
|
11
|
+
if (stacked) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-1">
|
|
14
|
+
<span className="block text-[10px] text-muted-foreground/50">{label}</span>
|
|
15
|
+
{children}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex items-center justify-between gap-2">
|
|
21
|
+
<span className="shrink-0 text-[10px] text-muted-foreground/50">{label}</span>
|
|
22
|
+
{children}
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Pill selector for small option sets (effort, reasoning, tour engine).
|
|
28
|
+
export function SegmentedPicker({ options, value, onChange }: { options: Array<{ value: string; label: string }>; value: string; onChange: (v: string) => void }) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="flex items-center gap-px rounded-lg bg-surface-1/50 p-0.5">
|
|
31
|
+
{options.map((opt) => (
|
|
32
|
+
<button
|
|
33
|
+
key={opt.value}
|
|
34
|
+
type="button"
|
|
35
|
+
onClick={() => onChange(opt.value)}
|
|
36
|
+
className={cn(
|
|
37
|
+
'flex-1 rounded-md px-2 py-1 font-medium text-[9px] transition-colors',
|
|
38
|
+
value === opt.value
|
|
39
|
+
? 'bg-card text-foreground shadow-sm'
|
|
40
|
+
: 'text-muted-foreground/50 hover:text-muted-foreground',
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
{opt.label}
|
|
44
|
+
</button>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Animated on/off switch (fast mode).
|
|
51
|
+
export function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
|
52
|
+
return (
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={() => onChange(!checked)}
|
|
56
|
+
className={cn('relative h-5 w-9 shrink-0 rounded-full transition-colors', checked ? 'bg-primary' : 'bg-border/50')}
|
|
57
|
+
>
|
|
58
|
+
<span
|
|
59
|
+
className={cn(
|
|
60
|
+
'absolute top-0.5 left-0.5 h-4 w-4 rounded-full bg-white shadow-sm transition-transform',
|
|
61
|
+
checked && 'translate-x-4',
|
|
62
|
+
)}
|
|
63
|
+
/>
|
|
64
|
+
</button>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Lists longer than this get a type-to-filter input and provider grouping —
|
|
69
|
+
// marker-engine catalogs (Cursor ~150, Pi ~59, OpenCode ~25) are unusable as a
|
|
70
|
+
// flat list; the classic Claude/Codex catalogs (7–11) stay a plain menu.
|
|
71
|
+
// Exported so other pickers over the same catalogs (e.g. GuideEmptyState's
|
|
72
|
+
// InlinePicker in review-editor) share this one threshold instead of hardcoding it.
|
|
73
|
+
export const SEARCHABLE_THRESHOLD = 12;
|
|
74
|
+
|
|
75
|
+
export interface SelectOptionGroup {
|
|
76
|
+
/** null = ungrouped (single flat list). */
|
|
77
|
+
label: string | null;
|
|
78
|
+
options: Array<{ value: string; label: string }>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Group `provider/model`-shaped option lists by their provider prefix.
|
|
82
|
+
* Returns one ungrouped bucket when the list is small or ids aren't
|
|
83
|
+
* provider-prefixed (Claude aliases, Codex models, Cursor ids). */
|
|
84
|
+
export function groupModelOptions(options: Array<{ value: string; label: string }>): SelectOptionGroup[] {
|
|
85
|
+
const slashed = options.filter((o) => o.value.includes('/')).length;
|
|
86
|
+
if (options.length <= SEARCHABLE_THRESHOLD || slashed < options.length / 2) {
|
|
87
|
+
return [{ label: null, options }];
|
|
88
|
+
}
|
|
89
|
+
const groups = new Map<string, Array<{ value: string; label: string }>>();
|
|
90
|
+
for (const o of options) {
|
|
91
|
+
const key = o.value.includes('/') ? o.value.slice(0, o.value.indexOf('/')) : 'other';
|
|
92
|
+
const bucket = groups.get(key);
|
|
93
|
+
if (bucket) bucket.push(o);
|
|
94
|
+
else groups.set(key, [o]);
|
|
95
|
+
}
|
|
96
|
+
return [...groups.entries()].map(([label, opts]) => ({ label, options: opts }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Strip a redundant `group/` prefix from an option label rendered under its
|
|
100
|
+
* group header (e.g. "anthropic/claude-sonnet-4-6" under "anthropic"). */
|
|
101
|
+
export function labelWithinGroup(label: string, group: string | null): string {
|
|
102
|
+
if (group && label.startsWith(`${group}/`)) return label.slice(group.length + 1);
|
|
103
|
+
return label;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Dropdown button + downward popover. Reused for the provider selector and the
|
|
107
|
+
// model picker (whose 7–9 options rule out a segmented control). The popover
|
|
108
|
+
// opens downward (`top-full`) because the launch panel is pinned to the top of
|
|
109
|
+
// the tab. Long catalogs automatically gain a filter input + provider grouping.
|
|
110
|
+
export function SelectMenu({ value, options, onChange, icon, placeholder, footerAction }: { value: string; options: Array<{ value: string; label: string }>; onChange: (v: string) => void; icon?: React.ReactNode; placeholder?: string; footerAction?: { label: string; onClick: () => void } }) {
|
|
111
|
+
const [open, setOpen] = useState(false);
|
|
112
|
+
const [query, setQuery] = useState('');
|
|
113
|
+
const current = options.find((o) => o.value === value);
|
|
114
|
+
const searchable = options.length > SEARCHABLE_THRESHOLD;
|
|
115
|
+
|
|
116
|
+
const q = query.trim().toLowerCase();
|
|
117
|
+
const filtered = q
|
|
118
|
+
? options.filter((o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q))
|
|
119
|
+
: options;
|
|
120
|
+
const grouped = groupModelOptions(filtered);
|
|
121
|
+
|
|
122
|
+
const close = () => {
|
|
123
|
+
setOpen(false);
|
|
124
|
+
setQuery('');
|
|
125
|
+
};
|
|
126
|
+
const select = (v: string) => {
|
|
127
|
+
onChange(v);
|
|
128
|
+
close();
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="relative w-full">
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
onClick={() => (open ? close() : setOpen(true))}
|
|
136
|
+
className="flex w-full items-center gap-2 rounded-lg border border-border/30 bg-surface-1/30 px-2.5 py-1.5 text-left transition-colors hover:bg-surface-1/50"
|
|
137
|
+
>
|
|
138
|
+
{icon}
|
|
139
|
+
<span className="min-w-0 flex-1 truncate text-[11px] text-foreground/80">{current?.label ?? placeholder}</span>
|
|
140
|
+
<ChevronDown className={cn('shrink-0 text-muted-foreground/30 transition-transform', open && 'rotate-180')} size={10} />
|
|
141
|
+
</button>
|
|
142
|
+
{open && (
|
|
143
|
+
<>
|
|
144
|
+
<div className="fixed inset-0 z-10" onClick={close} />
|
|
145
|
+
<div
|
|
146
|
+
className={cn(
|
|
147
|
+
'absolute right-0 top-full left-0 z-20 mt-1 rounded-xl bg-card shadow-[var(--card-shadow)] ring-1 ring-border/20',
|
|
148
|
+
searchable && 'min-w-[240px]',
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
{searchable && (
|
|
152
|
+
<div className="p-1 pb-0">
|
|
153
|
+
<input
|
|
154
|
+
autoFocus
|
|
155
|
+
value={query}
|
|
156
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
157
|
+
onKeyDown={(e) => {
|
|
158
|
+
e.stopPropagation();
|
|
159
|
+
if (e.key === 'Escape') close();
|
|
160
|
+
if (e.key === 'Enter' && filtered.length > 0) select(filtered[0].value);
|
|
161
|
+
}}
|
|
162
|
+
placeholder="Type to filter…"
|
|
163
|
+
className="w-full rounded-lg border border-border/30 bg-surface-1/30 px-2.5 py-1.5 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/40 focus:border-border/60"
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
<div className={cn('overflow-y-auto p-1', searchable ? 'max-h-72' : 'max-h-56')}>
|
|
168
|
+
{filtered.length === 0 && (
|
|
169
|
+
<div className="px-2.5 py-2 text-[11px] text-muted-foreground/50">No matches</div>
|
|
170
|
+
)}
|
|
171
|
+
{grouped.map((group) => (
|
|
172
|
+
<React.Fragment key={group.label ?? '__flat'}>
|
|
173
|
+
{group.label && (
|
|
174
|
+
<div className="px-2.5 pb-0.5 pt-1.5 font-medium text-[9px] uppercase tracking-wider text-muted-foreground/40">
|
|
175
|
+
{group.label}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
{group.options.map((o) => (
|
|
179
|
+
<button
|
|
180
|
+
key={o.value}
|
|
181
|
+
type="button"
|
|
182
|
+
onClick={() => select(o.value)}
|
|
183
|
+
className={cn(
|
|
184
|
+
'flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-[11px] transition-colors',
|
|
185
|
+
value === o.value
|
|
186
|
+
? 'bg-surface-1 text-foreground'
|
|
187
|
+
: 'text-muted-foreground hover:bg-surface-1/50 hover:text-foreground',
|
|
188
|
+
)}
|
|
189
|
+
>
|
|
190
|
+
<span className="min-w-0 flex-1 truncate">{labelWithinGroup(o.label, group.label)}</span>
|
|
191
|
+
</button>
|
|
192
|
+
))}
|
|
193
|
+
</React.Fragment>
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
{footerAction && (
|
|
197
|
+
<div className="border-t border-border/20 p-1">
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
onClick={() => {
|
|
201
|
+
close();
|
|
202
|
+
footerAction.onClick();
|
|
203
|
+
}}
|
|
204
|
+
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-[11px] text-muted-foreground transition-colors hover:bg-surface-1/50 hover:text-foreground"
|
|
205
|
+
>
|
|
206
|
+
<Plus className="shrink-0" size={11} />
|
|
207
|
+
{footerAction.label}
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|