@wix/himalaya-cli 0.1397.0 → 0.1399.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.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/serve-runtime/src/wix-bridge.d.ts +30 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/icon-catalog.generated.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/validation.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/auth/src/copy.generated.d.ts +109 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/auth/src/feedback.d.ts +15 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/auth/src/index.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/screens/src/index.d.ts +44 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/auth.mjs +304 -18
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core/validation.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +72 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +377 -20
- package/dist/cli.mjs +983 -342
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/component-reference.json +1 -1
- package/guides/component-reference.md +2 -0
- package/guides/headless-loop.md +1 -1
- package/guides/importable-sdks.md +2 -1
- package/guides/one-app-to-himi-app.md +30 -0
- package/guides/one-app-viewer.md +8 -1
- package/guides/sdk-reference.json +92 -1
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +41 -1
- package/templates/beauty-salon/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/beauty-salon-owner/dev/index.ts +304 -7
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/branded-lite/dev/index.ts +41 -1
- package/templates/branded-lite/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/community-club/dev/index.ts +41 -1
- package/templates/community-club/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/community-club-owner/dev/index.ts +304 -7
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/events-meetups/dev/index.ts +41 -1
- package/templates/events-meetups/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/events-meetups-owner/dev/index.ts +304 -7
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/gym-fitness/dev/index.ts +41 -1
- package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/gym-fitness-owner/dev/index.ts +304 -7
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/nonprofit-cause/dev/index.ts +41 -1
- package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/nonprofit-cause-owner/dev/index.ts +304 -7
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/owner-lite/dev/index.ts +304 -7
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/owner-lite/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/owner-lite/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/podcast-creator/dev/index.ts +41 -1
- package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/podcast-creator-owner/dev/index.ts +304 -7
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/portfolio-creative/dev/index.ts +41 -1
- package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/portfolio-creative-owner/dev/index.ts +304 -7
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/restaurant/dev/index.ts +41 -1
- package/templates/restaurant/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/restaurant-owner/dev/index.ts +304 -7
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/retail-store/dev/index.ts +41 -1
- package/templates/retail-store/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/retail-store-owner/dev/index.ts +304 -7
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/yoga-studio/dev/index.ts +41 -1
- package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/yoga-studio-owner/dev/index.ts +304 -7
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
|
@@ -2570,6 +2570,14 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2570
2570
|
const duplicate = (s: AriaChatState, content: string): boolean =>
|
|
2571
2571
|
isDuplicateSend(s, content) || inFlightSend === content;
|
|
2572
2572
|
|
|
2573
|
+
const withWidgetQuery = (w: WidgetView, query: string): WidgetView => {
|
|
2574
|
+
const needle = query.toLowerCase();
|
|
2575
|
+
const suggestions = !needle ? w.options : w.options.filter((option) =>
|
|
2576
|
+
`${option.title} ${option.subtitle} ${option.text}`.toLowerCase().includes(needle));
|
|
2577
|
+
return { ...w, query, suggestions, suggestionsEmpty: suggestions.length === 0,
|
|
2578
|
+
noMatches: !!needle && suggestions.length === 0 };
|
|
2579
|
+
};
|
|
2580
|
+
|
|
2573
2581
|
/** Fold a freshly read server transcript into the observed-count marks.
|
|
2574
2582
|
* Takes the RAW messages, not the bubbles — see `countTurns`. */
|
|
2575
2583
|
/** Carry the owner's SELECTIONS across a rebuild.
|
|
@@ -2625,7 +2633,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2625
2633
|
}
|
|
2626
2634
|
// The line explaining an option that could not open is the answer to a
|
|
2627
2635
|
// tap, and a poll is not a reason to take it back.
|
|
2628
|
-
const
|
|
2636
|
+
const withNotice = prev.notice ? { ...next, notice: prev.notice, noticeHidden: prev.noticeHidden } : next;
|
|
2637
|
+
// The same ask retains the owner's search, just as it retains selection.
|
|
2638
|
+
// Derive suggestions from the new canonical rows, never stale old objects.
|
|
2639
|
+
const kept = next.kind === "AUTOCOMPLETE" ? withWidgetQuery(withNotice, prev.query) : withNotice;
|
|
2629
2640
|
if (!prev.options.some((o, i) => o.selected !== next.options[i].selected)) return kept;
|
|
2630
2641
|
const options = next.options.map((o, i) => ({ ...o, selected: prev.options[i].selected }));
|
|
2631
2642
|
return reselect({ ...kept, options }, options);
|
|
@@ -2683,9 +2694,9 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2683
2694
|
const pictureUrl = isWixMediaHost(hostOf(item.imageUrl)) ? item.imageUrl : "";
|
|
2684
2695
|
return {
|
|
2685
2696
|
id: `o${i}`, value: item.id, title: item.title, subtitle: item.subtitle,
|
|
2686
|
-
subtitleHidden: !item.subtitle, text: "", textHidden: true,
|
|
2697
|
+
subtitleHidden: !item.subtitle, text: "", textHidden: true, badge: "", badgeHidden: true,
|
|
2687
2698
|
pictureUrl, pictureHidden: !pictureUrl, buttonLabel: "", buttonHidden: true,
|
|
2688
|
-
listText: "", listHidden: true, selected: false, respondText: "",
|
|
2699
|
+
listText: "", benefits: [], listHidden: true, selected: false, respondText: "",
|
|
2689
2700
|
actionUrl: "", actionScreenId: "", notSupportedText: "",
|
|
2690
2701
|
};
|
|
2691
2702
|
});
|
|
@@ -3499,7 +3510,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3499
3510
|
// explains the refusal.
|
|
3500
3511
|
const limitText = limitReached ? limitReachedText(w.maxSelected) : "";
|
|
3501
3512
|
const disabled = w.submitting || (w.requiresSelection && chosen === 0);
|
|
3502
|
-
|
|
3513
|
+
const selected = {
|
|
3503
3514
|
...w,
|
|
3504
3515
|
options,
|
|
3505
3516
|
limitReached,
|
|
@@ -3508,6 +3519,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3508
3519
|
primaryDisabled: disabled,
|
|
3509
3520
|
primaryEnabled: !disabled,
|
|
3510
3521
|
};
|
|
3522
|
+
return w.kind === "AUTOCOMPLETE" ? withWidgetQuery(selected, w.query) : selected;
|
|
3511
3523
|
};
|
|
3512
3524
|
|
|
3513
3525
|
const impl: AriaChatCore = {
|
|
@@ -3713,6 +3725,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3713
3725
|
const wasFast = s.status === "sending";
|
|
3714
3726
|
// Read BEFORE the await, compared after: see `turnSeq`.
|
|
3715
3727
|
const seq = turnSeq;
|
|
3728
|
+
// Empty and failed reads retain the last good widget. Edits may have
|
|
3729
|
+
// committed while this request was pending, so retain its current query.
|
|
3730
|
+
const retainWidgetEdits = (): AriaChatState => s.widget.kind === "AUTOCOMPLETE" && committed
|
|
3731
|
+
? { ...s, widget: committed.widget } : s;
|
|
3716
3732
|
try {
|
|
3717
3733
|
const gm = await getConversationMessages(s._convId);
|
|
3718
3734
|
const raw = messagesOf(gm);
|
|
@@ -3746,9 +3762,9 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3746
3762
|
if (!raw.length) {
|
|
3747
3763
|
if (slowed && live.takeActivity() === 0) {
|
|
3748
3764
|
slowed = false;
|
|
3749
|
-
return cadence(
|
|
3765
|
+
return cadence(retainWidgetEdits(), o.poll.fastMs, o.poll.streamMs ?? null);
|
|
3750
3766
|
}
|
|
3751
|
-
return
|
|
3767
|
+
return retainWidgetEdits();
|
|
3752
3768
|
}
|
|
3753
3769
|
const { messages, pending } = reconcile(server, s._pending, s._pendingAfter, you(), s._pendingId, s._pendingAttach ?? [], s._pendingRefs);
|
|
3754
3770
|
// …but still never PAINT nothing over something. `messages` is only
|
|
@@ -3789,9 +3805,15 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3789
3805
|
const withItems = await withWidgetItems(withJob);
|
|
3790
3806
|
if (turnSeq !== seq || abortingEpochs.has(seq)) return undefined;
|
|
3791
3807
|
// Slow the ladder the moment the turn is over; leave it alone otherwise.
|
|
3808
|
+
// A query or selection may land while this transcript read is parked.
|
|
3809
|
+
// Retain those latest edits only when the canonical widget is still the
|
|
3810
|
+
// same ask; carrySelection resets them for new rows/kinds/options.
|
|
3811
|
+
const withCurrentQuery = withItems.widget.kind === "AUTOCOMPLETE" && committed
|
|
3812
|
+
? { ...withItems, widget: carrySelection(withItems.widget, committed.widget) }
|
|
3813
|
+
: withItems;
|
|
3792
3814
|
if (ended && wasFast) {
|
|
3793
3815
|
slowed = false;
|
|
3794
|
-
return cadence(
|
|
3816
|
+
return cadence(withCurrentQuery, o.poll.idleMs, o.poll.fastMs);
|
|
3795
3817
|
}
|
|
3796
3818
|
// The ladder runs slow only while the stream proves it is delivering.
|
|
3797
3819
|
// A whole slow interval in which no frame changed anything is a stalled
|
|
@@ -3800,10 +3822,9 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3800
3822
|
// rate. No clock needed: this tick IS the interval.
|
|
3801
3823
|
if (slowed && live.takeActivity() === 0) {
|
|
3802
3824
|
slowed = false;
|
|
3803
|
-
return cadence(
|
|
3825
|
+
return cadence(withCurrentQuery, o.poll.fastMs, o.poll.streamMs ?? null);
|
|
3804
3826
|
}
|
|
3805
|
-
return
|
|
3806
|
-
|
|
3827
|
+
return withCurrentQuery;
|
|
3807
3828
|
} catch {
|
|
3808
3829
|
// A read that THREW is exactly as stale as one that resolved: if a turn
|
|
3809
3830
|
// boundary moved on while it was in flight, returning `s` merges the
|
|
@@ -3811,7 +3832,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3811
3832
|
// guard above prevents on the success path. Only a still-current tick
|
|
3812
3833
|
// may hold the ladder up by keeping the state as it is.
|
|
3813
3834
|
if (turnSeq !== seq || abortingEpochs.has(seq)) return undefined;
|
|
3814
|
-
return
|
|
3835
|
+
return retainWidgetEdits();
|
|
3815
3836
|
}
|
|
3816
3837
|
},
|
|
3817
3838
|
|
|
@@ -3823,27 +3844,8 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3823
3844
|
|
|
3824
3845
|
widgetQuery: (s, arg) => {
|
|
3825
3846
|
const q = typeof arg === "string" ? arg.trim() : firstString(arg?.value, arg?.text);
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
// Match on everything the owner can SEE. Matching only `title` makes a
|
|
3829
|
-
// subtitle that says "Downtown branch" unsearchable, which reads as the
|
|
3830
|
-
// filter being broken rather than as a deliberate scope.
|
|
3831
|
-
const suggestions = !needle
|
|
3832
|
-
? w.options
|
|
3833
|
-
: w.options.filter((o) =>
|
|
3834
|
-
`${o.title} ${o.subtitle} ${o.text}`.toLowerCase().includes(needle));
|
|
3835
|
-
return {
|
|
3836
|
-
...s,
|
|
3837
|
-
widget: {
|
|
3838
|
-
...w,
|
|
3839
|
-
query: q,
|
|
3840
|
-
suggestions,
|
|
3841
|
-
suggestionsEmpty: suggestions.length === 0,
|
|
3842
|
-
// Typed something and got nothing is a different state from having
|
|
3843
|
-
// typed nothing at all, and says something different to the owner.
|
|
3844
|
-
noMatches: !!needle && suggestions.length === 0,
|
|
3845
|
-
},
|
|
3846
|
-
};
|
|
3847
|
+
// Match title, subtitle and text with the same semantics after a poll.
|
|
3848
|
+
return { ...s, widget: withWidgetQuery(s.widget, q) };
|
|
3847
3849
|
},
|
|
3848
3850
|
|
|
3849
3851
|
widgetAnswer: (s, arg) => {
|
|
@@ -4026,7 +4028,11 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
4026
4028
|
// to answer for it. The clone's positional form can only say `value`, so
|
|
4027
4029
|
// a row id there is tried only once no option has claimed it.
|
|
4028
4030
|
const sectionId = typeof arg === "object" ? firstString(arg?.section) : "";
|
|
4029
|
-
|
|
4031
|
+
// Display cards may have no wire selection value. Their descriptor names
|
|
4032
|
+
// them by stable row id; selectable kinds retain the wire-value contract.
|
|
4033
|
+
const option = sectionId ? undefined : w.kind === "VISUAL_DISPLAY"
|
|
4034
|
+
? w.options.find((o) => o.id === which) ?? w.options.find((o) => o.value === which)
|
|
4035
|
+
: w.options.find((o) => o.value === which);
|
|
4030
4036
|
const section = sectionId
|
|
4031
4037
|
? w.rows.find((r) => r.id === sectionId)
|
|
4032
4038
|
: (option ? undefined : w.rows.find((r) => r.id === which));
|
|
@@ -59,14 +59,24 @@ export const WIDGET_KINDS: readonly WidgetKind[] = [
|
|
|
59
59
|
|
|
60
60
|
/** One selectable row.
|
|
61
61
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* the entire row, and a non-scalar stringifies to `""`. The existing transcript
|
|
62
|
+
* Display fields are scalar because ordinary `$item.` substitutions consume
|
|
63
|
+
* whole scalar prop values. Benefit arrays instead use a nested repeater's
|
|
64
|
+
* `itemsPath: "$item.benefits"`, which expands each benefit in its own scope. The existing transcript
|
|
66
65
|
* bubbles already carry fixed `link1..3Label/Url/Hidden` slots for exactly this
|
|
67
66
|
* reason (`aria-chat.ts`). A row that omits `subtitle` renders nothing at all
|
|
68
67
|
* rather than rendering without a subtitle. */
|
|
68
|
+
/** One PLANS benefit. `color` is the semantic tint; the three `*Hidden` flags pick which of the
|
|
69
|
+
* descriptor's statically tinted icons shows it, because a token prop cannot be `$item.`-bound
|
|
70
|
+
* and still pass static token validation. */
|
|
71
|
+
export interface WidgetBenefit {
|
|
72
|
+
text: string; icon: string; color: string; colorHex: string;
|
|
73
|
+
successHidden: boolean; plainHidden: boolean; customHidden: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
export interface WidgetOption {
|
|
77
|
+
benefits?: WidgetBenefit[];
|
|
78
|
+
/** A visual variant card badge, absent on ordinary selectable rows. */
|
|
79
|
+
badge?: string;
|
|
70
80
|
title: string;
|
|
71
81
|
subtitle: string;
|
|
72
82
|
text: string;
|
|
@@ -294,6 +304,7 @@ export function optionsOf(o: Record<string, unknown> | null): WidgetOption[] {
|
|
|
294
304
|
const sections = objectsAt(opt, "sections");
|
|
295
305
|
const first = sections[0] ?? null;
|
|
296
306
|
return {
|
|
307
|
+
...(badgeTextOf(opt) ? {badge: badgeTextOf(opt)} : {}),
|
|
297
308
|
title: textAt(opt, "title") || textAt(first, "title"),
|
|
298
309
|
subtitle: textAt(opt, "subtitle") || textAt(first, "subtitle"),
|
|
299
310
|
text: textAt(opt, "text"),
|
|
@@ -306,6 +317,17 @@ export function optionsOf(o: Record<string, unknown> | null): WidgetOption[] {
|
|
|
306
317
|
actionScreenId: stringAt(objectAt(action?.screen), "id"),
|
|
307
318
|
notSupportedText: textAt(objectAt(action?.notSupported), "description"),
|
|
308
319
|
listText: objectsAt(opt, "list").map((x) => textOf(x)).filter(Boolean).join("\n"),
|
|
320
|
+
...(objectsAt(opt, "list").length ? { benefits: objectsAt(opt, "list").map((entry) => {
|
|
321
|
+
const spec = objectAt(entry.icon);
|
|
322
|
+
const name = stringAt(spec, "name");
|
|
323
|
+
const color = stringAt(spec, "color");
|
|
324
|
+
const hex = /^#[0-9a-f]{6}$/i.test(color);
|
|
325
|
+
const tint = hex ? "accentPrimary" : !name || name === "Confirm" ? "success" : "textPrimary";
|
|
326
|
+
return { text: stripMarkdown(textOf(entry)),
|
|
327
|
+
icon: name === "Gift" ? "gift" : name === "Send" ? "paper_plane" : "check",
|
|
328
|
+
color: tint, colorHex: hex ? color : "",
|
|
329
|
+
successHidden: tint !== "success", plainHidden: tint !== "textPrimary", customHidden: tint !== "accentPrimary" };
|
|
330
|
+
}) } : {}),
|
|
309
331
|
};
|
|
310
332
|
});
|
|
311
333
|
}
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
// every collapsible element carries its own `*Hidden` boolean — the same shape
|
|
13
13
|
// the transcript bubbles already use for their three link slots.
|
|
14
14
|
|
|
15
|
-
import type { NormalizedWidget, WidgetKind, WidgetOption } from "./aria-widget-model.js";
|
|
15
|
+
import type { NormalizedWidget, WidgetBenefit, WidgetKind, WidgetOption } from "./aria-widget-model.js";
|
|
16
16
|
import { SELECTED_TOKEN, VALUES_TOKEN, isWebUrl, stripMarkdown } from "./aria-widget-model.js";
|
|
17
17
|
import { oauthView } from "./aria-widget-oauth.js";
|
|
18
18
|
import { composeGenericCard } from "./aria-generic-widget.js";
|
|
19
|
+
import { formatNumber } from "./engine-intl.js";
|
|
19
20
|
import { normalizeWidget } from "./aria-widget-kinds.js";
|
|
20
21
|
import { scanEnvelopes, widgetsOfParts } from "./aria-widgets.js";
|
|
21
22
|
|
|
@@ -90,9 +91,12 @@ export interface WidgetOptionRow {
|
|
|
90
91
|
/** The per-option button's label, already host-disclosed. */
|
|
91
92
|
buttonLabel: string;
|
|
92
93
|
buttonHidden: boolean;
|
|
93
|
-
/** PLANS
|
|
94
|
+
/** PLANS benefit prose plus nested repeater rows carrying each icon. */
|
|
94
95
|
listText: string;
|
|
96
|
+
benefits: WidgetBenefit[];
|
|
95
97
|
listHidden: boolean;
|
|
98
|
+
badge: string;
|
|
99
|
+
badgeHidden: boolean;
|
|
96
100
|
selected: boolean;
|
|
97
101
|
/** This option's own button `respondText`, before substitution. Carried on
|
|
98
102
|
* the row because the row is what a tap identifies. */
|
|
@@ -114,6 +118,7 @@ export interface WidgetOptionRow {
|
|
|
114
118
|
export interface WidgetSlice {
|
|
115
119
|
label: string;
|
|
116
120
|
value: number;
|
|
121
|
+
color: string;
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
/** One step of a TODO_PLAN, shaped for `Timeline`. */
|
|
@@ -218,6 +223,9 @@ export interface WidgetView {
|
|
|
218
223
|
* negation: an EMPTY chart is neither, so hiding each component on the
|
|
219
224
|
* other's flag left both on screen as blank boxes. */
|
|
220
225
|
chartPieHidden: boolean;
|
|
226
|
+
chartRingHidden: boolean;
|
|
227
|
+
chartPlainPieHidden: boolean;
|
|
228
|
+
chartLegend: { label: string; color: string; marker: "dot"; value: string }[];
|
|
221
229
|
chartSeriesHidden: boolean;
|
|
222
230
|
chartEmptyHidden: boolean;
|
|
223
231
|
/** Hide the async spinner. `asyncUntracked` is not the right flag: a TRACKED
|
|
@@ -526,7 +534,7 @@ export const NO_WIDGET: WidgetView = {
|
|
|
526
534
|
optionsGridHidden: true, optionsListHidden: false,
|
|
527
535
|
footerRowHidden: false, footerColumnHidden: true,
|
|
528
536
|
slices: [], values: [], chartIsPie: false, chartIsSeries: false, chartKind: "",
|
|
529
|
-
chartPieHidden: true, chartSeriesHidden: true, chartEmptyHidden: true,
|
|
537
|
+
chartPieHidden: true, chartRingHidden: true, chartPlainPieHidden: true, chartLegend: [], chartSeriesHidden: true, chartEmptyHidden: true,
|
|
530
538
|
asyncSpinnerHidden: true, itemsLoadingHidden: true,
|
|
531
539
|
chartEmpty: true, description: "", descriptionHidden: true,
|
|
532
540
|
steps: [], stepsEmpty: true, progress: 0, progressText: "",
|
|
@@ -574,11 +582,20 @@ function webOnly(url: string | undefined): string {
|
|
|
574
582
|
/** One option, flattened. `selected` is passed in because it is worker state
|
|
575
583
|
* rather than anything the payload said — except on the first build, where the
|
|
576
584
|
* service's `isPreselected` seeds it. */
|
|
585
|
+
/** An option that would draw nothing — every line stripped to empty, no picture, badge,
|
|
586
|
+
* benefit or usable button — is dropped rather than rendered as a blank card or an
|
|
587
|
+
* unlabeled row, and does not count toward `renderable`. */
|
|
588
|
+
function hasVisibleContent(row: WidgetOptionRow): boolean {
|
|
589
|
+
return !!(row.title || row.subtitle || row.text || row.pictureUrl || row.badge
|
|
590
|
+
|| row.benefits.length || !row.buttonHidden);
|
|
591
|
+
}
|
|
592
|
+
|
|
577
593
|
function optionRow(
|
|
578
594
|
o: {
|
|
579
595
|
value: string; title: string; subtitle: string; text: string;
|
|
580
596
|
pictureUrl: string; buttonLabel: string; actionUrl: string; listText: string;
|
|
581
|
-
respondText: string; actionScreenId?: string; notSupportedText?: string;
|
|
597
|
+
respondText: string; actionScreenId?: string; notSupportedText?: string; badge?: string;
|
|
598
|
+
benefits?: WidgetBenefit[];
|
|
582
599
|
},
|
|
583
600
|
i: number,
|
|
584
601
|
selected: boolean,
|
|
@@ -629,22 +646,33 @@ function optionRow(
|
|
|
629
646
|
// RADIO_BUTTON option, which may carry `text` and no `title` at all, renders
|
|
630
647
|
// as an empty row. So fold here rather than in each case's composition: the
|
|
631
648
|
// raw `text` is still exported below for anything that wants both.
|
|
632
|
-
|
|
633
|
-
|
|
649
|
+
// "Has a title" is judged on the STRIPPED title: a title of pure markup (`***`) falls back
|
|
650
|
+
// to the option's other prose exactly as a missing one does.
|
|
651
|
+
const strippedTitle = stripMarkdown(o.title);
|
|
652
|
+
const hasTitle = !!strippedTitle;
|
|
653
|
+
const rowTitle = hasTitle ? strippedTitle : stripMarkdown(o.text || o.subtitle);
|
|
634
654
|
const rowSubtitle = hasTitle
|
|
635
|
-
? (o.subtitle || o.text)
|
|
636
|
-
: (o.text && o.subtitle ? o.subtitle : "");
|
|
655
|
+
? stripMarkdown(o.subtitle || o.text)
|
|
656
|
+
: stripMarkdown(o.text && o.subtitle ? o.subtitle : "");
|
|
657
|
+
// Every `*Hidden` flag follows the STRIPPED value: prose that is only markup
|
|
658
|
+
// (`---`, `***`, an image with empty alt) strips to "" and must not paint an
|
|
659
|
+
// empty node.
|
|
660
|
+
const text = stripMarkdown(o.text);
|
|
661
|
+
const listText = stripMarkdown(o.listText);
|
|
662
|
+
const benefits = (o.benefits ?? []).filter((b) => b.text);
|
|
663
|
+
const badge = stripMarkdown(o.badge || "");
|
|
637
664
|
return {
|
|
638
665
|
id: `o${i}`,
|
|
639
666
|
value: o.value,
|
|
640
667
|
title: rowTitle,
|
|
641
668
|
subtitle: rowSubtitle, subtitleHidden: !rowSubtitle,
|
|
642
|
-
text
|
|
669
|
+
text, textHidden: !text,
|
|
643
670
|
pictureUrl, pictureHidden: !pictureUrl,
|
|
644
671
|
// A label is not enough: the button has to be able to DO something — say
|
|
645
672
|
// its text, go to its url, or explain why its screen is not here.
|
|
646
673
|
buttonLabel, buttonHidden: !(buttonLabel && (respondText || actionUrl || actionScreenId)),
|
|
647
|
-
listText
|
|
674
|
+
listText, benefits, listHidden: !listText && !benefits.length,
|
|
675
|
+
badge, badgeHidden: !badge,
|
|
648
676
|
selected,
|
|
649
677
|
respondText,
|
|
650
678
|
actionUrl,
|
|
@@ -757,22 +785,27 @@ export function viewOf(
|
|
|
757
785
|
// or screen destination, so that row must be removed.
|
|
758
786
|
return !rawUrl || isWebUrl(rawUrl);
|
|
759
787
|
})
|
|
760
|
-
|
|
788
|
+
.filter(hasVisibleContent)
|
|
789
|
+
: mappedOptions.filter(hasVisibleContent);
|
|
761
790
|
|
|
762
791
|
// CHART. `pieSlices` is the only shape the catalogue sends, but `chartType`
|
|
763
792
|
// decides the component: a pie is a `DonutChart` at zero thickness, anything
|
|
764
793
|
// else a `Chart` over the same magnitudes.
|
|
765
794
|
const rawSlices = Array.isArray(extra.slices) ? (extra.slices as { label?: unknown; value?: unknown }[]) : [];
|
|
766
|
-
|
|
767
|
-
|
|
795
|
+
// The first-party chart cycles the same palette for sectors and legend dots.
|
|
796
|
+
const chartColors = ["ariaChartBlue", "ariaChartGreen", "ariaChartPurple", "ariaChartGray"];
|
|
797
|
+
const slices: WidgetSlice[] = rawSlices.map((sl, i) => ({
|
|
798
|
+
label: typeof sl.label === "string" ? stripMarkdown(sl.label) : "Other",
|
|
768
799
|
value: typeof sl.value === "number" && Number.isFinite(sl.value) ? sl.value : 0,
|
|
800
|
+
color: chartColors[i % chartColors.length],
|
|
769
801
|
}));
|
|
770
802
|
const chartKind = typeof extra.chartType === "string" ? extra.chartType : "";
|
|
771
803
|
// "doughnut" is the spelling the reference catalogue uses; "donut" is the
|
|
772
804
|
// component's. Accepting only the component's spelling classified the real
|
|
773
805
|
// payload as a SERIES and drew it as bars.
|
|
774
806
|
const isPie = !chartKind || chartKind === "pie" || chartKind === "donut" || chartKind === "doughnut";
|
|
775
|
-
const
|
|
807
|
+
const isRing = chartKind === "donut" || chartKind === "doughnut";
|
|
808
|
+
const description = typeof extra.description === "string" ? stripMarkdown(extra.description) : "";
|
|
776
809
|
|
|
777
810
|
// TODO_PLAN. `status` is the service's word; `done` is what `Timeline` draws.
|
|
778
811
|
const rawItems = Array.isArray(extra.items) ? (extra.items as { label?: unknown; status?: unknown }[]) : [];
|
|
@@ -811,11 +844,16 @@ export function viewOf(
|
|
|
811
844
|
// A single-select starts with at most ONE preselected choice, whatever the
|
|
812
845
|
// payload marked — a radio group showing two ticks is a state the control
|
|
813
846
|
// cannot otherwise reach.
|
|
847
|
+
// A choice with nothing to show (a markup-only title and no prose) is dropped like any
|
|
848
|
+
// other empty option; the preselection seed counts only choices that are drawn, and each
|
|
849
|
+
// keeps its original index so its id is stable.
|
|
814
850
|
let seeded = false;
|
|
815
|
-
const choices =
|
|
851
|
+
const choices: WidgetOptionRow[] = [];
|
|
852
|
+
raw.forEach((o, j) => {
|
|
853
|
+
if (!hasVisibleContent(optionRow(o, j, false, disclose, allowPicture))) return;
|
|
816
854
|
const pre = !!o.preselected && (type !== "single-select" || !seeded);
|
|
817
855
|
if (pre) seeded = true;
|
|
818
|
-
|
|
856
|
+
choices.push(optionRow(o, j, pre, disclose, allowPicture));
|
|
819
857
|
});
|
|
820
858
|
// A question with no choices and no free-text option of its own can still
|
|
821
859
|
// be answered — text is the only thing it can take — so it gets the field.
|
|
@@ -947,6 +985,15 @@ export function viewOf(
|
|
|
947
985
|
chartIsPie: w.kind === "CHART" && isPie && slices.length > 0,
|
|
948
986
|
chartIsSeries: w.kind === "CHART" && !isPie && slices.length > 0,
|
|
949
987
|
chartPieHidden: !(w.kind === "CHART" && isPie && slices.length > 0),
|
|
988
|
+
chartRingHidden: !(w.kind === "CHART" && isRing && slices.length > 0),
|
|
989
|
+
chartPlainPieHidden: !(w.kind === "CHART" && isPie && !isRing && slices.length > 0),
|
|
990
|
+
// Name AND value per row: the legend is the only place a slice's magnitude
|
|
991
|
+
// is spelled out, and the a11y contract reads both. The worker formats it,
|
|
992
|
+
// not each renderer, through the shared cross-engine formatter.
|
|
993
|
+
// Only the wedges the donut actually draws: every renderer drops a slice
|
|
994
|
+
// whose value is not positive, so its legend row would name nothing.
|
|
995
|
+
chartLegend: slices.filter((s) => s.value > 0).map(({ label, color, value }) => ({ label, color, marker: "dot" as const,
|
|
996
|
+
value: formatNumber(value) })),
|
|
950
997
|
chartSeriesHidden: !(w.kind === "CHART" && !isPie && slices.length > 0),
|
|
951
998
|
chartKind,
|
|
952
999
|
chartEmpty: w.kind === "CHART" && slices.length === 0,
|
|
@@ -2068,6 +2068,16 @@ const SCREENS = runtimeSetting({
|
|
|
2068
2068
|
"props": {
|
|
2069
2069
|
"placeholder": "Email",
|
|
2070
2070
|
"keyboard": "email",
|
|
2071
|
+
"validation": {
|
|
2072
|
+
"required": true,
|
|
2073
|
+
"type": "email",
|
|
2074
|
+
"mode": "on_blur"
|
|
2075
|
+
},
|
|
2076
|
+
"errorPath": "$service:himiStoreAccount.emailError",
|
|
2077
|
+
"submitLabel": "go",
|
|
2078
|
+
"onSubmit": {
|
|
2079
|
+
"actionId": "tier5.himiStoreAccount.submit"
|
|
2080
|
+
},
|
|
2071
2081
|
"autoCapitalization": "none",
|
|
2072
2082
|
"focusPath": "$service:himiStoreAccount.emailFocus",
|
|
2073
2083
|
"onChange": {
|
|
@@ -2083,8 +2093,19 @@ const SCREENS = runtimeSetting({
|
|
|
2083
2093
|
"type": "TextField",
|
|
2084
2094
|
"props": {
|
|
2085
2095
|
"placeholder": "Password",
|
|
2086
|
-
"valuePath": "$service:himiStoreAccount.password",
|
|
2087
2096
|
"secure": true,
|
|
2097
|
+
"visibilityToggle": true,
|
|
2098
|
+
"validation": {
|
|
2099
|
+
"required": true,
|
|
2100
|
+
"mode": "on_blur",
|
|
2101
|
+
"message": "Enter your password."
|
|
2102
|
+
},
|
|
2103
|
+
"errorPath": "$service:himiStoreAccount.passwordError",
|
|
2104
|
+
"focusPath": "$service:himiStoreAccount.passwordFocus",
|
|
2105
|
+
"submitLabel": "go",
|
|
2106
|
+
"onSubmit": {
|
|
2107
|
+
"actionId": "tier5.himiStoreAccount.submit"
|
|
2108
|
+
},
|
|
2088
2109
|
"autoCapitalization": "none",
|
|
2089
2110
|
"onChange": {
|
|
2090
2111
|
"actionId": "tier5.himiStoreAccount.setPassword"
|
|
@@ -2095,6 +2116,18 @@ const SCREENS = runtimeSetting({
|
|
|
2095
2116
|
"hiddenPath": "$service:himiStoreAccount.isRecovering"
|
|
2096
2117
|
}
|
|
2097
2118
|
},
|
|
2119
|
+
{
|
|
2120
|
+
"id": "",
|
|
2121
|
+
"body": {
|
|
2122
|
+
"package": "himalaya",
|
|
2123
|
+
"type": "Text",
|
|
2124
|
+
"props": {
|
|
2125
|
+
"text": "$service:himiStoreAccount.busyMessage",
|
|
2126
|
+
"style": "footnote",
|
|
2127
|
+
"collapseWhenEmpty": true
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
},
|
|
2098
2131
|
{
|
|
2099
2132
|
"id": "himi_account.message",
|
|
2100
2133
|
"body": {
|
|
@@ -2117,6 +2150,7 @@ const SCREENS = runtimeSetting({
|
|
|
2117
2150
|
"title": "Sign in",
|
|
2118
2151
|
"style": "primary",
|
|
2119
2152
|
"fullWidth": true,
|
|
2153
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
2120
2154
|
"action": {
|
|
2121
2155
|
"actionId": "tier5.himiStoreAccount.signIn"
|
|
2122
2156
|
}
|
|
@@ -2135,6 +2169,7 @@ const SCREENS = runtimeSetting({
|
|
|
2135
2169
|
"title": "Create account",
|
|
2136
2170
|
"style": "primary",
|
|
2137
2171
|
"fullWidth": true,
|
|
2172
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
2138
2173
|
"action": {
|
|
2139
2174
|
"actionId": "tier5.himiStoreAccount.register"
|
|
2140
2175
|
}
|
|
@@ -2153,6 +2188,7 @@ const SCREENS = runtimeSetting({
|
|
|
2153
2188
|
"title": "Send reset link",
|
|
2154
2189
|
"style": "primary",
|
|
2155
2190
|
"fullWidth": true,
|
|
2191
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
2156
2192
|
"action": {
|
|
2157
2193
|
"actionId": "tier5.himiStoreAccount.resetPassword"
|
|
2158
2194
|
}
|
|
@@ -2171,6 +2207,7 @@ const SCREENS = runtimeSetting({
|
|
|
2171
2207
|
"title": "Forgot password?",
|
|
2172
2208
|
"style": "secondary",
|
|
2173
2209
|
"fullWidth": true,
|
|
2210
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
2174
2211
|
"action": {
|
|
2175
2212
|
"actionId": "tier5.himiStoreAccount.showRecovery"
|
|
2176
2213
|
}
|
|
@@ -2189,6 +2226,7 @@ const SCREENS = runtimeSetting({
|
|
|
2189
2226
|
"title": "Register",
|
|
2190
2227
|
"style": "secondary",
|
|
2191
2228
|
"fullWidth": true,
|
|
2229
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
2192
2230
|
"action": {
|
|
2193
2231
|
"actionId": "tier5.himiStoreAccount.showRegister"
|
|
2194
2232
|
}
|
|
@@ -2207,6 +2245,7 @@ const SCREENS = runtimeSetting({
|
|
|
2207
2245
|
"title": "Sign in instead",
|
|
2208
2246
|
"style": "secondary",
|
|
2209
2247
|
"fullWidth": true,
|
|
2248
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
2210
2249
|
"action": {
|
|
2211
2250
|
"actionId": "tier5.himiStoreAccount.showSignIn"
|
|
2212
2251
|
}
|
|
@@ -2225,6 +2264,7 @@ const SCREENS = runtimeSetting({
|
|
|
2225
2264
|
"title": "Back to sign in",
|
|
2226
2265
|
"style": "secondary",
|
|
2227
2266
|
"fullWidth": true,
|
|
2267
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
2228
2268
|
"action": {
|
|
2229
2269
|
"actionId": "tier5.himiStoreAccount.showSignIn"
|
|
2230
2270
|
}
|