@qontinui/ui-bridge-native 0.6.15 → 0.7.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/README.md +17 -0
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs.map +1 -1
- package/dist/core/index.js +42 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +42 -0
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/registry.d.ts +23 -1
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/types.d.ts +66 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +425 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +421 -25
- package/dist/index.mjs.map +1 -1
- package/dist/observation/index.d.ts +12 -0
- package/dist/observation/index.d.ts.map +1 -0
- package/dist/observation/observation.d.ts +233 -0
- package/dist/observation/observation.d.ts.map +1 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +425 -24
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +421 -25
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/routePattern.d.ts +145 -0
- package/dist/react/routePattern.d.ts.map +1 -0
- package/dist/react/useRouteAwareness.d.ts +106 -0
- package/dist/react/useRouteAwareness.d.ts.map +1 -0
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +314 -21
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +308 -22
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/page-health.d.ts +50 -7
- package/dist/server/page-health.d.ts.map +1 -1
- package/dist/server/types.d.ts +5 -3
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -313,6 +313,13 @@ function computeVisibility(state, clip) {
|
|
|
313
313
|
const onScreen = spansOverlap(rect.left, rect.right, clip.left, clip.right) && spansOverlap(rect.top, rect.bottom, clip.top, clip.bottom);
|
|
314
314
|
return onScreen ? { visibility: "visible" } : { visibility: "hidden", visibilityReason: "off-screen" };
|
|
315
315
|
}
|
|
316
|
+
function copyRouteInfo(info) {
|
|
317
|
+
const copy = { ...info };
|
|
318
|
+
if (info.params) copy.params = { ...info.params };
|
|
319
|
+
if (info.queryParams) copy.queryParams = { ...info.queryParams };
|
|
320
|
+
if (info.routeStack) copy.routeStack = [...info.routeStack];
|
|
321
|
+
return copy;
|
|
322
|
+
}
|
|
316
323
|
function inferActions(type, scrollAncestorId) {
|
|
317
324
|
const baseActions = scrollAncestorId ? ["focus", "blur", "scrollIntoView"] : ["focus", "blur"];
|
|
318
325
|
switch (type) {
|
|
@@ -377,6 +384,16 @@ var NativeUIBridgeRegistry = class {
|
|
|
377
384
|
* already installs.
|
|
378
385
|
*/
|
|
379
386
|
this.routeProvider = null;
|
|
387
|
+
/**
|
|
388
|
+
* The route TEMPLATE the app last reported through `useRouteAwareness`
|
|
389
|
+
* (via {@link setRouteInfo}), emitted on every snapshot as `page.route`.
|
|
390
|
+
*
|
|
391
|
+
* Deliberately NOT named `routeInfo`: `createSnapshot`'s parameter of that
|
|
392
|
+
* name is the CONCRETE `{currentRoute, segments, activeTab}` read from the
|
|
393
|
+
* route provider. The two must never be confused — the concrete path is
|
|
394
|
+
* user input, and this slot is the router-asserted template.
|
|
395
|
+
*/
|
|
396
|
+
this.reportedRoute = void 0;
|
|
380
397
|
/**
|
|
381
398
|
* Device pixel ratio used to project `state.layout` (logical dp) into the
|
|
382
399
|
* physical-pixel `bbox` emitted on each snapshot element. Defaults to `1`
|
|
@@ -435,6 +452,22 @@ var NativeUIBridgeRegistry = class {
|
|
|
435
452
|
getRouteProvider() {
|
|
436
453
|
return this.routeProvider;
|
|
437
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Set the route info emitted as `page.route` on every snapshot — the
|
|
457
|
+
* router-asserted TEMPLATE, normally reported by `useRouteAwareness`.
|
|
458
|
+
* Pass `undefined` to clear it (the hook does so on unmount), after which
|
|
459
|
+
* snapshots omit `page` again.
|
|
460
|
+
*
|
|
461
|
+
* The value is copied, so a later mutation by the caller cannot alter what
|
|
462
|
+
* is stored or what an already-emitted snapshot holds.
|
|
463
|
+
*/
|
|
464
|
+
setRouteInfo(info) {
|
|
465
|
+
this.reportedRoute = info ? copyRouteInfo(info) : void 0;
|
|
466
|
+
}
|
|
467
|
+
/** Read-only accessor for the reported route info, if any (a copy). */
|
|
468
|
+
getRouteInfo() {
|
|
469
|
+
return this.reportedRoute ? copyRouteInfo(this.reportedRoute) : void 0;
|
|
470
|
+
}
|
|
438
471
|
/**
|
|
439
472
|
* Resolve the snapshot's `activeTab`: an explicit `getActiveTab()` on the
|
|
440
473
|
* route provider wins, otherwise derive it from the Expo Router segments the
|
|
@@ -1275,6 +1308,12 @@ var NativeUIBridgeRegistry = class {
|
|
|
1275
1308
|
// hosts that wire no route provider.
|
|
1276
1309
|
...resolvedRoute.currentRoute ? { route: resolvedRoute.currentRoute } : {},
|
|
1277
1310
|
...resolvedRoute.activeTab ? { activeTab: resolvedRoute.activeTab } : {},
|
|
1311
|
+
// The router-asserted route TEMPLATE, spelled as the web SDK's
|
|
1312
|
+
// `snapshot.page.route`. Emitted on the ONE assembly point both route
|
|
1313
|
+
// branches above share (the explicit-`routeInfo` branch the server's
|
|
1314
|
+
// `getSnapshot` override uses, and the provider fallback). Omitted, not
|
|
1315
|
+
// nulled, when unset — the same convention as the aliases above.
|
|
1316
|
+
...this.reportedRoute ? { page: { route: copyRouteInfo(this.reportedRoute) } } : {},
|
|
1278
1317
|
registration: this.getRegistrationCoverage()
|
|
1279
1318
|
};
|
|
1280
1319
|
if (this.appInfo) {
|
|
@@ -1321,6 +1360,9 @@ var NativeUIBridgeRegistry = class {
|
|
|
1321
1360
|
}
|
|
1322
1361
|
}
|
|
1323
1362
|
}
|
|
1363
|
+
if (this.reportedRoute) {
|
|
1364
|
+
snapshot.page = { route: copyRouteInfo(this.reportedRoute) };
|
|
1365
|
+
}
|
|
1324
1366
|
}
|
|
1325
1367
|
return snapshot;
|
|
1326
1368
|
}
|
|
@@ -2422,7 +2464,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
2422
2464
|
if (rect && clip && !isEmptyRect(clip) && rect.right > rect.left && rect.bottom > rect.top && rect.top >= clip.top && rect.left >= clip.left && rect.bottom <= clip.bottom && rect.right <= clip.right) {
|
|
2423
2465
|
return { alreadyVisible: true, scrolled: false };
|
|
2424
2466
|
}
|
|
2425
|
-
const
|
|
2467
|
+
const measured2 = await new Promise(
|
|
2426
2468
|
(resolve, reject) => {
|
|
2427
2469
|
let settled = false;
|
|
2428
2470
|
const timer = setTimeout(() => {
|
|
@@ -2459,13 +2501,13 @@ var DefaultNativeActionExecutor = class {
|
|
|
2459
2501
|
}
|
|
2460
2502
|
}
|
|
2461
2503
|
);
|
|
2462
|
-
if (ancestorRect && Number.isFinite(
|
|
2504
|
+
if (ancestorRect && Number.isFinite(measured2.x) && Number.isFinite(measured2.width) && measured2.x + measured2.width > ancestorRect.right - ancestorRect.left + 1) {
|
|
2463
2505
|
throw new NativeActionError(
|
|
2464
2506
|
`scrollIntoView is not supported on element "${element.id}": it is laid out beyond the horizontal extent of its scroll ancestor "${ancestorId}" (sideways overflow, a horizontal container, or a nested horizontal container \u2014 declare that inner container as its scrollAncestorId); only vertical scrolling is supported`,
|
|
2465
2507
|
"NOT_SUPPORTED"
|
|
2466
2508
|
);
|
|
2467
2509
|
}
|
|
2468
|
-
container.scrollTo({ y: Math.max(0,
|
|
2510
|
+
container.scrollTo({ y: Math.max(0, measured2.y - padding), animated: false });
|
|
2469
2511
|
return { alreadyVisible: false, scrolled: true };
|
|
2470
2512
|
}
|
|
2471
2513
|
/**
|
|
@@ -3202,7 +3244,219 @@ function createDesignHandlers(registry, viewportProvider) {
|
|
|
3202
3244
|
};
|
|
3203
3245
|
}
|
|
3204
3246
|
|
|
3247
|
+
// src/observation/observation.ts
|
|
3248
|
+
var UNKNOWN_CODES = [
|
|
3249
|
+
"app_unreachable",
|
|
3250
|
+
"capability_unavailable_in_build",
|
|
3251
|
+
"producer_failed",
|
|
3252
|
+
"producer_not_run",
|
|
3253
|
+
"input_missing",
|
|
3254
|
+
"below_confidence_floor",
|
|
3255
|
+
"model_reply_unparseable",
|
|
3256
|
+
"needs_multi_frame_input",
|
|
3257
|
+
"stale_input"
|
|
3258
|
+
];
|
|
3259
|
+
function isUnknownCode(value) {
|
|
3260
|
+
return typeof value === "string" && UNKNOWN_CODES.includes(value);
|
|
3261
|
+
}
|
|
3262
|
+
var ObservationError = class extends Error {
|
|
3263
|
+
constructor(message) {
|
|
3264
|
+
super(message);
|
|
3265
|
+
this.name = "ObservationError";
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
function toRfc3339(t) {
|
|
3269
|
+
const d = t instanceof Date ? t : new Date(t);
|
|
3270
|
+
if (Number.isNaN(d.getTime())) {
|
|
3271
|
+
throw new ObservationError(`not a valid timestamp: ${String(t)}`);
|
|
3272
|
+
}
|
|
3273
|
+
return d.toISOString();
|
|
3274
|
+
}
|
|
3275
|
+
function normalizeConfidence(c) {
|
|
3276
|
+
if (c === null || c === void 0 || !Number.isFinite(c)) return null;
|
|
3277
|
+
return Math.min(1, Math.max(0, c));
|
|
3278
|
+
}
|
|
3279
|
+
function fullCoverage(n) {
|
|
3280
|
+
return { considered: n, measured: n, unmeasured: [] };
|
|
3281
|
+
}
|
|
3282
|
+
function assertCount(value, what) {
|
|
3283
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
3284
|
+
throw new ObservationError(`${what} must be a non-negative integer, got ${String(value)}`);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
function assertCoverage(coverage) {
|
|
3288
|
+
if (!coverage || typeof coverage !== "object") {
|
|
3289
|
+
throw new ObservationError("coverage must be an object");
|
|
3290
|
+
}
|
|
3291
|
+
assertCount(coverage.considered, "coverage.considered");
|
|
3292
|
+
assertCount(coverage.measured, "coverage.measured");
|
|
3293
|
+
if (!Array.isArray(coverage.unmeasured)) {
|
|
3294
|
+
throw new ObservationError("coverage.unmeasured must be an array");
|
|
3295
|
+
}
|
|
3296
|
+
coverage.unmeasured.forEach((u, i) => {
|
|
3297
|
+
if (!u || typeof u.dimension !== "string") {
|
|
3298
|
+
throw new ObservationError(`coverage.unmeasured[${i}].dimension must be a string`);
|
|
3299
|
+
}
|
|
3300
|
+
assertCount(u.count, `coverage.unmeasured[${i}].count`);
|
|
3301
|
+
if (!isUnknownCode(u.code)) {
|
|
3302
|
+
throw new ObservationError(
|
|
3303
|
+
`coverage.unmeasured[${i}].code is not an UnknownCode: ${String(u.code)}`
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
function assertSource(source) {
|
|
3309
|
+
if (source === null) return;
|
|
3310
|
+
if (typeof source !== "object" || Array.isArray(source)) {
|
|
3311
|
+
throw new ObservationError("source must be a JSON object or null");
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
var RFC3339 = /^(\d{4})-(\d{2})-(\d{2})[Tt ](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-](\d{2}):(\d{2}))$/;
|
|
3315
|
+
function isRfc3339(value) {
|
|
3316
|
+
if (typeof value !== "string") return false;
|
|
3317
|
+
const m = RFC3339.exec(value);
|
|
3318
|
+
if (!m) return false;
|
|
3319
|
+
const [y, mo, d, h, mi, s] = [m[1], m[2], m[3], m[4], m[5], m[6]].map(Number);
|
|
3320
|
+
if (mo < 1 || mo > 12 || d < 1 || h > 23 || mi > 59 || s > 60) return false;
|
|
3321
|
+
const leap = y % 4 === 0 && y % 100 !== 0 || y % 400 === 0;
|
|
3322
|
+
const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][mo - 1];
|
|
3323
|
+
if (d > daysInMonth) return false;
|
|
3324
|
+
if (m[9] !== void 0 && (Number(m[9]) > 23 || Number(m[10]) > 59)) return false;
|
|
3325
|
+
return true;
|
|
3326
|
+
}
|
|
3327
|
+
function assertProvenance(prov) {
|
|
3328
|
+
if (!prov || typeof prov !== "object") throw new ObservationError("provenance is required");
|
|
3329
|
+
if (!prov.producer || typeof prov.producer.id !== "string" || typeof prov.producer.version !== "string") {
|
|
3330
|
+
throw new ObservationError("producer must carry a string id and version");
|
|
3331
|
+
}
|
|
3332
|
+
if (!isRfc3339(prov.evaluatedAt)) {
|
|
3333
|
+
throw new ObservationError(
|
|
3334
|
+
`evaluatedAt must be an RFC3339 date-time, got ${String(prov.evaluatedAt)}`
|
|
3335
|
+
);
|
|
3336
|
+
}
|
|
3337
|
+
if (prov.observedAt !== null && !isRfc3339(prov.observedAt)) {
|
|
3338
|
+
throw new ObservationError(
|
|
3339
|
+
`observedAt must be an RFC3339 date-time or null, got ${String(prov.observedAt)}`
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
assertCoverage(prov.coverage);
|
|
3343
|
+
if (prov.confidence !== null && (typeof prov.confidence !== "number" || !Number.isFinite(prov.confidence) || prov.confidence < 0 || prov.confidence > 1)) {
|
|
3344
|
+
throw new ObservationError(
|
|
3345
|
+
`confidence must be in [0, 1] or null, got ${String(prov.confidence)}`
|
|
3346
|
+
);
|
|
3347
|
+
}
|
|
3348
|
+
if (prov.cache !== null) {
|
|
3349
|
+
if (typeof prov.cache !== "object" || typeof prov.cache.hit !== "boolean" || !Array.isArray(prov.cache.keyInputs) || !prov.cache.keyInputs.every((k) => typeof k === "string")) {
|
|
3350
|
+
throw new ObservationError(
|
|
3351
|
+
"cache must be { hit: boolean, storedAt, keyInputs: string[] } or null"
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3354
|
+
if (prov.cache.storedAt !== null && !isRfc3339(prov.cache.storedAt)) {
|
|
3355
|
+
throw new ObservationError(
|
|
3356
|
+
`cache.storedAt must be an RFC3339 date-time or null, got ${String(prov.cache.storedAt)}`
|
|
3357
|
+
);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
assertSource(prov.source);
|
|
3361
|
+
}
|
|
3362
|
+
function provenance(init) {
|
|
3363
|
+
if (!init.producer || typeof init.producer.id !== "string" || typeof init.producer.version !== "string") {
|
|
3364
|
+
throw new ObservationError("producer must carry a string id and version");
|
|
3365
|
+
}
|
|
3366
|
+
const coverage = init.coverage ?? fullCoverage(0);
|
|
3367
|
+
assertCoverage(coverage);
|
|
3368
|
+
const source = init.source ?? null;
|
|
3369
|
+
assertSource(source);
|
|
3370
|
+
const built = {
|
|
3371
|
+
producer: { id: init.producer.id, version: init.producer.version },
|
|
3372
|
+
observedAt: init.observedAt === void 0 || init.observedAt === null ? null : toRfc3339(init.observedAt),
|
|
3373
|
+
evaluatedAt: toRfc3339(init.evaluatedAt ?? /* @__PURE__ */ new Date()),
|
|
3374
|
+
coverage: {
|
|
3375
|
+
considered: coverage.considered,
|
|
3376
|
+
measured: coverage.measured,
|
|
3377
|
+
unmeasured: coverage.unmeasured.map((u) => ({
|
|
3378
|
+
dimension: u.dimension,
|
|
3379
|
+
count: u.count,
|
|
3380
|
+
code: u.code
|
|
3381
|
+
}))
|
|
3382
|
+
},
|
|
3383
|
+
confidence: normalizeConfidence(init.confidence),
|
|
3384
|
+
cache: init.cache ? {
|
|
3385
|
+
hit: init.cache.hit,
|
|
3386
|
+
storedAt: init.cache.storedAt === null ? null : toRfc3339(init.cache.storedAt),
|
|
3387
|
+
keyInputs: [...init.cache.keyInputs]
|
|
3388
|
+
} : null,
|
|
3389
|
+
source
|
|
3390
|
+
};
|
|
3391
|
+
assertProvenance(built);
|
|
3392
|
+
return built;
|
|
3393
|
+
}
|
|
3394
|
+
function measured(value, prov) {
|
|
3395
|
+
if (value === void 0) {
|
|
3396
|
+
throw new ObservationError(
|
|
3397
|
+
'measured requires a value; report "nothing there" as absent and "could not look" as unknown'
|
|
3398
|
+
);
|
|
3399
|
+
}
|
|
3400
|
+
assertProvenance(prov);
|
|
3401
|
+
return { status: "measured", value, provenance: prov };
|
|
3402
|
+
}
|
|
3403
|
+
function absent(prov) {
|
|
3404
|
+
assertProvenance(prov);
|
|
3405
|
+
const { considered, measured: measured2, unmeasured } = prov.coverage;
|
|
3406
|
+
if (unmeasured.length > 0) {
|
|
3407
|
+
const dims = unmeasured.map((u) => `${u.dimension} x${u.count} (${u.code})`).join(", ");
|
|
3408
|
+
throw new ObservationError(
|
|
3409
|
+
`cannot assert absent over unmeasured coverage: ${dims}. Report the observation as measured (degraded) or unknown instead.`
|
|
3410
|
+
);
|
|
3411
|
+
}
|
|
3412
|
+
if (considered === 0) {
|
|
3413
|
+
throw new ObservationError(
|
|
3414
|
+
"cannot assert absent over zero considered items: the producer looked at nothing. Report the observation as unknown instead."
|
|
3415
|
+
);
|
|
3416
|
+
}
|
|
3417
|
+
if (measured2 !== considered) {
|
|
3418
|
+
throw new ObservationError(
|
|
3419
|
+
`an \`absent\` observation must have measured every considered item (measured ${measured2} of ${considered}); a partial look is \`measured\` with its gaps in \`coverage.unmeasured\`, never \`absent\``
|
|
3420
|
+
);
|
|
3421
|
+
}
|
|
3422
|
+
return { status: "absent", provenance: prov };
|
|
3423
|
+
}
|
|
3424
|
+
function unknown(code, detail, prov) {
|
|
3425
|
+
if (!isUnknownCode(code)) {
|
|
3426
|
+
throw new ObservationError(`not an UnknownCode: ${String(code)}`);
|
|
3427
|
+
}
|
|
3428
|
+
if (typeof detail !== "string") {
|
|
3429
|
+
throw new ObservationError("unknown requires a string detail");
|
|
3430
|
+
}
|
|
3431
|
+
assertProvenance(prov);
|
|
3432
|
+
return { status: "unknown", unknown: { code, detail }, provenance: prov };
|
|
3433
|
+
}
|
|
3434
|
+
var Observation = {
|
|
3435
|
+
provenance,
|
|
3436
|
+
fullCoverage,
|
|
3437
|
+
measured,
|
|
3438
|
+
absent,
|
|
3439
|
+
unknown
|
|
3440
|
+
};
|
|
3441
|
+
|
|
3205
3442
|
// src/server/page-health.ts
|
|
3443
|
+
var NATIVE_PAGE_HEALTH_PRODUCER_ID = "sdk-native/page-health";
|
|
3444
|
+
function defaultProducer() {
|
|
3445
|
+
return {
|
|
3446
|
+
id: NATIVE_PAGE_HEALTH_PRODUCER_ID,
|
|
3447
|
+
version: "0.7.0"
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
function pageHealthUnknown(code, detail, options = {}) {
|
|
3451
|
+
return Observation.unknown(
|
|
3452
|
+
code,
|
|
3453
|
+
detail,
|
|
3454
|
+
Observation.provenance({
|
|
3455
|
+
producer: options.producer ?? defaultProducer(),
|
|
3456
|
+
evaluatedAt: options.evaluatedAt
|
|
3457
|
+
})
|
|
3458
|
+
);
|
|
3459
|
+
}
|
|
3206
3460
|
var GRID_SIZE = 20;
|
|
3207
3461
|
var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
|
|
3208
3462
|
var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
|
|
@@ -3271,13 +3525,66 @@ function normalizeRect(state, viewport) {
|
|
|
3271
3525
|
height: layout.height / viewport.height
|
|
3272
3526
|
};
|
|
3273
3527
|
}
|
|
3274
|
-
function diagnosePageHealth(
|
|
3528
|
+
function diagnosePageHealth(input, options = {}) {
|
|
3529
|
+
const producer = options.producer ?? defaultProducer();
|
|
3530
|
+
try {
|
|
3531
|
+
return observe(input, producer, options.evaluatedAt);
|
|
3532
|
+
} catch (err) {
|
|
3533
|
+
return Observation.unknown(
|
|
3534
|
+
"producer_failed",
|
|
3535
|
+
`page-health producer threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
3536
|
+
Observation.provenance({ producer })
|
|
3537
|
+
);
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
function observe(input, producer, evaluatedAt) {
|
|
3541
|
+
const base = {
|
|
3542
|
+
producer,
|
|
3543
|
+
observedAt: input.observedAt,
|
|
3544
|
+
evaluatedAt
|
|
3545
|
+
};
|
|
3546
|
+
if (!Array.isArray(input.elements)) {
|
|
3547
|
+
return Observation.unknown(
|
|
3548
|
+
"input_missing",
|
|
3549
|
+
"the registry handed over no `elements` array, so there was nothing to analyze; this says nothing about the screen itself",
|
|
3550
|
+
Observation.provenance({ ...base, observedAt: null })
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
3553
|
+
const elements = input.elements;
|
|
3554
|
+
const viewport = input.viewport;
|
|
3555
|
+
if (!viewport || !(viewport.width > 0) || !(viewport.height > 0)) {
|
|
3556
|
+
return Observation.unknown(
|
|
3557
|
+
"input_missing",
|
|
3558
|
+
viewport ? `the device viewport is degenerate (${viewport.width}x${viewport.height}), so no element can be placed on screen \u2014 pass \`body.viewport\` or wire \`viewportProvider\`` : "the device viewport is unknown, so no element can be placed on screen \u2014 pass `body.viewport` or wire `viewportProvider`",
|
|
3559
|
+
Observation.provenance(base)
|
|
3560
|
+
);
|
|
3561
|
+
}
|
|
3562
|
+
if (elements.length === 0 && (input.registeredComponents ?? 0) === 0) {
|
|
3563
|
+
return Observation.unknown(
|
|
3564
|
+
"producer_not_run",
|
|
3565
|
+
"the registry reports zero elements and zero components \u2014 nothing has registered yet, so there was nothing to look at",
|
|
3566
|
+
Observation.provenance(base)
|
|
3567
|
+
);
|
|
3568
|
+
}
|
|
3569
|
+
return analyze(elements, viewport, base);
|
|
3570
|
+
}
|
|
3571
|
+
function analyze(elements, viewport, base) {
|
|
3275
3572
|
const findings = [];
|
|
3276
|
-
const
|
|
3277
|
-
|
|
3278
|
-
rect
|
|
3279
|
-
|
|
3280
|
-
|
|
3573
|
+
const considered = elements.filter((el) => el.state.visible);
|
|
3574
|
+
const visible = considered.flatMap((el) => {
|
|
3575
|
+
const rect = normalizeRect(el.state, viewport);
|
|
3576
|
+
return rect ? [{ el, rect }] : [];
|
|
3577
|
+
});
|
|
3578
|
+
const withoutGeometry = considered.length - visible.length;
|
|
3579
|
+
const unmeasured = withoutGeometry > 0 ? [{ dimension: "geometry", count: withoutGeometry, code: "input_missing" }] : [];
|
|
3580
|
+
const coverage = { considered: considered.length, measured: visible.length, unmeasured };
|
|
3581
|
+
if (considered.length > 0 && visible.length === 0) {
|
|
3582
|
+
return Observation.unknown(
|
|
3583
|
+
"input_missing",
|
|
3584
|
+
`all ${considered.length} visible element(s) have no measured layout yet, so spatial coverage, layout regions and visual anomalies cannot be measured`,
|
|
3585
|
+
Observation.provenance({ ...base, coverage })
|
|
3586
|
+
);
|
|
3587
|
+
}
|
|
3281
3588
|
const grid = Array.from(
|
|
3282
3589
|
{ length: GRID_SIZE },
|
|
3283
3590
|
() => Array.from({ length: GRID_SIZE }, () => false)
|
|
@@ -3432,13 +3739,17 @@ function diagnosePageHealth(elements, viewport) {
|
|
|
3432
3739
|
const heatmap = grid.map(
|
|
3433
3740
|
(row) => row.map((cell) => cell ? "#" : ".").join("")
|
|
3434
3741
|
);
|
|
3435
|
-
return
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3742
|
+
return Observation.measured(
|
|
3743
|
+
{
|
|
3744
|
+
summary: worstSeverity(findings),
|
|
3745
|
+
findings,
|
|
3746
|
+
heatmap,
|
|
3747
|
+
element_count: elements.length,
|
|
3748
|
+
visible_count: visible.length
|
|
3749
|
+
},
|
|
3750
|
+
// A deduction over the registry, not an estimate.
|
|
3751
|
+
Observation.provenance({ ...base, coverage })
|
|
3752
|
+
);
|
|
3442
3753
|
}
|
|
3443
3754
|
|
|
3444
3755
|
// src/server/handlers.ts
|
|
@@ -3886,13 +4197,30 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3886
4197
|
},
|
|
3887
4198
|
getPageHealth: async (ctx) => {
|
|
3888
4199
|
const bodyViewport = ctx.body?.viewport;
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
4200
|
+
let viewport;
|
|
4201
|
+
try {
|
|
4202
|
+
viewport = bodyViewport ?? config?.viewportProvider?.() ?? null;
|
|
4203
|
+
} catch {
|
|
4204
|
+
viewport = null;
|
|
4205
|
+
}
|
|
4206
|
+
const observedAt = Date.now();
|
|
4207
|
+
let elements;
|
|
4208
|
+
let registeredComponents;
|
|
4209
|
+
try {
|
|
4210
|
+
elements = registry.getAllElements().map((e) => ({
|
|
4211
|
+
type: e.type,
|
|
4212
|
+
state: e.getState()
|
|
4213
|
+
}));
|
|
4214
|
+
registeredComponents = registry.getAllComponents().length;
|
|
4215
|
+
} catch (err) {
|
|
4216
|
+
return success2(
|
|
4217
|
+
pageHealthUnknown(
|
|
4218
|
+
"producer_failed",
|
|
4219
|
+
`could not read the registry: ${err instanceof Error ? err.message : String(err)}`
|
|
4220
|
+
)
|
|
4221
|
+
);
|
|
4222
|
+
}
|
|
4223
|
+
return success2(diagnosePageHealth({ elements, registeredComponents, viewport, observedAt }));
|
|
3896
4224
|
},
|
|
3897
4225
|
// Workflows
|
|
3898
4226
|
getWorkflows: async () => {
|
|
@@ -4396,7 +4724,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
4396
4724
|
};
|
|
4397
4725
|
if (config?.appInfo) {
|
|
4398
4726
|
response.uiBridge = {
|
|
4399
|
-
version: "0.
|
|
4727
|
+
version: "0.7.0",
|
|
4400
4728
|
...config.appInfo,
|
|
4401
4729
|
capabilities: ["elements", "components", "actions", "design"],
|
|
4402
4730
|
elementCount: stats.elements,
|
|
@@ -7893,6 +8221,74 @@ function useUIBridgeModal(opts) {
|
|
|
7893
8221
|
};
|
|
7894
8222
|
}, [modalDetector, id, title, type, blocking, dismissible]);
|
|
7895
8223
|
}
|
|
8224
|
+
function effectiveInfo(info, unmatched) {
|
|
8225
|
+
return unmatched ? { ...info, pattern: null, params: void 0 } : info;
|
|
8226
|
+
}
|
|
8227
|
+
function useRouteAwareness(info, options = {}) {
|
|
8228
|
+
const bridge = useUIBridgeNativeOptional();
|
|
8229
|
+
const unmatched = options.unmatched === true;
|
|
8230
|
+
const infoRef = react.useRef(info);
|
|
8231
|
+
infoRef.current = info;
|
|
8232
|
+
const pattern = info.pattern;
|
|
8233
|
+
const patternSource = info.patternSource;
|
|
8234
|
+
const paramsKey = info.params ? JSON.stringify(info.params) : "";
|
|
8235
|
+
const queryParamsKey = info.queryParams ? JSON.stringify(info.queryParams) : "";
|
|
8236
|
+
const routeStackKey = info.routeStack?.join(",");
|
|
8237
|
+
react.useEffect(() => {
|
|
8238
|
+
if (!bridge) return;
|
|
8239
|
+
const registry = bridge.registry;
|
|
8240
|
+
registry.setRouteInfo(effectiveInfo(infoRef.current, unmatched));
|
|
8241
|
+
return () => {
|
|
8242
|
+
registry.setRouteInfo(void 0);
|
|
8243
|
+
};
|
|
8244
|
+
}, [bridge, unmatched, pattern, patternSource, paramsKey, queryParamsKey, routeStackKey]);
|
|
8245
|
+
}
|
|
8246
|
+
|
|
8247
|
+
// src/react/routePattern.ts
|
|
8248
|
+
function focusedIndex(state, routeCount) {
|
|
8249
|
+
if (state.index !== void 0) return state.index;
|
|
8250
|
+
return typeof state.type === "string" && state.type !== "stack" ? 0 : routeCount - 1;
|
|
8251
|
+
}
|
|
8252
|
+
function isRouteIdentifier(value) {
|
|
8253
|
+
return typeof value === "string" && value.length > 0 && !value.includes("/") && !value.includes("?") && !value.includes("#");
|
|
8254
|
+
}
|
|
8255
|
+
function isExpoRouterUnmatched(segments) {
|
|
8256
|
+
return segments.some((s) => typeof s === "string" && s.startsWith("+"));
|
|
8257
|
+
}
|
|
8258
|
+
function expoRouterPattern(segments) {
|
|
8259
|
+
if (!Array.isArray(segments)) return null;
|
|
8260
|
+
for (const segment of segments) {
|
|
8261
|
+
if (!isRouteIdentifier(segment)) return null;
|
|
8262
|
+
if (segment.startsWith("+")) return null;
|
|
8263
|
+
}
|
|
8264
|
+
return "/" + segments.join("/");
|
|
8265
|
+
}
|
|
8266
|
+
function expoRouterRouteStack(segments) {
|
|
8267
|
+
if (expoRouterPattern(segments) === null) return void 0;
|
|
8268
|
+
const stack = ["/"];
|
|
8269
|
+
for (let depth = 1; depth <= segments.length; depth++) {
|
|
8270
|
+
stack.push("/" + segments.slice(0, depth).join("/"));
|
|
8271
|
+
}
|
|
8272
|
+
return stack;
|
|
8273
|
+
}
|
|
8274
|
+
function reactNavigationPattern(state, options = {}) {
|
|
8275
|
+
const names = [];
|
|
8276
|
+
let level = state ?? void 0;
|
|
8277
|
+
while (level) {
|
|
8278
|
+
const routes = level.routes;
|
|
8279
|
+
if (!Array.isArray(routes) || routes.length === 0) return null;
|
|
8280
|
+
const index = focusedIndex(level, routes.length);
|
|
8281
|
+
if (!Number.isInteger(index) || index < 0 || index >= routes.length) return null;
|
|
8282
|
+
const route = routes[index];
|
|
8283
|
+
if (!route || !isRouteIdentifier(route.name)) return null;
|
|
8284
|
+
names.push(route.name);
|
|
8285
|
+
level = route.state;
|
|
8286
|
+
}
|
|
8287
|
+
if (names.length === 0) return null;
|
|
8288
|
+
const leaf = names[names.length - 1];
|
|
8289
|
+
if (options.notFoundRouteNames?.includes(leaf)) return null;
|
|
8290
|
+
return "/" + names.join("/");
|
|
8291
|
+
}
|
|
7896
8292
|
function useUIBridgeToast(opts) {
|
|
7897
8293
|
const { toastCapture } = useUIBridgeNative();
|
|
7898
8294
|
const { id, message, level } = opts;
|
|
@@ -9088,6 +9484,8 @@ exports.createNativeServer = createNativeServer;
|
|
|
9088
9484
|
exports.createServerHandlers = createServerHandlers;
|
|
9089
9485
|
exports.deriveInteractable = deriveInteractable;
|
|
9090
9486
|
exports.deriveText = deriveText;
|
|
9487
|
+
exports.expoRouterPattern = expoRouterPattern;
|
|
9488
|
+
exports.expoRouterRouteStack = expoRouterRouteStack;
|
|
9091
9489
|
exports.extractHandlerNames = extractHandlerNames;
|
|
9092
9490
|
exports.findAllByPattern = findAllByPattern;
|
|
9093
9491
|
exports.findElementByIdentifier = findElementByIdentifier;
|
|
@@ -9100,6 +9498,7 @@ exports.inertAbortSignal = inertAbortSignal;
|
|
|
9100
9498
|
exports.intersectRects = intersectRects;
|
|
9101
9499
|
exports.isAddrInUseError = isAddrInUseError;
|
|
9102
9500
|
exports.isEmptyRect = isEmptyRect;
|
|
9501
|
+
exports.isExpoRouterUnmatched = isExpoRouterUnmatched;
|
|
9103
9502
|
exports.mapNativeStyleToExtended = mapNativeStyleToExtended;
|
|
9104
9503
|
exports.matchesIdentifier = matchesIdentifier;
|
|
9105
9504
|
exports.nativeStandardActionEffect = nativeStandardActionEffect;
|
|
@@ -9109,6 +9508,7 @@ exports.parseColorToRgb = parseColorToRgb;
|
|
|
9109
9508
|
exports.parseFontSizePx = parseFontSizePx;
|
|
9110
9509
|
exports.parseTreePath = parseTreePath;
|
|
9111
9510
|
exports.projectVisionFields = projectVisionFields;
|
|
9511
|
+
exports.reactNavigationPattern = reactNavigationPattern;
|
|
9112
9512
|
exports.resetDefaultParamValidationMode = resetDefaultParamValidationMode;
|
|
9113
9513
|
exports.resetGlobalRegistry = resetGlobalRegistry;
|
|
9114
9514
|
exports.resolveActionEffect = resolveActionEffect;
|
|
@@ -9116,6 +9516,7 @@ exports.roleForType = roleForType;
|
|
|
9116
9516
|
exports.runAbortable = runAbortable;
|
|
9117
9517
|
exports.setDefaultParamValidationMode = setDefaultParamValidationMode;
|
|
9118
9518
|
exports.setGlobalRegistry = setGlobalRegistry;
|
|
9519
|
+
exports.useRouteAwareness = useRouteAwareness;
|
|
9119
9520
|
exports.useToastRecorder = useToastRecorder;
|
|
9120
9521
|
exports.useUIBridge = useUIBridge;
|
|
9121
9522
|
exports.useUIBridgeModal = useUIBridgeModal;
|