@wix/himalaya-cli 0.1241.0 → 0.1242.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.
Files changed (46) hide show
  1. package/dist/cli.mjs +0 -1
  2. package/package.json +2 -2
  3. package/templates/beauty-salon-owner/dev/index.ts +2 -2
  4. package/templates/beauty-salon-owner/tier5-src/_shared/owner-selected-site.ts +9 -3
  5. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-overview/index.ts +14 -9
  6. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-sites/index.ts +35 -4
  7. package/templates/community-club-owner/dev/index.ts +2 -2
  8. package/templates/community-club-owner/tier5-src/_shared/owner-selected-site.ts +9 -3
  9. package/templates/community-club-owner/tier5-src/himi-owner-lite-overview/index.ts +14 -9
  10. package/templates/community-club-owner/tier5-src/himi-owner-lite-sites/index.ts +35 -4
  11. package/templates/events-meetups-owner/dev/index.ts +2 -2
  12. package/templates/events-meetups-owner/tier5-src/_shared/owner-selected-site.ts +9 -3
  13. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-overview/index.ts +14 -9
  14. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-sites/index.ts +35 -4
  15. package/templates/gym-fitness-owner/dev/index.ts +2 -2
  16. package/templates/gym-fitness-owner/tier5-src/_shared/owner-selected-site.ts +9 -3
  17. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-overview/index.ts +14 -9
  18. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-sites/index.ts +35 -4
  19. package/templates/nonprofit-cause-owner/dev/index.ts +2 -2
  20. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-selected-site.ts +9 -3
  21. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-overview/index.ts +14 -9
  22. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-sites/index.ts +35 -4
  23. package/templates/owner-lite/dev/index.ts +2 -2
  24. package/templates/owner-lite/tier5-src/_shared/owner-selected-site.ts +9 -3
  25. package/templates/owner-lite/tier5-src/himi-owner-lite-overview/index.ts +14 -9
  26. package/templates/owner-lite/tier5-src/himi-owner-lite-sites/index.ts +35 -4
  27. package/templates/podcast-creator-owner/dev/index.ts +2 -2
  28. package/templates/podcast-creator-owner/tier5-src/_shared/owner-selected-site.ts +9 -3
  29. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-overview/index.ts +14 -9
  30. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-sites/index.ts +35 -4
  31. package/templates/portfolio-creative-owner/dev/index.ts +2 -2
  32. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-selected-site.ts +9 -3
  33. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-overview/index.ts +14 -9
  34. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-sites/index.ts +35 -4
  35. package/templates/restaurant-owner/dev/index.ts +2 -2
  36. package/templates/restaurant-owner/tier5-src/_shared/owner-selected-site.ts +9 -3
  37. package/templates/restaurant-owner/tier5-src/himi-owner-lite-overview/index.ts +14 -9
  38. package/templates/restaurant-owner/tier5-src/himi-owner-lite-sites/index.ts +35 -4
  39. package/templates/retail-store-owner/dev/index.ts +2 -2
  40. package/templates/retail-store-owner/tier5-src/_shared/owner-selected-site.ts +9 -3
  41. package/templates/retail-store-owner/tier5-src/himi-owner-lite-overview/index.ts +14 -9
  42. package/templates/retail-store-owner/tier5-src/himi-owner-lite-sites/index.ts +35 -4
  43. package/templates/yoga-studio-owner/dev/index.ts +2 -2
  44. package/templates/yoga-studio-owner/tier5-src/_shared/owner-selected-site.ts +9 -3
  45. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-overview/index.ts +14 -9
  46. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-sites/index.ts +35 -4
package/dist/cli.mjs CHANGED
@@ -13432,7 +13432,6 @@ function resolveIdiom(tokens2, ctx = {}) {
13432
13432
  var IDIOM_CONTEXTS, UNSAFE_KEYS;
13433
13433
  var init_idiom = __esm({
13434
13434
  "../../core/ts/src/idiom.ts"() {
13435
- "use strict";
13436
13435
  IDIOM_CONTEXTS = {
13437
13436
  ios: { native: ["ios"] },
13438
13437
  android: { native: ["material"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1241.0",
3
+ "version": "0.1242.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": [
@@ -67,5 +67,5 @@
67
67
  "artifactId": "himalaya-cli"
68
68
  }
69
69
  },
70
- "falconPackageHash": "5e95fe6c20dcd9b9fbc99564f2535787252632211a307e0d5b9908f9"
70
+ "falconPackageHash": "b728c51ecb462034dd49f5a7e49e7f41069407835c052e156ff4d7ea"
71
71
  }
@@ -61885,7 +61885,7 @@ const SCREENS = runtimeSetting({
61885
61885
  },
61886
61886
  "pressFeedback": "scale",
61887
61887
  "haptic": "light",
61888
- "accessibilityLabel": "himi_owner_lite.site_row"
61888
+ "accessibilityLabel": "$item.spokenLabel"
61889
61889
  }
61890
61890
  }
61891
61891
  ]
@@ -62445,7 +62445,7 @@ const SCREENS = runtimeSetting({
62445
62445
  },
62446
62446
  "pressFeedback": "scale",
62447
62447
  "haptic": "light",
62448
- "accessibilityLabel": "himi_owner_lite.overview_row"
62448
+ "accessibilityLabel": "$item.spokenLabel"
62449
62449
  }
62450
62450
  }
62451
62451
  ]
@@ -27,6 +27,8 @@ type Host = typeof globalThis & { himalaya?: { storage?: StorageLike; input?: Re
27
27
  export interface SelectedSite {
28
28
  id: string;
29
29
  name: string;
30
+ /** True when the picker wrote this from a current, repaired Wix site row. */
31
+ nameFromWix?: boolean;
30
32
  }
31
33
 
32
34
  const KEY = "himi.ownerLite.selectedSite.v1";
@@ -60,13 +62,15 @@ export function readSelectedSite(): SelectedSite | null {
60
62
  if (account && account !== me) return null;
61
63
  // An entry the picker wrote before the host knew the owner is CLAIMED by the first
62
64
  // worker that does know it, so from then on no other owner on the device reads it.
63
- if (!account && me) host()?.storage?.setItem(KEY, JSON.stringify({ id, name, account: me }));
65
+ if (!account && me) host()?.storage?.setItem(KEY, JSON.stringify({
66
+ id, name, ...(parsed.nameFromWix === true ? { nameFromWix: true } : {}), account: me,
67
+ }));
64
68
  // A screen opened FOR a specific business (a deep link or push carrying
65
69
  // `business`) names that business's data; a name recorded for another one would
66
70
  // sit over the wrong numbers, so it falls back instead.
67
71
  const opened = inputString("business");
68
72
  if (opened && opened !== id) return null;
69
- return { id, name };
73
+ return { id, name, ...(parsed.nameFromWix === true ? { nameFromWix: true } : {}) };
70
74
  } catch {
71
75
  return null; // a corrupt entry reads as "nothing picked", never as a crash
72
76
  }
@@ -80,7 +84,9 @@ export function rememberSelectedSite(site: SelectedSite): void {
80
84
  if (!id || !name) return;
81
85
  const account = inputString("__himiPreferenceAccount");
82
86
  try {
83
- host()?.storage?.setItem(KEY, JSON.stringify(account ? { id, name, account } : { id, name }));
87
+ host()?.storage?.setItem(KEY, JSON.stringify(account
88
+ ? { id, name, nameFromWix: true, account }
89
+ : { id, name, nameFromWix: true }));
84
90
  } catch {
85
91
  // Quota / storage disabled: the chrome falls back to the build-time name.
86
92
  }
@@ -44,6 +44,8 @@ const PER_SITE_LIMIT = 50;
44
44
  interface OverviewRow {
45
45
  id: string;
46
46
  name: string;
47
+ /** A complete, useful spoken name for the row's tap target. */
48
+ spokenLabel: string;
47
49
  initial: string;
48
50
  /** "3", "50+", or "—" when that business could not be read. */
49
51
  attention: string;
@@ -79,21 +81,24 @@ function countLabel(n: number | null): string {
79
81
 
80
82
  function rowFor(id: string, name: string, count: number | null): OverviewRow {
81
83
  const needs = count !== null && count > 0;
84
+ const attention = countLabel(count);
85
+ const attentionLabel = count === null
86
+ ? copyParam("ownerOverview.unavailable", "Couldn’t read this business")
87
+ : count === 0
88
+ ? copyParam("ownerOverview.allClear", "All clear")
89
+ : count === 1
90
+ ? copyParam("ownerOverview.oneNeedsAttention", "order needs attention")
91
+ : copyParam("ownerOverview.manyNeedAttention", "orders need attention");
82
92
  return {
83
93
  id,
84
94
  name,
95
+ spokenLabel: [name, attention, attentionLabel].filter(Boolean).join(", "),
85
96
  initial: initialOf(name),
86
- attention: countLabel(count),
97
+ attention,
87
98
  // Reads as ONE phrase with the count rendered beside it — "3" + "orders need
88
99
  // attention". So the label never repeats the number, or the row would say "1 · 1 order
89
100
  // needs attention".
90
- attentionLabel: count === null
91
- ? copyParam("ownerOverview.unavailable", "Couldn’t read this business")
92
- : count === 0
93
- ? copyParam("ownerOverview.allClear", "All clear")
94
- : count === 1
95
- ? copyParam("ownerOverview.oneNeedsAttention", "order needs attention")
96
- : copyParam("ownerOverview.manyNeedAttention", "orders need attention"),
101
+ attentionLabel,
97
102
  needsAttention: needs,
98
103
  calm: !needs,
99
104
  };
@@ -179,7 +184,7 @@ async function loadRollup(): Promise<OverviewState> {
179
184
  .filter((r: WixSite) => r?.id)
180
185
  .map((r: WixSite) => ({
181
186
  id: r.id || "",
182
- name: r.displayName || (r as { name?: string }).name || r.id || "Site",
187
+ name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
183
188
  }));
184
189
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
185
190
 
@@ -13,6 +13,7 @@ import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
13
13
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
14
14
  import { getAll, upsert, type DbEntity } from "../_shared/owner-local-db.js";
15
15
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
16
+ import { readSelectedSite } from "../_shared/owner-selected-site.js";
16
17
 
17
18
  // Offline-first (data-offline): the owner's last-synced businesses are the
18
19
  // READ-THROUGH cache for the `sites` entity — paint them instantly (works
@@ -33,6 +34,8 @@ interface SiteRow {
33
34
  id: string; // metaSiteId
34
35
  name: string;
35
36
  meta: string;
37
+ /** A complete, useful spoken name for the row's tap target. */
38
+ spokenLabel: string;
36
39
  initial: string;
37
40
  }
38
41
 
@@ -55,12 +58,13 @@ interface SitesState {
55
58
  }
56
59
 
57
60
  function normalize(raw: WixSite): SiteRow {
58
- const name = raw.displayName || (raw as { name?: string }).name || raw.id || "Site";
61
+ const name = raw.displayName || (raw as { name?: string }).name || copyParam("ownerSites.unnamedBusiness", "Business");
59
62
  const studio = (raw as { namespace?: string }).namespace === "WIX_STUDIO";
60
63
  return {
61
64
  id: raw.id || "",
62
65
  name,
63
66
  meta: studio ? "Wix Studio site" : "Wix site",
67
+ spokenLabel: [name, studio ? "Wix Studio site" : "Wix site"].join(", "),
64
68
  initial: (name.trim()[0] || "S").toUpperCase(),
65
69
  };
66
70
  }
@@ -70,6 +74,30 @@ function applySearch(rows: SiteRow[], query: string): SiteRow[] {
70
74
  return q ? rows.filter((r) => r.name.toLowerCase().includes(q)) : rows;
71
75
  }
72
76
 
77
+ /** Upgrade rows cached by older bundles, which used the private site id as a fallback name. */
78
+ function repairCachedRow(row: SiteRow, selected: ReturnType<typeof readSelectedSite>): SiteRow {
79
+ // Rows written by this bundle already carry a spoken name, so preserve valid
80
+ // Wix display names even when a business name happens to equal its site ID.
81
+ if (typeof row.spokenLabel === "string" && row.spokenLabel) return row;
82
+ // A legacy cache has no provenance bit. For the currently selected business,
83
+ // the picker kept the Wix display name separately; use it to distinguish a
84
+ // legitimate same-as-ID name from the old private-ID fallback.
85
+ const selectedName = selected?.id === row.id
86
+ && (selected.name !== row.id || selected.nameFromWix === true)
87
+ ? selected.name : "";
88
+ const name = selectedName || (row.name && row.name !== row.id
89
+ ? row.name
90
+ : copyParam("ownerSites.unnamedBusiness", "Business"));
91
+ const meta = typeof row.meta === "string" ? row.meta : "";
92
+ return {
93
+ ...row,
94
+ name,
95
+ meta,
96
+ spokenLabel: [name, ...meta.split(" · ")].filter(Boolean).join(", "),
97
+ initial: (name.trim()[0] || "S").toUpperCase(),
98
+ };
99
+ }
100
+
73
101
  function emptyFor(query = ""): string {
74
102
  return query.trim()
75
103
  ? copyParam("ownerSites.emptySearch", "No businesses match that search.")
@@ -106,6 +134,7 @@ function demoList(s: SitesState): SitesState {
106
134
  id: d.id,
107
135
  name: d.name,
108
136
  meta: [d.domain, d.plan].filter(Boolean).join(" · "),
137
+ spokenLabel: [d.name, d.domain, d.plan].filter(Boolean).join(", "),
109
138
  initial: (d.name[0] || "S").toUpperCase(),
110
139
  }));
111
140
  return shape({
@@ -122,8 +151,10 @@ async function loadList(s: SitesState): Promise<SitesState> {
122
151
  if (isOwnerPreviewDemo()) return demoList(s);
123
152
  // 1) Instant paint from the on-device cache (offline-safe; empty on first-ever run).
124
153
  const cached = await readCachedRows();
125
- const base = cached.length
126
- ? shape({ ...s, status: "ready", allSites: cached, sites: applySearch(cached, s.searchQuery), error: "", demoNote: "" })
154
+ const selected = readSelectedSite();
155
+ const repairedCache = cached.map((row) => repairCachedRow(row, selected));
156
+ const base = repairedCache.length
157
+ ? shape({ ...s, status: "ready", allSites: repairedCache, sites: applySearch(repairedCache, s.searchQuery), error: "", demoNote: "" })
127
158
  : s;
128
159
  // 2) Revalidate from Wix and refresh the cache.
129
160
  try {
@@ -133,7 +164,7 @@ async function loadList(s: SitesState): Promise<SitesState> {
133
164
  return shape({ ...s, status: "ready", allSites: rows, sites: applySearch(rows, s.searchQuery), resultSummary: `${rows.length} ${rows.length === 1 ? "business" : "businesses"}`, error: "", demoNote: "" });
134
165
  } catch (e) {
135
166
  // Offline / fetch failure: keep the last-synced cache BEFORE the demo/error fallbacks.
136
- if (cached.length) return base;
167
+ if (repairedCache.length) return base;
137
168
  if (isBridgeUnavailable(e)) return demoList(s);
138
169
  return shape({ ...s, status: "error", allSites: [], sites: [], error: ownerLoadError(e) });
139
170
  }
@@ -61885,7 +61885,7 @@ const SCREENS = runtimeSetting({
61885
61885
  },
61886
61886
  "pressFeedback": "scale",
61887
61887
  "haptic": "light",
61888
- "accessibilityLabel": "himi_owner_lite.site_row"
61888
+ "accessibilityLabel": "$item.spokenLabel"
61889
61889
  }
61890
61890
  }
61891
61891
  ]
@@ -62445,7 +62445,7 @@ const SCREENS = runtimeSetting({
62445
62445
  },
62446
62446
  "pressFeedback": "scale",
62447
62447
  "haptic": "light",
62448
- "accessibilityLabel": "himi_owner_lite.overview_row"
62448
+ "accessibilityLabel": "$item.spokenLabel"
62449
62449
  }
62450
62450
  }
62451
62451
  ]
@@ -27,6 +27,8 @@ type Host = typeof globalThis & { himalaya?: { storage?: StorageLike; input?: Re
27
27
  export interface SelectedSite {
28
28
  id: string;
29
29
  name: string;
30
+ /** True when the picker wrote this from a current, repaired Wix site row. */
31
+ nameFromWix?: boolean;
30
32
  }
31
33
 
32
34
  const KEY = "himi.ownerLite.selectedSite.v1";
@@ -60,13 +62,15 @@ export function readSelectedSite(): SelectedSite | null {
60
62
  if (account && account !== me) return null;
61
63
  // An entry the picker wrote before the host knew the owner is CLAIMED by the first
62
64
  // worker that does know it, so from then on no other owner on the device reads it.
63
- if (!account && me) host()?.storage?.setItem(KEY, JSON.stringify({ id, name, account: me }));
65
+ if (!account && me) host()?.storage?.setItem(KEY, JSON.stringify({
66
+ id, name, ...(parsed.nameFromWix === true ? { nameFromWix: true } : {}), account: me,
67
+ }));
64
68
  // A screen opened FOR a specific business (a deep link or push carrying
65
69
  // `business`) names that business's data; a name recorded for another one would
66
70
  // sit over the wrong numbers, so it falls back instead.
67
71
  const opened = inputString("business");
68
72
  if (opened && opened !== id) return null;
69
- return { id, name };
73
+ return { id, name, ...(parsed.nameFromWix === true ? { nameFromWix: true } : {}) };
70
74
  } catch {
71
75
  return null; // a corrupt entry reads as "nothing picked", never as a crash
72
76
  }
@@ -80,7 +84,9 @@ export function rememberSelectedSite(site: SelectedSite): void {
80
84
  if (!id || !name) return;
81
85
  const account = inputString("__himiPreferenceAccount");
82
86
  try {
83
- host()?.storage?.setItem(KEY, JSON.stringify(account ? { id, name, account } : { id, name }));
87
+ host()?.storage?.setItem(KEY, JSON.stringify(account
88
+ ? { id, name, nameFromWix: true, account }
89
+ : { id, name, nameFromWix: true }));
84
90
  } catch {
85
91
  // Quota / storage disabled: the chrome falls back to the build-time name.
86
92
  }
@@ -44,6 +44,8 @@ const PER_SITE_LIMIT = 50;
44
44
  interface OverviewRow {
45
45
  id: string;
46
46
  name: string;
47
+ /** A complete, useful spoken name for the row's tap target. */
48
+ spokenLabel: string;
47
49
  initial: string;
48
50
  /** "3", "50+", or "—" when that business could not be read. */
49
51
  attention: string;
@@ -79,21 +81,24 @@ function countLabel(n: number | null): string {
79
81
 
80
82
  function rowFor(id: string, name: string, count: number | null): OverviewRow {
81
83
  const needs = count !== null && count > 0;
84
+ const attention = countLabel(count);
85
+ const attentionLabel = count === null
86
+ ? copyParam("ownerOverview.unavailable", "Couldn’t read this business")
87
+ : count === 0
88
+ ? copyParam("ownerOverview.allClear", "All clear")
89
+ : count === 1
90
+ ? copyParam("ownerOverview.oneNeedsAttention", "order needs attention")
91
+ : copyParam("ownerOverview.manyNeedAttention", "orders need attention");
82
92
  return {
83
93
  id,
84
94
  name,
95
+ spokenLabel: [name, attention, attentionLabel].filter(Boolean).join(", "),
85
96
  initial: initialOf(name),
86
- attention: countLabel(count),
97
+ attention,
87
98
  // Reads as ONE phrase with the count rendered beside it — "3" + "orders need
88
99
  // attention". So the label never repeats the number, or the row would say "1 · 1 order
89
100
  // needs attention".
90
- attentionLabel: count === null
91
- ? copyParam("ownerOverview.unavailable", "Couldn’t read this business")
92
- : count === 0
93
- ? copyParam("ownerOverview.allClear", "All clear")
94
- : count === 1
95
- ? copyParam("ownerOverview.oneNeedsAttention", "order needs attention")
96
- : copyParam("ownerOverview.manyNeedAttention", "orders need attention"),
101
+ attentionLabel,
97
102
  needsAttention: needs,
98
103
  calm: !needs,
99
104
  };
@@ -179,7 +184,7 @@ async function loadRollup(): Promise<OverviewState> {
179
184
  .filter((r: WixSite) => r?.id)
180
185
  .map((r: WixSite) => ({
181
186
  id: r.id || "",
182
- name: r.displayName || (r as { name?: string }).name || r.id || "Site",
187
+ name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
183
188
  }));
184
189
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
185
190
 
@@ -13,6 +13,7 @@ import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
13
13
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
14
14
  import { getAll, upsert, type DbEntity } from "../_shared/owner-local-db.js";
15
15
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
16
+ import { readSelectedSite } from "../_shared/owner-selected-site.js";
16
17
 
17
18
  // Offline-first (data-offline): the owner's last-synced businesses are the
18
19
  // READ-THROUGH cache for the `sites` entity — paint them instantly (works
@@ -33,6 +34,8 @@ interface SiteRow {
33
34
  id: string; // metaSiteId
34
35
  name: string;
35
36
  meta: string;
37
+ /** A complete, useful spoken name for the row's tap target. */
38
+ spokenLabel: string;
36
39
  initial: string;
37
40
  }
38
41
 
@@ -55,12 +58,13 @@ interface SitesState {
55
58
  }
56
59
 
57
60
  function normalize(raw: WixSite): SiteRow {
58
- const name = raw.displayName || (raw as { name?: string }).name || raw.id || "Site";
61
+ const name = raw.displayName || (raw as { name?: string }).name || copyParam("ownerSites.unnamedBusiness", "Business");
59
62
  const studio = (raw as { namespace?: string }).namespace === "WIX_STUDIO";
60
63
  return {
61
64
  id: raw.id || "",
62
65
  name,
63
66
  meta: studio ? "Wix Studio site" : "Wix site",
67
+ spokenLabel: [name, studio ? "Wix Studio site" : "Wix site"].join(", "),
64
68
  initial: (name.trim()[0] || "S").toUpperCase(),
65
69
  };
66
70
  }
@@ -70,6 +74,30 @@ function applySearch(rows: SiteRow[], query: string): SiteRow[] {
70
74
  return q ? rows.filter((r) => r.name.toLowerCase().includes(q)) : rows;
71
75
  }
72
76
 
77
+ /** Upgrade rows cached by older bundles, which used the private site id as a fallback name. */
78
+ function repairCachedRow(row: SiteRow, selected: ReturnType<typeof readSelectedSite>): SiteRow {
79
+ // Rows written by this bundle already carry a spoken name, so preserve valid
80
+ // Wix display names even when a business name happens to equal its site ID.
81
+ if (typeof row.spokenLabel === "string" && row.spokenLabel) return row;
82
+ // A legacy cache has no provenance bit. For the currently selected business,
83
+ // the picker kept the Wix display name separately; use it to distinguish a
84
+ // legitimate same-as-ID name from the old private-ID fallback.
85
+ const selectedName = selected?.id === row.id
86
+ && (selected.name !== row.id || selected.nameFromWix === true)
87
+ ? selected.name : "";
88
+ const name = selectedName || (row.name && row.name !== row.id
89
+ ? row.name
90
+ : copyParam("ownerSites.unnamedBusiness", "Business"));
91
+ const meta = typeof row.meta === "string" ? row.meta : "";
92
+ return {
93
+ ...row,
94
+ name,
95
+ meta,
96
+ spokenLabel: [name, ...meta.split(" · ")].filter(Boolean).join(", "),
97
+ initial: (name.trim()[0] || "S").toUpperCase(),
98
+ };
99
+ }
100
+
73
101
  function emptyFor(query = ""): string {
74
102
  return query.trim()
75
103
  ? copyParam("ownerSites.emptySearch", "No businesses match that search.")
@@ -106,6 +134,7 @@ function demoList(s: SitesState): SitesState {
106
134
  id: d.id,
107
135
  name: d.name,
108
136
  meta: [d.domain, d.plan].filter(Boolean).join(" · "),
137
+ spokenLabel: [d.name, d.domain, d.plan].filter(Boolean).join(", "),
109
138
  initial: (d.name[0] || "S").toUpperCase(),
110
139
  }));
111
140
  return shape({
@@ -122,8 +151,10 @@ async function loadList(s: SitesState): Promise<SitesState> {
122
151
  if (isOwnerPreviewDemo()) return demoList(s);
123
152
  // 1) Instant paint from the on-device cache (offline-safe; empty on first-ever run).
124
153
  const cached = await readCachedRows();
125
- const base = cached.length
126
- ? shape({ ...s, status: "ready", allSites: cached, sites: applySearch(cached, s.searchQuery), error: "", demoNote: "" })
154
+ const selected = readSelectedSite();
155
+ const repairedCache = cached.map((row) => repairCachedRow(row, selected));
156
+ const base = repairedCache.length
157
+ ? shape({ ...s, status: "ready", allSites: repairedCache, sites: applySearch(repairedCache, s.searchQuery), error: "", demoNote: "" })
127
158
  : s;
128
159
  // 2) Revalidate from Wix and refresh the cache.
129
160
  try {
@@ -133,7 +164,7 @@ async function loadList(s: SitesState): Promise<SitesState> {
133
164
  return shape({ ...s, status: "ready", allSites: rows, sites: applySearch(rows, s.searchQuery), resultSummary: `${rows.length} ${rows.length === 1 ? "business" : "businesses"}`, error: "", demoNote: "" });
134
165
  } catch (e) {
135
166
  // Offline / fetch failure: keep the last-synced cache BEFORE the demo/error fallbacks.
136
- if (cached.length) return base;
167
+ if (repairedCache.length) return base;
137
168
  if (isBridgeUnavailable(e)) return demoList(s);
138
169
  return shape({ ...s, status: "error", allSites: [], sites: [], error: ownerLoadError(e) });
139
170
  }
@@ -61517,7 +61517,7 @@ const SCREENS = runtimeSetting({
61517
61517
  },
61518
61518
  "pressFeedback": "scale",
61519
61519
  "haptic": "light",
61520
- "accessibilityLabel": "himi_owner_lite.site_row"
61520
+ "accessibilityLabel": "$item.spokenLabel"
61521
61521
  }
61522
61522
  }
61523
61523
  ]
@@ -62077,7 +62077,7 @@ const SCREENS = runtimeSetting({
62077
62077
  },
62078
62078
  "pressFeedback": "scale",
62079
62079
  "haptic": "light",
62080
- "accessibilityLabel": "himi_owner_lite.overview_row"
62080
+ "accessibilityLabel": "$item.spokenLabel"
62081
62081
  }
62082
62082
  }
62083
62083
  ]
@@ -27,6 +27,8 @@ type Host = typeof globalThis & { himalaya?: { storage?: StorageLike; input?: Re
27
27
  export interface SelectedSite {
28
28
  id: string;
29
29
  name: string;
30
+ /** True when the picker wrote this from a current, repaired Wix site row. */
31
+ nameFromWix?: boolean;
30
32
  }
31
33
 
32
34
  const KEY = "himi.ownerLite.selectedSite.v1";
@@ -60,13 +62,15 @@ export function readSelectedSite(): SelectedSite | null {
60
62
  if (account && account !== me) return null;
61
63
  // An entry the picker wrote before the host knew the owner is CLAIMED by the first
62
64
  // worker that does know it, so from then on no other owner on the device reads it.
63
- if (!account && me) host()?.storage?.setItem(KEY, JSON.stringify({ id, name, account: me }));
65
+ if (!account && me) host()?.storage?.setItem(KEY, JSON.stringify({
66
+ id, name, ...(parsed.nameFromWix === true ? { nameFromWix: true } : {}), account: me,
67
+ }));
64
68
  // A screen opened FOR a specific business (a deep link or push carrying
65
69
  // `business`) names that business's data; a name recorded for another one would
66
70
  // sit over the wrong numbers, so it falls back instead.
67
71
  const opened = inputString("business");
68
72
  if (opened && opened !== id) return null;
69
- return { id, name };
73
+ return { id, name, ...(parsed.nameFromWix === true ? { nameFromWix: true } : {}) };
70
74
  } catch {
71
75
  return null; // a corrupt entry reads as "nothing picked", never as a crash
72
76
  }
@@ -80,7 +84,9 @@ export function rememberSelectedSite(site: SelectedSite): void {
80
84
  if (!id || !name) return;
81
85
  const account = inputString("__himiPreferenceAccount");
82
86
  try {
83
- host()?.storage?.setItem(KEY, JSON.stringify(account ? { id, name, account } : { id, name }));
87
+ host()?.storage?.setItem(KEY, JSON.stringify(account
88
+ ? { id, name, nameFromWix: true, account }
89
+ : { id, name, nameFromWix: true }));
84
90
  } catch {
85
91
  // Quota / storage disabled: the chrome falls back to the build-time name.
86
92
  }
@@ -44,6 +44,8 @@ const PER_SITE_LIMIT = 50;
44
44
  interface OverviewRow {
45
45
  id: string;
46
46
  name: string;
47
+ /** A complete, useful spoken name for the row's tap target. */
48
+ spokenLabel: string;
47
49
  initial: string;
48
50
  /** "3", "50+", or "—" when that business could not be read. */
49
51
  attention: string;
@@ -79,21 +81,24 @@ function countLabel(n: number | null): string {
79
81
 
80
82
  function rowFor(id: string, name: string, count: number | null): OverviewRow {
81
83
  const needs = count !== null && count > 0;
84
+ const attention = countLabel(count);
85
+ const attentionLabel = count === null
86
+ ? copyParam("ownerOverview.unavailable", "Couldn’t read this business")
87
+ : count === 0
88
+ ? copyParam("ownerOverview.allClear", "All clear")
89
+ : count === 1
90
+ ? copyParam("ownerOverview.oneNeedsAttention", "order needs attention")
91
+ : copyParam("ownerOverview.manyNeedAttention", "orders need attention");
82
92
  return {
83
93
  id,
84
94
  name,
95
+ spokenLabel: [name, attention, attentionLabel].filter(Boolean).join(", "),
85
96
  initial: initialOf(name),
86
- attention: countLabel(count),
97
+ attention,
87
98
  // Reads as ONE phrase with the count rendered beside it — "3" + "orders need
88
99
  // attention". So the label never repeats the number, or the row would say "1 · 1 order
89
100
  // needs attention".
90
- attentionLabel: count === null
91
- ? copyParam("ownerOverview.unavailable", "Couldn’t read this business")
92
- : count === 0
93
- ? copyParam("ownerOverview.allClear", "All clear")
94
- : count === 1
95
- ? copyParam("ownerOverview.oneNeedsAttention", "order needs attention")
96
- : copyParam("ownerOverview.manyNeedAttention", "orders need attention"),
101
+ attentionLabel,
97
102
  needsAttention: needs,
98
103
  calm: !needs,
99
104
  };
@@ -179,7 +184,7 @@ async function loadRollup(): Promise<OverviewState> {
179
184
  .filter((r: WixSite) => r?.id)
180
185
  .map((r: WixSite) => ({
181
186
  id: r.id || "",
182
- name: r.displayName || (r as { name?: string }).name || r.id || "Site",
187
+ name: r.displayName || (r as { name?: string }).name || copyParam("ownerOverview.unnamedBusiness", "Business"),
183
188
  }));
184
189
  if (businesses.length === 0) return shape({ status: "ready", rows: [] });
185
190
 
@@ -13,6 +13,7 @@ import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
13
13
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
14
14
  import { getAll, upsert, type DbEntity } from "../_shared/owner-local-db.js";
15
15
  import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
16
+ import { readSelectedSite } from "../_shared/owner-selected-site.js";
16
17
 
17
18
  // Offline-first (data-offline): the owner's last-synced businesses are the
18
19
  // READ-THROUGH cache for the `sites` entity — paint them instantly (works
@@ -33,6 +34,8 @@ interface SiteRow {
33
34
  id: string; // metaSiteId
34
35
  name: string;
35
36
  meta: string;
37
+ /** A complete, useful spoken name for the row's tap target. */
38
+ spokenLabel: string;
36
39
  initial: string;
37
40
  }
38
41
 
@@ -55,12 +58,13 @@ interface SitesState {
55
58
  }
56
59
 
57
60
  function normalize(raw: WixSite): SiteRow {
58
- const name = raw.displayName || (raw as { name?: string }).name || raw.id || "Site";
61
+ const name = raw.displayName || (raw as { name?: string }).name || copyParam("ownerSites.unnamedBusiness", "Business");
59
62
  const studio = (raw as { namespace?: string }).namespace === "WIX_STUDIO";
60
63
  return {
61
64
  id: raw.id || "",
62
65
  name,
63
66
  meta: studio ? "Wix Studio site" : "Wix site",
67
+ spokenLabel: [name, studio ? "Wix Studio site" : "Wix site"].join(", "),
64
68
  initial: (name.trim()[0] || "S").toUpperCase(),
65
69
  };
66
70
  }
@@ -70,6 +74,30 @@ function applySearch(rows: SiteRow[], query: string): SiteRow[] {
70
74
  return q ? rows.filter((r) => r.name.toLowerCase().includes(q)) : rows;
71
75
  }
72
76
 
77
+ /** Upgrade rows cached by older bundles, which used the private site id as a fallback name. */
78
+ function repairCachedRow(row: SiteRow, selected: ReturnType<typeof readSelectedSite>): SiteRow {
79
+ // Rows written by this bundle already carry a spoken name, so preserve valid
80
+ // Wix display names even when a business name happens to equal its site ID.
81
+ if (typeof row.spokenLabel === "string" && row.spokenLabel) return row;
82
+ // A legacy cache has no provenance bit. For the currently selected business,
83
+ // the picker kept the Wix display name separately; use it to distinguish a
84
+ // legitimate same-as-ID name from the old private-ID fallback.
85
+ const selectedName = selected?.id === row.id
86
+ && (selected.name !== row.id || selected.nameFromWix === true)
87
+ ? selected.name : "";
88
+ const name = selectedName || (row.name && row.name !== row.id
89
+ ? row.name
90
+ : copyParam("ownerSites.unnamedBusiness", "Business"));
91
+ const meta = typeof row.meta === "string" ? row.meta : "";
92
+ return {
93
+ ...row,
94
+ name,
95
+ meta,
96
+ spokenLabel: [name, ...meta.split(" · ")].filter(Boolean).join(", "),
97
+ initial: (name.trim()[0] || "S").toUpperCase(),
98
+ };
99
+ }
100
+
73
101
  function emptyFor(query = ""): string {
74
102
  return query.trim()
75
103
  ? copyParam("ownerSites.emptySearch", "No businesses match that search.")
@@ -106,6 +134,7 @@ function demoList(s: SitesState): SitesState {
106
134
  id: d.id,
107
135
  name: d.name,
108
136
  meta: [d.domain, d.plan].filter(Boolean).join(" · "),
137
+ spokenLabel: [d.name, d.domain, d.plan].filter(Boolean).join(", "),
109
138
  initial: (d.name[0] || "S").toUpperCase(),
110
139
  }));
111
140
  return shape({
@@ -122,8 +151,10 @@ async function loadList(s: SitesState): Promise<SitesState> {
122
151
  if (isOwnerPreviewDemo()) return demoList(s);
123
152
  // 1) Instant paint from the on-device cache (offline-safe; empty on first-ever run).
124
153
  const cached = await readCachedRows();
125
- const base = cached.length
126
- ? shape({ ...s, status: "ready", allSites: cached, sites: applySearch(cached, s.searchQuery), error: "", demoNote: "" })
154
+ const selected = readSelectedSite();
155
+ const repairedCache = cached.map((row) => repairCachedRow(row, selected));
156
+ const base = repairedCache.length
157
+ ? shape({ ...s, status: "ready", allSites: repairedCache, sites: applySearch(repairedCache, s.searchQuery), error: "", demoNote: "" })
127
158
  : s;
128
159
  // 2) Revalidate from Wix and refresh the cache.
129
160
  try {
@@ -133,7 +164,7 @@ async function loadList(s: SitesState): Promise<SitesState> {
133
164
  return shape({ ...s, status: "ready", allSites: rows, sites: applySearch(rows, s.searchQuery), resultSummary: `${rows.length} ${rows.length === 1 ? "business" : "businesses"}`, error: "", demoNote: "" });
134
165
  } catch (e) {
135
166
  // Offline / fetch failure: keep the last-synced cache BEFORE the demo/error fallbacks.
136
- if (cached.length) return base;
167
+ if (repairedCache.length) return base;
137
168
  if (isBridgeUnavailable(e)) return demoList(s);
138
169
  return shape({ ...s, status: "error", allSites: [], sites: [], error: ownerLoadError(e) });
139
170
  }
@@ -61956,7 +61956,7 @@ const SCREENS = runtimeSetting({
61956
61956
  },
61957
61957
  "pressFeedback": "scale",
61958
61958
  "haptic": "light",
61959
- "accessibilityLabel": "himi_owner_lite.site_row"
61959
+ "accessibilityLabel": "$item.spokenLabel"
61960
61960
  }
61961
61961
  }
61962
61962
  ]
@@ -62516,7 +62516,7 @@ const SCREENS = runtimeSetting({
62516
62516
  },
62517
62517
  "pressFeedback": "scale",
62518
62518
  "haptic": "light",
62519
- "accessibilityLabel": "himi_owner_lite.overview_row"
62519
+ "accessibilityLabel": "$item.spokenLabel"
62520
62520
  }
62521
62521
  }
62522
62522
  ]