diff --git a/docs/src/plugins/worktrunk-terminal.mjs b/docs/src/plugins/worktrunk-terminal.mjs index d706ea974c..ae5a5c5e02 100644 --- a/docs/src/plugins/worktrunk-terminal.mjs +++ b/docs/src/plugins/worktrunk-terminal.mjs @@ -385,6 +385,7 @@ export function pluginWorktrunkTerminal() { color: var(--wt-ink-muted); } .expressive-code .frame.is-terminal .wt-terminal-gutter { + display: inline-block; background: var(--wt-terminal-gutter); } .expressive-code .frame.is-terminal .wt-terminal-bold { diff --git a/docs/src/themes/worktrunk-code.mjs b/docs/src/themes/worktrunk-code.mjs index dec3daa8d2..83e2391f03 100644 --- a/docs/src/themes/worktrunk-code.mjs +++ b/docs/src/themes/worktrunk-code.mjs @@ -34,6 +34,11 @@ const baseTokenColors = (palette) => [ scope: ['string', 'constant.other.symbol', 'entity.other.inherited-class'], settings: { foreground: palette.string }, }, + { + name: 'Quoted shell strings', + scope: ['string.quoted.single.shell', 'string.quoted.double.shell'], + settings: { foreground: palette.quotedShellString }, + }, { name: 'Variables', scope: ['variable', 'support.class', 'entity.name.class', 'entity.name.type.class'], @@ -78,8 +83,8 @@ function theme(name, type, palette) { }; } -// These are the syntax roles from the pre-Astro Worktrunk themes, adjusted only -// where the old foregrounds did not meet contrast on the current code surface. +// These roles follow the pre-Astro Worktrunk themes. Low-contrast colors and +// the shell-only quoted-string role use the current site's accessible palette. export const worktrunkLightCodeTheme = theme('worktrunk-light', 'light', { background: '#f8f7f6', foreground: '#3d3632', @@ -87,6 +92,7 @@ export const worktrunkLightCodeTheme = theme('worktrunk-light', 'light', { keyword: '#9f4f2a', function: '#9b6500', string: '#527a16', + quotedShellString: '#1d4ed8', variable: '#7e6247', constant: '#a03c15', support: '#9f4f2a', @@ -101,6 +107,7 @@ export const worktrunkDarkCodeTheme = theme('worktrunk-dark', 'dark', { keyword: '#d8b4fe', function: '#e5b567', string: '#a3be8c', + quotedShellString: '#93c5fd', variable: '#d9a0a5', constant: '#e09a82', support: '#67d4d4', diff --git a/docs/tests/browser-site.test.mjs b/docs/tests/browser-site.test.mjs index 23e07fa91b..c3da4d6634 100644 --- a/docs/tests/browser-site.test.mjs +++ b/docs/tests/browser-site.test.mjs @@ -235,6 +235,65 @@ test('code artifacts keep their visual hierarchy in both themes', async () => { await page.evaluate((selectedTheme) => { document.documentElement.dataset.theme = selectedTheme; }, theme); + const homepageTerminalStyles = await page.evaluate(() => { + const frames = [...document.querySelectorAll('.expressive-code .frame')]; + const agentFrame = frames.find((candidate) => candidate.textContent.includes( + "wt switch -x claude -c feature-a -- 'Add user authentication'", + )); + const mergeFrame = frames.find((candidate) => candidate.textContent.includes( + 'Merging 1 commit to main', + )); + const token = (text) => [...agentFrame.querySelectorAll('.code span')] + .find((candidate) => candidate.textContent === text); + const outputLines = [...mergeFrame.querySelectorAll('.ec-line.wt-output')]; + const gutters = outputLines.map((line, index) => ({ + index, + gutter: line.querySelector('.wt-terminal-gutter'), + })).filter(({ gutter }) => gutter); + const joins = gutters.slice(1).flatMap((current, index) => ( + current.index === gutters[index].index + 1 + ? [current.gutter.getBoundingClientRect().top + - gutters[index].gutter.getBoundingClientRect().bottom] + : [] + )); + const pre = agentFrame.querySelector('pre'); + return { + argument: getComputedStyle(token('feature-a')).color, + background: getComputedStyle(pre).backgroundColor, + gutterIndexes: gutters.map(({ index }) => index), + joins, + lineHeights: outputLines.map((line) => line.getBoundingClientRect().height), + quoted: getComputedStyle(token("'Add user authentication'")).color, + }; + }); + assert.notEqual( + homepageTerminalStyles.argument, + homepageTerminalStyles.quoted, + `${theme} quoted shell string uses the argument color`, + ); + assert.ok( + contrastRatio(homepageTerminalStyles.quoted, homepageTerminalStyles.background) >= 4.5, + `${theme} quoted shell string falls below 4.5:1 contrast`, + ); + assert.deepEqual( + homepageTerminalStyles.gutterIndexes, + [1, 4, 5, 6, 7], + `${theme} merge gutter rows moved — regenerate from quickstart_merge.snap`, + ); + assert.equal( + homepageTerminalStyles.joins.length, + 3, + `${theme} merge gutter run is no longer three adjacent joins`, + ); + assert.ok( + homepageTerminalStyles.joins.every((gap) => Math.abs(gap) < 0.5), + `${theme} merge gutter does not sit flush between rows`, + ); + assert.equal( + new Set(homepageTerminalStyles.lineHeights).size, + 1, + `${theme} gutter padding changes terminal line height`, + ); const comparisonStyles = await page.evaluate(() => { const table = document.querySelector('.cmd-compare'); return { @@ -331,6 +390,8 @@ test('code artifacts keep their visual hierarchy in both themes', async () => { titleBackground: getComputedStyle(title).backgroundColor, codeBackground: getComputedStyle(pre).backgroundColor, accentWidth: getComputedStyle(pre).borderInlineStartWidth, + stringColor: getComputedStyle([...frame.querySelectorAll('.code span')] + .find((span) => span.textContent === '".worktrees/{{ branch | sanitize }}"')).color, }; }); assert.equal(fileFrame.title, '~/.config/worktrunk/config.toml'); @@ -346,6 +407,11 @@ test('code artifacts keep their visual hierarchy in both themes', async () => { ); const titleContrast = contrastRatio(fileFrame.titleColor, fileFrame.titleBackground); assert.ok(titleContrast >= 4.5, `${theme} file-label contrast is ${titleContrast.toFixed(2)}:1`); + assert.notEqual( + fileFrame.stringColor, + homepageTerminalStyles.quoted, + `${theme} shell quote color leaks into TOML strings`, + ); assert.equal(fileFrame.accentWidth, '3px'); await page.close(); }