@astryxdesign/core 0.7.0 → 0.7.1-canary.ff40b78

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.
Files changed (37) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts +1 -1
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheet.js +12 -2
  4. package/dist/ContextMenu/ContextMenu.d.ts +1 -1
  5. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  6. package/dist/ContextMenu/ContextMenu.js +22 -8
  7. package/dist/Dialog/Dialog.d.ts.map +1 -1
  8. package/dist/Dialog/Dialog.js +3 -26
  9. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  10. package/dist/DropdownMenu/DropdownMenu.js +10 -3
  11. package/dist/Typeahead/BaseTypeahead.d.ts +1 -1
  12. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  13. package/dist/Typeahead/BaseTypeahead.js +12 -3
  14. package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
  15. package/dist/hooks/useIsomorphicLayoutEffect.d.ts.map +1 -1
  16. package/dist/utils/focusReturn.d.ts +12 -8
  17. package/dist/utils/focusReturn.d.ts.map +1 -1
  18. package/dist/utils/focusReturn.js +44 -3
  19. package/package.json +4 -4
  20. package/src/BottomSheet/BottomSheet.test.tsx +114 -0
  21. package/src/BottomSheet/BottomSheet.tsx +12 -2
  22. package/src/BottomSheet/__tests__/BottomSheetFocusReturn.a11y.browser.spec.ts +96 -0
  23. package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +5 -6
  24. package/src/CodeBlock/CodeBlock.spec.md +55 -44
  25. package/src/ContextMenu/ContextMenu.spec.md +1 -1
  26. package/src/ContextMenu/ContextMenu.test.tsx +36 -3
  27. package/src/ContextMenu/ContextMenu.tsx +27 -11
  28. package/src/ContextMenu/__tests__/ContextMenuCursorAnchor.a11y.browser.spec.ts +153 -0
  29. package/src/Dialog/Dialog.tsx +2 -30
  30. package/src/DropdownMenu/DropdownMenu.test.tsx +97 -1
  31. package/src/DropdownMenu/DropdownMenu.tsx +10 -3
  32. package/src/DropdownMenu/__tests__/DropdownMenuFocusReturn.a11y.browser.spec.ts +73 -0
  33. package/src/PowerSearch/PowerSearch.test.tsx +45 -0
  34. package/src/Typeahead/BaseTypeahead.test.tsx +99 -1
  35. package/src/Typeahead/BaseTypeahead.tsx +17 -3
  36. package/src/utils/focusReturn.test.ts +79 -2
  37. package/src/utils/focusReturn.ts +48 -3
@@ -0,0 +1,96 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file BottomSheetFocusReturn.a11y.browser.spec.ts
5
+ * @input The `Core/DropdownMenu` action-sheet story with a trigger the caller
6
+ * renders, in a built Storybook, and real Chromium/WebKit pointer and
7
+ * keyboard input.
8
+ * @output Browser evidence that the focus a BottomSheet returns when it closes
9
+ * follows the last input (docs/architecture/interaction-modality.md INV1):
10
+ * no ring after a pointer close, the ring after a keyboard close.
11
+ * @position Cross-browser proof; jsdom only proves which focus call the sheet
12
+ * makes. The menu's sheet returns focus through BottomSheet's
13
+ * `finalFocusRef`, and a control the caller renders carries no style of its
14
+ * own that would hide a ring, so the ring shown is the browser's answer.
15
+ * Chromium rings it when a keyboard-opened sheet is closed by a pointer;
16
+ * WebKit also rings it after a tap open and a tap close.
17
+ */
18
+
19
+ import {expect, test, type Locator, type Page} from '@playwright/test';
20
+ import {
21
+ DEFAULT_STORYBOOK_DIR,
22
+ serveStorybook,
23
+ type StaticServer,
24
+ } from '@astryxdesign/a11y-spec/storybook';
25
+
26
+ let storybook: StaticServer;
27
+ test.beforeAll(async () => {
28
+ storybook = await serveStorybook(
29
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
30
+ );
31
+ });
32
+ test.afterAll(async () => {
33
+ await storybook?.close();
34
+ });
35
+
36
+ async function mount(page: Page): Promise<Locator> {
37
+ await page.goto(
38
+ `${storybook.origin}/iframe.html?id=core-dropdownmenu--action-sheet-custom-trigger&viewMode=story`,
39
+ );
40
+ const trigger = page.getByRole('button', {
41
+ name: 'Project actions',
42
+ exact: true,
43
+ });
44
+ await expect(trigger).toBeVisible();
45
+ return trigger;
46
+ }
47
+
48
+ async function openFromKeyboard(page: Page, trigger: Locator): Promise<void> {
49
+ await trigger.focus();
50
+ await page.keyboard.press('Enter');
51
+ await expect(page.getByRole('dialog')).toBeVisible();
52
+ }
53
+
54
+ function row(page: Page): Locator {
55
+ return page.getByRole('dialog').getByRole('button', {name: /Edit project/});
56
+ }
57
+
58
+ async function expectReturnedFocus(
59
+ page: Page,
60
+ trigger: Locator,
61
+ ring: boolean,
62
+ ): Promise<void> {
63
+ await expect(page.getByRole('dialog')).toBeHidden();
64
+ await expect(trigger).toBeFocused();
65
+ expect(
66
+ await trigger.evaluate(element => element.matches(':focus-visible')),
67
+ ).toBe(ring);
68
+ }
69
+
70
+ test('a keyboard-opened sheet closed by a pointer pick returns focus without a ring', async ({
71
+ page,
72
+ }) => {
73
+ const trigger = await mount(page);
74
+ await openFromKeyboard(page, trigger);
75
+ await row(page).click();
76
+ await expectReturnedFocus(page, trigger, false);
77
+ });
78
+
79
+ test('a pointer open and pointer pick return focus without a ring', async ({
80
+ page,
81
+ }) => {
82
+ const trigger = await mount(page);
83
+ await trigger.click();
84
+ await expect(page.getByRole('dialog')).toBeVisible();
85
+ await row(page).click();
86
+ await expectReturnedFocus(page, trigger, false);
87
+ });
88
+
89
+ test('a keyboard-opened sheet closed by Escape keeps the ring', async ({
90
+ page,
91
+ }) => {
92
+ const trigger = await mount(page);
93
+ await openFromKeyboard(page, trigger);
94
+ await page.keyboard.press('Escape');
95
+ await expectReturnedFocus(page, trigger, true);
96
+ });
@@ -15,12 +15,11 @@
15
15
  * output—not an event wrapper, speculative internal structure, or a node
16
16
  * created only for layout plumbing."
17
17
  *
18
- * This is not observable in the unit lane and not observable to the probe-reach
19
- * gate either. jsdom resolves no cascade, so it cannot say which element paints.
20
- * The gate's reach check asks whether a target's override lands on the element
21
- * carrying the class, and it does — that element simply is not the one a reader
22
- * sees. Only a real engine can be asked the question that matters: does styling
23
- * the documented target change the pill?
18
+ * This is not observable in the unit lane: jsdom resolves no cascade, so it
19
+ * cannot say which element paints. An override can land on the element carrying
20
+ * the class while that element is not the one a reader sees. Only a real engine
21
+ * can be asked the question that matters: does styling the documented target
22
+ * change the pill?
24
23
  *
25
24
  * The pill is identified structurally, as the button's parent element, so every
26
25
  * measurement below is independent of where the class currently sits.
@@ -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. The current implementation still gives every code viewport a keyboard stop even when it does not overflow, contrary to `spec:AST-025/FR12`; that retained implementation gap is not accepted here. |
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, draft) 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 draft, owner-approved, and an implementation gap, and Markdown's default path for `diff` fences changes with it — a breaking default change shipped in a minor (Compatibility and migration). The current implementation still gives every code viewport a keyboard stop even when it does not overflow, contrary to `spec:AST-025/FR12`; that retained implementation gap is not accepted here. |
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
 
@@ -56,10 +57,11 @@ Present read-only, standalone or multi-line code with optional syntax, navigatio
56
57
 
57
58
  ## Compatibility and migration
58
59
 
59
- - Released default preserved: yes
60
- - Compatibility class: compatible; no prop, export, type, default, target, or ref migration is required
60
+ - Released default preserved: yes for `CodeBlock`; Markdown's default Code block path changes for `diff` fences (below)
61
+ - Compatibility class: compatible for `CodeBlock`; no prop, export, type, default, target, or ref migration is required
61
62
  - Controlled/uncontrolled behavior: collapse state remains component-owned, starts expanded when applicable, and returns to expanded when its control disappears
62
- - Migration decision: none
63
+ - Migration decision: none for `CodeBlock`
64
+ - Default-path rendering change for diff fences (FR9), breaking, shipped in a minor: a Markdown `diff` fence that rendered as plain text renders as tinted diff rows with a `diff` label, and a `diff-<language>` fence — which renders today as plain text with a `diff` label and viewport name, its leading word being all Markdown reads — as the layer over that language with a `<language> diff` label; the header, copy, disclosure, and viewport name stay, and a supplied `code` renderer's contract is unchanged. The way back to plain text is a `text` fence or a custom `code` renderer. Every other fence is unchanged. This entry moves to the Markdown record when the layer is current.
63
65
 
64
66
  Consumer migration instructions belong in consumer docs and release notes.
65
67
 
@@ -78,33 +80,35 @@ Consumer migration instructions belong in consumer docs and release notes.
78
80
 
79
81
  ## Public concepts
80
82
 
81
- | Concept | Closed values or states | Meaning | Availability | Default | Owner | Stability | Invalid-value behavior |
82
- | ------------------ | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------- | ------------------- | --------- | --------------------------------------------------------------------- |
83
- | 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 |
84
- | 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 |
85
- | 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 |
86
- | 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 |
87
- | 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` |
88
- | 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 |
89
- | 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 |
90
- | 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 |
83
+ | Concept | Closed values or states | Meaning | Availability | Default | Owner | Stability | Invalid-value behavior |
84
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------- | ------------------- | --------- | --------------------------------------------------------------------- |
85
+ | 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 |
86
+ | 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 |
87
+ | 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 |
88
+ | 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) |
89
+ | 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 |
90
+ | 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` |
91
+ | 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 |
92
+ | 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 |
93
+ | 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
94
 
92
95
  ## Behavioral and layout contract
93
96
 
94
- | ID | Candidate invariant | Basis | Draft review state |
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. | released types, source, tests, consumer docs; established public interface | verify |
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. | shipped source/tests; `architecture:component-test-sufficiency/INV2–INV6`; WCAG name/status obligations | verify |
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. | shipped source/tests/docs; established public interface | verify |
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. | released public type/docs; `architecture:public-component-api/INV3,INV9`; objective JavaScript nullish semantics | verified by focused regression |
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. | shipped docs/source/tests; WAI-ARIA disclosure pattern and HTML `inert` semantics | verify |
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. | `architecture:component-theming-surface/INV3–INV7,INV12`; source/docs/tests | verify |
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. | `architecture:public-component-api/INV5,INV6,INV8`; shipped source | verify |
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. | `spec:AST-025/FR12–FR13` | settled by current authority; implementation gap |
97
+ | ID | Candidate invariant | Basis | Draft review state |
98
+ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
99
+ | 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 |
100
+ | 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 |
101
+ | 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 |
102
+ | 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 |
103
+ | 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 |
104
+ | 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 |
105
+ | 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 |
106
+ | 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; implementation gap |
107
+ | 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 default Code block path selects the layer from the fence's full first word: `diff` is the layer over plaintext, `diff-<language>` the layer over `<language>` (the language itself read by the same leading-word rule as any other fence). A supplied `code` renderer's contract is unchanged: it keeps receiving the leading word characters as `language` — `diff` for both `diff` and `diff-<language>` fences, as today — and nothing new. Every other info string is unchanged. Row paint MUST compose under `highlightLines` and line numbers without moving text, and 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. The shipped presets do not yet meet the floor on the default tints with the layer on — a known shortfall, tracked separately. | this record DEC-1 | draft; owner-approved 2026-10-12; implementation gap |
104
108
 
105
109
  ### Allowed variation
106
110
 
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.
111
+ - **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
112
  - **AV2 — Theme paint.** Current CodeBlock targets may vary their accepted visual properties without changing text, control semantics, or ownership.
109
113
  - **AV3 — Code content.** The product supplies arbitrary code text and the matching language identifier; CodeBlock does not interpret the program's meaning.
110
114
 
@@ -174,20 +178,27 @@ Exact CodeBlock proportions and syntax colors are not contract requirements; the
174
178
 
175
179
  ## Verification map
176
180
 
177
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
178
- | ----------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
179
- | 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` |
180
- | 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` |
181
- | 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` |
182
- | 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` |
183
- | 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` |
184
- | 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` |
185
- | 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` |
186
- | 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` |
181
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
182
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
183
+ | 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` |
184
+ | 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` |
185
+ | 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` |
186
+ | 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` |
187
+ | 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` |
188
+ | 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` |
189
+ | 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); `Markdown.test.tsx` (a `diff` fence → plaintext + layer, a `diff-tsx` fence → tsx + layer, a plain fence unchanged, a custom `code` renderer receives `language="diff"` for both diff fences and nothing new); 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 shipped presets' shortfall on the default tints is known, tracked separately) (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`; 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, a fence word handed to a custom renderer changed, 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` |
190
+ | 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` |
191
+ | 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
192
 
188
193
  ## Decision log
189
194
 
190
- None. Every candidate invariant projects released behavior, current shared authority, or an objective external standard.
195
+ ### DEC-1 — Diff is a line layer over any language, tinted by three defaulted syntax-theme slots
196
+
197
+ **Decider:** `cixzhang`, `2026-10-12`
198
+
199
+ 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 fence mapping lives here until the layer is current; the Markdown record then takes it over.
200
+
201
+ 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
202
 
192
203
  ## Open questions
193
204
 
@@ -146,7 +146,7 @@ the highlight is DOM focus per `module:DropdownMenu/useMenuPress` AR1.
146
146
  | Touch action row | Presents one data-driven touch action or drill-in entry using ListItem. | `component:List` | Prominent | FR1, FR3, FR4 |
147
147
 
148
148
  The `context-menu` target intentionally appears on both alternative painted
149
- menu surfaces. Trigger area content stays caller-owned and the zero-size cursor
149
+ menu surfaces. Trigger area content stays caller-owned and the 1px cursor
150
150
  anchor is positioning infrastructure rather than consumer anatomy.
151
151
 
152
152
  ### Theming anatomy
@@ -658,7 +658,7 @@ describe('ContextMenu compound mode', () => {
658
658
  });
659
659
 
660
660
  describe('cursor anchor positioning (#3465)', () => {
661
- // The menu is anchored to a zero-size element placed at the cursor point
661
+ // The menu is anchored to a 1px element placed at the cursor point
662
662
  // *inside the trigger*, so it is positioned relative to the trigger's
663
663
  // context (scroll-follow + auto-flip) rather than the viewport.
664
664
  function getCursorAnchor(trigger: HTMLElement): HTMLElement {
@@ -669,7 +669,7 @@ describe('ContextMenu compound mode', () => {
669
669
  return anchor;
670
670
  }
671
671
 
672
- it('renders a zero-size cursor anchor inside the trigger', () => {
672
+ it('renders a 1px cursor anchor inside the trigger', () => {
673
673
  render(
674
674
  <ContextMenu items={[{label: 'Item 1'}]} data-testid="ctx">
675
675
  <div>Right-click me</div>
@@ -680,6 +680,9 @@ describe('ContextMenu compound mode', () => {
680
680
  // Carries an anchor-name so the menu can be anchored to it via CSS
681
681
  // anchor positioning (context mode), not fixed viewport coordinates.
682
682
  expect(anchor.style.anchorName).toMatch(/^--astryx-layer-/);
683
+ // Owns a box: WebKit counts an empty anchor box inside a scroll
684
+ // container as clipped, and `position-visibility` hides the menu.
685
+ expect(anchor).toHaveStyle({width: '1px', height: '1px'});
683
686
  });
684
687
 
685
688
  it('places the cursor anchor at the pointer position relative to the trigger', () => {
@@ -725,7 +728,37 @@ describe('ContextMenu compound mode', () => {
725
728
  fireEvent.contextMenu(trigger, {clientX: 0, clientY: 0, detail: 0});
726
729
  const anchor = getCursorAnchor(trigger);
727
730
  expect(anchor.style.left).toBe('0px');
728
- expect(anchor.style.top).toBe('20px');
731
+ // The 1px anchor steps back into the trigger's last pixel row, so its
732
+ // bottom edge (where the menu starts) is still the trigger's bottom.
733
+ expect(anchor.style.top).toBe('19px');
734
+ });
735
+
736
+ it('keeps the anchor inside the trigger at its end edges, and nowhere else', () => {
737
+ render(
738
+ <ContextMenu items={[{label: 'Item 1'}]} data-testid="ctx">
739
+ <div>Right-click me</div>
740
+ </ContextMenu>,
741
+ );
742
+ const trigger = screen.getByTestId('ctx');
743
+ trigger.getBoundingClientRect = () =>
744
+ ({
745
+ left: 100,
746
+ top: 50,
747
+ right: 300,
748
+ bottom: 150,
749
+ width: 200,
750
+ height: 100,
751
+ }) as DOMRect;
752
+ const anchor = getCursorAnchor(trigger);
753
+ // A point on the trigger's end edges steps back by the anchor's pixel.
754
+ fireEvent.contextMenu(trigger, {clientX: 300, clientY: 150, detail: 1});
755
+ expect(anchor.style.left).toBe('199px');
756
+ expect(anchor.style.top).toBe('99px');
757
+ // A point past the trigger (content overflowing it) stays at the
758
+ // pointer, as does a point on the trigger's start edges.
759
+ fireEvent.contextMenu(trigger, {clientX: 320, clientY: 50, detail: 1});
760
+ expect(anchor.style.left).toBe('220px');
761
+ expect(anchor.style.top).toBe('0px');
729
762
  });
730
763
  });
731
764
  });