@plannotator/ui 0.36.0 → 0.38.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/HANDOFF.md +6 -4
- package/README.md +57 -1
- package/components/ActionMenu.tsx +22 -24
- package/components/AnnotationToolbar.tsx +3 -1
- package/components/ApproveDropdown.tsx +15 -18
- package/components/CommentPopover.tsx +9 -4
- package/components/DecisionControl.tsx +626 -0
- package/components/DocBadges.tsx +9 -6
- package/components/FloatingQuickLabelPicker.tsx +4 -1
- package/components/KeyboardShortcuts.tsx +1 -0
- package/components/PlanHeaderMenu.tsx +1 -1
- package/components/Settings.tsx +67 -6
- package/components/StickyHeaderLane.tsx +16 -40
- package/components/ToolbarButtons.tsx +31 -9
- package/components/Viewer.tsx +277 -95
- package/components/VimTargetReticle.tsx +1 -1
- package/components/blocks/AlertBlock.tsx +51 -4
- package/components/compactHeaderLayout.ts +52 -0
- package/config/reviewView.ts +1 -0
- package/config/settings.ts +48 -3
- package/configure.ts +9 -0
- package/hooks/useAnnotationHighlighter.ts +31 -19
- package/hooks/useDismissablePopover.ts +65 -0
- package/hooks/useVimDocumentFocus.ts +6 -0
- package/package.json +22 -22
- package/shortcuts/decisionControl.shortcuts.ts +31 -0
- package/shortcuts/index.ts +1 -0
- package/shortcuts/plan-review/documentView.shortcuts.ts +13 -0
- package/styles.css +1 -1
- package/theme.css +11 -0
- package/utils/alertTitle.ts +69 -0
- package/utils/decisionSpec.ts +430 -0
- package/utils/platform.ts +16 -0
- package/utils/vimScroll.ts +1 -1
- package/sprite_package_additional/index.html +0 -34
- package/sprite_package_new/index.html +0 -34
- package/sprite_package_pulluphang/index.html +0 -34
|
@@ -88,6 +88,7 @@ const documentViewShortcuts: ShortcutSection = {
|
|
|
88
88
|
title: 'View',
|
|
89
89
|
shortcuts: [
|
|
90
90
|
{ keys: [modKey, '.'], desc: 'Toggle focus mode', hint: 'Collapses the Contents sidebar and the right-hand panel together; press again to restore whatever was open before. Markdown documents only; HTML pages keep their own layout.' },
|
|
91
|
+
{ keys: [modKey, 'E'], desc: 'Toggle edit mode', hint: 'Opens the markdown source editor in place, keeping your scroll position; press again to commit your edits and return to annotating. Markdown documents only; a source-backed document with unsaved changes keeps its two-step discard.' },
|
|
91
92
|
],
|
|
92
93
|
};
|
|
93
94
|
|
|
@@ -40,7 +40,7 @@ interface PlanHeaderMenuProps {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export interface CompactPlanAction {
|
|
43
|
-
id: 'exit' | 'feedback' | 'approve' | 'copy' | 'done' | 'edit' | 'tools' | 'annotate' | 'refresh' | 'annotations' | 'ai' | 'review';
|
|
43
|
+
id: 'exit' | 'feedback' | 'approve' | 'copy' | 'done' | 'edit' | 'tools' | 'annotate' | 'refresh' | 'annotations' | 'ai' | 'review' | 'note' | 'discard-finish';
|
|
44
44
|
label: string;
|
|
45
45
|
subtitle?: string;
|
|
46
46
|
onSelect: () => void;
|
package/components/Settings.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
|
|
|
3
3
|
import type { AnnotateAgentTerminalSide } from '@plannotator/core/agent-terminal';
|
|
4
4
|
import type { Origin } from '@plannotator/core/agents';
|
|
5
5
|
import type { DiffLineBgIntensity } from '@plannotator/core/config-types';
|
|
6
|
+
import type { TokenHoverDelay } from '@plannotator/core/token-hover';
|
|
6
7
|
import { configStore, useConfigValue, setReviewPanelView, setReviewDefaultDiffType, setReviewAutoViewed } from '../config';
|
|
7
8
|
import { setWebMcpToolsEnabled, useWebMcpToolsEnabled } from '../webmcp/preference';
|
|
8
9
|
import { loadDiffFont } from '../utils/diffFonts';
|
|
@@ -64,7 +65,7 @@ import { useAgents } from '../hooks/useAgents';
|
|
|
64
65
|
import { KeyboardShortcuts } from './KeyboardShortcuts';
|
|
65
66
|
import { type QuickLabel, getQuickLabels, saveQuickLabels, resetQuickLabels, DEFAULT_QUICK_LABELS, getLabelColors, LABEL_COLOR_MAP } from '../utils/quickLabels';
|
|
66
67
|
import { ThemeTab } from './ThemeTab';
|
|
67
|
-
import { isMac, modKey, altKey } from '../utils/platform';
|
|
68
|
+
import { isMac, modKey, modKeyWord, altKey } from '../utils/platform';
|
|
68
69
|
import { getAIProviderSettings, resolveAIProviderSelection } from '../utils/aiProvider';
|
|
69
70
|
import { AISettingsTab } from './AISettingsTab';
|
|
70
71
|
import { HooksTab } from './settings/HooksTab';
|
|
@@ -133,6 +134,23 @@ export const DIFF_STYLE_OPTIONS = [
|
|
|
133
134
|
{ value: 'split' as const, label: 'Split' },
|
|
134
135
|
{ value: 'unified' as const, label: 'Unified' },
|
|
135
136
|
];
|
|
137
|
+
/**
|
|
138
|
+
* The modifier gate is the platform's primary modifier: Cmd on macOS, Ctrl on
|
|
139
|
+
* Windows and Linux. Labeled through modKeyWord so the control names the key
|
|
140
|
+
* the reader actually has.
|
|
141
|
+
*/
|
|
142
|
+
export const TOKEN_HOVER_TRIGGER_OPTIONS = [
|
|
143
|
+
{ value: 'hover' as const, label: 'On hover' },
|
|
144
|
+
{ value: 'modifier' as const, label: `Hold ${modKeyWord}` },
|
|
145
|
+
{ value: 'off' as const, label: 'Off' },
|
|
146
|
+
];
|
|
147
|
+
/** SegmentedControl keys on strings, so the ms values ride as their digits. */
|
|
148
|
+
export type TokenHoverDelayOption = '150' | '300' | '700';
|
|
149
|
+
export const TOKEN_HOVER_DELAY_OPTIONS = [
|
|
150
|
+
{ value: '150' as const, label: 'Fast' },
|
|
151
|
+
{ value: '300' as const, label: 'Default' },
|
|
152
|
+
{ value: '700' as const, label: 'Relaxed' },
|
|
153
|
+
];
|
|
136
154
|
export const OVERFLOW_OPTIONS = [
|
|
137
155
|
{ value: 'scroll' as const, label: 'Scroll' },
|
|
138
156
|
{ value: 'wrap' as const, label: 'Wrap' },
|
|
@@ -157,6 +175,7 @@ const DEFAULT_DIFF_TYPE_OPTIONS = [
|
|
|
157
175
|
// "All Changes" belongs to since-base (the flagship composite); uncommitted
|
|
158
176
|
// reverts to its plain name so the two stay distinguishable side by side.
|
|
159
177
|
{ value: 'since-base' as const, label: 'All Changes (Recommended)', description: "Everything since your branch split from main — committed, uncommitted, and untracked" },
|
|
178
|
+
{ value: 'local-vs-remote' as const, label: 'Local vs Remote Branch', description: "Your local branch and working tree compared with its last-fetched remote-tracking branch" },
|
|
160
179
|
{ value: 'uncommitted' as const, label: 'Uncommitted', description: "Everything you've changed since your last commit" },
|
|
161
180
|
{ value: 'unstaged' as const, label: 'Unstaged', description: "Only changes you haven't staged yet" },
|
|
162
181
|
{ value: 'staged' as const, label: 'Staged', description: "Only changes you've staged for commit" },
|
|
@@ -170,22 +189,27 @@ const AGENT_TERMINAL_SIDE_OPTIONS: { value: AnnotateAgentTerminalSide; label: st
|
|
|
170
189
|
{ value: 'hidden', label: 'Hidden' },
|
|
171
190
|
];
|
|
172
191
|
|
|
173
|
-
function SegmentedControl<T extends string>({ options, value, onChange }: {
|
|
192
|
+
function SegmentedControl<T extends string>({ options, value, onChange, disabled = false }: {
|
|
174
193
|
options: { value: T; label: string }[];
|
|
175
194
|
value: T;
|
|
176
195
|
onChange: (v: T) => void;
|
|
196
|
+
/** Visible but inert, for a control whose axis does not apply right now. */
|
|
197
|
+
disabled?: boolean;
|
|
177
198
|
}) {
|
|
178
199
|
return (
|
|
179
|
-
<div className=
|
|
200
|
+
<div className={['flex items-center gap-1 bg-muted/50 rounded-lg p-0.5', disabled && 'opacity-50'].filter(Boolean).join(' ')}>
|
|
180
201
|
{options.map((opt) => (
|
|
181
202
|
<button
|
|
182
203
|
key={opt.value}
|
|
204
|
+
{...(disabled ? { disabled: true } : {})}
|
|
183
205
|
onClick={() => onChange(opt.value)}
|
|
184
|
-
className={
|
|
206
|
+
className={[
|
|
207
|
+
'flex-1 px-3 py-1.5 text-xs rounded-md transition-colors',
|
|
208
|
+
disabled && 'cursor-not-allowed',
|
|
185
209
|
value === opt.value
|
|
186
210
|
? 'bg-background text-foreground shadow-sm font-medium'
|
|
187
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
188
|
-
}
|
|
211
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
212
|
+
].filter(Boolean).join(' ')}
|
|
189
213
|
>
|
|
190
214
|
{opt.label}
|
|
191
215
|
</button>
|
|
@@ -470,6 +494,8 @@ const ReviewDisplayTab: React.FC<{ isCompactTouchLayout?: boolean }> = ({ isComp
|
|
|
470
494
|
const diffExpandUnchanged = useConfigValue('diffExpandUnchanged');
|
|
471
495
|
const diffFontFamily = useConfigValue('diffFontFamily');
|
|
472
496
|
const diffFontSize = useConfigValue('diffFontSize');
|
|
497
|
+
const tokenHoverTrigger = useConfigValue('tokenHoverTrigger');
|
|
498
|
+
const tokenHoverDelay = useConfigValue('tokenHoverDelay');
|
|
473
499
|
|
|
474
500
|
// Load font for the preview swatch
|
|
475
501
|
useEffect(() => {
|
|
@@ -493,6 +519,41 @@ const ReviewDisplayTab: React.FC<{ isCompactTouchLayout?: boolean }> = ({ isComp
|
|
|
493
519
|
|
|
494
520
|
<div className="border-t border-border" />
|
|
495
521
|
|
|
522
|
+
{/* Hover cards (internally tokenHover*; the label is what changed, not
|
|
523
|
+
the ids). One trigger select rather than a toggle plus a mode: `Off`
|
|
524
|
+
is a value of the same question, so there is no unreachable
|
|
525
|
+
enabled-but-off state to reason about. The delay stays a separate
|
|
526
|
+
axis because "too eager" is a complaint neither the hold-modifier option
|
|
527
|
+
nor Off answers. */}
|
|
528
|
+
<div className="space-y-3">
|
|
529
|
+
<div>
|
|
530
|
+
<div className="text-sm font-medium">Hover cards</div>
|
|
531
|
+
<div className="text-xs text-muted-foreground">
|
|
532
|
+
Rest the pointer on a symbol in a diff to see where it is defined and who
|
|
533
|
+
references it. Needs ripgrep and a local checkout; nothing appears when the
|
|
534
|
+
search comes back empty. {modKeyWord}+click still opens the References panel either way.
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
<SegmentedControl
|
|
538
|
+
options={TOKEN_HOVER_TRIGGER_OPTIONS}
|
|
539
|
+
value={tokenHoverTrigger}
|
|
540
|
+
onChange={(v) => configStore.set('tokenHoverTrigger', v)}
|
|
541
|
+
/>
|
|
542
|
+
<div className="space-y-2">
|
|
543
|
+
<div className="text-xs text-muted-foreground">
|
|
544
|
+
How long the pointer rests before a card is requested
|
|
545
|
+
</div>
|
|
546
|
+
<SegmentedControl
|
|
547
|
+
options={TOKEN_HOVER_DELAY_OPTIONS}
|
|
548
|
+
value={String(tokenHoverDelay) as TokenHoverDelayOption}
|
|
549
|
+
onChange={(v) => configStore.set('tokenHoverDelay', Number(v) as TokenHoverDelay)}
|
|
550
|
+
disabled={tokenHoverTrigger === 'off'}
|
|
551
|
+
/>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
<div className="border-t border-border" />
|
|
556
|
+
|
|
496
557
|
{/* Font Family */}
|
|
497
558
|
<div className="space-y-2">
|
|
498
559
|
<div>
|
|
@@ -34,30 +34,14 @@ import {
|
|
|
34
34
|
} from '../hooks/useScrollViewport';
|
|
35
35
|
import type { EditorMode, InputMethod } from '../types';
|
|
36
36
|
import type { PlanDiffStats } from '../utils/planDiffEngine';
|
|
37
|
+
import {
|
|
38
|
+
resolveCompactHeaderGeometry,
|
|
39
|
+
snapCompactHeaderWidth,
|
|
40
|
+
} from './compactHeaderLayout';
|
|
37
41
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// re-render on every pixel. Hoisted to module scope so the effects
|
|
41
|
-
// (which use [] deps) can't accidentally close over a stale instance.
|
|
42
|
-
// Floor (not round) so wrapper undershoots and actions overshoots — both
|
|
43
|
-
// errors push toward a more cautious layout, avoiding a one-bucket overlap
|
|
44
|
-
// flash right at the 300/460 thresholds during a slow drag.
|
|
45
|
-
const snap = (n: number) => Math.floor(n / 16) * 16;
|
|
46
|
-
|
|
47
|
-
// Layout geometry — static tuning constants, hoisted alongside `snap`.
|
|
48
|
-
// LEFT_OFFSET: matches the bar's `md:left-5` (20px).
|
|
49
|
-
// GAP: minimum breathing room between the bar's right edge and the
|
|
50
|
-
// action button cluster's left edge when they share a lane.
|
|
51
|
-
// Two-stage shared-lane shrinkage, mirroring the right side:
|
|
52
|
-
// WIDE_BAR_WIDTH: full toolstrip (active labels Pinpoint/Markup ~300px)
|
|
53
|
-
// + badges (~140px) on a single line.
|
|
54
|
-
// MIN_BAR_WIDTH: icon-only toolstrip (~140px) + badges (~140px).
|
|
55
|
-
// Below MIN, even the icon-only bar can't fit beside the (likely also
|
|
56
|
-
// icon-only) action cluster — stack as the final fallback.
|
|
42
|
+
// Matches the bar's `md:left-5` inset. The remaining shared geometry lives
|
|
43
|
+
// in compactHeaderLayout so Viewer-owned and standalone lanes cannot drift.
|
|
57
44
|
const LEFT_OFFSET = 20;
|
|
58
|
-
const GAP = 16;
|
|
59
|
-
const WIDE_BAR_WIDTH = 460;
|
|
60
|
-
const MIN_BAR_WIDTH = 300;
|
|
61
45
|
|
|
62
46
|
/** Controls when the compact header lane is visible and interactive. */
|
|
63
47
|
export type StickyHeaderLaneVisibility = 'stuck' | 'always';
|
|
@@ -145,27 +129,19 @@ export const StickyHeaderLane: React.FC<StickyHeaderLaneProps> = ({
|
|
|
145
129
|
// removes chrome at rest for the supported chrome-free presentation.
|
|
146
130
|
const showChrome = visibility === 'always' ? laneIsStuck : true;
|
|
147
131
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// (not narrow), so we don't flash the wrong layout on first paint.
|
|
157
|
-
const measured = wrapperWidth > 0 && actionsWidth > 0;
|
|
158
|
-
const isNarrow = measured && availableForBar < MIN_BAR_WIDTH;
|
|
159
|
-
// Tight = shared lane still fits, but only if the toolstrip drops its
|
|
160
|
-
// active labels and goes icon-only. Lets us stay horizontally aligned
|
|
161
|
-
// for an extra ~160px of width before stacking.
|
|
162
|
-
const isToolstripIconOnly =
|
|
163
|
-
measured && !isNarrow && availableForBar < WIDE_BAR_WIDTH;
|
|
132
|
+
const headerGeometry = resolveCompactHeaderGeometry({
|
|
133
|
+
containerWidth: wrapperWidth,
|
|
134
|
+
trailingWidth: actionsWidth,
|
|
135
|
+
leadingInset: LEFT_OFFSET,
|
|
136
|
+
});
|
|
137
|
+
const availableForBar = headerGeometry.availableForLeading;
|
|
138
|
+
const isNarrow = headerGeometry.layout === 'narrow';
|
|
139
|
+
const isToolstripIconOnly = headerGeometry.layout === 'tight';
|
|
164
140
|
|
|
165
141
|
useEffect(() => {
|
|
166
142
|
if (!wrapperRef.current) return;
|
|
167
143
|
const ro = new ResizeObserver(([entry]) => {
|
|
168
|
-
const next =
|
|
144
|
+
const next = snapCompactHeaderWidth(entry.contentRect.width);
|
|
169
145
|
setWrapperWidth((prev) => (prev === next ? prev : next));
|
|
170
146
|
});
|
|
171
147
|
ro.observe(wrapperRef.current);
|
|
@@ -188,7 +164,7 @@ export const StickyHeaderLane: React.FC<StickyHeaderLaneProps> = ({
|
|
|
188
164
|
const el = document.querySelector<HTMLElement>('[data-sticky-actions]');
|
|
189
165
|
if (!el) return;
|
|
190
166
|
const ro = new ResizeObserver(([entry]) => {
|
|
191
|
-
const next =
|
|
167
|
+
const next = snapCompactHeaderWidth(entry.contentRect.width);
|
|
192
168
|
setActionsWidth((prev) => (prev === next ? prev : next));
|
|
193
169
|
});
|
|
194
170
|
ro.observe(el);
|
|
@@ -14,7 +14,6 @@ interface FeedbackButtonProps {
|
|
|
14
14
|
loadingLabel?: string;
|
|
15
15
|
shortLoadingLabel?: string;
|
|
16
16
|
title?: string;
|
|
17
|
-
muted?: boolean;
|
|
18
17
|
labelBreakpoint?: ToolbarLabelBreakpoint;
|
|
19
18
|
}
|
|
20
19
|
|
|
@@ -27,7 +26,6 @@ export const FeedbackButton: React.FC<FeedbackButtonProps> = ({
|
|
|
27
26
|
loadingLabel = 'Sending...',
|
|
28
27
|
shortLoadingLabel,
|
|
29
28
|
title = 'Send Feedback',
|
|
30
|
-
muted = false,
|
|
31
29
|
labelBreakpoint = 'md',
|
|
32
30
|
}) => (
|
|
33
31
|
<Button
|
|
@@ -37,7 +35,6 @@ export const FeedbackButton: React.FC<FeedbackButtonProps> = ({
|
|
|
37
35
|
disabled={disabled}
|
|
38
36
|
title={title}
|
|
39
37
|
iconLeft={<Send className="size-3.5" />}
|
|
40
|
-
className={cn(muted && 'opacity-50 cursor-not-allowed')}
|
|
41
38
|
>
|
|
42
39
|
{shortLabel ? (
|
|
43
40
|
<>
|
|
@@ -65,8 +62,10 @@ export interface ApproveButtonProps {
|
|
|
65
62
|
mobileLabel?: string;
|
|
66
63
|
mobileLoadingLabel?: string;
|
|
67
64
|
title?: string;
|
|
65
|
+
/** Kept for plan mode (packages/editor AppHeader) — the softened "you have
|
|
66
|
+
* annotations" treatment. The old platform-mode `muted` prop died with
|
|
67
|
+
* PR6 (DecisionControl owns the self-approval mute now). */
|
|
68
68
|
dimmed?: boolean;
|
|
69
|
-
muted?: boolean;
|
|
70
69
|
labelBreakpoint?: ToolbarLabelBreakpoint;
|
|
71
70
|
}
|
|
72
71
|
|
|
@@ -80,7 +79,6 @@ export const ApproveButton: React.FC<ApproveButtonProps> = ({
|
|
|
80
79
|
mobileLoadingLabel = '...',
|
|
81
80
|
title,
|
|
82
81
|
dimmed = false,
|
|
83
|
-
muted = false,
|
|
84
82
|
labelBreakpoint = 'md',
|
|
85
83
|
}) => (
|
|
86
84
|
<Button
|
|
@@ -91,9 +89,8 @@ export const ApproveButton: React.FC<ApproveButtonProps> = ({
|
|
|
91
89
|
title={title}
|
|
92
90
|
iconLeft={<Check className="size-3.5" />}
|
|
93
91
|
className={cn(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
dimmed && !muted && !disabled && 'bg-success/50 text-success-foreground/70 hover:bg-success hover:text-success-foreground',
|
|
92
|
+
disabled && 'bg-muted text-muted-foreground hover:bg-muted',
|
|
93
|
+
dimmed && !disabled && 'bg-success/50 text-success-foreground/70 hover:bg-success hover:text-success-foreground',
|
|
97
94
|
)}
|
|
98
95
|
>
|
|
99
96
|
<span className={labelBreakpoint === 'lg' ? 'lg:hidden' : 'md:hidden'}>
|
|
@@ -111,6 +108,10 @@ interface ExitButtonProps {
|
|
|
111
108
|
isLoading?: boolean;
|
|
112
109
|
title?: string;
|
|
113
110
|
labelBreakpoint?: ToolbarLabelBreakpoint;
|
|
111
|
+
/** `'ghost'` renders the icon-only ghost-X form that sits beside the
|
|
112
|
+
* decision control. Default `'pill'` keeps every existing caller
|
|
113
|
+
* byte-identical. */
|
|
114
|
+
appearance?: 'pill' | 'ghost';
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
export const ExitButton: React.FC<ExitButtonProps> = ({
|
|
@@ -119,7 +120,28 @@ export const ExitButton: React.FC<ExitButtonProps> = ({
|
|
|
119
120
|
isLoading = false,
|
|
120
121
|
title = 'Close session without sending feedback',
|
|
121
122
|
labelBreakpoint = 'md',
|
|
122
|
-
|
|
123
|
+
appearance = 'pill',
|
|
124
|
+
}) => appearance === 'ghost' ? (
|
|
125
|
+
<Button
|
|
126
|
+
variant="ghost"
|
|
127
|
+
size="xs"
|
|
128
|
+
onClick={onClick}
|
|
129
|
+
disabled={disabled || isLoading}
|
|
130
|
+
title={title}
|
|
131
|
+
aria-label={title}
|
|
132
|
+
// Maintainer ruling (post-demo): a bare X carries "close without
|
|
133
|
+
// sending" semantics an unlabeled icon should not carry alone — the
|
|
134
|
+
// word renders at every breakpoint.
|
|
135
|
+
className="px-2 text-muted-foreground gap-1.5"
|
|
136
|
+
>
|
|
137
|
+
{isLoading ? '…' : (
|
|
138
|
+
<>
|
|
139
|
+
<X className="size-3.5" aria-hidden="true" />
|
|
140
|
+
<span>Close</span>
|
|
141
|
+
</>
|
|
142
|
+
)}
|
|
143
|
+
</Button>
|
|
144
|
+
) : (
|
|
123
145
|
<Button
|
|
124
146
|
variant="secondary"
|
|
125
147
|
size="xs"
|