@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
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Which ends of a graph edge carry an arrowhead.
3
+ *
4
+ * Shared by `box-version-graph` and `box-lineage-graph`, which draw the same
5
+ * edge geometry from the same layout and should not disagree about what an
6
+ * arrow means. Lives here rather than in either component because the lineage
7
+ * graph already depends on this directory for `computeVersionGraphLayout`, and
8
+ * the reverse dependency would be a cycle.
9
+ */
10
+ export type GraphArrows = "none" | "start" | "end" | "both";
11
+ /**
12
+ * Narrow an author-supplied value, falling back to `end`.
13
+ *
14
+ * `end` is the default because these are directed graphs: the layout builds
15
+ * every edge as parent → child, so a single head on the child is the reading
16
+ * that matches the data. A typo should not silently turn the direction off.
17
+ */
18
+ export declare const resolveGraphArrows: (value: string | null | undefined) => GraphArrows;
19
+ /** Whether the head at the child end is drawn. */
20
+ export declare const hasEndArrow: (arrows: GraphArrows) => boolean;
21
+ /** Whether the head at the parent end is drawn. */
22
+ export declare const hasStartArrow: (arrows: GraphArrows) => boolean;
@@ -0,0 +1,13 @@
1
+ const GRAPH_ARROWS = new Set(["none", "start", "end", "both"]);
2
+ /**
3
+ * Narrow an author-supplied value, falling back to `end`.
4
+ *
5
+ * `end` is the default because these are directed graphs: the layout builds
6
+ * every edge as parent → child, so a single head on the child is the reading
7
+ * that matches the data. A typo should not silently turn the direction off.
8
+ */
9
+ export const resolveGraphArrows = (value) => GRAPH_ARROWS.has(value) ? value : "end";
10
+ /** Whether the head at the child end is drawn. */
11
+ export const hasEndArrow = (arrows) => arrows === "end" || arrows === "both";
12
+ /** Whether the head at the parent end is drawn. */
13
+ export const hasStartArrow = (arrows) => arrows === "start" || arrows === "both";
@@ -1,4 +1,5 @@
1
1
  export * from "./types.js";
2
2
  export * from "./graph-layout.js";
3
+ export * from "./graph-arrows.js";
3
4
  export * from "./version-list.js";
4
5
  export * from "./version-graph.js";
@@ -2,6 +2,7 @@
2
2
  export * from "./types.js";
3
3
  // Pure git-network layout engine (shared with future lineage graphs)
4
4
  export * from "./graph-layout.js";
5
+ export * from "./graph-arrows.js";
5
6
  // Accessible core contract
6
7
  export * from "./version-list.js";
7
8
  // Git-style visualization layer
@@ -1,3 +1,4 @@
1
+ import type { GraphArrows } from "./graph-arrows.js";
1
2
  import type { VersionNode } from "./types.js";
2
3
  import { BaseElement } from "../../core/index.js";
3
4
  /**
@@ -12,6 +13,12 @@ import { BaseElement } from "../../core/index.js";
12
13
  export declare class VersionGraph extends BaseElement {
13
14
  static readonly tagName: string;
14
15
  static get observedAttributes(): string[];
16
+ /**
17
+ * Which ends of each edge carry an arrowhead. Defaults to `end`, the head on
18
+ * the child, because the layout builds edges parent → child.
19
+ */
20
+ get arrows(): GraphArrows;
21
+ set arrows(value: GraphArrows);
15
22
  private versionsValue;
16
23
  private compareIds;
17
24
  private rovingId;
@@ -1,4 +1,5 @@
1
1
  import { computeVersionGraphLayout, orderVersionsForDisplay } from "./graph-layout.js";
2
+ import { hasEndArrow, hasStartArrow, resolveGraphArrows } from "./graph-arrows.js";
2
3
  import { escapeHtml, STATUS_LABELS } from "./shared.js";
3
4
  import { isVersionNodeRecord, resolveVersionKind, resolveVersionStatus } from "./types.js";
4
5
  import { formatItemDate } from "../content-explorer/adapters/item-summary.js";
@@ -8,6 +9,14 @@ const DEFAULT_TAG_NAME = "box-version-graph";
8
9
  const LANE_WIDTH = 26;
9
10
  const ROW_HEIGHT = 46;
10
11
  const EDGE_PAD = 8;
12
+ /**
13
+ * How far short of a node's centre an edge stops.
14
+ *
15
+ * The arrowhead marks the path's end vertex, so an edge running to the centre
16
+ * would bury its head under the node disc. The largest node here is ~1.05rem
17
+ * across, so backing off 9px leaves the head just clear of the biggest one.
18
+ */
19
+ const NODE_EDGE_OFFSET = 9;
11
20
  const elementStyles = `
12
21
  [hidden] {
13
22
  display: none !important;
@@ -48,6 +57,13 @@ const elementStyles = `
48
57
  pointer-events: none;
49
58
  }
50
59
 
60
+ /* The arrowhead. Same colour as the stroke it terminates, stated
61
+ rather than inherited: context-stroke is not supported everywhere. */
62
+ [part="edge-arrow"] {
63
+ fill: color-mix(in srgb, var(--boe-token-text-text-secondary, #6f6f6f) 55%, transparent);
64
+ stroke: none;
65
+ }
66
+
51
67
  [part="edge"] {
52
68
  fill: none;
53
69
  stroke: color-mix(in srgb, var(--boe-token-text-text-secondary, #6f6f6f) 55%, transparent);
@@ -187,7 +203,17 @@ const elementStyles = `
187
203
  export class VersionGraph extends BaseElement {
188
204
  static tagName = DEFAULT_TAG_NAME;
189
205
  static get observedAttributes() {
190
- return ["heading", "versions"];
206
+ return ["arrows", "heading", "versions"];
207
+ }
208
+ /**
209
+ * Which ends of each edge carry an arrowhead. Defaults to `end`, the head on
210
+ * the child, because the layout builds edges parent → child.
211
+ */
212
+ get arrows() {
213
+ return resolveGraphArrows(this.getAttribute("arrows"));
214
+ }
215
+ set arrows(value) {
216
+ this.setAttribute("arrows", value);
191
217
  }
192
218
  versionsValue = [];
193
219
  compareIds = [];
@@ -359,16 +385,38 @@ export class VersionGraph extends BaseElement {
359
385
  const graphHeight = layout.rowCount * ROW_HEIGHT;
360
386
  const centerX = (lane) => EDGE_PAD + lane * LANE_WIDTH + LANE_WIDTH / 2;
361
387
  const centerY = (row) => row * ROW_HEIGHT + ROW_HEIGHT / 2;
388
+ const arrows = this.arrows;
389
+ const startMarker = hasStartArrow(arrows) ? ` marker-start="url(#boe-graph-arrow)"` : "";
390
+ const endMarker = hasEndArrow(arrows) ? ` marker-end="url(#boe-graph-arrow)"` : "";
362
391
  const edgePaths = layout.edges
363
392
  .map(edge => {
364
393
  const x1 = centerX(edge.fromLane);
365
394
  const y1 = centerY(edge.fromRow);
366
395
  const x2 = centerX(edge.toLane);
367
396
  const y2 = centerY(edge.toRow);
368
- const d = edge.fromLane === edge.toLane
369
- ? `M ${x1} ${y1} L ${x2} ${y2}`
370
- : `M ${x1} ${y1} C ${x1} ${y1 - ROW_HEIGHT / 2}, ${x2} ${y2 + ROW_HEIGHT / 2}, ${x2} ${y2}`;
371
- return `<path part="edge" data-from="${escapeHtml(edge.fromId)}" data-to="${escapeHtml(edge.toId)}" d="${d}"></path>`;
397
+ // One curve rule for every edge, rather than a straight line for
398
+ // same-lane edges and a cubic for lane changes: two rules made the
399
+ // graph read as two different diagrams. A cubic whose ends share an x
400
+ // degenerates to a straight vertical on its own, so same-lane edges
401
+ // still render straight without being a special case.
402
+ //
403
+ // Control points are pulled *along* the direction of travel — half the
404
+ // vertical distance each — so the bend always leaves the parent
405
+ // downward and enters the child from above. The previous version
406
+ // offset them by a fixed half-row in the wrong direction (up from the
407
+ // start, down from the end on a downward edge), so an edge spanning
408
+ // one row bowed backwards into an S while a longer one looked almost
409
+ // straight.
410
+ // Stop at each node's edge rather than its centre. A marker sits on
411
+ // the path's end vertex, so a vertex inside the node disc buries it —
412
+ // and an edge that starts and ends at the rim reads as connecting two
413
+ // nodes rather than as passing through them, which is what React Flow
414
+ // gets from anchoring edges to handles.
415
+ const yStart = y1 + Math.sign(y2 - y1) * NODE_EDGE_OFFSET;
416
+ const yEnd = y2 + Math.sign(y1 - y2) * NODE_EDGE_OFFSET;
417
+ const dy = (yEnd - yStart) / 2;
418
+ const d = `M ${x1} ${yStart} C ${x1} ${yStart + dy}, ${x2} ${yEnd - dy}, ${x2} ${yEnd}`;
419
+ return `<path part="edge" data-from="${escapeHtml(edge.fromId)}" data-to="${escapeHtml(edge.toId)}" d="${d}"${startMarker}${endMarker}></path>`;
372
420
  })
373
421
  .join("");
374
422
  const rovingId = this.rovingId && byId.has(this.rovingId)
@@ -416,7 +464,7 @@ export class VersionGraph extends BaseElement {
416
464
  ${layout.placements.length > 0
417
465
  ? `
418
466
  <div part="graph">
419
- <svg part="edges" width="${String(graphWidth)}" height="${String(graphHeight)}" viewBox="0 0 ${String(graphWidth)} ${String(graphHeight)}" aria-hidden="true">${edgePaths}</svg>
467
+ <svg part="edges" width="${String(graphWidth)}" height="${String(graphHeight)}" viewBox="0 0 ${String(graphWidth)} ${String(graphHeight)}" aria-hidden="true"><defs><marker id="boe-graph-arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="4" markerHeight="4" orient="auto-start-reverse"><path part="edge-arrow" d="M 0 0 L 8 4 L 0 8 z"></path></marker></defs>${edgePaths}</svg>
420
468
  <ol part="rows" role="list">${rows}</ol>
421
469
  </div>
422
470
  `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unofficialbox/box-open-elements",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
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",
@@ -1,44 +0,0 @@
1
- import { BaseElement } from "../../core/index.js";
2
- /** One point in a record's lifecycle. */
3
- export interface StagePathStage {
4
- id: string;
5
- label: string;
6
- /** Optional detail shown under the label on the current stage. */
7
- description?: string;
8
- }
9
- export type StageState = "complete" | "current" | "upcoming";
10
- /** Attribute payloads are author input — validate every record. */
11
- export declare const isStagePathStageRecord: (value: unknown) => value is StagePathStage;
12
- /** State of each stage relative to the current one. */
13
- export declare const resolveStageStates: (stages: readonly StagePathStage[], currentId: string) => StageState[];
14
- /**
15
- * Horizontal chevron lifecycle tracker for record headers — Draft → In
16
- * Review → Approved → Executed.
17
- *
18
- * Distinct from `box-progress-steps`, which is a vertical setup rail for a
19
- * task the reader is working through: this states where a *record* sits in
20
- * its lifecycle, is read-only, and belongs in a header rather than a form.
21
- *
22
- * Rendered as an ordered list with the current stage marked `aria-current`,
23
- * so the sequence and the position are both available without relying on the
24
- * chevron geometry — which is decoration, and collapses on narrow viewports.
25
- */
26
- export declare class StagePath extends BaseElement {
27
- static readonly tagName: string;
28
- static get observedAttributes(): string[];
29
- private pathEl;
30
- private stagesRaw;
31
- private stagesCache;
32
- get label(): string;
33
- set label(value: string);
34
- /** Id of the stage the record is at. An unknown id leaves every stage upcoming. */
35
- get current(): string;
36
- set current(value: string);
37
- get stages(): StagePathStage[];
38
- set stages(value: StagePathStage[]);
39
- /** State of each stage, in order. */
40
- get states(): StageState[];
41
- protected renderTemplate(): void;
42
- protected setupListeners(): void;
43
- protected update(): void;
44
- }
@@ -1,235 +0,0 @@
1
- import { BaseElement } from "../../core/index.js";
2
- import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
3
- const DEFAULT_TAG_NAME = "box-stage-path";
4
- const escapeHtml = (value) => value
5
- .replaceAll("&", "&amp;")
6
- .replaceAll("<", "&lt;")
7
- .replaceAll(">", "&gt;")
8
- .replaceAll('"', "&quot;")
9
- .replaceAll("'", "&#39;");
10
- /** Attribute payloads are author input — validate every record. */
11
- export const isStagePathStageRecord = (value) => {
12
- if (typeof value !== "object" || value === null) {
13
- return false;
14
- }
15
- const stage = value;
16
- return (typeof stage.id === "string" &&
17
- stage.id.length > 0 &&
18
- typeof stage.label === "string" &&
19
- stage.label.length > 0);
20
- };
21
- /** State of each stage relative to the current one. */
22
- export const resolveStageStates = (stages, currentId) => {
23
- const index = stages.findIndex(stage => stage.id === currentId);
24
- // An unknown current id means nothing is complete — better than silently
25
- // marking the whole path done because the host sent a stale value.
26
- return stages.map((_, position) => index === -1
27
- ? "upcoming"
28
- : position < index
29
- ? "complete"
30
- : position === index
31
- ? "current"
32
- : "upcoming");
33
- };
34
- const elementStyles = `
35
- :host {
36
- display: block;
37
- }
38
-
39
- [part="path"] {
40
- display: flex;
41
- flex-wrap: wrap;
42
- margin: 0;
43
- padding: 0;
44
- list-style: none;
45
- }
46
-
47
- [part="stage"] {
48
- position: relative;
49
- flex: 1 1 0;
50
- min-inline-size: 6rem;
51
- padding: 0.45rem 0.7rem 0.45rem 1.35rem;
52
- background: var(--boe-token-surface-surface-hover, #f4f4f4);
53
- color: var(--boe-token-text-text-secondary, #6f6f6f);
54
- font-size: 0.82rem;
55
- transition: background ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
56
- }
57
-
58
- [part="stage"]:first-child {
59
- padding-inline-start: 0.8rem;
60
- border-start-start-radius: 999px;
61
- border-end-start-radius: 999px;
62
- }
63
-
64
- [part="stage"]:last-child {
65
- border-start-end-radius: 999px;
66
- border-end-end-radius: 999px;
67
- }
68
-
69
- /* The chevron: a notch cut from the following stage plus an arrow on
70
- this one, so the path reads as a directed sequence rather than a row
71
- of pills. Drawn with clip-path so no extra elements are needed. */
72
- [part="stage"]:not(:last-child) {
73
- clip-path: polygon(0 0, calc(100% - 0.7rem) 0, 100% 50%, calc(100% - 0.7rem) 100%, 0 100%, 0.7rem 50%);
74
- margin-inline-end: -0.55rem;
75
- }
76
-
77
- [part="stage"]:first-child:not(:last-child) {
78
- clip-path: polygon(0 0, calc(100% - 0.7rem) 0, 100% 50%, calc(100% - 0.7rem) 100%, 0 100%);
79
- }
80
-
81
- [part="stage"]:last-child:not(:first-child) {
82
- clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 0.7rem 50%);
83
- }
84
-
85
- [part="stage"][data-state="complete"] {
86
- background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26a27b) 18%, transparent);
87
- color: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26a27b) 72%, black 28%);
88
- }
89
-
90
- [part="stage"][data-state="current"] {
91
- background: var(--boe-token-surface-surface-brand, #0061d5);
92
- color: var(--boe-token-text-text-on-brand, #ffffff);
93
- font-weight: 700;
94
- }
95
-
96
- [part="stage-label"] {
97
- display: block;
98
- }
99
-
100
- [part="stage-description"] {
101
- display: block;
102
- margin-block-start: 0.1rem;
103
- font-size: 0.72rem;
104
- font-weight: 400;
105
- opacity: 0.85;
106
- }
107
-
108
- [part="stage-marker"] {
109
- margin-inline-end: 0.3rem;
110
- font-weight: 700;
111
- }
112
-
113
- @media (prefers-reduced-motion: reduce) {
114
- [part="stage"] {
115
- transition: none;
116
- }
117
- }
118
-
119
- @media (max-width: 34rem) {
120
- /* Chevrons collapse below the width where they stop being legible;
121
- the states still read from tone and the completed marker. */
122
- [part="stage"],
123
- [part="stage"]:first-child:not(:last-child),
124
- [part="stage"]:last-child:not(:first-child) {
125
- clip-path: none;
126
- margin-inline-end: 0;
127
- padding-inline-start: 0.8rem;
128
- border-radius: 0;
129
- }
130
- }
131
- `;
132
- /**
133
- * Horizontal chevron lifecycle tracker for record headers — Draft → In
134
- * Review → Approved → Executed.
135
- *
136
- * Distinct from `box-progress-steps`, which is a vertical setup rail for a
137
- * task the reader is working through: this states where a *record* sits in
138
- * its lifecycle, is read-only, and belongs in a header rather than a form.
139
- *
140
- * Rendered as an ordered list with the current stage marked `aria-current`,
141
- * so the sequence and the position are both available without relying on the
142
- * chevron geometry — which is decoration, and collapses on narrow viewports.
143
- */
144
- export class StagePath extends BaseElement {
145
- static tagName = DEFAULT_TAG_NAME;
146
- static get observedAttributes() {
147
- return ["current", "label", "stages"];
148
- }
149
- pathEl;
150
- stagesRaw = null;
151
- stagesCache = [];
152
- get label() {
153
- return this.getAttribute("label") ?? "Lifecycle";
154
- }
155
- set label(value) {
156
- this.setAttribute("label", value);
157
- }
158
- /** Id of the stage the record is at. An unknown id leaves every stage upcoming. */
159
- get current() {
160
- return this.getAttribute("current") ?? "";
161
- }
162
- set current(value) {
163
- this.setAttribute("current", value);
164
- }
165
- get stages() {
166
- const raw = this.getAttribute("stages");
167
- if (!raw) {
168
- return [];
169
- }
170
- if (raw !== this.stagesRaw) {
171
- this.stagesRaw = raw;
172
- try {
173
- const parsed = JSON.parse(raw);
174
- this.stagesCache =
175
- Array.isArray(parsed) && parsed.every(isStagePathStageRecord)
176
- ? parsed
177
- : [];
178
- }
179
- catch {
180
- this.stagesCache = [];
181
- }
182
- }
183
- return [...this.stagesCache];
184
- }
185
- set stages(value) {
186
- if (value.length) {
187
- this.setAttribute("stages", JSON.stringify(value));
188
- return;
189
- }
190
- this.removeAttribute("stages");
191
- }
192
- /** State of each stage, in order. */
193
- get states() {
194
- return resolveStageStates(this.stages, this.current);
195
- }
196
- renderTemplate() {
197
- if (!this.shadowRoot) {
198
- return;
199
- }
200
- this.shadowRoot.innerHTML = `<style>${elementStyles}</style><ol part="path"></ol>`;
201
- this.pathEl = this.shadowRoot.querySelector('[part="path"]');
202
- }
203
- setupListeners() {
204
- // Read-only: a record's lifecycle position is not something a header edits.
205
- }
206
- update() {
207
- if (!this.pathEl) {
208
- return;
209
- }
210
- const stages = this.stages;
211
- const states = resolveStageStates(stages, this.current);
212
- this.pathEl.setAttribute("aria-label", this.label);
213
- this.pathEl.innerHTML = stages
214
- .map((stage, index) => {
215
- const state = states[index];
216
- // The completed marker is a second, non-colour signal for "done".
217
- const marker = state === "complete" ? `<span part="stage-marker" aria-hidden="true">✓</span>` : "";
218
- return `
219
- <li
220
- part="stage"
221
- data-stage-id="${escapeHtml(stage.id)}"
222
- data-state="${state}"
223
- ${state === "current" ? 'aria-current="step"' : ""}
224
- >
225
- <span part="stage-label">${marker}${escapeHtml(stage.label)}</span>
226
- ${state === "current" && stage.description
227
- ? `<span part="stage-description">${escapeHtml(stage.description)}</span>`
228
- : ""}
229
- </li>
230
- `;
231
- })
232
- .join("");
233
- }
234
- }
235
- StagePath.register();
@@ -1 +0,0 @@
1
- export * from "../components/feedback/stage-path.js";
@@ -1 +0,0 @@
1
- export * from "../components/feedback/stage-path.js";