@wix/himalaya-cli 0.1311.0 → 0.1313.0

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.
Files changed (185) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/auth-providers/wix/src/index.d.ts +8 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-auth.mjs +4 -2
  3. package/dist/cli.mjs +54 -34
  4. package/dist/preview-assets.generated.bin +0 -0
  5. package/guides/web-page-native-sdk.md +18 -8
  6. package/guides/webview-page-sdk.md +9 -3
  7. package/package.json +2 -2
  8. package/templates/beauty-salon/dev/index.ts +25 -25
  9. package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +10 -0
  10. package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +3 -1
  11. package/templates/beauty-salon-owner/dev/index.ts +844 -510
  12. package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +10 -0
  13. package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +3 -1
  14. package/templates/beauty-salon-owner/tier5-src/_shared/detail-facts.ts +26 -0
  15. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
  16. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
  17. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
  18. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
  19. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
  20. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
  21. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
  22. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
  23. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service/index.ts +10 -0
  24. package/templates/branded-lite/dev/index.ts +120 -120
  25. package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +10 -0
  26. package/templates/branded-lite/tier5-src/_shared/collection-service.ts +3 -1
  27. package/templates/community-club/dev/index.ts +20 -20
  28. package/templates/community-club/tier5-src/_shared/collection-contract.ts +10 -0
  29. package/templates/community-club/tier5-src/_shared/collection-service.ts +3 -1
  30. package/templates/community-club-owner/dev/index.ts +957 -595
  31. package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +10 -0
  32. package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +3 -1
  33. package/templates/community-club-owner/tier5-src/_shared/detail-facts.ts +26 -0
  34. package/templates/community-club-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
  35. package/templates/community-club-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
  36. package/templates/community-club-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
  37. package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
  38. package/templates/community-club-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
  39. package/templates/community-club-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
  40. package/templates/community-club-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
  41. package/templates/community-club-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
  42. package/templates/community-club-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
  43. package/templates/events-meetups/dev/index.ts +15 -15
  44. package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +10 -0
  45. package/templates/events-meetups/tier5-src/_shared/collection-service.ts +3 -1
  46. package/templates/events-meetups-owner/dev/index.ts +726 -433
  47. package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +10 -0
  48. package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +3 -1
  49. package/templates/events-meetups-owner/tier5-src/_shared/detail-facts.ts +26 -0
  50. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
  51. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
  52. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
  53. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
  54. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
  55. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
  56. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
  57. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
  58. package/templates/gym-fitness/dev/index.ts +30 -30
  59. package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +10 -0
  60. package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +3 -1
  61. package/templates/gym-fitness-owner/dev/index.ts +1041 -644
  62. package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +10 -0
  63. package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +3 -1
  64. package/templates/gym-fitness-owner/tier5-src/_shared/detail-facts.ts +26 -0
  65. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
  66. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
  67. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
  68. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
  69. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
  70. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
  71. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
  72. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
  73. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
  74. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service/index.ts +10 -0
  75. package/templates/nonprofit-cause/dev/index.ts +20 -20
  76. package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +10 -0
  77. package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +3 -1
  78. package/templates/nonprofit-cause-owner/dev/index.ts +810 -482
  79. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +10 -0
  80. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +3 -1
  81. package/templates/nonprofit-cause-owner/tier5-src/_shared/detail-facts.ts +26 -0
  82. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
  83. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
  84. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
  85. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
  86. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
  87. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
  88. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
  89. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
  90. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service/index.ts +10 -0
  91. package/templates/owner-lite/dev/index.ts +1066 -648
  92. package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +10 -0
  93. package/templates/owner-lite/tier5-src/_shared/collection-service.ts +3 -1
  94. package/templates/owner-lite/tier5-src/_shared/detail-facts.ts +26 -0
  95. package/templates/owner-lite/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
  96. package/templates/owner-lite/tier5-src/himi-owner-lite-ask/index.ts +2 -2
  97. package/templates/owner-lite/tier5-src/himi-owner-lite-event/index.ts +10 -0
  98. package/templates/owner-lite/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
  99. package/templates/owner-lite/tier5-src/himi-owner-lite-post/index.ts +10 -0
  100. package/templates/owner-lite/tier5-src/himi-owner-lite-posts/index.ts +21 -2
  101. package/templates/owner-lite/tier5-src/himi-owner-lite-product/index.ts +10 -0
  102. package/templates/owner-lite/tier5-src/himi-owner-lite-search/index.ts +6 -0
  103. package/templates/owner-lite/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
  104. package/templates/owner-lite/tier5-src/himi-owner-lite-service/index.ts +10 -0
  105. package/templates/podcast-creator/dev/index.ts +20 -20
  106. package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +10 -0
  107. package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +3 -1
  108. package/templates/podcast-creator-owner/dev/index.ts +957 -595
  109. package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +10 -0
  110. package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +3 -1
  111. package/templates/podcast-creator-owner/tier5-src/_shared/detail-facts.ts +26 -0
  112. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
  113. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
  114. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
  115. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
  116. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
  117. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
  118. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
  119. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
  120. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
  121. package/templates/portfolio-creative/dev/index.ts +20 -20
  122. package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +10 -0
  123. package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +3 -1
  124. package/templates/portfolio-creative-owner/dev/index.ts +844 -510
  125. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +10 -0
  126. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +3 -1
  127. package/templates/portfolio-creative-owner/tier5-src/_shared/detail-facts.ts +26 -0
  128. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
  129. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
  130. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
  131. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
  132. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
  133. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
  134. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
  135. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
  136. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service/index.ts +10 -0
  137. package/templates/restaurant/dev/index.ts +15 -15
  138. package/templates/restaurant/tier5-src/_shared/collection-contract.ts +10 -0
  139. package/templates/restaurant/tier5-src/_shared/collection-service.ts +3 -1
  140. package/templates/restaurant-owner/dev/index.ts +1041 -644
  141. package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +10 -0
  142. package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +3 -1
  143. package/templates/restaurant-owner/tier5-src/_shared/detail-facts.ts +26 -0
  144. package/templates/restaurant-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
  145. package/templates/restaurant-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
  146. package/templates/restaurant-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
  147. package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
  148. package/templates/restaurant-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
  149. package/templates/restaurant-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
  150. package/templates/restaurant-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
  151. package/templates/restaurant-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
  152. package/templates/restaurant-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
  153. package/templates/restaurant-owner/tier5-src/himi-owner-lite-service/index.ts +10 -0
  154. package/templates/retail-store/dev/index.ts +10 -10
  155. package/templates/retail-store/tier5-src/_shared/collection-contract.ts +10 -0
  156. package/templates/retail-store/tier5-src/_shared/collection-service.ts +3 -1
  157. package/templates/retail-store-owner/dev/index.ts +760 -461
  158. package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +10 -0
  159. package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +3 -1
  160. package/templates/retail-store-owner/tier5-src/_shared/detail-facts.ts +26 -0
  161. package/templates/retail-store-owner/tier5-src/_shared/offset-page.ts +10 -0
  162. package/templates/retail-store-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
  163. package/templates/retail-store-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
  164. package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
  165. package/templates/retail-store-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
  166. package/templates/retail-store-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
  167. package/templates/retail-store-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
  168. package/templates/retail-store-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
  169. package/templates/retail-store-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
  170. package/templates/yoga-studio/dev/index.ts +30 -30
  171. package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +10 -0
  172. package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +3 -1
  173. package/templates/yoga-studio-owner/dev/index.ts +810 -482
  174. package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +10 -0
  175. package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +3 -1
  176. package/templates/yoga-studio-owner/tier5-src/_shared/detail-facts.ts +26 -0
  177. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
  178. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
  179. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
  180. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
  181. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
  182. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
  183. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
  184. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
  185. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service/index.ts +10 -0
@@ -58,6 +58,13 @@ export interface CollectionConfig {
58
58
  /** At or below this many records, with no query/filter narrowing them and no further
59
59
  * page, the list/grid toggle, Refine, chips, search and count are noise. */
60
60
  export const COLLECTION_CHROME_MIN_ITEMS = 4;
61
+ /** The accessible name of a row's overflow menu: `template` with `{title}` filled, or the
62
+ * generic `fallback` for a record with no title. */
63
+ export function collectionActionsLabel(title: string, template: string, fallback: string): string {
64
+ const t = title.trim();
65
+ return t ? template.replace('{title}', () => t) : fallback;
66
+ }
67
+
61
68
  export const COLLECTIONS: Record<string, CollectionConfig> = {
62
69
  himiStoreShop: { rows: 'results', title: 'name', subtitle: 'subtitle', image: 'image', view: 'grid', backendQuery: true, countNoun: ['product', 'products'], favoriteToggle: true },
63
70
  himiBookingServices: { rows: 'results', title: 'name', subtitle: 'subtitle', image: 'image', filter: 'category', view: 'grid', backendQuery: true, searchPlaceholder: 'Search services', countNoun: ['service', 'services'], primaryVerb: { label: 'Book', screenId: 'himi_time' } },
@@ -184,6 +191,9 @@ export function projectCollection(source: CollectionRow[], config: CollectionCon
184
191
  collectionSelectLabel: prefs.selectedIds.includes(collectionId(row)) ? 'Selected' : 'Select',
185
192
  // The save toggle names the action it WILL take (the service localizes it).
186
193
  collectionSaveLabel: prefs.savedIds.includes(collectionId(row)) ? 'Remove from saved' : 'Save',
194
+ // The row's overflow menu, named for its record: out of context (a rotor, a voice
195
+ // command) a list of identical "Item actions" cannot be told apart.
196
+ collectionActionsLabel: collectionActionsLabel(collectionTitle(row, config), 'Actions for {title}', 'Item actions'),
187
197
  collectionSubtitle: scalar(row[config.subtitle ?? '']) || scalar(row.subtitle) || scalar(row.description) || scalar(row.meta),
188
198
  collectionImage: scalar(row[config.image ?? '']), collectionNoImage: !scalar(row[config.image ?? '']), collectionHasImage: !!scalar(row[config.image ?? '']),
189
199
  collectionSaved: prefs.savedIds.includes(collectionId(row)), collectionNotSaved: !prefs.savedIds.includes(collectionId(row)), collectionSelected: prefs.selectedIds.includes(collectionId(row)),
@@ -1,5 +1,5 @@
1
1
  import { defineService as register, type ServiceDefinition } from '@himalaya/state';
2
- import { COLLECTIONS, COLLECTION_CHROME_MIN_ITEMS, initialCollection, normalizeCollection, projectCollection, collectionId, type CollectionRow } from './collection-contract.js';
2
+ import { COLLECTIONS, COLLECTION_CHROME_MIN_ITEMS, initialCollection, normalizeCollection, projectCollection, collectionId, collectionActionsLabel, type CollectionRow } from './collection-contract.js';
3
3
  import { localPreferences, type PreferenceRuntime } from './local-preferences.js';
4
4
  import { copyParam, stashCopy } from './himi-copy.js';
5
5
  import { createCollectionQuery, type CollectionQuerySource, type CollectionQueryState } from './collection-query.js';
@@ -130,6 +130,8 @@ export function decorateCollection(name: string, def: { state: State; actions?:
130
130
  // the save toggle names the action it WILL take for this row. The
131
131
  // old static "Save / unsave" made the owner work out the row's state.
132
132
  row.collectionSaveLabel = row.collectionSaved ? copyParam('collection.unsave', 'Remove from saved') : copyParam('collection.save', 'Save');
133
+ row.collectionActionsLabel = collectionActionsLabel(String(row.collectionTitle ?? ''),
134
+ copyParam('collection.itemActionsFor', 'Actions for {title}'), copyParam('collection.itemActions', 'Item actions'));
133
135
  });
134
136
  if (previewId && !rows.some(row => collectionId(row) === previewId)) previewId = '';
135
137
  const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, { id: 'saved', label: copyParam('collection.saved', 'Saved') },
@@ -0,0 +1,26 @@
1
+ // Blank flags for a detail screen's fact rows.
2
+ //
3
+ // A detail screen lists fixed facts (Price, Status, SKU, Inventory…). A fact with no value
4
+ // painted its label beside nothing — "SKU" with a blank, "Registration" with a blank — which
5
+ // reads as a broken screen, or as a value that failed to load. `hiddenWhen` hides on a TRUE
6
+ // scalar only, so the worker publishes `<key>Blank` beside each fact and the descriptor hides
7
+ // the row (and its divider) on it. "—" counts as blank: it is the platform's canonical "no value"
8
+ // marker, which a `collapseWhenEmpty` Text collapses on web, iOS and Android alike — so a "—"
9
+ // value already painted nothing beside its label.
10
+
11
+ //
12
+ // Beside `<key>Blank`: `<key>NoDivider` (hide the rule above this row — it is blank, or nothing
13
+ // above it shows, so the card cannot open with a stray rule) and `factsBlank` (hide the card).
14
+ export function blankFlags(state: Record<string, unknown>, keys: readonly string[]): Record<string, boolean> {
15
+ const out: Record<string, boolean> = {};
16
+ let shownAbove = false;
17
+ for (const key of keys) {
18
+ const value = state[key];
19
+ const blank = typeof value === "number" ? !Number.isFinite(value) : typeof value !== "string" || value.trim() === "" || value.trim() === "—";
20
+ out[`${key}Blank`] = blank;
21
+ out[`${key}NoDivider`] = blank || !shownAbove;
22
+ if (!blank) shownAbove = true;
23
+ }
24
+ out.factsBlank = !shownAbove;
25
+ return out;
26
+ }
@@ -0,0 +1,10 @@
1
+ /** Offset APIs keep the offset opaque to collection UI. Advance by raw rows,
2
+ * never by a member/status-filtered projection. A full page without a total is
3
+ * deliberately incomplete until a subsequent (possibly empty) page confirms it. */
4
+ export function offsetPage(cursor: string, count: number, limit: number, metadata?: { total?: number }) {
5
+ const offset = cursor ? Number(cursor) : 0;
6
+ if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("Invalid page offset");
7
+ const total = typeof metadata?.total === "number" && Number.isFinite(metadata.total) && metadata.total >= 0 ? metadata.total : undefined;
8
+ const next = offset + count;
9
+ return { nextCursor: count > 0 && (total === undefined ? count >= limit : next < total) ? String(next) : "", ...(total === undefined ? {} : { total }) };
10
+ }
@@ -528,11 +528,15 @@ function listBlock(block: AskBlock, key: string, binding: Binding): Node | null
528
528
  },
529
529
  pressFeedback: "scale",
530
530
  haptic: "light",
531
- accessibilityLabel: `${key} ${proposeVerb} row ${i + 1}`,
531
+ // Its own visible verb. It sits inside a row named for its record, and a block key plus an
532
+ // index ("b1 delete row 1") is not a name anyone can act on.
533
+ accessibilityLabel: VERB_LABEL[proposeVerb] ?? proposeVerb,
532
534
  padding: { leading: 8, trailing: 8, top: 6, bottom: 6 },
533
535
  }));
534
536
  }
535
- const modifiers: Record<string, unknown> = { fillMaxWidth: true, accessibilityLabel: `${key} row ${i + 1}` };
537
+ // No label override: the row reads its own title, subtitle, badge and value. It was named
538
+ // "b1 row 1" — the block key and an index, an internal id spoken in place of all of them.
539
+ const modifiers: Record<string, unknown> = { fillMaxWidth: true };
536
540
  if (tappable) {
537
541
  modifiers.onTap = {
538
542
  actionId: OPEN_ACTION,
@@ -617,7 +621,7 @@ function timelineBlock(block: AskBlock, key: string, binding: Binding): Node | n
617
621
  // title barely a third of a phone's width.
618
622
  const badge = rowBadge(result, query, i, idFor(`${key}_r${i}_b`), [titleField, block.subtitleField, timeField]);
619
623
  if (badge) detail.push(badge);
620
- const modifiers: Record<string, unknown> = { fillMaxWidth: true, accessibilityLabel: `${key} entry ${i + 1}` };
624
+ const modifiers: Record<string, unknown> = { fillMaxWidth: true };
621
625
  if (tappable) {
622
626
  modifiers.onTap = { actionId: OPEN_ACTION, params: { id: result.rows[i]?.id ?? "", entity: String(entity) } };
623
627
  modifiers.pressFeedback = "scale";
@@ -506,7 +506,7 @@ const ask = defineService<AskState, {
506
506
  noAfterAnswer: state.noAnswer && recents.length === 0,
507
507
  // A saved link whose question has aged out of the recents list can't
508
508
  // be replayed — say so rather than opening a blank screen.
509
- ...(id ? { headline: "That saved answer is gone", note: "Ask the question again to bring it back." } : {}),
509
+ ...(id ? { headline: "That saved answer is gone", note: "Ask the question again to bring it back.", customerNotice: "Ask the question again to bring it back." } : {}),
510
510
  };
511
511
  }
512
512
  return askTurn({ ...state, intent }, intent, false);
@@ -614,7 +614,7 @@ const ask = defineService<AskState, {
614
614
  const screenId = DETAIL_SCREEN[entity];
615
615
  // No detail screen for this entity is a dead end, not a crash: say so
616
616
  // rather than navigating somewhere arbitrary.
617
- if (!screenId) return { ...state, note: `${singular(entity)}s have no detail screen yet.` };
617
+ if (!screenId) { const notice = `${singular(entity)}s have no detail screen yet.`; return { ...state, note: notice, customerNotice: notice }; }
618
618
  return navigate(state, { screenId, args: { id } });
619
619
  },
620
620
  },
@@ -50,6 +50,8 @@ export interface ManageArea {
50
50
  unavailable?: boolean;
51
51
  hideCreate?: boolean;
52
52
  hideHandoff?: boolean;
53
+ /** The accessible name of this area's "Open in Wix", which carries the area ("Open in Wix: Store"). */
54
+ handoffLabel?: string;
53
55
  }
54
56
 
55
57
  export interface ManageSection {
@@ -95,6 +97,7 @@ export function parseAreas(value: unknown): ManageArea[] {
95
97
  ...(createScreenId && createLabel ? { createScreenId, createLabel } : {}),
96
98
  hideCreate: !(createScreenId && createLabel),
97
99
  hideHandoff: !DASHBOARD_PATHS[area.id],
100
+ handoffLabel: clean(raw.handoffLabel) || `Open in Wix: ${area.title}`,
98
101
  metricText: "—",
99
102
  statusText: "",
100
103
  unavailable: false,
@@ -15,6 +15,7 @@ import { DEMO_POSTS, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-preview-
15
15
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
16
16
  import { get as dbGet, upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
17
17
  import { formatDateMedium } from "../_shared/engine-intl.js";
18
+ import { blankFlags } from "../_shared/detail-facts.js";
18
19
 
19
20
  // Offline-first (data-offline): owner CRUD is WRITE-BEHIND against the encrypted
20
21
  // `owner_posts` entity — publish/unpublish/delete update the local store
@@ -121,7 +122,16 @@ function statusLabel(raw?: string): string {
121
122
 
122
123
  function coverUrl(raw: any): string { return postCoverUrl(raw); }
123
124
 
125
+ /** The fact rows this detail shows (the descriptor"s DETAIL_T5 rows): each publishes
126
+ * `<key>Blank`, so a fact with no value hides instead of painting a dangling label. */
127
+ const FACT_KEYS = ["postStatus", "category", "published", "updated"] as const;
128
+
124
129
  function shape(s: Partial<PostState>): PostState {
130
+ const out = shapeFields(s);
131
+ return { ...out, ...blankFlags(out as unknown as Record<string, unknown>, FACT_KEYS) };
132
+ }
133
+
134
+ function shapeFields(s: Partial<PostState>): PostState {
125
135
  const status = s.status ?? "ready";
126
136
  const raw = s.postStatusRaw ?? "";
127
137
  const image = s.image ?? "";
@@ -20,7 +20,19 @@ import { wixMediaUrl } from "@himalaya/wix-media";
20
20
  // without the `local_database` grant behaves exactly as today (online-only).
21
21
  const ENTITY = "owner_posts";
22
22
  async function readCachedRows(): Promise<PostRow[]> {
23
- try { return (await getAll(ENTITY)) as unknown as PostRow[]; } catch { return []; }
23
+ try { return ((await getAll(ENTITY)) as unknown as PostRow[]).map(withStatus); } catch { return []; }
24
+ }
25
+
26
+ /** A row cached by an older bundle has no `statusLabel`. Its `dateLabel` led with the draft status
27
+ * ("Draft · Oct 1", "Scheduled · …") and a published row carried only a date, so derive it. */
28
+ function withStatus(row: PostRow): PostRow {
29
+ const dateLabel = String(row?.dateLabel ?? "");
30
+ const lead = dateLabel.split(" · ")[0];
31
+ const statusLabel = typeof row?.statusLabel === "string" && row.statusLabel
32
+ ? row.statusLabel
33
+ : /^(Draft|Scheduled)$/i.test(lead) ? lead : copyParam("ownerPosts.published", "Published");
34
+ // The offline/seed row paints `dateLabel`, so it must lead with the status too.
35
+ return { ...row, statusLabel, dateLabel: lead === statusLabel ? dateLabel : [statusLabel, dateLabel].filter(Boolean).join(" · ") };
24
36
  }
25
37
  async function writeCachedRows(rows: PostRow[]): Promise<void> {
26
38
  try { for (const r of rows) await upsert(ENTITY, r as unknown as DbEntity); } catch { /* capability not granted → online-only */ }
@@ -31,6 +43,9 @@ interface PostRow {
31
43
  title: string;
32
44
  excerpt: string;
33
45
  dateLabel: string;
46
+ /** Draft / Scheduled / Published. The collection row's meta line shows it, so a draft never
47
+ * looks like a live post. */
48
+ statusLabel: string;
34
49
  image: string; // cover image (web URL, "" = none → initial badge)
35
50
  initial: string;
36
51
  hasImage: boolean;
@@ -81,7 +96,9 @@ function normalize(raw: { id?: string; title?: string; excerpt?: string; firstPu
81
96
  id: raw.id || "",
82
97
  title,
83
98
  excerpt: (raw.excerpt || "").trim(),
84
- dateLabel: fmtDate(raw.firstPublishedDate || raw.lastPublishedDate || raw.editedDate || raw.createdDate),
99
+ // Status first, like a draft's — the offline/seed row shows this line, not the collection meta.
100
+ dateLabel: [copyParam("ownerPosts.published", "Published"), fmtDate(raw.firstPublishedDate || raw.lastPublishedDate || raw.editedDate || raw.createdDate)].filter(Boolean).join(" · "),
101
+ statusLabel: copyParam("ownerPosts.published", "Published"),
85
102
  image,
86
103
  initial: (title.trim()[0] || "?").toUpperCase(),
87
104
  hasImage: !!image,
@@ -111,6 +128,7 @@ function normalizeDraft(raw: { id?: string; title?: string; excerpt?: string; st
111
128
  title,
112
129
  excerpt: (raw.excerpt || "").trim(),
113
130
  dateLabel: [draftStatusLabel(raw.status), when].filter(Boolean).join(" · "),
131
+ statusLabel: draftStatusLabel(raw.status),
114
132
  image,
115
133
  initial: (title.trim()[0] || "?").toUpperCase(),
116
134
  hasImage: !!image,
@@ -163,6 +181,7 @@ function demoList(s: PostsState): PostsState {
163
181
  title: d.title,
164
182
  excerpt: d.excerpt,
165
183
  dateLabel: d.dateLabel,
184
+ statusLabel: d.postStatus,
166
185
  image: "",
167
186
  initial: (d.title[0] || "?").toUpperCase(),
168
187
  hasImage: false,
@@ -11,6 +11,7 @@ import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwn
11
11
  import { DEMO_PRODUCTS, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-preview-demo.js";
12
12
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
13
13
  import { get as dbGet, upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
14
+ import { blankFlags } from "../_shared/detail-facts.js";
14
15
 
15
16
  // Offline-first (data-offline): owner CRUD is WRITE-BEHIND against the encrypted
16
17
  // `owner_products` entity — the local store is updated OPTIMISTICALLY so the
@@ -109,7 +110,16 @@ function inventoryOf(p: WixProduct): string {
109
110
  return status ? status.charAt(0).toUpperCase() + status.slice(1) : "—";
110
111
  }
111
112
 
113
+ /** The fact rows this detail shows (the descriptor"s DETAIL_T5 rows): each publishes
114
+ * `<key>Blank`, so a fact with no value hides instead of painting a dangling label. */
115
+ const FACT_KEYS = ["price", "statusLabel", "sku", "inventory"] as const;
116
+
112
117
  function shape(s: Partial<ProductState>): ProductState {
118
+ const out = shapeFields(s);
119
+ return { ...out, ...blankFlags(out as unknown as Record<string, unknown>, FACT_KEYS) };
120
+ }
121
+
122
+ function shapeFields(s: Partial<ProductState>): ProductState {
113
123
  const status = s.status ?? "ready";
114
124
  const image = s.image ?? "";
115
125
  const visible = s.visibleRaw ?? true;
@@ -34,6 +34,7 @@ import {
34
34
  type SearchRow,
35
35
  type SearchState,
36
36
  } from "./logic.js";
37
+ import { verticalAdapter } from "./vertical-sources.js";
37
38
 
38
39
  /** How many loaded pages one source will walk looking for matches.
39
40
  *
@@ -196,7 +197,12 @@ export function adaptersFrom(raw: unknown): SearchAdapter[] {
196
197
  const ids = Array.isArray(raw) ? raw : [];
197
198
  const seen = new Set<OwnerModuleId>();
198
199
  const out: SearchAdapter[] = [];
200
+ const seenVertical = new Set<string>();
199
201
  for (const value of ids) {
202
+ // A vertical (orders, products, services, events, posts) is its own source with its own
203
+ // API — not a management module — so it is admitted by its own allowlist.
204
+ const vertical = typeof value === "string" && !seenVertical.has(value) ? verticalAdapter(value, normalizeQuery, classifyOwnerError) : null;
205
+ if (vertical) { seenVertical.add(value as string); out.push(vertical); continue; }
200
206
  const id = ownerModuleId(value);
201
207
  if (!id || seen.has(id)) continue;
202
208
  seen.add(id);
@@ -0,0 +1,178 @@
1
+ // Record search over the __HIMI_APP_NAME__ VERTICALS — orders, products, booking services, events and
2
+ // blog posts. These are the dedicated screens (`himi_owner_lite_orders` …), not management
3
+ // modules, so `queryOwnerModule` cannot read them: each has its own Wix API and its own detail
4
+ // screen. The search screen promised "Search orders, contacts, members…" and never asked for an
5
+ // order; these adapters are what makes that promise true.
6
+ //
7
+ // Same honesty rules as the module sources: matching is over the pages actually read (title and
8
+ // subtitle, exactly what the row shows), the walk is bounded, and no total is claimed. The source
9
+ // id IS the list screen's id, and every row routes to that vertical's own detail screen — the
10
+ // static destination its list rows already use, never a worker-built route.
11
+
12
+ import { searchOrders, type WixOrder } from "@himalaya/wix-ecom";
13
+ import { queryEvents } from "@himalaya/wix-events";
14
+ import { queryServices, type WixService } from "@himalaya/wix-bookings";
15
+ import { queryPosts, queryDraftPosts } from "@himalaya/wix-blog";
16
+ import { searchStoreProducts, productPrice, type StoreProduct } from "../_shared/himi-store.js";
17
+ import { normalizeEvent } from "../_shared/himi-events.js";
18
+ import { offsetPage } from "../_shared/offset-page.js";
19
+ import { isOwnerPreviewDemo } from "../_shared/owner-load-error.js";
20
+ import { DEMO_ORDERS, DEMO_PRODUCTS, DEMO_EVENTS, DEMO_POSTS, DEMO_SERVICES } from "../_shared/owner-preview-demo.js";
21
+ import { copyParam } from "../_shared/himi-copy.js";
22
+ import type { GroupProblem, SearchAdapter, SearchPage, SearchRow } from "./logic.js";
23
+
24
+ /** A vertical record before it becomes a row: what the row shows, and nothing else to match on. */
25
+ interface Found { id: string; title: string; subtitle: string; statusLabel?: string }
26
+
27
+ interface VerticalSource {
28
+ label: [key: string, fallback: string];
29
+ icon: string;
30
+ /** The vertical's detail screen — the one its own list rows open. */
31
+ detailScreenId: string;
32
+ /** One page of records. `cursor` is the source's own (an ecom cursor or an offset). */
33
+ page(query: string, cursor: string): Promise<{ items: Found[]; nextCursor: string }>;
34
+ demo(): Found[];
35
+ }
36
+
37
+ const PAGE = 50;
38
+ const MAX_PAGES = 3;
39
+ const join = (...parts: Array<string | undefined>) => parts.map((p) => (p ?? "").trim()).filter(Boolean).join(" · ");
40
+
41
+ const PAY_LABEL: Record<string, string> = {
42
+ PAID: "Paid", NOT_PAID: "Unpaid", PARTIALLY_PAID: "Partially paid", PENDING: "Pending",
43
+ FULLY_REFUNDED: "Refunded", PARTIALLY_REFUNDED: "Partly refunded",
44
+ };
45
+
46
+ function orderBuyer(raw: WixOrder): string {
47
+ const r = raw as unknown as { billingInfo?: { contactDetails?: { firstName?: string; lastName?: string } }; recipientInfo?: { contactDetails?: { firstName?: string; lastName?: string } } };
48
+ const c = r.billingInfo?.contactDetails ?? r.recipientInfo?.contactDetails;
49
+ return [c?.firstName, c?.lastName].filter(Boolean).join(" ").trim() || (raw.buyerInfo?.email ?? "").trim();
50
+ }
51
+
52
+ function orderFound(raw: WixOrder): Found {
53
+ const number = raw.number ? `#${raw.number}` : "";
54
+ const pay = PAY_LABEL[(raw.paymentStatus ?? "").toUpperCase()] ?? "";
55
+ // The number rides the SUBTITLE so "10041" finds the order the owner sees as "#10041".
56
+ return { id: raw.id ?? "", title: orderBuyer(raw) || number || "Order", subtitle: join(number, raw.priceSummary?.total?.formattedAmount, pay), statusLabel: pay };
57
+ }
58
+
59
+ function serviceFound(raw: WixService): Found {
60
+ const category = (raw as { category?: { name?: string } }).category?.name;
61
+ return { id: raw.id ?? "", title: (raw.name ?? "").trim() || "Service", subtitle: join(category) };
62
+ }
63
+
64
+ export const VERTICAL_SOURCES: Record<string, VerticalSource> = {
65
+ himi_owner_lite_orders: {
66
+ label: ["ownerSearch.source.orders", "Orders"],
67
+ icon: "cart",
68
+ detailScreenId: "himi_owner_lite_order",
69
+ async page(_query, cursor) {
70
+ const response = await searchOrders({ search: {
71
+ // The Orders list's own filter: an archived order is not one the owner can see there.
72
+ filter: { archived: false },
73
+ sort: [{ fieldName: "createdDate", order: "DESC" }],
74
+ cursorPaging: { limit: PAGE, ...(cursor ? { cursor } : {}) },
75
+ } });
76
+ const metadata = response.metadata as { cursors?: { next?: string | null } } | undefined;
77
+ return { items: (response.orders ?? []).filter((o) => !!o?.id).map(orderFound), nextCursor: metadata?.cursors?.next ?? "" };
78
+ },
79
+ demo: () => DEMO_ORDERS.map((d) => ({ id: d.id, title: d.buyer, subtitle: join(`#${d.number}`, d.total, d.paymentLabel), statusLabel: d.paymentLabel })),
80
+ },
81
+ himi_owner_lite_products: {
82
+ label: ["ownerSearch.source.products", "Store"],
83
+ icon: "bag",
84
+ detailScreenId: "himi_owner_lite_product",
85
+ async page(_query, cursor) {
86
+ // Read the catalog pages, like every other source here, and match locally on what the row
87
+ // shows. (V3 name search is a different endpoint with its own semantics; mixing a server
88
+ // match into a "loaded records" group would make its count mean two things.)
89
+ const response = await searchStoreProducts({ limit: PAGE, ...(cursor ? { cursor } : {}) });
90
+ const items = (response.products ?? []).filter((p: StoreProduct) => !!p?.id)
91
+ .map((p: StoreProduct) => ({ id: p.id ?? "", title: (p.name ?? "").trim() || "Untitled product", subtitle: productPrice(p) }));
92
+ return { items, nextCursor: response.pagingMetadata?.cursors?.next ?? "" };
93
+ },
94
+ demo: () => DEMO_PRODUCTS.map((d) => ({ id: d.id, title: d.name, subtitle: d.price })),
95
+ },
96
+ himi_owner_lite_services: {
97
+ label: ["ownerSearch.source.services", "Bookings"],
98
+ icon: "calendar",
99
+ detailScreenId: "himi_owner_lite_service",
100
+ async page(_query, cursor) {
101
+ const res = await queryServices({ query: { paging: { limit: PAGE, offset: Number(cursor || 0) }, sort: [{ fieldName: "name", order: "ASC" }] } });
102
+ const raw = res.services ?? [];
103
+ return { items: raw.filter((s) => !!s?.id).map(serviceFound), ...offsetPage(cursor, raw.length, PAGE, res.pagingMetadata) };
104
+ },
105
+ demo: () => DEMO_SERVICES.map((d) => ({ id: d.id, title: d.name, subtitle: join(d.category) })),
106
+ },
107
+ himi_owner_lite_events: {
108
+ label: ["ownerSearch.source.events", "Events"],
109
+ icon: "calendar",
110
+ detailScreenId: "himi_owner_lite_event",
111
+ async page(_query, cursor) {
112
+ const res = await queryEvents({ limit: PAGE, offset: Number(cursor || 0), query: { sort: [{ fieldName: "dateAndTimeSettings.startDate", order: "ASC" }, { fieldName: "id", order: "ASC" }] } });
113
+ const raw = res.events ?? [];
114
+ const items = raw.filter((e) => !!e?.id).map((e) => { const row = normalizeEvent(e); return { id: row.id, title: row.title, subtitle: join(row.dateLine, row.location) }; });
115
+ return { items, ...offsetPage(cursor, raw.length, PAGE, (res as { pagingMetadata?: { total?: number } }).pagingMetadata) };
116
+ },
117
+ demo: () => DEMO_EVENTS.map((d) => ({ id: d.id, title: d.title, subtitle: join(d.dateLine, d.location) })),
118
+ },
119
+ himi_owner_lite_posts: {
120
+ label: ["ownerSearch.source.posts", "Blog"],
121
+ icon: "newspaper",
122
+ detailScreenId: "himi_owner_lite_post",
123
+ async page(_query, cursor) {
124
+ // One walk over published posts; drafts ride the first page, best-effort, exactly as
125
+ // the Blog list reads them (a forbidden drafts resource must not fail the group).
126
+ const offset = Number(cursor || 0);
127
+ const [pub, draftPage] = await Promise.all([
128
+ queryPosts({ query: { paging: { limit: PAGE, offset } } }),
129
+ cursor ? Promise.resolve(null) : queryDraftPosts({ query: { paging: { limit: PAGE, offset: 0 } } }).catch(() => null),
130
+ ]);
131
+ const posts = (pub.posts ?? []).filter((p: { id?: string }) => !!p?.id) as Array<{ id: string; title?: string; excerpt?: string }>;
132
+ const seen = new Set(posts.map((p) => p.id));
133
+ // Drafts unavailable (missing or forbidden resource) → published only.
134
+ const drafts = (((draftPage as { draftPosts?: unknown[] } | null)?.draftPosts ?? []) as Array<{ id?: string; status?: string; title?: string; excerpt?: string }>)
135
+ .filter((p) => p?.id && p.status !== "PUBLISHED" && !seen.has(p.id!)) as Array<{ id: string; title?: string; excerpt?: string }>;
136
+ const draft = copyParam("ownerSearch.draft", "Draft");
137
+ const items = [
138
+ ...drafts.map((p) => ({ id: p.id, title: (p.title ?? "").trim() || "Untitled post", subtitle: join(draft, p.excerpt) })),
139
+ ...posts.map((p) => ({ id: p.id, title: (p.title ?? "").trim() || "Untitled post", subtitle: join(p.excerpt) })),
140
+ ];
141
+ return { items, ...offsetPage(cursor, posts.length, PAGE, (pub as { pagingMetadata?: { total?: number } }).pagingMetadata) };
142
+ },
143
+ demo: () => DEMO_POSTS.map((d) => ({ id: d.id, title: d.title, subtitle: join(/draft/i.test(d.postStatus) ? d.postStatus : "", d.excerpt) })),
144
+ },
145
+ };
146
+
147
+ const matches = (found: Found, needle: string): boolean =>
148
+ !!needle && `${found.title} ${found.subtitle}`.toLocaleLowerCase().includes(needle.toLocaleLowerCase());
149
+
150
+ export function verticalAdapter(id: string, normalizeQuery: (q: unknown) => string, classify: (e: unknown) => GroupProblem): SearchAdapter | null {
151
+ const source = Object.prototype.hasOwnProperty.call(VERTICAL_SOURCES, id) ? VERTICAL_SOURCES[id] : undefined;
152
+ if (!source) return null;
153
+ return {
154
+ id,
155
+ label: copyParam(source.label[0], source.label[1]),
156
+ icon: source.icon,
157
+ semantics: "loaded",
158
+ screenId: source.detailScreenId,
159
+ classify,
160
+ async search(query, cursor): Promise<SearchPage> {
161
+ const needle = normalizeQuery(query);
162
+ const toRow = (found: Found): SearchRow => ({
163
+ key: `${id}:${found.id}`, id: found.id, sourceId: id, title: found.title, subtitle: found.subtitle,
164
+ statusLabel: found.statusLabel ?? "", image: "", screenId: source.detailScreenId,
165
+ });
166
+ if (isOwnerPreviewDemo()) return { rows: source.demo().filter((f) => matches(f, needle)).map(toRow), nextCursor: "" };
167
+ const rows: SearchRow[] = [];
168
+ let next = cursor;
169
+ for (let page = 0; page < MAX_PAGES; page++) {
170
+ const result = await source.page(needle, next);
171
+ for (const found of result.items) if (found.id && matches(found, needle)) rows.push(toRow(found));
172
+ next = result.nextCursor;
173
+ if (!next) break;
174
+ }
175
+ return { rows, nextCursor: next };
176
+ },
177
+ };
178
+ }