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