@fusion.dev/helix-core 0.221.0 → 0.223.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/custom-elements.json +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mixins/mixin-voice/mixin-voice.d.ts +5 -0
- package/dist/mixins/mixin-voice/mixin-voice.js +69 -14
- package/dist/utils/speech/speech.d.ts +26 -3
- package/dist/utils/speech/speech.js +156 -30
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -159,7 +159,7 @@ export { slotFilled, slotNamed } from './utils/slot-filled/slot-filled.js';
|
|
|
159
159
|
export { SlottedGlyph } from './utils/slotted-glyph/slotted-glyph.js';
|
|
160
160
|
export { slottedWords } from './utils/slotted-words/slotted-words.js';
|
|
161
161
|
export { ChartSound, HIGH, LOW, panOf, pitchOf, type Sound, type SoundSeries, sweepPlan, TIMBRES, } from './utils/sonify/sonify.js';
|
|
162
|
-
export { browserSpeech, pageSpeechEngine, registerSpeechEngine, type SpeechEngine, type SpeechListener, type SpeechSession, } from './utils/speech/speech.js';
|
|
162
|
+
export { browserSpeech, pageSpeechEngine, recognizeSpeech, registerSpeechEngine, type SpeechEngine, type SpeechListener, type SpeechSession, } from './utils/speech/speech.js';
|
|
163
163
|
export { splitValues } from './utils/split-values/split-values.js';
|
|
164
164
|
export { captureState, type ElementState, elementAt, exposeState, type PageState, pathOf, type RestoreReport, restoreOn, restoreState, STATE_SLOT, snapshotOf, } from './utils/state/state.js';
|
|
165
165
|
export { stopPress } from './utils/stop-press/stop-press.js';
|
package/dist/index.js
CHANGED
|
@@ -159,7 +159,7 @@ export { slotFilled, slotNamed } from './utils/slot-filled/slot-filled.js';
|
|
|
159
159
|
export { SlottedGlyph } from './utils/slotted-glyph/slotted-glyph.js';
|
|
160
160
|
export { slottedWords } from './utils/slotted-words/slotted-words.js';
|
|
161
161
|
export { ChartSound, HIGH, LOW, panOf, pitchOf, sweepPlan, TIMBRES, } from './utils/sonify/sonify.js';
|
|
162
|
-
export { browserSpeech, pageSpeechEngine, registerSpeechEngine, } from './utils/speech/speech.js';
|
|
162
|
+
export { browserSpeech, pageSpeechEngine, recognizeSpeech, registerSpeechEngine, } from './utils/speech/speech.js';
|
|
163
163
|
export { splitValues } from './utils/split-values/split-values.js';
|
|
164
164
|
export { captureState, elementAt, exposeState, pathOf, restoreOn, restoreState, STATE_SLOT, snapshotOf, } from './utils/state/state.js';
|
|
165
165
|
export { stopPress } from './utils/stop-press/stop-press.js';
|
|
@@ -3,6 +3,9 @@ import type { HelixElement } from '../../helix-element/helix-element.js';
|
|
|
3
3
|
import { type SpeechEngine } from '../../utils/speech/speech.js';
|
|
4
4
|
type Constructor<T> = new (...args: any[]) => T;
|
|
5
5
|
export declare class VoiceElement {
|
|
6
|
+
dictationDeniedLabel: string;
|
|
7
|
+
dictationFailedLabel: string;
|
|
8
|
+
dictationUnreachableLabel: string;
|
|
6
9
|
/** Whether the field is listening now. */
|
|
7
10
|
readonly listening: boolean;
|
|
8
11
|
listeningLabel: string;
|
|
@@ -17,6 +20,8 @@ export declare class VoiceElement {
|
|
|
17
20
|
stopListening(): void;
|
|
18
21
|
/** The dictation button and its status, behind a seam a tier can move. */
|
|
19
22
|
protected renderVoice(): TemplateResult | typeof nothing;
|
|
23
|
+
/** What the dictation button draws: the microphone, or a level meter while listening. */
|
|
24
|
+
protected renderVoiceGlyph(): TemplateResult;
|
|
20
25
|
/** The native field dictation types into. */
|
|
21
26
|
protected voiceField(): HTMLInputElement | HTMLTextAreaElement | null;
|
|
22
27
|
/** The type of the field dictation types into. Dictation listens only in `text`, `search`, `tel` and `url`. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { __decorate } from "tslib";
|
|
2
2
|
import { microphone } from '@fusion.dev/helix-icons';
|
|
3
|
-
import {
|
|
3
|
+
import { nothing } from 'lit';
|
|
4
4
|
import { property, state } from 'lit/decorators.js';
|
|
5
|
+
import { html } from 'lit/static-html.js';
|
|
5
6
|
import { HelixBeforeListenEvent, HelixListenEvent } from '../../events/events.js';
|
|
6
7
|
import { drawIcon } from '../../utils/draw-icon/draw-icon.js';
|
|
7
8
|
import { pageSpeechEngine, } from '../../utils/speech/speech.js';
|
|
@@ -14,6 +15,9 @@ export const MixinVoice = (Base) => {
|
|
|
14
15
|
class Voice extends Base {
|
|
15
16
|
constructor() {
|
|
16
17
|
super(...arguments);
|
|
18
|
+
this.dictationDeniedLabel = 'Dictation needs the microphone';
|
|
19
|
+
this.dictationFailedLabel = 'Dictation stopped with an error';
|
|
20
|
+
this.dictationUnreachableLabel = 'Dictation could not reach the speech service';
|
|
17
21
|
this.listeningLabel = 'Listening';
|
|
18
22
|
this.speechEngine = undefined;
|
|
19
23
|
this.stoppedListeningLabel = 'Stopped listening';
|
|
@@ -89,35 +93,66 @@ export const MixinVoice = (Base) => {
|
|
|
89
93
|
this.toggleState('listening', true);
|
|
90
94
|
field.addEventListener('keydown', this.handleVoiceKey);
|
|
91
95
|
field.addEventListener('pointerdown', this.handleVoicePointer);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.
|
|
96
|
+
let session;
|
|
97
|
+
try {
|
|
98
|
+
session = this.engine.listen({
|
|
99
|
+
lang: wordsLocale(this),
|
|
100
|
+
listener: {
|
|
101
|
+
end: (reason, message) => this.ended(reason, message),
|
|
102
|
+
final: (text) => this.place(text, true),
|
|
103
|
+
level: (level) => {
|
|
104
|
+
this.style.setProperty('--dictation-level', level.toFixed(3));
|
|
105
|
+
const meter = this.partNamed('voice-level');
|
|
106
|
+
if (meter) {
|
|
107
|
+
meter.level = level;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
partial: (text) => this.place(text, false),
|
|
99
111
|
},
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
this.ended();
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (!this.heard) {
|
|
119
|
+
try {
|
|
120
|
+
session.stop();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
this.session = session;
|
|
103
127
|
return true;
|
|
104
128
|
}
|
|
105
129
|
stopListening() {
|
|
106
130
|
this.session?.stop();
|
|
107
131
|
this.ended();
|
|
108
132
|
}
|
|
109
|
-
ended() {
|
|
133
|
+
ended(reason, message) {
|
|
110
134
|
if (!this.heard) {
|
|
111
135
|
return;
|
|
112
136
|
}
|
|
113
137
|
this.session = undefined;
|
|
114
138
|
this.heard = false;
|
|
115
|
-
this.said = this.stoppedListeningLabel;
|
|
139
|
+
this.said = reason === 'error' ? this.failure(message) : this.stoppedListeningLabel;
|
|
116
140
|
this.style.removeProperty('--dictation-level');
|
|
117
141
|
this.toggleState('listening', false);
|
|
118
142
|
this.voiceField()?.removeEventListener('keydown', this.handleVoiceKey);
|
|
119
143
|
this.voiceField()?.removeEventListener('pointerdown', this.handleVoicePointer);
|
|
120
144
|
}
|
|
145
|
+
failure(message) {
|
|
146
|
+
if (message === 'network') {
|
|
147
|
+
return this.dictationUnreachableLabel;
|
|
148
|
+
}
|
|
149
|
+
if (message === 'not-allowed' ||
|
|
150
|
+
message === 'service-not-allowed' ||
|
|
151
|
+
message === 'audio-capture') {
|
|
152
|
+
return this.dictationDeniedLabel;
|
|
153
|
+
}
|
|
154
|
+
return this.dictationFailedLabel;
|
|
155
|
+
}
|
|
121
156
|
place(text, settled) {
|
|
122
157
|
const field = this.voiceField();
|
|
123
158
|
if (!field || !this.heard) {
|
|
@@ -140,6 +175,14 @@ export const MixinVoice = (Base) => {
|
|
|
140
175
|
this.dispatchEvent(new HelixListenEvent({ text }));
|
|
141
176
|
}
|
|
142
177
|
}
|
|
178
|
+
renderVoiceGlyph() {
|
|
179
|
+
const tier = this.constructor.tier;
|
|
180
|
+
if (this.heard && customElements.get(`${tier}voice-level`)) {
|
|
181
|
+
const meter = this.tag('voice-level');
|
|
182
|
+
return html `<${meter} part="voice-level" exportparts="canvas: voice-level-canvas" aria-hidden="true" variant="spectrum"></${meter}>`;
|
|
183
|
+
}
|
|
184
|
+
return drawIcon(microphone);
|
|
185
|
+
}
|
|
143
186
|
renderVoice() {
|
|
144
187
|
if (!this.voice || !this.voiceReady) {
|
|
145
188
|
return nothing;
|
|
@@ -161,11 +204,23 @@ export const MixinVoice = (Base) => {
|
|
|
161
204
|
}
|
|
162
205
|
}}
|
|
163
206
|
>
|
|
164
|
-
${
|
|
207
|
+
${this.renderVoiceGlyph()}
|
|
165
208
|
</button
|
|
166
209
|
><span part="voice-status" data-spoken role="status">${this.said}</span>`;
|
|
167
210
|
}
|
|
168
211
|
}
|
|
212
|
+
__decorate([
|
|
213
|
+
words,
|
|
214
|
+
property({ attribute: 'dictation-denied-label' })
|
|
215
|
+
], Voice.prototype, "dictationDeniedLabel", void 0);
|
|
216
|
+
__decorate([
|
|
217
|
+
words,
|
|
218
|
+
property({ attribute: 'dictation-failed-label' })
|
|
219
|
+
], Voice.prototype, "dictationFailedLabel", void 0);
|
|
220
|
+
__decorate([
|
|
221
|
+
words,
|
|
222
|
+
property({ attribute: 'dictation-unreachable-label' })
|
|
223
|
+
], Voice.prototype, "dictationUnreachableLabel", void 0);
|
|
169
224
|
__decorate([
|
|
170
225
|
words,
|
|
171
226
|
property({ attribute: 'listening-label' })
|
|
@@ -32,13 +32,36 @@ export interface SpeechEngine {
|
|
|
32
32
|
listener: SpeechListener;
|
|
33
33
|
}): SpeechSession;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Listens with the browser's recognizer in this page, or in `scope`, a window
|
|
37
|
+
* of the same origin. A field in a sandboxed frame cannot: its origin is
|
|
38
|
+
* opaque, and the recognizer refuses it.
|
|
39
|
+
*
|
|
40
|
+
* **It recognizes on the device where the language is installed**, and
|
|
41
|
+
* through the browser's service where it is not. A press starts the install
|
|
42
|
+
* where the browser offers one. Chrome's service is Google's, and Chromium browsers without Google's
|
|
43
|
+
* keys, such as Brave, reach no service at all and end with `network`.
|
|
44
|
+
*
|
|
45
|
+
* `meter` reads loudness for the dictation button's level from a second
|
|
46
|
+
* capture of the microphone, opened on the recognizer's `audiostart`. Opened
|
|
47
|
+
* at the same time as the recognizer, that capture ended recognition after the
|
|
48
|
+
* permission prompt. Turn it off with `meter: false`.
|
|
49
|
+
*/
|
|
50
|
+
export declare const recognizeSpeech: (lang: string, listener: SpeechListener, options?: {
|
|
51
|
+
meter?: boolean;
|
|
52
|
+
scope?: Window;
|
|
53
|
+
}) => SpeechSession;
|
|
35
54
|
/**
|
|
36
55
|
* The browser's own recognizer, `SpeechRecognition`, or `webkitSpeechRecognition`
|
|
37
56
|
* in Safari. Not available in Firefox.
|
|
38
57
|
*
|
|
39
|
-
* **
|
|
40
|
-
*
|
|
41
|
-
*
|
|
58
|
+
* **It recognizes on the device where the browser can**, and Chrome downloads
|
|
59
|
+
* the language from the first press, which listens through the service. Where it cannot, Chrome sends the audio to
|
|
60
|
+
* Google's servers, and Safari to Apple's. A page that cannot send what is
|
|
61
|
+
* said registers an engine of its own.
|
|
62
|
+
*
|
|
63
|
+
* **A sandboxed frame has an opaque origin, and the recognizer refuses that.**
|
|
64
|
+
* The field asks the page that framed it to listen, and that page has an origin.
|
|
42
65
|
*/
|
|
43
66
|
export declare const browserSpeech: SpeechEngine;
|
|
44
67
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const recognizer = () => {
|
|
2
|
-
const
|
|
3
|
-
return
|
|
1
|
+
const recognizer = (scope = globalThis) => {
|
|
2
|
+
const here = scope;
|
|
3
|
+
return here.SpeechRecognition ?? here.webkitSpeechRecognition;
|
|
4
4
|
};
|
|
5
5
|
const meter = (report) => {
|
|
6
6
|
let stopped = false;
|
|
@@ -59,22 +59,75 @@ const meter = (report) => {
|
|
|
59
59
|
release();
|
|
60
60
|
};
|
|
61
61
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
const installing = new Set();
|
|
63
|
+
const listenLocally = async (Constructor, lang) => {
|
|
64
|
+
const local = Constructor;
|
|
65
|
+
if (!local.available || !local.install) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const options = { langs: [lang], processLocally: true };
|
|
69
|
+
try {
|
|
70
|
+
const state = await local.available(options);
|
|
71
|
+
if (state === 'available') {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (state === 'unavailable') {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (!installing.has(lang)) {
|
|
78
|
+
installing.add(lang);
|
|
79
|
+
void local
|
|
80
|
+
.install(options)
|
|
81
|
+
.catch(() => false)
|
|
82
|
+
.then(() => installing.delete(lang));
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
export const recognizeSpeech = (lang, listener, options) => {
|
|
91
|
+
const Constructor = recognizer(options?.scope ?? globalThis);
|
|
92
|
+
if (!Constructor) {
|
|
93
|
+
listener.end('error', 'This browser has no speech recognition.');
|
|
94
|
+
return { stop: () => { } };
|
|
95
|
+
}
|
|
96
|
+
let asked = false;
|
|
97
|
+
let ended = false;
|
|
98
|
+
let recognition;
|
|
99
|
+
let quiet = () => { };
|
|
100
|
+
let metering = false;
|
|
101
|
+
const startMeter = () => {
|
|
102
|
+
if (options?.meter === false || metering || ended) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
metering = true;
|
|
106
|
+
quiet = meter((level, bands) => listener.level?.(level, bands));
|
|
107
|
+
};
|
|
108
|
+
const end = (reason, message) => {
|
|
109
|
+
if (ended) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
ended = true;
|
|
113
|
+
quiet();
|
|
114
|
+
listener.end(reason, message);
|
|
115
|
+
};
|
|
116
|
+
const listen = (locally) => {
|
|
117
|
+
const current = new Constructor();
|
|
118
|
+
recognition = current;
|
|
119
|
+
current.continuous = true;
|
|
120
|
+
current.interimResults = true;
|
|
121
|
+
current.lang = lang;
|
|
122
|
+
if (locally) {
|
|
123
|
+
;
|
|
124
|
+
current.processLocally = true;
|
|
125
|
+
}
|
|
75
126
|
let failed;
|
|
76
|
-
|
|
77
|
-
|
|
127
|
+
let heard = false;
|
|
128
|
+
current.onaudiostart = startMeter;
|
|
129
|
+
current.onresult = (event) => {
|
|
130
|
+
heard = true;
|
|
78
131
|
let guess = '';
|
|
79
132
|
for (let at = event.resultIndex; at < event.results.length; at += 1) {
|
|
80
133
|
const result = event.results[at];
|
|
@@ -90,21 +143,94 @@ export const browserSpeech = {
|
|
|
90
143
|
listener.partial(guess.trim());
|
|
91
144
|
}
|
|
92
145
|
};
|
|
93
|
-
|
|
94
|
-
|
|
146
|
+
current.onerror = (event) => {
|
|
147
|
+
if (event.error !== 'no-speech' && event.error !== 'aborted') {
|
|
148
|
+
failed = event.error;
|
|
149
|
+
}
|
|
95
150
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
151
|
+
current.onend = () => {
|
|
152
|
+
if (locally && !asked && !heard && failed === 'language-not-supported') {
|
|
153
|
+
listen(false);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
end(failed ? 'error' : asked ? 'stopped' : 'ended', failed);
|
|
99
157
|
};
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
158
|
+
try {
|
|
159
|
+
current.start();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
end('error', error instanceof Error ? error.message : 'not-allowed');
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
void listenLocally(Constructor, lang).then((locally) => {
|
|
166
|
+
if (asked) {
|
|
167
|
+
end('stopped');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
listen(locally);
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
stop: () => {
|
|
174
|
+
asked = true;
|
|
175
|
+
if (recognition) {
|
|
104
176
|
recognition.stop();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
const opaqueFrame = () => {
|
|
182
|
+
const here = globalThis;
|
|
183
|
+
return here.origin === 'null' && here.parent !== undefined && here.parent !== here;
|
|
184
|
+
};
|
|
185
|
+
let relayId = 0;
|
|
186
|
+
const relaySpeech = (lang, listener) => {
|
|
187
|
+
relayId += 1;
|
|
188
|
+
const id = relayId;
|
|
189
|
+
const parent = globalThis.parent;
|
|
190
|
+
const channel = new MessageChannel();
|
|
191
|
+
const take = (data) => {
|
|
192
|
+
if (data?.type !== 'helix-speech-heard' || data.id !== id) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (data.kind === 'partial' && data.text) {
|
|
196
|
+
listener.partial(data.text);
|
|
197
|
+
}
|
|
198
|
+
else if (data.kind === 'level' && typeof data.level === 'number') {
|
|
199
|
+
listener.level?.(data.level);
|
|
200
|
+
}
|
|
201
|
+
else if (data.kind === 'final' && data.text) {
|
|
202
|
+
listener.final(data.text);
|
|
203
|
+
}
|
|
204
|
+
else if (data.kind === 'error' || data.kind === 'end' || data.kind === 'stopped') {
|
|
205
|
+
channel.port1.onmessage = null;
|
|
206
|
+
window.removeEventListener('message', onMessage);
|
|
207
|
+
listener.end(data.kind === 'error' ? 'error' : data.kind === 'stopped' ? 'stopped' : 'ended', data.error);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const onMessage = (event) => {
|
|
211
|
+
const data = event.data;
|
|
212
|
+
if (data?.type !== 'helix-speech-heard' || data.id !== id) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (event.source !== parent && event.origin === 'null') {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
take(data);
|
|
219
|
+
};
|
|
220
|
+
channel.port1.onmessage = (event) => {
|
|
221
|
+
take(event.data);
|
|
222
|
+
};
|
|
223
|
+
window.addEventListener('message', onMessage);
|
|
224
|
+
parent.postMessage({ id, lang, type: 'helix-speech-start' }, '*', [channel.port2]);
|
|
225
|
+
return {
|
|
226
|
+
stop: () => {
|
|
227
|
+
parent.postMessage({ id, type: 'helix-speech-stop' }, '*');
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
export const browserSpeech = {
|
|
232
|
+
available: () => recognizer() !== undefined,
|
|
233
|
+
listen: ({ lang, listener }) => opaqueFrame() ? relaySpeech(lang, listener) : recognizeSpeech(lang, listener),
|
|
108
234
|
};
|
|
109
235
|
const KEY = Symbol.for('fusion.helix.speech');
|
|
110
236
|
const globals = globalThis;
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.223.0';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-core",
|
|
3
3
|
"description": "Helix element primitives: base classes, mixins, decorators, utilities.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.223.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -81,12 +81,12 @@
|
|
|
81
81
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
82
82
|
"dependencies": {
|
|
83
83
|
"@floating-ui/dom": "^1.8.0",
|
|
84
|
-
"@fusion.dev/helix-icons": "0.
|
|
84
|
+
"@fusion.dev/helix-icons": "0.223.0",
|
|
85
85
|
"tslib": "^2.8.1"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
|
-
"@fusion.dev/helix-cem": "0.
|
|
89
|
-
"@fusion.dev/helix-testing": "0.
|
|
88
|
+
"@fusion.dev/helix-cem": "0.223.0",
|
|
89
|
+
"@fusion.dev/helix-testing": "0.223.0",
|
|
90
90
|
"@lit-labs/ssr": "^4.1.0",
|
|
91
91
|
"lit": "^3.2.1"
|
|
92
92
|
},
|