@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
package/theme.css CHANGED
@@ -24,6 +24,7 @@
24
24
  @import "./themes/solar-dusk.css";
25
25
  @import "./themes/terminal.css";
26
26
  @import "./themes/tokyo-night.css";
27
+ @import "./themes/tokyo-night-moon.css";
27
28
  @import "./themes/tinacious.css";
28
29
  @import "./themes/cursor.css";
29
30
  @import "./themes/cursor-hc.css";
@@ -0,0 +1,34 @@
1
+ /* Tokyo Night Moon — folke/tokyonight.nvim "moon" (dark only) */
2
+ .theme-tokyo-night-moon,
3
+ .theme-tokyo-night-moon.light {
4
+ --background: #222436;
5
+ --foreground: #c8d3f5;
6
+ --card: #1e2030;
7
+ --card-foreground: #c8d3f5;
8
+ --popover: #1e2030;
9
+ --popover-foreground: #c8d3f5;
10
+ --primary: #82aaff;
11
+ --primary-foreground: #1e2030;
12
+ --secondary: #444a73;
13
+ --secondary-foreground: #c8d3f5;
14
+ --muted: #2f334d;
15
+ --muted-foreground: #828bb8;
16
+ --accent: #86e1fc;
17
+ --accent-foreground: #1e2030;
18
+ --destructive: #ff757f;
19
+ --destructive-foreground: #1e2030;
20
+ --border: #444a73;
21
+ --input: #2f334d;
22
+ --ring: #82aaff;
23
+ --success: #c3e88d;
24
+ --success-foreground: #1e2030;
25
+ --warning: #ffc777;
26
+ --warning-foreground: #1e2030;
27
+
28
+ --font-sans: system-ui, sans-serif;
29
+ --font-mono: ui-monospace, monospace;
30
+ --radius: 0.625rem;
31
+
32
+ --code-bg: #191b29;
33
+ --focus-highlight: #86e1fc;
34
+ }
@@ -13,8 +13,14 @@ export const CHAT_SANITIZE_CONFIG = {
13
13
  'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'a', 'blockquote',
14
14
  'table', 'thead', 'tbody', 'tr', 'th', 'td',
15
15
  ],
16
- // `align` is what marked writes on th/td for aligned GFM columns.
17
- ALLOWED_ATTR: ['href', 'rel', 'class', 'align'],
16
+ // `align` is what marked writes on th/td for aligned GFM columns. No `class`
17
+ // (an answer's raw HTML could style itself with the app's utility classes;
18
+ // nothing in `.ai-markdown` reads one) and no `rel` (marked never writes it
19
+ // and `target` is not allowed).
20
+ ALLOWED_ATTR: ['href', 'align'],
21
+ // DOMPurify keeps data-* and aria-* unless told not to.
22
+ ALLOW_DATA_ATTR: false,
23
+ ALLOW_ARIA_ATTR: false,
18
24
  };
19
25
 
20
26
  /** The markdown step of {@link renderChatMarkdown}, before sanitizing. */
@@ -22,16 +28,31 @@ export function chatMarkdownToHtml(text: string): string {
22
28
  return marked.parse(text, { async: false, breaks: true }) as string;
23
29
  }
24
30
 
31
+ /**
32
+ * Never throws: deeply nested input can overflow the stack in marked, and a
33
+ * throw inside a render would unmount the whole page. On any failure the
34
+ * answer is shown as plain text, line breaks kept, as a React text child
35
+ * (React escapes it).
36
+ */
25
37
  export function renderChatMarkdown(text: string): ReactNode {
26
- const clean = DOMPurify.sanitize(chatMarkdownToHtml(text), {
27
- ALLOWED_TAGS: [...CHAT_SANITIZE_CONFIG.ALLOWED_TAGS],
28
- ALLOWED_ATTR: [...CHAT_SANITIZE_CONFIG.ALLOWED_ATTR],
29
- });
30
-
31
- return createElement('div', {
32
- className: 'ai-markdown',
33
- dangerouslySetInnerHTML: { __html: clean },
34
- });
38
+ try {
39
+ const clean = DOMPurify.sanitize(chatMarkdownToHtml(text), {
40
+ ALLOWED_TAGS: [...CHAT_SANITIZE_CONFIG.ALLOWED_TAGS],
41
+ ALLOWED_ATTR: [...CHAT_SANITIZE_CONFIG.ALLOWED_ATTR],
42
+ ALLOW_DATA_ATTR: CHAT_SANITIZE_CONFIG.ALLOW_DATA_ATTR,
43
+ ALLOW_ARIA_ATTR: CHAT_SANITIZE_CONFIG.ALLOW_ARIA_ATTR,
44
+ });
45
+ return createElement('div', {
46
+ className: 'ai-markdown',
47
+ dangerouslySetInnerHTML: { __html: clean },
48
+ });
49
+ } catch {
50
+ return createElement(
51
+ 'div',
52
+ { className: 'ai-markdown', style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } },
53
+ String(text),
54
+ );
55
+ }
35
56
  }
36
57
 
37
58
  export function formatRelativeTime(ts: number): string {
@@ -9,14 +9,24 @@
9
9
  * activation line is 80 px below the top (it was 52), plan review's
10
10
  * `TableOfContents` scroll offset, so a pick lands where plan review's does.
11
11
  *
12
- * Plan review's own list keeps `hooks/useActiveSection` (an
13
- * IntersectionObserver band). One mechanism per surface: the rail, and a
12
+ * Plan review's own list uses `hooks/useActiveSection` (live heading
13
+ * positions at the same activation line). One mechanism per surface: the rail, and a
14
14
  * Contents list on the rail's surface (`DocumentContents`), use this one.
15
15
  */
16
16
 
17
17
  /** How far below the scroll element's top a heading becomes the current one. */
18
18
  export const CONTENTS_ACTIVATION_OFFSET = 80;
19
19
 
20
+ /**
21
+ * How far below the activation line a heading still counts as reached. A pick
22
+ * scrolls to the heading's top less the offset, and that top is often
23
+ * fractional (1522.22 px); the browser snaps the scroll offset to a whole
24
+ * device pixel (1442, not 1442.22), so the picked heading can end up a fraction
25
+ * of a pixel below the line and the heading above it would be marked. One
26
+ * pixel covers the snap, as the document's end does below.
27
+ */
28
+ export const CONTENTS_SNAP_TOLERANCE = 1;
29
+
20
30
  /** A heading drawn in the scroll element, with its top inside the scrolled content. */
21
31
  export interface HeadingBinding {
22
32
  /** The heading's index in the caller's own list. */
@@ -75,9 +85,9 @@ export function currentBinding(
75
85
  }
76
86
 
77
87
  /**
78
- * The heading being read: the last one at or above the activation line, or
79
- * the last heading once the document is scrolled to its end. `null` with no
80
- * bindings.
88
+ * The heading being read: the last one at or above the activation line (or
89
+ * within `CONTENTS_SNAP_TOLERANCE` below it), or the last heading once the
90
+ * document is scrolled to its end. `null` with no bindings.
81
91
  */
82
92
  export function activeHeadingBinding(
83
93
  bindings: readonly HeadingBinding[],
@@ -88,5 +98,8 @@ export function activeHeadingBinding(
88
98
  const atDocumentEnd = maxScrollTop > 0 && viewport.scrollTop >= maxScrollTop - 1;
89
99
  return atDocumentEnd
90
100
  ? bindings.at(-1)!
91
- : currentBinding(bindings, viewport.scrollTop + CONTENTS_ACTIVATION_OFFSET);
101
+ : currentBinding(
102
+ bindings,
103
+ viewport.scrollTop + CONTENTS_ACTIVATION_OFFSET + CONTENTS_SNAP_TOLERANCE,
104
+ );
92
105
  }
@@ -50,6 +50,7 @@ export const SHIKI_THEME_MAP: Record<string, { dark: string | null; light: strin
50
50
  'solarized': { dark: 'solarized-dark', light: 'solarized-light' },
51
51
  'synthwave-84': { dark: 'synthwave-84', light: null },
52
52
  'tokyo-night': { dark: 'tokyo-night', light: null },
53
+ 'tokyo-night-moon': { dark: 'tokyo-night', light: null },
53
54
  'vesper': { dark: 'vesper', light: null },
54
55
  'vitesse': { dark: 'vitesse-dark', light: 'vitesse-light' },
55
56
  'vitesse-black': { dark: 'vitesse-black', light: null },
@@ -544,6 +544,17 @@ export const BUILT_IN_THEMES: ThemeInfo[] = [
544
544
  light: { primary: '#2e7de9', secondary: '#a1a6c5', accent: '#007197', background: '#e1e2e7', foreground: '#3760bf' },
545
545
  },
546
546
  },
547
+ {
548
+ id: 'tokyo-night-moon',
549
+ name: 'Tokyo Night Moon',
550
+ builtIn: true,
551
+ modeSupport: 'dark-only',
552
+ syntaxHighlighting: true,
553
+ colors: {
554
+ dark: { primary: '#82aaff', secondary: '#444a73', accent: '#86e1fc', background: '#222436', foreground: '#c8d3f5' },
555
+ light: { primary: '#82aaff', secondary: '#444a73', accent: '#86e1fc', background: '#222436', foreground: '#c8d3f5' },
556
+ },
557
+ },
547
558
  {
548
559
  id: 'vesper',
549
560
  name: 'Vesper',