reviw 0.10.7 → 0.11.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.
Files changed (3) hide show
  1. package/README.md +56 -0
  2. package/cli.cjs +745 -52
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -20,6 +20,8 @@ A lightweight browser-based tool for reviewing and annotating tabular data, text
20
20
  ### Media Fullscreen
21
21
  - Click images in Markdown preview to open fullscreen viewer
22
22
  - Click videos to open fullscreen playback with native controls
23
+ - Click anywhere (including the image/video itself) to close the fullscreen overlay
24
+ - Clicking media automatically highlights the corresponding source line in the Markdown panel
23
25
 
24
26
  ### UI Features
25
27
  - **Theme toggle**: Switch between light and dark modes
@@ -103,10 +105,64 @@ comments:
103
105
  summary: Overall the data looks good, minor issues noted above.
104
106
  ```
105
107
 
108
+ ## Claude Code Plugin
109
+
110
+ This repository also serves as a Claude Code plugin marketplace. The plugin integrates reviw into Claude Code workflows with task management and review automation.
111
+
112
+ ### Installation
113
+
114
+ ```bash
115
+ # In Claude Code
116
+ /plugin marketplace add kazuph/reviw
117
+ /plugin install reviw-plugin@reviw-marketplace
118
+ ```
119
+
120
+ ### Plugin Features
121
+
122
+ | Component | Name | Description |
123
+ |-----------|------|-------------|
124
+ | **Command** | `/reviw:do` | Start a task - create worktree, plan, register todos |
125
+ | **Command** | `/reviw:done` | Complete checklist - collect evidence, start review with reviw |
126
+ | **Agent** | `report-builder` | Prepare reports and evidence for user review |
127
+ | **Skill** | `artifact-proof` | Collect evidence (screenshots, videos, logs) + reviw review workflow |
128
+ | **Hook** | PreToolUse | Remind to review before git commit/push |
129
+ | **Hook** | Stop | Warn if task is still in progress |
130
+
131
+ ### Workflow
132
+
133
+ ```
134
+ /reviw:do <task description>
135
+
136
+ Create worktree + Plan
137
+
138
+ Implementation
139
+
140
+ /reviw:done
141
+
142
+ Collect evidence + Create report
143
+
144
+ npx reviw opens report (foreground)
145
+
146
+ User comments → Submit & Exit
147
+
148
+ Register feedback to Todo
149
+
150
+ Fix → Re-review until approved
151
+ ```
152
+
153
+ ### Completion Criteria
154
+
155
+ | Stage | Content |
156
+ |-------|---------|
157
+ | 1/3 | Implementation complete |
158
+ | 2/3 | Build, start, verification complete |
159
+ | 3/3 | Review with reviw → User approval |
160
+
106
161
  ## Development
107
162
 
108
163
  - Main source: `cli.cjs`
109
164
  - Tests: `npm test` (vitest + playwright)
165
+ - Plugin: `plugin/` directory
110
166
 
111
167
  ## License
112
168
 
package/cli.cjs CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  const fs = require("fs");
15
15
  const http = require("http");
16
+ const os = require("os");
16
17
  const path = require("path");
17
18
  const { spawn } = require("child_process");
18
19
  const chardet = require("chardet");
@@ -465,7 +466,8 @@ function loadDiff(diffText) {
465
466
  return {
466
467
  rows,
467
468
  files: sortedFiles,
468
- title: "Git Diff",
469
+ projectRoot: "",
470
+ relativePath: "Git Diff",
469
471
  mode: "diff",
470
472
  };
471
473
  }
@@ -574,7 +576,7 @@ function loadCsv(filePath) {
574
576
  return {
575
577
  rows,
576
578
  cols: Math.max(1, maxCols),
577
- title: path.basename(filePath),
579
+ ...formatTitlePaths(filePath),
578
580
  };
579
581
  }
580
582
 
@@ -585,7 +587,7 @@ function loadText(filePath) {
585
587
  return {
586
588
  rows: lines.map((line) => [line]),
587
589
  cols: 1,
588
- title: path.basename(filePath),
590
+ ...formatTitlePaths(filePath),
589
591
  preview: null,
590
592
  };
591
593
  }
@@ -598,6 +600,7 @@ function loadMarkdown(filePath) {
598
600
  // Parse YAML frontmatter
599
601
  let frontmatterHtml = "";
600
602
  let contentStart = 0;
603
+ let reviwQuestions = []; // Extract reviw questions for modal
601
604
 
602
605
  if (lines[0] && lines[0].trim() === "---") {
603
606
  let frontmatterEnd = -1;
@@ -615,30 +618,91 @@ function loadMarkdown(filePath) {
615
618
  try {
616
619
  const frontmatter = yaml.load(frontmatterText);
617
620
  if (frontmatter && typeof frontmatter === "object") {
618
- // Create HTML table for frontmatter
619
- frontmatterHtml = '<div class="frontmatter-table"><table>';
620
- frontmatterHtml += '<colgroup><col style="width:12%"><col style="width:88%"></colgroup>';
621
- frontmatterHtml += '<thead><tr><th colspan="2">Document Metadata</th></tr></thead>';
622
- frontmatterHtml += "<tbody>";
623
-
624
- function renderValue(val) {
625
- if (Array.isArray(val)) {
626
- return val
627
- .map((v) => '<span class="fm-tag">' + escapeHtmlChars(String(v)) + "</span>")
628
- .join(" ");
621
+ // Extract reviw questions if present
622
+ if (frontmatter.reviw && Array.isArray(frontmatter.reviw.questions)) {
623
+ reviwQuestions = frontmatter.reviw.questions.map((q, idx) => ({
624
+ id: q.id || "q" + (idx + 1),
625
+ question: q.question || "",
626
+ resolved: q.resolved === true,
627
+ answer: q.answer || "",
628
+ options: Array.isArray(q.options) ? q.options : [],
629
+ }));
630
+ }
631
+
632
+ // Create HTML table for frontmatter (show reviw questions in detail for 1:1 correspondence with YAML source)
633
+ const displayFrontmatter = { ...frontmatter };
634
+ // Keep reviw as-is for detailed rendering
635
+
636
+ if (Object.keys(displayFrontmatter).length > 0) {
637
+ frontmatterHtml = '<div class="frontmatter-table"><table>';
638
+ frontmatterHtml += '<colgroup><col style="width:12%"><col style="width:88%"></colgroup>';
639
+ frontmatterHtml += '<thead><tr><th colspan="2">Document Metadata</th></tr></thead>';
640
+ frontmatterHtml += "<tbody>";
641
+
642
+ // Render reviw.questions as detailed cards
643
+ function renderReviwQuestions(questions) {
644
+ if (!Array.isArray(questions) || questions.length === 0) {
645
+ return '<span class="fm-tag">質問なし</span>';
646
+ }
647
+ let html = '<div class="reviw-questions-preview">';
648
+ questions.forEach((q, idx) => {
649
+ const statusIcon = q.resolved ? '✅' : '⏳';
650
+ const statusClass = q.resolved ? 'resolved' : 'pending';
651
+ html += '<div class="reviw-q-card ' + statusClass + '">';
652
+ html += '<div class="reviw-q-header">' + statusIcon + ' <strong>' + escapeHtmlChars(q.id || 'Q' + (idx + 1)) + '</strong></div>';
653
+ html += '<div class="reviw-q-question">' + escapeHtmlChars(q.question || '') + '</div>';
654
+ if (q.options && Array.isArray(q.options) && q.options.length > 0) {
655
+ html += '<div class="reviw-q-options">';
656
+ q.options.forEach(opt => {
657
+ html += '<span class="fm-tag">' + escapeHtmlChars(opt) + '</span>';
658
+ });
659
+ html += '</div>';
660
+ }
661
+ if (q.answer) {
662
+ html += '<div class="reviw-q-answer">💬 ' + escapeHtmlChars(q.answer) + '</div>';
663
+ }
664
+ html += '</div>';
665
+ });
666
+ html += '</div>';
667
+ return html;
629
668
  }
630
- if (typeof val === "object" && val !== null) {
631
- return "<pre>" + escapeHtmlChars(JSON.stringify(val, null, 2)) + "</pre>";
669
+
670
+ function renderValue(val, key) {
671
+ // Special handling for reviw object
672
+ if (key === 'reviw' && typeof val === 'object' && val !== null) {
673
+ let html = '';
674
+ if (val.questions && Array.isArray(val.questions)) {
675
+ html += renderReviwQuestions(val.questions);
676
+ }
677
+ // Render other reviw properties
678
+ const { questions, ...rest } = val;
679
+ if (Object.keys(rest).length > 0) {
680
+ html += '<div style="margin-top: 8px;">';
681
+ for (const [k, v] of Object.entries(rest)) {
682
+ html += '<div><strong>' + escapeHtmlChars(k) + ':</strong> ' + escapeHtmlChars(String(v)) + '</div>';
683
+ }
684
+ html += '</div>';
685
+ }
686
+ return html || '<span class="fm-tag">-</span>';
687
+ }
688
+ if (Array.isArray(val)) {
689
+ return val
690
+ .map((v) => '<span class="fm-tag">' + escapeHtmlChars(String(v)) + "</span>")
691
+ .join(" ");
692
+ }
693
+ if (typeof val === "object" && val !== null) {
694
+ return "<pre>" + escapeHtmlChars(JSON.stringify(val, null, 2)) + "</pre>";
695
+ }
696
+ return escapeHtmlChars(String(val));
632
697
  }
633
- return escapeHtmlChars(String(val));
634
- }
635
698
 
636
- for (const [key, val] of Object.entries(frontmatter)) {
637
- frontmatterHtml +=
638
- "<tr><th>" + escapeHtmlChars(key) + "</th><td>" + renderValue(val) + "</td></tr>";
639
- }
699
+ for (const [key, val] of Object.entries(displayFrontmatter)) {
700
+ frontmatterHtml +=
701
+ "<tr><th>" + escapeHtmlChars(key) + "</th><td>" + renderValue(val, key) + "</td></tr>";
702
+ }
640
703
 
641
- frontmatterHtml += "</tbody></table></div>";
704
+ frontmatterHtml += "</tbody></table></div>";
705
+ }
642
706
  contentStart = frontmatterEnd + 1;
643
707
  }
644
708
  } catch (e) {
@@ -654,8 +718,9 @@ function loadMarkdown(filePath) {
654
718
  return {
655
719
  rows: lines.map((line) => [line]),
656
720
  cols: 1,
657
- title: path.basename(filePath),
721
+ ...formatTitlePaths(filePath),
658
722
  preview,
723
+ reviwQuestions, // Pass questions to UI
659
724
  };
660
725
  }
661
726
 
@@ -667,6 +732,20 @@ function escapeHtmlChars(str) {
667
732
  .replace(/"/g, "&quot;");
668
733
  }
669
734
 
735
+ function formatTitlePaths(filePath) {
736
+ const cwd = process.cwd();
737
+ const home = os.homedir();
738
+ const relativePath = path.relative(cwd, filePath) || path.basename(filePath);
739
+ let projectRoot = cwd;
740
+ if (projectRoot.startsWith(home)) {
741
+ projectRoot = "~" + projectRoot.slice(home.length);
742
+ }
743
+ if (!projectRoot.endsWith("/")) {
744
+ projectRoot += "/";
745
+ }
746
+ return { projectRoot, relativePath };
747
+ }
748
+
670
749
  function loadData(filePath) {
671
750
  // Check if path exists
672
751
  if (!fs.existsSync(filePath)) {
@@ -714,7 +793,7 @@ function serializeForScript(value) {
714
793
  }
715
794
 
716
795
  function diffHtmlTemplate(diffData) {
717
- const { rows, title } = diffData;
796
+ const { rows, projectRoot, relativePath } = diffData;
718
797
  const serialized = serializeForScript(rows);
719
798
  const fileCount = rows.filter((r) => r.type === "file").length;
720
799
 
@@ -726,7 +805,7 @@ function diffHtmlTemplate(diffData) {
726
805
  <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" />
727
806
  <meta http-equiv="Pragma" content="no-cache" />
728
807
  <meta http-equiv="Expires" content="0" />
729
- <title>${title} | reviw</title>
808
+ <title>${relativePath} | reviw</title>
730
809
  <style>
731
810
  :root {
732
811
  color-scheme: dark;
@@ -795,7 +874,9 @@ function diffHtmlTemplate(diffData) {
795
874
  }
796
875
  header .meta { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
797
876
  header .actions { display: flex; gap: 8px; align-items: center; }
798
- header h1 { font-size: 16px; margin: 0; font-weight: 600; }
877
+ header h1 { display: flex; flex-direction: column; margin: 0; line-height: 1.3; }
878
+ header h1 .title-path { font-size: 11px; font-weight: 400; color: var(--muted); }
879
+ header h1 .title-file { font-size: 16px; font-weight: 600; }
799
880
  header .badge {
800
881
  background: var(--selected-bg);
801
882
  color: var(--text);
@@ -958,11 +1039,14 @@ function diffHtmlTemplate(diffData) {
958
1039
  display: none;
959
1040
  }
960
1041
  .floating header {
961
- position: relative;
1042
+ position: static;
962
1043
  background: transparent;
1044
+ backdrop-filter: none;
963
1045
  border: none;
964
1046
  padding: 0 0 10px 0;
1047
+ display: flex;
965
1048
  justify-content: space-between;
1049
+ align-items: center;
966
1050
  }
967
1051
  .floating h2 { font-size: 14px; margin: 0; font-weight: 600; }
968
1052
  .floating button {
@@ -987,6 +1071,10 @@ function diffHtmlTemplate(diffData) {
987
1071
  font-size: 13px;
988
1072
  font-family: inherit;
989
1073
  }
1074
+ .floating textarea:focus {
1075
+ outline: none;
1076
+ border-color: var(--accent);
1077
+ }
990
1078
  .floating .actions {
991
1079
  display: flex;
992
1080
  gap: 8px;
@@ -1104,7 +1192,7 @@ function diffHtmlTemplate(diffData) {
1104
1192
  <body>
1105
1193
  <header>
1106
1194
  <div class="meta">
1107
- <h1>${title}</h1>
1195
+ <h1>${projectRoot ? `<span class="title-path">${projectRoot}</span>` : ""}<span class="title-file">${relativePath}</span></h1>
1108
1196
  <span class="badge">${fileCount} file${fileCount !== 1 ? "s" : ""} changed</span>
1109
1197
  <span class="pill">Comments <strong id="comment-count">0</strong></span>
1110
1198
  </div>
@@ -1594,10 +1682,11 @@ function diffHtmlTemplate(diffData) {
1594
1682
  }
1595
1683
 
1596
1684
  // --- HTML template ---------------------------------------------------------
1597
- function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1685
+ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHtml, reviwQuestions = []) {
1598
1686
  const serialized = serializeForScript(dataRows);
1599
1687
  const modeJson = serializeForScript(mode);
1600
- const titleJson = serializeForScript(title);
1688
+ const titleJson = serializeForScript(relativePath); // Use relativePath as file identifier
1689
+ const questionsJson = serializeForScript(reviwQuestions || []);
1601
1690
  const hasPreview = !!previewHtml;
1602
1691
  return `<!doctype html>
1603
1692
  <html lang="ja">
@@ -1607,7 +1696,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1607
1696
  <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" />
1608
1697
  <meta http-equiv="Pragma" content="no-cache" />
1609
1698
  <meta http-equiv="Expires" content="0" />
1610
- <title>${title} | reviw</title>
1699
+ <title>${relativePath} | reviw</title>
1611
1700
  <style>
1612
1701
  /* Dark theme (default) */
1613
1702
  :root {
@@ -1688,7 +1777,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1688
1777
  }
1689
1778
  header .meta { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
1690
1779
  header .actions { display: flex; gap: 8px; align-items: center; }
1691
- header h1 { font-size: 16px; margin: 0; font-weight: 700; }
1780
+ header h1 { display: flex; flex-direction: column; margin: 0; line-height: 1.3; }
1781
+ header h1 .title-path { font-size: 11px; font-weight: 400; color: var(--muted); }
1782
+ header h1 .title-file { font-size: 16px; font-weight: 700; }
1692
1783
  header .badge {
1693
1784
  background: var(--selected-bg);
1694
1785
  color: var(--text);
@@ -1909,12 +2000,14 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1909
2000
  transition: background 200ms ease, border-color 200ms ease;
1910
2001
  }
1911
2002
  .floating header {
1912
- position: relative;
1913
- top: 0;
2003
+ position: static;
1914
2004
  background: transparent;
2005
+ backdrop-filter: none;
1915
2006
  border: none;
1916
2007
  padding: 0 0 8px 0;
2008
+ display: flex;
1917
2009
  justify-content: space-between;
2010
+ align-items: center;
1918
2011
  }
1919
2012
  .floating h2 { font-size: 14px; margin: 0; color: var(--text); }
1920
2013
  .floating button {
@@ -1943,6 +2036,10 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1943
2036
  line-height: 1.4;
1944
2037
  transition: background 200ms ease, border-color 200ms ease;
1945
2038
  }
2039
+ .floating textarea:focus {
2040
+ outline: none;
2041
+ border-color: var(--accent);
2042
+ }
1946
2043
  .floating .actions {
1947
2044
  display: flex;
1948
2045
  gap: 8px;
@@ -2120,6 +2217,50 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2120
2217
  border-radius: 4px;
2121
2218
  font-size: 11px;
2122
2219
  }
2220
+ /* Reviw questions preview cards */
2221
+ .reviw-questions-preview {
2222
+ display: flex;
2223
+ flex-direction: column;
2224
+ gap: 8px;
2225
+ }
2226
+ .reviw-q-card {
2227
+ background: var(--code-bg);
2228
+ border: 1px solid var(--border);
2229
+ border-radius: 8px;
2230
+ padding: 10px 12px;
2231
+ }
2232
+ .reviw-q-card.resolved {
2233
+ border-left: 3px solid #22c55e;
2234
+ }
2235
+ .reviw-q-card.pending {
2236
+ border-left: 3px solid #f59e0b;
2237
+ }
2238
+ .reviw-q-header {
2239
+ font-size: 12px;
2240
+ color: var(--text-dim);
2241
+ margin-bottom: 4px;
2242
+ }
2243
+ .reviw-q-header strong {
2244
+ color: var(--accent);
2245
+ }
2246
+ .reviw-q-question {
2247
+ font-size: 13px;
2248
+ color: var(--text);
2249
+ margin-bottom: 6px;
2250
+ }
2251
+ .reviw-q-options {
2252
+ display: flex;
2253
+ flex-wrap: wrap;
2254
+ gap: 4px;
2255
+ margin-bottom: 6px;
2256
+ }
2257
+ .reviw-q-answer {
2258
+ font-size: 12px;
2259
+ color: #22c55e;
2260
+ background: rgba(34, 197, 94, 0.1);
2261
+ padding: 4px 8px;
2262
+ border-radius: 4px;
2263
+ }
2123
2264
  [data-theme="light"] .frontmatter-table tbody th {
2124
2265
  color: #7c3aed;
2125
2266
  }
@@ -2267,6 +2408,267 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2267
2408
  border-radius: 8px;
2268
2409
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
2269
2410
  }
2411
+ /* Reviw Questions Modal */
2412
+ .reviw-questions-overlay {
2413
+ display: none;
2414
+ position: fixed;
2415
+ inset: 0;
2416
+ background: rgba(0, 0, 0, 0.8);
2417
+ z-index: 1100;
2418
+ justify-content: center;
2419
+ align-items: center;
2420
+ }
2421
+ .reviw-questions-overlay.visible {
2422
+ display: flex;
2423
+ }
2424
+ .reviw-questions-modal {
2425
+ background: var(--card-bg);
2426
+ border: 1px solid var(--border);
2427
+ border-radius: 16px;
2428
+ width: 90%;
2429
+ max-width: 600px;
2430
+ max-height: 80vh;
2431
+ display: flex;
2432
+ flex-direction: column;
2433
+ box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
2434
+ }
2435
+ .reviw-questions-header {
2436
+ display: flex;
2437
+ justify-content: space-between;
2438
+ align-items: center;
2439
+ padding: 16px 20px;
2440
+ border-bottom: 1px solid var(--border);
2441
+ }
2442
+ .reviw-questions-header h2 {
2443
+ margin: 0;
2444
+ font-size: 16px;
2445
+ font-weight: 600;
2446
+ color: var(--text);
2447
+ }
2448
+ .reviw-questions-header h2 span {
2449
+ font-size: 14px;
2450
+ color: var(--text-dim);
2451
+ font-weight: 400;
2452
+ }
2453
+ .reviw-questions-close {
2454
+ width: 32px;
2455
+ height: 32px;
2456
+ border: none;
2457
+ background: transparent;
2458
+ color: var(--text-dim);
2459
+ font-size: 18px;
2460
+ cursor: pointer;
2461
+ border-radius: 8px;
2462
+ transition: all 150ms ease;
2463
+ }
2464
+ .reviw-questions-close:hover {
2465
+ background: var(--border);
2466
+ color: var(--text);
2467
+ }
2468
+ .reviw-questions-body {
2469
+ flex: 1;
2470
+ overflow-y: auto;
2471
+ padding: 16px 20px;
2472
+ }
2473
+ .reviw-questions-footer {
2474
+ padding: 12px 20px;
2475
+ border-top: 1px solid var(--border);
2476
+ display: flex;
2477
+ justify-content: flex-end;
2478
+ }
2479
+ .reviw-questions-later {
2480
+ padding: 8px 16px;
2481
+ border: 1px solid var(--border);
2482
+ background: transparent;
2483
+ color: var(--text-dim);
2484
+ border-radius: 8px;
2485
+ cursor: pointer;
2486
+ font-size: 13px;
2487
+ transition: all 150ms ease;
2488
+ }
2489
+ .reviw-questions-later:hover {
2490
+ background: var(--border);
2491
+ color: var(--text);
2492
+ }
2493
+ /* Question Item */
2494
+ .reviw-question-item {
2495
+ margin-bottom: 20px;
2496
+ padding-bottom: 20px;
2497
+ border-bottom: 1px solid var(--border);
2498
+ }
2499
+ .reviw-question-item:last-child {
2500
+ margin-bottom: 0;
2501
+ padding-bottom: 0;
2502
+ border-bottom: none;
2503
+ }
2504
+ .reviw-question-text {
2505
+ font-size: 14px;
2506
+ color: var(--text);
2507
+ margin-bottom: 12px;
2508
+ line-height: 1.5;
2509
+ }
2510
+ .reviw-question-options {
2511
+ display: flex;
2512
+ flex-wrap: wrap;
2513
+ gap: 8px;
2514
+ margin-bottom: 12px;
2515
+ }
2516
+ .reviw-question-option {
2517
+ padding: 8px 14px;
2518
+ border: 1px solid var(--border);
2519
+ background: transparent;
2520
+ color: var(--text);
2521
+ border-radius: 8px;
2522
+ cursor: pointer;
2523
+ font-size: 13px;
2524
+ transition: all 150ms ease;
2525
+ }
2526
+ .reviw-question-option:hover {
2527
+ border-color: var(--accent);
2528
+ background: rgba(96, 165, 250, 0.1);
2529
+ }
2530
+ .reviw-question-option.selected {
2531
+ border-color: var(--accent);
2532
+ background: var(--accent);
2533
+ color: var(--text-inverse);
2534
+ }
2535
+ .reviw-question-input {
2536
+ width: 100%;
2537
+ padding: 10px 12px;
2538
+ border: 1px solid var(--border);
2539
+ background: var(--input-bg);
2540
+ color: var(--text);
2541
+ border-radius: 8px;
2542
+ font-size: 13px;
2543
+ resize: vertical;
2544
+ min-height: 60px;
2545
+ }
2546
+ .reviw-question-input:focus {
2547
+ outline: none;
2548
+ border-color: var(--accent);
2549
+ }
2550
+ .reviw-question-input::placeholder {
2551
+ color: var(--text-dim);
2552
+ }
2553
+ .reviw-check-mark {
2554
+ color: #22c55e;
2555
+ font-weight: bold;
2556
+ }
2557
+ .reviw-question-item.answered {
2558
+ border-color: #22c55e;
2559
+ background: rgba(34, 197, 94, 0.05);
2560
+ }
2561
+ .reviw-question-submit {
2562
+ margin-top: 10px;
2563
+ padding: 8px 16px;
2564
+ border: none;
2565
+ background: var(--accent);
2566
+ color: var(--text-inverse);
2567
+ border-radius: 8px;
2568
+ cursor: pointer;
2569
+ font-size: 13px;
2570
+ font-weight: 500;
2571
+ transition: all 150ms ease;
2572
+ }
2573
+ .reviw-question-submit:hover {
2574
+ filter: brightness(1.1);
2575
+ }
2576
+ .reviw-question-submit:disabled {
2577
+ opacity: 0.5;
2578
+ cursor: not-allowed;
2579
+ }
2580
+ /* Resolved Section */
2581
+ .reviw-resolved-section {
2582
+ margin-top: 16px;
2583
+ border-top: 1px solid var(--border);
2584
+ padding-top: 12px;
2585
+ }
2586
+ .reviw-resolved-toggle {
2587
+ display: flex;
2588
+ align-items: center;
2589
+ gap: 8px;
2590
+ background: none;
2591
+ border: none;
2592
+ color: var(--text-dim);
2593
+ font-size: 13px;
2594
+ cursor: pointer;
2595
+ padding: 4px 0;
2596
+ }
2597
+ .reviw-resolved-toggle:hover {
2598
+ color: var(--text);
2599
+ }
2600
+ .reviw-resolved-toggle .arrow {
2601
+ transition: transform 150ms ease;
2602
+ }
2603
+ .reviw-resolved-toggle.open .arrow {
2604
+ transform: rotate(90deg);
2605
+ }
2606
+ .reviw-resolved-list {
2607
+ display: none;
2608
+ margin-top: 12px;
2609
+ }
2610
+ .reviw-resolved-list.visible {
2611
+ display: block;
2612
+ }
2613
+ .reviw-resolved-item {
2614
+ padding: 10px 12px;
2615
+ background: var(--input-bg);
2616
+ border-radius: 8px;
2617
+ margin-bottom: 8px;
2618
+ opacity: 0.7;
2619
+ }
2620
+ .reviw-resolved-item:last-child {
2621
+ margin-bottom: 0;
2622
+ }
2623
+ .reviw-resolved-q {
2624
+ font-size: 12px;
2625
+ color: var(--text-dim);
2626
+ margin-bottom: 4px;
2627
+ }
2628
+ .reviw-resolved-a {
2629
+ font-size: 13px;
2630
+ color: var(--text);
2631
+ }
2632
+ /* Notice Bar */
2633
+ .reviw-questions-bar {
2634
+ display: none;
2635
+ position: fixed;
2636
+ top: 0;
2637
+ left: 0;
2638
+ right: 0;
2639
+ background: var(--accent);
2640
+ color: var(--text-inverse);
2641
+ padding: 8px 16px;
2642
+ font-size: 13px;
2643
+ z-index: 1050;
2644
+ justify-content: center;
2645
+ align-items: center;
2646
+ gap: 12px;
2647
+ }
2648
+ .reviw-questions-bar.visible {
2649
+ display: flex;
2650
+ }
2651
+ .reviw-questions-bar button {
2652
+ padding: 4px 12px;
2653
+ border: 1px solid rgba(255, 255, 255, 0.3);
2654
+ background: rgba(255, 255, 255, 0.1);
2655
+ color: var(--text-inverse);
2656
+ border-radius: 6px;
2657
+ cursor: pointer;
2658
+ font-size: 12px;
2659
+ transition: all 150ms ease;
2660
+ }
2661
+ .reviw-questions-bar button:hover {
2662
+ background: rgba(255, 255, 255, 0.2);
2663
+ }
2664
+ /* Adjust layout when bar is visible */
2665
+ body.has-questions-bar header {
2666
+ top: 36px;
2667
+ }
2668
+ body.has-questions-bar .toolbar,
2669
+ body.has-questions-bar .table-wrap {
2670
+ margin-top: 36px;
2671
+ }
2270
2672
  /* Copy notification toast */
2271
2673
  .copy-toast {
2272
2674
  position: fixed;
@@ -2529,7 +2931,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2529
2931
  <body>
2530
2932
  <header>
2531
2933
  <div class="meta">
2532
- <h1>${title}</h1>
2934
+ <h1><span class="title-path">${projectRoot}</span><span class="title-file">${relativePath}</span></h1>
2533
2935
  <span class="badge">Click to comment / ESC to cancel</span>
2534
2936
  <span class="pill">Comments <strong id="comment-count">0</strong></span>
2535
2937
  </div>
@@ -2682,11 +3084,32 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2682
3084
  <div class="video-container" id="video-container"></div>
2683
3085
  </div>
2684
3086
 
3087
+ <!-- Reviw Questions Modal -->
3088
+ <div class="reviw-questions-overlay" id="reviw-questions-overlay">
3089
+ <div class="reviw-questions-modal" id="reviw-questions-modal">
3090
+ <div class="reviw-questions-header">
3091
+ <h2>📋 AIからの質問 <span id="reviw-questions-count"></span></h2>
3092
+ <button class="reviw-questions-close" id="reviw-questions-close" aria-label="Close">✕</button>
3093
+ </div>
3094
+ <div class="reviw-questions-body" id="reviw-questions-body"></div>
3095
+ <div class="reviw-questions-footer">
3096
+ <button class="reviw-questions-later" id="reviw-questions-later">後で回答する</button>
3097
+ </div>
3098
+ </div>
3099
+ </div>
3100
+
3101
+ <!-- Reviw Questions Notice Bar -->
3102
+ <div class="reviw-questions-bar" id="reviw-questions-bar">
3103
+ <span id="reviw-questions-bar-message">\ud83d\udccb \u672a\u56de\u7b54\u306e\u8cea\u554f\u304c<span id="reviw-questions-bar-count">0</span>\u4ef6\u3042\u308a\u307e\u3059</span>
3104
+ <button id="reviw-questions-bar-open">\u8cea\u554f\u3092\u898b\u308b</button>
3105
+ </div>
3106
+
2685
3107
  <script>
2686
3108
  const DATA = ${serialized};
2687
3109
  const MAX_COLS = ${cols};
2688
3110
  const FILE_NAME = ${titleJson};
2689
3111
  const MODE = ${modeJson};
3112
+ const REVIW_QUESTIONS = ${questionsJson};
2690
3113
 
2691
3114
  // --- Theme Management ---
2692
3115
  (function initTheme() {
@@ -3045,6 +3468,12 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3045
3468
 
3046
3469
  function openCardForSelection() {
3047
3470
  if (!selection) return;
3471
+ // Don't open card while image/video modal is visible
3472
+ const imageOverlay = document.getElementById('image-fullscreen');
3473
+ const videoOverlay = document.getElementById('video-fullscreen');
3474
+ if (imageOverlay?.classList.contains('visible') || videoOverlay?.classList.contains('visible')) {
3475
+ return;
3476
+ }
3048
3477
  const { startRow, endRow, startCol, endCol } = selection;
3049
3478
  const isSingleCell = startRow === endRow && startCol === endCol;
3050
3479
 
@@ -3561,6 +3990,22 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3561
3990
  if (globalComment.trim()) {
3562
3991
  data.summary = globalComment.trim();
3563
3992
  }
3993
+ // Include answered questions
3994
+ if (window.REVIW_ANSWERS) {
3995
+ const answeredQuestions = [];
3996
+ for (const [id, answer] of Object.entries(window.REVIW_ANSWERS)) {
3997
+ if (answer.selected || answer.text.trim()) {
3998
+ answeredQuestions.push({
3999
+ id,
4000
+ selected: answer.selected,
4001
+ text: answer.text.trim()
4002
+ });
4003
+ }
4004
+ }
4005
+ if (answeredQuestions.length > 0) {
4006
+ data.reviwAnswers = answeredQuestions;
4007
+ }
4008
+ }
3564
4009
  return data;
3565
4010
  }
3566
4011
  function sendAndExit(reason = 'pagehide') {
@@ -4042,8 +4487,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
4042
4487
  }
4043
4488
 
4044
4489
  if (imageOverlay) {
4490
+ // Close on any click (including image itself)
4045
4491
  imageOverlay.addEventListener('click', (e) => {
4046
- if (e.target === imageOverlay) closeImageOverlay();
4492
+ closeImageOverlay();
4047
4493
  });
4048
4494
  }
4049
4495
 
@@ -4058,7 +4504,8 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
4058
4504
  img.title = 'Click to view fullscreen';
4059
4505
 
4060
4506
  img.addEventListener('click', (e) => {
4061
- e.stopPropagation();
4507
+ // Don't stop propagation - allow select to work
4508
+ e.preventDefault();
4062
4509
 
4063
4510
  imageContainer.innerHTML = '';
4064
4511
  const clonedImg = img.cloneNode(true);
@@ -4103,8 +4550,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
4103
4550
  }
4104
4551
 
4105
4552
  if (videoOverlay) {
4553
+ // Close on any click (including video itself)
4106
4554
  videoOverlay.addEventListener('click', (e) => {
4107
- if (e.target === videoOverlay) closeVideoOverlay();
4555
+ closeVideoOverlay();
4108
4556
  });
4109
4557
  }
4110
4558
 
@@ -4123,7 +4571,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
4123
4571
 
4124
4572
  link.addEventListener('click', (e) => {
4125
4573
  e.preventDefault();
4126
- e.stopPropagation();
4574
+ // Don't stop propagation - allow select to work
4127
4575
 
4128
4576
  // Remove existing video if any
4129
4577
  const existingVideo = videoContainer.querySelector('video');
@@ -4347,15 +4795,11 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
4347
4795
 
4348
4796
  // Click on block elements
4349
4797
  preview.addEventListener('click', (e) => {
4350
- // Handle image clicks
4798
+ // Handle image clicks - always select, even if modal is showing
4351
4799
  if (e.target.tagName === 'IMG') {
4352
- if (!e.defaultPrevented) {
4353
- const line = findImageSourceLine(e.target.src);
4354
- if (line > 0) {
4355
- e.preventDefault();
4356
- e.stopPropagation();
4357
- selectSourceRange(line);
4358
- }
4800
+ const line = findImageSourceLine(e.target.src);
4801
+ if (line > 0) {
4802
+ selectSourceRange(line);
4359
4803
  }
4360
4804
  return;
4361
4805
  }
@@ -4425,6 +4869,227 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
4425
4869
  }, 10);
4426
4870
  });
4427
4871
  })();
4872
+
4873
+ // --- Reviw Questions Modal ---
4874
+ (function initReviwQuestions() {
4875
+ if (MODE !== 'markdown') return;
4876
+ if (!REVIW_QUESTIONS || REVIW_QUESTIONS.length === 0) return;
4877
+
4878
+ const overlay = document.getElementById('reviw-questions-overlay');
4879
+ const modal = document.getElementById('reviw-questions-modal');
4880
+ const body = document.getElementById('reviw-questions-body');
4881
+ const closeBtn = document.getElementById('reviw-questions-close');
4882
+ const laterBtn = document.getElementById('reviw-questions-later');
4883
+ const countSpan = document.getElementById('reviw-questions-count');
4884
+ const bar = document.getElementById('reviw-questions-bar');
4885
+ const barMessage = document.getElementById('reviw-questions-bar-message');
4886
+ const barCount = document.getElementById('reviw-questions-bar-count');
4887
+ const barOpenBtn = document.getElementById('reviw-questions-bar-open');
4888
+
4889
+ if (!overlay || !modal || !body) return;
4890
+
4891
+ // Store answers locally
4892
+ const answers = {};
4893
+ REVIW_QUESTIONS.forEach(q => {
4894
+ answers[q.id] = { selected: '', text: '' };
4895
+ });
4896
+
4897
+ // Count unresolved questions
4898
+ const unresolvedQuestions = REVIW_QUESTIONS.filter(q => !q.resolved);
4899
+ const resolvedQuestions = REVIW_QUESTIONS.filter(q => q.resolved);
4900
+
4901
+ function getUnansweredCount() {
4902
+ // Count questions that have no answer (no selection and no text)
4903
+ return unresolvedQuestions.filter(q => {
4904
+ const a = answers[q.id];
4905
+ return !a.selected && !a.text.trim();
4906
+ }).length;
4907
+ }
4908
+
4909
+ function updateCounts() {
4910
+ const unansweredCount = getUnansweredCount();
4911
+ if (unansweredCount > 0) {
4912
+ countSpan.textContent = '(' + unansweredCount + '\u4ef6\u672a\u56de\u7b54)';
4913
+ barMessage.innerHTML = '\ud83d\udccb \u672a\u56de\u7b54\u306e\u8cea\u554f\u304c<span id="reviw-questions-bar-count">' + unansweredCount + '</span>\u4ef6\u3042\u308a\u307e\u3059';
4914
+ laterBtn.textContent = '\u5f8c\u3067\u56de\u7b54\u3059\u308b';
4915
+ } else {
4916
+ countSpan.textContent = '(\u5168\u3066\u56de\u7b54\u6e08\u307f)';
4917
+ barMessage.innerHTML = '\u2705 \u5168\u3066\u306e\u8cea\u554f\u306b\u56de\u7b54\u3057\u307e\u3057\u305f';
4918
+ laterBtn.textContent = '\u9589\u3058\u308b';
4919
+ }
4920
+ }
4921
+
4922
+ function checkAllAnswered() {
4923
+ if (getUnansweredCount() === 0) {
4924
+ // All answered - close modal but keep bar visible
4925
+ setTimeout(() => {
4926
+ overlay.classList.remove('visible');
4927
+ // Keep bar visible with different message
4928
+ bar.classList.add('visible');
4929
+ document.body.classList.add('has-questions-bar');
4930
+ }, 500);
4931
+ }
4932
+ }
4933
+
4934
+ function renderQuestions() {
4935
+ body.innerHTML = '';
4936
+
4937
+ // Render unresolved questions
4938
+ unresolvedQuestions.forEach((q, idx) => {
4939
+ const item = document.createElement('div');
4940
+ item.className = 'reviw-question-item';
4941
+ item.dataset.id = q.id;
4942
+
4943
+ let optionsHtml = '';
4944
+ if (q.options && q.options.length > 0) {
4945
+ optionsHtml = '<div class="reviw-question-options">' +
4946
+ q.options.map(opt =>
4947
+ '<button class="reviw-question-option" data-value="' + escapeAttr(opt) + '">' + escapeHtml(opt) + '</button>'
4948
+ ).join('') +
4949
+ '</div>';
4950
+ }
4951
+
4952
+ const isOkOnly = q.options && q.options.length === 1 && q.options[0] === 'OK';
4953
+
4954
+ item.innerHTML =
4955
+ '<div class="reviw-question-text">Q' + (idx + 1) + '. ' + escapeHtml(q.question) + '<span class="reviw-check-mark"></span></div>' +
4956
+ optionsHtml +
4957
+ (isOkOnly ? '' : '<textarea class="reviw-question-input" placeholder="\u30c6\u30ad\u30b9\u30c8\u3067\u56de\u7b54\u30fb\u88dc\u8db3..."></textarea>');
4958
+
4959
+ body.appendChild(item);
4960
+
4961
+ // Check mark element
4962
+ const checkMark = item.querySelector('.reviw-check-mark');
4963
+
4964
+ function updateCheckMark() {
4965
+ const answer = answers[q.id];
4966
+ const hasAnswer = answer.selected || answer.text.trim();
4967
+ if (hasAnswer) {
4968
+ checkMark.textContent = ' \u2713';
4969
+ item.classList.add('answered');
4970
+ } else {
4971
+ checkMark.textContent = '';
4972
+ item.classList.remove('answered');
4973
+ }
4974
+ }
4975
+
4976
+ // Option click handlers - always toggle
4977
+ const optionBtns = item.querySelectorAll('.reviw-question-option');
4978
+ optionBtns.forEach(btn => {
4979
+ btn.addEventListener('click', () => {
4980
+ const wasSelected = btn.classList.contains('selected');
4981
+ optionBtns.forEach(b => b.classList.remove('selected'));
4982
+ if (!wasSelected) {
4983
+ btn.classList.add('selected');
4984
+ answers[q.id].selected = btn.dataset.value;
4985
+ } else {
4986
+ answers[q.id].selected = '';
4987
+ }
4988
+ updateCheckMark();
4989
+ updateCounts();
4990
+ checkAllAnswered();
4991
+ });
4992
+ });
4993
+
4994
+ // Text input handler
4995
+ const textarea = item.querySelector('.reviw-question-input');
4996
+ if (textarea) {
4997
+ textarea.addEventListener('input', () => {
4998
+ answers[q.id].text = textarea.value;
4999
+ updateCheckMark();
5000
+ updateCounts();
5001
+ checkAllAnswered();
5002
+ });
5003
+ }
5004
+
5005
+ updateCheckMark();
5006
+ });
5007
+
5008
+ // Render resolved questions (collapsed)
5009
+ if (resolvedQuestions.length > 0) {
5010
+ const section = document.createElement('div');
5011
+ section.className = 'reviw-resolved-section';
5012
+ section.innerHTML =
5013
+ '<button class="reviw-resolved-toggle">' +
5014
+ '<span class="arrow">\u25b6</span> \u89e3\u6c7a\u6e08\u307f (' + resolvedQuestions.length + '\u4ef6)' +
5015
+ '</button>' +
5016
+ '<div class="reviw-resolved-list">' +
5017
+ resolvedQuestions.map(q =>
5018
+ '<div class="reviw-resolved-item">' +
5019
+ '<div class="reviw-resolved-q">' + escapeHtml(q.question) + '</div>' +
5020
+ '<div class="reviw-resolved-a">\u2192 ' + escapeHtml(q.answer || '(no answer)') + '</div>' +
5021
+ '</div>'
5022
+ ).join('') +
5023
+ '</div>';
5024
+ body.appendChild(section);
5025
+
5026
+ const toggle = section.querySelector('.reviw-resolved-toggle');
5027
+ const list = section.querySelector('.reviw-resolved-list');
5028
+ toggle.addEventListener('click', () => {
5029
+ toggle.classList.toggle('open');
5030
+ list.classList.toggle('visible');
5031
+ });
5032
+ }
5033
+ }
5034
+
5035
+ function escapeHtml(str) {
5036
+ return String(str)
5037
+ .replace(/&/g, '&amp;')
5038
+ .replace(/</g, '&lt;')
5039
+ .replace(/>/g, '&gt;');
5040
+ }
5041
+
5042
+ function escapeAttr(str) {
5043
+ return String(str)
5044
+ .replace(/&/g, '&amp;')
5045
+ .replace(/"/g, '&quot;');
5046
+ }
5047
+
5048
+ function openModal() {
5049
+ overlay.classList.add('visible');
5050
+ bar.classList.remove('visible');
5051
+ document.body.classList.remove('has-questions-bar');
5052
+ }
5053
+
5054
+ function closeModal(allAnswered) {
5055
+ overlay.classList.remove('visible');
5056
+ const unansweredCount = getUnansweredCount();
5057
+ if (unansweredCount > 0 && !allAnswered) {
5058
+ bar.classList.add('visible');
5059
+ document.body.classList.add('has-questions-bar');
5060
+ } else {
5061
+ bar.classList.remove('visible');
5062
+ document.body.classList.remove('has-questions-bar');
5063
+ }
5064
+ }
5065
+
5066
+ // Event listeners
5067
+ closeBtn.addEventListener('click', () => closeModal(false));
5068
+ laterBtn.addEventListener('click', () => closeModal(false));
5069
+ barOpenBtn.addEventListener('click', openModal);
5070
+
5071
+ overlay.addEventListener('click', (e) => {
5072
+ if (e.target === overlay) closeModal(false);
5073
+ });
5074
+
5075
+ document.addEventListener('keydown', (e) => {
5076
+ if (e.key === 'Escape' && overlay.classList.contains('visible')) {
5077
+ closeModal(false);
5078
+ }
5079
+ });
5080
+
5081
+ // Expose answers for submit (only answered ones)
5082
+ window.REVIW_ANSWERS = answers;
5083
+
5084
+ // Initialize
5085
+ updateCounts();
5086
+ renderQuestions();
5087
+
5088
+ // Show modal on load if there are unresolved questions
5089
+ if (unresolvedQuestions.length > 0) {
5090
+ setTimeout(() => openModal(), 300);
5091
+ }
5092
+ })();
4428
5093
  </script>
4429
5094
  </body>
4430
5095
  </html>`;
@@ -4435,8 +5100,8 @@ function buildHtml(filePath) {
4435
5100
  if (data.mode === "diff") {
4436
5101
  return diffHtmlTemplate(data);
4437
5102
  }
4438
- const { rows, cols, title, mode, preview } = data;
4439
- return htmlTemplate(rows, cols, title, mode, preview);
5103
+ const { rows, cols, projectRoot, relativePath, mode, preview, reviwQuestions } = data;
5104
+ return htmlTemplate(rows, cols, projectRoot, relativePath, mode, preview, reviwQuestions);
4440
5105
  }
4441
5106
 
4442
5107
  // --- HTTP Server -----------------------------------------------------------
@@ -4468,6 +5133,20 @@ function outputAllResults() {
4468
5133
  const yamlOut = yaml.dump(combined, { noRefs: true, lineWidth: 120 });
4469
5134
  console.log(yamlOut.trim());
4470
5135
  }
5136
+
5137
+ // Output answered questions if any
5138
+ const allAnswers = [];
5139
+ for (const result of allResults) {
5140
+ if (result.reviwAnswers && result.reviwAnswers.length > 0) {
5141
+ allAnswers.push(...result.reviwAnswers);
5142
+ }
5143
+ }
5144
+ if (allAnswers.length > 0) {
5145
+ console.log("\n[REVIW_ANSWERS]");
5146
+ const answersYaml = yaml.dump(allAnswers, { noRefs: true, lineWidth: 120 });
5147
+ console.log(answersYaml.trim());
5148
+ console.log("[/REVIW_ANSWERS]");
5149
+ }
4471
5150
  }
4472
5151
 
4473
5152
  function checkAllDone() {
@@ -4714,7 +5393,14 @@ function createFileServer(filePath, fileIndex = 0) {
4714
5393
  const delay = fileIndex * 300;
4715
5394
  setTimeout(() => {
4716
5395
  try {
4717
- spawn(opener, [url], { stdio: "ignore", detached: true });
5396
+ const child = spawn(opener, [url], { stdio: "ignore", detached: true });
5397
+ child.on('error', (err) => {
5398
+ console.warn(
5399
+ "Failed to open browser automatically. Please open this URL manually:",
5400
+ url,
5401
+ );
5402
+ });
5403
+ child.unref();
4718
5404
  } catch (err) {
4719
5405
  console.warn(
4720
5406
  "Failed to open browser automatically. Please open this URL manually:",
@@ -4882,7 +5568,14 @@ function createDiffServer(diffContent) {
4882
5568
  ? "start"
4883
5569
  : "xdg-open";
4884
5570
  try {
4885
- spawn(opener, [url], { stdio: "ignore", detached: true });
5571
+ const child = spawn(opener, [url], { stdio: "ignore", detached: true });
5572
+ child.on('error', (err) => {
5573
+ console.warn(
5574
+ "Failed to open browser automatically. Please open this URL manually:",
5575
+ url,
5576
+ );
5577
+ });
5578
+ child.unref();
4886
5579
  } catch (err) {
4887
5580
  console.warn(
4888
5581
  "Failed to open browser automatically. Please open this URL manually:",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reviw",
3
- "version": "0.10.7",
3
+ "version": "0.11.0",
4
4
  "description": "Lightweight file reviewer with in-browser comments for CSV, TSV, Markdown, and Git diffs.",
5
5
  "type": "module",
6
6
  "bin": {