@astryxdesign/core 0.7.8-canary.c239daf → 0.7.8-canary.cc034ed

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -133,8 +133,10 @@ export interface PowerSearchProps extends Omit<BaseProps<HTMLElement>, 'onChange
133
133
  * above it with the fields (or the content search with inline
134
134
  * suggestions), and a value step opens above that, stages operator →
135
135
  * value, and confirms from its footer. Dismissing a step reveals the
136
- * one beneath. Suited to compact touch screens, where the editor
137
- * popover competes with the software keyboard.
136
+ * one beneath. The flow edits a draft: `onChange` is called when Done
137
+ * applies it, once per edit in order, and a dismissal with a changed
138
+ * draft asks before discarding it. Suited to compact touch screens,
139
+ * where the editor popover competes with the software keyboard.
138
140
  * - `'adaptive'`: the bottom sheet on compact coarse-pointer screens,
139
141
  * otherwise the popover — the same resolution Selector uses. The server
140
142
  * render and the hydrating render are the popover.
@@ -1 +1 @@
1
- {"version":3,"file":"PowerSearch.d.ts","sourceRoot":"","sources":["../../src/PowerSearch/PowerSearch.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PowerSearch.d.ts","sourceRoot":"","sources":["../../src/PowerSearch/PowerSearch.tsx"],"names":[],"mappings":"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"}
@@ -72,7 +72,14 @@ export interface PowerSearchBottomSheetProps {
72
72
  config: InternalConfig;
73
73
  /** The caller's config, handed to Editor overrides as they expect it. */
74
74
  configProp: PowerSearchConfig;
75
+ /** The committed filters; the draft the sheet edits starts from them. */
75
76
  filters: ReadonlyArray<PowerSearchFilter>;
77
+ /**
78
+ * The field's `onChange`. Done replays the draft's edits through it, in
79
+ * order, onto the field's latest filters, each call carrying that list
80
+ * with the edit applied; nothing is reported before Done, and nothing when
81
+ * the field is read-only or disabled at Done.
82
+ */
76
83
  onChange: (filters: ReadonlyArray<PowerSearchFilter>, changeType: PowerSearchChangeType, index: number) => void;
77
84
  /** The field typeahead source; the content search lists its suggestions. */
78
85
  searchSource: SearchSource<PowerSearchItem>;
@@ -90,13 +97,29 @@ export interface PowerSearchBottomSheetProps {
90
97
  components?: PowerSearchComponents;
91
98
  editors: PowerSearchSheetEditors;
92
99
  }
100
+ /** One edit of the draft: the draft before and after it, and its type and index in the draft; Done replays it onto the field's latest filters. */
101
+ interface DraftEdit {
102
+ before: ReadonlyArray<PowerSearchFilter>;
103
+ after: ReadonlyArray<PowerSearchFilter>;
104
+ changeType: PowerSearchChangeType;
105
+ index: number;
106
+ }
107
+ /**
108
+ * Replays the draft's edits onto `base`, the field's latest filters, and
109
+ * reports each through `report` with the list after it. An add appends; an
110
+ * edit replaces the first filter matching the one it edited; a removal drops
111
+ * the filters it removed, by signature. An edit or removal whose target is
112
+ * not in the base list is skipped: the parent removed it meanwhile.
113
+ */
114
+ export declare function replayDraftEdits(base: ReadonlyArray<PowerSearchFilter>, edits: ReadonlyArray<DraftEdit>, report: (filters: ReadonlyArray<PowerSearchFilter>, changeType: PowerSearchChangeType, index: number) => void): ReadonlyArray<PowerSearchFilter>;
93
115
  /**
94
116
  * The sheet flow of PowerSearch's touch presentation: Filters, Add filter,
95
117
  * and the value step, as standalone sheets that stack, each open beneath the
96
118
  * step it led to.
97
119
  */
98
- export declare function PowerSearchBottomSheet({ isOpen, onOpenChange, finalFocusRef, resultCountText, config, configProp, filters, onChange, searchSource, placeholder, isReadOnly, isDisabled, hasClear, maxOperatorMenuItems, timezoneID, maxSearchResults, saveButtonLabel: saveButtonLabelFromProps, components: componentOverrides, editors, }: PowerSearchBottomSheetProps): React.JSX.Element;
120
+ export declare function PowerSearchBottomSheet({ isOpen, onOpenChange, finalFocusRef, resultCountText, config, configProp, filters: committedFilters, onChange: onCommit, searchSource, placeholder, isReadOnly, isDisabled, hasClear, maxOperatorMenuItems, timezoneID, maxSearchResults, saveButtonLabel: saveButtonLabelFromProps, components: componentOverrides, editors, }: PowerSearchBottomSheetProps): React.JSX.Element;
99
121
  export declare namespace PowerSearchBottomSheet {
100
122
  var displayName: string;
101
123
  }
124
+ export {};
102
125
  //# sourceMappingURL=PowerSearchBottomSheet.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PowerSearchBottomSheet.d.ts","sourceRoot":"","sources":["../../src/PowerSearch/PowerSearchBottomSheet.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,EASZ,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AAoBf,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAkCrD,OAAO,KAAK,EACV,wBAAwB,EACxB,sBAAsB,EACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAC;AAe3D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAIV,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EAEjB,iBAAiB,EACjB,eAAe,EAEhB,MAAM,SAAS,CAAC;AAajB;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,OAAO,YAAY,CAAC;IAClC,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;IACtD,wBAAwB,EAAE,OAAO,wBAAwB,CAAC;CAC3D;AAED,MAAM,WAAW,2BAA2B;IAC1C,wEAAwE;IACxE,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC;;;OAGG;IACH,aAAa,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC7C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,cAAc,CAAC;IACvB,yEAAyE;IACzE,UAAU,EAAE,iBAAiB,CAAC;IAC9B,OAAO,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAC1C,QAAQ,EAAE,CACR,OAAO,EAAE,aAAa,CAAC,iBAAiB,CAAC,EACzC,UAAU,EAAE,qBAAqB,EACjC,KAAK,EAAE,MAAM,KACV,IAAI,CAAC;IACV,4EAA4E;IAC5E,YAAY,EAAE,YAAY,CAAC,eAAe,CAAC,CAAC;IAC5C,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,OAAO,EAAE,uBAAuB,CAAC;CAClC;AA4mBD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,YAAY,EACZ,aAAa,EACb,eAAe,EACf,MAAM,EACN,UAAU,EACV,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,UAAU,EACV,UAAU,EACV,QAAQ,EACR,oBAAoB,EACpB,UAAU,EACV,gBAAgB,EAChB,eAAe,EAAE,wBAAwB,EACzC,UAAU,EAAE,kBAAkB,EAC9B,OAAO,GACR,EAAE,2BAA2B,qBAyoD7B;yBA7pDe,sBAAsB"}
1
+ {"version":3,"file":"PowerSearchBottomSheet.d.ts","sourceRoot":"","sources":["../../src/PowerSearch/PowerSearchBottomSheet.tsx"],"names":[],"mappings":"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"}
@@ -104,6 +104,82 @@ registerEnglishFallback(english);
104
104
  // Styles
105
105
  // =============================================================================
106
106
 
107
+ /** One edit of the draft: the draft before and after it, and its type and index in the draft; Done replays it onto the field's latest filters. */
108
+
109
+ // Whether two filter lists hold the same filters in the same order, by the
110
+ // key-order-insensitive signature the sheet matches filters with.
111
+ function hasSameFilters(a, b) {
112
+ return a.length === b.length && a.every((filter, index) => b[index] != null && filterSignature(filter) === filterSignature(b[index]));
113
+ }
114
+
115
+ // The filters `before` holds that `after` does not, as a multiset by
116
+ // signature, in `before` order.
117
+ function removedFilters(before, after) {
118
+ const remaining = new Map();
119
+ for (const filter of after) {
120
+ const signature = filterSignature(filter);
121
+ remaining.set(signature, (remaining.get(signature) ?? 0) + 1);
122
+ }
123
+ return before.filter(filter => {
124
+ const signature = filterSignature(filter);
125
+ const count = remaining.get(signature) ?? 0;
126
+ if (count > 0) {
127
+ remaining.set(signature, count - 1);
128
+ return false;
129
+ }
130
+ return true;
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Replays the draft's edits onto `base`, the field's latest filters, and
136
+ * reports each through `report` with the list after it. An add appends; an
137
+ * edit replaces the first filter matching the one it edited; a removal drops
138
+ * the filters it removed, by signature. An edit or removal whose target is
139
+ * not in the base list is skipped: the parent removed it meanwhile.
140
+ */
141
+ export function replayDraftEdits(base, edits, report) {
142
+ let current = base;
143
+ for (const edit of edits) {
144
+ if (edit.changeType === 'add') {
145
+ const added = edit.after[edit.index];
146
+ if (added == null) {
147
+ continue;
148
+ }
149
+ current = [...current, added];
150
+ report(current, 'add', current.length - 1);
151
+ } else if (edit.changeType === 'edit') {
152
+ const original = edit.before[edit.index];
153
+ const updated = edit.after[edit.index];
154
+ if (original == null || updated == null) {
155
+ continue;
156
+ }
157
+ const signature = filterSignature(original);
158
+ const index = current.findIndex(filter => filterSignature(filter) === signature);
159
+ if (index === -1) {
160
+ continue;
161
+ }
162
+ current = current.map((filter, i) => i === index ? updated : filter);
163
+ report(current, 'edit', index);
164
+ } else {
165
+ const removed = removedFilters(edit.before, edit.after).map(filterSignature);
166
+ const indexes = [];
167
+ for (const signature of removed) {
168
+ const index = current.findIndex((filter, i) => !indexes.includes(i) && filterSignature(filter) === signature);
169
+ if (index !== -1) {
170
+ indexes.push(index);
171
+ }
172
+ }
173
+ if (indexes.length === 0) {
174
+ continue;
175
+ }
176
+ current = current.filter((_, i) => !indexes.includes(i));
177
+ report(current, 'remove', Math.min(...indexes));
178
+ }
179
+ }
180
+ return current;
181
+ }
182
+
107
183
  // The trailing plus a field row shows when choosing it adds the filter at
108
184
  // once, and the Add filter actions' glyph. Core's default icon set has no plus.
109
185
  function PlusIcon(props) {
@@ -514,8 +590,8 @@ export function PowerSearchBottomSheet({
514
590
  resultCountText,
515
591
  config,
516
592
  configProp,
517
- filters,
518
- onChange,
593
+ filters: committedFilters,
594
+ onChange: onCommit,
519
595
  searchSource,
520
596
  placeholder,
521
597
  isReadOnly,
@@ -535,6 +611,27 @@ export function PowerSearchBottomSheet({
535
611
  const locale = useLocale();
536
612
  const formatList = useListFormatter();
537
613
  const isInteractive = !isDisabled && !isReadOnly;
614
+
615
+ // ---- draft ----
616
+ // One open flow edits one draft of the filter list; `onChange` waits for
617
+ // Done, and a dismissal with a changed draft asks first.
618
+ const [filtersAtOpen, setFiltersAtOpen] = useState(() => committedFilters);
619
+ const [filters, setFilters] = useState(() => committedFilters);
620
+ // The draft as of the last edit, for the edit log's `before` lists.
621
+ const draftRef = useRef(filters);
622
+ const draftEditsRef = useRef([]);
623
+ const onChange = useCallback((next, changeType, index) => {
624
+ draftEditsRef.current.push({
625
+ before: draftRef.current,
626
+ after: next,
627
+ changeType,
628
+ index
629
+ });
630
+ draftRef.current = next;
631
+ setFilters(next);
632
+ }, []);
633
+ const isDraftDirty = !hasSameFilters(filtersAtOpen, filters);
634
+ const [isDiscardConfirmationOpen, setIsDiscardConfirmationOpen] = useState(false);
538
635
  const contentSearchFieldKey = config.config.contentSearchFieldKey;
539
636
  const contentSearchOperator = contentSearchFieldKey == null ? undefined : config.getDefaultOperator(contentSearchFieldKey);
540
637
  const hasContentSearch = contentSearchOperator?.value.type === 'string';
@@ -616,9 +713,6 @@ export function PowerSearchBottomSheet({
616
713
  const [contentQuery, setContentQuery] = useState('');
617
714
  const [isClearConfirmationOpen, setIsClearConfirmationOpen] = useState(false);
618
715
  const clearAllButtonRef = useRef(null);
619
- // Set while Clear all commits, so the result count that changes in that
620
- // commit is spoken with Clear all's outcome, as one message.
621
- const isClearingRef = useRef(false);
622
716
 
623
717
  // `useAnnounce` delivers inside the open Astryx modal that encloses its
624
718
  // caller, and this component renders above the
@@ -645,13 +739,21 @@ export function PowerSearchBottomSheet({
645
739
  if (isOpen !== wasOpen) {
646
740
  setWasOpen(isOpen);
647
741
  if (isOpen) {
742
+ // A fresh draft from the committed filters; the entry step is judged
743
+ // against them, not the previous session's draft.
744
+ setFiltersAtOpen(committedFilters);
745
+ setFilters(committedFilters);
746
+ draftRef.current = committedFilters;
747
+ draftEditsRef.current = [];
748
+ const startsOnAddFilter = committedFilters.length === 0 && isInteractive;
648
749
  setFieldQuery('');
649
750
  setContentQuery('');
650
- setStep(isEntryAddFilter ? 'fields' : 'manage');
651
- setFieldPickerOrigin(isEntryAddFilter ? 'trigger' : 'manage');
751
+ setStep(startsOnAddFilter ? 'fields' : 'manage');
752
+ setFieldPickerOrigin(startsOnAddFilter ? 'trigger' : 'manage');
652
753
  } else {
653
754
  setStep(null);
654
755
  setIsClearConfirmationOpen(false);
756
+ setIsDiscardConfirmationOpen(false);
655
757
  }
656
758
  }
657
759
  const closeFlow = useCallback(() => {
@@ -659,6 +761,32 @@ export function PowerSearchBottomSheet({
659
761
  setStep(null);
660
762
  onOpenChange(false);
661
763
  }, [onOpenChange]);
764
+ // Done replays each edit in order onto the field's latest filters, so a
765
+ // consumer gets the calls a popover session makes and keeps a filter it
766
+ // added meanwhile.
767
+ const commitAndClose = useCallback(() => {
768
+ // A field that became read-only while the flow was open reports nothing.
769
+ if (isDraftDirty && isInteractive) {
770
+ replayDraftEdits(committedFilters, draftEditsRef.current, onCommit);
771
+ }
772
+ draftEditsRef.current = [];
773
+ closeFlow();
774
+ }, [closeFlow, committedFilters, isDraftDirty, isInteractive, onCommit]);
775
+ const requestDismiss = useCallback(() => {
776
+ if (isDraftDirty) {
777
+ setIsDiscardConfirmationOpen(true);
778
+ } else {
779
+ closeFlow();
780
+ }
781
+ }, [closeFlow, isDraftDirty]);
782
+ const closeDiscardConfirmation = () => {
783
+ // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves back into the sheet
784
+ flushSync(() => setIsDiscardConfirmationOpen(false));
785
+ const target = stepRef.current === 'manage' ? managerAddButtonRef.current ?? manageSheetRef.current?.closest('[tabindex="-1"]') : fieldsSheetRef.current?.closest('[tabindex="-1"]');
786
+ target?.focus({
787
+ preventScroll: true
788
+ });
789
+ };
662
790
 
663
791
  // Whether a Filters row is a button: the field interactive and the
664
792
  // filter not read-only. The row opens the value step — the editor for its
@@ -864,14 +992,8 @@ export function PowerSearchBottomSheet({
864
992
  // is quiet then, so the change is spoken here, inside the dialog. Only a
865
993
  // change after the flow opened is spoken.
866
994
  const announcedResultCountRef = useRef(resultCountText);
867
- // The latest result count, for Clear all's handler to read after its
868
- // commit has flushed.
869
- const latestResultCountRef = useRef(resultCountText);
870
- useLayoutEffect(() => {
871
- latestResultCountRef.current = resultCountText;
872
- }, [resultCountText]);
873
995
  useEffect(() => {
874
- if (isClearingRef.current || resultCountText === announcedResultCountRef.current) {
996
+ if (resultCountText === announcedResultCountRef.current) {
875
997
  return;
876
998
  }
877
999
  announcedResultCountRef.current = resultCountText;
@@ -1138,26 +1260,21 @@ export function PowerSearchBottomSheet({
1138
1260
  // confirmation must be gone before focus moves into the sheet and the
1139
1261
  // outcome is routed into the sheet's region, so its close is flushed
1140
1262
  // first. The outcome is "Editable filters removed" while a read-only filter
1141
- // remains, otherwise "All filters removed", joined with a result count that
1142
- // changed in the same commit as one catalog message; focus moves to the
1143
- // sheet's first remaining control: a kept read-only row has none, so Add
1144
- // filter.
1263
+ // remains, otherwise "All filters removed"; the result count follows the
1264
+ // applied filters, which change on Done, so none changes here. Focus moves
1265
+ // to the sheet's first remaining control: a kept read-only row has none,
1266
+ // so Add filter.
1145
1267
  const closeClearConfirmation = confirmed => {
1146
1268
  const kept = filters.filter(filter => filter.isReadOnly);
1147
1269
  const firstRemovedIndex = filters.findIndex(filter => !filter.isReadOnly);
1148
1270
  const isClearing = confirmed && isInteractive && firstRemovedIndex >= 0;
1149
- isClearingRef.current = isClearing;
1150
- try {
1151
- // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves into the sheet and the words are routed into it
1152
- flushSync(() => {
1153
- setIsClearConfirmationOpen(false);
1154
- if (isClearing) {
1155
- onChange(kept, 'remove', firstRemovedIndex);
1156
- }
1157
- });
1158
- } finally {
1159
- isClearingRef.current = false;
1160
- }
1271
+ // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves into the sheet and the words are routed into it
1272
+ flushSync(() => {
1273
+ setIsClearConfirmationOpen(false);
1274
+ if (isClearing) {
1275
+ onChange(kept, 'remove', firstRemovedIndex);
1276
+ }
1277
+ });
1161
1278
  if (!isClearing) {
1162
1279
  clearAllButtonRef.current?.focus();
1163
1280
  return;
@@ -1165,18 +1282,7 @@ export function PowerSearchBottomSheet({
1165
1282
  managerAddButtonRef.current?.focus({
1166
1283
  preventScroll: true
1167
1284
  });
1168
- const resultCount = latestResultCountRef.current;
1169
- const changedCount = resultCount !== announcedResultCountRef.current ? resultCount : null;
1170
- announcedResultCountRef.current = resultCount;
1171
- if (kept.length > 0) {
1172
- announce(changedCount == null ? t('@astryx.powersearch.editableFiltersRemoved') : t('@astryx.powersearch.sheet.editableFiltersClearedWithCount', {
1173
- resultCount: changedCount
1174
- }));
1175
- } else {
1176
- announce(changedCount == null ? t('@astryx.powersearch.allFiltersRemoved') : t('@astryx.powersearch.sheet.filtersClearedWithCount', {
1177
- resultCount: changedCount
1178
- }));
1179
- }
1285
+ announce(kept.length > 0 ? t('@astryx.powersearch.editableFiltersRemoved') : t('@astryx.powersearch.allFiltersRemoved'));
1180
1286
  };
1181
1287
  const handleOperatorSelect = useCallback(operator => {
1182
1288
  setDraft(current => {
@@ -1270,20 +1376,20 @@ export function PowerSearchBottomSheet({
1270
1376
  // Every step change goes through the handlers above.
1271
1377
  const handleManageOpenChange = useCallback(isSheetOpen => {
1272
1378
  if (!isSheetOpen && stepRef.current === 'manage') {
1273
- closeFlow();
1379
+ requestDismiss();
1274
1380
  }
1275
- }, [closeFlow]);
1381
+ }, [requestDismiss]);
1276
1382
  const handleFieldsOpenChange = useCallback(isSheetOpen => {
1277
1383
  if (isSheetOpen || stepRef.current !== 'fields') {
1278
1384
  return;
1279
1385
  }
1280
1386
  if (fieldPickerOrigin === 'trigger') {
1281
- closeFlow();
1387
+ requestDismiss();
1282
1388
  return;
1283
1389
  }
1284
1390
  queueManagerAddFocus();
1285
1391
  setStep('manage');
1286
- }, [closeFlow, fieldPickerOrigin, queueManagerAddFocus]);
1392
+ }, [fieldPickerOrigin, queueManagerAddFocus, requestDismiss]);
1287
1393
  const handleValueOpenChange = useCallback(isSheetOpen => {
1288
1394
  if (!isSheetOpen && stepRef.current === 'value') {
1289
1395
  returnFromValue();
@@ -1510,7 +1616,7 @@ export function PowerSearchBottomSheet({
1510
1616
  // action. Mounted only while the sheet is open, so its text
1511
1617
  // is spoken in this sheet when it appears.
1512
1618
  isManageSheetOpen && /*#__PURE__*/_jsx(EmptyState, {
1513
- title: t('@astryx.powersearch.sheet.noFilters'),
1619
+ title: t(isDraftDirty ? '@astryx.powersearch.sheet.noFiltersInDraft' : '@astryx.powersearch.sheet.noFilters'),
1514
1620
  description: t('@astryx.powersearch.sheet.noFiltersDescription'),
1515
1621
  headingLevel: 4,
1516
1622
  icon: /*#__PURE__*/_jsx(Icon, {
@@ -1574,7 +1680,7 @@ export function PowerSearchBottomSheet({
1574
1680
  })
1575
1681
  })
1576
1682
  })
1577
- }), filterRows.length > 0 && /*#__PURE__*/_jsxs("div", {
1683
+ }), (filterRows.length > 0 || isDraftDirty) && /*#__PURE__*/_jsxs("div", {
1578
1684
  ...{
1579
1685
  className: "x1vjfegm x78zum5 x6s0dn4 x1qughib x1txdalj x8o8v82 x1pzlopt x10xzikg x1n2onr6 xdd4er5 x2lah0s"
1580
1686
  },
@@ -1590,7 +1696,7 @@ export function PowerSearchBottomSheet({
1590
1696
  variant: "primary",
1591
1697
  size: "lg",
1592
1698
  xstyle: isClearAllShown ? styles.footerEqualAction : [styles.footerSoleAction, styles.touchHitArea],
1593
- onClick: closeFlow,
1699
+ onClick: commitAndClose,
1594
1700
  width: isClearAllShown ? undefined : '100%'
1595
1701
  })]
1596
1702
  })]
@@ -1776,7 +1882,7 @@ export function PowerSearchBottomSheet({
1776
1882
  variant: "primary",
1777
1883
  width: "100%",
1778
1884
  xstyle: styles.touchAction,
1779
- onClick: closeFlow
1885
+ onClick: commitAndClose
1780
1886
  })
1781
1887
  })]
1782
1888
  })
@@ -1913,6 +2019,22 @@ export function PowerSearchBottomSheet({
1913
2019
  actionLabel: t('@astryx.powersearch.sheet.clearAllConfirm'),
1914
2020
  cancelLabel: t('@astryx.powersearch.sheet.clearAllCancel'),
1915
2021
  onAction: () => closeClearConfirmation(true)
2022
+ }), isDiscardConfirmationOpen && /*#__PURE__*/_jsx(SheetConfirmation, {
2023
+ isOpen: true,
2024
+ onOpenChange: isConfirmationOpen => {
2025
+ if (!isConfirmationOpen) {
2026
+ closeDiscardConfirmation();
2027
+ }
2028
+ },
2029
+ title: t('@astryx.powersearch.sheet.discardTitle'),
2030
+ description: t('@astryx.powersearch.sheet.discardDescription'),
2031
+ actionLabel: t('@astryx.powersearch.sheet.discardConfirm'),
2032
+ cancelLabel: t('@astryx.powersearch.sheet.keepEditing'),
2033
+ onAction: () => {
2034
+ setIsDiscardConfirmationOpen(false);
2035
+ draftEditsRef.current = [];
2036
+ closeFlow();
2037
+ }
1916
2038
  })]
1917
2039
  });
1918
2040
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PowerSearchBottomSheet.generated.d.ts","sourceRoot":"","sources":["../../../../src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgC7C,CAAC;AAEF,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"PowerSearchBottomSheet.generated.d.ts","sourceRoot":"","sources":["../../../../src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmC7C,CAAC;AAEF,eAAe,OAAO,CAAC"}
@@ -18,15 +18,18 @@ const english = {
18
18
  "@astryx.powersearch.sheet.clearAllConfirm": "Clear filters",
19
19
  "@astryx.powersearch.sheet.clearAllDescription": "This removes all editable filters from this search.",
20
20
  "@astryx.powersearch.sheet.clearAllTitle": "Clear all filters?",
21
+ "@astryx.powersearch.sheet.discardConfirm": "Discard changes",
22
+ "@astryx.powersearch.sheet.discardDescription": "Your changes to the filters haven't been applied.",
23
+ "@astryx.powersearch.sheet.discardTitle": "Discard changes?",
21
24
  "@astryx.powersearch.sheet.done": "Done",
22
- "@astryx.powersearch.sheet.editableFiltersClearedWithCount": "Editable filters removed. {resultCount}",
23
25
  "@astryx.powersearch.sheet.fieldOperator": "{field} {operator}",
24
26
  "@astryx.powersearch.sheet.filterUnavailable": "This filter is no longer available",
25
- "@astryx.powersearch.sheet.filtersClearedWithCount": "All filters removed. {resultCount}",
26
27
  "@astryx.powersearch.sheet.filtersTitle": "Filters",
28
+ "@astryx.powersearch.sheet.keepEditing": "Keep editing",
27
29
  "@astryx.powersearch.sheet.labelValue": "{label} {value}",
28
30
  "@astryx.powersearch.sheet.noFilters": "No filters applied",
29
31
  "@astryx.powersearch.sheet.noFiltersDescription": "Add a filter to narrow your results and find what you need faster.",
32
+ "@astryx.powersearch.sheet.noFiltersInDraft": "No filters",
30
33
  "@astryx.powersearch.sheet.noMatchingFields": "No filters match your search",
31
34
  "@astryx.powersearch.sheet.noResults": "No results found",
32
35
  "@astryx.powersearch.sheet.removeFilter": "Remove {field} filter",
@@ -1 +1 @@
1
- {"version":3,"file":"en.generated.d.ts","sourceRoot":"","sources":["../../../src/i18n/generated-locales/en.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAiqBpD,CAAC;AAEF,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"en.generated.d.ts","sourceRoot":"","sources":["../../../src/i18n/generated-locales/en.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmqBpD,CAAC;AAEF,eAAe,cAAc,CAAC"}
@@ -74,6 +74,7 @@ const runtimeCatalog = {
74
74
  '@astryx.powersearch.sheet.filtersTitle': 'Filters',
75
75
  '@astryx.powersearch.sheet.addFilter': 'Add filter',
76
76
  '@astryx.powersearch.sheet.noFilters': 'No filters applied',
77
+ '@astryx.powersearch.sheet.noFiltersInDraft': 'No filters',
77
78
  '@astryx.powersearch.sheet.noFiltersDescription': 'Add a filter to narrow your results and find what you need faster.',
78
79
  '@astryx.powersearch.sheet.clearAll': 'Clear all',
79
80
  '@astryx.powersearch.sheet.clearAllTitle': 'Clear all filters?',
@@ -81,6 +82,10 @@ const runtimeCatalog = {
81
82
  '@astryx.powersearch.sheet.clearAllConfirm': 'Clear filters',
82
83
  '@astryx.powersearch.sheet.clearAllCancel': 'Keep filters',
83
84
  '@astryx.powersearch.sheet.done': 'Done',
85
+ '@astryx.powersearch.sheet.discardTitle': 'Discard changes?',
86
+ '@astryx.powersearch.sheet.discardDescription': "Your changes to the filters haven't been applied.",
87
+ '@astryx.powersearch.sheet.keepEditing': 'Keep editing',
88
+ '@astryx.powersearch.sheet.discardConfirm': 'Discard changes',
84
89
  '@astryx.powersearch.sheet.search': 'Search',
85
90
  '@astryx.powersearch.sheet.searchResults': 'Search results',
86
91
  '@astryx.powersearch.sheet.searchFields': 'Search filters',
@@ -99,8 +104,6 @@ const runtimeCatalog = {
99
104
  '@astryx.powersearch.sheet.relativeMonthsFromNow': '{count, number} {count, plural, one {month} other {months}} from now',
100
105
  '@astryx.powersearch.sheet.fieldOperator': '{field} {operator}',
101
106
  '@astryx.powersearch.sheet.labelValue': '{label} {value}',
102
- '@astryx.powersearch.sheet.filtersClearedWithCount': 'All filters removed. {resultCount}',
103
- '@astryx.powersearch.sheet.editableFiltersClearedWithCount': 'Editable filters removed. {resultCount}',
104
107
  '@astryx.alertDialog.cancel': 'Cancel',
105
108
  '@astryx.appShell.mobileNavigation': 'Mobile navigation',
106
109
  '@astryx.appShell.skipToContent': 'Skip to content',
@@ -1 +1 @@
1
- {"version":3,"file":"pseudo.generated.d.ts","sourceRoot":"","sources":["../../../src/i18n/generated-locales/pseudo.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6qBpD,CAAC;AAEF,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"pseudo.generated.d.ts","sourceRoot":"","sources":["../../../src/i18n/generated-locales/pseudo.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA+qBpD,CAAC;AAEF,eAAe,cAAc,CAAC"}
@@ -74,6 +74,7 @@ const runtimeCatalog = {
74
74
  '@astryx.powersearch.sheet.filtersTitle': '⟦Ƒíłţéřš⟧',
75
75
  '@astryx.powersearch.sheet.addFilter': '⟦Àðð ƒíłţéř⟧',
76
76
  '@astryx.powersearch.sheet.noFilters': '⟦Ñó ƒíłţéřš àþþłíéð⟧',
77
+ '@astryx.powersearch.sheet.noFiltersInDraft': '⟦Ñó ƒíłţéřš⟧',
77
78
  '@astryx.powersearch.sheet.noFiltersDescription': '⟦Àðð à ƒíłţéř ţó ñàřřóŵ ýóúř řéšúłţš àñð ƒíñð ŵĥàţ ýóú ñééð ƒàšţéř.⟧',
78
79
  '@astryx.powersearch.sheet.clearAll': '⟦Çłéàř àłł⟧',
79
80
  '@astryx.powersearch.sheet.clearAllTitle': '⟦Çłéàř àłł ƒíłţéřš?⟧',
@@ -81,6 +82,10 @@ const runtimeCatalog = {
81
82
  '@astryx.powersearch.sheet.clearAllConfirm': '⟦Çłéàř ƒíłţéřš⟧',
82
83
  '@astryx.powersearch.sheet.clearAllCancel': '⟦Ķééþ ƒíłţéřš⟧',
83
84
  '@astryx.powersearch.sheet.done': '⟦Ðóñé⟧',
85
+ '@astryx.powersearch.sheet.discardTitle': '⟦Ðíšçàřð çĥàñĝéš?⟧',
86
+ '@astryx.powersearch.sheet.discardDescription': "⟦Ýóúř çĥàñĝéš ţó ţĥé ƒíłţéřš ĥàṽéñ'ţ ƀééñ àþþłíéð.⟧",
87
+ '@astryx.powersearch.sheet.keepEditing': '⟦Ķééþ éðíţíñĝ⟧',
88
+ '@astryx.powersearch.sheet.discardConfirm': '⟦Ðíšçàřð çĥàñĝéš⟧',
84
89
  '@astryx.powersearch.sheet.search': '⟦Šéàřçĥ⟧',
85
90
  '@astryx.powersearch.sheet.searchResults': '⟦Šéàřçĥ řéšúłţš⟧',
86
91
  '@astryx.powersearch.sheet.searchFields': '⟦Šéàřçĥ ƒíłţéřš⟧',
@@ -99,8 +104,6 @@ const runtimeCatalog = {
99
104
  '@astryx.powersearch.sheet.relativeMonthsFromNow': '⟦{count, number} {count, plural, one {month} other {months}} ƒřóɱ ñóŵ⟧',
100
105
  '@astryx.powersearch.sheet.fieldOperator': '⟦{field} {operator}⟧',
101
106
  '@astryx.powersearch.sheet.labelValue': '⟦{label} {value}⟧',
102
- '@astryx.powersearch.sheet.filtersClearedWithCount': '⟦Àłł ƒíłţéřš řéɱóṽéð. {resultCount}⟧',
103
- '@astryx.powersearch.sheet.editableFiltersClearedWithCount': '⟦Éðíţàƀłé ƒíłţéřš řéɱóṽéð. {resultCount}⟧',
104
107
  '@astryx.alertDialog.cancel': '⟦Çàñçéł⟧',
105
108
  '@astryx.appShell.mobileNavigation': '⟦Ṁóƀíłé ñàṽíĝàţíóñ⟧',
106
109
  '@astryx.appShell.skipToContent': '⟦Šķíþ ţó çóñţéñţ⟧',
package/locales/en.json CHANGED
@@ -267,6 +267,10 @@
267
267
  "defaultMessage": "No filters applied",
268
268
  "description": "Heading of the PowerSearch Filters sheet empty state when no filter is active (experimental bottom-sheet presentation). Neutral tone."
269
269
  },
270
+ "@astryx.powersearch.sheet.noFiltersInDraft": {
271
+ "defaultMessage": "No filters",
272
+ "description": "Heading of the PowerSearch Filters sheet empty state while the sheet's draft is empty but the field still has applied filters, which Done replaces. Must not say the filters are applied or removed yet. Neutral tone."
273
+ },
270
274
  "@astryx.powersearch.sheet.noFiltersDescription": {
271
275
  "defaultMessage": "Add a filter to narrow your results and find what you need faster.",
272
276
  "description": "Guidance line under the PowerSearch Filters sheet empty state, above its Add filter action. One short sentence."
@@ -293,7 +297,23 @@
293
297
  },
294
298
  "@astryx.powersearch.sheet.done": {
295
299
  "defaultMessage": "Done",
296
- "description": "Primary button label in the PowerSearch Filters sheet footer that closes the sheet. Short; marks the end of the filtering task."
300
+ "description": "Primary button label in the PowerSearch Filters sheet footer that commits the sheet's filter draft and closes the sheet. Short; marks the end of the filtering task."
301
+ },
302
+ "@astryx.powersearch.sheet.discardTitle": {
303
+ "defaultMessage": "Discard changes?",
304
+ "description": "Title of the confirmation dialog shown when someone tries to dismiss the PowerSearch filter sheet after changing its filter draft. A question."
305
+ },
306
+ "@astryx.powersearch.sheet.discardDescription": {
307
+ "defaultMessage": "Your changes to the filters haven't been applied.",
308
+ "description": "Body of the dirty-dismiss confirmation inside the PowerSearch filter sheet. Done applies the draft; a dismissal would drop it."
309
+ },
310
+ "@astryx.powersearch.sheet.keepEditing": {
311
+ "defaultMessage": "Keep editing",
312
+ "description": "Label of the safe cancel action in the PowerSearch dirty-dismiss confirmation. It closes the confirmation and returns to the unchanged draft."
313
+ },
314
+ "@astryx.powersearch.sheet.discardConfirm": {
315
+ "defaultMessage": "Discard changes",
316
+ "description": "Label of the destructive action in the PowerSearch dirty-dismiss confirmation. It closes the sheet without applying its filter draft."
297
317
  },
298
318
  "@astryx.powersearch.sheet.search": {
299
319
  "defaultMessage": "Search",
@@ -367,14 +387,6 @@
367
387
  "defaultMessage": "{label} {value}",
368
388
  "description": "A Filters row's \"field operator\" text joined with its value, as the removal announcement reads it (for example \"Status is Open\"). Translators may reorder the two parts."
369
389
  },
370
- "@astryx.powersearch.sheet.filtersClearedWithCount": {
371
- "defaultMessage": "All filters removed. {resultCount}",
372
- "description": "Polite screen-reader announcement after Clear all in the PowerSearch touch sheet when the result count changed with it; resultCount is the field's already formatted result count (for example \"240 results\"). Live-region text, never shown on screen."
373
- },
374
- "@astryx.powersearch.sheet.editableFiltersClearedWithCount": {
375
- "defaultMessage": "Editable filters removed. {resultCount}",
376
- "description": "Polite screen-reader announcement after Clear all in the PowerSearch touch sheet removes the editable filters, keeps read-only ones, and the result count changed with it; resultCount is the field's already formatted result count (for example \"240 results\"). Live-region text, never shown on screen."
377
- },
378
390
  "@astryx.alertDialog.cancel": {
379
391
  "defaultMessage": "Cancel",
380
392
  "description": "Button label on the secondary/dismiss button of an AlertDialog (modal confirmation). Imperative verb; consumers usually override with task-specific text."
@@ -200,6 +200,9 @@
200
200
  "@astryx.powersearch.sheet.noFilters": {
201
201
  "defaultMessage": "⟦Ñó ƒíłţéřš àþþłíéð⟧"
202
202
  },
203
+ "@astryx.powersearch.sheet.noFiltersInDraft": {
204
+ "defaultMessage": "⟦Ñó ƒíłţéřš⟧"
205
+ },
203
206
  "@astryx.powersearch.sheet.noFiltersDescription": {
204
207
  "defaultMessage": "⟦Àðð à ƒíłţéř ţó ñàřřóŵ ýóúř řéšúłţš àñð ƒíñð ŵĥàţ ýóú ñééð ƒàšţéř.⟧"
205
208
  },
@@ -221,6 +224,18 @@
221
224
  "@astryx.powersearch.sheet.done": {
222
225
  "defaultMessage": "⟦Ðóñé⟧"
223
226
  },
227
+ "@astryx.powersearch.sheet.discardTitle": {
228
+ "defaultMessage": "⟦Ðíšçàřð çĥàñĝéš?⟧"
229
+ },
230
+ "@astryx.powersearch.sheet.discardDescription": {
231
+ "defaultMessage": "⟦Ýóúř çĥàñĝéš ţó ţĥé ƒíłţéřš ĥàṽéñ'ţ ƀééñ àþþłíéð.⟧"
232
+ },
233
+ "@astryx.powersearch.sheet.keepEditing": {
234
+ "defaultMessage": "⟦Ķééþ éðíţíñĝ⟧"
235
+ },
236
+ "@astryx.powersearch.sheet.discardConfirm": {
237
+ "defaultMessage": "⟦Ðíšçàřð çĥàñĝéš⟧"
238
+ },
224
239
  "@astryx.powersearch.sheet.search": {
225
240
  "defaultMessage": "⟦Šéàřçĥ⟧"
226
241
  },
@@ -275,12 +290,6 @@
275
290
  "@astryx.powersearch.sheet.labelValue": {
276
291
  "defaultMessage": "⟦{label} {value}⟧"
277
292
  },
278
- "@astryx.powersearch.sheet.filtersClearedWithCount": {
279
- "defaultMessage": "⟦Àłł ƒíłţéřš řéɱóṽéð. {resultCount}⟧"
280
- },
281
- "@astryx.powersearch.sheet.editableFiltersClearedWithCount": {
282
- "defaultMessage": "⟦Éðíţàƀłé ƒíłţéřš řéɱóṽéð. {resultCount}⟧"
283
- },
284
293
  "@astryx.alertDialog.cancel": {
285
294
  "defaultMessage": "⟦Çàñçéł⟧"
286
295
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.8-canary.c239daf",
3
+ "version": "0.7.8-canary.cc034ed",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -870,9 +870,9 @@
870
870
  "react-dom": ">=19.0.0"
871
871
  },
872
872
  "devDependencies": {
873
- "@astryxdesign/a11y-spec": "0.7.8-canary.c239daf",
874
- "@astryxdesign/cli": "0.7.8-canary.c239daf",
875
- "@astryxdesign/interaction-harness": "0.7.8-canary.c239daf",
873
+ "@astryxdesign/a11y-spec": "0.7.8-canary.cc034ed",
874
+ "@astryxdesign/cli": "0.7.8-canary.cc034ed",
875
+ "@astryxdesign/interaction-harness": "0.7.8-canary.cc034ed",
876
876
  "@babel/cli": "^7.29.7",
877
877
  "@babel/core": "^7.29.7",
878
878
  "@babel/preset-react": "^7.29.7",
@@ -211,7 +211,7 @@ export const docs = {
211
211
  name: 'presentation',
212
212
  type: "'popover' | 'bottom-sheet' | 'adaptive'",
213
213
  description:
214
- 'How the field presents filter editing. adaptive (the default) uses the bottom sheet on compact touch screens and the popover otherwise, resolved the same way Selector resolves it; the server render and the hydrating render are the popover. popover is the tokenizer field with its typeahead and the editor popover on every device. bottom-sheet makes the whole field, on every device, one trigger showing display-only filter tokens: it opens a Filters sheet listing the filters as rows that edit or remove a filter, with Add filter, Done, and Clear all (which asks first and keeps read-only filters); an Add filter sheet with the fields (or the content search with inline suggestions) opens above it, and a value step that stages operator → value and confirms from its footer opens above that. Dismissing a step reveals the one beneath. List values edit through the Tokenizer\'s own sheet. In the sheet presentation menuWidth and tokenOverflowBehavior have no surface to act on and warn in development. Pass presentation="popover" to keep the tokenizer field and the editor popover on every device.',
214
+ 'How the field presents filter editing. adaptive (the default) uses the bottom sheet on compact touch screens and the popover otherwise, resolved the same way Selector resolves it; the server render and the hydrating render are the popover. popover is the tokenizer field with its typeahead and the editor popover on every device. bottom-sheet makes the whole field, on every device, one trigger showing display-only filter tokens: it opens a Filters sheet listing the filters as rows that edit or remove a filter, with Add filter, Done, and Clear all (which asks first and keeps read-only filters); an Add filter sheet with the fields (or the content search with inline suggestions) opens above it, and a value step that stages operator → value and confirms from its footer opens above that. Dismissing a step reveals the one beneath. The flow edits a draft: adding, editing and removing filters and Clear all change the draft, and onChange is called when Done applies it, once per edit in order; dismissing the flow with a changed draft asks before discarding it. List values edit through the Tokenizer\'s own sheet. In the sheet presentation menuWidth and tokenOverflowBehavior have no surface to act on and warn in development. Pass presentation="popover" to keep the tokenizer field and the editor popover on every device.',
215
215
  default: "'adaptive'",
216
216
  },
217
217
  ],
@@ -484,7 +484,7 @@ export const docsZh = {
484
484
  name: 'presentation',
485
485
  type: "'popover' | 'bottom-sheet' | 'adaptive'",
486
486
  description:
487
- '字段如何呈现筛选编辑。adaptive(默认)在紧凑触摸屏上使用底部面板,其他情况使用 popover,解析方式与 Selector 相同;服务端渲染和水合期间的渲染均为 popover。popover 在所有设备上为带有预输入和编辑弹出层的标记字段。bottom-sheet 在所有设备上将整个字段变为一个显示只读筛选标记的触发按钮——点按后打开列出筛选项的「筛选」面板,每行可编辑或移除筛选(含添加筛选、完成、全部清除;全部清除会先确认,并保留只读筛选);列出字段(或带内联建议的内容搜索)的「添加筛选」面板在其上方打开,按运算符 → 值暂存并在底部确认的取值步骤再在其上方打开。关闭某一步会露出下方的那一步。列表值通过 Tokenizer 自身的面板编辑。在底部面板呈现中,menuWidth 和 tokenOverflowBehavior 没有作用对象,并会在开发环境中发出警告。传入 presentation="popover" 可在所有设备上保留标记字段和编辑弹出层。',
487
+ '字段如何呈现筛选编辑。adaptive(默认)在紧凑触摸屏上使用底部面板,其他情况使用 popover,解析方式与 Selector 相同;服务端渲染和水合期间的渲染均为 popover。popover 在所有设备上为带有预输入和编辑弹出层的标记字段。bottom-sheet 在所有设备上将整个字段变为一个显示只读筛选标记的触发按钮——点按后打开列出筛选项的「筛选」面板,每行可编辑或移除筛选(含添加筛选、完成、全部清除;全部清除会先确认,并保留只读筛选);列出字段(或带内联建议的内容搜索)的「添加筛选」面板在其上方打开,按运算符 → 值暂存并在底部确认的取值步骤再在其上方打开。关闭某一步会露出下方的那一步。整个流程编辑的是草稿:添加、编辑、移除筛选和全部清除都只改动草稿,点按「完成」应用草稿时才按编辑顺序逐次触发 onChange;草稿有改动时关闭流程会先询问是否放弃。列表值通过 Tokenizer 自身的面板编辑。在底部面板呈现中,menuWidth 和 tokenOverflowBehavior 没有作用对象,并会在开发环境中发出警告。传入 presentation="popover" 可在所有设备上保留标记字段和编辑弹出层。',
488
488
  default: "'adaptive'",
489
489
  },
490
490
  ],
@@ -430,8 +430,10 @@ export interface PowerSearchProps extends Omit<
430
430
  * above it with the fields (or the content search with inline
431
431
  * suggestions), and a value step opens above that, stages operator →
432
432
  * value, and confirms from its footer. Dismissing a step reveals the
433
- * one beneath. Suited to compact touch screens, where the editor
434
- * popover competes with the software keyboard.
433
+ * one beneath. The flow edits a draft: `onChange` is called when Done
434
+ * applies it, once per edit in order, and a dismissal with a changed
435
+ * draft asks before discarding it. Suited to compact touch screens,
436
+ * where the editor popover competes with the software keyboard.
435
437
  * - `'adaptive'`: the bottom sheet on compact coarse-pointer screens,
436
438
  * otherwise the popover — the same resolution Selector uses. The server
437
439
  * render and the hydrating render are the popover.
@@ -181,7 +181,14 @@ export interface PowerSearchBottomSheetProps {
181
181
  config: InternalConfig;
182
182
  /** The caller's config, handed to Editor overrides as they expect it. */
183
183
  configProp: PowerSearchConfig;
184
+ /** The committed filters; the draft the sheet edits starts from them. */
184
185
  filters: ReadonlyArray<PowerSearchFilter>;
186
+ /**
187
+ * The field's `onChange`. Done replays the draft's edits through it, in
188
+ * order, onto the field's latest filters, each call carrying that list
189
+ * with the edit applied; nothing is reported before Done, and nothing when
190
+ * the field is read-only or disabled at Done.
191
+ */
185
192
  onChange: (
186
193
  filters: ReadonlyArray<PowerSearchFilter>,
187
194
  changeType: PowerSearchChangeType,
@@ -208,6 +215,116 @@ export interface PowerSearchBottomSheetProps {
208
215
  // Styles
209
216
  // =============================================================================
210
217
 
218
+ /** One edit of the draft: the draft before and after it, and its type and index in the draft; Done replays it onto the field's latest filters. */
219
+ interface DraftEdit {
220
+ before: ReadonlyArray<PowerSearchFilter>;
221
+ after: ReadonlyArray<PowerSearchFilter>;
222
+ changeType: PowerSearchChangeType;
223
+ index: number;
224
+ }
225
+
226
+ // Whether two filter lists hold the same filters in the same order, by the
227
+ // key-order-insensitive signature the sheet matches filters with.
228
+ function hasSameFilters(
229
+ a: ReadonlyArray<PowerSearchFilter>,
230
+ b: ReadonlyArray<PowerSearchFilter>,
231
+ ): boolean {
232
+ return (
233
+ a.length === b.length &&
234
+ a.every(
235
+ (filter, index) =>
236
+ b[index] != null &&
237
+ filterSignature(filter) === filterSignature(b[index]),
238
+ )
239
+ );
240
+ }
241
+
242
+ // The filters `before` holds that `after` does not, as a multiset by
243
+ // signature, in `before` order.
244
+ function removedFilters(
245
+ before: ReadonlyArray<PowerSearchFilter>,
246
+ after: ReadonlyArray<PowerSearchFilter>,
247
+ ): PowerSearchFilter[] {
248
+ const remaining = new Map<string, number>();
249
+ for (const filter of after) {
250
+ const signature = filterSignature(filter);
251
+ remaining.set(signature, (remaining.get(signature) ?? 0) + 1);
252
+ }
253
+ return before.filter(filter => {
254
+ const signature = filterSignature(filter);
255
+ const count = remaining.get(signature) ?? 0;
256
+ if (count > 0) {
257
+ remaining.set(signature, count - 1);
258
+ return false;
259
+ }
260
+ return true;
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Replays the draft's edits onto `base`, the field's latest filters, and
266
+ * reports each through `report` with the list after it. An add appends; an
267
+ * edit replaces the first filter matching the one it edited; a removal drops
268
+ * the filters it removed, by signature. An edit or removal whose target is
269
+ * not in the base list is skipped: the parent removed it meanwhile.
270
+ */
271
+ export function replayDraftEdits(
272
+ base: ReadonlyArray<PowerSearchFilter>,
273
+ edits: ReadonlyArray<DraftEdit>,
274
+ report: (
275
+ filters: ReadonlyArray<PowerSearchFilter>,
276
+ changeType: PowerSearchChangeType,
277
+ index: number,
278
+ ) => void,
279
+ ): ReadonlyArray<PowerSearchFilter> {
280
+ let current = base;
281
+ for (const edit of edits) {
282
+ if (edit.changeType === 'add') {
283
+ const added = edit.after[edit.index];
284
+ if (added == null) {
285
+ continue;
286
+ }
287
+ current = [...current, added];
288
+ report(current, 'add', current.length - 1);
289
+ } else if (edit.changeType === 'edit') {
290
+ const original = edit.before[edit.index];
291
+ const updated = edit.after[edit.index];
292
+ if (original == null || updated == null) {
293
+ continue;
294
+ }
295
+ const signature = filterSignature(original);
296
+ const index = current.findIndex(
297
+ filter => filterSignature(filter) === signature,
298
+ );
299
+ if (index === -1) {
300
+ continue;
301
+ }
302
+ current = current.map((filter, i) => (i === index ? updated : filter));
303
+ report(current, 'edit', index);
304
+ } else {
305
+ const removed = removedFilters(edit.before, edit.after).map(
306
+ filterSignature,
307
+ );
308
+ const indexes: number[] = [];
309
+ for (const signature of removed) {
310
+ const index = current.findIndex(
311
+ (filter, i) =>
312
+ !indexes.includes(i) && filterSignature(filter) === signature,
313
+ );
314
+ if (index !== -1) {
315
+ indexes.push(index);
316
+ }
317
+ }
318
+ if (indexes.length === 0) {
319
+ continue;
320
+ }
321
+ current = current.filter((_, i) => !indexes.includes(i));
322
+ report(current, 'remove', Math.min(...indexes));
323
+ }
324
+ }
325
+ return current;
326
+ }
327
+
211
328
  // The trailing plus a field row shows when choosing it adds the filter at
212
329
  // once, and the Add filter actions' glyph. Core's default icon set has no plus.
213
330
  function PlusIcon(props: SVGProps<SVGSVGElement>) {
@@ -834,8 +951,8 @@ export function PowerSearchBottomSheet({
834
951
  resultCountText,
835
952
  config,
836
953
  configProp,
837
- filters,
838
- onChange,
954
+ filters: committedFilters,
955
+ onChange: onCommit,
839
956
  searchSource,
840
957
  placeholder,
841
958
  isReadOnly,
@@ -854,6 +971,35 @@ export function PowerSearchBottomSheet({
854
971
  const formatList = useListFormatter();
855
972
  const isInteractive = !isDisabled && !isReadOnly;
856
973
 
974
+ // ---- draft ----
975
+ // One open flow edits one draft of the filter list; `onChange` waits for
976
+ // Done, and a dismissal with a changed draft asks first.
977
+ const [filtersAtOpen, setFiltersAtOpen] = useState(() => committedFilters);
978
+ const [filters, setFilters] = useState(() => committedFilters);
979
+ // The draft as of the last edit, for the edit log's `before` lists.
980
+ const draftRef = useRef(filters);
981
+ const draftEditsRef = useRef<DraftEdit[]>([]);
982
+ const onChange = useCallback(
983
+ (
984
+ next: ReadonlyArray<PowerSearchFilter>,
985
+ changeType: PowerSearchChangeType,
986
+ index: number,
987
+ ) => {
988
+ draftEditsRef.current.push({
989
+ before: draftRef.current,
990
+ after: next,
991
+ changeType,
992
+ index,
993
+ });
994
+ draftRef.current = next;
995
+ setFilters(next);
996
+ },
997
+ [],
998
+ );
999
+ const isDraftDirty = !hasSameFilters(filtersAtOpen, filters);
1000
+ const [isDiscardConfirmationOpen, setIsDiscardConfirmationOpen] =
1001
+ useState(false);
1002
+
857
1003
  const contentSearchFieldKey = config.config.contentSearchFieldKey;
858
1004
  const contentSearchOperator =
859
1005
  contentSearchFieldKey == null
@@ -961,9 +1107,6 @@ export function PowerSearchBottomSheet({
961
1107
  const [contentQuery, setContentQuery] = useState('');
962
1108
  const [isClearConfirmationOpen, setIsClearConfirmationOpen] = useState(false);
963
1109
  const clearAllButtonRef = useRef<HTMLButtonElement>(null);
964
- // Set while Clear all commits, so the result count that changes in that
965
- // commit is spoken with Clear all's outcome, as one message.
966
- const isClearingRef = useRef(false);
967
1110
 
968
1111
  // `useAnnounce` delivers inside the open Astryx modal that encloses its
969
1112
  // caller, and this component renders above the
@@ -990,13 +1133,21 @@ export function PowerSearchBottomSheet({
990
1133
  if (isOpen !== wasOpen) {
991
1134
  setWasOpen(isOpen);
992
1135
  if (isOpen) {
1136
+ // A fresh draft from the committed filters; the entry step is judged
1137
+ // against them, not the previous session's draft.
1138
+ setFiltersAtOpen(committedFilters);
1139
+ setFilters(committedFilters);
1140
+ draftRef.current = committedFilters;
1141
+ draftEditsRef.current = [];
1142
+ const startsOnAddFilter = committedFilters.length === 0 && isInteractive;
993
1143
  setFieldQuery('');
994
1144
  setContentQuery('');
995
- setStep(isEntryAddFilter ? 'fields' : 'manage');
996
- setFieldPickerOrigin(isEntryAddFilter ? 'trigger' : 'manage');
1145
+ setStep(startsOnAddFilter ? 'fields' : 'manage');
1146
+ setFieldPickerOrigin(startsOnAddFilter ? 'trigger' : 'manage');
997
1147
  } else {
998
1148
  setStep(null);
999
1149
  setIsClearConfirmationOpen(false);
1150
+ setIsDiscardConfirmationOpen(false);
1000
1151
  }
1001
1152
  }
1002
1153
 
@@ -1005,6 +1156,34 @@ export function PowerSearchBottomSheet({
1005
1156
  setStep(null);
1006
1157
  onOpenChange(false);
1007
1158
  }, [onOpenChange]);
1159
+ // Done replays each edit in order onto the field's latest filters, so a
1160
+ // consumer gets the calls a popover session makes and keeps a filter it
1161
+ // added meanwhile.
1162
+ const commitAndClose = useCallback(() => {
1163
+ // A field that became read-only while the flow was open reports nothing.
1164
+ if (isDraftDirty && isInteractive) {
1165
+ replayDraftEdits(committedFilters, draftEditsRef.current, onCommit);
1166
+ }
1167
+ draftEditsRef.current = [];
1168
+ closeFlow();
1169
+ }, [closeFlow, committedFilters, isDraftDirty, isInteractive, onCommit]);
1170
+ const requestDismiss = useCallback(() => {
1171
+ if (isDraftDirty) {
1172
+ setIsDiscardConfirmationOpen(true);
1173
+ } else {
1174
+ closeFlow();
1175
+ }
1176
+ }, [closeFlow, isDraftDirty]);
1177
+ const closeDiscardConfirmation = () => {
1178
+ // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves back into the sheet
1179
+ flushSync(() => setIsDiscardConfirmationOpen(false));
1180
+ const target =
1181
+ stepRef.current === 'manage'
1182
+ ? (managerAddButtonRef.current ??
1183
+ manageSheetRef.current?.closest<HTMLElement>('[tabindex="-1"]'))
1184
+ : fieldsSheetRef.current?.closest<HTMLElement>('[tabindex="-1"]');
1185
+ target?.focus({preventScroll: true});
1186
+ };
1008
1187
 
1009
1188
  // Whether a Filters row is a button: the field interactive and the
1010
1189
  // filter not read-only. The row opens the value step — the editor for its
@@ -1279,17 +1458,8 @@ export function PowerSearchBottomSheet({
1279
1458
  // is quiet then, so the change is spoken here, inside the dialog. Only a
1280
1459
  // change after the flow opened is spoken.
1281
1460
  const announcedResultCountRef = useRef(resultCountText);
1282
- // The latest result count, for Clear all's handler to read after its
1283
- // commit has flushed.
1284
- const latestResultCountRef = useRef(resultCountText);
1285
- useLayoutEffect(() => {
1286
- latestResultCountRef.current = resultCountText;
1287
- }, [resultCountText]);
1288
1461
  useEffect(() => {
1289
- if (
1290
- isClearingRef.current ||
1291
- resultCountText === announcedResultCountRef.current
1292
- ) {
1462
+ if (resultCountText === announcedResultCountRef.current) {
1293
1463
  return;
1294
1464
  }
1295
1465
  announcedResultCountRef.current = resultCountText;
@@ -1630,52 +1800,31 @@ export function PowerSearchBottomSheet({
1630
1800
  // confirmation must be gone before focus moves into the sheet and the
1631
1801
  // outcome is routed into the sheet's region, so its close is flushed
1632
1802
  // first. The outcome is "Editable filters removed" while a read-only filter
1633
- // remains, otherwise "All filters removed", joined with a result count that
1634
- // changed in the same commit as one catalog message; focus moves to the
1635
- // sheet's first remaining control: a kept read-only row has none, so Add
1636
- // filter.
1803
+ // remains, otherwise "All filters removed"; the result count follows the
1804
+ // applied filters, which change on Done, so none changes here. Focus moves
1805
+ // to the sheet's first remaining control: a kept read-only row has none,
1806
+ // so Add filter.
1637
1807
  const closeClearConfirmation = (confirmed: boolean) => {
1638
1808
  const kept = filters.filter(filter => filter.isReadOnly);
1639
1809
  const firstRemovedIndex = filters.findIndex(filter => !filter.isReadOnly);
1640
1810
  const isClearing = confirmed && isInteractive && firstRemovedIndex >= 0;
1641
- isClearingRef.current = isClearing;
1642
- try {
1643
- // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves into the sheet and the words are routed into it
1644
- flushSync(() => {
1645
- setIsClearConfirmationOpen(false);
1646
- if (isClearing) {
1647
- onChange(kept, 'remove', firstRemovedIndex);
1648
- }
1649
- });
1650
- } finally {
1651
- isClearingRef.current = false;
1652
- }
1811
+ // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves into the sheet and the words are routed into it
1812
+ flushSync(() => {
1813
+ setIsClearConfirmationOpen(false);
1814
+ if (isClearing) {
1815
+ onChange(kept, 'remove', firstRemovedIndex);
1816
+ }
1817
+ });
1653
1818
  if (!isClearing) {
1654
1819
  clearAllButtonRef.current?.focus();
1655
1820
  return;
1656
1821
  }
1657
1822
  managerAddButtonRef.current?.focus({preventScroll: true});
1658
- const resultCount = latestResultCountRef.current;
1659
- const changedCount =
1660
- resultCount !== announcedResultCountRef.current ? resultCount : null;
1661
- announcedResultCountRef.current = resultCount;
1662
- if (kept.length > 0) {
1663
- announce(
1664
- changedCount == null
1665
- ? t('@astryx.powersearch.editableFiltersRemoved')
1666
- : t('@astryx.powersearch.sheet.editableFiltersClearedWithCount', {
1667
- resultCount: changedCount,
1668
- }),
1669
- );
1670
- } else {
1671
- announce(
1672
- changedCount == null
1673
- ? t('@astryx.powersearch.allFiltersRemoved')
1674
- : t('@astryx.powersearch.sheet.filtersClearedWithCount', {
1675
- resultCount: changedCount,
1676
- }),
1677
- );
1678
- }
1823
+ announce(
1824
+ kept.length > 0
1825
+ ? t('@astryx.powersearch.editableFiltersRemoved')
1826
+ : t('@astryx.powersearch.allFiltersRemoved'),
1827
+ );
1679
1828
  };
1680
1829
 
1681
1830
  const handleOperatorSelect = useCallback((operator: PowerSearchOperator) => {
@@ -1776,10 +1925,10 @@ export function PowerSearchBottomSheet({
1776
1925
  const handleManageOpenChange = useCallback(
1777
1926
  (isSheetOpen: boolean) => {
1778
1927
  if (!isSheetOpen && stepRef.current === 'manage') {
1779
- closeFlow();
1928
+ requestDismiss();
1780
1929
  }
1781
1930
  },
1782
- [closeFlow],
1931
+ [requestDismiss],
1783
1932
  );
1784
1933
  const handleFieldsOpenChange = useCallback(
1785
1934
  (isSheetOpen: boolean) => {
@@ -1787,13 +1936,13 @@ export function PowerSearchBottomSheet({
1787
1936
  return;
1788
1937
  }
1789
1938
  if (fieldPickerOrigin === 'trigger') {
1790
- closeFlow();
1939
+ requestDismiss();
1791
1940
  return;
1792
1941
  }
1793
1942
  queueManagerAddFocus();
1794
1943
  setStep('manage');
1795
1944
  },
1796
- [closeFlow, fieldPickerOrigin, queueManagerAddFocus],
1945
+ [fieldPickerOrigin, queueManagerAddFocus, requestDismiss],
1797
1946
  );
1798
1947
  const handleValueOpenChange = useCallback(
1799
1948
  (isSheetOpen: boolean) => {
@@ -2076,7 +2225,11 @@ export function PowerSearchBottomSheet({
2076
2225
  // is spoken in this sheet when it appears.
2077
2226
  isManageSheetOpen && (
2078
2227
  <EmptyState
2079
- title={t('@astryx.powersearch.sheet.noFilters')}
2228
+ title={t(
2229
+ isDraftDirty
2230
+ ? '@astryx.powersearch.sheet.noFiltersInDraft'
2231
+ : '@astryx.powersearch.sheet.noFilters',
2232
+ )}
2080
2233
  description={t(
2081
2234
  '@astryx.powersearch.sheet.noFiltersDescription',
2082
2235
  )}
@@ -2157,7 +2310,10 @@ export function PowerSearchBottomSheet({
2157
2310
  )}
2158
2311
  </div>
2159
2312
  </div>
2160
- {filterRows.length > 0 && (
2313
+ {/* The footer stays while the draft differs from the applied
2314
+ filters: an emptied draft still needs Done to apply the
2315
+ removals. */}
2316
+ {(filterRows.length > 0 || isDraftDirty) && (
2161
2317
  <div {...stylex.props(styles.footer, styles.fixedFooter)}>
2162
2318
  {isClearAllShown && (
2163
2319
  <Button
@@ -2178,7 +2334,7 @@ export function PowerSearchBottomSheet({
2178
2334
  ? styles.footerEqualAction
2179
2335
  : [styles.footerSoleAction, styles.touchHitArea]
2180
2336
  }
2181
- onClick={closeFlow}
2337
+ onClick={commitAndClose}
2182
2338
  width={isClearAllShown ? undefined : '100%'}
2183
2339
  />
2184
2340
  </div>
@@ -2373,7 +2529,7 @@ export function PowerSearchBottomSheet({
2373
2529
  variant="primary"
2374
2530
  width="100%"
2375
2531
  xstyle={styles.touchAction}
2376
- onClick={closeFlow}
2532
+ onClick={commitAndClose}
2377
2533
  />
2378
2534
  </div>
2379
2535
  )}
@@ -2518,6 +2674,25 @@ export function PowerSearchBottomSheet({
2518
2674
  onAction={() => closeClearConfirmation(true)}
2519
2675
  />
2520
2676
  )}
2677
+ {isDiscardConfirmationOpen && (
2678
+ <SheetConfirmation
2679
+ isOpen
2680
+ onOpenChange={isConfirmationOpen => {
2681
+ if (!isConfirmationOpen) {
2682
+ closeDiscardConfirmation();
2683
+ }
2684
+ }}
2685
+ title={t('@astryx.powersearch.sheet.discardTitle')}
2686
+ description={t('@astryx.powersearch.sheet.discardDescription')}
2687
+ actionLabel={t('@astryx.powersearch.sheet.discardConfirm')}
2688
+ cancelLabel={t('@astryx.powersearch.sheet.keepEditing')}
2689
+ onAction={() => {
2690
+ setIsDiscardConfirmationOpen(false);
2691
+ draftEditsRef.current = [];
2692
+ closeFlow();
2693
+ }}
2694
+ />
2695
+ )}
2521
2696
  </>
2522
2697
  );
2523
2698
  }
@@ -18,15 +18,18 @@ const english: Readonly<Record<string, string>> = {
18
18
  "@astryx.powersearch.sheet.clearAllConfirm": "Clear filters",
19
19
  "@astryx.powersearch.sheet.clearAllDescription": "This removes all editable filters from this search.",
20
20
  "@astryx.powersearch.sheet.clearAllTitle": "Clear all filters?",
21
+ "@astryx.powersearch.sheet.discardConfirm": "Discard changes",
22
+ "@astryx.powersearch.sheet.discardDescription": "Your changes to the filters haven't been applied.",
23
+ "@astryx.powersearch.sheet.discardTitle": "Discard changes?",
21
24
  "@astryx.powersearch.sheet.done": "Done",
22
- "@astryx.powersearch.sheet.editableFiltersClearedWithCount": "Editable filters removed. {resultCount}",
23
25
  "@astryx.powersearch.sheet.fieldOperator": "{field} {operator}",
24
26
  "@astryx.powersearch.sheet.filterUnavailable": "This filter is no longer available",
25
- "@astryx.powersearch.sheet.filtersClearedWithCount": "All filters removed. {resultCount}",
26
27
  "@astryx.powersearch.sheet.filtersTitle": "Filters",
28
+ "@astryx.powersearch.sheet.keepEditing": "Keep editing",
27
29
  "@astryx.powersearch.sheet.labelValue": "{label} {value}",
28
30
  "@astryx.powersearch.sheet.noFilters": "No filters applied",
29
31
  "@astryx.powersearch.sheet.noFiltersDescription": "Add a filter to narrow your results and find what you need faster.",
32
+ "@astryx.powersearch.sheet.noFiltersInDraft": "No filters",
30
33
  "@astryx.powersearch.sheet.noMatchingFields": "No filters match your search",
31
34
  "@astryx.powersearch.sheet.noResults": "No results found",
32
35
  "@astryx.powersearch.sheet.removeFilter": "Remove {field} filter",
@@ -81,6 +81,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
81
81
  '@astryx.powersearch.sheet.filtersTitle': 'Filters',
82
82
  '@astryx.powersearch.sheet.addFilter': 'Add filter',
83
83
  '@astryx.powersearch.sheet.noFilters': 'No filters applied',
84
+ '@astryx.powersearch.sheet.noFiltersInDraft': 'No filters',
84
85
  '@astryx.powersearch.sheet.noFiltersDescription':
85
86
  'Add a filter to narrow your results and find what you need faster.',
86
87
  '@astryx.powersearch.sheet.clearAll': 'Clear all',
@@ -90,6 +91,11 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
90
91
  '@astryx.powersearch.sheet.clearAllConfirm': 'Clear filters',
91
92
  '@astryx.powersearch.sheet.clearAllCancel': 'Keep filters',
92
93
  '@astryx.powersearch.sheet.done': 'Done',
94
+ '@astryx.powersearch.sheet.discardTitle': 'Discard changes?',
95
+ '@astryx.powersearch.sheet.discardDescription':
96
+ "Your changes to the filters haven't been applied.",
97
+ '@astryx.powersearch.sheet.keepEditing': 'Keep editing',
98
+ '@astryx.powersearch.sheet.discardConfirm': 'Discard changes',
93
99
  '@astryx.powersearch.sheet.search': 'Search',
94
100
  '@astryx.powersearch.sheet.searchResults': 'Search results',
95
101
  '@astryx.powersearch.sheet.searchFields': 'Search filters',
@@ -115,10 +121,6 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
115
121
  '{count, number} {count, plural, one {month} other {months}} from now',
116
122
  '@astryx.powersearch.sheet.fieldOperator': '{field} {operator}',
117
123
  '@astryx.powersearch.sheet.labelValue': '{label} {value}',
118
- '@astryx.powersearch.sheet.filtersClearedWithCount':
119
- 'All filters removed. {resultCount}',
120
- '@astryx.powersearch.sheet.editableFiltersClearedWithCount':
121
- 'Editable filters removed. {resultCount}',
122
124
  '@astryx.alertDialog.cancel': 'Cancel',
123
125
  '@astryx.appShell.mobileNavigation': 'Mobile navigation',
124
126
  '@astryx.appShell.skipToContent': 'Skip to content',
@@ -83,6 +83,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
83
83
  '@astryx.powersearch.sheet.filtersTitle': '⟦Ƒíłţéřš⟧',
84
84
  '@astryx.powersearch.sheet.addFilter': '⟦Àðð ƒíłţéř⟧',
85
85
  '@astryx.powersearch.sheet.noFilters': '⟦Ñó ƒíłţéřš àþþłíéð⟧',
86
+ '@astryx.powersearch.sheet.noFiltersInDraft': '⟦Ñó ƒíłţéřš⟧',
86
87
  '@astryx.powersearch.sheet.noFiltersDescription':
87
88
  '⟦Àðð à ƒíłţéř ţó ñàřřóŵ ýóúř řéšúłţš àñð ƒíñð ŵĥàţ ýóú ñééð ƒàšţéř.⟧',
88
89
  '@astryx.powersearch.sheet.clearAll': '⟦Çłéàř àłł⟧',
@@ -92,6 +93,11 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
92
93
  '@astryx.powersearch.sheet.clearAllConfirm': '⟦Çłéàř ƒíłţéřš⟧',
93
94
  '@astryx.powersearch.sheet.clearAllCancel': '⟦Ķééþ ƒíłţéřš⟧',
94
95
  '@astryx.powersearch.sheet.done': '⟦Ðóñé⟧',
96
+ '@astryx.powersearch.sheet.discardTitle': '⟦Ðíšçàřð çĥàñĝéš?⟧',
97
+ '@astryx.powersearch.sheet.discardDescription':
98
+ "⟦Ýóúř çĥàñĝéš ţó ţĥé ƒíłţéřš ĥàṽéñ'ţ ƀééñ àþþłíéð.⟧",
99
+ '@astryx.powersearch.sheet.keepEditing': '⟦Ķééþ éðíţíñĝ⟧',
100
+ '@astryx.powersearch.sheet.discardConfirm': '⟦Ðíšçàřð çĥàñĝéš⟧',
95
101
  '@astryx.powersearch.sheet.search': '⟦Šéàřçĥ⟧',
96
102
  '@astryx.powersearch.sheet.searchResults': '⟦Šéàřçĥ řéšúłţš⟧',
97
103
  '@astryx.powersearch.sheet.searchFields': '⟦Šéàřçĥ ƒíłţéřš⟧',
@@ -118,10 +124,6 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
118
124
  '⟦{count, number} {count, plural, one {month} other {months}} ƒřóɱ ñóŵ⟧',
119
125
  '@astryx.powersearch.sheet.fieldOperator': '⟦{field} {operator}⟧',
120
126
  '@astryx.powersearch.sheet.labelValue': '⟦{label} {value}⟧',
121
- '@astryx.powersearch.sheet.filtersClearedWithCount':
122
- '⟦Àłł ƒíłţéřš řéɱóṽéð. {resultCount}⟧',
123
- '@astryx.powersearch.sheet.editableFiltersClearedWithCount':
124
- '⟦Éðíţàƀłé ƒíłţéřš řéɱóṽéð. {resultCount}⟧',
125
127
  '@astryx.alertDialog.cancel': '⟦Çàñçéł⟧',
126
128
  '@astryx.appShell.mobileNavigation': '⟦Ṁóƀíłé ñàṽíĝàţíóñ⟧',
127
129
  '@astryx.appShell.skipToContent': '⟦Šķíþ ţó çóñţéñţ⟧',