@intent-framework/dom 0.1.0-alpha.5 → 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 +257 -122
  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,25 +84,30 @@ 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);
100
106
  if (!reasonEl) {
101
107
  reasonEl = document.createElement("p");
102
108
  reasonEl.id = reasonId;
109
+ reasonEl.className = "intent-blocked-reason";
110
+ reasonEl.setAttribute("role", "alert");
103
111
  form.appendChild(reasonEl);
104
112
  }
105
113
  reasonEl.textContent = act.blockedReasons[0];
@@ -107,18 +115,27 @@ function renderDom(screenDef, options) {
107
115
  button.removeAttribute("aria-describedby");
108
116
  if (reasonEl) reasonEl.remove();
109
117
  }
110
- });
111
- unsubscribers.push(unsub);
112
- }
118
+ }
119
+ });
120
+ unsubscribers.push(unsub);
113
121
  }
114
122
  for (const act of screenDef.acts) {
115
123
  const unsub = act.onStatusChange(() => {
116
- 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
+ }
117
131
  });
118
132
  unsubscribers.push(unsub);
119
133
  }
120
- for (const act of screenDef.acts) {
121
- 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}`);
122
139
  if (button) button.addEventListener("click", () => {
123
140
  if (act.enabled.current) runtime.executeAct(act);
124
141
  });
@@ -127,50 +144,88 @@ function renderDom(screenDef, options) {
127
144
  if (defaultActionForHint) {
128
145
  const unsub = defaultActionForHint.enabled.subscribe(() => {
129
146
  const isEnabled = defaultActionForHint.enabled.current;
130
- for (const ask of screenDef.asks) {
131
- const input = form.querySelector(`#${ask.id}`);
132
- const hint = form.querySelector(`#${getEnterHintId(ask.id)}`);
133
- if (input && hint) {
134
- const hintId = getEnterHintId(ask.id);
135
- if (isEnabled) {
136
- hint.style.display = "";
137
- const existing = input.getAttribute("aria-describedby") || "";
138
- const ids = existing.split(/\s+/).filter(Boolean);
139
- if (!ids.includes(hintId)) ids.push(hintId);
140
- input.setAttribute("aria-describedby", ids.join(" "));
141
- } else {
142
- hint.style.display = "none";
143
- const existing = input.getAttribute("aria-describedby") || "";
144
- const ids = existing.split(/\s+/).filter(Boolean).filter((id) => id !== hintId);
145
- if (ids.length > 0) input.setAttribute("aria-describedby", ids.join(" "));
146
- 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
+ }
147
171
  }
148
172
  }
149
173
  }
150
174
  });
151
175
  unsubscribers.push(unsub);
152
176
  }
153
- for (const ask of screenDef.asks) {
154
- const input = form.querySelector(`#${ask.id}`);
155
- if (input) {
156
- const onKeyDown = (event) => {
157
- if (event.key !== "Enter") return;
158
- if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
159
- if (input.tagName === "TEXTAREA") return;
160
- const defaultAction = findDefaultAction(screenDef.acts);
161
- if (!defaultAction || !defaultAction.enabled.current) return;
162
- event.preventDefault();
163
- runtime.executeAct(defaultAction);
164
- };
165
- input.addEventListener("keydown", onKeyDown);
166
- 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
+ }
167
200
  }
168
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
+ }
169
217
  return () => {
170
218
  for (const unsub of unsubscribers) unsub();
171
219
  runtime.dispose();
172
220
  };
173
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
+ }
174
229
  function buildDom(screenDef, showScreenName, showSemanticIds) {
175
230
  let inspected;
176
231
  let askSemanticIds;
@@ -180,94 +235,171 @@ function buildDom(screenDef, showScreenName, showSemanticIds) {
180
235
  askSemanticIds = new Map(inspected.asks.map((a) => [a.id, a.semanticId]));
181
236
  actSemanticIds = new Map(inspected.acts.map((a) => [a.id, a.semanticId]));
182
237
  }
183
- const surface = screenDef.surfaces[0];
238
+ const isMulti = screenDef.surfaces.length > 1;
184
239
  const main = document.createElement("main");
185
- if (surface) main.id = surface.id;
186
240
  if (showSemanticIds && inspected) main.setAttribute("data-intent-screen", inspected.semanticId);
187
241
  if (showScreenName) {
188
242
  const heading = document.createElement("h1");
189
243
  heading.textContent = screenDef.name;
190
244
  main.appendChild(heading);
191
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);
192
269
  const form = document.createElement("form");
193
270
  form.setAttribute("method", "POST");
194
271
  form.setAttribute("novalidate", "");
195
- for (const ask of screenDef.asks) {
196
- const container = document.createElement("div");
197
- container.className = "ask-group";
198
- const label = document.createElement("label");
199
- label.textContent = ask.label;
200
- label.htmlFor = ask.id;
201
- if (showSemanticIds && askSemanticIds) {
202
- const sid = askSemanticIds.get(ask.id);
203
- if (sid) label.setAttribute("data-intent-ask", sid);
204
- }
205
- container.appendChild(label);
206
- const input = createInputForAsk(ask);
207
- input.id = ask.id;
208
- input.name = ask.id;
209
- if (showSemanticIds && askSemanticIds) {
210
- const sid = askSemanticIds.get(ask.id);
211
- if (sid) input.setAttribute("data-intent-ask", sid);
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
285
+ });
286
+ } else if ("handler" in item) {
287
+ const act = item;
288
+ surfaceActIds.add(act.id);
289
+ orderedItems.push({
290
+ kind: "act",
291
+ node: act
292
+ });
212
293
  }
213
- if (ask.required) input.required = true;
214
- if (ask.kind === "contact" && ask.contactKind) input.setAttribute("autocomplete", ask.contactKind);
215
- input.addEventListener("input", () => {
216
- const stateObj = ask.state;
217
- if (typeof stateObj.set === "function") stateObj.set(input.value);
294
+ for (const ask of screenDef.asks) if (!surfaceAskIds.has(ask.id)) orderedItems.push({
295
+ kind: "ask",
296
+ node: ask
218
297
  });
219
- container.appendChild(input);
220
- if (ask.hintText) {
221
- const hint = document.createElement("p");
222
- hint.id = `${ask.id}-hint`;
223
- hint.textContent = ask.hintText;
224
- container.appendChild(hint);
225
- }
226
- const defaultAction = findDefaultAction(screenDef.acts);
227
- if (defaultAction) {
228
- const hintId = getEnterHintId(ask.id);
229
- const hint = document.createElement("p");
230
- hint.id = hintId;
231
- hint.textContent = `Press Enter to ${sanitizeLabel(defaultAction.label)}.`;
232
- if (!defaultAction.enabled.current) hint.style.display = "none";
233
- container.appendChild(hint);
234
- if (defaultAction.enabled.current) {
235
- const existing = input.getAttribute("aria-describedby");
236
- if (existing) input.setAttribute("aria-describedby", `${existing} ${hintId}`);
237
- else input.setAttribute("aria-describedby", hintId);
238
- }
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);
239
345
  }
240
- 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
+ });
241
355
  }
242
- for (const act of screenDef.acts) {
243
- const button = document.createElement("button");
244
- button.id = act.id;
245
- button.type = "button";
246
- button.textContent = act.label;
247
- if (showSemanticIds && actSemanticIds) {
248
- const sid = actSemanticIds.get(act.id);
249
- 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);
250
375
  }
251
- if (act.primary) button.className = "primary";
252
- if (!act.enabled.current) {
253
- button.disabled = true;
254
- if (act.blockedReasons.length > 0) {
255
- const reasonId = getReasonId(act.id);
256
- button.setAttribute("aria-describedby", reasonId);
257
- const reasonEl = document.createElement("p");
258
- reasonEl.id = reasonId;
259
- reasonEl.textContent = act.blockedReasons[0];
260
- form.appendChild(reasonEl);
261
- }
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);
262
400
  }
263
- form.appendChild(button);
264
401
  }
265
- const output = document.createElement("output");
266
- output.id = "feedback-output";
267
- output.setAttribute("aria-live", "polite");
268
- form.appendChild(output);
269
- main.appendChild(form);
270
- return main;
402
+ form.appendChild(button);
271
403
  }
272
404
  function updateFeedback(act, output) {
273
405
  const msg = act.feedback && act.statusMessage ? act.statusMessage : "";
@@ -275,13 +407,16 @@ function updateFeedback(act, output) {
275
407
  else output.textContent = "";
276
408
  }
277
409
  function createInputForAsk(ask) {
410
+ if (typeof ask.state.value === "boolean") {
411
+ const input$1 = document.createElement("input");
412
+ input$1.type = "checkbox";
413
+ return input$1;
414
+ }
415
+ if (ask.kind === "choice") return document.createElement("select");
278
416
  const input = document.createElement("input");
279
417
  if (ask.kind === "contact" && ask.contactKind === "email") input.type = "email";
280
418
  else if (ask.kind === "secret") input.type = "password";
281
- else if (ask.kind === "choice") {
282
- input.type = "text";
283
- input.setAttribute("role", "combobox");
284
- } else input.type = "text";
419
+ else input.type = "text";
285
420
  return input;
286
421
  }
287
422
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.0-alpha.5",
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.5",
30
- "@intent-framework/router": "^0.1.0-alpha.5"
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",