@plannotator/ui 0.53.1 → 0.54.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 (135) hide show
  1. package/HANDOFF.md +217 -11
  2. package/README.md +65 -10
  3. package/components/AgentToolAnnouncementDialog.tsx +1 -1
  4. package/components/AgentsTab.tsx +1 -1
  5. package/components/AnnotationPanel.tsx +142 -24
  6. package/components/AnnotationSidebar.tsx +1 -1
  7. package/components/AnnotationToolbar.tsx +64 -9
  8. package/components/ApproveDropdown.tsx +7 -9
  9. package/components/AskSessionAnnouncementDialog.tsx +1 -1
  10. package/components/AttachmentsButton.tsx +1 -1
  11. package/components/BlockRenderer.tsx +19 -5
  12. package/components/CodeFilePopout.tsx +139 -4
  13. package/components/CommentPopover.tsx +44 -8
  14. package/components/DecisionControl.tsx +31 -5
  15. package/components/DiagramBlock.tsx +17 -12
  16. package/components/EditorAnnotationCard.tsx +1 -1
  17. package/components/ExportModal.tsx +77 -7
  18. package/components/HostTitle.tsx +0 -4
  19. package/components/ImageAnnotator/Toolbar.tsx +25 -1
  20. package/components/ImageAnnotator/index.tsx +24 -4
  21. package/components/ImageLightbox.tsx +658 -120
  22. package/components/ImagePeek.tsx +38 -4
  23. package/components/ImageRegions.tsx +369 -0
  24. package/components/ImageThumbnail.tsx +1 -1
  25. package/components/ImageZoomStage.tsx +796 -0
  26. package/components/InboxAnnouncementDialog.tsx +202 -0
  27. package/components/InlineMarkdown.tsx +152 -24
  28. package/components/KeyboardShortcuts.tsx +23 -0
  29. package/components/LazyBlockBoundary.tsx +100 -0
  30. package/components/ListMarker.tsx +3 -3
  31. package/components/LookAndFeelAnnouncementDialog.tsx +12 -2
  32. package/components/MenuVersionSection.tsx +31 -9
  33. package/components/PlanDecisionMenuExtras.tsx +129 -0
  34. package/components/PlanHeaderMenu.tsx +15 -1
  35. package/components/ProviderIcons.tsx +8 -0
  36. package/components/RefHoverCard.tsx +913 -0
  37. package/components/SendFailureToast.tsx +126 -0
  38. package/components/Settings.tsx +617 -126
  39. package/components/TerminalToolsAnnouncementDialog.tsx +1 -1
  40. package/components/ThemeProvider.tsx +14 -1
  41. package/components/ThemeTab.tsx +16 -24
  42. package/components/Tooltip.tsx +49 -18
  43. package/components/Viewer.tsx +280 -41
  44. package/components/ai/DocumentAIChatPanel.tsx +29 -3
  45. package/components/ai/NewChatButton.tsx +74 -0
  46. package/components/blocks/CodeBlock.tsx +1 -1
  47. package/components/blocks/HtmlBlock.tsx +7 -1
  48. package/components/blocks/QuestionBlock.tsx +522 -62
  49. package/components/diagram/DiagramComposer.tsx +5 -1
  50. package/components/diagram/DiagramPending.tsx +12 -2
  51. package/components/diagram/DiagramViewer.tsx +19 -3
  52. package/components/html-embed/HtmlEmbedBlock.tsx +374 -0
  53. package/components/html-embed/SvgFenceBlock.tsx +46 -0
  54. package/components/html-embed/index.ts +3 -0
  55. package/components/html-embed/languages.ts +15 -0
  56. package/components/html-embed/loadGate.ts +107 -0
  57. package/components/html-viewer/HtmlViewer.tsx +47 -6
  58. package/components/html-viewer/bridge-script.asset.js +173 -1
  59. package/components/html-viewer/bridge-script.ts +173 -1
  60. package/components/html-viewer/useHtmlAnnotation.ts +125 -0
  61. package/components/imageZoom.ts +265 -0
  62. package/components/plan-diff/PlanCleanDiffView.tsx +1 -1
  63. package/components/plan-diff/PlanDiffViewer.tsx +13 -2
  64. package/components/plan-diff/PlanRawDiffView.tsx +1 -1
  65. package/components/sidebar/FileBrowser.tsx +16 -2
  66. package/components/sidebar/SidebarTabs.tsx +3 -0
  67. package/config/index.ts +3 -0
  68. package/config/reviewView.ts +41 -1
  69. package/config/settings.ts +155 -4
  70. package/configure.ts +28 -0
  71. package/hooks/pfm/useCodeFilePopout.ts +7 -1
  72. package/hooks/useAIChat.ts +72 -25
  73. package/hooks/useAnnotationDraft.ts +71 -6
  74. package/hooks/useAnnotationHighlighter.ts +57 -3
  75. package/hooks/useCodeAnnotationDraft.ts +113 -17
  76. package/hooks/useCookieHostSetting.ts +96 -0
  77. package/hooks/useDocumentFont.ts +17 -0
  78. package/hooks/useDocumentTextSize.ts +20 -0
  79. package/hooks/useFileBrowser.ts +10 -1
  80. package/hooks/useLinkedDoc.ts +226 -13
  81. package/hooks/usePackageChangesSetting.ts +90 -0
  82. package/hooks/usePlanLookDialog.ts +67 -0
  83. package/hooks/useSharing.ts +11 -7
  84. package/hooks/useThemePaletteHalves.ts +37 -0
  85. package/hooks/useUpdateCheck.ts +22 -4
  86. package/hooks/useUpdateToast.ts +1 -1
  87. package/package.json +23 -22
  88. package/shortcuts/code-review/annotationToolbar.shortcuts.ts +1 -0
  89. package/shortcuts/code-review/explorer.shortcuts.ts +82 -0
  90. package/shortcuts/core.ts +63 -4
  91. package/shortcuts/index.ts +1 -0
  92. package/shortcuts/plan-review/annotationPanel.shortcuts.ts +1 -1
  93. package/shortcuts/plan-review/commentPopover.shortcuts.ts +1 -0
  94. package/shortcuts/runtime.ts +4 -3
  95. package/styles.css +1 -1
  96. package/theme.css +179 -1
  97. package/themes/plannotator-graphite.css +66 -0
  98. package/types.ts +26 -9
  99. package/utils/agentSwitch.ts +41 -20
  100. package/utils/agentToolSetting.ts +1 -1
  101. package/utils/aiChatFormat.ts +23 -7
  102. package/utils/annotationOwnsHighlight.ts +4 -2
  103. package/utils/asciidocParser.ts +570 -0
  104. package/utils/autoUpdateNotice.ts +24 -0
  105. package/utils/belowTitleSlot.ts +174 -0
  106. package/utils/codeLineScroll.ts +100 -0
  107. package/utils/commentSubmitKey.ts +48 -0
  108. package/utils/cssFontFamily.ts +65 -0
  109. package/utils/decisionSpec.ts +190 -2
  110. package/utils/defaultNotesApp.ts +1 -1
  111. package/utils/diffFonts.ts +4 -1
  112. package/utils/documentFont.ts +68 -0
  113. package/utils/documentTextSize.ts +54 -0
  114. package/utils/fontDetect.ts +63 -0
  115. package/utils/inboxAnnouncement.ts +78 -0
  116. package/utils/lookAndFeelAnnouncement.ts +82 -5
  117. package/utils/markdownExtensions.ts +2 -2
  118. package/utils/mermaid.ts +20 -3
  119. package/utils/mermaidTheme.ts +3 -51
  120. package/utils/notion.ts +58 -0
  121. package/utils/parser.ts +28 -1
  122. package/utils/platform.ts +46 -0
  123. package/utils/refCard.ts +82 -0
  124. package/utils/richClipboard.ts +380 -0
  125. package/utils/richClipboardLists.ts +57 -0
  126. package/utils/richCopyPolicy.ts +22 -0
  127. package/utils/scrollAnchor.ts +82 -0
  128. package/utils/sectionRefs.ts +123 -0
  129. package/utils/selectionBoundary.ts +40 -0
  130. package/utils/sendFailure.ts +85 -0
  131. package/utils/sharing.ts +5 -2
  132. package/utils/themeRegistry.ts +10 -0
  133. package/utils/themeTokens.ts +74 -0
  134. package/utils/updateNotice.ts +30 -8
  135. package/utils/vscodePanel.ts +9 -0
@@ -231,7 +231,7 @@ function DemoPlayer({ demo, reducedMotion }: DemoPlayerProps) {
231
231
  data-terminal-tools-playback={playing ? 'pause' : 'play'}
232
232
  className={
233
233
  playing
234
- ? 'absolute bottom-3 left-3 grid size-9 place-items-center rounded-full border border-border/60 bg-background/70 text-foreground opacity-0 outline-none backdrop-blur-sm transition-opacity motion-reduce:transition-none group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-primary'
234
+ ? 'absolute bottom-3 left-3 grid size-9 place-items-center rounded-full border border-border/60 bg-background/70 text-foreground opacity-0 outline-none backdrop-blur-sm transition-opacity motion-reduce:transition-none group-hover:opacity-100 focus-visible:opacity-100 [@media(hover:none)]:opacity-100 focus-visible:ring-2 focus-visible:ring-primary'
235
235
  : 'absolute left-1/2 top-1/2 grid size-16 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border border-border/60 bg-background/80 text-foreground shadow-lg outline-none backdrop-blur-sm transition-transform motion-reduce:transition-none hover:scale-105 focus-visible:ring-2 focus-visible:ring-primary'
236
236
  }
237
237
  >
@@ -18,6 +18,7 @@ import {
18
18
  } from '../utils/themeRegistry';
19
19
  import type { Mode } from './themeModes';
20
20
  import { usePrintMedia } from '../hooks/usePrintMedia';
21
+ import { markRootPlatform } from '../utils/platform';
21
22
 
22
23
  // Kept here because published consumers already import Mode from ThemeProvider.
23
24
  export type { Mode } from './themeModes';
@@ -62,12 +63,22 @@ const ThemeProviderContext = createContext<ThemeProviderState>({
62
63
  manageFavicon: false,
63
64
  });
64
65
 
65
- /** Sync theme classes on <html> without stripping non-theme classes (e.g. transitions-ready). */
66
+ /**
67
+ * Sync theme classes on <html> without stripping non-theme classes (e.g. transitions-ready).
68
+ *
69
+ * A page that sets an inline `color-scheme` on <html> before its scripts run
70
+ * (Plannotator's packed pages do, so the empty page does not paint white under
71
+ * a dark theme) hands it over here: from the first render it follows the mode
72
+ * on screen, so it never goes stale after a theme switch. A host page that sets
73
+ * none keeps none.
74
+ */
66
75
  function applyThemeClasses(themeId: string, resolvedMode: 'dark' | 'light'): void {
67
76
  const el = document.documentElement;
68
77
  const themeClass = `theme-${themeId}`;
69
78
  const wantLight = resolvedMode === 'light';
70
79
 
80
+ if (el.style.colorScheme && el.style.colorScheme !== resolvedMode) el.style.colorScheme = resolvedMode;
81
+
71
82
  if (el.classList.contains(themeClass) && el.classList.contains('light') === wantLight) return;
72
83
 
73
84
  for (const cls of Array.from(el.classList)) {
@@ -223,6 +234,8 @@ export function ThemeProvider({
223
234
  // without this the first frame has no valid --background/--foreground.
224
235
  if (typeof window !== 'undefined') {
225
236
  applyThemeClasses(colorTheme, resolvedMode);
237
+ // theme.css scopes the Linux-only font-feature rule to this class (#1961).
238
+ markRootPlatform();
226
239
  }
227
240
 
228
241
  // Keep class in sync after state changes
@@ -1,7 +1,8 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React from 'react';
2
2
  import { useTheme } from './ThemeProvider';
3
3
  import { THEME_MODES } from './themeModes';
4
- import { themesForHalf, type ThemeHalf } from '../utils/themeRegistry';
4
+ import type { ThemeHalf, ThemeInfo } from '../utils/themeRegistry';
5
+ import { useThemePaletteHalves } from '../hooks/useThemePaletteHalves';
5
6
  import { configStore } from '../config/configStore';
6
7
  import { useConfigValue } from '../config/useConfig';
7
8
  import { faviconDataUrl, type FaviconStyle } from '@plannotator/core/favicon';
@@ -56,27 +57,19 @@ const FaviconStyleControl: React.FC<{ selected: FaviconStyle }> = ({ selected })
56
57
  </div>
57
58
  );
58
59
 
59
- export const ThemeTab: React.FC<ThemeTabProps> = ({ onPreview, compact }) => {
60
- const {
61
- mode,
62
- setMode,
63
- lightTheme,
64
- darkTheme,
65
- setHalfTheme,
66
- availableThemes,
67
- preferredMode,
68
- manageFavicon,
69
- } = useTheme();
70
- const faviconStyle = useConfigValue('faviconStyle');
60
+ // The picker's state lives in hooks/useThemePaletteHalves; re-exported here so existing imports keep working.
61
+ export { useThemePaletteHalves, type ThemePaletteHalves } from '../hooks/useThemePaletteHalves';
71
62
 
72
- // Which half the grid assigns to. Follows the mode you are actually seeing,
73
- // so opening Settings in dark mode edits the dark half first.
74
- const [half, setHalf] = useState<ThemeHalf>(preferredMode);
75
- useEffect(() => setHalf(preferredMode), [preferredMode]);
63
+ /** The five colors a palette tile shows for one half, in ThemeTab's order. */
64
+ export function paletteSwatchColors(theme: ThemeInfo, half: ThemeHalf): string[] {
65
+ const colors = theme.colors[half];
66
+ return [colors.primary, colors.secondary, colors.accent, colors.background, colors.foreground];
67
+ }
76
68
 
77
- const pair: Record<ThemeHalf, string> = { light: lightTheme, dark: darkTheme };
78
- const themes = themesForHalf(availableThemes, half);
79
- const nameOf = (id: string) => availableThemes.find(theme => theme.id === id)?.name ?? id;
69
+ export const ThemeTab: React.FC<ThemeTabProps> = ({ onPreview, compact }) => {
70
+ const { mode, setMode, manageFavicon } = useTheme();
71
+ const faviconStyle = useConfigValue('faviconStyle');
72
+ const { half, setHalf, pair, themes, nameOf, choose } = useThemePaletteHalves();
80
73
 
81
74
  const summary = (
82
75
  <div className={`flex items-center gap-1.5 text-[11px] text-muted-foreground ${compact ? '' : 'flex-wrap'}`}>
@@ -190,11 +183,10 @@ export const ThemeTab: React.FC<ThemeTabProps> = ({ onPreview, compact }) => {
190
183
  <div className={`grid gap-2 overflow-y-auto pr-1 ${compact ? 'grid-cols-4' : 'grid-cols-3'}`}>
191
184
  {themes.map(theme => {
192
185
  const isSelected = pair[half] === theme.id;
193
- const colors = theme.colors[half];
194
186
  return (
195
187
  <button
196
188
  key={theme.id}
197
- onClick={() => setHalfTheme(half, theme.id)}
189
+ onClick={() => choose(half, theme.id)}
198
190
  className={`relative p-2 rounded-md border text-left transition-colors ${
199
191
  isSelected
200
192
  ? 'border-primary bg-primary/5'
@@ -209,7 +201,7 @@ export const ThemeTab: React.FC<ThemeTabProps> = ({ onPreview, compact }) => {
209
201
  )}
210
202
  {/* Color swatches */}
211
203
  <div className="flex gap-1 mb-1.5">
212
- {[colors.primary, colors.secondary, colors.accent, colors.background, colors.foreground].map((color, i) => (
204
+ {paletteSwatchColors(theme, half).map((color, i) => (
213
205
  <div
214
206
  key={i}
215
207
  className="w-3 h-3 rounded-full border border-border/50"
@@ -1,6 +1,22 @@
1
1
  import React from 'react';
2
2
  import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
3
3
 
4
+ /**
5
+ * One tooltip timing for every surface (owner's design-system ruling q4): open
6
+ * after 150 ms of rest, and open the next one at once for 400 ms after one
7
+ * closes. A host's own `TooltipProvider` props, or a trigger's
8
+ * `delayDuration`, still win.
9
+ */
10
+ export const TOOLTIP_DELAY_MS = 150;
11
+ export const TOOLTIP_SKIP_DELAY_MS = 400;
12
+
13
+ interface TooltipProviderContextValue {
14
+ disableHoverablePopup: boolean;
15
+ }
16
+
17
+ /** Null outside a `TooltipProvider`: a lone Tooltip then sets its own 150 ms (Base UI's own default is 600). */
18
+ const TooltipProviderContext = React.createContext<TooltipProviderContextValue | null>(null);
19
+
4
20
  /**
5
21
  * TooltipProvider keeps the Radix-era prop names (`delayDuration`,
6
22
  * `skipDelayDuration`, `disableHoverableContent`) as its public API and maps
@@ -9,8 +25,6 @@ import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
9
25
  * in Base UI (it moved to per-root `disableHoverablePopup`), so it is carried
10
26
  * via context and applied to every Tooltip root under the provider.
11
27
  */
12
- const DisableHoverablePopupContext = React.createContext(false);
13
-
14
28
  interface TooltipProviderProps {
15
29
  delayDuration?: number;
16
30
  skipDelayDuration?: number;
@@ -23,13 +37,15 @@ export const TooltipProvider: React.FC<TooltipProviderProps> = ({
23
37
  skipDelayDuration,
24
38
  disableHoverableContent = false,
25
39
  children,
26
- }) => (
27
- <BaseTooltip.Provider delay={delayDuration} timeout={skipDelayDuration}>
28
- <DisableHoverablePopupContext.Provider value={disableHoverableContent}>
29
- {children}
30
- </DisableHoverablePopupContext.Provider>
31
- </BaseTooltip.Provider>
32
- );
40
+ }) => {
41
+ // One object per setting, so tooltips behind a memo boundary don't re-render with the provider.
42
+ const value = React.useMemo(() => ({ disableHoverablePopup: disableHoverableContent }), [disableHoverableContent]);
43
+ return (
44
+ <BaseTooltip.Provider delay={delayDuration ?? TOOLTIP_DELAY_MS} timeout={skipDelayDuration ?? TOOLTIP_SKIP_DELAY_MS}>
45
+ <TooltipProviderContext.Provider value={value}>{children}</TooltipProviderContext.Provider>
46
+ </BaseTooltip.Provider>
47
+ );
48
+ };
33
49
 
34
50
  interface TooltipProps {
35
51
  content: React.ReactNode;
@@ -63,6 +79,13 @@ interface TooltipProps {
63
79
  * default, where the Escape that closes a tooltip goes no further.
64
80
  */
65
81
  escapePassesThrough?: boolean;
82
+ /**
83
+ * When true, the tooltip is shown whatever the pointer does: a host shows
84
+ * it while the element that holds the trigger has keyboard focus (the
85
+ * Inbox's row badges). False or unset: hover and focus on the trigger decide,
86
+ * as before.
87
+ */
88
+ forceOpen?: boolean;
66
89
  }
67
90
 
68
91
  export const Tooltip: React.FC<TooltipProps> = ({
@@ -76,25 +99,30 @@ export const Tooltip: React.FC<TooltipProps> = ({
76
99
  shouldOpen,
77
100
  container,
78
101
  escapePassesThrough = false,
102
+ forceOpen,
79
103
  }) => {
80
- const disableHoverablePopup = React.useContext(DisableHoverablePopupContext);
81
- // Controlled only for escapePassesThrough: the Escape close is canceled (so Base UI neither prevents nor stops
82
- // the key) and the tooltip is closed here instead.
104
+ const provider = React.useContext(TooltipProviderContext);
105
+ const disableHoverablePopup = provider?.disableHoverablePopup ?? false;
106
+ // Under a provider, an unset delay takes the provider's; alone, the shared default.
107
+ const triggerDelay = delayDuration ?? (provider ? undefined : TOOLTIP_DELAY_MS);
108
+ // Controlled for escapePassesThrough (the Escape close is canceled, so Base UI neither prevents nor stops the
109
+ // key, and the tooltip is closed here instead) and for forceOpen (shown on top of what hover decides).
83
110
  const [controlledOpen, setControlledOpen] = React.useState(false);
84
- const controlled = escapePassesThrough ? { open: controlledOpen } : {};
111
+ const controlledMode = escapePassesThrough || forceOpen !== undefined;
112
+ const controlled = controlledMode ? { open: controlledOpen || forceOpen === true } : {};
85
113
  return (
86
114
  <BaseTooltip.Root
87
115
  disableHoverablePopup={disableHoverablePopup}
88
116
  {...controlled}
89
117
  onOpenChange={
90
- shouldOpen || escapePassesThrough
118
+ shouldOpen || controlledMode
91
119
  ? (open, details) => {
92
120
  if (open && shouldOpen && !(details.trigger && shouldOpen(details.trigger))) {
93
121
  details.cancel();
94
122
  return;
95
123
  }
96
- if (!escapePassesThrough) return;
97
- if (!open && details.reason === 'escape-key') {
124
+ if (!controlledMode) return;
125
+ if (escapePassesThrough && !open && details.reason === 'escape-key') {
98
126
  details.cancel();
99
127
  details.allowPropagation();
100
128
  }
@@ -103,13 +131,16 @@ export const Tooltip: React.FC<TooltipProps> = ({
103
131
  : undefined
104
132
  }
105
133
  >
106
- <BaseTooltip.Trigger render={children} delay={delayDuration} />
134
+ <BaseTooltip.Trigger render={children} delay={triggerDelay} />
107
135
  <BaseTooltip.Portal container={container ?? undefined}>
108
136
  {/* Above the annotation toolbar (z-[100]), so a tooltip that flips toward it is never tucked under it. */}
109
137
  <BaseTooltip.Positioner side={side} align={align} sideOffset={sideOffset} className="isolate z-[110]">
138
+ {/* A borderless float (owner's design-system ruling q4): the shadow (--float-shadow, theme.css) separates
139
+ it from the page. The 1px border stays, transparent, so forced colors (Windows High Contrast), which
140
+ drop shadows and paint borders in the system colour, still draw its edge. */}
110
141
  <BaseTooltip.Popup
111
142
  data-pn-tooltip=""
112
- className={`z-[110] px-2 py-1 text-xs bg-popover text-popover-foreground border border-border rounded shadow-md origin-[var(--transform-origin)] transition-[opacity,scale] duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 ${
143
+ className={`z-[110] px-2 py-1 text-xs bg-popover text-popover-foreground border border-transparent rounded shadow-[var(--float-shadow)] origin-[var(--transform-origin)] transition-[opacity,scale] duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 ${
113
144
  wide
114
145
  ? 'max-w-[min(260px,calc(100vw-1rem))] leading-snug whitespace-normal [overflow-wrap:anywhere]'
115
146
  : 'whitespace-nowrap'