@plannotator/ui 0.22.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 +60 -0
- package/assets/diff-options.png +0 -0
- package/assets/icon-codex.png +0 -0
- package/assets/look-flat.png +0 -0
- package/assets/look-grid.png +0 -0
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/assets/workspaces.webp +0 -0
- package/components/AISettingsTab.tsx +145 -0
- package/components/ActionMenu.tsx +101 -0
- package/components/AgentControls.tsx +216 -0
- package/components/AgentsTab.tsx +1294 -0
- package/components/AnnotationPanel.tsx +731 -0
- package/components/AnnotationSidebar.tsx +86 -0
- package/components/AnnotationToolbar.tsx +323 -0
- package/components/AnnotationToolstrip.tsx +355 -0
- package/components/ApproveDropdown.tsx +170 -0
- package/components/AttachmentsButton.tsx +410 -0
- package/components/BlockRenderer.tsx +163 -0
- package/components/BorderTrail.tsx +38 -0
- package/components/CodeFilePicker.tsx +64 -0
- package/components/CodeFilePopout.tsx +602 -0
- package/components/CodePathValidationContext.tsx +14 -0
- package/components/CommentPopover.tsx +512 -0
- package/components/CompletionOverlay.tsx +99 -0
- package/components/ConfirmDialog.tsx +128 -0
- package/components/DocBadges.tsx +231 -0
- package/components/EditorAnnotationCard.tsx +65 -0
- package/components/ExportModal.tsx +565 -0
- package/components/FloatingQuickLabelPicker.tsx +144 -0
- package/components/GitHubIcon.tsx +16 -0
- package/components/GitLabIcon.tsx +30 -0
- package/components/GraphvizBlock.tsx +511 -0
- package/components/ImageAnnotator/Canvas.tsx +118 -0
- package/components/ImageAnnotator/Toolbar.tsx +218 -0
- package/components/ImageAnnotator/index.tsx +272 -0
- package/components/ImageAnnotator/types.ts +39 -0
- package/components/ImageAnnotator/utils.ts +147 -0
- package/components/ImageThumbnail.tsx +126 -0
- package/components/ImportModal.tsx +146 -0
- package/components/InlineMarkdown.tsx +1094 -0
- package/components/KeyboardShortcuts.tsx +208 -0
- package/components/Landing.tsx +496 -0
- package/components/ListItemBody.tsx +62 -0
- package/components/ListMarker.tsx +73 -0
- package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
- package/components/MarkdownEditor.tsx +44 -0
- package/components/MenuVersionSection.tsx +91 -0
- package/components/MermaidBlock.tsx +579 -0
- package/components/ModeToggle.tsx +68 -0
- package/components/OpenInAppButton.tsx +299 -0
- package/components/OverlayScrollArea.tsx +82 -0
- package/components/PermissionModeSetup.tsx +92 -0
- package/components/PinpointOverlay.tsx +99 -0
- package/components/PlanAIAnnouncementDialog.tsx +187 -0
- package/components/PlanHeaderMenu.tsx +299 -0
- package/components/PopoutDialog.tsx +91 -0
- package/components/Popover.tsx +27 -0
- package/components/ProviderIcons.tsx +51 -0
- package/components/PullRequestIcon.tsx +12 -0
- package/components/QuickLabelDropdown.tsx +63 -0
- package/components/RenderedMarkdown.tsx +54 -0
- package/components/RepoIcon.tsx +12 -0
- package/components/ResizeHandle.tsx +86 -0
- package/components/ReviewAgentsIcon.tsx +18 -0
- package/components/SearchableSelect.tsx +157 -0
- package/components/Settings.tsx +2220 -0
- package/components/SparklesIcon.tsx +45 -0
- package/components/StickyHeaderLane.tsx +260 -0
- package/components/TableOfContents.tsx +157 -0
- package/components/TaterSpritePullup.tsx +34 -0
- package/components/TaterSpriteRunning.tsx +54 -0
- package/components/TaterSpriteSitting.tsx +37 -0
- package/components/TextShimmer.tsx +57 -0
- package/components/ThemeProvider.tsx +162 -0
- package/components/ThemeTab.tsx +139 -0
- package/components/ToolbarButtons.tsx +116 -0
- package/components/Tooltip.tsx +45 -0
- package/components/Viewer.tsx +977 -0
- package/components/ai/AIProviderBar.tsx +95 -0
- package/components/ai/DocumentAIChatPanel.tsx +313 -0
- package/components/blocks/AlertBlock.tsx +58 -0
- package/components/blocks/Callout.tsx +60 -0
- package/components/blocks/CodeBlock.tsx +75 -0
- package/components/blocks/HtmlBlock.tsx +122 -0
- package/components/blocks/MathBlock.tsx +36 -0
- package/components/blocks/TableBlock.tsx +146 -0
- package/components/blocks/TablePopout.tsx +279 -0
- package/components/blocks/TableToolbar.tsx +153 -0
- package/components/blocks/proseBody.tsx +102 -0
- package/components/core/button.tsx +44 -0
- package/components/core/textarea.tsx +26 -0
- package/components/diagramLanguages.ts +14 -0
- package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
- package/components/html-viewer/HtmlViewer.tsx +386 -0
- package/components/html-viewer/bridge-script.ts +505 -0
- package/components/html-viewer/index.ts +1 -0
- package/components/html-viewer/useHtmlAnnotation.ts +400 -0
- package/components/icons/AgentIcons.tsx +66 -0
- package/components/icons/AppIcon.tsx +56 -0
- package/components/icons/MessagesIcon.tsx +11 -0
- package/components/icons/ObsidianIcons.tsx +208 -0
- package/components/icons/app/android-studio.svg +369 -0
- package/components/icons/app/antigravity.svg +97 -0
- package/components/icons/app/cursor.svg +16 -0
- package/components/icons/app/file-explorer.svg +20 -0
- package/components/icons/app/finder.png +0 -0
- package/components/icons/app/ghostty.svg +13 -0
- package/components/icons/app/iterm2.svg +13 -0
- package/components/icons/app/powershell.svg +14 -0
- package/components/icons/app/sublime-text.svg +17 -0
- package/components/icons/app/terminal.png +0 -0
- package/components/icons/app/textmate.png +0 -0
- package/components/icons/app/vscode.svg +39 -0
- package/components/icons/app/warp.png +0 -0
- package/components/icons/app/xcode.png +0 -0
- package/components/icons/app/zed-dark.svg +15 -0
- package/components/icons/app/zed.svg +15 -0
- package/components/icons/themeIcons.tsx +47 -0
- package/components/mermaidSvg.ts +33 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
- package/components/plan-diff/PlanDiffBadge.tsx +48 -0
- package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
- package/components/plan-diff/PlanDiffViewer.tsx +214 -0
- package/components/plan-diff/PlanRawDiffView.tsx +102 -0
- package/components/plan-diff/VSCodeIcon.tsx +132 -0
- package/components/settings/HooksTab.tsx +208 -0
- package/components/sidebar/ArchiveBrowser.tsx +98 -0
- package/components/sidebar/CountBadge.tsx +12 -0
- package/components/sidebar/FileBrowser.tsx +498 -0
- package/components/sidebar/MessagesBrowser.tsx +109 -0
- package/components/sidebar/SidebarContainer.tsx +353 -0
- package/components/sidebar/SidebarTabs.tsx +155 -0
- package/components/sidebar/VersionBrowser.tsx +141 -0
- package/components/types.d.ts +7 -0
- package/components/ui/badge.tsx +44 -0
- package/components/ui/button.tsx +83 -0
- package/components/ui/card.tsx +58 -0
- package/components/ui/dialog.tsx +106 -0
- package/components/ui/dropdown-menu.tsx +240 -0
- package/components/ui/state-pill.tsx +45 -0
- package/components/ui/tabs.tsx +46 -0
- package/components/ui/textarea.tsx +25 -0
- package/config/configStore.ts +214 -0
- package/config/index.ts +4 -0
- package/config/reviewView.ts +42 -0
- package/config/settings.ts +310 -0
- package/config/useConfig.ts +20 -0
- package/configure.ts +69 -0
- package/globals.d.ts +14 -0
- package/hooks/pfm/useCodeFilePopout.ts +111 -0
- package/hooks/useAIChat.ts +532 -0
- package/hooks/useAIProviderConfig.ts +115 -0
- package/hooks/useActiveSection.ts +78 -0
- package/hooks/useAgentJobs.ts +306 -0
- package/hooks/useAgentSettings.ts +579 -0
- package/hooks/useAgents.ts +90 -0
- package/hooks/useAnnotationDraft.ts +523 -0
- package/hooks/useAnnotationHighlighter.ts +1170 -0
- package/hooks/useArchive.ts +171 -0
- package/hooks/useAutoClose.ts +104 -0
- package/hooks/useCodeAnnotationDraft.ts +194 -0
- package/hooks/useCodeFilePopout.ts +1 -0
- package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
- package/hooks/useDraggable.ts +108 -0
- package/hooks/useEditorAnnotations.ts +64 -0
- package/hooks/useExternalAnnotationHighlights.ts +105 -0
- package/hooks/useExternalAnnotations.ts +268 -0
- package/hooks/useFileBrowser.ts +402 -0
- package/hooks/useInputMethodSwitch.ts +89 -0
- package/hooks/useIsMobile.ts +17 -0
- package/hooks/useLinkedDoc.ts +494 -0
- package/hooks/useOverlayViewport.ts +36 -0
- package/hooks/usePinpoint.ts +182 -0
- package/hooks/usePlanDiff.ts +176 -0
- package/hooks/usePrintMode.ts +27 -0
- package/hooks/useResizablePanel.ts +173 -0
- package/hooks/useScrollViewport.ts +38 -0
- package/hooks/useSharing.ts +441 -0
- package/hooks/useSidebar.ts +55 -0
- package/hooks/useUpdateCheck.ts +132 -0
- package/hooks/useValidatedCodePaths.ts +94 -0
- package/icons/GitUser.tsx +17 -0
- package/lib/utils.ts +11 -0
- package/package.json +117 -0
- package/plannotator.webp +0 -0
- package/print.css +427 -0
- package/shortcuts/code-review/ai.shortcuts.ts +25 -0
- package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
- package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
- package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
- package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
- package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
- package/shortcuts/core.ts +399 -0
- package/shortcuts/index.ts +21 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
- package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
- package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
- package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
- package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
- package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
- package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
- package/shortcuts/runtime.ts +259 -0
- package/sprite_package_additional/index.html +34 -0
- package/sprite_package_additional/sprite.png +0 -0
- package/sprite_package_new/index.html +34 -0
- package/sprite_package_new/sprite.png +0 -0
- package/sprite_package_pulluphang/index.html +34 -0
- package/sprite_package_pulluphang/sprite.png +0 -0
- package/styles.css +1 -0
- package/theme.css +893 -0
- package/themes/adwaita.css +62 -0
- package/themes/andromeeda.css +66 -0
- package/themes/aurora-x.css +66 -0
- package/themes/ayu-dark.css +66 -0
- package/themes/caffeine.css +113 -0
- package/themes/catppuccin.css +62 -0
- package/themes/claude-plus.css +60 -0
- package/themes/cursor-hc.css +34 -0
- package/themes/cursor-midnight.css +34 -0
- package/themes/cursor.css +62 -0
- package/themes/dark-plus.css +66 -0
- package/themes/doom-64.css +109 -0
- package/themes/dracula.css +33 -0
- package/themes/everforest-hard.css +62 -0
- package/themes/everforest-soft.css +62 -0
- package/themes/everforest.css +62 -0
- package/themes/github.css +66 -0
- package/themes/gruvbox.css +62 -0
- package/themes/houston.css +66 -0
- package/themes/kanagawa-dragon.css +34 -0
- package/themes/kanagawa-lotus.css +34 -0
- package/themes/kanagawa-wave.css +34 -0
- package/themes/laserwave.css +66 -0
- package/themes/material.css +62 -0
- package/themes/min.css +66 -0
- package/themes/monokai-pro.css +34 -0
- package/themes/neutral.css +59 -0
- package/themes/night-owl.css +66 -0
- package/themes/nord.css +66 -0
- package/themes/one-dark-pro.css +66 -0
- package/themes/one-light.css +66 -0
- package/themes/paulmillr.css +34 -0
- package/themes/plannotator.css +60 -0
- package/themes/plastic.css +66 -0
- package/themes/poimandres.css +66 -0
- package/themes/quantum-rose.css +109 -0
- package/themes/red.css +66 -0
- package/themes/rose-pine.css +62 -0
- package/themes/simple.css +124 -0
- package/themes/slack.css +66 -0
- package/themes/snazzy-light.css +66 -0
- package/themes/soft-pop.css +60 -0
- package/themes/solar-dusk.css +109 -0
- package/themes/solarized.css +66 -0
- package/themes/synthwave-84.css +34 -0
- package/themes/terminal.css +62 -0
- package/themes/tinacious.css +57 -0
- package/themes/tokyo-night.css +62 -0
- package/themes/vesper.css +62 -0
- package/themes/vitesse-black.css +66 -0
- package/themes/vitesse.css +62 -0
- package/types.ts +264 -0
- package/utils/agentSwitch.ts +71 -0
- package/utils/aiChatFormat.ts +33 -0
- package/utils/aiPrompt.ts +29 -0
- package/utils/aiProvider.ts +217 -0
- package/utils/anchors.ts +11 -0
- package/utils/annotateAgentTerminal.ts +29 -0
- package/utils/annotationHelpers.ts +101 -0
- package/utils/bear.ts +59 -0
- package/utils/blockTargeting.ts +240 -0
- package/utils/callback.ts +99 -0
- package/utils/commentContent.ts +8 -0
- package/utils/defaultNotesApp.ts +20 -0
- package/utils/diffFonts.ts +33 -0
- package/utils/editorMode.ts +31 -0
- package/utils/fileBrowser.ts +40 -0
- package/utils/generateId.ts +7 -0
- package/utils/generateIdentity.ts +29 -0
- package/utils/identity.ts +112 -0
- package/utils/inlineTransforms.ts +41 -0
- package/utils/inputMethod.ts +17 -0
- package/utils/lookAndFeelAnnouncement.ts +18 -0
- package/utils/obsidian.ts +201 -0
- package/utils/octarine.ts +53 -0
- package/utils/parser.ts +991 -0
- package/utils/permissionMode.ts +77 -0
- package/utils/planAIAnnouncement.ts +17 -0
- package/utils/planAgentInstructions.ts +137 -0
- package/utils/planDiffEngine.ts +590 -0
- package/utils/planSave.ts +49 -0
- package/utils/platform.ts +11 -0
- package/utils/quickLabels.ts +77 -0
- package/utils/reviewAgentInstructions.ts +182 -0
- package/utils/sanitizeHtml.ts +45 -0
- package/utils/sharing.ts +350 -0
- package/utils/slugify.ts +37 -0
- package/utils/storage.ts +141 -0
- package/utils/themeRegistry.ts +567 -0
- package/utils/uiPreferences.ts +34 -0
- package/utils/upload.ts +56 -0
- package/utils/wideMode.ts +48 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from "react";
|
|
2
|
+
import { isCodeFilePath } from "@plannotator/core/code-file";
|
|
3
|
+
import { Block } from "../../types";
|
|
4
|
+
import { sanitizeBlockHtml } from "../../utils/sanitizeHtml";
|
|
5
|
+
import { getImageSrc } from "../ImageThumbnail";
|
|
6
|
+
|
|
7
|
+
interface HtmlBlockProps {
|
|
8
|
+
block: Block;
|
|
9
|
+
imageBaseDir?: string;
|
|
10
|
+
onOpenLinkedDoc?: (path: string) => void;
|
|
11
|
+
onOpenCodeFile?: (path: string) => void;
|
|
12
|
+
onNavigateAnchor?: (hash: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Walks the sanitized DOM and rewrites relative <img src> / <a href> so they
|
|
16
|
+
// behave the same as their markdown counterparts:
|
|
17
|
+
// - Relative image paths route through /api/image?path=... so they load from
|
|
18
|
+
// the plan's directory, not the plannotator server root.
|
|
19
|
+
// - Relative .md / .mdx / .txt / .html links open in the linked-doc overlay when
|
|
20
|
+
// onOpenLinkedDoc is provided (same as [[wiki-links]] and [label](./x.md)).
|
|
21
|
+
// Absolute http(s) URLs and mailto: are left untouched.
|
|
22
|
+
function rewriteRelativeRefs(
|
|
23
|
+
root: HTMLElement,
|
|
24
|
+
imageBaseDir?: string,
|
|
25
|
+
onOpenLinkedDoc?: (path: string) => void,
|
|
26
|
+
onOpenCodeFile?: (path: string) => void,
|
|
27
|
+
onNavigateAnchor?: (hash: string) => void,
|
|
28
|
+
): (() => void) {
|
|
29
|
+
const cleanups: (() => void)[] = [];
|
|
30
|
+
|
|
31
|
+
root.querySelectorAll('img').forEach((img) => {
|
|
32
|
+
const src = img.getAttribute('src');
|
|
33
|
+
if (!src) return;
|
|
34
|
+
if (/^(https?:|data:|blob:)/i.test(src)) return;
|
|
35
|
+
img.setAttribute('src', getImageSrc(src, imageBaseDir));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
root.querySelectorAll('a').forEach((a) => {
|
|
39
|
+
const href = a.getAttribute('href');
|
|
40
|
+
if (!href) return;
|
|
41
|
+
// External http(s) links: open in a new tab and close the tab-nabbing
|
|
42
|
+
// vector (opener reference back to the plannotator tab). Matches the
|
|
43
|
+
// markdown renderer's behavior for [label](https://...).
|
|
44
|
+
if (/^(https?:|\/\/)/i.test(href)) {
|
|
45
|
+
a.setAttribute('target', '_blank');
|
|
46
|
+
a.setAttribute('rel', 'noopener noreferrer');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (/^(mailto:|tel:)/i.test(href)) return;
|
|
50
|
+
// In-page anchor: native browser jump doesn't target the scroll viewport,
|
|
51
|
+
// so route through onNavigateAnchor to match InlineMarkdown's behavior.
|
|
52
|
+
if (href.startsWith('#')) {
|
|
53
|
+
if (!onNavigateAnchor) return;
|
|
54
|
+
const handler = (e: Event) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
onNavigateAnchor(href);
|
|
57
|
+
};
|
|
58
|
+
a.addEventListener('click', handler);
|
|
59
|
+
cleanups.push(() => a.removeEventListener('click', handler));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (onOpenCodeFile && isCodeFilePath(href)) {
|
|
63
|
+
const handler = (e: Event) => {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
onOpenCodeFile(href.replace(/#.*$/, ''));
|
|
66
|
+
};
|
|
67
|
+
a.addEventListener('click', handler);
|
|
68
|
+
cleanups.push(() => a.removeEventListener('click', handler));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (onOpenLinkedDoc && /\.(mdx?|txt|html?)(#.*)?$/i.test(href)) {
|
|
72
|
+
const handler = (e: Event) => {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
onOpenLinkedDoc(href.replace(/#.*$/, ''));
|
|
75
|
+
};
|
|
76
|
+
a.addEventListener('click', handler);
|
|
77
|
+
cleanups.push(() => a.removeEventListener('click', handler));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return () => cleanups.forEach((fn) => fn());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The inner HTML is set imperatively (not via dangerouslySetInnerHTML) so that
|
|
85
|
+
// React's reconciliation never replaces the rendered subtree on re-render.
|
|
86
|
+
// That matters because <details open> is DOM-owned state — a stray innerHTML
|
|
87
|
+
// re-set on every parent re-render would collapse any open <details> the
|
|
88
|
+
// user just opened. Paired with React.memo below so the component itself
|
|
89
|
+
// stops re-rendering unless the block content actually changes.
|
|
90
|
+
const HtmlBlockImpl: React.FC<HtmlBlockProps> = ({ block, imageBaseDir, onOpenLinkedDoc, onOpenCodeFile, onNavigateAnchor }) => {
|
|
91
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
92
|
+
const sanitized = React.useMemo(
|
|
93
|
+
() => sanitizeBlockHtml(block.content),
|
|
94
|
+
[block.content],
|
|
95
|
+
);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!ref.current) return;
|
|
98
|
+
if (ref.current.innerHTML !== sanitized) {
|
|
99
|
+
ref.current.innerHTML = sanitized;
|
|
100
|
+
}
|
|
101
|
+
const cleanup = rewriteRelativeRefs(ref.current, imageBaseDir, onOpenLinkedDoc, onOpenCodeFile, onNavigateAnchor);
|
|
102
|
+
return cleanup;
|
|
103
|
+
}, [sanitized, imageBaseDir, onOpenLinkedDoc, onOpenCodeFile, onNavigateAnchor]);
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
ref={ref}
|
|
107
|
+
data-block-id={block.id}
|
|
108
|
+
data-block-type="html"
|
|
109
|
+
className="html-block my-4 text-[15px] leading-relaxed text-foreground/90"
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
export const HtmlBlock = React.memo(
|
|
114
|
+
HtmlBlockImpl,
|
|
115
|
+
(prev, next) =>
|
|
116
|
+
prev.block.id === next.block.id &&
|
|
117
|
+
prev.block.content === next.block.content &&
|
|
118
|
+
prev.imageBaseDir === next.imageBaseDir &&
|
|
119
|
+
prev.onOpenLinkedDoc === next.onOpenLinkedDoc &&
|
|
120
|
+
prev.onOpenCodeFile === next.onOpenCodeFile &&
|
|
121
|
+
prev.onNavigateAnchor === next.onNavigateAnchor,
|
|
122
|
+
);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import katex from 'katex';
|
|
3
|
+
import type { Block } from '../../types';
|
|
4
|
+
|
|
5
|
+
type MathBlockProps = {
|
|
6
|
+
block: Block;
|
|
7
|
+
};
|
|
8
|
+
|
|
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
|
+
export const MathBlock: React.FC<MathBlockProps> = ({ block }) => {
|
|
22
|
+
const tex = normalizeMathTex(block.content);
|
|
23
|
+
const html = useMemo(() => renderMathToHtml(tex, true), [tex]);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className="math-block math-annotatable my-5 overflow-x-auto py-2 text-foreground"
|
|
28
|
+
data-block-id={block.id}
|
|
29
|
+
data-block-type="math"
|
|
30
|
+
data-math-tex={tex}
|
|
31
|
+
data-math-display="true"
|
|
32
|
+
aria-label={tex}
|
|
33
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
|
+
import { Block } from '../../types';
|
|
3
|
+
import { InlineMarkdown } from '../InlineMarkdown';
|
|
4
|
+
|
|
5
|
+
interface TableBlockProps {
|
|
6
|
+
block: Block;
|
|
7
|
+
onHover?: (element: HTMLElement) => void;
|
|
8
|
+
onLeave?: () => void;
|
|
9
|
+
onOpenLinkedDoc?: (path: string) => void;
|
|
10
|
+
onOpenCodeFile?: (path: string) => void;
|
|
11
|
+
onNavigateAnchor?: (hash: string) => void;
|
|
12
|
+
imageBaseDir?: string;
|
|
13
|
+
onImageClick?: (src: string, alt: string) => void;
|
|
14
|
+
githubRepo?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Parse pipe-delimited markdown table content into headers + rows.
|
|
18
|
+
// Exported so TableToolbar can reuse it to build a CSV copy without
|
|
19
|
+
// needing a second parser.
|
|
20
|
+
export const parseTableContent = (content: string): { headers: string[]; rows: string[][] } => {
|
|
21
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
22
|
+
if (lines.length === 0) return { headers: [], rows: [] };
|
|
23
|
+
|
|
24
|
+
const parseRow = (line: string): string[] =>
|
|
25
|
+
line
|
|
26
|
+
.replace(/^\|/, '')
|
|
27
|
+
.replace(/\|$/, '')
|
|
28
|
+
.split(/(?<!\\)\|/)
|
|
29
|
+
.map((cell) => cell.trim().replace(/\\\|/g, '|'));
|
|
30
|
+
|
|
31
|
+
const headers = parseRow(lines[0]);
|
|
32
|
+
const rows: string[][] = [];
|
|
33
|
+
for (let i = 1; i < lines.length; i++) {
|
|
34
|
+
const line = lines[i].trim();
|
|
35
|
+
if (/^[\|\-:\s]+$/.test(line)) continue; // separator row
|
|
36
|
+
rows.push(parseRow(line));
|
|
37
|
+
}
|
|
38
|
+
return { headers, rows };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// RFC 4180 CSV escaping: quote values that contain a comma, double-quote,
|
|
42
|
+
// CR, or LF; escape inner double-quotes by doubling them.
|
|
43
|
+
const csvEscape = (value: string): string => {
|
|
44
|
+
if (/[",\r\n]/.test(value)) {
|
|
45
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Build RFC 4180 CSV from pre-parsed headers + rows. The popout uses this
|
|
51
|
+
// directly with TanStack's visible (filter + sort applied) rows.
|
|
52
|
+
export const buildCsvFromRows = (headers: string[], rows: string[][]): string => {
|
|
53
|
+
const lines = [headers, ...rows].map((row) => row.map(csvEscape).join(','));
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Rebuild a pipe-delimited markdown table from pre-parsed headers + rows.
|
|
58
|
+
// Inverse of parseTableContent. Whitespace normalized (one space per cell
|
|
59
|
+
// padding). The popout uses this for copy-as-markdown when filter or sort
|
|
60
|
+
// might change what the user sees.
|
|
61
|
+
// parseTableContent unescapes `\|` → `|`, so cells may hold literal pipes.
|
|
62
|
+
// Re-escape on the way out — otherwise the serialized table sprouts extra
|
|
63
|
+
// columns wherever a cell had a pipe.
|
|
64
|
+
const mdCellEscape = (value: string): string => value.replace(/\|/g, '\\|');
|
|
65
|
+
|
|
66
|
+
export const buildMarkdownTable = (headers: string[], rows: string[][]): string => {
|
|
67
|
+
const headerLine = `| ${headers.map(mdCellEscape).join(' | ')} |`;
|
|
68
|
+
const separator = `| ${headers.map(() => '---').join(' | ')} |`;
|
|
69
|
+
const bodyLines = rows.map((row) => `| ${row.map(mdCellEscape).join(' | ')} |`);
|
|
70
|
+
return [headerLine, separator, ...bodyLines].join('\n');
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Convert the markdown table's pipe-delimited source into RFC 4180 CSV.
|
|
74
|
+
// The hover toolbar uses this on the raw block content (no filter state).
|
|
75
|
+
export const buildCsv = (markdown: string): string => {
|
|
76
|
+
const { headers, rows } = parseTableContent(markdown);
|
|
77
|
+
return buildCsvFromRows(headers, rows);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const TableBlock: React.FC<TableBlockProps> = ({
|
|
81
|
+
block,
|
|
82
|
+
onHover,
|
|
83
|
+
onLeave,
|
|
84
|
+
onOpenLinkedDoc,
|
|
85
|
+
onOpenCodeFile,
|
|
86
|
+
onNavigateAnchor,
|
|
87
|
+
imageBaseDir,
|
|
88
|
+
onImageClick,
|
|
89
|
+
githubRepo,
|
|
90
|
+
}) => {
|
|
91
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
92
|
+
const { headers, rows } = parseTableContent(block.content);
|
|
93
|
+
|
|
94
|
+
const handleMouseEnter = () => {
|
|
95
|
+
if (containerRef.current && onHover) onHover(containerRef.current);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
ref={containerRef}
|
|
101
|
+
className="my-4 overflow-x-auto"
|
|
102
|
+
data-block-id={block.id}
|
|
103
|
+
onMouseEnter={handleMouseEnter}
|
|
104
|
+
onMouseLeave={onLeave}
|
|
105
|
+
>
|
|
106
|
+
<table className="min-w-full border-collapse text-sm">
|
|
107
|
+
<thead>
|
|
108
|
+
<tr className="border-b border-border">
|
|
109
|
+
{headers.map((header, i) => (
|
|
110
|
+
<th key={i} className="px-3 py-2 text-left font-semibold text-foreground/90 bg-muted/30">
|
|
111
|
+
<InlineMarkdown
|
|
112
|
+
imageBaseDir={imageBaseDir}
|
|
113
|
+
onImageClick={onImageClick}
|
|
114
|
+
text={header}
|
|
115
|
+
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
116
|
+
onOpenCodeFile={onOpenCodeFile}
|
|
117
|
+
onNavigateAnchor={onNavigateAnchor}
|
|
118
|
+
githubRepo={githubRepo}
|
|
119
|
+
/>
|
|
120
|
+
</th>
|
|
121
|
+
))}
|
|
122
|
+
</tr>
|
|
123
|
+
</thead>
|
|
124
|
+
<tbody>
|
|
125
|
+
{rows.map((row, rowIdx) => (
|
|
126
|
+
<tr key={rowIdx} className="border-b border-border/50 hover:bg-muted/20">
|
|
127
|
+
{row.map((cell, cellIdx) => (
|
|
128
|
+
<td key={cellIdx} className="px-3 py-2 text-foreground/80">
|
|
129
|
+
<InlineMarkdown
|
|
130
|
+
imageBaseDir={imageBaseDir}
|
|
131
|
+
onImageClick={onImageClick}
|
|
132
|
+
text={cell}
|
|
133
|
+
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
134
|
+
onOpenCodeFile={onOpenCodeFile}
|
|
135
|
+
onNavigateAnchor={onNavigateAnchor}
|
|
136
|
+
githubRepo={githubRepo}
|
|
137
|
+
/>
|
|
138
|
+
</td>
|
|
139
|
+
))}
|
|
140
|
+
</tr>
|
|
141
|
+
))}
|
|
142
|
+
</tbody>
|
|
143
|
+
</table>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
createColumnHelper,
|
|
4
|
+
flexRender,
|
|
5
|
+
getCoreRowModel,
|
|
6
|
+
getFilteredRowModel,
|
|
7
|
+
getSortedRowModel,
|
|
8
|
+
useReactTable,
|
|
9
|
+
type ColumnDef,
|
|
10
|
+
type SortingState,
|
|
11
|
+
} from '@tanstack/react-table';
|
|
12
|
+
import { Block } from '../../types';
|
|
13
|
+
import { InlineMarkdown } from '../InlineMarkdown';
|
|
14
|
+
import { PopoutDialog } from '../PopoutDialog';
|
|
15
|
+
import { parseTableContent, buildCsvFromRows, buildMarkdownTable } from './TableBlock';
|
|
16
|
+
|
|
17
|
+
interface TablePopoutProps {
|
|
18
|
+
block: Block;
|
|
19
|
+
open: boolean;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
/** Portal target — pass Viewer's annotation containerRef so annotation
|
|
22
|
+
* hooks can walk into the popout's text nodes. Null falls back to body. */
|
|
23
|
+
container?: HTMLElement | null;
|
|
24
|
+
onOpenLinkedDoc?: (path: string) => void;
|
|
25
|
+
onOpenCodeFile?: (path: string) => void;
|
|
26
|
+
onNavigateAnchor?: (hash: string) => void;
|
|
27
|
+
imageBaseDir?: string;
|
|
28
|
+
onImageClick?: (src: string, alt: string) => void;
|
|
29
|
+
githubRepo?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Row = Record<string, string>;
|
|
33
|
+
|
|
34
|
+
const TablePopoutImpl: React.FC<TablePopoutProps> = ({
|
|
35
|
+
block,
|
|
36
|
+
open,
|
|
37
|
+
onClose,
|
|
38
|
+
container,
|
|
39
|
+
onOpenLinkedDoc,
|
|
40
|
+
onOpenCodeFile,
|
|
41
|
+
onNavigateAnchor,
|
|
42
|
+
imageBaseDir,
|
|
43
|
+
onImageClick,
|
|
44
|
+
githubRepo,
|
|
45
|
+
}) => {
|
|
46
|
+
const { headers, rows } = useMemo(() => parseTableContent(block.content), [block.content]);
|
|
47
|
+
|
|
48
|
+
const columnIds = useMemo(() => {
|
|
49
|
+
const seen = new Map<string, number>();
|
|
50
|
+
return headers.map((h, i) => {
|
|
51
|
+
const base = h.trim() || `col-${i}`;
|
|
52
|
+
const n = (seen.get(base) ?? 0) + 1;
|
|
53
|
+
seen.set(base, n);
|
|
54
|
+
return n === 1 ? base : `${base}-${n}`;
|
|
55
|
+
});
|
|
56
|
+
}, [headers]);
|
|
57
|
+
|
|
58
|
+
const data = useMemo<Row[]>(
|
|
59
|
+
() =>
|
|
60
|
+
rows.map((row) => {
|
|
61
|
+
const obj: Row = {};
|
|
62
|
+
columnIds.forEach((id, i) => {
|
|
63
|
+
obj[id] = row[i] ?? '';
|
|
64
|
+
});
|
|
65
|
+
return obj;
|
|
66
|
+
}),
|
|
67
|
+
[rows, columnIds],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const columns = useMemo<ColumnDef<Row, string>[]>(() => {
|
|
71
|
+
const helper = createColumnHelper<Row>();
|
|
72
|
+
return columnIds.map((id, i) =>
|
|
73
|
+
helper.accessor((row) => row[id], {
|
|
74
|
+
id,
|
|
75
|
+
header: headers[i],
|
|
76
|
+
cell: (info) => (
|
|
77
|
+
<InlineMarkdown
|
|
78
|
+
imageBaseDir={imageBaseDir}
|
|
79
|
+
onImageClick={onImageClick}
|
|
80
|
+
text={info.getValue()}
|
|
81
|
+
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
82
|
+
onOpenCodeFile={onOpenCodeFile}
|
|
83
|
+
onNavigateAnchor={onNavigateAnchor}
|
|
84
|
+
githubRepo={githubRepo}
|
|
85
|
+
/>
|
|
86
|
+
),
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}, [columnIds, headers, imageBaseDir, onImageClick, onOpenLinkedDoc, onNavigateAnchor, githubRepo]);
|
|
90
|
+
|
|
91
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
92
|
+
const [globalFilter, setGlobalFilter] = useState('');
|
|
93
|
+
const [copiedMd, setCopiedMd] = useState(false);
|
|
94
|
+
const [copiedCsv, setCopiedCsv] = useState(false);
|
|
95
|
+
|
|
96
|
+
const getVisibleRowsData = (): string[][] =>
|
|
97
|
+
table.getRowModel().rows.map((row) =>
|
|
98
|
+
columnIds.map((id) => row.getValue<string>(id) ?? ''),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const handleCopyMarkdown = async () => {
|
|
102
|
+
try {
|
|
103
|
+
await navigator.clipboard.writeText(buildMarkdownTable(headers, getVisibleRowsData()));
|
|
104
|
+
setCopiedMd(true);
|
|
105
|
+
setTimeout(() => setCopiedMd(false), 1500);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error('Failed to copy:', err);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleCopyCsv = async () => {
|
|
112
|
+
try {
|
|
113
|
+
await navigator.clipboard.writeText(buildCsvFromRows(headers, getVisibleRowsData()));
|
|
114
|
+
setCopiedCsv(true);
|
|
115
|
+
setTimeout(() => setCopiedCsv(false), 1500);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error('Failed to copy:', err);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const table = useReactTable({
|
|
122
|
+
data,
|
|
123
|
+
columns,
|
|
124
|
+
state: { sorting, globalFilter },
|
|
125
|
+
onSortingChange: setSorting,
|
|
126
|
+
onGlobalFilterChange: setGlobalFilter,
|
|
127
|
+
getCoreRowModel: getCoreRowModel(),
|
|
128
|
+
getSortedRowModel: getSortedRowModel(),
|
|
129
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
130
|
+
globalFilterFn: 'includesString',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const visibleRows = table.getRowModel().rows;
|
|
134
|
+
const totalRows = data.length;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<PopoutDialog
|
|
138
|
+
open={open}
|
|
139
|
+
onClose={onClose}
|
|
140
|
+
title="Table"
|
|
141
|
+
container={container}
|
|
142
|
+
dataAttributes={{ 'data-block-id': block.id }}
|
|
143
|
+
>
|
|
144
|
+
<div className="flex items-center gap-3 px-5 pt-4 pb-3 pr-12">
|
|
145
|
+
<div className="relative max-w-sm flex-1">
|
|
146
|
+
<svg className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
147
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-4.35-4.35M10 18a8 8 0 100-16 8 8 0 000 16z" />
|
|
148
|
+
</svg>
|
|
149
|
+
<input
|
|
150
|
+
type="text"
|
|
151
|
+
value={globalFilter}
|
|
152
|
+
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
153
|
+
placeholder="Filter rows…"
|
|
154
|
+
className="w-full pl-8 pr-3 py-1.5 text-sm bg-muted/40 border border-border/60 rounded-md focus:outline-none focus:ring-1 focus:ring-primary/50 focus:border-primary/50"
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
<span className="text-xs text-muted-foreground tabular-nums">
|
|
158
|
+
{visibleRows.length === totalRows
|
|
159
|
+
? `${totalRows} row${totalRows === 1 ? '' : 's'}`
|
|
160
|
+
: `${visibleRows.length} of ${totalRows}`}
|
|
161
|
+
</span>
|
|
162
|
+
<div className="ml-auto flex items-center gap-1">
|
|
163
|
+
<button
|
|
164
|
+
onClick={handleCopyMarkdown}
|
|
165
|
+
title={
|
|
166
|
+
copiedMd
|
|
167
|
+
? 'Copied!'
|
|
168
|
+
: visibleRows.length === totalRows
|
|
169
|
+
? 'Copy as markdown'
|
|
170
|
+
: `Copy ${visibleRows.length} row${visibleRows.length === 1 ? '' : 's'} as markdown`
|
|
171
|
+
}
|
|
172
|
+
className={`p-1.5 rounded-md transition-colors ${
|
|
173
|
+
copiedMd ? 'text-success' : 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
174
|
+
}`}
|
|
175
|
+
>
|
|
176
|
+
{copiedMd ? (
|
|
177
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
178
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
179
|
+
</svg>
|
|
180
|
+
) : (
|
|
181
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
182
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
183
|
+
</svg>
|
|
184
|
+
)}
|
|
185
|
+
</button>
|
|
186
|
+
<button
|
|
187
|
+
onClick={handleCopyCsv}
|
|
188
|
+
title={
|
|
189
|
+
copiedCsv
|
|
190
|
+
? 'Copied as CSV!'
|
|
191
|
+
: visibleRows.length === totalRows
|
|
192
|
+
? 'Copy as CSV'
|
|
193
|
+
: `Copy ${visibleRows.length} row${visibleRows.length === 1 ? '' : 's'} as CSV`
|
|
194
|
+
}
|
|
195
|
+
className={`px-2 py-1 rounded-md text-[10px] font-bold tracking-tight uppercase leading-none transition-colors ${
|
|
196
|
+
copiedCsv ? 'text-success' : 'text-primary hover:bg-primary/10'
|
|
197
|
+
}`}
|
|
198
|
+
>
|
|
199
|
+
{copiedCsv ? '✓' : 'CSV'}
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div className="overflow-auto flex-1 px-5 pb-5">
|
|
205
|
+
<table className="min-w-full border-collapse text-sm">
|
|
206
|
+
<thead>
|
|
207
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
208
|
+
<tr key={headerGroup.id} className="border-b border-border">
|
|
209
|
+
{headerGroup.headers.map((header) => {
|
|
210
|
+
const sort = header.column.getIsSorted();
|
|
211
|
+
return (
|
|
212
|
+
<th
|
|
213
|
+
key={header.id}
|
|
214
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
215
|
+
className="px-3 py-2 text-left font-semibold text-foreground/90 bg-muted/30 sticky top-0 z-10 select-none cursor-pointer hover:bg-muted/50 transition-colors"
|
|
216
|
+
>
|
|
217
|
+
<span className="inline-flex items-center gap-1.5">
|
|
218
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
219
|
+
<SortIndicator dir={sort} />
|
|
220
|
+
</span>
|
|
221
|
+
</th>
|
|
222
|
+
);
|
|
223
|
+
})}
|
|
224
|
+
</tr>
|
|
225
|
+
))}
|
|
226
|
+
</thead>
|
|
227
|
+
<tbody>
|
|
228
|
+
{visibleRows.length === 0 ? (
|
|
229
|
+
<tr>
|
|
230
|
+
<td colSpan={columnIds.length} className="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
231
|
+
No rows match the filter.
|
|
232
|
+
</td>
|
|
233
|
+
</tr>
|
|
234
|
+
) : (
|
|
235
|
+
visibleRows.map((row) => (
|
|
236
|
+
<tr key={row.id} className="border-b border-border/50 hover:bg-muted/20">
|
|
237
|
+
{row.getVisibleCells().map((cell) => (
|
|
238
|
+
<td key={cell.id} className="px-3 py-2 text-foreground/80">
|
|
239
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
240
|
+
</td>
|
|
241
|
+
))}
|
|
242
|
+
</tr>
|
|
243
|
+
))
|
|
244
|
+
)}
|
|
245
|
+
</tbody>
|
|
246
|
+
</table>
|
|
247
|
+
</div>
|
|
248
|
+
</PopoutDialog>
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Memoize on meaningful props (block identity, content, open, container).
|
|
253
|
+
// Upstream Viewer re-renders (annotation toolbar opening, selection change,
|
|
254
|
+
// hover state shuffling) keep firing while the popout is mounted. Without
|
|
255
|
+
// this memo, TanStack's flexRender re-evaluates every cell on every parent
|
|
256
|
+
// re-render, which conflicts with web-highlighter's DOM mutations (the
|
|
257
|
+
// library inserts <mark> tags into the live DOM) and React's reconciler
|
|
258
|
+
// throws NotFoundError trying to remove nodes it doesn't own.
|
|
259
|
+
export const TablePopout = React.memo(
|
|
260
|
+
TablePopoutImpl,
|
|
261
|
+
(prev, next) =>
|
|
262
|
+
prev.block.id === next.block.id &&
|
|
263
|
+
prev.block.content === next.block.content &&
|
|
264
|
+
prev.open === next.open &&
|
|
265
|
+
prev.container === next.container &&
|
|
266
|
+
prev.imageBaseDir === next.imageBaseDir &&
|
|
267
|
+
prev.githubRepo === next.githubRepo,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const SortIndicator: React.FC<{ dir: false | 'asc' | 'desc' }> = ({ dir }) => {
|
|
271
|
+
const activeUp = dir === 'asc';
|
|
272
|
+
const activeDown = dir === 'desc';
|
|
273
|
+
return (
|
|
274
|
+
<span className="inline-flex flex-col leading-none text-[9px]">
|
|
275
|
+
<span className={activeUp ? 'text-foreground' : 'text-muted-foreground/40'}>▲</span>
|
|
276
|
+
<span className={activeDown ? 'text-foreground' : 'text-muted-foreground/40'}>▼</span>
|
|
277
|
+
</span>
|
|
278
|
+
);
|
|
279
|
+
};
|