@mrsf/markdown-it-mrsf 0.1.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.
@@ -0,0 +1,159 @@
1
+ /**
2
+ * MRSF markdown-it plugin — renderer rules.
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
7
+ */
8
+ function escapeHtml(str) {
9
+ return str
10
+ .replace(/&/g, "&amp;")
11
+ .replace(/</g, "&lt;")
12
+ .replace(/>/g, "&gt;")
13
+ .replace(/"/g, "&quot;");
14
+ }
15
+ function formatTime(iso) {
16
+ if (!iso)
17
+ return "";
18
+ try {
19
+ const d = new Date(iso);
20
+ return d.toLocaleDateString(undefined, {
21
+ year: "numeric",
22
+ month: "short",
23
+ day: "numeric",
24
+ });
25
+ }
26
+ catch {
27
+ return "";
28
+ }
29
+ }
30
+ function renderCommentHtml(comment, isReply, interactive) {
31
+ const resolvedClass = comment.resolved ? " mrsf-resolved" : "";
32
+ const replyClass = isReply ? " mrsf-reply" : "";
33
+ let html = `<span class="mrsf-comment${resolvedClass}${replyClass}" data-mrsf-comment-id="${escapeHtml(comment.id)}">`;
34
+ // Header
35
+ html += `<span class="mrsf-comment-header">`;
36
+ html += `<span class="mrsf-author">${escapeHtml(comment.author)}</span>`;
37
+ if (comment.timestamp) {
38
+ html += `<span class="mrsf-date">${escapeHtml(formatTime(comment.timestamp))}</span>`;
39
+ }
40
+ if (comment.severity) {
41
+ html += `<span class="mrsf-severity mrsf-severity-${escapeHtml(comment.severity)}">${escapeHtml(comment.severity)}</span>`;
42
+ }
43
+ if (comment.type) {
44
+ html += `<span class="mrsf-type">${escapeHtml(comment.type)}</span>`;
45
+ }
46
+ if (comment.resolved) {
47
+ html += `<span class="mrsf-resolved-badge">✓ resolved</span>`;
48
+ }
49
+ html += `</span>`;
50
+ // Selected text quote
51
+ if (comment.selected_text) {
52
+ html += `<span class="mrsf-selected-text">${escapeHtml(comment.selected_text)}</span>`;
53
+ }
54
+ // Body
55
+ html += `<span class="mrsf-comment-body">${escapeHtml(comment.text)}</span>`;
56
+ // Action buttons (interactive mode)
57
+ if (interactive) {
58
+ const line = comment.line != null ? String(comment.line) : "";
59
+ html += `<span class="mrsf-actions">`;
60
+ if (comment.resolved) {
61
+ html += `<button class="mrsf-action-btn" data-mrsf-action="unresolve" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Unresolve</button>`;
62
+ }
63
+ else {
64
+ html += `<button class="mrsf-action-btn" data-mrsf-action="resolve" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Resolve</button>`;
65
+ }
66
+ html += `<button class="mrsf-action-btn" data-mrsf-action="reply" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Reply</button>`;
67
+ html += `<button class="mrsf-action-btn" data-mrsf-action="edit" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Edit</button>`;
68
+ html += `</span>`;
69
+ }
70
+ html += `</span>`;
71
+ return html;
72
+ }
73
+ function renderThreadHtml(thread, interactive) {
74
+ let html = `<span class="mrsf-thread">`;
75
+ html += renderCommentHtml(thread.comment, false, interactive);
76
+ if (thread.replies.length > 0) {
77
+ html += `<span class="mrsf-replies">`;
78
+ for (const reply of thread.replies) {
79
+ html += renderCommentHtml(reply, true, interactive);
80
+ }
81
+ html += `</span>`;
82
+ }
83
+ html += `</span>`;
84
+ return html;
85
+ }
86
+ /**
87
+ * Install MRSF renderer rules on a markdown-it instance.
88
+ */
89
+ export function installRendererRules(md) {
90
+ // Badge + tooltip (gutter)
91
+ md.renderer.rules["mrsf_badge"] = (tokens, idx) => {
92
+ const { line, threads, interactive, gutterPosition } = tokens[idx].meta;
93
+ const total = threads.reduce((n, t) => n + 1 + t.replies.length, 0);
94
+ const allResolved = threads.every((t) => t.comment.resolved);
95
+ const highestSeverity = threads.reduce((sev, t) => {
96
+ if (t.comment.severity === "high" || sev === "high")
97
+ return "high";
98
+ if (t.comment.severity === "medium" || sev === "medium")
99
+ return "medium";
100
+ if (t.comment.severity === "low" || sev === "low")
101
+ return "low";
102
+ return sev;
103
+ }, null);
104
+ const resolvedClass = allResolved ? " mrsf-badge-resolved" : "";
105
+ const severityClass = highestSeverity === "high" || highestSeverity === "medium"
106
+ ? ` mrsf-badge-severity-${highestSeverity}`
107
+ : "";
108
+ const positionClass = ` mrsf-gutter-${gutterPosition}`;
109
+ const icon = allResolved ? "✓" : "💬";
110
+ let html = `<span class="mrsf-tooltip-anchor${positionClass}">`;
111
+ // Badge
112
+ 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>`;
113
+ // Tooltip
114
+ html += `<span class="mrsf-tooltip" data-mrsf-line="${line}">`;
115
+ for (const thread of threads) {
116
+ html += renderThreadHtml(thread, interactive);
117
+ }
118
+ html += `</span>`;
119
+ html += `</span>`;
120
+ return html;
121
+ };
122
+ // Highlight open — wraps in tooltip-anchor for inline hover (feature D)
123
+ md.renderer.rules["mrsf_highlight_open"] = (tokens, idx) => {
124
+ const token = tokens[idx];
125
+ const cls = token.attrGet("class") || "mrsf-highlight";
126
+ const commentId = token.attrGet("data-mrsf-comment-id") || "";
127
+ const thread = token.meta?.thread;
128
+ let html = "";
129
+ if (thread) {
130
+ html += `<span class="mrsf-tooltip-anchor mrsf-inline-anchor">`;
131
+ }
132
+ html += `<mark class="${escapeHtml(cls)}" data-mrsf-comment-id="${escapeHtml(commentId)}" tabindex="0">`;
133
+ return html;
134
+ };
135
+ // Highlight close — adds inline tooltip and closes tooltip-anchor
136
+ md.renderer.rules["mrsf_highlight_close"] = (tokens, idx) => {
137
+ const token = tokens[idx];
138
+ const meta = token.meta;
139
+ let html = "</mark>";
140
+ if (meta?.thread) {
141
+ const thread = meta.thread;
142
+ const interactive = meta.interactive;
143
+ html += `<span class="mrsf-tooltip mrsf-inline-tooltip">`;
144
+ html += renderThreadHtml(thread, interactive);
145
+ html += `</span>`;
146
+ html += `</span>`;
147
+ }
148
+ return html;
149
+ };
150
+ // Gutter container wrapper (left gutter mode)
151
+ md.renderer.rules["mrsf_gutter_open"] = (tokens, idx) => {
152
+ const cls = tokens[idx].attrGet("class") || "mrsf-gutter-container";
153
+ return `<div class="${escapeHtml(cls)}">`;
154
+ };
155
+ md.renderer.rules["mrsf_gutter_close"] = () => {
156
+ return `</div>`;
157
+ };
158
+ }
159
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared logic for @mrsf/markdown-it-mrsf.
3
+ *
4
+ * Contains the plugin factory, comment transformation, and grouping logic
5
+ * used by both the Node.js (index.ts) and browser (browser.ts) entry points.
6
+ */
7
+ import type MarkdownIt from "markdown-it";
8
+ import type { MrsfDocument } from "@mrsf/cli";
9
+ import type { MrsfPluginOptions, SlimComment, LineMap } from "./types.js";
10
+ /**
11
+ * A function that loads sidecar data from plugin options.
12
+ */
13
+ export type CommentLoader = (options: MrsfPluginOptions) => MrsfDocument | null;
14
+ /**
15
+ * Convert an MrsfDocument into a slim comment array.
16
+ */
17
+ export declare function toSlimComments(doc: MrsfDocument): SlimComment[];
18
+ /**
19
+ * Group comments by line number, threading replies under parents.
20
+ */
21
+ export declare function groupByLine(comments: SlimComment[]): LineMap;
22
+ /**
23
+ * Create a markdown-it plugin function with the given comment loader.
24
+ *
25
+ * @param loader - Function that resolves sidecar data from plugin options
26
+ * @returns A markdown-it plugin function
27
+ */
28
+ export declare function createMrsfPlugin(loader: CommentLoader): (md: MarkdownIt, options?: MrsfPluginOptions) => void;
29
+ //# sourceMappingURL=shared.d.ts.map
package/dist/shared.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Shared logic for @mrsf/markdown-it-mrsf.
3
+ *
4
+ * Contains the plugin factory, comment transformation, and grouping logic
5
+ * used by both the Node.js (index.ts) and browser (browser.ts) entry points.
6
+ */
7
+ import { installCoreRule } from "./rules/core.js";
8
+ import { installRendererRules } from "./rules/renderer.js";
9
+ /**
10
+ * Convert an MrsfDocument into a slim comment array.
11
+ */
12
+ export function toSlimComments(doc) {
13
+ return doc.comments.map((c) => ({
14
+ id: c.id,
15
+ author: c.author || "Unknown",
16
+ text: c.text || "",
17
+ line: c.line ?? null,
18
+ end_line: c.end_line ?? null,
19
+ start_column: c.start_column ?? null,
20
+ end_column: c.end_column ?? null,
21
+ selected_text: c.selected_text || null,
22
+ resolved: !!c.resolved,
23
+ reply_to: c.reply_to || null,
24
+ severity: c.severity || null,
25
+ type: c.type || null,
26
+ timestamp: c.timestamp || null,
27
+ }));
28
+ }
29
+ /**
30
+ * Group comments by line number, threading replies under parents.
31
+ */
32
+ export function groupByLine(comments) {
33
+ const rootComments = comments.filter((c) => !c.reply_to && c.line != null);
34
+ const replies = comments.filter((c) => c.reply_to);
35
+ const replyMap = new Map();
36
+ for (const r of replies) {
37
+ const list = replyMap.get(r.reply_to) || [];
38
+ list.push(r);
39
+ replyMap.set(r.reply_to, list);
40
+ }
41
+ const lineMap = new Map();
42
+ for (const c of rootComments) {
43
+ const line = c.line;
44
+ const threads = lineMap.get(line) || [];
45
+ threads.push({
46
+ comment: c,
47
+ replies: replyMap.get(c.id) || [],
48
+ });
49
+ lineMap.set(line, threads);
50
+ }
51
+ return lineMap;
52
+ }
53
+ /**
54
+ * Create a markdown-it plugin function with the given comment loader.
55
+ *
56
+ * @param loader - Function that resolves sidecar data from plugin options
57
+ * @returns A markdown-it plugin function
58
+ */
59
+ export function createMrsfPlugin(loader) {
60
+ return function mrsfPlugin(md, options = {}) {
61
+ const showResolved = options.showResolved ?? true;
62
+ const interactive = options.interactive ?? false;
63
+ const gutterPosition = options.gutterPosition ?? "right";
64
+ const gutterForInline = options.gutterForInline ?? true;
65
+ const inlineHighlights = options.inlineHighlights ?? true;
66
+ const doc = loader(options);
67
+ if (!doc || !doc.comments || doc.comments.length === 0) {
68
+ return;
69
+ }
70
+ let comments = toSlimComments(doc);
71
+ if (!showResolved) {
72
+ comments = comments.filter((c) => !c.resolved);
73
+ }
74
+ if (comments.length === 0)
75
+ return;
76
+ const lineMap = groupByLine(comments);
77
+ installCoreRule(md, lineMap, {
78
+ interactive,
79
+ gutterPosition,
80
+ gutterForInline,
81
+ inlineHighlights,
82
+ });
83
+ installRendererRules(md);
84
+ };
85
+ }
86
+ //# sourceMappingURL=shared.js.map
package/dist/style.css ADDED
@@ -0,0 +1,356 @@
1
+ /*
2
+ * Sidemark — markdown-it plugin default styles
3
+ *
4
+ * Include via: import "@mrsf/markdown-it-mrsf/style.css"
5
+ * or <link rel="stylesheet" href=".../@mrsf/markdown-it-mrsf/dist/style.css">
6
+ *
7
+ * Customise with CSS custom properties (--mrsf-*).
8
+ */
9
+
10
+ /* ── Custom property defaults ────────────────────────────── */
11
+
12
+ :root {
13
+ --mrsf-accent: #3794ff;
14
+ --mrsf-badge-bg: #007acc;
15
+ --mrsf-badge-fg: #fff;
16
+ --mrsf-badge-resolved-bg: #388a34;
17
+ --mrsf-tooltip-bg: #252526;
18
+ --mrsf-tooltip-fg: #cccccc;
19
+ --mrsf-tooltip-border: #454545;
20
+ --mrsf-highlight-bg: rgba(255, 213, 79, 0.25);
21
+ --mrsf-highlight-border: rgba(255, 213, 79, 0.7);
22
+ --mrsf-severity-high: #e74c3c;
23
+ --mrsf-severity-medium: #f39c12;
24
+ --mrsf-severity-low: #3498db;
25
+ --mrsf-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
26
+ }
27
+
28
+ @media (prefers-color-scheme: light) {
29
+ :root {
30
+ --mrsf-tooltip-bg: #f3f3f3;
31
+ --mrsf-tooltip-fg: #333333;
32
+ --mrsf-tooltip-border: #cccccc;
33
+ }
34
+ }
35
+
36
+ /* ── Gutter container (left gutter mode) ────────────────── */
37
+
38
+ .mrsf-gutter-container {
39
+ --mrsf-gutter-width: 44px;
40
+ position: relative;
41
+ padding-left: var(--mrsf-gutter-width);
42
+ }
43
+
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;
49
+ }
50
+
51
+ /* ── Line highlight ─────────────────────────────────────── */
52
+
53
+ .mrsf-line-highlight {
54
+ border-left: 3px solid var(--mrsf-accent);
55
+ padding-left: 8px;
56
+ background: rgba(55, 148, 255, 0.06);
57
+ border-radius: 2px;
58
+ position: relative;
59
+ transition: background 0.15s ease;
60
+ }
61
+
62
+ .mrsf-line-highlight:hover {
63
+ background: rgba(55, 148, 255, 0.12);
64
+ }
65
+
66
+ /* ── Badge ──────────────────────────────────────────────── */
67
+
68
+ .mrsf-badge {
69
+ display: inline-flex;
70
+ align-items: center;
71
+ gap: 2px;
72
+ font-size: 11px;
73
+ font-weight: 600;
74
+ line-height: 1;
75
+ padding: 2px 6px;
76
+ border-radius: 10px;
77
+ cursor: default;
78
+ user-select: none;
79
+ white-space: nowrap;
80
+ vertical-align: middle;
81
+ margin-left: 6px;
82
+
83
+ color: var(--mrsf-badge-fg);
84
+ background: var(--mrsf-badge-bg);
85
+ border-left: 3px solid transparent;
86
+
87
+ transition: opacity 0.15s ease;
88
+ }
89
+
90
+ .mrsf-badge-resolved {
91
+ background: var(--mrsf-badge-resolved-bg);
92
+ opacity: 0.7;
93
+ }
94
+
95
+ .mrsf-badge-severity-high {
96
+ border-left-color: var(--mrsf-severity-high);
97
+ }
98
+
99
+ .mrsf-badge-severity-medium {
100
+ border-left-color: var(--mrsf-severity-medium);
101
+ }
102
+
103
+ /* ── Tooltip container ──────────────────────────────────── */
104
+
105
+ .mrsf-tooltip-anchor {
106
+ position: relative;
107
+ display: inline-block;
108
+ vertical-align: baseline;
109
+ }
110
+
111
+ /* Gutter position: right (default) — float the badge to the right */
112
+ .mrsf-tooltip-anchor.mrsf-gutter-right {
113
+ float: right;
114
+ margin-left: 8px;
115
+ }
116
+
117
+ .mrsf-tooltip-anchor.mrsf-gutter-right .mrsf-badge {
118
+ margin-left: 0;
119
+ }
120
+
121
+ /* Gutter position: tight — badge inline immediately before text */
122
+ .mrsf-tooltip-anchor.mrsf-gutter-tight .mrsf-badge {
123
+ margin-right: 6px;
124
+ margin-left: 0;
125
+ }
126
+
127
+ /* Gutter position: left — badge in the left margin gutter.
128
+ Positioned against the gutter container (not the line element). */
129
+ .mrsf-tooltip-anchor.mrsf-gutter-left {
130
+ position: absolute;
131
+ left: 4px;
132
+ }
133
+
134
+ .mrsf-tooltip-anchor.mrsf-gutter-left .mrsf-badge {
135
+ margin-left: 0;
136
+ }
137
+
138
+ /* Tooltip for left-gutter badges */
139
+ .mrsf-tooltip-anchor.mrsf-gutter-left > .mrsf-tooltip {
140
+ left: 0;
141
+ right: auto;
142
+ }
143
+
144
+ /* Inline highlight tooltip anchor */
145
+ .mrsf-inline-anchor {
146
+ /* inherit inline-block from .mrsf-tooltip-anchor */
147
+ }
148
+
149
+ .mrsf-tooltip {
150
+ display: none;
151
+ position: absolute;
152
+ left: 0;
153
+ top: 100%;
154
+ z-index: 1000;
155
+ min-width: 300px;
156
+ max-width: 480px;
157
+ max-height: 400px;
158
+ overflow-y: auto;
159
+ padding: 8px;
160
+ margin-top: 4px;
161
+ border-radius: 6px;
162
+
163
+ background: var(--mrsf-tooltip-bg);
164
+ border: 1px solid var(--mrsf-tooltip-border);
165
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
166
+
167
+ font-family: var(--mrsf-font-family);
168
+ font-size: 13px;
169
+ line-height: 1.5;
170
+ color: var(--mrsf-tooltip-fg);
171
+ }
172
+
173
+ /* Right-aligned gutter: anchor tooltip to the right edge */
174
+ .mrsf-tooltip-anchor.mrsf-gutter-right > .mrsf-tooltip {
175
+ left: auto;
176
+ right: 0;
177
+ }
178
+
179
+ /* Inline tooltip: smaller and anchored to the left of the highlight */
180
+ .mrsf-inline-tooltip {
181
+ min-width: 260px;
182
+ }
183
+
184
+ /* Show tooltip on hover or focus-within (keyboard accessible) */
185
+ .mrsf-tooltip-anchor:hover > .mrsf-tooltip,
186
+ .mrsf-tooltip-anchor:focus-within > .mrsf-tooltip {
187
+ display: block;
188
+ animation: mrsf-fade-in 0.15s ease;
189
+ }
190
+
191
+ @keyframes mrsf-fade-in {
192
+ from { opacity: 0; transform: translateY(-4px); }
193
+ to { opacity: 1; transform: translateY(0); }
194
+ }
195
+
196
+ /* ── Thread / Comment ───────────────────────────────────── */
197
+ /* All inner elements use <span> for valid HTML inside <p>/<h> elements,
198
+ with display: block to maintain block layout. */
199
+
200
+ .mrsf-thread {
201
+ display: block;
202
+ padding: 4px 0;
203
+ }
204
+
205
+ .mrsf-thread + .mrsf-thread {
206
+ border-top: 1px solid var(--mrsf-tooltip-border);
207
+ margin-top: 4px;
208
+ padding-top: 8px;
209
+ }
210
+
211
+ .mrsf-comment {
212
+ display: block;
213
+ padding: 4px 0;
214
+ }
215
+
216
+ .mrsf-comment.mrsf-resolved {
217
+ border-left: 2px solid var(--mrsf-badge-resolved-bg);
218
+ padding-left: 6px;
219
+ }
220
+
221
+ /* ── Replies ────────────────────────────────────────────── */
222
+
223
+ .mrsf-replies {
224
+ display: block;
225
+ margin-left: 16px;
226
+ padding-left: 8px;
227
+ border-left: 2px solid var(--mrsf-tooltip-border);
228
+ }
229
+
230
+ .mrsf-reply {
231
+ font-size: 12px;
232
+ }
233
+
234
+ /* ── Comment header ─────────────────────────────────────── */
235
+
236
+ .mrsf-comment-header {
237
+ display: flex;
238
+ align-items: center;
239
+ gap: 6px;
240
+ flex-wrap: wrap;
241
+ margin-bottom: 2px;
242
+ }
243
+
244
+ .mrsf-author {
245
+ font-weight: 600;
246
+ }
247
+
248
+ .mrsf-date {
249
+ font-size: 11px;
250
+ opacity: 0.6;
251
+ }
252
+
253
+ .mrsf-severity {
254
+ font-size: 10px;
255
+ font-weight: 600;
256
+ padding: 1px 5px;
257
+ border-radius: 8px;
258
+ text-transform: uppercase;
259
+ letter-spacing: 0.3px;
260
+ }
261
+
262
+ .mrsf-severity-high {
263
+ background: rgba(231, 76, 60, 0.2);
264
+ color: var(--mrsf-severity-high);
265
+ }
266
+
267
+ .mrsf-severity-medium {
268
+ background: rgba(243, 156, 18, 0.2);
269
+ color: var(--mrsf-severity-medium);
270
+ }
271
+
272
+ .mrsf-severity-low {
273
+ background: rgba(52, 152, 219, 0.2);
274
+ color: var(--mrsf-severity-low);
275
+ }
276
+
277
+ .mrsf-type {
278
+ font-size: 10px;
279
+ padding: 1px 5px;
280
+ border-radius: 8px;
281
+ background: rgba(127, 127, 127, 0.15);
282
+ opacity: 0.8;
283
+ }
284
+
285
+ .mrsf-resolved-badge {
286
+ font-size: 10px;
287
+ color: var(--mrsf-badge-resolved-bg);
288
+ font-weight: 600;
289
+ }
290
+
291
+ /* ── Comment body ───────────────────────────────────────── */
292
+
293
+ .mrsf-comment-body {
294
+ display: block;
295
+ white-space: pre-wrap;
296
+ word-break: break-word;
297
+ }
298
+
299
+ /* ── Selected text quote in tooltip ─────────────────────── */
300
+
301
+ .mrsf-selected-text {
302
+ display: block;
303
+ font-family: monospace;
304
+ font-size: 0.85em;
305
+ background: rgba(127, 127, 127, 0.1);
306
+ border-left: 3px solid rgba(127, 127, 127, 0.3);
307
+ padding: 4px 8px;
308
+ margin: 4px 0;
309
+ border-radius: 2px;
310
+ white-space: pre-wrap;
311
+ word-break: break-word;
312
+ }
313
+
314
+ /* ── Inline text highlight ──────────────────────────────── */
315
+
316
+ .mrsf-highlight {
317
+ background: var(--mrsf-highlight-bg);
318
+ border-bottom: 2px solid var(--mrsf-highlight-border);
319
+ border-radius: 2px;
320
+ padding: 0 1px;
321
+ transition: background 0.15s ease;
322
+ }
323
+
324
+ .mrsf-highlight:hover {
325
+ background: rgba(255, 213, 79, 0.45);
326
+ }
327
+
328
+ /* ── Interactive action buttons ─────────────────────────── */
329
+
330
+ .mrsf-actions {
331
+ display: none;
332
+ gap: 4px;
333
+ margin-top: 4px;
334
+ padding-top: 4px;
335
+ border-top: 1px solid var(--mrsf-tooltip-border);
336
+ }
337
+
338
+ .mrsf-interactive .mrsf-actions {
339
+ display: flex;
340
+ }
341
+
342
+ .mrsf-action-btn {
343
+ font-size: 11px;
344
+ padding: 2px 8px;
345
+ border-radius: 4px;
346
+ border: 1px solid var(--mrsf-tooltip-border);
347
+ background: transparent;
348
+ color: var(--mrsf-tooltip-fg);
349
+ cursor: pointer;
350
+ font-family: var(--mrsf-font-family);
351
+ transition: background 0.15s ease;
352
+ }
353
+
354
+ .mrsf-action-btn:hover {
355
+ background: rgba(127, 127, 127, 0.2);
356
+ }