@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,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>
|