@plannotator/ui 0.30.0 → 0.32.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.
- package/README.md +46 -1
- package/components/ActionMenu.tsx +6 -1
- package/components/AgentsTab.tsx +8 -9
- package/components/AnalysisLayerToggle.tsx +48 -0
- package/components/AnnotationPanel.tsx +158 -36
- package/components/AnnotationToolbar.tsx +50 -30
- package/components/AnnotationToolstrip.tsx +9 -0
- package/components/CommentPopover.tsx +238 -46
- package/components/ConfirmDialog.tsx +42 -28
- package/components/GraphvizBlock.tsx +86 -7
- package/components/HtmlSurfaceControls.tsx +170 -0
- package/components/InlineMarkdown.tsx +25 -4
- package/components/KeyboardShortcuts.tsx +9 -0
- package/components/Landing.tsx +1 -1
- package/components/LookAndFeelAnnouncementDialog.tsx +147 -178
- package/components/MarkdownEditor/embedPicker.ts +349 -0
- package/components/MarkdownEditor.tsx +12 -0
- package/components/MermaidBlock.tsx +60 -26
- package/components/ModeToggle.tsx +2 -1
- package/components/PermissionModeSetup.tsx +24 -5
- package/components/PinpointOverlay.tsx +11 -6
- package/components/PlanHeaderMenu.tsx +140 -1
- package/components/SearchableSelect.tsx +2 -0
- package/components/Settings.tsx +175 -10
- package/components/SkillReferenceMenu.tsx +9 -0
- package/components/StickyHeaderLane.tsx +9 -2
- package/components/TableOfContents.tsx +9 -4
- package/components/TextShimmer.tsx +8 -5
- package/components/ThemeProvider.tsx +43 -1
- package/components/ThemeTab.tsx +52 -1
- package/components/Tooltip.tsx +3 -1
- package/components/Viewer.tsx +19 -5
- package/components/VimTargetReticle.tsx +12 -4
- package/components/ai/DocumentAIChatPanel.tsx +1 -0
- package/components/blocks/MathBlock.tsx +26 -14
- package/components/core/button.tsx +14 -6
- package/components/html-viewer/HtmlViewer.tsx +320 -41
- package/components/html-viewer/bridge-script.ts +360 -72
- package/components/html-viewer/composerYield.ts +1 -51
- package/components/html-viewer/hostThreads.ts +37 -0
- package/components/html-viewer/index.ts +9 -0
- package/components/html-viewer/unanchored.ts +47 -0
- package/components/html-viewer/useHtmlAnnotation.ts +240 -61
- package/components/plan-diff/PlanCleanDiffView.tsx +1 -0
- package/components/sidebar/FileBrowser.tsx +17 -5
- package/components/sidebar/SidebarContainer.tsx +124 -28
- package/components/ui/button.tsx +10 -8
- package/components/ui/dialog.tsx +35 -25
- package/config/index.ts +6 -1
- package/config/reviewView.ts +42 -9
- package/config/settings.ts +141 -0
- package/configure.ts +32 -0
- package/hooks/useAIProviderConfig.ts +8 -7
- package/hooks/useActiveSection.ts +6 -4
- package/hooks/useAgentJobs.ts +3 -0
- package/hooks/useAnnotationHighlighter.ts +20 -0
- package/hooks/useHtmlRefresh.ts +149 -0
- package/hooks/useIsMobile.ts +37 -0
- package/hooks/useLinkedDoc.ts +7 -0
- package/hooks/useMathRenderer.ts +30 -0
- package/hooks/useScrollViewport.ts +74 -0
- package/hooks/useSharing.ts +31 -5
- package/hooks/useViewportEnvironment.ts +350 -0
- package/package.json +5 -2
- package/shortcuts/index.ts +3 -0
- package/shortcuts/plan-review/annotationMode.shortcuts.ts +91 -0
- package/shortcuts/plan-review/documentView.shortcuts.ts +26 -0
- package/shortcuts/plan-review/htmlAnnotate.shortcuts.ts +24 -0
- package/styles.css +1 -1
- package/theme.css +229 -0
- package/types.ts +35 -0
- package/utils/annotateAgentTerminal.ts +36 -5
- package/utils/blockTargeting.ts +6 -3
- package/utils/composerYield.ts +45 -0
- package/utils/generateIdentity.ts +64 -14
- package/utils/htmlChrome.ts +20 -16
- package/utils/identity-tater.ts +36 -0
- package/utils/lookAndFeelAnnouncement.ts +12 -8
- package/utils/markdownExtensions.ts +57 -0
- package/utils/math-eager.ts +25 -0
- package/utils/math.ts +146 -0
- package/utils/mermaid-eager.ts +28 -0
- package/utils/mermaid.ts +132 -0
- package/utils/parser.ts +75 -2
- package/utils/quickLabels.ts +13 -0
- package/utils/vimNavigation.ts +4 -1
- package/utils/vimScroll.ts +9 -4
- package/utils/wideMode.ts +20 -0
- package/webmcp/activity.ts +46 -0
- package/webmcp/changes.ts +227 -0
- package/webmcp/index.ts +72 -0
- package/webmcp/modelContext.ts +103 -0
- package/webmcp/nudges.ts +174 -0
- package/webmcp/policy.ts +50 -0
- package/webmcp/preference.ts +50 -0
- package/webmcp/schema.ts +81 -0
- package/webmcp/toolset.ts +337 -0
- package/webmcp/useToolset.ts +74 -0
- package/components/PlanAIAnnouncementDialog.tsx +0 -187
- package/components/VimModeAnnouncementDialog.tsx +0 -557
- package/utils/planAIAnnouncement.ts +0 -17
- package/utils/vimModeAnnouncement.ts +0 -23
|
@@ -2,6 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
|
|
|
2
2
|
import { configStore } from '../config/configStore';
|
|
3
3
|
import { readThemePairCookies, writeThemePairCookies } from '../config/settings';
|
|
4
4
|
import { useConfigValue } from '../config/useConfig';
|
|
5
|
+
import { faviconDataUrl } from '@plannotator/core/favicon';
|
|
5
6
|
import { storage } from '../utils/storage';
|
|
6
7
|
import {
|
|
7
8
|
BUILT_IN_THEMES,
|
|
@@ -36,6 +37,12 @@ type ThemeProviderState = {
|
|
|
36
37
|
darkTheme: string;
|
|
37
38
|
setHalfTheme: (half: ThemeHalf, theme: string) => void;
|
|
38
39
|
availableThemes: ThemeInfo[];
|
|
40
|
+
/**
|
|
41
|
+
* Whether this provider owns the document's favicon. Published so the Settings
|
|
42
|
+
* UI can hide the favicon control on hosts that did not opt in: a knob that
|
|
43
|
+
* changes nothing is worse than no knob.
|
|
44
|
+
*/
|
|
45
|
+
manageFavicon: boolean;
|
|
39
46
|
};
|
|
40
47
|
|
|
41
48
|
const ThemeProviderContext = createContext<ThemeProviderState>({
|
|
@@ -51,6 +58,7 @@ const ThemeProviderContext = createContext<ThemeProviderState>({
|
|
|
51
58
|
darkTheme: 'plannotator',
|
|
52
59
|
setHalfTheme: () => null,
|
|
53
60
|
availableThemes: BUILT_IN_THEMES,
|
|
61
|
+
manageFavicon: false,
|
|
54
62
|
});
|
|
55
63
|
|
|
56
64
|
/** Sync theme classes on <html> without stripping non-theme classes (e.g. transitions-ready). */
|
|
@@ -94,6 +102,18 @@ interface ThemeProviderProps {
|
|
|
94
102
|
* `plannotator-dark-theme`; these props rename only the two legacy values.
|
|
95
103
|
*/
|
|
96
104
|
colorThemeStorageKey?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Opt in to letting this provider own `<link rel="icon">` on the document.
|
|
107
|
+
*
|
|
108
|
+
* OFF by default, and deliberately so: `@plannotator/ui` is installed into
|
|
109
|
+
* host applications with their own branding, and a mounted provider must not
|
|
110
|
+
* silently replace a host page's favicon with Plannotator's. Plannotator's own
|
|
111
|
+
* apps pass `manageFavicon`; hosts opt in only if they want the same feature.
|
|
112
|
+
*
|
|
113
|
+
* The value also gates the Settings favicon control (see ThemeTab), so a host
|
|
114
|
+
* that leaves it off never renders a switch that would do nothing.
|
|
115
|
+
*/
|
|
116
|
+
manageFavicon?: boolean;
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
export function ThemeProvider({
|
|
@@ -102,6 +122,7 @@ export function ThemeProvider({
|
|
|
102
122
|
defaultColorTheme = 'plannotator',
|
|
103
123
|
storageKey = 'plannotator-theme',
|
|
104
124
|
colorThemeStorageKey = 'plannotator-color-theme',
|
|
125
|
+
manageFavicon = false,
|
|
105
126
|
}: ThemeProviderProps) {
|
|
106
127
|
const legacyKeys = useMemo(
|
|
107
128
|
() => ({ mode: storageKey, colorTheme: colorThemeStorageKey }),
|
|
@@ -124,6 +145,26 @@ export function ThemeProvider({
|
|
|
124
145
|
const storePair = useConfigValue('themePair');
|
|
125
146
|
const pair = pendingSeed.current ?? storePair;
|
|
126
147
|
const mode = pair.mode;
|
|
148
|
+
const faviconStyle = useConfigValue('faviconStyle');
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!manageFavicon) return;
|
|
152
|
+
if (typeof document === 'undefined') return;
|
|
153
|
+
let link = document.head.querySelector<HTMLLinkElement>('link[rel="icon"]');
|
|
154
|
+
if (!link) {
|
|
155
|
+
link = document.createElement('link');
|
|
156
|
+
link.rel = 'icon';
|
|
157
|
+
document.head.appendChild(link);
|
|
158
|
+
}
|
|
159
|
+
if (faviconStyle === 'classic') {
|
|
160
|
+
link.type = 'image/svg+xml';
|
|
161
|
+
link.removeAttribute('sizes');
|
|
162
|
+
} else {
|
|
163
|
+
link.type = 'image/png';
|
|
164
|
+
link.setAttribute('sizes', '64x64');
|
|
165
|
+
}
|
|
166
|
+
link.href = faviconDataUrl(faviconStyle);
|
|
167
|
+
}, [faviconStyle, manageFavicon]);
|
|
127
168
|
|
|
128
169
|
// Hand the resolved pair to the store as a SEED, not a user choice: seeding
|
|
129
170
|
// writes memory + cookies only. Routing it through set() would queue a
|
|
@@ -243,7 +284,8 @@ export function ThemeProvider({
|
|
|
243
284
|
darkTheme: pair.dark,
|
|
244
285
|
setHalfTheme,
|
|
245
286
|
availableThemes: BUILT_IN_THEMES,
|
|
246
|
-
|
|
287
|
+
manageFavicon,
|
|
288
|
+
}), [mode, preferredMode, resolvedMode, colorTheme, pair.light, pair.dark, setMode, setColorTheme, setHalfTheme, manageFavicon]);
|
|
247
289
|
|
|
248
290
|
return (
|
|
249
291
|
<ThemeProviderContext.Provider value={value}>
|
package/components/ThemeTab.tsx
CHANGED
|
@@ -2,6 +2,9 @@ import React, { useEffect, useState } from 'react';
|
|
|
2
2
|
import { useTheme } from './ThemeProvider';
|
|
3
3
|
import { THEME_MODES } from './themeModes';
|
|
4
4
|
import { themesForHalf, type ThemeHalf } from '../utils/themeRegistry';
|
|
5
|
+
import { configStore } from '../config/configStore';
|
|
6
|
+
import { useConfigValue } from '../config/useConfig';
|
|
7
|
+
import { faviconDataUrl, type FaviconStyle } from '@plannotator/core/favicon';
|
|
5
8
|
|
|
6
9
|
interface ThemeTabProps {
|
|
7
10
|
onPreview?: () => void;
|
|
@@ -13,12 +16,46 @@ const HALVES: { id: ThemeHalf; label: string }[] = [
|
|
|
13
16
|
{ id: 'dark', label: 'Dark' },
|
|
14
17
|
];
|
|
15
18
|
|
|
19
|
+
const FAVICON_STYLES: { id: FaviconStyle; label: string }[] = [
|
|
20
|
+
{ id: 'totman', label: 'Totman' },
|
|
21
|
+
{ id: 'classic', label: 'Classic P' },
|
|
22
|
+
];
|
|
23
|
+
|
|
16
24
|
const SyntaxLinesIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
17
25
|
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
18
26
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h7" />
|
|
19
27
|
</svg>
|
|
20
28
|
);
|
|
21
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The favicon style choices. Rendered in both layouts, so it lives in one place.
|
|
32
|
+
* The compact layout drops the visible "Favicon" heading the full layout has, so
|
|
33
|
+
* the group carries its own accessible name and two unlabelled image buttons are
|
|
34
|
+
* never all a screen reader gets.
|
|
35
|
+
*/
|
|
36
|
+
const FaviconStyleControl: React.FC<{ selected: FaviconStyle }> = ({ selected }) => (
|
|
37
|
+
<div className="flex gap-1" role="group" aria-label="Favicon style">
|
|
38
|
+
{FAVICON_STYLES.map(({ id, label }) => (
|
|
39
|
+
<button
|
|
40
|
+
key={id}
|
|
41
|
+
type="button"
|
|
42
|
+
aria-pressed={selected === id}
|
|
43
|
+
onClick={() => configStore.set('faviconStyle', id)}
|
|
44
|
+
className={`px-2.5 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
|
45
|
+
selected === id
|
|
46
|
+
? 'bg-primary text-primary-foreground'
|
|
47
|
+
: 'bg-muted text-muted-foreground hover:text-foreground'
|
|
48
|
+
}`}
|
|
49
|
+
>
|
|
50
|
+
<span className="flex items-center gap-1.5">
|
|
51
|
+
<img src={faviconDataUrl(id)} alt="" className="w-5 h-5 rounded-sm shrink-0" />
|
|
52
|
+
{label}
|
|
53
|
+
</span>
|
|
54
|
+
</button>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
|
|
22
59
|
export const ThemeTab: React.FC<ThemeTabProps> = ({ onPreview, compact }) => {
|
|
23
60
|
const {
|
|
24
61
|
mode,
|
|
@@ -28,7 +65,9 @@ export const ThemeTab: React.FC<ThemeTabProps> = ({ onPreview, compact }) => {
|
|
|
28
65
|
setHalfTheme,
|
|
29
66
|
availableThemes,
|
|
30
67
|
preferredMode,
|
|
68
|
+
manageFavicon,
|
|
31
69
|
} = useTheme();
|
|
70
|
+
const faviconStyle = useConfigValue('faviconStyle');
|
|
32
71
|
|
|
33
72
|
// Which half the grid assigns to. Follows the mode you are actually seeing,
|
|
34
73
|
// so opening Settings in dark mode edits the dark half first.
|
|
@@ -68,6 +107,7 @@ export const ThemeTab: React.FC<ThemeTabProps> = ({ onPreview, compact }) => {
|
|
|
68
107
|
{THEME_MODES.map(({ id, label, Icon }) => (
|
|
69
108
|
<button
|
|
70
109
|
key={id}
|
|
110
|
+
type="button"
|
|
71
111
|
onClick={() => setMode(id)}
|
|
72
112
|
className={`px-2.5 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
|
73
113
|
mode === id
|
|
@@ -82,12 +122,23 @@ export const ThemeTab: React.FC<ThemeTabProps> = ({ onPreview, compact }) => {
|
|
|
82
122
|
</button>
|
|
83
123
|
))}
|
|
84
124
|
</div>
|
|
125
|
+
{!compact && manageFavicon && (
|
|
126
|
+
<div className="space-y-1.5 pt-1">
|
|
127
|
+
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Favicon</label>
|
|
128
|
+
<FaviconStyleControl selected={faviconStyle} />
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
85
131
|
{!compact && (
|
|
86
132
|
<p className="text-[11px] text-muted-foreground/70">
|
|
87
133
|
System follows your OS and switches between the two themes below.
|
|
88
134
|
</p>
|
|
89
135
|
)}
|
|
90
|
-
{compact &&
|
|
136
|
+
{compact && (
|
|
137
|
+
<>
|
|
138
|
+
{manageFavicon && <FaviconStyleControl selected={faviconStyle} />}
|
|
139
|
+
<div className="ml-auto">{summary}</div>
|
|
140
|
+
</>
|
|
141
|
+
)}
|
|
91
142
|
</div>
|
|
92
143
|
|
|
93
144
|
{/* Theme pair */}
|
package/components/Tooltip.tsx
CHANGED
|
@@ -63,7 +63,9 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
|
|
63
63
|
<BaseTooltip.Positioner side={side} align={align} sideOffset={sideOffset} className="isolate z-50">
|
|
64
64
|
<BaseTooltip.Popup
|
|
65
65
|
className={`z-50 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 ${
|
|
66
|
-
wide
|
|
66
|
+
wide
|
|
67
|
+
? 'max-w-[min(260px,calc(100vw-1rem))] leading-snug whitespace-normal [overflow-wrap:anywhere]'
|
|
68
|
+
: 'whitespace-nowrap'
|
|
67
69
|
}`}
|
|
68
70
|
>
|
|
69
71
|
{content}
|
package/components/Viewer.tsx
CHANGED
|
@@ -49,7 +49,13 @@ import { PinpointOverlay } from './PinpointOverlay';
|
|
|
49
49
|
import { usePinpoint } from '../hooks/usePinpoint';
|
|
50
50
|
import { useAnnotationHighlighter } from '../hooks/useAnnotationHighlighter';
|
|
51
51
|
import { useVimSelection } from '../hooks/useVimSelection';
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
getScrollViewportIntersectionRoot,
|
|
54
|
+
getScrollViewportRect,
|
|
55
|
+
getScrollViewportTop,
|
|
56
|
+
scrollViewportTo,
|
|
57
|
+
useScrollViewport,
|
|
58
|
+
} from '../hooks/useScrollViewport';
|
|
53
59
|
import { decodeAnchorHash } from '../utils/anchors';
|
|
54
60
|
import { VimModeOverlay } from './VimModeOverlay';
|
|
55
61
|
|
|
@@ -602,7 +608,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
602
608
|
if (!stickyActions || !stickySentinelRef.current || !scrollViewport) return;
|
|
603
609
|
const observer = new IntersectionObserver(
|
|
604
610
|
([entry]) => setIsStuck(!entry.isIntersecting),
|
|
605
|
-
{ root: scrollViewport, threshold: 0 }
|
|
611
|
+
{ root: getScrollViewportIntersectionRoot(scrollViewport), threshold: 0 }
|
|
606
612
|
);
|
|
607
613
|
observer.observe(stickySentinelRef.current);
|
|
608
614
|
return () => observer.disconnect();
|
|
@@ -635,12 +641,12 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
635
641
|
const headerOffset = stickyActionsEl
|
|
636
642
|
? stickyActionsEl.getBoundingClientRect().height + stickyTop
|
|
637
643
|
: 0;
|
|
638
|
-
const containerRect = scrollViewport
|
|
644
|
+
const containerRect = getScrollViewportRect(scrollViewport);
|
|
639
645
|
const targetRect = target.getBoundingClientRect();
|
|
640
646
|
const relativeTop = targetRect.top - containerRect.top;
|
|
641
|
-
const offsetPosition = scrollViewport
|
|
647
|
+
const offsetPosition = getScrollViewportTop(scrollViewport) + relativeTop - headerOffset;
|
|
642
648
|
|
|
643
|
-
scrollViewport
|
|
649
|
+
scrollViewportTo(scrollViewport, {
|
|
644
650
|
top: Math.max(0, offsetPosition),
|
|
645
651
|
behavior: 'smooth',
|
|
646
652
|
});
|
|
@@ -778,6 +784,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
778
784
|
setViewerCommentPopover(null);
|
|
779
785
|
}, []);
|
|
780
786
|
|
|
787
|
+
const commentDraftScope = linkedDocInfo?.filepath ?? sourceInfo ?? markdown.slice(0, 120);
|
|
788
|
+
|
|
781
789
|
const codePathValidation = useValidatedCodePaths(markdown, codePathBaseDir, disableCodePathValidation);
|
|
782
790
|
|
|
783
791
|
return (
|
|
@@ -1154,6 +1162,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1154
1162
|
contextText={hookCommentPopover.contextText}
|
|
1155
1163
|
isGlobal={false}
|
|
1156
1164
|
initialText={hookCommentPopover.initialText}
|
|
1165
|
+
draftKey={`plan:${commentDraftScope}:${hookCommentPopover.draftKey}`}
|
|
1157
1166
|
onSubmit={hookCommentSubmit}
|
|
1158
1167
|
onClose={hookCommentClose}
|
|
1159
1168
|
allowImages={allowImages}
|
|
@@ -1173,6 +1182,11 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1173
1182
|
contextText={viewerCommentPopover.contextText}
|
|
1174
1183
|
isGlobal={viewerCommentPopover.isGlobal}
|
|
1175
1184
|
initialText={viewerCommentPopover.initialText}
|
|
1185
|
+
draftKey={`plan:${commentDraftScope}:${
|
|
1186
|
+
viewerCommentPopover.isGlobal
|
|
1187
|
+
? 'global'
|
|
1188
|
+
: `code-block:${viewerCommentPopover.codeBlock?.block.id ?? viewerCommentPopover.contextText}`
|
|
1189
|
+
}`}
|
|
1176
1190
|
onSubmit={handleViewerCommentSubmit}
|
|
1177
1191
|
onClose={handleViewerCommentClose}
|
|
1178
1192
|
allowImages={allowImages}
|
|
@@ -4,7 +4,11 @@ import {
|
|
|
4
4
|
useState,
|
|
5
5
|
type RefObject,
|
|
6
6
|
} from 'react';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
addScrollViewportListener,
|
|
9
|
+
getScrollViewportRect,
|
|
10
|
+
useScrollViewport,
|
|
11
|
+
} from '../hooks/useScrollViewport';
|
|
8
12
|
import type { SemanticTarget } from '../utils/blockTargeting';
|
|
9
13
|
import {
|
|
10
14
|
createRangeBetweenTextPositions,
|
|
@@ -186,7 +190,9 @@ export function VimTargetReticle({
|
|
|
186
190
|
return;
|
|
187
191
|
}
|
|
188
192
|
const containerRect = container.getBoundingClientRect();
|
|
189
|
-
const viewportTop = scrollViewport
|
|
193
|
+
const viewportTop = scrollViewport
|
|
194
|
+
? getScrollViewportRect(scrollViewport).top
|
|
195
|
+
: containerRect.top;
|
|
190
196
|
const stickyBottom = container
|
|
191
197
|
.querySelector<HTMLElement>('[data-sticky-actions]')
|
|
192
198
|
?.getBoundingClientRect()
|
|
@@ -205,7 +211,9 @@ export function VimTargetReticle({
|
|
|
205
211
|
|
|
206
212
|
update();
|
|
207
213
|
window.addEventListener('resize', scheduleUpdate, { passive: true });
|
|
208
|
-
|
|
214
|
+
const removeScrollListener = scrollViewport
|
|
215
|
+
? addScrollViewportListener(scrollViewport, scheduleUpdate)
|
|
216
|
+
: undefined;
|
|
209
217
|
const resizeObserver = typeof ResizeObserver === 'undefined'
|
|
210
218
|
? null
|
|
211
219
|
: new ResizeObserver(scheduleUpdate);
|
|
@@ -214,7 +222,7 @@ export function VimTargetReticle({
|
|
|
214
222
|
return () => {
|
|
215
223
|
cancelAnimationFrame(rafRef.current);
|
|
216
224
|
window.removeEventListener('resize', scheduleUpdate);
|
|
217
|
-
|
|
225
|
+
removeScrollListener?.();
|
|
218
226
|
resizeObserver?.disconnect();
|
|
219
227
|
};
|
|
220
228
|
}, [containerRef, restoredState, scrollViewport, target]);
|
|
@@ -268,6 +268,7 @@ const GeneralInput: React.FC<{
|
|
|
268
268
|
<div className="border-t border-border/50 p-2">
|
|
269
269
|
<div className="flex items-end gap-1.5">
|
|
270
270
|
<textarea
|
|
271
|
+
data-pn-mobile-editable="true"
|
|
271
272
|
ref={textareaRef}
|
|
272
273
|
value={value}
|
|
273
274
|
onChange={(event) => onChange(event.target.value)}
|
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
-
import katex from 'katex';
|
|
3
2
|
import type { Block } from '../../types';
|
|
3
|
+
import { normalizeMathTex, renderMathToHtml } from '../../utils/math';
|
|
4
|
+
import { useMathRenderer } from '../../hooks/useMathRenderer';
|
|
5
|
+
|
|
6
|
+
// Kept as re-exports: InlineMarkdown and older consumers import them from here.
|
|
7
|
+
export { normalizeMathTex, renderMathToHtml };
|
|
4
8
|
|
|
5
9
|
type MathBlockProps = {
|
|
6
10
|
block: Block;
|
|
7
11
|
};
|
|
8
12
|
|
|
9
|
-
export const normalizeMathTex = (tex: string): string => tex.trim();
|
|
10
|
-
|
|
11
|
-
export const renderMathToHtml = (tex: string, displayMode: boolean): string => (
|
|
12
|
-
katex.renderToString(tex, {
|
|
13
|
-
displayMode,
|
|
14
|
-
throwOnError: false,
|
|
15
|
-
strict: 'warn',
|
|
16
|
-
trust: false,
|
|
17
|
-
output: 'html',
|
|
18
|
-
})
|
|
19
|
-
);
|
|
20
|
-
|
|
21
13
|
export const MathBlock: React.FC<MathBlockProps> = ({ block }) => {
|
|
22
14
|
const tex = normalizeMathTex(block.content);
|
|
23
|
-
const
|
|
15
|
+
const renderer = useMathRenderer();
|
|
16
|
+
const html = useMemo(() => renderMathToHtml(tex, true, renderer), [tex, renderer]);
|
|
17
|
+
|
|
18
|
+
// The wrapper (class names, data-* anchors, aria-label) is identical in both
|
|
19
|
+
// branches: block targeting and math-annotation restore key on these
|
|
20
|
+
// attributes, so a placeholder must be addressable exactly like the typeset
|
|
21
|
+
// block. The placeholder's child is a React text node, never markup.
|
|
22
|
+
if (html === null) {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className="math-block math-annotatable my-5 overflow-x-auto py-2 text-foreground"
|
|
26
|
+
data-block-id={block.id}
|
|
27
|
+
data-block-type="math"
|
|
28
|
+
data-math-tex={tex}
|
|
29
|
+
data-math-display="true"
|
|
30
|
+
aria-label={tex}
|
|
31
|
+
>
|
|
32
|
+
{tex}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
24
36
|
|
|
25
37
|
return (
|
|
26
38
|
<div
|
|
@@ -13,6 +13,12 @@ function cx(...classes: Array<string | false | null | undefined>): string {
|
|
|
13
13
|
return classes.filter(Boolean).join(' ');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Legacy shared button primitive.
|
|
18
|
+
*
|
|
19
|
+
* The visual sizes remain compact, while the shared touch marker expands the
|
|
20
|
+
* physical target only when a coarse pointer is present.
|
|
21
|
+
*/
|
|
16
22
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({
|
|
17
23
|
className,
|
|
18
24
|
variant = 'outline',
|
|
@@ -22,23 +28,25 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({
|
|
|
22
28
|
...props
|
|
23
29
|
}, ref) => (
|
|
24
30
|
<button
|
|
31
|
+
{...props}
|
|
25
32
|
ref={ref}
|
|
26
33
|
type={type}
|
|
34
|
+
data-pn-touch-target="true"
|
|
35
|
+
data-pn-touch-target-icon={size === 'icon' || variant === 'icon' ? 'true' : undefined}
|
|
27
36
|
className={cx(
|
|
28
37
|
'inline-flex items-center justify-center gap-2 rounded-md text-xs font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50',
|
|
29
38
|
size === 'sm' && 'h-8 px-2.5',
|
|
30
39
|
size === 'md' && 'h-9 px-3',
|
|
31
40
|
size === 'icon' && 'h-8 w-8',
|
|
32
|
-
variant === 'primary' && 'bg-primary text-primary-foreground hover:opacity-90',
|
|
33
|
-
variant === 'outline' && 'border border-border bg-card text-foreground hover:bg-muted',
|
|
34
|
-
variant === 'ghost' && 'text-muted-foreground hover:bg-muted hover:text-foreground',
|
|
41
|
+
variant === 'primary' && 'bg-primary text-primary-foreground [@media(hover:hover)_and_(pointer:fine)]:hover:opacity-90',
|
|
42
|
+
variant === 'outline' && 'border border-border bg-card text-foreground [@media(hover:hover)_and_(pointer:fine)]:hover:bg-muted',
|
|
43
|
+
variant === 'ghost' && 'text-muted-foreground [@media(hover:hover)_and_(pointer:fine)]:hover:bg-muted [@media(hover:hover)_and_(pointer:fine)]:hover:text-foreground',
|
|
35
44
|
variant === 'icon' && (active
|
|
36
45
|
? 'border border-primary/50 bg-primary/10 text-primary'
|
|
37
|
-
: 'border border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground'),
|
|
38
|
-
variant === 'danger' && 'border border-border bg-background text-muted-foreground hover:border-destructive/50 hover:bg-destructive/10 hover:text-destructive',
|
|
46
|
+
: 'border border-border bg-background text-muted-foreground [@media(hover:hover)_and_(pointer:fine)]:hover:bg-muted [@media(hover:hover)_and_(pointer:fine)]:hover:text-foreground'),
|
|
47
|
+
variant === 'danger' && 'border border-border bg-background text-muted-foreground [@media(hover:hover)_and_(pointer:fine)]:hover:border-destructive/50 [@media(hover:hover)_and_(pointer:fine)]:hover:bg-destructive/10 [@media(hover:hover)_and_(pointer:fine)]:hover:text-destructive',
|
|
39
48
|
className
|
|
40
49
|
)}
|
|
41
|
-
{...props}
|
|
42
50
|
/>
|
|
43
51
|
));
|
|
44
52
|
Button.displayName = 'Button';
|