<script lang="ts">
import type { Agent, RefHit } from '../types';
import { api } from '../api';
import { highlightSnippetLines, resolveLang, type HighlightedLine } from '../highlight';
import { codeState } from '../stores.svelte';
import { fileKey, groupRefHitsByFile } from '../codeTree';
import { formatAge } from '../format';
import { buildGutterEntries, entryColorVar, gutterColumnCount, type GutterEntry } from '../codeGutter';
import { buildMinimapSegments, computeViewportIndicator, type ViewportIndicator } from '../codeMinimap';
import {
computeGaps,
computeOpenSpans,
PARTIAL_EXPAND_STEP,
planRows,
revealAll,
revealFromBottom,
revealFromTop,
type RevealState,
type Row,
} from '../codeCollapse';
import EmptyState from './EmptyState.svelte';
import Skeleton from './Skeleton.svelte';
import FileIcon from './FileIcon.svelte';
interface Props {
hub: string;
agents?: Agent[];
onOpenRefs?: (ctx: { repo: string; path: string; range: [number, number] | null }, hits: RefHit[]) => void;
onFileRefs?: (ctx: { repo: string; path: string }, hits: RefHit[]) => void;
onRepoRefs?: (repo: string, hits: RefHit[]) => void;
activeScope?: { repo: string; path: string | null; range: [number, number] | null } | null;
}
let { hub, agents = [], onOpenRefs, onFileRefs, onRepoRefs, activeScope = null }: Props = $props();
const s = $derived(codeState.forHub(hub));
$effect(() => {
const h = hub;
void codeState.load(h);
});
const active = $derived(s.files.find((f) => fileKey(f) === s.activeKey) ?? null);
const repoTarget = $derived(s.viewMode === 'repo' ? s.activeRepo : null);
let repoLoading = $state(false);
let repoHits = $state<RefHit[]>([]);
const repoGroups = $derived(groupRefHitsByFile(repoHits));
async function loadRepoRollup(repo: string, hubId: string) {
repoLoading = true;
try {
const hits = await api.getRefs(hubId, repo, true);
repoHits = hits;
onRepoRefs?.(repo, hits);
} finally {
repoLoading = false;
}
}
$effect(() => {
const repo = repoTarget;
const h = hub;
if (repo) void loadRepoRollup(repo, h);
});
function selectFileFromRollup(repo: string, path: string) {
s.activeKey = fileKey({ repo, path });
s.viewMode = 'file';
}
let loading = $state(false);
let lines = $state<{ n: number; text: string }[]>([]);
let lang = $state<string | null>(null);
let highlighted = $state<HighlightedLine[]>([]);
let fileRefs = $state<RefHit[]>([]);
const wholeFileHits = $derived(fileRefs.filter((h) => h.range === null));
const gutterEntries = $derived(buildGutterEntries(fileRefs));
const gutterColumns = $derived(gutterColumnCount(gutterEntries));
let showAll = $state(false);
let reveal = $state(new Map<string, RevealState>());
const firstLine = $derived(lines[0]?.n ?? null);
const lastLine = $derived(lines[lines.length - 1]?.n ?? null);
const openSpans = $derived.by(() => {
if (firstLine === null || lastLine === null) return [];
return computeOpenSpans(gutterEntries, firstLine, lastLine);
});
const collapseGaps = $derived.by(() => {
if (firstLine === null || lastLine === null) return [];
return computeGaps(openSpans, firstLine, lastLine);
});
const rowPlan = $derived.by((): Row[] => planRows(lines.map((l) => l.n), collapseGaps, reveal, showAll));
const lineByN = $derived(new Map(lines.map((l) => [l.n, l])));
function withScrollAnchor(mutate: () => void) {
const el = scrollEl;
if (!el) {
mutate();
return;
}
const containerTop = el.getBoundingClientRect().top;
let anchor: { line: string; offset: number } | null = null;
for (const row of el.querySelectorAll<HTMLElement>('[data-line]')) {
const r = row.getBoundingClientRect();
if (r.bottom > containerTop) {
anchor = { line: row.getAttribute('data-line')!, offset: r.top - containerTop };
break;
}
}
mutate();
if (!anchor) return;
const { line, offset } = anchor;
requestAnimationFrame(() => {
const row = el.querySelector<HTMLElement>(`[data-line="${line}"]`);
if (!row) return;
const newOffset = row.getBoundingClientRect().top - containerTop;
el.scrollTop += newOffset - offset;
});
}
function setReveal(key: string, next: RevealState) {
const map = new Map(reveal);
map.set(key, next);
reveal = map;
}
function expandTop(row: Extract<Row, { kind: 'fold' }>) {
withScrollAnchor(() => setReveal(row.key, revealFromTop(row.hiddenStart, row.hiddenEnd)));
}
function expandBottom(row: Extract<Row, { kind: 'fold' }>) {
withScrollAnchor(() => setReveal(row.key, revealFromBottom(row.hiddenStart, row.hiddenEnd)));
}
function expandAllOf(row: Extract<Row, { kind: 'fold' }>) {
withScrollAnchor(() => setReveal(row.key, revealAll(row.hiddenStart, row.hiddenEnd)));
}
function setShowAll(next: boolean) {
if (next === showAll) return;
withScrollAnchor(() => (showAll = next));
}
const minimapSegments = $derived.by(() => {
if (firstLine === null || lastLine === null) return [];
return buildMinimapSegments(gutterEntries, firstLine, lastLine);
});
let scrollEl = $state<HTMLElement | undefined>();
let codeEl = $state<HTMLElement | undefined>();
let viewport = $state<ViewportIndicator>({ top: 0, height: 1 });
function updateViewport() {
viewport = computeViewportIndicator({
containerScrollTop: scrollEl?.scrollTop ?? 0,
containerClientHeight: scrollEl?.clientHeight ?? 0,
codeOffsetTop: codeEl?.offsetTop ?? 0,
codeScrollHeight: codeEl?.scrollHeight ?? 0,
});
}
$effect(() => {
void lines;
const el = scrollEl;
if (!el) return;
updateViewport();
el.addEventListener('scroll', updateViewport, { passive: true });
window.addEventListener('resize', updateViewport);
return () => {
el.removeEventListener('scroll', updateViewport);
window.removeEventListener('resize', updateViewport);
};
});
function scrollToRange(range: [number, number]) {
const target = scrollEl?.querySelector<HTMLElement>(`[data-line="${range[0]}"]`);
target?.scrollIntoView({ block: 'center' });
}
const isActiveFileScope = $derived(!!active && !!activeScope && activeScope.repo === active.repo && activeScope.path === active.path);
const fileLaneActive = $derived(isActiveFileScope && activeScope!.range === null);
const activeEntryKey = $derived(
isActiveFileScope && activeScope!.range ? `${activeScope!.range[0]}-${activeScope!.range[1]}` : null
);
interface LineSegment {
entry: GutterEntry;
pos: 'start' | 'middle' | 'end' | 'tick';
}
const lineColumns = $derived.by((): Map<number, (LineSegment | null)[]> => {
const map = new Map<number, (LineSegment | null)[]>();
for (const line of lines) map.set(line.n, new Array(gutterColumns).fill(null));
for (const entry of gutterEntries) {
for (let n = entry.range[0]; n <= entry.range[1]; n++) {
const slots = map.get(n);
if (!slots) continue;
const pos: LineSegment['pos'] = entry.isTick ? 'tick' : n === entry.range[0] ? 'start' : n === entry.range[1] ? 'end' : 'middle';
slots[entry.column] = { entry, pos };
}
}
return map;
});
const tabsByLine = $derived.by((): Map<number, GutterEntry[]> => {
const map = new Map<number, GutterEntry[]>();
for (const entry of gutterEntries) {
const arr = map.get(entry.range[0]) ?? [];
arr.push(entry);
map.set(entry.range[0], arr);
}
return map;
});
function resolveAgent(id: string): Agent | undefined {
return agents.find((a) => a.id === id);
}
function cap(s: string): string {
return s.length ? s[0]!.toUpperCase() + s.slice(1) : s;
}
function tabInitials(entry: GutterEntry): string {
const seen = new Set<string>();
const initials: string[] = [];
for (const h of entry.hits) {
if (seen.has(h.from)) continue;
seen.add(h.from);
initials.push(resolveAgent(h.from)?.abbr ?? h.from.slice(0, 2).toUpperCase());
}
return initials.join(' ');
}
function tabTitle(entry: GutterEntry): string {
const count = entry.hits.length;
const latest = [...entry.hits].sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0))[0]!;
const who = [...new Set(entry.hits.map((h) => resolveAgent(h.from)?.display ?? cap(h.from)))].join(', ');
return `${count} conversation${count === 1 ? '' : 's'} · ${who} · latest ${formatAge(latest.ts)}${entry.drift ? ' · drifted from the pinned lines' : ''}`;
}
function clickEntry(entry: GutterEntry) {
if (!active) return;
onOpenRefs?.({ repo: active.repo, path: active.path, range: entry.range }, entry.hits);
}
function clickFileLane() {
if (!active || wholeFileHits.length === 0) return;
onOpenRefs?.({ repo: active.repo, path: active.path, range: null }, wholeFileHits);
}
const newestHit = $derived.by((): RefHit | null => {
if (fileRefs.length === 0) return null;
return [...fileRefs].sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0))[0]!;
});
const emptyCodeMessage = $derived.by((): { title: string; body: string } => {
const hit = newestHit;
const repo = active?.repo ?? hit?.repo ?? 'this repo';
if (!hit || hit.sha === 'HEAD') {
return {
title: 'No code returned',
body: `confer's index has no content for this file at HEAD — it may have been deleted, or never committed to ${repo}.`,
};
}
const shortSha = hit.sha.slice(0, 10);
const span = hit.range ? `lines ${hit.range[0]}–${hit.range[1]}` : 'whole file';
if (hit.staleness === 'moved') {
return {
title: 'Referenced path not found at that revision',
body: `Referenced at \`${shortSha}\` (${span}) — the path was moved or renamed since; it isn't at that path in ${repo} at \`${shortSha}\`.`,
};
}
return {
title: "Referenced revision isn't in your clone",
body: `Referenced at \`${shortSha}\` (${span}) — that revision isn't in your local clone of ${repo}.`,
};
});
function pickSha(hits: RefHit[]): string {
if (hits.length === 0) return 'HEAD';
return [...hits].sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0))[0]!.sha;
}
let lastLoadedKey: string | null = null;
async function loadFile() {
const f = active;
const currentKey = f ? fileKey(f) : null;
const isFileSwitch = currentKey !== lastLoadedKey;
lastLoadedKey = currentKey;
if (isFileSwitch) {
showAll = false;
reveal = new Map();
s.pinnedSha = null;
}
if (!f || !f.mapped) {
lines = [];
fileRefs = [];
s.codeSha = 'HEAD';
loading = false;
return;
}
if (!isFileSwitch) {
const sha = s.pinnedSha ?? pickSha(fileRefs);
if (sha === s.codeSha) return; loading = true;
try {
s.codeSha = sha;
const snippet = await api.getCode(hub, f.repo, f.path, sha);
lines = snippet.lines;
lang = snippet.lang;
highlighted = lines.length ? await highlightSnippetLines(lines, resolveLang(lang)) : [];
} finally {
loading = false;
}
return;
}
loading = true;
try {
const hits = await api.getRefs(hub, `${f.repo}:${f.path}`, true);
const relevant = hits.filter((h) => h.repo === f.repo && h.path === f.path);
fileRefs = relevant;
onFileRefs?.({ repo: f.repo, path: f.path }, relevant);
const sha = pickSha(relevant);
s.codeSha = sha;
const snippet = await api.getCode(hub, f.repo, f.path, sha);
lines = snippet.lines;
lang = snippet.lang;
highlighted = lines.length ? await highlightSnippetLines(lines, resolveLang(lang)) : [];
} finally {
loading = false;
}
}
$effect(() => {
void active;
void hub;
void s.pinnedSha;
if (s.viewMode === 'repo') return;
void loadFile();
});
function highlightedFor(n: number): HighlightedLine | undefined {
return highlighted.find((h) => h.n === n);
}
</script>
<div class="code-wrap" data-testid="code-view">
{#if !s.loaded}
<div class="codepage">
<div class="densitywrap"><Skeleton rows={6} /></div>
</div>
{:else if s.files.length === 0}
<div class="clonestub">
<EmptyState
glyph="◇"
title="No code referenced in this hub yet"
body="Messages here haven't pinned any `--ref`s — once someone posts a note or request that references this repo — a file, a line range, or the repo itself — it'll show up here, browsable with its conversation-density gutter."
/>
</div>
{:else if repoTarget}
<div class="codetool">
<span class="ct-hint">repo rollup · every conversation referencing {repoTarget} · click a file to open it</span>
</div>
<div class="codepage">
<div class="densitywrap">
{#if repoLoading}
<Skeleton rows={4} />
{:else if repoGroups.length === 0}
<div class="clonestub">
<EmptyState
glyph="◇"
title="No conversations reference this repo yet"
body={`Nobody has referenced anything in ${repoTarget} via --ref yet.`}
/>
</div>
{:else}
<div class="repo-rollup" data-testid="repo-rollup">
{#each repoGroups as g (g.path)}
<button type="button" class="rollup-row" onclick={() => selectFileFromRollup(repoTarget, g.path)}>
<FileIcon path={g.path} size={14} />
<span class="rollup-path">{g.path}</span>
<span class="rollup-count">{g.count} ref{g.count === 1 ? '' : 's'}</span>
<span class="rollup-ts">{formatAge(g.lastTs)}</span>
</button>
{/each}
</div>
{/if}
</div>
</div>
{:else if active}
<div class="codetool">
<span class="ct-hint">conversation gutter · shape=scope, color=meaning, column=overlap · click a range tab to read it</span>
{#if collapseGaps.length > 0}
<span class="rtoggle" data-testid="collapse-toggle">
<button type="button" class:on={!showAll} onclick={() => setShowAll(false)} data-testid="collapse-toggle-referenced">referenced</button>
<button type="button" class:on={showAll} onclick={() => setShowAll(true)} data-testid="collapse-toggle-showall">show all</button>
</span>
{/if}
</div>
<div class="codepage">
<div class="densitywrap" bind:this={scrollEl} onscroll={updateViewport}>
{#if loading}
<Skeleton rows={4} />
{:else if !active.mapped}
<div class="clonestub">
<EmptyState
glyph="◇"
title="No clone mapped for this repo"
body={`confer can only show code it can read from a local clone. Map a clone of ${active.repo} to see ${active.path} with its conversation-density gutter.`}
actionLabel="+ map a clone to see the code"
disabled
/>
</div>
{:else if lines.length === 0}
<div class="clonestub">
<EmptyState glyph="◇" title={emptyCodeMessage.title} body={emptyCodeMessage.body} />
</div>
{:else}
<div class="densefile">
{#if wholeFileHits.length > 0}
<button type="button" class="filelane" class:act={fileLaneActive} onclick={clickFileLane} data-testid="file-lane">
<span class="fl-ico">▤</span>
<span class="fl-tx"
><b>{wholeFileHits.length} conversation{wholeFileHits.length === 1 ? '' : 's'}</b> about the whole file</span
>
</button>
{/if}
<div class="code" data-lang={lang} bind:this={codeEl} style="--gutter-cols:{gutterColumns}">
{#each rowPlan as row (row.kind === 'line' ? 'l' + row.n : 'f' + row.key)}
{#if row.kind === 'fold'}
{@const hiddenCount = row.hiddenEnd - row.hiddenStart + 1}
<div class="fold" data-testid="fold-row" title={`lines ${row.hiddenStart}–${row.hiddenEnd}`}>
<span class="exp">⋯</span>
<span class="n">expand {hiddenCount} line{hiddenCount === 1 ? '' : 's'}</span>
<span class="updown">
{#if row.edge === 'top'}
<button type="button" onclick={() => expandAllOf(row)} data-testid="fold-expand-edge">↑ top</button>
{:else if row.edge === 'bottom'}
<button type="button" onclick={() => expandAllOf(row)} data-testid="fold-expand-edge">↓ bottom</button>
{:else}
<button type="button" onclick={() => expandTop(row)} data-testid="fold-expand-up"
>↑ {Math.min(PARTIAL_EXPAND_STEP, hiddenCount)}</button
>
<button type="button" onclick={() => expandBottom(row)} data-testid="fold-expand-down"
>↓ {Math.min(PARTIAL_EXPAND_STEP, hiddenCount)}</button
>
<button type="button" onclick={() => expandAllOf(row)} data-testid="fold-expand-all">⤢ all</button>
{/if}
</span>
</div>
{:else}
{@const n = row.n}
{@const text = lineByN.get(n)?.text ?? ''}
{@const cols = lineColumns.get(n) ?? []}
{@const tabs = tabsByLine.get(n) ?? []}
{@const rowActive = cols.some((seg) => !!seg && activeEntryKey === `${seg.entry.range[0]}-${seg.entry.range[1]}`)}
<div class="cl" class:hot={cols.some((c) => c !== null)} class:act={rowActive} data-line={n}>
<span class="g">
{#each cols as seg, ci (ci)}
{@const segActive = !!seg && activeEntryKey === `${seg.entry.range[0]}-${seg.entry.range[1]}`}
<span class="gcol">
{#if seg}
{#if seg.pos === 'tick'}
<span class="tick" class:drift={seg.entry.drift} class:act={segActive} style="--bc:{entryColorVar(seg.entry)}"></span>
{:else}
<span
class="br"
class:s={seg.pos === 'start'}
class:e={seg.pos === 'end'}
class:drift={seg.entry.drift}
class:act={segActive}
style="--bc:{entryColorVar(seg.entry)}"
></span>
{/if}
{/if}
</span>
{/each}
</span>
<span class="ln">{n}</span>
<span class="cc">
{#each highlightedFor(n)?.tokens ?? [{ text, style: '' }] as tok, i (i)}
<span class="shiki-tok" style={tok.style}>{tok.text}</span>
{/each}
</span>
{#if tabs.length > 0}
<span class="tabs-wrap">
{#each tabs as entry (entry.range[0] + '-' + entry.range[1])}
<button
type="button"
class="tab"
class:drift={entry.drift}
class:act={activeEntryKey === `${entry.range[0]}-${entry.range[1]}`}
style="--tc:{entryColorVar(entry)}"
title={tabTitle(entry)}
onclick={() => clickEntry(entry)}
data-testid="gutter-tab"
>{entry.drift ? '◷ ' : ''}{entry.hits.length} · {tabInitials(entry)}</button>
{/each}
</span>
{/if}
</div>
{/if}
{/each}
</div>
</div>
{/if}
</div>
{#if minimapSegments.length > 0}
<div class="minimap" data-testid="code-minimap" title="conversation minimap — whole file">
{#each minimapSegments as seg (seg.range[0] + '-' + seg.range[1])}
<button
type="button"
class="mm"
style="top:{seg.top * 100}%;height:{seg.height * 100}%;--bc:{seg.color}"
title={`lines ${seg.range[0]}–${seg.range[1]}`}
onclick={() => scrollToRange(seg.range)}
data-testid="minimap-segment"
></button>
{/each}
<div class="mmview" style="top:{viewport.top * 100}%;height:{viewport.height * 100}%" data-testid="minimap-viewport"></div>
</div>
{/if}
</div>
{/if}
</div>
<style>
.code-wrap {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.codetool {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
padding: 10px 20px;
border-bottom: 1px solid var(--border);
background: var(--panel);
flex: 0 0 auto;
font-size: 13px;
}
.ct-hint {
color: var(--faint);
font-size: 11.5px;
}
.rtoggle {
margin-left: auto;
display: inline-flex;
border: 1px solid var(--border);
border-radius: 7px;
overflow: hidden;
font: 600 10.5px/1 var(--mono);
}
.rtoggle button {
background: transparent;
border: 0;
color: var(--muted);
padding: 4px 9px;
cursor: pointer;
font: inherit;
}
.rtoggle button.on {
background: var(--panel-3);
color: var(--text);
}
.codepage {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
}
.densitywrap {
flex: 1;
overflow: auto;
padding: 14px 0 30px;
}
.repo-rollup {
display: flex;
flex-direction: column;
gap: 6px;
padding: 0 20px;
}
.rollup-row {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
text-align: left;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel-2);
padding: 9px 12px;
color: var(--text);
cursor: pointer;
font-size: 12.5px;
}
.rollup-row:hover {
border-color: var(--border-2);
background: var(--panel-3);
}
.rollup-path {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font: 600 12px/1 var(--mono);
}
.rollup-count {
flex: 0 0 auto;
font: 600 10px/1 var(--mono);
color: var(--accent);
background: color-mix(in srgb, var(--accent) 14%, transparent);
padding: 3px 6px;
border-radius: 5px;
}
.rollup-ts {
flex: 0 0 auto;
font: 500 10.5px/1 var(--mono);
color: var(--faint);
}
.clonestub {
margin: 40px 20px 0;
}
.densefile .code {
padding: 0;
font: 500 12px/1.65 var(--mono);
}
.densefile .cl {
display: flex;
align-items: stretch;
padding: 0 14px;
white-space: pre;
position: relative;
}
.densefile .cl:hover,
.densefile .cl.hot:hover {
background: var(--panel-2);
}
.densefile .cl.hot {
background: color-mix(in srgb, var(--accent) 4%, transparent);
}
.densefile .cl.act {
background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.densefile .cl.act:hover {
background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.filelane {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
text-align: left;
padding: 7px 20px;
background: color-mix(in srgb, var(--state-metric) 9%, transparent);
border: 0;
border-bottom: 1px dashed color-mix(in srgb, var(--state-metric) 35%, transparent);
cursor: pointer;
font: inherit;
color: inherit;
}
.filelane:hover {
background: color-mix(in srgb, var(--state-metric) 15%, transparent);
}
.filelane.act {
background: color-mix(in srgb, var(--state-metric) 22%, transparent);
box-shadow: inset 3px 0 0 var(--state-metric);
}
.filelane .fl-ico {
font: 700 11px/1 var(--mono);
color: var(--state-metric);
}
.filelane .fl-tx {
font-size: 11.5px;
color: var(--muted);
}
.filelane .fl-tx b {
color: var(--text);
}
.g {
flex: 0 0 auto;
display: flex;
width: calc(var(--gutter-cols, 1) * 13px + 6px);
margin-right: 10px;
}
.gcol {
width: 13px;
flex: 0 0 auto;
position: relative;
}
.br {
position: absolute;
left: 3px;
right: 2px;
top: 0;
bottom: 0;
}
.br.s {
top: 3px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.br.e {
bottom: 3px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
.br::before {
content: '';
position: absolute;
inset: 0;
background: color-mix(in srgb, var(--bc) 20%, transparent);
}
.br::after {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2.5px;
background: var(--bc);
}
.br.drift::after {
background: repeating-linear-gradient(0deg, var(--bc) 0 4px, transparent 4px 7px);
}
.br.act::before {
background: color-mix(in srgb, var(--bc) 40%, transparent);
}
.tick {
position: absolute;
left: 3px;
width: 7px;
height: 7px;
border-radius: 2px;
top: 50%;
transform: translateY(-50%);
background: var(--bc);
}
.tick.act {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bc) 50%, transparent);
}
.tick.drift {
background: transparent;
border: 1.5px dashed var(--bc);
box-sizing: border-box;
}
.tabs-wrap {
position: absolute;
right: 14px;
top: -1px;
display: flex;
gap: 3px;
z-index: 2;
}
.tab {
font: 700 9px/1 var(--mono);
color: var(--tc, var(--accent));
background: var(--panel);
border: 1px solid color-mix(in srgb, var(--tc, var(--accent)) 42%, transparent);
border-radius: 0 0 5px 5px;
padding: 1px 5px;
cursor: pointer;
}
.tab:hover {
background: color-mix(in srgb, var(--tc, var(--accent)) 16%, var(--panel));
}
.tab.drift {
border-style: dashed;
}
.tab.act {
background: color-mix(in srgb, var(--tc, var(--accent)) 28%, var(--panel));
color: var(--text);
}
.ln {
width: 24px;
flex: 0 0 auto;
text-align: right;
margin-right: 14px;
color: var(--faint);
user-select: none;
}
.cc {
color: var(--text);
}
.fold {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 14px;
margin: 2px 0;
color: var(--muted);
cursor: default;
background: color-mix(in srgb, var(--accent) 6%, transparent);
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
font: 500 10.5px/1 var(--mono);
}
.fold .exp {
color: var(--accent);
}
.fold .updown {
margin-left: auto;
display: flex;
gap: 10px;
}
.fold .updown button {
background: transparent;
border: 0;
padding: 0;
color: var(--accent);
cursor: pointer;
font: inherit;
}
.fold .updown button:hover {
text-decoration: underline;
}
.minimap {
flex: 0 0 auto;
width: 10px;
position: relative;
background: var(--panel-2);
border-left: 1px solid var(--border);
}
.mm {
position: absolute;
left: 2px;
right: 2px;
border: 0;
border-radius: 2px;
padding: 0;
cursor: pointer;
background: var(--bc);
}
.mm:hover {
outline: 1.5px solid color-mix(in srgb, var(--bc) 60%, transparent);
outline-offset: 0.5px;
}
.mmview {
position: absolute;
left: -1px;
right: -1px;
border: 1.5px solid var(--faint);
border-radius: 3px;
background: color-mix(in srgb, var(--text) 8%, transparent);
pointer-events: none;
}
</style>