@wix/himalaya-cli 0.1040.0 → 0.1042.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 (158) hide show
  1. package/dist/cli.mjs +33 -17
  2. package/dist/preview-assets.generated.bin +0 -0
  3. package/package.json +2 -2
  4. package/templates/beauty-salon/dev/index.ts +42 -42
  5. package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +2 -0
  6. package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +6 -1
  7. package/templates/beauty-salon-owner/config.ts +2 -2
  8. package/templates/beauty-salon-owner/dev/index.ts +287 -260
  9. package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +2 -0
  10. package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +6 -1
  11. package/templates/beauty-salon-owner/tier5-src/_shared/owner-account.ts +45 -0
  12. package/templates/beauty-salon-owner/tier5-src/_shared/owner-preview-demo.ts +21 -0
  13. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-account/index.ts +68 -0
  14. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-ask/index.ts +4 -3
  15. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post-form/index.ts +10 -4
  16. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +29 -13
  17. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service-form/index.ts +9 -3
  18. package/templates/branded-lite/dev/index.ts +175 -175
  19. package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +2 -0
  20. package/templates/branded-lite/tier5-src/_shared/collection-service.ts +6 -1
  21. package/templates/community-club/dev/index.ts +28 -28
  22. package/templates/community-club/tier5-src/_shared/collection-contract.ts +2 -0
  23. package/templates/community-club/tier5-src/_shared/collection-service.ts +6 -1
  24. package/templates/community-club-owner/config.ts +2 -2
  25. package/templates/community-club-owner/dev/index.ts +287 -260
  26. package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +2 -0
  27. package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +6 -1
  28. package/templates/community-club-owner/tier5-src/_shared/owner-account.ts +45 -0
  29. package/templates/community-club-owner/tier5-src/_shared/owner-preview-demo.ts +21 -0
  30. package/templates/community-club-owner/tier5-src/himi-owner-lite-account/index.ts +68 -0
  31. package/templates/community-club-owner/tier5-src/himi-owner-lite-ask/index.ts +4 -3
  32. package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +9 -3
  33. package/templates/community-club-owner/tier5-src/himi-owner-lite-post-form/index.ts +10 -4
  34. package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +29 -13
  35. package/templates/events-meetups/dev/index.ts +21 -21
  36. package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +2 -0
  37. package/templates/events-meetups/tier5-src/_shared/collection-service.ts +6 -1
  38. package/templates/events-meetups-owner/config.ts +2 -2
  39. package/templates/events-meetups-owner/dev/index.ts +259 -208
  40. package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +2 -0
  41. package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +6 -1
  42. package/templates/events-meetups-owner/tier5-src/_shared/owner-account.ts +45 -0
  43. package/templates/events-meetups-owner/tier5-src/_shared/owner-preview-demo.ts +21 -0
  44. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-account/index.ts +68 -0
  45. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-ask/index.ts +4 -3
  46. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +9 -3
  47. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post-form/index.ts +10 -4
  48. package/templates/gym-fitness/dev/index.ts +49 -49
  49. package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +2 -0
  50. package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +6 -1
  51. package/templates/gym-fitness-owner/config.ts +2 -2
  52. package/templates/gym-fitness-owner/dev/index.ts +294 -285
  53. package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +2 -0
  54. package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +6 -1
  55. package/templates/gym-fitness-owner/tier5-src/_shared/owner-account.ts +45 -0
  56. package/templates/gym-fitness-owner/tier5-src/_shared/owner-preview-demo.ts +21 -0
  57. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-account/index.ts +68 -0
  58. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-ask/index.ts +4 -3
  59. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +9 -3
  60. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post-form/index.ts +10 -4
  61. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +29 -13
  62. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service-form/index.ts +9 -3
  63. package/templates/nonprofit-cause/dev/index.ts +28 -28
  64. package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +2 -0
  65. package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +6 -1
  66. package/templates/nonprofit-cause-owner/config.ts +2 -2
  67. package/templates/nonprofit-cause-owner/dev/index.ts +266 -233
  68. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +2 -0
  69. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +6 -1
  70. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-account.ts +45 -0
  71. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-preview-demo.ts +21 -0
  72. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-account/index.ts +68 -0
  73. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-ask/index.ts +4 -3
  74. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +9 -3
  75. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post-form/index.ts +10 -4
  76. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service-form/index.ts +9 -3
  77. package/templates/owner-lite/config.ts +2 -2
  78. package/templates/owner-lite/dev/index.ts +295 -287
  79. package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +2 -0
  80. package/templates/owner-lite/tier5-src/_shared/collection-service.ts +6 -1
  81. package/templates/owner-lite/tier5-src/_shared/owner-account.ts +45 -0
  82. package/templates/owner-lite/tier5-src/_shared/owner-preview-demo.ts +21 -0
  83. package/templates/owner-lite/tier5-src/himi-owner-lite-account/index.ts +68 -0
  84. package/templates/owner-lite/tier5-src/himi-owner-lite-ask/index.ts +4 -3
  85. package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +9 -3
  86. package/templates/owner-lite/tier5-src/himi-owner-lite-post-form/index.ts +10 -4
  87. package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +29 -13
  88. package/templates/owner-lite/tier5-src/himi-owner-lite-service-form/index.ts +9 -3
  89. package/templates/podcast-creator/dev/index.ts +28 -28
  90. package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +2 -0
  91. package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +6 -1
  92. package/templates/podcast-creator-owner/config.ts +2 -2
  93. package/templates/podcast-creator-owner/dev/index.ts +287 -260
  94. package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +2 -0
  95. package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +6 -1
  96. package/templates/podcast-creator-owner/tier5-src/_shared/owner-account.ts +45 -0
  97. package/templates/podcast-creator-owner/tier5-src/_shared/owner-preview-demo.ts +21 -0
  98. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-account/index.ts +68 -0
  99. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-ask/index.ts +4 -3
  100. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +9 -3
  101. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post-form/index.ts +10 -4
  102. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +29 -13
  103. package/templates/portfolio-creative/dev/index.ts +35 -35
  104. package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +2 -0
  105. package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +6 -1
  106. package/templates/portfolio-creative-owner/config.ts +2 -2
  107. package/templates/portfolio-creative-owner/dev/index.ts +287 -260
  108. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +2 -0
  109. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +6 -1
  110. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-account.ts +45 -0
  111. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-preview-demo.ts +21 -0
  112. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-account/index.ts +68 -0
  113. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-ask/index.ts +4 -3
  114. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post-form/index.ts +10 -4
  115. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +29 -13
  116. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service-form/index.ts +9 -3
  117. package/templates/restaurant/dev/index.ts +21 -21
  118. package/templates/restaurant/tier5-src/_shared/collection-contract.ts +2 -0
  119. package/templates/restaurant/tier5-src/_shared/collection-service.ts +6 -1
  120. package/templates/restaurant-owner/config.ts +2 -2
  121. package/templates/restaurant-owner/dev/index.ts +294 -285
  122. package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +2 -0
  123. package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +6 -1
  124. package/templates/restaurant-owner/tier5-src/_shared/owner-account.ts +45 -0
  125. package/templates/restaurant-owner/tier5-src/_shared/owner-preview-demo.ts +21 -0
  126. package/templates/restaurant-owner/tier5-src/himi-owner-lite-account/index.ts +68 -0
  127. package/templates/restaurant-owner/tier5-src/himi-owner-lite-ask/index.ts +4 -3
  128. package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +9 -3
  129. package/templates/restaurant-owner/tier5-src/himi-owner-lite-post-form/index.ts +10 -4
  130. package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +29 -13
  131. package/templates/restaurant-owner/tier5-src/himi-owner-lite-service-form/index.ts +9 -3
  132. package/templates/retail-store/dev/index.ts +21 -21
  133. package/templates/retail-store/tier5-src/_shared/collection-contract.ts +2 -0
  134. package/templates/retail-store/tier5-src/_shared/collection-service.ts +6 -1
  135. package/templates/retail-store-owner/config.ts +2 -2
  136. package/templates/retail-store-owner/dev/index.ts +280 -235
  137. package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +2 -0
  138. package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +6 -1
  139. package/templates/retail-store-owner/tier5-src/_shared/owner-account.ts +45 -0
  140. package/templates/retail-store-owner/tier5-src/_shared/owner-preview-demo.ts +21 -0
  141. package/templates/retail-store-owner/tier5-src/himi-owner-lite-account/index.ts +68 -0
  142. package/templates/retail-store-owner/tier5-src/himi-owner-lite-ask/index.ts +4 -3
  143. package/templates/retail-store-owner/tier5-src/himi-owner-lite-post-form/index.ts +10 -4
  144. package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +29 -13
  145. package/templates/yoga-studio/dev/index.ts +42 -42
  146. package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +2 -0
  147. package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +6 -1
  148. package/templates/yoga-studio-owner/config.ts +2 -2
  149. package/templates/yoga-studio-owner/dev/index.ts +266 -233
  150. package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +2 -0
  151. package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +6 -1
  152. package/templates/yoga-studio-owner/tier5-src/_shared/owner-account.ts +45 -0
  153. package/templates/yoga-studio-owner/tier5-src/_shared/owner-preview-demo.ts +21 -0
  154. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-account/index.ts +68 -0
  155. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-ask/index.ts +4 -3
  156. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +9 -3
  157. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post-form/index.ts +10 -4
  158. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service-form/index.ts +9 -3
@@ -168,6 +168,8 @@ export function projectCollection(source: CollectionRow[], config: CollectionCon
168
168
  items: visible.map(row => ({ ...row, collectionId: collectionId(row), collectionTitle: collectionTitle(row, config),
169
169
  collectionMeta: [...new Set((config.metadata ?? ['total', 'statusLabel', 'price', 'priceLine', 'dateLine', 'meta']).filter(key => key !== config.subtitle).map(key => scalar(row[key])).filter(Boolean))].join(' · '),
170
170
  collectionSelectLabel: prefs.selectedIds.includes(collectionId(row)) ? 'Selected' : 'Select',
171
+ // The save toggle names the action it WILL take (the service localizes it).
172
+ collectionSaveLabel: prefs.savedIds.includes(collectionId(row)) ? 'Remove from saved' : 'Save',
171
173
  collectionSubtitle: scalar(row[config.subtitle ?? '']) || scalar(row.subtitle) || scalar(row.description) || scalar(row.meta),
172
174
  collectionImage: scalar(row[config.image ?? '']), collectionNoImage: !scalar(row[config.image ?? '']), collectionHasImage: !!scalar(row[config.image ?? '']),
173
175
  collectionSaved: prefs.savedIds.includes(collectionId(row)), collectionNotSaved: !prefs.savedIds.includes(collectionId(row)), collectionSelected: prefs.selectedIds.includes(collectionId(row)),
@@ -123,7 +123,12 @@ export function decorateCollection(name: string, def: { state: State; actions?:
123
123
  pendingFilter = '';
124
124
  }
125
125
  const projected = projectCollection(rows, config, prefs, !!backend && config.backendQuery === true, known);
126
- projected.items.forEach(row => { row.collectionSelectLabel = row.collectionSelected ? copyParam('collection.selected', 'Selected') : copyParam('collection.select', 'Select'); });
126
+ projected.items.forEach(row => {
127
+ row.collectionSelectLabel = row.collectionSelected ? copyParam('collection.selected', 'Selected') : copyParam('collection.select', 'Select');
128
+ // the save toggle names the action it WILL take for this row. The
129
+ // old static "Save / unsave" made the owner work out the row's state.
130
+ row.collectionSaveLabel = row.collectionSaved ? copyParam('collection.unsave', 'Remove from saved') : copyParam('collection.save', 'Save');
131
+ });
127
132
  if (previewId && !rows.some(row => collectionId(row) === previewId)) previewId = '';
128
133
  const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, { id: 'saved', label: copyParam('collection.saved', 'Saved') },
129
134
  ...projected.filters.map(label => ({ id: `value:${label}`, label: copyParam(`collection.filter.${label}`, label) }))];
@@ -0,0 +1,45 @@
1
+ // Pure seams of the owner-lite Account worker — no @himalaya/* imports, so a
2
+ // plain node test can load them without the bundle build's module aliases.
3
+
4
+ export interface AccountState {
5
+ status: "loading" | "ready";
6
+ /** "Name · email", the email alone, or "" (the line collapses). */
7
+ ownerLine: string;
8
+ /** "Version 1.2.0 (34)", or "" when the host cannot say. */
9
+ versionLine: string;
10
+ /** Why the owner line is empty, when the identity read failed; "" otherwise. */
11
+ error: string;
12
+ supportUrl: string;
13
+ privacyUrl: string;
14
+ termsUrl: string;
15
+ /** hiddenPath flags — TRUE hides, so each is phrased negatively. */
16
+ noSupport: boolean;
17
+ noPrivacy: boolean;
18
+ noTerms: boolean;
19
+ }
20
+
21
+ /** Only absolute http(s) URLs are openable; anything else is treated as absent. */
22
+ export function safeUrl(v: unknown): string {
23
+ if (typeof v !== "string") return "";
24
+ const t = v.trim();
25
+ return /^https?:\/\/[^\s]+$/i.test(t) ? t : "";
26
+ }
27
+
28
+ /** "Name · email" when both are known, whichever one is when only one is, else "". */
29
+ export function ownerLineOf(name: string, email: string): string {
30
+ const n = name.trim(), e = email.trim();
31
+ if (n && e && n !== e) return `${n} · ${e}`;
32
+ return n || e;
33
+ }
34
+
35
+ export function shapeAccount(s: Partial<AccountState>): AccountState {
36
+ const supportUrl = safeUrl(s.supportUrl), privacyUrl = safeUrl(s.privacyUrl), termsUrl = safeUrl(s.termsUrl);
37
+ return {
38
+ status: s.status ?? "loading",
39
+ ownerLine: s.ownerLine ?? "",
40
+ versionLine: s.versionLine ?? "",
41
+ error: s.error ?? "",
42
+ supportUrl, privacyUrl, termsUrl,
43
+ noSupport: !supportUrl, noPrivacy: !privacyUrl, noTerms: !termsUrl,
44
+ };
45
+ }
@@ -18,6 +18,27 @@ export const DEMO_NOTE =
18
18
 
19
19
  export const DEMO_SAVE_NOTE = "Demo mode — changes aren’t saved in this preview.";
20
20
 
21
+ /** The state patch for a write refused because this is the demo preview. A demo
22
+ * refusal is a MODE, not an error: no red error line — the neutral info banner
23
+ * (already on screen before the owner typed anything) switches to the save
24
+ * note, so the tap still visibly answers. `saveNote` is the (copy-pack) text. */
25
+ export function demoRefusalPatch(saveNote: string = DEMO_SAVE_NOTE): { status: "idle"; error: ""; savedMessage: ""; demoNote: string } {
26
+ return { status: "idle", error: "", savedMessage: "", demoNote: saveNote || DEMO_SAVE_NOTE };
27
+ }
28
+
29
+ /** "€1,234.50" / "12,50" / " 10 " → "1234.50" / "12.50" / "10": digits and one
30
+ * decimal point — what an editable price field accepts and a price write needs.
31
+ * A separator followed by exactly 1–2 digits is the decimal; any other groups. */
32
+ export function editablePrice(raw: string): string {
33
+ const t = raw.replace(/[^\d.,]/g, "");
34
+ if (!t) return "";
35
+ const lastSep = Math.max(t.lastIndexOf("."), t.lastIndexOf(","));
36
+ if (lastSep >= 0 && /^\d{1,2}$/.test(t.slice(lastSep + 1))) {
37
+ return `${t.slice(0, lastSep).replace(/[.,]/g, "")}.${t.slice(lastSep + 1)}`;
38
+ }
39
+ return t.replace(/[.,]/g, "");
40
+ }
41
+
21
42
  export const DEMO_IDS = ["demo-1", "demo-2", "demo-3"] as const;
22
43
 
23
44
  export interface DemoProduct {
@@ -0,0 +1,68 @@
1
+ // Tier-5 bundle: __HIMI_APP_NAME__ — Account.
2
+ //
3
+ // The Account screen used to be a static SDUI descriptor whose subtitle was the
4
+ // literal "Wix owner (user)" — a role string, not a person. A descriptor cannot
5
+ // know who is signed in; a worker can. This one reads:
6
+ // • the signed-in Wix user (`getMyData`, the same users.wix.com read
7
+ // wix-owner-profile uses) → "Name · email" under the title;
8
+ // • the app's real version through @himalaya/app-info (never a baked literal,
9
+ // which goes stale the moment the binary is bumped);
10
+ // • the help / privacy / terms links, which arrive as onAppear params baked
11
+ // from provisioning.json `identity` — so a branded template ships ITS links,
12
+ // and `openLink` can only ever open one of those three URLs.
13
+ //
14
+ // Every read is best-effort: an identity or version the host cannot answer
15
+ // collapses its line rather than rendering an error on a settings screen.
16
+
17
+ import { defineService } from "@himalaya/state";
18
+ import { getMyData } from "@himalaya/wix-users";
19
+ import { displayVersion } from "@himalaya/app-info";
20
+ import { open } from "@himalaya/open-url";
21
+ import { stashCopy, copyTemplate } from "../_shared/himi-copy.js";
22
+ import { ownerLineOf, shapeAccount, type AccountState } from "../_shared/owner-account.js";
23
+
24
+ type LinkKind = "support" | "privacy" | "terms";
25
+
26
+ const obj = (v: unknown): Record<string, unknown> => (v && typeof v === "object" ? v as Record<string, unknown> : {});
27
+
28
+ const svc = defineService<AccountState, {
29
+ load: (s: AccountState, arg?: unknown) => Promise<AccountState>;
30
+ openLink: (s: AccountState, arg?: unknown) => Promise<AccountState>;
31
+ }>("himiOwnerLiteAccount", {
32
+ state: shapeAccount({}),
33
+ actions: {
34
+ load: async (s, arg) => {
35
+ stashCopy(arg);
36
+ const p = obj(arg);
37
+ const links = { supportUrl: String(p.supportUrl ?? s.supportUrl), privacyUrl: String(p.privacyUrl ?? s.privacyUrl), termsUrl: String(p.termsUrl ?? s.termsUrl) };
38
+ let ownerLine = "";
39
+ // Why the identity line is missing, when it is. Kept in state (the failure is
40
+ // modelled, not swallowed) but not painted: a settings screen that cannot name
41
+ // its owner still works, so the line collapses rather than showing an error.
42
+ let error: string = "";
43
+ try {
44
+ const d = await getMyData();
45
+ // The PROFILE name only — never the userName handle, an account nickname
46
+ // ("apptestercoowner"): exactly the kind of placeholder this line replaces.
47
+ const name = [d.profile?.firstName, d.profile?.lastName].filter(Boolean).join(" ");
48
+ ownerLine = ownerLineOf(name, String(d.email || d.contactEmail || ""));
49
+ } catch (e) {
50
+ error = String((e as Error)?.message ?? e);
51
+ }
52
+ let versionLine = "";
53
+ try {
54
+ const v = await displayVersion();
55
+ versionLine = v ? copyTemplate("ownerAccount.version", "Version {version}", { version: v }) : "";
56
+ } catch { /* host without app_info — the row collapses */ }
57
+ return shapeAccount({ ...s, ...links, status: "ready", ownerLine, versionLine, error });
58
+ },
59
+ openLink: async (s, arg) => {
60
+ const kind = String(obj(arg).kind ?? arg ?? "") as LinkKind;
61
+ const url = kind === "support" ? s.supportUrl : kind === "privacy" ? s.privacyUrl : kind === "terms" ? s.termsUrl : "";
62
+ if (url) await open(url);
63
+ return s;
64
+ },
65
+ },
66
+ });
67
+
68
+ export { svc };
@@ -111,7 +111,8 @@ const STARTER: Node = {
111
111
  props: {
112
112
  title: "Ask about your business",
113
113
  subtitle: "Try “which products are out of stock”, “unpaid orders”, or “what's coming up”.",
114
- icon: "search",
114
+ // Not `search`: Ask is not search, and a magnifier says it is.
115
+ icon: "ai",
115
116
  },
116
117
  },
117
118
  },
@@ -129,7 +130,7 @@ const enc = (n: Node): string => encodeSubtree(n as unknown as Parameters<typeof
129
130
  const initialState: AskState = {
130
131
  status: "idle",
131
132
  intent: "",
132
- headline: "Ask",
133
+ headline: "",
133
134
  note: "",
134
135
  customerNotice: "",
135
136
  repairs: "",
@@ -267,7 +268,7 @@ function failed(state: AskState, headline: string, note: string): AskState {
267
268
  body: {
268
269
  package: "himalaya",
269
270
  type: "EmptyState",
270
- props: { title: headline, subtitle: note, icon: "search" },
271
+ props: { title: headline, subtitle: note, icon: "ai" },
271
272
  },
272
273
  }],
273
274
  },
@@ -10,7 +10,7 @@ import { defineService } from "@himalaya/state";
10
10
  import { getEvent, updateEvent, type WixEvent } from "@himalaya/wix-events";
11
11
  import { normalizeEvent, eventDescription } from "../_shared/himi-events.js";
12
12
  import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
13
- import { DEMO_EVENTS, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-preview-demo.js";
13
+ import { DEMO_EVENTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
14
14
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
15
15
  import { get as dbGet, upsert as dbUpsert, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
16
16
 
@@ -113,6 +113,12 @@ function shape(s: Partial<EventFormState>): EventFormState {
113
113
 
114
114
  const startMode: Mode = "edit";
115
115
 
116
+
117
+ /** A demo-preview write refusal: see demoRefusalPatch (a mode, not an error). */
118
+ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
119
+ return shape({ ...s, ...demoRefusalPatch(copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE)) });
120
+ }
121
+
116
122
  const svc = defineService<EventFormState, {
117
123
  load: (s: EventFormState, arg?: unknown) => Promise<EventFormState>;
118
124
  submit: (s: EventFormState, values?: Record<string, unknown>) => Promise<EventFormState>;
@@ -185,7 +191,7 @@ const svc = defineService<EventFormState, {
185
191
  }
186
192
  },
187
193
  submit: async (s, values) => {
188
- if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, status: "error", error: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
194
+ if (isOwnerPreviewDemo() || s.demoNote) return demoRefusal(s);
189
195
  const v = values ?? {};
190
196
  const title = String(v.title ?? s.title).trim();
191
197
  const description = String(v.description ?? s.description).trim();
@@ -221,7 +227,7 @@ const svc = defineService<EventFormState, {
221
227
  // Demo / web-preview (no owner bridge; the record came from demo sample
222
228
  // data, marked by demoNote): a queued write could never replay, so answer
223
229
  // with the demo save note instead of the real-offline write-behind.
224
- if (s.demoNote) return shape({ ...s, status: "error", error: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE), savedMessage: "" });
230
+ if (s.demoNote) return demoRefusal(s);
225
231
  // Real device offline: the optimistic cache row already shows; queue the
226
232
  // write to replay (WriteQueue, idempotent) when connectivity returns.
227
233
  await queueWrite("tier5.himiOwnerLiteEventForm.submit", { id: s.id, title, description });
@@ -9,7 +9,7 @@ import { wixMediaUrl, WIX_DETAIL_IMAGE } from "../_shared/himi-store.js";
9
9
  import { defineService } from "@himalaya/state";
10
10
  import { getDraftPost, createDraftPost, updateDraftPost, type WixDraftPostWrite, type WixRichContent } from "@himalaya/wix-blog";
11
11
  import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
12
- import { DEMO_POSTS, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-preview-demo.js";
12
+ import { DEMO_POSTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
13
13
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
14
14
  import { get as dbGet, upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
15
15
 
@@ -127,6 +127,12 @@ function shape(s: Partial<PostFormState>): PostFormState {
127
127
 
128
128
  const startMode: Mode = initialInput.mode === "edit" || initialInput.id ? "edit" : "create";
129
129
 
130
+
131
+ /** A demo-preview write refusal: see demoRefusalPatch (a mode, not an error). */
132
+ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
133
+ return shape({ ...s, ...demoRefusalPatch(copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE)) });
134
+ }
135
+
130
136
  const svc = defineService<PostFormState, {
131
137
  load: (s: PostFormState, arg?: unknown) => Promise<PostFormState>;
132
138
  setCover: (s: PostFormState, arg?: unknown) => PostFormState;
@@ -140,7 +146,7 @@ const svc = defineService<PostFormState, {
140
146
  // bakes no id/mode); the onAppear `id` param is the #570 suffixed-build
141
147
  // fallback. An id ⇒ edit; no id anywhere ⇒ the create shell.
142
148
  const id = navInputId() || idFrom(arg, s.id);
143
- if (!id) return shape({ ...s, mode: "create", id: "", status: "idle", error: "" });
149
+ if (!id) return shape({ ...s, mode: "create", id: "", status: "idle", error: "", demoNote: isOwnerPreviewDemo() ? copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) : "" });
144
150
  if (isOwnerPreviewDemo()) {
145
151
  const d = DEMO_POSTS.find((x) => x.id === id);
146
152
  if (d) {
@@ -203,7 +209,7 @@ const svc = defineService<PostFormState, {
203
209
  return shape({ ...s, coverUrl, error: "" });
204
210
  },
205
211
  submit: async (s, values) => {
206
- if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, status: "error", error: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
212
+ if (isOwnerPreviewDemo() || s.demoNote) return demoRefusal(s);
207
213
  const v = values ?? {};
208
214
  const title = String(v.title ?? s.title).trim();
209
215
  const excerpt = String(v.excerpt ?? s.excerpt).trim();
@@ -272,7 +278,7 @@ const svc = defineService<PostFormState, {
272
278
  // Demo / web-preview (no owner bridge; the record came from demo sample
273
279
  // data, marked by demoNote): a queued write could never replay, so answer
274
280
  // with the demo save note instead of the real-offline write-behind.
275
- if (s.demoNote) return shape({ ...s, status: "error", error: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE), savedMessage: "" });
281
+ if (s.demoNote) return demoRefusal(s);
276
282
  // Real device offline: cache and queue the edit for reconnect.
277
283
  await cachePut(localRow(localId));
278
284
  await queueWrite("tier5.himiOwnerLitePostForm.submit", { id: targetId, title, excerpt, body, coverUrl, coverChanged: String(coverChanged), preserveBody: String(preserveBody), isFeatured: String(isFeatured), mode: editing ? "edit" : "create" });
@@ -18,7 +18,7 @@ import {
18
18
  import { get as dbGet, upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
19
19
  import { plainText } from "../_shared/himi-store.js";
20
20
  import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
21
- import { DEMO_PRODUCTS, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-preview-demo.js";
21
+ import { DEMO_PRODUCTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch, editablePrice } from "../_shared/owner-preview-demo.js";
22
22
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
23
23
 
24
24
  type Mode = "create" | "edit";
@@ -95,9 +95,14 @@ function boolString(value: unknown, fallback: string): string {
95
95
  // fall back to the raw numeric if the formatted string is missing.
96
96
  function priceOf(p: WixProduct): string {
97
97
  const pr = (p as any).price;
98
- return (pr?.formatted?.price || (pr?.price != null ? String(pr.price) : "")) as string;
98
+ // The field is EDITABLE and renderer-validated as an amount, so prefill the
99
+ // raw number — never the display string ("€10.00"), which would fail the rule
100
+ // and disable Save before the owner typed anything.
101
+ if (typeof pr?.price === "number" && Number.isFinite(pr.price)) return pr.price.toFixed(2);
102
+ return editablePrice(String(pr?.formatted?.price ?? pr?.price ?? ""));
99
103
  }
100
104
 
105
+
101
106
  function shape(s: Partial<ProductFormState>): ProductFormState {
102
107
  const mode: Mode = s.mode ?? "create";
103
108
  const id = typeof s.id === "string" ? s.id : "";
@@ -128,6 +133,12 @@ function shape(s: Partial<ProductFormState>): ProductFormState {
128
133
 
129
134
  const startMode: Mode = initialInput.mode === "edit" || initialInput.id ? "edit" : "create";
130
135
 
136
+
137
+ /** A demo-preview write refusal: see demoRefusalPatch (a mode, not an error). */
138
+ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
139
+ return shape({ ...s, ...demoRefusalPatch(copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE)) });
140
+ }
141
+
131
142
  const svc = defineService<ProductFormState, {
132
143
  load: (s: ProductFormState, arg?: unknown) => Promise<ProductFormState>;
133
144
  submit: (s: ProductFormState, values?: Record<string, unknown>) => Promise<ProductFormState>;
@@ -141,13 +152,13 @@ const svc = defineService<ProductFormState, {
141
152
  // bakes no id/mode); the onAppear `id` param is the #570 suffixed-build
142
153
  // fallback. An id ⇒ edit; no id anywhere ⇒ the create shell.
143
154
  const id = navInputId() || idFrom(arg, s.id);
144
- if (!id) return shape({ ...s, mode: "create", id: "", status: "idle", error: "" });
155
+ if (!id) return shape({ ...s, mode: "create", id: "", status: "idle", error: "", demoNote: isOwnerPreviewDemo() ? copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) : "" });
145
156
  if (isOwnerPreviewDemo()) {
146
157
  const d = DEMO_PRODUCTS.find((p) => p.id === id);
147
158
  if (d) {
148
159
  return shape({
149
160
  ...s, mode: "edit", id, heading: copyParam("ownerProductForm.headingEdit", "Edit product"),
150
- name: d.name, price: d.price.replace(/[^\d.]/g, ""), sku: d.sku, description: d.description,
161
+ name: d.name, price: editablePrice(d.price), sku: d.sku, description: d.description,
151
162
  visible: d.visible ? "true" : "false",
152
163
  status: "idle", error: "", savedMessage: "", demoNote: copyParam("ownerCommon.demoNote", DEMO_NOTE),
153
164
  });
@@ -178,7 +189,7 @@ const svc = defineService<ProductFormState, {
178
189
  if (cached) {
179
190
  return shape({
180
191
  ...s, mode: "edit", id, heading: "Edit product",
181
- name: String(cached.name ?? ""), price: String(cached.price ?? ""), sku: String(cached.sku ?? ""),
192
+ name: String(cached.name ?? ""), price: editablePrice(String(cached.price ?? "")), sku: String(cached.sku ?? ""),
182
193
  visible: cached.visible === "false" ? "false" : "true",
183
194
  status: "idle", error: "", savedMessage: "",
184
195
  });
@@ -188,7 +199,7 @@ const svc = defineService<ProductFormState, {
188
199
  if (d) {
189
200
  return shape({
190
201
  ...s, mode: "edit", id, heading: copyParam("ownerProductForm.headingEdit", "Edit product"),
191
- name: d.name, price: d.price.replace(/[^\d.]/g, ""), sku: d.sku, description: d.description,
202
+ name: d.name, price: editablePrice(d.price), sku: d.sku, description: d.description,
192
203
  visible: d.visible ? "true" : "false",
193
204
  status: "idle", error: "", savedMessage: "", demoNote: copyParam("ownerCommon.demoNote", DEMO_NOTE),
194
205
  });
@@ -198,17 +209,22 @@ const svc = defineService<ProductFormState, {
198
209
  }
199
210
  },
200
211
  submit: async (s, values) => {
201
- if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, status: "error", error: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
212
+ if (isOwnerPreviewDemo() || s.demoNote) return demoRefusal(s);
202
213
  const v = values ?? {};
203
214
  const name = String(v.name ?? s.name).trim();
204
- const price = String(v.price ?? s.price).trim();
215
+ const rawPrice = String(v.price ?? s.price).trim();
216
+ // "12,50" (comma-decimal keypad) is accepted and written as 12.50.
217
+ const price = editablePrice(rawPrice);
205
218
  const sku = String(v.sku ?? s.sku).trim();
206
219
  const description = String(v.description ?? s.description).trim();
207
220
  const visible = boolString(v.visible, s.visible) === "true";
208
221
  if (!name) return shape({ ...s, status: "error", error: copyParam("ownerProductForm.nameRequired", "Name is required"), savedMessage: "" });
209
222
  const priceNum = Number(price);
210
- if (!price || !Number.isFinite(priceNum)) {
211
- return shape({ ...s, name, price, sku, description, visible: visible ? "true" : "false", status: "error", error: copyParam("ownerProductForm.priceInvalid", "Enter a valid price (a number)."), savedMessage: "" });
223
+ // The renderer already blocks these (declarative `validation`); this is the
224
+ // worker's own guard for a host or queue replay that skips it. Checked on the
225
+ // RAW value, so "-5" is refused rather than normalized into "5".
226
+ if (!/^\d+([.,]\d{1,2})?$/.test(rawPrice) || !Number.isFinite(priceNum)) {
227
+ return shape({ ...s, name, price, sku, description, visible: visible ? "true" : "false", status: "error", error: copyParam("ownerProductForm.priceInvalid", "Enter a price of 0 or more."), savedMessage: "" });
212
228
  }
213
229
 
214
230
  const write: WixProductWrite = {
@@ -254,7 +270,7 @@ const svc = defineService<ProductFormState, {
254
270
  // Demo / web-preview (no owner bridge; the record came from DEMO_PRODUCTS,
255
271
  // marked by demoNote): a queued write could never replay, so answer with
256
272
  // the demo save note instead of the real-offline write-behind.
257
- if (s.demoNote) return shape({ ...s, status: "error", error: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE), savedMessage: "" });
273
+ if (s.demoNote) return demoRefusal(s);
258
274
  // Real device offline: the optimistic cache row already shows; queue the
259
275
  // write to replay (WriteQueue, idempotent) when connectivity returns.
260
276
  await queueWrite("tier5.himiOwnerLiteProductForm.submit", { id: s.id, name, price, sku, visible: String(visible), mode: s.mode });
@@ -269,7 +285,7 @@ const svc = defineService<ProductFormState, {
269
285
  }
270
286
  },
271
287
  remove: async (s) => {
272
- if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, status: "error", error: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
288
+ if (isOwnerPreviewDemo() || s.demoNote) return demoRefusal(s);
273
289
  if (!s.id) return shape({ ...s, status: "error", error: copyParam("ownerProductForm.errorNoDelete", "No product to delete."), savedMessage: "" });
274
290
  // Optimistic local delete — the list drops the row immediately.
275
291
  await cacheDrop(s.id);
@@ -278,7 +294,7 @@ const svc = defineService<ProductFormState, {
278
294
  return shape({ ...s, id: "", status: "saved", error: "", savedMessage: copyParam("ownerProductForm.deletedMessage", "Product deleted."), savedId: "" });
279
295
  } catch (e) {
280
296
  if (shouldQueueOwnerWrite(e)) {
281
- if (s.demoNote) return shape({ ...s, status: "error", error: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE), savedMessage: "" });
297
+ if (s.demoNote) return demoRefusal(s);
282
298
  await queueWrite("tier5.himiOwnerLiteProductForm.remove", { id: s.id });
283
299
  return shape({ ...s, id: "", status: "saved", error: "", savedMessage: copyParam("ownerProductForm.deletedOffline", "Deleted offline — will sync when you reconnect."), savedId: "" });
284
300
  }
@@ -7,7 +7,7 @@
7
7
  import { defineService } from "@himalaya/state";
8
8
  import { getService, updateService, type WixService } from "@himalaya/wix-bookings";
9
9
  import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
10
- import { DEMO_SERVICES, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-preview-demo.js";
10
+ import { DEMO_SERVICES, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
11
11
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
12
12
  import { get as dbGet, upsert as dbUpsert, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
13
13
 
@@ -103,6 +103,12 @@ function shape(s: Partial<ServiceFormState>): ServiceFormState {
103
103
  };
104
104
  }
105
105
 
106
+
107
+ /** A demo-preview write refusal: see demoRefusalPatch (a mode, not an error). */
108
+ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
109
+ return shape({ ...s, ...demoRefusalPatch(copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE)) });
110
+ }
111
+
106
112
  const svc = defineService<ServiceFormState, {
107
113
  load: (s: ServiceFormState, arg?: unknown) => Promise<ServiceFormState>;
108
114
  submit: (s: ServiceFormState, values?: Record<string, unknown>) => Promise<ServiceFormState>;
@@ -164,7 +170,7 @@ const svc = defineService<ServiceFormState, {
164
170
  }
165
171
  },
166
172
  submit: async (s, values) => {
167
- if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, status: "error", error: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
173
+ if (isOwnerPreviewDemo() || s.demoNote) return demoRefusal(s);
168
174
  const v = values ?? {};
169
175
  const name = String(v.name ?? s.name).trim();
170
176
  const description = String(v.description ?? s.description).trim();
@@ -193,7 +199,7 @@ const svc = defineService<ServiceFormState, {
193
199
  // Demo / web-preview (no owner bridge; the record came from demo sample
194
200
  // data, marked by demoNote): a queued write could never replay, so answer
195
201
  // with the demo save note instead of the real-offline write-behind.
196
- if (s.demoNote) return shape({ ...s, status: "error", error: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE), savedMessage: "" });
202
+ if (s.demoNote) return demoRefusal(s);
197
203
  // Real device offline: the optimistic cache row already shows; queue the
198
204
  // write to replay (WriteQueue, idempotent) when connectivity returns.
199
205
  await queueWrite("tier5.himiOwnerLiteServiceForm.submit", { id: s.id, name, description });