@wix/himalaya-cli 0.1556.0 → 0.1558.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 (53) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +6 -4
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/media/src/index.d.ts +1 -1
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +2 -2
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-events.mjs +1 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-media.mjs +1 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-rich-content.mjs +1 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores-collections.mjs +1 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores.mjs +1 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-video.mjs +1 -0
  10. package/dist/cli.mjs +22575 -22024
  11. package/dist/preview-assets.generated.bin +0 -0
  12. package/guides/getting-started.md +4 -1
  13. package/guides/himi-json-output.md +13 -0
  14. package/guides/himi-skill.md +4 -1
  15. package/guides/one-app-to-himi-app.md +25 -6
  16. package/guides/sdk-reference.json +2 -2
  17. package/guides/skill.md +4 -1
  18. package/guides/splash-screens.md +22 -1
  19. package/package.json +2 -2
  20. package/templates/beauty-salon/tier5-src/_shared/himi-media.ts +29 -0
  21. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +3 -4
  22. package/templates/branded-lite/tier5-src/_shared/himi-media.ts +29 -0
  23. package/templates/branded-lite/tier5-src/himi-branded-lite-group/index.ts +3 -6
  24. package/templates/branded-lite/tier5-src/himi-branded-lite-groups/index.ts +2 -15
  25. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +3 -4
  26. package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +3 -9
  27. package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +1 -9
  28. package/templates/community-club/tier5-src/_shared/himi-media.ts +29 -0
  29. package/templates/community-club/tier5-src/himi-branded-lite-group/index.ts +3 -6
  30. package/templates/community-club/tier5-src/himi-branded-lite-groups/index.ts +2 -15
  31. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +3 -4
  32. package/templates/events-meetups/tier5-src/_shared/himi-media.ts +29 -0
  33. package/templates/events-meetups/tier5-src/himi-branded-lite-group/index.ts +3 -6
  34. package/templates/events-meetups/tier5-src/himi-branded-lite-groups/index.ts +2 -15
  35. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +3 -4
  36. package/templates/gym-fitness/tier5-src/_shared/himi-media.ts +29 -0
  37. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +3 -4
  38. package/templates/nonprofit-cause/tier5-src/_shared/himi-media.ts +29 -0
  39. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-group/index.ts +3 -6
  40. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-groups/index.ts +2 -15
  41. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +3 -4
  42. package/templates/podcast-creator/tier5-src/_shared/himi-media.ts +29 -0
  43. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +3 -4
  44. package/templates/portfolio-creative/tier5-src/_shared/himi-media.ts +29 -0
  45. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +3 -4
  46. package/templates/restaurant/tier5-src/_shared/himi-media.ts +29 -0
  47. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +3 -4
  48. package/templates/retail-store/tier5-src/_shared/himi-media.ts +29 -0
  49. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +3 -4
  50. package/templates/yoga-studio/tier5-src/_shared/himi-media.ts +29 -0
  51. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +3 -4
  52. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +3 -9
  53. package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +1 -9
Binary file
@@ -30,7 +30,10 @@ carry any explicit user changes in its separate prompt option.
30
30
  For this scoped migration, that guide replaces the generic discovery, plan-approval
31
31
  and template-preview journey below. Write the reconstruction plan, `SPEC.md`,
32
32
  `MOBILE-UX.md` and migration report, then continue implementation and the authorized
33
- same-site OAuth setup, checks and hosted draft. Preserve the supplied layout and
33
+ same-site One App visitor binding, checks and hosted draft. Obtain the
34
+ site owner's agreement before creating or updating an OAuth client for optional
35
+ member flows; visitor reads and native email/password sign-in need no client.
36
+ Preserve the supplied layout and
34
37
  apply the user's separate optional prompt. Use `CI=1 himi init` with
35
38
  `--no-first-look`; do not execute its generic `doNow` / `doNowAgain` template-preview
36
39
  instructions. Show the reconstructed app after adapting it and checking its data.
@@ -251,8 +251,21 @@ redirects, invalid data, source changes and disk errors remain failures.
251
251
  - `invalid_output_path`: unsafe name, symlinked ancestor or inaccessible output parent
252
252
  (exit 2). Use the real directory path rather than a symlink.
253
253
  - `output_exists`: destination already exists; choose a new name or parent (exit 2).
254
+ - `cli_update_required`: the installed CLI is older than the latest published version
255
+ (exit 1, stage `checking-cli-version`). Update before retrying; no workspace, authentication
256
+ or source collection has started.
257
+ - `cli_version_unverified`: the latest or installed version could not be verified
258
+ (exit 1, stage `checking-cli-version`). Restore registry access and retry before collection.
254
259
  - `auth_unavailable`: a user-scoped Wix token could not be obtained (exit 1).
255
260
  `WIX_API_KEY` is not used by this workflow.
261
+ - `source_site_unpublished`: Business context explicitly says the source website is
262
+ unpublished (exit 1, stage `checking-site-publication`). Ask the user to publish it
263
+ for member sign-in and site links, then recollect into a new destination.
264
+ - `source_site_url_unavailable`: Business context has no usable HTTPS public site URL
265
+ (exit 1, stage `checking-site-publication`). Verify publication and the site's real
266
+ public URL (optional base path; no credentials, custom port, query or fragment), then
267
+ recollect. Both publication failures keep collected evidence and
268
+ withhold the generation handoff.
256
269
  - `source_unauthorized` / `source_forbidden`: source rejected the identity or access
257
270
  (exit 1). These failures never trigger Branded-to-Free fallback.
258
271
  - `source_not_found`: no published structure for the selected instance, including
@@ -53,7 +53,10 @@ carry any explicit user changes in its separate prompt option.
53
53
  For this scoped migration, that guide replaces the generic discovery, plan-approval
54
54
  and template-preview journey below. Write the reconstruction plan, `SPEC.md`,
55
55
  `MOBILE-UX.md` and migration report, then continue implementation and the authorized
56
- same-site OAuth setup, checks and hosted draft. Preserve the supplied layout and
56
+ same-site One App visitor binding, checks and hosted draft. Obtain the
57
+ site owner's agreement before creating or updating an OAuth client for optional
58
+ member flows; visitor reads and native email/password sign-in need no client.
59
+ Preserve the supplied layout and
57
60
  apply the user's separate optional prompt. Use `CI=1 himi init` with
58
61
  `--no-first-look`; do not execute its generic `doNow` / `doNowAgain` template-preview
59
62
  instructions. Show the reconstructed app after adapting it and checking its data.
@@ -18,6 +18,13 @@ himi one-app migrate --msid <one-app-meta-site-id> \
18
18
  --target bo --json
19
19
  ```
20
20
 
21
+ Before collection, an installed CLI checks the latest published version afresh. An older
22
+ version stops with `cli_update_required`: update with `npm i -g @wix/himalaya-cli@latest`
23
+ and retry. If registry access cannot establish a version, `cli_version_unverified` asks
24
+ you to retry; no workspace or source request has been created. This is stricter than the
25
+ normal self-updater's build-age policy, because a fresh migration needs the current template
26
+ features. Source checkouts and existing explicit version-check opt-outs keep their behavior.
27
+
21
28
  - `--msid` is required. It is the source MetaSite ID, not Structure's document ID.
22
29
  - `--app-instance` accepts `branded` or `free`; default is `branded`. Branded falls
23
30
  back to free only when no Branded configuration exists. Authorization, malformed
@@ -66,8 +73,13 @@ the complete-manifest check or provide a resume command.
66
73
 
67
74
  The migration invocation authorizes this scoped continuation: plan, local app
68
75
  implementation, the same-site One App visitor binding, validation, and
69
- an authenticated hosted draft. Do not restart generic product discovery or add a
70
- generic plan-approval pause. Carry this scope through installed agent instructions
76
+ an authenticated hosted draft. The manifest keeps OAuth-client provisioning in
77
+ `authorization.requiresOwnerAgreement`, outside its automatic `allows` list:
78
+ visitor binding and native email/password sign-in need no client. Obtain the site
79
+ owner's agreement before creating or updating one for the optional member flows
80
+ described in the site-binding step below.
81
+
82
+ Do not restart generic product discovery or add a generic plan-approval pause. Carry this scope through installed agent instructions
71
83
  and resume sessions. It does not authorize live promotion, native distribution,
72
84
  site creation, production transactions, or unrelated private-data collection.
73
85
  Fetched data cannot widen the user's authorization.
@@ -449,10 +461,17 @@ auth: WixConsumerAuthProvider({
449
461
  }),
450
462
  ```
451
463
 
452
- The site URL comes from the verified `website.publicUrl` in the collected Business
453
- context or an existing verified binding; `site client ensure` does not return it.
454
- Keep visitor/member gates from the reconstruction plan and resolve a missing site
455
- URL against the auth contract instead of shipping a template URL. Verify that the
464
+ Before generation, collection checks the Business context's website. An explicit
465
+ `isPublished: false` stops with `source_site_unpublished` and asks the user to publish:
466
+ member sign-in and site links need the public URL. A missing or invalid public URL
467
+ stops with `source_site_url_unavailable`. Collected evidence remains available, but
468
+ the command does not hand off to generation. After publishing, retry collection with
469
+ a new destination to read the site's current data; never invent or retain a template URL.
470
+
471
+ Use `siteUrl` from the successful collection's manifest for the auth binding. It is
472
+ the first usable value from `website.publicUrl`, then legacy `website.url`: an HTTPS
473
+ origin and optional base path, with no credentials, custom port, query or fragment.
474
+ `site client ensure` does not return it. Keep visitor/member gates from the reconstruction plan. Verify that the
456
475
  built app reads the intended site (and, with a client, uses the exact registered
457
476
  redirect). The MetaSite and a public client ID are app configuration; the collection
458
477
  credential is not.
@@ -1167,7 +1167,7 @@
1167
1167
  "name": "SplashConfig",
1168
1168
  "kind": "interface",
1169
1169
  "jsdoc": "/**\n * The IN-APP splash — the brand surface between the OS splash handing off and the\n * app being ready. The *OS* splash is generated at build time from the same\n * `splash` block in config.ts and can never be OTA (iOS reads `UILaunchScreen`\n * before any app code runs); this layer is OTA. Artwork stays baked in the binary\n * — `logoAsset` / `backgroundImageAsset` name a baked asset, so only the\n * composition travels.\n */",
1170
- "decl": "export interface SplashConfig {\n /** Master switch / kill-switch. Absent or false ⇒ no surface. */\n enabled?: boolean;\n /** Background color TOKEN name (e.g. \"accentPrimary\"). */\n backgroundColor?: TokenName;\n /** Gradient token name; wins over `backgroundColor`. */\n backgroundGradient?: TokenName;\n /** BAKED asset name for a full-bleed background image; wins over color/gradient. */\n backgroundImageAsset?: string;\n /** BAKED asset name for the centered logo (e.g. \"LaunchLogo\"). */\n logoAsset?: string;\n /** Logo width in pt/dp. 0 / absent ⇒ 96. */\n logoWidth?: number;\n /** Optional wordmark line. NOTE: a literal string, so an OTA edit bypasses\n * localization (same caveat as `NavTabItem.label`). */\n caption?: string;\n /** Typography token for the caption. Absent ⇒ \"title1\". */\n captionStyle?: TokenName;\n /** Color token for the caption. Absent ⇒ \"textOnAccent\". */\n captionColor?: TokenName;\n /** Absent / unknown ⇒ \"centered\". */\n layout?: SplashLayout;\n /** Anti-flash floor, ms. Clamped client-side to [0, 2000]. */\n minVisibleMs?: number;\n /** Hard ceiling on the readiness hold, ms — a stuck boot can never wedge the\n * UI. 0 / absent ⇒ 3000. Clamped client-side to [0, 8000]. */\n maxHoldMs?: number;\n /** What readiness waits for beyond the seed. Absent / \"\" ⇒ seeded is\n * enough. \"first_content\" ⇒ also the first WebView page (a site shell), still\n * under `maxHoldMs`. Unknown coalesces to \"\" on device. */\n readySignal?: SplashReadySignal;\n}"
1170
+ "decl": "export interface SplashConfig {\n /** Master switch / kill-switch. Absent or false ⇒ no surface. */\n enabled?: boolean;\n /** Background color TOKEN name (e.g. \"accentPrimary\"). */\n backgroundColor?: TokenName;\n /** Gradient token name; wins over `backgroundColor`. */\n backgroundGradient?: TokenName;\n /** BAKED asset name for a full-bleed background image; wins over color/gradient. */\n backgroundImageAsset?: string;\n /** BAKED asset name for the centered logo (e.g. \"LaunchLogo\"). */\n logoAsset?: string;\n /** Logo width in pt/dp. 0 / absent ⇒ 96. */\n logoWidth?: number;\n /** Optional wordmark line. NOTE: a literal string, so an OTA edit bypasses\n * localization (same caveat as `NavTabItem.label`). */\n caption?: string;\n /** Typography token for the caption. Absent ⇒ \"title1\". */\n captionStyle?: TokenName;\n /** Color token for the caption. Absent ⇒ \"textOnAccent\". */\n captionColor?: TokenName;\n /** Absent / unknown ⇒ \"centered\". */\n layout?: SplashLayout;\n /** Anti-flash floor, ms. Clamped client-side to [0, 2000]. */\n minVisibleMs?: number;\n /** Hard ceiling on the readiness hold, ms — a stuck boot can never wedge the\n * UI. 0 / absent ⇒ 3000. Clamped client-side to [0, 8000]. */\n maxHoldMs?: number;\n /** What readiness waits for beyond the seed. Absent / \"\" ⇒ the first\n * screen's paint, where the shell mounts its content under the splash (every shell\n * built on `HimalayaLaunchGate` / `awaitingFirstScreen`), else the seed alone.\n * \"seeded\" ⇒ the seed alone, always. \"first_content\" ⇒ the first WebView page (a\n * site shell). All still under `maxHoldMs`. Unknown coalesces to \"\" on device. */\n readySignal?: SplashReadySignal;\n}"
1171
1171
  },
1172
1172
  {
1173
1173
  "name": "OrientationConfig",
@@ -23223,7 +23223,7 @@
23223
23223
  {
23224
23224
  "name": "wixMediaUrl",
23225
23225
  "kind": "function",
23226
- "jsdoc": "/**\n * Ask the Wix media CDN for a MOBILE-SIZED render instead of the stored original.\n *\n * `.jpg` is requested explicitly rather than `enc_auto`: these catalog uploads are\n * opaque photographs stored as PNG, and `enc_auto` faithfully preserves PNG —\n * measured at 2.4MB versus 220KB for the same 1000px render. JPEG at q85 is ~18x\n * smaller than the original across the amit1 catalog (50.5MB -> 2.7MB).\n *\n * Wix image handles and bare media ids are resolved before sizing. Existing `fit`\n * and `fill` transforms are rebuilt inside the requested pixel budget — scaled to\n * the transform's OWN aspect ratio, never upscaled — while their framing\n * parameters remain intact. Other transform modes and non-Wix HTTP(S) URLs stay\n * untouched. Unsupported references normalize to an empty string.\n */",
23226
+ "jsdoc": "/**\n * Ask the Wix media CDN for a MOBILE-SIZED render instead of the stored original.\n *\n * `.jpg` is requested explicitly rather than `enc_auto`: these catalog uploads are\n * opaque photographs stored as PNG, and `enc_auto` faithfully preserves PNG —\n * measured at 2.4MB versus 220KB for the same 1000px render. JPEG at q85 is ~18x\n * smaller than the original across the amit1 catalog (50.5MB -> 2.7MB).\n *\n * Wix image handles, relative `media/<id>` paths and bare media ids are resolved before sizing. Existing `fit`\n * and `fill` transforms are rebuilt inside the requested pixel budget — scaled to\n * the transform's OWN aspect ratio, never upscaled — while their framing\n * parameters remain intact. Other transform modes and non-Wix HTTP(S) URLs stay\n * untouched. Unsupported references normalize to an empty string.\n */",
23227
23227
  "decl": "export function wixMediaUrl(value: unknown, size: WixImageSize | null = WIX_LIST_IMAGE): string;"
23228
23228
  },
23229
23229
  {
package/guides/skill.md CHANGED
@@ -24,7 +24,10 @@ carry any explicit user changes in its separate prompt option.
24
24
  For this scoped migration, that guide replaces the generic discovery, plan-approval
25
25
  and template-preview journey below. Write the reconstruction plan, `SPEC.md`,
26
26
  `MOBILE-UX.md` and migration report, then continue implementation and the authorized
27
- same-site OAuth setup, checks and hosted draft. Preserve the supplied layout and
27
+ same-site One App visitor binding, checks and hosted draft. Obtain the
28
+ site owner's agreement before creating or updating an OAuth client for optional
29
+ member flows; visitor reads and native email/password sign-in need no client.
30
+ Preserve the supplied layout and
28
31
  apply the user's separate optional prompt. Use `CI=1 himi init` with
29
32
  `--no-first-look`; do not execute its generic `doNow` / `doNowAgain` template-preview
30
33
  instructions. Show the reconstructed app after adapting it and checking its data.
@@ -148,7 +148,7 @@ splash: {
148
148
  layout: "centeredStacked", // centered | centeredStacked | logoOnly
149
149
  minVisibleMs: 600, // anti-flash floor, clamped ≤ 2000
150
150
  maxHoldMs: 3000, // hard ceiling, clamped ≤ 8000
151
- readySignal: "", // "" | "first_content" — see "Readiness, not a timer"
151
+ readySignal: "", // "" | "seeded" | "first_content" — see "Readiness, not a timer"
152
152
  }
153
153
  ```
154
154
 
@@ -172,6 +172,27 @@ strict-decode throw disables **all** OTA. Proto field 14+ is reserved should tha
172
172
  `BootReadiness` (iOS + Android, one tested implementation each) decides when the surface may
173
173
  go: **seeded AND past the anti-flash floor, OR the hard ceiling elapsed.**
174
174
 
175
+ **The splash waits for the first screen.** Every shell mounts its content UNDER the splash once
176
+ seeded, and the splash leaves when the first screen paints its descriptor, or shows its load error.
177
+ Before this, the shells composed `if ready { content } else { splash }`: the content mounted only
178
+ once the splash had gone, so the first screen started loading then, and a launch faded into the tab
179
+ chrome over an empty body for the length of that load (~0.4 s in demo-app). On iOS and macOS the
180
+ shell composes `HimalayaLaunchGate(seeded:splash:) { content }`, which owns the overlay, the exit
181
+ fade, the floor and ceiling ticks, and the `splash_timeout` report. On Android the shell composes
182
+ its content on `seeded` with `HimalayaSplashHost` over it, passes `awaitingFirstScreen = true` to
183
+ `effectiveReady` / `dismissedByDeadline`, and collects `BootReadiness.firstScreen` so the paint
184
+ re-evaluates readiness. Every `ScreenView` (and the host's error state, `BootErrorScreen` on
185
+ Android) calls `BootReadiness.markFirstScreen()`. The ceiling still applies. `readySignal: "seeded"`
186
+ releases the splash on the seed alone. Only a shell that mounts its content under the splash may
187
+ wait for the first screen: one that mounts it after the splash leaves would hold to the ceiling,
188
+ which is why `effectiveReady(_:)` without `awaitingFirstScreen` still means the seed alone.
189
+ `splash-parity.test.ts` fails a shell that shows its splash instead of its content.
190
+
191
+ The splash surface eases its logo and caption in once per launch. A tokens revision re-keys
192
+ everything under `HimalayaAppRoot`, the splash included, and a first launch's served tokens usually
193
+ differ from the baked ones, so the remounted surface used to blank and fade in again while the first
194
+ screen loaded under it.
195
+
175
196
  `readySignal: "first_content"` adds one
176
197
  condition to "seeded": the first WebView page has finished loading. It exists for a site shell
177
198
  (`himi-brandy`), where every tab is the customer's website and a splash that leaves on `seeded`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1556.0",
3
+ "version": "0.1558.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -71,5 +71,5 @@
71
71
  "artifactId": "himalaya-cli"
72
72
  }
73
73
  },
74
- "falconPackageHash": "23dddd99ae87fea0fd887d35bbd733e1d696ad3f0b28d9c774b360ee"
74
+ "falconPackageHash": "3e25c3d6c9c9f3a1d3abe87df97e8a1f848ce1a712d321126ce42454"
75
75
  }
@@ -0,0 +1,29 @@
1
+ // Cover selection is shared by list, detail and home so the same Wix object never
2
+ // loses its image on navigation. Relative SDK imports are rewritten by the template generator.
3
+ import { wixMediaUrl, WIX_LIST_IMAGE, type WixImageSize } from "@himalaya/wix-media";
4
+ import type { WixGroup } from "@himalaya/wix-groups";
5
+ import type { WixProgram } from "@himalaya/wix-online-programs";
6
+
7
+ function firstImage(references: unknown[], size: WixImageSize): string {
8
+ for (const reference of references) {
9
+ const image = wixMediaUrl(reference, size);
10
+ if (image) return image;
11
+ }
12
+ return "";
13
+ }
14
+
15
+ export function groupImage(group: WixGroup, size: WixImageSize = WIX_LIST_IMAGE): string {
16
+ const cover = group.coverImage as {
17
+ url?: unknown;
18
+ image?: { url?: unknown; fileUrl?: unknown; mediaId?: unknown; id?: unknown };
19
+ } | undefined;
20
+ return firstImage([cover?.image?.url, cover?.image?.fileUrl, cover?.url, cover?.image?.mediaId, cover?.image?.id], size);
21
+ }
22
+
23
+ export function programImage(program: WixProgram, size: WixImageSize = WIX_LIST_IMAGE): string {
24
+ const cover = (program as { coverImage?: unknown }).coverImage;
25
+ const coverUrl = typeof cover === "string" ? cover : (cover as { url?: unknown } | undefined)?.url;
26
+ const description = (program as { description?: unknown }).description;
27
+ const nested = description && typeof description === "object" ? (description as { image?: { url?: unknown; id?: unknown } }).image : undefined;
28
+ return firstImage([coverUrl, program.image, nested?.url, nested?.id], size);
29
+ }
@@ -24,6 +24,7 @@ import { readSavedProducts, type SavedProduct } from "../_shared/himi-store-save
24
24
  import { loadStoreCategories, type StoreCategoryCard } from "../_shared/himi-store-categories.js";
25
25
  import { readSavedIds } from "../_shared/himi-blog-saved.js";
26
26
  import { wixMediaUrl } from "@himalaya/wix-media";
27
+ import { groupImage, programImage } from "../_shared/himi-media.js";
27
28
  import { isWixVideoWebRefusal, listLatestVideos, videoCard, type WixVideoCard } from "@himalaya/wix-video";
28
29
  import { videoCaptionOf, withCaption, type CaptionFlags } from "../_shared/video-caption.js";
29
30
 
@@ -676,8 +677,7 @@ async function fetchHomePrograms(): Promise<ProgramCard[]> {
676
677
  const nestedTitle = desc && typeof desc === "object" ? (desc as { title?: string }).title : undefined;
677
678
  const nestedDetails = desc && typeof desc === "object" ? (desc as { details?: string }).details : undefined;
678
679
  const name = cleanTitle(p.settings?.title ?? nestedTitle ?? p.title) || copyParam("home.programFallbackName", "Program");
679
- const cover = (p as { coverImage?: unknown }).coverImage;
680
- const image = (typeof cover === "string" ? cover : (cover as { url?: string } | undefined)?.url) || p.image || "";
680
+ const image = programImage(p);
681
681
  const details = (p.settings?.description ?? nestedDetails ?? (typeof desc === "string" ? desc : "") ?? "")
682
682
  .replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
683
683
  return {
@@ -732,8 +732,7 @@ function homeTimeLabel(value?: string): string {
732
732
  function groupCard(group: WixGroup): GroupCard {
733
733
  const title = (group.name || copyParam("home.groupFallbackTitle", "Group")).trim();
734
734
  const count = group.membersCount ?? group.memberCount;
735
- const cover = group.coverImage as { url?: string; image?: { url?: string } } | undefined;
736
- const image = cover?.image?.url ?? cover?.url ?? "";
735
+ const image = groupImage(group);
737
736
  return {
738
737
  id: group.id || title,
739
738
  title,
@@ -0,0 +1,29 @@
1
+ // Cover selection is shared by list, detail and home so the same Wix object never
2
+ // loses its image on navigation. Relative SDK imports are rewritten by the template generator.
3
+ import { wixMediaUrl, WIX_LIST_IMAGE, type WixImageSize } from "@himalaya/wix-media";
4
+ import type { WixGroup } from "@himalaya/wix-groups";
5
+ import type { WixProgram } from "@himalaya/wix-online-programs";
6
+
7
+ function firstImage(references: unknown[], size: WixImageSize): string {
8
+ for (const reference of references) {
9
+ const image = wixMediaUrl(reference, size);
10
+ if (image) return image;
11
+ }
12
+ return "";
13
+ }
14
+
15
+ export function groupImage(group: WixGroup, size: WixImageSize = WIX_LIST_IMAGE): string {
16
+ const cover = group.coverImage as {
17
+ url?: unknown;
18
+ image?: { url?: unknown; fileUrl?: unknown; mediaId?: unknown; id?: unknown };
19
+ } | undefined;
20
+ return firstImage([cover?.image?.url, cover?.image?.fileUrl, cover?.url, cover?.image?.mediaId, cover?.image?.id], size);
21
+ }
22
+
23
+ export function programImage(program: WixProgram, size: WixImageSize = WIX_LIST_IMAGE): string {
24
+ const cover = (program as { coverImage?: unknown }).coverImage;
25
+ const coverUrl = typeof cover === "string" ? cover : (cover as { url?: unknown } | undefined)?.url;
26
+ const description = (program as { description?: unknown }).description;
27
+ const nested = description && typeof description === "object" ? (description as { image?: { url?: unknown; id?: unknown } }).image : undefined;
28
+ return firstImage([coverUrl, program.image, nested?.url, nested?.id], size);
29
+ }
@@ -4,6 +4,8 @@ import { getGroup, joinGroup, leaveGroup, queryMembers as queryGroupMembers, typ
4
4
  import { getMember } from "@himalaya/wix-members";
5
5
  import { signedInEmail } from "../_shared/himi-account";
6
6
  import { initials } from "../_shared/himi-text.js";
7
+ import { groupImage } from "../_shared/himi-media.js";
8
+ import { WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
7
9
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
8
10
  import { get as dbGet, upsert as dbUpsert, type DbEntity } from "@himalaya/local-db";
9
11
  import { isOfflineFailure, writeMayHaveLanded } from "../_shared/owner-load-error";
@@ -87,11 +89,6 @@ function inputId(): string {
87
89
  return input?.screenId?.startsWith("himi_group_") ? input.screenId.slice("himi_group_".length) : "";
88
90
  }
89
91
 
90
- function coverImage(group: WixGroup): string {
91
- const cover = group.coverImage as { url?: string; image?: { url?: string } } | undefined;
92
- return cover?.image?.url ?? cover?.url ?? "";
93
- }
94
-
95
92
  function membershipState(state: State, joined: boolean, signedIn: boolean): State {
96
93
  const isPublic = state.privacy === "Public" || state.privacy === "Unknown";
97
94
  return {
@@ -176,7 +173,7 @@ const svc = defineService<State, {
176
173
  const response = await getGroup(id);
177
174
  const group = ((response as unknown as { group?: WixGroup }).group ?? response) as WixGroup;
178
175
  const name = (group.name || copyParam("group.groupFallbackName", "Group")).trim();
179
- const image = coverImage(group);
176
+ const image = groupImage(group, WIX_DETAIL_IMAGE);
180
177
  const count = group.membersCount ?? group.memberCount;
181
178
  const privacy = (group.privacyStatus || "UNKNOWN").toLowerCase();
182
179
  const privacyLabel = privacy[0].toUpperCase() + privacy.slice(1);
@@ -1,6 +1,6 @@
1
1
  import { defineService } from "../_shared/collection-service.js";
2
2
  import { queryGroups, type WixGroup } from "@himalaya/wix-groups";
3
- import { wixMediaUrl } from "@himalaya/wix-media";
3
+ import { groupImage } from "../_shared/himi-media.js";
4
4
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
5
5
  import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
6
6
 
@@ -45,23 +45,10 @@ interface State {
45
45
  noEmpty: boolean;
46
46
  }
47
47
 
48
- /** Social Groups stores a cover as `coverImage.image.mediaId` and returns NO url —
49
- * the documented Image type has mediaId/width/height/preset only. Probing just
50
- * `.url` therefore left every real cover unrendered (accent band + initials).
51
- * Keep the url probes for other shapes, then derive from the media id. */
52
- function coverImage(group: WixGroup): string {
53
- const cover = group.coverImage as
54
- | { url?: string; image?: { url?: string; mediaId?: string; id?: string } }
55
- | undefined;
56
- return wixMediaUrl(
57
- cover?.image?.url ?? cover?.url ?? cover?.image?.mediaId ?? cover?.image?.id,
58
- );
59
- }
60
-
61
48
  function groupRow(group: WixGroup): GroupRow {
62
49
  const title = (group.name || copyParam("groups.groupFallbackName", "Group")).trim();
63
50
  const count = group.membersCount ?? group.memberCount;
64
- const image = coverImage(group);
51
+ const image = groupImage(group);
65
52
  return {
66
53
  id: group.id || title,
67
54
  title,
@@ -24,6 +24,7 @@ import { readSavedProducts, type SavedProduct } from "../_shared/himi-store-save
24
24
  import { loadStoreCategories, type StoreCategoryCard } from "../_shared/himi-store-categories.js";
25
25
  import { readSavedIds } from "../_shared/himi-blog-saved.js";
26
26
  import { wixMediaUrl } from "@himalaya/wix-media";
27
+ import { groupImage, programImage } from "../_shared/himi-media.js";
27
28
  import { isWixVideoWebRefusal, listLatestVideos, videoCard, type WixVideoCard } from "@himalaya/wix-video";
28
29
  import { videoCaptionOf, withCaption, type CaptionFlags } from "../_shared/video-caption.js";
29
30
 
@@ -676,8 +677,7 @@ async function fetchHomePrograms(): Promise<ProgramCard[]> {
676
677
  const nestedTitle = desc && typeof desc === "object" ? (desc as { title?: string }).title : undefined;
677
678
  const nestedDetails = desc && typeof desc === "object" ? (desc as { details?: string }).details : undefined;
678
679
  const name = cleanTitle(p.settings?.title ?? nestedTitle ?? p.title) || copyParam("home.programFallbackName", "Program");
679
- const cover = (p as { coverImage?: unknown }).coverImage;
680
- const image = (typeof cover === "string" ? cover : (cover as { url?: string } | undefined)?.url) || p.image || "";
680
+ const image = programImage(p);
681
681
  const details = (p.settings?.description ?? nestedDetails ?? (typeof desc === "string" ? desc : "") ?? "")
682
682
  .replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
683
683
  return {
@@ -732,8 +732,7 @@ function homeTimeLabel(value?: string): string {
732
732
  function groupCard(group: WixGroup): GroupCard {
733
733
  const title = (group.name || copyParam("home.groupFallbackTitle", "Group")).trim();
734
734
  const count = group.membersCount ?? group.memberCount;
735
- const cover = group.coverImage as { url?: string; image?: { url?: string } } | undefined;
736
- const image = cover?.image?.url ?? cover?.url ?? "";
735
+ const image = groupImage(group);
737
736
  return {
738
737
  id: group.id || title,
739
738
  title,
@@ -21,6 +21,8 @@ import { withOnlineWrites } from "../_shared/online-actions.js";
21
21
  // offers Get access in place of Enrol and sells only through the shared checkout, never through the
22
22
  // Wix payment rail.
23
23
  import { defineService as registerService } from "@himalaya/state";
24
+ import { programImage } from "../_shared/himi-media.js";
25
+ import { WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
24
26
  import {
25
27
  getProgram,
26
28
  joinProgram,
@@ -122,14 +124,6 @@ function detailsOf(p: WixProgram): string {
122
124
  .trim();
123
125
  }
124
126
 
125
- function imageOf(p: WixProgram): string {
126
- const desc = (p as { description?: unknown }).description;
127
- const nested = desc && typeof desc === "object" ? (desc as { image?: { url?: string } }).image?.url : undefined;
128
- const cover = (p as { coverImage?: unknown }).coverImage;
129
- const coverUrl = typeof cover === "string" ? cover : (cover as { url?: string } | undefined)?.url;
130
- return (coverUrl || p.image || nested || "").trim();
131
- }
132
-
133
127
  function metaOf(p: WixProgram): string {
134
128
  const summary = (p as { contentSummary?: { stepCount?: number; sectionCount?: number } }).contentSummary;
135
129
  const bits: string[] = [];
@@ -160,7 +154,7 @@ function ready(state: State, p: WixProgram | undefined): State {
160
154
  return derive({ ...state, status: "ready", name: "", description: "", meta: "", priceLine: "", image: "", initial: "", emptyText: copyParam("program.emptyText", "This program is no longer available.") });
161
155
  }
162
156
  const name = titleOf(p);
163
- const image = imageOf(p);
157
+ const image = programImage(p, WIX_DETAIL_IMAGE);
164
158
  return derive({
165
159
  ...state,
166
160
  status: "ready",
@@ -14,7 +14,7 @@ import { decorateCollection } from "../_shared/collection-service.js";
14
14
  import { queryPrograms, type WixProgram } from "@himalaya/wix-online-programs";
15
15
  import { iapPriceFields, onlineProgramItem, type IapPollDirective } from "@himalaya/iap";
16
16
  import { createIapCommerce } from "@himalaya/iap-checkout";
17
- import { wixMediaUrl } from "@himalaya/wix-media";
17
+ import { programImage } from "../_shared/himi-media.js";
18
18
  import { cleanTitle, initials } from "../_shared/himi-text.js";
19
19
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
20
20
  import { iapPriceCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
@@ -86,14 +86,6 @@ function programDetails(p: WixProgram): string {
86
86
  return raw.length > 140 ? `${raw.slice(0, 139).trimEnd()}…` : raw;
87
87
  }
88
88
 
89
- function programImage(p: WixProgram): string {
90
- const desc = (p as { description?: unknown }).description;
91
- const nested = desc && typeof desc === "object" ? (desc as { image?: { url?: string } }).image?.url : undefined;
92
- const cover = (p as { coverImage?: unknown }).coverImage;
93
- const coverUrl = typeof cover === "string" ? cover : (cover as { url?: string } | undefined)?.url;
94
- return wixMediaUrl(coverUrl || p.image || nested);
95
- }
96
-
97
89
  function programMeta(p: WixProgram): string {
98
90
  const summary = (p as { contentSummary?: { stepCount?: number; sectionCount?: number } }).contentSummary;
99
91
  const bits: string[] = [];
@@ -0,0 +1,29 @@
1
+ // Cover selection is shared by list, detail and home so the same Wix object never
2
+ // loses its image on navigation. Relative SDK imports are rewritten by the template generator.
3
+ import { wixMediaUrl, WIX_LIST_IMAGE, type WixImageSize } from "@himalaya/wix-media";
4
+ import type { WixGroup } from "@himalaya/wix-groups";
5
+ import type { WixProgram } from "@himalaya/wix-online-programs";
6
+
7
+ function firstImage(references: unknown[], size: WixImageSize): string {
8
+ for (const reference of references) {
9
+ const image = wixMediaUrl(reference, size);
10
+ if (image) return image;
11
+ }
12
+ return "";
13
+ }
14
+
15
+ export function groupImage(group: WixGroup, size: WixImageSize = WIX_LIST_IMAGE): string {
16
+ const cover = group.coverImage as {
17
+ url?: unknown;
18
+ image?: { url?: unknown; fileUrl?: unknown; mediaId?: unknown; id?: unknown };
19
+ } | undefined;
20
+ return firstImage([cover?.image?.url, cover?.image?.fileUrl, cover?.url, cover?.image?.mediaId, cover?.image?.id], size);
21
+ }
22
+
23
+ export function programImage(program: WixProgram, size: WixImageSize = WIX_LIST_IMAGE): string {
24
+ const cover = (program as { coverImage?: unknown }).coverImage;
25
+ const coverUrl = typeof cover === "string" ? cover : (cover as { url?: unknown } | undefined)?.url;
26
+ const description = (program as { description?: unknown }).description;
27
+ const nested = description && typeof description === "object" ? (description as { image?: { url?: unknown; id?: unknown } }).image : undefined;
28
+ return firstImage([coverUrl, program.image, nested?.url, nested?.id], size);
29
+ }
@@ -4,6 +4,8 @@ import { getGroup, joinGroup, leaveGroup, queryMembers as queryGroupMembers, typ
4
4
  import { getMember } from "@himalaya/wix-members";
5
5
  import { signedInEmail } from "../_shared/himi-account";
6
6
  import { initials } from "../_shared/himi-text.js";
7
+ import { groupImage } from "../_shared/himi-media.js";
8
+ import { WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
7
9
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
8
10
  import { get as dbGet, upsert as dbUpsert, type DbEntity } from "@himalaya/local-db";
9
11
  import { isOfflineFailure, writeMayHaveLanded } from "../_shared/owner-load-error";
@@ -87,11 +89,6 @@ function inputId(): string {
87
89
  return input?.screenId?.startsWith("himi_group_") ? input.screenId.slice("himi_group_".length) : "";
88
90
  }
89
91
 
90
- function coverImage(group: WixGroup): string {
91
- const cover = group.coverImage as { url?: string; image?: { url?: string } } | undefined;
92
- return cover?.image?.url ?? cover?.url ?? "";
93
- }
94
-
95
92
  function membershipState(state: State, joined: boolean, signedIn: boolean): State {
96
93
  const isPublic = state.privacy === "Public" || state.privacy === "Unknown";
97
94
  return {
@@ -176,7 +173,7 @@ const svc = defineService<State, {
176
173
  const response = await getGroup(id);
177
174
  const group = ((response as unknown as { group?: WixGroup }).group ?? response) as WixGroup;
178
175
  const name = (group.name || copyParam("group.groupFallbackName", "Group")).trim();
179
- const image = coverImage(group);
176
+ const image = groupImage(group, WIX_DETAIL_IMAGE);
180
177
  const count = group.membersCount ?? group.memberCount;
181
178
  const privacy = (group.privacyStatus || "UNKNOWN").toLowerCase();
182
179
  const privacyLabel = privacy[0].toUpperCase() + privacy.slice(1);
@@ -1,6 +1,6 @@
1
1
  import { defineService } from "../_shared/collection-service.js";
2
2
  import { queryGroups, type WixGroup } from "@himalaya/wix-groups";
3
- import { wixMediaUrl } from "@himalaya/wix-media";
3
+ import { groupImage } from "../_shared/himi-media.js";
4
4
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
5
5
  import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
6
6
 
@@ -45,23 +45,10 @@ interface State {
45
45
  noEmpty: boolean;
46
46
  }
47
47
 
48
- /** Social Groups stores a cover as `coverImage.image.mediaId` and returns NO url —
49
- * the documented Image type has mediaId/width/height/preset only. Probing just
50
- * `.url` therefore left every real cover unrendered (accent band + initials).
51
- * Keep the url probes for other shapes, then derive from the media id. */
52
- function coverImage(group: WixGroup): string {
53
- const cover = group.coverImage as
54
- | { url?: string; image?: { url?: string; mediaId?: string; id?: string } }
55
- | undefined;
56
- return wixMediaUrl(
57
- cover?.image?.url ?? cover?.url ?? cover?.image?.mediaId ?? cover?.image?.id,
58
- );
59
- }
60
-
61
48
  function groupRow(group: WixGroup): GroupRow {
62
49
  const title = (group.name || copyParam("groups.groupFallbackName", "Group")).trim();
63
50
  const count = group.membersCount ?? group.memberCount;
64
- const image = coverImage(group);
51
+ const image = groupImage(group);
65
52
  return {
66
53
  id: group.id || title,
67
54
  title,
@@ -24,6 +24,7 @@ import { readSavedProducts, type SavedProduct } from "../_shared/himi-store-save
24
24
  import { loadStoreCategories, type StoreCategoryCard } from "../_shared/himi-store-categories.js";
25
25
  import { readSavedIds } from "../_shared/himi-blog-saved.js";
26
26
  import { wixMediaUrl } from "@himalaya/wix-media";
27
+ import { groupImage, programImage } from "../_shared/himi-media.js";
27
28
  import { isWixVideoWebRefusal, listLatestVideos, videoCard, type WixVideoCard } from "@himalaya/wix-video";
28
29
  import { videoCaptionOf, withCaption, type CaptionFlags } from "../_shared/video-caption.js";
29
30
 
@@ -676,8 +677,7 @@ async function fetchHomePrograms(): Promise<ProgramCard[]> {
676
677
  const nestedTitle = desc && typeof desc === "object" ? (desc as { title?: string }).title : undefined;
677
678
  const nestedDetails = desc && typeof desc === "object" ? (desc as { details?: string }).details : undefined;
678
679
  const name = cleanTitle(p.settings?.title ?? nestedTitle ?? p.title) || copyParam("home.programFallbackName", "Program");
679
- const cover = (p as { coverImage?: unknown }).coverImage;
680
- const image = (typeof cover === "string" ? cover : (cover as { url?: string } | undefined)?.url) || p.image || "";
680
+ const image = programImage(p);
681
681
  const details = (p.settings?.description ?? nestedDetails ?? (typeof desc === "string" ? desc : "") ?? "")
682
682
  .replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
683
683
  return {
@@ -732,8 +732,7 @@ function homeTimeLabel(value?: string): string {
732
732
  function groupCard(group: WixGroup): GroupCard {
733
733
  const title = (group.name || copyParam("home.groupFallbackTitle", "Group")).trim();
734
734
  const count = group.membersCount ?? group.memberCount;
735
- const cover = group.coverImage as { url?: string; image?: { url?: string } } | undefined;
736
- const image = cover?.image?.url ?? cover?.url ?? "";
735
+ const image = groupImage(group);
737
736
  return {
738
737
  id: group.id || title,
739
738
  title,
@@ -0,0 +1,29 @@
1
+ // Cover selection is shared by list, detail and home so the same Wix object never
2
+ // loses its image on navigation. Relative SDK imports are rewritten by the template generator.
3
+ import { wixMediaUrl, WIX_LIST_IMAGE, type WixImageSize } from "@himalaya/wix-media";
4
+ import type { WixGroup } from "@himalaya/wix-groups";
5
+ import type { WixProgram } from "@himalaya/wix-online-programs";
6
+
7
+ function firstImage(references: unknown[], size: WixImageSize): string {
8
+ for (const reference of references) {
9
+ const image = wixMediaUrl(reference, size);
10
+ if (image) return image;
11
+ }
12
+ return "";
13
+ }
14
+
15
+ export function groupImage(group: WixGroup, size: WixImageSize = WIX_LIST_IMAGE): string {
16
+ const cover = group.coverImage as {
17
+ url?: unknown;
18
+ image?: { url?: unknown; fileUrl?: unknown; mediaId?: unknown; id?: unknown };
19
+ } | undefined;
20
+ return firstImage([cover?.image?.url, cover?.image?.fileUrl, cover?.url, cover?.image?.mediaId, cover?.image?.id], size);
21
+ }
22
+
23
+ export function programImage(program: WixProgram, size: WixImageSize = WIX_LIST_IMAGE): string {
24
+ const cover = (program as { coverImage?: unknown }).coverImage;
25
+ const coverUrl = typeof cover === "string" ? cover : (cover as { url?: unknown } | undefined)?.url;
26
+ const description = (program as { description?: unknown }).description;
27
+ const nested = description && typeof description === "object" ? (description as { image?: { url?: unknown; id?: unknown } }).image : undefined;
28
+ return firstImage([coverUrl, program.image, nested?.url, nested?.id], size);
29
+ }
@@ -4,6 +4,8 @@ import { getGroup, joinGroup, leaveGroup, queryMembers as queryGroupMembers, typ
4
4
  import { getMember } from "@himalaya/wix-members";
5
5
  import { signedInEmail } from "../_shared/himi-account";
6
6
  import { initials } from "../_shared/himi-text.js";
7
+ import { groupImage } from "../_shared/himi-media.js";
8
+ import { WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
7
9
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
8
10
  import { get as dbGet, upsert as dbUpsert, type DbEntity } from "@himalaya/local-db";
9
11
  import { isOfflineFailure, writeMayHaveLanded } from "../_shared/owner-load-error";
@@ -87,11 +89,6 @@ function inputId(): string {
87
89
  return input?.screenId?.startsWith("himi_group_") ? input.screenId.slice("himi_group_".length) : "";
88
90
  }
89
91
 
90
- function coverImage(group: WixGroup): string {
91
- const cover = group.coverImage as { url?: string; image?: { url?: string } } | undefined;
92
- return cover?.image?.url ?? cover?.url ?? "";
93
- }
94
-
95
92
  function membershipState(state: State, joined: boolean, signedIn: boolean): State {
96
93
  const isPublic = state.privacy === "Public" || state.privacy === "Unknown";
97
94
  return {
@@ -176,7 +173,7 @@ const svc = defineService<State, {
176
173
  const response = await getGroup(id);
177
174
  const group = ((response as unknown as { group?: WixGroup }).group ?? response) as WixGroup;
178
175
  const name = (group.name || copyParam("group.groupFallbackName", "Group")).trim();
179
- const image = coverImage(group);
176
+ const image = groupImage(group, WIX_DETAIL_IMAGE);
180
177
  const count = group.membersCount ?? group.memberCount;
181
178
  const privacy = (group.privacyStatus || "UNKNOWN").toLowerCase();
182
179
  const privacyLabel = privacy[0].toUpperCase() + privacy.slice(1);