@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.
- package/api.json +1 -1
- package/custom-elements.json +1 -1
- package/dist/area-chart/root/area-chart-root.d.ts +1 -0
- package/dist/area-chart/root/area-chart-root.js +1 -1
- package/dist/bar-chart/root/bar-chart-root.d.ts +1 -0
- package/dist/bar-chart/root/bar-chart-root.js +1 -1
- package/dist/button/root/button-root.d.ts +3 -1
- package/dist/button/root/button-root.js +2 -1
- package/dist/chat/composer/chat-composer.d.ts +18 -1
- package/dist/chat/composer/chat-composer.js +8 -3
- package/dist/checkbox/root/checkbox-root.d.ts +3 -1
- package/dist/checkbox/root/checkbox-root.js +2 -1
- package/dist/code-block/highlight.d.ts +9 -0
- package/dist/code-block/highlight.js +39 -0
- package/dist/code-block/highlighter.d.ts +44 -0
- package/dist/code-block/highlighter.js +44 -0
- package/dist/code-block/index.d.ts +1 -0
- package/dist/code-block/index.js +1 -0
- package/dist/code-block/keyed-lines.d.ts +23 -0
- package/dist/code-block/keyed-lines.js +75 -0
- package/dist/code-block/root/code-block-root.d.ts +6 -23
- package/dist/code-block/root/code-block-root.js +8 -25
- package/dist/code-editor/index.d.ts +1 -0
- package/dist/code-editor/index.js +1 -0
- package/dist/code-editor/root/code-editor-root.d.ts +367 -0
- package/dist/code-editor/root/code-editor-root.js +746 -0
- package/dist/code-input/index.d.ts +1 -0
- package/dist/code-input/index.js +1 -0
- package/dist/code-input/root/code-input-root.d.ts +119 -0
- package/dist/code-input/root/code-input-root.js +142 -0
- package/dist/combobox/root/combobox-root.d.ts +6 -1
- package/dist/combobox/root/combobox-root.js +4 -2
- package/dist/command/root/command-root.d.ts +61 -11
- package/dist/command/root/command-root.js +88 -3
- package/dist/donut-chart/root/donut-chart-root.d.ts +1 -0
- package/dist/donut-chart/root/donut-chart-root.js +1 -1
- package/dist/element-events.generated.js +36 -3
- package/dist/filter-input/root/filter-input-root.d.ts +9 -1
- package/dist/filter-input/root/filter-input-root.js +7 -3
- package/dist/heatmap/root/heatmap-root.d.ts +1 -0
- package/dist/heatmap/root/heatmap-root.js +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/input/root/input-root.d.ts +14 -2
- package/dist/input/root/input-root.js +11 -3
- package/dist/kbd/root/kbd-root.js +9 -2
- package/dist/line-chart/root/line-chart-root.d.ts +1 -0
- package/dist/line-chart/root/line-chart-root.js +1 -1
- package/dist/listbox/root/listbox-root.d.ts +8 -1
- package/dist/listbox/root/listbox-root.js +7 -4
- package/dist/markdown/blocks.d.ts +66 -0
- package/dist/markdown/blocks.js +326 -0
- package/dist/markdown/index.d.ts +6 -0
- package/dist/markdown/index.js +5 -0
- package/dist/markdown/inline.d.ts +57 -0
- package/dist/markdown/inline.js +483 -0
- package/dist/markdown/root/markdown-root.d.ts +99 -0
- package/dist/markdown/root/markdown-root.js +93 -0
- package/dist/markdown/stream.d.ts +47 -0
- package/dist/markdown/stream.js +62 -0
- package/dist/markdown/tree.d.ts +57 -0
- package/dist/markdown/tree.js +144 -0
- package/dist/markdown/writer.d.ts +33 -0
- package/dist/markdown/writer.js +139 -0
- package/dist/menu/root/menu-root.d.ts +53 -0
- package/dist/menu/root/menu-root.js +92 -15
- package/dist/pagination/size/pagination-size.d.ts +3 -0
- package/dist/pagination/size/pagination-size.js +2 -1
- package/dist/scatter-chart/root/scatter-chart-root.d.ts +1 -0
- package/dist/scatter-chart/root/scatter-chart-root.js +1 -1
- package/dist/select/root/select-root.d.ts +6 -1
- package/dist/select/root/select-root.js +4 -2
- package/dist/shortcut-overlay/index.d.ts +1 -0
- package/dist/shortcut-overlay/index.js +1 -0
- package/dist/shortcut-overlay/root/shortcut-overlay-root.d.ts +128 -0
- package/dist/shortcut-overlay/root/shortcut-overlay-root.js +275 -0
- package/dist/slider/root/slider-root.d.ts +3 -1
- package/dist/slider/root/slider-root.js +2 -1
- package/dist/spatial-bar-chart/root/spatial-bar-chart-root.d.ts +1 -0
- package/dist/spatial-bar-chart/root/spatial-bar-chart-root.js +1 -1
- package/dist/spatial-heatmap/root/spatial-heatmap-root.d.ts +1 -0
- package/dist/spatial-heatmap/root/spatial-heatmap-root.js +1 -1
- package/dist/spatial-ribbon-chart/root/spatial-ribbon-chart-root.d.ts +1 -0
- package/dist/spatial-ribbon-chart/root/spatial-ribbon-chart-root.js +1 -1
- package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.d.ts +1 -0
- package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.js +1 -1
- package/dist/spatial-surface-chart/root/spatial-surface-chart-root.d.ts +1 -0
- package/dist/spatial-surface-chart/root/spatial-surface-chart-root.js +1 -1
- package/dist/spatial-treemap/root/spatial-treemap-root.d.ts +1 -0
- package/dist/spatial-treemap/root/spatial-treemap-root.js +1 -1
- package/dist/switch/root/switch-root.d.ts +3 -1
- package/dist/switch/root/switch-root.js +2 -1
- package/dist/tabs/tab/tab.d.ts +3 -1
- package/dist/tabs/tab/tab.js +2 -1
- package/dist/textarea/root/textarea-root.d.ts +20 -4
- package/dist/textarea/root/textarea-root.js +31 -23
- package/dist/time-input/root/time-input-root.d.ts +6 -0
- package/dist/time-input/root/time-input-root.js +4 -2
- package/dist/time-picker/root/time-picker-root.d.ts +6 -0
- package/dist/time-picker/root/time-picker-root.js +4 -2
- package/dist/toast/root/toast-root.d.ts +64 -0
- package/dist/toast/root/toast-root.js +128 -3
- package/dist/toggle/root/toggle-root.d.ts +3 -1
- package/dist/toggle/root/toggle-root.js +2 -1
- package/dist/virtual-list/root/virtual-list-root.d.ts +18 -2
- package/dist/virtual-list/root/virtual-list-root.js +103 -5
- package/examples.json +1 -1
- package/lint.json +1 -1
- package/package.json +6 -6
- package/types/jsx.d.ts +5315 -596
- package/types/svelte.d.ts +4 -0
- package/types/vue.d.ts +1085 -4
- 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
|
|
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
|
+
};
|
package/dist/code-block/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 ??
|
|
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}${
|
|
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';
|