@fusion.dev/helix-headless 0.2.0 → 0.4.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/custom-elements.json +234 -234
- package/dist/accordion/item/accordion-item.js +0 -108
- package/dist/accordion/root/accordion-root.js +0 -33
- package/dist/announcement/root/announcement-root.js +1 -76
- package/dist/app-header/root/app-header-root.js +0 -84
- package/dist/avatar/group/avatar-group.js +0 -77
- package/dist/avatar/root/avatar-root.js +0 -72
- package/dist/badge/root/badge-root.js +0 -67
- package/dist/bar-chart/root/bar-chart-root.js +3 -263
- package/dist/blockquote/root/blockquote-root.js +0 -46
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
- package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
- package/dist/button/root/button-root.js +0 -215
- package/dist/button-group/root/button-group-root.js +0 -30
- package/dist/callout/root/callout-root.js +0 -52
- package/dist/card/root/card-root.js +0 -41
- package/dist/carousel/root/carousel-root.d.ts +2 -2
- package/dist/carousel/root/carousel-root.js +2 -362
- package/dist/chat/composer/chat-composer.js +0 -131
- package/dist/chat/message/chat-message.js +0 -137
- package/dist/chat/root/chat-root.js +0 -193
- package/dist/checkbox/root/checkbox-root.js +0 -113
- package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
- package/dist/code/root/code-root.js +0 -26
- package/dist/code-block/root/code-block-root.js +0 -100
- package/dist/color-input/root/color-input-root.js +0 -217
- package/dist/color-picker/color.js +0 -69
- package/dist/color-picker/root/color-picker-root.js +0 -272
- package/dist/combobox/root/combobox-root.js +0 -428
- package/dist/command/root/command-root.js +0 -177
- package/dist/copy-button/root/copy-button-root.js +0 -134
- package/dist/data-table/root/data-table-root.js +0 -336
- package/dist/data-table/root/order.js +0 -100
- package/dist/date-input/root/date-input-root.js +1 -398
- package/dist/date-picker/root/date-picker-root.js +0 -358
- package/dist/dialog/root/dialog-root.js +0 -189
- package/dist/divider/root/divider-root.js +0 -71
- package/dist/donut-chart/root/donut-chart-root.js +0 -214
- package/dist/drawer/root/drawer-root.js +0 -116
- package/dist/drilldown/item/drilldown-item.js +0 -82
- package/dist/drilldown/root/drilldown-root.js +0 -91
- package/dist/element-events.generated.js +0 -5
- package/dist/empty/root/empty-root.js +0 -35
- package/dist/file-input/root/file-input-root.js +0 -232
- package/dist/format-date/root/format-date-root.js +0 -95
- package/dist/heatmap/root/buckets.js +0 -57
- package/dist/heatmap/root/heatmap-root.js +0 -313
- package/dist/icon/root/icon-root.js +0 -48
- package/dist/icon-button/root/icon-button-root.js +0 -30
- package/dist/image/root/image-root.js +0 -174
- package/dist/index.js +0 -9
- package/dist/input/root/input-root.js +0 -228
- package/dist/kbd/root/kbd-root.js +0 -117
- package/dist/legend/root/legend-root.js +0 -73
- package/dist/line-chart/root/line-chart-root.js +0 -172
- package/dist/list/item/list-item.js +0 -92
- package/dist/list/root/list-root.js +0 -26
- package/dist/listbox/root/listbox-root.js +2 -429
- package/dist/loader/root/loader-root.js +0 -40
- package/dist/megamenu/root/megamenu-root.js +0 -284
- package/dist/menu/item/menu-item.js +1 -221
- package/dist/menu/root/menu-root.js +1 -171
- package/dist/meter/root/meter-root.js +1 -152
- package/dist/navigation/root/navigation-root.js +0 -56
- package/dist/notifications/notification/notification.js +0 -159
- package/dist/notifications/root/notifications-root.js +0 -148
- package/dist/number-input/root/number-input-root.js +1 -263
- package/dist/option/group/option-group.js +0 -26
- package/dist/option/root/option-root.js +0 -157
- package/dist/outline/root/outline-root.js +0 -292
- package/dist/pagination/root/pagination-root.js +0 -361
- package/dist/pagination/size/pagination-size.js +0 -104
- package/dist/pagination/summary/pagination-summary.js +0 -90
- package/dist/pin-input/root/pin-input-root.js +0 -276
- package/dist/popover/root/popover-root.js +0 -103
- package/dist/progress/root/progress-root.js +1 -133
- package/dist/qr-code/root/qr-code-root.js +0 -96
- package/dist/quote/root/quote-root.js +0 -25
- package/dist/radio/group/radio-group.js +0 -81
- package/dist/radio/root/radio-root.js +0 -85
- package/dist/rating/root/rating-root.js +0 -220
- package/dist/scroll-area/root/scroll-area-root.js +0 -100
- package/dist/select/root/select-root.js +2 -218
- package/dist/skeleton/root/skeleton-root.js +0 -20
- package/dist/slider/root/slider-root.js +1 -267
- package/dist/sparkline/root/sparkline-root.js +3 -164
- package/dist/speed-dial/item/speed-dial-item.js +0 -52
- package/dist/speed-dial/root/speed-dial-root.js +1 -116
- package/dist/splitter/root/splitter-root.js +0 -489
- package/dist/stat/group/stat-group.js +0 -18
- package/dist/stat/root/stat-root.js +0 -23
- package/dist/steps/root/steps-root.js +0 -62
- package/dist/steps/step/step.js +0 -131
- package/dist/switch/root/switch-root.js +0 -80
- package/dist/table/cell/table-cell.js +0 -23
- package/dist/table/column/table-column.js +0 -158
- package/dist/table/root/table-root.js +0 -82
- package/dist/table/row/table-row.js +0 -21
- package/dist/tabs/panel/tab-panel.js +0 -49
- package/dist/tabs/root/tabs-root.js +0 -98
- package/dist/tabs/tab/tab.js +0 -59
- package/dist/tag/root/tag-root.js +0 -31
- package/dist/textarea/root/textarea-root.js +0 -162
- package/dist/time-input/root/time-input-root.js +0 -283
- package/dist/time-picker/root/time-picker-root.js +0 -326
- package/dist/timeline/item/timeline-item.js +0 -66
- package/dist/timeline/root/timeline-root.js +0 -41
- package/dist/toast/item/toast-item.js +0 -165
- package/dist/toast/root/toast-root.js +0 -51
- package/dist/toggle/group/toggle-group.js +0 -150
- package/dist/toggle/root/toggle-root.js +0 -78
- package/dist/toolbar/root/toolbar-root.js +0 -116
- package/dist/tooltip/root/tooltip-root.js +0 -427
- package/dist/tour/root/tour-root.js +0 -130
- package/dist/tour/stop/tour-stop.js +1 -127
- package/dist/tree/item/tree-item.js +0 -198
- package/dist/tree/root/tree-root.js +0 -97
- package/dist/trend/root/trend-root.js +0 -53
- package/dist/video/root/video-root.d.ts +8 -12
- package/dist/video/root/video-root.js +5 -756
- package/dist/video-playlist/item/video-playlist-item.js +1 -90
- package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
- package/dist/video-playlist/root/video-playlist-root.js +0 -113
- package/dist/virtual-list/root/virtual-list-root.js +0 -105
- package/jsx.d.ts +1408 -2576
- package/package.json +6 -6
- package/vue.d.ts +1 -1
|
@@ -9,97 +9,18 @@ import { arrowElbowDownLeft } from '@fusion.dev/helix-icons';
|
|
|
9
9
|
import { html } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
11
11
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
12
|
-
/**
|
|
13
|
-
* @summary Where the reader writes their turn.
|
|
14
|
-
*
|
|
15
|
-
* @description
|
|
16
|
-
* **A separate element because it is separately replaceable.** A consumer with
|
|
17
|
-
* attachments, slash commands or a model picker replaces the composer and keeps
|
|
18
|
-
* the transcript, which is only possible if the two were never one component.
|
|
19
|
-
*
|
|
20
|
-
* **Enter sends and Shift+Enter opens a line, and it is configurable rather than
|
|
21
|
-
* decided.** The convention is near-universal in messaging and near-inverted in
|
|
22
|
-
* anything that is really a document editor, and a component that picks one
|
|
23
|
-
* makes the other consumer reimplement the composer. `submit-on` takes `enter`
|
|
24
|
-
* or `button`, and the second is what a long-form composer wants.
|
|
25
|
-
*
|
|
26
|
-
* The composer clears only when nobody refused the send, because a composer that
|
|
27
|
-
* empties itself and then hears the request failed has thrown away the only copy
|
|
28
|
-
* of what the reader wrote.
|
|
29
|
-
*
|
|
30
|
-
* @since 0.1.0
|
|
31
|
-
* @status experimental
|
|
32
|
-
* @category forms
|
|
33
|
-
* @aliases Message box, Prompt input
|
|
34
|
-
* @documentation https://helix.fusion.dev/components/chat
|
|
35
|
-
*
|
|
36
|
-
* @slot end - Under the field, beside the send control.
|
|
37
|
-
* @slot start - Under the field, at the leading edge: attachments, a model picker.
|
|
38
|
-
*
|
|
39
|
-
* @event {HelixSendEvent} helix-send - Fired when the reader offers their turn, and cancelable.
|
|
40
|
-
*
|
|
41
|
-
* @csspart composer - The whole composer.
|
|
42
|
-
* @csspart end - The trailing slot's box.
|
|
43
|
-
* @csspart field - The text field.
|
|
44
|
-
* @csspart mark - The glyph inside the send control.
|
|
45
|
-
* @csspart send - The control that offers the turn.
|
|
46
|
-
* @csspart start - The leading slot's box.
|
|
47
|
-
* @csspart tools - The row under the field.
|
|
48
|
-
*/
|
|
49
12
|
let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixElement {
|
|
50
13
|
constructor() {
|
|
51
14
|
super(...arguments);
|
|
52
|
-
/**
|
|
53
|
-
* Whether the composer refuses input, while a turn is in flight say.
|
|
54
|
-
*/
|
|
55
15
|
this.disabled = false;
|
|
56
|
-
/**
|
|
57
|
-
* Accessible name for the field.
|
|
58
|
-
*/
|
|
59
16
|
this.label = 'Message';
|
|
60
|
-
/**
|
|
61
|
-
* Shown in an empty field.
|
|
62
|
-
*/
|
|
63
17
|
this.placeholder = '';
|
|
64
|
-
/**
|
|
65
|
-
* How many rows the field opens at.
|
|
66
|
-
*
|
|
67
|
-
* It grows from there.
|
|
68
|
-
*/
|
|
69
18
|
this.rows = 1;
|
|
70
|
-
/**
|
|
71
|
-
* Label for the control that sends.
|
|
72
|
-
*/
|
|
73
19
|
this.sendLabel = 'Send';
|
|
74
|
-
/**
|
|
75
|
-
* What sends: pressing Enter, or only the control.
|
|
76
|
-
*
|
|
77
|
-
* `enter` is the messaging convention and makes Shift+Enter the line break.
|
|
78
|
-
* `button` is what a composer holding several paragraphs wants, where Enter
|
|
79
|
-
* has to mean a new line and nothing else.
|
|
80
|
-
*/
|
|
81
20
|
this.submitOn = 'enter';
|
|
82
|
-
/**
|
|
83
|
-
* What the reader has written.
|
|
84
|
-
*
|
|
85
|
-
* Read it, or set it to seed a draft.
|
|
86
|
-
*/
|
|
87
21
|
this.value = '';
|
|
88
22
|
this.draft = '';
|
|
89
|
-
/**
|
|
90
|
-
* @internal Read by the transcript to find the composer among its children.
|
|
91
|
-
*/
|
|
92
23
|
this.isChatComposer = true;
|
|
93
|
-
/**
|
|
94
|
-
* Records the draft, and stops the textarea speaking in this element's name.
|
|
95
|
-
*
|
|
96
|
-
* **The stop is not incidental.** `input` is composed by specification, so the
|
|
97
|
-
* `<textarea>` in this shadow root sent its own past this host and on through
|
|
98
|
-
* whatever `<helix-chat>` it is slotted into, and neither element declares an
|
|
99
|
-
* `input`: a consumer binding one on either received an undeclared event whose
|
|
100
|
-
* target was a control they cannot reach. What a composer offers is
|
|
101
|
-
* `helix-send`, once, when the reader is done.
|
|
102
|
-
*/
|
|
103
24
|
this.handleInput = (event) => {
|
|
104
25
|
event.stopPropagation();
|
|
105
26
|
this.draft = event.target.value;
|
|
@@ -107,15 +28,6 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixEle
|
|
|
107
28
|
this.handleSend = () => {
|
|
108
29
|
this.send();
|
|
109
30
|
};
|
|
110
|
-
/**
|
|
111
|
-
* Enter sends where that is what `submit-on` says, and never mid-composition.
|
|
112
|
-
*
|
|
113
|
-
* **`isComposing` is the part that is easy to leave out and breaks a whole
|
|
114
|
-
* writing system.** A reader typing Japanese, Chinese or Korean presses Enter
|
|
115
|
-
* to accept the candidate their input method is offering, and a composer that
|
|
116
|
-
* reads it as send fires off a half-finished word every time. The flag is on
|
|
117
|
-
* the event for exactly this and costs one condition.
|
|
118
|
-
*/
|
|
119
31
|
this.handleKey = (event) => {
|
|
120
32
|
if (event.key !== 'Enter' || this.submitOn !== 'enter') {
|
|
121
33
|
return;
|
|
@@ -127,17 +39,9 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixEle
|
|
|
127
39
|
this.send();
|
|
128
40
|
};
|
|
129
41
|
}
|
|
130
|
-
/** What is in the field, whoever last wrote it. */
|
|
131
42
|
get text() {
|
|
132
43
|
return this.draft || this.value;
|
|
133
44
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Offers what is written, and reports whether it stood.
|
|
136
|
-
*
|
|
137
|
-
* Public because a consumer's own send control is as likely to ask as this
|
|
138
|
-
* element's is, and because a keyboard shortcut somewhere else on the page is
|
|
139
|
-
* a reasonable way to send.
|
|
140
|
-
*/
|
|
141
45
|
send() {
|
|
142
46
|
const value = this.text.trim();
|
|
143
47
|
if (value === '' || this.disabled) {
|
|
@@ -146,21 +50,10 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixEle
|
|
|
146
50
|
if (!this.dispatchEvent(new HelixSendEvent({ value }))) {
|
|
147
51
|
return false;
|
|
148
52
|
}
|
|
149
|
-
/* Cleared only here, after nobody refused. A composer that empties itself
|
|
150
|
-
first and hears about the failure afterwards has thrown away the only copy
|
|
151
|
-
of what the reader wrote. */
|
|
152
53
|
this.draft = '';
|
|
153
54
|
this.value = '';
|
|
154
55
|
return true;
|
|
155
56
|
}
|
|
156
|
-
/**
|
|
157
|
-
* Says on each tool box whether anything is in it.
|
|
158
|
-
*
|
|
159
|
-
* They are boxes in a flex row, so an empty one still takes the gap beside it
|
|
160
|
-
* and the send control drifts inward by one for every slot a consumer left
|
|
161
|
-
* alone. No selector can ask this: a slot's assigned nodes are not its DOM
|
|
162
|
-
* children, so `:empty` describes the fallback rather than the fill.
|
|
163
|
-
*/
|
|
164
57
|
markEmpty() {
|
|
165
58
|
for (const name of ['start', 'end']) {
|
|
166
59
|
const region = this.renderRoot?.querySelector(`[part="${name}"]`);
|
|
@@ -177,17 +70,6 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixEle
|
|
|
177
70
|
super.willUpdate(changed);
|
|
178
71
|
hostAria(this.internals, { label: this.label, role: 'group' });
|
|
179
72
|
}
|
|
180
|
-
/**
|
|
181
|
-
* The control that offers the turn, behind a seam so a tier can draw its own.
|
|
182
|
-
*
|
|
183
|
-
* **A glyph with the label as its accessible name, rather than the label as
|
|
184
|
-
* text.** The word "Send" beside a field is the widest thing in the row and
|
|
185
|
-
* says nothing a return key does not. The return glyph is the mark because
|
|
186
|
-
* Enter is what actually sends, which `submit-on` says out loud: the control
|
|
187
|
-
* and the key are one affordance and should look like it. `send-label` is the
|
|
188
|
-
* accessible name rather than a visible one, so a screen reader still hears
|
|
189
|
-
* the word.
|
|
190
|
-
*/
|
|
191
73
|
renderSend() {
|
|
192
74
|
return html `<button
|
|
193
75
|
part="send"
|
|
@@ -206,19 +88,6 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixEle
|
|
|
206
88
|
</svg>
|
|
207
89
|
</button>`;
|
|
208
90
|
}
|
|
209
|
-
/**
|
|
210
|
-
* The field, and a row of tools under it.
|
|
211
|
-
*
|
|
212
|
-
* **The tools take a line of their own rather than sharing the field's.** On
|
|
213
|
-
* one line every tool a consumer adds takes width away from the thing being
|
|
214
|
-
* typed into; under it, the field spans the whole composer at every width and
|
|
215
|
-
* the tools have room to grow along.
|
|
216
|
-
*
|
|
217
|
-
* Send is at the trailing end of that line rather than in the field, which is
|
|
218
|
-
* the same corner and costs no overlay: a control positioned inside a
|
|
219
|
-
* scrolling textarea either covers the last line of a long draft or scrolls
|
|
220
|
-
* away with it.
|
|
221
|
-
*/
|
|
222
91
|
render() {
|
|
223
92
|
return html `<div part="composer">
|
|
224
93
|
<textarea
|
|
@@ -8,103 +8,24 @@ import { customElement, HelixElement, HelixSendEvent, hostAria, relativeTime, sl
|
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property, state } from 'lit/decorators.js';
|
|
10
10
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
11
|
-
/**
|
|
12
|
-
* @summary One turn in a conversation, and what has happened to it.
|
|
13
|
-
*
|
|
14
|
-
* @description
|
|
15
|
-
* **The states are the component.** Bubbles are paint; what a chat message has
|
|
16
|
-
* to carry is whether it is on its way, arrived, still arriving, or failed, and
|
|
17
|
-
* a failed message needs a retry that is a real action rather than a toast the
|
|
18
|
-
* reader has to catch.
|
|
19
|
-
*
|
|
20
|
-
* `streaming` is the state everything else is arranged around. A message
|
|
21
|
-
* arriving token by token changes height on every frame, so it is marked
|
|
22
|
-
* `aria-busy` while it arrives and announced once when it settles: a screen
|
|
23
|
-
* reader reading a stream character by character is unusable, and announcing on
|
|
24
|
-
* completion is the only version of this that works.
|
|
25
|
-
*
|
|
26
|
-
* @since 0.1.0
|
|
27
|
-
* @status experimental
|
|
28
|
-
* @category feedback
|
|
29
|
-
* @aliases Message, Bubble, Turn
|
|
30
|
-
* @documentation https://helix.fusion.dev/components/chat
|
|
31
|
-
*
|
|
32
|
-
* @slot - What was said.
|
|
33
|
-
* @slot actions - What can be done with it: copy, retry, react.
|
|
34
|
-
* @slot avatar - Who said it.
|
|
35
|
-
* @slot sources - What it was said from: citations, documents, a retrieved page.
|
|
36
|
-
*
|
|
37
|
-
* @event {HelixSendEvent} helix-send - Fired when the reader asks to send a failed message again, and cancelable.
|
|
38
|
-
*
|
|
39
|
-
* @csspart actions - The controls offered on the turn, beside its time.
|
|
40
|
-
* @csspart avatar - The leading slot's box.
|
|
41
|
-
* @csspart body - What was said, and the line under it.
|
|
42
|
-
* @csspart content - The consumer's own markup.
|
|
43
|
-
* @csspart mark - The status glyph beside a failure.
|
|
44
|
-
* @csspart message - The whole turn.
|
|
45
|
-
* @csspart meta - The line under a turn: its time, its state, and its controls.
|
|
46
|
-
* @csspart retry - The control offered on a failed message.
|
|
47
|
-
* @csspart sources - What the turn was drawn from, under it and outside the bubble.
|
|
48
|
-
* @csspart state - What has happened to it, where that is worth saying.
|
|
49
|
-
* @csspart time - When it was said.
|
|
50
|
-
*/
|
|
51
11
|
let HelixHeadlessChatMessage = class HelixHeadlessChatMessage extends HelixElement {
|
|
52
12
|
constructor() {
|
|
53
13
|
super(...arguments);
|
|
54
|
-
/**
|
|
55
|
-
* When it was said, as anything `Date` parses.
|
|
56
|
-
*/
|
|
57
14
|
this.at = '';
|
|
58
|
-
/**
|
|
59
|
-
* Label for a message that did not send.
|
|
60
|
-
*/
|
|
61
15
|
this.failedLabel = 'Not sent';
|
|
62
|
-
/**
|
|
63
|
-
* Locale for the timestamp.
|
|
64
|
-
*
|
|
65
|
-
* Empty is the runtime's own.
|
|
66
|
-
*/
|
|
67
16
|
this.locale = '';
|
|
68
|
-
/**
|
|
69
|
-
* Label for the control offered on a failed message.
|
|
70
|
-
*/
|
|
71
17
|
this.retryLabel = 'Try again';
|
|
72
|
-
/**
|
|
73
|
-
* What to call each state, for a reader who cannot see it.
|
|
74
|
-
*/
|
|
75
18
|
this.sendingLabel = 'Sending';
|
|
76
|
-
/**
|
|
77
|
-
* @internal Read by the transcript to identify its children across the light
|
|
78
|
-
* DOM, which is cheaper than `instanceof`. Not a consumer's to read or set.
|
|
79
|
-
*/
|
|
80
19
|
this.isChatMessage = true;
|
|
81
20
|
this.clock = undefined;
|
|
82
|
-
/**
|
|
83
|
-
* A turn's own content arrives and changes no property here.
|
|
84
|
-
*
|
|
85
|
-
* It is the streaming case that needs this rather than a tidiness one: the
|
|
86
|
-
* first token of an answer is a light-DOM child appearing under an element
|
|
87
|
-
* nothing else told to look again, and it is exactly the moment the typing
|
|
88
|
-
* indicator has to stop being drawn.
|
|
89
|
-
*/
|
|
90
21
|
this.handleFill = () => {
|
|
91
22
|
this.markEmpty();
|
|
92
23
|
};
|
|
93
|
-
/**
|
|
94
|
-
* Retry is `helix-send` again, from the message rather than the composer.
|
|
95
|
-
*
|
|
96
|
-
* **Not an event of its own**, because a retry is the same request: the reader
|
|
97
|
-
* is offering what they wrote, and the only difference is which element is
|
|
98
|
-
* offering it. A listener that wants to tell them apart has `event.target`,
|
|
99
|
-
* which is the message on a retry and the composer on a first send, and that
|
|
100
|
-
* is a distinction the DOM already makes without a second name for it.
|
|
101
|
-
*/
|
|
102
24
|
this.handleRetry = (event) => {
|
|
103
25
|
event.stopPropagation();
|
|
104
26
|
this.dispatchEvent(new HelixSendEvent({ value: this.textContent?.trim() ?? '' }));
|
|
105
27
|
};
|
|
106
28
|
}
|
|
107
|
-
/** The timestamp, or nothing where there is not one to read. */
|
|
108
29
|
get moment() {
|
|
109
30
|
if (this.at === '') {
|
|
110
31
|
return undefined;
|
|
@@ -117,13 +38,6 @@ let HelixHeadlessChatMessage = class HelixHeadlessChatMessage extends HelixEleme
|
|
|
117
38
|
if (changed.has('at')) {
|
|
118
39
|
this.clock = Date.now();
|
|
119
40
|
}
|
|
120
|
-
/* `article` rather than `listitem`, because the transcript is a `log` and a
|
|
121
|
-
log's children are not list items: a turn is a self-contained thing a
|
|
122
|
-
reader can be moved to, which is what an article is for.
|
|
123
|
-
|
|
124
|
-
`aria-busy` while it arrives is the whole of the streaming contract on
|
|
125
|
-
this side: the live region above it holds its announcement until the flag
|
|
126
|
-
clears, so the message is read once, whole, rather than per token. */
|
|
127
41
|
hostAria(this.internals, { role: 'article' });
|
|
128
42
|
this.toggleAttribute('aria-busy', this.state === 'streaming');
|
|
129
43
|
}
|
|
@@ -135,16 +49,6 @@ let HelixHeadlessChatMessage = class HelixHeadlessChatMessage extends HelixEleme
|
|
|
135
49
|
const { text } = relativeTime(this.locale)(moment, new Date(this.clock ?? Date.now()));
|
|
136
50
|
return html `<time part="time" datetime=${moment.toISOString()}>${text}</time>`;
|
|
137
51
|
}
|
|
138
|
-
/**
|
|
139
|
-
* The status glyph, behind a seam so a tier can draw its own.
|
|
140
|
-
*
|
|
141
|
-
* `TONE_MARKS.danger` rather than a glyph chosen here: the library has one
|
|
142
|
-
* status vocabulary, `Callout`, `Toast.Item` and `Announcement` all read from
|
|
143
|
-
* it, and a reader meets these four marks in a passage, in a message and now
|
|
144
|
-
* on a turn and has to read them as the same four things. It carries the
|
|
145
|
-
* accessible name with it, which is the half that matters: `danger` is the
|
|
146
|
-
* word the API uses and "Error" is the word a reader understands.
|
|
147
|
-
*/
|
|
148
52
|
renderMark() {
|
|
149
53
|
const { icon, label } = TONE_MARKS.danger;
|
|
150
54
|
return html `<svg
|
|
@@ -157,14 +61,6 @@ let HelixHeadlessChatMessage = class HelixHeadlessChatMessage extends HelixEleme
|
|
|
157
61
|
${unsafeSVG(icon.content)}
|
|
158
62
|
</svg>`;
|
|
159
63
|
}
|
|
160
|
-
/**
|
|
161
|
-
* What has happened to it, in words, and the way back from a failure.
|
|
162
|
-
*
|
|
163
|
-
* A seam of its own, because the styled tier draws a failure as an icon and a
|
|
164
|
-
* consumer may want neither. `sent` says nothing: every settled message is
|
|
165
|
-
* sent, and a transcript that labels each one is a transcript of the word
|
|
166
|
-
* "sent".
|
|
167
|
-
*/
|
|
168
64
|
renderState() {
|
|
169
65
|
if (this.state === 'sending') {
|
|
170
66
|
return html `<span part="state">${this.sendingLabel}</span>`;
|
|
@@ -172,26 +68,11 @@ let HelixHeadlessChatMessage = class HelixHeadlessChatMessage extends HelixEleme
|
|
|
172
68
|
if (this.state !== 'failed') {
|
|
173
69
|
return nothing;
|
|
174
70
|
}
|
|
175
|
-
/* **The mark, and not because color needs a second carrier here.** "Not
|
|
176
|
-
sent" is words, so the meaning survives without it. It is here because a
|
|
177
|
-
failure is the one state that has to be *found* rather than noticed, and a
|
|
178
|
-
shape is what a reader scanning a transcript finds. */
|
|
179
71
|
return html `<span part="state"
|
|
180
72
|
>${this.renderMark()} ${this.failedLabel}
|
|
181
73
|
<button part="retry" type="button" @click=${this.handleRetry}>${this.retryLabel}</button>
|
|
182
74
|
</span>`;
|
|
183
75
|
}
|
|
184
|
-
/**
|
|
185
|
-
* Says on each region whether anything is in it.
|
|
186
|
-
*
|
|
187
|
-
* Three of them need the answer and no selector can give it: a slot's assigned
|
|
188
|
-
* nodes are not its DOM children, so `:empty` and `:has(*)` both describe the
|
|
189
|
-
* fallback rather than the fill. `sources` and `actions` are regions that cost
|
|
190
|
-
* a gap when they are boxes at all, which most turns should not pay. `content`
|
|
191
|
-
* is the one that is not about space: a turn that is `streaming` with nothing
|
|
192
|
-
* in it yet is what the typing indicator is drawn for, and "nothing in it yet"
|
|
193
|
-
* is exactly this question.
|
|
194
|
-
*/
|
|
195
76
|
markEmpty() {
|
|
196
77
|
for (const name of ['content', 'sources', 'actions']) {
|
|
197
78
|
const region = this.renderRoot?.querySelector(`[part="${name}"]`);
|
|
@@ -200,8 +81,6 @@ let HelixHeadlessChatMessage = class HelixHeadlessChatMessage extends HelixEleme
|
|
|
200
81
|
: this.renderRoot?.querySelector(`slot[name="${name}"]`);
|
|
201
82
|
const empty = !slotFilled(slot);
|
|
202
83
|
region?.toggleAttribute('data-empty', empty);
|
|
203
|
-
/* `content` keeps its box when empty, because the typing indicator is
|
|
204
|
-
painted into it. The other two are gaps nobody asked for. */
|
|
205
84
|
if (name !== 'content') {
|
|
206
85
|
region?.toggleAttribute('hidden', empty);
|
|
207
86
|
}
|
|
@@ -211,22 +90,6 @@ let HelixHeadlessChatMessage = class HelixHeadlessChatMessage extends HelixEleme
|
|
|
211
90
|
super.updated(changed);
|
|
212
91
|
this.markEmpty();
|
|
213
92
|
}
|
|
214
|
-
/**
|
|
215
|
-
* The turn: who said it, what was said, what it was drawn from, and its line.
|
|
216
|
-
*
|
|
217
|
-
* **`sources` and `actions` are inside the body rather than beside it.**
|
|
218
|
-
* `actions` as a third grid column puts a copy control on a fifteen-word turn
|
|
219
|
-
* at the container's trailing edge, a hundred and fifty pixels of nothing
|
|
220
|
-
* between it and the bubble it acts on, on both sides of the conversation. As
|
|
221
|
-
* a column of its own inside the body it is no better: a bubble spanning two
|
|
222
|
-
* auto tracks inflates both, so the controls land under the middle of the
|
|
223
|
-
* turn.
|
|
224
|
-
*
|
|
225
|
-
* So they are inside `meta`, which is the line under a turn rather than only
|
|
226
|
-
* its timestamp, and that line is one flex row: time, state, controls, all
|
|
227
|
-
* hugging whichever edge the turn is against. Nothing has to be sized against
|
|
228
|
-
* anything else for that to hold at every width.
|
|
229
|
-
*/
|
|
230
93
|
render() {
|
|
231
94
|
return html `<div part="message">
|
|
232
95
|
<span part="avatar"><slot name="avatar"></slot></span>
|
|
@@ -7,125 +7,16 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixBeforePinEvent, HelixElement, hostAria, MixinCollection, requestChange, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property, state } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* The regions a consumer fills, in the order they are rendered.
|
|
12
|
-
*
|
|
13
|
-
* Named once because two things read the list: the manual assignment that puts
|
|
14
|
-
* a child in the region its `slot` names, and the emptiness each region is
|
|
15
|
-
* hidden on. Written out twice they go out of step.
|
|
16
|
-
*/
|
|
17
10
|
const REGIONS = ['header', 'actions', 'resume', 'status', 'composer'];
|
|
18
|
-
/**
|
|
19
|
-
* @summary A conversation: the turns, and where the reader is in them.
|
|
20
|
-
*
|
|
21
|
-
* @description
|
|
22
|
-
* **It has no opinion about where it sits.** A chat is asked for as a floating
|
|
23
|
-
* button, a page, a palette, a sidebar, a modal and an inline block, and none of
|
|
24
|
-
* those is a variant of a conversation: they are containers a conversation is
|
|
25
|
-
* put in, and the containers already exist. `Popover` and `SpeedDial` make the
|
|
26
|
-
* floating one, `Drawer` the sidebar, `Dialog` the modal, `Command` the palette,
|
|
27
|
-
* and a page is a page. Nothing here is positioned, nothing here has a backdrop,
|
|
28
|
-
* and nothing here owns a trigger, because a trigger opens a container and
|
|
29
|
-
* containers own their own. If a form factor needs a mode on this element, the
|
|
30
|
-
* split is wrong.
|
|
31
|
-
*
|
|
32
|
-
* **Scroll ownership is the behavior everything else depends on, and it is the
|
|
33
|
-
* most-got-wrong thing in the category.** A transcript that grows wants to stay
|
|
34
|
-
* at the bottom, except when the reader has scrolled up to read, where pinning
|
|
35
|
-
* is hostile. The rule: pin while the reader is within `threshold` of the
|
|
36
|
-
* bottom, release the moment they leave it, and offer a way back. **Never scroll
|
|
37
|
-
* a reader who is reading.**
|
|
38
|
-
*
|
|
39
|
-
* The element owns it, because it is the only party that knows a message arrived
|
|
40
|
-
* and where the viewport was when it did. `pinned` announces before it moves, so
|
|
41
|
-
* a consumer who wants to own it refuses the request and assigns their own.
|
|
42
|
-
*
|
|
43
|
-
* **The live region announces on completion, not on arrival.** The transcript is
|
|
44
|
-
* `role="log"` with `aria-live="polite"`, and a streaming message carries
|
|
45
|
-
* `aria-busy` while it arrives: a screen reader reading a stream token by token
|
|
46
|
-
* is unusable, and holding the announcement until the flag clears is the only
|
|
47
|
-
* arrangement that works. Somebody typing is a separate status region, so it
|
|
48
|
-
* never interleaves with what was said.
|
|
49
|
-
*
|
|
50
|
-
* **Not a client.** No transport, no protocol, no model. It renders a
|
|
51
|
-
* conversation and reports what the reader did.
|
|
52
|
-
*
|
|
53
|
-
* @since 0.1.0
|
|
54
|
-
* @status experimental
|
|
55
|
-
* @category feedback
|
|
56
|
-
* @aliases Conversation, Transcript, Thread, Messages
|
|
57
|
-
* @documentation https://helix.fusion.dev/components/chat
|
|
58
|
-
*
|
|
59
|
-
* @slot - The turns, as `helix-chat-message` children.
|
|
60
|
-
* @slot actions - Beside the header: what can be done to the conversation itself.
|
|
61
|
-
* @slot composer - Below the transcript, where the reader writes.
|
|
62
|
-
* @slot header - Above the transcript: what this conversation is.
|
|
63
|
-
* @slot resume - The way back to the bottom, shown while the reader has scrolled away.
|
|
64
|
-
* @slot status - Beside the composer: somebody typing, connection lost.
|
|
65
|
-
*
|
|
66
|
-
* @event {HelixBeforePinEvent} helix-before-pin - Fired when the reader pins or unpins the transcript by scrolling. Cancelable.
|
|
67
|
-
*
|
|
68
|
-
* @csspart actions - The controls for the conversation, at the header's trailing edge.
|
|
69
|
-
* @csspart composer - The region the composer sits in.
|
|
70
|
-
* @csspart empty - Shown when the conversation has no turns.
|
|
71
|
-
* @csspart header - The region above the transcript.
|
|
72
|
-
* @csspart loading - Shown while the history is still on its way.
|
|
73
|
-
* @csspart log - The transcript, which is the thing that scrolls.
|
|
74
|
-
* @csspart resume - The affordance offered while the reader is away from the bottom.
|
|
75
|
-
* @csspart status - The status region, which is not part of the log.
|
|
76
|
-
*/
|
|
77
11
|
let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixElement) {
|
|
78
12
|
constructor() {
|
|
79
13
|
super(...arguments);
|
|
80
|
-
/**
|
|
81
|
-
* Whether to open pinned, where nothing declared it.
|
|
82
|
-
*/
|
|
83
14
|
this.defaultPinned = true;
|
|
84
|
-
/**
|
|
85
|
-
* Accessible name for the conversation.
|
|
86
|
-
*/
|
|
87
15
|
this.label = '';
|
|
88
|
-
/**
|
|
89
|
-
* How near the bottom still counts as being at it, in pixels.
|
|
90
|
-
*
|
|
91
|
-
* A hook rather than a constant, because the right number depends on how tall
|
|
92
|
-
* a turn is: a transcript of one-line replies wants a smaller threshold than
|
|
93
|
-
* one of paragraphs, and a reader who has scrolled up by half a message has
|
|
94
|
-
* not left.
|
|
95
|
-
*/
|
|
96
16
|
this.threshold = 48;
|
|
97
17
|
this.pinnedState = undefined;
|
|
98
|
-
/** Watches for turns arriving, since a manual slot hears nothing about them. */
|
|
99
18
|
this.watcher = undefined;
|
|
100
|
-
/**
|
|
101
|
-
* Which regions were given something, or undefined where nobody has counted.
|
|
102
|
-
*
|
|
103
|
-
* **A slot that is assigned by hand has no children, so no stylesheet can ask
|
|
104
|
-
* whether it was filled.** `[part="header"]:not(:has(> slot:has(*)))` is the
|
|
105
|
-
* rule every other collection here hides an empty region with, and `:has()`
|
|
106
|
-
* walks the DOM tree: with `slotAssignment: 'manual'` the assigned elements
|
|
107
|
-
* are children of this host and never of the slot, so the test was false for
|
|
108
|
-
* a filled header exactly as often as for an empty one and the rule never
|
|
109
|
-
* matched. Measured on a chat with nothing slotted at all: the header was 25
|
|
110
|
-
* pixels tall and drew its hairline under nothing, and so did the composer.
|
|
111
|
-
*
|
|
112
|
-
* Undefined rather than empty on the way in, for `MixinCollection`'s reason:
|
|
113
|
-
* the server has no children to count, and an unknown answer is not "empty".
|
|
114
|
-
* It serves every region and the first assignment on the client settles them,
|
|
115
|
-
* which is a shadow-root attribute rather than a host one and so is nothing a
|
|
116
|
-
* consumer's hydration compares.
|
|
117
|
-
*/
|
|
118
19
|
this.filled = undefined;
|
|
119
|
-
/**
|
|
120
|
-
* A press anywhere in the way back takes the reader back.
|
|
121
|
-
*
|
|
122
|
-
* The control is slotted so the consumer picks the words, which on its own
|
|
123
|
-
* leaves a button this element positions, lifts, shows and hides doing nothing
|
|
124
|
-
* until somebody wires it. The region exists for exactly one purpose, so a
|
|
125
|
-
* press inside it has exactly one meaning and this element is entitled to act
|
|
126
|
-
* on it. `resume()` stays public for the other callers: a keyboard shortcut,
|
|
127
|
-
* or a consumer's own control somewhere else on the page.
|
|
128
|
-
*/
|
|
129
20
|
this.handleResume = () => {
|
|
130
21
|
this.resume();
|
|
131
22
|
};
|
|
@@ -133,25 +24,6 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
133
24
|
this.assign();
|
|
134
25
|
this.requestUpdate();
|
|
135
26
|
};
|
|
136
|
-
/**
|
|
137
|
-
* The reader's own scrolling, which is the only thing that unpins.
|
|
138
|
-
*
|
|
139
|
-
* **Bound to the transcript in the template, and it has to be.** A listener on
|
|
140
|
-
* the host never runs, in any phase, and capture does not rescue it: `scroll`
|
|
141
|
-
* is not *composed*, so its propagation path stops at the shadow root and
|
|
142
|
-
* never includes the host. Not bubbling is the red herring.
|
|
143
|
-
*
|
|
144
|
-
* Measured by dispatching both shapes at the transcript: with
|
|
145
|
-
* `composed: false`, which is what the platform sends, the host saw zero and
|
|
146
|
-
* `pinned` stayed true; with `composed: true` the host saw one and `pinned`
|
|
147
|
-
* went false.
|
|
148
|
-
*
|
|
149
|
-
* **The scroll this element does itself must not unpin it**, which is the
|
|
150
|
-
* subtle half: `scrollTop = scrollHeight` fires the same event a reader's
|
|
151
|
-
* wheel does. Reading the position rather than counting events is what makes
|
|
152
|
-
* that harmless, since after a programmatic scroll the position is the bottom
|
|
153
|
-
* and the answer is the one that was already true.
|
|
154
|
-
*/
|
|
155
27
|
this.handleScroll = () => {
|
|
156
28
|
const wanted = this.atBottom;
|
|
157
29
|
if (wanted !== this.pinned) {
|
|
@@ -159,37 +31,10 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
159
31
|
}
|
|
160
32
|
};
|
|
161
33
|
}
|
|
162
|
-
/**
|
|
163
|
-
* Nothing is assigned by name, so the turns land in the log and the rest does not.
|
|
164
|
-
*
|
|
165
|
-
* The messages are ordinary children a consumer writes with no `slot`, and a
|
|
166
|
-
* `role="log"` whose turns are somewhere else is a log of nothing. Manual
|
|
167
|
-
* assignment is the only way to wrap them without editing the consumer's
|
|
168
|
-
* markup, which is a change a React consumer's hydration reports and does not
|
|
169
|
-
* patch up.
|
|
170
|
-
*/
|
|
171
34
|
static { this.shadowRootOptions = {
|
|
172
35
|
...HelixElement.shadowRootOptions,
|
|
173
36
|
slotAssignment: 'manual',
|
|
174
37
|
}; }
|
|
175
|
-
/**
|
|
176
|
-
* Whether the transcript follows new turns to the bottom.
|
|
177
|
-
*
|
|
178
|
-
* The reader's scrolling asks first: `helix-before-pin` is cancelable, so a
|
|
179
|
-
* consumer refusing it takes the decision and assigns their own value.
|
|
180
|
-
* `default-pinned` seeds the state this element then owns, and it defaults to
|
|
181
|
-
* pinned because a conversation opens at its most recent turn.
|
|
182
|
-
*
|
|
183
|
-
* **Not reflected, and the default is why.** A reflected property that
|
|
184
|
-
* defaults to true writes its attribute on upgrade whether or not the
|
|
185
|
-
* consumer wrote one, so the server serves `<helix-chat label="x">` and the
|
|
186
|
-
* client turns it into `<helix-chat label="x" pinned>`: a hydration mismatch a
|
|
187
|
-
* React consumer reports and does not patch up. Nothing needs the attribute
|
|
188
|
-
* either, because the one thing that changes with the pin is the way back, and
|
|
189
|
-
* that is a part inside the shadow tree where a stylesheet can already reach
|
|
190
|
-
* it. Measured rather than assumed: `label, pinned` after upgrade against
|
|
191
|
-
* `label` from the server.
|
|
192
|
-
*/
|
|
193
38
|
get pinned() {
|
|
194
39
|
return this.pinnedState ?? this.defaultPinned;
|
|
195
40
|
}
|
|
@@ -198,21 +43,15 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
198
43
|
this.pinnedState = next;
|
|
199
44
|
this.requestUpdate('pinned', previous);
|
|
200
45
|
}
|
|
201
|
-
/** Whether a region has something in it, taking unknown for yes. */
|
|
202
46
|
has(name) {
|
|
203
47
|
return this.filled?.has(name) ?? true;
|
|
204
48
|
}
|
|
205
|
-
/** The turns declared as children, in the order they are written. */
|
|
206
49
|
get messages() {
|
|
207
|
-
/* `?? []` is the server: Lit's element shim carries attributes and not a
|
|
208
|
-
tree, so `children` is undefined there and spreading it throws. */
|
|
209
50
|
return [...(this.children ?? [])].filter((child) => 'isChatMessage' in child && child.isChatMessage === true);
|
|
210
51
|
}
|
|
211
|
-
/** The element that scrolls, once there is one. */
|
|
212
52
|
get log() {
|
|
213
53
|
return this.renderRoot?.querySelector?.('[part="log"]') ?? null;
|
|
214
54
|
}
|
|
215
|
-
/** Whether the reader is within `threshold` of the bottom right now. */
|
|
216
55
|
get atBottom() {
|
|
217
56
|
const log = this.log;
|
|
218
57
|
if (!log) {
|
|
@@ -220,13 +59,6 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
220
59
|
}
|
|
221
60
|
return log.scrollHeight - log.scrollTop - log.clientHeight <= Math.max(0, this.threshold);
|
|
222
61
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Puts the transcript back at its most recent turn.
|
|
225
|
-
*
|
|
226
|
-
* Public because the way back is a slot, so the control offering it is the
|
|
227
|
-
* consumer's and needs something to call. Pins again on the way, which is what
|
|
228
|
-
* a reader asking to go to the bottom means.
|
|
229
|
-
*/
|
|
230
62
|
resume() {
|
|
231
63
|
const log = this.log;
|
|
232
64
|
if (log) {
|
|
@@ -234,14 +66,6 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
234
66
|
}
|
|
235
67
|
this.requestPinned(true);
|
|
236
68
|
}
|
|
237
|
-
/**
|
|
238
|
-
* Asks to pin or unpin, and reports whether the request stood.
|
|
239
|
-
*
|
|
240
|
-
* Announced before it is applied, so a consumer owning the pin hears the
|
|
241
|
-
* reader's scroll and decides. `property` names it, which is what
|
|
242
|
-
* `helix-before-pin`'s detail is for: a listener switching on the name is
|
|
243
|
-
* switching on something with one meaning.
|
|
244
|
-
*/
|
|
245
69
|
requestPinned(next) {
|
|
246
70
|
if (next === this.pinned) {
|
|
247
71
|
return false;
|
|
@@ -273,8 +97,6 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
273
97
|
filled.add(name);
|
|
274
98
|
}
|
|
275
99
|
}
|
|
276
|
-
/* Compared before it is set, because this runs from `updated` and a new Set
|
|
277
|
-
every render is a new value every render, which is a render loop. */
|
|
278
100
|
const previous = this.filled;
|
|
279
101
|
if (!previous ||
|
|
280
102
|
previous.size !== filled.size ||
|
|
@@ -284,17 +106,6 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
284
106
|
}
|
|
285
107
|
connectedCallback() {
|
|
286
108
|
super.connectedCallback();
|
|
287
|
-
/* **A turn that arrives after first paint reaches nothing on its own.**
|
|
288
|
-
Slots are assigned by hand here, so appending a child assigns it to
|
|
289
|
-
nothing, fires no `slotchange`, and schedules no render: the transcript
|
|
290
|
-
went on showing "Nothing to show" over six children it already had, and
|
|
291
|
-
the one thing a conversation does is grow. Measured with `assignedElements()`
|
|
292
|
-
at zero against `children` at nine.
|
|
293
|
-
|
|
294
|
-
`childList` and not `subtree`, which is the difference between watching a
|
|
295
|
-
conversation and watching every token of it: a streaming turn rewrites its
|
|
296
|
-
own text on every frame, and a subtree observer would re-render the whole
|
|
297
|
-
transcript each time for a change inside a child that owns it. */
|
|
298
109
|
this.watcher = new MutationObserver(this.handleSlotChange);
|
|
299
110
|
this.watcher.observe(this, { childList: true });
|
|
300
111
|
}
|
|
@@ -310,8 +121,6 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
310
121
|
updated(changed) {
|
|
311
122
|
super.updated(changed);
|
|
312
123
|
this.assign();
|
|
313
|
-
/* After the render that added the turn, because the scroll height it is
|
|
314
|
-
being sent to does not exist until then. */
|
|
315
124
|
if (this.pinned) {
|
|
316
125
|
const log = this.log;
|
|
317
126
|
if (log) {
|
|
@@ -321,8 +130,6 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
321
130
|
}
|
|
322
131
|
render() {
|
|
323
132
|
const away = !this.pinned;
|
|
324
|
-
/* The header stands while either half of it is filled: a conversation with
|
|
325
|
-
no title and a single control still needs the row that control sits in. */
|
|
326
133
|
const titled = this.has('header') || this.has('actions');
|
|
327
134
|
return html `<div part="header" ?hidden=${!titled}>
|
|
328
135
|
<slot name="header"></slot>
|