@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.
- package/dist/components/feedback/alert.d.ts +2 -0
- package/dist/components/feedback/alert.js +41 -8
- package/dist/components/feedback/indicator.d.ts +34 -0
- package/dist/components/feedback/indicator.js +167 -0
- package/dist/components/feedback/nudge.js +0 -1
- package/dist/components/feedback/toast.d.ts +4 -5
- package/dist/components/feedback/toast.js +19 -46
- package/dist/components/feedback/tone.d.ts +18 -0
- package/dist/components/feedback/tone.js +23 -0
- package/dist/components/forms/calendar.d.ts +25 -0
- package/dist/components/forms/calendar.js +94 -2
- package/dist/components/forms/tile-group.d.ts +58 -0
- package/dist/components/forms/tile-group.js +280 -0
- package/dist/components/output/code-block.d.ts +49 -0
- package/dist/components/output/code-block.js +236 -0
- package/dist/components/output/formatted-date.d.ts +38 -0
- package/dist/components/output/formatted-date.js +90 -0
- package/dist/components/output/formatted-file-size.d.ts +31 -0
- package/dist/components/output/formatted-file-size.js +61 -0
- package/dist/components/output/formatted-number.d.ts +37 -0
- package/dist/components/output/formatted-number.js +106 -0
- package/dist/components/output/formatted-value.d.ts +44 -0
- package/dist/components/output/formatted-value.js +100 -0
- package/dist/components/output/index.d.ts +6 -0
- package/dist/components/output/index.js +9 -0
- package/dist/components/output/relative-time.d.ts +41 -0
- package/dist/components/output/relative-time.js +82 -0
- package/dist/element-maps.d.ts +17 -1
- package/dist/entries/code-block.d.ts +1 -0
- package/dist/entries/code-block.js +1 -0
- package/dist/entries/comment-thread.d.ts +1 -0
- package/dist/entries/comment-thread.js +1 -0
- package/dist/entries/formatted-date.d.ts +1 -0
- package/dist/entries/formatted-date.js +1 -0
- package/dist/entries/formatted-file-size.d.ts +1 -0
- package/dist/entries/formatted-file-size.js +1 -0
- package/dist/entries/formatted-number.d.ts +1 -0
- package/dist/entries/formatted-number.js +1 -0
- package/dist/entries/indicator.d.ts +1 -0
- package/dist/entries/indicator.js +1 -0
- package/dist/entries/relative-time.d.ts +1 -0
- package/dist/entries/relative-time.js +1 -0
- package/dist/entries/tile-group.d.ts +1 -0
- package/dist/entries/tile-group.js +1 -0
- package/dist/foundations/format/format.d.ts +82 -0
- package/dist/foundations/format/format.js +122 -0
- package/dist/foundations/format/index.d.ts +1 -0
- package/dist/foundations/format/index.js +3 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +12 -0
- 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/preview/annotation-thread.d.ts +67 -48
- package/dist/patterns/preview/annotation-thread.js +96 -440
- 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("&", "&")
|
|
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();
|