@lingxia/html 0.17.0 → 0.19.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.
@@ -1,6 +1,6 @@
1
1
  var LingXiaPage = (function(exports) {
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- //#region dist/__page_runtime_runtime__.js
3
+ //#region ../lingxia-page-runtime/dist/shared/runtime.js
4
4
  let snapshot = {};
5
5
  let stateInfo = {
6
6
  rev: -1,
@@ -8,7 +8,16 @@ var LingXiaPage = (function(exports) {
8
8
  };
9
9
  let subscribed = false;
10
10
  let subscribeRetryTimer = null;
11
+ let snapshotRetryTimer = null;
12
+ let snapshotRetries = 0;
13
+ /**
14
+ * The host pushes a page's first state at bridge-ready, so the request is only
15
+ * a fallback: a few tries, backing off, then stop. A page with no Logic never
16
+ * answers, and must not be asked for the life of the page.
17
+ */
18
+ const MAX_SNAPSHOT_RETRIES = 3;
11
19
  let initialSnapshotResolved = false;
20
+ let actions = null;
12
21
  let snapshotRequestInFlight = false;
13
22
  const listeners = /* @__PURE__ */ new Set();
14
23
  function notifyListeners() {
@@ -18,8 +27,22 @@ var LingXiaPage = (function(exports) {
18
27
  } catch {}
19
28
  });
20
29
  }
30
+ /**
31
+ * In a dev session the page data is frozen, so a View that writes to it fails
32
+ * at the write instead of drifting from Logic until the next update. Each
33
+ * push is a fresh copy, so nothing else holds these objects.
34
+ */
35
+ function isDevSession() {
36
+ return typeof window !== "undefined" && window.__LX_BRIDGE_CFG?.dev === true;
37
+ }
38
+ function deepFreeze(value) {
39
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return;
40
+ Object.freeze(value);
41
+ for (const key of Object.keys(value)) deepFreeze(value[key]);
42
+ }
21
43
  function updateSnapshot(next, info) {
22
44
  snapshot = next && typeof next === "object" ? next : {};
45
+ if (isDevSession()) deepFreeze(snapshot);
23
46
  stateInfo = info;
24
47
  notifyListeners();
25
48
  }
@@ -30,14 +53,31 @@ var LingXiaPage = (function(exports) {
30
53
  ensurePageBridgeSubscription();
31
54
  }, 10);
32
55
  }
56
+ /**
57
+ * Retry the snapshot request itself. The subscription is already in place, so
58
+ * retrying it (as this used to) returned at once and never asked again; the
59
+ * page then relied on the host's own push at bridge-ready.
60
+ */
61
+ function scheduleSnapshotRetry() {
62
+ if (snapshotRetryTimer !== null || snapshotRetries >= MAX_SNAPSHOT_RETRIES) return;
63
+ snapshotRetries += 1;
64
+ snapshotRetryTimer = setTimeout(() => {
65
+ snapshotRetryTimer = null;
66
+ requestInitialSnapshot(window.LingXiaBridge);
67
+ }, 250 * 2 ** (snapshotRetries - 1));
68
+ }
33
69
  function requestInitialSnapshot(bridge) {
70
+ if (stateInfo.rev >= 0) initialSnapshotResolved = true;
34
71
  if (initialSnapshotResolved || snapshotRequestInFlight) return;
35
- if (!bridge?.raw?.call) return;
72
+ if (!bridge?.raw?.call) {
73
+ scheduleSnapshotRetry();
74
+ return;
75
+ }
36
76
  snapshotRequestInFlight = true;
37
77
  bridge.raw.call("state.getSnapshot", { scope: "page" }).then(() => {
38
78
  initialSnapshotResolved = true;
39
79
  }).catch(() => {
40
- scheduleSubscribeRetry();
80
+ scheduleSnapshotRetry();
41
81
  }).finally(() => {
42
82
  snapshotRequestInFlight = false;
43
83
  });
@@ -63,35 +103,58 @@ var LingXiaPage = (function(exports) {
63
103
  listeners.delete(listener);
64
104
  };
65
105
  }
66
- function subscribePageData(callback) {
67
- if (typeof callback !== "function") return () => {};
106
+ /** Whether the page's first state has arrived. Flips once per document. */
107
+ function isPageReady() {
68
108
  ensurePageBridgeSubscription();
69
- if (stateInfo.rev >= 0) callback(snapshot, {
70
- rev: stateInfo.rev,
71
- initial: true
72
- });
73
- return subscribePageSnapshot(() => {
74
- callback(snapshot, stateInfo);
109
+ return stateInfo.rev >= 0;
110
+ }
111
+ /**
112
+ * Resolves once the page's first state has arrived — the host pushes it at
113
+ * bridge-ready, before `onLoad`, carrying `Page({ data })`'s defaults — so a
114
+ * page can mount with its data whole. With `timeoutMs`, rejects if Logic has
115
+ * not delivered it by then (it failed to load, or threw first); `null` waits
116
+ * without limit.
117
+ */
118
+ function whenPageReady(options = {}) {
119
+ if (isPageReady()) return Promise.resolve();
120
+ const timeoutMs = options.timeoutMs === void 0 ? 1e4 : options.timeoutMs;
121
+ return new Promise((resolve, reject) => {
122
+ let timer = null;
123
+ const check = () => {
124
+ if (stateInfo.rev < 0) return;
125
+ if (timer !== null) clearTimeout(timer);
126
+ listeners.delete(check);
127
+ resolve();
128
+ };
129
+ if (timeoutMs !== null) timer = setTimeout(() => {
130
+ listeners.delete(check);
131
+ reject(/* @__PURE__ */ new Error(`Page Logic did not deliver the page state within ${timeoutMs} ms`));
132
+ }, timeoutMs);
133
+ listeners.add(check);
75
134
  });
76
135
  }
77
136
  function getPageSnapshot() {
78
137
  ensurePageBridgeSubscription();
79
138
  return snapshot;
80
139
  }
140
+ /**
141
+ * The page's actions: one object for the whole page, so it is a stable
142
+ * dependency. Built on first use — the page's action names arrive with its
143
+ * bridge metadata, after the page module has evaluated.
144
+ */
81
145
  function getPageActions() {
82
- const actions = {};
146
+ if (actions) return actions;
83
147
  const bridge = window.__pageBridge;
84
- if (!bridge?.__names) return actions;
148
+ if (!bridge?.__names) throw new Error("Page actions are not ready. Read useLxPage() during component setup/render, or getPage() after pageReady(); pass actions into shared helpers.");
149
+ const built = {};
85
150
  for (const name of bridge.__names) {
86
151
  if (typeof name !== "string") continue;
87
152
  const fn = getOrCreatePageAction(bridge, name);
88
- if (typeof fn === "function") actions[name] = fn;
153
+ if (typeof fn === "function") built[name] = fn;
89
154
  }
155
+ actions = built;
90
156
  return actions;
91
157
  }
92
- function getPageStateInfo() {
93
- return stateInfo;
94
- }
95
158
  function getOrCreatePageAction(bridge, name) {
96
159
  const existing = bridge[name];
97
160
  if (typeof existing === "function") return existing;
@@ -101,7 +164,8 @@ var LingXiaPage = (function(exports) {
101
164
  }
102
165
  function resolvePageActionMode(bridge, name) {
103
166
  const mode = bridge.__modes?.[name];
104
- return mode === "call" || mode === "stream" ? mode : "notify";
167
+ if (mode === "call" || mode === "stream") return mode;
168
+ throw new Error(`Invalid bridge mode for page action '${name}'; regenerate the page with the current CLI`);
105
169
  }
106
170
  function definePageBridgeAction(name, mode) {
107
171
  function action(...args) {
@@ -116,13 +180,13 @@ var LingXiaPage = (function(exports) {
116
180
  return handle;
117
181
  }
118
182
  if (mode === "call") {
119
- const promise = bridge.raw.call(name, payload);
183
+ const promise = callUnaryPageAction(bridge, name, payload);
120
184
  if (promise && typeof promise.catch === "function") promise.catch((err) => {
121
185
  console.warn(`[PageFunc] ${name} failed:`, err instanceof Error ? err.message : err);
122
186
  });
123
187
  return promise;
124
188
  }
125
- bridge.raw.notify(name, payload);
189
+ throw new Error(`Invalid bridge mode for page action '${name}'`);
126
190
  }
127
191
  Object.assign(action, {
128
192
  __logicFunc: true,
@@ -131,31 +195,450 @@ var LingXiaPage = (function(exports) {
131
195
  });
132
196
  return action;
133
197
  }
198
+ /**
199
+ * The one wire path of a unary action. It has no bridge deadline: an action
200
+ * may wait for a person or poll for minutes, and settles when Logic settles
201
+ * or the document goes away.
202
+ */
203
+ async function callUnaryPageAction(bridge, name, payload, dropWhenGone = true) {
204
+ if (!isPageReady()) await whenPageReady({ timeoutMs: null });
205
+ try {
206
+ return await bridge.raw.call(name, payload, { timeoutMs: 0 });
207
+ } catch (error) {
208
+ if (dropWhenGone && error?.code === "BRIDGE_NOT_READY") return new Promise(() => {});
209
+ throw error;
210
+ }
211
+ }
212
+ /**
213
+ * A DOM or framework event, in this realm or another. A JSON payload never
214
+ * carries methods, so event methods tell the two apart.
215
+ */
216
+ function isEventLike(value) {
217
+ if (typeof Event !== "undefined" && value instanceof Event) return true;
218
+ if (!value || typeof value !== "object") return false;
219
+ const event = value;
220
+ return typeof event.preventDefault === "function" && typeof event.stopPropagation === "function";
221
+ }
222
+ const PAGE_ACTIONS_NOT_READY = "PAGE_ACTIONS_NOT_READY";
223
+ function pageActionError(code, message) {
224
+ return Object.assign(new Error(message), { code });
225
+ }
226
+ /**
227
+ * Automation entry (`PageDriver.action`): invoke one unary action of this
228
+ * page with a JSON payload, as the View would. The payload is passed as is —
229
+ * no DOM-event repackaging.
230
+ */
231
+ function invokePageActionForAutomation(name, payload) {
232
+ if (typeof name !== "string" || name === "") return Promise.reject(pageActionError("BRIDGE_MALFORMED_MESSAGE", "Page action name must be a non-empty string"));
233
+ const metadata = window.__pageBridge;
234
+ const bridge = window.LingXiaBridge;
235
+ if (!Array.isArray(metadata?.__names) || !bridge?.raw) return Promise.reject(pageActionError(PAGE_ACTIONS_NOT_READY, "Page actions are not ready yet"));
236
+ if (!metadata.__names.includes(name)) {
237
+ const known = metadata.__names.length ? metadata.__names.join(", ") : "none";
238
+ return Promise.reject(pageActionError("BRIDGE_METHOD_NOT_FOUND", `'${name}' is not an action of this page (actions: ${known})`));
239
+ }
240
+ if (metadata.__modes?.[name] !== "call") return Promise.reject(pageActionError("PAGE_ACTION_NOT_UNARY", `Page action '${name}' is a stream action; only unary actions can be invoked`));
241
+ return callUnaryPageAction(bridge, name, payload, false);
242
+ }
243
+ if (typeof window !== "undefined") Object.defineProperty(window, "__lxInvokePageAction", {
244
+ value: invokePageActionForAutomation,
245
+ configurable: true
246
+ });
134
247
  function filterPayload(name, args) {
135
248
  const clean = [];
136
249
  for (const value of args) {
137
- if (typeof CustomEvent !== "undefined" && value instanceof CustomEvent) {
138
- clean.push({
139
- type: value.type,
140
- detail: value.detail
250
+ if (isEventLike(value)) {
251
+ const event = value;
252
+ if (typeof event.type === "string" && "detail" in event) clean.push({
253
+ type: event.type,
254
+ detail: event.detail
141
255
  });
142
256
  continue;
143
257
  }
144
- const maybeEvent = value;
145
- if (maybeEvent && typeof maybeEvent === "object" && typeof maybeEvent.type === "string" && "detail" in maybeEvent) {
146
- clean.push({
147
- type: maybeEvent.type,
148
- detail: maybeEvent.detail
149
- });
150
- continue;
151
- }
152
- if (value instanceof Event) continue;
153
- if (value && typeof value === "object" && "stopPropagation" in value && typeof value.stopPropagation === "function") continue;
154
258
  clean.push(value);
155
259
  }
156
260
  if (clean.length > 1) throw new Error(`Page action '${name}' accepts at most one payload argument`);
157
261
  return clean[0];
158
262
  }
263
+ //#endregion
264
+ //#region ../lingxia-bridge/dist/es2020/error.js
265
+ const ERROR_STYLES = `
266
+ * { margin: 0; padding: 0; box-sizing: border-box; }
267
+ body {
268
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, sans-serif;
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ min-height: 100vh;
273
+ background: #f5f5f7;
274
+ color: #1d1d1f;
275
+ padding: 20px;
276
+ -webkit-font-smoothing: antialiased;
277
+ }
278
+ .lx-error {
279
+ text-align: center;
280
+ max-width: 400px;
281
+ }
282
+ .lx-error-code {
283
+ font-size: 120px;
284
+ font-weight: 700;
285
+ color: #d1d1d6;
286
+ line-height: 1;
287
+ letter-spacing: -4px;
288
+ }
289
+ .lx-error-title {
290
+ font-size: 21px;
291
+ font-weight: 600;
292
+ margin: 16px 0 8px;
293
+ color: #1d1d1f;
294
+ }
295
+ .lx-error-desc {
296
+ font-size: 15px;
297
+ color: #86868b;
298
+ line-height: 1.5;
299
+ }
300
+ .lx-error-path {
301
+ display: inline-block;
302
+ margin-top: 20px;
303
+ padding: 10px 16px;
304
+ background: rgba(0,0,0,0.04);
305
+ border-radius: 8px;
306
+ font-family: 'SF Mono', ui-monospace, Menlo, Monaco, monospace;
307
+ font-size: 13px;
308
+ color: #1d1d1f;
309
+ word-break: break-all;
310
+ max-width: 100%;
311
+ }
312
+ @media (prefers-color-scheme: dark) {
313
+ body { background: #000; color: #f5f5f7; }
314
+ .lx-error-code { color: #3a3a3c; }
315
+ .lx-error-title { color: #f5f5f7; }
316
+ .lx-error-desc { color: #86868b; }
317
+ .lx-error-path { background: rgba(255,255,255,0.08); color: #f5f5f7; }
318
+ }
319
+ `;
320
+ function escapeHtml(str) {
321
+ const div = document.createElement("div");
322
+ div.textContent = str;
323
+ return div.innerHTML;
324
+ }
325
+ /**
326
+ * A page whose Logic has not delivered its first state in time — it failed to
327
+ * load, threw before the page could start, or is very slow. Shown over the page
328
+ * instead of a blank screen, naming the page; returns a function that removes
329
+ * it, for when the state does arrive after all.
330
+ */
331
+ function renderPageFault(pagePath, reason) {
332
+ const styleEl = document.createElement("style");
333
+ styleEl.textContent = `
334
+ .lx-page-fault { position: fixed; inset: 0; z-index: 2147483647; overflow: auto;
335
+ background: #fff; color: #1d1d1f; }
336
+ @media (prefers-color-scheme: dark) { .lx-page-fault { background: #000; color: #f5f5f7; } }
337
+ ${ERROR_STYLES.replace(/\bbody\b/g, ".lx-page-fault")}`;
338
+ document.head.appendChild(styleEl);
339
+ const overlay = document.createElement("div");
340
+ overlay.className = "lx-page-fault";
341
+ overlay.setAttribute("role", "alert");
342
+ const container = document.createElement("div");
343
+ container.className = "lx-error";
344
+ let html = "<div class=\"lx-error-code\">Page</div>";
345
+ html += `<h1 class="lx-error-title">${escapeHtml("This page couldn't start")}</h1>`;
346
+ html += `<p class="lx-error-desc">${escapeHtml("Its Logic has not delivered the page state. Check the Logic log for an error.")}</p>`;
347
+ if (pagePath) html += `<div class="lx-error-path">${escapeHtml(pagePath)}</div>`;
348
+ html += `<p class="lx-error-desc" style="margin-top:12px">${escapeHtml(reason)}</p>`;
349
+ container.innerHTML = html;
350
+ overlay.appendChild(container);
351
+ document.body.appendChild(overlay);
352
+ return () => {
353
+ overlay.remove();
354
+ styleEl.remove();
355
+ };
356
+ }
357
+ //#endregion
358
+ //#region ../lingxia-bridge/dist/es2020/runtime-env.js
359
+ const BRIDGE_CONFIG = typeof window !== "undefined" && window.__LX_BRIDGE_CFG || {};
360
+ /**
361
+ * One store per document, deliberately on `window`.
362
+ *
363
+ * This module is present twice in a page: once as the global bridge runtime
364
+ * the host injects, once bundled into the page's own JS. Module-local state
365
+ * would give each copy its own value — the host would push the change into
366
+ * whichever copy won the race to install the hook, and the other, which is the
367
+ * one the framework hooks read, would answer with the boot value forever.
368
+ */
369
+ const fallbackStore$1 = {
370
+ value: BRIDGE_CONFIG.displayLanguage?.trim() || "en-US",
371
+ listeners: /* @__PURE__ */ new Set()
372
+ };
373
+ function store$1() {
374
+ if (typeof window === "undefined") return fallbackStore$1;
375
+ if (!window.__lxDisplayLanguage) window.__lxDisplayLanguage = {
376
+ value: BRIDGE_CONFIG.displayLanguage?.trim() || "en-US",
377
+ listeners: /* @__PURE__ */ new Set()
378
+ };
379
+ return window.__lxDisplayLanguage;
380
+ }
381
+ /** Primary subtags of languages written right to left, for engines without `Intl.Locale#getTextInfo`. */
382
+ const RTL_LANGUAGES = /* @__PURE__ */ new Set([
383
+ "ar",
384
+ "ckb",
385
+ "dv",
386
+ "fa",
387
+ "he",
388
+ "ps",
389
+ "sd",
390
+ "ug",
391
+ "ur",
392
+ "yi"
393
+ ]);
394
+ /** The writing direction of a BCP-47 tag. */
395
+ function textDirection(tag) {
396
+ try {
397
+ const locale = new Intl.Locale(tag);
398
+ const info = typeof locale.getTextInfo === "function" ? locale.getTextInfo() : locale.textInfo;
399
+ if (info?.direction === "rtl" || info?.direction === "ltr") return info.direction;
400
+ } catch {}
401
+ return RTL_LANGUAGES.has(tag.split(/[-_]/)[0].toLowerCase()) ? "rtl" : "ltr";
402
+ }
403
+ /**
404
+ * `lang` and `dir` on `<html>` follow the display language, so a page never
405
+ * sets either: CSS logical properties and `:dir()` just work.
406
+ */
407
+ function stampDocumentLanguage() {
408
+ if (typeof document !== "undefined" && document.documentElement) {
409
+ const language = store$1().value;
410
+ document.documentElement.lang = language;
411
+ document.documentElement.dir = textDirection(language);
412
+ }
413
+ }
414
+ stampDocumentLanguage();
415
+ /**
416
+ * Host entry point for a language the user changed while this document was
417
+ * open. Bootstrap alone would leave a live page in the language it started in,
418
+ * with the native chrome around it already switched.
419
+ *
420
+ * The host pushes each change once, and again — the current value — when a
421
+ * document's bridge reports ready, since a change made while it loaded found
422
+ * no hook to call. Those two can land in either order, so a push carries the
423
+ * host revision it took effect at and an older one never overwrites a newer
424
+ * one. A push without a revision applies as it always did.
425
+ */
426
+ function applyDisplayLanguage(next, revision) {
427
+ const normalized = typeof next === "string" ? next.trim() : "";
428
+ const current = store$1();
429
+ if (typeof revision === "number" && Number.isFinite(revision)) {
430
+ if (revision <= (current.revision ?? 0)) return;
431
+ current.revision = revision;
432
+ }
433
+ if (!normalized || normalized === current.value) return;
434
+ current.value = normalized;
435
+ stampDocumentLanguage();
436
+ for (const listener of [...current.listeners]) listener(normalized);
437
+ }
438
+ if (typeof window !== "undefined" && !window.__lingxiaApplyDisplayLanguage) Object.defineProperty(window, "__lingxiaApplyDisplayLanguage", {
439
+ configurable: false,
440
+ enumerable: false,
441
+ value: applyDisplayLanguage
442
+ });
443
+ function getDisplayLanguage() {
444
+ return store$1().value;
445
+ }
446
+ /**
447
+ * Subscribe to host display-language changes. Returns an unsubscribe.
448
+ *
449
+ * Change-only, so that this composes with `useSyncExternalStore`: the listener
450
+ * runs when the language changes, never on subscribe. Read the current value
451
+ * with `getDisplayLanguage()`. Logic's `lx.host.displayLanguage.watch` differs
452
+ * deliberately — it has no render loop to feed, so it delivers immediately.
453
+ */
454
+ function subscribeDisplayLanguage(listener) {
455
+ const listeners = store$1().listeners;
456
+ listeners.add(listener);
457
+ return () => {
458
+ listeners.delete(listener);
459
+ };
460
+ }
461
+ function getPlatformOS() {
462
+ return BRIDGE_CONFIG.os || "unknown";
463
+ }
464
+ /**
465
+ * Which kind of machine this is, mobile or desktop. Read through `isMobile()`
466
+ * and `isDesktop()`; the class itself is host vocabulary, not lxapp API.
467
+ *
468
+ * Fixed for the life of the document. A shipped host is one machine; the
469
+ * Runner re-serves the page when its simulated device changes class, so this
470
+ * never has to change under a page that is already rendering.
471
+ *
472
+ * Hosts from before this config key shipped send nothing, so fall back to the
473
+ * OS: every one of them is the machine it names.
474
+ */
475
+ function hostClass() {
476
+ if (BRIDGE_CONFIG.hostClass === "mobile" || BRIDGE_CONFIG.hostClass === "desktop") return BRIDGE_CONFIG.hostClass;
477
+ return BRIDGE_CONFIG.os === "macOS" || BRIDGE_CONFIG.os === "Windows" ? "desktop" : "mobile";
478
+ }
479
+ function isDesktop() {
480
+ return hostClass() === "desktop";
481
+ }
482
+ //#endregion
483
+ //#region ../lingxia-bridge/dist/es2020/surface-context.js
484
+ /** What a host that sends nothing — one older than this API — is taken to say. */
485
+ const UNREPORTED = {
486
+ sizeClass: "compact",
487
+ width: 0,
488
+ height: 0,
489
+ aside: false
490
+ };
491
+ /**
492
+ * One store per document, on `window`, for the same reason as the display
493
+ * language: the bridge module is present twice in a page, and the host must
494
+ * reach the copy the framework hooks read.
495
+ */
496
+ function store() {
497
+ if (typeof window === "undefined") return fallbackStore;
498
+ if (!window.__lxSurfaceContext) {
499
+ const config = window.__LX_BRIDGE_CFG;
500
+ window.__lxSurfaceContext = {
501
+ value: parse(config?.surfaceContext) ?? UNREPORTED,
502
+ revision: typeof config?.surfaceContextRevision === "number" ? config.surfaceContextRevision : 0,
503
+ listeners: /* @__PURE__ */ new Set()
504
+ };
505
+ }
506
+ return window.__lxSurfaceContext;
507
+ }
508
+ const fallbackStore = {
509
+ value: UNREPORTED,
510
+ revision: 0,
511
+ listeners: /* @__PURE__ */ new Set()
512
+ };
513
+ function parse(next) {
514
+ if (typeof next !== "object" || next === null) return null;
515
+ const { sizeClass, width, height, aside } = next;
516
+ if (sizeClass !== "compact" && sizeClass !== "regular") return null;
517
+ if (typeof width !== "number" || typeof height !== "number") return null;
518
+ return {
519
+ sizeClass,
520
+ width,
521
+ height,
522
+ aside: aside === true
523
+ };
524
+ }
525
+ function same$1(a, b) {
526
+ return a.sizeClass === b.sizeClass && a.width === b.width && a.height === b.height && a.aside === b.aside;
527
+ }
528
+ /**
529
+ * Host entry point: once the page's bridge is up, and on every change. Pushes
530
+ * can run out of order across host threads, so each carries a revision and an
531
+ * older one than the store holds is dropped.
532
+ */
533
+ function applySurfaceContext(next, revision) {
534
+ const context = parse(next);
535
+ const current = store();
536
+ if (!context) return;
537
+ if (typeof revision === "number") {
538
+ if (revision <= current.revision) return;
539
+ current.revision = revision;
540
+ }
541
+ if (same$1(current.value, context)) return;
542
+ current.value = context;
543
+ for (const listener of [...current.listeners]) listener();
544
+ }
545
+ if (typeof window !== "undefined" && !window.__lingxiaApplySurfaceContext) Object.defineProperty(window, "__lingxiaApplySurfaceContext", {
546
+ configurable: false,
547
+ enumerable: false,
548
+ value: applySurfaceContext
549
+ });
550
+ /**
551
+ * The lxapp's adaptive context. The host seeds it into every page before any
552
+ * script runs and pushes each change, so there is always a value; a host older
553
+ * than this API reports `compact` with a 0×0 viewport.
554
+ */
555
+ function getSurfaceContext() {
556
+ return store().value;
557
+ }
558
+ /**
559
+ * Follow the adaptive context. Change-only, like `subscribeDisplayLanguage`,
560
+ * so it composes with `useSyncExternalStore`. Returns an unsubscribe.
561
+ */
562
+ function subscribeSurfaceContext(listener) {
563
+ const listeners = store().listeners;
564
+ listeners.add(listener);
565
+ return () => {
566
+ listeners.delete(listener);
567
+ };
568
+ }
569
+ //#endregion
570
+ //#region ../lingxia-bridge/dist/es2020/host.js
571
+ let current = null;
572
+ function read() {
573
+ const surface = getSurfaceContext();
574
+ return {
575
+ sizeClass: surface.sizeClass,
576
+ aside: surface.aside,
577
+ displayLanguage: getDisplayLanguage(),
578
+ formFactor: isDesktop() ? "desktop" : "mobile",
579
+ os: getPlatformOS(),
580
+ runner: BRIDGE_CONFIG.runner === true
581
+ };
582
+ }
583
+ function same(a, b) {
584
+ return a.sizeClass === b.sizeClass && a.aside === b.aside && a.displayLanguage === b.displayLanguage && a.formFactor === b.formFactor && a.os === b.os && a.runner === b.runner;
585
+ }
586
+ /**
587
+ * The host facts for this page. The same object is returned until a field
588
+ * changes, so it composes with `useSyncExternalStore` and with memoization.
589
+ */
590
+ function getHost() {
591
+ const next = read();
592
+ if (current === null || !same(current, next)) current = next;
593
+ return current;
594
+ }
595
+ /**
596
+ * Follow the host facts. Change-only: the listener runs when a field of
597
+ * `getHost()` changes — not on subscribe, and not when only the viewport's
598
+ * size moves within its size class. Returns an unsubscribe.
599
+ */
600
+ function subscribeHost(listener) {
601
+ let last = getHost();
602
+ const check = () => {
603
+ const next = getHost();
604
+ if (next === last) return;
605
+ last = next;
606
+ listener();
607
+ };
608
+ const stopLanguage = subscribeDisplayLanguage(check);
609
+ const stopSurface = subscribeSurfaceContext(check);
610
+ return () => {
611
+ stopLanguage();
612
+ stopSurface();
613
+ };
614
+ }
615
+ //#endregion
616
+ //#region ../lingxia-page-runtime/dist/shared/startup.js
617
+ let pending;
618
+ /** Mount with complete initial data; a delayed startup stays recoverable. */
619
+ function waitForPageState() {
620
+ if (isPageReady()) return Promise.resolve();
621
+ return pending ?? (pending = wait().finally(() => {
622
+ pending = void 0;
623
+ }));
624
+ }
625
+ async function wait() {
626
+ const delay = 1e4;
627
+ const panel = {};
628
+ const timer = setTimeout(() => {
629
+ const reason = `Page Logic has not delivered the page state after ${delay} ms`;
630
+ console.error(`[LingXia] ${location.pathname}: ${reason}`);
631
+ panel.dismiss = renderPageFault(location.pathname, reason);
632
+ }, delay);
633
+ try {
634
+ await whenPageReady({ timeoutMs: null });
635
+ } finally {
636
+ clearTimeout(timer);
637
+ panel.dismiss?.();
638
+ }
639
+ }
640
+ //#endregion
641
+ //#region ../lingxia-page-runtime/dist/page-chrome.js
159
642
  const initialLayout = Object.freeze({
160
643
  revision: 0,
161
644
  topInset: 0,
@@ -171,21 +654,15 @@ var LingXiaPage = (function(exports) {
171
654
  root?.style.setProperty("--lx-page-chrome-top-inset", `${layout.topInset}px`);
172
655
  root?.style.setProperty("--lx-page-chrome-bottom-inset", `${layout.bottomInset}px`);
173
656
  root?.style.setProperty("--lx-page-chrome-capsule-inline-end-inset", `${layout.capsuleInlineEndInset}px`);
174
- }
175
- /** Read the latest realized page-chrome layout synchronously. */
176
- function getPageChromeLayout() {
177
- if (typeof window === "undefined") return initialLayout;
178
- return installPageChromeRuntime()?.layout ?? initialLayout;
179
- }
180
- /** Subscribe to realized page-chrome layout changes. */
181
- function subscribePageChromeLayout(listener) {
182
- if (typeof window === "undefined") return () => {};
183
- installPageChromeRuntime();
184
- const handleChange = (event) => {
185
- listener(event.detail);
186
- };
187
- window.addEventListener("lxpagechromechange", handleChange);
188
- return () => window.removeEventListener("lxpagechromechange", handleChange);
657
+ const capsule = layout.capsuleRect;
658
+ for (const edge of [
659
+ "top",
660
+ "right",
661
+ "bottom",
662
+ "left",
663
+ "width",
664
+ "height"
665
+ ]) root?.style.setProperty(`--lx-page-chrome-capsule-${edge}`, `${capsule ? capsule[edge] : 0}px`);
189
666
  }
190
667
  /** Ensure browser previews have the same synchronous contract as native pages. */
191
668
  function installPageChromeRuntime() {
@@ -224,12 +701,34 @@ var LingXiaPage = (function(exports) {
224
701
  }
225
702
  installPageChromeRuntime();
226
703
  //#endregion
227
- exports.getActions = getPageActions;
228
- exports.getPageChromeLayout = getPageChromeLayout;
229
- exports.getSnapshot = getPageSnapshot;
230
- exports.getStateInfo = getPageStateInfo;
231
- exports.subscribe = subscribePageData;
232
- exports.subscribePageChromeLayout = subscribePageChromeLayout;
233
- exports.subscribeSnapshot = subscribePageSnapshot;
704
+ //#region dist/page.js
705
+ /**
706
+ * Resolves once the first state arrives. By default a delayed startup shows
707
+ * a fault panel and keeps waiting, just like React/Vue. An explicit timeout
708
+ * rejects instead; `null` waits without the panel. Await before `getPage()`.
709
+ */
710
+ function pageReady(options) {
711
+ return options?.timeoutMs === void 0 ? waitForPageState() : whenPageReady(options);
712
+ }
713
+ /**
714
+ * This page's Logic state and actions — `this.data` and the page's methods.
715
+ * `data` is readonly: Logic owns it (in a dev session a write throws).
716
+ */
717
+ function getPage() {
718
+ return {
719
+ data: getPageSnapshot(),
720
+ actions: getPageActions()
721
+ };
722
+ }
723
+ /** Follow the page's state. Change-only; read it with `getPage()`. Returns an unsubscribe. */
724
+ function subscribePage(listener) {
725
+ return subscribePageSnapshot(listener);
726
+ }
727
+ //#endregion
728
+ exports.getHost = getHost;
729
+ exports.getPage = getPage;
730
+ exports.pageReady = pageReady;
731
+ exports.subscribeHost = subscribeHost;
732
+ exports.subscribePage = subscribePage;
234
733
  return exports;
235
734
  })({});