@unofficialbox/box-open-elements 0.5.0 → 0.6.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 (219) hide show
  1. package/README.md +3 -3
  2. package/dist/components/collections/table.d.ts +57 -3
  3. package/dist/components/collections/table.js +339 -20
  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/element-maps.d.ts +292 -0
  30. package/dist/element-maps.js +1 -0
  31. package/dist/entries/activity-density.d.ts +1 -0
  32. package/dist/entries/activity-density.js +1 -0
  33. package/dist/entries/agent-chat.d.ts +1 -0
  34. package/dist/entries/agent-chat.js +1 -0
  35. package/dist/entries/audit-log.d.ts +1 -0
  36. package/dist/entries/audit-log.js +1 -0
  37. package/dist/entries/command-palette.d.ts +1 -0
  38. package/dist/entries/command-palette.js +1 -0
  39. package/dist/entries/compare-view.d.ts +1 -0
  40. package/dist/entries/compare-view.js +1 -0
  41. package/dist/entries/diff-viewer.d.ts +1 -0
  42. package/dist/entries/diff-viewer.js +1 -0
  43. package/dist/entries/due-badge.d.ts +1 -0
  44. package/dist/entries/due-badge.js +1 -0
  45. package/dist/entries/form-wizard.d.ts +1 -0
  46. package/dist/entries/form-wizard.js +1 -0
  47. package/dist/entries/lineage-graph.d.ts +1 -0
  48. package/dist/entries/lineage-graph.js +1 -0
  49. package/dist/entries/notification-bell.d.ts +1 -0
  50. package/dist/entries/notification-bell.js +1 -0
  51. package/dist/entries/notification-inbox.d.ts +1 -0
  52. package/dist/entries/notification-inbox.js +1 -0
  53. package/dist/entries/provenance-strip.d.ts +1 -0
  54. package/dist/entries/provenance-strip.js +1 -0
  55. package/dist/entries/run-trace.d.ts +1 -0
  56. package/dist/entries/run-trace.js +1 -0
  57. package/dist/entries/shortcuts-overlay.d.ts +1 -0
  58. package/dist/entries/shortcuts-overlay.js +1 -0
  59. package/dist/entries/signature-ceremony.d.ts +1 -0
  60. package/dist/entries/signature-ceremony.js +1 -0
  61. package/dist/entries/stage-path.d.ts +1 -0
  62. package/dist/entries/stage-path.js +1 -0
  63. package/dist/entries/timeline.d.ts +1 -0
  64. package/dist/entries/timeline.js +1 -0
  65. package/dist/entries/version-graph.d.ts +1 -0
  66. package/dist/entries/version-graph.js +1 -0
  67. package/dist/entries/version-list.d.ts +1 -0
  68. package/dist/entries/version-list.js +1 -0
  69. package/dist/entries/wizard-summary.d.ts +1 -0
  70. package/dist/entries/wizard-summary.js +1 -0
  71. package/dist/entries/work-queue.d.ts +1 -0
  72. package/dist/entries/work-queue.js +1 -0
  73. package/dist/entries/workload-board.d.ts +1 -0
  74. package/dist/entries/workload-board.js +1 -0
  75. package/dist/index.d.ts +21 -0
  76. package/dist/index.js +21 -0
  77. package/dist/patterns/agent-chat/agent-chat.d.ts +75 -0
  78. package/dist/patterns/agent-chat/agent-chat.js +820 -0
  79. package/dist/patterns/agent-chat/controller.d.ts +27 -0
  80. package/dist/patterns/agent-chat/controller.js +182 -0
  81. package/dist/patterns/agent-chat/index.d.ts +3 -0
  82. package/dist/patterns/agent-chat/index.js +6 -0
  83. package/dist/patterns/agent-chat/types.d.ts +109 -0
  84. package/dist/patterns/agent-chat/types.js +1 -0
  85. package/dist/patterns/audit/activity-density.d.ts +68 -0
  86. package/dist/patterns/audit/activity-density.js +428 -0
  87. package/dist/patterns/audit/audit-log.d.ts +81 -0
  88. package/dist/patterns/audit/audit-log.js +794 -0
  89. package/dist/patterns/audit/index.d.ts +3 -0
  90. package/dist/patterns/audit/index.js +7 -0
  91. package/dist/patterns/audit/shared.d.ts +10 -0
  92. package/dist/patterns/audit/shared.js +50 -0
  93. package/dist/patterns/audit/types.d.ts +143 -0
  94. package/dist/patterns/audit/types.js +327 -0
  95. package/dist/patterns/content-explorer/actions/controller.js +12 -2
  96. package/dist/patterns/content-explorer/box-transport.js +117 -25
  97. package/dist/patterns/content-explorer/content-explorer.d.ts +13 -1
  98. package/dist/patterns/content-explorer/content-explorer.js +36 -3
  99. package/dist/patterns/content-explorer/contracts.d.ts +24 -1
  100. package/dist/patterns/content-explorer/contracts.js +68 -11
  101. package/dist/patterns/content-explorer/controller.d.ts +16 -1
  102. package/dist/patterns/content-explorer/controller.js +130 -4
  103. package/dist/patterns/content-explorer/navigation/controller.js +11 -9
  104. package/dist/patterns/content-explorer/types.d.ts +58 -1
  105. package/dist/patterns/content-picker/content-picker.d.ts +71 -0
  106. package/dist/patterns/content-picker/content-picker.js +747 -0
  107. package/dist/patterns/content-picker/controller.d.ts +38 -0
  108. package/dist/patterns/content-picker/controller.js +141 -0
  109. package/dist/patterns/content-picker/index.d.ts +3 -0
  110. package/dist/patterns/content-picker/index.js +6 -0
  111. package/dist/patterns/content-picker/types.d.ts +52 -0
  112. package/dist/patterns/content-picker/types.js +26 -0
  113. package/dist/patterns/content-sidebar/content-sidebar.d.ts +43 -0
  114. package/dist/patterns/content-sidebar/content-sidebar.js +293 -0
  115. package/dist/patterns/content-sidebar/index.d.ts +2 -0
  116. package/dist/patterns/content-sidebar/index.js +4 -0
  117. package/dist/patterns/content-sidebar/types.d.ts +18 -0
  118. package/dist/patterns/content-sidebar/types.js +20 -0
  119. package/dist/patterns/content-uploader/box-transport.d.ts +14 -0
  120. package/dist/patterns/content-uploader/box-transport.js +54 -0
  121. package/dist/patterns/content-uploader/content-uploader.d.ts +62 -0
  122. package/dist/patterns/content-uploader/content-uploader.js +559 -0
  123. package/dist/patterns/content-uploader/controller.d.ts +39 -0
  124. package/dist/patterns/content-uploader/controller.js +227 -0
  125. package/dist/patterns/content-uploader/index.d.ts +4 -0
  126. package/dist/patterns/content-uploader/index.js +8 -0
  127. package/dist/patterns/content-uploader/types.d.ts +120 -0
  128. package/dist/patterns/content-uploader/types.js +38 -0
  129. package/dist/patterns/diff/compare-view.d.ts +47 -0
  130. package/dist/patterns/diff/compare-view.js +276 -0
  131. package/dist/patterns/diff/diff-viewer.d.ts +42 -0
  132. package/dist/patterns/diff/diff-viewer.js +434 -0
  133. package/dist/patterns/diff/engine.d.ts +22 -0
  134. package/dist/patterns/diff/engine.js +276 -0
  135. package/dist/patterns/diff/index.d.ts +4 -0
  136. package/dist/patterns/diff/index.js +8 -0
  137. package/dist/patterns/diff/types.d.ts +69 -0
  138. package/dist/patterns/diff/types.js +24 -0
  139. package/dist/patterns/form-wizard/controller.d.ts +39 -0
  140. package/dist/patterns/form-wizard/controller.js +157 -0
  141. package/dist/patterns/form-wizard/form-wizard.d.ts +63 -0
  142. package/dist/patterns/form-wizard/form-wizard.js +468 -0
  143. package/dist/patterns/form-wizard/index.d.ts +4 -0
  144. package/dist/patterns/form-wizard/index.js +8 -0
  145. package/dist/patterns/form-wizard/types.d.ts +109 -0
  146. package/dist/patterns/form-wizard/types.js +83 -0
  147. package/dist/patterns/form-wizard/wizard-summary.d.ts +62 -0
  148. package/dist/patterns/form-wizard/wizard-summary.js +324 -0
  149. package/dist/patterns/internal/safe-href.d.ts +17 -0
  150. package/dist/patterns/internal/safe-href.js +17 -0
  151. package/dist/patterns/lineage/index.d.ts +3 -0
  152. package/dist/patterns/lineage/index.js +6 -0
  153. package/dist/patterns/lineage/lineage-graph.d.ts +30 -0
  154. package/dist/patterns/lineage/lineage-graph.js +434 -0
  155. package/dist/patterns/lineage/provenance-strip.d.ts +22 -0
  156. package/dist/patterns/lineage/provenance-strip.js +194 -0
  157. package/dist/patterns/lineage/types.d.ts +53 -0
  158. package/dist/patterns/lineage/types.js +49 -0
  159. package/dist/patterns/notifications/index.d.ts +3 -0
  160. package/dist/patterns/notifications/index.js +6 -0
  161. package/dist/patterns/notifications/notification-bell.d.ts +38 -0
  162. package/dist/patterns/notifications/notification-bell.js +198 -0
  163. package/dist/patterns/notifications/notification-inbox.d.ts +46 -0
  164. package/dist/patterns/notifications/notification-inbox.js +537 -0
  165. package/dist/patterns/notifications/types.d.ts +61 -0
  166. package/dist/patterns/notifications/types.js +108 -0
  167. package/dist/patterns/preview/annotation-inspector.d.ts +25 -6
  168. package/dist/patterns/preview/annotation-inspector.js +239 -94
  169. package/dist/patterns/preview/annotation-thread.d.ts +25 -5
  170. package/dist/patterns/preview/annotation-thread.js +233 -82
  171. package/dist/patterns/preview/annotation-toolbar.d.ts +20 -6
  172. package/dist/patterns/preview/annotation-toolbar.js +162 -96
  173. package/dist/patterns/preview/content-preview-adapter.d.ts +10 -1
  174. package/dist/patterns/preview/content-preview-adapter.js +26 -1
  175. package/dist/patterns/preview/preview-element.d.ts +15 -2
  176. package/dist/patterns/preview/preview-element.js +223 -52
  177. package/dist/patterns/preview/provider-adapter.d.ts +47 -0
  178. package/dist/patterns/preview/provider-adapter.js +17 -0
  179. package/dist/patterns/run/index.d.ts +2 -0
  180. package/dist/patterns/run/index.js +4 -0
  181. package/dist/patterns/run/run-trace.d.ts +40 -0
  182. package/dist/patterns/run/run-trace.js +441 -0
  183. package/dist/patterns/run/types.d.ts +66 -0
  184. package/dist/patterns/run/types.js +152 -0
  185. package/dist/patterns/signature/index.d.ts +2 -0
  186. package/dist/patterns/signature/index.js +4 -0
  187. package/dist/patterns/signature/signature-ceremony.d.ts +33 -0
  188. package/dist/patterns/signature/signature-ceremony.js +317 -0
  189. package/dist/patterns/signature/types.d.ts +68 -0
  190. package/dist/patterns/signature/types.js +89 -0
  191. package/dist/patterns/timeline/index.d.ts +2 -0
  192. package/dist/patterns/timeline/index.js +4 -0
  193. package/dist/patterns/timeline/timeline.d.ts +27 -0
  194. package/dist/patterns/timeline/timeline.js +430 -0
  195. package/dist/patterns/timeline/types.d.ts +36 -0
  196. package/dist/patterns/timeline/types.js +27 -0
  197. package/dist/patterns/versions/graph-layout.d.ts +43 -0
  198. package/dist/patterns/versions/graph-layout.js +159 -0
  199. package/dist/patterns/versions/index.d.ts +4 -0
  200. package/dist/patterns/versions/index.js +8 -0
  201. package/dist/patterns/versions/shared.d.ts +4 -0
  202. package/dist/patterns/versions/shared.js +13 -0
  203. package/dist/patterns/versions/types.d.ts +37 -0
  204. package/dist/patterns/versions/types.js +39 -0
  205. package/dist/patterns/versions/version-graph.d.ts +36 -0
  206. package/dist/patterns/versions/version-graph.js +438 -0
  207. package/dist/patterns/versions/version-list.d.ts +39 -0
  208. package/dist/patterns/versions/version-list.js +407 -0
  209. package/dist/patterns/work-queue/controller.d.ts +26 -0
  210. package/dist/patterns/work-queue/controller.js +150 -0
  211. package/dist/patterns/work-queue/index.d.ts +4 -0
  212. package/dist/patterns/work-queue/index.js +7 -0
  213. package/dist/patterns/work-queue/types.d.ts +128 -0
  214. package/dist/patterns/work-queue/types.js +45 -0
  215. package/dist/patterns/work-queue/work-queue.d.ts +48 -0
  216. package/dist/patterns/work-queue/work-queue.js +534 -0
  217. package/dist/patterns/work-queue/workload-board.d.ts +53 -0
  218. package/dist/patterns/work-queue/workload-board.js +608 -0
  219. package/package.json +89 -5
@@ -1,6 +1,15 @@
1
1
  import { FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDataFromNamedValues, formDescriptionMarkup, formErrorMessageMarkup, stringValuesFromFormValue, } from "../../core/index.js";
2
2
  import { boeControl, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
3
+ import { boeMotionDuration, boeMotionEasing, boeReducedMotionStyles, } from "../../foundations/motion/index.js";
3
4
  const DEFAULT_TAG_NAME = "box-select";
5
+ /** Attribute payloads are author input — validate every record. */
6
+ const isSelectOptionRecord = (value) => {
7
+ if (typeof value !== "object" || value === null) {
8
+ return false;
9
+ }
10
+ const option = value;
11
+ return typeof option.value === "string" && typeof option.label === "string";
12
+ };
4
13
  const escapeHtml = (value) => value
5
14
  .replaceAll("&", "&")
6
15
  .replaceAll("<", "&lt;")
@@ -77,16 +86,56 @@ const selectStyles = `
77
86
  border-radius: ${boeRadius.size};
78
87
  }
79
88
 
89
+ /* Control wrapper exists for the trailing status; with no status it is
90
+ layout-neutral. */
91
+ [part="control"] {
92
+ position: relative;
93
+ display: block;
94
+ }
95
+
96
+ [part="status"] {
97
+ position: absolute;
98
+ inset-block-start: 50%;
99
+ /* Left of the chevron. */
100
+ inset-inline-end: 2rem;
101
+ transform: translateY(-50%);
102
+ display: none;
103
+ align-items: center;
104
+ pointer-events: none;
105
+ }
106
+
107
+ [part="control"][data-status="loading"] [part="status"] {
108
+ display: inline-flex;
109
+ }
110
+
111
+ [part="spinner"] {
112
+ inline-size: 1rem;
113
+ block-size: 1rem;
114
+ border-radius: 999px;
115
+ border: 2px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 20%, transparent);
116
+ border-top-color: var(--boe-token-surface-surface-brand, #0061d5);
117
+ animation: boe-select-spin ${boeMotionDuration.spin} ${boeMotionEasing.linear} infinite;
118
+ }
119
+
120
+ @keyframes boe-select-spin {
121
+ to { transform: rotate(360deg); }
122
+ }
123
+
124
+ ${boeReducedMotionStyles('[part="spinner"]', "animation-duration: 1.6s;")}
125
+
80
126
  ${boeFormFieldErrorStyles}
81
127
  ${boeFormFieldSupportStyles}
82
128
  `;
129
+ const spinnerMarkup = '<span part="spinner" aria-hidden="true"></span>';
83
130
  export class Select extends FormAssociatedElement {
84
131
  static tagName = DEFAULT_TAG_NAME;
85
132
  static get observedAttributes() {
86
133
  return [
87
134
  ...FormAssociatedElement.fieldObservedAttributes,
88
135
  "disabled",
136
+ "empty-text",
89
137
  "label",
138
+ "loading",
90
139
  "multiple",
91
140
  "options",
92
141
  "value",
@@ -98,6 +147,8 @@ export class Select extends FormAssociatedElement {
98
147
  labelEl;
99
148
  descriptionEl;
100
149
  errorEl;
150
+ controlEl;
151
+ statusEl;
101
152
  /** Allow selecting several options (native multi-select list box). */
102
153
  get multiple() {
103
154
  return this.hasAttribute("multiple");
@@ -146,6 +197,28 @@ export class Select extends FormAssociatedElement {
146
197
  set label(value) {
147
198
  this.setAttribute("label", value);
148
199
  }
200
+ /**
201
+ * True while options are being fetched: shows a trailing spinner (matching
202
+ * `box-text-field`'s status vocabulary) and states "Loading options…" in
203
+ * the control instead of leaving it blank.
204
+ */
205
+ get loading() {
206
+ return this.hasAttribute("loading");
207
+ }
208
+ set loading(value) {
209
+ this.toggleAttribute("loading", Boolean(value));
210
+ }
211
+ /**
212
+ * What the control says when it has no options and is not loading. An
213
+ * async-loaded select with an empty result must say so — a silently blank
214
+ * control reads as broken.
215
+ */
216
+ get emptyText() {
217
+ return this.getAttribute("empty-text") ?? "No options available";
218
+ }
219
+ set emptyText(value) {
220
+ this.setAttribute("empty-text", value);
221
+ }
149
222
  get options() {
150
223
  const raw = this.getAttribute("options");
151
224
  if (!raw) {
@@ -153,7 +226,9 @@ export class Select extends FormAssociatedElement {
153
226
  }
154
227
  try {
155
228
  const parsed = JSON.parse(raw);
156
- return Array.isArray(parsed) ? parsed : [];
229
+ return Array.isArray(parsed) && parsed.every(isSelectOptionRecord)
230
+ ? parsed
231
+ : [];
157
232
  }
158
233
  catch {
159
234
  return [];
@@ -202,7 +277,10 @@ export class Select extends FormAssociatedElement {
202
277
  <label part="field">
203
278
  <span part="label"></span>
204
279
  ${formDescriptionMarkup()}
205
- <select part="select"></select>
280
+ <span part="control">
281
+ <select part="select"></select>
282
+ <span part="status" aria-hidden="true"></span>
283
+ </span>
206
284
  ${formErrorMessageMarkup()}
207
285
  </label>
208
286
  `;
@@ -210,6 +288,8 @@ export class Select extends FormAssociatedElement {
210
288
  this.descriptionEl = this.shadowRoot.querySelector('[part="description"]');
211
289
  this.selectEl = this.shadowRoot.querySelector('[part="select"]');
212
290
  this.errorEl = this.shadowRoot.querySelector('[part="error-message"]');
291
+ this.controlEl = this.shadowRoot.querySelector('[part="control"]');
292
+ this.statusEl = this.shadowRoot.querySelector('[part="status"]');
213
293
  }
214
294
  setupListeners() {
215
295
  this.selectEl.addEventListener("change", event => {
@@ -262,8 +342,25 @@ export class Select extends FormAssociatedElement {
262
342
  }
263
343
  this.labelEl.textContent = this.label;
264
344
  // Rebuild options (the list itself may change), grouping by `group`.
345
+ // While loading — and when an async load came back empty — the control
346
+ // says so in words instead of presenting a silently blank select.
265
347
  this.selectEl.multiple = this.multiple;
266
- this.selectEl.innerHTML = this.optionsMarkup();
348
+ const options = this.options;
349
+ if (this.loading) {
350
+ this.controlEl.dataset.status = "loading";
351
+ this.statusEl.innerHTML = spinnerMarkup;
352
+ this.selectEl.innerHTML = `<option value="" disabled selected>Loading options…</option>`;
353
+ }
354
+ else if (options.length === 0) {
355
+ this.controlEl.removeAttribute("data-status");
356
+ this.statusEl.innerHTML = "";
357
+ this.selectEl.innerHTML = `<option value="" disabled selected>${escapeHtml(this.emptyText)}</option>`;
358
+ }
359
+ else {
360
+ this.controlEl.removeAttribute("data-status");
361
+ this.statusEl.innerHTML = "";
362
+ this.selectEl.innerHTML = this.optionsMarkup();
363
+ }
267
364
  // Patch selected value(s) and disabled after rebuilding options.
268
365
  if (this.multiple) {
269
366
  const selected = new Set(this.valuesInternal);
@@ -1,9 +1,14 @@
1
1
  import { FormAssociatedElement } from "../../core/index.js";
2
2
  import type { FormValue } from "../../core/index.js";
3
+ /** Detail of this element's `value-changed` event, for typed listeners. */
4
+ export interface TextFieldValueChangedDetail {
5
+ value: string;
6
+ }
3
7
  export declare class TextField extends FormAssociatedElement {
4
8
  static readonly tagName: string;
5
9
  static get observedAttributes(): string[];
6
10
  private valueInternal;
11
+ private revealed;
7
12
  private inputEl;
8
13
  private labelEl;
9
14
  private descriptionEl;
@@ -11,9 +16,25 @@ export declare class TextField extends FormAssociatedElement {
11
16
  private controlEl;
12
17
  private iconSlot;
13
18
  private statusEl;
19
+ private revealEl;
14
20
  /** Input type passthrough (text/email/tel/url/password/search/number). */
15
21
  get type(): string;
16
22
  set type(value: string);
23
+ /**
24
+ * `autocomplete` passthrough to the inner input ("email",
25
+ * "current-password", "off", …). Without it password managers and
26
+ * autofill cannot classify the field — the shadow boundary hides any
27
+ * autocomplete the host writes on the custom element itself.
28
+ */
29
+ get autocomplete(): string;
30
+ set autocomplete(value: string);
31
+ /**
32
+ * Opt-in visibility toggle for `type="password"`: renders a Show/Hide
33
+ * control that swaps the inner input between password and text without
34
+ * changing the field's declared `type`.
35
+ */
36
+ get reveal(): boolean;
37
+ set reveal(value: boolean);
17
38
  /** Shows a trailing spinner (e.g. while validating/looking up asynchronously). */
18
39
  get loading(): boolean;
19
40
  set loading(value: boolean);
@@ -109,6 +109,47 @@ const textFieldStyles = `
109
109
  color: var(--boe-token-surface-status-surface-success, #26c281);
110
110
  }
111
111
 
112
+ /* Password visibility toggle: a word, not an eye glyph, so the state is
113
+ stated. Hidden entirely unless reveal is opted in on a password. */
114
+ [part="reveal"] {
115
+ position: absolute;
116
+ inset-block-start: 50%;
117
+ inset-inline-end: 0.35rem;
118
+ transform: translateY(-50%);
119
+ appearance: none;
120
+ border: 0;
121
+ padding: 0.15rem 0.4rem;
122
+ border-radius: 4px;
123
+ background: transparent;
124
+ font: inherit;
125
+ font-size: 0.78rem;
126
+ font-weight: 700;
127
+ color: var(--boe-token-surface-surface-brand, #0061d5);
128
+ cursor: pointer;
129
+ }
130
+
131
+ [part="reveal"]:hover {
132
+ background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 8%, transparent);
133
+ }
134
+
135
+ [part="reveal"]:focus-visible {
136
+ outline: 2px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 34%, transparent);
137
+ outline-offset: 1px;
138
+ }
139
+
140
+ [part="control"][data-reveal="true"] [part="input"] {
141
+ padding-inline-end: 3.4rem;
142
+ }
143
+
144
+ /* Reveal and a trailing status can coexist: the status shifts inward. */
145
+ [part="control"][data-reveal="true"] [part="status"] {
146
+ inset-inline-end: 3.4rem;
147
+ }
148
+
149
+ [part="control"][data-reveal="true"][data-status] [part="input"] {
150
+ padding-inline-end: 5rem;
151
+ }
152
+
112
153
  ${boeFormFieldErrorStyles}
113
154
  ${boeFormFieldSupportStyles}
114
155
  `;
@@ -117,9 +158,11 @@ export class TextField extends FormAssociatedElement {
117
158
  static get observedAttributes() {
118
159
  return [
119
160
  ...FormAssociatedElement.fieldObservedAttributes,
161
+ "autocomplete",
120
162
  "disabled",
121
163
  "label",
122
164
  "placeholder",
165
+ "reveal",
123
166
  "type",
124
167
  "loading",
125
168
  "valid",
@@ -127,6 +170,7 @@ export class TextField extends FormAssociatedElement {
127
170
  ];
128
171
  }
129
172
  valueInternal = "";
173
+ revealed = false;
130
174
  inputEl;
131
175
  labelEl;
132
176
  descriptionEl;
@@ -134,6 +178,7 @@ export class TextField extends FormAssociatedElement {
134
178
  controlEl;
135
179
  iconSlot;
136
180
  statusEl;
181
+ revealEl;
137
182
  /** Input type passthrough (text/email/tel/url/password/search/number). */
138
183
  get type() {
139
184
  const value = this.getAttribute("type");
@@ -142,6 +187,34 @@ export class TextField extends FormAssociatedElement {
142
187
  set type(value) {
143
188
  this.setAttribute("type", value);
144
189
  }
190
+ /**
191
+ * `autocomplete` passthrough to the inner input ("email",
192
+ * "current-password", "off", …). Without it password managers and
193
+ * autofill cannot classify the field — the shadow boundary hides any
194
+ * autocomplete the host writes on the custom element itself.
195
+ */
196
+ get autocomplete() {
197
+ return this.getAttribute("autocomplete") ?? "";
198
+ }
199
+ set autocomplete(value) {
200
+ if (value) {
201
+ this.setAttribute("autocomplete", value);
202
+ }
203
+ else {
204
+ this.removeAttribute("autocomplete");
205
+ }
206
+ }
207
+ /**
208
+ * Opt-in visibility toggle for `type="password"`: renders a Show/Hide
209
+ * control that swaps the inner input between password and text without
210
+ * changing the field's declared `type`.
211
+ */
212
+ get reveal() {
213
+ return this.hasAttribute("reveal");
214
+ }
215
+ set reveal(value) {
216
+ this.toggleAttribute("reveal", Boolean(value));
217
+ }
145
218
  /** Shows a trailing spinner (e.g. while validating/looking up asynchronously). */
146
219
  get loading() {
147
220
  return this.hasAttribute("loading");
@@ -214,6 +287,7 @@ export class TextField extends FormAssociatedElement {
214
287
  <slot name="icon" part="icon"></slot>
215
288
  <input type="text" part="input" />
216
289
  <span part="status" aria-hidden="true"></span>
290
+ <button type="button" part="reveal" aria-pressed="false" hidden></button>
217
291
  </span>
218
292
  ${formErrorMessageMarkup()}
219
293
  </label>
@@ -225,9 +299,14 @@ export class TextField extends FormAssociatedElement {
225
299
  this.controlEl = this.shadowRoot.querySelector('[part="control"]');
226
300
  this.iconSlot = this.shadowRoot.querySelector('slot[name="icon"]');
227
301
  this.statusEl = this.shadowRoot.querySelector('[part="status"]');
302
+ this.revealEl = this.shadowRoot.querySelector('[part="reveal"]');
228
303
  }
229
304
  setupListeners() {
230
305
  this.iconSlot.addEventListener("slotchange", () => this.syncIconSlot());
306
+ this.revealEl.addEventListener("click", () => {
307
+ this.revealed = !this.revealed;
308
+ this.update();
309
+ });
231
310
  this.inputEl.addEventListener("input", event => {
232
311
  const nextValue = event.currentTarget.value;
233
312
  this.valueInternal = nextValue;
@@ -253,9 +332,29 @@ export class TextField extends FormAssociatedElement {
253
332
  return;
254
333
  }
255
334
  this.labelEl.textContent = this.label;
256
- this.inputEl.type = this.type;
335
+ const revealActive = this.reveal && this.type === "password";
336
+ this.inputEl.type = revealActive && this.revealed ? "text" : this.type;
257
337
  this.inputEl.placeholder = this.placeholder;
338
+ if (this.autocomplete) {
339
+ this.inputEl.setAttribute("autocomplete", this.autocomplete);
340
+ }
341
+ else {
342
+ this.inputEl.removeAttribute("autocomplete");
343
+ }
258
344
  this.syncIconSlot();
345
+ // The reveal control only exists for a password with reveal opted in.
346
+ this.revealEl.hidden = !revealActive;
347
+ this.controlEl.dataset.reveal = revealActive ? "true" : "false";
348
+ if (revealActive) {
349
+ // The visible word and the accessible name agree; aria-pressed makes
350
+ // the toggle state machine explicit to AT.
351
+ this.revealEl.textContent = this.revealed ? "Hide" : "Show";
352
+ this.revealEl.setAttribute("aria-label", this.revealed ? "Hide password" : "Show password");
353
+ this.revealEl.setAttribute("aria-pressed", String(this.revealed));
354
+ }
355
+ else {
356
+ this.revealed = false;
357
+ }
259
358
  // Trailing status: loading spinner takes precedence over the valid check.
260
359
  if (this.loading) {
261
360
  this.controlEl.dataset.status = "loading";
@@ -9,6 +9,12 @@ import { BaseElement } from "../../core/index.js";
9
9
  * Collapsed icon contract: slot `a`/`button` rows with an icon and a
10
10
  * `[data-nav-label]` span. Host exposes `--boe-nav-label-display` (`none` when
11
11
  * collapsed) so light-DOM CSS can hide labels without piercing the shadow tree.
12
+ *
13
+ * Hiding the label span removes the row's only accessible name, so while
14
+ * collapsed this element mirrors each row's label text onto `aria-label` (and
15
+ * `title`, for the hover tooltip) — but only on rows the host has not named
16
+ * itself, and it removes exactly what it added on expand. The host stays the
17
+ * authority: an author-set `aria-label` or `title` is never overwritten.
12
18
  */
13
19
  export declare class NavSidebar extends BaseElement {
14
20
  static readonly tagName: string;
@@ -19,5 +25,13 @@ export declare class NavSidebar extends BaseElement {
19
25
  get label(): string;
20
26
  set label(value: string);
21
27
  protected renderTemplate(): void;
28
+ protected setupListeners(): void;
29
+ /**
30
+ * Collapsing hides `[data-nav-label]`, which is a row's only accessible
31
+ * name. Mirror the label text onto `aria-label`/`title` while collapsed —
32
+ * marked, so expand removes only what collapse added and a host-authored
33
+ * name is never touched.
34
+ */
35
+ private syncCollapsedRowNames;
22
36
  protected update(): void;
23
37
  }
@@ -134,6 +134,12 @@ const navSidebarStyles = `
134
134
  * Collapsed icon contract: slot `a`/`button` rows with an icon and a
135
135
  * `[data-nav-label]` span. Host exposes `--boe-nav-label-display` (`none` when
136
136
  * collapsed) so light-DOM CSS can hide labels without piercing the shadow tree.
137
+ *
138
+ * Hiding the label span removes the row's only accessible name, so while
139
+ * collapsed this element mirrors each row's label text onto `aria-label` (and
140
+ * `title`, for the hover tooltip) — but only on rows the host has not named
141
+ * itself, and it removes exactly what it added on expand. The host stays the
142
+ * authority: an author-set `aria-label` or `title` is never overwritten.
137
143
  */
138
144
  export class NavSidebar extends BaseElement {
139
145
  static tagName = DEFAULT_TAG_NAME;
@@ -173,12 +179,55 @@ export class NavSidebar extends BaseElement {
173
179
  `;
174
180
  this.sidebarEl = this.shadowRoot.querySelector('[part="sidebar"]');
175
181
  }
182
+ setupListeners() {
183
+ // Rows slotted in while already collapsed still need their names mirrored.
184
+ this.shadowRoot
185
+ ?.querySelector('[part="body"] slot')
186
+ ?.addEventListener("slotchange", () => {
187
+ this.update();
188
+ });
189
+ }
190
+ /**
191
+ * Collapsing hides `[data-nav-label]`, which is a row's only accessible
192
+ * name. Mirror the label text onto `aria-label`/`title` while collapsed —
193
+ * marked, so expand removes only what collapse added and a host-authored
194
+ * name is never touched.
195
+ */
196
+ syncCollapsedRowNames() {
197
+ for (const row of Array.from(this.querySelectorAll("a, button"))) {
198
+ if (this.collapsed) {
199
+ const label = row.querySelector("[data-nav-label]")?.textContent?.trim();
200
+ if (!label) {
201
+ continue;
202
+ }
203
+ if (!row.hasAttribute("aria-label")) {
204
+ row.setAttribute("aria-label", label);
205
+ row.setAttribute("data-boe-managed-name", "");
206
+ }
207
+ if (!row.hasAttribute("title")) {
208
+ row.setAttribute("title", label);
209
+ row.setAttribute("data-boe-managed-title", "");
210
+ }
211
+ }
212
+ else {
213
+ if (row.hasAttribute("data-boe-managed-name")) {
214
+ row.removeAttribute("aria-label");
215
+ row.removeAttribute("data-boe-managed-name");
216
+ }
217
+ if (row.hasAttribute("data-boe-managed-title")) {
218
+ row.removeAttribute("title");
219
+ row.removeAttribute("data-boe-managed-title");
220
+ }
221
+ }
222
+ }
223
+ }
176
224
  update() {
177
225
  if (!this.sidebarEl) {
178
226
  return;
179
227
  }
180
228
  this.sidebarEl.dataset.collapsed = this.collapsed ? "true" : "false";
181
229
  this.sidebarEl.setAttribute("aria-label", this.label);
230
+ this.syncCollapsedRowNames();
182
231
  }
183
232
  }
184
233
  NavSidebar.register();
@@ -1,4 +1,8 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
+ /** Detail of `detail-dismissed`: what asked the slide-over to close. */
3
+ export interface SplitViewDetailDismissedDetail {
4
+ source: "escape";
5
+ }
2
6
  export declare class SplitView extends BaseElement {
3
7
  static readonly tagName: string;
4
8
  static get observedAttributes(): string[];
@@ -11,6 +15,16 @@ export declare class SplitView extends BaseElement {
11
15
  set ratio(value: number);
12
16
  get resizable(): boolean;
13
17
  set resizable(value: boolean);
18
+ /** "auto" collapses to master-detail when the container narrows. */
19
+ get collapse(): string;
20
+ set collapse(value: string);
21
+ /**
22
+ * Host-controlled: whether the secondary pane shows as a slide-over while
23
+ * collapsed. The host sets it on selection and clears it on
24
+ * `detail-dismissed` — selection state stays where it lives, in the list.
25
+ */
26
+ get detailOpen(): boolean;
27
+ set detailOpen(value: boolean);
14
28
  private setRatioFromResize;
15
29
  private stopResize;
16
30
  protected renderTemplate(): void;
@@ -33,11 +33,47 @@ const splitViewStyles = `
33
33
  height: 100%;
34
34
  background: color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 82%, transparent);
35
35
  }
36
+
37
+ :host {
38
+ container-type: inline-size;
39
+ }
40
+
41
+ /* Master-detail collapse (opt-in via collapse="auto"): when the container
42
+ narrows, the primary pane takes the full width and the secondary pane
43
+ becomes a slide-over that the host opens with detail-open — typically on
44
+ selection — and closes by clearing it (Escape asks via detail-dismissed).
45
+ The slotted content never moves; only the shadow wrapper changes shape. */
46
+ @container (max-width: 640px) {
47
+ :host([collapse="auto"]) [part="split-view"] {
48
+ grid-template-columns: minmax(0, 1fr) !important;
49
+ }
50
+
51
+ :host([collapse="auto"]) [part="separator"] {
52
+ display: none;
53
+ }
54
+
55
+ :host([collapse="auto"]) [part="secondary"] {
56
+ display: none;
57
+ }
58
+
59
+ :host([collapse="auto"][detail-open]) [part="secondary"] {
60
+ display: block;
61
+ position: fixed;
62
+ inset-block: 0;
63
+ inset-inline-end: 0;
64
+ z-index: 1100;
65
+ width: min(90vw, 26rem);
66
+ overflow: auto;
67
+ background: var(--boe-token-surface-surface, #ffffff);
68
+ border-inline-start: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
69
+ box-shadow: -12px 0 32px rgb(0 0 0 / 18%);
70
+ }
71
+ }
36
72
  `;
37
73
  export class SplitView extends BaseElement {
38
74
  static tagName = DEFAULT_TAG_NAME;
39
75
  static get observedAttributes() {
40
- return ["label", "ratio", "resizable"];
76
+ return ["collapse", "detail-open", "label", "ratio", "resizable"];
41
77
  }
42
78
  isResizing = false;
43
79
  splitViewEl;
@@ -64,6 +100,29 @@ export class SplitView extends BaseElement {
64
100
  set resizable(value) {
65
101
  this.toggleAttribute("resizable", value);
66
102
  }
103
+ /** "auto" collapses to master-detail when the container narrows. */
104
+ get collapse() {
105
+ return this.getAttribute("collapse") ?? "";
106
+ }
107
+ set collapse(value) {
108
+ if (value) {
109
+ this.setAttribute("collapse", value);
110
+ }
111
+ else {
112
+ this.removeAttribute("collapse");
113
+ }
114
+ }
115
+ /**
116
+ * Host-controlled: whether the secondary pane shows as a slide-over while
117
+ * collapsed. The host sets it on selection and clears it on
118
+ * `detail-dismissed` — selection state stays where it lives, in the list.
119
+ */
120
+ get detailOpen() {
121
+ return this.hasAttribute("detail-open");
122
+ }
123
+ set detailOpen(value) {
124
+ this.toggleAttribute("detail-open", Boolean(value));
125
+ }
67
126
  setRatioFromResize(nextRatio) {
68
127
  const clamped = Math.max(0.2, Math.min(0.8, nextRatio));
69
128
  const previous = this.ratio;
@@ -108,6 +167,26 @@ export class SplitView extends BaseElement {
108
167
  this.separatorEl = this.shadowRoot.querySelector('[part="separator"]');
109
168
  }
110
169
  setupListeners() {
170
+ // Escape inside the collapsed slide-over asks the host to close it; the
171
+ // host owns detail-open, so this only asks.
172
+ this.addEventListener("keydown", event => {
173
+ const keyboardEvent = event;
174
+ if (keyboardEvent.key !== "Escape" || !this.detailOpen || this.collapse !== "auto") {
175
+ return;
176
+ }
177
+ // The slide-over only exists under the 640px container query; on a wide
178
+ // container the secondary pane is an ordinary pane and Escape means
179
+ // nothing. Mirror the CSS condition rather than emitting a phantom ask.
180
+ if (this.offsetWidth > 640) {
181
+ return;
182
+ }
183
+ keyboardEvent.preventDefault();
184
+ this.dispatchEvent(new CustomEvent("detail-dismissed", {
185
+ bubbles: true,
186
+ composed: true,
187
+ detail: { source: "escape" },
188
+ }));
189
+ });
111
190
  this.separatorEl.addEventListener("pointerdown", event => {
112
191
  const pointerEvent = event;
113
192
  this.isResizing = true;
@@ -0,0 +1,62 @@
1
+ import type { CommandDescriptor } from "./command-types.js";
2
+ import { BaseElement } from "../../core/index.js";
3
+ /**
4
+ * Keyboard-first action launcher (CLM gap 4) over the existing overlay and
5
+ * listbox machinery.
6
+ *
7
+ * Follows the ARIA combobox-with-listbox pattern: focus never leaves the
8
+ * search input, so typing and navigating are the same gesture. The active
9
+ * option is communicated with `aria-activedescendant` rather than by moving
10
+ * focus, which is what lets Up/Down browse results while the query stays
11
+ * editable.
12
+ *
13
+ * The host owns what commands mean — the palette only reports the choice.
14
+ */
15
+ export declare class CommandPalette extends BaseElement {
16
+ static readonly tagName: string;
17
+ static get observedAttributes(): string[];
18
+ private hostEl;
19
+ private readonly focusRestore;
20
+ private openValue;
21
+ private wasOpen;
22
+ private query;
23
+ private activeIndex;
24
+ /** Flattened, ranked matches — the index space the keyboard walks. */
25
+ private rankedMatches;
26
+ private commandsRaw;
27
+ private commandsCache;
28
+ private recentIdsValue;
29
+ private readonly onDocumentKeydown;
30
+ get open(): boolean;
31
+ set open(value: boolean);
32
+ get placeholder(): string;
33
+ set placeholder(value: string);
34
+ /**
35
+ * Global shortcut that opens the palette, e.g. `mod+k`. `mod` resolves to
36
+ * Cmd or Ctrl per platform. Omit to let the host own the trigger.
37
+ */
38
+ get hotkey(): string;
39
+ set hotkey(value: string);
40
+ get hideDisabled(): boolean;
41
+ set hideDisabled(value: boolean);
42
+ /** Command catalogue; JSON payloads are validated per record. */
43
+ get commands(): CommandDescriptor[];
44
+ set commands(value: CommandDescriptor[]);
45
+ /** Most recently run command ids, most recent first; boosts ranking. */
46
+ get recentIds(): string[];
47
+ set recentIds(value: string[]);
48
+ /** The ranked matches currently on screen. */
49
+ get visibleCommands(): CommandDescriptor[];
50
+ show(): void;
51
+ hide(): void;
52
+ connectedCallback(): void;
53
+ disconnectedCallback(): void;
54
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
55
+ private runActive;
56
+ private moveActive;
57
+ private scrollActiveIntoView;
58
+ private optionHtml;
59
+ protected renderTemplate(): void;
60
+ protected setupListeners(): void;
61
+ protected update(): void;
62
+ }