@fusion.dev/helix-headless 0.217.0 → 0.219.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/api.json +1 -1
  2. package/custom-elements.json +1 -1
  3. package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +13 -2
  4. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +19 -9
  5. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +16 -1
  6. package/dist/breadcrumbs/root/breadcrumbs-root.js +6 -0
  7. package/dist/button/root/button-root.d.ts +33 -0
  8. package/dist/button/root/button-root.js +13 -3
  9. package/dist/canvas/root/canvas-root.d.ts +11 -0
  10. package/dist/canvas/root/canvas-root.js +1 -0
  11. package/dist/chat/composer/chat-composer.d.ts +221 -6
  12. package/dist/chat/composer/chat-composer.js +249 -19
  13. package/dist/chat/message/chat-message.d.ts +7 -0
  14. package/dist/chat/message/chat-message.js +2 -1
  15. package/dist/chat/root/chat-root.d.ts +1 -0
  16. package/dist/chat/root/chat-root.js +1 -1
  17. package/dist/copy-button/root/copy-button-root.js +3 -3
  18. package/dist/device-screen/root/device-screen-root.d.ts +11 -0
  19. package/dist/device-screen/root/device-screen-root.js +22 -10
  20. package/dist/dialog/root/dialog-root.js +6 -1
  21. package/dist/drilldown/item/drilldown-item.js +1 -1
  22. package/dist/element-events.generated.js +1 -0
  23. package/dist/filter-input/root/filter-input-root.d.ts +4 -3
  24. package/dist/filter-input/root/filter-input-root.js +1 -20
  25. package/dist/graph/root/graph-root.d.ts +11 -0
  26. package/dist/graph/root/graph-root.js +4 -1
  27. package/dist/input/root/input-root.d.ts +6 -0
  28. package/dist/input/root/input-root.js +7 -0
  29. package/dist/lightbox/root/lightbox-root.d.ts +4 -1
  30. package/dist/lightbox/root/lightbox-root.js +10 -8
  31. package/dist/listbox/root/listbox-root.d.ts +21 -0
  32. package/dist/listbox/root/listbox-root.js +37 -2
  33. package/dist/log/group/log-group.d.ts +7 -1
  34. package/dist/log/group/log-group.js +14 -1
  35. package/dist/log/root/log-root.d.ts +13 -3
  36. package/dist/log/root/log-root.js +5 -2
  37. package/dist/map/root/map-root.d.ts +11 -0
  38. package/dist/map/root/map-root.js +28 -12
  39. package/dist/menu/root/menu-root.d.ts +8 -0
  40. package/dist/menu/root/menu-root.js +22 -1
  41. package/dist/menu/section/menu-section.d.ts +10 -0
  42. package/dist/menu/section/menu-section.js +10 -0
  43. package/dist/meter/index.d.ts +1 -0
  44. package/dist/meter/index.js +1 -0
  45. package/dist/meter/root/meter-root.d.ts +16 -1
  46. package/dist/meter/root/meter-root.js +27 -11
  47. package/dist/meter/segment/meter-segment.d.ts +74 -0
  48. package/dist/meter/segment/meter-segment.js +47 -0
  49. package/dist/scroll-area/root/scroll-area-root.d.ts +20 -3
  50. package/dist/scroll-area/root/scroll-area-root.js +23 -4
  51. package/dist/spatial-bar-chart/bars.d.ts +125 -5
  52. package/dist/spatial-bar-chart/bars.js +194 -17
  53. package/dist/spatial-bar-chart/root/spatial-bar-chart-root.d.ts +148 -6
  54. package/dist/spatial-bar-chart/root/spatial-bar-chart-root.js +285 -16
  55. package/dist/splitter/root/splitter-root.js +1 -0
  56. package/dist/tabs/root/tabs-root.d.ts +4 -0
  57. package/dist/tabs/tab/tab.d.ts +1 -0
  58. package/dist/tabs/tab/tab.js +2 -2
  59. package/dist/tag/root/tag-root.d.ts +1 -1
  60. package/dist/terminal/root/terminal-root.d.ts +11 -0
  61. package/dist/terminal/root/terminal-root.js +23 -10
  62. package/dist/toolbar/root/toolbar-root.d.ts +4 -1
  63. package/dist/tooltip/root/tooltip-root.d.ts +23 -1
  64. package/dist/tooltip/root/tooltip-root.js +28 -5
  65. package/dist/tour/root/tour-root.d.ts +1 -0
  66. package/dist/tour/root/tour-root.js +4 -1
  67. package/dist/tour/stop/tour-stop.js +1 -0
  68. package/examples.json +1 -1
  69. package/lint.json +1 -1
  70. package/package.json +5 -5
  71. package/types/jsx.d.ts +1373 -52
  72. package/types/svelte.d.ts +1 -0
  73. package/types/vue.d.ts +312 -17
  74. package/types/words.d.ts +9 -1
@@ -1,19 +1,39 @@
1
1
  import { __decorate } from "tslib";
2
- import { customElement, DelayController, HelixFormElement, HelixSendEvent, HelixTypingEvent, hostAria, MixinVoice, markRegion, numberConverter, SPOKEN_RULE, timeConverter, words, } from '@fusion.dev/helix-core';
3
- import { arrowElbowDownLeft } from '@fusion.dev/helix-icons';
2
+ import { customElement, DelayController, deepActiveElement, HelixFormElement, HelixSendEvent, HelixStopEvent, HelixTypingEvent, hostAria, MixinVoice, markRegion, motionPaused, numberConverter, onMotionChange, pressedOwnControl, reducedMotion, SPOKEN_RULE, timeConverter, words, } from '@fusion.dev/helix-core';
3
+ import { arrowElbowDownLeft, stop } from '@fusion.dev/helix-icons';
4
4
  import { html, nothing } from 'lit';
5
5
  import { property, state } from 'lit/decorators.js';
6
6
  import { live } from 'lit/directives/live.js';
7
7
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
8
+ import { html as staticHtml } from 'lit/static-html.js';
9
+ import '../../tooltip/index.js';
10
+ const EXAMPLE_SHOWN = 6500;
11
+ const EXAMPLE_SHORT = 40;
12
+ const EXAMPLE_PER_CHARACTER = 60;
13
+ const EXAMPLE_LONGEST = 10000;
14
+ const EXAMPLE_LEAVE = 250;
15
+ const EXAMPLE_CROSS = 100;
16
+ const EXAMPLE_ENTER = 350;
17
+ export function exampleShown(example) {
18
+ const extra = Math.max(0, Array.from(example).length - EXAMPLE_SHORT);
19
+ return Math.min(EXAMPLE_LONGEST, EXAMPLE_SHOWN + extra * EXAMPLE_PER_CHARACTER);
20
+ }
8
21
  let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends MixinVoice(HelixFormElement) {
9
22
  constructor() {
10
23
  super(...arguments);
24
+ this.attached = false;
25
+ this.busy = false;
11
26
  this.feedback = '';
12
27
  this.label = 'Message';
13
28
  this.placeholder = '';
29
+ this.placeholders = [];
30
+ this.queue = false;
14
31
  this.required = false;
15
32
  this.rows = 1;
33
+ this.sendDisabled = false;
16
34
  this.sendLabel = 'Send';
35
+ this.sendReason = '';
36
+ this.stopLabel = 'Stop';
17
37
  this.submitOn = 'enter';
18
38
  this.typingTimeout = 3000;
19
39
  this.value = '';
@@ -22,6 +42,9 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends MixinVoi
22
42
  this.touched = false;
23
43
  this.edited = false;
24
44
  this.shownInvalid = false;
45
+ this.example = 0;
46
+ this.focused = false;
47
+ this.onScreen = true;
25
48
  this.pause = new DelayController(this, { renders: false });
26
49
  this.touch = () => {
27
50
  this.touched = true;
@@ -32,6 +55,19 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends MixinVoi
32
55
  this.touch();
33
56
  }
34
57
  };
58
+ this.handleAttachmentsDown = (event) => {
59
+ const region = event.currentTarget;
60
+ if (event.button !== 0 || pressedOwnControl(event, region) || this.isDisabled) {
61
+ return;
62
+ }
63
+ event.preventDefault();
64
+ this.partNamed('field')?.focus();
65
+ };
66
+ this.focusedOn = 'elsewhere';
67
+ this.handleFocus = (event) => {
68
+ this.focused = event.type === 'focus';
69
+ this.turnExamples();
70
+ };
35
71
  this.handleInput = (event) => {
36
72
  event.stopPropagation();
37
73
  this.value = event.target.value;
@@ -42,6 +78,9 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends MixinVoi
42
78
  this.handleSend = () => {
43
79
  this.send();
44
80
  };
81
+ this.handleStop = () => {
82
+ this.dispatchEvent(new HelixStopEvent());
83
+ };
45
84
  this.handleKey = (event) => {
46
85
  if (event.key !== 'Enter' || this.submitOn !== 'enter') {
47
86
  return;
@@ -77,7 +116,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends MixinVoi
77
116
  send() {
78
117
  this.stopListening();
79
118
  const value = this.value.trim();
80
- if (this.isDisabled) {
119
+ if (this.isDisabled || this.sendDisabled || (this.busy && !this.queue)) {
81
120
  return false;
82
121
  }
83
122
  if (value === '') {
@@ -116,12 +155,154 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends MixinVoi
116
155
  this.value = typeof state === 'string' ? state : this.declared;
117
156
  }
118
157
  markEmpty() {
119
- for (const name of ['start', 'end']) {
158
+ for (const name of ['attachments', 'start', 'end']) {
120
159
  markRegion(this.renderRoot, name, [name]);
121
160
  }
122
161
  }
162
+ get idling() {
163
+ return (this.knownChildren.some((child) => child.getAttribute('slot') === 'idle') &&
164
+ !this.busy &&
165
+ !this.attached &&
166
+ this.value.trim() === '');
167
+ }
168
+ get carrying() {
169
+ return this.knownChildren.some((child) => child.getAttribute('slot') === 'attachments');
170
+ }
171
+ noteFocus() {
172
+ if (!this.hasUpdated) {
173
+ this.focusedOn = 'elsewhere';
174
+ return;
175
+ }
176
+ const active = deepActiveElement();
177
+ const send = this.partNamed('send');
178
+ const idle = this.renderRoot
179
+ ?.querySelector('slot[name="idle"]')
180
+ ?.assignedElements({ flatten: true });
181
+ this.focusedOn =
182
+ active !== null && active === send
183
+ ? 'send'
184
+ : active !== null && idle?.some((element) => element === active || element.contains(active))
185
+ ? 'idle'
186
+ : 'elsewhere';
187
+ }
188
+ keepFocus() {
189
+ if (this.focusedOn === 'send' && this.idling) {
190
+ const idle = this.renderRoot
191
+ ?.querySelector('slot[name="idle"]')
192
+ ?.assignedElements({ flatten: true })[0];
193
+ idle?.focus();
194
+ }
195
+ else if (this.focusedOn === 'idle' && !this.idling) {
196
+ this.partNamed('send')?.focus();
197
+ }
198
+ this.focusedOn = 'elsewhere';
199
+ }
200
+ connectedCallback() {
201
+ super.connectedCallback();
202
+ this.unmoved ??= onMotionChange(() => this.turnExamples());
203
+ if (typeof IntersectionObserver === 'function' && this.sight === undefined) {
204
+ this.sight = new IntersectionObserver((entries) => {
205
+ const seen = entries.at(-1)?.isIntersecting ?? this.onScreen;
206
+ if (seen !== this.onScreen) {
207
+ this.onScreen = seen;
208
+ this.turnExamples();
209
+ }
210
+ });
211
+ this.sight.observe(this);
212
+ }
213
+ }
214
+ disconnectedCallback() {
215
+ super.disconnectedCallback();
216
+ this.unmoved?.();
217
+ this.unmoved = undefined;
218
+ this.sight?.disconnect();
219
+ this.sight = undefined;
220
+ this.onScreen = true;
221
+ this.stopExamples();
222
+ }
223
+ renderExamples() {
224
+ const { length } = this.placeholders;
225
+ const at = (index) => ({ index, text: this.placeholders[index % length] ?? '' });
226
+ const leaving = this.leavingExample;
227
+ return this.renderExample(at(this.example), leaving === undefined ? undefined : at(leaving));
228
+ }
229
+ get shownPlaceholder() {
230
+ const { length } = this.placeholders;
231
+ return length > 0 ? (this.placeholders[this.example % length] ?? '') : this.placeholder;
232
+ }
233
+ turnExamples() {
234
+ const free = this.isConnected &&
235
+ this.placeholders.length > 1 &&
236
+ this.value === '' &&
237
+ this.onScreen &&
238
+ !reducedMotion() &&
239
+ !motionPaused();
240
+ if (!free) {
241
+ this.stopExamples();
242
+ return;
243
+ }
244
+ if (this.focused) {
245
+ clearTimeout(this.turning);
246
+ this.turning = undefined;
247
+ return;
248
+ }
249
+ if (this.turning === undefined && this.crossing === undefined && this.leaving === undefined) {
250
+ this.turning = setTimeout(() => this.leaveExample(), exampleShown(this.shownPlaceholder));
251
+ }
252
+ }
253
+ leaveExample() {
254
+ this.turning = undefined;
255
+ if (reducedMotion() || motionPaused()) {
256
+ this.stopExamples();
257
+ return;
258
+ }
259
+ clearTimeout(this.entering);
260
+ this.entering = undefined;
261
+ this.toggleState('example-entering', false);
262
+ this.toggleState('example-leaving', true);
263
+ this.leavingExample = this.example;
264
+ this.crossing = setTimeout(() => this.arriveExample(), EXAMPLE_CROSS);
265
+ this.leaving = setTimeout(() => {
266
+ this.leaving = undefined;
267
+ this.leavingExample = undefined;
268
+ this.toggleState('example-leaving', false);
269
+ }, EXAMPLE_LEAVE);
270
+ }
271
+ arriveExample() {
272
+ this.crossing = undefined;
273
+ this.example = (this.example + 1) % Math.max(this.placeholders.length, 1);
274
+ this.toggleState('example-entering', true);
275
+ this.entering = setTimeout(() => {
276
+ this.entering = undefined;
277
+ this.toggleState('example-entering', false);
278
+ this.exampleLanded();
279
+ }, EXAMPLE_ENTER);
280
+ this.turnExamples();
281
+ }
282
+ stopExamples() {
283
+ clearTimeout(this.turning);
284
+ clearTimeout(this.crossing);
285
+ clearTimeout(this.leaving);
286
+ clearTimeout(this.entering);
287
+ this.turning = undefined;
288
+ this.crossing = undefined;
289
+ this.leaving = undefined;
290
+ this.entering = undefined;
291
+ this.leavingExample = undefined;
292
+ this.toggleState('example-leaving', false);
293
+ this.toggleState('example-entering', false);
294
+ this.exampleLanded();
295
+ }
296
+ exampleLanded() { }
297
+ renderExample(_example, _leaving) {
298
+ return nothing;
299
+ }
123
300
  updated(changed) {
124
301
  super.updated(changed);
302
+ this.keepFocus();
303
+ if (changed.has('placeholders') || changed.has('value')) {
304
+ this.turnExamples();
305
+ }
125
306
  this.markEmpty();
126
307
  if (changed.has('value')) {
127
308
  this.internals.setFormValue(this.value, this.value);
@@ -139,6 +320,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends MixinVoi
139
320
  }
140
321
  firstUpdated(changed) {
141
322
  super.firstUpdated(changed);
323
+ this.finishHydration();
142
324
  this.addEventListener('invalid', () => {
143
325
  if (!this.checkingValidity) {
144
326
  this.touch();
@@ -148,32 +330,42 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends MixinVoi
148
330
  willUpdate(changed) {
149
331
  super.willUpdate(changed);
150
332
  hostAria(this.internals, { label: this.label, role: 'group' });
333
+ this.noteFocus();
151
334
  }
152
335
  renderSend() {
153
- return html `<button
336
+ const mark = this.busy ? stop : arrowElbowDownLeft;
337
+ const held = this.sendDisabled && !this.busy;
338
+ const reasoned = held && !this.isDisabled && this.sendReason !== '';
339
+ const control = html `<button
154
340
  part="send"
155
341
  type="button"
156
- aria-label=${this.sendLabel}
157
- ?disabled=${this.isDisabled}
158
- @click=${this.handleSend}
342
+ aria-label=${this.busy ? this.stopLabel : this.sendLabel}
343
+ aria-disabled=${reasoned ? 'true' : nothing}
344
+ aria-describedby=${reasoned ? 'send-reason' : nothing}
345
+ ?disabled=${this.isDisabled || (held && !reasoned)}
346
+ @click=${this.busy ? this.handleStop : this.handleSend}
159
347
  >
160
- <svg
161
- part="mark"
162
- fill="currentColor"
163
- viewBox=${arrowElbowDownLeft.viewBox}
164
- aria-hidden="true"
165
- >
166
- ${unsafeSVG(arrowElbowDownLeft.content)}
348
+ <svg part="mark" fill="currentColor" viewBox=${mark.viewBox} aria-hidden="true">
349
+ ${unsafeSVG(mark.content)}
167
350
  </svg>
168
351
  </button>`;
352
+ if (!reasoned) {
353
+ return control;
354
+ }
355
+ return staticHtml `<${this.tag('tooltip')} id="send-reason" content=${this.sendReason}
356
+ exportparts="arrow: reason-arrow, arrow-bottom: reason-arrow-bottom, arrow-left: reason-arrow-left, arrow-right: reason-arrow-right, arrow-top: reason-arrow-top, surface: reason-surface, surface-bottom: reason-surface-bottom, surface-left: reason-surface-left, surface-right: reason-surface-right, surface-top: reason-surface-top"
357
+ >${control}</${this.tag('tooltip')}>`;
169
358
  }
170
359
  render() {
171
360
  return html `<div part="composer">
361
+ <div part="attachments" ?hidden=${!this.carrying} @mousedown=${this.handleAttachmentsDown}
362
+ ><slot name="attachments" @slotchange=${() => this.markEmpty()}></slot
363
+ ></div>
172
364
  <textarea
173
365
  part="field"
174
366
  .value=${live(this.value)}
175
367
  aria-label=${this.label}
176
- placeholder=${this.placeholder}
368
+ placeholder=${this.shownPlaceholder}
177
369
  rows=${this.rows}
178
370
  ?required=${this.required}
179
371
  minlength=${this.minLength ?? nothing}
@@ -181,20 +373,36 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends MixinVoi
181
373
  aria-invalid=${this.shownInvalid ? 'true' : nothing}
182
374
  aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
183
375
  ?disabled=${this.isDisabled}
376
+ @blur=${this.handleFocus}
184
377
  @change=${this.handleChange}
378
+ @focus=${this.handleFocus}
185
379
  @input=${this.handleInput}
186
380
  @keydown=${this.handleKey}
187
381
  ></textarea>
382
+ ${this.placeholders.length > 1 ? this.renderExamples() : nothing}
188
383
  <div part="tools">
189
- <span part="start"><slot name="start"></slot></span>
190
- <span part="end"><slot name="end"></slot></span>
191
- ${this.renderVoice()}${this.renderSend()}
384
+ <span part="start"><slot name="start" @slotchange=${() => this.markEmpty()}></slot></span>
385
+ <span part="end"><slot name="end" @slotchange=${() => this.markEmpty()}></slot></span>
386
+ ${this.renderVoice()}
387
+ <span part="idle" ?hidden=${!this.idling}
388
+ ><slot
389
+ name="idle"
390
+ @slotchange=${() => this.requestUpdate()}
391
+ ></slot
392
+ ></span>
393
+ ${this.idling ? nothing : this.renderSend()}
192
394
  </div>
193
395
  </div>${this.feedback === ''
194
396
  ? nothing
195
397
  : html `<p part="feedback" id="feedback" role="alert">${this.feedback}</p>`}`;
196
398
  }
197
399
  };
400
+ __decorate([
401
+ property({ reflect: true, type: Boolean })
402
+ ], HelixHeadlessChatComposer.prototype, "attached", void 0);
403
+ __decorate([
404
+ property({ reflect: true, type: Boolean })
405
+ ], HelixHeadlessChatComposer.prototype, "busy", void 0);
198
406
  __decorate([
199
407
  property()
200
408
  ], HelixHeadlessChatComposer.prototype, "feedback", void 0);
@@ -211,16 +419,32 @@ __decorate([
211
419
  __decorate([
212
420
  property()
213
421
  ], HelixHeadlessChatComposer.prototype, "placeholder", void 0);
422
+ __decorate([
423
+ property({ attribute: false })
424
+ ], HelixHeadlessChatComposer.prototype, "placeholders", void 0);
425
+ __decorate([
426
+ property({ reflect: true, type: Boolean })
427
+ ], HelixHeadlessChatComposer.prototype, "queue", void 0);
214
428
  __decorate([
215
429
  property({ type: Boolean })
216
430
  ], HelixHeadlessChatComposer.prototype, "required", void 0);
217
431
  __decorate([
218
432
  property({ converter: numberConverter('rows', 1) })
219
433
  ], HelixHeadlessChatComposer.prototype, "rows", void 0);
434
+ __decorate([
435
+ property({ attribute: 'send-disabled', type: Boolean })
436
+ ], HelixHeadlessChatComposer.prototype, "sendDisabled", void 0);
220
437
  __decorate([
221
438
  words,
222
439
  property({ attribute: 'send-label' })
223
440
  ], HelixHeadlessChatComposer.prototype, "sendLabel", void 0);
441
+ __decorate([
442
+ property({ attribute: 'send-reason' })
443
+ ], HelixHeadlessChatComposer.prototype, "sendReason", void 0);
444
+ __decorate([
445
+ words,
446
+ property({ attribute: 'stop-label' })
447
+ ], HelixHeadlessChatComposer.prototype, "stopLabel", void 0);
224
448
  __decorate([
225
449
  property({ attribute: 'submit-on' })
226
450
  ], HelixHeadlessChatComposer.prototype, "submitOn", void 0);
@@ -233,6 +457,12 @@ __decorate([
233
457
  __decorate([
234
458
  state()
235
459
  ], HelixHeadlessChatComposer.prototype, "shownInvalid", void 0);
460
+ __decorate([
461
+ state()
462
+ ], HelixHeadlessChatComposer.prototype, "example", void 0);
463
+ __decorate([
464
+ state()
465
+ ], HelixHeadlessChatComposer.prototype, "leavingExample", void 0);
236
466
  HelixHeadlessChatComposer = __decorate([
237
467
  customElement('helix-headless-chat-composer')
238
468
  ], HelixHeadlessChatComposer);
@@ -26,6 +26,11 @@ export declare const FAILED_LABEL = "Not sent";
26
26
  * a page in another language sets them once there. The time moves on by
27
27
  * itself, so "now" becomes "1 minute ago" while the page is open.
28
28
  *
29
+ * **What a turn carries goes in `attachments`**, above the bubble rather
30
+ * in it: a picture inside a tinted bubble is framed twice, and one sent with
31
+ * no words is an empty bubble around it. It sits against the same edge as the
32
+ * turn, and the words keep their bubble under it.
33
+ *
29
34
  * **An agent's work goes in `work`, and its answer in the content.** The work,
30
35
  * its thinking, the commands it ran and what they printed, is a record read
31
36
  * the way a log is, so a `Log` with `separation="plain"` and `quiet` sits there
@@ -40,6 +45,7 @@ export declare const FAILED_LABEL = "Not sent";
40
45
  *
41
46
  * @slot - The turn's content.
42
47
  * @slot actions - Controls for the turn, such as copy, retry, or react.
48
+ * @slot attachments - What the turn carries, such as images or files, above its content and outside the bubble, against the author's edge.
43
49
  * @slot avatar - The author's avatar.
44
50
  * @slot indicator - The indicator shown before the first token arrives.
45
51
  * Defaults to the typing indicator.
@@ -49,6 +55,7 @@ export declare const FAILED_LABEL = "Not sent";
49
55
  * @event {HelixSendEvent} helix-send - Fired when the user retries a failed message. Cancelable: if it is not canceled, the turn is sent again. Canceling it leaves the failure and the retry button as they were.
50
56
  *
51
57
  * @csspart actions - The controls on the turn, beside its time.
58
+ * @csspart attachments - The box around the `attachments` slot, above the content and outside the bubble.
52
59
  * @csspart avatar - The box around the `avatar` slot.
53
60
  * @csspart body - The content and the line under it.
54
61
  * @csspart content - Your own markup.
@@ -108,7 +108,7 @@ let HelixHeadlessChatMessage = class HelixHeadlessChatMessage extends HelixEleme
108
108
  }
109
109
  markEmpty() {
110
110
  markRegion(this.renderRoot, 'content', [''], { hide: false });
111
- for (const name of ['work', 'sources', 'actions']) {
111
+ for (const name of ['work', 'attachments', 'sources', 'actions']) {
112
112
  markRegion(this.renderRoot, name, [name]);
113
113
  }
114
114
  }
@@ -121,6 +121,7 @@ let HelixHeadlessChatMessage = class HelixHeadlessChatMessage extends HelixEleme
121
121
  <span part="avatar"><slot name="avatar"></slot></span>
122
122
  <div part="body">
123
123
  <div part="work"><slot name="work" @slotchange=${this.handleFill}></slot></div>
124
+ <div part="attachments"><slot name="attachments" @slotchange=${this.handleFill}></slot></div>
124
125
  <div part="content">
125
126
  <slot @slotchange=${this.handleFill}></slot>
126
127
  ${this.renderIndicator()}
@@ -138,6 +138,7 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
138
138
  * @csspart log - The transcript, which scrolls. On this tier, it needs only a block size.
139
139
  * @csspart message - The message drawn for a turn from `turns`, forwarded whole.
140
140
  * @csspart message-actions - The controls on a turn from `turns`, beside its time.
141
+ * @csspart message-attachments - The attachments above the bubble of a turn from `turns`.
141
142
  * @csspart message-avatar - The avatar box of a turn from `turns`.
142
143
  * @csspart message-avatar-mine - The avatar box of a turn from `turns` by `me`.
143
144
  * @csspart message-body - The content of a turn from `turns`, and the line under it.
@@ -705,7 +705,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
705
705
  return html `<div part="before" role="presentation" style="block-size: ${before}px; flex: none"></div>
706
706
  ${repeat(this.turns.slice(first, last), (turn, offset) => turn.key ?? first + offset, (turn, offset) => html `<div part="turn" data-index=${first + offset}>
707
707
  <${message}
708
- exportparts="actions: message-actions, avatar: message-avatar, body: message-body,
708
+ exportparts="actions: message-actions, attachments: message-attachments, avatar: message-avatar, body: message-body,
709
709
  content: message-content, indicator: message-indicator, mark: message-mark, message,
710
710
  meta: message-meta, retry: message-retry, sources: message-sources,
711
711
  state: message-state, time: message-time, work: message-work${secondNames(SECOND_NAMES.author, turn.author)}${secondNames(SECOND_NAMES.state, turn.state)}"
@@ -24,7 +24,7 @@ let HelixHeadlessCopyButton = class HelixHeadlessCopyButton extends HelixHeadles
24
24
  this.showedResult = false;
25
25
  this.clipboard = new CopyController(this, { after: () => this.feedbackDuration });
26
26
  this.handleCopy = async (event) => {
27
- if (this.pending) {
27
+ if (this.refusesPress) {
28
28
  event.preventDefault();
29
29
  event.stopImmediatePropagation();
30
30
  return;
@@ -100,12 +100,12 @@ let HelixHeadlessCopyButton = class HelixHeadlessCopyButton extends HelixHeadles
100
100
  part="button"
101
101
  type="button"
102
102
  aria-busy=${this.pending ? 'true' : nothing}
103
- aria-disabled=${this.pending ? 'true' : nothing}
103
+ aria-disabled=${this.refusesPress ? 'true' : nothing}
104
104
  aria-label=${this.says}
105
105
  data-copied=${this.copied ? '' : nothing}
106
106
  data-failed=${this.failed ? '' : nothing}
107
107
  tabindex=${this.focusable ? nothing : '-1'}
108
- ?disabled=${this.isDisabled}
108
+ ?disabled=${this.isDisabled && !this.heldDisabled}
109
109
  @click=${this.handleCopy}
110
110
  @keydown=${this.notePress}
111
111
  @pointerdown=${this.notePress}
@@ -1,6 +1,7 @@
1
1
  import { ChildrenController, type DeviceButton, HelixElement, ToolbarController } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../icon-button/index.js';
4
+ import '../../tooltip/index.js';
4
5
  /**
5
6
  * @summary A device's screen, drawn from a stream you supply, that sends taps, swipes, keys, and button presses back to the device.
6
7
  *
@@ -143,6 +144,16 @@ import '../../icon-button/index.js';
143
144
  * @csspart screenshot-live - The screenshot button's live region, which speaks while the button is pending.
144
145
  * @csspart screenshot-spinner - The ring the screenshot button turns while it is pending.
145
146
  * @csspart screenshot-text - The box the screenshot button's icon sits in.
147
+ * @csspart tooltip-arrow - The arrow of the tooltip that names each button, forwarded from `Tooltip`.
148
+ * @csspart tooltip-arrow-bottom - The arrow while the bubble is below its button, on the bubble's top edge. Set together with `tooltip-arrow`.
149
+ * @csspart tooltip-arrow-left - The arrow while the bubble is left of its button, on the bubble's right edge. Set together with `tooltip-arrow`.
150
+ * @csspart tooltip-arrow-right - The arrow while the bubble is right of its button, on the bubble's left edge. Set together with `tooltip-arrow`.
151
+ * @csspart tooltip-arrow-top - The arrow while the bubble is above its button, on the bubble's bottom edge. Set together with `tooltip-arrow`.
152
+ * @csspart tooltip-surface - The bubble of the tooltip that names each button, forwarded from `Tooltip`.
153
+ * @csspart tooltip-surface-bottom - The bubble while it is below its button. Set together with `tooltip-surface`.
154
+ * @csspart tooltip-surface-left - The bubble while it is left of its button. Set together with `tooltip-surface`.
155
+ * @csspart tooltip-surface-right - The bubble while it is right of its button. Set together with `tooltip-surface`.
156
+ * @csspart tooltip-surface-top - The bubble while it is above its button. Set together with `tooltip-surface`.
146
157
  * @csspart touch - A ring where a touch is or was.
147
158
  * @csspart touch-held - A ring under a finger that is down, beside `touch`, with `show-touches`.
148
159
  * @csspart volume-down - The Volume down button.
@@ -6,6 +6,7 @@ import { property, state } from 'lit/decorators.js';
6
6
  import { repeat } from 'lit/directives/repeat.js';
7
7
  import { html } from 'lit/static-html.js';
8
8
  import '../../icon-button/index.js';
9
+ import '../../tooltip/index.js';
9
10
  import { gestureOf } from '../gesture.js';
10
11
  import { quarterTurns, turn, unturn, unturnDistance } from '../turn.js';
11
12
  const MARK_SHOWS = 700;
@@ -546,6 +547,7 @@ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixEle
546
547
  return nothing;
547
548
  }
548
549
  const button = this.tag('icon-button');
550
+ const tip = this.tag('tooltip');
549
551
  const off = this.readonly;
550
552
  const raised = this.controlsRaised;
551
553
  return html `<div
@@ -557,6 +559,7 @@ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixEle
557
559
  ${this.draws('back')
558
560
  ? html `<${button}
559
561
  part="back"
562
+ id="back"
560
563
  exportparts="button: back-button, live: back-live, spinner: back-spinner, text: back-text"
561
564
  ?disabled=${off}
562
565
  label=${this.backLabel}
@@ -566,11 +569,12 @@ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixEle
566
569
  @pointerdown=${this.handleHoldStart}
567
570
  >
568
571
  <slot name="back-icon">${drawIcon(caretLeft)}</slot>
569
- </${button}>`
572
+ </${button}><${tip} for="back" content=${this.backLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>`
570
573
  : nothing}
571
574
  ${this.draws('home')
572
575
  ? html `<${button}
573
576
  part="home"
577
+ id="home"
574
578
  exportparts="button: home-button, live: home-live, spinner: home-spinner, text: home-text"
575
579
  ?disabled=${off}
576
580
  label=${this.homeLabel}
@@ -580,11 +584,12 @@ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixEle
580
584
  @pointerdown=${this.handleHoldStart}
581
585
  >
582
586
  <slot name="home-icon">${drawIcon(house)}</slot>
583
- </${button}>`
587
+ </${button}><${tip} for="home" content=${this.homeLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>`
584
588
  : nothing}
585
589
  ${this.draws('recents')
586
590
  ? html `<${button}
587
591
  part="recents"
592
+ id="recents"
588
593
  exportparts="button: recents-button, live: recents-live, spinner: recents-spinner, text: recents-text"
589
594
  ?disabled=${off}
590
595
  label=${this.recentsLabel}
@@ -594,11 +599,12 @@ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixEle
594
599
  @pointerdown=${this.handleHoldStart}
595
600
  >
596
601
  <slot name="recents-icon">${drawIcon(square)}</slot>
597
- </${button}>`
602
+ </${button}><${tip} for="recents" content=${this.recentsLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>`
598
603
  : nothing}
599
604
  ${this.draws('power')
600
605
  ? html `<${button}
601
606
  part="power"
607
+ id="power"
602
608
  exportparts="button: power-button, live: power-live, spinner: power-spinner, text: power-text"
603
609
  ?disabled=${off}
604
610
  label=${this.powerLabel}
@@ -608,11 +614,12 @@ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixEle
608
614
  @pointerdown=${this.handleHoldStart}
609
615
  >
610
616
  <slot name="power-icon">${drawIcon(power)}</slot>
611
- </${button}>`
617
+ </${button}><${tip} for="power" content=${this.powerLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>`
612
618
  : nothing}
613
619
  ${this.draws('volume')
614
620
  ? html `<${button}
615
621
  part="volume-up"
622
+ id="volume-up"
616
623
  exportparts="button: volume-up-button, live: volume-up-live, spinner: volume-up-spinner, text: volume-up-text"
617
624
  ?disabled=${off}
618
625
  label=${this.volumeUpLabel}
@@ -622,9 +629,10 @@ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixEle
622
629
  @pointerdown=${this.handleHoldStart}
623
630
  >
624
631
  <slot name="volume-up-icon">${drawIcon(speakerHigh)}</slot>
625
- </${button}>
632
+ </${button}><${tip} for="volume-up" content=${this.volumeUpLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>
626
633
  <${button}
627
634
  part="volume-down"
635
+ id="volume-down"
628
636
  exportparts="button: volume-down-button, live: volume-down-live, spinner: volume-down-spinner, text: volume-down-text"
629
637
  ?disabled=${off}
630
638
  label=${this.volumeDownLabel}
@@ -634,11 +642,12 @@ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixEle
634
642
  @pointerdown=${this.handleHoldStart}
635
643
  >
636
644
  <slot name="volume-down-icon">${drawIcon(speakerLow)}</slot>
637
- </${button}>`
645
+ </${button}><${tip} for="volume-down" content=${this.volumeDownLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>`
638
646
  : nothing}
639
647
  ${this.draws('rotate')
640
648
  ? html `<${button}
641
649
  part="rotate-left"
650
+ id="rotate-left"
642
651
  exportparts="button: rotate-left-button, live: rotate-left-live, spinner: rotate-left-spinner, text: rotate-left-text"
643
652
  ?disabled=${off}
644
653
  label=${this.rotateLeftLabel}
@@ -648,9 +657,10 @@ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixEle
648
657
  @pointerdown=${this.handleHoldStart}
649
658
  >
650
659
  <slot name="rotate-left-icon">${drawIcon(arrowCounterClockwise)}</slot>
651
- </${button}>
660
+ </${button}><${tip} for="rotate-left" content=${this.rotateLeftLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>
652
661
  <${button}
653
662
  part="rotate-right"
663
+ id="rotate-right"
654
664
  exportparts="button: rotate-right-button, live: rotate-right-live, spinner: rotate-right-spinner, text: rotate-right-text"
655
665
  ?disabled=${off}
656
666
  label=${this.rotateRightLabel}
@@ -660,11 +670,12 @@ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixEle
660
670
  @pointerdown=${this.handleHoldStart}
661
671
  >
662
672
  <slot name="rotate-right-icon">${drawIcon(arrowClockwise)}</slot>
663
- </${button}>`
673
+ </${button}><${tip} for="rotate-right" content=${this.rotateRightLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>`
664
674
  : nothing}
665
675
  ${this.draws('screenshot')
666
676
  ? html `<${button}
667
677
  part="screenshot"
678
+ id="screenshot"
668
679
  exportparts="button: screenshot-button, live: screenshot-live, spinner: screenshot-spinner, text: screenshot-text"
669
680
  label=${this.screenshotLabel}
670
681
  ?raised=${raised}
@@ -672,12 +683,13 @@ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixEle
672
683
  @helix-press=${stopPress}
673
684
  >
674
685
  <slot name="screenshot-icon">${drawIcon(camera)}</slot>
675
- </${button}>`
686
+ </${button}><${tip} for="screenshot" content=${this.screenshotLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>`
676
687
  : nothing}
677
688
  <slot name="controls" @slotchange=${this.handleSlotChange}></slot>
678
689
  ${this.draws('fullscreen')
679
690
  ? html `<${button}
680
691
  part="fullscreen"
692
+ id="fullscreen"
681
693
  exportparts="button: fullscreen-button, live: fullscreen-live, spinner: fullscreen-spinner, text: fullscreen-text"
682
694
  aria-pressed=${this.filled ? 'true' : 'false'}
683
695
  label=${this.fullscreenLabel}
@@ -686,7 +698,7 @@ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixEle
686
698
  @helix-press=${stopPress}
687
699
  >
688
700
  <slot name="fullscreen-icon">${drawIcon(cornersOut)}</slot>
689
- </${button}>`
701
+ </${button}><${tip} for="fullscreen" content=${this.fullscreenLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>`
690
702
  : nothing}
691
703
  </div>`;
692
704
  }