kempo-ui 0.4.3 → 0.4.4

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 (96) 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/docs/components/accordion.html +8 -0
  11. package/docs/components/aside.html +8 -0
  12. package/docs/components/calendar.html +8 -0
  13. package/docs/components/card.html +8 -0
  14. package/docs/components/code-editor.html +8 -0
  15. package/docs/components/color-picker.html +8 -0
  16. package/docs/components/combobox.html +8 -0
  17. package/docs/components/content-slider.html +8 -0
  18. package/docs/components/context.html +8 -0
  19. package/docs/components/dialog.html +8 -0
  20. package/docs/components/dropdown.html +8 -0
  21. package/docs/components/filter-list.html +8 -0
  22. package/docs/components/focus-capture.html +8 -0
  23. package/docs/components/html-editor.html +8 -0
  24. package/docs/components/hybrid-component.html +8 -0
  25. package/docs/components/icon.html +8 -0
  26. package/docs/components/import.html +8 -0
  27. package/docs/components/light-component.html +8 -0
  28. package/docs/components/nav-spacer.html +8 -0
  29. package/docs/components/nav.html +8 -0
  30. package/docs/components/photo-viewer.html +8 -0
  31. package/docs/components/progress.html +8 -0
  32. package/docs/components/resize.html +8 -0
  33. package/docs/components/shadow-component.html +8 -0
  34. package/docs/components/show-more.html +8 -0
  35. package/docs/components/slider.html +8 -0
  36. package/docs/components/sortable.html +8 -0
  37. package/docs/components/speech-to-text.html +717 -0
  38. package/docs/components/spinner.html +8 -0
  39. package/docs/components/split.html +8 -0
  40. package/docs/components/table.html +8 -0
  41. package/docs/components/tableControls.html +8 -0
  42. package/docs/components/tableCustomFields.html +8 -0
  43. package/docs/components/tableFetchRecords.html +8 -0
  44. package/docs/components/tableFieldSortHide.html +8 -0
  45. package/docs/components/tablePagination.html +8 -0
  46. package/docs/components/tablePlaceholder.html +8 -0
  47. package/docs/components/tableRecordEditing.html +8 -0
  48. package/docs/components/tableRecordFiltering.html +8 -0
  49. package/docs/components/tableRecordHiding.html +8 -0
  50. package/docs/components/tableRecordSearching.html +8 -0
  51. package/docs/components/tableRecordSelection.html +8 -0
  52. package/docs/components/tableRowControls.html +8 -0
  53. package/docs/components/tableServerSync.html +8 -0
  54. package/docs/components/tableSorting.html +8 -0
  55. package/docs/components/tabs.html +8 -0
  56. package/docs/components/tags.html +8 -0
  57. package/docs/components/text-to-speech.html +668 -0
  58. package/docs/components/theme-select.html +8 -0
  59. package/docs/components/theme-switcher.html +8 -0
  60. package/docs/components/time.html +8 -0
  61. package/docs/components/timestamp.html +8 -0
  62. package/docs/components/toast.html +8 -0
  63. package/docs/components/toggle.html +8 -0
  64. package/docs/components/tree.html +8 -0
  65. package/docs/icons/mic.svg +1 -0
  66. package/docs/icons/record_voice_over.svg +1 -0
  67. package/docs/icons/stop.svg +1 -0
  68. package/docs/index.html +20 -0
  69. package/docs/src/components/SpeechToText.js +60 -0
  70. package/docs/src/components/TextToSpeech.js +56 -0
  71. package/docs/utils/context.html +8 -0
  72. package/docs/utils/cookie.html +8 -0
  73. package/docs/utils/debounce.html +8 -0
  74. package/docs/utils/drag.html +8 -0
  75. package/docs/utils/elevation.html +8 -0
  76. package/docs/utils/formatTimestamp.html +8 -0
  77. package/docs/utils/object.html +8 -0
  78. package/docs/utils/propConverters.html +8 -0
  79. package/docs/utils/string.html +8 -0
  80. package/docs/utils/theme.html +8 -0
  81. package/docs/utils/toTitleCase.html +8 -0
  82. package/docs/utils/type.html +8 -0
  83. package/docs/utils/wait.html +8 -0
  84. package/docs-src/components/speech-to-text.page.html +251 -0
  85. package/docs-src/components/text-to-speech.page.html +202 -0
  86. package/docs-src/index.page.html +12 -0
  87. package/docs-src/nav.fragment.html +8 -0
  88. package/icons/mic.svg +1 -0
  89. package/icons/record_voice_over.svg +1 -0
  90. package/icons/stop.svg +1 -0
  91. package/llms.txt +2 -0
  92. package/package.json +1 -1
  93. package/src/components/SpeechToText.js +252 -0
  94. package/src/components/TextToSpeech.js +194 -0
  95. package/tests/components/SpeechToText.browser-test.js +530 -0
  96. package/tests/components/TextToSpeech.browser-test.js +432 -0
@@ -0,0 +1,202 @@
1
+ <page pageName="Text To Speech" title="Text To Speech - 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="#slotted">Slotted Text</a><br />
10
+ <a href="#rate">Rate &amp; Pitch</a><br />
11
+ <a href="#language">Language</a><br />
12
+ <a href="#dynamic">Dynamic Text</a><br />
13
+ <a href="#disabled">Disabled</a><br />
14
+
15
+ <h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
16
+ <a href="#constructor">Constructor</a><br />
17
+ <a href="#requirements">Requirements</a><br />
18
+ <a href="#properties">Properties</a><br />
19
+ <a href="#methods">Methods</a><br />
20
+ <a href="#cssVariables">CSS Variables</a><br />
21
+ <a href="#events">Events</a><br />
22
+ </div>
23
+ </k-accordion-panel>
24
+ </k-accordion>
25
+
26
+ <p>Wraps the browser's <a href="https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis" target="_blank">SpeechSynthesis API</a> in a single button. Click to speak; click again while speaking to stop. Works in all modern browsers (Chromium, Firefox, Safari).</p>
27
+
28
+ <h3 id="basicUsage"><a href="#basicUsage" class="no-link">Basic Usage</a></h3>
29
+ <p>The <code>text</code> attribute holds the phrase to read aloud.</p>
30
+ <div class="row -mx">
31
+ <div class="col m-span-12 px">
32
+ <k-card label="HTML">
33
+ <pre><code class="hljs xml"><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;Hello, world!&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-text-to-speech</span>&gt;</span></code></pre>
34
+ </k-card>
35
+ </div>
36
+ <div class="col m-span-12 px">
37
+ <k-card label="Output">
38
+ <k-text-to-speech text="Hello, world!"></k-text-to-speech>
39
+ </k-card>
40
+ </div>
41
+ </div>
42
+
43
+ <h3 id="slotted"><a href="#slotted" class="no-link">Slotted Text</a></h3>
44
+ <p>If <code>text</code> is not set, the element's text content is spoken. Useful when the text is longer or already lives in the DOM.</p>
45
+ <div class="row -mx">
46
+ <div class="col m-span-12 px">
47
+ <k-card label="HTML">
48
+ <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">k-text-to-speech</span>&gt;</span>The quick brown fox jumps over the lazy dog.<span class="hljs-tag">&lt;/<span class="hljs-name">k-text-to-speech</span>&gt;</span></code></pre>
49
+ </k-card>
50
+ </div>
51
+ <div class="col m-span-12 px">
52
+ <k-card label="Output">
53
+ <k-text-to-speech>The quick brown fox jumps over the lazy dog.</k-text-to-speech>
54
+ </k-card>
55
+ </div>
56
+ </div>
57
+
58
+ <h3 id="rate"><a href="#rate" class="no-link">Rate &amp; Pitch</a></h3>
59
+ <p>Tune playback with the <code>rate</code> (<code>0.1</code>&ndash;<code>10</code>, default <code>1</code>) and <code>pitch</code> (<code>0</code>&ndash;<code>2</code>, default <code>1</code>) attributes. <code>volume</code> (<code>0</code>&ndash;<code>1</code>, default <code>1</code>) is also available.</p>
60
+ <div class="row -mx">
61
+ <div class="col m-span-12 px">
62
+ <k-card label="HTML">
63
+ <pre><code class="hljs xml"><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;Wow that is fast&quot;</span> <span class="hljs-attr">rate</span>=<span class="hljs-string">&quot;2&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-text-to-speech</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;A bit higher pitch&quot;</span> <span class="hljs-attr">pitch</span>=<span class="hljs-string">&quot;1.6&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-text-to-speech</span>&gt;</span></code></pre>
64
+ </k-card>
65
+ </div>
66
+ <div class="col m-span-12 px">
67
+ <k-card label="Output">
68
+ <k-text-to-speech text="Wow that is fast" rate="2" class="mr"></k-text-to-speech>
69
+ <k-text-to-speech text="A bit higher pitch" pitch="1.6"></k-text-to-speech>
70
+ </k-card>
71
+ </div>
72
+ </div>
73
+
74
+ <h3 id="language"><a href="#language" class="no-link">Language</a></h3>
75
+ <p>Set <code>language</code> to a BCP&#x2011;47 tag to pick a localized voice (browser-dependent &mdash; the OS-installed voices determine availability).</p>
76
+ <div class="row -mx">
77
+ <div class="col m-span-12 px">
78
+ <k-card label="HTML">
79
+ <pre><code class="hljs xml"><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;Bonjour le monde&quot;</span> <span class="hljs-attr">language</span>=<span class="hljs-string">&quot;fr-FR&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-text-to-speech</span>&gt;</span></code></pre>
80
+ </k-card>
81
+ </div>
82
+ <div class="col m-span-12 px">
83
+ <k-card label="Output">
84
+ <k-text-to-speech text="Bonjour le monde" language="fr-FR"></k-text-to-speech>
85
+ </k-card>
86
+ </div>
87
+ </div>
88
+
89
+ <h3 id="dynamic"><a href="#dynamic" class="no-link">Dynamic Text</a></h3>
90
+ <p>Set the <code>text</code> property at runtime to read whatever is in an input or other DOM source.</p>
91
+ <div class="row -mx">
92
+ <div class="col m-span-12 px">
93
+ <k-card label="HTML">
94
+ <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">k-text-to-speech</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;ttsExample&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-text-to-speech</span>&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;ttsInput&quot;</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;text&quot;</span> <span class="hljs-attr">value</span>=<span class="hljs-string">&quot;Type something then click the button&quot;</span> /&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="language-javascript"><br> <span class="hljs-keyword">const</span> $tts = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">&#x27;ttsExample&#x27;</span>);<br> <span class="hljs-keyword">const</span> $input = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">&#x27;ttsInput&#x27;</span>);<br> $input.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;input&#x27;</span>, <span class="hljs-function">() =&gt;</span> {<br> $tts.<span class="hljs-property">text</span> = $input.<span class="hljs-property">value</span>;<br> });<br> $tts.<span class="hljs-property">text</span> = $input.<span class="hljs-property">value</span>;<br></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></code></pre>
95
+ </k-card>
96
+ </div>
97
+ <div class="col m-span-12 px">
98
+ <k-card label="Output">
99
+ <k-text-to-speech id="ttsExample" class="mr"></k-text-to-speech>
100
+ <input id="ttsInput" type="text" value="Type something then click the button" />
101
+ <script>
102
+ const $tts = document.getElementById('ttsExample');
103
+ const $input = document.getElementById('ttsInput');
104
+ $input.addEventListener('input', () => {
105
+ $tts.text = $input.value;
106
+ });
107
+ $tts.text = $input.value;
108
+ </script>
109
+ </k-card>
110
+ </div>
111
+ </div>
112
+
113
+ <h3 id="disabled"><a href="#disabled" class="no-link">Disabled</a></h3>
114
+ <p>The <code>disabled</code> attribute prevents user interaction. The button also auto-disables when the browser doesn't support the SpeechSynthesis API.</p>
115
+ <div class="row -mx">
116
+ <div class="col m-span-12 px">
117
+ <k-card label="HTML">
118
+ <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">k-text-to-speech</span> <span class="hljs-attr">disabled</span> <span class="hljs-attr">text</span>=<span class="hljs-string">&quot;You can't hear this&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-text-to-speech</span>&gt;</span></code></pre>
119
+ </k-card>
120
+ </div>
121
+ <div class="col m-span-12 px">
122
+ <k-card label="Output">
123
+ <k-text-to-speech disabled text="You can't hear this"></k-text-to-speech>
124
+ </k-card>
125
+ </div>
126
+ </div>
127
+
128
+ <h2 id="jsRef"><a href="#jsRef" class="no-link">JavaScript Reference</a></h2>
129
+
130
+ <h3 id="constructor"><a href="#constructor" class="no-link">Constructor</a></h3>
131
+ <h6>Extends <a href="./shadow-component.html">ShadowComponent</a></h6>
132
+ <h5><code>new TextToSpeech()</code></h5>
133
+
134
+ <h3 id="requirements"><a href="#requirements" class="no-link">Requirements</a></h3>
135
+ <ul>
136
+ <li><a href="./shadow-component.html">ShadowComponent</a></li>
137
+ <li>A browser with <code>window.speechSynthesis</code> (all major modern browsers)</li>
138
+ </ul>
139
+
140
+ <h3 id="properties"><a href="#properties" class="no-link">Properties</a></h3>
141
+ <h5><code>text<i>: String</i></code></h5>
142
+ <p>The text to speak. When empty, the element's slotted text content is used. Syncs to <code>text</code> attribute.</p>
143
+
144
+ <h5><code>voice<i>: String</i></code></h5>
145
+ <p>Optional voice name (matched against <code>speechSynthesis.getVoices()</code>). Falls back to the default voice for the language. Syncs to <code>voice</code> attribute.</p>
146
+
147
+ <h5><code>language<i>: String</i></code></h5>
148
+ <p>BCP&#x2011;47 language tag (e.g. <code>"en-US"</code>, <code>"fr-FR"</code>). Default is empty (browser default). Syncs to <code>language</code> attribute.</p>
149
+
150
+ <h5><code>rate<i>: Number</i></code></h5>
151
+ <p>Speech rate from <code>0.1</code> (slow) to <code>10</code> (fast). Default is <code>1</code>. Syncs to <code>rate</code> attribute.</p>
152
+
153
+ <h5><code>pitch<i>: Number</i></code></h5>
154
+ <p>Pitch from <code>0</code> to <code>2</code>. Default is <code>1</code>. Syncs to <code>pitch</code> attribute.</p>
155
+
156
+ <h5><code>volume<i>: Number</i></code></h5>
157
+ <p>Volume from <code>0</code> (silent) to <code>1</code> (loud). Default is <code>1</code>. Syncs to <code>volume</code> attribute.</p>
158
+
159
+ <h5><code>disabled<i>: Boolean</i></code></h5>
160
+ <p>When <code>true</code>, the button cannot be clicked. Syncs to <code>disabled</code> attribute.</p>
161
+
162
+ <h5><code>speaking<i>: Boolean</i></code></h5>
163
+ <p>Reflects whether the component is currently speaking. The button shows the stop icon and switches to the active color while <code>true</code>. Syncs to <code>speaking</code> attribute.</p>
164
+
165
+ <h3 id="methods"><a href="#methods" class="no-link">Methods</a></h3>
166
+ <h5><code>speak(textOverride?)<i>: void</i></code></h5>
167
+ <p>Begin speaking. If <code>textOverride</code> is supplied, it is spoken instead of <code>text</code>/slot content. Cancels any in-flight utterance first.</p>
168
+
169
+ <h5><code>stop()<i>: void</i></code></h5>
170
+ <p>Cancel any speech in progress.</p>
171
+
172
+ <h5><code>toggle()<i>: void</i></code></h5>
173
+ <p>Calls <code>stop()</code> if currently speaking, otherwise <code>speak()</code> &mdash; the same behavior as clicking the button.</p>
174
+
175
+ <h3 id="cssVariables"><a href="#cssVariables" class="no-link">CSS Variables</a></h3>
176
+ <table class="b">
177
+ <thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
178
+ <tbody>
179
+ <tr><td><code>--btn_size</code></td><td><code>2.5rem</code></td><td>Diameter of the button</td></tr>
180
+ <tr><td><code>--btn_bg</code></td><td><code>var(--c_bg)</code></td><td>Background of the idle button</td></tr>
181
+ <tr><td><code>--btn_bg__speaking</code></td><td><code>var(--c_primary)</code></td><td>Background while actively speaking</td></tr>
182
+ <tr><td><code>--btn_tc__speaking</code></td><td><code>white</code></td><td>Icon color while speaking</td></tr>
183
+ </tbody>
184
+ </table>
185
+
186
+ <h3 id="events"><a href="#events" class="no-link">Events</a></h3>
187
+ <h5><code>start</code></h5>
188
+ <p>Fires when speech begins.</p>
189
+
190
+ <h5><code>end</code></h5>
191
+ <p>Fires when speech finishes (naturally or via <code>stop()</code>).</p>
192
+
193
+ <h5><code>error</code></h5>
194
+ <p>Fires if the underlying <code>SpeechSynthesisUtterance</code> reports an error. <code>detail</code> contains <code>{ error: String }</code>.</p>
195
+ </content>
196
+ <content location="scripts">
197
+ <script type="module" src="{{pathToRoot}}src/components/TextToSpeech.js"></script>
198
+ <script type="module" src="{{pathToRoot}}src/components/Icon.js"></script>
199
+ <script type="module" src="{{pathToRoot}}src/components/Accordion.js"></script>
200
+ <script type="module" src="{{pathToRoot}}src/components/Card.js"></script>
201
+ </content>
202
+ </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>
@@ -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>
@@ -281,11 +287,13 @@
281
287
  <a href="{{pathToRoot}}components/show-more.html">Show More</a>
282
288
  <a href="{{pathToRoot}}components/slider.html">Slider</a>
283
289
  <a href="{{pathToRoot}}components/sortable.html">Sortable</a>
290
+ <a href="{{pathToRoot}}components/speech-to-text.html">Speech To Text</a>
284
291
  <a href="{{pathToRoot}}components/spinner.html">Spinner</a>
285
292
  <a href="{{pathToRoot}}components/split.html">Split</a>
286
293
  <a href="{{pathToRoot}}components/table.html">Table</a>
287
294
  <a href="{{pathToRoot}}components/tabs.html">Tabs</a>
288
295
  <a href="{{pathToRoot}}components/tags.html">Tags</a>
296
+ <a href="{{pathToRoot}}components/text-to-speech.html">Text To Speech</a>
289
297
  <a href="{{pathToRoot}}components/theme-select.html">Theme Select</a>
290
298
  <a href="{{pathToRoot}}components/theme-switcher.html">Theme Switcher</a>
291
299
  <a href="{{pathToRoot}}components/time.html">Time</a>
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) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo-ui",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
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);