@lingxia/elements 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.
package/src/navigator.ts CHANGED
@@ -8,13 +8,26 @@ 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>;
@@ -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: {
package/src/picker.ts CHANGED
@@ -19,8 +19,13 @@ function hasNativePicker(): boolean {
19
19
  const HARMONY_PROPS_PREFIX = "data:application/json,";
20
20
 
21
21
  // Type definitions
22
- export type LxPickerColumn = string[];
23
- export type LxPickerCascadingColumns = [string[], Record<string, string[]>];
22
+ export type LxPickerColumn = readonly string[];
23
+ export type LxPickerCascadingColumns = readonly [
24
+ readonly string[],
25
+ Readonly<Record<string, readonly string[]>>,
26
+ ];
27
+ /** Selector or multi-column columns, or cascading keys and their options. */
28
+ export type LxPickerColumns = readonly LxPickerColumn[] | LxPickerCascadingColumns;
24
29
  type LxPickerViewEventHandler = (e: LxPickerEvent) => void;
25
30
 
26
31
  export interface LxPickerEventDetail {
@@ -37,7 +42,7 @@ export interface LxPickerEvent extends CustomEvent<LxPickerEventDetail> {
37
42
  export type LxPickerAttributes = {
38
43
  id?: string;
39
44
  mode?: 'selector' | 'multiSelector' | 'cascading' | 'date' | 'time';
40
- columns?: LxPickerColumn[] | LxPickerCascadingColumns;
45
+ columns?: LxPickerColumns;
41
46
  defaultIndex?: number | number[];
42
47
  value?: string;
43
48
  start?: string;
@@ -54,7 +59,6 @@ export type LxPickerAttributes = {
54
59
  ref?: any;
55
60
  onChange?: LxPickerViewEventHandler;
56
61
  onScroll?: LxPickerViewEventHandler;
57
- pageBindings?: Record<string, string>;
58
62
  };
59
63
 
60
64
  declare global {
@@ -91,7 +95,6 @@ export class LxPickerElement extends HTMLElement {
91
95
  private unregister?: () => void;
92
96
  private _handlers: Record<string, EventListenerOrEventListenerObject> = {};
93
97
  private rawHandlers: Record<string, EventListenerOrEventListenerObject> = {};
94
- private _pageBindings: Record<string, string> = {};
95
98
  private harmonyEmbed?: HTMLEmbedElement;
96
99
  private lastHarmonyProps?: string;
97
100
  private webCleanup?: () => void;
@@ -112,19 +115,7 @@ export class LxPickerElement extends HTMLElement {
112
115
  self[propName] = value;
113
116
  }
114
117
 
115
- set pageBindings(bindings: Record<string, string>) {
116
- this._pageBindings = bindings ?? {};
117
- if (this.isConnected) {
118
- this.mountPicker();
119
- }
120
- }
121
-
122
- get pageBindings(): Record<string, string> {
123
- return this._pageBindings;
124
- }
125
-
126
118
  connectedCallback() {
127
- this.upgradeProperty("pageBindings");
128
119
  this.upgradeProperty("onchange");
129
120
 
130
121
  this.componentId = ensureComponentId(this, "lx-picker", this.componentId);
@@ -266,42 +257,10 @@ export class LxPickerElement extends HTMLElement {
266
257
  this.attrObserver = undefined;
267
258
  }
268
259
 
269
- private dataAttrToDatasetKey(attr: string): string {
270
- const raw = attr.slice(5).trim();
271
- if (!raw) return "";
272
- const parts = raw.split("-").filter(Boolean);
273
- if (parts.length === 0) return "";
274
- return parts
275
- .map((segment, index) => {
276
- if (index === 0) return segment.toLowerCase();
277
- return segment.charAt(0).toUpperCase() + segment.slice(1);
278
- })
279
- .join("");
280
- }
281
-
282
- private collectDataset(): Record<string, string> {
283
- const dataset: Record<string, string> = {};
284
- const attrs = this.getAttributeNames();
285
- for (const attr of attrs) {
286
- if (!attr.startsWith("data-")) continue;
287
- const key = this.dataAttrToDatasetKey(attr);
288
- if (!key) continue;
289
- const value = this.getAttribute(attr);
290
- if (value == null) continue;
291
- dataset[key] = value;
292
- }
293
- return dataset;
294
- }
295
-
296
260
  private async mountPicker() {
297
261
  if (!this.componentId) return;
298
262
 
299
263
  const props = this.collectProps();
300
- const dataset = this.collectDataset();
301
- props.dataset = dataset;
302
- props.datasetJson = JSON.stringify(dataset);
303
- props.pageFuncBindings = this._pageBindings;
304
- props.pageFuncBindingsJson = JSON.stringify(this._pageBindings);
305
264
  const { rect, cornerRadius } = measureElement(this);
306
265
 
307
266
  if (isHarmony()) {
package/src/video.ts CHANGED
@@ -51,8 +51,8 @@ export type LxVideoAttributes = {
51
51
  progressBar?: boolean;
52
52
  live?: boolean;
53
53
  volume?: string | number;
54
- qualities?: LxVideoQuality[];
55
- playbackRates?: number[];
54
+ qualities?: readonly LxVideoQuality[];
55
+ playbackRates?: readonly number[];
56
56
  className?: string;
57
57
  style?: unknown;
58
58
  ref?: unknown;
@@ -70,7 +70,6 @@ export type LxVideoAttributes = {
70
70
  onQualityChange?: (event: CustomEvent<LxVideoEventPayloads["onQualityChange"]>) => void;
71
71
  onRateChange?: (event: CustomEvent<LxVideoEventPayloads["onRateChange"]>) => void;
72
72
  onVolumeChange?: (event: CustomEvent<LxVideoEventPayloads["onVolumeChange"]>) => void;
73
- pageBindings?: Record<string, string>;
74
73
  };
75
74
 
76
75
  type LxObjectFit = "cover" | "contain" | "fill" | "fit";
@@ -114,24 +113,6 @@ export class LxVideoElement extends HTMLElement {
114
113
  private unregister?: () => void;
115
114
  private handlers: Record<string, EventListenerOrEventListenerObject> = {};
116
115
  private rawHandlers: Record<string, EventListenerOrEventListenerObject> = {};
117
- private bindings: Record<string, string> = {};
118
-
119
- set pageBindings(bindings: Record<string, string>) {
120
- this.bindings = bindings ?? {};
121
- if (this.isConnected) this.requestRootCompile();
122
- }
123
-
124
- get pageBindings(): Record<string, string> {
125
- return this.bindings;
126
- }
127
-
128
- get pageFuncBindings(): Record<string, string> {
129
- return this.bindings;
130
- }
131
-
132
- get pageFuncBindingsJson(): string {
133
- return JSON.stringify(this.bindings);
134
- }
135
116
 
136
117
  set src(value: string | null | undefined) {
137
118
  if (value == null) {
@@ -160,7 +141,6 @@ export class LxVideoElement extends HTMLElement {
160
141
 
161
142
  connectedCallback(): void {
162
143
  for (const property of [
163
- "pageBindings",
164
144
  "contentRotate",
165
145
  "src",
166
146
  "onplayrequest",