From 73bbdcd882454da6624291c8de426ce339012851 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 12 Nov 2026 17:40:05 +1100 Subject: [PATCH 1/4] fix(staged): underline search matches instead of filling them Match highlighting was drawn by three unrelated rules in three different yellows. In project search the matched word sat in the faint label colour under a mustard wash, which made it the least readable text on the row. The diff viewer's light values followed the operating system's colour scheme rather than the diff theme. Implement the "Underline" option from the search highlighting note. A match now keeps its surface and gains a solid amber line beneath it, and only the current match carries a fill. - Replace the `--search-match-bg` token with `--search-match-accent` (`theme.search.matchBg` becomes `matchAccent`). The current-match background becomes a wash of the same amber. - Project search draws the matched word in the primary text colour over the line. - Note and session find, and the diff viewer, read the theme tokens instead of hard-coded yellows. The diff viewer's `prefers-color-scheme` override is removed. - Update the differ app's static defaults to the renamed token. It shares the diff viewer and the theme module. The command palette from #1061 is not on this branch. It still reads `--search-match-bg` and needs to adopt the new token once this branch meets main. Signed-off-by: Matt Toohey Co-Authored-By: Claude Fable 5.2 --- apps/differ/src/app.css | 4 ++-- apps/staged/src/app.css | 13 ++++++----- .../projects/ProjectMatchReason.svelte | 10 ++++++--- apps/staged/src/lib/theme.ts | 16 ++++++-------- .../src/lib/components/DiffViewer.svelte | 22 ++++++------------- 5 files changed, 31 insertions(+), 34 deletions(-) diff --git a/apps/differ/src/app.css b/apps/differ/src/app.css index edb09ea33..47dedda79 100644 --- a/apps/differ/src/app.css +++ b/apps/differ/src/app.css @@ -30,8 +30,8 @@ --diff-range-border: #524d58; --diff-comment-highlight: rgba(88, 166, 255, 0.5); - --search-match-bg: rgba(250, 200, 50, 0.35); - --search-current-match-bg: rgba(255, 150, 50, 0.5); + --search-match-accent: #f2c744; + --search-current-match-bg: rgba(242, 199, 68, 0.3); --annotation-overlay-bg: rgba(0, 0, 0, 0.5); --annotation-border: rgba(255, 255, 255, 0.1); diff --git a/apps/staged/src/app.css b/apps/staged/src/app.css index 9a69fb83d..056c29f52 100644 --- a/apps/staged/src/app.css +++ b/apps/staged/src/app.css @@ -92,8 +92,8 @@ --diff-comment-border: rgba(167, 139, 250, 0.56); --diff-comment-accent: #a78bfa; - --search-match-bg: rgba(250, 200, 50, 0.35); - --search-current-match-bg: rgba(255, 150, 50, 0.5); + --search-match-accent: #f2c744; + --search-current-match-bg: rgba(242, 199, 68, 0.3); --annotation-overlay-bg: rgba(0, 0, 0, 0.5); --annotation-border: rgba(255, 255, 255, 0.1); @@ -271,13 +271,16 @@ svg.lucide:not(.animate-spin), /* ── In-content search highlights ── */ mark.search-match { - background-color: rgba(255, 235, 59, 0.4); - border-radius: 2px; + background-color: transparent; color: inherit; + text-decoration: underline 2px var(--search-match-accent); + text-underline-offset: 3px; + text-decoration-skip-ink: none; } mark.search-match-current { - background-color: rgba(255, 152, 0, 0.6); + background-color: var(--search-current-match-bg); + border-radius: 2px 2px 0 0; } /* ── Hashtag badges (shared across all badge contexts) ── */ diff --git a/apps/staged/src/lib/features/projects/ProjectMatchReason.svelte b/apps/staged/src/lib/features/projects/ProjectMatchReason.svelte index b68900a87..0ac5c8fa1 100644 --- a/apps/staged/src/lib/features/projects/ProjectMatchReason.svelte +++ b/apps/staged/src/lib/features/projects/ProjectMatchReason.svelte @@ -84,10 +84,14 @@ text-overflow: ellipsis; } + /* No fill: the match reads in the primary text colour over a warm line. + The offset keeps the line inside the label's 14px line box, which clips. */ .match-label mark { - background-color: var(--search-match-bg); - border-radius: 2px; - color: inherit; + background-color: transparent; + color: var(--text-primary); + text-decoration: underline 1.5px var(--search-match-accent); + text-underline-offset: 1.5px; + text-decoration-skip-ink: none; } .match-more { diff --git a/apps/staged/src/lib/theme.ts b/apps/staged/src/lib/theme.ts index 34ca8f3bc..51c97a8da 100644 --- a/apps/staged/src/lib/theme.ts +++ b/apps/staged/src/lib/theme.ts @@ -70,7 +70,7 @@ export interface Theme { // Search highlight colors search: { - matchBg: string; // Background for search matches + matchAccent: string; // Line under a search match currentMatchBg: string; // Background for current/focused match }; @@ -510,13 +510,11 @@ export function createAdaptiveTheme( }, search: { - // Search yellow derived from foreground warmth — mix fg toward warm tone - matchBg: overlay(mix(syntaxFg, isDark ? '#fac832' : '#e8b000', 0.7), isDark ? 0.35 : 0.5), - // Current match: warmer/more saturated variant of above - currentMatchBg: overlay( - mix(syntaxFg, isDark ? '#ff9632' : '#d07000', 0.7), - isDark ? 0.5 : 0.6 - ), + // A match keeps its surface and gains a warm line beneath it. Solid, so + // it reads the same on plain, hovered and selected rows. + matchAccent: isDark ? '#f2c744' : '#a87400', + // Only the current match is filled, in a wash of the same amber. + currentMatchBg: overlay(isDark ? '#f2c744' : '#f2b600', isDark ? 0.3 : 0.35), }, annotation: { @@ -632,7 +630,7 @@ export function themeToVarMap(t: Theme): Record { '--diff-comment-border': t.diff.commentBorder, '--diff-comment-accent': t.diff.commentAccent, - '--search-match-bg': t.search.matchBg, + '--search-match-accent': t.search.matchAccent, '--search-current-match-bg': t.search.currentMatchBg, '--annotation-overlay-bg': t.annotation.overlayBg, diff --git a/packages/diff-viewer/src/lib/components/DiffViewer.svelte b/packages/diff-viewer/src/lib/components/DiffViewer.svelte index a8c5928fa..c83fab1cc 100644 --- a/packages/diff-viewer/src/lib/components/DiffViewer.svelte +++ b/packages/diff-viewer/src/lib/components/DiffViewer.svelte @@ -3352,23 +3352,15 @@ } /* Search result highlighting */ - .search-match { - background-color: rgba(250, 200, 50, 0.35); - border-radius: 2px; - } - + .search-match, .search-current { - background-color: rgba(255, 150, 50, 0.5); - border-radius: 2px; + text-decoration: underline 2px var(--search-match-accent, #f2c744); + text-underline-offset: 3px; + text-decoration-skip-ink: none; } - @media (prefers-color-scheme: light) { - .search-match { - background-color: rgba(250, 200, 50, 0.5); - } - - .search-current { - background-color: rgba(255, 150, 50, 0.6); - } + .search-current { + background-color: var(--search-current-match-bg, rgba(242, 199, 68, 0.3)); + border-radius: 2px 2px 0 0; } From 9b2379a0b4280e0b61ffc8b31c731b9f0d5cef13 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 12 Nov 2026 17:42:20 +1100 Subject: [PATCH 2/4] fix(staged): underline command palette matches The command palette from #1061 filled its matches with `--search-match-bg`. The underline restyle of search highlighting removed that token, so once the two met, a palette match in a project name, repo path or repo alias had no styling at all. Give those matches the treatment project search uses in `ProjectMatchReason.svelte`: no fill, the matched run in the primary text colour, and a solid line in `--search-match-accent` beneath it. The repo path and alias sit in muted colours, so the primary colour lifts the match out of them the same way it does for the match reason on the same line. The project name is already primary and only gains the line. No other reader of `--search-match-bg` or `search.matchBg` remains after merging main. `--search-current-match-bg` is still defined and still drives the current match in note find, session find and the diff viewer. Not rendered: the change was made without running the tests or the screenshot harness. Signed-off-by: Matt Toohey Co-Authored-By: Claude Fable 5.2 --- .../features/commandPalette/CommandPaletteRow.svelte | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/staged/src/lib/features/commandPalette/CommandPaletteRow.svelte b/apps/staged/src/lib/features/commandPalette/CommandPaletteRow.svelte index 5618bae0c..d02b77742 100644 --- a/apps/staged/src/lib/features/commandPalette/CommandPaletteRow.svelte +++ b/apps/staged/src/lib/features/commandPalette/CommandPaletteRow.svelte @@ -240,12 +240,18 @@ min-width: 0; } + /* The same treatment as ProjectMatchReason, which draws the reason beside + these: no fill, the match in the primary text colour over a warm line. + That lifts a match out of the muted repo line and leaves the name as it + is. The offset keeps the line inside the line boxes, which clip. */ .row-name mark, .row-alias mark, .row-repo mark { - background-color: var(--search-match-bg); - border-radius: 2px; - color: inherit; + background-color: transparent; + color: var(--text-primary); + text-decoration: underline 1.5px var(--search-match-accent); + text-underline-offset: 1.5px; + text-decoration-skip-ink: none; } .row-badges { From 1f9824ae3f4e6ca315fb1c386a688452fd179065 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Fri, 13 Nov 2026 11:03:52 +1100 Subject: [PATCH 3/4] fix(staged): write search underlines as longhands and share the label rule The underline restyle of search highlighting (#1063) wrote the match underline as `text-decoration: underline 2px var(--search-match-accent)`. WebKit before Safari 26.2 rejects a thickness inside that shorthand, and because the value carries a `var()` the build passes it through unchanged, so on those engines the whole declaration is dropped and a match has no styling at all. The desktop app runs on the system WebKit and the Tauri config sets no minimum macOS version. - Write the underline as `text-decoration-line`, `-thickness` and `-color` longhands in every rule. Safari has accepted these since 12.1. - Replace the two identical scoped rules in `ProjectMatchReason.svelte` and `CommandPaletteRow.svelte` with one global `mark.label-match` rule in `app.css`, next to `mark.search-match`. The branch exists because three highlight rules drifted apart; two more copies could drift the same way. - Pull the palette's alias-pill underline up to a 0.5px offset. At 1.5px it sat flush on the pill's bottom border and read as part of it. - Size the diff viewer's underline offset in `em`. At small `--code-font-size` values in differ the fixed 3px offset put the line into the row below. - Tighten the palette tests to `mark.label-match`, since the class is now the only thing that styles a palette match. Verified with `pnpm -s check`, Prettier, and the palette, project search and text highlight test files (263 tests). Not rendered in the app. Signed-off-by: Matt Toohey Co-Authored-By: Claude Fable 5.1 --- apps/staged/src/app.css | 28 ++++++++++++++++--- .../CommandPalette.component.test.ts | 10 +++++-- .../commandPalette/CommandPaletteRow.svelte | 19 ++++--------- .../projects/ProjectMatchReason.svelte | 12 +------- .../src/lib/components/DiffViewer.svelte | 9 ++++-- 5 files changed, 45 insertions(+), 33 deletions(-) diff --git a/apps/staged/src/app.css b/apps/staged/src/app.css index 056c29f52..abf507055 100644 --- a/apps/staged/src/app.css +++ b/apps/staged/src/app.css @@ -269,13 +269,33 @@ svg.lucide:not(.animate-spin), transform: translateZ(0); } -/* ── In-content search highlights ── */ -mark.search-match { +/* ── Search match highlights ── */ +/* Longhands on purpose: WebKit before Safari 26.2 rejects a thickness inside + the `text-decoration` shorthand, and a rejected `var()` declaration drops + the whole underline, not just its thickness. */ +mark.search-match, +mark.label-match { background-color: transparent; + text-decoration-line: underline; + text-decoration-color: var(--search-match-accent); + text-decoration-skip-ink: none; +} + +/* In-content find: notes and session chat. */ +mark.search-match { color: inherit; - text-decoration: underline 2px var(--search-match-accent); + text-decoration-thickness: 2px; text-underline-offset: 3px; - text-decoration-skip-ink: none; +} + +/* A match inside a one-line label: project search rows and the command + palette. No fill, the match in the primary text colour over a warm line, + which lifts it out of muted text. The offset keeps the line inside the + label's line box, which clips. */ +mark.label-match { + color: var(--text-primary); + text-decoration-thickness: 1.5px; + text-underline-offset: 1.5px; } mark.search-match-current { diff --git a/apps/staged/src/lib/features/commandPalette/CommandPalette.component.test.ts b/apps/staged/src/lib/features/commandPalette/CommandPalette.component.test.ts index f72886532..eefb2c00a 100644 --- a/apps/staged/src/lib/features/commandPalette/CommandPalette.component.test.ts +++ b/apps/staged/src/lib/features/commandPalette/CommandPalette.component.test.ts @@ -872,7 +872,7 @@ describe('CommandPalette filtering', () => { await type('beta'); expect(optionNames()).toEqual(['Beta tools']); - expect(options()[0].querySelector('.row-name mark')?.textContent).toBe('Beta'); + expect(options()[0].querySelector('.row-name mark.label-match')?.textContent).toBe('Beta'); }); it('returns the highlight to the first result when the query changes', async () => { @@ -934,11 +934,15 @@ describe('CommandPalette filtering', () => { await type('payments'); expect(optionIds()).toEqual(['beta']); expect(optionFor('beta').querySelector('.row-repo')?.textContent).toBe('org/payments'); - expect(optionFor('beta').querySelector('.row-repo mark')?.textContent).toBe('payments'); + expect(optionFor('beta').querySelector('.row-repo mark.label-match')?.textContent).toBe( + 'payments' + ); await type('storefront'); expect(optionIds()).toEqual(['gamma']); - expect(optionFor('gamma').querySelector('.row-alias mark')?.textContent).toBe('storefront'); + expect(optionFor('gamma').querySelector('.row-alias mark.label-match')?.textContent).toBe( + 'storefront' + ); expect(optionFor('gamma').querySelector('.row-repo')?.textContent).toBe('org/web/apps/site'); }); }); diff --git a/apps/staged/src/lib/features/commandPalette/CommandPaletteRow.svelte b/apps/staged/src/lib/features/commandPalette/CommandPaletteRow.svelte index d02b77742..7592177ce 100644 --- a/apps/staged/src/lib/features/commandPalette/CommandPaletteRow.svelte +++ b/apps/staged/src/lib/features/commandPalette/CommandPaletteRow.svelte @@ -62,7 +62,7 @@ {#snippet highlighted(segments: readonly HighlightSegment[])} - {#each segments as segment}{#if segment.match}{segment.text}{segment.text}{:else}{segment.text}{/if}{/each} {/snippet} @@ -240,18 +240,11 @@ min-width: 0; } - /* The same treatment as ProjectMatchReason, which draws the reason beside - these: no fill, the match in the primary text colour over a warm line. - That lifts a match out of the muted repo line and leaves the name as it - is. The offset keeps the line inside the line boxes, which clip. */ - .row-name mark, - .row-alias mark, - .row-repo mark { - background-color: transparent; - color: var(--text-primary); - text-decoration: underline 1.5px var(--search-match-accent); - text-underline-offset: 1.5px; - text-decoration-skip-ink: none; + /* Matches use the global `label-match` rule, shared with ProjectMatchReason, + which draws the reason beside these. The pill's border sits right under + the text; pull the line up off it. */ + .row-alias mark { + text-underline-offset: 0.5px; } .row-badges { diff --git a/apps/staged/src/lib/features/projects/ProjectMatchReason.svelte b/apps/staged/src/lib/features/projects/ProjectMatchReason.svelte index 0ac5c8fa1..4eb5a2c4a 100644 --- a/apps/staged/src/lib/features/projects/ProjectMatchReason.svelte +++ b/apps/staged/src/lib/features/projects/ProjectMatchReason.svelte @@ -57,7 +57,7 @@ {KIND_NAMES[reason.kind]}: {#each segments as segment}{#if segment.match}{segment.text}{#each segments as segment}{#if segment.match}{segment.text}{:else}{segment.text}{/if}{/each} {#if reason.more > 0} @@ -84,16 +84,6 @@ text-overflow: ellipsis; } - /* No fill: the match reads in the primary text colour over a warm line. - The offset keeps the line inside the label's 14px line box, which clips. */ - .match-label mark { - background-color: transparent; - color: var(--text-primary); - text-decoration: underline 1.5px var(--search-match-accent); - text-underline-offset: 1.5px; - text-decoration-skip-ink: none; - } - .match-more { flex-shrink: 0; color: var(--text-faint); diff --git a/packages/diff-viewer/src/lib/components/DiffViewer.svelte b/packages/diff-viewer/src/lib/components/DiffViewer.svelte index c83fab1cc..9e4f2107a 100644 --- a/packages/diff-viewer/src/lib/components/DiffViewer.svelte +++ b/packages/diff-viewer/src/lib/components/DiffViewer.svelte @@ -3352,10 +3352,15 @@ } /* Search result highlighting */ + /* Longhands on purpose: WebKit before Safari 26.2 rejects a thickness inside + the shorthand and would drop the whole underline. The `em` offset keeps + the line inside the row at any `--code-font-size`. */ .search-match, .search-current { - text-decoration: underline 2px var(--search-match-accent, #f2c744); - text-underline-offset: 3px; + text-decoration-line: underline; + text-decoration-thickness: 2px; + text-decoration-color: var(--search-match-accent, #f2c744); + text-underline-offset: 0.2em; text-decoration-skip-ink: none; } From 6840e299fe30d6d28bbc8ce1343264569c24e82b Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Fri, 13 Nov 2026 11:04:17 +1100 Subject: [PATCH 4/4] fix(staged): ring the current search match After the underline restyle, the current match differs from the others only by a 30 percent amber wash. In the diff viewer that wash replaces the changed-character tint, which is also amber at nearly the same strength, so on a modified line the current match and its changed neighbours are almost the same colour (contrast ratio 1.05 in the light theme). In the light theme the wash is faint on plain lines and in notes too. Add a 1px `box-shadow` ring in `--search-match-accent` around the current match, in note find, session find and the diff viewer. The accent stays readable over every surface, which is why a ring works where a stronger wash would not. The radius becomes uniform so the ring closes cleanly. Separate commit so it can be dropped without touching the longhand and shared-rule change before it. Signed-off-by: Matt Toohey Co-Authored-By: Claude Fable 5.1 --- apps/staged/src/app.css | 5 ++++- packages/diff-viewer/src/lib/components/DiffViewer.svelte | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/staged/src/app.css b/apps/staged/src/app.css index abf507055..15de5de59 100644 --- a/apps/staged/src/app.css +++ b/apps/staged/src/app.css @@ -298,9 +298,12 @@ mark.label-match { text-underline-offset: 1.5px; } +/* The wash alone is faint in the light theme; the ring keeps the current + match obvious on every surface. */ mark.search-match-current { background-color: var(--search-current-match-bg); - border-radius: 2px 2px 0 0; + border-radius: 2px; + box-shadow: 0 0 0 1px var(--search-match-accent); } /* ── Hashtag badges (shared across all badge contexts) ── */ diff --git a/packages/diff-viewer/src/lib/components/DiffViewer.svelte b/packages/diff-viewer/src/lib/components/DiffViewer.svelte index 9e4f2107a..4b61efb59 100644 --- a/packages/diff-viewer/src/lib/components/DiffViewer.svelte +++ b/packages/diff-viewer/src/lib/components/DiffViewer.svelte @@ -3364,8 +3364,12 @@ text-decoration-skip-ink: none; } + /* On a modified line the wash is nearly the same colour as the + changed-character tint it replaces; the ring is what keeps the current + match visible there. */ .search-current { background-color: var(--search-current-match-bg, rgba(242, 199, 68, 0.3)); - border-radius: 2px 2px 0 0; + border-radius: 2px; + box-shadow: 0 0 0 1px var(--search-match-accent, #f2c744); }