@olares/lares-mobile 0.15.0

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.
@@ -0,0 +1,449 @@
1
+ <template>
2
+ <div class="lares-agent" :aria-busy="loading ? 'true' : 'false'">
3
+ <div v-if="loading" class="lares-agent__loading" role="status">
4
+ <span class="lares-agent__spinner" aria-hidden="true" />
5
+ <p>{{ t("agent.loading") }}</p>
6
+ </div>
7
+ <div class="lares-agent__body" :inert="loading || undefined">
8
+ <section class="lares-agent__group">
9
+ <h2>{{ tModel("settings.title") }}</h2>
10
+ <div class="lares-agent__list">
11
+ <LaresSettingRow
12
+ :label="tModel('settings.default')"
13
+ :value="modelLabel"
14
+ chevron
15
+ boxed
16
+ :disabled="Boolean(pending)"
17
+ @click="openSheet('model')"
18
+ />
19
+ </div>
20
+ <p v-for="entry in modelFailures" :key="entry.provider" class="lares-agent__hint" data-status="error">
21
+ {{ tModel("settings.providerFailed", { name: entry.name || entry.provider, msg: entry.message }) }}
22
+ </p>
23
+ <p v-if="modelError" class="lares-agent__hint" data-status="error">{{ modelError }}</p>
24
+ </section>
25
+ <section class="lares-agent__group">
26
+ <h2>{{ tVoice("settings.title") }}</h2>
27
+ <div class="lares-agent__list">
28
+ <LaresSettingRow
29
+ :label="tVoice('settings.model.title')"
30
+ :value="voiceModelLabel"
31
+ chevron
32
+ boxed
33
+ :disabled="Boolean(pending)"
34
+ @click="openSheet('voiceModel')"
35
+ />
36
+ <LaresSettingRow
37
+ :label="tVoice('settings.language.title')"
38
+ :value="voiceLangLabel"
39
+ chevron
40
+ boxed
41
+ :disabled="Boolean(pending)"
42
+ @click="openSheet('voiceLang')"
43
+ />
44
+ </div>
45
+ <p v-if="voiceError" class="lares-agent__hint" data-status="error">{{ voiceError }}</p>
46
+ </section>
47
+ <section class="lares-agent__group">
48
+ <h2>{{ tSearch("settings.title") }}</h2>
49
+ <div class="lares-agent__list">
50
+ <LaresSettingRow
51
+ :label="tSearch('settings.default')"
52
+ :value="searchLabel"
53
+ chevron
54
+ boxed
55
+ :disabled="Boolean(pending)"
56
+ @click="openSheet('search')"
57
+ />
58
+ </div>
59
+ <p v-if="searchError" class="lares-agent__hint" data-status="error">{{ searchError }}</p>
60
+ </section>
61
+ <LaresSheet :open="Boolean(panel)" :title="sheetTitle" @close="panel = ''">
62
+ <p v-if="sheetEmpty" class="lares-agent__hint lares-agent__hint--sheet">{{ sheetEmpty }}</p>
63
+ <template v-for="section in sheetSections" :key="section.heading || 'main'">
64
+ <p v-if="section.heading" class="lares-agent__heading">{{ section.heading }}</p>
65
+ <LaresSettingRow
66
+ v-for="item in section.items"
67
+ :key="item.id"
68
+ :label="item.label"
69
+ :checked="item.id === sheetCurrent"
70
+ :disabled="busy"
71
+ @click="pick(item)"
72
+ />
73
+ </template>
74
+ </LaresSheet>
75
+ </div>
76
+ </div>
77
+ </template>
78
+
79
+ <script>
80
+ import { t as translate } from "@olares/lares-core/i18n/t";
81
+ import { EN as MODEL_EN, ZH as MODEL_ZH } from "@olares/lares-core/i18n/chat-model";
82
+ import { EN as VOICE_EN, ZH as VOICE_ZH } from "@olares/lares-core/i18n/voice";
83
+ import { EN as SEARCH_EN, ZH as SEARCH_ZH } from "@olares/lares-core/i18n/search";
84
+ import { groupModelsByProvider, selectionKey } from "@olares/lares-core/router/session-model";
85
+ import {
86
+ voiceLanguageItems,
87
+ voiceMenuValue,
88
+ voiceModelItems,
89
+ voiceValueFromMenu,
90
+ } from "@olares/lares-core/voice/languages";
91
+ import {
92
+ searchMenuValue,
93
+ searchSelectorItems,
94
+ searchValueFromMenu,
95
+ } from "@olares/lares-core/search/menu";
96
+ import { rememberedSettings } from "@olares/lares-core/larepass/settings";
97
+ import { createHostClient } from "../host.js";
98
+ import { createT } from "../i18n.js";
99
+ import LaresSettingRow from "./SettingRow.vue";
100
+ import LaresSheet from "./Sheet.vue";
101
+
102
+ const catalogs = {
103
+ model: { zh: MODEL_ZH, en: MODEL_EN },
104
+ voice: { zh: VOICE_ZH, en: VOICE_EN },
105
+ search: { zh: SEARCH_ZH, en: SEARCH_EN },
106
+ };
107
+
108
+ function itemLabel(items, id) {
109
+ return items.find((item) => item.id === id)?.label || "—";
110
+ }
111
+
112
+ export default {
113
+ name: "LaresAgentSettings",
114
+ components: { LaresSettingRow, LaresSheet },
115
+ expose: ["reload"],
116
+ props: {
117
+ locale: { type: String, default: "en" },
118
+ baseUrl: { type: String, default: undefined },
119
+ proxyPrefix: { type: String, default: undefined },
120
+ request: { type: Function, default: undefined },
121
+ env: { type: Object, default: undefined },
122
+ },
123
+ data() {
124
+ const remembered = rememberedSettings();
125
+ return {
126
+ loading: !(remembered.models && remembered.voice && remembered.search),
127
+ pending: "",
128
+ panel: "",
129
+ models: remembered.models,
130
+ voice: remembered.voice,
131
+ search: remembered.search,
132
+ modelError: "",
133
+ voiceError: "",
134
+ searchError: "",
135
+ };
136
+ },
137
+ computed: {
138
+ t() {
139
+ return createT(this.locale);
140
+ },
141
+ settings() {
142
+ return createHostClient({
143
+ baseUrl: this.baseUrl,
144
+ proxyPrefix: this.proxyPrefix,
145
+ request: this.request,
146
+ env: this.env,
147
+ }).settings;
148
+ },
149
+ busy() {
150
+ return this.loading || Boolean(this.pending);
151
+ },
152
+ modelGroups() {
153
+ return groupModelsByProvider(this.models?.models ?? []);
154
+ },
155
+ modelCurrent() {
156
+ return this.models?.default ? selectionKey(this.models.default) : "";
157
+ },
158
+ modelFailures() {
159
+ return this.models?.failures ?? [];
160
+ },
161
+ voiceModels() {
162
+ return voiceModelItems(this.voice?.sttModels ?? [], this.tVoice("settings.model.auto"));
163
+ },
164
+ voiceLanguages() {
165
+ return voiceLanguageItems(this.tVoice("lang.auto"));
166
+ },
167
+ searchModels() {
168
+ return Array.isArray(this.search?.searchModels) ? this.search.searchModels : [];
169
+ },
170
+ searchItems() {
171
+ return searchSelectorItems(this.searchModels, {
172
+ none: this.tSearch("settings.default.none"),
173
+ empty: this.tSearch("settings.default.empty"),
174
+ });
175
+ },
176
+ modelLabel() {
177
+ if (!this.models) return "—";
178
+ for (const group of this.modelGroups) {
179
+ for (const model of group.models) {
180
+ if (this.modelKey(model) === this.modelCurrent) return model.name;
181
+ }
182
+ }
183
+ return this.models.default?.model || "—";
184
+ },
185
+ voiceModelLabel() {
186
+ if (!this.voice) return "—";
187
+ return itemLabel(this.voiceModels, voiceMenuValue(this.voice.config?.model));
188
+ },
189
+ voiceLangLabel() {
190
+ if (!this.voice) return "—";
191
+ return itemLabel(this.voiceLanguages, voiceMenuValue(this.voice.config?.language));
192
+ },
193
+ searchLabel() {
194
+ if (!this.search) return "—";
195
+ return itemLabel(this.searchItems, searchMenuValue(this.search.defaultSearchModel));
196
+ },
197
+ sheetTitle() {
198
+ if (this.panel === "model") return this.tModel("settings.title");
199
+ if (this.panel === "voiceModel") return this.tVoice("settings.model.title");
200
+ if (this.panel === "voiceLang") return this.tVoice("settings.language.title");
201
+ if (this.panel === "search") return this.tSearch("settings.default");
202
+ return "";
203
+ },
204
+ sheetCurrent() {
205
+ if (this.panel === "model") return this.modelCurrent;
206
+ if (this.panel === "voiceModel") return voiceMenuValue(this.voice?.config?.model);
207
+ if (this.panel === "voiceLang") return voiceMenuValue(this.voice?.config?.language);
208
+ if (this.panel === "search") return searchMenuValue(this.search?.defaultSearchModel);
209
+ return "";
210
+ },
211
+ sheetSections() {
212
+ if (this.panel === "model") {
213
+ return this.modelGroups.map((group) => ({
214
+ heading: group.provider,
215
+ items: group.models.map((model) => ({
216
+ id: this.modelKey(model),
217
+ label: model.name,
218
+ model,
219
+ })),
220
+ }));
221
+ }
222
+ if (this.panel === "voiceModel") {
223
+ return [{ items: this.voiceModels.map((item) => ({ id: item.id, label: item.label })) }];
224
+ }
225
+ if (this.panel === "voiceLang") {
226
+ return [{ items: this.voiceLanguages.map((item) => ({ id: item.id, label: item.label })) }];
227
+ }
228
+ if (this.panel === "search") {
229
+ return [{ items: this.searchItems.map((item) => ({ id: item.id, label: item.label })) }];
230
+ }
231
+ return [];
232
+ },
233
+ sheetEmpty() {
234
+ const count = this.sheetSections.reduce((sum, section) => sum + section.items.length, 0);
235
+ if (count > 0) return "";
236
+ if (this.panel === "model") return this.tModel("settings.empty");
237
+ if (this.panel === "search") return this.tSearch("settings.default.empty");
238
+ return "";
239
+ },
240
+ },
241
+ mounted() {
242
+ this.load(false);
243
+ },
244
+ methods: {
245
+ tModel(key, params) {
246
+ return translate(catalogs.model, this.locale, key, params);
247
+ },
248
+ tVoice(key, params) {
249
+ return translate(catalogs.voice, this.locale, key, params);
250
+ },
251
+ tSearch(key, params) {
252
+ return translate(catalogs.search, this.locale, key, params);
253
+ },
254
+ modelKey(model) {
255
+ return selectionKey({ provider: model.provider, model: model.id });
256
+ },
257
+ failText(catalogT, key, err) {
258
+ return catalogT(key, { msg: err instanceof Error ? err.message : String(err) });
259
+ },
260
+ openSheet(panel) {
261
+ if (this.loading) return;
262
+ const ready = panel === "model" ? this.models : panel === "search" ? this.search : this.voice;
263
+ if (!ready) return;
264
+ this.panel = panel;
265
+ },
266
+ async pick(item) {
267
+ const panel = this.panel;
268
+ if (item.id === this.sheetCurrent) {
269
+ this.panel = "";
270
+ return;
271
+ }
272
+ this.panel = "";
273
+ if (panel === "model") await this.chooseModel(item.model);
274
+ if (panel === "voiceModel") await this.patchVoice("model", voiceValueFromMenu(item.id));
275
+ if (panel === "voiceLang") await this.patchVoice("language", voiceValueFromMenu(item.id));
276
+ if (panel === "search") await this.chooseSearch(item.id);
277
+ },
278
+ reload() {
279
+ return this.load(true);
280
+ },
281
+ async load(force) {
282
+ const remembered = rememberedSettings();
283
+ if (!force) {
284
+ this.models = remembered.models ?? this.models;
285
+ this.voice = remembered.voice ?? this.voice;
286
+ this.search = remembered.search ?? this.search;
287
+ if (this.models && this.voice && this.search) return;
288
+ }
289
+ this.panel = "";
290
+ this.loading = true;
291
+ this.modelError = "";
292
+ this.voiceError = "";
293
+ this.searchError = "";
294
+ const [models, voice, search] = await Promise.allSettled([
295
+ this.settings.models({ force }),
296
+ this.settings.voice(force),
297
+ this.settings.search({ force }),
298
+ ]);
299
+ if (models.status === "fulfilled") this.models = models.value;
300
+ else this.modelError = this.failText(this.tModel, "settings.loadFailed", models.reason);
301
+ if (voice.status === "fulfilled") this.voice = voice.value;
302
+ else this.voiceError = this.tVoice("settings.loadFailed");
303
+ if (search.status === "fulfilled") this.search = search.value;
304
+ else this.searchError = this.failText(this.tSearch, "settings.loadFailed", search.reason);
305
+ this.loading = false;
306
+ },
307
+ async chooseModel(model) {
308
+ const selection = { provider: model.provider, model: model.id };
309
+ const key = selectionKey(selection);
310
+ if (key === this.modelCurrent || this.busy) return;
311
+ this.pending = key;
312
+ this.modelError = "";
313
+ try {
314
+ this.models = await this.settings.setDefaultModel(selection);
315
+ } catch (err) {
316
+ this.modelError = this.failText(this.tModel, "settings.saveFailed", err);
317
+ } finally {
318
+ this.pending = "";
319
+ }
320
+ },
321
+ async patchVoice(key, value) {
322
+ if (!this.voice || this.busy) return;
323
+ const previous = this.voice;
324
+ this.voice = { ...this.voice, config: { ...this.voice.config, [key]: value } };
325
+ this.pending = `voice:${key}`;
326
+ this.voiceError = "";
327
+ try {
328
+ this.voice = await this.settings.saveVoice({ [key]: value });
329
+ } catch {
330
+ this.voice = previous;
331
+ this.voiceError = this.tVoice("settings.saveFailed");
332
+ } finally {
333
+ this.pending = "";
334
+ }
335
+ },
336
+ async chooseSearch(rawId) {
337
+ if (!this.search || this.busy) return;
338
+ const id = searchValueFromMenu(rawId);
339
+ this.pending = "search";
340
+ this.searchError = "";
341
+ try {
342
+ this.search = await this.settings.setSearchDefault(id);
343
+ } catch (err) {
344
+ this.searchError = this.failText(this.tSearch, "settings.saveFailed", err);
345
+ } finally {
346
+ this.pending = "";
347
+ }
348
+ },
349
+ },
350
+ };
351
+ </script>
352
+
353
+ <style scoped>
354
+ .lares-agent {
355
+ position: relative;
356
+ display: flex;
357
+ flex-direction: column;
358
+ min-height: 280px;
359
+ color: var(--q-ink-1);
360
+ }
361
+
362
+ .lares-agent__body {
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: 20px;
366
+ padding: 16px 20px 32px;
367
+ }
368
+
369
+ .lares-agent__loading {
370
+ position: absolute;
371
+ inset: 0;
372
+ z-index: 2;
373
+ display: flex;
374
+ flex-direction: column;
375
+ align-items: center;
376
+ justify-content: center;
377
+ gap: 12px;
378
+ min-height: 280px;
379
+ background: color-mix(in srgb, var(--q-background-1) 88%, transparent);
380
+ color: var(--q-ink-2);
381
+ }
382
+
383
+ .lares-agent__loading p {
384
+ margin: 0;
385
+ font-size: 14px;
386
+ line-height: 20px;
387
+ }
388
+
389
+ .lares-agent__spinner {
390
+ width: 22px;
391
+ height: 22px;
392
+ border: 2px solid var(--q-separator);
393
+ border-top-color: var(--q-ink-1);
394
+ border-radius: 50%;
395
+ animation: lares-agent-spin 0.8s linear infinite;
396
+ }
397
+
398
+ @keyframes lares-agent-spin {
399
+ to { transform: rotate(360deg); }
400
+ }
401
+
402
+ @media (prefers-reduced-motion: reduce) {
403
+ .lares-agent__spinner {
404
+ animation: none;
405
+ }
406
+ }
407
+
408
+ .lares-agent__group {
409
+ display: flex;
410
+ flex-direction: column;
411
+ gap: 8px;
412
+ }
413
+
414
+ .lares-agent__group h2 {
415
+ margin: 0;
416
+ font-size: 16px;
417
+ font-weight: 500;
418
+ line-height: 22px;
419
+ }
420
+
421
+ .lares-agent__list {
422
+ display: flex;
423
+ flex-direction: column;
424
+ gap: 12px;
425
+ }
426
+
427
+ .lares-agent__hint {
428
+ margin: 0;
429
+ font-size: 13px;
430
+ color: var(--q-ink-3);
431
+ }
432
+
433
+ .lares-agent__hint[data-status="error"] {
434
+ color: var(--q-orange-default);
435
+ }
436
+
437
+ .lares-agent__hint--sheet {
438
+ padding: 8px 20px 16px;
439
+ }
440
+
441
+ .lares-agent__heading {
442
+ margin: 0;
443
+ padding: 12px 16px 4px;
444
+ color: var(--q-ink-3);
445
+ font-size: 12px;
446
+ font-weight: 500;
447
+ line-height: 16px;
448
+ }
449
+ </style>
@@ -0,0 +1,116 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ class="lares-set-row"
5
+ :class="{ 'lares-set-row--boxed': boxed }"
6
+ :disabled="disabled"
7
+ @click="$emit('click')"
8
+ >
9
+ <span class="lares-set-row__label">{{ label }}</span>
10
+ <span class="lares-set-row__side">
11
+ <span v-if="value" class="lares-set-row__value">{{ value }}</span>
12
+ <svg v-if="chevron" class="lares-set-row__icon" viewBox="0 0 24 24" aria-hidden="true">
13
+ <path d="M9 6.5 15.5 12 9 17.5" />
14
+ </svg>
15
+ <svg
16
+ v-else-if="checked"
17
+ class="lares-set-row__icon lares-set-row__icon--check"
18
+ viewBox="0 0 24 24"
19
+ aria-hidden="true"
20
+ >
21
+ <path d="M5 12.5 9.5 17 19 7.5" />
22
+ </svg>
23
+ </span>
24
+ </button>
25
+ </template>
26
+
27
+ <script>
28
+ export default {
29
+ name: "LaresSettingRow",
30
+ props: {
31
+ label: { type: String, required: true },
32
+ value: { type: String, default: "" },
33
+ chevron: { type: Boolean, default: false },
34
+ checked: { type: Boolean, default: false },
35
+ disabled: { type: Boolean, default: false },
36
+ boxed: { type: Boolean, default: false },
37
+ },
38
+ emits: ["click"],
39
+ };
40
+ </script>
41
+
42
+ <style scoped>
43
+ .lares-set-row {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ gap: 12px;
48
+ width: 100%;
49
+ min-height: 56px;
50
+ border: 0;
51
+ padding: 12px 16px;
52
+ background: transparent;
53
+ color: var(--q-ink-1);
54
+ text-align: left;
55
+ }
56
+
57
+ .lares-set-row--boxed {
58
+ min-height: 64px;
59
+ border: 1px solid var(--q-separator);
60
+ border-radius: 20px;
61
+ }
62
+
63
+ .lares-set-row:disabled {
64
+ opacity: 0.5;
65
+ }
66
+
67
+ .lares-set-row:not(.lares-set-row--boxed) + .lares-set-row:not(.lares-set-row--boxed) {
68
+ border-top: 1px solid var(--q-separator);
69
+ }
70
+
71
+ .lares-set-row:active:not(:disabled) {
72
+ background: var(--q-background-hover);
73
+ }
74
+
75
+ .lares-set-row__label {
76
+ min-width: 0;
77
+ flex: 1;
78
+ font-size: 14px;
79
+ line-height: 20px;
80
+ word-break: break-word;
81
+ }
82
+
83
+ .lares-set-row__side {
84
+ display: flex;
85
+ flex-shrink: 0;
86
+ align-items: center;
87
+ justify-content: flex-end;
88
+ gap: 4px;
89
+ max-width: 52%;
90
+ }
91
+
92
+ .lares-set-row__value {
93
+ overflow: hidden;
94
+ font-size: 13px;
95
+ line-height: 18px;
96
+ color: var(--q-ink-2);
97
+ text-align: right;
98
+ text-overflow: ellipsis;
99
+ white-space: nowrap;
100
+ }
101
+
102
+ .lares-set-row__icon {
103
+ width: 20px;
104
+ height: 20px;
105
+ flex-shrink: 0;
106
+ fill: none;
107
+ stroke: var(--q-ink-3);
108
+ stroke-width: 1.75;
109
+ stroke-linecap: round;
110
+ stroke-linejoin: round;
111
+ }
112
+
113
+ .lares-set-row__icon--check {
114
+ stroke: var(--q-blue-default);
115
+ }
116
+ </style>
@@ -0,0 +1,134 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <div
4
+ class="lares-sheet"
5
+ :data-open="open ? 'true' : 'false'"
6
+ :inert="!open"
7
+ :aria-hidden="!open"
8
+ >
9
+ <div class="lares-sheet__backdrop" @click="$emit('close')" />
10
+ <div class="lares-sheet__panel" role="dialog" :aria-label="title">
11
+ <header class="lares-sheet__head">
12
+ <h2 class="lares-sheet__title">{{ title }}</h2>
13
+ <button type="button" class="lares-sheet__close" @click="$emit('close')">
14
+ <svg viewBox="0 0 24 24" aria-hidden="true">
15
+ <path d="M6 6l12 12M18 6 6 18" />
16
+ </svg>
17
+ </button>
18
+ </header>
19
+ <div class="lares-sheet__body">
20
+ <slot />
21
+ </div>
22
+ </div>
23
+ </div>
24
+ </Teleport>
25
+ </template>
26
+
27
+ <script>
28
+ export default {
29
+ name: "LaresSheet",
30
+ props: {
31
+ open: { type: Boolean, default: false },
32
+ title: { type: String, default: "" },
33
+ },
34
+ emits: ["close"],
35
+ };
36
+ </script>
37
+
38
+ <style scoped>
39
+ .lares-sheet {
40
+ position: fixed;
41
+ inset: 0;
42
+ z-index: 40;
43
+ pointer-events: none;
44
+ }
45
+
46
+ .lares-sheet[data-open="true"] {
47
+ pointer-events: auto;
48
+ }
49
+
50
+ .lares-sheet__backdrop {
51
+ position: absolute;
52
+ inset: 0;
53
+ background: rgb(0 0 0 / 32%);
54
+ opacity: 0;
55
+ transition: opacity 0.28s cubic-bezier(0.32, 0.72, 0, 1);
56
+ }
57
+
58
+ .lares-sheet[data-open="true"] .lares-sheet__backdrop {
59
+ opacity: 1;
60
+ }
61
+
62
+ .lares-sheet__panel {
63
+ position: absolute;
64
+ right: 0;
65
+ bottom: 0;
66
+ left: 0;
67
+ display: flex;
68
+ max-height: min(70vh, 560px);
69
+ flex-direction: column;
70
+ min-height: 0;
71
+ border-radius: 12px 12px 0 0;
72
+ border-top: 1px solid var(--q-separator);
73
+ background: var(--q-background-1);
74
+ box-shadow: 0 -8px 32px rgb(0 0 0 / 12%);
75
+ transform: translateY(100%);
76
+ transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
77
+ }
78
+
79
+ .lares-sheet[data-open="true"] .lares-sheet__panel {
80
+ transform: translateY(0);
81
+ }
82
+
83
+ .lares-sheet__head {
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: space-between;
87
+ flex-shrink: 0;
88
+ height: 64px;
89
+ padding: 0 8px 0 20px;
90
+ }
91
+
92
+ .lares-sheet__title {
93
+ margin: 0;
94
+ font-size: 18px;
95
+ font-weight: 500;
96
+ line-height: 24px;
97
+ }
98
+
99
+ .lares-sheet__close {
100
+ display: inline-flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ width: 40px;
104
+ height: 40px;
105
+ border: 0;
106
+ background: transparent;
107
+ color: var(--q-ink-2);
108
+ }
109
+
110
+ .lares-sheet__close svg {
111
+ width: 20px;
112
+ height: 20px;
113
+ fill: none;
114
+ stroke: currentColor;
115
+ stroke-width: 1.75;
116
+ stroke-linecap: round;
117
+ }
118
+
119
+ .lares-sheet__body {
120
+ min-height: 0;
121
+ overflow-x: hidden;
122
+ overflow-y: auto;
123
+ padding: 0 0 calc(12px + env(safe-area-inset-bottom, 0px));
124
+ overscroll-behavior: contain;
125
+ -webkit-overflow-scrolling: touch;
126
+ }
127
+
128
+ @media (prefers-reduced-motion: reduce) {
129
+ .lares-sheet__backdrop,
130
+ .lares-sheet__panel {
131
+ transition: none;
132
+ }
133
+ }
134
+ </style>