summaryrefslogtreecommitdiff
path: root/vrsite/assets
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-30 17:05:12 -0300
committerGabriel Schneider <[email protected]>2026-07-30 17:56:23 -0300
commit8ec4e135e318c7799b2576a8729493c063da4a97 (patch)
treeab37cdbb7f478eb996264cf941891f127f303c85 /vrsite/assets
parentcb05c363045bf0e7af5858f6d7bc26f026fa9d70 (diff)
downloadnotevi-8ec4e135e318c7799b2576a8729493c063da4a97.tar.gz
notevi-8ec4e135e318c7799b2576a8729493c063da4a97.zip
Diffstat (limited to 'vrsite/assets')
-rw-r--r--vrsite/assets/app.js49
-rw-r--r--vrsite/assets/bash.scm21
-rw-r--r--vrsite/assets/c.scm36
-rw-r--r--vrsite/assets/cpp.scm25
-rw-r--r--vrsite/assets/go.scm34
-rw-r--r--vrsite/assets/javascript-params.scm12
-rw-r--r--vrsite/assets/javascript.scm52
-rw-r--r--vrsite/assets/json.scm13
-rw-r--r--vrsite/assets/jsx.scm16
-rw-r--r--vrsite/assets/python.scm50
-rw-r--r--vrsite/assets/style.css.tmpl102
-rw-r--r--vrsite/assets/typescript.scm15
12 files changed, 421 insertions, 4 deletions
diff --git a/vrsite/assets/app.js b/vrsite/assets/app.js
index 79dc190..104cc85 100644
--- a/vrsite/assets/app.js
+++ b/vrsite/assets/app.js
@@ -138,6 +138,55 @@ for (const ta of document.querySelectorAll(".notform textarea"))
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) ta.form.requestSubmit();
});
+// ── conversation ──────────────────────────────────────────────────────────
+// A session page opened from a note lands on that note, flashed the same way
+// clicking a noted line flashes it.
+const conv = document.querySelector(".conv[data-anchor]");
+if (conv && conv.dataset.anchor) {
+ const here = document.getElementById(conv.dataset.anchor);
+ if (here) {
+ here.scrollIntoView({block: "start"});
+ here.classList.remove("flash"); void here.offsetWidth; here.classList.add("flash");
+ }
+}
+
+// The trip back: a note in a conversation opens the code it was written
+// about. The card is the target, so aim anywhere on it — except at the links
+// it already carries.
+const goTo = w => { location.href = w.dataset.href; };
+for (const w of document.querySelectorAll(".notewrap[data-href]")) {
+ w.addEventListener("click", e => { if (!e.target.closest("a")) goTo(w); });
+ w.addEventListener("keydown", e => {
+ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); goTo(w); }
+ });
+}
+
+// ── arriving at a note ────────────────────────────────────────────────────
+// ?note=N means someone followed a note here rather than opening the file:
+// show what they came for — notes focus (coverage hides the note), the card
+// flashed in the pane, the lines it covers marked.
+const want = new URLSearchParams(location.search).get("note");
+if (want && document.body.dataset.kind === "file" && /^\d+$/.test(want)) {
+ setMode("notes");
+ const card = document.getElementById("note-" + want);
+ if (card) {
+ // the card's own range, not the lines' data-note: a line belongs to the
+ // first note that claimed it, so overlapping notes would mark nothing
+ const from = +card.dataset.start, to = +card.dataset.end || +card.dataset.start;
+ let first = null;
+ for (let n = from; n > 0 && n <= to; n++) {
+ const li = document.getElementById("L" + n);
+ if (!li) continue;
+ li.classList.add("focus");
+ first = first || li;
+ }
+ // after the browser has had its own go at the #L anchor
+ if (first) requestAnimationFrame(() => first.scrollIntoView({block: "center"}));
+ card.scrollIntoView({block: "nearest"});
+ card.classList.remove("flash"); void card.offsetWidth; card.classList.add("flash");
+ }
+}
+
// ── note filters ──────────────────────────────────────────────────────────
// Live pages let the server filter (htmx); the static export filters the
// rendered list in place, so the same form works either way.
diff --git a/vrsite/assets/bash.scm b/vrsite/assets/bash.scm
new file mode 100644
index 0000000..4c7926a
--- /dev/null
+++ b/vrsite/assets/bash.scm
@@ -0,0 +1,21 @@
+; Bash highlights, from tree-sitter/tree-sitter-bash v0.25.1
+; queries/highlights.scm (MIT). Reordered so expansions stay visible inside the
+; strings that contain them, and capture names mapped onto zed syntax keys.
+; Upstream's @embedded captures are dropped: zed paints `embedded` as plain
+; foreground, so they only cost query time.
+
+["$" "&&" "||" ">" ">>" "<" "|" ";" "=" "!"] @operator
+
+["case" "do" "done" "elif" "else" "esac" "export" "fi" "for" "function" "if"
+ "in" "local" "readonly" "select" "then" "unset" "until" "while"] @keyword
+
+[(string) (raw_string) (heredoc_body) (heredoc_start)] @string
+(variable_name) @property
+(file_descriptor) @number
+
+(command_name) @function
+(function_definition name: (word) @function)
+((command (_) @constant)
+ (#match? @constant "^-"))
+
+(comment) @comment
diff --git a/vrsite/assets/c.scm b/vrsite/assets/c.scm
new file mode 100644
index 0000000..789bf93
--- /dev/null
+++ b/vrsite/assets/c.scm
@@ -0,0 +1,36 @@
+; C highlights, from tree-sitter/tree-sitter-c v0.24.2 queries/highlights.scm
+; (MIT). Reordered for later-patterns-win, capture names mapped onto zed syntax
+; keys (preprocessor directives get @preproc rather than upstream's @keyword,
+; which zed themes colour separately). Also loaded ahead of cpp.scm, the way
+; upstream composes the C++ highlights.
+
+["(" ")" "[" "]" "{" "}"] @punctuation.bracket
+[";" "," "."] @punctuation.delimiter
+["--" "-" "-=" "->" "=" "!=" "*" "&" "&&" "+" "++" "+=" "<" "==" ">"
+ "||"] @operator
+
+["break" "case" "const" "continue" "default" "do" "else" "enum" "extern"
+ "for" "if" "inline" "return" "sizeof" "static" "struct" "switch" "typedef"
+ "union" "volatile" "while"] @keyword
+
+["#define" "#elif" "#else" "#endif" "#if" "#ifdef" "#ifndef"
+ "#include"] @preproc
+(preproc_directive) @preproc
+
+[(type_identifier) (primitive_type) (sized_type_specifier)] @type
+(field_identifier) @property
+(statement_identifier) @label
+((identifier) @constant
+ (#match? @constant "^[A-Z][A-Z0-9_]*$"))
+
+(call_expression function: (identifier) @function)
+(call_expression function: (field_expression
+ field: (field_identifier) @function.method))
+(function_declarator declarator: (identifier) @function)
+(preproc_function_def name: (identifier) @function)
+
+(number_literal) @number
+(null) @constant
+[(string_literal) (char_literal) (system_lib_string)] @string
+(escape_sequence) @string.escape
+(comment) @comment
diff --git a/vrsite/assets/cpp.scm b/vrsite/assets/cpp.scm
new file mode 100644
index 0000000..e939a7a
--- /dev/null
+++ b/vrsite/assets/cpp.scm
@@ -0,0 +1,25 @@
+; C++ highlights, from tree-sitter/tree-sitter-cpp v0.23.4
+; queries/highlights.scm (MIT). A supplement loaded after c.scm, as upstream
+; composes it, so these patterns win. Capture names mapped onto zed syntax
+; keys; `this` gets @variable.special to match what rust.scm does for `self`.
+
+["catch" "class" "co_await" "co_return" "co_yield" "constexpr" "constinit"
+ "consteval" "delete" "explicit" "final" "friend" "mutable" "namespace"
+ "noexcept" "new" "override" "private" "protected" "public" "template"
+ "throw" "try" "typename" "using" "concept" "requires" "virtual"] @keyword
+
+(auto) @type
+((namespace_identifier) @type
+ (#match? @type "^[A-Z]"))
+
+(call_expression function: (qualified_identifier
+ name: (identifier) @function))
+(template_function name: (identifier) @function)
+(template_method name: (field_identifier) @function.method)
+(function_declarator declarator: (qualified_identifier
+ name: (identifier) @function))
+(function_declarator declarator: (field_identifier) @function.method)
+
+(raw_string_literal) @string
+(this) @variable.special
+(null "nullptr" @constant)
diff --git a/vrsite/assets/go.scm b/vrsite/assets/go.scm
new file mode 100644
index 0000000..56f676c
--- /dev/null
+++ b/vrsite/assets/go.scm
@@ -0,0 +1,34 @@
+; Go highlights, from tree-sitter/tree-sitter-go v0.25.0 queries/highlights.scm
+; (MIT). Reordered — upstream is written first-pattern-wins, this engine paints
+; later patterns over earlier ones — capture names mapped onto zed syntax keys,
+; and the builtin-function #match? turned into #any-of?, which needs no regex.
+
+["(" ")" "[" "]" "{" "}"] @punctuation.bracket
+[";" "," "." ":"] @punctuation.delimiter
+["--" "-" "-=" ":=" "!" "!=" "..." "*" "*=" "/" "/=" "&" "&&" "&=" "%" "%="
+ "^" "^=" "+" "++" "+=" "<-" "<" "<<" "<<=" "<=" "=" "==" ">" ">=" ">>" ">>="
+ "|" "|=" "||" "~"] @operator
+
+["break" "case" "chan" "const" "continue" "default" "defer" "else"
+ "fallthrough" "for" "func" "go" "goto" "if" "import" "interface" "map"
+ "package" "range" "return" "select" "struct" "switch" "type" "var"] @keyword
+
+(type_identifier) @type
+(field_identifier) @property
+(label_name) @label
+
+(function_declaration name: (identifier) @function)
+(method_declaration name: (field_identifier) @function.method)
+(call_expression function: (identifier) @function)
+(call_expression function: (selector_expression
+ field: (field_identifier) @function.method))
+((call_expression function: (identifier) @function.builtin)
+ (#any-of? @function.builtin "append" "cap" "clear" "close" "complex" "copy"
+ "delete" "imag" "len" "make" "max" "min" "new" "panic" "print" "println"
+ "real" "recover"))
+
+[(int_literal) (float_literal) (imaginary_literal)] @number
+[(true) (false) (nil) (iota)] @constant
+[(interpreted_string_literal) (raw_string_literal) (rune_literal)] @string
+(escape_sequence) @string.escape
+(comment) @comment
diff --git a/vrsite/assets/javascript-params.scm b/vrsite/assets/javascript-params.scm
new file mode 100644
index 0000000..db300e3
--- /dev/null
+++ b/vrsite/assets/javascript-params.scm
@@ -0,0 +1,12 @@
+; JavaScript parameters, from tree-sitter/tree-sitter-javascript v0.25.0
+; queries/highlights-params.scm (MIT), verbatim. Its own file because upstream
+; keeps it out of the TypeScript composition: there a formal_parameters child
+; is always a required_parameter or optional_parameter, so these patterns are
+; not merely dead but rejected by the query compiler as impossible.
+
+(formal_parameters [
+ (identifier) @variable.parameter
+ (array_pattern (identifier) @variable.parameter)
+ (object_pattern [
+ (pair_pattern value: (identifier) @variable.parameter)
+ (shorthand_property_identifier_pattern) @variable.parameter])])
diff --git a/vrsite/assets/javascript.scm b/vrsite/assets/javascript.scm
new file mode 100644
index 0000000..a5c8f01
--- /dev/null
+++ b/vrsite/assets/javascript.scm
@@ -0,0 +1,52 @@
+; JavaScript highlights, from tree-sitter/tree-sitter-javascript v0.25.0
+; queries/highlights.scm (MIT). Reordered for later-patterns-win and capture
+; names mapped onto zed syntax keys. Upstream's @variable.builtin and require()
+; patterns are gone: they lean on (#is-not? local), which no query engine
+; outside nvim evaluates. Also loaded ahead of typescript.scm, the way upstream
+; composes the TypeScript highlights.
+
+["(" ")" "[" "]" "{" "}"] @punctuation.bracket
+[";" (optional_chain) "." ","] @punctuation.delimiter
+["-" "--" "-=" "+" "++" "+=" "*" "*=" "**" "**=" "/" "/=" "%" "%=" "<" "<="
+ "<<" "<<=" "=" "==" "===" "!" "!=" "!==" "=>" ">" ">=" ">>" ">>=" ">>>"
+ ">>>=" "~" "^" "&" "|" "^=" "&=" "|=" "&&" "||" "??" "&&=" "||="
+ "??="] @operator
+
+["as" "async" "await" "break" "case" "catch" "class" "const" "continue"
+ "debugger" "default" "delete" "do" "else" "export" "extends" "finally" "for"
+ "from" "function" "get" "if" "import" "in" "instanceof" "let" "new" "of"
+ "return" "set" "static" "switch" "target" "throw" "try" "typeof" "var"
+ "void" "while" "with" "yield"] @keyword
+
+[(property_identifier) (shorthand_property_identifier)] @property
+
+((identifier) @constructor
+ (#match? @constructor "^[A-Z]"))
+([(identifier) (shorthand_property_identifier)
+ (shorthand_property_identifier_pattern)] @constant
+ (#match? @constant "^[A-Z_][A-Z0-9_]+$"))
+
+(function_expression name: (identifier) @function)
+(function_declaration name: (identifier) @function)
+(method_definition name: (property_identifier) @function.method)
+(pair key: (property_identifier) @function.method
+ value: [(function_expression) (arrow_function)])
+(assignment_expression
+ left: (member_expression property: (property_identifier) @function.method)
+ right: [(function_expression) (arrow_function)])
+(variable_declarator name: (identifier) @function
+ value: [(function_expression) (arrow_function)])
+(assignment_expression left: (identifier) @function
+ right: [(function_expression) (arrow_function)])
+(call_expression function: (identifier) @function)
+(call_expression function: (member_expression
+ property: (property_identifier) @function.method))
+
+[(this) (super)] @variable.special
+(number) @number
+[(true) (false) (null) (undefined)] @constant
+[(string) (template_string)] @string
+(escape_sequence) @string.escape
+(regex) @string.regex
+(template_substitution "${" @punctuation.special "}" @punctuation.special)
+(comment) @comment
diff --git a/vrsite/assets/json.scm b/vrsite/assets/json.scm
new file mode 100644
index 0000000..e82e125
--- /dev/null
+++ b/vrsite/assets/json.scm
@@ -0,0 +1,13 @@
+; JSON highlights, from tree-sitter/tree-sitter-json v0.24.8
+; queries/highlights.scm (MIT). Reordered so object keys repaint the string
+; they are; capture names are already zed syntax keys but for @escape.
+
+["{" "}" "[" "]"] @punctuation.bracket
+[":" ","] @punctuation.delimiter
+
+(number) @number
+[(null) (true) (false)] @constant
+(string) @string
+(pair key: (string) @string.special.key)
+(escape_sequence) @string.escape
+(comment) @comment
diff --git a/vrsite/assets/jsx.scm b/vrsite/assets/jsx.scm
new file mode 100644
index 0000000..708eba2
--- /dev/null
+++ b/vrsite/assets/jsx.scm
@@ -0,0 +1,16 @@
+; JSX highlights, from tree-sitter/tree-sitter-javascript v0.25.0
+; queries/highlights-jsx.scm (MIT), verbatim but for the capture rename.
+; Loaded last for .jsx and .tsx; the plain typescript grammar has no JSX nodes,
+; so .ts must not get this file.
+
+(jsx_opening_element ["<" ">"] @punctuation.bracket)
+(jsx_closing_element ["</" ">"] @punctuation.bracket)
+(jsx_self_closing_element ["<" "/>"] @punctuation.bracket)
+
+(jsx_attribute (property_identifier) @attribute)
+
+; lowercase names are html elements, capitalized ones are components and keep
+; the @constructor javascript.scm gave them
+((jsx_opening_element (identifier) @tag) (#match? @tag "^[a-z][^.]*$"))
+((jsx_closing_element (identifier) @tag) (#match? @tag "^[a-z][^.]*$"))
+((jsx_self_closing_element (identifier) @tag) (#match? @tag "^[a-z][^.]*$"))
diff --git a/vrsite/assets/python.scm b/vrsite/assets/python.scm
new file mode 100644
index 0000000..f64e673
--- /dev/null
+++ b/vrsite/assets/python.scm
@@ -0,0 +1,50 @@
+; Python highlights, from tree-sitter/tree-sitter-python v0.25.0
+; queries/highlights.scm (MIT). Reordered for later-patterns-win, capture names
+; mapped onto zed syntax keys, and the builtin-function #match? turned into
+; #any-of?. The two casing heuristics stay regexes: nothing else can express
+; "CONST_CASE" and "ClassName" over bare identifiers.
+
+["(" ")" "[" "]" "{" "}"] @punctuation.bracket
+[";" "," "." ":"] @punctuation.delimiter
+["-" "-=" "!=" "*" "**" "**=" "*=" "/" "//" "//=" "/=" "&" "&=" "%" "%="
+ "^" "^=" "+" "->" "+=" "<" "<<" "<<=" "<=" "<>" "=" ":=" "==" ">" ">="
+ ">>" ">>=" "|" "|=" "~" "@=" "and" "in" "is" "not" "or" "is not"
+ "not in"] @operator
+
+["as" "assert" "async" "await" "break" "class" "continue" "def" "del" "elif"
+ "else" "except" "exec" "finally" "for" "from" "global" "if" "import" "lambda"
+ "nonlocal" "pass" "print" "raise" "return" "try" "while" "with" "yield"
+ "match" "case"] @keyword
+
+((identifier) @constructor
+ (#match? @constructor "^[A-Z]"))
+((identifier) @constant
+ (#match? @constant "^[A-Z][A-Z_0-9]*$"))
+((identifier) @variable.special
+ (#any-of? @variable.special "self" "cls"))
+
+(type (identifier) @type)
+(attribute attribute: (identifier) @property)
+(function_definition name: (identifier) @function)
+(call function: (identifier) @function)
+(call function: (attribute attribute: (identifier) @function.method))
+((call function: (identifier) @function.builtin)
+ (#any-of? @function.builtin "abs" "all" "any" "ascii" "bin" "bool"
+ "breakpoint" "bytearray" "bytes" "callable" "chr" "classmethod" "compile"
+ "complex" "delattr" "dict" "dir" "divmod" "enumerate" "eval" "exec" "filter"
+ "float" "format" "frozenset" "getattr" "globals" "hasattr" "hash" "help"
+ "hex" "id" "input" "int" "isinstance" "issubclass" "iter" "len" "list"
+ "locals" "map" "max" "memoryview" "min" "next" "object" "oct" "open" "ord"
+ "pow" "print" "property" "range" "repr" "reversed" "round" "set" "setattr"
+ "slice" "sorted" "staticmethod" "str" "sum" "super" "tuple" "type" "vars"
+ "zip" "__import__"))
+
+[(integer) (float)] @number
+[(none) (true) (false)] @constant
+(string) @string
+(escape_sequence) @string.escape
+(interpolation "{" @punctuation.special "}" @punctuation.special)
+
+(decorator "@" @attribute)
+(decorator [(identifier) (attribute)] @attribute)
+(comment) @comment
diff --git a/vrsite/assets/style.css.tmpl b/vrsite/assets/style.css.tmpl
index a808203..f57e454 100644
--- a/vrsite/assets/style.css.tmpl
+++ b/vrsite/assets/style.css.tmpl
@@ -73,7 +73,44 @@ nav > header {
}
nav > header a { color: var(--fg); }
nav .brand { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-nav .atrev { margin: 0 0 8px; }
+
+/* ── the current change ────────────────────────────────────────
+ Everything on screen is read at one change: the tree, the code, the
+ coverage, the notes, and anything you write. It says so, always. */
+.changebar {
+ margin-bottom: 10px; padding: 7px 8px;
+ border: 1px solid var(--border); border-left: 3px solid var(--accent);
+ border-radius: 6px; background: var(--bg);
+}
+.changebar.away { border-left-color: var(--note); }
+.changebar .cid { display: flex; align-items: baseline; gap: 6px; }
+.changebar .lbl { color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; }
+.changebar .cid code { font-size: 13px; font-weight: 600; color: var(--fg); }
+.changebar .cid a code { color: var(--accent); }
+.changebar .flags { color: var(--note); font-size: 10px; font-style: italic; margin-left: auto; }
+.changebar .desc {
+ margin: 3px 0 0; font-size: 11.5px; color: var(--muted);
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+}
+.changebar .counts { margin: 5px 0 0; font-size: 11px; color: var(--muted); }
+.changebar .counts b { color: var(--fg); font-weight: 600; }
+.changebar .away { color: var(--note); }
+.changebar .scope { display: flex; gap: 0; margin-top: 7px; border: 1px solid var(--border); border-radius: 5px; overflow: hidden; }
+.changebar .scope a {
+ flex: 1; text-align: center; padding: 2px 4px;
+ font-size: 11px; color: var(--muted); text-decoration: none;
+}
+.changebar .scope a:hover { background: var(--hover); color: var(--fg); }
+.changebar .scope a.on { background: var(--hover); color: var(--fg); font-weight: 600; }
+.changebar .revjump { margin-top: 6px; }
+.changebar .revjump input { width: 100%; font-family: ui-monospace, Menlo, monospace; font-size: 11px; }
+.changebar .back { margin: 6px 0 0; font-size: 11px; }
+
+/* a note written at another change: it pins to line numbers that have moved */
+.note.away { border-style: dashed; opacity: 0.82; }
+.note.away:hover { opacity: 1; }
+.atchg.away { color: var(--note); }
+.atchg.away code { color: inherit; }
.find, .filters input, .filters select, .notform input, .notform textarea, .jjform input {
background: var(--bg); color: var(--fg);
border: 1px solid var(--border); border-radius: 5px;
@@ -183,6 +220,8 @@ ol.code li {
white-space: pre;
min-height: 1lh;
padding-right: 16px;
+ /* a #L42 jump must clear the sticky header, or the line lands under it */
+ scroll-margin-top: 76px;
}
ol.code li::before {
content: counter(ln);
@@ -222,8 +261,62 @@ aside summary { cursor: pointer; color: var(--muted); }
.note footer { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; font-size: 11px; }
.note footer code { color: var(--muted); }
.note .model { opacity: 0.85; }
+.note .conv { margin-left: auto; }
.notecols { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 0 14px; }
+/* ── conversation ──────────────────────────────────────────── */
+.convpage { padding: 0 16px 40vh; max-width: 1100px; }
+.turn {
+ display: grid; grid-template-columns: 12ch 8ch minmax(0, 1fr);
+ gap: 0 10px; align-items: baseline;
+ padding: 2px 0 2px 8px; border-left: 2px solid transparent;
+}
+.turn .role {
+ color: var(--muted); font-size: 11px; text-align: right;
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+}
+.turn time { color: var(--ln); font-size: 11px; font-variant-numeric: tabular-nums; }
+.turn p { margin: 0; line-height: 1.5; white-space: pre-wrap; }
+.turn .what {
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+}
+.t-say { padding: 8px 0 8px 8px; }
+.t-user { border-left-color: var(--accent); }
+.t-user .role, .t-user p { color: var(--fg); }
+.t-assistant .role { color: var(--cov); }
+.t-thinking p { color: var(--muted); font-style: italic; }
+.t-tool .role { color: var(--note); }
+.t-result .what { color: var(--muted); }
+.t-result.bad .what { color: var(--note); }
+.t-vr { border-left-color: var(--cov); background: color-mix(in srgb, var(--cov) 7%, transparent); }
+.t-vr .role { color: var(--cov); font-weight: 600; }
+/* A note is what you came to the conversation to find, so it breaks the run
+ of log lines: the note color's rail, a tinted band, a card instead of a
+ line. scroll-margin clears the sticky header — the anchored one is
+ scrolled to on load. */
+.t-note {
+ padding: 8px 0 8px 8px;
+ border-left-color: var(--note);
+ background: color-mix(in srgb, var(--note) 7%, transparent);
+}
+.t-note .role { color: var(--note); font-weight: 600; }
+.t-note .role::before { content: "◆ "; font-size: 9px; vertical-align: 1px; }
+.t-note .note { max-width: 640px; margin: 0; scroll-margin-top: 96px; }
+.t-note.here { background: color-mix(in srgb, var(--accent) 12%, transparent); }
+.t-note.here .note { border-color: var(--accent); border-left-color: var(--accent); }
+/* the whole card is the way back to the code; the links inside still work */
+.notewrap { cursor: pointer; }
+.notewrap:hover .note, .notewrap:focus-visible .note {
+ border-color: var(--accent); background: var(--hover);
+}
+.notewrap:focus-visible { outline: none; }
+.notewrap[data-href] .note::after {
+ content: "open in source →"; display: block;
+ margin-top: 6px; color: var(--accent); font-size: 11px; opacity: 0;
+}
+.notewrap:hover .note::after, .notewrap:focus-visible .note::after { opacity: 1; }
+
/* ── live activity ─────────────────────────────────────────── */
.activity { margin-bottom: 10px; }
.actlist { display: flex; flex-direction: column; gap: 3px; }
@@ -279,9 +372,10 @@ aside summary { cursor: pointer; color: var(--muted); }
.asidehead { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
aside .meta { font-size: 11px; }
-/* selection while picking lines to comment on */
-ol.code li.sel { background: color-mix(in srgb, var(--accent) 16%, transparent); }
-ol.code li.sel::before { color: var(--accent); }
+/* lines picked to comment on, and lines you arrived at from a note */
+ol.code li.sel, ol.code li.focus { background: color-mix(in srgb, var(--accent) 16%, transparent); }
+ol.code li.sel::before, ol.code li.focus::before { color: var(--accent); }
+ol.code li.focus { box-shadow: inset 3px 0 var(--accent); }
/* ── jj ────────────────────────────────────────────────────── */
.jjform { display: flex; gap: 6px; align-items: center; padding: 10px 0; }
diff --git a/vrsite/assets/typescript.scm b/vrsite/assets/typescript.scm
new file mode 100644
index 0000000..96cf9c3
--- /dev/null
+++ b/vrsite/assets/typescript.scm
@@ -0,0 +1,15 @@
+; TypeScript highlights, from tree-sitter/tree-sitter-typescript v0.23.2
+; queries/highlights.scm (MIT). A supplement loaded after javascript.scm, as
+; upstream composes it, so these patterns win. Upstream's
+; ((identifier) @type (#match? "^[A-Z]")) is dropped: javascript.scm already
+; paints capitalized identifiers as @constructor, and repainting every one of
+; them as a type costs a second regex per identifier for a worse answer.
+
+["abstract" "declare" "enum" "implements" "interface" "keyof" "namespace"
+ "private" "protected" "public" "type" "readonly" "override"
+ "satisfies"] @keyword
+
+[(type_identifier) (predefined_type)] @type
+(type_arguments "<" @punctuation.bracket ">" @punctuation.bracket)
+(required_parameter (identifier) @variable.parameter)
+(optional_parameter (identifier) @variable.parameter)