diff --git a/.sync/PORTING.md b/.sync/PORTING.md index 778c509b..5419865d 100644 --- a/.sync/PORTING.md +++ b/.sync/PORTING.md @@ -493,6 +493,32 @@ material. Reproduce its *intent* in b24ui by editing files under `src/` only. - **a11y** — keep the `axe` test case for the component green. - **Tests** — for every new/changed prop, add a `renderEach` case so a snapshot exercises it; update snapshots with `pnpm run test run -u` when markup changes. +- **A focus outline is coloured by `--ui-color-design-outline-focused-stroke`.** + Upstream colours focus from whatever accent is at hand; this fork has one + token for it. What makes it the right one is not that it is defined per + context — `accent-main-primary` and `accent-soft-element-blue` are too — but + that it *changes kind*: a saturated blue in the light and dark contexts, and + a translucent luminance shift in the edge ones, black at 35% over a light + surface and white at 40% over a dark one. `outline-primary` is the odd one + out for a different reason: it reads `--color-primary` from the Tailwind + theme block, which has no per-context override at all. Reapplying an + upstream hunk that reintroduces `outline-primary`, + `--ui-color-accent-soft-element-blue` or `--ui-color-accent-main-primary` on a + `focus-visible:outline-*` reverts an accessibility fix, not a preference: those + measure 1.99:1 on white and 2.22:1 on the dark background, against the 3:1 WCAG + 2.2 SC 1.4.11 asks (#191). In the **edge** contexts the token is weaker than + what it replaced — it reaches about 2.4:1 there and cannot do better, because + a translucent shift over a backdrop the portal chooses has no guaranteed + ratio. That is a property of the token, tracked in #475, not a reason to paint + focus from an accent again. Guarded by + `test/utils/focus-accent-token.spec.ts`. + + Out of scope, and deliberately: focus that reads `--b24ui-border-color` or + `--b24ui-background`, whether as `ring-` (inputs) or `outline-` + (breadcrumb, checkbox, switch, listbox, input-rating, radio-group). There the + stroke is the control's own border following the component palette, not a + focus accent. + - **Tag width caps are relative to the field, never an absolute length.** Upstream's `tagsItemText` / `itemText` are plain `truncate` with no cap; b24ui caps the **tag** at `max-w-[70%]` with `min-w-0`, marks the delete @@ -791,3 +817,4 @@ forward, since every commit between the two would then never be judged. - 2026-08-23 — closed #98 (PR #468) by adding the §6 rule **carry upstream's breaking marker into our subject**, and the matching §7 reviewer check. The issue's other two actions were already done and are recorded elsewhere: the version arithmetic table in `releasing.md` maps any `BREAKING CHANGE` or `!` to a major, with no v2-line exception, and release automation landed as release-please. What was missing is the one step automation cannot supply. release-please derives the bump from the squashed subject, so the marker is now load-bearing in a way it was not when the CHANGELOG was hand-written and a human read the diff — a dropped `!` used to be a cosmetic slip and is now a semver violation that every gate passes. The rule is stated in both directions on purpose, because a port is not a copy: a §2 divergence can absorb a break that upstream had, and this fork's own prop surface can break where upstream's did not. Last reviewed: 2026-08-23. - 2026-08-23 — fix of #342 (PR #470): added the §2 rule **tag width caps are relative to the field**. `max-w-[180px]` on the tag label was ours — upstream is plain `truncate` — and it ellipsised tags that had room to spare, because a constant knows nothing about the field's width or what shares its row. Settled by measuring in Chromium rather than by reading the spec, which was the only way to tell three plausible readings apart: in a 562px field a tag wanting 590px renders at 199px under the old cap, 412px under `max-w-[70%]` with both tags still on one row, and 562px uncapped with the second tag pushed to the next line. The same measurement caught what review had flagged and reading had not — `input-tags.ts`'s root is `inline-flex` with no width, so the percentage was circular there: the field grew to 590px, overflowing its 562px parent, and clipped the label anyway. `max-w-full` on that root fixes both and is now part of the invariant. The guard went through three drafts, each corrected by mutation rather than by review: it credited one slot with a neighbour's class, then passed on its own comment (which contains the string `min-w-0`), then missed both the six per-size `tagsItem` overrides at deeper indentation and the `(prev) => [...]` slot form — an arrow function's parameter list closes before its body opens, so balancing brackets returned `(prev: string)` and nothing else. It ends at a comma at depth zero, and a self-check asserts the scanner matched something before reporting no offenders. Last reviewed: 2026-08-23. - 2026-08-23 — added the §6 rule **name the upstream commit in the subject** and the §7 reviewer check, on the maintainer's request that ported commits be traceable from the changelog. Only the subject reaches `CHANGELOG.md`, so that is the only place the reference can go. Upstream's own first line was the request as originally put and is deliberately not what shipped: their `Slider` is this fork's `Range`, and §1 makes that rename mandatory, so copying their wording would name a component this library does not have — the reference points at the commit and the sentence stays about ours. Enforced by `assert-commit-parses.mjs`, which keys on a new entry appearing in `processed` rather than on the ledger being edited, so the reconciliation commits §6 step 4 requires are unaffected — verified against #467, which passes, and against #466 and #464, which are real ports and are flagged. The same pass extended that guard to reject a type with no `changelog-sections` entry (#437): a breaking commit of an unconfigured type keeps its raw lowercase type as the group title and sorts above every real section, reproduced by running the writer release-please uses. Both checks read `release-please-config.json` and the ledger rather than restating either. Last reviewed: 2026-08-23. +- 2026-08-23 — fix of #191 (PR pending): added the §2 rule **a focus outline is coloured by the focus token**, and took two design-agnostic fixes upstream had shipped alongside — `a:focus-visible { outline-offset: 0 }` in `index.css`, and `overflow: hidden` on every frame of the accordion and collapsible height animations. The issue read as an inconsistency, four colours doing one job across 46 theme files, and the maintainer scoped it to colour only: form stays per component, `ring-*` on inputs untouched. Measuring the four turned it into an accessibility fix. `outline-primary` is not a token at all — it resolves to `--color-primary`, a legacy Bitrix cyan at 1.99:1 against white — and `--ui-color-accent-soft-element-blue` is a dark blue in every context including the dark ones, 2.22:1 against `#262626`; WCAG 2.2 SC 1.4.11 asks 3:1, and seven of the nine outline-coloured sites failed it in at least one theme. `--ui-color-design-outline-focused-stroke` is the design system's own name for this and the only one of the four defined in all four contexts: 4.21:1 light, 4.24:1 dark. Two things were deliberately left: the `isAction` link's red focus, where the colour matches the hover state and the real defect is the token having no dark value (#473), and the fifteen `--b24ui-border-color` focus rings on inputs, where the ring is the field's border rather than a focus accent. A dead `hover:text(` — missing its dash, so Tailwind generated nothing — was removed from `link.ts` on the way past. Last reviewed: 2026-08-23. diff --git a/.sync/nuxt-ui.json b/.sync/nuxt-ui.json index 58ba4007..42dd24b8 100644 --- a/.sync/nuxt-ui.json +++ b/.sync/nuxt-ui.json @@ -542,7 +542,7 @@ "pr": 192, "b24ui_sha": "6b11d5d4", "decision": "skip", - "summary": "feat(theme): uniformize focus styles across components (#6576) — SKIPPED (maintainer call): rewrites focus styling across ~50 themes to nuxt/ui's outline-3/outline-{color}/25 + theme.colors model. b24ui has its own air-token focus system (ring-(--b24ui-border-color), --ui-color-design-selection-content, …) and no theme.colors; adopting upstream would regress the air design. Generic bits (a:focus-visible reset, keyframes overflow:hidden) also deferred. Tracked in issue #191" + "summary": "feat(theme): uniformize focus styles across components (#6576) — SKIPPED (maintainer call): rewrites focus styling across ~50 themes to nuxt/ui's outline-3/outline-{color}/25 + theme.colors model. b24ui has its own air-token focus system (ring-(--b24ui-border-color), --ui-color-design-selection-content, …) and no theme.colors; adopting upstream would regress the air design. The two generic bits were later taken on their own — a:focus-visible reset and keyframes overflow:hidden — along with a colour-only consolidation of the focus outline onto --ui-color-design-outline-focused-stroke, which turned out to fix contrast failures rather than only inconsistency (1.99:1 and 2.22:1 against 3:1). The upstream pattern itself stays skipped: form remains per component by maintainer decision. See issue #191." }, "39d53ae54b9d810dc691c4fbe1ac6205df5f6e05": { "pr": 193, diff --git a/src/runtime/index.css b/src/runtime/index.css index ce19dbee..2c47fc9e 100644 --- a/src/runtime/index.css +++ b/src/runtime/index.css @@ -13,6 +13,17 @@ @variant web (&:where(html[data-platform="web"] *)); @layer base { + /* + * From upstream. Browsers give a focused link's outline the default offset, + * which on an inline link is measured from the text box and lands the ring + * over the line above or below in tight copy. Zero keeps it on the link. + * Only the offset — the colour, width and style stay with whatever rule the + * component's theme sets. + */ + a:focus-visible { + outline-offset: 0; + } + body, .air-custom-bg, /* @deprecate This rule (.sidebar-layout.--inner (not body !)) is deprecated and will be removed in version 3.0.0 */ diff --git a/src/runtime/keyframes.css b/src/runtime/keyframes.css index f0920cd2..9dbe3cae 100644 --- a/src/runtime/keyframes.css +++ b/src/runtime/keyframes.css @@ -1,39 +1,54 @@ +/* + * `overflow: hidden` on every frame of the height animations, from upstream. + * Reka drives these by animating `height` from or to `0` while the content + * keeps its own height, so without it the content is drawn outside the + * collapsing box for the length of the animation — visible as a flash of text + * over whatever sits below the accordion. + */ @keyframes accordion-up { from { + overflow: hidden; height: var(--reka-accordion-content-height); } to { + overflow: hidden; height: 0; } } @keyframes accordion-down { from { + overflow: hidden; height: 0; } to { + overflow: hidden; height: var(--reka-accordion-content-height); } } @keyframes collapsible-up { from { + overflow: hidden; height: var(--reka-collapsible-content-height); } to { + overflow: hidden; height: 0; } } @keyframes collapsible-down { from { + overflow: hidden; height: 0; } to { + overflow: hidden; height: var(--reka-collapsible-content-height); } } diff --git a/src/theme/accordion.ts b/src/theme/accordion.ts index 76bcff09..6a1d6b38 100644 --- a/src/theme/accordion.ts +++ b/src/theme/accordion.ts @@ -14,7 +14,7 @@ export default { 'min-w-0 group flex-1 flex items-center gap-1.5', 'py-[12px]', 'font-(--ui-font-weight-medium) text-(length:--ui-font-size-sm) leading-[20px]', - 'focus-visible:outline-(--ui-color-accent-soft-element-blue)', + 'focus-visible:outline-(--ui-color-design-outline-focused-stroke)', 'cursor-pointer' ].join(' '), content: 'motion-safe:data-[state=open]:animate-[accordion-down_200ms_var(--ease-out)] motion-safe:data-[state=closed]:animate-[accordion-up_200ms_var(--ease-out)] overflow-hidden focus:outline-none', diff --git a/src/theme/chat-reasoning.ts b/src/theme/chat-reasoning.ts index 61b85f7c..6662ff71 100644 --- a/src/theme/chat-reasoning.ts +++ b/src/theme/chat-reasoning.ts @@ -16,7 +16,7 @@ export default { 'disabled:cursor-default', 'disabled:hover:text-muted', 'hover:text-default', - 'focus-visible:outline-offset-2 focus-visible:outline-primary', + 'focus-visible:outline-offset-2 focus-visible:outline-(--ui-color-design-outline-focused-stroke)', 'transition-colors' ].join(' '), leading: 'relative size-5 shrink-0', diff --git a/src/theme/chat-tool.ts b/src/theme/chat-tool.ts index 9522e4c0..500bc370 100644 --- a/src/theme/chat-tool.ts +++ b/src/theme/chat-tool.ts @@ -19,7 +19,7 @@ export default { 'disabled:cursor-default', 'disabled:hover:text-muted', 'hover:text-default', - 'focus-visible:outline-offset-2 focus-visible:outline-primary', + 'focus-visible:outline-offset-2 focus-visible:outline-(--ui-color-design-outline-focused-stroke)', 'transition-colors' ].join(' '), leading: 'relative shrink-0 size-5', diff --git a/src/theme/file-upload.ts b/src/theme/file-upload.ts index e4801829..11f3965e 100644 --- a/src/theme/file-upload.ts +++ b/src/theme/file-upload.ts @@ -141,7 +141,7 @@ export default { }, compoundVariants: [ { - class: 'focus-visible:outline-inverted' + class: 'focus-visible:outline-(--ui-color-design-outline-focused-stroke)' }, { highlight: true, diff --git a/src/theme/footer-columns.ts b/src/theme/footer-columns.ts index 90ad14c5..4b01fe2c 100644 --- a/src/theme/footer-columns.ts +++ b/src/theme/footer-columns.ts @@ -12,7 +12,7 @@ export default { label: 'text-(length:--ui-font-size-lg)/[normal] font-(--ui-font-weight-semi-bold)', list: 'mt-6 space-y-4', item: 'relative', - link: 'group text-sm flex items-center gap-1.5 focus-visible:outline-primary', + link: 'group text-sm flex items-center gap-1.5 focus-visible:outline-(--ui-color-design-outline-focused-stroke)', linkLeadingIcon: 'size-5 shrink-0', linkLabel: 'truncate', linkLabelExternalIcon: 'inline-block size-[14px] text-(--ui-color-design-plain-content-icon-secondary)' diff --git a/src/theme/link.ts b/src/theme/link.ts index 20d53407..bff5a171 100644 --- a/src/theme/link.ts +++ b/src/theme/link.ts @@ -12,7 +12,7 @@ export default (options: Required) => ({ base: [ 'cursor-pointer', // 'focus-visible:outline-info-text' // fix - 'focus-visible:outline-(--ui-color-accent-main-primary)', + 'focus-visible:outline-(--ui-color-design-outline-focused-stroke)', 'focus-visible:outline-1 focus-visible:rounded-[4px]', 'text-start' ].join(' '), @@ -37,7 +37,7 @@ export default (options: Required) => ({ 'border border-x-0 border-t-0 border-dashed', 'text-(--ui-color-design-outline-a1-content) border-b-(--ui-color-design-outline-a1-content)', 'hover:not-disabled:not-aria-disabled:no-underline', - 'hover:text(--ui-color-accent-soft-element-red) hover:not-disabled:not-aria-disabled:text-(--ui-color-accent-soft-element-red) hover:border-b-(--ui-color-accent-soft-element-red)', + 'hover:not-disabled:not-aria-disabled:text-(--ui-color-accent-soft-element-red) hover:border-b-(--ui-color-accent-soft-element-red)', 'focus-visible:outline-(--ui-color-accent-soft-element-red)' ].join(' ') } diff --git a/src/theme/page-links.ts b/src/theme/page-links.ts index f47227b7..2722d6bb 100644 --- a/src/theme/page-links.ts +++ b/src/theme/page-links.ts @@ -18,7 +18,7 @@ export default { 'group', 'text-(length:--ui-font-size-sm)', 'cursor-pointer', - 'focus-visible:outline-(--ui-color-accent-main-primary)', + 'focus-visible:outline-(--ui-color-design-outline-focused-stroke)', 'focus-visible:outline-1', 'focus-visible:rounded-[4px]', 'text-start', diff --git a/src/theme/splitter.ts b/src/theme/splitter.ts index d8951dd8..fec6d421 100644 --- a/src/theme/splitter.ts +++ b/src/theme/splitter.ts @@ -11,7 +11,7 @@ export default { panel: 'flex', handle: [ 'group relative shrink-0', - 'outline-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-color-accent-soft-element-blue)', + 'outline-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-color-design-outline-focused-stroke)', 'data-[panel-resize-handle-enabled=false]:cursor-default' ].join(' ') }, diff --git a/src/theme/table.ts b/src/theme/table.ts index b4c96abd..4303a7ad 100644 --- a/src/theme/table.ts +++ b/src/theme/table.ts @@ -19,10 +19,10 @@ export default { 'isolate', '[&>tr]:data-[selectable=true]:hover:bg-(--ui-color-bg-content-secondary)', 'light:[&>tr]:data-[selectable=true]:hover:bg-[#f6f8f9]', - '[&>tr]:data-[selectable=true]:focus-visible:outline-(--ui-color-accent-soft-element-blue)', + '[&>tr]:data-[selectable=true]:focus-visible:outline-(--ui-color-design-outline-focused-stroke)', '[&>tr]:data-[selected=true]:hover:bg-(--ui-color-bg-content-secondary)', 'light:[&>tr]:data-[selected=true]:hover:bg-(#eff7d7)', - '[&>tr]:data-[selected=true]:focus-visible:outline-(--ui-color-accent-soft-element-blue)', + '[&>tr]:data-[selected=true]:focus-visible:outline-(--ui-color-design-outline-focused-stroke)', '[&>tr]:last:[&>td]:border-b-0', 'divide-y divide-(--ui-color-divider-default)' ].join(' '), diff --git a/src/theme/toast.ts b/src/theme/toast.ts index cc0f21c5..3454f36c 100644 --- a/src/theme/toast.ts +++ b/src/theme/toast.ts @@ -32,7 +32,7 @@ export default { 'relative group overflow-hidden', 'rounded-[8px] p-4', 'flex gap-4', - 'focus-visible:outline-(length:--ui-design-outline-stroke-weight) focus-visible:outline-offset-2 focus-visible:outline-(--ui-color-design-outline-content-divider)', + 'focus-visible:outline-(length:--ui-design-outline-stroke-weight) focus-visible:outline-offset-2 focus-visible:outline-(--ui-color-design-outline-focused-stroke)', 'font-[family-name:var(--ui-font-family-primary)]', 'bg-(--ui-color-base-black-fixed)/80', 'text-(--ui-color-design-plain-na-focused-content)', diff --git a/test/components/__snapshots__/Accordion-vue.spec.ts.snap b/test/components/__snapshots__/Accordion-vue.spec.ts.snap index b24b3020..af8b4b2f 100644 --- a/test/components/__snapshots__/Accordion-vue.spec.ts.snap +++ b/test/components/__snapshots__/Accordion-vue.spec.ts.snap @@ -3,7 +3,7 @@ exports[`Accordion > renders with as correctly 1`] = ` "
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1015,7 +1015,7 @@ exports[`Accordion > renders with leading slot correctly 1`] = `
-
-
-
-