@plannotator/ui 0.28.0 → 0.29.1
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 +6 -0
- package/components/AISettingsTab.tsx +5 -4
- package/components/ActionMenu.tsx +5 -1
- package/components/AgentsTab.tsx +10 -23
- package/components/AnnotationPanel.tsx +9 -5
- package/components/AnnotationToolbar.tsx +5 -13
- package/components/AnnotationToolstrip.tsx +2 -2
- package/components/ApproveDropdown.tsx +1 -1
- package/components/BlockRenderer.tsx +1 -1
- package/components/CodeFilePopout.tsx +5 -4
- package/components/CommentPopover.tsx +391 -65
- package/components/DocBadges.tsx +29 -11
- package/components/ExportModal.tsx +16 -8
- package/components/GraphvizBlock.tsx +1 -1
- package/components/InlineMarkdown.tsx +30 -13
- package/components/KeyboardShortcuts.tsx +37 -2
- package/components/Landing.tsx +7 -7
- package/components/MenuVersionSection.tsx +4 -4
- package/components/MermaidBlock.tsx +1 -1
- package/components/ModeToggle.tsx +7 -6
- package/components/OpenInAppButton.tsx +2 -5
- package/components/PinpointOverlay.tsx +9 -7
- package/components/PlanHeaderMenu.tsx +8 -8
- package/components/PopoutDialog.tsx +6 -1
- package/components/ResizeHandle.tsx +1 -0
- package/components/Settings.tsx +172 -12
- package/components/SkillReferenceMenu.tsx +260 -0
- package/components/StickyHeaderLane.tsx +7 -0
- package/components/ThemeProvider.tsx +131 -32
- package/components/ThemeTab.tsx +123 -77
- package/components/ToolbarButtons.tsx +29 -8
- package/components/Viewer.tsx +396 -130
- package/components/VimKeyHud.tsx +695 -0
- package/components/VimModeAnnouncementDialog.tsx +557 -0
- package/components/VimModeOverlay.tsx +235 -0
- package/components/VimTargetReticle.tsx +284 -0
- package/components/ai/DocumentAIChatPanel.tsx +1 -1
- package/components/blocks/CodeBlock.tsx +18 -18
- package/components/blocks/TablePopout.tsx +7 -8
- package/components/blocks/TableToolbar.tsx +7 -8
- package/components/goal-setup/GoalSetupSurface.tsx +16 -3
- package/components/html-viewer/HtmlViewer.tsx +450 -47
- package/components/html-viewer/annotationNumbering.ts +37 -0
- package/components/html-viewer/bridge-script.ts +4051 -298
- package/components/html-viewer/composerYield.ts +51 -0
- package/components/html-viewer/srcdoc.ts +18 -3
- package/components/html-viewer/useHtmlAnnotation.ts +457 -32
- package/components/icons/themeIcons.tsx +1 -1
- package/components/plan-diff/PlanCleanDiffView.tsx +9 -9
- package/components/plan-diff/PlanDiffBadge.tsx +22 -1
- package/components/settings/HooksTab.tsx +12 -8
- package/components/sidebar/FileBrowser.tsx +4 -1
- package/components/themeModes.tsx +28 -0
- package/config/configStore.ts +76 -1
- package/config/settings.ts +152 -0
- package/configure.ts +9 -0
- package/globals.d.ts +7 -1
- package/hooks/useAIChat.ts +5 -2
- package/hooks/useAIProviderActivation.ts +47 -0
- package/hooks/useAIProviderConfig.ts +5 -1
- package/hooks/useAgentSettings.ts +64 -23
- package/hooks/useAgents.ts +4 -4
- package/hooks/useAnnotationHighlighter.ts +100 -3
- package/hooks/useArchive.ts +2 -1
- package/hooks/useFenceTheme.ts +17 -0
- package/hooks/useLinkedDoc.ts +68 -1
- package/hooks/usePinpoint.ts +76 -75
- package/hooks/usePlanDiff.ts +73 -2
- package/hooks/useSkillReferenceAutocomplete.ts +239 -0
- package/hooks/useUpdateCheck.ts +1 -2
- package/hooks/useVimDocumentFocus.ts +116 -0
- package/hooks/useVimSelection.ts +1063 -0
- package/package.json +4 -4
- package/print.css +14 -13
- package/shortcuts/core.ts +38 -13
- package/shortcuts/index.ts +10 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +7 -0
- package/shortcuts/plan-review/vimSelection.shortcuts.ts +251 -0
- package/shortcuts/runtime.ts +111 -12
- package/styles.css +1 -1
- package/theme.css +504 -0
- package/themes/colorblind.css +89 -0
- package/themes/plannotator.css +2 -2
- package/types.ts +93 -10
- package/utils/agentSwitch.ts +33 -7
- package/utils/blockTargeting.ts +462 -178
- package/utils/clipboard.ts +110 -0
- package/utils/codeBlockMark.ts +50 -0
- package/utils/codeHighlight.ts +293 -0
- package/utils/codexModels.ts +79 -0
- package/utils/domSelection.ts +84 -0
- package/utils/htmlChrome.ts +73 -0
- package/utils/inputMethod.ts +79 -6
- package/utils/parser.ts +517 -21
- package/utils/preferenceTtl.ts +15 -0
- package/utils/sharing.ts +0 -1
- package/utils/skillCatalog.ts +269 -0
- package/utils/skillReferences.ts +475 -0
- package/utils/syntaxTheme.ts +83 -0
- package/utils/themeRegistry.ts +154 -0
- package/utils/vimHud.ts +263 -0
- package/utils/vimModeAnnouncement.ts +23 -0
- package/utils/vimNavigation.ts +417 -0
- package/utils/vimReticle.ts +88 -0
- package/utils/vimScroll.ts +162 -0
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { configStore } from '../config/configStore';
|
|
3
|
+
import { readThemePairCookies, writeThemePairCookies } from '../config/settings';
|
|
4
|
+
import { useConfigValue } from '../config/useConfig';
|
|
2
5
|
import { storage } from '../utils/storage';
|
|
3
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
BUILT_IN_THEMES,
|
|
8
|
+
getUnsupportedMode,
|
|
9
|
+
resolvePairTheme,
|
|
10
|
+
resolveThemeMode,
|
|
11
|
+
seedThemePair,
|
|
12
|
+
setDefaultThemePair,
|
|
13
|
+
themeSupportsHalf,
|
|
14
|
+
type ThemeHalf,
|
|
15
|
+
type ThemeInfo,
|
|
16
|
+
type ThemePair,
|
|
17
|
+
} from '../utils/themeRegistry';
|
|
18
|
+
import type { Mode } from './themeModes';
|
|
4
19
|
|
|
5
|
-
|
|
20
|
+
// Kept here because published consumers already import Mode from ThemeProvider.
|
|
21
|
+
export type { Mode } from './themeModes';
|
|
6
22
|
|
|
7
23
|
type ThemeProviderState = {
|
|
8
24
|
// Mode (dark/light/system) — backward-compatible with old "theme" API
|
|
@@ -10,10 +26,15 @@ type ThemeProviderState = {
|
|
|
10
26
|
setTheme: (mode: Mode) => void;
|
|
11
27
|
mode: Mode;
|
|
12
28
|
setMode: (mode: Mode) => void;
|
|
29
|
+
preferredMode: 'dark' | 'light';
|
|
13
30
|
resolvedMode: 'dark' | 'light';
|
|
14
|
-
// Color theme (palette)
|
|
31
|
+
// Color theme (the palette the current mode renders — pair[preferredMode])
|
|
15
32
|
colorTheme: string;
|
|
16
33
|
setColorTheme: (theme: string) => void;
|
|
34
|
+
// The pair itself: one palette per half, assignable independently
|
|
35
|
+
lightTheme: string;
|
|
36
|
+
darkTheme: string;
|
|
37
|
+
setHalfTheme: (half: ThemeHalf, theme: string) => void;
|
|
17
38
|
availableThemes: ThemeInfo[];
|
|
18
39
|
};
|
|
19
40
|
|
|
@@ -22,29 +43,21 @@ const ThemeProviderContext = createContext<ThemeProviderState>({
|
|
|
22
43
|
setTheme: () => null,
|
|
23
44
|
mode: 'dark',
|
|
24
45
|
setMode: () => null,
|
|
46
|
+
preferredMode: 'dark',
|
|
25
47
|
resolvedMode: 'dark',
|
|
26
48
|
colorTheme: 'plannotator',
|
|
27
49
|
setColorTheme: () => null,
|
|
50
|
+
lightTheme: 'plannotator',
|
|
51
|
+
darkTheme: 'plannotator',
|
|
52
|
+
setHalfTheme: () => null,
|
|
28
53
|
availableThemes: BUILT_IN_THEMES,
|
|
29
54
|
});
|
|
30
55
|
|
|
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
56
|
/** Sync theme classes on <html> without stripping non-theme classes (e.g. transitions-ready). */
|
|
44
|
-
function applyThemeClasses(themeId: string,
|
|
57
|
+
function applyThemeClasses(themeId: string, resolvedMode: 'dark' | 'light'): void {
|
|
45
58
|
const el = document.documentElement;
|
|
46
59
|
const themeClass = `theme-${themeId}`;
|
|
47
|
-
const wantLight =
|
|
60
|
+
const wantLight = resolvedMode === 'light';
|
|
48
61
|
|
|
49
62
|
if (el.classList.contains(themeClass) && el.classList.contains('light') === wantLight) return;
|
|
50
63
|
|
|
@@ -66,7 +79,20 @@ interface ThemeProviderProps {
|
|
|
66
79
|
children: React.ReactNode;
|
|
67
80
|
defaultTheme?: Mode;
|
|
68
81
|
defaultColorTheme?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Where the mode is stored. Read when resolving the initial pair and written
|
|
84
|
+
* by the legacy mirror, so a host's already-stored preference survives the
|
|
85
|
+
* upgrade to pairs.
|
|
86
|
+
*/
|
|
69
87
|
storageKey?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Where the single pre-pair palette is stored. Read as the migration source
|
|
90
|
+
* for both halves and kept in sync with the palette on screen.
|
|
91
|
+
*
|
|
92
|
+
* Note: the two halves themselves are a new concept with no pre-existing
|
|
93
|
+
* host data, so they always live under `plannotator-light-theme` /
|
|
94
|
+
* `plannotator-dark-theme`; these props rename only the two legacy values.
|
|
95
|
+
*/
|
|
70
96
|
colorThemeStorageKey?: string;
|
|
71
97
|
}
|
|
72
98
|
|
|
@@ -77,18 +103,53 @@ export function ThemeProvider({
|
|
|
77
103
|
storageKey = 'plannotator-theme',
|
|
78
104
|
colorThemeStorageKey = 'plannotator-color-theme',
|
|
79
105
|
}: ThemeProviderProps) {
|
|
80
|
-
const
|
|
81
|
-
() => (
|
|
106
|
+
const legacyKeys = useMemo(
|
|
107
|
+
() => ({ mode: storageKey, colorTheme: colorThemeStorageKey }),
|
|
108
|
+
[storageKey, colorThemeStorageKey],
|
|
82
109
|
);
|
|
83
110
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
// Resolve the pair this provider starts on from ITS OWN storage keys: what
|
|
112
|
+
// the user persisted (migrating a host's pre-pair values), else these props.
|
|
113
|
+
// The config store is a singleton that may already have resolved a default of
|
|
114
|
+
// its own, so storage is asked directly rather than trusting that value.
|
|
115
|
+
const [initialPair] = useState<ThemePair>(() => {
|
|
116
|
+
const resolved = readThemePairCookies(legacyKeys)
|
|
117
|
+
?? seedThemePair(defaultColorTheme, defaultTheme);
|
|
118
|
+
setDefaultThemePair(resolved);
|
|
119
|
+
return resolved;
|
|
120
|
+
});
|
|
121
|
+
const pendingSeed = useRef<ThemePair | null>(initialPair);
|
|
122
|
+
const [, setSeedApplied] = useState(false);
|
|
123
|
+
|
|
124
|
+
const storePair = useConfigValue('themePair');
|
|
125
|
+
const pair = pendingSeed.current ?? storePair;
|
|
126
|
+
const mode = pair.mode;
|
|
127
|
+
|
|
128
|
+
// Hand the resolved pair to the store as a SEED, not a user choice: seeding
|
|
129
|
+
// writes memory + cookies only. Routing it through set() would queue a
|
|
130
|
+
// server write of a value nobody picked, which (flushing after the server
|
|
131
|
+
// config arrives) would overwrite the user's real ~/.plannotator/config.json
|
|
132
|
+
// theme from any cookie-less visit.
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const seed = pendingSeed.current;
|
|
135
|
+
if (!seed) return;
|
|
136
|
+
pendingSeed.current = null;
|
|
137
|
+
configStore.seed('themePair', seed);
|
|
138
|
+
setSeedApplied(true);
|
|
139
|
+
}, []);
|
|
87
140
|
|
|
88
141
|
const [systemIsLight, setSystemIsLight] = useState(getSystemIsLight);
|
|
89
142
|
|
|
90
|
-
//
|
|
91
|
-
const
|
|
143
|
+
// Keep the OS-resolved preference separate from the half it selects.
|
|
144
|
+
const preferredMode: 'dark' | 'light' =
|
|
145
|
+
mode === 'system' ? (systemIsLight ? 'light' : 'dark') : mode;
|
|
146
|
+
const colorTheme = resolvePairTheme(pair, preferredMode);
|
|
147
|
+
const resolvedMode = resolveThemeMode(colorTheme, preferredMode);
|
|
148
|
+
|
|
149
|
+
// Read by the legacy setColorTheme, which must target the half on screen
|
|
150
|
+
// without re-creating its callback on every mode change.
|
|
151
|
+
const preferredModeRef = useRef(preferredMode);
|
|
152
|
+
preferredModeRef.current = preferredMode;
|
|
92
153
|
|
|
93
154
|
// [P3 fix] Apply theme class synchronously during initialization to prevent
|
|
94
155
|
// flash of unstyled content. CSS tokens live under .theme-* selectors, so
|
|
@@ -126,25 +187,63 @@ export function ThemeProvider({
|
|
|
126
187
|
}, [mode]);
|
|
127
188
|
|
|
128
189
|
const setMode = useCallback((newMode: Mode) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
190
|
+
configStore.set('themePair', { ...configStore.get('themePair'), mode: newMode });
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
193
|
+
/** Assign one palette to one half of the pair. */
|
|
194
|
+
const setHalfTheme = useCallback((half: ThemeHalf, newTheme: string) => {
|
|
195
|
+
if (!themeSupportsHalf(newTheme, half)) return;
|
|
196
|
+
configStore.set('themePair', { ...configStore.get('themePair'), [half]: newTheme });
|
|
197
|
+
}, []);
|
|
132
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Legacy single-palette API, kept for published consumers. It assigns exactly
|
|
201
|
+
* ONE half and changes nothing else:
|
|
202
|
+
*
|
|
203
|
+
* - a palette that renders both modes goes to the half currently on screen,
|
|
204
|
+
* leaving the other half's assignment alone;
|
|
205
|
+
* - a mode-restricted palette goes to the half it supports without touching
|
|
206
|
+
* the mode, because render-time resolution (`resolveThemeMode`) already
|
|
207
|
+
* keeps a System user on a palette that can be drawn.
|
|
208
|
+
*
|
|
209
|
+
* Persistence stays cookie-only unless a host installed a serverSync
|
|
210
|
+
* transport, matching the side effects this API had before the pair existed.
|
|
211
|
+
*/
|
|
133
212
|
const setColorTheme = useCallback((newTheme: string) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
213
|
+
const current = configStore.get('themePair');
|
|
214
|
+
const unsupported = getUnsupportedMode(newTheme);
|
|
215
|
+
const half: ThemeHalf = unsupported
|
|
216
|
+
? (unsupported === 'light' ? 'dark' : 'light')
|
|
217
|
+
: preferredModeRef.current;
|
|
218
|
+
configStore.setLocal('themePair', { ...current, [half]: newTheme });
|
|
219
|
+
}, []);
|
|
220
|
+
|
|
221
|
+
// Mirror the resolved choice onto the keys older releases read, so a
|
|
222
|
+
// downgrade lands on the user's palette instead of an unstyled first frame.
|
|
223
|
+
// The pair itself is written first: a pair migrated from the legacy
|
|
224
|
+
// single-palette key is derived, and the mirror below overwrites the key it
|
|
225
|
+
// was derived from.
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
writeThemePairCookies(pair, legacyKeys);
|
|
228
|
+
if (storage.getItem(colorThemeStorageKey) !== colorTheme) {
|
|
229
|
+
storage.setItem(colorThemeStorageKey, colorTheme);
|
|
230
|
+
}
|
|
231
|
+
}, [pair, colorTheme, colorThemeStorageKey, legacyKeys]);
|
|
137
232
|
|
|
138
233
|
const value = useMemo<ThemeProviderState>(() => ({
|
|
139
234
|
theme: mode,
|
|
140
235
|
setTheme: setMode,
|
|
141
236
|
mode,
|
|
142
237
|
setMode,
|
|
238
|
+
preferredMode,
|
|
143
239
|
resolvedMode,
|
|
144
240
|
colorTheme,
|
|
145
241
|
setColorTheme,
|
|
242
|
+
lightTheme: pair.light,
|
|
243
|
+
darkTheme: pair.dark,
|
|
244
|
+
setHalfTheme,
|
|
146
245
|
availableThemes: BUILT_IN_THEMES,
|
|
147
|
-
}), [mode, resolvedMode, colorTheme, setMode, setColorTheme]);
|
|
246
|
+
}), [mode, preferredMode, resolvedMode, colorTheme, pair.light, pair.dark, setMode, setColorTheme, setHalfTheme]);
|
|
148
247
|
|
|
149
248
|
return (
|
|
150
249
|
<ThemeProviderContext.Provider value={value}>
|
package/components/ThemeTab.tsx
CHANGED
|
@@ -1,14 +1,63 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useTheme
|
|
3
|
-
import {
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { useTheme } from './ThemeProvider';
|
|
3
|
+
import { THEME_MODES } from './themeModes';
|
|
4
|
+
import { themesForHalf, type ThemeHalf } from '../utils/themeRegistry';
|
|
4
5
|
|
|
5
6
|
interface ThemeTabProps {
|
|
6
7
|
onPreview?: () => void;
|
|
7
8
|
compact?: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
const HALVES: { id: ThemeHalf; label: string }[] = [
|
|
12
|
+
{ id: 'light', label: 'Light' },
|
|
13
|
+
{ id: 'dark', label: 'Dark' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const SyntaxLinesIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
17
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
18
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h7" />
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
|
|
10
22
|
export const ThemeTab: React.FC<ThemeTabProps> = ({ onPreview, compact }) => {
|
|
11
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
mode,
|
|
25
|
+
setMode,
|
|
26
|
+
lightTheme,
|
|
27
|
+
darkTheme,
|
|
28
|
+
setHalfTheme,
|
|
29
|
+
availableThemes,
|
|
30
|
+
preferredMode,
|
|
31
|
+
} = useTheme();
|
|
32
|
+
|
|
33
|
+
// Which half the grid assigns to. Follows the mode you are actually seeing,
|
|
34
|
+
// so opening Settings in dark mode edits the dark half first.
|
|
35
|
+
const [half, setHalf] = useState<ThemeHalf>(preferredMode);
|
|
36
|
+
useEffect(() => setHalf(preferredMode), [preferredMode]);
|
|
37
|
+
|
|
38
|
+
const pair: Record<ThemeHalf, string> = { light: lightTheme, dark: darkTheme };
|
|
39
|
+
const themes = themesForHalf(availableThemes, half);
|
|
40
|
+
const nameOf = (id: string) => availableThemes.find(theme => theme.id === id)?.name ?? id;
|
|
41
|
+
|
|
42
|
+
const summary = (
|
|
43
|
+
<div className={`flex items-center gap-1.5 text-[11px] text-muted-foreground ${compact ? '' : 'flex-wrap'}`}>
|
|
44
|
+
{HALVES.map(({ id, label }, index) => (
|
|
45
|
+
<React.Fragment key={id}>
|
|
46
|
+
{index > 0 && <span className="text-muted-foreground/40">·</span>}
|
|
47
|
+
<button
|
|
48
|
+
onClick={() => setHalf(id)}
|
|
49
|
+
title={`Assign the ${label.toLowerCase()} theme`}
|
|
50
|
+
className={`rounded px-1 py-0.5 transition-colors hover:bg-muted ${
|
|
51
|
+
half === id ? 'text-foreground' : ''
|
|
52
|
+
}`}
|
|
53
|
+
>
|
|
54
|
+
<span className="text-muted-foreground/70">{label}: </span>
|
|
55
|
+
<span className="font-medium">{nameOf(pair[id])}</span>
|
|
56
|
+
</button>
|
|
57
|
+
</React.Fragment>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
12
61
|
|
|
13
62
|
return (
|
|
14
63
|
<div className={compact ? '' : 'space-y-5'}>
|
|
@@ -16,98 +65,95 @@ export const ThemeTab: React.FC<ThemeTabProps> = ({ onPreview, compact }) => {
|
|
|
16
65
|
<div className={compact ? 'flex items-center gap-3 mb-2' : 'space-y-2'}>
|
|
17
66
|
{!compact && <label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Mode</label>}
|
|
18
67
|
<div className="flex gap-1">
|
|
19
|
-
{(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
})}
|
|
68
|
+
{THEME_MODES.map(({ id, label, Icon }) => (
|
|
69
|
+
<button
|
|
70
|
+
key={id}
|
|
71
|
+
onClick={() => setMode(id)}
|
|
72
|
+
className={`px-2.5 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
|
73
|
+
mode === id
|
|
74
|
+
? 'bg-primary text-primary-foreground'
|
|
75
|
+
: 'bg-muted text-muted-foreground hover:text-foreground'
|
|
76
|
+
}`}
|
|
77
|
+
>
|
|
78
|
+
<span className="flex items-center gap-1.5">
|
|
79
|
+
<Icon className="w-3 h-3" />
|
|
80
|
+
{label}
|
|
81
|
+
</span>
|
|
82
|
+
</button>
|
|
83
|
+
))}
|
|
52
84
|
</div>
|
|
53
|
-
{compact && (
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
</svg>
|
|
58
|
-
syntax match
|
|
59
|
-
</span>
|
|
85
|
+
{!compact && (
|
|
86
|
+
<p className="text-[11px] text-muted-foreground/70">
|
|
87
|
+
System follows your OS and switches between the two themes below.
|
|
88
|
+
</p>
|
|
60
89
|
)}
|
|
90
|
+
{compact && <div className="ml-auto">{summary}</div>}
|
|
61
91
|
</div>
|
|
62
92
|
|
|
63
|
-
{/* Theme */}
|
|
64
|
-
<div className={compact ? '' : 'space-y-3'}>
|
|
93
|
+
{/* Theme pair */}
|
|
94
|
+
<div className={compact ? 'space-y-2' : 'space-y-3'}>
|
|
65
95
|
{!compact && (
|
|
66
|
-
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
<
|
|
70
|
-
<
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
<>
|
|
97
|
+
<div className="flex items-center justify-between border-t border-border pt-5">
|
|
98
|
+
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Theme</label>
|
|
99
|
+
<div className="flex items-center gap-3">
|
|
100
|
+
<span className="text-[10px] text-muted-foreground/70 flex items-center gap-1">
|
|
101
|
+
<SyntaxLinesIcon className="w-2.5 h-2.5" />
|
|
102
|
+
= matched syntax colors
|
|
103
|
+
</span>
|
|
104
|
+
{onPreview && (
|
|
105
|
+
<button
|
|
106
|
+
onClick={onPreview}
|
|
107
|
+
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"
|
|
108
|
+
>
|
|
109
|
+
Preview Mode
|
|
110
|
+
</button>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
83
113
|
</div>
|
|
84
|
-
|
|
114
|
+
{summary}
|
|
115
|
+
</>
|
|
85
116
|
)}
|
|
117
|
+
|
|
118
|
+
{/* Which half the grid assigns to */}
|
|
119
|
+
<div className="flex items-center gap-2">
|
|
120
|
+
<span className="text-[11px] text-muted-foreground/70">Assigning</span>
|
|
121
|
+
<div className="flex items-center gap-0.5 rounded-lg bg-muted/50 p-0.5">
|
|
122
|
+
{HALVES.map(({ id, label }) => (
|
|
123
|
+
<button
|
|
124
|
+
key={id}
|
|
125
|
+
onClick={() => setHalf(id)}
|
|
126
|
+
aria-pressed={half === id}
|
|
127
|
+
className={`rounded-md px-2.5 py-1 text-[11px] font-medium transition-colors ${
|
|
128
|
+
half === id
|
|
129
|
+
? 'bg-background text-foreground shadow-sm'
|
|
130
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
131
|
+
}`}
|
|
132
|
+
>
|
|
133
|
+
{label} theme
|
|
134
|
+
</button>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
86
139
|
<div className={`grid gap-2 overflow-y-auto pr-1 ${compact ? 'grid-cols-4' : 'grid-cols-3'}`}>
|
|
87
|
-
{
|
|
88
|
-
const isSelected =
|
|
89
|
-
const colors = theme.colors[
|
|
90
|
-
const modeUnavailable =
|
|
91
|
-
(resolvedMode === 'light' && theme.modeSupport === 'dark-only') ||
|
|
92
|
-
(resolvedMode === 'dark' && theme.modeSupport === 'light-only');
|
|
140
|
+
{themes.map(theme => {
|
|
141
|
+
const isSelected = pair[half] === theme.id;
|
|
142
|
+
const colors = theme.colors[half];
|
|
93
143
|
return (
|
|
94
144
|
<button
|
|
95
145
|
key={theme.id}
|
|
96
|
-
onClick={() =>
|
|
146
|
+
onClick={() => setHalfTheme(half, theme.id)}
|
|
97
147
|
className={`relative p-2 rounded-md border text-left transition-colors ${
|
|
98
148
|
isSelected
|
|
99
149
|
? 'border-primary bg-primary/5'
|
|
100
|
-
:
|
|
101
|
-
? 'border-border/50 opacity-45'
|
|
102
|
-
: 'border-border hover:border-muted-foreground/30 hover:bg-muted/30'
|
|
150
|
+
: 'border-border hover:border-muted-foreground/30 hover:bg-muted/30'
|
|
103
151
|
}`}
|
|
104
152
|
>
|
|
105
153
|
{/* Syntax highlighting badge */}
|
|
106
154
|
{theme.syntaxHighlighting && (
|
|
107
155
|
<div className="absolute top-1 right-1" title="Matched syntax highlighting in diffs">
|
|
108
|
-
<
|
|
109
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h7" />
|
|
110
|
-
</svg>
|
|
156
|
+
<SyntaxLinesIcon className="w-2.5 h-2.5 text-muted-foreground/50" />
|
|
111
157
|
</div>
|
|
112
158
|
)}
|
|
113
159
|
{/* Color swatches */}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Button } from './ui/button';
|
|
3
3
|
import { cn } from '../lib/utils';
|
|
4
|
-
import { Send, Check } from 'lucide-react';
|
|
4
|
+
import { Send, Check, X } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
type ToolbarLabelBreakpoint = 'md' | 'lg';
|
|
5
7
|
|
|
6
8
|
interface FeedbackButtonProps {
|
|
7
9
|
onClick: () => void;
|
|
@@ -13,6 +15,7 @@ interface FeedbackButtonProps {
|
|
|
13
15
|
shortLoadingLabel?: string;
|
|
14
16
|
title?: string;
|
|
15
17
|
muted?: boolean;
|
|
18
|
+
labelBreakpoint?: ToolbarLabelBreakpoint;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export const FeedbackButton: React.FC<FeedbackButtonProps> = ({
|
|
@@ -25,6 +28,7 @@ export const FeedbackButton: React.FC<FeedbackButtonProps> = ({
|
|
|
25
28
|
shortLoadingLabel,
|
|
26
29
|
title = 'Send Feedback',
|
|
27
30
|
muted = false,
|
|
31
|
+
labelBreakpoint = 'md',
|
|
28
32
|
}) => (
|
|
29
33
|
<Button
|
|
30
34
|
variant="outline"
|
|
@@ -37,11 +41,17 @@ export const FeedbackButton: React.FC<FeedbackButtonProps> = ({
|
|
|
37
41
|
>
|
|
38
42
|
{shortLabel ? (
|
|
39
43
|
<>
|
|
40
|
-
<span className=
|
|
41
|
-
|
|
44
|
+
<span className={labelBreakpoint === 'lg' ? 'hidden lg:inline xl:hidden' : 'hidden md:inline lg:hidden'}>
|
|
45
|
+
{isLoading ? (shortLoadingLabel ?? loadingLabel) : shortLabel}
|
|
46
|
+
</span>
|
|
47
|
+
<span className={labelBreakpoint === 'lg' ? 'hidden xl:inline' : 'hidden lg:inline'}>
|
|
48
|
+
{isLoading ? loadingLabel : label}
|
|
49
|
+
</span>
|
|
42
50
|
</>
|
|
43
51
|
) : (
|
|
44
|
-
<span className=
|
|
52
|
+
<span className={labelBreakpoint === 'lg' ? 'hidden lg:inline' : 'hidden md:inline'}>
|
|
53
|
+
{isLoading ? loadingLabel : label}
|
|
54
|
+
</span>
|
|
45
55
|
)}
|
|
46
56
|
</Button>
|
|
47
57
|
);
|
|
@@ -57,6 +67,7 @@ export interface ApproveButtonProps {
|
|
|
57
67
|
title?: string;
|
|
58
68
|
dimmed?: boolean;
|
|
59
69
|
muted?: boolean;
|
|
70
|
+
labelBreakpoint?: ToolbarLabelBreakpoint;
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
export const ApproveButton: React.FC<ApproveButtonProps> = ({
|
|
@@ -70,6 +81,7 @@ export const ApproveButton: React.FC<ApproveButtonProps> = ({
|
|
|
70
81
|
title,
|
|
71
82
|
dimmed = false,
|
|
72
83
|
muted = false,
|
|
84
|
+
labelBreakpoint = 'md',
|
|
73
85
|
}) => (
|
|
74
86
|
<Button
|
|
75
87
|
variant="success"
|
|
@@ -84,8 +96,12 @@ export const ApproveButton: React.FC<ApproveButtonProps> = ({
|
|
|
84
96
|
dimmed && !muted && !disabled && 'bg-success/50 text-success-foreground/70 hover:bg-success hover:text-success-foreground',
|
|
85
97
|
)}
|
|
86
98
|
>
|
|
87
|
-
<span className=
|
|
88
|
-
|
|
99
|
+
<span className={labelBreakpoint === 'lg' ? 'lg:hidden' : 'md:hidden'}>
|
|
100
|
+
{isLoading ? mobileLoadingLabel : mobileLabel}
|
|
101
|
+
</span>
|
|
102
|
+
<span className={labelBreakpoint === 'lg' ? 'hidden lg:inline' : 'hidden md:inline'}>
|
|
103
|
+
{isLoading ? loadingLabel : label}
|
|
104
|
+
</span>
|
|
89
105
|
</Button>
|
|
90
106
|
);
|
|
91
107
|
|
|
@@ -94,6 +110,7 @@ interface ExitButtonProps {
|
|
|
94
110
|
disabled?: boolean;
|
|
95
111
|
isLoading?: boolean;
|
|
96
112
|
title?: string;
|
|
113
|
+
labelBreakpoint?: ToolbarLabelBreakpoint;
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
export const ExitButton: React.FC<ExitButtonProps> = ({
|
|
@@ -101,6 +118,7 @@ export const ExitButton: React.FC<ExitButtonProps> = ({
|
|
|
101
118
|
disabled = false,
|
|
102
119
|
isLoading = false,
|
|
103
120
|
title = 'Close session without sending feedback',
|
|
121
|
+
labelBreakpoint = 'md',
|
|
104
122
|
}) => (
|
|
105
123
|
<Button
|
|
106
124
|
variant="secondary"
|
|
@@ -108,9 +126,12 @@ export const ExitButton: React.FC<ExitButtonProps> = ({
|
|
|
108
126
|
onClick={onClick}
|
|
109
127
|
disabled={disabled || isLoading}
|
|
110
128
|
title={title}
|
|
129
|
+
aria-label={title}
|
|
111
130
|
className="bg-muted text-muted-foreground hover:bg-muted/80"
|
|
112
131
|
>
|
|
113
|
-
<span className=
|
|
114
|
-
|
|
132
|
+
<span className={labelBreakpoint === 'lg' ? 'lg:hidden' : 'md:hidden'}>
|
|
133
|
+
{isLoading ? '…' : <X className="size-3.5" aria-hidden="true" />}
|
|
134
|
+
</span>
|
|
135
|
+
<span className={labelBreakpoint === 'lg' ? 'hidden lg:inline' : 'hidden md:inline'}>{isLoading ? 'Closing...' : 'Close'}</span>
|
|
115
136
|
</Button>
|
|
116
137
|
);
|