@unofficialbox/box-open-elements 0.27.0 → 0.28.1

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 (42) hide show
  1. package/README.md +13 -13
  2. package/dist/components/actions/button.d.ts +3 -0
  3. package/dist/components/actions/button.js +16 -1
  4. package/dist/components/collections/resource-row.d.ts +28 -0
  5. package/dist/components/collections/resource-row.js +75 -0
  6. package/dist/components/feedback/mode-indicator.d.ts +21 -0
  7. package/dist/components/feedback/mode-indicator.js +67 -0
  8. package/dist/components/feedback/progress-steps.d.ts +7 -2
  9. package/dist/components/feedback/progress-steps.js +42 -3
  10. package/dist/components/feedback/verdict-banner.d.ts +23 -0
  11. package/dist/components/feedback/verdict-banner.js +59 -0
  12. package/dist/components/forms/code-editor.d.ts +10 -0
  13. package/dist/components/forms/code-editor.js +54 -4
  14. package/dist/components/forms/number-input.d.ts +1 -0
  15. package/dist/components/forms/number-input.js +11 -0
  16. package/dist/components/forms/text-area.d.ts +1 -0
  17. package/dist/components/forms/text-area.js +2 -0
  18. package/dist/components/forms/text-field.d.ts +3 -0
  19. package/dist/components/forms/text-field.js +15 -1
  20. package/dist/components/forms/tile-group.d.ts +6 -0
  21. package/dist/components/forms/tile-group.js +29 -2
  22. package/dist/components/layout/app-shell.js +13 -3
  23. package/dist/components/layout/nav-sidebar.js +14 -2
  24. package/dist/components/overlays/dialog.d.ts +6 -0
  25. package/dist/components/overlays/dialog.js +18 -2
  26. package/dist/components/overlays/drawer.d.ts +3 -0
  27. package/dist/components/overlays/drawer.js +11 -3
  28. package/dist/entries/mode-indicator.d.ts +1 -0
  29. package/dist/entries/mode-indicator.js +1 -0
  30. package/dist/entries/resource-row.d.ts +1 -0
  31. package/dist/entries/resource-row.js +1 -0
  32. package/dist/entries/verdict-banner.d.ts +1 -0
  33. package/dist/entries/verdict-banner.js +1 -0
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.js +3 -0
  36. package/dist/patterns/flow-builder/flow-builder.d.ts +8 -0
  37. package/dist/patterns/flow-builder/flow-builder.js +34 -0
  38. package/dist/patterns/flow-builder/primitives.d.ts +14 -2
  39. package/dist/patterns/flow-builder/primitives.js +28 -5
  40. package/dist/patterns/form-wizard/form-wizard.js +31 -8
  41. package/dist/patterns/form-wizard/wizard-summary.js +11 -2
  42. package/package.json +1 -1
@@ -12,6 +12,7 @@ import { FLOW_SPINE_PARTS, InsertPoint, KIND_PICKER_PARTS } from "./primitives.j
12
12
  export class FlowBuilder extends BaseElement {
13
13
  static tagName = "box-flow-builder";
14
14
  nodesValue = [];
15
+ figuresValue = new Map();
15
16
  modelValue = defaultFlowModel;
16
17
  catalogValue = [];
17
18
  selectedValue = null;
@@ -27,6 +28,38 @@ export class FlowBuilder extends BaseElement {
27
28
  get narrow() { return this.narrowValue; }
28
29
  /** Focus a rendered card without changing selection or opening the editor. */
29
30
  focusNode(node) { this.spine?.focusNode(node); }
31
+ /** Update one live card figure without rebuilding the flow or moving focus. */
32
+ setNodeFigure(target, figure) {
33
+ const node = typeof target === "string" ? this.findNodeById(target)
34
+ : Array.isArray(target) ? nodeAtPath({ body: this.nodes }, target)
35
+ : target;
36
+ if (!node)
37
+ return;
38
+ if (figure)
39
+ this.figuresValue.set(node, figure);
40
+ else
41
+ this.figuresValue.delete(node);
42
+ this.spine?.setNodeFigure(node, figure);
43
+ }
44
+ findNodeById(id) {
45
+ const seen = new Set();
46
+ const visit = (nodes) => {
47
+ for (const node of nodes) {
48
+ if (seen.has(node))
49
+ continue;
50
+ seen.add(node);
51
+ if (node.id === id)
52
+ return node;
53
+ for (const child of this.model.children(node)) {
54
+ const found = visit(child.list);
55
+ if (found)
56
+ return found;
57
+ }
58
+ }
59
+ return null;
60
+ };
61
+ return visit(this.nodes);
62
+ }
30
63
  insertion;
31
64
  invoker;
32
65
  spine;
@@ -106,6 +139,7 @@ export class FlowBuilder extends BaseElement {
106
139
  [hidden]{display:none!important} [part=inspector-heading]{font:inherit;font-weight:650;margin:0 0 12px}
107
140
  </style><div part="error" hidden></div><div part="layout"><box-flow-spine exportparts="${FLOW_SPINE_PARTS.join(", ")}"></box-flow-spine><div part="inspector" role="region" aria-label="Add a step"><box-kind-picker exportparts="${KIND_PICKER_PARTS.join(", ")}"></box-kind-picker><div part="editor"></div></div></div><div part="picker-popup" hidden><box-kind-picker exportparts="${KIND_PICKER_PARTS.join(", ")}"></box-kind-picker></div><box-drawer position="bottom" size="large"></box-drawer>`;
108
141
  this.spine = this.shadowRoot.querySelector("box-flow-spine");
142
+ this.spine.figures = this.figuresValue;
109
143
  this.aside = this.shadowRoot.querySelector("[part=inspector]");
110
144
  this.palette = this.aside.querySelector("box-kind-picker");
111
145
  this.inspector = this.aside.querySelector("[part=editor]");
@@ -1,9 +1,9 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
2
  import { type FlowInsertDetail, type FlowKind, type FlowModel, type FlowNodeBase } from "./model.js";
3
3
  /** Parts a card exposes; the spine and builder forward them unchanged. */
4
- export declare const FLOW_CARD_PARTS: readonly ["card", "icon", "title", "kind", "description", "status", "error"];
4
+ export declare const FLOW_CARD_PARTS: readonly ["card", "icon", "title", "kind", "description", "status", "figure", "error"];
5
5
  /** Parts the spine exposes (its own plus forwarded card and insert-point parts). */
6
- export declare const FLOW_SPINE_PARTS: readonly ["spine", "endpoint", "body", "branches", "branch", "branch-header", "branch-label", "branch-remove", "branch-add", "insert", "card", "icon", "title", "kind", "description", "status", "error"];
6
+ export declare const FLOW_SPINE_PARTS: readonly ["spine", "endpoint", "body", "branches", "branch", "branch-header", "branch-label", "branch-remove", "branch-add", "insert", "card", "icon", "title", "kind", "description", "status", "figure", "error"];
7
7
  /** Parts a kind picker exposes. */
8
8
  export declare const KIND_PICKER_PARTS: readonly ["choices", "group-heading", "choice", "choice-icon", "choice-label", "choice-description"];
9
9
  export declare class FlowCard extends BaseElement {
@@ -13,6 +13,10 @@ export declare class FlowCard extends BaseElement {
13
13
  model: FlowModel<FlowNodeBase>;
14
14
  selected: boolean;
15
15
  invalid: boolean;
16
+ figure: {
17
+ text: string;
18
+ tone?: "neutral" | "warning" | "error";
19
+ } | null;
16
20
  protected renderTemplate(): void;
17
21
  protected setupListeners(): void;
18
22
  refresh(): void;
@@ -49,10 +53,18 @@ export declare class FlowSpine extends BaseElement {
49
53
  endLabel: string;
50
54
  headingLevel: number;
51
55
  private cards;
56
+ figures: Map<FlowNodeBase, {
57
+ text: string;
58
+ tone?: "neutral" | "warning" | "error";
59
+ }>;
52
60
  protected renderTemplate(): void;
53
61
  refresh(): void;
54
62
  /** Re-render one card after its fields change, without rebuilding the spine. */
55
63
  refreshNode(node: FlowNodeBase): void;
64
+ setNodeFigure(node: FlowNodeBase, figure: {
65
+ text: string;
66
+ tone?: "neutral" | "warning" | "error";
67
+ } | null): void;
56
68
  focusNode(node: FlowNodeBase): void;
57
69
  select(node: FlowNodeBase | null, invalid?: FlowNodeBase | null): void;
58
70
  protected update(): void;
@@ -9,7 +9,7 @@ const styles = `
9
9
  `;
10
10
  function emit(host, name, detail) { host.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })); }
11
11
  /** Parts a card exposes; the spine and builder forward them unchanged. */
12
- export const FLOW_CARD_PARTS = ["card", "icon", "title", "kind", "description", "status", "error"];
12
+ export const FLOW_CARD_PARTS = ["card", "icon", "title", "kind", "description", "status", "figure", "error"];
13
13
  /** Parts the spine exposes (its own plus forwarded card and insert-point parts). */
14
14
  export const FLOW_SPINE_PARTS = ["spine", "endpoint", "body", "branches", "branch", "branch-header", "branch-label", "branch-remove", "branch-add", "insert", ...FLOW_CARD_PARTS];
15
15
  /** Parts a kind picker exposes. */
@@ -21,15 +21,19 @@ export class FlowCard extends BaseElement {
21
21
  model = defaultFlowModel;
22
22
  selected = false;
23
23
  invalid = false;
24
+ figure = null;
24
25
  renderTemplate() {
25
26
  this.shadowRoot.innerHTML = `<style>${styles}
26
27
  button { width:100%; text-align:start; display:grid; gap:6px; padding:16px; border:1px solid var(--boe-token-stroke-stroke,#ddd); border-radius:var(--boe-profile-radius-med,8px); background:var(--boe-token-surface-surface,#fff); overflow-wrap:anywhere; }
27
28
  button[aria-pressed=true] { border-color:var(--boe-token-surface-surface-brand,#0061d5); background:color-mix(in srgb,var(--boe-token-surface-surface-brand,#0061d5) 5%,var(--boe-token-surface-surface,#fff)); }
28
29
  button[data-invalid=true] { border-color:var(--boe-token-text-status-text-error,#b92340); }
29
- button.has-icon{grid-template-columns:20px minmax(0,1fr);column-gap:12px} button.has-icon [part=icon]{grid-column:1;grid-row:1/span 4} button.has-icon [part=title],button.has-icon [part=kind],button.has-icon [part=description],button.has-icon [part=status],button.has-icon [part=error]{grid-column:2}
30
+ button.has-icon{grid-template-columns:20px minmax(0,1fr);column-gap:12px} button.has-icon [part=icon]{grid-column:1;grid-row:1/span 5} button.has-icon [part=title],button.has-icon [part=kind],button.has-icon [part=description],button.has-icon [part=status],button.has-icon [part=figure],button.has-icon [part=error]{grid-column:2}
30
31
  [part=icon]{color:var(--boe-token-surface-surface-brand,#0061d5);width:20px;height:20px} [part=icon][data-tone=neutral]{color:var(--boe-token-text-text-secondary,#666)} [part=icon] svg{display:block;width:20px;height:20px} [part=title] { font-weight:650; } [part=kind],[part=description] { color:var(--boe-token-text-text-secondary,#666); } [part=error] { color:var(--boe-token-text-status-text-error,#b92340); }
32
+ [part=figure]{font-size:.78rem;font-variant-numeric:tabular-nums;color:var(--boe-token-text-text-secondary,#666);min-width:0;overflow-wrap:anywhere}
33
+ [part=figure][data-tone=warning]{color:var(--boe-token-text-status-text-warning,#805600)}
34
+ [part=figure][data-tone=error]{color:var(--boe-token-text-status-text-error,#b92340)}
31
35
  [hidden] { display:none!important; }
32
- </style><button type="button" part="card"><span part="icon" aria-hidden="true"></span><span part="title"></span><span part="kind"></span><span part="description"></span><span part="status"></span><span part="error">Needs attention</span></button>`;
36
+ </style><button type="button" part="card"><span part="icon" aria-hidden="true"></span><span part="title"></span><span part="kind"></span><span part="description"></span><span part="status"></span><span part="figure" hidden></span><span part="error">Needs attention</span></button>`;
33
37
  }
34
38
  setupListeners() { this.shadowRoot.querySelector("button").addEventListener("click", () => emit(this, "node-select", { node: this.node })); }
35
39
  refresh() { if (this.isRendered)
@@ -41,7 +45,7 @@ export class FlowCard extends BaseElement {
41
45
  const button = this.shadowRoot.querySelector("button");
42
46
  button.setAttribute("aria-pressed", String(this.selected));
43
47
  button.classList.toggle("has-icon", Boolean(kind?.icon));
44
- button.setAttribute("aria-label", cardLabel(this.node, this.catalog, this.invalid, this.model));
48
+ button.setAttribute("aria-label", [cardLabel(this.node, this.catalog, this.invalid, this.model), this.figure?.text].filter(Boolean).join(". "));
45
49
  button.dataset.invalid = String(this.invalid);
46
50
  const iconEl = this.shadowRoot.querySelector("[part=icon]");
47
51
  iconEl.replaceChildren(kind?.icon?.() ?? "");
@@ -53,6 +57,10 @@ export class FlowCard extends BaseElement {
53
57
  element.hidden = !text;
54
58
  }
55
59
  this.shadowRoot.querySelector("[part=error]").hidden = !this.invalid;
60
+ const figureEl = this.shadowRoot.querySelector("[part=figure]");
61
+ figureEl.textContent = this.figure?.text ?? "";
62
+ figureEl.hidden = !this.figure?.text;
63
+ figureEl.dataset.tone = this.figure?.tone ?? "neutral";
56
64
  }
57
65
  }
58
66
  export class InsertPoint extends BaseElement {
@@ -157,11 +165,23 @@ export class FlowSpine extends BaseElement {
157
165
  endLabel = "Flow ends";
158
166
  headingLevel = 3;
159
167
  cards = new Map();
168
+ figures = new Map();
160
169
  renderTemplate() { this.shadowRoot.innerHTML = `<style>${styles} [part=spine]{display:grid;gap:0;min-width:0} [part=branches]{display:flex;gap:16px;overflow-x:auto;max-width:100%;padding:8px} [part=branch]{flex:1 0 240px;min-width:0} [part=body]{margin:0 0 0 16px;border-inline-start:2px solid var(--boe-token-stroke-stroke,#ddd);padding:8px 12px} [part=endpoint]{text-align:center;font-weight:600;padding:12px} [part=branch-header]{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:1em 0} [part=branch-label]{font:inherit;font-weight:600;margin:0} [part=branch-remove],[part=branch-add]{border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:16px;background:var(--boe-token-surface-surface,#fff);padding:4px 10px} [part=branch-add]{justify-self:center;margin:4px 0}</style><div part="spine"></div>`; }
161
170
  refresh() { if (this.isRendered)
162
171
  this.update(); }
163
172
  /** Re-render one card after its fields change, without rebuilding the spine. */
164
173
  refreshNode(node) { this.cards.get(node)?.refresh(); }
174
+ setNodeFigure(node, figure) {
175
+ if (figure)
176
+ this.figures.set(node, figure);
177
+ else
178
+ this.figures.delete(node);
179
+ const card = this.cards.get(node);
180
+ if (card) {
181
+ card.figure = figure;
182
+ card.refresh();
183
+ }
184
+ }
165
185
  focusNode(node) { this.cards.get(node)?.focus(); }
166
186
  select(node, invalid = this.invalid) { this.selected = node; this.invalid = invalid; for (const [item, card] of this.cards) {
167
187
  card.selected = item === node;
@@ -193,6 +213,7 @@ export class FlowSpine extends BaseElement {
193
213
  card.model = this.model;
194
214
  card.selected = node === this.selected;
195
215
  card.invalid = node === this.invalid;
216
+ card.figure = this.figures.get(node) ?? null;
196
217
  card.refresh();
197
218
  fragment.append(card);
198
219
  const nextAncestors = new Set([...ancestors, node]);
@@ -254,8 +275,10 @@ export class FlowSpine extends BaseElement {
254
275
  end.textContent = this.endLabel;
255
276
  root.replaceChildren(start, listView(this.nodes, "in the flow", new Set()), end);
256
277
  for (const node of this.cards.keys())
257
- if (!seen.has(node))
278
+ if (!seen.has(node)) {
258
279
  this.cards.delete(node);
280
+ this.figures.delete(node);
281
+ }
259
282
  }
260
283
  }
261
284
  FlowCard.register();
@@ -77,7 +77,7 @@ const elementStyles = `
77
77
  font-size: 0.9rem;
78
78
  background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 10%, var(--boe-token-surface-surface, #ffffff));
79
79
  border: 1px solid color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 34%, transparent);
80
- color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 72%, black 28%);
80
+ color: var(--boe-token-text-status-text-error, #b92340);
81
81
  }
82
82
 
83
83
  [part="error"][hidden] {
@@ -338,6 +338,11 @@ export class FormWizard extends BaseElement {
338
338
  }));
339
339
  if (this.isRendered) {
340
340
  this.update();
341
+ if (eventName === "stepChanged") {
342
+ queueMicrotask(() => {
343
+ this.panelsEl.querySelector('[part="panel"]:not([hidden])')?.focus();
344
+ });
345
+ }
341
346
  }
342
347
  }));
343
348
  }
@@ -357,7 +362,7 @@ export class FormWizard extends BaseElement {
357
362
  this.shadowRoot.innerHTML = `
358
363
  <style>${elementStyles}</style>
359
364
  <section part="wizard" aria-label="Form wizard">
360
- <h2 part="title" hidden></h2>
365
+ <h2 id="wizard-title" part="title" hidden></h2>
361
366
  <div part="layout">
362
367
  <box-progress-steps part="rail"></box-progress-steps>
363
368
  <div part="body">
@@ -376,6 +381,7 @@ export class FormWizard extends BaseElement {
376
381
  `;
377
382
  this.titleEl = this.shadowRoot.querySelector('[part="title"]');
378
383
  this.railEl = this.shadowRoot.querySelector('[part="rail"]');
384
+ this.railEl.compact = true;
379
385
  this.panelsEl = this.shadowRoot.querySelector('[part="panels"]');
380
386
  this.errorEl = this.shadowRoot.querySelector('[part="error"]');
381
387
  this.backEl = this.shadowRoot.querySelector('[part="back"]');
@@ -418,7 +424,7 @@ export class FormWizard extends BaseElement {
418
424
  rebuildPanels(steps) {
419
425
  this.panelsEl.innerHTML = steps
420
426
  .map(step => `
421
- <div part="panel" data-step-id="${escapeHtml(step.id)}" role="group" aria-label="${escapeHtml(step.label)}" hidden>
427
+ <div part="panel" data-step-id="${escapeHtml(step.id)}" role="group" aria-label="${escapeHtml(step.label)}" tabindex="-1" hidden>
422
428
  <slot name="${escapeHtml(step.id)}"></slot>
423
429
  </div>
424
430
  `)
@@ -432,6 +438,15 @@ export class FormWizard extends BaseElement {
432
438
  const steps = state?.steps ?? [];
433
439
  this.titleEl.hidden = !this.heading;
434
440
  this.titleEl.textContent = this.heading;
441
+ const wizard = this.shadowRoot?.querySelector('[part="wizard"]');
442
+ if (this.heading) {
443
+ wizard?.setAttribute("aria-labelledby", "wizard-title");
444
+ wizard?.removeAttribute("aria-label");
445
+ }
446
+ else {
447
+ wizard?.removeAttribute("aria-labelledby");
448
+ wizard?.setAttribute("aria-label", "Form wizard");
449
+ }
435
450
  const signature = JSON.stringify(steps.map(step => step.id));
436
451
  if (signature !== this.panelsSignature) {
437
452
  this.panelsSignature = signature;
@@ -440,11 +455,19 @@ export class FormWizard extends BaseElement {
440
455
  this.suppressRailEvent = true;
441
456
  try {
442
457
  this.railEl.label = this.heading ? `${this.heading} steps` : "Wizard steps";
443
- this.railEl.items = steps.map(step => ({
444
- label: step.label,
445
- value: step.id,
446
- ...(step.description ? { description: step.description } : {}),
447
- }));
458
+ const furthestVisitedIndex = Math.max(-1, ...steps.map((step, index) => state?.visitedStepIds.includes(step.id) ? index : -1));
459
+ this.railEl.items = steps.map((step, index) => {
460
+ const visited = state?.visitedStepIds.includes(step.id) ?? false;
461
+ const status = visited && step.id !== state?.currentStepId
462
+ ? (index < furthestVisitedIndex || step.optional || state?.submitted ? "complete" : "visited")
463
+ : undefined;
464
+ return {
465
+ label: step.label,
466
+ value: step.id,
467
+ ...(step.description ? { description: step.description } : {}),
468
+ ...(status ? { status } : {}),
469
+ };
470
+ });
448
471
  if (state && this.railEl.value !== state.currentStepId) {
449
472
  this.railEl.value = state.currentStepId;
450
473
  }
@@ -11,6 +11,7 @@ const escapeHtml = (value) => value
11
11
  const elementStyles = `
12
12
  :host {
13
13
  display: block;
14
+ container-type: inline-size;
14
15
  }
15
16
 
16
17
  /* The host's own display would otherwise beat the UA rule for [hidden],
@@ -87,7 +88,7 @@ const elementStyles = `
87
88
  margin: 0;
88
89
  padding: 0 1rem 0.8rem;
89
90
  display: grid;
90
- grid-template-columns: minmax(8rem, 14rem) 1fr;
91
+ grid-template-columns: minmax(8rem, 14rem) minmax(0, 1fr);
91
92
  gap: 0.3rem 1rem;
92
93
  }
93
94
 
@@ -99,9 +100,17 @@ const elementStyles = `
99
100
 
100
101
  [part="row-value"] {
101
102
  margin: 0;
103
+ min-inline-size: 0;
104
+ max-inline-size: 100%;
105
+ overflow-x: auto;
102
106
  font-size: 0.85rem;
103
107
  color: var(--boe-token-text-text, #1f1e1b);
104
- overflow-wrap: anywhere;
108
+ overflow-wrap: normal;
109
+ word-break: normal;
110
+ }
111
+
112
+ @container (max-width: 32rem) {
113
+ [part="rows"] { grid-template-columns: minmax(0, 1fr); }
105
114
  }
106
115
 
107
116
  [part="row-value"][data-empty="true"] {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unofficialbox/box-open-elements",
3
- "version": "0.27.0",
3
+ "version": "0.28.1",
4
4
  "description": "Box Open Elements: an open, framework-agnostic design system and web component library for Box-style experiences",
5
5
  "license": "MIT",
6
6
  "homepage": "https://unofficialbox.github.io/box-open-elements",