Repository navigation
docs(spec): PowerSearch records the experimental touch sheet presentation (DEC-3) - #8000
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
|
There was a problem hiding this comment.
This amendment needs an engineering owner's decision before it can be approved: DEC-3 and its new requirements (FR10–FR21, AR4–AR6, PR5, and FR1–FR9 rescoped to the popover) have no owner approval for this head yet. Whatever that decision, these corrections are needed:
- AR6 and FR19 send
Added {label}and count changes to the page-body live region while a modal sheet is open, where screen readers cannot hear them (spec:AST-086, Platform support); name a polite region inside the open sheet instead. - Drop
family:input-fieldsand its FR10 citations, since that family excludes PowerSearch and requires the oppositerefand styling placement, and keepdata-testidon the popover's combobox input, where it is today. - FR17 and FR21 rely on Tokenizer DEC-3 and FR16–FR24, which are not on
mainyet, so this record should land after #7976.
[Automated review]
| "Search filters"; every sheet action is at least 44px tall. Implementation gap: | ||
| no sheets exist. | ||
| - **AR6 — Announcements and focus.** A suggestion that commits a filter announces | ||
| `Added {label}` through the shared polite live region; the result count follows |
There was a problem hiding this comment.
Added {label} goes through the shared page-body live region while a modal sheet is open. spec:AST-086 (Platform support) says that channel is not exposed to assistive technology while a native modal dialog is open, and modal sheets open with showModal(). Consider naming a persistent polite region inside the open sheet (the FR3 route CommandPalette uses) and using the shared channel only after the flow closes; FR19's count announcement and the sheets' empty-result messages need the same route.
| ] | ||
| modules: [] | ||
| families: [family:overlay-dismissal] | ||
| families: [family:input-fields, family:overlay-dismissal] |
There was a problem hiding this comment.
family:input-fields lists its members exhaustively and excludes PowerSearch ("higher-level compositions that consume member behavior"). Its FR10 also requires ref and data-testid on the primary semantic control and forbids root xstyle, className and style, which is the opposite of what Public concepts states. Consider dropping this family and the FR10 citations (lines 130 and 323), and resting the placement on architecture:public-component-api INV8.
| development; neither is silently dropped. `ref`, `data-testid`, `xstyle`, | ||
| `className`, and `style` reach the same elements in both presentations | ||
| (`family:input-fields/FR10`, `architecture:public-component-api/INV8`): | ||
| `ref` the root element, `data-testid` the field's control box, and the styling |
There was a problem hiding this comment.
In the shipped popover, data-testid reaches the combobox input (PowerSearch → Tokenizer → BaseTypeahead), not the control box. FR10 keeps popover unchanged and spec:AST-017 FR21 requires omitting experimental to preserve stable behavior, so this should say it stays on the combobox input in the popover presentation and name the trigger button for the sheet presentation.
| | FR14 | The value step is a tall modal `form` sheet whose title is the field label, or "{field label} {operator label}" when the field offers one operator. When the field offers more than one operator the body starts with an "Operator" radio group whose options read "{field label} {operator label}"; the default operator is checked. 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 operator's value type it replaces the body and footer, receiving `mode`, the partial filter, `saveButtonLabel`, `timezoneID`, `isReadOnly`, and `onSave`/`onCancel`; `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 shows "This filter is no longer available" with Cancel as the way out. | 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. 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 creation, Add filter in the Filters header; after Apply or Cancel from editing, the edited filter's row; after Remove, the nearest remaining editable row (the following one on a tie), 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; after Back from Add filter, the Filters header's Add filter; 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 non-`nested` value type with the same editor the popover presentation uses, with three sheet-native differences: `enum`, `enum_list`, and `date_relative` render as full-width row lists with a trailing check on the selected rows (a tap toggles the row; a selected row's name ends in ", selected") instead of a dropdown that would stack a second layer on the sheet; `integer` and `float` stage every parseable keystroke, so the confirm action enables while the person is still typing; 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 the same way the popover presentation reads and writes them. `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. | |
There was a problem hiding this comment.
|
/approve-spec 600ed09 |
600ed09 to
589b44a
Compare
There was a problem hiding this comment.
This amendment needs an engineering owner's decision before it can be approved: DEC-3 and its new requirements (FR10–FR21, AR4–AR6, PR5, and FR1–FR9 scoped to the popover) have no owner approval for this head, because the earlier /approve-spec named the previous commit. Any engineering owner, the author included, records that decision by commenting /approve-spec 589b44a1efbcf535de122c52d978a6cb096946a0 on this pull request. Whatever the decision, two corrections are needed:
- FR11 makes the field one button and its only tab stop while still rendering
endContent, which PowerSearch documents for action buttons, so say where an interactiveendContentgoes in the sheet presentation (spec:AST-055FR3 forbids nesting a caller's control inside a component-owned button). - Fix three citations: AR6, the Family bullet and
system_specscitespec:AST-086/FR10, which covers owners rehosted above a modal (FR3 and FR4 fit the in-sheet region); FR11 citesspec:AST-085/FR2where FR4 governs the focus destination; and the Sheet trigger design row namescomponent:Token, which has no record.
[Automated review]
| | 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. `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`). A resolution change while the flow is open takes effect after it closes and focus has returned to the trigger; when the new presentation is `popover`, focus then moves to the tokenizer's combobox input, so it 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 is one trigger: a button, the field's only tab stop, 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, with no remove control and no click of their own; a tap anywhere on the field opens the flow. Tokens wrap onto further lines; the trigger does not reserve a row of its own once a token exists. `startIcon`, `endContent`, and the result count render in the field as in the popover presentation. 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`). When the component becomes disabled with the flow open, the flow closes and focus returns to the trigger (`spec:AST-085/FR2`). | DEC-3; Lab touch-flow evidence; `spec:AST-085`; WCAG 2.5.3 | Current contract. Implementation gap: no trigger exists. | |
There was a problem hiding this comment.
FR11 makes the field one button and its only tab stop, opening on a tap anywhere, and still renders endContent in the field. PowerSearch documents endContent for action buttons, so such a control either nests inside the trigger, which spec:AST-055 FR3 and AR2 forbid, or becomes a second tab stop whose taps open the flow. Consider stating that endContent renders outside the trigger with its own tab stop and taps (narrowing "only tab stop" and "a tap anywhere" to the rest of the field), or limiting it to non-interactive content in the sheet presentation with a development warning.
The same row cites spec:AST-085/FR2 for closing the flow and returning focus when the field becomes disabled; FR4 is the claim that lets a record name that destination.
| of FR19, and the sheets' empty-result messages — 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`, `spec:AST-086/FR10`), so the announcement reaches |
There was a problem hiding this comment.
spec:AST-086/FR10 covers a globally hosted owner that spec:AST-003 rehosts above native modals, and AST-086's Platform support says other delivery inside native modals is outside that record. The in-sheet region fits FR3 and FR4; consider citing those (and Platform support) here, in the Family bullet at line 341, and in system_specs at line 39, instead of FR10.
| | 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, one tab stop. | PowerSearch source; `component:Token` | Prominent | FR11, AR4 | |
There was a problem hiding this comment.
No record with the id component:Token exists on main; consider naming the Token source here instead.
d19e19f to
d7ee224
Compare
589b44a to
128fc99
Compare
128fc99 to
52cb7b9
Compare
d7ee224 to
a3bbf10
Compare
52cb7b9 to
b31bddf
Compare
There was a problem hiding this comment.
This amendment needs an engineering owner's decision before it can be approved: DEC-3 and its new requirements (FR10–FR21, AR4–AR6, PR5, AV5–AV6, and FR1–FR9 scoped to the popover) have no owner approval for this head, because the earlier /approve-spec named a previous commit. Any engineering owner, the author included, records that decision by commenting /approve-spec b31bddf4f1410eab86ba1b8db759f71cd94d6b4b on this pull request. Whatever the decision, two corrections are needed:
- FR14 should hand a custom
components[type].Editorthe props the popover passes (configincluded, and no sheet-onlytimezoneID), or a custom date editor can show one filter on two different days asadaptiveswitches. - FR10 should name where focus goes when
adaptiveswitches while the closed field holds focus, so it never lands on the page body.
[Automated review]
| | 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, with no remove control and no click of their own, 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. `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. 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 the field label, the operator label, and the formatted value in bold (formatted with `timezoneID` and untruncated; a date-only `date_absolute` value reads as a day with no time, as its token does), and a footer with Done and, while `hasClear` is set, the field is interactive, and at least one filter exists, Clear all. 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. With no filter the body shows "No filters selected". Done closes the flow. Clear all commits `onChange([], 'remove', <index of the last filter>)` — the same change the popover presentation's clear control commits, read-only filters included — 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, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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. Fields whose every operator has a `nested` value are left out, with a development warning naming how many. | 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 whose title is the field label, or "{field label} {operator label}" when the field offers one operator. When the field offers more than one operator the body starts with an "Operator" radio group whose options read "{field label} {operator label}"; the default operator is checked. 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 operator's value type it replaces the body and footer, receiving `mode`, the partial filter, `saveButtonLabel`, `timezoneID`, `isReadOnly`, and `onSave`/`onCancel`; `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 shows "This filter is no longer available" with Cancel as the way out. | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no value step exists. | |
There was a problem hiding this comment.
The popover hands a custom Editor config, filter, mode, onSave, onCancel, saveButtonLabel and isReadOnly, and no timezoneID (PowerSearch.tsx lines 1004–1013); PowerSearchEditorProps marks config required. As written, the sheet would drop config and add timezoneID, so an Editor that formats dates with it shows one filter on two days as adaptive switches, the outcome DEC-3 rejects. Consider listing exactly the popover's props and leaving timezoneID to the open question on the date editors' time zone.
| | 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. `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`). A resolution change while the flow is open takes effect after it closes and focus has returned to the trigger; when the new presentation is `popover`, focus then moves to the tokenizer's combobox input, so it 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. | |
There was a problem hiding this comment.
This names a destination only for a switch while the flow is open. When the closed field holds focus (the trigger, or the combobox input with the keyboard up) and adaptive flips, for example on rotation or when a mouse is attached, the focused element is removed with no destination, so focus can land on the page body. Consider naming the destination for that case too, or holding the resolution while the field has focus.
b31bddf to
7495f99
Compare
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3, with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9, is not yet part of the current PowerSearch contract, and the only /approve-spec here names an earlier commit. Any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after that pull request. Two small corrections would tighten the new nested handling:
- FR13's content search offers the popover's "{field} {nested operator}" suggestions and opens the value step on that operator, which FR14 defines only when
components.nested.Editoris set; leave those suggestions out or state the step's create state. - FR14 titles the step "{field label} {operator label}" when the field has one non-
nestedoperator, so anestedfilter's step can name that other operator; say it takes the filter's own operator.
[Automated review]
| | 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`). A resolution change while the flow is open takes effect after it closes and focus has returned to the trigger; when the new presentation is `popover`, focus then moves to the tokenizer's combobox input. 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); 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, with no remove control and no click of their own, 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, before the token's width budget — 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: 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 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 filter the sheet cannot edit but the popover's token can remove keeps a way out: a filter whose operator has a `nested` value is a row button that opens the value step, where a caller's `components.nested.Editor` edits it as FR14 states and, without one, the step shows "This filter can't be edited here" (the sheet has no editor of its own for it, FR17), and an editable filter whose field or operator the config no longer has — which the trigger still shows as a token — lists by its raw keys as a row button that opens the step's "This filter is no longer available" state; in both, Remove is the way out, as the popover's token removal is, and both are plain rows 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 `onChange([], 'remove', <index of the last filter>)` — the same change the popover presentation's clear control commits, read-only filters included — 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, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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. Fields whose every operator has a `nested` value are left out, with a development warning naming how many. | DEC-3; Lab touch-flow evidence; current typeahead source | Current contract. Implementation gap: no Add filter sheet exists. | |
There was a problem hiding this comment.
The content search offers "the same suggestions the popover presentation's typeahead offers", including field-and-operator pairs, and the popover adds a pair for every operator, nested included. Only fields whose every operator is nested are left out, so a "{field} {nested operator}" suggestion opens the value step on a nested operator, whose create state FR14 defines only when components.nested.Editor is set. Leave those pairs out of the sheet's suggestions, or state what the step shows without the override.
| | 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, with no remove control and no click of their own, 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, before the token's width budget — 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: 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 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 filter the sheet cannot edit but the popover's token can remove keeps a way out: a filter whose operator has a `nested` value is a row button that opens the value step, where a caller's `components.nested.Editor` edits it as FR14 states and, without one, the step shows "This filter can't be edited here" (the sheet has no editor of its own for it, FR17), and an editable filter whose field or operator the config no longer has — which the trigger still shows as a token — lists by its raw keys as a row button that opens the step's "This filter is no longer available" state; in both, Remove is the way out, as the popover's token removal is, and both are plain rows 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 `onChange([], 'remove', <index of the last filter>)` — the same change the popover presentation's clear control commits, read-only filters included — 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, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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. Fields whose every operator has a `nested` value are left out, with a development warning naming how many. | 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 whose title is the field label, or "{field label} {operator label}" when the field offers one non-`nested` operator. When the field offers more than one non-`nested` operator the body starts with an "Operator" radio group whose options read "{field label} {operator label}" and list only the non-`nested` operators; the default operator is checked, or the first non-`nested` one when the default is `nested`. 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 operator's value type — `nested` included, the one type the sheet has no editor of its own for — it replaces the body and footer, 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 shows "This filter is no longer available" with Cancel as the way out. | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no value step exists. | |
There was a problem hiding this comment.
On a field with one non-nested operator, this title rule's condition is met by that operator, and the record does not say whether a nested filter's step, which FR12 opens and the caller's Editor now edits, is titled with it or with the filter's own operator. AR5 names the dialog by this title, so the literal reading announces the wrong operator. Say that a nested filter's step is titled with its own operator, or with the field label alone.
2abcbe7 to
f06ba38
Compare
ab4f1d4 to
e2bb492
Compare
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3, with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9, is not yet part of the current PowerSearch contract, and the only /approve-spec here names an earlier commit. Any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after that pull request. Whatever the decision, two corrections are needed:
- DEC-3 says every value type other than the choice rows and lists uses the popover's editor, but the popover creates and edits
nestedfilters with its built-in nested editor while the sheet cannot withoutcomponents.nested.Editor; reuse that editor, or namenestedas the exception and record the gap. - FR14 checks the default operator when creating, while FR13 opens the step on a field-and-operator suggestion's own operator, as the popover does; say the step checks the operator it opened with.
[Automated review]
| 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; every other value type uses the editor the popover presentation |
There was a problem hiding this comment.
This sentence covers nested too, but FR14 and FR17 give the sheet no editor for it, while the popover creates and edits nested filters with its built-in nested editor (PowerSearchEditPopover.tsx lines 819–880). Either reuse that editor in the value step, or name nested as the exception here and record the gap under Open questions.
| | 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, with no remove control and no click of their own, 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, before the token's width budget — 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: 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 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 filter the sheet cannot edit but the popover's token can remove keeps a way out: a filter whose operator has a `nested` value is a row button that opens the value step, where a caller's `components.nested.Editor` edits it as FR14 states and, without one, the step shows "This filter can't be edited here" (the sheet has no editor of its own for it, FR17), and an editable filter whose field or operator the config no longer has — which the trigger still shows as a token — lists by its raw keys as a row button that opens the step's "This filter is no longer available" state; in both, Remove is the way out, as the popover's token removal is, and both are plain rows 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 `onChange([], 'remove', <index of the last filter>)` — the same change the popover presentation's clear control commits, read-only filters included — 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 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 — less any suggestion whose operator the value step cannot edit here: a `nested` operator's suggestion is offered only when `components.nested.Editor` is set, and choosing it then opens the value step on that operator, creating through the override (FR14); Enter commits the first suggestion, ArrowDown and ArrowUp move between the search and the rows, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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. Fields whose every operator the value step cannot edit here — every operator `nested`, with no `components.nested.Editor` — are left out, with a development warning naming how many; with the override set they list like any other field. | 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. The operators the step can edit are every operator but `nested`, plus `nested` when `components.nested.Editor` is set. When the field offers more than one editable operator and the filter's operator is among them, the body starts with an "Operator" radio group whose options read "{field label} {operator label}" and list the editable operators, the filter's operator checked (the default operator, or the first editable one, when creating); otherwise there is no group and the title is "{field label} {operator label}" naming the filter's own operator — a `nested` filter's step on a field with one other operator names the `nested` operator, not that one. When no operator label applies the title is the field label. 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 operator's value type — `nested` included, the one type the sheet has no editor of its own for — it replaces the body and footer, 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 shows "This filter is no longer available" with Cancel as the way out. | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no value step exists. | |
There was a problem hiding this comment.
"the default operator, or the first editable one, when creating" can override the operator FR13 opens the step on for a field-and-operator suggestion, including the new {field} {nested operator} path; the popover opens on the suggestion's operator (PowerSearch.tsx lines 698–700 and 725–732). Consider: the operator the step opened with is checked, a suggestion's own operator, otherwise the default operator, else the first editable one.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3, with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9, is not yet part of the current PowerSearch contract, and the only /approve-spec here names an earlier commit. Any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after that pull request. Whatever the decision, two corrections are needed:
- FR14 now reuses the popover's nested editor, whose own field, operator and value dropdowns, inline Tokenizer and group-operator picker are not covered by FR17's choice rows or FR21's Tokenizer sheet, so please state which rules govern a sub-filter in the sheet.
- The description still says nested-only fields are left out and nested filters are plain rows, the reverse of FR12–FR14 at this head, so please update it to match the record.
[Automated review]
| | 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, with no remove control and no click of their own, 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, before the token's width budget — 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: 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 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 by its raw keys as a row button 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 `onChange([], 'remove', <index of the last filter>)` — the same change the popover presentation's clear control commits, read-only filters included — 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, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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. 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 — so 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 shows "This filter is no longer available" with Cancel as the way out. | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no value step exists. | |
There was a problem hiding this comment.
The built-in nested editor this row reuses renders the popover's own sub-filter controls: field and operator Selectors, PowerSearchValueEditor with Selector and inline Tokenizer values (PowerSearchEditPopover.tsx lines 336–372), and a group-operator Selector at the tree root whenever the field has more than one operator (lines 584–594), beside this row's own Operator group. FR17 gives choices rows "instead of a dropdown that would stack a second layer on the sheet" and FR21 sends a list value "inside the value step" to the Tokenizer's sheet, but neither says whether it reaches a sub-filter. Consider one sentence here naming which rules govern the nested editor's controls in the sheet, and that the Operator group replaces its group-operator picker.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3, with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9, is not yet part of the current PowerSearch contract, and the only /approve-spec here names an earlier commit. Any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after that pull request. Whatever the decision, FR14 needs one correction: it renders the built-in nested editor "as the popover renders it", whose root group-operator picker duplicates the value step's own Operator group on any field with more than one operator, so please say that the Operator group takes its place and add that state to the sheet verification row.
[Automated review]
| | 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, with no remove control and no click of their own, 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, before the token's width budget — 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: 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 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 by its raw keys as a row button 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 `onChange([], 'remove', <index of the last filter>)` — the same change the popover presentation's clear control commits, read-only filters included — 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, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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. 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: 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 shows "This filter is no longer available" with Cancel as the way out. | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no value step exists. | |
There was a problem hiding this comment.
This row reuses the popover's nested editor "as the popover renders it", and that editor's tree root is a group-operator Selector over every visible operator whenever the field has more than one (PowerSearchEditPopover.tsx lines 582–598), which is the same case in which this row's own "Operator" radio group appears, so the step would carry two operator controls. Consider one clause: the Operator group takes the place of the tree root's group-operator picker, whose row shows the checked operator's label; and add that state to the sheet verification row.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3, with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9, is not yet part of the current PowerSearch contract, and the only /approve-spec here names an earlier commit. Any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after that pull request. The FR14 point from the last review is resolved. Two things are worth settling with the approval: whether the record should cite architecture:public-component-api INV16, now current, and what an adaptive field renders on the server and during hydration, where the shared resolver answers popover.
[Automated review]
| | 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`). A resolution change while the flow is open takes effect after it closes and focus has returned to the trigger; when the new presentation is `popover`, focus then moves to the tokenizer's combobox input. 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); 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. | |
There was a problem hiding this comment.
adaptive resolves through the shared resolver, which answers popover on the server and in the hydrating render (hooks/useAdaptivePresentation.ts line 29, through useMediaQuery with serverDefault = false). For Selector that is invisible until the popup opens, but here the closed field itself differs by presentation, so a server-rendered page on a phone paints the tokenizer field and then swaps it for the trigger after hydration. Consider one sentence on what an adaptive field renders on the server and in the hydrating render, with a matching state in the sheet verification row.
| region: a persistent container inside the open sheet that delivers the | ||
| content added to it, the route an owner rendered inside an open native modal | ||
| takes because the shared page-body channel is not exposed there. | ||
| - `spec:AST-017/FR21`, `FR22`, and `FR27` govern the experimental boundary, its |
There was a problem hiding this comment.
architecture:public-component-api INV16, now current, governs exactly this kind of touch presentation: a presentation value keeps the fine-pointer surface on every device, every touch mode is caller-reachable, and touch behavior is the default where it serves touch users, with spec:AST-017 FR3 and DEC-23 naming that value as the restoring input. DEC-3 meets its checks while the option is experimental; consider citing INV16 here and in DEC-3 so the path to promotion is on record.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3, with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9, is not yet part of the current PowerSearch contract, and the only /approve-spec here names an earlier commit. Any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after that pull request. The server-render and INV16 points from the last review are resolved. One sentence still needs correcting either way: FR10 says nothing in the field has focus before an adaptive field swaps to the trigger after hydration, but with hasAutoFocus the server markup focuses the combobox input, so the swap moves focus to the trigger; please say so in FR10 and add that case to the verification row.
[Automated review]
| | 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 is not disturbed by that swap: the resolution rule below applies, and before any interaction nothing in the field has focus, so nothing moves. The two closed fields share the field's wrapper, border, and minimum height for the size, and lay the same tokens out in the same rows; the popover field also carries its inline combobox input after the tokens, so the field's height is the same when that input fits on the last token row and one token row taller when the input wraps onto a row of its own — the swap then shortens the field by that row. A resolution change while the flow is open takes effect after it closes and focus has returned to the trigger; when the new presentation is `popover`, focus then moves to the tokenizer's combobox input. 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); 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. | |
There was a problem hiding this comment.
With hasAutoFocus, "before any interaction nothing in the field has focus" does not hold: PowerSearch passes it through Tokenizer to the combobox input's autoFocus (BaseTypeahead.tsx line 1221), and React's server renderer writes that as an autofocus attribute, so the input is focused as the server markup loads. On a compact coarse-pointer screen the post-hydration swap then unmounts it and focus moves to the trigger under this row's closed-switch rule. Consider naming that case here and adding an adaptive field with hasAutoFocus to the verification row, which today checks only "focus on the body".
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3 (with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9) is not yet part of the current PowerSearch contract, and its new known gap would have PowerSearch compose Field while staying outside family:input-fields FR10, an exception that family record does not grant. Any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change and whether that FR10 exception is accepted; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after it. Either way, please correct the gap note: it should also name FR10 item 1, since the record keeps ref on the root element, and the coverage check's pending list is empty today, so it does not list PowerSearch; the description's "not cited" line for family:input-fields and "no record" line for component:Token are also out of date.
[Automated review]
| - 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 | ||
| even outside the family's membership. PowerSearch's stable props carry root | ||
| `xstyle`, `className`, and `style`, which both presentations apply to that | ||
| field box — FR10's `container*` target — so the styling lands where FR10 | ||
| puts it, but the props type still accepts the root names and offers no | ||
| `container*` or `control*` parts. Removing the root names is a stable API | ||
| change for existing callers (`spec:AST-017/FR5`), so the conversion is a | ||
| separate stable decision with its own migration, not part of DEC-3; the | ||
| family's coverage check lists PowerSearch as pending until then. |
There was a problem hiding this comment.
This gap names only FR10 item 2 (root styling in the props type). Item 1 of family:input-fields FR10 (docs/families/input-fields.md lines 199–205) also binds a component that composes Field: ref, id, data-*, aria-* and DOM event handlers reach the primary semantic control, yet Public concepts keeps ref on the root element (lines 146–148). Root styling routed to the Field box is the arrangement the family's DEC-7 rejects (line 516), so "lands where FR10 puts it" does not hold. And the coverage check does not list PowerSearch as pending: PENDING_FR10 in packages/core/src/Field/fieldInputFamily.test.tsx is an empty set under "Every member is converted" (lines 18–20). Consider naming item 1 here too, stating that the implementation must add PowerSearch to that pending list, and recording the exception in the family record if an owner accepts it.
| development; neither is silently dropped. `ref` reaches the root element in | ||
| both presentations, the element this record promises | ||
| (`architecture:public-component-api/INV8`); `xstyle`, `className`, and `style` |
There was a problem hiding this comment.
Once the sheet trigger composes Field (line 543), family:input-fields FR10 item 1 sends ref to the primary semantic control, the trigger button in the sheet presentation, as the stacked Tokenizer record does for its own trigger. This sentence keeps ref on the root element in both presentations and the known gap does not mention it; consider saying which rule governs ref here, or listing it in the gap.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3 (with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9) is not yet part of the current PowerSearch contract, and its third known gap would have PowerSearch compose Field while keeping a root ref and root styling props that family:input-fields FR10 does not allow. Any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change and whether that FR10 exception is accepted, which the family record would also need to carry; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after it. Either way, please correct the gap note: it should also name FR10 item 1, since the record keeps ref on the root element, and the coverage check's pending list is empty today, so it does not list PowerSearch.
[Automated review]
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3 (with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9) is not yet part of the current PowerSearch contract, and its third known gap would keep PowerSearch outside family:input-fields FR10 once the sheet trigger composes Field. Any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change and whether that FR10 exception is accepted, which the family record would also need to carry; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after it. Either way, please correct the gap note: it says the record sends aria-* and DOM handlers to the primary control, but Public concepts routes only data-testid there and today's PowerSearch forwards no id, aria-*, other data-*, hidden, inert, dir or handler, so the item-1 departure should list those props too.
[Automated review]
| two places. Item 1: `ref`, `id`, `data-*`, `aria-*`, and DOM handlers go to | ||
| the primary semantic control under FR10, while this record keeps `ref` on | ||
| the root in both presentations (the INV8 promise above) and sends only | ||
| `data-testid`, `aria-*`, and the handlers to the primary control. Items |
There was a problem hiding this comment.
This says the record sends aria-* and the handlers to the primary control, but Public concepts (lines 146–152) routes only data-testid there, and today's PowerSearch forwards no id, aria-*, other data-*, hidden, inert, dir or DOM handler at all (it destructures its props with no rest spread). Consider stating what the record does (ref on the root, data-testid on the primary control, onFocus/onBlur as FR11's field-level events, every other base prop applied nowhere) and listing those props in the item-1 departure; FR10 item 1 also names hidden, inert and dir.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3 (with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9) is not yet part of the current PowerSearch contract, and its third known gap would keep PowerSearch outside family:input-fields FR10 once the sheet trigger composes Field. Any engineering owner, the author included, can record that decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change and whether the FR10 exception is accepted, which the family record would also need to carry. Because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after it. The third known gap now states the base-prop targets correctly, and one small optional fix remains: the first known gap could also list the “, ” join that list values keep on the Filters rows and in Removed {label}.
[Automated review]
| count when the joined text exceeds the width budget, where the token | ||
| compares the labels' total length, so the two collapse at different | ||
| lengths (both show a relative-date value as stored). The sheet presentation | ||
| resolves all of these from the catalog on its own surfaces (FR12, FR20) and |
There was a problem hiding this comment.
This list leaves out the “, ” join: for a string_list, enum_list or entity_list value that lists its items, the token and formatFilterValue join them with a literal “, ” (PowerSearch.tsx lines 220, 249, 289; formatFilterValue.ts lines 130, 149, 173), and FR12's rows and AR6's Removed {label} inherit it, so another locale does not translate the separator. Consider listing it here as an inherited architecture:internationalization INV1a departure, or resolving it on the sheet's surfaces as the summaries are.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3 (with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9) is not yet part of the current PowerSearch contract, and its third known gap would keep PowerSearch outside family:input-fields FR10 once the sheet trigger composes Field. Any engineering owner, the author included, can record that decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change and whether the FR10 exception is accepted, which the family record would also need to carry. Separately, the list join added to the first known gap needs a fix whatever the decision: the unit list type joins Simplified Chinese values with no separator (“高” and “中” become “高中”), unit is the list's type rather than its style, and the rule belongs in FR12 and the verification row, where {type: 'conjunction', style: 'narrow'} would keep “Open, Closed” in English and separate values in every shipped locale. Because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after it.
[Automated review]
| a list value on a Filters row and in `Removed {label}` is joined by the | ||
| locale's list format (`Intl.ListFormat`, `unit` style, through the shared | ||
| number-and-list helper module), not the literal — and shows the popover |
There was a problem hiding this comment.
The unit list type has no separator in Simplified Chinese: new Intl.ListFormat('zh-CN', {type: 'unit'}) turns “高” and “中” into “高中” and “Bug”, “Feature” into “BugFeature”, so a two-value filter reads as one value on the Filters row and in Removed {label}; Japanese and Korean get only a space. unit is also the list's type, not its style ({style: 'unit'} throws a RangeError). Consider stating the join in FR12 (AR6 reuses its text) with {type: 'conjunction', style: 'narrow'}, which reads “Open, Closed” in English as the token does and “Bug、Feature” in zh-CN, and adding a zh-CN multi-value case to the sheet verification row.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3 (with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9) is not yet part of the current PowerSearch contract, and its third known gap would keep PowerSearch outside family:input-fields FR10 once the sheet trigger composes Field. Any engineering owner, the author included, can record that decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change and whether the FR10 exception is accepted, which the family record would also need to carry. The list join now stated in FR12 fixes the earlier run-together of Chinese values, and two small record fixes would ride well with the decision:
- FR12 calls the narrow conjunction "the one list form that keeps every item separated in every shipped locale", but the short and long conjunction forms also keep items apart in every shipped catalog; what only the narrow form does is keep English identical to the token's ", ".
- FR14's controlled-update "This filter is no longer available" state names no focus destination, so focus on a removed editor control can fall to the page body; FR16 could name one.
Because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after it.
[Automated review]
| | 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 lay the same tokens out in the same rows; the popover field also carries its inline combobox input after the tokens, so the field's height is the same when that input fits on the last token row and one token row taller when the input wraps onto a row of its own — the swap then shortens the field by that row. 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); 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, before the token's width budget — 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 that fits reads as 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), the one list form that keeps every item separated in every shipped locale, where the token joins with a literal ", "; 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 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 by its raw keys as a row button 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 `onChange([], 'remove', <index of the last filter>)` — the same change the popover presentation's clear control commits, read-only filters included — 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. | |
There was a problem hiding this comment.
“the one list form that keeps every item separated in every shipped locale” overstates it: across the 30 catalogs in packages/core/locales/, the short and long conjunction forms also keep items apart (zh-CN “高和中”, de-DE “Open und Closed”); of the forms measured, only the unit type runs them together. What only the narrow form does among the conjunctions is keep English identical to the token's “, ” (“Open, Closed” rather than “Open & Closed” or “Open and Closed”). Consider: “the conjunction form that keeps English as the token's “, ” and every item apart in every shipped locale”.
| | 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, before the token's width budget — 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 that fits reads as 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), the one list form that keeps every item separated in every shipped locale, where the token joins with a literal ", "; 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 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 by its raw keys as a row button 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 `onChange([], 'remove', <index of the last filter>)` — the same change the popover presentation's clear control commits, read-only filters included — 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, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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. 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 shows "This filter is no longer available" with Cancel as the way out. | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no value step exists. | |
There was a problem hiding this comment.
The controlled-update stale state replaces the step's body while focus may be on one of its controls (a choice row, a number field), but FR16 names no destination for this transition and AR6 lists no announcement for it, so focus can fall to the page body inside the open sheet. Consider naming the destination in FR16 (the step's Cancel or its heading) and adding the case to the verification row's focus states.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: DEC-3 and the rows it adds or rescopes (FR10–FR21, AR4–AR6, PR5, AV5–AV6, and FR1–FR9 scoped to the popover) are not yet part of the current PowerSearch contract, and the only /approve-spec here names an earlier commit. Any engineering owner, the author included, can record that decision by commenting /approve-spec <full head SHA> on this pull request, and should also say whether PowerSearch may keep the family:input-fields FR10 exception the third known gap records; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after that pull request. Three record fixes are needed whatever the decision:
- FR16 should name a focus destination for Apply or Cancel when the edited filter is gone or ambiguous, for Cancel after a controlled update removes a creating filter's field, and for a custom Editor's
onSave(null)while creating. - FR14 should give a title to the step of a filter whose field the config no longer has, for example the raw keys FR12's row shows.
- FR19's implementation-gap note should also say that today's count goes through
useAnnounce, which writes only to the page-body channel, so a field inside an open Dialog does not meet its route yet.
[Automated review]
| | 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, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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. 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. 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; 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; after Back from Add filter, the Filters header's Add filter; 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. | |
There was a problem hiding this comment.
FR15 returns the flow to the Filters sheet when Apply finds the edited filter gone or ambiguous after a controlled update, but this row sends focus "after Apply or Cancel from editing" to "the edited filter's row", which then does not exist, or is not one row; Cancel meets the same once the filter is gone. spec:AST-085 FR4 asks the record to name a destination whenever the component removes a focused control. Consider "when the edited filter is gone or ambiguous, the row now at its original position, else Add filter", with the state added to the sheet verification row.
| | 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, before the token's width budget — 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 that fits reads as 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 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 by its raw keys as a row button 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 `onChange([], 'remove', <index of the last filter>)` — the same change the popover presentation's clear control commits, read-only filters included — 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, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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. 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. | |
There was a problem hiding this comment.
For a filter whose field the config no longer has (FR12 lists it by its raw keys), neither "{field label} {operator label}" nor "the field label" exists, so the step's title, which AR5 makes the dialog's name, is unstated. Consider saying the title reads the raw keys, as the row does.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: DEC-3 and the rows it adds or rescopes (FR10–FR21, AR4–AR6, PR5, AV5–AV6, and FR1–FR9 scoped to the popover) are not yet part of the current PowerSearch contract, its third known gap keeps PowerSearch outside family:input-fields FR10 once the trigger composes Field, and the only /approve-spec here names an earlier commit. Any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change, whether the FR10 exception is accepted, and whether the closed field's pointer-driven swap needs an architecture:component-style-authoring INV7 exception; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after that pull request. Whatever the decision, these record fixes are needed:
- FR16 should name a focus destination whenever the open flow removes the focused control: Cancel after a config update dropped the field, a custom Editor's
onSave(null)while creating, a controlled update that removes the focused Filters row, field row, Clear all or Add filter, andisReadOnlyturning on (spec:AST-085FR4). - FR19's gap note should also say that the shipped count goes through
useAnnounce, which writes only to the page-body regions, so a popover field inside an open Dialog does not meet FR19 today. - FR12 should say which width budget a Filters row's value uses, since “before the token's width budget” and a list value “that fits” read differently for long values.
- PR5's “renders its trigger alone” should read “renders none of the flow”, since FR11 keeps the result count and
endContentbeside the trigger.
[Automated review]
| | 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, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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 the raw keys, "{field key} {operator key}", as the Filters row does (FR12). 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; after Back from Add filter, the Filters header's Add filter; 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. | |
There was a problem hiding this comment.
Two exits still have no destination that exists. When a controlled update removes the field of a filter being created, FR14 shows the unavailable state with Cancel, and "after Cancel from creation, the field row that opened the step" is gone from Add filter (FR13 and FR18 derive its rows from the config). A custom Editor's onSave(null) while creating returns to Filters (FR14), and this row has no case for it. spec:AST-085 FR4 asks the record to name the destination; for example, Add filter's Back action when the field row or search no longer exists, and Add filter in the Filters header after onSave(null) while creating, plus both states in the verification row.
| | 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; after Back from Add filter, the Filters header's Add filter; 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. | |
There was a problem hiding this comment.
The shipped count goes through useAnnounce, which writes only to the page-body regions (hooks/useAnnounce.ts; spec:AST-086 records the same gap), so a popover field inside an open Dialog does not meet this row's in-modal route today. Per the record's own rule, the gap note should name that as well as the StrictMode guard.
| presentation does not load the sheet flow's code; the flow's chunk loads on | ||
| 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 its trigger alone: the flow — its |
There was a problem hiding this comment.
"renders its trigger alone" reads as forbidding the end lane FR11 requires (the result count and endContent sit outside the trigger). Suggest: "At rest the field renders none of the flow: …".
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3 (with FR10–FR21, AR4–AR6, PR5, AV5–AV6 and the popover-only scope of FR1–FR9) is not yet part of the current PowerSearch contract, and its third known gap keeps PowerSearch outside family:input-fields FR10 once the sheet trigger composes Field. Any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA> on the pull request, or by naming the rows that should change and whether that FR10 exception is accepted; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after it. Either way, these record fixes are needed:
- FR16 should name where focus goes when a controlled update removes the focused control on the Filters or Add filter sheet, or a custom Editor's
onSave(null)declines a new filter, asspec:AST-085FR4 requires. - DEC-3 should record the closed field's pointer-driven element swap under
adaptiveas an INV7 exception, or say why INV7's surface budget does not apply to it. - FR13's ArrowDown and ArrowUp over the content-search row buttons need a named
spec:AST-020FR15 exception, or a stated composite role and tab stop. - FR12 should name the width budget a Filters row's value uses, FR10 should not promise the same token rows once the trigger's tokens drop their remove buttons, and the fourth known gap should not cite an AST-086 rule that does not exist.
[Automated review]
| | 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, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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; 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. | |
There was a problem hiding this comment.
FR16 still names no focus destination after a custom Editor's onSave(null) while creating (FR14 returns to Filters), or when a controlled update removes the focused control on the Filters or Add filter sheet: a filter's row, a dropped field's row, Clear all after the last filter, or the rows and Add filter once isReadOnly turns on. spec:AST-085 FR4 asks the record to name the destination; one catch-all clause would cover these, for example the nearest remaining row button in the sheet now shown, else its header action, else Done.
| 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, 318 with the value step open — the | ||
| open flow stays under the open popover, so no budget exception is recorded. |
There was a problem hiding this comment.
The counts answer INV7's element budget, but its surface budget also says “A pointer-specific presentation is a change of attributes and styles on the same elements”, and under adaptive the closed field swaps its tokenizer field and combobox input for a trigger button by pointer. Consider recording that swap as an INV7 exception here (the reason and the counts are already in this paragraph), or say why the surface budget does not apply.
| | 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 lay the same tokens out in the same rows; the popover field also carries its inline combobox input after the tokens, so the field's height is the same when that input fits on the last token row and one token row taller when the input wraps onto a row of its own — the swap then shortens the field by that row. 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); 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, before the token's width budget — 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 that fits reads as 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 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 `onChange([], 'remove', <index of the last filter>)` — the same change the popover presentation's clear control commits, read-only filters included — 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. | |
There was a problem hiding this comment.
“before the token's width budget” and “a list value that fits” read two ways: the token's function uses one budget for both string truncation and the “{n} items” collapse. Naming the budget the row passes (for example the list-collapse threshold without string truncation) would make the row, the token and Removed {label} text comparable as the verification row expects.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3 (with FR10–FR21, AR4–AR6, PR5, AV5–AV6, the popover-only scope of FR1–FR9 and its INV7 surface-budget exception) is not yet part of the current PowerSearch contract, and its third known gap keeps PowerSearch outside family:input-fields FR10 once the sheet trigger composes Field. Any engineering owner, the author included, can record that decision by commenting /approve-spec <full head SHA> for the current head, or by naming the rows that should change and whether that FR10 exception is accepted; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after it. Whatever the decision, these record fixes are needed:
- FR16's controlled-update rule should send focus to the nearest remaining row that is a button, since a read-only filter's row, and every row once
isReadOnlyturns on, is a plain row focus cannot reach. - FR13's ArrowDown and ArrowUp between the search and its suggestion buttons need a named
spec:AST-020FR15 exception, or the suggestions described as one composite with a single tab stop. - FR10 should not promise the same token rows, since the popover's editable tokens carry a 16 px Remove button the trigger's tokens drop, and the fourth known gap should not credit a rule
spec:AST-086does not state.
[Automated review]
| | 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, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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 by the removed one's index (the following one, else the last), 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. | |
There was a problem hiding this comment.
The new controlled-update rule sends focus to "the nearest remaining row by the removed one's index", but FR12 makes a read-only filter, and every filter while the field is read-only, a plain row, which is not focusable. The Remove and Apply/Cancel rules in this row pass over plain rows; this one should too (for example "the nearest remaining row that is a button, else the sheet's first control, else its panel"), and the state where isReadOnly turns on while the sheet is open belongs in the verification row. spec:AST-085 FR4 asks the record to name a destination focus can actually reach.
| | 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 lay the same tokens out in the same rows; the popover field also carries its inline combobox input after the tokens, so the field's height is the same when that input fits on the last token row and one token row taller when the input wraps onto a row of its own — the swap then shortens the field by that row. 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); 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 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 `onChange([], 'remove', <index of the last filter>)` — the same change the popover presentation's clear control commits, read-only filters included — 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, an IME-composing Enter does nothing, and a query with no suggestion shows "No results found". 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. | |
There was a problem hiding this comment.
ArrowDown and ArrowUp move focus between the searchbox and the suggestion row buttons, a set of independently focusable controls (AR5 makes each row a button). spec:AST-020 FR15 adopts arrow-key movement only for one composite control and asks the record to name any extra as an exception; consider naming it here and in AR5 with its reason, or describing the suggestions as one composite with a single tab stop.
| | 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 lay the same tokens out in the same rows; the popover field also carries its inline combobox input after the tokens, so the field's height is the same when that input fits on the last token row and one token row taller when the input wraps onto a row of its own — the swap then shortens the field by that row. 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); 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. | |
There was a problem hiding this comment.
The trigger's tokens drop the 16 px remove button each editable popover token carries (FR11; Token renders it whenever onRemove is set), so the two closed fields need not lay their tokens out in the same rows: seven of today's filters take seven rows editable and six as static tokens in a 326 px field. Consider stating the swap's layout effect as up to the rows the inline input and the remove buttons take.
| 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`, recorded as that record's own rule |
There was a problem hiding this comment.
spec:AST-086 states no rule asking a component record to record this gap; it lists the shared hook's modal route under its own Current-state impact. Consider dropping "as that record's own rule asks", or citing this record's gap-note rule (lines 64–65).
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: DEC-3 and the rows it adds or rescopes (FR10–FR21, AR4–AR6, PR5, AV5–AV6, FR1–FR9 scoped to the popover, and its INV7 surface-budget exception) are not yet part of the current PowerSearch contract, and the only /approve-spec here names an earlier commit. Any engineering owner, the author included, can record that decision by commenting /approve-spec <full head SHA> for the current head, saying at the same time whether PowerSearch may keep the family:input-fields FR10 and DEC-7 exception its third known gap records; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after that pull request. Whatever the decision, two record fixes are needed:
- AR6 should drop "as the popover's token removal names its token", since the popover announces a removal with the
formatFilterValuelabel (a number with its units, a date with a time of day), and the first known gap should list that difference. - AV5 and DEC-3 should count the Add filter sheet and the value step under the Tokenizer's list-value sheet before concluding that the open flow needs no INV7 layer exception, now that
spec:AST-017FR60 allows an open layer at most 5 extra elements under touch, each with a named job.
[Automated review]
| 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 — as the popover's token removal names its token), the result count of |
There was a problem hiding this comment.
The popover's token removal does not name the token by its text: Tokenizer announces tokenRemoved with the item label PowerSearch builds from formatFilterValue (PowerSearch.tsx lines 646–668 on main), so a date_absolute under timezoneID is spoken with a time of day and a number with its units, while this row's label reads the token's text. Consider dropping “as the popover's token removal names its token” and listing the popover's removal wording in the first known gap.
| fine pointer 166 at rest and 334 with the editor popover open; coarse pointer | ||
| 123 at rest, 296 with the Filters sheet open, 318 with the value step open — the | ||
| open flow stays under the open popover, so the layer budget needs no | ||
| exception. Owner exception to INV7's surface budget, which asks that a |
There was a problem hiding this comment.
These counts cover Filters (296) and the value step (318) but not the Add filter sheet, the flow's third open state. Consider adding its count here and in AV5, or narrowing “the open flow stays under the open popover” to the two measured sheets.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: the proposed DEC-3 (with FR10–FR21, AR4–AR6, PR5, AV5–AV6, the popover-only scope of FR1–FR9 and its INV7 surface-budget exception) is not yet part of the current PowerSearch contract, and its third known gap keeps PowerSearch outside family:input-fields FR10 and DEC-7 once the sheet trigger composes Field. Any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA> for this exact revision, or by naming the rows that should change and whether that FR10 exception is accepted; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after it. Either way, two record fixes are needed:
- AR6 should state the actual differences between the two presentations' removal announcements (matched relative dates, long single values, non-English list joins), and the first known gap should add the single-value truncation difference.
- AV5 and DEC-3 should count the value step with the Tokenizer's list-value sheet open, or point that state at the Tokenizer record's INV7 exception, before concluding that no layer exception is needed.
[Automated review]
| states; the popover presentation's token removal is announced by the | ||
| Tokenizer with the token's `formatFilterValue` label instead — "{field}: | ||
| {operator} {value}" with a `date_absolute` in `timezoneID` with its time of | ||
| day and a number with its operator's units — so the two presentations name | ||
| a removed filter as each shows it, which differs where the first known gap | ||
| says the token and `formatFilterValue` differ), the result count of |
There was a problem hiding this comment.
The popover's label is formatFilterValue's text, and the sheet's is the Filters row's, which FR12 makes differ from the token too. So the two announcements also differ where the first known gap does not say: a date_relative value a choice matches (“3 days ago” against the stored key), a string or enum value longer than maxTokenLength (formatFilterValue cuts it with “…”, the row never truncates), and a list in a locale whose narrow conjunction is not “, ” (German “A, B und C” against “A, B, C”). Suggest listing those differences here, or saying plainly that the sheet reads the row and the popover reads formatFilterValue.
| 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, and 318 with the | ||
| value step open under a coarse pointer. The Tokenizer's list-value sheet opens as a | ||
| standalone sheet above the step in either composition. |
There was a problem hiding this comment.
FR21 puts the value step with the Tokenizer's list-value sheet above it inside the open flow, and that state has no count. On the two records' own figures (318 for the value step, 47 for the Tokenizer's open sheet dialog) it would pass the 334 this proviso compares against. Consider counting it against the fine-pointer editor with the Tokenizer's inline menu open, or scoping this proviso to PowerSearch's own layers and leaving the stacked sheet to the Tokenizer record's INV7 exception.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: DEC-3 and the rows it adds or rewords (FR10–FR21, AR4–AR6, PR5, AV5–AV6, FR1–FR9 scoped to the popover, and its two INV7 owner exceptions) are not yet part of the current PowerSearch contract, and its third known gap keeps PowerSearch outside family:input-fields FR10 and DEC-7, which approving DEC-3 does not amend. Any engineering owner, the author included, can record that decision by commenting /approve-spec <full head SHA> on this pull request for the current head, or by naming the rows to change and saying whether the FR10 exception is accepted; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after that pull request. Whatever the decision, these record fixes are needed:
- Bring AV5, the list-value state's "two scrims" and the ownership and overlay-dismissal bullets in line with the now-current BottomSheet sheet stack (one scrim per stack, dismissal at the top sheet) and the deprecated BottomSheetSwitcher.
- Record the 401-element stacked state as an owner exception beyond
spec:AST-017FR60's 5-element allowance, and describe the covered value step as a covered sheet rather than INV7's transient copy. - Drop AR6's "as it displays it" (the popover announces its
formatFilterValuelabel, not its token text) and count the single-value cut among the first known gap's differences.
[Automated review]
| open popover's 334; the stacked state does not, and is recorded under the | ||
| open-layer allowance (`spec:AST-017/FR60`, `DEC-29`) as this record's INV7 | ||
| exception for the open layer: two modal layers are open at once, the value | ||
| step's dialog (held through the hand-off, as INV7's transient copy allows) |
There was a problem hiding this comment.
spec:AST-017 FR60's open-layer allowance is at most 5 more elements, and "Anything beyond this needs an owner exception with before and after counts", so 401 against 334 is an exception beyond the allowance, not under it. The parenthetical does not fit INV7 either: its transient copy is made by one Core mechanism and removed within 1 second of a transition, while FR21 keeps the value step open beneath the Tokenizer's sheet for as long as that sheet is open. Consider "recorded as this record's owner exception to INV7's open-layer budget, beyond spec:AST-017 FR60's 5-element allowance (334 fine, 401 coarse): two modal layers are open at once, the value step's dialog, which stays open beneath (FR21), and the Tokenizer's sheet …", with the same wording in AV5 (line 227).
| 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; each presentation announces a removed filter as it displays it: the |
There was a problem hiding this comment.
The popover's visible token is PowerSearchTokenValue at its own budget, while tokenRemoved reads the label formatFilterValue builds, so the popover does not announce a removal as it displays it: a 1500 value whose operator has units: 'ms' shows "1500" on the token and is announced as "1,500 ms". Consider dropping "as it displays it" and saying the popover announces the formatFilterValue label, which differs from its token where the first known gap lists.
| 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 four places: it |
There was a problem hiding this comment.
There is a fifth difference: a single value is cut at a different length and with a different mark. The token's value budget is maxTokenLength minus the field and operator label lengths, at least 10, passing up to 3 more characters before cutting with "..."; formatFilterValue cuts at maxTokenLength with "…". With the default of 40, a 38-character value of a Title field with a contains operator shows 27 characters and "..." on the token but is announced whole.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: DEC-3 and the rows it adds or rewords (FR10–FR21, AR4–AR6, PR5, AV5–AV6, FR1–FR9 scoped to the popover, and its two INV7 owner exceptions) are not yet part of the current PowerSearch contract, and its third known gap keeps PowerSearch outside family:input-fields FR10 and DEC-7, which approving DEC-3 does not amend. Any engineering owner, the author included, can record that decision by commenting /approve-spec <full head SHA> on this pull request for the current head, or by naming the rows to change and saying whether the FR10 exception is accepted; because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after that pull request. The BottomSheet-stack, owner-exception, AR6 and first-gap corrections raised on the previous head are in, and three wording fixes remain:
- AV5 (line 234) still calls the 401-element stacked state "under the open-layer allowance", which DEC-3 now records as an owner exception beyond
spec:AST-017FR60's allowance. - The first known gap (line 590) says
formatFilterValuediffers from the token "in four places" and lists five. - The list-value state (line 257) cites BottomSheet FR5 for "one scrim per stack"; the scrim rule is FR6.
[Automated review]
| 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 under the open-layer allowance DEC-3 records. The Tokenizer's list-value sheet opens as a |
There was a problem hiding this comment.
DEC-3 (lines 552–555) now says this 401-element state exceeds the open popover's 334 "by more than the 5-element open-layer allowance" and is an owner exception to INV7's layer budget, so it is not "under the open-layer allowance". spec:AST-017 FR60: "Anything beyond this needs an owner exception with before and after counts." Consider "the last by the owner exception DEC-3 records".
| 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 four places: it |
There was a problem hiding this comment.
This now lists five differences (the date_absolute time of day, "{n} entities", locale number format with units, the list-collapse threshold, and the single-value cut), so "four places" should read "five places", or drop the count.
| | 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 scrim per stack, dismissal at the top sheet (`component:BottomSheet` FR5, FR7). | |
There was a problem hiding this comment.
component:BottomSheet FR5 is the stack's open order; the one-scrim rule is FR6 ("Exactly one backdrop is visible for a stack — the bottom open sheet's"). Consider citing FR6 and FR7 here.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: DEC-3 and the rows it adds or rewords (FR10–FR21, AR4–AR6, PR5, AV5–AV6, FR1–FR9 scoped to the popover, and its two INV7 owner exceptions) are not yet part of the current PowerSearch contract, and its third known gap keeps PowerSearch outside family:input-fields FR10 and DEC-7, which approving DEC-3 does not amend. Any engineering owner, the author included, can record that decision by commenting /approve-spec <full head SHA> on this pull request for the current head, or by naming the rows to change and saying whether the FR10 exception is accepted and where it is recorded; the earlier /approve-spec names a superseded commit and does not count for this head. The three wording fixes raised on an earlier head are in and no other change is needed, but because FR17 and FR21 rely on the Tokenizer sheet proposed in #7976, this record should merge after that pull request.
[Automated review]
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: DEC-3 and the rows it adds or rewords (FR10–FR21, AR4–AR6, PR5, AV5–AV6, FR1–FR9 scoped to the popover, and its two INV7 owner exceptions) are not yet part of the current PowerSearch contract, and its third known gap keeps PowerSearch outside family:input-fields FR10 and DEC-7, which approving DEC-3 does not amend. Any engineering owner, the author included, can record that decision by commenting /approve-spec <full head SHA> on this pull request for the current head, or by naming the rows to change and saying whether the FR10 exception is accepted and where it is recorded; the earlier /approve-spec names a superseded commit and does not count for this head. The Tokenizer amendment that FR17 and FR21 rely on is now on main, so the one record change still suggested is for FR10 to cite architecture:layer-runtime INV14 for the hold that keeps an open flow's presentation until it closes, and to note that the runtime does not provide that hold yet, as Tokenizer FR16 does.
[Automated review]
| | 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); 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. | |
There was a problem hiding this comment.
The rule that a resolution change while the flow is open takes effect only after it closes is the "presentation hold" that architecture:layer-runtime INV14 gives the layer runtime ("A component that needs one of these uses the runtime's rather than building its own"), and the runtime does not provide one yet. component:Tokenizer FR16, now current, records the identical hold as an INV14 gap: "the presentation hold architecture:layer-runtime/INV14 assigns to the layer runtime; INV14 gap: the runtime does not yet provide one …, so until it does the field keeps its own hold". Adding the same clause here would record the implementation's interim hold instead of leaving it to depart from INV14 silently.
There was a problem hiding this comment.
This amendment is waiting on an engineering owner's decision: DEC-3 and the rows it adds or rewords (FR10–FR21, AR4–AR6, PR5, AV5–AV6, FR1–FR9 scoped to the popover, and its two INV7 owner exceptions) are not yet part of the current PowerSearch contract, and its third known gap keeps PowerSearch outside family:input-fields FR10 and DEC-7, which approving DEC-3 does not amend. Any engineering owner, the author included if they are one, can record that decision by commenting /approve-spec <full head SHA> on this pull request for the current revision, or by naming the rows to change and saying whether the FR10 exception is accepted and where it is recorded; the earlier /approve-spec names a superseded commit and does not count. FR10 now cites architecture:layer-runtime INV14 for the hold that keeps an open flow's presentation until it closes, as suggested last time, and no other record change is needed.
[Automated review]
|
/approve-spec 80098e1 |
Exact decision needed
Whether
PowerSearchoffers its touch surface — the whole field as one trigger with display-only filter tokens, and the filter work in a flow of tall modal bottom sheets (Filters, Add filter, value step) — and through which public boundary. The record iscomponent:PowerSearch(packages/core/src/PowerSearch/PowerSearch.spec.md,current), so a new public surface needs a record amendment before the implementation lands, following the shapecomponent:DrawerDEC-4 set forexperimental.isFullWidthOnMobileandcomponent:TokenizerDEC-3 set for its own touch sheet.This pull request targets
mainand changes onlyPowerSearch.spec.md. FR17 and FR21 citecomponent:TokenizerDEC-3 and FR16–FR24, which the Tokenizer spec pull request #7976 adds, so it lands after #7976. The cited Tokenizer numbers were re-checked against that branch's current head: FR16–FR24, AR6, PR6 and DEC-3 are unchanged in number (FR16, FR19, FR20 and AR6 changed in wording, none in a way this record relies on).Existing authority searched
component:PowerSearch(current): FR1–FR9, AR1–AR3, PR1–PR4, DEC-1, DEC-2 — all about the editor popover's width and alignment. No row admits a second presentation; the Public concepts section says no public concept is introduced. Nothing settles how a touch surface is exposed.spec:AST-017(current) FR21/FR22/FR27: an experimental prop on a stable component lives inside one optionalexperimentalobject; omitting it preserves stable behavior; declarations carry@experimental, docs carrystability: 'experimental'; changes ship as[experimental]patch Changesets; experimental CSS and tokens use experimental names, so the surface adds no stable theming target.component:DrawerDEC-4 andcomponent:TokenizerDEC-3 apply it.component:TokenizerDEC-3 (the Tokenizer spec pull request docs(spec): Tokenizer records the experimental touch sheet presentation (DEC-3) #7976):experimental.presentation,'popover' | 'bottom-sheet' | 'adaptive', one searchable modal sheet named by the label. The value step'sstring_listandentity_listeditors open that sheet; this record names it and claims nothing of its rows, search, or Clear all.component:SelectorFR4,component:MultiSelector,component:DropdownMenu:adaptiveresolves to the sheet on compact coarse-pointer screens through one shared policy; their pending default change does not reach PowerSearch.component:BottomSheet,component:BottomSheetSwitcher,family:overlay-dismissal(current): the sheets' dialogs, scrim, gestures, step handoff, and dismissal ordering stay with their owners; the amendment names them. The shared layer dismissal stack is what lets the Tokenizer's sheet take Escape before the value step does.spec:AST-085(current) FR1/FR2/FR4: disabled controls stay focusable witharia-disabled, carry no nativedisabled, and refuse activation; a component that removes a focused control names the destination in its own record — FR11 names the trigger as the destination when the flow closes because the field became disabled.spec:AST-086(current) FR3/FR4/FR14 and its Platform support section: while a native modal dialog is open the shared page-body live channel is not exposed to assistive technology, so an owner rendered inside the modal announces through a persistent polite region inside that dialog (FR14), which stays mounted (FR3) and delivers the content added to it (FR4). Every announcement the field makes follows FR14's route for where the field is at that moment — the open sheet's dialog while a sheet is open, else the nearest open modal that holds the field (a Dialog, a modal Drawer), else the shared channel (AR6, FR19); the record no longer names a channel itself, so a PowerSearch inside a Dialog announces inside that Dialog. FR10 of that record covers owners rehosted above a modal byspec:AST-003and is not cited.spec:AST-055(current) FR3: a caller's control is never nested inside a component-owned button — FR11 rendersendContentoutside the trigger, in the field's end lane, with its own tab stop and taps.component:BottomSheetis now current with its sheet stack (spec(core): BottomSheet stacks by open order and replaces on a same-update switch; BottomSheetSwitcher is deprecated #8010: one scrim per stack, dismissal at the top sheet, same-update replace) andcomponent:BottomSheetSwitcheris deprecated (DEP-0015): AV5 names standalone replacing step sheets first and the switcher only until that replace ships; the ownership bullets, the overlay-dismissal bullet and the list-value state (one scrim per stack, the Tokenizer's sheet the top sheet) are in line with it; the first known gap gains a fifth token-vs-formatFilterValuedifference (single-value truncation length and mark).architecture:public-component-apiINV8:refreaches the element the record promises — the root, in both presentations. INV16 (now current) is cited for the touch presentation: the fine-pointer surface stays reachable on every device (popover), every touch mode is caller-reachable (bottom-sheeteverywhere,adaptiveon compact coarse-pointer screens), and the caller's explicit value wins; DEC-3'spopoverdefault follows the experimental boundary (spec:AST-017/FR21) — INV16's touch default "where it serves touch users" is the stable-promotion decision, to be recorded then, so an opt-inadaptivenow is consistent with it.family:input-fieldsis not a linked family: it lists its members exhaustively and excludes PowerSearch as a composition that consumes member behavior. Its FR10 is cited once, in the third known gap: the sheet trigger composes Field, FR10 binds every component that composes Field, and PowerSearch's stable contract is the opposite placement — item 1:refon the root, onlydata-testidto the primary control, nothing else forwarded; items 2–4: rootxstyle/className/style— a stable API change that needs its own decision; the implementation (feat(core): PowerSearch touch sheet flow behind experimental.presentation #8001) adds PowerSearch to the family coverage check's pending list (this branch alone leaves that list empty), pending that decision or an amendment of the family record.data-testidstays where it is today in the popover presentation — the combobox input — and lands on the trigger button in the sheet presentation, the primary control of each.packages/core/src/PowerSearch/PowerSearch.tsx,PowerSearchProps,@astryxdesign/core/PowerSearch. The Lab prototype inpackages/lab/src/MobilePowerSearch/is the touch-flow evidence and is not touched. Open pull requests editing PowerSearch change the popover presentation; none adds a presentation option. The Lab prototype's own review findings — catalog strings, the trigger's name containing its visible text, a StrictMode-safe result-count announcement, the time zone of the date editors — are settled by FR11, FR17, FR19, and FR20 here.Decision
component:PowerSearchDEC-3 (deciderimdreamrunner, 2026-10-23):experimental.presentation,'popover' | 'bottom-sheet' | 'adaptive', inlined on the prop; no experimental hook or type is exported.popover— the value whenexperimentalor the option is omitted — is today's tokenizer field with its typeahead and editor popover, unchanged except as FR19 (no mount announcement) states;adaptiveserver-renders and hydrates as that popover field (the shared resolver answerspopoveron the server and in the hydrating render) and swaps to the trigger on a compact coarse-pointer screen in the first client render after hydration, without disturbing focus (nothing in the field has focus before interaction) — except withhasAutoFocus, where the markup'sautofocushas focused the combobox input and the swap moves focus to the trigger within the same visit, noonBlur/onFocusto the caller (the decision shared with Tokenizer; every FR10 hand-off is now worded as a continuation of one visit) — FR10 says so, and that the two closed fields share wrapper, border and minimum height and show the same tokens, but their row counts may differ — the popover's editable tokens carry a 16 px remove control the trigger's display-only tokens do not, so the trigger's tokens can wrap into fewer rows, and the popover's inline input may take a row of its own; the swap changes the height by those rows (none on the measured story at 390 px, one token row at 414 px) (Chromium: identical 86 px boxes at 390 px; 86 → 59 px at 414 px); a Compatibility bullet classifies that one as a stable[fix]outside the experimental exception (spec:AST-017/FR22) and says why it qualifies under FR5 (restores the documented contract; noonChangevalue, prop or rendered text changes; no valid consumer must change), and the verification row carries the same exception. The popover is otherwise unchanged for dates: no token or editor zone change ships with this amendment.adaptiveresolves through the shared compact coarse-pointer policy, so a PowerSearch, a Tokenizer, and a Selector on one page switch together; a resolution change while the flow is open applies after it closes (FR10).size,statusVariantandhasAutoFocusapply to the trigger as to the tokenizer field;onFocusfires when focus enters the field from outside andonBlurwhen it leaves, so opening the flow (focus moves into a sheet) firesonBlurand the close that returns focus to the trigger firesonFocus, as the popover fires them around its editor. The field's control is one button witharia-haspopup="dialog"/aria-expanded, named by the label plus its visible text (placeholder or the token text, so the selected filters reach assistive technology); tokens are display-only — acomponents[type].Tokenoverride renders on the trigger in place of PowerSearch's token with the popover's props (config,filter,field,operator,maxLength,isDisabled) andonClick/onRemoveundefined, as the popover hands them for a read-only filter, and formats its own value; and the tokens, placeholder and the space around them belong to the trigger; the result count andendContentrender in the field's end lane outside the trigger button, so an interactiveendContentkeeps its own tab stop, taps and name and never opens the flow; disabled isaria-disabledand focusable, never nativedisabled; becoming disabled while open closes the flow and moves focus to the trigger (spec:AST-085/FR4). FR10 names the focus destination for everyadaptiveswitch (now including a switch to the sheet while the popover presentation's editor popover is open: the popover unmounts with the tokenizer field and focus moves to the trigger): while the flow is open it waits for the close and the trigger's focus, then the combobox input takes over if the popover is the new presentation; while the flow is closed and focus is anywhere inside the field (trigger, combobox input, a token or its remove control, the clear control, an interactiveendContent) and the focused element does not survive the switch, focus moves to the other presentation's primary control; when it survives, or focus is outside the field, focus stays where it is. A resolution change while the flow is open applies after it closes and focus has returned to the trigger; when the new presentation ispopover, focus then moves to the combobox input, so it never lands on the page body (FR10).Intl.ListFormat,{type: 'conjunction', style: 'narrow'}— "Open, Closed" / "高、中" / "A, B und C"), a conjunction list separates every item in every shipped locale (theunittype ran Chinese items together) and the narrow style is the one that reads in English exactly as the token's ", " join does; heading, Add filter while interactive, one row per filter ("{field} {operator}" as one catalog message a locale can reorder, and in bold the value as the popover token shows it — the same function, so the same words and zone handling, before the token's width budget, with the token's summaries resolved from the catalog (@astryx.powersearch.valueEditor.dateRange,itemsCount,filtersCount; ICU plurals) so English reads as the token and other locales translate: a date as a day, adate_rangeas "date range", anestedvalue as "{n} filters", adate_relativevalue as the value step's choice row reads it — "3 days ago" — when a choice matches, otherwise as stored (any string; the token always shows it as stored); the row's label and value are the one "{label} {value}" message AR6's removal announcement also uses, value bold, so a reordering locale reorders both), read-only filters as plain rows; anestedfilter's row is a row button like any other, its step editing the group with the built-in nested editor; a stale filter's row (field or operator gone from the config, raw keys shown) opens the step's "no longer available" state with Remove as the way out, a plain row while the filter or the field is read-only; editable rows as chevron buttons; Done; and — underhasClear, interactive, with a filter present — Clear all, which commits the popover presentation's own clear change (onChange([], 'remove', lastIndex), read-only filters included) and closes the flow.aria-controlsnames the results list, or the empty-result message while nothing lists; "No filters match your search" shows only for a non-empty query that matches nothing, never for a config with no field; ArrowDown/ArrowUp between the searchbox and the suggestion rows is recorded as this record's exception tospec:AST-020/FR15(arrow keys for a single composite only), for the searchbox-to-list continuity the popover's combobox gives the same suggestions, with the rows also in the Tab order; every visible field lists (nested-only included) and the content search offers the popover's suggestions as they are, since the value step edits every operator's value; Back; grouped field rows with icons and chevrons; a "Search filters" field from eight fields, capped bymaxSearchResults; or, with a string content-search field, a "Search" searchbox carrying the popover presentation's own typeahead suggestions inline as rows, with Enter, ArrowDown/ArrowUp, an IME guard, andAdded {label}on a value suggestion. every visible field lists, nested-only fields included, since the value step edits every operator's value.onSave(null)while creating landing on Add filter; a stale filter's row and step title read the field's label when the field remains and the raw key for what is gone; a controlled update that removes the edited field or operator replaces the body with the unavailable state, announced from its rendered element inside the sheet (AR6), with Remove and Cancel as the ways out and focus moved from the replaced body to Remove, else Cancel (FR16), never the page body; edits every operator's value — anestedvalue through the popover presentation's built-in nested editor (the sub-filter tree), so a group is created and edited the same way in both presentations (DEC-3 rationale updated); the Operator group shows when the field offers more than one operator, with the operator the step opened with checked (the filter's while editing; the suggestion's, else the default, else the first while creating) and stays outside a caller'sEditoroverride so switching operators swaps the body; otherwise the title is "{field} {operator}" naming that operator, an "Operator" radio group when more than one, a staged editor, Cancel and the confirm action (popoverSaveButtonLabel, else "Add filter"/"Apply"), Remove while editing;components[type].Editorreplaces the body and receives exactly the props the popover presentation hands it —config,filter,mode,saveButtonLabel,isReadOnly,onSave,onCancel— and no sheet-onlytimezoneID, so a custom date editor reads one filter on one day in both presentations; exactonChangemetadata for add/edit/remove; identity following across controlled reorders; focus after each step named control by control (Add filter after a value-step commit; the new filter's row after a content-search suggestion commits directly; after Remove, the nearest remaining row that is a button — read-only rows, and every row while the field is read-only, are plain and passed over — else Add filter).enum/enum_list/date_relativeas choice rows:radiorows in aradiogroupnamed by the field (a tap selects) for single choice,checkboxrows in agroup(a tap toggles) forenum_list, state inaria-checkedas the Tokenizer's sheet rows carry it, with the keyboard model stated: a radio group is one tab stop (the checked row, else the first) whose arrow keys move focus and check, wrapping, and Home/End move to and check the first/last row (no native-radio comparison), Space or Enter checking the focused row (spec:AST-020/FR15, now insystem_specs); checkbox rows one tab stop each, toggled by Space or Enter; AR5 adds that the Operator group is native radio inputs the arrows move and check and Space checks (AR5 restated; no ", selected" name suffix), numbers staged as typed and unstaged when the field is cleared or stops parsing, andstring_list/entity_listthrough the Tokenizer's sheet presentation titled with the field label; dates read and written exactly as the popover's editors read and write them (UTC midnight / UTC day), so within one zone setup the sheet agrees with the popover everywhere. The pre-existing popover inconsistency — editors store UTC midnight while tokens format in the device zone andformatFilterValueintimezoneIDor the device zone, so an off-UTC zone can show a neighbouring day — is recorded under Open questions as a known gap, not a requirement (a second known gap lists exactly where the popover's text departs from the catalog, against INV1a: the token's "{n} items" / "date range" / "{n} filters" literals, its literal ", " join of list values where FR12's rows use the locale's list format, and its stored-key relative-date value (3d_ago), the relative-date editor's label literals, andformatFilterValue's four differences from the token — adate_absolutewith a time of day intimezoneIDwhere the token shows a date only in the device zone; "{n} entities" where the token says "{n} items"; locale number formatting plus the operator'sunitswhere the token prints the raw number; and a list collapsed to a count at a different length threshold (both show a relative-date value as stored); the sheet shows the popover token as is on the trigger and resolves all of these from the catalog on its own surfaces): changing what the editor stores changesonChange'sunixSecondsand how saved filters reopen (spec:AST-017/FR5,FR7), a separate stable decision for the owner. A fourth known gap, a present-tense implementation gap note as this record carries them, is againstspec:AST-086/FR14: the shippeduseAnnouncewrites only to the page-body regions, so a popover field inside an open Dialog or modal Drawer does not yet take FR19/AR6's in-modal route; the reworkeduseAnnounce(fix(core): useAnnounce delivers inside the open Astryx modal that encloses its caller #8009) closes it for every owner at once with no change to PowerSearch's calls. A third known gap, opened by DEC-3, is againstfamily:input-fields/FR10: the sheet trigger composes Field, so PowerSearch is now a Field-composed component the family's rule binds, and the record departs from FR10 in two places — item 1 (refstays on the root in both presentations, the INV8 promise; onlydata-testidis routed to the primary control, and noid,aria-*, otherdata-*,hidden,inert,diror handler is forwarded at all — the stable component destructures its props with no rest) and items 2–4 (stable rootxstyle/className/style, applied in both presentations to the field box, FR10'scontainer*target, and nocontainer*/control*parts); movingrefor removing the root names is a stable API change for existing callers (spec:AST-017/FR5), so the conversion is a separate stable decision with its own migration; the implementation (feat(core): PowerSearch touch sheet flow behind experimental.presentation #8001) adds PowerSearch to the family coverage check's pending list, where it stays until that decision or an amendment of the family record (raised with the family owner). Anestedfilter's step holds the popover presentation's built-in nested editor — the sub-filter tree with its Add action and each sub-filter's field, operator and value controls — rendered as the popover renders it except that the step's Operator group takes the place of the editor's root group-operator picker (not shown; the tree root reads the checked operator's label): the sub-filters' controls are the popover editor's own, in their default presentation (a Selector's menu, the Tokenizer's inline field), since the sheet passes noexperimentaloption down; the sheet's own choice rows (FR17) and Tokenizer sheet (FR21) apply to the step's top-level value only. FR14 records this. The Tokenizer's sheet opens above the step; Escape dismisses only it.@astryx.powersearch.*catalog key (content the popover presentation already renders — config text, formatted values, the relative-date choice labels — keeps its source).menuWidthandtokenOverflowBehaviorhave no sheet surface and warn once in development rather than being silently dropped;refreaches the root in both presentations; the styling props reach the field container in both;data-testidreaches the primary control of each — the combobox input in the popover, the trigger button in the sheet.tokenRemovedwith the filter'sformatFilterValuelabel ("{field}: {operator} {value}"), not the token's text:1500withunits: 'ms'shows "1500" and is announced "1,500 ms" — against the sheet'sRemoved {label}from the Filters row's text, so the readings differ wherever FR12's row andformatFilterValuediffer; trigger and sheet semantics (an interactiveendContentis a sibling of the trigger, never a descendant); announcements — while a sheet is open,Added {label},Removed {label}(the Filters row's text, "{field} {operator} {value}" through one catalog message with its summaries from the catalog, for a removal from the value step), the result count and the Add filter sheet's empty-result messages (plain rendered text, not an EmptyState, read and spoken once per non-matching query by the shared own-content module,architecture:react-component-runtime/INV10, so nothing speaks them twice) are delivered by a persistent polite region inside the open sheet (spec:AST-086/FR3,FR4, andFR14, the in-modal route the AST-086 record now states), never by the page-body channel a native modal hides; Clear all closes the flow and announces "All filters removed" only once the dialog's exit has finished and focus is back on the trigger, through FR14's route for the field's position then (the modal that holds the field, or the shared channel when none does) (the closing dialog stays modal through its exit, hiding both its region and the page-body channel); a result count that changes in that closing commit waits the same way and the pair is one catalog message, "All filters removed. {result count}", so a locale orders and punctuates it — and focus; and the opt-in cost rule (nothing loaded forpopover; the flow's chunk preloaded while the sheet presentation is active; nothing on the server or in the hydrating render; and the flow mounted only from the press until the exit completes and focus is back on the trigger, so at rest the field renders none of the flow while the trigger, its end lane and the field chrome render as the field does —architecture:component-style-authoring/INV7; AV5's structure proviso cites INV7 with the measured counts: Experimental Adaptive story at 390×844, fine pointer 166 at rest / 334 editor popover open, coarse pointer 123 at rest / 296 Filters open / 296 Add filter open / 318 value step open (choice rows) / 328 (list value) / 401 with the Tokenizer's list-value sheet stacked above the step — the stacked state exceeds the popover's 334 by more than FR60's 5-element open-layer allowance (spec:AST-017/FR60,DEC-29) and is recorded as an owner exception with before/after counts — 328 before the Tokenizer's sheet opens, 401 after, the value step staying mounted as a covered sheet of the stack (component:BottomSheetFR6) under the Tokenizer's sheet — the open flow under the open popover, so the layer budget needs no exception; DEC-3 records an owner exception to INV7's surface budget foradaptive, whose closed field swaps a combobox field for a trigger button by pointer because the two differ in role — fewer elements, 123 vs 166, only on a resolution change).design:bidirectionality/DR4),system_specsgainsspec:AST-017/FR21,spec:AST-017/FR27,spec:AST-020/FR15,spec:AST-085/FR1,spec:AST-085/FR2,spec:AST-085/FR4,spec:AST-086/FR3,spec:AST-086/FR4,spec:AST-086/FR14, and the open questions (two known gaps and the remaining open items). The overlay-dismissal Family bullet says DEC-1/DEC-2 change no dismissal behavior and DEC-3's sheets take the family's dismissal paths throughcomponent:BottomSheet, pointing by content at the "Does not own" bullet on the sheets' dialog, scrim, gestures, snap behavior, keyboard accommodation, and dismissal paths.popoverpresentation, since the editor popover they describe does not render in the sheet presentation.Every row main does not meet carries a present-tense "Implementation gap" note; the implementation pull request deletes those notes and adds its test file to
verified_by.Rejected alternatives
pointer: coarsewith no opt-in: changes stable behavior for every existing caller, whichspec:AST-017FR21 forbids outside an experimental boundary.presentationprop, matching the selectors today: the surface has no consumer evidence yet, and a stable prop cannot be changed or removed in a patch.date_absoluteanddate_rangeintimezoneIDwhile the popover's read them in UTC (the Lab prototype's behavior): one filter would show two different days asadaptiveswitches; parity stands, and the time zone of both editors is an open question.Each is one
Rejected:line inside DEC-3.Claim scope and non-goals
The record governs the experimental option, its resolution, and the observable behavior of the trigger and the three sheets listed above. It does not decide the stable default (whether
adaptiveever becomes PowerSearch's default follows the selection controls' own lifecycle), the sheets' visual tuning, BottomSheet's or BottomSheetSwitcher's dialog and gesture contract, the shared compact-touch query, the Tokenizer sheet's behavior, a date-and-time editor forisDateOnly: false, or the time zone of the date editors (left open).Evidence and implementation relationship
onChangemetadata, focus after each step, identity following across controlled reorders.adaptivetests stub the compact-touch query both ways; the implementation's presentation tests mirror them.spec:AST-086's Platform support section and the modal-aware announce helper's tests (the shared channel outside a modal, the dialog's own region inside one); the implementation's tests assert the in-sheet region and an empty page-body channel while a sheet is open.endContentevidence: the implementation's test renders an action button inendContent, asserts it is not a descendant of the trigger, has its own tab stop, fires its own click without opening the flow, and is absent from the trigger's name.dir="rtl"and asserts the shared mirror style on every row chevron and the Back glyph; Chromium withdir="rtl"shows each withscaleX(-1).endContent, which each presentation mounts anew → the primary control), while it does not (focus stays outside), and while the flow is open (the latch).Removed …announcement carry exactly that text — for a boundary date undertimezoneIDUTC, Tokyo, Honolulu and unset, and for astring_list, anintegerand adate_range; the editor test asserts the UTC day as the popover reads it.:modal.onChange([], 'remove', lastIndex)through the Tokenizer's clear change (current source andPowerSearch.test.tsx).feat/powersearch-experimental-bottom-sheet, targeting the Tokenizer implementation branch) addsexperimental.presentation, the sheet flow, i18n keys, docs, stories, tests, and the[experimental]Changeset, and deletes the gap notes.Writing rubric results
component:Tokenis now a current record and FR11 cites its FR2 for the trigger's display-only tokens (the static structure: no inner control inside the trigger button).Rejected:entries.#+ digits,/pull/,/issues/; the file had none before and has none now).Scope
component:BottomSheetandfamily:overlay-dismissaldepend on them; the Tokenizer's sheet presentation is named because the list-value editor opens it andcomponent:Tokenizerowns it.component:Tokenizer).Validation
pnpm check:knowledge— "Knowledge templates and records are aligned."packages/core/src/PowerSearch/PowerSearch.spec.mdformatted.git diff --check— clean.component:Tokenizer/DEC-3,FR16–FR24,FR18exist with those numbers.