sootsim 0.1.240 → 0.1.241
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cli/bin.js +4 -4
- package/dist-cli/chunks/{agent-YS355M7O.js → agent-7Z3WG3YJ.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-DJJH2DE5.js → agent-wrapper-IMTGEJ7T.js} +2 -2
- package/dist-cli/chunks/{app-fonts-2KURCNO7.js → app-fonts-7E66PSAV.js} +2 -2
- package/dist-cli/chunks/{assert-Y7SV6LAY.js → assert-IEBAQZC7.js} +2 -2
- package/dist-cli/chunks/{auth-5VMBSD6F.js → auth-FRMHPA2X.js} +2 -2
- package/dist-cli/chunks/beta-AMMCLKHD.js +2 -0
- package/dist-cli/chunks/{camera-WCQNMZRH.js → camera-O67TQUAH.js} +2 -2
- package/dist-cli/chunks/{chunk-ZYMOUSJC.js → chunk-2DY3JHFK.js} +1 -1
- package/dist-cli/chunks/{chunk-BWR3AJZ3.js → chunk-4JSCYLLH.js} +1 -1
- package/dist-cli/chunks/{chunk-AX56F2Z5.js → chunk-4V6MFWDY.js} +1 -1
- package/dist-cli/chunks/{chunk-OBQJXFV3.js → chunk-5COOJUTE.js} +1 -1
- package/dist-cli/chunks/{chunk-SFNTUDIP.js → chunk-5ZNYFTOB.js} +2 -2
- package/dist-cli/chunks/{chunk-KY2HHVDG.js → chunk-7I3PV2RE.js} +2 -2
- package/dist-cli/chunks/{chunk-XVE2O2IR.js → chunk-AROVTS3P.js} +2 -2
- package/dist-cli/chunks/{chunk-DQEI7LOC.js → chunk-BIHEU677.js} +3 -3
- package/dist-cli/chunks/{chunk-4FUKKXDQ.js → chunk-CAOW2O5V.js} +2 -2
- package/dist-cli/chunks/{chunk-2ZEVD46I.js → chunk-CSZOZRKB.js} +3 -3
- package/dist-cli/chunks/{chunk-GW7REFUB.js → chunk-DS2KFLBO.js} +1 -1
- package/dist-cli/chunks/chunk-ECRSTV5C.js +118 -0
- package/dist-cli/chunks/{chunk-OUW757HM.js → chunk-EY3ZQWUQ.js} +52 -52
- package/dist-cli/chunks/{chunk-H6GS5C4F.js → chunk-F5YD4BLA.js} +2 -2
- package/dist-cli/chunks/{chunk-TFXCEOCW.js → chunk-FR7JCFAR.js} +1 -1
- package/dist-cli/chunks/{chunk-LBOSTYWQ.js → chunk-GFZLDVLE.js} +1 -1
- package/dist-cli/chunks/{chunk-OKHNWLZ7.js → chunk-GVTSKLLS.js} +2 -2
- package/dist-cli/chunks/{chunk-XV4VXQDA.js → chunk-H7G2QGUG.js} +2 -2
- package/dist-cli/chunks/{chunk-XSSTKHK7.js → chunk-H7SP6J3H.js} +2 -2
- package/dist-cli/chunks/{chunk-C4MSUAGM.js → chunk-HNI6U6M5.js} +1 -1
- package/dist-cli/chunks/{chunk-HIFBYPJK.js → chunk-IF5VXL45.js} +2 -2
- package/dist-cli/chunks/{chunk-5LCZXDDX.js → chunk-JMTQBHQY.js} +1 -1
- package/dist-cli/chunks/{chunk-T7BLBFPG.js → chunk-JX4N376W.js} +2 -2
- package/dist-cli/chunks/{chunk-WYT6JDAV.js → chunk-KFI26HW5.js} +1 -1
- package/dist-cli/chunks/chunk-KLXLOMI6.js +1 -0
- package/dist-cli/chunks/{chunk-DEO54ZY3.js → chunk-KN3Q6VSC.js} +2 -2
- package/dist-cli/chunks/{chunk-EJ6ULQFO.js → chunk-LVUOVKJO.js} +2 -2
- package/dist-cli/chunks/{chunk-SW5HPJC7.js → chunk-MCAJBAWD.js} +2 -2
- package/dist-cli/chunks/{chunk-JDRG4T3G.js → chunk-MGLTIS7P.js} +2 -2
- package/dist-cli/chunks/{chunk-NJYDI627.js → chunk-PR6AOTP5.js} +1 -1
- package/dist-cli/chunks/{chunk-GYWIGJB7.js → chunk-PVUUG5YW.js} +2 -2
- package/dist-cli/chunks/{chunk-6XHVSTMH.js → chunk-R3EZVIEM.js} +2 -2
- package/dist-cli/chunks/{chunk-4F63QBYN.js → chunk-U7XJNNGI.js} +3 -3
- package/dist-cli/chunks/{chunk-VU5WTUZJ.js → chunk-UF333CGY.js} +2 -2
- package/dist-cli/chunks/{chunk-3ULI43M6.js → chunk-VVUANJFA.js} +72 -23
- package/dist-cli/chunks/{chunk-2QILUPQJ.js → chunk-WFB3ICCN.js} +1 -1
- package/dist-cli/chunks/{chunk-M3VY5LEN.js → chunk-WIHKSZM4.js} +1 -1
- package/dist-cli/chunks/{chunk-KENWUOKC.js → chunk-WPWLJFKF.js} +2 -2
- package/dist-cli/chunks/{chunk-M2MJXMTO.js → chunk-YLWUYESW.js} +1 -1
- package/dist-cli/chunks/{chunk-7ZTOWAQF.js → chunk-YX7S6N42.js} +2 -2
- package/dist-cli/chunks/{chunk-M2OTE66X.js → chunk-Z72IF5FC.js} +2 -2
- package/dist-cli/chunks/chunk-ZGSGMGE3.js +1 -0
- package/dist-cli/chunks/{chunk-ILU6JNUJ.js → chunk-ZUZLQ4ZJ.js} +1 -1
- package/dist-cli/chunks/{chunk-GI36DI2I.js → chunk-ZYJCVJFP.js} +1 -1
- package/dist-cli/chunks/cli-version-DIYZFJMN.js +2 -0
- package/dist-cli/chunks/{compat-LUERFKMW.js → compat-ZRUVBIK7.js} +3 -3
- package/dist-cli/chunks/{config-U4Z6TYT6.js → config-VKQDFC5Z.js} +2 -2
- package/dist-cli/chunks/{control-I5YB63IZ.js → control-OYK2J4JW.js} +2 -2
- package/dist-cli/chunks/daemon-YQLY7PVM.js +2 -0
- package/dist-cli/chunks/{debug-HXECWDLV.js → debug-3WEO7ZRW.js} +3 -3
- package/dist-cli/chunks/{desktop-NZJLDJIO.js → desktop-DVOFHJKU.js} +3 -3
- package/dist-cli/chunks/{detox-LUOT4TIA.js → detox-GAGC276D.js} +2 -2
- package/dist-cli/chunks/{device-7TZ7BD36.js → device-256KFP2G.js} +2 -2
- package/dist-cli/chunks/{diagnose-DW2ANEGF.js → diagnose-GYKUU3N5.js} +2 -2
- package/dist-cli/chunks/drivers-FHWFHVQF.js +2 -0
- package/dist-cli/chunks/{film-6MYMKZ3N.js → film-WAV3WPQK.js} +3 -3
- package/dist-cli/chunks/flow-WKPPIDM6.js +2 -0
- package/dist-cli/chunks/help-P4A7NNSC.js +2 -0
- package/dist-cli/chunks/{hidden-runtime-alias-FUDVEMFB.js → hidden-runtime-alias-DODMMNOJ.js} +2 -2
- package/dist-cli/chunks/{home-paths-KU4MY22X.js → home-paths-QU5UX3MT.js} +2 -2
- package/dist-cli/chunks/inspect-NIHY67NU.js +2 -0
- package/dist-cli/chunks/install-desktop-NA53KD6G.js +2 -0
- package/dist-cli/chunks/{login-EVS4SZQN.js → login-OURQ3DVM.js} +4 -4
- package/dist-cli/chunks/{logout-K5MLSAWM.js → logout-ZYVJAPBO.js} +2 -2
- package/dist-cli/chunks/{maestro-OEDT2T7V.js → maestro-H2NGXNF4.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-T4RZSVSM.js → maestro-generate-DA2IZPK2.js} +3 -3
- package/dist-cli/chunks/{mode-TZU7JML2.js → mode-QTK3ERUW.js} +2 -2
- package/dist-cli/chunks/optional-demo-registry-ZLN23OYV.js +2 -0
- package/dist-cli/chunks/{perf-YNBU57WH.js → perf-WZ4OPYXV.js} +2 -2
- package/dist-cli/chunks/{record-V6T2INPF.js → record-ZQSUHQJU.js} +3 -3
- package/dist-cli/chunks/runtime-RM67M6H3.js +2 -0
- package/dist-cli/chunks/{screenshot-command-CZZ6IXSD.js → screenshot-command-UDJ6TEAM.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-WVR5FCHX.js → screenshots-capture-4FK4NYXG.js} +2 -2
- package/dist-cli/chunks/{serve-6I35UUFI.js → serve-MCT4HKR7.js} +2 -2
- package/dist-cli/chunks/{setup-O7GD6GME.js → setup-I2IELNJN.js} +3 -3
- package/dist-cli/chunks/{skills-GBTBS2OK.js → skills-PVXI7LM6.js} +2 -2
- package/dist-cli/chunks/{storage-FI2YOPQE.js → storage-KZFCVRGD.js} +2 -2
- package/dist-cli/chunks/store-CEMC5MTK.js +2 -0
- package/dist-cli/chunks/telemetry-LFIQWPZD.js +2 -0
- package/dist-cli/chunks/{timeline-E5DVVATK.js → timeline-YAULXFEC.js} +2 -2
- package/dist-cli/chunks/{upgrade-QKOI3Q4U.js → upgrade-63TVREBM.js} +2 -2
- package/dist-cli/chunks/upload-APW46WK3.js +2 -0
- package/dist-cli/chunks/{version-UANAPNZM.js → version-JWWEGZS7.js} +2 -2
- package/dist-cli/chunks/{web-4LZ7DUUS.js → web-XO724HJL.js} +2 -2
- package/dist-cli/chunks/{what-happened-JOLNICMX.js → what-happened-DQ4MXAPJ.js} +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +157 -5
- package/dist-lib/sdk.mjs +156 -5
- package/dist-lib/skills.cjs +144 -4
- package/dist-lib/vite.cjs +1 -1
- package/package.json +1 -1
- package/skills/contrast/SKILL.md +1 -1
- package/skills/sootsim-debug/SKILL.md +21 -12
- package/dist-cli/chunks/beta-KMI4GCCN.js +0 -2
- package/dist-cli/chunks/chunk-3RYTIG4Y.js +0 -118
- package/dist-cli/chunks/chunk-R54EGANH.js +0 -1
- package/dist-cli/chunks/chunk-U3VMQUN6.js +0 -1
- package/dist-cli/chunks/cli-version-2BXTYTLJ.js +0 -2
- package/dist-cli/chunks/daemon-VTG3XKWA.js +0 -2
- package/dist-cli/chunks/drivers-RDXY5SDO.js +0 -2
- package/dist-cli/chunks/flow-KS77CBEA.js +0 -2
- package/dist-cli/chunks/help-H3ZD3LVG.js +0 -2
- package/dist-cli/chunks/inspect-ACDDO276.js +0 -2
- package/dist-cli/chunks/install-desktop-UQVXJAXS.js +0 -2
- package/dist-cli/chunks/optional-demo-registry-NIJ3MA5N.js +0 -2
- package/dist-cli/chunks/runtime-7XBSKGNO.js +0 -2
- package/dist-cli/chunks/store-FU7T5NPV.js +0 -2
- package/dist-cli/chunks/telemetry-REPVX6LA.js +0 -2
- package/dist-cli/chunks/upload-IFQM3FG4.js +0 -2
package/dist-lib/sdk.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! sootsim v0.1.
|
|
1
|
+
/*! sootsim v0.1.241 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
|
|
3
3
|
// cli/commands/inspect/core.ts
|
|
4
4
|
function resolveMaxMsFlag(args, fallbackMs) {
|
|
@@ -146,6 +146,101 @@ async function inspectDescribe(bridge, dumpOpts) {
|
|
|
146
146
|
const result = await bridge.send({ type: "evaluate", code });
|
|
147
147
|
return result ?? {};
|
|
148
148
|
}
|
|
149
|
+
function scoreLayoutTextContrast(input) {
|
|
150
|
+
const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
|
|
151
|
+
const { normal, background } = input;
|
|
152
|
+
if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
|
|
153
|
+
return { ratio: null, required, reason: "capture dimensions do not match" };
|
|
154
|
+
}
|
|
155
|
+
const left = Math.max(0, Math.floor(input.box.x));
|
|
156
|
+
const top = Math.max(0, Math.floor(input.box.y));
|
|
157
|
+
const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
|
|
158
|
+
const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
|
|
159
|
+
if (right <= left || bottom <= top) {
|
|
160
|
+
return { ratio: null, required, reason: "text is outside the captured frame" };
|
|
161
|
+
}
|
|
162
|
+
const linear = (value) => {
|
|
163
|
+
const channel = value / 255;
|
|
164
|
+
return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
|
|
165
|
+
};
|
|
166
|
+
const ratioOf = (foreground, behind) => {
|
|
167
|
+
const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
|
|
168
|
+
const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
|
|
169
|
+
return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
|
|
170
|
+
};
|
|
171
|
+
const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
|
|
172
|
+
const area = (right - left) * (bottom - top);
|
|
173
|
+
const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
|
|
174
|
+
const offsetAt = (x, y) => (y * normal.width + x) * 4;
|
|
175
|
+
const deltaAt = (offset) => Math.max(
|
|
176
|
+
Math.abs(normal.data[offset] - background.data[offset]),
|
|
177
|
+
Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
|
|
178
|
+
Math.abs(normal.data[offset + 2] - background.data[offset + 2])
|
|
179
|
+
);
|
|
180
|
+
let strongestDelta = 0;
|
|
181
|
+
let strongestForeground = null;
|
|
182
|
+
let opaqueBackground = null;
|
|
183
|
+
for (let y = top; y < bottom; y += step) {
|
|
184
|
+
for (let x = left; x < right; x += step) {
|
|
185
|
+
const offset = offsetAt(x, y);
|
|
186
|
+
if (background.data[offset + 3] < 250) continue;
|
|
187
|
+
const behind = [
|
|
188
|
+
background.data[offset],
|
|
189
|
+
background.data[offset + 1],
|
|
190
|
+
background.data[offset + 2]
|
|
191
|
+
];
|
|
192
|
+
opaqueBackground ?? (opaqueBackground = behind);
|
|
193
|
+
const delta = deltaAt(offset);
|
|
194
|
+
if (delta <= strongestDelta) continue;
|
|
195
|
+
strongestDelta = delta;
|
|
196
|
+
strongestForeground = [
|
|
197
|
+
normal.data[offset],
|
|
198
|
+
normal.data[offset + 1],
|
|
199
|
+
normal.data[offset + 2]
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
let bestRatio = Infinity;
|
|
204
|
+
let bestForeground = null;
|
|
205
|
+
let bestBackground = null;
|
|
206
|
+
const consider = (foreground, behind) => {
|
|
207
|
+
const ratio = ratioOf(foreground, behind);
|
|
208
|
+
if (ratio >= bestRatio) return;
|
|
209
|
+
bestRatio = ratio;
|
|
210
|
+
bestForeground = foreground;
|
|
211
|
+
bestBackground = behind;
|
|
212
|
+
};
|
|
213
|
+
if (strongestDelta >= 2 && strongestForeground) {
|
|
214
|
+
for (let y = top; y < bottom; y += step) {
|
|
215
|
+
for (let x = left; x < right; x += step) {
|
|
216
|
+
const offset = offsetAt(x, y);
|
|
217
|
+
if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
consider(strongestForeground, [
|
|
221
|
+
background.data[offset],
|
|
222
|
+
background.data[offset + 1],
|
|
223
|
+
background.data[offset + 2]
|
|
224
|
+
]);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} else if (opaqueBackground) {
|
|
228
|
+
consider(opaqueBackground, opaqueBackground);
|
|
229
|
+
}
|
|
230
|
+
if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
|
|
231
|
+
return {
|
|
232
|
+
ratio: null,
|
|
233
|
+
required,
|
|
234
|
+
reason: "could not isolate opaque rendered text pixels"
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
ratio: Math.round(bestRatio * 100) / 100,
|
|
239
|
+
required,
|
|
240
|
+
foreground: hex(bestForeground),
|
|
241
|
+
background: hex(bestBackground)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
149
244
|
function layoutGetNativeEval(opts = {}) {
|
|
150
245
|
const includeStyle = opts.styling === true;
|
|
151
246
|
const filter = opts.filter?.trim().toLowerCase() ?? "";
|
|
@@ -155,6 +250,7 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
155
250
|
const list = await t.listInspectable({})
|
|
156
251
|
if (!Array.isArray(list)) return []
|
|
157
252
|
const includeStyle = ${JSON.stringify(includeStyle)}
|
|
253
|
+
const scoreContrast = ${scoreLayoutTextContrast.toString()}
|
|
158
254
|
// narrows by ELEMENT, not by style field: fewer rows, full detail on each.
|
|
159
255
|
const filter = ${JSON.stringify(filter)}
|
|
160
256
|
const matchesFilter = (n, text) => {
|
|
@@ -199,7 +295,7 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
199
295
|
}
|
|
200
296
|
const weightOf = (style) => {
|
|
201
297
|
const w = style && style.fontWeight
|
|
202
|
-
const n = typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN)
|
|
298
|
+
const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
|
|
203
299
|
return Number.isFinite(n) && n >= 600 ? n : undefined
|
|
204
300
|
}
|
|
205
301
|
const decoOf = (style) => {
|
|
@@ -242,7 +338,7 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
242
338
|
return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
|
|
243
339
|
}
|
|
244
340
|
|
|
245
|
-
|
|
341
|
+
const elements = list.filter(isVisible).filter((n) => {
|
|
246
342
|
const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
|
|
247
343
|
return matchesFilter(n, t)
|
|
248
344
|
}).map((n) => {
|
|
@@ -277,7 +373,7 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
277
373
|
...base,
|
|
278
374
|
borderRadius: radiusOf(style),
|
|
279
375
|
padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
|
|
280
|
-
fontSize: fontSizeOf(style),
|
|
376
|
+
fontSize: text ? (fontSizeOf(style) || 14) : 0,
|
|
281
377
|
color: text ? colorStr(style.color) : undefined,
|
|
282
378
|
bg: colorStr(style.backgroundColor),
|
|
283
379
|
opacity: opacityOf(style),
|
|
@@ -288,6 +384,50 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
288
384
|
borderColor: borderWidth ? borderColorOf(style) : undefined,
|
|
289
385
|
}
|
|
290
386
|
})
|
|
387
|
+
const textElements = elements.filter((element) => element.text && element.fontSize > 0)
|
|
388
|
+
if (!includeStyle || textElements.length === 0) return elements
|
|
389
|
+
|
|
390
|
+
const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
|
|
391
|
+
if (typeof screenshot !== 'function') {
|
|
392
|
+
throw new Error('styled native layout requires the SootSim capture bridge')
|
|
393
|
+
}
|
|
394
|
+
const device = list.find((node) => node && node.device)?.device || {}
|
|
395
|
+
const width = Math.round(Number(device.width) || window.innerWidth || 0)
|
|
396
|
+
const height = Math.round(Number(device.height) || window.innerHeight || 0)
|
|
397
|
+
if (!width || !height) throw new Error('native layout has no capture dimensions')
|
|
398
|
+
const capture = async (suppressText) => {
|
|
399
|
+
const dataUrl = await screenshot({
|
|
400
|
+
format: 'png',
|
|
401
|
+
outputWidth: width,
|
|
402
|
+
outputHeight: height,
|
|
403
|
+
...(suppressText ? { suppressText: true } : {}),
|
|
404
|
+
})
|
|
405
|
+
const image = await new Promise((resolve, reject) => {
|
|
406
|
+
const next = new Image()
|
|
407
|
+
next.onload = () => resolve(next)
|
|
408
|
+
next.onerror = () => reject(new Error('could not decode native layout pixels'))
|
|
409
|
+
next.src = dataUrl
|
|
410
|
+
})
|
|
411
|
+
const canvas = document.createElement('canvas')
|
|
412
|
+
canvas.width = image.naturalWidth
|
|
413
|
+
canvas.height = image.naturalHeight
|
|
414
|
+
const context = canvas.getContext('2d', { willReadFrequently: true })
|
|
415
|
+
if (!context) throw new Error('could not read native layout pixels')
|
|
416
|
+
context.drawImage(image, 0, 0)
|
|
417
|
+
return context.getImageData(0, 0, canvas.width, canvas.height)
|
|
418
|
+
}
|
|
419
|
+
const normal = await capture(false)
|
|
420
|
+
const background = await capture(true)
|
|
421
|
+
for (const element of textElements) {
|
|
422
|
+
element.contrast = scoreContrast({
|
|
423
|
+
normal,
|
|
424
|
+
background,
|
|
425
|
+
box: element.box,
|
|
426
|
+
fontSize: element.fontSize,
|
|
427
|
+
fontWeight: element.fontWeight,
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
return elements
|
|
291
431
|
})()`;
|
|
292
432
|
}
|
|
293
433
|
var LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval();
|
|
@@ -314,6 +454,11 @@ function formatLayoutElements(elements, opts) {
|
|
|
314
454
|
if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
|
|
315
455
|
if (el.color) parts.push(`color:${el.color}`);
|
|
316
456
|
if (el.bg) parts.push(`bg:${el.bg}`);
|
|
457
|
+
if (el.contrast) {
|
|
458
|
+
const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
|
|
459
|
+
parts.push(`contrast:${score}`);
|
|
460
|
+
if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
|
|
461
|
+
}
|
|
317
462
|
if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
|
|
318
463
|
if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
|
|
319
464
|
if (el.overflow) parts.push(`overflow:${el.overflow}`);
|
|
@@ -1114,6 +1259,10 @@ async function inspectScreens(bridge) {
|
|
|
1114
1259
|
test && typeof test.getNavigationSnapshot === 'function'
|
|
1115
1260
|
? await test.getNavigationSnapshot()
|
|
1116
1261
|
: null
|
|
1262
|
+
const route =
|
|
1263
|
+
test && typeof test.getRouteInfo === 'function'
|
|
1264
|
+
? await test.getRouteInfo()
|
|
1265
|
+
: null
|
|
1117
1266
|
const keyboard =
|
|
1118
1267
|
kb && typeof kb.getLayout === 'function'
|
|
1119
1268
|
? (() => {
|
|
@@ -1130,13 +1279,14 @@ async function inspectScreens(bridge) {
|
|
|
1130
1279
|
} : null
|
|
1131
1280
|
})()
|
|
1132
1281
|
: null
|
|
1133
|
-
return { nav: navSnap, keyboard }
|
|
1282
|
+
return { nav: navSnap, route, keyboard }
|
|
1134
1283
|
})()`
|
|
1135
1284
|
});
|
|
1136
1285
|
const shell = await getShellState(bridge, 500).catch(() => null);
|
|
1137
1286
|
return {
|
|
1138
1287
|
shell,
|
|
1139
1288
|
nav: payload?.nav ?? null,
|
|
1289
|
+
route: payload?.route ?? null,
|
|
1140
1290
|
keyboard: payload?.keyboard ?? null
|
|
1141
1291
|
};
|
|
1142
1292
|
}
|
|
@@ -1993,6 +2143,7 @@ export {
|
|
|
1993
2143
|
resolveFindMode,
|
|
1994
2144
|
resolveMaxMsFlag,
|
|
1995
2145
|
scoreInteractive,
|
|
2146
|
+
scoreLayoutTextContrast,
|
|
1996
2147
|
setDebugChannels,
|
|
1997
2148
|
shouldSkipAutoSettleForInspectPick,
|
|
1998
2149
|
tapBest,
|
package/dist-lib/skills.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! sootsim v0.1.
|
|
1
|
+
/*! sootsim v0.1.241 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -16429,6 +16429,101 @@ var init_app_state_reset = __esm({
|
|
|
16429
16429
|
});
|
|
16430
16430
|
|
|
16431
16431
|
// cli/commands/inspect/core.ts
|
|
16432
|
+
function scoreLayoutTextContrast(input) {
|
|
16433
|
+
const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
|
|
16434
|
+
const { normal, background } = input;
|
|
16435
|
+
if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
|
|
16436
|
+
return { ratio: null, required, reason: "capture dimensions do not match" };
|
|
16437
|
+
}
|
|
16438
|
+
const left = Math.max(0, Math.floor(input.box.x));
|
|
16439
|
+
const top = Math.max(0, Math.floor(input.box.y));
|
|
16440
|
+
const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
|
|
16441
|
+
const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
|
|
16442
|
+
if (right <= left || bottom <= top) {
|
|
16443
|
+
return { ratio: null, required, reason: "text is outside the captured frame" };
|
|
16444
|
+
}
|
|
16445
|
+
const linear = (value) => {
|
|
16446
|
+
const channel = value / 255;
|
|
16447
|
+
return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
|
|
16448
|
+
};
|
|
16449
|
+
const ratioOf = (foreground, behind) => {
|
|
16450
|
+
const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
|
|
16451
|
+
const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
|
|
16452
|
+
return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
|
|
16453
|
+
};
|
|
16454
|
+
const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
|
|
16455
|
+
const area = (right - left) * (bottom - top);
|
|
16456
|
+
const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
|
|
16457
|
+
const offsetAt = (x, y) => (y * normal.width + x) * 4;
|
|
16458
|
+
const deltaAt = (offset) => Math.max(
|
|
16459
|
+
Math.abs(normal.data[offset] - background.data[offset]),
|
|
16460
|
+
Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
|
|
16461
|
+
Math.abs(normal.data[offset + 2] - background.data[offset + 2])
|
|
16462
|
+
);
|
|
16463
|
+
let strongestDelta = 0;
|
|
16464
|
+
let strongestForeground = null;
|
|
16465
|
+
let opaqueBackground = null;
|
|
16466
|
+
for (let y = top; y < bottom; y += step) {
|
|
16467
|
+
for (let x = left; x < right; x += step) {
|
|
16468
|
+
const offset = offsetAt(x, y);
|
|
16469
|
+
if (background.data[offset + 3] < 250) continue;
|
|
16470
|
+
const behind = [
|
|
16471
|
+
background.data[offset],
|
|
16472
|
+
background.data[offset + 1],
|
|
16473
|
+
background.data[offset + 2]
|
|
16474
|
+
];
|
|
16475
|
+
opaqueBackground ??= behind;
|
|
16476
|
+
const delta = deltaAt(offset);
|
|
16477
|
+
if (delta <= strongestDelta) continue;
|
|
16478
|
+
strongestDelta = delta;
|
|
16479
|
+
strongestForeground = [
|
|
16480
|
+
normal.data[offset],
|
|
16481
|
+
normal.data[offset + 1],
|
|
16482
|
+
normal.data[offset + 2]
|
|
16483
|
+
];
|
|
16484
|
+
}
|
|
16485
|
+
}
|
|
16486
|
+
let bestRatio = Infinity;
|
|
16487
|
+
let bestForeground = null;
|
|
16488
|
+
let bestBackground = null;
|
|
16489
|
+
const consider = (foreground, behind) => {
|
|
16490
|
+
const ratio = ratioOf(foreground, behind);
|
|
16491
|
+
if (ratio >= bestRatio) return;
|
|
16492
|
+
bestRatio = ratio;
|
|
16493
|
+
bestForeground = foreground;
|
|
16494
|
+
bestBackground = behind;
|
|
16495
|
+
};
|
|
16496
|
+
if (strongestDelta >= 2 && strongestForeground) {
|
|
16497
|
+
for (let y = top; y < bottom; y += step) {
|
|
16498
|
+
for (let x = left; x < right; x += step) {
|
|
16499
|
+
const offset = offsetAt(x, y);
|
|
16500
|
+
if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
|
|
16501
|
+
continue;
|
|
16502
|
+
}
|
|
16503
|
+
consider(strongestForeground, [
|
|
16504
|
+
background.data[offset],
|
|
16505
|
+
background.data[offset + 1],
|
|
16506
|
+
background.data[offset + 2]
|
|
16507
|
+
]);
|
|
16508
|
+
}
|
|
16509
|
+
}
|
|
16510
|
+
} else if (opaqueBackground) {
|
|
16511
|
+
consider(opaqueBackground, opaqueBackground);
|
|
16512
|
+
}
|
|
16513
|
+
if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
|
|
16514
|
+
return {
|
|
16515
|
+
ratio: null,
|
|
16516
|
+
required,
|
|
16517
|
+
reason: "could not isolate opaque rendered text pixels"
|
|
16518
|
+
};
|
|
16519
|
+
}
|
|
16520
|
+
return {
|
|
16521
|
+
ratio: Math.round(bestRatio * 100) / 100,
|
|
16522
|
+
required,
|
|
16523
|
+
foreground: hex(bestForeground),
|
|
16524
|
+
background: hex(bestBackground)
|
|
16525
|
+
};
|
|
16526
|
+
}
|
|
16432
16527
|
function layoutGetNativeEval(opts = {}) {
|
|
16433
16528
|
const includeStyle = opts.styling === true;
|
|
16434
16529
|
const filter = opts.filter?.trim().toLowerCase() ?? "";
|
|
@@ -16438,6 +16533,7 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
16438
16533
|
const list = await t.listInspectable({})
|
|
16439
16534
|
if (!Array.isArray(list)) return []
|
|
16440
16535
|
const includeStyle = ${JSON.stringify(includeStyle)}
|
|
16536
|
+
const scoreContrast = ${scoreLayoutTextContrast.toString()}
|
|
16441
16537
|
// narrows by ELEMENT, not by style field: fewer rows, full detail on each.
|
|
16442
16538
|
const filter = ${JSON.stringify(filter)}
|
|
16443
16539
|
const matchesFilter = (n, text) => {
|
|
@@ -16482,7 +16578,7 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
16482
16578
|
}
|
|
16483
16579
|
const weightOf = (style) => {
|
|
16484
16580
|
const w = style && style.fontWeight
|
|
16485
|
-
const n = typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN)
|
|
16581
|
+
const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
|
|
16486
16582
|
return Number.isFinite(n) && n >= 600 ? n : undefined
|
|
16487
16583
|
}
|
|
16488
16584
|
const decoOf = (style) => {
|
|
@@ -16525,7 +16621,7 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
16525
16621
|
return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
|
|
16526
16622
|
}
|
|
16527
16623
|
|
|
16528
|
-
|
|
16624
|
+
const elements = list.filter(isVisible).filter((n) => {
|
|
16529
16625
|
const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
|
|
16530
16626
|
return matchesFilter(n, t)
|
|
16531
16627
|
}).map((n) => {
|
|
@@ -16560,7 +16656,7 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
16560
16656
|
...base,
|
|
16561
16657
|
borderRadius: radiusOf(style),
|
|
16562
16658
|
padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
|
|
16563
|
-
fontSize: fontSizeOf(style),
|
|
16659
|
+
fontSize: text ? (fontSizeOf(style) || 14) : 0,
|
|
16564
16660
|
color: text ? colorStr(style.color) : undefined,
|
|
16565
16661
|
bg: colorStr(style.backgroundColor),
|
|
16566
16662
|
opacity: opacityOf(style),
|
|
@@ -16571,6 +16667,50 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
16571
16667
|
borderColor: borderWidth ? borderColorOf(style) : undefined,
|
|
16572
16668
|
}
|
|
16573
16669
|
})
|
|
16670
|
+
const textElements = elements.filter((element) => element.text && element.fontSize > 0)
|
|
16671
|
+
if (!includeStyle || textElements.length === 0) return elements
|
|
16672
|
+
|
|
16673
|
+
const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
|
|
16674
|
+
if (typeof screenshot !== 'function') {
|
|
16675
|
+
throw new Error('styled native layout requires the SootSim capture bridge')
|
|
16676
|
+
}
|
|
16677
|
+
const device = list.find((node) => node && node.device)?.device || {}
|
|
16678
|
+
const width = Math.round(Number(device.width) || window.innerWidth || 0)
|
|
16679
|
+
const height = Math.round(Number(device.height) || window.innerHeight || 0)
|
|
16680
|
+
if (!width || !height) throw new Error('native layout has no capture dimensions')
|
|
16681
|
+
const capture = async (suppressText) => {
|
|
16682
|
+
const dataUrl = await screenshot({
|
|
16683
|
+
format: 'png',
|
|
16684
|
+
outputWidth: width,
|
|
16685
|
+
outputHeight: height,
|
|
16686
|
+
...(suppressText ? { suppressText: true } : {}),
|
|
16687
|
+
})
|
|
16688
|
+
const image = await new Promise((resolve, reject) => {
|
|
16689
|
+
const next = new Image()
|
|
16690
|
+
next.onload = () => resolve(next)
|
|
16691
|
+
next.onerror = () => reject(new Error('could not decode native layout pixels'))
|
|
16692
|
+
next.src = dataUrl
|
|
16693
|
+
})
|
|
16694
|
+
const canvas = document.createElement('canvas')
|
|
16695
|
+
canvas.width = image.naturalWidth
|
|
16696
|
+
canvas.height = image.naturalHeight
|
|
16697
|
+
const context = canvas.getContext('2d', { willReadFrequently: true })
|
|
16698
|
+
if (!context) throw new Error('could not read native layout pixels')
|
|
16699
|
+
context.drawImage(image, 0, 0)
|
|
16700
|
+
return context.getImageData(0, 0, canvas.width, canvas.height)
|
|
16701
|
+
}
|
|
16702
|
+
const normal = await capture(false)
|
|
16703
|
+
const background = await capture(true)
|
|
16704
|
+
for (const element of textElements) {
|
|
16705
|
+
element.contrast = scoreContrast({
|
|
16706
|
+
normal,
|
|
16707
|
+
background,
|
|
16708
|
+
box: element.box,
|
|
16709
|
+
fontSize: element.fontSize,
|
|
16710
|
+
fontWeight: element.fontWeight,
|
|
16711
|
+
})
|
|
16712
|
+
}
|
|
16713
|
+
return elements
|
|
16574
16714
|
})()`;
|
|
16575
16715
|
}
|
|
16576
16716
|
function readyProbeHasContent(probe) {
|
package/dist-lib/vite.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! sootsim v0.1.
|
|
1
|
+
/*! sootsim v0.1.241 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
package/package.json
CHANGED
package/skills/contrast/SKILL.md
CHANGED
|
@@ -29,7 +29,7 @@ automation, inspecting the render tree, and managing sims.
|
|
|
29
29
|
| `sootsim find` | locate nodes by text / testID / role / type / predicate |
|
|
30
30
|
| `sootsim assert` | run any read verb and convert its output into an exit code |
|
|
31
31
|
| `sootsim screenshot` | capture PNGs and manage App Store screenshot assets |
|
|
32
|
-
| `sootsim get` | read runtime state (tree, a11y, url, count, errors, …) |
|
|
32
|
+
| `sootsim get` | read runtime state (tree, a11y, layout, url, count, errors, …) |
|
|
33
33
|
| `sootsim do` | drive the app (tap, type, scroll, swipe, key, reload, …) |
|
|
34
34
|
| `sootsim wait` | block until the runtime reaches a known state |
|
|
35
35
|
| `sootsim shell` | drive shell state and simulator chrome |
|
|
@@ -239,17 +239,19 @@ watch out for:
|
|
|
239
239
|
## branch — accessibility
|
|
240
240
|
|
|
241
241
|
```sh
|
|
242
|
-
sootsim get a11y #
|
|
243
|
-
sootsim get
|
|
242
|
+
sootsim get a11y # roles, labels, hints, and target geometry
|
|
243
|
+
sootsim get layout --styling # boxes, styles, and native pixel contrast
|
|
244
244
|
sootsim describe --a11y # inline a11y info on every node
|
|
245
245
|
```
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
native styled layout reads rendered pixels rather than inferring contrast from
|
|
248
|
+
tokens. it compares the normal frame with one CanvasKit capture where text is
|
|
249
|
+
suppressed and adds each text score to the existing layout row and issue list.
|
|
250
|
+
|
|
251
|
+
contrast summary format:
|
|
248
252
|
|
|
249
253
|
```
|
|
250
|
-
|
|
251
|
-
[button] "➜" @(340,280) 20x20 (tap) ⚠ touch target <44×44
|
|
252
|
-
<text> "Sign in" color=#777 bg=#888 ⚠ contrast 1.4:1 (need 4.5:1)
|
|
254
|
+
#loginButton @(24,347) 120x24 font:16 color:#777 contrast:1.4/4.5 FAIL on:#888888 "Sign in"
|
|
253
255
|
```
|
|
254
256
|
|
|
255
257
|
finding offenders fast:
|
|
@@ -266,7 +268,8 @@ what gets checked:
|
|
|
266
268
|
- **missing roles** — `Pressable`/`Touchable` without `accessibilityRole`
|
|
267
269
|
- **missing hints** — complex interactions without `accessibilityHint`
|
|
268
270
|
- **touch target size** — tappable nodes with hitSlop-expanded rect <44×44
|
|
269
|
-
- **contrast** —
|
|
271
|
+
- **contrast** — run `sootsim get layout --styling`; it samples final text and
|
|
272
|
+
text-suppressed background pixels, including gradients and composited opacity
|
|
270
273
|
- **label / visible-text mismatch** — label that reads differently from
|
|
271
274
|
the text VoiceOver would fall back to (often a translation bug)
|
|
272
275
|
- **duplicate labels** — two reachable nodes with identical labels on
|
|
@@ -282,10 +285,13 @@ triage order:
|
|
|
282
285
|
fix → verify loop (the same shape as debugging):
|
|
283
286
|
|
|
284
287
|
```sh
|
|
285
|
-
sootsim get a11y
|
|
288
|
+
sootsim get a11y > before-a11y.txt
|
|
289
|
+
sootsim get layout --styling --json > before-layout.json
|
|
286
290
|
# apply the fix
|
|
287
|
-
sootsim get a11y
|
|
288
|
-
|
|
291
|
+
sootsim get a11y > after-a11y.txt
|
|
292
|
+
sootsim get layout --styling --json > after-layout.json
|
|
293
|
+
diff before-a11y.txt after-a11y.txt
|
|
294
|
+
diff before-layout.json after-layout.json
|
|
289
295
|
```
|
|
290
296
|
|
|
291
297
|
fixing common cases:
|
|
@@ -340,8 +346,11 @@ type names sneak in).
|
|
|
340
346
|
`--visible`, or both.
|
|
341
347
|
- **`perf shell` reports zero frames** — you didn't run `perf shell start`
|
|
342
348
|
first, or the capture window closed before any frame committed.
|
|
343
|
-
- **`get a11y
|
|
344
|
-
|
|
349
|
+
- **`get a11y` returns 100+ items** — narrow with `find --role`,
|
|
350
|
+
`find --pressable`, or `find --no-label`; the a11y tree is descriptive and
|
|
351
|
+
intentionally does not invent an `--issues` grading mode.
|
|
352
|
+
- **styled layout cannot capture pixels** — open a tenant app, dismiss system
|
|
353
|
+
overlays, and let the launch transition finish before retrying.
|
|
345
354
|
- **timeline footer keeps showing 1 error but `get errors` is empty** —
|
|
346
355
|
the footer reads "since last cursor advance"; `get errors` is absolute.
|
|
347
356
|
the error already scrolled out of the absolute ring buffer.
|