You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"about": "Audit scores for Astryx components, one entry per AUDITED component. A component with no entry here has not been audited — the roster of components is read from packages/core/src and packages/lab/src, never from a list. This file is the ONLY stored form of the ledger: there is no generated table, because a second copy goes stale the moment this one changes. A rendered view in the sandbox app is planned but not built, so this file is also the only way to read it today. Hand-edit it: add or replace the component's entry, pull, commit, push. A score-ledger tool that would apply the ratchet automatically is planned but is not on main, so the ratchet is a rule the auditor applies, not a program.",
"caveats": [
"All five rows come from the same calibration exercise on commit `dccdabea0b`, run by five independent graders. They are the only audits that exist so far, and they are honest about their limits rather than tidy.",
Expand DownExpand Up
@@ -1253,6 +1253,137 @@
],
"notes": "Zero Effects in an 832-line component. The dark-mode primary label contrast (3.11:1) is token-inherited and deliberately not scored against the component while the system-wide fix is open."
},
{
"component": "ButtonGroup",
"package": "core",
"status": "audited",
"score": 72,
"grade": "C",
"sections": {
"a11y": {
"score": 3,
"state": "scored",
"note": "One open BLOCK caps this at 3. A4: arrow keys pressed inside a member's open layer are handled by the group. ButtonGroup passes no boundarySelector to useListFocus, so ownsEvent() returns true for a keydown that bubbled out of an open DropdownMenu; getCurrentIndex() is -1 for the focused menuitem, so the group focuses its first or last member and preventDefaults the key. Measured in Chromium on the component's own WithDropdownMenu story: the menu stays visible and focus lands on 'Allow once' outside it. A2 open as a FIX: no APG pattern is named in the docs or the source, and the implemented model matches none (role=group with arrow navigation but three tab stops, measured). A20 and A21 are not measured: no story renders a disabled member, so both Chromium sweeps checked 0 elements across 0 of 9 stories. Passing and verified: axe 0 violations with the baseline unchanged, focus ring 2px accent at 3px offset in light and dark, forced-colors keeps the separators and the ring, icon targets 32x32.",
"weight": 16
},
"theming": {
"score": 3,
"state": "scored",
"note": "One open BLOCK caps this at 3. T6: elevation selects between StyleX style objects (elevationStyles[elevation]) but is absent from the sibling themeProps('button-group', {size, orientation}), so the group renders data-size and data-orientation only. Measured: an injected .astryx-button-group[data-elevation=\"none\"] rule reaches nothing, while rules on the class, [data-size] and [data-orientation] all reach the painted element. Everything else is clean: no colour or spacing literals, shadows are whole shadowVars entries, one theme target on the painting element, both theming guards green, and the y2k theme reaches the component (radius and font both change).",
"weight": 14
},
"api": {
"score": 4,
"state": "scored",
"note": "No BLOCKs. rest is captured and spread on the primary element with owned attributes set after it, className and style merged through mergeProps, onKeyDown composed. One FIX, deferred: ref={mergeRefs(ref, listRef)} is built inline during render, which is the exact line open PR #5267 migrates to useMergedRefs (#5269 lints it), so it is left alone. One NIT: the documented default for size reads 'md' without saying the group first inherits an ambient SizeContext.",
"weight": 14
},
"behavior": {
"score": 4,
"state": "scored",
"note": "No BLOCKs. B4: a member inside a group has no pressed representation. Measured with the pointer down, :active matching: transform stays none because Button drops styles.pressable inside a group, and the background is identical to hover, so press and hover are the same pixels. A standalone Button gets scale(0.98). Whether a connected member should depress is a design call, so it is carried rather than changed. B5 disabled is correct in the DOM (native disabled via context) but was not seen in pixels: no story renders it. Empty children is a NIT: children is required, and an empty group renders a zero-width div.",
"weight": 12
},
"design_objective": {
"score": 5,
"state": "scored",
"note": "Clean from source and confirmed in the browser: radii are the --radius-element role token and the wrapper and its members are concentric at gap 0, heights resolve to 28/32/36 from --size-element-*, the hover tint is an alpha overlay over the member's own base rather than an opaque fill, elevation is a whole shadowVars tier, motion is Button's transform/opacity/paint transition with reduced motion collapsing to 0s.",
"weight": 6
},
"design_rendered": {
"score": 3,
"state": "scored",
"note": "Graded numerically from 34 captures: 9 stories x light and dark, RTL for the four direction-sensitive ones, the y2k custom theme, forced-colors, and hover/active/focus-visible pairs. State-visual conformance: rest pass, hover pass (Overlay style, alpha tint over own base), focus-visible pass (Outline style, 2px accent at 3px offset, both modes), pressed FAILS its row (no representation at all, identical to hover), disabled uncaptured because no story renders it. Partial state coverage caps this section at 3 under the rubric's own rule, which is where the failing pressed row lands it anyway.",
"weight": 4
},
"testing": {
"score": 2.5,
"state": "scored",
"note": "Two open BLOCKs cap this at 2.5. V6: 28 tests and not one keyboard assertion. The component wires useListFocus arrow navigation, wrapping, Home/End and RTL flipping, and no test uses user-event at all. V10: no story renders a disabled member, so the a11y sweep, the disabled-hover sweep and the disabled-cursor sweep each report a pass over zero elements for this component. What is there is good: the trailing-radius suite compiles Button.tsx with the real StyleX babel plugin and reads the emitted selector rather than hand-copying it. V11 open as a FIX: rtl:audit --filter ButtonGroup yields 0 pass / 0 fail / 1 N-A because there is no curated targets.json entry.",
"weight": 8
},
"code_health": {
"score": 4,
"state": "scored",
"note": "No Effects, context value memoized, focus behaviour composed from the shared hook rather than reimplemented, no refs read during render, lint:strict clean. Held off 5 by the inline mergeRefs identity churn, which is scored in the API section and belongs to open PR #5267.",
"weight": 8
},
"docs": {
"score": 4,
"state": "scored",
"note": "No BLOCKs, three FIXes. X10: isDisabled is the one documented prop no story exercises under the pinned parse rule (not an args key, not an argTypes key, not a JSX attribute), so 8 of 9 documented props are covered; there is no coverage script in scripts/, so this is a hand count. X12: no story for the disabled state, and elevation only exists as an argTypes control. X16: no accessibility guidance in bestPractices and no APG pattern named. The rest is in good shape: description, three do's and three don'ts, anatomy, a hero block plus two example blocks, bare-fence examples, theming in sync, playground defaults.",
"weight": 8
},
"i18n_rtl": {
"score": 4,
"state": "scored",
"note": "No BLOCKs. RTL was measured rather than read: the end-cap radii flip, the separator border moves to the inline-start side, and ArrowLeft advances in RTL while ArrowRight retreats. No physical properties, and direction is resolved lazily by the shared hook. Two FIXes: no targets.json entry, so the curated RTL sweep sees nothing for this component (0 pass / 0 fail / 1 N-A), and no long-text story, so text expansion is unexercised. The component itself carries no user-visible strings, so most of the section passes vacuously; it stays scored rather than limited at exactly one third non-vacuous verdicts.",
"weight": 5
},
"responsive": {
"score": 4,
"state": "scored",
"note": "Measured at 320px: document scrollWidth equals clientWidth, no horizontal scroll, no clipped member. Under an emulated coarse pointer the icon members stay 32x32 with no expansion toward 44px, which is Button and IconButton sizing rather than the group's, so it is advisory here. One FIX: no narrow-container, long-text or empty story, which is the same story gap the testing section blocks on.",
"weight": 5
}
},
"blocks": {
"count": 4,
"open": [
{
"id": "A4",
"summary": "Arrow keys pressed inside a member's open layer move focus out of it: no boundarySelector, so the group handles keydowns that bubble from an open DropdownMenu and focuses a member while the menu stays open.",
"issue": null
},
{
"id": "T6",
"summary": "elevation selects between StyleX style objects but is missing from the themeProps call, so no [data-elevation] hook exists for a theme.",
"issue": null
},
{
"id": "V6",
"summary": "No keyboard test: arrow navigation, wrapping, Home/End and RTL flipping are all wired and none is asserted.",
"issue": null
},
{
"id": "V10",
"summary": "No story renders a disabled member, so the a11y, disabled-hover and disabled-cursor sweeps all pass over zero elements.",
"note": "34 Chromium captures: 9 stories x light and dark, RTL for horizontal, icon-only, split-button and with-dropdown-menu, the y2k custom theme, forced-colors, and hover/active/focus-visible states."
"note": "WithDropdownMenu story, menu open, focus on a menuitem: ArrowLeft focuses the last member, ArrowRight focuses 'Allow once', and the menu stays visible with focus outside it."
},
{
"label": "Theming reach probe",
"note": "Injected theme rules on .astryx-button-group, [data-size] and [data-orientation] all reach the painted element; box-shadow from a theme wins over the component's StyleX rule, so #5095 does not reproduce here; [data-elevation] matches nothing because elevation is absent from themeProps."
},
{
"label": "Disabled sweeps are vacuous",
"note": "disabled-cursor-audit and disabled-hover-audit both report 'Checked 0 disabled element(s) across 0 of 9 stories' for ButtonGroup, so A20 and A21 are not measured rather than passing."
"notes": "First audit of this component. Pre-fix baseline, taken before any change, at b594d5c on the night of 2026-08-23. Graded under rubric 1.7.1, including A21 against 'default' rather than 'not-allowed'; A20 and A21 could not be measured because no story renders a disabled member. Section 5b is graded numerically from captures rather than left unmeasured. The roving-tabindex question for this family is an open design decision in issue #4238, which asserts that ButtonGroup already implements roving; it does not, and all three members are tab stops."