@astryxdesign/core 0.7.0 → 0.7.1-canary.2b6d129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BottomSheet/BottomSheet.d.ts +1 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +12 -2
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +25 -16
- package/dist/ContextMenu/ContextMenu.d.ts +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +22 -8
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +3 -26
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +10 -3
- package/dist/Typeahead/BaseTypeahead.d.ts +1 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +12 -3
- package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
- package/dist/hooks/useIsomorphicLayoutEffect.d.ts.map +1 -1
- package/dist/utils/focusReturn.d.ts +12 -8
- package/dist/utils/focusReturn.d.ts.map +1 -1
- package/dist/utils/focusReturn.js +44 -3
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheet.test.tsx +114 -0
- package/src/BottomSheet/BottomSheet.tsx +12 -2
- package/src/BottomSheet/__tests__/BottomSheetFocusReturn.a11y.browser.spec.ts +96 -0
- package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +5 -6
- package/src/CodeBlock/CodeBlock.spec.md +55 -45
- package/src/CodeBlock/CodeBlock.test.tsx +11 -9
- package/src/CodeBlock/CodeBlock.tsx +29 -18
- package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +125 -0
- package/src/ContextMenu/ContextMenu.spec.md +1 -1
- package/src/ContextMenu/ContextMenu.test.tsx +36 -3
- package/src/ContextMenu/ContextMenu.tsx +27 -11
- package/src/ContextMenu/__tests__/ContextMenuCursorAnchor.a11y.browser.spec.ts +153 -0
- package/src/Dialog/Dialog.tsx +2 -30
- package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +21 -14
- package/src/DropdownMenu/DropdownMenu.test.tsx +97 -1
- package/src/DropdownMenu/DropdownMenu.tsx +10 -3
- package/src/DropdownMenu/__tests__/DropdownMenuFocusReturn.a11y.browser.spec.ts +73 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +21 -14
- package/src/Markdown/Markdown.spec.md +37 -23
- package/src/Markdown/plugins/semanticFence.test.tsx +3 -2
- package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +22 -15
- package/src/PowerSearch/PowerSearch.test.tsx +45 -0
- package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +25 -18
- package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +19 -12
- package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +21 -14
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +18 -11
- package/src/Typeahead/BaseTypeahead.test.tsx +99 -1
- package/src/Typeahead/BaseTypeahead.tsx +17 -3
- package/src/utils/focusReturn.test.ts +79 -2
- package/src/utils/focusReturn.ts +48 -3
|
@@ -3,11 +3,11 @@ schema_version: 3
|
|
|
3
3
|
template_version: 7
|
|
4
4
|
kind: component
|
|
5
5
|
id: component:CodeBlock
|
|
6
|
-
authority:
|
|
6
|
+
authority: current
|
|
7
7
|
archive_reason: null
|
|
8
8
|
superseded_by: null
|
|
9
|
-
approved_by:
|
|
10
|
-
approved_at:
|
|
9
|
+
approved_by: cixzhang
|
|
10
|
+
approved_at: 2026-10-12
|
|
11
11
|
owners: [cixzhang]
|
|
12
12
|
review_triggers: [public-api, behavior, layout, theming, accessibility, testing]
|
|
13
13
|
verified_by:
|
|
@@ -29,6 +29,7 @@ architecture:
|
|
|
29
29
|
architecture:public-component-api,
|
|
30
30
|
architecture:component-theming-surface,
|
|
31
31
|
architecture:component-test-sufficiency,
|
|
32
|
+
architecture:theme-tokens,
|
|
32
33
|
]
|
|
33
34
|
contributing: []
|
|
34
35
|
system_specs: [spec:AST-025]
|
|
@@ -38,15 +39,15 @@ system_specs: [spec:AST-025]
|
|
|
38
39
|
|
|
39
40
|
## Contract at a glance
|
|
40
41
|
|
|
41
|
-
| Area | Contract
|
|
42
|
-
| ----------------------- |
|
|
43
|
-
| Public contract | `CodeBlock` and `CodeBlockProps` are exported from `@astryxdesign/core/CodeBlock`; `code` is required, and the remaining released props configure language, header, line presentation, copy, wrapping, sizing, syntax theme, and collapse behavior. |
|
|
44
|
-
| Behavior | CodeBlock renders a read-only multi-line code surface, applies syntax highlighting when available, preserves the source string for copy, and exposes optional line, header, copy, theme, and disclosure behavior without editing the content.
|
|
45
|
-
| End-user impact | Readers can inspect and copy code, understand optional line emphasis, and operate long or collapsible examples with keyboard and assistive technology.
|
|
46
|
-
| Builder impact | Builders choose the released display axes without an additional migration or opt-in.
|
|
47
|
-
| Compatibility/readiness | The released API and defaults remain stable
|
|
48
|
-
| Review checks | Reject lost code text, copy of a transformed string, an unnamed copy or disclosure control, stale disclosure state/wiring, dropped root props/ref, a theme target off its painter, or viewport focusability that ignores effective overflow.
|
|
49
|
-
| Governing rules | `architecture:public-component-api/INV1,INV5–INV9`; `architecture:component-theming-surface/INV3–INV7,INV12`; `architecture:component-test-sufficiency/INV1–INV7,INV10–INV11`; `spec:AST-025/FR12–FR13
|
|
42
|
+
| Area | Contract |
|
|
43
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| Public contract | `CodeBlock` and `CodeBlockProps` are exported from `@astryxdesign/core/CodeBlock`; `code` is required, and the remaining released props configure language, header, line presentation, copy, wrapping, sizing, syntax theme, and collapse behavior; `isDiff` (FR9) adds the diff layer as a caller choice over any language. |
|
|
45
|
+
| Behavior | CodeBlock renders a read-only multi-line code surface, applies syntax highlighting when available, preserves the source string for copy, and exposes optional line, header, copy, theme, and disclosure behavior without editing the content. |
|
|
46
|
+
| End-user impact | Readers can inspect and copy code, understand optional line emphasis, and operate long or collapsible examples with keyboard and assistive technology. |
|
|
47
|
+
| Builder impact | Builders choose the released display axes without an additional migration or opt-in; the diff layer is one more opt-in axis, and five defaulted syntax-theme slots mean no syntax theme has to change for it. |
|
|
48
|
+
| Compatibility/readiness | The released `CodeBlock` API and defaults remain stable; FR9 (the diff layer) is current and an implementation gap until its implementation ships; Markdown's default path for `diff` fences changes with it (`component:Markdown` FR29). |
|
|
49
|
+
| Review checks | Reject lost code text, copy of a transformed string, an unnamed copy or disclosure control, stale disclosure state/wiring, dropped root props/ref, a theme target off its painter, or viewport focusability that ignores effective overflow. |
|
|
50
|
+
| Governing rules | `architecture:public-component-api/INV1,INV5–INV9`; `architecture:component-theming-surface/INV3–INV7,INV12`; `architecture:component-test-sufficiency/INV1–INV7,INV10–INV11`; `spec:AST-025/FR12–FR13`; `architecture:theme-tokens` (syntax vocabulary and the diff slot defaults). |
|
|
50
51
|
|
|
51
52
|
This table is a review projection; the body below is authoritative only after owner approval.
|
|
52
53
|
|
|
@@ -78,33 +79,35 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
78
79
|
|
|
79
80
|
## Public concepts
|
|
80
81
|
|
|
81
|
-
| Concept | Closed values or states
|
|
82
|
-
| ------------------ |
|
|
83
|
-
| code content | caller-supplied string
|
|
84
|
-
| language | built-in identifier, custom-tokenizer identifier, or plaintext/unknown
|
|
85
|
-
| line presentation | numbers off/on; highlights absent or one-indexed line set; wrapped/unwrapped
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
82
|
+
| Concept | Closed values or states | Meaning | Availability | Default | Owner | Stability | Invalid-value behavior |
|
|
83
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------- | ------------------- | --------- | --------------------------------------------------------------------- |
|
|
84
|
+
| code content | caller-supplied string | Supplies the exact read-only source that is displayed and copied. | Every render | required | product callsite | stable | rejected by the public type |
|
|
85
|
+
| language | built-in identifier, custom-tokenizer identifier, or plaintext/unknown | Selects tokenization and the optional visible/accessible language label. | Every render | `plaintext` | component:CodeBlock | stable | unsupported built-ins render without tokens |
|
|
86
|
+
| line presentation | numbers off/on; highlights absent or one-indexed line set; wrapped/unwrapped | Adds navigation and emphasis without changing the copied source string. | Code body | off/absent | component:CodeBlock | stable | out-of-range highlights have no rendered line |
|
|
87
|
+
| diff layer | off; on — each line classified by its first character: `+` added, `-` removed, header (`@@`; a `--- `/`+++ ` pair directly before an `@@`), else context | Tints added and removed rows and quiets header rows from the syntax theme while the rest of every line keeps the base language's syntax paint; the glyph stays in display and copy. | Code body, any `language` | off | product callsite | stable | rejected by the public type (boolean) |
|
|
88
|
+
| header | absent; title; language label; title and language label | Gives the snippet context and houses the copy control when any visible header content exists. | Derived from title and label choices | derived | component:CodeBlock | stable | empty visible content still requires AT naming |
|
|
89
|
+
| copy | hidden; available; transient copied feedback | Copies the exact code string, then invokes `onCopy` only after a successful clipboard write. | Every render when enabled | available | component:CodeBlock | stable | failed clipboard writes do not invoke `onCopy` |
|
|
90
|
+
| syntax rendering | auto; CSS ranges; spans; custom tokenizer; optional per-instance syntax theme | Selects a supported paint path without changing the text or public code semantics. | Every render | `auto` | component:CodeBlock | stable | unsupported range capability falls back to spans |
|
|
91
|
+
| collapse | unavailable; expanded; collapsed | Turns an eligible visible header into a disclosure when the line threshold is met. | Long blocks with a header and collapse enabled | unavailable | component:CodeBlock | stable | missing-header or below-threshold blocks remain expanded and ordinary |
|
|
92
|
+
| container geometry | card/section; small/medium; fit-content/custom width; unbounded/bounded height | Selects presentation and overflow geometry while preserving the read-only code task. | Every render | card/md | component:CodeBlock | stable | values follow their documented CSS/type behavior |
|
|
91
93
|
|
|
92
94
|
## Behavioral and layout contract
|
|
93
95
|
|
|
94
|
-
| ID | Candidate invariant
|
|
95
|
-
| --- |
|
|
96
|
-
| FR1 | CodeBlock MUST display the caller's code string as read-only code. Built-in or custom tokenization MAY change syntax paint but MUST NOT change displayed or copied source text.
|
|
97
|
-
| FR2 | A successful copy MUST write the exact `code` string, invoke `onCopy` once, expose localized copied feedback, and preserve the copy control's discoverable purpose. A failed write MUST NOT report success.
|
|
98
|
-
| FR3 | A title or visible non-plaintext language label MUST create the visible header. Hiding the language label MUST NOT remove copy behavior. Headerless copy remains available when enabled.
|
|
99
|
-
| FR4 | Line numbers, highlighted lines, wrapping, width, container presentation, size, and maximum height MUST affect only their documented presentation axes. Numeric `maxHeight={0}` MUST remain a valid zero-pixel bound rather than being discarded.
|
|
100
|
-
| FR5 | Collapse MUST become available only when enabled, a visible header exists, and the line threshold is met. The control MUST expose expanded state and a resolvable controlled region; collapsed content remains mounted but inert, and losing the control expands the region before hiding the control.
|
|
101
|
-
| FR6 | The root and documented header/title/copy targets MUST remain on their visible painters, reflect the documented visual axes, and continue emitting the approved deprecated aliases until their authorized removal window.
|
|
102
|
-
| FR7 | Supported DOM, data, ARIA, class, style, and `xstyle` inputs MUST compose on the documented root, and the public ref MUST reach that root.
|
|
103
|
-
| FR8 | The code viewport MUST enter sequential keyboard navigation only while at least one applicable axis effectively overflows; losing overflow MUST NOT move current focus. It retains an appropriate role and accessible name while focusable.
|
|
96
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
97
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
98
|
+
| FR1 | CodeBlock MUST display the caller's code string as read-only code. Built-in or custom tokenization MAY change syntax paint but MUST NOT change displayed or copied source text. | released types, source, tests, consumer docs; established public interface | verify |
|
|
99
|
+
| FR2 | A successful copy MUST write the exact `code` string, invoke `onCopy` once, expose localized copied feedback, and preserve the copy control's discoverable purpose. A failed write MUST NOT report success. | shipped source/tests; `architecture:component-test-sufficiency/INV2–INV6`; WCAG name/status obligations | verify |
|
|
100
|
+
| FR3 | A title or visible non-plaintext language label MUST create the visible header. Hiding the language label MUST NOT remove copy behavior. Headerless copy remains available when enabled. | shipped source/tests/docs; established public interface | verify |
|
|
101
|
+
| FR4 | Line numbers, highlighted lines, wrapping, width, container presentation, size, and maximum height MUST affect only their documented presentation axes. Numeric `maxHeight={0}` MUST remain a valid zero-pixel bound rather than being discarded. | released public type/docs; `architecture:public-component-api/INV3,INV9`; objective JavaScript nullish semantics | verified by focused regression |
|
|
102
|
+
| FR5 | Collapse MUST become available only when enabled, a visible header exists, and the line threshold is met. The control MUST expose expanded state and a resolvable controlled region; collapsed content remains mounted but inert, and losing the control expands the region before hiding the control. | shipped docs/source/tests; WAI-ARIA disclosure pattern and HTML `inert` semantics | verify |
|
|
103
|
+
| FR6 | The root and documented header/title/copy targets MUST remain on their visible painters, reflect the documented visual axes, and continue emitting the approved deprecated aliases until their authorized removal window. | `architecture:component-theming-surface/INV3–INV7,INV12`; source/docs/tests | verify |
|
|
104
|
+
| FR7 | Supported DOM, data, ARIA, class, style, and `xstyle` inputs MUST compose on the documented root, and the public ref MUST reach that root. | `architecture:public-component-api/INV5,INV6,INV8`; shipped source | verify |
|
|
105
|
+
| FR8 | The code viewport MUST enter sequential keyboard navigation only while at least one applicable axis effectively overflows; losing overflow MUST NOT move current focus. It retains an appropriate role and accessible name while focusable. | `spec:AST-025/FR12–FR13` | settled by current authority |
|
|
106
|
+
| FR9 | With the diff layer on (`isDiff`), CodeBlock MUST classify each line by its first character — `+` added, `-` removed, `@@` header, anything else context — except that a `--- ` line (three minus signs and a space, the unified-diff file header form) directly followed by a `+++ ` line and then an `@@` line is a file header (both lines), wherever it occurs; every other `---`/`+++` line, `---i;` included, is removed or added. CodeBlock MUST set the glyph column aside and tokenize the REST of every line (context, added, and removed alike) with the base `language`'s syntax highlighting. Added, removed, and header lines MUST paint as whole rows — the line-number gutter cell included — from the syntax-theme slots `--color-syntax-added`, `--color-syntax-removed`, and `--color-syntax-diff-header`, whose defaults `architecture:theme-tokens` owns; header rows take the syntax theme's `punctuation` colour as text (`--color-text-secondary` in the default theme) and never change paint; context rows keep ordinary paint. On an added or removed row, the line number and the glyph take the row's marker slot — `--color-syntax-added-marker` / `--color-syntax-removed-marker` — so the margin reads the change in colour; every run of code text, painted by a token or not, keeps its syntax-theme colour. In the Astryx default syntax theme the marker slots default to the theme's tinted-text tokens and the header paint to the muted background (`architecture:theme-tokens` owns the values); a syntax theme that does not set them inherits its own colours — the marker slots and the header text its line-number colour (its `punctuation` slot), the header paint none at all (the header row sits on the theme's own syntax background) — so number, glyph, and header text stay legible on that theme's background in every page mode, and the glyph's shape carries the meaning where its colour does not. The glyph (`+`, `-`, the leading space of a context line, or nothing for a context or header line without one — the marker column is then empty and the whole line is code) MUST render in its own fixed-width marker column — secondary colour on context rows and the row's marker slot on added and removed rows, separated from the code by a token gap (`--spacing-2`, a proportion themes may vary like the rest), excluded from selection for layout yet present in the copied text — so the code column starts at the same inline offset on every line and the content's own indentation is never shifted; the gutter order is line number, marker, code, mirrored in RTL with the code's direction unchanged. The header (language label, copy, disclosure) and the viewport's name stay as for any other code: with the layer over plaintext the visible language label and the viewport name are `diff`, and over a language they name both (`tsx diff`). Markdown's fence mapping onto the layer is `component:Markdown` FR29. Row paint MUST compose with line numbers without moving text; on a highlighted added, removed, or header line the highlight REPLACES the row paint rather than stacking on it, so text keeps its single-paint contrast; and row paint MUST span the full line width — the scrolled overflow of a long line included, in wrapped and unwrapped mode — not only the visible viewport. Contrast floor: in the Astryx default syntax theme, every default token colour and the row's marker slot (line number and glyph) on a painted row MUST measure ≥ 4.5:1 against the default tints composited over the code block's own `--color-syntax-background` on the default card surface. In a syntax theme that overrides a diff slot, a token that already measures under 4.5:1 on that theme's own syntax background MUST NOT drop further on the slot, and every other token MUST clear 4.5:1 on it. Two known shortfalls, tracked separately: the shipped syntax presets do not yet meet the floor on the default tints with the layer on, and shipped page themes whose own text tokens fall under it on the tints keep that shortfall, tracked for a fix in those themes, until each sets its tokens or the diff slots. | this record DEC-1 | current; owner-approved 2026-10-12; implementation gap |
|
|
104
107
|
|
|
105
108
|
### Allowed variation
|
|
106
109
|
|
|
107
|
-
- **AV1 — Syntax paint.** Built-in token colors, custom token types, CSS Highlight API use, span fallback, and syntax themes may vary paint without changing the source string or code task.
|
|
110
|
+
- **AV1 — Syntax paint.** Built-in token colors, custom token types, CSS Highlight API use, span fallback, and syntax themes may vary paint without changing the source string or code task. Diff row tints are three syntax-theme slots like any other syntax paint: a syntax theme may harmonize them with its palette, and a theme that sets none keeps the defaults.
|
|
108
111
|
- **AV2 — Theme paint.** Current CodeBlock targets may vary their accepted visual properties without changing text, control semantics, or ownership.
|
|
109
112
|
- **AV3 — Code content.** The product supplies arbitrary code text and the matching language identifier; CodeBlock does not interpret the program's meaning.
|
|
110
113
|
|
|
@@ -174,24 +177,31 @@ Exact CodeBlock proportions and syntax colors are not contract requirements; the
|
|
|
174
177
|
|
|
175
178
|
## Verification map
|
|
176
179
|
|
|
177
|
-
| Contract | Verification
|
|
178
|
-
| ----------------- |
|
|
179
|
-
| FR1, PR1–PR3 | `tokenizer.test.ts`; `highlightRanges.test.ts`; CodeBlock focused tests and built Storybook
|
|
180
|
-
| FR2, AR1, AR3 | `CodeBlock.test.tsx`; component-scoped accessibility audit
|
|
181
|
-
| FR3–FR5, AR2 | `CodeBlock.test.tsx`; `CodeBlock.stories.tsx`; exact-head browser evidence
|
|
182
|
-
| FR6 | `themingTargets.test.ts`; consumer docs; source review
|
|
183
|
-
| FR7 | focused root-prop/ref tests; strict lint/typecheck; consumer-doc validation
|
|
184
|
-
| FR8, AR4 | `spec:AST-025` integration evidence plus real-browser fit/overflow/focus transitions
|
|
185
|
-
|
|
|
186
|
-
|
|
|
180
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
181
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
|
|
182
|
+
| FR1, PR1–PR3 | `tokenizer.test.ts`; `highlightRanges.test.ts`; CodeBlock focused tests and built Storybook | short/long, built-in/custom, ranges/spans, empty lines | Text changes, stale tokens paint, ranges leak, source order changes, or long work stops yielding/canceling. | `audit:CodeBlock/behavior` |
|
|
183
|
+
| FR2, AR1, AR3 | `CodeBlock.test.tsx`; component-scoped accessibility audit | copy success, rapid repeat, localized feedback, empty title | Exact source is not copied, callback fires on failure, announcement is absent/duplicated, or a control is unnamed. | `audit:CodeBlock/a11y` |
|
|
184
|
+
| FR3–FR5, AR2 | `CodeBlock.test.tsx`; `CodeBlock.stories.tsx`; exact-head browser evidence | headerless, titled, zero max height, line options, collapse/control removal | Header/copy placement changes, zero is dropped, disclosure state/wiring/keyboard behavior breaks, or inert outlives its control. | `audit:CodeBlock/behavior` |
|
|
185
|
+
| FR6 | `themingTargets.test.ts`; consumer docs; source review | root, header, title, copy; current and deprecated names | A target leaves its painter, an axis stops reflecting, or a deprecated alias disappears before authorization. | `audit:CodeBlock/theming` |
|
|
186
|
+
| FR7 | focused root-prop/ref tests; strict lint/typecheck; consumer-doc validation | ref plus supported root/style/data/ARIA inputs | The ref misses the root, accepted props disappear, or styling inputs replace rather than compose. | `audit:CodeBlock/api` |
|
|
187
|
+
| FR8, AR4 | `spec:AST-025` integration evidence plus real-browser fit/overflow/focus transitions | fitting, inline overflow, block overflow, losing overflow | A fitting viewport adds a stop, overflow is unreachable, naming disappears, or focus moves when overflow ends. | `audit:CodeBlock/a11y` |
|
|
188
|
+
| FR9 | `tokenizer.test.ts` (diff line classes; file-header pair rule; base-language tokens past the glyph); `CodeBlock.test.tsx` (row attributes, marker column, copy string incl. glyphs, `highlightLines`, header kept with the `diff` / `tsx diff` label and viewport name); syntax-theme tests (five slot defaults and overrides; a preset without marker or header slots takes punctuation text and no header paint); exact-head browser evidence incl. every default syntax token colour and the row marker slot (line number and glyph) measured ≥ 4.5:1 on painted rows in light and dark against the code block's own `--color-syntax-background` on the default card surface (the two known shortfalls in FR9 excepted) (on the light page background the surface is translucent and a plain row's default keyword already sits under the floor — a pre-existing shortfall that affects every CodeBlock there, tracked separately), and equal code-column x-offsets for context, added, and removed lines | added/removed/header/context over `tsx` and over plaintext; a removed line starting with `--`; `---i;` / `+++i;` right before an `@@` (removed/added, not header); a `--- `/`+++ ` pair with no `@@` after it (removed/added); a `--- `/`+++ `/`@@` file header after an earlier hunk (header); the layer over plaintext (label and viewport name `diff`) and over `tsx` (`tsx diff`); a context line with no leading space; a line longer than the viewport, scrolled (tint to the full scroll width) and wrapped; with `highlightLines` on an added line and on a header line (highlight paint only, text measured on it); with line numbers (gutter cell tinted, number in the row's marker slot); plain and token-painted text on a changed row keeping their syntax colours; light and dark (contrast measured on each); a custom syntax theme overriding the slots; indented code under all three line kinds; RTL (gutter mirrored, code direction unchanged) | A glyph removed from display or copy, a changed line's code starting at a different offset than its neighbours, a header row painted as a change or hidden, a header's text or paint taken from the page theme under a defined syntax theme, a `--`-prefixed removed line painted as a header, syntax paint lost past the glyph, a tint from anywhere but the three row slots, a gutter cell left untinted, a line number or glyph left in the ordinary colour on a changed row, code text recoloured by the row, a tint that stops at the viewport edge of a scrolled line, the header, collapse control, or viewport name lost on a diff, a `---i;` line painted as a header, or a default token or marker on a painted row under 4.5:1 on the default surface, or an override on which a token that clears 4.5:1 on the theme's own background falls under it or an already-short token drops further | `audit:CodeBlock/a11y` |
|
|
189
|
+
| RTL | `rtl:audit -- --filter CodeBlock` D2 target plus exact-head browser evidence | title and copy order in LTR/RTL | Logical order fails to mirror or physical placement leaks. | `audit:CodeBlock/rtl` |
|
|
190
|
+
| Consumer metadata | CLI `component CodeBlock --json`; docs typecheck; prop/target validation; package-export checks | released props/defaults/ref and current/deprecated targets | A released prop/default/ref/target disappears from consumer discovery or docs name an unsupported value. | `audit:CodeBlock/docs` |
|
|
187
191
|
|
|
188
192
|
## Decision log
|
|
189
193
|
|
|
190
|
-
|
|
194
|
+
### DEC-1 — Diff is a line layer over any language, tinted by three defaulted syntax-theme slots
|
|
195
|
+
|
|
196
|
+
**Decider:** `cixzhang`, `2026-10-12`
|
|
197
|
+
|
|
198
|
+
A reader of a changelog, migration note, or review comment sees what was added and what was removed at a glance, and still reads the code as code: with the diff layer on, the leading `+`/`-` column is set aside and the rest of every line keeps the base language's syntax highlighting, so a `tsx` diff looks like `tsx` with two kinds of tinted rows. The tints are syntax paint, so they live in the syntax theme as `--color-syntax-added`, `--color-syntax-removed`, and `--color-syntax-diff-header`, with the margin colours beside them as `--color-syntax-added-marker` and `--color-syntax-removed-marker`, all with defaults `architecture:theme-tokens` owns; the gutter cell, line number, and glyph share the row's hue because a reader scanning the margin should find the change there, while the code — every token and every plain run — keeps the syntax theme's colours so a property or identifier reads the same on a changed row as on a context row; a theme may override them to sit with its own palette. No single default strength keeps every shipped preset's passing tokens above the floor on the tints while the tint stays visible — a dark preset under a light page theme receives a pale tint — so the floor binds the default theme and overriding themes, and the presets' shortfall is known and tracked separately. Hunk and file headers stay visible as quiet rows in the syntax theme's own secondary (punctuation) colour — on the muted band in the default theme, on the theme's own background in a defined theme — the treatment the common diff renderers give them — because a fence is the author's exact text: paint never changes it (FR1) and copy round-trips it; an author who wants no headers leaves them out of the fence. A `--- ` line (with its space) is a file header only directly before a `+++ ` line and an `@@` line, so a removed line that starts with `--` — `---i;` right before the next hunk included — is not misread, and a multi-file diff keeps every file header; the one shape the rule cannot tell apart is a removed `-- ` line directly followed by an added `++ ` line and a hunk header, which reads as a file header and is accepted. A Markdown fence selects the layer by its info string on the default path only, so a custom renderer's contract stays unchanged. The `+`/`-` glyph sits in its own marker column with a token gap before the code, so a reader's eye finds the change at the margin and the code's indentation reads exactly as written. On a tinted row the glyph and the line number take the row's marker slot, because the glyph is the cue a reader without colour depends on and it reads with the code it marks; in the default theme the marker slots default to the theme's own tinted-text tokens, which are made to sit on tinted surfaces, and a syntax theme that does not set them keeps its own colours — its line-number colour for the markers and the header text, and no header paint — rather than borrowing the page theme's tokens, which could sit on the wrong polarity under a dark preset on a light page. Rejected: deriving the marker colours from the theme's `string` and `tag` tokens (a theme whose `tag` is green would mark removed rows green); recolouring the code itself to the row hue, all of it or only the runs no token paints (it changes what a property or identifier looks like between rows). A light tint of plain code toward the row hue is not adopted. Text keeps its syntax colours — the semantic text colours fall under the floor on their tints — and the glyphs stay in the text so the change reads without colour and copies exactly. The Markdown fence mapping is Markdown's own fact (its FR29); this record owns only the layer.
|
|
199
|
+
|
|
200
|
+
A per-line typed wash (an added/removed flavour of `highlightLines`) is not decided here: the layer answers the need with the diff text itself. On a highlighted changed line the highlight replaces the tint because two translucent paints would compound into a colour no theme chose and push tokens under the floor. Rejected: hiding header lines (paint would change the displayed text and break copy; authors omit headers by omitting them from the fence); `diff` as a standalone language (every line falls to plaintext, so the code stops reading as code); tinting from the semantic tokens alone with no syntax slots (a custom syntax theme could not harmonize the rows with its palette); colouring the changed text with `--color-success`/`--color-error` (below the 4.5:1 text minimum on the tinted rows); per-language `diff-<lang>` identifiers in the component API (they multiply the language list; the fence convention maps to the prop instead); a separate Diff component (a fence is a CodeBlock and Markdown already routes fences there).
|
|
191
201
|
|
|
192
202
|
## Open questions
|
|
193
203
|
|
|
194
|
-
None.
|
|
204
|
+
None.
|
|
195
205
|
|
|
196
206
|
## Content boundary
|
|
197
207
|
|
|
@@ -81,17 +81,19 @@ describe('CodeBlock', () => {
|
|
|
81
81
|
expect(root).toHaveStyle({opacity: '0.75'});
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
it('
|
|
84
|
+
it('names the code viewport and gives it a tab stop only while it overflows', () => {
|
|
85
|
+
// jsdom has no layout, so the viewport fits: a fitting viewport is not a
|
|
86
|
+
// tab stop (AST-025 FR12); the overflowing states are browser evidence.
|
|
85
87
|
render(<CodeBlock code="const x = 1;" language="javascript" />);
|
|
86
88
|
const region = screen.getByRole('group');
|
|
87
|
-
expect(region).toHaveAttribute('tabindex'
|
|
89
|
+
expect(region).not.toHaveAttribute('tabindex');
|
|
88
90
|
expect(region).toHaveAttribute('aria-label', 'javascript');
|
|
91
|
+
expect(region).toHaveAttribute('data-scroll-axis', 'both');
|
|
89
92
|
});
|
|
90
93
|
|
|
91
94
|
it('labels the scroll region "Code" when no language label is shown', () => {
|
|
92
95
|
render(<CodeBlock code="hello" hasLanguageLabel={false} />);
|
|
93
96
|
const region = screen.getByRole('group');
|
|
94
|
-
expect(region).toHaveAttribute('tabindex', '0');
|
|
95
97
|
expect(region).toHaveAttribute('aria-label', 'Code');
|
|
96
98
|
});
|
|
97
99
|
|
|
@@ -371,9 +373,9 @@ describe('CodeBlock', () => {
|
|
|
371
373
|
|
|
372
374
|
fireEvent.click(header);
|
|
373
375
|
expect(header).toHaveAttribute('aria-expanded', 'false');
|
|
374
|
-
// Collapsed: the wrapper is inert, so the
|
|
375
|
-
//
|
|
376
|
-
//
|
|
376
|
+
// Collapsed: the wrapper is inert, so the scroll container inside it
|
|
377
|
+
// drops out of the tab order and the accessibility tree instead of
|
|
378
|
+
// remaining an invisible tab stop while it overflows.
|
|
377
379
|
expect(region).toHaveAttribute('inert');
|
|
378
380
|
const scrollContainer = screen.getByRole('group');
|
|
379
381
|
expect(scrollContainer.closest('[inert]')).toBe(region);
|
|
@@ -399,12 +401,12 @@ describe('CodeBlock', () => {
|
|
|
399
401
|
expect(region).toHaveAttribute('inert');
|
|
400
402
|
fireEvent.click(header);
|
|
401
403
|
expect(header).toHaveAttribute('aria-expanded', 'true');
|
|
402
|
-
// Expanded again: inert is removed and the scroll container is a
|
|
403
|
-
//
|
|
404
|
+
// Expanded again: inert is removed and the scroll container is a named
|
|
405
|
+
// group once more (its tab stop follows overflow, see the browser spec).
|
|
404
406
|
expect(region).not.toHaveAttribute('inert');
|
|
405
407
|
const scrollContainer = screen.getByRole('group');
|
|
406
408
|
expect(scrollContainer.closest('[inert]')).toBeNull();
|
|
407
|
-
expect(scrollContainer).toHaveAttribute('
|
|
409
|
+
expect(scrollContainer).toHaveAttribute('data-scroll-axis', 'both');
|
|
408
410
|
});
|
|
409
411
|
|
|
410
412
|
it('applies a per-instance syntax theme via the syntaxTheme prop', () => {
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from '../theme/tokens.stylex';
|
|
35
35
|
import {mergeProps} from '../utils';
|
|
36
36
|
import {useClipboard} from '../hooks/useClipboard';
|
|
37
|
+
import {useScrollableArea} from '../hooks/useScrollableArea';
|
|
37
38
|
import {Icon} from '../Icon';
|
|
38
39
|
import {IconButton} from '../IconButton';
|
|
39
40
|
import {
|
|
@@ -117,6 +118,13 @@ const styles = stylex.create({
|
|
|
117
118
|
backgroundColor: 'var(--color-syntax-background)',
|
|
118
119
|
overflow: 'hidden',
|
|
119
120
|
},
|
|
121
|
+
// The viewport is a flex item of the column root. Its implied min-height
|
|
122
|
+
// would be its content while it fits (the shared capability keeps it
|
|
123
|
+
// `overflow: clip` then), so a root sized from outside could never make it
|
|
124
|
+
// shrink and scroll; 0 lets the root's bound reach it.
|
|
125
|
+
viewport: {
|
|
126
|
+
minHeight: 0,
|
|
127
|
+
},
|
|
120
128
|
headerRow: {
|
|
121
129
|
display: 'flex',
|
|
122
130
|
alignItems: 'center',
|
|
@@ -160,10 +168,6 @@ const styles = stylex.create({
|
|
|
160
168
|
margin: 0,
|
|
161
169
|
lineHeight: typeScaleVars['--text-supporting-leading'],
|
|
162
170
|
},
|
|
163
|
-
scrollContainer: {
|
|
164
|
-
overflowX: 'auto',
|
|
165
|
-
overflowY: 'auto',
|
|
166
|
-
},
|
|
167
171
|
codeWrapper: {
|
|
168
172
|
display: 'flex',
|
|
169
173
|
minWidth: 'fit-content',
|
|
@@ -800,7 +804,19 @@ export function CodeBlock({
|
|
|
800
804
|
// this is always a resolvable reference — aria-controls can be unconditional.
|
|
801
805
|
const regionId = useId();
|
|
802
806
|
|
|
803
|
-
|
|
807
|
+
// The code viewport enters the Tab order only while its content actually
|
|
808
|
+
// overflows (AST-025 FR12–FR13): the shared scroll capability measures
|
|
809
|
+
// both axes, keeps focus where it is when overflow goes away, and owns the
|
|
810
|
+
// overflow declarations. role="group" (not "region") so several code
|
|
811
|
+
// blocks on a page don't create duplicate same-named landmarks.
|
|
812
|
+
const {getViewportProps, getContentProps} = useScrollableArea({
|
|
813
|
+
axis: 'both',
|
|
814
|
+
keyboardAccess: {
|
|
815
|
+
owner: 'viewport',
|
|
816
|
+
role: 'group',
|
|
817
|
+
label: languageLabel ?? t('@astryx.codeBlock.code'),
|
|
818
|
+
},
|
|
819
|
+
});
|
|
804
820
|
const scrollStyle: CSSProperties | undefined =
|
|
805
821
|
maxHeight != null
|
|
806
822
|
? {
|
|
@@ -895,21 +911,16 @@ export function CodeBlock({
|
|
|
895
911
|
|
|
896
912
|
const codeBody = (
|
|
897
913
|
<div
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
// scroll long or wide code that overflows the viewport. Uses
|
|
901
|
-
// role="group" (not "region") so multiple code blocks on a page don't
|
|
902
|
-
// create duplicate same-named landmarks (axe: landmark-unique).
|
|
903
|
-
tabIndex={0}
|
|
904
|
-
role="group"
|
|
905
|
-
aria-label={languageLabel ?? t('@astryx.codeBlock.code')}
|
|
906
|
-
{...mergeProps(stylex.props(styles.scrollContainer), {
|
|
914
|
+
{...getViewportProps<HTMLDivElement>({
|
|
915
|
+
...stylex.props(styles.viewport),
|
|
907
916
|
style: scrollStyle,
|
|
908
917
|
})}>
|
|
909
918
|
<div
|
|
910
|
-
{...
|
|
911
|
-
|
|
912
|
-
|
|
919
|
+
{...getContentProps<HTMLDivElement>(
|
|
920
|
+
stylex.props(
|
|
921
|
+
styles.codeWrapper,
|
|
922
|
+
showHeader && !hasLineNumbers && styles.codeWrapperCompact,
|
|
923
|
+
),
|
|
913
924
|
)}>
|
|
914
925
|
{useSpans ? (
|
|
915
926
|
<SpanCodeContent
|
|
@@ -958,7 +969,7 @@ export function CodeBlock({
|
|
|
958
969
|
// While collapsed, the region is only hidden visually (0fr grid
|
|
959
970
|
// row); inert also removes it from the tab order and accessibility
|
|
960
971
|
// tree so keyboard users cannot Tab into the invisible scroll
|
|
961
|
-
// container (
|
|
972
|
+
// container (a tab stop while it overflows). aria-controls pointing at an inert
|
|
962
973
|
// element remains a valid, resolvable reference.
|
|
963
974
|
inert={isCollapsed ? true : undefined}
|
|
964
975
|
{...stylex.props(
|