@wix/himalaya-cli 0.1026.0 → 0.1027.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 (74) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/builder.d.ts +9 -0
  2. package/dist/cli.mjs +1052 -653
  3. package/dist/preview-assets.generated.bin +0 -0
  4. package/guides/app-config.md +6 -0
  5. package/guides/headless-loop.md +3 -1
  6. package/package.json +2 -2
  7. package/templates/beauty-salon/dev/index.ts +331 -199
  8. package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +18 -8
  9. package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +28 -3
  10. package/templates/beauty-salon-owner/dev/index.ts +857 -458
  11. package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +18 -8
  12. package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +28 -3
  13. package/templates/branded-lite/dev/index.ts +1539 -623
  14. package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +18 -8
  15. package/templates/branded-lite/tier5-src/_shared/collection-service.ts +28 -3
  16. package/templates/community-club/dev/index.ts +237 -142
  17. package/templates/community-club/tier5-src/_shared/collection-contract.ts +18 -8
  18. package/templates/community-club/tier5-src/_shared/collection-service.ts +28 -3
  19. package/templates/community-club-owner/dev/index.ts +857 -458
  20. package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +18 -8
  21. package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +28 -3
  22. package/templates/events-meetups/dev/index.ts +202 -129
  23. package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +18 -8
  24. package/templates/events-meetups/tier5-src/_shared/collection-service.ts +28 -3
  25. package/templates/events-meetups-owner/dev/index.ts +815 -458
  26. package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +18 -8
  27. package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +28 -3
  28. package/templates/gym-fitness/dev/index.ts +461 -212
  29. package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +18 -8
  30. package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +28 -3
  31. package/templates/gym-fitness-owner/dev/index.ts +758 -338
  32. package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +18 -8
  33. package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +28 -3
  34. package/templates/himi-chat/dev/index.ts +26 -1
  35. package/templates/nonprofit-cause/dev/index.ts +246 -153
  36. package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +18 -8
  37. package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +28 -3
  38. package/templates/nonprofit-cause-owner/dev/index.ts +716 -338
  39. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +18 -8
  40. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +28 -3
  41. package/templates/owner-lite/dev/index.ts +758 -338
  42. package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +18 -8
  43. package/templates/owner-lite/tier5-src/_shared/collection-service.ts +28 -3
  44. package/templates/podcast-creator/dev/index.ts +255 -164
  45. package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +18 -8
  46. package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +28 -3
  47. package/templates/podcast-creator-owner/dev/index.ts +857 -458
  48. package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +18 -8
  49. package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +28 -3
  50. package/templates/portfolio-creative/dev/index.ts +287 -175
  51. package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +18 -8
  52. package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +28 -3
  53. package/templates/portfolio-creative-owner/dev/index.ts +857 -458
  54. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +18 -8
  55. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +28 -3
  56. package/templates/restaurant/dev/index.ts +200 -126
  57. package/templates/restaurant/tier5-src/_shared/collection-contract.ts +18 -8
  58. package/templates/restaurant/tier5-src/_shared/collection-service.ts +28 -3
  59. package/templates/restaurant-owner/dev/index.ts +758 -338
  60. package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +18 -8
  61. package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +28 -3
  62. package/templates/retail-store/dev/index.ts +113 -138
  63. package/templates/retail-store/tier5-src/_shared/collection-contract.ts +18 -8
  64. package/templates/retail-store/tier5-src/_shared/collection-service.ts +28 -3
  65. package/templates/retail-store-owner/dev/index.ts +716 -338
  66. package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +18 -8
  67. package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +28 -3
  68. package/templates/surface-showcase/dev/index.ts +27 -0
  69. package/templates/yoga-studio/dev/index.ts +427 -198
  70. package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +18 -8
  71. package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +28 -3
  72. package/templates/yoga-studio-owner/dev/index.ts +716 -338
  73. package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +18 -8
  74. package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +28 -3
@@ -8,6 +8,9 @@ export interface CollectionConfig {
8
8
  image?: string;
9
9
  filter?: string;
10
10
  filterValues?: string[];
11
+ /** A state key holding `{ label }` rows (e.g. a worker's full category list) whose
12
+ * labels are filter choices even when no loaded row carries them yet. */
13
+ filterValuesFrom?: string;
11
14
  searchPlaceholder?: string;
12
15
  sortLabels?: { default: string; az: string; za: string };
13
16
  metadata?: string[];
@@ -20,15 +23,22 @@ export interface CollectionConfig {
20
23
  reviewSelectedAction?: string;
21
24
  /** Optional comfortable grid media height for immersive visual catalogs. */
22
25
  gridImageHeight?: number;
26
+ /** The card's primary verb, shown as a visible button beside the overflow.
27
+ * `screenId` opens that screen with the row id; without it the verb reuses the
28
+ * row's own tap action (the detail screen that owns the verb). */
29
+ primaryVerb?: { label: string; screenId?: string };
23
30
  }
31
+ /** At or below this many records, with no query/filter narrowing them and no further
32
+ * page, the list/grid toggle, Refine, chips, search and count are noise. */
33
+ export const COLLECTION_CHROME_MIN_ITEMS = 4;
24
34
  export const COLLECTIONS: Record<string, CollectionConfig> = {
25
35
  himiStoreShop: { rows: 'results', title: 'name', subtitle: 'subtitle', image: 'image', view: 'grid', backendQuery: true },
26
- himiBookingServices: { rows: 'results', title: 'name', subtitle: 'subtitle', image: 'image', filter: 'category', view: 'grid', backendQuery: true, searchPlaceholder: 'Service name starts with…' },
36
+ himiBookingServices: { rows: 'results', title: 'name', subtitle: 'subtitle', image: 'image', filter: 'category', view: 'grid', backendQuery: true, searchPlaceholder: 'Service name starts with…', primaryVerb: { label: 'Book', screenId: 'himi_time' } },
27
37
  himiBookingBookings: { rows: 'rows', title: 'title', subtitle: 'subtitle', metadata: ['status'], view: 'list', backendQuery: true,
28
38
  defaultFilter: 'value:Upcoming', searchPlaceholder: 'Exact booking ID', filterValues: ['Upcoming', 'Past', 'Cancelled'],
29
39
  sortLabels: { default: 'Soonest', az: 'Oldest', za: 'Latest' } },
30
- 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' } },
31
- himiBlogFeed: { rows: 'posts', title: 'title', subtitle: 'excerpt', image: 'image', view: 'grid' },
40
+ 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' } },
41
+ himiBlogFeed: { rows: 'posts', title: 'title', subtitle: 'excerpt', image: 'image', filter: 'category', filterValuesFrom: 'categories', view: 'grid' },
32
42
  himiOwnerLiteProducts: { rows: 'products', title: 'name', subtitle: 'price', image: 'image', view: 'grid', backendQuery: true },
33
43
  himiOwnerModules: { instanceInput: 'module', rows: 'rows', title: 'title', subtitle: 'subtitle', image: 'image', filter: 'statusLabel', view: 'list', paged: true, reviewAction: 'prepareRecord', reviewSelectedAction: 'prepareSelected' },
34
44
  himiOwnerLiteServices: { rows: 'services', title: 'name', subtitle: 'subtitle', view: 'list', backendQuery: true, searchPlaceholder: 'Service name starts with…' },
@@ -45,14 +55,14 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
45
55
  sortLabels: { default: 'Newest', az: 'Oldest', za: 'Order # ↓' } },
46
56
  himiBrandedPlans: { rows: 'rows', title: 'name', subtitle: 'description', metadata: ['priceLine', 'periodLine', 'trialBadge', 'perk1', 'perk2', 'perk3'], view: 'grid' },
47
57
  himiBrandedPrograms: { rows: 'rows', title: 'name', subtitle: 'description', image: 'image', view: 'grid' },
48
- himiBrandedCoupons: { rows: 'coupons', title: 'name', subtitle: 'subtitle', view: 'list' },
58
+ himiBrandedCoupons: { rows: 'coupons', title: 'name', subtitle: 'subtitle', view: 'list', primaryVerb: { label: 'Get code' } },
49
59
  himiBrandedNotifications: { rows: 'items', title: 'name', subtitle: 'subtitle', image: 'photo', metadata: ['unreadDot'], view: 'list' },
50
- himiBrandedLoyalty: { rows: 'rewards', title: 'name', subtitle: 'description', view: 'grid' },
60
+ himiBrandedLoyalty: { rows: 'rewards', title: 'name', subtitle: 'description', view: 'grid', primaryVerb: { label: 'Redeem' } },
51
61
  himiBrandedReviews: { rows: 'reviews', title: 'title', subtitle: 'body', metadata: ['author', 'stars'], view: 'list' },
52
62
  himiBrandedPortfolio: { rows: 'projects', title: 'title', subtitle: 'subtitle', image: 'image', view: 'grid', gridImageHeight: 180 },
53
63
  himiBrandedInvoices: { rows: 'invoices', title: 'title', subtitle: 'subtitle', view: 'list' },
54
64
  himiBrandedBadges: { rows: 'badges', title: 'name', subtitle: 'description', view: 'grid' },
55
- himiBrandedDonations: { rows: 'campaigns', title: 'name', subtitle: 'description', view: 'grid' },
65
+ himiBrandedDonations: { rows: 'campaigns', title: 'name', subtitle: 'description', view: 'grid', primaryVerb: { label: 'Donate' } },
56
66
  himiBrandedGiftCards: { rows: 'cards', title: 'name', subtitle: 'description', view: 'grid' },
57
67
  himiBrandedVideos: { rows: 'channels', title: 'title', subtitle: 'description', view: 'grid' },
58
68
  himiBrandedPodcasts: { rows: 'episodes', title: 'title', subtitle: 'subtitle', image: 'image', metadata: ['progressLabel'], view: 'list' },
@@ -114,7 +124,7 @@ export function collectionTitle(row: CollectionRow, config: CollectionConfig): s
114
124
  return scalar(row[config.title]) || scalar(row.title) || scalar(row.name) || scalar(row.display) || scalar(row.label);
115
125
  }
116
126
  /** Does not mutate domain rows, paging, or domain-derived state. */
117
- export function projectCollection(source: CollectionRow[], config: CollectionConfig, prefs: CollectionPreferences, backendQuery = false) {
127
+ export function projectCollection(source: CollectionRow[], config: CollectionConfig, prefs: CollectionPreferences, backendQuery = false, knownFilters: string[] = []) {
118
128
  const query = prefs.query.trim().toLocaleLowerCase();
119
129
  const unique = new Set<string>();
120
130
  // Menu sections describe their following items; they are not actionable
@@ -130,7 +140,7 @@ export function projectCollection(source: CollectionRow[], config: CollectionCon
130
140
  if (unique.has(id)) return false;
131
141
  unique.add(id); return true;
132
142
  });
133
- const filters = config.filterValues ?? [...new Set(rows.map(row => config.filter ? scalar(row[config.filter]) : '').filter(Boolean))].sort();
143
+ const filters = config.filterValues ?? [...new Set([...knownFilters, ...rows.map(row => config.filter ? scalar(row[config.filter]) : '')].filter(Boolean))].sort();
134
144
  const visible = rows.filter(row => {
135
145
  if (backendQuery) return true;
136
146
  if (prefs.filter === 'saved' && !prefs.savedIds.includes(collectionId(row))) return false;
@@ -1,5 +1,5 @@
1
1
  import { defineService as register, type ServiceDefinition } from '@himalaya/state';
2
- import { COLLECTIONS, normalizeCollection, projectCollection, collectionId, type CollectionRow } from './collection-contract.js';
2
+ import { COLLECTIONS, COLLECTION_CHROME_MIN_ITEMS, initialCollection, normalizeCollection, projectCollection, collectionId, 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';
@@ -54,6 +54,14 @@ export function decorateCollection(name: string, def: { state: State; actions?:
54
54
  /** Navigation can open a collection in a useful, reviewable context. Keep
55
55
  * this input additive and constrained to the same criteria the collection UI
56
56
  * can select; arbitrary route values must never become backend filters. */
57
+ /** Labels of the worker-listed filter choices (`filterValuesFrom`), minus the "All" row. */
58
+ const listedFilters = (state: State): string[] => config.filterValuesFrom && Array.isArray(state[config.filterValuesFrom])
59
+ ? (state[config.filterValuesFrom] as unknown[]).flatMap(row => {
60
+ const r = row && typeof row === 'object' ? row as Record<string, unknown> : {};
61
+ return typeof r.label === 'string' && r.id !== '' ? [r.label] : [];
62
+ })
63
+ : [];
64
+ let pendingFilter = '';
57
65
  const applyNavigationCriteria = (arg: unknown, state: State): boolean => {
58
66
  const input = object(arg);
59
67
  let changed = false;
@@ -70,9 +78,14 @@ export function decorateCollection(name: string, def: { state: State; actions?:
70
78
  const configured = config.filterValues ?? [];
71
79
  const rows = Array.isArray(state[config.rows]) ? state[config.rows] as CollectionRow[] : [];
72
80
  const derived = config.filter ? rows.map(row => String(row[config.filter!] ?? '')).filter(Boolean) : [];
81
+ const known = listedFilters(state);
73
82
  const allowed = filter === 'all' || filter === 'saved' ||
74
- (filter.startsWith('value:') && [...configured, ...derived].includes(filter.slice(6)));
83
+ (filter.startsWith('value:') && [...configured, ...derived, ...known].includes(filter.slice(6)));
75
84
  if (allowed) { prefs.filter = filter; changed = true; }
85
+ // A worker-listed choice (e.g. a blog category) may not have loaded yet at boot:
86
+ // hold it and settle it against the list once the list arrives, instead of
87
+ // discarding a valid deep link before load().
88
+ else if (filter.startsWith('value:') && config.filterValuesFrom && known.length === 0) pendingFilter = filter;
76
89
  }
77
90
  if (changed) {
78
91
  prefs.selectedIds = [];
@@ -90,7 +103,12 @@ export function decorateCollection(name: string, def: { state: State; actions?:
90
103
  const derive = (s: State): State => {
91
104
  syncSaved();
92
105
  const rows = Array.isArray(s[config.rows]) ? (s[config.rows] as CollectionRow[]).filter(x => x && typeof x === 'object') : [];
93
- const projected = projectCollection(rows, config, prefs, !!backend && config.backendQuery === true);
106
+ const known = listedFilters(s);
107
+ if (pendingFilter && known.length) {
108
+ if (known.includes(pendingFilter.slice(6))) { prefs.filter = pendingFilter; persisted.write(prefs); }
109
+ pendingFilter = '';
110
+ }
111
+ const projected = projectCollection(rows, config, prefs, !!backend && config.backendQuery === true, known);
94
112
  projected.items.forEach(row => { row.collectionSelectLabel = row.collectionSelected ? copyParam('collection.selected', 'Selected') : copyParam('collection.select', 'Select'); });
95
113
  if (previewId && !rows.some(row => collectionId(row) === previewId)) previewId = '';
96
114
  const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, { id: 'saved', label: copyParam('collection.saved', 'Saved') },
@@ -106,7 +124,14 @@ export function decorateCollection(name: string, def: { state: State; actions?:
106
124
  const queryError = typeof backendState?.error === 'string' ? backendState.error.trim() : '';
107
125
  const errorMessage = queryError || domainError || copyParam('collection.loadError', 'This collection could not load. Try again.');
108
126
  const canRetry = !!backend || typeof def.actions?.refresh === 'function' || typeof def.actions?.load === 'function' || typeof def.actions?.collectionRetry === 'function';
127
+ // A default view is what the screen opens on; anything else is the member's own
128
+ // narrowing, which keeps the chrome (and the Clear that undoes it) on screen.
129
+ // "All" and the collection's own default (e.g. Upcoming) are both un-narrowed: Clear
130
+ // 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;
109
133
  return { ...s, collection: { ...prefs, ...projected,
134
+ hideChrome, noActiveCriteria: !narrowed,
110
135
  viewName, viewMessage, saveMessage, noSavedViews: prefs.savedViews.length === 0,
111
136
  cursor: s.pageError === true || s.loadingMore === true ? '' : s.nextCursor,
112
137
  noMore: !s.nextCursor || s.pageError === true || s.loadingMore === true,
@@ -4338,7 +4338,8 @@ const SCREENS = runtimeSetting({
4338
4338
  "modifiers": {
4339
4339
  "padding": {
4340
4340
  "bottom": 12
4341
- }
4341
+ },
4342
+ "hiddenPath": "$service:himiStoreShop.collection.hideChrome"
4342
4343
  }
4343
4344
  },
4344
4345
  {
@@ -4597,6 +4598,7 @@ const SCREENS = runtimeSetting({
4597
4598
  }
4598
4599
  },
4599
4600
  "modifiers": {
4601
+ "hiddenPath": "$service:himiStoreShop.collection.hideChrome",
4600
4602
  "padding": {
4601
4603
  "bottom": 12
4602
4604
  }
@@ -4634,37 +4636,55 @@ const SCREENS = runtimeSetting({
4634
4636
  "actionId": "tier5.himiStoreShop.collectionFilter"
4635
4637
  }
4636
4638
  }
4639
+ },
4640
+ "modifiers": {
4641
+ "weight": 1
4637
4642
  }
4638
4643
  },
4639
4644
  {
4640
- "id": "",
4641
- "body": {
4642
- "package": "himalaya",
4643
- "type": "Spacer",
4644
- "props": {}
4645
- }
4646
- },
4647
- {
4648
- "id": "",
4645
+ "id": "himi_shop.list.collection.clear",
4649
4646
  "body": {
4650
4647
  "package": "himalaya",
4651
- "type": "Text",
4648
+ "type": "Button",
4652
4649
  "props": {
4653
- "text": "$service:himiStoreShop.collection.summary",
4654
- "style": "caption",
4655
- "color": "textSecondary"
4650
+ "title": "Clear",
4651
+ "style": "plain",
4652
+ "fullWidth": false,
4653
+ "action": {
4654
+ "actionId": "tier5.himiStoreShop.collectionClear"
4655
+ }
4656
4656
  }
4657
+ },
4658
+ "modifiers": {
4659
+ "hiddenPath": "$service:himiStoreShop.collection.noActiveCriteria"
4657
4660
  }
4658
4661
  }
4659
4662
  ]
4660
4663
  }
4661
4664
  },
4662
4665
  "modifiers": {
4666
+ "hiddenPath": "$service:himiStoreShop.collection.hideChrome",
4663
4667
  "padding": {
4664
4668
  "bottom": 12
4665
4669
  }
4666
4670
  }
4667
4671
  },
4672
+ {
4673
+ "id": "himi_shop.list.collection.summary",
4674
+ "body": {
4675
+ "package": "himalaya",
4676
+ "type": "Text",
4677
+ "props": {
4678
+ "text": "$service:himiStoreShop.collection.summary",
4679
+ "style": "caption",
4680
+ "color": "textSecondary",
4681
+ "maxLines": 1
4682
+ }
4683
+ },
4684
+ "modifiers": {
4685
+ "hiddenPath": "$service:himiStoreShop.collection.hideChrome"
4686
+ }
4687
+ },
4668
4688
  {
4669
4689
  "id": "",
4670
4690
  "body": {
@@ -8493,6 +8513,27 @@ const SCREENS = runtimeSetting({
8493
8513
  "props": {
8494
8514
  "spacing": 0,
8495
8515
  "children": [
8516
+ {
8517
+ "id": "himi_orders.list.collection.search",
8518
+ "body": {
8519
+ "package": "himalaya",
8520
+ "type": "SearchField",
8521
+ "props": {
8522
+ "placeholder": "Exact order number",
8523
+ "value": "$service:himiBrandedOrders.collection.query",
8524
+ "debounceMs": 200,
8525
+ "onChange": {
8526
+ "actionId": "tier5.himiBrandedOrders.collectionSearch"
8527
+ }
8528
+ }
8529
+ },
8530
+ "modifiers": {
8531
+ "hiddenPath": "$service:himiBrandedOrders.collection.hideChrome",
8532
+ "padding": {
8533
+ "bottom": 12
8534
+ }
8535
+ }
8536
+ },
8496
8537
  {
8497
8538
  "id": "",
8498
8539
  "body": {
@@ -8547,6 +8588,7 @@ const SCREENS = runtimeSetting({
8547
8588
  }
8548
8589
  },
8549
8590
  "modifiers": {
8591
+ "hiddenPath": "$service:himiBrandedOrders.collection.hideChrome",
8550
8592
  "padding": {
8551
8593
  "bottom": 12
8552
8594
  }
@@ -8584,55 +8626,53 @@ const SCREENS = runtimeSetting({
8584
8626
  "actionId": "tier5.himiBrandedOrders.collectionFilter"
8585
8627
  }
8586
8628
  }
8629
+ },
8630
+ "modifiers": {
8631
+ "weight": 1
8587
8632
  }
8588
8633
  },
8589
8634
  {
8590
- "id": "",
8591
- "body": {
8592
- "package": "himalaya",
8593
- "type": "Spacer",
8594
- "props": {}
8595
- }
8596
- },
8597
- {
8598
- "id": "",
8635
+ "id": "himi_orders.list.collection.clear",
8599
8636
  "body": {
8600
8637
  "package": "himalaya",
8601
- "type": "Text",
8638
+ "type": "Button",
8602
8639
  "props": {
8603
- "text": "$service:himiBrandedOrders.collection.summary",
8604
- "style": "caption",
8605
- "color": "textSecondary"
8640
+ "title": "Clear",
8641
+ "style": "plain",
8642
+ "fullWidth": false,
8643
+ "action": {
8644
+ "actionId": "tier5.himiBrandedOrders.collectionClear"
8645
+ }
8606
8646
  }
8647
+ },
8648
+ "modifiers": {
8649
+ "hiddenPath": "$service:himiBrandedOrders.collection.noActiveCriteria"
8607
8650
  }
8608
8651
  }
8609
8652
  ]
8610
8653
  }
8611
8654
  },
8612
8655
  "modifiers": {
8656
+ "hiddenPath": "$service:himiBrandedOrders.collection.hideChrome",
8613
8657
  "padding": {
8614
8658
  "bottom": 12
8615
8659
  }
8616
8660
  }
8617
8661
  },
8618
8662
  {
8619
- "id": "himi_orders.list.collection.search",
8663
+ "id": "himi_orders.list.collection.summary",
8620
8664
  "body": {
8621
8665
  "package": "himalaya",
8622
- "type": "SearchField",
8666
+ "type": "Text",
8623
8667
  "props": {
8624
- "placeholder": "Exact order number",
8625
- "value": "$service:himiBrandedOrders.collection.query",
8626
- "debounceMs": 200,
8627
- "onChange": {
8628
- "actionId": "tier5.himiBrandedOrders.collectionSearch"
8629
- }
8668
+ "text": "$service:himiBrandedOrders.collection.summary",
8669
+ "style": "caption",
8670
+ "color": "textSecondary",
8671
+ "maxLines": 1
8630
8672
  }
8631
8673
  },
8632
8674
  "modifiers": {
8633
- "padding": {
8634
- "bottom": 12
8635
- }
8675
+ "hiddenPath": "$service:himiBrandedOrders.collection.hideChrome"
8636
8676
  }
8637
8677
  },
8638
8678
  {
@@ -11019,59 +11059,9 @@ const SCREENS = runtimeSetting({
11019
11059
  },
11020
11060
  "value": "$service:himiBlogFeed.collection.query"
11021
11061
  }
11022
- }
11023
- },
11024
- {
11025
- "id": "himi_blog.categories",
11026
- "body": {
11027
- "package": "himalaya",
11028
- "type": "Grid",
11029
- "props": {
11030
- "columns": 2,
11031
- "spacing": 8,
11032
- "itemsPath": "$service:himiBlogFeed.categories",
11033
- "items": [
11034
- {
11035
- "id": "",
11036
- "body": {
11037
- "package": "himalaya",
11038
- "type": "Text",
11039
- "props": {
11040
- "text": "$item.label",
11041
- "style": "footnote",
11042
- "color": "textPrimary",
11043
- "maxLines": 1
11044
- }
11045
- },
11046
- "modifiers": {
11047
- "background": {
11048
- "token": "accentSurface"
11049
- },
11050
- "border": {
11051
- "width": 1,
11052
- "color": {
11053
- "token": "border"
11054
- }
11055
- },
11056
- "cornerRadius": 8,
11057
- "padding": {
11058
- "leading": 12,
11059
- "trailing": 12,
11060
- "top": 9,
11061
- "bottom": 9
11062
- },
11063
- "onTap": {
11064
- "actionId": "tier5.himiBlogFeed.selectCategory",
11065
- "params": {
11066
- "categoryId": "$item.id"
11067
- }
11068
- },
11069
- "pressFeedback": "scale",
11070
- "haptic": "light"
11071
- }
11072
- }
11073
- ]
11074
- }
11062
+ },
11063
+ "modifiers": {
11064
+ "hiddenPath": "$service:himiBlogFeed.collection.hideChrome"
11075
11065
  }
11076
11066
  },
11077
11067
  {
@@ -11266,40 +11256,6 @@ const SCREENS = runtimeSetting({
11266
11256
  "spacing": 12,
11267
11257
  "alignment": "leading",
11268
11258
  "children": [
11269
- {
11270
- "id": "",
11271
- "body": {
11272
- "package": "himalaya",
11273
- "type": "HStack",
11274
- "props": {
11275
- "spacing": 8,
11276
- "children": [
11277
- {
11278
- "id": "",
11279
- "body": {
11280
- "package": "himalaya",
11281
- "type": "Spacer",
11282
- "props": {}
11283
- }
11284
- },
11285
- {
11286
- "id": "himi_blog.clear",
11287
- "body": {
11288
- "package": "himalaya",
11289
- "type": "Button",
11290
- "props": {
11291
- "title": "Clear",
11292
- "style": "secondary",
11293
- "action": {
11294
- "actionId": "tier5.himiBlogFeed.clearFilters"
11295
- }
11296
- }
11297
- }
11298
- }
11299
- ]
11300
- }
11301
- }
11302
- },
11303
11259
  {
11304
11260
  "id": "himi_blog.list.collection.toolbar",
11305
11261
  "body": {
@@ -11362,6 +11318,7 @@ const SCREENS = runtimeSetting({
11362
11318
  }
11363
11319
  },
11364
11320
  "modifiers": {
11321
+ "hiddenPath": "$service:himiBlogFeed.collection.hideChrome",
11365
11322
  "padding": {
11366
11323
  "bottom": 12
11367
11324
  }
@@ -11399,37 +11356,55 @@ const SCREENS = runtimeSetting({
11399
11356
  "actionId": "tier5.himiBlogFeed.collectionFilter"
11400
11357
  }
11401
11358
  }
11359
+ },
11360
+ "modifiers": {
11361
+ "weight": 1
11402
11362
  }
11403
11363
  },
11404
11364
  {
11405
- "id": "",
11406
- "body": {
11407
- "package": "himalaya",
11408
- "type": "Spacer",
11409
- "props": {}
11410
- }
11411
- },
11412
- {
11413
- "id": "",
11365
+ "id": "himi_blog.list.collection.clear",
11414
11366
  "body": {
11415
11367
  "package": "himalaya",
11416
- "type": "Text",
11368
+ "type": "Button",
11417
11369
  "props": {
11418
- "text": "$service:himiBlogFeed.collection.summary",
11419
- "style": "caption",
11420
- "color": "textSecondary"
11370
+ "title": "Clear",
11371
+ "style": "plain",
11372
+ "fullWidth": false,
11373
+ "action": {
11374
+ "actionId": "tier5.himiBlogFeed.collectionClear"
11375
+ }
11421
11376
  }
11377
+ },
11378
+ "modifiers": {
11379
+ "hiddenPath": "$service:himiBlogFeed.collection.noActiveCriteria"
11422
11380
  }
11423
11381
  }
11424
11382
  ]
11425
11383
  }
11426
11384
  },
11427
11385
  "modifiers": {
11386
+ "hiddenPath": "$service:himiBlogFeed.collection.hideChrome",
11428
11387
  "padding": {
11429
11388
  "bottom": 12
11430
11389
  }
11431
11390
  }
11432
11391
  },
11392
+ {
11393
+ "id": "himi_blog.list.collection.summary",
11394
+ "body": {
11395
+ "package": "himalaya",
11396
+ "type": "Text",
11397
+ "props": {
11398
+ "text": "$service:himiBlogFeed.collection.summary",
11399
+ "style": "caption",
11400
+ "color": "textSecondary",
11401
+ "maxLines": 1
11402
+ }
11403
+ },
11404
+ "modifiers": {
11405
+ "hiddenPath": "$service:himiBlogFeed.collection.hideChrome"
11406
+ }
11407
+ },
11433
11408
  {
11434
11409
  "id": "",
11435
11410
  "body": {
@@ -8,6 +8,9 @@ export interface CollectionConfig {
8
8
  image?: string;
9
9
  filter?: string;
10
10
  filterValues?: string[];
11
+ /** A state key holding `{ label }` rows (e.g. a worker's full category list) whose
12
+ * labels are filter choices even when no loaded row carries them yet. */
13
+ filterValuesFrom?: string;
11
14
  searchPlaceholder?: string;
12
15
  sortLabels?: { default: string; az: string; za: string };
13
16
  metadata?: string[];
@@ -20,15 +23,22 @@ export interface CollectionConfig {
20
23
  reviewSelectedAction?: string;
21
24
  /** Optional comfortable grid media height for immersive visual catalogs. */
22
25
  gridImageHeight?: number;
26
+ /** The card's primary verb, shown as a visible button beside the overflow.
27
+ * `screenId` opens that screen with the row id; without it the verb reuses the
28
+ * row's own tap action (the detail screen that owns the verb). */
29
+ primaryVerb?: { label: string; screenId?: string };
23
30
  }
31
+ /** At or below this many records, with no query/filter narrowing them and no further
32
+ * page, the list/grid toggle, Refine, chips, search and count are noise. */
33
+ export const COLLECTION_CHROME_MIN_ITEMS = 4;
24
34
  export const COLLECTIONS: Record<string, CollectionConfig> = {
25
35
  himiStoreShop: { rows: 'results', title: 'name', subtitle: 'subtitle', image: 'image', view: 'grid', backendQuery: true },
26
- himiBookingServices: { rows: 'results', title: 'name', subtitle: 'subtitle', image: 'image', filter: 'category', view: 'grid', backendQuery: true, searchPlaceholder: 'Service name starts with…' },
36
+ himiBookingServices: { rows: 'results', title: 'name', subtitle: 'subtitle', image: 'image', filter: 'category', view: 'grid', backendQuery: true, searchPlaceholder: 'Service name starts with…', primaryVerb: { label: 'Book', screenId: 'himi_time' } },
27
37
  himiBookingBookings: { rows: 'rows', title: 'title', subtitle: 'subtitle', metadata: ['status'], view: 'list', backendQuery: true,
28
38
  defaultFilter: 'value:Upcoming', searchPlaceholder: 'Exact booking ID', filterValues: ['Upcoming', 'Past', 'Cancelled'],
29
39
  sortLabels: { default: 'Soonest', az: 'Oldest', za: 'Latest' } },
30
- 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' } },
31
- himiBlogFeed: { rows: 'posts', title: 'title', subtitle: 'excerpt', image: 'image', view: 'grid' },
40
+ 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' } },
41
+ himiBlogFeed: { rows: 'posts', title: 'title', subtitle: 'excerpt', image: 'image', filter: 'category', filterValuesFrom: 'categories', view: 'grid' },
32
42
  himiOwnerLiteProducts: { rows: 'products', title: 'name', subtitle: 'price', image: 'image', view: 'grid', backendQuery: true },
33
43
  himiOwnerModules: { instanceInput: 'module', rows: 'rows', title: 'title', subtitle: 'subtitle', image: 'image', filter: 'statusLabel', view: 'list', paged: true, reviewAction: 'prepareRecord', reviewSelectedAction: 'prepareSelected' },
34
44
  himiOwnerLiteServices: { rows: 'services', title: 'name', subtitle: 'subtitle', view: 'list', backendQuery: true, searchPlaceholder: 'Service name starts with…' },
@@ -45,14 +55,14 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
45
55
  sortLabels: { default: 'Newest', az: 'Oldest', za: 'Order # ↓' } },
46
56
  himiBrandedPlans: { rows: 'rows', title: 'name', subtitle: 'description', metadata: ['priceLine', 'periodLine', 'trialBadge', 'perk1', 'perk2', 'perk3'], view: 'grid' },
47
57
  himiBrandedPrograms: { rows: 'rows', title: 'name', subtitle: 'description', image: 'image', view: 'grid' },
48
- himiBrandedCoupons: { rows: 'coupons', title: 'name', subtitle: 'subtitle', view: 'list' },
58
+ himiBrandedCoupons: { rows: 'coupons', title: 'name', subtitle: 'subtitle', view: 'list', primaryVerb: { label: 'Get code' } },
49
59
  himiBrandedNotifications: { rows: 'items', title: 'name', subtitle: 'subtitle', image: 'photo', metadata: ['unreadDot'], view: 'list' },
50
- himiBrandedLoyalty: { rows: 'rewards', title: 'name', subtitle: 'description', view: 'grid' },
60
+ himiBrandedLoyalty: { rows: 'rewards', title: 'name', subtitle: 'description', view: 'grid', primaryVerb: { label: 'Redeem' } },
51
61
  himiBrandedReviews: { rows: 'reviews', title: 'title', subtitle: 'body', metadata: ['author', 'stars'], view: 'list' },
52
62
  himiBrandedPortfolio: { rows: 'projects', title: 'title', subtitle: 'subtitle', image: 'image', view: 'grid', gridImageHeight: 180 },
53
63
  himiBrandedInvoices: { rows: 'invoices', title: 'title', subtitle: 'subtitle', view: 'list' },
54
64
  himiBrandedBadges: { rows: 'badges', title: 'name', subtitle: 'description', view: 'grid' },
55
- himiBrandedDonations: { rows: 'campaigns', title: 'name', subtitle: 'description', view: 'grid' },
65
+ himiBrandedDonations: { rows: 'campaigns', title: 'name', subtitle: 'description', view: 'grid', primaryVerb: { label: 'Donate' } },
56
66
  himiBrandedGiftCards: { rows: 'cards', title: 'name', subtitle: 'description', view: 'grid' },
57
67
  himiBrandedVideos: { rows: 'channels', title: 'title', subtitle: 'description', view: 'grid' },
58
68
  himiBrandedPodcasts: { rows: 'episodes', title: 'title', subtitle: 'subtitle', image: 'image', metadata: ['progressLabel'], view: 'list' },
@@ -114,7 +124,7 @@ export function collectionTitle(row: CollectionRow, config: CollectionConfig): s
114
124
  return scalar(row[config.title]) || scalar(row.title) || scalar(row.name) || scalar(row.display) || scalar(row.label);
115
125
  }
116
126
  /** Does not mutate domain rows, paging, or domain-derived state. */
117
- export function projectCollection(source: CollectionRow[], config: CollectionConfig, prefs: CollectionPreferences, backendQuery = false) {
127
+ export function projectCollection(source: CollectionRow[], config: CollectionConfig, prefs: CollectionPreferences, backendQuery = false, knownFilters: string[] = []) {
118
128
  const query = prefs.query.trim().toLocaleLowerCase();
119
129
  const unique = new Set<string>();
120
130
  // Menu sections describe their following items; they are not actionable
@@ -130,7 +140,7 @@ export function projectCollection(source: CollectionRow[], config: CollectionCon
130
140
  if (unique.has(id)) return false;
131
141
  unique.add(id); return true;
132
142
  });
133
- const filters = config.filterValues ?? [...new Set(rows.map(row => config.filter ? scalar(row[config.filter]) : '').filter(Boolean))].sort();
143
+ const filters = config.filterValues ?? [...new Set([...knownFilters, ...rows.map(row => config.filter ? scalar(row[config.filter]) : '')].filter(Boolean))].sort();
134
144
  const visible = rows.filter(row => {
135
145
  if (backendQuery) return true;
136
146
  if (prefs.filter === 'saved' && !prefs.savedIds.includes(collectionId(row))) return false;