@wix/himalaya-cli 0.1577.0 → 0.1579.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/sdk/calendar/src/index.d.ts +42 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/clipboard/src/index.d.ts +32 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/haptic/src/index.d.ts +69 -5
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/capabilities.generated.d.ts +3 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/ui/src/index.d.ts +19 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +6 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/audio-record.mjs +3 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calendar.mjs +48 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calls.mjs +3 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/clipboard.mjs +58 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/contacts.mjs +3 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/haptic.mjs +73 -4
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/location.mjs +3 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/motion.mjs +3 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/photos.mjs +3 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdk-types.mjs +3 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/tasks.mjs +3 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/ui.mjs +5 -0
- package/dist/cli.mjs +1061 -917
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/one-app-section-mappings.md +26 -0
- package/guides/qa-builds.md +14 -6
- package/guides/sdk-reference.json +159 -9
- package/guides/web-page-sdk-reference.md +3 -3
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +14 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-bookings.ts +21 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/beauty-salon/tier5-src/himi-booking-services/index.ts +36 -12
- package/templates/beauty-salon-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/branded-lite/dev/index.ts +14 -0
- package/templates/branded-lite/tier5-src/_shared/himi-bookings.ts +21 -0
- package/templates/branded-lite/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/branded-lite/tier5-src/himi-booking-services/index.ts +36 -12
- package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +40 -2
- package/templates/brandy/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/community-club/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/community-club-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/events-meetups/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/events-meetups-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/gym-fitness/dev/index.ts +14 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-bookings.ts +21 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/gym-fitness/tier5-src/himi-booking-services/index.ts +36 -12
- package/templates/gym-fitness-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/himi-chat/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/nonprofit-cause/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/nonprofit-cause-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/owner-lite/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/podcast-creator/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/podcast-creator-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/portfolio-creative/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/portfolio-creative-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/restaurant/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/restaurant-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/retail-store/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/retail-store-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/yoga-studio/dev/index.ts +14 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-bookings.ts +21 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/yoga-studio/tier5-src/himi-booking-services/index.ts +36 -12
- package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +40 -2
- package/templates/yoga-studio-owner/tier5-src/_shared/himi-copy.ts +8 -3
|
Binary file
|
|
@@ -51,6 +51,32 @@ The **Himalaya implementation** cell is blank for every unassigned row. Notes on
|
|
|
51
51
|
| `achievements_carousel_section` | `wix-one-app-achievements.MemberChallengesCarousel` | [Programs data/detail](https://github.com/wix-private/himalaya/blob/97f1542f2f7d6e8cb7a902b912c87dcaa37e5a67/core/dev-server/tier5-bundles-src/himi-branded-lite-programs/index.ts "audience-lint-ok: optional pinned upstream evidence, not a local authoring dependency") + [Core Carousel](https://github.com/wix-private/himalaya/blob/97f1542f2f7d6e8cb7a902b912c87dcaa37e5a67/core/dev-server/src/builder.ts "audience-lint-ok: optional pinned upstream evidence, not a local authoring dependency") | Existing base; adaptation needed | Partial | Reuse the Programs feature and compose a carousel; preserve source extraProps.limit=5 and selected programs. No ready-made legacy Challenges carousel adapter is claimed. |
|
|
52
52
|
| `achievements_participant_carousel_section` | `wix-one-app-achievements.ParticipantChallengesCarousel` | | | Empty | The public Programs catalog is not a member-participating-programs carousel. No verified consumer participant/progress list implementation was found. |
|
|
53
53
|
|
|
54
|
+
For authored Programs sections, pass the source settings as a JSON string in the
|
|
55
|
+
`programsSection` parameter to `tier5.himiBrandedPrograms.load`:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const programsSection = JSON.stringify({ settings: {
|
|
59
|
+
sortingSettings: { ordering: "MANUAL", challengesOrder: ["program-id-b", "program-id-a"] },
|
|
60
|
+
} });
|
|
61
|
+
// Source IDs come from the collected section, never customer-specific constants in a template.
|
|
62
|
+
const onAppear = { actionId: "tier5.himiBrandedPrograms.load", params: { programsSection } };
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The worker also reads the same string from navigation input at boot. Preserve it
|
|
66
|
+
in the authored section's onAppear params when forwarding the copy pack. A
|
|
67
|
+
copy-only load or Retry retains the last settings. Sending an explicit empty
|
|
68
|
+
settings object, `DEFAULT`, or an empty manual order restores the complete
|
|
69
|
+
newest-first catalog. Manual ordering puts known configured IDs first, then all
|
|
70
|
+
remaining Programs in catalog order; it is not an inclusion list. Stale and
|
|
71
|
+
repeated IDs add no cards. Each worker instance retains its own source settings.
|
|
72
|
+
The collection's member-selected A–Z/Z–A sort still overrides its default order;
|
|
73
|
+
pass `collectionSort: "default"` when opening a source section that must start in
|
|
74
|
+
source order. The member list (`himi_member_programs`) does not read
|
|
75
|
+
`programsSection` and has no manual order yet. This is an authoring recipe, not an
|
|
76
|
+
automatic section importer.
|
|
77
|
+
Category selection, participant status/counts, display flags, limits, See All and
|
|
78
|
+
carousel placement remain separate gaps; the mappings above stay Partial.
|
|
79
|
+
|
|
54
80
|
### 3. bookings-section-definitions.json
|
|
55
81
|
|
|
56
82
|
[Server definitions](https://github.com/wix-private/catalyst-server/blob/617c06d43d2a5608b6876d724553269b1e5346f0/one-app-sections/one-app-sections-definitions/src/main/resources/sections/bookings-section-definitions.json) · 4 definition(s).
|
package/guides/qa-builds.md
CHANGED
|
@@ -1210,12 +1210,20 @@ himi dist --app <app> --platform android --aab --wait --target bo
|
|
|
1210
1210
|
a bundle id saved in between is refused and stays changeable). The owner must already have the app
|
|
1211
1211
|
in App Store Connect under that bundle id. The files
|
|
1212
1212
|
and the key are never stored: the pool and the upload each fetch them again, the job keeps only
|
|
1213
|
-
their hashes. The upload
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1213
|
+
their hashes. The upload starts on its own once the pool reports the validated IPA: the pool's
|
|
1214
|
+
callback starts it in the background, and so does every list read of the app's builds (the owner
|
|
1215
|
+
card's poll) while it can be claimed, which also picks up one a stopped serve pod left. A status
|
|
1216
|
+
read still uploads too; any read the caller may make of the app moves it on, since the owner was
|
|
1217
|
+
proven when the build started (the Containers server's reads carry a project token, not the
|
|
1218
|
+
owner's Wix login). A caller reads the job until `customerAppStore.status` is `UPLOADED` or
|
|
1219
|
+
`FAILED` to learn how it ended; it no longer has to read for the upload to happen. An attempt that
|
|
1220
|
+
fails on a passing error (a 429 or 5xx from mobile-apps-containers-builder or Apple, a timeout, a
|
|
1221
|
+
download that broke off) goes back to `READY_FOR_UPLOAD` with `failedAttempts`, `retryAt` and
|
|
1222
|
+
`lastError`, and is tried again 1, 5 and 15 minutes later. The 4th failed attempt (an attempt
|
|
1223
|
+
whose 15-minute lease ran out counts as one) and any error the owner must fix (no app in App Store
|
|
1224
|
+
Connect, a refused key, a closed version) end `FAILED`, with `error`. When the IPA's 7-day link is
|
|
1225
|
+
within an hour of lapsing, the upload signs a fresh one from the stored bucket and key (the plane's
|
|
1226
|
+
`DIST_ARTIFACT_AWS_*` key pair; without it, the recorded link).
|
|
1219
1227
|
Refusals, each with `code` equal to `error`: `store_build_ios_only`, `store_build_version_invalid`
|
|
1220
1228
|
and `store_build_shell_unsupported` 400, `store_build_site_forbidden` 403, `store_id_missing`,
|
|
1221
1229
|
`store_app_site_taken`, `release_predates_store_builds`, `apple_credentials_missing`,
|
|
@@ -681,7 +681,7 @@
|
|
|
681
681
|
{
|
|
682
682
|
"name": "createEvent",
|
|
683
683
|
"kind": "function",
|
|
684
|
-
"jsdoc": "",
|
|
684
|
+
"jsdoc": "/** Create an event directly. Needs calendar access — to let the user add an event with\n * NO permission prompt, use {@link presentEditor} (the system editor) instead. */",
|
|
685
685
|
"decl": "export async function createEvent(event: NewCalendarEvent): Promise<{ id: string | null }>;"
|
|
686
686
|
},
|
|
687
687
|
{
|
|
@@ -696,6 +696,48 @@
|
|
|
696
696
|
"jsdoc": "",
|
|
697
697
|
"decl": "export async function deleteEvent(id: string): Promise<{ deleted: boolean }>;"
|
|
698
698
|
},
|
|
699
|
+
{
|
|
700
|
+
"name": "CalendarInfo",
|
|
701
|
+
"kind": "interface",
|
|
702
|
+
"jsdoc": "/** A calendar the user has, from {@link listCalendars}. `color` is `#RRGGBB` (\"\" if unknown). */",
|
|
703
|
+
"decl": "export interface CalendarInfo {\n id: string;\n title: string;\n color: string;\n allowsModifications: boolean;\n isPrimary: boolean;\n}"
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
"name": "CalendarEditorResult",
|
|
707
|
+
"kind": "interface",
|
|
708
|
+
"jsdoc": "/** What {@link presentEditor} resolved with. `saved` is false on cancel. `id` is the new\n * event's id when the platform can report it — always `null` on Android (ACTION_INSERT\n * returns no id) and on web, and `null` on iOS without full access. On web, `downloaded`\n * is true when an .ics file was handed to the browser (the user imports it themselves,\n * so `saved` stays false). */",
|
|
709
|
+
"decl": "export interface CalendarEditorResult {\n saved: boolean;\n id: string | null;\n downloaded?: boolean;\n}"
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
"name": "CalendarViewerAction",
|
|
713
|
+
"kind": "type",
|
|
714
|
+
"jsdoc": "/** How the system viewer was closed. Android reports `done` always (ACTION_VIEW has no result). */",
|
|
715
|
+
"decl": "export type CalendarViewerAction = \"done\" | \"responded\" | \"deleted\";"
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
"name": "presentEditor",
|
|
719
|
+
"kind": "function",
|
|
720
|
+
"jsdoc": "/**\n * Open the system event editor prefilled with `event`; the user saves or cancels. Needs\n * NO calendar permission (the OS editor writes the event, not the app). Resolves\n * `{ saved: false, id: null }` on cancel. Web downloads an .ics instead; macOS rejects\n * `unavailable` (no system editor there).\n */",
|
|
721
|
+
"decl": "export async function presentEditor(event: Partial<NewCalendarEvent> = {}): Promise<CalendarEditorResult>;"
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
"name": "presentViewer",
|
|
725
|
+
"kind": "function",
|
|
726
|
+
"jsdoc": "/**\n * Show an existing event in the system viewer (iOS EKEventViewController, Android\n * ACTION_VIEW). Needs full calendar access — rejects `permission_denied` without it, and\n * `not_found` when the id does not resolve. Web and macOS reject `unavailable`.\n */",
|
|
727
|
+
"decl": "export async function presentViewer(id: string): Promise<{ action: CalendarViewerAction }>;"
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
"name": "listCalendars",
|
|
731
|
+
"kind": "function",
|
|
732
|
+
"jsdoc": "/** The user's calendars. Needs full access — rejects `permission_denied` without it. */",
|
|
733
|
+
"decl": "export async function listCalendars(): Promise<CalendarInfo[]>;"
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
"name": "getEvent",
|
|
737
|
+
"kind": "function",
|
|
738
|
+
"jsdoc": "/** One event by id, or `null` when it does not resolve. Needs full access — rejects\n * `permission_denied` without it. */",
|
|
739
|
+
"decl": "export async function getEvent(id: string): Promise<CalendarEvent | null>;"
|
|
740
|
+
},
|
|
699
741
|
{
|
|
700
742
|
"name": "version",
|
|
701
743
|
"kind": "const",
|
|
@@ -1017,6 +1059,12 @@
|
|
|
1017
1059
|
"source": "core/sdk/clipboard/src/index.ts",
|
|
1018
1060
|
"description": "Tier-5 typed binding for the system clipboard.",
|
|
1019
1061
|
"api": [
|
|
1062
|
+
{
|
|
1063
|
+
"name": "\"@himalaya/sdk-types\"",
|
|
1064
|
+
"kind": "reexport",
|
|
1065
|
+
"jsdoc": "",
|
|
1066
|
+
"decl": "export type { FileHandle } from \"@himalaya/sdk-types\";"
|
|
1067
|
+
},
|
|
1020
1068
|
{
|
|
1021
1069
|
"name": "writeText",
|
|
1022
1070
|
"kind": "function",
|
|
@@ -1035,11 +1083,59 @@
|
|
|
1035
1083
|
"jsdoc": "/** Whether the clipboard holds text — no content access, no iOS paste banner. On\n * web this can't be known without reading, so it returns true pessimistically;\n * treat it as a hint, not a guarantee. */",
|
|
1036
1084
|
"decl": "export async function hasText(): Promise<boolean>;"
|
|
1037
1085
|
},
|
|
1086
|
+
{
|
|
1087
|
+
"name": "MAX_IMAGE_BYTES",
|
|
1088
|
+
"kind": "const",
|
|
1089
|
+
"jsdoc": "/** Largest image writeImage accepts (20 MB). Hosts enforce it too. */",
|
|
1090
|
+
"decl": "export const MAX_IMAGE_BYTES = 20 * 1024 * 1024;"
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
"name": "IMAGE_MIME_TYPES",
|
|
1094
|
+
"kind": "const",
|
|
1095
|
+
"jsdoc": "/** Image MIME types writeImage copies. HEIC is iOS/macOS only (Android and web\n * reject it `unsupported_type`); web accepts PNG only. The host sniffs the file's\n * bytes, so a handle is judged by its content, not its `mimeType` — the binding only\n * refuses up front a handle whose type is plainly not an image (e.g. application/pdf). */",
|
|
1096
|
+
"decl": "export const IMAGE_MIME_TYPES: readonly string[] = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/heic\"];"
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
"name": "ClipboardImageErrorCode",
|
|
1100
|
+
"kind": "type",
|
|
1101
|
+
"jsdoc": "/** Why writeImage rejected. */",
|
|
1102
|
+
"decl": "export type ClipboardImageErrorCode = \"unsupported_type\" | \"too_large\" | \"not_allowed\" | \"not_found\" | \"unavailable\";"
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
"name": "ClipboardImageError",
|
|
1106
|
+
"kind": "interface",
|
|
1107
|
+
"jsdoc": "/** A writeImage rejection: an `Error` named \"ClipboardImageError\" carrying a stable\n * `code`. (A plain Error, not a subclass — Tier-5 bundles target an engine without\n * class syntax.) Branch on `isClipboardImageError(e) && e.code === \"too_large\"`. */",
|
|
1108
|
+
"decl": "export interface ClipboardImageError extends Error {\n name: \"ClipboardImageError\";\n code: ClipboardImageErrorCode;\n}"
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
"name": "clipboardImageError",
|
|
1112
|
+
"kind": "function",
|
|
1113
|
+
"jsdoc": "/** Build a coded writeImage rejection. */",
|
|
1114
|
+
"decl": "export function clipboardImageError(code: ClipboardImageErrorCode, message?: string): ClipboardImageError;"
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
"name": "isClipboardImageError",
|
|
1118
|
+
"kind": "function",
|
|
1119
|
+
"jsdoc": "/** Whether `e` is a coded writeImage rejection. */",
|
|
1120
|
+
"decl": "export function isClipboardImageError(e: unknown): e is ClipboardImageError;"
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
"name": "writeImage",
|
|
1124
|
+
"kind": "function",
|
|
1125
|
+
"jsdoc": "/** Copy an image file the worker already holds to the clipboard. Accepts a\n * `FileHandle` (from files / camera / photos) or its substrate `uri`. Rejects with a\n * `ClipboardImageError` (see `isClipboardImageError`) whose `code` is `unsupported_type` (not PNG/JPEG/WebP/HEIC, or\n * not supported on this host), `too_large` (over 20 MB), `not_allowed` (outside the\n * app's files substrate), `not_found`, or `unavailable` (no image clipboard here). */",
|
|
1126
|
+
"decl": "export async function writeImage(file: FileHandle | string): Promise<void>;"
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
"name": "hasImage",
|
|
1130
|
+
"kind": "function",
|
|
1131
|
+
"jsdoc": "/** Whether the clipboard currently holds an image — without reading it (no iOS paste\n * banner). Web cannot know without a permission-gated read, so it answers false. */",
|
|
1132
|
+
"decl": "export async function hasImage(): Promise<boolean>;"
|
|
1133
|
+
},
|
|
1038
1134
|
{
|
|
1039
1135
|
"name": "version",
|
|
1040
1136
|
"kind": "const",
|
|
1041
1137
|
"jsdoc": "/** Capability version, for bundles that gate on min binary version. */",
|
|
1042
|
-
"decl": "export const version =
|
|
1138
|
+
"decl": "export const version = 2;"
|
|
1043
1139
|
}
|
|
1044
1140
|
]
|
|
1045
1141
|
},
|
|
@@ -1550,20 +1646,68 @@
|
|
|
1550
1646
|
{
|
|
1551
1647
|
"name": "HapticPattern",
|
|
1552
1648
|
"kind": "type",
|
|
1553
|
-
"jsdoc": "
|
|
1554
|
-
"decl": "export type HapticPattern =\n | \"selection\"\n | \"light\"\n | \"medium\"\n | \"heavy\"\n | \"success\"\n | \"warning\"\n | \"error\";"
|
|
1649
|
+
"jsdoc": "/**\n * Maps to a native impact/notification/selection generator.\n *\n * - `rigid` / `soft` — iOS `UIImpactFeedbackGenerator.FeedbackStyle.rigid` / `.soft`.\n * Android: `rigid` is a short full-amplitude click (`VibrationEffect.EFFECT_CLICK`-like),\n * `soft` a longer low-amplitude tick (`PRIMITIVE_LOW_TICK`-like); on a device without\n * amplitude control both are plain one-shots of different length.\n */",
|
|
1650
|
+
"decl": "export type HapticPattern =\n | \"selection\"\n | \"light\"\n | \"medium\"\n | \"heavy\"\n | \"rigid\"\n | \"soft\"\n | \"success\"\n | \"warning\"\n | \"error\";"
|
|
1651
|
+
},
|
|
1652
|
+
{
|
|
1653
|
+
"name": "PlayOptions",
|
|
1654
|
+
"kind": "interface",
|
|
1655
|
+
"jsdoc": "",
|
|
1656
|
+
"decl": "export interface PlayOptions {\n /**\n * 0..1, clamped. Scales the strength of an IMPACT style (`light`, `medium`, `heavy`,\n * `rigid`, `soft`): iOS `impactOccurred(intensity:)`, Android amplitude where the\n * vibrator has amplitude control. Ignored for `selection` and the notification styles\n * (`success`, `warning`, `error`) — those have a fixed system strength — and on web.\n */\n intensity?: number;\n}"
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
"name": "HapticStep",
|
|
1660
|
+
"kind": "interface",
|
|
1661
|
+
"jsdoc": "",
|
|
1662
|
+
"decl": "export interface HapticStep {\n pattern: HapticPattern;\n /** Milliseconds to wait BEFORE this step plays (≥ 0). */\n delayMs: number;\n /** Same meaning as {@link PlayOptions.intensity}. */\n intensity?: number;\n}"
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
"name": "HapticResult",
|
|
1666
|
+
"kind": "type",
|
|
1667
|
+
"jsdoc": "/**\n * What a verb reports. `ok: false` is honest: `unavailable` (this surface has no haptic\n * hardware/API — web without `navigator.vibrate`, macOS) or `unsupported_binary` (the\n * native app predates the verb; nothing played, nothing broke).\n */",
|
|
1668
|
+
"decl": "export type HapticResult = { ok: true } | { ok: false; reason: \"unavailable\" | \"unsupported_binary\" };"
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
"name": "MAX_SEQUENCE_STEPS",
|
|
1672
|
+
"kind": "const",
|
|
1673
|
+
"jsdoc": "/** Most steps one {@link sequence} may carry. */",
|
|
1674
|
+
"decl": "export const MAX_SEQUENCE_STEPS = 32;"
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
"name": "MAX_DURATION_MS",
|
|
1678
|
+
"kind": "const",
|
|
1679
|
+
"jsdoc": "/** Longest a {@link sequence} or {@link vibrate} pattern may run, in milliseconds. */",
|
|
1680
|
+
"decl": "export const MAX_DURATION_MS = 10_000;"
|
|
1681
|
+
},
|
|
1682
|
+
{
|
|
1683
|
+
"name": "clampIntensity",
|
|
1684
|
+
"kind": "function",
|
|
1685
|
+
"jsdoc": "/** Clamp an intensity into 0..1; `undefined` / non-finite → `undefined` (system default). */",
|
|
1686
|
+
"decl": "export function clampIntensity(intensity: number | undefined): number | undefined;"
|
|
1555
1687
|
},
|
|
1556
1688
|
{
|
|
1557
1689
|
"name": "play",
|
|
1558
1690
|
"kind": "function",
|
|
1559
|
-
"jsdoc": "/**\n * Play a haptic feedback pattern. Fire-and-forget; resolves once handed to the\n * platform.
|
|
1560
|
-
"decl": "export async function play(pattern: HapticPattern = \"light\"): Promise<
|
|
1691
|
+
"jsdoc": "/**\n * Play a haptic feedback pattern. Fire-and-forget; resolves once handed to the\n * platform. Unknown pattern names keep their historical behaviour (the platform's\n * default tap). `intensity` applies to impact styles only — see {@link PlayOptions}.\n */",
|
|
1692
|
+
"decl": "export async function play(pattern: HapticPattern = \"light\", options: PlayOptions = {}): Promise<HapticResult>;"
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
"name": "sequence",
|
|
1696
|
+
"kind": "function",
|
|
1697
|
+
"jsdoc": "/**\n * Play `steps` in order, waiting each step's `delayMs` before it plays. Runs natively\n * as ONE bridge call (the timing does not depend on the worker), so a drum roll or a\n * countdown keeps its rhythm. A new `sequence` cancels one still running. Resolves once\n * the sequence has been scheduled.\n *\n * Rejects (`RangeError`) with more than {@link MAX_SEQUENCE_STEPS} steps, a negative or\n * non-finite delay, or delays totalling over {@link MAX_DURATION_MS}. An empty list just\n * cancels a running sequence.\n */",
|
|
1698
|
+
"decl": "export async function sequence(steps: HapticStep[]): Promise<HapticResult>;"
|
|
1699
|
+
},
|
|
1700
|
+
{
|
|
1701
|
+
"name": "vibrate",
|
|
1702
|
+
"kind": "function",
|
|
1703
|
+
"jsdoc": "/**\n * Raw vibration: `patternMs` alternates OFF / ON durations in milliseconds, starting\n * with an off gap (`[0, 200, 100, 400]` = buzz 200, pause 100, buzz 400).\n *\n * Android plays it as `VibrationEffect.createWaveform`. iOS approximates: with Core\n * Haptics each ON span is a continuous event (a transient one under 30 ms); without it,\n * each ON span becomes one `heavy` impact at its start. Web uses `navigator.vibrate`.\n *\n * Rejects (`RangeError`) on an empty list, a negative or non-finite duration, or a\n * pattern totalling over {@link MAX_DURATION_MS}.\n */",
|
|
1704
|
+
"decl": "export async function vibrate(patternMs: number[]): Promise<HapticResult>;"
|
|
1561
1705
|
},
|
|
1562
1706
|
{
|
|
1563
1707
|
"name": "version",
|
|
1564
1708
|
"kind": "const",
|
|
1565
1709
|
"jsdoc": "/** Capability version, for bundles that gate on a min binary version. */",
|
|
1566
|
-
"decl": "export const version =
|
|
1710
|
+
"decl": "export const version = 2;"
|
|
1567
1711
|
}
|
|
1568
1712
|
]
|
|
1569
1713
|
},
|
|
@@ -4867,7 +5011,13 @@
|
|
|
4867
5011
|
"name": "AlertOptions",
|
|
4868
5012
|
"kind": "interface",
|
|
4869
5013
|
"jsdoc": "",
|
|
4870
|
-
"decl": "export interface AlertOptions {\n /** Required — the native presenters drop alerts with an empty title. */\n title: string;\n message?: string;\n /** Confirm button label. Default \"Confirm\". */\n confirmLabel?: string;\n /** Cancel button label. OMIT for a single-button alert (no cancel button). */\n cancelLabel?: string;\n /** Style the confirm button with the `danger` token. */\n destructive?: boolean;\n}"
|
|
5014
|
+
"decl": "export interface AlertOptions {\n /** Required — the native presenters drop alerts with an empty title. */\n title: string;\n message?: string;\n /** Confirm button label. Default \"Confirm\". */\n confirmLabel?: string;\n /** Cancel button label. OMIT for a single-button alert (no cancel button). */\n cancelLabel?: string;\n /** Style the confirm button with the `danger` token. A destructive confirm is not the\n * affirmative button: its `confirm.actionId` still runs, but without gesture credit. */\n destructive?: boolean;\n /** The affirmative button's ACTION. Pressing it runs `actionId` as a NEW invocation\n * with user-gesture origin (so an app service's `app.<svc>.<action>` may return `_nav` and\n * navigate — the \"Ready — View\" alert). `label` replaces `confirmLabel`. The id is checked when\n * you set it, against this worker: a `capability.<id>.*` needs `<id>` allowed, an `app.*` from a\n * screen worker needs `app_services` and an exported action, a `tier5.*` must be this worker's\n * own; anything else rejects (`action_not_allowed`). Cancel and dismissal run nothing. */\n confirm?: AlertConfirm;\n}"
|
|
5015
|
+
},
|
|
5016
|
+
{
|
|
5017
|
+
"name": "AlertConfirm",
|
|
5018
|
+
"kind": "interface",
|
|
5019
|
+
"jsdoc": "/** The affirmative button of an `alert()`: its label and the action its press runs. */",
|
|
5020
|
+
"decl": "export interface AlertConfirm {\n label: string;\n actionId: string;\n /** Passed to the action (JSON types kept). */\n args?: Record<string, unknown>;\n}"
|
|
4871
5021
|
},
|
|
4872
5022
|
{
|
|
4873
5023
|
"name": "AlertResult",
|
|
@@ -4878,7 +5028,7 @@
|
|
|
4878
5028
|
{
|
|
4879
5029
|
"name": "alert",
|
|
4880
5030
|
"kind": "function",
|
|
4881
|
-
"jsdoc": "/** Present a modal alert on the current screen. Resolves when the user picks a\n * button. Rejects with `ui_busy` while another alert() is pending, and with\n * `no_ui_host` if the app has no presenter attached. */",
|
|
5031
|
+
"jsdoc": "/** Present a modal alert on the current screen. Resolves when the user picks a\n * button. Rejects with `ui_busy` while another alert() is pending, and with\n * `no_ui_host` if the app has no presenter attached — or, from an APP SERVICE, when no\n * shell is in the foreground (re-show it from `onForeground`). An app service's alert shows on\n * whatever screen is open. */",
|
|
4882
5032
|
"decl": "export async function alert(options: AlertOptions): Promise<AlertResult>;"
|
|
4883
5033
|
},
|
|
4884
5034
|
{
|
|
@@ -19,15 +19,15 @@ the SDK sources, so it matches the shipped SDKs.
|
|
|
19
19
|
- [`base44`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fbase44?format=md) — A Base44 app's backend. 11 methods · Brandy: conditional.
|
|
20
20
|
- [`biometrics`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fbiometrics?format=md) — Biometric authentication capability. 2 methods.
|
|
21
21
|
- [`browser_auth`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fbrowser_auth?format=md) — Native browser SSO. 1 method.
|
|
22
|
-
- [`calendar`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fcalendar?format=md) — Device calendar.
|
|
22
|
+
- [`calendar`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fcalendar?format=md) — Device calendar. 10 methods.
|
|
23
23
|
- [`calls`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fcalls?format=md) — Real-time 1:1 audio calls (Stage 1). 16 methods.
|
|
24
24
|
- [`camera`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fcamera?format=md) — Camera capability. 4 methods · returns FileHandles.
|
|
25
|
-
- [`clipboard`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fclipboard?format=md) — System clipboard.
|
|
25
|
+
- [`clipboard`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fclipboard?format=md) — System clipboard. 5 methods.
|
|
26
26
|
- [`contacts`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fcontacts?format=md) — Device address book. 4 methods.
|
|
27
27
|
- [`file_picker`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Ffile_picker?format=md) — User-mediated document access OUTSIDE the app sandbox. 3 methods · returns FileHandles.
|
|
28
28
|
- [`files`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Ffiles?format=md) — App-scoped document storage. 6 methods · returns FileHandles.
|
|
29
29
|
- [`godot`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fgodot?format=md) — A Godot game surface. 3 methods · not in Brandy.
|
|
30
|
-
- [`haptic`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fhaptic?format=md) — Haptic capability.
|
|
30
|
+
- [`haptic`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fhaptic?format=md) — Haptic capability. 3 methods.
|
|
31
31
|
- [`health`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fhealth?format=md) — Platform HEALTH store. 5 methods.
|
|
32
32
|
- [`himi_apps`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fhimi_apps?format=md) — Cross-app open / host queries. 4 methods · not in Brandy.
|
|
33
33
|
- [`iap`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fiap?format=md) — In-app-purchase capability. 9 methods.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1579.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
|
|
6
6
|
"keywords": [
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"artifactId": "himalaya-cli"
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
|
-
"falconPackageHash": "
|
|
74
|
+
"falconPackageHash": "939c5230409902d493ec5b28c46a83496669032bc8f663eabd42dcf6"
|
|
75
75
|
}
|
|
@@ -20977,6 +20977,20 @@ const SCREENS = runtimeSetting({
|
|
|
20977
20977
|
"hiddenPath": "$item.noTypeTag"
|
|
20978
20978
|
}
|
|
20979
20979
|
},
|
|
20980
|
+
{
|
|
20981
|
+
"id": "himi_services.row.$item.id/ended",
|
|
20982
|
+
"body": {
|
|
20983
|
+
"package": "himalaya",
|
|
20984
|
+
"type": "Text",
|
|
20985
|
+
"props": {
|
|
20986
|
+
"text": "$item.endedText",
|
|
20987
|
+
"style": "caption",
|
|
20988
|
+
"color": "textSecondary",
|
|
20989
|
+
"maxLines": 1,
|
|
20990
|
+
"collapseWhenEmpty": true
|
|
20991
|
+
}
|
|
20992
|
+
}
|
|
20993
|
+
},
|
|
20980
20994
|
{
|
|
20981
20995
|
"id": "",
|
|
20982
20996
|
"body": {
|
|
@@ -80,6 +80,19 @@ type CoursePolicyShape = {
|
|
|
80
80
|
};
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
/** A completed course, independent of whether bookings are currently allowed. Missing,
|
|
84
|
+
* invalid or reversed dates are unknown, never evidence that a course has ended. */
|
|
85
|
+
export function courseHasEnded(service: CoursePolicyShape, now: number): boolean {
|
|
86
|
+
const start = service.schedule?.firstSessionStart ?? "";
|
|
87
|
+
const end = service.schedule?.lastSessionEnd ?? "";
|
|
88
|
+
// The schedule has no zone beside these fields. A local wall time must not be interpreted
|
|
89
|
+
// in the device's zone and presented as a confirmed business end instant.
|
|
90
|
+
if (!HAS_OFFSET.test(start) || !HAS_OFFSET.test(end)) return false;
|
|
91
|
+
const first = validDate(start)?.getTime() ?? NaN;
|
|
92
|
+
const last = validDate(end)?.getTime() ?? NaN;
|
|
93
|
+
return Number.isFinite(now) && Number.isFinite(first) && Number.isFinite(last) && first <= last && last <= now;
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
/** Whether the business takes course bookings at `now` (ms): the course has not ended, has not
|
|
84
97
|
* started (unless the policy lets people join late), and `now` is inside its booking window —
|
|
85
98
|
* which the early and late limits count from the first session. */
|
|
@@ -238,6 +251,14 @@ export function serviceDurationText(service: PricedServiceShape, label: BookingL
|
|
|
238
251
|
|
|
239
252
|
function validDate(value: string | undefined): Date | undefined {
|
|
240
253
|
if (!value) return undefined;
|
|
254
|
+
const parts = /^(\d{4})-(\d{2})-(\d{2})(?:T|$)/i.exec(value);
|
|
255
|
+
if (!parts) return undefined;
|
|
256
|
+
const year = Number(parts[1]), month = Number(parts[2]) - 1, day = Number(parts[3]);
|
|
257
|
+
// Date accepts February 30 by rolling into March. Check the stated calendar day first;
|
|
258
|
+
// setUTCFullYear also preserves years 0–99, which Date.UTC would reinterpret as 1900–1999.
|
|
259
|
+
const calendar = new Date(0);
|
|
260
|
+
calendar.setUTCFullYear(year, month, day);
|
|
261
|
+
if (calendar.getUTCFullYear() !== year || calendar.getUTCMonth() !== month || calendar.getUTCDate() !== day) return undefined;
|
|
241
262
|
const date = new Date(value);
|
|
242
263
|
return Number.isFinite(date.getTime()) ? date : undefined;
|
|
243
264
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
export type CopyPackValue = string | { source: string; translation: string };
|
|
22
22
|
|
|
23
23
|
let stashedCopy: Record<string, CopyPackValue> = Object.create(null);
|
|
24
|
+
let copyReceived = false;
|
|
24
25
|
|
|
25
26
|
/** Absorb the `copy` onAppear param (customer strings or stock translation records)
|
|
26
27
|
* from an action arg. Call first in every action that can receive onAppear
|
|
@@ -44,15 +45,19 @@ export function stashCopy(arg: unknown): void {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
stashedCopy = next;
|
|
48
|
+
copyReceived = true;
|
|
47
49
|
} catch {
|
|
48
50
|
// Malformed pack param — keep the stock copy rather than dying mid-action.
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/** The provisioned override for `key`, or `fallback` (the stock copy)
|
|
53
|
-
*
|
|
54
|
-
export function copyParam(key: string, fallback: string): string {
|
|
54
|
+
/** The provisioned override for `key`, or `fallback` (the stock copy). An optional
|
|
55
|
+
* restored label survives only until a current copy pack arrives. */
|
|
56
|
+
export function copyParam(key: string, fallback: string, restoredFallback = fallback): string {
|
|
55
57
|
const value = stashedCopy[key];
|
|
58
|
+
// A disk-restored row may paint before onAppear supplies its copy pack. Keep its
|
|
59
|
+
// last label only before a pack arrives; even an empty current pack selects stock copy.
|
|
60
|
+
if (value === undefined) return copyReceived ? fallback : restoredFallback;
|
|
56
61
|
if (typeof value === "string") return value !== "" ? value : fallback;
|
|
57
62
|
return value?.source === fallback && value.translation !== "" ? value.translation : fallback;
|
|
58
63
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { defineBackendCollection } from "../_shared/collection-service.js";
|
|
11
11
|
import { COLLECTION_CHROME_MIN_ITEMS } from "../_shared/collection-contract.js";
|
|
12
12
|
import { queryServiceCategories, queryServices, type WixService } from "@himalaya/wix-bookings";
|
|
13
|
-
import { categoryName, courseDatesText, serviceDurationText, servicePriceLine, type BookingLabel } from "../_shared/himi-bookings.js";
|
|
13
|
+
import { categoryName, courseDatesText, courseHasEnded, serviceDurationText, servicePriceLine, type BookingLabel } from "../_shared/himi-bookings.js";
|
|
14
14
|
import { formatServicePrice } from "../_shared/wix-service-price.js";
|
|
15
15
|
import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
|
|
16
16
|
import { initials } from "../_shared/himi-text.js";
|
|
@@ -60,6 +60,9 @@ interface Row {
|
|
|
60
60
|
/** "Class" or "Course"; "" for an appointment. */
|
|
61
61
|
typeTag: string;
|
|
62
62
|
noTypeTag: boolean;
|
|
63
|
+
endedText: string;
|
|
64
|
+
/** Retain course dates so a cached catalog can refresh its status after the course ends. */
|
|
65
|
+
courseSchedule?: { firstSessionStart?: string; lastSessionEnd?: string };
|
|
63
66
|
/** "1 hr · ₪70"; a course: "Nov 3 – Dec 15, 2026 · ₪1,200". "" when the record has neither. */
|
|
64
67
|
meta: string;
|
|
65
68
|
/** Online booking by time is possible: online booking is on and the service is not a course. */
|
|
@@ -186,8 +189,8 @@ function typeTagOf(type: unknown): string {
|
|
|
186
189
|
return "";
|
|
187
190
|
}
|
|
188
191
|
|
|
189
|
-
function spokenName(row: Pick<Row, "name" | "typeTag" | "tagline" | "meta">): string {
|
|
190
|
-
return [row.name, row.typeTag, row.tagline, row.meta].filter(Boolean).join(", ");
|
|
192
|
+
function spokenName(row: Pick<Row, "name" | "typeTag" | "tagline" | "meta" | "endedText">): string {
|
|
193
|
+
return [row.name, row.typeTag, row.endedText, row.tagline, row.meta].filter(Boolean).join(", ");
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
function bookCtaFor(request: boolean): string {
|
|
@@ -212,6 +215,11 @@ function norm(service: WixService): Row {
|
|
|
212
215
|
].filter(Boolean);
|
|
213
216
|
const image = imageUrl(service);
|
|
214
217
|
const isCourse = service.type === "COURSE";
|
|
218
|
+
const courseSchedule = isCourse ? {
|
|
219
|
+
firstSessionStart: service.schedule?.firstSessionStart,
|
|
220
|
+
lastSessionEnd: service.schedule?.lastSessionEnd,
|
|
221
|
+
} : undefined;
|
|
222
|
+
const endedText = isCourse && courseHasEnded(service, Date.now()) ? copyParam("services.ended", "Ended") : "";
|
|
215
223
|
const price = servicePriceLine(service, listLabel).text;
|
|
216
224
|
const meta = [isCourse ? courseDatesText(service) : serviceDurationText(service, listLabel), price].filter(Boolean).join(" · ");
|
|
217
225
|
const typeTag = typeTagOf(service.type);
|
|
@@ -234,18 +242,20 @@ function norm(service: WixService): Row {
|
|
|
234
242
|
tagline,
|
|
235
243
|
typeTag,
|
|
236
244
|
noTypeTag: !typeTag,
|
|
245
|
+
endedText,
|
|
246
|
+
courseSchedule,
|
|
237
247
|
meta,
|
|
238
248
|
canBook,
|
|
239
249
|
noBook: !canBook,
|
|
240
250
|
bookCta: bookCtaFor(request),
|
|
241
251
|
bookA11y: bookLabel(name, request),
|
|
242
|
-
a11y: spokenName({ name, typeTag, tagline, meta }),
|
|
252
|
+
a11y: spokenName({ name, typeTag, endedText, tagline, meta }),
|
|
243
253
|
};
|
|
244
254
|
}
|
|
245
255
|
|
|
246
256
|
/** A row saved by an older worker (offline cache, restored state) has none of the new
|
|
247
257
|
* fields: it renders with no tagline, meta or tag, and no Book pill until the live read. */
|
|
248
|
-
function withRowDefaults(row: Partial<Row
|
|
258
|
+
function withRowDefaults(row: Partial<Row>, shownEnded?: string): Row {
|
|
249
259
|
const name = typeof row.name === "string" && row.name ? row.name : "Service";
|
|
250
260
|
const str = (v: unknown) => (typeof v === "string" ? v : "");
|
|
251
261
|
const image = str(row.image);
|
|
@@ -253,6 +263,9 @@ function withRowDefaults(row: Partial<Row>): Row {
|
|
|
253
263
|
const tagline = str(row.tagline);
|
|
254
264
|
const meta = str(row.meta);
|
|
255
265
|
const canBook = row.canBook === true;
|
|
266
|
+
const courseSchedule = row.courseSchedule;
|
|
267
|
+
const endedText = courseSchedule && courseHasEnded({ schedule: courseSchedule }, Date.now())
|
|
268
|
+
? copyParam("services.ended", "Ended", shownEnded || str(row.endedText) || "Ended") : "";
|
|
256
269
|
return {
|
|
257
270
|
...row,
|
|
258
271
|
id: str(row.id),
|
|
@@ -268,12 +281,13 @@ function withRowDefaults(row: Partial<Row>): Row {
|
|
|
268
281
|
tagline,
|
|
269
282
|
typeTag,
|
|
270
283
|
noTypeTag: !typeTag,
|
|
284
|
+
endedText,
|
|
271
285
|
meta,
|
|
272
286
|
canBook,
|
|
273
287
|
noBook: !canBook,
|
|
274
288
|
bookCta: str(row.bookCta) || bookCtaFor(false),
|
|
275
289
|
bookA11y: str(row.bookA11y) || bookLabel(name),
|
|
276
|
-
a11y:
|
|
290
|
+
a11y: spokenName({ name, typeTag, endedText, tagline, meta }),
|
|
277
291
|
};
|
|
278
292
|
}
|
|
279
293
|
|
|
@@ -287,6 +301,9 @@ let categoriesInFlight = false;
|
|
|
287
301
|
let categoriesFailedAt = 0;
|
|
288
302
|
const CATEGORY_RETRY_MS = 60_000;
|
|
289
303
|
const rowCategories = new Map<string, number>();
|
|
304
|
+
/** A restored Ended label bridges only the first live page after boot, because an app that sends no
|
|
305
|
+
* copy pack gives no sign that none is coming: after it, current copy (pack or stock) decides. */
|
|
306
|
+
let restoredEndedBridge = true;
|
|
290
307
|
|
|
291
308
|
/** Repaint the chips only, on the state already shown. Re-deriving from the query controller
|
|
292
309
|
* would publish its working state, which a revalidation deliberately holds back (a restored list
|
|
@@ -302,7 +319,7 @@ function withChips(s: S): S {
|
|
|
302
319
|
// rows of the whole list (no category, no query), or the stand-in would be one category.
|
|
303
320
|
const whole = filter === "all" && !(typeof collection?.query === "string" && collection.query.trim());
|
|
304
321
|
// Not while a load is out: the rows on screen may still be a previous search's.
|
|
305
|
-
if (!siteCategories?.length && rowCategories.size === 0 && whole && s.isLoading !== true) rememberRowCategories((s.results ?? []).map(withRowDefaults));
|
|
322
|
+
if (!siteCategories?.length && rowCategories.size === 0 && whole && s.isLoading !== true) rememberRowCategories((s.results ?? []).map((row) => withRowDefaults(row)));
|
|
306
323
|
const chips = categoryChips(filter);
|
|
307
324
|
return { ...s, categoryChips: chips, hideCategories: collection?.hideChrome === true || (chips.length < 3 && !filter.startsWith("value:")) };
|
|
308
325
|
}
|
|
@@ -345,6 +362,11 @@ function categoryChips(filter: string): CategoryChip[] {
|
|
|
345
362
|
|
|
346
363
|
const svc = defineBackendCollection<S, Row>("himiBookingServices", {
|
|
347
364
|
restorePagesOnLoad: true,
|
|
365
|
+
onLoad() {
|
|
366
|
+
// Copy is stashed before this hook. Repaint restored labels immediately, even
|
|
367
|
+
// when the background query holds the old page through a slow or failed read.
|
|
368
|
+
(svc as unknown as { normalizeRestored?: () => unknown }).normalizeRestored?.();
|
|
369
|
+
},
|
|
348
370
|
state: { status: "loading", query: "", all: [], results: [], emptyText: "", countLabel: "", errored: false,
|
|
349
371
|
notErrored: true, isLoading: true, notLoading: false, hasResults: false, noEmpty: true,
|
|
350
372
|
categoryChips: [], hideCategories: true, hideRows: false, noMatchText: "", hasQuery: false, noQuery: true, categoriesLabel: "" },
|
|
@@ -377,11 +399,10 @@ const svc = defineBackendCollection<S, Row>("himiBookingServices", {
|
|
|
377
399
|
},
|
|
378
400
|
actions: {
|
|
379
401
|
categoriesLanded: (s) => withChips(s),
|
|
380
|
-
//
|
|
381
|
-
// `noBook`
|
|
402
|
+
// Normalize old snapshots and refresh Ended/copy before revalidation settles.
|
|
403
|
+
// A missing `noBook` must never show Book on a restored course.
|
|
382
404
|
normalizeRestored: (s) => {
|
|
383
405
|
const rows = Array.isArray(s.results) ? s.results : [];
|
|
384
|
-
if (rows.every((row) => typeof (row as Partial<Row>).noBook === "boolean")) return s;
|
|
385
406
|
const fixed = rows.map((row) => withRowDefaults(row as Partial<Row>));
|
|
386
407
|
return { ...s, results: fixed, all: fixed };
|
|
387
408
|
},
|
|
@@ -389,7 +410,11 @@ const svc = defineBackendCollection<S, Row>("himiBookingServices", {
|
|
|
389
410
|
apply(s, page) {
|
|
390
411
|
const loading = page.status === "loading";
|
|
391
412
|
const errored = page.status === "error";
|
|
392
|
-
|
|
413
|
+
// A live row is built before a cold boot's copy pack arrives: the first live page keeps the
|
|
414
|
+
// Ended label the restored row for the same course showed, until a current pack replaces it.
|
|
415
|
+
const shown = new Map((restoredEndedBridge && Array.isArray(s.results) ? s.results : []).map((row) => [row.id, row.endedText]));
|
|
416
|
+
const rows = page.items.map((item) => withRowDefaults(item, shown.get(item.id)));
|
|
417
|
+
if (page.metadata?.fromCache === false && !loading && !errored) restoredEndedBridge = false;
|
|
393
418
|
// The stand-in categories come from the accepted rows of the whole list (a superseded
|
|
394
419
|
// response never reaches apply): a live page replaces them, the device's copy only fills a gap.
|
|
395
420
|
if (!page.criteria.query && page.criteria.filter === "all" && rows.length > 0) {
|
|
@@ -427,7 +452,6 @@ const svc = defineBackendCollection<S, Row>("himiBookingServices", {
|
|
|
427
452
|
// (there is no Refine now): both go back to the business's own view before the first read.
|
|
428
453
|
type Retired = { getState?: () => { collection?: { filter?: unknown; sort?: unknown } };
|
|
429
454
|
collectionFilter?: (arg: unknown) => unknown; collectionSort?: (arg: unknown) => unknown };
|
|
430
|
-
(svc as unknown as { normalizeRestored?: () => unknown }).normalizeRestored?.();
|
|
431
455
|
const retired = svc as unknown as Retired;
|
|
432
456
|
const persisted = retired.getState?.()?.collection;
|
|
433
457
|
if (persisted?.filter === "saved") void retired.collectionFilter?.({ selectedIds: ["all"] });
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
export type CopyPackValue = string | { source: string; translation: string };
|
|
22
22
|
|
|
23
23
|
let stashedCopy: Record<string, CopyPackValue> = Object.create(null);
|
|
24
|
+
let copyReceived = false;
|
|
24
25
|
|
|
25
26
|
/** Absorb the `copy` onAppear param (customer strings or stock translation records)
|
|
26
27
|
* from an action arg. Call first in every action that can receive onAppear
|
|
@@ -44,15 +45,19 @@ export function stashCopy(arg: unknown): void {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
stashedCopy = next;
|
|
48
|
+
copyReceived = true;
|
|
47
49
|
} catch {
|
|
48
50
|
// Malformed pack param — keep the stock copy rather than dying mid-action.
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/** The provisioned override for `key`, or `fallback` (the stock copy)
|
|
53
|
-
*
|
|
54
|
-
export function copyParam(key: string, fallback: string): string {
|
|
54
|
+
/** The provisioned override for `key`, or `fallback` (the stock copy). An optional
|
|
55
|
+
* restored label survives only until a current copy pack arrives. */
|
|
56
|
+
export function copyParam(key: string, fallback: string, restoredFallback = fallback): string {
|
|
55
57
|
const value = stashedCopy[key];
|
|
58
|
+
// A disk-restored row may paint before onAppear supplies its copy pack. Keep its
|
|
59
|
+
// last label only before a pack arrives; even an empty current pack selects stock copy.
|
|
60
|
+
if (value === undefined) return copyReceived ? fallback : restoredFallback;
|
|
56
61
|
if (typeof value === "string") return value !== "" ? value : fallback;
|
|
57
62
|
return value?.source === fallback && value.translation !== "" ? value.translation : fallback;
|
|
58
63
|
}
|