kempo-ui 0.4.3 → 0.4.5

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 (108) hide show
  1. package/.claude/skills/get-icon/SKILL.md +144 -0
  2. package/.claude/skills/highlight-code/SKILL.md +62 -0
  3. package/.claude/skills/new-component/SKILL.md +362 -0
  4. package/.github/skills/get-icon/SKILL.md +4 -4
  5. package/.github/skills/highlight-code/SKILL.md +51 -20
  6. package/.github/skills/new-component/SKILL.md +183 -152
  7. package/AGENTS.md +2 -2
  8. package/dist/components/SpeechToText.js +60 -0
  9. package/dist/components/TextToSpeech.js +56 -0
  10. package/dist/components/VoiceSelector.js +49 -0
  11. package/dist/utils/voice.js +1 -0
  12. package/docs/components/accordion.html +15 -0
  13. package/docs/components/aside.html +15 -0
  14. package/docs/components/calendar.html +15 -0
  15. package/docs/components/card.html +15 -0
  16. package/docs/components/code-editor.html +15 -0
  17. package/docs/components/color-picker.html +15 -0
  18. package/docs/components/combobox.html +15 -0
  19. package/docs/components/content-slider.html +15 -0
  20. package/docs/components/context.html +15 -0
  21. package/docs/components/dialog.html +15 -0
  22. package/docs/components/dropdown.html +15 -0
  23. package/docs/components/filter-list.html +15 -0
  24. package/docs/components/focus-capture.html +15 -0
  25. package/docs/components/html-editor.html +15 -0
  26. package/docs/components/hybrid-component.html +15 -0
  27. package/docs/components/icon.html +15 -0
  28. package/docs/components/import.html +15 -0
  29. package/docs/components/light-component.html +15 -0
  30. package/docs/components/nav-spacer.html +15 -0
  31. package/docs/components/nav.html +15 -0
  32. package/docs/components/photo-viewer.html +15 -0
  33. package/docs/components/progress.html +15 -0
  34. package/docs/components/resize.html +15 -0
  35. package/docs/components/shadow-component.html +15 -0
  36. package/docs/components/show-more.html +15 -0
  37. package/docs/components/slider.html +15 -0
  38. package/docs/components/sortable.html +15 -0
  39. package/docs/components/speech-to-text.html +724 -0
  40. package/docs/components/spinner.html +15 -0
  41. package/docs/components/split.html +15 -0
  42. package/docs/components/table.html +15 -0
  43. package/docs/components/tableControls.html +15 -0
  44. package/docs/components/tableCustomFields.html +15 -0
  45. package/docs/components/tableFetchRecords.html +15 -0
  46. package/docs/components/tableFieldSortHide.html +15 -0
  47. package/docs/components/tablePagination.html +15 -0
  48. package/docs/components/tablePlaceholder.html +15 -0
  49. package/docs/components/tableRecordEditing.html +15 -0
  50. package/docs/components/tableRecordFiltering.html +15 -0
  51. package/docs/components/tableRecordHiding.html +15 -0
  52. package/docs/components/tableRecordSearching.html +15 -0
  53. package/docs/components/tableRecordSelection.html +15 -0
  54. package/docs/components/tableRowControls.html +15 -0
  55. package/docs/components/tableServerSync.html +15 -0
  56. package/docs/components/tableSorting.html +15 -0
  57. package/docs/components/tabs.html +15 -0
  58. package/docs/components/tags.html +15 -0
  59. package/docs/components/text-to-speech.html +718 -0
  60. package/docs/components/theme-select.html +15 -0
  61. package/docs/components/theme-switcher.html +15 -0
  62. package/docs/components/time.html +15 -0
  63. package/docs/components/timestamp.html +15 -0
  64. package/docs/components/toast.html +15 -0
  65. package/docs/components/toggle.html +15 -0
  66. package/docs/components/tree.html +15 -0
  67. package/docs/components/voice-selector.html +598 -0
  68. package/docs/icons/mic.svg +1 -0
  69. package/docs/icons/record_voice_over.svg +1 -0
  70. package/docs/icons/stop.svg +1 -0
  71. package/docs/index.html +39 -0
  72. package/docs/src/components/SpeechToText.js +60 -0
  73. package/docs/src/components/TextToSpeech.js +56 -0
  74. package/docs/src/components/VoiceSelector.js +49 -0
  75. package/docs/src/utils/voice.js +1 -0
  76. package/docs/utils/context.html +15 -0
  77. package/docs/utils/cookie.html +15 -0
  78. package/docs/utils/debounce.html +15 -0
  79. package/docs/utils/drag.html +15 -0
  80. package/docs/utils/elevation.html +15 -0
  81. package/docs/utils/formatTimestamp.html +15 -0
  82. package/docs/utils/object.html +15 -0
  83. package/docs/utils/propConverters.html +15 -0
  84. package/docs/utils/string.html +15 -0
  85. package/docs/utils/theme.html +15 -0
  86. package/docs/utils/toTitleCase.html +15 -0
  87. package/docs/utils/type.html +15 -0
  88. package/docs/utils/voice.html +536 -0
  89. package/docs/utils/wait.html +15 -0
  90. package/docs-src/components/speech-to-text.page.html +251 -0
  91. package/docs-src/components/text-to-speech.page.html +245 -0
  92. package/docs-src/components/voice-selector.page.html +125 -0
  93. package/docs-src/index.page.html +24 -0
  94. package/docs-src/nav.fragment.html +15 -0
  95. package/docs-src/utils/voice.page.html +66 -0
  96. package/icons/mic.svg +1 -0
  97. package/icons/record_voice_over.svg +1 -0
  98. package/icons/stop.svg +1 -0
  99. package/llms.txt +4 -0
  100. package/package.json +1 -1
  101. package/src/components/SpeechToText.js +252 -0
  102. package/src/components/TextToSpeech.js +205 -0
  103. package/src/components/VoiceSelector.js +271 -0
  104. package/src/utils/voice.js +103 -0
  105. package/tests/components/SpeechToText.browser-test.js +530 -0
  106. package/tests/components/TextToSpeech.browser-test.js +581 -0
  107. package/tests/components/VoiceSelector.browser-test.js +374 -0
  108. package/tests/utils/voice.browser-test.js +128 -0
@@ -0,0 +1,125 @@
1
+ <page pageName="Voice Selector" title="Voice Selector - Components - Kempo Docs - A Web Components Solution">
2
+ <content>
3
+ <k-accordion persistent-id="toc" class="b r mb">
4
+ <k-accordion-header for-panel="toc-panel">Table of Contents</k-accordion-header>
5
+ <k-accordion-panel name="toc-panel">
6
+ <div class="m pl">
7
+ <h6>Examples</h6>
8
+ <a href="#basicUsage">Basic Usage</a><br />
9
+ <a href="#languageFilter">Language Filter</a><br />
10
+ <a href="#withTextToSpeech">With TextToSpeech</a><br />
11
+ <a href="#disabled">Disabled</a><br />
12
+
13
+ <h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
14
+ <a href="#constructor">Constructor</a><br />
15
+ <a href="#requirements">Requirements</a><br />
16
+ <a href="#properties">Properties</a><br />
17
+ <a href="#methods">Methods</a><br />
18
+ <a href="#events">Events</a><br />
19
+ </div>
20
+ </k-accordion-panel>
21
+ </k-accordion>
22
+
23
+ <p>Two cascading <code>&lt;select&gt;</code>s &mdash; the first picks a language, the second lists voices for that language. The chosen voice is persisted via the <a href="../utils/voice.html">voice utility</a>, so every <a href="./text-to-speech.html"><code>&lt;k-text-to-speech&gt;</code></a> on the page (and across reloads) uses it as a default when no <code>voice</code> attribute is set.</p>
24
+ <p>The language dropdown defaults to the browser's primary language (<code>navigator.language</code>) so a user almost always sees a relevant voice list immediately.</p>
25
+
26
+ <h3 id="basicUsage"><a href="#basicUsage" class="no-link">Basic Usage</a></h3>
27
+ <p>Just drop the element into a settings page or somewhere visible. Voices load asynchronously, so the dropdowns may briefly be empty before the rest populate.</p>
28
+ <div class="row -mx">
29
+ <div class="col m-span-12 px">
30
+ <k-card label="HTML">
31
+ <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">k-voice-selector</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-voice-selector</span>&gt;</span></code></pre>
32
+ </k-card>
33
+ </div>
34
+ <div class="col m-span-12 px">
35
+ <k-card label="Output">
36
+ <k-voice-selector></k-voice-selector>
37
+ </k-card>
38
+ </div>
39
+ </div>
40
+
41
+ <h3 id="languageFilter"><a href="#languageFilter" class="no-link">Initial Language</a></h3>
42
+ <p>Set <code>language</code> to override the default browser language. Accepts a primary language code (e.g. <code>"fr"</code>, <code>"es"</code>) or a full BCP&#x2011;47 tag (the primary part is used). The user can still switch via the dropdown afterward.</p>
43
+ <div class="row -mx">
44
+ <div class="col m-span-12 px">
45
+ <k-card label="HTML">
46
+ <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">k-voice-selector</span> <span class="hljs-attr">language</span>=<span class="hljs-string">&quot;fr&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-voice-selector</span>&gt;</span></code></pre>
47
+ </k-card>
48
+ </div>
49
+ <div class="col m-span-12 px">
50
+ <k-card label="Output">
51
+ <k-voice-selector language="fr"></k-voice-selector>
52
+ </k-card>
53
+ </div>
54
+ </div>
55
+
56
+ <h3 id="withTextToSpeech"><a href="#withTextToSpeech" class="no-link">With TextToSpeech</a></h3>
57
+ <p>Pick a voice in the dropdown, then click any <code>&lt;k-text-to-speech&gt;</code> on the page that doesn't have its own <code>voice</code> attribute &mdash; it'll use your saved preference. The choice persists across page reloads.</p>
58
+ <div class="row -mx">
59
+ <div class="col m-span-12 px">
60
+ <k-card label="HTML">
61
+ <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">k-voice-selector</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-voice-selector</span>&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">k-text-to-speech</span> <span class="hljs-attr">text</span>=<span class="hljs-string">&quot;This will use the voice you picked above&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-text-to-speech</span>&gt;</span></code></pre>
62
+ </k-card>
63
+ </div>
64
+ <div class="col m-span-12 px">
65
+ <k-card label="Output">
66
+ <k-voice-selector class="mr"></k-voice-selector>
67
+ <k-text-to-speech text="This will use the voice you picked above"></k-text-to-speech>
68
+ </k-card>
69
+ </div>
70
+ </div>
71
+
72
+ <h3 id="disabled"><a href="#disabled" class="no-link">Disabled</a></h3>
73
+ <p>The <code>disabled</code> attribute prevents user interaction. The dropdown also auto-disables when the browser doesn't support the SpeechSynthesis API.</p>
74
+ <div class="row -mx">
75
+ <div class="col m-span-12 px">
76
+ <k-card label="HTML">
77
+ <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">k-voice-selector</span> <span class="hljs-attr">disabled</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-voice-selector</span>&gt;</span></code></pre>
78
+ </k-card>
79
+ </div>
80
+ <div class="col m-span-12 px">
81
+ <k-card label="Output">
82
+ <k-voice-selector disabled></k-voice-selector>
83
+ </k-card>
84
+ </div>
85
+ </div>
86
+
87
+ <h2 id="jsRef"><a href="#jsRef" class="no-link">JavaScript Reference</a></h2>
88
+
89
+ <h3 id="constructor"><a href="#constructor" class="no-link">Constructor</a></h3>
90
+ <h6>Extends <a href="./shadow-component.html">ShadowComponent</a></h6>
91
+ <h5><code>new VoiceSelector()</code></h5>
92
+
93
+ <h3 id="requirements"><a href="#requirements" class="no-link">Requirements</a></h3>
94
+ <ul>
95
+ <li><a href="./shadow-component.html">ShadowComponent</a></li>
96
+ <li>The <a href="../utils/voice.html">voice utility</a></li>
97
+ <li>A browser with <code>window.speechSynthesis</code></li>
98
+ </ul>
99
+
100
+ <h3 id="properties"><a href="#properties" class="no-link">Properties</a></h3>
101
+ <h5><code>language<i>: String</i></code></h5>
102
+ <p>Optional initial language. If set, overrides the default <code>navigator.language</code> on first render. Accepts a primary language code (<code>"en"</code>, <code>"es"</code>) or a full BCP&#x2011;47 tag (the primary part is used). The user can change the language via the first dropdown afterward. Syncs to <code>language</code> attribute.</p>
103
+
104
+ <h5><code>placeholder<i>: String</i></code></h5>
105
+ <p>Label for the empty-selection option. Default is <code>"Browser default"</code>. Syncs to <code>placeholder</code> attribute.</p>
106
+
107
+ <h5><code>disabled<i>: Boolean</i></code></h5>
108
+ <p>When <code>true</code>, the dropdown cannot be changed. Syncs to <code>disabled</code> attribute.</p>
109
+
110
+ <h3 id="methods"><a href="#methods" class="no-link">Methods</a></h3>
111
+ <h5><code>refresh()<i>: void</i></code></h5>
112
+ <p>Force a re-read of <code>speechSynthesis.getVoices()</code>. Rarely needed since the component subscribes to the <code>voiceschanged</code> event, but useful if you populate voices dynamically.</p>
113
+
114
+ <h3 id="events"><a href="#events" class="no-link">Events</a></h3>
115
+ <h5><code>change</code></h5>
116
+ <p>Fires when the user picks a new voice. <code>detail</code> contains <code>{ voice: String }</code> &mdash; the voice name, or empty string for "browser default". The voice utility is updated automatically; this event is for callers that want to react in addition.</p>
117
+ </content>
118
+ <content location="scripts">
119
+ <script type="module" src="{{pathToRoot}}src/components/VoiceSelector.js"></script>
120
+ <script type="module" src="{{pathToRoot}}src/components/TextToSpeech.js"></script>
121
+ <script type="module" src="{{pathToRoot}}src/components/Icon.js"></script>
122
+ <script type="module" src="{{pathToRoot}}src/components/Accordion.js"></script>
123
+ <script type="module" src="{{pathToRoot}}src/components/Card.js"></script>
124
+ </content>
125
+ </page>
@@ -179,6 +179,12 @@
179
179
  <p class="tc-muted">Drag-and-drop lists with reordering and sort events.</p>
180
180
  </a>
181
181
  </div>
182
+ <div class="span-12 t-span-6 d-span-4 px">
183
+ <a href="{{pathToRoot}}components/speech-to-text.html" class="card mb no-link d-b">
184
+ <h3 class="tc-primary">Speech To Text</h3>
185
+ <p class="tc-muted">Microphone button that uses the Web Speech API to transcribe spoken input.</p>
186
+ </a>
187
+ </div>
182
188
  <div class="span-12 t-span-6 d-span-4 px">
183
189
  <a href="{{pathToRoot}}components/spinner.html" class="card mb no-link d-b">
184
190
  <h3 class="tc-primary">Spinner</h3>
@@ -209,6 +215,12 @@
209
215
  <p class="tc-muted">Input field for adding and removing tags with keyboard support.</p>
210
216
  </a>
211
217
  </div>
218
+ <div class="span-12 t-span-6 d-span-4 px">
219
+ <a href="{{pathToRoot}}components/text-to-speech.html" class="card mb no-link d-b">
220
+ <h3 class="tc-primary">Text To Speech</h3>
221
+ <p class="tc-muted">Button that speaks the given text aloud using the browser's SpeechSynthesis API.</p>
222
+ </a>
223
+ </div>
212
224
  <div class="span-12 t-span-6 d-span-4 px">
213
225
  <a href="{{pathToRoot}}components/theme-select.html" class="card mb no-link d-b">
214
226
  <h3 class="tc-primary">Theme Select</h3>
@@ -251,6 +263,12 @@
251
263
  <p class="tc-muted">Hierarchical data visualization with collapsible branches and custom leaf types.</p>
252
264
  </a>
253
265
  </div>
266
+ <div class="span-12 t-span-6 d-span-4 px">
267
+ <a href="{{pathToRoot}}components/voice-selector.html" class="card mb no-link d-b">
268
+ <h3 class="tc-primary">Voice Selector</h3>
269
+ <p class="tc-muted">Dropdown of available speech-synthesis voices, persisted via the voice utility for site-wide use.</p>
270
+ </a>
271
+ </div>
254
272
  </div>
255
273
 
256
274
  <h2>Base Components</h2>
@@ -337,6 +355,12 @@
337
355
  <p class="tc-muted">Functions for manipulating and analyzing JavaScript objects.</p>
338
356
  </a>
339
357
  </div>
358
+ <div class="span-12 t-span-6 d-span-4 px">
359
+ <a href="{{pathToRoot}}utils/voice.html" class="card mb no-link d-b">
360
+ <h3 class="tc-primary">voice</h3>
361
+ <p class="tc-muted">Persisted speech-synthesis voice preference with pub/sub, mirroring the theme utility.</p>
362
+ </a>
363
+ </div>
340
364
  <div class="span-12 t-span-6 d-span-4 px">
341
365
  <a href="{{pathToRoot}}utils/wait.html" class="card mb no-link d-b">
342
366
  <h3 class="tc-primary">wait</h3>
@@ -104,6 +104,9 @@
104
104
  <k-filter-item filter-keywords="sortable drag drop sort components"><a
105
105
  href="{{pathToRoot}}components/sortable.html"
106
106
  >Sortable<br><small>Component</small></a></k-filter-item>
107
+ <k-filter-item filter-keywords="speech to text speechtotext microphone voice recognition transcription components"><a
108
+ href="{{pathToRoot}}components/speech-to-text.html"
109
+ >Speech To Text<br><small>Component</small></a></k-filter-item>
107
110
  <k-filter-item filter-keywords="spinner loading components"><a
108
111
  href="{{pathToRoot}}components/spinner.html">Spinner<br><small>Component</small></a></k-filter-item>
109
112
  <k-filter-item filter-keywords="split pane components"><a
@@ -156,6 +159,9 @@
156
159
  href="{{pathToRoot}}components/tabs.html">Tabs<br><small>Component</small></a></k-filter-item>
157
160
  <k-filter-item filter-keywords="tags tag input components"><a
158
161
  href="{{pathToRoot}}components/tags.html">Tags<br><small>Component</small></a></k-filter-item>
162
+ <k-filter-item filter-keywords="text to speech texttospeech tts speak voice synthesis components"><a
163
+ href="{{pathToRoot}}components/text-to-speech.html"
164
+ >Text To Speech<br><small>Component</small></a></k-filter-item>
159
165
  <k-filter-item filter-keywords="theme select dropdown dark light components"><a
160
166
  href="{{pathToRoot}}components/theme-select.html"
161
167
  >Theme Select<br><small>Component</small></a></k-filter-item>
@@ -176,6 +182,9 @@
176
182
  >Toggle<br><small>Component</small></a></k-filter-item>
177
183
  <k-filter-item filter-keywords="tree treeview components"><a
178
184
  href="{{pathToRoot}}components/tree.html">Tree<br><small>Component</small></a></k-filter-item>
185
+ <k-filter-item filter-keywords="voice selector dropdown speech synthesis preference components"><a
186
+ href="{{pathToRoot}}components/voice-selector.html"
187
+ >Voice Selector<br><small>Component</small></a></k-filter-item>
179
188
  <k-filter-item filter-keywords="shadow component base"><a
180
189
  href="{{pathToRoot}}components/shadow-component.html"
181
190
  >Shadow Component<br><small>Base Component</small></a></k-filter-item>
@@ -209,6 +218,8 @@
209
218
  href="{{pathToRoot}}utils/theme.html">theme<br><small>Utility</small></a></k-filter-item>
210
219
  <k-filter-item filter-keywords="type utils utility"><a
211
220
  href="{{pathToRoot}}utils/type.html">type<br><small>Utility</small></a></k-filter-item>
221
+ <k-filter-item filter-keywords="voice speech synthesis preference utils utility"><a
222
+ href="{{pathToRoot}}utils/voice.html">voice<br><small>Utility</small></a></k-filter-item>
212
223
  <k-filter-item filter-keywords="wait async utils utility"><a
213
224
  href="{{pathToRoot}}utils/wait.html">wait<br><small>Utility</small></a></k-filter-item>
214
225
  </k-filter-list>
@@ -281,11 +292,13 @@
281
292
  <a href="{{pathToRoot}}components/show-more.html">Show More</a>
282
293
  <a href="{{pathToRoot}}components/slider.html">Slider</a>
283
294
  <a href="{{pathToRoot}}components/sortable.html">Sortable</a>
295
+ <a href="{{pathToRoot}}components/speech-to-text.html">Speech To Text</a>
284
296
  <a href="{{pathToRoot}}components/spinner.html">Spinner</a>
285
297
  <a href="{{pathToRoot}}components/split.html">Split</a>
286
298
  <a href="{{pathToRoot}}components/table.html">Table</a>
287
299
  <a href="{{pathToRoot}}components/tabs.html">Tabs</a>
288
300
  <a href="{{pathToRoot}}components/tags.html">Tags</a>
301
+ <a href="{{pathToRoot}}components/text-to-speech.html">Text To Speech</a>
289
302
  <a href="{{pathToRoot}}components/theme-select.html">Theme Select</a>
290
303
  <a href="{{pathToRoot}}components/theme-switcher.html">Theme Switcher</a>
291
304
  <a href="{{pathToRoot}}components/time.html">Time</a>
@@ -293,6 +306,7 @@
293
306
  <a href="{{pathToRoot}}components/toast.html">Toast</a>
294
307
  <a href="{{pathToRoot}}components/toggle.html">Toggle</a>
295
308
  <a href="{{pathToRoot}}components/tree.html">Tree</a>
309
+ <a href="{{pathToRoot}}components/voice-selector.html">Voice Selector</a>
296
310
  </div>
297
311
 
298
312
  <h5 class="mb0">Base Components</h5>
@@ -315,6 +329,7 @@
315
329
  <a href="{{pathToRoot}}utils/string.html">string</a>
316
330
  <a href="{{pathToRoot}}utils/theme.html">theme</a>
317
331
  <a href="{{pathToRoot}}utils/type.html">type</a>
332
+ <a href="{{pathToRoot}}utils/voice.html">voice</a>
318
333
  <a href="{{pathToRoot}}utils/wait.html">wait</a>
319
334
  </div>
320
335
  </menu>
@@ -0,0 +1,66 @@
1
+ <page pageName="Voice Utility" title="Voice Utility - Kempo UI">
2
+ <content location="header"></content>
3
+ <content>
4
+ <h1>Voice Utility</h1>
5
+
6
+ <k-accordion persistent-id="toc" class="b r mb">
7
+ <k-accordion-header for-panel="toc-panel">Table of Contents</k-accordion-header>
8
+ <k-accordion-panel name="toc-panel">
9
+ <div class="m">
10
+ <h6>Usage</h6>
11
+ <a href="#basicUsage">Basic Usage</a><br />
12
+ <a href="#api">API Methods</a><br />
13
+ <a href="#examples">Examples</a><br />
14
+ </div>
15
+ </k-accordion-panel>
16
+ </k-accordion>
17
+
18
+ <h3>Description</h3>
19
+ <p>The <code>voice</code> utility manages the user's preferred speech-synthesis voice across the site. It mirrors the <a href="./theme.html">theme</a> utility &mdash; persisting to <code>localStorage</code>, syncing across tabs via the <code>storage</code> event, and exposing a pub/sub API. Pair it with <a href="../components/voice-selector.html"><code>&lt;k-voice-selector&gt;</code></a> for a settings UI; <a href="../components/text-to-speech.html"><code>&lt;k-text-to-speech&gt;</code></a> automatically uses the saved preference when no <code>voice</code> attribute is set.</p>
20
+
21
+ <h3 id="basicUsage"><a href="#basicUsage" class="no-link">Basic Usage</a></h3>
22
+ <pre><code class="hljs javascript"><span class="hljs-keyword">import</span> voice <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./utils/voice.js&#x27;</span>;<br><span class="hljs-comment">// Get current preferred voice (empty string = use browser default)</span><br><span class="hljs-keyword">const</span> current = voice.<span class="hljs-title function_">get</span>();<br><span class="hljs-comment">// Set a preferred voice</span><br>voice.<span class="hljs-title function_">set</span>(<span class="hljs-string">&#x27;Google US English&#x27;</span>);<br><span class="hljs-comment">// Subscribe to changes</span><br><span class="hljs-keyword">const</span> unsubscribe = voice.<span class="hljs-title function_">subscribe</span>(<span class="hljs-function">(<span class="hljs-params">name</span>) =&gt;</span> {<br> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">&#x27;Voice preference changed to:&#x27;</span>, name);<br>});<br><span class="hljs-comment">// Enumerate available voices (may need to wait — the list loads async)</span><br><span class="hljs-keyword">const</span> voices = <span class="hljs-keyword">await</span> voice.<span class="hljs-title function_">waitForVoices</span>();</code></pre>
23
+
24
+ <h3 id="api"><a href="#api" class="no-link">API Methods</a></h3>
25
+
26
+ <h4><code>voice.get()</code></h4>
27
+ <p>Returns the user's saved voice name. An empty string means "no preference &mdash; use the browser default for the language".</p>
28
+ <h5>Returns: <code>string</code></h5>
29
+
30
+ <h4><code>voice.set(name)</code></h4>
31
+ <p>Sets the saved voice and persists it to <code>localStorage</code>. Pass an empty string (or a falsy value) to clear the preference.</p>
32
+ <h5><code>name<i>: string</i></code></h5>
33
+ <p>The voice <code>name</code> as reported by <code>speechSynthesis.getVoices()</code>, or a comma-separated fallback chain (<code>"Samantha, Google US English"</code>).</p>
34
+
35
+ <h4><code>voice.subscribe(callback)</code></h4>
36
+ <p>Subscribes to preference changes. Fires once immediately with the current value, then on every change (including changes from other tabs).</p>
37
+ <h5>Returns: <code>Function</code></h5>
38
+ <p>An unsubscribe function.</p>
39
+
40
+ <h4><code>voice.getAvailable()</code></h4>
41
+ <p>Synchronous shortcut for <code>window.speechSynthesis.getVoices()</code>. May return <code>[]</code> if the browser hasn't populated the list yet.</p>
42
+ <h5>Returns: <code>SpeechSynthesisVoice[]</code></h5>
43
+
44
+ <h4><code>voice.waitForVoices(timeoutMs?)</code></h4>
45
+ <p>Returns a promise that resolves with the list of available voices, waiting for the browser's <code>voiceschanged</code> event if the list is initially empty.</p>
46
+ <h5><code>timeoutMs<i>: number</i></code> &mdash; default <code>2000</code></h5>
47
+ <p>How long to wait before giving up and resolving with whatever the browser has.</p>
48
+ <h5>Returns: <code>Promise&lt;SpeechSynthesisVoice[]&gt;</code></h5>
49
+
50
+ <h4><code>voice.subscribeAvailable(callback)</code></h4>
51
+ <p>Subscribes to changes in the available voice list (fires on the <code>voiceschanged</code> event). Useful when populating a settings UI.</p>
52
+ <h5>Returns: <code>Function</code></h5>
53
+ <p>An unsubscribe function.</p>
54
+
55
+ <h3 id="examples"><a href="#examples" class="no-link">Examples</a></h3>
56
+
57
+ <h4>Building a Voice Picker by Hand</h4>
58
+ <pre><code class="hljs javascript"><span class="hljs-keyword">import</span> voice <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./utils/voice.js&#x27;</span>;<br><br><span class="hljs-keyword">const</span> $select = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">&#x27;voice-picker&#x27;</span>);<br><br>voice.<span class="hljs-title function_">subscribeAvailable</span>(<span class="hljs-function">(<span class="hljs-params">voices</span>) =&gt;</span> {<br> $select.<span class="hljs-property">innerHTML</span> = <span class="hljs-string">&#x27;&lt;option value=&quot;&quot;&gt;Browser default&lt;/option&gt;&#x27;</span>;<br> <span class="hljs-keyword">for</span>(<span class="hljs-keyword">const</span> v <span class="hljs-keyword">of</span> voices){<br> <span class="hljs-keyword">const</span> opt = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">&#x27;option&#x27;</span>);<br> opt.<span class="hljs-property">value</span> = v.<span class="hljs-property">name</span>;<br> opt.<span class="hljs-property">textContent</span> = <span class="hljs-string">`<span class="hljs-subst">${v.name}</span> (<span class="hljs-subst">${v.lang}</span>)`</span>;<br> $select.<span class="hljs-title function_">appendChild</span>(opt);<br> }<br> $select.<span class="hljs-property">value</span> = voice.<span class="hljs-title function_">get</span>();<br>});<br><br>$select.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;change&#x27;</span>, <span class="hljs-function">() =&gt;</span> voice.<span class="hljs-title function_">set</span>($select.<span class="hljs-property">value</span>));</code></pre>
59
+
60
+ <h4>Reading the Preference Without a Component</h4>
61
+ <pre><code class="hljs javascript"><span class="hljs-keyword">import</span> voice <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./utils/voice.js&#x27;</span>;<br><br><span class="hljs-keyword">const</span> preferred = voice.<span class="hljs-title function_">get</span>();<br><span class="hljs-keyword">if</span>(preferred){<br> <span class="hljs-built_in">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`User picked: <span class="hljs-subst">${preferred}</span>`</span>);<br>} <span class="hljs-keyword">else</span> {<br> <span class="hljs-built_in">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">&#x27;No preference set — using browser default&#x27;</span>);<br>}</code></pre>
62
+ </content>
63
+ <content location="scripts">
64
+ <script type="module" src="{{pathToRoot}}src/components/Accordion.js"></script>
65
+ </content>
66
+ </page>
package/icons/mic.svg ADDED
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="currentColor" d="M395-435q-35-35-35-85v-240q0-50 35-85t85-35q50 0 85 35t35 85v240q0 50-35 85t-85 35q-50 0-85-35Zm85-205Zm-40 520v-123q-104-14-172-93t-68-184h80q0 83 58.5 141.5T480-320q83 0 141.5-58.5T680-520h80q0 105-68 184t-172 93v123h-80Zm68.5-371.5Q520-503 520-520v-240q0-17-11.5-28.5T480-800q-17 0-28.5 11.5T440-760v240q0 17 11.5 28.5T480-480q17 0 28.5-11.5Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="currentColor" d="m798-322-62-62q44-41 69-97t25-119q0-63-25-118t-69-96l62-64q56 53 89 125t33 153q0 81-33 153t-89 125ZM670-450l-64-64q18-17 29-38.5t11-47.5q0-26-11-47.5T606-686l64-64q32 29 50 67.5t18 82.5q0 44-18 82.5T670-450Zm-310 10q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM40-120v-112q0-33 17-62t47-44q51-26 115-44t141-18q77 0 141 18t115 44q30 15 47 44t17 62v112H40Zm80-80h480v-32q0-11-5.5-20T580-266q-36-18-92.5-36T360-320q-71 0-127.5 18T140-266q-9 5-14.5 14t-5.5 20v32Zm296.5-343.5Q440-567 440-600t-23.5-56.5Q393-680 360-680t-56.5 23.5Q280-633 280-600t23.5 56.5Q327-520 360-520t56.5-23.5ZM360-600Zm0 400Z"/></svg>
package/icons/stop.svg ADDED
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="currentColor" d="M240-240v-480h480v480H240Z"/></svg>
package/llms.txt CHANGED
@@ -76,11 +76,13 @@ import { html, css, LitElement } from 'kempo-ui/src/lit-all.min.js';
76
76
  | `<k-show-more>` | `ShowMore.js` | Truncated content with expand toggle | [show-more.html](https://dustinpoissant.github.io/kempo-ui/components/show-more.html) |
77
77
  | `<k-slider>` | `Slider.js` | Slider with min, max, discrete step snap points, vertical orientation, and dual-handle range mode via comma-separated `value` (e.g. `"20,80"`); `format` attribute for value formatting (e.g. `"$0.00"`) applied to change events, form submission, and optional drag `tooltip`; form-associated; fires `change` | [slider.html](https://dustinpoissant.github.io/kempo-ui/components/slider.html) |
78
78
  | `<k-sortable>` `<k-sortable-item>` | `Sortable.js` | Drag-to-reorder list | [sortable.html](https://dustinpoissant.github.io/kempo-ui/components/sortable.html) |
79
+ | `<k-speech-to-text>` | `SpeechToText.js` | Microphone button wrapping the Web Speech API; click to start, click again or pause to stop; `language`, `continuous`, `interim`, `min-confidence` (0-1 noise filter), `timeout` (max recording seconds) attrs; fires `start`, `result` (with `{text, isFinal}`), `end` (with `{text}`), `error` | [speech-to-text.html](https://dustinpoissant.github.io/kempo-ui/components/speech-to-text.html) |
79
80
  | `<k-spinner>` | `Spinner.js` | Loading indicator; `size` (xs/sm/md/lg/xl), `variant` (spinner/dots/bar) | [spinner.html](https://dustinpoissant.github.io/kempo-ui/components/spinner.html) |
80
81
  | `<k-split>` | `Split.js` | Resizable split panes | [split.html](https://dustinpoissant.github.io/kempo-ui/components/split.html) |
81
82
  | `<k-table>` | `Table.js` | Data table with sorting, filtering, pagination, row selection, server sync; `placeholder` (default "No Records") shown when empty; `filtered-placeholder` shown when all records are filtered/hidden | [table.html](https://dustinpoissant.github.io/kempo-ui/components/table.html) |
82
83
  | `<k-tabs>` `<k-tab>` `<k-tab-content>` `<k-tab-spacer>` | `Tabs.js` | Tabbed interface; `<k-tab for-content="id">` links to `<k-tab-content name="id">` | [tabs.html](https://dustinpoissant.github.io/kempo-ui/components/tabs.html) |
83
84
  | `<k-tags>` `<k-tag>` | `Tags.js` | Tag input; add/remove tags; fires `change` | [tags.html](https://dustinpoissant.github.io/kempo-ui/components/tags.html) |
85
+ | `<k-text-to-speech>` | `TextToSpeech.js` | Button that wraps `window.speechSynthesis`; speaks `text` attr or slotted content; `voice`, `language`, `rate`, `pitch`, `volume` attrs; methods `speak(text?)` / `stop()` / `toggle()`; fires `start`, `end`, `error` | [text-to-speech.html](https://dustinpoissant.github.io/kempo-ui/components/text-to-speech.html) |
84
86
  | `<k-theme-select>` | `ThemeSelect.js` | Dropdown to pick a theme | [theme-select.html](https://dustinpoissant.github.io/kempo-ui/components/theme-select.html) |
85
87
  | `<k-theme-switcher>` | `ThemeSwitcher.js` | Theme switcher with segmented and toggle display modes | [theme-switcher.html](https://dustinpoissant.github.io/kempo-ui/components/theme-switcher.html) |
86
88
  | `<k-time>` | `Time.js` | Wraps native `<input type="time">` with segmented hour/minute/AM-PM editing; `value` is 24-hour `HH:MM`; `increment` attr sets minute step (arrow-key granularity); `military` attr forces 24-hour display via `lang="en-GB"`; form-associated; fires `change` | [time.html](https://dustinpoissant.github.io/kempo-ui/components/time.html) |
@@ -88,6 +90,7 @@ import { html, css, LitElement } from 'kempo-ui/src/lit-all.min.js';
88
90
  | `<k-toast>` `<k-toast-container>` | `Toast.js` | Toast notifications; use `<k-toast-container>` once in page, call `.addToast(options)` | [toast.html](https://dustinpoissant.github.io/kempo-ui/components/toast.html) |
89
91
  | `<k-toggle>` | `Toggle.js` | On/off toggle; `value` (Boolean, reflects); methods: `on()`, `off()`, `toggle()`; fires `change`, `on`, `off` | [toggle.html](https://dustinpoissant.github.io/kempo-ui/components/toggle.html) |
90
92
  | `<k-tree>` `<k-tree-node>` | `Tree.js` | Expandable tree; nodes have `label`, `icon`, `href`, `expanded` | [tree.html](https://dustinpoissant.github.io/kempo-ui/components/tree.html) |
93
+ | `<k-voice-selector>` | `VoiceSelector.js` | `<select>` populated with `speechSynthesis.getVoices()`; persists choice via the `voice` utility so any `<k-text-to-speech>` without an explicit `voice` attr uses the saved preference; `language` (BCP-47 prefix) attr filters the list; fires `change` | [voice-selector.html](https://dustinpoissant.github.io/kempo-ui/components/voice-selector.html) |
91
94
 
92
95
  ---
93
96
 
@@ -243,6 +246,7 @@ Located in `src/utils/`. Import individually.
243
246
  | `debounce.js` | `default` | `debounce(fn, ms)` — delay fn until calls stop | [debounce.html](https://dustinpoissant.github.io/kempo-ui/utils/debounce.html) |
244
247
  | `wait.js` | `wait`, `waitFrames` | `wait(ms)` → Promise; `waitFrames(n)` → Promise after n animation frames | [wait.html](https://dustinpoissant.github.io/kempo-ui/utils/wait.html) |
245
248
  | `theme.js` | `setTheme`, `getTheme`, `subscribeToTheme`, `initTheme`, `getCalculatedTheme` | Manage light/dark/auto theme; persists to localStorage | [theme.html](https://dustinpoissant.github.io/kempo-ui/utils/theme.html) |
249
+ | `voice.js` | `getVoice`, `setVoice`, `subscribeToVoice`, `getAvailableVoices`, `waitForVoices`, `subscribeToAvailableVoices` | Persisted speech-synthesis voice preference (mirrors `theme`); used by `<k-voice-selector>` and consumed by `<k-text-to-speech>` when no explicit `voice` attr is set | [voice.html](https://dustinpoissant.github.io/kempo-ui/utils/voice.html) |
246
250
  | `propConverters.js` | `boolExists`, `boolTrueFalse` | Lit property converters for boolean attributes | [propConverters.html](https://dustinpoissant.github.io/kempo-ui/utils/propConverters.html) |
247
251
  | `context.js` | `default` | `createContext(key, initialValue)` → `{ get, set, subscribe }` pub/sub state | [context.html](https://dustinpoissant.github.io/kempo-ui/utils/context.html) |
248
252
  | `cookie.js` | `saveCookie`, `getCookie`, `deleteCookie` | Get/set/delete browser cookies | [cookie.html](https://dustinpoissant.github.io/kempo-ui/utils/cookie.html) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo-ui",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "type": "module",
5
5
  "description": "A Lit based web-component library",
6
6
  "main": "index.js",
@@ -0,0 +1,252 @@
1
+ import { html, css } from '../lit-all.min.js';
2
+ import ShadowComponent from './ShadowComponent.js';
3
+ import './Icon.js';
4
+
5
+ export default class SpeechToText extends ShadowComponent {
6
+ static properties = {
7
+ language: { type: String, reflect: true },
8
+ continuous: { type: Boolean, reflect: true },
9
+ interim: { type: Boolean, reflect: true },
10
+ minConfidence: { type: Number, reflect: true, attribute: 'min-confidence' },
11
+ timeout: { type: Number, reflect: true },
12
+ disabled: { type: Boolean, reflect: true },
13
+ listening: { type: Boolean, reflect: true }
14
+ };
15
+
16
+ #recognition = null;
17
+ #buffer = '';
18
+ #interim = '';
19
+ #unsupported = false;
20
+ #timeoutId = null;
21
+
22
+ /*
23
+ Lifecycle Callbacks
24
+ */
25
+ constructor() {
26
+ super();
27
+ this.language = 'en-US';
28
+ this.continuous = false;
29
+ this.interim = false;
30
+ this.minConfidence = 0;
31
+ this.timeout = 0;
32
+ this.disabled = false;
33
+ this.listening = false;
34
+ }
35
+
36
+ connectedCallback() {
37
+ super.connectedCallback();
38
+ const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
39
+ if(!SR){
40
+ this.#unsupported = true;
41
+ return;
42
+ }
43
+ this.#recognition = new SR();
44
+ this.#recognition.lang = this.language;
45
+ this.#recognition.continuous = this.continuous;
46
+ // Always enable interim internally so the `end` event has a fallback
47
+ // transcript when the engine doesn't finalize. The public `interim` attr
48
+ // controls whether `result` events fire for unfinalized chunks.
49
+ this.#recognition.interimResults = true;
50
+ this.#recognition.onstart = this.handleStart;
51
+ this.#recognition.onend = this.handleEnd;
52
+ this.#recognition.onresult = this.handleResult;
53
+ this.#recognition.onerror = this.handleError;
54
+ }
55
+
56
+ disconnectedCallback() {
57
+ super.disconnectedCallback();
58
+ this.#clearTimeout();
59
+ if(this.#recognition){
60
+ try { this.#recognition.stop(); } catch(e) {}
61
+ this.#recognition.onstart = null;
62
+ this.#recognition.onend = null;
63
+ this.#recognition.onresult = null;
64
+ this.#recognition.onerror = null;
65
+ this.#recognition = null;
66
+ }
67
+ }
68
+
69
+ updated(changedProperties) {
70
+ super.updated(changedProperties);
71
+ if(this.#recognition){
72
+ if(changedProperties.has('language')) this.#recognition.lang = this.language;
73
+ if(changedProperties.has('continuous')) this.#recognition.continuous = this.continuous;
74
+ }
75
+ }
76
+
77
+ /*
78
+ Event Handlers
79
+ */
80
+ handleStart = () => {
81
+ this.#buffer = '';
82
+ this.#interim = '';
83
+ this.listening = true;
84
+ this.#armTimeout();
85
+ this.dispatchEvent(new CustomEvent('start', { bubbles: true }));
86
+ };
87
+
88
+ handleResult = (e) => {
89
+ let final = '';
90
+ let interim = '';
91
+ for(let i = e.resultIndex; i < e.results.length; i++){
92
+ const res = e.results[i];
93
+ const alt = res[0];
94
+ const confidence = typeof alt.confidence === 'number' ? alt.confidence : 0;
95
+ if(confidence < this.minConfidence) continue;
96
+ if(res.isFinal) final += alt.transcript;
97
+ else interim += alt.transcript;
98
+ }
99
+ if(final) this.#buffer += final;
100
+ // Only overwrite the cached interim when this event actually carries text.
101
+ // Chrome occasionally fires a "settling" result event where final/interim
102
+ // are both empty — wiping #interim in that case loses the last transcript
103
+ // and the `end` event ends up with nothing to report.
104
+ if(final || interim){
105
+ this.#interim = interim;
106
+ }
107
+ if(final || this.interim){
108
+ this.dispatchEvent(new CustomEvent('result', {
109
+ detail: {
110
+ text: (this.#buffer + (interim || this.#interim)).trim(),
111
+ isFinal: !!final && !interim
112
+ },
113
+ bubbles: true
114
+ }));
115
+ }
116
+ };
117
+
118
+ handleEnd = () => {
119
+ this.listening = false;
120
+ this.#clearTimeout();
121
+ // Include any unfinalized interim text — Chrome occasionally fires `end`
122
+ // without finalizing the last utterance.
123
+ const text = (this.#buffer + this.#interim).trim();
124
+ this.dispatchEvent(new CustomEvent('end', {
125
+ detail: { text },
126
+ bubbles: true
127
+ }));
128
+ };
129
+
130
+ handleError = (e) => {
131
+ this.listening = false;
132
+ this.#clearTimeout();
133
+ this.dispatchEvent(new CustomEvent('error', {
134
+ detail: { error: e.error, message: e.message },
135
+ bubbles: true
136
+ }));
137
+ };
138
+
139
+ #armTimeout = () => {
140
+ this.#clearTimeout();
141
+ if(this.timeout > 0){
142
+ this.#timeoutId = setTimeout(() => this.stop(), this.timeout * 1000);
143
+ }
144
+ };
145
+
146
+ #clearTimeout = () => {
147
+ if(this.#timeoutId){
148
+ clearTimeout(this.#timeoutId);
149
+ this.#timeoutId = null;
150
+ }
151
+ };
152
+
153
+ handleClick = () => {
154
+ if(this.disabled) return;
155
+ if(this.listening) this.stop();
156
+ else this.start();
157
+ };
158
+
159
+ /*
160
+ Public Methods
161
+ */
162
+ start = () => {
163
+ if(this.disabled || !this.#recognition || this.listening) return;
164
+ try { this.#recognition.start(); } catch(e) {}
165
+ };
166
+
167
+ stop = () => {
168
+ if(!this.#recognition || !this.listening) return;
169
+ try { this.#recognition.stop(); } catch(e) {}
170
+ };
171
+
172
+ toggle = () => {
173
+ this.handleClick();
174
+ };
175
+
176
+ /*
177
+ Rendering
178
+ */
179
+ render() {
180
+ const supported = !this.#unsupported;
181
+ const label = !supported
182
+ ? 'Speech recognition not supported in this browser'
183
+ : this.listening ? 'Stop listening' : 'Start listening';
184
+ return html`
185
+ <button
186
+ type="button"
187
+ class="no-btn btn ${this.listening ? 'listening' : ''}"
188
+ ?disabled=${this.disabled || !supported}
189
+ @click=${this.handleClick}
190
+ aria-label=${label}
191
+ title=${label}
192
+ >
193
+ <k-icon name=${this.listening ? 'stop' : 'mic'}></k-icon>
194
+ </button>
195
+ `;
196
+ }
197
+
198
+ /*
199
+ Styles
200
+ */
201
+ static styles = css`
202
+ :host {
203
+ --btn_size: 2.5rem;
204
+ --btn_bg: var(--c_bg);
205
+ --btn_bg__listening: var(--c_danger, #d32f2f);
206
+ --btn_tc__listening: white;
207
+ display: inline-block;
208
+ }
209
+ :host([disabled]) {
210
+ opacity: 0.5;
211
+ pointer-events: none;
212
+ }
213
+ .btn {
214
+ display: inline-flex;
215
+ align-items: center;
216
+ justify-content: center;
217
+ width: var(--btn_size);
218
+ height: var(--btn_size);
219
+ padding: 0;
220
+ border: 1px solid var(--c_border);
221
+ border-radius: 50%;
222
+ background: var(--btn_bg);
223
+ color: var(--tc);
224
+ cursor: pointer;
225
+ transition: background var(--animation_ms), color var(--animation_ms), border-color var(--animation_ms);
226
+ }
227
+ .btn:hover:not(:disabled) {
228
+ background: var(--c_bg__alt);
229
+ }
230
+ .btn:focus-visible {
231
+ outline: none;
232
+ box-shadow: var(--focus_shadow);
233
+ }
234
+ :host([listening]) .btn,
235
+ :host([listening]) .btn:hover,
236
+ :host([listening]) .btn:focus-visible {
237
+ background: var(--btn_bg__listening);
238
+ color: var(--btn_tc__listening);
239
+ border-color: transparent;
240
+ animation: pulse 1.4s infinite;
241
+ }
242
+ .btn:disabled {
243
+ cursor: not-allowed;
244
+ }
245
+ @keyframes pulse {
246
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(211, 47, 47, 0.5); }
247
+ 50% { box-shadow: 0 0 0 10px rgba(211, 47, 47, 0); }
248
+ }
249
+ `;
250
+ }
251
+
252
+ customElements.define('k-speech-to-text', SpeechToText);