@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.
- package/dist/components/feedback/alert.js +2 -22
- package/dist/components/feedback/nudge.js +0 -1
- package/dist/components/feedback/path.d.ts +84 -0
- package/dist/components/feedback/path.js +455 -0
- package/dist/components/feedback/skeleton.d.ts +70 -0
- package/dist/components/feedback/skeleton.js +231 -1
- package/dist/components/feedback/toast.d.ts +37 -0
- package/dist/components/feedback/toast.js +182 -17
- package/dist/components/feedback/tone.d.ts +16 -0
- package/dist/components/feedback/tone.js +31 -0
- package/dist/components/layout/grid.d.ts +41 -0
- package/dist/components/layout/grid.js +152 -0
- package/dist/element-maps.d.ts +7 -3
- package/dist/entries/comment-thread.d.ts +1 -0
- package/dist/entries/comment-thread.js +1 -0
- package/dist/entries/grid.d.ts +1 -0
- package/dist/entries/grid.js +1 -0
- package/dist/entries/path.d.ts +1 -0
- package/dist/entries/path.js +1 -0
- package/dist/foundations/layout/grid.d.ts +70 -0
- package/dist/foundations/layout/grid.js +84 -0
- package/dist/foundations/layout/index.d.ts +1 -0
- package/dist/foundations/layout/index.js +3 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +6 -1
- package/dist/patterns/audit/audit-log.js +0 -1
- package/dist/patterns/comments/comment-thread.d.ts +79 -0
- package/dist/patterns/comments/comment-thread.js +561 -0
- package/dist/patterns/comments/index.d.ts +2 -0
- package/dist/patterns/comments/index.js +5 -0
- package/dist/patterns/comments/types.d.ts +39 -0
- package/dist/patterns/comments/types.js +8 -0
- package/dist/patterns/lineage/lineage-graph.d.ts +7 -0
- package/dist/patterns/lineage/lineage-graph.js +67 -6
- package/dist/patterns/lineage/provenance-strip.js +106 -15
- package/dist/patterns/notifications/notification-inbox.js +2 -18
- package/dist/patterns/preview/annotation-thread.d.ts +67 -48
- package/dist/patterns/preview/annotation-thread.js +96 -440
- package/dist/patterns/timeline/timeline.js +44 -12
- package/dist/patterns/versions/graph-arrows.d.ts +22 -0
- package/dist/patterns/versions/graph-arrows.js +13 -0
- package/dist/patterns/versions/index.d.ts +1 -0
- package/dist/patterns/versions/index.js +1 -0
- package/dist/patterns/versions/version-graph.d.ts +7 -0
- package/dist/patterns/versions/version-graph.js +54 -6
- package/package.json +1 -1
- package/dist/components/feedback/stage-path.d.ts +0 -44
- package/dist/components/feedback/stage-path.js +0 -235
- package/dist/entries/stage-path.d.ts +0 -1
- package/dist/entries/stage-path.js +0 -1
|
@@ -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("&", "&")
|
|
8
|
+
.replaceAll("<", "<")
|
|
9
|
+
.replaceAll(">", ">")
|
|
10
|
+
.replaceAll('"', """)
|
|
11
|
+
.replaceAll("'", "'");
|
|
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,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The comment model.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the component so the data contract does not depend on the
|
|
5
|
+
* class: a host that only transports comments — a controller, an adapter, a
|
|
6
|
+
* server route — imports these without pulling a custom element in with them.
|
|
7
|
+
*/
|
|
8
|
+
/** A button offered beneath the thread, e.g. "Resolve" or "Delete". */
|
|
9
|
+
export type CommentAction = {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
tone?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* One comment.
|
|
16
|
+
*
|
|
17
|
+
* `badge` is the generic pill beside the author — whatever short qualifier the
|
|
18
|
+
* surface wants to show. `box-annotation-thread` maps its own `toolLabel` onto
|
|
19
|
+
* it, which is why the base model does not name it after annotation tools.
|
|
20
|
+
*/
|
|
21
|
+
export type CommentEntry = {
|
|
22
|
+
author: string;
|
|
23
|
+
badge?: string;
|
|
24
|
+
body: string;
|
|
25
|
+
createdAt?: string;
|
|
26
|
+
id: string;
|
|
27
|
+
initials?: string;
|
|
28
|
+
status?: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* A composed comment, before it has an id.
|
|
32
|
+
*
|
|
33
|
+
* `inReplyToId` carries the selected entry, so the same event serves both a new
|
|
34
|
+
* top-level comment and a reply — the host decides which by reading it.
|
|
35
|
+
*/
|
|
36
|
+
export type CommentSubmittedDetail = {
|
|
37
|
+
body: string;
|
|
38
|
+
inReplyToId: string | null;
|
|
39
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The comment model.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the component so the data contract does not depend on the
|
|
5
|
+
* class: a host that only transports comments — a controller, an adapter, a
|
|
6
|
+
* server route — imports these without pulling a custom element in with them.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LineageNode } from "./types.js";
|
|
2
|
+
import type { GraphArrows } from "../versions/graph-arrows.js";
|
|
2
3
|
import { BaseElement } from "../../core/index.js";
|
|
3
4
|
/**
|
|
4
5
|
* Provenance DAG for clause lineage (opportunity 2): source clause →
|
|
@@ -13,6 +14,12 @@ import { BaseElement } from "../../core/index.js";
|
|
|
13
14
|
export declare class LineageGraph extends BaseElement {
|
|
14
15
|
static readonly tagName: string;
|
|
15
16
|
static get observedAttributes(): string[];
|
|
17
|
+
/**
|
|
18
|
+
* Which ends of each edge carry an arrowhead. Defaults to `end`, the head on
|
|
19
|
+
* the derived node, because the layout builds edges source → derived.
|
|
20
|
+
*/
|
|
21
|
+
get arrows(): GraphArrows;
|
|
22
|
+
set arrows(value: GraphArrows);
|
|
16
23
|
private nodesValue;
|
|
17
24
|
private rovingId;
|
|
18
25
|
private hostEl;
|