@plannotator/ui 0.28.0 → 0.29.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 (105) hide show
  1. package/README.md +6 -0
  2. package/components/AISettingsTab.tsx +5 -4
  3. package/components/ActionMenu.tsx +5 -1
  4. package/components/AgentsTab.tsx +10 -23
  5. package/components/AnnotationPanel.tsx +9 -5
  6. package/components/AnnotationToolbar.tsx +5 -13
  7. package/components/AnnotationToolstrip.tsx +2 -2
  8. package/components/ApproveDropdown.tsx +1 -1
  9. package/components/BlockRenderer.tsx +1 -1
  10. package/components/CodeFilePopout.tsx +5 -4
  11. package/components/CommentPopover.tsx +391 -65
  12. package/components/DocBadges.tsx +29 -11
  13. package/components/ExportModal.tsx +16 -8
  14. package/components/GraphvizBlock.tsx +1 -1
  15. package/components/InlineMarkdown.tsx +30 -13
  16. package/components/KeyboardShortcuts.tsx +37 -2
  17. package/components/Landing.tsx +7 -7
  18. package/components/MenuVersionSection.tsx +4 -4
  19. package/components/MermaidBlock.tsx +1 -1
  20. package/components/ModeToggle.tsx +7 -6
  21. package/components/OpenInAppButton.tsx +2 -5
  22. package/components/PinpointOverlay.tsx +9 -7
  23. package/components/PlanHeaderMenu.tsx +8 -8
  24. package/components/PopoutDialog.tsx +6 -1
  25. package/components/ResizeHandle.tsx +1 -0
  26. package/components/Settings.tsx +172 -12
  27. package/components/SkillReferenceMenu.tsx +260 -0
  28. package/components/StickyHeaderLane.tsx +7 -0
  29. package/components/ThemeProvider.tsx +131 -32
  30. package/components/ThemeTab.tsx +123 -77
  31. package/components/ToolbarButtons.tsx +29 -8
  32. package/components/Viewer.tsx +396 -130
  33. package/components/VimKeyHud.tsx +695 -0
  34. package/components/VimModeAnnouncementDialog.tsx +557 -0
  35. package/components/VimModeOverlay.tsx +235 -0
  36. package/components/VimTargetReticle.tsx +284 -0
  37. package/components/ai/DocumentAIChatPanel.tsx +1 -1
  38. package/components/blocks/CodeBlock.tsx +18 -18
  39. package/components/blocks/TablePopout.tsx +7 -8
  40. package/components/blocks/TableToolbar.tsx +7 -8
  41. package/components/goal-setup/GoalSetupSurface.tsx +16 -3
  42. package/components/html-viewer/HtmlViewer.tsx +450 -47
  43. package/components/html-viewer/annotationNumbering.ts +37 -0
  44. package/components/html-viewer/bridge-script.ts +4051 -298
  45. package/components/html-viewer/composerYield.ts +51 -0
  46. package/components/html-viewer/srcdoc.ts +18 -3
  47. package/components/html-viewer/useHtmlAnnotation.ts +457 -32
  48. package/components/icons/themeIcons.tsx +1 -1
  49. package/components/plan-diff/PlanCleanDiffView.tsx +9 -9
  50. package/components/plan-diff/PlanDiffBadge.tsx +22 -1
  51. package/components/settings/HooksTab.tsx +12 -8
  52. package/components/sidebar/FileBrowser.tsx +4 -1
  53. package/components/themeModes.tsx +28 -0
  54. package/config/configStore.ts +76 -1
  55. package/config/settings.ts +152 -0
  56. package/configure.ts +9 -0
  57. package/globals.d.ts +7 -1
  58. package/hooks/useAIChat.ts +5 -2
  59. package/hooks/useAIProviderActivation.ts +47 -0
  60. package/hooks/useAIProviderConfig.ts +5 -1
  61. package/hooks/useAgentSettings.ts +64 -23
  62. package/hooks/useAgents.ts +4 -4
  63. package/hooks/useAnnotationHighlighter.ts +100 -3
  64. package/hooks/useArchive.ts +2 -1
  65. package/hooks/useFenceTheme.ts +17 -0
  66. package/hooks/useLinkedDoc.ts +68 -1
  67. package/hooks/usePinpoint.ts +76 -75
  68. package/hooks/usePlanDiff.ts +73 -2
  69. package/hooks/useSkillReferenceAutocomplete.ts +239 -0
  70. package/hooks/useUpdateCheck.ts +1 -2
  71. package/hooks/useVimDocumentFocus.ts +116 -0
  72. package/hooks/useVimSelection.ts +1063 -0
  73. package/package.json +4 -4
  74. package/print.css +14 -13
  75. package/shortcuts/core.ts +38 -13
  76. package/shortcuts/index.ts +10 -0
  77. package/shortcuts/plan-review/commentPopover.shortcuts.ts +7 -0
  78. package/shortcuts/plan-review/vimSelection.shortcuts.ts +251 -0
  79. package/shortcuts/runtime.ts +111 -12
  80. package/styles.css +1 -1
  81. package/theme.css +504 -0
  82. package/themes/colorblind.css +89 -0
  83. package/themes/plannotator.css +2 -2
  84. package/types.ts +93 -10
  85. package/utils/agentSwitch.ts +33 -7
  86. package/utils/blockTargeting.ts +462 -178
  87. package/utils/clipboard.ts +110 -0
  88. package/utils/codeBlockMark.ts +50 -0
  89. package/utils/codeHighlight.ts +293 -0
  90. package/utils/codexModels.ts +79 -0
  91. package/utils/domSelection.ts +84 -0
  92. package/utils/htmlChrome.ts +73 -0
  93. package/utils/inputMethod.ts +79 -6
  94. package/utils/parser.ts +517 -21
  95. package/utils/preferenceTtl.ts +15 -0
  96. package/utils/sharing.ts +0 -1
  97. package/utils/skillCatalog.ts +269 -0
  98. package/utils/skillReferences.ts +475 -0
  99. package/utils/syntaxTheme.ts +83 -0
  100. package/utils/themeRegistry.ts +154 -0
  101. package/utils/vimHud.ts +263 -0
  102. package/utils/vimModeAnnouncement.ts +23 -0
  103. package/utils/vimNavigation.ts +417 -0
  104. package/utils/vimReticle.ts +88 -0
  105. package/utils/vimScroll.ts +162 -0
@@ -1,3 +1,5 @@
1
+ import { parseThemeMode, type Mode } from '../components/themeModes';
2
+
1
3
  export interface ThemeColors {
2
4
  primary: string;
3
5
  secondary: string;
@@ -133,6 +135,17 @@ export const BUILT_IN_THEMES: ThemeInfo[] = [
133
135
  light: { primary: '#3C7CAB', secondary: '#E8E8E8', accent: '#4C7F8C', background: '#FCFCFC', foreground: '#141414' },
134
136
  },
135
137
  },
138
+ {
139
+ id: 'colorblind',
140
+ name: 'Colorblind',
141
+ builtIn: true,
142
+ modeSupport: 'both',
143
+ syntaxHighlighting: true,
144
+ colors: {
145
+ dark: { primary: '#7fb0f2', secondary: '#2a2e37', accent: '#e5b84a', background: '#16181d', foreground: '#eaedf2' },
146
+ light: { primary: '#1a63b8', secondary: '#e7eaef', accent: '#946300', background: '#f8f9fb', foreground: '#22262e' },
147
+ },
148
+ },
136
149
  {
137
150
  id: 'dark-plus',
138
151
  name: 'Dark+',
@@ -565,3 +578,144 @@ export const BUILT_IN_THEMES: ThemeInfo[] = [
565
578
  },
566
579
  },
567
580
  ];
581
+
582
+ /** Return the explicit mode a palette cannot render, if any. */
583
+ export function getUnsupportedMode(themeId: string): 'light' | 'dark' | null {
584
+ const theme = BUILT_IN_THEMES.find(({ id }) => id === themeId);
585
+ if (theme?.modeSupport === 'dark-only') return 'light';
586
+ if (theme?.modeSupport === 'light-only') return 'dark';
587
+ return null;
588
+ }
589
+
590
+ /**
591
+ * Return whether a palette can honor a mode choice without coercion.
592
+ *
593
+ * @deprecated Every mode is always selectable now that a palette is assigned to
594
+ * one half of a light/dark pair: a palette that cannot render a mode simply
595
+ * never occupies that half. Use {@link themeSupportsHalf} to ask whether a
596
+ * palette may be ASSIGNED to a half. Kept for published consumers.
597
+ */
598
+ export function isThemeModeAvailable(themeId: string, mode: Mode): boolean {
599
+ return mode === 'system' || getUnsupportedMode(themeId) !== mode;
600
+ }
601
+
602
+ /**
603
+ * Keep System intact while coercing an unsupported explicit mode.
604
+ *
605
+ * @deprecated Modes are no longer coerced. Assign the palette to the half it
606
+ * supports ({@link themeSupportsHalf}) and let {@link resolveThemeMode} handle
607
+ * rendering. Kept for published consumers.
608
+ */
609
+ export function normalizeThemeMode(themeId: string, mode: Mode): Mode {
610
+ const unsupportedMode = getUnsupportedMode(themeId);
611
+ if (mode === 'system' || mode !== unsupportedMode) return mode;
612
+ return unsupportedMode === 'light' ? 'dark' : 'light';
613
+ }
614
+
615
+ /** Resolve the mode a palette actually renders. */
616
+ export function resolveThemeMode(
617
+ themeId: string,
618
+ preferredMode: 'light' | 'dark',
619
+ ): 'light' | 'dark' {
620
+ const unsupportedMode = getUnsupportedMode(themeId);
621
+ if (preferredMode !== unsupportedMode) return preferredMode;
622
+ return unsupportedMode === 'light' ? 'dark' : 'light';
623
+ }
624
+
625
+ // --- Light/dark theme pairs -------------------------------------------------
626
+ //
627
+ // A user assigns one palette to the light half and one to the dark half. The
628
+ // active palette is pair[preferredMode], so System mode flips between the two
629
+ // (Kanagawa Lotus by day, Kanagawa Wave at night). Mode-restricted palettes are
630
+ // only ever offered for the half they can render, which is why no mode needs to
631
+ // be coerced any more.
632
+
633
+ /** Which half of a light/dark pair a palette is assigned to. */
634
+ export type ThemeHalf = 'light' | 'dark';
635
+
636
+ /** A user's full appearance choice: which mode, and a palette for each half. */
637
+ export interface ThemePair {
638
+ mode: Mode;
639
+ light: string;
640
+ dark: string;
641
+ }
642
+
643
+ /** The palette every fresh install starts on, in both halves. */
644
+ export const DEFAULT_COLOR_THEME = 'plannotator';
645
+
646
+ export const DEFAULT_THEME_PAIR: ThemePair = {
647
+ mode: 'dark',
648
+ light: DEFAULT_COLOR_THEME,
649
+ dark: DEFAULT_COLOR_THEME,
650
+ };
651
+
652
+ /** Return whether a palette is registered. */
653
+ export function isKnownTheme(themeId: unknown): themeId is string {
654
+ return typeof themeId === 'string' && BUILT_IN_THEMES.some(({ id }) => id === themeId);
655
+ }
656
+
657
+ /** Return whether a palette can occupy one half of the pair. */
658
+ export function themeSupportsHalf(themeId: string, half: ThemeHalf): boolean {
659
+ return getUnsupportedMode(themeId) !== half;
660
+ }
661
+
662
+ /** The palettes assignable to one half — `both` palettes appear in each. */
663
+ export function themesForHalf(themes: ThemeInfo[], half: ThemeHalf): ThemeInfo[] {
664
+ return themes.filter(({ id }) => themeSupportsHalf(id, half));
665
+ }
666
+
667
+ /**
668
+ * Seed a pair from the single palette older versions persisted. A `both`
669
+ * palette takes over both halves; a mode-restricted one takes the half it
670
+ * supports and the other half falls back to the default palette.
671
+ */
672
+ export function seedThemePair(colorThemeId: unknown, mode: Mode): ThemePair {
673
+ const id = isKnownTheme(colorThemeId) ? colorThemeId : DEFAULT_COLOR_THEME;
674
+ return {
675
+ mode,
676
+ light: themeSupportsHalf(id, 'light') ? id : DEFAULT_COLOR_THEME,
677
+ dark: themeSupportsHalf(id, 'dark') ? id : DEFAULT_COLOR_THEME,
678
+ };
679
+ }
680
+
681
+ /**
682
+ * Repair an untrusted pair (cookie, config.json, or an older release) so every
683
+ * half holds a registered palette that can actually render it.
684
+ */
685
+ export function normalizeThemePair(
686
+ input: Partial<Record<keyof ThemePair, unknown>> | undefined,
687
+ fallback: ThemePair = DEFAULT_THEME_PAIR,
688
+ ): ThemePair {
689
+ const half = (key: ThemeHalf): string => {
690
+ const candidate = input?.[key];
691
+ if (isKnownTheme(candidate) && themeSupportsHalf(candidate, key)) return candidate;
692
+ return themeSupportsHalf(fallback[key], key) ? fallback[key] : DEFAULT_COLOR_THEME;
693
+ };
694
+ return {
695
+ mode: parseThemeMode(input?.mode, fallback.mode),
696
+ light: half('light'),
697
+ dark: half('dark'),
698
+ };
699
+ }
700
+
701
+ /** The palette a pair renders for the mode the user is actually seeing. */
702
+ export function resolvePairTheme(pair: ThemePair, preferredMode: ThemeHalf): string {
703
+ return pair[preferredMode];
704
+ }
705
+
706
+ /* The pair a fresh install starts from. ThemeProvider installs its own props
707
+ here before the config store resolves, so a host embedding @plannotator/ui
708
+ keeps its `defaultTheme` / `defaultColorTheme` defaults. */
709
+ let defaultThemePair: ThemePair = DEFAULT_THEME_PAIR;
710
+
711
+ export function setDefaultThemePair(pair: ThemePair): void {
712
+ defaultThemePair = pair;
713
+ }
714
+
715
+ export function getDefaultThemePair(): ThemePair {
716
+ return defaultThemePair;
717
+ }
718
+
719
+ export function resetDefaultThemePair(): void {
720
+ defaultThemePair = DEFAULT_THEME_PAIR;
721
+ }
@@ -0,0 +1,263 @@
1
+ import {
2
+ describeVimSelectionAction,
3
+ vimSelectionShortcuts,
4
+ type VimSelectionActionId,
5
+ type VimSelectionHudContext,
6
+ } from '../shortcuts/plan-review/vimSelection.shortcuts';
7
+
8
+ /** Semantic label displayed in the Vim key HUD. */
9
+ export type VimHudPhase =
10
+ | 'BLOCK'
11
+ | 'INLINE'
12
+ | 'LINE'
13
+ | 'WORD'
14
+ | 'TEXT'
15
+ | 'VISUAL'
16
+ | 'ACTION';
17
+
18
+ /** One successfully handled Vim command rendered by the key HUD. */
19
+ export interface VimHudCommand {
20
+ readonly sequence: number;
21
+ readonly actionId: VimSelectionActionId;
22
+ readonly key: string;
23
+ readonly description: string;
24
+ readonly context: VimSelectionHudContext;
25
+ }
26
+
27
+ /** Stable section identifiers used by the expanded Vim HUD key map. */
28
+ export type VimHudLegendGroupId =
29
+ | 'structure'
30
+ | 'text'
31
+ | 'selection'
32
+ | 'annotation'
33
+ | 'control';
34
+
35
+ /** One registered Vim command projected into the expanded HUD key map. */
36
+ export interface VimHudLegendItem {
37
+ readonly actionId: VimSelectionActionId;
38
+ readonly key: string;
39
+ readonly description: string;
40
+ }
41
+
42
+ /** One learnable command family rendered in the expanded HUD key map. */
43
+ export interface VimHudLegendGroup {
44
+ readonly id: VimHudLegendGroupId;
45
+ readonly title: string;
46
+ readonly description: string;
47
+ readonly items: readonly VimHudLegendItem[];
48
+ }
49
+
50
+ interface VimHudLegendGroupSpec {
51
+ readonly id: VimHudLegendGroupId;
52
+ readonly title: string;
53
+ readonly description: string;
54
+ readonly context: VimSelectionHudContext;
55
+ readonly actionIds: readonly VimSelectionActionId[];
56
+ }
57
+
58
+ const VIM_HUD_LEGEND_GROUP_SPECS: readonly VimHudLegendGroupSpec[] = [
59
+ {
60
+ id: 'structure',
61
+ title: 'Document',
62
+ description: 'Move by blocks and semantic structure',
63
+ context: 'block',
64
+ actionIds: [
65
+ 'moveDown',
66
+ 'moveUp',
67
+ 'documentStart',
68
+ 'documentEnd',
69
+ 'moveOut',
70
+ 'refine',
71
+ ],
72
+ },
73
+ {
74
+ id: 'text',
75
+ title: 'Text',
76
+ description: 'Move by characters, lines, words, and paragraphs',
77
+ context: 'text',
78
+ actionIds: [
79
+ 'moveOut',
80
+ 'refine',
81
+ 'moveDown',
82
+ 'moveUp',
83
+ 'wordForward',
84
+ 'wordBackward',
85
+ 'wordEnd',
86
+ 'lineStart',
87
+ 'lineEnd',
88
+ 'previousTextBlock',
89
+ 'nextTextBlock',
90
+ ],
91
+ },
92
+ {
93
+ id: 'selection',
94
+ title: 'Select',
95
+ description: 'Grow an exact or whole-block selection',
96
+ context: 'block',
97
+ actionIds: ['visual', 'visualBlock', 'swapSelectionEnds'],
98
+ },
99
+ {
100
+ id: 'annotation',
101
+ title: 'Annotate',
102
+ description: 'Act on the current target or selection',
103
+ context: 'block',
104
+ actionIds: [
105
+ 'activeAnnotation',
106
+ 'annotationMenu',
107
+ 'comment',
108
+ 'redline',
109
+ 'markup',
110
+ 'label',
111
+ 'copy',
112
+ ],
113
+ },
114
+ {
115
+ id: 'control',
116
+ title: 'Control',
117
+ description: 'Back out or show this key map',
118
+ context: 'block',
119
+ actionIds: ['cancel', 'help'],
120
+ },
121
+ ];
122
+
123
+ function normalizeVimHudKey(
124
+ actionId: VimSelectionActionId,
125
+ rawKey: string,
126
+ ): string {
127
+ if (actionId === 'documentStart') return 'gg';
128
+ if (rawKey === 'Escape') return 'esc';
129
+ if (rawKey === 'Enter') return 'enter';
130
+ if (rawKey === ' ' || rawKey === 'Space' || rawKey === 'Spacebar') return 'space';
131
+ return rawKey;
132
+ }
133
+
134
+ function formatVimLegendKey(
135
+ actionId: VimSelectionActionId,
136
+ binding: string,
137
+ ): string {
138
+ if (actionId === 'documentStart') return 'gg';
139
+ if (actionId === 'documentEnd') return 'G';
140
+ if (actionId === 'visualBlock') return 'V';
141
+ if (binding === 'Escape') return 'esc';
142
+ if (binding === 'Enter') return 'enter';
143
+ if (binding === 'Space') return 'space';
144
+ return binding.length === 1 ? binding.toLowerCase() : binding;
145
+ }
146
+
147
+ /**
148
+ * Build immutable HUD feedback from a command that the Vim controller handled.
149
+ */
150
+ export function createVimHudCommand(
151
+ sequence: number,
152
+ actionId: VimSelectionActionId,
153
+ rawKey: string,
154
+ context: VimSelectionHudContext,
155
+ ): VimHudCommand {
156
+ return {
157
+ sequence,
158
+ actionId,
159
+ key: normalizeVimHudKey(actionId, rawKey),
160
+ description: describeVimSelectionAction(actionId, context),
161
+ context,
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Project the registered Vim shortcut scope into learnable HUD groups.
167
+ *
168
+ * Movement actions intentionally appear in both Document and Text with
169
+ * contextual descriptions because the same Vim keys change granularity after
170
+ * the user refines into text.
171
+ */
172
+ export function getVimHudLegendGroups(): readonly VimHudLegendGroup[] {
173
+ return VIM_HUD_LEGEND_GROUP_SPECS.map((group) => ({
174
+ id: group.id,
175
+ title: group.title,
176
+ description: group.description,
177
+ items: group.actionIds.map((actionId) => {
178
+ const shortcut = vimSelectionShortcuts.shortcuts[actionId];
179
+ return {
180
+ actionId,
181
+ key: formatVimLegendKey(actionId, shortcut.bindings[0] ?? ''),
182
+ description: describeVimSelectionAction(actionId, group.context),
183
+ };
184
+ }),
185
+ }));
186
+ }
187
+
188
+ /**
189
+ * Return whether a legend group represents the HUD's current movement level.
190
+ */
191
+ export function isVimHudLegendGroupActive(
192
+ groupId: VimHudLegendGroupId,
193
+ phase: VimHudPhase,
194
+ ): boolean {
195
+ switch (phase) {
196
+ case 'BLOCK':
197
+ case 'INLINE':
198
+ return groupId === 'structure';
199
+ case 'LINE':
200
+ case 'WORD':
201
+ case 'TEXT':
202
+ return groupId === 'text';
203
+ case 'VISUAL':
204
+ return groupId === 'selection';
205
+ case 'ACTION':
206
+ return groupId === 'annotation';
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Project live Vim navigation state and the latest motion into the video HUD's
212
+ * semantic phase vocabulary.
213
+ */
214
+ export function getVimHudPhase(
215
+ state: VimSelectionHudContext,
216
+ actionId?: VimSelectionActionId,
217
+ ): VimHudPhase {
218
+ switch (state) {
219
+ case 'action':
220
+ return 'ACTION';
221
+ case 'visual':
222
+ case 'visual-block':
223
+ return 'VISUAL';
224
+ case 'inline':
225
+ return 'INLINE';
226
+ case 'block':
227
+ case 'inactive':
228
+ return 'BLOCK';
229
+ case 'text':
230
+ switch (actionId) {
231
+ case 'moveDown':
232
+ case 'moveUp':
233
+ case 'lineStart':
234
+ case 'lineEnd':
235
+ return 'LINE';
236
+ case 'wordForward':
237
+ case 'wordBackward':
238
+ case 'wordEnd':
239
+ return 'WORD';
240
+ case 'previousTextBlock':
241
+ case 'nextTextBlock':
242
+ return 'BLOCK';
243
+ case 'documentStart':
244
+ case 'documentEnd':
245
+ case 'moveOut':
246
+ case 'refine':
247
+ case 'visual':
248
+ case 'visualBlock':
249
+ case 'swapSelectionEnds':
250
+ case 'activeAnnotation':
251
+ case 'annotationMenu':
252
+ case 'comment':
253
+ case 'redline':
254
+ case 'markup':
255
+ case 'label':
256
+ case 'copy':
257
+ case 'cancel':
258
+ case 'help':
259
+ case undefined:
260
+ return 'TEXT';
261
+ }
262
+ }
263
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tracks whether the user has seen the Vim controls announcement.
3
+ *
4
+ * Cookie-backed storage keeps the dismissal across Plannotator's random
5
+ * localhost ports while still honoring host-provided storage backends.
6
+ */
7
+
8
+ import { storage } from './storage';
9
+
10
+ const STORAGE_KEY = 'plannotator-vim-mode-announcement-seen';
11
+ // v2 is the full interactive introduction with the live settings and HUD
12
+ // showcase. Earlier development builds used v1 for a smaller notice.
13
+ const CURRENT_VERSION = '2';
14
+
15
+ /** Return whether the current announcement version has not been dismissed. */
16
+ export function needsVimModeAnnouncement(): boolean {
17
+ return storage.getItem(STORAGE_KEY) !== CURRENT_VERSION;
18
+ }
19
+
20
+ /** Persist dismissal of the current announcement version in shared UI storage. */
21
+ export function markVimModeAnnouncementSeen(): void {
22
+ storage.setItem(STORAGE_KEY, CURRENT_VERSION);
23
+ }