@plannotator/ui 0.55.0 → 0.56.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 (42) hide show
  1. package/HANDOFF.md +43 -1
  2. package/README.md +81 -4
  3. package/components/AnnotationPanel.tsx +2 -1
  4. package/components/AttachmentsButton.tsx +1 -1
  5. package/components/CodeFilePopout.tsx +6 -4
  6. package/components/ComposerTokenTextarea.tsx +43 -2
  7. package/components/DocBadges.tsx +47 -35
  8. package/components/ImageZoomStage.tsx +32 -4
  9. package/components/Settings.tsx +2 -1
  10. package/components/TableOfContents.tsx +2 -1
  11. package/components/Viewer.tsx +43 -17
  12. package/components/chat/ChatComposer.tsx +192 -0
  13. package/components/chat/ChatFrame.tsx +524 -0
  14. package/components/chat/ChatTranscript.tsx +422 -0
  15. package/components/chat/chatMarkdown.ts +436 -0
  16. package/components/chat/chatRecipes.tsx +39 -0
  17. package/components/chat/chatWindowStore.ts +196 -0
  18. package/components/chat/index.ts +72 -0
  19. package/components/diagram/DiagramCanvas.tsx +7 -0
  20. package/components/diagram/DiagramComposer.tsx +15 -3
  21. package/components/diagram/DiagramViewer.tsx +1 -0
  22. package/components/docBadgesWidth.ts +29 -0
  23. package/components/imageZoom.ts +10 -0
  24. package/components/inbox-thread/FileName.tsx +118 -0
  25. package/components/inbox-thread/GuideCard.tsx +39 -0
  26. package/components/inbox-thread/ReaskNote.tsx +25 -0
  27. package/components/inbox-thread/SkillsLine.tsx +40 -0
  28. package/components/inbox-thread/Tip.tsx +105 -0
  29. package/components/inbox-thread/icons.tsx +262 -0
  30. package/components/inbox-thread/reask.ts +222 -0
  31. package/hooks/useActiveSection.ts +46 -63
  32. package/hooks/useAnnotationHighlighter.ts +40 -1
  33. package/hooks/useFileBrowser.ts +4 -3
  34. package/inbox-thread.css +4542 -0
  35. package/package.json +10 -3
  36. package/styles.css +1 -1
  37. package/theme.css +1 -0
  38. package/themes/tokyo-night-moon.css +34 -0
  39. package/utils/aiChatFormat.ts +32 -11
  40. package/utils/headingScrollSpy.ts +19 -6
  41. package/utils/syntaxTheme.ts +1 -0
  42. package/utils/themeRegistry.ts +11 -0
@@ -421,6 +421,17 @@ const excludedAncestor = (node: Node | null): HTMLElement | null => {
421
421
  return element?.closest<HTMLElement>(ANNOTATION_EXCLUDED_SELECTOR) ?? null;
422
422
  };
423
423
 
424
+ /**
425
+ * The whole range sits inside one `.annotation-exclude` subtree of `container`
426
+ * (an Inbox action item or question card under text comments, a diagram):
427
+ * nothing in it can be annotated, so the selection is left to the person.
428
+ * An excluded element around the container itself does not count.
429
+ */
430
+ export const rangeInsideExcluded = (container: HTMLElement, range: Range): boolean => {
431
+ const excluded = excludedAncestor(range.commonAncestorContainer);
432
+ return excluded !== null && excluded !== container && container.contains(excluded);
433
+ };
434
+
424
435
  /**
425
436
  * Move a range's start off any `.annotation-exclude` subtree it begins inside,
426
437
  * onto the first annotatable text position the range covers.
@@ -1508,6 +1519,9 @@ export function useAnnotationHighlighter({
1508
1519
  trimWhitespaceOnlyBoundaryNodes(args[1] as SelectedNodeLike[])) as SelectedNodesTap);
1509
1520
 
1510
1521
  highlighterRef.current = highlighter;
1522
+ // Whether the library's pointer-end handler is attached (`run()`), and whether this effect was cleaned up.
1523
+ let listening = false;
1524
+ let disposed = false;
1511
1525
 
1512
1526
  // Stored positions can resolve into chrome the reviewer could never have
1513
1527
  // selected; painting never enters such a subtree, so a boundary left there
@@ -1650,6 +1664,25 @@ export function useAnnotationHighlighter({
1650
1664
  // AROUND the document and whose end boundary is an empty <div> — the
1651
1665
  // exact shape that throws — so a containment bail here would hand that
1652
1666
  // selection straight to the serializer.
1667
+ if (rangeInsideExcluded(container, range)) {
1668
+ // Nothing here can be annotated (an Inbox action item or question
1669
+ // card under text comments, a diagram): the selection is the
1670
+ // person's to copy. The library's own pointer-end handler paints
1671
+ // nothing for it and then removes the DOM selection anyway, so it
1672
+ // sits this one event out (a listener removed during dispatch never
1673
+ // runs) and listens again on the next task.
1674
+ pendingRangeRunsRef.current = null;
1675
+ if (listening) {
1676
+ highlighter.stop();
1677
+ listening = false;
1678
+ window.setTimeout(() => {
1679
+ if (disposed) return;
1680
+ highlighter.run();
1681
+ listening = true;
1682
+ }, 0);
1683
+ }
1684
+ return;
1685
+ }
1653
1686
  if (isBlankQuote(selection.toString()) || !isSerializableRange(range)) {
1654
1687
  pendingRangeRunsRef.current = null;
1655
1688
  selection.removeAllRanges();
@@ -1676,7 +1709,10 @@ export function useAnnotationHighlighter({
1676
1709
  // person chooses its Comment (takeSelection); the library painting it at
1677
1710
  // touchend would end the system selection and its edit menu first.
1678
1711
  const isTouchPrimary = window.matchMedia('(pointer: coarse)').matches;
1679
- if (!(isTouchPrimary && hostComposesRef.current)) highlighter.run();
1712
+ if (!(isTouchPrimary && hostComposesRef.current)) {
1713
+ highlighter.run();
1714
+ listening = true;
1715
+ }
1680
1716
 
1681
1717
  const handleMathMouseDown = (event: MouseEvent) => {
1682
1718
  mouseDownMathRef.current = closestMathElement(event.target as Node, containerRef.current);
@@ -1773,6 +1809,8 @@ export function useAnnotationHighlighter({
1773
1809
  if (!sel || sel.isCollapsed || sel.rangeCount === 0) return false;
1774
1810
  if (!containerRef.current?.contains(sel.anchorNode)) return false;
1775
1811
  const range = sel.getRangeAt(0).cloneRange();
1812
+ // Nothing to annotate there: the system selection and its copy menu stay the person's.
1813
+ if (rangeInsideExcluded(containerRef.current, range)) return false;
1776
1814
  // Same fail-closed rule as the pointer-end guard (#881): touch
1777
1815
  // selections settle on block boundaries just as often.
1778
1816
  if (isBlankQuote(sel.toString()) || !isSerializableRange(range)) return false;
@@ -1810,6 +1848,7 @@ export function useAnnotationHighlighter({
1810
1848
  containerRef.current?.removeEventListener('mouseup', handleMathMouseUp, true);
1811
1849
  container.removeEventListener('mouseup', handlePointerEndCapture, true);
1812
1850
  container.removeEventListener('touchend', handlePointerEndCapture, true);
1851
+ disposed = true;
1813
1852
  highlighter.dispose();
1814
1853
  };
1815
1854
  }, [clearPendingSelection, enabled]);
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { useState, useCallback, useEffect, useMemo, useRef } from "react";
11
11
  import type { VaultNode } from "../types";
12
+ import { basenameFromPath } from "@plannotator/core/source-save";
12
13
  import type { WorkspaceStatusPayload } from "@plannotator/core/workspace-status-types";
13
14
 
14
15
  export interface DirState {
@@ -200,7 +201,7 @@ export function useFileBrowser(): UseFileBrowserReturn {
200
201
  }, []);
201
202
 
202
203
  const fetchTree = useCallback(async (dirPath: string, options: { quiet?: boolean } = {}) => {
203
- const name = dirPath.split("/").pop() || dirPath;
204
+ const name = basenameFromPath(dirPath);
204
205
 
205
206
  setDirs((prev) => {
206
207
  const exists = prev.find((d) => d.path === dirPath);
@@ -291,7 +292,7 @@ export function useFileBrowser(): UseFileBrowserReturn {
291
292
  const vaultDirs = prev.filter((d) => d.isVault);
292
293
  const regularDirs = directories.map((path) => ({
293
294
  path,
294
- name: path.split("/").pop() || path,
295
+ name: basenameFromPath(path),
295
296
  tree: [],
296
297
  // Keep initial roots loading until their first snapshot resolves. If
297
298
  // the SSE watcher starts before /api/reference/files finishes,
@@ -313,7 +314,7 @@ export function useFileBrowser(): UseFileBrowserReturn {
313
314
  }, []);
314
315
 
315
316
  const addVaultDir = useCallback(async (vaultPath: string) => {
316
- const name = vaultPath.split("/").pop() || vaultPath;
317
+ const name = basenameFromPath(vaultPath);
317
318
 
318
319
  // Atomically replace any existing vault dirs (handles vault path change without accumulating stale entries)
319
320
  setDirs((prev) => {