@fusion.dev/helix-headless 0.170.0 → 0.172.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 +1045 -154
- package/dist/chat/composer/chat-composer.d.ts +30 -2
- package/dist/chat/composer/chat-composer.js +29 -4
- package/dist/chat/root/chat-root.d.ts +73 -7
- package/dist/chat/root/chat-root.js +90 -5
- package/dist/code-block/root/code-block-root.d.ts +20 -2
- package/dist/code-block/root/code-block-root.js +29 -8
- package/dist/color-input/root/color-input-root.d.ts +15 -1
- package/dist/color-input/root/color-input-root.js +19 -1
- package/dist/color-picker/root/color-picker-root.d.ts +54 -7
- package/dist/color-picker/root/color-picker-root.js +62 -1
- package/dist/combobox/root/combobox-root.d.ts +16 -6
- package/dist/combobox/root/combobox-root.js +9 -2
- package/dist/command/root/command-root.d.ts +16 -1
- package/dist/command/root/command-root.js +8 -0
- package/dist/data-table/root/data-table-root.d.ts +5 -1
- package/dist/data-table/root/data-table-root.js +1 -0
- package/dist/dialog/root/dialog-root.js +1 -0
- package/dist/drawer/root/drawer-root.d.ts +4 -0
- package/dist/drawer/root/drawer-root.js +1 -0
- package/dist/element-events.generated.js +1 -0
- package/dist/filter-input/chip/filter-chip.d.ts +13 -3
- package/dist/filter-input/chip/filter-chip.js +5 -0
- package/dist/filter-input/menu/filter-menu.js +0 -1
- package/dist/filter-input/root/filter-input-root.d.ts +71 -4
- package/dist/filter-input/root/filter-input-root.js +87 -2
- package/dist/format-date/root/format-date-root.d.ts +3 -1
- package/dist/input/root/input-root.d.ts +6 -0
- package/dist/input/root/input-root.js +13 -0
- package/dist/lightbox/root/lightbox-root.js +1 -0
- package/dist/listbox/root/listbox-root.d.ts +8 -1
- package/dist/listbox/root/listbox-root.js +9 -3
- package/dist/menu/item/menu-item.d.ts +2 -0
- package/dist/menu/item/menu-item.js +2 -0
- package/dist/menu/root/menu-root.d.ts +73 -12
- package/dist/menu/root/menu-root.js +103 -7
- package/dist/number-input/root/number-input-root.d.ts +16 -6
- package/dist/number-input/root/number-input-root.js +15 -4
- package/dist/popover/root/popover-root.d.ts +59 -6
- package/dist/popover/root/popover-root.js +51 -3
- package/dist/select/root/select-root.d.ts +2 -2
- package/dist/select/root/select-root.js +4 -2
- package/dist/steps/root/steps-root.d.ts +32 -8
- package/dist/steps/root/steps-root.js +70 -9
- package/dist/steps/step/step.d.ts +9 -1
- package/dist/steps/step/step.js +5 -0
- package/dist/textarea/root/textarea-root.d.ts +8 -0
- package/dist/textarea/root/textarea-root.js +16 -0
- package/dist/video-playlist/root/video-playlist-root.d.ts +19 -1
- package/dist/video-playlist/root/video-playlist-root.js +14 -3
- package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
- package/dist/virtual-list/root/virtual-list-root.js +7 -2
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +800 -144
- package/types/vue.d.ts +165 -31
- package/types/words.d.ts +20 -2
|
@@ -25,7 +25,13 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
25
25
|
* field taking more. A draft that fails them does not send, and the composer
|
|
26
26
|
* takes `:state(invalid)` and `aria-invalid` once the reader has tried to send
|
|
27
27
|
* it, the form was submitted, or the reader edited the field and left it.
|
|
28
|
-
*
|
|
28
|
+
* Sending an empty required composer counts as trying. Tabbing past an empty
|
|
29
|
+
* composer marks nothing.
|
|
30
|
+
*
|
|
31
|
+
* **`feedback` is a message of your own about the draft**, such as one your
|
|
32
|
+
* server sent back. While it is set, the composer is invalid at once, does
|
|
33
|
+
* not send or submit, shows the message under the field, and a screen reader
|
|
34
|
+
* hears it as it appears. Clear it to let the draft go.
|
|
29
35
|
*
|
|
30
36
|
* **`typing` says whether the user is writing**, and `helix-typing` is sent
|
|
31
37
|
* each time it changes: when they start, and when they send or stop for
|
|
@@ -47,13 +53,14 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
47
53
|
*
|
|
48
54
|
* @csspart composer - The whole composer.
|
|
49
55
|
* @csspart end - The box around the `end` slot.
|
|
56
|
+
* @csspart feedback - The `feedback` message, under the field while it is set.
|
|
50
57
|
* @csspart field - The text field.
|
|
51
58
|
* @csspart mark - The icon inside the send button.
|
|
52
59
|
* @csspart send - The send button.
|
|
53
60
|
* @csspart start - The box around the `start` slot.
|
|
54
61
|
* @csspart tools - The row under the field.
|
|
55
62
|
*
|
|
56
|
-
* @cssstate invalid - The draft fails `required`, `minlength`, `maxlength`, or a custom error, and the reader has tried to send or submit it, or edited the field and left it.
|
|
63
|
+
* @cssstate invalid - The draft fails `required`, `minlength`, `maxlength`, or a custom error, and the reader has tried to send or submit it, or edited the field and left it. Or `feedback` is set.
|
|
57
64
|
*/
|
|
58
65
|
export declare class HelixHeadlessChatComposer extends HelixFormElement {
|
|
59
66
|
/**
|
|
@@ -61,6 +68,12 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
|
|
|
61
68
|
* `registerWords('de', { 'helix-chat-composer': { sendLabel: 'Senden' } })`.
|
|
62
69
|
*/
|
|
63
70
|
static words: string;
|
|
71
|
+
/**
|
|
72
|
+
* A message about the draft, shown under the field. While set, the composer
|
|
73
|
+
* is invalid, does not send, and does not submit, as a field's `feedback`
|
|
74
|
+
* does, and the message is announced as it appears.
|
|
75
|
+
*/
|
|
76
|
+
feedback: string;
|
|
64
77
|
/**
|
|
65
78
|
* The field's name, read by screen readers.
|
|
66
79
|
*/
|
|
@@ -122,6 +135,17 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
|
|
|
122
135
|
private writing;
|
|
123
136
|
/** Whether the reader has tried to send or submit the draft, or edited it and left. */
|
|
124
137
|
private touched;
|
|
138
|
+
/**
|
|
139
|
+
* Whether the reader has typed in the field since it last held a fresh
|
|
140
|
+
* draft: since it rendered, since a send cleared it, or since a reset.
|
|
141
|
+
*
|
|
142
|
+
* **A send clears the field, and the field still sends `change` as it is
|
|
143
|
+
* left**, because the platform compares the text with what it held when it
|
|
144
|
+
* was focused. Measured in Chromium before this: a required composer sent
|
|
145
|
+
* "hi", cleared, and on Tab showed itself invalid over an empty draft
|
|
146
|
+
* nobody had written.
|
|
147
|
+
*/
|
|
148
|
+
private edited;
|
|
125
149
|
/** Whether the composer shows that its draft is wrong. */
|
|
126
150
|
private shownInvalid;
|
|
127
151
|
/** The pause after which the user has stopped writing. */
|
|
@@ -146,10 +170,14 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
|
|
|
146
170
|
* back to. `value` does not reflect, so its attribute stays as written.
|
|
147
171
|
*/
|
|
148
172
|
private get declared();
|
|
173
|
+
/** @internal `feedback` marks the composer invalid, as a field's does. */
|
|
174
|
+
protected get validityFeedback(): string;
|
|
149
175
|
/** Puts back the draft the markup declared when the form is reset. */
|
|
150
176
|
formResetCallback(): void;
|
|
151
177
|
/** The reader has had a go at the draft, so whether it is wrong can be shown. */
|
|
152
178
|
private readonly touch;
|
|
179
|
+
/** The field's `change`, which counts only after the reader typed in it. */
|
|
180
|
+
private readonly handleChange;
|
|
153
181
|
/** Brings back the draft when the browser restores the form. */
|
|
154
182
|
formStateRestoreCallback(state: string | File | FormData | null): void;
|
|
155
183
|
/**
|
|
@@ -13,6 +13,7 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
|
13
13
|
let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFormElement {
|
|
14
14
|
constructor() {
|
|
15
15
|
super(...arguments);
|
|
16
|
+
this.feedback = '';
|
|
16
17
|
this.label = 'Message';
|
|
17
18
|
this.placeholder = '';
|
|
18
19
|
this.required = false;
|
|
@@ -24,15 +25,22 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
|
|
|
24
25
|
this.isChatComposer = true;
|
|
25
26
|
this.writing = false;
|
|
26
27
|
this.touched = false;
|
|
28
|
+
this.edited = false;
|
|
27
29
|
this.shownInvalid = false;
|
|
28
30
|
this.pause = new DelayController(this, { renders: false });
|
|
29
31
|
this.touch = () => {
|
|
30
32
|
this.touched = true;
|
|
31
33
|
this.requestUpdate();
|
|
32
34
|
};
|
|
35
|
+
this.handleChange = () => {
|
|
36
|
+
if (this.edited) {
|
|
37
|
+
this.touch();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
33
40
|
this.handleInput = (event) => {
|
|
34
41
|
event.stopPropagation();
|
|
35
42
|
this.value = event.target.value;
|
|
43
|
+
this.edited = true;
|
|
36
44
|
this.dispatchEvent(new Event('input'));
|
|
37
45
|
this.markTyping(this.value !== '');
|
|
38
46
|
};
|
|
@@ -69,7 +77,13 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
|
|
|
69
77
|
}
|
|
70
78
|
send() {
|
|
71
79
|
const value = this.value.trim();
|
|
72
|
-
if (
|
|
80
|
+
if (this.isDisabled) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (value === '') {
|
|
84
|
+
if (this.required) {
|
|
85
|
+
this.touch();
|
|
86
|
+
}
|
|
73
87
|
return false;
|
|
74
88
|
}
|
|
75
89
|
if (!this.internals.validity.valid) {
|
|
@@ -81,16 +95,21 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
|
|
|
81
95
|
}
|
|
82
96
|
this.value = '';
|
|
83
97
|
this.touched = false;
|
|
98
|
+
this.edited = false;
|
|
84
99
|
this.markTyping(false);
|
|
85
100
|
return true;
|
|
86
101
|
}
|
|
87
102
|
get declared() {
|
|
88
103
|
return this.getAttribute('value') ?? '';
|
|
89
104
|
}
|
|
105
|
+
get validityFeedback() {
|
|
106
|
+
return this.feedback;
|
|
107
|
+
}
|
|
90
108
|
formResetCallback() {
|
|
91
109
|
super.formResetCallback();
|
|
92
110
|
this.value = this.declared;
|
|
93
111
|
this.touched = false;
|
|
112
|
+
this.edited = false;
|
|
94
113
|
this.requestUpdate();
|
|
95
114
|
}
|
|
96
115
|
formStateRestoreCallback(state) {
|
|
@@ -111,7 +130,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
|
|
|
111
130
|
if (field?.validity) {
|
|
112
131
|
this.internals.setValidity(field.validity, field.validationMessage, field);
|
|
113
132
|
}
|
|
114
|
-
const invalid = this.touched && !this.internals.validity.valid;
|
|
133
|
+
const invalid = this.feedback !== '' || (this.touched && !this.internals.validity.valid);
|
|
115
134
|
this.toggleState('invalid', invalid);
|
|
116
135
|
this.internals.ariaInvalid = invalid ? 'true' : 'false';
|
|
117
136
|
if (invalid !== this.shownInvalid) {
|
|
@@ -160,8 +179,9 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
|
|
|
160
179
|
minlength=${this.minLength ?? nothing}
|
|
161
180
|
maxlength=${this.maxLength ?? nothing}
|
|
162
181
|
aria-invalid=${this.shownInvalid ? 'true' : nothing}
|
|
182
|
+
aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
|
|
163
183
|
?disabled=${this.isDisabled}
|
|
164
|
-
@change=${this.
|
|
184
|
+
@change=${this.handleChange}
|
|
165
185
|
@input=${this.handleInput}
|
|
166
186
|
@keydown=${this.handleKey}
|
|
167
187
|
></textarea>
|
|
@@ -170,9 +190,14 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
|
|
|
170
190
|
<span part="end"><slot name="end"></slot></span>
|
|
171
191
|
${this.renderSend()}
|
|
172
192
|
</div>
|
|
173
|
-
</div
|
|
193
|
+
</div>${this.feedback === ''
|
|
194
|
+
? nothing
|
|
195
|
+
: html `<p part="feedback" id="feedback" role="alert">${this.feedback}</p>`}`;
|
|
174
196
|
}
|
|
175
197
|
};
|
|
198
|
+
__decorate([
|
|
199
|
+
property()
|
|
200
|
+
], HelixHeadlessChatComposer.prototype, "feedback", void 0);
|
|
176
201
|
__decorate([
|
|
177
202
|
words,
|
|
178
203
|
property()
|
|
@@ -37,16 +37,31 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
|
|
|
37
37
|
* `loading` while you fetch, then prepend the older turns. The turn the user
|
|
38
38
|
* was reading stays in place on screen.
|
|
39
39
|
*
|
|
40
|
+
* The transcript is a `log`, the role for content added in order where the
|
|
41
|
+
* newest matters most, and it answers the APG feed's keys. The `feed` role
|
|
42
|
+
* reads its articles as a list from the top, with no newest end to stay
|
|
43
|
+
* pinned to, so the log keeps the role a conversation has. While `loading` is
|
|
44
|
+
* set or older turns load, the log carries `aria-busy`, as the APG asks of a
|
|
45
|
+
* feed.
|
|
46
|
+
*
|
|
40
47
|
* The log is `aria-live="off"`. A separate status region announces each new
|
|
41
48
|
* turn once, when it has finished arriving. It skips the user's own turns
|
|
42
49
|
* unless they failed to send, turns loaded as history, and the turns already
|
|
43
50
|
* there when the transcript opened. A turn is said with its author first, such
|
|
44
51
|
* as "Ada: It ships today" (`arrival-label`), where its author can be named.
|
|
45
52
|
*
|
|
46
|
-
* Pass the turns as children or as `turns` data. Do not use both
|
|
47
|
-
*
|
|
53
|
+
* Pass the turns as children or as `turns` data. Do not use both: a chat given
|
|
54
|
+
* both draws every turn from `turns` and then the children, and says so once
|
|
55
|
+
* in the console. Children render from the server and suit most
|
|
56
|
+
* conversations. `turns` draws a long
|
|
48
57
|
* transcript one window at a time, so the log needs a block size of its own.
|
|
49
58
|
*
|
|
59
|
+
* The log scrolls on this tier. It carries `data-scroller`, and `core` ships
|
|
60
|
+
* `[data-scroller] { overflow: auto }` into every shadow root, so pinning,
|
|
61
|
+
* history and the window work with no stylesheet. You still need to give the
|
|
62
|
+
* log a block size, such as `helix-headless-chat::part(log) { block-size:
|
|
63
|
+
* 20rem }`.
|
|
64
|
+
*
|
|
50
65
|
* **A conversation is built from three elements.** The chat holds the
|
|
51
66
|
* transcript, a `Chat.Message` stands for each turn, and a `Chat.Composer` sits
|
|
52
67
|
* in the `composer` slot. When the user sends, `helix-send` reaches the chat:
|
|
@@ -60,7 +75,12 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
|
|
|
60
75
|
* it takes them away. Anything slotted into `status` is drawn first.
|
|
61
76
|
*
|
|
62
77
|
* **Keyboard.** The transcript moves between turns the way a feed does. Each
|
|
63
|
-
* turn says its place, such as "3 of 40", windowed or not.
|
|
78
|
+
* turn says its place, such as "3 of 40", windowed or not. The transcript is
|
|
79
|
+
* a tab stop while it holds a turn, and the turn a reader moved to is one
|
|
80
|
+
* too. Moving to an older turn unpins a transcript that scrolls and scrolls
|
|
81
|
+
* the turn into view, and moving to the newest pins it again. When the turn
|
|
82
|
+
* holding focus is removed, focus moves to the turn that took its place, or to
|
|
83
|
+
* the one before it where it was the last.
|
|
64
84
|
*
|
|
65
85
|
* | Key | What it does |
|
|
66
86
|
* |---|---|
|
|
@@ -115,7 +135,7 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
|
|
|
115
135
|
* @csspart indicator - The three dots before the words that say who is typing, which the styled tier animates as a message's are. Hidden from screen readers.
|
|
116
136
|
* @csspart live - The region where screen readers announce each turn once it has arrived.
|
|
117
137
|
* @csspart loading - Shown while the history loads.
|
|
118
|
-
* @csspart log - The transcript, which scrolls.
|
|
138
|
+
* @csspart log - The transcript, which scrolls. On this tier, it needs only a block size.
|
|
119
139
|
* @csspart message - The message drawn for a turn from `turns`, forwarded whole.
|
|
120
140
|
* @csspart message-actions - The controls on a turn from `turns`, beside its time.
|
|
121
141
|
* @csspart message-avatar - The avatar box of a turn from `turns`.
|
|
@@ -142,6 +162,11 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
|
|
|
142
162
|
* @csspart status - The status region, outside the log.
|
|
143
163
|
* @csspart turn - The wrapper around the message drawn for one turn from `turns`.
|
|
144
164
|
* @csspart typing - The words that say who is typing, in the status band, while `typing` names someone.
|
|
165
|
+
*
|
|
166
|
+
* @cssstate empty - The conversation has no turns.
|
|
167
|
+
* @cssstate loading - Older turns are loading: `history` is `loading`.
|
|
168
|
+
* @cssstate pinned - The transcript follows its newest turn.
|
|
169
|
+
* @cssstate typing - `typing` names someone on the other side.
|
|
145
170
|
*/
|
|
146
171
|
export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
|
|
147
172
|
/**
|
|
@@ -362,13 +387,16 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
|
|
|
362
387
|
/** The element that scrolls, once there is one. */
|
|
363
388
|
private get log();
|
|
364
389
|
/**
|
|
365
|
-
* Makes the log a tab stop while it scrolls, and takes the
|
|
366
|
-
*
|
|
390
|
+
* Makes the log a tab stop while it holds a turn or scrolls, and takes the
|
|
391
|
+
* stop away from an empty log.
|
|
367
392
|
*
|
|
368
393
|
* **A reader on a keyboard scrolls the history from the log itself.**
|
|
369
394
|
* Chromium and Firefox make a box that scrolls focusable on their own, and
|
|
370
395
|
* Safari does not, so there the arrow keys could not reach a long history.
|
|
371
|
-
*
|
|
396
|
+
*
|
|
397
|
+
* **And it is where the turns are entered.** Page Down from the log goes to
|
|
398
|
+
* the turn at the top of the view. A log that fits was no stop at all, so a
|
|
399
|
+
* conversation whose turns held no control had no way into them by keyboard.
|
|
372
400
|
*/
|
|
373
401
|
private markScroller;
|
|
374
402
|
/** Whether the reader is within `threshold` of the bottom right now. */
|
|
@@ -491,6 +519,10 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
|
|
|
491
519
|
connectedCallback(): void;
|
|
492
520
|
disconnectedCallback(): void;
|
|
493
521
|
protected willUpdate(changed: PropertyValues): void;
|
|
522
|
+
/** Whether the chat has said it was given turns both ways. */
|
|
523
|
+
private warnedMixed;
|
|
524
|
+
/** Says once that turns arrived both as children and as `turns`. */
|
|
525
|
+
private warnMixed;
|
|
494
526
|
/**
|
|
495
527
|
* Keeps the window on the same turns when the set changes around them.
|
|
496
528
|
*
|
|
@@ -501,6 +533,27 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
|
|
|
501
533
|
*/
|
|
502
534
|
private follow;
|
|
503
535
|
protected updated(changed: PropertyValues): void;
|
|
536
|
+
/** The turn focus is in, by its place and its identity, once a reader is in one. */
|
|
537
|
+
private held;
|
|
538
|
+
/** Notes the turn focus went into, or that it went somewhere else. */
|
|
539
|
+
private readonly handleFocusIn;
|
|
540
|
+
/**
|
|
541
|
+
* Forgets the turn once focus moves to something outside the transcript.
|
|
542
|
+
* A move to nothing is kept, since removing the turn that held focus is
|
|
543
|
+
* one.
|
|
544
|
+
*/
|
|
545
|
+
private readonly handleFocusOut;
|
|
546
|
+
/**
|
|
547
|
+
* Moves focus to a neighbor where removing the turn that held it dropped
|
|
548
|
+
* focus to the body: the turn that took its place, or the one before it
|
|
549
|
+
* where it was the last.
|
|
550
|
+
*
|
|
551
|
+
* **Focus went to the body**, and Page Down did nothing until the reader
|
|
552
|
+
* tabbed back into the transcript. List and Virtual List move focus to a
|
|
553
|
+
* neighbor the same way, as React Aria's GridList does. Focus anywhere but
|
|
554
|
+
* the body is where you put it, and is left there.
|
|
555
|
+
*/
|
|
556
|
+
private followRemoved;
|
|
504
557
|
/**
|
|
505
558
|
* Hands each turn written as a child the words it is named with, once it
|
|
506
559
|
* has hydrated, so its first render is the server's.
|
|
@@ -522,6 +575,8 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
|
|
|
522
575
|
* and never on a turn nobody moved to.
|
|
523
576
|
*/
|
|
524
577
|
private focusTurn;
|
|
578
|
+
/** Draws turn `at` where it is windowed and not drawn, and visits it. */
|
|
579
|
+
private showTurn;
|
|
525
580
|
/** The turn that is a tab stop because a reader moved to it. */
|
|
526
581
|
private stop;
|
|
527
582
|
/**
|
|
@@ -534,6 +589,17 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
|
|
|
534
589
|
* `tabindex` you wrote on a turn is left as it is.
|
|
535
590
|
*/
|
|
536
591
|
private visit;
|
|
592
|
+
/** Where this element last scrolled the transcript to show a turn, until the scroll that follows. */
|
|
593
|
+
private placed;
|
|
594
|
+
/**
|
|
595
|
+
* Scrolls the transcript, and nothing around it, until `turn` is in view,
|
|
596
|
+
* and notes the turn at the top as the reader's place. A pinned transcript
|
|
597
|
+
* is left at the bottom, which is where whoever holds the pin wants it.
|
|
598
|
+
*
|
|
599
|
+
* The edge nearest the turn moves the least, and a turn taller than the
|
|
600
|
+
* transcript is shown from its start.
|
|
601
|
+
*/
|
|
602
|
+
private reveal;
|
|
537
603
|
/** The first control in the regions `names` holds, in their order. */
|
|
538
604
|
private firstControlIn;
|
|
539
605
|
/**
|
|
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
7
|
var HelixHeadlessChat_1;
|
|
8
|
-
import { ChildrenController, customElement, drawIcon, focusableElements, formatWords, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, requestChange, UpgradeController, VirtualizeController, whenHydrated, words, wordsConverter, } from '@fusion.dev/helix-core';
|
|
8
|
+
import { ChildrenController, customElement, deepActiveElement, drawIcon, focusableElements, formatWords, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, reportRuntime, requestChange, UpgradeController, VirtualizeController, whenHydrated, words, wordsConverter, } from '@fusion.dev/helix-core';
|
|
9
9
|
import { arrowDown } from '@fusion.dev/helix-icons';
|
|
10
10
|
import { nothing } from 'lit';
|
|
11
11
|
import { property, state } from 'lit/decorators.js';
|
|
@@ -97,6 +97,22 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
97
97
|
this.handleResume = () => {
|
|
98
98
|
this.resume();
|
|
99
99
|
};
|
|
100
|
+
this.warnedMixed = false;
|
|
101
|
+
this.handleFocusIn = (event) => {
|
|
102
|
+
const at = this.turnFrom(event.composedPath());
|
|
103
|
+
if (at === undefined || at < 0) {
|
|
104
|
+
this.held = undefined;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const id = this.fed ? this.identity(this.turns[at]) : this.messages[at];
|
|
108
|
+
this.held = { at, id };
|
|
109
|
+
};
|
|
110
|
+
this.handleFocusOut = (event) => {
|
|
111
|
+
const to = event.relatedTarget;
|
|
112
|
+
if (to instanceof Node && !this.contains(to) && !this.renderRoot?.contains?.(to)) {
|
|
113
|
+
this.held = undefined;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
100
116
|
this.handleKeydown = (event) => {
|
|
101
117
|
if (event.defaultPrevented || event.altKey || event.metaKey || event.shiftKey) {
|
|
102
118
|
return;
|
|
@@ -138,6 +154,13 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
138
154
|
};
|
|
139
155
|
this.handleScroll = () => {
|
|
140
156
|
const log = this.log;
|
|
157
|
+
const placed = this.placed;
|
|
158
|
+
this.placed = undefined;
|
|
159
|
+
if (log && placed !== undefined && Math.abs(log.scrollTop - placed) < 1 && !this.pinned) {
|
|
160
|
+
this.remember();
|
|
161
|
+
this.reachHistory();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
141
164
|
if (this.pinned && log && log.scrollTop >= this.stuckAt - 1 && !this.atBottom) {
|
|
142
165
|
this.stick();
|
|
143
166
|
}
|
|
@@ -183,7 +206,8 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
183
206
|
if (!log) {
|
|
184
207
|
return;
|
|
185
208
|
}
|
|
186
|
-
|
|
209
|
+
const turns = this.fed ? this.turns.length : this.messages.length;
|
|
210
|
+
if (turns > 0 || log.scrollHeight > log.clientHeight) {
|
|
187
211
|
log.tabIndex = 0;
|
|
188
212
|
}
|
|
189
213
|
else {
|
|
@@ -414,6 +438,8 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
414
438
|
connectedCallback() {
|
|
415
439
|
super.connectedCallback();
|
|
416
440
|
this.addEventListener('keydown', this.handleKeydown);
|
|
441
|
+
this.addEventListener('focusin', this.handleFocusIn);
|
|
442
|
+
this.addEventListener('focusout', this.handleFocusOut);
|
|
417
443
|
this.settling = new MutationObserver(this.handleSettle);
|
|
418
444
|
this.settling.observe(this, { attributeFilter: ['state'], subtree: true });
|
|
419
445
|
this.opening = true;
|
|
@@ -427,6 +453,8 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
427
453
|
disconnectedCallback() {
|
|
428
454
|
super.disconnectedCallback();
|
|
429
455
|
this.removeEventListener('keydown', this.handleKeydown);
|
|
456
|
+
this.removeEventListener('focusin', this.handleFocusIn);
|
|
457
|
+
this.removeEventListener('focusout', this.handleFocusOut);
|
|
430
458
|
this.settling?.disconnect();
|
|
431
459
|
this.settling = undefined;
|
|
432
460
|
this.grower?.disconnect();
|
|
@@ -449,6 +477,22 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
449
477
|
if (changed.has('turns')) {
|
|
450
478
|
this.follow(changed.get('turns'));
|
|
451
479
|
}
|
|
480
|
+
this.warnMixed();
|
|
481
|
+
this.toggleState('pinned', this.pinned);
|
|
482
|
+
this.toggleState('typing', this.typing !== '');
|
|
483
|
+
this.toggleState('loading', this.history === 'loading');
|
|
484
|
+
this.toggleState('empty', this.childrenKnown && this.turns.length === 0 && this.messages.length === 0);
|
|
485
|
+
}
|
|
486
|
+
warnMixed() {
|
|
487
|
+
if (this.warnedMixed || this.turns.length === 0 || this.messages.length === 0) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
this.warnedMixed = true;
|
|
491
|
+
reportRuntime({
|
|
492
|
+
kind: 'mixed-turns',
|
|
493
|
+
message: `A chat was given ${this.messages.length} turns as children and ${this.turns.length} as \`turns\`, and draws the children after every turn from \`turns\`. Pass one or the other.`,
|
|
494
|
+
tag: this.localName,
|
|
495
|
+
});
|
|
452
496
|
}
|
|
453
497
|
follow(previous) {
|
|
454
498
|
if (!previous?.length || this.turns.length === 0) {
|
|
@@ -481,6 +525,23 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
481
525
|
this.remember();
|
|
482
526
|
this.reachHistory();
|
|
483
527
|
this.announce();
|
|
528
|
+
this.followRemoved();
|
|
529
|
+
}
|
|
530
|
+
followRemoved() {
|
|
531
|
+
const held = this.held;
|
|
532
|
+
if (held === undefined) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const ids = this.fed ? this.turns.map((turn) => this.identity(turn)) : this.messages;
|
|
536
|
+
if (ids.includes(held.id)) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
this.held = undefined;
|
|
540
|
+
const active = deepActiveElement();
|
|
541
|
+
if (ids.length === 0 || (active !== null && active !== document.body)) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
void this.focusTurn(Math.min(held.at, ids.length - 1));
|
|
484
545
|
}
|
|
485
546
|
tellTurns() {
|
|
486
547
|
const messages = this.messages;
|
|
@@ -526,9 +587,15 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
526
587
|
const count = this.fed ? this.turns.length : this.messages.length;
|
|
527
588
|
const log = this.log;
|
|
528
589
|
const scrolls = log !== null && log !== undefined && log.scrollHeight > log.clientHeight;
|
|
529
|
-
if (at < count - 1 && this.pinned && scrolls) {
|
|
530
|
-
this.
|
|
590
|
+
if (at < count - 1 && this.pinned && scrolls && this.requestPinned(false)) {
|
|
591
|
+
await this.updateComplete;
|
|
592
|
+
}
|
|
593
|
+
await this.showTurn(at);
|
|
594
|
+
if (at === count - 1 && !this.pinned && this.atBottom) {
|
|
595
|
+
this.requestPinned(true);
|
|
531
596
|
}
|
|
597
|
+
}
|
|
598
|
+
async showTurn(at) {
|
|
532
599
|
if (this.fed) {
|
|
533
600
|
const drawn = () => this.renderRoot
|
|
534
601
|
?.querySelector(`[part="turn"][data-index="${at}"]`)
|
|
@@ -559,7 +626,24 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
559
626
|
this.stop = turn;
|
|
560
627
|
}
|
|
561
628
|
}
|
|
562
|
-
turn.focus();
|
|
629
|
+
turn.focus({ preventScroll: true });
|
|
630
|
+
this.reveal(turn);
|
|
631
|
+
}
|
|
632
|
+
reveal(turn) {
|
|
633
|
+
const log = this.log;
|
|
634
|
+
if (!log || this.pinned) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const box = log.getBoundingClientRect();
|
|
638
|
+
const rect = turn.getBoundingClientRect();
|
|
639
|
+
const by = rect.top < box.top || rect.height > box.height
|
|
640
|
+
? rect.top - box.top
|
|
641
|
+
: Math.max(0, rect.bottom - box.bottom);
|
|
642
|
+
if (Math.abs(by) >= 1) {
|
|
643
|
+
log.scrollTop += by;
|
|
644
|
+
this.placed = log.scrollTop;
|
|
645
|
+
}
|
|
646
|
+
this.remember();
|
|
563
647
|
}
|
|
564
648
|
firstControlIn(names) {
|
|
565
649
|
for (const name of names) {
|
|
@@ -622,6 +706,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
622
706
|
part="log"
|
|
623
707
|
role="log"
|
|
624
708
|
aria-live="off"
|
|
709
|
+
data-scroller
|
|
625
710
|
aria-label=${this.label || this.fallbackLabel}
|
|
626
711
|
aria-busy=${busy ? 'true' : nothing}
|
|
627
712
|
?data-windowed=${fed}
|
|
@@ -18,7 +18,11 @@ import '../../copy-button/index.js';
|
|
|
18
18
|
* from the accessibility tree and left out of a copy. Set `mark-lines` to
|
|
19
19
|
* mark lines a reader should look at, such as `2, 4-6`: the block draws a
|
|
20
20
|
* band behind each run, placed where the lines are laid out, so it follows a
|
|
21
|
-
* highlighter's markup and lines that wrap.
|
|
21
|
+
* highlighter's markup and lines that wrap. `added-lines` and
|
|
22
|
+
* `removed-lines` mark lines the same way as inserted and deleted, as
|
|
23
|
+
* Expressive Code's `ins` and `del` do, in code that is not a diff. The bands
|
|
24
|
+
* are drawn and hidden from screen readers, so say in the text around the
|
|
25
|
+
* block what a marked line means. Set `wrap` to wrap long lines
|
|
22
26
|
* rather than scroll them; a gutter then numbers each line where it starts.
|
|
23
27
|
* Set `wrappable` for a button in the header that turns `wrap` on and off,
|
|
24
28
|
* pressed while the lines wrap.
|
|
@@ -93,6 +97,8 @@ import '../../copy-button/index.js';
|
|
|
93
97
|
* @slot actions - Controls for the block, in the header before the copy button.
|
|
94
98
|
* @slot copied-icon - The copy button's icon after a copy. Defaults to a check mark.
|
|
95
99
|
* @slot copy-icon - The copy button's icon before a copy. Defaults to a clipboard.
|
|
100
|
+
* @slot expand-icon - An icon on the button that shows every line of a capped block, before its words.
|
|
101
|
+
* @slot label - The block's name in the header, such as a file icon beside the file name. Replaces `label` and `language` there, and draws the header.
|
|
96
102
|
*
|
|
97
103
|
* @event {Event} change - A file tab showed its file, whose name `file` now holds.
|
|
98
104
|
* @event {HelixBeforeChangeEvent} helix-before-change - A file tab is about to show its file, with the file's name in `detail.value`. Cancelable: cancel it to keep the file shown.
|
|
@@ -134,7 +140,9 @@ import '../../copy-button/index.js';
|
|
|
134
140
|
* @csspart header - The bar that holds the name and the copy button, shown when there is a name.
|
|
135
141
|
* @csspart hunk - In a diff, a `@@` line saying where the lines under it are.
|
|
136
142
|
* @csspart label - The block's name: its `label`, or its `language` where it has no label.
|
|
137
|
-
* @csspart mark - The band behind a run of lines `mark-lines` names. The block writes where the run is laid out into `--code-block-mark-start`, `--code-block-mark-size`, and `--code-block-mark-inline-size` on it. Hidden from the accessibility tree.
|
|
143
|
+
* @csspart mark - The band behind a run of lines `mark-lines`, `added-lines` or `removed-lines` names. The block writes where the run is laid out into `--code-block-mark-start`, `--code-block-mark-size`, and `--code-block-mark-inline-size` on it. Hidden from the accessibility tree.
|
|
144
|
+
* @csspart mark-added - The band behind a run of lines `added-lines` names. Applied with `mark`.
|
|
145
|
+
* @csspart mark-removed - The band behind a run of lines `removed-lines` names. Applied with `mark`.
|
|
138
146
|
* @csspart marker - In a diff, the `+`, `-`, or space that starts a line.
|
|
139
147
|
* @csspart new - In a split diff, the new version's `<pre>`.
|
|
140
148
|
* @csspart number - In a diff, a line number. In a `wrap` block with `line-numbers`, one line's number in the gutter, which the block makes as tall as the line it numbers through `--code-block-line-size`.
|
|
@@ -158,6 +166,11 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
158
166
|
* `registerWords('de', { 'helix-code-block': { copyLabel: 'Code kopieren' } })`.
|
|
159
167
|
*/
|
|
160
168
|
static words: string;
|
|
169
|
+
/**
|
|
170
|
+
* The lines to mark as inserted, as numbers and ranges counted from 1, such
|
|
171
|
+
* as `3-4`. Drawn as `mark-lines` is, in the added color. Not for a diff.
|
|
172
|
+
*/
|
|
173
|
+
addedLines: string;
|
|
161
174
|
/**
|
|
162
175
|
* The name of a block that scrolls, which is a tab stop while it does.
|
|
163
176
|
*
|
|
@@ -312,6 +325,11 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
312
325
|
* `false`.
|
|
313
326
|
*/
|
|
314
327
|
noCopy: boolean;
|
|
328
|
+
/**
|
|
329
|
+
* The lines to mark as deleted, as numbers and ranges counted from 1. Drawn
|
|
330
|
+
* as `mark-lines` is, in the removed color. Not for a diff.
|
|
331
|
+
*/
|
|
332
|
+
removedLines: string;
|
|
315
333
|
/**
|
|
316
334
|
* The words of a button that shows a folded run of unchanged lines, in a
|
|
317
335
|
* diff with `context-lines`. `{count}` is how many.
|
|
@@ -27,6 +27,7 @@ const nameOf = (block) => {
|
|
|
27
27
|
let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
28
28
|
constructor() {
|
|
29
29
|
super(...arguments);
|
|
30
|
+
this.addedLines = '';
|
|
30
31
|
this.codeLabel = 'Code';
|
|
31
32
|
this.codeNameLabel = '{code}, {name}';
|
|
32
33
|
this.collapseLabel = 'Show fewer lines';
|
|
@@ -43,6 +44,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
43
44
|
this.lineNumbers = false;
|
|
44
45
|
this.markLines = '';
|
|
45
46
|
this.noCopy = false;
|
|
47
|
+
this.removedLines = '';
|
|
46
48
|
this.showUnchangedLabel = '{count, plural, one {Show # unchanged line} other {Show # unchanged lines}}';
|
|
47
49
|
this.wrap = false;
|
|
48
50
|
this.wrapLabel = 'Wrap lines';
|
|
@@ -223,7 +225,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
223
225
|
}
|
|
224
226
|
placeLines() {
|
|
225
227
|
const code = this.renderRoot.querySelector('[part="code"]');
|
|
226
|
-
const marks = [...this.renderRoot.querySelectorAll('[part
|
|
228
|
+
const marks = [...this.renderRoot.querySelectorAll('[part~="mark"]')];
|
|
227
229
|
const numbers = [
|
|
228
230
|
...this.renderRoot.querySelectorAll('[part="gutter"] > [part="number"]'),
|
|
229
231
|
];
|
|
@@ -356,8 +358,13 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
356
358
|
catch (error) {
|
|
357
359
|
globalThis.reportError?.(error);
|
|
358
360
|
}
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
+
const marks = this.childrenKnown
|
|
362
|
+
? [
|
|
363
|
+
...lineRuns(this.markLines).map(([first, last]) => html `<span part="mark" aria-hidden="true" data-lines="${first}-${last}"></span>`),
|
|
364
|
+
...lineRuns(this.addedLines).map(([first, last]) => html `<span part="mark mark-added" aria-hidden="true" data-lines="${first}-${last}"></span>`),
|
|
365
|
+
...lineRuns(this.removedLines).map(([first, last]) => html `<span part="mark mark-removed" aria-hidden="true" data-lines="${first}-${last}"></span>`),
|
|
366
|
+
]
|
|
367
|
+
: [];
|
|
361
368
|
const code = highlighted === undefined
|
|
362
369
|
? html `<code part="code">${marks}<slot @slotchange=${() => this.requestUpdate()}></slot></code>`
|
|
363
370
|
: html `<code part="code">${marks}${unsafeHTML(highlighted)}</code><slot
|
|
@@ -376,7 +383,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
376
383
|
: nothing;
|
|
377
384
|
const name = this.label || this.language;
|
|
378
385
|
const drawn = this.tabbed ? '' : name;
|
|
379
|
-
return html `${this.renderHeader(name, undefined, drawn)}
|
|
386
|
+
return html `${this.renderHeader(name, undefined, drawn, true)}
|
|
380
387
|
<div part="frame">
|
|
381
388
|
<pre id="block" part="block" translate="no">${gutter}${code}</pre>
|
|
382
389
|
${this.headed(drawn) ? nothing : this.renderCopy()}
|
|
@@ -396,16 +403,24 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
396
403
|
this.toggleState('expanded', this.expanded);
|
|
397
404
|
}}
|
|
398
405
|
>
|
|
399
|
-
${this.expanded ? this.collapseLabel : this.expandLabel}
|
|
406
|
+
${this.slotted('expand-icon') ? html `<slot name="expand-icon"></slot>` : nothing}${this.expanded ? this.collapseLabel : this.expandLabel}
|
|
400
407
|
</button>`;
|
|
401
408
|
}
|
|
402
409
|
headed(name) {
|
|
403
|
-
return name !== '' ||
|
|
410
|
+
return (name !== '' ||
|
|
411
|
+
(this.wrappable && !this.diff) ||
|
|
412
|
+
this.slotted('actions') ||
|
|
413
|
+
(!this.diff && !this.tabbed && this.slotted('label')));
|
|
404
414
|
}
|
|
405
|
-
renderHeader(name, value, drawn = name) {
|
|
415
|
+
renderHeader(name, value, drawn = name, own = false) {
|
|
406
416
|
const acting = this.slotted('actions');
|
|
417
|
+
const label = own && !this.tabbed && this.slotted('label')
|
|
418
|
+
? html `<span part="label"><slot name="label">${drawn}</slot></span>`
|
|
419
|
+
: drawn
|
|
420
|
+
? html `<span part="label">${drawn}</span>`
|
|
421
|
+
: nothing;
|
|
407
422
|
return this.headed(drawn)
|
|
408
|
-
? html `<div part="header">${
|
|
423
|
+
? html `<div part="header">${label}${acting ? html `<span part="actions"><slot name="actions"></slot></span>` : nothing}${this.renderWrapToggle()}${this.renderCopy(value, name)}</div>`
|
|
409
424
|
: nothing;
|
|
410
425
|
}
|
|
411
426
|
renderWrapToggle() {
|
|
@@ -501,6 +516,9 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
501
516
|
})}<slot hidden @slotchange=${() => this.requestUpdate()}></slot>`;
|
|
502
517
|
}
|
|
503
518
|
};
|
|
519
|
+
__decorate([
|
|
520
|
+
property({ attribute: 'added-lines' })
|
|
521
|
+
], HelixHeadlessCodeBlock.prototype, "addedLines", void 0);
|
|
504
522
|
__decorate([
|
|
505
523
|
words,
|
|
506
524
|
property({ attribute: 'code-label' })
|
|
@@ -567,6 +585,9 @@ __decorate([
|
|
|
567
585
|
__decorate([
|
|
568
586
|
property({ attribute: 'no-copy', reflect: true, type: Boolean })
|
|
569
587
|
], HelixHeadlessCodeBlock.prototype, "noCopy", void 0);
|
|
588
|
+
__decorate([
|
|
589
|
+
property({ attribute: 'removed-lines' })
|
|
590
|
+
], HelixHeadlessCodeBlock.prototype, "removedLines", void 0);
|
|
570
591
|
__decorate([
|
|
571
592
|
words,
|
|
572
593
|
property({ attribute: 'show-unchanged-label' })
|