@frockbot/plugin-bot-template 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 +43 -0
- package/package.json +47 -6
- package/src/agent.test.ts +194 -0
- package/src/agent.ts +221 -0
- package/src/backend.test.ts +326 -0
- package/src/backend.ts +262 -0
- package/src/client/BotTemplateImportSection.vue +325 -0
- package/src/client/BotTemplateSection.vue +338 -0
- package/src/client/index.ts +210 -0
- package/src/client/state.ts +43 -0
- package/src/env.d.ts +6 -0
- package/src/import-apply.test.ts +455 -0
- package/src/import.test.ts +266 -0
- package/src/import.ts +292 -0
- package/src/index.ts +9 -0
- package/src/manifest.ts +3 -0
- package/src/scrub.test.ts +528 -0
- package/src/scrub.ts +523 -0
- package/src/shared.ts +620 -0
- package/src/user.test.ts +361 -0
- package/src/user.ts +913 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +31 -0
- package/README.md +0 -3
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Importing a Bot from a shared template.
|
|
3
|
+
//
|
|
4
|
+
// The review card is the whole point of this surface. A template is prose from
|
|
5
|
+
// a stranger, and it names Packages, Skills and Routines that are about to
|
|
6
|
+
// become durable state in this User's account — so it is shown as four honest
|
|
7
|
+
// sections before anything happens, and `Create the Bot` is the only control
|
|
8
|
+
// that writes. Planning is a read; the record stays `planned` until it is
|
|
9
|
+
// pressed.
|
|
10
|
+
//
|
|
11
|
+
// What the card never offers is a way to connect anything. An import creates no
|
|
12
|
+
// Connection and no Assignment, so "Needs your own Connection" is a list of
|
|
13
|
+
// things the User must go and do themselves, not a checkbox.
|
|
14
|
+
import { UiButton, UiField, UiIcon } from "@frockbot/client-ui";
|
|
15
|
+
import { computed, inject, ref } from "vue";
|
|
16
|
+
import { botTemplateStateKey } from "./state.js";
|
|
17
|
+
|
|
18
|
+
const providedState = inject(botTemplateStateKey);
|
|
19
|
+
if (!providedState) {
|
|
20
|
+
throw new Error("Bot template client services were not provided");
|
|
21
|
+
}
|
|
22
|
+
const templates = providedState;
|
|
23
|
+
const shareLink = ref("");
|
|
24
|
+
|
|
25
|
+
const review = computed(() => templates.value.reviewing);
|
|
26
|
+
|
|
27
|
+
const willInstall = computed(() =>
|
|
28
|
+
(review.value?.packages ?? []).filter(
|
|
29
|
+
(entry) => entry.status === "will-install",
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
const alreadyInstalled = computed(() =>
|
|
33
|
+
(review.value?.packages ?? []).filter(
|
|
34
|
+
(entry) => entry.status === "already-installed",
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
const missing = computed(() =>
|
|
38
|
+
(review.value?.packages ?? []).filter((entry) => entry.status === "missing"),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const failedStep = computed(() =>
|
|
42
|
+
review.value?.steps.find((step) => step.status === "failed"),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A pasted link carries the share id in its last path segment. Anything else is
|
|
47
|
+
* handed over untouched, so a bare share id works too.
|
|
48
|
+
*/
|
|
49
|
+
function shareIdFrom(value: string): string {
|
|
50
|
+
const trimmed = value.trim();
|
|
51
|
+
if (!trimmed) return "";
|
|
52
|
+
const url = URL.parse(trimmed);
|
|
53
|
+
if (!url) return trimmed;
|
|
54
|
+
const last = url.pathname.split("/").filter(Boolean).at(-1);
|
|
55
|
+
return last ? decodeURIComponent(last) : trimmed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function plan(): Promise<void> {
|
|
59
|
+
const shareId = shareIdFrom(shareLink.value);
|
|
60
|
+
if (!shareId) return;
|
|
61
|
+
await templates.value.planImport(shareId);
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<section class="import">
|
|
67
|
+
<header class="import__header">
|
|
68
|
+
<span class="import__icon" aria-hidden="true"
|
|
69
|
+
><UiIcon name="plus"
|
|
70
|
+
/></span>
|
|
71
|
+
<span class="import__intro">
|
|
72
|
+
<strong>Import a Bot template</strong>
|
|
73
|
+
<small>
|
|
74
|
+
Paste a template link to see exactly what it would create before
|
|
75
|
+
anything happens. Importing never brings across Memory, credentials,
|
|
76
|
+
Connections or Assignments.
|
|
77
|
+
</small>
|
|
78
|
+
</span>
|
|
79
|
+
</header>
|
|
80
|
+
|
|
81
|
+
<p v-if="templates.importError" class="import__error" role="alert">
|
|
82
|
+
{{ templates.importError }}
|
|
83
|
+
</p>
|
|
84
|
+
|
|
85
|
+
<div v-if="!review" class="import__form">
|
|
86
|
+
<UiField label="Template link">
|
|
87
|
+
<input
|
|
88
|
+
v-model="shareLink"
|
|
89
|
+
maxlength="500"
|
|
90
|
+
placeholder="https://…/templates/v1/…"
|
|
91
|
+
/>
|
|
92
|
+
</UiField>
|
|
93
|
+
<UiButton
|
|
94
|
+
type="button"
|
|
95
|
+
:disabled="templates.busy || !shareLink.trim()"
|
|
96
|
+
@click="plan"
|
|
97
|
+
>
|
|
98
|
+
{{ templates.busy ? "Reading…" : "Review the template" }}
|
|
99
|
+
</UiButton>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<article v-else class="review">
|
|
103
|
+
<div class="review__head">
|
|
104
|
+
<span class="review__text">
|
|
105
|
+
<strong>{{ review.botName }}</strong>
|
|
106
|
+
<small>
|
|
107
|
+
{{
|
|
108
|
+
review.status === "planned"
|
|
109
|
+
? "Nothing has been created yet."
|
|
110
|
+
: `Import ${review.status}.`
|
|
111
|
+
}}
|
|
112
|
+
</small>
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<section class="review__section">
|
|
117
|
+
<h4>Will create</h4>
|
|
118
|
+
<ul>
|
|
119
|
+
<li>The Bot “{{ review.botName }}”</li>
|
|
120
|
+
<li v-if="review.skills.length > 0">
|
|
121
|
+
{{ review.skills.length }} Skill(s):
|
|
122
|
+
{{ review.skills.join(", ") }}
|
|
123
|
+
</li>
|
|
124
|
+
<li v-for="routine in review.routines" :key="routine.slug">
|
|
125
|
+
Routine “{{ routine.slug }}”{{
|
|
126
|
+
routine.disabled ? " — created paused, with no webhook key" : ""
|
|
127
|
+
}}
|
|
128
|
+
</li>
|
|
129
|
+
</ul>
|
|
130
|
+
</section>
|
|
131
|
+
|
|
132
|
+
<section v-if="willInstall.length > 0" class="review__section">
|
|
133
|
+
<h4>Will install</h4>
|
|
134
|
+
<ul>
|
|
135
|
+
<li v-for="entry in willInstall" :key="entry.catalogId">
|
|
136
|
+
{{ entry.displayName }} ({{ entry.version }})
|
|
137
|
+
</li>
|
|
138
|
+
</ul>
|
|
139
|
+
</section>
|
|
140
|
+
|
|
141
|
+
<section v-if="alreadyInstalled.length > 0" class="review__section">
|
|
142
|
+
<h4>Already installed</h4>
|
|
143
|
+
<ul>
|
|
144
|
+
<li v-for="entry in alreadyInstalled" :key="entry.catalogId">
|
|
145
|
+
{{ entry.displayName }}
|
|
146
|
+
</li>
|
|
147
|
+
</ul>
|
|
148
|
+
</section>
|
|
149
|
+
|
|
150
|
+
<section v-if="missing.length > 0" class="review__section">
|
|
151
|
+
<h4>Missing from your catalog</h4>
|
|
152
|
+
<ul>
|
|
153
|
+
<li v-for="entry in missing" :key="entry.catalogId">
|
|
154
|
+
{{ entry.displayName }} — not in the Catalog generation you are
|
|
155
|
+
pinned to, so it will be skipped.
|
|
156
|
+
</li>
|
|
157
|
+
</ul>
|
|
158
|
+
</section>
|
|
159
|
+
|
|
160
|
+
<section v-if="review.connections.length > 0" class="review__section">
|
|
161
|
+
<h4>Needs your own Connection</h4>
|
|
162
|
+
<ul>
|
|
163
|
+
<li v-for="entry in review.connections" :key="entry.name">
|
|
164
|
+
{{ entry.name
|
|
165
|
+
}}<template v-if="entry.url"> — {{ entry.url }}</template>
|
|
166
|
+
<small v-if="entry.hint"> {{ entry.hint }}</small>
|
|
167
|
+
</li>
|
|
168
|
+
</ul>
|
|
169
|
+
<p class="import__note">
|
|
170
|
+
None of these is created for you. Add each one yourself, then assign
|
|
171
|
+
it to the Bot.
|
|
172
|
+
</p>
|
|
173
|
+
</section>
|
|
174
|
+
|
|
175
|
+
<p v-if="review.failure" class="import__error" role="alert">
|
|
176
|
+
{{ review.failure }}
|
|
177
|
+
</p>
|
|
178
|
+
<p v-if="failedStep" class="import__note">
|
|
179
|
+
The import stopped at “{{ failedStep.key }}”. Everything before it is
|
|
180
|
+
already in place; confirming again retries from there.
|
|
181
|
+
</p>
|
|
182
|
+
|
|
183
|
+
<div class="review__actions">
|
|
184
|
+
<UiButton
|
|
185
|
+
v-if="review.status !== 'applied'"
|
|
186
|
+
type="button"
|
|
187
|
+
variant="primary"
|
|
188
|
+
:disabled="templates.busy"
|
|
189
|
+
@click="templates.applyImport(review.importId)"
|
|
190
|
+
>
|
|
191
|
+
{{
|
|
192
|
+
templates.busy
|
|
193
|
+
? "Creating…"
|
|
194
|
+
: review.status === "failed"
|
|
195
|
+
? "Retry the import"
|
|
196
|
+
: "Create the Bot"
|
|
197
|
+
}}
|
|
198
|
+
</UiButton>
|
|
199
|
+
<UiButton type="button" @click="templates.dismissReview()">
|
|
200
|
+
{{ review.status === "applied" ? "Done" : "Cancel" }}
|
|
201
|
+
</UiButton>
|
|
202
|
+
</div>
|
|
203
|
+
</article>
|
|
204
|
+
</section>
|
|
205
|
+
</template>
|
|
206
|
+
|
|
207
|
+
<style scoped>
|
|
208
|
+
.import {
|
|
209
|
+
display: flex;
|
|
210
|
+
flex-direction: column;
|
|
211
|
+
gap: 12px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.import__header {
|
|
215
|
+
display: flex;
|
|
216
|
+
align-items: center;
|
|
217
|
+
gap: 10px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.import__icon {
|
|
221
|
+
display: grid;
|
|
222
|
+
width: var(--frock-avatar-sm);
|
|
223
|
+
height: var(--frock-avatar-sm);
|
|
224
|
+
flex: 0 0 auto;
|
|
225
|
+
place-items: center;
|
|
226
|
+
border-radius: 8px;
|
|
227
|
+
color: var(--frock-action-primary);
|
|
228
|
+
background: var(--frock-surface);
|
|
229
|
+
box-shadow: inset 0 0 0 1px var(--frock-border);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.import__intro {
|
|
233
|
+
display: flex;
|
|
234
|
+
min-width: 0;
|
|
235
|
+
flex: 1 1 auto;
|
|
236
|
+
flex-direction: column;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.import__intro strong {
|
|
240
|
+
color: var(--frock-text);
|
|
241
|
+
font-size: var(--frock-text-md);
|
|
242
|
+
font-weight: 600;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.import__intro small,
|
|
246
|
+
.import__note {
|
|
247
|
+
color: var(--frock-text-muted);
|
|
248
|
+
font-size: var(--frock-text-sm);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.import__error {
|
|
252
|
+
color: var(--frock-danger-text);
|
|
253
|
+
font-size: var(--frock-text-sm);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.import__form {
|
|
257
|
+
display: flex;
|
|
258
|
+
flex-direction: column;
|
|
259
|
+
gap: 8px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.review {
|
|
263
|
+
display: flex;
|
|
264
|
+
flex-direction: column;
|
|
265
|
+
gap: 10px;
|
|
266
|
+
border: 1px solid var(--frock-border);
|
|
267
|
+
border-radius: var(--frock-radius-card);
|
|
268
|
+
padding: 12px;
|
|
269
|
+
background: var(--frock-surface-subtle);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.review__head {
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
gap: 10px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.review__text {
|
|
279
|
+
display: flex;
|
|
280
|
+
min-width: 0;
|
|
281
|
+
flex: 1 1 auto;
|
|
282
|
+
flex-direction: column;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.review__text strong {
|
|
286
|
+
color: var(--frock-text);
|
|
287
|
+
font-size: var(--frock-text-md);
|
|
288
|
+
font-weight: 600;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.review__text small {
|
|
292
|
+
color: var(--frock-text-muted);
|
|
293
|
+
font-size: var(--frock-text-sm);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.review__section h4 {
|
|
297
|
+
margin: 0 0 4px;
|
|
298
|
+
color: var(--frock-text-subtle);
|
|
299
|
+
font-size: var(--frock-text-sm);
|
|
300
|
+
font-weight: 600;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.review__section ul {
|
|
304
|
+
display: flex;
|
|
305
|
+
flex-direction: column;
|
|
306
|
+
gap: 4px;
|
|
307
|
+
margin: 0;
|
|
308
|
+
padding-left: 18px;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.review__section li {
|
|
312
|
+
color: var(--frock-text);
|
|
313
|
+
font-size: var(--frock-text-sm);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.review__section li small {
|
|
317
|
+
color: var(--frock-text-muted);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.review__actions {
|
|
321
|
+
display: flex;
|
|
322
|
+
flex-wrap: wrap;
|
|
323
|
+
gap: 8px;
|
|
324
|
+
}
|
|
325
|
+
</style>
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Bot templates, in Bot settings.
|
|
3
|
+
//
|
|
4
|
+
// The User half of the export: staging packs a recipe privately, and choosing
|
|
5
|
+
// a visibility is a separate, deliberate click here — "Publication beyond the
|
|
6
|
+
// authoring User is a User action." The link is only shown once a share is
|
|
7
|
+
// actually `link` or `public`, so a copied URL always resolves.
|
|
8
|
+
import { UiButton } from "@frockbot/client-ui";
|
|
9
|
+
import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
|
|
10
|
+
import type {
|
|
11
|
+
TemplateShareRecordV1,
|
|
12
|
+
TemplateVisibilityV1,
|
|
13
|
+
} from "@frockbot/template-core";
|
|
14
|
+
import { computed, inject, ref, watch } from "vue";
|
|
15
|
+
import { templateSharePathV1 } from "../shared.js";
|
|
16
|
+
import { botTemplateStateKey } from "./state.js";
|
|
17
|
+
|
|
18
|
+
const providedWeb = inject(frockBotWebDataKey);
|
|
19
|
+
const providedState = inject(botTemplateStateKey);
|
|
20
|
+
if (!providedWeb || !providedState) {
|
|
21
|
+
throw new Error("Bot template client services were not provided");
|
|
22
|
+
}
|
|
23
|
+
const web = providedWeb;
|
|
24
|
+
const templates = providedState;
|
|
25
|
+
const copied = ref<string>();
|
|
26
|
+
const flowOpen = ref(false);
|
|
27
|
+
|
|
28
|
+
const botId = computed(() => web.value.activeBotId);
|
|
29
|
+
|
|
30
|
+
const shares = computed(() =>
|
|
31
|
+
templates.value.shares.filter((share) => share.botId === botId.value),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const visibilities: {
|
|
35
|
+
value: TemplateVisibilityV1;
|
|
36
|
+
label: string;
|
|
37
|
+
hint: string;
|
|
38
|
+
}[] = [
|
|
39
|
+
{
|
|
40
|
+
value: "private",
|
|
41
|
+
label: "Private",
|
|
42
|
+
hint: "Only you. The recipe is staged and shared with nobody.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
value: "link",
|
|
46
|
+
label: "Anyone with the link",
|
|
47
|
+
hint: "Unlisted. Anyone holding the link can read the recipe.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: "public",
|
|
51
|
+
label: "Public",
|
|
52
|
+
hint: "Readable by anyone, and listable.",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
watch(
|
|
57
|
+
botId,
|
|
58
|
+
(id) => {
|
|
59
|
+
if (id && !templates.value.loaded) void templates.value.load();
|
|
60
|
+
},
|
|
61
|
+
{ immediate: true },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
function shareUrl(share: TemplateShareRecordV1): string {
|
|
65
|
+
return `${globalThis.location?.origin ?? ""}${templateSharePathV1(share.shareId)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function describe(share: TemplateShareRecordV1): string {
|
|
69
|
+
if (share.revokedAt) return `Revoked ${share.revokedAt}`;
|
|
70
|
+
return (
|
|
71
|
+
visibilities.find((option) => option.value === share.visibility)?.label ??
|
|
72
|
+
share.visibility
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function copyLink(share: TemplateShareRecordV1): Promise<void> {
|
|
77
|
+
try {
|
|
78
|
+
await navigator.clipboard.writeText(shareUrl(share));
|
|
79
|
+
copied.value = share.shareId;
|
|
80
|
+
} catch {
|
|
81
|
+
// Clipboard access is a progressive enhancement; the link is on screen
|
|
82
|
+
// either way, so a refusal costs the convenience and nothing else.
|
|
83
|
+
copied.value = undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<section class="templates">
|
|
90
|
+
<UiButton
|
|
91
|
+
class="templates__open"
|
|
92
|
+
type="button"
|
|
93
|
+
:aria-expanded="flowOpen"
|
|
94
|
+
@click="flowOpen = !flowOpen"
|
|
95
|
+
>
|
|
96
|
+
Share as template
|
|
97
|
+
</UiButton>
|
|
98
|
+
|
|
99
|
+
<div v-if="flowOpen" class="templates__flow">
|
|
100
|
+
<header class="templates__header">
|
|
101
|
+
<span class="templates__intro">
|
|
102
|
+
<strong>Share this Bot</strong>
|
|
103
|
+
<small>
|
|
104
|
+
Packs the profile, Bot-authored Skills, Routines, and required
|
|
105
|
+
Packages. Memory, credentials, Connections, Assignments, and
|
|
106
|
+
Computer files stay private.
|
|
107
|
+
</small>
|
|
108
|
+
</span>
|
|
109
|
+
<UiButton
|
|
110
|
+
type="button"
|
|
111
|
+
:disabled="!botId || templates.busy"
|
|
112
|
+
@click="botId && templates.stage(botId)"
|
|
113
|
+
>
|
|
114
|
+
{{ templates.busy ? "Packing…" : "Pack template" }}
|
|
115
|
+
</UiButton>
|
|
116
|
+
</header>
|
|
117
|
+
|
|
118
|
+
<p v-if="templates.error" class="templates__error" role="alert">
|
|
119
|
+
{{ templates.error }}
|
|
120
|
+
</p>
|
|
121
|
+
|
|
122
|
+
<p
|
|
123
|
+
v-if="templates.summary && templates.openShareId"
|
|
124
|
+
class="templates__summary"
|
|
125
|
+
>
|
|
126
|
+
Packed {{ templates.summary.skills }} Skill(s),
|
|
127
|
+
{{ templates.summary.routines }} Routine(s),
|
|
128
|
+
{{ templates.summary.packages }} Package(s) and
|
|
129
|
+
{{ templates.summary.publicServers }} public MCP server(s).
|
|
130
|
+
<template v-if="templates.summary.needsConnection > 0">
|
|
131
|
+
{{ templates.summary.needsConnection }} server(s) are left as a
|
|
132
|
+
placeholder the importer fills with their own Connection.
|
|
133
|
+
</template>
|
|
134
|
+
Memory, credentials, Connections and Assignments were not included.
|
|
135
|
+
</p>
|
|
136
|
+
|
|
137
|
+
<p
|
|
138
|
+
v-if="templates.loaded && shares.length === 0"
|
|
139
|
+
class="templates__empty"
|
|
140
|
+
>
|
|
141
|
+
No template has been shared from this Bot yet.
|
|
142
|
+
</p>
|
|
143
|
+
|
|
144
|
+
<article
|
|
145
|
+
v-for="share in shares"
|
|
146
|
+
:key="share.shareId"
|
|
147
|
+
class="template-card"
|
|
148
|
+
>
|
|
149
|
+
<div class="template-card__head">
|
|
150
|
+
<span class="template-card__text">
|
|
151
|
+
<strong>{{ describe(share) }}</strong>
|
|
152
|
+
<small>Packed {{ share.createdAt }}</small>
|
|
153
|
+
</span>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<fieldset v-if="!share.revokedAt" class="template-card__visibility">
|
|
157
|
+
<legend>Who can read it</legend>
|
|
158
|
+
<label v-for="option in visibilities" :key="option.value">
|
|
159
|
+
<input
|
|
160
|
+
type="radio"
|
|
161
|
+
:name="`visibility-${share.shareId}`"
|
|
162
|
+
:value="option.value"
|
|
163
|
+
:checked="share.visibility === option.value"
|
|
164
|
+
:disabled="templates.busy"
|
|
165
|
+
@change="templates.setVisibility(share.shareId, option.value)"
|
|
166
|
+
/>
|
|
167
|
+
<span>
|
|
168
|
+
<strong>{{ option.label }}</strong>
|
|
169
|
+
<small>{{ option.hint }}</small>
|
|
170
|
+
</span>
|
|
171
|
+
</label>
|
|
172
|
+
</fieldset>
|
|
173
|
+
|
|
174
|
+
<p
|
|
175
|
+
v-if="!share.revokedAt && share.visibility !== 'private'"
|
|
176
|
+
class="template-card__link"
|
|
177
|
+
>
|
|
178
|
+
<code>{{ shareUrl(share) }}</code>
|
|
179
|
+
</p>
|
|
180
|
+
|
|
181
|
+
<div class="template-card__actions">
|
|
182
|
+
<UiButton
|
|
183
|
+
v-if="!share.revokedAt && share.visibility !== 'private'"
|
|
184
|
+
type="button"
|
|
185
|
+
@click="copyLink(share)"
|
|
186
|
+
>
|
|
187
|
+
{{ copied === share.shareId ? "Copied" : "Copy link" }}
|
|
188
|
+
</UiButton>
|
|
189
|
+
<UiButton
|
|
190
|
+
v-if="!share.revokedAt"
|
|
191
|
+
type="button"
|
|
192
|
+
variant="danger"
|
|
193
|
+
:disabled="templates.busy"
|
|
194
|
+
@click="templates.revoke(share.shareId)"
|
|
195
|
+
>
|
|
196
|
+
Revoke
|
|
197
|
+
</UiButton>
|
|
198
|
+
</div>
|
|
199
|
+
</article>
|
|
200
|
+
</div>
|
|
201
|
+
</section>
|
|
202
|
+
</template>
|
|
203
|
+
|
|
204
|
+
<style scoped>
|
|
205
|
+
.templates {
|
|
206
|
+
display: flex;
|
|
207
|
+
flex-direction: column;
|
|
208
|
+
gap: 12px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.templates__open {
|
|
212
|
+
width: 100%;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.templates__flow {
|
|
216
|
+
display: flex;
|
|
217
|
+
flex-direction: column;
|
|
218
|
+
gap: 12px;
|
|
219
|
+
padding-top: 4px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.templates__header {
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
gap: 10px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.templates__intro {
|
|
229
|
+
display: flex;
|
|
230
|
+
min-width: 0;
|
|
231
|
+
flex: 1 1 auto;
|
|
232
|
+
flex-direction: column;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.templates__intro strong {
|
|
236
|
+
color: var(--frock-text);
|
|
237
|
+
font-size: var(--frock-text-md);
|
|
238
|
+
font-weight: 600;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.templates__intro small,
|
|
242
|
+
.templates__empty,
|
|
243
|
+
.templates__summary {
|
|
244
|
+
color: var(--frock-text-muted);
|
|
245
|
+
font-size: var(--frock-text-sm);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.templates__error {
|
|
249
|
+
color: var(--frock-danger-text);
|
|
250
|
+
font-size: var(--frock-text-sm);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.template-card {
|
|
254
|
+
display: flex;
|
|
255
|
+
flex-direction: column;
|
|
256
|
+
gap: 10px;
|
|
257
|
+
border: 1px solid var(--frock-border);
|
|
258
|
+
border-radius: var(--frock-radius-card);
|
|
259
|
+
padding: 12px;
|
|
260
|
+
background: var(--frock-surface-subtle);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.template-card__head {
|
|
264
|
+
display: flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
gap: 10px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.template-card__text {
|
|
270
|
+
display: flex;
|
|
271
|
+
min-width: 0;
|
|
272
|
+
flex: 1 1 auto;
|
|
273
|
+
flex-direction: column;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.template-card__text strong {
|
|
277
|
+
color: var(--frock-text);
|
|
278
|
+
font-size: var(--frock-text-md);
|
|
279
|
+
font-weight: 600;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.template-card__text small {
|
|
283
|
+
color: var(--frock-text-muted);
|
|
284
|
+
font-size: var(--frock-text-sm);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.template-card__visibility {
|
|
288
|
+
display: flex;
|
|
289
|
+
flex-direction: column;
|
|
290
|
+
gap: 6px;
|
|
291
|
+
border: 0;
|
|
292
|
+
margin: 0;
|
|
293
|
+
padding: 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.template-card__visibility legend {
|
|
297
|
+
color: var(--frock-text-subtle);
|
|
298
|
+
font-size: var(--frock-text-sm);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.template-card__visibility label {
|
|
302
|
+
display: flex;
|
|
303
|
+
align-items: flex-start;
|
|
304
|
+
gap: 8px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.template-card__visibility label span {
|
|
308
|
+
display: flex;
|
|
309
|
+
flex-direction: column;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.template-card__visibility label strong {
|
|
313
|
+
color: var(--frock-text);
|
|
314
|
+
font-size: var(--frock-text-sm);
|
|
315
|
+
font-weight: 600;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.template-card__visibility label small {
|
|
319
|
+
color: var(--frock-text-muted);
|
|
320
|
+
font-size: var(--frock-text-sm);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.template-card__link {
|
|
324
|
+
overflow-x: auto;
|
|
325
|
+
margin: 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.template-card__link code {
|
|
329
|
+
color: var(--frock-text-muted);
|
|
330
|
+
font-size: var(--frock-text-sm);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.template-card__actions {
|
|
334
|
+
display: flex;
|
|
335
|
+
flex-wrap: wrap;
|
|
336
|
+
gap: 8px;
|
|
337
|
+
}
|
|
338
|
+
</style>
|