From 7d0621e1e54107959358c6e135eba464c19094e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 10:51:38 +0000 Subject: [PATCH 1/3] fix(design-reaches-app): normalise CSS, whole-class and var-fallback matching (#670) Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr --- CHANGELOG.md | 1 + project-bin/check-design-reaches-app.sh | 76 +- .../fixtures/design-reaches-app/CAPTURE.md | 11 + .../design-reaches-app/real-built-excerpt.css | 80 +++ .../real-custom-variables.scss | 676 ++++++++++++++++++ .../fixtures/design-reaches-app/real-ds.css | 131 ++++ tests/wave2/test-design-reaches-app.sh | 70 ++ 7 files changed, 1033 insertions(+), 12 deletions(-) create mode 100644 tests/wave2/fixtures/design-reaches-app/real-built-excerpt.css create mode 100644 tests/wave2/fixtures/design-reaches-app/real-custom-variables.scss create mode 100644 tests/wave2/fixtures/design-reaches-app/real-ds.css diff --git a/CHANGELOG.md b/CHANGELOG.md index dee96878..d255080b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Kinds: `new` · `fix` · `learn` (a skill/learning) · `process` (rules, templat Credit the person or project that surfaced the change — the credit line is the thank-you. **Release cycle (from 2026-09-19).** New lines land under `## Unreleased`, at the top. Every few +- fix(design-reaches-app): **the CSS readers now normalise both stylesheets first and match whole classes (#670)**: a one-line `:root{--a:1;--b:2}` read as 1 token with `}` in its value, a minified built sheet as having no bare `:root`, a rule inside `@media` or a multi-line selector list was missed, and `var(--x, fallback)` did not count as using `--x`. All five fixed by one `normalise_css()` step (comments dropped, one selector/declaration/`}` per line, `;` inside `url(...)`/strings kept). On the field project's real sheets the design went from 10 to 38 tokens read and knobs bound from 12 to 37. **Can turn a project red:** class presence was a substring match, so `.card` counted as arrived when only `.card-header` existed; it is now a whole-class match, and a project that passed on that false reading can now report `0 of N design-system classes` (FAIL) or fewer classes arrived. No verdict names or exit codes changed — outside review of v2026.11.12 days the maintainer cuts a release with `bin/cut-release.sh`: the `## Unreleased` heading becomes `## vYYYY.MM.DD`, master is tagged `vYYYY.MM.DD`, and a fresh empty `## Unreleased` opens. So the top section is always "what changed since the last release", a project reports the release it is diff --git a/project-bin/check-design-reaches-app.sh b/project-bin/check-design-reaches-app.sh index 273eaa06..ad3fbae3 100755 --- a/project-bin/check-design-reaches-app.sh +++ b/project-bin/check-design-reaches-app.sh @@ -171,6 +171,56 @@ report() { printf 'FAIL %s\n %s\n' "$1" "$2"; VIOLATIONS=$((VIOLATIONS + 1 warn() { printf 'WARN %s\n %s\n' "$1" "$2"; WARNINGS=$((WARNINGS + 1)); } fault() { printf 'FAULT %s\n %s\n' "$1" "$2"; FAULTS=$((FAULTS + 1)); } +# ── Normalise both stylesheets once (#670) ───────────────────────────────────────────────── +# Every reader below is line-based and was written for a pretty-printed sheet: one declaration +# per line, `:root {` alone on its line. A minified or one-line sheet (`:root{--a:1;--b:2}`) +# read as ONE token whose value kept its `}`, and a minified built sheet as having no bare +# :root at all — the report then printed denominators like "1 of 1" over almost nothing. +# So: drop /* comments */, then put every selector (`sel {`), declaration (`prop: value;`) and +# closing `}` on a line of its own. `;` `{` `}` inside (...) or "..." / '...' (url(data:...;base64,..)) do not +# split; a multi-line selector list collapses onto one line; a rule inside @media gets its own +# line. A sheet that was already one-declaration-per-line comes out with the same lines. +normalise_css() { + awk ' + function emit(s) { gsub(/[ \t\r]+/, " ", s); sub(/^ /, "", s); sub(/ $/, "", s); return s } + BEGIN { SPECIAL = "[/*\"(){};\\\\\047]"; buf = ""; depth = 0; q = ""; inc = 0 } + { + line = $0 + if (!inc && q == "" && depth == 0 && line !~ SPECIAL) { buf = buf " " line; next } + n = length(line) + for (i = 1; i <= n; i++) { + c = substr(line, i, 1) + if (inc) { if (c == "*" && substr(line, i + 1, 1) == "/") { inc = 0; i++; buf = buf " " } continue } + if (q != "") { + buf = buf c + if (c == "\\") { buf = buf substr(line, i + 1, 1); i++ } else if (c == q) q = "" + continue + } + if (c == "/" && substr(line, i + 1, 1) == "*") { inc = 1; i++; continue } + if (c == "\"" || c == "\047") { q = c; buf = buf c; continue } + if (c == "\\") { buf = buf c substr(line, i + 1, 1); i++; continue } + if (c == "(") { depth++; buf = buf c; continue } + if (c == ")") { if (depth > 0) depth--; buf = buf c; continue } + if (depth == 0 && c == "{") { s = emit(buf); print (s == "" ? "{" : s " {"); buf = ""; continue } + if (depth == 0 && c == ";") { s = emit(buf); if (s != "") print s ";"; buf = ""; continue } + if (depth == 0 && c == "}") { s = emit(buf); if (s != "") print s; print "}"; buf = ""; continue } + buf = buf c + } + q = ""; buf = buf " " + } + END { s = emit(buf); if (s != "") print s } + ' "$1" +} +NORM_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cdra.XXXXXX")" +trap 'rm -rf "$NORM_DIR"' EXIT +DESIGN_N="$NORM_DIR/design.css"; BUILT_N="$NORM_DIR/built.css" +normalise_css "$DESIGN" > "$DESIGN_N" +normalise_css "$BUILT" > "$BUILT_N" + +# A whole-class match: `.card` is present when `.card` is followed by a character that cannot +# continue a class name, never when only `.card-header` exists (a substring match counted it). +has_class() { grep -qE -- "$(printf '%s' "$2" | sed 's/\./\\./g')([^A-Za-z0-9_-]|\$)" "$1"; } + # ── Pass 0: read the design system ────────────────────────────────────────────────────────── # Tokens: custom properties declared in a :root block. Values too, because a knob may be bound # by repeating the literal rather than by var() — still bound, and still correct. @@ -179,15 +229,15 @@ TOKENS="$(awk ' inroot && /^[[:space:]]*}/ { inroot = 0 } inroot && match($0, /--[a-z0-9-]+[[:space:]]*:/) { name = substr($0, RSTART, RLENGTH); sub(/[[:space:]]*:$/, "", name) - rest = substr($0, RSTART + RLENGTH); sub(/;.*$/, "", rest) + rest = substr($0, RSTART + RLENGTH); sub(/;[[:space:]]*$/, "", rest) gsub(/^[[:space:]]+|[[:space:]]+$/, "", rest) if (!(name in seen)) { seen[name] = 1; print name "\t" rest } } -' "$DESIGN")" +' "$DESIGN_N")" TOKEN_COUNT="$(printf '%s\n' "$TOKENS" | grep -c '^--' || true)" # Classes: any `.name` appearing at the head of a rule. Compound selectors contribute each. -CLASSES="$(grep -oE '^[^{@}]*\{' "$DESIGN" \ +CLASSES="$(grep -oE '^[^{@}]*\{' "$DESIGN_N" \ | grep -oE '\.[A-Za-z_-][A-Za-z0-9_-]*' | sort -u)" CLASS_COUNT="$(printf '%s\n' "$CLASSES" | grep -c '^\.' || true)" @@ -211,7 +261,7 @@ BUILT_ROOT="$(awk ' map[name] = rest } END { for (k in map) print k "\t" map[k] } -' "$BUILT")" +' "$BUILT_N")" # Themed palettes. A design system may keep its whole palette in scoped blocks, # `:root.theme-light { ... }`, `:root.theme-dark { ... }`, with no bare `:root` at all. Pass 1 @@ -227,21 +277,21 @@ scoped_root() { inscope && /^[[:space:]]*\}/ { inscope = 0; next } inscope && match($0, /--[a-zA-Z0-9-]+[[:space:]]*:/) { name = substr($0, RSTART, RLENGTH); sub(/[[:space:]]*:$/, "", name) - rest = substr($0, RSTART + RLENGTH); sub(/;.*$/, "", rest) + rest = substr($0, RSTART + RLENGTH); sub(/;[[:space:]]*$/, "", rest) gsub(/^[[:space:]]+|[[:space:]]+$/, "", rest) key = sel "\t" name; if (!(key in map)) order[++n] = key; map[key] = rest } END { for (i = 1; i <= n; i++) print order[i] "\t" map[order[i]] } ' "$1" } -DESIGN_SCOPED="$(scoped_root "$DESIGN")" -BUILT_SCOPED="$(scoped_root "$BUILT")" +DESIGN_SCOPED="$(scoped_root "$DESIGN_N")" +BUILT_SCOPED="$(scoped_root "$BUILT_N")" # Names the design declares in a bare `:root`; every other token is themed-only. DESIGN_BARE_NAMES="$(awk ' /^[[:space:]]*:root[[:space:]]*\{[[:space:]]*$/ { inroot = 1; next } inroot && /^[[:space:]]*\}/ { inroot = 0; next } inroot && match($0, /--[a-zA-Z0-9-]+[[:space:]]*:/) { n = substr($0, RSTART, RLENGTH); sub(/[[:space:]]*:$/, "", n); print n } -' "$DESIGN" | sort -u)" +' "$DESIGN_N" | sort -u)" KNOB_TOTAL=0 KNOB_BOUND=0 @@ -263,10 +313,12 @@ if [ -f "$CUSTOM_VARS" ]; then val="$(printf '%s\n' "$BUILT_ROOT" | awk -F'\t' -v k="$knob" '$1 == k { v = $2 } END { print v }')" [ -z "$val" ] && continue bound=0 + vals="$(printf '%s' "$val" | tr -d ' ')" # (a) bound by reference while IFS="$(printf '\t')" read -r tname tval; do [ -z "$tname" ] && continue - case "$val" in *"var($tname)"*) bound=1; break ;; esac + # `var(--x)` and `var(--x, fallback)` both use --x + case "$vals" in *"var($tname)"*|*"var($tname,"*) bound=1; break ;; esac # (b) bound by repeating the literal. Only for values that look like a value, so an # empty or inherit-ish token cannot mark every knob bound. if [ -n "$tval" ] && [ "${#tval}" -ge 3 ]; then @@ -289,7 +341,7 @@ fi TOKENS_ARRIVED=0 while IFS="$(printf '\t')" read -r tname tval; do [ -z "$tname" ] && continue - grep -qE -- "$tname[[:space:]]*:" "$BUILT" && TOKENS_ARRIVED=$((TOKENS_ARRIVED + 1)) + grep -qE -- "$tname[[:space:]]*:" "$BUILT_N" && TOKENS_ARRIVED=$((TOKENS_ARRIVED + 1)) done < ...`), 26662-26664 (`.card-body`). diff --git a/tests/wave2/fixtures/design-reaches-app/real-built-excerpt.css b/tests/wave2/fixtures/design-reaches-app/real-built-excerpt.css new file mode 100644 index 00000000..7a30e6bd --- /dev/null +++ b/tests/wave2/fixtures/design-reaches-app/real-built-excerpt.css @@ -0,0 +1,80 @@ +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +:root { + /* ---- the one colour that defines the app --------------------------- */ + --mxt-brand: #2F5D62; + --mxt-brand-hover: #244A4E; + --mxt-brand-ink: #FFFFFF; + /* text on a brand-filled surface */ + /* ---- semantic status ----------------------------------------------- */ + --mxt-success: #2E7D5B; + --mxt-warning: #B7791F; + --mxt-danger: #B4412F; + --mxt-info: #3B6EA5; + /* ---- surfaces and ink ----------------------------------------------- */ + --mxt-ground: #F6F3EE; + /* the app background */ + --mxt-surface: #FFFFFF; + /* cards, modals, panels */ + --mxt-surface-alt: #FAF8F5; + /* striped rows, subtle fills */ + --mxt-surface-hover: #F1EEE8; + --mxt-surface-selected: #E6EFEF; + --mxt-ink: #1E2A32; + --mxt-ink-muted: #56636C; + --mxt-ink-faint: #8A949B; + --mxt-line: #E4DED5; + /* ---- the navigation rail -------------------------------------------- */ + --mxt-rail: #243138; + --mxt-rail-line: #34454D; + --mxt-rail-ink: #BCC8CD; + --mxt-rail-ink-active: #FFFFFF; + /* ---- status pill tones ---------------------------------------------- */ + --mxt-tint-ok: #E3F1EA; + --mxt-tone-ok: #1F5C41; + --mxt-tint-warn: #FBEFD9; + --mxt-tone-warn: #7A4E0C; + --mxt-tint-risk: #F8E4DF; + --mxt-tone-risk: #8A2E1F; + --mxt-tint-info: #E3ECF5; + --mxt-tone-info: #274D78; + --mxt-tint-neutral: #ECE9E3; + /* ---- shape, density, elevation --------------------------------------- */ + --mxt-radius: 8px; + --mxt-radius-lg: 14px; + --mxt-row-height: 56px; + --mxt-control-height: 40px; + --mxt-shadow: 0 1px 2px rgba(30,42,50,.06), 0 4px 14px rgba(30,42,50,.06); + --mxt-focus-halo: 0 0 0 3px rgba(15, 110, 107, 0.16); + /* ---- type ------------------------------------------------------------ */ + --mxt-font: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --mxt-font-heading: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --mxt-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace; + --mxt-font-size: 15px; + --mxt-line-height: 1.5; +} + Cards +========================================================================== */ +.card, .mx-layoutgrid > .row.cards > .col, +.mx-layoutgrid > .row.cards > [class*=col-], .mx-layoutgrid.cards > .row > .col, +.mx-layoutgrid.cards > .row > [class*=col-] { + border: var(--card-border); + border-radius: var(--card-border-radius); + background-color: var(--card-bg); + overflow: hidden; + position: relative; + padding: var(--card-padding); + box-shadow: var(--card-shadow); + margin-bottom: var(--card-margin-bottom); +} +.card-body { + padding: var(--spacing-large); +} diff --git a/tests/wave2/fixtures/design-reaches-app/real-custom-variables.scss b/tests/wave2/fixtures/design-reaches-app/real-custom-variables.scss new file mode 100644 index 00000000..cee98355 --- /dev/null +++ b/tests/wave2/fixtures/design-reaches-app/real-custom-variables.scss @@ -0,0 +1,676 @@ +// Theme options +$brand-logo: false; +$font-family-import: false; +$form-input-style: default; +$btn-bordered: false; // Default value false, set to true if you want this effect + +:root { + /* Brand Colors */ + --brand-primary: #264ae5; + --brand-success: #16aa16; + --brand-warning: #cd8501; + --brand-danger: #EA3337; + --brand-default: color-mix(in srgb, var(--brand-primary) 10%, #e7e7e9); + --gray: #B8BABF; + +// /* Hover Color Variants */ +// --brand-default-hover: var(--brand-default-100); +// --brand-primary-hover: var(--brand-primary-700); +// --brand-success-hover: var(--brand-success-700); +// --brand-warning-hover: var(--brand-warning-700); +// --brand-danger-hover: var(--brand-danger-700); + +// /* Brand Logo */ +// --brand-logo-height: 24px; +// --brand-logo-width: 32px; +// --brand-gradient: linear-gradient(to bottom, var(--brand-primary-900), var(--brand-primary-700)); + +// /* Default Font Size & Color */ +// --font-size-default: 14px; +// --font-color-default: var(--gray-900); +// --font-color-contrast: var(--color-base); + +// /* Global Border */ +// --border-color-default: var(--brand-default-300); +// --border-radius-s: 4px; +// --border-radius-m: 8px; +// --border-radius-l: 12px; +// --border-radius-default: var(--border-radius-s); +// --border-width-thin: 1px; +// --border-width-medium: 2px; +// --border-width-thick: 3px; +// --border-width-default: var(--border-width-thin); +// --border-default: var(--border-width-default) solid var(--border-color-default); + +// /* Focus indicators for accessibility */ +// --focus-outline: 2px solid var(--form-input-border-focus-color); +// --focus-outline-offset: 2px; + +// /* Topbar */ +// --topbar-bg: linear-gradient(to bottom, var(--brand-primary-500), var(--brand-primary-600)); +// --topbar-minimalheight: 48px; +// --topbar-border-color: var(--border-color-default); + +// /* Sidebar */ +// --sidebar-bg: linear-gradient(to bottom, var(--brand-primary-600), var(--brand-primary-700)); + +// /* Topbar mobile */ +// --m-header-height: 45px; +// --m-header-bg: var(--topbar-bg); +// --m-header-color: var(--font-color-contrast); +// --m-header-title-size: 16px; + +// /* Navbar Brand Name */ +// --navbar-brand-name: var(--font-color-default); + +// /* Background Colors */ +// --bg-color: var(--brand-default-100); +// --bg-color-secondary: var(--color-base); + +// /* Default Link Color */ +// --link-color: var(--brand-primary); +// --link-hover-color: var(--brand-primary-400); + +// /* Font Family */ +// --font-family-base: "Poppins", sans-serif; + +// /* Font Sizes */ +// --font-size-large: 18px; +// --font-size-small: 12px; + +// /* Font Weights */ +// --font-weight-light: 300; +// --font-weight-normal: 400; +// --font-weight-semibold: 600; +// --font-weight-bold: 700; + +// /* Header Font Sizes */ +// --font-size-h1: 34px; +// --font-size-h2: 26px; +// --font-size-h3: 24px; +// --font-size-h4: 16px; +// --font-size-h5: var(--font-size-default); +// --font-size-h6: 12px; + +// /* Header Font Weight */ +// --font-weight-header: var(--font-weight-semibold); + +// /* Line Height */ +// --line-height-base: 1.428571429; + +// /* Header Margin */ +// --font-header-margin: 0 0 8px 0; + +// /* Font Colors */ +// --font-color-detail: var(--brand-primary-700); +// --font-color-header: var(--brand-primary-800); + +// /* Navigation */ +// --navigation-item-height: unset; +// --navigation-item-padding: var(--spacing-medium); +// --navigation-border-radius: var(--border-radius-m); +// --navigation-scrollbar-color: rgba(255, 255, 255, 0.5) transparent; + +// --navigation-font-size: var(--font-size-default); +// --navigation-sub-font-size: var(--font-size-small); +// --navigation-glyph-size: 20px; + +// --navigation-color: var(--font-color-contrast); +// --navigation-color-hover: var(--font-color-contrast); +// --navigation-color-active: var(--font-color-contrast); + +// --navigation-sub-color: var(--navigation-color); +// --navigation-sub-color-hover: var(--navigation-color-hover); +// --navigation-sub-color-active: var(--navigation-color-active); + +// /* Sidebar Navigation */ +// --navigation-bg: var(--topbar-bg); +// --navigation-bg-hover: rgba(0, 0, 0, 0.2); +// --navigation-bg-active: rgba(0, 0, 0, 0.3); + +// --navigation-sub-bg: rgba(0, 0, 0, 0.2); +// --navigation-sub-bg-hover: var(--navigation-bg-hover); +// --navigation-sub-bg-active: var(--navigation-bg-active); + +// --navigation-border-color: var(--navigation-bg-hover); + +// --navsidebar-bg: var(--sidebar-bg); +// --navsidebar-bg-hover: var(--navigation-bg-hover); +// --navsidebar-bg-active: var(--navigation-bg-active); + +// --navsidebar-sub-bg: rgba(0, 0, 0, 0.2); +// --navsidebar-sub-bg-hover: var(--navigation-bg-hover); +// --navsidebar-sub-bg-active: var(--navigation-bg-active); +// --navsidebar-sub-bg-header: rgba(0, 0, 0, 0.2); +// --navsidebar-sub-bg-collapsed: var(--brand-primary-700); + +// --navsidebar-border-color: var(--border-color-default); +// --navsidebar-border-radius: var(--navigation-border-radius); +// --navsidebar-shadow: 0 0 4px rgb(0 0 0 / 14%), 2px 4px 8px rgb(0 0 0 / 28%); + +// --navsidebar-font-size: var(--font-size-default); +// --navsidebar-sub-font-size: var(--font-size-small); +// --navsidebar-glyph-size: 20px; + +// --navsidebar-color: var(--navigation-color); +// --navsidebar-color-hover: var(--navigation-color-hover); +// --navsidebar-color-active: var(--navigation-color-active); + +// --navsidebar-sub-color: var(--navigation-color); +// --navsidebar-sub-color-hover: var(--navigation-color-hover); +// --navsidebar-sub-color-active: var(--navigation-color-active); + +// --navsidebar-width-closed: 48px; +// --navsidebar-width-open: 232px; +// --navsidebar-toggle-size: 32px; + +// --navsidebar-icon-height: 48px; +// --navsidebar-icon-width: 48px; +// --navsidebar-animation-duration: 250ms; +// --navsidebar-animation-function: ease-in; + +// /* Topbar Navigation */ +// --navtopbar-font-size: var(--font-size-small); +// --navtopbar-sub-font-size: var(--font-size-small); +// --navtopbar-glyph-size: 1.2em; + +// --navtopbar-bg: var(--topbar-bg); +// --navtopbar-bg-hover: var(--navigation-bg-hover); +// --navtopbar-bg-active: var(--navigation-bg-active); +// --navtopbar-color: var(--navigation-color); +// --navtopbar-color-hover: var(--navigation-color-hover); +// --navtopbar-color-active: var(--navigation-color-active); + +// --navtopbar-sub-bg: var(--navtopbar-bg); +// --navtopbar-sub-bg-hover: var(--navtopbar-bg-hover); +// --navtopbar-sub-bg-active: var(--navtopbar-bg-active); +// --navtopbar-sub-color: var(--navigation-color); +// --navtopbar-sub-color-hover: var(--navigation-color-hover); +// --navtopbar-sub-color-active: var(--navigation-color-active); + +// --navtopbar-border-color: var(--topbar-border-color); +// --navtopbar-border-radius: var(--navigation-border-radius); + +// /* Shadows */ +// --shadow-color-border: var(--border-color-default); +// --shadow-color: var(--brand-default-300); +// --shadow-small: 0 2px 4px 0; +// --shadow-medium: 0 4px 6px 0; +// --shadow-large: 0 8px 10px 0; + +// /* Form Inputs */ +// --form-label-color: var(--font-color-default); +// --form-label-size: var(--font-size-default); +// --form-label-weight: var(--font-weight-normal); +// --form-label-gutter: 8px; + +// --form-input-height: auto; +// --form-input-padding-y: 8px; +// --form-input-padding-x: 8px; +// --form-input-static-padding-y: 8px; +// --form-input-static-padding-x: 0; +// --form-input-font-size: var(--form-label-size); +// --form-input-line-height: var(--line-height-base); +// --form-input-border-radius: 6px; + +// --form-input-bg: var(--color-base); +// --form-input-bg-focus: var(--color-base); +// --form-input-bg-hover: var(--color-base); +// --form-input-bg-disabled: var(--bg-color); +// --form-input-color: var(--font-color-default); +// --form-input-focus-color: var(--form-input-color); +// --form-input-disabled-color: var(--gray-700); +// --form-input-placeholder-color: var(--gray-600); +// --form-input-border-color: var(--brand-default); +// --form-input-border-focus-color: var(--brand-primary); +// --form-input-border-hover-color: color-mix(in srgb, var(--form-input-border-color), var(--form-input-border-focus-color) 50%); + +// --form-input-static-border-color: var(--gray-200); + +// --form-group-margin-bottom: 16px; +// --form-group-gutter: 16px; + +// /* Buttons */ +// --btn-font-size: var(--font-size-default); +// --btn-border-radius: var(--border-radius-default); + +// --btn-default-bg: var(--color-base); +// --btn-primary-bg: var(--brand-primary-600); +// --btn-success-bg: var(--brand-success-600); +// --btn-warning-bg: var(--brand-warning-600); +// --btn-danger-bg: var(--brand-danger-600); + +// --btn-default-border-color: var(--brand-default); +// --btn-primary-border-color: var(--brand-primary-600); +// --btn-success-border-color: var(--brand-success-600); +// --btn-warning-border-color: var(--brand-warning-600); +// --btn-danger-border-color: var(--brand-danger-600); + +// --btn-default-color: var(--brand-primary); +// --btn-primary-color: var(--font-color-contrast); +// --btn-success-color: var(--font-color-contrast); +// --btn-warning-color: var(--font-color-contrast); +// --btn-danger-color: var(--font-color-contrast); + +// --btn-default-icon-color: var(--gray); + +// --btn-default-bg-hover: var(--brand-default-hover); +// --btn-primary-bg-hover: var(--brand-primary-hover); +// --btn-success-bg-hover: var(--brand-success-hover); +// --btn-warning-bg-hover: var(--brand-warning-hover); +// --btn-danger-bg-hover: var(--brand-danger-hover); +// --btn-link-bg-hover: var(--brand-default-hover); + +// --btn-default-bg-active: var(--brand-default-300); +// --btn-primary-bg-active: var(--brand-primary-500); +// --btn-success-bg-active: var(--brand-success-500); +// --btn-warning-bg-active: var(--brand-warning-500); +// --btn-danger-bg-active: var(--brand-danger-500); +// --btn-link-bg-active: var(--brand-default-500); + +// /* Header */ +// --header-min-height: 240px; +// --header-bg-color: var(--brand-primary); +// --header-bgimage-filter: brightness(60%); +// --header-text-color: var(--color-base); +// --header-text-color-detail: rgba(0, 0, 0, 0.2); + +// /* Grid */ +// --grid-border-color: var(--border-color-default); + +// --grid-bg: transparent; +// --grid-bg-header: transparent; +// --grid-bg-hover: var(--brand-default-hover); +// --grid-bg-selected: var(--brand-default); +// --grid-bg-selected-hover: var(--brand-default-600); +// --grid-bg-striped: var(--brand-default-50); +// --grid-footer-bg: var(--brand-default); + +// --grid-selected-color: var(--font-color-default); + +// --grid-paging-bg: transparent; +// --grid-paging-bg-hover: transparent; +// --grid-paging-border-color: transparent; +// --grid-paging-border-color-hover: transparent; +// --grid-paging-color: var(--gray-300); +// --grid-paging-color-hover: var(--brand-primary); + +// /* Tabs */ +// --tabs-color: var(--font-color-default); +// --tabs-color-active: var(--font-color-detail); +// --tabs-lined-color-active: var(--font-color-detail); + +// --tabs-lined-border-width: 3px; +// --tabs-border-color: var(--border-color-default); +// --tabs-lined-border-color: var(--brand-primary); +// --tabs-bg: transparent; +// --tabs-bg-pills: var(--brand-default-200); +// --tabs-bg-hover: var(--brand-default-300); +// --tabs-bg-active: var(--brand-primary-600); + +// /* Modal */ +// --modal-header-bg: transparent; +// --modal-header-border-color: var(--border-color-default); +// --modal-header-color: var(--font-color-header); +// --modal-body-bg: var(--bg-color-secondary); +// --modal-footer-bg: var(--bg-color-secondary); + +// /* Data View */ +// --dataview-controls-bg: inherit; +// --dataview-controls-border-color: var(--border-color-default); +// --dataview-controls-alignment: left; + +// --dataview-emptymessage-bg: var(--bg-color); +// --dataview-emptymessage-color: var(--font-color-default); + +// /* Cards */ +// --card-shadow: var(--shadow-small) var(--shadow-color); +// --card-border: var(--border-default); +// --card-border-radius: var(--border-radius-m); +// --card-padding: var(--spacing-large); +// --card-margin-bottom: var(--spacing-smaller); +// --card-bg: var(--bg-color-secondary); + +// /* Alerts */ +// --alert-primary-bg: var(--brand-primary-50); +// --alert-success-bg: var(--brand-success-50); +// --alert-warning-bg: var(--brand-warning-50); +// --alert-danger-bg: var(--brand-danger-50); + +// --alert-primary-color: var(--brand-primary-700); +// --alert-success-color: var(--brand-success-700); +// --alert-warning-color: var(--brand-warning-700); +// --alert-danger-color: var(--brand-danger-700); + +// --alert-primary-border-color: var(--brand-primary-600); +// --alert-success-border-color: var(--brand-success-600); +// --alert-warning-border-color: var(--brand-warning-600); +// --alert-danger-border-color: var(--brand-danger-600); + +// /* Wizard */ +// --wizard-step-height: 48px; +// --wizard-step-number-size: 64px; +// --wizard-step-number-font-size: var(--font-size-h3); + +// --wizard-default-bg: var(--color-base); +// --wizard-default-color: var(--color-base); +// --wizard-default-step-color: var(--font-color-default); +// --wizard-default-border-color: var(--border-color-default); + +// --wizard-active-bg: var(--brand-primary-200); +// --wizard-active-color: var(--brand-primary-700); +// --wizard-active-step-color: var(--brand-primary-700); +// --wizard-active-border-color: var(--brand-primary-700); + +// --wizard-visited-bg: var(--brand-success-200); +// --wizard-visited-color: var(--brand-success-700); +// --wizard-visited-step-color: var(--brand-success-700); +// --wizard-visited-border-color: var(--brand-success-700); + +// /* Labels */ +// --label-default-bg: var(--brand-default); +// --label-primary-bg: var(--brand-primary-600); +// --label-success-bg: var(--brand-success-600); +// --label-warning-bg: var(--brand-warning-600); +// --label-danger-bg: var(--brand-danger-600); + +// --label-default-border-color: var(--brand-default-600); +// --label-primary-border-color: var(--brand-primary-600); +// --label-success-border-color: var(--brand-success-600); +// --label-warning-border-color: var(--brand-warning-600); +// --label-danger-border-color: var(--brand-danger-600); + +// --label-default-color: var(--font-color-default); +// --label-primary-color: var(--color-base); +// --label-success-color: var(--color-base); +// --label-warning-color: var(--color-base); +// --label-danger-color: var(--color-base); + +// /* Groupbox */ +// --groupbox-default-bg: var(--brand-default); +// --groupbox-primary-bg: var(--brand-primary-600); +// --groupbox-success-bg: var(--brand-success-600); +// --groupbox-warning-bg: var(--brand-warning-600); +// --groupbox-danger-bg: var(--brand-danger-600); +// --groupbox-white-bg: var(--color-base-600); + +// --groupbox-default-color: var(--font-color-default); +// --groupbox-primary-color: var(--font-color-contrast); +// --groupbox-success-color: var(--font-color-contrast); +// --groupbox-warning-color: var(--font-color-contrast); +// --groupbox-danger-color: var(--font-color-contrast); +// --groupbox-white-color: var(--font-color-default); + +// /* Callouts */ +// --callout-primary-color: var(--brand-primary-600); +// --callout-default-color: var(--font-color-default); +// --callout-success-color: var(--brand-success-700); +// --callout-warning-color: var(--brand-warning-700); +// --callout-danger-color: var(--brand-danger-600); + +// --callout-primary-bg: var(--brand-primary-100); +// --callout-default-bg: var(--gray-50); +// --callout-success-bg: var(--brand-success-100); +// --callout-warning-bg: var(--brand-warning-100); +// --callout-danger-bg: var(--brand-danger-100); + +// /* Timeline */ +// --timeline-icon-color: var(--brand-primary); +// --timeline-border-color: var(--border-color-default); +// --timeline-event-time-color: var(--brand-primary); +// --timeline-icon-size: 18px; +// --timeline-image-size: 36px; +// --timeline-grouping-size: 120px; +// --timeline-grouping-border-radius: 30px; +// --timeline-grouping-border-color: var(--timeline-border-color); + +// /* Accordion */ +// --accordion-header-default-bg: var(--bg-color-secondary); +// --accordion-header-default-bg-hover: var(--brand-default-hover); +// --accordion-header-default-color: var(--font-color-header); +// --accordion-default-border-color: var(--border-color-default); +// --accordion-bg-striped: var(--brand-default-200); +// --accordion-bg-striped-hover: var(--brand-default-hover); + +// --accordion-header-primary-bg: var(--brand-primary-600); +// --accordion-header-secondary-bg: var(--bg-color-secondary); +// --accordion-header-success-bg: var(--brand-success-600); +// --accordion-header-warning-bg: var(--brand-warning-600); +// --accordion-header-danger-bg: var(--brand-danger-600); + +// --accordion-header-primary-bg-hover: var(--brand-primary-hover); +// --accordion-header-secondary-bg-hover: var(---brand-default-hover); +// --accordion-header-success-bg-hover: var(--brand-success-hover); +// --accordion-header-warning-bg-hover: var(--brand-warning-hover); +// --accordion-header-danger-bg-hover: var(--brand-danger-hover); + +// --accordion-header-primary-color: var(--font-color-contrast); +// --accordion-header-secondary-color: var(--brand-primary); +// --accordion-header-success-color: var(--font-color-contrast); +// --accordion-header-warning-color: var(--font-color-contrast); +// --accordion-header-danger-color: var(--font-color-contrast); + +// --accordion-primary-border-color: var(--brand-primary); +// --accordion-secondary-border-color: var(--gray-200); +// --accordion-success-border-color: var(--brand-success); +// --accordion-warning-border-color: var(--brand-warning); +// --accordion-danger-border-color: var(--brand-danger); + +// /* Spacing */ +// --spacing-none: 0px; +// --spacing-smallest: 2px; +// --spacing-smaller: 4px; +// --spacing-small: 8px; +// --spacing-medium: 16px; + +// --t-spacing-medium: 16px; +// --m-spacing-medium: 16px; + +// --spacing-large: 24px; +// --t-spacing-large: 24px; +// --m-spacing-large: 16px; + +// --spacing-larger: 32px; +// --spacing-largest: 48px; + +// --layout-spacing-top: 24px; +// --layout-spacing-right: 24px; +// --layout-spacing-bottom: 24px; +// --layout-spacing-left: 24px; + +// --t-layout-spacing-top: 24px; +// --t-layout-spacing-right: 24px; +// --t-layout-spacing-bottom: 24px; +// --t-layout-spacing-left: 24px; + +// --m-layout-spacing-top: 16px; +// --m-layout-spacing-right: 16px; +// --m-layout-spacing-bottom: 16px; +// --m-layout-spacing-left: 16px; + +// --layout-spacing: var(--layout-spacing-top) var(--layout-spacing-right) var(--layout-spacing-bottom) var(--layout-spacing-left); +// --m-layout-spacing: var(--m-layout-spacing-top) var(--m-layout-spacing-right) var(--m-layout-spacing-bottom) var(--m-layout-spacing-left); +// --t-layout-spacing: var(--t-layout-spacing-top) var(--t-layout-spacing-right) var(--t-layout-spacing-bottom) var(--t-layout-spacing-left); + +// --gutter-size: var(--spacing-medium); + +// --padding-table-cell-top: 8px; +// --padding-table-cell-bottom: 8px; +// --padding-table-cell-left: 8px; +// --padding-table-cell-right: 8px; + +// /* Brand Variants */ +// --btn-inverse-bg: var(--brand-primary-400); +// --btn-info-bg: var(--brand-primary-300); +// --btn-inverse-border-color: var(--brand-primary-400); +// --btn-info-border-color: var(--brand-primary-300); + +// --btn-inverse-color: var(--color-base); +// --btn-info-color: var(--color-base); + +// --btn-inverse-bg-hover: var(--brand-primary-300); +// --btn-info-bg-hover: var(--brand-primary-300); + +// /* Alerts */ +// --alert-info-bg: var(--brand-primary-50); +// --alert-info-color: var(--brand-primary-600); +// --alert-info-border-color: var(--brand-primary); + +// /* Labels */ +// --label-info-bg: var(--brand-primary-300); +// --label-inverse-bg: var(--brand-primary-600); + +// --label-info-border-color: var(--brand-primary-300); +// --label-inverse-border-color: var(--brand-primary-600); + +// --label-info-color: var(--color-base); +// --label-inverse-color: var(--color-base); + +// /* Groupbox */ +// --groupbox-inverse-bg: var(--brand-primary-600); +// --groupbox-info-bg: var(--brand-primary-300); + +// --groupbox-inverse-color: var(--color-base); +// --groupbox-info-color: var(--color-base); + +// /* Callouts */ +// --callout-info-color: var(--brand-primary-300); +// --callout-info-bg: var(--brand-primary-50); + +// --screen-xs: #{$screen-xs}; +// --screen-sm: #{$screen-sm}; +// --screen-md: #{$screen-md}; +// --screen-lg: #{$screen-lg}; +// --screen-xl: #{$screen-xl}; +// --screen-xxl: #{$screen-xxl}; + +// /* Legacy variables */ +// --gray-lighter: var(--gray-50); +// --gray-light: var(--gray-300); +// --gray-primary: var(--gray-200); +// --gray-dark: var(--gray-700); +// --gray-darker: var(--gray-800); + +// /* Color variations */ +// --color-default-darker: var(--gray-700); +// --color-default-dark: var(--gray-600); +// --color-default-light: var(--gray-100); +// --color-default-lighter: var(--gray-50); + +// --color-primary-darker: var(--brand-primary-700); +// --color-primary-dark: var(--brand-primary-600); +// --color-primary-light: var(--brand-primary-100); +// --color-primary-lighter: var(--brand-primary-50); + +// --color-success-darker: var(--brand-success-700); +// --color-success-dark: var(--brand-success-600); +// --color-success-light: var(--brand-success-100); +// --color-success-lighter: var(--brand-success-50); + +// --color-warning-darker: var(--brand-warning-700); +// --color-warning-dark: var(--brand-warning-600); +// --color-warning-light: var(--brand-warning-100); +// --color-warning-lighter: var(--brand-warning-50); + +// --color-danger-darker: var(--brand-danger-700); +// --color-danger-dark: var(--brand-danger-600); +// --color-danger-light: var(--brand-danger-100); +// --color-danger-lighter: var(--brand-danger-50); + +// --grid-padding-top: 16px; +// --grid-padding-right: 16px; +// --grid-padding-bottom: 16px; +// --grid-padding-left: 16px; + +// --listview-padding-top: 16px; +// --listview-padding-right: 16px; +// --listview-padding-bottom: 16px; +// --listview-padding-left: 16px; +} + + +//== Media queries breakpoints +//## Define the breakpoints at which your layout will change, adapting to different screen sizes. +// $screen-xs: 480px; +// $screen-sm: 576px; +// $screen-md: 768px; +// $screen-lg: 992px; +// $screen-xl: 1200px; +// $screen-xxl: 1400px; + +// Ensures backwards compatibility. Set to false if the theme variables is not using CSS variables (e.g. --brand-primary). +$use-css-variables: true; +@import "../../themesource/atlas_core/web/variables"; + +// mxcli:theme:begin frontdesk v1 — generated by `mxcli theme apply`; edit outside this block +// Layer 1 — the Frontdesk palette. This is the file to edit. +// +// These are the theme's own semantic tokens. The next file down +// (_mxcli-frontdesk.scss) maps them onto the ~60 Atlas variables that Atlas +// components, form controls and the brand-aware pluggable widgets (Switch, +// Slider, ProgressBar, BadgeButton) actually read — so changing --mxt-brand +// here re-brands the entire app, with no per-widget CSS. +// +// The values below are the LIGHT palette, which is Frontdesk's default. The dark +// palette lives beside the light-to-Atlas mapping in _mxcli-frontdesk.scss. +// +// Mendix 11 Atlas is CSS-custom-property-first, so these are `:root` +// declarations rather than SCSS `!default` variables. This file is imported +// once per module, so it must hold declarations only — never rules. + +:root { + /* ---- the one colour that defines the app --------------------------- */ + --mxt-brand: #2F5D62; + --mxt-brand-hover: #244A4E; + --mxt-brand-ink: #FFFFFF; /* text on a brand-filled surface */ + + /* ---- semantic status ----------------------------------------------- */ + --mxt-success: #2E7D5B; + --mxt-warning: #B7791F; + --mxt-danger: #B4412F; + --mxt-info: #3B6EA5; + + /* ---- surfaces and ink ----------------------------------------------- */ + --mxt-ground: #F6F3EE; /* the app background */ + --mxt-surface: #FFFFFF; /* cards, modals, panels */ + --mxt-surface-alt: #FAF8F5; /* striped rows, subtle fills */ + --mxt-surface-hover: #F1EEE8; + --mxt-surface-selected: #E6EFEF; + --mxt-ink: #1E2A32; + --mxt-ink-muted: #56636C; + --mxt-ink-faint: #8A949B; + --mxt-line: #E4DED5; + + /* ---- the navigation rail -------------------------------------------- */ + --mxt-rail: #243138; + --mxt-rail-line: #34454D; + --mxt-rail-ink: #BCC8CD; + --mxt-rail-ink-active: #FFFFFF; + + /* ---- status pill tones ---------------------------------------------- */ + --mxt-tint-ok: #E3F1EA; + --mxt-tone-ok: #1F5C41; + --mxt-tint-warn: #FBEFD9; + --mxt-tone-warn: #7A4E0C; + --mxt-tint-risk: #F8E4DF; + --mxt-tone-risk: #8A2E1F; + --mxt-tint-info: #E3ECF5; + --mxt-tone-info: #274D78; + --mxt-tint-neutral: #ECE9E3; + + /* ---- shape, density, elevation --------------------------------------- */ + --mxt-radius: 8px; + --mxt-radius-lg: 14px; + --mxt-row-height: 56px; + --mxt-control-height: 40px; + --mxt-shadow: 0 1px 2px rgba(30,42,50,.06), 0 4px 14px rgba(30,42,50,.06); + --mxt-focus-halo: 0 0 0 3px rgba(15, 110, 107, 0.16); + + /* ---- type ------------------------------------------------------------ */ + --mxt-font: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --mxt-font-heading: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --mxt-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace; + --mxt-font-size: 15px; + --mxt-line-height: 1.5; +} +// mxcli:theme:end frontdesk 5b70ac3b9d7edcf7 diff --git a/tests/wave2/fixtures/design-reaches-app/real-ds.css b/tests/wave2/fixtures/design-reaches-app/real-ds.css new file mode 100644 index 00000000..10593ab5 --- /dev/null +++ b/tests/wave2/fixtures/design-reaches-app/real-ds.css @@ -0,0 +1,131 @@ +/* ds.css — Visitor passes design system (FrontDesk). Tokens come from mxt-tokens.css (same values, inlined here + so every wireframe links ONE file). Never redeclare a token in a wireframe. */ +:root { + --mxt-brand:#2F5D62; --mxt-brand-hover:#244A4E; --mxt-brand-ink:#fff; + --mxt-ground:#F6F3EE; --mxt-surface:#fff; --mxt-surface-alt:#FAF8F5; --mxt-surface-hover:#F1EEE8; --mxt-surface-selected:#E6EFEF; + --mxt-ink:#1E2A32; --mxt-ink-muted:#56636C; --mxt-ink-faint:#8A949B; --mxt-line:#E4DED5; + --mxt-success:#2E7D5B; --mxt-warning:#B7791F; --mxt-danger:#B4412F; --mxt-info:#3B6EA5; + --mxt-tint-ok:#E3F1EA; --mxt-tint-warn:#FBEFD9; --mxt-tint-risk:#F8E4DF; --mxt-tint-info:#E3ECF5; --mxt-tint-neutral:#ECE9E3; + --mxt-tone-ok:#1F5C41; --mxt-tone-warn:#7A4E0C; --mxt-tone-risk:#8A2E1F; --mxt-tone-info:#274D78; + --mxt-rail:#243138; --mxt-rail-ink:#BCC8CD; --mxt-rail-line:#34454D; + --mxt-radius:8px; --mxt-radius-lg:14px; --mxt-shadow:0 1px 2px rgba(30,42,50,.06),0 4px 14px rgba(30,42,50,.06); + --mxt-font:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; + --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px; +} +*{box-sizing:border-box} +body{margin:0;background:#E9E5DE;color:var(--mxt-ink);font:15px/1.5 var(--mxt-font)} +.sheet{padding:24px;display:flex;flex-direction:column;gap:28px} +.note{font-size:13px;color:var(--mxt-ink-muted);margin:0 0 6px} +.statelabel{font-weight:600;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--mxt-ink-muted);margin:0 0 8px} +.frames{display:flex;gap:28px;align-items:flex-start;flex-wrap:wrap} +.frame{background:var(--mxt-ground);border:1px solid var(--mxt-line);border-radius:12px;overflow:hidden;box-shadow:var(--mxt-shadow)} +.frame.desk{width:1180px;max-width:100%} +.frame.phone{width:390px;max-width:100%} +/* shell */ +.app{display:grid;grid-template-columns:220px 1fr;min-height:560px} +.phone .app{grid-template-columns:1fr} +.rail{background:var(--mxt-rail);color:var(--mxt-rail-ink);padding:20px 12px;display:flex;flex-direction:column;gap:4px} +.phone .rail{display:none} +.brand{color:#fff;font-weight:600;font-size:17px;padding:4px 10px 16px;letter-spacing:.01em} +.brand small{display:block;font-weight:400;font-size:12px;color:var(--mxt-rail-ink)} +.nav{display:block;padding:10px 12px;border-radius:var(--mxt-radius);color:var(--mxt-rail-ink);text-decoration:none;font-size:14px} +.nav.active{background:rgba(255,255,255,.1);color:#fff;font-weight:600} +.top{display:flex;align-items:center;justify-content:space-between;padding:14px 24px;background:var(--mxt-surface);border-bottom:1px solid var(--mxt-line)} +.phone .top{padding:12px 16px} +.who{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--mxt-ink-muted)} +.who b{color:var(--mxt-ink);font-weight:600} +.avatar{width:30px;height:30px;border-radius:50%;background:var(--mxt-surface-selected);color:var(--mxt-brand);display:grid;place-items:center;font-weight:600;font-size:12px} +.link{color:var(--mxt-brand);text-decoration:none;font-weight:600;font-size:13px} +.bottombar{display:none} +.phone .bottombar{display:flex;background:var(--mxt-surface);border-top:1px solid var(--mxt-line);position:sticky;bottom:0} +.phone .bottombar a{flex:1;text-align:center;padding:12px 4px;font-size:12px;color:var(--mxt-ink-muted);text-decoration:none} +.phone .bottombar a.active{color:var(--mxt-brand);font-weight:600;box-shadow:inset 0 2px 0 var(--mxt-brand)} +.main{padding:24px;display:flex;flex-direction:column;gap:20px} +.phone .main{padding:16px;gap:14px} +.head{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap} +h1{font-size:24px;line-height:1.2;margin:0;font-weight:600;letter-spacing:-.01em} +.phone h1{font-size:20px} +.sub{color:var(--mxt-ink-muted);font-size:14px;margin-top:2px} +/* controls */ +.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:40px;padding:0 16px;border-radius:var(--mxt-radius);border:1px solid var(--mxt-line);background:var(--mxt-surface);color:var(--mxt-ink);font:600 14px var(--mxt-font);cursor:pointer;text-decoration:none} +.btn.primary{background:var(--mxt-brand);border-color:var(--mxt-brand);color:var(--mxt-brand-ink)} +.btn.primary:hover{background:var(--mxt-brand-hover)} +.btn.big{height:44px;padding:0 22px;font-size:15px} +.btn.quiet{border-color:transparent;background:transparent;color:var(--mxt-ink-muted)} +.btn.danger{color:var(--mxt-danger);border-color:#E7C3BB} +.btn.block{width:100%} +.btn[disabled],.btn.off{opacity:.45;pointer-events:none} +.field{display:flex;flex-direction:column;gap:6px;margin-bottom:14px} +.field label{font-size:13px;font-weight:600;color:var(--mxt-ink)} +.field .in{height:40px;border:1px solid #CFC8BD;border-radius:var(--mxt-radius);background:#fff;padding:0 12px;display:flex;align-items:center;color:var(--mxt-ink)} +.field .in.ph{color:var(--mxt-ink-faint)} +.field .hint{font-size:12px;color:var(--mxt-ink-muted)} +.field.err .in{border-color:var(--mxt-danger);box-shadow:0 0 0 3px var(--mxt-tint-risk)} +.msg{display:flex;gap:8px;align-items:flex-start;padding:10px 12px;border-radius:var(--mxt-radius);font-size:14px} +.msg.err{background:var(--mxt-tint-risk);color:var(--mxt-tone-risk)} +.msg.ok{background:var(--mxt-tint-ok);color:var(--mxt-tone-ok)} +.msg.warn{background:var(--mxt-tint-warn);color:var(--mxt-tone-warn)} +/* cards, stats */ +.card{background:var(--mxt-surface);border:1px solid var(--mxt-line);border-radius:var(--mxt-radius-lg);box-shadow:var(--mxt-shadow)} +.card.pad{padding:18px} +.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:14px} +.phone .stats{gap:8px} +.stat{padding:14px 16px;border-radius:var(--mxt-radius-lg);background:var(--mxt-surface);border:1px solid var(--mxt-line)} +.phone .stat{padding:10px 12px} +.stat .n{font-size:30px;font-weight:600;line-height:1.1;letter-spacing:-.02em} +.phone .stat .n{font-size:24px} +.stat .l{font-size:12px;color:var(--mxt-ink-muted);text-transform:uppercase;letter-spacing:.06em} +.stat.late{background:var(--mxt-tint-warn);border-color:#EAD3A6}.stat.late .n{color:var(--mxt-tone-warn)} +.stat.in{background:var(--mxt-tint-ok);border-color:#BFDCCB}.stat.in .n{color:var(--mxt-tone-ok)} +.sec{display:flex;align-items:center;gap:10px;margin:6px 0 -8px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--mxt-ink-muted);font-weight:600} +.sec .c{background:var(--mxt-tint-neutral);border-radius:99px;padding:0 8px;font-size:12px;color:var(--mxt-ink)} +.sec.late{color:var(--mxt-tone-warn)}.sec.late .c{background:var(--mxt-tint-warn);color:var(--mxt-tone-warn)} +/* pills */ +.pill{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 10px;border-radius:99px;font-size:12px;font-weight:600;white-space:nowrap} +.pill::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor} +.pill.expected{background:var(--mxt-tint-info);color:var(--mxt-tone-info)} +.pill.in{background:var(--mxt-tint-ok);color:var(--mxt-tone-ok)} +.pill.out{background:var(--mxt-tint-neutral);color:var(--mxt-ink-muted)} +.pill.cancelled{background:var(--mxt-tint-neutral);color:var(--mxt-ink-faint);text-decoration:line-through} +.pill.noshow{background:var(--mxt-tint-risk);color:var(--mxt-tone-risk)} +.pill.late{background:var(--mxt-tint-warn);color:var(--mxt-tone-warn)} +.pill.svc{background:var(--mxt-tint-ok);color:var(--mxt-tone-ok)} +.pill.oos{background:var(--mxt-tint-risk);color:var(--mxt-tone-risk)} +/* visit rows */ +.rows{display:flex;flex-direction:column;gap:10px} +.vrow{display:grid;grid-template-columns:78px 1fr auto;align-items:center;gap:16px;padding:14px 18px;background:var(--mxt-surface);border:1px solid var(--mxt-line);border-radius:var(--mxt-radius-lg);box-shadow:var(--mxt-shadow)} +.vrow.late{border-left:5px solid var(--mxt-warning);background:#FFFDF8} +.vrow .t{font-size:20px;font-weight:600;font-variant-numeric:tabular-nums} +.vrow .t small{display:block;font-size:11px;font-weight:600;color:var(--mxt-tone-warn);letter-spacing:.04em;text-transform:uppercase} +.vrow .n{font-size:16px;font-weight:600} +.vrow .m{font-size:13px;color:var(--mxt-ink-muted)} +.vrow .acts{display:flex;gap:8px;align-items:center} +.badge-no{font-variant-numeric:tabular-nums;background:var(--mxt-ink);color:#fff;border-radius:6px;padding:2px 9px;font-weight:600;font-size:13px} +.phone .vrow{grid-template-columns:1fr;gap:10px;padding:12px 14px} +.phone .vrow .acts .btn{flex:1} +.hostcard{padding:14px 16px;background:var(--mxt-surface);border:1px solid var(--mxt-line);border-radius:var(--mxt-radius-lg);display:flex;flex-direction:column;gap:6px} +.hostcard .top2{display:flex;justify-content:space-between;align-items:flex-start;gap:10px} +/* table */ +.tbl{width:100%;border-collapse:collapse;background:var(--mxt-surface);border:1px solid var(--mxt-line);border-radius:var(--mxt-radius-lg);overflow:hidden} +.tbl th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--mxt-ink-muted);text-align:left;padding:10px 16px;background:var(--mxt-surface-alt);border-bottom:1px solid var(--mxt-line)} +.tbl td{padding:12px 16px;border-bottom:1px solid var(--mxt-line);height:56px} +.tbl tr:last-child td{border-bottom:0} +/* dialog */ +.scrim{background:rgba(30,42,50,.45);padding:40px 20px;display:flex;justify-content:center} +.phone .scrim{padding:20px 10px} +.dlg{background:var(--mxt-surface);border-radius:var(--mxt-radius-lg);width:460px;max-width:100%;box-shadow:0 12px 40px rgba(0,0,0,.25);padding:22px;display:flex;flex-direction:column;gap:6px} +.dlg h3{margin:0 0 4px;font-size:18px} +.dlg .foot{display:flex;justify-content:flex-end;gap:8px;margin-top:12px} +.phone .dlg .foot .btn{flex:1} +.empty{text-align:center;padding:36px 16px;color:var(--mxt-ink-muted);border:1px dashed #D2CABD;border-radius:var(--mxt-radius-lg);background:var(--mxt-surface-alt)} +.empty b{display:block;color:var(--mxt-ink);font-size:16px;margin-bottom:4px} +/* binding annotation (wireframe apparatus) */ +.wf-contract{font-size:12px;color:var(--mxt-ink-muted);background:#fff;border:1px dashed #C9C1B4;border-radius:8px;padding:8px 12px;max-width:760px} +.wf-contract p{margin:2px 0} +table.bind{border-collapse:collapse;font-size:12px;background:#fff;max-width:1180px} +table.bind th,table.bind td{border:1px solid var(--mxt-line);padding:4px 8px;text-align:left} +table.bind th{background:var(--mxt-surface-alt)} +/* approval change: Check in is the most prominent thing on an expected visitor row */ +.vrow .btn.primary.big{height:48px;min-width:128px;font-size:16px;box-shadow:0 2px 8px rgba(47,93,98,.35)} +.phone .vrow .btn.primary.big{height:52px;font-size:17px} +.vrow .btn.big:not(.primary){height:40px;font-size:14px} diff --git a/tests/wave2/test-design-reaches-app.sh b/tests/wave2/test-design-reaches-app.sh index 81a05d92..31ce1a8d 100755 --- a/tests/wave2/test-design-reaches-app.sh +++ b/tests/wave2/test-design-reaches-app.sh @@ -243,5 +243,75 @@ out="$(run "$d")" hasnt "a descendant selector is not the root theme block" "$out" "tokens valued 9 of 9" has "the descendant block counts as built absent" "$out" "carry the design's value" +# ── 12. CSS layout must not change the reading (#670) ────────────────────────────────────── +# Golden input is REAL (see CAPTURE.md, "#670 captures"): real-ds.css is a design stylesheet with +# several tokens per line, real-built-excerpt.css is verbatim line ranges of a compiled theme +# (bare :root with the design's tokens, a multi-line selector list `.card, ...`, two rules +# inside @media, and `.card-body`). The old readers saw 10 of the design's 38 tokens. +real() { # $1 name $2 design $3 built [$4 "two" for the app/ layout] + d="$TMP/$1"; rm -rf "$d"; mkdir -p "$d/design" "$d/deployment/web" "$d/theme/web" + cp "$2" "$d/design/ds.css"; cp "$3" "$d/deployment/web/theme.compiled.css" + cp "$FIX/real-custom-variables.scss" "$d/theme/web/custom-variables.scss" + if [ "${4:-}" = "two" ]; then + mkdir -p "$d/app"; mv "$d/theme" "$d/deployment" "$d/app/"; : > "$d/app/App.mpr" + fi + printf '%s' "$d" +} +summ() { printf '%s\n' "$1" | grep -E '^(knobs bound|tokens arrived|tokens valued|classes arrived)'; } +tr '\n' ' ' < "$FIX/real-ds.css" > "$TMP/min-ds.css" +tr '\n' ' ' < "$FIX/real-built-excerpt.css" > "$TMP/min-built.css" + +d="$(real realml "$FIX/real-ds.css" "$FIX/real-built-excerpt.css")" +base="$(summ "$(run "$d")")" +has "real sheets: every design token is counted, several per line" "$base" "tokens arrived 32 of 38" +has "real sheets: values compared with the \`}\` gone" "$base" "tokens valued 30 of 38" +has "real sheets: knobs bound through var() are counted" "$base" "knobs bound 31 of 46" +has "real sheets: whole-class count" "$base" "classes arrived 1 of 66" + +# The same two sheets with every newline removed (one line each, comments left in) must read +# identically: nothing in the numbers may depend on how the CSS is laid out. +d="$(real realmin "$TMP/min-ds.css" "$TMP/min-built.css")" +check "one-line design + one-line built read as the multi-line pair" "$(summ "$(run "$d")")" "$base" +check "one-line pair exits like the multi-line pair" "$(code "$d")" "$(code "$(real realml2 "$FIX/real-ds.css" "$FIX/real-built-excerpt.css")")" + +# Both layouts: the same real pair with the app under app/ (.mpr present). +d="$(real realtwo "$FIX/real-ds.css" "$FIX/real-built-excerpt.css" two)" +check "two-tree reads the real pair like the single tree" "$(summ "$(run "$d")")" "$base" +has "two-tree names the built sheet under app/" "$(run "$d")" "built stylesheet app/deployment/web/theme.compiled.css" + +# Small synthetic twins for the individual faults; the real pair above is the golden input. +mini() { # $1 name $2 design css $3 built css + d="$(project "$1" "" true)"; printf '%s' "$2" > "$d/design/ds.css"; printf '%s' "$3" > "$d/deployment/web/theme.compiled.css"; printf '%s' "$d" +} +# fault 1: a one-line :root; the value must not keep its `}`; a later rule's --leak is no token +d="$(mini oneline ':root{--a:1px;--b:2px;--c:3px}.x{--leak:9px}' ':root{--a:1px;--b:2px;--c:3px}')" +out="$(run "$d")" +has "one-line :root declares 3 tokens, not 1" "$out" "tokens arrived 3 of 3" +has "values compare without a trailing brace" "$out" "tokens valued 3 of 3" +# fault 2: minified built sheet still has its bare :root +d="$(mini minbuilt ':root{--brand-primary:#111111}' ':root{--brand-primary:#111111}.x{color:red}')" +has "minified built sheet: bare :root is found, knob bound by literal" "$(run "$d")" "knobs bound 1 of 4" +# fault 3: .card is not arrived because .card-header is +d="$(mini wholeclass '.card{a:b}.hdr{a:b}' '.card-header{a:b}.hdr{a:b}')" +out="$(run "$d")" +has ".card is NOT counted by .card-header" "$out" "classes arrived 1 of 2" +has "names the missing class" "$out" "Absent: .card" +d="$(mini wholeclass2 '.card{a:b}.hdr{a:b}' '.card, .x > .y{a:b}.hdr:hover{a:b}')" +has "a class inside a selector list / pseudo still counts" "$(run "$d")" "classes arrived 2 of 2" +# fault 4: multi-line selector list, rule inside @media +d="$(mini media '@media (min-width: 1px) { + .m1 { a: b } +} +.ml-a, +.ml-b { a: b }' '.m1{a:b}.ml-a{a:b}.ml-b{a:b}')" +has "rules in @media and multi-line selector lists are read" "$(run "$d")" "classes arrived 3 of 3" +# fault 5: var(--x, fallback) uses --x +d="$(mini fallback ':root{--t1:#abcdef}' ':root{--brand-primary:var(--t1, #fff);--font-size-default:var( --nope , 1px)}')" +has "var(--t1, fallback) counts as bound to --t1" "$(run "$d")" "knobs bound 1 of 4" +# a model class: whole-class match against the built sheet +d="$(mini modelcls '.a{a:b}' '.a{a:b}.card-header{a:b}')" +mkdir -p "$d/mdlsource"; printf "Class: 'card'\n" > "$d/mdlsource/p.mdl" +has "model class .card is not defined by .card-header" "$(run "$d")" "1 of 1 classes the MODEL asks for are not defined" + printf '\n%s passed, %s failed\n' "$PASS" "$FAIL" [ "$FAIL" -eq 0 ] || exit 1 From 9d9d93a402009df782674aa5a100440a915ff925 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 11:00:09 +0000 Subject: [PATCH 2/3] fix(changelog): design-reaches-app line under Unreleased, figures from the committed fixture pair Review fix: the line had landed inside the release-cycle paragraph, where cut-release.sh would not find it. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d255080b..01874d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ Kinds: `new` · `fix` · `learn` (a skill/learning) · `process` (rules, templat Credit the person or project that surfaced the change — the credit line is the thank-you. **Release cycle (from 2026-09-19).** New lines land under `## Unreleased`, at the top. Every few -- fix(design-reaches-app): **the CSS readers now normalise both stylesheets first and match whole classes (#670)**: a one-line `:root{--a:1;--b:2}` read as 1 token with `}` in its value, a minified built sheet as having no bare `:root`, a rule inside `@media` or a multi-line selector list was missed, and `var(--x, fallback)` did not count as using `--x`. All five fixed by one `normalise_css()` step (comments dropped, one selector/declaration/`}` per line, `;` inside `url(...)`/strings kept). On the field project's real sheets the design went from 10 to 38 tokens read and knobs bound from 12 to 37. **Can turn a project red:** class presence was a substring match, so `.card` counted as arrived when only `.card-header` existed; it is now a whole-class match, and a project that passed on that false reading can now report `0 of N design-system classes` (FAIL) or fewer classes arrived. No verdict names or exit codes changed — outside review of v2026.11.12 days the maintainer cuts a release with `bin/cut-release.sh`: the `## Unreleased` heading becomes `## vYYYY.MM.DD`, master is tagged `vYYYY.MM.DD`, and a fresh empty `## Unreleased` opens. So the top section is always "what changed since the last release", a project reports the release it is @@ -19,6 +18,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m `docs/RELEASE-PROCESS.md` §8. ## Unreleased +- fix(design-reaches-app): **the CSS readers now normalise both stylesheets first and match whole classes (#670)**: a one-line `:root{--a:1;--b:2}` read as 1 token with `}` in its value, a minified built sheet as having no bare `:root`, a rule inside `@media` or a multi-line selector list was missed, and `var(--x, fallback)` did not count as using `--x`. All five fixed by one `normalise_css()` step (comments dropped, one selector/declaration/`}` per line, `;` inside `url(...)`/strings kept). On the committed real pair (design and built sheets of a fictional test app, 2026-11) tokens arrived went from 9 of 10 to 32 of 38 and theme knobs bound from 10 of 46 to 31 of 46; the check takes about 7 s instead of 3 s on a 1 MB built sheet, because it now reads all the tokens. **Can turn a project red:** class presence was a substring match, so `.card` counted as arrived when only `.card-header` existed; it is now a whole-class match, and a project that passed on that false reading can now report `0 of N design-system classes` (FAIL) or fewer classes arrived. No verdict names or exit codes changed — #670; outside review of v2026.11.12 ## v2026.11.12 - new(parallel-builds): **`skills/parallel-builds.md`, an optional reference outside the pipeline (experimental tier, no stage map, no agent stub; used only when the user asks): several builders on one app in git worktrees, and how to merge their branches without losing a module (#741)**: a plain `git merge` of the model left one branch's units unregistered and both build gates reported 0 errors; the skill sets up worktrees (ports, database, number blocks, owners), forbids the plain merge, names the merge paths per CLI version, and checks `mxcli fix hashes`, `show modules` and the shared units before the build gate; forbids `diag --check-units --fix` after a suspect merge; a field run, 2026-11, lost a 63-unit module. Verbatim `fix hashes` capture under `tests/wave2/fixtures/worktree-merge/`. Guard script, exec.sh merge mode and lint delta are not built — field reports #741 and #744; LoanDesk worktree merge test From 9ed2425fc68245394aea75fadb6575ae008ea5a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 11:14:38 +0000 Subject: [PATCH 3/3] fix(changelog): the timing is for the committed pair, not a sheet size Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01874d53..3a574f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m `docs/RELEASE-PROCESS.md` §8. ## Unreleased -- fix(design-reaches-app): **the CSS readers now normalise both stylesheets first and match whole classes (#670)**: a one-line `:root{--a:1;--b:2}` read as 1 token with `}` in its value, a minified built sheet as having no bare `:root`, a rule inside `@media` or a multi-line selector list was missed, and `var(--x, fallback)` did not count as using `--x`. All five fixed by one `normalise_css()` step (comments dropped, one selector/declaration/`}` per line, `;` inside `url(...)`/strings kept). On the committed real pair (design and built sheets of a fictional test app, 2026-11) tokens arrived went from 9 of 10 to 32 of 38 and theme knobs bound from 10 of 46 to 31 of 46; the check takes about 7 s instead of 3 s on a 1 MB built sheet, because it now reads all the tokens. **Can turn a project red:** class presence was a substring match, so `.card` counted as arrived when only `.card-header` existed; it is now a whole-class match, and a project that passed on that false reading can now report `0 of N design-system classes` (FAIL) or fewer classes arrived. No verdict names or exit codes changed — #670; outside review of v2026.11.12 +- fix(design-reaches-app): **the CSS readers now normalise both stylesheets first and match whole classes (#670)**: a one-line `:root{--a:1;--b:2}` read as 1 token with `}` in its value, a minified built sheet as having no bare `:root`, a rule inside `@media` or a multi-line selector list was missed, and `var(--x, fallback)` did not count as using `--x`. All five fixed by one `normalise_css()` step (comments dropped, one selector/declaration/`}` per line, `;` inside `url(...)`/strings kept). On the committed real pair (design and built sheets of a fictional test app, 2026-11) tokens arrived went from 9 of 10 to 32 of 38 and theme knobs bound from 10 of 46 to 31 of 46; the check takes about 7 s instead of 3 s on that pair, because it now reads all the tokens. **Can turn a project red:** class presence was a substring match, so `.card` counted as arrived when only `.card-header` existed; it is now a whole-class match, and a project that passed on that false reading can now report `0 of N design-system classes` (FAIL) or fewer classes arrived. No verdict names or exit codes changed — #670; outside review of v2026.11.12 ## v2026.11.12 - new(parallel-builds): **`skills/parallel-builds.md`, an optional reference outside the pipeline (experimental tier, no stage map, no agent stub; used only when the user asks): several builders on one app in git worktrees, and how to merge their branches without losing a module (#741)**: a plain `git merge` of the model left one branch's units unregistered and both build gates reported 0 errors; the skill sets up worktrees (ports, database, number blocks, owners), forbids the plain merge, names the merge paths per CLI version, and checks `mxcli fix hashes`, `show modules` and the shared units before the build gate; forbids `diag --check-units --fix` after a suspect merge; a field run, 2026-11, lost a 63-unit module. Verbatim `fix hashes` capture under `tests/wave2/fixtures/worktree-merge/`. Guard script, exec.sh merge mode and lint delta are not built — field reports #741 and #744; LoanDesk worktree merge test