@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.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/tokens.d.ts +36 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +7 -0
- package/dist/cli.mjs +317 -17
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/himi-test.md +16 -0
- package/guides/tokens.md +10 -0
- package/package.json +2 -2
package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/tokens.d.ts
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
48071
|
-
{ key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset:
|
|
48072
|
-
{ key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset:
|
|
48073
|
-
{ key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset:
|
|
48074
|
-
{ key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset:
|
|
48075
|
-
{ key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset:
|
|
48076
|
-
{ key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset:
|
|
48077
|
-
{ key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset:
|
|
48078
|
-
{ key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset:
|
|
48079
|
-
{ key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset:
|
|
48080
|
-
{ key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset:
|
|
48081
|
-
{ key: "index.html", contentType: "text/html; charset=utf-8", offset:
|
|
48082
|
-
{ key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset:
|
|
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
|
package/guides/himi-test.md
CHANGED
|
@@ -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.
|
|
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": "
|
|
68
|
+
"falconPackageHash": "74688239abb5b83cdc592a78513066472b921f1273d54470d9796cab"
|
|
69
69
|
}
|