@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,101 @@
|
|
|
1
|
+
import type { Block, Annotation } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculate annotation counts per section (grouped by headings)
|
|
5
|
+
* A section includes all blocks from a heading until the next heading of same/higher level
|
|
6
|
+
*/
|
|
7
|
+
export function getAnnotationCountBySection(
|
|
8
|
+
blocks: Block[],
|
|
9
|
+
annotations: Annotation[]
|
|
10
|
+
): Map<string, number> {
|
|
11
|
+
const counts = new Map<string, number>();
|
|
12
|
+
|
|
13
|
+
// Find all headings
|
|
14
|
+
const headings = blocks.filter(b => b.type === 'heading' && (b.level ?? 0) <= 3);
|
|
15
|
+
|
|
16
|
+
if (headings.length === 0) return counts;
|
|
17
|
+
|
|
18
|
+
// For each heading, determine which blocks belong to its section
|
|
19
|
+
for (let i = 0; i < headings.length; i++) {
|
|
20
|
+
const heading = headings[i];
|
|
21
|
+
const currentLevel = heading.level ?? 1;
|
|
22
|
+
const startLine = heading.startLine;
|
|
23
|
+
|
|
24
|
+
// Find the end of this section (next heading of same/higher level)
|
|
25
|
+
let endLine = Infinity;
|
|
26
|
+
for (let j = i + 1; j < headings.length; j++) {
|
|
27
|
+
const nextHeading = headings[j];
|
|
28
|
+
const nextLevel = nextHeading.level ?? 1;
|
|
29
|
+
if (nextLevel <= currentLevel) {
|
|
30
|
+
endLine = nextHeading.startLine;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Count annotations in blocks within this section
|
|
36
|
+
let count = 0;
|
|
37
|
+
for (const block of blocks) {
|
|
38
|
+
if (block.startLine >= startLine && block.startLine < endLine) {
|
|
39
|
+
// Count annotations that belong to this block
|
|
40
|
+
const blockAnnotations = annotations.filter(a => a.blockId === block.id);
|
|
41
|
+
count += blockAnnotations.length;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
counts.set(heading.id, count);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return counts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build hierarchical TOC structure from flat blocks array
|
|
53
|
+
*/
|
|
54
|
+
export interface TocItem {
|
|
55
|
+
id: string;
|
|
56
|
+
content: string;
|
|
57
|
+
level: number;
|
|
58
|
+
order: number;
|
|
59
|
+
children: TocItem[];
|
|
60
|
+
annotationCount: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildTocHierarchy(
|
|
64
|
+
blocks: Block[],
|
|
65
|
+
annotationCounts: Map<string, number>
|
|
66
|
+
): TocItem[] {
|
|
67
|
+
const headings = blocks
|
|
68
|
+
.filter(b => b.type === 'heading' && (b.level ?? 0) <= 3)
|
|
69
|
+
.sort((a, b) => a.order - b.order);
|
|
70
|
+
|
|
71
|
+
const root: TocItem[] = [];
|
|
72
|
+
const stack: TocItem[] = [];
|
|
73
|
+
|
|
74
|
+
for (const heading of headings) {
|
|
75
|
+
const item: TocItem = {
|
|
76
|
+
id: heading.id,
|
|
77
|
+
content: heading.content,
|
|
78
|
+
level: heading.level ?? 1,
|
|
79
|
+
order: heading.order,
|
|
80
|
+
children: [],
|
|
81
|
+
annotationCount: annotationCounts.get(heading.id) ?? 0,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Find the correct parent based on level
|
|
85
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= item.level) {
|
|
86
|
+
stack.pop();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (stack.length === 0) {
|
|
90
|
+
// Top-level item
|
|
91
|
+
root.push(item);
|
|
92
|
+
} else {
|
|
93
|
+
// Child of the last item in stack
|
|
94
|
+
stack[stack.length - 1].children.push(item);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
stack.push(item);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return root;
|
|
101
|
+
}
|
package/utils/bear.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bear Notes Integration Utility
|
|
3
|
+
*
|
|
4
|
+
* Manages settings for auto-saving plans to Bear.
|
|
5
|
+
* Uses x-callback-url protocol - no vault detection needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { storage } from './storage';
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY_ENABLED = 'plannotator-bear-enabled';
|
|
11
|
+
const STORAGE_KEY_CUSTOM_TAGS = 'plannotator-bear-custom-tags';
|
|
12
|
+
const STORAGE_KEY_TAG_POSITION = 'plannotator-bear-tag-position';
|
|
13
|
+
const STORAGE_KEY_AUTOSAVE = 'plannotator-bear-autosave';
|
|
14
|
+
|
|
15
|
+
export type TagPosition = 'prepend' | 'append';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Bear integration settings
|
|
19
|
+
*/
|
|
20
|
+
export interface BearSettings {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
customTags: string;
|
|
23
|
+
tagPosition: TagPosition;
|
|
24
|
+
autoSave: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get current Bear settings from storage
|
|
29
|
+
*/
|
|
30
|
+
export function getBearSettings(): BearSettings {
|
|
31
|
+
return {
|
|
32
|
+
enabled: storage.getItem(STORAGE_KEY_ENABLED) === 'true',
|
|
33
|
+
customTags: storage.getItem(STORAGE_KEY_CUSTOM_TAGS) ?? '',
|
|
34
|
+
tagPosition: (storage.getItem(STORAGE_KEY_TAG_POSITION) as TagPosition) || 'append',
|
|
35
|
+
autoSave: storage.getItem(STORAGE_KEY_AUTOSAVE) === 'true',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Save Bear settings to storage
|
|
41
|
+
*/
|
|
42
|
+
export function saveBearSettings(settings: BearSettings): void {
|
|
43
|
+
storage.setItem(STORAGE_KEY_ENABLED, String(settings.enabled));
|
|
44
|
+
storage.setItem(STORAGE_KEY_CUSTOM_TAGS, settings.customTags);
|
|
45
|
+
storage.setItem(STORAGE_KEY_TAG_POSITION, settings.tagPosition);
|
|
46
|
+
storage.setItem(STORAGE_KEY_AUTOSAVE, String(settings.autoSave));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Normalize raw tag input to clean kebab-case, comma-separated string.
|
|
51
|
+
* Strips # prefixes, lowercases, replaces spaces with hyphens, removes invalid chars.
|
|
52
|
+
*/
|
|
53
|
+
export function normalizeTags(raw: string): string {
|
|
54
|
+
return raw
|
|
55
|
+
.split(',')
|
|
56
|
+
.map(t => t.trim().replace(/^#+/, '').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9\-\/]/g, '').replace(/\/+/g, '/').replace(/^\/|\/$/g, ''))
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.join(', ');
|
|
59
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block Targeting — resolves which element to annotate in pinpoint mode.
|
|
3
|
+
*
|
|
4
|
+
* Walks from the element under the cursor upward through the block tree
|
|
5
|
+
* to find the most specific targetable element (inline, cell, or block).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Elements that should never be targeted */
|
|
9
|
+
const SKIP_SELECTORS = [
|
|
10
|
+
'.annotation-toolbar',
|
|
11
|
+
'.annotation-highlight',
|
|
12
|
+
'mark[data-bind-id]',
|
|
13
|
+
'button',
|
|
14
|
+
'[data-pinpoint-ignore]',
|
|
15
|
+
].join(',');
|
|
16
|
+
|
|
17
|
+
/** Inline elements that are individually targetable within a block */
|
|
18
|
+
const INLINE_TARGETS = new Set(['STRONG', 'EM', 'A']);
|
|
19
|
+
|
|
20
|
+
/** Table cell elements */
|
|
21
|
+
const CELL_TARGETS = new Set(['TD', 'TH']);
|
|
22
|
+
|
|
23
|
+
export interface PinpointTarget {
|
|
24
|
+
/** The DOM element to highlight and select */
|
|
25
|
+
element: HTMLElement;
|
|
26
|
+
/** The data-block-id of the parent block */
|
|
27
|
+
blockId: string;
|
|
28
|
+
/** Human-readable label for the hover tooltip */
|
|
29
|
+
label: string;
|
|
30
|
+
/** Whether this is a code block (needs special annotation path) */
|
|
31
|
+
isCodeBlock: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Edge-zone threshold for table hover (px). Covers the outermost cell padding
|
|
35
|
+
* area (cells have px-3/py-2 = 12px/8px padding) so you need to aim at actual
|
|
36
|
+
* text content to target a specific cell. */
|
|
37
|
+
const TABLE_EDGE_ZONE = 22;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Given a mousemove/click target element, find the best annotation target
|
|
41
|
+
* within the viewer container. Optionally accepts mouse coordinates for
|
|
42
|
+
* edge-zone detection (tables).
|
|
43
|
+
*/
|
|
44
|
+
export function resolvePinpointTarget(
|
|
45
|
+
target: HTMLElement,
|
|
46
|
+
container: HTMLElement,
|
|
47
|
+
mousePos?: { clientX: number; clientY: number },
|
|
48
|
+
): PinpointTarget | null {
|
|
49
|
+
// Skip toolbar, buttons, existing annotations
|
|
50
|
+
if (target.closest(SKIP_SELECTORS)) return null;
|
|
51
|
+
if (!container.contains(target)) return null;
|
|
52
|
+
|
|
53
|
+
// Group detection: cursor is in the gap/gutter of a list group wrapper
|
|
54
|
+
const groupEl = target.closest('[data-pinpoint-group]') as HTMLElement | null;
|
|
55
|
+
if (groupEl && container.contains(groupEl) && !target.closest('[data-block-id]')) {
|
|
56
|
+
const groupType = groupEl.getAttribute('data-pinpoint-group');
|
|
57
|
+
const label = groupType === 'list' ? 'list' : groupType === 'blockquote' ? 'blockquote group' : 'group';
|
|
58
|
+
return { element: groupEl, blockId: '', label, isCodeBlock: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Find the parent block
|
|
62
|
+
const blockEl = target.closest('[data-block-id]') as HTMLElement | null;
|
|
63
|
+
if (!blockEl || !container.contains(blockEl)) return null;
|
|
64
|
+
|
|
65
|
+
const blockId = blockEl.getAttribute('data-block-id')!;
|
|
66
|
+
|
|
67
|
+
// Skip hr (no text content)
|
|
68
|
+
if (blockEl.tagName === 'HR') return null;
|
|
69
|
+
|
|
70
|
+
// Code block detection: pre > code.hljs
|
|
71
|
+
const codeEl = blockEl.querySelector('pre > code.hljs');
|
|
72
|
+
if (codeEl && (target === codeEl || codeEl.contains(target) || target.closest('pre'))) {
|
|
73
|
+
return {
|
|
74
|
+
element: blockEl,
|
|
75
|
+
blockId,
|
|
76
|
+
label: getCodeBlockLabel(blockEl),
|
|
77
|
+
isCodeBlock: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Table edge-zone detection: edges target whole table or row
|
|
82
|
+
const tableEl = blockEl.querySelector('table');
|
|
83
|
+
if (tableEl && mousePos) {
|
|
84
|
+
const tableRect = tableEl.getBoundingClientRect();
|
|
85
|
+
const nearLeft = mousePos.clientX - tableRect.left < TABLE_EDGE_ZONE;
|
|
86
|
+
const nearRight = tableRect.right - mousePos.clientX < TABLE_EDGE_ZONE;
|
|
87
|
+
const nearTop = mousePos.clientY - tableRect.top < TABLE_EDGE_ZONE;
|
|
88
|
+
const nearBottom = tableRect.bottom - mousePos.clientY < TABLE_EDGE_ZONE;
|
|
89
|
+
|
|
90
|
+
// Top/bottom edge → whole table
|
|
91
|
+
if (nearTop || nearBottom) {
|
|
92
|
+
return { element: blockEl, blockId, label: 'table', isCodeBlock: false };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Left/right edge → the row at this Y position
|
|
96
|
+
if (nearLeft || nearRight) {
|
|
97
|
+
const row = findRowAtY(tableEl, mousePos.clientY);
|
|
98
|
+
if (row) {
|
|
99
|
+
return { element: row, blockId, label: getRowLabel(row), isCodeBlock: false };
|
|
100
|
+
}
|
|
101
|
+
return { element: blockEl, blockId, label: 'table', isCodeBlock: false };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Inline code (not inside a code block) — target the <code> element
|
|
106
|
+
if (target.tagName === 'CODE' && !target.classList.contains('hljs')) {
|
|
107
|
+
const text = target.textContent?.trim() || '';
|
|
108
|
+
if (text) {
|
|
109
|
+
return {
|
|
110
|
+
element: target,
|
|
111
|
+
blockId,
|
|
112
|
+
label: `code: \`${truncate(text, 30)}\``,
|
|
113
|
+
isCodeBlock: false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Inline elements: strong, em, a
|
|
119
|
+
if (INLINE_TARGETS.has(target.tagName)) {
|
|
120
|
+
const text = target.textContent?.trim() || '';
|
|
121
|
+
if (text) {
|
|
122
|
+
return {
|
|
123
|
+
element: target,
|
|
124
|
+
blockId,
|
|
125
|
+
label: getInlineLabel(target, text),
|
|
126
|
+
isCodeBlock: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Table cells (only reached when cursor is deep inside, not near edge)
|
|
132
|
+
if (CELL_TARGETS.has(target.tagName)) {
|
|
133
|
+
return {
|
|
134
|
+
element: target,
|
|
135
|
+
blockId,
|
|
136
|
+
label: 'table cell',
|
|
137
|
+
isCodeBlock: false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Check if inside a table cell
|
|
141
|
+
const cell = target.closest('td, th') as HTMLElement | null;
|
|
142
|
+
if (cell && blockEl.contains(cell)) {
|
|
143
|
+
return {
|
|
144
|
+
element: cell,
|
|
145
|
+
blockId,
|
|
146
|
+
label: 'table cell',
|
|
147
|
+
isCodeBlock: false,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// List item — target the content span (second child), not the bullet
|
|
152
|
+
if (blockEl.querySelector('.select-none')) {
|
|
153
|
+
// This is a list item with a bullet. Find the content span.
|
|
154
|
+
const contentSpan = blockEl.children[1] as HTMLElement | undefined;
|
|
155
|
+
if (contentSpan && (contentSpan === target || contentSpan.contains(target))) {
|
|
156
|
+
return {
|
|
157
|
+
element: contentSpan,
|
|
158
|
+
blockId,
|
|
159
|
+
label: getListItemLabel(contentSpan),
|
|
160
|
+
isCodeBlock: false,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Clicked on the bullet area — still target the content span
|
|
164
|
+
if (contentSpan) {
|
|
165
|
+
return {
|
|
166
|
+
element: contentSpan,
|
|
167
|
+
blockId,
|
|
168
|
+
label: getListItemLabel(contentSpan),
|
|
169
|
+
isCodeBlock: false,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Fall back to the full block
|
|
175
|
+
return {
|
|
176
|
+
element: blockEl,
|
|
177
|
+
blockId,
|
|
178
|
+
label: getBlockLabel(blockEl),
|
|
179
|
+
isCodeBlock: false,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getInlineLabel(el: HTMLElement, text: string): string {
|
|
184
|
+
switch (el.tagName) {
|
|
185
|
+
case 'STRONG': return `bold: "${truncate(text, 30)}"`;
|
|
186
|
+
case 'EM': return `italic: "${truncate(text, 30)}"`;
|
|
187
|
+
case 'A': return `link: "${truncate(text, 25)}"`;
|
|
188
|
+
default: return truncate(text, 30);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getBlockLabel(el: HTMLElement): string {
|
|
193
|
+
const tag = el.tagName.toLowerCase();
|
|
194
|
+
const text = el.textContent?.trim() || '';
|
|
195
|
+
|
|
196
|
+
if (el.querySelector('table')) return 'table';
|
|
197
|
+
if (el.dataset.blockType === 'heading' || /^h[1-6]$/.test(tag)) {
|
|
198
|
+
return `heading: "${truncate(text, 35)}"`;
|
|
199
|
+
}
|
|
200
|
+
if (tag === 'blockquote') return `blockquote: "${truncate(text, 30)}"`;
|
|
201
|
+
if (tag === 'p') return text ? `paragraph: "${truncate(text, 35)}"` : 'paragraph';
|
|
202
|
+
return truncate(text, 35) || tag;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getListItemLabel(contentSpan: HTMLElement): string {
|
|
206
|
+
const text = contentSpan.textContent?.trim() || '';
|
|
207
|
+
return text ? `list item: "${truncate(text, 30)}"` : 'list item';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getCodeBlockLabel(blockEl: HTMLElement): string {
|
|
211
|
+
const codeEl = blockEl.querySelector('code');
|
|
212
|
+
const lang = codeEl?.className?.match(/language-(\S+)/)?.[1];
|
|
213
|
+
return lang ? `code block (${lang})` : 'code block';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Find the table row whose bounding box contains the given Y coordinate */
|
|
217
|
+
function findRowAtY(tableEl: HTMLTableElement, clientY: number): HTMLTableRowElement | null {
|
|
218
|
+
const rows = tableEl.querySelectorAll('tr');
|
|
219
|
+
for (const row of rows) {
|
|
220
|
+
const rect = row.getBoundingClientRect();
|
|
221
|
+
if (clientY >= rect.top && clientY <= rect.bottom) {
|
|
222
|
+
return row;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Human-readable label for a table row */
|
|
229
|
+
function getRowLabel(row: HTMLTableRowElement): string {
|
|
230
|
+
// Header row
|
|
231
|
+
if (row.querySelector('th')) return 'table header row';
|
|
232
|
+
// Body row — use first cell text as hint
|
|
233
|
+
const firstCell = row.querySelector('td');
|
|
234
|
+
const text = firstCell?.textContent?.trim() || '';
|
|
235
|
+
return text ? `row: "${truncate(text, 25)}"` : 'table row';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function truncate(text: string, max: number): string {
|
|
239
|
+
return text.length > max ? text.slice(0, max) + '...' : text;
|
|
240
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bot callback integration for Plannotator share URLs.
|
|
3
|
+
*
|
|
4
|
+
* When the bot posts a plan URL with ?cb=<encoded_url>&ct=<token>,
|
|
5
|
+
* these utilities parse the config and POST the user's decision back.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Actions the user can trigger via the in-Plannotator callback buttons. */
|
|
9
|
+
export enum CallbackAction {
|
|
10
|
+
Approve = "approve",
|
|
11
|
+
Feedback = "feedback",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Callback config parsed from ?cb=<encoded_url>&ct=<token> in the URL. */
|
|
15
|
+
export interface CallbackConfig {
|
|
16
|
+
callbackUrl: string;
|
|
17
|
+
token: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ToastSuccess { readonly type: 'success'; readonly message: string }
|
|
21
|
+
export interface ToastError { readonly type: 'error'; readonly message: string }
|
|
22
|
+
export type ToastPayload = ToastSuccess | ToastError | null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse callback configuration from URL search params.
|
|
26
|
+
* Checks both the standard query string (before #) and query-style params
|
|
27
|
+
* embedded in the hash (e.g. `#<hash>?cb=...&ct=...`).
|
|
28
|
+
*
|
|
29
|
+
* The decoded `cb` value must use http: or https: — other schemes (file://, javascript://, etc.)
|
|
30
|
+
* are rejected to prevent protocol-based attacks.
|
|
31
|
+
*
|
|
32
|
+
* @param loc - Location object to parse (defaults to window.location; injectable for tests)
|
|
33
|
+
* @returns CallbackConfig if both cb and ct are present and valid, null otherwise
|
|
34
|
+
*/
|
|
35
|
+
export function getCallbackConfig(
|
|
36
|
+
loc: { readonly search: string; readonly hash: string } = window.location,
|
|
37
|
+
): CallbackConfig | null {
|
|
38
|
+
const searchParams = new URLSearchParams(loc.search);
|
|
39
|
+
let cb = searchParams.get("cb");
|
|
40
|
+
let ct = searchParams.get("ct");
|
|
41
|
+
|
|
42
|
+
if (!cb || !ct) {
|
|
43
|
+
const qIdx = loc.hash.indexOf("?");
|
|
44
|
+
if (qIdx !== -1) {
|
|
45
|
+
const hashParams = new URLSearchParams(loc.hash.slice(qIdx + 1));
|
|
46
|
+
cb = cb ?? hashParams.get("cb");
|
|
47
|
+
ct = ct ?? hashParams.get("ct");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!cb || !ct) return null;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const { protocol } = new URL(cb);
|
|
55
|
+
if (protocol !== "https:" && protocol !== "http:") return null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null; // malformed URL
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { callbackUrl: cb, token: ct };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Execute a bot callback POST request.
|
|
65
|
+
*
|
|
66
|
+
* Pure function (no React deps) — testable without a DOM.
|
|
67
|
+
*
|
|
68
|
+
* @param action - The action to send
|
|
69
|
+
* @param config - Callback URL and single-use token from `getCallbackConfig()`
|
|
70
|
+
* @param annotatedUrl - Current share URL (with annotations encoded); sent to the bot
|
|
71
|
+
* @returns Toast payload to display
|
|
72
|
+
*/
|
|
73
|
+
export async function executeCallback(
|
|
74
|
+
action: CallbackAction,
|
|
75
|
+
config: CallbackConfig,
|
|
76
|
+
annotatedUrl: string,
|
|
77
|
+
): Promise<ToastPayload> {
|
|
78
|
+
const successMsg = action === CallbackAction.Approve
|
|
79
|
+
? "Plan approved! The bot will proceed to implementation."
|
|
80
|
+
: "Feedback sent! The bot will re-plan with your annotations.";
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch(config.callbackUrl, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
body: JSON.stringify({ action, token: config.token, annotated_url: annotatedUrl }),
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
return {
|
|
89
|
+
type: "error",
|
|
90
|
+
message: res.status === 401
|
|
91
|
+
? "Plan link expired — request a new one from the bot."
|
|
92
|
+
: "Callback failed.",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { type: "success", message: successMsg };
|
|
96
|
+
} catch {
|
|
97
|
+
return { type: "error", message: "Callback failed." };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Notes App Preference
|
|
3
|
+
*
|
|
4
|
+
* Stores the user's preferred notes app for the Cmd/Ctrl+S shortcut.
|
|
5
|
+
* Uses cookies (not localStorage) because each hook invocation runs on a random port.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { storage } from './storage';
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY = 'plannotator-default-notes-app';
|
|
11
|
+
|
|
12
|
+
export type DefaultNotesApp = 'obsidian' | 'bear' | 'octarine' | 'download' | 'ask';
|
|
13
|
+
|
|
14
|
+
export function getDefaultNotesApp(): DefaultNotesApp {
|
|
15
|
+
return (storage.getItem(STORAGE_KEY) as DefaultNotesApp) || 'ask';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function saveDefaultNotesApp(app: DefaultNotesApp): void {
|
|
19
|
+
storage.setItem(STORAGE_KEY, app);
|
|
20
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic font loading for code review diff viewer.
|
|
3
|
+
*
|
|
4
|
+
* Injects Google Fonts / CDN stylesheet links on demand when the user
|
|
5
|
+
* selects a custom diff font. Each font is loaded at most once.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const FONT_URLS: Record<string, string> = {
|
|
9
|
+
'Red Hat Mono': 'https://fonts.googleapis.com/css2?family=Red+Hat+Mono:wght@300..700&display=swap',
|
|
10
|
+
'Fira Code': 'https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap',
|
|
11
|
+
'Atkinson Hyperlegible Mono': 'https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:wght@200..700&display=swap',
|
|
12
|
+
'Source Code Pro': 'https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@300..700&display=swap',
|
|
13
|
+
'JetBrains Mono': 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300..700&display=swap',
|
|
14
|
+
'IBM Plex Mono': 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300..700&display=swap',
|
|
15
|
+
'Inconsolata': 'https://fonts.googleapis.com/css2?family=Inconsolata:wght@300..700&display=swap',
|
|
16
|
+
'Roboto Mono': 'https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300..700&display=swap',
|
|
17
|
+
'Hack': 'https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const loaded = new Set<string>();
|
|
21
|
+
|
|
22
|
+
export function loadDiffFont(fontFamily: string): void {
|
|
23
|
+
if (!fontFamily || loaded.has(fontFamily)) return;
|
|
24
|
+
const url = FONT_URLS[fontFamily];
|
|
25
|
+
if (!url) return;
|
|
26
|
+
|
|
27
|
+
const link = document.createElement('link');
|
|
28
|
+
link.rel = 'stylesheet';
|
|
29
|
+
link.href = url;
|
|
30
|
+
link.dataset.diffFont = fontFamily;
|
|
31
|
+
document.head.appendChild(link);
|
|
32
|
+
loaded.add(fontFamily);
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor Mode Settings Utility
|
|
3
|
+
*
|
|
4
|
+
* Persists the last-used editor mode (selection, comment, redline) so it
|
|
5
|
+
* carries over between plan reviews.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { storage } from './storage';
|
|
9
|
+
import type { EditorMode } from '../types';
|
|
10
|
+
|
|
11
|
+
const STORAGE_KEY = 'plannotator-editor-mode';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_MODE: EditorMode = 'selection';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the last-used editor mode from storage
|
|
17
|
+
*/
|
|
18
|
+
export function getEditorMode(): EditorMode {
|
|
19
|
+
const stored = storage.getItem(STORAGE_KEY);
|
|
20
|
+
if (stored === 'selection' || stored === 'comment' || stored === 'redline' || stored === 'quickLabel') {
|
|
21
|
+
return stored;
|
|
22
|
+
}
|
|
23
|
+
return DEFAULT_MODE;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Save editor mode to storage
|
|
28
|
+
*/
|
|
29
|
+
export function saveEditorMode(mode: EditorMode): void {
|
|
30
|
+
storage.setItem(STORAGE_KEY, mode);
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Browser Settings
|
|
3
|
+
*
|
|
4
|
+
* Manages settings for the markdown file browser sidebar tab.
|
|
5
|
+
* Users add directories to browse; settings persist via cookies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { storage } from './storage';
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY_ENABLED = 'plannotator-filebrowser-enabled';
|
|
11
|
+
const STORAGE_KEY_DIRS = 'plannotator-filebrowser-dirs';
|
|
12
|
+
|
|
13
|
+
export interface FileBrowserSettings {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
directories: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getFileBrowserSettings(): FileBrowserSettings {
|
|
19
|
+
const dirsRaw = storage.getItem(STORAGE_KEY_DIRS);
|
|
20
|
+
let directories: string[] = [];
|
|
21
|
+
if (dirsRaw) {
|
|
22
|
+
try {
|
|
23
|
+
directories = JSON.parse(dirsRaw);
|
|
24
|
+
} catch { /* ignore corrupt cookie */ }
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
enabled: storage.getItem(STORAGE_KEY_ENABLED) === 'true',
|
|
28
|
+
directories,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function saveFileBrowserSettings(settings: FileBrowserSettings): void {
|
|
33
|
+
storage.setItem(STORAGE_KEY_ENABLED, String(settings.enabled));
|
|
34
|
+
storage.setItem(STORAGE_KEY_DIRS, JSON.stringify(settings.directories));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isFileBrowserEnabled(): boolean {
|
|
38
|
+
const settings = getFileBrowserSettings();
|
|
39
|
+
return settings.enabled && settings.directories.length > 0;
|
|
40
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tater name generator — pure function, no storage dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Extracted to its own module to avoid circular imports:
|
|
5
|
+
* settings.ts needs this for the default value, and identity.ts
|
|
6
|
+
* needs configStore (which imports settings.ts).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { uniqueUsernameGenerator, adjectives, nouns } from 'unique-username-generator';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a new random tater identity.
|
|
13
|
+
* Format: {adjective}-{noun}-tater
|
|
14
|
+
* Examples: "swift-falcon-tater", "gentle-crystal-tater"
|
|
15
|
+
*/
|
|
16
|
+
export function generateIdentity(): string {
|
|
17
|
+
// Use a unique separator to split adjective from noun, avoiding issues
|
|
18
|
+
// with compound words that contain hyphens (e.g., "behind-the-scenes")
|
|
19
|
+
const generated = uniqueUsernameGenerator({
|
|
20
|
+
dictionaries: [adjectives, nouns],
|
|
21
|
+
separator: '|||',
|
|
22
|
+
style: 'lowerCase',
|
|
23
|
+
randomDigits: 0,
|
|
24
|
+
length: 50, // Prevent word truncation (default is too short)
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const [adjective, noun] = generated.split('|||');
|
|
28
|
+
return `${adjective}-${noun}-tater`;
|
|
29
|
+
}
|