@fusion.dev/helix-headless 0.218.0 → 0.220.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 (84) 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/code-editor/root/code-editor-root.d.ts +48 -51
  18. package/dist/code-editor/root/code-editor-root.js +67 -249
  19. package/dist/copy-button/root/copy-button-root.js +3 -3
  20. package/dist/device-screen/root/device-screen-root.d.ts +11 -0
  21. package/dist/device-screen/root/device-screen-root.js +22 -10
  22. package/dist/dialog/root/dialog-root.js +6 -1
  23. package/dist/drilldown/item/drilldown-item.js +1 -1
  24. package/dist/element-events.generated.js +4 -1
  25. package/dist/filter-input/root/filter-input-root.d.ts +4 -3
  26. package/dist/filter-input/root/filter-input-root.js +1 -20
  27. package/dist/find/index.d.ts +1 -0
  28. package/dist/find/index.js +1 -0
  29. package/dist/find/root/find-root.d.ts +314 -0
  30. package/dist/find/root/find-root.js +577 -0
  31. package/dist/graph/root/graph-root.d.ts +11 -0
  32. package/dist/graph/root/graph-root.js +4 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +1 -0
  35. package/dist/input/root/input-root.d.ts +6 -0
  36. package/dist/input/root/input-root.js +7 -0
  37. package/dist/lightbox/root/lightbox-root.d.ts +4 -1
  38. package/dist/lightbox/root/lightbox-root.js +10 -8
  39. package/dist/listbox/root/listbox-root.d.ts +21 -0
  40. package/dist/listbox/root/listbox-root.js +37 -2
  41. package/dist/log/group/log-group.d.ts +18 -3
  42. package/dist/log/group/log-group.js +31 -4
  43. package/dist/log/root/log-root.d.ts +13 -3
  44. package/dist/log/root/log-root.js +5 -2
  45. package/dist/map/root/map-root.d.ts +11 -0
  46. package/dist/map/root/map-root.js +28 -12
  47. package/dist/menu/root/menu-root.d.ts +8 -0
  48. package/dist/menu/root/menu-root.js +22 -1
  49. package/dist/menu/section/menu-section.d.ts +10 -0
  50. package/dist/menu/section/menu-section.js +10 -0
  51. package/dist/meter/index.d.ts +1 -0
  52. package/dist/meter/index.js +1 -0
  53. package/dist/meter/root/meter-root.d.ts +16 -1
  54. package/dist/meter/root/meter-root.js +27 -11
  55. package/dist/meter/segment/meter-segment.d.ts +74 -0
  56. package/dist/meter/segment/meter-segment.js +47 -0
  57. package/dist/scroll-area/root/scroll-area-root.d.ts +20 -3
  58. package/dist/scroll-area/root/scroll-area-root.js +23 -4
  59. package/dist/spatial-bar-chart/bars.d.ts +125 -5
  60. package/dist/spatial-bar-chart/bars.js +194 -17
  61. package/dist/spatial-bar-chart/root/spatial-bar-chart-root.d.ts +148 -6
  62. package/dist/spatial-bar-chart/root/spatial-bar-chart-root.js +285 -16
  63. package/dist/splitter/root/splitter-root.js +1 -0
  64. package/dist/tabs/root/tabs-root.d.ts +4 -0
  65. package/dist/tabs/tab/tab.d.ts +1 -0
  66. package/dist/tabs/tab/tab.js +2 -2
  67. package/dist/tag/root/tag-root.d.ts +1 -1
  68. package/dist/terminal/root/terminal-root.d.ts +60 -32
  69. package/dist/terminal/root/terminal-root.js +82 -178
  70. package/dist/toolbar/root/toolbar-root.d.ts +4 -1
  71. package/dist/tooltip/root/tooltip-root.d.ts +23 -1
  72. package/dist/tooltip/root/tooltip-root.js +28 -5
  73. package/dist/tour/root/tour-root.d.ts +1 -0
  74. package/dist/tour/root/tour-root.js +4 -1
  75. package/dist/tour/stop/tour-stop.js +1 -0
  76. package/dist/tree/item/tree-item.d.ts +11 -0
  77. package/dist/tree/item/tree-item.js +19 -1
  78. package/examples.json +1 -1
  79. package/lint.json +1 -1
  80. package/package.json +5 -5
  81. package/types/jsx.d.ts +2222 -118
  82. package/types/svelte.d.ts +2 -0
  83. package/types/vue.d.ts +468 -19
  84. package/types/words.d.ts +96 -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)}"
@@ -1,6 +1,7 @@
1
- import { AriaForwardController, HelixFormElement, ListedController } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, findRanges, HelixFormElement, ListedController } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../button/index.js';
4
+ import '../../find/index.js';
4
5
  import '../../icon-button/index.js';
5
6
  import '../../toolbar/index.js';
6
7
  /** A problem in the text, from a linter or a language server, by where it is. */
@@ -81,8 +82,8 @@ export declare const positionOf: (value: string, at: number) => {
81
82
  column: number;
82
83
  line: number;
83
84
  };
84
- /** Where a query matches in a text, as `[from, to]` offsets, with no empty match. */
85
- export declare const findRanges: (text: string, query: EditorFindQuery) => Array<[number, number]>;
85
+ /** Where a query matches in a text, as `[from, to]` offsets, with no empty match. Find's own, from core. */
86
+ export { findRanges };
86
87
  declare const HelixHeadlessCodeEditor_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
87
88
  /**
88
89
  * @summary An editor for a file of code, drawn by a code editor you connect.
@@ -128,6 +129,7 @@ declare const HelixHeadlessCodeEditor_base: (new (...args: any[]) => import("@fu
128
129
  *
129
130
  * @event {Event} change - The reader finished an edit and focus left the code, in the engine or the plain field.
130
131
  * @event {HelixBeforeChangeEvent} helix-before-change - The text is about to change. Cancelable: cancel it to keep the current text.
132
+ * @event {HelixFoundEvent} helix-found - The find bar searched or moved among the matches, with `count` and the selected match's `index`, counting from 0, or -1 for none.
131
133
  * @event {Event} input - Fired after the text changes.
132
134
  *
133
135
  * @csspart body - The box the code is drawn in, between the header and the problems.
@@ -142,9 +144,9 @@ declare const HelixHeadlessCodeEditor_base: (new (...args: any[]) => import("@fu
142
144
  * @csspart diagnostics - The list of problems under the code, while there are any.
143
145
  * @csspart feedback - The message that says what is wrong.
144
146
  * @csspart field - The frame around the header, the code, the problems and the status line.
145
- * @csspart find - The button in the header that opens the find bar.
146
- * @csspart find-bar - The find bar under the header, while it is open.
147
- * @csspart find-button - The Find button's inner button.
147
+ * @csspart find - The find bar under the header, where the reader searches the code: a Find of its own.
148
+ * @csspart find-actions - The find bar's `actions` slot, which the editor fills with nothing.
149
+ * @csspart find-bar - The find bar's strip: the replace toggle, the field, the count, the toolbar, and the replace row while it shows.
148
150
  * @csspart find-close - The button that closes the find bar.
149
151
  * @csspart find-close-button - The close button's inner button.
150
152
  * @csspart find-close-live - The close button's live region, which speaks while the button is pending.
@@ -154,10 +156,11 @@ declare const HelixHeadlessCodeEditor_base: (new (...args: any[]) => import("@fu
154
156
  * @csspart find-controls-toolbar - The box inside the find bar's toolbar that lays its buttons out.
155
157
  * @csspart find-count - Which match is selected, of how many.
156
158
  * @csspart find-field - The field the search is typed in.
157
- * @csspart find-live - The Find button's live region, which speaks while the button is pending.
159
+ * @csspart find-live - The find bar's live region, which says the count as it changes.
158
160
  * @csspart find-match-case - The option that finds only the same case. Pressed while it applies.
159
161
  * @csspart find-match-case-button - The match case button's inner button.
160
162
  * @csspart find-match-case-live - The match case button's live region, which speaks while the button is pending.
163
+ * @csspart find-match-case-pressed - The match case option while it applies, beside `find-match-case`.
161
164
  * @csspart find-match-case-spinner - The ring the match case button turns while it is pending.
162
165
  * @csspart find-match-case-text - The box the match case button's icon sits in.
163
166
  * @csspart find-next - The button that selects the next match.
@@ -173,18 +176,34 @@ declare const HelixHeadlessCodeEditor_base: (new (...args: any[]) => import("@fu
173
176
  * @csspart find-regex - The option that reads the search as a regular expression. Pressed while it applies.
174
177
  * @csspart find-regex-button - The regular expression button's inner button.
175
178
  * @csspart find-regex-live - The regular expression button's live region, which speaks while the button is pending.
179
+ * @csspart find-regex-pressed - The regular expression option while it applies, beside `find-regex`.
176
180
  * @csspart find-regex-spinner - The ring the regular expression button turns while it is pending.
177
181
  * @csspart find-regex-text - The box the regular expression button's icon sits in.
178
- * @csspart find-replace - The button that shows the replace field under the find field. Expanded while it shows.
179
- * @csspart find-replace-button - The find and replace button's inner button.
180
- * @csspart find-replace-live - The find and replace button's live region, which speaks while the button is pending.
181
- * @csspart find-replace-spinner - The ring the find and replace button turns while it is pending.
182
- * @csspart find-replace-text - The box the find and replace button's icon sits in.
183
- * @csspart find-spinner - The ring the Find button turns while it is pending.
184
- * @csspart find-text - The box the Find button's icon sits in.
182
+ * @csspart find-replace - The button that replaces the selected match.
183
+ * @csspart find-replace-all - The button that replaces every match.
184
+ * @csspart find-replace-all-button - The Replace all button's inner button.
185
+ * @csspart find-replace-all-live - The Replace all button's live region, which speaks while the button is pending.
186
+ * @csspart find-replace-all-spinner - The ring the Replace all button turns while it is pending.
187
+ * @csspart find-replace-all-text - The box the Replace all button's words sits in.
188
+ * @csspart find-replace-button - The Replace button's inner button.
189
+ * @csspart find-replace-field - The field the replacement is typed in.
190
+ * @csspart find-replace-live - The Replace button's live region, which speaks while the button is pending.
191
+ * @csspart find-replace-spinner - The ring the Replace button turns while it is pending.
192
+ * @csspart find-replace-text - The box the Replace button's words sits in.
193
+ * @csspart find-replace-toggle - The button that shows the replace field under the find field. Expanded while it shows. Not drawn while `readonly`.
194
+ * @csspart find-replace-toggle-button - The find and replace button's inner button.
195
+ * @csspart find-replace-toggle-live - The find and replace button's live region, which speaks while the button is pending.
196
+ * @csspart find-replace-toggle-spinner - The ring the find and replace button turns while it is pending.
197
+ * @csspart find-replace-toggle-text - The box the find and replace button's icon sits in.
198
+ * @csspart find-toggle - The button in the header that opens and closes the find bar. Expanded while it is open.
199
+ * @csspart find-toggle-button - The Find button's inner button.
200
+ * @csspart find-toggle-live - The Find button's live region, which speaks while the button is pending.
201
+ * @csspart find-toggle-spinner - The ring the Find button turns while it is pending.
202
+ * @csspart find-toggle-text - The box the Find button's icon sits in.
185
203
  * @csspart find-whole-word - The option that finds only whole words. Pressed while it applies.
186
204
  * @csspart find-whole-word-button - The whole word button's inner button.
187
205
  * @csspart find-whole-word-live - The whole word button's live region, which speaks while the button is pending.
206
+ * @csspart find-whole-word-pressed - The whole word option while it applies, beside `find-whole-word`.
188
207
  * @csspart find-whole-word-spinner - The ring the whole word button turns while it is pending.
189
208
  * @csspart find-whole-word-text - The box the whole word button's icon sits in.
190
209
  * @csspart header - The strip above the code: the name, the language, and the controls.
@@ -193,27 +212,14 @@ declare const HelixHeadlessCodeEditor_base: (new (...args: any[]) => import("@fu
193
212
  * @csspart language - The language's name in the header.
194
213
  * @csspart position - The caret's line and column, in the status line.
195
214
  * @csspart problem-count - How many problems there are, in the status line.
196
- * @csspart replace - The button that replaces the selected match.
197
- * @csspart replace-all - The button that replaces every match.
198
- * @csspart replace-all-button - The Replace all button's inner button.
199
- * @csspart replace-all-live - The Replace all button's live region, which speaks while the button is pending.
200
- * @csspart replace-all-spinner - The ring the Replace all button turns while it is pending.
201
- * @csspart replace-all-text - The box the Replace all button's words sit in.
202
- * @csspart replace-button - The Replace button's inner button.
203
- * @csspart replace-field - The field the replacement is typed in.
204
- * @csspart replace-live - The Replace button's live region, which speaks while the button is pending.
205
- * @csspart replace-spinner - The ring the Replace button turns while it is pending.
206
- * @csspart replace-text - The box the Replace button's words sit in.
207
215
  * @csspart status - The strip under the code.
208
216
  * @csspart view - Where an engine draws, while one is connected.
209
217
  *
210
218
  * @cssstate connected - An engine draws the editor.
211
219
  * @cssstate find-open - The find bar is open.
212
220
  * @cssstate invalid - The editor has an error to show, from `required` or your `feedback`.
213
- * @cssstate replace-open - The replace field shows under the find field.
214
221
  */
215
222
  export declare class HelixHeadlessCodeEditor extends HelixHeadlessCodeEditor_base {
216
- protected static markRules: string[];
217
223
  /**
218
224
  * The key this family's words are registered under, its styled tag:
219
225
  * `registerWords('de', { 'helix-code-editor': { findLabel: 'Suchen' } })`.
@@ -286,13 +292,10 @@ export declare class HelixHeadlessCodeEditor extends HelixHeadlessCodeEditor_bas
286
292
  wrap: boolean;
287
293
  /** Where the caret is, as an offset into the value. */
288
294
  private caret;
289
- /** Whether the find bar is open. */
295
+ /** Whether the find bar is open, as it last said. */
290
296
  private findOpen;
291
- /** Where the search stands, or nothing before one. */
292
- private found;
293
- /** Whether the replace field shows under the find field. */
294
- private replaceOpen;
295
- private query;
297
+ /** The find bar drawn now, and how a search is reported to it. */
298
+ private find;
296
299
  private view;
297
300
  /** Whether Escape came just before, which lets Tab leave the plain field. */
298
301
  private escaped;
@@ -303,8 +306,6 @@ export declare class HelixHeadlessCodeEditor extends HelixHeadlessCodeEditor_bas
303
306
  /** The textarea, which the form's validity is read from either way. */
304
307
  private get control();
305
308
  private get engineState();
306
- /** Whether the search can be read, which a regular expression may not be. */
307
- private get readable();
308
309
  /** Puts the caret in the editor, as a label pointing at it would. */
309
310
  focus(options?: FocusOptions): void;
310
311
  /** Selects a range of the code and puts the caret there, in the engine or the plain field. */
@@ -319,19 +320,18 @@ export declare class HelixHeadlessCodeEditor extends HelixHeadlessCodeEditor_bas
319
320
  private readonly handleViewFocusOut;
320
321
  /** Opens the find bar, with the selection as the search where one is selected, and puts focus in its field. */
321
322
  private openFind;
322
- /** Closes the find bar, takes the marks away, and puts focus back in the code. */
323
- private closeFind;
323
+ /** Hands the find bar drawn now the editor to search through, once per bar. */
324
+ private attachFind;
325
+ /** The editor, as the find bar searches through it: the engine, or the plain field. */
326
+ private readonly finder;
324
327
  /** Finds the search and selects a match, in the engine or the plain field. */
325
- private search;
326
- /** Replaces the selected match, or every match, through `requestValue`. */
327
- private replace;
328
- /** Turns one of the find bar's options on or off, and searches again with it. */
329
- private toggleOption;
328
+ private findIn;
329
+ /** Replaces the selected match, or every match, through `requestValue`, and finds the next. */
330
+ private replaceIn;
331
+ /** The find bar opened or closed, which the Find button shows. */
332
+ private readonly handleFindToggle;
330
333
  /** The find key opens the find bar from anywhere in the editor, ahead of the browser's own. */
331
334
  private readonly handleKeys;
332
- private readonly handleFindInput;
333
- private readonly handleReplaceInput;
334
- private readonly handleFindKeys;
335
335
  /** An edit in the textarea, which has taken the character already: put back where it is refused. */
336
336
  private readonly handleInput;
337
337
  /** Relayed, because `change` does not cross a shadow boundary on its own. */
@@ -343,14 +343,12 @@ export declare class HelixHeadlessCodeEditor extends HelixHeadlessCodeEditor_bas
343
343
  * Escape came just before it, which lets the reader leave.
344
344
  */
345
345
  private readonly handleKeydown;
346
- /** The words the find bar's count says, or its regular expression's fault. */
347
- private get countText();
348
346
  /** The header: the name, the language, and the toolbar of the page's actions and Find. */
349
347
  protected renderHeader(): TemplateResult;
350
348
  /**
351
- * The find bar, kept in the tree while closed so its fields keep what was
352
- * typed. The fields stand outside the toolbar, which leaves the arrows to a
353
- * text field.
349
+ * The find bar, a Find of the editor's own that searches the engine or the
350
+ * plain field, kept in the tree while closed so its fields keep what was
351
+ * typed. The editor takes the find key itself, so the bar takes none.
354
352
  */
355
353
  protected renderFind(): TemplateResult;
356
354
  /** The problems, each a button that moves the caret to it, in its tone. */
@@ -364,4 +362,3 @@ declare global {
364
362
  'helix-headless-code-editor': HelixHeadlessCodeEditor;
365
363
  }
366
364
  }
367
- export {};