@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.
Files changed (305) hide show
  1. package/README.md +60 -0
  2. package/assets/diff-options.png +0 -0
  3. package/assets/icon-codex.png +0 -0
  4. package/assets/look-flat.png +0 -0
  5. package/assets/look-grid.png +0 -0
  6. package/assets/review-sections.png +0 -0
  7. package/assets/review-tree.png +0 -0
  8. package/assets/workspaces.webp +0 -0
  9. package/components/AISettingsTab.tsx +145 -0
  10. package/components/ActionMenu.tsx +101 -0
  11. package/components/AgentControls.tsx +216 -0
  12. package/components/AgentsTab.tsx +1294 -0
  13. package/components/AnnotationPanel.tsx +731 -0
  14. package/components/AnnotationSidebar.tsx +86 -0
  15. package/components/AnnotationToolbar.tsx +323 -0
  16. package/components/AnnotationToolstrip.tsx +355 -0
  17. package/components/ApproveDropdown.tsx +170 -0
  18. package/components/AttachmentsButton.tsx +410 -0
  19. package/components/BlockRenderer.tsx +163 -0
  20. package/components/BorderTrail.tsx +38 -0
  21. package/components/CodeFilePicker.tsx +64 -0
  22. package/components/CodeFilePopout.tsx +602 -0
  23. package/components/CodePathValidationContext.tsx +14 -0
  24. package/components/CommentPopover.tsx +512 -0
  25. package/components/CompletionOverlay.tsx +99 -0
  26. package/components/ConfirmDialog.tsx +128 -0
  27. package/components/DocBadges.tsx +231 -0
  28. package/components/EditorAnnotationCard.tsx +65 -0
  29. package/components/ExportModal.tsx +565 -0
  30. package/components/FloatingQuickLabelPicker.tsx +144 -0
  31. package/components/GitHubIcon.tsx +16 -0
  32. package/components/GitLabIcon.tsx +30 -0
  33. package/components/GraphvizBlock.tsx +511 -0
  34. package/components/ImageAnnotator/Canvas.tsx +118 -0
  35. package/components/ImageAnnotator/Toolbar.tsx +218 -0
  36. package/components/ImageAnnotator/index.tsx +272 -0
  37. package/components/ImageAnnotator/types.ts +39 -0
  38. package/components/ImageAnnotator/utils.ts +147 -0
  39. package/components/ImageThumbnail.tsx +126 -0
  40. package/components/ImportModal.tsx +146 -0
  41. package/components/InlineMarkdown.tsx +1094 -0
  42. package/components/KeyboardShortcuts.tsx +208 -0
  43. package/components/Landing.tsx +496 -0
  44. package/components/ListItemBody.tsx +62 -0
  45. package/components/ListMarker.tsx +73 -0
  46. package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
  47. package/components/MarkdownEditor.tsx +44 -0
  48. package/components/MenuVersionSection.tsx +91 -0
  49. package/components/MermaidBlock.tsx +579 -0
  50. package/components/ModeToggle.tsx +68 -0
  51. package/components/OpenInAppButton.tsx +299 -0
  52. package/components/OverlayScrollArea.tsx +82 -0
  53. package/components/PermissionModeSetup.tsx +92 -0
  54. package/components/PinpointOverlay.tsx +99 -0
  55. package/components/PlanAIAnnouncementDialog.tsx +187 -0
  56. package/components/PlanHeaderMenu.tsx +299 -0
  57. package/components/PopoutDialog.tsx +91 -0
  58. package/components/Popover.tsx +27 -0
  59. package/components/ProviderIcons.tsx +51 -0
  60. package/components/PullRequestIcon.tsx +12 -0
  61. package/components/QuickLabelDropdown.tsx +63 -0
  62. package/components/RenderedMarkdown.tsx +54 -0
  63. package/components/RepoIcon.tsx +12 -0
  64. package/components/ResizeHandle.tsx +86 -0
  65. package/components/ReviewAgentsIcon.tsx +18 -0
  66. package/components/SearchableSelect.tsx +157 -0
  67. package/components/Settings.tsx +2220 -0
  68. package/components/SparklesIcon.tsx +45 -0
  69. package/components/StickyHeaderLane.tsx +260 -0
  70. package/components/TableOfContents.tsx +157 -0
  71. package/components/TaterSpritePullup.tsx +34 -0
  72. package/components/TaterSpriteRunning.tsx +54 -0
  73. package/components/TaterSpriteSitting.tsx +37 -0
  74. package/components/TextShimmer.tsx +57 -0
  75. package/components/ThemeProvider.tsx +162 -0
  76. package/components/ThemeTab.tsx +139 -0
  77. package/components/ToolbarButtons.tsx +116 -0
  78. package/components/Tooltip.tsx +45 -0
  79. package/components/Viewer.tsx +977 -0
  80. package/components/ai/AIProviderBar.tsx +95 -0
  81. package/components/ai/DocumentAIChatPanel.tsx +313 -0
  82. package/components/blocks/AlertBlock.tsx +58 -0
  83. package/components/blocks/Callout.tsx +60 -0
  84. package/components/blocks/CodeBlock.tsx +75 -0
  85. package/components/blocks/HtmlBlock.tsx +122 -0
  86. package/components/blocks/MathBlock.tsx +36 -0
  87. package/components/blocks/TableBlock.tsx +146 -0
  88. package/components/blocks/TablePopout.tsx +279 -0
  89. package/components/blocks/TableToolbar.tsx +153 -0
  90. package/components/blocks/proseBody.tsx +102 -0
  91. package/components/core/button.tsx +44 -0
  92. package/components/core/textarea.tsx +26 -0
  93. package/components/diagramLanguages.ts +14 -0
  94. package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
  95. package/components/html-viewer/HtmlViewer.tsx +386 -0
  96. package/components/html-viewer/bridge-script.ts +505 -0
  97. package/components/html-viewer/index.ts +1 -0
  98. package/components/html-viewer/useHtmlAnnotation.ts +400 -0
  99. package/components/icons/AgentIcons.tsx +66 -0
  100. package/components/icons/AppIcon.tsx +56 -0
  101. package/components/icons/MessagesIcon.tsx +11 -0
  102. package/components/icons/ObsidianIcons.tsx +208 -0
  103. package/components/icons/app/android-studio.svg +369 -0
  104. package/components/icons/app/antigravity.svg +97 -0
  105. package/components/icons/app/cursor.svg +16 -0
  106. package/components/icons/app/file-explorer.svg +20 -0
  107. package/components/icons/app/finder.png +0 -0
  108. package/components/icons/app/ghostty.svg +13 -0
  109. package/components/icons/app/iterm2.svg +13 -0
  110. package/components/icons/app/powershell.svg +14 -0
  111. package/components/icons/app/sublime-text.svg +17 -0
  112. package/components/icons/app/terminal.png +0 -0
  113. package/components/icons/app/textmate.png +0 -0
  114. package/components/icons/app/vscode.svg +39 -0
  115. package/components/icons/app/warp.png +0 -0
  116. package/components/icons/app/xcode.png +0 -0
  117. package/components/icons/app/zed-dark.svg +15 -0
  118. package/components/icons/app/zed.svg +15 -0
  119. package/components/icons/themeIcons.tsx +47 -0
  120. package/components/mermaidSvg.ts +33 -0
  121. package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
  122. package/components/plan-diff/PlanDiffBadge.tsx +48 -0
  123. package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
  124. package/components/plan-diff/PlanDiffViewer.tsx +214 -0
  125. package/components/plan-diff/PlanRawDiffView.tsx +102 -0
  126. package/components/plan-diff/VSCodeIcon.tsx +132 -0
  127. package/components/settings/HooksTab.tsx +208 -0
  128. package/components/sidebar/ArchiveBrowser.tsx +98 -0
  129. package/components/sidebar/CountBadge.tsx +12 -0
  130. package/components/sidebar/FileBrowser.tsx +498 -0
  131. package/components/sidebar/MessagesBrowser.tsx +109 -0
  132. package/components/sidebar/SidebarContainer.tsx +353 -0
  133. package/components/sidebar/SidebarTabs.tsx +155 -0
  134. package/components/sidebar/VersionBrowser.tsx +141 -0
  135. package/components/types.d.ts +7 -0
  136. package/components/ui/badge.tsx +44 -0
  137. package/components/ui/button.tsx +83 -0
  138. package/components/ui/card.tsx +58 -0
  139. package/components/ui/dialog.tsx +106 -0
  140. package/components/ui/dropdown-menu.tsx +240 -0
  141. package/components/ui/state-pill.tsx +45 -0
  142. package/components/ui/tabs.tsx +46 -0
  143. package/components/ui/textarea.tsx +25 -0
  144. package/config/configStore.ts +214 -0
  145. package/config/index.ts +4 -0
  146. package/config/reviewView.ts +42 -0
  147. package/config/settings.ts +310 -0
  148. package/config/useConfig.ts +20 -0
  149. package/configure.ts +69 -0
  150. package/globals.d.ts +14 -0
  151. package/hooks/pfm/useCodeFilePopout.ts +111 -0
  152. package/hooks/useAIChat.ts +532 -0
  153. package/hooks/useAIProviderConfig.ts +115 -0
  154. package/hooks/useActiveSection.ts +78 -0
  155. package/hooks/useAgentJobs.ts +306 -0
  156. package/hooks/useAgentSettings.ts +579 -0
  157. package/hooks/useAgents.ts +90 -0
  158. package/hooks/useAnnotationDraft.ts +523 -0
  159. package/hooks/useAnnotationHighlighter.ts +1170 -0
  160. package/hooks/useArchive.ts +171 -0
  161. package/hooks/useAutoClose.ts +104 -0
  162. package/hooks/useCodeAnnotationDraft.ts +194 -0
  163. package/hooks/useCodeFilePopout.ts +1 -0
  164. package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
  165. package/hooks/useDraggable.ts +108 -0
  166. package/hooks/useEditorAnnotations.ts +64 -0
  167. package/hooks/useExternalAnnotationHighlights.ts +105 -0
  168. package/hooks/useExternalAnnotations.ts +268 -0
  169. package/hooks/useFileBrowser.ts +402 -0
  170. package/hooks/useInputMethodSwitch.ts +89 -0
  171. package/hooks/useIsMobile.ts +17 -0
  172. package/hooks/useLinkedDoc.ts +494 -0
  173. package/hooks/useOverlayViewport.ts +36 -0
  174. package/hooks/usePinpoint.ts +182 -0
  175. package/hooks/usePlanDiff.ts +176 -0
  176. package/hooks/usePrintMode.ts +27 -0
  177. package/hooks/useResizablePanel.ts +173 -0
  178. package/hooks/useScrollViewport.ts +38 -0
  179. package/hooks/useSharing.ts +441 -0
  180. package/hooks/useSidebar.ts +55 -0
  181. package/hooks/useUpdateCheck.ts +132 -0
  182. package/hooks/useValidatedCodePaths.ts +94 -0
  183. package/icons/GitUser.tsx +17 -0
  184. package/lib/utils.ts +11 -0
  185. package/package.json +117 -0
  186. package/plannotator.webp +0 -0
  187. package/print.css +427 -0
  188. package/shortcuts/code-review/ai.shortcuts.ts +25 -0
  189. package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
  190. package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
  191. package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
  192. package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
  193. package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
  194. package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
  195. package/shortcuts/core.ts +399 -0
  196. package/shortcuts/index.ts +21 -0
  197. package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
  198. package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
  199. package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
  200. package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
  201. package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
  202. package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
  203. package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
  204. package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
  205. package/shortcuts/runtime.ts +259 -0
  206. package/sprite_package_additional/index.html +34 -0
  207. package/sprite_package_additional/sprite.png +0 -0
  208. package/sprite_package_new/index.html +34 -0
  209. package/sprite_package_new/sprite.png +0 -0
  210. package/sprite_package_pulluphang/index.html +34 -0
  211. package/sprite_package_pulluphang/sprite.png +0 -0
  212. package/styles.css +1 -0
  213. package/theme.css +893 -0
  214. package/themes/adwaita.css +62 -0
  215. package/themes/andromeeda.css +66 -0
  216. package/themes/aurora-x.css +66 -0
  217. package/themes/ayu-dark.css +66 -0
  218. package/themes/caffeine.css +113 -0
  219. package/themes/catppuccin.css +62 -0
  220. package/themes/claude-plus.css +60 -0
  221. package/themes/cursor-hc.css +34 -0
  222. package/themes/cursor-midnight.css +34 -0
  223. package/themes/cursor.css +62 -0
  224. package/themes/dark-plus.css +66 -0
  225. package/themes/doom-64.css +109 -0
  226. package/themes/dracula.css +33 -0
  227. package/themes/everforest-hard.css +62 -0
  228. package/themes/everforest-soft.css +62 -0
  229. package/themes/everforest.css +62 -0
  230. package/themes/github.css +66 -0
  231. package/themes/gruvbox.css +62 -0
  232. package/themes/houston.css +66 -0
  233. package/themes/kanagawa-dragon.css +34 -0
  234. package/themes/kanagawa-lotus.css +34 -0
  235. package/themes/kanagawa-wave.css +34 -0
  236. package/themes/laserwave.css +66 -0
  237. package/themes/material.css +62 -0
  238. package/themes/min.css +66 -0
  239. package/themes/monokai-pro.css +34 -0
  240. package/themes/neutral.css +59 -0
  241. package/themes/night-owl.css +66 -0
  242. package/themes/nord.css +66 -0
  243. package/themes/one-dark-pro.css +66 -0
  244. package/themes/one-light.css +66 -0
  245. package/themes/paulmillr.css +34 -0
  246. package/themes/plannotator.css +60 -0
  247. package/themes/plastic.css +66 -0
  248. package/themes/poimandres.css +66 -0
  249. package/themes/quantum-rose.css +109 -0
  250. package/themes/red.css +66 -0
  251. package/themes/rose-pine.css +62 -0
  252. package/themes/simple.css +124 -0
  253. package/themes/slack.css +66 -0
  254. package/themes/snazzy-light.css +66 -0
  255. package/themes/soft-pop.css +60 -0
  256. package/themes/solar-dusk.css +109 -0
  257. package/themes/solarized.css +66 -0
  258. package/themes/synthwave-84.css +34 -0
  259. package/themes/terminal.css +62 -0
  260. package/themes/tinacious.css +57 -0
  261. package/themes/tokyo-night.css +62 -0
  262. package/themes/vesper.css +62 -0
  263. package/themes/vitesse-black.css +66 -0
  264. package/themes/vitesse.css +62 -0
  265. package/types.ts +264 -0
  266. package/utils/agentSwitch.ts +71 -0
  267. package/utils/aiChatFormat.ts +33 -0
  268. package/utils/aiPrompt.ts +29 -0
  269. package/utils/aiProvider.ts +217 -0
  270. package/utils/anchors.ts +11 -0
  271. package/utils/annotateAgentTerminal.ts +29 -0
  272. package/utils/annotationHelpers.ts +101 -0
  273. package/utils/bear.ts +59 -0
  274. package/utils/blockTargeting.ts +240 -0
  275. package/utils/callback.ts +99 -0
  276. package/utils/commentContent.ts +8 -0
  277. package/utils/defaultNotesApp.ts +20 -0
  278. package/utils/diffFonts.ts +33 -0
  279. package/utils/editorMode.ts +31 -0
  280. package/utils/fileBrowser.ts +40 -0
  281. package/utils/generateId.ts +7 -0
  282. package/utils/generateIdentity.ts +29 -0
  283. package/utils/identity.ts +112 -0
  284. package/utils/inlineTransforms.ts +41 -0
  285. package/utils/inputMethod.ts +17 -0
  286. package/utils/lookAndFeelAnnouncement.ts +18 -0
  287. package/utils/obsidian.ts +201 -0
  288. package/utils/octarine.ts +53 -0
  289. package/utils/parser.ts +991 -0
  290. package/utils/permissionMode.ts +77 -0
  291. package/utils/planAIAnnouncement.ts +17 -0
  292. package/utils/planAgentInstructions.ts +137 -0
  293. package/utils/planDiffEngine.ts +590 -0
  294. package/utils/planSave.ts +49 -0
  295. package/utils/platform.ts +11 -0
  296. package/utils/quickLabels.ts +77 -0
  297. package/utils/reviewAgentInstructions.ts +182 -0
  298. package/utils/sanitizeHtml.ts +45 -0
  299. package/utils/sharing.ts +350 -0
  300. package/utils/slugify.ts +37 -0
  301. package/utils/storage.ts +141 -0
  302. package/utils/themeRegistry.ts +567 -0
  303. package/utils/uiPreferences.ts +34 -0
  304. package/utils/upload.ts +56 -0
  305. 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
+ );