@unofficialbox/box-open-elements 0.11.0 → 0.13.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 (59) hide show
  1. package/dist/components/feedback/alert.d.ts +2 -0
  2. package/dist/components/feedback/alert.js +41 -8
  3. package/dist/components/feedback/indicator.d.ts +34 -0
  4. package/dist/components/feedback/indicator.js +167 -0
  5. package/dist/components/feedback/nudge.js +0 -1
  6. package/dist/components/feedback/toast.d.ts +4 -5
  7. package/dist/components/feedback/toast.js +19 -46
  8. package/dist/components/feedback/tone.d.ts +18 -0
  9. package/dist/components/feedback/tone.js +23 -0
  10. package/dist/components/forms/calendar.d.ts +25 -0
  11. package/dist/components/forms/calendar.js +94 -2
  12. package/dist/components/forms/tile-group.d.ts +58 -0
  13. package/dist/components/forms/tile-group.js +280 -0
  14. package/dist/components/output/code-block.d.ts +49 -0
  15. package/dist/components/output/code-block.js +236 -0
  16. package/dist/components/output/formatted-date.d.ts +38 -0
  17. package/dist/components/output/formatted-date.js +90 -0
  18. package/dist/components/output/formatted-file-size.d.ts +31 -0
  19. package/dist/components/output/formatted-file-size.js +61 -0
  20. package/dist/components/output/formatted-number.d.ts +37 -0
  21. package/dist/components/output/formatted-number.js +106 -0
  22. package/dist/components/output/formatted-value.d.ts +44 -0
  23. package/dist/components/output/formatted-value.js +100 -0
  24. package/dist/components/output/index.d.ts +6 -0
  25. package/dist/components/output/index.js +9 -0
  26. package/dist/components/output/relative-time.d.ts +41 -0
  27. package/dist/components/output/relative-time.js +82 -0
  28. package/dist/element-maps.d.ts +17 -1
  29. package/dist/entries/code-block.d.ts +1 -0
  30. package/dist/entries/code-block.js +1 -0
  31. package/dist/entries/comment-thread.d.ts +1 -0
  32. package/dist/entries/comment-thread.js +1 -0
  33. package/dist/entries/formatted-date.d.ts +1 -0
  34. package/dist/entries/formatted-date.js +1 -0
  35. package/dist/entries/formatted-file-size.d.ts +1 -0
  36. package/dist/entries/formatted-file-size.js +1 -0
  37. package/dist/entries/formatted-number.d.ts +1 -0
  38. package/dist/entries/formatted-number.js +1 -0
  39. package/dist/entries/indicator.d.ts +1 -0
  40. package/dist/entries/indicator.js +1 -0
  41. package/dist/entries/relative-time.d.ts +1 -0
  42. package/dist/entries/relative-time.js +1 -0
  43. package/dist/entries/tile-group.d.ts +1 -0
  44. package/dist/entries/tile-group.js +1 -0
  45. package/dist/foundations/format/format.d.ts +82 -0
  46. package/dist/foundations/format/format.js +122 -0
  47. package/dist/foundations/format/index.d.ts +1 -0
  48. package/dist/foundations/format/index.js +3 -0
  49. package/dist/index.d.ts +10 -0
  50. package/dist/index.js +12 -0
  51. package/dist/patterns/comments/comment-thread.d.ts +79 -0
  52. package/dist/patterns/comments/comment-thread.js +561 -0
  53. package/dist/patterns/comments/index.d.ts +2 -0
  54. package/dist/patterns/comments/index.js +5 -0
  55. package/dist/patterns/comments/types.d.ts +39 -0
  56. package/dist/patterns/comments/types.js +8 -0
  57. package/dist/patterns/preview/annotation-thread.d.ts +67 -48
  58. package/dist/patterns/preview/annotation-thread.js +96 -440
  59. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ export { DESIGN_PROFILE_CHANGE_EVENT, DESIGN_PROFILE_VARIABLES, DEFAULT_DESIGN_P
7
7
  export { boxIconography, boxIconographyAliases } from "./foundations/icons/index.js";
8
8
  // Foundations: Adobe Spectrum's responsive grid (shared by box-grid and box-skeleton)
9
9
  export * from "./foundations/layout/index.js";
10
+ // Foundations: locale-aware typed-value formatting (shared by box-formatted-*)
11
+ export * from "./foundations/format/index.js";
10
12
  // Foundations: viewport-aware overlay positioning (for building custom overlays)
11
13
  export { anchorFloating, parsePlacement, resolvePosition, trackAnchor, } from "./foundations/overlay/index.js";
12
14
  // Components: actions
@@ -38,6 +40,7 @@ export * from "./components/feedback/due-types.js";
38
40
  export * from "./components/feedback/empty-state.js";
39
41
  export * from "./components/feedback/error-mask.js";
40
42
  export * from "./components/feedback/help-text.js";
43
+ export * from "./components/feedback/indicator.js";
41
44
  export * from "./components/feedback/nudge.js";
42
45
  export * from "./components/feedback/progress-bar.js";
43
46
  export * from "./components/feedback/progress-ring.js";
@@ -72,6 +75,7 @@ export * from "./components/forms/slider.js";
72
75
  export * from "./components/forms/spin-button.js";
73
76
  export * from "./components/forms/switch.js";
74
77
  export * from "./components/forms/tag-input.js";
78
+ export * from "./components/forms/tile-group.js";
75
79
  export * from "./components/forms/time-field.js";
76
80
  export * from "./components/forms/category-selector.js";
77
81
  export * from "./components/forms/checkbox-group.js";
@@ -86,6 +90,13 @@ export * from "./components/forms/search-field.js";
86
90
  export * from "./components/forms/select.js";
87
91
  export * from "./components/forms/text-area.js";
88
92
  export * from "./components/forms/text-field.js";
93
+ // Components: output (read-only typed-value renderers)
94
+ export * from "./components/output/formatted-value.js";
95
+ export * from "./components/output/code-block.js";
96
+ export * from "./components/output/formatted-date.js";
97
+ export * from "./components/output/relative-time.js";
98
+ export * from "./components/output/formatted-number.js";
99
+ export * from "./components/output/formatted-file-size.js";
89
100
  // Components: layout
90
101
  export * from "./components/layout/app-shell.js";
91
102
  export * from "./components/layout/divider.js";
@@ -119,6 +130,7 @@ export * from "./patterns/versions/index.js";
119
130
  export * from "./patterns/lineage/index.js";
120
131
  export * from "./patterns/agent-chat/index.js";
121
132
  export * from "./patterns/audit/index.js";
133
+ export * from "./patterns/comments/index.js";
122
134
  export * from "./patterns/notifications/index.js";
123
135
  // Patterns: compositions and workflow areas
124
136
  export * from "./patterns/search/index.js";
@@ -0,0 +1,79 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ import type { CommentAction, CommentEntry } from "./types.js";
3
+ /**
4
+ * A comment thread: authored entries, selection, actions, and a composer.
5
+ *
6
+ * Comments are a standalone concept. They hang off a file, a folder, a task or
7
+ * a contract clause just as readily as off a region of a document, so the
8
+ * thread does not know what it is attached to — a host that needs an anchor
9
+ * composes one on top. `box-annotation-thread` is exactly that: this component
10
+ * plus the document anchor, which is the only thing that makes an annotation an
11
+ * annotation.
12
+ *
13
+ * The extension points below (`threadStyles`, `headerExtras`, `defaultHeading`,
14
+ * `emptyText`, `entryBadge`, and the composer copy) exist so a subclass can
15
+ * specialise the chrome without reimplementing entry rendering, selection
16
+ * patching, or the composer.
17
+ */
18
+ export declare class CommentThread extends BaseElement {
19
+ static readonly tagName: string;
20
+ static get observedAttributes(): string[];
21
+ protected titleEl: HTMLElement;
22
+ protected messageEl: HTMLElement;
23
+ protected entriesEl: HTMLElement;
24
+ protected emptyEl: HTMLElement;
25
+ protected actionsEl: HTMLElement;
26
+ protected composerEl: HTMLElement;
27
+ protected composerLabelEl: HTMLElement;
28
+ protected composerSubmitEl: HTMLElement;
29
+ protected composerInputEl: HTMLTextAreaElement;
30
+ private entriesSignature;
31
+ private actionsSignature;
32
+ /** Styles for the thread. Subclasses append rather than replace. */
33
+ protected threadStyles(): string;
34
+ /** Extra markup inside the header, after the message. Empty by default. */
35
+ protected headerExtras(): string;
36
+ /** Heading when the host sets none. */
37
+ protected defaultHeading(): string;
38
+ /** Text shown when there are no entries. */
39
+ protected emptyText(): string;
40
+ /** Label on the composer, and on its submit button. */
41
+ protected defaultComposerLabel(): string;
42
+ /** Placeholder in the composer input. */
43
+ protected defaultPlaceholder(): string;
44
+ /**
45
+ * The pill beside the author. Reads the generic `badge`; a subclass with its
46
+ * own vocabulary overrides this rather than forcing hosts to rename fields.
47
+ */
48
+ protected entryBadge(entry: CommentEntry): string | undefined;
49
+ get actions(): CommentAction[];
50
+ set actions(value: CommentAction[]);
51
+ get composable(): boolean;
52
+ set composable(value: boolean);
53
+ get entries(): CommentEntry[];
54
+ set entries(value: CommentEntry[]);
55
+ get message(): string;
56
+ set message(value: string);
57
+ get selectedEntryId(): string;
58
+ set selectedEntryId(value: string);
59
+ get heading(): string;
60
+ set heading(value: string);
61
+ /** Composer label and submit-button text. */
62
+ get composerLabel(): string;
63
+ set composerLabel(value: string);
64
+ get placeholder(): string;
65
+ set placeholder(value: string);
66
+ protected parseJsonAttribute<T>(name: string, fallback: T): T;
67
+ private emitAction;
68
+ private emitEntrySelected;
69
+ private submitComposer;
70
+ private entriesKey;
71
+ private actionsKey;
72
+ private rebuildEntries;
73
+ private patchEntrySelection;
74
+ private rebuildActions;
75
+ private patchActionLabels;
76
+ protected renderTemplate(): void;
77
+ protected setupListeners(): void;
78
+ protected update(): void;
79
+ }
@@ -0,0 +1,561 @@
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
+ import { boePanel } from "../../foundations/geometry/index.js";
5
+ const DEFAULT_TAG_NAME = "box-comment-thread";
6
+ const escapeHtml = (value) => value
7
+ .replaceAll("&", "&amp;")
8
+ .replaceAll("<", "&lt;")
9
+ .replaceAll(">", "&gt;")
10
+ .replaceAll('"', "&quot;")
11
+ .replaceAll("'", "&#39;");
12
+ const threadStyles = `
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;
66
+ 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);
73
+ 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
+ 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
+ }
141
+
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"] {
167
+ font-size: 0.74rem;
168
+ font-weight: 700;
169
+ letter-spacing: 0.08em;
170
+ text-transform: uppercase;
171
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
172
+ }
173
+
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;
182
+ 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
+ }
215
+ `;
216
+ /**
217
+ * A comment thread: authored entries, selection, actions, and a composer.
218
+ *
219
+ * Comments are a standalone concept. They hang off a file, a folder, a task or
220
+ * a contract clause just as readily as off a region of a document, so the
221
+ * thread does not know what it is attached to — a host that needs an anchor
222
+ * composes one on top. `box-annotation-thread` is exactly that: this component
223
+ * plus the document anchor, which is the only thing that makes an annotation an
224
+ * annotation.
225
+ *
226
+ * The extension points below (`threadStyles`, `headerExtras`, `defaultHeading`,
227
+ * `emptyText`, `entryBadge`, and the composer copy) exist so a subclass can
228
+ * specialise the chrome without reimplementing entry rendering, selection
229
+ * patching, or the composer.
230
+ */
231
+ export class CommentThread extends BaseElement {
232
+ static tagName = DEFAULT_TAG_NAME;
233
+ static get observedAttributes() {
234
+ return [
235
+ "actions",
236
+ "composable",
237
+ "composer-label",
238
+ "entries",
239
+ "heading",
240
+ "message",
241
+ "placeholder",
242
+ "selected-entry-id",
243
+ ];
244
+ }
245
+ titleEl;
246
+ messageEl;
247
+ entriesEl;
248
+ emptyEl;
249
+ actionsEl;
250
+ composerEl;
251
+ composerLabelEl;
252
+ composerSubmitEl;
253
+ composerInputEl;
254
+ entriesSignature = "";
255
+ actionsSignature = "";
256
+ /** Styles for the thread. Subclasses append rather than replace. */
257
+ threadStyles() {
258
+ return threadStyles;
259
+ }
260
+ /** Extra markup inside the header, after the message. Empty by default. */
261
+ headerExtras() {
262
+ return "";
263
+ }
264
+ /** Heading when the host sets none. */
265
+ defaultHeading() {
266
+ return "Comments";
267
+ }
268
+ /** Text shown when there are no entries. */
269
+ emptyText() {
270
+ return "No comments yet.";
271
+ }
272
+ /** Label on the composer, and on its submit button. */
273
+ defaultComposerLabel() {
274
+ return "Comment";
275
+ }
276
+ /** Placeholder in the composer input. */
277
+ defaultPlaceholder() {
278
+ return "Add a comment";
279
+ }
280
+ /**
281
+ * The pill beside the author. Reads the generic `badge`; a subclass with its
282
+ * own vocabulary overrides this rather than forcing hosts to rename fields.
283
+ */
284
+ entryBadge(entry) {
285
+ return entry.badge;
286
+ }
287
+ get actions() {
288
+ return this.parseJsonAttribute("actions", []);
289
+ }
290
+ set actions(value) {
291
+ this.setAttribute("actions", JSON.stringify(value));
292
+ }
293
+ get composable() {
294
+ return this.hasAttribute("composable");
295
+ }
296
+ set composable(value) {
297
+ this.toggleAttribute("composable", value);
298
+ }
299
+ get entries() {
300
+ return this.parseJsonAttribute("entries", []);
301
+ }
302
+ set entries(value) {
303
+ this.setAttribute("entries", JSON.stringify(value));
304
+ }
305
+ get message() {
306
+ return this.getAttribute("message") ?? "";
307
+ }
308
+ set message(value) {
309
+ if (!value) {
310
+ this.removeAttribute("message");
311
+ return;
312
+ }
313
+ this.setAttribute("message", value);
314
+ }
315
+ get selectedEntryId() {
316
+ return this.getAttribute("selected-entry-id") ?? "";
317
+ }
318
+ set selectedEntryId(value) {
319
+ if (!value) {
320
+ this.removeAttribute("selected-entry-id");
321
+ return;
322
+ }
323
+ this.setAttribute("selected-entry-id", value);
324
+ }
325
+ get heading() {
326
+ return this.getAttribute("heading") ?? this.defaultHeading();
327
+ }
328
+ set heading(value) {
329
+ this.setAttribute("heading", value);
330
+ }
331
+ /** Composer label and submit-button text. */
332
+ get composerLabel() {
333
+ return this.getAttribute("composer-label") ?? this.defaultComposerLabel();
334
+ }
335
+ set composerLabel(value) {
336
+ this.setAttribute("composer-label", value);
337
+ }
338
+ get placeholder() {
339
+ return this.getAttribute("placeholder") ?? this.defaultPlaceholder();
340
+ }
341
+ set placeholder(value) {
342
+ this.setAttribute("placeholder", value);
343
+ }
344
+ parseJsonAttribute(name, fallback) {
345
+ const raw = this.getAttribute(name);
346
+ if (!raw) {
347
+ return fallback;
348
+ }
349
+ try {
350
+ return JSON.parse(raw);
351
+ }
352
+ catch {
353
+ return fallback;
354
+ }
355
+ }
356
+ emitAction(actionId) {
357
+ this.dispatchEvent(new CustomEvent("action", {
358
+ bubbles: true,
359
+ composed: true,
360
+ detail: {
361
+ action: actionId,
362
+ selectedEntryId: this.selectedEntryId || null,
363
+ },
364
+ }));
365
+ }
366
+ emitEntrySelected(entry) {
367
+ this.selectedEntryId = entry.id;
368
+ this.dispatchEvent(new CustomEvent("entry-selected", {
369
+ bubbles: true,
370
+ composed: true,
371
+ detail: entry,
372
+ }));
373
+ }
374
+ submitComposer() {
375
+ const body = this.composerInputEl.value.trim();
376
+ if (!this.composable || !body) {
377
+ return;
378
+ }
379
+ this.dispatchEvent(new CustomEvent("entry-submitted", {
380
+ bubbles: true,
381
+ composed: true,
382
+ detail: {
383
+ body,
384
+ inReplyToId: this.selectedEntryId || null,
385
+ },
386
+ }));
387
+ this.composerInputEl.value = "";
388
+ }
389
+ entriesKey() {
390
+ return this.getAttribute("entries") ?? "";
391
+ }
392
+ actionsKey() {
393
+ return JSON.stringify(this.actions.map(action => action.id));
394
+ }
395
+ rebuildEntries() {
396
+ this.entriesEl.innerHTML = this.entries
397
+ .map(entry => {
398
+ const selected = entry.id === this.selectedEntryId;
399
+ const badge = this.entryBadge(entry);
400
+ return `
401
+ <li part="entry-item" role="listitem">
402
+ <button
403
+ type="button"
404
+ part="entry"
405
+ data-entry-id="${escapeHtml(entry.id)}"
406
+ aria-pressed="${selected ? "true" : "false"}"
407
+ >
408
+ <span part="entry-avatar">${escapeHtml(entry.initials ?? entry.author.slice(0, 2).toUpperCase())}</span>
409
+ <span part="entry-copy">
410
+ <span part="entry-topline">
411
+ <span part="entry-author">${escapeHtml(entry.author)}</span>
412
+ ${badge ? `<span part="entry-tool">${escapeHtml(badge)}</span>` : ""}
413
+ ${entry.status ? `<span part="entry-status">${escapeHtml(entry.status)}</span>` : ""}
414
+ </span>
415
+ <span part="entry-body">${escapeHtml(entry.body)}</span>
416
+ ${entry.createdAt ? `<span part="entry-time">${escapeHtml(entry.createdAt)}</span>` : ""}
417
+ </span>
418
+ </button>
419
+ </li>
420
+ `;
421
+ })
422
+ .join("");
423
+ }
424
+ patchEntrySelection() {
425
+ this.entriesEl.querySelectorAll('[part="entry"]').forEach(button => {
426
+ const selected = button.getAttribute("data-entry-id") === this.selectedEntryId;
427
+ button.setAttribute("aria-pressed", selected ? "true" : "false");
428
+ });
429
+ }
430
+ rebuildActions() {
431
+ this.actionsEl.innerHTML = this.actions
432
+ .map(action => `
433
+ <button type="button" part="action" data-action-id="${escapeHtml(action.id)}" data-tone="${escapeHtml(action.tone ?? "neutral")}">
434
+ ${escapeHtml(action.label)}
435
+ </button>
436
+ `)
437
+ .join("");
438
+ }
439
+ patchActionLabels() {
440
+ // Action IDs are arbitrary strings (quotes, newlines, …) — match on the
441
+ // dataset instead of interpolating them into a CSS selector.
442
+ const buttons = Array.from(this.actionsEl.querySelectorAll('[part="action"]'));
443
+ this.actions.forEach(action => {
444
+ const button = buttons.find(candidate => candidate.dataset.actionId === action.id);
445
+ if (!button) {
446
+ return;
447
+ }
448
+ button.textContent = action.label;
449
+ button.dataset.tone = action.tone ?? "neutral";
450
+ });
451
+ }
452
+ renderTemplate() {
453
+ if (!this.shadowRoot) {
454
+ return;
455
+ }
456
+ this.shadowRoot.innerHTML = `
457
+ <style>${this.threadStyles()}</style>
458
+ <article part="thread">
459
+ <header part="header">
460
+ <h2 part="title"></h2>
461
+ <div part="message" hidden></div>
462
+ ${this.headerExtras()}
463
+ </header>
464
+ <ul part="entries" role="list" hidden></ul>
465
+ <div part="empty" hidden></div>
466
+ <div part="actions" hidden></div>
467
+ <div part="composer" hidden>
468
+ <label part="composer-label" for="comment-thread-composer-input"></label>
469
+ <textarea
470
+ part="composer-input"
471
+ id="comment-thread-composer-input"
472
+ ></textarea>
473
+ <button type="button" part="composer-submit"></button>
474
+ </div>
475
+ </article>
476
+ `;
477
+ this.titleEl = this.shadowRoot.querySelector('[part="title"]');
478
+ this.messageEl = this.shadowRoot.querySelector('[part="message"]');
479
+ this.entriesEl = this.shadowRoot.querySelector('[part="entries"]');
480
+ this.emptyEl = this.shadowRoot.querySelector('[part="empty"]');
481
+ this.actionsEl = this.shadowRoot.querySelector('[part="actions"]');
482
+ this.composerEl = this.shadowRoot.querySelector('[part="composer"]');
483
+ this.composerLabelEl = this.shadowRoot.querySelector('[part="composer-label"]');
484
+ this.composerSubmitEl = this.shadowRoot.querySelector('[part="composer-submit"]');
485
+ this.composerInputEl = this.shadowRoot.querySelector('[part="composer-input"]');
486
+ }
487
+ setupListeners() {
488
+ this.entriesEl.addEventListener("click", event => {
489
+ const button = event.target.closest('[part="entry"]');
490
+ if (!button || !this.entriesEl.contains(button)) {
491
+ return;
492
+ }
493
+ const entryId = button.getAttribute("data-entry-id");
494
+ const entry = this.entries.find(item => item.id === entryId);
495
+ if (entry) {
496
+ this.emitEntrySelected(entry);
497
+ }
498
+ });
499
+ this.actionsEl.addEventListener("click", event => {
500
+ const button = event.target.closest('[part="action"]');
501
+ if (!button || !this.actionsEl.contains(button)) {
502
+ return;
503
+ }
504
+ const actionId = button.getAttribute("data-action-id");
505
+ if (actionId) {
506
+ this.emitAction(actionId);
507
+ }
508
+ });
509
+ this.composerEl.addEventListener("click", event => {
510
+ const button = event.target.closest('[part="composer-submit"]');
511
+ if (button) {
512
+ this.submitComposer();
513
+ }
514
+ });
515
+ this.composerInputEl.addEventListener("keydown", event => {
516
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
517
+ event.preventDefault();
518
+ this.submitComposer();
519
+ }
520
+ });
521
+ }
522
+ update() {
523
+ if (!this.entriesEl) {
524
+ return;
525
+ }
526
+ this.titleEl.textContent = this.heading;
527
+ this.messageEl.hidden = !this.message;
528
+ this.messageEl.textContent = this.message;
529
+ this.emptyEl.textContent = this.emptyText();
530
+ const composerLabel = this.composerLabel;
531
+ this.composerLabelEl.textContent = composerLabel;
532
+ this.composerSubmitEl.textContent = composerLabel;
533
+ this.composerInputEl.placeholder = this.placeholder;
534
+ const entries = this.entries;
535
+ this.entriesEl.hidden = entries.length === 0;
536
+ this.emptyEl.hidden = entries.length > 0;
537
+ this.entriesEl.setAttribute("aria-label", `${this.heading} entries`);
538
+ const nextEntries = this.entriesKey();
539
+ if (nextEntries !== this.entriesSignature) {
540
+ this.entriesSignature = nextEntries;
541
+ this.rebuildEntries();
542
+ }
543
+ else {
544
+ this.patchEntrySelection();
545
+ }
546
+ const actions = this.actions;
547
+ this.actionsEl.hidden = actions.length === 0;
548
+ const nextActions = this.actionsKey();
549
+ if (nextActions !== this.actionsSignature || this.actionsEl.childElementCount === 0) {
550
+ this.actionsSignature = nextActions;
551
+ this.rebuildActions();
552
+ }
553
+ else {
554
+ this.patchActionLabels();
555
+ }
556
+ const composable = this.composable;
557
+ this.composerEl.hidden = !composable;
558
+ this.composerInputEl.disabled = !composable;
559
+ }
560
+ }
561
+ CommentThread.register();
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export * from "./comment-thread.js";
@@ -0,0 +1,5 @@
1
+ // Comments as a standalone concept: the model, then the thread that renders it.
2
+ // `box-annotation-thread` (patterns/preview) composes this and adds the
3
+ // document anchor.
4
+ export * from "./types.js";
5
+ export * from "./comment-thread.js";