@lingfan-engine/ui 0.1.5 → 0.2.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/package.json +2 -2
- package/src/dom/materialize.ts +1 -1
- package/src/dom/overlay-surface.ts +16 -0
- package/src/element/render.ts +1 -1
- package/src/host/state-view.ts +5 -0
- package/src/overlay/choices-view.ts +12 -1
- package/src/overlay/create.ts +1 -0
- package/src/overlay/projector.ts +8 -0
- package/src/overlay/surface.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lingfan-engine/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "灵泛叙事引擎参考展示层:框架无关的可测纯逻辑(内联标记、打字机、音频差量规划)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"sideEffects": false,
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@lingfan-engine/core": "^0.
|
|
16
|
+
"@lingfan-engine/core": "^0.2.0"
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
package/src/dom/materialize.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 本目录是 `packages/ui` 内**唯一**允许出现 DOM 类型与 DOM 全局的目录。
|
|
3
3
|
*
|
|
4
4
|
* 与 `packages/ui/src/host/index.ts` 同一口气的规则:其余目录一律只产出 `RenderSpec`;
|
|
5
|
-
* 需要碰 DOM 的地方经 `RenderPort`
|
|
5
|
+
* 需要碰 DOM 的地方经 `RenderPort` 完成到这里,别的平台(小程序)在同一位置换成自己的物化器。
|
|
6
6
|
*/
|
|
7
7
|
import type {
|
|
8
8
|
RenderEvent,
|
|
@@ -229,6 +229,22 @@ export function createDomOverlaySurface(
|
|
|
229
229
|
});
|
|
230
230
|
form.append(field, submit);
|
|
231
231
|
choices.append(label, form);
|
|
232
|
+
// 候选按钮排在输入表单之后:点一项直接以该项 value 走同一提交出口
|
|
233
|
+
if (view.input.candidates.length > 0) {
|
|
234
|
+
const row = doc().createElement("div");
|
|
235
|
+
row.className = "lf-choice-row";
|
|
236
|
+
for (const candidate of view.input.candidates) {
|
|
237
|
+
const button = clickable(doc().createElement("button"));
|
|
238
|
+
button.type = "button";
|
|
239
|
+
button.textContent = candidate.label;
|
|
240
|
+
button.setAttribute("aria-label", candidate.label);
|
|
241
|
+
button.addEventListener("click", () =>
|
|
242
|
+
surface.onSubmitInput?.(candidate.value),
|
|
243
|
+
);
|
|
244
|
+
row.appendChild(button);
|
|
245
|
+
}
|
|
246
|
+
choices.appendChild(row);
|
|
247
|
+
}
|
|
232
248
|
return;
|
|
233
249
|
}
|
|
234
250
|
|
package/src/element/render.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { RenderPort, RenderSpec } from "./spec";
|
|
|
16
16
|
* 其余三个回调不传即「没有对应的宿主能力」(表现为元素不可点、表达式不求解、资源显示替代文本)。
|
|
17
17
|
*
|
|
18
18
|
* `port` 是宿主注入的渲染端口(Web 侧 `createDomRenderPort`)——渲染器只产出描述树,
|
|
19
|
-
*
|
|
19
|
+
* 平台差异全部完成在端口实现里。
|
|
20
20
|
*/
|
|
21
21
|
export interface ElementTreeRenderOptions {
|
|
22
22
|
registry: ElementRegistry;
|
package/src/host/state-view.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface StateKeyTable {
|
|
|
62
62
|
menuOptions: string;
|
|
63
63
|
/** 菜单选项目标 */
|
|
64
64
|
menuTargets: string;
|
|
65
|
+
/** 输入候选(可选:不订阅则输入等待只有输入框,行为同加候选之前) */
|
|
66
|
+
inputOptions?: string;
|
|
65
67
|
/** 对话框显隐 */
|
|
66
68
|
dialogVisible: string;
|
|
67
69
|
/** 舞台元素表 */
|
|
@@ -140,6 +142,9 @@ export function createStateView(
|
|
|
140
142
|
// 选项与目标两键分别写入:任一到达都做一次整体重建
|
|
141
143
|
if (key === keys.menuOptions || key === keys.menuTargets)
|
|
142
144
|
return { kind: "choices" };
|
|
145
|
+
// 输入候选与输入提示同属选择层:候选到达也重建(否则点了候选后旧候选会留在屏上)
|
|
146
|
+
if (keys.inputOptions !== undefined && key === keys.inputOptions)
|
|
147
|
+
return { kind: "choices" };
|
|
143
148
|
if (key === keys.dialogVisible)
|
|
144
149
|
return { kind: "dialog-visible", hidden: value === "hide" };
|
|
145
150
|
if (key === keys.elements) {
|
|
@@ -15,6 +15,8 @@ export interface ChoicesRenderInput {
|
|
|
15
15
|
menuPrompt: string;
|
|
16
16
|
menuOptions: readonly { text: string; target: string }[];
|
|
17
17
|
inputPrompt: string;
|
|
18
|
+
/** 输入候选(缺省空数组 = 只有输入框) */
|
|
19
|
+
inputCandidates?: readonly { text: string; value: string }[];
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/** 选择层文案(宿主可覆盖;缺省实现即内建文案) */
|
|
@@ -50,11 +52,20 @@ export function choicesLayerView(
|
|
|
50
52
|
): ChoicesLayerView | null {
|
|
51
53
|
if (input.waiting === "input") {
|
|
52
54
|
const texts: ChoicesTexts = { ...DEFAULT_CHOICES_TEXTS, ...deps.texts };
|
|
55
|
+
// 候选与输入框并存:玩家可以点候选,也可以自己打字(两者走同一个提交出口)
|
|
56
|
+
const candidates = (input.inputCandidates ?? []).map((item) => ({
|
|
57
|
+
label: item.text,
|
|
58
|
+
value: item.value,
|
|
59
|
+
}));
|
|
53
60
|
return {
|
|
54
61
|
rootClass: "",
|
|
55
62
|
promptHtml: "",
|
|
56
63
|
options: [],
|
|
57
|
-
input: {
|
|
64
|
+
input: {
|
|
65
|
+
prompt: input.inputPrompt,
|
|
66
|
+
submitText: texts.inputSubmit,
|
|
67
|
+
candidates,
|
|
68
|
+
},
|
|
58
69
|
};
|
|
59
70
|
}
|
|
60
71
|
if (input.waiting !== "menu") return null;
|
package/src/overlay/create.ts
CHANGED
package/src/overlay/projector.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface RenderState {
|
|
|
43
43
|
menuPrompt: string;
|
|
44
44
|
menuOptions: Array<{ text: string; target: string }>;
|
|
45
45
|
inputPrompt: string;
|
|
46
|
+
/** 输入候选(显示文本与要填的值成对;空数组 = 只有输入框) */
|
|
47
|
+
inputCandidates: Array<{ text: string; value: string }>;
|
|
46
48
|
nvlMode: string;
|
|
47
49
|
nvlBuffer: string[];
|
|
48
50
|
elementList: ElementInstance[];
|
|
@@ -103,6 +105,7 @@ export function createProjector(deps: {
|
|
|
103
105
|
menuPrompt: "",
|
|
104
106
|
menuOptions: [],
|
|
105
107
|
inputPrompt: "",
|
|
108
|
+
inputCandidates: [],
|
|
106
109
|
nvlMode: "none",
|
|
107
110
|
nvlBuffer: [],
|
|
108
111
|
elementList: [],
|
|
@@ -190,6 +193,10 @@ export function createProjector(deps: {
|
|
|
190
193
|
state.inputPrompt = reader.inputPrompt();
|
|
191
194
|
return { kind: "render", targets: ["choices"] };
|
|
192
195
|
}
|
|
196
|
+
if (key === SYS.inputOptions) {
|
|
197
|
+
state.inputCandidates = reader.inputCandidates();
|
|
198
|
+
return { kind: "render", targets: ["choices"] };
|
|
199
|
+
}
|
|
193
200
|
if (key === SYS.nvlMode) {
|
|
194
201
|
const next = reader.nvlMode();
|
|
195
202
|
state.nvlMode = next === "" ? "none" : next;
|
|
@@ -218,6 +225,7 @@ export function createProjector(deps: {
|
|
|
218
225
|
state.menuPrompt = reader.menuPrompt();
|
|
219
226
|
readMenuOptions();
|
|
220
227
|
state.inputPrompt = reader.inputPrompt();
|
|
228
|
+
state.inputCandidates = reader.inputCandidates();
|
|
221
229
|
const mode = reader.nvlMode();
|
|
222
230
|
state.nvlMode = mode === "" ? "none" : mode;
|
|
223
231
|
state.nvlBuffer = reader.nvlLines();
|
package/src/overlay/surface.ts
CHANGED
|
@@ -62,6 +62,19 @@ export interface ChoicesLayerInput {
|
|
|
62
62
|
prompt: string;
|
|
63
63
|
/** 提交按钮显示文本 */
|
|
64
64
|
submitText: string;
|
|
65
|
+
/**
|
|
66
|
+
* 候选快捷值(缺省空数组 = 只有输入框):点一项即把该项 `value` 当输入值提交,
|
|
67
|
+
* 输入框仍可用。`label` 是显示文本(可能已翻译),`value` 是要写进变量的原样值。
|
|
68
|
+
*/
|
|
69
|
+
candidates: readonly ChoicesLayerCandidate[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 输入候选(显示文本与要填的值成对;与菜单选项不同——它不跳列,只替玩家填值) */
|
|
73
|
+
export interface ChoicesLayerCandidate {
|
|
74
|
+
/** 显示文本(已走翻译;也用作按钮的可访问名) */
|
|
75
|
+
label: string;
|
|
76
|
+
/** 要填进变量的原样值(不进翻译表) */
|
|
77
|
+
value: string;
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
/**
|