@frockbot/plugin-settings 0.0.0 → 0.1.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.
- package/frockbot.json +55 -0
- package/package.json +43 -6
- package/src/backend.test.ts +397 -0
- package/src/backend.ts +262 -0
- package/src/client/BotPanel.vue +57 -0
- package/src/client/BotSettingsSurface.vue +1076 -0
- package/src/client/BotSettingsTrigger.vue +26 -0
- package/src/client/ConnectionsSurface.vue +813 -0
- package/src/client/ModelsSurface.vue +419 -0
- package/src/client/PackageAccounts.vue +330 -0
- package/src/client/PackageCatalogSurface.vue +584 -0
- package/src/client/PackageSettingsForm.vue +150 -0
- package/src/client/PackageSettingsSection.vue +66 -0
- package/src/client/PluginsSurface.vue +412 -0
- package/src/client/PluginsTrigger.vue +62 -0
- package/src/client/UserProfileTrigger.vue +223 -0
- package/src/client/UserSettingsSurface.vue +242 -0
- package/src/client/assignment-operations.ts +22 -0
- package/src/client/bot-settings.test.ts +218 -0
- package/src/client/bot-settings.ts +150 -0
- package/src/client/index.test.ts +113 -0
- package/src/client/index.ts +78 -0
- package/src/client/package-settings.test.ts +63 -0
- package/src/client/package-settings.ts +77 -0
- package/src/client/package-surfaces.test.ts +147 -0
- package/src/client/package-surfaces.ts +93 -0
- package/src/client/user-display-name.test.ts +44 -0
- package/src/client/user-display-name.ts +16 -0
- package/src/env.d.ts +6 -0
- package/src/index.ts +1 -0
- package/src/manifest.ts +3 -0
- package/src/user.test.ts +1277 -0
- package/src/user.ts +1221 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +30 -0
- package/README.md +0 -3
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { catalogSetupFieldKeyV1 } from "@frockbot/catalog-core";
|
|
3
|
+
import { UiButton, UiIcon } from "@frockbot/client-ui";
|
|
4
|
+
import {
|
|
5
|
+
frockBotWebDataKey,
|
|
6
|
+
type CatalogEntryV1,
|
|
7
|
+
type CatalogIndexEntryV1,
|
|
8
|
+
} from "@frockbot/plugin-shell/shared";
|
|
9
|
+
import { computed, inject, onMounted, ref } from "vue";
|
|
10
|
+
|
|
11
|
+
const providedWeb = inject(frockBotWebDataKey);
|
|
12
|
+
if (!providedWeb)
|
|
13
|
+
throw new Error("Package Catalog client data was not provided");
|
|
14
|
+
const web = providedWeb;
|
|
15
|
+
|
|
16
|
+
const search = ref("");
|
|
17
|
+
const openCatalogId = ref<string>();
|
|
18
|
+
const openCatalogEntry = ref<CatalogEntryV1>();
|
|
19
|
+
const catalogEntryLoading = ref(false);
|
|
20
|
+
const uninstallingPackageId = ref<string>();
|
|
21
|
+
const setupValues = ref<Record<string, string>>({});
|
|
22
|
+
const setupValuesFor = ref<string>();
|
|
23
|
+
const installingCatalogId = ref<string>();
|
|
24
|
+
|
|
25
|
+
const filteredCatalog = computed(() => {
|
|
26
|
+
const query = search.value.trim().toLocaleLowerCase();
|
|
27
|
+
if (!query) return web.value.packageCatalog;
|
|
28
|
+
return web.value.packageCatalog.filter(
|
|
29
|
+
(entry) =>
|
|
30
|
+
entry.displayName.toLocaleLowerCase().includes(query) ||
|
|
31
|
+
entry.packageId.toLocaleLowerCase().includes(query) ||
|
|
32
|
+
entry.description.toLocaleLowerCase().includes(query),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
onMounted(() => void web.value.loadPackageCatalog());
|
|
37
|
+
|
|
38
|
+
function setupFieldsOf(entry: CatalogEntryV1 | undefined): Array<{
|
|
39
|
+
key: string;
|
|
40
|
+
title: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
maxLength?: number;
|
|
43
|
+
}> {
|
|
44
|
+
return (entry?.setupFields ?? []).map((field, index) => ({
|
|
45
|
+
key: catalogSetupFieldKeyV1(field, index),
|
|
46
|
+
title: field.title ?? catalogSetupFieldKeyV1(field, index),
|
|
47
|
+
...(field.description ? { description: field.description } : {}),
|
|
48
|
+
...(field.maxLength === undefined ? {} : { maxLength: field.maxLength }),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function entryUsesOAuth(entry: CatalogEntryV1 | undefined): boolean {
|
|
53
|
+
return (entry?.servers ?? []).some((server) => server.auth === "oauth");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function beginSetup(entry: CatalogEntryV1): void {
|
|
57
|
+
setupValuesFor.value = entry.catalogId;
|
|
58
|
+
setupValues.value = Object.fromEntries(
|
|
59
|
+
setupFieldsOf(entry).map((field) => [field.key, ""]),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function connectCatalogServer(label: string, url: string): Promise<void> {
|
|
64
|
+
const redirect = await web.value.startMcpAuthorization({
|
|
65
|
+
label,
|
|
66
|
+
settings: { url, transport: "streamable-http" },
|
|
67
|
+
});
|
|
68
|
+
if (redirect) await web.value.openConnectionAuthorization(redirect);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function installWithSetupValues(
|
|
72
|
+
index: CatalogIndexEntryV1,
|
|
73
|
+
entry: CatalogEntryV1,
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
installingCatalogId.value = index.catalogId;
|
|
76
|
+
try {
|
|
77
|
+
const values = Object.fromEntries(
|
|
78
|
+
Object.entries(setupValues.value)
|
|
79
|
+
.map(([key, value]) => [key, value.trim()] as const)
|
|
80
|
+
.filter(([, value]) => value.length > 0),
|
|
81
|
+
);
|
|
82
|
+
await web.value.installCatalogPackage(index, values);
|
|
83
|
+
setupValuesFor.value = undefined;
|
|
84
|
+
setupValues.value = {};
|
|
85
|
+
const server = (entry.servers ?? []).find(
|
|
86
|
+
(candidate) => candidate.auth === "oauth",
|
|
87
|
+
);
|
|
88
|
+
if (server) await connectCatalogServer(entry.displayName, server.url);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
web.value.settingsError =
|
|
91
|
+
error instanceof Error ? error.message : "Could not install the Package";
|
|
92
|
+
} finally {
|
|
93
|
+
installingCatalogId.value = undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function toggleCatalogEntry(entry: CatalogIndexEntryV1): Promise<void> {
|
|
98
|
+
if (openCatalogId.value === entry.catalogId) {
|
|
99
|
+
openCatalogId.value = undefined;
|
|
100
|
+
openCatalogEntry.value = undefined;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
openCatalogId.value = entry.catalogId;
|
|
104
|
+
openCatalogEntry.value = undefined;
|
|
105
|
+
catalogEntryLoading.value = true;
|
|
106
|
+
try {
|
|
107
|
+
const detail = await web.value.loadCatalogEntry(entry.catalogId);
|
|
108
|
+
if (openCatalogId.value === entry.catalogId)
|
|
109
|
+
openCatalogEntry.value = detail;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
web.value.settingsError =
|
|
112
|
+
error instanceof Error ? error.message : "Could not load the entry";
|
|
113
|
+
} finally {
|
|
114
|
+
catalogEntryLoading.value = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function install(entry: CatalogIndexEntryV1): Promise<void> {
|
|
119
|
+
try {
|
|
120
|
+
await web.value.installCatalogPackage(entry);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
web.value.settingsError =
|
|
123
|
+
error instanceof Error ? error.message : "Could not install the Package";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function confirmUninstall(packageId: string): Promise<void> {
|
|
128
|
+
try {
|
|
129
|
+
await web.value.uninstallPackage(packageId);
|
|
130
|
+
uninstallingPackageId.value = undefined;
|
|
131
|
+
if (openCatalogEntry.value?.packageId === packageId) {
|
|
132
|
+
openCatalogId.value = undefined;
|
|
133
|
+
openCatalogEntry.value = undefined;
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
web.value.settingsError =
|
|
137
|
+
error instanceof Error
|
|
138
|
+
? error.message
|
|
139
|
+
: "Could not uninstall the Package";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isPackageInstalled(packageId: string): boolean {
|
|
144
|
+
return Boolean(
|
|
145
|
+
web.value.userSettings?.packages.some(
|
|
146
|
+
(item) => item.packageId === packageId && item.state === "installed",
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<template>
|
|
153
|
+
<div class="package-catalog-surface">
|
|
154
|
+
<p class="catalog-intro">
|
|
155
|
+
Browse published Packages. Installing one makes it available to every Bot
|
|
156
|
+
you own; each Bot still needs an explicit Assignment to use it.
|
|
157
|
+
</p>
|
|
158
|
+
<label class="catalog-search">
|
|
159
|
+
<UiIcon name="search" />
|
|
160
|
+
<input
|
|
161
|
+
v-model="search"
|
|
162
|
+
placeholder="Search the Catalog"
|
|
163
|
+
aria-label="Search the Package Catalog"
|
|
164
|
+
/>
|
|
165
|
+
</label>
|
|
166
|
+
<p v-if="web.packageCatalogGeneration" class="catalog-generation">
|
|
167
|
+
Generation {{ web.packageCatalogGeneration }}
|
|
168
|
+
</p>
|
|
169
|
+
<div class="catalog-grid">
|
|
170
|
+
<article
|
|
171
|
+
v-for="entry in filteredCatalog"
|
|
172
|
+
:key="entry.catalogId"
|
|
173
|
+
class="catalog-card"
|
|
174
|
+
>
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
class="catalog-summary"
|
|
178
|
+
:aria-expanded="openCatalogId === entry.catalogId"
|
|
179
|
+
:aria-controls="`catalog-detail-${entry.catalogId}`"
|
|
180
|
+
@click="toggleCatalogEntry(entry)"
|
|
181
|
+
>
|
|
182
|
+
<span class="catalog-logo" aria-hidden="true">
|
|
183
|
+
{{ entry.displayName.slice(0, 1) }}
|
|
184
|
+
</span>
|
|
185
|
+
<span class="catalog-copy">
|
|
186
|
+
<strong>{{ entry.displayName }}</strong>
|
|
187
|
+
<small>{{ entry.description }}</small>
|
|
188
|
+
</span>
|
|
189
|
+
<span class="catalog-version">{{ entry.version }}</span>
|
|
190
|
+
<UiIcon
|
|
191
|
+
class="catalog-chevron"
|
|
192
|
+
:class="{
|
|
193
|
+
'catalog-chevron--open': openCatalogId === entry.catalogId,
|
|
194
|
+
}"
|
|
195
|
+
name="chevrons-right"
|
|
196
|
+
size="sm"
|
|
197
|
+
/>
|
|
198
|
+
</button>
|
|
199
|
+
<div
|
|
200
|
+
:id="`catalog-detail-${entry.catalogId}`"
|
|
201
|
+
class="catalog-collapse"
|
|
202
|
+
:class="{
|
|
203
|
+
'catalog-collapse--open': openCatalogId === entry.catalogId,
|
|
204
|
+
}"
|
|
205
|
+
:inert="openCatalogId === entry.catalogId ? undefined : true"
|
|
206
|
+
>
|
|
207
|
+
<div class="catalog-collapse-inner">
|
|
208
|
+
<div class="catalog-detail">
|
|
209
|
+
<p v-if="catalogEntryLoading" class="catalog-note">Loading…</p>
|
|
210
|
+
<template v-else-if="openCatalogEntry">
|
|
211
|
+
<p class="catalog-note">{{ openCatalogEntry.description }}</p>
|
|
212
|
+
<dl class="catalog-facts">
|
|
213
|
+
<div>
|
|
214
|
+
<dt>Package</dt>
|
|
215
|
+
<dd>{{ openCatalogEntry.packageId }}</dd>
|
|
216
|
+
</div>
|
|
217
|
+
<div>
|
|
218
|
+
<dt>Version</dt>
|
|
219
|
+
<dd>{{ openCatalogEntry.version }}</dd>
|
|
220
|
+
</div>
|
|
221
|
+
<div v-if="openCatalogEntry.homepage">
|
|
222
|
+
<dt>Homepage</dt>
|
|
223
|
+
<dd>
|
|
224
|
+
<a
|
|
225
|
+
:href="openCatalogEntry.homepage"
|
|
226
|
+
rel="noreferrer noopener"
|
|
227
|
+
target="_blank"
|
|
228
|
+
>
|
|
229
|
+
{{ openCatalogEntry.homepage }}
|
|
230
|
+
</a>
|
|
231
|
+
</dd>
|
|
232
|
+
</div>
|
|
233
|
+
</dl>
|
|
234
|
+
</template>
|
|
235
|
+
<div
|
|
236
|
+
v-if="uninstallingPackageId === entry.packageId"
|
|
237
|
+
class="catalog-confirm"
|
|
238
|
+
role="alert"
|
|
239
|
+
>
|
|
240
|
+
<p>
|
|
241
|
+
Uninstalling {{ entry.displayName }} removes it from every
|
|
242
|
+
Bot. Its Assignments stay visible as unavailable so you can
|
|
243
|
+
repair or remove them. Connections and credentials are kept.
|
|
244
|
+
</p>
|
|
245
|
+
<div class="catalog-actions">
|
|
246
|
+
<UiButton @click="uninstallingPackageId = undefined">
|
|
247
|
+
Keep it
|
|
248
|
+
</UiButton>
|
|
249
|
+
<UiButton
|
|
250
|
+
variant="danger"
|
|
251
|
+
@click="confirmUninstall(entry.packageId)"
|
|
252
|
+
>
|
|
253
|
+
Uninstall
|
|
254
|
+
</UiButton>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
<form
|
|
258
|
+
v-else-if="
|
|
259
|
+
setupValuesFor === entry.catalogId && openCatalogEntry
|
|
260
|
+
"
|
|
261
|
+
class="catalog-form"
|
|
262
|
+
@submit.prevent="
|
|
263
|
+
installWithSetupValues(entry, openCatalogEntry)
|
|
264
|
+
"
|
|
265
|
+
>
|
|
266
|
+
<label
|
|
267
|
+
v-for="field in setupFieldsOf(openCatalogEntry)"
|
|
268
|
+
:key="field.key"
|
|
269
|
+
>
|
|
270
|
+
<span>{{ field.title }}</span>
|
|
271
|
+
<input
|
|
272
|
+
v-model="setupValues[field.key]"
|
|
273
|
+
autocomplete="off"
|
|
274
|
+
:maxlength="field.maxLength ?? 2048"
|
|
275
|
+
/>
|
|
276
|
+
<span v-if="field.description" class="catalog-hint">
|
|
277
|
+
{{ field.description }}
|
|
278
|
+
</span>
|
|
279
|
+
</label>
|
|
280
|
+
<p v-if="entryUsesOAuth(openCatalogEntry)" class="catalog-hint">
|
|
281
|
+
After installing, you will be sent to the connector to sign
|
|
282
|
+
in. FrockBot records the request; only you can complete it.
|
|
283
|
+
</p>
|
|
284
|
+
<div class="catalog-actions">
|
|
285
|
+
<UiButton @click="setupValuesFor = undefined"
|
|
286
|
+
>Cancel</UiButton
|
|
287
|
+
>
|
|
288
|
+
<UiButton
|
|
289
|
+
type="submit"
|
|
290
|
+
variant="primary"
|
|
291
|
+
:disabled="installingCatalogId === entry.catalogId"
|
|
292
|
+
>
|
|
293
|
+
{{
|
|
294
|
+
installingCatalogId === entry.catalogId
|
|
295
|
+
? "Installing…"
|
|
296
|
+
: "Install and connect"
|
|
297
|
+
}}
|
|
298
|
+
</UiButton>
|
|
299
|
+
</div>
|
|
300
|
+
</form>
|
|
301
|
+
<div v-else class="catalog-actions">
|
|
302
|
+
<UiButton
|
|
303
|
+
v-if="isPackageInstalled(entry.packageId)"
|
|
304
|
+
variant="danger"
|
|
305
|
+
@click="uninstallingPackageId = entry.packageId"
|
|
306
|
+
>
|
|
307
|
+
Uninstall
|
|
308
|
+
</UiButton>
|
|
309
|
+
<UiButton
|
|
310
|
+
v-else-if="
|
|
311
|
+
openCatalogEntry &&
|
|
312
|
+
(setupFieldsOf(openCatalogEntry).length > 0 ||
|
|
313
|
+
entryUsesOAuth(openCatalogEntry))
|
|
314
|
+
"
|
|
315
|
+
variant="primary"
|
|
316
|
+
@click="beginSetup(openCatalogEntry)"
|
|
317
|
+
>
|
|
318
|
+
Set up
|
|
319
|
+
</UiButton>
|
|
320
|
+
<UiButton v-else variant="primary" @click="install(entry)">
|
|
321
|
+
Install
|
|
322
|
+
</UiButton>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</article>
|
|
328
|
+
</div>
|
|
329
|
+
<p
|
|
330
|
+
v-if="web.packageCatalog.length === 0 && !web.settingsError"
|
|
331
|
+
class="catalog-empty"
|
|
332
|
+
>
|
|
333
|
+
The Catalog has nothing to offer yet.
|
|
334
|
+
</p>
|
|
335
|
+
<p v-else-if="filteredCatalog.length === 0" class="catalog-empty">
|
|
336
|
+
No Catalog entry matches that search.
|
|
337
|
+
</p>
|
|
338
|
+
<p v-if="web.settingsError" class="settings-error" role="alert">
|
|
339
|
+
{{ web.settingsError }}
|
|
340
|
+
</p>
|
|
341
|
+
</div>
|
|
342
|
+
</template>
|
|
343
|
+
|
|
344
|
+
<style scoped>
|
|
345
|
+
.package-catalog-surface {
|
|
346
|
+
padding: 24px;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.catalog-intro,
|
|
350
|
+
.catalog-empty {
|
|
351
|
+
margin: 0 0 14px;
|
|
352
|
+
color: var(--frock-text-muted);
|
|
353
|
+
font-size: var(--frock-text-base);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.catalog-search {
|
|
357
|
+
display: flex;
|
|
358
|
+
height: 42px;
|
|
359
|
+
align-items: center;
|
|
360
|
+
gap: 9px;
|
|
361
|
+
padding: 0 13px;
|
|
362
|
+
border: 1px solid var(--frock-border);
|
|
363
|
+
border-radius: 999px;
|
|
364
|
+
color: var(--frock-text-subtle);
|
|
365
|
+
background: var(--frock-surface-raised);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.catalog-search:focus-within {
|
|
369
|
+
border-color: var(--frock-border-focus);
|
|
370
|
+
box-shadow: 0 0 0 3px var(--frock-focus-ring);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.catalog-search input {
|
|
374
|
+
min-width: 0;
|
|
375
|
+
flex: 1;
|
|
376
|
+
border: 0;
|
|
377
|
+
outline: 0;
|
|
378
|
+
color: var(--frock-text);
|
|
379
|
+
background: transparent;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.catalog-generation {
|
|
383
|
+
margin: 10px 0 0;
|
|
384
|
+
color: var(--frock-text-subtle);
|
|
385
|
+
font-size: var(--frock-text-sm);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.catalog-grid {
|
|
389
|
+
display: grid;
|
|
390
|
+
gap: 12px;
|
|
391
|
+
margin-top: 18px;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.catalog-card {
|
|
395
|
+
min-width: 0;
|
|
396
|
+
padding: 8px;
|
|
397
|
+
border: 1px solid var(--frock-border);
|
|
398
|
+
border-radius: var(--frock-radius-card);
|
|
399
|
+
background: var(--frock-surface-raised);
|
|
400
|
+
box-shadow: var(--frock-shadow-card);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.catalog-summary {
|
|
404
|
+
display: grid;
|
|
405
|
+
width: 100%;
|
|
406
|
+
min-width: 0;
|
|
407
|
+
grid-template-columns: 44px minmax(0, 1fr) auto 16px;
|
|
408
|
+
align-items: center;
|
|
409
|
+
gap: 12px;
|
|
410
|
+
padding: 8px;
|
|
411
|
+
border: 0;
|
|
412
|
+
border-radius: var(--frock-radius-control);
|
|
413
|
+
color: inherit;
|
|
414
|
+
background: transparent;
|
|
415
|
+
text-align: left;
|
|
416
|
+
cursor: pointer;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.catalog-summary:hover {
|
|
420
|
+
background: var(--frock-fill-hover);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.catalog-logo {
|
|
424
|
+
display: grid;
|
|
425
|
+
width: 44px;
|
|
426
|
+
height: 44px;
|
|
427
|
+
place-items: center;
|
|
428
|
+
border-radius: 12px;
|
|
429
|
+
color: var(--frock-action-secondary-text);
|
|
430
|
+
background: var(--frock-surface-accent);
|
|
431
|
+
font-weight: 800;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.catalog-copy {
|
|
435
|
+
min-width: 0;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.catalog-copy strong,
|
|
439
|
+
.catalog-copy small {
|
|
440
|
+
display: block;
|
|
441
|
+
overflow: hidden;
|
|
442
|
+
white-space: nowrap;
|
|
443
|
+
text-overflow: ellipsis;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.catalog-copy strong {
|
|
447
|
+
font-size: var(--frock-text-md);
|
|
448
|
+
font-weight: 600;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.catalog-copy small,
|
|
452
|
+
.catalog-version,
|
|
453
|
+
.catalog-note,
|
|
454
|
+
.catalog-hint {
|
|
455
|
+
color: var(--frock-text-muted);
|
|
456
|
+
font-size: var(--frock-text-sm);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.catalog-copy small {
|
|
460
|
+
margin-top: 4px;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.catalog-version {
|
|
464
|
+
white-space: nowrap;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.catalog-chevron {
|
|
468
|
+
color: var(--frock-text-subtle);
|
|
469
|
+
transition: transform var(--frock-motion-panel);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.catalog-chevron--open {
|
|
473
|
+
transform: rotate(90deg);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.catalog-collapse {
|
|
477
|
+
display: grid;
|
|
478
|
+
grid-template-rows: 0fr;
|
|
479
|
+
opacity: 0;
|
|
480
|
+
transition:
|
|
481
|
+
grid-template-rows var(--frock-motion-panel),
|
|
482
|
+
opacity var(--frock-motion-panel);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.catalog-collapse--open {
|
|
486
|
+
grid-template-rows: 1fr;
|
|
487
|
+
opacity: 1;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.catalog-collapse-inner {
|
|
491
|
+
min-height: 0;
|
|
492
|
+
overflow: hidden;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.catalog-detail {
|
|
496
|
+
display: grid;
|
|
497
|
+
gap: 12px;
|
|
498
|
+
padding: 12px 8px;
|
|
499
|
+
border-top: 1px solid var(--frock-border);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.catalog-note,
|
|
503
|
+
.catalog-hint {
|
|
504
|
+
margin: 0;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.catalog-facts {
|
|
508
|
+
display: grid;
|
|
509
|
+
gap: 6px;
|
|
510
|
+
margin: 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.catalog-facts div {
|
|
514
|
+
display: grid;
|
|
515
|
+
grid-template-columns: 90px minmax(0, 1fr);
|
|
516
|
+
gap: 8px;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.catalog-facts dt {
|
|
520
|
+
color: var(--frock-text-subtle);
|
|
521
|
+
font-size: var(--frock-text-sm);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.catalog-facts dd {
|
|
525
|
+
margin: 0;
|
|
526
|
+
overflow-wrap: anywhere;
|
|
527
|
+
font-size: var(--frock-text-sm);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.catalog-confirm {
|
|
531
|
+
display: grid;
|
|
532
|
+
gap: 10px;
|
|
533
|
+
padding: 12px;
|
|
534
|
+
border: 1px solid var(--frock-border);
|
|
535
|
+
border-radius: var(--frock-radius-control);
|
|
536
|
+
background: var(--frock-surface-subtle);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.catalog-confirm p {
|
|
540
|
+
margin: 0;
|
|
541
|
+
font-size: var(--frock-text-sm);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.catalog-form {
|
|
545
|
+
display: grid;
|
|
546
|
+
gap: 12px;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.catalog-form label {
|
|
550
|
+
display: grid;
|
|
551
|
+
gap: 6px;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.catalog-form label > span:first-child {
|
|
555
|
+
color: var(--frock-text-muted);
|
|
556
|
+
font-size: var(--frock-text-sm);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.catalog-form input {
|
|
560
|
+
min-width: 0;
|
|
561
|
+
padding: 8px 11px;
|
|
562
|
+
border: 1px solid var(--frock-border);
|
|
563
|
+
border-radius: 9px;
|
|
564
|
+
color: var(--frock-text);
|
|
565
|
+
background: var(--frock-surface-raised);
|
|
566
|
+
font-size: var(--frock-text-base);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.catalog-actions {
|
|
570
|
+
display: flex;
|
|
571
|
+
flex-wrap: wrap;
|
|
572
|
+
justify-content: flex-end;
|
|
573
|
+
gap: 8px;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.catalog-empty {
|
|
577
|
+
margin-top: 18px;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.settings-error {
|
|
581
|
+
color: var(--frock-danger-text);
|
|
582
|
+
font-size: var(--frock-text-sm);
|
|
583
|
+
}
|
|
584
|
+
</style>
|