From b93e7335e8bb5f305248d08207c544f84ed7b54c Mon Sep 17 00:00:00 2001 From: Ivor Zhou Date: Mon, 2 Nov 2026 12:52:55 -0800 Subject: [PATCH] docs(spec): PowerSearch's sheet presentation follows the Lab touch flow (DEC-3) The owner named Lab MobilePowerSearch's current flow as the target for Core's experimental sheet presentation. The record now describes that flow as Core's contract. - Steps stack and stay open beneath the next (FR15a). A dismissal reveals the previous step, and no sheet carries Back. A creation's commit shows Filters. - Headers, search fields and footers stay fixed while only the rows scroll. - Every removable Filters row has a trailing remove action, one-tap boolean filters included. The value step has no Remove. - Add filter's search stays unfocused until tapped. Its empty results are a compact empty state, and its ungrouped fields sit under "General". - A lone editor sits on the sheet surface. The empty Filters state is one centered Add filter. - Clear all sits behind a confirmation, keeps read-only filters and leaves Filters open. - A date-only value edits as its day in timezoneID, else UTC. A value with a time of day edits in timezoneID, else the viewer's zone. DEC-3 lists where Core's contract differs from the prototype. The rules for identity, stale filters, focus recovery and announcements only from the open step are unchanged. The presentation stays unexposed, so every sheet row keeps its implementation-gap note. The sheet's Clear all matches the popover presentation's (FR23). The popover's date handling is stable behavior outside this experimental option, and the known gap lists where it differs from the sheet. --- .../PowerSearch/PowerSearch.spec.md | 533 ++++++++++-------- 1 file changed, 288 insertions(+), 245 deletions(-) diff --git a/docs/specs/components/PowerSearch/PowerSearch.spec.md b/docs/specs/components/PowerSearch/PowerSearch.spec.md index c1d3e2dca621e..ca5d10afff290 100644 --- a/docs/specs/components/PowerSearch/PowerSearch.spec.md +++ b/docs/specs/components/PowerSearch/PowerSearch.spec.md @@ -17,6 +17,7 @@ verified_by: packages/core/src/Tokenizer/Tokenizer.test.tsx, packages/core/src/Typeahead/Typeahead.test.tsx, packages/core/src/Layer/useLayer.test.tsx, + packages/lab/src/MobilePowerSearch/MobilePowerSearch.test.tsx, scripts/check-knowledge.mjs, ] modules: [] @@ -56,10 +57,13 @@ across the page or depending on input modality. PowerSearch also offers an experimental touch presentation (DEC-3): the whole field becomes one trigger showing display-only filter tokens, and the filter -work moves into a flow of tall modal bottom sheets — Filters, Add filter, and a -value step — that reads on a compact touch screen without a software keyboard -covering the editor. Omitting the option keeps the tokenizer and editor popover -on every device. +work moves into a stack of tall modal bottom sheets — Filters, Add filter, and a +value step, each opened above the one that led to it and each dismissed back to +it — that reads on a compact touch screen without a software keyboard covering +the editor. Omitting the option keeps the tokenizer and editor popover on every +device. The flow's shape is the one the Lab prototype `MobilePowerSearch` +ships; this record states it as Core's contract, with the differences DEC-3 +lists. Where the shipped PowerSearch does not yet meet a requirement, the row carries a present-tense implementation gap note. @@ -103,6 +107,13 @@ present-tense implementation gap note. all means every removable filter, and a Clear all that removes a read-only filter is a defect. `onChange` documents `index` as the affected position, which the first removed index is. It ships as a `[fix]` patch release. +- DEC-3 changes nothing in the `popover` presentation. Its Clear all is FR23's + (DEC-4), which the sheet presentation's matches. Where its date handling + differs from the sheet presentation's (the known gap in Open questions), + bringing it to the sheet's behavior changes what the stable presentation + reports through `onChange`, so it ships as its own stable change, classified + under `spec:AST-017/FR5`, outside this experimental option + (`spec:AST-017/FR22`). Consumer migration instructions belong in consumer docs and release notes. @@ -118,9 +129,10 @@ Consumer migration instructions belong in consumer docs and release notes. - Preserving the editing flow, content, and focus behavior while the editor's width and horizontal alignment change. - The touch sheet presentation: its trigger, the Filters, Add filter, and value - sheets, the order of steps, which control receives focus after each step, - the staging of a value until the step confirms it, the `onChange` metadata of - each commit, and which PowerSearch props the sheets honor. + sheets, which of them are open at once and which one dismissing reveals, + which control receives focus after each step, the staging of a value until + the step confirms it, the `onChange` metadata of each commit, Clear all's + confirmation, and which PowerSearch props the sheets honor. **Does not own / non-goals** @@ -138,11 +150,9 @@ Consumer migration instructions belong in consumer docs and release notes. (`component:Selector/FR4`); it adds no query of its own. - The sheets' dialog, the one scrim of the stack of open modal sheets, gestures, snap behavior, keyboard accommodation, which sheet is on top and so takes a - dismissal, and the handoff between steps — owned by `component:BottomSheet` - (its sheet stack and same-update replace) and `family:overlay-dismissal`; - `component:BottomSheetSwitcher`, the deprecated predecessor of standalone - step sheets (its DEP-0015), only while the shipped flow still hosts its - steps in it. + dismissal, how a covered sheet recedes, and how a dismissed top sheet reveals + the sheet beneath it — owned by `component:BottomSheet` (its sheet stack) and + `family:overlay-dismissal`. - The list-value sheet a `string_list` or `entity_list` value opens inside the value step — owned by `component:Tokenizer` (its DEC-3 sheet presentation). @@ -177,10 +187,11 @@ apply to the trigger as FR11 states; `hasClear` governs Clear all (FR12); FR15); `maxSearchResults` and `maxOperatorMenuItems` apply to the Add filter and value steps (FR13, FR17); `popoverSaveButtonLabel` and `components` apply to the value step and the trigger (FR11, FR14); `maxTokenLength` applies to the -trigger's tokens as to the popover's (FR11). `timezoneID` changes nothing in -the sheet presentation: the trigger's tokens, the Filters rows, the custom -Editor's props, and the date editors read and format exactly as the popover -presentation's do (FR11, FR12, FR14, FR17). +trigger's tokens as to the popover's (FR11). `timezoneID` names the zone the +sheet presentation reads and writes a date value in, on the trigger's tokens, +the Filters rows, and the date editors: a date-only value is its day there, +else the UTC day, and a value whose operator sets `isDateOnly: false` is its +date and time there, else in the viewer's zone (FR11, FR12, FR17). ## Behavioral and layout contract @@ -193,31 +204,32 @@ popover, rendered when `experimental` is omitted or resolves to `popover`. The editor popover does not render in the `bottom-sheet` presentation, whose surface FR10–FR21 govern. -| ID | Invariant | Basis | Status | -| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| FR1 | In the `popover` presentation, the filter-editor popover MUST have a rendered border-box width no greater than 720 CSS px. | Owner direction; legibility issue demonstrated by wide PowerSearch adopters | Accepted contract; implementation pending | -| FR2 | In the `popover` presentation, before viewport collision handling, the editor's preferred width MUST be the outer PowerSearch width clamped to the inclusive 400–720 CSS px range. The 400 px floor MUST yield when the available viewport width cannot fit it. | Existing 400 px editing floor plus `component:PowerSearch/DEC-1` | Accepted contract; implementation pending | -| FR3 | In the `popover` presentation, when the 720 CSS px cap does not make the editor narrower than the outer PowerSearch, including when the 400 px floor is active, the editor MUST preserve current logical-start alignment to the outer PowerSearch. | Compatibility requirement | Accepted contract; implementation pending | -| FR4 | In the `popover` presentation, when the 720 CSS px cap makes the editor narrower, PowerSearch MUST resolve one stable opening control for that open: the inner typeahead input for the add path, or the actual activated token/control for the edit path. | Owner direction plus current composition analysis | Accepted contract; implementation pending | -| FR5 | In the `popover` presentation, for a capped editor, PowerSearch MUST compare the opening control's border-box distance from the outer PowerSearch's physical left and right edges, select the nearer outer edge, and align the editor to that edge. Equal distances MUST fall back to the outer PowerSearch's logical-start edge. The result MUST depend on control geometry, not pointer coordinates or pointer/keyboard modality. | `component:PowerSearch/DEC-2` | Accepted contract; implementation pending | -| FR6 | In the `popover` presentation, opening-control identity and the selected edge MUST be latched for the open lifetime. Reopening from a control on the opposite side MUST recalculate both; movement after open MUST NOT move the editor. | Stable interaction and performance requirement | Accepted contract; implementation pending | -| FR7 | In the `popover` presentation, width and preferred-edge selection MUST still yield to the layer runtime's viewport collision handling so the editor remains horizontally visible within the runtime's existing safe area. | `architecture:layer-runtime` | Accepted contract; implementation pending | -| FR8 | In the `popover` presentation, the width and alignment change MUST NOT alter editor content, field/operator/value state, vertical placement, focus movement, selection, dismissal, or screen-reader semantics. | Compatibility and accessibility requirement | Accepted contract; implementation pending | -| FR9 | In the `popover` presentation, the opening-control descriptor owned by `component:Tokenizer/DEC-1` and consumed by PowerSearch MUST remain package-internal. PowerSearch MUST NOT add a public width, maximum-width, input-ref, or positioning prop for this behavior. | `component:Tokenizer/DEC-1`, `architecture:public-component-api`, and `component:PowerSearch/DEC-1` | Accepted contract; implementation pending | -| FR10 | `experimental.presentation` selects the surface. `popover`, the value when `experimental` or the option is omitted, is the tokenizer field with its typeahead and editor popover, unchanged except as FR19 (no mount announcement) states. `bottom-sheet` is the trigger and sheet flow of FR11–FR21 on every device. `adaptive` resolves to `bottom-sheet` on compact coarse-pointer screens and to `popover` otherwise, through the same shared resolution the selection controls use (`component:Selector/FR4`). That resolution answers `popover` on the server and during the hydrating render, so an `adaptive` field server-renders as, and hydrates without a mismatch as, the popover presentation's tokenizer field; on a compact coarse-pointer screen the first client render after hydration swaps it to the trigger. Focus follows the resolution rule below: before any interaction nothing in the field has focus and nothing moves, except that with `hasAutoFocus` the server markup's `autofocus` has focused the combobox input, and the swap then moves focus to the trigger within the same visit — the caller is told of neither a blur nor a new focus. The two closed fields share the field's wrapper, border, and minimum height for the size, and show the same tokens, but their row counts may differ: the popover field's editable tokens each carry a 16 px remove control the trigger's display-only tokens do not (FR11), so the trigger's tokens can wrap into fewer rows (seven filters in a 326 px field take seven rows editable and six as display-only tokens), and the popover field also carries its inline combobox input after the tokens, which may take a row of its own — the swap then changes the field's height by those rows (none on the measured story at 390 px, one token row at 414 px). A resolution change while the flow is open takes effect after it closes and focus has returned to the trigger (the focus the close reports, FR11) — the presentation hold `architecture:layer-runtime/INV14` assigns to the layer runtime; INV14 gap: the runtime does not yet provide one (its record names INV14 as ownership not yet met), so until it does the field keeps its own hold, to be replaced by the runtime's when it exists; when the new presentation is `popover`, focus then moves to the tokenizer's combobox input as the same visit — no `onBlur` and no second `onFocus` reaches the caller. A resolution change while the flow is closed takes effect at once; when focus is anywhere inside the closed field — the trigger, the combobox input, a token or its remove control, the clear control, or an interactive `endContent` — and the focused element does not survive the switch, focus moves to the other presentation's primary control (the combobox input, or the trigger) as a continuation of the same visit — no `onBlur` and no `onFocus` reaches the caller for the hand-off; when the focused element survives, or focus is outside the field, focus stays where it is. When the switch to the sheet presentation happens while the popover presentation's editor popover is open, the popover and its focused control unmount with the tokenizer field, and focus moves to the trigger. Focus never lands on the page body. | DEC-3; touch-flow evidence from the Lab prototype | Current contract. Implementation gap: the shipped PowerSearch has no `experimental` prop and renders the tokenizer field on every device. | -| FR11 | In the sheet presentation the field's control is one trigger: a button with `aria-haspopup="dialog"` and `aria-expanded`, whose accessible name is the field label followed by the trigger's visible text — the placeholder while no filter exists, otherwise the text of the filter tokens — so the name contains what is visible and exposes the selected filters. The tokens are display-only — each is the token's static structure (`component:Token/FR2`): no remove control, no click of its own, no inner control inside the trigger button — and the tokens, the placeholder, and the empty space around them belong to the trigger: a tap on any of them opens the flow. Tokens wrap onto further lines; the trigger does not reserve a row of its own once a token exists. The trigger's tokens are the popover presentation's tokens: the same component, the same words for a value, and the same zone handling, so a value reads on the trigger exactly as it reads on the tokenizer field. A `components[type].Token` override renders on the trigger in place of PowerSearch's token, receiving the props the popover presentation hands it — `config`, `filter`, `field`, `operator`, `maxLength`, `isDisabled` — with `onClick` and `onRemove` undefined, as the popover hands them for a read-only filter: the trigger's tokens are display-only, and the override formats its own value. `startIcon` renders in the trigger's lane. The result count and `endContent` render in the field's end lane, outside the trigger button and after it: an interactive `endContent` keeps its own tab stop, its own taps, and its own accessible name, and a tap on it does not open the flow, so a caller's control is never nested inside the component's button and the end lane's text is not part of the trigger's name. `size` sizes the trigger field and its tokens as it sizes the tokenizer field; `statusVariant` places `status.message` as the popover presentation places it; `hasAutoFocus` focuses the trigger on mount. `onFocus` fires when focus enters the field from outside and `onBlur` when it leaves the field: opening the flow moves focus into a sheet, outside the field, so `onBlur` fires, and the close that returns focus to the trigger fires `onFocus` — as the popover presentation fires them around its editor. While disabled the trigger stays focusable with `aria-disabled="true"`, never carries the native `disabled` attribute, does not open, and shows `disabledMessage` as the field does (`spec:AST-085/FR1`, `FR2`). When the component becomes disabled with the flow open, the flow closes and focus moves to the trigger, the destination this record names (`spec:AST-085/FR4`). | DEC-3; Lab touch-flow evidence; `spec:AST-085`; WCAG 2.5.3 | Current contract. Implementation gap: no trigger exists. | -| FR12 | Opening the flow from a field with at least one filter, or from a read-only field, shows the Filters sheet, a tall modal sheet named "Filters": a heading, an Add filter action in the header while the field is interactive, one row per filter showing "{field label} {operator label}" (one catalog message, so a locale can reorder it) and, in bold, the value as the popover presentation's token shows it — the same function, so the same words and the same zone handling, with the token's width budget lifted: the row never truncates a single value and never collapses a list to its "{n} items" count, both of which the token does once a value's text outgrows its width — with the token's summaries resolved from the catalog (`@astryx.powersearch.valueEditor.dateRange`, `itemsCount`, `filtersCount`, ICU plurals), so in English a row reads as the token does and another locale translates it: an `integer` or `float` value reads as the token's own text (`String(value)`: `2.3456`, `1e+21`, `1e-7`, and `0` for negative zero) wherever the locale writes numbers with the token's numerals — Latin digits and a `.` decimal mark — and elsewhere with the locale's digits and decimal mark, no grouping and no rounding, so that it differs from the token only in notation (`0,0000001`, `-0`); a `string_list`, `enum_list`, or `entity_list` value reads as all of its items joined by the locale's narrow conjunction list (`Intl.ListFormat`, `{type: 'conjunction', style: 'narrow'}` — "Open, Closed" in English, "高、中" in Simplified Chinese and Japanese, "A, B und C" in German): a conjunction list keeps every item separated in every shipped locale, where the `unit` type runs some together, and of the conjunction styles the narrow one is the one that reads in English exactly as the token's ", " join does; a `date_absolute` value reads as a day with no time, a `date_range` as "date range", a `nested` value as "{n} filters", and a `date_relative` value as the value step's choice row reads it ("3 days ago", FR20) when a choice matches the stored value, otherwise as stored (the value is any string; the token always shows it as stored) — and a footer with Done and, while `hasClear` is set, the field is interactive, and at least one editable filter exists, Clear all. A row's label and value are one catalog message, "{label} {value}", the same message the removal announcement of AR6 uses, with the value segment bold, so a locale that reorders it reorders both. A filter the field can edit is a row button with a trailing chevron; a read-only filter, or every filter while the field is read-only, is a plain row, as the popover gives such a token no remove control. A `nested` filter's row is a row button like any other; its step edits the group with the popover presentation's built-in nested editor (FR14). An editable filter whose field or operator the config no longer has — which the trigger still shows as a token — lists as a row button reading the field's label when the config still has the field and the raw key for whatever is gone — "{field key} {operator key}" when both are, "{field label} {operator key}" when only the operator is — that opens the step's "This filter is no longer available" state, where Remove is the way out, as the popover's token removal is; it is a plain row while the filter, or the field, is read-only. With no filter the body shows "No filters selected". Done closes the flow. Clear all commits the same change the popover presentation's clear control commits (FR23) — read-only filters stay, with `remove` at the first removed index — and closes the flow. After the flow closes focus returns to the trigger. | DEC-3; Lab touch-flow evidence; current popover clear semantics | Current contract. Implementation gap: no Filters sheet exists. | -| FR13 | Add filter — from the Filters sheet's header, or directly when an interactive field with no filter opens — is a tall modal sheet named "Add filter" with a Back action. Without a content-search field it lists the visible fields grouped by `group` (ungrouped first, each group a named list), each a row button with the field's icon and a trailing chevron, and, once the field count reaches eight, a "Search filters" field above the list that matches labels and `typeaheadAliases` case-insensitively, caps a non-empty query's matches at `maxSearchResults`, and shows "No filters match your search" when nothing matches. With a `contentSearchFieldKey` whose default operator takes a `string` value it shows instead a "Search" searchbox using `placeholder`, focused on arrival, with the same suggestions the popover presentation's typeahead offers listed inline as row buttons ("Search results"): fields, field-and-operator pairs, value matches, and a leading free-text item for the content field; Enter commits the first suggestion, ArrowDown and ArrowUp move between the search and the rows — arrow movement across independently focusable controls, recorded here as this record's exception to `spec:AST-020/FR15`, which adopts arrow keys for a single composite only: the searchbox-to-list continuity is what the popover presentation's combobox gives the same suggestions, and the rows stay in the Tab order as well — an IME-composing Enter does nothing, and a query with no suggestion shows "No results found" — the searchbox's `aria-controls` names the results list, or that message while nothing lists, so it always names an element; the field list's "No filters match your search" likewise shows only for a non-empty query that matches nothing, never for a config with no field. A suggestion that carries a value commits `onChange([...filters, filter], 'add', filters.length)`, announces `Added {label}` through the flow's own polite region (AR6), and returns to the Filters sheet; any other suggestion or field row opens the value step with that field and operator. Back returns to the Filters sheet, or closes the flow when the sheet was the entry point. Every visible field lists, a field whose every operator is `nested` included: the value step edits every operator's value (FR14). | DEC-3; Lab touch-flow evidence; current typeahead source | Current contract. Implementation gap: no Add filter sheet exists. | -| FR14 | The value step is a tall modal `form` sheet that edits every operator's value; its title is the dialog's name (AR5), and for a filter whose field or operator the config no longer has it reads as the Filters row does (FR12): the field's label when the config still has the field, the raw key for whatever is gone. When the field offers more than one operator the body starts with an "Operator" radio group whose options read "{field label} {operator label}" and list the field's operators, with the operator the step opened with checked — the filter's operator while editing, and while creating the operator the suggestion named (FR13) or else the field's default operator, else its first; otherwise there is no group and the title is "{field label} {operator label}" naming that operator. When no operator label applies the title is the field label. The body below the group is the editor for the checked operator's value type: the sheet-native controls of FR17, or for a `nested` value the popover presentation's built-in nested editor — the sub-filter tree with its Add action, each sub-filter's field, operator, and value controls, and removal — rendered as the popover renders it, with one difference: the step's Operator group takes the place of the editor's root group-operator picker, which is not shown, and the tree root reads the checked operator's label; a sub-filter's controls are the popover editor's own, in their default presentation (a Selector's menu, the Tokenizer's inline field), not the sheet's choice rows (FR17) or Tokenizer sheet (FR21), which govern the step's top-level value only, because the sheet passes no `experimental` option down to them; the tree is its own width container, so below 400 CSS px its rows wrap instead of overflowing the sheet, as they wrap in a narrow popover. A group is created and edited the same way in both presentations; switching the operator swaps the body. A value chosen in the body is staged, not committed: no `onChange` fires until the footer confirms it. Changing the operator keeps a staged value only when the new operator reads the same value type; an `empty` operator stages `{type: 'empty'}`. The footer holds Cancel and the confirm action — `popoverSaveButtonLabel` when given, otherwise "Add filter" while creating and "Apply" while editing — disabled until the staged value is complete, plus Remove while editing. When `components[type].Editor` is set for the checked operator's value type it replaces the body below the Operator group and the footer — the group stays outside it, so switching operators still swaps the body — receiving the props the popover presentation hands it, with the same meaning: the caller's `config`, the partial `filter`, `mode`, `saveButtonLabel`, `isReadOnly`, `onSave`, and `onCancel`, and nothing the popover does not pass; `onSave(filter)` commits it, `onSave(null)` removes the filter while editing and returns to Filters unchanged while creating. A controlled update that removes the field or operator being edited replaces the body with "This filter is no longer available", announced from its rendered element inside the sheet (AR6), and keeps Remove (while editing) and Cancel as the ways out; when focus was on a control of the replaced body it moves to Remove, else to Cancel, so it never falls to the page body inside the open sheet (FR16). | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no value step exists. | -| FR15 | Confirming a created filter commits `onChange([...filters, filter], 'add', filters.length)`. Tapping an editable Filters row opens the value step on that filter in edit mode; Apply commits `onChange(updated, 'edit', index)` and Remove commits `onChange(rest, 'remove', index)`, where `index` is the filter's position at commit time: a controlled parent that reorders or clones its filters while the step is open is followed by object identity, then by a structurally equal filter in the original slot, then by a unique structural match; when the target is gone or ambiguous nothing is committed and the flow returns to the Filters sheet, focus landing as FR16 states for a row that no longer exists. Every commit returns the flow to the Filters sheet. While the field is read-only, or disabled, no commit happens. | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no edit path exists. | -| FR16 | Focus follows the step: after a commit from the value step while creating, Add filter in the Filters header; after a content-search suggestion commits a filter directly, that filter's row; after Apply or Cancel from editing, the edited filter's row — followed across a controlled reorder or clone, and, when a controlled update removed it or left two filters it could be, the nearest remaining row that is a button around its former position (the following one on a tie), else Add filter; after Remove, the nearest remaining row that is a button (the following one on a tie) — a read-only filter's row, and any row while the field is read-only, are plain rows and are passed over — or Add filter when none remains; after Cancel from creation, the field row that opened the step, or the content search when a suggestion opened it — and when a controlled update has removed that field, or the content-search field itself, the Add filter sheet's first control: its search field when one shows, else its first field row, else Back; after Back from Add filter, the Filters header's Add filter; after a caller's `Editor` override calls `onSave(null)` while creating, the Filters header's Add filter (the flow returns to Filters unchanged, FR14); and, as one rule for the Filters and Add filter sheets, when a controlled update unmounts the control that holds focus — a filter's row, a dropped field's row, Clear all gone with the last filter — the nearest remaining row that is a button, counted among the button rows by the removed one's index (the following one, else the last) — a plain row (a read-only filter, or every filter while the field is read-only) is passed over, as the Remove and Apply/Cancel rules pass it over — else the sheet's first control (the Add filter sheet's search field, Add filter in the Filters header), else the sheet's panel, so focus never lands on the page body inside the open sheet; when a controlled update replaces the value step's body with the unavailable state while focus was on one of its controls, Remove, or Cancel when there is no Remove (FR14); after the flow closes, the trigger. | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no flow exists. | -| FR17 | The value step edits every value type with the same editor the popover presentation uses (`nested` through its built-in nested editor, FR14), with three sheet-native differences: `enum`, `enum_list`, and `date_relative` render as full-width choice rows with a trailing indicator instead of a dropdown that would stack a second layer on the sheet — `radio` rows in a `radiogroup` named by the field label for `enum` and `date_relative` (a tap selects the row; the group is one tab stop — the checked row, else the first — and the arrow keys move focus and check the row they land on, wrapping, Home and End move to and check the first and last row, and Space or Enter checks the focused row — `spec:AST-020/FR15`), `checkbox` rows in a `group` named by the field label for `enum_list` (a tap toggles the row; each row is its own tab stop, toggled with Space or Enter), each carrying its state as `aria-checked`, as the Tokenizer's sheet rows do; `integer` and `float` stage every parseable keystroke, so the confirm action enables while the person is still typing, and unstage when the field is cleared or stops parsing, so it disables again over an empty field; and a `string_list` or `entity_list` value edits through the Tokenizer's own sheet presentation (`component:Tokenizer/FR16–FR24`), its trigger showing the staged tokens in the step and its sheet titled with the field label. `date_absolute` and `date_range` values are read and written exactly as the popover presentation's editors read and write them — a picked day as its UTC midnight, a stored instant as its UTC day — so within one `timezoneID` and device the sheet agrees with the popover everywhere (see the known gap on date-only days). `maxOperatorMenuItems` caps the string and entity suggestions as in the popover presentation. | DEC-3; Lab touch-flow evidence; current value editors | Current contract. Implementation gap: no sheet editors exist. | -| FR18 | Every list in the sheets derives synchronously from the config — the field list, its search, the content-search suggestions, and the choice rows — so no sheet shows an empty state before results arrive: "No filters match your search" and "No results found" render only for a non-empty query whose synchronous result set is empty. The value typeaheads and tokenizers inside the value step own their own pending states. | DEC-3; `architecture:react-component-runtime/INV1` | Current contract. Implementation gap: no sheets exist. | -| FR19 | The result count renders at the end of the field in both presentations and is announced only when it changes after mount; the first render announces nothing, including under StrictMode's effect replay. The announcement takes the route `spec:AST-086/FR14` names for where the field is at that moment: inside the open sheet's dialog while a sheet of the flow is open (AR6), otherwise inside the nearest open native modal that holds the field (a Dialog, a modal Drawer), and the shared polite channel only when no modal does — never a channel the record names itself. | Current popover behavior; `architecture:react-component-runtime/INV3` | Current contract. Implementation gap: the shipped guard announces the initial count under StrictMode. | -| FR20 | Every string the sheets introduce comes from the `@astryx.powersearch.*` catalog, reusing the existing editor keys where their meaning fits (Cancel, Apply, Operator, Value, Values, Entities, the placeholders, the result count). Content the popover presentation already renders keeps its source — field, operator, and value text from the config and its formatters — except that every label the sheets render for a generated choice comes from the catalog as one ICU message with a plural: the relative-date choice rows (`@astryx.powersearch.sheet.relative{Days,Weeks,Months}{Ago,FromNow}`, the same choices and, in English, the same words as the popover's editor lists) and the value summaries of FR12. | DEC-3; catalog contract | Current contract. Implementation gap: no sheet strings exist. | -| FR21 | A `string_list` or `entity_list` value inside the value step opens the Tokenizer's sheet above the value step. Escape and swipe dismiss only that topmost sheet; closing it returns focus to its trigger inside the value step; the value step keeps its staged state. | DEC-3; `family:overlay-dismissal`; `component:Tokenizer/FR18` | Current contract. Implementation gap: no nested sheet exists. | -| FR22 | In the `popover` presentation, a read-only field (`isReadOnly`) MUST NOT remove a filter: it shows no Clear all, no token has a remove control, and Backspace on an empty input removes nothing. | The `isReadOnly` prop docs: "Prevent adding, editing, or removing filters." | Current contract | -| FR23 | In the `popover` presentation, Clear all MUST remove every editable filter and keep every read-only filter: it commits `onChange(, 'remove', )` and announces "Editable filters removed" while a read-only filter remains, otherwise "All filters removed". The clear control MUST render only while `hasClear` is set, the field is neither read-only nor disabled, and at least one editable filter exists. Backspace on an empty input MUST NOT remove a read-only filter; its token has no remove control. | DEC-4; a read-only filter's token has no remove control (current source) | Current contract | +| ID | Invariant | Basis | Status | +| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| FR1 | In the `popover` presentation, the filter-editor popover MUST have a rendered border-box width no greater than 720 CSS px. | Owner direction; legibility issue demonstrated by wide PowerSearch adopters | Accepted contract; implementation pending | +| FR2 | In the `popover` presentation, before viewport collision handling, the editor's preferred width MUST be the outer PowerSearch width clamped to the inclusive 400–720 CSS px range. The 400 px floor MUST yield when the available viewport width cannot fit it. | Existing 400 px editing floor plus `component:PowerSearch/DEC-1` | Accepted contract; implementation pending | +| FR3 | In the `popover` presentation, when the 720 CSS px cap does not make the editor narrower than the outer PowerSearch, including when the 400 px floor is active, the editor MUST preserve current logical-start alignment to the outer PowerSearch. | Compatibility requirement | Accepted contract; implementation pending | +| FR4 | In the `popover` presentation, when the 720 CSS px cap makes the editor narrower, PowerSearch MUST resolve one stable opening control for that open: the inner typeahead input for the add path, or the actual activated token/control for the edit path. | Owner direction plus current composition analysis | Accepted contract; implementation pending | +| FR5 | In the `popover` presentation, for a capped editor, PowerSearch MUST compare the opening control's border-box distance from the outer PowerSearch's physical left and right edges, select the nearer outer edge, and align the editor to that edge. Equal distances MUST fall back to the outer PowerSearch's logical-start edge. The result MUST depend on control geometry, not pointer coordinates or pointer/keyboard modality. | `component:PowerSearch/DEC-2` | Accepted contract; implementation pending | +| FR6 | In the `popover` presentation, opening-control identity and the selected edge MUST be latched for the open lifetime. Reopening from a control on the opposite side MUST recalculate both; movement after open MUST NOT move the editor. | Stable interaction and performance requirement | Accepted contract; implementation pending | +| FR7 | In the `popover` presentation, width and preferred-edge selection MUST still yield to the layer runtime's viewport collision handling so the editor remains horizontally visible within the runtime's existing safe area. | `architecture:layer-runtime` | Accepted contract; implementation pending | +| FR8 | In the `popover` presentation, the width and alignment change MUST NOT alter editor content, field/operator/value state, vertical placement, focus movement, selection, dismissal, or screen-reader semantics. | Compatibility and accessibility requirement | Accepted contract; implementation pending | +| FR9 | In the `popover` presentation, the opening-control descriptor owned by `component:Tokenizer/DEC-1` and consumed by PowerSearch MUST remain package-internal. PowerSearch MUST NOT add a public width, maximum-width, input-ref, or positioning prop for this behavior. | `component:Tokenizer/DEC-1`, `architecture:public-component-api`, and `component:PowerSearch/DEC-1` | Accepted contract; implementation pending | +| FR10 | `experimental.presentation` selects the surface. `popover`, the value when `experimental` or the option is omitted, is the tokenizer field with its typeahead and editor popover, unchanged except as FR19 (no mount announcement) states. `bottom-sheet` is the trigger and sheet flow of FR11–FR21 on every device. `adaptive` resolves to `bottom-sheet` on compact coarse-pointer screens and to `popover` otherwise, through the same shared resolution the selection controls use (`component:Selector/FR4`). That resolution answers `popover` on the server and during the hydrating render, so an `adaptive` field server-renders as, and hydrates without a mismatch as, the popover presentation's tokenizer field; on a compact coarse-pointer screen the first client render after hydration swaps it to the trigger. Focus follows the resolution rule below: before any interaction nothing in the field has focus and nothing moves, except that with `hasAutoFocus` the server markup's `autofocus` has focused the combobox input, and the swap then moves focus to the trigger within the same visit — the caller is told of neither a blur nor a new focus. The two closed fields share the field's wrapper, border, and minimum height for the size, and show the same tokens, but their row counts may differ: the popover field's editable tokens each carry a 16 px remove control the trigger's display-only tokens do not (FR11), so the trigger's tokens can wrap into fewer rows (seven filters in a 326 px field take seven rows editable and six as display-only tokens), and the popover field also carries its inline combobox input after the tokens, which may take a row of its own — the swap then changes the field's height by those rows (none on the measured story at 390 px, one token row at 414 px). A resolution change while the flow is open takes effect after it closes and focus has returned to the trigger (the focus the close reports, FR11) — the presentation hold `architecture:layer-runtime/INV14` assigns to the layer runtime; INV14 gap: the runtime does not yet provide one (its record names INV14 as ownership not yet met), so until it does the field keeps its own hold, to be replaced by the runtime's when it exists; when the new presentation is `popover`, focus then moves to the tokenizer's combobox input as the same visit — no `onBlur` and no second `onFocus` reaches the caller. A resolution change while the flow is closed takes effect at once; when focus is anywhere inside the closed field — the trigger, the combobox input, a token or its remove control, the clear control, or an interactive `endContent` — and the focused element does not survive the switch, focus moves to the other presentation's primary control (the combobox input, or the trigger) as a continuation of the same visit — no `onBlur` and no `onFocus` reaches the caller for the hand-off; when the focused element survives, or focus is outside the field, focus stays where it is. When the switch to the sheet presentation happens while the popover presentation's editor popover is open, the popover and its focused control unmount with the tokenizer field, and focus moves to the trigger. Focus never lands on the page body. | DEC-3; touch-flow evidence from the Lab prototype | Current contract. Implementation gap: the shipped PowerSearch has no `experimental` prop and renders the tokenizer field on every device. | +| FR11 | In the sheet presentation the field's control is one trigger: a button with `aria-haspopup="dialog"` and `aria-expanded`, whose accessible name is the field label followed by the trigger's visible text — the placeholder while no filter exists, otherwise the text of the filter tokens — so the name contains what is visible and exposes the selected filters. The tokens are display-only — each is the token's static structure (`component:Token/FR2`): no remove control, no click of its own, no inner control inside the trigger button — and the tokens, the placeholder, and the empty space around them belong to the trigger: a tap on any of them opens the flow. Tokens wrap onto further lines; the trigger does not reserve a row of its own once a token exists. The trigger's tokens are the popover presentation's tokens, the same component and the same words for a value, and a `date_absolute` value reads as the sheet's date editor shows it (FR17): a date-only value as its day in `timezoneID`, else the UTC day, and a value whose operator sets `isDateOnly: false` as its date and time in `timezoneID`, else the viewer's zone. The popover's token does not read dates this way yet (see the known gap on dates). A `components[type].Token` override renders on the trigger in place of PowerSearch's token, receiving the props the popover presentation hands it — `config`, `filter`, `field`, `operator`, `maxLength`, `isDisabled` — with `onClick` and `onRemove` undefined, as the popover hands them for a read-only filter: the trigger's tokens are display-only, and the override formats its own value. `startIcon` renders in the trigger's lane. The result count and `endContent` render in the field's end lane, outside the trigger button and after it: an interactive `endContent` keeps its own tab stop, its own taps, and its own accessible name, and a tap on it does not open the flow, so a caller's control is never nested inside the component's button and the end lane's text is not part of the trigger's name. `size` sizes the trigger field and its tokens as it sizes the tokenizer field; `statusVariant` places `status.message` as the popover presentation places it; `hasAutoFocus` focuses the trigger on mount. `onFocus` fires when focus enters the field from outside and `onBlur` when it leaves the field: opening the flow moves focus into a sheet, outside the field, so `onBlur` fires, and the close that returns focus to the trigger fires `onFocus` — as the popover presentation fires them around its editor. While disabled the trigger stays focusable with `aria-disabled="true"`, never carries the native `disabled` attribute, does not open, and shows `disabledMessage` as the field does (`spec:AST-085/FR1`, `FR2`). When the component becomes disabled with the flow open, every open sheet of the flow closes and focus moves to the trigger, the destination this record names (`spec:AST-085/FR4`). | DEC-3; Lab touch-flow evidence; `spec:AST-085`; WCAG 2.5.3 | Current contract. Implementation gap: no trigger exists. | +| FR12 | Opening the flow from a field with at least one filter, or from a read-only field, shows the Filters sheet, a tall modal sheet named "Filters". Its header — a heading and, while the field is interactive and at least one filter exists, an Add filter button at the medium size whose hit area is at least 44 px tall — and its footer — Done and, while `hasClear` is set, the field is interactive, and at least one editable filter exists, Clear all — stay fixed while only the rows between them scroll, the scroll viewport reaching the sheet's edge so its scrollbar does too, with the rows' text on the header's inset. One row per filter shows "{field label} {operator label}" (one catalog message, so a locale can reorder it) and, in bold, the value as the trigger's token shows it — the same formatter, so the same words, with the token's width budget lifted: the row never truncates a single value and never collapses a list to its "{n} items" count — with the token's summaries resolved from the catalog (`@astryx.powersearch.valueEditor.dateRange`, `itemsCount`, `filtersCount`, ICU plurals): an `integer` or `float` value reads as the token's own text (`String(value)`: `2.3456`, `1e+21`, `1e-7`, and `0` for negative zero) wherever the locale writes numbers with the token's numerals — Latin digits and a `.` decimal mark — and elsewhere with the locale's digits and decimal mark, no grouping and no rounding; a `string_list`, `enum_list`, or `entity_list` value reads as all of its items joined by the locale's narrow conjunction list (`Intl.ListFormat`, `{type: 'conjunction', style: 'narrow'}` — "Open, Closed" in English, "高、中" in Simplified Chinese and Japanese, "A, B und C" in German); a date-only `date_absolute` value reads as its day in `timezoneID`, else the UTC day, with no time, and one whose operator sets `isDateOnly: false` as its date and time in `timezoneID`, else the viewer's zone, a `date_range` as "date range", a `nested` value as "{n} filters", and a `date_relative` value as the value step's choice row reads it ("3 days ago", FR20) when a choice matches the stored value, otherwise as stored. A row's label and value are one catalog message, "{label} {value}", the same message the removal announcement of AR6 uses, with the value segment bold. A filter the field can edit is a row button that opens the value step on it, with a trailing remove action — an icon button at the medium size named "Remove {field label} filter", its hit area at least 44 px square, that commits `onChange(rest, 'remove', index)` without opening the step — except that a filter whose operator takes an `empty` value (a one-tap boolean filter) is a row whose tap removes it, with the same trailing remove action, since it has no value to edit; a read-only filter, or every filter while the field is read-only, is a plain row with no remove action, as the popover gives such a token no remove control. A `nested` filter's row is a row button like any other; its step edits the group with the popover presentation's built-in nested editor (FR14). An editable filter whose field or operator the config no longer has — which the trigger still shows as a token — lists as a row button reading the field's label when the config still has the field and the raw key for whatever is gone — "{field key} {operator key}" when both are, "{field label} {operator key}" when only the operator is — that opens the step's "This filter is no longer available" state (FR14), its trailing remove action still the removal path; it is a plain row while the filter, or the field, is read-only. With no filter the body is one empty state: a heading "No filters applied", a line of guidance, and, while the field is interactive, a centered primary Add filter action that is then the sheet's only Add filter (the header shows none), so the single thing to do is the only visible action. Done closes the flow. Clear all opens a confirmation above the sheet — a dialog titled "Clear all filters?" with "Clear filters" and "Keep filters" — and only confirming commits `onChange(kept, 'remove', )`, where `kept` is the read-only filters in their order: read-only filters are the consumer's, not the person's, so Clear all leaves them in place, as the popover presentation's Clear all does (FR23); the Filters sheet stays open for further editing and focus returns to it (FR16). After the flow closes focus returns to the trigger. | DEC-3; Lab touch-flow evidence; the read-only filter contract (`isReadOnly`, FR22); DEC-4 | Current contract. Implementation gap: no Filters sheet exists. | +| FR13 | Add filter — from the Filters sheet's Add filter action, or directly when an interactive field with no filter opens — is a tall modal sheet named "Add filter" opened above the Filters sheet, which stays open beneath it (FR15a), so the sheet has no Back action: dismissing it — Escape, a scrim tap, a swipe — reveals Filters again, or closes the flow when Add filter was the entry point, in which case its fixed footer holds Done. Its header — the heading and, when one shows, the search field — stays fixed while the rows scroll. Without a content-search field it lists the visible fields grouped by `group`, each group a named list: the ungrouped fields first, under the heading "General", then each group under its name in order of first appearance, each a row button with the field's icon, a trailing chevron — or, for a field whose only operator takes an `empty` value, a trailing plus, since choosing it adds the filter at once (FR15) — and, once the field count reaches eight, a "Search filters" field above the list that matches labels and `typeaheadAliases` case-insensitively, caps a non-empty query's matches at `maxSearchResults`, and shows the empty state "No filters match your search" when nothing matches. With a `contentSearchFieldKey` whose default operator takes a `string` value it shows instead a "Search" searchbox using `placeholder`, with the same suggestions the popover presentation's typeahead offers listed inline as row buttons ("Search results"): fields, field-and-operator pairs, value matches, and a leading free-text item for the content field; Enter commits the first suggestion, ArrowDown and ArrowUp move between the search and the rows — arrow movement across independently focusable controls, recorded here as this record's exception to `spec:AST-020/FR15`, which adopts arrow keys for a single composite only: the searchbox-to-list continuity is what the popover presentation's combobox gives the same suggestions, and the rows stay in the Tab order as well — an IME-composing Enter does nothing, and a query with no suggestion shows the empty state "No results found" — the searchbox's `aria-controls` names the results list, or that message while nothing lists, so it always names an element; the field list's "No filters match your search" likewise shows only for a non-empty query that matches nothing, never for a config with no field. Each of the two is a compact empty state with a search icon, that title, and the description "Try a different search.", mounted only while the Add filter sheet is the open one. Neither search field takes focus when the sheet opens: the sheet opens with focus on its panel and the software keyboard stays down until the person taps the search; a direct vertical touch drag on the rows while a search field has focus blurs it, so the keyboard leaves and the rows can be read. A suggestion that carries a value commits `onChange([...filters, filter], 'add', filters.length)`, announces `Added {label}` inside the Filters sheet (AR6), and shows Filters as FR15a states; any other suggestion or field row opens the value step with that field and operator, above Add filter. Every visible field lists, a field whose every operator is `nested` included: the value step edits every operator's value (FR14). | DEC-3; Lab touch-flow evidence; current typeahead source | Current contract. Implementation gap: no Add filter sheet exists. | +| FR14 | The value step is a tall modal sheet that edits every operator's value, opened above the sheet that led to it — Filters for an edit, Add filter for a creation — which stays open beneath it (FR15a); its title is the dialog's name (AR5), and for a filter whose field or operator the config no longer has it reads as the Filters row does (FR12): the field's label when the config still has the field, the raw key for whatever is gone. Its header and footer stay fixed while the body scrolls. When the field offers more than one operator the body starts with an "Operator" radio group whose options read "{field label} {operator label}" and list the field's operators, with the operator the step opened with checked — the filter's operator while editing, and while creating the operator the suggestion named (FR13) or else the field's default operator, else its first; otherwise there is no group and the title is "{field label} {operator label}" naming that operator. When no operator label applies the title is the field label. The body below the group is the editor for the checked operator's value type: the sheet-native controls of FR17, or for a `nested` value the popover presentation's built-in nested editor — the sub-filter tree with its Add action, each sub-filter's field, operator, and value controls, and removal — rendered as the popover renders it, with one difference: the step's Operator group takes the place of the editor's root group-operator picker, which is not shown, and the tree root reads the checked operator's label; a sub-filter's controls are the popover editor's own, in their default presentation (a Selector's menu, the Tokenizer's inline field), not the sheet's choice rows (FR17) or Tokenizer sheet (FR21), which govern the step's top-level value only, because the sheet passes no `experimental` option down to them; the tree is its own width container, so below 400 CSS px its rows wrap instead of overflowing the sheet, as they wrap in a narrow popover. A group is created and edited the same way in both presentations; switching the operator swaps the body. A value chosen in the body is staged, not committed: no `onChange` fires until the footer confirms it. Changing the operator keeps a staged value only when the new operator reads the same value type; an `empty` operator stages `{type: 'empty'}`. The body's sections sit on the sheet's surface when the editor is the body's only section; when the body has more than one section — the Operator group above the editor — each section is a muted container, so the grouping reads without a frame around a lone editor. The footer holds Cancel and the confirm action — `popoverSaveButtonLabel` when given, otherwise "Add filter" while creating and "Apply" while editing — disabled until the staged value is complete; the step has no Remove action: a filter is removed from its Filters row (FR12). When `components[type].Editor` is set for the checked operator's value type it replaces the body below the Operator group and the footer — the group stays outside it, so switching operators still swaps the body — receiving the props the popover presentation hands it, with the same meaning: the caller's `config`, the partial `filter`, `mode`, `saveButtonLabel`, `isReadOnly`, `onSave`, and `onCancel`, and nothing the popover does not pass; `onSave(filter)` commits it, `onSave(null)` removes the filter while editing and returns to Filters unchanged while creating. A controlled update that removes the field or operator being edited replaces the body with "This filter is no longer available", announced from its rendered element inside the sheet while the step is the open one (AR6), and keeps Cancel as the way out, the filter's trailing remove action on its Filters row being the removal path (FR12); when focus was on a control of the replaced body it moves to Cancel, so it never falls to the page body inside the open sheet (FR16). | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no value step exists. | +| FR15 | Confirming a created filter commits `onChange([...filters, filter], 'add', filters.length)`; choosing a field whose only operator takes an `empty` value commits that filter the same way from the Add filter row, with no value step. Tapping an editable Filters row opens the value step on that filter in edit mode; Apply commits `onChange(updated, 'edit', index)`; the row's trailing remove action, or a tap on an `empty`-valued filter's row, commits `onChange(rest, 'remove', index)`, where `index` is the filter's position at commit time: a controlled parent that reorders or clones its filters while the step is open is followed by object identity, then by a structurally equal filter in the original slot, then by a unique structural match; when the target is gone or ambiguous nothing is committed and the step closes back to Filters, focus landing as FR16 states for a row that no longer exists. Every commit shows the Filters sheet as FR15a states. While the field is read-only, or disabled, no commit happens. | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no edit path exists. | +| FR15a | The steps stack: each step opens above the sheet that led to it, which stays open — receded and blocked as a covered sheet of the stack (`component:BottomSheet` FR5, FR6) — so the stack is Filters alone; Filters under Add filter; Filters under the value step for an edit; Filters under Add filter under the value step for a creation; or, when Add filter was the entry point, Add filter alone and then Add filter under the value step. No sheet of the flow carries a Back action: a dismissal — Escape, a scrim tap, a swipe, a platform close request — reaches only the top sheet (`component:BottomSheet` FR7), closes it, and reveals the sheet beneath, which takes focus back as FR16 states; dismissing the bottom sheet of the stack closes the flow. Cancel on the value step is that dismissal. A commit closes the step that made it: an edit's commit reveals Filters beneath the value step; a creation's commit, from the value step or from an Add filter row or suggestion that commits directly, closes Add filter, and the value step above it, together and shows Filters, so the person sees the filter just added: Filters is revealed when it is open beneath them; after a direct entry it opens in the same update that closes them — BottomSheet's same-update replace (`component:BottomSheet` FR10) when Add filter alone closes, and the stack's ordinary closes and open (`component:BottomSheet` FR5–FR9) when the value step closes with it. A creating `Editor` override's `onSave(null)` (FR14) shows Filters the same way. The flow therefore has one direction forward (open a step above) and one back (close the top step): no dismissal replaces a step, and Filters opens as other sheets close only when a creation ends after a direct entry. | DEC-3; `component:BottomSheet` FR5–FR9 | Current contract. Implementation gap: no flow exists. | +| FR16 | Focus follows the step: after a commit from the value step while creating, the value step and Add filter close and focus lands on the Filters header's Add filter, ready for the next filter; after a content-search suggestion or an `empty`-valued field row commits a filter directly, that filter's row; after Apply or Cancel from editing, the edited filter's row — followed across a controlled reorder or clone, and, when a controlled update removed it or left two filters it could be, the nearest remaining row that is a button around its former position (the following one on a tie), else Add filter; after a row's trailing remove action, or a caller's `Editor` override's `onSave(null)` while editing, which closes the value step, the trailing remove action of the nearest remaining row that has one (the following one on a tie) — a plain row, a read-only filter's, has none and is passed over — so a run of removals needs no re-aiming, else Add filter when no such row remains; after Cancel from creation, Add filter is revealed with focus on the field row that opened the step, or on the content search when a suggestion opened it — and when a controlled update has removed that field, or the content-search field itself, the Add filter sheet's first control: its search field when one shows, else its first field row; after Add filter is dismissed, the Filters header's Add filter, or the empty state's; after a caller's `Editor` override calls `onSave(null)` while creating, the same (the flow returns to Filters unchanged, FR14); after a confirmed Clear all, the Filters sheet's first remaining control — a kept read-only filter's row has none, so Add filter — with the sheet still open; after Keep filters, Escape, or a scrim tap dismisses the confirmation, Clear all (`component:Dialog` AR2); and, as one rule for the Filters and Add filter sheets, when a controlled update unmounts the control that holds focus — a filter's row or its remove action, a dropped field's row, Clear all gone with the last editable filter — the nearest remaining row that is a button, counted among the button rows by the removed one's index (the following one, else the last) — a plain row (a read-only filter, or every filter while the field is read-only) is passed over — else the sheet's first control (the Add filter sheet's search field, Add filter in the Filters header or empty state), else the sheet's panel, so focus never lands on the page body inside the open sheet; when a controlled update replaces the value step's body with the unavailable state while focus was on one of its controls, Cancel (FR14); after the flow closes, the trigger. A sheet that opens takes focus on its panel, not on a control, except where this row names one; the Add filter sheet's search fields never take it on open (FR13). | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no flow exists. | +| FR17 | The value step edits every value type with the same editor the popover presentation uses (`nested` through its built-in nested editor, FR14), with four sheet-native differences: `enum`, `enum_list`, and `date_relative` render as full-width choice rows with a trailing indicator instead of a dropdown that would stack a second layer on the sheet — `radio` rows in a `radiogroup` named by the field label for `enum` and `date_relative` (a tap selects the row; the group is one tab stop — the checked row, else the first — and the arrow keys move focus and check the row they land on, wrapping, Home and End move to and check the first and last row, and Space or Enter checks the focused row — `spec:AST-020/FR15`), `checkbox` rows in a `group` named by the field label for `enum_list` (a tap toggles the row; each row is its own tab stop, toggled with Space or Enter), each carrying its state as `aria-checked`, as the Tokenizer's sheet rows do; `integer` and `float` stage every parseable keystroke, so the confirm action enables while the person is still typing, and unstage when the field is cleared or stops parsing, so it disables again over an empty field; a `string_list` or `entity_list` value edits through the Tokenizer's own sheet presentation (`component:Tokenizer/FR16–FR24`), its trigger showing the staged tokens in the step and its sheet titled with the field label; and a `date_absolute` value with `isDateOnly` unset or `true`, and a `date_range` value, edit through an inline Calendar on the sheet's surface — no popover opens from a sheet — reading a stored instant as its day in `timezoneID`, else the UTC day, and writing a picked day as that day's midnight in the same zone, the day the trigger's token and the Filters row show (FR11, FR12); a `date_absolute` value with `isDateOnly: false` edits through a date-and-time input, sized for touch, that reads and writes the wall-clock time in `timezoneID`, else the viewer's zone. The popover presentation's date editors do not follow these zones yet (see the known gap on dates). `maxOperatorMenuItems` caps the string and entity suggestions as in the popover presentation. | DEC-3; Lab touch-flow evidence; current value editors | Current contract. Implementation gap: no sheet editors exist. | +| FR18 | Every list in the sheets derives synchronously from the config — the field list, its search, the content-search suggestions, and the choice rows — so no sheet shows an empty state before results arrive: "No filters match your search" and "No results found" render only for a non-empty query whose synchronous result set is empty, and only while their sheet is the open one; a sheet that is open beneath another, or closed with its body retained, renders and announces no empty-result or unavailable message (AR6). The value typeaheads and tokenizers inside the value step own their own pending states. | DEC-3; `architecture:react-component-runtime/INV1` | Current contract. Implementation gap: no sheets exist. | +| FR19 | The result count renders at the end of the field in both presentations and is announced only when it changes after mount; the first render announces nothing, including under StrictMode's effect replay. The announcement takes the route `spec:AST-086/FR14` names for where the field is at that moment: inside the open sheet's dialog while a sheet of the flow is open (AR6), otherwise inside the nearest open native modal that holds the field (a Dialog, a modal Drawer), and the shared polite channel only when no modal does — never a channel the record names itself. | Current popover behavior; `architecture:react-component-runtime/INV3` | Current contract. Implementation gap: the closed field meets this row, with no announcement on mount under StrictMode and the route into the modal that holds it; no sheet exists to carry the in-sheet route. | +| FR20 | Every string the sheets introduce comes from the `@astryx.powersearch.*` catalog, reusing the existing editor keys where their meaning fits (Cancel, Apply, Operator, Value, Values, Entities, the placeholders, the result count). Content the popover presentation already renders keeps its source — field, operator, and value text from the config and its formatters — except that every label the sheets render for a generated choice comes from the catalog as one ICU message with a plural: the relative-date choice rows (`@astryx.powersearch.sheet.relative{Days,Weeks,Months}{Ago,FromNow}`, the same choices and, in English, the same words as the popover's editor lists) and the value summaries of FR12. | DEC-3; catalog contract | Current contract. Implementation gap: no sheet strings exist. | +| FR21 | A `string_list` or `entity_list` value inside the value step opens the Tokenizer's sheet above the value step, one more sheet of the same stack (FR15a). Escape, a scrim tap, and a swipe dismiss only that topmost sheet; closing it returns focus to its trigger inside the value step; the value step keeps its staged state. | DEC-3; `family:overlay-dismissal`; `component:Tokenizer/FR18` | Current contract. Implementation gap: no nested sheet exists. | +| FR22 | In the `popover` presentation, a read-only field (`isReadOnly`) MUST NOT remove a filter: it shows no Clear all, no token has a remove control, and Backspace on an empty input removes nothing. | The `isReadOnly` prop docs: "Prevent adding, editing, or removing filters." | Current contract | +| FR23 | In the `popover` presentation, Clear all MUST remove every editable filter and keep every read-only filter: it commits `onChange(, 'remove', )` and announces "Editable filters removed" while a read-only filter remains, otherwise "All filters removed". The clear control MUST render only while `hasClear` is set, the field is neither read-only nor disabled, and at least one editable filter exists. Backspace on an empty input MUST NOT remove a read-only filter; its token has no remove control. | DEC-4; a read-only filter's token has no remove control (current source) | Current contract | ### Allowed variation @@ -232,48 +244,45 @@ FR10–FR21 govern. refs, elements, or an equivalent package-internal value. Its observable geometry, lifetime, and precedence must satisfy FR3–FR9. - **AV5 — Sheet flow composition.** The sheet presentation hosts its steps as - standalone BottomSheets driven by one step state — a step that closes in the - update another opens is replaced by it in place (`component:BottomSheet` - FR10), the pattern `component:BottomSheetSwitcher` DEP-0015 names as the - deprecated switcher's replacement — or, until that replace is shipped, in a - BottomSheetSwitcher; it may compose List, RadioList, TextInput, Button, Heading, and Text in any - internal structure, and may load the whole surface on demand, provided - FR10–FR21, AR4–AR6, and PR5 hold and the composition stays within + standalone BottomSheets driven by one step state, each step's `isOpen` true + while it is open or beneath an open step (FR15a), so the stack is + `component:BottomSheet`'s own (FR5–FR9) and Filters opens as other sheets + close only where FR15a names it; it may compose List, RadioList, TextInput, + Calendar, Button, Heading, EmptyState and Text in any internal structure, and + may load the whole surface on demand, provided FR10–FR21, AR4–AR6, and PR5 + hold and the composition stays within `architecture:component-style-authoring/INV7`: one presentation mounted per - open layer, no content mounted twice, and the open flow under a coarse - pointer with at most as many elements as the popover presentation's open - editor under a fine pointer — measured on the Experimental Adaptive story at - 390×844 as `document.body.querySelectorAll('*').length`: 166 at rest and 334 - with the editor popover open under a fine pointer; 123 at rest, 296 with the - Filters sheet open, 296 with the Add filter sheet open, 318 with the value - step open on a choice-row value, 328 on a list value, and 401 with the - Tokenizer's list-value sheet open above the step under a coarse pointer — - the last by the owner exception DEC-3 records. The Tokenizer's list-value sheet opens as a - standalone sheet above the step in either composition. + open layer, no content mounted twice, and, while one sheet of the flow is + open, the open flow under a coarse pointer within the popover presentation's + open editor under a fine pointer; each sheet open beneath the top one is the + owner exception DEC-3 records with its counts, beyond the open-layer allowance + (`spec:AST-017/FR60`, `DEC-29`). The Tokenizer's list-value sheet opens as one + more sheet of the same stack above the step. - **AV6 — Sheet layout.** Sheet padding, the field-list search threshold's - visual placement, row density, and the footer's button widths may vary; the - 44px minimum size of every sheet action and the edge-to-edge rows on the + visual placement, row density, and the footer's button widths may vary; the 44 + px minimum hit area of every sheet action and the edge-to-edge rows on the body's inset are the constants. ### Representative states -| State | Required invariant | Allowed variation | -| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| Outer PowerSearch narrower than 400 px | Editor prefers 400 px and logical-start alignment to the outer surface, but yields to available viewport width. | Layer runtime may shift or shrink it to remain visible. | -| Outer PowerSearch from 400 through 720 px | Editor matches the outer surface and remains logical-start aligned. | Existing vertical collision behavior may flip. | -| Wider than 720 px; add from inner input near left | Editor is 720 px wide and prefers the outer surface's physical left edge. | Viewport collision handling may shift it. | -| Wider than 720 px; add from inner input near right | Editor is 720 px wide and prefers the outer surface's physical right edge. | Viewport collision handling may shift it. | -| Wider than 720 px; edit a token/control near either end | Editor is 720 px wide and prefers the outer edge nearest that activated control. | Custom token markup may vary; identity remains exact. | -| Wider than 720 px; opening control is equidistant | Editor is 720 px wide and uses the outer surface's logical-start edge. | Direction determines the physical start edge. | -| Pointer, keyboard, or programmatic activation | The same stable opening control produces the same preferred edge regardless of activation modality. | Focus behavior remains unchanged. | -| Reopen from a control on the opposite side | Opening-control identity and preferred edge are recalculated for the new open, then remain latched until close. | Later layout changes do not move the open editor. | -| Sheet presentation, closed | The trigger shows the tokens or placeholder, `aria-expanded="false"`; its name is the label plus the visible text. | Token wrapping and placeholder text may vary. | -| Sheet presentation, Filters | One tall modal sheet named "Filters": heading, Add filter, one row per filter, Done and Clear all. | Row count and empty state may vary. | -| Sheet presentation, Add filter | Grouped field rows, or the content search with inline suggestions; Back returns or closes. | Field search appears from eight fields. | -| Sheet presentation, value step | Title, operator radios when needed, the staged editor, Cancel and the confirm action, Remove while editing. | Editor per value type follows FR17. | -| Sheet presentation, list value | The Tokenizer's sheet opens above the step, titled with the field label, as the top sheet of the stack under its one scrim; Escape closes it alone. | One backdrop per stack, dismissal at the top sheet (`component:BottomSheet` FR6, FR7). | -| Sheet presentation, disabled while open | The flow closes; focus returns to the trigger; the trigger stays focusable. | Exit motion may vary. | -| Sheet presentation, `adaptive` resolution | The surface follows the shared compact-touch query while closed; an open flow keeps its presentation until it closes. | The query is shared, not PowerSearch-owned. | +| State | Required invariant | Allowed variation | +| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | +| Outer PowerSearch narrower than 400 px | Editor prefers 400 px and logical-start alignment to the outer surface, but yields to available viewport width. | Layer runtime may shift or shrink it to remain visible. | +| Outer PowerSearch from 400 through 720 px | Editor matches the outer surface and remains logical-start aligned. | Existing vertical collision behavior may flip. | +| Wider than 720 px; add from inner input near left | Editor is 720 px wide and prefers the outer surface's physical left edge. | Viewport collision handling may shift it. | +| Wider than 720 px; add from inner input near right | Editor is 720 px wide and prefers the outer surface's physical right edge. | Viewport collision handling may shift it. | +| Wider than 720 px; edit a token/control near either end | Editor is 720 px wide and prefers the outer edge nearest that activated control. | Custom token markup may vary; identity remains exact. | +| Wider than 720 px; opening control is equidistant | Editor is 720 px wide and uses the outer surface's logical-start edge. | Direction determines the physical start edge. | +| Pointer, keyboard, or programmatic activation | The same stable opening control produces the same preferred edge regardless of activation modality. | Focus behavior remains unchanged. | +| Reopen from a control on the opposite side | Opening-control identity and preferred edge are recalculated for the new open, then remain latched until close. | Later layout changes do not move the open editor. | +| Sheet presentation, closed | The trigger shows the tokens or placeholder, `aria-expanded="false"`; its name is the label plus the visible text. | Token wrapping and placeholder text may vary. | +| Sheet presentation, Filters | One tall modal sheet named "Filters": fixed header with Add filter, rows with trailing remove actions scrolling between it and the fixed footer's Done and Clear all; with no filter, the centered Add filter is the only action. | Row count and the empty state's guidance text may vary. | +| Sheet presentation, Add filter | Above Filters: fixed header with an unfocused search when one shows, grouped field rows (the ungrouped ones under "General") or the content search's inline suggestions, and a compact empty state when a search matches nothing; no Back — a dismissal reveals Filters. | Field search appears from eight fields. | +| Sheet presentation, value step | Above the sheet that led to it: title, operator radios when needed, the staged editor on the sheet surface (or in muted sections with the operator group), Cancel and the confirm action; no Remove. | Editor per value type follows FR17. | +| Sheet presentation, Clear all | A confirmation dialog above Filters; confirming removes the editable filters, keeps read-only ones, and leaves Filters open with focus on it. | The confirmation's wording may vary. | +| Sheet presentation, list value | The Tokenizer's sheet opens above the step, titled with the field label, as the top sheet of the stack under its one scrim; Escape closes it alone. | One backdrop per stack, dismissal at the top sheet (`component:BottomSheet` FR6, FR7). | +| Sheet presentation, disabled while open | The flow closes; focus returns to the trigger; the trigger stays focusable. | Exit motion may vary. | +| Sheet presentation, `adaptive` resolution | The surface follows the shared compact-touch query while closed; an open flow keeps its presentation until it closes. | The query is shared, not PowerSearch-owned. | ### Transformation and precedence order @@ -309,8 +318,9 @@ FR10–FR21 govern. demand while a field resolves to `bottom-sheet`, so the first press does not wait for the network, and renders nothing on the server or during the hydrating render. At rest the field renders none of the flow: its three - dialogs, rows, search field and editors are mounted only from the press until - the exit has completed and focus is back on the trigger + dialogs, rows, search fields, editors and the Clear all confirmation are + mounted only from the press until the exit has completed and focus is back + on the trigger (`architecture:component-style-authoring/INV7`); the trigger, its end lane (the result count, `endContent`) and the field chrome of FR11 render as the field does. Implementation gap: no sheet surface exists. @@ -330,72 +340,81 @@ FR10–FR21 govern. and `aria-expanded`; `status.message` and `disabledMessage` describe it; a disabled trigger uses `aria-disabled` and keeps its focus order; an interactive `endContent` is a sibling of the trigger with its own name and - tab stop, never a descendant of it. Implementation - gap: no trigger exists. + tab stop, never a descendant of it. Implementation gap: no trigger exists. - **AR5 — Sheet semantics.** Each step is a modal dialog named by its title - ("Filters", "Add filter", the value step's title); the Filters rows, field rows, and - suggestion rows are buttons named by their visible text; a choice row is a - `radio` (single choice) or a `checkbox` (`enum_list`) named by its visible - text and checked through `aria-checked`, in a `radiogroup` or `group` named - by the field label — the radio group one tab stop with arrow-key movement - that checks and Space or Enter checking the focused row, the checkbox rows - one tab stop each toggled by Space or Enter (FR17, `spec:AST-020/FR15`); + ("Filters", "Add filter", the value step's title), the open one on top of the + sheets it was opened from, which are `inert` beneath it + (`component:BottomSheet` FR6); the Filters rows, field rows, and suggestion + rows are buttons named by their visible text, and a Filters row's trailing + remove action is a button named "Remove {field label} filter" with that name + as its tooltip; the Clear all confirmation is a modal `alertdialog` named + "Clear all filters?" whose actions read "Clear filters" and "Keep filters", + with Keep filters focused when it opens, so a stray Enter keeps the filters; a + choice row is a `radio` (single choice) or a `checkbox` (`enum_list`) named by + its visible text and checked through `aria-checked`, in a `radiogroup` or + `group` named by the field label — the radio group one tab stop with arrow-key + movement that checks and Space or Enter checking the focused row, the checkbox + rows one tab stop each toggled by Space or Enter (FR17, `spec:AST-020/FR15`); the operator step is a radio group named "Operator" of native radio inputs, - which the arrow keys move and check and Space checks; the content search is a searchbox named "Search" - controlling the suggestion list; the field-list search is a textbox named - "Search filters"; every sheet action is at least 44px tall. Implementation gap: - no sheets exist. + which the arrow keys move and check and Space checks; the content search is a + searchbox named "Search" controlling the suggestion list; the field-list + search is a textbox named "Search filters"; every sheet action has a hit area + at least 44 px tall, and the trailing remove action and the header's Add + filter keep their medium visible size, their hit area meeting that minimum. + Implementation gap: no sheets exist. - **AR6 — Announcements and focus.** While a sheet is open, every announcement - the flow makes — `Added {label}` for a committed suggestion, `Removed {label}` - for a filter removed from the value step (the label is the Filters row's - text — the same "{label} {value}" catalog message the row renders, its - summaries and relative-date values resolved from the catalog as FR12 - states; the popover presentation's removal is the Tokenizer's `tokenRemoved` - with the filter's `formatFilterValue` label, "{field}: {operator} {value}" - — not the token's text: `1500` with `units: 'ms'` shows "1500" on the token - and is announced "1,500 ms" — and the sheet's is the Filters row's text, so - the two readings differ wherever FR12's row and `formatFilterValue` differ), - the result count of - FR19, the Add filter sheet's empty-result messages ("No results found", - "No filters match your search") and the value step's "This filter is no - longer available" (FR14), which are plain rendered text, not an EmptyState, - read from the rendered element and spoken once per query that matches - nothing, or once per opened filter, by the shared own-content module - (`architecture:react-component-runtime/INV10`), so nothing speaks them a - second time — is delivered by a persistent polite live region inside the - open sheet: an element that stays mounted while - its text changes and never mounts already holding the text to announce - (`spec:AST-086/FR3`), that delivers the content added inside it at its own - polite urgency (`spec:AST-086/FR4`), and that is the route an owner inside an - open native modal takes (`spec:AST-086/FR14`), so the announcement reaches - assistive technology behind the native modal dialog where a channel outside - it does not. Clear all closes the flow and announces its FR23 words ("All - filters removed", or "Editable filters removed" while a read-only filter - remains) once the sheet's dialog has finished its exit and focus is back on - the trigger — not before, since the closing dialog stays modal through its - exit and hides both its own region and the channel outside it — through the - route `spec:AST-086/FR14` names for the field's position then: the modal - that holds the field, or the shared channel when none does. A result count that changes while the flow is - closing (Clear all's commit) waits the same way and the two are spoken as one - catalog message per wording, "All filters removed. {result count}" or - "Editable filters removed. {result count}", so a locale orders - and punctuates the pair; while the flow is closed the result count takes the - same FR14 route (FR19). Focus moves as FR16 states and returns to the - trigger after the flow closes. Implementation gap: no flow exists. + the flow makes — `Added {label}` for a committed suggestion or an + `empty`-valued field row, `Removed {label}` for a filter removed by its row's + trailing remove action, by a tap on an `empty`-valued filter's row, or by a + caller's `Editor` override that calls `onSave(null)` while editing (FR14) (the + label is the Filters row's text — the same "{label} {value}" catalog message + the row renders, its summaries and relative-date values resolved from the + catalog as FR12 states; the popover presentation's removal is the Tokenizer's + `tokenRemoved` with the filter's `formatFilterValue` label, "{field}: + {operator} {value}" — not the token's text: `1500` with `units: 'ms'` shows + "1500" on the token and is announced "1,500 ms" — and the sheet's is the + Filters row's text, so the two readings differ wherever FR12's row and + `formatFilterValue` differ), the result count of FR19, the Add filter sheet's + empty-result messages ("No results found", "No filters match your search"), + each a compact EmptyState (FR13), and the value step's "This filter is no + longer available" (FR14), plain rendered text, each read from its rendered + element and spoken once per query that matches nothing, or once per opened + filter, by the shared own-content module + (`architecture:react-component-runtime/INV10`), and only while the message's + sheet is the open one — a sheet beneath another, or one closed with its body + retained, speaks nothing (FR18) — so nothing speaks them a second time, and + reopening the same stale filter speaks its message again — is delivered by a + persistent polite live region inside the open sheet: an element that stays + mounted while its text changes and never mounts already holding the text to + announce (`spec:AST-086/FR3`), that delivers the content added inside it at + its own polite urgency (`spec:AST-086/FR4`), and that is the route an owner + inside an open native modal takes (`spec:AST-086/FR14`), so the announcement + reaches assistive technology behind the native modal dialog where a channel + outside it does not. Clear all announces "Editable filters removed" while a + read-only filter remains, otherwise "All filters removed", inside the Filters + sheet once its confirmation dialog has closed — not before, since the + confirmation is the top modal until then and hides the sheet's region — and a + result count that changes in the same commit is spoken with it as one catalog + message per wording, "Editable filters removed. {result count}" or "All + filters removed. {result count}", so a locale orders and punctuates the pair; + while the flow is closed the result count takes the route `spec:AST-086/FR14` + names for the field's position (FR19). Focus moves as FR16 states and returns + to the trigger after the flow closes. Implementation gap: no flow exists. ## Design relationships -| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract | -| ----------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -------------- | ----------------------- | -| Search field and tokens | Continue presenting the structured query and its current interaction states. | Current source and public docs | Prominent | FR8 | -| Main field/search menu | Retain its exact `menuWidth` surface and current selection behavior; it does not inherit the editor's 720 px cap. | Current source and public docs | Prominent | Ownership boundary, FR9 | -| Filter-editor popover | Stay within the readable 400–720 px range before viewport clamping. | `component:PowerSearch/DEC-1` | Prominent | FR1–FR3, FR7 | -| Width-capped editor | Prefer the outer edge nearest the stable opening control, independent of activation modality. | `component:PowerSearch/DEC-2` | Prominent | FR4–FR8 | -| Keyboard-opened editor | Use the same control-geometry rule and preserve current focus behavior. | Accessibility contract | Prominent | FR5, FR8, AR1 | -| Sheet trigger | The field's control surface in the sheet presentation: display-only tokens or the placeholder; `endContent` beside it. | PowerSearch and Token source | Prominent | FR11, AR4 | -| Filters sheet | Heading, Add filter, divided edge-to-edge rows with bold values, sticky Done and Clear all. | PowerSearch source; `component:List` | Prominent | FR12, FR16 | -| Add filter sheet | Back, grouped field rows or the content search with inline suggestion rows. | PowerSearch source; `component:List` | Prominent | FR13 | -| Value step | Title, operator radios, the staged editor, Cancel, the confirm action, Remove. | PowerSearch source; current editors | Prominent | FR14, FR15, FR17 | +| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | -------------- | ----------------------- | +| Search field and tokens | Continue presenting the structured query and its current interaction states. | Current source and public docs | Prominent | FR8 | +| Main field/search menu | Retain its exact `menuWidth` surface and current selection behavior; it does not inherit the editor's 720 px cap. | Current source and public docs | Prominent | Ownership boundary, FR9 | +| Filter-editor popover | Stay within the readable 400–720 px range before viewport clamping. | `component:PowerSearch/DEC-1` | Prominent | FR1–FR3, FR7 | +| Width-capped editor | Prefer the outer edge nearest the stable opening control, independent of activation modality. | `component:PowerSearch/DEC-2` | Prominent | FR4–FR8 | +| Keyboard-opened editor | Use the same control-geometry rule and preserve current focus behavior. | Accessibility contract | Prominent | FR5, FR8, AR1 | +| Sheet trigger | The field's control surface in the sheet presentation: display-only tokens or the placeholder; `endContent` beside it. | PowerSearch and Token source | Prominent | FR11, AR4 | +| Filters sheet | Fixed header with Add filter, divided edge-to-edge rows with bold values and trailing remove actions, fixed Done and Clear all; one centered Add filter when empty. | PowerSearch source; `component:List` | Prominent | FR12, FR16 | +| Add filter sheet | Fixed header with an unfocused search when one shows; grouped field rows or the content search with inline suggestion rows; no Back. | PowerSearch source; `component:List` | Prominent | FR13, FR15a | +| Value step | Title, operator radios, the staged editor on the sheet surface or in muted sections, Cancel and the confirm action. | PowerSearch source; current editors | Prominent | FR14, FR15, FR17 | +| Clear all confirmation | A dialog above Filters; confirming keeps read-only filters and leaves Filters open. | PowerSearch source; `component:Dialog` | Prominent | FR12, AR5 | DEC-1 and DEC-2 change editor geometry only. The sheet presentation (DEC-3) adds no theming target or theme state while it is experimental @@ -435,14 +454,12 @@ target in both presentations. `entity_list` value opens inside the value step; PowerSearch passes the field label as that sheet's title and claims nothing of its rows, search, or Clear all. - `component:BottomSheet` owns the sheets' dialogs, the stack's one scrim, - gestures, keyboard accommodation, and the handoff between steps (a step that - closes in the update another opens is replaced by it); the Tokenizer's sheet - above the value step is the top sheet of that stack, so Escape, a scrim tap, - a swipe, and a platform close request reach it alone (`component:BottomSheet` - FR7), with `family:overlay-dismissal` owning the Escape and platform-close - ordering. `component:BottomSheetSwitcher` is deprecated (DEP-0015) and owns - only the shipped flow's step hosting until the steps become standalone - sheets. + gestures, keyboard accommodation, how a covered step recedes and how a + dismissed step reveals the one beneath (FR15a); the top sheet of that stack + — the value step, Add filter, or the Tokenizer's list-value sheet above the + value step — is the one Escape, a scrim tap, a swipe, and a platform close + request reach (`component:BottomSheet` FR7), with `family:overlay-dismissal` + owning the Escape and platform-close ordering. - `architecture:public-component-api/INV8` governs where `ref` lands (the root element this record promises, in both presentations); `data-testid` reaches the primary control of each presentation. PowerSearch is a composition that consumes `family:input-fields` @@ -476,16 +493,16 @@ mark implementation and focused real-Chromium verification as pending. ## Verification map -| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | -| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | -| FR1–FR3, FR7 | PowerSearch unit tests plus real-Chromium geometry assertions | 399/400, 720/721, wide outer surface, narrow viewport, LTR/RTL, collision | Removing either clamp, capping the main menu, or allowing viewport overflow fails. | `audit:PowerSearch/layout` | -| FR4–FR6, FR9 | Opening-path tests plus real-Chromium geometry assertions | Add from inner input; edit actual left/right/equidistant token/control; pointer/keyboard; reopen opposite side | Using pointer coordinates, modality, wrapper-only identity, stale identity, or public props fails. | `audit:PowerSearch/interaction` | -| PR1–PR4 | Instrumented unit tests plus first-frame real-Chromium capture | Open, close, reopen opposite side; hidden placement commit; first painted frame | Render-time reads, repeated reads, observers, pointer tracking, or a default-position flash fails. | `audit:PowerSearch/performance` | -| FR8, AR1–AR3 | Existing PowerSearch/editor suites plus focused browser checks | Pointer and keyboard edit, apply, cancel, Escape, light dismiss, focus return, selection, semantics | Focus, semantics, selection, dismissal, or reachability changes fail. | `audit:PowerSearch/accessibility` | -| FR22 | PowerSearch and Tokenizer unit tests | A read-only field with a filter: no Clear all, no token remove control, Backspace on the empty input | A read-only field removes a filter through Backspace, Clear all, or a token control. | `audit:PowerSearch/interaction` | -| FR23 | PowerSearch and Tokenizer unit tests | Clear all with a read-only filter before two editable ones; every filter read-only; Backspace with a read-only last filter; the words with and without a read-only filter left | A read-only filter is removed, Clear all reports an index other than the first removed one, Clear all shows with no editable filter, or its words say every filter went while one remains. | `audit:PowerSearch/interaction` | -| FR10–FR21, AR4–AR6, PR5 | Focused PowerSearch sheet tests with the compact-touch query stubbed both ways, plus real-engine touch evidence (implementation gap: not yet in the tree) | default popover, forced sheet, adaptive both ways (server markup and hydration as the popover field on a coarse-pointer client, then the trigger, no recoverable error, focus on the body; with `hasAutoFocus`, the autofocused input's focus ending on the trigger with no `onBlur` or `onFocus` to the caller), trigger name, Filters/Add filter/value steps, each value type, list value through the Tokenizer sheet, Clear all, read-only, disabled while open, focus after each step, focus after an `adaptive` switch from the trigger, the input, a removed control, an open editor popover, and from outside the field, a field inside an open Dialog announcing the count, Clear all, and in-sheet messages inside that Dialog and never on the page body, StrictMode result count, in-sheet live region while a sheet is open (added, removed, count, empty results), Clear all announced only after the dialog's exit, row, token and Removed text equal to the popover token's value text (UTC, Tokyo, Honolulu, unset), summaries and relative-date labels and values from the catalog, the row and Removed through one reorderable message, an unmatched relative-date value as stored, a filter the config no longer knows listed and removable, a cleared number unstaged, choice rows as radios and checkboxes with `aria-checked`, a stale read-only filter as a plain row, a removal next to a nested row focusing that row, a nested filter removed from its step, a list value joined by the locale's narrow conjunction list in English, Simplified Chinese, Japanese, and German, an empty-result message spoken once per query from its rendered element, a controlled update that removes the edited field moving focus from the replaced body to Remove and announcing the unavailable state in the sheet, Apply and Cancel after a controlled update removed the edited filter focusing the nearest row, else Add filter, a stale filter's step titled as its row reads (raw keys for what is gone, the field's label when the field remains), Cancel from creation after the opening field was removed focusing the Add filter sheet's first control, a removed focused Filters row, a removed Clear all, and a dropped focused field row each handing focus to the nearest remaining row or the sheet's first control, element counts at rest and with each of the Filters, Add filter and value sheets open, and with the Tokenizer's list-value sheet stacked above the value step, under fine and coarse pointers, nothing of the flow in the DOM at rest before a press and after the exit, a nested filter edited and created through the built-in nested editor (its sub-filter rows wrapping inside a 390 and a 414 px sheet; its root operator picker absent on a mixed field, the Operator group in its place), a nested-only field and its suggestion offered, a suggestion's operator checked over the field's default, the Operator group outside a caller's Editor override on a mixed field, a count changed during the exit spoken with Clear all's message as one catalog message after the close, a radio group's arrow keys, Home, and End moving and checking, Space and Enter checking a focused radio row and toggling a checkbox row, right-to-left layout with the row chevrons and Back glyph mirrored (`design:bidirectionality/DR4`) | A field without `experimental` changes beyond what FR19 states, a value commits before the step confirms, Clear all drops a read-only filter, the trigger carries native `disabled`, focus lands elsewhere, an announcement reaches a channel outside the modal that holds the field at that moment, or the popover bundle carries the sheets. | `audit:PowerSearch/sheet` | -| Record structure | `scripts/check-knowledge.mjs` | Component-contract schema and current-authority dependency | Missing or invalid metadata and sections fail repository validation. | `audit:PowerSearch/knowledge` | +| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- | +| FR1–FR3, FR7 | PowerSearch unit tests plus real-Chromium geometry assertions | 399/400, 720/721, wide outer surface, narrow viewport, LTR/RTL, collision | Removing either clamp, capping the main menu, or allowing viewport overflow fails. | `audit:PowerSearch/layout` | +| FR4–FR6, FR9 | Opening-path tests plus real-Chromium geometry assertions | Add from inner input; edit actual left/right/equidistant token/control; pointer/keyboard; reopen opposite side | Using pointer coordinates, modality, wrapper-only identity, stale identity, or public props fails. | `audit:PowerSearch/interaction` | +| PR1–PR4 | Instrumented unit tests plus first-frame real-Chromium capture | Open, close, reopen opposite side; hidden placement commit; first painted frame | Render-time reads, repeated reads, observers, pointer tracking, or a default-position flash fails. | `audit:PowerSearch/performance` | +| FR8, AR1–AR3 | Existing PowerSearch/editor suites plus focused browser checks | Pointer and keyboard edit, apply, cancel, Escape, light dismiss, focus return, selection, semantics | Focus, semantics, selection, dismissal, or reachability changes fail. | `audit:PowerSearch/accessibility` | +| FR22 | PowerSearch and Tokenizer unit tests | A read-only field with a filter: no Clear all, no token remove control, Backspace on the empty input | A read-only field removes a filter through Backspace, Clear all, or a token control. | `audit:PowerSearch/interaction` | +| FR23 | PowerSearch and Tokenizer unit tests | Clear all with a read-only filter before two editable ones; every filter read-only; Backspace with a read-only last filter; the words with and without a read-only filter left | A read-only filter is removed, Clear all reports an index other than the first removed one, Clear all shows with no editable filter, or its words say every filter went while one remains. | `audit:PowerSearch/interaction` | +| FR10–FR21, AR4–AR6, PR5 | Focused PowerSearch sheet tests with the compact-touch query stubbed both ways, plus real-engine touch evidence (implementation gap: not yet in the tree; the Lab prototype's suite, `packages/lab/src/MobilePowerSearch/MobilePowerSearch.test.tsx`, is the current evidence for the flow the rows describe) | default popover, forced sheet, adaptive both ways (server markup and hydration as the popover field on a coarse-pointer client, then the trigger, no recoverable error, focus on the body; with `hasAutoFocus`, the autofocused input's focus ending on the trigger with no `onBlur` or `onFocus` to the caller), trigger name, Filters/Add filter/value steps, each value type, list value through the Tokenizer sheet, Clear all, read-only, disabled while open, focus after each step, focus after an `adaptive` switch from the trigger, the input, a removed control, an open editor popover, and from outside the field, a field inside an open Dialog announcing the count, Clear all, and in-sheet messages inside that Dialog and never on the page body, StrictMode result count, in-sheet live region while a sheet is open (added, removed, count, empty results), Clear all announced only after the dialog's exit, row, token and Removed text sharing one formatter, a date-only value as its day in `timezoneID`, else the UTC day, and a date-and-time value in `timezoneID`, else the viewer's zone, on all three (UTC, Tokyo, Honolulu, unset), summaries and relative-date labels and values from the catalog, the row and Removed through one reorderable message, an unmatched relative-date value as stored, a filter the config no longer knows listed and removable, a cleared number unstaged, choice rows as radios and checkboxes with `aria-checked`, a stale read-only filter as a plain row, a removal next to a nested row focusing that row's remove action, a list value joined by the locale's narrow conjunction list in English, Simplified Chinese, Japanese, and German, an empty-result message spoken once per query from its rendered element, a controlled update that removes the edited field moving focus from the replaced body to Cancel and announcing the unavailable state in the sheet while the step is open and never from a step beneath another or retained closed, Apply and Cancel after a controlled update removed the edited filter focusing the nearest row, else Add filter, a row's trailing remove action focusing the next row's remove action, a one-tap `empty`-valued filter added from its field row and removed from its row, Clear all behind its confirmation keeping read-only filters and leaving Filters open, a dismissed step revealing the step beneath with focus restored to it, Add filter opening with its search unfocused and a touch drag on the rows blurring a focused search, a stale filter's step titled as its row reads (raw keys for what is gone, the field's label when the field remains), Cancel from creation after the opening field was removed focusing the Add filter sheet's first control, a removed focused Filters row, a removed Clear all, and a dropped focused field row each handing focus to the nearest remaining row or the sheet's first control, element counts at rest and with each of the Filters, Add filter and value sheets open, and with the Tokenizer's list-value sheet stacked above the value step, under fine and coarse pointers, nothing of the flow in the DOM at rest before a press and after the exit, a nested filter edited and created through the built-in nested editor (its sub-filter rows wrapping inside a 390 and a 414 px sheet; its root operator picker absent on a mixed field, the Operator group in its place), a nested-only field and its suggestion offered, a suggestion's operator checked over the field's default, the Operator group outside a caller's Editor override on a mixed field, a count changed during the exit spoken with Clear all's message as one catalog message after the close, a radio group's arrow keys, Home, and End moving and checking, Space and Enter checking a focused radio row and toggling a checkbox row, right-to-left layout with the row chevrons mirrored in RTL (`design:bidirectionality/DR4`) | A field without `experimental` changes beyond what FR19 states, a value commits before the step confirms, Clear all removes a read-only filter, the trigger carries native `disabled`, focus lands elsewhere, an announcement reaches a channel outside the modal that holds the field at that moment, or the popover bundle carries the sheets. | `audit:PowerSearch/sheet` | +| Record structure | `scripts/check-knowledge.mjs` | Component-contract schema and current-authority dependency | Missing or invalid metadata and sections fail repository validation. | `audit:PowerSearch/knowledge` | Current-source Chromium evidence establishes the geometry gap, not implementation completion: @@ -550,54 +567,92 @@ editor popover on every device. `adaptive` resolves through the shared compact coarse-pointer policy, so a PowerSearch, a Tokenizer, and a Selector on the same page switch together. -The sheet surface is the Lab touch flow: the whole field is one trigger with -display-only tokens; a Filters sheet lists the filters as rows with Add filter, -Done, and Clear all; Add filter lists the fields, or carries the content search -with inline suggestions; the value step stages field → operator → value and -confirms from its footer, with Remove while editing. Closed vocabularies are -rows; list values edit through the Tokenizer's sheet presentation titled with -the field label; a `nested` value uses the popover presentation's built-in -nested editor; every other value type uses the editor the popover presentation -uses, so a filter reads the same in both presentations of one field. Clear all -keeps the popover presentation's semantics (DEC-4). Rejected: switching -surfaces automatically on `pointer: coarse` without -an opt-in (changes stable behavior). Rejected: a top-level stable `presentation` -prop before the surface has consumer evidence. Rejected: a separate touch -component chosen by the caller (two components, two draft states, an import swap -per breakpoint). Measured against `architecture:component-style-authoring/INV7` on the -Experimental Adaptive story (390×844, `document.body.querySelectorAll('*')`): -fine pointer 166 at rest and 334 with the editor popover open; coarse pointer -123 at rest, 296 with the Filters sheet open, 296 with the Add filter sheet -open, 318 with the value step open on a choice-row value, 328 with it open on a -list value (its Tokenizer trigger), and 401 with the Tokenizer's list-value -sheet open above the step (FR21) — every single-sheet state stays under the -open popover's 334. The stacked state does not, and exceeds it by more than -the 5-element open-layer allowance (`spec:AST-017/FR60`, `DEC-29`), so it is -this record's owner exception to INV7's layer budget, with its counts: before -the Tokenizer's sheet opens, 328 (the value step open on a list value, the -Tokenizer's trigger inside it); after, 401 — the step's dialog stays mounted -as a covered sheet of the stack (`component:BottomSheet` FR6: receded, blocked, -its state kept so the staged value survives) and the Tokenizer's sheet adds -73 above it, its own dialog with search field, checkbox rows, footer and live -region — a second surface the popover presentation never has, since its list -value edits inline in the one editor popover. Existing elements cannot do the -job: the covered step must stay mounted to keep its staged state and take -focus back when the top sheet closes (FR21), and the top sheet is the -Tokenizer's own surface. The counts are re-measured when either surface -changes. Owner exception to INV7's surface budget, which asks that a -pointer-specific presentation be a change of attributes and styles on the same -elements: under `adaptive` the closed field swaps its elements by pointer — the -tokenizer field with its combobox input and token remove controls for one -trigger button holding display-only tokens (FR11) — because the two surfaces -differ in role (a combobox editing inline against a button opening a modal -flow), and a combobox cannot be restyled into a dialog trigger without lying -about its role. The swap renders fewer elements, not more (123 against 166 at -rest), and happens only on resolution changes (FR10); the Tokenizer's own -`adaptive` trigger records the same exception. -Rejected: sheet-only Clear all semantics (the two presentations of one field -would clear differently). Rejected: sheet-only date editors that read dates in -`timezoneID` while the popover's read them in UTC (one filter would show two -days). +The sheet surface follows the Lab touch flow of `MobilePowerSearch`, which the +owner named the target for Core (2026-11-02): the whole field is one trigger +with display-only tokens; a Filters sheet lists the filters as rows with +trailing remove actions, under a fixed header with Add filter and over a fixed +footer with Done and Clear all, and is one centered Add filter when empty; Add +filter opens above Filters and lists the fields, or carries the content search +with inline suggestions, its search unfocused until tapped; the value step opens +above the sheet that led to it, stages operator → value with the editor on the +sheet's surface, and confirms from its footer. Every step stays open beneath the +next, so a dismissal reveals the previous step and no sheet needs a Back action. +Closed vocabularies are rows; list values edit through the Tokenizer's sheet +presentation titled with the field label; a `nested` value uses the popover +presentation's built-in nested editor; a date-only value edits on an inline +Calendar in `timezoneID`, else UTC, and a value with a time of day on a +date-and-time control in `timezoneID`, else the viewer's zone; every other value +type uses the editor the popover presentation uses. Clear all asks for +confirmation, removes the filters the person can edit, keeps read-only ones, and +leaves Filters open. Core's contract differs from the prototype in places that +include these eight: a date-only value with no `timezoneID` is its UTC day, +where the prototype reads the viewer's zone; a field whose every operator is +`nested` lists, edited through the built-in nested editor, where the prototype +leaves it out; every string comes from the catalog (FR20), where the prototype +writes English; the empty results of Add filter's searches carry Core's titles +(FR13), where the prototype titles the field search's "No filters found"; Clear +all says "All filters removed" when no read-only filter remains (AR6), where the +prototype always says "Editable filters removed"; a one-tap `empty` filter added +from its field row is announced `Added {label}` (AR6), where the prototype +announces only a suggestion's; a caller's `Editor` override receives only what +the popover presentation passes it (FR14), where the prototype also passes +`timezoneID`; and the flow mounts nothing until a press (PR5), where the +prototype keeps its sheets mounted from the first render. Rejected: switching +surfaces automatically on `pointer: coarse` without an opt-in (changes stable +behavior). Rejected: steps that replace one another in place with Back actions +to return (the first Core draft and the earlier Lab flow): a dismissal then +closed the whole flow from any step, and the person re-entered Filters by its +entry point rather than finding it where they left it. Rejected: Remove in the +value step's footer (a second removal path, reached only by opening the step; +the row's trailing action removes in one tap). Rejected: focusing Add filter's +search on open (the software keyboard then covers the rows the person came to +read). Rejected: a top-level stable `presentation` prop before the surface has +consumer evidence. Rejected: a separate touch component chosen by the caller +(two components, two draft states, an import swap per breakpoint). + +Owner exception to `architecture:component-style-authoring/INV7`'s layer budget: +the flow keeps every step open beneath the next (FR15a), so with the value step +open two or three sheets are mounted at once — each a covered sheet of the stack +(`component:BottomSheet` FR6: receded, blocked, its state kept so the person +finds it where they left it) — and with the Tokenizer's list-value sheet open +above a creation's value step, four; the Clear all confirmation open above +Filters covers it the same way; each covered sheet is a whole dialog, more than +the 5-element open-layer allowance (`spec:AST-017/FR60`, `DEC-29`). Existing +elements cannot do the job: a replaced step would have to be rebuilt on return +and its staged state and scroll position recreated, and a dismissal could not +reveal it. The counts are `document.body.querySelectorAll('*').length` at +390×844. The bound AV5 measures the flow against is the popover presentation's +open editor on the Experimental Adaptive story under a fine pointer: 334, and +166 at rest. The prototype's Touch flow story under a coarse pointer reads 338 +at rest, 339 with Filters open, 340 with Add filter above Filters, 366 with a +choice-row value step above both, 429 with a list-value step above both, and 413 +with an edit step above Filters; it keeps all three sheets mounted from the +first render, so its rest count already carries them. Core's own counts for each +state of the stack are recorded here with its implementation; implementation +gap: Core has no sheet flow to measure. + +Owner exception to INV7's surface budget, which asks that a pointer-specific +presentation be a change of attributes and styles on the same elements: under +`adaptive` the closed field swaps its elements by pointer — the tokenizer field +with its combobox input and token remove controls for one trigger button holding +display-only tokens (FR11) — because the two surfaces differ in role (a combobox +editing inline against a button opening a modal flow), and a combobox cannot be +restyled into a dialog trigger without lying about its role. The swap renders +fewer elements, not more (123 against 166 at rest), and happens only on +resolution changes (FR10); the Tokenizer's own `adaptive` trigger records the +same exception. + +The sheet presentation's Clear all keeps read-only filters, which are the +consumer's, and reports `remove` at the first removed index, as the popover +presentation's Clear all does (FR23, DEC-4). Its date editors, tokens and rows +read and write a date-only `date_absolute` value, and each absolute part of a +`date_range`, as one day in one zone, its day in `timezoneID` when the prop is +set, else the UTC day; a value whose operator sets `isDateOnly: false` is an +instant with a time of day, read and written in `timezoneID`, else the viewer's +zone (FR11, FR12, FR17). The owner has ruled the same dates for the `popover` +presentation; that change to the stable presentation is outside this +experimental option (Compatibility and migration), and the known gap below lists +where the `popover` presentation's date handling still differs. ### DEC-4 — Clear all keeps read-only filters @@ -615,40 +670,40 @@ would go with the rest). ## Open questions +- Known gap between the two presentations of one field: the popover + presentation's date editor reads and writes a date-only value as the UTC day + whatever `timezoneID` is, while its token reads the device's day, where the + sheet reads and writes one day in `timezoneID`, else UTC (FR11, FR12, FR17); + and its editor for a value whose operator sets `isDateOnly: false` edits a + date only and stores midnight, where the sheet edits a time of day (FR17). One + value can therefore read differently on the popover's token and on the sheet's + trigger and rows. Bringing the popover presentation's date handling to the + sheet's is a stable change outside this experimental option (Compatibility and + migration). - Known gap, pre-existing in the popover presentation, against `architecture:internationalization/INV1a`: its token writes the list, - date-range, and nested summaries ("{n} items", "date range", "{n} filters") - as English literals, joins the items of a list value with a literal ", " - where FR12's rows use the locale's list format, and shows a relative-date - value as the stored key (`3d_ago`), and its relative-date editor writes the - choice labels ("3 days ago") as English literals. `formatFilterValue` resolves its + date-range, and nested summaries ("{n} items", "date range", "{n} filters") as + English literals, joins the items of a list value with a literal ", " where + FR12's rows use the locale's list format, and shows a relative-date value as + the stored key (`3d_ago`), and its relative-date editor writes the choice + labels ("3 days ago") as English literals. `formatFilterValue` resolves its summaries from the catalog but differs from the token in five places: it formats a `date_absolute` value with a time of day in `timezoneID` where the token shows a date only in the device's zone; it says "{n} entities" for an entity list where the token says "{n} items"; it formats an `integer` or - `float` through the locale's number format and appends the operator's - `units` where the token prints the raw number; it collapses a list to a - count when the joined text exceeds the width budget, where the token - compares the labels' total length, so the two collapse at different - lengths; and it cuts a single value at a different length and with a - different mark — the token's budget is `maxTokenLength` less the field and - operator label lengths, at least 10, with 3 characters of slack and "..." - (three periods), where `formatFilterValue` cuts at `maxTokenLength` with "…" - (the ellipsis character) — (both show a relative-date value as stored). The sheet presentation - resolves all of these from the catalog, or the locale's formats, on its own - surfaces (FR12, FR20) and shows the popover token as it is on the trigger - (FR11); changing the - token's and the editor's own text is a separate stable change for the - owner. -- Known gap, pre-existing in the popover presentation and inherited by the - sheet: the date editors store a picked day as UTC midnight, while the - tokens format `date_absolute` in the device's zone and `formatFilterValue` - in `timezoneID` or the device's zone, so in a zone offset from UTC a filter - can read as a neighbouring day on its token or in `formatFilterValue`'s - output. Changing what the editor stores changes the `unixSeconds` that - `onChange` reports and how saved filters reopen (`spec:AST-017/FR5`, - `FR7`), so the fix is a separate stable decision for the owner, not part of - DEC-3; the sheet matches the popover as it is. + `float` through the locale's number format and appends the operator's `units` + where the token prints the raw number; it collapses a list to a count when the + joined text exceeds the width budget, where the token compares the labels' + total length, so the two collapse at different lengths; and it cuts a single + value at a different length and with a different mark — the token's budget is + `maxTokenLength` less the field and operator label lengths, at least 10, with + 3 characters of slack and "..." (three periods), where `formatFilterValue` + cuts at `maxTokenLength` with "…" (the ellipsis character) — (both show a + relative-date value as stored). The sheet presentation resolves all of these + from the catalog, or the locale's formats, on its own surfaces (FR12, FR20) + and shows the popover token's text on the trigger, except a date, which FR11 + reads in `timezoneID` or its fallback zone; changing the token's and the + editor's own text is a separate stable change for the owner. - Known gap against `family:input-fields/FR10`, opened by DEC-3: the sheet presentation's trigger composes Field for the box the tokenizer field has, so PowerSearch is now a component that composes Field, which FR10 binds @@ -670,18 +725,6 @@ would go with the rest). pending list, where it stays until that decision or an amendment of the family record resolves which rule governs a composition that consumes member behavior. -- Known gap against `spec:AST-086/FR14`, a present-tense implementation gap - note as this record carries where the shipped PowerSearch does not yet meet - a requirement: FR19 routes the closed field's result count, and AR6 Clear all's - after-close message, to the modal that holds the field. On the shipped - `useAnnounce` those messages write only to the page-body regions, so a - PowerSearch inside an open Dialog or modal Drawer does not yet meet that - route; the AST-086 record lists the same gap for every owner that announces - through the shared hook, and owns its closing: `useAnnounce` delivering - inside the open Astryx modal that encloses its caller closes it for every - owner at once, PowerSearch's calls unchanged. -- Should `date_absolute` with `isDateOnly: false` get a date-and-time editor in - both presentations? Today both offer a date-only editor. - The editor geometry contract (DEC-1, DEC-2) is accepted and current; its implementation and focused real-Chromium verification remain pending.