@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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { storage } from '../utils/storage';
|
|
3
|
+
import { BUILT_IN_THEMES, type ThemeInfo } from '../utils/themeRegistry';
|
|
4
|
+
|
|
5
|
+
export type Mode = 'dark' | 'light' | 'system';
|
|
6
|
+
|
|
7
|
+
type ThemeProviderState = {
|
|
8
|
+
// Mode (dark/light/system) — backward-compatible with old "theme" API
|
|
9
|
+
theme: Mode;
|
|
10
|
+
setTheme: (mode: Mode) => void;
|
|
11
|
+
mode: Mode;
|
|
12
|
+
setMode: (mode: Mode) => void;
|
|
13
|
+
resolvedMode: 'dark' | 'light';
|
|
14
|
+
// Color theme (palette)
|
|
15
|
+
colorTheme: string;
|
|
16
|
+
setColorTheme: (theme: string) => void;
|
|
17
|
+
availableThemes: ThemeInfo[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const ThemeProviderContext = createContext<ThemeProviderState>({
|
|
21
|
+
theme: 'dark',
|
|
22
|
+
setTheme: () => null,
|
|
23
|
+
mode: 'dark',
|
|
24
|
+
setMode: () => null,
|
|
25
|
+
resolvedMode: 'dark',
|
|
26
|
+
colorTheme: 'plannotator',
|
|
27
|
+
setColorTheme: () => null,
|
|
28
|
+
availableThemes: BUILT_IN_THEMES,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** Resolve the class string for a theme + mode combination */
|
|
32
|
+
function resolveThemeClasses(themeId: string, effectiveMode: 'dark' | 'light'): string {
|
|
33
|
+
const themeInfo = BUILT_IN_THEMES.find(t => t.id === themeId);
|
|
34
|
+
const modeSupport = themeInfo?.modeSupport ?? 'both';
|
|
35
|
+
|
|
36
|
+
let applyLight = effectiveMode === 'light';
|
|
37
|
+
if (modeSupport === 'dark-only') applyLight = false;
|
|
38
|
+
if (modeSupport === 'light-only') applyLight = true;
|
|
39
|
+
|
|
40
|
+
return `theme-${themeId}${applyLight ? ' light' : ''}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Sync theme classes on <html> without stripping non-theme classes (e.g. transitions-ready). */
|
|
44
|
+
function applyThemeClasses(themeId: string, effectiveMode: 'dark' | 'light'): void {
|
|
45
|
+
const el = document.documentElement;
|
|
46
|
+
const themeClass = `theme-${themeId}`;
|
|
47
|
+
const wantLight = resolveThemeClasses(themeId, effectiveMode).includes(' light');
|
|
48
|
+
|
|
49
|
+
if (el.classList.contains(themeClass) && el.classList.contains('light') === wantLight) return;
|
|
50
|
+
|
|
51
|
+
for (const cls of Array.from(el.classList)) {
|
|
52
|
+
if (cls.startsWith('theme-')) el.classList.remove(cls);
|
|
53
|
+
}
|
|
54
|
+
el.classList.remove('light');
|
|
55
|
+
|
|
56
|
+
el.classList.add(themeClass);
|
|
57
|
+
if (wantLight) el.classList.add('light');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Read system preference synchronously */
|
|
61
|
+
function getSystemIsLight(): boolean {
|
|
62
|
+
return typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: light)').matches;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ThemeProviderProps {
|
|
66
|
+
children: React.ReactNode;
|
|
67
|
+
defaultTheme?: Mode;
|
|
68
|
+
defaultColorTheme?: string;
|
|
69
|
+
storageKey?: string;
|
|
70
|
+
colorThemeStorageKey?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function ThemeProvider({
|
|
74
|
+
children,
|
|
75
|
+
defaultTheme = 'dark',
|
|
76
|
+
defaultColorTheme = 'plannotator',
|
|
77
|
+
storageKey = 'plannotator-theme',
|
|
78
|
+
colorThemeStorageKey = 'plannotator-color-theme',
|
|
79
|
+
}: ThemeProviderProps) {
|
|
80
|
+
const [mode, setModeState] = useState<Mode>(
|
|
81
|
+
() => (storage.getItem(storageKey) as Mode) || defaultTheme
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const [colorTheme, setColorThemeState] = useState<string>(
|
|
85
|
+
() => storage.getItem(colorThemeStorageKey) || defaultColorTheme
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const [systemIsLight, setSystemIsLight] = useState(getSystemIsLight);
|
|
89
|
+
|
|
90
|
+
// Compute resolved mode once — consumers use this instead of re-querying matchMedia
|
|
91
|
+
const resolvedMode: 'dark' | 'light' = mode === 'system' ? (systemIsLight ? 'light' : 'dark') : mode;
|
|
92
|
+
|
|
93
|
+
// [P3 fix] Apply theme class synchronously during initialization to prevent
|
|
94
|
+
// flash of unstyled content. CSS tokens live under .theme-* selectors, so
|
|
95
|
+
// without this the first frame has no valid --background/--foreground.
|
|
96
|
+
if (typeof window !== 'undefined') {
|
|
97
|
+
applyThemeClasses(colorTheme, resolvedMode);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Keep class in sync after state changes
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
applyThemeClasses(colorTheme, resolvedMode);
|
|
103
|
+
}, [resolvedMode, colorTheme]);
|
|
104
|
+
|
|
105
|
+
// Enable color transitions after mount settles — prevents the global *
|
|
106
|
+
// transition rule from firing during initial load.
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
requestAnimationFrame(() => {
|
|
109
|
+
document.documentElement.classList.add('transitions-ready');
|
|
110
|
+
});
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
// [P2 fix] Listen for system theme changes AND re-read current value when
|
|
114
|
+
// entering system mode (OS may have changed while pinned to explicit mode)
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (mode !== 'system') return;
|
|
117
|
+
|
|
118
|
+
// Sync immediately — OS preference may have changed since we last checked
|
|
119
|
+
setSystemIsLight(getSystemIsLight());
|
|
120
|
+
|
|
121
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: light)');
|
|
122
|
+
const handleChange = () => setSystemIsLight(mediaQuery.matches);
|
|
123
|
+
|
|
124
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
125
|
+
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
126
|
+
}, [mode]);
|
|
127
|
+
|
|
128
|
+
const setMode = useCallback((newMode: Mode) => {
|
|
129
|
+
storage.setItem(storageKey, newMode);
|
|
130
|
+
setModeState(newMode);
|
|
131
|
+
}, [storageKey]);
|
|
132
|
+
|
|
133
|
+
const setColorTheme = useCallback((newTheme: string) => {
|
|
134
|
+
storage.setItem(colorThemeStorageKey, newTheme);
|
|
135
|
+
setColorThemeState(newTheme);
|
|
136
|
+
}, [colorThemeStorageKey]);
|
|
137
|
+
|
|
138
|
+
const value = useMemo<ThemeProviderState>(() => ({
|
|
139
|
+
theme: mode,
|
|
140
|
+
setTheme: setMode,
|
|
141
|
+
mode,
|
|
142
|
+
setMode,
|
|
143
|
+
resolvedMode,
|
|
144
|
+
colorTheme,
|
|
145
|
+
setColorTheme,
|
|
146
|
+
availableThemes: BUILT_IN_THEMES,
|
|
147
|
+
}), [mode, resolvedMode, colorTheme, setMode, setColorTheme]);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<ThemeProviderContext.Provider value={value}>
|
|
151
|
+
{children}
|
|
152
|
+
</ThemeProviderContext.Provider>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const useTheme = () => {
|
|
157
|
+
const context = useContext(ThemeProviderContext);
|
|
158
|
+
if (context === undefined) {
|
|
159
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
160
|
+
}
|
|
161
|
+
return context;
|
|
162
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTheme, type Mode } from './ThemeProvider';
|
|
3
|
+
import { SunIcon, MoonIcon, SystemIcon } from './icons/themeIcons';
|
|
4
|
+
|
|
5
|
+
interface ThemeTabProps {
|
|
6
|
+
onPreview?: () => void;
|
|
7
|
+
compact?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ThemeTab: React.FC<ThemeTabProps> = ({ onPreview, compact }) => {
|
|
11
|
+
const { mode, setMode, colorTheme, setColorTheme, availableThemes, resolvedMode } = useTheme();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className={compact ? '' : 'space-y-5'}>
|
|
15
|
+
{/* Mode */}
|
|
16
|
+
<div className={compact ? 'flex items-center gap-3 mb-2' : 'space-y-2'}>
|
|
17
|
+
{!compact && <label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Mode</label>}
|
|
18
|
+
<div className="flex gap-1">
|
|
19
|
+
{(['dark', 'light', 'system'] as Mode[]).map(m => {
|
|
20
|
+
const isActive = mode === m;
|
|
21
|
+
return (
|
|
22
|
+
<button
|
|
23
|
+
key={m}
|
|
24
|
+
onClick={() => setMode(m)}
|
|
25
|
+
className={`px-2.5 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
|
26
|
+
isActive
|
|
27
|
+
? 'bg-primary text-primary-foreground'
|
|
28
|
+
: 'bg-muted text-muted-foreground hover:text-foreground'
|
|
29
|
+
}`}
|
|
30
|
+
>
|
|
31
|
+
{m === 'dark' && (
|
|
32
|
+
<span className="flex items-center gap-1.5">
|
|
33
|
+
<MoonIcon className="w-3 h-3" />
|
|
34
|
+
Dark
|
|
35
|
+
</span>
|
|
36
|
+
)}
|
|
37
|
+
{m === 'light' && (
|
|
38
|
+
<span className="flex items-center gap-1.5">
|
|
39
|
+
<SunIcon className="w-3 h-3" />
|
|
40
|
+
Light
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
{m === 'system' && (
|
|
44
|
+
<span className="flex items-center gap-1.5">
|
|
45
|
+
<SystemIcon className="w-3 h-3" />
|
|
46
|
+
System
|
|
47
|
+
</span>
|
|
48
|
+
)}
|
|
49
|
+
</button>
|
|
50
|
+
);
|
|
51
|
+
})}
|
|
52
|
+
</div>
|
|
53
|
+
{compact && (
|
|
54
|
+
<span className="text-[10px] text-muted-foreground/60 ml-auto flex items-center gap-1">
|
|
55
|
+
<svg className="w-2.5 h-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
56
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h7" />
|
|
57
|
+
</svg>
|
|
58
|
+
syntax match
|
|
59
|
+
</span>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Theme */}
|
|
64
|
+
<div className={compact ? '' : 'space-y-3'}>
|
|
65
|
+
{!compact && (
|
|
66
|
+
<div className="flex items-center justify-between border-t border-border pt-5">
|
|
67
|
+
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Theme</label>
|
|
68
|
+
<div className="flex items-center gap-3">
|
|
69
|
+
<span className="text-[10px] text-muted-foreground/70 flex items-center gap-1">
|
|
70
|
+
<svg className="w-2.5 h-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
71
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h7" />
|
|
72
|
+
</svg>
|
|
73
|
+
= matched syntax colors
|
|
74
|
+
</span>
|
|
75
|
+
{onPreview && (
|
|
76
|
+
<button
|
|
77
|
+
onClick={onPreview}
|
|
78
|
+
className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-primary/10 text-primary border border-primary/20 hover:bg-primary/20 hover:border-primary/40 transition-colors"
|
|
79
|
+
>
|
|
80
|
+
Preview Mode
|
|
81
|
+
</button>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
<div className={`grid gap-2 overflow-y-auto pr-1 ${compact ? 'grid-cols-4' : 'grid-cols-3'}`}>
|
|
87
|
+
{availableThemes.map(theme => {
|
|
88
|
+
const isSelected = colorTheme === theme.id;
|
|
89
|
+
const colors = theme.colors[resolvedMode];
|
|
90
|
+
const modeUnavailable =
|
|
91
|
+
(resolvedMode === 'light' && theme.modeSupport === 'dark-only') ||
|
|
92
|
+
(resolvedMode === 'dark' && theme.modeSupport === 'light-only');
|
|
93
|
+
return (
|
|
94
|
+
<button
|
|
95
|
+
key={theme.id}
|
|
96
|
+
onClick={() => setColorTheme(theme.id)}
|
|
97
|
+
className={`relative p-2 rounded-md border text-left transition-colors ${
|
|
98
|
+
isSelected
|
|
99
|
+
? 'border-primary bg-primary/5'
|
|
100
|
+
: modeUnavailable
|
|
101
|
+
? 'border-border/50 opacity-45'
|
|
102
|
+
: 'border-border hover:border-muted-foreground/30 hover:bg-muted/30'
|
|
103
|
+
}`}
|
|
104
|
+
>
|
|
105
|
+
{/* Syntax highlighting badge */}
|
|
106
|
+
{theme.syntaxHighlighting && (
|
|
107
|
+
<div className="absolute top-1 right-1" title="Matched syntax highlighting in diffs">
|
|
108
|
+
<svg className="w-2.5 h-2.5 text-muted-foreground/50" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
109
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h7" />
|
|
110
|
+
</svg>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
{/* Color swatches */}
|
|
114
|
+
<div className="flex gap-1 mb-1.5">
|
|
115
|
+
{[colors.primary, colors.secondary, colors.accent, colors.background, colors.foreground].map((color, i) => (
|
|
116
|
+
<div
|
|
117
|
+
key={i}
|
|
118
|
+
className="w-3 h-3 rounded-full border border-border/50"
|
|
119
|
+
style={{ backgroundColor: color }}
|
|
120
|
+
/>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
{/* Name + checkmark */}
|
|
124
|
+
<div className="flex items-center justify-between">
|
|
125
|
+
<span className="text-xs text-foreground truncate">{theme.name}</span>
|
|
126
|
+
{isSelected && (
|
|
127
|
+
<svg className="w-3 h-3 text-primary flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
128
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
129
|
+
</svg>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</button>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button } from './ui/button';
|
|
3
|
+
import { cn } from '../lib/utils';
|
|
4
|
+
import { Send, Check } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface FeedbackButtonProps {
|
|
7
|
+
onClick: () => void;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
isLoading?: boolean;
|
|
10
|
+
label?: string;
|
|
11
|
+
shortLabel?: string;
|
|
12
|
+
loadingLabel?: string;
|
|
13
|
+
shortLoadingLabel?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
muted?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const FeedbackButton: React.FC<FeedbackButtonProps> = ({
|
|
19
|
+
onClick,
|
|
20
|
+
disabled = false,
|
|
21
|
+
isLoading = false,
|
|
22
|
+
label = 'Send Feedback',
|
|
23
|
+
shortLabel,
|
|
24
|
+
loadingLabel = 'Sending...',
|
|
25
|
+
shortLoadingLabel,
|
|
26
|
+
title = 'Send Feedback',
|
|
27
|
+
muted = false,
|
|
28
|
+
}) => (
|
|
29
|
+
<Button
|
|
30
|
+
variant="outline"
|
|
31
|
+
size="xs"
|
|
32
|
+
onClick={onClick}
|
|
33
|
+
disabled={disabled}
|
|
34
|
+
title={title}
|
|
35
|
+
iconLeft={<Send className="size-3.5" />}
|
|
36
|
+
className={cn(muted && 'opacity-50 cursor-not-allowed')}
|
|
37
|
+
>
|
|
38
|
+
{shortLabel ? (
|
|
39
|
+
<>
|
|
40
|
+
<span className="hidden md:inline lg:hidden">{isLoading ? (shortLoadingLabel ?? loadingLabel) : shortLabel}</span>
|
|
41
|
+
<span className="hidden lg:inline">{isLoading ? loadingLabel : label}</span>
|
|
42
|
+
</>
|
|
43
|
+
) : (
|
|
44
|
+
<span className="hidden md:inline">{isLoading ? loadingLabel : label}</span>
|
|
45
|
+
)}
|
|
46
|
+
</Button>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export interface ApproveButtonProps {
|
|
50
|
+
onClick: () => void;
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
isLoading?: boolean;
|
|
53
|
+
label?: string;
|
|
54
|
+
loadingLabel?: string;
|
|
55
|
+
mobileLabel?: string;
|
|
56
|
+
mobileLoadingLabel?: string;
|
|
57
|
+
title?: string;
|
|
58
|
+
dimmed?: boolean;
|
|
59
|
+
muted?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const ApproveButton: React.FC<ApproveButtonProps> = ({
|
|
63
|
+
onClick,
|
|
64
|
+
disabled = false,
|
|
65
|
+
isLoading = false,
|
|
66
|
+
label = 'Approve',
|
|
67
|
+
loadingLabel = 'Approving...',
|
|
68
|
+
mobileLabel = 'OK',
|
|
69
|
+
mobileLoadingLabel = '...',
|
|
70
|
+
title,
|
|
71
|
+
dimmed = false,
|
|
72
|
+
muted = false,
|
|
73
|
+
}) => (
|
|
74
|
+
<Button
|
|
75
|
+
variant="success"
|
|
76
|
+
size="xs"
|
|
77
|
+
onClick={onClick}
|
|
78
|
+
disabled={disabled}
|
|
79
|
+
title={title}
|
|
80
|
+
iconLeft={<Check className="size-3.5" />}
|
|
81
|
+
className={cn(
|
|
82
|
+
muted && 'opacity-40 cursor-not-allowed bg-muted text-muted-foreground hover:bg-muted',
|
|
83
|
+
disabled && !muted && 'bg-muted text-muted-foreground hover:bg-muted',
|
|
84
|
+
dimmed && !muted && !disabled && 'bg-success/50 text-success-foreground/70 hover:bg-success hover:text-success-foreground',
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<span className="md:hidden">{isLoading ? mobileLoadingLabel : mobileLabel}</span>
|
|
88
|
+
<span className="hidden md:inline">{isLoading ? loadingLabel : label}</span>
|
|
89
|
+
</Button>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
interface ExitButtonProps {
|
|
93
|
+
onClick: () => void;
|
|
94
|
+
disabled?: boolean;
|
|
95
|
+
isLoading?: boolean;
|
|
96
|
+
title?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const ExitButton: React.FC<ExitButtonProps> = ({
|
|
100
|
+
onClick,
|
|
101
|
+
disabled = false,
|
|
102
|
+
isLoading = false,
|
|
103
|
+
title = 'Close session without sending feedback',
|
|
104
|
+
}) => (
|
|
105
|
+
<Button
|
|
106
|
+
variant="secondary"
|
|
107
|
+
size="xs"
|
|
108
|
+
onClick={onClick}
|
|
109
|
+
disabled={disabled || isLoading}
|
|
110
|
+
title={title}
|
|
111
|
+
className="bg-muted text-muted-foreground hover:bg-muted/80"
|
|
112
|
+
>
|
|
113
|
+
<span className="md:hidden">{isLoading ? '...' : '✕'}</span>
|
|
114
|
+
<span className="hidden md:inline">{isLoading ? 'Closing...' : 'Close'}</span>
|
|
115
|
+
</Button>
|
|
116
|
+
);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import * as RadixTooltip from '@radix-ui/react-tooltip';
|
|
3
|
+
|
|
4
|
+
export const TooltipProvider = RadixTooltip.Provider;
|
|
5
|
+
|
|
6
|
+
interface TooltipProps {
|
|
7
|
+
content: React.ReactNode;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
10
|
+
align?: 'start' | 'center' | 'end';
|
|
11
|
+
delayDuration?: number;
|
|
12
|
+
sideOffset?: number;
|
|
13
|
+
/**
|
|
14
|
+
* When true, allow the tooltip to wrap onto multiple lines with a reasonable
|
|
15
|
+
* max width. Default is single-line (nowrap) — matches the original callsites
|
|
16
|
+
* that use it for short button labels.
|
|
17
|
+
*/
|
|
18
|
+
wide?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Tooltip: React.FC<TooltipProps> = ({
|
|
22
|
+
content,
|
|
23
|
+
children,
|
|
24
|
+
side = 'top',
|
|
25
|
+
align = 'center',
|
|
26
|
+
delayDuration,
|
|
27
|
+
sideOffset = 8,
|
|
28
|
+
wide = false,
|
|
29
|
+
}) => (
|
|
30
|
+
<RadixTooltip.Root delayDuration={delayDuration}>
|
|
31
|
+
<RadixTooltip.Trigger asChild>{children}</RadixTooltip.Trigger>
|
|
32
|
+
<RadixTooltip.Portal>
|
|
33
|
+
<RadixTooltip.Content
|
|
34
|
+
side={side}
|
|
35
|
+
align={align}
|
|
36
|
+
sideOffset={sideOffset}
|
|
37
|
+
className={`z-50 px-2 py-1 text-xs bg-popover text-popover-foreground border border-border rounded shadow-md origin-[var(--radix-tooltip-content-transform-origin)] transition-[opacity,transform] duration-150 ease-out data-[state=closed]:opacity-0 data-[state=closed]:scale-95 data-[state=delayed-open]:opacity-100 data-[state=delayed-open]:scale-100 data-[state=instant-open]:opacity-100 data-[state=instant-open]:scale-100 ${
|
|
38
|
+
wide ? 'max-w-[260px] leading-snug whitespace-normal' : 'whitespace-nowrap'
|
|
39
|
+
}`}
|
|
40
|
+
>
|
|
41
|
+
{content}
|
|
42
|
+
</RadixTooltip.Content>
|
|
43
|
+
</RadixTooltip.Portal>
|
|
44
|
+
</RadixTooltip.Root>
|
|
45
|
+
);
|