@wix/himalaya-cli 0.1000.0 → 0.1002.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 (42) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +39 -1
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/validator.d.ts +50 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +46 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +46 -0
  5. package/dist/cli.mjs +207 -20
  6. package/dist/preview-assets.generated.bin +0 -0
  7. package/guides/component-reference.json +2 -2
  8. package/guides/component-reference.md +32 -0
  9. package/package.json +2 -2
  10. package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +11 -31
  11. package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
  12. package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
  13. package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +11 -31
  14. package/templates/community-club-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
  15. package/templates/community-club-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
  16. package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +11 -31
  17. package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
  18. package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
  19. package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +11 -31
  20. package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
  21. package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
  22. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +11 -31
  23. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
  24. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
  25. package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +11 -31
  26. package/templates/owner-lite/tier5-src/_shared/aria-widget-model.ts +31 -0
  27. package/templates/owner-lite/tier5-src/_shared/aria-widget-view.ts +48 -13
  28. package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +11 -31
  29. package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
  30. package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
  31. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +11 -31
  32. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
  33. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
  34. package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +11 -31
  35. package/templates/restaurant-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
  36. package/templates/restaurant-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
  37. package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +11 -31
  38. package/templates/retail-store-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
  39. package/templates/retail-store-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
  40. package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +11 -31
  41. package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
  42. package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
@@ -13,6 +13,7 @@
13
13
  // the transcript bubbles already use for their three link slots.
14
14
 
15
15
  import type { NormalizedWidget, WidgetKind } from "./aria-widget-model.js";
16
+ import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
16
17
  import { composeGenericCard } from "./aria-generic-widget.js";
17
18
  import { normalizeWidget } from "./aria-widget-kinds.js";
18
19
  import { scanEnvelopes, widgetsOfParts } from "./aria-widgets.js";
@@ -333,7 +334,22 @@ export const NO_WIDGET: WidgetView = {
333
334
  *
334
335
  * A button with no url is left alone: there is no destination to disclose. */
335
336
  function discloseButton(disclose: Disclose, label: string, url: string): string {
336
- return url ? disclose({ label, url }) : label;
337
+ const text = stripMarkdown(label);
338
+ return url ? disclose({ label: text, url }) : text;
339
+ }
340
+
341
+ /** A destination this region is willing to EXPOSE.
342
+ *
343
+ * `isWebUrl` guards the sink in `aria-chat.ts`, which stops a non-web scheme
344
+ * being opened. It does not stop it being PAINTED: a button whose url is
345
+ * `tel:` still has a label and a non-empty url, so it renders, and the owner
346
+ * taps a control that silently does nothing. Asking the same question here
347
+ * collapses the button instead — which is this screen's own rule, and the
348
+ * reason the two checks are not redundant. Same predicate, so they cannot
349
+ * drift into disagreeing about what a web url is. */
350
+ function webOnly(url: string | undefined): string {
351
+ const t = (url ?? "").trim();
352
+ return isWebUrl(t) ? t : "";
337
353
  }
338
354
 
339
355
  /** One option, flattened. `selected` is passed in because it is worker state
@@ -451,7 +467,12 @@ export function viewOf(
451
467
  allowPicture: (url: string) => boolean = (url) => isDisclosedSame(disclose, url),
452
468
  ): WidgetView {
453
469
  const extra = w.extra as Record<string, unknown>;
454
- const title = typeof extra.title === "string" ? extra.title : "";
470
+ // Markdown, flattened. Aria replies are markdown off the wire and there is no
471
+ // native markdown renderer, so a marker reaching a `Text` is painted
472
+ // literally — the catalogue sends `Upgrade to **Business Basic**`, and the
473
+ // card showed the asterisks while the bubble beside it did not. Same
474
+ // `stripMarkdown` the transcript uses, so the two cannot disagree.
475
+ const title = stripMarkdown(typeof extra.title === "string" ? extra.title : "");
455
476
  // Through the SAME media check as an option's picture. Counting `imageUrl`
456
477
  // towards renderability made an image-only VISUAL_DISPLAY reachable, and the
457
478
  // descriptor binds this path straight into an `Image` — so an off-Wix url
@@ -464,13 +485,13 @@ export function viewOf(
464
485
 
465
486
  const sections = Array.isArray(extra.sections) ? (extra.sections as Record<string, unknown>[]) : [];
466
487
  const rows: WidgetRow[] = sections.map((sec, i) => {
467
- const subtitle = typeof sec.subtitle === "string" ? sec.subtitle : "";
488
+ const subtitle = stripMarkdown(typeof sec.subtitle === "string" ? sec.subtitle : "");
468
489
  return {
469
490
  // A stable id per row. Index-based on purpose: a section has no id of its
470
491
  // own, and the list is rebuilt wholesale on every tick, so there is
471
492
  // nothing for a content-derived key to preserve.
472
493
  id: `s${i}`,
473
- title: typeof sec.title === "string" ? sec.title : "",
494
+ title: stripMarkdown(typeof sec.title === "string" ? sec.title : ""),
474
495
  subtitle,
475
496
  subtitleHidden: !subtitle,
476
497
  };
@@ -540,8 +561,11 @@ export function viewOf(
540
561
 
541
562
  const providerKey = typeof extra.providerKey === "string" ? extra.providerKey : "";
542
563
 
543
- const primaryUrl = w.primary?.externalUrl ?? "";
544
- const secondaryUrl = w.secondary?.externalUrl ?? "";
564
+ const primaryUrl = webOnly(w.primary?.externalUrl);
565
+ const secondaryUrl = webOnly(w.secondary?.externalUrl);
566
+ const prose = stripMarkdown(w.text);
567
+ const bottom = stripMarkdown(w.bottomText);
568
+ const badgeText = stripMarkdown(w.badge);
545
569
  // An UNKNOWN kind is the one case where a MISSING `respondText` must not
546
570
  // make a control inert.
547
571
  //
@@ -563,6 +587,15 @@ export function viewOf(
563
587
  || (generic ? (w.secondary?.label ?? "") : "");
564
588
  const primaryLabel = w.primary ? discloseButton(disclose, w.primary.label, primaryUrl) : "";
565
589
  const secondaryLabel = w.secondary ? discloseButton(disclose, w.secondary.label, secondaryUrl) : "";
590
+ // Whether each footer button is actually SHOWN — a label to read AND
591
+ // something to do (a web destination, or text to send). Defined once and
592
+ // used twice, by the `*Hidden` flags and by `renderable` below, because the
593
+ // two disagreeing is exactly the blank-card bug: `renderable` counted a bare
594
+ // label, so a widget whose only content was a button `webOnly` had just
595
+ // stripped (a `tel:` link with no `respondText`) stayed visible as an empty
596
+ // card with nothing inside it.
597
+ const primaryShown = !!(primaryLabel && (primaryUrl || primaryRespondText));
598
+ const secondaryShown = !!(secondaryLabel && (secondaryUrl || secondaryRespondText));
566
599
 
567
600
  // Hoisted out of the object literal below so `renderable` can see it. It
568
601
  // was computed inline, so naming it in the guard was a ReferenceError that
@@ -587,8 +620,10 @@ export function viewOf(
587
620
  // though they had something to show. Hiding a widget that has content is a
588
621
  // worse failure than the blank card this guard exists to prevent, so the
589
622
  // list has to match what the descriptor renders, not what I remembered.
590
- const renderable = !!(w.text || w.bottomText || w.badge || title || imageUrl
591
- || description || options.length || primaryLabel || secondaryLabel
623
+ // The STRIPPED prose, not the raw fields: those are what the card draws, and
624
+ // a reply of pure markup (`***`, an image-only `![](…)`) strips to nothing.
625
+ const renderable = !!(prose || bottom || badgeText || title || imageUrl
626
+ || description || options.length || primaryShown || secondaryShown
592
627
  || sections.length || slices.length || steps.length || questions.length
593
628
  || asyncMessage || treeJson);
594
629
 
@@ -651,10 +686,10 @@ export function viewOf(
651
686
  Object.entries(msgs).filter((kv): kv is [string, string] => typeof kv[1] === "string"),
652
687
  ),
653
688
  submitting: false,
654
- text: w.text, textHidden: !w.text,
689
+ text: prose, textHidden: !prose,
655
690
  title, titleHidden: !title,
656
- bottomText: w.bottomText, bottomTextHidden: !w.bottomText,
657
- badge: w.badge, badgeHidden: !w.badge,
691
+ bottomText: bottom, bottomTextHidden: !bottom,
692
+ badge: badgeText, badgeHidden: !badgeText,
658
693
  imageUrl, imageHidden: !imageUrl,
659
694
  // Shown only when there is a LABEL to read and a DESTINATION to go to.
660
695
  //
@@ -671,9 +706,9 @@ export function viewOf(
671
706
  // #2605 hid a button that had nothing to DO. Now that `respondText` can be
672
707
  // sent, having something to SAY counts too — which is the `&& url` that
673
708
  // issue said this one would remove.
674
- primaryLabel, primaryHidden: !(primaryLabel && (primaryUrl || primaryRespondText)), primaryUrl,
709
+ primaryLabel, primaryHidden: !primaryShown, primaryUrl,
675
710
  primaryRespondText,
676
- secondaryLabel, secondaryHidden: !(secondaryLabel && (secondaryUrl || secondaryRespondText)), secondaryUrl,
711
+ secondaryLabel, secondaryHidden: !secondaryShown, secondaryUrl,
677
712
  secondaryRespondText,
678
713
  rows,
679
714
  };
@@ -42,7 +42,7 @@ import {
42
42
  type ConversationSummary,
43
43
  } from "@himalaya/wix-genie-chat";
44
44
  import { proseOfParts, scanEnvelopes, stripHidden } from "./aria-widgets.js";
45
- import { isWebUrl } from "./aria-widget-model.js";
45
+ import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
46
46
  import { NO_WIDGET, activeWidgetOf, asyncAdvanced, type SubtreeSlot, type WidgetOptionRow, type WidgetView }
47
47
  from "./aria-widget-view.js";
48
48
  import { substituteSelection } from "./aria-widget-model.js";
@@ -528,36 +528,16 @@ export function disclosedLabel(link: { label: string; url: string }): string {
528
528
  return `${link.label} (${host || UNKNOWN_HOST_LABEL})`;
529
529
  }
530
530
 
531
- /** Aria replies come back as markdown. There is no native markdown renderer
532
- * (adding one is a CI-blocking parity change across both renderers), so
533
- * normalize to clean plain text on-device: drop code fences, unwrap
534
- * links/emphasis/headings, turn bullet markers into '•', collapse blank runs.
535
- * Safe and idempotent. */
536
- export function stripMarkdown(src: string): string {
537
- if (!src) return "";
538
- let t = src;
539
- // fenced code blocks -> keep inner text, drop the ``` fences
540
- t = t.replace(/```[a-zA-Z0-9]*\n?/g, "").replace(/```/g, "");
541
- // inline code `x` -> x
542
- t = t.replace(/`([^`]+)`/g, "$1");
543
- // images ![alt](url) -> alt ; links [text](url) -> text
544
- t = t.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
545
- t = t.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
546
- // bold/italic **x** *x* __x__ _x_ -> x
547
- t = t.replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1");
548
- t = t.replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1");
549
- // ATX headings: '### Title' -> 'Title'
550
- t = t.replace(/^#{1,6}\s+/gm, "");
551
- // blockquotes '> ' -> ''
552
- t = t.replace(/^>\s?/gm, "");
553
- // unordered list markers '- ' / '* ' / '+ ' -> bullet
554
- t = t.replace(/^\s*[-*+]\s+/gm, "• ");
555
- // horizontal rules
556
- t = t.replace(/^\s*([-*_])\1{2,}\s*$/gm, "");
557
- // collapse 3+ newlines to a double
558
- t = t.replace(/\n{3,}/g, "\n\n");
559
- return t.trim();
560
- }
531
+ /** `stripMarkdown` used to live here. It moved to `aria-widget-model.ts` —
532
+ * which imports NOTHING, so the widget view can reach it without closing a
533
+ * cycle back through this file. Re-exported rather than relocated silently,
534
+ * so every consumer and test that names `aria-chat.ts` still resolves.
535
+ *
536
+ * Imported AND re-exported, and that is not redundant: `export { x } from "y"`
537
+ * does not bind `x` in this module's scope, and `toBubbles` below calls it —
538
+ * written that way the identifier is unbound at runtime and the whole turn
539
+ * machinery dies. (Measured: 20 tests, every one of them about polling.) */
540
+ export { stripMarkdown };
561
541
 
562
542
  /** A greeting or optimistic bubble: no links, all slots defaulted. */
563
543
  export function plainBubble(id: string, role: string, who: string, content: string): AriaBubble {
@@ -167,6 +167,37 @@ export function objectAt(v: unknown): Record<string, unknown> | null {
167
167
  return typeof v === "object" && v !== null && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
168
168
  }
169
169
 
170
+ /** Aria replies come back as markdown. There is no native markdown renderer
171
+ * (adding one is a CI-blocking parity change across both renderers), so
172
+ * normalize to clean plain text on-device: drop code fences, unwrap
173
+ * links/emphasis/headings, turn bullet markers into '•', collapse blank runs.
174
+ * Safe and idempotent. */
175
+ export function stripMarkdown(src: string): string {
176
+ if (!src) return "";
177
+ let t = src;
178
+ // fenced code blocks -> keep inner text, drop the ``` fences
179
+ t = t.replace(/```[a-zA-Z0-9]*\n?/g, "").replace(/```/g, "");
180
+ // inline code `x` -> x
181
+ t = t.replace(/`([^`]+)`/g, "$1");
182
+ // images ![alt](url) -> alt ; links [text](url) -> text
183
+ t = t.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
184
+ t = t.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
185
+ // bold/italic **x** *x* __x__ _x_ -> x
186
+ t = t.replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1");
187
+ t = t.replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1");
188
+ // ATX headings: '### Title' -> 'Title'
189
+ t = t.replace(/^#{1,6}\s+/gm, "");
190
+ // blockquotes '> ' -> ''
191
+ t = t.replace(/^>\s?/gm, "");
192
+ // unordered list markers '- ' / '* ' / '+ ' -> bullet
193
+ t = t.replace(/^\s*[-*+]\s+/gm, "• ");
194
+ // horizontal rules
195
+ t = t.replace(/^\s*([-*_])\1{2,}\s*$/gm, "");
196
+ // collapse 3+ newlines to a double
197
+ t = t.replace(/\n{3,}/g, "\n\n");
198
+ return t.trim();
199
+ }
200
+
170
201
  /** A display string, whether the wire wrote it bare or wrapped.
171
202
  *
172
203
  * QUESTIONNAIRE writes every label as `{ text: "…" }` — question titles,
@@ -13,6 +13,7 @@
13
13
  // the transcript bubbles already use for their three link slots.
14
14
 
15
15
  import type { NormalizedWidget, WidgetKind } from "./aria-widget-model.js";
16
+ import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
16
17
  import { composeGenericCard } from "./aria-generic-widget.js";
17
18
  import { normalizeWidget } from "./aria-widget-kinds.js";
18
19
  import { scanEnvelopes, widgetsOfParts } from "./aria-widgets.js";
@@ -333,7 +334,22 @@ export const NO_WIDGET: WidgetView = {
333
334
  *
334
335
  * A button with no url is left alone: there is no destination to disclose. */
335
336
  function discloseButton(disclose: Disclose, label: string, url: string): string {
336
- return url ? disclose({ label, url }) : label;
337
+ const text = stripMarkdown(label);
338
+ return url ? disclose({ label: text, url }) : text;
339
+ }
340
+
341
+ /** A destination this region is willing to EXPOSE.
342
+ *
343
+ * `isWebUrl` guards the sink in `aria-chat.ts`, which stops a non-web scheme
344
+ * being opened. It does not stop it being PAINTED: a button whose url is
345
+ * `tel:` still has a label and a non-empty url, so it renders, and the owner
346
+ * taps a control that silently does nothing. Asking the same question here
347
+ * collapses the button instead — which is this screen's own rule, and the
348
+ * reason the two checks are not redundant. Same predicate, so they cannot
349
+ * drift into disagreeing about what a web url is. */
350
+ function webOnly(url: string | undefined): string {
351
+ const t = (url ?? "").trim();
352
+ return isWebUrl(t) ? t : "";
337
353
  }
338
354
 
339
355
  /** One option, flattened. `selected` is passed in because it is worker state
@@ -451,7 +467,12 @@ export function viewOf(
451
467
  allowPicture: (url: string) => boolean = (url) => isDisclosedSame(disclose, url),
452
468
  ): WidgetView {
453
469
  const extra = w.extra as Record<string, unknown>;
454
- const title = typeof extra.title === "string" ? extra.title : "";
470
+ // Markdown, flattened. Aria replies are markdown off the wire and there is no
471
+ // native markdown renderer, so a marker reaching a `Text` is painted
472
+ // literally — the catalogue sends `Upgrade to **Business Basic**`, and the
473
+ // card showed the asterisks while the bubble beside it did not. Same
474
+ // `stripMarkdown` the transcript uses, so the two cannot disagree.
475
+ const title = stripMarkdown(typeof extra.title === "string" ? extra.title : "");
455
476
  // Through the SAME media check as an option's picture. Counting `imageUrl`
456
477
  // towards renderability made an image-only VISUAL_DISPLAY reachable, and the
457
478
  // descriptor binds this path straight into an `Image` — so an off-Wix url
@@ -464,13 +485,13 @@ export function viewOf(
464
485
 
465
486
  const sections = Array.isArray(extra.sections) ? (extra.sections as Record<string, unknown>[]) : [];
466
487
  const rows: WidgetRow[] = sections.map((sec, i) => {
467
- const subtitle = typeof sec.subtitle === "string" ? sec.subtitle : "";
488
+ const subtitle = stripMarkdown(typeof sec.subtitle === "string" ? sec.subtitle : "");
468
489
  return {
469
490
  // A stable id per row. Index-based on purpose: a section has no id of its
470
491
  // own, and the list is rebuilt wholesale on every tick, so there is
471
492
  // nothing for a content-derived key to preserve.
472
493
  id: `s${i}`,
473
- title: typeof sec.title === "string" ? sec.title : "",
494
+ title: stripMarkdown(typeof sec.title === "string" ? sec.title : ""),
474
495
  subtitle,
475
496
  subtitleHidden: !subtitle,
476
497
  };
@@ -540,8 +561,11 @@ export function viewOf(
540
561
 
541
562
  const providerKey = typeof extra.providerKey === "string" ? extra.providerKey : "";
542
563
 
543
- const primaryUrl = w.primary?.externalUrl ?? "";
544
- const secondaryUrl = w.secondary?.externalUrl ?? "";
564
+ const primaryUrl = webOnly(w.primary?.externalUrl);
565
+ const secondaryUrl = webOnly(w.secondary?.externalUrl);
566
+ const prose = stripMarkdown(w.text);
567
+ const bottom = stripMarkdown(w.bottomText);
568
+ const badgeText = stripMarkdown(w.badge);
545
569
  // An UNKNOWN kind is the one case where a MISSING `respondText` must not
546
570
  // make a control inert.
547
571
  //
@@ -563,6 +587,15 @@ export function viewOf(
563
587
  || (generic ? (w.secondary?.label ?? "") : "");
564
588
  const primaryLabel = w.primary ? discloseButton(disclose, w.primary.label, primaryUrl) : "";
565
589
  const secondaryLabel = w.secondary ? discloseButton(disclose, w.secondary.label, secondaryUrl) : "";
590
+ // Whether each footer button is actually SHOWN — a label to read AND
591
+ // something to do (a web destination, or text to send). Defined once and
592
+ // used twice, by the `*Hidden` flags and by `renderable` below, because the
593
+ // two disagreeing is exactly the blank-card bug: `renderable` counted a bare
594
+ // label, so a widget whose only content was a button `webOnly` had just
595
+ // stripped (a `tel:` link with no `respondText`) stayed visible as an empty
596
+ // card with nothing inside it.
597
+ const primaryShown = !!(primaryLabel && (primaryUrl || primaryRespondText));
598
+ const secondaryShown = !!(secondaryLabel && (secondaryUrl || secondaryRespondText));
566
599
 
567
600
  // Hoisted out of the object literal below so `renderable` can see it. It
568
601
  // was computed inline, so naming it in the guard was a ReferenceError that
@@ -587,8 +620,10 @@ export function viewOf(
587
620
  // though they had something to show. Hiding a widget that has content is a
588
621
  // worse failure than the blank card this guard exists to prevent, so the
589
622
  // list has to match what the descriptor renders, not what I remembered.
590
- const renderable = !!(w.text || w.bottomText || w.badge || title || imageUrl
591
- || description || options.length || primaryLabel || secondaryLabel
623
+ // The STRIPPED prose, not the raw fields: those are what the card draws, and
624
+ // a reply of pure markup (`***`, an image-only `![](…)`) strips to nothing.
625
+ const renderable = !!(prose || bottom || badgeText || title || imageUrl
626
+ || description || options.length || primaryShown || secondaryShown
592
627
  || sections.length || slices.length || steps.length || questions.length
593
628
  || asyncMessage || treeJson);
594
629
 
@@ -651,10 +686,10 @@ export function viewOf(
651
686
  Object.entries(msgs).filter((kv): kv is [string, string] => typeof kv[1] === "string"),
652
687
  ),
653
688
  submitting: false,
654
- text: w.text, textHidden: !w.text,
689
+ text: prose, textHidden: !prose,
655
690
  title, titleHidden: !title,
656
- bottomText: w.bottomText, bottomTextHidden: !w.bottomText,
657
- badge: w.badge, badgeHidden: !w.badge,
691
+ bottomText: bottom, bottomTextHidden: !bottom,
692
+ badge: badgeText, badgeHidden: !badgeText,
658
693
  imageUrl, imageHidden: !imageUrl,
659
694
  // Shown only when there is a LABEL to read and a DESTINATION to go to.
660
695
  //
@@ -671,9 +706,9 @@ export function viewOf(
671
706
  // #2605 hid a button that had nothing to DO. Now that `respondText` can be
672
707
  // sent, having something to SAY counts too — which is the `&& url` that
673
708
  // issue said this one would remove.
674
- primaryLabel, primaryHidden: !(primaryLabel && (primaryUrl || primaryRespondText)), primaryUrl,
709
+ primaryLabel, primaryHidden: !primaryShown, primaryUrl,
675
710
  primaryRespondText,
676
- secondaryLabel, secondaryHidden: !(secondaryLabel && (secondaryUrl || secondaryRespondText)), secondaryUrl,
711
+ secondaryLabel, secondaryHidden: !secondaryShown, secondaryUrl,
677
712
  secondaryRespondText,
678
713
  rows,
679
714
  };