<script lang="ts">
import AppIcon from "../layout/AppIcon.svelte";
import { listPathNotes, type PathNote } from "../../api/client.js";
import { annotations } from "../../stores/annotations.svelte.js";
import AnnotationsPanel from "../annotations/AnnotationsPanel.svelte";
import { notes as notesStore } from "../../stores/notes.svelte.js";
import { workbench } from "../../stores/workbench.svelte.js";
import { explorer as explorerStore } from "../../stores/explorer.svelte.js";
import { conflicts } from "../../stores/conflicts.svelte.js";
import { issues as issuesStore } from "../../stores/issues.svelte.js";
import IssueItem from "../issues/IssueItem.svelte";
import NodePeers from "../machine/NodePeers.svelte";
import IssueCreate from "../issues/IssueCreate.svelte";
import { github } from "../../stores/github.svelte.js";
import { pullState, describeStanding } from "../../api/types/github.js";
import { scoped, viewWorktree } from "../../stores/worktree-scope.svelte.js";
const explorer = scoped(explorerStore);
const scopedIssues = scoped(issuesStore);
const scopedNotes = scoped(notesStore);
const worktree = viewWorktree("the node info page");
interface Props {
path: string;
}
let { path }: Props = $props();
let name = $derived(path.split("/").pop() ?? path);
let isDir = $derived(!name.includes(".") || path.endsWith("/"));
// Find the worktree entry for this path to get submodule info
let parentPath = $derived(path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "");
let treeBranchEntries = $derived(parentPath !== undefined ? explorer.treeEntries.get(parentPath) : undefined);
let entry = $derived(
treeBranchEntries?.find((e) => e.path === path) ?? null,
);
let submoduleError = $derived(entry?.submodule?.error ?? null);
// This node's repository, when the node IS one. Everything a repository row
// in the old fleet panel carried is here, including the changes the tree
// cannot show: a deleted file has no row in a file tree, and it is a change
// like any other.
let repo = $derived(entry?.repo_status ?? null);
let changes = $derived((repo?.files ?? []).filter((file) => file.status !== "ignored"));
let repoConflicts = $derived(repo ? conflicts.forRepository(path) : null);
function openDiff(file: string) {
workbench.openDiff(file, repo?.is_superproject ? null : path, true, worktree);
}
function openMachine() {
workbench.showPeers();
}
let submoduleVersion = $derived(entry?.submodule?.version ?? null);
let submoduleGithub = $derived(entry?.submodule?.github_repo ?? null);
// This node's GitHub repository, when it has one. Issues belong to a
// repository, and a repository is a node in this tree — so they are shown
// here, beside that node's notes, rather than in a panel of their own where
// "which repository" was a filter somebody had to set.
let issueFilter = $state<"open" | "closed" | "all">("open");
let githubRepo = $derived(submoduleGithub ?? superprojectRepo());
let repoIssues = $derived(
githubRepo ? scopedIssues.forRepo(githubRepo, issueFilter) : [],
);
let issueSync = $derived(githubRepo ? scopedIssues.syncOf(githubRepo) : null);
let creatingIssue = $state(false);
$effect(() => {
const repo = githubRepo;
if (!repo) return;
// Local reads of the mirror. Opening this page does not call GitHub, so it
// costs nothing to do every time it opens.
void scopedIssues.loadRepo(repo);
void github.loadRepo(repo);
});
// Everything else this repository has, out of the mirror.
let mirror = $derived(githubRepo ? github.of(githubRepo) : null);
let pullFilter = $state<"open" | "merged" | "all">("open");
let visiblePulls = $derived(
(mirror?.pulls ?? []).filter((pull) => {
if (pullFilter === "all") return true;
const state = pullState(pull);
return pullFilter === "open" ? state === "open" || state === "draft" : state === "merged";
}),
);
/**
* The superproject's GitHub repository, for the worktree root node.
*
* Read from the tree rather than configured: the root's own entry carries
* it, the same way a submodule's does.
*/
function superprojectRepo(): string | null {
if (path !== "" && path !== ".") return null;
for (const entries of explorer.treeEntries.values()) {
for (const candidate of entries) {
if (candidate.repo_status?.is_superproject && candidate.submodule?.github_repo) {
return candidate.submodule.github_repo;
}
}
}
return null;
}
/** The ref notes about this path are linked to; null when it has none. */
let about = $state<string | null>(null);
/** The worktree's documents that mention the path. */
let mentioned = $state<PathNote[]>([]);
/** Why notes could not be read or made here, said rather than hidden. */
let problems = $state<string[]>([]);
let loading = $state(true);
let loadError = $state<string | null>(null);
async function loadNotes() {
loading = true;
loadError = null;
try {
const res = await listPathNotes(path, worktree);
about = res.about;
mentioned = res.mentioned_in;
problems = res.problems;
} catch (e) {
loadError = e instanceof Error ? e.message : String(e);
} finally {
loading = false;
}
}
$effect(() => {
void loadNotes();
});
// The explorer's badge: the notes linked here and the documents that
// mention it, kept as notes are added or removed on this page.
$effect(() => {
if (loading || loadError) return;
const linked = about ? annotations.notesAbout(about).length : 0;
scopedNotes.set(path, linked + mentioned.length);
});
function openNote(note: PathNote) {
workbench.openFile(note.path, true, undefined, worktree);
}
function formatTime(iso: string): string {
try {
return new Date(iso).toLocaleString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
} catch {
return iso;
}
}
</script>
<div class="node-info">
<div class="header">
<span class="header-icon">
<AppIcon name={isDir ? "folder" : "file-text"} size={18} />
</span>
<div class="header-text">
<h1 class="node-name">{name}</h1>
<span class="node-path">{path}</span>
{#if submoduleError}
<span class="header-error">
<AppIcon name="triangle-alert" size={13} />
{submoduleError}
</span>
{/if}
{#if submoduleVersion && !submoduleError}
<span class="header-version">v{submoduleVersion}</span>
{/if}
{#if submoduleGithub && !submoduleError}
<span class="header-github">
<AppIcon name="git-branch" size={12} />
{submoduleGithub}
</span>
{/if}
</div>
</div>
{#if repo}
<section class="repo-section">
<div class="section-header">
<h2 class="section-heading">Repository</h2>
<span class="repo-facts">
{#if !repo.exists}
<span class="repo-fact absent">declared, not checked out</span>
{:else}
<span class="repo-fact" class:detached={repo.is_detached}>
{repo.is_detached ? "detached HEAD" : (repo.branch ?? "no branch")}
</span>
{#if repo.ahead > 0}<span class="repo-fact">{repo.ahead} ahead</span>{/if}
{#if repo.behind > 0}<span class="repo-fact">{repo.behind} behind</span>{/if}
{/if}
</span>
</div>
{#if repoConflicts}
<!-- A pointer to the section below rather than to another page: the
decision is on this page now, path by path, and sending somebody
away from the node they are looking at was the whole problem with
the line that used to be here. -->
<p class="conflict-note">
<AppIcon name="triangle-alert" size={13} />
{repoConflicts.conflicts} conflict{repoConflicts.conflicts === 1 ? "" : "s"}
with {repoConflicts.peers.join(", ")} — in Peers, below.
</p>
{/if}
{#if !repo.exists}
<p class="empty-hint">
Nothing is checked out here. Worktree Update brings it in.
</p>
{:else if changes.length === 0}
<p class="empty-hint">No uncommitted changes.</p>
{:else}
<ul class="change-list">
{#each changes as file (file.path)}
<li>
<button class="change-row" onclick={() => openDiff(file.path)} title="Open the diff for {file.path}">
<span class="change-status change-status--{file.status}" title={file.status}>
{file.status.slice(0, 1).toUpperCase()}
</span>
<span class="change-path">{file.path}</span>
</button>
</li>
{/each}
</ul>
{/if}
</section>
{/if}
<!-- After the repository, and for every node rather than only a repository:
a folder inside one holds differences too, and somebody looking at that
folder is asking the same question about it. -->
<NodePeers {path} />
{#if githubRepo}
<section class="issues-section">
<div class="section-header">
<h2 class="section-heading">Issues</h2>
<span class="section-note">{githubRepo}</span>
<div class="section-filters">
{#each ["open", "closed", "all"] as filter}
<button
class="filter-btn"
class:active={issueFilter === filter}
onclick={() => (issueFilter = filter as "open" | "closed" | "all")}
>{filter}</button>
{/each}
</div>
<button
class="new-note-btn"
onclick={() => (creatingIssue = !creatingIssue)}
title="Open an issue on {githubRepo}"
>
<AppIcon name="plus" size={11} />
New issue
</button>
<button
class="new-note-btn"
onclick={() => scopedIssues.refresh()}
disabled={scopedIssues.refreshing}
title="Ask GitHub now, rather than waiting for the next background sync"
>
<AppIcon name="refresh" size={11} />
{scopedIssues.refreshing ? "Refreshing…" : "Refresh"}
</button>
</div>
<!--
What is shown is what the mirror holds, and it says so. A list that
implied it was live would be claiming the network answered — which is
exactly the lie the old page told when it failed.
-->
<p class="sync-note">
{#if issueSync?.last_error}
<span class="sync-problem">Last refresh failed: {issueSync.last_error}</span>
{#if issueSync.synced_at}
· showing what was read {formatTime(issueSync.synced_at)}
{/if}
{:else if issueSync?.synced_at}
As of {formatTime(issueSync.synced_at)}
{:else if scopedIssues.noToken}
Not synced: there is no GitHub login — <code>gh auth login</code>, or set <code>GITHUB_TOKEN</code>.
{:else}
Not synced yet — the daemon reads GitHub in the background.
{/if}
</p>
{#if creatingIssue}
<IssueCreate
repos={[githubRepo]}
onclose={() => (creatingIssue = false)}
/>
{/if}
{#if repoIssues.length === 0}
<p class="empty-hint">
{#if issueSync?.ever_synced}
No {issueFilter === "all" ? "" : issueFilter} issues on this repository.
{:else}
Nothing read yet.
{/if}
</p>
{:else}
<ul class="issue-list">
{#each repoIssues as issue (issue.number)}
<IssueItem
{issue}
onclick={() => workbench.openIssue({ ...issue, repo: githubRepo })}
/>
{/each}
</ul>
{/if}
</section>
{/if}
{#if githubRepo && mirror}
<section class="issues-section">
<div class="section-header">
<h2 class="section-heading">Pull requests</h2>
<div class="section-filters">
{#each ["open", "merged", "all"] as filter}
<button
class="filter-btn"
class:active={pullFilter === filter}
onclick={() => (pullFilter = filter as "open" | "merged" | "all")}
>{filter}</button>
{/each}
</div>
</div>
<p class="sync-note">{describeStanding(mirror.pullsSync)}</p>
{#if visiblePulls.length === 0}
<p class="empty-hint">
{mirror.pullsSync?.ever_synced ? "None." : "Nothing read yet."}
</p>
{:else}
<ul class="mirror-list">
{#each visiblePulls as pull (pull.number)}
<li>
<a class="mirror-row" href={pull.html_url} target="_blank" rel="noreferrer">
<span class="pull-state pull-state--{pullState(pull)}">{pullState(pull)}</span>
<span class="mirror-title">{pull.title}</span>
<span class="mirror-meta">
#{pull.number} · {pull.head.ref}
<AppIcon name="arrow-right" size={10} />
{pull.base.ref}
</span>
</a>
</li>
{/each}
</ul>
{/if}
</section>
{#if mirror.checks}
<section class="issues-section">
<div class="section-header">
<h2 class="section-heading">Checks</h2>
<span class="check-state check-state--{mirror.checks.state}">{mirror.checks.state}</span>
<span class="section-note">{mirror.checks.sha.slice(0, 7)}</span>
</div>
<p class="sync-note">{describeStanding(mirror.checksSync)}</p>
<ul class="mirror-list">
{#each mirror.checks.runs as run (run.name)}
<li>
<span class="mirror-row">
<span class="check-state check-state--{run.conclusion ?? 'pending'}">
{run.conclusion ?? run.status}
</span>
<span class="mirror-title">{run.name}</span>
</span>
</li>
{/each}
</ul>
</section>
{/if}
<section class="issues-section">
<div class="section-header">
<h2 class="section-heading">Releases and tags</h2>
</div>
<p class="sync-note">{describeStanding(mirror.releasesSync)}</p>
{#if mirror.releases.length === 0 && mirror.tags.length === 0}
<p class="empty-hint">
{mirror.releasesSync?.ever_synced ? "None published." : "Nothing read yet."}
</p>
{:else}
<ul class="mirror-list">
{#each mirror.releases.slice(0, 8) as release (release.id)}
<li>
<a class="mirror-row" href={release.html_url} target="_blank" rel="noreferrer">
<span class="release-tag">{release.tag_name}</span>
<span class="mirror-title">{release.name ?? release.tag_name}</span>
<span class="mirror-meta">
{#if release.draft}draft{:else if release.prerelease}pre-release{:else}released{/if}
{#if release.assets.length > 0}
· {release.assets.length} asset{release.assets.length === 1 ? "" : "s"}
{/if}
</span>
</a>
</li>
{/each}
</ul>
{#if mirror.tags.length > 0}
<p class="tag-line">
<span class="section-note">tags</span>
{#each mirror.tags.slice(0, 12) as tag (tag.name)}
<span class="release-tag">{tag.name}</span>
{/each}
{#if mirror.tags.length > 12}
<span class="section-note">+{mirror.tags.length - 12} more</span>
{/if}
</p>
{/if}
{/if}
</section>
{#if mirror.milestones.length > 0}
<section class="issues-section">
<div class="section-header">
<h2 class="section-heading">Milestones</h2>
</div>
<ul class="mirror-list">
{#each mirror.milestones as milestone (milestone.number)}
<li>
<a class="mirror-row" href={milestone.html_url} target="_blank" rel="noreferrer">
<span class="mirror-title">{milestone.title}</span>
<span class="mirror-meta">
{milestone.open_issues} open · {milestone.closed_issues} closed
{#if milestone.due_on}· due {new Date(milestone.due_on).toLocaleDateString()}{/if}
</span>
</a>
</li>
{/each}
</ul>
</section>
{/if}
{#if mirror.metadata || mirror.traffic}
<section class="issues-section">
<div class="section-header">
<h2 class="section-heading">Repository</h2>
{#if mirror.metadata}
<span class="section-note">
{mirror.metadata.private ? "private" : "public"}
{#if mirror.metadata.archived}· archived{/if}
</span>
{/if}
</div>
<p class="sync-note">{describeStanding(mirror.metadataSync)}</p>
<div class="stat-row">
{#if mirror.metadata}
<span class="stat"><strong>{mirror.metadata.stargazers_count}</strong> stars</span>
<span class="stat"><strong>{mirror.metadata.forks_count}</strong> forks</span>
<span class="stat"><strong>{mirror.metadata.watchers_count}</strong> watchers</span>
{/if}
{#if mirror.traffic}
<span class="stat"><strong>{mirror.traffic.views}</strong> views (14d)</span>
<span class="stat"><strong>{mirror.traffic.unique_views}</strong> unique</span>
<span class="stat"><strong>{mirror.traffic.clones}</strong> clones</span>
{/if}
</div>
{#if mirror.trafficSync?.last_error}
<!-- Traffic needs push access, so a repository somebody only reads
answers 403. Said plainly: it is not a fault, and not zero views. -->
<p class="sync-note">Traffic is not readable here: {mirror.trafficSync.last_error}</p>
{/if}
{#if mirror.metadata && mirror.metadata.topics.length > 0}
<p class="tag-line">
{#each mirror.metadata.topics as topic (topic)}
<span class="release-tag">{topic}</span>
{/each}
</p>
{/if}
</section>
{/if}
{/if}
<section class="notes-section">
{#if loading}
<div class="loading">
<span class="spinner"><AppIcon name="refresh" size={12} /></span>
Loading…
</div>
{:else if loadError}
<div class="error-banner">
<AppIcon name="circle-x" size={13} />
{loadError}
<button class="retry-btn" onclick={loadNotes}>Retry</button>
</div>
{:else}
{#each problems as problem (problem)}
<div class="error-banner">
<AppIcon name="circle-x" size={13} />
{problem}
</div>
{/each}
{#if about}
<AnnotationsPanel {about} writeInto={worktree} title="Notes" />
{/if}
<div class="section-header">
<h2 class="section-heading">Mentioned in</h2>
</div>
{#if mentioned.length === 0}
<p class="empty-hint">No document of this worktree links to this path.</p>
{:else}
<ul class="note-list">
{#each mentioned as note (note.path)}
<li class="note-item">
<button class="note-row" onclick={() => openNote(note)}>
<span class="note-icon"><AppIcon name="file-text" size={13} /></span>
<span class="note-body">
<span class="note-title">{note.title}</span>
<span class="note-meta">{note.path} · {formatTime(note.updated_at)}</span>
</span>
<span class="note-open-hint"><AppIcon name="chevron-right" size={11} /></span>
</button>
</li>
{/each}
</ul>
{/if}
{/if}
</section>
</div>
<style>
.issues-section {
margin-bottom: var(--wb-spacing-18);
}
.section-note {
font-size: var(--wb-font-size-11);
color: var(--vscode-descriptionForeground);
font-family: var(--vscode-editor-font-family, monospace);
}
.section-filters {
display: flex;
gap: var(--wb-spacing-4);
margin-left: auto;
}
.filter-btn {
background: none;
border: 1px solid transparent;
color: var(--vscode-descriptionForeground);
font-size: var(--wb-font-size-11);
padding: var(--wb-spacing-1) var(--wb-spacing-6);
border-radius: var(--wb-corner-radius-2);
cursor: pointer;
}
.filter-btn.active {
border-color: var(--vscode-panel-border);
color: var(--vscode-editor-foreground);
}
.sync-note {
font-size: var(--wb-font-size-11);
color: var(--vscode-descriptionForeground);
margin: var(--wb-spacing-2) 0 var(--wb-spacing-8);
}
.sync-problem {
color: var(--vscode-editorWarning-foreground, #cca700);
}
.issue-list,
.mirror-list {
list-style: none;
margin: 0;
padding: 0;
}
.mirror-row {
display: flex;
align-items: baseline;
gap: var(--wb-spacing-8);
padding: var(--wb-spacing-3) 0;
text-decoration: none;
color: var(--vscode-editor-foreground);
font-size: var(--wb-font-size-12);
}
.mirror-row:hover .mirror-title {
text-decoration: underline;
}
.mirror-meta,
.stat {
color: var(--vscode-descriptionForeground);
font-size: var(--wb-font-size-11);
}
.stat-row {
display: flex;
gap: var(--wb-spacing-14);
flex-wrap: wrap;
margin: var(--wb-spacing-4) 0;
}
.tag-line {
display: flex;
gap: var(--wb-spacing-6);
flex-wrap: wrap;
align-items: center;
margin: var(--wb-spacing-6) 0 0;
}
.release-tag {
font-family: var(--vscode-editor-font-family, monospace);
font-size: var(--wb-font-size-10);
padding: var(--wb-spacing-1) var(--wb-spacing-6);
border-radius: var(--wb-corner-radius-8);
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
}
.pull-state,
.check-state {
font-size: var(--wb-font-size-10);
text-transform: uppercase;
letter-spacing: 0.05em;
min-width: var(--wb-control-size-58);
}
.pull-state--open { color: var(--vscode-testing-iconPassed, #2d7d46); }
.pull-state--draft { color: var(--vscode-descriptionForeground); }
.pull-state--merged { color: #8957e5; }
.pull-state--closed { color: var(--vscode-errorForeground); }
.check-state--success { color: var(--vscode-testing-iconPassed, #2d7d46); }
.check-state--failure { color: var(--vscode-errorForeground); }
.check-state--pending { color: var(--vscode-editorWarning-foreground, #cca700); }
.check-state--neutral { color: var(--vscode-descriptionForeground); }
.repo-facts {
display: inline-flex;
gap: var(--wb-spacing-8);
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
}
.repo-fact.detached,
.repo-fact.absent {
color: var(--vscode-errorForeground, #f14c4c);
}
.repo-fact.absent {
color: var(--vscode-disabledForeground);
font-style: italic;
}
.conflict-note {
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
margin: 0 0 var(--wb-spacing-8);
font-size: var(--wb-font-size-12);
color: var(--vscode-errorForeground, #f14c4c);
}
.change-list {
display: flex;
flex-direction: column;
list-style: none;
margin: 0;
padding: 0;
}
.change-row {
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
width: 100%;
padding: var(--wb-spacing-3) var(--wb-spacing-6);
border: none;
background: none;
color: inherit;
font-size: var(--wb-font-size-12);
text-align: left;
cursor: pointer;
}
.change-row:hover {
background: var(--vscode-list-hoverBackground);
}
.change-status {
width: var(--wb-control-size-14);
flex-shrink: 0;
font-weight: 700;
color: var(--vscode-disabledForeground);
}
.change-status--conflict {
color: var(--vscode-errorForeground, #f14c4c);
}
.change-status--untracked {
color: var(--vscode-testing-iconPassed, #4caf50);
}
.change-status--deleted {
color: var(--vscode-errorForeground, #f14c4c);
}
.change-path {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-info {
height: 100%;
overflow-y: auto;
padding: var(--wb-spacing-28) var(--wb-spacing-36);
display: flex;
flex-direction: column;
gap: var(--wb-spacing-28);
font-size: var(--wb-font-size-13);
color: var(--vscode-editor-foreground);
}
.header {
display: flex;
align-items: flex-start;
gap: var(--wb-spacing-14);
}
.header-icon {
color: var(--vscode-disabledForeground);
flex-shrink: 0;
margin-top: var(--wb-spacing-3);
}
.header-text {
display: flex;
flex-direction: column;
gap: var(--wb-spacing-3);
min-width: 0;
}
.node-name {
font-size: var(--wb-font-size-17);
font-weight: 650;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-path {
font-family: var(--wsb-font-mono);
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-error {
display: inline-flex;
align-items: center;
gap: var(--wb-spacing-4);
font-size: var(--wb-font-size-12);
color: #e09030;
margin-top: var(--wb-spacing-2);
}
.header-version {
font-family: var(--wsb-font-mono);
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
margin-top: var(--wb-spacing-2);
}
.header-github {
display: inline-flex;
align-items: center;
gap: var(--wb-spacing-4);
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
margin-top: var(--wb-spacing-2);
}
.notes-section {
display: flex;
flex-direction: column;
gap: var(--wb-spacing-10);
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
/* The heading, the repository it is about and the actions are separate
words; without this they run together. */
gap: var(--wb-spacing-8);
}
.section-heading {
font-size: var(--wb-font-size-11);
font-weight: 650;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--vscode-disabledForeground);
margin: 0;
}
.new-note-btn {
display: flex;
align-items: center;
gap: var(--wb-spacing-4);
padding: var(--wb-spacing-3) var(--wb-spacing-8);
font-size: var(--wb-font-size-11);
color: var(--vscode-button-foreground);
background: var(--vscode-button-background);
border-radius: var(--wsb-radius-sm);
cursor: pointer;
}
.new-note-btn:hover:not(:disabled) {
background: var(--vscode-button-hoverBackground);
}
.new-note-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading {
display: flex;
align-items: center;
gap: var(--wb-spacing-6);
color: var(--vscode-disabledForeground);
font-size: var(--wb-font-size-12);
}
.spinner {
animation: spin 1s linear infinite;
display: inline-flex;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.error-banner {
display: flex;
align-items: center;
gap: var(--wb-spacing-6);
padding: var(--wb-spacing-8) var(--wb-spacing-12);
background: color-mix(in srgb, var(--vscode-errorForeground) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--vscode-errorForeground) 30%, transparent);
border-radius: var(--wsb-radius-sm);
color: var(--vscode-errorForeground);
font-size: var(--wb-font-size-12);
}
.retry-btn {
margin-left: auto;
font-size: var(--wb-font-size-11);
text-decoration: underline;
color: var(--vscode-errorForeground);
cursor: pointer;
}
.empty-hint {
color: var(--vscode-disabledForeground);
font-size: var(--wb-font-size-12);
font-style: italic;
margin: 0;
}
.note-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--wb-spacing-4);
}
.note-item {
display: flex;
}
.note-row {
flex: 1;
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
padding: var(--wb-spacing-8) var(--wb-spacing-10);
background: var(--vscode-list-hoverBackground);
border-radius: var(--wsb-radius-sm);
border: 1px solid var(--vscode-panel-border);
cursor: pointer;
text-align: left;
min-width: 0;
}
.note-row:hover {
background: var(--vscode-list-activeSelectionBackground, var(--vscode-list-hoverBackground));
border-color: var(--vscode-focusBorder);
}
.note-icon {
color: var(--vscode-disabledForeground);
flex-shrink: 0;
}
.note-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: var(--wb-spacing-2);
}
.note-title {
font-size: var(--wb-font-size-13);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--vscode-editor-foreground);
}
.note-meta {
font-size: var(--wb-font-size-10);
color: var(--vscode-disabledForeground);
}
.note-open-hint {
color: var(--vscode-disabledForeground);
flex-shrink: 0;
opacity: 0;
transition: opacity 0.1s;
}
.note-row:hover .note-open-hint {
opacity: 1;
}
</style>