@lingxia/skill 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/bin/install.mjs +247 -0
- package/package.json +49 -0
- package/scripts/sync.mjs +69 -0
- package/skill/SKILL.md +334 -0
- package/skill/app/apple-sdk.md +312 -0
- package/skill/app/applinks.md +289 -0
- package/skill/app/project.md +760 -0
- package/skill/cli/lxdev.md +195 -0
- package/skill/cli/reference.md +481 -0
- package/skill/examples/hello-host-js/README.md +25 -0
- package/skill/examples/hello-host-js/home/lxapp.json +12 -0
- package/skill/examples/hello-host-js/home/pages/home/index.json +4 -0
- package/skill/examples/hello-host-js/home/pages/home/index.ts +14 -0
- package/skill/examples/hello-host-js/home/pages/home/index.tsx +15 -0
- package/skill/examples/hello-host-js/lingxia.yaml +39 -0
- package/skill/examples/hello-host-rust/Cargo.toml +15 -0
- package/skill/examples/hello-host-rust/README.md +44 -0
- package/skill/examples/hello-host-rust/home/lxapp.json +13 -0
- package/skill/examples/hello-host-rust/home/pages/home/index.html +46 -0
- package/skill/examples/hello-host-rust/home/pages/home/index.json +4 -0
- package/skill/examples/hello-host-rust/lingxia.yaml +32 -0
- package/skill/examples/hello-host-rust/src/lib.rs +58 -0
- package/skill/examples/hello-lxapp/README.md +29 -0
- package/skill/examples/hello-lxapp/lxapp.config.ts +8 -0
- package/skill/examples/hello-lxapp/lxapp.json +14 -0
- package/skill/examples/hello-lxapp/package.json +14 -0
- package/skill/examples/hello-lxapp/pages/home/index.json +4 -0
- package/skill/examples/hello-lxapp/pages/home/index.ts +35 -0
- package/skill/examples/hello-lxapp/pages/home/index.tsx +34 -0
- package/skill/lxapp/bridge.md +654 -0
- package/skill/lxapp/components.md +375 -0
- package/skill/lxapp/guide.md +675 -0
- package/skill/lxapp/lx-api.md +481 -0
- package/skill/native/development.md +414 -0
- package/skill/reference/file-lifecycle.md +325 -0
- package/skill/skill-manifest.json +6 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
# LxApp Development Guide
|
|
2
|
+
|
|
3
|
+
This guide covers how to write lxapp pages — project layout, the View + Logic architecture, data flow, event handling, and native component integration.
|
|
4
|
+
|
|
5
|
+
Companion pages in this skill:
|
|
6
|
+
|
|
7
|
+
- [Components](./components.md) — `LxInput`, `LxTextarea`, `LxPicker`, `LxVideo`, `LxMediaSwiper`, `LxNavigator` — every attribute and event.
|
|
8
|
+
- [Logic-side `lx.*` API](./lx-api.md) — full Logic API surface map + how to install `@lingxia/types` for typing.
|
|
9
|
+
- [Bridge Guide](./bridge.md) — `setData`, stream, channel mechanics in depth.
|
|
10
|
+
- [App Project](../app/project.md) — host app setup (`lingxia.yaml`, macOS App UI).
|
|
11
|
+
|
|
12
|
+
For first-time CLI install and platform toolchains (one-time, human onramp), the LingXia repo has `docs/quick-start.md`.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Create an LxApp
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
lingxia new my-lxapp -t lxapp -y
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This creates a standalone lxapp project. To create a host app (which contains an embedded home lxapp), use `-t native-app` instead (see [App Project](../app/project.md)).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Project Layout
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
my-lxapp/
|
|
30
|
+
├── lxapp.json
|
|
31
|
+
├── lxapp.config.ts
|
|
32
|
+
├── package.json
|
|
33
|
+
├── pages/
|
|
34
|
+
│ └── home/
|
|
35
|
+
│ ├── index.tsx # View — runs in WebView (React or Vue)
|
|
36
|
+
│ ├── index.ts # Logic — runs in native JS runtime
|
|
37
|
+
│ └── index.json # Page config (navigation bar, style)
|
|
38
|
+
├── public/
|
|
39
|
+
└── shared/
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Key files:
|
|
43
|
+
|
|
44
|
+
- `lxapp.json`: Runtime metadata (`appId`, `appName` or `name`, `version`, `pages`) and lxapp security policy.
|
|
45
|
+
- `lxapp.config.ts`: Build config for view tooling, aliases, and static asset directories.
|
|
46
|
+
- `pages/<name>/index.tsx` (or `.vue`): View layer — UI rendering in WebView.
|
|
47
|
+
- `pages/<name>/index.ts`: Logic layer — page lifecycle and business operations.
|
|
48
|
+
- `pages/<name>/index.json`: Page-level config (navigation/title/style and related options).
|
|
49
|
+
|
|
50
|
+
### Static assets
|
|
51
|
+
|
|
52
|
+
Use `staticDirs` in `lxapp.config.ts` to declare root-level directories that should be copied into `dist/` as-is for `html`, `react`, and `vue`.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
export default {
|
|
56
|
+
staticDirs: ['public', 'view', 'assets'],
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Rules:
|
|
61
|
+
|
|
62
|
+
- `public/`, `assets/`, and `.lingxia/` are the default static directories. If the project root contains any of them, LingXia copies it to `dist/` even when `staticDirs` is omitted.
|
|
63
|
+
- Additional directories must be declared explicitly in `staticDirs`.
|
|
64
|
+
- Explicit `staticDirs` entries must exist at the project root. LingXia treats missing configured directories as build errors.
|
|
65
|
+
- Paths are preserved. For example, `view/info-panel.js` becomes `dist/view/info-panel.js`.
|
|
66
|
+
- LingXia does not scan HTML, manifest files, or arbitrary source strings to discover static assets.
|
|
67
|
+
|
|
68
|
+
### Security Policy
|
|
69
|
+
|
|
70
|
+
`lxapp.json` must declare the lxapp security policy. New projects include an explicit deny-by-default policy:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"security": {
|
|
75
|
+
"network": {
|
|
76
|
+
"trustedDomains": []
|
|
77
|
+
},
|
|
78
|
+
"privileges": []
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Rules:
|
|
84
|
+
|
|
85
|
+
- `security.network.trustedDomains: []` denies all remote hosts.
|
|
86
|
+
- Use exact host names, for example `api.example.com` or `cdn.example.com`.
|
|
87
|
+
- Do not include scheme, path, or port. `https://api.example.com`, `api.example.com/path`, and `api.example.com:443` are invalid.
|
|
88
|
+
- Use `"*"` only when the lxapp intentionally allows all remote hosts, for example during local experiments.
|
|
89
|
+
- Do not combine `"*"` with host names. It is an explicit allow-all policy.
|
|
90
|
+
- Domain matching is host-only and normalized to lowercase.
|
|
91
|
+
- The policy is a host allowlist. It does not distinguish `http` and `https`; prefer HTTPS in production.
|
|
92
|
+
- The policy applies to Logic network requests, `lx.downloadFile`, `lx.uploadFile`, and WebView HTTPS resources resolved by LingXia.
|
|
93
|
+
- `security.privileges` is for high-risk host-defined capabilities such as automation or devtools. Ordinary APIs like media, camera, or location remain guarded by host and platform permission flows.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"security": {
|
|
100
|
+
"network": {
|
|
101
|
+
"trustedDomains": ["api.example.com", "cdn.example.com"]
|
|
102
|
+
},
|
|
103
|
+
"privileges": ["agent.automation"]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Native client
|
|
109
|
+
|
|
110
|
+
Views call Rust native APIs through a generated Native client. LxApp projects do not configure Rust source paths. Native host builds generate the client from the native Rust crate's `build.rs` with `lingxia-native-codegen`.
|
|
111
|
+
|
|
112
|
+
The CLI passes the canonical output path through `LINGXIA_NATIVE_CLIENT_OUT` during native cargo builds. React/Vue projects get `.lingxia/native.ts` and import it through `@lingxia/native`; HTML projects get `.lingxia/native.js`, which is copied into `dist/.lingxia/native.js` by the default static asset rules.
|
|
113
|
+
|
|
114
|
+
### Build
|
|
115
|
+
|
|
116
|
+
- `lingxia build` builds page assets and runtime artifacts into `dist/`.
|
|
117
|
+
- `lingxia build --release --package` produces package archive for publish.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Page Architecture
|
|
122
|
+
|
|
123
|
+
Every page is split into two layers that communicate through a bridge:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
┌─────────────────────────┐ setData() ┌──────────────────────────┐
|
|
127
|
+
│ View (WebView) │ ◄────────────────── │ Logic (Native Runtime) │
|
|
128
|
+
│ React/Vue + useLxPage │ ────────────────────► Page({}) instance │
|
|
129
|
+
│ │ bridge functions │ │
|
|
130
|
+
└─────────────────────────┘ └──────────────────────────┘
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**View** renders UI. **Logic** owns state and business operations. Logic pushes state to View via `setData()`, and View calls Logic functions through auto-generated bridge bindings.
|
|
134
|
+
|
|
135
|
+
Recommended reading path:
|
|
136
|
+
|
|
137
|
+
- This guide: page layout, `Page({})`, `useLxPage`, events, and native components.
|
|
138
|
+
- [Bridge Guide](./bridge.md): deeper mechanics of `setData`, stream, and channel.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Logic Layer — `Page({})`
|
|
143
|
+
|
|
144
|
+
The Logic file exports a `Page({})` call. The `Page` function is provided globally by the runtime — you don't import it.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// pages/home/index.ts
|
|
148
|
+
Page({
|
|
149
|
+
data: {
|
|
150
|
+
count: 0,
|
|
151
|
+
message: "Hello",
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
onLoad: function (options) {
|
|
155
|
+
// Called when page is created. `options` contains URL query params.
|
|
156
|
+
console.log("query:", options);
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
onShow: function () {
|
|
160
|
+
// Called every time the page becomes visible.
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// Action functions — callable from View
|
|
164
|
+
increment: function () {
|
|
165
|
+
this.setData({ count: this.data.count + 1 });
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
updateMessage: function (params) {
|
|
169
|
+
// params is whatever the View passes
|
|
170
|
+
this.setData({ message: params?.text || "" });
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Key concepts
|
|
176
|
+
|
|
177
|
+
| API | Description |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| `this.data` | Current page state. Read-only — use `setData()` to change. |
|
|
180
|
+
| `this.setData(patch)` | Merge `patch` into `data` and replicate to View. Triggers re-render. |
|
|
181
|
+
| `onLoad(options)` | Lifecycle — page created. `options` are URL query params. |
|
|
182
|
+
| `onShow()` | Lifecycle — page becomes visible (including back-navigation). |
|
|
183
|
+
| `lx.*` | Global platform APIs (e.g. `lx.setNavigationBarTitle()`, `lx.createVideoContext()`). |
|
|
184
|
+
|
|
185
|
+
### Private helpers
|
|
186
|
+
|
|
187
|
+
Functions starting with `_` are private — they are **not** exposed to the View. Use them for internal logic:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
Page({
|
|
191
|
+
data: { total: 0 },
|
|
192
|
+
|
|
193
|
+
_calculateTotal: function (items) {
|
|
194
|
+
return items.reduce((sum, item) => sum + item.price, 0);
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
onCheckout: function (params) {
|
|
198
|
+
const total = this._calculateTotal(params?.items || []);
|
|
199
|
+
this.setData({ total });
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## View Layer
|
|
207
|
+
|
|
208
|
+
The View file can be a standard React component, a Vue component, or an HTML module entry. The framework packages connect View to the Logic layer and expose:
|
|
209
|
+
|
|
210
|
+
- `data` — reactive page state replicated from Logic via `setData()`
|
|
211
|
+
- `actions` — public functions exported from `Page({})`
|
|
212
|
+
|
|
213
|
+
### Typing `PageData` and `PageActions`
|
|
214
|
+
|
|
215
|
+
The runtime guarantees that **(a)** `data` reflects Logic's initial `data: { … }` literal by first paint, and **(b)** every public method on `Page({})` is wired into `actions` during page setup. So in your typed shapes:
|
|
216
|
+
|
|
217
|
+
- **Required by default.** Fields you declare in `data: { … }` are always present; public methods are always callable. Mark them required.
|
|
218
|
+
- **Mark `?:` only when the field is genuinely populated lazily** — for example, a field that starts unset and is filled by `this.setData(…)` after an async fetch in `onLoad`.
|
|
219
|
+
|
|
220
|
+
Using all-`?` fields is a footgun: it propagates `actions.foo?.()` and `data?.x ?? default` through every component for no reason. Don't do that.
|
|
221
|
+
|
|
222
|
+
### React
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
// pages/home/index.tsx
|
|
226
|
+
import { useLxPage } from '@lingxia/react';
|
|
227
|
+
|
|
228
|
+
type PageData = {
|
|
229
|
+
count: number;
|
|
230
|
+
message: string;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
type PageActions = {
|
|
234
|
+
increment: () => void;
|
|
235
|
+
updateMessage: (params: { text: string }) => void;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export default function HomePage() {
|
|
239
|
+
const { data, actions } = useLxPage<PageData, PageActions>();
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div>
|
|
243
|
+
<p>Count: {data.count}</p>
|
|
244
|
+
<p>{data.message}</p>
|
|
245
|
+
<button onClick={() => actions.increment()}>+1</button>
|
|
246
|
+
<button onClick={() => actions.updateMessage({ text: 'World' })}>
|
|
247
|
+
Update
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Vue
|
|
255
|
+
|
|
256
|
+
```vue
|
|
257
|
+
<!-- pages/home/index.vue -->
|
|
258
|
+
<template>
|
|
259
|
+
<div>
|
|
260
|
+
<p>Count: {{ data.count }}</p>
|
|
261
|
+
<p>{{ data.message }}</p>
|
|
262
|
+
<button @click="actions.increment()">+1</button>
|
|
263
|
+
<button @click="actions.updateMessage({ text: 'World' })">Update</button>
|
|
264
|
+
</div>
|
|
265
|
+
</template>
|
|
266
|
+
|
|
267
|
+
<script setup lang="ts">
|
|
268
|
+
import { useLxPage } from '@lingxia/vue';
|
|
269
|
+
|
|
270
|
+
type PageData = {
|
|
271
|
+
count: number;
|
|
272
|
+
message: string;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
type PageActions = {
|
|
276
|
+
increment: () => void;
|
|
277
|
+
updateMessage: (params: { text: string }) => void;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const { data, actions } = useLxPage<PageData, PageActions>();
|
|
281
|
+
</script>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### HTML
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
// pages/home/entry.ts
|
|
288
|
+
import { getActions, subscribe } from '@lingxia/html';
|
|
289
|
+
|
|
290
|
+
type PageData = {
|
|
291
|
+
count: number;
|
|
292
|
+
message: string;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
type PageActions = {
|
|
296
|
+
increment: () => void;
|
|
297
|
+
updateMessage: (params: { text: string }) => void;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const actions = getActions<PageActions>();
|
|
301
|
+
const countEl = document.getElementById('count');
|
|
302
|
+
const messageEl = document.getElementById('message');
|
|
303
|
+
|
|
304
|
+
document.getElementById('inc-btn')?.addEventListener('click', () => {
|
|
305
|
+
actions.increment();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
subscribe((data: PageData) => {
|
|
309
|
+
if (countEl) countEl.textContent = String(data.count);
|
|
310
|
+
if (messageEl) messageEl.textContent = data.message;
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```html
|
|
315
|
+
<!-- pages/home/index.html -->
|
|
316
|
+
<script type="module" src="./entry.ts"></script>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### What `useLxPage()` returns
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
const { data, actions } = useLxPage<PageData, PageActions>();
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
- **`data`** — Reactive page state, updated whenever Logic calls `setData()`. In React this triggers a re-render; in Vue it's a `reactive()` object.
|
|
326
|
+
- **`actions`** — All public functions from `Page({})` (except lifecycle hooks and `_`-prefixed methods). Each action is a bridge function that calls through to the Logic layer.
|
|
327
|
+
|
|
328
|
+
Use typed `PageActions` interfaces so View and Logic stay aligned as your page grows.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Data Flow
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
Logic: this.setData({ count: 1 })
|
|
336
|
+
↓
|
|
337
|
+
Bridge: state replication (native → WebView)
|
|
338
|
+
↓
|
|
339
|
+
View: useLxPage().data updates → component re-renders
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
View: actions.increment?.()
|
|
344
|
+
↓
|
|
345
|
+
Bridge: function call (WebView → native → Logic JS runtime)
|
|
346
|
+
↓
|
|
347
|
+
Logic: increment() runs, may call this.setData() → cycle repeats
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
State flows **one way**: Logic → View. View never mutates `data` directly. Instead, View calls Logic actions which call `setData()` to update state.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Event Handling
|
|
355
|
+
|
|
356
|
+
### Two event paths
|
|
357
|
+
|
|
358
|
+
LingXia components support two event paths:
|
|
359
|
+
|
|
360
|
+
1. **Logic short path** (native → Rust → Logic JS, 3 hops) — when the handler is a function from `actions`. The CLI auto-generates `pageFuncBindings` so the native layer routes the event directly to Logic, bypassing the WebView.
|
|
361
|
+
|
|
362
|
+
2. **View DOM path** (native → WebView CustomEvent → handler, 2 hops) — when the handler is a local View function. Events arrive as standard DOM CustomEvents.
|
|
363
|
+
|
|
364
|
+
As a developer, you don't need to choose between these paths. Use framework-native event syntax (`onX` in React, `@event` in Vue) and the system routes automatically.
|
|
365
|
+
|
|
366
|
+
### Native component events
|
|
367
|
+
|
|
368
|
+
LingXia ships native-backed components (`LxInput`, `LxTextarea`, `LxPicker`, `LxVideo`, `LxMediaSwiper`, `LxNavigator`) from `@lingxia/react`, `@lingxia/vue`, and `@lingxia/html`. Event handlers use standard framework-native syntax:
|
|
369
|
+
|
|
370
|
+
**React:**
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
import { useLxPage, LxInput, LxPicker, LxVideo } from '@lingxia/react';
|
|
374
|
+
|
|
375
|
+
const { actions } = useLxPage<PageData, PageActions>();
|
|
376
|
+
|
|
377
|
+
// Input — handler receives unwrapped detail object
|
|
378
|
+
<LxInput onInput={actions.onInputChange} />
|
|
379
|
+
|
|
380
|
+
// Picker — handler receives resolved value directly
|
|
381
|
+
<LxPicker
|
|
382
|
+
columns={[['A', 'B', 'C']]}
|
|
383
|
+
onConfirm={(value) => actions.onPickerConfirm({ field: 'choice', value })}
|
|
384
|
+
/>
|
|
385
|
+
|
|
386
|
+
// Video — handler receives raw DOM Event
|
|
387
|
+
<LxVideo src={url} onPlaying={actions.onPlaying} />
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Vue:**
|
|
391
|
+
|
|
392
|
+
```vue
|
|
393
|
+
<script setup lang="ts">
|
|
394
|
+
import { useLxPage, LxInput, LxPicker, LxVideo } from '@lingxia/vue';
|
|
395
|
+
|
|
396
|
+
const { actions } = useLxPage<PageData, PageActions>();
|
|
397
|
+
</script>
|
|
398
|
+
|
|
399
|
+
<LxInput @input="actions.onInputChange" />
|
|
400
|
+
|
|
401
|
+
<LxPicker
|
|
402
|
+
:columns="[['A', 'B', 'C']]"
|
|
403
|
+
@confirm="(value) => actions.onPickerConfirm({ field: 'choice', value })"
|
|
404
|
+
/>
|
|
405
|
+
|
|
406
|
+
<LxVideo :src="url" @playing="actions.onPlaying" />
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Callback signatures vary by component
|
|
410
|
+
|
|
411
|
+
The framework wrappers unwrap or reshape some events; others come through as raw DOM `CustomEvent`. Quick reference:
|
|
412
|
+
|
|
413
|
+
| Component | Callback receives | Example |
|
|
414
|
+
| --- | --- | --- |
|
|
415
|
+
| `LxInput` / `LxTextarea` | Unwrapped `event.detail` object | `onInput(detail)` → `detail.value` |
|
|
416
|
+
| `LxPicker` | Resolved value directly (on `onConfirm`) | `onConfirm(value)` → `value` is `string \| string[]` |
|
|
417
|
+
| `LxVideo` | Raw DOM Event | `onPlaying(event)` → `event.detail` |
|
|
418
|
+
| `LxMediaSwiper` | Raw `CustomEvent` with typed `detail` | `onChange(e)` → `e.detail.index` |
|
|
419
|
+
| `LxNavigator` | Raw `CustomEvent` | `onFail(e)` → `e.detail.errMsg` |
|
|
420
|
+
|
|
421
|
+
**Full attribute, event, and behavior reference for every component — including imperative control of `LxVideo` via `lx.createVideoContext()` — lives in [`./components.md`](./components.md).**
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Action Shapes
|
|
426
|
+
|
|
427
|
+
From a page author's perspective, public `Page({})` methods come in three useful shapes:
|
|
428
|
+
|
|
429
|
+
| Logic method shape | Use from View | Typical use |
|
|
430
|
+
| --- | --- | --- |
|
|
431
|
+
| normal function / async function | `actions.foo(...)` from `useLxPage()` | button actions, navigation, one-shot work |
|
|
432
|
+
| async generator | `useLxStream(actions.foo, ...)` | progress, incremental output, chat-style streaming |
|
|
433
|
+
| channel-style session | `useLxChannel(actions.foo, ...)` | long-lived bidirectional sessions |
|
|
434
|
+
|
|
435
|
+
Examples:
|
|
436
|
+
|
|
437
|
+
- `increment()` and `updateMessage()` stay in the normal `actions` bucket.
|
|
438
|
+
- `async *onSend(...)` is a stream action and belongs with `useLxStream()`.
|
|
439
|
+
- Session-style logic that stays open over time belongs with `useLxChannel()`.
|
|
440
|
+
|
|
441
|
+
The runtime inspects the Logic method shape and routes it automatically. Use this guide for page authoring; use [Bridge Guide](./bridge.md) for stream/channel lifecycle, cancellation, and transport details.
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## App-wide lifecycle — `App({})`
|
|
446
|
+
|
|
447
|
+
`Page({})` defines a single page; **`App({})`** defines the **lxapp-wide singleton** — created once when the lxapp boots, shared by every page. Use it for app-scope state, cross-page coordination, and lifecycle hooks that fire regardless of which page is on screen.
|
|
448
|
+
|
|
449
|
+
Like `Page`, `App` is a runtime-provided global. Define it in a single file at the lxapp root (conventionally `app.ts`). It is **optional** — many lxapps don't need it.
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
// app.ts
|
|
453
|
+
interface AppGlobals {
|
|
454
|
+
userId: string;
|
|
455
|
+
theme: 'light' | 'dark';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
App({
|
|
459
|
+
globalData: <AppGlobals>{
|
|
460
|
+
userId: '',
|
|
461
|
+
theme: 'light',
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
async onLaunch(options) {
|
|
465
|
+
// Called once when the lxapp boots.
|
|
466
|
+
// `options`: AppLaunchOptions — { path?, query?, scene?, referrerInfo? }
|
|
467
|
+
// referrerInfo is populated when this lxapp was opened by another lxapp.
|
|
468
|
+
const stored = await lx.getStorage().get<string>('userId');
|
|
469
|
+
if (stored) this.globalData.userId = stored;
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
onShow(args) {
|
|
473
|
+
// Called every time the lxapp comes to the foreground.
|
|
474
|
+
// args: AppLifecycleEventArgs
|
|
475
|
+
// source: 'host' | 'lxapp'
|
|
476
|
+
// reason: 'foreground' | 'background' | 'screenshot' | 'open' | 'close' | 'switch_back' | 'switch_away'
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
onHide(args) {
|
|
480
|
+
// The lxapp is being backgrounded. Same AppLifecycleEventArgs shape.
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
onUserCaptureScreen() {
|
|
484
|
+
// The user took a screenshot while this lxapp was active.
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Read app-scope state from any page with `getApp<T>()`:
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
// pages/profile/index.ts
|
|
493
|
+
Page({
|
|
494
|
+
data: { userId: '' },
|
|
495
|
+
onLoad() {
|
|
496
|
+
const app = getApp<AppInstance & { globalData: AppGlobals }>();
|
|
497
|
+
if (app) this.setData({ userId: app.globalData.userId });
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Notes:
|
|
503
|
+
|
|
504
|
+
- `globalData` is a plain object. **Mutations are not reactive** — pages don't re-render when you change `app.globalData.x`. To propagate changes into the View, write to a page's `data` via `setData`.
|
|
505
|
+
- Lifecycle order on cold start: `App.onLaunch` → `App.onShow` → first page's `Page.onLoad` → `Page.onShow`. On foregrounding: `App.onShow` → top page's `Page.onShow`.
|
|
506
|
+
- `getCurrentPages()` returns the active page stack (top of stack last) when you need to coordinate across pages.
|
|
507
|
+
- Type declarations for `App`, `AppConfig`, `AppInstance`, `AppLaunchOptions`, `AppLifecycleEventArgs`, `getApp`, `getCurrentPages` come from [`@lingxia/types`](./lx-api.md#install-typing).
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Complete Example: Input Page
|
|
512
|
+
|
|
513
|
+
**Logic** (`pages/input/index.ts`):
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
Page({
|
|
517
|
+
data: {
|
|
518
|
+
inputValue: "",
|
|
519
|
+
syncValue: "",
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
onLoad: function () {},
|
|
523
|
+
|
|
524
|
+
onInputChange: function (detail) {
|
|
525
|
+
// detail is the unwrapped event.detail from LxInput
|
|
526
|
+
if (detail?.value === undefined) return;
|
|
527
|
+
console.log("input changed:", detail.value);
|
|
528
|
+
},
|
|
529
|
+
|
|
530
|
+
onSyncInput: function (detail) {
|
|
531
|
+
if (detail?.value === undefined) return;
|
|
532
|
+
// Write back to data → View re-renders with updated value
|
|
533
|
+
this.setData({ syncValue: String(detail.value) });
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**View** (`pages/input/index.tsx`):
|
|
539
|
+
|
|
540
|
+
```tsx
|
|
541
|
+
import { useLxPage, LxInput } from '@lingxia/react';
|
|
542
|
+
|
|
543
|
+
type PageData = { syncValue: string };
|
|
544
|
+
type PageActions = {
|
|
545
|
+
onInputChange: (detail: Record<string, unknown>) => void;
|
|
546
|
+
onSyncInput: (detail: Record<string, unknown>) => void;
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
export default function InputPage() {
|
|
550
|
+
const { data, actions } = useLxPage<PageData, PageActions>();
|
|
551
|
+
|
|
552
|
+
return (
|
|
553
|
+
<div>
|
|
554
|
+
<LxInput placeholder="Basic input" onInput={actions.onInputChange} />
|
|
555
|
+
|
|
556
|
+
<LxInput
|
|
557
|
+
value={data.syncValue}
|
|
558
|
+
placeholder="Synced input"
|
|
559
|
+
onInput={actions.onSyncInput}
|
|
560
|
+
/>
|
|
561
|
+
<p>Current: {data.syncValue}</p>
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
> Logic initializes `data: { syncValue: "" }`, so the field exists from first paint — required in the type.
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## Tab bar navigation
|
|
572
|
+
|
|
573
|
+
A **tab bar** is a persistent navigation strip — typically at the bottom of the screen — that shows the lxapp's primary pages. Tapping a tab switches the active page **without** push/pop semantics: the tab bar stays visible across all tab pages, and tab pages do not stack on each other.
|
|
574
|
+
|
|
575
|
+
> **Scope.** Tab bar is an **lxapp-internal navigation concept** declared in `lxapp.json`. It has nothing to do with host App UI surfaces — `lingxia.yaml.ui.surfaces` / `activators` live one layer up and describe the native shell (windows, panels, menu bars). A host shell renders an lxapp; that lxapp may have its own tab bar inside.
|
|
576
|
+
|
|
577
|
+
### Declaring the tab bar in `lxapp.json`
|
|
578
|
+
|
|
579
|
+
Add a `tabBar` block alongside `pages`:
|
|
580
|
+
|
|
581
|
+
```json
|
|
582
|
+
{
|
|
583
|
+
"appId": "my-app",
|
|
584
|
+
"version": "0.1.0",
|
|
585
|
+
"pages": [
|
|
586
|
+
{ "name": "home", "path": "pages/home/index" },
|
|
587
|
+
{ "name": "discover", "path": "pages/discover/index" },
|
|
588
|
+
{ "name": "profile", "path": "pages/profile/index" }
|
|
589
|
+
],
|
|
590
|
+
"tabBar": {
|
|
591
|
+
"color": "#999999",
|
|
592
|
+
"selectedColor": "#1677ff",
|
|
593
|
+
"backgroundColor": "#ffffff",
|
|
594
|
+
"borderStyle": "#eeeeee",
|
|
595
|
+
"position": "bottom",
|
|
596
|
+
"list": [
|
|
597
|
+
{
|
|
598
|
+
"text": "Home",
|
|
599
|
+
"pagePath": "pages/home/index",
|
|
600
|
+
"iconPath": "public/home.png",
|
|
601
|
+
"selectedIconPath": "public/home_selected.png",
|
|
602
|
+
"selected": true
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
"text": "Discover",
|
|
606
|
+
"pagePath": "pages/discover/index",
|
|
607
|
+
"iconPath": "public/discover.png"
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
"text": "Profile",
|
|
611
|
+
"pagePath": "pages/profile/index",
|
|
612
|
+
"iconPath": "public/profile.png"
|
|
613
|
+
}
|
|
614
|
+
]
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
Rules:
|
|
620
|
+
|
|
621
|
+
- Every `list[].pagePath` must match a registered page path under `pages[]`.
|
|
622
|
+
- `iconPath` / `selectedIconPath` are project-relative — usually under `public/` so they're copied verbatim into `dist/` by the default static-assets rule.
|
|
623
|
+
- `selected: true` on one entry picks the initial tab; if omitted, the first entry is selected.
|
|
624
|
+
- `position`: `"bottom"` (default) or `"top"`.
|
|
625
|
+
|
|
626
|
+
### Switching tabs at runtime
|
|
627
|
+
|
|
628
|
+
From Logic, use `lx.switchTab(...)`. **`lx.navigateTo` and `lx.redirectTo` do not work on tab pages** — the runtime rejects them with errors like `"redirectTo cannot navigate to a tabBar page"`. Switching is the only way in and out of tabs:
|
|
629
|
+
|
|
630
|
+
```ts
|
|
631
|
+
lx.switchTab({ url: '/pages/profile/index' });
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
`lx.navigateBack` still works for popping non-tab pages that were pushed on top of the current tab.
|
|
635
|
+
|
|
636
|
+
### Modifying the tab bar after declaration
|
|
637
|
+
|
|
638
|
+
The `lx.setTabBar*` family **mutates an already-declared tab bar** — none of these create or remove tabs. If the lxapp has no `tabBar` in `lxapp.json`, every call returns `false`.
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
lx.setTabBarItem({ index: 1, text: 'Inbox', iconPath: 'public/inbox.png' });
|
|
642
|
+
lx.setTabBarBadge({ index: 1, text: '3' });
|
|
643
|
+
lx.removeTabBarBadge({ index: 1 });
|
|
644
|
+
lx.showTabBarRedDot({ index: 0 });
|
|
645
|
+
lx.hideTabBarRedDot({ index: 0 });
|
|
646
|
+
lx.setTabBarStyle({ selectedColor: '#ff0000' });
|
|
647
|
+
lx.showTabBar();
|
|
648
|
+
lx.hideTabBar();
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Full option shapes: [`./lx-api.md#page-chrome--ui`](./lx-api.md#page-chrome--ui).
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## Common Pitfalls
|
|
656
|
+
|
|
657
|
+
- Mixing view logic and page logic in one file; keep `index.tsx` and `index.ts` roles clear.
|
|
658
|
+
- Mutating `data` directly in View instead of calling Logic actions.
|
|
659
|
+
- Re-documenting bridge behavior inside page code instead of leaning on [Bridge Guide](./bridge.md) for stream/channel details.
|
|
660
|
+
- Assuming every component's event handler receives the same shape — `LxInput` unwraps `event.detail`, `LxVideo` passes the raw DOM `Event`. See [Components](./components.md#callback-shapes-by-component).
|
|
661
|
+
- Skipping `@lingxia/types` in the lxapp's devDependencies and losing intellisense on the entire `lx.*` surface. See [Logic-side `lx.*` API](./lx-api.md).
|
|
662
|
+
- Forgetting that only public `Page({})` methods become actions; lifecycle hooks and `_`-prefixed helpers are not exposed.
|
|
663
|
+
- Mutating `App({}).globalData` and expecting page views to re-render — `globalData` is not reactive. Propagate to a page's `data` via `setData`.
|
|
664
|
+
- Calling `lx.navigateTo` / `lx.redirectTo` on a tab page — rejected by the runtime. Use `lx.switchTab` for tab-page entry; `navigateBack` for non-tab stack pops.
|
|
665
|
+
- Treating the tab bar as a host UI surface — it is an lxapp-internal feature declared in `lxapp.json`, orthogonal to `lingxia.yaml.ui` surfaces/activators.
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
## Tips
|
|
670
|
+
|
|
671
|
+
- **Type your data**: Define a `PageData` type in both Logic and View to catch mismatches early.
|
|
672
|
+
- **Keep Logic pure**: Logic has no DOM access. Use `lx.*` APIs for platform operations, `setData()` for state.
|
|
673
|
+
- **Avoid heavy View state**: Prefer Logic-managed state via `setData()` over local `useState`/`ref`. This keeps state consistent across the bridge boundary.
|
|
674
|
+
- **Private with `_` prefix**: Functions starting with `_` won't be exposed to View. Use them for internal helpers.
|
|
675
|
+
- **Page config**: `index.json` controls navigation bar title, background color, and other page-level settings.
|