@wix/himalaya-cli 0.1145.0 → 0.1147.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 (100) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/comments-v1/src/extensions.d.ts +4 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/forum/src/extensions.d.ts +10 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-comments.mjs +5 -2
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-forum.mjs +13 -0
  5. package/dist/cli.mjs +75 -19
  6. package/dist/preview-assets.generated.bin +0 -0
  7. package/guides/headless-loop.md +34 -0
  8. package/guides/himi-mcp.md +2 -0
  9. package/guides/sdk-reference.json +13 -1
  10. package/guides/tier-5-add-a-screen.md +4 -2
  11. package/package.json +2 -2
  12. package/templates/beauty-salon/dev/index.ts +62 -3
  13. package/templates/beauty-salon/tier5-src/_shared/rich-body.ts +10 -4
  14. package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +12 -2
  15. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +6 -1
  16. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +35 -4
  17. package/templates/beauty-salon-owner/tier5-src/_shared/rich-body.ts +10 -4
  18. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
  19. package/templates/branded-lite/dev/index.ts +273 -12
  20. package/templates/branded-lite/tier5-src/_shared/rich-body.ts +10 -4
  21. package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +12 -2
  22. package/templates/branded-lite/tier5-src/himi-branded-lite-cms-collection/index.ts +43 -3
  23. package/templates/branded-lite/tier5-src/himi-branded-lite-faq/index.ts +69 -13
  24. package/templates/branded-lite/tier5-src/himi-branded-lite-feed/index.ts +23 -3
  25. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +6 -1
  26. package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +40 -3
  27. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +35 -4
  28. package/templates/branded-lite/tier5-src/wix-spaces-forum-post/index.ts +29 -5
  29. package/templates/community-club/dev/index.ts +135 -5
  30. package/templates/community-club/tier5-src/_shared/rich-body.ts +10 -4
  31. package/templates/community-club/tier5-src/himi-blog-post/index.ts +12 -2
  32. package/templates/community-club/tier5-src/himi-branded-lite-cms-collection/index.ts +43 -3
  33. package/templates/community-club/tier5-src/himi-branded-lite-feed/index.ts +23 -3
  34. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +6 -1
  35. package/templates/community-club/tier5-src/himi-events-detail/index.ts +40 -3
  36. package/templates/community-club-owner/tier5-src/_shared/rich-body.ts +10 -4
  37. package/templates/community-club-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
  38. package/templates/events-meetups/dev/index.ts +91 -4
  39. package/templates/events-meetups/tier5-src/_shared/rich-body.ts +10 -4
  40. package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +12 -2
  41. package/templates/events-meetups/tier5-src/himi-branded-lite-feed/index.ts +23 -3
  42. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +6 -1
  43. package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +40 -3
  44. package/templates/events-meetups-owner/tier5-src/_shared/rich-body.ts +10 -4
  45. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
  46. package/templates/gym-fitness/dev/index.ts +92 -4
  47. package/templates/gym-fitness/tier5-src/_shared/rich-body.ts +10 -4
  48. package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +12 -2
  49. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +6 -1
  50. package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +40 -3
  51. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +35 -4
  52. package/templates/gym-fitness-owner/tier5-src/_shared/rich-body.ts +10 -4
  53. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
  54. package/templates/nonprofit-cause/dev/index.ts +91 -4
  55. package/templates/nonprofit-cause/tier5-src/_shared/rich-body.ts +10 -4
  56. package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +12 -2
  57. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-feed/index.ts +23 -3
  58. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +6 -1
  59. package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +40 -3
  60. package/templates/nonprofit-cause-owner/tier5-src/_shared/rich-body.ts +10 -4
  61. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
  62. package/templates/owner-lite/tier5-src/_shared/rich-body.ts +10 -4
  63. package/templates/owner-lite/tier5-src/himi-owner-lite-home/index.ts +6 -1
  64. package/templates/podcast-creator/dev/index.ts +91 -4
  65. package/templates/podcast-creator/tier5-src/_shared/rich-body.ts +10 -4
  66. package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +12 -2
  67. package/templates/podcast-creator/tier5-src/himi-branded-lite-feed/index.ts +23 -3
  68. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +6 -1
  69. package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +40 -3
  70. package/templates/podcast-creator-owner/tier5-src/_shared/rich-body.ts +10 -4
  71. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
  72. package/templates/portfolio-creative/dev/index.ts +92 -4
  73. package/templates/portfolio-creative/tier5-src/_shared/rich-body.ts +10 -4
  74. package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +12 -2
  75. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +6 -1
  76. package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +40 -3
  77. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +35 -4
  78. package/templates/portfolio-creative-owner/tier5-src/_shared/rich-body.ts +10 -4
  79. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
  80. package/templates/restaurant/dev/index.ts +60 -2
  81. package/templates/restaurant/tier5-src/_shared/rich-body.ts +10 -4
  82. package/templates/restaurant/tier5-src/himi-blog-post/index.ts +12 -2
  83. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +6 -1
  84. package/templates/restaurant/tier5-src/himi-events-detail/index.ts +40 -3
  85. package/templates/restaurant-owner/tier5-src/_shared/rich-body.ts +10 -4
  86. package/templates/restaurant-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
  87. package/templates/retail-store/dev/index.ts +62 -3
  88. package/templates/retail-store/tier5-src/_shared/rich-body.ts +10 -4
  89. package/templates/retail-store/tier5-src/himi-blog-post/index.ts +12 -2
  90. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +6 -1
  91. package/templates/retail-store/tier5-src/himi-store-product/index.ts +35 -4
  92. package/templates/retail-store-owner/tier5-src/_shared/rich-body.ts +10 -4
  93. package/templates/retail-store-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
  94. package/templates/yoga-studio/dev/index.ts +60 -2
  95. package/templates/yoga-studio/tier5-src/_shared/rich-body.ts +10 -4
  96. package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +12 -2
  97. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +6 -1
  98. package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +40 -3
  99. package/templates/yoga-studio-owner/tier5-src/_shared/rich-body.ts +10 -4
  100. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
@@ -35,6 +35,8 @@ import {
35
35
  } from "../_shared/himi-events";
36
36
  import { initials } from "../_shared/himi-text.js";
37
37
  import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
38
+ import { openRichLink } from "../_shared/rich-content-link.js";
39
+ import { richBody } from "../_shared/rich-body.js";
38
40
 
39
41
  interface MapMarker {
40
42
  latitude: number;
@@ -88,7 +90,13 @@ interface S {
88
90
  initial: string;
89
91
  when: string;
90
92
  location: string;
93
+ /** The plain-text description: RichContent's fallback, the calendar notes, and what shows with no document. */
91
94
  description: string;
95
+ /** [rico] #3372 — the v3 Ricos `description` (read with `fields=TEXTS`) as ONE document string for
96
+ * RichContent; "" when the event has none or the encoder rejected it. */
97
+ descriptionJson: string;
98
+ /** descriptionJson !== "" — gates the plain text, because RichContent draws nothing for "". */
99
+ hasDescriptionJson: boolean;
92
100
  image: string;
93
101
  hasImage: boolean;
94
102
  noImage: boolean;
@@ -212,6 +220,7 @@ function derive(s: S): S {
212
220
  const hasAttendees = s.attendeeAvatars.length > 0;
213
221
  return {
214
222
  ...s,
223
+ hasDescriptionJson: s.descriptionJson !== "",
215
224
  noMap: !s.mapUrl,
216
225
  hasMapMarkers,
217
226
  noMapMarkers: !hasMapMarkers,
@@ -357,6 +366,14 @@ async function attendeeSummary(eventId: string): Promise<{ goingLabel: string; a
357
366
  }
358
367
  }
359
368
 
369
+ /** The description pair: the v3 Ricos `description` for RichContent, and its plain text. A string description
370
+ * (or none) keeps `eventDescription`'s legacy read, with no document. */
371
+ function eventBody(e: WixEvent): { description: string; descriptionJson: string } {
372
+ const legacy = eventDescription(e);
373
+ const { bodyJson, bodyText } = richBody((e as unknown as { description?: unknown }).description, legacy);
374
+ return { description: bodyText, descriptionJson: bodyJson };
375
+ }
376
+
360
377
  /** getEvent returns the slim BASE fieldset (no `registration`); enrich from the
361
378
  * richer query when the type is unknown — best-effort, find-by-id. */
362
379
  async function enrichRegistration(id: string, e: WixEvent): Promise<WixEvent> {
@@ -364,7 +381,11 @@ async function enrichRegistration(id: string, e: WixEvent): Promise<WixEvent> {
364
381
  try {
365
382
  const list = await queryEvents({ limit: 100 });
366
383
  const match = (list.events ?? []).find((candidate) => candidate.id === id);
367
- return match ?? e;
384
+ if (!match) return e;
385
+ // [rico] #3372 — the query's rows are read without TEXTS, so they carry no Ricos `description`: keep the
386
+ // one this detail fetched, or the enrichment would silently drop the document on every real site.
387
+ const description = (e as unknown as { description?: unknown }).description;
388
+ return description === undefined ? match : ({ ...match, description } as unknown as WixEvent);
368
389
  } catch {
369
390
  return e;
370
391
  }
@@ -377,6 +398,8 @@ const svc = defineService<S, {
377
398
  openRegistration: (s: S) => Promise<S>;
378
399
  share: (s: S) => Promise<S>;
379
400
  addToCalendar: (s: S) => Promise<S>;
401
+ /** [rico] #3372 — RichContent's on_link: `{ url }` of the tapped link. */
402
+ openLink: (s: S, arg?: unknown) => Promise<S>;
380
403
  }>("himiEventDetail", {
381
404
  state: derive({
382
405
  status: "loading",
@@ -386,6 +409,8 @@ const svc = defineService<S, {
386
409
  when: "",
387
410
  location: "",
388
411
  description: "",
412
+ descriptionJson: "",
413
+ hasDescriptionJson: false,
389
414
  image: "",
390
415
  hasImage: false,
391
416
  noImage: true,
@@ -433,7 +458,8 @@ const svc = defineService<S, {
433
458
  const id = s.id || inputId();
434
459
  if (!id) return derive({ ...s, status: "ready", errored: true });
435
460
  try {
436
- const res = await getEvent(id);
461
+ // TEXTS adds the Ricos `description` to the base fieldset (a bare read returns none).
462
+ const res = await getEvent(id, { fields: ["TEXTS"] });
437
463
  let e = ((res as unknown as { event?: WixEvent }).event ?? res) as WixEvent;
438
464
  e = await enrichRegistration(id, e);
439
465
  const attendees = await attendeeSummary(id);
@@ -460,7 +486,9 @@ const svc = defineService<S, {
460
486
  initial: (title.trim()[0] || "E").toUpperCase(),
461
487
  when: eventWhen(e),
462
488
  location: eventLocation(e),
463
- description: eventDescription(e),
489
+ // The Ricos description when the event has one (its own text is the plain copy); the legacy
490
+ // string fields otherwise.
491
+ ...eventBody(e),
464
492
  image,
465
493
  hasImage: !!image,
466
494
  noImage: !image,
@@ -491,6 +519,9 @@ const svc = defineService<S, {
491
519
  initial: (title.trim()[0] || "E").toUpperCase(),
492
520
  when: cached.dateLine,
493
521
  location: cached.location,
522
+ // The cached list row carries no description: an offline retry of THIS event keeps the pair it
523
+ // already shows, and any other event's is cleared together, so neither half can linger alone.
524
+ ...(s.id === id ? {} : { description: "", descriptionJson: "" }),
494
525
  image: cached.image,
495
526
  hasImage: !!cached.image,
496
527
  noImage: !cached.image,
@@ -665,6 +696,12 @@ const svc = defineService<S, {
665
696
  return derive({ ...s, actionMessage: copyParam("event.calendarAddFailed", "Could not add this event to calendar."), isAddingCalendar: false });
666
697
  }
667
698
  },
699
+ // Only http(s), mailto and tel links reach open_url; a refused link, or one nothing opened, says so, and a
700
+ // link that opens clears an earlier message.
701
+ openLink: async (s, arg) => derive({
702
+ ...s,
703
+ actionMessage: (await openRichLink(arg)) ? "" : copyParam("event.linkOpenFailed", "This link could not be opened."),
704
+ }),
668
705
  },
669
706
  });
670
707
  svc.load();
@@ -14,15 +14,21 @@ export interface RichBody {
14
14
  bodyText: string;
15
15
  }
16
16
 
17
+ /** [rico] #3372 — the media width for a document drawn in a LIST row (a feed post, a comment, a CMS item): the
18
+ * list-image size (WIX_LIST_IMAGE), so a scrolling list never downloads every image at detail size. */
19
+ export const ROW_IMAGE_WIDTH = 1000;
20
+
17
21
  /** Prepares `richContent` (a Ricos document, as a Wix API returns it, or its JSON string). No document, or one
18
22
  * that is not a document, gives `{ bodyJson: "", bodyText: fallback }`. A document past the viewer's limits
19
23
  * gives bodyJson "" and its plain text — except one rejected as too deep, which is not walked again (the
20
- * plain-text walk has no depth limit), so `fallback` stands in for it. */
21
- export function richBody(richContent: unknown, fallback = ""): RichBody {
24
+ * plain-text walk has no depth limit), so `fallback` stands in for it. `imageWidth` sizes its media (default
25
+ * the 1400 px detail budget). */
26
+ export function richBody(richContent: unknown, fallback = "", imageWidth = 1400): RichBody {
22
27
  const doc = parseRicosJson(richContent);
23
28
  if (!doc) return { bodyJson: "", bodyText: fallback };
24
- // 1400 px — the detail-image budget, which is also the encoder's maximum.
25
- const { json, rejected } = encodeRichContent(doc, { imageWidth: 1400 });
29
+ // 1400 px by default — the detail-image budget, which is also the encoder's maximum; list rows pass
30
+ // ROW_IMAGE_WIDTH.
31
+ const { json, rejected } = encodeRichContent(doc, { imageWidth });
26
32
  const text = rejected === "too_deep" ? "" : ricosToPlainText(doc);
27
33
  return { bodyJson: json, bodyText: text || fallback };
28
34
  }
@@ -309,7 +309,12 @@ async function measuredRevenue7d(
309
309
  if (typeof measure.total === "number") return measure.total;
310
310
  const values = measure.values ?? [];
311
311
  return values.length ? values.reduce((sum, v) => sum + (v.value || 0), 0) : undefined;
312
- } catch {
312
+ } catch (e) {
313
+ // The fallback is silent on screen (Home shows the order-row sum), so say it where
314
+ // an agent looks: the DEV panel's Console tab and `himi logs`. The status only — a Wix
315
+ // failure's message carries part of the upstream body, which may hold personal data.
316
+ const status = (e as { status?: unknown } | null)?.status ?? /\bHTTP (\d{3})\b/.exec(String(e))?.[1];
317
+ console.warn("[home.revenue] analytics failed; showing the order-row sum", status !== undefined ? `HTTP ${String(status)}` : "");
313
318
  return undefined;
314
319
  }
315
320
  }
@@ -14,15 +14,21 @@ export interface RichBody {
14
14
  bodyText: string;
15
15
  }
16
16
 
17
+ /** [rico] #3372 — the media width for a document drawn in a LIST row (a feed post, a comment, a CMS item): the
18
+ * list-image size (WIX_LIST_IMAGE), so a scrolling list never downloads every image at detail size. */
19
+ export const ROW_IMAGE_WIDTH = 1000;
20
+
17
21
  /** Prepares `richContent` (a Ricos document, as a Wix API returns it, or its JSON string). No document, or one
18
22
  * that is not a document, gives `{ bodyJson: "", bodyText: fallback }`. A document past the viewer's limits
19
23
  * gives bodyJson "" and its plain text — except one rejected as too deep, which is not walked again (the
20
- * plain-text walk has no depth limit), so `fallback` stands in for it. */
21
- export function richBody(richContent: unknown, fallback = ""): RichBody {
24
+ * plain-text walk has no depth limit), so `fallback` stands in for it. `imageWidth` sizes its media (default
25
+ * the 1400 px detail budget). */
26
+ export function richBody(richContent: unknown, fallback = "", imageWidth = 1400): RichBody {
22
27
  const doc = parseRicosJson(richContent);
23
28
  if (!doc) return { bodyJson: "", bodyText: fallback };
24
- // 1400 px — the detail-image budget, which is also the encoder's maximum.
25
- const { json, rejected } = encodeRichContent(doc, { imageWidth: 1400 });
29
+ // 1400 px by default — the detail-image budget, which is also the encoder's maximum; list rows pass
30
+ // ROW_IMAGE_WIDTH.
31
+ const { json, rejected } = encodeRichContent(doc, { imageWidth });
26
32
  const text = rejected === "too_deep" ? "" : ricosToPlainText(doc);
27
33
  return { bodyJson: json, bodyText: text || fallback };
28
34
  }
@@ -309,7 +309,12 @@ async function measuredRevenue7d(
309
309
  if (typeof measure.total === "number") return measure.total;
310
310
  const values = measure.values ?? [];
311
311
  return values.length ? values.reduce((sum, v) => sum + (v.value || 0), 0) : undefined;
312
- } catch {
312
+ } catch (e) {
313
+ // The fallback is silent on screen (Home shows the order-row sum), so say it where
314
+ // an agent looks: the DEV panel's Console tab and `himi logs`. The status only — a Wix
315
+ // failure's message carries part of the upstream body, which may hold personal data.
316
+ const status = (e as { status?: unknown } | null)?.status ?? /\bHTTP (\d{3})\b/.exec(String(e))?.[1];
317
+ console.warn("[home.revenue] analytics failed; showing the order-row sum", status !== undefined ? `HTTP ${String(status)}` : "");
313
318
  return undefined;
314
319
  }
315
320
  }
@@ -8843,7 +8843,32 @@ const SCREENS = runtimeSetting({
8843
8843
  }
8844
8844
  },
8845
8845
  {
8846
- "id": "",
8846
+ "id": "feed_body_$item.id",
8847
+ "body": {
8848
+ "package": "himalaya",
8849
+ "type": "RichContent",
8850
+ "props": {
8851
+ "documentJsonPath": "$item.bodyJson",
8852
+ "onLink": {
8853
+ "actionId": "tier5.himiBrandedFeed.openLink"
8854
+ },
8855
+ "fallback": {
8856
+ "id": "",
8857
+ "body": {
8858
+ "package": "himalaya",
8859
+ "type": "Text",
8860
+ "props": {
8861
+ "text": "$item.text",
8862
+ "style": "body",
8863
+ "maxLines": 6
8864
+ }
8865
+ }
8866
+ }
8867
+ }
8868
+ }
8869
+ },
8870
+ {
8871
+ "id": "feed_body_text_$item.id",
8847
8872
  "body": {
8848
8873
  "package": "himalaya",
8849
8874
  "type": "Text",
@@ -8852,6 +8877,9 @@ const SCREENS = runtimeSetting({
8852
8877
  "style": "body",
8853
8878
  "maxLines": 6
8854
8879
  }
8880
+ },
8881
+ "modifiers": {
8882
+ "hiddenPath": "$item.hasBodyJson"
8855
8883
  }
8856
8884
  },
8857
8885
  {
@@ -9048,7 +9076,8 @@ const SCREENS = runtimeSetting({
9048
9076
  "integrity": "",
9049
9077
  "contentType": "application/javascript+ts-bundle",
9050
9078
  "declaredCapabilities": [
9051
- "wix_api"
9079
+ "wix_api",
9080
+ "open_url"
9052
9081
  ],
9053
9082
  "minBinaryVersion": "0.1.0",
9054
9083
  "bundleVersion": "0.0.1"
@@ -19110,7 +19139,33 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
19110
19139
  }
19111
19140
  },
19112
19141
  {
19113
- "id": "",
19142
+ "id": "event_body",
19143
+ "body": {
19144
+ "package": "himalaya",
19145
+ "type": "RichContent",
19146
+ "props": {
19147
+ "documentJsonPath": "$service:himiEventDetail.descriptionJson",
19148
+ "onLink": {
19149
+ "actionId": "tier5.himiEventDetail.openLink"
19150
+ },
19151
+ "fallback": {
19152
+ "id": "",
19153
+ "body": {
19154
+ "package": "himalaya",
19155
+ "type": "Text",
19156
+ "props": {
19157
+ "text": "$service:himiEventDetail.description",
19158
+ "style": "body",
19159
+ "color": "textSecondary",
19160
+ "collapseWhenEmpty": true
19161
+ }
19162
+ }
19163
+ }
19164
+ }
19165
+ }
19166
+ },
19167
+ {
19168
+ "id": "event_body_text",
19114
19169
  "body": {
19115
19170
  "package": "himalaya",
19116
19171
  "type": "Text",
@@ -19120,6 +19175,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
19120
19175
  "color": "textSecondary",
19121
19176
  "collapseWhenEmpty": true
19122
19177
  }
19178
+ },
19179
+ "modifiers": {
19180
+ "hiddenPath": "$service:himiEventDetail.hasDescriptionJson"
19123
19181
  }
19124
19182
  }
19125
19183
  ]
@@ -21014,7 +21072,33 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
21014
21072
  }
21015
21073
  },
21016
21074
  {
21017
- "id": "",
21075
+ "id": "comment_body_$item.id",
21076
+ "body": {
21077
+ "package": "himalaya",
21078
+ "type": "RichContent",
21079
+ "props": {
21080
+ "documentJsonPath": "$item.bodyJson",
21081
+ "onLink": {
21082
+ "actionId": "tier5.himiBlogPost.openLink"
21083
+ },
21084
+ "fallback": {
21085
+ "id": "",
21086
+ "body": {
21087
+ "package": "himalaya",
21088
+ "type": "Text",
21089
+ "props": {
21090
+ "text": "$item.text",
21091
+ "style": "body",
21092
+ "color": "textSecondary",
21093
+ "collapseWhenEmpty": true
21094
+ }
21095
+ }
21096
+ }
21097
+ }
21098
+ }
21099
+ },
21100
+ {
21101
+ "id": "comment_body_text_$item.id",
21018
21102
  "body": {
21019
21103
  "package": "himalaya",
21020
21104
  "type": "Text",
@@ -21024,6 +21108,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
21024
21108
  "color": "textSecondary",
21025
21109
  "collapseWhenEmpty": true
21026
21110
  }
21111
+ },
21112
+ "modifiers": {
21113
+ "hiddenPath": "$item.hasBodyJson"
21027
21114
  }
21028
21115
  }
21029
21116
  ]
@@ -14,15 +14,21 @@ export interface RichBody {
14
14
  bodyText: string;
15
15
  }
16
16
 
17
+ /** [rico] #3372 — the media width for a document drawn in a LIST row (a feed post, a comment, a CMS item): the
18
+ * list-image size (WIX_LIST_IMAGE), so a scrolling list never downloads every image at detail size. */
19
+ export const ROW_IMAGE_WIDTH = 1000;
20
+
17
21
  /** Prepares `richContent` (a Ricos document, as a Wix API returns it, or its JSON string). No document, or one
18
22
  * that is not a document, gives `{ bodyJson: "", bodyText: fallback }`. A document past the viewer's limits
19
23
  * gives bodyJson "" and its plain text — except one rejected as too deep, which is not walked again (the
20
- * plain-text walk has no depth limit), so `fallback` stands in for it. */
21
- export function richBody(richContent: unknown, fallback = ""): RichBody {
24
+ * plain-text walk has no depth limit), so `fallback` stands in for it. `imageWidth` sizes its media (default
25
+ * the 1400 px detail budget). */
26
+ export function richBody(richContent: unknown, fallback = "", imageWidth = 1400): RichBody {
22
27
  const doc = parseRicosJson(richContent);
23
28
  if (!doc) return { bodyJson: "", bodyText: fallback };
24
- // 1400 px — the detail-image budget, which is also the encoder's maximum.
25
- const { json, rejected } = encodeRichContent(doc, { imageWidth: 1400 });
29
+ // 1400 px by default — the detail-image budget, which is also the encoder's maximum; list rows pass
30
+ // ROW_IMAGE_WIDTH.
31
+ const { json, rejected } = encodeRichContent(doc, { imageWidth });
26
32
  const text = rejected === "too_deep" ? "" : ricosToPlainText(doc);
27
33
  return { bodyJson: json, bodyText: text || fallback };
28
34
  }
@@ -8,7 +8,7 @@ import { isSavedId, toggleSavedId } from "../_shared/himi-blog-saved";
8
8
  import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
9
9
  import { formatDateMedium, formatMonthDay } from "../_shared/engine-intl.js";
10
10
  import { openRichLink } from "../_shared/rich-content-link.js";
11
- import { richBody } from "../_shared/rich-body.js";
11
+ import { richBody, ROW_IMAGE_WIDTH } from "../_shared/rich-body.js";
12
12
 
13
13
  // Offline-first (data-offline): the post body is a public read-through — when the
14
14
  // live fetch fails (offline) fall back to the last-synced row the feed cached under
@@ -23,8 +23,14 @@ async function readCachedPost(id: string): Promise<DbEntity | null> {
23
23
  interface CommentRow {
24
24
  id: string;
25
25
  authorName: string;
26
+ /** The comment's plain text: the row RichContent's fallback. */
26
27
  text: string;
27
28
  meta: string;
29
+ /** [rico] #3372 — the comment's Ricos document as ONE string for the row's RichContent; "" when it has none
30
+ * or the encoder rejected it. */
31
+ bodyJson: string;
32
+ /** bodyJson !== "" — gates the row's plain text, because RichContent draws nothing for "". */
33
+ hasBodyJson: boolean;
28
34
  }
29
35
 
30
36
  interface S {
@@ -168,11 +174,15 @@ function commentMeta(row: WixBlogCommentRow): string {
168
174
  }
169
175
 
170
176
  function toCommentRow(row: WixBlogCommentRow): CommentRow {
177
+ // The plain text is the document's own (runs joined as written, not space-separated) when it has one.
178
+ const { bodyJson, bodyText } = richBody(row.richContent, row.text || "Comment", ROW_IMAGE_WIDTH);
171
179
  return {
172
180
  id: row.id,
173
181
  authorName: row.authorName || "Member",
174
- text: row.text || "Comment",
182
+ text: bodyText,
175
183
  meta: commentMeta(row),
184
+ bodyJson,
185
+ hasBodyJson: bodyJson !== "",
176
186
  };
177
187
  }
178
188
 
@@ -23,6 +23,8 @@ import { signedInEmail } from "../_shared/himi-account";
23
23
  import { initials } from "../_shared/himi-text.js";
24
24
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
25
25
  import { formatMonthDay } from "../_shared/engine-intl.js";
26
+ import { openRichLink } from "../_shared/rich-content-link.js";
27
+ import { richBody, ROW_IMAGE_WIDTH } from "../_shared/rich-body.js";
26
28
 
27
29
  interface FeedRow {
28
30
  id: string;
@@ -33,8 +35,15 @@ interface FeedRow {
33
35
  avatar: string;
34
36
  hasAvatar: boolean;
35
37
  noAvatar: boolean;
38
+ /** The post's plain text: RichContent's fallback, and what shows for a post with no Ricos document. */
36
39
  text: string;
37
- /** First inline IMAGE node in the post content ("" for text-only posts). */
40
+ /** [rico] #3372 — a Ricos post's `content` as ONE document string for the row's RichContent; "" for a
41
+ * DraftJS-blocks post, a lean one, or a document the encoder rejected. */
42
+ bodyJson: string;
43
+ /** bodyJson !== "" — gates the row's plain text, because RichContent draws nothing for "". */
44
+ hasBodyJson: boolean;
45
+ /** First inline IMAGE node in the post content ("" for text-only posts). Shown only for a row with no
46
+ * document: RichContent draws the document's images itself. */
38
47
  image: string;
39
48
  hasImage: boolean;
40
49
  noImage: boolean;
@@ -161,7 +170,10 @@ function feedRow(item: WixFeedItem, profiles: WixFeedProfile[]): FeedRow {
161
170
  const author = authorName(item, profiles);
162
171
  const count = Array.isArray(item.reactionsList) ? item.reactionsList.length : 0;
163
172
  const avatar = authorAvatar(item, profiles);
164
- const image = contentImage(item);
173
+ // A Ricos post's plain text is the whole document's (every node level), not feedText's one-level read.
174
+ const { bodyJson, bodyText: text } = richBody(item.content, feedText(item), ROW_IMAGE_WIDTH);
175
+ // A Ricos post draws its own images in RichContent, so the separate row image would repeat the first one.
176
+ const image = bodyJson ? "" : contentImage(item);
165
177
  return {
166
178
  id: item.feedItemId || "",
167
179
  author,
@@ -169,7 +181,9 @@ function feedRow(item: WixFeedItem, profiles: WixFeedProfile[]): FeedRow {
169
181
  avatar,
170
182
  hasAvatar: !!avatar,
171
183
  noAvatar: !avatar,
172
- text: feedText(item),
184
+ text,
185
+ bodyJson,
186
+ hasBodyJson: bodyJson !== "",
173
187
  image,
174
188
  hasImage: !!image,
175
189
  noImage: !image,
@@ -292,6 +306,8 @@ const svc = defineService<State, {
292
306
  filter: (state: State, arg?: unknown) => State;
293
307
  post: (state: State, arg?: unknown) => Promise<State>;
294
308
  react: (state: State, arg?: unknown) => Promise<State>;
309
+ /** [rico] #3372 — a row RichContent's on_link: `{ url }` of the tapped link. */
310
+ openLink: (state: State, arg?: unknown) => Promise<State>;
295
311
  }>("himiBrandedFeed", {
296
312
  state: derive({
297
313
  status: "loading",
@@ -367,6 +383,10 @@ const svc = defineService<State, {
367
383
  return state;
368
384
  }
369
385
  },
386
+ // Only http(s), mailto and tel links reach open_url; one that does not open says so in the message line.
387
+ openLink: async (state, arg) => ((await openRichLink(arg))
388
+ ? state
389
+ : { ...state, message: copyParam("feed.linkOpenFailed", "This link could not be opened.") }),
370
390
  },
371
391
  });
372
392
 
@@ -508,7 +508,12 @@ async function fetchBookingsSignal(arg: unknown): Promise<BookingsSignal> {
508
508
  try {
509
509
  const response = await queryServices({ query: { paging: { limit: 50 } } });
510
510
  return bookingsSignal(response.services ?? []);
511
- } catch {
511
+ } catch (e) {
512
+ // The block collapses without a word on screen, so say why where an agent looks:
513
+ // the DEV panel's Console tab and `himi logs`. The status only — a Wix failure's
514
+ // message carries part of the upstream body, which may hold personal data.
515
+ const status = (e as { status?: unknown } | null)?.status ?? /\bHTTP (\d{3})\b/.exec(String(e))?.[1];
516
+ console.warn("[home.bookings] services query failed; showcase hidden", status !== undefined ? `HTTP ${String(status)}` : "");
512
517
  return emptyBookingsSignal();
513
518
  }
514
519
  }
@@ -35,6 +35,8 @@ import {
35
35
  } from "../_shared/himi-events";
36
36
  import { initials } from "../_shared/himi-text.js";
37
37
  import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
38
+ import { openRichLink } from "../_shared/rich-content-link.js";
39
+ import { richBody } from "../_shared/rich-body.js";
38
40
 
39
41
  interface MapMarker {
40
42
  latitude: number;
@@ -88,7 +90,13 @@ interface S {
88
90
  initial: string;
89
91
  when: string;
90
92
  location: string;
93
+ /** The plain-text description: RichContent's fallback, the calendar notes, and what shows with no document. */
91
94
  description: string;
95
+ /** [rico] #3372 — the v3 Ricos `description` (read with `fields=TEXTS`) as ONE document string for
96
+ * RichContent; "" when the event has none or the encoder rejected it. */
97
+ descriptionJson: string;
98
+ /** descriptionJson !== "" — gates the plain text, because RichContent draws nothing for "". */
99
+ hasDescriptionJson: boolean;
92
100
  image: string;
93
101
  hasImage: boolean;
94
102
  noImage: boolean;
@@ -212,6 +220,7 @@ function derive(s: S): S {
212
220
  const hasAttendees = s.attendeeAvatars.length > 0;
213
221
  return {
214
222
  ...s,
223
+ hasDescriptionJson: s.descriptionJson !== "",
215
224
  noMap: !s.mapUrl,
216
225
  hasMapMarkers,
217
226
  noMapMarkers: !hasMapMarkers,
@@ -357,6 +366,14 @@ async function attendeeSummary(eventId: string): Promise<{ goingLabel: string; a
357
366
  }
358
367
  }
359
368
 
369
+ /** The description pair: the v3 Ricos `description` for RichContent, and its plain text. A string description
370
+ * (or none) keeps `eventDescription`'s legacy read, with no document. */
371
+ function eventBody(e: WixEvent): { description: string; descriptionJson: string } {
372
+ const legacy = eventDescription(e);
373
+ const { bodyJson, bodyText } = richBody((e as unknown as { description?: unknown }).description, legacy);
374
+ return { description: bodyText, descriptionJson: bodyJson };
375
+ }
376
+
360
377
  /** getEvent returns the slim BASE fieldset (no `registration`); enrich from the
361
378
  * richer query when the type is unknown — best-effort, find-by-id. */
362
379
  async function enrichRegistration(id: string, e: WixEvent): Promise<WixEvent> {
@@ -364,7 +381,11 @@ async function enrichRegistration(id: string, e: WixEvent): Promise<WixEvent> {
364
381
  try {
365
382
  const list = await queryEvents({ limit: 100 });
366
383
  const match = (list.events ?? []).find((candidate) => candidate.id === id);
367
- return match ?? e;
384
+ if (!match) return e;
385
+ // [rico] #3372 — the query's rows are read without TEXTS, so they carry no Ricos `description`: keep the
386
+ // one this detail fetched, or the enrichment would silently drop the document on every real site.
387
+ const description = (e as unknown as { description?: unknown }).description;
388
+ return description === undefined ? match : ({ ...match, description } as unknown as WixEvent);
368
389
  } catch {
369
390
  return e;
370
391
  }
@@ -377,6 +398,8 @@ const svc = defineService<S, {
377
398
  openRegistration: (s: S) => Promise<S>;
378
399
  share: (s: S) => Promise<S>;
379
400
  addToCalendar: (s: S) => Promise<S>;
401
+ /** [rico] #3372 — RichContent's on_link: `{ url }` of the tapped link. */
402
+ openLink: (s: S, arg?: unknown) => Promise<S>;
380
403
  }>("himiEventDetail", {
381
404
  state: derive({
382
405
  status: "loading",
@@ -386,6 +409,8 @@ const svc = defineService<S, {
386
409
  when: "",
387
410
  location: "",
388
411
  description: "",
412
+ descriptionJson: "",
413
+ hasDescriptionJson: false,
389
414
  image: "",
390
415
  hasImage: false,
391
416
  noImage: true,
@@ -433,7 +458,8 @@ const svc = defineService<S, {
433
458
  const id = s.id || inputId();
434
459
  if (!id) return derive({ ...s, status: "ready", errored: true });
435
460
  try {
436
- const res = await getEvent(id);
461
+ // TEXTS adds the Ricos `description` to the base fieldset (a bare read returns none).
462
+ const res = await getEvent(id, { fields: ["TEXTS"] });
437
463
  let e = ((res as unknown as { event?: WixEvent }).event ?? res) as WixEvent;
438
464
  e = await enrichRegistration(id, e);
439
465
  const attendees = await attendeeSummary(id);
@@ -460,7 +486,9 @@ const svc = defineService<S, {
460
486
  initial: (title.trim()[0] || "E").toUpperCase(),
461
487
  when: eventWhen(e),
462
488
  location: eventLocation(e),
463
- description: eventDescription(e),
489
+ // The Ricos description when the event has one (its own text is the plain copy); the legacy
490
+ // string fields otherwise.
491
+ ...eventBody(e),
464
492
  image,
465
493
  hasImage: !!image,
466
494
  noImage: !image,
@@ -491,6 +519,9 @@ const svc = defineService<S, {
491
519
  initial: (title.trim()[0] || "E").toUpperCase(),
492
520
  when: cached.dateLine,
493
521
  location: cached.location,
522
+ // The cached list row carries no description: an offline retry of THIS event keeps the pair it
523
+ // already shows, and any other event's is cleared together, so neither half can linger alone.
524
+ ...(s.id === id ? {} : { description: "", descriptionJson: "" }),
494
525
  image: cached.image,
495
526
  hasImage: !!cached.image,
496
527
  noImage: !cached.image,
@@ -665,6 +696,12 @@ const svc = defineService<S, {
665
696
  return derive({ ...s, actionMessage: copyParam("event.calendarAddFailed", "Could not add this event to calendar."), isAddingCalendar: false });
666
697
  }
667
698
  },
699
+ // Only http(s), mailto and tel links reach open_url; a refused link, or one nothing opened, says so, and a
700
+ // link that opens clears an earlier message.
701
+ openLink: async (s, arg) => derive({
702
+ ...s,
703
+ actionMessage: (await openRichLink(arg)) ? "" : copyParam("event.linkOpenFailed", "This link could not be opened."),
704
+ }),
668
705
  },
669
706
  });
670
707
  svc.load();
@@ -14,15 +14,21 @@ export interface RichBody {
14
14
  bodyText: string;
15
15
  }
16
16
 
17
+ /** [rico] #3372 — the media width for a document drawn in a LIST row (a feed post, a comment, a CMS item): the
18
+ * list-image size (WIX_LIST_IMAGE), so a scrolling list never downloads every image at detail size. */
19
+ export const ROW_IMAGE_WIDTH = 1000;
20
+
17
21
  /** Prepares `richContent` (a Ricos document, as a Wix API returns it, or its JSON string). No document, or one
18
22
  * that is not a document, gives `{ bodyJson: "", bodyText: fallback }`. A document past the viewer's limits
19
23
  * gives bodyJson "" and its plain text — except one rejected as too deep, which is not walked again (the
20
- * plain-text walk has no depth limit), so `fallback` stands in for it. */
21
- export function richBody(richContent: unknown, fallback = ""): RichBody {
24
+ * plain-text walk has no depth limit), so `fallback` stands in for it. `imageWidth` sizes its media (default
25
+ * the 1400 px detail budget). */
26
+ export function richBody(richContent: unknown, fallback = "", imageWidth = 1400): RichBody {
22
27
  const doc = parseRicosJson(richContent);
23
28
  if (!doc) return { bodyJson: "", bodyText: fallback };
24
- // 1400 px — the detail-image budget, which is also the encoder's maximum.
25
- const { json, rejected } = encodeRichContent(doc, { imageWidth: 1400 });
29
+ // 1400 px by default — the detail-image budget, which is also the encoder's maximum; list rows pass
30
+ // ROW_IMAGE_WIDTH.
31
+ const { json, rejected } = encodeRichContent(doc, { imageWidth });
26
32
  const text = rejected === "too_deep" ? "" : ricosToPlainText(doc);
27
33
  return { bodyJson: json, bodyText: text || fallback };
28
34
  }