@unofficialbox/box-open-elements 0.5.0 → 0.7.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.
Files changed (227) hide show
  1. package/README.md +27 -73
  2. package/dist/components/collections/table.d.ts +113 -3
  3. package/dist/components/collections/table.js +540 -37
  4. package/dist/components/feedback/due-badge.d.ts +38 -0
  5. package/dist/components/feedback/due-badge.js +168 -0
  6. package/dist/components/feedback/due-types.d.ts +32 -0
  7. package/dist/components/feedback/due-types.js +91 -0
  8. package/dist/components/feedback/progress-steps.d.ts +51 -5
  9. package/dist/components/feedback/progress-steps.js +200 -22
  10. package/dist/components/feedback/stage-path.d.ts +44 -0
  11. package/dist/components/feedback/stage-path.js +235 -0
  12. package/dist/components/forms/select.d.ts +21 -0
  13. package/dist/components/forms/select.js +100 -3
  14. package/dist/components/forms/text-field.d.ts +21 -0
  15. package/dist/components/forms/text-field.js +100 -1
  16. package/dist/components/layout/nav-sidebar.d.ts +14 -0
  17. package/dist/components/layout/nav-sidebar.js +49 -0
  18. package/dist/components/layout/split-view.d.ts +14 -0
  19. package/dist/components/layout/split-view.js +80 -1
  20. package/dist/components/overlays/command-palette.d.ts +62 -0
  21. package/dist/components/overlays/command-palette.js +537 -0
  22. package/dist/components/overlays/command-types.d.ts +90 -0
  23. package/dist/components/overlays/command-types.js +259 -0
  24. package/dist/components/overlays/drawer.d.ts +18 -0
  25. package/dist/components/overlays/drawer.js +135 -9
  26. package/dist/components/overlays/shortcuts-overlay.d.ts +48 -0
  27. package/dist/components/overlays/shortcuts-overlay.js +385 -0
  28. package/dist/components/overlays/tooltip.js +1 -1
  29. package/dist/core/index.d.ts +4 -0
  30. package/dist/core/index.js +2 -0
  31. package/dist/core/offset-window.d.ts +70 -0
  32. package/dist/core/offset-window.js +134 -0
  33. package/dist/core/virtualize.d.ts +71 -0
  34. package/dist/core/virtualize.js +67 -0
  35. package/dist/element-maps.d.ts +292 -0
  36. package/dist/element-maps.js +1 -0
  37. package/dist/entries/activity-density.d.ts +1 -0
  38. package/dist/entries/activity-density.js +1 -0
  39. package/dist/entries/agent-chat.d.ts +1 -0
  40. package/dist/entries/agent-chat.js +1 -0
  41. package/dist/entries/audit-log.d.ts +1 -0
  42. package/dist/entries/audit-log.js +1 -0
  43. package/dist/entries/command-palette.d.ts +1 -0
  44. package/dist/entries/command-palette.js +1 -0
  45. package/dist/entries/compare-view.d.ts +1 -0
  46. package/dist/entries/compare-view.js +1 -0
  47. package/dist/entries/diff-viewer.d.ts +1 -0
  48. package/dist/entries/diff-viewer.js +1 -0
  49. package/dist/entries/due-badge.d.ts +1 -0
  50. package/dist/entries/due-badge.js +1 -0
  51. package/dist/entries/form-wizard.d.ts +1 -0
  52. package/dist/entries/form-wizard.js +1 -0
  53. package/dist/entries/lineage-graph.d.ts +1 -0
  54. package/dist/entries/lineage-graph.js +1 -0
  55. package/dist/entries/notification-bell.d.ts +1 -0
  56. package/dist/entries/notification-bell.js +1 -0
  57. package/dist/entries/notification-inbox.d.ts +1 -0
  58. package/dist/entries/notification-inbox.js +1 -0
  59. package/dist/entries/provenance-strip.d.ts +1 -0
  60. package/dist/entries/provenance-strip.js +1 -0
  61. package/dist/entries/run-trace.d.ts +1 -0
  62. package/dist/entries/run-trace.js +1 -0
  63. package/dist/entries/shortcuts-overlay.d.ts +1 -0
  64. package/dist/entries/shortcuts-overlay.js +1 -0
  65. package/dist/entries/signature-ceremony.d.ts +1 -0
  66. package/dist/entries/signature-ceremony.js +1 -0
  67. package/dist/entries/stage-path.d.ts +1 -0
  68. package/dist/entries/stage-path.js +1 -0
  69. package/dist/entries/timeline.d.ts +1 -0
  70. package/dist/entries/timeline.js +1 -0
  71. package/dist/entries/version-graph.d.ts +1 -0
  72. package/dist/entries/version-graph.js +1 -0
  73. package/dist/entries/version-list.d.ts +1 -0
  74. package/dist/entries/version-list.js +1 -0
  75. package/dist/entries/wizard-summary.d.ts +1 -0
  76. package/dist/entries/wizard-summary.js +1 -0
  77. package/dist/entries/work-queue.d.ts +1 -0
  78. package/dist/entries/work-queue.js +1 -0
  79. package/dist/entries/workload-board.d.ts +1 -0
  80. package/dist/entries/workload-board.js +1 -0
  81. package/dist/index.d.ts +21 -0
  82. package/dist/index.js +21 -0
  83. package/dist/patterns/agent-chat/agent-chat.d.ts +75 -0
  84. package/dist/patterns/agent-chat/agent-chat.js +820 -0
  85. package/dist/patterns/agent-chat/controller.d.ts +27 -0
  86. package/dist/patterns/agent-chat/controller.js +182 -0
  87. package/dist/patterns/agent-chat/index.d.ts +3 -0
  88. package/dist/patterns/agent-chat/index.js +6 -0
  89. package/dist/patterns/agent-chat/types.d.ts +109 -0
  90. package/dist/patterns/agent-chat/types.js +1 -0
  91. package/dist/patterns/audit/activity-density.d.ts +68 -0
  92. package/dist/patterns/audit/activity-density.js +428 -0
  93. package/dist/patterns/audit/audit-log.d.ts +182 -0
  94. package/dist/patterns/audit/audit-log.js +1082 -0
  95. package/dist/patterns/audit/index.d.ts +3 -0
  96. package/dist/patterns/audit/index.js +7 -0
  97. package/dist/patterns/audit/shared.d.ts +10 -0
  98. package/dist/patterns/audit/shared.js +50 -0
  99. package/dist/patterns/audit/types.d.ts +143 -0
  100. package/dist/patterns/audit/types.js +327 -0
  101. package/dist/patterns/audit/window.d.ts +116 -0
  102. package/dist/patterns/audit/window.js +132 -0
  103. package/dist/patterns/content-explorer/actions/controller.js +12 -2
  104. package/dist/patterns/content-explorer/box-transport.js +117 -25
  105. package/dist/patterns/content-explorer/content-explorer.d.ts +13 -1
  106. package/dist/patterns/content-explorer/content-explorer.js +36 -3
  107. package/dist/patterns/content-explorer/contracts.d.ts +24 -1
  108. package/dist/patterns/content-explorer/contracts.js +68 -11
  109. package/dist/patterns/content-explorer/controller.d.ts +16 -1
  110. package/dist/patterns/content-explorer/controller.js +130 -4
  111. package/dist/patterns/content-explorer/navigation/controller.js +11 -9
  112. package/dist/patterns/content-explorer/types.d.ts +58 -1
  113. package/dist/patterns/content-picker/content-picker.d.ts +71 -0
  114. package/dist/patterns/content-picker/content-picker.js +747 -0
  115. package/dist/patterns/content-picker/controller.d.ts +38 -0
  116. package/dist/patterns/content-picker/controller.js +141 -0
  117. package/dist/patterns/content-picker/index.d.ts +3 -0
  118. package/dist/patterns/content-picker/index.js +6 -0
  119. package/dist/patterns/content-picker/types.d.ts +52 -0
  120. package/dist/patterns/content-picker/types.js +26 -0
  121. package/dist/patterns/content-sidebar/content-sidebar.d.ts +43 -0
  122. package/dist/patterns/content-sidebar/content-sidebar.js +293 -0
  123. package/dist/patterns/content-sidebar/index.d.ts +2 -0
  124. package/dist/patterns/content-sidebar/index.js +4 -0
  125. package/dist/patterns/content-sidebar/types.d.ts +18 -0
  126. package/dist/patterns/content-sidebar/types.js +20 -0
  127. package/dist/patterns/content-uploader/box-transport.d.ts +14 -0
  128. package/dist/patterns/content-uploader/box-transport.js +54 -0
  129. package/dist/patterns/content-uploader/content-uploader.d.ts +62 -0
  130. package/dist/patterns/content-uploader/content-uploader.js +559 -0
  131. package/dist/patterns/content-uploader/controller.d.ts +39 -0
  132. package/dist/patterns/content-uploader/controller.js +227 -0
  133. package/dist/patterns/content-uploader/index.d.ts +4 -0
  134. package/dist/patterns/content-uploader/index.js +8 -0
  135. package/dist/patterns/content-uploader/types.d.ts +120 -0
  136. package/dist/patterns/content-uploader/types.js +38 -0
  137. package/dist/patterns/diff/compare-view.d.ts +47 -0
  138. package/dist/patterns/diff/compare-view.js +276 -0
  139. package/dist/patterns/diff/diff-viewer.d.ts +42 -0
  140. package/dist/patterns/diff/diff-viewer.js +434 -0
  141. package/dist/patterns/diff/engine.d.ts +22 -0
  142. package/dist/patterns/diff/engine.js +276 -0
  143. package/dist/patterns/diff/index.d.ts +4 -0
  144. package/dist/patterns/diff/index.js +8 -0
  145. package/dist/patterns/diff/types.d.ts +69 -0
  146. package/dist/patterns/diff/types.js +24 -0
  147. package/dist/patterns/form-wizard/controller.d.ts +39 -0
  148. package/dist/patterns/form-wizard/controller.js +157 -0
  149. package/dist/patterns/form-wizard/form-wizard.d.ts +63 -0
  150. package/dist/patterns/form-wizard/form-wizard.js +468 -0
  151. package/dist/patterns/form-wizard/index.d.ts +4 -0
  152. package/dist/patterns/form-wizard/index.js +8 -0
  153. package/dist/patterns/form-wizard/types.d.ts +109 -0
  154. package/dist/patterns/form-wizard/types.js +83 -0
  155. package/dist/patterns/form-wizard/wizard-summary.d.ts +62 -0
  156. package/dist/patterns/form-wizard/wizard-summary.js +324 -0
  157. package/dist/patterns/internal/safe-href.d.ts +17 -0
  158. package/dist/patterns/internal/safe-href.js +17 -0
  159. package/dist/patterns/lineage/index.d.ts +3 -0
  160. package/dist/patterns/lineage/index.js +6 -0
  161. package/dist/patterns/lineage/lineage-graph.d.ts +30 -0
  162. package/dist/patterns/lineage/lineage-graph.js +434 -0
  163. package/dist/patterns/lineage/provenance-strip.d.ts +22 -0
  164. package/dist/patterns/lineage/provenance-strip.js +194 -0
  165. package/dist/patterns/lineage/types.d.ts +53 -0
  166. package/dist/patterns/lineage/types.js +49 -0
  167. package/dist/patterns/notifications/index.d.ts +3 -0
  168. package/dist/patterns/notifications/index.js +6 -0
  169. package/dist/patterns/notifications/notification-bell.d.ts +38 -0
  170. package/dist/patterns/notifications/notification-bell.js +198 -0
  171. package/dist/patterns/notifications/notification-inbox.d.ts +46 -0
  172. package/dist/patterns/notifications/notification-inbox.js +537 -0
  173. package/dist/patterns/notifications/types.d.ts +61 -0
  174. package/dist/patterns/notifications/types.js +108 -0
  175. package/dist/patterns/preview/annotation-inspector.d.ts +25 -6
  176. package/dist/patterns/preview/annotation-inspector.js +239 -94
  177. package/dist/patterns/preview/annotation-thread.d.ts +25 -5
  178. package/dist/patterns/preview/annotation-thread.js +233 -82
  179. package/dist/patterns/preview/annotation-toolbar.d.ts +20 -6
  180. package/dist/patterns/preview/annotation-toolbar.js +162 -96
  181. package/dist/patterns/preview/content-preview-adapter.d.ts +10 -1
  182. package/dist/patterns/preview/content-preview-adapter.js +26 -1
  183. package/dist/patterns/preview/preview-element.d.ts +15 -2
  184. package/dist/patterns/preview/preview-element.js +223 -52
  185. package/dist/patterns/preview/provider-adapter.d.ts +47 -0
  186. package/dist/patterns/preview/provider-adapter.js +17 -0
  187. package/dist/patterns/run/index.d.ts +2 -0
  188. package/dist/patterns/run/index.js +4 -0
  189. package/dist/patterns/run/run-trace.d.ts +40 -0
  190. package/dist/patterns/run/run-trace.js +441 -0
  191. package/dist/patterns/run/types.d.ts +66 -0
  192. package/dist/patterns/run/types.js +152 -0
  193. package/dist/patterns/signature/index.d.ts +2 -0
  194. package/dist/patterns/signature/index.js +4 -0
  195. package/dist/patterns/signature/signature-ceremony.d.ts +33 -0
  196. package/dist/patterns/signature/signature-ceremony.js +317 -0
  197. package/dist/patterns/signature/types.d.ts +68 -0
  198. package/dist/patterns/signature/types.js +89 -0
  199. package/dist/patterns/timeline/index.d.ts +2 -0
  200. package/dist/patterns/timeline/index.js +4 -0
  201. package/dist/patterns/timeline/timeline.d.ts +27 -0
  202. package/dist/patterns/timeline/timeline.js +430 -0
  203. package/dist/patterns/timeline/types.d.ts +36 -0
  204. package/dist/patterns/timeline/types.js +27 -0
  205. package/dist/patterns/versions/graph-layout.d.ts +43 -0
  206. package/dist/patterns/versions/graph-layout.js +159 -0
  207. package/dist/patterns/versions/index.d.ts +4 -0
  208. package/dist/patterns/versions/index.js +8 -0
  209. package/dist/patterns/versions/shared.d.ts +4 -0
  210. package/dist/patterns/versions/shared.js +13 -0
  211. package/dist/patterns/versions/types.d.ts +37 -0
  212. package/dist/patterns/versions/types.js +39 -0
  213. package/dist/patterns/versions/version-graph.d.ts +36 -0
  214. package/dist/patterns/versions/version-graph.js +438 -0
  215. package/dist/patterns/versions/version-list.d.ts +39 -0
  216. package/dist/patterns/versions/version-list.js +407 -0
  217. package/dist/patterns/work-queue/controller.d.ts +26 -0
  218. package/dist/patterns/work-queue/controller.js +150 -0
  219. package/dist/patterns/work-queue/index.d.ts +4 -0
  220. package/dist/patterns/work-queue/index.js +7 -0
  221. package/dist/patterns/work-queue/types.d.ts +128 -0
  222. package/dist/patterns/work-queue/types.js +45 -0
  223. package/dist/patterns/work-queue/work-queue.d.ts +48 -0
  224. package/dist/patterns/work-queue/work-queue.js +534 -0
  225. package/dist/patterns/work-queue/workload-board.d.ts +53 -0
  226. package/dist/patterns/work-queue/workload-board.js +608 -0
  227. package/package.json +89 -5
@@ -8,6 +8,50 @@ const escapeHtml = (value) => value
8
8
  .replaceAll(">", ">")
9
9
  .replaceAll('"', """)
10
10
  .replaceAll("'", "'");
11
+ const STEP_STATUSES = new Set(["complete", "blocked", "failed", "disabled"]);
12
+ /** Attribute payloads are author input — validate every record. */
13
+ export const isProgressStepRecord = (value) => {
14
+ if (typeof value !== "object" || value === null) {
15
+ return false;
16
+ }
17
+ const step = value;
18
+ return (typeof step.value === "string" &&
19
+ step.value.length > 0 &&
20
+ typeof step.label === "string" &&
21
+ step.label.length > 0 &&
22
+ (step.status === undefined || STEP_STATUSES.has(step.status)) &&
23
+ // Non-string text fields would reach escapeHtml and throw mid-render.
24
+ (step.description === undefined || typeof step.description === "string") &&
25
+ (step.statusNote === undefined || typeof step.statusNote === "string"));
26
+ };
27
+ /**
28
+ * The state rules, pure and DOM-free, so a host can drive its own surface
29
+ * (or decide whether "Continue" is enabled) from the same function the
30
+ * component renders from. An unknown `value` falls back to the first step,
31
+ * matching the component's long-standing behaviour.
32
+ */
33
+ export const resolveStepStates = (items, value) => {
34
+ const currentIndex = Math.max(0, items.findIndex(item => item.value === value));
35
+ return items.map((item, index) => {
36
+ const positional = index < currentIndex ? "complete" : index === currentIndex ? "current" : "upcoming";
37
+ const state = item.status ?? positional;
38
+ return {
39
+ item,
40
+ state,
41
+ isCurrent: index === currentIndex,
42
+ interactive: state !== "blocked" && state !== "disabled",
43
+ };
44
+ });
45
+ };
46
+ /** Every state in words — colour never carries the meaning alone. */
47
+ export const STEP_STATE_LABEL = {
48
+ complete: "Complete",
49
+ current: "Current step",
50
+ upcoming: "Not started",
51
+ blocked: "Blocked",
52
+ failed: "Failed",
53
+ disabled: "Unavailable",
54
+ };
11
55
  const progressStepsStyles = `
12
56
  :host {
13
57
  display: block;
@@ -100,6 +144,66 @@ const progressStepsStyles = `
100
144
  color: var(--boe-token-text-text-secondary, #6f6f6f);
101
145
  line-height: 1.45;
102
146
  }
147
+
148
+ [part="step"]:disabled {
149
+ cursor: not-allowed;
150
+ background: var(--boe-token-surface-surface, #ffffff);
151
+ }
152
+
153
+ [part="step"][data-state="blocked"] [part="step-label"],
154
+ [part="step"][data-state="disabled"] [part="step-label"] {
155
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
156
+ }
157
+
158
+ [part="step"][data-state="blocked"] [part="marker"] {
159
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-warning, #f5b31b) 18%, var(--boe-token-surface-surface, #ffffff) 82%);
160
+ color: color-mix(in srgb, var(--boe-token-surface-status-surface-warning, #f5b31b) 52%, var(--boe-token-text-text, #222222));
161
+ }
162
+
163
+ [part="step"][data-state="failed"] {
164
+ border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 34%, transparent);
165
+ }
166
+
167
+ [part="step"][data-state="failed"] [part="marker"] {
168
+ border-color: transparent;
169
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 16%, var(--boe-token-surface-surface, #ffffff) 84%);
170
+ color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 72%, black 28%);
171
+ }
172
+
173
+ [part="step"][data-state="disabled"] {
174
+ opacity: 0.62;
175
+ }
176
+
177
+ /* The status, in words: visible for the conditions a user must not miss. */
178
+ [part="step-status"] {
179
+ font-size: 0.8rem;
180
+ font-weight: 700;
181
+ }
182
+
183
+ [part="step"][data-state="blocked"] [part="step-status"] {
184
+ color: color-mix(in srgb, var(--boe-token-surface-status-surface-warning, #f5b31b) 48%, var(--boe-token-text-text, #222222));
185
+ }
186
+
187
+ [part="step"][data-state="failed"] [part="step-status"] {
188
+ color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 76%, black 24%);
189
+ }
190
+
191
+ [part="step-status-note"] {
192
+ font-size: 0.82rem;
193
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
194
+ }
195
+
196
+ .boe-sr-only {
197
+ position: absolute;
198
+ width: 1px;
199
+ height: 1px;
200
+ margin: -1px;
201
+ padding: 0;
202
+ border: 0;
203
+ clip: rect(0, 0, 0, 0);
204
+ overflow: hidden;
205
+ white-space: nowrap;
206
+ }
103
207
  `;
104
208
  export class ProgressSteps extends BaseElement {
105
209
  static tagName = DEFAULT_TAG_NAME;
@@ -108,6 +212,7 @@ export class ProgressSteps extends BaseElement {
108
212
  }
109
213
  valueInternal = "";
110
214
  stepsEl;
215
+ liveEl;
111
216
  itemsSignature = "";
112
217
  get label() {
113
218
  return this.getAttribute("label") ?? "Progress Steps";
@@ -122,7 +227,9 @@ export class ProgressSteps extends BaseElement {
122
227
  }
123
228
  try {
124
229
  const parsed = JSON.parse(raw);
125
- return Array.isArray(parsed) ? parsed : [];
230
+ return Array.isArray(parsed) && parsed.every(isProgressStepRecord)
231
+ ? parsed
232
+ : [];
126
233
  }
127
234
  catch {
128
235
  return [];
@@ -131,6 +238,10 @@ export class ProgressSteps extends BaseElement {
131
238
  set items(value) {
132
239
  this.setAttribute("items", JSON.stringify(value));
133
240
  }
241
+ /** The resolved states — same objects the render uses. */
242
+ get resolvedSteps() {
243
+ return resolveStepStates(this.items, this.valueInternal);
244
+ }
134
245
  get value() {
135
246
  return this.valueInternal;
136
247
  }
@@ -154,8 +265,10 @@ export class ProgressSteps extends BaseElement {
154
265
  this.shadowRoot.innerHTML = `
155
266
  <style>${progressStepsStyles}</style>
156
267
  <div part="steps" role="group"></div>
268
+ <div part="live" class="boe-sr-only" aria-live="polite"></div>
157
269
  `;
158
270
  this.stepsEl = this.shadowRoot.querySelector('[part="steps"]');
271
+ this.liveEl = this.shadowRoot.querySelector('[part="live"]');
159
272
  }
160
273
  setupListeners() {
161
274
  this.stepsEl.addEventListener("click", event => {
@@ -175,34 +288,51 @@ export class ProgressSteps extends BaseElement {
175
288
  if (!step || !this.stepsEl.contains(step)) {
176
289
  return;
177
290
  }
178
- const items = this.items;
291
+ const resolved = this.resolvedSteps;
179
292
  const index = Array.from(this.stepsEl.querySelectorAll('[part="step"]')).indexOf(step);
180
- if (index < 0) {
293
+ if (index < 0 || resolved.length === 0) {
181
294
  return;
182
295
  }
183
- const lastIndex = items.length - 1;
184
- let nextIndex = index;
296
+ // Arrows walk to the next *interactive* step — blocked and disabled
297
+ // steps are skipped, not landed on and bounced off.
298
+ const isInteractive = (candidate) => resolved[candidate]?.interactive ?? false;
299
+ const walk = (direction) => {
300
+ for (let offset = 1; offset <= resolved.length; offset += 1) {
301
+ const candidate = (((index + direction * offset) % resolved.length) + resolved.length) % resolved.length;
302
+ if (isInteractive(candidate)) {
303
+ return candidate;
304
+ }
305
+ }
306
+ return -1;
307
+ };
308
+ let nextIndex;
185
309
  if (keyboardEvent.key === "ArrowRight" || keyboardEvent.key === "ArrowDown") {
186
- nextIndex = index >= lastIndex ? 0 : index + 1;
310
+ nextIndex = walk(1);
187
311
  }
188
312
  else if (keyboardEvent.key === "ArrowLeft" || keyboardEvent.key === "ArrowUp") {
189
- nextIndex = index <= 0 ? lastIndex : index - 1;
313
+ nextIndex = walk(-1);
190
314
  }
191
315
  else if (keyboardEvent.key === "Home") {
192
- nextIndex = 0;
316
+ nextIndex = resolved.findIndex(entry => entry.interactive);
193
317
  }
194
318
  else if (keyboardEvent.key === "End") {
195
- nextIndex = lastIndex;
319
+ nextIndex = -1;
320
+ for (let candidate = resolved.length - 1; candidate >= 0; candidate -= 1) {
321
+ if (isInteractive(candidate)) {
322
+ nextIndex = candidate;
323
+ break;
324
+ }
325
+ }
196
326
  }
197
327
  else {
198
328
  return;
199
329
  }
200
330
  keyboardEvent.preventDefault();
201
- const nextStep = items[nextIndex];
202
- if (!nextStep) {
331
+ const nextStep = resolved[nextIndex];
332
+ if (nextIndex < 0 || !nextStep) {
203
333
  return;
204
334
  }
205
- this.selectValue(nextStep.value);
335
+ this.selectValue(nextStep.item.value);
206
336
  queueMicrotask(() => {
207
337
  const nextButton = this.stepsEl.querySelectorAll('[part="step"]')[nextIndex];
208
338
  nextButton?.focus();
@@ -210,6 +340,13 @@ export class ProgressSteps extends BaseElement {
210
340
  });
211
341
  }
212
342
  selectValue(nextValue) {
343
+ // Disabled buttons never emit clicks and the keyboard path skips them,
344
+ // but the invariant should not depend on every caller knowing that.
345
+ const resolved = resolveStepStates(this.items, this.valueInternal);
346
+ const target = resolved.find(entry => entry.item.value === nextValue);
347
+ if (!target || !target.interactive) {
348
+ return;
349
+ }
213
350
  this.valueInternal = nextValue;
214
351
  this.setAttribute("value", nextValue);
215
352
  this.dispatchEvent(new CustomEvent("value-changed", {
@@ -218,6 +355,28 @@ export class ProgressSteps extends BaseElement {
218
355
  detail: { value: nextValue },
219
356
  }));
220
357
  this.update();
358
+ this.announceStep(target);
359
+ }
360
+ /** A polite announcement on user-driven step changes, never on render. */
361
+ announceStep(target) {
362
+ if (!this.liveEl) {
363
+ return;
364
+ }
365
+ const items = this.items;
366
+ const position = items.findIndex(item => item.value === target.item.value) + 1;
367
+ this.liveEl.textContent = `${target.item.label}, step ${String(position)} of ${String(items.length)}`;
368
+ }
369
+ /**
370
+ * The tab stop lives on the current step — unless that step is blocked or
371
+ * disabled (a host can point `value` anywhere), in which case the first
372
+ * interactive step takes it so the group stays reachable.
373
+ */
374
+ tabbableIndex(resolved) {
375
+ const currentIndex = resolved.findIndex(entry => entry.isCurrent);
376
+ if (resolved[currentIndex]?.interactive) {
377
+ return currentIndex;
378
+ }
379
+ return resolved.findIndex(entry => entry.interactive);
221
380
  }
222
381
  update() {
223
382
  if (!this.stepsEl) {
@@ -231,24 +390,35 @@ export class ProgressSteps extends BaseElement {
231
390
  this.setAttribute("value", selectedValue);
232
391
  }
233
392
  this.stepsEl.setAttribute("aria-label", this.label);
393
+ const resolved = resolveStepStates(items, this.valueInternal);
394
+ const tabbable = this.tabbableIndex(resolved);
234
395
  const signature = JSON.stringify(items);
235
396
  if (signature !== this.itemsSignature) {
236
397
  this.itemsSignature = signature;
237
- this.stepsEl.innerHTML = items
238
- .map((item, index) => {
239
- const state = index < activeIndex ? "complete" : index === activeIndex ? "current" : "upcoming";
398
+ this.stepsEl.innerHTML = resolved
399
+ .map((entry, index) => {
400
+ const { item, state, isCurrent, interactive } = entry;
401
+ // Blocked / failed / disabled are said out loud on the surface; the
402
+ // positional states get a screen-reader word so colour and a number
403
+ // are never the only carriers of the state.
404
+ const explicitStatus = item.status
405
+ ? `<span part="step-status">${STEP_STATE_LABEL[state]}</span>
406
+ ${item.statusNote ? `<span part="step-status-note">${escapeHtml(item.statusNote)}</span>` : ""}`
407
+ : `<span part="step-state" class="boe-sr-only">${STEP_STATE_LABEL[state]}</span>`;
240
408
  return `
241
409
  <button
242
410
  type="button"
243
411
  part="step"
244
412
  data-state="${state}"
245
413
  data-value="${escapeHtml(item.value)}"
246
- aria-current="${state === "current" ? "step" : "false"}"
247
- tabindex="${index === activeIndex ? "0" : "-1"}"
414
+ aria-current="${isCurrent ? "step" : "false"}"
415
+ tabindex="${index === tabbable ? "0" : "-1"}"
416
+ ${interactive ? "" : "disabled"}
248
417
  >
249
418
  <span part="marker">${index + 1}</span>
250
419
  <span part="content">
251
420
  <strong part="step-label">${escapeHtml(item.label)}</strong>
421
+ ${explicitStatus}
252
422
  ${item.description ? `<span part="step-description">${escapeHtml(item.description)}</span>` : ""}
253
423
  </span>
254
424
  </button>
@@ -258,12 +428,20 @@ export class ProgressSteps extends BaseElement {
258
428
  return;
259
429
  }
260
430
  this.stepsEl.querySelectorAll('[part="step"]').forEach((step, index) => {
261
- const state = index < activeIndex ? "complete" : index === activeIndex ? "current" : "upcoming";
431
+ const entry = resolved[index];
432
+ if (!entry) {
433
+ return;
434
+ }
262
435
  const button = step;
263
- button.dataset.state = state;
264
- button.setAttribute("aria-current", state === "current" ? "step" : "false");
265
- button.setAttribute("aria-selected", String(index === activeIndex));
266
- button.tabIndex = index === activeIndex ? 0 : -1;
436
+ button.dataset.state = entry.state;
437
+ button.setAttribute("aria-current", entry.isCurrent ? "step" : "false");
438
+ button.tabIndex = index === tabbable ? 0 : -1;
439
+ button.disabled = !entry.interactive;
440
+ // Positional states move with `value`; keep the spoken word in step.
441
+ const stateWord = button.querySelector('[part="step-state"]');
442
+ if (stateWord) {
443
+ stateWord.textContent = STEP_STATE_LABEL[entry.state];
444
+ }
267
445
  });
268
446
  }
269
447
  }
@@ -0,0 +1,44 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ /** One point in a record's lifecycle. */
3
+ export interface StagePathStage {
4
+ id: string;
5
+ label: string;
6
+ /** Optional detail shown under the label on the current stage. */
7
+ description?: string;
8
+ }
9
+ export type StageState = "complete" | "current" | "upcoming";
10
+ /** Attribute payloads are author input — validate every record. */
11
+ export declare const isStagePathStageRecord: (value: unknown) => value is StagePathStage;
12
+ /** State of each stage relative to the current one. */
13
+ export declare const resolveStageStates: (stages: readonly StagePathStage[], currentId: string) => StageState[];
14
+ /**
15
+ * Horizontal chevron lifecycle tracker for record headers — Draft → In
16
+ * Review → Approved → Executed.
17
+ *
18
+ * Distinct from `box-progress-steps`, which is a vertical setup rail for a
19
+ * task the reader is working through: this states where a *record* sits in
20
+ * its lifecycle, is read-only, and belongs in a header rather than a form.
21
+ *
22
+ * Rendered as an ordered list with the current stage marked `aria-current`,
23
+ * so the sequence and the position are both available without relying on the
24
+ * chevron geometry — which is decoration, and collapses on narrow viewports.
25
+ */
26
+ export declare class StagePath extends BaseElement {
27
+ static readonly tagName: string;
28
+ static get observedAttributes(): string[];
29
+ private pathEl;
30
+ private stagesRaw;
31
+ private stagesCache;
32
+ get label(): string;
33
+ set label(value: string);
34
+ /** Id of the stage the record is at. An unknown id leaves every stage upcoming. */
35
+ get current(): string;
36
+ set current(value: string);
37
+ get stages(): StagePathStage[];
38
+ set stages(value: StagePathStage[]);
39
+ /** State of each stage, in order. */
40
+ get states(): StageState[];
41
+ protected renderTemplate(): void;
42
+ protected setupListeners(): void;
43
+ protected update(): void;
44
+ }
@@ -0,0 +1,235 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
3
+ const DEFAULT_TAG_NAME = "box-stage-path";
4
+ const escapeHtml = (value) => value
5
+ .replaceAll("&", "&amp;")
6
+ .replaceAll("<", "&lt;")
7
+ .replaceAll(">", "&gt;")
8
+ .replaceAll('"', "&quot;")
9
+ .replaceAll("'", "&#39;");
10
+ /** Attribute payloads are author input — validate every record. */
11
+ export const isStagePathStageRecord = (value) => {
12
+ if (typeof value !== "object" || value === null) {
13
+ return false;
14
+ }
15
+ const stage = value;
16
+ return (typeof stage.id === "string" &&
17
+ stage.id.length > 0 &&
18
+ typeof stage.label === "string" &&
19
+ stage.label.length > 0);
20
+ };
21
+ /** State of each stage relative to the current one. */
22
+ export const resolveStageStates = (stages, currentId) => {
23
+ const index = stages.findIndex(stage => stage.id === currentId);
24
+ // An unknown current id means nothing is complete — better than silently
25
+ // marking the whole path done because the host sent a stale value.
26
+ return stages.map((_, position) => index === -1
27
+ ? "upcoming"
28
+ : position < index
29
+ ? "complete"
30
+ : position === index
31
+ ? "current"
32
+ : "upcoming");
33
+ };
34
+ const elementStyles = `
35
+ :host {
36
+ display: block;
37
+ }
38
+
39
+ [part="path"] {
40
+ display: flex;
41
+ flex-wrap: wrap;
42
+ margin: 0;
43
+ padding: 0;
44
+ list-style: none;
45
+ }
46
+
47
+ [part="stage"] {
48
+ position: relative;
49
+ flex: 1 1 0;
50
+ min-inline-size: 6rem;
51
+ padding: 0.45rem 0.7rem 0.45rem 1.35rem;
52
+ background: var(--boe-token-surface-surface-hover, #f4f4f4);
53
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
54
+ font-size: 0.82rem;
55
+ transition: background ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
56
+ }
57
+
58
+ [part="stage"]:first-child {
59
+ padding-inline-start: 0.8rem;
60
+ border-start-start-radius: 999px;
61
+ border-end-start-radius: 999px;
62
+ }
63
+
64
+ [part="stage"]:last-child {
65
+ border-start-end-radius: 999px;
66
+ border-end-end-radius: 999px;
67
+ }
68
+
69
+ /* The chevron: a notch cut from the following stage plus an arrow on
70
+ this one, so the path reads as a directed sequence rather than a row
71
+ of pills. Drawn with clip-path so no extra elements are needed. */
72
+ [part="stage"]:not(:last-child) {
73
+ clip-path: polygon(0 0, calc(100% - 0.7rem) 0, 100% 50%, calc(100% - 0.7rem) 100%, 0 100%, 0.7rem 50%);
74
+ margin-inline-end: -0.55rem;
75
+ }
76
+
77
+ [part="stage"]:first-child:not(:last-child) {
78
+ clip-path: polygon(0 0, calc(100% - 0.7rem) 0, 100% 50%, calc(100% - 0.7rem) 100%, 0 100%);
79
+ }
80
+
81
+ [part="stage"]:last-child:not(:first-child) {
82
+ clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 0.7rem 50%);
83
+ }
84
+
85
+ [part="stage"][data-state="complete"] {
86
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26a27b) 18%, transparent);
87
+ color: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26a27b) 72%, black 28%);
88
+ }
89
+
90
+ [part="stage"][data-state="current"] {
91
+ background: var(--boe-token-surface-surface-brand, #0061d5);
92
+ color: var(--boe-token-text-text-on-brand, #ffffff);
93
+ font-weight: 700;
94
+ }
95
+
96
+ [part="stage-label"] {
97
+ display: block;
98
+ }
99
+
100
+ [part="stage-description"] {
101
+ display: block;
102
+ margin-block-start: 0.1rem;
103
+ font-size: 0.72rem;
104
+ font-weight: 400;
105
+ opacity: 0.85;
106
+ }
107
+
108
+ [part="stage-marker"] {
109
+ margin-inline-end: 0.3rem;
110
+ font-weight: 700;
111
+ }
112
+
113
+ @media (prefers-reduced-motion: reduce) {
114
+ [part="stage"] {
115
+ transition: none;
116
+ }
117
+ }
118
+
119
+ @media (max-width: 34rem) {
120
+ /* Chevrons collapse below the width where they stop being legible;
121
+ the states still read from tone and the completed marker. */
122
+ [part="stage"],
123
+ [part="stage"]:first-child:not(:last-child),
124
+ [part="stage"]:last-child:not(:first-child) {
125
+ clip-path: none;
126
+ margin-inline-end: 0;
127
+ padding-inline-start: 0.8rem;
128
+ border-radius: 0;
129
+ }
130
+ }
131
+ `;
132
+ /**
133
+ * Horizontal chevron lifecycle tracker for record headers — Draft → In
134
+ * Review → Approved → Executed.
135
+ *
136
+ * Distinct from `box-progress-steps`, which is a vertical setup rail for a
137
+ * task the reader is working through: this states where a *record* sits in
138
+ * its lifecycle, is read-only, and belongs in a header rather than a form.
139
+ *
140
+ * Rendered as an ordered list with the current stage marked `aria-current`,
141
+ * so the sequence and the position are both available without relying on the
142
+ * chevron geometry — which is decoration, and collapses on narrow viewports.
143
+ */
144
+ export class StagePath extends BaseElement {
145
+ static tagName = DEFAULT_TAG_NAME;
146
+ static get observedAttributes() {
147
+ return ["current", "label", "stages"];
148
+ }
149
+ pathEl;
150
+ stagesRaw = null;
151
+ stagesCache = [];
152
+ get label() {
153
+ return this.getAttribute("label") ?? "Lifecycle";
154
+ }
155
+ set label(value) {
156
+ this.setAttribute("label", value);
157
+ }
158
+ /** Id of the stage the record is at. An unknown id leaves every stage upcoming. */
159
+ get current() {
160
+ return this.getAttribute("current") ?? "";
161
+ }
162
+ set current(value) {
163
+ this.setAttribute("current", value);
164
+ }
165
+ get stages() {
166
+ const raw = this.getAttribute("stages");
167
+ if (!raw) {
168
+ return [];
169
+ }
170
+ if (raw !== this.stagesRaw) {
171
+ this.stagesRaw = raw;
172
+ try {
173
+ const parsed = JSON.parse(raw);
174
+ this.stagesCache =
175
+ Array.isArray(parsed) && parsed.every(isStagePathStageRecord)
176
+ ? parsed
177
+ : [];
178
+ }
179
+ catch {
180
+ this.stagesCache = [];
181
+ }
182
+ }
183
+ return [...this.stagesCache];
184
+ }
185
+ set stages(value) {
186
+ if (value.length) {
187
+ this.setAttribute("stages", JSON.stringify(value));
188
+ return;
189
+ }
190
+ this.removeAttribute("stages");
191
+ }
192
+ /** State of each stage, in order. */
193
+ get states() {
194
+ return resolveStageStates(this.stages, this.current);
195
+ }
196
+ renderTemplate() {
197
+ if (!this.shadowRoot) {
198
+ return;
199
+ }
200
+ this.shadowRoot.innerHTML = `<style>${elementStyles}</style><ol part="path"></ol>`;
201
+ this.pathEl = this.shadowRoot.querySelector('[part="path"]');
202
+ }
203
+ setupListeners() {
204
+ // Read-only: a record's lifecycle position is not something a header edits.
205
+ }
206
+ update() {
207
+ if (!this.pathEl) {
208
+ return;
209
+ }
210
+ const stages = this.stages;
211
+ const states = resolveStageStates(stages, this.current);
212
+ this.pathEl.setAttribute("aria-label", this.label);
213
+ this.pathEl.innerHTML = stages
214
+ .map((stage, index) => {
215
+ const state = states[index];
216
+ // The completed marker is a second, non-colour signal for "done".
217
+ const marker = state === "complete" ? `<span part="stage-marker" aria-hidden="true">✓</span>` : "";
218
+ return `
219
+ <li
220
+ part="stage"
221
+ data-stage-id="${escapeHtml(stage.id)}"
222
+ data-state="${state}"
223
+ ${state === "current" ? 'aria-current="step"' : ""}
224
+ >
225
+ <span part="stage-label">${marker}${escapeHtml(stage.label)}</span>
226
+ ${state === "current" && stage.description
227
+ ? `<span part="stage-description">${escapeHtml(stage.description)}</span>`
228
+ : ""}
229
+ </li>
230
+ `;
231
+ })
232
+ .join("");
233
+ }
234
+ }
235
+ StagePath.register();
@@ -6,6 +6,11 @@ type SelectOption = {
6
6
  disabled?: boolean;
7
7
  group?: string;
8
8
  };
9
+ /** Detail of this element's `value-changed` event; `values` only when `multiple`. */
10
+ export interface SelectValueChangedDetail {
11
+ value: string;
12
+ values?: string[];
13
+ }
9
14
  export declare class Select extends FormAssociatedElement {
10
15
  static readonly tagName: string;
11
16
  static get observedAttributes(): string[];
@@ -15,6 +20,8 @@ export declare class Select extends FormAssociatedElement {
15
20
  private labelEl;
16
21
  private descriptionEl;
17
22
  private errorEl;
23
+ private controlEl;
24
+ private statusEl;
18
25
  /** Allow selecting several options (native multi-select list box). */
19
26
  get multiple(): boolean;
20
27
  set multiple(value: boolean);
@@ -27,6 +34,20 @@ export declare class Select extends FormAssociatedElement {
27
34
  set disabled(value: boolean);
28
35
  get label(): string;
29
36
  set label(value: string);
37
+ /**
38
+ * True while options are being fetched: shows a trailing spinner (matching
39
+ * `box-text-field`'s status vocabulary) and states "Loading options…" in
40
+ * the control instead of leaving it blank.
41
+ */
42
+ get loading(): boolean;
43
+ set loading(value: boolean);
44
+ /**
45
+ * What the control says when it has no options and is not loading. An
46
+ * async-loaded select with an empty result must say so — a silently blank
47
+ * control reads as broken.
48
+ */
49
+ get emptyText(): string;
50
+ set emptyText(value: string);
30
51
  get options(): SelectOption[];
31
52
  set options(value: SelectOption[]);
32
53
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;