From f90d537d6ac20240d9a1f43f67dbeea6bf90752a Mon Sep 17 00:00:00 2001 From: freddymeta <170298698+freddymeta@users.noreply.github.com> Date: Tue, 25 Aug 2026 02:45:36 -0700 Subject: [PATCH 1/5] feat(Banner): theme targets for the description and actions row The header's supporting line and its end-aligned actions row render with only StyleX atomics, so a theme cannot reach them: `astryx-banner` stops at the tinted header and the rest of the header is selectable only by child order. Add `astryx-banner-description` and `astryx-banner-actions` via `mergeProps(themeProps(...), stylex.props(...))`, documented in `docs` and `docsZh`. Purely additive. --- .changeset/banner-theme-targets.md | 9 +++++++++ packages/core/src/Banner/Banner.doc.mjs | 8 ++++++++ packages/core/src/Banner/Banner.tsx | 22 ++++++++++++++++++---- 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 .changeset/banner-theme-targets.md diff --git a/.changeset/banner-theme-targets.md b/.changeset/banner-theme-targets.md new file mode 100644 index 0000000000000..8dc6000b76018 --- /dev/null +++ b/.changeset/banner-theme-targets.md @@ -0,0 +1,9 @@ +--- +'@astryxdesign/core': patch +--- + +[feat] Banner: the two painted elements inside the header that a theme cannot reach now carry stable theme targets — `astryx-banner-description` (the supporting line) and `astryx-banner-actions` (the end-aligned actions row). Only the header, the status icon and the content panel were themeable before, so a theme restyling the description, the space between it and the title, or the actions row's edge compensation had to reach in with structural selectors like `.astryx-banner > div:nth-child(2) > div:nth-child(2)`. Purely additive: no existing class, data attribute, or style changes. + +The title, the two controls and the header's text column are deliberately not targets. The column paints nothing (`display: flex; flex-direction: column; gap: 0`) and the space it owns is expressible on `banner-description`; the title and the controls already render the way the consuming theme wants them. + +@freddymeta diff --git a/packages/core/src/Banner/Banner.doc.mjs b/packages/core/src/Banner/Banner.doc.mjs index bddd09d16f9bb..d4c7d0f5cfb9d 100644 --- a/packages/core/src/Banner/Banner.doc.mjs +++ b/packages/core/src/Banner/Banner.doc.mjs @@ -123,6 +123,8 @@ export const docs = { targets: [ {className: 'astryx-banner', visualProps: ['container', 'status']}, {className: 'astryx-banner-icon', visualProps: ['status']}, + {className: 'astryx-banner-description'}, + {className: 'astryx-banner-actions'}, {className: 'astryx-banner-content', visualProps: ['container', 'status']}, ], vars: [ @@ -192,6 +194,12 @@ export const docsZh = { 'status', ], }, + { + className: 'astryx-banner-description', + }, + { + className: 'astryx-banner-actions', + }, { className: 'astryx-banner-content', visualProps: [ diff --git a/packages/core/src/Banner/Banner.tsx b/packages/core/src/Banner/Banner.tsx index 95a6ea5ca8fd8..f3bf8691fd33a 100644 --- a/packages/core/src/Banner/Banner.tsx +++ b/packages/core/src/Banner/Banner.tsx @@ -15,6 +15,11 @@ * - Status icon (themeProps 'banner-icon'): the target rides on the default * itself — the element that paints — so 'status:X' overrides reach * the glyph (#4166); for a custom `icon` node it stays on the layout wrapper + * - Description (themeProps 'banner-description'): the supporting line owns its + * own colour and type, and the space between it and the title + * - End area (themeProps 'banner-actions'): the actions row, including the edge + * compensation — a theme whose banner grows to its buttons rather than + * letting them overhang the header padding overrides it here * - No left border accent — color is expressed through the full header background * - Each visual area owns its own border-radius (no overflow:clip on the container) * - Children are collapsible by default: a toggle appears in the header end @@ -612,14 +617,23 @@ export function Banner({ )}>
{title}
{isRenderable(description) && ( -
{description}
+
+ {description} +
)} {showEndArea && (
{endContent} {hasToggle && ( From 29eabdb70474c8db367117a9a28fdb0a293924be Mon Sep 17 00:00:00 2001 From: freddymeta <170298698+freddymeta@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:30:26 -0700 Subject: [PATCH 2/5] fix(Banner): center a title-only banner whose only control is the collapse toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `isSingleLine` drives the header's vertical centering and counted `endContent` and the dismiss button, but not the collapse toggle. A collapsible title-only banner therefore kept `align-items: flex-start` while the same banner with a dismiss button centered — its icon and title sat 4px above the 28px toggle beside them. The toggle is the same control in the same row, so it counts. --- .changeset/banner-single-line-toggle.md | 7 +++++ packages/core/src/Banner/Banner.test.tsx | 40 ++++++++++++++++++++++++ packages/core/src/Banner/Banner.tsx | 7 +++-- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 .changeset/banner-single-line-toggle.md diff --git a/.changeset/banner-single-line-toggle.md b/.changeset/banner-single-line-toggle.md new file mode 100644 index 0000000000000..e7783b6b1d8e2 --- /dev/null +++ b/.changeset/banner-single-line-toggle.md @@ -0,0 +1,7 @@ +--- +'@astryxdesign/core': patch +--- + +[fix] Banner: a title-only collapsible banner now centers its header, as a title-only banner with any other control already does. `isSingleLine` counted `endContent` and the dismiss button but not the collapse toggle, so a banner whose only control was the toggle kept `align-items: flex-start` — leaving its icon and title 4px above the 28px toggle they sit beside, while the same banner with a dismiss button centered correctly. + +@freddymeta diff --git a/packages/core/src/Banner/Banner.test.tsx b/packages/core/src/Banner/Banner.test.tsx index c092c3e5b2ecd..501413bd77192 100644 --- a/packages/core/src/Banner/Banner.test.tsx +++ b/packages/core/src/Banner/Banner.test.tsx @@ -650,4 +650,44 @@ describe('Banner', () => { expect(getComputedStyle(textColumn).flexBasis).not.toBe('8rem'); }); }); + describe('single-line centering', () => { + const headerOf = (ui: React.ReactElement) => { + const {container} = render(ui); + return container.firstElementChild!.firstElementChild!; + }; + + it('centers a title-only banner that has a dismiss button', () => { + const header = headerOf( + , + ); + expect(getComputedStyle(header).alignItems).toBe('center'); + }); + + it('centers a title-only banner whose only control is the collapse toggle', () => { + const header = headerOf( + +

Details

+
, + ); + expect(getComputedStyle(header).alignItems).toBe('center'); + }); + + it('keeps a described banner top-aligned, toggle or not', () => { + const header = headerOf( + +

Details

+
, + ); + expect(getComputedStyle(header).alignItems).toBe('flex-start'); + }); + + it('keeps a banner with no controls at all top-aligned', () => { + const header = headerOf( + +

Details

+
, + ); + expect(getComputedStyle(header).alignItems).toBe('flex-start'); + }); + }); }); diff --git a/packages/core/src/Banner/Banner.tsx b/packages/core/src/Banner/Banner.tsx index f3bf8691fd33a..081ea6003c29c 100644 --- a/packages/core/src/Banner/Banner.tsx +++ b/packages/core/src/Banner/Banner.tsx @@ -539,8 +539,11 @@ export function Banner({ // Show the end area if there are actions, dismiss, or a collapsible toggle const showEndArea = isRenderable(endContent) || isDismissable || hasToggle; // Center items vertically when there's only a title (no description) - // and the banner has action buttons - const hasActions = isRenderable(endContent) || isDismissable; + // and the banner has action buttons. The collapse toggle counts: it is the + // same 28px control as a dismiss button and shares the row with it, so a + // collapsible title-only banner is exactly the case this centers — leaving + // it out left the icon and title hanging above the toggle they sit beside. + const hasActions = isRenderable(endContent) || isDismissable || hasToggle; const isSingleLine = !isRenderable(description) && hasActions; // Non-collapsible children are always shown; collapsible ones follow the From 7d83f083c7c12fba624c1afc7276ace22af3972f Mon Sep 17 00:00:00 2001 From: freddymeta <170298698+freddymeta@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:29:30 -0700 Subject: [PATCH 3/5] feat(Banner): drop the banner-actions target, keep banner-description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `banner-actions` named the end area, which is a layout row — flex, wrap, `margin-inline-start: auto`, and the -4px block margin that lets its buttons overhang the header padding. Not a painted surface, so it fails the theme-target rule the same way a `display: contents` wrapper does. The use it was meant to serve — a header that grows around its buttons instead of letting them overhang — is `padding-block` on the existing `banner` target, which reaches the same 44→52px without exposing a private margin as contract. `banner-description` stays: it paints (colour, type, and the space between it and the title) and the structural selector it replaces is real. --- .changeset/banner-theme-targets.md | 4 ++-- packages/core/src/Banner/Banner.doc.mjs | 4 ---- packages/core/src/Banner/Banner.tsx | 15 ++++++--------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/.changeset/banner-theme-targets.md b/.changeset/banner-theme-targets.md index 8dc6000b76018..924e9c2df2293 100644 --- a/.changeset/banner-theme-targets.md +++ b/.changeset/banner-theme-targets.md @@ -2,8 +2,8 @@ '@astryxdesign/core': patch --- -[feat] Banner: the two painted elements inside the header that a theme cannot reach now carry stable theme targets — `astryx-banner-description` (the supporting line) and `astryx-banner-actions` (the end-aligned actions row). Only the header, the status icon and the content panel were themeable before, so a theme restyling the description, the space between it and the title, or the actions row's edge compensation had to reach in with structural selectors like `.astryx-banner > div:nth-child(2) > div:nth-child(2)`. Purely additive: no existing class, data attribute, or style changes. +[feat] Banner: the header's supporting line now carries a stable theme target, `astryx-banner-description`. Only the header, the status icon and the content panel were themeable before, so a theme restyling the description — its colour, its type, or the space between it and the title — had to reach in with a structural selector like `.astryx-banner > div:nth-child(2) > div:nth-child(2)`. Purely additive: no existing class, data attribute, or style changes. -The title, the two controls and the header's text column are deliberately not targets. The column paints nothing (`display: flex; flex-direction: column; gap: 0`) and the space it owns is expressible on `banner-description`; the title and the controls already render the way the consuming theme wants them. +Nothing else in the header becomes a target. The end area is a layout row — flex, wrap, and the edge compensation that lets its buttons overhang the header padding — not a painted surface, and a theme that wants the header to grow around its buttons instead of letting them overhang sets `padding-block` on the existing `banner` target, which reaches the same height without exposing a private margin. The title, the two controls and the text column are likewise left alone: the column paints nothing (`display: flex; flex-direction: column; gap: 0`) and the space it owns is expressible on `banner-description`, while the title and the controls already render the way the consuming theme wants them. @freddymeta diff --git a/packages/core/src/Banner/Banner.doc.mjs b/packages/core/src/Banner/Banner.doc.mjs index d4c7d0f5cfb9d..65f3af6aebfc6 100644 --- a/packages/core/src/Banner/Banner.doc.mjs +++ b/packages/core/src/Banner/Banner.doc.mjs @@ -124,7 +124,6 @@ export const docs = { {className: 'astryx-banner', visualProps: ['container', 'status']}, {className: 'astryx-banner-icon', visualProps: ['status']}, {className: 'astryx-banner-description'}, - {className: 'astryx-banner-actions'}, {className: 'astryx-banner-content', visualProps: ['container', 'status']}, ], vars: [ @@ -197,9 +196,6 @@ export const docsZh = { { className: 'astryx-banner-description', }, - { - className: 'astryx-banner-actions', - }, { className: 'astryx-banner-content', visualProps: [ diff --git a/packages/core/src/Banner/Banner.tsx b/packages/core/src/Banner/Banner.tsx index 081ea6003c29c..c389032d80a29 100644 --- a/packages/core/src/Banner/Banner.tsx +++ b/packages/core/src/Banner/Banner.tsx @@ -17,9 +17,9 @@ * the glyph (#4166); for a custom `icon` node it stays on the layout wrapper * - Description (themeProps 'banner-description'): the supporting line owns its * own colour and type, and the space between it and the title - * - End area (themeProps 'banner-actions'): the actions row, including the edge - * compensation — a theme whose banner grows to its buttons rather than - * letting them overhang the header padding overrides it here + * - The end area carries no target: it is a layout row (flex, wrap, edge + * compensation) rather than a painted surface, and a theme that wants the + * header to grow around its buttons sets `padding-block` on 'banner' * - No left border accent — color is expressed through the full header background * - Each visual area owns its own border-radius (no overflow:clip on the container) * - Children are collapsible by default: a toggle appears in the header end @@ -631,12 +631,9 @@ export function Banner({
{showEndArea && (
{endContent} {hasToggle && ( From a973d5976ff66376a97e8b6ce45f3742d38ae6d4 Mon Sep 17 00:00:00 2001 From: freddymeta <170298698+freddymeta@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:29:46 -0700 Subject: [PATCH 4/5] Revert "fix(Banner): center a title-only banner whose only control is the collapse toggle" This reverts commit 29eabdb70474c8db367117a9a28fdb0a293924be. --- .changeset/banner-single-line-toggle.md | 7 ----- packages/core/src/Banner/Banner.test.tsx | 40 ------------------------ packages/core/src/Banner/Banner.tsx | 7 ++--- 3 files changed, 2 insertions(+), 52 deletions(-) delete mode 100644 .changeset/banner-single-line-toggle.md diff --git a/.changeset/banner-single-line-toggle.md b/.changeset/banner-single-line-toggle.md deleted file mode 100644 index e7783b6b1d8e2..0000000000000 --- a/.changeset/banner-single-line-toggle.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@astryxdesign/core': patch ---- - -[fix] Banner: a title-only collapsible banner now centers its header, as a title-only banner with any other control already does. `isSingleLine` counted `endContent` and the dismiss button but not the collapse toggle, so a banner whose only control was the toggle kept `align-items: flex-start` — leaving its icon and title 4px above the 28px toggle they sit beside, while the same banner with a dismiss button centered correctly. - -@freddymeta diff --git a/packages/core/src/Banner/Banner.test.tsx b/packages/core/src/Banner/Banner.test.tsx index 501413bd77192..c092c3e5b2ecd 100644 --- a/packages/core/src/Banner/Banner.test.tsx +++ b/packages/core/src/Banner/Banner.test.tsx @@ -650,44 +650,4 @@ describe('Banner', () => { expect(getComputedStyle(textColumn).flexBasis).not.toBe('8rem'); }); }); - describe('single-line centering', () => { - const headerOf = (ui: React.ReactElement) => { - const {container} = render(ui); - return container.firstElementChild!.firstElementChild!; - }; - - it('centers a title-only banner that has a dismiss button', () => { - const header = headerOf( - , - ); - expect(getComputedStyle(header).alignItems).toBe('center'); - }); - - it('centers a title-only banner whose only control is the collapse toggle', () => { - const header = headerOf( - -

Details

-
, - ); - expect(getComputedStyle(header).alignItems).toBe('center'); - }); - - it('keeps a described banner top-aligned, toggle or not', () => { - const header = headerOf( - -

Details

-
, - ); - expect(getComputedStyle(header).alignItems).toBe('flex-start'); - }); - - it('keeps a banner with no controls at all top-aligned', () => { - const header = headerOf( - -

Details

-
, - ); - expect(getComputedStyle(header).alignItems).toBe('flex-start'); - }); - }); }); diff --git a/packages/core/src/Banner/Banner.tsx b/packages/core/src/Banner/Banner.tsx index c389032d80a29..7c4f870f8a823 100644 --- a/packages/core/src/Banner/Banner.tsx +++ b/packages/core/src/Banner/Banner.tsx @@ -539,11 +539,8 @@ export function Banner({ // Show the end area if there are actions, dismiss, or a collapsible toggle const showEndArea = isRenderable(endContent) || isDismissable || hasToggle; // Center items vertically when there's only a title (no description) - // and the banner has action buttons. The collapse toggle counts: it is the - // same 28px control as a dismiss button and shares the row with it, so a - // collapsible title-only banner is exactly the case this centers — leaving - // it out left the icon and title hanging above the toggle they sit beside. - const hasActions = isRenderable(endContent) || isDismissable || hasToggle; + // and the banner has action buttons + const hasActions = isRenderable(endContent) || isDismissable; const isSingleLine = !isRenderable(description) && hasActions; // Non-collapsible children are always shown; collapsible ones follow the From 2777396cb4926a455f89c78b9e11c05bbf2becc9 Mon Sep 17 00:00:00 2001 From: freddymeta <170298698+freddymeta@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:38:43 -0700 Subject: [PATCH 5/5] chore(probe): regenerate the probe theme for banner-description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `visual:probe-theme:check` was failing: the generated probe theme did not cover `astryx-banner-description`, so the visual gate could not exercise the new public target. The generator emits the file wholesale, so this also picks up probe coverage that main is already missing — `carousel-scroller` with its three `presentation:` variants (#5601) and `dropdown-menu-divider`'s `size:xl`. Clean `origin/main` fails the same check today; no workflow runs the guard, so the drift accumulates until a PR that touches a theming target regenerates it. --- packages/themes/probe/src/probeTheme.ts | 42 ++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/themes/probe/src/probeTheme.ts b/packages/themes/probe/src/probeTheme.ts index 7b13ef022d4b1..24e930c3f26dc 100644 --- a/packages/themes/probe/src/probeTheme.ts +++ b/packages/themes/probe/src/probeTheme.ts @@ -6,7 +6,7 @@ // test fixture. Regenerate with: pnpm visual:probe-theme // // defineTheme takes six things and this covers all six: -// components 270 targets, 875 selectors (generated from the docs) +// components 272 targets, 881 selectors (generated from the docs) // tokens custom properties, read back off the themed element // icons every registry entry swapped for a marked glyph // indicators check / radio / checkbox swapped — the swap that reaches furthest @@ -578,6 +578,14 @@ export const probeTheme = defineTheme({ outlineColor: 'hsl(62.1 85% 25%)', }, }, + 'banner-description': { + base: { + backgroundColor: 'hsl(347.7 94% 62%)', + color: 'hsl(118.1 87% 12%)', + borderColor: 'hsl(166.3 80% 25%)', + outlineColor: 'hsl(35.6 93% 25%)', + }, + }, 'banner-icon': { base: { backgroundColor: 'hsl(262.3 84% 51%)', @@ -974,6 +982,14 @@ export const probeTheme = defineTheme({ outlineColor: 'hsl(157.5 84% 25%)', }, }, + 'carousel-scroller': { + base: { + backgroundColor: 'hsl(330.8 89% 63%)', + color: 'hsl(272.4 89% 12%)', + borderColor: 'hsl(38.3 84% 25%)', + outlineColor: 'hsl(243.2 82% 25%)', + }, + }, center: { base: { backgroundColor: 'hsl(344.5 75% 59%)', @@ -2031,6 +2047,24 @@ export const probeTheme = defineTheme({ borderColor: 'hsl(170.7 73% 25%)', outlineColor: 'hsl(74.0 75% 25%)', }, + 'presentation:popover': { + backgroundColor: 'hsl(152.7 88% 51%)', + color: 'hsl(290.9 87% 12%)', + borderColor: 'hsl(252.4 93% 25%)', + outlineColor: 'hsl(80.4 87% 25%)', + }, + 'presentation:bottom-sheet': { + backgroundColor: 'hsl(211.4 72% 62%)', + color: 'hsl(32.5 82% 12%)', + borderColor: 'hsl(244.1 72% 25%)', + outlineColor: 'hsl(196.5 89% 25%)', + }, + 'presentation:adaptive': { + backgroundColor: 'hsl(64.2 82% 63%)', + color: 'hsl(35.5 86% 12%)', + borderColor: 'hsl(42.5 73% 25%)', + outlineColor: 'hsl(88.3 82% 25%)', + }, }, 'dropdown-menu-divider': { base: { @@ -4360,6 +4394,12 @@ export const probeTheme = defineTheme({ borderColor: 'hsl(22.0 83% 25%)', outlineColor: 'hsl(220.9 77% 25%)', }, + 'size:xl': { + backgroundColor: 'hsl(66.5 82% 64%)', + color: 'hsl(228.9 83% 12%)', + borderColor: 'hsl(259.5 74% 25%)', + outlineColor: 'hsl(299.9 71% 25%)', + }, 'shade:default': { backgroundColor: 'hsl(94.7 82% 60%)', color: 'hsl(304.2 86% 12%)',