@wix/himalaya-cli 0.1019.0 → 0.1020.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.
@@ -18,6 +18,8 @@ export interface DesignTokens {
18
18
  motions?: Record<string, MotionToken>;
19
19
  /** RFC 0021 — named surface materials (flat / glass / frosted). */
20
20
  materials?: Record<string, MaterialToken>;
21
+ /** RFC 0059 / #2931 — named surface elevations, paint only. A `Card` wears `card`. */
22
+ elevations?: Record<string, ElevationToken>;
21
23
  /** RFC 0021 (bottom-nav extension) — kit-driven tab bar styling. */
22
24
  tabBar?: TabBarStyle;
23
25
  /** RFC 0010/0021 — kit-driven slide-in drawer styling. */
@@ -264,6 +266,40 @@ export interface ShadowToken {
264
266
  x?: number;
265
267
  y?: number;
266
268
  }
269
+ /**
270
+ * RFC 0059 / #2931 — how raised a surface is, as PAINT: nothing here moves a box, so the
271
+ * structural diff stays blind to it. A `Card` wears the `card` elevation; declare none and every
272
+ * Card stays flat, as before. Each field is drawn the same way on every renderer — what makes a
273
+ * Card read as Material on Android and as iOS on an iPhone is the idiom overlay choosing different
274
+ * fields for the one token:
275
+ *
276
+ * "elevations": { "card": {} },
277
+ * "idiom": {
278
+ * "material": { "elevations": { "card": { "level": 1 } } },
279
+ * "ios": { "elevations": { "card": { "border": { "color": "border" }, "shadow": "card" } } }
280
+ * }
281
+ */
282
+ export interface ElevationToken {
283
+ /** Material 3 tonal elevation, 0–5: the surface is tinted with `tint` at M3's alpha for that
284
+ * level (1 → 5%, 2 → 8%, 3 → 11%, 4 → 12%, 5 → 14% — `TONAL_ELEVATION_ALPHA`). 0 = none. */
285
+ level?: number;
286
+ /** The tonal tint — a colour token, M3's surfaceTint. Default `accentPrimary`. */
287
+ tint?: string;
288
+ /** A `shadows` token drawn behind the surface, in the surface's own shape. */
289
+ shadow?: string;
290
+ /** A hairline drawn just inside the surface's edge, so it never grows the box. `width` in
291
+ * points, default 0.5; 0 = none. */
292
+ border?: {
293
+ color: string;
294
+ width?: number;
295
+ };
296
+ }
297
+ /** M3's tonal-elevation tint alpha by level (0–5), as `surfaceColorAtElevation` computes it for
298
+ * levels 1–5 (1, 3, 6, 8, 12 dp). Mirrored by Tokens.swift and Tokens.kt. */
299
+ export declare const TONAL_ELEVATION_ALPHA: readonly number[];
300
+ /** The tint alpha an `ElevationToken.level` asks for: rounded and clamped to 0–5; anything
301
+ * non-numeric is 0 (no tint). */
302
+ export declare function tonalElevationAlpha(level: number | undefined): number;
267
303
  /** RFC 0020 — a linear gradient. `colors` are color-token NAMES (theme-aware);
268
304
  * `angle` is degrees clockwise from top (0 = top→bottom, 90 = left→right).
269
305
  * `locations` (0..1) optionally positions each stop. */
@@ -7796,6 +7796,11 @@ function resolveResponsiveSize(rs, width) {
7796
7796
  return rs.base;
7797
7797
  }
7798
7798
  var SYSTEM_FONT_FAMILY = "$system";
7799
+ var TONAL_ELEVATION_ALPHA = [0, 0.05, 0.08, 0.11, 0.12, 0.14];
7800
+ function tonalElevationAlpha(level) {
7801
+ if (typeof level !== "number" || !Number.isFinite(level)) return 0;
7802
+ return TONAL_ELEVATION_ALPHA[Math.min(5, Math.max(0, Math.round(level)))];
7803
+ }
7799
7804
  function buildTokenRegistry(tokens) {
7800
7805
  return {
7801
7806
  colors: new Set(Object.keys(tokens.colors ?? {})),
@@ -12975,6 +12980,7 @@ export {
12975
12980
  SURFACE_GROUPS,
12976
12981
  SURFACE_VIEW_LADDER,
12977
12982
  SYSTEM_FONT_FAMILY,
12983
+ TONAL_ELEVATION_ALPHA,
12978
12984
  UNIVERSAL_LAUNCH_NAMESPACES,
12979
12985
  WIDGET_COMPONENTS,
12980
12986
  WIDGET_FAMILIES,
@@ -13047,6 +13053,7 @@ export {
13047
13053
  surfaceFrozenFamily,
13048
13054
  surfaceRenderMode,
13049
13055
  surfacesForApp,
13056
+ tonalElevationAlpha,
13050
13057
  validate,
13051
13058
  validateAll,
13052
13059
  validateDistribution,
package/dist/cli.mjs CHANGED
@@ -5293,6 +5293,10 @@ var init_quickjs_web_globals = __esm({
5293
5293
  });
5294
5294
 
5295
5295
  // ../../core/ts/src/tokens.ts
5296
+ function tonalElevationAlpha(level) {
5297
+ if (typeof level !== "number" || !Number.isFinite(level)) return 0;
5298
+ return TONAL_ELEVATION_ALPHA[Math.min(5, Math.max(0, Math.round(level)))];
5299
+ }
5296
5300
  function buildTokenRegistry(tokens) {
5297
5301
  return {
5298
5302
  colors: new Set(Object.keys(tokens.colors ?? {})),
@@ -5342,11 +5346,12 @@ function walk(node, registry, path2, out) {
5342
5346
  walk(value, registry, `${path2}.${key2}`, out);
5343
5347
  }
5344
5348
  }
5345
- var SYSTEM_FONT_FAMILY;
5349
+ var SYSTEM_FONT_FAMILY, TONAL_ELEVATION_ALPHA;
5346
5350
  var init_tokens = __esm({
5347
5351
  "../../core/ts/src/tokens.ts"() {
5348
5352
  "use strict";
5349
5353
  SYSTEM_FONT_FAMILY = "$system";
5354
+ TONAL_ELEVATION_ALPHA = [0, 0.05, 0.08, 0.11, 0.12, 0.14];
5350
5355
  }
5351
5356
  });
5352
5357
 
@@ -11079,6 +11084,29 @@ function tokenColorIssues(dir, tokensPathOverride) {
11079
11084
  );
11080
11085
  }
11081
11086
  }
11087
+ const shadowNames = (layer) => {
11088
+ const sh = layer?.shadows;
11089
+ return new Set(sh !== null && typeof sh === "object" && !Array.isArray(sh) ? Object.keys(sh) : []);
11090
+ };
11091
+ const checkElevations = (layer, where, declared) => {
11092
+ const els = layer?.elevations;
11093
+ if (els === null || typeof els !== "object" || Array.isArray(els)) return;
11094
+ for (const [name, token] of Object.entries(els)) {
11095
+ const shadow = token?.shadow;
11096
+ if (shadow === void 0 || typeof shadow === "string" && declared.has(shadow)) continue;
11097
+ error(
11098
+ `${tokensPath}: ${where}elevations.${name}.shadow is ${JSON.stringify(shadow)}, which names no \`shadows\` token \u2014 every renderer would paint it with no shadow at all. Declare shadows.${typeof shadow === "string" ? shadow : "<name>"} or name one that exists.`
11099
+ );
11100
+ }
11101
+ };
11102
+ const baseShadows = shadowNames(raw);
11103
+ checkElevations(raw, "", baseShadows);
11104
+ const idiom = raw.idiom;
11105
+ if (idiom !== null && typeof idiom === "object" && !Array.isArray(idiom)) {
11106
+ for (const [overlay, layer] of Object.entries(idiom)) {
11107
+ checkElevations(layer, `idiom.${overlay}.`, /* @__PURE__ */ new Set([...baseShadows, ...shadowNames(layer)]));
11108
+ }
11109
+ }
11082
11110
  const colors = raw.colors;
11083
11111
  if (colors === void 0) return issues;
11084
11112
  if (colors === null || typeof colors !== "object" || Array.isArray(colors)) {
@@ -12844,6 +12872,32 @@ function resolveSchemeColor(o, scheme) {
12844
12872
  if (typeof o?.light === "string") return o.light;
12845
12873
  return typeof o?.dark === "string" ? o.dark : void 0;
12846
12874
  }
12875
+ function mixColor(base, over, alpha2) {
12876
+ const a = hexToRgba(base), b = hexToRgba(over);
12877
+ if (a && b) {
12878
+ const t = alpha2 * b[3];
12879
+ const hex2 = (v) => Math.round(v).toString(16).padStart(2, "0");
12880
+ const rgb = [0, 1, 2].map((i) => hex2(a[i] * (1 - t) + b[i] * t)).join("");
12881
+ return a[3] < 1 ? `#${rgb}${hex2(a[3] * 255)}` : `#${rgb}`;
12882
+ }
12883
+ return `color-mix(in srgb, ${over} ${+(alpha2 * 100).toFixed(2)}%, ${base})`;
12884
+ }
12885
+ function hexToRgba(hex2) {
12886
+ const h = hex2.trim().replace(/^#/, "");
12887
+ if (/^[\da-f]{8}$/i.test(h)) {
12888
+ const rgb2 = hexToRgb2(h.slice(0, 6));
12889
+ return rgb2 ? [...rgb2, parseInt(h.slice(6), 16) / 255] : null;
12890
+ }
12891
+ const rgb = hexToRgb2(h);
12892
+ return rgb ? [...rgb, 1] : null;
12893
+ }
12894
+ function elevationBoxShadow(e) {
12895
+ const parts = [
12896
+ ...e.border && e.border.width > 0 ? [`inset 0 0 0 ${e.border.width}px ${e.border.color}`] : [],
12897
+ ...e.shadow ? [e.shadow] : []
12898
+ ];
12899
+ return parts.length ? parts.join(", ") : void 0;
12900
+ }
12847
12901
  function makeTokenResolvers(source, scheme, idiom = WEB_IDIOM_CONTEXT) {
12848
12902
  const tokens = resolveIdiom(source, idiom);
12849
12903
  const color = (name, fallback = "#1a1a1a") => {
@@ -12934,6 +12988,17 @@ function makeTokenResolvers(source, scheme, idiom = WEB_IDIOM_CONTEXT) {
12934
12988
  }
12935
12989
  return tint ? { background: rgba(tint, m.opacity ?? 1) } : void 0;
12936
12990
  };
12991
+ const elevation = (name) => {
12992
+ const e = name ? tokens.elevations?.[name] : void 0;
12993
+ if (!e) return void 0;
12994
+ const alpha2 = tonalElevationAlpha(e.level);
12995
+ const width = e.border ? typeof e.border.width === "number" && Number.isFinite(e.border.width) ? Math.max(0, e.border.width) : 0.5 : 0;
12996
+ return {
12997
+ ...alpha2 > 0 ? { tint: { color: color(e.tint ?? "accentPrimary", "#6F3BFF"), alpha: alpha2 } } : {},
12998
+ ...e.shadow && shadow(e.shadow) ? { shadow: shadow(e.shadow) } : {},
12999
+ ...e.border && width > 0 ? { border: { color: color(e.border.color, "#E0E0E0"), width } } : {}
13000
+ };
13001
+ };
12937
13002
  const densityScale = (density) => {
12938
13003
  switch (density) {
12939
13004
  case "compact":
@@ -12975,6 +13040,7 @@ function makeTokenResolvers(source, scheme, idiom = WEB_IDIOM_CONTEXT) {
12975
13040
  gradient,
12976
13041
  meshGradient,
12977
13042
  material,
13043
+ elevation,
12978
13044
  densityScale,
12979
13045
  motionTransition,
12980
13046
  appearMotion,
@@ -24959,8 +25025,17 @@ function renderInner(node, ctx) {
24959
25025
  }
24960
25026
  case "Grid":
24961
25027
  return /* @__PURE__ */ jsx6(GridNode, { props, ctx, wrap, gap });
24962
- case "Card":
24963
- return /* @__PURE__ */ jsx6("div", { style: { display: "flex", flexDirection: "column", gap: props.spacing == null ? 8 : ctx.sp(props.spacing), padding: props.padding == null ? 12 : ctx.sp(props.padding), background: C(props.background ?? "surfaceSecondary", "#f5f7fa"), borderRadius: ctx.radius(props.cornerRadius ?? "md", 10), overflow: "hidden", alignSelf: "stretch", ...props.alignment ? { alignItems: props.alignment === "center" ? "center" : props.alignment === "trailing" ? "flex-end" : "flex-start" } : {}, ...wrap }, children: kids(props.children, ctx) });
25028
+ case "Card": {
25029
+ const m = modifiers;
25030
+ const ownSurface = !!(m?.backgroundGradient ?? m?.background_gradient ?? m?.material) || m?.animatedGradient === true || m?.meshGradient === true;
25031
+ const fill = C(props.background ?? "surfaceSecondary", "#f5f7fa");
25032
+ const card = ctx.elevation("card");
25033
+ const elevation = card && ownSurface ? { border: card.border } : card;
25034
+ const wrapShadow = wrap.boxShadow;
25035
+ const explicit = m?.shadow != null || m?.boxShadow != null || m?.box_shadow != null;
25036
+ const elevationShadow = !elevation ? wrapShadow : explicit ? elevationBoxShadow({ border: elevation.border, shadow: wrapShadow }) : [elevationBoxShadow(elevation), wrapShadow].filter(Boolean).join(", ") || void 0;
25037
+ return /* @__PURE__ */ jsx6("div", { style: { display: "flex", flexDirection: "column", gap: props.spacing == null ? 8 : ctx.sp(props.spacing), padding: props.padding == null ? 12 : ctx.sp(props.padding), background: elevation?.tint ? mixColor(fill, elevation.tint.color, elevation.tint.alpha) : fill, borderRadius: ctx.radius(props.cornerRadius ?? "md", 10), overflow: "hidden", alignSelf: "stretch", ...props.alignment ? { alignItems: props.alignment === "center" ? "center" : props.alignment === "trailing" ? "flex-end" : "flex-start" } : {}, ...wrap, ...elevationShadow ? { boxShadow: elevationShadow } : {} }, children: kids(props.children, ctx) });
25038
+ }
24964
25039
  case "SplitView":
24965
25040
  return /* @__PURE__ */ jsx6(SplitViewNode, { props, ctx, wrap });
24966
25041
  case "Divider":
@@ -28894,6 +28969,7 @@ __export(render_descriptor_exports, {
28894
28969
  clientEnvFor: () => clientEnvFor,
28895
28970
  collectNativeOnly: () => collectNativeOnly,
28896
28971
  isMissingPlaywrightExecutable: () => isMissingPlaywrightExecutable,
28972
+ nodeHidden: () => nodeHidden,
28897
28973
  pngAvailable: () => pngAvailable,
28898
28974
  pngPageHtml: () => pngPageHtml,
28899
28975
  renderDescriptorWeb: () => renderDescriptorWeb,
@@ -35064,6 +35140,7 @@ __export(screen_crawl_exports, {
35064
35140
  netTransport: () => netTransport,
35065
35141
  parseWorkerAction: () => parseWorkerAction,
35066
35142
  recordScreenTraffic: () => recordScreenTraffic,
35143
+ renderedNameGaps: () => renderedNameGaps,
35067
35144
  rootOnAppear: () => rootOnAppear,
35068
35145
  snapshot: () => snapshot,
35069
35146
  structuralSignature: () => structuralSignature,
@@ -35493,6 +35570,185 @@ function scanTreeValues(node, hit) {
35493
35570
  }
35494
35571
  node.children?.forEach((child) => scanTreeValues(child, hit));
35495
35572
  }
35573
+ function joinPath(base, rel) {
35574
+ if (!base) return rel;
35575
+ if (!rel) return base;
35576
+ return rel.startsWith("[") ? `${base}${rel}` : `${base}.${rel}`;
35577
+ }
35578
+ function withinPath(p, root) {
35579
+ return !root || p === root || p.startsWith(`${root}.`) || p.startsWith(`${root}[`);
35580
+ }
35581
+ function resolveNameBinding(v, ctx) {
35582
+ if (v.startsWith("$service:")) return ctx.provider.value(v);
35583
+ if (v.startsWith("$item") && ctx.scope) {
35584
+ const key2 = v.replace(/^\$item\.?/, "");
35585
+ return key2 ? pathValue(ctx.scope, key2) : void 0;
35586
+ }
35587
+ return void 0;
35588
+ }
35589
+ function collapsesWhenEmpty(n, path2, ctx) {
35590
+ const wire = !!n.body && typeof n.body === "object" && !Array.isArray(n.body);
35591
+ const body = wire ? n.body : n;
35592
+ if (body.type !== "Text") return false;
35593
+ const props = body.props;
35594
+ if (!props || props.collapseWhenEmpty !== true) return false;
35595
+ const raw = props.text;
35596
+ const bound = typeof raw === "string" && (raw.startsWith("$service:") || raw.startsWith("$item"));
35597
+ if (bound && raw.startsWith("$item") && !ctx.scope) return false;
35598
+ const eff = bound ? resolveNameBinding(raw, ctx) : raw;
35599
+ const s = eff == null ? "" : String(eff).trim();
35600
+ if (s !== "" && s !== "\u2014") return false;
35601
+ if (bound && typeof eff === "string" && s === "") {
35602
+ ctx.emptied.push({ at: joinPath(path2, wire ? "body.props" : "props"), field: "text", binding: raw });
35603
+ }
35604
+ return true;
35605
+ }
35606
+ function resolveGateValue(raw, ctx) {
35607
+ return typeof raw === "string" && (raw.startsWith("$service:") || raw.startsWith("$item")) ? resolveNameBinding(raw, ctx) : raw;
35608
+ }
35609
+ function sheetNotPresented(n, ctx) {
35610
+ const body = n.body && typeof n.body === "object" && !Array.isArray(n.body) ? n.body : n;
35611
+ if (body.type !== "BottomSheet") return false;
35612
+ const props = body.props;
35613
+ if (!props || props.presentedPath == null && props.presented == null) return false;
35614
+ const presented = resolveGateValue(props.presentedPath != null ? props.presentedPath : props.presented, ctx);
35615
+ return presented !== true && presented !== "true";
35616
+ }
35617
+ function selectedFragmentCase(props, ctx) {
35618
+ const cases = Array.isArray(props.cases) ? props.cases : [];
35619
+ if (!cases.length) return null;
35620
+ const casePath = props.casePath ?? props.case_path;
35621
+ if (typeof casePath === "string" && casePath.startsWith("$") && !casePath.startsWith("$service:") && !casePath.startsWith("$item")) return null;
35622
+ const disc = resolveGateValue(casePath, ctx);
35623
+ const wanted = typeof disc === "string" ? disc : void 0;
35624
+ const idOf = (c) => c && typeof c === "object" ? c.id : void 0;
35625
+ let i = cases.findIndex((c) => idOf(c) === wanted);
35626
+ if (i < 0) {
35627
+ const fallback = props.defaultCase ?? props.default_case;
35628
+ i = cases.findIndex((c) => idOf(c) === fallback);
35629
+ }
35630
+ return i < 0 ? 0 : i;
35631
+ }
35632
+ function projectRenderedNames(node, path2, key2, ctx) {
35633
+ if (!node || typeof node !== "object") return node;
35634
+ if (Array.isArray(node)) return node.map((v, i) => projectRenderedNames(v, `${path2}[${i}]`, key2, ctx));
35635
+ if (isActionEnvelope(node) || key2 === "args") return node;
35636
+ const n = node;
35637
+ const rawMods = n.modifiers;
35638
+ const mods = rawMods && typeof rawMods === "object" && !Array.isArray(rawMods) ? rawMods : {};
35639
+ if (mods.accessibilityHidden === true) return node;
35640
+ if (ctx.unrendered.has(path2) || nodeHidden(mods, ctx.provider, ctx.scope, null) || collapsesWhenEmpty(n, path2, ctx) || sheetNotPresented(n, ctx)) {
35641
+ return { ...n, modifiers: { ...mods, accessibilityHidden: true } };
35642
+ }
35643
+ const props = n.props && typeof n.props === "object" && !Array.isArray(n.props) ? n.props : null;
35644
+ if (props && n.type === "FragmentSlot") {
35645
+ const selected = selectedFragmentCase(props, ctx);
35646
+ if (selected !== null) {
35647
+ props.cases.forEach((_, i) => {
35648
+ if (i !== selected) ctx.unrendered.add(joinPath(path2, `props.cases[${i}]`));
35649
+ });
35650
+ }
35651
+ }
35652
+ let boundTemplate = false;
35653
+ if (props && (n.type === "LazyList" || n.type === "Grid")) {
35654
+ const itemsPath = props.itemsPath ?? props.items_path;
35655
+ const template = Array.isArray(props.items) ? props.items[0] : void 0;
35656
+ if (typeof itemsPath === "string" && template !== void 0) {
35657
+ boundTemplate = true;
35658
+ const local = itemsPath.startsWith("$item.");
35659
+ const rows = local ? ctx.scope ? pathValue(ctx.scope, itemsPath.slice("$item.".length)) : void 0 : itemsPath.startsWith("$service:") ? ctx.provider.value(itemsPath) : void 0;
35660
+ if (Array.isArray(rows) || local) {
35661
+ const paintsRows = Array.isArray(rows) && rows.length > 0;
35662
+ props.items.forEach((_, i) => {
35663
+ const at = joinPath(path2, `props.items[${i}]`);
35664
+ if (i === 0 && paintsRows) ctx.rowTemplates.push(at);
35665
+ else ctx.unrendered.add(at);
35666
+ });
35667
+ }
35668
+ if (Array.isArray(rows) && rows.length) {
35669
+ ctx.lists.push({
35670
+ templatePath: joinPath(path2, "props.items[0]"),
35671
+ template,
35672
+ rows: rows.slice(0, A11Y_ROWS_PER_LIST)
35673
+ });
35674
+ }
35675
+ }
35676
+ }
35677
+ const out = {};
35678
+ for (const [k, v] of Object.entries(n)) {
35679
+ if (NAME_KEYS.has(k) && typeof v === "string" && (v.startsWith("$service:") || v.startsWith("$item"))) {
35680
+ const r = resolveNameBinding(v, ctx);
35681
+ if (typeof r === "string") {
35682
+ if (r.trim() === "") ctx.emptied.push({ at: path2, field: k, binding: v });
35683
+ out[k] = r;
35684
+ } else if (typeof r === "number" || typeof r === "boolean") {
35685
+ out[k] = String(r);
35686
+ } else {
35687
+ out[k] = v;
35688
+ }
35689
+ continue;
35690
+ }
35691
+ if (k === "props" && boundTemplate && props) {
35692
+ const p = {};
35693
+ for (const [pk, pv] of Object.entries(props)) {
35694
+ p[pk] = projectRenderedNames(
35695
+ pv,
35696
+ joinPath(path2, `props.${pk}`),
35697
+ pk,
35698
+ pk === "items" ? { ...ctx, scope: void 0 } : ctx
35699
+ );
35700
+ }
35701
+ out[k] = p;
35702
+ continue;
35703
+ }
35704
+ out[k] = projectRenderedNames(v, joinPath(path2, k), k, ctx);
35705
+ }
35706
+ return out;
35707
+ }
35708
+ function renderedNameGaps(descriptor, state) {
35709
+ const provider = new FakeServiceProvider(state);
35710
+ const found = /* @__PURE__ */ new Map();
35711
+ const rowsJudged = /* @__PURE__ */ new Map();
35712
+ const rowTemplateOf = /* @__PURE__ */ new Map();
35713
+ const judge = (root, base, scope, depth, inRow) => {
35714
+ if (inRow) rowsJudged.set(base, (rowsJudged.get(base) ?? 0) + 1);
35715
+ const ctx = { provider, scope, emptied: [], lists: [], unrendered: /* @__PURE__ */ new Set(), rowTemplates: [] };
35716
+ const projected = projectRenderedNames(root, "", "", ctx);
35717
+ for (const g of accessibleNameGaps(projected)) {
35718
+ const rel = g.at === "root" ? "" : g.at;
35719
+ const at = joinPath(base, rel) || "root";
35720
+ if (g.reason !== "interactive" && !CONTROL_ROLES.has(g.role)) continue;
35721
+ if (ctx.rowTemplates.some((t) => withinPath(rel, t))) continue;
35722
+ const cause = ctx.emptied.find((e) => withinPath(e.at, rel));
35723
+ if (!cause) continue;
35724
+ const prev = found.get(at);
35725
+ if (prev) {
35726
+ if (prev.rows && inRow) prev.rows.hit += 1;
35727
+ continue;
35728
+ }
35729
+ if (inRow) rowTemplateOf.set(at, base);
35730
+ found.set(at, {
35731
+ gap: { ...g, at },
35732
+ binding: cause.binding,
35733
+ field: cause.field,
35734
+ ...inRow ? { rows: { hit: 1, of: 0 } } : {}
35735
+ });
35736
+ }
35737
+ if (depth >= A11Y_LIST_DEPTH) return;
35738
+ for (const list of ctx.lists) {
35739
+ list.rows.forEach((el, i) => {
35740
+ const rowScope = el && typeof el === "object" && !Array.isArray(el) ? { ...el, __index: i } : { value: el, __index: i };
35741
+ judge(list.template, joinPath(base, list.templatePath), rowScope, depth + 1, true);
35742
+ });
35743
+ }
35744
+ };
35745
+ judge(descriptor, "", void 0, 0, false);
35746
+ for (const [at, template] of rowTemplateOf) {
35747
+ const rows = found.get(at)?.rows;
35748
+ if (rows) rows.of = rowsJudged.get(template) ?? rows.hit;
35749
+ }
35750
+ return [...found.values()];
35751
+ }
35496
35752
  function collectObservations(descriptorOrPayload, state) {
35497
35753
  const out = [];
35498
35754
  const walk2 = (node) => {
@@ -35922,6 +36178,21 @@ async function crawlScreenInner(opts) {
35922
36178
  });
35923
36179
  });
35924
36180
  }
36181
+ for (const r of renderedNameGaps(descriptor, state)) {
36182
+ findings.push({
36183
+ screen: screenId,
36184
+ trigger,
36185
+ invariant: "unnamed-control",
36186
+ severity: "error",
36187
+ // Stable across states on purpose: the row count lives in `actual`, so the same control
36188
+ // seen empty after boot and after a tap dedupes to one finding.
36189
+ message: `${r.gap.type} renders no accessible name: its only name, ${r.binding}, resolved to "" \u2014 a screen reader announces an unnamed \`${r.gap.role}\``,
36190
+ at: r.gap.at,
36191
+ field: r.field,
36192
+ actual: r.rows ? `"" on ${r.rows.hit} of ${r.rows.of} row(s)` : '""',
36193
+ hint: "`himi validate` counts a bound name as a name \u2014 it becomes copy on device \u2014 so only the crawl can see it come out empty. Give the control a name that cannot be empty: a literal `modifiers.accessibilityLabel` (an icon-only control can use `Icon.contentDescription`), or have the worker fall back to real copy instead of an empty string"
36194
+ });
36195
+ }
35925
36196
  for (const ref of collectServiceBindings(descriptor)) {
35926
36197
  if (!(ref.svc in state)) {
35927
36198
  findings.push({
@@ -36703,11 +36974,12 @@ async function crawlScreenRelations(opts) {
36703
36974
  return { findings: dedupe(findings), checked, byKind };
36704
36975
  });
36705
36976
  }
36706
- var ACTION_KEYS, UNTRACKED_ITEMS, NESTED_ITEMS_SEP, REJECT_ALL_MESSAGE, VLQ, WIX_DATA_QUERY, WIX_DATA_ANY, CAPABILITY_ACTION, detached, CRAWL_CLOCK, MEMBER_IDENTITY_ACTION, BLANK_WHEN_BOUND, STALE_SNAPSHOT_HINT, UNBINDABLE_ROW;
36977
+ var ACTION_KEYS, UNTRACKED_ITEMS, NESTED_ITEMS_SEP, REJECT_ALL_MESSAGE, VLQ, WIX_DATA_QUERY, WIX_DATA_ANY, CAPABILITY_ACTION, detached, CRAWL_CLOCK, MEMBER_IDENTITY_ACTION, BLANK_WHEN_BOUND, CONTROL_ROLES, NAME_KEYS, A11Y_ROWS_PER_LIST, A11Y_LIST_DEPTH, STALE_SNAPSHOT_HINT, UNBINDABLE_ROW;
36707
36978
  var init_screen_crawl = __esm({
36708
36979
  "../../core/dev-server/src/screen-crawl.ts"() {
36709
36980
  init_validate_descriptor_core();
36710
36981
  init_render_descriptor();
36982
+ init_a11y_contract();
36711
36983
  init_net_scenario();
36712
36984
  init_media_value();
36713
36985
  init_FakeServiceProvider();
@@ -36743,6 +37015,24 @@ var init_screen_crawl = __esm({
36743
37015
  Marquee: ["items", "itemsPath", "text"],
36744
37016
  MediaPlayer: ["items", "itemsPath"]
36745
37017
  };
37018
+ CONTROL_ROLES = /* @__PURE__ */ new Set([
37019
+ "button",
37020
+ "link",
37021
+ "toggle",
37022
+ "checkbox",
37023
+ "radio",
37024
+ "tab",
37025
+ "tablist",
37026
+ "adjustable",
37027
+ "search",
37028
+ "textfield",
37029
+ "combobox",
37030
+ "menu",
37031
+ "menuitem"
37032
+ ]);
37033
+ NAME_KEYS = new Set(ACCESSIBLE_NAME_KEYS);
37034
+ A11Y_ROWS_PER_LIST = 20;
37035
+ A11Y_LIST_DEPTH = 3;
36746
37036
  STALE_SNAPSHOT_HINT = 'if every path already does, look for a stale-snapshot write-back: `return s` (or `{ ...s }`) after an await or from an early-return guard writes back the state as it was BEFORE the await, loading flag included \u2014 to change nothing, return `undefined` instead (not `null` or `{}`); see `himi docs tier-5-add-a-screen`, "Should this result be committed at all?"';
36747
37037
  UNBINDABLE_ROW = Symbol("unbindable-row");
36748
37038
  }
@@ -46103,6 +46393,16 @@ function assembleLedger(r, options = {}) {
46103
46393
  remedy: "renders on device only \u2014 himi push a preview and check by eye"
46104
46394
  });
46105
46395
  }
46396
+ if ((r.crawled ?? []).length) {
46397
+ const found = [...r.failures, ...r.warnings].filter((f) => f.invariant === "unnamed-control").length;
46398
+ out.push({
46399
+ key: "a11y|app",
46400
+ kind: "a11y",
46401
+ screen: null,
46402
+ gap: `accessibility is judged for ONE property only \u2014 an operable control whose bound name rendered empty (\`unnamed-control\`, ${found} this run). NOT judged: names authored statically in the descriptor, role/state announcement, focus order, contrast, touch-target size \u2014 a clean run is not an accessibility pass`,
46403
+ remedy: "run `himi validate` for statically authored names (its a11y pass), then `himi push` a preview and walk every screen with VoiceOver and TalkBack"
46404
+ });
46405
+ }
46106
46406
  if (!/(^|\+ )mocked/.test(r.transport) && !r.transport.startsWith("offline")) {
46107
46407
  out.push({
46108
46408
  key: "no-user-mocks|app",
@@ -48067,19 +48367,19 @@ var init_preview_assets_generated = __esm({
48067
48367
  "../../packages/serve/src/preview-assets.generated.ts"() {
48068
48368
  init_preview_assets_format();
48069
48369
  PREVIEW_ASSET_MANIFEST = [
48070
- { key: "assets/index-Bsi3iDy5.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 1018562, sha256: "8487d8635f18c5a32f124ebc217f0570d5b8242ac964de661fa0644b80843116" },
48071
- { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 1018570, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
48072
- { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 1109494, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
48073
- { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 1200478, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
48074
- { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 1291458, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
48075
- { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 1382330, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
48076
- { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 1473310, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
48077
- { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 1581946, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
48078
- { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 1691030, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
48079
- { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 1800078, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
48080
- { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 1907778, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
48081
- { key: "index.html", contentType: "text/html; charset=utf-8", offset: 1995886, length: 4448, sha256: "60540583e7c95a5564940f1f05510886f0f59a6ec612af78881031a5e9c04b9a" },
48082
- { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 2000334, length: 17209, sha256: "dffc9718e2faa63ae2cc919e0a842f585ed565aaaa7c53778e90379442c731b6" }
48370
+ { key: "assets/index-DkRimsxm.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 1020087, sha256: "4cb472d9d6d952d85166c025544c8bae669215c1a5eafbb800cec32d199bad14" },
48371
+ { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 1020095, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
48372
+ { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 1111019, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
48373
+ { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 1202003, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
48374
+ { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 1292983, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
48375
+ { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 1383855, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
48376
+ { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 1474835, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
48377
+ { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 1583471, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
48378
+ { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 1692555, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
48379
+ { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 1801603, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
48380
+ { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 1909303, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
48381
+ { key: "index.html", contentType: "text/html; charset=utf-8", offset: 1997411, length: 4448, sha256: "22fbd62116f9571c6b01c1960990e392cce62198610eea2f661cb4d97bc05787" },
48382
+ { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 2001859, length: 17209, sha256: "dffc9718e2faa63ae2cc919e0a842f585ed565aaaa7c53778e90379442c731b6" }
48083
48383
  ];
48084
48384
  }
48085
48385
  });
Binary file
@@ -120,6 +120,22 @@ Only **unambiguous** defects, so a green run stays worth trusting:
120
120
  | `list-shrank-on-load-more` | **warning** | A "load more" returned FEWER rows than before it ran: the new page replaced the list instead of extending it. |
121
121
  | `list-grew-on-filter` | **warning** | A filter, search or sort made a list longer. Narrowing can reorder or remove, never add. |
122
122
  | `typed-value-ignored` | **warning** | The crawl typed the value a control really carries — the payload key every renderer injects for that control — and the dispatch changed **nothing**: no state, no navigation, no request. The value is read off the app itself (an id from the control's own options, or a word from the worker's seeded state) and is always one the control is *not* already showing, so a reducer that ignores it cannot work on device. A warning because the input is the crawl's: a reducer that legitimately no-ops on a value is possible, and `himi-test.json` `skip[]` takes the invariant. |
123
+ | `unnamed-control` | error | **Accessibility.** An operable control — a `Button`, `Toggle`, `TextField`, `Chip`, … or any node an `onTap` promotes to a button — whose *only* accessible name is a binding the worker resolved to an empty string, so a screen reader announces an unnamed control. `himi validate`'s a11y pass cannot see this: it counts `$service:` / `$item` as a name, because on device it becomes copy. Only the crawl has run the worker. Judged per real row inside a bound list, and reported once at the row template. Silent on a name authored empty (that is `himi validate`'s finding, not repeated here), on `null` / absent values (other invariants'), on content that is not a control (a bound `Text` that came out empty), and on nodes collapsed in that state. |
124
+
125
+ **Accessibility: what a clean run does NOT mean.** `unnamed-control` is the only
126
+ accessibility property the crawl judges, and it is deliberately the narrow one: the name a
127
+ worker actually produced for a control. It does not judge names authored in the descriptor
128
+ (`himi validate` does — its `a11y` pass is on by default, and the crawl never repeats what it
129
+ reports), role or state announcement, focus order, contrast, or touch-target size. The gap
130
+ ledger says so on every run with an `a11y` entry, because a headless crawl cannot close it —
131
+ before that entry existed, a clean crawl with an empty ledger read as an accessibility pass.
132
+ It ships at `error`: a control a screen reader cannot name is unusable, not a matter of taste.
133
+ Swept before it did, over 53 apps and templates: one false positive (a tap-to-dismiss `Text`
134
+ that `collapseWhenEmpty` removes, now honoured) and one real defect — a blank download button
135
+ on a lesson screen whose id resolves to no lesson. That last one is also why an input-less boot's
136
+ finding is **not** retired the way `no-request-issued` is: a detail screen reached with a stale
137
+ or unknown id renders exactly the state the crawl's input-less boot does, so a label that goes
138
+ blank there goes blank for a real user too.
123
139
 
124
140
  **Deliberately never reported:** an empty collection · an `error` state (often the correct
125
141
  answer to empty input) · a `null` optional.
package/guides/tokens.md CHANGED
@@ -20,6 +20,7 @@ one registry, mirrored byte-for-byte by the iOS + Android decoders.
20
20
  | `tabBar` / `drawer` / `sidebar` | nav-chrome style blocks | optional |
21
21
  | `kit` | { material?, variant?, density?, motion? } | optional — the active design kit |
22
22
  | `contentMaxWidth` | { base, medium?, expanded? } | optional |
23
+ | `elevations` | name → { level?, tint?, shadow?, border? } | optional — a Card wears `card` |
23
24
 
24
25
  ## ⚠️ Typography requires `size`, `weight`, AND `line`
25
26
 
@@ -86,3 +87,12 @@ Alternative full token sets ship with the CLI as design kits (material, shadcn,
86
87
  liquid-glass, brutalist, dark-luxe, neumorphic, skeuomorphic, minimal-editorial). Adopt a kit
87
88
  by copying its `tokens` into tokens.json; the active kit's app-wide axes
88
89
  (material/variant/density/motion) live in the top-level `kit` block.
90
+
91
+ ## Elevation
92
+
93
+ `elevations.card` is the depth every `Card` wears — paint only, never a layout change:
94
+ `level` 0–5 is Material 3 tonal elevation (the fill tinted with `tint`, default `accentPrimary`),
95
+ `shadow` names a `shadows` token cast in the Card's own shape, and `border: { color, width? }`
96
+ is a hairline inside the edge (default 0.5). Declare none and Cards stay flat. To look native on
97
+ each platform, declare `"card": {}` and let the idiom overlays fill it: `material` a `level`,
98
+ `ios` a `border` + `shadow`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1019.0",
3
+ "version": "0.1020.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": "1bc07d659f8a5254f7afe76b39faf5f6d53dff12518cfd6257039a78"
68
+ "falconPackageHash": "74688239abb5b83cdc592a78513066472b921f1273d54470d9796cab"
69
69
  }