@intent-framework/dom 0.1.0-alpha.6 → 0.1.0-alpha.7

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.
Files changed (2) hide show
  1. package/dist/index.js +247 -143
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -63,15 +63,18 @@ function renderRouter(router, options) {
63
63
 
64
64
  //#endregion
65
65
  //#region src/index.ts
66
- function getReasonId(actId) {
67
- return `${actId}-reason`;
66
+ function getReasonId(actId, suffix = "") {
67
+ return `${actId}-reason${suffix}`;
68
68
  }
69
- function getEnterHintId(askId) {
70
- return `${askId}-enter-hint`;
69
+ function getEnterHintId(askId, suffix = "") {
70
+ return `${askId}-enter-hint${suffix}`;
71
71
  }
72
72
  function sanitizeLabel(label) {
73
73
  return label.replace(/\.+$/, "");
74
74
  }
75
+ function surfaceSuffix(surfaceName, isMulti) {
76
+ return isMulti ? `--${surfaceName}` : "";
77
+ }
75
78
  function findDefaultAction(acts) {
76
79
  const primaryActs = acts.filter((a) => a.primary);
77
80
  if (primaryActs.length === 1) return primaryActs[0];
@@ -81,19 +84,22 @@ function findDefaultAction(acts) {
81
84
  function renderDom(screenDef, options) {
82
85
  const { target, services, showScreenName, showSemanticIds } = options;
83
86
  target.innerHTML = "";
87
+ const isMulti = screenDef.surfaces.length > 1;
84
88
  const root = buildDom(screenDef, showScreenName, showSemanticIds);
85
89
  target.appendChild(root);
86
90
  const runtime = createScreenRuntime(screenDef, { services });
87
91
  runtime.start();
88
- const form = target.querySelector("form");
89
- const output = target.querySelector("output#feedback-output");
90
92
  const unsubscribers = [];
91
93
  for (const act of screenDef.acts) {
92
- const button = form.querySelector(`#${act.id}`);
93
- if (button) {
94
- const unsub = act.enabled.subscribe(() => {
94
+ const unsub = act.enabled.subscribe(() => {
95
+ for (const surface of screenDef.surfaces) {
96
+ const suffix = surfaceSuffix(surface.name, isMulti);
97
+ const form = getSurfaceForm(root, surface, isMulti);
98
+ if (!form) continue;
99
+ const button = form.querySelector(`#${act.id}${suffix}`);
100
+ if (!button) continue;
95
101
  button.disabled = !act.enabled.current;
96
- const reasonId = getReasonId(act.id);
102
+ const reasonId = getReasonId(act.id, suffix);
97
103
  let reasonEl = form.querySelector(`#${reasonId}`);
98
104
  if (!act.enabled.current && act.blockedReasons.length > 0) {
99
105
  button.setAttribute("aria-describedby", reasonId);
@@ -109,18 +115,27 @@ function renderDom(screenDef, options) {
109
115
  button.removeAttribute("aria-describedby");
110
116
  if (reasonEl) reasonEl.remove();
111
117
  }
112
- });
113
- unsubscribers.push(unsub);
114
- }
118
+ }
119
+ });
120
+ unsubscribers.push(unsub);
115
121
  }
116
122
  for (const act of screenDef.acts) {
117
123
  const unsub = act.onStatusChange(() => {
118
- updateFeedback(act, output);
124
+ for (const surface of screenDef.surfaces) {
125
+ const suffix = surfaceSuffix(surface.name, isMulti);
126
+ const form = getSurfaceForm(root, surface, isMulti);
127
+ if (!form) continue;
128
+ const output = form.querySelector(`#feedback-output${suffix}`);
129
+ if (output) updateFeedback(act, output);
130
+ }
119
131
  });
120
132
  unsubscribers.push(unsub);
121
133
  }
122
- for (const act of screenDef.acts) {
123
- const button = form.querySelector(`#${act.id}`);
134
+ for (const act of screenDef.acts) for (const surface of screenDef.surfaces) {
135
+ const suffix = surfaceSuffix(surface.name, isMulti);
136
+ const form = getSurfaceForm(root, surface, isMulti);
137
+ if (!form) continue;
138
+ const button = form.querySelector(`#${act.id}${suffix}`);
124
139
  if (button) button.addEventListener("click", () => {
125
140
  if (act.enabled.current) runtime.executeAct(act);
126
141
  });
@@ -129,52 +144,88 @@ function renderDom(screenDef, options) {
129
144
  if (defaultActionForHint) {
130
145
  const unsub = defaultActionForHint.enabled.subscribe(() => {
131
146
  const isEnabled = defaultActionForHint.enabled.current;
132
- for (const ask of screenDef.asks) {
133
- const input = form.querySelector(`#${ask.id}`);
134
- const hint = form.querySelector(`#${getEnterHintId(ask.id)}`);
135
- if (input && hint) {
136
- const hintId = getEnterHintId(ask.id);
137
- if (isEnabled) {
138
- hint.style.display = "";
139
- const existing = input.getAttribute("aria-describedby") || "";
140
- const ids = existing.split(/\s+/).filter(Boolean);
141
- if (!ids.includes(hintId)) ids.push(hintId);
142
- input.setAttribute("aria-describedby", ids.join(" "));
143
- } else {
144
- hint.style.display = "none";
145
- const existing = input.getAttribute("aria-describedby") || "";
146
- const ids = existing.split(/\s+/).filter(Boolean).filter((id) => id !== hintId);
147
- if (ids.length > 0) input.setAttribute("aria-describedby", ids.join(" "));
148
- else input.removeAttribute("aria-describedby");
147
+ for (const surface of screenDef.surfaces) {
148
+ const suffix = surfaceSuffix(surface.name, isMulti);
149
+ const form = getSurfaceForm(root, surface, isMulti);
150
+ if (!form) continue;
151
+ for (const item of surface.items) {
152
+ if (!("state" in item)) continue;
153
+ const ask = item;
154
+ const input = form.querySelector(`#${ask.id}${suffix}`);
155
+ const hint = form.querySelector(`#${getEnterHintId(ask.id, suffix)}`);
156
+ if (input && hint) {
157
+ const hintId = getEnterHintId(ask.id, suffix);
158
+ if (isEnabled) {
159
+ hint.style.display = "";
160
+ const existing = input.getAttribute("aria-describedby") || "";
161
+ const ids = existing.split(/\s+/).filter(Boolean);
162
+ if (!ids.includes(hintId)) ids.push(hintId);
163
+ input.setAttribute("aria-describedby", ids.join(" "));
164
+ } else {
165
+ hint.style.display = "none";
166
+ const existing = input.getAttribute("aria-describedby") || "";
167
+ const ids = existing.split(/\s+/).filter(Boolean).filter((id) => id !== hintId);
168
+ if (ids.length > 0) input.setAttribute("aria-describedby", ids.join(" "));
169
+ else input.removeAttribute("aria-describedby");
170
+ }
149
171
  }
150
172
  }
151
173
  }
152
174
  });
153
175
  unsubscribers.push(unsub);
154
176
  }
155
- for (const ask of screenDef.asks) {
156
- const input = form.querySelector(`#${ask.id}`);
157
- if (input) {
158
- const onKeyDown = (event) => {
159
- if (event.key !== "Enter") return;
160
- if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
161
- if (input.tagName === "TEXTAREA") return;
162
- if (input.tagName === "SELECT") return;
163
- if (input.type === "checkbox") return;
164
- const defaultAction = findDefaultAction(screenDef.acts);
165
- if (!defaultAction || !defaultAction.enabled.current) return;
166
- event.preventDefault();
167
- runtime.executeAct(defaultAction);
168
- };
169
- input.addEventListener("keydown", onKeyDown);
170
- unsubscribers.push(() => input.removeEventListener("keydown", onKeyDown));
177
+ for (const surface of screenDef.surfaces) {
178
+ const suffix = surfaceSuffix(surface.name, isMulti);
179
+ const form = getSurfaceForm(root, surface, isMulti);
180
+ if (!form) continue;
181
+ for (const item of surface.items) {
182
+ if (!("state" in item)) continue;
183
+ const ask = item;
184
+ const input = form.querySelector(`#${ask.id}${suffix}`);
185
+ if (input) {
186
+ const onKeyDown = (event) => {
187
+ if (event.key !== "Enter") return;
188
+ if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
189
+ if (input.tagName === "TEXTAREA") return;
190
+ if (input.tagName === "SELECT") return;
191
+ if (input.type === "checkbox") return;
192
+ const defaultAction = findDefaultAction(screenDef.acts);
193
+ if (!defaultAction || !defaultAction.enabled.current) return;
194
+ event.preventDefault();
195
+ runtime.executeAct(defaultAction);
196
+ };
197
+ input.addEventListener("keydown", onKeyDown);
198
+ unsubscribers.push(() => input.removeEventListener("keydown", onKeyDown));
199
+ }
171
200
  }
172
201
  }
202
+ for (const ask of screenDef.asks) {
203
+ const unsub = ask.subscribe(() => {
204
+ for (const surface of screenDef.surfaces) {
205
+ const suffix = surfaceSuffix(surface.name, isMulti);
206
+ const form = getSurfaceForm(root, surface, isMulti);
207
+ if (!form) continue;
208
+ const control = form.querySelector(`#${ask.id}${suffix}`);
209
+ if (!control) continue;
210
+ if (typeof ask.state.value === "boolean") control.checked = ask.state.value;
211
+ else if (ask.kind === "choice") control.value = ask.state.value;
212
+ else control.value = ask.state.value;
213
+ }
214
+ });
215
+ unsubscribers.push(unsub);
216
+ }
173
217
  return () => {
174
218
  for (const unsub of unsubscribers) unsub();
175
219
  runtime.dispose();
176
220
  };
177
221
  }
222
+ function getSurfaceForm(root, surface, isMulti) {
223
+ if (isMulti) {
224
+ const section = root.querySelector(`#${surface.id}`);
225
+ return section ? section.querySelector("form") : null;
226
+ }
227
+ return root.querySelector("form");
228
+ }
178
229
  function buildDom(screenDef, showScreenName, showSemanticIds) {
179
230
  let inspected;
180
231
  let askSemanticIds;
@@ -184,118 +235,171 @@ function buildDom(screenDef, showScreenName, showSemanticIds) {
184
235
  askSemanticIds = new Map(inspected.asks.map((a) => [a.id, a.semanticId]));
185
236
  actSemanticIds = new Map(inspected.acts.map((a) => [a.id, a.semanticId]));
186
237
  }
187
- const surface = screenDef.surfaces[0];
238
+ const isMulti = screenDef.surfaces.length > 1;
188
239
  const main = document.createElement("main");
189
- if (surface) main.id = surface.id;
190
240
  if (showSemanticIds && inspected) main.setAttribute("data-intent-screen", inspected.semanticId);
191
241
  if (showScreenName) {
192
242
  const heading = document.createElement("h1");
193
243
  heading.textContent = screenDef.name;
194
244
  main.appendChild(heading);
195
245
  }
246
+ if (isMulti) for (const surface of screenDef.surfaces) {
247
+ const section = buildSurface(screenDef, surface, showSemanticIds, askSemanticIds, actSemanticIds, isMulti);
248
+ main.appendChild(section);
249
+ }
250
+ else if (screenDef.surfaces.length === 1) {
251
+ const surface = screenDef.surfaces[0];
252
+ main.id = surface.id;
253
+ const form = buildForm(screenDef, surface, showSemanticIds, askSemanticIds, actSemanticIds, isMulti);
254
+ main.appendChild(form);
255
+ }
256
+ return main;
257
+ }
258
+ function buildSurface(screenDef, surface, showSemanticIds, askSemanticIds, actSemanticIds, isMulti) {
259
+ const section = document.createElement("section");
260
+ section.id = surface.id;
261
+ section.setAttribute("aria-label", surface.name);
262
+ const form = buildForm(screenDef, surface, showSemanticIds, askSemanticIds, actSemanticIds, isMulti);
263
+ section.appendChild(form);
264
+ return section;
265
+ }
266
+ function buildForm(screenDef, surface, showSemanticIds, askSemanticIds, actSemanticIds, isMulti) {
267
+ const isMultiSurface = isMulti ?? screenDef.surfaces.length > 1;
268
+ const suffix = surfaceSuffix(surface.name, isMultiSurface);
196
269
  const form = document.createElement("form");
197
270
  form.setAttribute("method", "POST");
198
271
  form.setAttribute("novalidate", "");
199
- for (const ask of screenDef.asks) {
200
- const container = document.createElement("div");
201
- container.className = "ask-group";
202
- const label = document.createElement("label");
203
- label.textContent = ask.label;
204
- label.htmlFor = ask.id;
205
- if (showSemanticIds && askSemanticIds) {
206
- const sid = askSemanticIds.get(ask.id);
207
- if (sid) label.setAttribute("data-intent-ask", sid);
208
- }
209
- container.appendChild(label);
210
- const control = createInputForAsk(ask);
211
- control.id = ask.id;
212
- control.name = ask.id;
213
- if (showSemanticIds && askSemanticIds) {
214
- const sid = askSemanticIds.get(ask.id);
215
- if (sid) control.setAttribute("data-intent-ask", sid);
216
- }
217
- if (ask.required) control.required = true;
218
- if (ask.kind === "contact" && ask.contactKind) control.setAttribute("autocomplete", ask.contactKind);
219
- if (typeof ask.state.value === "boolean") {
220
- const stateObj = ask.state;
221
- control.checked = stateObj.value;
222
- control.addEventListener("change", () => {
223
- stateObj.set(control.checked);
224
- });
225
- } else if (ask.kind === "choice") {
226
- const stateObj = ask.state;
227
- const select = control;
228
- for (const opt of stateObj.options) {
229
- const option = document.createElement("option");
230
- option.value = opt;
231
- option.textContent = opt;
232
- select.appendChild(option);
233
- }
234
- select.value = stateObj.value;
235
- select.addEventListener("change", () => {
236
- stateObj.set(select.value);
272
+ if (isMultiSurface) {
273
+ for (const item of surface.items) if ("state" in item) buildAskControl(form, screenDef, item, suffix, showSemanticIds, askSemanticIds);
274
+ else if ("handler" in item) buildActionButton(form, item, suffix, showSemanticIds, actSemanticIds);
275
+ } else {
276
+ const surfaceAskIds = /* @__PURE__ */ new Set();
277
+ const surfaceActIds = /* @__PURE__ */ new Set();
278
+ const orderedItems = [];
279
+ for (const item of surface.items) if ("state" in item) {
280
+ const ask = item;
281
+ surfaceAskIds.add(ask.id);
282
+ orderedItems.push({
283
+ kind: "ask",
284
+ node: ask
237
285
  });
238
- } else {
239
- const textInput = control;
240
- textInput.addEventListener("input", () => {
241
- const stateObj = ask.state;
242
- if (typeof stateObj.set === "function") stateObj.set(textInput.value);
286
+ } else if ("handler" in item) {
287
+ const act = item;
288
+ surfaceActIds.add(act.id);
289
+ orderedItems.push({
290
+ kind: "act",
291
+ node: act
243
292
  });
244
293
  }
245
- container.appendChild(control);
246
- if (ask.hintText) {
247
- const hint = document.createElement("p");
248
- hint.id = `${ask.id}-hint`;
249
- hint.textContent = ask.hintText;
250
- container.appendChild(hint);
251
- }
252
- const defaultAction = findDefaultAction(screenDef.acts);
253
- if (defaultAction) {
254
- const hintId = getEnterHintId(ask.id);
255
- const hint = document.createElement("p");
256
- hint.id = hintId;
257
- hint.textContent = `Press Enter to ${sanitizeLabel(defaultAction.label)}.`;
258
- if (!defaultAction.enabled.current) hint.style.display = "none";
259
- container.appendChild(hint);
260
- if (defaultAction.enabled.current) {
261
- const existing = control.getAttribute("aria-describedby");
262
- if (existing) control.setAttribute("aria-describedby", `${existing} ${hintId}`);
263
- else control.setAttribute("aria-describedby", hintId);
264
- }
294
+ for (const ask of screenDef.asks) if (!surfaceAskIds.has(ask.id)) orderedItems.push({
295
+ kind: "ask",
296
+ node: ask
297
+ });
298
+ for (const act of screenDef.acts) if (!surfaceActIds.has(act.id)) orderedItems.push({
299
+ kind: "act",
300
+ node: act
301
+ });
302
+ for (const item of orderedItems) if (item.kind === "ask") buildAskControl(form, screenDef, item.node, suffix, showSemanticIds, askSemanticIds);
303
+ else buildActionButton(form, item.node, suffix, showSemanticIds, actSemanticIds);
304
+ }
305
+ const output = document.createElement("output");
306
+ output.id = `feedback-output${suffix}`;
307
+ output.setAttribute("aria-live", "polite");
308
+ form.appendChild(output);
309
+ return form;
310
+ }
311
+ function buildAskControl(form, screenDef, ask, suffix, showSemanticIds, askSemanticIds) {
312
+ const container = document.createElement("div");
313
+ container.className = "ask-group";
314
+ const label = document.createElement("label");
315
+ label.textContent = ask.label;
316
+ label.htmlFor = `${ask.id}${suffix}`;
317
+ if (showSemanticIds && askSemanticIds) {
318
+ const sid = askSemanticIds.get(ask.id);
319
+ if (sid) label.setAttribute("data-intent-ask", sid);
320
+ }
321
+ container.appendChild(label);
322
+ const control = createInputForAsk(ask);
323
+ control.id = `${ask.id}${suffix}`;
324
+ control.name = `${ask.id}${suffix}`;
325
+ if (showSemanticIds && askSemanticIds) {
326
+ const sid = askSemanticIds.get(ask.id);
327
+ if (sid) control.setAttribute("data-intent-ask", sid);
328
+ }
329
+ if (ask.required) control.required = true;
330
+ if (ask.kind === "contact" && ask.contactKind) control.setAttribute("autocomplete", ask.contactKind);
331
+ const stateObj = ask.state;
332
+ if (typeof ask.state.value === "boolean") {
333
+ control.checked = stateObj.value;
334
+ control.addEventListener("change", () => {
335
+ stateObj.set(control.checked);
336
+ });
337
+ } else if (ask.kind === "choice") {
338
+ const select = control;
339
+ const options = stateObj.options;
340
+ for (const opt of options) {
341
+ const option = document.createElement("option");
342
+ option.value = opt;
343
+ option.textContent = opt;
344
+ select.appendChild(option);
265
345
  }
266
- form.appendChild(container);
346
+ select.value = stateObj.value;
347
+ select.addEventListener("change", () => {
348
+ stateObj.set(select.value);
349
+ });
350
+ } else {
351
+ const textInput = control;
352
+ textInput.addEventListener("input", () => {
353
+ if (typeof stateObj.set === "function") stateObj.set(textInput.value);
354
+ });
267
355
  }
268
- for (const act of screenDef.acts) {
269
- const button = document.createElement("button");
270
- button.id = act.id;
271
- button.type = "button";
272
- button.textContent = act.label;
273
- if (showSemanticIds && actSemanticIds) {
274
- const sid = actSemanticIds.get(act.id);
275
- if (sid) button.setAttribute("data-intent-action", sid);
356
+ container.appendChild(control);
357
+ if (ask.hintText) {
358
+ const hint = document.createElement("p");
359
+ hint.id = `${ask.id}-hint${suffix}`;
360
+ hint.textContent = ask.hintText;
361
+ container.appendChild(hint);
362
+ }
363
+ const defaultAction = findDefaultAction(screenDef.acts);
364
+ if (defaultAction) {
365
+ const hintId = getEnterHintId(ask.id, suffix);
366
+ const hint = document.createElement("p");
367
+ hint.id = hintId;
368
+ hint.textContent = `Press Enter to ${sanitizeLabel(defaultAction.label)}.`;
369
+ if (!defaultAction.enabled.current) hint.style.display = "none";
370
+ container.appendChild(hint);
371
+ if (defaultAction.enabled.current) {
372
+ const existing = control.getAttribute("aria-describedby");
373
+ if (existing) control.setAttribute("aria-describedby", `${existing} ${hintId}`);
374
+ else control.setAttribute("aria-describedby", hintId);
276
375
  }
277
- if (act.primary) button.className = "primary";
278
- if (!act.enabled.current) {
279
- button.disabled = true;
280
- if (act.blockedReasons.length > 0) {
281
- const reasonId = getReasonId(act.id);
282
- button.setAttribute("aria-describedby", reasonId);
283
- const reasonEl = document.createElement("p");
284
- reasonEl.id = reasonId;
285
- reasonEl.className = "intent-blocked-reason";
286
- reasonEl.setAttribute("role", "alert");
287
- reasonEl.textContent = act.blockedReasons[0];
288
- form.appendChild(reasonEl);
289
- }
376
+ }
377
+ form.appendChild(container);
378
+ }
379
+ function buildActionButton(form, act, suffix, showSemanticIds, actSemanticIds) {
380
+ const button = document.createElement("button");
381
+ button.id = `${act.id}${suffix}`;
382
+ button.type = "button";
383
+ button.textContent = act.label;
384
+ if (showSemanticIds && actSemanticIds) {
385
+ const sid = actSemanticIds.get(act.id);
386
+ if (sid) button.setAttribute("data-intent-action", sid);
387
+ }
388
+ if (act.primary) button.className = "primary";
389
+ if (!act.enabled.current) {
390
+ button.disabled = true;
391
+ if (act.blockedReasons.length > 0) {
392
+ const reasonId = getReasonId(act.id, suffix);
393
+ button.setAttribute("aria-describedby", reasonId);
394
+ const reasonEl = document.createElement("p");
395
+ reasonEl.id = reasonId;
396
+ reasonEl.className = "intent-blocked-reason";
397
+ reasonEl.setAttribute("role", "alert");
398
+ reasonEl.textContent = act.blockedReasons[0];
399
+ form.appendChild(reasonEl);
290
400
  }
291
- form.appendChild(button);
292
401
  }
293
- const output = document.createElement("output");
294
- output.id = "feedback-output";
295
- output.setAttribute("aria-live", "polite");
296
- form.appendChild(output);
297
- main.appendChild(form);
298
- return main;
402
+ form.appendChild(button);
299
403
  }
300
404
  function updateFeedback(act, output) {
301
405
  const msg = act.feedback && act.statusMessage ? act.statusMessage : "";
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.0-alpha.6",
6
+ "version": "0.1.0-alpha.7",
7
7
  "description": "DOM materializer for Intent screens and router",
8
8
  "license": "MIT",
9
9
  "repository": {
@@ -26,8 +26,8 @@
26
26
  "dist"
27
27
  ],
28
28
  "dependencies": {
29
- "@intent-framework/core": "^0.1.0-alpha.6",
30
- "@intent-framework/router": "^0.1.0-alpha.6"
29
+ "@intent-framework/core": "^0.1.0-alpha.7",
30
+ "@intent-framework/router": "^0.1.0-alpha.7"
31
31
  },
32
32
  "devDependencies": {
33
33
  "jsdom": "^29.1.1",