From 6946025c5d82f3d36206188d7231fc2631229e57 Mon Sep 17 00:00:00 2001 From: Dean Stratakos <29683763+dastratakos@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:42:24 +0000 Subject: [PATCH 1/3] fix(desktop): read the --luke-* roles where CSS still named retired tokens CSS merged after #3524 and #3569 retired the desktop's token aliases went on reading --hairline, --space-*, --text-*, --raised, --control-*, --switch-on and --focus-ring*, which nothing declares any more, so the video avatar's faces, the Pull request tab's labels and stack, and the offline mark's title-bar room lost their gaps, edges and colours. Each now reads the role its alias pointed at. repository-checks.sh now refuses a var() with no fallback whose custom property no tracked sheet, page or script under apps/ or packages/ declares, naming the file and line. Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com> --- apps/desktop/src/renderer/styles/desktop.css | 8 +-- apps/desktop/src/renderer/styles/settings.css | 30 ++++----- apps/desktop/src/renderer/styles/tasks.css | 34 +++++----- .../components/breadcrumbs/breadcrumbs.css | 2 +- scripts/repository-checks.sh | 64 +++++++++++++++++++ 5 files changed, 101 insertions(+), 37 deletions(-) diff --git a/apps/desktop/src/renderer/styles/desktop.css b/apps/desktop/src/renderer/styles/desktop.css index a53d5b2f33..86d032849f 100644 --- a/apps/desktop/src/renderer/styles/desktop.css +++ b/apps/desktop/src/renderer/styles/desktop.css @@ -1778,8 +1778,8 @@ ul.actor-stack { } .desktop-shell .title-bar-controls .offline-mark { - width: var(--control-height); - height: var(--control-height); + width: var(--luke-control-height); + height: var(--luke-control-height); justify-content: center; padding: 0; } @@ -1792,8 +1792,8 @@ ul.actor-stack { --sidebar-toggle-clearance: calc( var(--title-bar-left) + 3 * - var(--control-height) + - var(--control-gap) + var(--luke-control-height) + + var(--luke-control-gap) ); } diff --git a/apps/desktop/src/renderer/styles/settings.css b/apps/desktop/src/renderer/styles/settings.css index c18ddb1209..725018fd36 100644 --- a/apps/desktop/src/renderer/styles/settings.css +++ b/apps/desktop/src/renderer/styles/settings.css @@ -636,7 +636,7 @@ .picture-choice { display: grid; align-items: start; - gap: var(--space-16); + gap: var(--luke-space-16); grid-template-columns: minmax(140px, 200px) minmax(0, 1fr); } @@ -644,9 +644,9 @@ position: relative; overflow: hidden; aspect-ratio: 1; - border-radius: var(--control-radius); - background: var(--hairline); - box-shadow: inset 0 0 0 1px var(--hairline-strong); + border-radius: var(--luke-control-radius); + background: var(--luke-color-border); + box-shadow: inset 0 0 0 1px var(--luke-color-border-strong); } .picture-choice-stage img, @@ -664,7 +664,7 @@ right: 0; bottom: 0; left: 0; - padding: var(--space-16) var(--space-8) var(--space-6); + padding: var(--luke-space-16) var(--luke-space-8) var(--luke-space-6); background: linear-gradient(transparent, rgb(0 0 0 / 65%)); color: #fff; font-size: 13px; @@ -677,7 +677,7 @@ margin: 0; padding: 0; border: 0; - gap: var(--space-12) var(--space-8); + gap: var(--luke-space-12) var(--luke-space-8); grid-template-columns: repeat(4, minmax(0, 1fr)); } @@ -687,11 +687,11 @@ min-width: 0; flex-direction: column; align-items: center; - gap: var(--space-4); + gap: var(--luke-space-4); padding: 0; border: 0; background: none; - color: var(--text-secondary); + color: var(--luke-color-text-muted); cursor: pointer; font: inherit; } @@ -709,22 +709,22 @@ max-width: 88px; aspect-ratio: 1; border-radius: 50%; - background: var(--hairline); - box-shadow: inset 0 0 0 1px var(--hairline-strong); + background: var(--luke-color-border); + box-shadow: inset 0 0 0 1px var(--luke-color-border-strong); transition: box-shadow var(--duration-quick) var(--motion-exit); } .picture-choice-face:hover .picture-choice-photo { - box-shadow: 0 0 0 2px var(--hairline-strong); + box-shadow: 0 0 0 2px var(--luke-color-border-strong); } .picture-choice-face:has(> :checked) .picture-choice-photo { - box-shadow: 0 0 0 2px var(--switch-on); + box-shadow: 0 0 0 2px var(--luke-color-switch-on); } :root[data-keyboard="true"] .picture-choice-face:has(> :focus-visible) .picture-choice-photo { - outline: var(--focus-ring-width) solid var(--focus-ring); - outline-offset: calc(2px + var(--focus-ring-offset)); + outline: var(--luke-focus-ring-width) solid var(--luke-color-focus-ring); + outline-offset: calc(2px + var(--luke-focus-ring-offset)); } .picture-choice-name { @@ -736,7 +736,7 @@ } .picture-choice-face:has(> :checked) .picture-choice-name { - color: var(--text-primary); + color: var(--luke-color-text); font-weight: 600; } diff --git a/apps/desktop/src/renderer/styles/tasks.css b/apps/desktop/src/renderer/styles/tasks.css index 90929edf9e..f3764d20bd 100644 --- a/apps/desktop/src/renderer/styles/tasks.css +++ b/apps/desktop/src/renderer/styles/tasks.css @@ -1883,7 +1883,7 @@ display: flex; min-width: 0; align-items: center; - gap: var(--space-8); + gap: var(--luke-space-8); } .pull-request-branch-row > .pull-request-branches { @@ -1895,20 +1895,20 @@ display: inline-flex; flex: none; align-items: center; - gap: var(--space-4); + gap: var(--luke-space-4); height: 22px; - margin-top: var(--space-2); - padding: 0 var(--space-6); - border: 1px solid var(--hairline); + margin-top: var(--luke-space-2); + padding: 0 var(--luke-space-6); + border: 1px solid var(--luke-color-border); border-radius: 6px; background: transparent; - color: var(--text-secondary); + color: var(--luke-color-text-muted); font-size: 11.5px; } .pull-request-stack:hover { - background: var(--raised); - color: var(--text-primary); + background: var(--luke-color-fill-hover); + color: var(--luke-color-text); } .pull-request-stack > svg { @@ -1917,7 +1917,7 @@ } .pull-request-stack-count { - color: var(--text-tertiary); + color: var(--luke-color-text-subtle); font-variant-numeric: tabular-nums; } @@ -1928,8 +1928,8 @@ .pull-request-labels { display: flex; flex-wrap: wrap; - gap: var(--space-4); - margin: var(--space-6) 0 var(--space-2); + gap: var(--luke-space-4); + margin: var(--luke-space-6) 0 var(--luke-space-2); padding: 0 var(--row-inset); list-style: none; } @@ -1937,11 +1937,11 @@ .pull-request-label { max-width: 100%; overflow: hidden; - padding: 1px var(--space-8); - border: 1px solid var(--label-edge-dark, var(--hairline)); + padding: 1px var(--luke-space-8); + border: 1px solid var(--label-edge-dark, var(--luke-color-border)); border-radius: 999px; background: var(--label-fill-dark, transparent); - color: var(--label-words-dark, var(--text-secondary)); + color: var(--label-words-dark, var(--luke-color-text-muted)); font-size: 11px; font-weight: 500; line-height: 1.45; @@ -1951,9 +1951,9 @@ @media (prefers-color-scheme: light) { .pull-request-label { - border-color: var(--label-edge-light, var(--hairline)); + border-color: var(--label-edge-light, var(--luke-color-border)); background: var(--label-fill-light, transparent); - color: var(--label-words-light, var(--text-secondary)); + color: var(--label-words-light, var(--luke-color-text-muted)); } } @@ -1961,7 +1961,7 @@ .pull-request-reviewers-glyph { width: 14px; height: 14px; - color: var(--text-tertiary); + color: var(--luke-color-text-subtle); } .pull-request-page { diff --git a/packages/ui/src/components/breadcrumbs/breadcrumbs.css b/packages/ui/src/components/breadcrumbs/breadcrumbs.css index e4852bbd4b..e39364cd04 100644 --- a/packages/ui/src/components/breadcrumbs/breadcrumbs.css +++ b/packages/ui/src/components/breadcrumbs/breadcrumbs.css @@ -23,7 +23,7 @@ flex: 0 1 auto; overflow: clip; overflow-clip-margin: padding-box - calc(var(--focus-ring-width, 2px) + var(--focus-ring-offset, 1px)); + calc(var(--luke-focus-ring-width) + var(--luke-focus-ring-offset)); } .luke-breadcrumbs ol { diff --git a/scripts/repository-checks.sh b/scripts/repository-checks.sh index 4e95aee12e..19c71a654d 100755 --- a/scripts/repository-checks.sh +++ b/scripts/repository-checks.sh @@ -627,6 +627,70 @@ fi # Oct 17 plan-to-task rename broke the 26 turns then running. pnpm --silent --filter @luke/database check:renames +# A `var(--name)` with no fallback, where nothing declares `--name`, fails +# nothing: the declaration it stands in is invalid at computed-value time, so a +# border, a gap, or a colour silently drops. That is how CSS merged after +# #3524 and #3569 retired the desktop's token aliases (`--hairline`, +# `--space-*`, `--focus-ring-width`, …) went on reading names nobody set. So +# every such read in a tracked sheet, page, or script under apps/ and packages/ +# must name a property one of them declares: a sheet's `--name:` or +# `@property --name`, or a script's `"--name"` literal (`setProperty`, a style +# object's key, a constant), where a `--name-${…}` template declares the whole +# family. A name only a third-party stylesheet sets is listed here by hand. +# Note that the declarations are pooled across every app, so a name one profile +# sets and another reads unset still passes; the check catches a name gone +# everywhere, which is what a retired token is. +node --input-type=module -e ' + import { execFileSync } from "node:child_process"; + import { readFileSync } from "node:fs"; + import path from "node:path"; + + const root = process.argv[1]; + // Set by the stylesheet Excalidraw ships, which the board imports. + const THIRD_PARTY = ["--default-button-size"]; + const SCRIPT = /\.tsx?$/; + const files = execFileSync("git", ["-C", root, "ls-files", "-z", "--", "apps", "packages"], { + encoding: "utf8", + }) + .split("\0") + .filter((file) => /\.(css|html|tsx?)$/.test(file) && !/\.test\.tsx?$/.test(file)); + const declared = new Set(THIRD_PARTY); + const families = []; + const reads = []; + for (const file of files) { + const source = readFileSync(path.join(root, file), "utf8"); + // Blank comments, keeping every newline, so a name a comment mentions + // declares nothing and a line number still points home. + const text = SCRIPT.test(file) + ? source.replace(/^\s*(\/\/|\/\*|\*).*$/gm, "") + : source.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, " ")); + if (SCRIPT.test(file)) { + for (const [, name, end] of text.matchAll(/(?<=["\x27\x60]\s*)(--[\w-]+?)(\$\{|["\x27\x60])/g)) { + if (end === "${") families.push(name); + else declared.add(name); + } + } else { + for (const [, name, registered] of text.matchAll(/(--[\w-]+)\s*:|@property\s+(--[\w-]+)/g)) { + declared.add(name ?? registered); + } + } + for (const read of text.matchAll(/var\(\s*(--[\w-]+)\s*\)/g)) { + const line = text.slice(0, read.index).split("\n").length; + reads.push({ file, line, name: read[1] }); + } + } + const unset = reads.filter( + ({ name }) => !declared.has(name) && !families.some((family) => name.startsWith(family)), + ); + if (unset.length > 0) { + const lines = unset.map(({ file, line, name }) => ` ${file}:${line}: ${name}`).join("\n"); + process.stderr.write( + `error: var() reads a custom property nothing declares and gives no fallback, so the declaration silently drops (a retired token reads its --luke-* role):\n${lines}\n`, + ); + process.exit(1); + } +' "$LUKE_REPO_ROOT" + # This is the one scan for secrets the repository runs (AGENTS.md's "Never"), and # a test fixture shaped like a provider's live credential is what GitHub's # secret scanning raises an alert on, which history keeps open once pushed. So From e46b5d4f14baa2527c02efd83b4c77a03c508db8 Mon Sep 17 00:00:00 2001 From: Dean Stratakos <29683763+dastratakos@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:47:11 +0000 Subject: [PATCH 2/3] fix(desktop): read the --luke-* roles in Files changed's inline threads #3245 merged its review-thread rules on the retired names; the new repository check caught them on this branch's merge with main. Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com> --- apps/desktop/src/renderer/styles/tasks.css | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/renderer/styles/tasks.css b/apps/desktop/src/renderer/styles/tasks.css index c59a9c76a7..d1df204eb8 100644 --- a/apps/desktop/src/renderer/styles/tasks.css +++ b/apps/desktop/src/renderer/styles/tasks.css @@ -2277,29 +2277,29 @@ button.pull-request-check:hover { so the window's own type and card draw it, inset from the code's edges as GitHub's are; an outdated one stands in a list above the diff. */ .diff-note { - padding: var(--space-6) var(--space-12); + padding: var(--luke-space-6) var(--luke-space-12); font-family: var(--luke-font-sans); white-space: normal; } .diff-inline-thread { - --focus-ring-offset: var(--focus-ring-inset); + --luke-focus-ring-offset: var(--luke-focus-ring-inset); max-width: 720px; overflow: clip; - border: 1px solid var(--hairline); - border-radius: var(--control-radius); - background: var(--section-fill); + border: 1px solid var(--luke-color-border); + border-radius: var(--luke-control-radius); + background: var(--luke-color-surface); } .diff-outdated-threads { display: flex; flex: none; flex-direction: column; - gap: var(--space-6); + gap: var(--luke-space-6); margin: 0; - padding: var(--space-8) var(--space-12); + padding: var(--luke-space-8) var(--luke-space-12); list-style: none; - border-bottom: 1px solid var(--hairline); + border-bottom: 1px solid var(--luke-color-border); } .pull-request-thread-head { From 130dff1f558ac4949c69108b23a6409a9baf0d45 Mon Sep 17 00:00:00 2001 From: Dean Stratakos <29683763+dastratakos@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:50:47 +0000 Subject: [PATCH 3/3] fix(repo): count only a script's writes as declaring a custom property A getPropertyValue read quoted the name too, so a token whose CSS declarations were gone still passed while a script read it (Devin's review on #3631). Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com> --- scripts/repository-checks.sh | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/scripts/repository-checks.sh b/scripts/repository-checks.sh index 19c71a654d..859cb7f553 100755 --- a/scripts/repository-checks.sh +++ b/scripts/repository-checks.sh @@ -634,9 +634,12 @@ pnpm --silent --filter @luke/database check:renames # `--space-*`, `--focus-ring-width`, …) went on reading names nobody set. So # every such read in a tracked sheet, page, or script under apps/ and packages/ # must name a property one of them declares: a sheet's `--name:` or -# `@property --name`, or a script's `"--name"` literal (`setProperty`, a style -# object's key, a constant), where a `--name-${…}` template declares the whole -# family. A name only a third-party stylesheet sets is listed here by hand. +# `@property --name`, or a script's write of it (CSS text in a string, a style +# object's `"--name":` key, a `["--name"] =`, or `setProperty`'s first +# argument, spelled or held in a constant), where a `--name-${…}` declaration +# declares the whole family. A script that only reads a name, through +# `getPropertyValue`, declares nothing. A name only a third-party stylesheet +# sets is listed here by hand. # Note that the declarations are pooled across every app, so a name one profile # sets and another reads unset still passes; the check catches a name gone # everywhere, which is what a retired token is. @@ -649,6 +652,19 @@ node --input-type=module -e ' // Set by the stylesheet Excalidraw ships, which the board imports. const THIRD_PARTY = ["--default-button-size"]; const SCRIPT = /\.tsx?$/; + const NAME = "(--[\\w-]+)"; + const QUOTED = `["\x27\x60]${NAME}["\x27\x60]`; + // What a script writes a property with, the name always the first group. + const SCRIPT_WRITES = [ + new RegExp(`(? comment.replace(/[^\n]/g, " ")); if (SCRIPT.test(file)) { - for (const [, name, end] of text.matchAll(/(?<=["\x27\x60]\s*)(--[\w-]+?)(\$\{|["\x27\x60])/g)) { - if (end === "${") families.push(name); - else declared.add(name); + for (const write of SCRIPT_WRITES) { + for (const [, name] of text.matchAll(write)) declared.add(name); + } + for (const family of SCRIPT_FAMILIES) { + for (const [, name] of text.matchAll(family)) families.push(name); + } + // A constant handed to setProperty declares the name it holds. + for (const [, constant] of text.matchAll(/setProperty\(\s*([A-Za-z_$][\w$]*)\s*,/g)) { + const held = text.match(new RegExp(`\\b${constant}\\s*=\\s*${QUOTED}`)); + if (held) declared.add(held[1]); } } else { for (const [, name, registered] of text.matchAll(/(--[\w-]+)\s*:|@property\s+(--[\w-]+)/g)) {