@fusion.dev/helix-headless 0.144.0 → 0.145.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 (155) hide show
  1. package/custom-elements.json +8312 -881
  2. package/dist/accordion/item/accordion-item.d.ts +48 -12
  3. package/dist/accordion/item/accordion-item.js +26 -5
  4. package/dist/accordion/root/accordion-root.d.ts +6 -0
  5. package/dist/accordion/root/accordion-root.js +7 -1
  6. package/dist/announcement/root/announcement-root.d.ts +21 -1
  7. package/dist/announcement/root/announcement-root.js +36 -1
  8. package/dist/app-header/root/app-header-root.d.ts +7 -0
  9. package/dist/app-header/root/app-header-root.js +5 -1
  10. package/dist/app-header/sectioning.d.ts +7 -0
  11. package/dist/app-header/sectioning.js +26 -0
  12. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +32 -1
  13. package/dist/breadcrumbs/root/breadcrumbs-root.js +25 -2
  14. package/dist/canvas/root/canvas-root.d.ts +38 -3
  15. package/dist/canvas/root/canvas-root.js +121 -40
  16. package/dist/carousel/root/carousel-root.d.ts +90 -0
  17. package/dist/carousel/root/carousel-root.js +134 -12
  18. package/dist/chat/composer/chat-composer.d.ts +38 -6
  19. package/dist/chat/composer/chat-composer.js +42 -9
  20. package/dist/chat/message/chat-message.d.ts +32 -1
  21. package/dist/chat/message/chat-message.js +25 -9
  22. package/dist/chat/naming.d.ts +22 -0
  23. package/dist/chat/naming.js +12 -0
  24. package/dist/chat/relative-clock.d.ts +39 -0
  25. package/dist/chat/relative-clock.js +31 -0
  26. package/dist/chat/root/chat-root.d.ts +36 -0
  27. package/dist/chat/root/chat-root.js +24 -1
  28. package/dist/code-block/root/code-block-root.d.ts +8 -1
  29. package/dist/code-block/root/code-block-root.js +5 -5
  30. package/dist/copy-button/root/copy-button-root.d.ts +21 -1
  31. package/dist/copy-button/root/copy-button-root.js +2 -2
  32. package/dist/data-table/root/data-table-root.d.ts +150 -2
  33. package/dist/data-table/root/data-table-root.js +303 -29
  34. package/dist/data-table/tree.d.ts +40 -0
  35. package/dist/data-table/tree.js +55 -0
  36. package/dist/description-list/item/description-list-item.d.ts +17 -2
  37. package/dist/description-list/item/description-list-item.js +15 -2
  38. package/dist/description-list/root/description-list-root.d.ts +27 -1
  39. package/dist/description-list/root/description-list-root.js +26 -1
  40. package/dist/drilldown/item/drilldown-item.d.ts +11 -6
  41. package/dist/drilldown/item/drilldown-item.js +8 -5
  42. package/dist/drilldown/root/drilldown-root.d.ts +11 -0
  43. package/dist/drilldown/root/drilldown-root.js +8 -3
  44. package/dist/element-events.generated.js +53 -8
  45. package/dist/filter-input/menu/filter-menu.d.ts +3 -0
  46. package/dist/filter-input/menu/filter-menu.js +8 -4
  47. package/dist/image/root/image-root.js +9 -1
  48. package/dist/input/root/input-root.d.ts +1 -1
  49. package/dist/input/root/input-root.js +2 -2
  50. package/dist/list/item/list-item.d.ts +68 -1
  51. package/dist/list/item/list-item.js +66 -4
  52. package/dist/list/numbering.d.ts +14 -0
  53. package/dist/list/numbering.js +8 -0
  54. package/dist/list/root/list-root.d.ts +106 -9
  55. package/dist/list/root/list-root.js +189 -4
  56. package/dist/list/selection.d.ts +19 -0
  57. package/dist/list/selection.js +21 -0
  58. package/dist/listbox/root/listbox-root.js +1 -2
  59. package/dist/map/root/map-root.d.ts +6 -2
  60. package/dist/map/root/map-root.js +5 -2
  61. package/dist/megamenu/root/megamenu-root.d.ts +21 -0
  62. package/dist/megamenu/root/megamenu-root.js +16 -2
  63. package/dist/menu/index.d.ts +1 -0
  64. package/dist/menu/index.js +1 -0
  65. package/dist/menu/item/menu-item.d.ts +38 -11
  66. package/dist/menu/item/menu-item.js +33 -1
  67. package/dist/menu/root/menu-root.d.ts +25 -1
  68. package/dist/menu/root/menu-root.js +63 -6
  69. package/dist/menu/section/menu-section.d.ts +55 -0
  70. package/dist/menu/section/menu-section.js +60 -0
  71. package/dist/navigation/root/navigation-root.d.ts +16 -1
  72. package/dist/navigation/root/navigation-root.js +27 -1
  73. package/dist/notifications/root/notifications-root.d.ts +18 -0
  74. package/dist/notifications/root/notifications-root.js +17 -1
  75. package/dist/object-inspector/root/object-inspector-root.d.ts +97 -16
  76. package/dist/object-inspector/root/object-inspector-root.js +179 -79
  77. package/dist/object-inspector/rows.d.ts +57 -0
  78. package/dist/object-inspector/rows.js +86 -0
  79. package/dist/outline/levels.d.ts +31 -0
  80. package/dist/outline/levels.js +23 -0
  81. package/dist/outline/root/outline-root.d.ts +68 -3
  82. package/dist/outline/root/outline-root.js +73 -8
  83. package/dist/pagination/size/pagination-size.d.ts +11 -2
  84. package/dist/pagination/size/pagination-size.js +19 -4
  85. package/dist/radio/group/radio-group.d.ts +4 -10
  86. package/dist/radio/group/radio-group.js +5 -10
  87. package/dist/rating/root/rating-root.d.ts +1 -1
  88. package/dist/rating/root/rating-root.js +2 -2
  89. package/dist/scroll-area/root/scroll-area-root.d.ts +113 -0
  90. package/dist/scroll-area/root/scroll-area-root.js +218 -3
  91. package/dist/scroll-area/scrollbar.d.ts +65 -0
  92. package/dist/scroll-area/scrollbar.js +24 -0
  93. package/dist/select/root/select-root.js +1 -1
  94. package/dist/splitter/root/splitter-root.d.ts +13 -0
  95. package/dist/splitter/root/splitter-root.js +20 -2
  96. package/dist/splitter/snap.d.ts +9 -0
  97. package/dist/splitter/snap.js +12 -0
  98. package/dist/steps/reach.d.ts +15 -0
  99. package/dist/steps/reach.js +12 -0
  100. package/dist/steps/root/steps-root.d.ts +30 -0
  101. package/dist/steps/root/steps-root.js +53 -0
  102. package/dist/steps/step/step.d.ts +38 -2
  103. package/dist/steps/step/step.js +28 -6
  104. package/dist/table/column/table-column.d.ts +11 -0
  105. package/dist/table/column/table-column.js +6 -1
  106. package/dist/table/root/table-root.d.ts +101 -0
  107. package/dist/table/root/table-root.js +166 -5
  108. package/dist/table/row/table-row.d.ts +47 -1
  109. package/dist/table/row/table-row.js +39 -2
  110. package/dist/tabs/panel/tab-panel.d.ts +9 -5
  111. package/dist/tabs/panel/tab-panel.js +3 -4
  112. package/dist/tabs/root/tabs-root.d.ts +25 -1
  113. package/dist/tabs/root/tabs-root.js +21 -3
  114. package/dist/tabs/tab/tab.d.ts +36 -4
  115. package/dist/tabs/tab/tab.js +46 -3
  116. package/dist/timeline/group/timeline-group.d.ts +46 -0
  117. package/dist/timeline/group/timeline-group.js +37 -0
  118. package/dist/timeline/index.d.ts +1 -0
  119. package/dist/timeline/index.js +1 -0
  120. package/dist/timeline/root/timeline-root.d.ts +10 -3
  121. package/dist/toast/item/toast-item.d.ts +10 -17
  122. package/dist/toast/item/toast-item.js +15 -36
  123. package/dist/toolbar/root/toolbar-root.d.ts +13 -0
  124. package/dist/toolbar/root/toolbar-root.js +15 -5
  125. package/dist/tree/item/tree-item.d.ts +54 -1
  126. package/dist/tree/item/tree-item.js +89 -20
  127. package/dist/tree/root/tree-root.d.ts +37 -0
  128. package/dist/tree/root/tree-root.js +51 -6
  129. package/dist/tree/selection.d.ts +24 -0
  130. package/dist/tree/selection.js +26 -0
  131. package/dist/video/media-events.d.ts +49 -0
  132. package/dist/video/media-events.js +39 -0
  133. package/dist/video/root/video-root.d.ts +99 -13
  134. package/dist/video/root/video-root.js +84 -47
  135. package/dist/video-playlist/menu/video-playlist-menu.d.ts +3 -0
  136. package/dist/video-playlist/menu/video-playlist-menu.js +2 -1
  137. package/dist/video-playlist/order.d.ts +41 -0
  138. package/dist/video-playlist/order.js +34 -0
  139. package/dist/video-playlist/root/video-playlist-root.d.ts +70 -2
  140. package/dist/video-playlist/root/video-playlist-root.js +60 -16
  141. package/dist/virtual-list/controls.d.ts +4 -0
  142. package/dist/virtual-list/controls.js +30 -0
  143. package/dist/virtual-list/fill.d.ts +30 -0
  144. package/dist/virtual-list/fill.js +39 -0
  145. package/dist/virtual-list/keys.d.ts +15 -0
  146. package/dist/virtual-list/keys.js +24 -0
  147. package/dist/virtual-list/root/virtual-list-root.d.ts +155 -23
  148. package/dist/virtual-list/root/virtual-list-root.js +175 -34
  149. package/lint.json +1 -1
  150. package/package.json +5 -5
  151. package/types/jsx.d.ts +3778 -801
  152. package/types/svelte.d.ts +2 -0
  153. package/types/vue.d.ts +782 -215
  154. package/dist/listbox/range.d.ts +0 -17
  155. package/dist/listbox/range.js +0 -5
@@ -1,4 +1,4 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ import { HelixFormElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  /**
4
4
  * @summary A field for writing a turn in a conversation.
@@ -15,6 +15,16 @@ import { type PropertyValues, type TemplateResult } from 'lit';
15
15
  * The composer clears only when no listener cancels the send, so the text is
16
16
  * kept when you cancel a send that failed.
17
17
  *
18
+ * **In a form, the draft is the composer's value**, submitted under `name`. A
19
+ * form reset puts back the `value` the markup declared, and a `<fieldset
20
+ * disabled>` turns the composer off. Sending is still `helix-send`, and it
21
+ * does not submit the form.
22
+ *
23
+ * **`typing` says whether the user is writing**, and `helix-typing` is sent
24
+ * each time it changes: when they start, and when they send or stop for
25
+ * `typing-timeout` milliseconds. Pass it on to show "is typing" to the other
26
+ * side.
27
+ *
18
28
  * @since 0.1.0
19
29
  * @status stable
20
30
  * @category forms
@@ -25,6 +35,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
25
35
  * @slot start - Content under the field at the leading edge, such as an attachment button or a model picker.
26
36
  *
27
37
  * @event {HelixSendEvent} helix-send - Fired when the user sends their turn, and cancelable.
38
+ * @event {HelixTypingEvent} helix-typing - The user started or stopped writing, with `typing` saying which, as the composer's `typing` does.
28
39
  * @event {Event} input - The user changed the text. Read it from `value`, for a typing indicator or a saved draft. Sent on the composer and does not bubble.
29
40
  *
30
41
  * @csspart composer - The whole composer.
@@ -35,11 +46,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
35
46
  * @csspart start - The box around the `start` slot.
36
47
  * @csspart tools - The row under the field.
37
48
  */
38
- export declare class HelixHeadlessChatComposer extends HelixElement {
39
- /**
40
- * Whether the composer blocks input, such as while a turn is being sent.
41
- */
42
- disabled: boolean;
49
+ export declare class HelixHeadlessChatComposer extends HelixFormElement {
43
50
  /**
44
51
  * The field's name, read by screen readers.
45
52
  */
@@ -67,6 +74,11 @@ export declare class HelixHeadlessChatComposer extends HelixElement {
67
74
  * has to mean a new line and nothing else.
68
75
  */
69
76
  submitOn: 'button' | 'enter';
77
+ /**
78
+ * How long after the last keystroke the user still counts as typing, in
79
+ * milliseconds.
80
+ */
81
+ typingTimeout: number;
70
82
  /**
71
83
  * The text the user has written.
72
84
  *
@@ -78,6 +90,17 @@ export declare class HelixHeadlessChatComposer extends HelixElement {
78
90
  * @internal Read by the transcript to find the composer among its children.
79
91
  */
80
92
  readonly isChatComposer = true;
93
+ /** Whether the user is writing, which `helix-typing` announces. */
94
+ private writing;
95
+ /** The pause after which the user has stopped writing. */
96
+ private readonly pause;
97
+ /**
98
+ * Whether the user is writing: true from their first keystroke until they
99
+ * send, clear the field, or pause for `typing-timeout` milliseconds.
100
+ */
101
+ get typing(): boolean;
102
+ /** Records whether the user is writing, and says so when that changed. */
103
+ private markTyping;
81
104
  /**
82
105
  * Sends what is written, and returns whether the send went through.
83
106
  *
@@ -86,6 +109,15 @@ export declare class HelixHeadlessChatComposer extends HelixElement {
86
109
  * a reasonable way to send.
87
110
  */
88
111
  send(): boolean;
112
+ /**
113
+ * The value the markup declared, which a form reset and an empty restore go
114
+ * back to. `value` does not reflect, so its attribute stays as written.
115
+ */
116
+ private get declared();
117
+ /** Puts back the draft the markup declared when the form is reset. */
118
+ formResetCallback(): void;
119
+ /** Brings back the draft when the browser restores the form. */
120
+ formStateRestoreCallback(state: string | File | FormData | null): void;
89
121
  /**
90
122
  * Says on each tool box whether anything is in it.
91
123
  *
@@ -4,27 +4,30 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, HelixElement, HelixSendEvent, hostAria, markRegion, } from '@fusion.dev/helix-core';
7
+ import { customElement, DelayController, HelixFormElement, HelixSendEvent, HelixTypingEvent, hostAria, markRegion, } from '@fusion.dev/helix-core';
8
8
  import { arrowElbowDownLeft } from '@fusion.dev/helix-icons';
9
9
  import { html } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { live } from 'lit/directives/live.js';
12
12
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
13
- let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixElement {
13
+ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFormElement {
14
14
  constructor() {
15
15
  super(...arguments);
16
- this.disabled = false;
17
16
  this.label = 'Message';
18
17
  this.placeholder = '';
19
18
  this.rows = 1;
20
19
  this.sendLabel = 'Send';
21
20
  this.submitOn = 'enter';
21
+ this.typingTimeout = 3000;
22
22
  this.value = '';
23
23
  this.isChatComposer = true;
24
+ this.writing = false;
25
+ this.pause = new DelayController(this, { renders: false });
24
26
  this.handleInput = (event) => {
25
27
  event.stopPropagation();
26
28
  this.value = event.target.value;
27
29
  this.dispatchEvent(new Event('input'));
30
+ this.markTyping(this.value !== '');
28
31
  };
29
32
  this.handleSend = () => {
30
33
  this.send();
@@ -40,17 +43,44 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixEle
40
43
  this.send();
41
44
  };
42
45
  }
46
+ get typing() {
47
+ return this.writing;
48
+ }
49
+ markTyping(typing) {
50
+ if (typing) {
51
+ this.pause.start(Math.max(0, this.typingTimeout), () => this.markTyping(false));
52
+ }
53
+ else {
54
+ this.pause.cancel();
55
+ }
56
+ if (typing === this.writing) {
57
+ return;
58
+ }
59
+ this.writing = typing;
60
+ this.dispatchEvent(new HelixTypingEvent({ typing }));
61
+ }
43
62
  send() {
44
63
  const value = this.value.trim();
45
- if (value === '' || this.disabled) {
64
+ if (value === '' || this.isDisabled) {
46
65
  return false;
47
66
  }
48
67
  if (!this.dispatchEvent(new HelixSendEvent({ value }))) {
49
68
  return false;
50
69
  }
51
70
  this.value = '';
71
+ this.markTyping(false);
52
72
  return true;
53
73
  }
74
+ get declared() {
75
+ return this.getAttribute('value') ?? '';
76
+ }
77
+ formResetCallback() {
78
+ super.formResetCallback();
79
+ this.value = this.declared;
80
+ }
81
+ formStateRestoreCallback(state) {
82
+ this.value = typeof state === 'string' ? state : this.declared;
83
+ }
54
84
  markEmpty() {
55
85
  for (const name of ['start', 'end']) {
56
86
  markRegion(this.renderRoot, name, [name]);
@@ -59,6 +89,9 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixEle
59
89
  updated(changed) {
60
90
  super.updated(changed);
61
91
  this.markEmpty();
92
+ if (changed.has('value')) {
93
+ this.internals.setFormValue(this.value === '' ? null : this.value);
94
+ }
62
95
  }
63
96
  willUpdate(changed) {
64
97
  super.willUpdate(changed);
@@ -69,7 +102,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixEle
69
102
  part="send"
70
103
  type="button"
71
104
  aria-label=${this.sendLabel}
72
- ?disabled=${this.disabled}
105
+ ?disabled=${this.isDisabled}
73
106
  @click=${this.handleSend}
74
107
  >
75
108
  <svg
@@ -90,7 +123,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixEle
90
123
  aria-label=${this.label}
91
124
  placeholder=${this.placeholder}
92
125
  rows=${this.rows}
93
- ?disabled=${this.disabled}
126
+ ?disabled=${this.isDisabled}
94
127
  @input=${this.handleInput}
95
128
  @keydown=${this.handleKey}
96
129
  ></textarea>
@@ -102,9 +135,6 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixEle
102
135
  </div>`;
103
136
  }
104
137
  };
105
- __decorate([
106
- property({ reflect: true, type: Boolean })
107
- ], HelixHeadlessChatComposer.prototype, "disabled", void 0);
108
138
  __decorate([
109
139
  property()
110
140
  ], HelixHeadlessChatComposer.prototype, "label", void 0);
@@ -120,6 +150,9 @@ __decorate([
120
150
  __decorate([
121
151
  property({ attribute: 'submit-on' })
122
152
  ], HelixHeadlessChatComposer.prototype, "submitOn", void 0);
153
+ __decorate([
154
+ property({ attribute: 'typing-timeout', type: Number })
155
+ ], HelixHeadlessChatComposer.prototype, "typingTimeout", void 0);
123
156
  __decorate([
124
157
  property()
125
158
  ], HelixHeadlessChatComposer.prototype, "value", void 0);
@@ -19,6 +19,13 @@ export declare const FAILED_LABEL = "Not sent";
19
19
  * and announced once when it settles. A screen reader cannot follow a stream
20
20
  * read out character by character.
21
21
  *
22
+ * A screen reader names the turn by its author and its time, such as "You, 2
23
+ * minutes ago". The author is `author-label`, or "You" for `me`, or the
24
+ * `author` itself where it is a name; give a turn by `them` an `author-label`
25
+ * to name it. The chat it sits in holds the sentence and the word for `me`, so
26
+ * a page in another language sets them once there. The time moves on by
27
+ * itself, so "now" becomes "1 minute ago" while the page is open.
28
+ *
22
29
  * @since 0.1.0
23
30
  * @status stable
24
31
  * @category feedback
@@ -62,6 +69,14 @@ export declare class HelixHeadlessChatMessage extends HelixElement {
62
69
  * anything else is a name the consumer styles.
63
70
  */
64
71
  author?: string;
72
+ /**
73
+ * The author's name, as a screen reader says it at the start of the turn,
74
+ * such as "Support" or "Ada Lovelace".
75
+ *
76
+ * Empty reads "You" for `me`, the `author` itself where it is a name, and
77
+ * nothing for `them`.
78
+ */
79
+ authorLabel: string;
65
80
  /**
66
81
  * Label for a message that did not send.
67
82
  */
@@ -93,15 +108,31 @@ export declare class HelixHeadlessChatMessage extends HelixElement {
93
108
  * holds the reason. Everything else that moves it is the consumer's.
94
109
  */
95
110
  state?: 'failed' | 'sending' | 'sent' | 'streaming';
111
+ /**
112
+ * @internal Set by the chat: the word for `me` and the sentence that names a
113
+ * turn with both an author and a time, in the chat's language.
114
+ */
115
+ turnWords: import("../naming.js").TurnWords;
96
116
  /**
97
117
  * @internal Read by the transcript to identify its children across the light
98
118
  * DOM, which is cheaper than `instanceof`. Not a consumer's to read or set.
99
119
  */
100
120
  readonly isChatMessage = true;
101
- private clock;
121
+ /** The time the reading is drawn against, moved on when the words go stale. */
122
+ private readonly clock;
102
123
  /** The timestamp, or nothing where there is not one to read. */
103
124
  private get moment();
104
125
  protected willUpdate(changed: PropertyValues): void;
126
+ /** The time as it reads now, or nothing where the turn has none. */
127
+ private get reading();
128
+ /**
129
+ * What a screen reader calls the turn: who wrote it, and when.
130
+ *
131
+ * **The name of an `article` is never its content**, so without one a reader
132
+ * moving through a transcript heard "article" for every turn and had to read
133
+ * each to learn who wrote it.
134
+ */
135
+ private get name();
105
136
  private renderTime;
106
137
  /**
107
138
  * The status glyph, behind a seam so a tier can draw its own.
@@ -6,19 +6,26 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { customElement, HelixElement, HelixSendEvent, hostAria, markRegion, relativeTime, requestChange, TONE_MARKS, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
- import { property, state } from 'lit/decorators.js';
9
+ import { property } from 'lit/decorators.js';
10
10
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
11
+ import { authorWords, TURN_WORDS, turnName } from '../naming.js';
12
+ import { RelativeClockController } from '../relative-clock.js';
11
13
  export const FAILED_LABEL = 'Not sent';
12
14
  let HelixHeadlessChatMessage = class HelixHeadlessChatMessage extends HelixElement {
13
15
  constructor() {
14
16
  super(...arguments);
15
17
  this.at = '';
18
+ this.authorLabel = '';
16
19
  this.failedLabel = FAILED_LABEL;
17
20
  this.locale = '';
18
21
  this.retryLabel = 'Try again';
19
22
  this.sendingLabel = 'Sending';
23
+ this.turnWords = TURN_WORDS;
20
24
  this.isChatMessage = true;
21
- this.clock = undefined;
25
+ this.clock = new RelativeClockController(this, {
26
+ locale: () => this.locale,
27
+ moment: () => this.moment,
28
+ });
22
29
  this.handleFill = () => {
23
30
  this.markEmpty();
24
31
  };
@@ -50,19 +57,25 @@ let HelixHeadlessChatMessage = class HelixHeadlessChatMessage extends HelixEleme
50
57
  }
51
58
  willUpdate(changed) {
52
59
  super.willUpdate(changed);
53
- if (changed.has('at')) {
54
- this.clock = Date.now();
60
+ if (changed.has('at') || changed.has('locale')) {
61
+ this.clock.tick();
55
62
  }
56
- hostAria(this.internals, { role: 'article' });
63
+ hostAria(this.internals, { label: this.name, role: 'article' });
57
64
  this.internals.ariaBusy = this.state === 'streaming' ? 'true' : null;
58
65
  }
66
+ get reading() {
67
+ const moment = this.moment;
68
+ return moment ? relativeTime(this.locale)(moment, new Date(this.clock.now)).text : '';
69
+ }
70
+ get name() {
71
+ return turnName(authorWords(this.author, this.authorLabel, this.turnWords), this.reading, this.turnWords);
72
+ }
59
73
  renderTime() {
60
74
  const moment = this.moment;
61
75
  if (!moment) {
62
76
  return nothing;
63
77
  }
64
- const { text } = relativeTime(this.locale)(moment, new Date(this.clock ?? Date.now()));
65
- return html `<time part="time" datetime=${moment.toISOString()}>${text}</time>`;
78
+ return html `<time part="time" datetime=${moment.toISOString()}>${this.reading}</time>`;
66
79
  }
67
80
  renderMark() {
68
81
  const { icon, label } = TONE_MARKS.danger;
@@ -124,6 +137,9 @@ __decorate([
124
137
  __decorate([
125
138
  property({ reflect: true })
126
139
  ], HelixHeadlessChatMessage.prototype, "author", void 0);
140
+ __decorate([
141
+ property({ attribute: 'author-label' })
142
+ ], HelixHeadlessChatMessage.prototype, "authorLabel", void 0);
127
143
  __decorate([
128
144
  property({ attribute: 'failed-label' })
129
145
  ], HelixHeadlessChatMessage.prototype, "failedLabel", void 0);
@@ -140,8 +156,8 @@ __decorate([
140
156
  property({ reflect: true })
141
157
  ], HelixHeadlessChatMessage.prototype, "state", void 0);
142
158
  __decorate([
143
- state()
144
- ], HelixHeadlessChatMessage.prototype, "clock", void 0);
159
+ property({ attribute: false })
160
+ ], HelixHeadlessChatMessage.prototype, "turnWords", void 0);
145
161
  HelixHeadlessChatMessage = __decorate([
146
162
  customElement('helix-headless-chat-message')
147
163
  ], HelixHeadlessChatMessage);
@@ -0,0 +1,22 @@
1
+ /** What the chat names its turns with, which it hands to each of them. */
2
+ export interface TurnWords {
3
+ /** The word for `me`, such as "You". */
4
+ me: string;
5
+ /** The sentence for a turn with an author and a time, with `{author}` and `{time}`. */
6
+ sentence: string;
7
+ }
8
+ /** The words a chat says its turns with, where nobody said otherwise. */
9
+ export declare const TURN_WORDS: TurnWords;
10
+ /**
11
+ * Who wrote a turn, in words: its `author-label`, or else the chat's word for
12
+ * `me`, or else the `author` itself where it is a name.
13
+ *
14
+ * `them` is the other side of a two-sided conversation and not a name, so it
15
+ * says nothing until it is given an `author-label`.
16
+ */
17
+ export declare const authorWords: (author: string | undefined, authorLabel: string, words: TurnWords) => string;
18
+ /**
19
+ * The turn's accessible name: the sentence where it has an author and a time,
20
+ * and whichever of the two it has otherwise. Empty where it has neither.
21
+ */
22
+ export declare const turnName: (author: string, time: string, words: TurnWords) => string;
@@ -0,0 +1,12 @@
1
+ import { fillTemplate } from '@fusion.dev/helix-core';
2
+ export const TURN_WORDS = { me: 'You', sentence: '{author}, {time}' };
3
+ export const authorWords = (author, authorLabel, words) => {
4
+ if (authorLabel !== '') {
5
+ return authorLabel;
6
+ }
7
+ if (author === 'me') {
8
+ return words.me;
9
+ }
10
+ return author === undefined || author === 'them' ? '' : author;
11
+ };
12
+ export const turnName = (author, time, words) => author !== '' && time !== '' ? fillTemplate(words.sentence, { author, time }) : author || time;
@@ -0,0 +1,39 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /** What the clock reads from its host. */
3
+ export interface RelativeClockOptions {
4
+ /** The locale the reading is written in. Empty is the runtime's own. */
5
+ locale: () => string;
6
+ /** The instant being read, or nothing where there is none. */
7
+ moment: () => Date | undefined;
8
+ }
9
+ /**
10
+ * The time a relative reading is drawn against, moved on when the reading
11
+ * would change.
12
+ *
13
+ * **One wakeup when the words go stale, and none in between.** `relativeTime`
14
+ * says how long its answer holds, so the clock asks core's shared ticker for
15
+ * exactly that moment: a turn a minute old wakes once a minute, an hour old
16
+ * once an hour. Every turn on the page shares the ticker's one timer.
17
+ *
18
+ * `Format Date` keeps the same arrangement inline, and the two belong in one
19
+ * core controller.
20
+ */
21
+ export declare class RelativeClockController implements ReactiveController {
22
+ private readonly host;
23
+ private readonly options;
24
+ /** The time the host last drew against, in milliseconds. */
25
+ now: number;
26
+ private stop;
27
+ constructor(host: ReactiveControllerHost & HTMLElement, options: RelativeClockOptions);
28
+ hostConnected(): void;
29
+ hostDisconnected(): void;
30
+ /**
31
+ * Reads the clock again and schedules the next reading.
32
+ *
33
+ * The host calls it when the instant or the locale changes, and the ticker
34
+ * calls it when the words go stale. **Only on a page**: a server has nobody
35
+ * to update a reading for, and a subscription there holds the element after
36
+ * its markup is sent.
37
+ */
38
+ tick(): void;
39
+ }
@@ -0,0 +1,31 @@
1
+ import { relativeTime, subscribe } from '@fusion.dev/helix-core';
2
+ export class RelativeClockController {
3
+ constructor(host, options) {
4
+ this.host = host;
5
+ this.options = options;
6
+ this.now = Date.now();
7
+ this.stop = undefined;
8
+ host.addController(this);
9
+ }
10
+ hostConnected() {
11
+ this.tick();
12
+ }
13
+ hostDisconnected() {
14
+ this.stop?.();
15
+ this.stop = undefined;
16
+ }
17
+ tick() {
18
+ this.stop?.();
19
+ this.stop = undefined;
20
+ this.now = Date.now();
21
+ this.host.requestUpdate();
22
+ const moment = this.options.moment();
23
+ if (!moment || !this.host.isConnected) {
24
+ return;
25
+ }
26
+ const { next } = relativeTime(this.options.locale())(moment, new Date(this.now));
27
+ if (Number.isFinite(next)) {
28
+ this.stop = subscribe(next, () => this.tick());
29
+ }
30
+ }
31
+ }
@@ -7,6 +7,8 @@ export interface ChatTurn {
7
7
  at?: string;
8
8
  /** Who said it: `me`, `them`, or a name the consumer styles. */
9
9
  author?: string;
10
+ /** Who said it, as a screen reader names the turn, as on `ChatMessage`. */
11
+ authorLabel?: string;
10
12
  /**
11
13
  * What identifies it across changes to the set.
12
14
  *
@@ -44,6 +46,20 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
44
46
  * render from the server and suit most conversations. `turns` draws a long
45
47
  * transcript one window at a time, so the log needs a block size of its own.
46
48
  *
49
+ * **A conversation is built from three elements.** The chat holds the
50
+ * transcript, a `Chat.Message` stands for each turn, and a `Chat.Composer` sits
51
+ * in the `composer` slot. When the user sends, `helix-send` reaches the chat:
52
+ * add their turn with `author="me"` and `state="sending"`, then set `state` to
53
+ * `sent` or `failed` when your server answers. Add the reply as a turn with
54
+ * `state="streaming"`, write its text as it arrives, and set `state` to `sent`
55
+ * when it is complete. While the user types, the composer sends
56
+ * `helix-typing`, which you can pass on to the other side. Show the other
57
+ * side's typing in the `status` slot.
58
+ *
59
+ * A screen reader names each turn by its author and its time, such as "You, 2
60
+ * minutes ago". Set `me-label` and `message-label` to say it in your language,
61
+ * and give each turn by `them` an `author-label`.
62
+ *
47
63
  * A turn from `turns` is drawn in the chat's shadow root, so style it through
48
64
  * the forwarded `message-*` parts. A turn by `me` also gets each part with
49
65
  * `-mine` added, and the turn's state adds `message-meta-failed`,
@@ -141,6 +157,16 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
141
157
  * The conversation's name, read by screen readers.
142
158
  */
143
159
  label: string;
160
+ /**
161
+ * The word a screen reader says for `me` when it names a turn.
162
+ */
163
+ meLabel: string;
164
+ /**
165
+ * The name a screen reader gives a turn with both an author and a time.
166
+ * `{author}` is who wrote it and `{time}` is when, as the turn shows it. A
167
+ * turn with only one of the two is named by that one.
168
+ */
169
+ messageLabel: string;
144
170
  /**
145
171
  * Whether the transcript follows new turns to the bottom.
146
172
  *
@@ -186,6 +212,11 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
186
212
  /** The last thing said, and how many times something has been. */
187
213
  private announcement;
188
214
  private said;
215
+ /**
216
+ * The words every turn is named with, kept as one object between changes so
217
+ * a turn is told again only when they change.
218
+ */
219
+ private words;
189
220
  /** Watches for turns arriving, since a manual slot hears nothing about them. */
190
221
  private watcher;
191
222
  /** Watches the turns' own `state`, which is when one of them settles. */
@@ -353,6 +384,11 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
353
384
  */
354
385
  private follow;
355
386
  protected updated(changed: PropertyValues): void;
387
+ /**
388
+ * Hands each turn written as a child the words it is named with, once it
389
+ * has hydrated, so its first render is the server's.
390
+ */
391
+ private tellTurns;
356
392
  private readonly handleSlotChange;
357
393
  private readonly handleSettle;
358
394
  /**
@@ -5,13 +5,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  var HelixHeadlessChat_1;
8
- import { customElement, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, requestChange, VirtualizeController, } from '@fusion.dev/helix-core';
8
+ import { customElement, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, requestChange, VirtualizeController, whenHydrated, } from '@fusion.dev/helix-core';
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { keyed } from 'lit/directives/keyed.js';
12
12
  import { repeat } from 'lit/directives/repeat.js';
13
13
  import { html } from 'lit/static-html.js';
14
14
  import { FAILED_LABEL } from '../message/chat-message.js';
15
+ import { TURN_WORDS } from '../naming.js';
15
16
  const REGIONS = ['header', 'actions', 'resume', 'status', 'composer'];
16
17
  const SECOND_NAMES = {
17
18
  author: {
@@ -39,11 +40,14 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
39
40
  this.history = '';
40
41
  this.historyText = 'Loading earlier messages';
41
42
  this.label = '';
43
+ this.meLabel = 'You';
44
+ this.messageLabel = '{author}, {time}';
42
45
  this.threshold = 48;
43
46
  this.turns = [];
44
47
  this.pinnedState = undefined;
45
48
  this.announcement = '';
46
49
  this.said = 0;
50
+ this.words = TURN_WORDS;
47
51
  this.watcher = undefined;
48
52
  this.settling = undefined;
49
53
  this.grower = undefined;
@@ -361,6 +365,9 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
361
365
  willUpdate(changed) {
362
366
  super.willUpdate(changed);
363
367
  hostAria(this.internals, { label: this.label || 'Conversation', role: 'region' });
368
+ if (changed.has('meLabel') || changed.has('messageLabel')) {
369
+ this.words = { me: this.meLabel, sentence: this.messageLabel };
370
+ }
364
371
  this.countRegions();
365
372
  if (changed.has('history')) {
366
373
  this.asked = false;
@@ -389,6 +396,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
389
396
  updated(changed) {
390
397
  super.updated(changed);
391
398
  this.assign();
399
+ this.tellTurns();
392
400
  if (this.pinned) {
393
401
  this.stick();
394
402
  }
@@ -403,6 +411,13 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
403
411
  this.reachHistory();
404
412
  this.announce();
405
413
  }
414
+ tellTurns() {
415
+ for (const message of this.messages) {
416
+ whenHydrated(message, () => {
417
+ message.turnWords = this.words;
418
+ });
419
+ }
420
+ }
406
421
  renderTurns() {
407
422
  const { after, before, first, last } = this.virtualize.window;
408
423
  const message = this.tag('chat-message');
@@ -414,7 +429,9 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
414
429
  meta: message-meta, retry: message-retry, sources: message-sources,
415
430
  state: message-state, time: message-time${secondNames(SECOND_NAMES.author, turn.author)}${secondNames(SECOND_NAMES.state, turn.state)}"
416
431
  author=${turn.author ?? nothing}
432
+ author-label=${turn.authorLabel ?? nothing}
417
433
  at=${turn.at ?? nothing}
434
+ .turnWords=${this.words}
418
435
  state=${turn.state ?? nothing}
419
436
  >${turn.text}</${message}>
420
437
  </div>`)}
@@ -469,6 +486,12 @@ __decorate([
469
486
  __decorate([
470
487
  property()
471
488
  ], HelixHeadlessChat.prototype, "label", void 0);
489
+ __decorate([
490
+ property({ attribute: 'me-label' })
491
+ ], HelixHeadlessChat.prototype, "meLabel", void 0);
492
+ __decorate([
493
+ property({ attribute: 'message-label' })
494
+ ], HelixHeadlessChat.prototype, "messageLabel", void 0);
472
495
  __decorate([
473
496
  property({ type: Boolean })
474
497
  ], HelixHeadlessChat.prototype, "pinned", null);
@@ -83,6 +83,10 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
83
83
  copiedLabel: string;
84
84
  /**
85
85
  * The copy button's name and tooltip text before a copy.
86
+ *
87
+ * Where the block has a name, the button says it too, such as "Copy code,
88
+ * app.ts", so each copy button on a page with several blocks is told apart.
89
+ * In a diff, that is each file's own name.
86
90
  */
87
91
  copyLabel: string;
88
92
  /**
@@ -207,8 +211,11 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
207
211
  * interpolated rather than written, so `tier` resolves it: this renders the
208
212
  * base button, and a styled subclass that sets `tier` renders the styled one
209
213
  * from the same template.
214
+ *
215
+ * @param value The text the button copies.
216
+ * @param name The name of what it copies, which its label carries.
210
217
  */
211
- protected renderCopyButton(value?: string): TemplateResult;
218
+ protected renderCopyButton(value?: string, name?: string): TemplateResult;
212
219
  /** A number padded to the widest in its file, so the columns line up as text. */
213
220
  private renderNumber;
214
221
  private renderLine;
@@ -83,13 +83,13 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
83
83
  }
84
84
  renderHeader(name, value) {
85
85
  return name
86
- ? html `<div part="header"><span part="label">${name}</span>${this.renderCopy(value)}</div>`
86
+ ? html `<div part="header"><span part="label">${name}</span>${this.renderCopy(value, name)}</div>`
87
87
  : nothing;
88
88
  }
89
- renderCopy(value) {
90
- return this.noCopy ? nothing : this.renderCopyButton(value);
89
+ renderCopy(value, name) {
90
+ return this.noCopy ? nothing : this.renderCopyButton(value, name);
91
91
  }
92
- renderCopyButton(value = this.text) {
92
+ renderCopyButton(value = this.text, name = this.label) {
93
93
  return html `<${this.tag('copy-button')}
94
94
  part="copy"
95
95
  exportparts="button: copy-button, text: copy-text, mark: copy-mark, live: copy-live, tooltip: copy-tooltip,
@@ -98,7 +98,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
98
98
  tooltip-arrow-top: copy-tooltip-arrow-top, tooltip-bottom: copy-tooltip-bottom,
99
99
  tooltip-left: copy-tooltip-left, tooltip-right: copy-tooltip-right, tooltip-top: copy-tooltip-top"
100
100
  copied-label=${this.copiedLabel}
101
- label=${this.copyLabel}
101
+ label=${name ? `${this.copyLabel}, ${name}` : this.copyLabel}
102
102
  .value=${value}
103
103
  ></${this.tag('copy-button')}>`;
104
104
  }