Created
September 29, 2026 10:30
-
-
Save SeanTAllen/793ceef2c4d29aa505c1a6a5b3966a07 to your computer and use it in GitHub Desktop.
PR Overview Example — ponylang/ponyc #6188
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
| <title>PR #6188 Overview</title> | |
| <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=Inter:wght@400;500;600;700&display=swap"> | |
| <style> | |
| /* Layout: fixed left nav rail, scrolling main content, expand/collapse detail panels */ | |
| :root { | |
| --bg: #fafaf9; | |
| --fg: #1c1917; | |
| --muted: #78716c; | |
| --accent: #2563eb; | |
| --accent-soft: #dbeafe; | |
| --border: #e7e5e4; | |
| --code-bg: #f5f5f4; | |
| --added-bg: #dcfce7; | |
| --added-fg: #166534; | |
| --removed-bg: #fee2e2; | |
| --removed-fg: #991b1b; | |
| --card-bg: #ffffff; | |
| --nav-bg: #1c1917; | |
| --nav-fg: #d6d3d1; | |
| --nav-active: #60a5fa; | |
| --diagram-node: #e0e7ff; | |
| --diagram-edge: #6366f1; | |
| --diagram-highlight: #f59e0b; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root:not([data-theme="light"]) { | |
| --bg: #0c0a09; | |
| --fg: #e7e5e4; | |
| --muted: #a8a29e; | |
| --accent: #60a5fa; | |
| --accent-soft: #1e3a5f; | |
| --border: #292524; | |
| --code-bg: #1c1917; | |
| --added-bg: #052e16; | |
| --added-fg: #86efac; | |
| --removed-bg: #450a0a; | |
| --removed-fg: #fca5a5; | |
| --card-bg: #1c1917; | |
| --nav-bg: #0c0a09; | |
| --nav-fg: #a8a29e; | |
| --nav-active: #60a5fa; | |
| --diagram-node: #312e81; | |
| --diagram-edge: #818cf8; | |
| --diagram-highlight: #fbbf24; | |
| color-scheme: dark; | |
| } | |
| } | |
| :root[data-theme="dark"] { | |
| --bg: #0c0a09; | |
| --fg: #e7e5e4; | |
| --muted: #a8a29e; | |
| --accent: #60a5fa; | |
| --accent-soft: #1e3a5f; | |
| --border: #292524; | |
| --code-bg: #1c1917; | |
| --added-bg: #052e16; | |
| --added-fg: #86efac; | |
| --removed-bg: #450a0a; | |
| --removed-fg: #fca5a5; | |
| --card-bg: #1c1917; | |
| --nav-bg: #0c0a09; | |
| --nav-fg: #a8a29e; | |
| --nav-active: #60a5fa; | |
| --diagram-node: #312e81; | |
| --diagram-edge: #818cf8; | |
| --diagram-highlight: #fbbf24; | |
| color-scheme: dark; | |
| } | |
| body { | |
| background: var(--bg); | |
| color: var(--fg); | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| line-height: 1.6; | |
| padding: 0; | |
| } | |
| /* --- Nav rail --- */ | |
| nav { | |
| position: fixed; | |
| top: env(safe-area-inset-top, 0px); | |
| left: 0; | |
| width: 220px; | |
| height: 100vh; | |
| background: var(--nav-bg); | |
| color: var(--nav-fg); | |
| padding: 24px 0; | |
| overflow-y: auto; | |
| z-index: 100; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| nav .pr-label { | |
| padding: 0 16px 8px; | |
| font-family: 'IBM Plex Mono', monospace; | |
| font-size: 11px; | |
| color: var(--muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| } | |
| nav .pr-title { | |
| padding: 0 16px 20px; | |
| font-size: 13px; | |
| font-weight: 600; | |
| border-bottom: 1px solid var(--border); | |
| line-height: 1.4; | |
| } | |
| nav ul { list-style: none; padding: 12px 0; flex: 1; } | |
| nav li { position: relative; } | |
| nav a { | |
| display: block; | |
| padding: 7px 16px 7px 20px; | |
| color: var(--nav-fg); | |
| text-decoration: none; | |
| font-size: 13px; | |
| transition: background 0.15s, color 0.15s; | |
| } | |
| nav a:hover { background: rgba(255,255,255,0.05); } | |
| nav a.active { color: var(--nav-active); background: rgba(96,165,250,0.08); } | |
| nav a.active::before { | |
| content: ''; | |
| position: absolute; | |
| left: 0; top: 4px; bottom: 4px; | |
| width: 3px; | |
| background: var(--nav-active); | |
| border-radius: 0 2px 2px 0; | |
| } | |
| nav .sub-items { padding-left: 12px; } | |
| nav .sub-items a { | |
| font-size: 12px; | |
| color: var(--muted); | |
| padding: 3px 16px 3px 20px; | |
| } | |
| nav .kbd-help { | |
| padding: 16px; | |
| border-top: 1px solid var(--border); | |
| font-size: 11px; | |
| color: var(--muted); | |
| line-height: 1.9; | |
| } | |
| kbd { | |
| background: var(--code-bg); | |
| border: 1px solid var(--border); | |
| border-radius: 3px; | |
| padding: 1px 5px; | |
| font-size: 11px; | |
| font-family: 'IBM Plex Mono', monospace; | |
| } | |
| /* --- Main content --- */ | |
| main { | |
| margin-left: 220px; | |
| padding: 32px 48px; | |
| max-width: 860px; | |
| } | |
| section { | |
| margin-bottom: 48px; | |
| scroll-margin-top: 24px; | |
| } | |
| h1 { font-size: 24px; font-weight: 700; margin-bottom: 8px; text-wrap: balance; } | |
| h2 { | |
| font-size: 17px; font-weight: 600; margin-bottom: 16px; | |
| padding-bottom: 6px; border-bottom: 2px solid var(--accent); | |
| display: inline-block; | |
| } | |
| h3 { font-size: 15px; font-weight: 600; margin: 20px 0 10px; } | |
| p { margin-bottom: 12px; } | |
| .summary-box { | |
| background: var(--accent-soft); | |
| border-left: 4px solid var(--accent); | |
| padding: 14px 18px; | |
| margin-bottom: 20px; | |
| border-radius: 0 6px 6px 0; | |
| font-size: 14px; | |
| } | |
| /* --- Expand/collapse panels --- */ | |
| .panel { | |
| background: var(--card-bg); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| margin-bottom: 12px; | |
| overflow: hidden; | |
| } | |
| .panel-header { | |
| padding: 10px 14px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| user-select: none; | |
| font-size: 14px; | |
| } | |
| .panel-header:hover { background: rgba(0,0,0,0.02); } | |
| @media (prefers-color-scheme: dark) { | |
| :root:not([data-theme="light"]) .panel-header:hover { background: rgba(255,255,255,0.03); } | |
| } | |
| :root[data-theme="dark"] .panel-header:hover { background: rgba(255,255,255,0.03); } | |
| .panel-header .arrow { | |
| transition: transform 0.2s; | |
| font-size: 10px; | |
| color: var(--muted); | |
| flex-shrink: 0; | |
| } | |
| .panel.open .arrow { transform: rotate(90deg); } | |
| .panel-body { | |
| display: none; | |
| padding: 0 14px 14px; | |
| border-top: 1px solid var(--border); | |
| } | |
| .panel.open .panel-body { display: block; padding-top: 14px; } | |
| .badge { | |
| display: inline-block; | |
| padding: 1px 7px; | |
| border-radius: 3px; | |
| font-family: 'IBM Plex Mono', monospace; | |
| font-size: 10px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.04em; | |
| flex-shrink: 0; | |
| } | |
| .badge-removed { background: var(--removed-bg); color: var(--removed-fg); } | |
| .badge-added { background: var(--added-bg); color: var(--added-fg); } | |
| .badge-changed { background: var(--accent-soft); color: var(--accent); } | |
| .sig { | |
| font-family: 'IBM Plex Mono', monospace; | |
| font-size: 13px; | |
| min-width: 0; | |
| } | |
| /* --- Before / After --- */ | |
| .before-after { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| margin: 12px 0; | |
| } | |
| .before-after > div { border-radius: 5px; overflow: hidden; min-width: 0; } | |
| .before-after label { | |
| display: block; | |
| padding: 3px 10px; | |
| font-family: 'IBM Plex Mono', monospace; | |
| font-size: 10px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| } | |
| .before-after .before label { background: var(--removed-bg); color: var(--removed-fg); } | |
| .before-after .after label { background: var(--added-bg); color: var(--added-fg); } | |
| pre { | |
| background: var(--code-bg); | |
| padding: 10px 14px; | |
| border-radius: 5px; | |
| overflow-x: auto; | |
| font-family: 'IBM Plex Mono', monospace; | |
| font-size: 12px; | |
| line-height: 1.5; | |
| } | |
| .before-after pre { border-radius: 0 0 5px 5px; margin: 0; } | |
| code { | |
| font-family: 'IBM Plex Mono', monospace; | |
| font-size: 12px; | |
| background: var(--code-bg); | |
| padding: 1px 4px; | |
| border-radius: 3px; | |
| } | |
| /* --- File map --- */ | |
| .file-group-label { font-weight: 600; font-size: 13px; } | |
| .file-count { font-size: 12px; color: var(--muted); } | |
| .file-list { | |
| list-style: none; | |
| padding-left: 18px; | |
| } | |
| .file-list li { | |
| padding: 3px 0; | |
| font-family: 'IBM Plex Mono', monospace; | |
| font-size: 12px; | |
| color: var(--muted); | |
| } | |
| .file-list .stat { font-size: 11px; margin-left: 6px; } | |
| .file-list .stat-add { color: var(--added-fg); } | |
| .file-list .stat-del { color: var(--removed-fg); } | |
| /* --- Stats row --- */ | |
| .stats { | |
| display: flex; | |
| gap: 16px; | |
| font-size: 13px; | |
| color: var(--muted); | |
| margin-bottom: 4px; | |
| } | |
| /* --- Diagram --- */ | |
| .diagram-container { | |
| margin: 16px 0; | |
| padding: 20px; | |
| background: var(--card-bg); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| overflow-x: auto; | |
| } | |
| .diagram-container svg { display: block; margin: 0 auto; } | |
| svg text { font-family: 'Inter', sans-serif; fill: var(--fg); } | |
| ul.migration-list { margin: 8px 0 8px 20px; } | |
| ul.migration-list li { margin-bottom: 6px; } | |
| /* --- Mobile --- */ | |
| @media (max-width: 768px) { | |
| nav { | |
| position: static; | |
| width: 100%; | |
| height: auto; | |
| padding: 12px 16px; | |
| } | |
| nav ul { display: flex; flex-wrap: wrap; gap: 2px; padding: 8px 0; } | |
| nav li { flex: none; } | |
| nav a { padding: 5px 10px; font-size: 12px; border-radius: 4px; } | |
| nav a.active::before { display: none; } | |
| nav .sub-items { display: none; } | |
| nav .kbd-help { display: none; } | |
| main { margin-left: 0; padding: 16px; } | |
| .before-after { grid-template-columns: 1fr; } | |
| } | |
| </style> | |
| <nav> | |
| <div class="pr-label">ponylang/ponyc</div> | |
| <div class="pr-title">#6188 — Reject runtime-reserved signals</div> | |
| <ul> | |
| <li><a href="#summary" class="active">Summary</a></li> | |
| <li><a href="#api">API Changes</a> | |
| <div class="sub-items"> | |
| <a href="#api-removed">Removed</a> | |
| <a href="#api-changed">Changed</a> | |
| </div> | |
| </li> | |
| <li><a href="#behavior">Behavioral Changes</a></li> | |
| <li><a href="#migration">Migration</a></li> | |
| <li><a href="#architecture">Architecture</a></li> | |
| <li><a href="#files">File Map</a></li> | |
| </ul> | |
| <div class="kbd-help"> | |
| <kbd>j</kbd> / <kbd>k</kbd> next / prev section<br> | |
| <kbd>Enter</kbd> expand focused item<br> | |
| <kbd>Esc</kbd> collapse all | |
| </div> | |
| </nav> | |
| <main> | |
| <section id="summary"> | |
| <h1>Reject runtime-reserved signals in HandleableSignalValidator</h1> | |
| <div class="summary-box"> | |
| <code>HandleableSignalValidator</code> now rejects signals the runtime reserves for tracing. | |
| Programs that registered a <code>SignalHandler</code> for SIGINFO (BSD/macOS) or real-time signals | |
| 32–35 (Linux) will get a <code>ValidationFailure</code> instead of silently replacing the runtime's handler. | |
| </div> | |
| <div class="stats"> | |
| <span>3 files changed</span> | |
| <span style="color:var(--added-fg)">+50</span> | |
| <span style="color:var(--removed-fg)">−81</span> | |
| <span class="badge badge-changed">changed</span> | |
| </div> | |
| </section> | |
| <section id="api"> | |
| <h2>API Changes</h2> | |
| <div id="api-removed" class="panel"> | |
| <div class="panel-header" onclick="this.parentElement.classList.toggle('open')"> | |
| <span class="arrow">▶</span> | |
| <span class="badge badge-removed">Removed</span> | |
| <span class="sig">Sig.info() from handleable whitelist (BSD/macOS)</span> | |
| </div> | |
| <div class="panel-body"> | |
| <p> | |
| <code>Sig.info()</code> was accepted by <code>HandleableSignalValidator</code> on BSD | |
| and macOS. The runtime uses SIGINFO for its tracing pause mechanism, so user handlers | |
| silently replaced the runtime's. | |
| </p> | |
| <div class="before-after"> | |
| <div class="before"> | |
| <label>Before</label> | |
| <pre>_assert_valid(h, Sig.prof()) | |
| _assert_valid(h, Sig.winch()) | |
| _assert_valid(h, Sig.info()) // accepted | |
| _assert_valid(h, Sig.usr1())</pre> | |
| </div> | |
| <div class="after"> | |
| <label>After</label> | |
| <pre>_assert_valid(h, Sig.prof()) | |
| _assert_valid(h, Sig.winch()) | |
| // gone | |
| _assert_valid(h, Sig.usr1())</pre> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="api-changed" class="panel"> | |
| <div class="panel-header" onclick="this.parentElement.classList.toggle('open')"> | |
| <span class="arrow">▶</span> | |
| <span class="badge badge-changed">Changed</span> | |
| <span class="sig">Sig.rt(n) — real-time signals 0–3 now rejected on Linux</span> | |
| </div> | |
| <div class="panel-body"> | |
| <p> | |
| The first four real-time signals (<code>Sig.rt(0)</code> through <code>Sig.rt(3)</code>) are | |
| now rejected. The C library reserves 32–33 and the Pony runtime reserves 34–35 for tracing. | |
| The handleable range starts at <code>Sig.rt(4)</code>. | |
| </p> | |
| <div class="before-after"> | |
| <div class="before"> | |
| <label>Before</label> | |
| <pre>// Linux real-time range started at offset 0 | |
| let rt_min: U32 = 32 | |
| // Sig.rt(0)? through Sig.rt(31)? all accepted</pre> | |
| </div> | |
| <div class="after"> | |
| <label>After</label> | |
| <pre>// Linux real-time range starts at offset 4 | |
| let rt_min: U32 = 36 | |
| // Sig.rt(0)? through Sig.rt(3)? rejected | |
| // Sig.rt(4)? and above accepted</pre> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <section id="behavior"> | |
| <h2>Behavioral Changes</h2> | |
| <h3>What happens differently</h3> | |
| <p> | |
| Before: calling <code>MakeHandleableSignal</code> with a runtime-reserved signal succeeded, | |
| returning a <code>HandleableSignal</code>. The user's handler silently replaced the runtime's | |
| tracing mechanism. No error, no warning — the tracing thread-pause just stopped working. | |
| </p> | |
| <p> | |
| After: the same call returns <code>ValidationFailure</code>. The runtime's handler stays intact. | |
| </p> | |
| <h3>Why now</h3> | |
| <p> | |
| PR #6182 removed the <code>runtime_tracing</code> build option — tracing is now always compiled in. | |
| Before that, the reservation only mattered for builds that opted into tracing. Now it's unconditional, | |
| so the validator must enforce it unconditionally. | |
| </p> | |
| </section> | |
| <section id="migration"> | |
| <h2>Migration</h2> | |
| <p>If your code registers a <code>SignalHandler</code> for any of these signals, it will now | |
| get a <code>ValidationFailure</code> from <code>MakeHandleableSignal</code>:</p> | |
| <ul class="migration-list"> | |
| <li><strong>BSD/macOS:</strong> <code>Sig.info()</code></li> | |
| <li><strong>Linux:</strong> <code>Sig.rt(0)?</code> through <code>Sig.rt(3)?</code></li> | |
| </ul> | |
| <p> | |
| If your <code>match</code> on the result of <code>MakeHandleableSignal</code> is exhaustive, | |
| you already handle <code>ValidationFailure</code> — these signals will take that path now. | |
| If you were only matching <code>HandleableSignal</code>, the compiler will tell you. | |
| </p> | |
| </section> | |
| <section id="architecture"> | |
| <h2>Architecture</h2> | |
| <div class="diagram-container"> | |
| <svg width="600" height="280" viewBox="0 0 600 280"> | |
| <rect x="200" y="10" width="200" height="40" rx="6" fill="var(--diagram-node)" stroke="var(--diagram-edge)" stroke-width="1.5"/> | |
| <text x="300" y="35" text-anchor="middle" font-size="13" font-weight="600">MakeHandleableSignal</text> | |
| <line x1="300" y1="50" x2="300" y2="80" stroke="var(--diagram-edge)" stroke-width="1.5" marker-end="url(#ah)"/> | |
| <rect x="170" y="80" width="260" height="40" rx="6" fill="var(--diagram-highlight)" stroke="var(--diagram-edge)" stroke-width="2"/> | |
| <text x="300" y="105" text-anchor="middle" font-size="13" font-weight="700">HandleableSignalValidator</text> | |
| <text x="300" y="135" text-anchor="middle" font-size="11" fill="var(--muted)">this is what changed</text> | |
| <line x1="220" y1="120" x2="100" y2="170" stroke="var(--diagram-edge)" stroke-width="1.5" marker-end="url(#ah)"/> | |
| <line x1="300" y1="120" x2="300" y2="170" stroke="var(--diagram-edge)" stroke-width="1.5" marker-end="url(#ah)"/> | |
| <line x1="380" y1="120" x2="500" y2="170" stroke="var(--diagram-edge)" stroke-width="1.5" marker-end="url(#ah)"/> | |
| <rect x="30" y="170" width="140" height="36" rx="6" fill="var(--diagram-node)" stroke="var(--diagram-edge)" stroke-width="1.5"/> | |
| <text x="100" y="193" text-anchor="middle" font-size="12">Reject fatal</text> | |
| <rect x="230" y="170" width="140" height="36" rx="6" fill="var(--diagram-node)" stroke="var(--diagram-edge)" stroke-width="1.5"/> | |
| <text x="300" y="193" text-anchor="middle" font-size="12">Reject uncatchable</text> | |
| <rect x="430" y="170" width="150" height="36" rx="6" fill="var(--diagram-highlight)" stroke="var(--diagram-edge)" stroke-width="2"/> | |
| <text x="505" y="193" text-anchor="middle" font-size="12" font-weight="600">Reject reserved</text> | |
| <text x="505" y="207" text-anchor="middle" font-size="10" fill="var(--muted)">(new)</text> | |
| <text x="100" y="228" text-anchor="middle" font-size="10" fill="var(--muted)">SIGKILL, SIGSTOP</text> | |
| <text x="300" y="228" text-anchor="middle" font-size="10" fill="var(--muted)">SIGCONT, SIGCHLD</text> | |
| <text x="505" y="228" text-anchor="middle" font-size="10" fill="var(--muted)">SIGINFO, rt(0-3)</text> | |
| <line x1="100" y1="238" x2="300" y2="263" stroke="var(--diagram-edge)" stroke-width="1" stroke-dasharray="4"/> | |
| <line x1="300" y1="238" x2="300" y2="263" stroke="var(--diagram-edge)" stroke-width="1" stroke-dasharray="4"/> | |
| <line x1="505" y1="238" x2="300" y2="263" stroke="var(--diagram-edge)" stroke-width="1" stroke-dasharray="4"/> | |
| <text x="300" y="276" text-anchor="middle" font-size="12" fill="var(--removed-fg)" font-weight="600">ValidationFailure</text> | |
| <defs> | |
| <marker id="ah" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"> | |
| <polygon points="0 0, 8 3, 0 6" fill="var(--diagram-edge)"/> | |
| </marker> | |
| </defs> | |
| </svg> | |
| </div> | |
| </section> | |
| <section id="files"> | |
| <h2>File Map</h2> | |
| <div class="panel open"> | |
| <div class="panel-header" onclick="this.parentElement.classList.toggle('open')"> | |
| <span class="arrow">▶</span> | |
| <span class="file-group-label">Core change</span> | |
| <span class="file-count">1 file</span> | |
| </div> | |
| <div class="panel-body"> | |
| <ul class="file-list"> | |
| <li> | |
| packages/signals/handleable_signal.pony | |
| <span class="stat"><span class="stat-add">+18</span> <span class="stat-del">−49</span></span> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-header" onclick="this.parentElement.classList.toggle('open')"> | |
| <span class="arrow">▶</span> | |
| <span class="file-group-label">Tests</span> | |
| <span class="file-count">1 file</span> | |
| </div> | |
| <div class="panel-body"> | |
| <ul class="file-list"> | |
| <li> | |
| packages/signals/_test.pony | |
| <span class="stat"><span class="stat-add">+27</span> <span class="stat-del">−27</span></span> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-header" onclick="this.parentElement.classList.toggle('open')"> | |
| <span class="arrow">▶</span> | |
| <span class="file-group-label">Release notes</span> | |
| <span class="file-count">1 file</span> | |
| </div> | |
| <div class="panel-body"> | |
| <ul class="file-list"> | |
| <li> | |
| .release-notes/reject-runtime-reserved-signals.md | |
| <span class="stat"><span class="stat-add">+5</span></span> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <script> | |
| const sections = document.querySelectorAll('section'); | |
| const navLinks = document.querySelectorAll('nav a[href^="#"]'); | |
| let currentSection = 0; | |
| function updateActiveNav() { | |
| navLinks.forEach(a => a.classList.remove('active')); | |
| const id = sections[currentSection]?.id; | |
| if (id) { | |
| const link = document.querySelector('nav a[href="#' + id + '"]'); | |
| if (link) link.classList.add('active'); | |
| } | |
| } | |
| const observer = new IntersectionObserver(entries => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| const idx = Array.from(sections).indexOf(entry.target); | |
| if (idx >= 0) { currentSection = idx; updateActiveNav(); } | |
| } | |
| }); | |
| }, { rootMargin: '-20% 0px -60% 0px' }); | |
| sections.forEach(s => observer.observe(s)); | |
| document.addEventListener('keydown', e => { | |
| if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; | |
| if (e.key === 'j' || e.key === 'J') { | |
| e.preventDefault(); | |
| currentSection = Math.min(currentSection + 1, sections.length - 1); | |
| sections[currentSection].scrollIntoView({ behavior: 'smooth' }); | |
| updateActiveNav(); | |
| } else if (e.key === 'k' || e.key === 'K') { | |
| e.preventDefault(); | |
| currentSection = Math.max(currentSection - 1, 0); | |
| sections[currentSection].scrollIntoView({ behavior: 'smooth' }); | |
| updateActiveNav(); | |
| } else if (e.key === 'Enter') { | |
| e.preventDefault(); | |
| sections[currentSection].querySelectorAll('.panel:not(.open)').forEach(el => el.classList.add('open')); | |
| } else if (e.key === 'Escape') { | |
| document.querySelectorAll('.panel.open').forEach(el => el.classList.remove('open')); | |
| } | |
| }); | |
| navLinks.forEach(a => { | |
| a.addEventListener('click', e => { | |
| e.preventDefault(); | |
| const target = document.querySelector(a.getAttribute('href')); | |
| if (target) { | |
| target.scrollIntoView({ behavior: 'smooth' }); | |
| const idx = Array.from(sections).indexOf(target); | |
| if (idx >= 0) { currentSection = idx; updateActiveNav(); } | |
| } | |
| }); | |
| }); | |
| </script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment