@wix/himalaya-cli 0.1567.0 → 0.1569.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 (168) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/l10n/src/index.d.ts +9 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/message-format.d.ts +28 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/online-programs/src/index.d.ts +7 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/l10n.mjs +191 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-online-programs.mjs +14 -1
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +4 -0
  7. package/dist/cli.mjs +20994 -20822
  8. package/dist/preview-assets.generated.bin +0 -0
  9. package/guides/app-config.md +10 -0
  10. package/guides/app-icon.md +20 -1
  11. package/guides/himi-json-output.md +7 -0
  12. package/guides/importable-sdks.md +1 -0
  13. package/guides/one-app-to-himi-app.md +14 -0
  14. package/guides/release-notes.md +6 -0
  15. package/guides/sdk-reference.json +41 -2
  16. package/guides/splash-screens.md +156 -5
  17. package/package.json +2 -2
  18. package/templates/beauty-salon/config.ts +12 -1
  19. package/templates/beauty-salon/dev/index.ts +265 -0
  20. package/templates/beauty-salon/himalaya.content.json +3 -0
  21. package/templates/beauty-salon/splash.json +3 -0
  22. package/templates/beauty-salon/tier5-src/_shared/himi-copy.ts +17 -6
  23. package/templates/beauty-salon/tier5-src/_shared/rich-body.ts +44 -0
  24. package/templates/beauty-salon/tier5-src/himi-booking-service/index.ts +13 -9
  25. package/templates/beauty-salon/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  26. package/templates/beauty-salon/tier5-src/himi-store-account/index.ts +4 -0
  27. package/templates/beauty-salon-owner/config.ts +8 -0
  28. package/templates/beauty-salon-owner/dev/index.ts +1 -0
  29. package/templates/beauty-salon-owner/tier5-src/_shared/himi-copy.ts +17 -6
  30. package/templates/beauty-salon-owner/tier5-src/_shared/rich-body.ts +44 -0
  31. package/templates/branded-lite/config.ts +12 -1
  32. package/templates/branded-lite/dev/index.ts +299 -2
  33. package/templates/branded-lite/himalaya.content.json +3 -0
  34. package/templates/branded-lite/splash.json +3 -0
  35. package/templates/branded-lite/tier5-src/_shared/himi-copy.ts +17 -6
  36. package/templates/branded-lite/tier5-src/_shared/rich-body.ts +44 -0
  37. package/templates/branded-lite/tier5-src/himi-booking-service/index.ts +13 -9
  38. package/templates/branded-lite/tier5-src/himi-branded-lite-group/index.ts +20 -3
  39. package/templates/branded-lite/tier5-src/himi-branded-lite-groups/index.ts +18 -2
  40. package/templates/branded-lite/tier5-src/himi-branded-lite-notifsettings/index.ts +4 -2
  41. package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +124 -15
  42. package/templates/branded-lite/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  43. package/templates/branded-lite/tier5-src/himi-store-account/index.ts +4 -0
  44. package/templates/brandy/tier5-src/_shared/himi-copy.ts +17 -6
  45. package/templates/community-club/config.ts +12 -1
  46. package/templates/community-club/dev/index.ts +267 -1
  47. package/templates/community-club/himalaya.content.json +3 -0
  48. package/templates/community-club/splash.json +3 -0
  49. package/templates/community-club/tier5-src/_shared/himi-copy.ts +17 -6
  50. package/templates/community-club/tier5-src/_shared/rich-body.ts +44 -0
  51. package/templates/community-club/tier5-src/himi-branded-lite-group/index.ts +20 -3
  52. package/templates/community-club/tier5-src/himi-branded-lite-groups/index.ts +18 -2
  53. package/templates/community-club/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  54. package/templates/community-club/tier5-src/himi-store-account/index.ts +4 -0
  55. package/templates/community-club-owner/config.ts +8 -0
  56. package/templates/community-club-owner/dev/index.ts +1 -0
  57. package/templates/community-club-owner/tier5-src/_shared/himi-copy.ts +17 -6
  58. package/templates/community-club-owner/tier5-src/_shared/rich-body.ts +44 -0
  59. package/templates/events-meetups/config.ts +12 -1
  60. package/templates/events-meetups/dev/index.ts +267 -1
  61. package/templates/events-meetups/himalaya.content.json +3 -0
  62. package/templates/events-meetups/splash.json +3 -0
  63. package/templates/events-meetups/tier5-src/_shared/himi-copy.ts +17 -6
  64. package/templates/events-meetups/tier5-src/_shared/rich-body.ts +44 -0
  65. package/templates/events-meetups/tier5-src/himi-branded-lite-group/index.ts +20 -3
  66. package/templates/events-meetups/tier5-src/himi-branded-lite-groups/index.ts +18 -2
  67. package/templates/events-meetups/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  68. package/templates/events-meetups/tier5-src/himi-store-account/index.ts +4 -0
  69. package/templates/events-meetups-owner/config.ts +8 -0
  70. package/templates/events-meetups-owner/dev/index.ts +1 -0
  71. package/templates/events-meetups-owner/tier5-src/_shared/himi-copy.ts +17 -6
  72. package/templates/events-meetups-owner/tier5-src/_shared/rich-body.ts +44 -0
  73. package/templates/gym-fitness/config.ts +12 -1
  74. package/templates/gym-fitness/dev/index.ts +265 -0
  75. package/templates/gym-fitness/himalaya.content.json +3 -0
  76. package/templates/gym-fitness/splash.json +3 -0
  77. package/templates/gym-fitness/tier5-src/_shared/himi-copy.ts +17 -6
  78. package/templates/gym-fitness/tier5-src/_shared/rich-body.ts +44 -0
  79. package/templates/gym-fitness/tier5-src/himi-booking-service/index.ts +13 -9
  80. package/templates/gym-fitness/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  81. package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +4 -0
  82. package/templates/gym-fitness-owner/config.ts +8 -0
  83. package/templates/gym-fitness-owner/dev/index.ts +1 -0
  84. package/templates/gym-fitness-owner/tier5-src/_shared/himi-copy.ts +17 -6
  85. package/templates/gym-fitness-owner/tier5-src/_shared/rich-body.ts +44 -0
  86. package/templates/himi-chat/dev/index.ts +1 -0
  87. package/templates/himi-chat/tier5-src/_shared/himi-copy.ts +17 -6
  88. package/templates/nonprofit-cause/config.ts +12 -1
  89. package/templates/nonprofit-cause/dev/index.ts +267 -1
  90. package/templates/nonprofit-cause/himalaya.content.json +3 -0
  91. package/templates/nonprofit-cause/splash.json +3 -0
  92. package/templates/nonprofit-cause/tier5-src/_shared/himi-copy.ts +17 -6
  93. package/templates/nonprofit-cause/tier5-src/_shared/rich-body.ts +44 -0
  94. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-group/index.ts +20 -3
  95. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-groups/index.ts +18 -2
  96. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  97. package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +4 -0
  98. package/templates/nonprofit-cause-owner/config.ts +8 -0
  99. package/templates/nonprofit-cause-owner/dev/index.ts +1 -0
  100. package/templates/nonprofit-cause-owner/tier5-src/_shared/himi-copy.ts +17 -6
  101. package/templates/nonprofit-cause-owner/tier5-src/_shared/rich-body.ts +44 -0
  102. package/templates/owner-lite/config.ts +8 -0
  103. package/templates/owner-lite/dev/index.ts +1 -0
  104. package/templates/owner-lite/tier5-src/_shared/himi-copy.ts +17 -6
  105. package/templates/owner-lite/tier5-src/_shared/rich-body.ts +44 -0
  106. package/templates/podcast-creator/config.ts +12 -1
  107. package/templates/podcast-creator/dev/index.ts +237 -0
  108. package/templates/podcast-creator/himalaya.content.json +3 -0
  109. package/templates/podcast-creator/splash.json +3 -0
  110. package/templates/podcast-creator/tier5-src/_shared/himi-copy.ts +17 -6
  111. package/templates/podcast-creator/tier5-src/_shared/rich-body.ts +44 -0
  112. package/templates/podcast-creator/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  113. package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +4 -0
  114. package/templates/podcast-creator-owner/config.ts +8 -0
  115. package/templates/podcast-creator-owner/dev/index.ts +1 -0
  116. package/templates/podcast-creator-owner/tier5-src/_shared/himi-copy.ts +17 -6
  117. package/templates/podcast-creator-owner/tier5-src/_shared/rich-body.ts +44 -0
  118. package/templates/portfolio-creative/config.ts +12 -1
  119. package/templates/portfolio-creative/dev/index.ts +237 -0
  120. package/templates/portfolio-creative/himalaya.content.json +3 -0
  121. package/templates/portfolio-creative/splash.json +3 -0
  122. package/templates/portfolio-creative/tier5-src/_shared/himi-copy.ts +17 -6
  123. package/templates/portfolio-creative/tier5-src/_shared/rich-body.ts +44 -0
  124. package/templates/portfolio-creative/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  125. package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +4 -0
  126. package/templates/portfolio-creative-owner/config.ts +8 -0
  127. package/templates/portfolio-creative-owner/dev/index.ts +1 -0
  128. package/templates/portfolio-creative-owner/tier5-src/_shared/himi-copy.ts +17 -6
  129. package/templates/portfolio-creative-owner/tier5-src/_shared/rich-body.ts +44 -0
  130. package/templates/restaurant/config.ts +12 -1
  131. package/templates/restaurant/dev/index.ts +237 -0
  132. package/templates/restaurant/himalaya.content.json +3 -0
  133. package/templates/restaurant/splash.json +3 -0
  134. package/templates/restaurant/tier5-src/_shared/himi-copy.ts +17 -6
  135. package/templates/restaurant/tier5-src/_shared/rich-body.ts +44 -0
  136. package/templates/restaurant/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  137. package/templates/restaurant/tier5-src/himi-store-account/index.ts +4 -0
  138. package/templates/restaurant-owner/config.ts +8 -0
  139. package/templates/restaurant-owner/dev/index.ts +1 -0
  140. package/templates/restaurant-owner/tier5-src/_shared/himi-copy.ts +17 -6
  141. package/templates/restaurant-owner/tier5-src/_shared/rich-body.ts +44 -0
  142. package/templates/retail-store/config.ts +12 -1
  143. package/templates/retail-store/dev/index.ts +237 -0
  144. package/templates/retail-store/himalaya.content.json +3 -0
  145. package/templates/retail-store/splash.json +3 -0
  146. package/templates/retail-store/tier5-src/_shared/himi-copy.ts +17 -6
  147. package/templates/retail-store/tier5-src/_shared/rich-body.ts +44 -0
  148. package/templates/retail-store/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  149. package/templates/retail-store/tier5-src/himi-store-account/index.ts +4 -0
  150. package/templates/retail-store-owner/config.ts +8 -0
  151. package/templates/retail-store-owner/dev/index.ts +1 -0
  152. package/templates/retail-store-owner/tier5-src/_shared/himi-copy.ts +17 -6
  153. package/templates/retail-store-owner/tier5-src/_shared/rich-body.ts +44 -0
  154. package/templates/surface-showcase/dev/index.ts +1 -0
  155. package/templates/yoga-studio/config.ts +12 -1
  156. package/templates/yoga-studio/dev/index.ts +269 -1
  157. package/templates/yoga-studio/himalaya.content.json +3 -0
  158. package/templates/yoga-studio/splash.json +3 -0
  159. package/templates/yoga-studio/tier5-src/_shared/himi-copy.ts +17 -6
  160. package/templates/yoga-studio/tier5-src/_shared/rich-body.ts +44 -0
  161. package/templates/yoga-studio/tier5-src/himi-booking-service/index.ts +13 -9
  162. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +124 -15
  163. package/templates/yoga-studio/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  164. package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +4 -0
  165. package/templates/yoga-studio-owner/config.ts +8 -0
  166. package/templates/yoga-studio-owner/dev/index.ts +1 -0
  167. package/templates/yoga-studio-owner/tier5-src/_shared/himi-copy.ts +17 -6
  168. package/templates/yoga-studio-owner/tier5-src/_shared/rich-body.ts +44 -0
@@ -1,8 +1,8 @@
1
1
  // Copy-as-configuration for WORKER-emitted strings (`himi docs designing-himi-screens`,
2
2
  // §"Copy is configuration"): a worker never hardcodes user-facing copy — the
3
3
  // descriptor bakes the provisioned copy-pack overrides relevant to this worker
4
- // into its onAppear action params as a `copy` JSON object (key → override
5
- // string, computed from the app contract at descriptor-build time), and the
4
+ // into its onAppear action params as a `copy` JSON object (customer strings or
5
+ // source-matched stock translations, computed at descriptor-build time), and the
6
6
  // worker reads every user-facing string through `copyParam(key, fallback)`.
7
7
  //
8
8
  // STASHED (the stashed appear-params idiom, himi-branded-lite-home): the
@@ -16,9 +16,13 @@
16
16
  // every worker that emits user-facing copy, and copied verbatim into the
17
17
  // generated templates' tier5-src/_shared/.
18
18
 
19
- let stashedCopy: Record<string, string> = {};
19
+ /** Customer strings override every read; translated stock is valid only for its
20
+ * original English fallback. Some existing keys serve more than one message. */
21
+ export type CopyPackValue = string | { source: string; translation: string };
20
22
 
21
- /** Absorb the `copy` onAppear param (a JSON object of key → override string)
23
+ let stashedCopy: Record<string, CopyPackValue> = Object.create(null);
24
+
25
+ /** Absorb the `copy` onAppear param (customer strings or stock translation records)
22
26
  * from an action arg. Call first in every action that can receive onAppear
23
27
  * params (load/refresh). Non-copy args and malformed packs are ignored — the
24
28
  * stock fallbacks keep rendering. */
@@ -29,9 +33,15 @@ export function stashCopy(arg: unknown): void {
29
33
  try {
30
34
  const parsed = JSON.parse(raw) as unknown;
31
35
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
32
- const next: Record<string, string> = {};
36
+ const next: Record<string, CopyPackValue> = Object.create(null);
33
37
  for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
34
38
  if (typeof value === "string") next[key] = value;
39
+ else if (value && typeof value === "object" && !Array.isArray(value)) {
40
+ const row = value as Record<string, unknown>;
41
+ if (typeof row.source === "string" && typeof row.translation === "string") {
42
+ next[key] = { source: row.source, translation: row.translation };
43
+ }
44
+ }
35
45
  }
36
46
  stashedCopy = next;
37
47
  } catch {
@@ -43,7 +53,8 @@ export function stashCopy(arg: unknown): void {
43
53
  * pack / no such key reached this worker. */
44
54
  export function copyParam(key: string, fallback: string): string {
45
55
  const value = stashedCopy[key];
46
- return typeof value === "string" && value !== "" ? value : fallback;
56
+ if (typeof value === "string") return value !== "" ? value : fallback;
57
+ return value?.source === fallback && value.translation !== "" ? value.translation : fallback;
47
58
  }
48
59
 
49
60
  /** Interpolate `{name}` placeholders in a (possibly overridden) template:
@@ -32,3 +32,47 @@ export function richBody(richContent: unknown, fallback = "", imageWidth = 1400)
32
32
  const text = rejected === "too_deep" ? "" : ricosToPlainText(doc);
33
33
  return { bodyJson: json, bodyText: text || fallback };
34
34
  }
35
+
36
+ /** A Wix description may be Ricos (including serialized JSON), HTML or ordinary text.
37
+ * Keep rich documents on the existing bounded encoder path. Non-document JSON and
38
+ * clearly malformed serialization use an independent fallback. Ambiguous bracketed
39
+ * prose stays readable; this does not attempt to repair arbitrary JSON syntax. */
40
+ export function descriptionBody(value: unknown, fallback = "", imageWidth = 1400): RichBody {
41
+ if (typeof value !== "string") return richBody(value, fallback, imageWidth);
42
+ const text = value.trim();
43
+ if (/^[{[]/.test(text)) {
44
+ try {
45
+ const parsed: unknown = JSON.parse(text);
46
+ // Passing the parsed value avoids parsing a valid Ricos document twice; a
47
+ // different JSON structure is rejected by the same existing document check.
48
+ if (parsed && typeof parsed === "object") return richBody(parsed, fallback, imageWidth);
49
+ } catch {
50
+ const objectLike = /^\{\s*(?:["'}]|nodes\b)/.test(text);
51
+ // Only reject scalar-looking malformed arrays when their syntax is clearly
52
+ // unfinished (or contains JS's non-JSON `undefined`). Balanced bracketed
53
+ // prose such as "[1,000]" and "[12:30, 13:30]" is not JSON and stays readable.
54
+ const arrayTokens = text.replace(/"(?:\\.|[^"\\])*"/g, "")
55
+ .replace(/\b(?:true|false|null|undefined)\b/g, "")
56
+ .replace(/-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g, "");
57
+ const arrayLike = /^\[\s*[{"]/.test(text)
58
+ || (text.startsWith("[") && /^[\s[\]{},:]*$/.test(arrayTokens)
59
+ && (/,\s*]?$/.test(text) || /\bundefined\b/.test(text) || !text.endsWith("]")));
60
+ if (objectLike || arrayLike) return { bodyJson: "", bodyText: fallback };
61
+ }
62
+ }
63
+ // Legacy HTML descriptions stay text. Preserve paragraph breaks and ordinary prose's
64
+ // bare '<' (e.g. "ages < 8"), without relying on browser-only DOM APIs in workers.
65
+ const plain = text
66
+ .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, "")
67
+ .replace(/<br\s*\/?\s*>/gi, "\n")
68
+ .replace(/<\/(?:p|div|h[1-6]|li)\s*>/gi, "\n\n")
69
+ .replace(/<\/?[a-z][^>]*>/gi, "")
70
+ .replace(/&(?:nbsp|amp|lt|gt|quot|apos|#39);/gi, (entity) => ({
71
+ "&nbsp;": " ", "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&apos;": "'", "&#39;": "'",
72
+ })[entity.toLowerCase()] ?? entity)
73
+ .replace(/[\t\r ]+/g, " ")
74
+ .replace(/ *\n */g, "\n")
75
+ .replace(/\n{3,}/g, "\n\n")
76
+ .trim();
77
+ return { bodyJson: "", bodyText: plain || fallback };
78
+ }
@@ -52,6 +52,16 @@ export default defineApp({
52
52
  ]
53
53
  },
54
54
  designTokensPath: "./tokens.json",
55
+ // Native fallback; the T5 splash descriptor owns the editable launch appearance.
56
+ splash: {
57
+ "enabled": true,
58
+ "backgroundColor": "accentPrimary",
59
+ "logoAsset": "branded_lite_app_icon",
60
+ "logoWidth": 192,
61
+ "layout": "logoOnly",
62
+ "minVisibleMs": 0,
63
+ "maxHoldMs": 5000
64
+ },
55
65
  navigation: {
56
66
  "navKind": "responsive",
57
67
  "scheme": "himalaya-__HIMI_APP_SLUG__",
@@ -220,7 +230,8 @@ export default defineApp({
220
230
  "contextName",
221
231
  "returnScreenId",
222
232
  "returnId",
223
- "returnChannelId"
233
+ "returnChannelId",
234
+ "authContinuation"
224
235
  ],
225
236
  "parentScreenId": "himi_account",
226
237
  "tabId": "profile"
@@ -2,6 +2,7 @@
2
2
  // Raw-descriptor dialect (see `himi docs tier-5-add-a-screen`); templated routes
3
3
  // fill __HIMI_ID__ sentinels per request.
4
4
  import config from "../config.js";
5
+ import splashIcon from "../splash.json" with { type: "json" };
5
6
 
6
7
  const runtimeSetting = (value: unknown): unknown => {
7
8
  if (value === "__HIMI_PODCAST_FEED_URL__") return process.env.PODCAST_FEED_URL?.trim() || "";
@@ -13,6 +14,227 @@ const runtimeSetting = (value: unknown): unknown => {
13
14
  };
14
15
 
15
16
  const SCREENS = runtimeSetting({
17
+ "himi_branded_splash": {
18
+ "screenId": "himi_branded_splash",
19
+ "renderer": "sdui_intent",
20
+ "title": "",
21
+ "schemaVersion": 1,
22
+ "cacheControl": {
23
+ "maxAge": 0,
24
+ "staleWhileRevalidate": 0
25
+ },
26
+ "input": {
27
+ "showIcon": true,
28
+ "title": "",
29
+ "caption": ""
30
+ },
31
+ "logicBundle": {
32
+ "url": "http://localhost:8190/v1/bundles/himi-branded-lite-splash?integrity=",
33
+ "integrity": "",
34
+ "contentType": "application/javascript+ts-bundle",
35
+ "declaredCapabilities": [],
36
+ "minBinaryVersion": "0.1.0",
37
+ "bundleVersion": "0.0.1"
38
+ },
39
+ "payload": {
40
+ "id": "splash_t5",
41
+ "body": {
42
+ "package": "himalaya",
43
+ "type": "ZStack",
44
+ "props": {
45
+ "alignment": "center",
46
+ "children": [
47
+ {
48
+ "id": "splash_t5_gradient",
49
+ "body": {
50
+ "package": "himalaya",
51
+ "type": "VStack",
52
+ "props": {
53
+ "children": [
54
+ {
55
+ "id": "splash_t5_gradient_fill",
56
+ "body": {
57
+ "package": "himalaya",
58
+ "type": "Spacer",
59
+ "props": {}
60
+ }
61
+ }
62
+ ]
63
+ }
64
+ },
65
+ "modifiers": {
66
+ "fillMaxWidth": true,
67
+ "backgroundGradient": {
68
+ "token": "brand"
69
+ },
70
+ "transition": {
71
+ "enter": "fade",
72
+ "motion": {
73
+ "token": "appearSoft"
74
+ }
75
+ },
76
+ "respectReducedMotion": true
77
+ }
78
+ },
79
+ {
80
+ "id": "splash_t5_icon",
81
+ "body": {
82
+ "package": "himalaya",
83
+ "type": "ZStack",
84
+ "props": {
85
+ "alignment": "center",
86
+ "children": [
87
+ {
88
+ "id": "splash_t5_icon_image",
89
+ "body": {
90
+ "package": "himalaya",
91
+ "type": "Image",
92
+ "props": {
93
+ "asset": "branded_lite_app_icon",
94
+ "width": 192,
95
+ "height": 192,
96
+ "contentMode": "fit"
97
+ }
98
+ },
99
+ "modifiers": {
100
+ "accessibilityRole": "decorative",
101
+ "scale": 1.5
102
+ }
103
+ }
104
+ ]
105
+ }
106
+ },
107
+ "modifiers": {
108
+ "width": 192,
109
+ "height": 192,
110
+ "clipShape": "circle",
111
+ "accessibilityRole": "decorative",
112
+ "hiddenPath": "$service:splash.iconHidden",
113
+ "transition": {
114
+ "enter": "none",
115
+ "exit": "fade",
116
+ "motion": {
117
+ "token": "appearSoft"
118
+ }
119
+ },
120
+ "respectReducedMotion": true
121
+ }
122
+ },
123
+ {
124
+ "id": "",
125
+ "body": {
126
+ "package": "himalaya",
127
+ "type": "VStack",
128
+ "props": {
129
+ "alignment": "center",
130
+ "spacing": 0,
131
+ "children": [
132
+ {
133
+ "id": "splash_t5_top",
134
+ "body": {
135
+ "package": "himalaya",
136
+ "type": "Spacer",
137
+ "props": {}
138
+ },
139
+ "modifiers": {
140
+ "weight": 1
141
+ }
142
+ },
143
+ {
144
+ "id": "splash_t5_copy",
145
+ "body": {
146
+ "package": "himalaya",
147
+ "type": "VStack",
148
+ "props": {
149
+ "alignment": "center",
150
+ "spacing": 16,
151
+ "children": [
152
+ {
153
+ "id": "splash_t5_title",
154
+ "body": {
155
+ "package": "himalaya",
156
+ "type": "Text",
157
+ "props": {
158
+ "text": "$service:splash.title",
159
+ "style": "title1",
160
+ "color": "textOnAccent",
161
+ "textAlign": "center"
162
+ }
163
+ }
164
+ },
165
+ {
166
+ "id": "splash_t5_caption",
167
+ "body": {
168
+ "package": "himalaya",
169
+ "type": "Text",
170
+ "props": {
171
+ "text": "$service:splash.caption",
172
+ "style": "body",
173
+ "color": "textOnAccent",
174
+ "collapseWhenEmpty": true,
175
+ "textAlign": "center"
176
+ }
177
+ }
178
+ },
179
+ {
180
+ "id": "splash_t5_bottom",
181
+ "body": {
182
+ "package": "himalaya",
183
+ "type": "Spacer",
184
+ "props": {}
185
+ }
186
+ }
187
+ ]
188
+ }
189
+ },
190
+ "modifiers": {
191
+ "weight": 1,
192
+ "fillMaxWidth": true,
193
+ "padding": {
194
+ "top": 120,
195
+ "bottom": 24,
196
+ "leading": 24,
197
+ "trailing": 24
198
+ }
199
+ }
200
+ }
201
+ ]
202
+ }
203
+ }
204
+ },
205
+ {
206
+ "id": "splash_t5_presented",
207
+ "body": {
208
+ "package": "himalaya",
209
+ "type": "Spacer",
210
+ "props": {}
211
+ },
212
+ "modifiers": {
213
+ "width": 0,
214
+ "height": 0,
215
+ "hiddenPath": "$service:splash.presentationPending",
216
+ "onAppear": {
217
+ "actionId": "splash.presented",
218
+ "params": {
219
+ "motion": "appearSoft"
220
+ }
221
+ }
222
+ }
223
+ }
224
+ ]
225
+ }
226
+ },
227
+ "modifiers": {
228
+ "fillMaxWidth": true,
229
+ "onAppear": {
230
+ "actionId": "tier5.splash.present"
231
+ },
232
+ "background": {
233
+ "token": "accentPrimary"
234
+ }
235
+ }
236
+ }
237
+ },
16
238
  "himi_branded_home": {
17
239
  "screenId": "himi_branded_home",
18
240
  "renderer": "sdui_intent",
@@ -17088,6 +17310,21 @@ const SCREENS = runtimeSetting({
17088
17310
  },
17089
17311
  }) as Record<string, unknown>;
17090
17312
 
17313
+ // The hosted app icon is content: updating splash.json needs an OTA publish, not a native build.
17314
+ // Without a hosted image, preserve the bundled icon and its native handoff crop.
17315
+ if (splashIcon.iconUrl) {
17316
+ const url = new URL(splashIcon.iconUrl);
17317
+ if (url.protocol !== "https:" || url.username || url.password) throw new Error("Splash images require a public HTTPS URL");
17318
+ interface IconNode { id?: string; body: { package: string; type: string; props: { children?: IconNode[]; asset?: string; url?: string } } }
17319
+ const splash = SCREENS.himi_branded_splash as { input?: { customLogo?: boolean }; payload?: { body?: { props?: { children?: IconNode[] } } } };
17320
+ const icon = splash.payload?.body?.props?.children?.find((node) => node.id === "splash_t5_icon");
17321
+ const image = icon?.body.type === "ZStack" ? icon.body.props.children?.find((node) => node.id === "splash_t5_icon_image") : icon;
17322
+ if (image && !splash.input?.customLogo) {
17323
+ delete image.body.props.asset;
17324
+ image.body.props.url = splashIcon.iconUrl;
17325
+ }
17326
+ }
17327
+
17091
17328
  // The language destination stays registered for deep links and future translations. Keep its
17092
17329
  // account entry out of a single-language app; adding a locale to config.locale.translations
17093
17330
  // makes the same entry visible after the app config reloads.
@@ -21618,6 +21855,31 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
21618
21855
  }
21619
21856
  }
21620
21857
  },
21858
+ {
21859
+ "id": "himi_group.description_rich",
21860
+ "body": {
21861
+ "package": "himalaya",
21862
+ "type": "RichContent",
21863
+ "props": {
21864
+ "documentJsonPath": "$service:himiBrandedGroup.descriptionJson",
21865
+ "onLink": {
21866
+ "actionId": "tier5.himiBrandedGroup.openLink"
21867
+ },
21868
+ "fallback": {
21869
+ "id": "",
21870
+ "body": {
21871
+ "package": "himalaya",
21872
+ "type": "Text",
21873
+ "props": {
21874
+ "text": "$service:himiBrandedGroup.description",
21875
+ "style": "body",
21876
+ "color": "textSecondary"
21877
+ }
21878
+ }
21879
+ }
21880
+ }
21881
+ }
21882
+ },
21621
21883
  {
21622
21884
  "id": "",
21623
21885
  "body": {
@@ -21629,6 +21891,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
21629
21891
  "color": "textSecondary",
21630
21892
  "collapseWhenEmpty": true
21631
21893
  }
21894
+ },
21895
+ "modifiers": {
21896
+ "hiddenPath": "$service:himiBrandedGroup.hasDescriptionJson"
21632
21897
  }
21633
21898
  },
21634
21899
  {
@@ -21771,7 +22036,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
21771
22036
  "contentType": "application/javascript+ts-bundle",
21772
22037
  "declaredCapabilities": [
21773
22038
  "wix_api",
21774
- "local_database"
22039
+ "local_database",
22040
+ "open_url"
21775
22041
  ],
21776
22042
  "minBinaryVersion": "0.1.0",
21777
22043
  "bundleVersion": "0.0.1"
@@ -25,6 +25,9 @@
25
25
  "himi_branded_home": {
26
26
  "displayName": "Home"
27
27
  },
28
+ "himi_branded_splash": {
29
+ "displayName": "Splash"
30
+ },
28
31
  "himi_create_post": {
29
32
  "displayName": "Create Post"
30
33
  },
@@ -0,0 +1,3 @@
1
+ {
2
+ "iconUrl": ""
3
+ }
@@ -1,8 +1,8 @@
1
1
  // Copy-as-configuration for WORKER-emitted strings (`himi docs designing-himi-screens`,
2
2
  // §"Copy is configuration"): a worker never hardcodes user-facing copy — the
3
3
  // descriptor bakes the provisioned copy-pack overrides relevant to this worker
4
- // into its onAppear action params as a `copy` JSON object (key → override
5
- // string, computed from the app contract at descriptor-build time), and the
4
+ // into its onAppear action params as a `copy` JSON object (customer strings or
5
+ // source-matched stock translations, computed at descriptor-build time), and the
6
6
  // worker reads every user-facing string through `copyParam(key, fallback)`.
7
7
  //
8
8
  // STASHED (the stashed appear-params idiom, himi-branded-lite-home): the
@@ -16,9 +16,13 @@
16
16
  // every worker that emits user-facing copy, and copied verbatim into the
17
17
  // generated templates' tier5-src/_shared/.
18
18
 
19
- let stashedCopy: Record<string, string> = {};
19
+ /** Customer strings override every read; translated stock is valid only for its
20
+ * original English fallback. Some existing keys serve more than one message. */
21
+ export type CopyPackValue = string | { source: string; translation: string };
20
22
 
21
- /** Absorb the `copy` onAppear param (a JSON object of key → override string)
23
+ let stashedCopy: Record<string, CopyPackValue> = Object.create(null);
24
+
25
+ /** Absorb the `copy` onAppear param (customer strings or stock translation records)
22
26
  * from an action arg. Call first in every action that can receive onAppear
23
27
  * params (load/refresh). Non-copy args and malformed packs are ignored — the
24
28
  * stock fallbacks keep rendering. */
@@ -29,9 +33,15 @@ export function stashCopy(arg: unknown): void {
29
33
  try {
30
34
  const parsed = JSON.parse(raw) as unknown;
31
35
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
32
- const next: Record<string, string> = {};
36
+ const next: Record<string, CopyPackValue> = Object.create(null);
33
37
  for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
34
38
  if (typeof value === "string") next[key] = value;
39
+ else if (value && typeof value === "object" && !Array.isArray(value)) {
40
+ const row = value as Record<string, unknown>;
41
+ if (typeof row.source === "string" && typeof row.translation === "string") {
42
+ next[key] = { source: row.source, translation: row.translation };
43
+ }
44
+ }
35
45
  }
36
46
  stashedCopy = next;
37
47
  } catch {
@@ -43,7 +53,8 @@ export function stashCopy(arg: unknown): void {
43
53
  * pack / no such key reached this worker. */
44
54
  export function copyParam(key: string, fallback: string): string {
45
55
  const value = stashedCopy[key];
46
- return typeof value === "string" && value !== "" ? value : fallback;
56
+ if (typeof value === "string") return value !== "" ? value : fallback;
57
+ return value?.source === fallback && value.translation !== "" ? value.translation : fallback;
47
58
  }
48
59
 
49
60
  /** Interpolate `{name}` placeholders in a (possibly overridden) template:
@@ -32,3 +32,47 @@ export function richBody(richContent: unknown, fallback = "", imageWidth = 1400)
32
32
  const text = rejected === "too_deep" ? "" : ricosToPlainText(doc);
33
33
  return { bodyJson: json, bodyText: text || fallback };
34
34
  }
35
+
36
+ /** A Wix description may be Ricos (including serialized JSON), HTML or ordinary text.
37
+ * Keep rich documents on the existing bounded encoder path. Non-document JSON and
38
+ * clearly malformed serialization use an independent fallback. Ambiguous bracketed
39
+ * prose stays readable; this does not attempt to repair arbitrary JSON syntax. */
40
+ export function descriptionBody(value: unknown, fallback = "", imageWidth = 1400): RichBody {
41
+ if (typeof value !== "string") return richBody(value, fallback, imageWidth);
42
+ const text = value.trim();
43
+ if (/^[{[]/.test(text)) {
44
+ try {
45
+ const parsed: unknown = JSON.parse(text);
46
+ // Passing the parsed value avoids parsing a valid Ricos document twice; a
47
+ // different JSON structure is rejected by the same existing document check.
48
+ if (parsed && typeof parsed === "object") return richBody(parsed, fallback, imageWidth);
49
+ } catch {
50
+ const objectLike = /^\{\s*(?:["'}]|nodes\b)/.test(text);
51
+ // Only reject scalar-looking malformed arrays when their syntax is clearly
52
+ // unfinished (or contains JS's non-JSON `undefined`). Balanced bracketed
53
+ // prose such as "[1,000]" and "[12:30, 13:30]" is not JSON and stays readable.
54
+ const arrayTokens = text.replace(/"(?:\\.|[^"\\])*"/g, "")
55
+ .replace(/\b(?:true|false|null|undefined)\b/g, "")
56
+ .replace(/-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g, "");
57
+ const arrayLike = /^\[\s*[{"]/.test(text)
58
+ || (text.startsWith("[") && /^[\s[\]{},:]*$/.test(arrayTokens)
59
+ && (/,\s*]?$/.test(text) || /\bundefined\b/.test(text) || !text.endsWith("]")));
60
+ if (objectLike || arrayLike) return { bodyJson: "", bodyText: fallback };
61
+ }
62
+ }
63
+ // Legacy HTML descriptions stay text. Preserve paragraph breaks and ordinary prose's
64
+ // bare '<' (e.g. "ages < 8"), without relying on browser-only DOM APIs in workers.
65
+ const plain = text
66
+ .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, "")
67
+ .replace(/<br\s*\/?\s*>/gi, "\n")
68
+ .replace(/<\/(?:p|div|h[1-6]|li)\s*>/gi, "\n\n")
69
+ .replace(/<\/?[a-z][^>]*>/gi, "")
70
+ .replace(/&(?:nbsp|amp|lt|gt|quot|apos|#39);/gi, (entity) => ({
71
+ "&nbsp;": " ", "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&apos;": "'", "&#39;": "'",
72
+ })[entity.toLowerCase()] ?? entity)
73
+ .replace(/[\t\r ]+/g, " ")
74
+ .replace(/ *\n */g, "\n")
75
+ .replace(/\n{3,}/g, "\n\n")
76
+ .trim();
77
+ return { bodyJson: "", bodyText: plain || fallback };
78
+ }
@@ -9,6 +9,8 @@ import { WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
9
9
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
10
10
  import { get as dbGet, upsert as dbUpsert, type DbEntity } from "@himalaya/local-db";
11
11
  import { isOfflineFailure, writeMayHaveLanded } from "../_shared/owner-load-error";
12
+ import { descriptionBody } from "../_shared/rich-body.js";
13
+ import { openRichLink } from "../_shared/rich-content-link.js";
12
14
 
13
15
  interface MemberAvatar {
14
16
  initials: string;
@@ -42,7 +44,8 @@ async function cachePutMembership(state: State, joined: boolean): Promise<void>
42
44
  noImage: state.noImage,
43
45
  detailScreenId: "himi_group",
44
46
  };
45
- await dbUpsert(ENTITY, { ...existing, id: state.id, joined: joined ? "true" : "false" });
47
+ await dbUpsert(ENTITY, { ...existing, id: state.id, description: state.description,
48
+ descriptionJson: state.descriptionJson, joined: joined ? "true" : "false" });
46
49
  } catch { /* capability not granted */ }
47
50
  }
48
51
 
@@ -51,6 +54,8 @@ interface State {
51
54
  id: string;
52
55
  name: string;
53
56
  description: string;
57
+ descriptionJson: string;
58
+ hasDescriptionJson: boolean;
54
59
  privacy: string;
55
60
  memberCount: string;
56
61
  /** Member avatar strip (photo or initials per member) — best-effort: the
@@ -132,6 +137,8 @@ const initial: State = {
132
137
  id: "",
133
138
  name: "",
134
139
  description: "",
140
+ descriptionJson: "",
141
+ hasDescriptionJson: false,
135
142
  privacy: "",
136
143
  memberCount: "",
137
144
  memberAvatars: [],
@@ -162,6 +169,7 @@ const svc = defineService<State, {
162
169
  load: (state: State, arg?: unknown) => Promise<State>;
163
170
  join: (state: State) => Promise<State>;
164
171
  leave: (state: State) => Promise<State>;
172
+ openLink: (state: State, arg?: unknown) => Promise<undefined>;
165
173
  }>("himiBrandedGroup", {
166
174
  state: initial,
167
175
  actions: {
@@ -179,12 +187,16 @@ const svc = defineService<State, {
179
187
  const privacyLabel = privacy[0].toUpperCase() + privacy.slice(1);
180
188
  const signedIn = Boolean(await signedInEmail());
181
189
  const memberAvatars = await memberAvatarsOf(id);
190
+ const body = descriptionBody(group.description, descriptionBody(group.teaser,
191
+ copyParam("group.descriptionFallback", "Join this group to connect with other members.")).bodyText);
182
192
  const loaded: State = {
183
193
  ...state,
184
194
  status: "ready",
185
195
  id,
186
196
  name,
187
- description: group.description || group.teaser || copyParam("group.descriptionFallback", "Join this group to connect with other members."),
197
+ description: body.bodyText,
198
+ descriptionJson: body.bodyJson,
199
+ hasDescriptionJson: body.bodyJson !== "",
188
200
  privacy: privacyLabel,
189
201
  memberCount: count == null ? "" : `${count} ${count === 1 ? "member" : "members"}`,
190
202
  memberAvatars,
@@ -209,12 +221,16 @@ const svc = defineService<State, {
209
221
  const name = (String(cached.title ?? cached.name ?? "Group")).trim() || "Group";
210
222
  const image = String(cached.image ?? "");
211
223
  const signedIn = Boolean(await signedInEmail());
224
+ const body = descriptionBody(cached.descriptionJson || cached.description || cached.subtitle,
225
+ copyParam("group.descriptionFallback", "Join this group to connect with other members."));
212
226
  const loaded: State = {
213
227
  ...state,
214
228
  status: "ready",
215
229
  id,
216
230
  name,
217
- description: String(cached.subtitle ?? cached.description ?? "Join this group to connect with other members."),
231
+ description: body.bodyText,
232
+ descriptionJson: body.bodyJson,
233
+ hasDescriptionJson: body.bodyJson !== "",
218
234
  privacy: "Unknown",
219
235
  memberCount: String(cached.meta ?? ""),
220
236
  image,
@@ -233,6 +249,7 @@ const svc = defineService<State, {
233
249
  return { ...state, id, status: "error", isLoading: false, notLoading: true, isErrored: true, notErrored: false, hideContent: true };
234
250
  }
235
251
  },
252
+ openLink: async (_state, arg) => { await openRichLink(arg); return undefined; },
236
253
  join: async (state) => {
237
254
  const signedIn = Boolean(await signedInEmail());
238
255
  if (!signedIn) return membershipState({ ...state, message: copyParam("group.joinSignInMessage", "Sign in from Profile to join groups.") }, false, false);