@astryxdesign/core 0.7.2 → 0.7.3-canary.b1e1936

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.
@@ -163,17 +163,17 @@ These requirements describe shipped behavior on current `main`.
163
163
 
164
164
  ### Representative states
165
165
 
166
- | State | Required invariant | Allowed variation |
167
- | ------------------------ | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
168
- | closed with no value | Label and placeholder identify the field | Consumer placeholder text |
169
- | closed with a value | Selected option is represented in the trigger | Custom `renderValue` content |
170
- | pointer / popover | Anchored surface exposes the listbox without modal-dialog semantics | Default or explicit placement |
171
- | compact coarse pointer | BottomSheet exposes a modal dialog containing the listbox | Default or explicit `adaptive`, or explicit `bottom-sheet` |
172
- | searching | Visible options, keyboard navigation, and announced result count use one filter | Consumer search and empty text |
173
- | loading | Trigger is busy; empty and no-results output is suppressed | Consumer loading duration |
174
- | disabled with reason | Trigger remains focusable enough to expose the reason while activation stays blocked | Consumer reason text |
175
- | read-only | Value stays focusable and submittable; menu, clear, and disclosure affordances are absent | Value rendering, status, and busy presentation |
176
- | selected/unselected rows | Empty marks consume no width; visible marks retain space at the logical edge | State-dependent label position/width and themed indicators |
166
+ | State | Required invariant | Allowed variation |
167
+ | ------------------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
168
+ | closed with no value | Label and placeholder identify the field | Consumer placeholder text |
169
+ | closed with a value | Selected option is represented in the trigger | Custom `renderValue` content |
170
+ | pointer / popover | Anchored surface exposes the listbox without modal-dialog semantics | Default or explicit placement |
171
+ | compact coarse pointer | BottomSheet exposes a modal dialog containing the listbox | Default or explicit `adaptive`, or explicit `bottom-sheet` |
172
+ | searching | Visible options, keyboard navigation, and announced result count use one filter | Consumer search and empty text |
173
+ | loading | Trigger is busy; empty and no-results output is suppressed | Consumer loading duration |
174
+ | disabled with reason | Trigger remains focusable (`spec:AST-085`) and exposes the reason; activation stays blocked | Consumer reason text |
175
+ | read-only | Value stays focusable and submittable; menu, clear, and disclosure affordances are absent | Value rendering, status, and busy presentation |
176
+ | selected/unselected rows | Empty marks consume no width; visible marks retain space at the logical edge | State-dependent label position/width and themed indicators |
177
177
 
178
178
  ### Transformation and precedence order
179
179
 
@@ -4809,6 +4809,22 @@ describe('Selector in a narrow row', () => {
4809
4809
  expect(root.getAttribute('style')).toContain('80');
4810
4810
  expect(getComputedStyle(root).minWidth).toBe('0');
4811
4811
  });
4812
+
4813
+ it('lets a narrow list shrink an option so its label can ellipsize', () => {
4814
+ render(
4815
+ <Selector
4816
+ label="Project"
4817
+ options={['An option with a deliberately long readable label']}
4818
+ value={undefined}
4819
+ onChange={() => {}}
4820
+ isDefaultOpen
4821
+ />,
4822
+ );
4823
+ const option = screen
4824
+ .getByRole('option', {hidden: true})
4825
+ .querySelector('.astryx-selector-option')!;
4826
+ expect(getComputedStyle(option).minWidth).toBe('0');
4827
+ });
4812
4828
  });
4813
4829
 
4814
4830
  describe('Selector and option actions (not adopted)', () => {
@@ -22,6 +22,10 @@ const embeddedStyles = stylex.create({
22
22
  paddingBlock: 0,
23
23
  paddingInline: 0,
24
24
  borderRadius: 0,
25
+ // The host row lays this out as a flex item, whose automatic minimum
26
+ // size is the full label width. Without the reset a long label runs
27
+ // past a narrow popover and is clipped instead of ellipsizing.
28
+ minWidth: 0,
25
29
  },
26
30
  });
27
31
 
@@ -134,8 +134,8 @@ value-tooltip behavior; AR1 records the keyboard model and its one correction.
134
134
  thumb as one composite control: every thumb is `role="slider"` with an
135
135
  accessible name (a range names each thumb), `aria-valuemin`, `aria-valuemax`,
136
136
  `aria-valuenow`, `aria-valuetext` when a formatter changes the spoken value,
137
- `aria-orientation`, and `aria-disabled` when disabled; an enabled thumb, and a
138
- disabled thumb that carries a disabled message, is a Tab stop.
137
+ `aria-orientation`, and `aria-disabled` when disabled; every thumb is a Tab
138
+ stop, enabled or disabled (`spec:AST-085`).
139
139
  ArrowUp and ArrowDown step by `step`; on a horizontal track the horizontal
140
140
  arrows follow the track's direction — ArrowRight increases and ArrowLeft
141
141
  decreases under LTR, and under RTL ArrowLeft increases and ArrowRight
@@ -225,15 +225,15 @@ separately listed value tooltip retains Tooltip's target.
225
225
 
226
226
  ## Verification map
227
227
 
228
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
229
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
230
- | FR1 | `Slider.test.tsx` structure, range, and marks suites | Single, range, horizontal, vertical, marks | Removing or misaligning stable parts breaks existing role, position, or mark assertions. | `audit:Slider/anatomy` |
231
- | AR1 | Slider accessibility contract (`internal/a11y-spec`, pending) bound to Slider, in jsdom and Chromium; `Slider.test.tsx` keyboard suites, including "follows the mirrored track under RTL: ArrowLeft increases, ArrowRight decreases, Up/Down unchanged" and "keeps ArrowLeft decreasing on a vertical track under RTL"; the Page, Home/End and Up/Down cases under the contract | Single and range; horizontal LTR and RTL; vertical; disabled; formatted value | A thumb loses its role, name or value properties; an arrow, Page, Home or End key stops stepping or steps against the mirrored track; a range thumb crosses the other | `audit:Slider/accessibility` |
232
- | FR2 | `Slider.test.tsx`, `themingTargets.test.ts`, generated probe theme, and Chromium | Root, horizontal/vertical control, disabled control, track, and thumb | A target class/state is missing, undocumented, or placed outside its owning anatomy. | `audit:Slider/theming` |
233
- | FR3 | Source and consumer-doc review | Filled range, marks, labels, adjacent text value | A missing target is inaccurately documented as present or intentionally permanent. | `audit:Slider/theming` |
234
- | FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason | Shared composition or its accessible association disappears; value-tooltip composition still lacks focused coverage. | `audit:Slider/anatomy` |
235
- | FR5 | Pending: `Slider.test.tsx` change-end cases driven by an owner that renders, clamps, or rejects the emitted value (not yet in the suite) | Single and range; pointer release before and after the owner re-renders; each keyboard step | `onChangeEnd` reports a value the owner did not render, fires more than once per interaction, or skips a completed keyboard step | `audit:Slider/accessibility` |
236
- | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and four current local targets | Missing, extra, prefixed, stale, or unclaimed mappings fail repository validation. | `audit:Slider/theming` |
228
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
229
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
230
+ | FR1 | `Slider.test.tsx` structure, range, and marks suites | Single, range, horizontal, vertical, marks | Removing or misaligning stable parts breaks existing role, position, or mark assertions. | `audit:Slider/anatomy` |
231
+ | AR1 | Slider accessibility contract (`internal/a11y-spec`, pending) bound to Slider; disabled-thumb Tab stop pending conformance; in jsdom and Chromium; `Slider.test.tsx` keyboard suites, including "follows the mirrored track under RTL: ArrowLeft increases, ArrowRight decreases, Up/Down unchanged" and "keeps ArrowLeft decreasing on a vertical track under RTL"; the Page, Home/End and Up/Down cases under the contract | Single and range; horizontal LTR and RTL; vertical; disabled; formatted value | A thumb loses its role, name or value properties; an arrow, Page, Home or End key stops stepping or steps against the mirrored track; a range thumb crosses the other | `audit:Slider/accessibility` |
232
+ | FR2 | `Slider.test.tsx`, `themingTargets.test.ts`, generated probe theme, and Chromium | Root, horizontal/vertical control, disabled control, track, and thumb | A target class/state is missing, undocumented, or placed outside its owning anatomy. | `audit:Slider/theming` |
233
+ | FR3 | Source and consumer-doc review | Filled range, marks, labels, adjacent text value | A missing target is inaccurately documented as present or intentionally permanent. | `audit:Slider/theming` |
234
+ | FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason | Shared composition or its accessible association disappears; value-tooltip composition still lacks focused coverage. | `audit:Slider/anatomy` |
235
+ | FR5 | Pending: `Slider.test.tsx` change-end cases driven by an owner that renders, clamps, or rejects the emitted value (not yet in the suite) | Single and range; pointer release before and after the owner re-renders; each keyboard step | `onChangeEnd` reports a value the owner did not render, fires more than once per interaction, or skips a completed keyboard step | `audit:Slider/accessibility` |
236
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and four current local targets | Missing, extra, prefixed, stale, or unclaimed mappings fail repository validation. | `audit:Slider/theming` |
237
237
 
238
238
  The focused Slider suite asserts the `slider-control` target's orientation and
239
239
  disabled state. Source/metadata guards cover all four target declarations. The
@@ -27,7 +27,9 @@ verified_by:
27
27
  modules:
28
28
  [
29
29
  module:Table/useTableActivation,
30
+ module:Table/useTableInfiniteScroll,
30
31
  module:Table/useTableRowStatus,
32
+ module:Table/useTableVirtualization,
31
33
  module:Table/useTableColumnResize,
32
34
  ]
33
35
  families: []
@@ -0,0 +1,244 @@
1
+ ---
2
+ schema_version: 3
3
+ template_version: 3
4
+ kind: module
5
+ id: module:Table/useTableInfiniteScroll
6
+ authority: draft
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: null
10
+ approved_at: null
11
+ owners: [cixzhang]
12
+ review_triggers:
13
+ [public-api, behavior, layout, theming, accessibility, interaction]
14
+ verified_by:
15
+ [
16
+ packages/core/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.test.tsx,
17
+ packages/core/src/Table/__tests__/TableInfiniteScroll.a11y.chromium.spec.ts,
18
+ packages/core/src/Table/Table.perf.test.tsx,
19
+ scripts/check-knowledge.mjs,
20
+ ]
21
+ parent_component: component:Table
22
+ references:
23
+ [
24
+ architecture:component-theming-surface,
25
+ architecture:interaction-modality,
26
+ architecture:public-component-api,
27
+ architecture:react-component-runtime,
28
+ spec:AST-002,
29
+ spec:AST-009,
30
+ spec:AST-020,
31
+ ]
32
+ ---
33
+
34
+ # useTableInfiniteScroll module contract
35
+
36
+ ## Contract at a glance
37
+
38
+ | Area | Contract |
39
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
40
+ | Public contract | Candidate `useTableInfiniteScroll` hook returning one `TablePlugin` from controlled `hasMore`, controlled `isLoading`, and `onLoadMore`; root, reset, fallback, and look-ahead inputs remain unadmitted until their owner decisions pass. |
41
+ | Behavior | One end sentinel requests the next batch as it approaches the relevant block-scroll viewport, never overlaps requests, and rearms when loading completes while more data remains. |
42
+ | End-user impact | People can continue through incrementally loaded rows without duplicate requests, focus disruption, inaccessible progress, or a dead end when automatic observation is unavailable. |
43
+ | Builder impact | Builders own data, request state, errors, and appended rows. The plugin owns observation, duplicate suppression, progress/fallback presentation, and cleanup. |
44
+ | Compatibility/readiness | Additive, unreleased module. This draft is not implementation authority. Scroll-root ownership, request completion/reset, parent rendering-mode seam, fallback policy, loading/error composition, customization, and announcement evidence require owner decisions before promotion. |
45
+ | Review checks | Reject viewport assumptions, overlapping loads, stale callbacks, a permanently visible sentinel loop, unexplained focusable sentinels, progress announced on every observer tick, a mouse-only fallback, invalid table DOM, retained observers, or router/data-client dependencies. |
46
+ | Governing rules | `component:Table` FR9-FR12 and ORD1-ORD4 own plugin composition; `architecture:react-component-runtime` owns observer lifetime; `architecture:public-component-api` and `spec:AST-002` own public caller burden. |
47
+
48
+ This table is a review projection; the body below is authoritative.
49
+
50
+ ## Intent
51
+
52
+ `useTableInfiniteScroll` connects incremental data loading to a data-driven Table
53
+ without making Table own a router, cache, query client, cursor format, or product data.
54
+ It owns an end sentinel, observation lifecycle, one-request-at-a-time behavior,
55
+ rearming, accessible progress, and a keyboard-operable fallback.
56
+
57
+ Infinite scroll is distinct from client pagination and virtualization. It appends data;
58
+ it does not choose the caller's page/cursor protocol or bound the number of rendered rows.
59
+
60
+ ## Compatibility and migration
61
+
62
+ - Released default preserved: `yes`; Table behavior remains unchanged unless the new plugin is supplied.
63
+ - Compatibility class: additive public hook, types, documentation, and examples.
64
+ - Migration decision: `module:Table/useTableInfiniteScroll/DEC-1` after the open ownership decisions are approved.
65
+
66
+ Consumer migration instructions belong in consumer docs and release notes.
67
+
68
+ ## Ownership boundary
69
+
70
+ **Owns**
71
+
72
+ - The public hook, configuration, sentinel, observer lifecycle, and loading trigger boundary.
73
+ - Duplicate-request suppression, rearming after completion, and short-batch behavior.
74
+ - Default progress and manual fallback behavior, accessibility, performance, and evidence.
75
+ - Composition with Table's scroll wrapper, empty state, loading state, appended rows, other plugins, and the candidate children-mode warning/no-op, blocked on OQ8's parent contract decision.
76
+
77
+ **Does not own / non-goals**
78
+
79
+ - Aggregate `TablePlugin` protocol, transform order, or scroll-wrapper implementation — owned by `component:Table`.
80
+ - Product data, request/cursor/page state, cache policy, retry policy, or error content — owned by the caller or another module.
81
+ - Client pagination, server pagination, Relay integration, row virtualization, or column virtualization.
82
+ - Scroll restoration, URL state, routing, or prefetching.
83
+ - A new public Table theme target unless a separate approved decision establishes a distinct theme-author need.
84
+ - Caller-composed children-mode semantics or the parent rendering-mode contract. The candidate warning/no-op depends on OQ8; the module never interprets caller children or infers mode from DOM to append loading UI.
85
+
86
+ ## Public API and concepts
87
+
88
+ The candidate minimum shape is:
89
+
90
+ ```ts
91
+ export interface UseTableInfiniteScrollConfig {
92
+ hasMore: boolean;
93
+ isLoading: boolean;
94
+ onLoadMore: () => void | Promise<void>;
95
+ }
96
+
97
+ export function useTableInfiniteScroll<T extends Record<string, unknown>>(
98
+ config: UseTableInfiniteScrollConfig,
99
+ ): TablePlugin<T>;
100
+ ```
101
+
102
+ The final shape is blocked on OQ1-OQ8. Consumer docs own final signatures,
103
+ defaults, imports, and examples after promotion.
104
+
105
+ | Concept | Closed values or states | Meaning | Default | Owner | Stability |
106
+ | --------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------ | ------------------------------------- | --------- |
107
+ | More-data state | `hasMore` true or false | Whether another load may be requested. | Controlled | Caller through this module | Candidate |
108
+ | Loading state | `isLoading` true or false | Whether a request is already in flight. | Controlled | Caller through this module | Candidate |
109
+ | Load request | `void` or promise-returning callback | Starts one caller-owned append operation without prescribing its data protocol. | No request while blocked | Caller through this module | Unsettled |
110
+ | Look-ahead | Optional non-negative distance, if admitted | Starts loading before the sentinel reaches the visible block-end edge. | Not yet admitted | Caller/module boundary | Unsettled |
111
+ | Sentinel | Observed, loading, exhausted, unavailable | Coordinates automatic loading without becoming unexplained table data or an accidental tab stop. | Derived | `module:Table/useTableInfiniteScroll` | Candidate |
112
+ | Manual fallback | Available or hidden | Provides an explicit load command when automatic observation is unavailable or unsuitable. | Unsettled | `module:Table/useTableInfiniteScroll` | Unsettled |
113
+
114
+ ## Behavioral contract
115
+
116
+ | ID | Candidate invariant | Basis | Draft review state |
117
+ | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------- |
118
+ | FR1 | Stable configuration callbacks and unchanged primitive inputs preserve one stable `TablePlugin`. | Parent plugin identity and runtime architecture | Settled candidate |
119
+ | FR2 | The plugin contributes one end sentinel after the semantic `<table>`, inside the Table scroll-wrapper content. It never inserts non-row content inside `<table>`, `<thead>`, `<tbody>`, or `<tr>`. | Valid HTML and parent wrapper contract | Settled candidate |
120
+ | FR3 | Automatic loading is armed only when `hasMore` is true and `isLoading` is false. One visibility episode produces at most one active request. | Duplicate suppression | Settled candidate |
121
+ | FR4 | While a load is in flight, the module does not call `onLoadMore` again, including through repeated observation, manual activation, or rerender. Completion can make another call eligible only for the current configuration, subject to the controlled inputs and FR14. The caller remains solely responsible for suppressing stale product-data commits. | Controlled state and async ownership | Callback completion contract unsettled |
122
+ | FR5 | When a short append leaves the sentinel visible and `hasMore` remains true, completing the prior load can request the next batch without requiring the user to scroll away and back. | Continuous loading behavior | Settled outcome; mechanism free |
123
+ | FR6 | `hasMore=false` disconnects observation, removes the manual command, and presents no loading state. Changing back to true can arm a new request. | Exhausted-state boundary | Settled candidate |
124
+ | FR7 | An observer-unavailable environment preserves server rendering/hydration and exposes the approved manual fallback rather than throwing or silently dead-ending the user. | Progressive enhancement | Fallback policy unsettled |
125
+ | FR8 | Loading progress is shown and announced at most once per state transition, not once per observer callback. Error and retry UI remain caller-owned and do not render duplicate progress chrome. | Accessibility and module boundary | Composition surface unsettled |
126
+ | FR9 | The module does not call another plugin's callbacks, mutate selection/activation/expansion/sort/filter state, replace product rows, or reset scroll position. Preservation of caller-controlled state follows from that non-interference; caller data updates remain caller-owned. | Plugin independence | Settled candidate |
127
+ | FR10 | Empty data may still load when `hasMore=true`; default EmptyState and load affordance do not obscure each other or communicate contradictory states. | Empty/loading composition | Human layout decision |
128
+ | FR11 | Approved scroll-root or look-ahead changes reconnect observation without leaking the old observer or triggering from a detached sentinel. Unmount and node replacement release every observer and retained node. | `architecture:react-component-runtime` | Settled candidate |
129
+ | FR12 | The plugin remains data-protocol agnostic. `onLoadMore` receives no Table-owned page, cursor, Relay, router, cache, or request-client object. | Public ownership boundary | Settled candidate |
130
+ | FR13 | In children mode the module is a no-op and emits at most one deduplicated development warning. This candidate outcome depends on OQ8's missing parent contract; it does not establish a rendering-mode signal on the current Table wrapper transform or permit mode inference from DOM. | Missing `component:Table` dependency, OQ8 | Hold: parent contract decision required |
131
+ | FR14 | Reconfiguration during a load follows one approved caller-visible reset or React remount boundary. Settlement from a load started before that boundary neither calls `onLoadMore`, makes a call eligible, nor changes loading presentation for the current configuration. Caller response ownership remains unchanged. | Caller-visible reset and stale-completion safety | Human API decision |
132
+
133
+ ### Transformation and precedence order
134
+
135
+ - **ORD1 — Exhausted/loading gate.** Resolve `hasMore` and `isLoading` before observing or exposing a manual command.
136
+ - **ORD2 — Current configuration.** Calls use the current `onLoadMore` and respect the approved reset/remount boundary; changing callback identity alone does not end an in-flight load or silently redefine product-data ownership.
137
+ - **ORD3 — One load at a time.** From invocation of `onLoadMore` until the approved completion condition, no observation or manual activation invokes it again, including reentrant activation from caller code. Synchronous throws and asynchronous settlement follow OQ2's completion rule.
138
+ - **ORD4 — Completion after reset.** FR14 excludes settlement from before a reset/remount from invoking or enabling a call or changing current loading presentation. Completion of a current-configuration load follows OQ2/OQ5 and the controlled inputs; the plugin never commits or rejects caller data.
139
+ - **ORD5 — Rearm.** Controlled completion and continued `hasMore` rearm the current sentinel, including when it remains visible.
140
+ - **ORD6 — Presentation.** Exhausted, loading, automatic, fallback, and error-adjacent states render one coherent after-table surface.
141
+ - **ORD7 — Parent pipeline.** In data mode the module appends to accumulated `afterTable` content and preserves prior wrapper props, styles, refs, and chrome; children mode returns prior props unchanged, subject to OQ8's parent contract dependency.
142
+
143
+ ### Performance and resources
144
+
145
+ - **PR1 — One observer.** At most one `IntersectionObserver` and one observed sentinel exist per mounted plugin instance.
146
+ - **PR2 — No scroll listener.** Native observation, not a per-frame or per-scroll JavaScript listener, drives automatic loading.
147
+ - **PR3 — Stable resources.** Callback updates do not recreate observation when the approved root/look-ahead geometry and sentinel are unchanged.
148
+ - **PR4 — Complete cleanup.** Unmount, sentinel replacement, root replacement, exhaustion, and loading transitions release obsolete observation.
149
+ - **PR5 — Constant callback work.** An observer entry performs constant coordination work and never scans rows or columns.
150
+
151
+ ## Accessibility contract
152
+
153
+ - **AR1 — Valid structure.** Sentinel, progress, and fallback UI stay outside the semantic table and do not appear as data rows or cells (WCAG 2.2 SC 1.3.1; DOM/accessibility-tree and browser evidence).
154
+ - **AR2 — Progress announcement.** Loading transitions use a non-interruptive status that does not repeat for observer churn or every appended row (WCAG 2.2 SC 4.1.3). Deterministic transition tests prove event count/order, and the approved implementation requires NVDA + Chrome on Windows and VoiceOver + Safari on macOS to prove spoken announcement timing under `spec:AST-009` FR4 and FR12.
155
+ - **AR3 — Operable fallback.** The approved fallback is a real keyboard-operable control with a localized name and visible focus; the sentinel itself is not an unexplained tab stop (WCAG 2.2 SC 2.1.1, 2.4.7, and 4.1.2; DOM/axe/real-browser evidence).
156
+ - **AR4 — Focus stability.** Appending rows does not move focus, reset focused descendants, or make the fallback disappear while it owns focus without a deterministic successor (WCAG 2.2 SC 2.4.3 and 2.4.7; real-browser keyboard evidence).
157
+ - **AR5 — Reduced motion.** Progress presentation does not require motion to communicate loading state and respects platform motion settings; real-browser visual evidence proves the supported states.
158
+ - **AR6 — Touch and zoom.** Automatic loading and fallback remain available without hover, precision pointing, or a fixed viewport size (WCAG 2.2 SC 1.4.10 and 2.1.1; responsive/touch real-browser evidence).
159
+ - **AR7 — Evidence boundaries.** Real AT evidence is limited to AR2's announcement claim or any later approved AT-dependent outcome; it supplements rather than replaces deterministic state, DOM, keyboard, and browser coverage.
160
+
161
+ ## Design relationships
162
+
163
+ | Anatomy or state | Design requirement | Representation authority | Module contract |
164
+ | ------------------- | ------------------------------------------------------------------------------------- | ------------------------- | ----------------- |
165
+ | End sentinel | Supplies observation geometry without masquerading as table data or a control. | This module | FR2-FR7, AR1 |
166
+ | Loading progress | Communicates one active append operation without duplicating caller-owned chrome. | Unsettled pending OQ4 | FR3-FR8, AR2, AR5 |
167
+ | Manual load command | Provides an explicit fallback with normal control semantics and focus treatment. | Unsettled pending OQ3/OQ4 | FR7-FR10, AR3-AR4 |
168
+ | Exhausted state | Stops requesting and removes load affordances without adding an unsolicited message. | This module | FR6, ORD1, ORD5 |
169
+ | Error/retry | Remains outside the module unless a later loading/error composition contract owns it. | Caller or another module | FR8, non-goal |
170
+
171
+ The module adds no direct theme target. OQ3/OQ4 must select any progress/fallback component and then name its exact existing target or record a classified `none`; no delegation is promised yet. Sentinel geometry is implementation detail rather than theme API.
172
+
173
+ ## Parent and system relationships
174
+
175
+ - `component:Table` owns the scroll-wrapper phase and accumulated `afterTable` slot. **Missing parent dependency (OQ8):** its current contract does not establish a data/children mode signal at that phase. The module remains on hold until the parent contract records the approved mode distinction or makes the invalid combination unrepresentable; FR13 then cites that owning claim. This draft neither supplies a field nor approves a parent API change.
176
+ - `architecture:react-component-runtime` owns observer acquisition, current-value synchronization, node replacement, StrictMode replay, and cleanup boundaries.
177
+ - `architecture:public-component-api` and `spec:AST-002` require a protocol-agnostic API whose inputs express caller-owned state rather than implementation clients.
178
+ - `spec:AST-009` and `spec:AST-020` own announcement evidence and the WCAG/evidence-layer boundary; AR2 is the only current real-AT-dependent claim.
179
+ - Pagination and virtualization remain separate modules. Infinite loading can compose with them only where their current contracts explicitly permit it.
180
+
181
+ ## Verification map
182
+
183
+ | Contract | Verification | Representative states | Mutation or failure expectation |
184
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
185
+ | FR1, FR3-FR7, FR14, ORD1-ORD5 | Deterministic observer events, callback count/order assertions, and controlled rerenders | Visible/hidden, loading, exhausted, short batch, reentrant activation, throw, async settle, reset/remount | Calls overlap, pre-reset completion invokes or enables a call or changes current loading presentation, short batches dead-end, observation throws, or rearm fails. |
186
+ | FR2, FR8-FR10, FR12-FR13, ORD6-ORD7 | Table render and plugin-composition tests against the parent rendering-mode contract once OQ8 is resolved | Data/children mode, empty/non-empty, wrapper chrome, loading, exhausted, caller error UI | Invalid DOM appears, children mode renders a sentinel, warning repeats, prior chrome is lost, caller state is mutated, or states contradict; absent parent authority keeps OQ8 on hold. |
187
+ | FR11, PR1-PR5 | Observer lifecycle tests under StrictMode, root/sentinel/look-ahead change, unmount, and no-op rerender | Every approved acquisition, replacement, and release path | More than one observer survives, detached nodes remain observed, or stable inputs churn resources. |
188
+ | AR1, AR3-AR7 | DOM/accessibility-tree assertions, axe, and real-browser keyboard/touch/zoom/reduced-motion/focus flows | Automatic, fallback, loading, append, exhaustion, observer unavailable | Sentinel becomes a row/tab stop, focus is lost, fallback is inoperable, or responsive/motion behavior carries required meaning. |
189
+ | AR2 | Deterministic status-transition count/order tests plus NVDA + Chrome on Windows and VoiceOver + Safari on macOS | Initial load, repeated observer entries, short batches, completion, exhaustion | Status repeats, arrives out of order, is skipped, or announces observer churn/appended rows instead of loading transitions. |
190
+ | Public API and docs | Public-subpath compile fixture, `.doc.mjs`, generated inventory, Storybook, and one generic async-cache recipe | Controlled state, promise/void callback, approved reset/root/fallback/look-ahead inputs | API leaks a client, exposes look-ahead without admission, cannot express the approved reset boundary, promises unsettled behavior, or permits contradictions. |
191
+ | Performance | Extend Table performance coverage with stable no-op, observer callback, and repeated append budgets | Large representative data and repeated batches | Stable rerenders recreate resources, callbacks scan rows, or appends exceed recorded budgets. |
192
+ | Structural ownership | `pnpm check:knowledge` | Draft module and parent backlink | A missing, orphaned, misnamed, wrong-kind, or duplicate module record passes validation. |
193
+
194
+ ## Decision log
195
+
196
+ ### DEC-1 — Infinite scroll owns loading coordination, not data
197
+
198
+ **Reference:** `module:Table/useTableInfiniteScroll/DEC-1`
199
+ **Decider:** pending owner approval
200
+
201
+ The module observes the end of rendered Table content and asks the caller for another batch. It owns at-most-one-in-flight loading, callback eligibility after completion/reset, rearming, and presentation under FR4/FR14 and ORD2-ORD5, not an internal coordination mechanism. The caller remains the sole owner of rows, request protocol, cache, cursor/page, stale-response suppression, errors, and retry policy.
202
+
203
+ Rejected: importing a router/query client, mutating Table data, or combining cursor,
204
+ server-pagination, Relay, and infinite-loading contracts.
205
+
206
+ ### DEC-2 — The sentinel is after the table
207
+
208
+ **Reference:** `module:Table/useTableInfiniteScroll/DEC-2`
209
+ **Decider:** pending owner approval
210
+
211
+ Observation and progress UI use the parent scroll wrapper's `afterTable` slot. They never
212
+ insert non-row elements into semantic table structure or synthesize a fake data row.
213
+
214
+ Rejected: a `<div>` inside `<tbody>`, a sentinel `<tr>` announced as data, and a focusable
215
+ sentinel with no user command.
216
+
217
+ ### DEC-3 — Automatic loading requires an operable fallback
218
+
219
+ **Reference:** `module:Table/useTableInfiniteScroll/DEC-3`
220
+ **Decider:** pending owner approval
221
+
222
+ Automatic observation is progressive enhancement. An approved fallback prevents a dead
223
+ end when observation is unsupported, disabled, or unsuitable and gives keyboard users an
224
+ explicit command without making the sentinel itself interactive.
225
+
226
+ Rejected: silently doing nothing without IntersectionObserver and making automatic loading
227
+ the only path.
228
+
229
+ ## Open questions
230
+
231
+ - **OQ1 — Who owns the block-scroll root?** (`human-api`) Decide viewport default versus an explicit caller root/ref, and how Table nested in a vertical `ScrollableArea` identifies the correct root without brittle ancestor discovery.
232
+ - **OQ2 — What observable event completes a load?** (`human-api`) Decide whether controlled `isLoading`, a returned promise, or both permit another `onLoadMore` call after synchronous throw and asynchronous settlement.
233
+ - **OQ3 — When is manual fallback shown?** (`human-design`) Decide always-visible, observer-unavailable-only, user-selected, or another policy; define focus behavior when loading/exhaustion removes it.
234
+ - **OQ4 — What loading customization is public?** (`human-api`) Decide whether the module renders one fixed default, accepts `renderLoadingIndicator`, composes an existing loading-state module, or stays behavior-only while still meeting progress requirements.
235
+ - **OQ5 — How do error and retry compose?** (`human-api`) Keep error content caller-owned, but define how a failed request rearms automatic loading and avoids immediate retry loops or duplicate loading/error chrome.
236
+ - **OQ6 — Is caller-configurable look-ahead admitted?** (`human-api`) First prove a non-derivable caller distinction under `spec:AST-002`; if admitted, choose an intent-named distance API with explicit units/default rather than reusing browser `threshold` terminology, which conventionally means an intersection ratio. Keep it out of the candidate interface until then.
237
+ - **OQ7 — What is the caller-visible reset boundary?** (`human-api`) Decide an explicit caller-owned reset identity versus a documented React remount/key boundary when dataset/query configuration changes during a load. Pre-reset settlement neither invokes nor enables a call nor changes loading presentation for the current configuration; stale product-data suppression remains the caller's job.
238
+ - **OQ8 — Which parent contract distinguishes data from children mode?** (`human-api`) Decide a parent-owned rendering-mode signal at the wrapper transform versus making the invalid combination unrepresentable, preserving FR13's warning/no-op outcome for children mode. The chosen behavior belongs in `component:Table`, and FR13 must cite its approved owning claim before this module is promoted. This missing dependency remains on hold; no parent field or approval is assumed.
239
+
240
+ ## Content boundary
241
+
242
+ This record does not duplicate consumer signatures/examples, the parent Table wrapper
243
+ protocol, general observer mechanics, current usage audits, implementation steps, or
244
+ pagination/virtualization/data-client contracts. It links to their canonical owners.