diff --git a/CLAUDE.md b/CLAUDE.md index 8196ed4e..646bd6ec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ file below is imported into context automatically. nothing. - **The offline guarantee, the renderer/main split and the crypto invariants are non-negotiable.** See the hard security rules. - - **A UI change is checked against all 14 themes** before it is proposed, + - **A UI change is checked against all 20 themes** before it is proposed, not after it is built. - prose / overly verbose comments must be removed / trimmed before stating feature is done. Code must be self describing. diff --git a/README.md b/README.md index 5a461934..696a65ca 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ Builds are **unsigned**, so SmartScreen and Gatekeeper warn on first launch (the | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Compare** | Two files or pasted text, split or inline, word-level highlights, in-view search, and a live re-diff when a file changes on disk. | | **Understand structure** | JSON, YAML and XML compared as _data_: reordering keys or reformatting stops counting, and unchanged keys collapse away. | -| **Excel** | `.xlsx` workbooks as aligned grids — sheet tabs and cell-level highlights, with inserted rows and columns that don't cascade into false changes. Dates read as dates, hidden sheets and rows are marked, and formulas are compared as well as their results, so a total pasted over the formula behind it is caught rather than shown as unchanged. Headers are found under a title row rather than assumed to be row 1, and rows can be paired by the columns that name them — one column or several — so the same export sorted differently reads as the one figure that moved instead of as a rewrite. Set a tolerance — one of the presets or a threshold of your own, percentage or raw — and rounding noise stops counting; export the whole change list as a CSV. | +| **Excel** | `.xlsx` workbooks as aligned grids — sheet tabs and cell-level highlights, with inserted rows and columns that don't cascade into false changes. Dates read as dates, hidden sheets and rows are marked, and formulas are compared as well as their results, so a total pasted over the formula behind it is caught rather than shown as unchanged. Headers are found under a title row rather than assumed to be row 1, and rows can be paired by the columns that name them — one column or several — so the same export sorted differently reads as the one figure that moved instead of as a rewrite. Set a tolerance — one of the presets or a threshold of your own, percentage or raw — and rounding noise stops counting; export the whole change list as a CSV. | | **CSV** | `.csv` and `.tsv` compare as text or, one toggle away, as the same grid — rows aligned by their first column, quoted fields kept whole. | -| **Dependencies** | A lockfile pair reads as the dependency moves it describes, not as the four thousand lines it is written in: which packages were added, removed, bumped or downgraded, the semver step of each, and — the part that matters — which of them you actually asked for rather than got carried along. `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `go.sum` and `composer.lock`. Nothing is fetched; every fact comes out of the file in front of you. | +| **Dependencies** | A lockfile pair reads as the dependency moves it describes, not as the four thousand lines it is written in: which packages were added, removed, bumped or downgraded, the semver step of each, and — the part that matters — which of them you actually asked for rather than got carried along. `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `go.sum` and `composer.lock`. Nothing is fetched; every fact comes out of the file in front of you. | | **Huge files** | Past 32 MB a file is indexed by line instead of loaded, and the rows you're looking at are read from disk as you scroll — a multi-gigabyte log opens in seconds. Marked as streamed, with the few actions that need the whole text saying so rather than half-working. | | **Keep** | Saved diffs: encrypted, tagged, optionally auto-expiring. Drag a row onto another to arrange the list yourself; starred rows stay above the rest. Your open tabs come back on the next launch, and the strip can be told to close the oldest comparison to make room for a new one. | | **Share** | One signed file only the recipients you ticked can open, carrying the expiry you chose so every copy dies at the same moment. Give a trusted key an email address and Diff Bro opens an addressed message in your own mail app with the sealed file on the clipboard — it never sends anything itself. The key swap rides the same rails: email your key from the My key dialog, and a key copied out of any chat app is offered — fingerprint first — when you press + Trusted key. | @@ -62,9 +62,9 @@ Builds are **unsigned**, so SmartScreen and Gatekeeper warn on first launch (the | **Guided first run** | Six coach marks over the real controls on a first launch — comparing, sealing, the library, then the way into Settings and around it — with four more if you want them. Each step points at a control and its button performs the action, so nothing opens unannounced. Back revisits a step, and everything it put on screen — the demo files, the example snippet — leaves when it does. Escape or Skip ends it for good; Help ▸ Show Tour brings it back. | | **Diagrams** | Two Mermaid files compare as a picture, not as text — one diagram carrying both revisions, so an inserted node reads as one change instead of a rewrite. | | **Tools** | JSON, Base64, UUID, JWT, Epoch, URL, Lines, XML, checksums, a regex tester, find & replace, text encryption — rich panels, not blank text boxes. All of them live in their own sidebar section; star the ones you reach for and they stay at the top. | -| **Merge conflicts** | Registered as git's `difftool` **and** `mergetool`: `git mergetool` opens the two conflicting versions and takes a choice per conflict — ours, theirs, both or neither — then writes the merged file back and tells git it is done. This is the one file Diff Bro writes over; everything else it produces is a new file you picked the place for. | -| **Terminal** | `diffbro compare a.json b.json` opens a comparison in the running app, and either side can name a git revision instead of a file — `diffbro compare HEAD~1:src/app.js src/app.js` reads the old copy straight out of the repository, so you never have to produce one first. `diffbro open` raises the app, `diffbro backup ` writes an encrypted archive. No port, no daemon. | -| **Yours to arrange** | Fourteen themes (Nord, Sepia, Solar, Nyan, Matrix, plus accessibility-grade Contrast and Beacon), shared tags, adjustable limits. | +| **Merge conflicts** | Registered as git's `difftool` **and** `mergetool`: `git mergetool` opens a real three-way view — the two branches either side, the file you are producing in the middle, and that middle one is a full editor. Each side is labelled with the branch it came from and read out of git's index, so no `<<<<<<<` ever reaches the screen; the chevrons in the gutters move a side across, F7 walks the conflicts, and where neither side is right you just type the answer. Thirty conflicted files are thirty stops, counted in the header. It writes the merged file back and tells git it is done — the one file Diff Bro writes over; everything else it produces is a new file you picked the place for. | +| **Terminal** | `diffbro compare a.json b.json` opens a comparison in the running app, and either side can name a git revision instead of a file — `diffbro compare HEAD~1:src/app.js src/app.js` reads the old copy straight out of the repository, so you never have to produce one first. `diffbro open` raises the app, `diffbro backup ` writes an encrypted archive. No port, no daemon. | +| **Yours to arrange** | Twenty themes (Nord, Sepia, Solar, Nyan, Matrix, Volcano, Tide, Graphite, plus accessibility-grade Contrast and Beacon), shared tags, adjustable limits. |
The smaller things @@ -112,7 +112,7 @@ flowchart LR The same JSON diff in the light theme — floating cards on a tinted ground -

Fourteen themes, GitHub-style rendering.

+

Twenty themes, GitHub-style rendering.

diff --git a/docs/brand/roadmap.svg b/docs/brand/roadmap.svg index 2dafede9..a185c5aa 100644 --- a/docs/brand/roadmap.svg +++ b/docs/brand/roadmap.svg @@ -5,7 +5,7 @@ sidebar. Track hues are real theme accents from src/renderer/src/utils/themes.js (Dim, Bloom, Neon, Beacon) — colour depth encodes sequence: solid now, faded later. Keep this in step with the items in docs/roadmap.md. --> - + @@ -117,7 +117,7 @@ Compare a git revision - Resolve a merge + Three-way merge view utils/lockfile/ diff --git a/docs/roadmap.md b/docs/roadmap.md index cd258e7b..c0d5023f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,7 +1,7 @@ # Roadmap Roadmap board — five tracks. Developer workflow, shipped: dependencies as lockfiles, compare a git revision, resolve a merge. Spreadsheet · finance: amounts read as amounts, delta and net variance, reading a big diff, caps that announce themselves. Diagrams: sequence · gantt · pie, click a change to pan to it. Comparing more: folder compare, image pairs, three-way merge — a decision first. Signing: macOS Developer ID, Windows deferred. + alt="Roadmap board — five tracks. Developer workflow, shipped: dependencies as lockfiles, compare a git revision, a three-way merge view. Spreadsheet · finance: amounts read as amounts, delta and net variance, reading a big diff, caps that announce themselves. Diagrams: sequence · gantt · pie, click a change to pan to it. Comparing more: folder compare, image pairs — a decision first. Signing: macOS Developer ID, Windows deferred."> Board is `docs/brand/roadmap.svg` — hand-authored, edit it alongside the sections below. @@ -171,7 +171,7 @@ flowchart LR extractor. They keep the text diff and the toggle stays hidden - **the register is read-only** — a row names a change you then hunt for by eye; clicking one should pan the diagram to it -- Still out of scope: editing a diagram from the diff view, three-way merge +- Still out of scope: editing a diagram from the diff view --- @@ -192,10 +192,11 @@ flowchart LR c["compare HEAD~1:path"] g --> c end - subgraph merge["merge"] - m["mergeConflicts.js — regions + four resolutions"] + subgraph merge["three-way merge"] + m["index :1: :2: :3: — no markers on screen"] + v["MergeView — ours │ result │ theirs, middle editable"] w["mergeSession.js — the one write"] - m --> w + m --> v --> w end ``` @@ -204,10 +205,14 @@ flowchart LR - **Revisions** — `diffbro compare HEAD~1:src/app.js src/app.js`. `git show` behind a fence: fixed argv, no shell, the repo root computed in main, hooks and the fsmonitor disabled, every inherited `GIT_*` dropped -- **Merge** — `git mergetool` now finishes. This CROSSES "Diff Bro never writes - files", deliberately: the app had already registered for the job. Main writes - only the `$MERGED` path it was launched with, the renderer sends text and - never a path, and the launcher waits so `trustExitCode` is honest +- **Merge** — `git mergetool` now finishes, in a three-pane view: the two + branches either side, named by branch, and the file you are producing in the + middle as a real editor. Sides come from the index, so no `<<<<<<<` reaches + the screen; gutter chevrons move a side across, F7 walks the conflicts, and + typing IS the answer where neither side was right. This CROSSES "Diff Bro + never writes files", deliberately: the app had already registered for the job. + Main writes only the `$MERGED` path it was launched with, the renderer sends + text and never a path, and the launcher waits so `trustExitCode` is honest **Open.** TOML lockfiles (`Cargo.lock`, `poetry.lock`) need a parser this repo does not have. A revision PICKER — the app takes a revision, it is not a git diff --git a/docs/screenshots/themes/deps-diff-amber.png b/docs/screenshots/themes/deps-diff-amber.png new file mode 100644 index 00000000..50f9c6da Binary files /dev/null and b/docs/screenshots/themes/deps-diff-amber.png differ diff --git a/docs/screenshots/themes/deps-diff-beacon.png b/docs/screenshots/themes/deps-diff-beacon.png new file mode 100644 index 00000000..bbde8bce Binary files /dev/null and b/docs/screenshots/themes/deps-diff-beacon.png differ diff --git a/docs/screenshots/themes/deps-diff-bloom.png b/docs/screenshots/themes/deps-diff-bloom.png new file mode 100644 index 00000000..6d44bf5b Binary files /dev/null and b/docs/screenshots/themes/deps-diff-bloom.png differ diff --git a/docs/screenshots/themes/deps-diff-contrast.png b/docs/screenshots/themes/deps-diff-contrast.png new file mode 100644 index 00000000..5910b5ea Binary files /dev/null and b/docs/screenshots/themes/deps-diff-contrast.png differ diff --git a/docs/screenshots/themes/deps-diff-dark.png b/docs/screenshots/themes/deps-diff-dark.png new file mode 100644 index 00000000..ec8cc9a0 Binary files /dev/null and b/docs/screenshots/themes/deps-diff-dark.png differ diff --git a/docs/screenshots/themes/deps-diff-dim.png b/docs/screenshots/themes/deps-diff-dim.png new file mode 100644 index 00000000..62f2d7bb Binary files /dev/null and b/docs/screenshots/themes/deps-diff-dim.png differ diff --git a/docs/screenshots/themes/deps-diff-ember.png b/docs/screenshots/themes/deps-diff-ember.png new file mode 100644 index 00000000..03a319c1 Binary files /dev/null and b/docs/screenshots/themes/deps-diff-ember.png differ diff --git a/docs/screenshots/themes/deps-diff-graphite.png b/docs/screenshots/themes/deps-diff-graphite.png new file mode 100644 index 00000000..4b41b32a Binary files /dev/null and b/docs/screenshots/themes/deps-diff-graphite.png differ diff --git a/docs/screenshots/themes/deps-diff-light.png b/docs/screenshots/themes/deps-diff-light.png new file mode 100644 index 00000000..e4d5eec8 Binary files /dev/null and b/docs/screenshots/themes/deps-diff-light.png differ diff --git a/docs/screenshots/themes/deps-diff-linen.png b/docs/screenshots/themes/deps-diff-linen.png new file mode 100644 index 00000000..797db45d Binary files /dev/null and b/docs/screenshots/themes/deps-diff-linen.png differ diff --git a/docs/screenshots/themes/deps-diff-matrix.png b/docs/screenshots/themes/deps-diff-matrix.png new file mode 100644 index 00000000..4a53ab37 Binary files /dev/null and b/docs/screenshots/themes/deps-diff-matrix.png differ diff --git a/docs/screenshots/themes/deps-diff-meridian.png b/docs/screenshots/themes/deps-diff-meridian.png new file mode 100644 index 00000000..621048c4 Binary files /dev/null and b/docs/screenshots/themes/deps-diff-meridian.png differ diff --git a/docs/screenshots/themes/deps-diff-neon.png b/docs/screenshots/themes/deps-diff-neon.png new file mode 100644 index 00000000..ab3de85b Binary files /dev/null and b/docs/screenshots/themes/deps-diff-neon.png differ diff --git a/docs/screenshots/themes/deps-diff-nord.png b/docs/screenshots/themes/deps-diff-nord.png new file mode 100644 index 00000000..847ad273 Binary files /dev/null and b/docs/screenshots/themes/deps-diff-nord.png differ diff --git a/docs/screenshots/themes/deps-diff-nyan.png b/docs/screenshots/themes/deps-diff-nyan.png new file mode 100644 index 00000000..d8ed139d Binary files /dev/null and b/docs/screenshots/themes/deps-diff-nyan.png differ diff --git a/docs/screenshots/themes/deps-diff-sepia.png b/docs/screenshots/themes/deps-diff-sepia.png new file mode 100644 index 00000000..b9e512c7 Binary files /dev/null and b/docs/screenshots/themes/deps-diff-sepia.png differ diff --git a/docs/screenshots/themes/deps-diff-solar.png b/docs/screenshots/themes/deps-diff-solar.png new file mode 100644 index 00000000..e0145783 Binary files /dev/null and b/docs/screenshots/themes/deps-diff-solar.png differ diff --git a/docs/screenshots/themes/deps-diff-tide.png b/docs/screenshots/themes/deps-diff-tide.png new file mode 100644 index 00000000..64283568 Binary files /dev/null and b/docs/screenshots/themes/deps-diff-tide.png differ diff --git a/docs/screenshots/themes/deps-diff-vector.png b/docs/screenshots/themes/deps-diff-vector.png new file mode 100644 index 00000000..9d64b3da Binary files /dev/null and b/docs/screenshots/themes/deps-diff-vector.png differ diff --git a/docs/screenshots/themes/deps-diff-volcano.png b/docs/screenshots/themes/deps-diff-volcano.png new file mode 100644 index 00000000..7bb7167c Binary files /dev/null and b/docs/screenshots/themes/deps-diff-volcano.png differ diff --git a/docs/screenshots/themes/launcher-compose-amber.png b/docs/screenshots/themes/launcher-compose-amber.png new file mode 100644 index 00000000..ee58b453 Binary files /dev/null and b/docs/screenshots/themes/launcher-compose-amber.png differ diff --git a/docs/screenshots/themes/launcher-compose-beacon.png b/docs/screenshots/themes/launcher-compose-beacon.png index ad411ec0..af5de639 100644 Binary files a/docs/screenshots/themes/launcher-compose-beacon.png and b/docs/screenshots/themes/launcher-compose-beacon.png differ diff --git a/docs/screenshots/themes/launcher-compose-bloom.png b/docs/screenshots/themes/launcher-compose-bloom.png index ad411ec0..9379fc3e 100644 Binary files a/docs/screenshots/themes/launcher-compose-bloom.png and b/docs/screenshots/themes/launcher-compose-bloom.png differ diff --git a/docs/screenshots/themes/launcher-compose-contrast.png b/docs/screenshots/themes/launcher-compose-contrast.png index ad411ec0..40c33e46 100644 Binary files a/docs/screenshots/themes/launcher-compose-contrast.png and b/docs/screenshots/themes/launcher-compose-contrast.png differ diff --git a/docs/screenshots/themes/launcher-compose-dark.png b/docs/screenshots/themes/launcher-compose-dark.png index ad411ec0..a10fb60f 100644 Binary files a/docs/screenshots/themes/launcher-compose-dark.png and b/docs/screenshots/themes/launcher-compose-dark.png differ diff --git a/docs/screenshots/themes/launcher-compose-dim.png b/docs/screenshots/themes/launcher-compose-dim.png index ad411ec0..b6474a13 100644 Binary files a/docs/screenshots/themes/launcher-compose-dim.png and b/docs/screenshots/themes/launcher-compose-dim.png differ diff --git a/docs/screenshots/themes/launcher-compose-ember.png b/docs/screenshots/themes/launcher-compose-ember.png new file mode 100644 index 00000000..8e884da9 Binary files /dev/null and b/docs/screenshots/themes/launcher-compose-ember.png differ diff --git a/docs/screenshots/themes/launcher-compose-graphite.png b/docs/screenshots/themes/launcher-compose-graphite.png new file mode 100644 index 00000000..6fe64e55 Binary files /dev/null and b/docs/screenshots/themes/launcher-compose-graphite.png differ diff --git a/docs/screenshots/themes/launcher-compose-light.png b/docs/screenshots/themes/launcher-compose-light.png index ad411ec0..cd904ec8 100644 Binary files a/docs/screenshots/themes/launcher-compose-light.png and b/docs/screenshots/themes/launcher-compose-light.png differ diff --git a/docs/screenshots/themes/launcher-compose-linen.png b/docs/screenshots/themes/launcher-compose-linen.png index ad411ec0..b4af44d0 100644 Binary files a/docs/screenshots/themes/launcher-compose-linen.png and b/docs/screenshots/themes/launcher-compose-linen.png differ diff --git a/docs/screenshots/themes/launcher-compose-matrix.png b/docs/screenshots/themes/launcher-compose-matrix.png index ad411ec0..a1cb2cbf 100644 Binary files a/docs/screenshots/themes/launcher-compose-matrix.png and b/docs/screenshots/themes/launcher-compose-matrix.png differ diff --git a/docs/screenshots/themes/launcher-compose-meridian.png b/docs/screenshots/themes/launcher-compose-meridian.png index ad411ec0..f113dbaf 100644 Binary files a/docs/screenshots/themes/launcher-compose-meridian.png and b/docs/screenshots/themes/launcher-compose-meridian.png differ diff --git a/docs/screenshots/themes/launcher-compose-neon.png b/docs/screenshots/themes/launcher-compose-neon.png index ad411ec0..ce30c063 100644 Binary files a/docs/screenshots/themes/launcher-compose-neon.png and b/docs/screenshots/themes/launcher-compose-neon.png differ diff --git a/docs/screenshots/themes/launcher-compose-nord.png b/docs/screenshots/themes/launcher-compose-nord.png index ad411ec0..1b95e9cb 100644 Binary files a/docs/screenshots/themes/launcher-compose-nord.png and b/docs/screenshots/themes/launcher-compose-nord.png differ diff --git a/docs/screenshots/themes/launcher-compose-nyan.png b/docs/screenshots/themes/launcher-compose-nyan.png index ad411ec0..e13482dc 100644 Binary files a/docs/screenshots/themes/launcher-compose-nyan.png and b/docs/screenshots/themes/launcher-compose-nyan.png differ diff --git a/docs/screenshots/themes/launcher-compose-sepia.png b/docs/screenshots/themes/launcher-compose-sepia.png index ad411ec0..e5c7f949 100644 Binary files a/docs/screenshots/themes/launcher-compose-sepia.png and b/docs/screenshots/themes/launcher-compose-sepia.png differ diff --git a/docs/screenshots/themes/launcher-compose-solar.png b/docs/screenshots/themes/launcher-compose-solar.png index ad411ec0..4ca634ff 100644 Binary files a/docs/screenshots/themes/launcher-compose-solar.png and b/docs/screenshots/themes/launcher-compose-solar.png differ diff --git a/docs/screenshots/themes/launcher-compose-tide.png b/docs/screenshots/themes/launcher-compose-tide.png new file mode 100644 index 00000000..34e70de9 Binary files /dev/null and b/docs/screenshots/themes/launcher-compose-tide.png differ diff --git a/docs/screenshots/themes/launcher-compose-vector.png b/docs/screenshots/themes/launcher-compose-vector.png new file mode 100644 index 00000000..53124057 Binary files /dev/null and b/docs/screenshots/themes/launcher-compose-vector.png differ diff --git a/docs/screenshots/themes/launcher-compose-volcano.png b/docs/screenshots/themes/launcher-compose-volcano.png new file mode 100644 index 00000000..850e948d Binary files /dev/null and b/docs/screenshots/themes/launcher-compose-volcano.png differ diff --git a/docs/screenshots/themes/merge-view-amber.png b/docs/screenshots/themes/merge-view-amber.png new file mode 100644 index 00000000..c4a5b07d Binary files /dev/null and b/docs/screenshots/themes/merge-view-amber.png differ diff --git a/docs/screenshots/themes/merge-view-beacon.png b/docs/screenshots/themes/merge-view-beacon.png new file mode 100644 index 00000000..0d728be5 Binary files /dev/null and b/docs/screenshots/themes/merge-view-beacon.png differ diff --git a/docs/screenshots/themes/merge-view-bloom.png b/docs/screenshots/themes/merge-view-bloom.png new file mode 100644 index 00000000..3b2b33e1 Binary files /dev/null and b/docs/screenshots/themes/merge-view-bloom.png differ diff --git a/docs/screenshots/themes/merge-view-contrast.png b/docs/screenshots/themes/merge-view-contrast.png new file mode 100644 index 00000000..14533f0a Binary files /dev/null and b/docs/screenshots/themes/merge-view-contrast.png differ diff --git a/docs/screenshots/themes/merge-view-dark.png b/docs/screenshots/themes/merge-view-dark.png new file mode 100644 index 00000000..fb2ea4b4 Binary files /dev/null and b/docs/screenshots/themes/merge-view-dark.png differ diff --git a/docs/screenshots/themes/merge-view-dim.png b/docs/screenshots/themes/merge-view-dim.png new file mode 100644 index 00000000..ac74e86c Binary files /dev/null and b/docs/screenshots/themes/merge-view-dim.png differ diff --git a/docs/screenshots/themes/merge-view-ember.png b/docs/screenshots/themes/merge-view-ember.png new file mode 100644 index 00000000..66a803ce Binary files /dev/null and b/docs/screenshots/themes/merge-view-ember.png differ diff --git a/docs/screenshots/themes/merge-view-graphite.png b/docs/screenshots/themes/merge-view-graphite.png new file mode 100644 index 00000000..c02329de Binary files /dev/null and b/docs/screenshots/themes/merge-view-graphite.png differ diff --git a/docs/screenshots/themes/merge-view-light.png b/docs/screenshots/themes/merge-view-light.png new file mode 100644 index 00000000..431ac27b Binary files /dev/null and b/docs/screenshots/themes/merge-view-light.png differ diff --git a/docs/screenshots/themes/merge-view-linen.png b/docs/screenshots/themes/merge-view-linen.png new file mode 100644 index 00000000..f9f28f80 Binary files /dev/null and b/docs/screenshots/themes/merge-view-linen.png differ diff --git a/docs/screenshots/themes/merge-view-matrix.png b/docs/screenshots/themes/merge-view-matrix.png new file mode 100644 index 00000000..49945c2a Binary files /dev/null and b/docs/screenshots/themes/merge-view-matrix.png differ diff --git a/docs/screenshots/themes/merge-view-meridian.png b/docs/screenshots/themes/merge-view-meridian.png new file mode 100644 index 00000000..b2e012b4 Binary files /dev/null and b/docs/screenshots/themes/merge-view-meridian.png differ diff --git a/docs/screenshots/themes/merge-view-neon.png b/docs/screenshots/themes/merge-view-neon.png new file mode 100644 index 00000000..ef83e9ee Binary files /dev/null and b/docs/screenshots/themes/merge-view-neon.png differ diff --git a/docs/screenshots/themes/merge-view-nord.png b/docs/screenshots/themes/merge-view-nord.png new file mode 100644 index 00000000..6ef61682 Binary files /dev/null and b/docs/screenshots/themes/merge-view-nord.png differ diff --git a/docs/screenshots/themes/merge-view-nyan.png b/docs/screenshots/themes/merge-view-nyan.png new file mode 100644 index 00000000..6b623f0a Binary files /dev/null and b/docs/screenshots/themes/merge-view-nyan.png differ diff --git a/docs/screenshots/themes/merge-view-sepia.png b/docs/screenshots/themes/merge-view-sepia.png new file mode 100644 index 00000000..3aab3c16 Binary files /dev/null and b/docs/screenshots/themes/merge-view-sepia.png differ diff --git a/docs/screenshots/themes/merge-view-solar.png b/docs/screenshots/themes/merge-view-solar.png new file mode 100644 index 00000000..8d98842c Binary files /dev/null and b/docs/screenshots/themes/merge-view-solar.png differ diff --git a/docs/screenshots/themes/merge-view-tide.png b/docs/screenshots/themes/merge-view-tide.png new file mode 100644 index 00000000..2cf21036 Binary files /dev/null and b/docs/screenshots/themes/merge-view-tide.png differ diff --git a/docs/screenshots/themes/merge-view-vector.png b/docs/screenshots/themes/merge-view-vector.png new file mode 100644 index 00000000..f50971c7 Binary files /dev/null and b/docs/screenshots/themes/merge-view-vector.png differ diff --git a/docs/screenshots/themes/merge-view-volcano.png b/docs/screenshots/themes/merge-view-volcano.png new file mode 100644 index 00000000..ca992e67 Binary files /dev/null and b/docs/screenshots/themes/merge-view-volcano.png differ diff --git a/docs/screenshots/themes/new-row-amber.png b/docs/screenshots/themes/new-row-amber.png new file mode 100644 index 00000000..3db7a342 Binary files /dev/null and b/docs/screenshots/themes/new-row-amber.png differ diff --git a/docs/screenshots/themes/new-row-beacon.png b/docs/screenshots/themes/new-row-beacon.png index 432e401a..234bcaa1 100644 Binary files a/docs/screenshots/themes/new-row-beacon.png and b/docs/screenshots/themes/new-row-beacon.png differ diff --git a/docs/screenshots/themes/new-row-bloom.png b/docs/screenshots/themes/new-row-bloom.png index a3fdfcd5..59bcc85e 100644 Binary files a/docs/screenshots/themes/new-row-bloom.png and b/docs/screenshots/themes/new-row-bloom.png differ diff --git a/docs/screenshots/themes/new-row-contrast.png b/docs/screenshots/themes/new-row-contrast.png index afad2792..dfb934dd 100644 Binary files a/docs/screenshots/themes/new-row-contrast.png and b/docs/screenshots/themes/new-row-contrast.png differ diff --git a/docs/screenshots/themes/new-row-dark.png b/docs/screenshots/themes/new-row-dark.png index da49c17b..b47b319e 100644 Binary files a/docs/screenshots/themes/new-row-dark.png and b/docs/screenshots/themes/new-row-dark.png differ diff --git a/docs/screenshots/themes/new-row-dim.png b/docs/screenshots/themes/new-row-dim.png index 215e8564..10ab70fe 100644 Binary files a/docs/screenshots/themes/new-row-dim.png and b/docs/screenshots/themes/new-row-dim.png differ diff --git a/docs/screenshots/themes/new-row-ember.png b/docs/screenshots/themes/new-row-ember.png new file mode 100644 index 00000000..fea449eb Binary files /dev/null and b/docs/screenshots/themes/new-row-ember.png differ diff --git a/docs/screenshots/themes/new-row-graphite.png b/docs/screenshots/themes/new-row-graphite.png new file mode 100644 index 00000000..d0b1d695 Binary files /dev/null and b/docs/screenshots/themes/new-row-graphite.png differ diff --git a/docs/screenshots/themes/new-row-light.png b/docs/screenshots/themes/new-row-light.png index 66fcef5b..d0bc0d0f 100644 Binary files a/docs/screenshots/themes/new-row-light.png and b/docs/screenshots/themes/new-row-light.png differ diff --git a/docs/screenshots/themes/new-row-linen.png b/docs/screenshots/themes/new-row-linen.png index 7f279541..bf559b43 100644 Binary files a/docs/screenshots/themes/new-row-linen.png and b/docs/screenshots/themes/new-row-linen.png differ diff --git a/docs/screenshots/themes/new-row-matrix.png b/docs/screenshots/themes/new-row-matrix.png index 66cfac5e..2d5d07bd 100644 Binary files a/docs/screenshots/themes/new-row-matrix.png and b/docs/screenshots/themes/new-row-matrix.png differ diff --git a/docs/screenshots/themes/new-row-meridian.png b/docs/screenshots/themes/new-row-meridian.png index b1b39c16..f3277d52 100644 Binary files a/docs/screenshots/themes/new-row-meridian.png and b/docs/screenshots/themes/new-row-meridian.png differ diff --git a/docs/screenshots/themes/new-row-neon.png b/docs/screenshots/themes/new-row-neon.png index 61246440..8ef66793 100644 Binary files a/docs/screenshots/themes/new-row-neon.png and b/docs/screenshots/themes/new-row-neon.png differ diff --git a/docs/screenshots/themes/new-row-nord.png b/docs/screenshots/themes/new-row-nord.png index 12cb4201..b6ceca58 100644 Binary files a/docs/screenshots/themes/new-row-nord.png and b/docs/screenshots/themes/new-row-nord.png differ diff --git a/docs/screenshots/themes/new-row-nyan.png b/docs/screenshots/themes/new-row-nyan.png index bd80cce0..a27c719a 100644 Binary files a/docs/screenshots/themes/new-row-nyan.png and b/docs/screenshots/themes/new-row-nyan.png differ diff --git a/docs/screenshots/themes/new-row-sepia.png b/docs/screenshots/themes/new-row-sepia.png index ee81b5ee..7cae1e0c 100644 Binary files a/docs/screenshots/themes/new-row-sepia.png and b/docs/screenshots/themes/new-row-sepia.png differ diff --git a/docs/screenshots/themes/new-row-solar.png b/docs/screenshots/themes/new-row-solar.png index aaa94fd4..7e419956 100644 Binary files a/docs/screenshots/themes/new-row-solar.png and b/docs/screenshots/themes/new-row-solar.png differ diff --git a/docs/screenshots/themes/new-row-tide.png b/docs/screenshots/themes/new-row-tide.png new file mode 100644 index 00000000..9f1e636f Binary files /dev/null and b/docs/screenshots/themes/new-row-tide.png differ diff --git a/docs/screenshots/themes/new-row-vector.png b/docs/screenshots/themes/new-row-vector.png new file mode 100644 index 00000000..ea9c67a8 Binary files /dev/null and b/docs/screenshots/themes/new-row-vector.png differ diff --git a/docs/screenshots/themes/new-row-volcano.png b/docs/screenshots/themes/new-row-volcano.png new file mode 100644 index 00000000..e0373780 Binary files /dev/null and b/docs/screenshots/themes/new-row-volcano.png differ diff --git a/docs/screenshots/themes/settings-appearance-amber.png b/docs/screenshots/themes/settings-appearance-amber.png new file mode 100644 index 00000000..e58c6819 Binary files /dev/null and b/docs/screenshots/themes/settings-appearance-amber.png differ diff --git a/docs/screenshots/themes/settings-appearance-beacon.png b/docs/screenshots/themes/settings-appearance-beacon.png index 290b8a88..5c71cbf0 100644 Binary files a/docs/screenshots/themes/settings-appearance-beacon.png and b/docs/screenshots/themes/settings-appearance-beacon.png differ diff --git a/docs/screenshots/themes/settings-appearance-bloom.png b/docs/screenshots/themes/settings-appearance-bloom.png index c2898cc1..45726856 100644 Binary files a/docs/screenshots/themes/settings-appearance-bloom.png and b/docs/screenshots/themes/settings-appearance-bloom.png differ diff --git a/docs/screenshots/themes/settings-appearance-contrast.png b/docs/screenshots/themes/settings-appearance-contrast.png index 9e087113..5cad1f7f 100644 Binary files a/docs/screenshots/themes/settings-appearance-contrast.png and b/docs/screenshots/themes/settings-appearance-contrast.png differ diff --git a/docs/screenshots/themes/settings-appearance-dark.png b/docs/screenshots/themes/settings-appearance-dark.png index e79235c6..50d94170 100644 Binary files a/docs/screenshots/themes/settings-appearance-dark.png and b/docs/screenshots/themes/settings-appearance-dark.png differ diff --git a/docs/screenshots/themes/settings-appearance-dim.png b/docs/screenshots/themes/settings-appearance-dim.png index 0561ecac..a2db4538 100644 Binary files a/docs/screenshots/themes/settings-appearance-dim.png and b/docs/screenshots/themes/settings-appearance-dim.png differ diff --git a/docs/screenshots/themes/settings-appearance-ember.png b/docs/screenshots/themes/settings-appearance-ember.png new file mode 100644 index 00000000..f43d3e79 Binary files /dev/null and b/docs/screenshots/themes/settings-appearance-ember.png differ diff --git a/docs/screenshots/themes/settings-appearance-graphite.png b/docs/screenshots/themes/settings-appearance-graphite.png new file mode 100644 index 00000000..5212f2b3 Binary files /dev/null and b/docs/screenshots/themes/settings-appearance-graphite.png differ diff --git a/docs/screenshots/themes/settings-appearance-light.png b/docs/screenshots/themes/settings-appearance-light.png index e629804d..99fd2edd 100644 Binary files a/docs/screenshots/themes/settings-appearance-light.png and b/docs/screenshots/themes/settings-appearance-light.png differ diff --git a/docs/screenshots/themes/settings-appearance-linen.png b/docs/screenshots/themes/settings-appearance-linen.png index 1a130977..75d35d9e 100644 Binary files a/docs/screenshots/themes/settings-appearance-linen.png and b/docs/screenshots/themes/settings-appearance-linen.png differ diff --git a/docs/screenshots/themes/settings-appearance-matrix.png b/docs/screenshots/themes/settings-appearance-matrix.png index c4d0ef72..8589b5f3 100644 Binary files a/docs/screenshots/themes/settings-appearance-matrix.png and b/docs/screenshots/themes/settings-appearance-matrix.png differ diff --git a/docs/screenshots/themes/settings-appearance-meridian.png b/docs/screenshots/themes/settings-appearance-meridian.png index a74a4d3e..56cfb9ab 100644 Binary files a/docs/screenshots/themes/settings-appearance-meridian.png and b/docs/screenshots/themes/settings-appearance-meridian.png differ diff --git a/docs/screenshots/themes/settings-appearance-neon.png b/docs/screenshots/themes/settings-appearance-neon.png index 02173191..fb6ac9cf 100644 Binary files a/docs/screenshots/themes/settings-appearance-neon.png and b/docs/screenshots/themes/settings-appearance-neon.png differ diff --git a/docs/screenshots/themes/settings-appearance-nord.png b/docs/screenshots/themes/settings-appearance-nord.png index 4a326c7e..4a574eb5 100644 Binary files a/docs/screenshots/themes/settings-appearance-nord.png and b/docs/screenshots/themes/settings-appearance-nord.png differ diff --git a/docs/screenshots/themes/settings-appearance-nyan.png b/docs/screenshots/themes/settings-appearance-nyan.png index 5cc7c75b..47860357 100644 Binary files a/docs/screenshots/themes/settings-appearance-nyan.png and b/docs/screenshots/themes/settings-appearance-nyan.png differ diff --git a/docs/screenshots/themes/settings-appearance-sepia.png b/docs/screenshots/themes/settings-appearance-sepia.png index bcf61e0b..c39ac2c1 100644 Binary files a/docs/screenshots/themes/settings-appearance-sepia.png and b/docs/screenshots/themes/settings-appearance-sepia.png differ diff --git a/docs/screenshots/themes/settings-appearance-solar.png b/docs/screenshots/themes/settings-appearance-solar.png index b1de43b1..79247012 100644 Binary files a/docs/screenshots/themes/settings-appearance-solar.png and b/docs/screenshots/themes/settings-appearance-solar.png differ diff --git a/docs/screenshots/themes/settings-appearance-tide.png b/docs/screenshots/themes/settings-appearance-tide.png new file mode 100644 index 00000000..10bf4d4f Binary files /dev/null and b/docs/screenshots/themes/settings-appearance-tide.png differ diff --git a/docs/screenshots/themes/settings-appearance-vector.png b/docs/screenshots/themes/settings-appearance-vector.png new file mode 100644 index 00000000..ae0a8867 Binary files /dev/null and b/docs/screenshots/themes/settings-appearance-vector.png differ diff --git a/docs/screenshots/themes/settings-appearance-volcano.png b/docs/screenshots/themes/settings-appearance-volcano.png new file mode 100644 index 00000000..f78a61dc Binary files /dev/null and b/docs/screenshots/themes/settings-appearance-volcano.png differ diff --git a/docs/screenshots/themes/settings-email-amber.png b/docs/screenshots/themes/settings-email-amber.png new file mode 100644 index 00000000..20ae2024 Binary files /dev/null and b/docs/screenshots/themes/settings-email-amber.png differ diff --git a/docs/screenshots/themes/settings-email-beacon.png b/docs/screenshots/themes/settings-email-beacon.png index 87486860..59ba9c3b 100644 Binary files a/docs/screenshots/themes/settings-email-beacon.png and b/docs/screenshots/themes/settings-email-beacon.png differ diff --git a/docs/screenshots/themes/settings-email-bloom.png b/docs/screenshots/themes/settings-email-bloom.png index d72924bb..d4be1b05 100644 Binary files a/docs/screenshots/themes/settings-email-bloom.png and b/docs/screenshots/themes/settings-email-bloom.png differ diff --git a/docs/screenshots/themes/settings-email-contrast.png b/docs/screenshots/themes/settings-email-contrast.png index 3a2351d4..061b50b2 100644 Binary files a/docs/screenshots/themes/settings-email-contrast.png and b/docs/screenshots/themes/settings-email-contrast.png differ diff --git a/docs/screenshots/themes/settings-email-dark.png b/docs/screenshots/themes/settings-email-dark.png index 3d0b68b1..95db286d 100644 Binary files a/docs/screenshots/themes/settings-email-dark.png and b/docs/screenshots/themes/settings-email-dark.png differ diff --git a/docs/screenshots/themes/settings-email-dim.png b/docs/screenshots/themes/settings-email-dim.png index 23949568..909abc76 100644 Binary files a/docs/screenshots/themes/settings-email-dim.png and b/docs/screenshots/themes/settings-email-dim.png differ diff --git a/docs/screenshots/themes/settings-email-ember.png b/docs/screenshots/themes/settings-email-ember.png new file mode 100644 index 00000000..12b3755d Binary files /dev/null and b/docs/screenshots/themes/settings-email-ember.png differ diff --git a/docs/screenshots/themes/settings-email-graphite.png b/docs/screenshots/themes/settings-email-graphite.png new file mode 100644 index 00000000..a089de95 Binary files /dev/null and b/docs/screenshots/themes/settings-email-graphite.png differ diff --git a/docs/screenshots/themes/settings-email-light.png b/docs/screenshots/themes/settings-email-light.png index 9c3ceb23..9c8fe0b1 100644 Binary files a/docs/screenshots/themes/settings-email-light.png and b/docs/screenshots/themes/settings-email-light.png differ diff --git a/docs/screenshots/themes/settings-email-linen.png b/docs/screenshots/themes/settings-email-linen.png index 29bed608..9e1386c6 100644 Binary files a/docs/screenshots/themes/settings-email-linen.png and b/docs/screenshots/themes/settings-email-linen.png differ diff --git a/docs/screenshots/themes/settings-email-matrix.png b/docs/screenshots/themes/settings-email-matrix.png index 49b402db..c70d5c8a 100644 Binary files a/docs/screenshots/themes/settings-email-matrix.png and b/docs/screenshots/themes/settings-email-matrix.png differ diff --git a/docs/screenshots/themes/settings-email-meridian.png b/docs/screenshots/themes/settings-email-meridian.png index 636f3bc3..2511f2da 100644 Binary files a/docs/screenshots/themes/settings-email-meridian.png and b/docs/screenshots/themes/settings-email-meridian.png differ diff --git a/docs/screenshots/themes/settings-email-neon.png b/docs/screenshots/themes/settings-email-neon.png index 16476956..cab06656 100644 Binary files a/docs/screenshots/themes/settings-email-neon.png and b/docs/screenshots/themes/settings-email-neon.png differ diff --git a/docs/screenshots/themes/settings-email-nord.png b/docs/screenshots/themes/settings-email-nord.png index 1b9e7523..333848ab 100644 Binary files a/docs/screenshots/themes/settings-email-nord.png and b/docs/screenshots/themes/settings-email-nord.png differ diff --git a/docs/screenshots/themes/settings-email-nyan.png b/docs/screenshots/themes/settings-email-nyan.png index 32e8a575..6e428b4d 100644 Binary files a/docs/screenshots/themes/settings-email-nyan.png and b/docs/screenshots/themes/settings-email-nyan.png differ diff --git a/docs/screenshots/themes/settings-email-sepia.png b/docs/screenshots/themes/settings-email-sepia.png index ca86745f..26195f08 100644 Binary files a/docs/screenshots/themes/settings-email-sepia.png and b/docs/screenshots/themes/settings-email-sepia.png differ diff --git a/docs/screenshots/themes/settings-email-solar.png b/docs/screenshots/themes/settings-email-solar.png index 16a65c3f..809d8f92 100644 Binary files a/docs/screenshots/themes/settings-email-solar.png and b/docs/screenshots/themes/settings-email-solar.png differ diff --git a/docs/screenshots/themes/settings-email-tide.png b/docs/screenshots/themes/settings-email-tide.png new file mode 100644 index 00000000..d4ea08b7 Binary files /dev/null and b/docs/screenshots/themes/settings-email-tide.png differ diff --git a/docs/screenshots/themes/settings-email-vector.png b/docs/screenshots/themes/settings-email-vector.png new file mode 100644 index 00000000..05e3a48f Binary files /dev/null and b/docs/screenshots/themes/settings-email-vector.png differ diff --git a/docs/screenshots/themes/settings-email-volcano.png b/docs/screenshots/themes/settings-email-volcano.png new file mode 100644 index 00000000..b904acff Binary files /dev/null and b/docs/screenshots/themes/settings-email-volcano.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-amber.png b/docs/screenshots/themes/sidebar-affordances-amber.png new file mode 100644 index 00000000..dab3cf25 Binary files /dev/null and b/docs/screenshots/themes/sidebar-affordances-amber.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-beacon.png b/docs/screenshots/themes/sidebar-affordances-beacon.png index b52ef8de..89a27302 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-beacon.png and b/docs/screenshots/themes/sidebar-affordances-beacon.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-bloom.png b/docs/screenshots/themes/sidebar-affordances-bloom.png index 9ddab3a1..d9c871cc 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-bloom.png and b/docs/screenshots/themes/sidebar-affordances-bloom.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-contrast.png b/docs/screenshots/themes/sidebar-affordances-contrast.png index 18b0ed88..2e6ba1b1 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-contrast.png and b/docs/screenshots/themes/sidebar-affordances-contrast.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-dark.png b/docs/screenshots/themes/sidebar-affordances-dark.png index 0150135f..804de891 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-dark.png and b/docs/screenshots/themes/sidebar-affordances-dark.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-dim.png b/docs/screenshots/themes/sidebar-affordances-dim.png index ff651f36..4e38f988 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-dim.png and b/docs/screenshots/themes/sidebar-affordances-dim.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-ember.png b/docs/screenshots/themes/sidebar-affordances-ember.png new file mode 100644 index 00000000..0bb9c7a7 Binary files /dev/null and b/docs/screenshots/themes/sidebar-affordances-ember.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-graphite.png b/docs/screenshots/themes/sidebar-affordances-graphite.png new file mode 100644 index 00000000..066aa53f Binary files /dev/null and b/docs/screenshots/themes/sidebar-affordances-graphite.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-light.png b/docs/screenshots/themes/sidebar-affordances-light.png index 124d1244..f5fb90aa 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-light.png and b/docs/screenshots/themes/sidebar-affordances-light.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-linen.png b/docs/screenshots/themes/sidebar-affordances-linen.png index c8d11b86..e27ecbd7 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-linen.png and b/docs/screenshots/themes/sidebar-affordances-linen.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-matrix.png b/docs/screenshots/themes/sidebar-affordances-matrix.png index 7521a4d0..c947f0cb 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-matrix.png and b/docs/screenshots/themes/sidebar-affordances-matrix.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-meridian.png b/docs/screenshots/themes/sidebar-affordances-meridian.png index 9edf771a..f6677d2d 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-meridian.png and b/docs/screenshots/themes/sidebar-affordances-meridian.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-neon.png b/docs/screenshots/themes/sidebar-affordances-neon.png index 9725ccff..a776830d 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-neon.png and b/docs/screenshots/themes/sidebar-affordances-neon.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-nord.png b/docs/screenshots/themes/sidebar-affordances-nord.png index 26b6bd3b..120e5f5a 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-nord.png and b/docs/screenshots/themes/sidebar-affordances-nord.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-nyan.png b/docs/screenshots/themes/sidebar-affordances-nyan.png index 8ae8f7a0..34e426b2 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-nyan.png and b/docs/screenshots/themes/sidebar-affordances-nyan.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-sepia.png b/docs/screenshots/themes/sidebar-affordances-sepia.png index f0ee720c..5591b73d 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-sepia.png and b/docs/screenshots/themes/sidebar-affordances-sepia.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-solar.png b/docs/screenshots/themes/sidebar-affordances-solar.png index bd93e88b..2d0d6aa3 100644 Binary files a/docs/screenshots/themes/sidebar-affordances-solar.png and b/docs/screenshots/themes/sidebar-affordances-solar.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-tide.png b/docs/screenshots/themes/sidebar-affordances-tide.png new file mode 100644 index 00000000..0fbcb5e2 Binary files /dev/null and b/docs/screenshots/themes/sidebar-affordances-tide.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-vector.png b/docs/screenshots/themes/sidebar-affordances-vector.png new file mode 100644 index 00000000..e6a53469 Binary files /dev/null and b/docs/screenshots/themes/sidebar-affordances-vector.png differ diff --git a/docs/screenshots/themes/sidebar-affordances-volcano.png b/docs/screenshots/themes/sidebar-affordances-volcano.png new file mode 100644 index 00000000..436bc0eb Binary files /dev/null and b/docs/screenshots/themes/sidebar-affordances-volcano.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-amber.png b/docs/screenshots/themes/snippet-name-ghost-amber.png new file mode 100644 index 00000000..3b171263 Binary files /dev/null and b/docs/screenshots/themes/snippet-name-ghost-amber.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-beacon.png b/docs/screenshots/themes/snippet-name-ghost-beacon.png index 1351c719..015f80d6 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-beacon.png and b/docs/screenshots/themes/snippet-name-ghost-beacon.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-bloom.png b/docs/screenshots/themes/snippet-name-ghost-bloom.png index 2f0aa28c..cd4e71ec 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-bloom.png and b/docs/screenshots/themes/snippet-name-ghost-bloom.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-contrast.png b/docs/screenshots/themes/snippet-name-ghost-contrast.png index bb1f4b9a..4411cfe9 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-contrast.png and b/docs/screenshots/themes/snippet-name-ghost-contrast.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-dark.png b/docs/screenshots/themes/snippet-name-ghost-dark.png index 45aadcd4..a514d9f0 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-dark.png and b/docs/screenshots/themes/snippet-name-ghost-dark.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-dim.png b/docs/screenshots/themes/snippet-name-ghost-dim.png index aaeb683c..f1ae4c95 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-dim.png and b/docs/screenshots/themes/snippet-name-ghost-dim.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-ember.png b/docs/screenshots/themes/snippet-name-ghost-ember.png new file mode 100644 index 00000000..1b305dd1 Binary files /dev/null and b/docs/screenshots/themes/snippet-name-ghost-ember.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-graphite.png b/docs/screenshots/themes/snippet-name-ghost-graphite.png new file mode 100644 index 00000000..eb24df86 Binary files /dev/null and b/docs/screenshots/themes/snippet-name-ghost-graphite.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-light.png b/docs/screenshots/themes/snippet-name-ghost-light.png index ae7e8840..5077d9e2 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-light.png and b/docs/screenshots/themes/snippet-name-ghost-light.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-linen.png b/docs/screenshots/themes/snippet-name-ghost-linen.png index a0828116..0f72d70e 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-linen.png and b/docs/screenshots/themes/snippet-name-ghost-linen.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-matrix.png b/docs/screenshots/themes/snippet-name-ghost-matrix.png index 9d0fdc5f..9e512259 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-matrix.png and b/docs/screenshots/themes/snippet-name-ghost-matrix.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-meridian.png b/docs/screenshots/themes/snippet-name-ghost-meridian.png index a25e646e..9bedd31f 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-meridian.png and b/docs/screenshots/themes/snippet-name-ghost-meridian.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-neon.png b/docs/screenshots/themes/snippet-name-ghost-neon.png index f442761a..9235bec4 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-neon.png and b/docs/screenshots/themes/snippet-name-ghost-neon.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-nord.png b/docs/screenshots/themes/snippet-name-ghost-nord.png index 0a77a126..fca5969e 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-nord.png and b/docs/screenshots/themes/snippet-name-ghost-nord.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-nyan.png b/docs/screenshots/themes/snippet-name-ghost-nyan.png index 3aa0fe03..574c615e 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-nyan.png and b/docs/screenshots/themes/snippet-name-ghost-nyan.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-sepia.png b/docs/screenshots/themes/snippet-name-ghost-sepia.png index b1f831ae..0c039b7d 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-sepia.png and b/docs/screenshots/themes/snippet-name-ghost-sepia.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-solar.png b/docs/screenshots/themes/snippet-name-ghost-solar.png index 49c38818..25800173 100644 Binary files a/docs/screenshots/themes/snippet-name-ghost-solar.png and b/docs/screenshots/themes/snippet-name-ghost-solar.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-tide.png b/docs/screenshots/themes/snippet-name-ghost-tide.png new file mode 100644 index 00000000..30bfbd17 Binary files /dev/null and b/docs/screenshots/themes/snippet-name-ghost-tide.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-vector.png b/docs/screenshots/themes/snippet-name-ghost-vector.png new file mode 100644 index 00000000..c191c73f Binary files /dev/null and b/docs/screenshots/themes/snippet-name-ghost-vector.png differ diff --git a/docs/screenshots/themes/snippet-name-ghost-volcano.png b/docs/screenshots/themes/snippet-name-ghost-volcano.png new file mode 100644 index 00000000..480b084e Binary files /dev/null and b/docs/screenshots/themes/snippet-name-ghost-volcano.png differ diff --git a/docs/screenshots/themes/status-band-amber.png b/docs/screenshots/themes/status-band-amber.png new file mode 100644 index 00000000..011738a1 Binary files /dev/null and b/docs/screenshots/themes/status-band-amber.png differ diff --git a/docs/screenshots/themes/status-band-beacon.png b/docs/screenshots/themes/status-band-beacon.png index 51344eb1..0d728a1a 100644 Binary files a/docs/screenshots/themes/status-band-beacon.png and b/docs/screenshots/themes/status-band-beacon.png differ diff --git a/docs/screenshots/themes/status-band-bloom.png b/docs/screenshots/themes/status-band-bloom.png index 15c1b096..4c01238b 100644 Binary files a/docs/screenshots/themes/status-band-bloom.png and b/docs/screenshots/themes/status-band-bloom.png differ diff --git a/docs/screenshots/themes/status-band-contrast.png b/docs/screenshots/themes/status-band-contrast.png index 72714fd3..3f190543 100644 Binary files a/docs/screenshots/themes/status-band-contrast.png and b/docs/screenshots/themes/status-band-contrast.png differ diff --git a/docs/screenshots/themes/status-band-dark.png b/docs/screenshots/themes/status-band-dark.png index d8ddcdce..9d926db1 100644 Binary files a/docs/screenshots/themes/status-band-dark.png and b/docs/screenshots/themes/status-band-dark.png differ diff --git a/docs/screenshots/themes/status-band-dim.png b/docs/screenshots/themes/status-band-dim.png index 22a30c14..a7d88a01 100644 Binary files a/docs/screenshots/themes/status-band-dim.png and b/docs/screenshots/themes/status-band-dim.png differ diff --git a/docs/screenshots/themes/status-band-ember.png b/docs/screenshots/themes/status-band-ember.png new file mode 100644 index 00000000..8a337e51 Binary files /dev/null and b/docs/screenshots/themes/status-band-ember.png differ diff --git a/docs/screenshots/themes/status-band-graphite.png b/docs/screenshots/themes/status-band-graphite.png new file mode 100644 index 00000000..3ef9953b Binary files /dev/null and b/docs/screenshots/themes/status-band-graphite.png differ diff --git a/docs/screenshots/themes/status-band-light.png b/docs/screenshots/themes/status-band-light.png index 22b0b0aa..8659417e 100644 Binary files a/docs/screenshots/themes/status-band-light.png and b/docs/screenshots/themes/status-band-light.png differ diff --git a/docs/screenshots/themes/status-band-linen.png b/docs/screenshots/themes/status-band-linen.png index e7516bc7..0f8d8a57 100644 Binary files a/docs/screenshots/themes/status-band-linen.png and b/docs/screenshots/themes/status-band-linen.png differ diff --git a/docs/screenshots/themes/status-band-matrix.png b/docs/screenshots/themes/status-band-matrix.png index 38c34362..360c43c7 100644 Binary files a/docs/screenshots/themes/status-band-matrix.png and b/docs/screenshots/themes/status-band-matrix.png differ diff --git a/docs/screenshots/themes/status-band-meridian.png b/docs/screenshots/themes/status-band-meridian.png index 152a9832..26cc46f3 100644 Binary files a/docs/screenshots/themes/status-band-meridian.png and b/docs/screenshots/themes/status-band-meridian.png differ diff --git a/docs/screenshots/themes/status-band-neon.png b/docs/screenshots/themes/status-band-neon.png index 1e63a0b1..3edef966 100644 Binary files a/docs/screenshots/themes/status-band-neon.png and b/docs/screenshots/themes/status-band-neon.png differ diff --git a/docs/screenshots/themes/status-band-nord.png b/docs/screenshots/themes/status-band-nord.png index ae371b4a..09f60acb 100644 Binary files a/docs/screenshots/themes/status-band-nord.png and b/docs/screenshots/themes/status-band-nord.png differ diff --git a/docs/screenshots/themes/status-band-nyan.png b/docs/screenshots/themes/status-band-nyan.png index 26488aca..97fd7f04 100644 Binary files a/docs/screenshots/themes/status-band-nyan.png and b/docs/screenshots/themes/status-band-nyan.png differ diff --git a/docs/screenshots/themes/status-band-sepia.png b/docs/screenshots/themes/status-band-sepia.png index dd66b0bc..b3feaf5f 100644 Binary files a/docs/screenshots/themes/status-band-sepia.png and b/docs/screenshots/themes/status-band-sepia.png differ diff --git a/docs/screenshots/themes/status-band-solar.png b/docs/screenshots/themes/status-band-solar.png index 5ac4d1eb..aed81720 100644 Binary files a/docs/screenshots/themes/status-band-solar.png and b/docs/screenshots/themes/status-band-solar.png differ diff --git a/docs/screenshots/themes/status-band-tide.png b/docs/screenshots/themes/status-band-tide.png new file mode 100644 index 00000000..8fa9d83c Binary files /dev/null and b/docs/screenshots/themes/status-band-tide.png differ diff --git a/docs/screenshots/themes/status-band-vector.png b/docs/screenshots/themes/status-band-vector.png new file mode 100644 index 00000000..60130299 Binary files /dev/null and b/docs/screenshots/themes/status-band-vector.png differ diff --git a/docs/screenshots/themes/status-band-volcano.png b/docs/screenshots/themes/status-band-volcano.png new file mode 100644 index 00000000..72e371b3 Binary files /dev/null and b/docs/screenshots/themes/status-band-volcano.png differ diff --git a/docs/screenshots/themes/tool-dialog-amber.png b/docs/screenshots/themes/tool-dialog-amber.png new file mode 100644 index 00000000..3dc50f5e Binary files /dev/null and b/docs/screenshots/themes/tool-dialog-amber.png differ diff --git a/docs/screenshots/themes/tool-dialog-beacon.png b/docs/screenshots/themes/tool-dialog-beacon.png index 1d18aa38..a6bcbfb7 100644 Binary files a/docs/screenshots/themes/tool-dialog-beacon.png and b/docs/screenshots/themes/tool-dialog-beacon.png differ diff --git a/docs/screenshots/themes/tool-dialog-bloom.png b/docs/screenshots/themes/tool-dialog-bloom.png index 5ca6d381..d7a8c629 100644 Binary files a/docs/screenshots/themes/tool-dialog-bloom.png and b/docs/screenshots/themes/tool-dialog-bloom.png differ diff --git a/docs/screenshots/themes/tool-dialog-contrast.png b/docs/screenshots/themes/tool-dialog-contrast.png index e4fa64f9..f1719c0d 100644 Binary files a/docs/screenshots/themes/tool-dialog-contrast.png and b/docs/screenshots/themes/tool-dialog-contrast.png differ diff --git a/docs/screenshots/themes/tool-dialog-dark.png b/docs/screenshots/themes/tool-dialog-dark.png index 542c87a0..3f9862cd 100644 Binary files a/docs/screenshots/themes/tool-dialog-dark.png and b/docs/screenshots/themes/tool-dialog-dark.png differ diff --git a/docs/screenshots/themes/tool-dialog-dim.png b/docs/screenshots/themes/tool-dialog-dim.png index c2823bf7..82b8abde 100644 Binary files a/docs/screenshots/themes/tool-dialog-dim.png and b/docs/screenshots/themes/tool-dialog-dim.png differ diff --git a/docs/screenshots/themes/tool-dialog-ember.png b/docs/screenshots/themes/tool-dialog-ember.png new file mode 100644 index 00000000..9e084ad5 Binary files /dev/null and b/docs/screenshots/themes/tool-dialog-ember.png differ diff --git a/docs/screenshots/themes/tool-dialog-graphite.png b/docs/screenshots/themes/tool-dialog-graphite.png new file mode 100644 index 00000000..b905e4e2 Binary files /dev/null and b/docs/screenshots/themes/tool-dialog-graphite.png differ diff --git a/docs/screenshots/themes/tool-dialog-light.png b/docs/screenshots/themes/tool-dialog-light.png index 9a9e22fe..17a3f866 100644 Binary files a/docs/screenshots/themes/tool-dialog-light.png and b/docs/screenshots/themes/tool-dialog-light.png differ diff --git a/docs/screenshots/themes/tool-dialog-linen.png b/docs/screenshots/themes/tool-dialog-linen.png index b466855b..255270bb 100644 Binary files a/docs/screenshots/themes/tool-dialog-linen.png and b/docs/screenshots/themes/tool-dialog-linen.png differ diff --git a/docs/screenshots/themes/tool-dialog-matrix.png b/docs/screenshots/themes/tool-dialog-matrix.png index 6fd0710d..70f3df01 100644 Binary files a/docs/screenshots/themes/tool-dialog-matrix.png and b/docs/screenshots/themes/tool-dialog-matrix.png differ diff --git a/docs/screenshots/themes/tool-dialog-meridian.png b/docs/screenshots/themes/tool-dialog-meridian.png index affc41da..1a2ae4cf 100644 Binary files a/docs/screenshots/themes/tool-dialog-meridian.png and b/docs/screenshots/themes/tool-dialog-meridian.png differ diff --git a/docs/screenshots/themes/tool-dialog-neon.png b/docs/screenshots/themes/tool-dialog-neon.png index 5f5f7645..1c995fa2 100644 Binary files a/docs/screenshots/themes/tool-dialog-neon.png and b/docs/screenshots/themes/tool-dialog-neon.png differ diff --git a/docs/screenshots/themes/tool-dialog-nord.png b/docs/screenshots/themes/tool-dialog-nord.png index 7f2dcc19..7837e006 100644 Binary files a/docs/screenshots/themes/tool-dialog-nord.png and b/docs/screenshots/themes/tool-dialog-nord.png differ diff --git a/docs/screenshots/themes/tool-dialog-nyan.png b/docs/screenshots/themes/tool-dialog-nyan.png index f63d3f78..0f627eb5 100644 Binary files a/docs/screenshots/themes/tool-dialog-nyan.png and b/docs/screenshots/themes/tool-dialog-nyan.png differ diff --git a/docs/screenshots/themes/tool-dialog-sepia.png b/docs/screenshots/themes/tool-dialog-sepia.png index 7fe8bfa8..75914897 100644 Binary files a/docs/screenshots/themes/tool-dialog-sepia.png and b/docs/screenshots/themes/tool-dialog-sepia.png differ diff --git a/docs/screenshots/themes/tool-dialog-solar.png b/docs/screenshots/themes/tool-dialog-solar.png index bea1883a..0e4892a6 100644 Binary files a/docs/screenshots/themes/tool-dialog-solar.png and b/docs/screenshots/themes/tool-dialog-solar.png differ diff --git a/docs/screenshots/themes/tool-dialog-tide.png b/docs/screenshots/themes/tool-dialog-tide.png new file mode 100644 index 00000000..f8e2955f Binary files /dev/null and b/docs/screenshots/themes/tool-dialog-tide.png differ diff --git a/docs/screenshots/themes/tool-dialog-vector.png b/docs/screenshots/themes/tool-dialog-vector.png new file mode 100644 index 00000000..c3ce4686 Binary files /dev/null and b/docs/screenshots/themes/tool-dialog-vector.png differ diff --git a/docs/screenshots/themes/tool-dialog-volcano.png b/docs/screenshots/themes/tool-dialog-volcano.png new file mode 100644 index 00000000..3386d182 Binary files /dev/null and b/docs/screenshots/themes/tool-dialog-volcano.png differ diff --git a/docs/screenshots/themes/toolbar-actions-amber.png b/docs/screenshots/themes/toolbar-actions-amber.png new file mode 100644 index 00000000..011738a1 Binary files /dev/null and b/docs/screenshots/themes/toolbar-actions-amber.png differ diff --git a/docs/screenshots/themes/toolbar-actions-beacon.png b/docs/screenshots/themes/toolbar-actions-beacon.png index 4432145b..0d728a1a 100644 Binary files a/docs/screenshots/themes/toolbar-actions-beacon.png and b/docs/screenshots/themes/toolbar-actions-beacon.png differ diff --git a/docs/screenshots/themes/toolbar-actions-bloom.png b/docs/screenshots/themes/toolbar-actions-bloom.png index 09066256..4c01238b 100644 Binary files a/docs/screenshots/themes/toolbar-actions-bloom.png and b/docs/screenshots/themes/toolbar-actions-bloom.png differ diff --git a/docs/screenshots/themes/toolbar-actions-contrast.png b/docs/screenshots/themes/toolbar-actions-contrast.png index a18b284e..3f190543 100644 Binary files a/docs/screenshots/themes/toolbar-actions-contrast.png and b/docs/screenshots/themes/toolbar-actions-contrast.png differ diff --git a/docs/screenshots/themes/toolbar-actions-dark.png b/docs/screenshots/themes/toolbar-actions-dark.png index 4e47c0ad..9d926db1 100644 Binary files a/docs/screenshots/themes/toolbar-actions-dark.png and b/docs/screenshots/themes/toolbar-actions-dark.png differ diff --git a/docs/screenshots/themes/toolbar-actions-dim.png b/docs/screenshots/themes/toolbar-actions-dim.png index 85c8246f..a7d88a01 100644 Binary files a/docs/screenshots/themes/toolbar-actions-dim.png and b/docs/screenshots/themes/toolbar-actions-dim.png differ diff --git a/docs/screenshots/themes/toolbar-actions-ember.png b/docs/screenshots/themes/toolbar-actions-ember.png new file mode 100644 index 00000000..8a337e51 Binary files /dev/null and b/docs/screenshots/themes/toolbar-actions-ember.png differ diff --git a/docs/screenshots/themes/toolbar-actions-graphite.png b/docs/screenshots/themes/toolbar-actions-graphite.png new file mode 100644 index 00000000..3ef9953b Binary files /dev/null and b/docs/screenshots/themes/toolbar-actions-graphite.png differ diff --git a/docs/screenshots/themes/toolbar-actions-light.png b/docs/screenshots/themes/toolbar-actions-light.png index 7973b090..958141ce 100644 Binary files a/docs/screenshots/themes/toolbar-actions-light.png and b/docs/screenshots/themes/toolbar-actions-light.png differ diff --git a/docs/screenshots/themes/toolbar-actions-linen.png b/docs/screenshots/themes/toolbar-actions-linen.png index 0b41ce82..0f8d8a57 100644 Binary files a/docs/screenshots/themes/toolbar-actions-linen.png and b/docs/screenshots/themes/toolbar-actions-linen.png differ diff --git a/docs/screenshots/themes/toolbar-actions-matrix.png b/docs/screenshots/themes/toolbar-actions-matrix.png index 81865909..360c43c7 100644 Binary files a/docs/screenshots/themes/toolbar-actions-matrix.png and b/docs/screenshots/themes/toolbar-actions-matrix.png differ diff --git a/docs/screenshots/themes/toolbar-actions-meridian.png b/docs/screenshots/themes/toolbar-actions-meridian.png index d073bc32..26cc46f3 100644 Binary files a/docs/screenshots/themes/toolbar-actions-meridian.png and b/docs/screenshots/themes/toolbar-actions-meridian.png differ diff --git a/docs/screenshots/themes/toolbar-actions-neon.png b/docs/screenshots/themes/toolbar-actions-neon.png index 3a8d3a91..3edef966 100644 Binary files a/docs/screenshots/themes/toolbar-actions-neon.png and b/docs/screenshots/themes/toolbar-actions-neon.png differ diff --git a/docs/screenshots/themes/toolbar-actions-nord.png b/docs/screenshots/themes/toolbar-actions-nord.png index 14feef8f..09f60acb 100644 Binary files a/docs/screenshots/themes/toolbar-actions-nord.png and b/docs/screenshots/themes/toolbar-actions-nord.png differ diff --git a/docs/screenshots/themes/toolbar-actions-nyan.png b/docs/screenshots/themes/toolbar-actions-nyan.png index 2db332b1..306edd77 100644 Binary files a/docs/screenshots/themes/toolbar-actions-nyan.png and b/docs/screenshots/themes/toolbar-actions-nyan.png differ diff --git a/docs/screenshots/themes/toolbar-actions-sepia.png b/docs/screenshots/themes/toolbar-actions-sepia.png index f6d7fe11..b3feaf5f 100644 Binary files a/docs/screenshots/themes/toolbar-actions-sepia.png and b/docs/screenshots/themes/toolbar-actions-sepia.png differ diff --git a/docs/screenshots/themes/toolbar-actions-solar.png b/docs/screenshots/themes/toolbar-actions-solar.png index c4b99ad9..aed81720 100644 Binary files a/docs/screenshots/themes/toolbar-actions-solar.png and b/docs/screenshots/themes/toolbar-actions-solar.png differ diff --git a/docs/screenshots/themes/toolbar-actions-tide.png b/docs/screenshots/themes/toolbar-actions-tide.png new file mode 100644 index 00000000..8fa9d83c Binary files /dev/null and b/docs/screenshots/themes/toolbar-actions-tide.png differ diff --git a/docs/screenshots/themes/toolbar-actions-vector.png b/docs/screenshots/themes/toolbar-actions-vector.png new file mode 100644 index 00000000..60130299 Binary files /dev/null and b/docs/screenshots/themes/toolbar-actions-vector.png differ diff --git a/docs/screenshots/themes/toolbar-actions-volcano.png b/docs/screenshots/themes/toolbar-actions-volcano.png new file mode 100644 index 00000000..72e371b3 Binary files /dev/null and b/docs/screenshots/themes/toolbar-actions-volcano.png differ diff --git a/docs/screenshots/themes/tools-section-amber.png b/docs/screenshots/themes/tools-section-amber.png new file mode 100644 index 00000000..25d84380 Binary files /dev/null and b/docs/screenshots/themes/tools-section-amber.png differ diff --git a/docs/screenshots/themes/tools-section-beacon.png b/docs/screenshots/themes/tools-section-beacon.png index a1241412..eec8436c 100644 Binary files a/docs/screenshots/themes/tools-section-beacon.png and b/docs/screenshots/themes/tools-section-beacon.png differ diff --git a/docs/screenshots/themes/tools-section-bloom.png b/docs/screenshots/themes/tools-section-bloom.png index 0d995bb7..9454f53d 100644 Binary files a/docs/screenshots/themes/tools-section-bloom.png and b/docs/screenshots/themes/tools-section-bloom.png differ diff --git a/docs/screenshots/themes/tools-section-contrast.png b/docs/screenshots/themes/tools-section-contrast.png index c7d21158..92b65646 100644 Binary files a/docs/screenshots/themes/tools-section-contrast.png and b/docs/screenshots/themes/tools-section-contrast.png differ diff --git a/docs/screenshots/themes/tools-section-dark.png b/docs/screenshots/themes/tools-section-dark.png index fb042dd9..f40af62a 100644 Binary files a/docs/screenshots/themes/tools-section-dark.png and b/docs/screenshots/themes/tools-section-dark.png differ diff --git a/docs/screenshots/themes/tools-section-dim.png b/docs/screenshots/themes/tools-section-dim.png index b5acd8c4..2fe7e89a 100644 Binary files a/docs/screenshots/themes/tools-section-dim.png and b/docs/screenshots/themes/tools-section-dim.png differ diff --git a/docs/screenshots/themes/tools-section-ember.png b/docs/screenshots/themes/tools-section-ember.png new file mode 100644 index 00000000..ac887dd7 Binary files /dev/null and b/docs/screenshots/themes/tools-section-ember.png differ diff --git a/docs/screenshots/themes/tools-section-graphite.png b/docs/screenshots/themes/tools-section-graphite.png new file mode 100644 index 00000000..5a087c2f Binary files /dev/null and b/docs/screenshots/themes/tools-section-graphite.png differ diff --git a/docs/screenshots/themes/tools-section-light.png b/docs/screenshots/themes/tools-section-light.png index 670f37d9..53c98c9e 100644 Binary files a/docs/screenshots/themes/tools-section-light.png and b/docs/screenshots/themes/tools-section-light.png differ diff --git a/docs/screenshots/themes/tools-section-linen.png b/docs/screenshots/themes/tools-section-linen.png index bb712c72..03388124 100644 Binary files a/docs/screenshots/themes/tools-section-linen.png and b/docs/screenshots/themes/tools-section-linen.png differ diff --git a/docs/screenshots/themes/tools-section-matrix.png b/docs/screenshots/themes/tools-section-matrix.png index 106a747c..69ae893a 100644 Binary files a/docs/screenshots/themes/tools-section-matrix.png and b/docs/screenshots/themes/tools-section-matrix.png differ diff --git a/docs/screenshots/themes/tools-section-meridian.png b/docs/screenshots/themes/tools-section-meridian.png index 7864fb49..6a3fe500 100644 Binary files a/docs/screenshots/themes/tools-section-meridian.png and b/docs/screenshots/themes/tools-section-meridian.png differ diff --git a/docs/screenshots/themes/tools-section-neon.png b/docs/screenshots/themes/tools-section-neon.png index 025d7194..fc9e3b79 100644 Binary files a/docs/screenshots/themes/tools-section-neon.png and b/docs/screenshots/themes/tools-section-neon.png differ diff --git a/docs/screenshots/themes/tools-section-nord.png b/docs/screenshots/themes/tools-section-nord.png index 436d70a1..6c2657a7 100644 Binary files a/docs/screenshots/themes/tools-section-nord.png and b/docs/screenshots/themes/tools-section-nord.png differ diff --git a/docs/screenshots/themes/tools-section-nyan.png b/docs/screenshots/themes/tools-section-nyan.png index b620546f..efea83c9 100644 Binary files a/docs/screenshots/themes/tools-section-nyan.png and b/docs/screenshots/themes/tools-section-nyan.png differ diff --git a/docs/screenshots/themes/tools-section-sepia.png b/docs/screenshots/themes/tools-section-sepia.png index 439ec93a..89e24543 100644 Binary files a/docs/screenshots/themes/tools-section-sepia.png and b/docs/screenshots/themes/tools-section-sepia.png differ diff --git a/docs/screenshots/themes/tools-section-solar.png b/docs/screenshots/themes/tools-section-solar.png index 8c57261a..777ca0f3 100644 Binary files a/docs/screenshots/themes/tools-section-solar.png and b/docs/screenshots/themes/tools-section-solar.png differ diff --git a/docs/screenshots/themes/tools-section-tide.png b/docs/screenshots/themes/tools-section-tide.png new file mode 100644 index 00000000..0e120802 Binary files /dev/null and b/docs/screenshots/themes/tools-section-tide.png differ diff --git a/docs/screenshots/themes/tools-section-vector.png b/docs/screenshots/themes/tools-section-vector.png new file mode 100644 index 00000000..fcd6ecef Binary files /dev/null and b/docs/screenshots/themes/tools-section-vector.png differ diff --git a/docs/screenshots/themes/tools-section-volcano.png b/docs/screenshots/themes/tools-section-volcano.png new file mode 100644 index 00000000..b5fb0713 Binary files /dev/null and b/docs/screenshots/themes/tools-section-volcano.png differ diff --git a/docs/screenshots/themes/tour-callout-amber.png b/docs/screenshots/themes/tour-callout-amber.png new file mode 100644 index 00000000..dda1502f Binary files /dev/null and b/docs/screenshots/themes/tour-callout-amber.png differ diff --git a/docs/screenshots/themes/tour-callout-beacon.png b/docs/screenshots/themes/tour-callout-beacon.png index 3c49cad1..24723b3d 100644 Binary files a/docs/screenshots/themes/tour-callout-beacon.png and b/docs/screenshots/themes/tour-callout-beacon.png differ diff --git a/docs/screenshots/themes/tour-callout-bloom.png b/docs/screenshots/themes/tour-callout-bloom.png index 5cb03a68..11909465 100644 Binary files a/docs/screenshots/themes/tour-callout-bloom.png and b/docs/screenshots/themes/tour-callout-bloom.png differ diff --git a/docs/screenshots/themes/tour-callout-contrast.png b/docs/screenshots/themes/tour-callout-contrast.png index 4fc4f1d2..fe3c55dd 100644 Binary files a/docs/screenshots/themes/tour-callout-contrast.png and b/docs/screenshots/themes/tour-callout-contrast.png differ diff --git a/docs/screenshots/themes/tour-callout-dark.png b/docs/screenshots/themes/tour-callout-dark.png index cd5ef3bc..5413e6df 100644 Binary files a/docs/screenshots/themes/tour-callout-dark.png and b/docs/screenshots/themes/tour-callout-dark.png differ diff --git a/docs/screenshots/themes/tour-callout-dim.png b/docs/screenshots/themes/tour-callout-dim.png index a340ce48..4e56d2b4 100644 Binary files a/docs/screenshots/themes/tour-callout-dim.png and b/docs/screenshots/themes/tour-callout-dim.png differ diff --git a/docs/screenshots/themes/tour-callout-ember.png b/docs/screenshots/themes/tour-callout-ember.png new file mode 100644 index 00000000..a4040109 Binary files /dev/null and b/docs/screenshots/themes/tour-callout-ember.png differ diff --git a/docs/screenshots/themes/tour-callout-graphite.png b/docs/screenshots/themes/tour-callout-graphite.png new file mode 100644 index 00000000..2ed1c8bd Binary files /dev/null and b/docs/screenshots/themes/tour-callout-graphite.png differ diff --git a/docs/screenshots/themes/tour-callout-light.png b/docs/screenshots/themes/tour-callout-light.png index 89a3e97e..c724226b 100644 Binary files a/docs/screenshots/themes/tour-callout-light.png and b/docs/screenshots/themes/tour-callout-light.png differ diff --git a/docs/screenshots/themes/tour-callout-linen.png b/docs/screenshots/themes/tour-callout-linen.png index 27e216e9..ce974f93 100644 Binary files a/docs/screenshots/themes/tour-callout-linen.png and b/docs/screenshots/themes/tour-callout-linen.png differ diff --git a/docs/screenshots/themes/tour-callout-matrix.png b/docs/screenshots/themes/tour-callout-matrix.png index 1786197a..060b8c9a 100644 Binary files a/docs/screenshots/themes/tour-callout-matrix.png and b/docs/screenshots/themes/tour-callout-matrix.png differ diff --git a/docs/screenshots/themes/tour-callout-meridian.png b/docs/screenshots/themes/tour-callout-meridian.png index f8f2ed60..fe456ee2 100644 Binary files a/docs/screenshots/themes/tour-callout-meridian.png and b/docs/screenshots/themes/tour-callout-meridian.png differ diff --git a/docs/screenshots/themes/tour-callout-neon.png b/docs/screenshots/themes/tour-callout-neon.png index 2edaa534..b865a8a4 100644 Binary files a/docs/screenshots/themes/tour-callout-neon.png and b/docs/screenshots/themes/tour-callout-neon.png differ diff --git a/docs/screenshots/themes/tour-callout-nord.png b/docs/screenshots/themes/tour-callout-nord.png index 6a5b2df1..df7a063e 100644 Binary files a/docs/screenshots/themes/tour-callout-nord.png and b/docs/screenshots/themes/tour-callout-nord.png differ diff --git a/docs/screenshots/themes/tour-callout-nyan.png b/docs/screenshots/themes/tour-callout-nyan.png index b7a3aecf..f59cfba2 100644 Binary files a/docs/screenshots/themes/tour-callout-nyan.png and b/docs/screenshots/themes/tour-callout-nyan.png differ diff --git a/docs/screenshots/themes/tour-callout-sepia.png b/docs/screenshots/themes/tour-callout-sepia.png index 3c06d59e..55399d38 100644 Binary files a/docs/screenshots/themes/tour-callout-sepia.png and b/docs/screenshots/themes/tour-callout-sepia.png differ diff --git a/docs/screenshots/themes/tour-callout-solar.png b/docs/screenshots/themes/tour-callout-solar.png index ea6edc91..03173697 100644 Binary files a/docs/screenshots/themes/tour-callout-solar.png and b/docs/screenshots/themes/tour-callout-solar.png differ diff --git a/docs/screenshots/themes/tour-callout-tide.png b/docs/screenshots/themes/tour-callout-tide.png new file mode 100644 index 00000000..b8cd426c Binary files /dev/null and b/docs/screenshots/themes/tour-callout-tide.png differ diff --git a/docs/screenshots/themes/tour-callout-vector.png b/docs/screenshots/themes/tour-callout-vector.png new file mode 100644 index 00000000..24335628 Binary files /dev/null and b/docs/screenshots/themes/tour-callout-vector.png differ diff --git a/docs/screenshots/themes/tour-callout-volcano.png b/docs/screenshots/themes/tour-callout-volcano.png new file mode 100644 index 00000000..f740e75d Binary files /dev/null and b/docs/screenshots/themes/tour-callout-volcano.png differ diff --git a/docs/screenshots/themes/trusted-keys-amber.png b/docs/screenshots/themes/trusted-keys-amber.png new file mode 100644 index 00000000..6a8939dc Binary files /dev/null and b/docs/screenshots/themes/trusted-keys-amber.png differ diff --git a/docs/screenshots/themes/trusted-keys-beacon.png b/docs/screenshots/themes/trusted-keys-beacon.png index 5bb2e76e..75de81e3 100644 Binary files a/docs/screenshots/themes/trusted-keys-beacon.png and b/docs/screenshots/themes/trusted-keys-beacon.png differ diff --git a/docs/screenshots/themes/trusted-keys-bloom.png b/docs/screenshots/themes/trusted-keys-bloom.png index 5666e371..e245c094 100644 Binary files a/docs/screenshots/themes/trusted-keys-bloom.png and b/docs/screenshots/themes/trusted-keys-bloom.png differ diff --git a/docs/screenshots/themes/trusted-keys-contrast.png b/docs/screenshots/themes/trusted-keys-contrast.png index d93a60be..c36445f0 100644 Binary files a/docs/screenshots/themes/trusted-keys-contrast.png and b/docs/screenshots/themes/trusted-keys-contrast.png differ diff --git a/docs/screenshots/themes/trusted-keys-dark.png b/docs/screenshots/themes/trusted-keys-dark.png index 2aac779d..86a3b861 100644 Binary files a/docs/screenshots/themes/trusted-keys-dark.png and b/docs/screenshots/themes/trusted-keys-dark.png differ diff --git a/docs/screenshots/themes/trusted-keys-dim.png b/docs/screenshots/themes/trusted-keys-dim.png index 16d10f0b..61f69e81 100644 Binary files a/docs/screenshots/themes/trusted-keys-dim.png and b/docs/screenshots/themes/trusted-keys-dim.png differ diff --git a/docs/screenshots/themes/trusted-keys-ember.png b/docs/screenshots/themes/trusted-keys-ember.png new file mode 100644 index 00000000..c54595e0 Binary files /dev/null and b/docs/screenshots/themes/trusted-keys-ember.png differ diff --git a/docs/screenshots/themes/trusted-keys-graphite.png b/docs/screenshots/themes/trusted-keys-graphite.png new file mode 100644 index 00000000..b57d5a27 Binary files /dev/null and b/docs/screenshots/themes/trusted-keys-graphite.png differ diff --git a/docs/screenshots/themes/trusted-keys-light.png b/docs/screenshots/themes/trusted-keys-light.png index 6abfb5eb..a94ce278 100644 Binary files a/docs/screenshots/themes/trusted-keys-light.png and b/docs/screenshots/themes/trusted-keys-light.png differ diff --git a/docs/screenshots/themes/trusted-keys-linen.png b/docs/screenshots/themes/trusted-keys-linen.png index 39cc1545..df7813a3 100644 Binary files a/docs/screenshots/themes/trusted-keys-linen.png and b/docs/screenshots/themes/trusted-keys-linen.png differ diff --git a/docs/screenshots/themes/trusted-keys-matrix.png b/docs/screenshots/themes/trusted-keys-matrix.png index ccced059..bf27601c 100644 Binary files a/docs/screenshots/themes/trusted-keys-matrix.png and b/docs/screenshots/themes/trusted-keys-matrix.png differ diff --git a/docs/screenshots/themes/trusted-keys-meridian.png b/docs/screenshots/themes/trusted-keys-meridian.png index 6b8baad8..ef383731 100644 Binary files a/docs/screenshots/themes/trusted-keys-meridian.png and b/docs/screenshots/themes/trusted-keys-meridian.png differ diff --git a/docs/screenshots/themes/trusted-keys-neon.png b/docs/screenshots/themes/trusted-keys-neon.png index 8aa38f5c..5912dfea 100644 Binary files a/docs/screenshots/themes/trusted-keys-neon.png and b/docs/screenshots/themes/trusted-keys-neon.png differ diff --git a/docs/screenshots/themes/trusted-keys-nord.png b/docs/screenshots/themes/trusted-keys-nord.png index e5fad090..0b72f8ee 100644 Binary files a/docs/screenshots/themes/trusted-keys-nord.png and b/docs/screenshots/themes/trusted-keys-nord.png differ diff --git a/docs/screenshots/themes/trusted-keys-nyan.png b/docs/screenshots/themes/trusted-keys-nyan.png index c7189e34..b1b05668 100644 Binary files a/docs/screenshots/themes/trusted-keys-nyan.png and b/docs/screenshots/themes/trusted-keys-nyan.png differ diff --git a/docs/screenshots/themes/trusted-keys-sepia.png b/docs/screenshots/themes/trusted-keys-sepia.png index 61a77263..446f6a62 100644 Binary files a/docs/screenshots/themes/trusted-keys-sepia.png and b/docs/screenshots/themes/trusted-keys-sepia.png differ diff --git a/docs/screenshots/themes/trusted-keys-solar.png b/docs/screenshots/themes/trusted-keys-solar.png index 5b364aa0..587ec166 100644 Binary files a/docs/screenshots/themes/trusted-keys-solar.png and b/docs/screenshots/themes/trusted-keys-solar.png differ diff --git a/docs/screenshots/themes/trusted-keys-tide.png b/docs/screenshots/themes/trusted-keys-tide.png new file mode 100644 index 00000000..e1e79e7c Binary files /dev/null and b/docs/screenshots/themes/trusted-keys-tide.png differ diff --git a/docs/screenshots/themes/trusted-keys-vector.png b/docs/screenshots/themes/trusted-keys-vector.png new file mode 100644 index 00000000..a521fbc7 Binary files /dev/null and b/docs/screenshots/themes/trusted-keys-vector.png differ diff --git a/docs/screenshots/themes/trusted-keys-volcano.png b/docs/screenshots/themes/trusted-keys-volcano.png new file mode 100644 index 00000000..e1115487 Binary files /dev/null and b/docs/screenshots/themes/trusted-keys-volcano.png differ diff --git a/docs/screenshots/themes/view-menu-amber.png b/docs/screenshots/themes/view-menu-amber.png new file mode 100644 index 00000000..9d5db242 Binary files /dev/null and b/docs/screenshots/themes/view-menu-amber.png differ diff --git a/docs/screenshots/themes/view-menu-beacon.png b/docs/screenshots/themes/view-menu-beacon.png index fc32d412..19625269 100644 Binary files a/docs/screenshots/themes/view-menu-beacon.png and b/docs/screenshots/themes/view-menu-beacon.png differ diff --git a/docs/screenshots/themes/view-menu-bloom.png b/docs/screenshots/themes/view-menu-bloom.png index b4cb82cf..8e6145bc 100644 Binary files a/docs/screenshots/themes/view-menu-bloom.png and b/docs/screenshots/themes/view-menu-bloom.png differ diff --git a/docs/screenshots/themes/view-menu-contrast.png b/docs/screenshots/themes/view-menu-contrast.png index 87b837e2..f9928d9d 100644 Binary files a/docs/screenshots/themes/view-menu-contrast.png and b/docs/screenshots/themes/view-menu-contrast.png differ diff --git a/docs/screenshots/themes/view-menu-dark.png b/docs/screenshots/themes/view-menu-dark.png index fd2c3f0e..b0da50ee 100644 Binary files a/docs/screenshots/themes/view-menu-dark.png and b/docs/screenshots/themes/view-menu-dark.png differ diff --git a/docs/screenshots/themes/view-menu-dim.png b/docs/screenshots/themes/view-menu-dim.png index 87a2d297..5375749c 100644 Binary files a/docs/screenshots/themes/view-menu-dim.png and b/docs/screenshots/themes/view-menu-dim.png differ diff --git a/docs/screenshots/themes/view-menu-ember.png b/docs/screenshots/themes/view-menu-ember.png new file mode 100644 index 00000000..2ee51b64 Binary files /dev/null and b/docs/screenshots/themes/view-menu-ember.png differ diff --git a/docs/screenshots/themes/view-menu-graphite.png b/docs/screenshots/themes/view-menu-graphite.png new file mode 100644 index 00000000..2ec50838 Binary files /dev/null and b/docs/screenshots/themes/view-menu-graphite.png differ diff --git a/docs/screenshots/themes/view-menu-light.png b/docs/screenshots/themes/view-menu-light.png index c085cdb7..db95fa4a 100644 Binary files a/docs/screenshots/themes/view-menu-light.png and b/docs/screenshots/themes/view-menu-light.png differ diff --git a/docs/screenshots/themes/view-menu-linen.png b/docs/screenshots/themes/view-menu-linen.png index 9cb79dab..eca598a2 100644 Binary files a/docs/screenshots/themes/view-menu-linen.png and b/docs/screenshots/themes/view-menu-linen.png differ diff --git a/docs/screenshots/themes/view-menu-matrix.png b/docs/screenshots/themes/view-menu-matrix.png index d9ba2799..5afffed3 100644 Binary files a/docs/screenshots/themes/view-menu-matrix.png and b/docs/screenshots/themes/view-menu-matrix.png differ diff --git a/docs/screenshots/themes/view-menu-meridian.png b/docs/screenshots/themes/view-menu-meridian.png index 566bab23..8a18f3e1 100644 Binary files a/docs/screenshots/themes/view-menu-meridian.png and b/docs/screenshots/themes/view-menu-meridian.png differ diff --git a/docs/screenshots/themes/view-menu-neon.png b/docs/screenshots/themes/view-menu-neon.png index 5fa55145..3301e47a 100644 Binary files a/docs/screenshots/themes/view-menu-neon.png and b/docs/screenshots/themes/view-menu-neon.png differ diff --git a/docs/screenshots/themes/view-menu-nord.png b/docs/screenshots/themes/view-menu-nord.png index 65f99425..e8b3a1dd 100644 Binary files a/docs/screenshots/themes/view-menu-nord.png and b/docs/screenshots/themes/view-menu-nord.png differ diff --git a/docs/screenshots/themes/view-menu-nyan.png b/docs/screenshots/themes/view-menu-nyan.png index 409c9f76..91a1525c 100644 Binary files a/docs/screenshots/themes/view-menu-nyan.png and b/docs/screenshots/themes/view-menu-nyan.png differ diff --git a/docs/screenshots/themes/view-menu-sepia.png b/docs/screenshots/themes/view-menu-sepia.png index b5a1a5d4..a63006a9 100644 Binary files a/docs/screenshots/themes/view-menu-sepia.png and b/docs/screenshots/themes/view-menu-sepia.png differ diff --git a/docs/screenshots/themes/view-menu-solar.png b/docs/screenshots/themes/view-menu-solar.png index 2c916142..a84372c6 100644 Binary files a/docs/screenshots/themes/view-menu-solar.png and b/docs/screenshots/themes/view-menu-solar.png differ diff --git a/docs/screenshots/themes/view-menu-tide.png b/docs/screenshots/themes/view-menu-tide.png new file mode 100644 index 00000000..61e94c0e Binary files /dev/null and b/docs/screenshots/themes/view-menu-tide.png differ diff --git a/docs/screenshots/themes/view-menu-vector.png b/docs/screenshots/themes/view-menu-vector.png new file mode 100644 index 00000000..b0d3cc60 Binary files /dev/null and b/docs/screenshots/themes/view-menu-vector.png differ diff --git a/docs/screenshots/themes/view-menu-volcano.png b/docs/screenshots/themes/view-menu-volcano.png new file mode 100644 index 00000000..e8a18df1 Binary files /dev/null and b/docs/screenshots/themes/view-menu-volcano.png differ diff --git a/docs/screenshots/themes/waiting-for-second-amber.png b/docs/screenshots/themes/waiting-for-second-amber.png new file mode 100644 index 00000000..03345bb2 Binary files /dev/null and b/docs/screenshots/themes/waiting-for-second-amber.png differ diff --git a/docs/screenshots/themes/waiting-for-second-beacon.png b/docs/screenshots/themes/waiting-for-second-beacon.png index c8504728..b174b90d 100644 Binary files a/docs/screenshots/themes/waiting-for-second-beacon.png and b/docs/screenshots/themes/waiting-for-second-beacon.png differ diff --git a/docs/screenshots/themes/waiting-for-second-bloom.png b/docs/screenshots/themes/waiting-for-second-bloom.png index 56d220d3..ba60ea99 100644 Binary files a/docs/screenshots/themes/waiting-for-second-bloom.png and b/docs/screenshots/themes/waiting-for-second-bloom.png differ diff --git a/docs/screenshots/themes/waiting-for-second-contrast.png b/docs/screenshots/themes/waiting-for-second-contrast.png index 83c5ff3e..73367536 100644 Binary files a/docs/screenshots/themes/waiting-for-second-contrast.png and b/docs/screenshots/themes/waiting-for-second-contrast.png differ diff --git a/docs/screenshots/themes/waiting-for-second-dark.png b/docs/screenshots/themes/waiting-for-second-dark.png index c94b2ee5..1684160e 100644 Binary files a/docs/screenshots/themes/waiting-for-second-dark.png and b/docs/screenshots/themes/waiting-for-second-dark.png differ diff --git a/docs/screenshots/themes/waiting-for-second-dim.png b/docs/screenshots/themes/waiting-for-second-dim.png index 23d6ee24..4d706d22 100644 Binary files a/docs/screenshots/themes/waiting-for-second-dim.png and b/docs/screenshots/themes/waiting-for-second-dim.png differ diff --git a/docs/screenshots/themes/waiting-for-second-ember.png b/docs/screenshots/themes/waiting-for-second-ember.png new file mode 100644 index 00000000..3215ed59 Binary files /dev/null and b/docs/screenshots/themes/waiting-for-second-ember.png differ diff --git a/docs/screenshots/themes/waiting-for-second-graphite.png b/docs/screenshots/themes/waiting-for-second-graphite.png new file mode 100644 index 00000000..8f68d056 Binary files /dev/null and b/docs/screenshots/themes/waiting-for-second-graphite.png differ diff --git a/docs/screenshots/themes/waiting-for-second-light.png b/docs/screenshots/themes/waiting-for-second-light.png index f08ec00f..364f0db2 100644 Binary files a/docs/screenshots/themes/waiting-for-second-light.png and b/docs/screenshots/themes/waiting-for-second-light.png differ diff --git a/docs/screenshots/themes/waiting-for-second-linen.png b/docs/screenshots/themes/waiting-for-second-linen.png index f438eb89..d68c28da 100644 Binary files a/docs/screenshots/themes/waiting-for-second-linen.png and b/docs/screenshots/themes/waiting-for-second-linen.png differ diff --git a/docs/screenshots/themes/waiting-for-second-matrix.png b/docs/screenshots/themes/waiting-for-second-matrix.png index ee3ec159..f92143ad 100644 Binary files a/docs/screenshots/themes/waiting-for-second-matrix.png and b/docs/screenshots/themes/waiting-for-second-matrix.png differ diff --git a/docs/screenshots/themes/waiting-for-second-meridian.png b/docs/screenshots/themes/waiting-for-second-meridian.png index 52ceb071..b9bfc2b0 100644 Binary files a/docs/screenshots/themes/waiting-for-second-meridian.png and b/docs/screenshots/themes/waiting-for-second-meridian.png differ diff --git a/docs/screenshots/themes/waiting-for-second-neon.png b/docs/screenshots/themes/waiting-for-second-neon.png index b0267fef..f677dfa4 100644 Binary files a/docs/screenshots/themes/waiting-for-second-neon.png and b/docs/screenshots/themes/waiting-for-second-neon.png differ diff --git a/docs/screenshots/themes/waiting-for-second-nord.png b/docs/screenshots/themes/waiting-for-second-nord.png index 427867e1..74f21653 100644 Binary files a/docs/screenshots/themes/waiting-for-second-nord.png and b/docs/screenshots/themes/waiting-for-second-nord.png differ diff --git a/docs/screenshots/themes/waiting-for-second-nyan.png b/docs/screenshots/themes/waiting-for-second-nyan.png index 81afc295..b95008ab 100644 Binary files a/docs/screenshots/themes/waiting-for-second-nyan.png and b/docs/screenshots/themes/waiting-for-second-nyan.png differ diff --git a/docs/screenshots/themes/waiting-for-second-sepia.png b/docs/screenshots/themes/waiting-for-second-sepia.png index 6ad58118..7f898ac7 100644 Binary files a/docs/screenshots/themes/waiting-for-second-sepia.png and b/docs/screenshots/themes/waiting-for-second-sepia.png differ diff --git a/docs/screenshots/themes/waiting-for-second-solar.png b/docs/screenshots/themes/waiting-for-second-solar.png index f7a02967..de278673 100644 Binary files a/docs/screenshots/themes/waiting-for-second-solar.png and b/docs/screenshots/themes/waiting-for-second-solar.png differ diff --git a/docs/screenshots/themes/waiting-for-second-tide.png b/docs/screenshots/themes/waiting-for-second-tide.png new file mode 100644 index 00000000..59a568d3 Binary files /dev/null and b/docs/screenshots/themes/waiting-for-second-tide.png differ diff --git a/docs/screenshots/themes/waiting-for-second-vector.png b/docs/screenshots/themes/waiting-for-second-vector.png new file mode 100644 index 00000000..c80493f1 Binary files /dev/null and b/docs/screenshots/themes/waiting-for-second-vector.png differ diff --git a/docs/screenshots/themes/waiting-for-second-volcano.png b/docs/screenshots/themes/waiting-for-second-volcano.png new file mode 100644 index 00000000..fee6a624 Binary files /dev/null and b/docs/screenshots/themes/waiting-for-second-volcano.png differ diff --git a/docs/security.md b/docs/security.md index 6731bbde..58655c05 100644 --- a/docs/security.md +++ b/docs/security.md @@ -64,6 +64,18 @@ takes bytes and a display name. With no mergetool launch there is no path held, so the handler writes nothing at all whatever arrives, and a session is spent once used. +The middle pane is a full editor, and that costs nothing here: `merge:write` +already took arbitrary text, so a renderer that wanted to write something +neither side said could always do it. Editing widens what the READER can +express, not what the surface accepts. + +The three inputs are read out of git's index (`:1:`/`:2:`/`:3:`) through the +same argv fence as any other git call — fixed vectors, `--end-of-options`, the +path checked as `readBlobArgs` checks it, and the stage one of three integers +the app chooses. `name-rev` labels the panes with the branches, and takes only +`HEAD` or `MERGE_HEAD`: literals, never a string from anywhere. All of it is +still untrusted input, so the binary refusal applies to each. + `mergetool.diffbro.trustExitCode` is `true`, which is only honest because the merge launcher WAITS for `$MERGED` to change before it exits. The app is single-instance, so the launch itself returns immediately; a script that exited diff --git a/docs/standards.md b/docs/standards.md index b4cfb37a..efb15834 100644 --- a/docs/standards.md +++ b/docs/standards.md @@ -298,7 +298,7 @@ directory move passes CI while silently removing enforcement. genuinely right. New UI must be checked in EVERY theme — see the design-proposal rule below, not just the two you happen to have open. `make theme-sweep` (scripts/theme-sweep.mjs) is how a NEW surface is checked: - it drives the app through all 14, reads the COMPUTED colours off the live DOM + it drives the app through all 20, reads the COMPUTED colours off the live DOM and holds each pair to a floor declared per probe. `check:themes` holds the tokens; this holds what they compose into — a label on a `color-mix` chip, a control in its unset state. Add a surface to `SURFACES` when you build one. @@ -315,16 +315,16 @@ directory move passes CI while silently removing enforcement. keyline and NEVER on the label, which scores below 4.5:1 on five themes. Off is the opposite language — flat, no lift, weak keyline — never the same properties at a lower opacity. `npm run check:themes` holds the resting face, - its label and its edge to floors on all 14; that check exists because nothing + its label and its edge to floors on all 20; that check exists because nothing measured the affordance the first time and a whole toolbar shipped looking disabled. - **A UI/UX proposal is not a proposal until it has been checked against every - supported theme.** `themes.css` ships 14 — light, dark, solar, neon, nord, - sepia, dim, beacon, meridian, linen, bloom, nyan, matrix, contrast — and seven - are light-ground. A design validated on one or two is a redesign waiting to + supported theme.** `themes.css` ships 20 — light, dark, solar, neon, nord, + sepia, dim, beacon, meridian, linen, bloom, nyan, matrix, contrast, volcano, + amber, tide, ember, graphite, vector — and eight are light-ground. A design validated on one or two is a redesign waiting to happen, and redoing it costs far more than checking it would have. Before proposing OR building any visual change: read the real token values out of - `themes.css` (parse them, never guess), render the idea against all 14, and + `themes.css` (parse them, never guess), render the idea against all 20, and say per theme where it breaks and why. Hardcoded colour is the usual culprit — a black rim plus a white top highlight is a dark-panel idiom that dies on every light theme; express it through `--border` / `--text` / `--shadow-rgb` / diff --git a/e2e/merge-many.spec.mjs b/e2e/merge-many.spec.mjs new file mode 100644 index 00000000..585c3d56 --- /dev/null +++ b/e2e/merge-many.spec.mjs @@ -0,0 +1,108 @@ +import { test, expect, launchApp, freshUserDataDir, firstReadyPage } from './fixtures.mjs' +import { workerEnv } from './workerEnv.mjs' +import { execFileSync, spawn } from 'node:child_process' +import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { createRequire } from 'node:module' +import { fileURLToPath } from 'node:url' + +const ROOT = fileURLToPath(new URL('..', import.meta.url)) +const MAIN = join(ROOT, 'build', 'main', 'index.js') +const ELECTRON = createRequire(import.meta.url)('electron') + +// Thirty conflicted files is an ordinary rebase, not an edge case. git calls the +// tool once per file and waits for each, so the app sees thirty launches in a +// row — and the tab cap is sixteen. +const FILES = 30 + +function bigConflict() { + const dir = mkdtempSync(join(tmpdir(), 'diffbro-many-')) + const env = { + ...process.env, + GIT_AUTHOR_NAME: 'T', + GIT_AUTHOR_EMAIL: 't@e', + GIT_COMMITTER_NAME: 'T', + GIT_COMMITTER_EMAIL: 't@e' + } + const git = (...args) => execFileSync('git', args, { cwd: dir, env }) + const names = Array.from({ length: FILES }, (_, i) => `file-${String(i).padStart(2, '0')}.txt`) + git('init', '-q', '-b', 'main') + for (const name of names) writeFileSync(join(dir, name), `one\nbase\nthree\n`) + git('add', '.') + git('commit', '-qm', 'base') + git('checkout', '-qb', 'feature') + for (const name of names) writeFileSync(join(dir, name), `one\ntheirs ${name}\nthree\n`) + git('commit', '-qam', 'theirs') + git('checkout', '-q', 'main') + for (const name of names) writeFileSync(join(dir, name), `one\nours ${name}\nthree\n`) + git('commit', '-qam', 'ours') + try { + git('merge', 'feature') + } catch { + // Expected: thirty conflicts. + } + return { dir, names, git } +} + +function mergetool(userDataDir, dir, file) { + const env = { ...workerEnv(userDataDir) } + delete env.ELECTRON_RUN_AS_NODE + return new Promise((resolve) => { + const p = spawn( + ELECTRON, + [MAIN, `--user-data-dir=${userDataDir}`, 'mergetool', file, file, file], + { cwd: dir, env, stdio: 'ignore' } + ) + p.on('exit', resolve) + setTimeout(resolve, 8000) + }) +} + +test('walks thirty conflicted files without running out of room', async () => { + // Thirty Electron launches, one after another, because that is what git does. + // The default 30s is a per-test budget written for a test that drives one. + test.setTimeout(180_000) + const { dir, names, git } = bigConflict() + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + // Whatever the strip holds before the walk is the baseline; the walk must + // not add to it. + const tabsBefore = await page.locator('.tab').count() + for (const [i, name] of names.entries()) { + await mergetool(userDataDir, dir, join(dir, name)) + const view = page.locator('.merge-view') + await expect(view, `file ${i} (${name}) never opened`).toBeVisible({ timeout: 20000 }) + await expect(page.locator('.merge-file')).toHaveText(name) + // git walks one launch at a time, so the band has to say where you are. + await expect(page.getByTestId('merge-walk')).toHaveText(`${i + 1} of ${FILES}`) + + // Alternate, so the walk proves both directions and every file gets the + // answer it was given rather than a lucky default. + await page.getByTestId(i % 2 ? 'merge-take-theirs' : 'merge-take-ours').click() + await page.getByTestId('merge-save').click() + await expect(view).toHaveCount(0, { timeout: 10000 }) + } + + // Every file resolved, with the side it was told, and nothing left unmerged. + for (const [i, name] of names.entries()) { + const want = i % 2 ? `theirs ${name}` : `ours ${name}` + expect(readFileSync(join(dir, name), 'utf8'), name).toBe(`one\n${want}\nthree\n`) + } + // The index is NOT checked here: this drives `diffbro mergetool` directly, + // and it is `git mergetool` that stages a file once the tool exits 0. That + // half is covered by the host run recorded in the spec; what this proves is + // that the app survives thirty launches back to back. + void git + + // The merge is self-contained, so the walk must not have filled the + // comparison strip — the cap is sixteen and the tool would have jammed. + expect(await page.locator('.tab').count(), 'the walk opened tabs').toBe(tabsBefore) + expect(await page.locator('.notice').count(), 'a notice appeared').toBe(0) + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) diff --git a/e2e/merge-resolve.spec.mjs b/e2e/merge-resolve.spec.mjs index d0cb70bb..3348a549 100644 --- a/e2e/merge-resolve.spec.mjs +++ b/e2e/merge-resolve.spec.mjs @@ -67,23 +67,19 @@ test('resolves a real conflict and writes the merged file back', async () => { try { await runMergetool(userDataDir, dir, file) - const dialog = page.getByRole('dialog', { name: 'Resolve merge conflicts' }) - await expect(dialog).toBeVisible({ timeout: 20000 }) + const view = page.locator('.merge-view') + await expect(view).toBeVisible({ timeout: 20000 }) + // Three panes: the two commits either side of the file being produced. + await expect(page.locator('.merge-pane')).toHaveCount(3) - // The two conflicting versions are what the README promises and what the - // dialog sits over — they used to open EMPTY, because the merge route - // returned before vouching for either path. - await expect(page.locator('.slot[data-side="left"] .name')).toContainText('app.txt') - await expect(page.locator('.slot[data-side="right"] .name')).toContainText('app.txt') - - // Nothing may be written while a conflict is undecided. + // Nothing may be written while a region is still undecided. const save = page.getByTestId('merge-save') await expect(save).toBeDisabled() - await page.getByTestId('merge-theirs-0').click() + await page.getByTestId('merge-take-theirs').click() await expect(save).toBeEnabled() await save.click() - await expect(dialog).toHaveCount(0, { timeout: 10000 }) + await expect(view).toHaveCount(0, { timeout: 10000 }) // The file git is left holding: their side, no markers. const merged = readFileSync(file, 'utf8') @@ -103,10 +99,9 @@ test('declining leaves the file exactly as git left it', async () => { const page = await firstReadyPage(app) try { await runMergetool(userDataDir, dir, file) - const dialog = page.getByRole('dialog', { name: 'Resolve merge conflicts' }) - await expect(dialog).toBeVisible({ timeout: 20000 }) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) await page.getByRole('button', { name: 'Cancel' }).click() - await expect(dialog).toHaveCount(0) + await expect(page.locator('.merge-view')).toHaveCount(0) // Untouched, markers and all — the reader said no. expect(readFileSync(file).equals(before)).toBe(true) @@ -130,7 +125,7 @@ test('refuses a binary conflict instead of destroying it', async () => { try { await runMergetool(userDataDir, dir, file) await page.waitForTimeout(1500) - await expect(page.getByRole('dialog', { name: 'Resolve merge conflicts' })).toHaveCount(0) + await expect(page.locator('.merge-view')).toHaveCount(0) expect(readFileSync(file).equals(bytes)).toBe(true) } finally { await app.close().catch(() => {}) @@ -150,10 +145,508 @@ test('refuses a file with no conflict markers', async () => { try { await runMergetool(userDataDir, dir, file) await page.waitForTimeout(1500) - await expect(page.getByRole('dialog', { name: 'Resolve merge conflicts' })).toHaveCount(0) + await expect(page.locator('.merge-view')).toHaveCount(0) expect(readFileSync(file, 'utf8')).toBe('nothing to resolve\n') } finally { await app.close().catch(() => {}) rmSync(dir, { recursive: true, force: true }) } }) + +// The whole reason for the rebuild: neither side is always right, and the +// result is a real editor. +test('takes a side, then lets the reader type the answer neither side had', async () => { + const { dir, file } = conflictedRepo() + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + + // The sides come out of git's index, so they are whole files with no + // markers in them — not the fragments the old dialog showed. + const ours = page.locator('.merge-pane').first() + await expect(ours).toContainText('ours') + await expect(ours).not.toContainText('<<<<<<<') + + await page.getByTestId('merge-take-ours').click() + await expect(page.getByTestId('merge-save')).toBeEnabled() + + // Now edit the result by hand, which the resolver could not express. + const result = page.locator('.merge-pane.result .merge-editor') + await result.click() + await page.locator('.merge-pane.result textarea').waitFor() + await page.keyboard.press('ControlOrMeta+a') + await page.keyboard.type('one\nhand written\nthree\n') + await page.getByTestId('merge-save').click() + await expect(page.locator('.merge-view')).toHaveCount(0, { timeout: 10000 }) + + expect(readFileSync(file, 'utf8')).toBe('one\nhand written\nthree\n') + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +// The gutter chevrons are the interaction a merge tool is judged on: the side +// you want, moved into the result from where it sits. +test('a gutter chevron moves that side into the result', async () => { + const { dir, file } = conflictedRepo() + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + + // The result opens as a valid FILE — no markers anywhere in the view. + const result = page.locator('.merge-pane.result') + await expect(result).not.toContainText('<<<<<<<') + await expect(result).not.toContainText('=======') + + const theirChevron = page.locator('.merge-pane').last().locator('.merge-take-theirs') + await expect(theirChevron).toHaveCount(1) + await theirChevron.click() + + await expect(page.getByTestId('merge-save')).toBeEnabled() + await page.getByTestId('merge-save').click() + await expect(page.locator('.merge-view')).toHaveCount(0, { timeout: 10000 }) + expect(readFileSync(file, 'utf8')).toBe('one\ntheirs\nthree\n') + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +// The sides come out of git's INDEX, not from re-reading the markers. The +// difference shows where git auto-merged a hunk: the reconstruction carries the +// merged line into both sides, the index does not. This was silently falling +// back on macOS, where git reports /private/var for a repo under /var and the +// relative path came out full of `..`. +test('reads both sides from the index, not from the markers', async () => { + const dir = mkdtempSync(join(tmpdir(), 'diffbro-merge-idx-')) + const env = { + ...process.env, + GIT_AUTHOR_NAME: 'T', + GIT_AUTHOR_EMAIL: 't@e', + GIT_COMMITTER_NAME: 'T', + GIT_COMMITTER_EMAIL: 't@e' + } + const git = (...args) => execFileSync('git', args, { cwd: dir, env }) + const file = join(dir, 'app.txt') + git('init', '-q', '-b', 'main') + // `mode` is changed by THEM alone and git merges it without asking; `value` + // is changed by both and conflicts. The unchanged lines between them matter: + // git folds ADJACENT changes into one hunk, and then nothing is auto-merged. + writeFileSync(file, 'value: base\nkeep one\nkeep two\nkeep three\nmode: fast\n') + git('add', '.') + git('commit', '-qm', 'base') + git('checkout', '-qb', 'feature') + writeFileSync(file, 'value: theirs\nkeep one\nkeep two\nkeep three\nmode: safe\n') + git('commit', '-qam', 'theirs') + git('checkout', '-q', 'main') + writeFileSync(file, 'value: ours\nkeep one\nkeep two\nkeep three\nmode: fast\n') + git('commit', '-qam', 'ours') + try { + git('merge', 'feature') + } catch { + // Expected. + } + + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + + // Our side still says `fast`; only the auto-merged RESULT says `safe`. + await expect(page.locator('.merge-pane').first()).toContainText('mode: fast') + await expect(page.locator('.merge-pane').last()).toContainText('mode: safe') + await expect(page.locator('.merge-pane.result')).toContainText('mode: safe') + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +// The answer neither side had, typed straight in — with no button pressed after +// it. A resolver that still demands a click has not accepted the edit. +test('typing in a conflict is itself the answer to it', async () => { + const { dir, file } = conflictedRepo() + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + + const save = page.getByTestId('merge-save') + await expect(save).toBeDisabled() + + const result = page.locator('.merge-pane.result .merge-editor') + await result.click() + await page.locator('.merge-pane.result textarea').waitFor() + await page.keyboard.press('ControlOrMeta+a') + await page.keyboard.type('one\nboth of us were wrong\nthree\n') + + await expect(save).toBeEnabled() + await save.click() + await expect(page.locator('.merge-view')).toHaveCount(0, { timeout: 10000 }) + expect(readFileSync(file, 'utf8')).toBe('one\nboth of us were wrong\nthree\n') + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +// Which branch a side IS, read out of git rather than guessed. The panes said +// only "Ours" and "Theirs", which names neither commit. +test('names the branches the two sides come from', async () => { + const { dir, file } = conflictedRepo() + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + await expect(page.locator('.merge-pane').first()).toContainText('main') + await expect(page.locator('.merge-pane').last()).toContainText('feature') + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +// The grips are grid children too. Left to take column tracks of their own they +// wrapped Theirs onto a second row and cut the result pane to half the height. +test('the three panes fill one full-height row', async () => { + const { dir, file } = conflictedRepo() + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + + const row = await page.locator('.merge-panes').boundingBox() + const panes = await page.locator('.merge-pane').all() + let spanned = 0 + for (const pane of panes) { + const box = await pane.boundingBox() + expect(Math.abs(box.y - row.y)).toBeLessThan(2) + expect(Math.abs(box.height - row.height)).toBeLessThan(2) + spanned += box.width + } + // Side by side across the whole row, with no track spent on a grip. + expect(Math.abs(spanned - row.width)).toBeLessThan(4) + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +// Three fixed thirds is not enough to read a wide file in; the dividers move. +test('the panes can be widened, and the split is remembered', async () => { + const { dir, file } = conflictedRepo() + const userDataDir = freshUserDataDir() + let app = await launchApp(userDataDir) + let page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + + const ours = page.locator('.merge-pane').first() + const before = (await ours.boundingBox()).width + const grip = page.locator('.merge-grip').first() + const box = await grip.boundingBox() + await page.mouse.move(box.x, box.y + box.height / 2) + await page.mouse.down() + await page.mouse.move(box.x + 220, box.y + box.height / 2, { steps: 8 }) + await page.mouse.up() + + const after = (await ours.boundingBox()).width + expect(after).toBeGreaterThan(before + 100) + + // A drag is a decision, so it outlives the window it was made in. + await app.close() + app = await launchApp(userDataDir) + page = await firstReadyPage(app) + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + const restored = (await page.locator('.merge-pane').first().boundingBox()).width + expect(Math.abs(restored - after)).toBeLessThan(24) + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +// A conflict where OUR side DELETED the lines is an insertion point, not a +// one-line region — and Monaco cannot tell the two ranges apart. Read as a line, +// taking theirs wrote over the first stable line after the conflict and that +// line was gone from the file git was handed. +function deletedSideRepo() { + const dir = mkdtempSync(join(tmpdir(), 'diffbro-merge-del-')) + const env = { + ...process.env, + GIT_AUTHOR_NAME: 'T', + GIT_AUTHOR_EMAIL: 't@e', + GIT_COMMITTER_NAME: 'T', + GIT_COMMITTER_EMAIL: 't@e' + } + const git = (...args) => execFileSync('git', args, { cwd: dir, env }) + const file = join(dir, 'app.txt') + git('init', '-q', '-b', 'main') + writeFileSync(file, 'head\nmiddle\ntail\n') + git('add', '.') + git('commit', '-qm', 'base') + git('checkout', '-qb', 'feature') + writeFileSync(file, 'head\nthey rewrote it\ntail\n') + git('commit', '-qam', 'theirs') + git('checkout', '-q', 'main') + // We DELETED the line they rewrote: our side of the conflict is empty. + writeFileSync(file, 'head\ntail\n') + git('commit', '-qam', 'ours') + try { + git('merge', 'feature') + } catch { + // Expected. + } + return { dir, file } +} + +test('takes their side into a region ours emptied without eating the next line', async () => { + const { dir, file } = deletedSideRepo() + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + + await page.getByTestId('merge-take-theirs').click() + await page.getByTestId('merge-save').click() + await expect(page.locator('.merge-view')).toHaveCount(0, { timeout: 10000 }) + + expect(readFileSync(file, 'utf8')).toBe('head\nthey rewrote it\ntail\n') + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +// Answered once it is no longer an insertion point, so changing your mind must +// REPLACE what the first answer wrote rather than leave both in the file. +test('lets the reader change their mind about a region ours emptied', async () => { + const { dir, file } = deletedSideRepo() + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + + await page.getByTestId('merge-take-theirs').click() + await page.getByTestId('merge-take-ours').click() + await page.getByTestId('merge-save').click() + await expect(page.locator('.merge-view')).toHaveCount(0, { timeout: 10000 }) + + expect(readFileSync(file, 'utf8')).toBe('head\ntail\n') + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +// Enough unchanged lines between the two changes that git keeps them as SEPARATE +// conflicts: with only three it folds them into one hunk and the test proves +// nothing about ordering. +const KEEP = Array.from({ length: 9 }, (_, i) => `keep ${i + 1}`).join('\n') + +// Resolving the SECOND conflict first is the ordinary way a reader works, and it +// is where a stale line number would show: the earlier region must still be +// where it was, and answering it must not disturb the one already settled. +test('resolves two conflicts out of order without disturbing either', async () => { + const dir = mkdtempSync(join(tmpdir(), 'diffbro-merge-two-')) + const env = { + ...process.env, + GIT_AUTHOR_NAME: 'T', + GIT_AUTHOR_EMAIL: 't@e', + GIT_COMMITTER_NAME: 'T', + GIT_COMMITTER_EMAIL: 't@e' + } + const git = (...args) => execFileSync('git', args, { cwd: dir, env }) + const file = join(dir, 'app.txt') + git('init', '-q', '-b', 'main') + writeFileSync(file, `top\nA base\n${KEEP}\nB base\nbottom\n`) + git('add', '.') + git('commit', '-qm', 'base') + git('checkout', '-qb', 'feature') + writeFileSync(file, `top\nA theirs\n${KEEP}\nB theirs\nbottom\n`) + git('commit', '-qam', 'theirs') + git('checkout', '-q', 'main') + writeFileSync(file, `top\nA ours\n${KEEP}\nB ours\nbottom\n`) + git('commit', '-qam', 'ours') + try { + git('merge', 'feature') + } catch { + // Expected. + } + + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + + // Second conflict first: step to it, take theirs, then come back for the first. + await page.getByTestId('merge-next').click() + await page.getByTestId('merge-take-theirs').click() + await page.getByTestId('merge-prev').click() + await page.getByTestId('merge-take-ours').click() + + await page.getByTestId('merge-save').click() + await expect(page.locator('.merge-view')).toHaveCount(0, { timeout: 10000 }) + expect(readFileSync(file, 'utf8')).toBe(`top\nA ours\n${KEEP}\nB theirs\nbottom\n`) + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +// A merge tool that reformats the file it resolves is one nobody trusts twice. +// Both of these are invisible on screen and only the bytes can say. +function endingsRepo(body) { + const dir = mkdtempSync(join(tmpdir(), 'diffbro-merge-eol-')) + const env = { + ...process.env, + GIT_AUTHOR_NAME: 'T', + GIT_AUTHOR_EMAIL: 't@e', + GIT_COMMITTER_NAME: 'T', + GIT_COMMITTER_EMAIL: 't@e' + } + const git = (...args) => execFileSync('git', args, { cwd: dir, env }) + const file = join(dir, 'app.txt') + git('init', '-q', '-b', 'main') + // core.autocrlf off, so what is written is what git stores and hands back. + git('config', 'core.autocrlf', 'false') + writeFileSync(file, body('base')) + git('add', '.') + git('commit', '-qm', 'base') + git('checkout', '-qb', 'feature') + writeFileSync(file, body('theirs')) + git('commit', '-qam', 'theirs') + git('checkout', '-q', 'main') + writeFileSync(file, body('ours')) + git('commit', '-qam', 'ours') + try { + git('merge', 'feature') + } catch { + // Expected. + } + return { dir, file } +} + +test('writes a CRLF file back with CRLF', async () => { + const { dir, file } = endingsRepo((v) => `one\r\n${v}\r\nthree\r\n`) + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + await page.getByTestId('merge-take-theirs').click() + await page.getByTestId('merge-save').click() + await expect(page.locator('.merge-view')).toHaveCount(0, { timeout: 10000 }) + + const merged = readFileSync(file, 'utf8') + expect(merged).toBe('one\r\ntheirs\r\nthree\r\n') + expect(merged).not.toMatch(/[^\r]\n/) + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +test('leaves a file that ended without a newline ending without one', async () => { + const { dir, file } = endingsRepo((v) => `one\n${v}\nthree`) + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + await page.getByTestId('merge-take-theirs').click() + await page.getByTestId('merge-save').click() + await expect(page.locator('.merge-view')).toHaveCount(0, { timeout: 10000 }) + + expect(readFileSync(file, 'utf8')).toBe('one\ntheirs\nthree') + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +// The headline promise, on the region that could not keep it: our side deleted +// the lines, so there is no chevron to click and the reader types the answer. +// It stayed unresolved and Save stayed locked with the conflict visibly answered. +test('takes a typed answer in a region ours emptied', async () => { + const { dir, file } = deletedSideRepo() + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + + const save = page.getByTestId('merge-save') + await expect(save).toBeDisabled() + + const result = page.locator('.merge-pane.result .merge-editor') + await result.click() + await page.locator('.merge-pane.result textarea').waitFor() + await page.keyboard.press('ControlOrMeta+a') + await page.keyboard.type('head\nneither of you\ntail\n') + + await expect(save).toBeEnabled() + await save.click() + await expect(page.locator('.merge-view')).toHaveCount(0, { timeout: 10000 }) + expect(readFileSync(file, 'utf8')).toBe('head\nneither of you\ntail\n') + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) + +// Undo puts back the lines "Neither" removed, so the region holds them again. +// Left cached as an insertion point, the next answer was INSERTED above them and +// the file went out holding both sides. +test('answers a region again after undoing a Neither', async () => { + const { dir, file } = deletedSideRepo() + const userDataDir = freshUserDataDir() + const app = await launchApp(userDataDir) + const page = await firstReadyPage(app) + try { + await runMergetool(userDataDir, dir, file) + await expect(page.locator('.merge-view')).toBeVisible({ timeout: 20000 }) + + await page.getByTestId('merge-take-theirs').click() + const result = page.locator('.merge-pane.result .merge-editor') + await result.click() + await page.locator('.merge-pane.result textarea').waitFor() + await page.keyboard.press('ControlOrMeta+z') + await page.getByTestId('merge-take-theirs').click() + + await page.getByTestId('merge-save').click() + await expect(page.locator('.merge-view')).toHaveCount(0, { timeout: 10000 }) + + const merged = readFileSync(file, 'utf8') + expect(merged).toBe('head\nthey rewrote it\ntail\n') + } finally { + await app.close().catch(() => {}) + rmSync(dir, { recursive: true, force: true }) + } +}) diff --git a/scripts/theme-sweep.mjs b/scripts/theme-sweep.mjs index d5c42b34..0eb5208a 100644 --- a/scripts/theme-sweep.mjs +++ b/scripts/theme-sweep.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Walks a surface through all 14 themes and MEASURES it, rather than leaving a +// Walks a surface through every theme and MEASURES it, rather than leaving a // reviewer to eyeball fourteen pictures. For every theme it reads the COMPUTED // colours off the live DOM and reports the contrast of each pair that carries // meaning — which is the check docs/standards.md asks for ("assert the @@ -17,10 +17,12 @@ // still look; the exit code is what gates. import { mkdtempSync, mkdirSync, existsSync, writeFileSync } from 'node:fs' +import { execFileSync, spawn } from 'node:child_process' import { tmpdir } from 'node:os' import { join } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { generateKeyPairSync } from 'node:crypto' +import { createRequire } from 'node:module' import { _electron as electron } from '@playwright/test' /* global document, getComputedStyle -- inside page.evaluate only */ @@ -28,6 +30,9 @@ import { _electron as electron } from '@playwright/test' const ROOT = fileURLToPath(new URL('..', import.meta.url)) const MAIN = join(ROOT, 'build', 'main', 'index.js') const OUT = join(ROOT, 'docs/screenshots/themes') +// The app's OWN Electron, the way the launcher would reach it — process.execPath +// here is node, which would run the main bundle as a plain script. +const ELECTRON = createRequire(import.meta.url)('electron') const THEMES = [ 'Light', @@ -43,7 +48,13 @@ const THEMES = [ 'Bloom', 'Nyan', 'Matrix', - 'Contrast' + 'Contrast', + 'Volcano', + 'Amber', + 'Tide', + 'Ember', + 'Graphite', + 'Vector' ] // The floors the repo already runs on. Declared PER PROBE rather than guessed @@ -184,7 +195,88 @@ const LOCKFILE = (version) => 2 ) +// The merge view has no entry point inside the app: it exists because git +// launched the tool. So the sweep makes a real conflict once and drives a real +// `git mergetool` at it for each theme, cancelling out again so the file — and +// the repo — are left exactly as git wrote them. +let conflicted = null + +function mergeConflictRepo() { + if (conflicted) return conflicted + const dir = mkdtempSync(join(tmpdir(), 'diffbro-sweep-merge-')) + const env = { + ...process.env, + GIT_AUTHOR_NAME: 'T', + GIT_AUTHOR_EMAIL: 't@e', + GIT_COMMITTER_NAME: 'T', + GIT_COMMITTER_EMAIL: 't@e' + } + const git = (...args) => execFileSync('git', args, { cwd: dir, env }) + const file = join(dir, 'config.yml') + git('init', '-q', '-b', 'main') + writeFileSync(file, 'service: diff-engine\nreplicas: 3\nregion: eu-west-1\nkeep: yes\n') + git('add', '.') + git('commit', '-qm', 'base') + git('checkout', '-qb', 'feature') + writeFileSync(file, 'service: diff-engine\nreplicas: 9\nregion: eu-west-1\nkeep: yes\n') + git('commit', '-qam', 'theirs') + git('checkout', '-q', 'main') + writeFileSync(file, 'service: diff-engine\nreplicas: 5\nregion: us-east-1\nkeep: yes\n') + git('commit', '-qam', 'ours') + try { + git('merge', 'feature') + } catch { + // Expected: this is the conflict the surface is for. + } + conflicted = { dir, file } + return conflicted +} + +async function openMerge(page, userDataDir) { + const { dir, file } = mergeConflictRepo() + const env = { ...process.env } + delete env.ELECTRON_RUN_AS_NODE + await new Promise((resolve) => { + const child = spawn( + ELECTRON, + [MAIN, `--user-data-dir=${userDataDir}`, 'mergetool', file, file, file], + { cwd: dir, env, stdio: 'ignore' } + ) + child.on('exit', resolve) + setTimeout(resolve, 8000) + }) + // The launcher window has been up by now, and the merge arrives through a + // SECOND process whose focus() does not reliably raise the main window under + // the container's window manager — leaving it occluded, producing no frames, + // and hanging the screenshot rather than failing it. + await page.bringToFront() + await page.locator('.merge-view').waitFor({ timeout: 20000 }) + await page.locator('.merge-word-theirs').first().waitFor({ timeout: 10000 }) +} + const SURFACES = [ + { + name: 'merge-view', + // The chrome only. The bands and the intra-line tints inside the panes are + // Monaco decorations built from --dg-add / --dg-del / --dg-chg alone, and + // check-theme-depth already holds those three to a contrast floor AND a + // pairwise deltaE floor on every theme — measuring an overlay div that + // carries no text of its own would report a number nothing depends on. + open: (page, { userDataDir }) => openMerge(page, userDataDir), + close: async (page) => { + await page.getByRole('button', { name: 'Cancel' }).click() + await page.locator('.merge-view').waitFor({ state: 'detached' }) + }, + probes: { + 'pane label': ['.merge-pane.result .merge-head', TEXT], + // The branch a side comes from: words a reader reads, so the reading floor + // and not the hint one. + 'branch name': ['.merge-rev', TEXT], + 'conflicts left': ['.merge-count', TEXT], + 'file name': ['.merge-file', TEXT], + "result pane's own head": ['.merge-pane.result .merge-hint', TEXT] + } + }, { name: 'deps-diff', // Reached through PASTE mode, because a lockfile is recognised by its NAME @@ -309,8 +401,13 @@ const SURFACES = [ }, { name: 'status-band', - // Already on screen — the sweep leaves a structure diff loaded. - open: async (page) => page.locator('.status-band').waitFor(), + // The startup diff is gone by now: deps-diff runs first and its close is a + // Clear. Reload rather than assume — assuming is what left this surface + // timing out on every run since deps-diff was added. + open: async (page) => { + if (!(await page.locator('.status-band').count())) await loadStructureDiff(page) + await page.locator('.status-band').waitFor() + }, close: async () => {}, probes: { // These carry the numbers a reader reads, so they take the reading floor — @@ -549,8 +646,24 @@ const SURFACES = [ // The launcher is a SECOND BrowserWindow with its own card surface, and no // surface here ever opened it — so its compose panel, body overlay and name // ghost were only ever measured against the main window's ground. +// Asked of MAIN, not of the page: a hidden BrowserWindow keeps reporting its +// document as visible, so only the window itself knows. +const launcherShown = (app) => + app.evaluate(({ BrowserWindow }) => + BrowserWindow.getAllWindows().some( + (w) => w.webContents.getURL().includes('quicklook') && w.isVisible() + ) + ) + async function launcherPage(app, page) { const open = app.windows().find((w) => w.url().includes('quicklook')) + // The launcher hides itself whenever something else takes focus — which the + // merge does, arriving in the main window from a second process. A hidden + // window produces no frames, so screenshotting it HANGS rather than fails. + if (open && !(await launcherShown(app))) { + await page.evaluate(() => globalThis.api.quickLookToggle()) + for (let i = 0; i < 50 && !(await launcherShown(app)); i++) await page.waitForTimeout(100) + } if (open) return open const [ql] = await Promise.all([ app.waitForEvent('window'), @@ -561,9 +674,16 @@ async function launcherPage(app, page) { return ql } -async function sweepSurface({ app, main, surface, theme, findings }) { +// A named subset while a new surface is being built: the full walk is 20 themes +// across every surface, which is a slow way to find out a selector moved. +const chosenSurfaces = () => { + const only = process.env.SWEEP_ONLY?.split(',').filter(Boolean) + return only?.length ? SURFACES.filter((s) => only.includes(s.name)) : SURFACES +} + +async function sweepSurface({ app, main, surface, theme, findings, userDataDir }) { const page = surface.window === 'launcher' ? await launcherPage(app, main) : main - await surface.open(page) + await surface.open(page, { userDataDir }) await page.waitForTimeout(120) await page.screenshot({ path: join(OUT, `${surface.name}-${theme.toLowerCase()}.png`) }) @@ -619,8 +739,8 @@ async function main() { for (const theme of THEMES) { await setTheme(page, theme) - for (const surface of SURFACES) { - await sweepSurface({ app, main: page, surface, theme, findings }) + for (const surface of chosenSurfaces()) { + await sweepSurface({ app, main: page, surface, theme, findings, userDataDir }) } process.stdout.write(` ${theme.padEnd(9)} ✓\n`) } diff --git a/specs/2026-08-10-three-way-merge-view/plan.md b/specs/2026-08-10-three-way-merge-view/plan.md new file mode 100644 index 00000000..f710bac5 --- /dev/null +++ b/specs/2026-08-10-three-way-merge-view/plan.md @@ -0,0 +1,245 @@ +# A real three-way merge view + +| | | +| --------------------------------------- | --------------------------- | +| **Status** | in-progress | +| **Progress** | 9 / 9 steps | +| **Branch** | `feat/three-way-merge-view` | +| **Started** | 2026-08-10 | +| **Finished** | — | +| **Bugs found and fixed this iteration** | 0 | +| **Token baseline** | — | +| **Claude tokens used** | — | + +## Problem + +v0.4.29 shipped a merge **resolver**, not a merge **view**. One card per conflict +with four buttons, no line-level context, no navigation, no editing. Asked to +resolve `config.yml`, it shows two blocks of three lines and offers Ours / +Theirs / Both / Neither. + +That was my scoping call — `specs/2026-08-09-developer-workflow/plan.md` records +"three-way merge as a full editor | out" as a decision — and it is the wrong one. +The reference every developer has is JetBrains: three panes, the result in the +middle, per-hunk chevrons in the gutters, and the result editable by hand when +neither side is right. A merge tool that cannot express "take theirs, then fix +the indentation" sends you to another editor at exactly the moment it promised +not to. + +Two smaller failures fall out of the same design: + +- **The ancestor is thrown away.** `mergeConflicts.js` parses a diff3 base when + git happens to write one, and nothing renders it. Which side _changed_ is the + first question in a merge and the view cannot answer it. +- **Markers are the wrong source.** Parsing `<<<<<<<` out of `$MERGED` is + reconstructing what git already holds. The index carries the three inputs + cleanly, verified on a real conflict: + + ``` + $ git show :1:config.yml # base → replicas: 3, region: eu-west-1, timeout: 30 + $ git show :2:config.yml # ours → replicas: 5, region: us-east-1, timeout: 30 + $ git show :3:config.yml # theirs → replicas: 9, region: eu-west-1, timeout: 90 + ``` + +## Solution + +**Three Monaco panes: Ours │ Result │ Theirs, with the result editable.** + +Inputs come from the index (`:1:`, `:2:`, `:3:`), so all three texts arrive +without a marker in them. Conflict regions are computed from those three rather +than scraped, which is also what makes the ancestor available. + +Monaco ships **no three-way merge editor** — VS Code's is internal and not part +of `monaco-editor` — so it is composed from three ordinary editors plus +decorations. That is the central cost of this proposal and the main risk. + +| option | why not | +| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| keep the dialog, add a diff pane inside it | the dialog is the wrong container: a merge needs the window, and `BaseDialog` is capped for a reason | +| two stacked `createDiffEditor`s sharing the centre model | Monaco's revert arrow reverts _modified → original_, so "accept theirs" would need Theirs on the left of its own pane. The layout fights the widget | +| VS Code's merge editor | not exposed by `monaco-editor`. Vendoring it is a fork, not a dependency | +| keep parsing markers | the index is the source of truth, and markers cannot give the ancestor unless the user set `merge.conflictStyle=diff3` | +| a new dependency for three-way merge | rule 2. The algorithm is already in `mergeConflicts.js`; what is missing is a view | + +## Scope + +**In:** + +- `readStageArgs(stage, path)` in `gitRepo.js` — `:1:`/`:2:`/`:3:` from the + index. `stage` is a literal the app chooses, never renderer input +- three-way region model from three texts (extends `mergeConflicts.js`, keeps + the marker parser as the fallback) +- `MergeView.vue` — full-area, replacing the dialog: three panes, scroll-synced +- glyph-margin actions per region: take ours · take theirs · both · neither +- an EDITABLE result pane (`useMonacoInput` already does this) +- next/previous conflict, and a status band counting what is left +- the save guard changes — see Decisions + +**Out:** + +- word-level merge within a line — hunk granularity, as JetBrains defaults to +- editing Ours or Theirs; they are what the two commits said +- resolving a binary conflict, which stays refused +- a merge that is not a `git mergetool` launch; nothing gains a "merge two + arbitrary files" entry point + +## Design + +Chrome is tokens; the panes are Monaco, themed through `applyMonacoTheme` as the +diff view already is. Region tints use `--dg-add` / `--dg-del` / `--dg-chg`, +which `check-theme-depth` already holds to a contrast floor **and** a pairwise +ΔE floor across all 20 themes — the same three the diagram view leans on. + +``` +┌ band ──────────────────────────────────────────────────────────────┐ +│ config.yml ‹ prev next › 2 of 3 resolved Save │ +├──────────────────┬──────────────────────────┬──────────────────────┤ +│ Ours (read-only) │ Result (EDITABLE) │ Theirs (read-only) │ +│ │ │ │ +│ replicas: 5 » │ replicas: 5 │ « replicas: 9 │ +│ region: us-east» │ region: us-east-1 │ « region: eu-west-1 │ +│ timeout: 30 » │ timeout: 90 │ « timeout: 90 │ +│ │ ^ hand-edited │ │ +└──────────────────┴──────────────────────────┴──────────────────────┘ +``` + +`»` and `«` are glyph-margin actions, not text — `` geometry rendered +into Monaco's glyph margin, so nothing tofus. + +### Theme verdict — all 20 + +The panes are Monaco surfaces, so the question is what the DECORATIONS compose +against Monaco's themed ground, not what a `.css` background does. Values parsed +from `styles/themes.css` + `tokens.css`. + +`--dg-add` is `--success-text`; `--dg-del` is `color-mix(--danger-border 70%, +--text)` — that mix exists because nord's raw danger token scored 2.24; `--dg-chg` +is `--warning-border`, overridden on nord, contrast and vector. + +| theme | ground | verdict | note | +| -------- | --------- | ------- | ------------------------------------------------------- | +| light | `#ffffff` | ok | region tints at 14%, the step the grid already uses | +| dark | `#0d1117` | ok | | +| solar | `#fffdf6` | ok | | +| neon | `#090d18` | ok | accent only on the focus ring | +| nord | `#2e3440` | ok | `--dg-del`'s mix is why this passes at all | +| sepia | `#e9dcbe` | ok | | +| dim | `#1b1917` | ok | | +| beacon | `#000000` | ok | hard keyline `#e0e0e0` — pane dividers keep `--border` | +| meridian | `#f5f7f4` | ok | | +| linen | `#faf7f0` | ok | | +| bloom | `#f9f4f5` | ok | | +| nyan | `#160a20` | ok | accent `#ff2ecb`, no glow, no accent fill under a label | +| matrix | `#020a04` | ok | accent `#00ff41`, same | +| contrast | `#ffffff` | ok | hard keyline `#111111`; `--dg-chg` overridden here | +| volcano | `#000000` | ok | | +| amber | `#0f0a02` | ok | | +| tide | `#0b1a1e` | ok | | +| ember | `#1a1013` | ok | | +| graphite | `#161616` | ok | achromatic ground, semantic tints carry the meaning | +| vector | `#ffffff` | ok | `--dg-chg` overridden here | + +**This table is not the check.** The new surface goes into `theme-sweep`'s +`SURFACES` and is read off real frames, because the audit found the last two new +surfaces failing the reading floor on 14 of 20 themes with a table just like this +one saying "ok". + +## Security rules touched + +Nothing new is opened. + +- **The write is unchanged.** `merge:write` already takes TEXT and main already + holds `$MERGED` from launch, so an editable pane needs no new IPC and no new + argument. This is why "freely editable" costs nothing here: the renderer could + already send arbitrary text. +- **Rule 7.** `readStageArgs` is one more argv builder inside the existing fence: + fixed argv, no shell, `--end-of-options`, the same hardening. `stage` is one of + three integers chosen by the app — it is never a string from anywhere. +- **Rule 6.** Three texts from the index are still untrusted input; the existing + binary refusal applies to each, and the region model is capped. + +## Test plan + +- **unit** — `mergeThreeWay.test.js`: regions from three texts; a region only one + side changed (auto-mergeable, not a conflict); both sides identical; one side + deleted; CRLF preserved; the marker parser still used when stages are absent +- **unit** — `gitRepo.test.js`: `readStageArgs` refuses a stage outside 1–3 and + builds `:2:path` with the path fenced as before +- **unit** — `useMergeRegions` composable: next/prev wrapping, region → line + range after an edit shifts the text +- **e2e** — `merge-resolve.spec.mjs` (11): a real conflict through + `git mergetool`; take a side, take from a gutter chevron, TYPE the answer, + save, assert the bytes on disk. Plus: the sides come from the index and not the + markers · the branches are named · the three panes fill one full-height row · + the split is draggable and remembered · declining leaves the file untouched · + a binary or marker-free file is refused +- **e2e** — `merge-many.spec.mjs`: a 30-file conflict walked to the end +- **theme-sweep** — a `merge-view` entry in `SURFACES` + +## Docs impact + +| surface | needed? | what changes | +| --------------------------------------- | ------- | -------------------------------------------------------- | +| `README.md` | **yes** | the Merge conflicts row describes a dialog | +| `docs/screenshots/*.png` | **yes** | a new full-area view is a new frame | +| `docs/roadmap.md` + `brand/roadmap.svg` | **yes** | Developer workflow gains a row | +| `docs/security.md` | **yes** | one line: the result is editable, the write is not wider | +| `docs/standards.md` | **no** | no new convention | + +## Implementation plan + +- [x] 1. `readStageArgs` + the index reader, with the marker parser as fallback +- [x] 2. Regions from the markers, both SIDES from the index — see Decisions +- [x] 3. Region ↔ line mapping under edits, via Monaco's own decoration ranges +- [x] 4. `MergeView.vue` shell: three panes, scroll sync, status band, walk counter +- [x] 5. Glyph-margin chevrons, region decorations, intra-line word tints, ruler marks +- [x] 6. The editable result; typing inside a region answers it +- [x] 7. Retire `MergeDialog`; route the merge session to the view +- [x] 8. `SURFACES` entry + measured on all 20 (1260 measurements, green) +- [x] 9. e2e, docs, roadmap + SVG + +## Decisions + +| date | decision | why | rejected | +| ---------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| 2026-08-10 | the result pane is freely editable | the user's call, asked and answered. "Take theirs, then fix the indentation" is the case a resolver cannot express | read-only result composed from choices | +| 2026-08-10 | **the save guard changes meaning** | with free editing, "every region answered" is no longer the truth — a hand-edited result can be complete with regions never touched, and a fully-answered one can still have markers typed back in. Save blocks on MARKERS PRESENT in the result, and warns (does not block) on untouched regions | keeping `unresolvedCount` as the gate, which would be wrong in both directions | +| 2026-08-10 | inputs come from the index, markers are the fallback | the index is what git actually holds, and it is the only way to get the ancestor without the user having set `merge.conflictStyle=diff3` | marker parsing only | +| 2026-08-10 | compose three editors rather than adopt a merge widget | Monaco exposes none; VS Code's is internal | vendoring VS Code's merge editor | +| 2026-08-11 | **regions come from the MARKERS; only the two SIDES come from the index** | a deviation from "regions computed from three texts". git already decided where the conflicts are — recomputing them from three texts would be a second, disagreeing opinion, and where it disagreed the view would offer a choice git will not accept. The index still supplies both sides as whole marker-free files, which is what the panes needed it for | a three-way region model recomputed in the renderer | +| 2026-08-11 | **the save guard is explicit region state, not a marker scan** | with no markers anywhere in the result there is nothing left to scan for. A region is unresolved until it is answered — by a button, a chevron, or an edit landing inside it | blocking on `<<<<<<<` present in the result | +| 2026-08-11 | typing inside a region resolves it | JetBrains' model, and the hole the button-only guard left: a reader who typed the whole answer still had to press something before Save unlocked | requiring an explicit "keep this" action | +| 2026-08-11 | the ancestor is read but not shown | `:1:` arrives in the payload and the store holds it; a fourth pane is a different design question and three panes is what JetBrains defaults to | a base pane in this change | +| 2026-08-11 | pane widths are FRACTIONS with five grid tracks | the grips are grid children as much as the panes are; three tracks for five children wrapped the last pane onto a row of its own, which is what "the result pane is glued to the bottom" was | pixel widths, or absolutely-positioned grips | + +## Validation + +- [x] `/validate` — folded into `/audit` below +- [x] `npm run check` — green, with the coverage floors RAISED (94/87/95/95 → + 95/88/95/96) rather than met: `useMergePanes.js` was 4.85% covered, so its + testable core moved to `mergePaneOps.js` and is now driven by fakes +- [x] `/audit` — two agents. Between them and a read of my own, **nine** defects, + four of them data loss. Every one is fixed and guarded except the last: 1. an empty region read as a one-line one, so taking a side ate the stable + line after it — found by reading, e2e-guarded 2. the same region answered twice ate the line below, because Monaco grows + a decoration around inserted text differently from this convention. + The unit test passed the first fix; only real Monaco caught it 3. undo left the cached emptiness disagreeing with the text, so the next + answer went in ABOVE the restored lines. Emptiness is no longer cached 4. typing into an emptied region never unlocked Save, and that region has + no chevron to click instead 5. F7/Shift+F7 never reached the view — Monaco owns both keys 6. no chevrons, bands or word tints at all in a CRLF repository 7. the index stage reads had no size cap and no binary sniff 8. `mergeInputsFor` had no `.catch()`, leaving `git mergetool` blocked 9. **still open, pre-existing**: a file whose ordinary text contains + marker-shaped lines (a doc about conflicts) has them parsed as real + conflicts, and answering them drops one side. Inherent to every + marker-based merge tool, and true on `main` today — filed, not fixed + here +- [x] read on all 20 themes off real frames — `make theme-sweep`, 1260 + measurements, green. Getting there fixed two pre-existing breaks in the + sweep itself: `status-band` had been timing out since `deps-diff` was added + ahead of it and cleared the comparison, and a launcher hidden by something + else taking focus HANGS the screenshot rather than failing it +- [x] Windows still unverified — no host. Said, not implied. E2E ran on macOS + only; `e2e/quick-look-window-recovery.spec.mjs` is darwin-gated and was not + exercised this round + +**Outcome:** shipped. The merge is a view rather than a resolver: three panes, +the middle one a real editor, sides named by branch and read from the index. +Two known limitations, both stated in the UI or here rather than hidden — a +mixed-ending file is normalised on save (Monaco keeps one EOL per model, and the +band says so before anything is written), and item 9 above. diff --git a/src/main/cliRoute.js b/src/main/cliRoute.js index 38adf941..6d376caa 100644 --- a/src/main/cliRoute.js +++ b/src/main/cliRoute.js @@ -12,7 +12,9 @@ import { ensureMainWindow } from './quickLook' import { allowCliPath } from './files' import { fileAtRevision, isRevisionSide, REVISION_ERROR_KEYS } from './gitCliFiles' import { beginMerge, cancelMerge, writeMerged } from './mergeSession' +import { mergeInputsFor } from './mergeInputs' import { readFileSync } from 'node:fs' +import { basename } from 'node:path' import { hasConflictMarkers, isBinaryBuffer } from './mergeGuards' import { t } from './i18n' @@ -109,11 +111,15 @@ function routeMerge(command) { return } beginMerge(command) - // Both sides are files main just vouched for; without this file:read refuses - // them and the two panes open empty. - allowCliPath(command.local) - allowCliPath(command.remote) - deliver({ name: 'merge', local: command.local, remote: command.remote, content }) + // A NAME for the band, never a path: the renderer must not be able to learn, + // or name, the file main is going to write. + const base = { name: 'merge', fileName: basename(command.merged), content } + mergeInputsFor(command.merged) + .then((inputs) => deliver({ ...base, ...inputs })) + // Without this the renderer is never told, no sentinel is written, and the + // terminal that ran `git mergetool` waits forever on a decision nobody was + // asked for. The markers alone are enough to resolve it. + .catch(() => deliver(base)) } // The verbs that need something done before the renderer hears about them. diff --git a/src/main/dataFiles.js b/src/main/dataFiles.js index 66892eee..cc5590c6 100644 --- a/src/main/dataFiles.js +++ b/src/main/dataFiles.js @@ -17,6 +17,7 @@ export const STORE_NAMES = [ 'email', // mail hand-off defaults — features/email/emailStore.js 'onboarding', // tour progress — features/onboarding/onboardingStore.js 'sidebar', // sidebar width — composables/useSidebarResize.js + 'mergePanes', // merge pane split — features/merge/useMergePaneSizes.js 'tools' // pinned tools — features/tools/toolsStore.js ] diff --git a/src/main/gitRepo.js b/src/main/gitRepo.js index 0e484fdc..9b2b3308 100644 --- a/src/main/gitRepo.js +++ b/src/main/gitRepo.js @@ -64,6 +64,45 @@ export function resolveRevisionArgs(rev) { ] } +// A conflicted file is in the index three times. The stage is one of three +// integers the app chooses — never a string from anywhere — so it needs no +// pattern, only a range. +const STAGES = [1, 2, 3] + +/** Every file still conflicted, in git's own order. */ +export function unmergedArgs() { + return [...HARDENING, 'diff', '--name-only', '--diff-filter=U', '--no-color'] +} + +/** + * Read one stage of a conflicted file out of the index: 1 ancestor, 2 ours, + * 3 theirs. Cleaner than scraping markers, and the only way to the ancestor. + */ +export function readStageArgs(stage, path) { + if (!STAGES.includes(stage)) throw new Error('unsafe-stage') + const safe = repoPath(path) + if (!safe) throw new Error('unsafe-path') + return [...HARDENING, 'show', '--end-of-options', `:${stage}:${safe}`] +} + +// The two refs a merge in progress puts on disk. Literals the app chooses, so +// the allowlist is the whole check. +const MERGE_REFS = ['HEAD', 'MERGE_HEAD'] + +/** What branch a ref names, so a pane can say `Ours · main` rather than `Ours`. */ +export function revisionNameArgs(ref) { + if (!MERGE_REFS.includes(ref)) throw new Error('unsafe-ref') + return [ + ...HARDENING, + 'name-rev', + '--name-only', + '--refs=refs/heads/*', + '--no-undefined', + '--end-of-options', + ref + ] +} + /** Read one file as it stood at a revision. */ export function readBlobArgs(rev, path) { const safe = repoPath(path) diff --git a/src/main/mergeInputs.js b/src/main/mergeInputs.js new file mode 100644 index 00000000..25132eec --- /dev/null +++ b/src/main/mergeInputs.js @@ -0,0 +1,85 @@ +// What the view needs about a conflict, read out of git rather than out of the +// working copy's markers: the three stages the index already holds, the names +// the two sides go by, and where this file sits in the walk. +import { realpathSync } from 'node:fs' +import { dirname, relative } from 'node:path' +import { readStageArgs, repoRootArgs, revisionNameArgs, runGitIn, unmergedArgs } from './gitRepo' +import { walkPosition } from './mergeSession' +import { isBinaryBuffer } from './mergeGuards' + +// The same ceiling an opened file gets. A stage read has no other bound — +// execFile's maxBuffer is 64 MB — and three of them are decoded, sent over IPC +// and set as Monaco models, with the launcher already waiting on a decision. +const MAX_STAGE_BYTES = 32 * 1024 * 1024 + +// A stage that is too big, or not text, sends the view back to reconstructing +// the sides from the markers — which is the designed fallback, not a failure. +const usableStage = (res) => + res.ok && + Buffer.byteLength(res.stdout, 'utf8') <= MAX_STAGE_BYTES && + !isBinaryBuffer(Buffer.from(res.stdout, 'utf8')) + +// A branch name and nothing else: `name-rev` answers `main~2` for a commit that +// is not a branch tip, which names no side and is worse than saying nothing. +const BRANCH = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/ + +const real = (path) => { + try { + return realpathSync(path) + } catch { + return path + } +} + +async function branchName(ref, dir) { + const res = await runGitIn(revisionNameArgs(ref), dir) + const name = res.ok ? res.stdout.trim() : '' + return BRANCH.test(name) ? name : '' +} + +/** + * `Ours · main` beside `Theirs · feature`, when git can say. A rebase or a + * cherry-pick writes no MERGE_HEAD, so both come back empty and the panes keep + * their plain labels rather than claiming a branch that is not the one. + */ +async function revisionNames(dir) { + const [oursName, theirsName] = await Promise.all([ + branchName('HEAD', dir), + branchName('MERGE_HEAD', dir) + ]) + return { oursName, theirsName } +} + +/** + * The three inputs git already holds. Absent — a mergetool invoked by hand, a + * file with no index entry — the view falls back to reconstructing the sides + * from the markers themselves. + * @returns {Promise} merged into the renderer's payload + */ +export async function mergeInputsFor(merged) { + const root = await runGitIn(repoRootArgs(), dirname(merged)) + if (!root.ok || !root.stdout.trim()) return {} + const dir = root.stdout.trim() + // Through realpath first: on macOS a repo under /var is reported by git as + // /private/var, and relative() between the two forms yields a path full of + // `..` that the fence then rejects — the index read failed silently and the + // view fell back to reconstructing the sides from the markers. + const rel = relative(real(dir), real(merged)) + try { + const [base, ours, theirs] = await Promise.all( + [1, 2, 3].map((stage) => runGitIn(readStageArgs(stage, rel), dir)) + ) + if (!usableStage(ours) || !usableStage(theirs)) return {} + const [unmerged, names] = await Promise.all([runGitIn(unmergedArgs(), dir), revisionNames(dir)]) + const files = unmerged.ok ? unmerged.stdout.split('\n').filter(Boolean).length : 0 + return { + ours: ours.stdout, + theirs: theirs.stdout, + base: usableStage(base) ? base.stdout : null, + ...names, + ...walkPosition(files, merged) + } + } catch { + return {} + } +} diff --git a/src/main/mergeSession.js b/src/main/mergeSession.js index 48f69afb..d4a4ac79 100644 --- a/src/main/mergeSession.js +++ b/src/main/mergeSession.js @@ -5,6 +5,31 @@ import { writeFileSync } from 'node:fs' let pending = null +// `git mergetool` walks the conflicted files one at a time, in separate +// launches. Only main survives between them, so the walk is remembered here. +// +// Counting the files main has SEEN, rather than what git still calls +// unmerged: a file the reader declined stays unmerged, and a tool invoked +// directly never stages anything, so the remaining count is not a position. +let walk = { total: 0, seen: new Set() } + +/** + * Where this file sits in the walk. A file arriving twice means a new walk over + * the same conflicts. + * @param {number} unmerged how many files git still calls conflicted + * @param {string} merged the file this launch is for + */ +export function walkPosition(unmerged, merged) { + if (!walk.total || walk.seen.has(merged)) walk = { total: unmerged || 1, seen: new Set() } + walk.seen.add(merged) + return { position: walk.seen.size, total: Math.max(walk.total, walk.seen.size) } +} + +/** A fresh walk next time — used when a run ends. */ +export function walkReset() { + walk = { total: 0, seen: new Set() } +} + /** * The file the launcher watches. Comparing $MERGED's own size and timestamp was * not enough: a resolution that writes the same bytes back changes neither, and diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index 5c653775..168e9463 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -11,11 +11,12 @@ import { useSnippetDiffSync } from './composables/useSnippetDiffSync' import { usePasteShortcut } from './composables/usePasteShortcut' import { useSessionPersistence } from './composables/useSessionPersistence' import { useTourCommands } from './composables/useTourCommands' -import FileSlot from './components/FileSlot.vue' +import FileSlotsRow from './components/FileSlotsRow.vue' import DiffViewer from './components/DiffViewer.vue' import SpreadsheetDiffViewer from './components/SpreadsheetDiffViewer.vue' import DiagramDiffViewer from './components/DiagramDiffViewer.vue' import DepsDiffViewer from './components/DepsDiffViewer.vue' +import { MergeView, useMergeStore } from './features/merge' import StructureDiffViewer from './components/StructureDiffViewer.vue' import StreamedDiffViewer from './components/StreamedDiffViewer.vue' import SupportedFormats from './components/SupportedFormats.vue' @@ -33,15 +34,15 @@ import SavedDiffs from './components/SavedDiffs.vue' import DiffTabBar from './components/DiffTabBar.vue' import { useTabsStore } from './stores/tabsStore' import FormatHintBanner from './components/FormatHintBanner.vue' -import AppIcon from './components/AppIcon.vue' import DiskChangeNotice from './components/DiskChangeNotice.vue' import { useSnippetStore } from './stores/snippetStore' import { useDiagramWarmup } from './composables/useDiagramWarmup' -import { MOD, isMac } from './keys' +import { isMac } from './keys' import { useUiStore } from './stores/uiStore' import { hasStatusBand } from './utils/viewChrome' const store = useDiffStore() +const merge = useMergeStore() const ui = useUiStore() const imageExport = useImageExportStore() const tabs = useTabsStore() @@ -137,35 +138,14 @@ useSnippetDiffSync() v-if="settings.theme === 'matrix' && !store.ready && store.mode !== 'paste'" fill /> -
-
- -
- -
- -
-
- - + + + + + +