@unofficialbox/box-open-elements 0.10.0 → 0.12.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 (50) hide show
  1. package/dist/components/feedback/alert.js +2 -22
  2. package/dist/components/feedback/nudge.js +0 -1
  3. package/dist/components/feedback/path.d.ts +84 -0
  4. package/dist/components/feedback/path.js +455 -0
  5. package/dist/components/feedback/skeleton.d.ts +70 -0
  6. package/dist/components/feedback/skeleton.js +231 -1
  7. package/dist/components/feedback/toast.d.ts +37 -0
  8. package/dist/components/feedback/toast.js +182 -17
  9. package/dist/components/feedback/tone.d.ts +16 -0
  10. package/dist/components/feedback/tone.js +31 -0
  11. package/dist/components/layout/grid.d.ts +41 -0
  12. package/dist/components/layout/grid.js +152 -0
  13. package/dist/element-maps.d.ts +7 -3
  14. package/dist/entries/comment-thread.d.ts +1 -0
  15. package/dist/entries/comment-thread.js +1 -0
  16. package/dist/entries/grid.d.ts +1 -0
  17. package/dist/entries/grid.js +1 -0
  18. package/dist/entries/path.d.ts +1 -0
  19. package/dist/entries/path.js +1 -0
  20. package/dist/foundations/layout/grid.d.ts +70 -0
  21. package/dist/foundations/layout/grid.js +84 -0
  22. package/dist/foundations/layout/index.d.ts +1 -0
  23. package/dist/foundations/layout/index.js +3 -0
  24. package/dist/index.d.ts +5 -1
  25. package/dist/index.js +6 -1
  26. package/dist/patterns/audit/audit-log.js +0 -1
  27. package/dist/patterns/comments/comment-thread.d.ts +79 -0
  28. package/dist/patterns/comments/comment-thread.js +561 -0
  29. package/dist/patterns/comments/index.d.ts +2 -0
  30. package/dist/patterns/comments/index.js +5 -0
  31. package/dist/patterns/comments/types.d.ts +39 -0
  32. package/dist/patterns/comments/types.js +8 -0
  33. package/dist/patterns/lineage/lineage-graph.d.ts +7 -0
  34. package/dist/patterns/lineage/lineage-graph.js +67 -6
  35. package/dist/patterns/lineage/provenance-strip.js +106 -15
  36. package/dist/patterns/notifications/notification-inbox.js +2 -18
  37. package/dist/patterns/preview/annotation-thread.d.ts +67 -48
  38. package/dist/patterns/preview/annotation-thread.js +96 -440
  39. package/dist/patterns/timeline/timeline.js +44 -12
  40. package/dist/patterns/versions/graph-arrows.d.ts +22 -0
  41. package/dist/patterns/versions/graph-arrows.js +13 -0
  42. package/dist/patterns/versions/index.d.ts +1 -0
  43. package/dist/patterns/versions/index.js +1 -0
  44. package/dist/patterns/versions/version-graph.d.ts +7 -0
  45. package/dist/patterns/versions/version-graph.js +54 -6
  46. package/package.json +1 -1
  47. package/dist/components/feedback/stage-path.d.ts +0 -44
  48. package/dist/components/feedback/stage-path.js +0 -235
  49. package/dist/entries/stage-path.d.ts +0 -1
  50. package/dist/entries/stage-path.js +0 -1
@@ -1,24 +1,9 @@
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 { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
4
5
  import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
5
6
  const DEFAULT_TAG_NAME = "box-alert";
6
- const toneAccessibleLabel = (tone) => {
7
- switch (tone) {
8
- case "success":
9
- return "Success";
10
- case "error":
11
- return "Error";
12
- case "warning":
13
- return "Warning";
14
- case "inprogress":
15
- return "In progress";
16
- case "info":
17
- return "Info";
18
- default:
19
- return tone.charAt(0).toUpperCase() + tone.slice(1);
20
- }
21
- };
22
7
  const alertStyles = `
23
8
  :host {
24
9
  display: block;
@@ -37,32 +22,27 @@ const alertStyles = `
37
22
  gap: ${boeSpace[3]};
38
23
  margin: ${boeSpace[3]} 0;
39
24
  padding: 14px 10px;
40
- border: 1px solid var(--boe-token-text-text-secondary, #6f6f6f);
41
25
  border-radius: ${boeRadius.large};
42
26
  background: var(--boe-token-surface-surface-secondary, #f4f4f4);
43
27
  color: var(--boe-token-text-text, #222222);
44
- transition: background ${boeMotionDuration.interactive} ${boeMotionEasing.standard}, border-color ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
28
+ transition: background ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
45
29
  }
46
30
 
47
31
  [part="alert"][data-tone="info"] {
48
32
  background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 10%, #fff);
49
- border-color: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 50%, #fff);
50
33
  }
51
34
 
52
35
  [part="alert"][data-tone="success"] {
53
36
  background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 10%, #fff);
54
- border-color: var(--boe-token-surface-status-surface-success, #26c281);
55
37
  }
56
38
 
57
39
  [part="alert"][data-tone="error"] {
58
40
  background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 10%, #fff);
59
- border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 50%, #fff);
60
41
  }
61
42
 
62
43
  [part="alert"][data-tone="warning"],
63
44
  [part="alert"][data-tone="inprogress"] {
64
45
  background: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 10%, #fff);
65
- border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 50%, #fff);
66
46
  }
67
47
 
68
48
  [part="content"] {
@@ -14,7 +14,6 @@ const nudgeStyles = `
14
14
  align-items: flex-start;
15
15
  gap: 0.55rem;
16
16
  padding: 0.55rem 0.7rem;
17
- border: 1px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 24%, var(--boe-token-surface-surface, #ffffff));
18
17
  border-radius: ${boeRadius.large};
19
18
  background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 7%, var(--boe-token-surface-surface, #ffffff));
20
19
  color: var(--boe-token-text-text, #222222);
@@ -0,0 +1,84 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ /** One point in a record's lifecycle. */
3
+ export interface PathStage {
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" | "error";
10
+ /**
11
+ * Shape of the path.
12
+ *
13
+ * `chevron` is the ribbon: every stage is a filled block carrying its own
14
+ * label, and the notches make the sequence read as directed. `base` is the
15
+ * marker rail — dots joined by a connector, with the label beneath — which is
16
+ * the shape Salesforce's progress indicator calls its base type, and the one
17
+ * that survives a long sequence without the labels colliding.
18
+ */
19
+ export type PathVariant = "chevron" | "base";
20
+ /**
21
+ * Narrow an author-supplied variant, falling back to `chevron`.
22
+ *
23
+ * A typo should render the default shape rather than an unstyled row, which is
24
+ * what an unrecognised `data-variant` would produce — every variant rule is
25
+ * scoped to a known value.
26
+ */
27
+ export declare const resolvePathVariant: (value: string | null | undefined) => PathVariant;
28
+ /** Attribute payloads are author input — validate every record. */
29
+ export declare const isPathStageRecord: (value: unknown) => value is PathStage;
30
+ /**
31
+ * State of each stage relative to the current one.
32
+ *
33
+ * `hasError` reports a failure at the stage the record is currently sitting
34
+ * at — the shape Salesforce's progress indicator uses, and the right one here:
35
+ * a lifecycle fails where it stopped, so an error is a property of the current
36
+ * position rather than of an arbitrary stage. Stages behind it stay complete;
37
+ * the work up to the failure did happen.
38
+ */
39
+ export declare const resolveStageStates: (stages: readonly PathStage[], currentId: string, hasError?: boolean) => StageState[];
40
+ /**
41
+ * Horizontal chevron lifecycle tracker for record headers — Draft → In
42
+ * Review → Approved → Executed.
43
+ *
44
+ * Distinct from `box-progress-steps`, which is a vertical setup rail for a
45
+ * task the reader is working through: this states where a *record* sits in
46
+ * its lifecycle, is read-only, and belongs in a header rather than a form.
47
+ *
48
+ * Rendered as an ordered list with the current stage marked `aria-current`,
49
+ * so the sequence and the position are both available without relying on the
50
+ * chevron geometry — which is decoration, and collapses on narrow viewports.
51
+ */
52
+ export declare class Path extends BaseElement {
53
+ static readonly tagName: string;
54
+ static get observedAttributes(): string[];
55
+ private pathEl;
56
+ private stagesRaw;
57
+ private stagesCache;
58
+ get label(): string;
59
+ set label(value: string);
60
+ /**
61
+ * Shape of the path: `chevron` (default) draws a continuous directed ribbon;
62
+ * `rounded` draws separated pills. An unknown value falls back to `chevron`
63
+ * rather than rendering an unstyled row.
64
+ */
65
+ get variant(): PathVariant;
66
+ set variant(value: PathVariant);
67
+ /** Id of the stage the record is at. An unknown id leaves every stage upcoming. */
68
+ get current(): string;
69
+ set current(value: string);
70
+ /**
71
+ * The record failed at the stage it is currently on — approval rejected, a
72
+ * signature declined. Earlier stages stay complete: the failure is where the
73
+ * lifecycle stopped, not a reset of everything behind it.
74
+ */
75
+ get hasError(): boolean;
76
+ set hasError(value: boolean);
77
+ get stages(): PathStage[];
78
+ set stages(value: PathStage[]);
79
+ /** State of each stage, in order. */
80
+ get states(): StageState[];
81
+ protected renderTemplate(): void;
82
+ protected setupListeners(): void;
83
+ protected update(): void;
84
+ }
@@ -0,0 +1,455 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
3
+ const DEFAULT_TAG_NAME = "box-path";
4
+ const escapeHtml = (value) => value
5
+ .replaceAll("&", "&")
6
+ .replaceAll("<", "&lt;")
7
+ .replaceAll(">", "&gt;")
8
+ .replaceAll('"', "&quot;")
9
+ .replaceAll("'", "&#39;");
10
+ const PATH_VARIANTS = new Set(["chevron", "base"]);
11
+ /** Assistive text for each state; colour and glyph are not enough on their own. */
12
+ const STATE_LABELS = {
13
+ complete: "Completed",
14
+ current: "Current stage",
15
+ upcoming: "Not started",
16
+ error: "Error",
17
+ };
18
+ /** Non-colour glyph for the states that carry one. */
19
+ const STATE_MARKERS = {
20
+ complete: "✓",
21
+ error: "!",
22
+ };
23
+ /**
24
+ * Narrow an author-supplied variant, falling back to `chevron`.
25
+ *
26
+ * A typo should render the default shape rather than an unstyled row, which is
27
+ * what an unrecognised `data-variant` would produce — every variant rule is
28
+ * scoped to a known value.
29
+ */
30
+ export const resolvePathVariant = (value) => PATH_VARIANTS.has(value) ? value : "chevron";
31
+ /** Attribute payloads are author input — validate every record. */
32
+ export const isPathStageRecord = (value) => {
33
+ if (typeof value !== "object" || value === null) {
34
+ return false;
35
+ }
36
+ const stage = value;
37
+ return (typeof stage.id === "string" &&
38
+ stage.id.length > 0 &&
39
+ typeof stage.label === "string" &&
40
+ stage.label.length > 0);
41
+ };
42
+ /**
43
+ * State of each stage relative to the current one.
44
+ *
45
+ * `hasError` reports a failure at the stage the record is currently sitting
46
+ * at — the shape Salesforce's progress indicator uses, and the right one here:
47
+ * a lifecycle fails where it stopped, so an error is a property of the current
48
+ * position rather than of an arbitrary stage. Stages behind it stay complete;
49
+ * the work up to the failure did happen.
50
+ */
51
+ export const resolveStageStates = (stages, currentId, hasError = false) => {
52
+ const index = stages.findIndex(stage => stage.id === currentId);
53
+ // An unknown current id means nothing is complete — better than silently
54
+ // marking the whole path done because the host sent a stale value.
55
+ return stages.map((_, position) => index === -1
56
+ ? "upcoming"
57
+ : position < index
58
+ ? "complete"
59
+ : position === index
60
+ ? hasError
61
+ ? "error"
62
+ : "current"
63
+ : "upcoming");
64
+ };
65
+ const elementStyles = `
66
+ :host {
67
+ display: block;
68
+ }
69
+
70
+ [part="path"] {
71
+ display: flex;
72
+ flex-wrap: wrap;
73
+ /* stretch, not start: every stage is the height of the tallest, so a
74
+ stage carrying a description does not stand proud of its
75
+ neighbours. The old rule let each stage size itself and the row
76
+ came out ragged. */
77
+ align-items: stretch;
78
+ margin: 0;
79
+ padding: 0;
80
+ list-style: none;
81
+ }
82
+
83
+ /* Sizing is taken from box-segmented-control, deliberately: both are a
84
+ horizontal row of equal-weight labels, and they should read at the
85
+ same density rather than each inventing a height. */
86
+ [part="stage"] {
87
+ position: relative;
88
+ flex: 1 1 0;
89
+ min-inline-size: 6rem;
90
+ display: flex;
91
+ /* A row, so a stage carrying a description stays one line tall.
92
+ Stacking it doubled the height of every stage in the row —
93
+ align-items: stretch means the tallest stage sets the rest — for a
94
+ detail that belongs beside the label, not under it. The base
95
+ variant, which has the vertical room, stacks instead. */
96
+ flex-direction: row;
97
+ align-items: center;
98
+ justify-content: center;
99
+ text-align: center;
100
+ gap: 0.4rem;
101
+ padding: 0.45em 1em;
102
+ font-size: 0.82rem;
103
+ font-weight: 600;
104
+ line-height: 1.2;
105
+ background: var(--boe-token-surface-surface-hover, #f4f4f4);
106
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
107
+ transition: background ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
108
+ }
109
+
110
+ [part="stage"][data-state="complete"] {
111
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26a27b) 18%, transparent);
112
+ color: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26a27b) 72%, black 28%);
113
+ }
114
+
115
+ [part="stage"][data-state="current"] {
116
+ background: var(--boe-token-surface-surface-brand, #0061d5);
117
+ color: var(--boe-token-text-text-on-brand, #ffffff);
118
+ font-weight: 700;
119
+ }
120
+
121
+ [part="stage"][data-state="error"] {
122
+ background: var(--boe-token-surface-status-surface-error, #ed3757);
123
+ color: var(--boe-token-text-text-on-brand, #ffffff);
124
+ font-weight: 700;
125
+ }
126
+
127
+ /* Assistive-only: the state is otherwise carried by fill and a glyph,
128
+ neither of which reaches a screen reader. aria-current covers the
129
+ current stage but has nothing to say about complete or error. */
130
+ [part="stage-state"] {
131
+ position: absolute;
132
+ inline-size: 1px;
133
+ block-size: 1px;
134
+ padding: 0;
135
+ margin: -1px;
136
+ overflow: hidden;
137
+ clip-path: inset(50%);
138
+ white-space: nowrap;
139
+ border: 0;
140
+ }
141
+
142
+ /* Marker and label together. A row in the ribbon shapes, where the
143
+ check reads as a prefix to the label; a column in the base rail,
144
+ where the marker is the rail itself and the label hangs beneath it.
145
+ One structure, one axis to flip — no per-variant markup. */
146
+ [part="stage-heading"] {
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: center;
150
+ }
151
+
152
+ [part="stage-label"] {
153
+ display: block;
154
+ }
155
+
156
+ [part="stage-description"] {
157
+ display: block;
158
+ font-size: 0.72rem;
159
+ font-weight: 400;
160
+ opacity: 0.85;
161
+ }
162
+
163
+ /* A chevron is too narrow to carry the description: at four stages in a
164
+ 760px header each one gets ~190px, and "In Review · With Morgan Lee"
165
+ wrapped to a second line, which — with align-items: stretch — took
166
+ the whole row from 27.5px to 43.3px. Salesforce's path type shows
167
+ labels only for the same reason. The detail stays in the DOM and is
168
+ still reachable via ::part for a host that has the width for it.
169
+ The base rail, which stacks, shows it by default. */
170
+ [part="path"][data-variant="chevron"] [part="stage-description"] {
171
+ display: none;
172
+ }
173
+
174
+ [part="stage-marker"] {
175
+ margin-inline-end: 0.3rem;
176
+ font-weight: 700;
177
+ }
178
+
179
+ /* Current and upcoming stages carry no glyph. In the ribbon shapes that
180
+ marker would only contribute stray margin; the base variant re-shows
181
+ it, because there the marker *is* the rail. */
182
+ [part="stage-marker"]:empty {
183
+ display: none;
184
+ }
185
+
186
+ /* ── variant: chevron (default) ──────────────────────────────────────
187
+ A notch cut from the following stage plus an arrow on this one, so
188
+ the path reads as a directed sequence rather than a row of pills.
189
+ Drawn with clip-path so no extra elements are needed. The inline
190
+ padding absorbs the arrow so centred text stays optically centred
191
+ rather than drifting into the notch. */
192
+ [part="path"][data-variant="chevron"] [part="stage"] {
193
+ padding-inline: 1.35em;
194
+ }
195
+
196
+ [part="path"][data-variant="chevron"] [part="stage"]:first-child {
197
+ border-start-start-radius: 999px;
198
+ border-end-start-radius: 999px;
199
+ padding-inline-start: 1em;
200
+ }
201
+
202
+ [part="path"][data-variant="chevron"] [part="stage"]:last-child {
203
+ border-start-end-radius: 999px;
204
+ border-end-end-radius: 999px;
205
+ padding-inline-end: 1em;
206
+ }
207
+
208
+ [part="path"][data-variant="chevron"] [part="stage"]:not(:last-child) {
209
+ clip-path: polygon(0 0, calc(100% - 0.7rem) 0, 100% 50%, calc(100% - 0.7rem) 100%, 0 100%, 0.7rem 50%);
210
+ margin-inline-end: -0.55rem;
211
+ }
212
+
213
+ [part="path"][data-variant="chevron"] [part="stage"]:first-child:not(:last-child) {
214
+ clip-path: polygon(0 0, calc(100% - 0.7rem) 0, 100% 50%, calc(100% - 0.7rem) 100%, 0 100%);
215
+ }
216
+
217
+ [part="path"][data-variant="chevron"] [part="stage"]:last-child:not(:first-child) {
218
+ clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 0.7rem 50%);
219
+ }
220
+
221
+ /* ── variant: base ───────────────────────────────────────────────────
222
+ A marker rail: a dot per stage on a connector line, label beneath.
223
+ Unlike the ribbon shapes this keeps its labels out of the fill, so a
224
+ long sequence stays readable where chevrons would crush the text.
225
+
226
+ The markers all occupy the same 1.25rem box whatever their state —
227
+ upcoming ones are shrunk with a transform, which is purely visual —
228
+ so the connector can sit at one fixed offset instead of chasing a
229
+ per-state centre. */
230
+ [part="path"][data-variant="base"] [part="stage"] {
231
+ flex-direction: column;
232
+ background: none;
233
+ color: var(--boe-token-text-text, #1a1a1a);
234
+ /* Top-aligned, not centred: the connector is drawn at one fixed
235
+ offset from the stage's top edge, so every marker has to sit at
236
+ that same offset. Centring moved the current stage's marker up by
237
+ the height of its description and left the line joining nothing. */
238
+ justify-content: flex-start;
239
+ padding: 0 0.5rem;
240
+ gap: 0.4rem;
241
+ font-weight: 500;
242
+ }
243
+
244
+ /* The segment joining this stage back to the previous one. Painted from
245
+ the previous marker's centre (-50%) to this one's, which lands
246
+ correctly because every stage is an equal flex share. */
247
+ [part="path"][data-variant="base"] [part="stage"]:not(:first-child)::before {
248
+ content: "";
249
+ position: absolute;
250
+ inset-block-start: calc(0.625rem - 1px);
251
+ inset-inline-end: 50%;
252
+ inline-size: 100%;
253
+ block-size: 2px;
254
+ background: var(--boe-token-stroke-stroke, #e8e8e8);
255
+ }
256
+
257
+ /* Reached stages pull the line in behind them: the connector is blue up
258
+ to wherever the record got to, grey from there on. */
259
+ /* The failed stage counts as reached, so its incoming segment stays
260
+ brand-coloured: the record did travel that far, and only the marker
261
+ reports the failure. */
262
+ [part="path"][data-variant="base"] [part="stage"]:not([data-state="upcoming"])::before {
263
+ background: var(--boe-token-surface-surface-brand, #0061d5);
264
+ }
265
+
266
+ [part="path"][data-variant="base"] [part="stage-heading"] {
267
+ flex-direction: column;
268
+ gap: 0.4rem;
269
+ inline-size: 100%;
270
+ }
271
+
272
+ [part="path"][data-variant="base"] [part="stage-marker"] {
273
+ position: relative;
274
+ z-index: 1;
275
+ display: flex;
276
+ align-items: center;
277
+ justify-content: center;
278
+ inline-size: 1.25rem;
279
+ block-size: 1.25rem;
280
+ margin-inline-end: 0;
281
+ border-radius: 999px;
282
+ font-size: 0.68rem;
283
+ line-height: 1;
284
+ background: var(--boe-token-surface-surface-brand, #0061d5);
285
+ color: var(--boe-token-text-text-on-brand, #ffffff);
286
+ transition: transform ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
287
+ }
288
+
289
+ [part="path"][data-variant="base"] [part="stage"][data-state="upcoming"] [part="stage-marker"] {
290
+ background: var(--boe-token-stroke-stroke, #e8e8e8);
291
+ transform: scale(0.45);
292
+ }
293
+
294
+ [part="path"][data-variant="base"] [part="stage"][data-state="error"] [part="stage-marker"] {
295
+ background: var(--boe-token-surface-status-surface-error, #ed3757);
296
+ }
297
+
298
+ [part="path"][data-variant="base"] [part="stage"][data-state="current"] [part="stage-label"],
299
+ [part="path"][data-variant="base"] [part="stage"][data-state="error"] [part="stage-label"] {
300
+ font-weight: 700;
301
+ }
302
+
303
+ @media (prefers-reduced-motion: reduce) {
304
+ [part="stage"],
305
+ [part="path"][data-variant="base"] [part="stage-marker"] {
306
+ transition: none;
307
+ }
308
+ }
309
+
310
+ @media (max-width: 34rem) {
311
+ /* Chevrons collapse below the width where they stop being legible;
312
+ the states still read from tone and the completed marker. */
313
+ [part="path"][data-variant="chevron"] [part="stage"],
314
+ [part="path"][data-variant="chevron"] [part="stage"]:first-child:not(:last-child),
315
+ [part="path"][data-variant="chevron"] [part="stage"]:last-child:not(:first-child) {
316
+ clip-path: none;
317
+ margin-inline-end: 0;
318
+ padding-inline: 1em;
319
+ border-radius: 0;
320
+ }
321
+ }
322
+ `;
323
+ /**
324
+ * Horizontal chevron lifecycle tracker for record headers — Draft → In
325
+ * Review → Approved → Executed.
326
+ *
327
+ * Distinct from `box-progress-steps`, which is a vertical setup rail for a
328
+ * task the reader is working through: this states where a *record* sits in
329
+ * its lifecycle, is read-only, and belongs in a header rather than a form.
330
+ *
331
+ * Rendered as an ordered list with the current stage marked `aria-current`,
332
+ * so the sequence and the position are both available without relying on the
333
+ * chevron geometry — which is decoration, and collapses on narrow viewports.
334
+ */
335
+ export class Path extends BaseElement {
336
+ static tagName = DEFAULT_TAG_NAME;
337
+ static get observedAttributes() {
338
+ return ["current", "has-error", "label", "stages", "variant"];
339
+ }
340
+ pathEl;
341
+ stagesRaw = null;
342
+ stagesCache = [];
343
+ get label() {
344
+ return this.getAttribute("label") ?? "Lifecycle";
345
+ }
346
+ set label(value) {
347
+ this.setAttribute("label", value);
348
+ }
349
+ /**
350
+ * Shape of the path: `chevron` (default) draws a continuous directed ribbon;
351
+ * `rounded` draws separated pills. An unknown value falls back to `chevron`
352
+ * rather than rendering an unstyled row.
353
+ */
354
+ get variant() {
355
+ return resolvePathVariant(this.getAttribute("variant"));
356
+ }
357
+ set variant(value) {
358
+ this.setAttribute("variant", value);
359
+ }
360
+ /** Id of the stage the record is at. An unknown id leaves every stage upcoming. */
361
+ get current() {
362
+ return this.getAttribute("current") ?? "";
363
+ }
364
+ set current(value) {
365
+ this.setAttribute("current", value);
366
+ }
367
+ /**
368
+ * The record failed at the stage it is currently on — approval rejected, a
369
+ * signature declined. Earlier stages stay complete: the failure is where the
370
+ * lifecycle stopped, not a reset of everything behind it.
371
+ */
372
+ get hasError() {
373
+ return this.hasAttribute("has-error");
374
+ }
375
+ set hasError(value) {
376
+ this.toggleAttribute("has-error", value);
377
+ }
378
+ get stages() {
379
+ const raw = this.getAttribute("stages");
380
+ if (!raw) {
381
+ return [];
382
+ }
383
+ if (raw !== this.stagesRaw) {
384
+ this.stagesRaw = raw;
385
+ try {
386
+ const parsed = JSON.parse(raw);
387
+ this.stagesCache =
388
+ Array.isArray(parsed) && parsed.every(isPathStageRecord)
389
+ ? parsed
390
+ : [];
391
+ }
392
+ catch {
393
+ this.stagesCache = [];
394
+ }
395
+ }
396
+ return [...this.stagesCache];
397
+ }
398
+ set stages(value) {
399
+ if (value.length) {
400
+ this.setAttribute("stages", JSON.stringify(value));
401
+ return;
402
+ }
403
+ this.removeAttribute("stages");
404
+ }
405
+ /** State of each stage, in order. */
406
+ get states() {
407
+ return resolveStageStates(this.stages, this.current, this.hasError);
408
+ }
409
+ renderTemplate() {
410
+ if (!this.shadowRoot) {
411
+ return;
412
+ }
413
+ this.shadowRoot.innerHTML = `<style>${elementStyles}</style><ol part="path"></ol>`;
414
+ this.pathEl = this.shadowRoot.querySelector('[part="path"]');
415
+ }
416
+ setupListeners() {
417
+ // Read-only: a record's lifecycle position is not something a header edits.
418
+ }
419
+ update() {
420
+ if (!this.pathEl) {
421
+ return;
422
+ }
423
+ const stages = this.stages;
424
+ const variant = this.variant;
425
+ const states = resolveStageStates(stages, this.current, this.hasError);
426
+ this.pathEl.setAttribute("aria-label", this.label);
427
+ this.pathEl.dataset.variant = variant;
428
+ this.pathEl.innerHTML = stages
429
+ .map((stage, index) => {
430
+ const state = states[index];
431
+ // A second, non-colour signal for the states that have one. Always
432
+ // emitted: the base variant styles this into the rail's marker, and an
433
+ // empty one is hidden by CSS rather than by a branch here.
434
+ const marker = `<span part="stage-marker" aria-hidden="true">${STATE_MARKERS[state] ?? ""}</span>`;
435
+ const isCurrentPosition = state === "current" || state === "error";
436
+ return `
437
+ <li
438
+ part="stage"
439
+ data-stage-id="${escapeHtml(stage.id)}"
440
+ data-state="${state}"
441
+ ${isCurrentPosition ? 'aria-current="step"' : ""}
442
+ ${state === "error" ? 'aria-invalid="true"' : ""}
443
+ >
444
+ <span part="stage-state">${STATE_LABELS[state]}</span>
445
+ <span part="stage-heading">${marker}<span part="stage-label">${escapeHtml(stage.label)}</span></span>
446
+ ${isCurrentPosition && stage.description
447
+ ? `<span part="stage-description">${escapeHtml(stage.description)}</span>`
448
+ : ""}
449
+ </li>
450
+ `;
451
+ })
452
+ .join("");
453
+ }
454
+ }
455
+ Path.register();