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