@frigear-nu/account 0.0.10 → 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 (33) hide show
  1. package/README.md +7 -4
  2. package/dist/module.d.mts +5 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +32 -26
  5. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.d.vue.ts +11 -0
  6. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue +95 -0
  7. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue.d.ts +11 -0
  8. package/dist/runtime/app/components/Account/AccountHeader.vue +14 -34
  9. package/dist/runtime/app/components/Account/AccountMenu.d.vue.ts +10 -1
  10. package/dist/runtime/app/components/Account/AccountMenu.vue +100 -51
  11. package/dist/runtime/app/components/Account/AccountMenu.vue.d.ts +10 -1
  12. package/dist/runtime/app/composables/index.d.ts +2 -0
  13. package/dist/runtime/app/composables/index.js +1 -0
  14. package/dist/runtime/app/composables/useAccountNav.d.ts +20 -0
  15. package/dist/runtime/app/composables/useAccountNav.js +18 -0
  16. package/dist/runtime/app/layouts/account.d.vue.ts +13 -0
  17. package/dist/runtime/app/layouts/account.vue +54 -0
  18. package/dist/runtime/app/layouts/account.vue.d.ts +13 -0
  19. package/dist/runtime/app/pages/api-tokens.vue +193 -261
  20. package/dist/runtime/app/pages/documents.vue +25 -26
  21. package/dist/runtime/app/pages/index.vue +73 -17
  22. package/dist/runtime/app/pages/profile.vue +152 -169
  23. package/dist/runtime/app/pages/security.vue +156 -151
  24. package/dist/runtime/app/utils/api-tokens.d.ts +19 -0
  25. package/dist/runtime/app/utils/api-tokens.js +0 -0
  26. package/dist/runtime/i18n/lang/da.json +41 -9
  27. package/dist/runtime/i18n/lang/en.json +41 -9
  28. package/dist/runtime/menus.d.ts +1 -1
  29. package/dist/runtime/menus.js +10 -0
  30. package/package.json +21 -15
  31. package/dist/runtime/app/pages/layout.d.vue.ts +0 -3
  32. package/dist/runtime/app/pages/layout.vue +0 -10
  33. package/dist/runtime/app/pages/layout.vue.d.ts +0 -3
@@ -0,0 +1,54 @@
1
+ <script setup>
2
+ defineOptions({ name: "AccountLayout" });
3
+ const siteLayout = "default";
4
+ const t = useModuleT("account");
5
+ const { user } = useUserSession();
6
+ const { links } = await useAccountNav();
7
+ const profile = computed(() => {
8
+ const value = user.value;
9
+ const name = value?.name || value?.email || t("title", "Account");
10
+ return {
11
+ name,
12
+ email: value?.name ? value.email ?? void 0 : void 0,
13
+ avatar: { src: value?.avatar || void 0, alt: name }
14
+ };
15
+ });
16
+ </script>
17
+
18
+ <template>
19
+ <NuxtLayout :name="siteLayout">
20
+ <UContainer>
21
+ <UPage>
22
+ <template #left>
23
+ <UPageAside>
24
+ <UUser
25
+ v-if="user"
26
+ :name="profile.name"
27
+ :description="profile.email"
28
+ :avatar="profile.avatar"
29
+ class="mb-4 px-2.5"
30
+ :ui="{ name: 'truncate', description: 'truncate' }"
31
+ />
32
+ <UNavigationMenu
33
+ :items="links"
34
+ orientation="vertical"
35
+ highlight
36
+ :aria-label="t('nav.label', 'Account navigation')"
37
+ />
38
+ </UPageAside>
39
+ </template>
40
+
41
+ <div class="flex min-w-0 flex-col gap-6 py-8">
42
+ <PageTabs
43
+ v-if="links.length"
44
+ class="lg:hidden"
45
+ :items="links"
46
+ :aria-label="t('nav.label', 'Account navigation')"
47
+ />
48
+ <AccountHeader />
49
+ <slot />
50
+ </div>
51
+ </UPage>
52
+ </UContainer>
53
+ </NuxtLayout>
54
+ </template>
@@ -0,0 +1,13 @@
1
+ declare var __VLS_47: {};
2
+ type __VLS_Slots = {} & {
3
+ default?: (props: typeof __VLS_47) => any;
4
+ };
5
+ declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
9
+ type __VLS_WithSlots<T, S> = T & {
10
+ new (): {
11
+ $slots: S;
12
+ };
13
+ };
@@ -1,72 +1,53 @@
1
1
  <script setup>
2
- import { z } from "zod";
2
+ import AccountApiTokenSlideover from "../components/Account/AccountApiTokenSlideover.vue";
3
3
  defineOptions({ name: "AccountApiTokensPage" });
4
4
  const t = useModuleT("account.apiTokens");
5
5
  const toast = useToast();
6
- const { data: tokens, refresh } = await useFetch("/api/auth/tokens", { key: "account-api-tokens", default: () => [] });
6
+ const showError = useErrorToast();
7
+ const confirm = useConfirmDialog();
8
+ const format = useFormat();
9
+ const { copy } = useCopyToClipboard();
10
+ const overlay = useOverlay();
11
+ const createSlideover = overlay.create(AccountApiTokenSlideover, { destroyOnClose: true });
12
+ const { data: tokens, refresh, status } = await useFetch("/api/auth/tokens", { key: "account-api-tokens", default: () => [] });
7
13
  const { data: scopes } = await useFetch("/api/auth/scopes", { key: "account-api-token-scopes", default: () => [] });
8
- const scopeItems = computed(() => (scopes.value ?? []).map((scope) => ({
9
- label: scope.name === "*" ? t("allScopes", "Full access") : scope.name,
10
- description: scope.description,
11
- value: scope.name
12
- })));
13
- const expiryItems = computed(() => [
14
- { label: t("expiry.30", "30 days"), value: 30 },
15
- { label: t("expiry.90", "90 days"), value: 90 },
16
- { label: t("expiry.365", "1 year"), value: 365 },
17
- { label: t("expiry.never", "No expiry"), value: 0 }
18
- ]);
19
- const schema = z.object({
20
- name: z.string().trim().min(1, t("name.required", "Give the token a name.")).max(80),
21
- scopes: z.array(z.string()).min(1, t("scopes.required", "Pick at least one scope.")),
22
- expiresInDays: z.number()
23
- });
24
- const state = reactive({ name: "", scopes: [], expiresInDays: 90 });
25
- const creating = ref(false);
26
- const revokingId = ref(null);
27
14
  const revealed = ref(null);
28
- function errorMessage(e, fallback) {
29
- const err = e;
30
- return err?.data?.statusMessage ?? err?.statusMessage ?? fallback;
15
+ async function onCreate() {
16
+ const created = await createSlideover.open({ scopes: scopes.value ?? [] });
17
+ if (!created) return;
18
+ revealed.value = { name: created.name, token: created.token };
19
+ toast.add({ title: t("created", "Token created"), description: created.name, color: "success", icon: "i-lucide-check" });
20
+ await refresh();
31
21
  }
32
- async function onCreate(event) {
33
- creating.value = true;
34
- try {
35
- const created = await $fetch("/api/auth/tokens", {
36
- method: "POST",
37
- body: { name: event.data.name, scopes: event.data.scopes, expiresInDays: event.data.expiresInDays || null }
38
- });
39
- revealed.value = { name: created.name, token: created.token };
40
- state.name = "";
41
- state.scopes = [];
42
- await refresh();
43
- } catch (e) {
44
- toast.add({ title: errorMessage(e, t("createError", "Could not create the token.")), color: "error" });
45
- } finally {
46
- creating.value = false;
47
- }
48
- }
49
- async function copyText(text, success) {
50
- try {
51
- await navigator.clipboard.writeText(text);
52
- toast.add({ title: success, color: "success" });
53
- } catch {
54
- toast.add({ title: t("copyError", "Could not copy. Select the token and copy it manually."), color: "warning" });
55
- }
56
- }
57
- const copyToken = () => revealed.value && copyText(revealed.value.token, t("copied", "Token copied."));
22
+ const copyToken = () => revealed.value && copy(revealed.value.token, t("copied", "Token copied."));
58
23
  async function onRevoke(token) {
59
- revokingId.value = token.id;
24
+ const confirmed = await confirm({
25
+ title: t("revokeConfirm.title", "Revoke {name}?", { name: token.name }),
26
+ description: t("revokeConfirm.description", "Scripts and tools using this token stop working immediately. This cannot be undone."),
27
+ confirmText: t("revoke", "Revoke token"),
28
+ destructive: true
29
+ });
30
+ if (!confirmed) return;
60
31
  try {
61
32
  await $fetch(`/api/auth/tokens/${token.id}`, { method: "DELETE" });
62
- toast.add({ title: t("revoked", "Token revoked."), color: "success" });
33
+ toast.add({ title: t("revoked", "Token revoked."), description: token.name, color: "success", icon: "i-lucide-check" });
63
34
  await refresh();
64
35
  } catch (e) {
65
- toast.add({ title: errorMessage(e, t("revokeError", "Could not revoke the token.")), color: "error" });
66
- } finally {
67
- revokingId.value = null;
36
+ showError(e, t("revokeError", "Could not revoke the token."));
68
37
  }
69
38
  }
39
+ const tokenMenu = (token) => token.status === "revoked" ? [] : [[{ label: t("revoke", "Revoke token"), icon: "i-lucide-ban", color: "error", onSelect: () => onRevoke(token) }]];
40
+ const columns = computed(() => [
41
+ { accessorKey: "name", header: t("list.name", "Name") },
42
+ { accessorKey: "scopes", header: t("scopes.label", "Scopes") },
43
+ { accessorKey: "createdAt", header: t("list.createdColumn", "Created") },
44
+ { accessorKey: "lastUsedAt", header: t("list.lastUsedColumn", "Last used") },
45
+ { accessorKey: "expiresAt", header: t("list.expiresColumn", "Expires") },
46
+ { id: "actions", meta: { class: { td: "w-px text-right" } } }
47
+ ]);
48
+ function tokenStatusColor(status2) {
49
+ return statusColor(status2, { ok: "success", expired: "warning" });
50
+ }
70
51
  const mcpEnabled = useRuntimeConfig().public.users?.mcp === true;
71
52
  const activeConnect = ref("claude");
72
53
  const mcpUrl = `${useRequestURL().origin}/mcp`;
@@ -85,224 +66,175 @@ const connectTabs = computed(() => [
85
66
  label: "ChatGPT",
86
67
  value: "chatgpt",
87
68
  steps: t("connect.chatgpt.steps", "ChatGPT: open Settings \u2192 Connectors \u2192 Advanced, turn on Developer mode, then create a connector with the URL below. Choose OAuth and sign in when asked. No token needed."),
88
- code: mcpUrl
69
+ code: mcpUrl,
70
+ extraSteps: void 0,
71
+ extraCode: void 0
89
72
  }
90
73
  ]);
91
- const dateFormatter = new Intl.DateTimeFormat(void 0, { dateStyle: "medium" });
92
- function formatDate(value) {
93
- if (!value) return "";
94
- const date = new Date(value);
95
- return Number.isNaN(date.getTime()) ? "" : dateFormatter.format(date);
96
- }
97
- function statusColor(status) {
98
- return status === "ok" ? "success" : status === "expired" ? "warning" : "neutral";
99
- }
74
+ const activeTab = computed(() => connectTabs.value.find((tab) => tab.value === activeConnect.value) ?? connectTabs.value[0]);
100
75
  </script>
101
76
 
102
77
  <template>
103
- <UContainer>
104
- <UPage>
105
- <UPageBody>
106
- <UPageCard
107
- :title="t('create.title', 'Create an API token')"
108
- :description="t('create.description', 'Tokens let scripts and tools such as MCP clients act as you. Give each one only the scopes it needs.')"
109
- variant="subtle"
110
- >
111
- <UAlert
112
- v-if="revealed"
113
- color="success"
114
- variant="subtle"
115
- icon="i-lucide-key-round"
116
- :title="t('reveal.title', 'Copy your new token now')"
117
- :description="t('reveal.description', 'This is the only time it is shown. If you lose it, create a new one.')"
78
+ <div class="flex flex-col gap-4">
79
+ <UAlert
80
+ v-if="revealed"
81
+ color="success"
82
+ variant="subtle"
83
+ icon="i-lucide-key-round"
84
+ :title="t('reveal.title', 'Copy your new token now')"
85
+ :description="t('reveal.description', 'This is the only time it is shown. If you lose it, create a new one.')"
86
+ >
87
+ <template #actions>
88
+ <UInput
89
+ :model-value="revealed.token"
90
+ readonly
91
+ class="w-full sm:w-96"
92
+ :aria-label="t('reveal.label', 'New token')"
93
+ @focus="$event.target.select()"
94
+ />
95
+ <UButton
96
+ icon="i-lucide-copy"
97
+ :label="t('copy', 'Copy token')"
98
+ @click="copyToken"
99
+ />
100
+ <UButton
101
+ color="neutral"
102
+ variant="ghost"
103
+ :label="t('reveal.done', 'Done')"
104
+ @click="revealed = null"
105
+ />
106
+ </template>
107
+ </UAlert>
108
+
109
+ <TableCard
110
+ :loading="status === 'pending' && !tokens?.length"
111
+ :refreshing="status === 'pending' && !!tokens?.length"
112
+ >
113
+ <template #header>
114
+ <div class="min-w-0">
115
+ <h2 class="text-sm font-semibold text-highlighted">
116
+ {{ t("list.title", "Your tokens") }}
117
+ </h2>
118
+ <p class="text-sm text-muted">
119
+ {{ t("list.description", "Tokens let scripts and tools such as MCP clients act as you. Revoke one to stop it working immediately.") }}
120
+ </p>
121
+ </div>
122
+ <UButton
123
+ icon="i-lucide-plus"
124
+ :label="t('create.submit', 'Create API token')"
125
+ @click="onCreate"
126
+ />
127
+ </template>
128
+
129
+ <UTable
130
+ :data="tokens ?? []"
131
+ :columns="columns"
132
+ :ui="tableUi({ tr: '' })"
133
+ >
134
+ <template #name-cell="{ row }">
135
+ <TableIdentity
136
+ :title="row.original.name"
137
+ :description="`${row.original.prefix}\u2026${row.original.last4}`"
118
138
  >
119
- <template #actions>
120
- <UInput
121
- :model-value="revealed.token"
122
- readonly
123
- class="w-full sm:w-96"
124
- :aria-label="t('reveal.label', 'New token')"
125
- @focus="$event.target.select()"
126
- />
127
- <UButton
128
- icon="i-lucide-copy"
129
- :label="t('copy', 'Copy')"
130
- @click="copyToken"
131
- />
132
- <UButton
133
- color="neutral"
134
- variant="ghost"
135
- :label="t('reveal.done', 'Done')"
136
- @click="revealed = null"
139
+ <template #badges>
140
+ <UBadge
141
+ :color="tokenStatusColor(row.original.status)"
142
+ variant="subtle"
143
+ size="sm"
144
+ :label="t(`status.${row.original.status}`, row.original.status)"
137
145
  />
138
146
  </template>
139
- </UAlert>
147
+ </TableIdentity>
148
+ </template>
149
+ <template #scopes-cell="{ row }">
150
+ <span class="text-muted">{{ row.original.scopes.join(", ") }}</span>
151
+ </template>
152
+ <template #createdAt-cell="{ row }">
153
+ <span class="tabular-nums">{{ format.date(row.original.createdAt) }}</span>
154
+ </template>
155
+ <template #lastUsedAt-cell="{ row }">
156
+ <span
157
+ v-if="row.original.lastUsedAt"
158
+ class="tabular-nums"
159
+ >{{ format.date(row.original.lastUsedAt) }}</span>
160
+ <span
161
+ v-else
162
+ class="text-dimmed"
163
+ >{{ t("list.neverUsed", "Never") }}</span>
164
+ </template>
165
+ <template #expiresAt-cell="{ row }">
166
+ <span
167
+ v-if="row.original.expiresAt"
168
+ class="tabular-nums"
169
+ >{{ format.date(row.original.expiresAt) }}</span>
170
+ <span
171
+ v-else
172
+ class="text-dimmed"
173
+ >{{ t("expiry.never", "No expiry") }}</span>
174
+ </template>
175
+ <template #actions-cell="{ row }">
176
+ <RowMenu
177
+ v-if="row.original.status !== 'revoked'"
178
+ :items="tokenMenu(row.original)"
179
+ :subject="row.original.name"
180
+ />
181
+ </template>
182
+ <template #empty>
183
+ <UEmpty
184
+ variant="naked"
185
+ icon="i-lucide-key-round"
186
+ :title="t('list.empty', 'You have not created any tokens yet.')"
187
+ :actions="[{ label: t('create.submit', 'Create API token'), icon: 'i-lucide-plus', onClick: onCreate }]"
188
+ />
189
+ </template>
190
+ </UTable>
191
+ </TableCard>
140
192
 
141
- <UForm
142
- :schema="schema"
143
- :state="state"
144
- class="space-y-4"
145
- @submit="onCreate"
146
- >
147
- <UFormField
148
- :label="t('name.label', 'Name')"
149
- name="name"
150
- required
151
- >
152
- <UInput
153
- v-model="state.name"
154
- :placeholder="t('name.placeholder', 'e.g. My MCP client')"
155
- maxlength="80"
156
- class="w-full"
157
- />
158
- </UFormField>
159
- <UFormField
160
- :label="t('scopes.label', 'Scopes')"
161
- name="scopes"
162
- required
163
- >
164
- <USelectMenu
165
- v-model="state.scopes"
166
- :items="scopeItems"
167
- value-key="value"
168
- multiple
169
- :placeholder="t('scopes.placeholder', 'Choose what the token may do')"
170
- class="w-full"
171
- />
172
- </UFormField>
173
- <UFormField
174
- :label="t('expiry.label', 'Expires')"
175
- name="expiresInDays"
176
- >
177
- <USelect
178
- v-model="state.expiresInDays"
179
- :items="expiryItems"
180
- value-key="value"
181
- class="w-full sm:max-w-xs"
182
- />
183
- </UFormField>
193
+ <SectionCard
194
+ v-if="mcpEnabled"
195
+ :title="t('connect.title', 'Connect to MCP')"
196
+ :description="t('connect.description', 'Use your account from an AI assistant. Your API token is only filled into the commands right after you create it.')"
197
+ >
198
+ <div class="flex min-w-0 flex-col gap-3">
199
+ <PageTabs
200
+ v-model="activeConnect"
201
+ :items="connectTabs.map((tab) => ({ label: tab.label, value: tab.value }))"
202
+ />
203
+ <template v-if="activeTab">
204
+ <p class="text-sm text-muted">
205
+ {{ activeTab.steps }}
206
+ </p>
207
+ <div class="relative min-w-0">
208
+ <pre class="max-w-full whitespace-pre-wrap break-all rounded-md bg-elevated p-3 pr-12 text-sm"><code>{{ activeTab.code }}</code></pre>
184
209
  <UButton
185
- type="submit"
186
- :loading="creating"
187
- :label="t('create.submit', 'Create token')"
210
+ class="absolute right-2 top-2"
211
+ color="neutral"
212
+ variant="ghost"
213
+ size="xs"
214
+ icon="i-lucide-copy"
215
+ :aria-label="t('copySnippet', 'Copy command')"
216
+ @click="copy(activeTab.code, t('copiedSnippet', 'Copied.'))"
188
217
  />
189
- </UForm>
190
- </UPageCard>
191
-
192
- <UPageCard
193
- v-if="mcpEnabled"
194
- :title="t('connect.title', 'Connect to MCP')"
195
- :description="t('connect.description', 'Use your account from an AI assistant. Your API token is only filled into the commands right after you create it.')"
196
- variant="subtle"
197
- >
198
- <UTabs
199
- v-model="activeConnect"
200
- :items="connectTabs"
201
- :content="false"
202
- class="w-full"
203
- />
204
- <template
205
- v-for="tab in connectTabs"
206
- :key="tab.value"
207
- >
208
- <div
209
- v-if="tab.value === activeConnect"
210
- class="min-w-0 max-w-full space-y-3"
211
- >
212
- <p class="text-sm text-muted">
213
- {{ tab.steps }}
214
- </p>
215
- <div class="relative min-w-0">
216
- <pre class="max-w-full whitespace-pre-wrap break-all rounded-md bg-elevated p-3 pr-12 text-sm"><code>{{ tab.code }}</code></pre>
217
- <UButton
218
- class="absolute right-2 top-2"
219
- color="neutral"
220
- variant="ghost"
221
- size="xs"
222
- icon="i-lucide-copy"
223
- :aria-label="t('copy', 'Copy')"
224
- @click="copyText(tab.code, t('copiedSnippet', 'Copied.'))"
225
- />
226
- </div>
227
- <template v-if="tab.extraCode">
228
- <p class="text-sm text-muted">
229
- {{ tab.extraSteps }}
230
- </p>
231
- <div class="relative min-w-0">
232
- <pre class="max-w-full whitespace-pre-wrap break-all rounded-md bg-elevated p-3 pr-12 text-sm"><code>{{ tab.extraCode }}</code></pre>
233
- <UButton
234
- class="absolute right-2 top-2"
235
- color="neutral"
236
- variant="ghost"
237
- size="xs"
238
- icon="i-lucide-copy"
239
- :aria-label="t('copy', 'Copy')"
240
- @click="copyText(tab.extraCode ?? '', t('copiedSnippet', 'Copied.'))"
241
- />
242
- </div>
243
- </template>
244
- </div>
245
- </template>
246
- </UPageCard>
247
-
248
- <UPageCard
249
- :title="t('list.title', 'Your tokens')"
250
- :description="t('list.description', 'Revoke a token to stop it working immediately.')"
251
- variant="subtle"
252
- >
253
- <ul
254
- v-if="tokens?.length"
255
- class="divide-y divide-default"
256
- >
257
- <li
258
- v-for="token in tokens"
259
- :key="token.id"
260
- class="flex items-center justify-between gap-3 py-3"
261
- >
262
- <div class="min-w-0">
263
- <p class="flex items-center gap-2 truncate font-medium text-highlighted">
264
- {{ token.name }}
265
- <UBadge
266
- :color="statusColor(token.status)"
267
- variant="subtle"
268
- :label="t(`status.${token.status}`, token.status)"
269
- />
270
- </p>
271
- <p class="font-mono text-sm text-muted">
272
- {{ token.prefix }}…{{ token.last4 }}
273
- </p>
274
- <p class="text-sm text-muted">
275
- {{ token.scopes.join(", ") }}
276
- </p>
277
- <p class="text-sm text-muted">
278
- {{ t("list.created", "Created {date}", { date: formatDate(token.createdAt) }) }}
279
- <template v-if="token.lastUsedAt">
280
- · {{ t("list.lastUsed", "Last used {date}", { date: formatDate(token.lastUsedAt) }) }}
281
- </template>
282
- <template v-if="token.expiresAt">
283
- · {{ t("list.expires", "Expires {date}", { date: formatDate(token.expiresAt) }) }}
284
- </template>
285
- </p>
286
- </div>
218
+ </div>
219
+ <template v-if="activeTab.extraCode">
220
+ <p class="text-sm text-muted">
221
+ {{ activeTab.extraSteps }}
222
+ </p>
223
+ <div class="relative min-w-0">
224
+ <pre class="max-w-full whitespace-pre-wrap break-all rounded-md bg-elevated p-3 pr-12 text-sm"><code>{{ activeTab.extraCode }}</code></pre>
287
225
  <UButton
288
- v-if="token.status !== 'revoked'"
289
- color="error"
226
+ class="absolute right-2 top-2"
227
+ color="neutral"
290
228
  variant="ghost"
291
- icon="i-lucide-trash-2"
292
- :loading="revokingId === token.id"
293
- :aria-label="t('revoke', 'Revoke token')"
294
- @click="onRevoke(token)"
229
+ size="xs"
230
+ icon="i-lucide-copy"
231
+ :aria-label="t('copySnippet', 'Copy command')"
232
+ @click="copy(activeTab.extraCode, t('copiedSnippet', 'Copied.'))"
295
233
  />
296
- </li>
297
- </ul>
298
- <p
299
- v-else
300
- class="text-sm text-muted"
301
- >
302
- {{ t("list.empty", "You have not created any tokens yet.") }}
303
- </p>
304
- </UPageCard>
305
- </UPageBody>
306
- </UPage>
307
- </UContainer>
234
+ </div>
235
+ </template>
236
+ </template>
237
+ </div>
238
+ </SectionCard>
239
+ </div>
308
240
  </template>
@@ -9,30 +9,29 @@ const documents = computed(() => visibleDocuments(useRuntimeConfig().public.acco
9
9
  </script>
10
10
 
11
11
  <template>
12
- <UContainer>
13
- <UPage>
14
- <UPageBody>
15
- <UPageGrid v-if="documents.length">
16
- <UPageCard
17
- v-for="document in documents"
18
- :key="document.id"
19
- :title="documentLabel(document, locale)"
20
- :icon="document.icon ?? 'i-lucide-file'"
21
- >
22
- <UButton
23
- download
24
- external
25
- :to="document.path"
26
- icon="i-lucide-download"
27
- :label="t('download', 'Download')"
28
- />
29
- </UPageCard>
30
- </UPageGrid>
31
- <UEmpty
32
- v-else
33
- :title="t('empty', 'No documents for you yet.')"
34
- />
35
- </UPageBody>
36
- </UPage>
37
- </UContainer>
12
+ <div class="flex flex-col gap-4">
13
+ <SectionCard>
14
+ <UPageGrid v-if="documents.length">
15
+ <UPageCard
16
+ v-for="document in documents"
17
+ :key="document.id"
18
+ :title="documentLabel(document, locale)"
19
+ :icon="document.icon ?? 'i-lucide-file'"
20
+ >
21
+ <UButton
22
+ download
23
+ external
24
+ :to="document.path"
25
+ icon="i-lucide-download"
26
+ :label="t('download', 'Download')"
27
+ />
28
+ </UPageCard>
29
+ </UPageGrid>
30
+ <UEmpty
31
+ v-else
32
+ variant="naked"
33
+ :title="t('empty', 'No documents for you yet.')"
34
+ />
35
+ </SectionCard>
36
+ </div>
38
37
  </template>