@wix/himalaya-cli 0.1559.0 → 0.1560.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
@@ -23086,9 +23086,9 @@ function inferScreenDisplayName(screenId) {
23086
23086
  const suffix = id.slice(product2[0].length).replace(/^_/, "");
23087
23087
  return `${kind}Product Details${suffix ? ` (${suffix})` : ""}`;
23088
23088
  }
23089
- const opaque = /(?:_|\/)([0-9a-f]{8}-[0-9a-f-]{27,}|[0-9]+|[0-9a-f]{16,})$/i.exec(id);
23090
- const words = (opaque ? id.slice(0, opaque.index) : id).replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[_\/\s-]+/).filter(Boolean).map((word) => /^(cms|faq|api|ai|id|url)$/i.test(word) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
23091
- return `${words || screenId}${opaque ? ` (${opaque[1]})` : ""}`;
23089
+ const opaque2 = /(?:_|\/)([0-9a-f]{8}-[0-9a-f-]{27,}|[0-9]+|[0-9a-f]{16,})$/i.exec(id);
23090
+ const words = (opaque2 ? id.slice(0, opaque2.index) : id).replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[_\/\s-]+/).filter(Boolean).map((word) => /^(cms|faq|api|ai|id|url)$/i.test(word) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
23091
+ return `${words || screenId}${opaque2 ? ` (${opaque2[1]})` : ""}`;
23092
23092
  }
23093
23093
  function screenDisplayName(screenId, displayName3, title, navigationLabel) {
23094
23094
  const literal = (value2) => typeof value2 === "string" && !!value2.trim() && !value2.trim().startsWith("$") && !/^__[A-Z0-9_]+__$/.test(value2.trim());
@@ -143328,7 +143328,7 @@ Transport: same targets/tokens as himi-serve \u2014 --target <local|prod|bo>, --
143328
143328
  --telemetry-url, --control-token/--telemetry-token, or HIMI_* env.
143329
143329
  Guides: ${DEVELOPER_PORTAL_URL}`;
143330
143330
  COMMANDS = [
143331
- { name: "one-app", summary: "collect One App source configuration and evidence for reconstruction; validate-topbar checks authored settings and resolved platform bars; validate-design compares the source theme's colors and button shape with the migrated tokens.json; coverage inventories and verifies source requirements. Branded falls back to Free/Spaces only when absent. Collection does not launch a model or build the app. User-scoped Wix authentication is required; WIX_API_KEY is not used.", required: "migrate --msid <uuid> [--app-instance branded|free] [--prompt <text> | --prompt-file <file>] [--name <slug>] [--dir <parent>] [--target bo] [--top-tabs bottom-tabs|keep] [--json] | validate-topbar --evidence-dir <.himi/one-app> --content <app-dir> [--json] | validate-design --evidence-dir <.himi/one-app> --content <app-dir> [--json] [--fix] [--record --release-hash <sha256>] | coverage init|sync|check|verify --evidence-dir <.himi/one-app> --content <app-dir> [--release-hash <sha256>]" },
143331
+ { name: "one-app", summary: "collect One App source configuration and evidence for reconstruction; validate-topbar checks authored settings and resolved platform bars; validate-design compares the source theme's colors, heading/body font families, button shape and accent text contrast with the migrated tokens.json; coverage inventories and verifies source requirements. Branded falls back to Free/Spaces only when absent. Collection does not launch a model or build the app. User-scoped Wix authentication is required; WIX_API_KEY is not used.", required: "migrate --msid <uuid> [--app-instance branded|free] [--prompt <text> | --prompt-file <file>] [--name <slug>] [--dir <parent>] [--target bo] [--top-tabs bottom-tabs|keep] [--json] | validate-topbar --evidence-dir <.himi/one-app> --content <app-dir> [--json] | validate-design --evidence-dir <.himi/one-app> --content <app-dir> [--json] [--fix] [--record --release-hash <sha256>] | coverage init|sync|check|verify --evidence-dir <.himi/one-app> --content <app-dir> [--release-hash <sha256>]" },
143332
143332
  { name: "hhp", summary: "author responsive HTML/CSS/TypeScript screens compiled to native SDUI and T5 workers; scaffold, inspect the app profile, lint or compile declared sources", required: "init <screen> | profile | lint | compile [--app <name>|--content <dir>] [--json] [--dry-run]" },
143333
143333
  { name: "captcha", summary: "create and store native CAPTCHA keys in Serve, or inspect setup status; customer setup verifies Wix site ownership; does not enable member CAPTCHA SDKs", required: "setup|status --app <app> [--content <dir>] [--site <msid>] [--ios-bundle-id <id>] [--android-package-name <id>] [--target <t>]" },
143334
143334
  { name: "init", summary: "scaffold a starter content package (OTA-only; no native shells). --template <t> starts from a full reference app (branded-lite = consumer storefront over a Wix site; owner-lite = owner CRUD/management) instead of the minimal counter; --templates lists bundled ones; --remote fetches the template off the serve plane (falls back to the bundled copy offline). The new app also gets its agent files (the same set `himi agent-init` writes: AGENTS.md, CLAUDE.md, the himalaya skill, HIMALAYA.md, a SessionStart hook) so the next session started inside it picks the work up; --no-agent skips them. It also makes the build board (.himi/studio/, exactly as `himi studio --no-open` does \u2014 it never opens a browser), so every later render, test, validate, push and progress entry refreshes it; and, for a template bundled with this CLI, it makes the FIRST PICTURE itself (never for a --remote one: that is its publisher's code, and a render runs it on this machine, so next[] leaves the render to the agent after it has read the code): exactly `himi render <homeScreenId> --png --mocks --device iphone` (offline \u2014 no request; never --live; --mocks loads the app's recorded data when it has any), run as a child it kills after a time budget, writing .himi/shots/<home>.png and journaling it (by: himi) as the STARTING POINT (\"Starting point: the <template> template, before any of your changes\") so the board and the journal hold a picture when init returns. init also records the source it wrote (.himi/init-source.json: a content fingerprint, Markdown left out), and the first home PNG render after the source diverges from it is journaled once as \"First look at your app\" \u2014 reported as `firstLook: {image}` or `{skipped: <reason>}`, never a failed init; `--no-first-look` skips it (for scripts and CI that render themselves). For a bundled template (never a --remote one \u2014 the preview would build and serve its publisher's code) init also STARTS the live preview itself: `himi run --watch --content <dir> --port <p> --no-open --idle-exit 30`, detached in its own process group with its output in .himi/watch.log, never waited for (the url is chosen before it builds), recorded in .himi/watch.json, and reported as `watch: {started, pid, url, board, log}` \u2014 or `{skipped: <reason>}` for a `CI` environment, a test runner, a watch already running, or a start that failed (never a failed init). Nothing else skips it: outside CI it is started, and `watch.note` says an opt-out was ignored. The envelope LEADS with `doNow`: three lines to run now (the consent preflight, the live preview \u2014 its url and board when init started it, else `himi run --watch --content <dir> &` with the board's path \u2014 and the postcard line), and ENDS with the same lines as `doNowAgain`, so an agent that reads only the head or only the tail (`| tail`) gets them; at a terminal they are also printed as plain text on stderr, before the JSON and again after it. Its `next[]` LEADS with the steps for THIS session, in this order for every app \u2014 the offline first render and that picture's `himi progress` postcard (only when init could not make the picture), the skill's consent preflight (authorize or mock any Wix or external startup request; with none, go straight on), and only then the live preview (the running one's url, or `himi run --watch &` when init did not start it) \u2014 ahead of the next-session guidance", required: "<name> [--dir] [--template <t>] [--remote] [--base44-app-id <id>] [--base44-app-url <https://tenant>] [--no-agent] [--no-first-look] | --templates" },
@@ -157546,11 +157546,11 @@ function deriveAccentText(accent, surfaces) {
157546
157546
  const all = (typeof surfaces === "string" ? [surfaces] : [...surfaces]).map((s2) => s2.slice(0, 7));
157547
157547
  const page = all[0];
157548
157548
  const worst2 = (color) => Math.min(...all.map((s2) => contrastRatio4(color, s2)));
157549
- const opaque = accent.length === 9 ? mix2(accent.slice(0, 7), page, channel2(accent, 7) / 255) : accent;
157550
- if (worst2(opaque) >= ACCENT_TEXT_MIN_CONTRAST) return opaque;
157549
+ const opaque2 = accent.length === 9 ? mix2(accent.slice(0, 7), page, channel2(accent, 7) / 255) : accent;
157550
+ if (worst2(opaque2) >= ACCENT_TEXT_MIN_CONTRAST) return opaque2;
157551
157551
  const pole = worst2("#FFFFFF") >= worst2("#000000") ? "#FFFFFF" : "#000000";
157552
157552
  for (let step = 1; step <= 100; step++) {
157553
- const candidate = mix2(pole, opaque, step / 100);
157553
+ const candidate = mix2(pole, opaque2, step / 100);
157554
157554
  if (worst2(candidate) >= ACCENT_TEXT_MIN_CONTRAST) return candidate;
157555
157555
  }
157556
157556
  if (all.length > 1) {
@@ -171783,6 +171783,14 @@ var init_graph = __esm({
171783
171783
  });
171784
171784
 
171785
171785
  // src/one-app/design-rules.ts
171786
+ function typographyRole(name) {
171787
+ return /(title|heading|headline|display|hero)/i.test(name) ? "heading" : "body";
171788
+ }
171789
+ function sourceFontFamily(id, role) {
171790
+ if (id === "madefor") return role === "heading" ? "Wix Madefor Display" : "Wix Madefor Text";
171791
+ if (id === "system") return SYSTEM_FONT_FAMILY;
171792
+ return Object.hasOwn(SOURCE_FONT_FAMILIES, id) ? SOURCE_FONT_FAMILIES[id] : void 0;
171793
+ }
171786
171794
  function sourceMode(manifest2, raw) {
171787
171795
  const resolved2 = obj3(manifest2.source)?.resolvedAppInstance;
171788
171796
  if (resolved2 === "branded") return "branded";
@@ -171829,6 +171837,15 @@ function sourceDesign(raw, mode) {
171829
171837
  if (mode === "branded") {
171830
171838
  add("colors.surfacePrimary.light", "/settings/theme/colors/background", "color", colors.background);
171831
171839
  add("colors.textPrimary.light", "/settings/theme/colors/content", "color", colors.content);
171840
+ const primary = fields.find((field) => field.field === "colors.accentPrimary.light");
171841
+ if (primary) {
171842
+ add("colors.textOnAccent.light", primary.pointer, "contrast", String(primary.value));
171843
+ add("colors.accentText.light", primary.pointer, "contrast", String(primary.value));
171844
+ }
171845
+ const typography = obj3(theme.typography) ?? {};
171846
+ for (const role of ["heading", "body"]) {
171847
+ add(`typography.${role}.family`, `/settings/theme/typography/${role}`, "font", typography[role]);
171848
+ }
171832
171849
  const button = obj3(theme.button) ?? {};
171833
171850
  for (const variant of BUTTON_VARIANTS2) {
171834
171851
  const style = obj3(button[variant]);
@@ -171848,9 +171865,33 @@ function sourceDesign(raw, mode) {
171848
171865
  leafPointers(theme, "/settings/theme", all);
171849
171866
  return { mode, fields, notCompared: all.filter((pointer) => !read.has(pointer)).sort() };
171850
171867
  }
171851
- var BUTTON_VARIANTS2, obj3, seg;
171868
+ var SOURCE_FONT_FAMILIES, BUTTON_VARIANTS2, obj3, seg;
171852
171869
  var init_design_rules = __esm({
171853
171870
  "src/one-app/design-rules.ts"() {
171871
+ init_tokens();
171872
+ SOURCE_FONT_FAMILIES = {
171873
+ lato: "Lato",
171874
+ poppins: "Poppins",
171875
+ notoSans: "Noto Sans",
171876
+ inconsolata: "Inconsolata",
171877
+ vollkorn: "Vollkorn",
171878
+ playfairDisplay: "Playfair Display",
171879
+ bebasNeue: "Bebas Neue",
171880
+ lobster: "Lobster",
171881
+ brygada1918: "Brygada 1918",
171882
+ spaceGrotesk: "Space Grotesk",
171883
+ rubik: "Rubik",
171884
+ caudex: "Caudex",
171885
+ fahkwang: "Fahkwang",
171886
+ forum: "Forum",
171887
+ leagueSpartan: "League Spartan",
171888
+ marcellus: "Marcellus",
171889
+ marmelad: "Marmelad",
171890
+ pacifico: "Pacifico",
171891
+ questrial: "Questrial",
171892
+ sora: "Sora",
171893
+ workSans: "Work Sans"
171894
+ };
171854
171895
  BUTTON_VARIANTS2 = ["primary", "secondary"];
171855
171896
  obj3 = (value2) => value2 !== null && typeof value2 === "object" && !Array.isArray(value2) ? value2 : void 0;
171856
171897
  seg = (key3) => key3.replaceAll("~", "~0").replaceAll("/", "~1");
@@ -172562,12 +172603,59 @@ function readButtonToken(tokens2, variant, property2) {
172562
172603
  const values = new Set(drawn.map((entry) => entry.radius));
172563
172604
  return values.size === 1 ? { value: drawn[0].radius, origin } : { value: drawn.map((entry) => `${entry.size} ${entry.radius}`).join(", "), origin };
172564
172605
  }
172606
+ function opaque(value2) {
172607
+ const hex2 = value2 ? canonicalHex(value2) : void 0;
172608
+ return hex2?.length === 7 ? hex2 : hex2?.endsWith("FF") ? hex2.slice(0, 7) : void 0;
172609
+ }
172610
+ function contrastRow(field, tokens2, base) {
172611
+ const accent = opaque(readColorToken(tokens2, "accentPrimary").value);
172612
+ const isFill = field.field === "colors.textOnAccent.light";
172613
+ const background = isFill ? accent : opaque(readColorToken(tokens2, "surfacePrimary").value);
172614
+ if (!accent || !background) return { ...base, status: "unresolved", note: !accent ? "Accent contrast requires an opaque target accentPrimary token." : "Author an opaque surfacePrimary token before checking accent text; native hosts do not share the web's white page fallback." };
172615
+ const name = isFill ? "textOnAccent" : "accentText";
172616
+ const read = readColorToken(tokens2, name);
172617
+ const target = opaque(read.value ?? (isFill ? "#FFFFFF" : accent));
172618
+ const neutral = isFill ? [] : (textSurfacesOf(obj5(tokens2.colors) ?? {}).light ?? []).map((value2) => opaque(value2));
172619
+ if (neutral.some((value2) => !value2)) return { ...base, status: "unresolved", note: "Accent text contrast requires opaque neutral surface tokens." };
172620
+ const surfaces = [background, ...neutral];
172621
+ const desired = isFill ? contrastRatio3("#FFFFFF", accent) >= 4.5 ? "#FFFFFF" : "#000000" : deriveAccentText(accent, surfaces);
172622
+ const ratio = target ? Math.min(...surfaces.map((surface) => contrastRatio3(target, surface))) : 0;
172623
+ if (ratio < 4.5 && surfaces.some((surface) => contrastRatio3(desired, surface) < 4.5)) {
172624
+ return { ...base, status: "unresolved", note: "No single derived accent text shade reaches 4.5:1 on every neutral surface; author separate local text roles for the conflicting backdrops." };
172625
+ }
172626
+ return {
172627
+ ...base,
172628
+ source: desired,
172629
+ ...target ? { target } : {},
172630
+ targetOrigin: read.origin === "absent" ? "renderer-default" : read.origin,
172631
+ status: target && ratio >= 4.5 ? "match" : "mismatch",
172632
+ note: `Accent text must reach 4.5:1; observed ${ratio.toFixed(2)}:1.`
172633
+ };
172634
+ }
172565
172635
  function compare3(field, tokens2) {
172566
172636
  const id = requirementId("design", field.pointer, field.field);
172567
172637
  const base = { id, field: field.field, sourcePath: field.pointer, source: field.value };
172568
172638
  if (field.kind === "unsupported") {
172569
172639
  return { ...base, status: "unresolved", note: field.note };
172570
172640
  }
172641
+ if (field.kind === "contrast") return contrastRow(field, tokens2, { ...base, status: "mismatch" });
172642
+ if (field.kind === "font") {
172643
+ const role = field.field.split(".")[1];
172644
+ const mapped = sourceFontFamily(String(field.value), role);
172645
+ const family = mapped === SYSTEM_FONT_FAMILY ? mapped : mapped ? canonicalFamilyName(mapped) : void 0;
172646
+ const entries = Object.entries(obj5(tokens2.typography) ?? {}).filter(([name]) => typographyRole(name) === role);
172647
+ if (!family || !entries.length || entries.some(([, entry]) => !obj5(entry))) {
172648
+ return { ...base, status: "unresolved", note: !family ? mapped ? `Source font ${mapped} is not in the Himi font catalog; resolve a supported family before importing it.` : "Unknown source font ID; resolve its family before importing it." : "No inspectable target typography entries for this role; author numeric metrics before importing the family." };
172649
+ }
172650
+ const wrong = entries.filter(([, entry]) => obj5(entry)?.family !== family);
172651
+ return {
172652
+ ...base,
172653
+ source: family,
172654
+ target: wrong.length ? wrong.map(([name, entry]) => `${name}: ${obj5(entry)?.family ?? "system"}`).join(", ") : family,
172655
+ targetOrigin: "token",
172656
+ status: wrong.length ? "mismatch" : "match"
172657
+ };
172658
+ }
172571
172659
  const parts = field.field.split(".");
172572
172660
  const read = field.kind === "color" ? readColorToken(tokens2, parts[1]) : readButtonToken(tokens2, parts[3], parts[4]);
172573
172661
  const row2 = { ...base, status: "mismatch", targetOrigin: read.origin, ...read.value !== void 0 ? { target: read.value } : {} };
@@ -172618,6 +172706,10 @@ function applyFix(tokens2, rows) {
172618
172706
  const previous = typeof existing === "string" ? existing : obj5(existing)?.light;
172619
172707
  const entry = obj5(existing) ?? {};
172620
172708
  colors[parts[1]] = typeof previous === "string" ? { ...entry, light: row2.source, dark: entry.dark ?? previous, highContrast: entry.highContrast ?? entry.dark ?? previous } : { ...entry, light: row2.source };
172709
+ } else if (parts[0] === "typography") {
172710
+ for (const [name, entry] of Object.entries(obj5(tokens2.typography) ?? {})) {
172711
+ if (typographyRole(name) === parts[1] && obj5(entry)) entry.family = row2.source;
172712
+ }
172621
172713
  } else {
172622
172714
  const variant = ensure2(ensure2(ensure2(ensure2(tokens2, "controls"), "button"), "variants"), parts[3]);
172623
172715
  variant[parts[4]] = row2.source;
@@ -172809,7 +172901,7 @@ function validateDesign(evidenceDir, contentDir2, options = {}) {
172809
172901
  const adapted = waivers(contentDir2, hashes);
172810
172902
  const evaluate2 = () => design.fields.map((field) => {
172811
172903
  const row2 = compare3(field, tokens2);
172812
- const waiver = adapted.get(row2.id);
172904
+ const waiver = field.kind === "contrast" || field.kind === "font" && row2.status === "unresolved" ? void 0 : adapted.get(row2.id);
172813
172905
  if (row2.status === "unresolved") return waiver ? { ...row2, status: "adapted" } : row2;
172814
172906
  let overlayDifference;
172815
172907
  let waivedOverlay = false;
@@ -172819,7 +172911,7 @@ function validateDesign(evidenceDir, contentDir2, options = {}) {
172819
172911
  if (!overlay) continue;
172820
172912
  const drawn = compare3(field, mergeIdiomOverlay(tokens2, overlay));
172821
172913
  if (drawn.status === "match") continue;
172822
- if (waiver?.has(name)) {
172914
+ if (waiver?.has(name) && !(field.kind === "font" && drawn.status === "unresolved")) {
172823
172915
  waivedOverlay = true;
172824
172916
  continue;
172825
172917
  }
@@ -172839,7 +172931,34 @@ function validateDesign(evidenceDir, contentDir2, options = {}) {
172839
172931
  let rows = evaluate2();
172840
172932
  let fixed = [];
172841
172933
  if (options.fix) {
172842
- fixed = applyFix(tokens2, rows);
172934
+ if (design.fields.some((field) => field.kind === "contrast") && !opaque(readColorToken(tokens2, "accentPrimary").value)) {
172935
+ return {
172936
+ ok: false,
172937
+ code: "design_unresolved",
172938
+ ...base,
172939
+ rows,
172940
+ fixed: [],
172941
+ unresolved: ["Author an opaque target accentPrimary palette before --fix; its existing dark/high-contrast appearance cannot be preserved from a missing or translucent accent."]
172942
+ };
172943
+ }
172944
+ const previousColors = structuredClone(obj5(tokens2.colors) ?? {});
172945
+ const contrastFields = new Set(design.fields.filter((field) => field.kind === "contrast").map((field) => field.field));
172946
+ fixed = applyFix(tokens2, rows.filter((row2) => !contrastFields.has(row2.field)));
172947
+ const contrastRows = evaluate2().filter((row2) => contrastFields.has(row2.field));
172948
+ for (const row2 of contrastRows) {
172949
+ const name = row2.field.split(".")[1];
172950
+ if (row2.status !== "mismatch" || row2.targetOrigin === "idiom-token" || previousColors[name] !== void 0) continue;
172951
+ const fallback = name === "textOnAccent" ? "#FFFFFF" : previousColors.accentPrimary;
172952
+ const prior = typeof fallback === "string" ? { light: fallback } : obj5(fallback);
172953
+ if (prior && typeof prior.light === "string") {
172954
+ tokens2.colors[name] = {
172955
+ light: prior.light,
172956
+ dark: prior.dark ?? prior.light,
172957
+ highContrast: prior.highContrast ?? prior.dark ?? prior.light
172958
+ };
172959
+ }
172960
+ }
172961
+ fixed.push(...applyFix(tokens2, contrastRows));
172843
172962
  if (fixed.length) {
172844
172963
  try {
172845
172964
  writeFixedTokens(contentDir2, location, tokensPath, tokens2);
@@ -172855,9 +172974,10 @@ function validateDesign(evidenceDir, contentDir2, options = {}) {
172855
172974
  }
172856
172975
  }
172857
172976
  const mismatches = rows.filter((row2) => row2.status === "mismatch");
172977
+ const unresolvedRequired = rows.some((row2) => row2.status === "unresolved" && design.fields.some((field) => (field.kind === "contrast" || field.kind === "font") && field.field === row2.field));
172858
172978
  return {
172859
- ok: mismatches.length === 0,
172860
- ...mismatches.length ? { code: "design_mismatch" } : {},
172979
+ ok: mismatches.length === 0 && !unresolvedRequired,
172980
+ ...mismatches.length ? { code: "design_mismatch" } : unresolvedRequired ? { code: "design_unresolved" } : {},
172861
172981
  ...base,
172862
172982
  rows,
172863
172983
  mismatches: mismatches.map((row2) => `${row2.field}: source ${row2.source} vs target ${row2.target ?? "absent"}`),
@@ -172986,10 +173106,12 @@ var init_validate_design = __esm({
172986
173106
  init_token_colors();
172987
173107
  init_token_lint();
172988
173108
  init_tokens();
173109
+ init_catalog2();
172989
173110
  init_idiom();
172990
173111
  init_preview_surfaces();
172991
173112
  init_config_source_edit();
172992
173113
  init_coverage2();
173114
+ init_brand_color_math();
172993
173115
  init_design_rules();
172994
173116
  init_progress();
172995
173117
  USAGE3 = "himi one-app validate-design --evidence-dir <.himi/one-app> --content <app-dir> [--json] [--fix] [--record --release-hash <sha256>]";
@@ -193078,10 +193200,10 @@ function tokensFromWst(wst) {
193078
193200
  const cream = accent2 && isNearWhite(accent2) ? accent2 : void 0;
193079
193201
  const surface2 = base2 && !isNearBlack(base2) ? base2 : cream;
193080
193202
  if (surface2) colors.surfaceSecondary = { light: surface2 };
193081
- const opaque = (c2) => c2 && c2.length === 7 ? c2 : void 0;
193082
- const surface = opaque(usableCssColor(wst["--wst-color-fill-base-1"]));
193083
- const ramp = !surface ? [] : ["--wst-color-fill-base-shade-1", "--wst-color-fill-base-shade-2", "--wst-color-fill-base-shade-3"].map((v2) => opaque(usableCssColor(wst[v2]))).filter((c2) => !!c2).map((c2) => ({ c: c2, ratio: contrastRatio3(c2, surface) }));
193084
- const primary = surface ? opaque(usableCssColor(wst["--wst-color-text-primary"])) : void 0;
193203
+ const opaque2 = (c2) => c2 && c2.length === 7 ? c2 : void 0;
193204
+ const surface = opaque2(usableCssColor(wst["--wst-color-fill-base-1"]));
193205
+ const ramp = !surface ? [] : ["--wst-color-fill-base-shade-1", "--wst-color-fill-base-shade-2", "--wst-color-fill-base-shade-3"].map((v2) => opaque2(usableCssColor(wst[v2]))).filter((c2) => !!c2).map((c2) => ({ c: c2, ratio: contrastRatio3(c2, surface) }));
193206
+ const primary = surface ? opaque2(usableCssColor(wst["--wst-color-text-primary"])) : void 0;
193085
193207
  const primaryRatio = primary && surface ? contrastRatio3(primary, surface) : void 0;
193086
193208
  const tertiary = primaryRatio === void 0 ? void 0 : ramp.filter((x2) => x2.ratio >= 4.5 && x2.ratio < primaryRatio).sort((a2, b2) => a2.ratio - b2.ratio)[0];
193087
193209
  if (tertiary) colors.textTertiary = { light: tertiary.c };
@@ -198899,7 +199021,7 @@ async function runOneAppCommand(argv, io2, deps = {}, commandIncluded = false) {
198899
199021
 
198900
199022
  Collect the source mobile app and hand it to the current coding agent. The agent continues with a faithful plan, local generation and a checked hosted draft. Branded falls back to Free (Spaces) only when absent. This command itself does not launch AI or build an app. --dir is the parent directory; an existing destination is refused. --top-tabs bottom-tabs (default) plans bottom tabs, one per visible page, for a source with top tabs; keep retains the top tabs.
198901
199023
 
198902
- validate-topbar checks authored source top-bar controls against the migrated config.ts. validate-design compares the source theme's colors and button shape with the migrated tokens.json, repairs the mismatches with --fix, and records a passing check for the coverage ledger with --record. Both are scoped to a One App evidence directory and do not change general himi validate behavior.`);
199024
+ validate-topbar checks authored source top-bar controls against the migrated config.ts. validate-design compares the source theme's colors, heading/body font families, button shape and accent text contrast with the migrated tokens.json, repairs the mismatches with --fix, and records a passing check for the coverage ledger with --record. Both are scoped to a One App evidence directory and do not change general himi validate behavior.`);
198903
199025
  return 0;
198904
199026
  }
198905
199027
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
@@ -163,20 +163,45 @@ Apply these interpretation rules:
163
163
  a feature to account for; they do not alone prove visibility or access for every
164
164
  visitor or member. Resolve those rules separately. This check is specific to this
165
165
  migration workflow; it does not change the behavior of `himi validate`.
166
- - Before publishing, run the migration-only design check:
166
+ - Apply the collected app theme before the first build: run
167
+ `himi one-app validate-design --evidence-dir .himi/one-app --content . --fix`,
168
+ resolve any remaining mismatches/unresolved values, then rebuild. The source is
169
+ `settings.theme`, not the website or the starter template. Before publishing,
170
+ run the migration-only design check:
167
171
  `himi one-app validate-design --evidence-dir .himi/one-app --content .`.
168
172
  It resolves the effective source theme with the reader the source app instance
169
- actually uses, then compares the resolved primary, background and content colors
170
- and each button's `borderRadius` and `outlineWidth` against the app's `tokens.json`.
173
+ actually uses, then compares the resolved primary, background and content colors,
174
+ each button's `borderRadius` and `outlineWidth`, and resolved heading/body font families
175
+ against the app's `tokens.json`.
171
176
  Hex case and shorthand are normalized; an absent button token is compared with the
172
177
  value the renderer draws (an outline only on `secondary`; a radius of `radius.pill`,
173
178
  else 24), not called a difference. A variant without its own radius must draw the
174
179
  source radius at every Button size. A descriptor's per-button `cornerRadius` overrides
175
- the tokens and is not checked. `--fix` writes the mismatching source values into
180
+ the tokens and is not checked; descriptor/control-specific fill and label overrides
181
+ also need a rendered check. `--fix` writes the mismatching source values into
176
182
  `tokens.json` and pins the dark and high-contrast schemes to what they drew before,
177
183
  because a missing scheme falls back to the light value. Rebuild after `--fix`.
184
+ For Branded apps, known source typography IDs (for example `lato`) become family
185
+ names (`Lato`); tokens whose names contain title/heading/headline/display/hero use the heading family; other
186
+ typography tokens use the body family. Numeric metrics stay unchanged. Unknown IDs,
187
+ families outside the Himi font catalog, and missing target typography roles remain unresolved; resolve them before recording
188
+ a pass, even when the coverage ledger cites an adaptation. A deliberate difference
189
+ between resolved font roles can still be adapted. Build/font validation still has
190
+ to prove the font bytes are available. Unresolved fonts and contrast also fail
191
+ the plain check used by push, not only `--record`.
192
+ Branded accent text must reach 4.5:1: `textOnAccent` against the fill, and `accentText` against the target page and declared neutral
193
+ surfaces. `--fix` chooses readable ink and derives a same-hue text shade while
194
+ retaining the exact source accent fill. For example, `#D6BD07` needs dark labels;
195
+ `#7A6C00` is a readable accent text shade on white. Translucent target colors cannot earn
196
+ an opaque contrast claim; an explicitly adapted source color still needs independently
197
+ readable opaque target roles. Author `surfacePrimary` explicitly when absent: native
198
+ hosts do not share the web's white page fallback. Conflicting page/card backdrops with no derived shade
199
+ that clears every surface stay unresolved; use separate local text roles. A missing
200
+ or translucent target accent palette must be authored explicitly before `--fix`,
201
+ because its non-light appearance cannot be preserved safely. Source `system` font
202
+ IDs use the portable `$system` sentinel. Contrast failures cannot be waived by a ledger adaptation.
178
203
  The report names every theme pointer the check did not compare, including typography
179
- kits and any button `uppercase`, which Himi tokens cannot carry; resolve those
204
+ kit IDs and any button `uppercase`, which Himi tokens cannot carry; resolve those
180
205
  explicitly rather than treating a pass as full design parity. A deliberate difference
181
206
  is waived only by a `design` coverage row with status `adapted` that cites the user's
182
207
  instruction and explains the change. This check compares values, not rendered screens.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1559.0",
3
+ "version": "0.1560.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -71,5 +71,5 @@
71
71
  "artifactId": "himalaya-cli"
72
72
  }
73
73
  },
74
- "falconPackageHash": "10b9cfbdf8247c393305b2341b984702640668ff76c32fbc09ae2d8e"
74
+ "falconPackageHash": "4a9c5b9b83751ee0bf714c03ed0abc9d8738a205eed4a022bdec8fcb"
75
75
  }