@wix/himalaya-cli 0.1209.0 → 0.1211.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 (180) hide show
  1. package/dist/cli.mjs +112 -18
  2. package/dist/preview-assets.generated.bin +0 -0
  3. package/package.json +2 -2
  4. package/templates/beauty-salon/tier5-src/_shared/engine-intl.ts +57 -16
  5. package/templates/beauty-salon-owner/dev/index.ts +80 -50
  6. package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +31 -16
  7. package/templates/beauty-salon-owner/tier5-src/_shared/engine-intl.ts +57 -16
  8. package/templates/beauty-salon-owner/tier5-src/_shared/himi-text.ts +24 -0
  9. package/templates/beauty-salon-owner/tier5-src/_shared/owner-account.ts +4 -0
  10. package/templates/beauty-salon-owner/tier5-src/_shared/owner-module-data.ts +105 -37
  11. package/templates/beauty-salon-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
  12. package/templates/beauty-salon-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
  13. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
  14. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
  15. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
  16. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
  17. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
  18. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
  19. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
  20. package/templates/branded-lite/tier5-src/_shared/engine-intl.ts +57 -16
  21. package/templates/community-club/tier5-src/_shared/engine-intl.ts +57 -16
  22. package/templates/community-club-owner/dev/index.ts +80 -50
  23. package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +31 -16
  24. package/templates/community-club-owner/tier5-src/_shared/engine-intl.ts +57 -16
  25. package/templates/community-club-owner/tier5-src/_shared/himi-text.ts +24 -0
  26. package/templates/community-club-owner/tier5-src/_shared/owner-account.ts +4 -0
  27. package/templates/community-club-owner/tier5-src/_shared/owner-module-data.ts +105 -37
  28. package/templates/community-club-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
  29. package/templates/community-club-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
  30. package/templates/community-club-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
  31. package/templates/community-club-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
  32. package/templates/community-club-owner/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
  33. package/templates/community-club-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
  34. package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
  35. package/templates/community-club-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
  36. package/templates/community-club-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
  37. package/templates/events-meetups/tier5-src/_shared/engine-intl.ts +57 -16
  38. package/templates/events-meetups-owner/dev/index.ts +80 -50
  39. package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +31 -16
  40. package/templates/events-meetups-owner/tier5-src/_shared/engine-intl.ts +57 -16
  41. package/templates/events-meetups-owner/tier5-src/_shared/himi-text.ts +24 -0
  42. package/templates/events-meetups-owner/tier5-src/_shared/owner-account.ts +4 -0
  43. package/templates/events-meetups-owner/tier5-src/_shared/owner-module-data.ts +105 -37
  44. package/templates/events-meetups-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
  45. package/templates/events-meetups-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
  46. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
  47. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
  48. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
  49. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
  50. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
  51. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
  52. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
  53. package/templates/gym-fitness/tier5-src/_shared/engine-intl.ts +57 -16
  54. package/templates/gym-fitness-owner/dev/index.ts +80 -50
  55. package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +31 -16
  56. package/templates/gym-fitness-owner/tier5-src/_shared/engine-intl.ts +57 -16
  57. package/templates/gym-fitness-owner/tier5-src/_shared/himi-text.ts +24 -0
  58. package/templates/gym-fitness-owner/tier5-src/_shared/owner-account.ts +4 -0
  59. package/templates/gym-fitness-owner/tier5-src/_shared/owner-module-data.ts +105 -37
  60. package/templates/gym-fitness-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
  61. package/templates/gym-fitness-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
  62. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
  63. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
  64. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
  65. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
  66. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
  67. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
  68. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
  69. package/templates/nonprofit-cause/tier5-src/_shared/engine-intl.ts +57 -16
  70. package/templates/nonprofit-cause-owner/dev/index.ts +80 -50
  71. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +31 -16
  72. package/templates/nonprofit-cause-owner/tier5-src/_shared/engine-intl.ts +57 -16
  73. package/templates/nonprofit-cause-owner/tier5-src/_shared/himi-text.ts +24 -0
  74. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-account.ts +4 -0
  75. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-module-data.ts +105 -37
  76. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
  77. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
  78. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
  79. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
  80. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
  81. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
  82. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
  83. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
  84. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
  85. package/templates/owner-lite/dev/index.ts +81 -51
  86. package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +31 -16
  87. package/templates/owner-lite/tier5-src/_shared/engine-intl.ts +57 -16
  88. package/templates/owner-lite/tier5-src/_shared/himi-text.ts +24 -0
  89. package/templates/owner-lite/tier5-src/_shared/owner-account.ts +4 -0
  90. package/templates/owner-lite/tier5-src/_shared/owner-module-data.ts +105 -37
  91. package/templates/owner-lite/tier5-src/_shared/owner-selected-site.ts +103 -0
  92. package/templates/owner-lite/tier5-src/_shared/owner-site-pick.ts +74 -0
  93. package/templates/owner-lite/tier5-src/himi-owner-lite-account/index.ts +12 -2
  94. package/templates/owner-lite/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
  95. package/templates/owner-lite/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
  96. package/templates/owner-lite/tier5-src/himi-owner-lite-faq/index.ts +8 -2
  97. package/templates/owner-lite/tier5-src/himi-owner-lite-home/index.ts +29 -1
  98. package/templates/owner-lite/tier5-src/himi-owner-lite-manage/index.ts +25 -5
  99. package/templates/owner-lite/tier5-src/himi-owner-lite-overview/index.ts +8 -0
  100. package/templates/owner-lite/tier5-src/himi-owner-lite-sites/index.ts +14 -31
  101. package/templates/podcast-creator/tier5-src/_shared/engine-intl.ts +57 -16
  102. package/templates/podcast-creator-owner/dev/index.ts +80 -50
  103. package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +31 -16
  104. package/templates/podcast-creator-owner/tier5-src/_shared/engine-intl.ts +57 -16
  105. package/templates/podcast-creator-owner/tier5-src/_shared/himi-text.ts +24 -0
  106. package/templates/podcast-creator-owner/tier5-src/_shared/owner-account.ts +4 -0
  107. package/templates/podcast-creator-owner/tier5-src/_shared/owner-module-data.ts +105 -37
  108. package/templates/podcast-creator-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
  109. package/templates/podcast-creator-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
  110. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
  111. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
  112. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
  113. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
  114. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
  115. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
  116. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
  117. package/templates/portfolio-creative/tier5-src/_shared/engine-intl.ts +57 -16
  118. package/templates/portfolio-creative-owner/dev/index.ts +80 -50
  119. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +31 -16
  120. package/templates/portfolio-creative-owner/tier5-src/_shared/engine-intl.ts +57 -16
  121. package/templates/portfolio-creative-owner/tier5-src/_shared/himi-text.ts +24 -0
  122. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-account.ts +4 -0
  123. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-module-data.ts +105 -37
  124. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
  125. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
  126. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
  127. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
  128. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
  129. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
  130. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
  131. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
  132. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
  133. package/templates/restaurant/tier5-src/_shared/engine-intl.ts +57 -16
  134. package/templates/restaurant-owner/dev/index.ts +80 -50
  135. package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +31 -16
  136. package/templates/restaurant-owner/tier5-src/_shared/engine-intl.ts +57 -16
  137. package/templates/restaurant-owner/tier5-src/_shared/himi-text.ts +24 -0
  138. package/templates/restaurant-owner/tier5-src/_shared/owner-account.ts +4 -0
  139. package/templates/restaurant-owner/tier5-src/_shared/owner-module-data.ts +105 -37
  140. package/templates/restaurant-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
  141. package/templates/restaurant-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
  142. package/templates/restaurant-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
  143. package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
  144. package/templates/restaurant-owner/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
  145. package/templates/restaurant-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
  146. package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
  147. package/templates/restaurant-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
  148. package/templates/restaurant-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
  149. package/templates/retail-store/tier5-src/_shared/engine-intl.ts +57 -16
  150. package/templates/retail-store-owner/dev/index.ts +80 -50
  151. package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +31 -16
  152. package/templates/retail-store-owner/tier5-src/_shared/engine-intl.ts +57 -16
  153. package/templates/retail-store-owner/tier5-src/_shared/himi-text.ts +24 -0
  154. package/templates/retail-store-owner/tier5-src/_shared/owner-account.ts +4 -0
  155. package/templates/retail-store-owner/tier5-src/_shared/owner-module-data.ts +105 -37
  156. package/templates/retail-store-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
  157. package/templates/retail-store-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
  158. package/templates/retail-store-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
  159. package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
  160. package/templates/retail-store-owner/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
  161. package/templates/retail-store-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
  162. package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
  163. package/templates/retail-store-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
  164. package/templates/retail-store-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
  165. package/templates/yoga-studio/tier5-src/_shared/engine-intl.ts +57 -16
  166. package/templates/yoga-studio-owner/dev/index.ts +80 -50
  167. package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +31 -16
  168. package/templates/yoga-studio-owner/tier5-src/_shared/engine-intl.ts +57 -16
  169. package/templates/yoga-studio-owner/tier5-src/_shared/himi-text.ts +24 -0
  170. package/templates/yoga-studio-owner/tier5-src/_shared/owner-account.ts +4 -0
  171. package/templates/yoga-studio-owner/tier5-src/_shared/owner-module-data.ts +105 -37
  172. package/templates/yoga-studio-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
  173. package/templates/yoga-studio-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
  174. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
  175. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
  176. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
  177. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
  178. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
  179. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
  180. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
@@ -48,6 +48,36 @@ const MONTHS_LONG = [
48
48
  "July", "August", "September", "October", "November", "December",
49
49
  ] as const;
50
50
 
51
+ /**
52
+ * A DateTimeFormat for `timeZone` when one is given and this engine knows it. An unknown or
53
+ * malformed IANA id is a RangeError in ECMA-402; these helpers never throw, so it degrades to
54
+ * the device zone rather than killing the screen. Without Intl there is no zone database at all,
55
+ * and the fallbacks below render in the device zone for the same reason.
56
+ *
57
+ * Formatters are memoised per (locale, zone, options): a management page formats several dates
58
+ * per row, and building a DateTimeFormat (or throwing on a bad zone) each time is not free on
59
+ * JSC or QuickJS. Only call it where `hasIntl()` holds — every caller does.
60
+ */
61
+ const FORMATTERS = new Map<string, Intl.DateTimeFormat>();
62
+ function dateFormat(locale: string, options: Intl.DateTimeFormatOptions, timeZone?: string): Intl.DateTimeFormat {
63
+ const key = `${locale}|${timeZone ?? ""}|${JSON.stringify(options)}`;
64
+ let formatter = FORMATTERS.get(key);
65
+ if (!formatter) {
66
+ if (hasIntl()) {
67
+ if (timeZone) {
68
+ try {
69
+ formatter = new Intl.DateTimeFormat(locale, { ...options, timeZone });
70
+ } catch {
71
+ /* unknown zone: device zone below, remembered under this key */
72
+ }
73
+ }
74
+ formatter ??= new Intl.DateTimeFormat(locale, options);
75
+ FORMATTERS.set(key, formatter);
76
+ }
77
+ }
78
+ return formatter as Intl.DateTimeFormat;
79
+ }
80
+
51
81
  // The four shapes below are not invented — they are the option bags this corpus actually uses,
52
82
  // counted across every bundle: "Jan 5, 2026" (33 sites), "Jan 5" (11), "Mon, Jan 5, 3:07 PM" (8)
53
83
  // and "Jan 5, 3:07 PM" (4). Naming them means a conversion is a one-line swap rather than a
@@ -55,46 +85,46 @@ const MONTHS_LONG = [
55
85
  // ECMA-402 in engine-intl-fallback.test.ts.
56
86
 
57
87
  /** `{ month: "short", day: "numeric", year: "numeric" }` — "Jan 5, 2026". */
58
- export function formatDateMedium(date: Date, locale = "en-US"): string {
88
+ export function formatDateMedium(date: Date, locale = "en-US", timeZone?: string): string {
59
89
  if (hasIntl()) {
60
- return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", year: "numeric" }).format(date);
90
+ return dateFormat(locale, { month: "short", day: "numeric", year: "numeric" }, timeZone).format(date);
61
91
  }
62
92
  return `${MONTHS_SHORT[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`;
63
93
  }
64
94
 
65
95
  /** `{ month: "short", day: "numeric" }` — "Jan 5". */
66
- export function formatMonthDay(date: Date, locale = "en-US"): string {
96
+ export function formatMonthDay(date: Date, locale = "en-US", timeZone?: string): string {
67
97
  if (hasIntl()) {
68
- return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric" }).format(date);
98
+ return dateFormat(locale, { month: "short", day: "numeric" }, timeZone).format(date);
69
99
  }
70
100
  return `${MONTHS_SHORT[date.getMonth()]} ${date.getDate()}`;
71
101
  }
72
102
 
73
103
  /** `{ month: "long", day: "numeric" }` — "January 5". */
74
- export function formatMonthDayLong(date: Date, locale = "en-US"): string {
104
+ export function formatMonthDayLong(date: Date, locale = "en-US", timeZone?: string): string {
75
105
  if (hasIntl()) {
76
- return new Intl.DateTimeFormat(locale, { month: "long", day: "numeric" }).format(date);
106
+ return dateFormat(locale, { month: "long", day: "numeric" }, timeZone).format(date);
77
107
  }
78
108
  return `${MONTHS_LONG[date.getMonth()]} ${date.getDate()}`;
79
109
  }
80
110
 
81
111
  /** `{ weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }`
82
112
  * — "Mon, Jan 5, 3:07 PM". */
83
- export function formatWeekdayDateTime(date: Date, locale = "en-US"): string {
113
+ export function formatWeekdayDateTime(date: Date, locale = "en-US", timeZone?: string): string {
84
114
  if (hasIntl()) {
85
- return new Intl.DateTimeFormat(locale, {
115
+ return dateFormat(locale, {
86
116
  weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit",
87
- }).format(date);
117
+ }, timeZone).format(date);
88
118
  }
89
119
  return `${WEEKDAYS_SHORT[date.getDay()]}, ${MONTHS_SHORT[date.getMonth()]} ${date.getDate()}, ${clock12(date)}`;
90
120
  }
91
121
 
92
122
  /** `{ month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }` — "Jan 5, 3:07 PM". */
93
- export function formatMonthDayTime(date: Date, locale = "en-US"): string {
123
+ export function formatMonthDayTime(date: Date, locale = "en-US", timeZone?: string): string {
94
124
  if (hasIntl()) {
95
- return new Intl.DateTimeFormat(locale, {
125
+ return dateFormat(locale, {
96
126
  month: "short", day: "numeric", hour: "numeric", minute: "2-digit",
97
- }).format(date);
127
+ }, timeZone).format(date);
98
128
  }
99
129
  return `${MONTHS_SHORT[date.getMonth()]} ${date.getDate()}, ${clock12(date)}`;
100
130
  }
@@ -380,9 +410,9 @@ export function normalizeFormattedPrice(formatted: string): string {
380
410
  return `${m[1]}${m[3]}${m[2]}`;
381
411
  }
382
412
 
383
- function clockParts(d: Date, locale: string): { time: string; meridiem: string } {
413
+ function clockParts(d: Date, locale: string, timeZone?: string): { time: string; meridiem: string } {
384
414
  const full = hasIntl()
385
- ? new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", hour12: true }).format(d)
415
+ ? dateFormat(locale, { hour: "numeric", minute: "2-digit", hour12: true }, timeZone).format(d)
386
416
  : clock12(d);
387
417
  // ICU puts a narrow no-break space before the meridiem; normalize so callers can split on it.
388
418
  const clean = full.replace(/[  ]/g, " ");
@@ -391,8 +421,8 @@ function clockParts(d: Date, locale: string): { time: string; meridiem: string }
391
421
  }
392
422
 
393
423
  /** "8:00 PM" — the one clock every customer screen uses (12-hour, en-US shape). */
394
- export function formatClock(date: Date, locale = "en-US"): string {
395
- const { time, meridiem } = clockParts(date, locale);
424
+ export function formatClock(date: Date, locale = "en-US", timeZone?: string): string {
425
+ const { time, meridiem } = clockParts(date, locale, timeZone);
396
426
  return meridiem ? `${time} ${meridiem}` : time;
397
427
  }
398
428
 
@@ -403,3 +433,14 @@ export function isPastDate(iso: string | undefined | null, now: number = Date.no
403
433
  const t = Date.parse(iso);
404
434
  return !Number.isNaN(t) && t < now;
405
435
  }
436
+
437
+ /** The calendar day `date` falls on in `timeZone` (device zone when absent, unknown, or when
438
+ * the engine has no Intl), as `YYYY-MM-DD`. For "is this today / this year" comparisons. */
439
+ export function calendarDay(date: Date, timeZone?: string): string {
440
+ if (hasIntl()) {
441
+ const parts = dateFormat("en-CA", { year: "numeric", month: "2-digit", day: "2-digit" }, timeZone).formatToParts(date);
442
+ const get = (type: string) => parts.find((p) => p.type === type)?.value ?? "";
443
+ return `${get("year")}-${get("month")}-${get("day")}`;
444
+ }
445
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
446
+ }
@@ -0,0 +1,24 @@
1
+ // Shared display-text helpers for himi cards: trim catalog artifacts from titles and
2
+ // derive up-to-two-letter placeholder initials so same-first-letter lists still vary.
3
+
4
+ /** Trim trailing artifact markers (stray asterisks / footnote daggers) and whitespace
5
+ * that leak from test/seed catalogs, without touching interior punctuation. Falls back
6
+ * to the original when trimming would empty the string. */
7
+ export function cleanTitle(value?: string): string {
8
+ const raw = (value ?? "").trim();
9
+ const cleaned = raw.replace(/[\s*†‡]+$/u, "").trim();
10
+ return cleaned || raw;
11
+ }
12
+
13
+ /** Up to two significant initials for a placeholder avatar. Two-plus words → first
14
+ * letter of the first two; a single word → its first two alphanumerics. Keeps a list
15
+ * of similar names (e.g. many "Booking…") from collapsing to one identical glyph. */
16
+ export function initials(value?: string): string {
17
+ const words = (value ?? "").trim().split(/\s+/).filter(Boolean);
18
+ if (words.length === 0) return "#";
19
+ if (words.length === 1) {
20
+ const alnum = words[0].replace(/[^\p{L}\p{N}]/gu, "");
21
+ return (alnum.slice(0, 2) || "#").toUpperCase();
22
+ }
23
+ return ((words[0][0] ?? "") + (words[1][0] ?? "")).toUpperCase();
24
+ }
@@ -3,6 +3,9 @@
3
3
 
4
4
  export interface AccountState {
5
5
  status: "loading" | "ready";
6
+ /** The business the "Business" row names: the one picked on Sites, else the
7
+ * build-time fallback (see `owner-selected-site.ts`). */
8
+ businessName: string;
6
9
  /** "Name · email", the email alone, or "" (the line collapses). */
7
10
  ownerLine: string;
8
11
  /** "Version 1.2.0 (34)", or "" when the host cannot say. */
@@ -36,6 +39,7 @@ export function shapeAccount(s: Partial<AccountState>): AccountState {
36
39
  const supportUrl = safeUrl(s.supportUrl), privacyUrl = safeUrl(s.privacyUrl), termsUrl = safeUrl(s.termsUrl);
37
40
  return {
38
41
  status: s.status ?? "loading",
42
+ businessName: s.businessName ?? "",
39
43
  ownerLine: s.ownerLine ?? "",
40
44
  versionLine: s.versionLine ?? "",
41
45
  error: s.error ?? "",
@@ -13,9 +13,11 @@ 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';
15
15
  import { wixMediaUrl } from '@himalaya/wix-media';
16
+ import { getSiteTimeZone } from '@himalaya/wix-analytics';
16
17
  import { type OwnerModuleId } from './owner-modules.js';
17
18
  import { type CollectionPage } from './collection-query.js';
18
- import { copyParam } from './himi-copy.js';
19
+ import { copyParam, copyTemplate } from './himi-copy.js';
20
+ import { calendarDay, formatDateMedium, formatMonthDay, formatWeekdayDateTime } from './engine-intl.js';
19
21
  import { eligibleOwnerOperations } from './owner-operations.js';
20
22
 
21
23
  export interface OwnerRecord {
@@ -33,91 +35,144 @@ const join = (...values: string[]) => values.filter(Boolean).join(' · ');
33
35
  const name = (value: unknown, first: string, last: string) => [field(value, first), field(value, last)].filter(Boolean).join(' ');
34
36
  const yesNo = (value: unknown) => typeof value === 'boolean' ? value ? 'Yes' : 'No' : '';
35
37
  const LIMIT = 50;
38
+
39
+ /** The site's IANA zone, resolved once per worker. Wix hands every date here as a UTC instant;
40
+ * formatting it in the viewer's device zone (or leaving it raw) shows an owner outside UTC the
41
+ * wrong hour and sometimes the wrong day. A failed or unset lookup degrades to the
42
+ * device zone — a zone is garnish on a date, never a reason to fail the collection. The
43
+ * answer, failure included, is kept for the worker's life: a role that cannot read site
44
+ * properties would otherwise re-send the failing request for every page a search walks. */
45
+ let zoneLookup: Promise<string | undefined> | undefined;
46
+ function siteZone(): Promise<string | undefined> {
47
+ // `Promise.resolve().then` so a bridge that throws synchronously is caught here too.
48
+ zoneLookup ??= Promise.resolve().then(getSiteTimeZone).catch(() => undefined);
49
+ return zoneLookup;
50
+ }
51
+
52
+ /** A Wix date value as an instant. A calendar date with no time ("2026-11-07") is a DAY, not an
53
+ * instant: it is built at local midnight and formatted with no zone, so it never shifts across
54
+ * one — including on engines without Intl, where a zone could not be applied anyway. */
55
+ function ownerInstant(value: string): { date: Date; dayOnly: boolean } | undefined {
56
+ if (!value) return undefined;
57
+ const day = /^(\d{4})-(\d\d)-(\d\d)$/.exec(value);
58
+ if (day) return { date: new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3])), dayOnly: true };
59
+ const ms = /^\d{13}$/.test(value) ? Number(value) : Date.parse(value);
60
+ return Number.isFinite(ms) ? { date: new Date(ms), dayOnly: false } : undefined;
61
+ }
62
+ /** Has the instant (or, for a calendar date, the whole day) passed? */
63
+ function ownerPast(value: string, now = Date.now()): boolean {
64
+ const parsed = ownerInstant(value);
65
+ if (!parsed) return false;
66
+ const { date, dayOnly } = parsed;
67
+ return (dayOnly ? new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1).getTime() : date.getTime()) <= now;
68
+ }
69
+
70
+ type DateStyle = 'date' | 'dateTime' | 'short';
71
+ /** A Wix date as owner copy, through the shared engine-intl shapes: `date` "Jan 5, 2026",
72
+ * `dateTime` "Mon, Jan 5, 3:07 PM", `short` "Jan 5" (with the year when it is not this year).
73
+ * A value that is not a date at all is kept as Wix sent it. */
74
+ function ownerDate(value: string, zone: string | undefined, style: DateStyle = 'date'): string {
75
+ if (!value) return '';
76
+ const parsed = ownerInstant(value);
77
+ if (!parsed) return value;
78
+ const { date } = parsed, tz = parsed.dayOnly ? undefined : zone;
79
+ if (style === 'dateTime') return formatWeekdayDateTime(date, undefined, tz);
80
+ if (style === 'short' && calendarDay(date, tz).slice(0, 4) === calendarDay(new Date(), tz).slice(0, 4)) return formatMonthDay(date, undefined, tz);
81
+ return formatDateMedium(date, undefined, tz);
82
+ }
36
83
  function offsetOf(cursor: string): number {
37
84
  if (cursor && !/^\d{1,9}$/.test(cursor)) throw new Error('Invalid collection cursor. Refresh this view.');
38
85
  return Number(cursor || 0);
39
86
  }
40
- function page(module: OwnerModuleId, rows: unknown[], nextCursor: string, total?: number, complete?: boolean): CollectionPage<OwnerRecord> {
41
- return { items: rows.map(row => ownerRecord(module, row)), nextCursor, total, ...(complete === undefined ? {} : { complete }) };
87
+ function page(rows: unknown[], nextCursor: string, total?: number, complete?: boolean): CollectionPage<unknown> {
88
+ return { items: rows, nextCursor, total, ...(complete === undefined ? {} : { complete }) };
42
89
  }
43
- function offsetPage(module: OwnerModuleId, rows: unknown[], offset: number, metadata: unknown) {
90
+ function offsetPage(rows: unknown[], offset: number, metadata: unknown) {
44
91
  const total = at(metadata, 'total');
45
92
  const more = rows.length > 0 && (typeof total === 'number' ? offset + rows.length < total : rows.length === LIMIT);
46
- return page(module, rows, more ? String(offset + rows.length) : '', typeof total === 'number' ? total : undefined);
93
+ return page(rows, more ? String(offset + rows.length) : '', typeof total === 'number' ? total : undefined);
47
94
  }
48
95
 
49
96
  /** Domain query shapes stay here, shared by list and detail workers. Unsupported
50
97
  * server name queries remain explicitly labelled searches of loaded records. */
51
98
  export async function queryOwnerModule(module: OwnerModuleId, cursor = ''): Promise<CollectionPage<OwnerRecord>> {
99
+ // The zone lookup runs beside the query, not after it: the two are independent, and serial
100
+ // they would double the time to a first paint.
101
+ const [raw, zone] = await Promise.all([queryRawOwnerModule(module, cursor), siteZone()]);
102
+ return { ...raw, items: raw.items.map(row => ownerRecord(module, row, zone)) };
103
+ }
104
+
105
+ async function queryRawOwnerModule(module: OwnerModuleId, cursor: string): Promise<CollectionPage<unknown>> {
52
106
  const cursorPaging = { limit: LIMIT, ...(cursor ? { cursor } : {}) };
53
107
  switch (module) {
54
108
  case 'invoices': {
55
109
  const offset = offsetOf(cursor), r = await queryInvoices({ query: { paging: { limit: LIMIT, offset } } });
56
- return offsetPage(module, r.invoices ?? [], offset, r.pagingMetadata);
110
+ return offsetPage(r.invoices ?? [], offset, r.pagingMetadata);
57
111
  }
58
112
  case 'coupons': {
59
113
  const offset = offsetOf(cursor), r = await queryCoupons({ paging: { limit: LIMIT, offset } });
60
- return offsetPage(module, r.coupons ?? [], offset, r.metadata);
114
+ return offsetPage(r.coupons ?? [], offset, r.metadata);
61
115
  }
62
116
  case 'giftCards': {
63
117
  const r = await queryGiftCards({ query: { cursorPaging, sort: [{ fieldName: 'createdDate', order: 'DESC' }] } });
64
- return page(module, r.giftCards ?? [], r.pagingMetadata?.cursors?.next ?? '');
118
+ return page(r.giftCards ?? [], r.pagingMetadata?.cursors?.next ?? '');
65
119
  }
66
120
  case 'contacts': {
67
121
  const offset = offsetOf(cursor), r = await queryContacts({ query: { paging: { limit: LIMIT, offset } } });
68
- return offsetPage(module, r.contacts ?? [], offset, r.pagingMetadata);
122
+ return offsetPage(r.contacts ?? [], offset, r.pagingMetadata);
69
123
  }
70
124
  case 'members': {
71
125
  const offset = offsetOf(cursor), r = await queryMembers({ query: { paging: { limit: LIMIT, offset } } });
72
- return offsetPage(module, r.members ?? [], offset, r.metadata);
126
+ return offsetPage(r.members ?? [], offset, r.metadata);
73
127
  }
74
128
  case 'groups': {
75
129
  const offset = offsetOf(cursor), r = await queryGroups({ paging: { limit: LIMIT, offset } });
76
- return offsetPage(module, r.groups ?? [], offset, r.metaData);
130
+ return offsetPage(r.groups ?? [], offset, r.metaData);
77
131
  }
78
132
  case 'reviews': {
79
133
  const r = await queryReviews({ cursorPaging, sort: [{ fieldName: 'createdDate', order: 'DESC' }] }, true);
80
- return page(module, r.reviews ?? r.results ?? [], r.pagingMetadata?.cursors?.next ?? '');
134
+ return page(r.reviews ?? r.results ?? [], r.pagingMetadata?.cursors?.next ?? '');
81
135
  }
82
136
  case 'programs': {
83
137
  const r = await queryPrograms({ cursorPaging });
84
- return page(module, r.programs ?? [], r.pagingMetadata?.cursors?.next ?? '');
138
+ return page(r.programs ?? [], r.pagingMetadata?.cursors?.next ?? '');
85
139
  }
86
140
  case 'badges': {
87
141
  const r = await queryBadges({ query: { cursorPaging } });
88
- return page(module, r.badges ?? [], r.pagingMetadata?.cursors?.next ?? '');
142
+ return page(r.badges ?? [], r.pagingMetadata?.cursors?.next ?? '');
89
143
  }
90
144
  case 'reservations': {
91
145
  const r = await queryReservations({ cursorPaging, sort: [{ fieldName: 'details.startDate', order: 'ASC' }] });
92
- return page(module, r.reservations ?? [], r.pagingMetadata?.cursors?.next ?? '');
146
+ return page(r.reservations ?? [], r.pagingMetadata?.cursors?.next ?? '');
93
147
  }
94
148
  case 'portfolio': {
95
149
  const r = await listProjects(LIMIT, cursor || undefined);
96
- return page(module, r.projects ?? [], r.pagingMetadata?.cursors?.next ?? '');
150
+ return page(r.projects ?? [], r.pagingMetadata?.cursors?.next ?? '');
97
151
  }
98
152
  case 'donationCampaigns': {
99
153
  const r = await queryDonationCampaigns({ query: { cursorPaging, sort: [{ fieldName: 'updatedDate', order: 'DESC' }] } });
100
- return page(module, r.donationCampaigns ?? [], r.pagingMetadata?.cursors?.next ?? '');
154
+ return page(r.donationCampaigns ?? [], r.pagingMetadata?.cursors?.next ?? '');
101
155
  }
102
156
  case 'forms': {
103
157
  // This collection is Wix Forms. Booking/event registration forms belong
104
158
  // to their respective operational records, rather than a mixed union
105
159
  // that silently drops namespaces when one permission check fails.
106
160
  const offset = offsetOf(cursor), r = await queryForms({ filter: { namespace: 'wix.form_app.form' }, paging: { limit: LIMIT, offset } });
107
- return offsetPage(module, r.forms ?? [], offset, r.metaData);
161
+ return offsetPage(r.forms ?? [], offset, r.metaData);
108
162
  }
109
163
  case 'plans': {
110
164
  const r = await listPlans();
111
- return page(module, r.plans ?? [], '', r.pagingMetadata?.total, false);
165
+ return page(r.plans ?? [], '', r.pagingMetadata?.total, false);
112
166
  }
113
167
  case 'subscriptions': {
114
168
  const r = await listPlanOrders();
115
- return page(module, r.orders ?? [], '', r.pagingMetadata?.total, false);
169
+ return page(r.orders ?? [], '', r.pagingMetadata?.total, false);
116
170
  }
117
171
  }
118
172
  }
119
173
 
120
- export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
174
+ export function ownerRecord(module: OwnerModuleId, raw: unknown, zone?: string): OwnerRecord {
175
+ const when = (path: string, style?: DateStyle) => ownerDate(field(raw, path), zone, style);
121
176
  const id = field(raw, 'id');
122
177
  if (!id) throw new Error('Wix returned a record without an identity. Refresh this view.');
123
178
  let title = '', subtitle = '', image = '', statusLabel = '';
@@ -127,7 +182,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
127
182
  title = field(raw, 'specification.name') || field(raw, 'displayData.name');
128
183
  subtitle = field(raw, 'specification.code');
129
184
  statusLabel = at(raw, 'expired') === true ? 'Expired' : at(raw, 'specification.active') === true ? 'Active' : 'Inactive';
130
- details = [['Code', subtitle], ['Uses', field(raw, 'numberOfUsages')], ['Offer', words(field(raw, 'specification.type'))], ['Expires', field(raw, 'specification.expirationTime')]];
185
+ details = [['Code', subtitle], ['Uses', field(raw, 'numberOfUsages')], ['Offer', words(field(raw, 'specification.type'))], ['Expires', when('specification.expirationTime')]];
131
186
  break;
132
187
  case 'giftCards': {
133
188
  const suffix = field(raw, 'codeSuffix');
@@ -147,7 +202,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
147
202
  ['Initial value', field(raw, 'initialValue.formattedAmount') || join(field(raw, 'initialValue.amount'), field(raw, 'currency'))],
148
203
  ['Recipient', recipient],
149
204
  ['Order number', field(raw, 'orderInfo.orderNumber')],
150
- ['Expires', expires],
205
+ ['Expires', when('expirationDate')],
151
206
  ['Source', words(field(raw, 'source'))],
152
207
  ];
153
208
  break;
@@ -156,7 +211,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
156
211
  title = field(raw, 'number') ? `#${field(raw, 'number')}` : field(raw, 'title');
157
212
  subtitle = field(raw, 'customerInfo.contactDetails.fullName') || name(raw, 'customerInfo.contactDetails.firstName', 'customerInfo.contactDetails.lastName') || field(raw, 'customerInfo.contactDetails.email');
158
213
  statusLabel = words(field(raw, 'status'));
159
- details = [['Customer', subtitle], ['Total', join(field(raw, 'totals.total'), field(raw, 'currency'))], ['Due', field(raw, 'dueDate')]];
214
+ details = [['Customer', subtitle], ['Total', join(field(raw, 'totals.total'), field(raw, 'currency'))], ['Due', when('dueDate')]];
160
215
  break;
161
216
  case 'contacts':
162
217
  title = name(raw, 'info.name.first', 'info.name.last') || field(raw, 'primaryInfo.email');
@@ -167,12 +222,12 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
167
222
  case 'members':
168
223
  title = field(raw, 'profile.nickname') || name(raw, 'contact.firstName', 'contact.lastName') || field(raw, 'loginEmail');
169
224
  subtitle = field(raw, 'loginEmail'); image = field(raw, 'profile.photo.url'); statusLabel = words(field(raw, 'status'));
170
- details = [['Email', subtitle], ['Last login', field(raw, 'lastLoginDate')]];
225
+ details = [['Email', subtitle], ['Last login', when('lastLoginDate')]];
171
226
  break;
172
227
  case 'groups':
173
228
  title = field(raw, 'name'); subtitle = field(raw, 'description') || field(raw, 'teaser');
174
229
  image = field(raw, 'coverImage.imageInfo.url'); statusLabel = words(field(raw, 'privacyStatus'));
175
- details = [['Members', field(raw, 'memberCount') || field(raw, 'membersCount')], ['Last activity', field(raw, 'lastActivityDate')]];
230
+ details = [['Members', field(raw, 'memberCount') || field(raw, 'membersCount')], ['Last activity', when('lastActivityDate')]];
176
231
  break;
177
232
  case 'reviews':
178
233
  title = field(raw, 'content.title'); subtitle = field(raw, 'content.body'); statusLabel = words(field(raw, 'moderation.moderationStatus'));
@@ -184,8 +239,14 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
184
239
  details = [['Price', join(field(raw, 'pricing.price.value'), field(raw, 'pricing.price.currency'))], ['Free trial days', field(raw, 'pricing.freeTrialDays')]];
185
240
  break;
186
241
  case 'subscriptions':
187
- title = field(raw, 'planName'); subtitle = field(raw, 'buyer.memberId'); statusLabel = words(field(raw, 'status'));
188
- details = [['Member ID', subtitle], ['Started', field(raw, 'startDate')], ['Ends', field(raw, 'endDate')], ['Payment', words(field(raw, 'lastPaymentStatus'))]];
242
+ // The buyer is only a member UUID here — not something an owner can read — so the
243
+ // row says when the plan started instead of echoing an internal id.
244
+ title = field(raw, 'planName'); statusLabel = words(field(raw, 'status'));
245
+ {
246
+ const started = when('startDate', 'short');
247
+ subtitle = started ? copyTemplate('ownerModules.startedOn', 'Started {date}', { date: started }) : '';
248
+ }
249
+ details = [['Started', when('startDate')], ['Ends', when('endDate')], ['Payment', words(field(raw, 'lastPaymentStatus'))]];
189
250
  break;
190
251
  case 'programs':
191
252
  title = field(raw, 'description.title') || field(raw, 'details.title') || field(raw, 'name');
@@ -203,13 +264,14 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
203
264
  break;
204
265
  }
205
266
  case 'reservations':
206
- title = name(raw, 'reservee.firstName', 'reservee.lastName'); subtitle = field(raw, 'details.startDate'); statusLabel = words(field(raw, 'status'));
267
+ title = name(raw, 'reservee.firstName', 'reservee.lastName'); subtitle = when('details.startDate', 'dateTime'); statusLabel = words(field(raw, 'status'));
207
268
  details = [['Arrival', subtitle], ['Party size', field(raw, 'details.partySize')], ['Email', field(raw, 'reservee.email')], ['Phone', field(raw, 'reservee.phone')]];
208
269
  break;
209
270
  case 'forms':
210
- title = field(raw, 'name'); subtitle = field(raw, 'namespace');
271
+ // The list is filtered to one namespace (`wix.form_app.form`), so the namespace is an
272
+ // internal constant, not information — it never reaches copy.
273
+ title = field(raw, 'name') || copyParam('ownerModules.untitledForm', 'Untitled form');
211
274
  statusLabel = typeof at(raw, 'enabled') === 'boolean' ? at(raw, 'enabled') ? 'Enabled' : 'Disabled' : '';
212
- details = [['Namespace', subtitle]];
213
275
  break;
214
276
  case 'portfolio':
215
277
  title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
@@ -217,20 +279,25 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
217
279
  break;
218
280
  case 'donationCampaigns':
219
281
  title = field(raw, 'name');
220
- subtitle = join(field(raw, 'campaignGoal.targetAmount.formattedAmount'), field(raw, 'campaignGoal.endDate'));
282
+ {
283
+ const end = when('campaignGoal.endDate', 'short');
284
+ subtitle = join(field(raw, 'campaignGoal.targetAmount.formattedAmount'), !end ? ''
285
+ : ownerPast(field(raw, 'campaignGoal.endDate')) ? copyTemplate('ownerModules.endedOn', 'ended {date}', { date: end })
286
+ : copyTemplate('ownerModules.endsOn', 'ends {date}', { date: end }));
287
+ }
221
288
  image = field(raw, 'coverImage');
222
289
  statusLabel = at(raw, 'archived') === true ? 'Archived' : words(field(raw, 'status'));
223
290
  details = [
224
291
  ['Goal', field(raw, 'campaignGoal.targetAmount.formattedAmount') || field(raw, 'campaignGoal.targetAmount.amount')],
225
- ['End date', field(raw, 'campaignGoal.endDate')],
292
+ ['End date', when('campaignGoal.endDate')],
226
293
  ['Frequencies', Array.isArray(at(raw, 'donationFrequencies')) ? (at(raw, 'donationFrequencies') as unknown[]).map(String).join(', ') : ''],
227
294
  ['Custom amount', yesNo(at(raw, 'customAmountEnabled'))],
228
295
  ];
229
296
  break;
230
297
  }
231
298
  details.push(
232
- ['Created', field(raw, 'createdDate') || field(raw, 'dateCreated') || field(raw, 'createDate')],
233
- ['Updated', field(raw, 'updatedDate') || field(raw, 'updateDate')],
299
+ ['Created', when('createdDate') || when('dateCreated') || when('createDate')],
300
+ ['Updated', when('updatedDate') || when('updateDate')],
234
301
  );
235
302
  return { id, moduleId: module, title: title || copyParam('ownerModules.untitled', 'Untitled record'), subtitle, image: wixMediaUrl(image), statusLabel,
236
303
  detailScreenId: `himi_owner_lite_record_${module}`,
@@ -240,6 +307,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
240
307
 
241
308
  export async function getOwnerRecord(module: OwnerModuleId, id: string): Promise<OwnerRecord> {
242
309
  if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(id)) throw new Error('This record link is invalid. Return to the collection.');
310
+ const zone = siteZone(); // started now, beside the record fetch; it never rejects
243
311
  let raw: unknown;
244
312
  switch (module) {
245
313
  case 'coupons': raw = (await getCoupon(id)).coupon; break;
@@ -270,13 +338,13 @@ export async function getOwnerRecord(module: OwnerModuleId, id: string): Promise
270
338
  seen.add(next); cursor = next;
271
339
  }
272
340
  if (!raw) throw new Error('This project could not be located. Open the portfolio in Wix to check its availability.');
273
- const record = ownerRecord(module, raw);
341
+ const record = ownerRecord(module, raw, await zone);
274
342
  const media = await listProjectItems(id, 100);
275
343
  return { ...record, fields: [...record.fields, { id: 'media', label: 'Loaded media items', value: String(media.items?.length ?? 0) }] };
276
344
  }
277
345
  case 'donationCampaigns': raw = (await getDonationCampaign(id)).donationCampaign; break;
278
346
  }
279
- const record = ownerRecord(module, raw);
347
+ const record = ownerRecord(module, raw, await zone);
280
348
  if (record.id !== id) throw new Error('The returned record does not match this link. Refresh the collection.');
281
349
  return record;
282
350
  }
@@ -0,0 +1,103 @@
1
+ // The business the owner is managing RIGHT NOW, shared by every __HIMI_APP_NAME__ worker.
2
+ //
3
+ // Home, Manage, Account and the drawer used to name the business from the build-time
4
+ // `provisioning.json` (`identity.businessName`), so after picking another site on the
5
+ // Sites list the chrome still named the provisioning one — three different names for
6
+ // "the current business" in one session, none of them the site actually picked.
7
+ //
8
+ // The picker (`owner-site-pick.ts`) is the one place that knows both the id and the
9
+ // display name of the business being switched to, so it records them here; every
10
+ // other worker reads them back. The channel is `himalaya.storage` — the app-scoped KV
11
+ // every worker of the app shares on every host (UserDefaults / SharedPreferences / one
12
+ // localStorage namespace) and that needs no capability.
13
+ //
14
+ // ONE key, not one per owner: the host only tells a worker who the owner is once a
15
+ // business is scoped, so the picker (which runs before that) and the screens after it
16
+ // would otherwise land on different keys. The owner is recorded INSIDE the entry
17
+ // instead, and an entry written for a different owner is never shown.
18
+ //
19
+ // The build-time name is only ever the cold-start fallback, passed in by the
20
+ // descriptor: a worker never hardcodes it.
21
+
22
+ import { initials } from "./himi-text.js";
23
+
24
+ type StorageLike = { getItem(key: string): string | null; setItem(key: string, value: string): void };
25
+ type Host = typeof globalThis & { himalaya?: { storage?: StorageLike; input?: Record<string, unknown> } };
26
+
27
+ export interface SelectedSite {
28
+ id: string;
29
+ name: string;
30
+ }
31
+
32
+ const KEY = "himi.ownerLite.selectedSite.v1";
33
+
34
+ function host(): Host["himalaya"] {
35
+ return (globalThis as Host).himalaya;
36
+ }
37
+
38
+ function inputString(key: string): string {
39
+ const v = host()?.input?.[key];
40
+ return typeof v === "string" && v !== "default" ? v.trim() : "";
41
+ }
42
+
43
+ /** The business the owner last picked, or null when none has been picked, the host
44
+ * has no storage, or the entry does not apply to this worker. Never throws. */
45
+ export function readSelectedSite(): SelectedSite | null {
46
+ try {
47
+ const raw = host()?.storage?.getItem(KEY);
48
+ if (!raw) return null;
49
+ const parsed = JSON.parse(raw) as Partial<SelectedSite> & { account?: unknown };
50
+ const id = typeof parsed?.id === "string" ? parsed.id.trim() : "";
51
+ const name = typeof parsed?.name === "string" ? parsed.name.trim() : "";
52
+ if (!id || !name) return null;
53
+ // Another owner's pick on a shared device is never this owner's business. Fail
54
+ // CLOSED: an owner-tagged entry is shown only to a worker that knows it IS that
55
+ // owner. A worker that cannot tell who the owner is (the account-level flow before
56
+ // a business is scoped, or a host that cannot supply the identity) gets the
57
+ // fallback, never a name that may be another owner's.
58
+ const account = typeof parsed.account === "string" ? parsed.account : "";
59
+ const me = inputString("__himiPreferenceAccount");
60
+ if (account && account !== me) return null;
61
+ // An entry the picker wrote before the host knew the owner is CLAIMED by the first
62
+ // worker that does know it, so from then on no other owner on the device reads it.
63
+ if (!account && me) host()?.storage?.setItem(KEY, JSON.stringify({ id, name, account: me }));
64
+ // A screen opened FOR a specific business (a deep link or push carrying
65
+ // `business`) names that business's data; a name recorded for another one would
66
+ // sit over the wrong numbers, so it falls back instead.
67
+ const opened = inputString("business");
68
+ if (opened && opened !== id) return null;
69
+ return { id, name };
70
+ } catch {
71
+ return null; // a corrupt entry reads as "nothing picked", never as a crash
72
+ }
73
+ }
74
+
75
+ /** Record the business the owner just picked. Best-effort: a host without storage
76
+ * simply keeps showing the fallback. */
77
+ export function rememberSelectedSite(site: SelectedSite): void {
78
+ const id = site.id.trim();
79
+ const name = site.name.trim();
80
+ if (!id || !name) return;
81
+ const account = inputString("__himiPreferenceAccount");
82
+ try {
83
+ host()?.storage?.setItem(KEY, JSON.stringify(account ? { id, name, account } : { id, name }));
84
+ } catch {
85
+ // Quota / storage disabled: the chrome falls back to the build-time name.
86
+ }
87
+ }
88
+
89
+ /** Display name for "the current business": the picked one, else `fallback`. */
90
+ export function selectedSiteName(fallback: string): string {
91
+ return readSelectedSite()?.name || fallback;
92
+ }
93
+
94
+ /** Badge letters for a business name ("Riverbend Market" → "RM"); Unicode-aware. */
95
+ export function siteInitials(name: string): string {
96
+ return initials(name);
97
+ }
98
+
99
+ /** The fallback name a descriptor passes in its `onAppear` params (`businessName`). */
100
+ export function fallbackNameFrom(arg: unknown, current = ""): string {
101
+ const v = (arg as { businessName?: unknown } | undefined)?.businessName;
102
+ return typeof v === "string" && v.trim() ? v.trim() : current;
103
+ }