Last active
March 9, 2026 19:37
-
-
Save bojanrajkovic/813a06309e80d9e445ab47ec1d8e12dc to your computer and use it in GitHub Desktop.
Loupe: Rendered-Mode Comment Re-Anchoring — Four Anchor States Prototype
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Loupe — Rendered-Mode Re-Anchoring States</title> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #0e0e10; | |
| --panel-bg: #131316; | |
| --surface: #1c1c22; | |
| --surface-hover: #202028; | |
| --border: rgba(255, 255, 255, 0.08); | |
| --border-subtle: rgba(255, 255, 255, 0.05); | |
| --text: #e8e6f0; | |
| --text-muted: #7e7a96; | |
| --text-dim: #4e4a66; | |
| /* document text */ | |
| --doc-text: #d4d2e0; | |
| --doc-text-muted: #7e7a96; | |
| --doc-heading: #eceaf4; | |
| --doc-link: #93c5fd; | |
| --doc-code-bg: #0f0f12; | |
| --doc-code-text: #a8b4c8; | |
| --doc-blockquote: rgba(255, 255, 255, 0.05); | |
| /* state accent colors — severity gradient */ | |
| --anchored: #34d399; | |
| --anchored-dim: rgba(52, 211, 153, 0.10); | |
| --shifted: #60a5fa; | |
| --shifted-dim: rgba(96, 165, 250, 0.12); | |
| --outdated: #f59e0b; | |
| --outdated-dim: rgba(245, 158, 11, 0.12); | |
| --orphaned: #dc2626; | |
| --orphaned-dim: rgba(220, 38, 38, 0.12); | |
| /* text highlight in anchored state */ | |
| --highlight-bg: rgba(250, 204, 21, 0.14); | |
| --highlight-border: rgba(250, 204, 21, 0.35); | |
| --radius-sm: 4px; | |
| --radius: 6px; | |
| --radius-lg: 10px; | |
| --mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', ui-monospace, 'Courier New', monospace; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| line-height: 1.5; | |
| } | |
| /* ── Layout ── */ | |
| .app { | |
| max-width: 960px; | |
| margin: 0 auto; | |
| padding: 32px 24px 64px; | |
| } | |
| /* ── Header ── */ | |
| .header { | |
| margin-bottom: 32px; | |
| } | |
| .header-eyebrow { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| margin-bottom: 10px; | |
| } | |
| .header h1 { | |
| font-size: 26px; | |
| font-weight: 700; | |
| color: var(--text); | |
| letter-spacing: -0.02em; | |
| margin-bottom: 6px; | |
| } | |
| .header p { | |
| font-size: 14px; | |
| color: var(--text-muted); | |
| max-width: 680px; | |
| line-height: 1.7; | |
| } | |
| /* ── Tab Navigation ── */ | |
| .tabs { | |
| display: flex; | |
| gap: 4px; | |
| margin-bottom: 28px; | |
| background: var(--panel-bg); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: 5px; | |
| } | |
| .tab { | |
| flex: 1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 9px 16px; | |
| border-radius: var(--radius); | |
| border: none; | |
| background: none; | |
| cursor: pointer; | |
| font-size: 13px; | |
| font-weight: 500; | |
| color: var(--text-muted); | |
| transition: background 0.18s ease, color 0.18s ease; | |
| white-space: nowrap; | |
| } | |
| .tab:hover { | |
| background: var(--surface-hover); | |
| color: var(--text); | |
| } | |
| .tab.active { | |
| background: var(--surface); | |
| color: var(--text); | |
| } | |
| .tab.active.tab-anchored { background: var(--anchored-dim); color: var(--anchored); } | |
| .tab.active.tab-shifted { background: var(--shifted-dim); color: var(--shifted); } | |
| .tab.active.tab-outdated { background: var(--outdated-dim); color: var(--outdated); } | |
| .tab.active.tab-orphaned { background: var(--orphaned-dim); color: var(--orphaned); } | |
| /* ── State Panels ── */ | |
| .state-panel { display: none; } | |
| .state-panel.active { display: block; } | |
| /* ── State Info Caption ── */ | |
| .state-info { | |
| padding: 6px 0; | |
| margin-bottom: 16px; | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| line-height: 1.5; | |
| } | |
| /* ── Review Layout: document + margin ── */ | |
| .review-layout { | |
| display: grid; | |
| grid-template-columns: 1fr 300px; | |
| gap: 0; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| overflow: hidden; | |
| background: var(--panel-bg); | |
| align-items: start; | |
| } | |
| /* ── Document Area ── */ | |
| .document-area { | |
| padding: 40px 48px 48px; | |
| border-right: 1px solid var(--border); | |
| min-height: 600px; | |
| overflow: hidden; | |
| } | |
| /* ── Document Typography ── */ | |
| .doc-title { | |
| font-size: 28px; | |
| font-weight: 700; | |
| color: var(--doc-heading); | |
| letter-spacing: -0.02em; | |
| line-height: 1.25; | |
| margin-bottom: 8px; | |
| } | |
| .doc-byline { | |
| font-size: 13px; | |
| color: var(--text-dim); | |
| margin-bottom: 36px; | |
| padding-bottom: 24px; | |
| border-bottom: 1px solid var(--border-subtle); | |
| } | |
| .doc-h2 { | |
| font-size: 20px; | |
| font-weight: 700; | |
| color: var(--doc-heading); | |
| letter-spacing: -0.01em; | |
| line-height: 1.3; | |
| margin-top: 36px; | |
| margin-bottom: 12px; | |
| } | |
| .doc-h3 { | |
| font-size: 15px; | |
| font-weight: 600; | |
| color: var(--doc-heading); | |
| letter-spacing: 0; | |
| line-height: 1.4; | |
| margin-top: 24px; | |
| margin-bottom: 8px; | |
| } | |
| .doc-p { | |
| font-size: 15px; | |
| color: var(--doc-text); | |
| line-height: 1.7; | |
| margin-bottom: 16px; | |
| } | |
| .doc-p:last-child { margin-bottom: 0; } | |
| .doc-a { | |
| color: var(--doc-link); | |
| text-decoration: none; | |
| } | |
| .doc-a:hover { text-decoration: underline; } | |
| .doc-code { | |
| font-family: var(--mono); | |
| font-size: 12.5px; | |
| background: rgba(255, 255, 255, 0.07); | |
| color: var(--doc-code-text); | |
| padding: 1px 5px; | |
| border-radius: 3px; | |
| } | |
| .doc-pre { | |
| background: var(--doc-code-bg); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 16px 20px; | |
| margin: 16px 0; | |
| overflow-x: auto; | |
| } | |
| .doc-pre code { | |
| font-family: var(--mono); | |
| font-size: 12.5px; | |
| line-height: 1.65; | |
| color: #a8b4c8; | |
| } | |
| .doc-pre .kw { color: #c792ea; } | |
| .doc-pre .fn { color: #82aaff; } | |
| .doc-pre .str { color: #c3e88d; } | |
| .doc-pre .cm { color: #546e7a; font-style: italic; } | |
| .doc-pre .num { color: #f78c6c; } | |
| .doc-pre .ty { color: #ffcb6b; } | |
| .doc-ul { | |
| padding-left: 24px; | |
| margin-bottom: 16px; | |
| } | |
| .doc-ul li { | |
| font-size: 15px; | |
| color: var(--doc-text); | |
| line-height: 1.7; | |
| margin-bottom: 4px; | |
| } | |
| .doc-ul li::marker { color: var(--text-dim); } | |
| .doc-ol { | |
| padding-left: 24px; | |
| margin-bottom: 16px; | |
| } | |
| .doc-ol li { | |
| font-size: 15px; | |
| color: var(--doc-text); | |
| line-height: 1.7; | |
| margin-bottom: 4px; | |
| } | |
| .doc-ol li::marker { color: var(--text-dim); } | |
| .doc-blockquote { | |
| border-left: 3px solid var(--border); | |
| background: var(--doc-blockquote); | |
| margin: 16px 0; | |
| padding: 12px 20px; | |
| border-radius: 0 var(--radius) var(--radius) 0; | |
| } | |
| .doc-blockquote p { | |
| font-size: 14px; | |
| color: var(--text-muted); | |
| line-height: 1.65; | |
| font-style: italic; | |
| margin: 0; | |
| } | |
| /* ── Text Highlight (annotation) ── */ | |
| .text-highlight { | |
| background: var(--highlight-bg); | |
| border-bottom: 1.5px solid var(--highlight-border); | |
| border-radius: 2px; | |
| cursor: pointer; | |
| transition: background 0.15s ease; | |
| padding: 0 1px; | |
| } | |
| .text-highlight:hover { | |
| background: rgba(250, 204, 21, 0.22); | |
| } | |
| .text-highlight.active-highlight { | |
| background: rgba(250, 204, 21, 0.24); | |
| border-bottom-color: rgba(250, 204, 21, 0.6); | |
| } | |
| /* Shifted highlight */ | |
| .text-highlight-shifted { | |
| background: rgba(96, 165, 250, 0.12); | |
| border-bottom: 1.5px solid rgba(96, 165, 250, 0.35); | |
| border-radius: 2px; | |
| cursor: pointer; | |
| transition: background 0.15s ease; | |
| padding: 0 1px; | |
| } | |
| .text-highlight-shifted:hover { | |
| background: rgba(96, 165, 250, 0.2); | |
| } | |
| /* Ghost passage (where comment used to be in Shifted) */ | |
| /* Displayed as a block band between heading and paragraph */ | |
| .ghost-passage { | |
| display: block; | |
| border-left: 2px dashed rgba(96, 165, 250, 0.35); | |
| padding: 5px 10px; | |
| margin-bottom: 10px; | |
| color: rgba(96, 165, 250, 0.45); | |
| font-size: 11.5px; | |
| font-style: italic; | |
| cursor: default; | |
| pointer-events: none; | |
| } | |
| /* Outdated — thin amber underline as click target only */ | |
| .text-highlight-outdated { | |
| border-bottom: 1.5px solid rgba(245, 158, 11, 0.45); | |
| border-radius: 1px; | |
| cursor: pointer; | |
| padding: 0 1px; | |
| transition: background 0.15s ease; | |
| } | |
| .text-highlight-outdated:hover { | |
| background: rgba(245, 158, 11, 0.08); | |
| } | |
| /* ── Margin (sidebar) ── */ | |
| .margin-area { | |
| padding: 40px 0 48px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| /* ── Margin Comment ── */ | |
| .margin-comment { | |
| padding: 14px 16px; | |
| border-bottom: 1px solid var(--border-subtle); | |
| position: relative; | |
| } | |
| .margin-comment:first-child { border-top: none; } | |
| .margin-comment:last-child { border-bottom: none; } | |
| /* Connector line — visual association from comment to highlight */ | |
| .margin-comment::before { | |
| content: ''; | |
| position: absolute; | |
| left: 0; | |
| top: 20px; | |
| width: 3px; | |
| height: 28px; | |
| background: rgba(250, 204, 21, 0.3); | |
| border-radius: 0 2px 2px 0; | |
| } | |
| .margin-comment.state-shifted::before { background: rgba(96, 165, 250, 0.4); } | |
| .margin-comment.state-outdated::before { background: rgba(245, 158, 11, 0.4); } | |
| .margin-comment.state-orphaned::before { background: rgba(220, 38, 38, 0.4); } | |
| .margin-comment-header { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| } | |
| .margin-avatar { | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 50%; | |
| flex-shrink: 0; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 10px; | |
| font-weight: 700; | |
| color: #fff; | |
| background: #3a3a4a; | |
| margin-top: 1px; | |
| } | |
| .margin-meta { | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| .margin-author { | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: var(--text); | |
| } | |
| .margin-time { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| margin-left: 6px; | |
| } | |
| .margin-body { | |
| font-size: 12.5px; | |
| color: var(--doc-text); | |
| line-height: 1.6; | |
| padding-left: 32px; | |
| } | |
| .margin-body code { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| background: rgba(255, 255, 255, 0.07); | |
| color: var(--doc-code-text); | |
| padding: 1px 4px; | |
| border-radius: 3px; | |
| } | |
| /* Quoted text in margin */ | |
| .margin-quote { | |
| font-size: 11px; | |
| color: var(--text-dim); | |
| font-style: italic; | |
| border-left: 2px solid var(--border); | |
| padding: 2px 8px 2px 10px; | |
| margin-bottom: 8px; | |
| margin-left: 32px; | |
| line-height: 1.5; | |
| } | |
| .margin-quote mark { | |
| background: none; | |
| color: var(--text-muted); | |
| font-style: normal; | |
| } | |
| /* ── Anchored check microtext ── */ | |
| .anchored-check { | |
| font-size: 10px; | |
| color: var(--anchored); | |
| font-weight: 500; | |
| margin-left: 4px; | |
| } | |
| /* ── Badges ── */ | |
| .badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 2px 7px; | |
| border-radius: 20px; | |
| font-size: 10.5px; | |
| font-weight: 600; | |
| line-height: 1.5; | |
| white-space: nowrap; | |
| } | |
| .badge-shifted { background: rgba(96, 165, 250, 0.14); color: var(--shifted); border: 1px solid rgba(96, 165, 250, 0.25); } | |
| .badge-outdated { background: rgba(245, 158, 11, 0.14); color: var(--outdated); border: 1px solid rgba(245, 158, 11, 0.25); } | |
| .badge-orphaned { background: rgba(220, 38, 38, 0.14); color: var(--orphaned); border: 1px solid rgba(220, 38, 38, 0.25); } | |
| /* ── Breadcrumb trail ── */ | |
| .breadcrumb { | |
| display: flex; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 4px; | |
| font-size: 10.5px; | |
| color: var(--text-dim); | |
| margin-top: 4px; | |
| padding-left: 32px; | |
| line-height: 1.6; | |
| } | |
| .breadcrumb-sep { | |
| color: var(--text-dim); | |
| opacity: 0.5; | |
| } | |
| .breadcrumb-section { | |
| font-weight: 500; | |
| color: var(--text-muted); | |
| } | |
| .breadcrumb-arrow { | |
| color: var(--text-dim); | |
| opacity: 0.6; | |
| margin: 0 1px; | |
| } | |
| /* ── Collapsible original text block ── */ | |
| .collapsible-toggle { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| font-size: 11px; | |
| font-weight: 500; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| padding-left: 32px; | |
| margin-top: 8px; | |
| user-select: none; | |
| background: none; | |
| border: none; | |
| text-align: left; | |
| width: 100%; | |
| transition: color 0.15s ease; | |
| } | |
| .collapsible-toggle:hover { color: var(--text); } | |
| .collapsible-toggle .chev { | |
| transition: transform 0.2s ease; | |
| color: var(--text-dim); | |
| flex-shrink: 0; | |
| } | |
| .collapsible-toggle .chev.open { transform: rotate(180deg); } | |
| .collapsible-body { | |
| overflow: hidden; | |
| max-height: 0; | |
| transition: max-height 0.3s ease; | |
| } | |
| .collapsible-body.expanded { max-height: 300px; } | |
| .collapsible-inner { | |
| padding: 8px 14px 8px 32px; | |
| margin-top: 6px; | |
| } | |
| .original-text-block { | |
| background: rgba(245, 158, 11, 0.04); | |
| border: 1px solid rgba(245, 158, 11, 0.18); | |
| border-radius: var(--radius); | |
| padding: 10px 12px; | |
| font-size: 11.5px; | |
| font-style: italic; | |
| color: #c4a84f; | |
| line-height: 1.6; | |
| font-family: inherit; | |
| } | |
| .original-text-block.orphaned-text { | |
| background: rgba(220, 38, 38, 0.04); | |
| border-color: rgba(220, 38, 38, 0.18); | |
| color: #b07070; | |
| } | |
| .original-text-label { | |
| font-size: 10px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| color: var(--outdated); | |
| opacity: 0.7; | |
| margin-bottom: 6px; | |
| } | |
| .original-text-label.orphaned-label { color: var(--orphaned); } | |
| /* Outdated diff display */ | |
| .outdated-diff { | |
| margin-top: 10px; | |
| padding-left: 32px; | |
| } | |
| .outdated-diff-row { | |
| display: flex; | |
| align-items: baseline; | |
| gap: 6px; | |
| font-size: 11.5px; | |
| line-height: 1.65; | |
| padding: 1px 0; | |
| } | |
| .diff-sign-del { | |
| color: #f87171; | |
| font-weight: 700; | |
| flex-shrink: 0; | |
| width: 10px; | |
| font-family: var(--mono); | |
| } | |
| .diff-sign-add { | |
| color: #4ade80; | |
| font-weight: 700; | |
| flex-shrink: 0; | |
| width: 10px; | |
| font-family: var(--mono); | |
| } | |
| .diff-text-del { | |
| color: #c4a0a0; | |
| text-decoration: line-through; | |
| text-decoration-color: rgba(248, 113, 113, 0.6); | |
| font-size: 11px; | |
| font-style: italic; | |
| flex: 1; | |
| } | |
| .diff-text-add { | |
| color: #a3d9a5; | |
| flex: 1; | |
| font-style: italic; | |
| font-size: 11px; | |
| } | |
| /* ── Orphaned banner ── */ | |
| .orphaned-banner { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 16px; | |
| background: rgba(220, 38, 38, 0.05); | |
| border-bottom: 1px solid rgba(220, 38, 38, 0.15); | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| } | |
| .orphaned-banner-icon { color: var(--orphaned); flex-shrink: 0; } | |
| .orphaned-banner strong { color: var(--orphaned); font-weight: 600; } | |
| /* ── Orphaned panel layout — full width ── */ | |
| .orphaned-layout { | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| overflow: hidden; | |
| background: var(--panel-bg); | |
| } | |
| .orphaned-comments-panel { | |
| border-bottom: 1px solid rgba(220, 38, 38, 0.15); | |
| } | |
| .orphaned-comments-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 12px 20px; | |
| background: rgba(220, 38, 38, 0.04); | |
| border-bottom: 1px solid rgba(220, 38, 38, 0.12); | |
| } | |
| .orphaned-comments-title { | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: var(--orphaned); | |
| } | |
| .orphaned-comments-desc { | |
| font-size: 11.5px; | |
| color: var(--text-muted); | |
| margin-left: auto; | |
| } | |
| .orphaned-comment-card { | |
| padding: 16px 20px; | |
| border-bottom: 1px solid var(--border-subtle); | |
| } | |
| .orphaned-comment-card:last-child { border-bottom: none; } | |
| .orphaned-card-meta { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 10px; | |
| } | |
| .orphaned-card-breadcrumb { | |
| font-size: 11px; | |
| color: var(--text-dim); | |
| margin-bottom: 10px; | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| flex-wrap: wrap; | |
| padding-left: 32px; | |
| } | |
| .orphaned-card-body { | |
| font-size: 13px; | |
| color: var(--doc-text); | |
| line-height: 1.65; | |
| padding-left: 32px; | |
| margin-bottom: 10px; | |
| } | |
| /* Document for orphaned tab shows no trace of the deleted section */ | |
| .orphaned-doc-layout { | |
| display: grid; | |
| grid-template-columns: 1fr 300px; | |
| gap: 0; | |
| border-top: 1px solid var(--border); | |
| align-items: start; | |
| } | |
| .orphaned-doc-area { | |
| padding: 40px 48px 48px; | |
| border-right: 1px solid var(--border); | |
| } | |
| .orphaned-sidebar-placeholder { | |
| padding: 24px 16px; | |
| border-left: 1px solid var(--border-subtle); | |
| } | |
| .orphaned-sidebar-placeholder p { | |
| font-size: 11.5px; | |
| color: var(--text-dim); | |
| font-style: italic; | |
| line-height: 1.6; | |
| } | |
| /* ── Keyboard hint ── */ | |
| .keyboard-hint { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 6px; | |
| margin-top: 28px; | |
| font-size: 11.5px; | |
| color: var(--text-dim); | |
| } | |
| kbd { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-width: 22px; | |
| height: 22px; | |
| padding: 0 6px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| box-shadow: 0 1px 0 var(--border); | |
| } | |
| ::-webkit-scrollbar { width: 6px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: rgba(180,180,190,0.18); border-radius: 3px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <!-- ── Header ── --> | |
| <div class="header"> | |
| <div class="header-eyebrow"> | |
| Loupe — Prototype | |
| </div> | |
| <h1>Rendered-Mode Re-Anchoring States</h1> | |
| <p>In rendered-mode, comments are text annotations anchored to highlighted passages using a TextQuoteSelector — not line numbers. As the document evolves, each comment lands in one of four states depending on whether its passage is still present, moved, changed, or gone.</p> | |
| </div> | |
| <!-- ── Tabs ── --> | |
| <div class="tabs" role="tablist" aria-label="Anchor states"> | |
| <button class="tab tab-anchored active" role="tab" aria-selected="true" data-state="anchored" onclick="switchTab('anchored')">Anchored</button> | |
| <button class="tab tab-shifted" role="tab" aria-selected="false" data-state="shifted" onclick="switchTab('shifted')">Shifted</button> | |
| <button class="tab tab-outdated" role="tab" aria-selected="false" data-state="outdated" onclick="switchTab('outdated')">Outdated</button> | |
| <button class="tab tab-orphaned" role="tab" aria-selected="false" data-state="orphaned" onclick="switchTab('orphaned')">Orphaned</button> | |
| </div> | |
| <!-- ════════════════════════════════════════════ | |
| STATE 1 — ANCHORED | |
| ════════════════════════════════════════════ --> | |
| <div class="state-panel active" id="panel-anchored"> | |
| <div class="state-info">Passage found at its original location — no action needed.</div> | |
| <div class="review-layout"> | |
| <!-- Document --> | |
| <div class="document-area" id="doc-anchored"> | |
| <div class="doc-title">Contributing to Meridian</div> | |
| <div class="doc-byline">Last updated March 2026 — Meridian Core Team</div> | |
| <div class="doc-h2">Getting Started</div> | |
| <p class="doc-p">Meridian is an open-source data pipeline toolkit written in Go and TypeScript. Before submitting your first pull request, take a few minutes to read through this guide — it will save you and the reviewers significant back-and-forth.</p> | |
| <div class="doc-h3">Prerequisites</div> | |
| <p class="doc-p">You need Go 1.22 or later, Node 20+, and <span class="doc-code">pnpm</span> installed. The test suite requires a running PostgreSQL 15 instance; the easiest path is Docker Compose:</p> | |
| <div class="doc-pre"><code><span class="cm"># Start services</span> | |
| <span class="fn">docker</span> compose up -d postgres redis | |
| <span class="fn">go</span> test ./...</code></div> | |
| <p class="doc-p">If you encounter connection errors during tests, verify that <span class="doc-code">DATABASE_URL</span> is exported in your shell and matches the credentials in <span class="doc-code">docker-compose.yml</span>.</p> | |
| <div class="doc-h2">Workflow</div> | |
| <div class="doc-h3">Branching</div> | |
| <p class="doc-p">Create a feature branch from <span class="doc-code">main</span>. Use the naming pattern <span class="doc-code">type/short-description</span> where type is one of <span class="doc-code">feat</span>, <span class="doc-code">fix</span>, <span class="doc-code">docs</span>, or <span class="doc-code">refactor</span>. Branches prefixed with <span class="doc-code">wip/</span> are ignored by CI.</p> | |
| <div class="doc-h3">Commit Messages</div> | |
| <p class="doc-p"><span class="text-highlight" id="hl-anchored" onclick="focusComment('mc-anchored')" title="Click to focus comment">We follow the Conventional Commits specification. Each commit message must have a type, an optional scope in parentheses, and a short imperative-mood description — no trailing period. The body, separated by a blank line, explains the <em>why</em> rather than the <em>what</em>.</span></p> | |
| <div class="doc-blockquote"><p>Good: <strong>feat(pipeline): add retry logic for transient errors</strong><br>Bad: <strong>fixed things</strong></p></div> | |
| <p class="doc-p">Breaking changes must include a <span class="doc-code">BREAKING CHANGE:</span> footer in the commit body. This triggers a major version bump in the automated release workflow.</p> | |
| <div class="doc-h2">Code Review</div> | |
| <p class="doc-p">All pull requests require at least one approval from a core team member. Reviews are expected to be thorough — plan for a 2–3 day turnaround on most submissions. If your PR has been open for more than five days without feedback, ping the <span class="doc-code">#pr-review</span> Slack channel.</p> | |
| <div class="doc-h3">Keeping Your PR Small</div> | |
| <p class="doc-p">PRs under 400 lines of diff get reviewed faster. If your change is larger, consider whether it can be split along logical seams — for example, data model changes in one PR and the feature that uses them in a follow-up.</p> | |
| </div> | |
| <!-- Margin --> | |
| <div class="margin-area"> | |
| <div class="margin-comment" id="mc-anchored"> | |
| <div class="margin-comment-header"> | |
| <div class="margin-avatar">SK</div> | |
| <div class="margin-meta"> | |
| <span class="margin-author">sasha.k</span> | |
| <span class="margin-time">3 days ago</span> | |
| <span class="anchored-check">✓ Anchored</span> | |
| </div> | |
| </div> | |
| <div class="margin-quote">“Each commit message must have a type, an optional scope…”</div> | |
| <div class="margin-body"> | |
| Worth mentioning the 72-character limit on the subject line here — most teams discover it the hard way when GitHub truncates the title in notifications. Maybe add a concrete example of a good body too, since “explains the why” is advice people find vague until they see it in practice. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ════════════════════════════════════════════ | |
| STATE 2 — SHIFTED | |
| ════════════════════════════════════════════ --> | |
| <div class="state-panel" id="panel-shifted"> | |
| <div class="state-info">Passage found at a new location — the section was reorganized. Comment follows automatically.</div> | |
| <div class="review-layout"> | |
| <!-- Document --> | |
| <div class="document-area" id="doc-shifted"> | |
| <div class="doc-title">Contributing to Meridian</div> | |
| <div class="doc-byline">Last updated March 2026 — Meridian Core Team</div> | |
| <div class="doc-h2">Getting Started</div> | |
| <p class="doc-p">Meridian is an open-source data pipeline toolkit written in Go and TypeScript. Before submitting your first pull request, take a few minutes to read through this guide.</p> | |
| <div class="doc-h3">Prerequisites</div> | |
| <p class="doc-p">You need Go 1.22 or later, Node 20+, and <span class="doc-code">pnpm</span> installed. Start services with Docker Compose and run <span class="doc-code">go test ./...</span>.</p> | |
| <!-- Old section with ghost placeholder --> | |
| <div class="doc-h2">Workflow</div> | |
| <div class="doc-h3">Branching</div> | |
| <p class="doc-p">Create a feature branch from <span class="doc-code">main</span>. Use the naming pattern <span class="doc-code">type/short-description</span> where type is one of <span class="doc-code">feat</span>, <span class="doc-code">fix</span>, <span class="doc-code">docs</span>, or <span class="doc-code">refactor</span>.</p> | |
| <!-- Ghost: comment was originally here under Workflow > Commit Messages --> | |
| <div class="doc-h3">Commit Messages</div> | |
| <span class="ghost-passage">Comment shifted to §Setup › Commit Standards</span> | |
| <p class="doc-p">We follow the Conventional Commits specification. Each commit message must have a type, an optional scope, and a short description.</p> | |
| <div class="doc-h2">Code Review</div> | |
| <p class="doc-p">All pull requests require at least one approval from a core team member. Reviews are expected to be thorough — plan for a 2–3 day turnaround on most submissions.</p> | |
| <!-- Reorganized: the passage now lives under Setup > Commit Standards --> | |
| <div class="doc-h2">Setup</div> | |
| <div class="doc-h3">Environment</div> | |
| <p class="doc-p">Export <span class="doc-code">DATABASE_URL</span> before running tests. Verify it matches the credentials in <span class="doc-code">docker-compose.yml</span>.</p> | |
| <div class="doc-h3">Commit Standards</div> | |
| <p class="doc-p"><span class="text-highlight-shifted" id="hl-shifted" onclick="focusComment('mc-shifted')" title="Click to focus comment">We follow the Conventional Commits specification. Each commit message must have a type, an optional scope in parentheses, and a short imperative-mood description — no trailing period. The body, separated by a blank line, explains the <em>why</em> rather than the <em>what</em>.</span></p> | |
| <div class="doc-blockquote"><p>Good: <strong>feat(pipeline): add retry logic for transient errors</strong></p></div> | |
| <p class="doc-p">Breaking changes must include a <span class="doc-code">BREAKING CHANGE:</span> footer. This triggers a major version bump in the automated release workflow.</p> | |
| </div> | |
| <!-- Margin --> | |
| <div class="margin-area"> | |
| <div class="margin-comment state-shifted" id="mc-shifted"> | |
| <div class="margin-comment-header"> | |
| <div class="margin-avatar">SK</div> | |
| <div class="margin-meta"> | |
| <span class="margin-author">sasha.k</span> | |
| <span class="margin-time">3 days ago</span> | |
| </div> | |
| </div> | |
| <div style="padding-left: 32px; margin-bottom: 4px;"> | |
| <span class="badge badge-shifted">↕ Shifted</span> | |
| </div> | |
| <div class="breadcrumb"> | |
| <span class="breadcrumb-section">Workflow</span> | |
| <span class="breadcrumb-sep">›</span> | |
| <span class="breadcrumb-section">Commit Messages</span> | |
| <span class="breadcrumb-arrow">→</span> | |
| <span class="breadcrumb-section">Setup</span> | |
| <span class="breadcrumb-sep">›</span> | |
| <span class="breadcrumb-section">Commit Standards</span> | |
| </div> | |
| <div class="margin-quote" style="margin-top: 8px;">“Each commit message must have a type, an optional scope…”</div> | |
| <div class="margin-body"> | |
| Worth mentioning the 72-character limit on the subject line here — most teams discover it the hard way when GitHub truncates the title in notifications. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ════════════════════════════════════════════ | |
| STATE 3 — OUTDATED | |
| ════════════════════════════════════════════ --> | |
| <div class="state-panel" id="panel-outdated"> | |
| <div class="state-info">The commented passage was rewritten — the comment remains visible but refers to old content.</div> | |
| <div class="review-layout"> | |
| <!-- Document --> | |
| <div class="document-area" id="doc-outdated"> | |
| <div class="doc-title">Contributing to Meridian</div> | |
| <div class="doc-byline">Last updated March 2026 — Meridian Core Team</div> | |
| <div class="doc-h2">Getting Started</div> | |
| <p class="doc-p">Meridian is an open-source data pipeline toolkit written in Go and TypeScript. Before submitting your first pull request, take a few minutes to read through this guide.</p> | |
| <div class="doc-h3">Prerequisites</div> | |
| <p class="doc-p">You need Go 1.22 or later, Node 20+, and <span class="doc-code">pnpm</span> installed. Start services with Docker Compose and run <span class="doc-code">go test ./...</span>.</p> | |
| <div class="doc-h2">Workflow</div> | |
| <div class="doc-h3">Branching</div> | |
| <p class="doc-p">Create a feature branch from <span class="doc-code">main</span>. Use the naming pattern <span class="doc-code">type/short-description</span>.</p> | |
| <div class="doc-h3">Commit Messages</div> | |
| <!-- The passage has been rewritten — current text only, no old text kept in document --> | |
| <p class="doc-p"><span class="text-highlight-outdated" id="hl-outdated" onclick="focusComment('mc-outdated')" title="Click to view outdated comment">Commit messages follow the Conventional Commits format. The subject line is limited to 72 characters. Use the imperative mood: “add retry logic” not “added retry logic.” Include a blank-line-separated body when the change warrants explanation. Breaking changes require a <span class="doc-code">BREAKING CHANGE:</span> footer.</span></p> | |
| <div class="doc-h2">Code Review</div> | |
| <p class="doc-p">All pull requests require at least one approval from a core team member. Reviews are expected to be thorough — plan for a 2–3 day turnaround.</p> | |
| <div class="doc-h3">Keeping Your PR Small</div> | |
| <p class="doc-p">PRs under 400 lines of diff get reviewed faster. If your change is larger, consider whether it can be split along logical seams.</p> | |
| </div> | |
| <!-- Margin --> | |
| <div class="margin-area"> | |
| <div class="margin-comment state-outdated" id="mc-outdated"> | |
| <div class="margin-comment-header"> | |
| <div class="margin-avatar">SK</div> | |
| <div class="margin-meta"> | |
| <span class="margin-author">sasha.k</span> | |
| <span class="margin-time">3 days ago</span> | |
| </div> | |
| </div> | |
| <div style="padding-left: 32px; margin-bottom: 8px;"> | |
| <span class="badge badge-outdated">⚠ Outdated</span> | |
| </div> | |
| <div class="margin-body"> | |
| Worth mentioning the 72-character limit on the subject line — most teams discover it the hard way. Add a concrete example of a good commit body too. | |
| </div> | |
| <!-- Visual diff between old and new text --> | |
| <div class="outdated-diff"> | |
| <div class="outdated-diff-row"> | |
| <span class="diff-sign-del">−</span> | |
| <span class="diff-text-del">Each commit message must have a type, an optional scope in parentheses, and a short imperative-mood description…</span> | |
| </div> | |
| <div class="outdated-diff-row"> | |
| <span class="diff-sign-add">+</span> | |
| <span class="diff-text-add">Commit messages follow the Conventional Commits format. The subject line is limited to 72 characters…</span> | |
| </div> | |
| </div> | |
| <button class="collapsible-toggle" onclick="toggleCollapsible('orig-outdated', this, 'Show original quoted text', 'Hide original quoted text')" aria-expanded="false"> | |
| <svg class="chev" width="12" height="12" viewBox="0 0 16 16" fill="none"> | |
| <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| Show original quoted text | |
| </button> | |
| <div class="collapsible-body" id="orig-outdated"> | |
| <div class="collapsible-inner"> | |
| <div class="original-text-label">Original passage</div> | |
| <div class="original-text-block">“We follow the Conventional Commits specification. Each commit message must have a type, an optional scope in parentheses, and a short imperative-mood description — no trailing period. The body, separated by a blank line, explains the <em>why</em> rather than the <em>what</em>.”</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ════════════════════════════════════════════ | |
| STATE 4 — ORPHANED | |
| ════════════════════════════════════════════ --> | |
| <div class="state-panel" id="panel-orphaned"> | |
| <div class="state-info">The entire section this comment was in was deleted — surfaced at the document level so nothing is silently lost.</div> | |
| <div class="orphaned-layout"> | |
| <!-- Orphaned comments panel at document level --> | |
| <div class="orphaned-comments-panel"> | |
| <div class="orphaned-comments-header"> | |
| <svg class="orphaned-banner-icon" width="14" height="14" viewBox="0 0 16 16" fill="none"> | |
| <path d="M8 5v4M8 11v.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/> | |
| <circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/> | |
| </svg> | |
| <span class="orphaned-comments-title">1 orphaned comment</span> | |
| <span class="orphaned-comments-desc">Section was removed from this document</span> | |
| </div> | |
| <div class="orphaned-comment-card"> | |
| <div class="orphaned-card-meta"> | |
| <div class="margin-avatar">SK</div> | |
| <span class="margin-author">sasha.k</span> | |
| <span class="margin-time">3 days ago</span> | |
| <span class="badge badge-orphaned" style="margin-left: auto;">✗ Orphaned</span> | |
| </div> | |
| <div class="orphaned-card-breadcrumb"> | |
| <span style="font-style: italic; color: var(--text-dim);">Was in:</span> | |
| <span style="font-weight: 500; color: var(--text-muted);">Code Review</span> | |
| <span style="color: var(--text-dim); opacity: 0.5;">›</span> | |
| <span style="font-weight: 500; color: var(--text-muted);">Review Etiquette</span> | |
| </div> | |
| <div class="orphaned-card-body"> | |
| This framing puts all the responsibility on the reviewer, but the author has a role here too — keeping the PR description up to date as the implementation changes, flagging when an earlier comment has been addressed. Worth a sentence or two about author obligations in review. | |
| </div> | |
| <button class="collapsible-toggle" onclick="toggleCollapsible('orig-orphaned', this, 'Show deleted passage', 'Hide deleted passage')" aria-expanded="false" style="padding-left: 0; margin-left: 32px; margin-top: 0;"> | |
| <svg class="chev" width="12" height="12" viewBox="0 0 16 16" fill="none"> | |
| <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| Show deleted passage | |
| </button> | |
| <div class="collapsible-body" id="orig-orphaned"> | |
| <div class="collapsible-inner" style="padding-left: 0; margin-left: 32px;"> | |
| <div class="original-text-label orphaned-label">Deleted passage</div> | |
| <div class="original-text-block orphaned-text">“Review comments should be precise and actionable. Avoid drive-by approvals — if you approve, you are vouching for the correctness of the change. If you request changes, explain why and suggest a concrete alternative where possible.”</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- The document itself: Code Review > Review Etiquette section is gone --> | |
| <div class="orphaned-doc-layout"> | |
| <div class="orphaned-doc-area"> | |
| <div class="doc-title">Contributing to Meridian</div> | |
| <div class="doc-byline">Last updated March 2026 — Meridian Core Team</div> | |
| <div class="doc-h2">Getting Started</div> | |
| <p class="doc-p">Meridian is an open-source data pipeline toolkit written in Go and TypeScript. Before submitting your first pull request, take a few minutes to read through this guide.</p> | |
| <div class="doc-h3">Prerequisites</div> | |
| <p class="doc-p">You need Go 1.22 or later, Node 20+, and <span class="doc-code">pnpm</span> installed. Start services with Docker Compose and run <span class="doc-code">go test ./...</span>.</p> | |
| <div class="doc-h2">Workflow</div> | |
| <div class="doc-h3">Branching</div> | |
| <p class="doc-p">Create a feature branch from <span class="doc-code">main</span>. Use the naming pattern <span class="doc-code">type/short-description</span>.</p> | |
| <div class="doc-h3">Commit Messages</div> | |
| <p class="doc-p">We follow the Conventional Commits specification. Each commit message must have a type, an optional scope in parentheses, and a short imperative-mood description. Breaking changes require a <span class="doc-code">BREAKING CHANGE:</span> footer.</p> | |
| <div class="doc-h2">Code Review</div> | |
| <!-- Note: Review Etiquette subsection was deleted — document jumps straight to PR size --> | |
| <p class="doc-p">All pull requests require at least one approval from a core team member. Plan for a 2–3 day turnaround on most submissions. Ping <span class="doc-code">#pr-review</span> on Slack after five days without feedback.</p> | |
| <div class="doc-h3">Keeping Your PR Small</div> | |
| <p class="doc-p">PRs under 400 lines of diff get reviewed faster. If your change is larger, consider whether it can be split along logical seams — data model changes in one PR, the feature in a follow-up.</p> | |
| <div class="doc-h2">Releases</div> | |
| <p class="doc-p">Releases are automated via semantic-release. Merging to <span class="doc-code">main</span> with a <span class="doc-code">feat:</span> commit bumps the minor version; <span class="doc-code">fix:</span> bumps the patch. Breaking changes trigger a major bump.</p> | |
| </div> | |
| <div class="orphaned-sidebar-placeholder"> | |
| <p>No active annotations in this document. The orphaned comment above refers to a section that no longer exists.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ── Keyboard hint ── --> | |
| <div class="keyboard-hint"> | |
| <kbd>1</kbd><kbd>2</kbd><kbd>3</kbd><kbd>4</kbd> to switch states · | |
| <kbd>←</kbd><kbd>→</kbd> to navigate | |
| </div> | |
| </div><!-- /app --> | |
| <script> | |
| const STATES = ['anchored', 'shifted', 'outdated', 'orphaned']; | |
| let current = 0; | |
| function switchTab(state) { | |
| const idx = STATES.indexOf(state); | |
| if (idx === -1) return; | |
| current = idx; | |
| document.querySelectorAll('.tab').forEach(t => { | |
| t.classList.toggle('active', t.dataset.state === state); | |
| t.setAttribute('aria-selected', t.dataset.state === state ? 'true' : 'false'); | |
| }); | |
| document.querySelectorAll('.state-panel').forEach(p => { | |
| p.classList.toggle('active', p.id === 'panel-' + state); | |
| }); | |
| // Re-align after the panel becomes visible (layout is now computed) | |
| requestAnimationFrame(() => alignCommentToHighlight('panel-' + state)); | |
| } | |
| function focusComment(commentId) { | |
| const comment = document.getElementById(commentId); | |
| if (!comment) return; | |
| comment.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| comment.style.transition = 'background 0.2s ease'; | |
| comment.style.background = 'rgba(250, 204, 21, 0.07)'; | |
| setTimeout(() => { comment.style.background = ''; }, 1200); | |
| } | |
| function toggleCollapsible(bodyId, btn, labelClosed, labelOpen) { | |
| const body = document.getElementById(bodyId); | |
| const chev = btn.querySelector('.chev'); | |
| const isOpen = body.classList.contains('expanded'); | |
| body.classList.toggle('expanded', !isOpen); | |
| chev.classList.toggle('open', !isOpen); | |
| btn.setAttribute('aria-expanded', String(!isOpen)); | |
| const label = btn.childNodes[btn.childNodes.length - 1]; | |
| if (label && label.nodeType === 3) { | |
| label.textContent = isOpen ? ' ' + labelClosed : ' ' + labelOpen; | |
| } | |
| } | |
| // Align a margin comment's top edge to the top edge of its highlight | |
| function alignCommentToHighlight(panelId) { | |
| const panel = document.getElementById(panelId); | |
| if (!panel) return; | |
| const highlight = panel.querySelector('.text-highlight, .text-highlight-shifted, .text-highlight-outdated'); | |
| const comment = panel.querySelector('.margin-comment'); | |
| if (!highlight || !comment) return; | |
| const marginArea = comment.parentElement; | |
| marginArea.style.position = 'relative'; | |
| comment.style.position = 'absolute'; | |
| comment.style.width = '100%'; | |
| comment.style.top = (highlight.getBoundingClientRect().top - marginArea.getBoundingClientRect().top + marginArea.scrollTop) + 'px'; | |
| // Ensure the margin area is tall enough for the absolutely-positioned comment | |
| const docArea = panel.querySelector('.document-area'); | |
| if (docArea) { | |
| marginArea.style.minHeight = docArea.offsetHeight + 'px'; | |
| } | |
| } | |
| const ALIGNED_PANELS = ['panel-anchored', 'panel-shifted', 'panel-outdated']; | |
| function alignAll() { | |
| ALIGNED_PANELS.forEach(id => alignCommentToHighlight(id)); | |
| } | |
| // Keyboard navigation | |
| document.addEventListener('keydown', e => { | |
| if (e.key >= '1' && e.key <= '4' && !e.ctrlKey && !e.metaKey && !e.altKey) { | |
| switchTab(STATES[parseInt(e.key, 10) - 1]); | |
| return; | |
| } | |
| if ((e.key === 'ArrowRight' || e.key === 'ArrowDown') && !e.ctrlKey && !e.metaKey) { | |
| e.preventDefault(); | |
| switchTab(STATES[(current + 1) % STATES.length]); | |
| } else if ((e.key === 'ArrowLeft' || e.key === 'ArrowUp') && !e.ctrlKey && !e.metaKey) { | |
| e.preventDefault(); | |
| switchTab(STATES[(current + STATES.length - 1) % STATES.length]); | |
| } | |
| }); | |
| // Initial alignment for the default tab (anchored) | |
| requestAnimationFrame(alignAll); | |
| // Re-align on window resize | |
| window.addEventListener('resize', alignAll); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment