From 43ac930586dea40f143e7ab85befcf32c3029062 Mon Sep 17 00:00:00 2001 From: Joel Gomez Caballe Date: Mon, 31 Aug 2026 15:43:36 +0200 Subject: [PATCH 01/12] Borderless title-less modal headers, bold h1, load-bearing hr margin - A .modal-header holding only the close button drew Bootstrap's hairline under nothing; detect the title structurally with :has() and zero the border width for that case only. - h1 goes bold (700): Bootstrap's reboot leaves headings at 500, and h1 carried 700 through v2 (Stisla set every heading bold). - The hr rhythm (dashed, 2.5rem) takes !important on its margin so scoped tweaks cannot silently compress the separator. Static catalogue rebuilt (castor catalog:build --no-shot). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G9HvRYqT42n7yvuCwqHGSh --- docs/catalog.html | 21 ++++++++++++++++++- docs/index.html | 21 ++++++++++++++++++- .../public/css/components/_modal.css | 10 +++++++++ .../public/css/foundations/_typography.css | 11 +++++++++- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/docs/catalog.html b/docs/catalog.html index 161ecf95..c0dc7b21 100644 --- a/docs/catalog.html +++ b/docs/catalog.html @@ -575,9 +575,18 @@ color: var(--aro-color-secondary); } +/* Bootstrap's reboot leaves headings at 500; h1 carried 700 through v2 (Stisla set every + heading bold) and reads underweight without it. Only h1 is pinned — the smaller headings + are sized and weighted by the components that use them. */ +h1 { + font-weight: var(--aro-weight-bold); +} + +/* The margin is load-bearing rhythm: !important so a scoped tweak cannot silently + compress the separator back to Bootstrap's default. */ hr { border-top-style: dashed; - margin: 2.5rem 0; + margin: 2.5rem 0 !important; } p { @@ -3503,6 +3512,16 @@ --bs-modal-header-padding: var(--aro-space-4) var(--aro-space-6); } + +/* + * A header with no title is just a close-button strip: the hairline under it would + * underline nothing and reads as a stray rule (e.g. a modal whose heading lives in the + * body). Detected structurally, so titled headers keep their separator. + */ +.modal-header:not(:has(.modal-title, h1, h2, h3, h4, h5, h6)) { + --bs-modal-header-border-width: 0; +} + /* * Size scale (design system): sm 400 · md 520 (default) · lg 720 · xl 960. Set on * `.modal-dialog`, so both plain modals and the Dialog component — which builds a diff --git a/docs/index.html b/docs/index.html index 161ecf95..c0dc7b21 100644 --- a/docs/index.html +++ b/docs/index.html @@ -575,9 +575,18 @@ color: var(--aro-color-secondary); } +/* Bootstrap's reboot leaves headings at 500; h1 carried 700 through v2 (Stisla set every + heading bold) and reads underweight without it. Only h1 is pinned — the smaller headings + are sized and weighted by the components that use them. */ +h1 { + font-weight: var(--aro-weight-bold); +} + +/* The margin is load-bearing rhythm: !important so a scoped tweak cannot silently + compress the separator back to Bootstrap's default. */ hr { border-top-style: dashed; - margin: 2.5rem 0; + margin: 2.5rem 0 !important; } p { @@ -3503,6 +3512,16 @@ --bs-modal-header-padding: var(--aro-space-4) var(--aro-space-6); } + +/* + * A header with no title is just a close-button strip: the hairline under it would + * underline nothing and reads as a stray rule (e.g. a modal whose heading lives in the + * body). Detected structurally, so titled headers keep their separator. + */ +.modal-header:not(:has(.modal-title, h1, h2, h3, h4, h5, h6)) { + --bs-modal-header-border-width: 0; +} + /* * Size scale (design system): sm 400 · md 520 (default) · lg 720 · xl 960. Set on * `.modal-dialog`, so both plain modals and the Dialog component — which builds a diff --git a/src/Resources/public/css/components/_modal.css b/src/Resources/public/css/components/_modal.css index 9f23ac92..45a2f039 100644 --- a/src/Resources/public/css/components/_modal.css +++ b/src/Resources/public/css/components/_modal.css @@ -16,6 +16,16 @@ --bs-modal-header-padding: var(--aro-space-4) var(--aro-space-6); } + +/* + * A header with no title is just a close-button strip: the hairline under it would + * underline nothing and reads as a stray rule (e.g. a modal whose heading lives in the + * body). Detected structurally, so titled headers keep their separator. + */ +.modal-header:not(:has(.modal-title, h1, h2, h3, h4, h5, h6)) { + --bs-modal-header-border-width: 0; +} + /* * Size scale (design system): sm 400 · md 520 (default) · lg 720 · xl 960. Set on * `.modal-dialog`, so both plain modals and the Dialog component — which builds a diff --git a/src/Resources/public/css/foundations/_typography.css b/src/Resources/public/css/foundations/_typography.css index 74284484..250c44b8 100644 --- a/src/Resources/public/css/foundations/_typography.css +++ b/src/Resources/public/css/foundations/_typography.css @@ -41,9 +41,18 @@ a:where(:not(.btn):not(.page-link)):focus { color: var(--aro-color-secondary); } +/* Bootstrap's reboot leaves headings at 500; h1 carried 700 through v2 (Stisla set every + heading bold) and reads underweight without it. Only h1 is pinned — the smaller headings + are sized and weighted by the components that use them. */ +h1 { + font-weight: var(--aro-weight-bold); +} + +/* The margin is load-bearing rhythm: !important so a scoped tweak cannot silently + compress the separator back to Bootstrap's default. */ hr { border-top-style: dashed; - margin: 2.5rem 0; + margin: 2.5rem 0 !important; } p { From 58548ebb956ee977acecf69f01e69b9c10d8e56a Mon Sep 17 00:00:00 2001 From: Joel Gomez Caballe Date: Mon, 31 Aug 2026 16:07:21 +0200 Subject: [PATCH 02/12] Give .dropdown-list panels back their width The navbar notification panels (communication, sync) collapsed to their content: v2's '.dropdown-list { width: 350px; padding: 0 }' was lost in the ablation. Restore it as a min-width, and drop the menu padding the rows never expect (they carry their own). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G9HvRYqT42n7yvuCwqHGSh --- docs/catalog.html | 11 +++++++++++ docs/index.html | 11 +++++++++++ src/Resources/public/css/components/_dropdown.css | 11 +++++++++++ 3 files changed, 33 insertions(+) diff --git a/docs/catalog.html b/docs/catalog.html index c0dc7b21..7ebd0bcf 100644 --- a/docs/catalog.html +++ b/docs/catalog.html @@ -2384,6 +2384,17 @@ line-height: var(--aro-leading-tight); } + +/* + * Notification-style panel (.dropdown-list): the navbar dropdowns whose items are rows + * with avatar/time metadata. v2 sized it at 350px with no menu padding (rows carry their + * own); both were lost in the ablation and the panel collapsed to its content. + */ +.dropdown-menu.dropdown-list { + min-width: 350px; + padding: 0; +} + /* Active / focused item — relocated from the style.css tail (lot B). Unlayered, so it beats Bootstrap's layered `.dropdown-item` rules without `!important`. */ a.dropdown-item:focus, a.dropdown-item:active, a.dropdown-item.active { diff --git a/docs/index.html b/docs/index.html index c0dc7b21..7ebd0bcf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2384,6 +2384,17 @@ line-height: var(--aro-leading-tight); } + +/* + * Notification-style panel (.dropdown-list): the navbar dropdowns whose items are rows + * with avatar/time metadata. v2 sized it at 350px with no menu padding (rows carry their + * own); both were lost in the ablation and the panel collapsed to its content. + */ +.dropdown-menu.dropdown-list { + min-width: 350px; + padding: 0; +} + /* Active / focused item — relocated from the style.css tail (lot B). Unlayered, so it beats Bootstrap's layered `.dropdown-item` rules without `!important`. */ a.dropdown-item:focus, a.dropdown-item:active, a.dropdown-item.active { diff --git a/src/Resources/public/css/components/_dropdown.css b/src/Resources/public/css/components/_dropdown.css index 7c229927..161eb7aa 100644 --- a/src/Resources/public/css/components/_dropdown.css +++ b/src/Resources/public/css/components/_dropdown.css @@ -79,6 +79,17 @@ line-height: var(--aro-leading-tight); } + +/* + * Notification-style panel (.dropdown-list): the navbar dropdowns whose items are rows + * with avatar/time metadata. v2 sized it at 350px with no menu padding (rows carry their + * own); both were lost in the ablation and the panel collapsed to its content. + */ +.dropdown-menu.dropdown-list { + min-width: 350px; + padding: 0; +} + /* Active / focused item — relocated from the style.css tail (lot B). Unlayered, so it beats Bootstrap's layered `.dropdown-item` rules without `!important`. */ a.dropdown-item:focus, a.dropdown-item:active, a.dropdown-item.active { From 3ac2fc32b403784fcca53b2a7009bec23a3d8804 Mon Sep 17 00:00:00 2001 From: Joel Gomez Caballe Date: Mon, 31 Aug 2026 16:13:34 +0200 Subject: [PATCH 03/12] Solid presentation for Bootstrap .alert-* variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flash messages and legacy templates use Bootstrap's alert vocabulary, which v3 left on Bootstrap's pale defaults. Paint them the way Tick&Live wore them through v2: semantic fill, white text (links included), no border — driven through --bs-alert-* variables like the other bridged components. Also kill the stray bottom margin on a

inside any alert (v2 rule). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G9HvRYqT42n7yvuCwqHGSh --- docs/catalog.html | 32 +++++++++++++++++++ docs/index.html | 32 +++++++++++++++++++ .../public/css/components/_alert.css | 32 +++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/docs/catalog.html b/docs/catalog.html index 7ebd0bcf..ed3f9bea 100644 --- a/docs/catalog.html +++ b/docs/catalog.html @@ -3975,6 +3975,38 @@ .aro-alert--info { --aro-alert-accent: var(--aro-color-info); } .aro-alert--primary { --aro-alert-accent: var(--aro-color-primary); } +/* ---------------------------------------------------- Bootstrap .alert-* variants */ + +/* + * Solid presentation for Bootstrap's own alert vocabulary — flash messages and legacy + * templates that never migrated to .aro-alert: semantic fill, white text, no border, + * the look Tick&Live carried through v2. The .aro-alert component above keeps its + * tinted default; `.aro-alert--solid` gives the same tone there. + * + * Driven through Bootstrap's own --bs-alert-* variables (unlayered, so they outrank the + * layered defaults), like the card and dropdown components. + */ +.alert-success, .alert-danger, .alert-warning, .alert-info { + --bs-alert-color: var(--aro-color-text-inverted); + --bs-alert-border-color: transparent; + --bs-alert-link-color: var(--aro-color-text-inverted); +} + +.alert-success { --bs-alert-bg: var(--aro-color-success); } +.alert-danger { --bs-alert-bg: var(--aro-color-danger); } +.alert-warning { --bs-alert-bg: var(--aro-color-warning); } +.alert-info { --bs-alert-bg: var(--aro-color-info); } + +/* Content links go white with their panel (v2: `.alert:not(.alert-light) a`). */ +.alert-success a, .alert-danger a, .alert-warning a, .alert-info a { + color: inherit; +} + +/* A paragraph is the alert's own line of text, not prose: no trailing rhythm (v2 rule). */ +.alert p { + margin-bottom: 0; +} + /* ./components/_section-header.css */ /** * Section header — the identity block at the top of every screen. diff --git a/docs/index.html b/docs/index.html index 7ebd0bcf..ed3f9bea 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3975,6 +3975,38 @@ .aro-alert--info { --aro-alert-accent: var(--aro-color-info); } .aro-alert--primary { --aro-alert-accent: var(--aro-color-primary); } +/* ---------------------------------------------------- Bootstrap .alert-* variants */ + +/* + * Solid presentation for Bootstrap's own alert vocabulary — flash messages and legacy + * templates that never migrated to .aro-alert: semantic fill, white text, no border, + * the look Tick&Live carried through v2. The .aro-alert component above keeps its + * tinted default; `.aro-alert--solid` gives the same tone there. + * + * Driven through Bootstrap's own --bs-alert-* variables (unlayered, so they outrank the + * layered defaults), like the card and dropdown components. + */ +.alert-success, .alert-danger, .alert-warning, .alert-info { + --bs-alert-color: var(--aro-color-text-inverted); + --bs-alert-border-color: transparent; + --bs-alert-link-color: var(--aro-color-text-inverted); +} + +.alert-success { --bs-alert-bg: var(--aro-color-success); } +.alert-danger { --bs-alert-bg: var(--aro-color-danger); } +.alert-warning { --bs-alert-bg: var(--aro-color-warning); } +.alert-info { --bs-alert-bg: var(--aro-color-info); } + +/* Content links go white with their panel (v2: `.alert:not(.alert-light) a`). */ +.alert-success a, .alert-danger a, .alert-warning a, .alert-info a { + color: inherit; +} + +/* A paragraph is the alert's own line of text, not prose: no trailing rhythm (v2 rule). */ +.alert p { + margin-bottom: 0; +} + /* ./components/_section-header.css */ /** * Section header — the identity block at the top of every screen. diff --git a/src/Resources/public/css/components/_alert.css b/src/Resources/public/css/components/_alert.css index ca7c584e..e3926c56 100644 --- a/src/Resources/public/css/components/_alert.css +++ b/src/Resources/public/css/components/_alert.css @@ -177,3 +177,35 @@ .aro-alert--warning { --aro-alert-accent: var(--aro-color-warning); } .aro-alert--info { --aro-alert-accent: var(--aro-color-info); } .aro-alert--primary { --aro-alert-accent: var(--aro-color-primary); } + +/* ---------------------------------------------------- Bootstrap .alert-* variants */ + +/* + * Solid presentation for Bootstrap's own alert vocabulary — flash messages and legacy + * templates that never migrated to .aro-alert: semantic fill, white text, no border, + * the look Tick&Live carried through v2. The .aro-alert component above keeps its + * tinted default; `.aro-alert--solid` gives the same tone there. + * + * Driven through Bootstrap's own --bs-alert-* variables (unlayered, so they outrank the + * layered defaults), like the card and dropdown components. + */ +.alert-success, .alert-danger, .alert-warning, .alert-info { + --bs-alert-color: var(--aro-color-text-inverted); + --bs-alert-border-color: transparent; + --bs-alert-link-color: var(--aro-color-text-inverted); +} + +.alert-success { --bs-alert-bg: var(--aro-color-success); } +.alert-danger { --bs-alert-bg: var(--aro-color-danger); } +.alert-warning { --bs-alert-bg: var(--aro-color-warning); } +.alert-info { --bs-alert-bg: var(--aro-color-info); } + +/* Content links go white with their panel (v2: `.alert:not(.alert-light) a`). */ +.alert-success a, .alert-danger a, .alert-warning a, .alert-info a { + color: inherit; +} + +/* A paragraph is the alert's own line of text, not prose: no trailing rhythm (v2 rule). */ +.alert p { + margin-bottom: 0; +} From b9d5081f14d4b08bc885b6cab02039f1377d8321 Mon Sep 17 00:00:00 2001 From: Joel Gomez Caballe Date: Mon, 31 Aug 2026 16:49:39 +0200 Subject: [PATCH 04/12] Derive the bridge's RGB triplets from companion tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bridge hardcoded every --bs-*-rgb triplet: an integrator overriding a colour token still got the default palette in everything Bootstrap builds from rgba() — .bg-*, .text-bg-*, focus rings — and no override rule can catch a utility, since Bootstrap's layered !important beats any unlayered one. CSS cannot decompose a hex token into a bare comma triplet (relative colour syntax cannot escape its own function), so each bridged colour now carries an --aro-color-*-rgb companion in _tokens.css and the bridge maps it: one place to override the pair, no literal left behind. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G9HvRYqT42n7yvuCwqHGSh --- doc/theming.md | 7 +++ docs/catalog.html | 43 +++++++++++++------ docs/index.html | 43 +++++++++++++------ .../css/foundations/_bootstrap-bridge.css | 28 ++++++------ .../public/css/foundations/_tokens.css | 15 +++++++ 5 files changed, 100 insertions(+), 36 deletions(-) diff --git a/doc/theming.md b/doc/theming.md index b6cb632b..758626df 100644 --- a/doc/theming.md +++ b/doc/theming.md @@ -60,6 +60,13 @@ Override only these — the semantic layer. Values below are the defaults. > The `-contrast` companion of each (`--aro-color-primary-contrast`…) is the text colour > drawn on top of it — set it too if your brand colour needs dark text. +> **Override the `-rgb` companion with its colour.** Every bridged colour has an +> `--aro-color-*-rgb` twin holding its bare `r, g, b` triplet (`--aro-color-secondary-rgb: +> 46, 79, 94`). Bootstrap's utilities (`.bg-*`, `.text-bg-*`, focus rings) consume the +> triplet, not the hex — re-theme a colour without its twin and those utilities keep the +> default palette. No override rule can catch them either: the layered `!important` on +> Bootstrap's utilities beats any unlayered one. The variable pair is the seam. + ### Semantic `--aro-color-success` `#63CEB3` · `--aro-color-danger` `#E52321` · diff --git a/docs/catalog.html b/docs/catalog.html index ed3f9bea..0fc69aa7 100644 --- a/docs/catalog.html +++ b/docs/catalog.html @@ -157,12 +157,21 @@ /* ------------------------------------------------------------------ Brand */ --aro-color-primary: #06BAB4; + /* + * `-rgb` companions. Bootstrap's legacy `rgba(var(--bs-*-rgb), a)` consumers need a + * bare comma triplet, and CSS cannot decompose a hex token into one (relative colour + * syntax cannot produce a naked triplet). So each colour the bridge translates carries + * its triplet as a token: override a colour, override its companion — the bridge maps + * both and hardcodes nothing. + */ + --aro-color-primary-rgb: 6, 186, 180; --aro-color-primary-hover: #05A29D; --aro-color-primary-contrast: #FFFFFF; /* Translucent teal tint — soft primary surface (e.g. the navbar user-icon circle). */ --aro-color-primary-subtle: rgba(122, 220, 218, 0.71); --aro-color-secondary: #2E4F5E; + --aro-color-secondary-rgb: 46, 79, 94; --aro-color-secondary-hover: #223C48; --aro-color-secondary-contrast: #FFFFFF; @@ -178,13 +187,18 @@ /* -------------------------------------------------------------- Semantic */ --aro-color-success: #63CEB3; + --aro-color-success-rgb: 99, 206, 179; --aro-color-danger: #E52321; + --aro-color-danger-rgb: 229, 35, 33; --aro-color-warning: #F25C05; + --aro-color-warning-rgb: 242, 92, 5; --aro-color-info: #E39B02; + --aro-color-info-rgb: 227, 155, 2; /* -------------------------------------------------------------- Surfaces */ --aro-color-bg: #F5F8FA; + --aro-color-bg-rgb: 245, 248, 250; --aro-color-surface: #FFFFFF; --aro-color-surface-muted: #EBEBEB; /* Select2 multi-select chip fill (lot B). */ /* Chrome that must read as inert beside the content it captions — the editor toolbar. @@ -195,6 +209,7 @@ /* ------------------------------------------------------------------ Text */ --aro-color-text: #333333; + --aro-color-text-rgb: 51, 51, 51; --aro-color-text-strong: #181C32; --aro-color-text-label: #3F4254; --aro-color-text-muted: #99A1B7; @@ -426,37 +441,41 @@ * `--bs-*-rgb` must be set alongside every colour. Bootstrap uses the RGB triplets inside * `rgba()` for subtle backgrounds, focus rings and `.text-bg-*`; setting only the hex * leaves those derivatives on Bootstrap's default blue, which produces components that - * are half-teal and half-blue — worse than not bridging at all. + * are half-teal and half-blue — worse than not bridging at all. The triplets come from + * the `-rgb` companion tokens in _tokens.css — never literals here — so an integrator who + * re-themes a colour re-themes every rgba() derivative by overriding the companion pair + * in one place. (The layered `!important` on Bootstrap's `.bg-*` utilities beats any + * unlayered override rule, so the variable is the only reliable seam.) */ :root { /* ------------------------------------------------------------ Theme colours */ --bs-primary: var(--aro-color-primary); - --bs-primary-rgb: 6, 186, 180; + --bs-primary-rgb: var(--aro-color-primary-rgb); --bs-secondary: var(--aro-color-secondary); - --bs-secondary-rgb: 46, 79, 94; + --bs-secondary-rgb: var(--aro-color-secondary-rgb); --bs-success: var(--aro-color-success); - --bs-success-rgb: 99, 206, 179; + --bs-success-rgb: var(--aro-color-success-rgb); --bs-danger: var(--aro-color-danger); - --bs-danger-rgb: 229, 35, 33; + --bs-danger-rgb: var(--aro-color-danger-rgb); --bs-warning: var(--aro-color-warning); - --bs-warning-rgb: 242, 92, 5; + --bs-warning-rgb: var(--aro-color-warning-rgb); --bs-info: var(--aro-color-info); - --bs-info-rgb: 227, 155, 2; + --bs-info-rgb: var(--aro-color-info-rgb); /* ------------------------------------------------------------------- Body */ --bs-body-bg: var(--aro-color-bg); - --bs-body-bg-rgb: 245, 248, 250; + --bs-body-bg-rgb: var(--aro-color-bg-rgb); --bs-body-color: var(--aro-color-text); - --bs-body-color-rgb: 51, 51, 51; + --bs-body-color-rgb: var(--aro-color-text-rgb); /* -------------------------------------------------------------- Emphasis */ @@ -466,9 +485,9 @@ /* ------------------------------------------------------------------ Links */ --bs-link-color: var(--aro-color-secondary); - --bs-link-color-rgb: 46, 79, 94; + --bs-link-color-rgb: var(--aro-color-secondary-rgb); --bs-link-hover-color: var(--aro-color-primary); - --bs-link-hover-color-rgb: 6, 186, 180; + --bs-link-hover-color-rgb: var(--aro-color-primary-rgb); /* --------------------------------------------------------------- Borders */ @@ -482,7 +501,7 @@ /* ---------------------------------------------------------- Focus ring */ - --bs-focus-ring-color: rgba(6, 186, 180, 0.25); + --bs-focus-ring-color: rgba(var(--aro-color-primary-rgb), 0.25); } /** diff --git a/docs/index.html b/docs/index.html index ed3f9bea..0fc69aa7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -157,12 +157,21 @@ /* ------------------------------------------------------------------ Brand */ --aro-color-primary: #06BAB4; + /* + * `-rgb` companions. Bootstrap's legacy `rgba(var(--bs-*-rgb), a)` consumers need a + * bare comma triplet, and CSS cannot decompose a hex token into one (relative colour + * syntax cannot produce a naked triplet). So each colour the bridge translates carries + * its triplet as a token: override a colour, override its companion — the bridge maps + * both and hardcodes nothing. + */ + --aro-color-primary-rgb: 6, 186, 180; --aro-color-primary-hover: #05A29D; --aro-color-primary-contrast: #FFFFFF; /* Translucent teal tint — soft primary surface (e.g. the navbar user-icon circle). */ --aro-color-primary-subtle: rgba(122, 220, 218, 0.71); --aro-color-secondary: #2E4F5E; + --aro-color-secondary-rgb: 46, 79, 94; --aro-color-secondary-hover: #223C48; --aro-color-secondary-contrast: #FFFFFF; @@ -178,13 +187,18 @@ /* -------------------------------------------------------------- Semantic */ --aro-color-success: #63CEB3; + --aro-color-success-rgb: 99, 206, 179; --aro-color-danger: #E52321; + --aro-color-danger-rgb: 229, 35, 33; --aro-color-warning: #F25C05; + --aro-color-warning-rgb: 242, 92, 5; --aro-color-info: #E39B02; + --aro-color-info-rgb: 227, 155, 2; /* -------------------------------------------------------------- Surfaces */ --aro-color-bg: #F5F8FA; + --aro-color-bg-rgb: 245, 248, 250; --aro-color-surface: #FFFFFF; --aro-color-surface-muted: #EBEBEB; /* Select2 multi-select chip fill (lot B). */ /* Chrome that must read as inert beside the content it captions — the editor toolbar. @@ -195,6 +209,7 @@ /* ------------------------------------------------------------------ Text */ --aro-color-text: #333333; + --aro-color-text-rgb: 51, 51, 51; --aro-color-text-strong: #181C32; --aro-color-text-label: #3F4254; --aro-color-text-muted: #99A1B7; @@ -426,37 +441,41 @@ * `--bs-*-rgb` must be set alongside every colour. Bootstrap uses the RGB triplets inside * `rgba()` for subtle backgrounds, focus rings and `.text-bg-*`; setting only the hex * leaves those derivatives on Bootstrap's default blue, which produces components that - * are half-teal and half-blue — worse than not bridging at all. + * are half-teal and half-blue — worse than not bridging at all. The triplets come from + * the `-rgb` companion tokens in _tokens.css — never literals here — so an integrator who + * re-themes a colour re-themes every rgba() derivative by overriding the companion pair + * in one place. (The layered `!important` on Bootstrap's `.bg-*` utilities beats any + * unlayered override rule, so the variable is the only reliable seam.) */ :root { /* ------------------------------------------------------------ Theme colours */ --bs-primary: var(--aro-color-primary); - --bs-primary-rgb: 6, 186, 180; + --bs-primary-rgb: var(--aro-color-primary-rgb); --bs-secondary: var(--aro-color-secondary); - --bs-secondary-rgb: 46, 79, 94; + --bs-secondary-rgb: var(--aro-color-secondary-rgb); --bs-success: var(--aro-color-success); - --bs-success-rgb: 99, 206, 179; + --bs-success-rgb: var(--aro-color-success-rgb); --bs-danger: var(--aro-color-danger); - --bs-danger-rgb: 229, 35, 33; + --bs-danger-rgb: var(--aro-color-danger-rgb); --bs-warning: var(--aro-color-warning); - --bs-warning-rgb: 242, 92, 5; + --bs-warning-rgb: var(--aro-color-warning-rgb); --bs-info: var(--aro-color-info); - --bs-info-rgb: 227, 155, 2; + --bs-info-rgb: var(--aro-color-info-rgb); /* ------------------------------------------------------------------- Body */ --bs-body-bg: var(--aro-color-bg); - --bs-body-bg-rgb: 245, 248, 250; + --bs-body-bg-rgb: var(--aro-color-bg-rgb); --bs-body-color: var(--aro-color-text); - --bs-body-color-rgb: 51, 51, 51; + --bs-body-color-rgb: var(--aro-color-text-rgb); /* -------------------------------------------------------------- Emphasis */ @@ -466,9 +485,9 @@ /* ------------------------------------------------------------------ Links */ --bs-link-color: var(--aro-color-secondary); - --bs-link-color-rgb: 46, 79, 94; + --bs-link-color-rgb: var(--aro-color-secondary-rgb); --bs-link-hover-color: var(--aro-color-primary); - --bs-link-hover-color-rgb: 6, 186, 180; + --bs-link-hover-color-rgb: var(--aro-color-primary-rgb); /* --------------------------------------------------------------- Borders */ @@ -482,7 +501,7 @@ /* ---------------------------------------------------------- Focus ring */ - --bs-focus-ring-color: rgba(6, 186, 180, 0.25); + --bs-focus-ring-color: rgba(var(--aro-color-primary-rgb), 0.25); } /** diff --git a/src/Resources/public/css/foundations/_bootstrap-bridge.css b/src/Resources/public/css/foundations/_bootstrap-bridge.css index d9e17c5d..3dc71761 100644 --- a/src/Resources/public/css/foundations/_bootstrap-bridge.css +++ b/src/Resources/public/css/foundations/_bootstrap-bridge.css @@ -14,37 +14,41 @@ * `--bs-*-rgb` must be set alongside every colour. Bootstrap uses the RGB triplets inside * `rgba()` for subtle backgrounds, focus rings and `.text-bg-*`; setting only the hex * leaves those derivatives on Bootstrap's default blue, which produces components that - * are half-teal and half-blue — worse than not bridging at all. + * are half-teal and half-blue — worse than not bridging at all. The triplets come from + * the `-rgb` companion tokens in _tokens.css — never literals here — so an integrator who + * re-themes a colour re-themes every rgba() derivative by overriding the companion pair + * in one place. (The layered `!important` on Bootstrap's `.bg-*` utilities beats any + * unlayered override rule, so the variable is the only reliable seam.) */ :root { /* ------------------------------------------------------------ Theme colours */ --bs-primary: var(--aro-color-primary); - --bs-primary-rgb: 6, 186, 180; + --bs-primary-rgb: var(--aro-color-primary-rgb); --bs-secondary: var(--aro-color-secondary); - --bs-secondary-rgb: 46, 79, 94; + --bs-secondary-rgb: var(--aro-color-secondary-rgb); --bs-success: var(--aro-color-success); - --bs-success-rgb: 99, 206, 179; + --bs-success-rgb: var(--aro-color-success-rgb); --bs-danger: var(--aro-color-danger); - --bs-danger-rgb: 229, 35, 33; + --bs-danger-rgb: var(--aro-color-danger-rgb); --bs-warning: var(--aro-color-warning); - --bs-warning-rgb: 242, 92, 5; + --bs-warning-rgb: var(--aro-color-warning-rgb); --bs-info: var(--aro-color-info); - --bs-info-rgb: 227, 155, 2; + --bs-info-rgb: var(--aro-color-info-rgb); /* ------------------------------------------------------------------- Body */ --bs-body-bg: var(--aro-color-bg); - --bs-body-bg-rgb: 245, 248, 250; + --bs-body-bg-rgb: var(--aro-color-bg-rgb); --bs-body-color: var(--aro-color-text); - --bs-body-color-rgb: 51, 51, 51; + --bs-body-color-rgb: var(--aro-color-text-rgb); /* -------------------------------------------------------------- Emphasis */ @@ -54,9 +58,9 @@ /* ------------------------------------------------------------------ Links */ --bs-link-color: var(--aro-color-secondary); - --bs-link-color-rgb: 46, 79, 94; + --bs-link-color-rgb: var(--aro-color-secondary-rgb); --bs-link-hover-color: var(--aro-color-primary); - --bs-link-hover-color-rgb: 6, 186, 180; + --bs-link-hover-color-rgb: var(--aro-color-primary-rgb); /* --------------------------------------------------------------- Borders */ @@ -70,7 +74,7 @@ /* ---------------------------------------------------------- Focus ring */ - --bs-focus-ring-color: rgba(6, 186, 180, 0.25); + --bs-focus-ring-color: rgba(var(--aro-color-primary-rgb), 0.25); } /** diff --git a/src/Resources/public/css/foundations/_tokens.css b/src/Resources/public/css/foundations/_tokens.css index 340b6a29..3d2c7c76 100644 --- a/src/Resources/public/css/foundations/_tokens.css +++ b/src/Resources/public/css/foundations/_tokens.css @@ -21,12 +21,21 @@ /* ------------------------------------------------------------------ Brand */ --aro-color-primary: #06BAB4; + /* + * `-rgb` companions. Bootstrap's legacy `rgba(var(--bs-*-rgb), a)` consumers need a + * bare comma triplet, and CSS cannot decompose a hex token into one (relative colour + * syntax cannot produce a naked triplet). So each colour the bridge translates carries + * its triplet as a token: override a colour, override its companion — the bridge maps + * both and hardcodes nothing. + */ + --aro-color-primary-rgb: 6, 186, 180; --aro-color-primary-hover: #05A29D; --aro-color-primary-contrast: #FFFFFF; /* Translucent teal tint — soft primary surface (e.g. the navbar user-icon circle). */ --aro-color-primary-subtle: rgba(122, 220, 218, 0.71); --aro-color-secondary: #2E4F5E; + --aro-color-secondary-rgb: 46, 79, 94; --aro-color-secondary-hover: #223C48; --aro-color-secondary-contrast: #FFFFFF; @@ -42,13 +51,18 @@ /* -------------------------------------------------------------- Semantic */ --aro-color-success: #63CEB3; + --aro-color-success-rgb: 99, 206, 179; --aro-color-danger: #E52321; + --aro-color-danger-rgb: 229, 35, 33; --aro-color-warning: #F25C05; + --aro-color-warning-rgb: 242, 92, 5; --aro-color-info: #E39B02; + --aro-color-info-rgb: 227, 155, 2; /* -------------------------------------------------------------- Surfaces */ --aro-color-bg: #F5F8FA; + --aro-color-bg-rgb: 245, 248, 250; --aro-color-surface: #FFFFFF; --aro-color-surface-muted: #EBEBEB; /* Select2 multi-select chip fill (lot B). */ /* Chrome that must read as inert beside the content it captions — the editor toolbar. @@ -59,6 +73,7 @@ /* ------------------------------------------------------------------ Text */ --aro-color-text: #333333; + --aro-color-text-rgb: 51, 51, 51; --aro-color-text-strong: #181C32; --aro-color-text-label: #3F4254; --aro-color-text-muted: #99A1B7; From 7840b16763c8a7c5313c4761bcb0759424b3a39c Mon Sep 17 00:00:00 2001 From: Joel Gomez Caballe Date: Mon, 31 Aug 2026 17:12:20 +0200 Subject: [PATCH 05/12] Restore the tab pane's airy line-height MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stisla gave .tab-content > .tab-pane its own leading (24px, plus a 28px prose rule); the ablation dropped both and panes fell back to the 18px body leading, which cramps prose framed by chrome on every side. 28px is the arbitrated value, kept as a literal like the body leading in _typography.css, pending the §4 density pass. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G9HvRYqT42n7yvuCwqHGSh --- docs/catalog.html | 11 +++++++++++ docs/index.html | 11 +++++++++++ src/Resources/public/css/components/_tabs.css | 11 +++++++++++ 3 files changed, 33 insertions(+) diff --git a/docs/catalog.html b/docs/catalog.html index 0fc69aa7..319fe3a9 100644 --- a/docs/catalog.html +++ b/docs/catalog.html @@ -5626,6 +5626,17 @@ border-bottom: 3px solid var(--aro-color-primary); } +/* + * Pane content breathes: prose inside a tab reads against chrome on every side, and the + * 18px body leading that suits table cells cramps it here. Stisla shipped this pane rule + * at 24px with its own padding; 28px is the arbitrated value — the airier rhythm the + * admin actually wore (v2's prose rule) — kept as a literal like the body-leading one + * in _typography.css, pending the §4 density pass. + */ +.tab-content > .tab-pane { + line-height: 28px; +} + /* ./components/_bg-utilities.css */ /** * Background-colour utilities — cross-suite. diff --git a/docs/index.html b/docs/index.html index 0fc69aa7..319fe3a9 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5626,6 +5626,17 @@ border-bottom: 3px solid var(--aro-color-primary); } +/* + * Pane content breathes: prose inside a tab reads against chrome on every side, and the + * 18px body leading that suits table cells cramps it here. Stisla shipped this pane rule + * at 24px with its own padding; 28px is the arbitrated value — the airier rhythm the + * admin actually wore (v2's prose rule) — kept as a literal like the body-leading one + * in _typography.css, pending the §4 density pass. + */ +.tab-content > .tab-pane { + line-height: 28px; +} + /* ./components/_bg-utilities.css */ /** * Background-colour utilities — cross-suite. diff --git a/src/Resources/public/css/components/_tabs.css b/src/Resources/public/css/components/_tabs.css index 22c66fa2..bf9c653d 100644 --- a/src/Resources/public/css/components/_tabs.css +++ b/src/Resources/public/css/components/_tabs.css @@ -13,3 +13,14 @@ .nav-tabs.tab-underlined .nav-link.active { border-bottom: 3px solid var(--aro-color-primary); } + +/* + * Pane content breathes: prose inside a tab reads against chrome on every side, and the + * 18px body leading that suits table cells cramps it here. Stisla shipped this pane rule + * at 24px with its own padding; 28px is the arbitrated value — the airier rhythm the + * admin actually wore (v2's prose rule) — kept as a literal like the body-leading one + * in _typography.css, pending the §4 density pass. + */ +.tab-content > .tab-pane { + line-height: 28px; +} From 197c4da743002f12d48c8686f348e4bc47c3c739 Mon Sep 17 00:00:00 2001 From: Joel Gomez Caballe Date: Mon, 31 Aug 2026 17:13:58 +0200 Subject: [PATCH 06/12] h2 goes semibold (600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same reasoning as h1's bold: Bootstrap's reboot leaves headings at 500. h1 bold, h2 semibold — a step apart so the hierarchy reads. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G9HvRYqT42n7yvuCwqHGSh --- docs/catalog.html | 4 ++++ docs/index.html | 4 ++++ src/Resources/public/css/foundations/_typography.css | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/docs/catalog.html b/docs/catalog.html index 319fe3a9..007a245a 100644 --- a/docs/catalog.html +++ b/docs/catalog.html @@ -601,6 +601,10 @@ font-weight: var(--aro-weight-bold); } +h2 { + font-weight: var(--aro-weight-semibold); +} + /* The margin is load-bearing rhythm: !important so a scoped tweak cannot silently compress the separator back to Bootstrap's default. */ hr { diff --git a/docs/index.html b/docs/index.html index 319fe3a9..007a245a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -601,6 +601,10 @@ font-weight: var(--aro-weight-bold); } +h2 { + font-weight: var(--aro-weight-semibold); +} + /* The margin is load-bearing rhythm: !important so a scoped tweak cannot silently compress the separator back to Bootstrap's default. */ hr { diff --git a/src/Resources/public/css/foundations/_typography.css b/src/Resources/public/css/foundations/_typography.css index 250c44b8..05c062c6 100644 --- a/src/Resources/public/css/foundations/_typography.css +++ b/src/Resources/public/css/foundations/_typography.css @@ -48,6 +48,10 @@ h1 { font-weight: var(--aro-weight-bold); } +h2 { + font-weight: var(--aro-weight-semibold); +} + /* The margin is load-bearing rhythm: !important so a scoped tweak cannot silently compress the separator back to Bootstrap's default. */ hr { From 53539b6b39def2396314a64e85a9286d3b0d2b96 Mon Sep 17 00:00:00 2001 From: Joel Gomez Caballe Date: Mon, 31 Aug 2026 17:37:03 +0200 Subject: [PATCH 07/12] Tolerate data objects without attributes/crops in InstanceToData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v3 ImageMapper calls getAttributes() for every ImageType, where v2 only read attributes in the gallery (whose items always carry one). Any integration passing a domain object without an 'attributes' property — legitimate, since only filename_value is configurable — now threw NoSuchPropertyException. Attributes and crops are optional and guarded by the mapper: read them through isReadable() and return null when absent. getFileName() stays strict, an unreadable filename is a real misconfiguration. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G9HvRYqT42n7yvuCwqHGSh --- src/Form/Type/Image/InstanceToData.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Form/Type/Image/InstanceToData.php b/src/Form/Type/Image/InstanceToData.php index f37d14b2..af7b59d5 100644 --- a/src/Form/Type/Image/InstanceToData.php +++ b/src/Form/Type/Image/InstanceToData.php @@ -39,6 +39,13 @@ public function getFileName(mixed $data): mixed return $value; } + /* + * Attributes and crops are optional: ImageMapper guards their use, and ImageType + * accepts arbitrary data objects (only `filename_value` is configurable). Reading + * them strictly broke every integration passing a domain object without an + * `attributes` property — v2 only read attributes in the gallery, whose items + * always have one. Tolerant read: null when the path is not readable. + */ public function getAttributes(mixed $data): mixed { $value = $data; @@ -46,7 +53,9 @@ public function getAttributes(mixed $data): mixed // invalid data type if ($data && !\is_string($data)) { $propertyAccessor = PropertyAccess::createPropertyAccessor(); - $value = $propertyAccessor->getValue($data, $this->attributesValue); + $value = $propertyAccessor->isReadable($data, $this->attributesValue) + ? $propertyAccessor->getValue($data, $this->attributesValue) + : null; } return $value; @@ -59,7 +68,9 @@ public function getCrops(mixed $data): mixed // invalid data type if (!\is_string($data)) { $propertyAccessor = PropertyAccess::createPropertyAccessor(); - $value = $propertyAccessor->getValue($data, $this->cropsValue); + $value = $propertyAccessor->isReadable($data, $this->cropsValue) + ? $propertyAccessor->getValue($data, $this->cropsValue) + : null; } return $value; From 5bc9b490f0693948eb5d75a58507795879ff736b Mon Sep 17 00:00:00 2001 From: Joel Gomez Caballe Date: Tue, 1 Sep 2026 09:27:05 +0200 Subject: [PATCH 08/12] Keep ConfirmDialog's translated labels when the caller passes none MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app.js reads its button labels from data-confirm-label / data-cancel-label, which yield undefined when the attributes are absent. Object.assign copied that undefined over the translated defaults, and textContent = undefined empties the node — so every caller not using the actions macro rendered two blank buttons. Drop the undefined entries before merging. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01G9HvRYqT42n7yvuCwqHGSh --- .../public/js/module/dialog/confirm-dialog.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Resources/public/js/module/dialog/confirm-dialog.js b/src/Resources/public/js/module/dialog/confirm-dialog.js index 58d10d8b..eb53dfbd 100644 --- a/src/Resources/public/js/module/dialog/confirm-dialog.js +++ b/src/Resources/public/js/module/dialog/confirm-dialog.js @@ -39,6 +39,17 @@ export class ConfirmDialog { constructor(options = {}) { // Translated fallbacks rendered by base.html.twig; safe if absent. const i18n = (typeof window !== 'undefined' && window.aroDialogI18n) || {}; + + // Callers read their labels from data-attributes, which yield `undefined` when the + // attribute is absent. Object.assign copies that `undefined` over the default, and + // `textContent = undefined` empties the node — blank buttons. Drop the holes first. + const given = {}; + for (const key of Object.keys(options)) { + if (options[key] !== undefined) { + given[key] = options[key]; + } + } + const o = Object.assign({ intent: 'danger', size: 'md', @@ -47,7 +58,7 @@ export class ConfirmDialog { confirmLabel: i18n.confirm || 'Confirm', cancelLabel: i18n.cancel || 'Cancel', onConfirm: () => {}, - }, options); + }, given); // Size scale shared with modals: sm 400 · md 520 (default) · lg 720 · xl 960. const sizeClass = { sm: 'modal-sm', lg: 'modal-lg', xl: 'modal-xl' }[o.size] || ''; From be52a838ff4c65dc2ac495058ee50eb5e8145786 Mon Sep 17 00:00:00 2001 From: Joel Gomez Caballe Date: Tue, 1 Sep 2026 09:27:05 +0200 Subject: [PATCH 09/12] Give the actions macro an extra slot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macro covers edit / status / delete and nothing else, so a row needing one more item — a preview link, a modal trigger — had to fall back to hand-written markup, and with it to the blank-button bug. Add an optional 'extra' parameter, rendered between edit and status and counted by the divider logic. Capture it with {% set %} so it stays Markup. The parameter table in doc/macros.md was also missing 'status' and 'delete_token'. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01G9HvRYqT42n7yvuCwqHGSh --- doc/macros.md | 29 ++++++++++++++++++++- src/Resources/views/Macro/actions.html.twig | 12 +++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/doc/macros.md b/doc/macros.md index 50b67c6c..71f1a2f1 100644 --- a/doc/macros.md +++ b/doc/macros.md @@ -26,10 +26,13 @@ To use this macro, you first need to import it in your Twig template: | Parameter | Type | Description | | :--- | :--- | :--- | -| `item` | `object` | The entity instance (used to generate the CSRF token for deletion). | +| `item` | `object` | The entity instance. Only used as the fallback CSRF token id (`delete`), which matches no shipped controller — pass `delete_token` instead. | | `edit_path` | `string` | (Optional) The URL for the "Edit" action. If omitted, the edit link will not be displayed. | | `delete_path` | `string` | (Optional) The URL for the "Delete" action. If omitted, the delete link will not be displayed. | | `delete_confirm_msg` | `string` | (Optional) A custom confirmation message for the deletion. Defaults to the translation of `text.confirm_delete`. | +| `status` | `hash` | (Optional) The online/offline toggle: `{ path, confirm, label }`. `confirm` may contain `%s`, replaced at runtime by the target state. | +| `delete_token` | `string` | (Optional) The CSRF token **value** for the delete form, e.g. `csrf_token('delete__post' ~ post.id)`. Each controller validates its own id, so the caller must compute it. | +| `extra` | `Markup` | (Optional) Caller-supplied markup inserted between the edit link and the status toggle — a preview link, a modal trigger, anything the fixed slots cannot express. Capture it with `{% set %}` so it is not escaped. | ### Example @@ -49,6 +52,30 @@ In a DataTable row: {% endblock %} ``` +### Adding an item the macro does not cover + +Capture the markup, then pass it by name — `extra` sits after `delete_token` in the +signature, so a named argument keeps the call readable: + +```twig +{% set extra %} + + {{ ux_icon('lucide:link') }} {{ 'text.preview'|trans }} + +{% endset %} + +{{ list.actions( + item, + path('admin_event_edit', {id: item.id}), + path('admin_event_delete', {id: item.id}), + extra: extra, + delete_token: csrf_token('delete__event' ~ item.id), +) }} +``` + +`{% set %}` yields a `Markup` object, which Twig prints as-is. A plain string passed here +would be escaped and show up as literal HTML. + ## Breadcrumb Macro The `breadcrumb` macro generates a navigation breadcrumb list from an array of items. diff --git a/src/Resources/views/Macro/actions.html.twig b/src/Resources/views/Macro/actions.html.twig index d911b449..40f42fef 100644 --- a/src/Resources/views/Macro/actions.html.twig +++ b/src/Resources/views/Macro/actions.html.twig @@ -10,12 +10,18 @@ # compute it — e.g. `csrf_token('delete__post' ~ post.id)`. Without it the macro falls back # to `delete`, which matches no shipped controller and is only a placeholder. # + # `extra` (optional) is caller-supplied markup inserted between edit and status — the place + # for a preview link, a "see more" modal trigger, anything the fixed slots cannot express. + # Capture it so it stays Markup and is not escaped, then pass it by name: + # {% set extra %}{% endset %} + # {{ list.actions(item, edit, del, msg, extra: extra) }} + # # Delete confirmation is the Dialog component: `a.delete[data-confirm]` is caught in app.js, # which opens a danger `ConfirmDialog` (css/components/_dialog.css) and, on confirm, submits # the sibling `

`. `data-confirm-title` is the bold question, `data-confirm` the detail # line under it; button labels come translated via `data-confirm-label` / `data-cancel-label`. #} -{% macro actions(item, edit_path, delete_path, delete_confirm_msg = null, status = null, delete_token = null) %} +{% macro actions(item, edit_path, delete_path, delete_confirm_msg = null, status = null, delete_token = null, extra = null) %}