@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,483 +1,139 @@
1
- import { BaseElement } from "../../core/index.js";
2
- import { boeBrandInteractiveStyles, boeFocusRingShadow, boeNeutralInteractiveStyles, } from "../../foundations/tokens/index.js";
3
- import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
4
1
  import { boePanel } from "../../foundations/geometry/index.js";
2
+ import { CommentThread } from "../comments/index.js";
5
3
  const DEFAULT_TAG_NAME = "box-annotation-thread";
6
- const escapeHtml = (value) => value
7
- .replaceAll("&", "&")
8
- .replaceAll("<", "&lt;")
9
- .replaceAll(">", "&gt;")
10
- .replaceAll('"', "&quot;")
11
- .replaceAll("'", "&#39;");
12
- const elementStyles = `
13
- /* Author display rules on parts would otherwise defeat the UA's
14
- [hidden] rule — state toggling relies on the hidden attribute. */
15
- [hidden] {
16
- display: none !important;
17
- }
18
-
19
- :host {
20
- display: block;
21
- color: inherit;
22
- font: inherit;
23
- }
24
-
25
- [part="thread"] {
26
- display: grid;
27
- gap: ${boePanel.gap};
28
- padding: ${boePanel.padding};
29
- border: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 82%, transparent);
30
- border-radius: ${boePanel.radius};
31
- background: ${boePanel.background};
32
- }
33
-
34
- [part="header"] {
35
- display: grid;
36
- gap: 0.42rem;
37
- }
38
-
39
- [part="title"] {
40
- margin: 0;
41
- font: inherit;
42
- font-size: 1.1rem;
43
- font-weight: 700;
44
- color: var(--boe-token-text-text, #1f1e1b);
45
- }
46
-
47
- [part="message"] {
48
- color: var(--boe-token-text-text-secondary, #6f6f6f);
49
- line-height: 1.5;
50
- }
51
-
52
- [part="entries"] {
53
- display: grid;
54
- gap: 0.6rem;
55
- margin: 0;
56
- padding: 0;
57
- list-style: none;
58
- }
59
-
60
- [part="entry-item"] {
61
- display: block;
62
- }
63
-
64
- [part="entry"] {
65
- appearance: none;
4
+ const anchorStyles = `
5
+ [part="anchor"] {
66
6
  display: grid;
67
- inline-size: 100%;
68
- grid-template-columns: auto 1fr;
69
- gap: ${boePanel.gap};
70
- align-items: start;
71
- padding: 0.55rem;
72
- border: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 48%, transparent);
7
+ gap: 0.3rem;
8
+ padding: 0.45rem 0.55rem;
9
+ border-inline-start: 2px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 45%, transparent);
73
10
  border-radius: ${boePanel.radius};
74
- background: color-mix(in srgb, var(--boe-token-surface-surface, #ffffff) 78%, transparent);
75
- color: inherit;
76
- font: inherit;
77
- text-align: left;
78
- cursor: pointer;
79
- transition:
80
- transform ${boeMotionDuration.fast} ${boeMotionEasing.standard},
81
- border-color ${boeMotionDuration.fast} ${boeMotionEasing.standard},
82
- box-shadow ${boeMotionDuration.fast} ${boeMotionEasing.standard};
83
- }
84
-
85
- [part="entry-avatar"] {
86
- display: inline-grid;
87
- place-items: center;
88
- inline-size: 2rem;
89
- block-size: 2rem;
90
- border-radius: 999px;
91
- background: var(--boe-token-surface-item-surface-selected, #f2f7fd);
92
- color: var(--boe-token-surface-surface-brand, #0061d5);
93
- font-size: 0.72rem;
94
- font-weight: 700;
95
- letter-spacing: 0.06em;
96
- text-transform: uppercase;
97
- }
98
-
99
- [part="entry-copy"] {
100
- display: grid;
101
- gap: 0.24rem;
102
- }
103
-
104
- [part="entry-topline"] {
105
- display: flex;
106
- flex-wrap: wrap;
107
- align-items: center;
108
- gap: 0.35rem;
109
- }
110
-
111
- [part="entry-author"] {
112
- font-weight: 700;
113
- color: var(--boe-token-text-text, #1f1e1b);
114
- }
115
-
116
- [part="entry-tool"],
117
- [part="entry-status"] {
118
- display: inline-flex;
119
- padding: 0.18rem 0.42rem;
120
- border-radius: 999px;
121
11
  background: var(--boe-token-surface-item-surface-selected, #f2f7fd);
122
- color: var(--boe-token-text-text-secondary, #6f6f6f);
123
- font-size: 0.75rem;
124
- }
125
-
126
- [part="entry-body"] {
127
- line-height: 1.55;
128
- color: var(--boe-token-text-text, #1f1e1b);
129
- }
130
-
131
- [part="entry-time"] {
132
- color: var(--boe-token-text-text-secondary, #6f6f6f);
133
- font-size: 0.78rem;
134
- }
135
-
136
- [part="actions"] {
137
- display: flex;
138
- flex-wrap: wrap;
139
- gap: 0.5rem;
140
12
  }
141
13
 
142
- [part="action"] {
143
- appearance: none;
144
- min-height: 2rem;
145
- padding: 0.4rem 0.7rem;
146
- border: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 60%, transparent);
147
- border-radius: 999px;
148
- background: var(--boe-token-surface-surface, #ffffff);
149
- color: inherit;
150
- font: inherit;
151
- cursor: pointer;
152
- }
153
-
154
- [part="empty"] {
155
- padding: ${boePanel.padding};
156
- border-radius: ${boePanel.radius};
157
- border: 1px dashed color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 70%, transparent);
158
- color: var(--boe-token-text-text-secondary, #6f6f6f);
159
- }
160
-
161
- [part="composer"] {
162
- display: grid;
163
- gap: 0.5rem;
164
- }
165
-
166
- [part="composer-label"] {
14
+ [part="anchor-location"] {
167
15
  font-size: 0.74rem;
168
16
  font-weight: 700;
169
- letter-spacing: 0.08em;
17
+ letter-spacing: 0.06em;
170
18
  text-transform: uppercase;
171
19
  color: var(--boe-token-text-text-secondary, #6f6f6f);
172
20
  }
173
21
 
174
- [part="composer-input"] {
175
- min-height: 4rem;
176
- padding: 0.55rem;
177
- border: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 60%, transparent);
178
- border-radius: ${boePanel.radius};
179
- background: var(--boe-token-surface-surface, #ffffff);
180
- color: inherit;
181
- font: inherit;
22
+ /* Quoted document text, so it reads as a citation rather than as one of
23
+ the comments below it. */
24
+ [part="anchor-quote"] {
25
+ margin: 0;
26
+ color: var(--boe-token-text-text, #1f1e1b);
27
+ font-style: italic;
182
28
  line-height: 1.5;
183
- resize: vertical;
184
- }
185
-
186
- [part="composer-submit"] {
187
- appearance: none;
188
- justify-self: start;
189
- min-height: 2rem;
190
- padding: 0.4rem 0.7rem;
191
- border: 1px solid transparent;
192
- border-radius: 999px;
193
- background: var(--boe-token-surface-surface-brand, #0061d5);
194
- color: var(--boe-token-text-text-on-brand, #ffffff);
195
- font: inherit;
196
- cursor: pointer;
197
- }
198
-
199
- ${boeNeutralInteractiveStyles('[part="entry"]')}
200
- ${boeNeutralInteractiveStyles('[part="action"]')}
201
- ${boeBrandInteractiveStyles('[part="action"][data-tone="primary"]')}
202
- ${boeBrandInteractiveStyles('[part="composer-submit"]')}
203
-
204
- [part="entry"][aria-pressed="true"],
205
- [part="entry"][aria-pressed="true"]:hover:not(:disabled) {
206
- border-color: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 34%, transparent);
207
- box-shadow: ${boeFocusRingShadow};
208
- }
209
-
210
- [part="action"][data-tone="primary"] {
211
- border-color: transparent;
212
- background: var(--boe-token-surface-surface-brand, #0061d5);
213
- color: var(--boe-token-text-text-on-brand, #ffffff);
214
29
  }
215
30
  `;
216
- export class AnnotationThread extends BaseElement {
31
+ /**
32
+ * A comment thread anchored to a place in a document.
33
+ *
34
+ * This is `box-comment-thread` plus the anchor. The distinction is the whole
35
+ * point: comments stand on their own — on a file, a task, a contract clause —
36
+ * and only become annotations when something ties them to a page, a region, or
37
+ * a run of selected text. Hosts that want the former should reach for
38
+ * `box-comment-thread` directly rather than for an annotation with no anchor.
39
+ *
40
+ * Every attribute, part and event of the underlying thread is inherited
41
+ * unchanged, so this remains a drop-in for the previous implementation.
42
+ */
43
+ export class AnnotationThread extends CommentThread {
217
44
  static tagName = DEFAULT_TAG_NAME;
218
45
  static get observedAttributes() {
219
- return ["actions", "composable", "entries", "message", "selected-entry-id", "heading"];
220
- }
221
- titleEl;
222
- messageEl;
223
- entriesEl;
224
- emptyEl;
225
- actionsEl;
226
- composerEl;
227
- composerInputEl;
228
- entriesSignature = "";
229
- actionsSignature = "";
230
- get actions() {
231
- return this.parseJsonAttribute("actions", []);
46
+ return [...CommentThread.observedAttributes, "anchor"];
47
+ }
48
+ anchorEl = null;
49
+ threadStyles() {
50
+ return `${super.threadStyles()}\n${anchorStyles}`;
51
+ }
52
+ headerExtras() {
53
+ return `
54
+ <div part="anchor" hidden>
55
+ <span part="anchor-location"></span>
56
+ <blockquote part="anchor-quote" hidden></blockquote>
57
+ </div>
58
+ `;
232
59
  }
233
- set actions(value) {
234
- this.setAttribute("actions", JSON.stringify(value));
60
+ defaultHeading() {
61
+ return "Annotation Thread";
235
62
  }
236
- get composable() {
237
- return this.hasAttribute("composable");
63
+ emptyText() {
64
+ return "No annotation thread entries available.";
238
65
  }
239
- set composable(value) {
240
- this.toggleAttribute("composable", value);
66
+ defaultComposerLabel() {
67
+ return "Reply";
241
68
  }
242
- get entries() {
243
- return this.parseJsonAttribute("entries", []);
69
+ defaultPlaceholder() {
70
+ return "Add a reply";
244
71
  }
245
- set entries(value) {
246
- this.setAttribute("entries", JSON.stringify(value));
72
+ /**
73
+ * Annotations name their pill `toolLabel`. Mapping it here keeps that
74
+ * vocabulary for existing hosts while the base model stays generic.
75
+ */
76
+ entryBadge(entry) {
77
+ return entry.toolLabel ?? entry.badge;
247
78
  }
248
- get message() {
249
- return this.getAttribute("message") ?? "";
79
+ /** Where in the document this thread sits. Null when unanchored. */
80
+ get anchor() {
81
+ return this.parseJsonAttribute("anchor", null);
250
82
  }
251
- set message(value) {
83
+ set anchor(value) {
252
84
  if (!value) {
253
- this.removeAttribute("message");
85
+ this.removeAttribute("anchor");
254
86
  return;
255
87
  }
256
- this.setAttribute("message", value);
257
- }
258
- get selectedEntryId() {
259
- return this.getAttribute("selected-entry-id") ?? "";
88
+ this.setAttribute("anchor", JSON.stringify(value));
260
89
  }
261
- set selectedEntryId(value) {
262
- if (!value) {
263
- this.removeAttribute("selected-entry-id");
264
- return;
265
- }
266
- this.setAttribute("selected-entry-id", value);
267
- }
268
- get heading() {
269
- return this.getAttribute("heading") ?? "Annotation Thread";
90
+ get entries() {
91
+ return super.entries;
270
92
  }
271
- set heading(value) {
272
- this.setAttribute("heading", value);
93
+ set entries(value) {
94
+ super.entries = value;
273
95
  }
274
- parseJsonAttribute(name, fallback) {
275
- const raw = this.getAttribute(name);
276
- if (!raw) {
277
- return fallback;
278
- }
279
- try {
280
- return JSON.parse(raw);
96
+ /**
97
+ * The anchor in words.
98
+ *
99
+ * A region is described by its page rather than by its coordinates: the
100
+ * numbers locate the highlight for the renderer, but they tell a reader
101
+ * nothing, and a screen reader least of all.
102
+ */
103
+ anchorLocation(anchor) {
104
+ if (typeof anchor.page === "number" && Number.isFinite(anchor.page)) {
105
+ return anchor.region ? `Page ${anchor.page} · region` : `Page ${anchor.page}`;
281
106
  }
282
- catch {
283
- return fallback;
107
+ if (anchor.region) {
108
+ return "Region";
284
109
  }
110
+ return anchor.quote ? "Selected text" : "";
285
111
  }
286
- emitAction(actionId) {
287
- this.dispatchEvent(new CustomEvent("action", {
288
- bubbles: true,
289
- composed: true,
290
- detail: {
291
- action: actionId,
292
- selectedEntryId: this.selectedEntryId || null,
293
- },
294
- }));
295
- }
296
- emitEntrySelected(entry) {
297
- this.selectedEntryId = entry.id;
298
- this.dispatchEvent(new CustomEvent("entry-selected", {
299
- bubbles: true,
300
- composed: true,
301
- detail: entry,
302
- }));
303
- }
304
- submitComposer() {
305
- const body = this.composerInputEl.value.trim();
306
- if (!this.composable || !body) {
307
- return;
308
- }
309
- this.dispatchEvent(new CustomEvent("entry-submitted", {
310
- bubbles: true,
311
- composed: true,
312
- detail: {
313
- body,
314
- inReplyToId: this.selectedEntryId || null,
315
- },
316
- }));
317
- this.composerInputEl.value = "";
318
- }
319
- entriesKey() {
320
- return this.getAttribute("entries") ?? "";
321
- }
322
- actionsKey() {
323
- return JSON.stringify(this.actions.map(action => action.id));
324
- }
325
- rebuildEntries() {
326
- this.entriesEl.innerHTML = this.entries
327
- .map(entry => {
328
- const selected = entry.id === this.selectedEntryId;
329
- return `
330
- <li part="entry-item" role="listitem">
331
- <button
332
- type="button"
333
- part="entry"
334
- data-entry-id="${escapeHtml(entry.id)}"
335
- aria-pressed="${selected ? "true" : "false"}"
336
- >
337
- <span part="entry-avatar">${escapeHtml(entry.initials ?? entry.author.slice(0, 2).toUpperCase())}</span>
338
- <span part="entry-copy">
339
- <span part="entry-topline">
340
- <span part="entry-author">${escapeHtml(entry.author)}</span>
341
- ${entry.toolLabel ? `<span part="entry-tool">${escapeHtml(entry.toolLabel)}</span>` : ""}
342
- ${entry.status ? `<span part="entry-status">${escapeHtml(entry.status)}</span>` : ""}
343
- </span>
344
- <span part="entry-body">${escapeHtml(entry.body)}</span>
345
- ${entry.createdAt ? `<span part="entry-time">${escapeHtml(entry.createdAt)}</span>` : ""}
346
- </span>
347
- </button>
348
- </li>
349
- `;
350
- })
351
- .join("");
352
- }
353
- patchEntrySelection() {
354
- this.entriesEl.querySelectorAll('[part="entry"]').forEach(button => {
355
- const selected = button.getAttribute("data-entry-id") === this.selectedEntryId;
356
- button.setAttribute("aria-pressed", selected ? "true" : "false");
357
- });
358
- }
359
- rebuildActions() {
360
- this.actionsEl.innerHTML = this.actions
361
- .map(action => `
362
- <button type="button" part="action" data-action-id="${escapeHtml(action.id)}" data-tone="${escapeHtml(action.tone ?? "neutral")}">
363
- ${escapeHtml(action.label)}
364
- </button>
365
- `)
366
- .join("");
367
- }
368
- patchActionLabels() {
369
- // Action IDs are arbitrary strings (quotes, newlines, …) — match on the
370
- // dataset instead of interpolating them into a CSS selector.
371
- const buttons = Array.from(this.actionsEl.querySelectorAll('[part="action"]'));
372
- this.actions.forEach(action => {
373
- const button = buttons.find(candidate => candidate.dataset.actionId === action.id);
374
- if (!button) {
375
- return;
376
- }
377
- button.textContent = action.label;
378
- button.dataset.tone = action.tone ?? "neutral";
379
- });
380
- }
381
- renderTemplate() {
112
+ update() {
113
+ super.update();
382
114
  if (!this.shadowRoot) {
383
115
  return;
384
116
  }
385
- this.shadowRoot.innerHTML = `
386
- <style>${elementStyles}</style>
387
- <article part="thread">
388
- <header part="header">
389
- <h2 part="title"></h2>
390
- <div part="message" hidden></div>
391
- </header>
392
- <ul part="entries" role="list" hidden></ul>
393
- <div part="empty" hidden>No annotation thread entries available.</div>
394
- <div part="actions" hidden></div>
395
- <div part="composer" hidden>
396
- <label part="composer-label" for="annotation-thread-composer-input">Reply</label>
397
- <textarea
398
- part="composer-input"
399
- id="annotation-thread-composer-input"
400
- placeholder="Add a reply"
401
- ></textarea>
402
- <button type="button" part="composer-submit">Reply</button>
403
- </div>
404
- </article>
405
- `;
406
- this.titleEl = this.shadowRoot.querySelector('[part="title"]');
407
- this.messageEl = this.shadowRoot.querySelector('[part="message"]');
408
- this.entriesEl = this.shadowRoot.querySelector('[part="entries"]');
409
- this.emptyEl = this.shadowRoot.querySelector('[part="empty"]');
410
- this.actionsEl = this.shadowRoot.querySelector('[part="actions"]');
411
- this.composerEl = this.shadowRoot.querySelector('[part="composer"]');
412
- this.composerInputEl = this.shadowRoot.querySelector('[part="composer-input"]');
413
- }
414
- setupListeners() {
415
- this.entriesEl.addEventListener("click", event => {
416
- const button = event.target.closest('[part="entry"]');
417
- if (!button || !this.entriesEl.contains(button)) {
418
- return;
419
- }
420
- const entryId = button.getAttribute("data-entry-id");
421
- const entry = this.entries.find(item => item.id === entryId);
422
- if (entry) {
423
- this.emitEntrySelected(entry);
424
- }
425
- });
426
- this.actionsEl.addEventListener("click", event => {
427
- const button = event.target.closest('[part="action"]');
428
- if (!button || !this.actionsEl.contains(button)) {
429
- return;
430
- }
431
- const actionId = button.getAttribute("data-action-id");
432
- if (actionId) {
433
- this.emitAction(actionId);
434
- }
435
- });
436
- this.composerEl.addEventListener("click", event => {
437
- const button = event.target.closest('[part="composer-submit"]');
438
- if (button) {
439
- this.submitComposer();
440
- }
441
- });
442
- this.composerInputEl.addEventListener("keydown", event => {
443
- if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
444
- event.preventDefault();
445
- this.submitComposer();
446
- }
447
- });
448
- }
449
- update() {
450
- if (!this.entriesEl) {
117
+ this.anchorEl ??= this.shadowRoot.querySelector('[part="anchor"]');
118
+ if (!this.anchorEl) {
451
119
  return;
452
120
  }
453
- this.titleEl.textContent = this.heading;
454
- this.messageEl.hidden = !this.message;
455
- this.messageEl.textContent = this.message;
456
- const entries = this.entries;
457
- this.entriesEl.hidden = entries.length === 0;
458
- this.emptyEl.hidden = entries.length > 0;
459
- this.entriesEl.setAttribute("aria-label", `${this.heading} entries`);
460
- const nextEntries = this.entriesKey();
461
- if (nextEntries !== this.entriesSignature) {
462
- this.entriesSignature = nextEntries;
463
- this.rebuildEntries();
464
- }
465
- else {
466
- this.patchEntrySelection();
467
- }
468
- const actions = this.actions;
469
- this.actionsEl.hidden = actions.length === 0;
470
- const nextActions = this.actionsKey();
471
- if (nextActions !== this.actionsSignature || this.actionsEl.childElementCount === 0) {
472
- this.actionsSignature = nextActions;
473
- this.rebuildActions();
474
- }
475
- else {
476
- this.patchActionLabels();
121
+ const anchor = this.anchor;
122
+ const location = anchor ? this.anchorLocation(anchor) : "";
123
+ const quote = anchor?.quote ?? "";
124
+ // Hidden only when the anchor says nothing renderable — an anchor carrying
125
+ // just a region still names its page, and a quote alone still shows.
126
+ this.anchorEl.hidden = !location && !quote;
127
+ const locationEl = this.anchorEl.querySelector('[part="anchor-location"]');
128
+ if (locationEl) {
129
+ locationEl.hidden = !location;
130
+ locationEl.textContent = location;
131
+ }
132
+ const quoteEl = this.anchorEl.querySelector('[part="anchor-quote"]');
133
+ if (quoteEl) {
134
+ quoteEl.hidden = !quote;
135
+ quoteEl.textContent = quote;
477
136
  }
478
- const composable = this.composable;
479
- this.composerEl.hidden = !composable;
480
- this.composerInputEl.disabled = !composable;
481
137
  }
482
138
  }
483
139
  AnnotationThread.register();
@@ -60,6 +60,14 @@ const elementStyles = `
60
60
  margin: 0;
61
61
  padding: 0;
62
62
  display: grid;
63
+
64
+ /* Named so the spine can derive how far it has to reach rather than
65
+ restating the numbers. The line has to cross this entry's bottom
66
+ padding, the next entry's top padding and the marker's own offset
67
+ to arrive at the next marker; spelling that out as arithmetic
68
+ keeps it correct if any of the three changes. */
69
+ --timeline-event-pad: 0.55rem;
70
+ --timeline-marker-offset: 0.3rem;
63
71
  }
64
72
 
65
73
  [part="event"] {
@@ -67,29 +75,53 @@ const elementStyles = `
67
75
  display: grid;
68
76
  grid-template-columns: auto 1fr;
69
77
  gap: 0.65rem;
70
- padding: 0.55rem 0 0.55rem 0.15rem;
78
+ padding: var(--timeline-event-pad) 0 var(--timeline-event-pad) 0.15rem;
71
79
  }
72
80
 
73
- /* The connecting spine between markers. */
74
- [part="event"]:not(:last-child)::after {
75
- content: "";
76
- position: absolute;
77
- left: calc(0.15rem + 0.55rem - 1px);
78
- top: 1.9rem;
79
- bottom: -0.35rem;
80
- width: 2px;
81
- background: color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 72%, transparent);
81
+ /* The marker column, built the way box-path's base variant builds its
82
+ rail: the marker is a fixed box and the spine is a flex sibling that
83
+ takes whatever height is left, so the line runs from the marker to
84
+ the bottom of the entry however tall the entry's content turns out
85
+ to be.
86
+
87
+ The previous version positioned the spine with hand-tuned constants
88
+ (top: 1.9rem, bottom: -0.35rem) that guessed where the marker ended.
89
+ They were only ever right for one font size and one content height,
90
+ which is why the line stopped short of the next marker and dangled. */
91
+ [part="marker-rail"] {
92
+ display: flex;
93
+ flex-direction: column;
94
+ align-items: center;
95
+ align-self: stretch;
96
+ margin-top: var(--timeline-marker-offset);
82
97
  }
83
98
 
84
99
  [part="marker"] {
100
+ flex: 0 0 auto;
85
101
  inline-size: 1.1rem;
86
102
  block-size: 1.1rem;
87
- margin-top: 0.3rem;
88
103
  border-radius: 999px;
89
104
  border: 2px solid var(--timeline-tone, var(--boe-token-text-text-secondary, #6f6f6f));
90
105
  background: color-mix(in srgb, var(--timeline-tone, var(--boe-token-text-text-secondary, #6f6f6f)) 14%, var(--boe-token-surface-surface, #ffffff) 86%);
91
106
  }
92
107
 
108
+ /* The spine. Grows to fill the column, and the negative bottom margin
109
+ carries it across the gap into the next entry's marker so the two
110
+ actually meet. Hidden on the last entry, which leads nowhere. */
111
+ [part="marker-rail"]::after {
112
+ content: "";
113
+ flex: 1 1 auto;
114
+ inline-size: 2px;
115
+ margin-block: 0.25rem
116
+ calc(-1 * (var(--timeline-event-pad) * 2 + var(--timeline-marker-offset)));
117
+ border-radius: 999px;
118
+ background: var(--boe-token-stroke-stroke, #e8e8e8);
119
+ }
120
+
121
+ [part="event"]:last-child [part="marker-rail"]::after {
122
+ display: none;
123
+ }
124
+
93
125
  [part="event-body"] {
94
126
  display: grid;
95
127
  gap: 0.25rem;
@@ -328,7 +360,7 @@ export class Timeline extends BaseElement {
328
360
  const timestamp = event.timestamp ? formatItemDate(event.timestamp) : "";
329
361
  return `
330
362
  <li part="event" data-event-id="${escapeHtml(event.id)}" style="--timeline-tone:${toneColor(tone)};">
331
- <span part="marker" aria-hidden="true"></span>
363
+ <span part="marker-rail" aria-hidden="true"><span part="marker"></span></span>
332
364
  <div part="event-body">
333
365
  <div part="event-topline">
334
366
  ${event.actor ? `<span part="actor">${escapeHtml(event.actor.name)}</span>` : ""}