@wix/himalaya-cli 0.1036.0 → 0.1037.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 (46) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/portfolio/src/index.d.ts +46 -9
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-portfolio.mjs +14 -2
  3. package/dist/cli.mjs +476 -109
  4. package/dist/preview-assets.generated.bin +0 -0
  5. package/fixtures/wix-canned.json +51 -2
  6. package/guides/sdk-reference.json +16 -10
  7. package/guides/wix-business-context.md +13 -1
  8. package/guides/wix-site-to-himi-app.md +10 -1
  9. package/package.json +2 -2
  10. package/templates/beauty-salon/tier5-src/himi-booking-bookings/index.ts +14 -8
  11. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +12 -1
  12. package/templates/beauty-salon-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  13. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  14. package/templates/branded-lite/tier5-src/himi-booking-bookings/index.ts +14 -8
  15. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +12 -1
  16. package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio/index.ts +2 -2
  17. package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -3
  18. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +12 -1
  19. package/templates/community-club-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  20. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +12 -1
  21. package/templates/events-meetups-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  22. package/templates/gym-fitness/tier5-src/himi-booking-bookings/index.ts +14 -8
  23. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +12 -1
  24. package/templates/gym-fitness-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  25. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  26. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +12 -1
  27. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  28. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  29. package/templates/owner-lite/tier5-src/_shared/owner-module-data.ts +2 -2
  30. package/templates/owner-lite/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  31. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +12 -1
  32. package/templates/podcast-creator-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  33. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +12 -1
  34. package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio/index.ts +2 -2
  35. package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -3
  36. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  37. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  38. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +12 -1
  39. package/templates/restaurant-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  40. package/templates/restaurant-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  41. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +12 -1
  42. package/templates/retail-store-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  43. package/templates/yoga-studio/tier5-src/himi-booking-bookings/index.ts +14 -8
  44. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +12 -1
  45. package/templates/yoga-studio-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  46. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
@@ -388,6 +388,10 @@ function resolveEffectiveLayout(arg: unknown, productCount: number): LayoutId {
388
388
  });
389
389
  }
390
390
 
391
+ /** The time-of-day greeting. Read at REFRESH time only, never at boot: the release build hashes
392
+ * the initial state, so a greeting read off the build machine's clock made every build of an
393
+ * unchanged template a different release (#3268, the `nondeterministic-initial-state` lint).
394
+ * The initial state carries `neutralWelcome()` instead, and the first refresh swaps this in. */
391
395
  function welcome(): string {
392
396
  const hour = new Date().getHours();
393
397
  if (hour < 12) return copyParam("home.greetingMorning", "Good morning");
@@ -395,6 +399,11 @@ function welcome(): string {
395
399
  return copyParam("home.greetingEvening", "Good evening");
396
400
  }
397
401
 
402
+ /** The boot-time greeting: clock-free, so the baked initial state is the same on every build. */
403
+ function neutralWelcome(): string {
404
+ return copyParam("home.greetingNeutral", "Welcome");
405
+ }
406
+
398
407
  function feature(p: StoreProduct): ProductFeature {
399
408
  const name = cleanTitle(p.name) || copyParam("home.productFallbackName", "Featured item");
400
409
  const image = productImage(p);
@@ -871,7 +880,9 @@ function baseState(): State {
871
880
  hideHeroInitial: true,
872
881
  hideHeroFallback: true,
873
882
  hideFeaturedSkeleton: false,
874
- welcome: welcome(),
883
+ // Not welcome(): that reads the clock, and this state is baked into the release. A neutral
884
+ // greeting keeps the hero pill painted (same size, no reflow) until the first refresh.
885
+ welcome: neutralWelcome(),
875
886
  refreshedAt: "",
876
887
  effectiveLayout: resolveEffectiveLayout(undefined, 0),
877
888
  accentColor: resolveAccent(undefined),
@@ -8,7 +8,7 @@ import { queryPrograms, getProgram } from '@himalaya/wix-online-programs';
8
8
  import { queryReviews, getReview } from '@himalaya/wix-reviews';
9
9
  import { queryReservations, getReservation } from '@himalaya/wix-table-reservations';
10
10
  import { queryForms, getForm } from '@himalaya/wix-forms';
11
- import { listProjects, listProjectItems } from '@himalaya/wix-portfolio';
11
+ import { listProjects, listProjectItems, portfolioCoverRef, type WixMediaImage } from '@himalaya/wix-portfolio';
12
12
  import { queryGiftCards, getGiftCard } from '@himalaya/wix-gift-cards';
13
13
  import { queryDonationCampaigns, getDonationCampaign } from '@himalaya/wix-donations';
14
14
  import { query as queryBadges, get as getBadge } from '@himalaya/wix-badges';
@@ -208,7 +208,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
208
208
  details = [['Namespace', subtitle]];
209
209
  break;
210
210
  case 'portfolio':
211
- title = field(raw, 'title'); subtitle = field(raw, 'description'); image = field(raw, 'coverImage');
211
+ title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
212
212
  statusLabel = typeof at(raw, 'hidden') === 'boolean' ? at(raw, 'hidden') ? 'Hidden' : 'Visible' : '';
213
213
  break;
214
214
  case 'donationCampaigns':
@@ -311,7 +311,10 @@ function shape(s: Partial<AppointmentsState>): AppointmentsState {
311
311
  // Until the owner picks a day, follow the soonest booked one, so a freshly
312
312
  // loaded month never opens on an empty day list.
313
313
  const soonest = all.find((r) => r._day)?._day ?? "";
314
- const focus = (s._picked ? s.selectedDate : "") || soonest || dayKey(new Date());
314
+ // Today only once the list is READY (the month is hidden until then): the initial state is baked
315
+ // into the release, and a day read off the build machine's clock made every build of unchanged
316
+ // source a different release (#3268, the `nondeterministic-initial-state` lint).
317
+ const focus = (s._picked ? s.selectedDate : "") || soonest || (status === "ready" ? dayKey(new Date()) : "");
315
318
  const dayAppointments = all.filter((r) => r._day === focus);
316
319
  const dayEvents = eventsOf(dayAppointments);
317
320
  const ready = status === "ready";
@@ -388,6 +388,10 @@ function resolveEffectiveLayout(arg: unknown, productCount: number): LayoutId {
388
388
  });
389
389
  }
390
390
 
391
+ /** The time-of-day greeting. Read at REFRESH time only, never at boot: the release build hashes
392
+ * the initial state, so a greeting read off the build machine's clock made every build of an
393
+ * unchanged template a different release (#3268, the `nondeterministic-initial-state` lint).
394
+ * The initial state carries `neutralWelcome()` instead, and the first refresh swaps this in. */
391
395
  function welcome(): string {
392
396
  const hour = new Date().getHours();
393
397
  if (hour < 12) return copyParam("home.greetingMorning", "Good morning");
@@ -395,6 +399,11 @@ function welcome(): string {
395
399
  return copyParam("home.greetingEvening", "Good evening");
396
400
  }
397
401
 
402
+ /** The boot-time greeting: clock-free, so the baked initial state is the same on every build. */
403
+ function neutralWelcome(): string {
404
+ return copyParam("home.greetingNeutral", "Welcome");
405
+ }
406
+
398
407
  function feature(p: StoreProduct): ProductFeature {
399
408
  const name = cleanTitle(p.name) || copyParam("home.productFallbackName", "Featured item");
400
409
  const image = productImage(p);
@@ -871,7 +880,9 @@ function baseState(): State {
871
880
  hideHeroInitial: true,
872
881
  hideHeroFallback: true,
873
882
  hideFeaturedSkeleton: false,
874
- welcome: welcome(),
883
+ // Not welcome(): that reads the clock, and this state is baked into the release. A neutral
884
+ // greeting keeps the hero pill painted (same size, no reflow) until the first refresh.
885
+ welcome: neutralWelcome(),
875
886
  refreshedAt: "",
876
887
  effectiveLayout: resolveEffectiveLayout(undefined, 0),
877
888
  accentColor: resolveAccent(undefined),
@@ -8,7 +8,7 @@ import { queryPrograms, getProgram } from '@himalaya/wix-online-programs';
8
8
  import { queryReviews, getReview } from '@himalaya/wix-reviews';
9
9
  import { queryReservations, getReservation } from '@himalaya/wix-table-reservations';
10
10
  import { queryForms, getForm } from '@himalaya/wix-forms';
11
- import { listProjects, listProjectItems } from '@himalaya/wix-portfolio';
11
+ import { listProjects, listProjectItems, portfolioCoverRef, type WixMediaImage } from '@himalaya/wix-portfolio';
12
12
  import { queryGiftCards, getGiftCard } from '@himalaya/wix-gift-cards';
13
13
  import { queryDonationCampaigns, getDonationCampaign } from '@himalaya/wix-donations';
14
14
  import { query as queryBadges, get as getBadge } from '@himalaya/wix-badges';
@@ -208,7 +208,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
208
208
  details = [['Namespace', subtitle]];
209
209
  break;
210
210
  case 'portfolio':
211
- title = field(raw, 'title'); subtitle = field(raw, 'description'); image = field(raw, 'coverImage');
211
+ title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
212
212
  statusLabel = typeof at(raw, 'hidden') === 'boolean' ? at(raw, 'hidden') ? 'Hidden' : 'Visible' : '';
213
213
  break;
214
214
  case 'donationCampaigns':
@@ -311,7 +311,10 @@ function shape(s: Partial<AppointmentsState>): AppointmentsState {
311
311
  // Until the owner picks a day, follow the soonest booked one, so a freshly
312
312
  // loaded month never opens on an empty day list.
313
313
  const soonest = all.find((r) => r._day)?._day ?? "";
314
- const focus = (s._picked ? s.selectedDate : "") || soonest || dayKey(new Date());
314
+ // Today only once the list is READY (the month is hidden until then): the initial state is baked
315
+ // into the release, and a day read off the build machine's clock made every build of unchanged
316
+ // source a different release (#3268, the `nondeterministic-initial-state` lint).
317
+ const focus = (s._picked ? s.selectedDate : "") || soonest || (status === "ready" ? dayKey(new Date()) : "");
315
318
  const dayAppointments = all.filter((r) => r._day === focus);
316
319
  const dayEvents = eventsOf(dayAppointments);
317
320
  const ready = status === "ready";
@@ -8,7 +8,7 @@ import { queryPrograms, getProgram } from '@himalaya/wix-online-programs';
8
8
  import { queryReviews, getReview } from '@himalaya/wix-reviews';
9
9
  import { queryReservations, getReservation } from '@himalaya/wix-table-reservations';
10
10
  import { queryForms, getForm } from '@himalaya/wix-forms';
11
- import { listProjects, listProjectItems } from '@himalaya/wix-portfolio';
11
+ import { listProjects, listProjectItems, portfolioCoverRef, type WixMediaImage } from '@himalaya/wix-portfolio';
12
12
  import { queryGiftCards, getGiftCard } from '@himalaya/wix-gift-cards';
13
13
  import { queryDonationCampaigns, getDonationCampaign } from '@himalaya/wix-donations';
14
14
  import { query as queryBadges, get as getBadge } from '@himalaya/wix-badges';
@@ -208,7 +208,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
208
208
  details = [['Namespace', subtitle]];
209
209
  break;
210
210
  case 'portfolio':
211
- title = field(raw, 'title'); subtitle = field(raw, 'description'); image = field(raw, 'coverImage');
211
+ title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
212
212
  statusLabel = typeof at(raw, 'hidden') === 'boolean' ? at(raw, 'hidden') ? 'Hidden' : 'Visible' : '';
213
213
  break;
214
214
  case 'donationCampaigns':
@@ -311,7 +311,10 @@ function shape(s: Partial<AppointmentsState>): AppointmentsState {
311
311
  // Until the owner picks a day, follow the soonest booked one, so a freshly
312
312
  // loaded month never opens on an empty day list.
313
313
  const soonest = all.find((r) => r._day)?._day ?? "";
314
- const focus = (s._picked ? s.selectedDate : "") || soonest || dayKey(new Date());
314
+ // Today only once the list is READY (the month is hidden until then): the initial state is baked
315
+ // into the release, and a day read off the build machine's clock made every build of unchanged
316
+ // source a different release (#3268, the `nondeterministic-initial-state` lint).
317
+ const focus = (s._picked ? s.selectedDate : "") || soonest || (status === "ready" ? dayKey(new Date()) : "");
315
318
  const dayAppointments = all.filter((r) => r._day === focus);
316
319
  const dayEvents = eventsOf(dayAppointments);
317
320
  const ready = status === "ready";
@@ -388,6 +388,10 @@ function resolveEffectiveLayout(arg: unknown, productCount: number): LayoutId {
388
388
  });
389
389
  }
390
390
 
391
+ /** The time-of-day greeting. Read at REFRESH time only, never at boot: the release build hashes
392
+ * the initial state, so a greeting read off the build machine's clock made every build of an
393
+ * unchanged template a different release (#3268, the `nondeterministic-initial-state` lint).
394
+ * The initial state carries `neutralWelcome()` instead, and the first refresh swaps this in. */
391
395
  function welcome(): string {
392
396
  const hour = new Date().getHours();
393
397
  if (hour < 12) return copyParam("home.greetingMorning", "Good morning");
@@ -395,6 +399,11 @@ function welcome(): string {
395
399
  return copyParam("home.greetingEvening", "Good evening");
396
400
  }
397
401
 
402
+ /** The boot-time greeting: clock-free, so the baked initial state is the same on every build. */
403
+ function neutralWelcome(): string {
404
+ return copyParam("home.greetingNeutral", "Welcome");
405
+ }
406
+
398
407
  function feature(p: StoreProduct): ProductFeature {
399
408
  const name = cleanTitle(p.name) || copyParam("home.productFallbackName", "Featured item");
400
409
  const image = productImage(p);
@@ -871,7 +880,9 @@ function baseState(): State {
871
880
  hideHeroInitial: true,
872
881
  hideHeroFallback: true,
873
882
  hideFeaturedSkeleton: false,
874
- welcome: welcome(),
883
+ // Not welcome(): that reads the clock, and this state is baked into the release. A neutral
884
+ // greeting keeps the hero pill painted (same size, no reflow) until the first refresh.
885
+ welcome: neutralWelcome(),
875
886
  refreshedAt: "",
876
887
  effectiveLayout: resolveEffectiveLayout(undefined, 0),
877
888
  accentColor: resolveAccent(undefined),
@@ -8,7 +8,7 @@ import { queryPrograms, getProgram } from '@himalaya/wix-online-programs';
8
8
  import { queryReviews, getReview } from '@himalaya/wix-reviews';
9
9
  import { queryReservations, getReservation } from '@himalaya/wix-table-reservations';
10
10
  import { queryForms, getForm } from '@himalaya/wix-forms';
11
- import { listProjects, listProjectItems } from '@himalaya/wix-portfolio';
11
+ import { listProjects, listProjectItems, portfolioCoverRef, type WixMediaImage } from '@himalaya/wix-portfolio';
12
12
  import { queryGiftCards, getGiftCard } from '@himalaya/wix-gift-cards';
13
13
  import { queryDonationCampaigns, getDonationCampaign } from '@himalaya/wix-donations';
14
14
  import { query as queryBadges, get as getBadge } from '@himalaya/wix-badges';
@@ -208,7 +208,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
208
208
  details = [['Namespace', subtitle]];
209
209
  break;
210
210
  case 'portfolio':
211
- title = field(raw, 'title'); subtitle = field(raw, 'description'); image = field(raw, 'coverImage');
211
+ title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
212
212
  statusLabel = typeof at(raw, 'hidden') === 'boolean' ? at(raw, 'hidden') ? 'Hidden' : 'Visible' : '';
213
213
  break;
214
214
  case 'donationCampaigns':
@@ -388,6 +388,10 @@ function resolveEffectiveLayout(arg: unknown, productCount: number): LayoutId {
388
388
  });
389
389
  }
390
390
 
391
+ /** The time-of-day greeting. Read at REFRESH time only, never at boot: the release build hashes
392
+ * the initial state, so a greeting read off the build machine's clock made every build of an
393
+ * unchanged template a different release (#3268, the `nondeterministic-initial-state` lint).
394
+ * The initial state carries `neutralWelcome()` instead, and the first refresh swaps this in. */
391
395
  function welcome(): string {
392
396
  const hour = new Date().getHours();
393
397
  if (hour < 12) return copyParam("home.greetingMorning", "Good morning");
@@ -395,6 +399,11 @@ function welcome(): string {
395
399
  return copyParam("home.greetingEvening", "Good evening");
396
400
  }
397
401
 
402
+ /** The boot-time greeting: clock-free, so the baked initial state is the same on every build. */
403
+ function neutralWelcome(): string {
404
+ return copyParam("home.greetingNeutral", "Welcome");
405
+ }
406
+
398
407
  function feature(p: StoreProduct): ProductFeature {
399
408
  const name = cleanTitle(p.name) || copyParam("home.productFallbackName", "Featured item");
400
409
  const image = productImage(p);
@@ -871,7 +880,9 @@ function baseState(): State {
871
880
  hideHeroInitial: true,
872
881
  hideHeroFallback: true,
873
882
  hideFeaturedSkeleton: false,
874
- welcome: welcome(),
883
+ // Not welcome(): that reads the clock, and this state is baked into the release. A neutral
884
+ // greeting keeps the hero pill painted (same size, no reflow) until the first refresh.
885
+ welcome: neutralWelcome(),
875
886
  refreshedAt: "",
876
887
  effectiveLayout: resolveEffectiveLayout(undefined, 0),
877
888
  accentColor: resolveAccent(undefined),
@@ -1,7 +1,7 @@
1
1
  // himi-branded-lite-portfolio — real, public Wix Portfolio projects.
2
2
  // Empty and error states stay explicit; media IDs are normalized for every host.
3
3
  import { defineService } from "../_shared/collection-service.js";
4
- import { listProjects, type WixPortfolioProject } from "@himalaya/wix-portfolio";
4
+ import { listProjects, portfolioCoverRef, type WixPortfolioProject } from "@himalaya/wix-portfolio";
5
5
  import { wixMediaUrl } from "@himalaya/wix-media";
6
6
 
7
7
  interface Row {
@@ -24,7 +24,7 @@ interface S {
24
24
 
25
25
  function norm(project: WixPortfolioProject): Row {
26
26
  const title = (project.title || "Project").trim() || "Project";
27
- const image = wixMediaUrl(project.coverImage || "");
27
+ const image = wixMediaUrl(portfolioCoverRef(project.coverImage));
28
28
  return {
29
29
  id: project.id || project.slug || title,
30
30
  title,
@@ -2,7 +2,7 @@
2
2
  // The public project row is resolved again from Wix, then its ordered project
3
3
  // items supply the actual image/video/text story. No sample project is invented.
4
4
  import { defineService } from "@himalaya/state";
5
- import { listProjectItems, listProjects, type WixPortfolioProjectItem } from "@himalaya/wix-portfolio";
5
+ import { listProjectItems, listProjects, portfolioCoverRef, type WixPortfolioProjectItem } from "@himalaya/wix-portfolio";
6
6
  import { wixMediaUrl, WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
7
7
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
8
8
  import { presence, type PresenceFlags } from "../_shared/detail-presence.js";
@@ -48,7 +48,7 @@ function inputId(): string {
48
48
  }
49
49
 
50
50
  function imageOf(item: WixPortfolioProjectItem): string {
51
- return wixMediaUrl(item.image?.imageInfo?.url ?? item.image?.imageInfo?.id, WIX_DETAIL_IMAGE);
51
+ return wixMediaUrl(portfolioCoverRef(item.image), WIX_DETAIL_IMAGE);
52
52
  }
53
53
 
54
54
  function row(item: WixPortfolioProjectItem, index: number): MediaRow {
@@ -133,7 +133,7 @@ const svc = defineService<State, { load: (state: State, arg?: unknown) => Promis
133
133
  title: (project.title || copyParam("portfolioProject.projectFallbackName", "Project")).trim(),
134
134
  initial: ((project.title || "P").trim()[0] || "P").toUpperCase(),
135
135
  description: (project.description ?? "").trim(),
136
- coverImage: wixMediaUrl(project.coverImage || "", WIX_DETAIL_IMAGE),
136
+ coverImage: wixMediaUrl(portfolioCoverRef(project.coverImage), WIX_DETAIL_IMAGE),
137
137
  items,
138
138
  emptyText: items.length ? "" : copyParam("portfolioProject.noMediaText", "This project has no published media yet."),
139
139
  ...presence("found"),
@@ -8,7 +8,7 @@ import { queryPrograms, getProgram } from '@himalaya/wix-online-programs';
8
8
  import { queryReviews, getReview } from '@himalaya/wix-reviews';
9
9
  import { queryReservations, getReservation } from '@himalaya/wix-table-reservations';
10
10
  import { queryForms, getForm } from '@himalaya/wix-forms';
11
- import { listProjects, listProjectItems } from '@himalaya/wix-portfolio';
11
+ import { listProjects, listProjectItems, portfolioCoverRef, type WixMediaImage } from '@himalaya/wix-portfolio';
12
12
  import { queryGiftCards, getGiftCard } from '@himalaya/wix-gift-cards';
13
13
  import { queryDonationCampaigns, getDonationCampaign } from '@himalaya/wix-donations';
14
14
  import { query as queryBadges, get as getBadge } from '@himalaya/wix-badges';
@@ -208,7 +208,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
208
208
  details = [['Namespace', subtitle]];
209
209
  break;
210
210
  case 'portfolio':
211
- title = field(raw, 'title'); subtitle = field(raw, 'description'); image = field(raw, 'coverImage');
211
+ title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
212
212
  statusLabel = typeof at(raw, 'hidden') === 'boolean' ? at(raw, 'hidden') ? 'Hidden' : 'Visible' : '';
213
213
  break;
214
214
  case 'donationCampaigns':
@@ -311,7 +311,10 @@ function shape(s: Partial<AppointmentsState>): AppointmentsState {
311
311
  // Until the owner picks a day, follow the soonest booked one, so a freshly
312
312
  // loaded month never opens on an empty day list.
313
313
  const soonest = all.find((r) => r._day)?._day ?? "";
314
- const focus = (s._picked ? s.selectedDate : "") || soonest || dayKey(new Date());
314
+ // Today only once the list is READY (the month is hidden until then): the initial state is baked
315
+ // into the release, and a day read off the build machine's clock made every build of unchanged
316
+ // source a different release (#3268, the `nondeterministic-initial-state` lint).
317
+ const focus = (s._picked ? s.selectedDate : "") || soonest || (status === "ready" ? dayKey(new Date()) : "");
315
318
  const dayAppointments = all.filter((r) => r._day === focus);
316
319
  const dayEvents = eventsOf(dayAppointments);
317
320
  const ready = status === "ready";
@@ -388,6 +388,10 @@ function resolveEffectiveLayout(arg: unknown, productCount: number): LayoutId {
388
388
  });
389
389
  }
390
390
 
391
+ /** The time-of-day greeting. Read at REFRESH time only, never at boot: the release build hashes
392
+ * the initial state, so a greeting read off the build machine's clock made every build of an
393
+ * unchanged template a different release (#3268, the `nondeterministic-initial-state` lint).
394
+ * The initial state carries `neutralWelcome()` instead, and the first refresh swaps this in. */
391
395
  function welcome(): string {
392
396
  const hour = new Date().getHours();
393
397
  if (hour < 12) return copyParam("home.greetingMorning", "Good morning");
@@ -395,6 +399,11 @@ function welcome(): string {
395
399
  return copyParam("home.greetingEvening", "Good evening");
396
400
  }
397
401
 
402
+ /** The boot-time greeting: clock-free, so the baked initial state is the same on every build. */
403
+ function neutralWelcome(): string {
404
+ return copyParam("home.greetingNeutral", "Welcome");
405
+ }
406
+
398
407
  function feature(p: StoreProduct): ProductFeature {
399
408
  const name = cleanTitle(p.name) || copyParam("home.productFallbackName", "Featured item");
400
409
  const image = productImage(p);
@@ -871,7 +880,9 @@ function baseState(): State {
871
880
  hideHeroInitial: true,
872
881
  hideHeroFallback: true,
873
882
  hideFeaturedSkeleton: false,
874
- welcome: welcome(),
883
+ // Not welcome(): that reads the clock, and this state is baked into the release. A neutral
884
+ // greeting keeps the hero pill painted (same size, no reflow) until the first refresh.
885
+ welcome: neutralWelcome(),
875
886
  refreshedAt: "",
876
887
  effectiveLayout: resolveEffectiveLayout(undefined, 0),
877
888
  accentColor: resolveAccent(undefined),
@@ -8,7 +8,7 @@ import { queryPrograms, getProgram } from '@himalaya/wix-online-programs';
8
8
  import { queryReviews, getReview } from '@himalaya/wix-reviews';
9
9
  import { queryReservations, getReservation } from '@himalaya/wix-table-reservations';
10
10
  import { queryForms, getForm } from '@himalaya/wix-forms';
11
- import { listProjects, listProjectItems } from '@himalaya/wix-portfolio';
11
+ import { listProjects, listProjectItems, portfolioCoverRef, type WixMediaImage } from '@himalaya/wix-portfolio';
12
12
  import { queryGiftCards, getGiftCard } from '@himalaya/wix-gift-cards';
13
13
  import { queryDonationCampaigns, getDonationCampaign } from '@himalaya/wix-donations';
14
14
  import { query as queryBadges, get as getBadge } from '@himalaya/wix-badges';
@@ -208,7 +208,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
208
208
  details = [['Namespace', subtitle]];
209
209
  break;
210
210
  case 'portfolio':
211
- title = field(raw, 'title'); subtitle = field(raw, 'description'); image = field(raw, 'coverImage');
211
+ title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
212
212
  statusLabel = typeof at(raw, 'hidden') === 'boolean' ? at(raw, 'hidden') ? 'Hidden' : 'Visible' : '';
213
213
  break;
214
214
  case 'donationCampaigns':
@@ -311,7 +311,10 @@ function shape(s: Partial<AppointmentsState>): AppointmentsState {
311
311
  // Until the owner picks a day, follow the soonest booked one, so a freshly
312
312
  // loaded month never opens on an empty day list.
313
313
  const soonest = all.find((r) => r._day)?._day ?? "";
314
- const focus = (s._picked ? s.selectedDate : "") || soonest || dayKey(new Date());
314
+ // Today only once the list is READY (the month is hidden until then): the initial state is baked
315
+ // into the release, and a day read off the build machine's clock made every build of unchanged
316
+ // source a different release (#3268, the `nondeterministic-initial-state` lint).
317
+ const focus = (s._picked ? s.selectedDate : "") || soonest || (status === "ready" ? dayKey(new Date()) : "");
315
318
  const dayAppointments = all.filter((r) => r._day === focus);
316
319
  const dayEvents = eventsOf(dayAppointments);
317
320
  const ready = status === "ready";
@@ -388,6 +388,10 @@ function resolveEffectiveLayout(arg: unknown, productCount: number): LayoutId {
388
388
  });
389
389
  }
390
390
 
391
+ /** The time-of-day greeting. Read at REFRESH time only, never at boot: the release build hashes
392
+ * the initial state, so a greeting read off the build machine's clock made every build of an
393
+ * unchanged template a different release (#3268, the `nondeterministic-initial-state` lint).
394
+ * The initial state carries `neutralWelcome()` instead, and the first refresh swaps this in. */
391
395
  function welcome(): string {
392
396
  const hour = new Date().getHours();
393
397
  if (hour < 12) return copyParam("home.greetingMorning", "Good morning");
@@ -395,6 +399,11 @@ function welcome(): string {
395
399
  return copyParam("home.greetingEvening", "Good evening");
396
400
  }
397
401
 
402
+ /** The boot-time greeting: clock-free, so the baked initial state is the same on every build. */
403
+ function neutralWelcome(): string {
404
+ return copyParam("home.greetingNeutral", "Welcome");
405
+ }
406
+
398
407
  function feature(p: StoreProduct): ProductFeature {
399
408
  const name = cleanTitle(p.name) || copyParam("home.productFallbackName", "Featured item");
400
409
  const image = productImage(p);
@@ -871,7 +880,9 @@ function baseState(): State {
871
880
  hideHeroInitial: true,
872
881
  hideHeroFallback: true,
873
882
  hideFeaturedSkeleton: false,
874
- welcome: welcome(),
883
+ // Not welcome(): that reads the clock, and this state is baked into the release. A neutral
884
+ // greeting keeps the hero pill painted (same size, no reflow) until the first refresh.
885
+ welcome: neutralWelcome(),
875
886
  refreshedAt: "",
876
887
  effectiveLayout: resolveEffectiveLayout(undefined, 0),
877
888
  accentColor: resolveAccent(undefined),
@@ -8,7 +8,7 @@ import { queryPrograms, getProgram } from '@himalaya/wix-online-programs';
8
8
  import { queryReviews, getReview } from '@himalaya/wix-reviews';
9
9
  import { queryReservations, getReservation } from '@himalaya/wix-table-reservations';
10
10
  import { queryForms, getForm } from '@himalaya/wix-forms';
11
- import { listProjects, listProjectItems } from '@himalaya/wix-portfolio';
11
+ import { listProjects, listProjectItems, portfolioCoverRef, type WixMediaImage } from '@himalaya/wix-portfolio';
12
12
  import { queryGiftCards, getGiftCard } from '@himalaya/wix-gift-cards';
13
13
  import { queryDonationCampaigns, getDonationCampaign } from '@himalaya/wix-donations';
14
14
  import { query as queryBadges, get as getBadge } from '@himalaya/wix-badges';
@@ -208,7 +208,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
208
208
  details = [['Namespace', subtitle]];
209
209
  break;
210
210
  case 'portfolio':
211
- title = field(raw, 'title'); subtitle = field(raw, 'description'); image = field(raw, 'coverImage');
211
+ title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
212
212
  statusLabel = typeof at(raw, 'hidden') === 'boolean' ? at(raw, 'hidden') ? 'Hidden' : 'Visible' : '';
213
213
  break;
214
214
  case 'donationCampaigns':
@@ -11,9 +11,14 @@ const input = (globalThis as { himalaya?: { input?: Record<string, unknown> } })
11
11
  const validDate = (value: unknown): value is string => typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && dayKey(new Date(`${value}T12:00:00`)) === value;
12
12
  const views = localPreferences("booking.schedule", value => ["agenda", "day", "month"].includes(String(value)) ? String(value) : "agenda");
13
13
  let mode = validDate(input?.date) ? "day" : views.read();
14
- let selectedDate = validDate(input?.date) ? input.date : dayKey(new Date());
14
+ // "" until the schedule is first shown, NOT today's date: the release build hashes this worker's
15
+ // initial state, so a date read off the build machine's clock made every build of unchanged
16
+ // source a different release (#3268, the `nondeterministic-initial-state` lint). `derive()` fills
17
+ // in today the first time the schedule is visible, which is never at build time (no member).
18
+ let selectedDate = validDate(input?.date) ? input.date : "";
19
+ const today = () => dayKey(new Date());
15
20
  let pageMember: MemberAccess | undefined;
16
- let queryNow = new Date().toISOString();
21
+ let queryNow = ""; // set by the first-page fetch, never at boot (see selectedDate)
17
22
  let cancelTarget: { row: MemberBookingRow; member: MemberAccess } | undefined;
18
23
  let cancelling = false;
19
24
  interface S {
@@ -26,11 +31,12 @@ interface S {
26
31
  nextCursor?: string; complete?: boolean; loadingMore?: boolean; pageError?: boolean; total?: number;
27
32
  }
28
33
  function derive(s: S): S {
29
- const dayRows = s.rows.filter(row => row.day === selectedDate);
34
+ const hideSchedule = !s.isMember || s.isLoading;
35
+ if (!selectedDate && !hideSchedule) selectedDate = today();
36
+ const dayRows = selectedDate ? s.rows.filter(row => row.day === selectedDate) : [];
30
37
  const counts = new Map<string, number>();
31
38
  for (const row of s.rows) if (row.day) counts.set(row.day, (counts.get(row.day) ?? 0) + 1);
32
- const hideSchedule = !s.isMember || s.isLoading;
33
- return { ...s, scheduleMode: mode, selectedDate, dayRows, dayHeading: dayLabel(selectedDate), hideSchedule,
39
+ return { ...s, scheduleMode: mode, selectedDate, dayRows, dayHeading: selectedDate ? dayLabel(selectedDate) : "", hideSchedule,
34
40
  hideAgenda: hideSchedule || mode !== "agenda", hideCalendar: hideSchedule || mode === "agenda", hideMonth: mode !== "month",
35
41
  decorations: [...counts].map(([date, n]) => ({ date, dotCount: Math.min(n, 3), dotColorToken: "accentPrimary" })),
36
42
  dayEmptyText: dayRows.length ? "" : s.complete ? copyParam("bookings.emptyDay", "No bookings on this day.") : copyParam("bookings.emptyLoadedDay", "No bookings on this day in loaded results. Load more to check later pages."),
@@ -48,7 +54,7 @@ const svc = defineBackendCollection<S, MemberBookingRow>("himiBookingBookings",
48
54
  async fetch(criteria, cursor) {
49
55
  const member = await memberAccess();
50
56
  if (cursor && (!pageMember || pageMember.email !== member.email || pageMember.scope !== member.scope)) throw memberAccessChanged();
51
- if (!cursor) queryNow = new Date().toISOString();
57
+ if (!cursor || !queryNow) queryNow = new Date().toISOString();
52
58
  return fetchMemberBookings(criteria, cursor, queryNow, member);
53
59
  },
54
60
  },
@@ -65,8 +71,8 @@ const svc = defineBackendCollection<S, MemberBookingRow>("himiBookingBookings",
65
71
  actions: {
66
72
  scheduleView(s, arg) { const value = (arg as { selectedId?: string })?.selectedId; if (["agenda", "day", "month"].includes(value || "")) { mode = value!; views.write(mode); } return derive(s); },
67
73
  selectDate(s, arg) { const date = (arg as { date?: unknown })?.date; if (validDate(date)) selectedDate = date; return derive(s); },
68
- previousDay(s) { const date = new Date(`${selectedDate}T12:00:00`); date.setDate(date.getDate() - 1); selectedDate = dayKey(date); return derive(s); },
69
- nextDay(s) { const date = new Date(`${selectedDate}T12:00:00`); date.setDate(date.getDate() + 1); selectedDate = dayKey(date); return derive(s); },
74
+ previousDay(s) { const date = new Date(`${selectedDate || today()}T12:00:00`); date.setDate(date.getDate() - 1); selectedDate = dayKey(date); return derive(s); },
75
+ nextDay(s) { const date = new Date(`${selectedDate || today()}T12:00:00`); date.setDate(date.getDate() + 1); selectedDate = dayKey(date); return derive(s); },
70
76
  async rebook(s, arg) {
71
77
  const member = pageMember, row = s.rows.find(row => row.id === (arg as { id?: string })?.id);
72
78
  if (!member || !row?.serviceId) return s;
@@ -388,6 +388,10 @@ function resolveEffectiveLayout(arg: unknown, productCount: number): LayoutId {
388
388
  });
389
389
  }
390
390
 
391
+ /** The time-of-day greeting. Read at REFRESH time only, never at boot: the release build hashes
392
+ * the initial state, so a greeting read off the build machine's clock made every build of an
393
+ * unchanged template a different release (#3268, the `nondeterministic-initial-state` lint).
394
+ * The initial state carries `neutralWelcome()` instead, and the first refresh swaps this in. */
391
395
  function welcome(): string {
392
396
  const hour = new Date().getHours();
393
397
  if (hour < 12) return copyParam("home.greetingMorning", "Good morning");
@@ -395,6 +399,11 @@ function welcome(): string {
395
399
  return copyParam("home.greetingEvening", "Good evening");
396
400
  }
397
401
 
402
+ /** The boot-time greeting: clock-free, so the baked initial state is the same on every build. */
403
+ function neutralWelcome(): string {
404
+ return copyParam("home.greetingNeutral", "Welcome");
405
+ }
406
+
398
407
  function feature(p: StoreProduct): ProductFeature {
399
408
  const name = cleanTitle(p.name) || copyParam("home.productFallbackName", "Featured item");
400
409
  const image = productImage(p);
@@ -871,7 +880,9 @@ function baseState(): State {
871
880
  hideHeroInitial: true,
872
881
  hideHeroFallback: true,
873
882
  hideFeaturedSkeleton: false,
874
- welcome: welcome(),
883
+ // Not welcome(): that reads the clock, and this state is baked into the release. A neutral
884
+ // greeting keeps the hero pill painted (same size, no reflow) until the first refresh.
885
+ welcome: neutralWelcome(),
875
886
  refreshedAt: "",
876
887
  effectiveLayout: resolveEffectiveLayout(undefined, 0),
877
888
  accentColor: resolveAccent(undefined),
@@ -8,7 +8,7 @@ import { queryPrograms, getProgram } from '@himalaya/wix-online-programs';
8
8
  import { queryReviews, getReview } from '@himalaya/wix-reviews';
9
9
  import { queryReservations, getReservation } from '@himalaya/wix-table-reservations';
10
10
  import { queryForms, getForm } from '@himalaya/wix-forms';
11
- import { listProjects, listProjectItems } from '@himalaya/wix-portfolio';
11
+ import { listProjects, listProjectItems, portfolioCoverRef, type WixMediaImage } from '@himalaya/wix-portfolio';
12
12
  import { queryGiftCards, getGiftCard } from '@himalaya/wix-gift-cards';
13
13
  import { queryDonationCampaigns, getDonationCampaign } from '@himalaya/wix-donations';
14
14
  import { query as queryBadges, get as getBadge } from '@himalaya/wix-badges';
@@ -208,7 +208,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
208
208
  details = [['Namespace', subtitle]];
209
209
  break;
210
210
  case 'portfolio':
211
- title = field(raw, 'title'); subtitle = field(raw, 'description'); image = field(raw, 'coverImage');
211
+ title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
212
212
  statusLabel = typeof at(raw, 'hidden') === 'boolean' ? at(raw, 'hidden') ? 'Hidden' : 'Visible' : '';
213
213
  break;
214
214
  case 'donationCampaigns':
@@ -311,7 +311,10 @@ function shape(s: Partial<AppointmentsState>): AppointmentsState {
311
311
  // Until the owner picks a day, follow the soonest booked one, so a freshly
312
312
  // loaded month never opens on an empty day list.
313
313
  const soonest = all.find((r) => r._day)?._day ?? "";
314
- const focus = (s._picked ? s.selectedDate : "") || soonest || dayKey(new Date());
314
+ // Today only once the list is READY (the month is hidden until then): the initial state is baked
315
+ // into the release, and a day read off the build machine's clock made every build of unchanged
316
+ // source a different release (#3268, the `nondeterministic-initial-state` lint).
317
+ const focus = (s._picked ? s.selectedDate : "") || soonest || (status === "ready" ? dayKey(new Date()) : "");
315
318
  const dayAppointments = all.filter((r) => r._day === focus);
316
319
  const dayEvents = eventsOf(dayAppointments);
317
320
  const ready = status === "ready";