From a13186f07f1866297c3e0c9c6789c6612dcd5a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Sat, 19 Sep 2026 05:57:49 +0000 Subject: [PATCH] fix(mobile): stop theme-token opacity modifiers from silently dropping Surface: the mobile app (apps/mobile). fix(mobile): stop theme-token opacity modifiers from silently dropping ## Problem The repo documents that Tailwind's `/opacity` modifier does not work on the CSS-variable theme colors. `apps/mobile/src/global.css` (lines 5-9 and 55-58) says the `/opacity` modifier 'still does NOT work with these CSS-variable tokens' and ships pre-baked alpha pairs for that reason; `apps/mobile/AGENTS.md` states 'Opacity modifiers do not work on theme colors (CSS variables): `bg-destructive/10` fails.' NativeWind v5 cannot decompose the variables, so the declaration is dropped and the element paints nothing, with no build error or warning. The repo already provides the fix tokens (`--danger-tile-bg` / `--danger-tile-border`, mapped to `bg-danger-tile-bg` / `border-danger-tile-border`), and `tools/nativewind/check-classes.mjs` exists to catch 'patterns that go wrong in a way nothing else reports' and runs in `.github/workflows/kilo-app-ci.yml`, but it has no rule for theme-token opacity, so instances survived and regressed (see commits 0921e4f07, 82448096d, 3a811fc21). ## Evidence - `apps/mobile/src/components/agents/permission-card.tsx:113` - ``: the permission-error banner renders with no destructive tint. - `apps/mobile/src/components/agents/question-card.tsx:210` - the same `bg-destructive/10` banner for question-submit errors. - `apps/mobile/src/components/agents/attachment-previe --- .../agents/attachment-preview-strip.tsx | 2 +- .../src/components/agents/permission-card.tsx | 2 +- .../src/components/agents/question-card.tsx | 2 +- .../pr-review/pr-review-overview-parts.tsx | 8 +++--- tools/nativewind/check-classes.mjs | 28 +++++++++++++++++++ 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/apps/mobile/src/components/agents/attachment-preview-strip.tsx b/apps/mobile/src/components/agents/attachment-preview-strip.tsx index 1853d94e7e..690b7aa840 100644 --- a/apps/mobile/src/components/agents/attachment-preview-strip.tsx +++ b/apps/mobile/src/components/agents/attachment-preview-strip.tsx @@ -383,7 +383,7 @@ function AttachmentChip({ 'overflow-hidden rounded-md border border-border bg-card', isImage ? 'h-16 w-20' : 'h-12 w-48', description.showRetry && 'border-destructive', - isErrored && !description.showRetry && 'border-destructive/60' + isErrored && !description.showRetry && 'border-danger-tile-border' )} > {description.showRetry ? ( diff --git a/apps/mobile/src/components/agents/permission-card.tsx b/apps/mobile/src/components/agents/permission-card.tsx index c7d592bf73..7170e1c438 100644 --- a/apps/mobile/src/components/agents/permission-card.tsx +++ b/apps/mobile/src/components/agents/permission-card.tsx @@ -110,7 +110,7 @@ export function PermissionCard({ {presentation.errorMessage ? ( - + {presentation.errorMessage} ) : null} diff --git a/apps/mobile/src/components/agents/question-card.tsx b/apps/mobile/src/components/agents/question-card.tsx index 6c56bc37a0..d3eec82b6e 100644 --- a/apps/mobile/src/components/agents/question-card.tsx +++ b/apps/mobile/src/components/agents/question-card.tsx @@ -207,7 +207,7 @@ export function QuestionCard({ {presentation.errorMessage ? ( - + {presentation.errorMessage} ) : null} diff --git a/apps/mobile/src/components/pr-review/pr-review-overview-parts.tsx b/apps/mobile/src/components/pr-review/pr-review-overview-parts.tsx index 832fac0fa6..381c1976aa 100644 --- a/apps/mobile/src/components/pr-review/pr-review-overview-parts.tsx +++ b/apps/mobile/src/components/pr-review/pr-review-overview-parts.tsx @@ -68,10 +68,10 @@ export function describePrState(args: { return { labelKey: 'prReview.overview.stateOpen', tone: 'muted', icon: GitPullRequest }; } -// Theme colors are CSS variables — Tailwind opacity modifiers like -// `bg-good/10` don't work on them. The chip uses a flat muted background -// and lets the foreground color carry the tone so it stays legible in -// both themes without needing per-tone backgrounds. +// Theme colors are CSS variables, so Tailwind opacity modifiers on them are +// silently dropped. The chip uses a flat muted background and lets the +// foreground color carry the tone so it stays legible in both themes +// without needing per-tone backgrounds. const TONE_FG_CLASS = { good: 'text-good', warn: 'text-warn', diff --git a/tools/nativewind/check-classes.mjs b/tools/nativewind/check-classes.mjs index 384be42e9f..a1c9d7e830 100644 --- a/tools/nativewind/check-classes.mjs +++ b/tools/nativewind/check-classes.mjs @@ -51,6 +51,19 @@ const RULES = [ pattern: new RegExp(`\\b(?:${COLOR_UTILITIES})-black/\\d+\\b`, 'g'), advice: 'compiles to an unparseable #NaN colour; use a concrete value, e.g. bg-[#00000066]', }, + { + // A `/opacity` modifier on a theme colour compiles to + // `color-mix(in oklab, var(--color-…) n%, transparent)`. NativeWind v5 + // cannot decompose the variable, so the runtime drops the declaration and + // the element paints nothing, with no build error. The `*-tile-bg` / + // `*-tile-border` pairs pre-bake the alpha instead. + pattern: new RegExp( + `\\b(?:${COLOR_UTILITIES})-(?:background|foreground|card|popover|primary|secondary|muted-soft|muted|accent-soft|accent|destructive|border|input|ring|ink2|good|warn|info|danger|agent-[a-z]+)(?:-foreground)?/\\d+\\b`, + 'g' + ), + advice: + 'the /opacity modifier is dropped on CSS-variable theme colours; use the paired *-tile-bg / *-tile-border token or a concrete colour', + }, { // `text-align` only accepts auto/left/right/center/justify, so the // logical keywords are dropped and the text keeps its default alignment. @@ -226,6 +239,21 @@ function checkCompilerStillDropsThese() { if (!JSON.stringify(alignment).includes('text-align')) { fail('check-classes: text-align: start is no longer dropped — drop the text-start/end rule'); } + + // A `/opacity` modifier on an `@theme inline` variable is emitted as a + // deferred colorMix over the variable; the runtime cannot decompose it and + // drops the declaration. When the compiler resolves it instead, this stops + // matching and the rule above can go. + const themeOpacity = firstValue( + '@theme inline { --color-destructive: var(--destructive); }\n' + + ':root { --destructive: #b0483a; }\n' + + '.x { background-color: color-mix(in oklab, var(--color-destructive) 10%, transparent); }' + ); + if (!themeOpacity.includes('colorMix')) { + fail( + `check-classes: color-mix over an @theme inline variable now compiles to ${themeOpacity} instead of a deferred colorMix — drop the theme-token opacity rule` + ); + } } checkCompilerStillDropsThese();