@wix/himalaya-cli 0.1046.0 → 0.1048.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
@@ -21498,6 +21498,7 @@ function shouldRevealError(args) {
21498
21498
  var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
21499
21499
  var init_validation = __esm({
21500
21500
  "../../core/ts/src/validation.ts"() {
21501
+ "use strict";
21501
21502
  EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
21502
21503
  URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
21503
21504
  INTEGER_RE = /^[+-]?[0-9]+$/;
@@ -25098,6 +25099,7 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
25098
25099
  var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
25099
25100
  var init_worker_subtree = __esm({
25100
25101
  "../../core/ts/src/worker-subtree.ts"() {
25102
+ "use strict";
25101
25103
  init_worker_subtree_safe_types_generated();
25102
25104
  WORKER_SUBTREE_CEILINGS = Object.freeze({
25103
25105
  maxEncodedBytes: 65536,
@@ -39707,7 +39709,8 @@ function hydrationServices(tier5Dir, bundleName, input = {}) {
39707
39709
  return js ? servicesFromBundleJs(js, input) : null;
39708
39710
  }
39709
39711
  function releaseIdFor(sha, content) {
39710
- return sha !== "dev" ? sha : contentReleaseId(content);
39712
+ if (sha === "dev") return contentReleaseId(content);
39713
+ return `${sha}-${contentReleaseId(content).slice(0, 12)}`;
39711
39714
  }
39712
39715
  function shellSha(root = REPO_ROOT7) {
39713
39716
  if (!isMonorepoCheckout(root)) return "";
@@ -42838,7 +42841,7 @@ function serializeAt(node, src, open) {
42838
42841
  const indent = lineIndent(src, open);
42839
42842
  return JSON.stringify(node, null, 2).split("\n").map((line, i) => i === 0 ? line : indent + line).join("\n");
42840
42843
  }
42841
- function patchGrids(src, installed, patch) {
42844
+ function patchGrids(src, installed, patch, keepFaq = false, dropFaq = false) {
42842
42845
  const OPEN = /"id": "([A-Za-z_0-9]+(?:\.[A-Za-z_0-9]+)*\.verticals)"/g;
42843
42846
  let out = "";
42844
42847
  let cursor = 0;
@@ -42859,12 +42862,17 @@ function patchGrids(src, installed, patch) {
42859
42862
  const prefix = m[1].slice(0, -".verticals".length);
42860
42863
  const baked = /* @__PURE__ */ new Map();
42861
42864
  const foreignCells = [];
42862
- for (const item of items) {
42863
- const id = item.id;
42864
- const legacy = item.modifiers?.accessibilityLabel;
42865
+ let faqCell;
42866
+ const claimOf = (item) => {
42867
+ const id = item?.id;
42868
+ const legacy = item?.modifiers?.accessibilityLabel;
42865
42869
  const claim = typeof id === "string" && id ? id : typeof legacy === "string" ? legacy : "";
42866
- const vertical = claim.startsWith(`${prefix}.`) ? claim.slice(prefix.length + 1) : "";
42870
+ return claim.startsWith(`${prefix}.`) ? claim.slice(prefix.length + 1) : "";
42871
+ };
42872
+ for (const item of items) {
42873
+ const vertical = claimOf(item);
42867
42874
  if (isKnown(vertical)) baked.set(vertical, item);
42875
+ else if (vertical === "faq" && keepFaq) faqCell = item;
42868
42876
  else if (!GRID_VERTICAL_LABELS.has(vertical)) foreignCells.push(item);
42869
42877
  }
42870
42878
  const wanted = [];
@@ -42882,10 +42890,36 @@ function patchGrids(src, installed, patch) {
42882
42890
  wanted.push(vertical);
42883
42891
  }
42884
42892
  const shape = baked.values().next().value ?? items[0];
42893
+ const bakedFaq = faqCell;
42894
+ if (keepFaq && !faqCell && hasScreen(src, FAQ_CARD.screenId)) {
42895
+ faqCell = cloneCell(shape, prefix, "faq", FAQ_CARD);
42896
+ }
42897
+ if (!installed.size) {
42898
+ if (dropFaq) {
42899
+ const kept = items.filter((item) => claimOf(item) !== "faq");
42900
+ if (kept.length !== items.length && kept.length > 0) {
42901
+ node.body.props.items = kept;
42902
+ out += src.slice(cursor, open) + serializeAt(node, src, open);
42903
+ cursor = close;
42904
+ OPEN.lastIndex = close;
42905
+ }
42906
+ continue;
42907
+ }
42908
+ const lastIsVertical = GRID_VERTICAL_LABELS.has(claimOf(items[items.length - 1]));
42909
+ if (!bakedFaq && faqCell && (items.length < EXPLORE_MAX_CELLS || lastIsVertical)) {
42910
+ if (items.length >= EXPLORE_MAX_CELLS) items.pop();
42911
+ items.push(faqCell);
42912
+ out += src.slice(cursor, open) + serializeAt(node, src, open);
42913
+ cursor = close;
42914
+ OPEN.lastIndex = close;
42915
+ }
42916
+ continue;
42917
+ }
42918
+ const cap = faqCell ? EXPLORE_MAX_CELLS - 1 : EXPLORE_MAX_CELLS;
42885
42919
  const next = [...foreignCells];
42886
42920
  const cells = [];
42887
42921
  for (const vertical of wanted) {
42888
- if (next.length >= EXPLORE_MAX_CELLS) break;
42922
+ if (next.length >= cap) break;
42889
42923
  const bakedCell = baked.get(vertical);
42890
42924
  if (bakedCell) {
42891
42925
  next.push(bakedCell);
@@ -42901,6 +42935,7 @@ function patchGrids(src, installed, patch) {
42901
42935
  cells.push(vertical);
42902
42936
  patch.added.push(vertical);
42903
42937
  }
42938
+ if (faqCell && next.length < EXPLORE_MAX_CELLS) next.push(faqCell);
42904
42939
  if (!cells.length) continue;
42905
42940
  patch.cells = cells;
42906
42941
  node.body.props.items = next;
@@ -43015,16 +43050,19 @@ function patchStoreCtas(src, verticals, homeActionId) {
43015
43050
  dropped
43016
43051
  };
43017
43052
  }
43018
- function patchHomeForVerticals(src, verticals, homeActionId) {
43053
+ function patchHomeForVerticals(src, verticals, homeActionId, opts = {}) {
43019
43054
  const installed = new Set(verticals.filter(isKnown));
43020
43055
  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);
43056
+ if (!installed.size) {
43057
+ if (opts.keepFaq || opts.dropFaq) patch.src = patchGrids(src, installed, patch, !!opts.keepFaq, !opts.keepFaq && !!opts.dropFaq);
43058
+ return patch;
43059
+ }
43060
+ const ctas = patchStoreCtas(patchModules(patchGrids(src, installed, patch, opts.keepFaq), installed, patch), [...installed], homeActionId);
43023
43061
  patch.src = ctas.src;
43024
43062
  patch.droppedCtas = ctas.dropped;
43025
43063
  return patch;
43026
43064
  }
43027
- var EXPLORE_CARDS, EXPLORE_ORDER, EXPLORE_MAX_CELLS, MODULE_SOURCE_VERTICALS, isKnown, GRID_VERTICAL_LABELS, escapeRe2, textProps, STORE_SCREENS, isStoreScreen, screenDeclarations, isNode2;
43065
+ 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
43066
  var init_home_descriptor = __esm({
43029
43067
  "src/site-analyze/home-descriptor.ts"() {
43030
43068
  init_types2();
@@ -43096,6 +43134,7 @@ var init_home_descriptor = __esm({
43096
43134
  };
43097
43135
  isKnown = (v) => KNOWN_VERTICALS.includes(v);
43098
43136
  GRID_VERTICAL_LABELS = /* @__PURE__ */ new Set([...KNOWN_VERTICALS, "faq"]);
43137
+ FAQ_CARD = { icon: "help", screenId: "himi_faq", title: "FAQ", body: "Find answers to common questions." };
43099
43138
  escapeRe2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43100
43139
  textProps = (n) => n?.body?.props;
43101
43140
  STORE_SCREENS = /* @__PURE__ */ new Set([
@@ -54141,7 +54180,28 @@ function deriveTailoring(ctx, overrides = {}, revision = "site-derived") {
54141
54180
  };
54142
54181
  return { appName, iconSource: deriveIconSource(ctx), provisioning, missing };
54143
54182
  }
54144
- var clean, ID_MAX, NAME_MAX, TYPE_MAX, CONTROL_CHARS, HAS_CONTROL_CHAR, plain, shown;
54183
+ function bounded(s, max) {
54184
+ if (s.length <= max) return s;
54185
+ const cut = s.slice(0, max - 1);
54186
+ const space = cut.lastIndexOf(" ");
54187
+ return `${(space > max * 0.6 ? cut.slice(0, space) : cut).trimEnd()}\u2026`;
54188
+ }
54189
+ function deriveStaticFaq(faq) {
54190
+ if (!Array.isArray(faq)) return [];
54191
+ const out = [];
54192
+ const seen = /* @__PURE__ */ new Set();
54193
+ for (const raw of faq) {
54194
+ if (out.length >= STATIC_FAQ_LIMITS.pairs) break;
54195
+ const pair = raw && typeof raw === "object" ? raw : {};
54196
+ const q = bounded(oneLine(pair.q), STATIC_FAQ_LIMITS.question);
54197
+ const a = bounded(oneLine(pair.a), STATIC_FAQ_LIMITS.answer);
54198
+ if (!q || !a || seen.has(q.toLowerCase())) continue;
54199
+ seen.add(q.toLowerCase());
54200
+ out.push({ q, a });
54201
+ }
54202
+ return out;
54203
+ }
54204
+ var clean, ID_MAX, NAME_MAX, TYPE_MAX, CONTROL_CHARS, HAS_CONTROL_CHAR, plain, shown, STATIC_FAQ_LIMITS, oneLine;
54145
54205
  var init_derive = __esm({
54146
54206
  "src/site-analyze/derive.ts"() {
54147
54207
  init_types2();
@@ -54153,6 +54213,8 @@ var init_derive = __esm({
54153
54213
  HAS_CONTROL_CHAR = /[\u0000-\u001F\u007F]/;
54154
54214
  plain = (value, max) => (value ?? "").replace(CONTROL_CHARS, "").trim().slice(0, max);
54155
54215
  shown = (value) => (typeof value === "string" ? JSON.stringify(value) : String(value)).slice(0, 64);
54216
+ STATIC_FAQ_LIMITS = { pairs: 20, question: 200, answer: 1500 };
54217
+ oneLine = (s) => typeof s === "string" ? s.replace(/[\u0000-\u001F\u007F\s]+/g, " ").trim() : "";
54156
54218
  }
54157
54219
  });
54158
54220
 
@@ -57211,7 +57273,8 @@ function mergeCustomerOwned(committed, overlay) {
57211
57273
  "navigation",
57212
57274
  "locale",
57213
57275
  "wix",
57214
- "copy"
57276
+ "copy",
57277
+ "faq"
57215
57278
  ];
57216
57279
  const customer = {};
57217
57280
  for (const key2 of CUSTOMER_OWNED) {
@@ -57305,12 +57368,56 @@ function buildMergedContract(options) {
57305
57368
  staged.push(localPath);
57306
57369
  return { path: path2, staged };
57307
57370
  }
57371
+ function catalogTypography(overlay, template) {
57372
+ const typography = {};
57373
+ const notes = [];
57374
+ const base = template && typeof template === "object" ? template : {};
57375
+ const usable = (family) => typeof family === "string" && family.trim() !== "" && (family === SYSTEM_FONT_FAMILY || canonicalFamilyName(family) !== void 0);
57376
+ for (const [role, token] of Object.entries(overlay ?? {})) {
57377
+ const family = token?.family;
57378
+ if (typeof family !== "string" || family.trim() === "") {
57379
+ typography[role] = token;
57380
+ continue;
57381
+ }
57382
+ if (family === SYSTEM_FONT_FAMILY) {
57383
+ typography[role] = token;
57384
+ continue;
57385
+ }
57386
+ const canonical3 = canonicalFamilyName(family.trim());
57387
+ if (canonical3) {
57388
+ typography[role] = { ...token, family: canonical3 };
57389
+ continue;
57390
+ }
57391
+ const kept = base[role]?.family;
57392
+ const using = usable(kept) ? `the template's ${kept}` : `the template's default (${TIER1_FLOOR[0]})`;
57393
+ notes.push(`site font "${family}" (${role}) isn't in the font catalog; using ${using}`);
57394
+ }
57395
+ return { typography, notes };
57396
+ }
57397
+ function staticFaqFor(overlay, overlayPath) {
57398
+ const own3 = overlay.faq;
57399
+ if (own3 !== void 0) return deriveStaticFaq(own3);
57400
+ const bookPath = join59(dirname32(resolve45(overlayPath)), "book.json");
57401
+ if (!existsSync61(bookPath)) return void 0;
57402
+ try {
57403
+ const book = JSON.parse(readFileSync72(bookPath, "utf8"));
57404
+ const copy = book?.copy;
57405
+ if (!copy || typeof copy !== "object" || copy.unavailable) return void 0;
57406
+ const pairs = deriveStaticFaq(copy.faq);
57407
+ if (!pairs.length && copy.partial) return void 0;
57408
+ return pairs;
57409
+ } catch {
57410
+ return void 0;
57411
+ }
57412
+ }
57308
57413
  async function applyOverlays(input) {
57309
57414
  const cwd = resolve45(input.cwd);
57310
57415
  const shell = input.shell ?? DEFAULT_BRANDABLE_SHELL;
57311
57416
  assertKnownShell(shell);
57312
57417
  const overlay = JSON.parse(readFileSync72(input.overlayPath, "utf8"));
57313
57418
  const tokens = JSON.parse(readFileSync72(input.tokensPath, "utf8"));
57419
+ const faq = staticFaqFor(overlay, input.overlayPath);
57420
+ if (faq !== void 0) overlay.faq = faq;
57314
57421
  const wrote = [];
57315
57422
  const notes = [];
57316
57423
  const repoRoot2 = findRepoRoot(cwd);
@@ -57344,9 +57451,11 @@ async function applyOverlays(input) {
57344
57451
  }
57345
57452
  const pkgProv = join59(cwd, "provisioning.json");
57346
57453
  assertNotCommitted(pkgProv, input.forbiddenProvisioning);
57454
+ let packageFaq = faq;
57347
57455
  if (existsSync61(pkgProv)) {
57348
57456
  const cur = JSON.parse(readFileSync72(pkgProv, "utf8"));
57349
57457
  const merged = deepMerge4(cur, overlay);
57458
+ packageFaq = merged.faq;
57350
57459
  writeFileSync27(pkgProv, JSON.stringify(merged, null, 2) + "\n");
57351
57460
  wrote.push(pkgProv);
57352
57461
  } else {
@@ -57356,7 +57465,13 @@ async function applyOverlays(input) {
57356
57465
  const pkgTokens = join59(cwd, "tokens.json");
57357
57466
  if (existsSync61(pkgTokens) && (tokens.colors || tokens.typography)) {
57358
57467
  const cur = JSON.parse(readFileSync72(pkgTokens, "utf8"));
57359
- const merged = deepMerge4(cur, tokens);
57468
+ let tokensIn = tokens;
57469
+ if (tokens.typography) {
57470
+ const fonts = catalogTypography(tokens.typography, cur.typography);
57471
+ notes.push(...fonts.notes);
57472
+ tokensIn = { ...tokens, typography: fonts.typography };
57473
+ }
57474
+ const merged = deepMerge4(cur, tokensIn);
57360
57475
  const mergedColors = merged.colors;
57361
57476
  if (mergedColors && typeof mergedColors === "object" && !Array.isArray(mergedColors)) {
57362
57477
  merged.colors = alignAccentsToSurfaces(mergedColors, tokens.colors);
@@ -57379,8 +57494,16 @@ async function applyOverlays(input) {
57379
57494
  const prev = readFileSync72(homeSrc, "utf8");
57380
57495
  const patched = patchBrandParams(prev, overlay, homeAction);
57381
57496
  let next = patched.src;
57382
- if (!patched.foreign && overlay.installedVerticals?.length) {
57383
- const home = patchHomeForVerticals(next, overlay.installedVerticals, homeAction);
57497
+ if (!patched.foreign && faq?.length && prev.includes('"tier5.himiBrandedFaq.load"') && !prev.includes('"staticFaq"')) {
57498
+ notes.push(
57499
+ "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"
57500
+ );
57501
+ }
57502
+ const keepFaq = Array.isArray(packageFaq) && packageFaq.length > 0;
57503
+ const dropFaq = Array.isArray(faq) && faq.length === 0;
57504
+ const ownsHome = prev.includes(`"actionId": "${homeAction}"`);
57505
+ if (ownsHome && !patched.foreign && (overlay.installedVerticals?.length || keepFaq || dropFaq)) {
57506
+ const home = patchHomeForVerticals(next, overlay.installedVerticals ?? [], homeAction, { keepFaq, dropFaq });
57384
57507
  next = home.src;
57385
57508
  if (home.droppedCtas.length) {
57386
57509
  notes.push(`removed the home's store call(s) to action (${home.droppedCtas.join(", ")}) \u2014 this site has no store`);
@@ -57426,10 +57549,18 @@ function patchBrandParams(src, overlay, homeActionId) {
57426
57549
  const layout = overlay.appearance?.homeLayout;
57427
57550
  const accent = overlay.branding?.primaryColor;
57428
57551
  const verts = overlay.installedVerticals;
57552
+ const faq = overlay.faq;
57429
57553
  const edits = [
57430
57554
  ...layout ? [{ pattern: /"ownerDefaultLayout": "[^"]*"/g, replacement: `"ownerDefaultLayout": ${JSON.stringify(layout)}` }] : [],
57431
57555
  ...accent ? [{ pattern: /"brandAccent": "[^"]*"/g, replacement: `"brandAccent": ${JSON.stringify(accent)}` }] : [],
57432
- ...verts?.length ? [{ pattern: /"verticals": "\[[^\]]*\]"/g, replacement: `"verticals": ${JSON.stringify(JSON.stringify(verts))}` }] : []
57556
+ ...verts?.length ? [{ pattern: /"verticals": "\[[^\]]*\]"/g, replacement: `"verticals": ${JSON.stringify(JSON.stringify(verts))}` }] : [],
57557
+ // The FAQ screen's fallback pairs (#3276), a JSON STRING like `verticals` because action
57558
+ // params are string-valued. The pattern matches any escaped string, so re-applying a
57559
+ // package whose pairs are already written replaces them instead of missing. A function
57560
+ // replacement, because the pairs are customer text and may contain `$&`. Patched whenever
57561
+ // the analysis SAID something, `[]` included, so a re-apply clears pairs an earlier one
57562
+ // baked in rather than leaving the worker another site's FAQ.
57563
+ ...faq ? [{ pattern: /"staticFaq": "(?:[^"\\]|\\.)*"/g, replacement: `"staticFaq": ${JSON.stringify(JSON.stringify(faq))}` }] : []
57433
57564
  ];
57434
57565
  if (!edits.length) return { src, applied: 0, unparsed: 0, foreign: false };
57435
57566
  if (!src.includes(`"actionId": "${homeActionId}"`)) {
@@ -57474,7 +57605,7 @@ function patchBrandParams(src, overlay, homeActionId) {
57474
57605
  pattern.lastIndex = 0;
57475
57606
  if (!pattern.test(block)) continue;
57476
57607
  pattern.lastIndex = 0;
57477
- block = block.replace(pattern, replacement);
57608
+ block = block.replace(pattern, () => replacement);
57478
57609
  applied += 1;
57479
57610
  }
57480
57611
  out += src.slice(cursor, open) + block;
@@ -57492,6 +57623,10 @@ var init_apply2 = __esm({
57492
57623
  init_branding_derive();
57493
57624
  init_brand_color_math();
57494
57625
  init_home_descriptor();
57626
+ init_derive();
57627
+ init_catalog();
57628
+ init_plan_fonts();
57629
+ init_tokens();
57495
57630
  STAGED = ".himi/brand";
57496
57631
  }
57497
57632
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1046.0",
3
+ "version": "0.1048.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": "dc270534070a6b5e662ac7738098c1328a07b027779bddf2a5d71a20"
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 };