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.
- package/.claude/skills/get-icon/SKILL.md +144 -0
- package/.claude/skills/highlight-code/SKILL.md +62 -0
- package/.claude/skills/new-component/SKILL.md +362 -0
- package/.github/skills/get-icon/SKILL.md +4 -4
- package/.github/skills/highlight-code/SKILL.md +51 -20
- package/.github/skills/new-component/SKILL.md +183 -152
- package/AGENTS.md +2 -2
- package/dist/components/SpeechToText.js +60 -0
- package/dist/components/TextToSpeech.js +56 -0
- package/docs/components/accordion.html +8 -0
- package/docs/components/aside.html +8 -0
- package/docs/components/calendar.html +8 -0
- package/docs/components/card.html +8 -0
- package/docs/components/code-editor.html +8 -0
- package/docs/components/color-picker.html +8 -0
- package/docs/components/combobox.html +8 -0
- package/docs/components/content-slider.html +8 -0
- package/docs/components/context.html +8 -0
- package/docs/components/dialog.html +8 -0
- package/docs/components/dropdown.html +8 -0
- package/docs/components/filter-list.html +8 -0
- package/docs/components/focus-capture.html +8 -0
- package/docs/components/html-editor.html +8 -0
- package/docs/components/hybrid-component.html +8 -0
- package/docs/components/icon.html +8 -0
- package/docs/components/import.html +8 -0
- package/docs/components/light-component.html +8 -0
- package/docs/components/nav-spacer.html +8 -0
- package/docs/components/nav.html +8 -0
- package/docs/components/photo-viewer.html +8 -0
- package/docs/components/progress.html +8 -0
- package/docs/components/resize.html +8 -0
- package/docs/components/shadow-component.html +8 -0
- package/docs/components/show-more.html +8 -0
- package/docs/components/slider.html +8 -0
- package/docs/components/sortable.html +8 -0
- package/docs/components/speech-to-text.html +717 -0
- package/docs/components/spinner.html +8 -0
- package/docs/components/split.html +8 -0
- package/docs/components/table.html +8 -0
- package/docs/components/tableControls.html +8 -0
- package/docs/components/tableCustomFields.html +8 -0
- package/docs/components/tableFetchRecords.html +8 -0
- package/docs/components/tableFieldSortHide.html +8 -0
- package/docs/components/tablePagination.html +8 -0
- package/docs/components/tablePlaceholder.html +8 -0
- package/docs/components/tableRecordEditing.html +8 -0
- package/docs/components/tableRecordFiltering.html +8 -0
- package/docs/components/tableRecordHiding.html +8 -0
- package/docs/components/tableRecordSearching.html +8 -0
- package/docs/components/tableRecordSelection.html +8 -0
- package/docs/components/tableRowControls.html +8 -0
- package/docs/components/tableServerSync.html +8 -0
- package/docs/components/tableSorting.html +8 -0
- package/docs/components/tabs.html +8 -0
- package/docs/components/tags.html +8 -0
- package/docs/components/text-to-speech.html +668 -0
- package/docs/components/theme-select.html +8 -0
- package/docs/components/theme-switcher.html +8 -0
- package/docs/components/time.html +8 -0
- package/docs/components/timestamp.html +8 -0
- package/docs/components/toast.html +8 -0
- package/docs/components/toggle.html +8 -0
- package/docs/components/tree.html +8 -0
- package/docs/icons/mic.svg +1 -0
- package/docs/icons/record_voice_over.svg +1 -0
- package/docs/icons/stop.svg +1 -0
- package/docs/index.html +20 -0
- package/docs/src/components/SpeechToText.js +60 -0
- package/docs/src/components/TextToSpeech.js +56 -0
- package/docs/utils/context.html +8 -0
- package/docs/utils/cookie.html +8 -0
- package/docs/utils/debounce.html +8 -0
- package/docs/utils/drag.html +8 -0
- package/docs/utils/elevation.html +8 -0
- package/docs/utils/formatTimestamp.html +8 -0
- package/docs/utils/object.html +8 -0
- package/docs/utils/propConverters.html +8 -0
- package/docs/utils/string.html +8 -0
- package/docs/utils/theme.html +8 -0
- package/docs/utils/toTitleCase.html +8 -0
- package/docs/utils/type.html +8 -0
- package/docs/utils/wait.html +8 -0
- package/docs-src/components/speech-to-text.page.html +251 -0
- package/docs-src/components/text-to-speech.page.html +202 -0
- package/docs-src/index.page.html +12 -0
- package/docs-src/nav.fragment.html +8 -0
- package/icons/mic.svg +1 -0
- package/icons/record_voice_over.svg +1 -0
- package/icons/stop.svg +1 -0
- package/llms.txt +2 -0
- package/package.json +1 -1
- package/src/components/SpeechToText.js +252 -0
- package/src/components/TextToSpeech.js +194 -0
- package/tests/components/SpeechToText.browser-test.js +530 -0
- package/tests/components/TextToSpeech.browser-test.js +432 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { html, css } from '../lit-all.min.js';
|
|
2
|
+
import ShadowComponent from './ShadowComponent.js';
|
|
3
|
+
import './Icon.js';
|
|
4
|
+
|
|
5
|
+
export default class TextToSpeech extends ShadowComponent {
|
|
6
|
+
static properties = {
|
|
7
|
+
text: { type: String, reflect: true },
|
|
8
|
+
voice: { type: String, reflect: true },
|
|
9
|
+
language: { type: String, reflect: true },
|
|
10
|
+
rate: { type: Number, reflect: true },
|
|
11
|
+
pitch: { type: Number, reflect: true },
|
|
12
|
+
volume: { type: Number, reflect: true },
|
|
13
|
+
disabled: { type: Boolean, reflect: true },
|
|
14
|
+
speaking: { type: Boolean, reflect: true }
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
#unsupported = false;
|
|
18
|
+
#utterance = null;
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
Lifecycle Callbacks
|
|
22
|
+
*/
|
|
23
|
+
constructor() {
|
|
24
|
+
super();
|
|
25
|
+
this.text = '';
|
|
26
|
+
this.voice = '';
|
|
27
|
+
this.language = '';
|
|
28
|
+
this.rate = 1;
|
|
29
|
+
this.pitch = 1;
|
|
30
|
+
this.volume = 1;
|
|
31
|
+
this.disabled = false;
|
|
32
|
+
this.speaking = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
connectedCallback() {
|
|
36
|
+
super.connectedCallback();
|
|
37
|
+
if(typeof window === 'undefined' || !window.speechSynthesis || typeof window.SpeechSynthesisUtterance !== 'function'){
|
|
38
|
+
this.#unsupported = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
disconnectedCallback() {
|
|
43
|
+
super.disconnectedCallback();
|
|
44
|
+
this.stop();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
Utility
|
|
49
|
+
*/
|
|
50
|
+
get effectiveText() {
|
|
51
|
+
if(this.text) return this.text;
|
|
52
|
+
return this.textContent.trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
resolveVoice = () => {
|
|
56
|
+
if(!this.voice || this.#unsupported) return null;
|
|
57
|
+
const voices = window.speechSynthesis.getVoices();
|
|
58
|
+
return voices.find(v => v.name === this.voice) || null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
Public Methods
|
|
63
|
+
*/
|
|
64
|
+
speak = (override) => {
|
|
65
|
+
if(this.disabled || this.#unsupported) return;
|
|
66
|
+
const text = (override !== undefined ? String(override) : this.effectiveText).trim();
|
|
67
|
+
if(!text) return;
|
|
68
|
+
// Cancel any in-flight speech (ours or otherwise) before queuing this one
|
|
69
|
+
window.speechSynthesis.cancel();
|
|
70
|
+
const utterance = new window.SpeechSynthesisUtterance(text);
|
|
71
|
+
utterance.rate = this.rate;
|
|
72
|
+
utterance.pitch = this.pitch;
|
|
73
|
+
utterance.volume = this.volume;
|
|
74
|
+
if(this.language) utterance.lang = this.language;
|
|
75
|
+
const voice = this.resolveVoice();
|
|
76
|
+
if(voice) utterance.voice = voice;
|
|
77
|
+
utterance.onstart = this.handleStart;
|
|
78
|
+
utterance.onend = this.handleEnd;
|
|
79
|
+
utterance.onerror = this.handleError;
|
|
80
|
+
this.#utterance = utterance;
|
|
81
|
+
window.speechSynthesis.speak(utterance);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
stop = () => {
|
|
85
|
+
if(this.#unsupported) return;
|
|
86
|
+
window.speechSynthesis.cancel();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
toggle = () => {
|
|
90
|
+
if(this.speaking) this.stop();
|
|
91
|
+
else this.speak();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
Event Handlers
|
|
96
|
+
*/
|
|
97
|
+
handleStart = () => {
|
|
98
|
+
this.speaking = true;
|
|
99
|
+
this.dispatchEvent(new CustomEvent('start', { bubbles: true }));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
handleEnd = () => {
|
|
103
|
+
this.speaking = false;
|
|
104
|
+
this.#utterance = null;
|
|
105
|
+
this.dispatchEvent(new CustomEvent('end', { bubbles: true }));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
handleError = (e) => {
|
|
109
|
+
this.speaking = false;
|
|
110
|
+
this.#utterance = null;
|
|
111
|
+
this.dispatchEvent(new CustomEvent('error', {
|
|
112
|
+
detail: { error: e.error || 'unknown' },
|
|
113
|
+
bubbles: true
|
|
114
|
+
}));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
handleClick = () => {
|
|
118
|
+
if(this.disabled) return;
|
|
119
|
+
this.toggle();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
Rendering
|
|
124
|
+
*/
|
|
125
|
+
render() {
|
|
126
|
+
const supported = !this.#unsupported;
|
|
127
|
+
const label = !supported
|
|
128
|
+
? 'Text-to-speech not supported in this browser'
|
|
129
|
+
: this.speaking ? 'Stop speaking' : 'Speak';
|
|
130
|
+
return html`
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
class="no-btn btn"
|
|
134
|
+
?disabled=${this.disabled || !supported}
|
|
135
|
+
@click=${this.handleClick}
|
|
136
|
+
aria-label=${label}
|
|
137
|
+
title=${label}
|
|
138
|
+
>
|
|
139
|
+
<k-icon name=${this.speaking ? 'stop' : 'record_voice_over'}></k-icon>
|
|
140
|
+
</button>
|
|
141
|
+
<slot style="display:none"></slot>
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
Styles
|
|
147
|
+
*/
|
|
148
|
+
static styles = css`
|
|
149
|
+
:host {
|
|
150
|
+
--btn_size: 2.5rem;
|
|
151
|
+
--btn_bg: var(--c_bg);
|
|
152
|
+
--btn_bg__speaking: var(--c_primary);
|
|
153
|
+
--btn_tc__speaking: white;
|
|
154
|
+
display: inline-block;
|
|
155
|
+
}
|
|
156
|
+
:host([disabled]) {
|
|
157
|
+
opacity: 0.5;
|
|
158
|
+
pointer-events: none;
|
|
159
|
+
}
|
|
160
|
+
.btn {
|
|
161
|
+
display: inline-flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
justify-content: center;
|
|
164
|
+
width: var(--btn_size);
|
|
165
|
+
height: var(--btn_size);
|
|
166
|
+
padding: 0;
|
|
167
|
+
border: 1px solid var(--c_border);
|
|
168
|
+
border-radius: 50%;
|
|
169
|
+
background: var(--btn_bg);
|
|
170
|
+
color: var(--tc);
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
transition: background var(--animation_ms), color var(--animation_ms), border-color var(--animation_ms);
|
|
173
|
+
}
|
|
174
|
+
.btn:hover:not(:disabled) {
|
|
175
|
+
background: var(--c_bg__alt);
|
|
176
|
+
}
|
|
177
|
+
.btn:focus-visible {
|
|
178
|
+
outline: none;
|
|
179
|
+
box-shadow: var(--focus_shadow);
|
|
180
|
+
}
|
|
181
|
+
:host([speaking]) .btn,
|
|
182
|
+
:host([speaking]) .btn:hover,
|
|
183
|
+
:host([speaking]) .btn:focus-visible {
|
|
184
|
+
background: var(--btn_bg__speaking);
|
|
185
|
+
color: var(--btn_tc__speaking);
|
|
186
|
+
border-color: transparent;
|
|
187
|
+
}
|
|
188
|
+
.btn:disabled {
|
|
189
|
+
cursor: not-allowed;
|
|
190
|
+
}
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
customElements.define('k-text-to-speech', TextToSpeech);
|