@wix/himalaya-cli 0.1538.0 → 0.1540.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 (151) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/builder.d.ts +5 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/index.d.ts +7 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +7 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +4 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/state.mjs +23 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-events.mjs +1 -0
  7. package/dist/cli.mjs +2184 -1825
  8. package/dist/preview-assets.generated.bin +0 -0
  9. package/guides/component-reference.json +29 -42
  10. package/guides/component-reference.md +67 -14
  11. package/guides/dynamic-list.service.ts +7 -1
  12. package/guides/form-submit.service.ts +7 -1
  13. package/guides/himi-json-output.md +7 -0
  14. package/guides/index.json +2 -2
  15. package/guides/recipe-four-state.md +10 -1
  16. package/guides/recipe-search.md +9 -7
  17. package/guides/release-notes.md +17 -0
  18. package/guides/sdk-reference.json +966 -46
  19. package/guides/search-list.service.ts +7 -1
  20. package/guides/templates.md +19 -0
  21. package/package.json +2 -2
  22. package/stdlib/design-kits/dark-luxe.json +4 -0
  23. package/templates/beauty-salon/dev/index.ts +5021 -4164
  24. package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +9 -3
  25. package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +6 -3
  26. package/templates/beauty-salon/tier5-src/_shared/engine-intl.ts +5 -2
  27. package/templates/beauty-salon/tier5-src/_shared/himi-store-categories.ts +18 -12
  28. package/templates/beauty-salon/tier5-src/_shared/store-order-presentation.ts +56 -0
  29. package/templates/beauty-salon/tier5-src/_shared/wix-service-price.ts +4 -2
  30. package/templates/beauty-salon/tier5-src/himi-branded-lite-category/index.ts +196 -0
  31. package/templates/beauty-salon/tier5-src/himi-branded-lite-order/index.ts +29 -6
  32. package/templates/beauty-salon/tier5-src/himi-branded-lite-orders/index.ts +58 -5
  33. package/templates/beauty-salon/tier5-src/wix-spaces-collections/index.ts +37 -88
  34. package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +9 -3
  35. package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +6 -3
  36. package/templates/beauty-salon-owner/tier5-src/_shared/engine-intl.ts +5 -2
  37. package/templates/branded-lite/dev/index.ts +4933 -4079
  38. package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +9 -3
  39. package/templates/branded-lite/tier5-src/_shared/collection-service.ts +6 -3
  40. package/templates/branded-lite/tier5-src/_shared/engine-intl.ts +5 -2
  41. package/templates/branded-lite/tier5-src/_shared/himi-store-categories.ts +18 -12
  42. package/templates/branded-lite/tier5-src/_shared/store-order-presentation.ts +56 -0
  43. package/templates/branded-lite/tier5-src/_shared/wix-service-price.ts +4 -2
  44. package/templates/branded-lite/tier5-src/himi-branded-lite-category/index.ts +196 -0
  45. package/templates/branded-lite/tier5-src/himi-branded-lite-order/index.ts +29 -6
  46. package/templates/branded-lite/tier5-src/himi-branded-lite-orders/index.ts +58 -5
  47. package/templates/branded-lite/tier5-src/wix-spaces-collections/index.ts +37 -88
  48. package/templates/branded-lite/tokens.json +1 -1
  49. package/templates/brandy/tokens.json +1 -1
  50. package/templates/community-club/tier5-src/_shared/collection-contract.ts +9 -3
  51. package/templates/community-club/tier5-src/_shared/collection-service.ts +6 -3
  52. package/templates/community-club/tier5-src/_shared/engine-intl.ts +5 -2
  53. package/templates/community-club/tier5-src/_shared/himi-store-categories.ts +18 -12
  54. package/templates/community-club/tier5-src/_shared/wix-service-price.ts +4 -2
  55. package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +9 -3
  56. package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +6 -3
  57. package/templates/community-club-owner/tier5-src/_shared/engine-intl.ts +5 -2
  58. package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +9 -3
  59. package/templates/events-meetups/tier5-src/_shared/collection-service.ts +6 -3
  60. package/templates/events-meetups/tier5-src/_shared/engine-intl.ts +5 -2
  61. package/templates/events-meetups/tier5-src/_shared/himi-store-categories.ts +18 -12
  62. package/templates/events-meetups/tier5-src/_shared/wix-service-price.ts +4 -2
  63. package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +9 -3
  64. package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +6 -3
  65. package/templates/events-meetups-owner/tier5-src/_shared/engine-intl.ts +5 -2
  66. package/templates/gym-fitness/dev/index.ts +5018 -4161
  67. package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +9 -3
  68. package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +6 -3
  69. package/templates/gym-fitness/tier5-src/_shared/engine-intl.ts +5 -2
  70. package/templates/gym-fitness/tier5-src/_shared/himi-store-categories.ts +18 -12
  71. package/templates/gym-fitness/tier5-src/_shared/store-order-presentation.ts +56 -0
  72. package/templates/gym-fitness/tier5-src/_shared/wix-service-price.ts +4 -2
  73. package/templates/gym-fitness/tier5-src/himi-branded-lite-category/index.ts +196 -0
  74. package/templates/gym-fitness/tier5-src/himi-branded-lite-order/index.ts +29 -6
  75. package/templates/gym-fitness/tier5-src/himi-branded-lite-orders/index.ts +58 -5
  76. package/templates/gym-fitness/tier5-src/wix-spaces-collections/index.ts +37 -88
  77. package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +9 -3
  78. package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +6 -3
  79. package/templates/gym-fitness-owner/tier5-src/_shared/engine-intl.ts +5 -2
  80. package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +9 -3
  81. package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +6 -3
  82. package/templates/nonprofit-cause/tier5-src/_shared/engine-intl.ts +5 -2
  83. package/templates/nonprofit-cause/tier5-src/_shared/himi-store-categories.ts +18 -12
  84. package/templates/nonprofit-cause/tier5-src/_shared/wix-service-price.ts +4 -2
  85. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +9 -3
  86. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +6 -3
  87. package/templates/nonprofit-cause-owner/tier5-src/_shared/engine-intl.ts +5 -2
  88. package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +9 -3
  89. package/templates/owner-lite/tier5-src/_shared/collection-service.ts +6 -3
  90. package/templates/owner-lite/tier5-src/_shared/engine-intl.ts +5 -2
  91. package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +9 -3
  92. package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +6 -3
  93. package/templates/podcast-creator/tier5-src/_shared/engine-intl.ts +5 -2
  94. package/templates/podcast-creator/tier5-src/_shared/himi-store-categories.ts +18 -12
  95. package/templates/podcast-creator/tier5-src/_shared/wix-service-price.ts +4 -2
  96. package/templates/podcast-creator/tokens.json +4 -4
  97. package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +9 -3
  98. package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +6 -3
  99. package/templates/podcast-creator-owner/tier5-src/_shared/engine-intl.ts +5 -2
  100. package/templates/podcast-creator-owner/tokens.json +4 -4
  101. package/templates/portfolio-creative/dev/index.ts +5017 -4157
  102. package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +9 -3
  103. package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +6 -3
  104. package/templates/portfolio-creative/tier5-src/_shared/engine-intl.ts +5 -2
  105. package/templates/portfolio-creative/tier5-src/_shared/himi-store-categories.ts +18 -12
  106. package/templates/portfolio-creative/tier5-src/_shared/store-order-presentation.ts +56 -0
  107. package/templates/portfolio-creative/tier5-src/_shared/wix-service-price.ts +4 -2
  108. package/templates/portfolio-creative/tier5-src/himi-branded-lite-category/index.ts +196 -0
  109. package/templates/portfolio-creative/tier5-src/himi-branded-lite-order/index.ts +29 -6
  110. package/templates/portfolio-creative/tier5-src/himi-branded-lite-orders/index.ts +58 -5
  111. package/templates/portfolio-creative/tier5-src/wix-spaces-collections/index.ts +37 -88
  112. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +9 -3
  113. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +6 -3
  114. package/templates/portfolio-creative-owner/tier5-src/_shared/engine-intl.ts +5 -2
  115. package/templates/restaurant/tier5-src/_shared/collection-contract.ts +9 -3
  116. package/templates/restaurant/tier5-src/_shared/collection-service.ts +6 -3
  117. package/templates/restaurant/tier5-src/_shared/engine-intl.ts +5 -2
  118. package/templates/restaurant/tier5-src/_shared/himi-store-categories.ts +18 -12
  119. package/templates/restaurant/tier5-src/_shared/wix-service-price.ts +4 -2
  120. package/templates/restaurant/tokens.json +4 -4
  121. package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +9 -3
  122. package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +6 -3
  123. package/templates/restaurant-owner/tier5-src/_shared/engine-intl.ts +5 -2
  124. package/templates/retail-store/dev/index.ts +5159 -4302
  125. package/templates/retail-store/tier5-src/_shared/collection-contract.ts +9 -3
  126. package/templates/retail-store/tier5-src/_shared/collection-service.ts +6 -3
  127. package/templates/retail-store/tier5-src/_shared/engine-intl.ts +5 -2
  128. package/templates/retail-store/tier5-src/_shared/himi-store-categories.ts +18 -12
  129. package/templates/retail-store/tier5-src/_shared/store-order-presentation.ts +56 -0
  130. package/templates/retail-store/tier5-src/_shared/wix-service-price.ts +4 -2
  131. package/templates/retail-store/tier5-src/himi-branded-lite-category/index.ts +196 -0
  132. package/templates/retail-store/tier5-src/himi-branded-lite-order/index.ts +29 -6
  133. package/templates/retail-store/tier5-src/himi-branded-lite-orders/index.ts +58 -5
  134. package/templates/retail-store/tier5-src/wix-spaces-collections/index.ts +37 -88
  135. package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +9 -3
  136. package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +6 -3
  137. package/templates/retail-store-owner/tier5-src/_shared/engine-intl.ts +5 -2
  138. package/templates/yoga-studio/dev/index.ts +0 -3
  139. package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +9 -3
  140. package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +6 -3
  141. package/templates/yoga-studio/tier5-src/_shared/engine-intl.ts +5 -2
  142. package/templates/yoga-studio/tier5-src/_shared/himi-store-categories.ts +18 -12
  143. package/templates/yoga-studio/tier5-src/_shared/wix-service-price.ts +4 -2
  144. package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +9 -3
  145. package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +6 -3
  146. package/templates/yoga-studio-owner/tier5-src/_shared/engine-intl.ts +5 -2
  147. package/templates/beauty-salon/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
  148. package/templates/branded-lite/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
  149. package/templates/gym-fitness/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
  150. package/templates/portfolio-creative/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
  151. package/templates/retail-store/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
@@ -2,6 +2,8 @@ import { compareText, compareIso } from "./engine-intl.js";
2
2
  /** Shared OTA collection contract. No host APIs: used by workers and builders. */
3
3
  export interface CollectionConfig {
4
4
  rows: string;
5
+ /** Flat customer history rows, divided rather than boxed. */
6
+ rowStyle?: "flat";
5
7
  instanceInput?: string;
6
8
  title: string;
7
9
  subtitle?: string;
@@ -84,7 +86,7 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
84
86
  himiBrandedGroups: { rows: 'rows', title: 'name', subtitle: 'description', image: 'image', view: 'grid' },
85
87
  himiBrandedCms: { rows: 'rows', title: 'name', subtitle: 'subtitle', view: 'grid' },
86
88
  himiBrandedCmsCollection: { instanceInput: 'collection', rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list' },
87
- himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list', backendQuery: true,
89
+ himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', image: 'image', metadata: ['status', 'meta'], rowStyle: 'flat', view: 'list', backendQuery: true,
88
90
  searchPlaceholder: 'Search by order number', countNoun: ['order', 'orders'], filterValues: ['Not fulfilled', 'Partially fulfilled', 'Fulfilled'],
89
91
  sortLabels: { default: 'Newest', az: 'Oldest', za: 'Order # ↓' } },
90
92
  himiBrandedPlans: { rows: 'rows', title: 'name', subtitle: 'description', metadata: ['badge', 'priceLine', 'periodLine', 'trialBadge', 'perk1', 'perk2', 'perk3', 'joinNote'], view: 'grid', gridColumns: 1, metaMaxLines: 8, countNoun: ['plan', 'plans'] },
@@ -139,7 +141,10 @@ export function normalizeCollection(config: CollectionConfig, value: unknown): C
139
141
  density: s.density === 'compact' ? 'compact' : 'comfortable',
140
142
  sort: s.sort === 'az' || s.sort === 'za' ? s.sort : 'default',
141
143
  query: typeof s.query === 'string' ? s.query.slice(0, 256) : '',
142
- filter: typeof s.filter === 'string' ? s.filter.slice(0, 256) : config.defaultFilter ?? 'all',
144
+ // Flat collections no longer offer Saved. Migrate its persisted value before
145
+ // the first backend request so an invisible old filter cannot empty the list.
146
+ filter: config.rowStyle === 'flat' && s.filter === 'saved' ? 'all'
147
+ : typeof s.filter === 'string' ? s.filter.slice(0, 256) : config.defaultFilter ?? 'all',
143
148
  savedIds: Array.isArray(s.savedIds) ? [...new Set(s.savedIds.filter((id): id is string => typeof id === 'string' && id.length > 0 && id.length <= 256))].slice(0, 500) : [],
144
149
  savedViews: Array.isArray(s.savedViews) ? s.savedViews.filter((v): v is SavedCollectionView => !!v && typeof v === 'object' && typeof v.name === 'string' && !!v.name.trim()).slice(0, 8).map((v, i) => ({
145
150
  id: `view-${i}`, name: v.name.trim().slice(0, 40),
@@ -147,7 +152,8 @@ export function normalizeCollection(config: CollectionConfig, value: unknown): C
147
152
  density: v.density === 'compact' ? 'compact' : 'comfortable',
148
153
  sort: v.sort === 'az' || v.sort === 'za' ? v.sort : 'default',
149
154
  query: typeof v.query === 'string' ? v.query.slice(0, 256) : '',
150
- filter: typeof v.filter === 'string' ? v.filter.slice(0, 256) : 'all',
155
+ filter: config.rowStyle === 'flat' && v.filter === 'saved' ? 'all'
156
+ : typeof v.filter === 'string' ? v.filter.slice(0, 256) : 'all',
151
157
  })) : [],
152
158
  };
153
159
  }
@@ -95,7 +95,10 @@ export function decorateCollection(name: string, def: { state: State; actions?:
95
95
  if (sort === 'default' || sort === 'az' || sort === 'za') { prefs.sort = sort; changed = true; }
96
96
  }
97
97
  if (Object.prototype.hasOwnProperty.call(input, 'collectionFilter')) {
98
- const filter = String(input.collectionFilter ?? '');
98
+ const requestedFilter = String(input.collectionFilter ?? '');
99
+ // Old links to Saved must migrate with the stored preference when this
100
+ // collection no longer exposes that filter.
101
+ const filter = config.rowStyle === 'flat' && requestedFilter === 'saved' ? 'all' : requestedFilter;
99
102
  const configured = config.filterValues ?? [];
100
103
  const rows = Array.isArray(state[config.rows]) ? state[config.rows] as CollectionRow[] : [];
101
104
  const derived = config.filter ? rows.map(row => String(row[config.filter!] ?? '')).filter(Boolean) : [];
@@ -139,7 +142,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
139
142
  copyParam('collection.itemActionsFor', 'Actions for {title}'), copyParam('collection.itemActions', 'Item actions'));
140
143
  });
141
144
  if (previewId && !rows.some(row => collectionId(row) === previewId)) previewId = '';
142
- const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, { id: 'saved', label: copyParam('collection.saved', 'Saved') },
145
+ const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, ...(config.rowStyle === 'flat' ? [] : [{ id: 'saved', label: copyParam('collection.saved', 'Saved') }]),
143
146
  ...projected.filters.map(label => ({ id: `value:${label}`, label: copyParam(`collection.filter.${label}`, label) }))];
144
147
  const loading = s.status === 'loading' || s.isLoading === true;
145
148
  // Older domain reducers expose `errored` rather than `isErrored`; backend
@@ -220,7 +223,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
220
223
  ? [copyParam(`collection.${name}.countOne`, config.countNoun[0]), copyParam(`collection.${name}.countMany`, config.countNoun[1])] : undefined }),
221
224
  selectedSummary: `${prefs.selectedIds.length} ${copyParam('collection.selected', 'selected')}`,
222
225
  filterChoices,
223
- chips: config.filterValues ? filterChoices.filter(choice => ['all', 'saved', prefs.filter].includes(choice.id)) : filterChoices,
226
+ chips: config.filterValues && config.rowStyle !== 'flat' ? filterChoices.filter(choice => ['all', 'saved', prefs.filter].includes(choice.id)) : filterChoices,
224
227
  } };
225
228
  };
226
229
  const actions: Record<string, Reducer> = {};
@@ -410,8 +410,11 @@ export function compareDateDesc(a: string, b: string): number {
410
410
  // kind of number. So: one money pattern, one clock, one range.
411
411
 
412
412
  // Money uses `formatCurrency(amount, currency, "en")` — the currency's NATURAL decimals
413
- // ("₪70.00", "¥500"). Passing 0 fraction digits for whole amounts is what made a list read
414
- // "₪70 · ₪69.50".
413
+ // ("₪70.00", "¥500") — for Shop, Menu and every other price. The one exception is a Bookings
414
+ // SERVICE price (`_shared/wix-service-price.ts`): it drops the cents of a whole amount ("₪70",
415
+ // "₪69.50"), the format the redesigned service screen and services list were approved with. It
416
+ // must read the same on every screen that shows a service (the list, the service screen, the
417
+ // time screen, Home, the agenda); do not put those back on natural decimals one screen at a time.
415
418
 
416
419
  const KNOWN_SYMBOLS = new Set(Object.values(CURRENCY_SYMBOL));
417
420
 
@@ -4,7 +4,7 @@
4
4
  // Catalog V3 Categories first, V1 collections only when V3 THROWS (a legacy V1 site):
5
5
  // a V3 success with zero rows is an empty catalogue, and V1 on a V3 site answers 428.
6
6
  // Same rule as wix-spaces-collections; see the reasoning there.
7
- import { categoryImage, queryCollections, queryStoreCategories, type StoreCategoriesResponse, type StoreCategory, type WixCollection } from "@himalaya/wix-stores-collections";
7
+ import { categoryCount, categoryImage, queryCollections, queryStoreCategories, type StoreCategoriesResponse, type StoreCategory, type WixCollection } from "@himalaya/wix-stores-collections";
8
8
  import { wixMediaUrl } from "@himalaya/wix-media";
9
9
 
10
10
  export interface StoreCategoryCard {
@@ -13,6 +13,7 @@ export interface StoreCategoryCard {
13
13
  image: string;
14
14
  hasImage: boolean;
15
15
  noImage: boolean;
16
+ subtitle?: string;
16
17
  }
17
18
 
18
19
  /** "All Products" is a Wix system collection — every product is in it, so it is
@@ -20,12 +21,12 @@ export interface StoreCategoryCard {
20
21
  const SYSTEM_NAMES = ["all products"];
21
22
  const SYSTEM_COLLECTION_IDS = ["00000000-000000-000000-000000000001"];
22
23
 
23
- function card(id: string, name: string, image: string): StoreCategoryCard {
24
- return { id, name, image, hasImage: !!image, noImage: !image };
24
+ function card(id: string, name: string, image: string, subtitle = ""): StoreCategoryCard {
25
+ return { id, name, image, hasImage: !!image, noImage: !image, subtitle };
25
26
  }
26
27
 
27
28
  function fromCategory(c: StoreCategory): StoreCategoryCard {
28
- return card(c.id ?? "", (c.name ?? "").trim(), categoryImage(c));
29
+ return card(c.id ?? "", (c.name ?? "").trim(), categoryImage(c), categoryCount(c));
29
30
  }
30
31
 
31
32
  function fromCollection(c: WixCollection): StoreCategoryCard {
@@ -34,19 +35,24 @@ function fromCollection(c: WixCollection): StoreCategoryCard {
34
35
  return card(raw.id ?? "", (raw.name ?? "").trim(), wixMediaUrl(media?.image?.url ?? media?.thumbnail?.url ?? ""));
35
36
  }
36
37
 
37
- function visible(rows: StoreCategoryCard[]): StoreCategoryCard[] {
38
+ function visible(rows: StoreCategoryCard[], includeAllProducts = false): StoreCategoryCard[] {
38
39
  const seen = new Set<string>();
39
40
  return rows.filter((r) => {
40
- if (!r.id || !r.name || SYSTEM_COLLECTION_IDS.includes(r.id) || SYSTEM_NAMES.includes(r.name.toLowerCase()) || seen.has(r.id)) return false;
41
+ if (!r.id || !r.name || (!includeAllProducts && (SYSTEM_COLLECTION_IDS.includes(r.id) || SYSTEM_NAMES.includes(r.name.toLowerCase()))) || seen.has(r.id)) return false;
41
42
  seen.add(r.id);
42
43
  return true;
44
+ }).map(row => {
45
+ // Counts belong to the directory opt-in; Home rails and Shop chips retain their
46
+ // established five-field card contract.
47
+ const { subtitle, ...card } = row;
48
+ return includeAllProducts ? row : card;
43
49
  });
44
50
  }
45
51
 
46
52
  const PAGE_SIZE = 50;
47
53
  const MAX_PAGES = 100;
48
54
 
49
- async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
55
+ async function loadLegacyCategories(includeAllProducts: boolean): Promise<StoreCategoryCard[]> {
50
56
  const cards: StoreCategoryCard[] = [];
51
57
  const seen = new Set<string>();
52
58
  let offset = 0;
@@ -58,7 +64,7 @@ async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
58
64
  if (reportedTotal != null) total = Math.max(total ?? 0, reportedTotal);
59
65
  if (!rows.length) {
60
66
  if (total != null && seen.size < total) throw new Error("Category pagination is incomplete");
61
- return visible(cards);
67
+ return visible(cards, includeAllProducts);
62
68
  }
63
69
  const priorCount = seen.size;
64
70
  rows.forEach(row => { if (row.id) seen.add(row.id); });
@@ -67,7 +73,7 @@ async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
67
73
  offset += rows.length;
68
74
  if (total != null ? offset >= total : rows.length < PAGE_SIZE) {
69
75
  if (total != null && seen.size < total) throw new Error("Category pagination is incomplete");
70
- return visible(cards);
76
+ return visible(cards, includeAllProducts);
71
77
  }
72
78
  }
73
79
  throw new Error("Category pagination exceeded the page limit");
@@ -75,11 +81,11 @@ async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
75
81
 
76
82
  /** The store's browsable categories in site order. Rejects incomplete reads so callers
77
83
  * can retain their last good catalog instead of invalidating a later category. */
78
- export async function loadStoreCategories(): Promise<StoreCategoryCard[]> {
84
+ export async function loadStoreCategories({ includeAllProducts = false }: { includeAllProducts?: boolean } = {}): Promise<StoreCategoryCard[]> {
79
85
  let r: StoreCategoriesResponse;
80
86
  try {
81
87
  r = await queryStoreCategories(PAGE_SIZE);
82
- } catch { return loadLegacyCategories(); /* Only the initial failure can identify a legacy V1 site. */ }
88
+ } catch { return loadLegacyCategories(includeAllProducts); /* Only the initial failure can identify a legacy V1 site. */ }
83
89
  const cards: StoreCategoryCard[] = [];
84
90
  const cursors = new Set<string>();
85
91
  const seen = new Set<string>();
@@ -92,7 +98,7 @@ export async function loadStoreCategories(): Promise<StoreCategoryCard[]> {
92
98
  const next = r.pagingMetadata?.cursors?.next;
93
99
  if (!next) {
94
100
  if (seen.size < total) throw new Error("Category pagination is incomplete");
95
- return visible(cards);
101
+ return visible(cards, includeAllProducts);
96
102
  }
97
103
  if (cursors.has(next)) throw new Error("Category pagination repeated a cursor");
98
104
  cursors.add(next);
@@ -118,7 +118,7 @@ export function formatServiceMoney(money: unknown, fallbackCurrency = "USD"): st
118
118
  }
119
119
 
120
120
  /**
121
- * Format a Wix Bookings service's price for display, e.g. "₪111" / "$20".
121
+ * Format a Wix Bookings service's price for display, e.g. "₪111" / "$20" / "$19.50".
122
122
  *
123
123
  * Reads, in order of preference:
124
124
  * 1. Bookings v2 fixed price — `payment.fixed.price.{value,currency}` (the
@@ -137,7 +137,9 @@ export function formatServicePrice(service: unknown): string {
137
137
  if (amount == null) return "";
138
138
  const currency = serviceCurrency(service);
139
139
  try {
140
- return formatCurrency(amount, currency, "en");
140
+ // A whole amount drops its cents ("₪70", not "₪70.00"), as formatServiceMoney and the service
141
+ // screen show it, so the same service reads the same price on every screen.
142
+ return formatCurrency(amount, currency, "en", Number.isInteger(amount) ? 0 : undefined);
141
143
  } catch {
142
144
  return `${amount}`;
143
145
  }
@@ -2,6 +2,8 @@ import { compareText, compareIso } from "./engine-intl.js";
2
2
  /** Shared OTA collection contract. No host APIs: used by workers and builders. */
3
3
  export interface CollectionConfig {
4
4
  rows: string;
5
+ /** Flat customer history rows, divided rather than boxed. */
6
+ rowStyle?: "flat";
5
7
  instanceInput?: string;
6
8
  title: string;
7
9
  subtitle?: string;
@@ -84,7 +86,7 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
84
86
  himiBrandedGroups: { rows: 'rows', title: 'name', subtitle: 'description', image: 'image', view: 'grid' },
85
87
  himiBrandedCms: { rows: 'rows', title: 'name', subtitle: 'subtitle', view: 'grid' },
86
88
  himiBrandedCmsCollection: { instanceInput: 'collection', rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list' },
87
- himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list', backendQuery: true,
89
+ himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', image: 'image', metadata: ['status', 'meta'], rowStyle: 'flat', view: 'list', backendQuery: true,
88
90
  searchPlaceholder: 'Search by order number', countNoun: ['order', 'orders'], filterValues: ['Not fulfilled', 'Partially fulfilled', 'Fulfilled'],
89
91
  sortLabels: { default: 'Newest', az: 'Oldest', za: 'Order # ↓' } },
90
92
  himiBrandedPlans: { rows: 'rows', title: 'name', subtitle: 'description', metadata: ['badge', 'priceLine', 'periodLine', 'trialBadge', 'perk1', 'perk2', 'perk3', 'joinNote'], view: 'grid', gridColumns: 1, metaMaxLines: 8, countNoun: ['plan', 'plans'] },
@@ -139,7 +141,10 @@ export function normalizeCollection(config: CollectionConfig, value: unknown): C
139
141
  density: s.density === 'compact' ? 'compact' : 'comfortable',
140
142
  sort: s.sort === 'az' || s.sort === 'za' ? s.sort : 'default',
141
143
  query: typeof s.query === 'string' ? s.query.slice(0, 256) : '',
142
- filter: typeof s.filter === 'string' ? s.filter.slice(0, 256) : config.defaultFilter ?? 'all',
144
+ // Flat collections no longer offer Saved. Migrate its persisted value before
145
+ // the first backend request so an invisible old filter cannot empty the list.
146
+ filter: config.rowStyle === 'flat' && s.filter === 'saved' ? 'all'
147
+ : typeof s.filter === 'string' ? s.filter.slice(0, 256) : config.defaultFilter ?? 'all',
143
148
  savedIds: Array.isArray(s.savedIds) ? [...new Set(s.savedIds.filter((id): id is string => typeof id === 'string' && id.length > 0 && id.length <= 256))].slice(0, 500) : [],
144
149
  savedViews: Array.isArray(s.savedViews) ? s.savedViews.filter((v): v is SavedCollectionView => !!v && typeof v === 'object' && typeof v.name === 'string' && !!v.name.trim()).slice(0, 8).map((v, i) => ({
145
150
  id: `view-${i}`, name: v.name.trim().slice(0, 40),
@@ -147,7 +152,8 @@ export function normalizeCollection(config: CollectionConfig, value: unknown): C
147
152
  density: v.density === 'compact' ? 'compact' : 'comfortable',
148
153
  sort: v.sort === 'az' || v.sort === 'za' ? v.sort : 'default',
149
154
  query: typeof v.query === 'string' ? v.query.slice(0, 256) : '',
150
- filter: typeof v.filter === 'string' ? v.filter.slice(0, 256) : 'all',
155
+ filter: config.rowStyle === 'flat' && v.filter === 'saved' ? 'all'
156
+ : typeof v.filter === 'string' ? v.filter.slice(0, 256) : 'all',
151
157
  })) : [],
152
158
  };
153
159
  }
@@ -95,7 +95,10 @@ export function decorateCollection(name: string, def: { state: State; actions?:
95
95
  if (sort === 'default' || sort === 'az' || sort === 'za') { prefs.sort = sort; changed = true; }
96
96
  }
97
97
  if (Object.prototype.hasOwnProperty.call(input, 'collectionFilter')) {
98
- const filter = String(input.collectionFilter ?? '');
98
+ const requestedFilter = String(input.collectionFilter ?? '');
99
+ // Old links to Saved must migrate with the stored preference when this
100
+ // collection no longer exposes that filter.
101
+ const filter = config.rowStyle === 'flat' && requestedFilter === 'saved' ? 'all' : requestedFilter;
99
102
  const configured = config.filterValues ?? [];
100
103
  const rows = Array.isArray(state[config.rows]) ? state[config.rows] as CollectionRow[] : [];
101
104
  const derived = config.filter ? rows.map(row => String(row[config.filter!] ?? '')).filter(Boolean) : [];
@@ -139,7 +142,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
139
142
  copyParam('collection.itemActionsFor', 'Actions for {title}'), copyParam('collection.itemActions', 'Item actions'));
140
143
  });
141
144
  if (previewId && !rows.some(row => collectionId(row) === previewId)) previewId = '';
142
- const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, { id: 'saved', label: copyParam('collection.saved', 'Saved') },
145
+ const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, ...(config.rowStyle === 'flat' ? [] : [{ id: 'saved', label: copyParam('collection.saved', 'Saved') }]),
143
146
  ...projected.filters.map(label => ({ id: `value:${label}`, label: copyParam(`collection.filter.${label}`, label) }))];
144
147
  const loading = s.status === 'loading' || s.isLoading === true;
145
148
  // Older domain reducers expose `errored` rather than `isErrored`; backend
@@ -220,7 +223,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
220
223
  ? [copyParam(`collection.${name}.countOne`, config.countNoun[0]), copyParam(`collection.${name}.countMany`, config.countNoun[1])] : undefined }),
221
224
  selectedSummary: `${prefs.selectedIds.length} ${copyParam('collection.selected', 'selected')}`,
222
225
  filterChoices,
223
- chips: config.filterValues ? filterChoices.filter(choice => ['all', 'saved', prefs.filter].includes(choice.id)) : filterChoices,
226
+ chips: config.filterValues && config.rowStyle !== 'flat' ? filterChoices.filter(choice => ['all', 'saved', prefs.filter].includes(choice.id)) : filterChoices,
224
227
  } };
225
228
  };
226
229
  const actions: Record<string, Reducer> = {};
@@ -410,8 +410,11 @@ export function compareDateDesc(a: string, b: string): number {
410
410
  // kind of number. So: one money pattern, one clock, one range.
411
411
 
412
412
  // Money uses `formatCurrency(amount, currency, "en")` — the currency's NATURAL decimals
413
- // ("₪70.00", "¥500"). Passing 0 fraction digits for whole amounts is what made a list read
414
- // "₪70 · ₪69.50".
413
+ // ("₪70.00", "¥500") — for Shop, Menu and every other price. The one exception is a Bookings
414
+ // SERVICE price (`_shared/wix-service-price.ts`): it drops the cents of a whole amount ("₪70",
415
+ // "₪69.50"), the format the redesigned service screen and services list were approved with. It
416
+ // must read the same on every screen that shows a service (the list, the service screen, the
417
+ // time screen, Home, the agenda); do not put those back on natural decimals one screen at a time.
415
418
 
416
419
  const KNOWN_SYMBOLS = new Set(Object.values(CURRENCY_SYMBOL));
417
420