@wix/himalaya-cli 0.1018.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,10 +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"() {
5352
+ "use strict";
5348
5353
  SYSTEM_FONT_FAMILY = "$system";
5354
+ TONAL_ELEVATION_ALPHA = [0, 0.05, 0.08, 0.11, 0.12, 0.14];
5349
5355
  }
5350
5356
  });
5351
5357
 
@@ -11078,6 +11084,29 @@ function tokenColorIssues(dir, tokensPathOverride) {
11078
11084
  );
11079
11085
  }
11080
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
+ }
11081
11110
  const colors = raw.colors;
11082
11111
  if (colors === void 0) return issues;
11083
11112
  if (colors === null || typeof colors !== "object" || Array.isArray(colors)) {
@@ -12843,6 +12872,32 @@ function resolveSchemeColor(o, scheme) {
12843
12872
  if (typeof o?.light === "string") return o.light;
12844
12873
  return typeof o?.dark === "string" ? o.dark : void 0;
12845
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
+ }
12846
12901
  function makeTokenResolvers(source, scheme, idiom = WEB_IDIOM_CONTEXT) {
12847
12902
  const tokens = resolveIdiom(source, idiom);
12848
12903
  const color = (name, fallback = "#1a1a1a") => {
@@ -12933,6 +12988,17 @@ function makeTokenResolvers(source, scheme, idiom = WEB_IDIOM_CONTEXT) {
12933
12988
  }
12934
12989
  return tint ? { background: rgba(tint, m.opacity ?? 1) } : void 0;
12935
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
+ };
12936
13002
  const densityScale = (density) => {
12937
13003
  switch (density) {
12938
13004
  case "compact":
@@ -12974,6 +13040,7 @@ function makeTokenResolvers(source, scheme, idiom = WEB_IDIOM_CONTEXT) {
12974
13040
  gradient,
12975
13041
  meshGradient,
12976
13042
  material,
13043
+ elevation,
12977
13044
  densityScale,
12978
13045
  motionTransition,
12979
13046
  appearMotion,
@@ -19815,6 +19882,7 @@ function shouldRevealError(args) {
19815
19882
  var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
19816
19883
  var init_validation = __esm({
19817
19884
  "../../core/ts/src/validation.ts"() {
19885
+ "use strict";
19818
19886
  EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
19819
19887
  URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
19820
19888
  INTEGER_RE = /^[+-]?[0-9]+$/;
@@ -23239,6 +23307,7 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
23239
23307
  var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
23240
23308
  var init_worker_subtree = __esm({
23241
23309
  "../../core/ts/src/worker-subtree.ts"() {
23310
+ "use strict";
23242
23311
  init_worker_subtree_safe_types_generated();
23243
23312
  WORKER_SUBTREE_CEILINGS = Object.freeze({
23244
23313
  maxEncodedBytes: 65536,
@@ -24956,8 +25025,17 @@ function renderInner(node, ctx) {
24956
25025
  }
24957
25026
  case "Grid":
24958
25027
  return /* @__PURE__ */ jsx6(GridNode, { props, ctx, wrap, gap });
24959
- case "Card":
24960
- 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
+ }
24961
25039
  case "SplitView":
24962
25040
  return /* @__PURE__ */ jsx6(SplitViewNode, { props, ctx, wrap });
24963
25041
  case "Divider":
@@ -28891,6 +28969,7 @@ __export(render_descriptor_exports, {
28891
28969
  clientEnvFor: () => clientEnvFor,
28892
28970
  collectNativeOnly: () => collectNativeOnly,
28893
28971
  isMissingPlaywrightExecutable: () => isMissingPlaywrightExecutable,
28972
+ nodeHidden: () => nodeHidden,
28894
28973
  pngAvailable: () => pngAvailable,
28895
28974
  pngPageHtml: () => pngPageHtml,
28896
28975
  renderDescriptorWeb: () => renderDescriptorWeb,
@@ -35061,6 +35140,7 @@ __export(screen_crawl_exports, {
35061
35140
  netTransport: () => netTransport,
35062
35141
  parseWorkerAction: () => parseWorkerAction,
35063
35142
  recordScreenTraffic: () => recordScreenTraffic,
35143
+ renderedNameGaps: () => renderedNameGaps,
35064
35144
  rootOnAppear: () => rootOnAppear,
35065
35145
  snapshot: () => snapshot,
35066
35146
  structuralSignature: () => structuralSignature,
@@ -35490,6 +35570,185 @@ function scanTreeValues(node, hit) {
35490
35570
  }
35491
35571
  node.children?.forEach((child) => scanTreeValues(child, hit));
35492
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
+ }
35493
35752
  function collectObservations(descriptorOrPayload, state) {
35494
35753
  const out = [];
35495
35754
  const walk2 = (node) => {
@@ -35919,6 +36178,21 @@ async function crawlScreenInner(opts) {
35919
36178
  });
35920
36179
  });
35921
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
+ }
35922
36196
  for (const ref of collectServiceBindings(descriptor)) {
35923
36197
  if (!(ref.svc in state)) {
35924
36198
  findings.push({
@@ -36700,11 +36974,12 @@ async function crawlScreenRelations(opts) {
36700
36974
  return { findings: dedupe(findings), checked, byKind };
36701
36975
  });
36702
36976
  }
36703
- 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;
36704
36978
  var init_screen_crawl = __esm({
36705
36979
  "../../core/dev-server/src/screen-crawl.ts"() {
36706
36980
  init_validate_descriptor_core();
36707
36981
  init_render_descriptor();
36982
+ init_a11y_contract();
36708
36983
  init_net_scenario();
36709
36984
  init_media_value();
36710
36985
  init_FakeServiceProvider();
@@ -36740,6 +37015,24 @@ var init_screen_crawl = __esm({
36740
37015
  Marquee: ["items", "itemsPath", "text"],
36741
37016
  MediaPlayer: ["items", "itemsPath"]
36742
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;
36743
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?"';
36744
37037
  UNBINDABLE_ROW = Symbol("unbindable-row");
36745
37038
  }
@@ -46100,6 +46393,16 @@ function assembleLedger(r, options = {}) {
46100
46393
  remedy: "renders on device only \u2014 himi push a preview and check by eye"
46101
46394
  });
46102
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
+ }
46103
46406
  if (!/(^|\+ )mocked/.test(r.transport) && !r.transport.startsWith("offline")) {
46104
46407
  out.push({
46105
46408
  key: "no-user-mocks|app",
@@ -46474,7 +46777,7 @@ var init_assets = __esm({
46474
46777
  async function checkPinnedRead(input) {
46475
46778
  const { secret, token, app, releaseId, screenId, authorizePreview } = input;
46476
46779
  if (!secret) return authorizePreview ? { status: 503, body: { error: "preview_token_unavailable" } } : null;
46477
- if (!token) return await input.isLive() ? null : { status: 401, body: { error: "preview_token_required", app, releaseId } };
46780
+ if (!token) return await input.isPublic() ? null : { status: 401, body: { error: "preview_token_required", app, releaseId } };
46478
46781
  const verified = await verifyPreviewToken({ token, secret, app, releaseId, screenId });
46479
46782
  if (verified.ok) {
46480
46783
  if (authorizePreview ? !await authorizePreview(verified.claims) : !!verified.claims.siteLease) {
@@ -46756,9 +47059,30 @@ function makeServeHandler(opts) {
46756
47059
  function resolvePin(request, url) {
46757
47060
  return request.headers.get("x-himalaya-release") || url.searchParams.get("release") || null;
46758
47061
  }
47062
+ const RELEASE_SOURCE_HEADER = "x-himalaya-release-source";
46759
47063
  function resolvePreviewToken(request, url) {
46760
47064
  return request.headers.get("x-himalaya-preview-token") || url.searchParams.get("pvt") || null;
46761
47065
  }
47066
+ const RING_CACHE_CAP = 500;
47067
+ const ringCache = /* @__PURE__ */ new Map();
47068
+ async function isRingRelease(app, releaseId) {
47069
+ if (!opts.ringReleases) return false;
47070
+ const hit = ringCache.get(app);
47071
+ let ids = hit && Date.now() - hit.at < manifestCacheTtlMs ? hit.ids : void 0;
47072
+ if (!ids) {
47073
+ try {
47074
+ ids = new Set(await opts.ringReleases(app));
47075
+ } catch {
47076
+ ids = /* @__PURE__ */ new Set();
47077
+ }
47078
+ if (!ringCache.has(app) && ringCache.size >= RING_CACHE_CAP) {
47079
+ const oldest = ringCache.keys().next().value;
47080
+ if (oldest !== void 0) ringCache.delete(oldest);
47081
+ }
47082
+ ringCache.set(app, { ids, at: Date.now() });
47083
+ }
47084
+ return ids.has(releaseId);
47085
+ }
46762
47086
  async function pinnedManifest(app, releaseId) {
46763
47087
  const key2 = `${app}
46764
47088
  ${releaseId}`;
@@ -46843,11 +47167,40 @@ ${releaseId}`;
46843
47167
  app,
46844
47168
  releaseId,
46845
47169
  screenId,
46846
- isLive: async () => (await manifest(app))?.releaseId === releaseId,
47170
+ isPublic: async () => (await manifest(app))?.releaseId === releaseId || await isRingRelease(app, releaseId),
46847
47171
  authorizePreview: opts.authorizePreview
46848
47172
  });
46849
47173
  return denied && json3(denied.status, denied.body);
46850
47174
  }
47175
+ async function honourRingPin(request) {
47176
+ const kept = { request, dropped: false };
47177
+ if (request.method !== "GET" && request.method !== "HEAD") return kept;
47178
+ if (request.headers.get(RELEASE_SOURCE_HEADER) !== "ring") return kept;
47179
+ const pin = request.headers.get("x-himalaya-release");
47180
+ if (!pin) return kept;
47181
+ const url = new URL(request.url);
47182
+ const app = /^\/v1\/app-config\/([^/]+)$/.exec(url.pathname)?.[1] ?? resolveApp(request, url);
47183
+ if (!app) return kept;
47184
+ const drop = () => {
47185
+ const headers2 = new Headers(request.headers);
47186
+ headers2.delete("x-himalaya-release");
47187
+ headers2.delete(RELEASE_SOURCE_HEADER);
47188
+ return { request: new Request(request, { headers: headers2 }), dropped: true };
47189
+ };
47190
+ if (/^\/v1\/app-config\/[^/]+$/.test(url.pathname)) return drop();
47191
+ let servable;
47192
+ try {
47193
+ const live2 = await manifest(app);
47194
+ const eligible = live2?.releaseId === pin || await isRingRelease(app, pin);
47195
+ const pinned = eligible ? await pinnedManifest(app, pin) : null;
47196
+ servable = pinned !== null;
47197
+ const screenId = /^\/v1\/screen\/([^/]+)$/.exec(url.pathname)?.[1];
47198
+ if (servable && screenId && !pinned?.screens[screenId]) servable = false;
47199
+ } catch {
47200
+ return kept;
47201
+ }
47202
+ return servable ? kept : drop();
47203
+ }
46851
47204
  const corsHeaders = (request) => {
46852
47205
  const origin = request.headers.get("origin");
46853
47206
  if (!origin) return {};
@@ -46856,7 +47209,7 @@ ${releaseId}`;
46856
47209
  "access-control-allow-origin": origin,
46857
47210
  vary: "Origin",
46858
47211
  "access-control-allow-methods": "GET, HEAD, POST, PUT, DELETE, OPTIONS",
46859
- "access-control-allow-headers": "Content-Type, If-None-Match, X-Himalaya-Schema-Versions, Accept-Language, X-Himalaya-Session, X-Himalaya-User, X-Himalaya-Platform, X-Himalaya-App-Version, X-Himalaya-App, X-Himalaya-Release, X-Himalaya-Preview-Token",
47212
+ "access-control-allow-headers": "Content-Type, If-None-Match, X-Himalaya-Schema-Versions, Accept-Language, X-Himalaya-Session, X-Himalaya-User, X-Himalaya-Platform, X-Himalaya-App-Version, X-Himalaya-App, X-Himalaya-Release, X-Himalaya-Release-Source, X-Himalaya-Preview-Token",
46860
47213
  "access-control-expose-headers": "ETag, X-Himalaya-Bundle-Integrity, X-Himalaya-App",
46861
47214
  "access-control-allow-credentials": "true"
46862
47215
  };
@@ -47396,7 +47749,8 @@ ${releaseId}`;
47396
47749
  }
47397
47750
  const mm = m;
47398
47751
  const readDoc = (hash, live2) => pin ? hash ? pinnedBody(app, hash, live2) : Promise.resolve(null) : live2();
47399
- const cfgBytes = await readDoc(mm.appConfigHash, () => store.getAppConfig(app));
47752
+ const ringPinned = pin !== null && request.headers.get(RELEASE_SOURCE_HEADER) === "ring";
47753
+ const cfgBytes = ringPinned ? await store.getAppConfig(app) : await readDoc(mm.appConfigHash, () => store.getAppConfig(app));
47400
47754
  if (!cfgBytes) return json3(404, { error: "app_config_not_found", app });
47401
47755
  let appConfig;
47402
47756
  try {
@@ -47404,7 +47758,7 @@ ${releaseId}`;
47404
47758
  } catch {
47405
47759
  return json3(500, { error: "app_config_invalid", app });
47406
47760
  }
47407
- const bootOverlay = !pin && opts.devControl ? devControlOverlay.get(app) : void 0;
47761
+ const bootOverlay = (!pin || ringPinned) && opts.devControl ? devControlOverlay.get(app) : void 0;
47408
47762
  if (bootOverlay) appConfig.control = { ...appConfig.control ?? {}, ...bootOverlay };
47409
47763
  const tokensBytes = await readDoc(mm.tokensHash, () => store.getTokens(app));
47410
47764
  let tokens = null;
@@ -47421,9 +47775,18 @@ ${releaseId}`;
47421
47775
  let screen = null;
47422
47776
  let screenHash = "";
47423
47777
  let homeBundle = null;
47424
- const homeMeta = homeScreenId ? mm.screens[homeScreenId] : void 0;
47778
+ let homeFrom = mm;
47779
+ let readHome = readDoc;
47780
+ if (ringPinned && homeScreenId && !mm.screens[homeScreenId]) {
47781
+ const live2 = await manifest(app);
47782
+ if (live2?.screens[homeScreenId]) {
47783
+ homeFrom = live2;
47784
+ readHome = (_hash, liveSlot) => liveSlot();
47785
+ }
47786
+ }
47787
+ const homeMeta = homeScreenId ? homeFrom.screens[homeScreenId] : void 0;
47425
47788
  if (homeScreenId && homeMeta) {
47426
- const body = await readDoc(homeMeta.descriptorHash, () => store.getScreen(app, homeScreenId));
47789
+ const body = await readHome(homeMeta.descriptorHash, () => store.getScreen(app, homeScreenId));
47427
47790
  if (body) {
47428
47791
  try {
47429
47792
  screen = JSON.parse(utf8Decode(body));
@@ -47433,7 +47796,7 @@ ${releaseId}`;
47433
47796
  }
47434
47797
  }
47435
47798
  if (homeMeta.bundle) {
47436
- const entry = mm.bundles[homeMeta.bundle];
47799
+ const entry = homeFrom.bundles[homeMeta.bundle];
47437
47800
  if (entry?.sha256 && entry?.sri) {
47438
47801
  homeBundle = {
47439
47802
  sha256: entry.sha256,
@@ -47443,9 +47806,10 @@ ${releaseId}`;
47443
47806
  }
47444
47807
  }
47445
47808
  }
47446
- const etag = strongEtag(`${mm.releaseId}:${mm.appConfigHash ?? ""}:${mm.tokensHash ?? ""}:${screenHash}:${homeBundle?.sha256 ?? ""}`);
47447
- const cache = pin ? PINNED_CACHE_CONTROL : DOC_CACHE_CONTROL;
47448
- const vary = pin ? { vary: "x-himalaya-release" } : {};
47809
+ const cfgTag = ringPinned ? sha256hex(JSON.stringify(appConfig)) : mm.appConfigHash ?? "";
47810
+ const etag = strongEtag(`${mm.releaseId}:${cfgTag}:${mm.tokensHash ?? ""}:${screenHash}:${homeBundle?.sha256 ?? ""}`);
47811
+ const cache = pin && !ringPinned ? PINNED_CACHE_CONTROL : DOC_CACHE_CONTROL;
47812
+ const vary = pin ? { vary: ringPinned ? `x-himalaya-release, ${RELEASE_SOURCE_HEADER}` : "x-himalaya-release" } : {};
47449
47813
  if (request.headers.get("if-none-match") === etag) {
47450
47814
  return new Response(null, { status: 304, headers: { etag, "cache-control": cache, ...vary } });
47451
47815
  }
@@ -47626,7 +47990,14 @@ ${releaseId}`;
47626
47990
  if (request.method === "OPTIONS") {
47627
47991
  return new Response(null, { status: 204, headers: { ...corsHeaders(request), "content-length": "0" } });
47628
47992
  }
47629
- const response = await handle(request);
47993
+ const ring = await honourRingPin(request);
47994
+ const response = await handle(ring.request);
47995
+ if (ring.dropped) {
47996
+ const parts = new Set(
47997
+ [...response.headers.get("vary")?.split(",") ?? [], "x-himalaya-release", RELEASE_SOURCE_HEADER].map((s) => s.trim()).filter(Boolean)
47998
+ );
47999
+ response.headers.set("vary", [...parts].join(", "));
48000
+ }
47630
48001
  if (resolvePreviewToken(request, new URL(request.url))) {
47631
48002
  response.headers.set("cache-control", "private, no-store");
47632
48003
  response.headers.set("vary", "x-himalaya-preview-token, x-himalaya-release");
@@ -47996,19 +48367,19 @@ var init_preview_assets_generated = __esm({
47996
48367
  "../../packages/serve/src/preview-assets.generated.ts"() {
47997
48368
  init_preview_assets_format();
47998
48369
  PREVIEW_ASSET_MANIFEST = [
47999
- { key: "assets/index-DRerTUYc.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 1016478, sha256: "42a5b901a0a58a495e5261f68782d28d12aa95cf64df33d9d8d14236e8ea6ead" },
48000
- { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 1016486, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
48001
- { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 1107410, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
48002
- { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 1198394, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
48003
- { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 1289374, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
48004
- { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 1380246, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
48005
- { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 1471226, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
48006
- { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 1579862, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
48007
- { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 1688946, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
48008
- { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 1797994, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
48009
- { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 1905694, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
48010
- { key: "index.html", contentType: "text/html; charset=utf-8", offset: 1993802, length: 4448, sha256: "98239860f1289aebffdb28d2b9f5ce15fbb95a657bb63d262c650ca21513dde3" },
48011
- { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 1998250, 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" }
48012
48383
  ];
48013
48384
  }
48014
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.1018.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": "94fed53fcfc6d8dbb2472ab3ebd2f6fc24d2cbe457604bc1f3601f2d"
68
+ "falconPackageHash": "74688239abb5b83cdc592a78513066472b921f1273d54470d9796cab"
69
69
  }