@wix/himalaya-cli 0.1046.0 → 0.1047.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/cli.mjs CHANGED
@@ -42838,7 +42838,7 @@ function serializeAt(node, src, open) {
42838
42838
  const indent = lineIndent(src, open);
42839
42839
  return JSON.stringify(node, null, 2).split("\n").map((line, i) => i === 0 ? line : indent + line).join("\n");
42840
42840
  }
42841
- function patchGrids(src, installed, patch) {
42841
+ function patchGrids(src, installed, patch, keepFaq = false, dropFaq = false) {
42842
42842
  const OPEN = /"id": "([A-Za-z_0-9]+(?:\.[A-Za-z_0-9]+)*\.verticals)"/g;
42843
42843
  let out = "";
42844
42844
  let cursor = 0;
@@ -42859,12 +42859,17 @@ function patchGrids(src, installed, patch) {
42859
42859
  const prefix = m[1].slice(0, -".verticals".length);
42860
42860
  const baked = /* @__PURE__ */ new Map();
42861
42861
  const foreignCells = [];
42862
- for (const item of items) {
42863
- const id = item.id;
42864
- const legacy = item.modifiers?.accessibilityLabel;
42862
+ let faqCell;
42863
+ const claimOf = (item) => {
42864
+ const id = item?.id;
42865
+ const legacy = item?.modifiers?.accessibilityLabel;
42865
42866
  const claim = typeof id === "string" && id ? id : typeof legacy === "string" ? legacy : "";
42866
- const vertical = claim.startsWith(`${prefix}.`) ? claim.slice(prefix.length + 1) : "";
42867
+ return claim.startsWith(`${prefix}.`) ? claim.slice(prefix.length + 1) : "";
42868
+ };
42869
+ for (const item of items) {
42870
+ const vertical = claimOf(item);
42867
42871
  if (isKnown(vertical)) baked.set(vertical, item);
42872
+ else if (vertical === "faq" && keepFaq) faqCell = item;
42868
42873
  else if (!GRID_VERTICAL_LABELS.has(vertical)) foreignCells.push(item);
42869
42874
  }
42870
42875
  const wanted = [];
@@ -42882,10 +42887,36 @@ function patchGrids(src, installed, patch) {
42882
42887
  wanted.push(vertical);
42883
42888
  }
42884
42889
  const shape = baked.values().next().value ?? items[0];
42890
+ const bakedFaq = faqCell;
42891
+ if (keepFaq && !faqCell && hasScreen(src, FAQ_CARD.screenId)) {
42892
+ faqCell = cloneCell(shape, prefix, "faq", FAQ_CARD);
42893
+ }
42894
+ if (!installed.size) {
42895
+ if (dropFaq) {
42896
+ const kept = items.filter((item) => claimOf(item) !== "faq");
42897
+ if (kept.length !== items.length && kept.length > 0) {
42898
+ node.body.props.items = kept;
42899
+ out += src.slice(cursor, open) + serializeAt(node, src, open);
42900
+ cursor = close;
42901
+ OPEN.lastIndex = close;
42902
+ }
42903
+ continue;
42904
+ }
42905
+ const lastIsVertical = GRID_VERTICAL_LABELS.has(claimOf(items[items.length - 1]));
42906
+ if (!bakedFaq && faqCell && (items.length < EXPLORE_MAX_CELLS || lastIsVertical)) {
42907
+ if (items.length >= EXPLORE_MAX_CELLS) items.pop();
42908
+ items.push(faqCell);
42909
+ out += src.slice(cursor, open) + serializeAt(node, src, open);
42910
+ cursor = close;
42911
+ OPEN.lastIndex = close;
42912
+ }
42913
+ continue;
42914
+ }
42915
+ const cap = faqCell ? EXPLORE_MAX_CELLS - 1 : EXPLORE_MAX_CELLS;
42885
42916
  const next = [...foreignCells];
42886
42917
  const cells = [];
42887
42918
  for (const vertical of wanted) {
42888
- if (next.length >= EXPLORE_MAX_CELLS) break;
42919
+ if (next.length >= cap) break;
42889
42920
  const bakedCell = baked.get(vertical);
42890
42921
  if (bakedCell) {
42891
42922
  next.push(bakedCell);
@@ -42901,6 +42932,7 @@ function patchGrids(src, installed, patch) {
42901
42932
  cells.push(vertical);
42902
42933
  patch.added.push(vertical);
42903
42934
  }
42935
+ if (faqCell && next.length < EXPLORE_MAX_CELLS) next.push(faqCell);
42904
42936
  if (!cells.length) continue;
42905
42937
  patch.cells = cells;
42906
42938
  node.body.props.items = next;
@@ -43015,16 +43047,19 @@ function patchStoreCtas(src, verticals, homeActionId) {
43015
43047
  dropped
43016
43048
  };
43017
43049
  }
43018
- function patchHomeForVerticals(src, verticals, homeActionId) {
43050
+ function patchHomeForVerticals(src, verticals, homeActionId, opts = {}) {
43019
43051
  const installed = new Set(verticals.filter(isKnown));
43020
43052
  const patch = { src, cells: [], added: [], droppedModules: [], droppedCtas: [], unreachable: [] };
43021
- if (!installed.size) return patch;
43022
- const ctas = patchStoreCtas(patchModules(patchGrids(src, installed, patch), installed, patch), [...installed], homeActionId);
43053
+ if (!installed.size) {
43054
+ if (opts.keepFaq || opts.dropFaq) patch.src = patchGrids(src, installed, patch, !!opts.keepFaq, !opts.keepFaq && !!opts.dropFaq);
43055
+ return patch;
43056
+ }
43057
+ const ctas = patchStoreCtas(patchModules(patchGrids(src, installed, patch, opts.keepFaq), installed, patch), [...installed], homeActionId);
43023
43058
  patch.src = ctas.src;
43024
43059
  patch.droppedCtas = ctas.dropped;
43025
43060
  return patch;
43026
43061
  }
43027
- var EXPLORE_CARDS, EXPLORE_ORDER, EXPLORE_MAX_CELLS, MODULE_SOURCE_VERTICALS, isKnown, GRID_VERTICAL_LABELS, escapeRe2, textProps, STORE_SCREENS, isStoreScreen, screenDeclarations, isNode2;
43062
+ var EXPLORE_CARDS, EXPLORE_ORDER, EXPLORE_MAX_CELLS, MODULE_SOURCE_VERTICALS, isKnown, GRID_VERTICAL_LABELS, FAQ_CARD, escapeRe2, textProps, STORE_SCREENS, isStoreScreen, screenDeclarations, isNode2;
43028
43063
  var init_home_descriptor = __esm({
43029
43064
  "src/site-analyze/home-descriptor.ts"() {
43030
43065
  init_types2();
@@ -43096,6 +43131,7 @@ var init_home_descriptor = __esm({
43096
43131
  };
43097
43132
  isKnown = (v) => KNOWN_VERTICALS.includes(v);
43098
43133
  GRID_VERTICAL_LABELS = /* @__PURE__ */ new Set([...KNOWN_VERTICALS, "faq"]);
43134
+ FAQ_CARD = { icon: "help", screenId: "himi_faq", title: "FAQ", body: "Find answers to common questions." };
43099
43135
  escapeRe2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43100
43136
  textProps = (n) => n?.body?.props;
43101
43137
  STORE_SCREENS = /* @__PURE__ */ new Set([
@@ -54141,7 +54177,28 @@ function deriveTailoring(ctx, overrides = {}, revision = "site-derived") {
54141
54177
  };
54142
54178
  return { appName, iconSource: deriveIconSource(ctx), provisioning, missing };
54143
54179
  }
54144
- var clean, ID_MAX, NAME_MAX, TYPE_MAX, CONTROL_CHARS, HAS_CONTROL_CHAR, plain, shown;
54180
+ function bounded(s, max) {
54181
+ if (s.length <= max) return s;
54182
+ const cut = s.slice(0, max - 1);
54183
+ const space = cut.lastIndexOf(" ");
54184
+ return `${(space > max * 0.6 ? cut.slice(0, space) : cut).trimEnd()}\u2026`;
54185
+ }
54186
+ function deriveStaticFaq(faq) {
54187
+ if (!Array.isArray(faq)) return [];
54188
+ const out = [];
54189
+ const seen = /* @__PURE__ */ new Set();
54190
+ for (const raw of faq) {
54191
+ if (out.length >= STATIC_FAQ_LIMITS.pairs) break;
54192
+ const pair = raw && typeof raw === "object" ? raw : {};
54193
+ const q = bounded(oneLine(pair.q), STATIC_FAQ_LIMITS.question);
54194
+ const a = bounded(oneLine(pair.a), STATIC_FAQ_LIMITS.answer);
54195
+ if (!q || !a || seen.has(q.toLowerCase())) continue;
54196
+ seen.add(q.toLowerCase());
54197
+ out.push({ q, a });
54198
+ }
54199
+ return out;
54200
+ }
54201
+ var clean, ID_MAX, NAME_MAX, TYPE_MAX, CONTROL_CHARS, HAS_CONTROL_CHAR, plain, shown, STATIC_FAQ_LIMITS, oneLine;
54145
54202
  var init_derive = __esm({
54146
54203
  "src/site-analyze/derive.ts"() {
54147
54204
  init_types2();
@@ -54153,6 +54210,8 @@ var init_derive = __esm({
54153
54210
  HAS_CONTROL_CHAR = /[\u0000-\u001F\u007F]/;
54154
54211
  plain = (value, max) => (value ?? "").replace(CONTROL_CHARS, "").trim().slice(0, max);
54155
54212
  shown = (value) => (typeof value === "string" ? JSON.stringify(value) : String(value)).slice(0, 64);
54213
+ STATIC_FAQ_LIMITS = { pairs: 20, question: 200, answer: 1500 };
54214
+ oneLine = (s) => typeof s === "string" ? s.replace(/[\u0000-\u001F\u007F\s]+/g, " ").trim() : "";
54156
54215
  }
54157
54216
  });
54158
54217
 
@@ -57211,7 +57270,8 @@ function mergeCustomerOwned(committed, overlay) {
57211
57270
  "navigation",
57212
57271
  "locale",
57213
57272
  "wix",
57214
- "copy"
57273
+ "copy",
57274
+ "faq"
57215
57275
  ];
57216
57276
  const customer = {};
57217
57277
  for (const key2 of CUSTOMER_OWNED) {
@@ -57305,12 +57365,56 @@ function buildMergedContract(options) {
57305
57365
  staged.push(localPath);
57306
57366
  return { path: path2, staged };
57307
57367
  }
57368
+ function catalogTypography(overlay, template) {
57369
+ const typography = {};
57370
+ const notes = [];
57371
+ const base = template && typeof template === "object" ? template : {};
57372
+ const usable = (family) => typeof family === "string" && family.trim() !== "" && (family === SYSTEM_FONT_FAMILY || canonicalFamilyName(family) !== void 0);
57373
+ for (const [role, token] of Object.entries(overlay ?? {})) {
57374
+ const family = token?.family;
57375
+ if (typeof family !== "string" || family.trim() === "") {
57376
+ typography[role] = token;
57377
+ continue;
57378
+ }
57379
+ if (family === SYSTEM_FONT_FAMILY) {
57380
+ typography[role] = token;
57381
+ continue;
57382
+ }
57383
+ const canonical3 = canonicalFamilyName(family.trim());
57384
+ if (canonical3) {
57385
+ typography[role] = { ...token, family: canonical3 };
57386
+ continue;
57387
+ }
57388
+ const kept = base[role]?.family;
57389
+ const using = usable(kept) ? `the template's ${kept}` : `the template's default (${TIER1_FLOOR[0]})`;
57390
+ notes.push(`site font "${family}" (${role}) isn't in the font catalog; using ${using}`);
57391
+ }
57392
+ return { typography, notes };
57393
+ }
57394
+ function staticFaqFor(overlay, overlayPath) {
57395
+ const own3 = overlay.faq;
57396
+ if (own3 !== void 0) return deriveStaticFaq(own3);
57397
+ const bookPath = join59(dirname32(resolve45(overlayPath)), "book.json");
57398
+ if (!existsSync61(bookPath)) return void 0;
57399
+ try {
57400
+ const book = JSON.parse(readFileSync72(bookPath, "utf8"));
57401
+ const copy = book?.copy;
57402
+ if (!copy || typeof copy !== "object" || copy.unavailable) return void 0;
57403
+ const pairs = deriveStaticFaq(copy.faq);
57404
+ if (!pairs.length && copy.partial) return void 0;
57405
+ return pairs;
57406
+ } catch {
57407
+ return void 0;
57408
+ }
57409
+ }
57308
57410
  async function applyOverlays(input) {
57309
57411
  const cwd = resolve45(input.cwd);
57310
57412
  const shell = input.shell ?? DEFAULT_BRANDABLE_SHELL;
57311
57413
  assertKnownShell(shell);
57312
57414
  const overlay = JSON.parse(readFileSync72(input.overlayPath, "utf8"));
57313
57415
  const tokens = JSON.parse(readFileSync72(input.tokensPath, "utf8"));
57416
+ const faq = staticFaqFor(overlay, input.overlayPath);
57417
+ if (faq !== void 0) overlay.faq = faq;
57314
57418
  const wrote = [];
57315
57419
  const notes = [];
57316
57420
  const repoRoot2 = findRepoRoot(cwd);
@@ -57344,9 +57448,11 @@ async function applyOverlays(input) {
57344
57448
  }
57345
57449
  const pkgProv = join59(cwd, "provisioning.json");
57346
57450
  assertNotCommitted(pkgProv, input.forbiddenProvisioning);
57451
+ let packageFaq = faq;
57347
57452
  if (existsSync61(pkgProv)) {
57348
57453
  const cur = JSON.parse(readFileSync72(pkgProv, "utf8"));
57349
57454
  const merged = deepMerge4(cur, overlay);
57455
+ packageFaq = merged.faq;
57350
57456
  writeFileSync27(pkgProv, JSON.stringify(merged, null, 2) + "\n");
57351
57457
  wrote.push(pkgProv);
57352
57458
  } else {
@@ -57356,7 +57462,13 @@ async function applyOverlays(input) {
57356
57462
  const pkgTokens = join59(cwd, "tokens.json");
57357
57463
  if (existsSync61(pkgTokens) && (tokens.colors || tokens.typography)) {
57358
57464
  const cur = JSON.parse(readFileSync72(pkgTokens, "utf8"));
57359
- const merged = deepMerge4(cur, tokens);
57465
+ let tokensIn = tokens;
57466
+ if (tokens.typography) {
57467
+ const fonts = catalogTypography(tokens.typography, cur.typography);
57468
+ notes.push(...fonts.notes);
57469
+ tokensIn = { ...tokens, typography: fonts.typography };
57470
+ }
57471
+ const merged = deepMerge4(cur, tokensIn);
57360
57472
  const mergedColors = merged.colors;
57361
57473
  if (mergedColors && typeof mergedColors === "object" && !Array.isArray(mergedColors)) {
57362
57474
  merged.colors = alignAccentsToSurfaces(mergedColors, tokens.colors);
@@ -57379,8 +57491,16 @@ async function applyOverlays(input) {
57379
57491
  const prev = readFileSync72(homeSrc, "utf8");
57380
57492
  const patched = patchBrandParams(prev, overlay, homeAction);
57381
57493
  let next = patched.src;
57382
- if (!patched.foreign && overlay.installedVerticals?.length) {
57383
- const home = patchHomeForVerticals(next, overlay.installedVerticals, homeAction);
57494
+ if (!patched.foreign && faq?.length && prev.includes('"tier5.himiBrandedFaq.load"') && !prev.includes('"staticFaq"')) {
57495
+ notes.push(
57496
+ "the site's FAQ pairs were saved to provisioning.json, but this package's FAQ screen predates the site-FAQ fallback and will not show them \u2014 re-scaffold from the current template to pick it up"
57497
+ );
57498
+ }
57499
+ const keepFaq = Array.isArray(packageFaq) && packageFaq.length > 0;
57500
+ const dropFaq = Array.isArray(faq) && faq.length === 0;
57501
+ const ownsHome = prev.includes(`"actionId": "${homeAction}"`);
57502
+ if (ownsHome && !patched.foreign && (overlay.installedVerticals?.length || keepFaq || dropFaq)) {
57503
+ const home = patchHomeForVerticals(next, overlay.installedVerticals ?? [], homeAction, { keepFaq, dropFaq });
57384
57504
  next = home.src;
57385
57505
  if (home.droppedCtas.length) {
57386
57506
  notes.push(`removed the home's store call(s) to action (${home.droppedCtas.join(", ")}) \u2014 this site has no store`);
@@ -57426,10 +57546,18 @@ function patchBrandParams(src, overlay, homeActionId) {
57426
57546
  const layout = overlay.appearance?.homeLayout;
57427
57547
  const accent = overlay.branding?.primaryColor;
57428
57548
  const verts = overlay.installedVerticals;
57549
+ const faq = overlay.faq;
57429
57550
  const edits = [
57430
57551
  ...layout ? [{ pattern: /"ownerDefaultLayout": "[^"]*"/g, replacement: `"ownerDefaultLayout": ${JSON.stringify(layout)}` }] : [],
57431
57552
  ...accent ? [{ pattern: /"brandAccent": "[^"]*"/g, replacement: `"brandAccent": ${JSON.stringify(accent)}` }] : [],
57432
- ...verts?.length ? [{ pattern: /"verticals": "\[[^\]]*\]"/g, replacement: `"verticals": ${JSON.stringify(JSON.stringify(verts))}` }] : []
57553
+ ...verts?.length ? [{ pattern: /"verticals": "\[[^\]]*\]"/g, replacement: `"verticals": ${JSON.stringify(JSON.stringify(verts))}` }] : [],
57554
+ // The FAQ screen's fallback pairs (#3276), a JSON STRING like `verticals` because action
57555
+ // params are string-valued. The pattern matches any escaped string, so re-applying a
57556
+ // package whose pairs are already written replaces them instead of missing. A function
57557
+ // replacement, because the pairs are customer text and may contain `$&`. Patched whenever
57558
+ // the analysis SAID something, `[]` included, so a re-apply clears pairs an earlier one
57559
+ // baked in rather than leaving the worker another site's FAQ.
57560
+ ...faq ? [{ pattern: /"staticFaq": "(?:[^"\\]|\\.)*"/g, replacement: `"staticFaq": ${JSON.stringify(JSON.stringify(faq))}` }] : []
57433
57561
  ];
57434
57562
  if (!edits.length) return { src, applied: 0, unparsed: 0, foreign: false };
57435
57563
  if (!src.includes(`"actionId": "${homeActionId}"`)) {
@@ -57474,7 +57602,7 @@ function patchBrandParams(src, overlay, homeActionId) {
57474
57602
  pattern.lastIndex = 0;
57475
57603
  if (!pattern.test(block)) continue;
57476
57604
  pattern.lastIndex = 0;
57477
- block = block.replace(pattern, replacement);
57605
+ block = block.replace(pattern, () => replacement);
57478
57606
  applied += 1;
57479
57607
  }
57480
57608
  out += src.slice(cursor, open) + block;
@@ -57492,6 +57620,10 @@ var init_apply2 = __esm({
57492
57620
  init_branding_derive();
57493
57621
  init_brand_color_math();
57494
57622
  init_home_descriptor();
57623
+ init_derive();
57624
+ init_catalog();
57625
+ init_plan_fonts();
57626
+ init_tokens();
57495
57627
  STAGED = ".himi/brand";
57496
57628
  }
57497
57629
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1046.0",
3
+ "version": "0.1047.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": "920a07437cf238c0bf1e34b799a4fe38573b57c6da92541fc2836079"
68
+ "falconPackageHash": "9a5f5cd39f2e85af3f506141935613aebbda4942b8cb91683db7492f"
69
69
  }
@@ -58239,7 +58239,8 @@ const SCREENS = runtimeSetting({
58239
58239
  "onAppear": {
58240
58240
  "actionId": "tier5.himiBrandedFaq.load",
58241
58241
  "params": {
58242
- "otherCategory": "Other"
58242
+ "otherCategory": "Other",
58243
+ "staticFaq": "[]"
58243
58244
  }
58244
58245
  }
58245
58246
  }
@@ -58,6 +58,55 @@ interface FaqState {
58
58
 
59
59
  const PAGE_SIZE = 100;
60
60
 
61
+ /** Caps on the static pairs, matching `BRANDED_LITE_FAQ_LIMITS` in the app contract. The
62
+ * param is re-bounded here because a worker must not trust a descriptor it did not build. */
63
+ const STATIC_LIMITS = { pairs: 20, question: 200, answer: 1500 } as const;
64
+ const STATIC_CATEGORY: FaqCategory = { id: "himi-static-faq", title: "" };
65
+
66
+ /** The site's own FAQ pairs — questions it publishes as page sections rather than
67
+ * through the Wix FAQ app, carried in by `site analyze --apply` as the `staticFaq` onAppear
68
+ * param (a JSON string: action params are string-valued). They are the FALLBACK: shown only
69
+ * when the FAQ app yields no visible question, so an owner-edited Wix FAQ always wins. */
70
+ let staticQuestions: FaqQuestion[] = [];
71
+
72
+ function parseStaticFaq(raw: unknown): FaqQuestion[] {
73
+ let pairs: unknown = raw;
74
+ if (typeof raw === "string") {
75
+ try {
76
+ pairs = JSON.parse(raw);
77
+ } catch {
78
+ // A malformed param is a DESCRIPTOR bug, not an empty FAQ: there is no fallback to
79
+ // offer, so the screen shows exactly what the Wix FAQ read says — its content, its
80
+ // empty card, or its error card — rather than pretending the site has no questions.
81
+ pairs = undefined;
82
+ }
83
+ }
84
+ if (!Array.isArray(pairs)) return [];
85
+ const out: FaqQuestion[] = [];
86
+ for (const pair of pairs) {
87
+ if (out.length >= STATIC_LIMITS.pairs) break;
88
+ const row = pair && typeof pair === "object" ? pair as { q?: unknown; a?: unknown } : {};
89
+ const question = typeof row.q === "string" ? row.q.trim().slice(0, STATIC_LIMITS.question) : "";
90
+ const answer = typeof row.a === "string" ? row.a.trim().slice(0, STATIC_LIMITS.answer) : "";
91
+ if (!question || !answer) continue;
92
+ out.push({
93
+ id: `himi-static-faq-${out.length}`,
94
+ categoryId: STATIC_CATEGORY.id,
95
+ question,
96
+ answer,
97
+ sortOrder: out.length,
98
+ status: "VISIBLE",
99
+ });
100
+ }
101
+ return out;
102
+ }
103
+
104
+ /** The site's pairs as a ready state, or undefined when there are none to fall back to. */
105
+ function staticFallback(state: FaqState): FaqState | undefined {
106
+ if (!staticQuestions.length) return undefined;
107
+ return ready({ ...state, categories: [STATIC_CATEGORY], questions: staticQuestions });
108
+ }
109
+
61
110
  function sortByWixOrder<T extends { id: string; sortOrder?: number }>(left: T, right: T): number {
62
111
  const a = Number.isFinite(left.sortOrder) ? left.sortOrder! : Number.POSITIVE_INFINITY;
63
112
  const b = Number.isFinite(right.sortOrder) ? right.sortOrder! : Number.POSITIVE_INFINITY;
@@ -101,7 +150,8 @@ function ready(state: FaqState): FaqState {
101
150
  const rows = sections.flatMap((section) => section.questions.map((question, index) => ({
102
151
  ...question,
103
152
  categoryTitle: section.title,
104
- categoryHeaderHidden: index !== 0,
153
+ // An untitled section (the static fallback's) has no header to show.
154
+ categoryHeaderHidden: index !== 0 || !section.title,
105
155
  })));
106
156
  return {
107
157
  ...state,
@@ -218,17 +268,28 @@ function commit(state: FaqState): FaqState {
218
268
  return state;
219
269
  }
220
270
 
221
- async function loadFaq(state: FaqState, arg?: { otherCategory?: string }): Promise<FaqState> {
271
+ async function loadFaq(state: FaqState, arg?: { otherCategory?: string; staticFaq?: unknown }): Promise<FaqState> {
222
272
  latestState = state;
273
+ // Only when the caller passed the param: `retry` is dispatched with no params and must keep
274
+ // the pairs the screen's onAppear delivered.
275
+ if (arg && "staticFaq" in arg) staticQuestions = parseStaticFaq(arg.staticFaq);
223
276
  const generation = ++loadGeneration;
224
277
  try {
225
278
  const [categories, questions] = await Promise.all([allCategories(), allQuestions()]);
226
279
  if (generation !== loadGeneration) return latestState;
227
280
  const otherTitle = String(arg?.otherCategory ?? latestState.otherTitle).trim() || latestState.otherTitle;
228
- return commit(ready({ ...latestState, categories, questions, otherTitle }));
281
+ const live = ready({ ...latestState, categories, questions, otherTitle });
282
+ // The Wix FAQ app answered but holds nothing a visitor can see — the site's own pairs,
283
+ // when it has any, instead of an empty state over a site whose FAQ is right there.
284
+ if (questions.length === 0) return commit(staticFallback(live) ?? live);
285
+ return commit(live);
229
286
  } catch (error) {
230
287
  if (generation !== loadGeneration) return latestState;
231
288
  const status = httpStatus(error);
289
+ // No FAQ app (428), no visitor access (403) or a failed read: a site whose FAQ is page
290
+ // content still has an FAQ to show, so its pairs replace the failure card.
291
+ const fallback = staticFallback(latestState);
292
+ if (fallback) return commit(fallback);
232
293
  return commit(failure(latestState, status === 403 ? "forbidden" : status === 428 ? "notInstalled" : "error"));
233
294
  }
234
295
  }
@@ -250,4 +311,4 @@ const svc = defineService("himiBrandedFaq", {
250
311
  },
251
312
  });
252
313
 
253
- export { svc, sectionsFor, httpStatus };
314
+ export { svc, sectionsFor, httpStatus, parseStaticFaq };