kempo-ui 0.4.4 → 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 (90) hide show
  1. package/dist/components/TextToSpeech.js +4 -4
  2. package/dist/components/VoiceSelector.js +49 -0
  3. package/dist/utils/voice.js +1 -0
  4. package/docs/components/accordion.html +7 -0
  5. package/docs/components/aside.html +7 -0
  6. package/docs/components/calendar.html +7 -0
  7. package/docs/components/card.html +7 -0
  8. package/docs/components/code-editor.html +7 -0
  9. package/docs/components/color-picker.html +7 -0
  10. package/docs/components/combobox.html +7 -0
  11. package/docs/components/content-slider.html +7 -0
  12. package/docs/components/context.html +7 -0
  13. package/docs/components/dialog.html +7 -0
  14. package/docs/components/dropdown.html +7 -0
  15. package/docs/components/filter-list.html +7 -0
  16. package/docs/components/focus-capture.html +7 -0
  17. package/docs/components/html-editor.html +7 -0
  18. package/docs/components/hybrid-component.html +7 -0
  19. package/docs/components/icon.html +7 -0
  20. package/docs/components/import.html +7 -0
  21. package/docs/components/light-component.html +7 -0
  22. package/docs/components/nav-spacer.html +7 -0
  23. package/docs/components/nav.html +7 -0
  24. package/docs/components/photo-viewer.html +7 -0
  25. package/docs/components/progress.html +7 -0
  26. package/docs/components/resize.html +7 -0
  27. package/docs/components/shadow-component.html +7 -0
  28. package/docs/components/show-more.html +7 -0
  29. package/docs/components/slider.html +7 -0
  30. package/docs/components/sortable.html +7 -0
  31. package/docs/components/speech-to-text.html +7 -0
  32. package/docs/components/spinner.html +7 -0
  33. package/docs/components/split.html +7 -0
  34. package/docs/components/table.html +7 -0
  35. package/docs/components/tableControls.html +7 -0
  36. package/docs/components/tableCustomFields.html +7 -0
  37. package/docs/components/tableFetchRecords.html +7 -0
  38. package/docs/components/tableFieldSortHide.html +7 -0
  39. package/docs/components/tablePagination.html +7 -0
  40. package/docs/components/tablePlaceholder.html +7 -0
  41. package/docs/components/tableRecordEditing.html +7 -0
  42. package/docs/components/tableRecordFiltering.html +7 -0
  43. package/docs/components/tableRecordHiding.html +7 -0
  44. package/docs/components/tableRecordSearching.html +7 -0
  45. package/docs/components/tableRecordSelection.html +7 -0
  46. package/docs/components/tableRowControls.html +7 -0
  47. package/docs/components/tableServerSync.html +7 -0
  48. package/docs/components/tableSorting.html +7 -0
  49. package/docs/components/tabs.html +7 -0
  50. package/docs/components/tags.html +7 -0
  51. package/docs/components/text-to-speech.html +51 -1
  52. package/docs/components/theme-select.html +7 -0
  53. package/docs/components/theme-switcher.html +7 -0
  54. package/docs/components/time.html +7 -0
  55. package/docs/components/timestamp.html +7 -0
  56. package/docs/components/toast.html +7 -0
  57. package/docs/components/toggle.html +7 -0
  58. package/docs/components/tree.html +7 -0
  59. package/docs/components/voice-selector.html +598 -0
  60. package/docs/index.html +19 -0
  61. package/docs/src/components/TextToSpeech.js +4 -4
  62. package/docs/src/components/VoiceSelector.js +49 -0
  63. package/docs/src/utils/voice.js +1 -0
  64. package/docs/utils/context.html +7 -0
  65. package/docs/utils/cookie.html +7 -0
  66. package/docs/utils/debounce.html +7 -0
  67. package/docs/utils/drag.html +7 -0
  68. package/docs/utils/elevation.html +7 -0
  69. package/docs/utils/formatTimestamp.html +7 -0
  70. package/docs/utils/object.html +7 -0
  71. package/docs/utils/propConverters.html +7 -0
  72. package/docs/utils/string.html +7 -0
  73. package/docs/utils/theme.html +7 -0
  74. package/docs/utils/toTitleCase.html +7 -0
  75. package/docs/utils/type.html +7 -0
  76. package/docs/utils/voice.html +536 -0
  77. package/docs/utils/wait.html +7 -0
  78. package/docs-src/components/text-to-speech.page.html +44 -1
  79. package/docs-src/components/voice-selector.page.html +125 -0
  80. package/docs-src/index.page.html +12 -0
  81. package/docs-src/nav.fragment.html +7 -0
  82. package/docs-src/utils/voice.page.html +66 -0
  83. package/llms.txt +2 -0
  84. package/package.json +1 -1
  85. package/src/components/TextToSpeech.js +13 -2
  86. package/src/components/VoiceSelector.js +271 -0
  87. package/src/utils/voice.js +103 -0
  88. package/tests/components/TextToSpeech.browser-test.js +150 -1
  89. package/tests/components/VoiceSelector.browser-test.js +374 -0
  90. package/tests/utils/voice.browser-test.js +128 -0
@@ -9,6 +9,7 @@
9
9
  <a href="#slotted">Slotted Text</a><br />
10
10
  <a href="#rate">Rate &amp; Pitch</a><br />
11
11
  <a href="#language">Language</a><br />
12
+ <a href="#voice">Voice Selection</a><br />
12
13
  <a href="#dynamic">Dynamic Text</a><br />
13
14
  <a href="#disabled">Disabled</a><br />
14
15
 
@@ -86,6 +87,48 @@
86
87
  </div>
87
88
  </div>
88
89
 
90
+ <h3 id="voice"><a href="#voice" class="no-link">Voice Selection</a></h3>
91
+ <p>Set <code>voice</code> to a specific voice name. The component looks it up in <code>speechSynthesis.getVoices()</code>; if the name isn't installed on the user's device the component silently falls back to the language default. Voice availability varies by browser and OS &mdash; here's a quick reference:</p>
92
+ <table class="b">
93
+ <thead><tr><th>Platform</th><th>Available voices</th></tr></thead>
94
+ <tbody>
95
+ <tr><td>Chrome (any OS)</td><td><code>Google US English</code>, <code>Google UK English Female</code>, <code>Google UK English Male</code>, plus localized <code>Google fran&ccedil;ais</code>, <code>Google espa&ntilde;ol</code>, etc. Most reliable cross-platform names.</td></tr>
96
+ <tr><td>macOS / iOS Safari</td><td><code>Samantha</code>, <code>Alex</code>, <code>Karen</code>, <code>Daniel</code>, <code>Moira</code>, plus any "premium" voices the user has downloaded.</td></tr>
97
+ <tr><td>Windows (Edge / non-Chrome)</td><td><code>Microsoft David</code>, <code>Microsoft Zira</code>, <code>Microsoft Mark</code>, plus regional packs.</td></tr>
98
+ <tr><td>Linux / eSpeak</td><td>A single generic voice; quality is rough.</td></tr>
99
+ <tr><td>Android Chrome</td><td>Whatever TTS engine the user has installed; defaults to Google TTS.</td></tr>
100
+ </tbody>
101
+ </table>
102
+ <p class="mt">There is no voice name that is identical across every platform. For portable code, prefer <code>language</code> over <code>voice</code> &mdash; the browser will pick the best installed voice for that language tag. Only set <code>voice</code> when you specifically want one and accept the device-dependent fallback.</p>
103
+ <div class="row -mx">
104
+ <div class="col m-span-12 px">
105
+ <k-card label="HTML">
106
+ <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, this is Google US English&quot;</span> <span class="hljs-attr">voice</span>=<span class="hljs-string">&quot;Google US English&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;Hello in French (browser picks a voice)&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>
107
+ </k-card>
108
+ </div>
109
+ <div class="col m-span-12 px">
110
+ <k-card label="Output">
111
+ <k-text-to-speech text="Hello, this is Google US English" voice="Google US English" class="mr"></k-text-to-speech>
112
+ <k-text-to-speech text="Hello in French (browser picks a voice)" language="fr-FR"></k-text-to-speech>
113
+ </k-card>
114
+ </div>
115
+ </div>
116
+
117
+ <h4>Voice Fallback Chain</h4>
118
+ <p>The <code>voice</code> attribute accepts a comma-separated list (like CSS <code>font-family</code>). The component tries each name in order and uses the first one installed; if none match it falls back to the language default.</p>
119
+ <div class="row -mx">
120
+ <div class="col m-span-12 px">
121
+ <k-card label="HTML">
122
+ <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;Trying Samantha first, then a Google fallback&quot;</span> <span class="hljs-attr">voice</span>=<span class="hljs-string">&quot;Samantha, Google US English, Microsoft Zira&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-text-to-speech</span>&gt;</span></code></pre>
123
+ </k-card>
124
+ </div>
125
+ <div class="col m-span-12 px">
126
+ <k-card label="Output">
127
+ <k-text-to-speech text="Trying Samantha first, then a Google fallback" voice="Samantha, Google US English, Microsoft Zira"></k-text-to-speech>
128
+ </k-card>
129
+ </div>
130
+ </div>
131
+
89
132
  <h3 id="dynamic"><a href="#dynamic" class="no-link">Dynamic Text</a></h3>
90
133
  <p>Set the <code>text</code> property at runtime to read whatever is in an input or other DOM source.</p>
91
134
  <div class="row -mx">
@@ -142,7 +185,7 @@
142
185
  <p>The text to speak. When empty, the element's slotted text content is used. Syncs to <code>text</code> attribute.</p>
143
186
 
144
187
  <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>
188
+ <p>Optional voice name, or a comma-separated fallback chain (e.g. <code>"Samantha, Google US English, Microsoft Zira"</code>). Each name is matched against <code>speechSynthesis.getVoices()</code> in order; the first installed match wins. If none match, the component falls back to the default voice for the configured <code>language</code>. Syncs to <code>voice</code> attribute.</p>
146
189
 
147
190
  <h5><code>language<i>: String</i></code></h5>
148
191
  <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>
@@ -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>
@@ -263,6 +263,12 @@
263
263
  <p class="tc-muted">Hierarchical data visualization with collapsible branches and custom leaf types.</p>
264
264
  </a>
265
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>
266
272
  </div>
267
273
 
268
274
  <h2>Base Components</h2>
@@ -349,6 +355,12 @@
349
355
  <p class="tc-muted">Functions for manipulating and analyzing JavaScript objects.</p>
350
356
  </a>
351
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>
352
364
  <div class="span-12 t-span-6 d-span-4 px">
353
365
  <a href="{{pathToRoot}}utils/wait.html" class="card mb no-link d-b">
354
366
  <h3 class="tc-primary">wait</h3>
@@ -182,6 +182,9 @@
182
182
  >Toggle<br><small>Component</small></a></k-filter-item>
183
183
  <k-filter-item filter-keywords="tree treeview components"><a
184
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>
185
188
  <k-filter-item filter-keywords="shadow component base"><a
186
189
  href="{{pathToRoot}}components/shadow-component.html"
187
190
  >Shadow Component<br><small>Base Component</small></a></k-filter-item>
@@ -215,6 +218,8 @@
215
218
  href="{{pathToRoot}}utils/theme.html">theme<br><small>Utility</small></a></k-filter-item>
216
219
  <k-filter-item filter-keywords="type utils utility"><a
217
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>
218
223
  <k-filter-item filter-keywords="wait async utils utility"><a
219
224
  href="{{pathToRoot}}utils/wait.html">wait<br><small>Utility</small></a></k-filter-item>
220
225
  </k-filter-list>
@@ -301,6 +306,7 @@
301
306
  <a href="{{pathToRoot}}components/toast.html">Toast</a>
302
307
  <a href="{{pathToRoot}}components/toggle.html">Toggle</a>
303
308
  <a href="{{pathToRoot}}components/tree.html">Tree</a>
309
+ <a href="{{pathToRoot}}components/voice-selector.html">Voice Selector</a>
304
310
  </div>
305
311
 
306
312
  <h5 class="mb0">Base Components</h5>
@@ -323,6 +329,7 @@
323
329
  <a href="{{pathToRoot}}utils/string.html">string</a>
324
330
  <a href="{{pathToRoot}}utils/theme.html">theme</a>
325
331
  <a href="{{pathToRoot}}utils/type.html">type</a>
332
+ <a href="{{pathToRoot}}utils/voice.html">voice</a>
326
333
  <a href="{{pathToRoot}}utils/wait.html">wait</a>
327
334
  </div>
328
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/llms.txt CHANGED
@@ -90,6 +90,7 @@ import { html, css, LitElement } from 'kempo-ui/src/lit-all.min.js';
90
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) |
91
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) |
92
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) |
93
94
 
94
95
  ---
95
96
 
@@ -245,6 +246,7 @@ Located in `src/utils/`. Import individually.
245
246
  | `debounce.js` | `default` | `debounce(fn, ms)` — delay fn until calls stop | [debounce.html](https://dustinpoissant.github.io/kempo-ui/utils/debounce.html) |
246
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) |
247
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) |
248
250
  | `propConverters.js` | `boolExists`, `boolTrueFalse` | Lit property converters for boolean attributes | [propConverters.html](https://dustinpoissant.github.io/kempo-ui/utils/propConverters.html) |
249
251
  | `context.js` | `default` | `createContext(key, initialValue)` → `{ get, set, subscribe }` pub/sub state | [context.html](https://dustinpoissant.github.io/kempo-ui/utils/context.html) |
250
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.4",
3
+ "version": "0.4.5",
4
4
  "type": "module",
5
5
  "description": "A Lit based web-component library",
6
6
  "main": "index.js",
@@ -1,5 +1,6 @@
1
1
  import { html, css } from '../lit-all.min.js';
2
2
  import ShadowComponent from './ShadowComponent.js';
3
+ import { getVoice as getPreferredVoice } from '../utils/voice.js';
3
4
  import './Icon.js';
4
5
 
5
6
  export default class TextToSpeech extends ShadowComponent {
@@ -53,9 +54,19 @@ export default class TextToSpeech extends ShadowComponent {
53
54
  }
54
55
 
55
56
  resolveVoice = () => {
56
- if(!this.voice || this.#unsupported) return null;
57
+ if(this.#unsupported) return null;
58
+ // Use the explicit `voice` attribute first; if none is set, fall back to
59
+ // the user's saved preference from the voice utility (so a site-wide
60
+ // VoiceSelector can drive every TextToSpeech that hasn't opted out).
61
+ const source = this.voice || getPreferredVoice() || '';
62
+ if(!source) return null;
57
63
  const voices = window.speechSynthesis.getVoices();
58
- return voices.find(v => v.name === this.voice) || null;
64
+ const candidates = source.split(',').map(s => s.trim()).filter(Boolean);
65
+ for(const name of candidates){
66
+ const match = voices.find(v => v.name === name);
67
+ if(match) return match;
68
+ }
69
+ return null;
59
70
  };
60
71
 
61
72
  /*