@wix/himalaya-cli 0.1033.0 → 0.1035.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 (127) hide show
  1. package/dist/cli.mjs +17 -15
  2. package/dist/preview-assets.generated.bin +0 -0
  3. package/package.json +2 -2
  4. package/templates/beauty-salon/dev/index.ts +398 -1
  5. package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +4 -1
  6. package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +36 -5
  7. package/templates/beauty-salon/tier5-src/_shared/empty-flags.ts +33 -0
  8. package/templates/beauty-salon/tier5-src/himi-store-saved/index.ts +3 -2
  9. package/templates/beauty-salon/tier5-src/wix-spaces-collections/index.ts +3 -2
  10. package/templates/beauty-salon/tier5-src/wix-spaces-my-subscriptions/index.ts +11 -2
  11. package/templates/beauty-salon/tier5-src/wix-spaces-whats-new/index.ts +3 -2
  12. package/templates/beauty-salon-owner/dev/index.ts +1216 -0
  13. package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +4 -1
  14. package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +36 -5
  15. package/templates/branded-lite/dev/index.ts +2239 -343
  16. package/templates/branded-lite/tier5-src/_shared/branded-load-state.ts +106 -0
  17. package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +4 -1
  18. package/templates/branded-lite/tier5-src/_shared/collection-service.ts +36 -5
  19. package/templates/branded-lite/tier5-src/_shared/empty-flags.ts +33 -0
  20. package/templates/branded-lite/tier5-src/himi-branded-lite-badges/index.ts +20 -16
  21. package/templates/branded-lite/tier5-src/himi-branded-lite-coupons/index.ts +20 -20
  22. package/templates/branded-lite/tier5-src/himi-branded-lite-donations/index.ts +20 -16
  23. package/templates/branded-lite/tier5-src/himi-branded-lite-forms/index.ts +23 -31
  24. package/templates/branded-lite/tier5-src/himi-branded-lite-galleries/index.ts +19 -14
  25. package/templates/branded-lite/tier5-src/himi-branded-lite-giftcards/index.ts +20 -16
  26. package/templates/branded-lite/tier5-src/himi-branded-lite-inbox/index.ts +20 -15
  27. package/templates/branded-lite/tier5-src/himi-branded-lite-invoices/index.ts +20 -16
  28. package/templates/branded-lite/tier5-src/himi-branded-lite-loyalty/index.ts +20 -17
  29. package/templates/branded-lite/tier5-src/himi-branded-lite-notifications/index.ts +20 -18
  30. package/templates/branded-lite/tier5-src/himi-branded-lite-notifsettings/index.ts +23 -19
  31. package/templates/branded-lite/tier5-src/himi-branded-lite-podcasts/index.ts +1 -1
  32. package/templates/branded-lite/tier5-src/himi-branded-lite-reservations/index.ts +24 -20
  33. package/templates/branded-lite/tier5-src/himi-branded-lite-reviews/index.ts +20 -17
  34. package/templates/branded-lite/tier5-src/himi-branded-lite-videos/index.ts +19 -15
  35. package/templates/branded-lite/tier5-src/himi-store-saved/index.ts +3 -2
  36. package/templates/branded-lite/tier5-src/wix-spaces-collections/index.ts +3 -2
  37. package/templates/branded-lite/tier5-src/wix-spaces-forum/index.ts +3 -2
  38. package/templates/branded-lite/tier5-src/wix-spaces-members/index.ts +19 -6
  39. package/templates/branded-lite/tier5-src/wix-spaces-my-subscriptions/index.ts +11 -2
  40. package/templates/branded-lite/tier5-src/wix-spaces-new-message/index.ts +3 -2
  41. package/templates/branded-lite/tier5-src/wix-spaces-whats-new/index.ts +3 -2
  42. package/templates/community-club/dev/index.ts +283 -2
  43. package/templates/community-club/tier5-src/_shared/collection-contract.ts +4 -1
  44. package/templates/community-club/tier5-src/_shared/collection-service.ts +36 -5
  45. package/templates/community-club/tier5-src/_shared/empty-flags.ts +33 -0
  46. package/templates/community-club/tier5-src/wix-spaces-whats-new/index.ts +3 -2
  47. package/templates/community-club-owner/dev/index.ts +1216 -0
  48. package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +4 -1
  49. package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +36 -5
  50. package/templates/events-meetups/dev/index.ts +197 -1
  51. package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +4 -1
  52. package/templates/events-meetups/tier5-src/_shared/collection-service.ts +36 -5
  53. package/templates/events-meetups/tier5-src/_shared/empty-flags.ts +33 -0
  54. package/templates/events-meetups/tier5-src/wix-spaces-whats-new/index.ts +3 -2
  55. package/templates/events-meetups-owner/dev/index.ts +1088 -0
  56. package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +4 -1
  57. package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +36 -5
  58. package/templates/gym-fitness/dev/index.ts +462 -1
  59. package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +4 -1
  60. package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +36 -5
  61. package/templates/gym-fitness/tier5-src/_shared/empty-flags.ts +33 -0
  62. package/templates/gym-fitness/tier5-src/himi-store-saved/index.ts +3 -2
  63. package/templates/gym-fitness/tier5-src/wix-spaces-collections/index.ts +3 -2
  64. package/templates/gym-fitness/tier5-src/wix-spaces-my-subscriptions/index.ts +11 -2
  65. package/templates/gym-fitness/tier5-src/wix-spaces-whats-new/index.ts +3 -2
  66. package/templates/gym-fitness-owner/dev/index.ts +1280 -0
  67. package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +4 -1
  68. package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +36 -5
  69. package/templates/nonprofit-cause/dev/index.ts +264 -1
  70. package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +4 -1
  71. package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +36 -5
  72. package/templates/nonprofit-cause/tier5-src/_shared/empty-flags.ts +33 -0
  73. package/templates/nonprofit-cause/tier5-src/wix-spaces-my-subscriptions/index.ts +11 -2
  74. package/templates/nonprofit-cause/tier5-src/wix-spaces-whats-new/index.ts +3 -2
  75. package/templates/nonprofit-cause-owner/dev/index.ts +1152 -0
  76. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +4 -1
  77. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +36 -5
  78. package/templates/owner-lite/dev/index.ts +1280 -0
  79. package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +4 -1
  80. package/templates/owner-lite/tier5-src/_shared/collection-service.ts +36 -5
  81. package/templates/podcast-creator/dev/index.ts +286 -23
  82. package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +4 -1
  83. package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +36 -5
  84. package/templates/podcast-creator/tier5-src/_shared/empty-flags.ts +33 -0
  85. package/templates/podcast-creator/tier5-src/himi-branded-lite-podcasts/index.ts +1 -1
  86. package/templates/podcast-creator/tier5-src/wix-spaces-my-subscriptions/index.ts +11 -2
  87. package/templates/podcast-creator/tier5-src/wix-spaces-whats-new/index.ts +3 -2
  88. package/templates/podcast-creator-owner/dev/index.ts +1216 -0
  89. package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +4 -1
  90. package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +36 -5
  91. package/templates/portfolio-creative/dev/index.ts +353 -23
  92. package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +4 -1
  93. package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +36 -5
  94. package/templates/portfolio-creative/tier5-src/_shared/empty-flags.ts +33 -0
  95. package/templates/portfolio-creative/tier5-src/himi-store-saved/index.ts +3 -2
  96. package/templates/portfolio-creative/tier5-src/wix-spaces-collections/index.ts +3 -2
  97. package/templates/portfolio-creative/tier5-src/wix-spaces-whats-new/index.ts +3 -2
  98. package/templates/portfolio-creative-owner/dev/index.ts +1216 -0
  99. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +4 -1
  100. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +36 -5
  101. package/templates/restaurant/dev/index.ts +197 -1
  102. package/templates/restaurant/tier5-src/_shared/collection-contract.ts +4 -1
  103. package/templates/restaurant/tier5-src/_shared/collection-service.ts +36 -5
  104. package/templates/restaurant/tier5-src/_shared/empty-flags.ts +33 -0
  105. package/templates/restaurant/tier5-src/wix-spaces-whats-new/index.ts +3 -2
  106. package/templates/restaurant-owner/dev/index.ts +1280 -0
  107. package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +4 -1
  108. package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +36 -5
  109. package/templates/retail-store/dev/index.ts +203 -1
  110. package/templates/retail-store/tier5-src/_shared/collection-contract.ts +4 -1
  111. package/templates/retail-store/tier5-src/_shared/collection-service.ts +36 -5
  112. package/templates/retail-store/tier5-src/_shared/empty-flags.ts +33 -0
  113. package/templates/retail-store/tier5-src/himi-store-saved/index.ts +3 -2
  114. package/templates/retail-store/tier5-src/wix-spaces-collections/index.ts +3 -2
  115. package/templates/retail-store/tier5-src/wix-spaces-whats-new/index.ts +3 -2
  116. package/templates/retail-store-owner/dev/index.ts +1152 -0
  117. package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +4 -1
  118. package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +36 -5
  119. package/templates/yoga-studio/dev/index.ts +392 -1
  120. package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +4 -1
  121. package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +36 -5
  122. package/templates/yoga-studio/tier5-src/_shared/empty-flags.ts +33 -0
  123. package/templates/yoga-studio/tier5-src/wix-spaces-my-subscriptions/index.ts +11 -2
  124. package/templates/yoga-studio/tier5-src/wix-spaces-whats-new/index.ts +3 -2
  125. package/templates/yoga-studio-owner/dev/index.ts +1152 -0
  126. package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +4 -1
  127. package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +36 -5
@@ -30,6 +30,9 @@ export interface CollectionConfig {
30
30
  * `screenId` opens that screen with the row id; without it the verb reuses the
31
31
  * row's own tap action (the detail screen that owns the verb). */
32
32
  primaryVerb?: { label: string; screenId?: string };
33
+ /** A state field the DOMAIN narrows `rows` by (e.g. a category picker). While it is set,
34
+ * zero rows is a filtered result, not an empty source. */
35
+ narrowedBy?: string;
33
36
  }
34
37
  /** At or below this many records, with no query/filter narrowing them and no further
35
38
  * page, the list/grid toggle, Refine, chips, search and count are noise. */
@@ -41,7 +44,7 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
41
44
  defaultFilter: 'value:Upcoming', searchPlaceholder: 'Exact booking ID', filterValues: ['Upcoming', 'Past', 'Cancelled'],
42
45
  sortLabels: { default: 'Soonest', az: 'Oldest', za: 'Latest' } },
43
46
  himiEvents: { rows: 'results', title: 'title', subtitle: 'dateLine', image: 'image', view: 'grid', backendQuery: true, searchPlaceholder: 'Exact event title', filterValues: ['Upcoming', 'Past'], sortLabels: { default: 'Date', az: 'Title A–Z', za: 'Title Z–A' }, primaryVerb: { label: 'Register' } },
44
- himiBlogFeed: { rows: 'posts', title: 'title', subtitle: 'excerpt', image: 'image', filter: 'category', filterValuesFrom: 'categories', view: 'grid' },
47
+ himiBlogFeed: { rows: 'posts', title: 'title', subtitle: 'excerpt', image: 'image', filter: 'category', filterValuesFrom: 'categories', view: 'grid', narrowedBy: 'selectedCategoryId' },
45
48
  himiOwnerLiteProducts: { rows: 'products', title: 'name', subtitle: 'price', image: 'image', view: 'grid', backendQuery: true },
46
49
  himiOwnerModules: { instanceInput: 'module', rows: 'rows', title: 'title', subtitle: 'subtitle', image: 'image', filter: 'statusLabel', view: 'list', paged: true, reviewAction: 'prepareRecord', reviewSelectedAction: 'prepareSelected' },
47
50
  himiOwnerLiteServices: { rows: 'services', title: 'name', subtitle: 'subtitle', view: 'list', backendQuery: true, searchPlaceholder: 'Service name starts with…' },
@@ -122,14 +122,38 @@ export function decorateCollection(name: string, def: { state: State; actions?:
122
122
  const errored = s.status === 'error' || s.isErrored === true || s.errored === true || backendState?.status === 'error';
123
123
  const domainError = typeof s.error === 'string' ? s.error.trim() : '';
124
124
  const queryError = typeof backendState?.error === 'string' ? backendState.error.trim() : '';
125
- const errorMessage = queryError || domainError || copyParam('collection.loadError', 'This collection could not load. Try again.');
125
+ // Some domains carry their failure sentence in `emptyText` (portfolio, podcasts);
126
+ // the screen folds that node into this block, so keep the domain's own words.
127
+ const domainEmptyError = errored && typeof s.emptyText === 'string' ? s.emptyText.trim() : '';
128
+ const errorMessage = queryError || domainError || domainEmptyError || copyParam('collection.loadError', 'This collection could not load. Try again.');
126
129
  const canRetry = !!backend || typeof def.actions?.refresh === 'function' || typeof def.actions?.load === 'function' || typeof def.actions?.collectionRetry === 'function';
130
+ // Wix refused the visitor: retrying cannot help, signing in can.
131
+ const signInRequired = errored && s.signInRequired === true;
132
+ // "the source has nothing" and "your filter matched nothing" are
133
+ // different states. Only the second may offer "Clear filters", and the list chrome
134
+ // (view, refine, chips, counter, search) means nothing over a source with no rows.
135
+ // Loaded-row collections hold the whole source in `rows`, so zero rows IS an empty
136
+ // source. A backend query returns only matches, so zero rows is source-empty only
137
+ // when nothing narrowed it.
138
+ // Anything the customer (or the domain) narrowed the result by. While narrowed, zero rows
139
+ // is "your filter matched nothing" and the controls that got them here must stay.
140
+ // The DOMAIN's own filter (config.narrowedBy, e.g. blog category) narrowed the rows: the
141
+ // domain owns that empty state and its clear action. The shared "Clear filters" block
142
+ // cannot reset a domain filter, so it never shows for one.
143
+ const domainNarrowed = !!config.narrowedBy && !!s[config.narrowedBy];
127
144
  // A default view is what the screen opens on; anything else is the member's own
128
145
  // narrowing, which keeps the chrome (and the Clear that undoes it) on screen.
129
146
  // "All" and the collection's own default (e.g. Upcoming) are both un-narrowed: Clear
130
147
  // resets to "All", so showing it on either would offer a reset that changes nothing.
131
- const narrowed = !!prefs.query.trim() || (prefs.filter !== 'all' && prefs.filter !== initialCollection(config).filter);
132
- const hideChrome = !narrowed && !s.nextCursor && projected.loaded < COLLECTION_CHROME_MIN_ITEMS;
148
+ const narrowed = !!prefs.query.trim() || (prefs.filter !== 'all' && prefs.filter !== initialCollection(config).filter) || domainNarrowed;
149
+ const sourceEmpty = !loading && !errored && rows.length === 0 &&
150
+ (!(backend && config.backendQuery) || !narrowed) && !domainNarrowed;
151
+ const emptySourceText = (typeof s.emptyText === 'string' && s.emptyText.trim()) || copyParam('collection.emptySource', 'Nothing here yet');
152
+ // Chrome is noise over a small un-narrowed list (fewer than COLLECTION_CHROME_MIN_ITEMS
153
+ // records, no further page), over an empty source, and before the FIRST load or an
154
+ // un-narrowed failure. A re-query while typing keeps it (and the search field's focus).
155
+ const hideChrome = (!narrowed && !s.nextCursor && projected.loaded < COLLECTION_CHROME_MIN_ITEMS)
156
+ || sourceEmpty || ((loading || errored) && rows.length === 0 && !narrowed);
133
157
  return { ...s, collection: { ...prefs, ...projected,
134
158
  hideChrome, noActiveCriteria: !narrowed,
135
159
  viewName, viewMessage, saveMessage, noSavedViews: prefs.savedViews.length === 0,
@@ -138,8 +162,15 @@ export function decorateCollection(name: string, def: { state: State; actions?:
138
162
  noPageError: s.pageError !== true,
139
163
  noOffline: s.fromCache !== true,
140
164
  noError: !errored,
141
- noRetry: !canRetry,
165
+ noRetry: !canRetry || signInRequired,
166
+ noSignIn: !signInRequired,
167
+ errorTitle: signInRequired ? copyParam('collection.signInTitle', 'Sign in to see this') : copyParam('collection.loadErrorTitle', 'This collection could not load'),
142
168
  errorMessage,
169
+ noLoading: !loading,
170
+ // De-dup against a domain that paints its OWN empty block (screen-kit emptyBlock is
171
+ // gated on the domain's top-level `noEmpty`): only one empty state ever shows.
172
+ noEmptySource: !sourceEmpty || s.noEmpty === false,
173
+ emptySourceText,
143
174
  hideList: prefs.view !== 'list', hideGrid: prefs.view !== 'grid',
144
175
  hideComfortable: prefs.density !== 'comfortable', hideCompact: prefs.density !== 'compact',
145
176
  selectingLabel: prefs.selecting ? copyParam('collection.done', 'Done') : copyParam('collection.select', 'Select'),
@@ -147,7 +178,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
147
178
  cannotSaveSelected: !prefs.selectedIds.length || prefs.selectedIds.some(id => !rows.some(row => collectionId(row) === id)),
148
179
  filtersPresented, previewPresented: !!previewId, preview: projected.items.filter(row => row.collectionId === previewId),
149
180
  persistenceLabel: persisted.available ? copyParam('collection.onDevice', 'Saved on this device') : copyParam('collection.sessionOnly', 'Choices last for this session'),
150
- noEmpty: loading || errored || projected.items.length > 0,
181
+ noEmpty: loading || errored || sourceEmpty || domainNarrowed || projected.items.length > 0,
151
182
  summary: backend && config.backendQuery
152
183
  ? (s.fromCache ? copyParam('collection.cached', 'Saved offline · may be outdated')
153
184
  : typeof s.total === 'number' ? `${projected.items.length} / ${s.total} ${copyParam('collection.results', 'results')}`
@@ -4637,7 +4637,8 @@ const SCREENS = runtimeSetting({
4637
4637
  "props": {
4638
4638
  "text": "$service:spacesDeleteAccount.message",
4639
4639
  "style": "caption",
4640
- "color": "textTertiary"
4640
+ "color": "textTertiary",
4641
+ "collapseWhenEmpty": true
4641
4642
  }
4642
4643
  }
4643
4644
  },
@@ -4932,6 +4933,9 @@ const SCREENS = runtimeSetting({
4932
4933
  }
4933
4934
  ]
4934
4935
  }
4936
+ },
4937
+ "modifiers": {
4938
+ "hiddenPath": "$service:spacesWhatsNew.noItems"
4935
4939
  }
4936
4940
  },
4937
4941
  {
@@ -5157,6 +5161,24 @@ const SCREENS = runtimeSetting({
5157
5161
  "props": {
5158
5162
  "spacing": 0,
5159
5163
  "children": [
5164
+ {
5165
+ "id": "himi_menu.rows.collection.loading",
5166
+ "body": {
5167
+ "package": "himalaya",
5168
+ "type": "Skeleton",
5169
+ "props": {
5170
+ "height": 72,
5171
+ "cornerRadius": 12,
5172
+ "count": 3
5173
+ }
5174
+ },
5175
+ "modifiers": {
5176
+ "hiddenPath": "$service:himiBrandedMenus.collection.noLoading",
5177
+ "padding": {
5178
+ "bottom": 12
5179
+ }
5180
+ }
5181
+ },
5160
5182
  {
5161
5183
  "id": "",
5162
5184
  "body": {
@@ -5375,6 +5397,52 @@ const SCREENS = runtimeSetting({
5375
5397
  }
5376
5398
  }
5377
5399
  },
5400
+ {
5401
+ "id": "himi_menu.rows.collection.empty-source",
5402
+ "body": {
5403
+ "package": "himalaya",
5404
+ "type": "VStack",
5405
+ "props": {
5406
+ "spacing": 8,
5407
+ "alignment": "center",
5408
+ "children": [
5409
+ {
5410
+ "id": "",
5411
+ "body": {
5412
+ "package": "himalaya",
5413
+ "type": "Icon",
5414
+ "props": {
5415
+ "name": "inbox",
5416
+ "size": 28,
5417
+ "tint": "textTertiary"
5418
+ }
5419
+ },
5420
+ "modifiers": {
5421
+ "accessibilityRole": "decorative"
5422
+ }
5423
+ },
5424
+ {
5425
+ "id": "",
5426
+ "body": {
5427
+ "package": "himalaya",
5428
+ "type": "Text",
5429
+ "props": {
5430
+ "text": "$service:himiBrandedMenus.collection.emptySourceText",
5431
+ "style": "body",
5432
+ "color": "textSecondary"
5433
+ }
5434
+ }
5435
+ }
5436
+ ]
5437
+ }
5438
+ },
5439
+ "modifiers": {
5440
+ "hiddenPath": "$service:himiBrandedMenus.collection.noEmptySource",
5441
+ "padding": {
5442
+ "bottom": 12
5443
+ }
5444
+ }
5445
+ },
5378
5446
  {
5379
5447
  "id": "",
5380
5448
  "body": {
@@ -8336,6 +8404,24 @@ const SCREENS = runtimeSetting({
8336
8404
  "props": {
8337
8405
  "spacing": 0,
8338
8406
  "children": [
8407
+ {
8408
+ "id": "himi_events.list.collection.loading",
8409
+ "body": {
8410
+ "package": "himalaya",
8411
+ "type": "Skeleton",
8412
+ "props": {
8413
+ "height": 72,
8414
+ "cornerRadius": 12,
8415
+ "count": 3
8416
+ }
8417
+ },
8418
+ "modifiers": {
8419
+ "hiddenPath": "$service:himiEvents.collection.noLoading",
8420
+ "padding": {
8421
+ "bottom": 12
8422
+ }
8423
+ }
8424
+ },
8339
8425
  {
8340
8426
  "id": "",
8341
8427
  "body": {
@@ -8597,6 +8683,52 @@ const SCREENS = runtimeSetting({
8597
8683
  }
8598
8684
  }
8599
8685
  },
8686
+ {
8687
+ "id": "himi_events.list.collection.empty-source",
8688
+ "body": {
8689
+ "package": "himalaya",
8690
+ "type": "VStack",
8691
+ "props": {
8692
+ "spacing": 8,
8693
+ "alignment": "center",
8694
+ "children": [
8695
+ {
8696
+ "id": "",
8697
+ "body": {
8698
+ "package": "himalaya",
8699
+ "type": "Icon",
8700
+ "props": {
8701
+ "name": "inbox",
8702
+ "size": 28,
8703
+ "tint": "textTertiary"
8704
+ }
8705
+ },
8706
+ "modifiers": {
8707
+ "accessibilityRole": "decorative"
8708
+ }
8709
+ },
8710
+ {
8711
+ "id": "",
8712
+ "body": {
8713
+ "package": "himalaya",
8714
+ "type": "Text",
8715
+ "props": {
8716
+ "text": "$service:himiEvents.collection.emptySourceText",
8717
+ "style": "body",
8718
+ "color": "textSecondary"
8719
+ }
8720
+ }
8721
+ }
8722
+ ]
8723
+ }
8724
+ },
8725
+ "modifiers": {
8726
+ "hiddenPath": "$service:himiEvents.collection.noEmptySource",
8727
+ "padding": {
8728
+ "bottom": 12
8729
+ }
8730
+ }
8731
+ },
8600
8732
  {
8601
8733
  "id": "",
8602
8734
  "body": {
@@ -11640,6 +11772,24 @@ const SCREENS = runtimeSetting({
11640
11772
  "props": {
11641
11773
  "spacing": 0,
11642
11774
  "children": [
11775
+ {
11776
+ "id": "himi_blog.list.collection.loading",
11777
+ "body": {
11778
+ "package": "himalaya",
11779
+ "type": "Skeleton",
11780
+ "props": {
11781
+ "height": 72,
11782
+ "cornerRadius": 12,
11783
+ "count": 3
11784
+ }
11785
+ },
11786
+ "modifiers": {
11787
+ "hiddenPath": "$service:himiBlogFeed.collection.noLoading",
11788
+ "padding": {
11789
+ "bottom": 12
11790
+ }
11791
+ }
11792
+ },
11643
11793
  {
11644
11794
  "id": "",
11645
11795
  "body": {
@@ -11858,6 +12008,52 @@ const SCREENS = runtimeSetting({
11858
12008
  }
11859
12009
  }
11860
12010
  },
12011
+ {
12012
+ "id": "himi_blog.list.collection.empty-source",
12013
+ "body": {
12014
+ "package": "himalaya",
12015
+ "type": "VStack",
12016
+ "props": {
12017
+ "spacing": 8,
12018
+ "alignment": "center",
12019
+ "children": [
12020
+ {
12021
+ "id": "",
12022
+ "body": {
12023
+ "package": "himalaya",
12024
+ "type": "Icon",
12025
+ "props": {
12026
+ "name": "inbox",
12027
+ "size": 28,
12028
+ "tint": "textTertiary"
12029
+ }
12030
+ },
12031
+ "modifiers": {
12032
+ "accessibilityRole": "decorative"
12033
+ }
12034
+ },
12035
+ {
12036
+ "id": "",
12037
+ "body": {
12038
+ "package": "himalaya",
12039
+ "type": "Text",
12040
+ "props": {
12041
+ "text": "$service:himiBlogFeed.collection.emptySourceText",
12042
+ "style": "body",
12043
+ "color": "textSecondary"
12044
+ }
12045
+ }
12046
+ }
12047
+ ]
12048
+ }
12049
+ },
12050
+ "modifiers": {
12051
+ "hiddenPath": "$service:himiBlogFeed.collection.noEmptySource",
12052
+ "padding": {
12053
+ "bottom": 12
12054
+ }
12055
+ }
12056
+ },
11861
12057
  {
11862
12058
  "id": "",
11863
12059
  "body": {
@@ -30,6 +30,9 @@ export interface CollectionConfig {
30
30
  * `screenId` opens that screen with the row id; without it the verb reuses the
31
31
  * row's own tap action (the detail screen that owns the verb). */
32
32
  primaryVerb?: { label: string; screenId?: string };
33
+ /** A state field the DOMAIN narrows `rows` by (e.g. a category picker). While it is set,
34
+ * zero rows is a filtered result, not an empty source. */
35
+ narrowedBy?: string;
33
36
  }
34
37
  /** At or below this many records, with no query/filter narrowing them and no further
35
38
  * page, the list/grid toggle, Refine, chips, search and count are noise. */
@@ -41,7 +44,7 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
41
44
  defaultFilter: 'value:Upcoming', searchPlaceholder: 'Exact booking ID', filterValues: ['Upcoming', 'Past', 'Cancelled'],
42
45
  sortLabels: { default: 'Soonest', az: 'Oldest', za: 'Latest' } },
43
46
  himiEvents: { rows: 'results', title: 'title', subtitle: 'dateLine', image: 'image', view: 'grid', backendQuery: true, searchPlaceholder: 'Exact event title', filterValues: ['Upcoming', 'Past'], sortLabels: { default: 'Date', az: 'Title A–Z', za: 'Title Z–A' }, primaryVerb: { label: 'Register' } },
44
- himiBlogFeed: { rows: 'posts', title: 'title', subtitle: 'excerpt', image: 'image', filter: 'category', filterValuesFrom: 'categories', view: 'grid' },
47
+ himiBlogFeed: { rows: 'posts', title: 'title', subtitle: 'excerpt', image: 'image', filter: 'category', filterValuesFrom: 'categories', view: 'grid', narrowedBy: 'selectedCategoryId' },
45
48
  himiOwnerLiteProducts: { rows: 'products', title: 'name', subtitle: 'price', image: 'image', view: 'grid', backendQuery: true },
46
49
  himiOwnerModules: { instanceInput: 'module', rows: 'rows', title: 'title', subtitle: 'subtitle', image: 'image', filter: 'statusLabel', view: 'list', paged: true, reviewAction: 'prepareRecord', reviewSelectedAction: 'prepareSelected' },
47
50
  himiOwnerLiteServices: { rows: 'services', title: 'name', subtitle: 'subtitle', view: 'list', backendQuery: true, searchPlaceholder: 'Service name starts with…' },
@@ -122,14 +122,38 @@ export function decorateCollection(name: string, def: { state: State; actions?:
122
122
  const errored = s.status === 'error' || s.isErrored === true || s.errored === true || backendState?.status === 'error';
123
123
  const domainError = typeof s.error === 'string' ? s.error.trim() : '';
124
124
  const queryError = typeof backendState?.error === 'string' ? backendState.error.trim() : '';
125
- const errorMessage = queryError || domainError || copyParam('collection.loadError', 'This collection could not load. Try again.');
125
+ // Some domains carry their failure sentence in `emptyText` (portfolio, podcasts);
126
+ // the screen folds that node into this block, so keep the domain's own words.
127
+ const domainEmptyError = errored && typeof s.emptyText === 'string' ? s.emptyText.trim() : '';
128
+ const errorMessage = queryError || domainError || domainEmptyError || copyParam('collection.loadError', 'This collection could not load. Try again.');
126
129
  const canRetry = !!backend || typeof def.actions?.refresh === 'function' || typeof def.actions?.load === 'function' || typeof def.actions?.collectionRetry === 'function';
130
+ // Wix refused the visitor: retrying cannot help, signing in can.
131
+ const signInRequired = errored && s.signInRequired === true;
132
+ // "the source has nothing" and "your filter matched nothing" are
133
+ // different states. Only the second may offer "Clear filters", and the list chrome
134
+ // (view, refine, chips, counter, search) means nothing over a source with no rows.
135
+ // Loaded-row collections hold the whole source in `rows`, so zero rows IS an empty
136
+ // source. A backend query returns only matches, so zero rows is source-empty only
137
+ // when nothing narrowed it.
138
+ // Anything the customer (or the domain) narrowed the result by. While narrowed, zero rows
139
+ // is "your filter matched nothing" and the controls that got them here must stay.
140
+ // The DOMAIN's own filter (config.narrowedBy, e.g. blog category) narrowed the rows: the
141
+ // domain owns that empty state and its clear action. The shared "Clear filters" block
142
+ // cannot reset a domain filter, so it never shows for one.
143
+ const domainNarrowed = !!config.narrowedBy && !!s[config.narrowedBy];
127
144
  // A default view is what the screen opens on; anything else is the member's own
128
145
  // narrowing, which keeps the chrome (and the Clear that undoes it) on screen.
129
146
  // "All" and the collection's own default (e.g. Upcoming) are both un-narrowed: Clear
130
147
  // resets to "All", so showing it on either would offer a reset that changes nothing.
131
- const narrowed = !!prefs.query.trim() || (prefs.filter !== 'all' && prefs.filter !== initialCollection(config).filter);
132
- const hideChrome = !narrowed && !s.nextCursor && projected.loaded < COLLECTION_CHROME_MIN_ITEMS;
148
+ const narrowed = !!prefs.query.trim() || (prefs.filter !== 'all' && prefs.filter !== initialCollection(config).filter) || domainNarrowed;
149
+ const sourceEmpty = !loading && !errored && rows.length === 0 &&
150
+ (!(backend && config.backendQuery) || !narrowed) && !domainNarrowed;
151
+ const emptySourceText = (typeof s.emptyText === 'string' && s.emptyText.trim()) || copyParam('collection.emptySource', 'Nothing here yet');
152
+ // Chrome is noise over a small un-narrowed list (fewer than COLLECTION_CHROME_MIN_ITEMS
153
+ // records, no further page), over an empty source, and before the FIRST load or an
154
+ // un-narrowed failure. A re-query while typing keeps it (and the search field's focus).
155
+ const hideChrome = (!narrowed && !s.nextCursor && projected.loaded < COLLECTION_CHROME_MIN_ITEMS)
156
+ || sourceEmpty || ((loading || errored) && rows.length === 0 && !narrowed);
133
157
  return { ...s, collection: { ...prefs, ...projected,
134
158
  hideChrome, noActiveCriteria: !narrowed,
135
159
  viewName, viewMessage, saveMessage, noSavedViews: prefs.savedViews.length === 0,
@@ -138,8 +162,15 @@ export function decorateCollection(name: string, def: { state: State; actions?:
138
162
  noPageError: s.pageError !== true,
139
163
  noOffline: s.fromCache !== true,
140
164
  noError: !errored,
141
- noRetry: !canRetry,
165
+ noRetry: !canRetry || signInRequired,
166
+ noSignIn: !signInRequired,
167
+ errorTitle: signInRequired ? copyParam('collection.signInTitle', 'Sign in to see this') : copyParam('collection.loadErrorTitle', 'This collection could not load'),
142
168
  errorMessage,
169
+ noLoading: !loading,
170
+ // De-dup against a domain that paints its OWN empty block (screen-kit emptyBlock is
171
+ // gated on the domain's top-level `noEmpty`): only one empty state ever shows.
172
+ noEmptySource: !sourceEmpty || s.noEmpty === false,
173
+ emptySourceText,
143
174
  hideList: prefs.view !== 'list', hideGrid: prefs.view !== 'grid',
144
175
  hideComfortable: prefs.density !== 'comfortable', hideCompact: prefs.density !== 'compact',
145
176
  selectingLabel: prefs.selecting ? copyParam('collection.done', 'Done') : copyParam('collection.select', 'Select'),
@@ -147,7 +178,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
147
178
  cannotSaveSelected: !prefs.selectedIds.length || prefs.selectedIds.some(id => !rows.some(row => collectionId(row) === id)),
148
179
  filtersPresented, previewPresented: !!previewId, preview: projected.items.filter(row => row.collectionId === previewId),
149
180
  persistenceLabel: persisted.available ? copyParam('collection.onDevice', 'Saved on this device') : copyParam('collection.sessionOnly', 'Choices last for this session'),
150
- noEmpty: loading || errored || projected.items.length > 0,
181
+ noEmpty: loading || errored || sourceEmpty || domainNarrowed || projected.items.length > 0,
151
182
  summary: backend && config.backendQuery
152
183
  ? (s.fromCache ? copyParam('collection.cached', 'Saved offline · may be outdated')
153
184
  : typeof s.total === 'number' ? `${projected.items.length} / ${s.total} ${copyParam('collection.results', 'results')}`
@@ -0,0 +1,33 @@
1
+ // Empty-collection flags for descriptors.
2
+ //
3
+ // A bound Grid/LazyList with zero rows still renders a zero-height box, and a stack
4
+ // allocates its `spacing` between CHILDREN, not between painted children — so every
5
+ // empty list left a full gap of blank on the screen (the measure pass's `dead-gap`, on
6
+ // iOS and Android as much as on web). `hiddenWhen` hides on a TRUE scalar only, and an
7
+ // empty array is not one, so a descriptor cannot gate a list on its own rows.
8
+ //
9
+ // `withEmptyFlags(def, { rows: "noRows" })` derives `noRows = rows is empty` into the
10
+ // initial state and after EVERY action (sync or async), so a builder can write
11
+ // `hiddenWhen(grid(...), "$service:svc.noRows")` without each reducer remembering to.
12
+ type State = Record<string, unknown>;
13
+ type Action = (s: State, ...args: unknown[]) => unknown;
14
+
15
+ export function withEmptyFlags<D extends { state: object; actions?: object }>(def: D, flags: Record<string, string>): D {
16
+ const derive = <T>(s: T): T => {
17
+ if (!s || typeof s !== "object") return s;
18
+ const out = { ...(s as State) };
19
+ for (const [rows, flag] of Object.entries(flags)) {
20
+ const v = out[rows];
21
+ out[flag] = !Array.isArray(v) || v.length === 0;
22
+ }
23
+ return out as T;
24
+ };
25
+ const actions: Record<string, Action> = {};
26
+ for (const [name, action] of Object.entries((def.actions ?? {}) as Record<string, Action>)) {
27
+ actions[name] = (s, ...args) => {
28
+ const r = action(s, ...args);
29
+ return r && typeof (r as Promise<unknown>).then === "function" ? (r as Promise<unknown>).then(derive) : derive(r);
30
+ };
31
+ }
32
+ return { ...def, state: derive(def.state), actions } as D;
33
+ }
@@ -1,5 +1,6 @@
1
1
  // Tier-5 bundle: Wix Spaces clone — What's New (ON-DEVICE, account-level).
2
2
  import { defineService } from "@himalaya/state";
3
+ import { withEmptyFlags } from "../_shared/empty-flags.js";
3
4
  import { listAnnouncements, type WixAnnouncement } from "@himalaya/wix-new-releases";
4
5
 
5
6
  interface Row { id: string; name: string; subtitle: string }
@@ -10,7 +11,7 @@ function norm(a: WixAnnouncement, i: number): Row {
10
11
  return { id: aa.id || `a${i}`, name: aa.title || "Announcement", subtitle: aa.description || aa.bodyText || "" };
11
12
  }
12
13
 
13
- const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesWhatsNew", {
14
+ const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesWhatsNew", withEmptyFlags({
14
15
  state: { status: "loading", items: [], emptyText: "" },
15
16
  actions: {
16
17
  load: async (s) => {
@@ -21,6 +22,6 @@ const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesWhatsNew", {
21
22
  } catch { return { ...s, status: "ready", items: [], emptyText: "Nothing new right now" }; }
22
23
  },
23
24
  },
24
- });
25
+ }, { items: "noItems" }));
25
26
  svc.load();
26
27
  export { svc };