Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ SPDX-License-Identifier: MIT
let ghostEl = $state<HTMLDivElement>();
let viewportEl = $state<HTMLDivElement>();

// The section marker is drawn outside the scrolling overlay, so it has to be
// offset by hand to stay level with the lines it spans.
let scrollTop = $state(0);

let sdpText = $state(exampleSDP);
let caret = $state(0);
let hover = $state<SDPLocation | null>(null);
Expand Down Expand Up @@ -72,6 +76,27 @@ SPDX-License-Identifier: MIT
)
);

// The run of lines making up the media description the active line belongs to,
// marked in the gutter so the block a line is read against is visible without
// scanning upwards for the "m=" that opened it. Session-level lines are the
// preamble rather than a section, so they get no marker.
let sectionSpan = $derived.by(() => {
const section = active ? lines[active.lineIndex]?.section : null;
if (section == null) return null;

let first = -1;
let last = -1;
lines.forEach((line, i) => {
if (line.section !== section) return;
if (first === -1) first = i;
// Blank lines trailing the section say nothing about it, so the marker
// stops at the last line that does.
if (line.content.trim()) last = i;
});

return first === -1 ? null : { first, count: Math.max(last, first) - first + 1 };
});

// Where this line sits, which is what decides whether "a=" lines are in scope.
let placement = $derived(
activeLine == null || activeLine.section === null
Expand Down Expand Up @@ -206,6 +231,7 @@ SPDX-License-Identifier: MIT
if (!ghostEl || !editorEl) return;
ghostEl.scrollTop = editorEl.scrollTop;
ghostEl.scrollLeft = editorEl.scrollLeft;
scrollTop = editorEl.scrollTop;
// Scrolling moves the text under a stationary pointer.
syncHover();
placeTooltip();
Expand Down Expand Up @@ -343,6 +369,22 @@ SPDX-License-Identifier: MIT
class="relative overflow-hidden rounded-[5px] border border-hairline bg-surface transition-colors focus-within:border-subtle-outline"
bind:this={viewportEl}
>
<!--
The outer layer clips the marker to the editor; the inner one carries the
scroll offset, so only a change of section animates and scrolling stays
pinned to the text.
-->
<div class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
<div style="transform: translateY({-scrollTop}px);">
{#if sectionSpan}
<div
class="absolute left-[5px] w-[2px] rounded-full bg-token-type opacity-60 transition-[top,height] duration-150"
style="top: calc(var(--sdp-padding) + {sectionSpan.first} * var(--sdp-line-height)); height: calc({sectionSpan.count} * var(--sdp-line-height));"
></div>
{/if}
</div>
</div>

<div
class="sdp-layer pointer-events-none absolute inset-0 overflow-hidden"
aria-hidden="true"
Expand Down
13 changes: 9 additions & 4 deletions src/routes/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
color-scheme: light dark;
font-family: var(--font-sans);
font-size: 18px;

--sdp-padding: 0.75rem;
--sdp-line-height: 24px;
}

body {
Expand Down Expand Up @@ -100,19 +103,21 @@ code {
/*
* The editor stacks a transparent textarea over a highlighted overlay, so both
* layers must share identical text metrics or the caret drifts away from the
* glyphs beneath it. Defining them once here is what keeps them honest.
* glyphs beneath it. Defining them once here is what keeps them honest — the
* two metrics the section marker measures itself against are named, so that
* marker cannot drift out of step with the text either.
*/
.sdp-layer {
padding: 0.75rem;
padding: var(--sdp-padding);
font-family: var(--font-mono);
font-size: 16px;
line-height: 24px;
line-height: var(--sdp-line-height);
white-space: pre;
tab-size: 2;
}

.sdp-line {
height: 24px;
height: var(--sdp-line-height);
}

/* The wordmark is drawn with fill="currentColor"; the dot keeps the brand red. */
Expand Down
Loading