@frockbot/plugin-settings 0.0.0 → 0.1.1

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 (36) hide show
  1. package/frockbot.json +55 -0
  2. package/package.json +43 -6
  3. package/src/backend.test.ts +397 -0
  4. package/src/backend.ts +262 -0
  5. package/src/client/BotPanel.vue +57 -0
  6. package/src/client/BotSettingsSurface.vue +1076 -0
  7. package/src/client/BotSettingsTrigger.vue +26 -0
  8. package/src/client/ConnectionsSurface.vue +813 -0
  9. package/src/client/ModelsSurface.vue +419 -0
  10. package/src/client/PackageAccounts.vue +330 -0
  11. package/src/client/PackageCatalogSurface.vue +584 -0
  12. package/src/client/PackageSettingsForm.vue +150 -0
  13. package/src/client/PackageSettingsSection.vue +66 -0
  14. package/src/client/PluginsSurface.vue +412 -0
  15. package/src/client/PluginsTrigger.vue +62 -0
  16. package/src/client/UserProfileTrigger.vue +223 -0
  17. package/src/client/UserSettingsSurface.vue +242 -0
  18. package/src/client/assignment-operations.ts +22 -0
  19. package/src/client/bot-settings.test.ts +218 -0
  20. package/src/client/bot-settings.ts +150 -0
  21. package/src/client/index.test.ts +113 -0
  22. package/src/client/index.ts +78 -0
  23. package/src/client/package-settings.test.ts +63 -0
  24. package/src/client/package-settings.ts +77 -0
  25. package/src/client/package-surfaces.test.ts +147 -0
  26. package/src/client/package-surfaces.ts +93 -0
  27. package/src/client/user-display-name.test.ts +44 -0
  28. package/src/client/user-display-name.ts +16 -0
  29. package/src/env.d.ts +6 -0
  30. package/src/index.ts +1 -0
  31. package/src/manifest.ts +3 -0
  32. package/src/user.test.ts +1277 -0
  33. package/src/user.ts +1221 -0
  34. package/tsconfig.json +15 -0
  35. package/vite.config.ts +30 -0
  36. package/README.md +0 -3
@@ -0,0 +1,150 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * The knobs one Package declares, wherever that Package's configuration lives.
4
+ * Rendered from the manifest, so a Package that adds a setting needs no edit
5
+ * here and none to the surface that hosts this form.
6
+ */
7
+ import { UiButton } from "@frockbot/client-ui";
8
+ import {
9
+ frockBotWebDataKey,
10
+ type PluginCatalogItem,
11
+ } from "@frockbot/plugin-shell/shared";
12
+ import { computed, inject, ref, watch } from "vue";
13
+ import {
14
+ collectSettingsValues,
15
+ seedSettingsDraft,
16
+ settingFieldKind,
17
+ settingLabel,
18
+ } from "./package-settings.js";
19
+
20
+ const props = defineProps<{ item: PluginCatalogItem }>();
21
+
22
+ const providedWeb = inject(frockBotWebDataKey);
23
+ if (!providedWeb) throw new Error("shell client data was not provided");
24
+ const web = providedWeb;
25
+
26
+ const definitions = computed(() => props.item.settings ?? []);
27
+ const draft = ref<Record<string, string | number | boolean>>({});
28
+
29
+ /** The stored values of this Package, as the User settings hold them. */
30
+ const stored = computed<Record<string, unknown>>(() => {
31
+ const installation = web.value.userSettings?.packages.find(
32
+ (candidate) => candidate.packageId === props.item.packageId,
33
+ );
34
+ return (installation?.values ?? {}) as Record<string, unknown>;
35
+ });
36
+
37
+ watch(
38
+ [definitions, stored],
39
+ () => {
40
+ draft.value = seedSettingsDraft(definitions.value, stored.value);
41
+ },
42
+ { immediate: true },
43
+ );
44
+
45
+ async function save(): Promise<void> {
46
+ const values = collectSettingsValues(definitions.value, draft.value);
47
+ if (Object.keys(values).length === 0) return;
48
+ try {
49
+ await web.value.savePackageSettings(props.item.packageId, values);
50
+ } catch (error) {
51
+ web.value.settingsError =
52
+ error instanceof Error
53
+ ? error.message
54
+ : "Could not save the Package settings";
55
+ }
56
+ }
57
+ </script>
58
+
59
+ <template>
60
+ <form
61
+ v-if="definitions.length > 0"
62
+ class="package-settings-form"
63
+ @submit.prevent="save"
64
+ >
65
+ <label v-for="definition in definitions" :key="definition.id">
66
+ <span>{{ settingLabel(definition) }}</span>
67
+ <select
68
+ v-if="settingFieldKind(definition.schema) === 'enum'"
69
+ v-model="draft[definition.id]"
70
+ >
71
+ <option value="">Package default</option>
72
+ <option
73
+ v-for="choice in definition.schema.enum ?? []"
74
+ :key="String(choice)"
75
+ :value="choice ?? ''"
76
+ >
77
+ {{ String(choice) }}
78
+ </option>
79
+ </select>
80
+ <input
81
+ v-else-if="settingFieldKind(definition.schema) === 'boolean'"
82
+ v-model="draft[definition.id]"
83
+ type="checkbox"
84
+ />
85
+ <input
86
+ v-else-if="settingFieldKind(definition.schema) === 'number'"
87
+ v-model="draft[definition.id]"
88
+ type="number"
89
+ inputmode="numeric"
90
+ :min="definition.schema.minimum"
91
+ :max="definition.schema.maximum"
92
+ :step="definition.schema.type === 'integer' ? 1 : 'any'"
93
+ />
94
+ <input
95
+ v-else
96
+ v-model="draft[definition.id]"
97
+ type="text"
98
+ :maxlength="definition.schema.maxLength"
99
+ />
100
+ <small v-if="definition.schema.description" class="field-hint">
101
+ {{ definition.schema.description }}
102
+ </small>
103
+ </label>
104
+ <div class="package-settings-actions">
105
+ <UiButton type="submit" variant="primary">Save settings</UiButton>
106
+ </div>
107
+ </form>
108
+ </template>
109
+
110
+ <style scoped>
111
+ .package-settings-form {
112
+ display: grid;
113
+ gap: 12px;
114
+ margin: 0 8px;
115
+ padding: 12px 0 8px;
116
+ border-top: 1px solid var(--frock-border);
117
+ }
118
+
119
+ .package-settings-form label {
120
+ display: grid;
121
+ gap: 6px;
122
+ }
123
+
124
+ .package-settings-form span {
125
+ color: var(--frock-text-muted);
126
+ font-size: var(--frock-text-sm);
127
+ }
128
+
129
+ .package-settings-form input,
130
+ .package-settings-form select {
131
+ min-width: 0;
132
+ padding: 8px 11px;
133
+ border: 1px solid var(--frock-border);
134
+ border-radius: 9px;
135
+ background: var(--frock-surface-raised);
136
+ color: var(--frock-text);
137
+ font-size: var(--frock-text-base);
138
+ }
139
+
140
+ .field-hint {
141
+ color: var(--frock-text-muted);
142
+ font-size: var(--frock-text-sm);
143
+ }
144
+
145
+ .package-settings-actions {
146
+ display: flex;
147
+ justify-content: flex-end;
148
+ gap: 8px;
149
+ }
150
+ </style>
@@ -0,0 +1,66 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * The knobs of enabled Packages that configure nothing external — no model
4
+ * provider account, no Connection — and so have no surface of their own. They
5
+ * are still configuration, and configuration never lives in Plugins, so their
6
+ * home is Application settings.
7
+ */
8
+ import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
9
+ import { computed, inject } from "vue";
10
+ import { configurablePackages } from "./package-surfaces.js";
11
+ import PackageSettingsForm from "./PackageSettingsForm.vue";
12
+
13
+ const providedWeb = inject(frockBotWebDataKey);
14
+ if (!providedWeb) throw new Error("shell client data was not provided");
15
+ const web = providedWeb;
16
+
17
+ const packages = computed(() =>
18
+ configurablePackages({
19
+ catalog: web.value.pluginCatalog,
20
+ packages: web.value.userSettings?.packages ?? [],
21
+ home: "user-settings",
22
+ }),
23
+ );
24
+ </script>
25
+
26
+ <template>
27
+ <div v-if="packages.length > 0" class="package-settings">
28
+ <article
29
+ v-for="item in packages"
30
+ :key="item.packageId"
31
+ class="package-settings-card"
32
+ >
33
+ <strong>{{ item.displayName }}</strong>
34
+ <PackageSettingsForm :item="item" />
35
+ </article>
36
+ </div>
37
+ <p v-else class="package-settings-empty">
38
+ No enabled Package has settings of its own.
39
+ </p>
40
+ </template>
41
+
42
+ <style scoped>
43
+ .package-settings {
44
+ display: grid;
45
+ gap: 12px;
46
+ }
47
+
48
+ .package-settings-card {
49
+ padding: 12px 8px 4px;
50
+ border: 1px solid var(--frock-border);
51
+ border-radius: var(--frock-radius-card);
52
+ background: var(--frock-surface-raised);
53
+ }
54
+
55
+ .package-settings-card strong {
56
+ padding-left: 8px;
57
+ font-size: var(--frock-text-md);
58
+ font-weight: 600;
59
+ }
60
+
61
+ .package-settings-empty {
62
+ margin: 0;
63
+ color: var(--frock-text-muted);
64
+ font-size: var(--frock-text-sm);
65
+ }
66
+ </style>
@@ -0,0 +1,412 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Plugins: what a User has, and whether it is on.
4
+ *
5
+ * Enablement only. Nothing a Package declares — its accounts, its credentials,
6
+ * its settings — is edited here: each of those lives on the surface that owns
7
+ * what it configures, and this surface links to it. A disabled Package keeps
8
+ * all of it and simply stops being available to any Bot. Installing something
9
+ * new is the Package Catalog, one surface over.
10
+ */
11
+ import { clientSurfaceRegistryKey } from "@frockbot/client-core";
12
+ import { UiAnchor, UiButton, UiIcon } from "@frockbot/client-ui";
13
+ import {
14
+ frockBotWebDataKey,
15
+ type PluginCatalogItem,
16
+ } from "@frockbot/plugin-shell/shared";
17
+ import { settingsLinkV1 } from "@frockbot/plugin-shell/settings-links";
18
+ import { computed, inject, onMounted, ref } from "vue";
19
+ import {
20
+ configurationHomeLabel,
21
+ isPackageEnabled,
22
+ isPackageInstalled,
23
+ packageConfigurationHome,
24
+ type PackageConfigurationHome,
25
+ } from "./package-surfaces.js";
26
+
27
+ const providedSurfaces = inject(clientSurfaceRegistryKey);
28
+ const providedWeb = inject(frockBotWebDataKey);
29
+ if (!providedSurfaces || !providedWeb) {
30
+ throw new Error("settings client services were not provided");
31
+ }
32
+ const surfaces = providedSurfaces;
33
+ const web = providedWeb;
34
+ // Packages are User-scoped, so the row's link names no Bot.
35
+ const packagesLink = settingsLinkV1({ anchor: "user-packages" });
36
+ const search = ref("");
37
+ const busyPackageId = ref<string>();
38
+
39
+ const installations = computed(() => web.value.userSettings?.packages ?? []);
40
+ const installedPluginCount = computed(
41
+ () =>
42
+ web.value.pluginCatalog.filter((item) =>
43
+ isPackageInstalled(installations.value, item.packageId),
44
+ ).length,
45
+ );
46
+ const filteredCatalog = computed(() => {
47
+ const query = search.value.trim().toLocaleLowerCase();
48
+ if (!query) return web.value.pluginCatalog;
49
+ return web.value.pluginCatalog.filter(
50
+ (item) =>
51
+ item.displayName.toLocaleLowerCase().includes(query) ||
52
+ item.packageId.toLocaleLowerCase().includes(query),
53
+ );
54
+ });
55
+
56
+ onMounted(() => {
57
+ void web.value.loadPluginCatalog();
58
+ });
59
+
60
+ /** What a Package offers, in the words its manifest uses. */
61
+ function capabilitySummary(item: PluginCatalogItem): string {
62
+ const kinds = [...new Set(item.capabilities.map((entry) => entry.kind))];
63
+ if (kinds.length === 0) return "No Capabilities";
64
+ return kinds.join(", ");
65
+ }
66
+
67
+ function installed(packageId: string): boolean {
68
+ return isPackageInstalled(installations.value, packageId);
69
+ }
70
+
71
+ function enabled(packageId: string): boolean {
72
+ return isPackageEnabled(installations.value, packageId);
73
+ }
74
+
75
+ function failure(packageId: string): string | undefined {
76
+ return installations.value.find(
77
+ (candidate) => candidate.packageId === packageId,
78
+ )?.failure;
79
+ }
80
+
81
+ function stateLabel(packageId: string): string {
82
+ if (!installed(packageId)) return "Not installed";
83
+ if (enabled(packageId)) return "Enabled";
84
+ const installation = installations.value.find(
85
+ (candidate) => candidate.packageId === packageId,
86
+ );
87
+ return installation?.state === "failed" ? "Failed" : "Disabled";
88
+ }
89
+
90
+ function home(item: PluginCatalogItem): PackageConfigurationHome {
91
+ return packageConfigurationHome(item);
92
+ }
93
+
94
+ /** The surface that owns this Package's configuration, when it has one. */
95
+ function homeLabel(item: PluginCatalogItem): string | undefined {
96
+ return enabled(item.packageId)
97
+ ? configurationHomeLabel(home(item))
98
+ : undefined;
99
+ }
100
+
101
+ function openHome(item: PluginCatalogItem): void {
102
+ const target = home(item);
103
+ if (target === "none") return;
104
+ surfaces.open(target);
105
+ }
106
+
107
+ function openPackageCatalog(): void {
108
+ surfaces.open("package-catalog");
109
+ }
110
+
111
+ async function install(item: PluginCatalogItem): Promise<void> {
112
+ busyPackageId.value = item.packageId;
113
+ try {
114
+ await web.value.installPackage(item.packageId, item.version);
115
+ } catch (error) {
116
+ web.value.settingsError =
117
+ error instanceof Error ? error.message : "Could not add the Package";
118
+ } finally {
119
+ busyPackageId.value = undefined;
120
+ }
121
+ }
122
+
123
+ async function setEnabled(packageId: string, next: boolean): Promise<void> {
124
+ busyPackageId.value = packageId;
125
+ try {
126
+ await web.value.setPackageEnabled(packageId, next);
127
+ } catch (error) {
128
+ web.value.settingsError =
129
+ error instanceof Error
130
+ ? error.message
131
+ : "Could not change the Package state";
132
+ } finally {
133
+ busyPackageId.value = undefined;
134
+ }
135
+ }
136
+ </script>
137
+
138
+ <template>
139
+ <div class="plugins-surface">
140
+ <div class="installed-strip" aria-live="polite">
141
+ <span class="plugin-status-badge" aria-hidden="true">
142
+ <UiIcon name="check" :size="12" :weight="2.5" />
143
+ </span>
144
+ {{ installedPluginCount }} installed
145
+ </div>
146
+ <label class="plugin-search">
147
+ <UiIcon name="search" />
148
+ <input
149
+ v-model="search"
150
+ placeholder="Search Plugins"
151
+ aria-label="Search Plugins"
152
+ />
153
+ </label>
154
+ <UiAnchor
155
+ anchor="user-packages"
156
+ label="Packages"
157
+ :href="packagesLink"
158
+ class="plugin-anchor"
159
+ >
160
+ <p class="plugin-intro">
161
+ Turn Packages on and off for your Bots. Set one up where it belongs:
162
+ model providers in Models, accounts in Connections.
163
+ </p>
164
+ <div class="plugin-catalog-link">
165
+ <UiButton type="button" @click="openPackageCatalog">
166
+ Browse the Package Catalog
167
+ </UiButton>
168
+ </div>
169
+ </UiAnchor>
170
+ <div class="plugin-grid">
171
+ <article
172
+ v-for="item in filteredCatalog"
173
+ :key="item.packageId"
174
+ class="plugin-card"
175
+ >
176
+ <div class="plugin-summary">
177
+ <span class="plugin-logo" aria-hidden="true">
178
+ {{ item.displayName.slice(0, 1) }}
179
+ </span>
180
+ <span class="plugin-card-copy">
181
+ <strong>{{ item.displayName }}</strong>
182
+ <small>{{ capabilitySummary(item) }}</small>
183
+ </span>
184
+ <span
185
+ class="plugin-state"
186
+ :class="{ 'plugin-state--on': enabled(item.packageId) }"
187
+ >
188
+ {{ stateLabel(item.packageId) }}
189
+ </span>
190
+ <span class="plugin-actions">
191
+ <UiButton v-if="homeLabel(item)" @click="openHome(item)">
192
+ Set up in {{ homeLabel(item) }}
193
+ </UiButton>
194
+ <UiButton
195
+ v-if="!installed(item.packageId)"
196
+ variant="primary"
197
+ :disabled="busyPackageId === item.packageId"
198
+ @click="install(item)"
199
+ >
200
+ Add
201
+ </UiButton>
202
+ <UiButton
203
+ v-else-if="enabled(item.packageId)"
204
+ :disabled="busyPackageId === item.packageId"
205
+ @click="setEnabled(item.packageId, false)"
206
+ >
207
+ Disable
208
+ </UiButton>
209
+ <UiButton
210
+ v-else
211
+ variant="primary"
212
+ :disabled="busyPackageId === item.packageId"
213
+ @click="setEnabled(item.packageId, true)"
214
+ >
215
+ Enable
216
+ </UiButton>
217
+ </span>
218
+ </div>
219
+ <p v-if="failure(item.packageId)" class="plugin-failure" role="alert">
220
+ {{ failure(item.packageId) }}
221
+ </p>
222
+ </article>
223
+ </div>
224
+
225
+ <p
226
+ v-if="web.pluginCatalog.length === 0 && !web.settingsError"
227
+ class="plugin-empty"
228
+ >
229
+ This application ships no Packages.
230
+ </p>
231
+ <p v-else-if="filteredCatalog.length === 0" class="plugin-empty">
232
+ No Package matches that search.
233
+ </p>
234
+ <p v-if="web.settingsError" class="settings-error" role="alert">
235
+ {{ web.settingsError }}
236
+ </p>
237
+ </div>
238
+ </template>
239
+
240
+ <style scoped>
241
+ .plugins-surface {
242
+ padding: 24px;
243
+ }
244
+
245
+ .installed-strip {
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 8px;
249
+ margin-bottom: 10px;
250
+ color: var(--frock-text-muted);
251
+ font-size: var(--frock-text-sm);
252
+ font-weight: 600;
253
+ }
254
+
255
+ .plugin-status-badge {
256
+ display: grid;
257
+ width: 18px;
258
+ height: 18px;
259
+ place-items: center;
260
+ border-radius: 999px;
261
+ color: var(--frock-on-accent);
262
+ background: var(--frock-success);
263
+ }
264
+
265
+ .plugin-anchor {
266
+ padding-right: var(--frock-control-sm);
267
+ }
268
+
269
+ .plugin-search {
270
+ display: flex;
271
+ height: 42px;
272
+ align-items: center;
273
+ gap: 9px;
274
+ padding: 0 13px;
275
+ border: 1px solid var(--frock-border);
276
+ border-radius: 999px;
277
+ color: var(--frock-text-subtle);
278
+ background: var(--frock-surface-raised);
279
+ }
280
+
281
+ .plugin-search:focus-within {
282
+ border-color: var(--frock-border-focus);
283
+ box-shadow: 0 0 0 3px var(--frock-focus-ring);
284
+ }
285
+
286
+ .plugin-search input {
287
+ min-width: 0;
288
+ flex: 1;
289
+ border: 0;
290
+ outline: 0;
291
+ background: transparent;
292
+ }
293
+
294
+ .plugin-intro,
295
+ .plugin-empty {
296
+ margin: 14px 0 0;
297
+ color: var(--frock-text-muted);
298
+ font-size: var(--frock-text-base);
299
+ }
300
+
301
+ .plugin-catalog-link {
302
+ margin-top: 12px;
303
+ }
304
+
305
+ .plugin-grid {
306
+ display: grid;
307
+ gap: 12px;
308
+ margin-top: 18px;
309
+ }
310
+
311
+ .plugin-card {
312
+ min-width: 0;
313
+ padding: 8px;
314
+ border: 1px solid var(--frock-border);
315
+ border-radius: var(--frock-radius-card);
316
+ background: var(--frock-surface-raised);
317
+ box-shadow: var(--frock-shadow-card);
318
+ transition:
319
+ transform var(--frock-motion-fast),
320
+ box-shadow var(--frock-motion-fast);
321
+ }
322
+
323
+ .plugin-card:hover {
324
+ transform: translateY(-1px);
325
+ box-shadow: var(--frock-shadow-control);
326
+ }
327
+
328
+ .plugin-summary {
329
+ display: grid;
330
+ width: 100%;
331
+ min-width: 0;
332
+ grid-template-columns: 44px minmax(0, 1fr) auto auto;
333
+ align-items: center;
334
+ gap: 12px;
335
+ padding: 8px;
336
+ border: 0;
337
+ border-radius: var(--frock-radius-control);
338
+ color: inherit;
339
+ background: transparent;
340
+ text-align: left;
341
+ }
342
+
343
+ .plugin-logo {
344
+ display: grid;
345
+ width: 44px;
346
+ height: 44px;
347
+ place-items: center;
348
+ border-radius: 12px;
349
+ color: var(--frock-action-secondary-text);
350
+ background: var(--frock-surface-accent);
351
+ font-weight: 800;
352
+ }
353
+
354
+ .plugin-card-copy {
355
+ min-width: 0;
356
+ }
357
+
358
+ .plugin-card-copy strong,
359
+ .plugin-card-copy small {
360
+ display: block;
361
+ overflow: hidden;
362
+ white-space: nowrap;
363
+ text-overflow: ellipsis;
364
+ }
365
+
366
+ .plugin-card-copy strong {
367
+ font-size: var(--frock-text-md);
368
+ font-weight: 600;
369
+ }
370
+
371
+ .plugin-card-copy small {
372
+ margin-top: 4px;
373
+ color: var(--frock-text-muted);
374
+ font-size: var(--frock-text-sm);
375
+ text-transform: capitalize;
376
+ }
377
+
378
+ .plugin-state {
379
+ color: var(--frock-text-muted);
380
+ font-size: var(--frock-text-sm);
381
+ font-weight: 700;
382
+ white-space: nowrap;
383
+ }
384
+
385
+ .plugin-state--on {
386
+ color: var(--frock-success);
387
+ }
388
+
389
+ .plugin-actions {
390
+ display: flex;
391
+ flex-wrap: wrap;
392
+ justify-content: flex-end;
393
+ gap: 8px;
394
+ }
395
+
396
+ .plugin-actions :deep(.ui-button) {
397
+ min-height: 28px;
398
+ padding: 0 10px;
399
+ font-size: var(--frock-text-sm);
400
+ }
401
+
402
+ .plugin-failure {
403
+ margin: 0 8px 8px;
404
+ color: var(--frock-danger-text);
405
+ font-size: var(--frock-text-sm);
406
+ }
407
+
408
+ .settings-error {
409
+ color: var(--frock-danger-text);
410
+ font-size: var(--frock-text-sm);
411
+ }
412
+ </style>
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ import { clientSurfaceRegistryKey } from "@frockbot/client-core";
3
+ import { UiIcon } from "@frockbot/client-ui";
4
+ import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
5
+ import { inject } from "vue";
6
+
7
+ const surfaces = inject(clientSurfaceRegistryKey);
8
+ const web = inject(frockBotWebDataKey);
9
+ if (!surfaces || !web)
10
+ throw new Error("settings client services were not provided");
11
+ </script>
12
+
13
+ <template>
14
+ <button
15
+ v-if="web.connectionsAvailable"
16
+ class="settings-trigger"
17
+ type="button"
18
+ @click="surfaces.open('plugins')"
19
+ >
20
+ <span class="settings-trigger__icon"><UiIcon name="plugins" /></span>
21
+ Plugins
22
+ </button>
23
+ </template>
24
+
25
+ <style scoped>
26
+ .settings-trigger {
27
+ display: flex;
28
+ width: 100%;
29
+ height: 40px;
30
+ align-items: center;
31
+ gap: 10px;
32
+ padding: 0 8px;
33
+ border-radius: var(--frock-radius-control);
34
+ color: var(--frock-text);
35
+ background: transparent;
36
+ font-size: var(--frock-text-md);
37
+ font-weight: 500;
38
+ text-align: left;
39
+ cursor: pointer;
40
+ transition: background-color var(--frock-motion-fast);
41
+ }
42
+
43
+ .settings-trigger:hover {
44
+ background: var(--frock-fill-hover);
45
+ }
46
+
47
+ .settings-trigger:active {
48
+ background: var(--frock-fill-pressed);
49
+ }
50
+
51
+ .settings-trigger__icon {
52
+ display: grid;
53
+ width: var(--frock-avatar-sm);
54
+ height: var(--frock-avatar-sm);
55
+ flex: 0 0 auto;
56
+ place-items: center;
57
+ border-radius: 8px;
58
+ color: var(--frock-action-primary);
59
+ background: var(--frock-surface);
60
+ box-shadow: inset 0 0 0 1px var(--frock-border);
61
+ }
62
+ </style>