@cimplify/cli 0.15.3 → 0.16.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/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/add-SXC6RUPQ.mjs +140 -0
- package/dist/{add-KRIIKAS4.mjs → add-XQVQLD3F.mjs} +4 -4
- package/dist/ai-NQH5L4I6.mjs +17 -0
- package/dist/{assets-CWB72PZG.mjs → assets-FZGVOQXH.mjs} +12 -7
- package/dist/{auth-step-up-Z6KOTTV7.mjs → auth-step-up-67LCPDNL.mjs} +5 -5
- package/dist/build-J4ZUIOPX.mjs +625 -0
- package/dist/{cancel-ZHVVILOU.mjs → cancel-VGJHZJ4J.mjs} +4 -4
- package/dist/capture-RAJ2B3BH.mjs +69 -0
- package/dist/chunk-25ZPFHLF.mjs +51 -0
- package/dist/chunk-47ZXSY47.mjs +52 -0
- package/dist/{chunk-O2TO2J4I.mjs → chunk-54J3XR4T.mjs} +2 -2
- package/dist/chunk-5HP2INMY.mjs +10631 -0
- package/dist/chunk-7JR6JCFF.mjs +32 -0
- package/dist/chunk-7PQL7E7K.mjs +68 -0
- package/dist/chunk-AKACUJ3W.mjs +81 -0
- package/dist/chunk-BYNE553A.mjs +2945 -0
- package/dist/{chunk-NYXA6X56.mjs → chunk-CLQYPTJ2.mjs} +31 -11
- package/dist/chunk-CNZAOVSK.mjs +104 -0
- package/dist/chunk-CPBESPWK.mjs +5576 -0
- package/dist/{chunk-3RPR33MO.mjs → chunk-DYWUPHV3.mjs} +11 -7
- package/dist/chunk-EAV2P4N3.mjs +1941 -0
- package/dist/{chunk-UTD34C2H.mjs → chunk-EDZSOL5O.mjs} +2 -4
- package/dist/chunk-EGQIPYUN.mjs +696 -0
- package/dist/{chunk-DMLQBAGC.mjs → chunk-EOO4UOQD.mjs} +1 -1
- package/dist/chunk-EYEMPIMP.mjs +74 -0
- package/dist/chunk-G2ZEFSQJ.mjs +46 -0
- package/dist/{chunk-TH4XENKH.mjs → chunk-I4HNHPEM.mjs} +1 -1
- package/dist/chunk-JEVSFNPT.mjs +363 -0
- package/dist/chunk-JKECUAAV.mjs +2169 -0
- package/dist/chunk-KMHORI4Y.mjs +638 -0
- package/dist/chunk-KNPYBZ6B.mjs +502 -0
- package/dist/{chunk-3QUK5BPY.mjs → chunk-KOD4GBQ5.mjs} +8 -1
- package/dist/chunk-KQNZKV6E.mjs +535 -0
- package/dist/{chunk-2MKZKUVH.mjs → chunk-LLRUOIMU.mjs} +1 -2
- package/dist/{chunk-OLZKNUMK.mjs → chunk-LNMITJ4Z.mjs} +6 -6
- package/dist/chunk-MEM3MDV4.mjs +6463 -0
- package/dist/chunk-NY4YT7O6.mjs +84 -0
- package/dist/{chunk-LZEG3FEY.mjs → chunk-Q6CF4C3E.mjs} +7 -7
- package/dist/chunk-RP66A52P.mjs +86 -0
- package/dist/{chunk-I2UP7YTG.mjs → chunk-SAXGWS2K.mjs} +1 -1
- package/dist/chunk-SCKQ4XSD.mjs +41 -0
- package/dist/chunk-SF44L2OD.mjs +1152 -0
- package/dist/chunk-SGT5IJVW.mjs +28 -0
- package/dist/{chunk-LJEKKFC7.mjs → chunk-SS4JTMIV.mjs} +8 -8
- package/dist/chunk-T4MW3PD4.mjs +1456 -0
- package/dist/chunk-THATLMEX.mjs +1227 -0
- package/dist/{chunk-MLE4MVYC.mjs → chunk-TT4YI33G.mjs} +2 -2
- package/dist/chunk-TYV2SVFN.mjs +101 -0
- package/dist/chunk-VHK7WBO3.mjs +189 -0
- package/dist/chunk-VSA44UKR.mjs +391 -0
- package/dist/chunk-XL54SQJ3.mjs +42 -0
- package/dist/{chunk-5KYR6WJB.mjs → chunk-XQAHREKO.mjs} +17 -1
- package/dist/chunk-ZUCDXRFI.mjs +2011 -0
- package/dist/create-4S2EAUR3.mjs +10 -0
- package/dist/credits-NJOAOPPW.mjs +33 -0
- package/dist/{deploy-STWZ3OSM.mjs → deploy-26Y4UGZZ.mjs} +11 -11
- package/dist/dev-MOYURRWQ.mjs +401 -0
- package/dist/dev-WL43Q7VD.mjs +8 -0
- package/dist/dispatcher.mjs +131 -30
- package/dist/{doctor-3RW3OGZJ.mjs → doctor-E6ETD2GJ.mjs} +12 -20
- package/dist/{domains-FTNK4IKY.mjs → domains-LOTQMVOR.mjs} +6 -6
- package/dist/engine-runtime-fetch-CIE7S7PB.mjs +10 -0
- package/dist/{env-TNCYUAMT.mjs → env-3PDFC5XF.mjs} +7 -7
- package/dist/env-MOTYOPM5.mjs +19 -0
- package/dist/explain-K5QEVWCD.mjs +251 -0
- package/dist/{inspect-X3M7WSTL.mjs → inspect-Q3E2DHYY.mjs} +3 -3
- package/dist/inspect-SJ4WBAXO.mjs +86 -0
- package/dist/introspect-7F34YN5V.mjs +9 -0
- package/dist/kernel-DEGWMHFL.mjs +662 -0
- package/dist/kernel-plugin.js +6862 -0
- package/dist/keys-W5SN3DPK.mjs +71 -0
- package/dist/lines-XGJYTQ7P.mjs +56 -0
- package/dist/{link-76Z6V6CJ.mjs → link-NKUAVNM2.mjs} +6 -6
- package/dist/{lint-DDZ7HUQN.mjs → lint-G4YPQSSO.mjs} +2 -2
- package/dist/{list-2MFOQKKQ.mjs → list-TDR7K2JB.mjs} +3 -3
- package/dist/lock-EJPW2LM3.mjs +57 -0
- package/dist/{login-RLMDEWAL.mjs → login-AYLU7JU7.mjs} +10 -8
- package/dist/{logout-Z57X6HPE.mjs → logout-3M6VMHLH.mjs} +2 -2
- package/dist/{logs-TNDM4RFE.mjs → logs-4EL5SOXN.mjs} +5 -5
- package/dist/logs-4QQGG2XY.mjs +44 -0
- package/dist/mcp-4R2R6E5M.mjs +1077 -0
- package/dist/{mock-EVER34IX.mjs → mock-AK24GSP2.mjs} +2 -2
- package/dist/outdated-JEKCVLTG.mjs +58 -0
- package/dist/port-SBZ7X3EH.mjs +2408 -0
- package/dist/{projects-YL4T24VV.mjs → projects-7JKTC5WJ.mjs} +6 -6
- package/dist/publish-QQCX47HK.mjs +20 -0
- package/dist/remix-NLLYD3BI.mjs +125 -0
- package/dist/repo-Z4G4EW3N.mjs +9 -0
- package/dist/report-M7F4ZYP7.mjs +2 -0
- package/dist/rollback-2WQ2CW7T.mjs +43 -0
- package/dist/{rollback-47VQ4HJK.mjs → rollback-ZGU32HGT.mjs} +6 -6
- package/dist/scene-HXARHUM7.mjs +324 -0
- package/dist/{status-TWPOMWNA.mjs → status-C3ZSLZ7Q.mjs} +5 -5
- package/dist/status-WEQOC25K.mjs +18 -0
- package/dist/test-ZPBWV4VX.mjs +233 -0
- package/dist/testers-A3LMXOJJ.mjs +49 -0
- package/dist/{unlink-H3BO3DOQ.mjs → unlink-4G4FE4RJ.mjs} +2 -2
- package/dist/{update-FLY46XI3.mjs → update-GHKKM5FS.mjs} +4 -4
- package/dist/{whoami-CQPPARRS.mjs → whoami-3QQN6G2A.mjs} +3 -3
- package/dist/workshop-CD74D2LK.mjs +1703 -0
- package/package.json +11 -7
- package/templates/storefront-auto/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-auto/package.json +1 -1
- package/templates/storefront-bakery/package.json +1 -1
- package/templates/storefront-fashion/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-fashion/package.json +1 -1
- package/templates/storefront-grocery/package.json +1 -1
- package/templates/storefront-pharmacy/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-pharmacy/package.json +1 -1
- package/templates/storefront-restaurant/package.json +1 -1
- package/templates/storefront-retail/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-retail/package.json +1 -1
- package/templates/storefront-services/package.json +1 -1
- package/dist/chunk-ARIDYKLO.mjs +0 -6463
- package/dist/dev-7O65HY6C.mjs +0 -8
- package/dist/explain-DLRUUCNS.mjs +0 -251
- package/dist/introspect-RC265ACF.mjs +0 -9
- package/dist/repo-UUT3AZ3L.mjs +0 -9
|
@@ -0,0 +1,1227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { UiProp, UiKind, UiFlag, UI_HEADER_BYTES, UI_MAGIC, PatchWriter, UiRole, UiTrait, UiAnchor, UiOp, UI_VERSION } from '@cimplify/sdk/interactives';
|
|
3
|
+
|
|
4
|
+
// src/interactives/inspect/contrast.ts
|
|
5
|
+
var BYTE = 255;
|
|
6
|
+
var LINEAR_KNEE = 0.03928;
|
|
7
|
+
var LINEAR_SLOPE = 12.92;
|
|
8
|
+
var GAMMA_OFFSET = 0.055;
|
|
9
|
+
var GAMMA_SCALE = 1.055;
|
|
10
|
+
var GAMMA = 2.4;
|
|
11
|
+
var RED = 0.2126;
|
|
12
|
+
var GREEN = 0.7152;
|
|
13
|
+
var BLUE = 0.0722;
|
|
14
|
+
var FLARE = 0.05;
|
|
15
|
+
var MIX_STEP = 0.05;
|
|
16
|
+
var HEX = 16;
|
|
17
|
+
var BODY_CONTRAST = 4.5;
|
|
18
|
+
var LARGE_CONTRAST = 3;
|
|
19
|
+
var LARGE_TEXT_PT = 18;
|
|
20
|
+
var LARGE_BOLD_TEXT_PT = 14;
|
|
21
|
+
var BOLD_WEIGHT = 700;
|
|
22
|
+
function fromWord(word2) {
|
|
23
|
+
return { r: word2 >>> 24 & BYTE, g: word2 >>> 16 & BYTE, b: word2 >>> 8 & BYTE, a: (word2 & BYTE) / BYTE };
|
|
24
|
+
}
|
|
25
|
+
function toHex(color) {
|
|
26
|
+
const byte = (v) => Math.round(Math.max(0, Math.min(BYTE, v))).toString(HEX).padStart(2, "0");
|
|
27
|
+
return `#${byte(color.r)}${byte(color.g)}${byte(color.b)}${color.a >= 1 ? "" : byte(color.a * BYTE)}`;
|
|
28
|
+
}
|
|
29
|
+
function over(top, bottom) {
|
|
30
|
+
const a = top.a + bottom.a * (1 - top.a);
|
|
31
|
+
if (a === 0) return { r: 0, g: 0, b: 0, a: 0 };
|
|
32
|
+
const mix2 = (t, b) => (t * top.a + b * bottom.a * (1 - top.a)) / a;
|
|
33
|
+
return { r: mix2(top.r, bottom.r), g: mix2(top.g, bottom.g), b: mix2(top.b, bottom.b), a };
|
|
34
|
+
}
|
|
35
|
+
function channel(value) {
|
|
36
|
+
const c = value / BYTE;
|
|
37
|
+
return c <= LINEAR_KNEE ? c / LINEAR_SLOPE : ((c + GAMMA_OFFSET) / GAMMA_SCALE) ** GAMMA;
|
|
38
|
+
}
|
|
39
|
+
function luminance(color) {
|
|
40
|
+
return RED * channel(color.r) + GREEN * channel(color.g) + BLUE * channel(color.b);
|
|
41
|
+
}
|
|
42
|
+
function contrastRatio(foreground, background2) {
|
|
43
|
+
const base = background2.a >= 1 ? background2 : over(background2, { r: 0, g: 0, b: 0, a: 1 });
|
|
44
|
+
const text = over(foreground, base);
|
|
45
|
+
const a = luminance(text);
|
|
46
|
+
const b = luminance(base);
|
|
47
|
+
return (Math.max(a, b) + FLARE) / (Math.min(a, b) + FLARE);
|
|
48
|
+
}
|
|
49
|
+
function largeText(sizePt, weight) {
|
|
50
|
+
return sizePt >= LARGE_TEXT_PT || sizePt >= LARGE_BOLD_TEXT_PT && weight >= BOLD_WEIGHT;
|
|
51
|
+
}
|
|
52
|
+
function requiredContrast(sizePt, weight) {
|
|
53
|
+
return largeText(sizePt, weight) ? LARGE_CONTRAST : BODY_CONTRAST;
|
|
54
|
+
}
|
|
55
|
+
function mix(from, to, t) {
|
|
56
|
+
return { r: from.r + (to.r - from.r) * t, g: from.g + (to.g - from.g) * t, b: from.b + (to.b - from.b) * t, a: from.a + (to.a - from.a) * t };
|
|
57
|
+
}
|
|
58
|
+
function passingColor(foreground, background2, required) {
|
|
59
|
+
const opaque = { ...foreground, a: 1 };
|
|
60
|
+
const towards = luminance(background2) > luminance({ r: BYTE / 2, g: BYTE / 2, b: BYTE / 2}) ? [{ r: 0, g: 0, b: 0, a: 1 }, { r: BYTE, g: BYTE, b: BYTE, a: 1 }] : [{ r: BYTE, g: BYTE, b: BYTE, a: 1 }, { r: 0, g: 0, b: 0, a: 1 }];
|
|
61
|
+
for (const target of towards) {
|
|
62
|
+
for (let t = 0; t <= 1 + MIX_STEP / 2; t += MIX_STEP) {
|
|
63
|
+
const candidate = mix(opaque, target, Math.min(1, t));
|
|
64
|
+
if (contrastRatio(candidate, background2) >= required) return candidate;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return towards[0];
|
|
68
|
+
}
|
|
69
|
+
var QUANT_SHIFT = 4;
|
|
70
|
+
function dominantColor(pixels, width, height, rect) {
|
|
71
|
+
const x0 = Math.max(0, Math.floor(rect.x0));
|
|
72
|
+
const y0 = Math.max(0, Math.floor(rect.y0));
|
|
73
|
+
const x1 = Math.min(width, Math.ceil(rect.x1));
|
|
74
|
+
const y1 = Math.min(height, Math.ceil(rect.y1));
|
|
75
|
+
if (x1 <= x0 || y1 <= y0) return null;
|
|
76
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
77
|
+
for (let y = y0; y < y1; y++) {
|
|
78
|
+
for (let x = x0; x < x1; x++) {
|
|
79
|
+
const at = (y * width + x) * 4;
|
|
80
|
+
const r = pixels[at];
|
|
81
|
+
const g = pixels[at + 1];
|
|
82
|
+
const b = pixels[at + 2];
|
|
83
|
+
const key = r >> QUANT_SHIFT << 8 | g >> QUANT_SHIFT << 4 | b >> QUANT_SHIFT;
|
|
84
|
+
const bucket = buckets.get(key);
|
|
85
|
+
if (bucket === void 0) buckets.set(key, { n: 1, r, g, b });
|
|
86
|
+
else {
|
|
87
|
+
bucket.n++;
|
|
88
|
+
bucket.r += r;
|
|
89
|
+
bucket.g += g;
|
|
90
|
+
bucket.b += b;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
let best = null;
|
|
95
|
+
for (const bucket of buckets.values()) if (best === null || bucket.n > best.n) best = bucket;
|
|
96
|
+
return best === null ? null : { r: best.r / best.n, g: best.g / best.n, b: best.b / best.n, a: 1 };
|
|
97
|
+
}
|
|
98
|
+
var OP_STYLE = 25;
|
|
99
|
+
var OP_STYLE_CLEAR = 26;
|
|
100
|
+
var OP_TOKENS = 27;
|
|
101
|
+
var OP_SLOTS = 28;
|
|
102
|
+
var PROP_DISABLED = 47;
|
|
103
|
+
var Condition = { focus: 2, disabled: 4, landscape: 8, portrait: 16, safe: 32, sm: 64, lg: 128 };
|
|
104
|
+
var CONDITION_NAMES = [":pressed", ":focus", ":disabled", "@landscape", "@portrait", "@safe", "@sm", "@lg"];
|
|
105
|
+
var CONDITION_PRIORITY = [170, 150, 92, 200, 200, 200, 200, 200];
|
|
106
|
+
var BREAKPOINT_SM = 640;
|
|
107
|
+
var BREAKPOINT_LG = 1024;
|
|
108
|
+
var WORD = 4;
|
|
109
|
+
var HEADER_WORDS = 2;
|
|
110
|
+
var EXTENDED = 0;
|
|
111
|
+
var COUNT_OFFSET = 8;
|
|
112
|
+
var AUX_PROP_MASK = 255;
|
|
113
|
+
var UNIT_SHIFT = 8;
|
|
114
|
+
var UNIT_MASK = 15;
|
|
115
|
+
var NO_NODE = 4294967295;
|
|
116
|
+
var WORLD_ROOT_WORDS = 13;
|
|
117
|
+
var ENTRY_PROP_MASK = 255;
|
|
118
|
+
var ENTRY_UNIT_SHIFT = 8;
|
|
119
|
+
var ENTRY_UNIT_MASK = 15;
|
|
120
|
+
var ENTRY_COUNT_SHIFT = 16;
|
|
121
|
+
var ENTRY_COUNT_MASK = 255;
|
|
122
|
+
var ENTRY_REFERENCED = 1 << 24;
|
|
123
|
+
var ENTRY_SAFE_EDGES = 1 << 25;
|
|
124
|
+
var SLOT_REFERENCE = 2147483648;
|
|
125
|
+
var TOKEN_MASK = 1073741823;
|
|
126
|
+
var QUAD = 4;
|
|
127
|
+
var ALL_EDGES = 15;
|
|
128
|
+
var UNIT_POINTS = 1;
|
|
129
|
+
var SAFE_QUAD_PROPS = /* @__PURE__ */ new Set([UiProp.padding, UiProp.margin]);
|
|
130
|
+
var INSET_PROPS = [UiProp.inset_top, UiProp.inset_right, UiProp.inset_bottom, UiProp.inset_left];
|
|
131
|
+
var KIND_NAMES = (() => {
|
|
132
|
+
const names = [];
|
|
133
|
+
for (const [name, code] of Object.entries(UiKind)) names[code] = name;
|
|
134
|
+
return names;
|
|
135
|
+
})();
|
|
136
|
+
var decoder = new TextDecoder();
|
|
137
|
+
function emptyModel() {
|
|
138
|
+
return { nodes: /* @__PURE__ */ new Map(), strings: /* @__PURE__ */ new Map(), fonts: [], tokens: /* @__PURE__ */ new Map(), slots: /* @__PURE__ */ new Map(), problems: [], patches: 0, records: 0, created: 0 };
|
|
139
|
+
}
|
|
140
|
+
function conditionPriority(mask) {
|
|
141
|
+
let total = 0;
|
|
142
|
+
for (let bit = 0; bit < CONDITION_PRIORITY.length; bit++) if ((mask & 1 << bit) !== 0) total += CONDITION_PRIORITY[bit];
|
|
143
|
+
return total;
|
|
144
|
+
}
|
|
145
|
+
function conditionName(mask) {
|
|
146
|
+
if (mask === 0) return "default";
|
|
147
|
+
let out = "";
|
|
148
|
+
for (let bit = 0; bit < CONDITION_NAMES.length; bit++) if ((mask & 1 << bit) !== 0) out += CONDITION_NAMES[bit];
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
function detach(model, node) {
|
|
152
|
+
if (node.parent === null) return;
|
|
153
|
+
const parent = model.nodes.get(node.parent);
|
|
154
|
+
node.parent = null;
|
|
155
|
+
if (parent === void 0) return;
|
|
156
|
+
const at = parent.children.indexOf(node.id);
|
|
157
|
+
if (at >= 0) parent.children.splice(at, 1);
|
|
158
|
+
}
|
|
159
|
+
function forget(model, id) {
|
|
160
|
+
const node = model.nodes.get(id);
|
|
161
|
+
if (node === void 0) return;
|
|
162
|
+
detach(model, node);
|
|
163
|
+
for (const child of node.children.slice()) forget(model, child);
|
|
164
|
+
model.nodes.delete(id);
|
|
165
|
+
}
|
|
166
|
+
function readText(view, at, end) {
|
|
167
|
+
if (at + WORD > end) return "";
|
|
168
|
+
const length = view.getUint32(at, true);
|
|
169
|
+
const start = at + WORD;
|
|
170
|
+
const stop = Math.min(start + length, end);
|
|
171
|
+
return decoder.decode(new Uint8Array(view.buffer, view.byteOffset + start, stop - start));
|
|
172
|
+
}
|
|
173
|
+
function create(model, id, kind) {
|
|
174
|
+
if (model.nodes.has(id)) forget(model, id);
|
|
175
|
+
model.nodes.set(id, {
|
|
176
|
+
id,
|
|
177
|
+
kind: KIND_NAMES[kind] ?? "box",
|
|
178
|
+
order: model.created++,
|
|
179
|
+
parent: null,
|
|
180
|
+
children: [],
|
|
181
|
+
base: /* @__PURE__ */ new Map(),
|
|
182
|
+
variants: /* @__PURE__ */ new Map(),
|
|
183
|
+
props: /* @__PURE__ */ new Map(),
|
|
184
|
+
active: 0,
|
|
185
|
+
text: null,
|
|
186
|
+
textRef: null,
|
|
187
|
+
number: null,
|
|
188
|
+
label: null,
|
|
189
|
+
value: null,
|
|
190
|
+
placeholder: null,
|
|
191
|
+
image: null,
|
|
192
|
+
pathPoints: 0,
|
|
193
|
+
root: null,
|
|
194
|
+
worldRoot: null,
|
|
195
|
+
transition: null,
|
|
196
|
+
focused: false,
|
|
197
|
+
occluded: false
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function readEntries(view, at, end) {
|
|
201
|
+
const entries = [];
|
|
202
|
+
let cursor = at;
|
|
203
|
+
while (cursor < end) {
|
|
204
|
+
const head = view.getUint32(cursor, true);
|
|
205
|
+
cursor += WORD;
|
|
206
|
+
const count = head >>> ENTRY_COUNT_SHIFT & ENTRY_COUNT_MASK;
|
|
207
|
+
let refs = 0;
|
|
208
|
+
let safe = 0;
|
|
209
|
+
if ((head & ENTRY_REFERENCED) !== 0) {
|
|
210
|
+
refs = view.getUint32(cursor, true);
|
|
211
|
+
cursor += WORD;
|
|
212
|
+
}
|
|
213
|
+
if ((head & ENTRY_SAFE_EDGES) !== 0) {
|
|
214
|
+
safe = view.getUint32(cursor, true);
|
|
215
|
+
cursor += WORD;
|
|
216
|
+
}
|
|
217
|
+
if (cursor + count * WORD > end) return null;
|
|
218
|
+
const words = [];
|
|
219
|
+
for (let i = 0; i < count; i++) words.push(view.getUint32(cursor + i * WORD, true));
|
|
220
|
+
cursor += count * WORD;
|
|
221
|
+
entries.push({ prop: head & ENTRY_PROP_MASK, unit: head >>> ENTRY_UNIT_SHIFT & ENTRY_UNIT_MASK, refs, safe, words });
|
|
222
|
+
}
|
|
223
|
+
return entries;
|
|
224
|
+
}
|
|
225
|
+
function applyRecord(model, view, op, aux, nodeId, at, end) {
|
|
226
|
+
const word2 = (i) => view.getUint32(at + i * WORD, true);
|
|
227
|
+
const float2 = (i) => view.getFloat32(at + i * WORD, true);
|
|
228
|
+
const payloadWords = (end - at) / WORD;
|
|
229
|
+
if (op === UiOp.create) {
|
|
230
|
+
create(model, nodeId, aux);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (op === UiOp.string) {
|
|
234
|
+
model.strings.set(nodeId, readText(view, at, end));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (op === UiOp.font) {
|
|
238
|
+
model.fonts.push({ family: word2(0), weight: word2(1), font: word2(2) });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (op === OP_TOKENS || op === OP_SLOTS) {
|
|
242
|
+
const table = op === OP_TOKENS ? model.tokens : model.slots;
|
|
243
|
+
if (op === OP_TOKENS && (aux & 1) !== 0) table.clear();
|
|
244
|
+
for (let i = 0; i + 1 < payloadWords; i += 2) table.set(op === OP_TOKENS ? word2(i) & TOKEN_MASK : word2(i), word2(i + 1));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const node = model.nodes.get(nodeId);
|
|
248
|
+
if (node === void 0) {
|
|
249
|
+
model.problems.push(`op ${op} targets node ${nodeId}, which does not exist`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
switch (op) {
|
|
253
|
+
case UiOp.destroy:
|
|
254
|
+
forget(model, nodeId);
|
|
255
|
+
return;
|
|
256
|
+
case UiOp.insert: {
|
|
257
|
+
const parent = model.nodes.get(word2(0));
|
|
258
|
+
if (parent === void 0) {
|
|
259
|
+
model.problems.push(`node ${nodeId} is inserted into node ${word2(0)}, which does not exist`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
detach(model, node);
|
|
263
|
+
const before = word2(1);
|
|
264
|
+
const index = before === NO_NODE ? -1 : parent.children.indexOf(before);
|
|
265
|
+
if (index < 0) parent.children.push(nodeId);
|
|
266
|
+
else parent.children.splice(index, 0, nodeId);
|
|
267
|
+
node.parent = parent.id;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
case UiOp.remove:
|
|
271
|
+
detach(model, node);
|
|
272
|
+
return;
|
|
273
|
+
case UiOp.root:
|
|
274
|
+
node.root = { anchor: word2(0), safeArea: word2(1) !== 0, offset: [float2(2), float2(3)], z: view.getInt32(at + 4 * WORD, true) };
|
|
275
|
+
node.worldRoot = null;
|
|
276
|
+
return;
|
|
277
|
+
case UiOp.world_root:
|
|
278
|
+
if (payloadWords >= WORLD_ROOT_WORDS) node.worldRoot = { position: [float2(0), float2(1), float2(2)], z: view.getInt32(at + 12 * WORD, true) };
|
|
279
|
+
node.root = null;
|
|
280
|
+
return;
|
|
281
|
+
case UiOp.unroot:
|
|
282
|
+
node.root = null;
|
|
283
|
+
node.worldRoot = null;
|
|
284
|
+
return;
|
|
285
|
+
case UiOp.prop: {
|
|
286
|
+
const words = [];
|
|
287
|
+
for (let i = 0; i < payloadWords; i++) words.push(word2(i));
|
|
288
|
+
const prop = aux & AUX_PROP_MASK;
|
|
289
|
+
node.base.set(prop, { prop, unit: aux >> UNIT_SHIFT & UNIT_MASK, refs: 0, safe: 0, words });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
case UiOp.text:
|
|
293
|
+
node.text = readText(view, at, end);
|
|
294
|
+
node.textRef = null;
|
|
295
|
+
node.number = null;
|
|
296
|
+
return;
|
|
297
|
+
case UiOp.text_ref:
|
|
298
|
+
node.textRef = word2(0);
|
|
299
|
+
node.text = null;
|
|
300
|
+
node.number = null;
|
|
301
|
+
return;
|
|
302
|
+
case UiOp.number:
|
|
303
|
+
node.number = { value: view.getFloat64(at, true), format: view.getUint8(at + 2 * WORD), decimals: view.getUint8(at + 2 * WORD + 1) };
|
|
304
|
+
node.text = null;
|
|
305
|
+
node.textRef = null;
|
|
306
|
+
return;
|
|
307
|
+
case UiOp.label:
|
|
308
|
+
node.label = readText(view, at, end);
|
|
309
|
+
return;
|
|
310
|
+
case UiOp.value:
|
|
311
|
+
node.value = readText(view, at, end);
|
|
312
|
+
return;
|
|
313
|
+
case UiOp.placeholder:
|
|
314
|
+
node.placeholder = readText(view, at, end);
|
|
315
|
+
return;
|
|
316
|
+
case UiOp.image:
|
|
317
|
+
node.image = { asset: word2(0), width: float2(1), height: float2(2) };
|
|
318
|
+
return;
|
|
319
|
+
case UiOp.path:
|
|
320
|
+
node.pathPoints = Math.floor(payloadWords / 2);
|
|
321
|
+
return;
|
|
322
|
+
case UiOp.transition:
|
|
323
|
+
node.transition = { mask: aux, duration: float2(0), easing: word2(1) };
|
|
324
|
+
return;
|
|
325
|
+
case UiOp.focus:
|
|
326
|
+
for (const other of model.nodes.values()) other.focused = false;
|
|
327
|
+
node.focused = true;
|
|
328
|
+
return;
|
|
329
|
+
case UiOp.blur:
|
|
330
|
+
node.focused = false;
|
|
331
|
+
return;
|
|
332
|
+
case UiOp.occluded:
|
|
333
|
+
node.occluded = word2(0) !== 0;
|
|
334
|
+
return;
|
|
335
|
+
case OP_STYLE: {
|
|
336
|
+
const mask = aux & ENTRY_PROP_MASK;
|
|
337
|
+
const entries = readEntries(view, at, end);
|
|
338
|
+
if (entries === null) {
|
|
339
|
+
model.problems.push(`OP_STYLE for node ${nodeId} (mask ${mask}) is malformed`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (mask === 0) for (const entry2 of entries) node.base.set(entry2.prop, entry2);
|
|
343
|
+
else if (entries.length === 0) node.variants.delete(mask);
|
|
344
|
+
else node.variants.set(mask, entries);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
case OP_STYLE_CLEAR:
|
|
348
|
+
node.variants.clear();
|
|
349
|
+
return;
|
|
350
|
+
default:
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function applyPatch(model, bytes) {
|
|
355
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
356
|
+
if (bytes.byteLength < UI_HEADER_BYTES || view.getUint32(0, true) !== UI_MAGIC) {
|
|
357
|
+
model.problems.push(`patch ${model.patches} has no CUI1 header`);
|
|
358
|
+
model.patches++;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const count = view.getUint32(COUNT_OFFSET, true);
|
|
362
|
+
let at = UI_HEADER_BYTES;
|
|
363
|
+
for (let i = 0; i < count; i++) {
|
|
364
|
+
if (at + HEADER_WORDS * WORD > bytes.byteLength) {
|
|
365
|
+
model.problems.push(`patch ${model.patches} ends inside record ${i}`);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
const op = view.getUint8(at);
|
|
369
|
+
const compact = view.getUint8(at + 1);
|
|
370
|
+
const aux = view.getUint16(at + 2, true);
|
|
371
|
+
const node = view.getUint32(at + 4, true);
|
|
372
|
+
let words = compact;
|
|
373
|
+
let payload = at + HEADER_WORDS * WORD;
|
|
374
|
+
if (compact === EXTENDED) {
|
|
375
|
+
words = view.getUint32(payload, true);
|
|
376
|
+
payload += WORD;
|
|
377
|
+
}
|
|
378
|
+
const end = at + words * WORD;
|
|
379
|
+
if (words < HEADER_WORDS || end > bytes.byteLength) {
|
|
380
|
+
model.problems.push(`patch ${model.patches} record ${i} (op ${op}) is malformed`);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
applyRecord(model, view, op, aux, node, payload, end);
|
|
384
|
+
model.records++;
|
|
385
|
+
at = end;
|
|
386
|
+
}
|
|
387
|
+
model.patches++;
|
|
388
|
+
}
|
|
389
|
+
function contextBits(context) {
|
|
390
|
+
let bits2 = Condition.safe | (context.width > context.height ? Condition.landscape : Condition.portrait);
|
|
391
|
+
if (context.width >= BREAKPOINT_SM) bits2 |= Condition.sm;
|
|
392
|
+
if (context.width >= BREAKPOINT_LG) bits2 |= Condition.lg;
|
|
393
|
+
return bits2;
|
|
394
|
+
}
|
|
395
|
+
var f32 = new Float32Array(1);
|
|
396
|
+
var u32 = new Uint32Array(f32.buffer);
|
|
397
|
+
function addFloat(word2, delta) {
|
|
398
|
+
u32[0] = word2;
|
|
399
|
+
f32[0] = f32[0] + delta;
|
|
400
|
+
return u32[0];
|
|
401
|
+
}
|
|
402
|
+
function entryValue(entry2, model, insets) {
|
|
403
|
+
let words = entry2.words.slice();
|
|
404
|
+
for (let i = 0; i < words.length; i++) {
|
|
405
|
+
if ((entry2.refs & 1 << i) === 0) continue;
|
|
406
|
+
const ref = words[i];
|
|
407
|
+
if ((ref & SLOT_REFERENCE) !== 0) {
|
|
408
|
+
words[i] = model.slots.get((ref & ~SLOT_REFERENCE) >>> 0) ?? 0;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const token = model.tokens.get(ref & TOKEN_MASK);
|
|
412
|
+
if (token === void 0) return null;
|
|
413
|
+
words[i] = token;
|
|
414
|
+
}
|
|
415
|
+
if (entry2.safe !== 0) {
|
|
416
|
+
const edges = [insets.top, insets.right, insets.bottom, insets.left];
|
|
417
|
+
if (SAFE_QUAD_PROPS.has(entry2.prop)) {
|
|
418
|
+
const broadcast = words.length === 1;
|
|
419
|
+
if (broadcast) words = [words[0], words[0], words[0], words[0]];
|
|
420
|
+
const safe = broadcast && (entry2.safe & 1) !== 0 ? ALL_EDGES : entry2.safe;
|
|
421
|
+
for (let i = 0; i < QUAD; i++) if ((safe & 1 << i) !== 0) words[i] = addFloat(words[i], edges[i]);
|
|
422
|
+
} else {
|
|
423
|
+
const edge = INSET_PROPS.indexOf(entry2.prop);
|
|
424
|
+
if (edge >= 0 && (entry2.safe & 1) !== 0 && entry2.unit === UNIT_POINTS) words[0] = addFloat(words[0], edges[edge]);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return { unit: entry2.unit, words };
|
|
428
|
+
}
|
|
429
|
+
function stateBits(model, node) {
|
|
430
|
+
let bits2 = 0;
|
|
431
|
+
let cursor = node;
|
|
432
|
+
while (cursor !== void 0) {
|
|
433
|
+
if (cursor.focused) bits2 |= Condition.focus;
|
|
434
|
+
if (cursor.base.get(PROP_DISABLED)?.words[0] === 1) bits2 |= Condition.disabled;
|
|
435
|
+
cursor = cursor.parent === null ? void 0 : model.nodes.get(cursor.parent);
|
|
436
|
+
}
|
|
437
|
+
if ((bits2 & Condition.disabled) !== 0) bits2 &= -4;
|
|
438
|
+
return bits2;
|
|
439
|
+
}
|
|
440
|
+
function resolveModel(model, context) {
|
|
441
|
+
const ctx = contextBits(context);
|
|
442
|
+
for (const node of model.nodes.values()) {
|
|
443
|
+
const active = stateBits(model, node) | ctx;
|
|
444
|
+
node.active = active;
|
|
445
|
+
const props = /* @__PURE__ */ new Map();
|
|
446
|
+
for (const [prop, entry2] of node.base) {
|
|
447
|
+
const value = entryValue(entry2, model, context.insets);
|
|
448
|
+
if (value !== null) props.set(prop, value);
|
|
449
|
+
}
|
|
450
|
+
const masks = [...node.variants.keys()].filter((m) => (m & ~active) === 0).sort((a, b) => conditionPriority(a) - conditionPriority(b) || a - b);
|
|
451
|
+
for (const mask of masks) {
|
|
452
|
+
for (const entry2 of node.variants.get(mask) ?? []) {
|
|
453
|
+
const value = entryValue(entry2, model, context.insets);
|
|
454
|
+
if (value !== null) props.set(entry2.prop, value);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
node.props = props;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function decodeStream(patches, context = null) {
|
|
461
|
+
const model = emptyModel();
|
|
462
|
+
for (const patch of patches) applyPatch(model, patch);
|
|
463
|
+
resolveModel(model, context ?? { width: 0, height: 0, insets: { top: 0, right: 0, bottom: 0, left: 0 } });
|
|
464
|
+
return model;
|
|
465
|
+
}
|
|
466
|
+
function encodeEntry(entry2) {
|
|
467
|
+
let head = entry2.prop | entry2.unit << ENTRY_UNIT_SHIFT | entry2.words.length << ENTRY_COUNT_SHIFT;
|
|
468
|
+
const out = [];
|
|
469
|
+
if (entry2.refs !== 0) head |= ENTRY_REFERENCED;
|
|
470
|
+
if (entry2.safe !== 0) head |= ENTRY_SAFE_EDGES;
|
|
471
|
+
out.push(head >>> 0);
|
|
472
|
+
if (entry2.refs !== 0) out.push(entry2.refs);
|
|
473
|
+
if (entry2.safe !== 0) out.push(entry2.safe);
|
|
474
|
+
out.push(...entry2.words);
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/interactives/inspect/props.ts
|
|
479
|
+
var PROP_FOCUSABLE = 48;
|
|
480
|
+
var PROP_TEXT_WRAP_STYLE = 74;
|
|
481
|
+
var DEFAULT_FONT_SIZE = 16;
|
|
482
|
+
var DEFAULT_FONT_WEIGHT = 400;
|
|
483
|
+
var DEFAULT_TEXT_COLOR = 4294967295;
|
|
484
|
+
var DIRECTION = { 0: "row", 1: "column", 2: "row-reverse", 3: "column-reverse" };
|
|
485
|
+
var JUSTIFY = { 0: "start", 1: "end", 2: "center", 3: "space-between", 4: "space-around", 5: "space-evenly" };
|
|
486
|
+
var ALIGN = { 0: "auto", 1: "start", 2: "end", 3: "center", 4: "stretch" };
|
|
487
|
+
var POSITION = { 0: "relative", 1: "absolute" };
|
|
488
|
+
var TEXT_ALIGN = { 0: "start", 1: "end", 2: "left", 3: "center", 4: "right" };
|
|
489
|
+
var VERTICAL_ALIGN = { 0: "top", 1: "center", 2: "bottom" };
|
|
490
|
+
var SHAPE_KIND = { 0: "rect", 1: "ellipse", 2: "path" };
|
|
491
|
+
var TEXT_WRAP_STYLE = { 0: "greedy", 1: "balance", 2: "pretty" };
|
|
492
|
+
var PROPS = {
|
|
493
|
+
[UiProp.width]: ["width", "length"],
|
|
494
|
+
[UiProp.height]: ["height", "length"],
|
|
495
|
+
[UiProp.min_width]: ["minWidth", "length"],
|
|
496
|
+
[UiProp.min_height]: ["minHeight", "length"],
|
|
497
|
+
[UiProp.max_width]: ["maxWidth", "length"],
|
|
498
|
+
[UiProp.max_height]: ["maxHeight", "length"],
|
|
499
|
+
[UiProp.basis]: ["basis", "length"],
|
|
500
|
+
[UiProp.padding]: ["padding", "quad"],
|
|
501
|
+
[UiProp.margin]: ["margin", "quad"],
|
|
502
|
+
[UiProp.border_width]: ["borderWidth", "quad"],
|
|
503
|
+
[UiProp.inset_top]: ["top", "length"],
|
|
504
|
+
[UiProp.inset_right]: ["right", "length"],
|
|
505
|
+
[UiProp.inset_bottom]: ["bottom", "length"],
|
|
506
|
+
[UiProp.inset_left]: ["left", "length"],
|
|
507
|
+
[UiProp.gap]: ["gap", "pair"],
|
|
508
|
+
[UiProp.grow]: ["grow", "float"],
|
|
509
|
+
[UiProp.shrink]: ["shrink", "float"],
|
|
510
|
+
[UiProp.aspect]: ["aspect", "float"],
|
|
511
|
+
[UiProp.direction]: ["direction", DIRECTION],
|
|
512
|
+
[UiProp.justify]: ["justify", JUSTIFY],
|
|
513
|
+
[UiProp.align_items]: ["alignItems", ALIGN],
|
|
514
|
+
[UiProp.align_self]: ["alignSelf", ALIGN],
|
|
515
|
+
[UiProp.align_content]: ["alignContent", ALIGN],
|
|
516
|
+
[UiProp.position]: ["position", POSITION],
|
|
517
|
+
[UiProp.wrap]: ["wrap", "bool"],
|
|
518
|
+
[UiProp.display_none]: ["displayNone", "bool"],
|
|
519
|
+
[UiProp.fill]: ["fill", "color"],
|
|
520
|
+
[UiProp.border_color]: ["borderColor", "colors"],
|
|
521
|
+
[UiProp.radii]: ["radius", "quad"],
|
|
522
|
+
[UiProp.opacity]: ["opacity", "float"],
|
|
523
|
+
[UiProp.translate]: ["translate", "pair"],
|
|
524
|
+
[UiProp.scale]: ["scale", "pair"],
|
|
525
|
+
[UiProp.rotate]: ["rotate", "float"],
|
|
526
|
+
[UiProp.origin]: ["origin", "pair"],
|
|
527
|
+
[UiProp.z_index]: ["zIndex", "int"],
|
|
528
|
+
[UiProp.hidden]: ["hidden", "bool"],
|
|
529
|
+
[UiProp.clip]: ["clip", "bool"],
|
|
530
|
+
[UiProp.pointer_events_none]: ["pointerEventsNone", "bool"],
|
|
531
|
+
[UiProp.shadow]: ["shadow", "shadow"],
|
|
532
|
+
[UiProp.gradient]: ["gradient", "gradient"],
|
|
533
|
+
[PROP_DISABLED]: ["disabled", "bool"],
|
|
534
|
+
[PROP_FOCUSABLE]: ["focusable", "bool"],
|
|
535
|
+
[UiProp.font_family]: ["fontFamily", "word"],
|
|
536
|
+
[UiProp.font_weight]: ["fontWeight", "word"],
|
|
537
|
+
[UiProp.font_size]: ["fontSize", "float"],
|
|
538
|
+
[UiProp.line_height]: ["lineHeight", "float"],
|
|
539
|
+
[UiProp.letter_spacing]: ["letterSpacing", "float"],
|
|
540
|
+
[UiProp.text_align]: ["textAlign", TEXT_ALIGN],
|
|
541
|
+
[UiProp.text_color]: ["textColor", "color"],
|
|
542
|
+
[UiProp.text_wrap]: ["textWrap", "bool"],
|
|
543
|
+
[UiProp.vertical_align]: ["verticalAlign", VERTICAL_ALIGN],
|
|
544
|
+
[UiProp.placeholder_color]: ["placeholderColor", "color"],
|
|
545
|
+
[PROP_TEXT_WRAP_STYLE]: ["textWrapStyle", TEXT_WRAP_STYLE],
|
|
546
|
+
[UiProp.image_tint]: ["tint", "color"],
|
|
547
|
+
[UiProp.image_uv]: ["uv", "quad"],
|
|
548
|
+
[UiProp.nine_slice]: ["nineSlice", "quad"],
|
|
549
|
+
[UiProp.nine_slice_uv]: ["nineSliceUv", "quad"],
|
|
550
|
+
[UiProp.shape_kind]: ["shape", SHAPE_KIND],
|
|
551
|
+
[UiProp.stroke_color]: ["stroke", "color"],
|
|
552
|
+
[UiProp.stroke_width]: ["strokeWidth", "float"],
|
|
553
|
+
[UiProp.scroll_axes]: ["scrollAxes", "word"]
|
|
554
|
+
};
|
|
555
|
+
var NOT_STYLE = /* @__PURE__ */ new Set([UiProp.flags, UiProp.role, UiProp.traits, UiProp.actions]);
|
|
556
|
+
var f322 = new Float32Array(1);
|
|
557
|
+
var u322 = new Uint32Array(f322.buffer);
|
|
558
|
+
var i32 = new Int32Array(f322.buffer);
|
|
559
|
+
function floatOf(word2) {
|
|
560
|
+
u322[0] = word2;
|
|
561
|
+
return Math.round(f322[0] * 1e3) / 1e3;
|
|
562
|
+
}
|
|
563
|
+
function intOf(word2) {
|
|
564
|
+
u322[0] = word2;
|
|
565
|
+
return i32[0];
|
|
566
|
+
}
|
|
567
|
+
var HEX2 = 16;
|
|
568
|
+
var BYTE_HEX = 2;
|
|
569
|
+
function hexColor(word2) {
|
|
570
|
+
return `#${(word2 >>> 0).toString(HEX2).padStart(BYTE_HEX * 4, "0")}`;
|
|
571
|
+
}
|
|
572
|
+
function quad(words) {
|
|
573
|
+
if (words.length < 4) return floatOf(words[0] ?? 0);
|
|
574
|
+
const values = words.slice(0, 4).map(floatOf);
|
|
575
|
+
return values.every((v) => v === values[0]) ? values[0] : values;
|
|
576
|
+
}
|
|
577
|
+
function pair(words) {
|
|
578
|
+
if (words.length < 2) return floatOf(words[0] ?? 0);
|
|
579
|
+
const values = words.slice(0, 2).map(floatOf);
|
|
580
|
+
return values[0] === values[1] ? values[0] : values;
|
|
581
|
+
}
|
|
582
|
+
function decode(shape, raw) {
|
|
583
|
+
const words = raw.words;
|
|
584
|
+
const first = words[0] ?? 0;
|
|
585
|
+
if (typeof shape === "object") return shape[first] ?? first;
|
|
586
|
+
switch (shape) {
|
|
587
|
+
case "length":
|
|
588
|
+
return raw.unit === 0 ? "auto" : raw.unit === 2 ? `${floatOf(first)}%` : floatOf(first);
|
|
589
|
+
case "float":
|
|
590
|
+
return floatOf(first);
|
|
591
|
+
case "int":
|
|
592
|
+
return intOf(first);
|
|
593
|
+
case "word":
|
|
594
|
+
return first;
|
|
595
|
+
case "bool":
|
|
596
|
+
return first !== 0;
|
|
597
|
+
case "color":
|
|
598
|
+
return hexColor(first);
|
|
599
|
+
case "colors": {
|
|
600
|
+
if (words.length < 4) return hexColor(first);
|
|
601
|
+
const colors = words.slice(0, 4).map(hexColor);
|
|
602
|
+
return colors.every((c) => c === colors[0]) ? colors[0] : colors;
|
|
603
|
+
}
|
|
604
|
+
case "quad":
|
|
605
|
+
return quad(words);
|
|
606
|
+
case "pair":
|
|
607
|
+
return pair(words);
|
|
608
|
+
case "shadow":
|
|
609
|
+
return words.length < 5 ? null : { color: hexColor(words[0]), x: floatOf(words[1]), y: floatOf(words[2]), blur: floatOf(words[3]), spread: floatOf(words[4]) };
|
|
610
|
+
case "gradient":
|
|
611
|
+
return words.length === 0 ? null : { kind: words[0] === 1 ? "radial" : "linear", stops: words.slice(1, 5).map(hexColor) };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function resolvedStyle(node) {
|
|
615
|
+
const out = {};
|
|
616
|
+
const ids = [...node.props.keys()].sort((a, b) => a - b);
|
|
617
|
+
for (const id of ids) {
|
|
618
|
+
if (NOT_STYLE.has(id)) continue;
|
|
619
|
+
const spec = PROPS[id];
|
|
620
|
+
const raw = node.props.get(id);
|
|
621
|
+
if (spec === void 0) {
|
|
622
|
+
out[`prop${id}`] = raw.words.slice();
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
out[spec[0]] = decode(spec[1], raw);
|
|
626
|
+
}
|
|
627
|
+
return out;
|
|
628
|
+
}
|
|
629
|
+
function word(node, prop) {
|
|
630
|
+
const raw = node.props.get(prop);
|
|
631
|
+
return raw === void 0 || raw.words.length === 0 ? null : raw.words[0];
|
|
632
|
+
}
|
|
633
|
+
function float(node, prop) {
|
|
634
|
+
const w = word(node, prop);
|
|
635
|
+
return w === null ? null : floatOf(w);
|
|
636
|
+
}
|
|
637
|
+
function fontSize(node) {
|
|
638
|
+
return float(node, UiProp.font_size) ?? DEFAULT_FONT_SIZE;
|
|
639
|
+
}
|
|
640
|
+
function fontWeight(node) {
|
|
641
|
+
return word(node, UiProp.font_weight) ?? DEFAULT_FONT_WEIGHT;
|
|
642
|
+
}
|
|
643
|
+
function textColor(node) {
|
|
644
|
+
return word(node, UiProp.text_color) ?? DEFAULT_TEXT_COLOR;
|
|
645
|
+
}
|
|
646
|
+
function flags(node) {
|
|
647
|
+
return word(node, UiProp.flags) ?? 0;
|
|
648
|
+
}
|
|
649
|
+
function traits(node) {
|
|
650
|
+
return word(node, UiProp.traits) ?? 0;
|
|
651
|
+
}
|
|
652
|
+
function role(node) {
|
|
653
|
+
const code = word(node, UiProp.role) ?? UiRole.none;
|
|
654
|
+
for (const [name, value] of Object.entries(UiRole)) if (value === code) return name;
|
|
655
|
+
return "none";
|
|
656
|
+
}
|
|
657
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set(["button", "link", "adjustable", "tab", "text_field"]);
|
|
658
|
+
var INTERACTIVE_FLAGS = UiFlag.tap | UiFlag.press | UiFlag.drag | UiFlag.long_press;
|
|
659
|
+
function interactive(node) {
|
|
660
|
+
return node.kind === "input" || (flags(node) & INTERACTIVE_FLAGS) !== 0 || INTERACTIVE_ROLES.has(role(node));
|
|
661
|
+
}
|
|
662
|
+
function gestureNames(node) {
|
|
663
|
+
const bits2 = flags(node);
|
|
664
|
+
const out = [];
|
|
665
|
+
if ((bits2 & UiFlag.tap) !== 0) out.push("tap");
|
|
666
|
+
if ((bits2 & UiFlag.press) !== 0) out.push("press");
|
|
667
|
+
if ((bits2 & UiFlag.long_press) !== 0) out.push("long_press");
|
|
668
|
+
if ((bits2 & UiFlag.drag) !== 0) out.push("drag");
|
|
669
|
+
return out;
|
|
670
|
+
}
|
|
671
|
+
function disabled(node) {
|
|
672
|
+
return (traits(node) & UiTrait.disabled) !== 0 || (node.active & Condition.disabled) !== 0;
|
|
673
|
+
}
|
|
674
|
+
function hiddenFromAccessibility(node) {
|
|
675
|
+
return (traits(node) & UiTrait.hidden) !== 0;
|
|
676
|
+
}
|
|
677
|
+
var INSPECT_SCHEMA = "cimplify.inspect/1";
|
|
678
|
+
var SITE = /^(.*):(\d+):(\d+)$/;
|
|
679
|
+
function parseSite(where) {
|
|
680
|
+
const match = SITE.exec(where);
|
|
681
|
+
if (match === null) return null;
|
|
682
|
+
return { file: match[1], line: Number(match[2]), column: Number(match[3]) };
|
|
683
|
+
}
|
|
684
|
+
function anchorName(code) {
|
|
685
|
+
for (const [name, value] of Object.entries(UiAnchor)) if (value === code) return name;
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
function displayedText(node, model, shown) {
|
|
689
|
+
if (node.kind !== "text" && node.kind !== "input") return null;
|
|
690
|
+
if (shown !== void 0 && shown.length > 0) return shown;
|
|
691
|
+
if (node.text !== null) return node.text;
|
|
692
|
+
if (node.textRef !== null) return model.strings.get(node.textRef) ?? null;
|
|
693
|
+
if (node.number !== null) return String(node.number.value);
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
function viewportOf(device, reported) {
|
|
697
|
+
if (reported === null || reported.length < 7) return { width: device.width, height: device.height, scale: device.scale, insets: device.insets };
|
|
698
|
+
return { width: reported[0], height: reported[1], scale: reported[2], insets: { top: reported[3], right: reported[4], bottom: reported[5], left: reported[6] } };
|
|
699
|
+
}
|
|
700
|
+
function buildNodes(input) {
|
|
701
|
+
const { model, measured } = input;
|
|
702
|
+
const sites = /* @__PURE__ */ new Map();
|
|
703
|
+
for (const site of input.sites) sites.set(site[0], site);
|
|
704
|
+
const exempt = /* @__PURE__ */ new Map();
|
|
705
|
+
for (const [node, check, reason] of input.exempt) {
|
|
706
|
+
const list = exempt.get(node) ?? [];
|
|
707
|
+
list.push({ check, reason });
|
|
708
|
+
exempt.set(node, list);
|
|
709
|
+
}
|
|
710
|
+
const nodes = [...model.nodes.values()].sort((a, b) => {
|
|
711
|
+
const za = measured?.paint.get(a.id);
|
|
712
|
+
const zb = measured?.paint.get(b.id);
|
|
713
|
+
if (za !== void 0 && zb !== void 0) return za - zb;
|
|
714
|
+
if (za !== void 0) return -1;
|
|
715
|
+
if (zb !== void 0) return 1;
|
|
716
|
+
return a.order - b.order;
|
|
717
|
+
});
|
|
718
|
+
return nodes.map((node) => {
|
|
719
|
+
const rect = measured?.rects.get(node.id) ?? null;
|
|
720
|
+
const site = sites.get(node.id);
|
|
721
|
+
return {
|
|
722
|
+
id: node.id,
|
|
723
|
+
kind: node.kind,
|
|
724
|
+
name: site?.[2] ?? null,
|
|
725
|
+
parent: node.parent,
|
|
726
|
+
children: node.children.slice(),
|
|
727
|
+
rect,
|
|
728
|
+
visible: rect !== null && rect.width > 0 && rect.height > 0,
|
|
729
|
+
z: measured?.paint.get(node.id) ?? null,
|
|
730
|
+
text: displayedText(node, model, measured?.shown.get(node.id)),
|
|
731
|
+
label: node.label,
|
|
732
|
+
value: node.value,
|
|
733
|
+
placeholder: node.placeholder,
|
|
734
|
+
role: role(node),
|
|
735
|
+
gestures: gestureNames(node),
|
|
736
|
+
interactive: interactive(node),
|
|
737
|
+
state: { disabled: disabled(node), focused: node.focused, accessibility_hidden: hiddenFromAccessibility(node), occluded: node.occluded },
|
|
738
|
+
style: resolvedStyle(node),
|
|
739
|
+
variants: [...node.variants.keys()].sort((a, b) => conditionPriority(a) - conditionPriority(b) || a - b).map(conditionName),
|
|
740
|
+
conditions: conditionName(node.active),
|
|
741
|
+
root: node.root !== null ? { kind: "screen", anchor: anchorName(node.root.anchor), safe_area: node.root.safeArea, z: node.root.z } : node.worldRoot !== null ? { kind: "world", anchor: null, safe_area: null, z: node.worldRoot.z } : null,
|
|
742
|
+
intrinsic: measured?.intrinsic.get(node.id) ?? null,
|
|
743
|
+
source: site === void 0 ? null : parseSite(site[1]),
|
|
744
|
+
exempt: exempt.get(node.id) ?? []
|
|
745
|
+
};
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
var SNIPPET = 32;
|
|
749
|
+
function quote(text) {
|
|
750
|
+
const flat = text.replace(/\s+/g, " ");
|
|
751
|
+
return JSON.stringify(flat.length > SNIPPET ? `${flat.slice(0, SNIPPET - 1)}\u2026` : flat);
|
|
752
|
+
}
|
|
753
|
+
function describeNode(node) {
|
|
754
|
+
const parts = [`${node.kind} #${node.id}`];
|
|
755
|
+
if (node.name !== null) parts.push(node.name);
|
|
756
|
+
if (node.text !== null && node.text.length > 0) parts.push(quote(node.text));
|
|
757
|
+
else if (node.label !== null && node.label.length > 0) parts.push(quote(node.label));
|
|
758
|
+
return parts.join(" ");
|
|
759
|
+
}
|
|
760
|
+
function sourceText(source) {
|
|
761
|
+
return source === null ? null : `${source.file}:${source.line}:${source.column}`;
|
|
762
|
+
}
|
|
763
|
+
function rectText(rect) {
|
|
764
|
+
return `${rect.x},${rect.y} ${rect.width}x${rect.height}`;
|
|
765
|
+
}
|
|
766
|
+
var COMPACT_STYLE_KEYS = ["fill", "textColor", "fontSize", "opacity", "direction", "padding", "gap", "radius", "clip", "hidden", "displayNone"];
|
|
767
|
+
function compactTree(nodes, viewport, header, marks) {
|
|
768
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
769
|
+
const lines = [
|
|
770
|
+
`${header} viewport ${viewport.width}x${viewport.height}@${viewport.scale} safe ${viewport.insets.top}/${viewport.insets.right}/${viewport.insets.bottom}/${viewport.insets.left}`
|
|
771
|
+
];
|
|
772
|
+
const visit = (node, depth) => {
|
|
773
|
+
const parts = [`${" ".repeat(depth)}#${node.id} ${node.kind}`];
|
|
774
|
+
if (node.name !== null) parts.push(node.name);
|
|
775
|
+
if (node.text !== null && node.text.length > 0) parts.push(quote(node.text));
|
|
776
|
+
else if (node.label !== null && node.label.length > 0) parts.push(`label=${quote(node.label)}`);
|
|
777
|
+
parts.push(node.rect === null ? "(not drawn)" : rectText(node.rect));
|
|
778
|
+
if (node.z !== null) parts.push(`z${node.z}`);
|
|
779
|
+
if (node.root !== null) parts.push(node.root.kind === "world" ? "world-root" : `root:${node.root.anchor ?? "?"}${node.root.safe_area ? "+safe" : ""}`);
|
|
780
|
+
if (node.gestures.length > 0) parts.push(node.gestures.join("+"));
|
|
781
|
+
if (node.role !== "none" && node.role !== "group") parts.push(`role:${node.role}`);
|
|
782
|
+
if (node.state.disabled) parts.push("disabled");
|
|
783
|
+
if (node.state.focused) parts.push("focused");
|
|
784
|
+
for (const key of COMPACT_STYLE_KEYS) {
|
|
785
|
+
const value = node.style[key];
|
|
786
|
+
if (value === void 0 || value === false) continue;
|
|
787
|
+
parts.push(`${key}=${typeof value === "object" ? JSON.stringify(value) : String(value)}`);
|
|
788
|
+
}
|
|
789
|
+
const source = sourceText(node.source);
|
|
790
|
+
if (source !== null) parts.push(`@${source}`);
|
|
791
|
+
for (const mark of marks.get(node.id) ?? []) parts.push(`!${mark}`);
|
|
792
|
+
lines.push(parts.join(" "));
|
|
793
|
+
for (const child of node.children) {
|
|
794
|
+
const next = byId.get(child);
|
|
795
|
+
if (next !== void 0) visit(next, depth + 1);
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
const roots = nodes.filter((n) => n.parent === null || !byId.has(n.parent));
|
|
799
|
+
for (const root of roots) visit(root, 0);
|
|
800
|
+
return lines.join("\n");
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/interactives/inspect/checks.ts
|
|
804
|
+
var UI_EXEMPT_REASONS = {
|
|
805
|
+
text_overflow: ["intentional_clip", "marquee", "decorative"],
|
|
806
|
+
tap_target: ["expanded_hit_area", "dense_grid", "secondary_control", "decorative"],
|
|
807
|
+
contrast: ["decorative", "disabled", "logo", "inside_image"],
|
|
808
|
+
safe_area: ["full_bleed_background", "dismissible_overlay", "decorative"],
|
|
809
|
+
text_size: ["badge_counter", "legal_fine_print", "decorative"]
|
|
810
|
+
};
|
|
811
|
+
var MIN_TAP_PT = 44;
|
|
812
|
+
var MIN_TEXT_PT = 11;
|
|
813
|
+
var OVERFLOW_ALLOWANCE_PT = 0.5;
|
|
814
|
+
var SAFE_ALLOWANCE_PT = 0.5;
|
|
815
|
+
function ceil(value) {
|
|
816
|
+
return Math.ceil(value);
|
|
817
|
+
}
|
|
818
|
+
function fixed(value) {
|
|
819
|
+
return Math.round(value * 100) / 100;
|
|
820
|
+
}
|
|
821
|
+
function intersects(a, b) {
|
|
822
|
+
return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
|
|
823
|
+
}
|
|
824
|
+
function overhang(inner, outer) {
|
|
825
|
+
return Math.max(outer.x - inner.x, outer.y - inner.y, inner.x + inner.width - (outer.x + outer.width), inner.y + inner.height - (outer.y + outer.height), 0);
|
|
826
|
+
}
|
|
827
|
+
function inWorld(node, byId) {
|
|
828
|
+
let cursor = node;
|
|
829
|
+
while (cursor !== void 0) {
|
|
830
|
+
if (cursor.root?.kind === "world") return true;
|
|
831
|
+
cursor = cursor.parent === null ? void 0 : byId.get(cursor.parent);
|
|
832
|
+
}
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
function textOverflow(node, model, byId, viewport, out) {
|
|
836
|
+
const rect = node.rect;
|
|
837
|
+
const base = { check: "text_overflow", node: node.id, element: describeNode(node), source: sourceText(node.source) };
|
|
838
|
+
const intrinsic = node.intrinsic;
|
|
839
|
+
const wraps = (word(model, UiProp.text_wrap) ?? 0) !== 0;
|
|
840
|
+
const size = fontSize(model);
|
|
841
|
+
if (intrinsic !== null) {
|
|
842
|
+
const overX = intrinsic.width - rect.width;
|
|
843
|
+
const overY = intrinsic.height - rect.height;
|
|
844
|
+
if (overX > OVERFLOW_ALLOWANCE_PT || overY > OVERFLOW_ALLOWANCE_PT) {
|
|
845
|
+
const smaller = Math.max(1, Math.floor(size * Math.min(rect.width / Math.max(intrinsic.width, 1), rect.height / Math.max(intrinsic.height, 1))));
|
|
846
|
+
const fix = overX > OVERFLOW_ALLOWANCE_PT && !wraps ? `set textWrap: true (class "text-wrap") so the text breaks onto more lines, widen the box to at least ${ceil(intrinsic.width)} pt, or lower fontSize to ${smaller}` : `remove the fixed height or raise it to at least ${ceil(intrinsic.height)} pt, or lower fontSize to ${smaller}`;
|
|
847
|
+
out.push({
|
|
848
|
+
...base,
|
|
849
|
+
message: `text needs ${fixed(intrinsic.width)}x${fixed(intrinsic.height)} pt but its box is ${rect.width}x${rect.height} pt`,
|
|
850
|
+
fix,
|
|
851
|
+
measured: { needed_width: fixed(intrinsic.width), needed_height: fixed(intrinsic.height), width: rect.width, height: rect.height }
|
|
852
|
+
});
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
let cursor = node.parent === null ? void 0 : byId.get(node.parent);
|
|
857
|
+
while (cursor !== void 0) {
|
|
858
|
+
const clips = cursor.style.clip === true || cursor.kind === "scroll";
|
|
859
|
+
if (clips && cursor.kind !== "scroll" && cursor.rect !== null) {
|
|
860
|
+
const cut = overhang(rect, cursor.rect);
|
|
861
|
+
if (cut > OVERFLOW_ALLOWANCE_PT) {
|
|
862
|
+
out.push({
|
|
863
|
+
...base,
|
|
864
|
+
message: `text is clipped by ${describeNode(cursor)} by ${fixed(cut)} pt`,
|
|
865
|
+
fix: `let ${describeNode(cursor)} grow (remove its fixed width or height), give the text textWrap: true, or lower fontSize to ${Math.max(1, Math.floor(size * 0.85))}`,
|
|
866
|
+
measured: { clipped_by: cursor.id, overhang: fixed(cut) }
|
|
867
|
+
});
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
cursor = cursor.parent === null ? void 0 : byId.get(cursor.parent);
|
|
872
|
+
}
|
|
873
|
+
const screen = { x: 0, y: 0, width: viewport.width, height: viewport.height };
|
|
874
|
+
const off = overhang(rect, screen);
|
|
875
|
+
if (off > OVERFLOW_ALLOWANCE_PT && !inWorld(node, byId)) {
|
|
876
|
+
out.push({
|
|
877
|
+
...base,
|
|
878
|
+
message: `text runs ${fixed(off)} pt off the ${viewport.width}x${viewport.height} screen`,
|
|
879
|
+
fix: `constrain its container with maxWidth: "100%" and textWrap: true, or anchor its root inside the screen`,
|
|
880
|
+
measured: { overhang: fixed(off) }
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function tapTarget(node, out) {
|
|
885
|
+
const rect = node.rect;
|
|
886
|
+
if (rect.width >= MIN_TAP_PT && rect.height >= MIN_TAP_PT) return;
|
|
887
|
+
const padX = Math.max(0, ceil((MIN_TAP_PT - rect.width) / 2));
|
|
888
|
+
const padY = Math.max(0, ceil((MIN_TAP_PT - rect.height) / 2));
|
|
889
|
+
out.push({
|
|
890
|
+
check: "tap_target",
|
|
891
|
+
node: node.id,
|
|
892
|
+
element: describeNode(node),
|
|
893
|
+
source: sourceText(node.source),
|
|
894
|
+
message: `tap target is ${rect.width}x${rect.height} pt, under the ${MIN_TAP_PT}x${MIN_TAP_PT} pt minimum`,
|
|
895
|
+
fix: `set minWidth: ${MIN_TAP_PT} and minHeight: ${MIN_TAP_PT} (class "min-w-11 min-h-11"), or add padding of ${padY} pt vertically and ${padX} pt horizontally`,
|
|
896
|
+
measured: { width: rect.width, height: rect.height, minimum: MIN_TAP_PT }
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
function layerColor(model) {
|
|
900
|
+
if (model.kind === "image") return "unknown";
|
|
901
|
+
const gradient = model.props.get(UiProp.gradient);
|
|
902
|
+
if (gradient !== void 0 && gradient.words.length > 0) return "unknown";
|
|
903
|
+
const fill = word(model, UiProp.fill);
|
|
904
|
+
if (fill === null) return null;
|
|
905
|
+
const color = fromWord(fill);
|
|
906
|
+
const opacity = float(model, UiProp.opacity) ?? 1;
|
|
907
|
+
return { ...color, a: color.a * opacity };
|
|
908
|
+
}
|
|
909
|
+
function background(start, ui) {
|
|
910
|
+
const layers = [];
|
|
911
|
+
let cursor = start;
|
|
912
|
+
while (cursor !== void 0) {
|
|
913
|
+
const layer = layerColor(cursor);
|
|
914
|
+
if (layer === "unknown") return null;
|
|
915
|
+
if (layer !== null && layer.a > 0) {
|
|
916
|
+
layers.push(layer);
|
|
917
|
+
if (layer.a >= 1) {
|
|
918
|
+
let color = layers[layers.length - 1];
|
|
919
|
+
for (let i = layers.length - 2; i >= 0; i--) color = over(layers[i], color);
|
|
920
|
+
return color;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
cursor = cursor.parent === null ? void 0 : ui.nodes.get(cursor.parent);
|
|
924
|
+
}
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
function contrast(node, model, input, out, unresolved) {
|
|
928
|
+
const rect = node.rect;
|
|
929
|
+
const fg = fromWord(textColor(model));
|
|
930
|
+
const own = float(model, UiProp.opacity) ?? 1;
|
|
931
|
+
const foreground = { ...fg, a: fg.a * own };
|
|
932
|
+
let bg = background(model, input.model);
|
|
933
|
+
let sampled = false;
|
|
934
|
+
if (bg === null && input.screenshot !== null) {
|
|
935
|
+
const s = input.viewport.scale;
|
|
936
|
+
bg = dominantColor(input.screenshot.pixels, input.screenshot.width, input.screenshot.height, { x0: rect.x * s, y0: rect.y * s, x1: (rect.x + rect.width) * s, y1: (rect.y + rect.height) * s });
|
|
937
|
+
sampled = bg !== null;
|
|
938
|
+
}
|
|
939
|
+
if (bg === null) {
|
|
940
|
+
unresolved.push({ check: "contrast", node: node.id, element: describeNode(node), why: "the background is the game scene, an image or a gradient and no screenshot was taken" });
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const size = fontSize(model);
|
|
944
|
+
const weight = fontWeight(model);
|
|
945
|
+
const required = requiredContrast(size, weight);
|
|
946
|
+
const ratio = contrastRatio(foreground, bg);
|
|
947
|
+
if (ratio >= required) return;
|
|
948
|
+
const suggestion = passingColor(foreground, bg, required);
|
|
949
|
+
out.push({
|
|
950
|
+
check: "contrast",
|
|
951
|
+
node: node.id,
|
|
952
|
+
element: describeNode(node),
|
|
953
|
+
source: sourceText(node.source),
|
|
954
|
+
message: `contrast is ${fixed(ratio)}:1 (${toHex(foreground)} on ${toHex(bg)}${sampled ? ", background sampled from the frame" : ""}), under ${required}:1 for ${required === 3 ? "large" : "body"} text`,
|
|
955
|
+
fix: `use textColor ${toHex(suggestion)} (${fixed(contrastRatio(suggestion, bg))}:1), or darken or lighten the background behind it`,
|
|
956
|
+
measured: { ratio: fixed(ratio), required, foreground: toHex(foreground), background: toHex(bg), font_size: size }
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
function safeArea(node, viewport, out) {
|
|
960
|
+
const rect = node.rect;
|
|
961
|
+
const insets = viewport.insets;
|
|
962
|
+
const landscape = viewport.width > viewport.height;
|
|
963
|
+
const bands = [];
|
|
964
|
+
if (insets.top > 0) bands.push({ name: landscape ? "status bar" : "notch", band: { x: 0, y: 0, width: viewport.width, height: insets.top }, move: "down", amount: insets.top - rect.y });
|
|
965
|
+
if (insets.bottom > 0) bands.push({ name: "home indicator", band: { x: 0, y: viewport.height - insets.bottom, width: viewport.width, height: insets.bottom }, move: "up", amount: rect.y + rect.height - (viewport.height - insets.bottom) });
|
|
966
|
+
if (insets.left > 0) bands.push({ name: "notch", band: { x: 0, y: 0, width: insets.left, height: viewport.height }, move: "right", amount: insets.left - rect.x });
|
|
967
|
+
if (insets.right > 0) bands.push({ name: landscape ? "notch" : "screen edge", band: { x: viewport.width - insets.right, y: 0, width: insets.right, height: viewport.height }, move: "left", amount: rect.x + rect.width - (viewport.width - insets.right) });
|
|
968
|
+
for (const band of bands) {
|
|
969
|
+
if (!intersects(rect, band.band) || band.amount <= SAFE_ALLOWANCE_PT) continue;
|
|
970
|
+
out.push({
|
|
971
|
+
check: "safe_area",
|
|
972
|
+
node: node.id,
|
|
973
|
+
element: describeNode(node),
|
|
974
|
+
source: sourceText(node.source),
|
|
975
|
+
message: `${node.interactive ? "interactive" : "text"} node sits ${fixed(band.amount)} pt under the ${band.name}`,
|
|
976
|
+
fix: `root it with ui.root(element, { safe_area: true }), or add @safe padding on its container, for example { padding: { default: 8, "@safe": 8 } }; it needs to move ${band.move} ${ceil(band.amount)} pt`,
|
|
977
|
+
measured: { overlap: fixed(band.amount), inset: band.name === "home indicator" ? insets.bottom : band.move === "down" ? insets.top : band.move === "right" ? insets.left : insets.right }
|
|
978
|
+
});
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
function textSize(node, model, out) {
|
|
983
|
+
const size = fontSize(model);
|
|
984
|
+
if (size >= MIN_TEXT_PT) return;
|
|
985
|
+
out.push({
|
|
986
|
+
check: "text_size",
|
|
987
|
+
node: node.id,
|
|
988
|
+
element: describeNode(node),
|
|
989
|
+
source: sourceText(node.source),
|
|
990
|
+
message: `text is ${size} pt, under the ${MIN_TEXT_PT} pt minimum`,
|
|
991
|
+
fix: `use fontSize: 12 or more (class "text-xs" is 12 pt)`,
|
|
992
|
+
measured: { font_size: size, minimum: MIN_TEXT_PT }
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
function runChecks(input) {
|
|
996
|
+
const byId = new Map(input.nodes.map((n) => [n.id, n]));
|
|
997
|
+
const found = [];
|
|
998
|
+
const unresolved = [];
|
|
999
|
+
for (const node of input.nodes) {
|
|
1000
|
+
const model = input.model.nodes.get(node.id);
|
|
1001
|
+
if (model === void 0 || !node.visible || node.rect === null) continue;
|
|
1002
|
+
const isText = (node.kind === "text" || node.kind === "input") && (node.text ?? "").trim().length > 0;
|
|
1003
|
+
if (isText) {
|
|
1004
|
+
textOverflow(node, model, byId, input.viewport, found);
|
|
1005
|
+
textSize(node, model, found);
|
|
1006
|
+
if (!node.state.disabled) contrast(node, model, input, found, unresolved);
|
|
1007
|
+
}
|
|
1008
|
+
if (node.interactive && !node.state.disabled) tapTarget(node, found);
|
|
1009
|
+
if ((isText || node.interactive) && !inWorld(node, byId)) safeArea(node, input.viewport, found);
|
|
1010
|
+
}
|
|
1011
|
+
const findings = [];
|
|
1012
|
+
const exempted = [];
|
|
1013
|
+
const invalid = [];
|
|
1014
|
+
for (const node of input.nodes) {
|
|
1015
|
+
for (const exemption of node.exempt) {
|
|
1016
|
+
const reasons = UI_EXEMPT_REASONS[exemption.check];
|
|
1017
|
+
if (reasons === void 0 || !reasons.includes(exemption.reason)) invalid.push(`${describeNode(node)}: "${exemption.reason}" is not a reason to exempt ${exemption.check}`);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
for (const finding of found) {
|
|
1021
|
+
const node = byId.get(finding.node);
|
|
1022
|
+
const exemption = node?.exempt.find((e) => e.check === finding.check && UI_EXEMPT_REASONS[finding.check].includes(e.reason));
|
|
1023
|
+
if (exemption === void 0) findings.push(finding);
|
|
1024
|
+
else exempted.push({ ...finding, reason: exemption.reason });
|
|
1025
|
+
}
|
|
1026
|
+
return { findings, exempted, unresolved, invalid_exemptions: invalid };
|
|
1027
|
+
}
|
|
1028
|
+
function findingMarks(report) {
|
|
1029
|
+
const marks = /* @__PURE__ */ new Map();
|
|
1030
|
+
for (const finding of report.findings) {
|
|
1031
|
+
const list = marks.get(finding.node) ?? [];
|
|
1032
|
+
list.push(finding.check);
|
|
1033
|
+
marks.set(finding.node, list);
|
|
1034
|
+
}
|
|
1035
|
+
return marks;
|
|
1036
|
+
}
|
|
1037
|
+
function formatFinding(finding) {
|
|
1038
|
+
return `${finding.source ?? "(no source)"}: ${finding.check}: ${finding.element}: ${finding.message}. Fix: ${finding.fix}`;
|
|
1039
|
+
}
|
|
1040
|
+
var RECORD_BYTES = 52;
|
|
1041
|
+
var SETTLE_MICROS = 1e6;
|
|
1042
|
+
var SETTLE_FRAMES = 2;
|
|
1043
|
+
var UNIT_AUTO = 0;
|
|
1044
|
+
var UNIT_POINTS2 = 1;
|
|
1045
|
+
var ALIGN_START = 1;
|
|
1046
|
+
var PATCH_BYTES = 64 * 1024;
|
|
1047
|
+
var WORD2 = 4;
|
|
1048
|
+
var RECORD_HEADER_WORDS = 2;
|
|
1049
|
+
var MAX_COMPACT_WORDS = 255;
|
|
1050
|
+
var VERSION_OFFSET = 4;
|
|
1051
|
+
var COUNT_OFFSET2 = 8;
|
|
1052
|
+
var TEXT_KINDS = /* @__PURE__ */ new Set(["text", "input"]);
|
|
1053
|
+
var decoder2 = new TextDecoder();
|
|
1054
|
+
function round(value) {
|
|
1055
|
+
return Math.round(value * 100) / 100;
|
|
1056
|
+
}
|
|
1057
|
+
function decodeShadow(exported) {
|
|
1058
|
+
const records = Buffer.from(exported.records, "base64");
|
|
1059
|
+
const strings = Buffer.from(exported.strings, "base64");
|
|
1060
|
+
const view = new DataView(records.buffer, records.byteOffset, records.byteLength);
|
|
1061
|
+
const text = (offset, length) => length === 0 ? "" : decoder2.decode(strings.subarray(offset, offset + length));
|
|
1062
|
+
const out = [];
|
|
1063
|
+
for (let at = 0; at + RECORD_BYTES <= records.byteLength; at += RECORD_BYTES) {
|
|
1064
|
+
const u = (i) => view.getUint32(at + i * 4, true);
|
|
1065
|
+
const f = (i) => view.getFloat32(at + i * 4, true);
|
|
1066
|
+
out.push({
|
|
1067
|
+
node: u(0),
|
|
1068
|
+
parent: u(1),
|
|
1069
|
+
role: u(2),
|
|
1070
|
+
traits: u(3),
|
|
1071
|
+
rect: { x: round(f(5)), y: round(f(6)), width: round(f(7)), height: round(f(8)) },
|
|
1072
|
+
label: text(u(9), u(10)),
|
|
1073
|
+
value: text(u(11), u(12))
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
return out;
|
|
1077
|
+
}
|
|
1078
|
+
function encoded(writer) {
|
|
1079
|
+
if (writer.empty) return null;
|
|
1080
|
+
const length = writer.finish();
|
|
1081
|
+
return Buffer.from(writer.bytes.subarray(0, length)).toString("base64");
|
|
1082
|
+
}
|
|
1083
|
+
function preparePatch(model) {
|
|
1084
|
+
const writer = new PatchWriter(PATCH_BYTES);
|
|
1085
|
+
for (const node of model.nodes.values()) {
|
|
1086
|
+
if ((word(node, UiProp.role) ?? UiRole.none) === UiRole.none) writer.propWord(node.id, UiProp.role, UiRole.group);
|
|
1087
|
+
const bits2 = traits(node);
|
|
1088
|
+
if ((bits2 & UiTrait.hidden) !== 0) writer.propWord(node.id, UiProp.traits, bits2 & ~UiTrait.hidden);
|
|
1089
|
+
if (TEXT_KINDS.has(node.kind) && node.label !== null) writer.label(node.id, "");
|
|
1090
|
+
}
|
|
1091
|
+
return encoded(writer);
|
|
1092
|
+
}
|
|
1093
|
+
var f323 = new Float32Array(1);
|
|
1094
|
+
var u323 = new Uint32Array(f323.buffer);
|
|
1095
|
+
function bits(value) {
|
|
1096
|
+
f323[0] = value;
|
|
1097
|
+
return u323[0];
|
|
1098
|
+
}
|
|
1099
|
+
function entry(prop, unit, value) {
|
|
1100
|
+
return { prop, unit, refs: 0, safe: 0, words: [value] };
|
|
1101
|
+
}
|
|
1102
|
+
function styleRecords(records) {
|
|
1103
|
+
if (records.length === 0) return null;
|
|
1104
|
+
const total = records.reduce((sum, r) => sum + RECORD_HEADER_WORDS + (RECORD_HEADER_WORDS + r.words.length > MAX_COMPACT_WORDS ? 1 : 0) + r.words.length, 0);
|
|
1105
|
+
const view = new DataView(new ArrayBuffer(UI_HEADER_BYTES + total * WORD2));
|
|
1106
|
+
view.setUint32(0, UI_MAGIC, true);
|
|
1107
|
+
view.setUint16(VERSION_OFFSET, UI_VERSION, true);
|
|
1108
|
+
view.setUint32(COUNT_OFFSET2, records.length, true);
|
|
1109
|
+
let at = UI_HEADER_BYTES;
|
|
1110
|
+
for (const record of records) {
|
|
1111
|
+
const extended = RECORD_HEADER_WORDS + record.words.length > MAX_COMPACT_WORDS;
|
|
1112
|
+
view.setUint8(at, OP_STYLE);
|
|
1113
|
+
view.setUint8(at + 1, extended ? 0 : RECORD_HEADER_WORDS + record.words.length);
|
|
1114
|
+
view.setUint16(at + 2, record.mask, true);
|
|
1115
|
+
view.setUint32(at + 4, record.node, true);
|
|
1116
|
+
at += RECORD_HEADER_WORDS * WORD2;
|
|
1117
|
+
if (extended) {
|
|
1118
|
+
view.setUint32(at, RECORD_HEADER_WORDS + 1 + record.words.length, true);
|
|
1119
|
+
at += WORD2;
|
|
1120
|
+
}
|
|
1121
|
+
for (const w of record.words) {
|
|
1122
|
+
view.setUint32(at, w >>> 0, true);
|
|
1123
|
+
at += WORD2;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return Buffer.from(view.buffer).toString("base64");
|
|
1127
|
+
}
|
|
1128
|
+
function intrinsicPatch(model, rects) {
|
|
1129
|
+
const records = [];
|
|
1130
|
+
for (const node of model.nodes.values()) {
|
|
1131
|
+
if (!TEXT_KINDS.has(node.kind)) continue;
|
|
1132
|
+
const rect = rects.get(node.id);
|
|
1133
|
+
if (rect === void 0) continue;
|
|
1134
|
+
const wraps = (word(node, UiProp.text_wrap) ?? 0) !== 0;
|
|
1135
|
+
const overrides = [
|
|
1136
|
+
wraps ? entry(UiProp.width, UNIT_POINTS2, bits(rect.width)) : entry(UiProp.width, UNIT_AUTO, 0),
|
|
1137
|
+
entry(UiProp.max_width, UNIT_AUTO, 0),
|
|
1138
|
+
entry(UiProp.height, UNIT_AUTO, 0),
|
|
1139
|
+
entry(UiProp.max_height, UNIT_AUTO, 0),
|
|
1140
|
+
entry(UiProp.align_self, UNIT_AUTO, ALIGN_START),
|
|
1141
|
+
entry(UiProp.shrink, UNIT_AUTO, bits(0)),
|
|
1142
|
+
entry(UiProp.grow, UNIT_AUTO, bits(0))
|
|
1143
|
+
];
|
|
1144
|
+
const kept = node.variants.get(node.active) ?? [];
|
|
1145
|
+
const words = [];
|
|
1146
|
+
for (const e of [...kept, ...overrides]) words.push(...encodeEntry(e));
|
|
1147
|
+
records.push({ node: node.id, mask: node.active, words });
|
|
1148
|
+
}
|
|
1149
|
+
return styleRecords(records);
|
|
1150
|
+
}
|
|
1151
|
+
async function settled(driver) {
|
|
1152
|
+
let last = null;
|
|
1153
|
+
for (let i = 0; i < SETTLE_FRAMES; i++) last = await driver.frame(SETTLE_MICROS, true);
|
|
1154
|
+
return last === null ? [] : decodeShadow(last);
|
|
1155
|
+
}
|
|
1156
|
+
async function measure(model, driver) {
|
|
1157
|
+
if (!await driver.open()) return null;
|
|
1158
|
+
try {
|
|
1159
|
+
const prepare = preparePatch(model);
|
|
1160
|
+
if (prepare !== null) await driver.apply(prepare);
|
|
1161
|
+
const first = await settled(driver);
|
|
1162
|
+
const rects = /* @__PURE__ */ new Map();
|
|
1163
|
+
const paint = /* @__PURE__ */ new Map();
|
|
1164
|
+
const shown = /* @__PURE__ */ new Map();
|
|
1165
|
+
first.forEach((record, index) => {
|
|
1166
|
+
rects.set(record.node, record.rect);
|
|
1167
|
+
paint.set(record.node, index);
|
|
1168
|
+
shown.set(record.node, record.label);
|
|
1169
|
+
});
|
|
1170
|
+
const intrinsic = /* @__PURE__ */ new Map();
|
|
1171
|
+
const patch = intrinsicPatch(model, rects);
|
|
1172
|
+
if (patch !== null) {
|
|
1173
|
+
await driver.apply(patch);
|
|
1174
|
+
for (const record of await settled(driver)) {
|
|
1175
|
+
const node = model.nodes.get(record.node);
|
|
1176
|
+
if (node !== void 0 && TEXT_KINDS.has(node.kind)) intrinsic.set(record.node, record.rect);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return { rects, paint, shown, intrinsic };
|
|
1180
|
+
} finally {
|
|
1181
|
+
await driver.close();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/interactives/inspect/report.ts
|
|
1186
|
+
function patchesOf(snapshot) {
|
|
1187
|
+
return snapshot.patches.map((p) => new Uint8Array(Buffer.from(p, "base64")));
|
|
1188
|
+
}
|
|
1189
|
+
async function inspectSnapshot(snapshot, driver, device, screenshot) {
|
|
1190
|
+
const viewport = viewportOf(device, snapshot.viewport);
|
|
1191
|
+
const model = decodeStream(patchesOf(snapshot), viewport);
|
|
1192
|
+
const measured = snapshot.ui ? await measure(model, driver) : null;
|
|
1193
|
+
const nodes = buildNodes({ model, measured, sites: snapshot.sites, exempt: snapshot.exempt });
|
|
1194
|
+
const checks = runChecks({ model, nodes, viewport, screenshot });
|
|
1195
|
+
const problems = [...model.problems, ...snapshot.errors];
|
|
1196
|
+
if (!snapshot.ui) problems.push("the game has not created a game UI yet");
|
|
1197
|
+
else if (measured === null) problems.push("the engine could not lay out a copy of the game UI");
|
|
1198
|
+
return {
|
|
1199
|
+
model,
|
|
1200
|
+
report: {
|
|
1201
|
+
schema: INSPECT_SCHEMA,
|
|
1202
|
+
frame: snapshot.frame,
|
|
1203
|
+
time_ms: Math.round(snapshot.clock_ms * 1e3) / 1e3,
|
|
1204
|
+
device: device.name,
|
|
1205
|
+
viewport,
|
|
1206
|
+
scene: snapshot.scene,
|
|
1207
|
+
measured: measured !== null,
|
|
1208
|
+
nodes,
|
|
1209
|
+
checks,
|
|
1210
|
+
problems
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
function compactReport(report) {
|
|
1215
|
+
const scene = report.scene.current ?? report.scene.applied ?? null;
|
|
1216
|
+
const header = `frame ${report.frame} t=${report.time_ms}ms device ${report.device}${scene === null ? "" : ` scene ${scene}`}`;
|
|
1217
|
+
const tree = compactTree(report.nodes, report.viewport, header, findingMarks(report.checks));
|
|
1218
|
+
const lines = [tree];
|
|
1219
|
+
if (report.checks.findings.length > 0) {
|
|
1220
|
+
lines.push("findings:");
|
|
1221
|
+
for (const f of report.checks.findings) lines.push(`- ${f.check} #${f.node}${f.source === null ? "" : ` @${f.source}`}: ${f.message}; fix: ${f.fix}`);
|
|
1222
|
+
}
|
|
1223
|
+
for (const p of report.problems) lines.push(`problem: ${p}`);
|
|
1224
|
+
return lines.join("\n");
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
export { compactReport, formatFinding, inspectSnapshot, patchesOf };
|