@unofficialbox/box-open-elements 0.9.0 → 0.11.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 (55) hide show
  1. package/dist/components/feedback/alert.js +1 -16
  2. package/dist/components/feedback/path.d.ts +84 -0
  3. package/dist/components/feedback/path.js +455 -0
  4. package/dist/components/feedback/skeleton.d.ts +70 -0
  5. package/dist/components/feedback/skeleton.js +231 -1
  6. package/dist/components/feedback/toast.d.ts +38 -0
  7. package/dist/components/feedback/toast.js +189 -13
  8. package/dist/components/feedback/tone.d.ts +16 -0
  9. package/dist/components/feedback/tone.js +31 -0
  10. package/dist/components/layout/grid.d.ts +41 -0
  11. package/dist/components/layout/grid.js +152 -0
  12. package/dist/components/overlays/command-palette.d.ts +1 -0
  13. package/dist/components/overlays/command-palette.js +42 -3
  14. package/dist/components/overlays/context-menu.js +11 -1
  15. package/dist/components/overlays/dialog.d.ts +1 -0
  16. package/dist/components/overlays/dialog.js +43 -3
  17. package/dist/components/overlays/drawer.d.ts +1 -3
  18. package/dist/components/overlays/drawer.js +4 -20
  19. package/dist/components/overlays/guide-tooltip.js +15 -1
  20. package/dist/components/overlays/popover.js +16 -1
  21. package/dist/components/overlays/shortcuts-overlay.d.ts +1 -0
  22. package/dist/components/overlays/shortcuts-overlay.js +40 -3
  23. package/dist/components/overlays/tooltip.js +15 -1
  24. package/dist/element-maps.d.ts +5 -3
  25. package/dist/entries/grid.d.ts +1 -0
  26. package/dist/entries/grid.js +1 -0
  27. package/dist/entries/path.d.ts +1 -0
  28. package/dist/entries/path.js +1 -0
  29. package/dist/foundations/layout/grid.d.ts +70 -0
  30. package/dist/foundations/layout/grid.js +84 -0
  31. package/dist/foundations/layout/index.d.ts +1 -0
  32. package/dist/foundations/layout/index.js +3 -0
  33. package/dist/foundations/overlay/index.d.ts +1 -0
  34. package/dist/foundations/overlay/index.js +1 -0
  35. package/dist/foundations/overlay/top-layer.d.ts +62 -0
  36. package/dist/foundations/overlay/top-layer.js +95 -0
  37. package/dist/index.d.ts +4 -1
  38. package/dist/index.js +5 -1
  39. package/dist/patterns/audit/audit-log.js +0 -1
  40. package/dist/patterns/lineage/lineage-graph.d.ts +7 -0
  41. package/dist/patterns/lineage/lineage-graph.js +67 -6
  42. package/dist/patterns/lineage/provenance-strip.js +106 -15
  43. package/dist/patterns/notifications/notification-inbox.js +2 -18
  44. package/dist/patterns/timeline/timeline.js +44 -12
  45. package/dist/patterns/versions/graph-arrows.d.ts +22 -0
  46. package/dist/patterns/versions/graph-arrows.js +13 -0
  47. package/dist/patterns/versions/index.d.ts +1 -0
  48. package/dist/patterns/versions/index.js +1 -0
  49. package/dist/patterns/versions/version-graph.d.ts +7 -0
  50. package/dist/patterns/versions/version-graph.js +54 -6
  51. package/package.json +1 -1
  52. package/dist/components/feedback/stage-path.d.ts +0 -44
  53. package/dist/components/feedback/stage-path.js +0 -235
  54. package/dist/entries/stage-path.d.ts +0 -1
  55. package/dist/entries/stage-path.js +0 -1
@@ -1,12 +1,48 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
2
  import { boeRadius } from "../../foundations/geometry/index.js";
3
+ import { BOE_GRID_COLUMNS, BOE_GRID_GUTTER_PROPERTY, boeGridGutterStyles, isBoeGridPlacementRecord, resolveBoeGridPlacement, resolveGridCount, } from "../../foundations/layout/index.js";
3
4
  import { boeMotionDuration, boeMotionEasing, boeReducedMotionStyles, } from "../../foundations/motion/index.js";
4
5
  const DEFAULT_TAG_NAME = "box-skeleton";
6
+ const SKELETON_VARIANTS = new Set(["box", "line", "grid"]);
7
+ /**
8
+ * Narrow an author-supplied variant, falling back to `box`.
9
+ *
10
+ * A typo should render the original single rectangle rather than an empty
11
+ * element: every multi-part branch is scoped to a known value.
12
+ */
13
+ export const resolveSkeletonVariant = (value) => SKELETON_VARIANTS.has(value) ? value : "box";
14
+ /**
15
+ * The columns in Adobe Spectrum's responsive grid.
16
+ *
17
+ * Delegated to the layout foundation that `box-grid` also builds on: a
18
+ * skeleton stands in for a real layout, so if the two disagreed about what a
19
+ * column is worth the placeholder would stop matching what replaces it.
20
+ */
21
+ export const SKELETON_DEFAULT_COLUMNS = BOE_GRID_COLUMNS;
22
+ /** Attribute payloads are author input — validate every record. */
23
+ export const isSkeletonGridItemRecord = isBoeGridPlacementRecord;
24
+ /**
25
+ * Fit a region to the grid, clamping every value to the declared totals.
26
+ *
27
+ * Still exported here because it is part of this component's published
28
+ * surface; the behaviour now lives in the foundation.
29
+ */
30
+ export const resolveSkeletonGridItem = resolveBoeGridPlacement;
5
31
  const skeletonStyles = `
6
32
  :host {
7
33
  display: inline-block;
8
34
  color: inherit;
9
35
  font: inherit;
36
+
37
+ }
38
+
39
+ ${boeGridGutterStyles(":host")}
40
+
41
+ /* The multi-part variants are layout, so they take the full width they are
42
+ given rather than shrink-wrapping like the single box. */
43
+ :host([variant="line"]),
44
+ :host([variant="grid"]) {
45
+ display: block;
10
46
  }
11
47
 
12
48
  [part="skeleton"] {
@@ -22,6 +58,38 @@ const skeletonStyles = `
22
58
  animation: boe-skeleton-shimmer ${boeMotionDuration.shimmer} ${boeMotionEasing.standard} infinite;
23
59
  }
24
60
 
61
+ [part="lines"] {
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: 0.55rem;
65
+ }
66
+
67
+ [part="lines"] [part="skeleton"] {
68
+ display: block;
69
+ inline-size: 100%;
70
+ }
71
+
72
+ /* The last line stops short, the way a paragraph's does. Without it a stack
73
+ of equal bars reads as a table rather than as text. */
74
+ [part="lines"] [part="skeleton"]:last-child:not(:only-child) {
75
+ inline-size: 62%;
76
+ }
77
+
78
+ [part="grid"] {
79
+ display: grid;
80
+ grid-template-columns: repeat(var(--boe-skeleton-columns, ${BOE_GRID_COLUMNS}), minmax(0, 1fr));
81
+ grid-auto-rows: var(--boe-skeleton-row-height, 48px);
82
+ gap: var(${BOE_GRID_GUTTER_PROPERTY});
83
+ }
84
+
85
+ /* Offsets are spacers rather than an explicit grid-column-start, so they
86
+ compose with auto-placement instead of fighting it: an explicit start makes
87
+ the item hunt for a row where that exact column is free, which reorders the
88
+ regions the author listed. */
89
+ [part="grid-offset"] {
90
+ visibility: hidden;
91
+ }
92
+
25
93
  @keyframes boe-skeleton-shimmer {
26
94
  0% {
27
95
  background-position: 100% 0;
@@ -37,11 +105,15 @@ const skeletonStyles = `
37
105
  export class Skeleton extends BaseElement {
38
106
  static tagName = DEFAULT_TAG_NAME;
39
107
  static get observedAttributes() {
40
- return ["height", "width"];
108
+ return ["columns", "height", "items", "lines", "row-height", "rows", "variant", "width"];
41
109
  }
42
110
  skeletonEl;
111
+ rootEl;
43
112
  appliedWidth = "";
44
113
  appliedHeight = "";
114
+ renderedVariant = null;
115
+ itemsRaw = null;
116
+ itemsCache = [];
45
117
  get width() {
46
118
  return this.getAttribute("width") ?? "100%";
47
119
  }
@@ -54,6 +126,85 @@ export class Skeleton extends BaseElement {
54
126
  set height(value) {
55
127
  this.setAttribute("height", value);
56
128
  }
129
+ /** Shape of the placeholder. An unknown value falls back to `box`. */
130
+ get variant() {
131
+ return resolveSkeletonVariant(this.getAttribute("variant"));
132
+ }
133
+ set variant(value) {
134
+ this.setAttribute("variant", value);
135
+ }
136
+ /** Lines drawn by the `line` variant. Values below 1 render a single line. */
137
+ get lines() {
138
+ const parsed = Number.parseInt(this.getAttribute("lines") ?? "", 10);
139
+ return Number.isFinite(parsed) ? Math.max(parsed, 1) : 3;
140
+ }
141
+ set lines(value) {
142
+ this.setAttribute("lines", String(value));
143
+ }
144
+ /** Columns in the `grid` variant. Defaults to Spectrum's twelve. */
145
+ get columns() {
146
+ return resolveGridCount(this.getAttribute("columns"), BOE_GRID_COLUMNS);
147
+ }
148
+ set columns(value) {
149
+ this.setAttribute("columns", String(value));
150
+ }
151
+ /** Rows in the `grid` variant. Also the ceiling for any region's `rowSpan`. */
152
+ get rows() {
153
+ return resolveGridCount(this.getAttribute("rows"), 1);
154
+ }
155
+ set rows(value) {
156
+ this.setAttribute("rows", String(value));
157
+ }
158
+ /** Height of one grid row; a region spanning two is twice this plus a gutter. */
159
+ get rowHeight() {
160
+ return this.getAttribute("row-height") ?? "48px";
161
+ }
162
+ set rowHeight(value) {
163
+ this.setAttribute("row-height", value);
164
+ }
165
+ /**
166
+ * Regions of the `grid` variant, in order.
167
+ *
168
+ * With none given the grid is filled with single-column regions — `rows` ×
169
+ * `columns` of them — so `rows` and `columns` alone describe a uniform grid.
170
+ */
171
+ get items() {
172
+ const raw = this.getAttribute("items");
173
+ if (!raw) {
174
+ return [];
175
+ }
176
+ if (raw !== this.itemsRaw) {
177
+ this.itemsRaw = raw;
178
+ try {
179
+ const parsed = JSON.parse(raw);
180
+ this.itemsCache =
181
+ Array.isArray(parsed) && parsed.every(isSkeletonGridItemRecord)
182
+ ? parsed
183
+ : [];
184
+ }
185
+ catch {
186
+ this.itemsCache = [];
187
+ }
188
+ }
189
+ return [...this.itemsCache];
190
+ }
191
+ set items(value) {
192
+ if (value.length) {
193
+ this.setAttribute("items", JSON.stringify(value));
194
+ return;
195
+ }
196
+ this.removeAttribute("items");
197
+ }
198
+ /** The grid's regions as rendered: clamped to `columns` and `rows`. */
199
+ get resolvedItems() {
200
+ const columns = this.columns;
201
+ const rows = this.rows;
202
+ const declared = this.items;
203
+ const source = declared.length
204
+ ? declared
205
+ : Array.from({ length: rows * columns }, () => ({}));
206
+ return source.map(item => resolveSkeletonGridItem(item, columns, rows));
207
+ }
57
208
  renderTemplate() {
58
209
  if (!this.shadowRoot) {
59
210
  return;
@@ -65,8 +216,29 @@ export class Skeleton extends BaseElement {
65
216
  this.skeletonEl = this.shadowRoot.querySelector('[part="skeleton"]');
66
217
  this.appliedWidth = "";
67
218
  this.appliedHeight = "";
219
+ this.renderedVariant = "box";
68
220
  }
69
221
  update() {
222
+ if (!this.shadowRoot) {
223
+ return;
224
+ }
225
+ const variant = this.variant;
226
+ if (variant !== "box") {
227
+ this.updateGroup(variant);
228
+ return;
229
+ }
230
+ if (this.renderedVariant !== "box") {
231
+ // Coming back from a multi-part variant: restore the single box that the
232
+ // width/height path writes into.
233
+ this.shadowRoot.innerHTML = `
234
+ <style>${skeletonStyles}</style>
235
+ <span part="skeleton" style="display:inline-block;" aria-hidden="true"></span>
236
+ `;
237
+ this.skeletonEl = this.shadowRoot.querySelector('[part="skeleton"]');
238
+ this.appliedWidth = "";
239
+ this.appliedHeight = "";
240
+ this.renderedVariant = "box";
241
+ }
70
242
  if (!this.skeletonEl) {
71
243
  return;
72
244
  }
@@ -85,5 +257,63 @@ export class Skeleton extends BaseElement {
85
257
  this.appliedHeight = nextHeight;
86
258
  }
87
259
  }
260
+ /** Render the `line` or `grid` variant, which draw many bars rather than one. */
261
+ updateGroup(variant) {
262
+ const shadowRoot = this.shadowRoot;
263
+ if (this.renderedVariant !== variant) {
264
+ shadowRoot.innerHTML = `
265
+ <style>${skeletonStyles}</style>
266
+ <div part="${variant === "line" ? "lines" : "grid"}" aria-hidden="true"></div>
267
+ `;
268
+ this.rootEl = shadowRoot.querySelector(`[part="${variant === "line" ? "lines" : "grid"}"]`);
269
+ this.renderedVariant = variant;
270
+ this.appliedWidth = "";
271
+ this.appliedHeight = "";
272
+ }
273
+ if (variant === "line") {
274
+ this.renderLines();
275
+ return;
276
+ }
277
+ this.renderGrid();
278
+ }
279
+ renderLines() {
280
+ const count = this.lines;
281
+ const height = this.height;
282
+ // Reuse the bars already there: only the count changes between renders, and
283
+ // replacing every node would restart the shimmer on each update.
284
+ while (this.rootEl.childElementCount > count) {
285
+ this.rootEl.lastElementChild.remove();
286
+ }
287
+ while (this.rootEl.childElementCount < count) {
288
+ const line = document.createElement("span");
289
+ line.setAttribute("part", "skeleton");
290
+ this.rootEl.append(line);
291
+ }
292
+ for (const child of Array.from(this.rootEl.children)) {
293
+ // Via the CSSOM, for the same reason the box variant does: the value is
294
+ // author input and setProperty drops anything that is not a length.
295
+ child.style.setProperty("height", height);
296
+ }
297
+ }
298
+ renderGrid() {
299
+ this.rootEl.style.setProperty("--boe-skeleton-columns", String(this.columns));
300
+ this.rootEl.style.setProperty("--boe-skeleton-row-height", this.rowHeight);
301
+ // Rebuilt rather than patched: a change to items can alter every region's
302
+ // span and offset, so there is no stable identity to reuse.
303
+ this.rootEl.replaceChildren();
304
+ for (const item of this.resolvedItems) {
305
+ if (item.offset > 0) {
306
+ const spacer = document.createElement("span");
307
+ spacer.setAttribute("part", "grid-offset");
308
+ spacer.style.setProperty("grid-column", `span ${item.offset}`);
309
+ this.rootEl.append(spacer);
310
+ }
311
+ const region = document.createElement("span");
312
+ region.setAttribute("part", "skeleton");
313
+ region.style.setProperty("grid-column", `span ${item.span}`);
314
+ region.style.setProperty("grid-row", `span ${item.rowSpan}`);
315
+ this.rootEl.append(region);
316
+ }
317
+ }
88
318
  }
89
319
  Skeleton.register();
@@ -1,11 +1,25 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
+ /**
3
+ * Whether the toast clears itself.
4
+ *
5
+ * `dismissible` auto-dismisses after `duration` and can also be closed by hand;
6
+ * `sticky` stays until the reader closes it, whatever `duration` says. Both
7
+ * carry the close control — a toast the reader cannot get rid of is a trap.
8
+ */
9
+ export type ToastMode = "dismissible" | "sticky";
10
+ /** Narrow an author-supplied mode, falling back to `dismissible`. */
11
+ export declare const resolveToastMode: (value: string | null | undefined) => ToastMode;
2
12
  export declare class Toast extends BaseElement {
3
13
  static readonly tagName: string;
4
14
  static get observedAttributes(): string[];
5
15
  private openValue;
6
16
  private timeoutId;
7
17
  private toastEl;
18
+ private iconEl;
19
+ private headingEl;
20
+ private toneLabelEl;
8
21
  private messageEl;
22
+ private renderedTone;
9
23
  private actionSlot;
10
24
  private dismissEl;
11
25
  /** Auto-dismiss delay in ms for the declarative `open` path. 0 = sticky. */
@@ -16,6 +30,30 @@ export declare class Toast extends BaseElement {
16
30
  private scheduleAutoDismiss;
17
31
  get message(): string;
18
32
  set message(value: string);
33
+ /**
34
+ * Optional bold line above the message — "Upload failed" over "3 of 12 files
35
+ * could not be read". Named to match `box-alert`, which established the
36
+ * heading-plus-message shape in this family. Salesforce calls it `label`.
37
+ */
38
+ get heading(): string;
39
+ set heading(value: string);
40
+ /**
41
+ * Drop the outline for a softer, fill-only toast.
42
+ *
43
+ * Purely presentational and handled entirely in CSS, so it is deliberately
44
+ * not an observed attribute — there is nothing to re-render. The default
45
+ * stays bordered: that outline tracks box-ui-elements' `.notification` and
46
+ * is pinned by the colour conformance manifest.
47
+ */
48
+ get borderless(): boolean;
49
+ set borderless(value: boolean);
50
+ /**
51
+ * Whether the toast clears itself. `sticky` overrides any `duration`: a
52
+ * caller that has said "this one stays" should not have it taken away by a
53
+ * duration set elsewhere.
54
+ */
55
+ get mode(): ToastMode;
56
+ set mode(value: ToastMode);
19
57
  get tone(): string;
20
58
  set tone(value: string);
21
59
  disconnectedCallback(): void;
@@ -1,8 +1,29 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
+ import { toneAccessibleLabel } from "./tone.js";
2
3
  import { boeRadius, boeSpace } from "../../foundations/geometry/index.js";
3
4
  import { boeFocusVisibleStyles } from "../../foundations/tokens/index.js";
4
5
  import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
5
6
  const DEFAULT_TAG_NAME = "box-toast";
7
+ const TOAST_MODES = new Set(["dismissible", "sticky"]);
8
+ /** Narrow an author-supplied mode, falling back to `dismissible`. */
9
+ export const resolveToastMode = (value) => TOAST_MODES.has(value) ? value : "dismissible";
10
+ /**
11
+ * Status glyphs, one per tone.
12
+ *
13
+ * A toast is read at a glance and often out of the corner of an eye, so the
14
+ * shape has to carry the meaning before the colour does — a round tick and a
15
+ * warning triangle are distinguishable to a reader who cannot separate green
16
+ * from amber. Literal markup, never author input, so innerHTML is safe here.
17
+ */
18
+ const TONE_ICONS = {
19
+ info: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 3.4a1.15 1.15 0 110 2.3 1.15 1.15 0 010-2.3zM11.1 15H8.9V9.3h2.2V15z"/></svg>`,
20
+ success: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm4.06 5.86-5 5.2a1 1 0 01-1.44 0L5.94 11.3A1 1 0 117.38 9.9l1.04 1.08 4.28-4.52a1 1 0 011.36 1.38z"/></svg>`,
21
+ warning: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M9.13 2.6 1.4 15.9a1 1 0 00.87 1.5h15.46a1 1 0 00.87-1.5L10.87 2.6a1 1 0 00-1.74 0zM11.1 15.4H8.9v-2.2h2.2v2.2zm0-3.4H8.9V7.6h2.2V12z"/></svg>`,
22
+ error: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 2c1.29 0 2.48.41 3.46 1.1L5.1 13.46A6 6 0 0110 4zm0 12c-1.29 0-2.48-.41-3.46-1.1l8.36-8.36A6 6 0 0110 16z"/></svg>`,
23
+ };
24
+ /** The tone glyph, falling back to the info mark for an unknown tone. */
25
+ const toneIcon = (tone) => TONE_ICONS[tone] ?? TONE_ICONS.info;
26
+ const DISMISS_ICON = `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" focusable="false"><path d="M5.5 5.5l9 9M14.5 5.5l-9 9"/></svg>`;
6
27
  const toastStyles = `
7
28
  :host {
8
29
  display: block;
@@ -16,9 +37,18 @@ const toastStyles = `
16
37
  display: none;
17
38
  }
18
39
 
40
+ /* Fill, border, text colour and shadow all track box-ui-elements'
41
+ .notification and are pinned by the colour conformance manifest — they are
42
+ deliberately not derived from the tone accent. The --toast-accent property
43
+ exists only to colour the status glyph, which upstream has no equivalent
44
+ for, so adding it costs no conformance. */
19
45
  [part="toast"] {
46
+ --toast-accent: var(--boe-token-text-text, #222222);
47
+
20
48
  display: inline-flex;
21
- align-items: center;
49
+ /* Top, not centre: with a heading above the message the glyph belongs on
50
+ the heading's line, and centring floats it into the gap between them. */
51
+ align-items: flex-start;
22
52
  gap: ${boeSpace[3]};
23
53
  min-height: 48px;
24
54
  max-inline-size: min(100%, 572px);
@@ -30,33 +60,107 @@ const toastStyles = `
30
60
  box-shadow: 0 2px 6px rgb(0 0 0 / 15%);
31
61
  }
32
62
 
63
+ /* Opt-in softer treatment: the outline goes, the geometry does not. Setting
64
+ the colour to transparent rather than the width to 0 keeps the 2px in the
65
+ box model, so a borderless toast is exactly the size of a bordered one and
66
+ the two can sit in the same stack without jumping. The background paints
67
+ under the border by default, so the fill simply extends into it.
68
+
69
+ The bordered default stays: the 2px solid text-colour rule below is pinned
70
+ by the colour conformance manifest against upstream's .notification, and
71
+ strict mode fails if that declaration goes. */
72
+ :host([borderless]) [part="toast"] {
73
+ border-color: transparent;
74
+ }
75
+
76
+ /* The neutral toast keeps upstream's grey fill and near-black border, so the
77
+ glyph is the only place the "this is information" signal can live. */
78
+ [part="toast"][data-tone="info"] {
79
+ --toast-accent: var(--boe-token-surface-surface-brand, #0061d5);
80
+ }
81
+
33
82
  [part="toast"][data-tone="success"] {
83
+ --toast-accent: var(--boe-token-surface-status-surface-success, #26c281);
34
84
  background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 20%, #fff);
35
85
  border-color: var(--boe-token-surface-status-surface-success, #26c281);
36
86
  }
37
87
 
38
88
  [part="toast"][data-tone="error"] {
89
+ --toast-accent: var(--boe-token-surface-status-surface-error, #ed3757);
39
90
  background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 20%, #fff);
40
91
  border-color: var(--boe-token-surface-status-surface-error, #ed3757);
41
92
  }
42
93
 
43
94
  [part="toast"][data-tone="warning"],
44
95
  [part="toast"][data-tone="inprogress"] {
96
+ --toast-accent: var(--boe-token-surface-status-surface-inprogress, #f5b31b);
45
97
  background: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 20%, #fff);
46
98
  border-color: var(--boe-token-surface-status-surface-inprogress, #f5b31b);
47
99
  }
48
100
 
49
- [part="message"] {
101
+ .sr-only {
102
+ position: absolute;
103
+ inline-size: 1px;
104
+ block-size: 1px;
105
+ padding: 0;
106
+ margin: -1px;
107
+ overflow: hidden;
108
+ clip-path: inset(50%);
109
+ white-space: nowrap;
110
+ border: 0;
111
+ }
112
+
113
+ [part="icon"] {
114
+ flex: 0 0 auto;
115
+ inline-size: 20px;
116
+ block-size: 20px;
117
+ /* The accent at full strength: the glyph is the one element that should
118
+ read as the status colour rather than a darkened version of it. */
119
+ color: var(--toast-accent);
120
+ }
121
+
122
+ [part="icon"] svg {
123
+ display: block;
124
+ inline-size: 100%;
125
+ block-size: 100%;
126
+ }
127
+
128
+ [part="content"] {
50
129
  flex: 1 1 auto;
51
130
  min-inline-size: 0;
52
- padding-inline-end: ${boeSpace[3]};
131
+ padding-inline-end: ${boeSpace[2]};
132
+ }
133
+
134
+ [part="heading"] {
135
+ display: block;
53
136
  font-size: 15px;
54
137
  font-weight: 700;
138
+ line-height: 1.3;
139
+ overflow-wrap: anywhere;
140
+ }
141
+
142
+ /* Optional: with no heading the message stands alone, and an empty element
143
+ would still contribute its line box. */
144
+ [part="heading"]:empty {
145
+ display: none;
146
+ }
147
+
148
+ [part="message"] {
149
+ display: block;
150
+ font-size: 14px;
151
+ font-weight: 400;
55
152
  line-height: 1.35;
56
153
  /* Long unbroken tokens (URLs, filenames) wrap instead of overflowing. */
57
154
  overflow-wrap: anywhere;
58
155
  }
59
156
 
157
+ /* Without a heading the message is the toast's only line, so it carries the
158
+ weight the heading would have had. */
159
+ [part="heading"]:empty + [part="message"] {
160
+ font-size: 15px;
161
+ font-weight: 700;
162
+ }
163
+
60
164
  /* Optional action affordance (e.g. "Undo") before the dismiss button.
61
165
  Hidden — taking no flex gap — until content is assigned. */
62
166
  [part="action"] {
@@ -77,21 +181,33 @@ const toastStyles = `
77
181
  cursor: pointer;
78
182
  }
79
183
 
184
+ /* An icon button rather than a "Dismiss" label: the toast is chrome over the
185
+ reader's work, and a word-width button competes with the message it sits
186
+ beside. The accessible name is on the button, so nothing is lost. */
80
187
  [part="dismiss"] {
81
188
  appearance: none;
82
189
  flex: 0 0 auto;
83
- border: 1px solid var(--boe-token-text-text, #222222);
190
+ display: inline-flex;
191
+ align-items: center;
192
+ justify-content: center;
193
+ inline-size: 24px;
194
+ block-size: 24px;
195
+ padding: 4px;
196
+ border: 0;
84
197
  border-radius: ${boeRadius.med};
85
198
  background: transparent;
86
- color: var(--boe-token-text-text, #222222);
199
+ color: inherit;
87
200
  font: inherit;
88
- font-size: 13px;
89
- font-weight: 700;
90
- padding: 7px 13px;
91
201
  cursor: pointer;
92
202
  transition: background ${boeMotionDuration.interactive} ${boeMotionEasing.standard}, color ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
93
203
  }
94
204
 
205
+ [part="dismiss"] svg {
206
+ display: block;
207
+ inline-size: 100%;
208
+ block-size: 100%;
209
+ }
210
+
95
211
  [part="dismiss"]:hover:not(:disabled) {
96
212
  background: rgb(0 0 0 / 6%);
97
213
  }
@@ -111,12 +227,16 @@ const toastStyles = `
111
227
  export class Toast extends BaseElement {
112
228
  static tagName = DEFAULT_TAG_NAME;
113
229
  static get observedAttributes() {
114
- return ["message", "open", "tone", "duration"];
230
+ return ["duration", "heading", "message", "mode", "open", "tone"];
115
231
  }
116
232
  openValue = false;
117
233
  timeoutId = null;
118
234
  toastEl;
235
+ iconEl;
236
+ headingEl;
237
+ toneLabelEl;
119
238
  messageEl;
239
+ renderedTone = null;
120
240
  actionSlot;
121
241
  dismissEl;
122
242
  /** Auto-dismiss delay in ms for the declarative `open` path. 0 = sticky. */
@@ -161,7 +281,8 @@ export class Toast extends BaseElement {
161
281
  clearTimeout(this.timeoutId);
162
282
  this.timeoutId = null;
163
283
  }
164
- if (duration > 0) {
284
+ // Sticky wins over any duration, including one passed to show().
285
+ if (duration > 0 && this.mode !== "sticky") {
165
286
  this.timeoutId = setTimeout(() => this.hide(), duration);
166
287
  }
167
288
  }
@@ -171,6 +292,42 @@ export class Toast extends BaseElement {
171
292
  set message(value) {
172
293
  this.setAttribute("message", value);
173
294
  }
295
+ /**
296
+ * Optional bold line above the message — "Upload failed" over "3 of 12 files
297
+ * could not be read". Named to match `box-alert`, which established the
298
+ * heading-plus-message shape in this family. Salesforce calls it `label`.
299
+ */
300
+ get heading() {
301
+ return this.getAttribute("heading") ?? "";
302
+ }
303
+ set heading(value) {
304
+ this.setAttribute("heading", value);
305
+ }
306
+ /**
307
+ * Drop the outline for a softer, fill-only toast.
308
+ *
309
+ * Purely presentational and handled entirely in CSS, so it is deliberately
310
+ * not an observed attribute — there is nothing to re-render. The default
311
+ * stays bordered: that outline tracks box-ui-elements' `.notification` and
312
+ * is pinned by the colour conformance manifest.
313
+ */
314
+ get borderless() {
315
+ return this.hasAttribute("borderless");
316
+ }
317
+ set borderless(value) {
318
+ this.toggleAttribute("borderless", value);
319
+ }
320
+ /**
321
+ * Whether the toast clears itself. `sticky` overrides any `duration`: a
322
+ * caller that has said "this one stays" should not have it taken away by a
323
+ * duration set elsewhere.
324
+ */
325
+ get mode() {
326
+ return resolveToastMode(this.getAttribute("mode"));
327
+ }
328
+ set mode(value) {
329
+ this.setAttribute("mode", value);
330
+ }
174
331
  get tone() {
175
332
  return this.getAttribute("tone") ?? "info";
176
333
  }
@@ -211,12 +368,20 @@ export class Toast extends BaseElement {
211
368
  this.shadowRoot.innerHTML = `
212
369
  <style>${toastStyles}</style>
213
370
  <div part="toast" role="status" aria-live="polite">
214
- <span part="message"></span>
371
+ <span part="icon" aria-hidden="true"></span>
372
+ <div part="content">
373
+ <span part="tone-label" class="sr-only"></span>
374
+ <span part="heading"></span>
375
+ <span part="message"></span>
376
+ </div>
215
377
  <slot name="action" part="action"></slot>
216
- <button type="button" part="dismiss">Dismiss</button>
378
+ <button type="button" part="dismiss" aria-label="Dismiss">${DISMISS_ICON}</button>
217
379
  </div>
218
380
  `;
219
381
  this.toastEl = this.shadowRoot.querySelector('[part="toast"]');
382
+ this.iconEl = this.shadowRoot.querySelector('[part="icon"]');
383
+ this.headingEl = this.shadowRoot.querySelector('[part="heading"]');
384
+ this.toneLabelEl = this.shadowRoot.querySelector('[part="tone-label"]');
220
385
  this.messageEl = this.shadowRoot.querySelector('[part="message"]');
221
386
  this.actionSlot = this.shadowRoot.querySelector('slot[name="action"]');
222
387
  this.dismissEl = this.shadowRoot.querySelector('[part="dismiss"]');
@@ -240,7 +405,18 @@ export class Toast extends BaseElement {
240
405
  if (!visible) {
241
406
  return;
242
407
  }
243
- this.toastEl.dataset.tone = this.tone;
408
+ const tone = this.tone;
409
+ this.toastEl.dataset.tone = tone;
410
+ // Only when it actually changes: this re-parses SVG markup, and update()
411
+ // runs on every attribute write.
412
+ if (tone !== this.renderedTone) {
413
+ this.iconEl.innerHTML = toneIcon(tone);
414
+ this.renderedTone = tone;
415
+ }
416
+ // The glyph is aria-hidden and the fill is colour, so without this the
417
+ // tone reaches a screen reader not at all.
418
+ this.toneLabelEl.textContent = toneAccessibleLabel(tone);
419
+ this.headingEl.textContent = this.heading;
244
420
  this.messageEl.textContent = this.message;
245
421
  }
246
422
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The status vocabulary shared by the feedback surfaces.
3
+ *
4
+ * `box-alert` and `box-toast` both state their tone in words for screen
5
+ * readers, and both must say the *same* words: a reader who hears "In progress"
6
+ * from an alert should not hear "Inprogress" from a toast reporting the same
7
+ * thing. One definition is what keeps them in step.
8
+ */
9
+ /**
10
+ * The tone as a word, for assistive technology.
11
+ *
12
+ * Tone is otherwise carried by fill and glyph, neither of which reaches a
13
+ * screen reader. An unrecognised tone is title-cased rather than dropped —
14
+ * a host that invents a tone should still get something spoken.
15
+ */
16
+ export declare const toneAccessibleLabel: (tone: string) => string;