@lingxia/elements 0.16.0 → 0.18.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/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { registerVideoComponent, LxVideoElement, type LxVideoAttributes, type LxVideoEventPayloads, type LxVideoEventHandlers, } from "./video.js";
2
2
  export { registerMediaSwiperComponent, LxMediaSwiperElement, type LxMediaSwiperAttributes, type LxMediaSwiperItem, type LxMediaSwiperChangeEvent, type LxMediaSwiperChangeEventDetail, type LxMediaSwiperTransitionEndEvent, type LxMediaSwiperTransitionEndEventDetail, type LxMediaSwiperItemEvent, type LxMediaSwiperEventDetail, type LxMediaSwiperEndReachedEvent, type LxMediaSwiperEndReachedEventDetail, type LxMediaSwiperErrorEvent, type LxMediaSwiperErrorEventDetail, type LxMediaSwiperChangeSource, type LxMediaSwiperErrorCode, } from "./media_swiper.js";
3
3
  export { registerPickerComponent, LxPickerElement, type LxPickerAttributes, type LxPickerColumn, type LxPickerCascadingColumns, type LxPickerEvent, type LxPickerEventDetail, } from "./picker.js";
4
- export { LxNavigatorElement, type LxNavigatorAttributes, type LxNavigatorEvent, type NavigatorChannel, type NavigatorOpenType, type NavigatorQuery, type NavigatorQueryValue, type NavigatorTarget, } from "./navigator.js";
4
+ export { LxNavigatorElement, type LxNavigatorAttributes, type LxNavigatorEvent, type NavigatorChannel, type NavigatorEdge, type NavigatorFloatPosition, type NavigatorInteraction, type NavigatorPagePlacement, type NavigatorPlacement, type NavigatorSize, type NavigatorSizeValue, type NavigatorUrlPlacement, type NavigatorWindowChrome, type NavigatorOpenType, type NavigatorQuery, type NavigatorQueryValue, type NavigatorTarget, } from "./navigator.js";
5
5
  export { buildVideoNativeAttrs, buildMediaSwiperNativeAttrs, buildNavigatorNativeAttrs, buildPickerNativeAttrs, getPickerDisplayText, getPickerValueFromIndex, VIDEO_DOM_EVENT_MAP, MEDIA_SWIPER_DOM_EVENT_MAP, } from "./native_component_wrapper_shared.js";
6
6
  export { isHarmony, isIOS, isAndroid, isMacOS, isDesktop, getOS, } from "./platform.js";
7
7
  export { ensureComponentId, rectEquals, NativeComponentUpdateState, type Rect, } from "./component.js";
@@ -1,4 +1,4 @@
1
- import type { NavigatorChannel, NavigatorOpenType, NavigatorTarget } from "./navigator.js";
1
+ import type { NavigatorChannel, NavigatorEdge, NavigatorFloatPosition, NavigatorInteraction, NavigatorOpenType, NavigatorPlacement, NavigatorSize, NavigatorTarget, NavigatorWindowChrome } from "./navigator.js";
2
2
  import type { NavigatorQuery } from "./navigator.js";
3
3
  import type { LxMediaSwiperItem } from "./media_swiper.js";
4
4
  import type { LxVideoQuality } from "./video.js";
@@ -10,6 +10,12 @@ export interface NavigatorNativeAttrOptions {
10
10
  openType?: NavigatorOpenType;
11
11
  target?: NavigatorTarget;
12
12
  delta?: number;
13
+ as?: NavigatorPlacement;
14
+ edge?: NavigatorEdge;
15
+ position?: NavigatorFloatPosition;
16
+ chrome?: NavigatorWindowChrome;
17
+ size?: NavigatorSize;
18
+ interaction?: NavigatorInteraction;
13
19
  appId?: string;
14
20
  query?: NavigatorQuery;
15
21
  channel?: NavigatorChannel;
@@ -39,6 +39,18 @@ export function buildNavigatorNativeAttrs(options, extraAttrs = {}) {
39
39
  result.page = options.page;
40
40
  if (options.target)
41
41
  result.target = options.target;
42
+ if (options.as)
43
+ result.as = options.as;
44
+ if (options.edge)
45
+ result.edge = options.edge;
46
+ if (options.position)
47
+ result.position = options.position;
48
+ if (options.chrome)
49
+ result.chrome = options.chrome;
50
+ if (options.size)
51
+ result.size = JSON.stringify(options.size);
52
+ if (options.interaction)
53
+ result.interaction = JSON.stringify(options.interaction);
42
54
  if (options.appId)
43
55
  result["app-id"] = options.appId;
44
56
  if (options.query)
@@ -1,8 +1,24 @@
1
- export type NavigatorOpenType = 'navigate' | 'redirect' | 'navigateBack' | 'reLaunch' | 'switchTab' | 'exit' | 'openUrl' | 'tel';
2
- export type NavigatorTarget = 'self' | 'lxapp' | 'browser';
1
+ export type NavigatorOpenType = 'navigate' | 'redirect' | 'navigateBack' | 'reLaunch' | 'switchTab' | 'exit' | 'tel';
2
+ export type NavigatorTarget = 'page' | 'lxapp' | 'url';
3
+ export type NavigatorUrlPlacement = 'external' | 'tab' | 'aside';
4
+ export type NavigatorPagePlacement = 'float' | 'window';
5
+ export type NavigatorPlacement = NavigatorUrlPlacement | NavigatorPagePlacement;
6
+ export type NavigatorEdge = 'left' | 'right' | 'top' | 'bottom';
7
+ export type NavigatorFloatPosition = 'center' | 'top' | 'bottom' | 'left' | 'right';
8
+ export type NavigatorWindowChrome = 'system' | 'full';
9
+ export type NavigatorSizeValue = number | `${number}%`;
10
+ export type NavigatorSize = {
11
+ width?: NavigatorSizeValue;
12
+ height?: NavigatorSizeValue;
13
+ };
14
+ export type NavigatorInteraction = {
15
+ closeButton?: boolean;
16
+ dismiss?: 'tapOutside' | 'manual';
17
+ modal?: boolean;
18
+ };
3
19
  export type NavigatorQueryValue = string | number | boolean | null | undefined;
4
20
  export type NavigatorQuery = Record<string, NavigatorQueryValue>;
5
- export type NavigatorChannel = 'release' | 'preview' | 'draft';
21
+ export type NavigatorChannel = 'release' | 'draft';
6
22
  export interface LxNavigatorEventDetail {
7
23
  success?: boolean;
8
24
  errMsg?: string;
@@ -17,6 +33,12 @@ export type LxNavigatorAttributes = {
17
33
  'open-type'?: NavigatorOpenType;
18
34
  target?: NavigatorTarget;
19
35
  delta?: number;
36
+ as?: NavigatorPlacement;
37
+ edge?: NavigatorEdge;
38
+ position?: NavigatorFloatPosition;
39
+ chrome?: NavigatorWindowChrome;
40
+ size?: string;
41
+ interaction?: string;
20
42
  'app-id'?: string;
21
43
  channel?: NavigatorChannel;
22
44
  'target-version'?: string;
@@ -75,17 +97,15 @@ export declare class LxNavigatorElement extends HTMLElement {
75
97
  private removeHoverClass;
76
98
  private clearHoverTimer;
77
99
  private handleClick;
78
- /**
79
- * Auto-infer navigation target based on context
80
- * Priority: explicit target > appId > HTTPS URL > default (self)
81
- */
82
- private inferTarget;
83
100
  private navigate;
84
101
  private dispatchSuccess;
85
102
  private resolveErrorMessage;
86
103
  private dispatchFail;
104
+ private bridge;
87
105
  private callHost;
106
+ private callSurface;
88
107
  private performNavigation;
108
+ private readJsonObject;
89
109
  private readQuery;
90
110
  private buildPageTarget;
91
111
  private buildLxAppTarget;
package/dist/navigator.js CHANGED
@@ -43,6 +43,12 @@ export class LxNavigatorElement extends HTMLElement {
43
43
  "open-type",
44
44
  "target",
45
45
  "delta",
46
+ "as",
47
+ "edge",
48
+ "position",
49
+ "chrome",
50
+ "size",
51
+ "interaction",
46
52
  "app-id",
47
53
  "channel",
48
54
  "target-version",
@@ -158,55 +164,33 @@ export class LxNavigatorElement extends HTMLElement {
158
164
  }
159
165
  }
160
166
  handleClick(e) {
167
+ var _a;
161
168
  e.preventDefault();
162
- const url = this.getAttribute('url') || '';
163
- const page = this.getAttribute('page');
164
- // Read the removed attribute only to provide a forward-only runtime error
165
- // for untyped HTML callers instead of silently ignoring it.
166
- const path = this.getAttribute('path');
167
- const query = this.getAttribute('query');
168
- const openType = (this.getAttribute('open-type') || 'navigate');
169
- const explicitTarget = this.getAttribute('target');
170
- const delta = parseInt(this.getAttribute('delta') || '1', 10);
171
- const appId = this.getAttribute('app-id');
172
- const channel = this.getAttribute('channel');
173
- const targetVersion = this.getAttribute('target-version');
174
- const phoneNumber = this.getAttribute('phone-number');
175
- // Auto-infer target if not explicitly specified
176
- const target = this.inferTarget(explicitTarget, url, appId);
177
- void this.navigate({
169
+ const attr = (name) => this.getAttribute(name);
170
+ const url = attr('url') || '';
171
+ const appId = attr('app-id');
172
+ const options = {
178
173
  url,
179
- page,
180
- path,
181
- query,
182
- openType,
183
- target,
184
- delta,
174
+ page: attr('page'),
175
+ // Read the removed attribute only to provide a forward-only runtime error
176
+ // for untyped HTML callers instead of silently ignoring it.
177
+ path: attr('path'),
178
+ query: attr('query'),
179
+ openType: (attr('open-type') || 'navigate'),
180
+ target: (_a = attr('target')) !== null && _a !== void 0 ? _a : (appId ? 'lxapp' : url ? 'url' : 'page'),
181
+ delta: parseInt(attr('delta') || '1', 10),
182
+ placement: attr('as'),
183
+ edge: attr('edge'),
184
+ position: attr('position'),
185
+ chrome: attr('chrome'),
186
+ size: attr('size'),
187
+ interaction: attr('interaction'),
185
188
  appId,
186
- channel,
187
- targetVersion,
188
- phoneNumber
189
- });
190
- }
191
- /**
192
- * Auto-infer navigation target based on context
193
- * Priority: explicit target > appId > HTTPS URL > default (self)
194
- */
195
- inferTarget(explicitTarget, url, appId) {
196
- // 1. Explicit target has highest priority
197
- if (explicitTarget) {
198
- return explicitTarget;
199
- }
200
- // 2. If appId is specified, target is another lxapp
201
- if (appId) {
202
- return 'lxapp';
203
- }
204
- // 3. If URL starts with http:// or https://, open in browser
205
- if (/^https?:\/\//i.test(url)) {
206
- return 'browser';
207
- }
208
- // 4. Default: navigate within current lxapp
209
- return 'self';
189
+ channel: attr('channel'),
190
+ targetVersion: attr('target-version'),
191
+ phoneNumber: attr('phone-number'),
192
+ };
193
+ void this.navigate(options);
210
194
  }
211
195
  async navigate(options) {
212
196
  try {
@@ -273,17 +257,24 @@ export class LxNavigatorElement extends HTMLElement {
273
257
  });
274
258
  this.dispatchEvent(completeEvent);
275
259
  }
276
- callHost(route, params) {
260
+ bridge() {
277
261
  const bridge = window.LingXiaBridge;
278
262
  if (!bridge || typeof bridge.invoke !== 'function') {
279
- return Promise.reject(new Error('LingXiaBridge is not available'));
263
+ throw new Error('LingXiaBridge is not available');
280
264
  }
281
- return bridge.invoke(route, params).then(() => undefined);
265
+ return bridge;
266
+ }
267
+ async callHost(route, params) {
268
+ await this.bridge().invoke(route, params);
269
+ }
270
+ // Placements run through Logic's lx.surface, which owns surface handles.
271
+ async callSurface(method, params) {
272
+ await this.bridge().raw.call(`surface.${method}`, params);
282
273
  }
283
274
  async performNavigation(options) {
284
- const url = options.url || '';
275
+ const { url, placement } = options;
285
276
  const delta = Number.isFinite(options.delta) && options.delta > 0 ? options.delta : 1;
286
- if (options.path !== null && options.path !== undefined) {
277
+ if (options.path !== null) {
287
278
  throw new Error('path is not supported; pass the configured page name in page');
288
279
  }
289
280
  if (options.openType === 'tel') {
@@ -300,39 +291,71 @@ export class LxNavigatorElement extends HTMLElement {
300
291
  if (options.openType === 'navigateBack') {
301
292
  if (options.target === 'lxapp') {
302
293
  await this.callHost('navigator.navigateBackApp');
303
- return;
304
294
  }
305
- if (options.target === 'browser') {
306
- throw new Error('navigateBack is not supported for browser target');
295
+ else {
296
+ await this.callHost('navigation.navigateBack', { delta });
307
297
  }
308
- await this.callHost('navigation.navigateBack', { delta });
309
298
  return;
310
299
  }
311
- if (options.target === 'browser') {
312
- if (!url) {
313
- throw new Error('openUrl requires url');
314
- }
315
- await this.callHost('device.openUrl', { url, target: 'external' });
316
- return;
317
- }
318
- if (options.target === 'lxapp') {
319
- if (!options.appId) {
320
- throw new Error('navigateToApp requires app-id');
321
- }
322
- await this.callHost('navigator.navigateToApp', this.buildLxAppTarget(options));
323
- return;
324
- }
325
- if (options.target === 'self' && /^https?:\/\//i.test(url)) {
326
- if (!url) {
327
- throw new Error('openUrl requires url');
300
+ switch (options.target) {
301
+ case 'url': {
302
+ if (!url) {
303
+ throw new Error('target url requires url');
304
+ }
305
+ if (!placement || placement === 'external') {
306
+ await this.callHost('device.openUrl', { url, target: 'external' });
307
+ return;
308
+ }
309
+ if (placement !== 'tab' && placement !== 'aside') {
310
+ throw new Error(`a url opens as external, tab, or aside; got ${placement}`);
311
+ }
312
+ const surfaceOptions = { as: placement };
313
+ if (options.edge)
314
+ surfaceOptions.edge = options.edge;
315
+ const size = this.readJsonObject('size', options.size);
316
+ if (size)
317
+ surfaceOptions.size = size;
318
+ await this.callSurface('openUrl', { url, options: surfaceOptions });
319
+ return;
328
320
  }
329
- await this.callHost('device.openUrl', { url, target: 'self' });
330
- return;
321
+ case 'lxapp':
322
+ if (!options.appId) {
323
+ throw new Error('target lxapp requires app-id');
324
+ }
325
+ if (placement) {
326
+ throw new Error('as is not supported for target lxapp');
327
+ }
328
+ await this.callHost('navigator.navigateToApp', this.buildLxAppTarget(options));
329
+ return;
330
+ case 'page':
331
+ break;
332
+ default:
333
+ throw new Error(`Unsupported target: ${options.target}`);
331
334
  }
332
335
  const target = this.buildPageTarget(options);
333
336
  if (!target) {
334
337
  throw new Error(`${options.openType} requires page`);
335
338
  }
339
+ if (placement) {
340
+ if (placement !== 'float' && placement !== 'window') {
341
+ throw new Error(`a page opens as float or window; got ${placement}`);
342
+ }
343
+ const surfaceOptions = { as: placement };
344
+ if (target.query)
345
+ surfaceOptions.query = target.query;
346
+ if (options.position)
347
+ surfaceOptions.position = options.position;
348
+ if (options.chrome)
349
+ surfaceOptions.chrome = options.chrome;
350
+ const size = this.readJsonObject('size', options.size);
351
+ if (size)
352
+ surfaceOptions.size = size;
353
+ const interaction = this.readJsonObject('interaction', options.interaction);
354
+ if (interaction)
355
+ surfaceOptions.interaction = interaction;
356
+ await this.callSurface('openPage', { page: target.page, options: surfaceOptions });
357
+ return;
358
+ }
336
359
  switch (options.openType) {
337
360
  case 'navigate':
338
361
  await this.callHost('navigation.navigateTo', target);
@@ -346,25 +369,22 @@ export class LxNavigatorElement extends HTMLElement {
346
369
  case 'reLaunch':
347
370
  await this.callHost('navigation.reLaunch', target);
348
371
  break;
349
- case 'openUrl':
350
- await this.callHost('device.openUrl', {
351
- url,
352
- target: options.target === 'self' ? 'self' : 'external'
353
- });
354
- break;
355
372
  default:
356
373
  throw new Error(`Unsupported openType: ${options.openType}`);
357
374
  }
358
375
  }
359
- readQuery(raw) {
376
+ readJsonObject(name, raw) {
360
377
  if (!raw)
361
378
  return undefined;
362
379
  const parsed = JSON.parse(raw);
363
380
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
364
- throw new Error('query must be an object');
381
+ throw new Error(`${name} must be an object`);
365
382
  }
366
383
  return parsed;
367
384
  }
385
+ readQuery(raw) {
386
+ return this.readJsonObject('query', raw);
387
+ }
368
388
  buildPageTarget(options) {
369
389
  var _a;
370
390
  const page = (_a = options.page) === null || _a === void 0 ? void 0 : _a.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingxia/elements",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "private": false,
5
5
  "description": "LingXia custom elements — native-backed web components for LxApp pages",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "scripts": {
17
17
  "build": "npm --prefix ../lingxia-bridge run build:types && tsc -p tsconfig.json",
18
- "test:inline-native": "npm run build && node tests/inline-native-structure.mjs && node tests/inline-native-unwrap.mjs && node tests/inline-native-runtime.mjs && node tests/inline-native-events.mjs && node tests/inline-native-style.mjs && node tests/video-events.mjs && node tests/inline-native-identity.mjs && node tests/inline-native-contract.mjs",
18
+ "test:inline-native": "npm run build && node tests/inline-native-structure.mjs && node tests/inline-native-unwrap.mjs && node tests/inline-native-runtime.mjs && node tests/inline-native-events.mjs && node tests/inline-native-style.mjs && node tests/video-events.mjs && node tests/navigator-routing.mjs && node tests/inline-native-identity.mjs && node tests/inline-native-contract.mjs",
19
19
  "clean": "rm -rf dist",
20
20
  "prepublishOnly": "npm run build"
21
21
  },
package/src/index.ts CHANGED
@@ -41,6 +41,15 @@ export {
41
41
  type LxNavigatorAttributes,
42
42
  type LxNavigatorEvent,
43
43
  type NavigatorChannel,
44
+ type NavigatorEdge,
45
+ type NavigatorFloatPosition,
46
+ type NavigatorInteraction,
47
+ type NavigatorPagePlacement,
48
+ type NavigatorPlacement,
49
+ type NavigatorSize,
50
+ type NavigatorSizeValue,
51
+ type NavigatorUrlPlacement,
52
+ type NavigatorWindowChrome,
44
53
  type NavigatorOpenType,
45
54
  type NavigatorQuery,
46
55
  type NavigatorQueryValue,
@@ -1,4 +1,14 @@
1
- import type { NavigatorChannel, NavigatorOpenType, NavigatorTarget } from "./navigator.js";
1
+ import type {
2
+ NavigatorChannel,
3
+ NavigatorEdge,
4
+ NavigatorFloatPosition,
5
+ NavigatorInteraction,
6
+ NavigatorOpenType,
7
+ NavigatorPlacement,
8
+ NavigatorSize,
9
+ NavigatorTarget,
10
+ NavigatorWindowChrome,
11
+ } from "./navigator.js";
2
12
  import type { NavigatorQuery } from "./navigator.js";
3
13
  import type { LxMediaSwiperItem } from "./media_swiper.js";
4
14
  import type { LxVideoQuality, LxVideoEventPayloads } from "./video.js";
@@ -37,6 +47,12 @@ export interface NavigatorNativeAttrOptions {
37
47
  openType?: NavigatorOpenType;
38
48
  target?: NavigatorTarget;
39
49
  delta?: number;
50
+ as?: NavigatorPlacement;
51
+ edge?: NavigatorEdge;
52
+ position?: NavigatorFloatPosition;
53
+ chrome?: NavigatorWindowChrome;
54
+ size?: NavigatorSize;
55
+ interaction?: NavigatorInteraction;
40
56
  appId?: string;
41
57
  query?: NavigatorQuery;
42
58
  channel?: NavigatorChannel;
@@ -64,6 +80,12 @@ export function buildNavigatorNativeAttrs(
64
80
  if (options.url) result.url = options.url;
65
81
  if (options.page) result.page = options.page;
66
82
  if (options.target) result.target = options.target;
83
+ if (options.as) result.as = options.as;
84
+ if (options.edge) result.edge = options.edge;
85
+ if (options.position) result.position = options.position;
86
+ if (options.chrome) result.chrome = options.chrome;
87
+ if (options.size) result.size = JSON.stringify(options.size);
88
+ if (options.interaction) result.interaction = JSON.stringify(options.interaction);
67
89
  if (options.appId) result["app-id"] = options.appId;
68
90
  if (options.query) result.query = JSON.stringify(options.query);
69
91
  if (options.channel) result.channel = options.channel;
package/src/navigator.ts CHANGED
@@ -8,17 +8,30 @@ export type NavigatorOpenType =
8
8
  | 'reLaunch' // Restart app with new page
9
9
  | 'switchTab' // Switch to tab page
10
10
  | 'exit' // Exit current lxapp
11
- | 'openUrl' // Open external URL or lxapp
12
11
  | 'tel'; // Make a phone call
13
12
 
14
- export type NavigatorTarget =
15
- | 'self' // Navigate within current lxapp (default)
16
- | 'lxapp' // Open another lxapp
17
- | 'browser'; // Open in external browser
13
+ // What to open. Inferred when omitted: app-id → lxapp, url → url, else page.
14
+ export type NavigatorTarget = 'page' | 'lxapp' | 'url';
15
+
16
+ // Where to open it; mirrors `as` of lx.surface.openUrl / openPage.
17
+ // A url defaults to `external`; a page without `as` follows open-type.
18
+ export type NavigatorUrlPlacement = 'external' | 'tab' | 'aside';
19
+ export type NavigatorPagePlacement = 'float' | 'window';
20
+ export type NavigatorPlacement = NavigatorUrlPlacement | NavigatorPagePlacement;
21
+ export type NavigatorEdge = 'left' | 'right' | 'top' | 'bottom';
22
+ export type NavigatorFloatPosition = 'center' | 'top' | 'bottom' | 'left' | 'right';
23
+ export type NavigatorWindowChrome = 'system' | 'full';
24
+ export type NavigatorSizeValue = number | `${number}%`;
25
+ export type NavigatorSize = { width?: NavigatorSizeValue; height?: NavigatorSizeValue };
26
+ export type NavigatorInteraction = {
27
+ closeButton?: boolean;
28
+ dismiss?: 'tapOutside' | 'manual';
29
+ modal?: boolean;
30
+ };
18
31
 
19
32
  export type NavigatorQueryValue = string | number | boolean | null | undefined;
20
33
  export type NavigatorQuery = Record<string, NavigatorQueryValue>;
21
- export type NavigatorChannel = 'release' | 'preview' | 'draft';
34
+ export type NavigatorChannel = 'release' | 'draft';
22
35
 
23
36
  export interface LxNavigatorEventDetail {
24
37
  success?: boolean;
@@ -31,17 +44,46 @@ export interface LxNavigatorEvent extends CustomEvent<LxNavigatorEventDetail> {
31
44
 
32
45
  type LingXiaBridgeCall = {
33
46
  invoke(route: string, params?: unknown): Promise<unknown>;
47
+ raw: { call(method: string, params?: unknown): Promise<unknown> };
48
+ };
49
+
50
+ type NavigateOptions = {
51
+ url: string;
52
+ page: string | null;
53
+ path: string | null;
54
+ query: string | null;
55
+ openType: NavigatorOpenType;
56
+ target: NavigatorTarget;
57
+ delta: number;
58
+ placement: NavigatorPlacement | null;
59
+ edge: string | null;
60
+ position: string | null;
61
+ chrome: string | null;
62
+ size: string | null;
63
+ interaction: string | null;
64
+ appId: string | null;
65
+ channel: NavigatorChannel | null;
66
+ targetVersion: string | null;
67
+ phoneNumber: string | null;
34
68
  };
35
69
 
36
70
  export type LxNavigatorAttributes = {
37
71
  // Navigation
38
- url?: string; // Browser URL for openUrl/browser target
72
+ url?: string; // http(s) URL
39
73
  page?: string; // Configured page name from lxapp.json; routes are unsupported
40
74
  query?: string; // JSON-encoded page query params
41
75
  'open-type'?: NavigatorOpenType; // Navigation type
42
- target?: NavigatorTarget; // Navigation target (auto-inferred if not specified)
76
+ target?: NavigatorTarget; // What to open (inferred if not specified)
43
77
  delta?: number; // Pages to go back (for navigateBack)
44
78
 
79
+ // Placement
80
+ as?: NavigatorPlacement;
81
+ edge?: NavigatorEdge; // as="aside"
82
+ position?: NavigatorFloatPosition; // as="float"
83
+ chrome?: NavigatorWindowChrome; // as="window"
84
+ size?: string; // JSON-encoded NavigatorSize
85
+ interaction?: string; // JSON-encoded NavigatorInteraction (float/window)
86
+
45
87
  // Open external lxapp
46
88
  'app-id'?: string; // Target lxapp ID
47
89
  channel?: NavigatorChannel; // Target lxapp channel
@@ -88,6 +130,12 @@ export class LxNavigatorElement extends HTMLElement {
88
130
  "open-type",
89
131
  "target",
90
132
  "delta",
133
+ "as",
134
+ "edge",
135
+ "position",
136
+ "chrome",
137
+ "size",
138
+ "interaction",
91
139
  "app-id",
92
140
  "channel",
93
141
  "target-version",
@@ -252,80 +300,34 @@ export class LxNavigatorElement extends HTMLElement {
252
300
 
253
301
  private handleClick(e: MouseEvent) {
254
302
  e.preventDefault();
255
-
256
- const url = this.getAttribute('url') || '';
257
- const page = this.getAttribute('page');
258
- // Read the removed attribute only to provide a forward-only runtime error
259
- // for untyped HTML callers instead of silently ignoring it.
260
- const path = this.getAttribute('path');
261
- const query = this.getAttribute('query');
262
- const openType = (this.getAttribute('open-type') || 'navigate') as NavigatorOpenType;
263
- const explicitTarget = this.getAttribute('target') as NavigatorTarget | null;
264
- const delta = parseInt(this.getAttribute('delta') || '1', 10);
265
- const appId = this.getAttribute('app-id');
266
- const channel = this.getAttribute('channel') as NavigatorChannel | null;
267
- const targetVersion = this.getAttribute('target-version');
268
- const phoneNumber = this.getAttribute('phone-number');
269
-
270
- // Auto-infer target if not explicitly specified
271
- const target = this.inferTarget(explicitTarget, url, appId);
272
-
273
- void this.navigate({
303
+ const attr = (name: string) => this.getAttribute(name);
304
+ const url = attr('url') || '';
305
+ const appId = attr('app-id');
306
+ const options: NavigateOptions = {
274
307
  url,
275
- page,
276
- path,
277
- query,
278
- openType,
279
- target,
280
- delta,
308
+ page: attr('page'),
309
+ // Read the removed attribute only to provide a forward-only runtime error
310
+ // for untyped HTML callers instead of silently ignoring it.
311
+ path: attr('path'),
312
+ query: attr('query'),
313
+ openType: (attr('open-type') || 'navigate') as NavigatorOpenType,
314
+ target: (attr('target') as NavigatorTarget | null) ?? (appId ? 'lxapp' : url ? 'url' : 'page'),
315
+ delta: parseInt(attr('delta') || '1', 10),
316
+ placement: attr('as') as NavigatorPlacement | null,
317
+ edge: attr('edge'),
318
+ position: attr('position'),
319
+ chrome: attr('chrome'),
320
+ size: attr('size'),
321
+ interaction: attr('interaction'),
281
322
  appId,
282
- channel,
283
- targetVersion,
284
- phoneNumber
285
- });
286
- }
287
-
288
- /**
289
- * Auto-infer navigation target based on context
290
- * Priority: explicit target > appId > HTTPS URL > default (self)
291
- */
292
- private inferTarget(
293
- explicitTarget: NavigatorTarget | null,
294
- url: string,
295
- appId: string | null
296
- ): NavigatorTarget {
297
- // 1. Explicit target has highest priority
298
- if (explicitTarget) {
299
- return explicitTarget;
300
- }
301
-
302
- // 2. If appId is specified, target is another lxapp
303
- if (appId) {
304
- return 'lxapp';
305
- }
306
-
307
- // 3. If URL starts with http:// or https://, open in browser
308
- if (/^https?:\/\//i.test(url)) {
309
- return 'browser';
310
- }
311
-
312
- // 4. Default: navigate within current lxapp
313
- return 'self';
323
+ channel: attr('channel') as NavigatorChannel | null,
324
+ targetVersion: attr('target-version'),
325
+ phoneNumber: attr('phone-number'),
326
+ };
327
+ void this.navigate(options);
314
328
  }
315
329
 
316
- private async navigate(options: {
317
- url: string;
318
- page?: string | null;
319
- path?: string | null;
320
- query?: string | null;
321
- openType: NavigatorOpenType;
322
- target: NavigatorTarget;
323
- delta: number;
324
- appId?: string | null;
325
- channel?: NavigatorChannel | null;
326
- targetVersion?: string | null;
327
- phoneNumber?: string | null;
328
- }) {
330
+ private async navigate(options: NavigateOptions) {
329
331
  try {
330
332
  await this.performNavigation(options);
331
333
  this.dispatchSuccess();
@@ -398,31 +400,28 @@ export class LxNavigatorElement extends HTMLElement {
398
400
  this.dispatchEvent(completeEvent);
399
401
  }
400
402
 
401
- private callHost(route: string, params?: unknown): Promise<void> {
403
+ private bridge(): LingXiaBridgeCall {
402
404
  const bridge = (window as unknown as { LingXiaBridge?: LingXiaBridgeCall }).LingXiaBridge;
403
405
  if (!bridge || typeof bridge.invoke !== 'function') {
404
- return Promise.reject(new Error('LingXiaBridge is not available'));
406
+ throw new Error('LingXiaBridge is not available');
405
407
  }
406
- return bridge.invoke(route, params).then(() => undefined);
408
+ return bridge;
407
409
  }
408
410
 
409
- private async performNavigation(options: {
410
- url: string;
411
- page?: string | null;
412
- path?: string | null;
413
- query?: string | null;
414
- openType: NavigatorOpenType;
415
- target: NavigatorTarget;
416
- delta: number;
417
- appId?: string | null;
418
- channel?: NavigatorChannel | null;
419
- targetVersion?: string | null;
420
- phoneNumber?: string | null;
421
- }) {
422
- const url = options.url || '';
411
+ private async callHost(route: string, params?: unknown): Promise<void> {
412
+ await this.bridge().invoke(route, params);
413
+ }
414
+
415
+ // Placements run through Logic's lx.surface, which owns surface handles.
416
+ private async callSurface(method: 'openPage' | 'openUrl', params: unknown): Promise<void> {
417
+ await this.bridge().raw.call(`surface.${method}`, params);
418
+ }
419
+
420
+ private async performNavigation(options: NavigateOptions) {
421
+ const { url, placement } = options;
423
422
  const delta = Number.isFinite(options.delta) && options.delta > 0 ? options.delta : 1;
424
423
 
425
- if (options.path !== null && options.path !== undefined) {
424
+ if (options.path !== null) {
426
425
  throw new Error('path is not supported; pass the configured page name in page');
427
426
  }
428
427
 
@@ -442,44 +441,67 @@ export class LxNavigatorElement extends HTMLElement {
442
441
  if (options.openType === 'navigateBack') {
443
442
  if (options.target === 'lxapp') {
444
443
  await this.callHost('navigator.navigateBackApp');
445
- return;
446
- }
447
- if (options.target === 'browser') {
448
- throw new Error('navigateBack is not supported for browser target');
444
+ } else {
445
+ await this.callHost('navigation.navigateBack', { delta });
449
446
  }
450
- await this.callHost('navigation.navigateBack', { delta });
451
447
  return;
452
448
  }
453
449
 
454
- if (options.target === 'browser') {
455
- if (!url) {
456
- throw new Error('openUrl requires url');
450
+ switch (options.target) {
451
+ case 'url': {
452
+ if (!url) {
453
+ throw new Error('target url requires url');
454
+ }
455
+ if (!placement || placement === 'external') {
456
+ await this.callHost('device.openUrl', { url, target: 'external' });
457
+ return;
458
+ }
459
+ if (placement !== 'tab' && placement !== 'aside') {
460
+ throw new Error(`a url opens as external, tab, or aside; got ${placement}`);
461
+ }
462
+ const surfaceOptions: Record<string, unknown> = { as: placement };
463
+ if (options.edge) surfaceOptions.edge = options.edge;
464
+ const size = this.readJsonObject('size', options.size);
465
+ if (size) surfaceOptions.size = size;
466
+ await this.callSurface('openUrl', { url, options: surfaceOptions });
467
+ return;
457
468
  }
458
- await this.callHost('device.openUrl', { url, target: 'external' });
459
- return;
469
+ case 'lxapp':
470
+ if (!options.appId) {
471
+ throw new Error('target lxapp requires app-id');
472
+ }
473
+ if (placement) {
474
+ throw new Error('as is not supported for target lxapp');
475
+ }
476
+ await this.callHost('navigator.navigateToApp', this.buildLxAppTarget(options));
477
+ return;
478
+ case 'page':
479
+ break;
480
+ default:
481
+ throw new Error(`Unsupported target: ${options.target}`);
460
482
  }
461
483
 
462
- if (options.target === 'lxapp') {
463
- if (!options.appId) {
464
- throw new Error('navigateToApp requires app-id');
465
- }
466
- await this.callHost('navigator.navigateToApp', this.buildLxAppTarget(options));
467
- return;
484
+ const target = this.buildPageTarget(options);
485
+ if (!target) {
486
+ throw new Error(`${options.openType} requires page`);
468
487
  }
469
488
 
470
- if (options.target === 'self' && /^https?:\/\//i.test(url)) {
471
- if (!url) {
472
- throw new Error('openUrl requires url');
489
+ if (placement) {
490
+ if (placement !== 'float' && placement !== 'window') {
491
+ throw new Error(`a page opens as float or window; got ${placement}`);
473
492
  }
474
- await this.callHost('device.openUrl', { url, target: 'self' });
493
+ const surfaceOptions: Record<string, unknown> = { as: placement };
494
+ if (target.query) surfaceOptions.query = target.query;
495
+ if (options.position) surfaceOptions.position = options.position;
496
+ if (options.chrome) surfaceOptions.chrome = options.chrome;
497
+ const size = this.readJsonObject('size', options.size);
498
+ if (size) surfaceOptions.size = size;
499
+ const interaction = this.readJsonObject('interaction', options.interaction);
500
+ if (interaction) surfaceOptions.interaction = interaction;
501
+ await this.callSurface('openPage', { page: target.page, options: surfaceOptions });
475
502
  return;
476
503
  }
477
504
 
478
- const target = this.buildPageTarget(options);
479
- if (!target) {
480
- throw new Error(`${options.openType} requires page`);
481
- }
482
-
483
505
  switch (options.openType) {
484
506
  case 'navigate':
485
507
  await this.callHost('navigation.navigateTo', target);
@@ -493,24 +515,22 @@ export class LxNavigatorElement extends HTMLElement {
493
515
  case 'reLaunch':
494
516
  await this.callHost('navigation.reLaunch', target);
495
517
  break;
496
- case 'openUrl':
497
- await this.callHost('device.openUrl', {
498
- url,
499
- target: options.target === 'self' ? 'self' : 'external'
500
- });
501
- break;
502
518
  default:
503
519
  throw new Error(`Unsupported openType: ${options.openType}`);
504
520
  }
505
521
  }
506
522
 
507
- private readQuery(raw?: string | null): NavigatorQuery | undefined {
523
+ private readJsonObject(name: string, raw?: string | null): Record<string, unknown> | undefined {
508
524
  if (!raw) return undefined;
509
525
  const parsed = JSON.parse(raw) as unknown;
510
526
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
511
- throw new Error('query must be an object');
527
+ throw new Error(`${name} must be an object`);
512
528
  }
513
- return parsed as NavigatorQuery;
529
+ return parsed as Record<string, unknown>;
530
+ }
531
+
532
+ private readQuery(raw?: string | null): NavigatorQuery | undefined {
533
+ return this.readJsonObject('query', raw) as NavigatorQuery | undefined;
514
534
  }
515
535
 
516
536
  private buildPageTarget(options: {