@olares/lares-mobile 0.15.24 → 0.15.26

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.
@@ -1,32 +1,64 @@
1
1
  <template>
2
- <section class="lares-tts-settings">
2
+ <section class="lares-tts">
3
3
  <h3>{{ t('settings.tts.title') }}</h3>
4
- <label>{{ t('settings.tts.model') }}
5
- <select :value="view.value.config.ttsModel || ''" :disabled="state.saving" @change="save(controller.selectModel($event.target.value))">
6
- <option value="">{{ t('settings.model.auto') }}</option>
7
- <option v-for="id in models" :key="id" :value="id">{{ id }}</option>
8
- </select>
4
+ <label v-if="isDesktop" class="lares-tts__field">
5
+ <span>{{ t('settings.tts.model') }}</span>
6
+ <LaresSelect :value="modelId" :sections="modelSections" :disabled="state.saving" @select="chooseModel" />
9
7
  </label>
10
- <p v-if="view.errors.ttsModel" role="alert">{{ t('settings.saveFailed') }}</p>
11
- <p v-if="!state.model">{{ t('preferences.modelsMissing') }}</p>
12
- <p v-else-if="state.loading" role="status">{{ t('tts.loading') }}</p>
13
- <p v-else-if="state.error" role="alert">{{ t('tts.loadFailed') }} <button type="button" @click="controller.load()">{{ t('dictation.retry') }}</button></p>
14
- <label v-else-if="state.voices.length || state.unavailable">{{ t('tts.voice') }}
15
- <select :value="state.selected" :disabled="state.saving" @change="save(controller.selectVoice($event.target.value))">
16
- <option value="">{{ t('tts.default') }}</option>
17
- <option v-if="state.unavailable" :value="state.selected" disabled>{{ state.selected }} · {{ t('preferences.unavailable') }}</option>
18
- <option v-for="voice in state.voices" :key="voice.id" :value="voice.id">{{ voice.label }}</option>
19
- </select>
20
- </label>
21
- <p v-else>{{ t('tts.noPresets') }}</p>
22
- <p v-if="view.errors.ttsVoiceByModel" role="alert">{{ t('settings.saveFailed') }}</p>
23
- <div class="lares-tts-settings__preview">
24
- <button type="button" :disabled="!state.model || state.loading || Boolean(state.error) || state.saving || state.unavailable || state.blocked" @click="controller.audition()">{{ t(state.playing ? 'tts.stopPreview' : 'tts.preview') }}</button>
25
- <p role="status">{{ t(state.blocked ? 'tts.busy' : 'tts.previewHint') }}</p>
8
+ <LaresSettingRow
9
+ v-else
10
+ :label="t('settings.tts.model')"
11
+ :value="modelLabel"
12
+ chevron
13
+ boxed
14
+ :disabled="state.saving"
15
+ @click="sheet = 'model'"
16
+ />
17
+ <p v-if="view.errors.ttsModel" class="lares-tts__alert" role="alert">{{ t('settings.saveFailed') }}</p>
18
+ <p v-if="!state.model" class="lares-tts__hint">{{ t('preferences.modelsMissing') }}</p>
19
+ <p v-else-if="state.loading" class="lares-tts__hint" role="status">{{ t('tts.loading') }}</p>
20
+ <div v-else-if="state.error" class="lares-tts__status" role="alert">
21
+ <p>{{ t('tts.loadFailed') }}</p>
22
+ <button type="button" @click="controller.load()">{{ t('dictation.retry') }}</button>
23
+ </div>
24
+ <template v-else-if="state.voices.length || state.unavailable">
25
+ <label v-if="isDesktop" class="lares-tts__field">
26
+ <span>{{ t('tts.voice') }}</span>
27
+ <LaresSelect :value="voiceId" :sections="voiceSections" :disabled="state.saving" @select="chooseVoice" />
28
+ </label>
29
+ <LaresSettingRow
30
+ v-else
31
+ :label="t('tts.voice')"
32
+ :value="voiceLabel"
33
+ chevron
34
+ boxed
35
+ :disabled="state.saving"
36
+ @click="sheet = 'voice'"
37
+ />
38
+ </template>
39
+ <p v-else class="lares-tts__hint">{{ t('tts.noPresets') }}</p>
40
+ <p v-if="view.errors.ttsVoiceByModel" class="lares-tts__alert" role="alert">{{ t('settings.saveFailed') }}</p>
41
+ <div class="lares-tts__action">
42
+ <p class="lares-tts__hint">{{ t(state.blocked ? 'tts.busy' : 'tts.previewHint') }}</p>
43
+ <button type="button" :disabled="previewDisabled" @click="controller.audition()">{{ t(state.playing ? 'tts.stopPreview' : 'tts.preview') }}</button>
26
44
  </div>
27
- <p v-if="state.previewError" role="alert">{{ t('tts.previewFailed') }}</p>
28
- <label class="lares-tts-settings__speak"><input type="checkbox" :checked="view.value.config.speak === true" :disabled="Boolean(view.pending.speak)" @change="save(preferences.patch({ speak: $event.target.checked }))" />{{ t('settings.speak.title') }}</label>
29
- <p v-if="view.errors.speak" role="alert">{{ t('settings.saveFailed') }}</p>
45
+ <p v-if="state.previewError" class="lares-tts__alert" role="alert">{{ t('tts.previewFailed') }}</p>
46
+ <label class="lares-tts__switch">
47
+ <span>{{ t('settings.speak.title') }}</span>
48
+ <input type="checkbox" role="switch" :checked="view.value.config.speak === true" :disabled="Boolean(view.pending.speak)" :aria-checked="view.value.config.speak === true ? 'true' : 'false'" @change="save(preferences.patch({ speak: $event.target.checked }))" />
49
+ </label>
50
+ <p class="lares-tts__hint">{{ t('settings.tts.hint') }}</p>
51
+ <p v-if="view.errors.speak" class="lares-tts__alert" role="alert">{{ t('settings.saveFailed') }}</p>
52
+ <LaresSheet v-if="!isDesktop" :open="Boolean(sheet)" :title="sheetTitle" @close="sheet = ''">
53
+ <LaresSettingRow
54
+ v-for="item in sheetItems"
55
+ :key="item.id || 'auto'"
56
+ :label="item.label"
57
+ :checked="item.id === sheetCurrent"
58
+ :disabled="state.saving || item.disabled"
59
+ @click="chooseSheet(item)"
60
+ />
61
+ </LaresSheet>
30
62
  </section>
31
63
  </template>
32
64
  <script>
@@ -34,27 +66,216 @@ import { markRaw } from "vue";
34
66
  import { createTtsSettings } from "@olares/lares-core/voice/tts-settings";
35
67
  import { fetchTtsVoices } from "@olares/lares-core/voice/tts-voice";
36
68
  import { previewTts } from "@olares/lares-core/voice/tts-preview";
69
+ import LaresSettingRow from "./SettingRow.vue";
70
+ import LaresSelect from "./Select.vue";
71
+ import LaresSheet from "./Sheet.vue";
37
72
  export default {
38
- props: { preferences: { type: Object, required: true }, view: { type: Object, required: true }, t: { type: Function, required: true }, fetcher: { type: Function, required: true } },
73
+ components: { LaresSettingRow, LaresSelect, LaresSheet },
74
+ props: { preferences: { type: Object, required: true }, view: { type: Object, required: true }, t: { type: Function, required: true }, fetcher: { type: Function, required: true }, device: { type: String, default: "mobile" } },
39
75
  data() {
40
76
  const controller = markRaw(createTtsSettings({ preferences: this.preferences, list: (model, options) => fetchTtsVoices(model, { ...options, fetcher: this.fetcher }), preview: take => previewTts(take, { fetcher: this.fetcher }) }));
41
- return { controller, state: controller.getSnapshot() };
77
+ return { controller, state: controller.getSnapshot(), sheet: "" };
78
+ },
79
+ computed: {
80
+ isDesktop() { return this.device === "desktop"; },
81
+ models() { return [...new Set([this.view.value.config.ttsModel, ...(this.view.value.ttsModels || [])].filter(Boolean))]; },
82
+ modelId() { return this.view.value.config.ttsModel || ""; },
83
+ modelLabel() { return this.modelId ? this.modelId : this.t("settings.model.auto"); },
84
+ modelSections() { return [{ items: [{ id: "", label: this.t("settings.model.auto") }, ...this.models.map(id => ({ id, label: id }))] }]; },
85
+ voiceId() { return this.state.selected || ""; },
86
+ voiceItems() {
87
+ return [
88
+ { id: "", label: this.t("tts.default") },
89
+ ...(this.state.unavailable && this.state.selected ? [{ id: this.state.selected, label: `${this.state.selected} · ${this.t("preferences.unavailable")}`, disabled: true }] : []),
90
+ ...this.state.voices.map(voice => ({ id: voice.id, label: voice.label })),
91
+ ];
92
+ },
93
+ voiceSections() { return [{ items: this.voiceItems }]; },
94
+ voiceLabel() { return this.voiceItems.find(item => item.id === this.voiceId)?.label || this.t("tts.default"); },
95
+ previewDisabled() { return !this.state.model || this.state.loading || Boolean(this.state.error) || this.state.saving || this.state.unavailable || this.state.blocked; },
96
+ sheetTitle() { return this.t(this.sheet === "voice" ? "tts.voice" : "settings.tts.model"); },
97
+ sheetItems() { return this.sheet === "voice" ? this.voiceItems : this.modelSections[0].items; },
98
+ sheetCurrent() { return this.sheet === "voice" ? this.voiceId : this.modelId; },
42
99
  },
43
- computed: { models() { return [...new Set([this.view.value.config.ttsModel, ...(this.view.value.ttsModels || [])].filter(Boolean))]; } },
44
100
  mounted() { this.off = this.controller.subscribe(() => { this.state = this.controller.getSnapshot(); }); void this.controller.load(); },
45
101
  beforeUnmount() { this.off?.(); this.controller.dispose(); },
46
- methods: { save(operation) { void operation.catch(() => {}); } },
102
+ methods: {
103
+ save(operation) { void operation.catch(() => {}); },
104
+ chooseModel(item) { this.save(this.controller.selectModel(item.id)); },
105
+ chooseVoice(item) { if (!item.disabled) this.save(this.controller.selectVoice(item.id)); },
106
+ chooseSheet(item) {
107
+ if (item.disabled || this.state.saving) return;
108
+ const sheet = this.sheet;
109
+ this.sheet = "";
110
+ if (item.id === (sheet === "voice" ? this.voiceId : this.modelId)) return;
111
+ if (sheet === "voice") this.chooseVoice(item);
112
+ else this.chooseModel(item);
113
+ },
114
+ },
47
115
  };
48
116
  </script>
49
117
  <style scoped>
50
- .lares-tts-settings { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
51
- .lares-tts-settings label { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
52
- .lares-tts-settings select { width: 100%; min-width: 0; max-width: 100%; font: inherit; color: inherit; border: 1px solid var(--q-separator); border-radius: 8px; background: var(--q-background-2, transparent); padding: 10px; }
53
- .lares-tts-settings button:disabled { opacity: .45; cursor: default; }
54
- .lares-tts-settings button:focus-visible, .lares-tts-settings select:focus-visible { outline: 2px solid var(--q-blue-default, #3978f6); outline-offset: 2px; }
55
- .lares-tts-settings button { font: inherit; color: inherit; border: 1px solid var(--q-separator); border-radius: 8px; background: transparent; padding: 8px 14px; }
56
- .lares-tts-settings p { margin: 0; font-size: 13px; color: var(--q-ink-3); }
57
- .lares-tts-settings [role="alert"] { color: var(--q-orange-default); }
58
- .lares-tts-settings label.lares-tts-settings__speak { flex-direction: row; align-items: center; }
59
- .lares-tts-settings__preview { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
118
+ .lares-tts {
119
+ display: flex;
120
+ flex-direction: column;
121
+ gap: 12px;
122
+ min-width: 0;
123
+ color: var(--q-ink-1);
124
+ font-size: 15px;
125
+ line-height: 22px;
126
+ }
127
+
128
+ .lares-tts h3 {
129
+ margin: 4px 0 0;
130
+ color: var(--q-ink-3);
131
+ font-size: 14px;
132
+ font-weight: 500;
133
+ line-height: 18px;
134
+ }
135
+
136
+ .lares-tts__field {
137
+ display: flex;
138
+ flex-direction: column;
139
+ gap: 8px;
140
+ min-width: 0;
141
+ }
142
+
143
+ .lares-tts__hint {
144
+ margin: 0;
145
+ color: var(--q-ink-3);
146
+ font-size: 15px;
147
+ line-height: 22px;
148
+ }
149
+
150
+ .lares-tts__alert {
151
+ margin: 0;
152
+ color: var(--q-orange-default);
153
+ font-size: 15px;
154
+ line-height: 22px;
155
+ }
156
+
157
+ .lares-tts__status {
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 12px;
161
+ min-height: 44px;
162
+ }
163
+
164
+ .lares-tts__status p {
165
+ flex: 1;
166
+ min-width: 0;
167
+ margin: 0;
168
+ color: var(--q-orange-default);
169
+ font-size: 15px;
170
+ line-height: 22px;
171
+ }
172
+
173
+ .lares-tts__status > button,
174
+ .lares-tts__action > button {
175
+ flex: none;
176
+ min-height: 36px;
177
+ border: 0;
178
+ border-radius: 12px;
179
+ padding: 0 14px;
180
+ background: var(--q-blue-alpha);
181
+ color: var(--q-blue-default);
182
+ font: inherit;
183
+ font-size: 15px;
184
+ font-weight: 500;
185
+ line-height: 20px;
186
+ cursor: pointer;
187
+ }
188
+
189
+ .lares-tts__status > button:disabled,
190
+ .lares-tts__action > button:disabled {
191
+ cursor: default;
192
+ opacity: 0.45;
193
+ }
194
+
195
+ .lares-tts__status > button:focus-visible,
196
+ .lares-tts__action > button:focus-visible {
197
+ outline: 2px solid var(--q-blue-default);
198
+ outline-offset: 2px;
199
+ }
200
+
201
+ .lares-tts__action,
202
+ .lares-tts__switch {
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: space-between;
206
+ gap: 12px;
207
+ min-height: 64px;
208
+ box-sizing: border-box;
209
+ border: 1px solid var(--q-separator);
210
+ border-radius: 20px;
211
+ padding: 12px 16px;
212
+ }
213
+
214
+ .lares-tts__action .lares-tts__hint {
215
+ flex: 1;
216
+ min-width: 0;
217
+ font-size: 15px;
218
+ line-height: 22px;
219
+ }
220
+
221
+ .lares-tts__switch > span {
222
+ flex: 1;
223
+ min-width: 0;
224
+ font-size: 16px;
225
+ line-height: 22px;
226
+ }
227
+
228
+ .lares-tts__switch {
229
+ cursor: pointer;
230
+ }
231
+
232
+ .lares-tts__switch input {
233
+ position: relative;
234
+ flex: none;
235
+ width: 44px;
236
+ height: 26px;
237
+ margin: 0;
238
+ border: 0;
239
+ border-radius: 999px;
240
+ background: var(--q-background-3);
241
+ appearance: none;
242
+ cursor: pointer;
243
+ }
244
+
245
+ .lares-tts__switch input::after {
246
+ content: "";
247
+ position: absolute;
248
+ top: 2px;
249
+ left: 2px;
250
+ width: 22px;
251
+ height: 22px;
252
+ border-radius: 50%;
253
+ background: var(--q-ink-1);
254
+ transition: transform var(--lares-duration-fast, 160ms) var(--lares-ease-out, ease);
255
+ }
256
+
257
+ .lares-tts__switch input:checked {
258
+ background: var(--q-blue-default);
259
+ }
260
+
261
+ .lares-tts__switch input:checked::after {
262
+ background: var(--q-ink-on-brand);
263
+ transform: translateX(18px);
264
+ }
265
+
266
+ .lares-tts__switch input:disabled {
267
+ cursor: default;
268
+ opacity: 0.45;
269
+ }
270
+
271
+ .lares-tts__switch input:focus-visible {
272
+ outline: 2px solid var(--q-blue-default);
273
+ outline-offset: 2px;
274
+ }
275
+
276
+ @media (prefers-reduced-motion: reduce) {
277
+ .lares-tts__switch input::after {
278
+ transition: none;
279
+ }
280
+ }
60
281
  </style>
@@ -0,0 +1,267 @@
1
+ <template>
2
+ <section class="lares-voice-keys">
3
+ <h3>{{ t('settings.group.keys') }}</h3>
4
+ <p>{{ t('settings.keys.hint') }}</p>
5
+ <ul v-if="keys && keys.length" class="lares-voice-keys__list">
6
+ <li v-for="key in keys" :key="key.id">
7
+ <span class="lares-voice-keys__name">{{ key.name }}<small>{{ key.models.join(', ') }}</small></span>
8
+ <button type="button" :disabled="busy" @click="revoking = key">{{ t('settings.keys.revoke') }}</button>
9
+ </li>
10
+ </ul>
11
+ <p v-else-if="keys">{{ t('settings.keys.empty') }}</p>
12
+ <label class="lares-voice-keys__field">
13
+ <span>{{ t('settings.keys.name') }}</span>
14
+ <input v-model="name" type="text" maxlength="64" :placeholder="t('settings.keys.namePlaceholder')" :disabled="busy" />
15
+ </label>
16
+ <div class="lares-voice-keys__models" role="group" :aria-label="t('settings.keys.models')">
17
+ <p>{{ t('settings.keys.models') }}</p>
18
+ <label v-for="model in allModels" :key="model">
19
+ <input type="checkbox" :checked="models.includes(model)" :disabled="busy" @change="toggle(model)" />
20
+ <span>{{ model }}</span>
21
+ </label>
22
+ </div>
23
+ <p v-if="agentSelected" role="note">{{ t('settings.keys.agentWarning') }}</p>
24
+ <button type="button" class="lares-voice-keys__issue" :disabled="busy || !name.trim() || !models.length" @click="issue">{{ t(busy ? 'settings.keys.issuing' : 'settings.keys.issue') }}</button>
25
+ <p v-if="error" role="alert">{{ error }}</p>
26
+ <div v-if="issued" class="lares-voice-keys__dialog" role="dialog">
27
+ <p>{{ t('settings.keys.issued') }}</p>
28
+ <code>{{ issued.key }}</code>
29
+ <div class="lares-voice-keys__actions">
30
+ <button type="button" @click="copy">{{ t(copied ? 'settings.keys.copied' : 'settings.keys.copy') }}</button>
31
+ <button type="button" class="is-primary" @click="issued = null">{{ t('settings.keys.done') }}</button>
32
+ </div>
33
+ </div>
34
+ <div v-if="revoking" class="lares-voice-keys__dialog" role="dialog">
35
+ <p>{{ t('settings.keys.revokeConfirm', { name: revoking.name }) }}</p>
36
+ <div class="lares-voice-keys__actions">
37
+ <button type="button" :disabled="busy" @click="revoke">{{ t('settings.keys.revoke') }}</button>
38
+ <button type="button" @click="revoking = null">{{ t('settings.keys.done') }}</button>
39
+ </div>
40
+ </div>
41
+ </section>
42
+ </template>
43
+ <script>
44
+ import { markRaw } from "vue";
45
+ import { messageFromCode } from "@olares/lares-core/i18n/t";
46
+ import { VOICE_KEY_AGENT_MODELS, VOICE_KEY_DEFAULT_MODELS, VOICE_KEY_MODELS } from "@olares/lares-core/voice/keys-client";
47
+ export default {
48
+ props: { client: { type: Object, required: true }, t: { type: Function, required: true } },
49
+ data() {
50
+ return { keys: null, name: "", models: [...VOICE_KEY_DEFAULT_MODELS], issued: null, copied: false, revoking: null, busy: false, error: "", allModels: markRaw([...VOICE_KEY_MODELS]) };
51
+ },
52
+ computed: { agentSelected() { return this.models.some((model) => VOICE_KEY_AGENT_MODELS.includes(model)); } },
53
+ mounted() { void this.load(); },
54
+ methods: {
55
+ fail(err) { this.error = messageFromCode(this.t, err?.message || "", "error.voice_key_failed"); },
56
+ async load() { try { this.keys = await this.client.list(); } catch (err) { this.fail(err); } },
57
+ toggle(model) { this.models = this.models.includes(model) ? this.models.filter((value) => value !== model) : VOICE_KEY_MODELS.filter((value) => value === model || this.models.includes(value)); },
58
+ async issue() {
59
+ this.busy = true; this.error = "";
60
+ try { this.issued = await this.client.issue({ name: this.name, models: this.models }); this.copied = false; this.name = ""; this.models = [...VOICE_KEY_DEFAULT_MODELS]; await this.load(); }
61
+ catch (err) { this.fail(err); }
62
+ finally { this.busy = false; }
63
+ },
64
+ async revoke() {
65
+ this.busy = true; this.error = "";
66
+ try { await this.client.revoke(this.revoking.id); this.revoking = null; await this.load(); }
67
+ catch (err) { this.fail(err); }
68
+ finally { this.busy = false; }
69
+ },
70
+ async copy() { try { await navigator.clipboard.writeText(this.issued.key); this.copied = true; } catch (err) { this.fail(err); } },
71
+ },
72
+ };
73
+ </script>
74
+ <style scoped>
75
+ .lares-voice-keys {
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 12px;
79
+ min-width: 0;
80
+ margin-top: 8px;
81
+ color: var(--q-ink-1);
82
+ font-size: 15px;
83
+ line-height: 22px;
84
+ }
85
+
86
+ .lares-voice-keys h3 {
87
+ margin: 4px 0 0;
88
+ color: var(--q-ink-3);
89
+ font-size: 14px;
90
+ font-weight: 500;
91
+ line-height: 18px;
92
+ }
93
+
94
+ .lares-voice-keys p {
95
+ margin: 0;
96
+ color: var(--q-ink-3);
97
+ font-size: 15px;
98
+ line-height: 22px;
99
+ }
100
+
101
+ .lares-voice-keys [role="alert"],
102
+ .lares-voice-keys [role="note"] {
103
+ color: var(--q-orange-default);
104
+ }
105
+
106
+ .lares-voice-keys__list {
107
+ display: flex;
108
+ flex-direction: column;
109
+ gap: 12px;
110
+ margin: 0;
111
+ padding: 0;
112
+ list-style: none;
113
+ }
114
+
115
+ .lares-voice-keys__list li,
116
+ .lares-voice-keys__dialog {
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: space-between;
120
+ gap: 12px;
121
+ min-height: 64px;
122
+ box-sizing: border-box;
123
+ border: 1px solid var(--q-separator);
124
+ border-radius: 20px;
125
+ padding: 12px 16px;
126
+ }
127
+
128
+ .lares-voice-keys__name {
129
+ min-width: 0;
130
+ flex: 1;
131
+ font-size: 16px;
132
+ line-height: 22px;
133
+ }
134
+
135
+ .lares-voice-keys__name small {
136
+ display: block;
137
+ overflow: hidden;
138
+ color: var(--q-ink-3);
139
+ font-size: 13px;
140
+ line-height: 18px;
141
+ text-overflow: ellipsis;
142
+ }
143
+
144
+ .lares-voice-keys__field {
145
+ display: flex;
146
+ flex-direction: column;
147
+ gap: 8px;
148
+ }
149
+
150
+ .lares-voice-keys__field span,
151
+ .lares-voice-keys__models > p {
152
+ color: var(--q-ink-2);
153
+ }
154
+
155
+ .lares-voice-keys input[type="text"] {
156
+ box-sizing: border-box;
157
+ width: 100%;
158
+ min-height: 48px;
159
+ border: 1px solid var(--q-separator);
160
+ border-radius: 16px;
161
+ padding: 0 16px;
162
+ background: transparent;
163
+ color: inherit;
164
+ font: inherit;
165
+ font-size: 16px;
166
+ }
167
+
168
+ .lares-voice-keys input[type="text"]:focus-visible {
169
+ outline: 2px solid var(--q-blue-default);
170
+ outline-offset: 2px;
171
+ }
172
+
173
+ .lares-voice-keys__models {
174
+ overflow: hidden;
175
+ border: 1px solid var(--q-separator);
176
+ border-radius: 20px;
177
+ }
178
+
179
+ .lares-voice-keys__models > p {
180
+ margin: 0;
181
+ padding: 12px 16px 4px;
182
+ font-size: 14px;
183
+ font-weight: 500;
184
+ line-height: 18px;
185
+ }
186
+
187
+ .lares-voice-keys__models label {
188
+ display: flex;
189
+ align-items: center;
190
+ gap: 12px;
191
+ min-height: 48px;
192
+ padding: 0 16px;
193
+ border-top: 1px solid var(--q-separator);
194
+ font-size: 16px;
195
+ line-height: 22px;
196
+ }
197
+
198
+ .lares-voice-keys__models input {
199
+ width: 20px;
200
+ height: 20px;
201
+ margin: 0;
202
+ accent-color: var(--q-blue-default);
203
+ }
204
+
205
+ .lares-voice-keys button {
206
+ flex: none;
207
+ min-height: 36px;
208
+ border: 1px solid var(--q-separator);
209
+ border-radius: 12px;
210
+ padding: 0 14px;
211
+ background: transparent;
212
+ color: inherit;
213
+ font: inherit;
214
+ font-size: 15px;
215
+ cursor: pointer;
216
+ }
217
+
218
+ .lares-voice-keys button:disabled {
219
+ cursor: default;
220
+ opacity: 0.45;
221
+ }
222
+
223
+ .lares-voice-keys button:focus-visible {
224
+ outline: 2px solid var(--q-blue-default);
225
+ outline-offset: 2px;
226
+ }
227
+
228
+ .lares-voice-keys__issue,
229
+ .lares-voice-keys button.is-primary {
230
+ border: 0;
231
+ background: var(--q-blue-alpha);
232
+ color: var(--q-blue-default);
233
+ font-weight: 500;
234
+ }
235
+
236
+ .lares-voice-keys__issue {
237
+ min-height: 48px;
238
+ border-radius: 16px;
239
+ font-size: 16px;
240
+ }
241
+
242
+ .lares-voice-keys__dialog {
243
+ flex-direction: column;
244
+ align-items: stretch;
245
+ }
246
+
247
+ .lares-voice-keys__dialog p {
248
+ color: var(--q-ink-1);
249
+ }
250
+
251
+ .lares-voice-keys code {
252
+ overflow-wrap: anywhere;
253
+ border-radius: 12px;
254
+ padding: 12px;
255
+ background: var(--q-background-3);
256
+ font-size: 13px;
257
+ line-height: 18px;
258
+ user-select: all;
259
+ }
260
+
261
+ .lares-voice-keys__actions {
262
+ display: flex;
263
+ flex-wrap: wrap;
264
+ gap: 8px;
265
+ justify-content: flex-end;
266
+ }
267
+ </style>