@plannotator/ui 0.30.0 → 0.31.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 (74) hide show
  1. package/README.md +1 -0
  2. package/components/ActionMenu.tsx +6 -1
  3. package/components/AgentsTab.tsx +8 -9
  4. package/components/AnalysisLayerToggle.tsx +48 -0
  5. package/components/AnnotationPanel.tsx +62 -32
  6. package/components/AnnotationToolbar.tsx +28 -8
  7. package/components/AnnotationToolstrip.tsx +9 -0
  8. package/components/CommentPopover.tsx +212 -46
  9. package/components/ConfirmDialog.tsx +42 -28
  10. package/components/InlineMarkdown.tsx +3 -2
  11. package/components/KeyboardShortcuts.tsx +9 -0
  12. package/components/Landing.tsx +1 -1
  13. package/components/LookAndFeelAnnouncementDialog.tsx +147 -178
  14. package/components/MarkdownEditor/embedPicker.ts +349 -0
  15. package/components/MarkdownEditor.tsx +12 -0
  16. package/components/ModeToggle.tsx +2 -1
  17. package/components/PermissionModeSetup.tsx +24 -5
  18. package/components/PinpointOverlay.tsx +11 -6
  19. package/components/PlanHeaderMenu.tsx +140 -1
  20. package/components/SearchableSelect.tsx +2 -0
  21. package/components/Settings.tsx +136 -10
  22. package/components/SkillReferenceMenu.tsx +9 -0
  23. package/components/StickyHeaderLane.tsx +9 -2
  24. package/components/TableOfContents.tsx +9 -4
  25. package/components/TextShimmer.tsx +8 -5
  26. package/components/ThemeProvider.tsx +43 -1
  27. package/components/ThemeTab.tsx +52 -1
  28. package/components/Tooltip.tsx +3 -1
  29. package/components/Viewer.tsx +19 -5
  30. package/components/VimTargetReticle.tsx +12 -4
  31. package/components/ai/DocumentAIChatPanel.tsx +1 -0
  32. package/components/core/button.tsx +14 -6
  33. package/components/html-viewer/HtmlViewer.tsx +208 -39
  34. package/components/html-viewer/bridge-script.ts +319 -65
  35. package/components/html-viewer/composerYield.ts +1 -51
  36. package/components/html-viewer/useHtmlAnnotation.ts +146 -57
  37. package/components/plan-diff/PlanCleanDiffView.tsx +1 -0
  38. package/components/sidebar/FileBrowser.tsx +17 -5
  39. package/components/sidebar/SidebarContainer.tsx +124 -28
  40. package/components/ui/button.tsx +10 -8
  41. package/components/ui/dialog.tsx +35 -25
  42. package/config/index.ts +6 -1
  43. package/config/reviewView.ts +42 -9
  44. package/config/settings.ts +141 -0
  45. package/hooks/useAIProviderConfig.ts +8 -7
  46. package/hooks/useActiveSection.ts +6 -4
  47. package/hooks/useAgentJobs.ts +3 -0
  48. package/hooks/useAnnotationHighlighter.ts +20 -0
  49. package/hooks/useIsMobile.ts +37 -0
  50. package/hooks/useLinkedDoc.ts +7 -0
  51. package/hooks/useScrollViewport.ts +74 -0
  52. package/hooks/useViewportEnvironment.ts +350 -0
  53. package/package.json +3 -2
  54. package/shortcuts/index.ts +3 -0
  55. package/shortcuts/plan-review/annotationMode.shortcuts.ts +91 -0
  56. package/shortcuts/plan-review/documentView.shortcuts.ts +26 -0
  57. package/shortcuts/plan-review/htmlAnnotate.shortcuts.ts +24 -0
  58. package/styles.css +1 -1
  59. package/theme.css +229 -0
  60. package/types.ts +34 -0
  61. package/utils/annotateAgentTerminal.ts +36 -5
  62. package/utils/blockTargeting.ts +6 -3
  63. package/utils/composerYield.ts +45 -0
  64. package/utils/htmlChrome.ts +20 -16
  65. package/utils/lookAndFeelAnnouncement.ts +12 -8
  66. package/utils/markdownExtensions.ts +57 -0
  67. package/utils/parser.ts +37 -2
  68. package/utils/vimNavigation.ts +4 -1
  69. package/utils/vimScroll.ts +9 -4
  70. package/utils/wideMode.ts +20 -0
  71. package/components/PlanAIAnnouncementDialog.tsx +0 -187
  72. package/components/VimModeAnnouncementDialog.tsx +0 -557
  73. package/utils/planAIAnnouncement.ts +0 -17
  74. package/utils/vimModeAnnouncement.ts +0 -23
@@ -84,9 +84,17 @@ const inputMethodShortcuts: ShortcutSection = {
84
84
  ],
85
85
  };
86
86
 
87
+ const documentViewShortcuts: ShortcutSection = {
88
+ title: 'View',
89
+ shortcuts: [
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
+ ],
92
+ };
93
+
87
94
  const annotationShortcuts: ShortcutSection = {
88
95
  title: 'Annotations',
89
96
  shortcuts: [
97
+ { keys: [shiftKey, '1-4'], desc: 'Switch annotation mode', hint: '1 Markup, 2 Comment, 3 Redline, 4 Label — matches the toolstrip order' },
90
98
  { keys: ['a-z'], desc: 'Start typing comment', hint: 'When the annotation toolbar is open, any letter key opens the comment editor with that character' },
91
99
  { keys: [altKey, '1-0'], desc: 'Apply quick label', hint: 'Instantly applies the Nth preset label (0 = 10th). When the label picker is open, bare digits also work.' },
92
100
  { keys: [modKey, enter], desc: 'Submit comment' },
@@ -108,6 +116,7 @@ const imageAnnotatorShortcuts: ShortcutSection = {
108
116
  };
109
117
 
110
118
  const sharedPlanEditorShortcuts: ShortcutSection[] = [
119
+ documentViewShortcuts,
111
120
  inputMethodShortcuts,
112
121
  annotationShortcuts,
113
122
  imageAnnotatorShortcuts,
@@ -50,7 +50,7 @@ export const Landing: React.FC<LandingProps> = ({ onEnter, shareBaseUrl }) => {
50
50
  <div className="max-w-2xl">
51
51
  <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 text-primary text-xs font-medium mb-6">
52
52
  <span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
53
- For Claude Code & OpenCode
53
+ For Claude Code &amp; OpenCode
54
54
  </div>
55
55
 
56
56
  <div className="flex items-center gap-6 mb-4">
@@ -1,10 +1,8 @@
1
1
  /// <reference path="../globals.d.ts" />
2
- import React, { useState } from 'react';
2
+ import React, { useEffect, useRef } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
- import { TextShimmer } from './TextShimmer';
5
4
  import lookGridImg from '../assets/look-grid.png';
6
5
  import lookFlatImg from '../assets/look-flat.png';
7
- import workspacesImg from '../assets/workspaces.webp';
8
6
 
9
7
  interface LookAndFeelAnnouncementDialogProps {
10
8
  isOpen: boolean;
@@ -12,33 +10,16 @@ interface LookAndFeelAnnouncementDialogProps {
12
10
  gridEnabled: boolean;
13
11
  /** App owns this: it calls configStore.set('gridEnabled', value). */
14
12
  onToggleGrid: (value: boolean) => void;
15
- /** Marks the announcement seen and closes the dialog. */
13
+ /** Persists the current choice and closes the dialog. */
16
14
  onDismiss: () => void;
17
15
  }
18
16
 
19
- const WAITLIST_URL = 'https://plannotator.ai/workspaces';
20
-
21
- const FEATURES: { title: string; desc: string }[] = [
22
- {
23
- title: 'Leaner install',
24
- desc: 'Only the core skills ship by default. Extra skills install separately.',
25
- },
26
- { title: 'A fresh new look', desc: 'Refreshed UI 2.0 with new Simple and Neutral themes.' },
27
- { title: 'Semantic code review', desc: 'Diffs grouped by what changed, not just which lines.' },
28
- { title: 'Multi-repo reviews', desc: 'Review nested repositories together in one pass.' },
29
- {
30
- title: 'Full-page HTML',
31
- desc: 'Render HTML reports and explainers full-screen, then annotate them in place.',
32
- },
33
- ];
34
-
35
17
  const LOOK_OPTIONS: {
36
18
  key: string;
37
19
  /** gridEnabled value this option selects. */
38
20
  value: boolean;
39
21
  img: string;
40
22
  title: string;
41
- tag: string;
42
23
  desc: string;
43
24
  }[] = [
44
25
  {
@@ -46,185 +27,173 @@ const LOOK_OPTIONS: {
46
27
  value: true,
47
28
  img: lookGridImg,
48
29
  title: 'Grid',
49
- tag: 'Classic',
50
- desc: 'Your plan as a floating card on grid paper.',
30
+ desc: 'A floating plan card on grid paper.',
51
31
  },
52
32
  {
53
- key: 'flat',
33
+ key: 'clean',
54
34
  value: false,
55
35
  img: lookFlatImg,
56
36
  title: 'Clean',
57
- tag: 'New',
58
- desc: 'A simpler, edge-to-edge flat card.',
37
+ desc: 'A simple, edge-to-edge document.',
59
38
  },
60
39
  ];
61
40
 
41
+ /**
42
+ * First-use plan appearance choice. The version announcement that previously
43
+ * wrapped this decision is intentionally gone: startup asks only for the
44
+ * preference Plannotator cannot infer, and Settings remains the long-term home.
45
+ */
62
46
  export const LookAndFeelAnnouncementDialog: React.FC<LookAndFeelAnnouncementDialogProps> = ({
63
47
  isOpen,
64
48
  gridEnabled,
65
49
  onToggleGrid,
66
50
  onDismiss,
67
51
  }) => {
68
- const [page, setPage] = useState<1 | 2>(1);
69
- const [hovered, setHovered] = useState<string | null>(null);
52
+ const dialogRef = useRef<HTMLDivElement>(null);
53
+ const continueRef = useRef<HTMLButtonElement>(null);
54
+ const previousFocusRef = useRef<HTMLElement | null>(null);
55
+ const onDismissRef = useRef(onDismiss);
56
+
57
+ useEffect(() => {
58
+ onDismissRef.current = onDismiss;
59
+ }, [onDismiss]);
60
+
61
+ useEffect(() => {
62
+ if (!isOpen) return;
63
+
64
+ previousFocusRef.current = document.activeElement instanceof HTMLElement
65
+ ? document.activeElement
66
+ : null;
67
+ continueRef.current?.focus();
68
+
69
+ const handleKeyDown = (event: KeyboardEvent) => {
70
+ if (event.key === 'Escape') {
71
+ event.preventDefault();
72
+ onDismissRef.current();
73
+ return;
74
+ }
75
+ if (event.key !== 'Tab') return;
76
+
77
+ const focusable = Array.from(
78
+ dialogRef.current?.querySelectorAll<HTMLElement>(
79
+ 'button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])',
80
+ ) ?? [],
81
+ );
82
+ if (focusable.length === 0) return;
83
+
84
+ const first = focusable[0];
85
+ const last = focusable[focusable.length - 1];
86
+ if (event.shiftKey && document.activeElement === first) {
87
+ event.preventDefault();
88
+ last.focus();
89
+ } else if (!event.shiftKey && document.activeElement === last) {
90
+ event.preventDefault();
91
+ first.focus();
92
+ }
93
+ };
94
+
95
+ document.addEventListener('keydown', handleKeyDown);
96
+ return () => {
97
+ document.removeEventListener('keydown', handleKeyDown);
98
+ previousFocusRef.current?.focus();
99
+ previousFocusRef.current = null;
100
+ };
101
+ }, [isOpen]);
102
+
70
103
  if (!isOpen) return null;
71
104
 
72
105
  return createPortal(
73
- <div className="fixed inset-0 z-[100] flex items-center justify-center bg-background/90 backdrop-blur-sm p-4">
74
- {/* Fixed size across both pages; the dialog never resizes. */}
75
- <div className="bg-card border border-border rounded-xl w-full max-w-5xl h-[760px] shadow-2xl flex flex-col">
76
- {page === 1 ? (
77
- <>
78
- {/* Header */}
79
- <div className="p-7 border-b border-border">
80
- <div className="flex items-start justify-between gap-4">
81
- <h3 className="font-semibold text-2xl mb-1.5">Plannotator 0.20.0 is here</h3>
82
- <a
83
- href="https://github.com/backnotprop/plannotator/releases/tag/v0.20.0"
84
- target="_blank"
85
- rel="noopener noreferrer"
86
- className="shrink-0 mt-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
87
- >
88
- Full release notes &#8599;
89
- </a>
90
- </div>
91
- <p className="text-sm text-muted-foreground">
92
- A big release. Here&apos;s what&apos;s new, plus a choice of how your plans look.
93
- </p>
94
- </div>
95
-
96
- {/* What's new */}
97
- <div className="px-7 pt-6 grid grid-cols-5 gap-3">
98
- {FEATURES.map((f) => (
99
- <div key={f.title} className="rounded-lg border border-border bg-muted/40 p-3">
100
- <div className="text-sm font-semibold">{f.title}</div>
101
- <div className="text-xs text-muted-foreground mt-0.5 leading-snug">{f.desc}</div>
102
- </div>
103
- ))}
104
- </div>
106
+ <div
107
+ className="fixed inset-0 z-[100] flex items-center justify-center overflow-y-auto bg-background/90 backdrop-blur-sm"
108
+ style={{
109
+ paddingTop: 'max(0.75rem, var(--pn-safe-top, 0px))',
110
+ paddingRight: 'max(0.75rem, var(--pn-safe-right, 0px))',
111
+ paddingBottom: 'max(0.75rem, var(--pn-safe-bottom, 0px))',
112
+ paddingLeft: 'max(0.75rem, var(--pn-safe-left, 0px))',
113
+ }}
114
+ >
115
+ <div
116
+ ref={dialogRef}
117
+ role="dialog"
118
+ aria-modal="true"
119
+ aria-labelledby="plan-look-choice-title"
120
+ aria-describedby="plan-look-choice-description"
121
+ className="flex w-full max-w-2xl flex-col overflow-hidden rounded-xl border border-border bg-card shadow-2xl"
122
+ style={{
123
+ maxHeight:
124
+ 'calc(var(--pn-viewport-height, 100vh) - max(0.75rem, var(--pn-safe-top, 0px)) - max(0.75rem, var(--pn-safe-bottom, 0px)))',
125
+ }}
126
+ >
127
+ <header className="border-b border-border px-4 py-4 sm:px-6 sm:py-5">
128
+ <h2
129
+ id="plan-look-choice-title"
130
+ className="text-balance text-xl font-semibold tracking-tight sm:text-2xl"
131
+ >
132
+ Choose how plans look
133
+ </h2>
134
+ <p
135
+ id="plan-look-choice-description"
136
+ className="mt-1 text-sm leading-relaxed text-muted-foreground"
137
+ >
138
+ Pick a starting view. You can change it anytime in Settings.
139
+ </p>
140
+ </header>
105
141
 
106
- {/* Plan-look chooser */}
107
- <div className="px-7 pt-6 flex-1 min-h-0">
108
- <div className="text-sm font-medium mb-3">Choose your plan look</div>
109
- <div className="flex gap-6">
110
- {LOOK_OPTIONS.map((opt) => {
111
- const selected = gridEnabled === opt.value;
112
- const isHovered = hovered === opt.key;
113
- return (
114
- <button
115
- key={opt.key}
116
- type="button"
117
- onClick={() => onToggleGrid(opt.value)}
118
- onMouseEnter={() => setHovered(opt.key)}
119
- onMouseLeave={() => setHovered((h) => (h === opt.key ? null : h))}
120
- aria-pressed={selected}
121
- className={`flex-1 min-w-0 flex flex-col items-stretch gap-2 rounded-lg border p-2 text-left transition-colors ${
122
- selected
123
- ? 'border-primary bg-primary/5'
124
- : 'border-border hover:border-muted-foreground/40'
142
+ <div className="min-h-0 overflow-y-auto p-3 sm:p-5">
143
+ <div className="grid grid-cols-2 gap-2.5 sm:gap-4" role="group" aria-label="Plan appearance">
144
+ {LOOK_OPTIONS.map((option) => {
145
+ const selected = gridEnabled === option.value;
146
+ return (
147
+ <button
148
+ key={option.key}
149
+ type="button"
150
+ onClick={() => onToggleGrid(option.value)}
151
+ aria-pressed={selected}
152
+ className={`plan-look-choice-option min-h-11 min-w-0 rounded-xl border p-2 text-left outline-none transition-[background-color,border-color,box-shadow] motion-reduce:transition-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-card active:bg-muted/35 sm:p-3 ${
153
+ selected
154
+ ? 'border-primary bg-primary/[0.06] shadow-sm'
155
+ : 'border-border bg-muted/20'
156
+ }`}
157
+ >
158
+ <img
159
+ src={option.img}
160
+ alt=""
161
+ aria-hidden="true"
162
+ className="aspect-[1000/626] w-full rounded-lg border border-border/70 object-cover object-top select-none"
163
+ draggable={false}
164
+ />
165
+ <span className="mt-2.5 flex min-w-0 items-center justify-between gap-2 px-0.5">
166
+ <span className="truncate text-sm font-semibold sm:text-base">{option.title}</span>
167
+ <span
168
+ aria-hidden="true"
169
+ className={`h-4 w-4 shrink-0 rounded-full border-2 p-[3px] ${
170
+ selected ? 'border-primary' : 'border-muted-foreground/35'
125
171
  }`}
126
172
  >
127
- <div className="relative">
128
- <img
129
- src={opt.img}
130
- alt={`${opt.title} plan look`}
131
- className="w-full rounded-md select-none"
132
- draggable={false}
133
- style={{
134
- border: `2px solid ${
135
- selected
136
- ? 'var(--primary)'
137
- : 'color-mix(in srgb, var(--primary) 25%, transparent)'
138
- }`,
139
- transform: isHovered ? 'scale(1.22)' : 'scale(1)',
140
- transformOrigin: 'center',
141
- zIndex: isHovered ? 50 : 0,
142
- position: 'relative',
143
- boxShadow: isHovered ? '0 14px 36px rgba(0,0,0,0.4)' : 'none',
144
- transition:
145
- 'transform 0.25s cubic-bezier(0.34,1.56,0.64,1), border-color 0.2s ease, box-shadow 0.2s ease',
146
- }}
147
- />
148
- </div>
149
- <div className="flex items-center justify-between gap-2 px-0.5 mt-1">
150
- <span className="text-base font-semibold">{opt.title}</span>
151
- <span
152
- className={`text-[11px] leading-none px-2 py-0.5 rounded-full ${
153
- selected ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'
154
- }`}
155
- >
156
- {selected ? 'Selected' : opt.tag}
157
- </span>
158
- </div>
159
- <p className="text-sm text-muted-foreground px-0.5 leading-snug">{opt.desc}</p>
160
- </button>
161
- );
162
- })}
163
- </div>
164
- </div>
165
-
166
- {/* Footer */}
167
- <div className="px-7 py-5 border-t border-border flex justify-end items-center gap-4">
168
- <button
169
- type="button"
170
- onClick={() => setPage(2)}
171
- className="px-4 py-2 rounded-lg border border-primary/35 hover:opacity-80 transition-opacity"
172
- >
173
- <TextShimmer className="text-sm font-medium" duration={2.5} spread={1.5}>
174
- {'✨ Workspaces are coming 🎉 →'}
175
- </TextShimmer>
176
- </button>
177
- <button
178
- onClick={onDismiss}
179
- className="px-4 py-2 bg-primary text-primary-foreground rounded-lg text-sm font-medium hover:opacity-90 transition-opacity"
180
- >
181
- Got it
182
- </button>
183
- </div>
184
- </>
185
- ) : (
186
- <>
187
- {/* Header */}
188
- <div className="p-7 border-b border-border">
189
- <h3 className="font-semibold text-2xl mb-1.5">Workspaces are coming 🎉</h3>
190
- <p className="text-sm text-muted-foreground">
191
- A shared context workspace for specs, reviews, and decisions your agents can build
192
- on. Join the waitlist.
193
- </p>
194
- </div>
195
-
196
- {/* The landscape teaser image, scaled to fit the fixed dialog. */}
197
- <div className="flex-1 min-h-0 p-6 flex items-center justify-center">
198
- <img
199
- src={workspacesImg}
200
- alt="Plannotator Workspaces, a shared context workspace across your agents"
201
- className="max-h-full max-w-full w-auto object-contain rounded-lg border border-border select-none"
202
- draggable={false}
203
- />
204
- </div>
173
+ {selected && <span className="block h-full w-full rounded-full bg-primary" />}
174
+ </span>
175
+ </span>
176
+ <span className="mt-0.5 block px-0.5 text-xs leading-snug text-muted-foreground sm:text-sm">
177
+ {option.desc}
178
+ </span>
179
+ </button>
180
+ );
181
+ })}
182
+ </div>
183
+ </div>
205
184
 
206
- {/* Footer */}
207
- <div className="px-7 py-5 border-t border-border flex justify-end items-center gap-4">
208
- <button
209
- type="button"
210
- onClick={() => setPage(1)}
211
- className="text-sm text-muted-foreground hover:text-foreground transition-colors"
212
- >
213
- &larr; Back
214
- </button>
215
- <a
216
- href={WAITLIST_URL}
217
- target="_blank"
218
- rel="noopener noreferrer"
219
- className="px-4 py-2 bg-primary text-primary-foreground rounded-lg text-sm font-medium hover:opacity-90 transition-opacity"
220
- >
221
- Join the waitlist
222
- </a>
223
- </div>
224
- </>
225
- )}
185
+ <footer className="border-t border-border px-4 py-3 sm:flex sm:justify-end sm:px-6 sm:py-4">
186
+ <button
187
+ ref={continueRef}
188
+ type="button"
189
+ onClick={onDismiss}
190
+ className="min-h-11 w-full rounded-lg bg-primary px-5 text-sm font-medium text-primary-foreground outline-none transition-opacity motion-reduce:transition-none hover:opacity-90 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-card sm:w-auto"
191
+ >
192
+ Continue
193
+ </button>
194
+ </footer>
226
195
  </div>
227
196
  </div>,
228
- document.body
197
+ document.body,
229
198
  );
230
199
  };