Skip to content

docs(spec): PowerSearch records the experimental touch sheet presentation (DEC-3) - #8000

Merged
imdreamrunner merged 1 commit into
mainfrom
spec/powersearch-experimental-presentation
Oct 26, 2026
Merged

imdreamrunner merged 1 commit into
mainfrom
spec/powersearch-experimental-presentation

Conversation

@imdreamrunner

@imdreamrunner imdreamrunner commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Exact decision needed

Whether PowerSearch offers 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 is component:PowerSearch (packages/core/src/PowerSearch/PowerSearch.spec.md, current), so a new public surface needs a record amendment before the implementation lands, following the shape component:Drawer DEC-4 set for experimental.isFullWidthOnMobile and component:Tokenizer DEC-3 set for its own touch sheet.

This pull request targets main and changes only PowerSearch.spec.md. FR17 and FR21 cite component:Tokenizer DEC-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 optional experimental object; omitting it preserves stable behavior; declarations carry @experimental, docs carry stability: 'experimental'; changes ship as [experimental] patch Changesets; experimental CSS and tokens use experimental names, so the surface adds no stable theming target. component:Drawer DEC-4 and component:Tokenizer DEC-3 apply it.
  • component:Tokenizer DEC-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's string_list and entity_list editors open that sheet; this record names it and claims nothing of its rows, search, or Clear all.
  • component:Selector FR4, component:MultiSelector, component:DropdownMenu: adaptive resolves 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 with aria-disabled, carry no native disabled, 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 by spec:AST-003 and is not cited.
  • spec:AST-055 (current) FR3: a caller's control is never nested inside a component-owned button — FR11 renders endContent outside the trigger, in the field's end lane, with its own tab stop and taps.
  • component:BottomSheet is 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) and component:BottomSheetSwitcher is 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-formatFilterValue difference (single-value truncation length and mark).
  • architecture:public-component-api INV8: ref reaches 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-sheet everywhere, adaptive on compact coarse-pointer screens), and the caller's explicit value wins; DEC-3's popover default 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-in adaptive now is consistent with it. family:input-fields is 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: ref on the root, only data-testid to the primary control, nothing else forwarded; items 2–4: root xstyle/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-testid stays 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.
  • Affected paths and symbols: packages/core/src/PowerSearch/PowerSearch.tsx, PowerSearchProps, @astryxdesign/core/PowerSearch. The Lab prototype in packages/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:PowerSearch DEC-3 (decider imdreamrunner, 2026-10-23):

  • Surface: experimental.presentation, 'popover' | 'bottom-sheet' | 'adaptive', inlined on the prop; no experimental hook or type is exported. popover — the value when experimental or the option is omitted — is today's tokenizer field with its typeahead and editor popover, unchanged except as FR19 (no mount announcement) states; adaptive server-renders and hydrates as that popover field (the shared resolver answers popover on 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 with hasAutoFocus, where the markup's autofocus has focused the combobox input and the swap moves focus to the trigger within the same visit, no onBlur/onFocus to 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; no onChange value, 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. adaptive resolves 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).
  • Trigger (FR11): the trigger's tokens are the popover presentation's tokens — same component, same words, same zone handling; size, statusVariant and hasAutoFocus apply to the trigger as to the tokenizer field; onFocus fires when focus enters the field from outside and onBlur when it leaves, so opening the flow (focus moves into a sheet) fires onBlur and the close that returns focus to the trigger fires onFocus, as the popover fires them around its editor. The field's control is one button with aria-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 — a components[type].Token override renders on the trigger in place of PowerSearch's token with the popover's props (config, filter, field, operator, maxLength, isDisabled) and onClick/onRemove undefined, 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 and endContent render in the field's end lane outside the trigger button, so an interactive endContent keeps its own tab stop, taps and name and never opens the flow; disabled is aria-disabled and focusable, never native disabled; becoming disabled while open closes the flow and moves focus to the trigger (spec:AST-085/FR4). FR10 names the focus destination for every adaptive switch (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 interactive endContent) 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 is popover, focus then moves to the combobox input, so it never lands on the page body (FR10).
  • Filters sheet (FR12): a list value reads as all its items joined by the locale's narrow conjunction list — the row calls the token's formatter with its width budget lifted, so it never truncates a value or collapses a list to "{n} items" — (Intl.ListFormat, {type: 'conjunction', style: 'narrow'} — "Open, Closed" / "高、中" / "A, B und C"), a conjunction list separates every item in every shipped locale (the unit type 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, a date_range as "date range", a nested value as "{n} filters", a date_relative value 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; a nested filter'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 — under hasClear, 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.
  • Add filter (FR13): the content searchbox's aria-controls names 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 to spec: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 by maxSearchResults; 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, and Added {label} on a value suggestion. every visible field lists, nested-only fields included, since the value step edits every operator's value.
  • Value step (FR14, FR15, FR16): the title is the dialog's name and reads the raw keys for a filter whose field or operator the config no longer has, as the row does; when a controlled update removed the edited filter or left two it could be, Apply writes nothing and Apply and Cancel return focus to the nearest remaining row around its former position, else Add filter; Cancel from creation after the opening field (or the content-search field) was removed focuses the Add filter sheet's first control — its search field, else its first row, else Back (FR16 names both destinations), and FR16 now carries one general rule — a controlled update that unmounts the focused control of the Filters or Add filter sheet (a filter's row, a dropped field's row, Clear all gone with the last filter) hands focus to the nearest remaining row that is a button, by index among the button rows (plain read-only rows passed over, as Remove and Apply/Cancel pass them), else the sheet's first control, else its panel — plus a custom Editor's 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 — a nested value 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's Editor override 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].Editor replaces the body and receives exactly the props the popover presentation hands it — config, filter, mode, saveButtonLabel, isReadOnly, onSave, onCancel — and no sheet-only timezoneID, so a custom date editor reads one filter on one day in both presentations; exact onChange metadata 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).
  • Editors (FR17, FR21): the popover presentation's editors, with three sheet-native differences — enum/enum_list/date_relative as choice rows: radio rows in a radiogroup named by the field (a tap selects) for single choice, checkbox rows in a group (a tap toggles) for enum_list, state in aria-checked as 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 in system_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, and string_list/entity_list through 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 and formatFilterValue in timezoneID or 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, and formatFilterValue's four differences from the token — a date_absolute with a time of day in timezoneID where the token shows a date only in the device zone; "{n} entities" where the token says "{n} items"; locale number formatting plus the operator's units where 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 changes onChange's unixSeconds and 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 against spec:AST-086/FR14: the shipped useAnnounce writes 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 reworked useAnnounce (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 against family: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 (ref stays on the root in both presentations, the INV8 promise; only data-testid is routed to the primary control, and no id, aria-*, other data-*, hidden, inert, dir or handler is forwarded at all — the stable component destructures its props with no rest) and items 2–4 (stable root xstyle/className/style, applied in both presentations to the field box, FR10's container* target, and no container*/control* parts); moving ref or 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). A nested filter'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 no experimental option 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.
  • FR18–FR20: every sheet list derives synchronously from the config, so no empty state can precede a result; the result count announces only on change and never on mount, including under StrictMode, through the shared channel while the flow is closed and through the open sheet's own region while it is open; every string the sheets introduce is a @astryx.powersearch.* catalog key (content the popover presentation already renders — config text, formatted values, the relative-date choice labels — keeps its source).
  • Public concepts: menuWidth and tokenOverflowBehavior have no sheet surface and warn once in development rather than being silently dropped; ref reaches the root in both presentations; the styling props reach the field container in both; data-testid reaches the primary control of each — the combobox input in the popover, the trigger button in the sheet.
  • AR4–AR6, PR5: AR6 states how the popover presentation names a removed filter — the Tokenizer's tokenRemoved with the filter's formatFilterValue label ("{field}: {operator} {value}"), not the token's text: 1500 with units: 'ms' shows "1500" and is announced "1,500 ms" — against the sheet's Removed {label} from the Filters row's text, so the readings differ wherever FR12's row and formatFilterValue differ; trigger and sheet semantics (an interactive endContent is 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, and FR14, 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 for popover; 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:BottomSheet FR6) 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 for adaptive, 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).
  • Supporting rows: Intent, Compatibility bullets (the "Compatibility class … no public API change" and "changes no dismissal behavior" sentences are scoped to DEC-1/DEC-2, with DEC-3's experimental option and its sheets' dismissal ownership stated beside them), Ownership bullets (owns the touch flow; does not own the adaptive rule, the sheets' mechanics, or the Tokenizer's sheet), AV5–AV6, seven representative states, four Design-relationship rows (the trigger row names the PowerSearch and Token source plainly), a Design note that the surface adds no theming target while experimental, five Family-and-system bullets, a verification-map row (its states now include Home/End and Space/Enter on a radio group, Space on a checkbox row, and a right-to-left layout with the row chevrons and Back glyph mirrored, cited in full as design:bidirectionality/DR4), system_specs gains spec: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 through component:BottomSheet, pointing by content at the "Does not own" bullet on the sheets' dialog, scrim, gestures, snap behavior, keyboard accommodation, and dismissal paths.
  • FR1–FR9 are scoped to the popover presentation, 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

  • Switching surfaces automatically on pointer: coarse with no opt-in: changes stable behavior for every existing caller, which spec:AST-017 FR21 forbids outside an experimental boundary.
  • A top-level stable presentation prop, matching the selectors today: the surface has no consumer evidence yet, and a stable prop cannot be changed or removed in a patch.
  • A separate touch component the caller chooses per breakpoint (the Lab prototype's shape): two components, two draft states, and an import swap at every breakpoint.
  • Sheet-only Clear all semantics that keep read-only filters (the Lab prototype's behavior): the two presentations of one field would clear differently; the popover presentation's semantics stand, and whether both should keep read-only filters is an open question.
  • Sheet-only date editors that read date_absolute and date_range in timezoneID while the popover's read them in UTC (the Lab prototype's behavior): one filter would show two different days as adaptive switches; 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 adaptive ever 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 for isDateOnly: false, or the time zone of the date editors (left open).

Evidence and implementation relationship

  • Touch-flow evidence: the Lab prototype's three-sheet flow, verified in real browsers at a 390×844 touch viewport — one trigger, Filters rows with bold values, Add filter with grouped fields or the content search, staged field → operator → value, exact onChange metadata, focus after each step, identity following across controlled reorders.
  • Shared policy evidence: the selection controls' adaptive tests stub the compact-touch query both ways; the implementation's presentation tests mirror them.
  • Live-region evidence: 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.
  • endContent evidence: the implementation's test renders an action button in endContent, 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.
  • Right-to-left evidence: the implementation's test renders the flow in dir="rtl" and asserts the shared mirror style on every row chevron and the Back glyph; Chromium with dir="rtl" shows each with scaleX(-1).
  • In-Dialog evidence: the implementation's test renders the field inside an open native dialog and asserts the count (both presentations) and Clear all's message land in that dialog's region with the page-body channel empty; a verification state for a field inside an open Dialog is added.
  • Focus-destination evidence: the implementation's tests flip the compact-touch query while the closed field holds focus (trigger → combobox input and back; a focused token remove control → the trigger; a focused endContent, which each presentation mounts anew → the primary control), while it does not (focus stays outside), and while the flow is open (the latch).
  • Editor-props evidence: the implementation's test asserts the exact prop set a custom Editor receives in the sheet presentation equals the popover's.
  • Value-text evidence: the implementation's tests render the popover presentation for the same filters, read each token's value text, and assert the sheet's trigger token, the Filters row's bold value and the Removed … announcement carry exactly that text — for a boundary date under timezoneID UTC, Tokyo, Honolulu and unset, and for a string_list, an integer and a date_range; the editor test asserts the UTC day as the popover reads it.
  • Clear-all timing evidence: the implementation's test asserts nothing is spoken while the dialog is still presented and that the shared channel carries the message after the exit; a Chromium run (iPhone-size, motion on) shows the page-body region receiving "All filters removed" 363 ms after the tap with no dialog open and none :modal.
  • Clear-all evidence: the popover presentation's clear control commits onChange([], 'remove', lastIndex) through the Tokenizer's clear change (current source and PowerSearch.test.tsx).
  • Implementation: the stacked feature pull request (feat/powersearch-experimental-bottom-sheet, targeting the Tokenizer implementation branch) adds experimental.presentation, the sheet flow, i18n keys, docs, stories, tests, and the [experimental] Changeset, and deletes the gap notes.

Writing rubric results

  • R1 narration: Intent, Compatibility, Ownership, Public concepts, FR10–FR21, AV5–AV6, the representative states, PR5, AR4–AR6, the design rows, the family bullets, the verification row, and DEC-3 are written as present-tense statements about the system; the gap notes say what the shipped PowerSearch does today, with no fix tracking. "Lab touch-flow evidence" in the Basis column names the source of the facts, not a change. The pre-existing "This proposal changes editor geometry only" sentence was rewritten as "DEC-1 and DEC-2 change editor geometry only". This revision's AR6/FR19 live-region text and the prop-target paragraph are present-tense statements about the surface, with no reference to the review that prompted them.
  • R2 research in the record: none; the prototype history and the Clear-all comparison stay in this description.
  • R3 evidence naming a change: none added; the new verification row names test layers and real-engine evidence; the Basis column names the Lab evidence, the current popover source, and the current typeahead source. Every cited record id resolves; component:Token is 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).
  • R4 rejected alternatives over one line or outside a decision: none; DEC-3 carries five one-line Rejected: entries.
  • R5 pull request or issue references remaining: none (searched # + digits, /pull/, /issues/; the file had none before and has none now).
  • R6 ruled open questions still listing options: none; the three new open questions are unanswered, each states today's behavior and the alternative side by side.

Scope

  • This PR contains specification records only.
  • It records observable behavior and preserves equivalent internal implementations (AV5, AV6).
  • Any named internal mechanism is intentionally public, names its dependent caller/system, and explains why an equivalent implementation would not satisfy the contract: the native modal dialogs are named because component:BottomSheet and family:overlay-dismissal depend on them; the Tokenizer's sheet presentation is named because the list-value editor opens it and component:Tokenizer owns it.
  • Verification states the evidence layer and failure signal without prescribing CI job/workflow topology.
  • It records one intentional durable decision, not a rescue for a separable tagalong.
  • The canonical knowledge owner, affected current claims, and overlapping open work are named; no ownership collision (the Tokenizer sheet stays with component:Tokenizer).
  • Adjacent API, visual, accessibility, composition, and implementation facts remain outside scope unless this exact decision depends on them.
  • Public text and artifacts contain no internal Meta context.

Validation

  • pnpm check:knowledge — "Knowledge templates and records are aligned."
  • Prettier — packages/core/src/PowerSearch/PowerSearch.spec.md formatted.
  • git diff --check — clean.
  • Cited Tokenizer claims re-checked against the Tokenizer spec branch's current head: component:Tokenizer/DEC-3, FR16–FR24, FR18 exist with those numbers.

@vercel

vercel Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 26, 2026 9:38pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 23, 2026
@astracat-bot

astracat-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 26, 2026, 10:06 PM UTC

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 23, 2026
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Previews ready: Storybook and Sandbox were built with this PR's docsite. CI evidence will be added when available.

View Storybook for this PR · View Sandbox for this PR

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-fields and its FR10 citations, since that family excludes PowerSearch and requires the opposite ref and styling placement, and keep data-testid on the popover's combobox input, where it is today.
  • FR17 and FR21 rely on Tokenizer DEC-3 and FR16–FR24, which are not on main yet, 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

component:Tokenizer on main has FR1–FR15 and DEC-1–DEC-2 only; FR16–FR24, FR18 and DEC-3 are proposed in #7976. If this record becomes current first, FR17, FR21 and the ownership bullets cite claims that do not exist. Consider landing after #7976, or stacking on it, and re-checking the numbers.

@imdreamrunner

Copy link
Copy Markdown
Contributor Author

/approve-spec 600ed09

@github-actions github-actions Bot added spec-auto-merge Auto-merge was enabled by the spec owner gate and removed needs:spec-owner-review Current knowledge records await owner approval labels Oct 23, 2026
@imdreamrunner
imdreamrunner force-pushed the spec/powersearch-experimental-presentation branch from 600ed09 to 589b44a Compare October 23, 2026 21:39
@imdreamrunner
imdreamrunner changed the base branch from main to spec/tokenizer-experimental-presentation October 23, 2026 21:39
@github-actions github-actions Bot added needs:spec-owner-review Current knowledge records await owner approval and removed spec-auto-merge Auto-merge was enabled by the spec owner gate labels Oct 23, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 interactive endContent goes in the sheet presentation (spec:AST-055 FR3 forbids nesting a caller's control inside a component-owned button).
  • Fix three citations: AR6, the Family bullet and system_specs cite spec:AST-086/FR10, which covers owners rehosted above a modal (FR3 and FR4 fit the in-sheet region); FR11 cites spec:AST-085/FR2 where FR4 governs the focus destination; and the Sheet trigger design row names component: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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No record with the id component:Token exists on main; consider naming the Token source here instead.

@imdreamrunner
imdreamrunner force-pushed the spec/tokenizer-experimental-presentation branch from d19e19f to d7ee224 Compare October 23, 2026 22:10
@imdreamrunner
imdreamrunner force-pushed the spec/powersearch-experimental-presentation branch from 589b44a to 128fc99 Compare October 23, 2026 22:12
@imdreamrunner
imdreamrunner force-pushed the spec/powersearch-experimental-presentation branch from 128fc99 to 52cb7b9 Compare October 23, 2026 22:21
@imdreamrunner
imdreamrunner force-pushed the spec/tokenizer-experimental-presentation branch from d7ee224 to a3bbf10 Compare October 23, 2026 22:33
@imdreamrunner
imdreamrunner force-pushed the spec/powersearch-experimental-presentation branch from 52cb7b9 to b31bddf Compare October 23, 2026 22:34

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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].Editor the props the popover passes (config included, and no sheet-only timezoneID), or a custom date editor can show one filter on two different days as adaptive switches.
  • FR10 should name where focus goes when adaptive switches 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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Editor is 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-nested operator, so a nested filter'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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@imdreamrunner
imdreamrunner force-pushed the spec/tokenizer-experimental-presentation branch from 2abcbe7 to f06ba38 Compare October 24, 2026 16:15
@imdreamrunner
imdreamrunner force-pushed the spec/powersearch-experimental-presentation branch from ab4f1d4 to e2bb492 Compare October 24, 2026 16:18

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 nested filters with its built-in nested editor while the sheet cannot without components.nested.Editor; reuse that editor, or name nested as 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

Comment on lines +542 to +552
- 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +146 to +148
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`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

Comment on lines +546 to +549
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

Comment on lines +532 to +534
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and isReadOnly turning on (spec:AST-085 FR4).
  • 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 endContent beside 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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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: …".

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, as spec:AST-085 FR4 requires.
  • DEC-3 should record the closed field's pointer-driven element swap under adaptive as 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-020 FR15 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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“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.

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 isReadOnly turns 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-020 FR15 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-086 does 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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 formatFilterValue label (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-017 FR60 allows an open layer at most 5 extra elements under touch, each with a named job.

[Automated review]

Comment on lines +322 to +326
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +528 to +531
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

Comment on lines +327 to +332
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +221 to +226
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-017 FR60'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 formatFilterValue label, not its token text) and count the single-value cut among the first known gap's differences.

[Automated review]

Comment on lines +540 to +543
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-017 FR60's allowance.
  • The first known gap (line 590) says formatFilterValue differs 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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). |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

@imdreamrunner

Copy link
Copy Markdown
Contributor Author

/approve-spec 80098e1

This branch was successfully deployed

1 active deployment
Preview — 80098e10 Deployed Oct 26, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant