vantage-md 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,211 @@
1
+ # Changelog
2
+
3
+ What changed in each release of Vantage, newest first.
4
+
5
+ Releases before 0.7.0 are summarized one section per minor line — `0.6.x`,
6
+ `0.5.x` — rather than one per patch release. The per-patch detail is in the
7
+ commit log.
8
+
9
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
10
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
11
+
12
+ ## [0.7.0] - 2026-09-23
13
+
14
+ Vantage now supports color themes and bookmarks, and reads per-project settings
15
+ from `.vantage.toml`.
16
+
17
+ ### Added
18
+
19
+ **Color themes.** Pick a palette under **Colors** in the settings menu. Six come
20
+ with Vantage: Catppuccin, Gruvbox, Lila, Nord, Solarized, and Tokyo Night, each
21
+ with its own light and dark variants, alongside the default look, now called
22
+ Slate.
23
+
24
+ To write your own, put a CSS file in `~/.config/vantage/themes/`. It shows up in
25
+ the menu on the next page load, with no restart. Set `theme = "gruvbox"` in your
26
+ config to start every browser in one. A project can suggest a theme in its
27
+ `.vantage.toml`, and your own choice always wins. A theme with no dark colors is
28
+ listed as "(light only)". Every built-in theme keeps its text at 3:1 contrast or
29
+ better in both modes, checked in a real browser; your own themes aren't checked.
30
+ Review mode's comment colors and printing still ignore the theme. See
31
+ [Color Themes](userguide/guides/themes.md).
32
+
33
+ **Bookmarks.** Click the star next to a document's name to bookmark it. Folders
34
+ work too. Bookmarks appear in a **Starred** section above the file tree. They're
35
+ stored in `~/.local/share/vantage/starred/` instead of in your browser, so they
36
+ survive a restart and show up in every tab. If a bookmarked file disappears, the
37
+ entry stays put and offers to remove itself when you open it.
38
+
39
+ A project can also list the documents worth reading first in its `.vantage.toml`,
40
+ and you can list files you always want starred in your own config. Those entries
41
+ are marked with a pin and name the file that added them. See
42
+ [Starred](userguide/features.md#starred).
43
+
44
+ **Open questions in the contents panel.** A document's open questions now appear
45
+ in the table of contents, under the heading they sit below, with a count next to
46
+ **Contents**. `💬 3` means three are waiting on a decision; `💬 1 ✅ 2` means one
47
+ is open and two are answered. Click one to jump to it. See
48
+ [Table of Contents](userguide/features.md#table-of-contents).
49
+
50
+ ### Changed
51
+
52
+ Settings now follow you between tabs. Before, a second tab read most settings
53
+ once when it loaded and then drifted, so changing the theme in one tab left the
54
+ other on the old one. `Shift+D` had the same problem inside a single tab: it
55
+ changed the page without updating the settings menu. The sidebar and its width,
56
+ the tree filters, the project sort order, light/dark, and the color theme all
57
+ stay in sync now.
58
+
59
+ Review mode is a deliberate exception. Two tabs are usually on different
60
+ documents, and picking up a toggle from another tab would close the review pane
61
+ while you're partway through a comment.
62
+
63
+ ### Fixed
64
+
65
+ - The file pickers (`t`, `Shift+T`, `Shift+R`, `Shift+P`) reload their list every
66
+ time they open, and a picker that's already open keeps up with the filesystem.
67
+ Before, a file created after the page loaded was unfindable until you
68
+ reloaded.
69
+ - A modified file's icon was too faint to read in the file tree in light mode.
70
+ - A document that fails to load no longer moves you off the one you're reading,
71
+ and the error names the file you actually asked for.
72
+ - A slow response for a document you've already navigated away from is thrown
73
+ away instead of replacing what you're reading.
74
+ - `vantage-check check --config <a directory>` now says
75
+ `docs is a directory, not a config file` and exits 2, instead of printing a
76
+ stack trace.
77
+
78
+ ### Contributors
79
+
80
+ Thanks to [@nichiflu](https://github.com/nichiflu) for the initial color-theme
81
+ implementation.
82
+
83
+ ### Works well with
84
+
85
+ [matt-craft](https://github.com/mschulkind-oss/matt-craft) is out — the skills I use to
86
+ write the documents I then read in Vantage: design notes that carry their open
87
+ questions, roadmaps, research rounds, user stories, and one for Vantage's own Markdown
88
+ conventions. It is not part of Vantage and Vantage does not need it. If you write your
89
+ own documents your own way, nothing here changes for you.
90
+
91
+ ## 0.6.x
92
+
93
+ _0.6.0 – 0.6.2, September 2026._
94
+
95
+ Long documents have a table of contents. The list button beside the breadcrumb
96
+ shows the document's headings in the left margin, and the list stays put while
97
+ you scroll. The button beside it lets the text fill the pane. The reading band
98
+ moved to the left edge too, so the file list, the contents, and the text sit
99
+ together.
100
+
101
+ - Serving a git repository no longer makes the browser reload itself once a
102
+ second. Vantage's own `git status` calls rewrote `.git/index`, and the watcher
103
+ read that as your edit.
104
+ - A repository cloned under one of your `source_dirs` shows up within thirty
105
+ seconds, with no restart. One whose directory goes away is dropped, then
106
+ picked back up when it returns. See
107
+ [Daemon Mode](userguide/guides/daemon-mode.md).
108
+ - A port you name is bound exactly or startup fails. Only the default 8000 walks
109
+ to the next free port, and it tells you which one it took.
110
+ - Images on adjacent lines render on one row, the way GitHub lays them out. A
111
+ README's badges used to stack into a column.
112
+ - `vantage-check` splits a run across worker threads by default, and the report
113
+ reads the same either way.
114
+
115
+ ### Contributors
116
+
117
+ Thanks to Eduardo Hidalgo ([@edus44](https://github.com/edus44)) for the table
118
+ of contents, the Vantage favicon, and fixes to port selection and spurious
119
+ browser reloads.
120
+
121
+ ## 0.5.x
122
+
123
+ _0.5.0 – 0.5.10, May to September 2026._
124
+
125
+ Vantage stopped being a Python server and became one Go binary. The viewer and
126
+ the shortcuts are the same; installing it is not. You install it with `brew
127
+ install`, `uvx vantage-md ~/notes`, or a platform archive from a release.
128
+ `vantage serve` opens your browser, and `--no-open` stops it.
129
+
130
+ - `vantage-check` is new. It runs your documents through the viewer's own
131
+ pipeline and reports dead links, dead anchors, broken mermaid diagrams, and
132
+ style problems. See [vantage-check](userguide/guides/vantage-check.md).
133
+ - You can mark a document up with `<!-- vantage: … -->` comments that GitHub
134
+ ignores and Vantage reads: a status chip beside a heading, collapsible
135
+ sections, emphasis, and badges. GitHub's alert blocks render too.
136
+ - Review mode was rebuilt. A comment anchors to a whole block, replies thread, a
137
+ rail shows where the comments are, and you can answer an open question in one
138
+ click. Agents reply through a `.vantage/` directory. See
139
+ [The `.vantage/` directory](userguide/guides/review-inbox.md).
140
+ - The "What's New" popup, the changed-document highlight, and the jj history
141
+ viewer were removed.
142
+ - `.vantageignore` and `~/.config/vantage/ignore` keep files out of the tree, and
143
+ the sidebar has a drag handle that remembers its width.
144
+
145
+ ## 0.4.x
146
+
147
+ _0.4.0 – 0.4.2, April 2026._
148
+
149
+ Installing Vantage got short. `uvx vantage-md ~/notes` started the server and
150
+ opened your browser at the directory you named, and naming a file landed on that
151
+ file with the sidebar already expanded. There was a Homebrew formula as well.
152
+ macOS became a platform Vantage was tested on.
153
+
154
+ - Review mode became per file. Turning it on for one document no longer followed
155
+ you to the next, and the toggle survived a reload even before you had written a
156
+ comment.
157
+ - Hovering any paragraph in review mode put a button in the gutter, so you could
158
+ comment on a whole block without selecting text first.
159
+ - You could edit a comment in place, from the document or from the sidebar.
160
+ Resolved comments collapsed into one indicator instead of asking to be
161
+ dismissed one at a time.
162
+ - A comment you had just written was sometimes marked "Outdated" immediately.
163
+ - `log_level` in the config, `VANTAGE_LOG_LEVEL` in the environment. The watcher
164
+ reported your inotify limits at startup and warned near the ceiling, which is
165
+ the usual reason live reload stops working.
166
+
167
+ ## 0.3.x
168
+
169
+ _0.3.0 – 0.3.8, April 2026._
170
+
171
+ This is the line that added review mode. You turned it on for a document,
172
+ selected some text, and left a comment. The server stored the comments, so they
173
+ survived a restart. When the text under a comment changed, the comment turned
174
+ amber and said "Outdated". Every save while review mode was on snapshotted the
175
+ previous version, so you could step back through revisions and see which
176
+ paragraphs had changed. One button copied every comment as Markdown quotes, with
177
+ line numbers and surrounding context, ready to paste to an agent.
178
+
179
+ - Opening a project no longer waited for its file tree. The document rendered
180
+ first and the sidebar filled in behind it.
181
+ - Headings got hover anchors, and `#L42` or `#L42-L50` in the URL scrolled to
182
+ those source lines and highlighted them.
183
+ - You could collapse the sidebar to give the document the window, and reveal
184
+ hidden or gitignored files with new toggles.
185
+ - TOML frontmatter between `+++` fences was parsed and shown, with taxonomies and
186
+ extra fields as tag pills.
187
+ - A red banner with a timer appeared when the page lost its connection, so a
188
+ document that had stopped live-reloading said so.
189
+
190
+ ## 0.2.x and earlier
191
+
192
+ _0.0.1 – 0.2.0, February to March 2026._
193
+
194
+ Where Vantage started. It served the Markdown in your git repositories over a
195
+ local port and reloaded the page when you saved: GitHub-flavored Markdown,
196
+ syntax highlighting, KaTeX math, mermaid diagrams you could click to zoom,
197
+ commit history and diffs, a fuzzy file picker on `t`, dark mode on `Shift+D`,
198
+ vim-style scrolling, and a print stylesheet. It could also export a static site
199
+ to host on Cloudflare Pages or S3. The server was written in Python then.
200
+
201
+ - `Shift+T` searched files across every project, `P` switched projects, `r` and
202
+ `Shift+R` found recently modified files, and `y` copied the current file's
203
+ absolute path.
204
+ - `source_dirs` in the config found every git repository under a directory, so
205
+ you did not have to list them one by one.
206
+ - The project picker became a full page, with repository names, relative
207
+ timestamps, and a toggle between alphabetical and recent order.
208
+ - A "What's New" popup showed the changelog the first time you opened a new
209
+ version. You could turn it off in settings. Vantage dropped it in 0.5.
210
+ - A jj history and evolution viewer sat beside the git one, for Jujutsu
211
+ repositories. That went in 0.5 too.
package/README.md CHANGED
@@ -76,7 +76,7 @@ The React component includes mermaid diagram rendering (lazy-loaded), frontmatte
76
76
  ### Your own processor, Vantage's chain
77
77
 
78
78
  `buildPipeline` returns the exact remark and rehype lists `renderMarkdown` and
79
- the React viewer use, in the exact order — including the sanitiser schema, and
79
+ the React viewer use, in the exact order — including the sanitizer schema, and
80
80
  `rehypeSlug` after it, which is what keeps generated heading ids free of
81
81
  `rehype-sanitize`'s `user-content-` prefix. Use it rather than assembling the
82
82
  chain yourself; that is how a document ends up rendering differently in two
@@ -118,7 +118,7 @@ if all you want is `data-source-line`.
118
118
 
119
119
  | Entry point | Description |
120
120
  |-------------|-------------|
121
- | `vantage-md` | `renderMarkdown`, `buildPipeline`, `buildRemarkPlugins`, `rehypeSourceLines`, `scrollToLineAnchor`, `parseLineAnchor`, `parseFrontmatter`, `readVantageFrontmatter`, `sanitizeSchema` |
121
+ | `vantage-md` | `renderMarkdown`, `buildPipeline`, `buildRemarkPlugins`, `rehypeSourceLines`, `scrollToLineAnchor`, `parseLineAnchor`, `parseFrontmatter`, `readVantageFrontmatter`, `sanitizeSchema`, `COLOR_THEME_ATTRIBUTE`, `COLOR_THEME_SOURCE_ATTRIBUTE`, `currentColorTheme` |
122
122
  | `vantage-md/react` | `MarkdownViewer`, `useLineAnchor`, `MermaidDiagram`, `FrontmatterDisplay`, `DocumentStatusChip` + all core exports |
123
123
  | `vantage-md/styles` | Line-anchor highlight CSS, plus the theme layer for the `data-vantage-*` directive attributes and the chrome chips (light + dark mode) |
124
124
 
@@ -132,6 +132,7 @@ if all you want is `data-source-line`.
132
132
  - **Frontmatter** — YAML (`---`) and TOML (`+++`) parsing. A reserved `vantage:` key carries file-scoped chrome: `status-chip: true` makes `FrontmatterDisplay` render the document's `status:` as a chip above the metadata card, and the reserved key itself is never shown as a metadata row
133
133
  - **Sanitization** — XSS-safe with allowlisted KaTeX/MathML elements
134
134
  - **Dark mode** — all styles support `.dark` class
135
+ - **Color themes** — `MermaidDiagram` follows a host page's color theme. While `<html>` carries `data-vantage-theme` (`COLOR_THEME_ATTRIBUTE`), diagram colors are read from the page's `--color-slate-*` variables instead of the built-in hex, and diagrams redraw when it or `data-vantage-theme-source` (`COLOR_THEME_SOURCE_ATTRIBUTE`: `built-in` or `user`, which tells a user theme apart from the built-in it replaces under the same id) changes. `currentColorTheme()` reads the first. A page that sets neither renders exactly as before
135
136
 
136
137
  ## License
137
138
 
package/dist/index.cjs CHANGED
@@ -116,14 +116,14 @@ const rehypeSourceLines = (options) => {
116
116
  * `remark-gfm` does not implement alerts, so until this plugin existed a
117
117
  * `> [!WARNING]` rendered as an ordinary blockquote with the literal marker
118
118
  * visible as its first words. Worse than merely unstyled: `@tailwindcss/typography`
119
- * italicises blockquotes and draws `open-quote`/`close-quote` around the first
119
+ * italicizes blockquotes and draws `open-quote`/`close-quote` around the first
120
120
  * paragraph, so a callout came out as an italic *quotation* whose opening words
121
121
  * were `"[!WARNING]`. That was the "Known gaps" entry in
122
122
  * `docs/reference/inline-markup.md` and OQ-10, filed rather than fixed, while
123
123
  * `styleGuide.ts` went on telling every agent to write them.
124
124
  *
125
125
  * The tokens are deliberately the ones the `tone` vocabulary already resolves —
126
- * an alert *is* the six-colour light/dark treatment `tone` shipped, which is
126
+ * an alert *is* the six-color light/dark treatment `tone` shipped, which is
127
127
  * exactly what the gap entry said whoever fixed this should do rather than
128
128
  * building a second palette. `[!WARNING]` and `<!-- vantage: block tone=warning -->`
129
129
  * therefore agree by construction, and adding a theme still touches one
@@ -137,7 +137,7 @@ const rehypeSourceLines = (options) => {
137
137
  *
138
138
  * ## What it does not do
139
139
  *
140
- * It does not touch a blockquote that carries no marker, and an unrecognised
140
+ * It does not touch a blockquote that carries no marker, and an unrecognized
141
141
  * marker (`[!HINT]`) is left exactly as it was — visible literal text, which is
142
142
  * the honest rendering of something GitHub also would not style. Silently
143
143
  * swallowing it would hide a typo that reads as a callout on neither renderer.
@@ -421,11 +421,63 @@ const VANTAGE_OQ_HOST_TARGETS = VANTAGE_ANCHOR_TARGETS.filter((tag) => tag !== "
421
421
  * file, and requiring it everywhere would fire on every single-doc sketch.
422
422
  *
423
423
  * Three consumers read it from here and none of them may re-spell it: the
424
- * plugin that stamps the anchor, the sanitiser that allowlists the value, and
424
+ * plugin that stamps the anchor, the sanitizer that allowlists the value, and
425
425
  * the checker's `vantage/oq-id-format`. A fourth copy is how the checker starts
426
426
  * calling a working anchor malformed.
427
427
  */
428
428
  const VANTAGE_OQ_ID = /^OQ-(?:[A-Z][A-Z0-9]{0,5})?[0-9]+$/;
429
+ /**
430
+ * The status emoji the documentation convention marks an Open Question with, and
431
+ * what each one means to a reader deciding whether the question wants them.
432
+ *
433
+ * These are *prose* — ordinary characters in the question's title, not part of
434
+ * any directive — which is exactly why they need a home that both readers of
435
+ * them can import. Two consumers ask what a marker means and they must not
436
+ * answer differently: the checker's `vantage/oq-missing`, which demands a
437
+ * directive on an open question and must never demand one on a blocked one, and
438
+ * the viewer's contents column, which shows the marker and needs a word for it
439
+ * that a screen reader can say. They were private constants in the checker until
440
+ * the second consumer arrived.
441
+ *
442
+ * `open` is the only state that wants a one-click answer. `settled` and
443
+ * `blocked` deliberately carry no directive at all — a control offering to
444
+ * answer a question that is already decided, or that cannot be answered yet, is
445
+ * a lie — so the checker keys on the distinction rather than on the word
446
+ * "Leaning:" alone. Either non-open marker wins when both appear on one item.
447
+ */
448
+ const VANTAGE_OQ_STATUS = {
449
+ /** 💬 — an active decision awaiting a ruling. */
450
+ open: "💬",
451
+ /** ✅ — decided, awaiting compaction into a Decision Ledger. */
452
+ settled: "✅",
453
+ /** 🔒 — blocked on an upstream decision or experiment. */
454
+ blocked: "🔒"
455
+ };
456
+ /**
457
+ * What a marker says, in words, for somewhere an emoji cannot go.
458
+ *
459
+ * An accessible name is the reason this exists: a contents entry whose whole
460
+ * status is one glyph says nothing to a screen reader, and "speech bubble" —
461
+ * which is what it would otherwise read out — is worse than nothing.
462
+ */
463
+ const VANTAGE_OQ_STATUS_LABEL = {
464
+ open: "Open question",
465
+ settled: "Answered question",
466
+ blocked: "Blocked question"
467
+ };
468
+ /**
469
+ * The state `text` is marked with, or `null` when it carries no marker.
470
+ *
471
+ * Non-open wins over open, the resolution `vantage/oq-missing` has always made:
472
+ * a question marked both 💬 and ✅ has been answered and the stale marker simply
473
+ * has not been cleared yet, so treating it as open would re-open a ruling.
474
+ */
475
+ function vantageOqStatus(text) {
476
+ if (text.includes(VANTAGE_OQ_STATUS.settled)) return "settled";
477
+ if (text.includes(VANTAGE_OQ_STATUS.blocked)) return "blocked";
478
+ if (text.includes(VANTAGE_OQ_STATUS.open)) return "open";
479
+ return null;
480
+ }
429
481
  const STYLE_KEYS = {
430
482
  tone: VANTAGE_TONES,
431
483
  emphasis: VANTAGE_EMPHASIS,
@@ -589,7 +641,7 @@ const HEADING_DEPTHS = /* @__PURE__ */ new Map([
589
641
  /**
590
642
  * Key → hast property, for the keys that treat a whole run.
591
643
  *
592
- * A camelCase hast property serialises to the kebab-case attribute, so
644
+ * A camelCase hast property serializes to the kebab-case attribute, so
593
645
  * `dataVantageTone` is `data-vantage-tone` in every renderer.
594
646
  *
595
647
  * `tone` and `emphasis` describe what a section *is* and how loud it is, so
@@ -624,11 +676,11 @@ const LEANING_PROPERTY = "dataVantageLeaning";
624
676
  * The id, carried as a `data-` attribute rather than written straight to `id`.
625
677
  *
626
678
  * This plugin runs *before* `rehypeSanitize` — it has to, it reads comments and
627
- * the sanitiser deletes them — and the sanitiser's default schema clobbers `id`
679
+ * the sanitizer deletes them — and the sanitizer's default schema clobbers `id`
628
680
  * with the prefix `user-content-`. A bare `id` set here would reach the page as
629
681
  * `user-content-OQ-4`, every `#OQ-4` link in every document would land nowhere,
630
682
  * and nothing would error. `rehypeVantageAnchors` promotes this to a real `id`
631
- * on the other side of the sanitiser, which is the same reason `rehypeSlug` is
683
+ * on the other side of the sanitizer, which is the same reason `rehypeSlug` is
632
684
  * registered there (`pipeline.ts`).
633
685
  */
634
686
  const OQ_ID_PROPERTY = "dataVantageOqId";
@@ -660,7 +712,7 @@ const MAX_LEANING = 500;
660
712
  * A whitespace-only `text` node always does — measured, with or without a blank
661
713
  * line in the source. Comments do too, and an unrelated `<!-- TODO -->` must not
662
714
  * break the chain: it is invisible in every renderer and deleted by the
663
- * sanitiser, so letting it change a directive's meaning would make behaviour
715
+ * sanitizer, so letting it change a directive's meaning would make behavior
664
716
  * depend on something no reader can see.
665
717
  */
666
718
  function isSkippable(node) {
@@ -712,7 +764,7 @@ function accepts(name, key, value) {
712
764
  * two stamped paragraphs — and the section's one continuous vertical rule is
713
765
  * drawn per member, so an unstamped member is a hole the height of the block
714
766
  * plus its margins. Measured over the real stylesheet: 44px for a one-line
715
- * `<figure>`, against the 40px a neighbour can bleed upward, and arbitrarily
767
+ * `<figure>`, against the 40px a neighbor can bleed upward, and arbitrarily
716
768
  * large for anything taller. `collapsed=true` had the same shape of bug the
717
769
  * other way round — it hid the paragraphs and left the figure on the page.
718
770
  *
@@ -894,7 +946,7 @@ function classNames(node) {
894
946
  * A `<pre>` `rehype-katex` will replace — its own condition, restated.
895
947
  *
896
948
  * `language-math` is the only class to test: `rehype-katex` keys the
897
- * pre-as-scope branch on it, and the sanitiser strips the `math-display` that
949
+ * pre-as-scope branch on it, and the sanitizer strips the `math-display` that
898
950
  * `remark-math` also emits (measured — a stamped fence arrives here with
899
951
  * `className: ["language-math"]` alone).
900
952
  */
@@ -1048,7 +1100,7 @@ const COLLAPSE_GROUP_ID = /^[0-9]+$/;
1048
1100
  * `false` — comments are not elements, so `tagNames` has nothing to do with it.
1049
1101
  * `rehypeVantageDirectives` relies on that deletion: it consumes a
1050
1102
  * `<!-- vantage: … -->` comment into attributes and deliberately leaves the node
1051
- * for the sanitiser. Turning the switch on readmits every directive comment —
1103
+ * for the sanitizer. Turning the switch on readmits every directive comment —
1052
1104
  * valid and malformed alike — into the rendered HTML, which breaks the carrier's
1053
1105
  * whole premise. `vantageDirectives.test.ts` ("leaves no comment in the rendered
1054
1106
  * markup") is the guard.
@@ -1319,7 +1371,7 @@ async function renderMarkdown(content, options = {}) {
1319
1371
  *
1320
1372
  * Split out from scrollToLineAnchor.ts so that non-browser consumers — the
1321
1373
  * `vantage-check` CLI, which validates `#L42` links against the file on disk —
1322
- * can share the *same* syntax the viewer honours instead of reimplementing it
1374
+ * can share the *same* syntax the viewer honors instead of reimplementing it
1323
1375
  * and drifting.
1324
1376
  */
1325
1377
  /**
@@ -1436,7 +1488,7 @@ const DOC_STATUSES = [
1436
1488
  /** Every key this build knows under `vantage:`. Closed. */
1437
1489
  const VANTAGE_FRONTMATTER_KEYS = ["status-chip"];
1438
1490
  /**
1439
- * Which tone each status borrows its colours from.
1491
+ * Which tone each status borrows its colors from.
1440
1492
  *
1441
1493
  * The chip has no palette of its own: it reuses the tone chips
1442
1494
  * (`.vantage-chip--<tone>` in `styles/directives.css`), which is also what makes
@@ -1543,6 +1595,49 @@ function currentMermaidTheme() {
1543
1595
  return typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "default";
1544
1596
  }
1545
1597
  /**
1598
+ * The attribute on `<html>` naming the active color theme. Absent means the
1599
+ * built-in look. The app sets it only once the theme's stylesheet has loaded,
1600
+ * so a reader of this attribute can trust the theme's variables are in effect.
1601
+ */
1602
+ const COLOR_THEME_ATTRIBUTE = "data-vantage-theme";
1603
+ /**
1604
+ * The attribute on `<html>` saying where the active theme came from: `"user"`
1605
+ * for a stylesheet in the reader's themes directory, `"built-in"` for one the
1606
+ * app ships. Set with {@link COLOR_THEME_ATTRIBUTE}, and absent with it.
1607
+ *
1608
+ * It exists because an id alone is not a palette. A user theme may share a
1609
+ * built-in's id — that is how a reader tweaks one — and the app applies the
1610
+ * stored built-in synchronously, then swaps in the same-id user file once
1611
+ * /api/themes answers. Keyed on the id, the diagrams drawn in between kept the
1612
+ * built-in's colors: the key did not change, so neither the cache nor
1613
+ * `useSyncExternalStore` saw a reason to redraw.
1614
+ */
1615
+ const COLOR_THEME_SOURCE_ATTRIBUTE = "data-vantage-theme-source";
1616
+ /** The active color theme's id, or `""` for the built-in look. */
1617
+ function currentColorTheme() {
1618
+ if (typeof document === "undefined") return "";
1619
+ return document.documentElement.getAttribute("data-vantage-theme") ?? "";
1620
+ }
1621
+ /**
1622
+ * Everything a rendered diagram's colors depend on, as one string: the
1623
+ * light/dark mode, plus the color theme when one is active.
1624
+ *
1625
+ * A diagram is baked at render time, so anything that changes its colors has
1626
+ * to change this key — it is what the SVG cache and the loader's "configured
1627
+ * for" check compare. Under the built-in look it is exactly the mode name,
1628
+ * which is what both keyed on before color themes existed.
1629
+ */
1630
+ function currentMermaidPalette() {
1631
+ const theme = currentColorTheme();
1632
+ if (!theme) return currentMermaidTheme();
1633
+ const user = document.documentElement.getAttribute(COLOR_THEME_SOURCE_ATTRIBUTE) === "user";
1634
+ return `${currentMermaidTheme()} ${theme}${user ? " user" : ""}`;
1635
+ }
1636
+ /** The mermaid theme name a palette key was built from. */
1637
+ function mermaidThemeOf(palette) {
1638
+ return palette.startsWith("dark") ? "dark" : "default";
1639
+ }
1640
+ /**
1546
1641
  * Theme variables per theme. Mermaid derives most of its palette from these, so
1547
1642
  * the set is deliberately small: the surfaces, the ink, and the lines.
1548
1643
  */
@@ -1566,27 +1661,108 @@ const THEME_VARIABLES = {
1566
1661
  edgeLabelBackground: "#f8fafc"
1567
1662
  }
1568
1663
  };
1664
+ /**
1665
+ * Where each variable comes from under a color theme: the palette step the
1666
+ * built-in value was chosen from. The hex above IS that step in Tailwind's own
1667
+ * palette, so reading the step back out of the page gives the same diagram
1668
+ * under the built-in look and the theme's colors under any other.
1669
+ */
1670
+ const THEME_SOURCES = {
1671
+ dark: {
1672
+ background: "--color-slate-800",
1673
+ mainBkg: "--color-slate-700",
1674
+ nodeBorder: "--color-slate-400",
1675
+ nodeTextColor: "--color-slate-100",
1676
+ lineColor: "--color-slate-400",
1677
+ textColor: "--color-slate-200",
1678
+ edgeLabelBackground: "--color-slate-800"
1679
+ },
1680
+ default: {
1681
+ background: "--color-slate-50",
1682
+ mainBkg: "--color-slate-100",
1683
+ nodeBorder: "--color-slate-500",
1684
+ nodeTextColor: "--color-slate-900",
1685
+ lineColor: "--color-slate-500",
1686
+ textColor: "--color-slate-800",
1687
+ edgeLabelBackground: "--color-slate-50"
1688
+ }
1689
+ };
1569
1690
  function mermaidThemeVariables(theme) {
1570
- return THEME_VARIABLES[theme];
1691
+ const fixed = THEME_VARIABLES[theme];
1692
+ if (!currentColorTheme()) return fixed;
1693
+ const out = {};
1694
+ for (const [key, hex] of Object.entries(fixed)) out[key] = resolveCssColor(THEME_SOURCES[theme][key]) ?? hex;
1695
+ return out;
1696
+ }
1697
+ /**
1698
+ * A custom property's color as `#rrggbb`, or `null` when it cannot be read.
1699
+ *
1700
+ * Mermaid wants hex at `initialize()` time, and a theme's value can be any CSS
1701
+ * color — `oklch()`, `color-mix()`, a `var()` of another variable. So the
1702
+ * browser does the work: a probe element resolves the property to a computed
1703
+ * color, and a 1×1 canvas turns that into sRGB bytes whatever syntax it came
1704
+ * back in. No canvas (jsdom, a locked-down embed) is `null`, and the caller
1705
+ * falls back to the built-in value.
1706
+ *
1707
+ * A property nobody declared is `null` too, and has to be checked for up
1708
+ * front: `color: var(--unset)` is invalid at computed-value time, so the probe
1709
+ * *inherits* its color instead — the page's text color, which the canvas
1710
+ * would dutifully turn into a plausible hex and mermaid would paint every node
1711
+ * box with.
1712
+ */
1713
+ function resolveCssColor(property) {
1714
+ if (typeof document === "undefined") return null;
1715
+ if (!getComputedStyle(document.documentElement).getPropertyValue(property).trim()) return null;
1716
+ const probe = document.createElement("span");
1717
+ probe.style.display = "none";
1718
+ probe.style.color = `var(${property})`;
1719
+ document.documentElement.appendChild(probe);
1720
+ const computed = getComputedStyle(probe).color;
1721
+ probe.remove();
1722
+ if (!computed) return null;
1723
+ const ctx = pixelContext();
1724
+ if (!ctx) return null;
1725
+ ctx.fillStyle = computed;
1726
+ ctx.fillRect(0, 0, 1, 1);
1727
+ const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
1728
+ return `#${[
1729
+ r,
1730
+ g,
1731
+ b
1732
+ ].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
1733
+ }
1734
+ /** A 1×1 2D context to read a color back from, or `null` where there is none. */
1735
+ function pixelContext() {
1736
+ try {
1737
+ const canvas = document.createElement("canvas");
1738
+ canvas.width = canvas.height = 1;
1739
+ return canvas.getContext("2d", { willReadFrequently: true });
1740
+ } catch {
1741
+ return null;
1742
+ }
1571
1743
  }
1572
1744
  //#endregion
1573
1745
  //#region src/mermaidCache.ts
1574
1746
  const svgCache = /* @__PURE__ */ new Map();
1575
- const cacheKey = (code, theme) => `${theme}${code}`;
1747
+ const cacheKey = (code, theme) => `${theme}\u0000${code}`;
1576
1748
  /** The SVG for this fence in the theme the page is currently asking for. */
1577
- function getCachedSvg(code, theme = currentMermaidTheme()) {
1749
+ function getCachedSvg(code, theme = currentMermaidPalette()) {
1578
1750
  return svgCache.get(cacheKey(code, theme));
1579
1751
  }
1580
- function setCachedSvg(code, svg, theme = currentMermaidTheme()) {
1752
+ function setCachedSvg(code, svg, theme = currentMermaidPalette()) {
1581
1753
  svgCache.set(cacheKey(code, theme), svg);
1582
1754
  }
1583
1755
  //#endregion
1584
1756
  //#region src/mermaidLoader.ts
1585
1757
  let mermaidInstance = null;
1586
1758
  let mermaidLoading = null;
1587
- /** The theme the loaded instance was last configured for, `null` until loaded. */
1759
+ /**
1760
+ * The palette key (`currentMermaidPalette`) the loaded instance was last
1761
+ * configured for, `null` until loaded.
1762
+ */
1588
1763
  let configuredTheme = null;
1589
- function configure(m, theme) {
1764
+ function configure(m, palette) {
1765
+ const theme = mermaidThemeOf(palette);
1590
1766
  m.initialize({
1591
1767
  startOnLoad: false,
1592
1768
  theme,
@@ -1594,7 +1770,7 @@ function configure(m, theme) {
1594
1770
  securityLevel: "strict",
1595
1771
  suppressErrorRendering: true
1596
1772
  });
1597
- configuredTheme = theme;
1773
+ configuredTheme = palette;
1598
1774
  }
1599
1775
  /**
1600
1776
  * The mermaid module, configured for the theme the page is asking for *now*.
@@ -1608,19 +1784,19 @@ function configure(m, theme) {
1608
1784
  * served instead (`mermaidCache.ts`).
1609
1785
  */
1610
1786
  async function getMermaid() {
1611
- const theme = currentMermaidTheme();
1787
+ const theme = currentMermaidPalette();
1612
1788
  if (mermaidInstance) {
1613
1789
  if (configuredTheme !== theme) configure(mermaidInstance, theme);
1614
1790
  return mermaidInstance;
1615
1791
  }
1616
1792
  if (!mermaidLoading) mermaidLoading = import("mermaid").then((mod) => {
1617
1793
  const m = mod.default;
1618
- configure(m, currentMermaidTheme());
1794
+ configure(m, currentMermaidPalette());
1619
1795
  mermaidInstance = m;
1620
1796
  return m;
1621
1797
  });
1622
1798
  const loaded = await mermaidLoading;
1623
- const wanted = currentMermaidTheme();
1799
+ const wanted = currentMermaidPalette();
1624
1800
  if (configuredTheme !== wanted) configure(loaded, wanted);
1625
1801
  return loaded;
1626
1802
  }
@@ -1822,7 +1998,7 @@ vantage:
1822
1998
  status-chip: true # show \`status\` as a chip above the metadata card
1823
1999
  ---
1824
2000
  \`\`\`
1825
- - **Nothing may sit above the opening delimiter** — not a blank line, not an editorial comment, not a \`<!-- vantage: … -->\` directive. Frontmatter is recognised only at the very first byte of the file (in Vantage, on GitHub, and in every other reader), so one line above it turns the whole block into body text: a horizontal rule followed by a heading made of the raw keys, with every field lost. \`vantage-check\` reports it as \`frontmatter/not-at-top\`.
2001
+ - **Nothing may sit above the opening delimiter** — not a blank line, not an editorial comment, not a \`<!-- vantage: … -->\` directive. Frontmatter is recognized only at the very first byte of the file (in Vantage, on GitHub, and in every other reader), so one line above it turns the whole block into body text: a horizontal rule followed by a heading made of the raw keys, with every field lost. \`vantage-check\` reports it as \`frontmatter/not-at-top\`.
1826
2002
  - **\`vantage:\` is Vantage's own reserved key.** It holds chrome that belongs to the file rather than to a section, it never shows up in the metadata card, and every other renderer ignores it. One key today: \`status-chip\`.
1827
2003
  - **Prefer \`status-chip: true\`**, which shows the document's own \`status:\` and therefore cannot disagree with it. A literal \`status-chip: accepted\` is accepted too, but it is a second value that goes stale on its own — \`vantage-check\` reports the disagreement.
1828
2004
  - The chip's vocabulary is \`status\`'s, exactly: \`draft | in-review | accepted | deprecated\`, lowercase. \`Draft\` renders no chip at all, silently.
@@ -1874,7 +2050,7 @@ The steps below predate the rewrite.
1874
2050
  \`\`\`
1875
2051
 
1876
2052
  - **Three names**: \`section\` (the heading and everything under it), \`block\` (the one block after it), \`oq\` (one answerable Open Question).
1877
- - **The keys and values are a closed set**: \`tone\` = \`note | tip | important | warning | caution | muted\`; \`emphasis\` = \`strong | normal | quiet\`; \`badge\` = \`draft | stale | blocked | done | wip\`; \`collapsed\` = \`true | false\`. Name a *tone*, never a colour — the theme decides what a warning looks like, in light mode, in dark mode, and in print.
2053
+ - **The keys and values are a closed set**: \`tone\` = \`note | tip | important | warning | caution | muted\`; \`emphasis\` = \`strong | normal | quiet\`; \`badge\` = \`draft | stale | blocked | done | wip\`; \`collapsed\` = \`true | false\`. Name a *tone*, never a color — the theme decides what a warning looks like, in light mode, in dark mode, and in print.
1878
2054
  - **Use them sparingly.** One or two per document, on the sections that genuinely differ. A document where everything is toned says nothing, and a rainbow one is harder to read than a plain one.
1879
2055
  - **Anything outside those sets is silently ignored** — nothing breaks, and nothing styles either. Run \`vantage-check\` on the document: the \`vantage/*\` rules are the only thing that will ever tell you a directive did nothing.
1880
2056
  - **Always close the comment with \`-->\`.** Never \`--!>\`, and never leave it open: Markdown reads every line below an unclosed \`<!--\` as part of the comment, and the whole rest of the document vanishes from the page. For the same reason \`-->\` cannot appear *inside* a value — it ends the comment early and spills the remainder into the page as literal text.
@@ -1900,6 +2076,8 @@ The steps below predate the rewrite.
1900
2076
  `;
1901
2077
  //#endregion
1902
2078
  exports.ALERT_TITLES = ALERT_TITLES;
2079
+ exports.COLOR_THEME_ATTRIBUTE = COLOR_THEME_ATTRIBUTE;
2080
+ exports.COLOR_THEME_SOURCE_ATTRIBUTE = COLOR_THEME_SOURCE_ATTRIBUTE;
1903
2081
  exports.DIRECTIVE_NAMES = DIRECTIVE_NAMES;
1904
2082
  exports.DIRECTIVE_VOCABULARY = DIRECTIVE_VOCABULARY;
1905
2083
  exports.DOC_STATUSES = DOC_STATUSES;
@@ -1912,12 +2090,15 @@ exports.VANTAGE_COLLAPSED = VANTAGE_COLLAPSED;
1912
2090
  exports.VANTAGE_EMPHASIS = VANTAGE_EMPHASIS;
1913
2091
  exports.VANTAGE_FRONTMATTER_KEYS = VANTAGE_FRONTMATTER_KEYS;
1914
2092
  exports.VANTAGE_OQ_HOST_TARGETS = VANTAGE_OQ_HOST_TARGETS;
2093
+ exports.VANTAGE_OQ_STATUS = VANTAGE_OQ_STATUS;
2094
+ exports.VANTAGE_OQ_STATUS_LABEL = VANTAGE_OQ_STATUS_LABEL;
1915
2095
  exports.VANTAGE_RUNS = VANTAGE_RUNS;
1916
2096
  exports.VANTAGE_SENTINEL = VANTAGE_SENTINEL;
1917
2097
  exports.VANTAGE_TONES = VANTAGE_TONES;
1918
2098
  exports.buildPipeline = buildPipeline;
1919
2099
  exports.buildRemarkPlugins = buildRemarkPlugins;
1920
2100
  exports.clearLineAnchorHighlights = clearLineAnchorHighlights;
2101
+ exports.currentColorTheme = currentColorTheme;
1921
2102
  exports.hasVantageSentinel = hasVantageSentinel;
1922
2103
  exports.isDocStatus = isDocStatus;
1923
2104
  exports.parseFrontmatter = parseFrontmatter;
@@ -1933,5 +2114,6 @@ exports.renderMermaidBlocks = renderMermaidBlocks;
1933
2114
  exports.resolveLinks = resolveLinks;
1934
2115
  exports.sanitizeSchema = sanitizeSchema;
1935
2116
  exports.scrollToLineAnchor = scrollToLineAnchor;
2117
+ exports.vantageOqStatus = vantageOqStatus;
1936
2118
 
1937
2119
  //# sourceMappingURL=index.cjs.map