@wix/himalaya-cli 0.995.0 → 0.997.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.
Binary file
@@ -316,7 +316,8 @@ every **foreign** collection — one this app neither owns nor declares:
316
316
  { "id": "QaRecipes", "displayName": "Recipes", "readRole": "ANYONE", "itemCount": null,
317
317
  "fields": [ { "key": "title", "type": "TEXT" }, { "key": "cookMinutes", "type": "NUMBER" } ] }
318
318
  ],
319
- "truncated": 6
319
+ "truncated": 6,
320
+ "others": [ { "id": "Orders", "displayName": "Orders" } ]
320
321
  }
321
322
  }
322
323
  ```
@@ -333,16 +334,23 @@ before declaring it, rather than guessing; the scan will not invent one for you.
333
334
  - **Foreign collections are never item-counted** (`itemCount: null`). A row count is one
334
335
  query each, and fanning that across a whole site is the cost this scan must not pay.
335
336
  Declare or adopt the collection and the count arrives with the next `himi cms query`.
336
- - **Only the first 25 are described.** `foreign.count` is how many the scan listed and
337
- `foreign.truncated` says how many of those went undescribed, so a short list is never
338
- mistaken for a short site. For the full per-collection picture — field types,
339
- read/write roles and sample rows — use
340
- [`himi site analyze --site <msid>`](wix-business-context.md).
341
- - **`foreign.count` is not a site census.** `GET /data/v2/collections` is paged and this
342
- scan reads one page. When Wix reports a total larger than that page,
343
- `unlistedCollections` says how many the listing left out; when Wix reports no total,
344
- neither field can tell you, so treat `foreign.count` as "foreign collections in what we
345
- listed" and use `site analyze` if you need certainty about a very large site.
337
+ - **Only the first 25 are described; every one is named.** `foreign.count` is how many the
338
+ scan listed and `foreign.truncated` says how many of those went undescribed — and
339
+ `foreign.others` still lists each of them as `{ id, displayName }`, so "does this site
340
+ already have an orders collection?" is answerable from this one call. To see one of them
341
+ in full, declare it (`wixId`) and run `himi cms query` again, or use
342
+ [`himi site analyze --site <msid>`](wix-business-context.md) for the per-collection
343
+ picture — field types, read/write roles and sample rows.
344
+ - **The listing is followed page by page.** `GET /data/v2/collections` is offset-paged, and
345
+ the scan reads until Wix's `pagingMetadata.total` is reached — or, when Wix flags
346
+ `tooManyToCount` and sends no total, until a page comes back empty. If the walk stops
347
+ before that — Wix stopped honoring the offset, or the page backstop was hit — the report
348
+ carries `listingIncomplete: true`, and `foreign` covers only what was listed.
349
+ `unlistedCollections` adds how many were left out when Wix gave a total; with
350
+ `tooManyToCount` nobody knows, so only the flag is set. A declared collection the partial
351
+ listing did not reach is read by its id, so it is never reported as `missing` just
352
+ because the scan stopped short. When Wix reports no total and does not flag
353
+ `tooManyToCount`, one page is taken as the whole listing.
346
354
 
347
355
  **The scan is owner-scoped, and there is no tokenless alternative.** It needs
348
356
  `--site <msid>` (or `WIX_SITE_ID`) and owner auth via `wix token`, because enumerating
@@ -128,7 +128,7 @@
128
128
  "bindings": [
129
129
  "title"
130
130
  ],
131
- "propsSource": "export interface ButtonProps {\n /** Button label — binding-aware (\"$item.\"/\"$service:\") on web, iOS and Android. */\n title: string;\n action: Action;\n style?: string;\n disabled?: boolean;\n /** Capability enrichments. */\n leadingIcon?: IconRef;\n trailingIcon?: IconRef;\n loading?: boolean;\n size?: \"sm\" | \"md\" | \"lg\";\n fullWidth?: boolean;\n iconOnly?: boolean;\n /** #1049 — corner radius; reshapes the button (overrides the forced pill). */\n cornerRadius?: number;\n /** #1049 — minimum height (pt/dp/px). */\n minHeight?: number;\n}"
131
+ "propsSource": "export interface ButtonProps {\n /** Button label — binding-aware (\"$item.\"/\"$service:\") on web, iOS and Android. */\n title: string;\n action: Action;\n style?: string;\n /** Literal only, NOT bindable: a `\"$service:…\"` string here fails `validate`, and there is no\n * `disabledPath`. To disable from worker state, author two buttons — the live one and a\n * `disabled: true` copy — and swap them with complementary `modifiers.hiddenPath` flags\n * (e.g. `canSubmit` / `cannotSubmit`); also guard the action so a stray tap is a no-op. */\n disabled?: boolean;\n /** Capability enrichments. */\n leadingIcon?: IconRef;\n trailingIcon?: IconRef;\n loading?: boolean;\n size?: \"sm\" | \"md\" | \"lg\";\n fullWidth?: boolean;\n iconOnly?: boolean;\n /** #1049 — corner radius; reshapes the button (overrides the forced pill). */\n cornerRadius?: number;\n /** #1049 — minimum height (pt/dp/px). */\n minHeight?: number;\n}"
132
132
  },
133
133
  {
134
134
  "name": "TextField",
@@ -475,7 +475,7 @@
475
475
  ]
476
476
  }
477
477
  ],
478
- "propsSource": "export interface FilterChipsProps {\n chips: FilterChip[];\n /** Literal ids, or a binding-aware `$service:`/`$item.` live selection. */\n selectedIds?: string[];\n multiSelect?: boolean;\n onChange?: Action;\n /** `$service:` binding → a live options array (overrides `chips`). */\n optionsPath?: string;\n}"
478
+ "propsSource": "export interface FilterChipsProps {\n chips: FilterChip[];\n /** Literal ids, or binding-aware: put the `$service:` sentinel INSIDE the array as its ONE\n * element — `selectedIds: [\"$service:<svc>.<field>\"]`, where the field holds the selected\n * ids. A bare `\"$service:…\"` string is rejected by `validate` — the prop is an array. */\n selectedIds?: string[];\n multiSelect?: boolean;\n onChange?: Action;\n /** `$service:` binding → a live options array (overrides `chips`). */\n optionsPath?: string;\n}"
479
479
  },
480
480
  {
481
481
  "name": "Tabs",
@@ -1258,6 +1258,7 @@
1258
1258
  "Presentation": "export type Presentation =\n | \"push\"\n | \"sheet\"\n | \"replace\"\n | \"dismiss\"\n | \"back\"\n | \"full_screen\"\n | \"alert\"\n | \"snackbar\";",
1259
1259
  "FilterChip": "export interface FilterChip {\n id: string;\n label: string;\n leadingIcon?: IconRef;\n count?: number;\n}",
1260
1260
  "TabItem": "export interface TabItem {\n id: string;\n label: string;\n badge?: string;\n}",
1261
+ "PickerOption": "export interface PickerOption {\n id: string;\n label: string;\n}",
1261
1262
  "GaugeRing": "export interface GaugeRing {\n /** Fraction 0..1 for this ring. */\n value?: number;\n /** `$service:`/`$item.` binding → a live number (overrides `value`). */\n valuePath?: string;\n tint?: TokenName;\n}",
1262
1263
  "WidgetTimeRef": "export interface WidgetTimeRef {\n /** Epoch seconds the style counts from/to. */\n at: number;\n /** SwiftUI `Text.DateStyle` / Android time style (default \"timer\"). */\n style?: \"timer\" | \"relative\" | \"date\" | \"time\" | \"offset\";\n}",
1263
1264
  "Border": "export interface Border {\n width: number;\n color: TokenRef;\n}"
@@ -481,6 +481,10 @@ export interface ButtonProps {
481
481
  title: string;
482
482
  action: Action;
483
483
  style?: string;
484
+ /** Literal only, NOT bindable: a `"$service:…"` string here fails `validate`, and there is no
485
+ * `disabledPath`. To disable from worker state, author two buttons — the live one and a
486
+ * `disabled: true` copy — and swap them with complementary `modifiers.hiddenPath` flags
487
+ * (e.g. `canSubmit` / `cannotSubmit`); also guard the action so a stray tap is a no-op. */
484
488
  disabled?: boolean;
485
489
  /** Capability enrichments. */
486
490
  leadingIcon?: IconRef;
@@ -904,7 +908,9 @@ export interface SearchFieldProps {
904
908
  ```ts
905
909
  export interface FilterChipsProps {
906
910
  chips: FilterChip[];
907
- /** Literal ids, or a binding-aware `$service:`/`$item.` live selection. */
911
+ /** Literal ids, or binding-aware: put the `$service:` sentinel INSIDE the array as its ONE
912
+ * element — `selectedIds: ["$service:<svc>.<field>"]`, where the field holds the selected
913
+ * ids. A bare `"$service:…"` string is rejected by `validate` — the prop is an array. */
908
914
  selectedIds?: string[];
909
915
  multiSelect?: boolean;
910
916
  onChange?: Action;
@@ -2141,7 +2147,8 @@ export interface LogicBundleRef {
2141
2147
 
2142
2148
  A few props take a typed **object** rather than nested components. Some are arrays of them:
2143
2149
  `FilterChips.chips` is a `FilterChip[]`, `SegmentedControl.options` / `Tabs.tabs` are a
2144
- `TabItem[]`, and `Gauge.rings` is a `GaugeRing[]` (concentric rings, drawn outer→inner).
2150
+ `TabItem[]`, `Picker.options` / `WheelPicker.options` are a `PickerOption[]`, and
2151
+ `Gauge.rings` is a `GaugeRing[]` (concentric rings, drawn outer→inner).
2145
2152
  One is a single object: `Text.timeRef` is a `WidgetTimeRef`, the widget-safe live clock —
2146
2153
  note its `at` is REQUIRED and is in epoch SECONDS. The prop blocks above name these types;
2147
2154
  here are the interfaces themselves (for the arrays, each item's own fields — `id` is what an
@@ -2161,6 +2168,11 @@ export interface TabItem {
2161
2168
  badge?: string;
2162
2169
  }
2163
2170
 
2171
+ export interface PickerOption {
2172
+ id: string;
2173
+ label: string;
2174
+ }
2175
+
2164
2176
  export interface GaugeRing {
2165
2177
  /** Fraction 0..1 for this ring. */
2166
2178
  value?: number;
@@ -2444,3 +2456,30 @@ export type Presentation =
2444
2456
  | "alert"
2445
2457
  | "snackbar";
2446
2458
  ```
2459
+
2460
+ ### What each `presentation` does
2461
+
2462
+ The verb decides what happens to the navigation stack. Where there is a target screen it is
2463
+ `params.screenId`, and an action that carries a `screenId` but no verb is a `push`.
2464
+
2465
+ | `presentation` | Effect |
2466
+ |---|---|
2467
+ | `push` | Open `screenId` on top of the current screen; Back returns to where you were. The default. |
2468
+ | `replace` | Open `screenId` without keeping the current screen to go Back to. The hosts differ on what Back does next: on iOS and Android the ACTIVE stack is reset to its root before `screenId` is pushed (the open sheet's or cover's own stack when one is up, otherwise the current tab's or the app's single stack), so Back returns to that root and, inside a modal, a further Back closes the modal; on web only the top screen of the current tab is swapped, so Back returns to the screen that was beneath it. |
2469
+ | `back` | Pop the current screen (nothing happens at a stack root). On iOS and Android, inside a sheet or cover it pops the modal's own stack first and closes the modal at its root; on web it pops the tab's stack, so close a modal with `dismiss`. |
2470
+ | `dismiss` | Close the presented sheet or full-screen cover (on web, an open alert too). Nothing happens when no modal is up. |
2471
+ | `sheet` | Present `screenId` as a sheet over the current screen. One at a time: presenting another replaces it. |
2472
+ | `full_screen` | Present `screenId` as a full-screen cover. |
2473
+ | `alert` | Show an alert built from `args`: `title` (required), `message`, `confirmLabel`, `cancelLabel`, `destructive`, and `confirmActionId` / `cancelActionId` — the action each button runs, with this action's `params`. No `screenId`. |
2474
+ | `snackbar` | Show a transient message from `args`: `message` (required) and `tone`. No `screenId`. |
2475
+
2476
+ A Close or Done button is a descriptor action: `{ "actionId": "himi.open", "presentation": "dismiss" }`
2477
+ (or `"back"`). Two limits to know before you design a flow around them:
2478
+
2479
+ - **No action switches tabs.** A `himi.open` whose target is another tab's root screen PUSHES
2480
+ that screen onto the current tab's stack; it does not select the other tab. Only the tab bar
2481
+ and other app chrome select a tab.
2482
+ - **A worker can only push.** A worker action that returns `_nav` (`navigate()` from
2483
+ `@himalaya/state`, see `himi docs tier-5-add-a-screen`) opens a linkable screen as a
2484
+ `push`. There is no `_nav` form of `back` or `dismiss`: put those verbs on a descriptor
2485
+ action.
@@ -69,11 +69,15 @@ decision.
69
69
 
70
70
  **Kit typography.** A design kit (or an app's tokens) may set a per-style `family`. The rules:
71
71
  every typography entry must stay **complete** (`size`+`weight`+`line` — native decode is strict, a
72
- family-only entry blanks the whole token set; CI-gated); a `family` must be **registered in the host
73
- app** or it silently falls back to the system font; safe cross-platform values today are the system
74
- default (no family), `Wix Madefor Text`/`Wix Madefor Display` where the host registers them, and
75
- `Georgia` (an OS font on iOS/macOS/web;
76
- approximated by the `serif` generic on Android).
72
+ family-only entry blanks the whole token set; CI-gated); a `family` must come from the **font
73
+ catalog** — `himi fonts list` is the whole vocabulary (curated families such as Playfair Display,
74
+ Karla, Oswald or Lora; the `Wix Madefor Text`/`Wix Madefor Display` pair every host carries; and
75
+ `Georgia`, an OS font on iOS/macOS/web approximated by the `serif` generic on Android) — or be
76
+ `$system` (the platform face by name — how an idiom overlay goes native over a brand base).
77
+ Anything else silently falls back to the system font. Naming a catalog family in tokens is all it
78
+ takes: `himi push` publishes its faces with the release and every renderer, web included, loads
79
+ them. Run `himi fonts check` before pushing — it fails on a name outside the catalog, and on a
80
+ family whose faces could not be fetched, which would otherwise ship as its fallback on web.
77
81
 
78
82
  ### Token discipline — every value is a `TokenRef`, never a literal
79
83
  Colors, gradients, shadows, materials, motions are named tokens (`{ token: "accentPrimary" }`), never
@@ -70,9 +70,11 @@ the UX source of truth even if that guide is absent from the installed CLI.
70
70
  `himi init my-shop --template branded-lite` (a full consumer reference over a Wix
71
71
  site — store/bookings/events/blog/cart/CMS, wired to `@himalaya/wix-*` + a visitor session)
72
72
  or `--template owner-lite` (owner CRUD/management: lists, details, forms). That reference is
73
- **dozens of screens** with a generated `dev/index.ts` — keep it and trim to the approved
74
- `SPEC.md` (`himi docs trim-a-template`); do not delete `tier5-src/` + `dev/` and start from
75
- blank. For a small **non-Wix** pattern, `himi init --template chat-bot --remote`. `himi template
73
+ **dozens of screens** in one megabytes-long `dev/index.ts` of raw descriptors. Keep its
74
+ worker layer — trim it or mine it (`himi docs trim-a-template`): **trim** to the approved
75
+ `SPEC.md` when most of its screens fit, or **mine** it when few do — keep the
76
+ `tier5-src/_shared/` helpers and Wix worker chains and write your own screens. Do not delete
77
+ `tier5-src/` + `dev/` and start from blank. For a small **non-Wix** pattern, `himi init --template chat-bot --remote`. `himi template
76
78
  list --remote` shows the full catalog (title/description/tags) — including templates published
77
79
  to Wix — and `--remote` on `himi init` fetches one from there (the bundled copy is
78
80
  the offline fallback). Publish any app of your own as a template with `himi template publish` —
@@ -86,8 +88,8 @@ template's identity, fixtures and boot-time worker calls would all have to be to
86
88
  when consent is **synthetic/local-only** and you need something that cannot auto-fetch (see the
87
89
  preflight below); or when you are **learning the wiring** and want one screen and one worker with
88
90
  nothing else in the file. The recipes below are written for exactly these cases. What blank is
89
- NOT good for is a real customer app whose shape a published template already has — trimming that
90
- template (`himi docs trim-a-template`) beats rebuilding it screen by screen.
91
+ NOT good for is a real customer app whose shape a published template already has — trimming or
92
+ mining that template (`himi docs trim-a-template`) beats rebuilding its Wix wiring from scratch.
91
93
 
92
94
  **A template is a SHOWCASE APP, so look at it before you pick it.** Each one is a real published
93
95
  release running on the plane: `GET /v1/templates.md` gives every row a `preview/a/<id>` URL that
@@ -210,10 +210,20 @@ JSC (iOS) and WebView (Android) worker scopes don't have them — so a bundle th
210
210
  die on a real device fails on your laptop. The most common instance:
211
211
 
212
212
  ```js
213
+ async function loadList() { /* fetch + shape; return the next state */ } // module level
214
+
213
215
  retry: async (s) => svc.actions.load(s), // ✗ `.actions` is undefined on device
214
- retry: async (s) => svc.load(s), // ✓ actions are DIRECT methods
216
+ retry: async () => loadList(), // ✓ call the helper, not the handle
215
217
  ```
216
218
 
219
+ Actions are DIRECT methods on the handle (`svc.load()`), and they take the action's payload
220
+ arguments, never the state. Even so, never call the handle from inside its own action:
221
+ `svc`'s type is inferred from the very object you are writing, so the reference is circular and
222
+ `validate --strict` rejects it (TS7022/TS7023), and passing `s` is
223
+ `Expected 0 arguments, but got 1`. Put the work in a module-level function, return it from both
224
+ `load` and `retry`, and keep `svc.load()` for the one module-eval kick at the bottom of the
225
+ bundle — the shape `himi docs recipe-dynamic-list` ships.
226
+
217
227
  With no fixture the crawl runs **offline** (the Wix transport rejects), which exercises the
218
228
  failure UI your screens should have. `--net` takes the ordinary net-mocks shape
219
229
  (`{ "rules": [...] }`) — the same file the on-device `fake.net` seam reads.
@@ -7262,7 +7262,7 @@
7262
7262
  "name": "WixServiceAvailabilityOptions",
7263
7263
  "kind": "interface",
7264
7264
  "jsdoc": "/** How to bound an availability query. `bookable` is a `query.filter` member — the\n * request's `query` has only `filter` and `sort`, so there is NO paging member and\n * a `paging: { limit }` you add is silently dropped rather than honoured. */",
7265
- "decl": "export interface WixServiceAvailabilityOptions {\n /** Bookable slots only. Defaults to `true`; pass `false` to get the unbookable\n * ones too (a greyed-out calendar), which is what the endpoint returns unfiltered. */\n bookable?: boolean;\n}"
7265
+ "decl": "export interface WixServiceAvailabilityOptions {\n /** Bookable slots only. Defaults to `true`; pass `false` to get the unbookable\n * ones too (a greyed-out calendar), which is what the endpoint returns unfiltered.\n * `false` OMITS the filter key rather than sending `bookable: false` — the latter\n * is a filter for unbookable slots ONLY, i.e. an empty calendar on a live site.\n * Unfiltered is the large response described below, so keep the window to the\n * days on screen. For unbookable slots alone, filter the entries on\n * `entry.bookable === false`. */\n bookable?: boolean;\n}"
7266
7266
  },
7267
7267
  {
7268
7268
  "name": "queryServiceAvailability",
@@ -7296,11 +7296,23 @@
7296
7296
  "jsdoc": "/** Convenience: staff members = resources tagged \"staff\" (filtered client-side). */",
7297
7297
  "decl": "export async function queryStaff(limit = 100): Promise<WixResource[]>;"
7298
7298
  },
7299
+ {
7300
+ "name": "WixSelectedPaymentOption",
7301
+ "kind": "type",
7302
+ "jsdoc": "/** How the customer intends to pay — the Create Booking `SelectedPaymentOption` enum.\n * `OFFLINE` = pay in person at the venue; `ONLINE` = pay through checkout;\n * `MEMBERSHIP` = a Wix Pricing Plan; `MEMBERSHIP_OFFLINE` = a pricing plan the\n * business redeems by hand. It must be one the service offers. */",
7303
+ "decl": "export type WixSelectedPaymentOption = \"OFFLINE\" | \"ONLINE\" | \"MEMBERSHIP\" | \"MEMBERSHIP_OFFLINE\";"
7304
+ },
7305
+ {
7306
+ "name": "WixBookSlotOptions",
7307
+ "kind": "interface",
7308
+ "jsdoc": "",
7309
+ "decl": "export interface WixBookSlotOptions {\n /** Sent as `booking.selectedPaymentOption`. Omitted from the request when unset,\n * so a call without it sends exactly the body it always did. */\n selectedPaymentOption?: WixSelectedPaymentOption;\n}"
7310
+ },
7299
7311
  {
7300
7312
  "name": "bookSlot",
7301
7313
  "kind": "function",
7302
- "jsdoc": "/** Book a specific availability slot — POST /bookings/v2/bookings. Pass the FULL\n * slot from queryServiceAvailability + the booker's contact. The v1 createBooking\n * above rejects a bare slot; the v2 route with bookedEntity.slot is the one that\n * works. Verified live (status CREATED). */",
7303
- "decl": "export function bookSlot(slot: WixBookingSlotFull, firstName: string, lastName: string, email: string): Promise<WixCreateBookingResponse>;",
7314
+ "jsdoc": "/** Book a specific availability slot — POST /bookings/v2/bookings. Pass the FULL\n * slot from queryServiceAvailability + the booker's contact. The v1 createBooking\n * above rejects a bare slot; the v2 route with bookedEntity.slot is the one that\n * works. Verified live (status CREATED).\n *\n * A visitor booking a PAID service to pay in person (no on-device payment) is a\n * three-call chain:\n *\n * 1. `bookSlot(slot, first, last, email, { selectedPaymentOption: \"OFFLINE\" })`\n * → the booking, status CREATED.\n * 2. `createCheckout([{ quantity: 1, catalogReference: { appId: <Wix Bookings\n * appDefId 13d21c63-b5ec-5912-8397-c3a5ddb27a97>, catalogItemId: booking.id } }])`\n * from `@himalaya/wix-ecom` → the checkout id.\n * 3. `createOrder(checkoutId)` from `@himalaya/wix-ecom` → the order, with the\n * balance left for the venue to collect.\n *\n * This chain was verified with raw Wix calls, not through these wrappers.\n * It is not verified whether `createOrder` still returns a `paymentGatewayOrderId`\n * for an OFFLINE booking, so do not read one as \"charge now\" and send a\n * pay-in-person customer into the cashier.\n *\n * Wix documents `selectedPaymentOption` as REQUIRED whenever the booking is paid\n * through an eCommerce checkout, and it must be an option the service offers.\n * Leave `options` out and the request is the one this function always sent. */",
7315
+ "decl": "export function bookSlot(\n slot: WixBookingSlotFull,\n firstName: string,\n lastName: string,\n email: string,\n options?: WixBookSlotOptions,\n): Promise<WixCreateBookingResponse>;",
7304
7316
  "http": "POST /bookings/v2/bookings"
7305
7317
  },
7306
7318
  {
@@ -83,10 +83,13 @@ back to the offline copy — so it never hard-fails. Then run the normal loop
83
83
  (`himi build` / `validate` / `render` / `push --draft`).
84
84
 
85
85
  **`branded-lite` is a full reference, not a small starter.** After `himi init --template
86
- branded-lite`, trim it to the screens in the approved `SPEC.md` rather than deleting
87
- `tier5-src/` + `dev/` and starting from blank. Which files to delete, and how
88
- `dev/index.ts`, `tier5-src/`, and `navigation.linkableScreens` relate:
89
- `himi docs trim-a-template`.
86
+ branded-lite`, trim it or mine it rather than deleting `tier5-src/` + `dev/` and starting from
87
+ blank. **Trim** when most of the approved `SPEC.md` is screens the template already has: delete
88
+ the rest. **Mine** when few of its screens match: keep the `tier5-src/_shared/` helpers and the
89
+ Wix worker chains, and write your own screens in `dev/index.ts` — the file holds every screen
90
+ as a raw descriptor (megabytes for the full templates), so replacing it is often faster than
91
+ editing it. Both paths, and how `dev/index.ts`, `tier5-src/`, and `navigation.linkableScreens`
92
+ relate: `himi docs trim-a-template`.
90
93
 
91
94
  **Point a `branded-lite` (or vertical overlay) scaffold at your site.** Set
92
95
  `WIX_CLIENT_ID` (Headless OAuth client on that site) and `WIX_SITE_URL` before build /
@@ -137,12 +137,21 @@ worker scope: **no `setTimeout`/`setInterval`**. That makes a whole class of on-
137
137
  fail locally — including this one, which every other local check passes:
138
138
 
139
139
  ```js
140
+ async function loadList() { /* fetch + shape; return the next state */ } // module level
141
+
140
142
  // ✗ `.actions` does not exist on the handle — undefined on device
141
143
  retry: async (s) => svc.actions.load({ ...s, status: "loading" }),
142
- // ✓ actions are DIRECT methods
143
- retry: async (s) => svc.load({ ...s, status: "loading" }),
144
+ // ✓ call the module-level helper, not the handle
145
+ retry: async () => loadList(),
144
146
  ```
145
147
 
148
+ Actions are DIRECT methods on the handle (`svc.load()`), and they take the action's payload
149
+ arguments, never the state. Even so, never call the handle from inside its own action: `svc`'s
150
+ type is inferred from the very object you are writing, so the reference is circular and
151
+ `validate --strict` rejects it (TS7022/TS7023), and passing `s` is `Expected 0 arguments, but got 1`. Put the work in a module-level function, return it
152
+ from both `load` and `retry`, and keep `svc.load()` for the one module-eval kick at the bottom
153
+ of the bundle — the shape `himi docs recipe-dynamic-list` ships.
154
+
146
155
  In-repo, [`tier5-bundle-guardrails.test.ts`](../core/dev-server/tests/tier5-bundle-guardrails.test.ts)
147
156
  forbids both statically across every committed bundle; that gate does not ship off-repo, which
148
157
  is why the crawl enforces it dynamically. The clock and `Math.random` are pinned too, so two
@@ -394,8 +403,11 @@ Don't hand-write fixtures — capture the real thing once:
394
403
  3. Replay it in any tier — as a `fake.net` fixture (tests) or an OTA scenario (live dev).
395
404
 
396
405
  The transform lives in [`net-freeze.ts`](../core/dev-server/src/net-freeze.ts) (one static rule
397
- per distinct `method+host+path`, last capture wins). Caveat: the traffic ring truncates large
398
- bodies — trim/verify a frozen fixture before relying on it as a JSON mock.
406
+ per distinct `method+host+path`, last capture wins). A request whose body names a
407
+ `dataCollectionId` is keyed on that too and frozen with `matcher.bodyContains`, so each Wix Data
408
+ collection's `POST /wix-data/v2/items/query` replays its own rows instead of the last one
409
+ recorded. Caveat: the traffic ring truncates large bodies — trim/verify a frozen fixture before
410
+ relying on it as a JSON mock.
399
411
 
400
412
  ---
401
413
 
@@ -557,6 +569,41 @@ Recorded so the next person does not re-walk them (#2231):
557
569
 
558
570
  ---
559
571
 
572
+ ## Web preview: which Wix visitor you are
573
+
574
+ A web preview that nobody has signed into calls Wix as an **anonymous visitor**, and that
575
+ visitor owns what it writes: a cart, a booking, an order. The browser holds the identity
576
+ itself. It gets its own visitor straight from Wix, so there is **one anonymous Wix visitor per
577
+ browser, per origin, per app `clientId`**. The visitor's refresh token is kept in
578
+ `localStorage` under `himalaya.wixVisitor.<clientId>`; the short-lived access token stays in
579
+ memory and is renewed from it.
580
+
581
+ The visitor **survives** page reloads, draft pushes and new releases, a renewed preview link
582
+ (`pvt`), and other tabs of the same browser, which share it.
583
+
584
+ You get a **new** visitor, who cannot see the old one's records, when:
585
+
586
+ - the site data is cleared, or the preview is opened in a private window or another browser;
587
+ - the same app is opened from a different origin — the `www.wixapis.com` preview link and a
588
+ mount on another Wix host each keep their own store;
589
+ - renewing the stored visitor fails, so the browser starts a fresh one;
590
+ - the app's `clientId` changes.
591
+
592
+ One path is **not** stable at all: the call goes through the preview server instead. That
593
+ happens when the browser cannot reach Wix itself (its visitor mint or a request fails, after
594
+ which the rest of that page load stays on the fallback); when the preview's identity check
595
+ fails or takes longer than 4 seconds, which sends calls through the server until 5 seconds
596
+ after that check started; when the server has switched browser-direct calls off; and for a call that carries its
597
+ own headers or an absolute URL. A write made in one of those windows can be missing when you
598
+ read it back as the browser's own visitor.
599
+ That server uses one anonymous visitor per server instance, **shared** by everyone it serves and
600
+ replaced when its token expires. A signed-in member is a separate identity (the member session)
601
+ and is not affected by any of this.
602
+
603
+ So when a visitor's own record vanishes from a preview, first check whether the browser, the
604
+ origin or the site data changed. A record that has to outlive those belongs to a signed-in
605
+ member, or should be read back by an id the app kept.
606
+
560
607
  ## Web automation: selector contract
561
608
 
562
609
  Driving the web host (the `/preview` co-host, the Himi Player, or a standalone web build)
@@ -104,7 +104,7 @@ The first arg after `state` is the action's payload object: the descriptor's `Ac
104
104
  | `TextField` / `RichTextField` | `onChange`, `onSubmit` | `{ value }` — the current text (onChange fires per keystroke) |
105
105
  | `SearchField` | `onChange`, `onSubmit` | `{ value }` — the query, debounced by `debounceMs` |
106
106
  | `Picker` / `SegmentedControl` / `Tabs` / `WheelPicker` | `onChange` | `{ selectedId }` — the id of the newly selected option |
107
- | `FilterChips` | `onChange` | `{ selectedIds }` — the **array** of ids selected *after* the tap (honors `multiSelect`) |
107
+ | `FilterChips` | `onChange` | `{ selectedIds }` — the **array** of ids selected *after* the tap (honors `multiSelect`). To drive the highlight from state, bind the sentinel **inside** the array: `selectedIds: ["$service:<svc>.<field>"]` |
108
108
  | `Toggle` | `onChange` | `{ value }` — the next state, as a boolean |
109
109
  | `SelectionControl` | `onChange` | `{ value }` — the next checked state, as a boolean |
110
110
  | `Stepper` | `onChange` | `{ value }` — the clamped next value, as an int |
@@ -395,13 +395,23 @@ mis-pointed `refresh` clobbers good data every tick); the module-eval `load()` i
395
395
  *once, on entry*. Reach for module-eval `load()` for initial load; use `refresh`
396
396
  only for genuinely live data.
397
397
 
398
- > The `modifiers.onAppear` action is a lifecycle hook that fires when a node appears.
399
- > It fires on **web too**, at parity with iOS/Android — the web host's screen host
400
- > dispatches it, with test coverage to match, shipped in #722. Reach for it for
401
- > **re-entry refresh** and navigation-borne params. Do **not** reach for it as your
402
- > *initial-load* path: module-eval `svc.load()` above is the blessed initial-load
403
- > primitive on every surface, and since #2282 it is honoured in the contained web
404
- > frame as well.
398
+ > The `modifiers.onAppear` action fires when the screen **mounts** — on web, iOS and Android.
399
+ > Use it to hand the worker navigation-borne or build-time params. Do **not** reach for it as
400
+ > your *initial-load* path: module-eval `svc.load()` above is the blessed initial-load
401
+ > primitive on every surface, and since #2282 it is honoured in the contained web frame as
402
+ > well.
403
+ >
404
+ > **It is not a "the user came back" hook.** Whether returning to a screen fires it again
405
+ > depends on whether the host kept that screen mounted, and that differs by platform. iOS
406
+ > fires it once per mounted view, and keeps both visited tabs and the screen under a pushed
407
+ > detail mounted, so a tab return or a Back does not re-fire it. Android fires it each time the
408
+ > node enters composition. On web it fires when the screen host mounts the screen.
409
+ >
410
+ > So for state another screen may have changed — a cart, a signed-in flag, a record saved
411
+ > elsewhere — re-read it in the action that loads the screen (`load`) or in an explicit
412
+ > `refresh`, not in `onAppear` (and see
413
+ > [State does not cross screens automatically](#state-does-not-cross-screens-automatically)
414
+ > for how to hand it off).
405
415
 
406
416
  **Load with the sanctioned transport, not raw `fetch`.** A worker action calls the
407
417
  typed Wix clients (`@himalaya/wix-*`) or the raw chokepoint
@@ -558,12 +568,15 @@ accepted limitations: [RFC 0040 § Amendment — the `_poll` directive](../core/
558
568
  **1. The list opens the canonical detail screen, passing the id as a param** (only the
559
569
  `screenId` is canonical — the id rides alongside it):
560
570
 
561
- ```ts
562
- // in the list worker, per row:
563
- await dispatch("himi.open", { screenId: "event", id: row.id, presentation: "push" });
564
- // descriptor onTap form: { actionId: "himi.open", params: { screenId: "event", id: "$item.id" } }
571
+ ```jsonc
572
+ // the row's onTap — `$item.id` is substituted per row:
573
+ { "actionId": "himi.open", "params": { "screenId": "event", "id": "$item.id" } }
565
574
  ```
566
575
 
576
+ When the decision to navigate is the worker's (after a save, or once a lookup resolves), return
577
+ `navigate(...)` from the action instead — see [Navigate from the worker](#navigate-from-the-worker-navigate-and-_nav)
578
+ below.
579
+
567
580
  **2. Declare the linkable route with `paramKeys` in `config.ts`** (the id key the host
568
581
  threads into `himalaya.input`):
569
582
 
@@ -575,7 +588,7 @@ linkableScreens: [
575
588
 
576
589
  **3. The detail worker reads `himalaya.input.id`** and loads in module-eval `load()`
577
590
  (the blessed initial-load primitive above), **not** an `onAppear` action — `onAppear`
578
- does fire on web, but it is the re-entry hook, not the initial-load path (see the
591
+ fires on every platform, but it is a mount hook, not the initial-load path (see the
579
592
  callout above):
580
593
 
581
594
  ```ts
@@ -645,6 +658,47 @@ For a bottom-tab app, list each tab as a `NavTabItem` in the app config's `navig
645
658
  block — note the single-screen `himi init` scaffold ships **no** `navigation`, so a
646
659
  multi-tab app adds it.
647
660
 
661
+ #### Navigate from the worker: `navigate()` and `_nav`
662
+
663
+ A worker action moves the user by returning its next state with a transient `_nav` field.
664
+ `navigate` from `@himalaya/state` writes it for you:
665
+
666
+ ```ts
667
+ import { defineService, navigate } from "@himalaya/state";
668
+
669
+ // in an action — after the save lands, open the record it created:
670
+ save: async (s) => {
671
+ const id = await saveDraft(s.draft); // your module-level helper; returns the new id
672
+ return navigate({ ...s, saving: false }, { screenId: "event", args: { id } });
673
+ },
674
+ ```
675
+
676
+ The runtime strips `_nav` before the state is stored, and the host opens `screenId` exactly as
677
+ a tapped `himi.open` would — including the `args` arriving as `himalaya.input` on the target.
678
+ The rules:
679
+
680
+ - `screenId` must be a **linkable** screen (`navigation.linkableScreens`), or the host
681
+ ignores the directive.
682
+ - `args` values are **strings only**; anything else is dropped rather than coerced.
683
+ - It always **pushes**. There is no `_nav` form of `back`, `dismiss`, `sheet` or `replace`.
684
+
685
+ `_nav` is the worker-side navigation all three hosts route. A worker's
686
+ `dispatch("himi.open", …)` is routed by the web host's nav bridge only; on iOS and Android a
687
+ worker `dispatch` reaches the native action registry, which has no `himi.open` handler.
688
+
689
+ #### Back, dismiss, and tabs
690
+
691
+ Everything that is not a push is a **descriptor** action with a `presentation` verb — a Close
692
+ or Done button is `{ "actionId": "himi.open", "presentation": "dismiss" }` (or `"back"`). The
693
+ component reference tabulates every verb (`himi components --json`, or the Actions section of
694
+ `himi docs component-reference`): `back` pops the current screen, `dismiss` closes a presented
695
+ sheet or cover.
696
+
697
+ **No action switches tabs.** A `himi.open` — tapped or from `_nav` — whose target is another
698
+ tab's root screen pushes that screen onto the *current* tab's stack; it does not select the
699
+ other tab. Only the tab bar and other app chrome select a tab, so design a cross-tab jump as a
700
+ push of the detail you want, not as a hop to the other tab's root.
701
+
648
702
  ### Enumerable variants with `FragmentSlot` (RFC 0045)
649
703
 
650
704
  Use `fragmentSlot()` when a discriminator selects among a finite set of shipped
package/guides/tokens.md CHANGED
@@ -29,6 +29,9 @@ tokens.json fail to decode on iOS + Android** — and the web renderer is lenien
29
29
  the missing field), so the bug passes web review and only breaks on device. CI guards it:
30
30
  `himi validate` reports a token set that would not decode on device.
31
31
 
32
+ `family` is optional: a font the app can ship (`himi fonts list`), or `"$system"` for each
33
+ platform's own UI face — SF Pro on iOS/macOS, Roboto on Android, the system UI stack on the web.
34
+
32
35
  ## Colors
33
36
 
34
37
  A color is a hex string, OR a per-scheme object `{ light, dark?, highContrast? }`. In the
@@ -1,13 +1,22 @@
1
1
  # Trim a template to N screens
2
2
 
3
3
  `himi init --template branded-lite` (and the vertical overlays) scaffolds a **full
4
- reference app**, not a 5-screen starter: on the order of **70 `tier5-src/` workers**,
5
- **~90 screens**, and a **~1 MB AUTO-GENERATED `dev/index.ts`**, plus a matching
6
- `navigation.linkableScreens` list. That is the right starting point for a Wix-backed
7
- customer app — keep it, then **delete what the approved `SPEC.md` does not need**. Do
8
- not wipe `tier5-src/` + `dev/` and re-author from the blank `himi init` shape; that
9
- throws away the live Wix wiring the template exists to give you. Vertical overlays
10
- retheme copy/tokens; they do not shrink the screen count.
4
+ reference app**, not a 5-screen starter: on the order of **70 `tier5-src/` workers** and
5
+ **70 screens**, plus a matching `navigation.linkableScreens` list. Every screen is a nested
6
+ raw descriptor in one `dev/index.ts`, and that file is big: about **4.5 MB / 80,000 lines**
7
+ for `branded-lite`, and **0.8–1.8 MB** for the vertical templates (`retail-store` ~0.85 MB,
8
+ `gym-fitness` ~1.8 MB / 32,000 lines). Its header says *"Edit them freely — this file is
9
+ yours now"*, and it is: nothing regenerates it after `himi init`.
10
+
11
+ Do not wipe `tier5-src/` + `dev/` and re-author from the blank `himi init` shape; that throws
12
+ away the live Wix wiring the template exists to give you. Vertical overlays retheme
13
+ copy/tokens; they do not shrink the screen count. There are two ways to keep that wiring:
14
+
15
+ - **Trim it** when the approved `SPEC.md` is mostly screens the template already has: keep
16
+ those, **delete what the plan does not need** ([How to trim](#how-to-trim)).
17
+ - **Mine it** when the app's screens are its own and few template screens match — the
18
+ realistic case for a multi-megabyte file of nested descriptors. Keep the worker layer,
19
+ replace the screens ([Or mine it](#or-mine-it-keep-the-wix-plumbing-write-your-own-screens)).
11
20
 
12
21
  For a small **non-Wix** pattern (chat, feed, habits, tip calculator), skip this guide
13
22
  and scaffold a remote starter instead: `himi init my-app --template chat-bot --remote`
@@ -20,7 +29,7 @@ A screen is not one file. After `himi init`, these three must agree:
20
29
  | Keep / delete together | What it is |
21
30
  |---|---|
22
31
  | `tier5-src/<worker-dir>/` | The worker (state + actions). Directory name is the logic-bundle id (`himi-branded-lite-home`, `himi-booking-services`, …). |
23
- | `dev/index.ts` `SCREENS` map | The descriptor JSON. The AUTO-GENERATED header means **the in-repo app is the generator's source of truth** — after `himi init` this copy is **yours**; edit it. |
32
+ | `dev/index.ts` `SCREENS` map | The descriptors, as raw objects. After `himi init` the file is **yours** ("this file is yours now") — edit it, cut it down, or replace it. |
24
33
  | `config.ts` `navigation.linkableScreens` | Deep-link / tab targets. `himalaya://screen/<id>` resolves **only** if the id is listed here. Tab roots also appear under `navigation.tabBar.items`. |
25
34
 
26
35
  `himi validate --strict` fails if a tab/linkable id has no descriptor, or a descriptor
@@ -52,6 +61,52 @@ Worked example — keep Home + Shop + Account from branded-lite:
52
61
  `himi_account`, … and drops `himi_services`, `himi_events`, …
53
62
  - `linkableScreens` + `tabBar.items` match that set (Home / Shop / Profile).
54
63
 
64
+ ## Or mine it: keep the Wix plumbing, write your own screens
65
+
66
+ When few of the template's screens match the app — say a class schedule with a day strip,
67
+ spots left and a waitlist, none of which the template draws — hand-editing the descriptor file
68
+ is not the fast path. What a template gives you that is hard to write and easy to reuse is the
69
+ **worker layer**. Keep that, and author the screens yourself:
70
+
71
+ 1. **Keep the helpers.** `tier5-src/_shared/` holds the tested Wix plumbing: price and
72
+ currency formatting (`wix-service-price.ts`, `engine-intl.ts`), text helpers
73
+ (`himi-text.ts`), and the per-vertical shaping (`himi-bookings.ts`, `himi-store.ts`,
74
+ `himi-events.ts`, …). Import them from your own workers.
75
+ 2. **Keep the write chains you need.** A transaction that spans several Wix calls is the
76
+ expensive part to rediscover — the booking worker's `createBooking → createCheckout →
77
+ createOrder` chain in `tier5-src/himi-booking-time/`, or the cart's checkout in
78
+ `tier5-src/himi-branded-lite-cart/`. Keep that worker and bind your screen to it, or move
79
+ the chain into your own worker.
80
+ 3. **Delete the template screens.** Remove the worker dirs you are not keeping, and replace the
81
+ `SCREENS` map in `dev/index.ts` with your own screens (and `linkableScreens` /
82
+ `tabBar.items` to match).
83
+ 4. **Write each screen as small local functions** in `dev/index.ts` that return raw descriptor
84
+ objects. They are plain TypeScript functions in your own file, not a package (there is no
85
+ builder-DSL package — `himi docs designing-himi-screens`), so they compile to the same raw
86
+ descriptors `himi init` writes:
87
+
88
+ ```ts
89
+ // dev/index.ts
90
+ const node = (id: string, type: string, props: Record<string, unknown>) =>
91
+ ({ id, body: { package: "himalaya", type, props } });
92
+ const text = (id: string, value: string, style = "body") => node(id, "Text", { text: value, style });
93
+
94
+ function classDetail() {
95
+ return {
96
+ screenId: "class_detail",
97
+ // renderer, schemaVersion, cacheControl, logicBundle: exactly as `himi init` writes them
98
+ payload: node("root", "VStack", { spacing: 12, children: [
99
+ text("name", "$service:classDetail.name", "title1"),
100
+ text("spots", "$service:classDetail.spotsLabel", "caption"),
101
+ node("book", "Button", { title: "Book", style: "primary", action: { actionId: "tier5.classDetail.book" } }),
102
+ ] }),
103
+ };
104
+ }
105
+ ```
106
+
107
+ 5. **Prove it** the same way as a trim: `himi validate --strict`, `himi render <screen>`, then
108
+ `himi test`.
109
+
55
110
  ## What not to do
56
111
 
57
112
  - Do not hand-edit the in-repo `apps/himi-branded-lite/dev/index.ts` to "slim the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.995.0",
3
+ "version": "0.997.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": [
@@ -65,5 +65,5 @@
65
65
  "artifactId": "himalaya-cli"
66
66
  }
67
67
  },
68
- "falconPackageHash": "5b0f23dafc9d3aeac20f9bc73a248437fbcc9e65b1a8a25e8bb80cac"
68
+ "falconPackageHash": "1da0e386b23642ce946cc95fbb56bb7988c6100b680fe7dbaf954a99"
69
69
  }