@plannotator/ui 0.46.1 → 0.49.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.
@@ -72,6 +72,8 @@ interface UseCodeAnnotationDraftOptions {
72
72
  autoViewSuppressed?: Set<string>;
73
73
  isApiMode: boolean;
74
74
  submitted: boolean;
75
+ /** Hosts with independent review-progress storage omit viewed state from drafts. */
76
+ persistViewedFiles?: boolean;
75
77
  /** Receives the unsent items found on a target the session switched onto
76
78
  * in place (#1590), already filtered to ids the session neither holds nor
77
79
  * deleted. The host adds them to its state; autosave then saves the merge.
@@ -125,12 +127,15 @@ export function useCodeAnnotationDraft({
125
127
  submitted,
126
128
  onDraftTargetMerge,
127
129
  targetLoadTimeoutMs = TARGET_LOAD_TIMEOUT_MS,
130
+ persistViewedFiles = true,
128
131
  }: UseCodeAnnotationDraftOptions): UseCodeAnnotationDraftResult {
129
132
  const [draftBanner, setDraftBanner] = useState<{ count: number; viewedCount: number; timeAgo: string } | null>(null);
130
133
  const draftDataRef = useRef<DraftData | null>(null);
131
134
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
132
135
  const hasMountedRef = useRef(false);
133
136
  const draftGenerationRef = useRef(0);
137
+ const persistViewedRef = useRef(persistViewedFiles);
138
+ persistViewedRef.current = persistViewedFiles;
134
139
  // True once the user has actually had annotations this session. Used to decide
135
140
  // whether an empty state is a real "cleared everything" edit (persist it) vs a
136
141
  // fresh/unengaged session (leave the server alone). Keyed on annotations only —
@@ -186,6 +191,9 @@ export function useCodeAnnotationDraft({
186
191
  return data as DraftData | null;
187
192
  })
188
193
  .then((data: DraftData | null) => {
194
+ // Keep even a viewed-only draft while independent progress support is
195
+ // unresolved; a failed/unsupported load may need to offer it later.
196
+ draftDataRef.current = data;
189
197
  const generation = readDraftGeneration(data?.draftGeneration);
190
198
  if (generation !== null) {
191
199
  draftGenerationRef.current = Math.max(draftGenerationRef.current, generation);
@@ -193,7 +201,7 @@ export function useCodeAnnotationDraft({
193
201
  const annotationCount = (Array.isArray(data?.codeAnnotations) ? data.codeAnnotations.length : 0)
194
202
  + (Array.isArray(data?.descriptionAnnotations) ? data.descriptionAnnotations.length : 0)
195
203
  + (Array.isArray(data?.commentAnnotations) ? data.commentAnnotations.length : 0);
196
- const viewedCount = Array.isArray(data?.viewedFiles) ? data.viewedFiles.length : 0;
204
+ const viewedCount = persistViewedRef.current && Array.isArray(data?.viewedFiles) ? data.viewedFiles.length : 0;
197
205
  if (annotationCount > 0 || viewedCount > 0) {
198
206
  draftDataRef.current = data;
199
207
  setDraftBanner({
@@ -209,6 +217,15 @@ export function useCodeAnnotationDraft({
209
217
  });
210
218
  }, [isApiMode]);
211
219
 
220
+ useEffect(() => {
221
+ const data = draftDataRef.current;
222
+ if (!data) return;
223
+ const count = (data.codeAnnotations?.length ?? 0)
224
+ + (data.descriptionAnnotations?.length ?? 0) + (data.commentAnnotations?.length ?? 0);
225
+ const viewedCount = persistViewedFiles ? data.viewedFiles?.length ?? 0 : 0;
226
+ setDraftBanner(count || viewedCount ? { count, viewedCount, timeAgo: formatTimeAgo(data.ts || 0) } : null);
227
+ }, [persistViewedFiles]);
228
+
212
229
  // Debounced auto-save on annotation/viewed changes
213
230
  useEffect(() => {
214
231
  if (!isApiMode || submitted) return;
@@ -223,7 +240,7 @@ export function useCodeAnnotationDraft({
223
240
  // via `allAnnotations` and have their own lifecycle, separate from the draft.
224
241
  if (annotations.some((a) => !a.source) || descriptionAnnotations.length > 0 || commentAnnotations.length > 0) hasHadAnnotationsRef.current = true;
225
242
 
226
- const isEmpty = annotations.length === 0 && descriptionAnnotations.length === 0 && commentAnnotations.length === 0 && viewedFiles.size === 0;
243
+ const isEmpty = annotations.length === 0 && descriptionAnnotations.length === 0 && commentAnnotations.length === 0 && (!persistViewedFiles || viewedFiles.size === 0);
227
244
  // Leave the server alone for an empty state until the user has actually had
228
245
  // annotations this session. This preserves an unrestored draft sitting on disk
229
246
  // at mount (the draft-recovery banner can still offer it).
@@ -259,8 +276,8 @@ export function useCodeAnnotationDraft({
259
276
  codeAnnotations: annotations,
260
277
  descriptionAnnotations,
261
278
  commentAnnotations,
262
- viewedFiles: [...viewedFiles],
263
- ...(autoViewSuppressed && autoViewSuppressed.size > 0
279
+ ...(persistViewedFiles ? { viewedFiles: [...viewedFiles] } : {}),
280
+ ...(persistViewedFiles && autoViewSuppressed && autoViewSuppressed.size > 0
264
281
  ? { autoViewSuppressed: [...autoViewSuppressed] }
265
282
  : {}),
266
283
  draftGeneration,
@@ -273,7 +290,7 @@ export function useCodeAnnotationDraft({
273
290
  return () => {
274
291
  if (timerRef.current) clearTimeout(timerRef.current);
275
292
  };
276
- }, [annotations, descriptionAnnotations, commentAnnotations, viewedFiles, autoViewSuppressed, isApiMode, submitted, saveNudge]);
293
+ }, [annotations, descriptionAnnotations, commentAnnotations, viewedFiles, autoViewSuppressed, isApiMode, submitted, saveNudge, persistViewedFiles]);
277
294
 
278
295
  const restoreDraft = useCallback(() => {
279
296
  // Cancel any pending autosave so it can't fire with pre-restore state and
@@ -286,9 +303,9 @@ export function useCodeAnnotationDraft({
286
303
  annotations: data?.codeAnnotations ?? [],
287
304
  descriptionAnnotations: data?.descriptionAnnotations ?? [],
288
305
  commentAnnotations: data?.commentAnnotations ?? [],
289
- viewedFiles: data?.viewedFiles ?? [],
290
- autoViewSuppressed: data?.autoViewSuppressed ?? [],
291
306
  patchChanged: data?.patchChanged === true,
307
+ viewedFiles: persistViewedRef.current ? data?.viewedFiles ?? [] : [],
308
+ autoViewSuppressed: persistViewedRef.current ? data?.autoViewSuppressed ?? [] : [],
292
309
  };
293
310
  }, []);
294
311
 
@@ -3,6 +3,7 @@ import {
3
3
  CLAUDE_FALLBACK_MODELS,
4
4
  CODEX_FALLBACK_MODELS,
5
5
  type CatalogModel,
6
+ type ModelsSource,
6
7
  } from '@plannotator/core/model-catalog';
7
8
  import type { AgentCapabilities } from '../types';
8
9
 
@@ -26,8 +27,14 @@ export type CatalogEngine = 'claude' | 'codex';
26
27
  export interface ModelCatalog {
27
28
  models: CatalogModel[];
28
29
  settled: boolean;
30
+ /** Where the server says the list came from; absent when it did not say. */
31
+ modelsSource?: ModelsSource;
32
+ /** The installed CLI's version, when the server reports it. */
33
+ toolVersion?: string;
29
34
  }
30
35
 
36
+ type LoadedCatalog = Pick<ModelCatalog, 'models' | 'modelsSource' | 'toolVersion'>;
37
+
31
38
  export interface ModelCatalogs extends Record<CatalogEngine, ModelCatalog> {
32
39
  /** Fetch one engine's catalog (no-op for other engines or when not installed). */
33
40
  load: (engine: string | null | undefined) => void;
@@ -47,19 +54,19 @@ const isCatalogEngine = (engine: unknown): engine is CatalogEngine => engine ===
47
54
 
48
55
  // One request per engine per page: every launcher surface shares the answer.
49
56
  // A failed request is forgotten so the next load retries it.
50
- const loads = new Map<CatalogEngine, Promise<CatalogModel[]>>();
57
+ const loads = new Map<CatalogEngine, Promise<LoadedCatalog>>();
51
58
 
52
- export function loadModelCatalog(engine: CatalogEngine): Promise<CatalogModel[]> {
59
+ function loadCatalogEntry(engine: CatalogEngine): Promise<LoadedCatalog> {
53
60
  let load = loads.get(engine);
54
61
  if (!load) {
55
62
  const id = AI_PROVIDER_ID[engine];
56
- const failed = () => {
63
+ const failed = (): LoadedCatalog => {
57
64
  loads.delete(engine);
58
- return FALLBACK_MODELS[engine];
65
+ return { models: FALLBACK_MODELS[engine] };
59
66
  };
60
67
  load = fetch(`/api/ai/capabilities?activate=${encodeURIComponent(id)}`)
61
68
  .then((res) => (res.ok ? res.json() : null))
62
- .then((data) => {
69
+ .then((data): LoadedCatalog => {
63
70
  const provider = data?.providers?.find((p: { id?: string; name?: string }) => p.id === id || p.name === id);
64
71
  const models = provider?.models;
65
72
  if (!Array.isArray(models) || models.length === 0) return failed();
@@ -67,7 +74,13 @@ export function loadModelCatalog(engine: CatalogEngine): Promise<CatalogModel[]>
67
74
  // failed; show it, but forget it so the next load retries (the
68
75
  // server's own cooldown bounds how often that spawns the CLI).
69
76
  if (provider.modelsSource === 'fallback') loads.delete(engine);
70
- return models as CatalogModel[];
77
+ return {
78
+ models: models as CatalogModel[],
79
+ ...(provider.modelsSource === 'fallback' || provider.modelsSource === 'discovered'
80
+ ? { modelsSource: provider.modelsSource as ModelsSource }
81
+ : {}),
82
+ ...(typeof provider.toolVersion === 'string' && provider.toolVersion ? { toolVersion: provider.toolVersion } : {}),
83
+ };
71
84
  })
72
85
  .catch(failed);
73
86
  loads.set(engine, load);
@@ -75,6 +88,10 @@ export function loadModelCatalog(engine: CatalogEngine): Promise<CatalogModel[]>
75
88
  return load;
76
89
  }
77
90
 
91
+ export function loadModelCatalog(engine: CatalogEngine): Promise<CatalogModel[]> {
92
+ return loadCatalogEntry(engine).then((entry) => entry.models);
93
+ }
94
+
78
95
  /** Test seam: forget cached catalogs. */
79
96
  export function __resetModelCatalogsForTests(): void {
80
97
  loads.clear();
@@ -85,7 +102,7 @@ export function useModelCatalogs(capabilities: AgentCapabilities | null): ModelC
85
102
  capabilities?.providers.some((p) => p.id === id && p.available) ?? false;
86
103
  const claudeOn = available('claude');
87
104
  const codexOn = available('codex');
88
- const [loaded, setLoaded] = useState<Partial<Record<CatalogEngine, CatalogModel[]>>>({});
105
+ const [loaded, setLoaded] = useState<Partial<Record<CatalogEngine, LoadedCatalog>>>({});
89
106
  const mounted = useRef(true);
90
107
  useEffect(() => {
91
108
  mounted.current = true;
@@ -98,8 +115,8 @@ export function useModelCatalogs(capabilities: AgentCapabilities | null): ModelC
98
115
  (engine: string | null | undefined) => {
99
116
  if (!isCatalogEngine(engine)) return;
100
117
  if (!(engine === 'claude' ? claudeOn : codexOn)) return;
101
- void loadModelCatalog(engine).then((models) => {
102
- if (mounted.current) setLoaded((prev) => (prev[engine] === models ? prev : { ...prev, [engine]: models }));
118
+ void loadCatalogEntry(engine).then((entry) => {
119
+ if (mounted.current) setLoaded((prev) => (prev[engine] === entry ? prev : { ...prev, [engine]: entry }));
103
120
  });
104
121
  },
105
122
  [claudeOn, codexOn],
@@ -107,8 +124,8 @@ export function useModelCatalogs(capabilities: AgentCapabilities | null): ModelC
107
124
 
108
125
  return useMemo(
109
126
  () => ({
110
- claude: { models: loaded.claude ?? FALLBACK_MODELS.claude, settled: !!loaded.claude },
111
- codex: { models: loaded.codex ?? FALLBACK_MODELS.codex, settled: !!loaded.codex },
127
+ claude: loaded.claude ? { ...loaded.claude, settled: true } : { models: FALLBACK_MODELS.claude, settled: false },
128
+ codex: loaded.codex ? { ...loaded.codex, settled: true } : { models: FALLBACK_MODELS.codex, settled: false },
112
129
  load,
113
130
  }),
114
131
  [loaded, load],
@@ -137,6 +137,10 @@ export function usePinpoint({
137
137
 
138
138
  const handleClick = (e: MouseEvent) => {
139
139
  const target = e.target as HTMLElement;
140
+ // A `:::question` card's form controls are the answer UI, never a pin
141
+ // target: a click on a radio, a checkbox or a text box answers.
142
+ const inQuestion = !!target.closest?.('fieldset.question-block');
143
+ if (inQuestion && target.closest('input, textarea, select')) return;
140
144
  const resolved = resolveSemanticTargetAtPoint(
141
145
  buildSemanticTargetGraph(container),
142
146
  target,
@@ -144,6 +148,12 @@ export function usePinpoint({
144
148
  );
145
149
  if (!resolved) return;
146
150
 
151
+ // Pinning a choice's text must not also pick the choice: the row is a
152
+ // <label>, which would forward this click to its radio or checkbox.
153
+ if (inQuestion && target.closest('[data-question-option]')) {
154
+ e.preventDefault();
155
+ }
156
+
147
157
  // Prevent link navigation in pinpoint mode
148
158
  const link = (target.closest('a') as HTMLAnchorElement | null);
149
159
  if (link && container.contains(link)) {
@@ -0,0 +1,157 @@
1
+ import { useEffect } from 'react';
2
+ import { hasBlockingOverlay } from './useVimDocumentFocus';
3
+ import { isDocumentScrollViewport } from './useScrollViewport';
4
+
5
+ /**
6
+ * Scroll keys with no browser target (#1647).
7
+ *
8
+ * The desktop editor scrolls inside an element (`<main>`), not the window. On
9
+ * load nothing is focused and the user has not clicked anywhere, so the
10
+ * browser's keyboard-scroll starting point is the root scroller, which cannot
11
+ * scroll: ArrowDown, PageDown and Space do nothing until the reader clicks into
12
+ * the document. The same dead state returns after the node the user last
13
+ * clicked is removed (a dismissed first-run dialog) or when that click landed
14
+ * on chrome with nothing scrollable above it (the header).
15
+ *
16
+ * This hook fills exactly that gap and nothing else. It never moves focus, so
17
+ * dialogs, popovers, screen readers and `:focus-visible` are untouched. It
18
+ * only acts when all of these hold:
19
+ * - focus is neutral (body), so no control, input, editor or iframe owns
20
+ * the key;
21
+ * - no earlier listener consumed the key (`defaultPrevented`);
22
+ * - no modifier except Shift+Space (page up), and not mid-composition;
23
+ * - no dialog, modal overlay, popover, menu or listbox is open;
24
+ * - the browser has no scroll target of its own: the last pointerdown is
25
+ * absent, detached, or has no vertically scrollable ancestor. When it has
26
+ * one (a click in the document, the annotations panel, the sidebar), the
27
+ * native key scroll already works and is left alone.
28
+ */
29
+
30
+ const LINE_STEP_PX = 40;
31
+ /**
32
+ * Page step as Chromium and WebKit compute it: the larger of 87.5% of the
33
+ * viewport (kMinFractionToStepWhenPaging) and the viewport minus a 40px
34
+ * overlap (kMaxOverlapBetweenPages). On a normal window the overlap rule wins,
35
+ * so a routed PageDown matches the native one taken after a click.
36
+ */
37
+ const PAGE_FRACTION = 0.875;
38
+ const PAGE_OVERLAP_PX = 40;
39
+
40
+ const OPEN_POPUP_SELECTOR = '[role="menu"], [role="listbox"]';
41
+
42
+ export type ScrollKeyAction =
43
+ | { readonly kind: 'line'; readonly direction: 1 | -1 }
44
+ | { readonly kind: 'page'; readonly direction: 1 | -1 }
45
+ | { readonly kind: 'edge'; readonly edge: 'start' | 'end' };
46
+
47
+ /** The keydown fields the mapping reads (pure, so it runs without a DOM). */
48
+ export type ScrollKeyInput = Pick<
49
+ KeyboardEvent,
50
+ 'key' | 'isComposing' | 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'
51
+ >;
52
+
53
+ /** Map a keydown to the scroll it asks for, or null when it is not a plain scroll key. */
54
+ export function scrollKeyAction(event: ScrollKeyInput): ScrollKeyAction | null {
55
+ if (event.isComposing || event.ctrlKey || event.metaKey || event.altKey) return null;
56
+ const isSpace = event.key === ' ' || event.key === 'Spacebar';
57
+ if (isSpace) return { kind: 'page', direction: event.shiftKey ? -1 : 1 };
58
+ if (event.shiftKey) return null;
59
+ switch (event.key) {
60
+ case 'ArrowDown': return { kind: 'line', direction: 1 };
61
+ case 'ArrowUp': return { kind: 'line', direction: -1 };
62
+ case 'PageDown': return { kind: 'page', direction: 1 };
63
+ case 'PageUp': return { kind: 'page', direction: -1 };
64
+ case 'Home': return { kind: 'edge', edge: 'start' };
65
+ case 'End': return { kind: 'edge', edge: 'end' };
66
+ default: return null;
67
+ }
68
+ }
69
+
70
+ function canScrollVertically(element: Element): boolean {
71
+ if (!(element instanceof HTMLElement)) return false;
72
+ if (element.scrollHeight <= element.clientHeight) return false;
73
+ const overflowY = getComputedStyle(element).overflowY;
74
+ return overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay';
75
+ }
76
+
77
+ /**
78
+ * Whether the browser already has a keyboard-scroll target: the last clicked
79
+ * node is still in the page and sits inside a vertically scrollable element.
80
+ */
81
+ export function browserHasScrollTarget(lastPointerTarget: Node | null): boolean {
82
+ if (!lastPointerTarget || !lastPointerTarget.isConnected) return false;
83
+ const root = lastPointerTarget.ownerDocument?.documentElement ?? null;
84
+ let node: Node | null = lastPointerTarget;
85
+ while (node && node !== root) {
86
+ if (node instanceof Element && canScrollVertically(node)) return true;
87
+ const parent: Node | null = node.parentNode;
88
+ node = parent instanceof ShadowRoot ? parent.host : parent;
89
+ }
90
+ return false;
91
+ }
92
+
93
+ function focusIsNeutral(event: KeyboardEvent): boolean {
94
+ const doc = document;
95
+ const active = doc.activeElement;
96
+ if (active !== null && active !== doc.body && active !== doc.documentElement) return false;
97
+ const origin = event.composedPath?.()[0] ?? event.target;
98
+ return origin === doc.body || origin === doc.documentElement || origin === doc || origin === window;
99
+ }
100
+
101
+ function prefersReducedMotion(): boolean {
102
+ return typeof window.matchMedia === 'function'
103
+ && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
104
+ }
105
+
106
+ function applyScroll(viewport: HTMLElement, action: ScrollKeyAction): void {
107
+ if (action.kind === 'line') {
108
+ viewport.scrollBy({ top: action.direction * LINE_STEP_PX, behavior: 'auto' });
109
+ return;
110
+ }
111
+ const behavior: ScrollBehavior = prefersReducedMotion() ? 'auto' : 'smooth';
112
+ if (action.kind === 'edge') {
113
+ viewport.scrollTo({ top: action.edge === 'start' ? 0 : viewport.scrollHeight, behavior });
114
+ return;
115
+ }
116
+ const height = viewport.clientHeight;
117
+ const page = Math.max(1, Math.round(Math.max(height * PAGE_FRACTION, height - PAGE_OVERLAP_PX)));
118
+ viewport.scrollBy({ top: action.direction * page, behavior });
119
+ }
120
+
121
+ export interface UseScrollKeyRoutingOptions {
122
+ /** The element that scrolls the document. Page-scrolling viewports are ignored. */
123
+ readonly viewport: HTMLElement | null;
124
+ /** False where another owner has the keys (vim, iframe surfaces). */
125
+ readonly enabled: boolean;
126
+ }
127
+
128
+ /** Route unowned scroll keys to the document viewport. See the module comment. */
129
+ export function useScrollKeyRouting({ viewport, enabled }: UseScrollKeyRoutingOptions): void {
130
+ useEffect(() => {
131
+ if (!enabled || !viewport || isDocumentScrollViewport(viewport)) return;
132
+
133
+ let lastPointerTarget: Node | null = null;
134
+ const handlePointerDown = (event: PointerEvent) => {
135
+ const origin = event.composedPath?.()[0] ?? event.target;
136
+ lastPointerTarget = origin instanceof Node ? origin : null;
137
+ };
138
+
139
+ const handleKeyDown = (event: KeyboardEvent) => {
140
+ if (event.defaultPrevented) return;
141
+ const action = scrollKeyAction(event);
142
+ if (!action) return;
143
+ if (!viewport.isConnected || !focusIsNeutral(event)) return;
144
+ if (hasBlockingOverlay() || document.querySelector(OPEN_POPUP_SELECTOR)) return;
145
+ if (browserHasScrollTarget(lastPointerTarget)) return;
146
+ event.preventDefault();
147
+ applyScroll(viewport, action);
148
+ };
149
+
150
+ window.addEventListener('pointerdown', handlePointerDown, true);
151
+ window.addEventListener('keydown', handleKeyDown);
152
+ return () => {
153
+ window.removeEventListener('pointerdown', handlePointerDown, true);
154
+ window.removeEventListener('keydown', handleKeyDown);
155
+ };
156
+ }, [enabled, viewport]);
157
+ }
@@ -13,6 +13,13 @@ export interface UndoHistoryApi<TAction> {
13
13
  readonly canUndo: boolean;
14
14
  readonly canRedo: boolean;
15
15
  record: (action: TAction) => void;
16
+ /**
17
+ * Coalesce into the newest undo entry: when it is still `expected` (the
18
+ * same object), replace it with `next`, or drop it when `next` is null.
19
+ * Returns false, changing nothing, when anything was recorded, undone or
20
+ * cleared since. Used to fold a typing burst into one entry.
21
+ */
22
+ replaceLast: (expected: TAction, next: TAction | null) => boolean;
16
23
  undo: () => boolean;
17
24
  redo: () => boolean;
18
25
  clear: () => void;
@@ -57,6 +64,16 @@ export function useUndoHistory<TAction>({
57
64
  const history = historyRef.current;
58
65
  history.state = recordUndoAction(history.state, action, optionsRef.current.capacity);
59
66
  },
67
+ replaceLast(expected, next) {
68
+ const history = historyRef.current;
69
+ const { past, future } = history.state;
70
+ if (past.length === 0 || past[past.length - 1] !== expected || future.length > 0) return false;
71
+ history.state = {
72
+ past: next === null ? past.slice(0, -1) : [...past.slice(0, -1), next],
73
+ future,
74
+ };
75
+ return true;
76
+ },
60
77
  undo() {
61
78
  const history = historyRef.current;
62
79
  const step = takeUndoAction(history.state);
@@ -38,7 +38,8 @@ function pageFocusIsNeutral(): boolean {
38
38
  return document.activeElement === document.body || document.activeElement === null;
39
39
  }
40
40
 
41
- function hasBlockingOverlay(): boolean {
41
+ /** True while a dialog, modal overlay, or dismissable popover owns the page. */
42
+ export function hasBlockingOverlay(): boolean {
42
43
  return document.querySelector(BLOCKING_OVERLAY_SELECTOR) !== null;
43
44
  }
44
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/ui",
3
- "version": "0.46.1",
3
+ "version": "0.49.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./components/*": "./components/*.tsx",
@@ -75,9 +75,9 @@
75
75
  "@lezer/common": "^1.5.2",
76
76
  "@lezer/highlight": "^1.2.3",
77
77
  "@pierre/diffs": "1.3.6",
78
- "@plannotator/atomic-editor": "^0.8.0",
79
- "@plannotator/core": "0.25.6",
80
- "@plannotator/markdown-editor": "^0.4.0",
78
+ "@plannotator/atomic-editor": "^0.9.0",
79
+ "@plannotator/core": "0.25.9",
80
+ "@plannotator/markdown-editor": "^0.5.0",
81
81
  "@plannotator/web-highlighter": "^0.8.1",
82
82
  "@tanstack/react-table": "^8.21.3",
83
83
  "@viz-js/viz": "3.30.0",