@fusion.dev/helix-headless 0.215.3 → 0.216.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 (113) hide show
  1. package/api.json +1 -1
  2. package/custom-elements.json +1 -1
  3. package/dist/area-chart/root/area-chart-root.d.ts +1 -0
  4. package/dist/area-chart/root/area-chart-root.js +1 -1
  5. package/dist/bar-chart/root/bar-chart-root.d.ts +1 -0
  6. package/dist/bar-chart/root/bar-chart-root.js +1 -1
  7. package/dist/button/root/button-root.d.ts +3 -1
  8. package/dist/button/root/button-root.js +2 -1
  9. package/dist/chat/composer/chat-composer.d.ts +18 -1
  10. package/dist/chat/composer/chat-composer.js +8 -3
  11. package/dist/checkbox/root/checkbox-root.d.ts +3 -1
  12. package/dist/checkbox/root/checkbox-root.js +2 -1
  13. package/dist/code-block/highlight.d.ts +9 -0
  14. package/dist/code-block/highlight.js +39 -0
  15. package/dist/code-block/highlighter.d.ts +44 -0
  16. package/dist/code-block/highlighter.js +44 -0
  17. package/dist/code-block/index.d.ts +1 -0
  18. package/dist/code-block/index.js +1 -0
  19. package/dist/code-block/keyed-lines.d.ts +23 -0
  20. package/dist/code-block/keyed-lines.js +75 -0
  21. package/dist/code-block/root/code-block-root.d.ts +6 -23
  22. package/dist/code-block/root/code-block-root.js +8 -25
  23. package/dist/code-editor/index.d.ts +1 -0
  24. package/dist/code-editor/index.js +1 -0
  25. package/dist/code-editor/root/code-editor-root.d.ts +367 -0
  26. package/dist/code-editor/root/code-editor-root.js +746 -0
  27. package/dist/code-input/index.d.ts +1 -0
  28. package/dist/code-input/index.js +1 -0
  29. package/dist/code-input/root/code-input-root.d.ts +119 -0
  30. package/dist/code-input/root/code-input-root.js +142 -0
  31. package/dist/combobox/root/combobox-root.d.ts +6 -1
  32. package/dist/combobox/root/combobox-root.js +4 -2
  33. package/dist/command/root/command-root.d.ts +61 -11
  34. package/dist/command/root/command-root.js +88 -3
  35. package/dist/donut-chart/root/donut-chart-root.d.ts +1 -0
  36. package/dist/donut-chart/root/donut-chart-root.js +1 -1
  37. package/dist/element-events.generated.js +36 -3
  38. package/dist/filter-input/root/filter-input-root.d.ts +9 -1
  39. package/dist/filter-input/root/filter-input-root.js +7 -3
  40. package/dist/heatmap/root/heatmap-root.d.ts +1 -0
  41. package/dist/heatmap/root/heatmap-root.js +1 -1
  42. package/dist/index.d.ts +4 -0
  43. package/dist/index.js +4 -0
  44. package/dist/input/root/input-root.d.ts +14 -2
  45. package/dist/input/root/input-root.js +11 -3
  46. package/dist/kbd/root/kbd-root.js +9 -2
  47. package/dist/line-chart/root/line-chart-root.d.ts +1 -0
  48. package/dist/line-chart/root/line-chart-root.js +1 -1
  49. package/dist/listbox/root/listbox-root.d.ts +8 -1
  50. package/dist/listbox/root/listbox-root.js +7 -4
  51. package/dist/markdown/blocks.d.ts +66 -0
  52. package/dist/markdown/blocks.js +326 -0
  53. package/dist/markdown/index.d.ts +6 -0
  54. package/dist/markdown/index.js +5 -0
  55. package/dist/markdown/inline.d.ts +57 -0
  56. package/dist/markdown/inline.js +483 -0
  57. package/dist/markdown/root/markdown-root.d.ts +99 -0
  58. package/dist/markdown/root/markdown-root.js +93 -0
  59. package/dist/markdown/stream.d.ts +47 -0
  60. package/dist/markdown/stream.js +62 -0
  61. package/dist/markdown/tree.d.ts +57 -0
  62. package/dist/markdown/tree.js +144 -0
  63. package/dist/markdown/writer.d.ts +33 -0
  64. package/dist/markdown/writer.js +139 -0
  65. package/dist/menu/root/menu-root.d.ts +53 -0
  66. package/dist/menu/root/menu-root.js +92 -15
  67. package/dist/pagination/size/pagination-size.d.ts +3 -0
  68. package/dist/pagination/size/pagination-size.js +2 -1
  69. package/dist/scatter-chart/root/scatter-chart-root.d.ts +1 -0
  70. package/dist/scatter-chart/root/scatter-chart-root.js +1 -1
  71. package/dist/select/root/select-root.d.ts +6 -1
  72. package/dist/select/root/select-root.js +4 -2
  73. package/dist/shortcut-overlay/index.d.ts +1 -0
  74. package/dist/shortcut-overlay/index.js +1 -0
  75. package/dist/shortcut-overlay/root/shortcut-overlay-root.d.ts +128 -0
  76. package/dist/shortcut-overlay/root/shortcut-overlay-root.js +275 -0
  77. package/dist/slider/root/slider-root.d.ts +3 -1
  78. package/dist/slider/root/slider-root.js +2 -1
  79. package/dist/spatial-bar-chart/root/spatial-bar-chart-root.d.ts +1 -0
  80. package/dist/spatial-bar-chart/root/spatial-bar-chart-root.js +1 -1
  81. package/dist/spatial-heatmap/root/spatial-heatmap-root.d.ts +1 -0
  82. package/dist/spatial-heatmap/root/spatial-heatmap-root.js +1 -1
  83. package/dist/spatial-ribbon-chart/root/spatial-ribbon-chart-root.d.ts +1 -0
  84. package/dist/spatial-ribbon-chart/root/spatial-ribbon-chart-root.js +1 -1
  85. package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.d.ts +1 -0
  86. package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.js +1 -1
  87. package/dist/spatial-surface-chart/root/spatial-surface-chart-root.d.ts +1 -0
  88. package/dist/spatial-surface-chart/root/spatial-surface-chart-root.js +1 -1
  89. package/dist/spatial-treemap/root/spatial-treemap-root.d.ts +1 -0
  90. package/dist/spatial-treemap/root/spatial-treemap-root.js +1 -1
  91. package/dist/switch/root/switch-root.d.ts +3 -1
  92. package/dist/switch/root/switch-root.js +2 -1
  93. package/dist/tabs/tab/tab.d.ts +3 -1
  94. package/dist/tabs/tab/tab.js +2 -1
  95. package/dist/textarea/root/textarea-root.d.ts +20 -4
  96. package/dist/textarea/root/textarea-root.js +31 -23
  97. package/dist/time-input/root/time-input-root.d.ts +6 -0
  98. package/dist/time-input/root/time-input-root.js +4 -2
  99. package/dist/time-picker/root/time-picker-root.d.ts +6 -0
  100. package/dist/time-picker/root/time-picker-root.js +4 -2
  101. package/dist/toast/root/toast-root.d.ts +64 -0
  102. package/dist/toast/root/toast-root.js +128 -3
  103. package/dist/toggle/root/toggle-root.d.ts +3 -1
  104. package/dist/toggle/root/toggle-root.js +2 -1
  105. package/dist/virtual-list/root/virtual-list-root.d.ts +18 -2
  106. package/dist/virtual-list/root/virtual-list-root.js +103 -5
  107. package/examples.json +1 -1
  108. package/lint.json +1 -1
  109. package/package.json +6 -6
  110. package/types/jsx.d.ts +5315 -596
  111. package/types/svelte.d.ts +4 -0
  112. package/types/vue.d.ts +1085 -4
  113. package/types/words.d.ts +491 -0
@@ -120,6 +120,7 @@ declare const HelixHeadlessAreaChart_base: typeof HelixElement & (new (...args:
120
120
  * @csspart loading - The words in place of the drawing while `loading` is set and there are no readings: the `loading` slot, falling back to `loading-text`.
121
121
  * @csspart mark - The invisible point the tooltip is anchored to.
122
122
  * @csspart plot - The drawing, which takes focus to be read by key. With nothing to draw, a group named as the chart is, holding the `empty` or `loading` part.
123
+ * @csspart sonify - The button that plays the chart as sound, under `sonify`.
123
124
  * @csspart summary - The total and each part's share at the latest reading, written as a sentence for screen readers.
124
125
  * @csspart svg - The drawing surface the bands sit on.
125
126
  * @csspart table - The readings as a table, for a screen reader, rendered only where they were drawn from `data`, a page of them at a time past a few hundred. Hide it visually.
@@ -112,7 +112,7 @@ let HelixHeadlessAreaChart = class HelixHeadlessAreaChart extends MixinChartStat
112
112
  const slot = PLOT_WIDTH / rows.length;
113
113
  const x = linear([0, Math.max(rows.length - 1, 1)], [slot / 2, PLOT_WIDTH - slot / 2]);
114
114
  return html `${this.valueAxis ? this.renderAxis(y, domain) : nothing}${this.renderPlot(stack, rows.length, x, y, domain)}
115
- ${this.renderLabels(rows.map((row) => row.label))} ${this.renderSummary(stack, rows)}${this.renderTable()}`;
115
+ ${this.renderLabels(rows.map((row) => row.label))} ${this.renderSummary(stack, rows)}${this.renderSonify()}${this.renderTable()}`;
116
116
  }
117
117
  renderAxis(y, domain) {
118
118
  const locale = wordsLocale(this);
@@ -104,6 +104,7 @@ declare const HelixHeadlessBarChart_base: typeof HelixElement & (new (...args: a
104
104
  * @csspart loading - The words in place of the drawing while `loading` is set and there are no readings: the `loading` slot, falling back to `loading-text`.
105
105
  * @csspart mark - The invisible point the tooltip is anchored to.
106
106
  * @csspart plot - The drawing, which takes focus to be read by key. With nothing to draw, a group named as the chart is, holding the `empty` or `loading` part.
107
+ * @csspart sonify - The button that plays the chart as sound, under `sonify`.
107
108
  * @csspart summary - The chart's name and each series' highest reading, written as a sentence for screen readers. Hide it visually.
108
109
  * @csspart svg - The drawing surface the bars and lines sit on.
109
110
  * @csspart table - The readings as a table, for a screen reader, rendered only where they were drawn from `data`, a page of them at a time past a few hundred. Hide it visually.
@@ -63,7 +63,7 @@ let HelixHeadlessBarChart = class HelixHeadlessBarChart extends MixinChartStatus
63
63
  : undefined;
64
64
  return html `${z === undefined && !this.valueAxis
65
65
  ? nothing
66
- : this.renderAxis(y, domain, 'primary', sideways, widest)}${this.renderPlot(series, categories, y, domain, floor, z)}${z === undefined ? nothing : this.renderAxis(z, other, 'secondary', sideways, widest, tint)}${this.renderLabels(categories)}${this.renderSummary(series)}${this.renderTable()}`;
66
+ : this.renderAxis(y, domain, 'primary', sideways, widest)}${this.renderPlot(series, categories, y, domain, floor, z)}${z === undefined ? nothing : this.renderAxis(z, other, 'secondary', sideways, widest, tint)}${this.renderLabels(categories)}${this.renderSummary(series)}${this.renderSonify()}${this.renderTable()}`;
67
67
  }
68
68
  frameOf(series, count) {
69
69
  const key = [
@@ -1,4 +1,4 @@
1
- import { AriaForwardController, HelixFormElementBase } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixFormElementBase, ListedController } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  /**
4
4
  * @summary Triggers an action, submits a form, or navigates.
@@ -66,6 +66,8 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
66
66
  * @interactive button
67
67
  */
68
68
  export declare class HelixHeadlessButton extends HelixFormElementBase {
69
+ /** On the page's list of controls, where a command palette and a shortcut overlay find it. */
70
+ protected readonly listed: ListedController;
69
71
  /**
70
72
  * The key this family's words are registered under, its styled tag:
71
73
  * `registerWords('de', { 'helix-button': { pendingLabel: 'Wird geladen' } })`.
@@ -1,5 +1,5 @@
1
1
  import { __decorate } from "tslib";
2
- import { AriaForwardController, customElement, defaultButton, drawStatus, elementById, HelixFallbackCommandEvent, HelixFormElementBase, HelixPressEvent, pressType, StatusController, submitsOnEnter, supportsInvokers, words, } from '@fusion.dev/helix-core';
2
+ import { AriaForwardController, customElement, defaultButton, drawStatus, elementById, HelixFallbackCommandEvent, HelixFormElementBase, HelixPressEvent, ListedController, pressType, StatusController, submitsOnEnter, supportsInvokers, words, } from '@fusion.dev/helix-core';
3
3
  import { html, nothing } from 'lit';
4
4
  import { property } from 'lit/decorators.js';
5
5
  import { submitAs } from '../submitter.js';
@@ -21,6 +21,7 @@ const afterDispatch = (click, then) => {
21
21
  let HelixHeadlessButton = class HelixHeadlessButton extends HelixFormElementBase {
22
22
  constructor() {
23
23
  super(...arguments);
24
+ this.listed = new ListedController(this, 'press', () => this.internals?.ariaKeyShortcuts);
24
25
  this.aria = new AriaForwardController(this, {
25
26
  label: () => this.forwardsLabel,
26
27
  target: () => this.partNamed('button'),
@@ -1,5 +1,6 @@
1
1
  import { HelixFormElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
+ declare const HelixHeadlessChatComposer_base: (new (...args: any[]) => import("@fusion.dev/helix-core").VoiceElement) & typeof HelixFormElement;
3
4
  /**
4
5
  * @summary A field for writing a turn in a conversation.
5
6
  *
@@ -15,6 +16,13 @@ import { type PropertyValues, type TemplateResult } from 'lit';
15
16
  * The composer clears only when no listener cancels the send, so the text is
16
17
  * kept when you cancel a send that failed.
17
18
  *
19
+ * **Set `voice` for dictation.** A microphone button before send listens
20
+ * through the page's speech engine, the browser's own unless
21
+ * `registerSpeechEngine` gave the page another, and types what it hears at the
22
+ * caret as the reader speaks, each guess replaced by the next. Press it again,
23
+ * press Escape, or send, and it stops. Where the browser has no engine, as in
24
+ * Firefox, no button is drawn.
25
+ *
18
26
  * **In a form, the draft is the composer's value**, submitted under `name`,
19
27
  * and an empty draft submits an empty entry, as a native textarea does. A
20
28
  * form reset puts back the `value` the markup declared, and a `<fieldset
@@ -48,6 +56,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
48
56
  * @slot end - Content under the field, beside the send button.
49
57
  * @slot start - Content under the field at the leading edge, such as an attachment button or a model picker.
50
58
  *
59
+ * @event {HelixBeforeListenEvent} helix-before-listen - The dictation button was pressed. Cancelable: cancel it to keep the composer from listening.
60
+ * @event {HelixListenEvent} helix-listen - Dictation settled on a phrase and put it in the draft, after the draft's own `input`.
51
61
  * @event {HelixSendEvent} helix-send - Fired when the user sends their turn, and cancelable.
52
62
  * @event {HelixTypingEvent} helix-typing - The user started or stopped writing, with `typing` saying which, as the composer's `typing` does.
53
63
  * @event {Event} input - The user changed the text. Read it from `value`, for a typing indicator or a saved draft. Bubbles and is composed, as a native field's is.
@@ -60,10 +70,15 @@ import { type PropertyValues, type TemplateResult } from 'lit';
60
70
  * @csspart send - The send button.
61
71
  * @csspart start - The box around the `start` slot.
62
72
  * @csspart tools - The row under the field.
73
+ * @csspart voice - The dictation button, under `voice`, before the send button.
74
+ * @csspart voice-listening - The dictation button while it listens. Set alongside `voice`.
75
+ * @csspart voice-status - What a screen reader hears as dictation starts and stops, drawn for no one else.
63
76
  *
64
77
  * @cssstate invalid - The draft fails `required`, `minlength`, `maxlength`, or a custom error, and the reader has tried to send or submit it, or edited the field and left it. Or `feedback` is set.
78
+ * @cssstate listening - While the composer listens for dictation. The element sets `--dictation-level` on itself meanwhile, from 0 to 1, for a meter.
65
79
  */
66
- export declare class HelixHeadlessChatComposer extends HelixFormElement {
80
+ export declare class HelixHeadlessChatComposer extends HelixHeadlessChatComposer_base {
81
+ protected static markRules: string[];
67
82
  /**
68
83
  * The key this part's words are registered under, its styled tag:
69
84
  * `registerWords('de', { 'helix-chat-composer': { sendLabel: 'Senden' } })`.
@@ -158,6 +173,7 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
158
173
  get typing(): boolean;
159
174
  /** Records whether the user is writing, and says so when that changed. */
160
175
  private markTyping;
176
+ protected voiceField(): HTMLTextAreaElement | null;
161
177
  /**
162
178
  * Sends what is written, and returns whether the send went through.
163
179
  *
@@ -254,3 +270,4 @@ declare global {
254
270
  'helix-headless-chat-composer': HelixHeadlessChatComposer;
255
271
  }
256
272
  }
273
+ export {};
@@ -1,11 +1,11 @@
1
1
  import { __decorate } from "tslib";
2
- import { customElement, DelayController, HelixFormElement, HelixSendEvent, HelixTypingEvent, hostAria, markRegion, numberConverter, timeConverter, words, } from '@fusion.dev/helix-core';
2
+ import { customElement, DelayController, HelixFormElement, HelixSendEvent, HelixTypingEvent, hostAria, MixinVoice, markRegion, numberConverter, SPOKEN_RULE, timeConverter, words, } from '@fusion.dev/helix-core';
3
3
  import { arrowElbowDownLeft } 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
- let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFormElement {
8
+ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends MixinVoice(HelixFormElement) {
9
9
  constructor() {
10
10
  super(...arguments);
11
11
  this.feedback = '';
@@ -53,6 +53,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
53
53
  this.send();
54
54
  };
55
55
  }
56
+ static { this.markRules = [SPOKEN_RULE]; }
56
57
  static { this.words = 'helix-chat-composer'; }
57
58
  get typing() {
58
59
  return this.writing;
@@ -70,7 +71,11 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
70
71
  this.writing = typing;
71
72
  this.dispatchEvent(new HelixTypingEvent({ typing }));
72
73
  }
74
+ voiceField() {
75
+ return this.partNamed('field');
76
+ }
73
77
  send() {
78
+ this.stopListening();
74
79
  const value = this.value.trim();
75
80
  if (this.isDisabled) {
76
81
  return false;
@@ -183,7 +188,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
183
188
  <div part="tools">
184
189
  <span part="start"><slot name="start"></slot></span>
185
190
  <span part="end"><slot name="end"></slot></span>
186
- ${this.renderSend()}
191
+ ${this.renderVoice()}${this.renderSend()}
187
192
  </div>
188
193
  </div>${this.feedback === ''
189
194
  ? nothing
@@ -1,4 +1,4 @@
1
- import { AriaForwardController, HelixFormElement, HostClickController } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixFormElement, HostClickController, ListedController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessCheckbox_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerCheckedInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
4
4
  /**
@@ -66,6 +66,8 @@ declare const HelixHeadlessCheckbox_base: (new (...args: any[]) => import("@fusi
66
66
  * @cssstate invalid - The control has an error to show, from the browser's validation or from your `feedback`.
67
67
  */
68
68
  export declare class HelixHeadlessCheckbox extends HelixHeadlessCheckbox_base {
69
+ /** On the page's list of controls, where a command palette and a shortcut overlay find it. */
70
+ protected readonly listed: ListedController;
69
71
  /**
70
72
  * The key this family's words are registered under, its styled tag:
71
73
  * `registerWords('de', { 'helix-checkbox': { valueMissingMessage: '...' } })`.
@@ -1,10 +1,11 @@
1
1
  import { __decorate } from "tslib";
2
- import { AriaForwardController, customElement, HelixFormElement, HostClickController, MixinControllerChecked, MixinField, slotFilled, words, } from '@fusion.dev/helix-core';
2
+ import { AriaForwardController, customElement, HelixFormElement, HostClickController, ListedController, MixinControllerChecked, MixinField, slotFilled, words, } from '@fusion.dev/helix-core';
3
3
  import { html, nothing } from 'lit';
4
4
  import { property } from 'lit/decorators.js';
5
5
  let HelixHeadlessCheckbox = class HelixHeadlessCheckbox extends MixinControllerChecked(MixinField(HelixFormElement)) {
6
6
  constructor() {
7
7
  super(...arguments);
8
+ this.listed = new ListedController(this, 'press', () => this.internals?.ariaKeyShortcuts);
8
9
  this.aria = new AriaForwardController(this, {
9
10
  target: () => this.partNamed('control'),
10
11
  });
@@ -13,3 +13,12 @@
13
13
  * box and colors, so the wrapper's `style` and `class` go with it.
14
14
  */
15
15
  export declare const highlightedLines: (markup: string) => string;
16
+ /**
17
+ * Highlighted markup cut into one string per line, each closing what is open
18
+ * at its end and opening it again on the next, so a token that runs over a
19
+ * line break, such as a block comment, is drawn whole on both lines.
20
+ *
21
+ * What lets a block that grows redraw only the lines that changed: a line is
22
+ * replaced when its own markup differs, and every other line keeps its nodes.
23
+ */
24
+ export declare const highlightedLineList: (markup: string) => string[];
@@ -2,3 +2,42 @@ export const highlightedLines = (markup) => {
2
2
  const lines = /^\s*<pre\b[^>]*>\s*<code\b[^>]*>([\s\S]*)<\/code>\s*<\/pre>\s*$/i.exec(markup)?.[1];
3
3
  return lines === undefined || /<\/?pre\b/i.test(lines) ? markup : lines;
4
4
  };
5
+ const VOID_TAGS = new Set(['br', 'img', 'wbr']);
6
+ export const highlightedLineList = (markup) => {
7
+ const lines = [];
8
+ const open = [];
9
+ let line = '';
10
+ for (const [, tag, text] of markup.matchAll(/(<[^>]*>)|([^<]+)/g)) {
11
+ if (tag !== undefined) {
12
+ line += tag;
13
+ const name = /^<\/?([A-Za-z][\w-]*)/.exec(tag)?.[1]?.toLowerCase();
14
+ if (name === undefined || VOID_TAGS.has(name) || tag.endsWith('/>')) {
15
+ continue;
16
+ }
17
+ if (tag.startsWith('</')) {
18
+ const at = open.map((one) => one.name).lastIndexOf(name);
19
+ if (at >= 0) {
20
+ open.splice(at);
21
+ }
22
+ }
23
+ else {
24
+ open.push({ name, tag });
25
+ }
26
+ continue;
27
+ }
28
+ const pieces = (text ?? '').split('\n');
29
+ pieces.forEach((piece, index) => {
30
+ if (index > 0) {
31
+ lines.push(line +
32
+ open
33
+ .map((one) => `</${one.name}>`)
34
+ .reverse()
35
+ .join(''));
36
+ line = open.map((one) => one.tag).join('');
37
+ }
38
+ line += piece;
39
+ });
40
+ }
41
+ lines.push(line);
42
+ return lines;
43
+ };
@@ -0,0 +1,44 @@
1
+ import type { CSSResult, ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /**
3
+ * Turns code into highlighted markup for a `language`, or returns `undefined`
4
+ * for a language it does not read, which the element then draws plain.
5
+ */
6
+ export type CodeBlockHighlighter = (code: string, language: string) => string | undefined;
7
+ /**
8
+ * An element that draws with the registered highlighter: Code Block and Code
9
+ * Input. Told when the registration changes, so it draws again.
10
+ */
11
+ export interface HighlighterConsumer extends Element {
12
+ highlighterChanged(): void;
13
+ }
14
+ /** The highlighter a page registered, or nothing where it registered none. */
15
+ export declare const registeredHighlighter: () => CodeBlockHighlighter | undefined;
16
+ /**
17
+ * Keeps an element that draws with the registered highlighter on the page's
18
+ * list while it is connected, with the registered sheet in its shadow root,
19
+ * so a registration reaches it without searching the page.
20
+ */
21
+ export declare class HighlighterController implements ReactiveController {
22
+ private readonly host;
23
+ constructor(host: ReactiveControllerHost & HighlighterConsumer);
24
+ hostConnected(): void;
25
+ hostDisconnected(): void;
26
+ }
27
+ /**
28
+ * Gives every Code Block and Code Input of both tiers a highlighter, for the
29
+ * ones that set no `highlight` of their own, and the sheet that colors what
30
+ * it returns.
31
+ *
32
+ * **Opt-in, so a page that shows plain code carries no grammar.** Helix ships
33
+ * none: a grammar is the larger part of any highlighter, and an element that
34
+ * registers one for every page makes every page pay for it. A page that wants
35
+ * color calls this once, or imports an entry that calls it, such as
36
+ * `@fusion.dev/helix-sandbox/code-block`. `sheet` is adopted into every
37
+ * consumer's shadow root, for a highlighter that writes classes, since a
38
+ * page's own stylesheet does not reach inside one.
39
+ *
40
+ * An element's own `highlight` wins over this. Elements already on the page
41
+ * draw again with it, and every element built after has it. `undefined` takes
42
+ * it away again.
43
+ */
44
+ export declare const registerHighlighter: (highlighter: CodeBlockHighlighter | undefined, sheet?: CSSResult) => void;
@@ -0,0 +1,44 @@
1
+ const SLOT = Symbol.for('fusion.dev/helix/highlighter');
2
+ const store = globalThis;
3
+ store[SLOT] ??= { consumers: new Set(), registered: undefined, sheets: new Set() };
4
+ const registry = store[SLOT];
5
+ export const registeredHighlighter = () => registry.registered?.highlighter;
6
+ const adoptSheet = (root) => {
7
+ if (!root) {
8
+ return;
9
+ }
10
+ const sheet = registry.registered?.sheet?.styleSheet;
11
+ const kept = root.adoptedStyleSheets.filter((one) => one === sheet || !registry.sheets.has(one));
12
+ const next = sheet && !kept.includes(sheet) ? [...kept, sheet] : kept;
13
+ if (next.length !== root.adoptedStyleSheets.length ||
14
+ next.some((one, at) => one !== root.adoptedStyleSheets[at])) {
15
+ root.adoptedStyleSheets = next;
16
+ }
17
+ };
18
+ export class HighlighterController {
19
+ constructor(host) {
20
+ this.host = host;
21
+ host.addController(this);
22
+ }
23
+ hostConnected() {
24
+ registry.consumers.add(this.host);
25
+ adoptSheet(this.host.shadowRoot);
26
+ }
27
+ hostDisconnected() {
28
+ registry.consumers.delete(this.host);
29
+ }
30
+ }
31
+ export const registerHighlighter = (highlighter, sheet) => {
32
+ registry.registered = highlighter ? { highlighter, sheet } : undefined;
33
+ if (sheet?.styleSheet) {
34
+ registry.sheets.add(sheet.styleSheet);
35
+ }
36
+ for (const consumer of registry.consumers) {
37
+ if (!consumer.isConnected) {
38
+ registry.consumers.delete(consumer);
39
+ continue;
40
+ }
41
+ adoptSheet(consumer.shadowRoot);
42
+ consumer.highlighterChanged();
43
+ }
44
+ };
@@ -1,2 +1,3 @@
1
+ export * from './highlighter.js';
1
2
  export * from './patch.js';
2
3
  export * from './root/code-block-root.js';
@@ -1,2 +1,3 @@
1
+ export * from './highlighter.js';
1
2
  export * from './patch.js';
2
3
  export * from './root/code-block-root.js';
@@ -0,0 +1,23 @@
1
+ import { type ChildPart, Directive, type PartInfo } from 'lit/directive.js';
2
+ /**
3
+ * Draws highlighted markup line by line, and on each change replaces only the
4
+ * lines whose markup changed.
5
+ *
6
+ * **Why not `unsafeHTML`**, which this replaced: it rebuilds every node of the
7
+ * block on every change, so a block that grows a line at a time, as a streamed
8
+ * reply's does, threw away its whole drawing for each character, and a reader's
9
+ * selection in it with the drawing. Here an unchanged line keeps its nodes. A
10
+ * line break between lines is a text node of its own, as it was, so the shadow
11
+ * root holds the same nodes `unsafeHTML` drew and no rule that styles them sees
12
+ * a wrapper.
13
+ */
14
+ declare class KeyedLines extends Directive {
15
+ /** Each line's markup as drawn, and its nodes, the break before it first from the second line on. */
16
+ private lines;
17
+ constructor(info: PartInfo);
18
+ render(markup: string): unknown;
19
+ update(part: ChildPart, [markup]: [string]): unknown;
20
+ }
21
+ /** Highlighted markup drawn so that a change redraws only the lines it changed. */
22
+ export declare const keyedLines: (markup: string) => import("lit-html/directive.js").DirectiveResult<typeof KeyedLines>;
23
+ export {};
@@ -0,0 +1,75 @@
1
+ import { noChange } from 'lit';
2
+ import { Directive, directive, PartType } from 'lit/directive.js';
3
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
4
+ import { highlightedLineList } from './highlight.js';
5
+ class KeyedLines extends Directive {
6
+ constructor(info) {
7
+ super(info);
8
+ this.lines = [];
9
+ if (info.type !== PartType.CHILD) {
10
+ throw new Error('keyedLines belongs in a child position');
11
+ }
12
+ }
13
+ render(markup) {
14
+ return unsafeHTML(markup);
15
+ }
16
+ update(part, [markup]) {
17
+ const parent = part.parentNode;
18
+ const document = parent.ownerDocument ?? parent;
19
+ const end = part.endNode;
20
+ const start = part.startNode;
21
+ if (end === null &&
22
+ this.lines.length === 0 &&
23
+ start?.nodeType === 8 &&
24
+ start.data.startsWith('lit-part')) {
25
+ return this.render(markup);
26
+ }
27
+ const next = highlightedLineList(markup);
28
+ if (this.lines.length === 0) {
29
+ for (let node = start?.nextSibling; node && node !== end;) {
30
+ const following = node.nextSibling;
31
+ node.parentNode?.removeChild(node);
32
+ node = following;
33
+ }
34
+ }
35
+ const parse = (line) => {
36
+ const template = document.createElement('template');
37
+ template.innerHTML = line;
38
+ return [...template.content.childNodes];
39
+ };
40
+ next.forEach((line, index) => {
41
+ const drawn = this.lines[index];
42
+ if (drawn?.markup === line) {
43
+ return;
44
+ }
45
+ const nodes = parse(line);
46
+ if (drawn) {
47
+ const own = index > 0 ? drawn.nodes.slice(1) : drawn.nodes;
48
+ const after = this.lines[index + 1]?.nodes[0] ?? end;
49
+ for (const node of nodes) {
50
+ parent.insertBefore(node, after);
51
+ }
52
+ for (const node of own) {
53
+ node.parentNode?.removeChild(node);
54
+ }
55
+ this.lines[index] = {
56
+ markup: line,
57
+ nodes: index > 0 ? [drawn.nodes[0], ...nodes] : nodes,
58
+ };
59
+ return;
60
+ }
61
+ const all = index > 0 ? [document.createTextNode('\n'), ...nodes] : nodes;
62
+ for (const node of all) {
63
+ parent.insertBefore(node, end);
64
+ }
65
+ this.lines[index] = { markup: line, nodes: all };
66
+ });
67
+ for (const gone of this.lines.splice(next.length)) {
68
+ for (const node of gone.nodes) {
69
+ node.parentNode?.removeChild(node);
70
+ }
71
+ }
72
+ return noChange;
73
+ }
74
+ }
75
+ export const keyedLines = directive(KeyedLines);
@@ -1,28 +1,7 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type CSSResult, type PropertyValues, type TemplateResult } from 'lit';
2
+ import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../copy-button/index.js';
4
- /**
5
- * Turns code into highlighted markup for a block's `language`, or returns
6
- * `undefined` for a language it does not read, which the block then draws
7
- * plain.
8
- */
9
- export type CodeBlockHighlighter = (code: string, language: string) => string | undefined;
10
- /**
11
- * Gives every Code Block of both tiers a highlighter, for the blocks that set
12
- * no `highlight` of their own, and the sheet that colors what it returns.
13
- *
14
- * **Opt-in, so a page that shows plain code carries no grammar.** Code Block
15
- * ships none: a grammar is the larger part of any highlighter, and a block
16
- * that registers one for every page makes every page pay for it. A page that
17
- * wants color calls this once, or imports an entry that calls it. `sheet` is
18
- * adopted into every block's shadow root, for a highlighter that writes
19
- * classes, since a page's own stylesheet does not reach inside a block.
20
- *
21
- * A block's own `highlight` wins over this. Blocks already on the page draw
22
- * again with it, and every block built after has it. `undefined` takes it
23
- * away again.
24
- */
25
- export declare const registerHighlighter: (highlighter: CodeBlockHighlighter | undefined, sheet?: CSSResult) => void;
4
+ import { HighlighterController } from '../highlighter.js';
26
5
  /**
27
6
  * @summary A block of code with an optional name and a copy button.
28
7
  *
@@ -524,6 +503,10 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
524
503
  connectedCallback(): void;
525
504
  disconnectedCallback(): void;
526
505
  protected firstUpdated(changed: PropertyValues): void;
506
+ /** On the page's list of elements a highlighter registration reaches. */
507
+ protected readonly highlighting: HighlighterController;
508
+ /** The page registered another highlighter: draws again with it. */
509
+ highlighterChanged(): void;
527
510
  protected render(): TemplateResult;
528
511
  /**
529
512
  * The button that shows every line of a capped block, and fewer again, or
@@ -2,33 +2,13 @@ import { __decorate } from "tslib";
2
2
  import { customElement, formatWords, HelixBeforeToggleEvent, HelixElement, HelixToggleEvent, inlineStep, requestChange, requestValue, stopPress, words, } from '@fusion.dev/helix-core';
3
3
  import { nothing } from 'lit';
4
4
  import { property, state } from 'lit/decorators.js';
5
- import { unsafeHTML } from 'lit/directives/unsafe-html.js';
6
5
  import { html } from 'lit/static-html.js';
7
6
  import '../../copy-button/index.js';
8
7
  import { highlightedLines } from '../highlight.js';
8
+ import { HighlighterController, registeredHighlighter } from '../highlighter.js';
9
+ import { keyedLines } from '../keyed-lines.js';
9
10
  import { gutterNumbers, lineCount, lineRuns, lineSpans } from '../lines.js';
10
11
  import { foldContext, pairLines, parsePatch, } from '../patch.js';
11
- let registered;
12
- const adoptRegistered = (block) => {
13
- const root = block.shadowRoot;
14
- const sheet = registered?.sheet?.styleSheet;
15
- if (root && sheet && !root.adoptedStyleSheets.includes(sheet)) {
16
- root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
17
- }
18
- };
19
- const blocksOnPage = (root = document) => [...root.querySelectorAll('*')].flatMap((node) => [
20
- ...(node instanceof HelixHeadlessCodeBlock ? [node] : []),
21
- ...(node.shadowRoot ? blocksOnPage(node.shadowRoot) : []),
22
- ]);
23
- export const registerHighlighter = (highlighter, sheet) => {
24
- registered = highlighter ? { highlighter, sheet } : undefined;
25
- if (typeof globalThis.document?.querySelectorAll === 'function') {
26
- for (const block of blocksOnPage()) {
27
- adoptRegistered(block);
28
- block.requestUpdate();
29
- }
30
- }
31
- };
32
12
  const isBlock = (element) => element?.localName.endsWith('code-block') === true;
33
13
  const blocksIn = (host) => [...host.children].filter((child) => child.slot === '' && isBlock(child));
34
14
  const nameOf = (block) => {
@@ -102,6 +82,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
102
82
  };
103
83
  this.hid = new Set();
104
84
  this.fileCount = 0;
85
+ this.highlighting = new HighlighterController(this);
105
86
  }
106
87
  static { this.words = 'helix-code-block'; }
107
88
  toggleWrap() {
@@ -352,7 +333,9 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
352
333
  firstUpdated(changed) {
353
334
  super.firstUpdated(changed);
354
335
  this.finishHydration();
355
- adoptRegistered(this);
336
+ }
337
+ highlighterChanged() {
338
+ this.requestUpdate();
356
339
  }
357
340
  render() {
358
341
  const blocks = this.blocks;
@@ -365,7 +348,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
365
348
  }
366
349
  const text = this.text;
367
350
  let highlighted;
368
- const highlight = this.highlight ?? registered?.highlighter;
351
+ const highlight = this.highlight ?? registeredHighlighter();
369
352
  try {
370
353
  const markup = highlight && text !== '' ? highlight(text, this.language) : undefined;
371
354
  highlighted = markup === undefined ? undefined : highlightedLines(markup);
@@ -382,7 +365,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
382
365
  : [];
383
366
  const code = highlighted === undefined
384
367
  ? html `<code part="code">${marks}<slot @slotchange=${() => this.requestUpdate()}></slot></code>`
385
- : html `<code part="code">${marks}${unsafeHTML(highlighted)}</code><slot
368
+ : html `<code part="code">${marks}${keyedLines(highlighted)}</code><slot
386
369
  hidden
387
370
  @slotchange=${() => this.requestUpdate()}
388
371
  ></slot>`;
@@ -0,0 +1 @@
1
+ export * from './root/code-editor-root.js';
@@ -0,0 +1 @@
1
+ export * from './root/code-editor-root.js';