From d7127884b5dfcd159d1904eff3e009000334ace9 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 24 Oct 2026 10:31:17 -0700 Subject: [PATCH 1/4] spec(AST-012): compact is width below md or height below 500, the same on every platform AST-012 FR9 and DEC-6: one compact definition the theme exports, with the same numbers in CSS px, iOS points, and Android dp, and no mapping to platform size classes; every presentation resolver uses it. FR2 adds the height condition {below: 500}. architecture:public-component-api INV17 makes components use it, and Selector FR4 names it. --- docs/architecture/public-component-api.md | 11 +++- docs/specs/AST-012/spec.md | 59 ++++++++++++++++----- packages/core/src/Selector/Selector.spec.md | 6 +-- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/docs/architecture/public-component-api.md b/docs/architecture/public-component-api.md index f914c0d204e41..219aea278f326 100644 --- a/docs/architecture/public-component-api.md +++ b/docs/architecture/public-component-api.md @@ -32,6 +32,7 @@ deciding_specs: spec:AST-012/DEC-2, spec:AST-071/DEC-1, spec:AST-017/DEC-23, + spec:AST-012/DEC-6, ] --- @@ -165,6 +166,11 @@ guidance owns the process used to propose and test APIs. When a touch default changes a released component's behavior, that `presentation` value is the restoring prop `spec:AST-017` FR3 names (`spec:AST-017/DEC-23`). +- **INV17 — Compact has one shared definition.** A component that chooses a + presentation by screen size uses the theme's compact definition: width below + the effective `md` point or height below 500, with the same numbers on every + platform (`spec:AST-012` FR9). It keeps no compact numbers of its own, as + INV14 keeps no parallel breakpoint table. This record applies to stable public packages. Lab components are not stable public promises until promotion. @@ -199,7 +205,8 @@ public promises until promotion. evidence required by the release process. - A component exposing a named theme width point uses the fixed vocabulary and exclusive upper-edge behavior from `spec:AST-012`; it does not maintain a - parallel hardcoded breakpoint table. + parallel hardcoded breakpoint table. A component that chooses a presentation + by screen size uses the compact definition (INV17). - Changes to a family-owned API update the family contract rather than copying its rule into this record or every component. - A component that accepts or derives a navigation destination follows @@ -227,6 +234,7 @@ public promises until promotion. - `spec:AST-017/DEC-23` — a touch presentation or open-surface touch mode is a default the caller can always override, through one `presentation` prop that also restores the previous surface and mode. +- `spec:AST-012/DEC-6` — compact is one definition, the same on every platform. Transition Action sequencing and pending behavior belong to their component or family contract. This record links that owner once it is current; it does not @@ -246,6 +254,7 @@ copy the component matrix. | INV14 | `AppShell.test.tsx` | A component hardcodes a divergent point or treats equality as below | | INV15 | Public-API review of the generated declaration inventory | A part or StyleX-typed styling prop ships without class-name and inline-style counterparts on the same element | | INV16 | Public-API review of touch presentations plus coarse- and fine-pointer runtime tests | A stable component that presents a different surface or switches an open surface's input mode under a coarse pointer and has no `presentation` value that keeps its fine-pointer surface and mode; such a touch mode callers cannot reach; a default that overrides an explicit caller choice | +| INV17 | Resolver tests at the compact edges plus public-API review | A component that chooses a presentation by screen size with its own numbers, or disagrees with the theme compact definition at width 768 or height 500 | | Consumer-doc projection | `docPropReferences.test.ts` and `docPropLiterals.test.ts` | Docs name a nonexistent prop or omit public literal choices | Known verification gaps: diff --git a/docs/specs/AST-012/spec.md b/docs/specs/AST-012/spec.md index f527988f07dfe..0e113f065ed27 100644 --- a/docs/specs/AST-012/spec.md +++ b/docs/specs/AST-012/spec.md @@ -7,7 +7,7 @@ authority: current archive_reason: null superseded_by: null approved_by: cixzhang -approved_at: 2026-09-02 +approved_at: 2026-10-24 phase: accepted owners: [cixzhang, rubyycheung, imdreamrunner] affects_architecture: @@ -98,11 +98,13 @@ A breakpoint map alone emits no CSS. Only rules with adaptation values do. base below `sm`; there is no configurable `xs: 0` point. Defining or overriding the map alone MUST NOT emit CSS. - **FR2 — Conditions are named, closed, and composable.** A `when` object may - contain `width`, `pointer`, `contrast`, and `motion`; fields are ANDed. + contain `width`, `height`, `pointer`, `contrast`, and `motion`; fields are + ANDed. `width` accepts `from` and `below` using the five fixed names and MUST contain one or both. `from` is inclusive, `below` is exclusive, either edge may be omitted but not both, and a bounded width MUST resolve to `from < below`. - `pointer` accepts `coarse | fine`; `contrast` accepts + `height` accepts only `{below: 500}`: a viewport height below 500, exclusive, + the height edge of the FR9 compact definition. `pointer` accepts `coarse | fine`; `contrast` accepts `more | less | no-preference`; `motion` accepts `reduce | no-preference`. Future environment fields require explicit additions to this closed vocabulary. - **FR3 — Rules are named objects.** `adaptations.rules` contains @@ -161,6 +163,16 @@ A breakpoint map alone emits no CSS. Only rules with adaptation values do. `architecture:theme-application`, and with no active theme it uses the FR1 defaults. `defaultIsMobile` remains an SSR hint; the client query uses the effective map after hydration. +- **FR9 — Compact has one definition on every platform.** A screen is compact + while its viewport width is below the effective `md` point (FR1) or its + viewport height is below 500. The numbers are the same on every platform: CSS + pixels on the web, points on iOS, and density-independent pixels on Android; + native presentations do not map compact to platform size classes. The theme + exports this one definition, and every resolver that chooses a presentation + by screen size uses it, including the web touch presentation resolver, + Selector's `adaptive` presentation, and `@astryxdesign/native`; none keeps + size numbers of its own. A resolver that also needs a coarse pointer ANDs + that with compact. - **IR1 — Normalize and reject atomically.** One path validates width breakpoints, named condition fields, width ranges, theme extension, enrolled local-token names, and concrete expanded values for both `defineTheme` and @@ -197,8 +209,8 @@ A breakpoint map alone emits no CSS. Only rules with adaptation values do. ### Platform support -- Adaptations use viewport-width, primary-pointer, contrast-preference, and - reduced-motion queries within Astryx's browser support floor; there is no +- Adaptations use viewport-width, viewport-height, primary-pointer, + contrast-preference, and reduced-motion queries within Astryx's browser support floor; there is no JavaScript styling fallback. - Real Chromium MUST verify conditions, nesting, AppShell agreement, and first paint. Real Safari MUST verify exclusive width edges and primary-pointer @@ -223,15 +235,25 @@ implemented. Its value resolver, explicit-value preservation, CSS generation/build parity, and extension metadata remain useful implementation evidence. +The theme exports no compact definition, and `when` has no `height` field. +Selector's `adaptive` presentation and DropdownMenu's compact-touch decisions use +`(max-width: 768px) and (pointer: coarse)`: a coarse-pointer screen exactly 768 +pixels wide counts as compact, and a landscape phone at least 768 pixels wide +but under 500 tall does not. Under FR9 the first opens the popover and the +second the bottom sheet; the release note for that change names `presentation` +as the way to keep a surface. `@astryxdesign/native` has no compact resolver +yet; it adopts FR9 when it gains one. + ## Verification -| Contract | Verification | Representative states | Failure signal | -| -------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| FR1–FR3 | Width-breakpoint and condition matrix plus browser boundaries | defaults/overrides; from/below/range; pointer/contrast/motion; multi-field AND; empty/no rules | A point acts as an upper bound, intervals gap/overlap, fields OR together, or an unused map emits CSS. | -| FR4–FR7 | Shared resolver, surface, cascade-order, and extension tests | enrolled/unenrolled local tokens; incomplete scales; finite built-ins; open primitive values absent from root; root-enrolled and rule-only custom values; unresolved shared results; later generated vs earlier explicit leaves; surface collisions | A rule enrolls a local or component value, a valid open value requires a dummy root rule, adaptation validation diverges from root validation, an unresolved result becomes a permanent exemption, or inherited order is lost. | -| FR8 | Theme/AppShell integration | all names/`none`; SSR hint; no/nearest/root/nested theme; exact boundaries | AppShell uses the wrong map, equality, or scope. | -| IR1–IR3 | Runtime/build and CLI positive/negative fixtures | invalid map/range/field; source axis metadata; duplicate `when`; root-restoring rules; finite/open/unresolved component domains; declaration targets; root-enrolled and rule-only custom values; non-CSS token reads | Invalid input writes output, root and adaptation component validation disagree, an independently valid value is forced into root, an adaptation conditionally enrolls a custom value, or tooling misses a rule-only value. | -| IR4 | Generated-module import and size tests | no rules/rules; source/built parent and child; generative/local-token metadata | Breakpoints, rules, order, or metadata disappear; extension diverges; CSS text enters the module; or resolved layers duplicate. | +| Contract | Verification | Representative states | Failure signal | +| ----------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| FR1–FR3 | Width-breakpoint and condition matrix plus browser boundaries | defaults/overrides; from/below/range; pointer/contrast/motion; multi-field AND; empty/no rules | A point acts as an upper bound, intervals gap/overlap, fields OR together, or an unused map emits CSS. | +| FR4–FR7 | Shared resolver, surface, cascade-order, and extension tests | enrolled/unenrolled local tokens; incomplete scales; finite built-ins; open primitive values absent from root; root-enrolled and rule-only custom values; unresolved shared results; later generated vs earlier explicit leaves; surface collisions | A rule enrolls a local or component value, a valid open value requires a dummy root rule, adaptation validation diverges from root validation, an unresolved result becomes a permanent exemption, or inherited order is lost. | +| FR8 | Theme/AppShell integration | all names/`none`; SSR hint; no/nearest/root/nested theme; exact boundaries | AppShell uses the wrong map, equality, or scope. | +| FR2 (height), FR9 | Condition matrix plus resolver tests on web and native | width 767/768 at height 800; width 1024 at height 499/500; overridden `md`; Selector `adaptive`; the web touch resolver; a native resolver | A resolver with its own size numbers or an inclusive edge, a native resolver using platform size classes or different numbers, or a `height` value other than `below: 500` accepted | +| IR1–IR3 | Runtime/build and CLI positive/negative fixtures | invalid map/range/field; source axis metadata; duplicate `when`; root-restoring rules; finite/open/unresolved component domains; declaration targets; root-enrolled and rule-only custom values; non-CSS token reads | Invalid input writes output, root and adaptation component validation disagree, an independently valid value is forced into root, an adaptation conditionally enrolls a custom value, or tooling misses a rule-only value. | +| IR4 | Generated-module import and size tests | no rules/rules; source/built parent and child; generative/local-token metadata | Breakpoints, rules, order, or metadata disappear; extension diverges; CSS text enters the module; or resolved layers duplicate. | ## Decision log @@ -305,6 +327,19 @@ validation tighten adaptations automatically. Rejected: adaptation-specific value resolvers, allowlists, unresolved-domain exceptions, and dummy root rules for independently valid values. +### DEC-6 — Compact is one definition on every platform + +**Reference:** `spec:AST-012/DEC-6` +**Decider:** `cixzhang`, `2026-10-24` + +A phone, a small tablet, and a short landscape window all lack room for an +anchored surface, so compact is width below the `md` point or height below 500. The same numbers on every platform keep one component presenting the same +way on the web and in native code, and one exported definition keeps resolvers +from drifting apart. + +Rejected: mapping compact to each platform's size classes, which makes the +same screen compact on one platform and not another. + ## Open questions None. diff --git a/packages/core/src/Selector/Selector.spec.md b/packages/core/src/Selector/Selector.spec.md index 2914bd6bdc896..16edbc025a0b2 100644 --- a/packages/core/src/Selector/Selector.spec.md +++ b/packages/core/src/Selector/Selector.spec.md @@ -7,7 +7,7 @@ authority: current archive_reason: null superseded_by: null approved_by: cixzhang -approved_at: 2026-08-30 +approved_at: 2026-10-24 owners: [cixzhang, imdreamrunner] review_triggers: [public-api, behavior, layout, theming, accessibility] verified_by: @@ -145,7 +145,7 @@ These requirements describe shipped behavior on current `main`. | FR1 | Selecting an enabled option updates the one selected value, closes the active presentation, and returns the trigger to its stable closed state. | Consumer docs and selection interaction tests | | FR2 | Without explicit placement, a non-search popover aligns the selected row over the trigger and clamps it to the viewport. Search popovers and explicit placement use normal Layer positioning. | Consumer docs, implementation, and geometry tests | | FR3 | An option whose resolved selection indicator draws no content reserves no mark-column space. Visible selected or themed replacement indicators remain in layout at `indicatorPosition`; the resulting state-dependent label position or available width is intentional, and row content keeps its existing overflow behavior. | `itemMarkColumn`, focused indicator tests, and Chromium evidence stories | -| FR4 | `popover` uses an anchored Popover. `bottom-sheet` uses a modal BottomSheet. `adaptive` resolves to the modal bottom sheet on compact coarse-pointer screens and Popover otherwise. | Presentation controller and adaptive-presentation tests | +| FR4 | `popover` uses an anchored Popover. `bottom-sheet` uses a modal BottomSheet. `adaptive` resolves to the modal bottom sheet on coarse-pointer screens that are compact under `spec:AST-012` FR9 and Popover otherwise. | Presentation controller and adaptive-presentation tests | | FR5 | While `isLoading` is true, the trigger exposes busy state and the listbox suppresses empty and no-results output. | Loading, empty-state, and announcement tests | | FR6 | While `isReadOnly` is true, the selected value remains focusable and form-submittable, while the selection surface, clear action, disclosure indicator, and every value-change path are unavailable. | Read-only interaction, form, and accessibility tests | | FR7 | The listbox carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR3, FR5 and AR2: the option under a pointer's release is the one selected, the highlight follows a held pointer through `aria-activedescendant` while DOM focus stays on the combobox, a mouse released outside closes the list and a finger leaves it open, and the browser's stray click never selects. Trigger opening, arrow clamping and closed-trigger typeahead are unchanged. | `module:DropdownMenu/useMenuPress`; `Selector.test.tsx` press model suite | @@ -356,7 +356,7 @@ FR3 implements the system decision owned by `spec:AST-004/DEC-1`. An omitted `presentation` resolves `adaptive` (FR4): the modal bottom sheet on compact coarse-pointer screens and the anchored popover elsewhere, so fine-pointer and large-screen output matches `popover`. `adaptive` keeps its -width-and-pointer query; a pointer-only test belongs to `spec:AST-043` OQ1. +compact-and-pointer test, compact as `spec:AST-012` FR9 defines it; a pointer-only test belongs to `spec:AST-043` OQ1. Rejected: keeping `popover` as the default, because a phone then gets a pointer-sized anchored list unless the caller opts in. From a5162bf418ea05abc5c39dc17613c74d4d7014ec Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 24 Oct 2026 11:13:26 -0700 Subject: [PATCH 2/4] spec(AST-012): native reads compact from the portable theme; AST-082 and AST-093 name it; compact excludes layout widths; Selector change pending --- docs/architecture/public-component-api.md | 3 ++- docs/specs/AST-012/spec.md | 15 ++++++++++----- docs/specs/AST-082-native-first-surfaces/spec.md | 12 ++++++++---- .../AST-093-native-component-guidelines/spec.md | 5 +++-- packages/core/src/Selector/Selector.spec.md | 12 ++++++++++-- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/docs/architecture/public-component-api.md b/docs/architecture/public-component-api.md index 219aea278f326..db632a68ded6d 100644 --- a/docs/architecture/public-component-api.md +++ b/docs/architecture/public-component-api.md @@ -170,7 +170,8 @@ guidance owns the process used to propose and test APIs. presentation by screen size uses the theme's compact definition: width below the effective `md` point or height below 500, with the same numbers on every platform (`spec:AST-012` FR9). It keeps no compact numbers of its own, as - INV14 keeps no parallel breakpoint table. + INV14 keeps no parallel breakpoint table. A layout width the caller picks, + such as AppShell's `mobileNav.breakpoint`, follows INV14 instead. This record applies to stable public packages. Lab components are not stable public promises until promotion. diff --git a/docs/specs/AST-012/spec.md b/docs/specs/AST-012/spec.md index 0e113f065ed27..ccc3352585350 100644 --- a/docs/specs/AST-012/spec.md +++ b/docs/specs/AST-012/spec.md @@ -172,7 +172,11 @@ A breakpoint map alone emits no CSS. Only rules with adaptation values do. by screen size uses it, including the web touch presentation resolver, Selector's `adaptive` presentation, and `@astryxdesign/native`; none keeps size numbers of its own. A resolver that also needs a coarse pointer ANDs - that with compact. + that with compact. Compact governs only the choice of a presentation, such as + a sheet in place of a popover; a layout width a caller picks, such as + AppShell's `mobileNav.breakpoint` (FR8), is not compact. Theme `adaptations` + stay web-only (`spec:AST-082` FR18); native reads the compact numbers from + the portable theme. - **IR1 — Normalize and reject atomically.** One path validates width breakpoints, named condition fields, width ranges, theme extension, enrolled local-token names, and concrete expanded values for both `defineTheme` and @@ -236,13 +240,14 @@ Its value resolver, explicit-value preservation, CSS generation/build parity, an extension metadata remain useful implementation evidence. The theme exports no compact definition, and `when` has no `height` field. -Selector's `adaptive` presentation and DropdownMenu's compact-touch decisions use -`(max-width: 768px) and (pointer: coarse)`: a coarse-pointer screen exactly 768 +Selector's, MultiSelector's, ContextMenu's, and DropdownMenu's `adaptive` +presentation (MoreMenu forwards to DropdownMenu) and DropdownMenu's compact-touch +sub-menu decisions use `(max-width: 768px) and (pointer: coarse)`, and the +ContextMenu and MoreMenu consumer docs describe it as "768px and below": a coarse-pointer screen exactly 768 pixels wide counts as compact, and a landscape phone at least 768 pixels wide but under 500 tall does not. Under FR9 the first opens the popover and the second the bottom sheet; the release note for that change names `presentation` -as the way to keep a surface. `@astryxdesign/native` has no compact resolver -yet; it adopts FR9 when it gains one. +as the way to keep a surface. `PortableTheme` carries no compact definition, and `@astryxdesign/native` has no compact resolver yet; it adopts FR9 when it gains one. ## Verification diff --git a/docs/specs/AST-082-native-first-surfaces/spec.md b/docs/specs/AST-082-native-first-surfaces/spec.md index 3ba70bf5365db..1c67d2cc7ae34 100644 --- a/docs/specs/AST-082-native-first-surfaces/spec.md +++ b/docs/specs/AST-082-native-first-surfaces/spec.md @@ -7,7 +7,7 @@ authority: current archive_reason: null superseded_by: null approved_by: cixzhang -approved_at: 2026-10-21 +approved_at: 2026-10-24 phase: accepted owners: [cixzhang] affects_architecture: @@ -251,7 +251,10 @@ depends only on runtimes it can maintain. that version's controls and materials. A capability newer than the running OS degrades to the closest earlier platform equivalent or to durable intent, never to a broken or blank state. Touch targets meet the platform minimum, and text - follows the platform's text-size setting. A native platform is supported only + follows the platform's text-size setting. When a component chooses between + presentations by screen size, compact is `spec:AST-012` FR9 in points or + density-independent pixels, not the platform's size classes; the platform's + own control still presents the chosen surface. A native platform is supported only when it meets the FR12 criteria on its oldest and newest supported OS versions. - **FR12 — Approaches are compared on one scorecard.** Every candidate way of building a native platform is measured the same way. Candidates include @@ -363,8 +366,9 @@ depends only on runtimes it can maintain. core contract: an icon is decorative unless it carries a label. - **FR18 — The theme definition has a portable part and a web part.** The - portable entry exports `PortableTheme`: a theme's name, tokens, and theme-local - tokens, each resolvable without CSS to a concrete value per color mode, such as + portable entry exports `PortableTheme`: a theme's name, tokens, theme-local + tokens, and the compact definition of `spec:AST-012` FR9 (the effective `md` + width point and the 500 height edge), each resolvable without CSS to a concrete value per color mode, such as a color, a length in density-independent units, a number, or a font name. The web part holds what only the web compiles or renders: component overrides keyed by web theme targets, icon and indicator registries of React elements, diff --git a/docs/specs/AST-093-native-component-guidelines/spec.md b/docs/specs/AST-093-native-component-guidelines/spec.md index 0028eaba7a0f8..e544890340500 100644 --- a/docs/specs/AST-093-native-component-guidelines/spec.md +++ b/docs/specs/AST-093-native-component-guidelines/spec.md @@ -7,7 +7,7 @@ authority: current archive_reason: null superseded_by: null approved_by: cixzhang -approved_at: 2026-10-23 +approved_at: 2026-10-24 phase: accepted owners: [cixzhang] affects_architecture: [] @@ -84,7 +84,8 @@ validating, and reviewing individual native components while including Reduce Motion, haptics, and VoiceOver. Each spec names the guideline pages it follows. The core contract keeps the component's meaning, words, states, and accessibility semantics; the guidelines decide how the platform - presents them. + presents them, except that a choice between presentations by screen size uses + the compact definition of `spec:AST-012` FR9 rather than size classes. - **FR4 — Brand shows through the theme everywhere else.** What FR2 and FR3 leave open, such as accent and fill color, type family, corner shape, spacing, and icon choice, comes from the app's theme: tokens from its diff --git a/packages/core/src/Selector/Selector.spec.md b/packages/core/src/Selector/Selector.spec.md index 16edbc025a0b2..ba8faa88bb9fc 100644 --- a/packages/core/src/Selector/Selector.spec.md +++ b/packages/core/src/Selector/Selector.spec.md @@ -57,6 +57,14 @@ connection between the closed trigger and its selection surface. stable-default change, so it ships as `[breaking]` in a scheduled minor under `spec:AST-017`; callers that need the anchored popover on every device pass `popover`. +- `adaptive`'s compact test becomes `spec:AST-012` FR9 (width below the + effective `md` point or height below 500) in place of the shipped + `(max-width: 768px)` query: a coarse-pointer screen exactly 768 pixels wide + opens the popover, and a coarse-pointer screen at least 768 wide but under + 500 tall opens the bottom sheet. Both surfaces keep selection working, so no + caller must change source and the change is not breaking under + `spec:AST-017` FR1; `popover` and `bottom-sheet` pin one surface. It is + pending until the shared resolver conforms; FR4 describes the shipped query. - `hasClear` changes the value contract to include `null`; that distinction is already part of the public type. - `isReadOnly` is additive and defaults to `false`. It preserves the selected @@ -145,7 +153,7 @@ These requirements describe shipped behavior on current `main`. | FR1 | Selecting an enabled option updates the one selected value, closes the active presentation, and returns the trigger to its stable closed state. | Consumer docs and selection interaction tests | | FR2 | Without explicit placement, a non-search popover aligns the selected row over the trigger and clamps it to the viewport. Search popovers and explicit placement use normal Layer positioning. | Consumer docs, implementation, and geometry tests | | FR3 | An option whose resolved selection indicator draws no content reserves no mark-column space. Visible selected or themed replacement indicators remain in layout at `indicatorPosition`; the resulting state-dependent label position or available width is intentional, and row content keeps its existing overflow behavior. | `itemMarkColumn`, focused indicator tests, and Chromium evidence stories | -| FR4 | `popover` uses an anchored Popover. `bottom-sheet` uses a modal BottomSheet. `adaptive` resolves to the modal bottom sheet on coarse-pointer screens that are compact under `spec:AST-012` FR9 and Popover otherwise. | Presentation controller and adaptive-presentation tests | +| FR4 | `popover` uses an anchored Popover. `bottom-sheet` uses a modal BottomSheet. `adaptive` resolves to the modal bottom sheet on compact coarse-pointer screens and Popover otherwise. | Presentation controller and adaptive-presentation tests | | FR5 | While `isLoading` is true, the trigger exposes busy state and the listbox suppresses empty and no-results output. | Loading, empty-state, and announcement tests | | FR6 | While `isReadOnly` is true, the selected value remains focusable and form-submittable, while the selection surface, clear action, disclosure indicator, and every value-change path are unavailable. | Read-only interaction, form, and accessibility tests | | FR7 | The listbox carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR3, FR5 and AR2: the option under a pointer's release is the one selected, the highlight follows a held pointer through `aria-activedescendant` while DOM focus stays on the combobox, a mouse released outside closes the list and a finger leaves it open, and the browser's stray click never selects. Trigger opening, arrow clamping and closed-trigger typeahead are unchanged. | `module:DropdownMenu/useMenuPress`; `Selector.test.tsx` press model suite | @@ -356,7 +364,7 @@ FR3 implements the system decision owned by `spec:AST-004/DEC-1`. An omitted `presentation` resolves `adaptive` (FR4): the modal bottom sheet on compact coarse-pointer screens and the anchored popover elsewhere, so fine-pointer and large-screen output matches `popover`. `adaptive` keeps its -compact-and-pointer test, compact as `spec:AST-012` FR9 defines it; a pointer-only test belongs to `spec:AST-043` OQ1. +width-and-pointer query; a pointer-only test belongs to `spec:AST-043` OQ1. Rejected: keeping `popover` as the default, because a phone then gets a pointer-sized anchored list unless the caller opts in. From 4676a942f18597c6c5604a7df9a5839f761a49ad Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 24 Oct 2026 11:47:50 -0700 Subject: [PATCH 3/4] spec(AST-082): DefinedTheme gains the compact definition additively; verify an md override --- docs/specs/AST-012/spec.md | 3 +-- docs/specs/AST-082-native-first-surfaces/spec.md | 13 ++++++++----- packages/core/src/Selector/Selector.spec.md | 3 +-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/specs/AST-012/spec.md b/docs/specs/AST-012/spec.md index ccc3352585350..07c5125c3df33 100644 --- a/docs/specs/AST-012/spec.md +++ b/docs/specs/AST-012/spec.md @@ -243,8 +243,7 @@ The theme exports no compact definition, and `when` has no `height` field. Selector's, MultiSelector's, ContextMenu's, and DropdownMenu's `adaptive` presentation (MoreMenu forwards to DropdownMenu) and DropdownMenu's compact-touch sub-menu decisions use `(max-width: 768px) and (pointer: coarse)`, and the -ContextMenu and MoreMenu consumer docs describe it as "768px and below": a coarse-pointer screen exactly 768 -pixels wide counts as compact, and a landscape phone at least 768 pixels wide +ContextMenu and MoreMenu consumer docs describe it as "768px and below": a coarse-pointer screen exactly 768 pixels wide and at least 500 tall counts as compact, and a landscape phone at least 768 pixels wide but under 500 tall does not. Under FR9 the first opens the popover and the second the bottom sheet; the release note for that change names `presentation` as the way to keep a surface. `PortableTheme` carries no compact definition, and `@astryxdesign/native` has no compact resolver yet; it adopts FR9 when it gains one. diff --git a/docs/specs/AST-082-native-first-surfaces/spec.md b/docs/specs/AST-082-native-first-surfaces/spec.md index 1c67d2cc7ae34..7830f8b39a9bc 100644 --- a/docs/specs/AST-082-native-first-surfaces/spec.md +++ b/docs/specs/AST-082-native-first-surfaces/spec.md @@ -431,8 +431,10 @@ depends only on runtimes it can maintain. - `architecture:theme-compilation` gains the native theme boundary when a native platform ships (INV9, INV10). - `architecture:theme-authoring-contract` gains `PortableTheme` as the portable - part of `DefinedTheme` when the portable entry ships. `DefineThemeInput` and - `DefinedTheme` are unchanged for web callers. + part of `DefinedTheme` when the portable entry ships. `DefineThemeInput` is + unchanged for web callers; `DefinedTheme` gains the compact definition + (`spec:AST-012` FR9) additively, derived from the theme's effective `md` point, + and is otherwise unchanged. - The `theme` and `cli` consumer docs and every component's documentation gain platform marking when the first native contract ships. - `@astryxdesign/native` ships on the canary channel with the FR19 wire: the @@ -467,7 +469,7 @@ depends only on runtimes it can maintain. | FR15 | Package metadata and release-channel checks | native package manifest; a stable release; a canary release | The native package is publishable as a stable release before a platform is supported | | FR16 | Package-boundary check, plus a native fixture app installed and bundled without `react-dom` or StyleX | an import of the portable entry; an import of a core component or the web theme entry from native; a listed build-time read from a native build script; an unlisted one; a core import of native; a core contract change | Native resolves a DOM type, `react-dom`, StyleX, or a web module; either direction of the boundary is crossed, an unlisted build-time read passes; or a core contract change does not reach native contracts on the next build | | FR17 | Icon map coverage check per native platform, plus override resolution tests | every built-in name; a namespaced name; a theme platform icon with and without a version range; an app-registered icon; an icon slot given an unknown name in a development and a release build; an asset icon the app ships and one it does not; a missing asset icon in a development and a release build | A built-in name has no platform default, a theme or app icon fails to replace the default, a scoped icon applies outside its range, a web icon is substituted, or an unknown name draws something in a release build or does not stop a development build with its name, or a missing asset icon draws a substitute, or a development build does not stop with a missing asset’s name | -| FR18 | Type tests over `PortableTheme` and `DefinedTheme`, token resolution tests per color mode, and `defineNativeTheme` reporting tests | each maintained theme; a theme with component overrides, icons, indicators, media surfaces, and adaptations; light and dark; a length, a color, and a font token | `DefinedTheme` changes for a web caller, a portable token needs CSS to resolve, `PortableTheme` references a React or DOM type, or a web part with content is dropped without a report | +| FR18 | Type tests over `PortableTheme` and `DefinedTheme`, token resolution tests per color mode, and `defineNativeTheme` reporting tests | each maintained theme; a theme with component overrides, icons, indicators, media surfaces, and adaptations; light and dark; a length, a color, and a font token; a theme that overrides `md`, whose compact definition follows the override | A `DefinedTheme` member a web caller uses is removed or changes, a portable token needs CSS to resolve, `PortableTheme` references a React or DOM type, a web part with content is dropped without a report, or the compact definition does not follow a theme's effective `md` | | FR19 | Wire type tests per platform condition, Swift decoding tests, host tests in a JavaScript engine, and a logic core run with no host globals | a tree with a content slot; a listed and an unlisted callback; an unknown component; an unknown closed-set value; a missing required prop; a node carrying a prop the host does not know; input that is not an action; a bundle with no entry; a JavaScript error; a bridge version mismatch; a tree the host cannot read at all; a tree sent after a failure | A view gets an unlisted callback, an action carries a different key or arguments, one unreadable node blanks its siblings, a prop the host does not know makes its node unsupported, the logic core accepts input that is not an action, or a failed host draws a tree or sends an action | ## Decision log @@ -618,8 +620,9 @@ foreign next to system controls. Native platforms need a theme's identity, not its CSS. Splitting `DefinedTheme` into `PortableTheme` and a web part gives native the shared identity through the -portable entry, keeps React elements and CSS out of native apps, and changes -nothing for web callers, because `DefinedTheme` is still the whole. +portable entry, keeps React elements and CSS out of native apps, and removes +nothing for web callers, because `DefinedTheme` is still the whole; the portable +compact definition it gains is additive. Rejected: native reading the whole `DefinedTheme`, because it carries React elements, CSS maps, and DOM-typed imports native cannot use. diff --git a/packages/core/src/Selector/Selector.spec.md b/packages/core/src/Selector/Selector.spec.md index ba8faa88bb9fc..2829ca088e347 100644 --- a/packages/core/src/Selector/Selector.spec.md +++ b/packages/core/src/Selector/Selector.spec.md @@ -59,8 +59,7 @@ connection between the closed trigger and its selection surface. `popover`. - `adaptive`'s compact test becomes `spec:AST-012` FR9 (width below the effective `md` point or height below 500) in place of the shipped - `(max-width: 768px)` query: a coarse-pointer screen exactly 768 pixels wide - opens the popover, and a coarse-pointer screen at least 768 wide but under + `(max-width: 768px)` query: a coarse-pointer screen exactly 768 pixels wide and at least 500 tall opens the popover, and a coarse-pointer screen at least 768 wide but under 500 tall opens the bottom sheet. Both surfaces keep selection working, so no caller must change source and the change is not breaking under `spec:AST-017` FR1; `popover` and `bottom-sheet` pin one surface. It is From e760193d191ddb615908ddda76d4032573efc269 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 24 Oct 2026 13:23:18 -0700 Subject: [PATCH 4/4] spec(AST-012): wider than 768 for the short-landscape case; Selector names the adaptive change up front --- docs/specs/AST-012/spec.md | 4 ++-- packages/core/src/Selector/Selector.spec.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/specs/AST-012/spec.md b/docs/specs/AST-012/spec.md index 07c5125c3df33..a841f8e5b2666 100644 --- a/docs/specs/AST-012/spec.md +++ b/docs/specs/AST-012/spec.md @@ -243,8 +243,8 @@ The theme exports no compact definition, and `when` has no `height` field. Selector's, MultiSelector's, ContextMenu's, and DropdownMenu's `adaptive` presentation (MoreMenu forwards to DropdownMenu) and DropdownMenu's compact-touch sub-menu decisions use `(max-width: 768px) and (pointer: coarse)`, and the -ContextMenu and MoreMenu consumer docs describe it as "768px and below": a coarse-pointer screen exactly 768 pixels wide and at least 500 tall counts as compact, and a landscape phone at least 768 pixels wide -but under 500 tall does not. Under FR9 the first opens the popover and the +ContextMenu and MoreMenu consumer docs describe it as "768px and below": a coarse-pointer screen exactly 768 pixels wide and at least 500 tall counts as compact, and a landscape phone wider than 768 pixels but under +500 tall does not. Under FR9 the first opens the popover and the second the bottom sheet; the release note for that change names `presentation` as the way to keep a surface. `PortableTheme` carries no compact definition, and `@astryxdesign/native` has no compact resolver yet; it adopts FR9 when it gains one. diff --git a/packages/core/src/Selector/Selector.spec.md b/packages/core/src/Selector/Selector.spec.md index 2829ca088e347..a66f26aced939 100644 --- a/packages/core/src/Selector/Selector.spec.md +++ b/packages/core/src/Selector/Selector.spec.md @@ -48,7 +48,7 @@ connection between the closed trigger and its selection surface. ## Compatibility and migration - Released defaults and behavior are unchanged except the `presentation` - default, which DEC-1 sets. + default, which DEC-1 sets, and `adaptive`'s compact test (below). - `indicatorPosition` defaults to `end`; a rendered selection mark occupies space at that logical edge, while an empty resolved indicator occupies none. - `presentation` defaults to `adaptive` (DEC-1): the modal bottom sheet on @@ -59,7 +59,7 @@ connection between the closed trigger and its selection surface. `popover`. - `adaptive`'s compact test becomes `spec:AST-012` FR9 (width below the effective `md` point or height below 500) in place of the shipped - `(max-width: 768px)` query: a coarse-pointer screen exactly 768 pixels wide and at least 500 tall opens the popover, and a coarse-pointer screen at least 768 wide but under + `(max-width: 768px)` query: a coarse-pointer screen exactly 768 pixels wide and at least 500 tall opens the popover, and a coarse-pointer screen wider than 768 but under 500 tall opens the bottom sheet. Both surfaces keep selection working, so no caller must change source and the change is not breaking under `spec:AST-017` FR1; `popover` and `bottom-sheet` pin one surface. It is