@mrsf/markdown-it-mrsf 0.3.0 → 0.4.1

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/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/index.ts", "../../shared/src/comments.ts", "../../shared/src/html.ts", "../src/rules/core.ts", "../src/rules/renderer.ts", "../src/shared.ts"],
4
- "sourcesContent": ["/**\n * @mrsf/markdown-it-mrsf \u2014 markdown-it plugin for MRSF review comments.\n *\n * This is the full-featured Node.js entry point. It supports all four\n * sidecar provisioning modes: `comments`, `loader`, `sidecarPath`, and\n * `documentPath`. The latter two require Node.js file-system APIs.\n *\n * In browser / bundler environments, the `\"browser\"` export condition in\n * package.json automatically resolves to the browser-safe entry, so you\n * never need to import a different path \u2014 just use:\n *\n * @example\n * ```ts\n * import MarkdownIt from \"markdown-it\";\n * import { mrsfPlugin } from \"@mrsf/markdown-it-mrsf\";\n *\n * const md = new MarkdownIt();\n * md.use(mrsfPlugin, { documentPath: \"docs/guide.md\" });\n * const html = md.render(markdownSource);\n * ```\n */\n\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { parseSidecarContent } from \"@mrsf/cli\";\nimport type { MrsfPluginOptions } from \"./types.js\";\nimport { createMrsfPlugin } from \"./shared.js\";\n\n// Re-export types for consumers\nexport type { MrsfPluginOptions, SlimComment, CommentThread, LineMap, CommentLoader } from \"./types.js\";\n\n/**\n * The markdown-it plugin function (Node.js \u2014 full feature set).\n */\nexport const mrsfPlugin = createMrsfPlugin((options: MrsfPluginOptions) => {\n // Priority 1: inline data\n if (options.comments) {\n return options.comments;\n }\n\n // Priority 2: custom loader function\n if (options.loader) {\n try {\n return options.loader();\n } catch {\n return null;\n }\n }\n\n // Priority 3: explicit sidecar path\n if (options.sidecarPath) {\n try {\n const abs = path.resolve(options.cwd || process.cwd(), options.sidecarPath);\n const content = readFileSync(abs, \"utf-8\");\n return parseSidecarContent(content, abs);\n } catch {\n return null;\n }\n }\n\n // Priority 4: auto-discover from documentPath\n if (options.documentPath) {\n try {\n const cwd = options.cwd || process.cwd();\n const abs = path.resolve(cwd, options.documentPath);\n const yamlPath = abs + \".review.yaml\";\n const jsonPath = abs + \".review.json\";\n\n let raw: string | null = null;\n let hint: string | null = null;\n try {\n raw = readFileSync(yamlPath, \"utf-8\");\n hint = yamlPath;\n } catch {\n try {\n raw = readFileSync(jsonPath, \"utf-8\");\n hint = jsonPath;\n } catch {\n return null;\n }\n }\n\n if (!raw) return null;\n return parseSidecarContent(raw, hint!);\n } catch {\n return null;\n }\n }\n\n return null;\n});\n\nexport default mrsfPlugin;\n", "/**\n * Shared comment loading and grouping logic for MRSF rendering plugins.\n */\n\nimport type { MrsfDocument } from \"@mrsf/cli\";\nimport type { MrsfPluginOptions, SlimComment, CommentThread, LineMap, CommentLoader } from \"./types.js\";\n\nexport type { CommentLoader };\n\n/**\n * Convert an MrsfDocument into a slim comment array.\n */\nexport function toSlimComments(doc: MrsfDocument): SlimComment[] {\n return doc.comments.map((c) => ({\n id: c.id,\n author: c.author || \"Unknown\",\n text: c.text || \"\",\n line: c.line ?? null,\n end_line: c.end_line ?? null,\n start_column: c.start_column ?? null,\n end_column: c.end_column ?? null,\n selected_text: c.selected_text || null,\n resolved: !!c.resolved,\n reply_to: c.reply_to || null,\n severity: c.severity || null,\n type: c.type || null,\n timestamp: c.timestamp || null,\n }));\n}\n\n/**\n * Group comments by line number, threading replies under parents.\n */\nexport function groupByLine(comments: SlimComment[]): LineMap {\n const rootComments = comments.filter((c) => !c.reply_to && c.line != null);\n const replies = comments.filter((c) => c.reply_to);\n\n const replyMap = new Map<string, SlimComment[]>();\n for (const r of replies) {\n const list = replyMap.get(r.reply_to!) || [];\n list.push(r);\n replyMap.set(r.reply_to!, list);\n }\n\n const lineMap: LineMap = new Map();\n for (const c of rootComments) {\n const line = c.line!;\n const threads = lineMap.get(line) || [];\n threads.push({\n comment: c,\n replies: replyMap.get(c.id) || [],\n });\n lineMap.set(line, threads);\n }\n\n return lineMap;\n}\n\n/**\n * Resolve options into a filtered LineMap ready for rendering.\n * Returns null if there are no comments to render.\n */\nexport function resolveComments(\n loader: CommentLoader,\n options: MrsfPluginOptions,\n): { lineMap: LineMap; comments: SlimComment[] } | null {\n const showResolved = options.showResolved ?? true;\n\n const doc = loader(options);\n if (!doc || !doc.comments || doc.comments.length === 0) {\n return null;\n }\n\n let comments = toSlimComments(doc);\n if (!showResolved) {\n comments = comments.filter((c) => !c.resolved);\n }\n\n if (comments.length === 0) return null;\n\n const lineMap = groupByLine(comments);\n return { lineMap, comments };\n}\n", "/**\n * Shared HTML rendering helpers for MRSF rendering plugins.\n *\n * These produce the HTML strings used in tooltips, badges, and comment\n * rendering. Both the markdown-it and rehype plugins use these functions\n * to ensure identical visual output.\n */\n\nimport type { CommentThread, SlimComment } from \"./types.js\";\n\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\nexport function formatTime(iso: string | null): string {\n if (!iso) return \"\";\n try {\n const d = new Date(iso);\n return d.toLocaleDateString(undefined, {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n });\n } catch {\n return \"\";\n }\n}\n\nexport function renderCommentHtml(\n comment: SlimComment,\n isReply: boolean,\n interactive: boolean,\n): string {\n const resolvedClass = comment.resolved ? \" mrsf-resolved\" : \"\";\n const replyClass = isReply ? \" mrsf-reply\" : \"\";\n let html = `<span class=\"mrsf-comment${resolvedClass}${replyClass}\" data-mrsf-comment-id=\"${escapeHtml(comment.id)}\">`;\n\n // Header\n html += `<span class=\"mrsf-comment-header\">`;\n html += `<span class=\"mrsf-author\">${escapeHtml(comment.author)}</span>`;\n if (comment.timestamp) {\n html += `<span class=\"mrsf-date\">${escapeHtml(formatTime(comment.timestamp))}</span>`;\n }\n if (comment.severity) {\n html += `<span class=\"mrsf-severity mrsf-severity-${escapeHtml(comment.severity)}\">${escapeHtml(comment.severity)}</span>`;\n }\n if (comment.type) {\n html += `<span class=\"mrsf-type\">${escapeHtml(comment.type)}</span>`;\n }\n if (comment.resolved) {\n html += `<span class=\"mrsf-resolved-badge\">\u2713 resolved</span>`;\n }\n html += `</span>`;\n\n // Selected text quote\n if (comment.selected_text) {\n html += `<span class=\"mrsf-selected-text\">${escapeHtml(comment.selected_text)}</span>`;\n }\n\n // Body\n html += `<span class=\"mrsf-comment-body\">${escapeHtml(comment.text)}</span>`;\n\n // Action buttons (interactive mode)\n if (interactive) {\n const line = comment.line != null ? String(comment.line) : \"\";\n html += `<span class=\"mrsf-actions\">`;\n if (comment.resolved) {\n html += `<button class=\"mrsf-action-btn\" data-mrsf-action=\"unresolve\" data-mrsf-comment-id=\"${escapeHtml(comment.id)}\" data-mrsf-line=\"${line}\">Unresolve</button>`;\n } else {\n html += `<button class=\"mrsf-action-btn\" data-mrsf-action=\"resolve\" data-mrsf-comment-id=\"${escapeHtml(comment.id)}\" data-mrsf-line=\"${line}\">Resolve</button>`;\n }\n html += `<button class=\"mrsf-action-btn\" data-mrsf-action=\"reply\" data-mrsf-comment-id=\"${escapeHtml(comment.id)}\" data-mrsf-line=\"${line}\">Reply</button>`;\n html += `<button class=\"mrsf-action-btn\" data-mrsf-action=\"edit\" data-mrsf-comment-id=\"${escapeHtml(comment.id)}\" data-mrsf-line=\"${line}\">Edit</button>`;\n html += `<button class=\"mrsf-action-btn mrsf-action-danger\" data-mrsf-action=\"delete\" data-mrsf-comment-id=\"${escapeHtml(comment.id)}\" data-mrsf-line=\"${line}\">Delete</button>`;\n html += `</span>`;\n }\n\n html += `</span>`;\n return html;\n}\n\nexport function renderThreadHtml(thread: CommentThread, interactive: boolean): string {\n let html = `<span class=\"mrsf-thread\">`;\n html += renderCommentHtml(thread.comment, false, interactive);\n if (thread.replies.length > 0) {\n html += `<span class=\"mrsf-replies\">`;\n for (const reply of thread.replies) {\n html += renderCommentHtml(reply, true, interactive);\n }\n html += `</span>`;\n }\n html += `</span>`;\n return html;\n}\n", "/**\n * MRSF markdown-it plugin \u2014 core rule.\n *\n * Walks the parsed token stream and injects `mrsf_badge` tokens\n * at lines that have comments, and wraps `selected_text` matches\n * with `mrsf_highlight_open`/`close` tokens (with tooltip metadata).\n */\n\nimport type Token from \"markdown-it/lib/token.mjs\";\nimport type { Nesting } from \"markdown-it/lib/token.mjs\";\nimport type StateCore from \"markdown-it/lib/rules_core/state_core.mjs\";\nimport type { LineMap, CommentThread } from \"../types.js\";\n\n/** Options forwarded from the plugin to the core rule. */\nexport interface CoreOptions {\n interactive: boolean;\n gutterPosition: \"left\" | \"tight\" | \"right\";\n gutterForInline: boolean;\n inlineHighlights: boolean;\n}\n\n/**\n * Find the token index for a given 1-based source line.\n * markdown-it tokens use `token.map = [startLine, endLine]` (0-based).\n */\nfunction findTokenIndexForLine(\n tokens: Token[],\n line: number,\n): number {\n const line0 = line - 1; // convert to 0-based\n for (let i = 0; i < tokens.length; i++) {\n const map = tokens[i].map;\n if (map && map[0] <= line0 && line0 < map[1]) {\n return i;\n }\n }\n return -1;\n}\n\n/**\n * Wrap occurrences of `text` inside inline token children with\n * highlight open/close tokens. Stores thread data on the tokens\n * so the renderer can produce an inline tooltip.\n */\nfunction highlightInlineText(\n token: Token,\n thread: CommentThread,\n interactive: boolean,\n Token: new (type: string, tag: string, nesting: Nesting) => Token,\n): boolean {\n if (!token.children) return false;\n\n const text = thread.comment.selected_text;\n if (!text) return false;\n const trimmed = text.trim();\n if (!trimmed) return false;\n\n for (let i = 0; i < token.children.length; i++) {\n const child = token.children[i];\n if (child.type !== \"text\") continue;\n\n const idx = child.content.indexOf(trimmed);\n if (idx === -1) continue;\n\n // Split the text token into: before + highlight_open + matched + highlight_close + after\n const newTokens: Token[] = [];\n\n // Text before match\n if (idx > 0) {\n const before = new Token(\"text\", \"\", 0);\n before.content = child.content.slice(0, idx);\n newTokens.push(before);\n }\n\n // Highlight open \u2014 carries thread metadata for inline tooltip\n const open = new Token(\"mrsf_highlight_open\", \"mark\", 1);\n open.attrSet(\"class\", \"mrsf-highlight\");\n open.attrSet(\"data-mrsf-comment-id\", thread.comment.id);\n open.meta = { thread };\n newTokens.push(open);\n\n // Matched text\n const matched = new Token(\"text\", \"\", 0);\n matched.content = trimmed;\n newTokens.push(matched);\n\n // Highlight close \u2014 carries thread metadata for inline tooltip\n const close = new Token(\"mrsf_highlight_close\", \"mark\", -1);\n close.meta = { thread, interactive };\n newTokens.push(close);\n\n // Text after match\n if (idx + trimmed.length < child.content.length) {\n const after = new Token(\"text\", \"\", 0);\n after.content = child.content.slice(idx + trimmed.length);\n newTokens.push(after);\n }\n\n // Replace the original child with our new tokens\n token.children.splice(i, 1, ...newTokens);\n return true;\n }\n\n return false;\n}\n\n/**\n * Install the MRSF core rule on a markdown-it instance.\n */\nexport function installCoreRule(\n md: { core: { ruler: { push: (name: string, fn: (state: StateCore) => void) => void } } },\n lineMap: LineMap,\n options: CoreOptions,\n): void {\n const { interactive, gutterPosition, gutterForInline, inlineHighlights } = options;\n\n md.core.ruler.push(\"mrsf_inject\", (state: StateCore) => {\n const tokens = state.tokens;\n const TokenCtor = state.Token;\n\n // Track which lines we've already processed (to avoid double-injection\n // when multiple tokens share the same source line).\n const processed = new Set<number>();\n\n // We iterate backwards so that inserting tokens doesn't shift indices\n // for tokens we haven't visited yet.\n for (let i = tokens.length - 1; i >= 0; i--) {\n const token = tokens[i];\n const map = token.map;\n if (!map) continue;\n\n // Skip inline tokens \u2014 they share the same map as their parent block.\n // We must process the parent block (paragraph_open, heading_open, etc.)\n // so the badge and class land on the correct rendered element.\n if (token.type === \"inline\") continue;\n\n // Always expose source line metadata for selection-based adds\n const startLine1 = map[0] + 1;\n const endLine1 = map[1];\n token.attrSet(\"data-mrsf-line\", String(startLine1));\n token.attrSet(\"data-mrsf-start-line\", String(startLine1));\n token.attrSet(\"data-mrsf-end-line\", String(endLine1));\n\n // Mark line elements as hover targets (for gutter add visibility)\n if (interactive) {\n const existingClass = token.attrGet(\"class\") || \"\";\n token.attrSet(\n \"class\",\n existingClass\n ? `${existingClass} mrsf-line-hover-target`\n : \"mrsf-line-hover-target\",\n );\n }\n\n // Check each line in this token's range for comments\n for (let line0 = map[0]; line0 < map[1]; line0++) {\n const line = line0 + 1; // 1-based\n if (processed.has(line)) continue;\n\n const threads = lineMap.get(line) || [];\n const hasThreads = threads.length > 0;\n processed.add(line);\n\n // Add line-highlight class to the block token if we have threads on this line\n if (hasThreads) {\n const existingClass = token.attrGet(\"class\") || \"\";\n token.attrSet(\n \"class\",\n existingClass\n ? `${existingClass} mrsf-line-highlight`\n : \"mrsf-line-highlight\",\n );\n token.attrSet(\"data-mrsf-line\", String(line));\n\n // Determine whether all threads on this line have inline highlights\n const allHaveInline = inlineHighlights &&\n threads.every((t) => !!t.comment.selected_text);\n\n // Inject badge unless gutterForInline is false AND all threads are inline-highlighted\n const showBadge = gutterForInline || !allHaveInline || !inlineHighlights;\n if (showBadge) {\n const badgeToken = new TokenCtor(\"mrsf_badge\", \"\", 0);\n badgeToken.meta = { line, threads, interactive, gutterPosition };\n tokens.splice(i + 1, 0, badgeToken);\n }\n\n // Highlight selected_text in the following inline token\n if (inlineHighlights) {\n const offset = showBadge ? 2 : 1;\n const inlineAfter = tokens[i + offset];\n if (inlineAfter && inlineAfter.type === \"inline\") {\n for (const thread of threads) {\n if (thread.comment.selected_text) {\n highlightInlineText(inlineAfter, thread, interactive, TokenCtor);\n }\n }\n }\n }\n } else if (interactive) {\n // No existing threads: inject a gutter add button for this line\n const addToken = new TokenCtor(\"mrsf_gutter_add\", \"\", 0);\n addToken.meta = { line, gutterPosition };\n tokens.splice(i + 1, 0, addToken);\n }\n }\n }\n\n // Wrap the entire output in a gutter container when using left gutter\n if (gutterPosition === \"left\" && processed.size > 0) {\n const openToken = new TokenCtor(\"mrsf_gutter_open\", \"div\", 1);\n openToken.attrSet(\"class\", \"mrsf-gutter-container\");\n tokens.unshift(openToken);\n const closeToken = new TokenCtor(\"mrsf_gutter_close\", \"div\", -1 as Nesting);\n tokens.push(closeToken);\n }\n });\n}\n", "/**\n * MRSF markdown-it plugin \u2014 renderer rules.\n *\n * Adds custom renderer rules for MRSF tokens:\n * - mrsf_badge: renders badge + tooltip HTML (gutter)\n * - mrsf_highlight_open/close: renders <mark> wrappers with inline tooltips\n */\n\nimport type { CommentThread } from \"../types.js\";\nimport { escapeHtml, renderThreadHtml } from \"@mrsf/plugin-shared\";\n\n/**\n * Install MRSF renderer rules on a markdown-it instance.\n */\nexport function installRendererRules(\n md: { renderer: { rules: Record<string, ((...args: any[]) => string) | undefined> } },\n): void {\n // Badge + tooltip (gutter)\n md.renderer.rules[\"mrsf_badge\"] = (\n tokens: { meta: { line: number; threads: CommentThread[]; interactive: boolean; gutterPosition: \"left\" | \"tight\" | \"right\" } }[],\n idx: number,\n ): string => {\n const { line, threads, interactive, gutterPosition } = tokens[idx].meta;\n\n const total = threads.reduce(\n (n, t) => n + 1 + t.replies.length,\n 0,\n );\n const allResolved = threads.every((t) => t.comment.resolved);\n const highestSeverity = threads.reduce<string | null>((sev, t) => {\n if (t.comment.severity === \"high\" || sev === \"high\") return \"high\";\n if (t.comment.severity === \"medium\" || sev === \"medium\") return \"medium\";\n if (t.comment.severity === \"low\" || sev === \"low\") return \"low\";\n return sev;\n }, null);\n\n const resolvedClass = allResolved ? \" mrsf-badge-resolved\" : \"\";\n const severityClass = highestSeverity === \"high\" || highestSeverity === \"medium\"\n ? ` mrsf-badge-severity-${highestSeverity}`\n : \"\";\n const positionClass = ` mrsf-gutter-${gutterPosition}`;\n\n const icon = allResolved ? \"\u2713\" : \"\uD83D\uDCAC\";\n\n let html = `<span class=\"mrsf-tooltip-anchor${positionClass}\">`;\n\n // Badge\n html += `<span class=\"mrsf-badge${resolvedClass}${severityClass}\" data-mrsf-line=\"${line}\" data-mrsf-action=\"navigate\" data-mrsf-comment-id=\"${escapeHtml(threads[0].comment.id)}\" tabindex=\"0\">${icon} ${total}</span>`;\n\n if (interactive) {\n html += `<button class=\"mrsf-gutter-add\" data-mrsf-action=\"add\" data-mrsf-line=\"${line}\" data-mrsf-start-line=\"${line}\" data-mrsf-end-line=\"${line}\" aria-label=\"Add comment\">+</button>`;\n }\n\n // Tooltip\n const tooltipClass = interactive ? \"mrsf-tooltip mrsf-interactive\" : \"mrsf-tooltip\";\n html += `<span class=\"${tooltipClass}\" data-mrsf-line=\"${line}\">`;\n for (const thread of threads) {\n html += renderThreadHtml(thread, interactive);\n }\n if (interactive) {\n html += `<span class=\"mrsf-tooltip-actions\"><button class=\"mrsf-action-btn\" data-mrsf-action=\"add\" data-mrsf-line=\"${line}\">Add comment</button></span>`;\n }\n html += `</span>`;\n\n html += `</span>`;\n return html;\n };\n\n // Gutter add button for lines without existing threads\n md.renderer.rules[\"mrsf_gutter_add\"] = (\n tokens: { meta: { line: number; gutterPosition: \"left\" | \"tight\" | \"right\" } }[],\n idx: number,\n ): string => {\n const { line, gutterPosition } = tokens[idx].meta;\n const positionClass = ` mrsf-gutter-${gutterPosition}`;\n let html = `<span class=\"mrsf-tooltip-anchor mrsf-gutter-add-only${positionClass}\">`;\n html += `<button class=\"mrsf-gutter-add\" data-mrsf-action=\"add\" data-mrsf-line=\"${line}\" data-mrsf-start-line=\"${line}\" data-mrsf-end-line=\"${line}\" aria-label=\"Add comment\">+</button>`;\n html += `</span>`;\n return html;\n };\n\n // Highlight open \u2014 wraps in tooltip-anchor for inline hover (feature D)\n md.renderer.rules[\"mrsf_highlight_open\"] = (\n tokens: any[],\n idx: number,\n ): string => {\n const token = tokens[idx];\n const cls = token.attrGet(\"class\") || \"mrsf-highlight\";\n const commentId = token.attrGet(\"data-mrsf-comment-id\") || \"\";\n const thread = token.meta?.thread as CommentThread | undefined;\n\n const line = thread?.comment.line ?? null;\n const endLine = thread?.comment.end_line ?? line ?? null;\n const startCol = thread?.comment.start_column ?? null;\n const endCol = thread?.comment.end_column ?? null;\n\n let html = \"\";\n if (thread) {\n const dataAttrs = [\n line != null ? `data-mrsf-line=\"${line}\"` : \"\",\n line != null ? `data-mrsf-start-line=\"${line}\"` : \"\",\n endLine != null ? `data-mrsf-end-line=\"${endLine}\"` : \"\",\n startCol != null ? `data-mrsf-start-column=\"${startCol}\"` : \"\",\n endCol != null ? `data-mrsf-end-column=\"${endCol}\"` : \"\",\n ].filter(Boolean).join(\" \");\n html += `<span class=\"mrsf-tooltip-anchor mrsf-inline-anchor\" ${dataAttrs}>`;\n }\n html += `<mark class=\"${escapeHtml(cls)}\" data-mrsf-comment-id=\"${escapeHtml(commentId)}\" tabindex=\"0\">`;\n return html;\n };\n\n // Highlight close \u2014 adds inline tooltip and closes tooltip-anchor\n md.renderer.rules[\"mrsf_highlight_close\"] = (\n tokens: any[],\n idx: number,\n ): string => {\n const token = tokens[idx];\n const meta = token.meta;\n let html = \"</mark>\";\n\n if (meta?.thread) {\n const thread = meta.thread as CommentThread;\n const interactive = meta.interactive as boolean;\n const tooltipClass = interactive\n ? \"mrsf-tooltip mrsf-inline-tooltip mrsf-interactive\"\n : \"mrsf-tooltip mrsf-inline-tooltip\";\n html += `<span class=\"${tooltipClass}\">`;\n html += renderThreadHtml(thread, interactive);\n html += `</span>`;\n html += `</span>`;\n }\n\n return html;\n };\n\n // Gutter container wrapper (left gutter mode)\n md.renderer.rules[\"mrsf_gutter_open\"] = (\n tokens: any[],\n idx: number,\n ): string => {\n const cls = tokens[idx].attrGet(\"class\") || \"mrsf-gutter-container\";\n return `<div class=\"${escapeHtml(cls)}\">`;\n };\n\n md.renderer.rules[\"mrsf_gutter_close\"] = (): string => {\n return `</div>`;\n };\n}\n", "/**\n * Shared logic for @mrsf/markdown-it-mrsf.\n *\n * Contains the plugin factory using shared comment loading/grouping logic.\n */\n\nimport type MarkdownIt from \"markdown-it\";\nimport type { MrsfPluginOptions, CommentLoader } from \"./types.js\";\nimport { resolveComments } from \"@mrsf/plugin-shared\";\nimport { installCoreRule } from \"./rules/core.js\";\nimport { installRendererRules } from \"./rules/renderer.js\";\n\n/**\n * Create a markdown-it plugin function with the given comment loader.\n *\n * @param loader - Function that resolves sidecar data from plugin options\n * @returns A markdown-it plugin function\n */\nexport function createMrsfPlugin(loader: CommentLoader) {\n return function mrsfPlugin(md: MarkdownIt, options: MrsfPluginOptions = {}): void {\n const interactive = options.interactive ?? false;\n const gutterPosition = options.gutterPosition ?? \"right\";\n const gutterForInline = options.gutterForInline ?? true;\n const inlineHighlights = options.inlineHighlights ?? true;\n\n const result = resolveComments(loader, options);\n if (!result) return;\n\n installCoreRule(md, result.lineMap, {\n interactive,\n gutterPosition,\n gutterForInline,\n inlineHighlights,\n });\n installRendererRules(md);\n };\n}\n"],
5
- "mappings": "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",
3
+ "sources": ["../src/index.ts", "../../shared/src/comments.ts", "../src/rules/core.ts", "../src/rules/renderer.ts", "../src/shared.ts"],
4
+ "sourcesContent": ["/**\n * @mrsf/markdown-it-mrsf \u2014 markdown-it plugin for MRSF review comments.\n *\n * This is the full-featured Node.js entry point. It supports all four\n * sidecar provisioning modes: `comments`, `loader`, `sidecarPath`, and\n * `documentPath`. The latter two require Node.js file-system APIs.\n *\n * In browser / bundler environments, the `\"browser\"` export condition in\n * package.json automatically resolves to the browser-safe entry, so you\n * never need to import a different path \u2014 just use:\n *\n * @example\n * ```ts\n * import MarkdownIt from \"markdown-it\";\n * import { mrsfPlugin } from \"@mrsf/markdown-it-mrsf\";\n *\n * const md = new MarkdownIt();\n * md.use(mrsfPlugin, { documentPath: \"docs/guide.md\" });\n * const html = md.render(markdownSource);\n * ```\n */\n\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { parseSidecarContent } from \"@mrsf/cli\";\nimport type { MrsfPluginOptions } from \"./types.js\";\nimport { createMrsfPlugin } from \"./shared.js\";\n\n// Re-export types for consumers\nexport type { MrsfPluginOptions, SlimComment, CommentThread, LineMap, CommentLoader } from \"./types.js\";\n\n/**\n * The markdown-it plugin function (Node.js \u2014 full feature set).\n */\nexport const mrsfPlugin = createMrsfPlugin((options: MrsfPluginOptions) => {\n // Priority 1: inline data\n if (options.comments) {\n return options.comments;\n }\n\n // Priority 2: custom loader function\n if (options.loader) {\n try {\n return options.loader();\n } catch {\n return null;\n }\n }\n\n // Priority 3: explicit sidecar path\n if (options.sidecarPath) {\n try {\n const abs = path.resolve(options.cwd || process.cwd(), options.sidecarPath);\n const content = readFileSync(abs, \"utf-8\");\n return parseSidecarContent(content, abs);\n } catch {\n return null;\n }\n }\n\n // Priority 4: auto-discover from documentPath\n if (options.documentPath) {\n try {\n const cwd = options.cwd || process.cwd();\n const abs = path.resolve(cwd, options.documentPath);\n const yamlPath = abs + \".review.yaml\";\n const jsonPath = abs + \".review.json\";\n\n let raw: string | null = null;\n let hint: string | null = null;\n try {\n raw = readFileSync(yamlPath, \"utf-8\");\n hint = yamlPath;\n } catch {\n try {\n raw = readFileSync(jsonPath, \"utf-8\");\n hint = jsonPath;\n } catch {\n return null;\n }\n }\n\n if (!raw) return null;\n return parseSidecarContent(raw, hint!);\n } catch {\n return null;\n }\n }\n\n return null;\n});\n\nexport default mrsfPlugin;\n", "/**\n * Shared comment loading and grouping logic for MRSF rendering plugins.\n */\n\nimport type { MrsfDocument } from \"@mrsf/cli\";\nimport type { MrsfPluginOptions, SlimComment, CommentThread, LineMap, CommentLoader } from \"./types.js\";\n\nexport type { CommentLoader };\n\n/**\n * Convert an MrsfDocument into a slim comment array.\n */\nexport function toSlimComments(doc: MrsfDocument): SlimComment[] {\n return doc.comments.map((c) => ({\n id: c.id,\n author: c.author || \"Unknown\",\n text: c.text || \"\",\n line: c.line ?? null,\n end_line: c.end_line ?? null,\n start_column: c.start_column ?? null,\n end_column: c.end_column ?? null,\n selected_text: c.selected_text || null,\n resolved: !!c.resolved,\n reply_to: c.reply_to || null,\n severity: c.severity || null,\n type: c.type || null,\n timestamp: c.timestamp || null,\n }));\n}\n\n/**\n * Group comments by line number, threading replies under parents.\n */\nexport function groupByLine(comments: SlimComment[]): LineMap {\n const rootComments = comments.filter((c) => !c.reply_to && c.line != null);\n const replies = comments.filter((c) => c.reply_to);\n\n const replyMap = new Map<string, SlimComment[]>();\n for (const r of replies) {\n const list = replyMap.get(r.reply_to!) || [];\n list.push(r);\n replyMap.set(r.reply_to!, list);\n }\n\n const lineMap: LineMap = new Map();\n for (const c of rootComments) {\n const line = c.line!;\n const threads = lineMap.get(line) || [];\n threads.push({\n comment: c,\n replies: replyMap.get(c.id) || [],\n });\n lineMap.set(line, threads);\n }\n\n return lineMap;\n}\n\n/**\n * Resolve options into a filtered LineMap ready for rendering.\n * Returns null if there are no comments to render.\n */\nexport function resolveComments(\n loader: CommentLoader,\n options: MrsfPluginOptions,\n): { lineMap: LineMap; comments: SlimComment[] } | null {\n const showResolved = options.showResolved ?? true;\n\n const doc = loader(options);\n if (!doc || !doc.comments || doc.comments.length === 0) {\n return null;\n }\n\n let comments = toSlimComments(doc);\n if (!showResolved) {\n comments = comments.filter((c) => !c.resolved);\n }\n\n if (comments.length === 0) return null;\n\n const lineMap = groupByLine(comments);\n return { lineMap, comments };\n}\n", "/**\n * MRSF markdown-it plugin \u2014 core rule (overlay gutter architecture).\n *\n * Walks the parsed token stream and annotates block tokens with\n * `data-mrsf-line` attributes. Adds `mrsf-line-highlight` class on\n * commented lines. Appends an `mrsf_data_script` token with serialized\n * comment data for the client-side MrsfController.\n *\n * NO visual tokens are injected \u2014 all badges, tooltips, and gutter\n * elements are created at runtime by the controller.\n */\n\nimport type Token from \"markdown-it/lib/token.mjs\";\nimport type { Nesting } from \"markdown-it/lib/token.mjs\";\nimport type StateCore from \"markdown-it/lib/rules_core/state_core.mjs\";\nimport type { LineMap, CommentThread } from \"../types.js\";\n\nexport interface CoreRuleOptions {\n /** Whether to add mrsf-line-highlight class on commented tokens. Default: false. */\n lineHighlight?: boolean;\n}\n\n/**\n * Install the MRSF core rule on a markdown-it instance.\n */\nexport function installCoreRule(\n md: { core: { ruler: { push: (name: string, fn: (state: StateCore) => void) => void } } },\n lineMap: LineMap,\n options: CoreRuleOptions = {},\n): void {\n md.core.ruler.push(\"mrsf_inject\", (state: StateCore) => {\n const tokens = state.tokens;\n const TokenCtor = state.Token;\n const processed = new Set<number>();\n\n for (let i = tokens.length - 1; i >= 0; i--) {\n const token = tokens[i];\n const map = token.map;\n if (!map) continue;\n\n // Skip inline tokens \u2014 process their parent block tokens only.\n if (token.type === \"inline\") continue;\n\n // Annotate the element with line range\n const startLine1 = map[0] + 1;\n const endLine1 = map[1];\n token.attrSet(\"data-mrsf-line\", String(startLine1));\n token.attrSet(\"data-mrsf-start-line\", String(startLine1));\n token.attrSet(\"data-mrsf-end-line\", String(endLine1));\n\n // Check each line in this token's range for comments\n for (let line0 = map[0]; line0 < map[1]; line0++) {\n const line = line0 + 1; // 1-based\n if (processed.has(line)) continue;\n processed.add(line);\n\n const threads = lineMap.get(line);\n if (threads && threads.length > 0) {\n if (options.lineHighlight) {\n const existingClass = token.attrGet(\"class\") || \"\";\n if (!existingClass.includes(\"mrsf-line-highlight\")) {\n token.attrSet(\n \"class\",\n existingClass\n ? `${existingClass} mrsf-line-highlight`\n : \"mrsf-line-highlight\",\n );\n }\n }\n token.attrSet(\"data-mrsf-line\", String(line));\n }\n }\n }\n\n // Append a data script token with all comment data\n const allThreads: CommentThread[] = [];\n for (const threads of lineMap.values()) {\n allThreads.push(...threads);\n }\n if (allThreads.length > 0) {\n const scriptToken = new TokenCtor(\"mrsf_data_script\", \"\", 0);\n scriptToken.meta = { threads: allThreads };\n tokens.push(scriptToken);\n }\n });\n}\n", "/**\n * MRSF markdown-it plugin \u2014 renderer rules (overlay gutter architecture).\n *\n * Adds a single custom renderer rule:\n * - mrsf_data_script: renders a <script type=\"application/mrsf+json\">\n * containing serialized comment data for the MrsfController.\n */\n\nimport type { CommentThread } from \"../types.js\";\n\n/**\n * Install MRSF renderer rules on a markdown-it instance.\n */\nexport function installRendererRules(\n md: { renderer: { rules: Record<string, ((...args: any[]) => string) | undefined> } },\n): void {\n md.renderer.rules[\"mrsf_data_script\"] = (\n tokens: { meta: { threads: CommentThread[] } }[],\n idx: number,\n ): string => {\n const { threads } = tokens[idx].meta;\n // Escape < to prevent script tag injection (XSS safety)\n const data = JSON.stringify({ threads }).replace(/</g, \"\\\\u003c\");\n return `<script type=\"application/mrsf+json\">${data}</script>`;\n };\n}\n", "/**\n * Shared logic for @mrsf/markdown-it-mrsf.\n *\n * Contains the plugin factory using shared comment loading/grouping logic.\n */\n\nimport type MarkdownIt from \"markdown-it\";\nimport type { MrsfPluginOptions, CommentLoader } from \"./types.js\";\nimport { resolveComments } from \"@mrsf/plugin-shared\";\nimport { installCoreRule } from \"./rules/core.js\";\nimport { installRendererRules } from \"./rules/renderer.js\";\n\n/**\n * Create a markdown-it plugin function with the given comment loader.\n *\n * @param loader - Function that resolves sidecar data from plugin options\n * @returns A markdown-it plugin function\n */\nexport function createMrsfPlugin(loader: CommentLoader) {\n return function mrsfPlugin(md: MarkdownIt, options: MrsfPluginOptions = {}): void {\n const result = resolveComments(loader, options);\n if (!result) return;\n\n installCoreRule(md, result.lineMap, { lineHighlight: options.lineHighlight ?? false });\n installRendererRules(md);\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["mrsfPlugin"]
7
7
  }
@@ -1,18 +1,19 @@
1
1
  /**
2
- * MRSF markdown-it plugin — core rule.
2
+ * MRSF markdown-it plugin — core rule (overlay gutter architecture).
3
3
  *
4
- * Walks the parsed token stream and injects `mrsf_badge` tokens
5
- * at lines that have comments, and wraps `selected_text` matches
6
- * with `mrsf_highlight_open`/`close` tokens (with tooltip metadata).
4
+ * Walks the parsed token stream and annotates block tokens with
5
+ * `data-mrsf-line` attributes. Adds `mrsf-line-highlight` class on
6
+ * commented lines. Appends an `mrsf_data_script` token with serialized
7
+ * comment data for the client-side MrsfController.
8
+ *
9
+ * NO visual tokens are injected — all badges, tooltips, and gutter
10
+ * elements are created at runtime by the controller.
7
11
  */
8
12
  import type StateCore from "markdown-it/lib/rules_core/state_core.mjs";
9
13
  import type { LineMap } from "../types.js";
10
- /** Options forwarded from the plugin to the core rule. */
11
- export interface CoreOptions {
12
- interactive: boolean;
13
- gutterPosition: "left" | "tight" | "right";
14
- gutterForInline: boolean;
15
- inlineHighlights: boolean;
14
+ export interface CoreRuleOptions {
15
+ /** Whether to add mrsf-line-highlight class on commented tokens. Default: false. */
16
+ lineHighlight?: boolean;
16
17
  }
17
18
  /**
18
19
  * Install the MRSF core rule on a markdown-it instance.
@@ -23,5 +24,5 @@ export declare function installCoreRule(md: {
23
24
  push: (name: string, fn: (state: StateCore) => void) => void;
24
25
  };
25
26
  };
26
- }, lineMap: LineMap, options: CoreOptions): void;
27
+ }, lineMap: LineMap, options?: CoreRuleOptions): void;
27
28
  //# sourceMappingURL=core.d.ts.map
@@ -1,9 +1,9 @@
1
1
  /**
2
- * MRSF markdown-it plugin — renderer rules.
2
+ * MRSF markdown-it plugin — renderer rules (overlay gutter architecture).
3
3
  *
4
- * Adds custom renderer rules for MRSF tokens:
5
- * - mrsf_badge: renders badge + tooltip HTML (gutter)
6
- * - mrsf_highlight_open/close: renders <mark> wrappers with inline tooltips
4
+ * Adds a single custom renderer rule:
5
+ * - mrsf_data_script: renders a <script type="application/mrsf+json">
6
+ * containing serialized comment data for the MrsfController.
7
7
  */
8
8
  /**
9
9
  * Install MRSF renderer rules on a markdown-it instance.
package/dist/style.css CHANGED
@@ -1,10 +1,15 @@
1
1
  /*
2
- * Sidemark — markdown-it plugin default styles
2
+ * Sidemark — MRSF plugin default styles (overlay gutter architecture).
3
3
  *
4
- * Include via: import "@mrsf/markdown-it-mrsf/style.css"
5
- * or <link rel="stylesheet" href=".../@mrsf/markdown-it-mrsf/dist/style.css">
4
+ * Include via: import "@mrsf/plugin-shared/style.css"
5
+ * or <link rel="stylesheet" href=".../@mrsf/plugin-shared/dist/style.css">
6
6
  *
7
7
  * Customise with CSS custom properties (--mrsf-*).
8
+ *
9
+ * Layout: The controller wraps the rendered markdown in an overlay root
10
+ * with position:relative, and creates absolute-positioned gutter columns
11
+ * on the left and/or right. The plugins only add data-mrsf-line
12
+ * attributes — no visual DOM injection.
8
13
  */
9
14
 
10
15
  /* ── Custom property defaults ────────────────────────────── */
@@ -14,6 +19,9 @@
14
19
  --mrsf-badge-bg: #007acc;
15
20
  --mrsf-badge-fg: #fff;
16
21
  --mrsf-badge-resolved-bg: #388a34;
22
+ --mrsf-add-bg: rgba(0, 122, 204, 0.12);
23
+ --mrsf-add-fg: #007acc;
24
+ --mrsf-add-border: rgba(0, 122, 204, 0.28);
17
25
  --mrsf-tooltip-bg: #252526;
18
26
  --mrsf-tooltip-fg: #cccccc;
19
27
  --mrsf-tooltip-border: #454545;
@@ -22,6 +30,7 @@
22
30
  --mrsf-severity-high: #e74c3c;
23
31
  --mrsf-severity-medium: #f39c12;
24
32
  --mrsf-severity-low: #3498db;
33
+ --mrsf-gutter-width: 36px;
25
34
  --mrsf-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
26
35
  }
27
36
 
@@ -33,29 +42,36 @@
33
42
  }
34
43
  }
35
44
 
36
- /* ── Gutter container (left gutter mode) ────────────────── */
45
+ /* ── Overlay root (created by controller) ────────────────── */
37
46
 
38
- .mrsf-gutter-container {
39
- --mrsf-gutter-width: 44px;
47
+ .mrsf-overlay-root {
40
48
  position: relative;
41
- padding-left: var(--mrsf-gutter-width);
42
49
  }
43
50
 
44
- /* In left gutter mode, line-highlight must be static so the badge's
45
- position: absolute resolves against the gutter container — not the
46
- individual line element, which may be deeply nested inside lists. */
47
- .mrsf-gutter-container .mrsf-line-highlight {
48
- position: static;
51
+ .mrsf-gutter {
52
+ position: absolute;
53
+ top: 0;
54
+ bottom: 0;
55
+ width: var(--mrsf-gutter-width);
56
+ pointer-events: none;
57
+ z-index: 10;
49
58
  }
50
59
 
51
- /* ── Line highlight ─────────────────────────────────────── */
60
+ .mrsf-gutter-left {
61
+ left: calc(-1 * var(--mrsf-gutter-width) - 8px);
62
+ }
63
+
64
+ .mrsf-gutter-right {
65
+ right: calc(-1 * var(--mrsf-gutter-width) - 8px);
66
+ }
67
+
68
+ /* ── Line highlight (CSS-only tinting for commented lines) ── */
52
69
 
53
70
  .mrsf-line-highlight {
54
71
  border-left: 3px solid var(--mrsf-accent);
55
72
  padding-left: 8px;
56
73
  background: rgba(55, 148, 255, 0.06);
57
74
  border-radius: 2px;
58
- position: relative;
59
75
  transition: background 0.15s ease;
60
76
  }
61
77
 
@@ -63,28 +79,43 @@
63
79
  background: rgba(55, 148, 255, 0.12);
64
80
  }
65
81
 
82
+ /* ── Gutter items (badges & add buttons, positioned by JS) ── */
83
+
84
+ .mrsf-gutter-item {
85
+ position: absolute;
86
+ left: 0;
87
+ right: 0;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ pointer-events: auto;
92
+ }
93
+
66
94
  /* ── Badge ──────────────────────────────────────────────── */
67
95
 
68
- .mrsf-badge {
96
+ .mrsf-badge,
97
+ .mrsf-gutter-add {
69
98
  display: inline-flex;
70
99
  align-items: center;
100
+ justify-content: center;
71
101
  gap: 2px;
72
102
  font-size: 11px;
73
103
  font-weight: 600;
74
104
  line-height: 1;
75
- padding: 2px 6px;
76
- border-radius: 10px;
77
- cursor: default;
105
+ min-height: 20px;
106
+ padding: 3px 8px;
107
+ border-radius: 999px;
78
108
  user-select: none;
79
109
  white-space: nowrap;
80
- vertical-align: middle;
81
- margin-left: 6px;
110
+ transition: opacity 0.15s ease, background 0.15s ease, border-color 0.15s ease;
111
+ }
82
112
 
113
+ .mrsf-badge {
114
+ cursor: pointer;
83
115
  color: var(--mrsf-badge-fg);
84
116
  background: var(--mrsf-badge-bg);
117
+ border: 1px solid transparent;
85
118
  border-left: 3px solid transparent;
86
-
87
- transition: opacity 0.15s ease;
88
119
  }
89
120
 
90
121
  .mrsf-badge-resolved {
@@ -100,108 +131,44 @@
100
131
  border-left-color: var(--mrsf-severity-medium);
101
132
  }
102
133
 
103
- /* ── Tooltip container ──────────────────────────────────── */
104
-
105
- .mrsf-tooltip-anchor {
106
- position: relative;
107
- display: inline-block;
108
- vertical-align: baseline;
109
- }
134
+ /* ── Add button (in gutter) ─────────────────────────────── */
110
135
 
111
136
  .mrsf-gutter-add {
112
- position: absolute;
113
- right: -18px;
114
- top: 50%;
115
- transform: translateY(-50%);
116
- opacity: 0;
117
- pointer-events: none;
118
- border: 1px solid var(--mrsf-tooltip-border);
119
- background: var(--mrsf-tooltip-bg);
120
- color: var(--mrsf-tooltip-fg);
121
- border-radius: 50%;
122
- width: 20px;
123
- height: 20px;
124
- font-size: 12px;
125
- line-height: 18px;
126
- text-align: center;
127
- cursor: pointer;
128
- transition: opacity 0.12s ease, transform 0.12s ease;
129
- }
130
-
131
- .mrsf-tooltip-anchor:hover > .mrsf-gutter-add,
132
- .mrsf-tooltip-anchor:focus-within > .mrsf-gutter-add {
133
- opacity: 1;
137
+ appearance: none;
138
+ opacity: 0.38;
134
139
  pointer-events: auto;
135
- transform: translateY(-50%) scale(1.02);
140
+ border: 1px solid var(--mrsf-add-border);
141
+ background: var(--mrsf-add-bg);
142
+ color: var(--mrsf-add-fg);
143
+ cursor: pointer;
144
+ font-family: var(--mrsf-font-family);
136
145
  }
137
146
 
138
- /* Add-only anchors need hoverable area even without a badge */
139
- .mrsf-tooltip-anchor.mrsf-gutter-add-only {
140
- display: inline-block;
141
- width: 24px;
142
- height: 24px;
147
+ @media (prefers-color-scheme: light) {
148
+ :root {
149
+ --mrsf-add-bg: rgba(0, 122, 204, 0.08);
150
+ --mrsf-add-border: rgba(0, 122, 204, 0.18);
151
+ }
143
152
  }
144
153
 
145
- /* Reveal gutter add when hovering the whole line */
146
- .mrsf-line-hover-target:hover .mrsf-gutter-add,
147
- .mrsf-line-hover-target:focus-within .mrsf-gutter-add {
154
+ .mrsf-gutter-add:hover,
155
+ .mrsf-gutter-add:focus-visible {
148
156
  opacity: 1;
149
- pointer-events: auto;
150
- transform: translateY(-50%) scale(1.02);
151
- }
152
-
153
- /* Gutter position: right (default) — float the badge to the right */
154
- .mrsf-tooltip-anchor.mrsf-gutter-right {
155
- float: right;
156
- margin-left: 8px;
157
- }
158
-
159
- .mrsf-tooltip-anchor.mrsf-gutter-right .mrsf-badge {
160
- margin-left: 0;
161
- }
162
-
163
- /* Gutter position: tight — badge inline immediately before text */
164
- .mrsf-tooltip-anchor.mrsf-gutter-tight .mrsf-badge {
165
- margin-right: 6px;
166
- margin-left: 0;
167
- }
168
-
169
- /* Gutter position: left — badge in the left margin gutter.
170
- Positioned against the gutter container (not the line element). */
171
- .mrsf-tooltip-anchor.mrsf-gutter-left {
172
- position: absolute;
173
- left: 4px;
174
- }
175
-
176
- .mrsf-tooltip-anchor.mrsf-gutter-left > .mrsf-gutter-add {
177
- left: -18px;
178
- right: auto;
157
+ background: rgba(0, 122, 204, 0.18);
158
+ border-color: rgba(0, 122, 204, 0.38);
179
159
  }
180
160
 
181
- .mrsf-tooltip-anchor.mrsf-gutter-right > .mrsf-gutter-add {
182
- right: -18px;
183
- left: auto;
161
+ .mrsf-gutter-add:focus-visible,
162
+ .mrsf-badge:focus-visible {
163
+ outline: 2px solid var(--mrsf-accent);
164
+ outline-offset: 2px;
184
165
  }
185
166
 
186
- .mrsf-tooltip-anchor.mrsf-gutter-left .mrsf-badge {
187
- margin-left: 0;
188
- }
189
-
190
- /* Tooltip for left-gutter badges */
191
- .mrsf-tooltip-anchor.mrsf-gutter-left > .mrsf-tooltip {
192
- left: 0;
193
- right: auto;
194
- }
195
-
196
- /* Inline highlight tooltip anchor */
197
- .mrsf-inline-anchor {
198
- /* inherit inline-block from .mrsf-tooltip-anchor */
199
- }
167
+ /* ── Tooltip (positioned by JS, appended to gutter item) ── */
200
168
 
201
169
  .mrsf-tooltip {
202
170
  display: none;
203
171
  position: absolute;
204
- left: 0;
205
172
  top: 100%;
206
173
  z-index: 1000;
207
174
  min-width: 300px;
@@ -222,20 +189,19 @@
222
189
  color: var(--mrsf-tooltip-fg);
223
190
  }
224
191
 
225
- /* Right-aligned gutter: anchor tooltip to the right edge */
226
- .mrsf-tooltip-anchor.mrsf-gutter-right > .mrsf-tooltip {
227
- left: auto;
228
- right: 0;
192
+ /* Left gutter: tooltip opens to the right */
193
+ .mrsf-gutter-left .mrsf-tooltip {
194
+ left: 0;
195
+ right: auto;
229
196
  }
230
197
 
231
- /* Inline tooltip: smaller and anchored to the left of the highlight */
232
- .mrsf-inline-tooltip {
233
- min-width: 260px;
198
+ /* Right gutter: tooltip opens to the left */
199
+ .mrsf-gutter-right .mrsf-tooltip {
200
+ left: auto;
201
+ right: 0;
234
202
  }
235
203
 
236
- /* Show tooltip on hover or focus-within (keyboard accessible) */
237
- .mrsf-tooltip-anchor:hover > .mrsf-tooltip,
238
- .mrsf-tooltip-anchor:focus-within > .mrsf-tooltip {
204
+ .mrsf-tooltip.mrsf-tooltip-visible {
239
205
  display: block;
240
206
  animation: mrsf-fade-in 0.15s ease;
241
207
  }
@@ -246,11 +212,8 @@
246
212
  }
247
213
 
248
214
  /* ── Thread / Comment ───────────────────────────────────── */
249
- /* All inner elements use <span> for valid HTML inside <p>/<h> elements,
250
- with display: block to maintain block layout. */
251
215
 
252
216
  .mrsf-thread {
253
- display: block;
254
217
  padding: 4px 0;
255
218
  }
256
219
 
@@ -261,7 +224,6 @@
261
224
  }
262
225
 
263
226
  .mrsf-comment {
264
- display: block;
265
227
  padding: 4px 0;
266
228
  }
267
229
 
@@ -273,7 +235,6 @@
273
235
  /* ── Replies ────────────────────────────────────────────── */
274
236
 
275
237
  .mrsf-replies {
276
- display: block;
277
238
  margin-left: 16px;
278
239
  padding-left: 8px;
279
240
  border-left: 2px solid var(--mrsf-tooltip-border);
@@ -343,7 +304,6 @@
343
304
  /* ── Comment body ───────────────────────────────────────── */
344
305
 
345
306
  .mrsf-comment-body {
346
- display: block;
347
307
  white-space: pre-wrap;
348
308
  word-break: break-word;
349
309
  }
@@ -351,30 +311,92 @@
351
311
  /* ── Selected text quote in tooltip ─────────────────────── */
352
312
 
353
313
  .mrsf-selected-text {
354
- display: block;
355
314
  font-family: monospace;
356
315
  font-size: 0.85em;
357
316
  background: rgba(127, 127, 127, 0.1);
358
317
  border-left: 3px solid rgba(127, 127, 127, 0.3);
359
- padding: 4px 8px;
360
318
  margin: 4px 0;
361
319
  border-radius: 2px;
320
+ }
321
+
322
+ .mrsf-selected-text-summary {
323
+ padding: 4px 8px;
324
+ cursor: pointer;
325
+ overflow: hidden;
326
+ text-overflow: ellipsis;
327
+ white-space: nowrap;
328
+ display: block;
329
+ list-style: none;
330
+ color: var(--mrsf-fg, inherit);
331
+ opacity: 0.7;
332
+ }
333
+
334
+ .mrsf-selected-text-summary::-webkit-details-marker {
335
+ display: none;
336
+ }
337
+
338
+ .mrsf-selected-text-summary::before {
339
+ content: "▶ ";
340
+ font-size: 0.7em;
341
+ vertical-align: middle;
342
+ margin-right: 2px;
343
+ }
344
+
345
+ .mrsf-selected-text[open] > .mrsf-selected-text-summary {
346
+ display: none;
347
+ }
348
+
349
+ .mrsf-selected-text-full {
350
+ display: block;
351
+ padding: 4px 8px;
362
352
  white-space: pre-wrap;
363
353
  word-break: break-word;
364
354
  }
365
355
 
366
- /* ── Inline text highlight ──────────────────────────────── */
356
+ /* ── Inline text highlight (overlay positioned by JS) ───── */
357
+
358
+ .mrsf-highlight-overlay {
359
+ position: absolute;
360
+ background: var(--mrsf-highlight-bg);
361
+ border-bottom: 2px solid var(--mrsf-highlight-border);
362
+ border-radius: 2px;
363
+ pointer-events: none;
364
+ transition: background 0.15s ease;
365
+ }
366
+
367
+ /* ── Inline comment text highlight (<mark> by controller) ── */
367
368
 
368
- .mrsf-highlight {
369
+ .mrsf-inline-highlight {
369
370
  background: var(--mrsf-highlight-bg);
370
371
  border-bottom: 2px solid var(--mrsf-highlight-border);
371
372
  border-radius: 2px;
372
- padding: 0 1px;
373
+ cursor: pointer;
373
374
  transition: background 0.15s ease;
374
375
  }
375
376
 
376
- .mrsf-highlight:hover {
377
- background: rgba(255, 213, 79, 0.45);
377
+ .mrsf-inline-highlight:hover {
378
+ background: rgba(255, 213, 79, 0.4);
379
+ }
380
+
381
+ /* Inline tooltip (appended to document.body, positioned by JS) */
382
+ .mrsf-inline-tooltip {
383
+ position: fixed;
384
+ z-index: 1100;
385
+ min-width: 300px;
386
+ max-width: 480px;
387
+ max-height: 400px;
388
+ overflow-y: auto;
389
+ padding: 8px;
390
+ border-radius: 6px;
391
+ background: var(--mrsf-tooltip-bg);
392
+ border: 1px solid var(--mrsf-tooltip-border);
393
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
394
+ font-family: var(--mrsf-font-family);
395
+ font-size: 13px;
396
+ line-height: 1.5;
397
+ color: var(--mrsf-tooltip-fg);
398
+ white-space: normal;
399
+ animation: mrsf-fade-in 0.15s ease;
378
400
  }
379
401
 
380
402
  /* ── Interactive action buttons ─────────────────────────── */
@@ -421,6 +443,39 @@
421
443
  background: rgba(127, 127, 127, 0.2);
422
444
  }
423
445
 
446
+ /* ── Floating "Add comment" button (selection-based) ────── */
447
+
448
+ /* ── Orphaned comments section (bottom of container) ────── */
449
+
450
+ .mrsf-orphaned-section {
451
+ margin-top: 24px;
452
+ padding-top: 16px;
453
+ border-top: 2px dashed var(--mrsf-tooltip-border);
454
+ }
455
+
456
+ .mrsf-orphaned-heading {
457
+ font-family: var(--mrsf-font-family);
458
+ font-size: 12px;
459
+ font-weight: 600;
460
+ text-transform: uppercase;
461
+ letter-spacing: 0.5px;
462
+ color: #999;
463
+ margin-bottom: 8px;
464
+ }
465
+
466
+ .mrsf-orphaned-thread {
467
+ padding: 8px;
468
+ margin-bottom: 6px;
469
+ border-radius: 6px;
470
+ border: 1px solid var(--mrsf-tooltip-border);
471
+ background: rgba(127, 127, 127, 0.06);
472
+ opacity: 0.7;
473
+ font-family: var(--mrsf-font-family);
474
+ font-size: 13px;
475
+ line-height: 1.5;
476
+ color: var(--mrsf-tooltip-fg);
477
+ }
478
+
424
479
  .mrsf-add-inline-button {
425
480
  position: absolute;
426
481
  z-index: 1200;
package/dist/types.d.ts CHANGED
@@ -55,6 +55,12 @@ export interface MrsfPluginOptions {
55
55
  * Default: true.
56
56
  */
57
57
  inlineHighlights?: boolean;
58
+ /**
59
+ * Whether to add a `mrsf-line-highlight` CSS class on block elements
60
+ * that have comments. Provides a subtle visual tint on commented lines.
61
+ * Default: false.
62
+ */
63
+ lineHighlight?: boolean;
58
64
  /** Color scheme hint (default: 'auto'). */
59
65
  theme?: "light" | "dark" | "auto";
60
66
  /** Working directory for sidecar discovery (default: process.cwd()). */