@lingfan-engine/ui 0.1.4 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingfan-engine/ui",
3
- "version": "0.1.4",
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.1.4"
16
+ "@lingfan-engine/core": "^0.2.0"
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",
@@ -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
 
@@ -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;
@@ -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: { prompt: input.inputPrompt, submitText: texts.inputSubmit },
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;
@@ -138,6 +138,7 @@ export function createNarrativeOverlay(
138
138
  menuPrompt: s.menuPrompt,
139
139
  menuOptions: s.menuOptions,
140
140
  inputPrompt: s.inputPrompt,
141
+ inputCandidates: s.inputCandidates,
141
142
  },
142
143
  choicesDeps,
143
144
  ),
@@ -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();
@@ -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
  /**