@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.
- package/README.md +7 -4
- package/dist/module.d.mts +5 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +32 -26
- package/dist/runtime/app/components/Account/AccountApiTokenSlideover.d.vue.ts +11 -0
- package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue +95 -0
- package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue.d.ts +11 -0
- package/dist/runtime/app/components/Account/AccountHeader.vue +14 -34
- package/dist/runtime/app/components/Account/AccountMenu.d.vue.ts +10 -1
- package/dist/runtime/app/components/Account/AccountMenu.vue +100 -51
- package/dist/runtime/app/components/Account/AccountMenu.vue.d.ts +10 -1
- package/dist/runtime/app/composables/index.d.ts +2 -0
- package/dist/runtime/app/composables/index.js +1 -0
- package/dist/runtime/app/composables/useAccountNav.d.ts +20 -0
- package/dist/runtime/app/composables/useAccountNav.js +18 -0
- package/dist/runtime/app/layouts/account.d.vue.ts +13 -0
- package/dist/runtime/app/layouts/account.vue +54 -0
- package/dist/runtime/app/layouts/account.vue.d.ts +13 -0
- package/dist/runtime/app/pages/api-tokens.vue +193 -261
- package/dist/runtime/app/pages/documents.vue +25 -26
- package/dist/runtime/app/pages/index.vue +73 -17
- package/dist/runtime/app/pages/profile.vue +152 -169
- package/dist/runtime/app/pages/security.vue +156 -151
- package/dist/runtime/app/utils/api-tokens.d.ts +19 -0
- package/dist/runtime/app/utils/api-tokens.js +0 -0
- package/dist/runtime/i18n/lang/da.json +41 -9
- package/dist/runtime/i18n/lang/en.json +41 -9
- package/dist/runtime/menus.d.ts +1 -1
- package/dist/runtime/menus.js +10 -0
- package/package.json +21 -15
- package/dist/runtime/app/pages/layout.d.vue.ts +0 -3
- package/dist/runtime/app/pages/layout.vue +0 -10
- package/dist/runtime/app/pages/layout.vue.d.ts +0 -3
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@ A module for the account area (profile, security, documents, API tokens pages) i
|
|
|
10
10
|
- 🔒 Change your password.
|
|
11
11
|
- 🔑 Manage passkeys (via `@frigear-nu/auth`'s `usePasskeys`, when WebAuthn is enabled there).
|
|
12
12
|
- 🧩 Overridable composable (`useAccount`) and opt-in profile/security pages (built with [`@nuxt/ui`](https://ui.nuxt.com)).
|
|
13
|
-
- 🗂️
|
|
14
|
-
listing
|
|
13
|
+
- 🗂️ An `account` layout (inside the site layout) with a left account navigation built from the `account` menu
|
|
14
|
+
of `@frigear-nu/menu` (when installed), an overview page listing the same entries, and an `account.overview` widget
|
|
15
|
+
area (when `@frigear-nu/widget` is installed).
|
|
15
16
|
|
|
16
17
|
This module is a UI shell: it owns no tables and no mutation routes. The forms call the self-service routes of
|
|
17
18
|
`@frigear-nu/users` (`/api/users/me/*`) and the token routes of `@frigear-nu/auth`.
|
|
@@ -36,7 +37,7 @@ export default defineNuxtConfig({
|
|
|
36
37
|
modules: [
|
|
37
38
|
'@nuxthub/core',
|
|
38
39
|
'@frigear-nu/users',
|
|
39
|
-
'@frigear-nu/menu', // optional - enables the
|
|
40
|
+
'@frigear-nu/menu', // optional - enables the account navigation and overview page
|
|
40
41
|
'@frigear-nu/account',
|
|
41
42
|
],
|
|
42
43
|
account: {
|
|
@@ -78,4 +79,6 @@ Strings live under the `account.*` namespace in `da`/`en` locale files and can b
|
|
|
78
79
|
## Overriding parts
|
|
79
80
|
|
|
80
81
|
Any `Account*` component can be replaced by a same-named component in your app (the module's have a lower priority).
|
|
81
|
-
`AccountHeaderRight` is the block
|
|
82
|
+
`AccountHeaderRight` is the actions block of the account page heading (and of the overview's summary card);
|
|
83
|
+
override it to show other details. Set `account.layout: false` to opt out of the account layout and render the
|
|
84
|
+
pages in your own.
|
package/dist/module.d.mts
CHANGED
|
@@ -26,6 +26,11 @@ type PublicModuleOptions = {
|
|
|
26
26
|
};
|
|
27
27
|
type ModuleOptions = {
|
|
28
28
|
settings?: {
|
|
29
|
+
/**
|
|
30
|
+
* Renders `/account`, `/account/**` and the configured page paths in the `account` layout (the site layout with
|
|
31
|
+
* a left account navigation). `false` leaves the layout to the host. Defaults to `true`.
|
|
32
|
+
*/
|
|
33
|
+
layout?: boolean;
|
|
29
34
|
pages?: {
|
|
30
35
|
index?: string | boolean;
|
|
31
36
|
profile?: string | boolean;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, hasNuxtModule, addImports,
|
|
2
|
-
import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, addOptionalMenuSource } from '@frigear-nu/module-kit/utils';
|
|
1
|
+
import { defineNuxtModule, createResolver, hasNuxtModule, addImports, addLayout, addComponentsDir } from '@nuxt/kit';
|
|
2
|
+
import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, addModulePages, addOptionalMenuSource } from '@frigear-nu/module-kit/utils';
|
|
3
3
|
|
|
4
4
|
const module$1 = defineNuxtModule({
|
|
5
5
|
meta: { name: "@frigear-nu/account", configKey: "account" },
|
|
@@ -18,9 +18,16 @@ const module$1 = defineNuxtModule({
|
|
|
18
18
|
"@frigear-nu/users": {
|
|
19
19
|
version: ">=0.0.1"
|
|
20
20
|
},
|
|
21
|
+
// Page pattern components (`PageHeading`, `SectionCard`, ...), `useNavLinks` and the error/format helpers.
|
|
22
|
+
"@frigear-nu/ui": { version: ">=0.0.0" },
|
|
21
23
|
"@frigear-nu/menu": {
|
|
22
24
|
version: ">=0.0.1",
|
|
23
25
|
optional: true
|
|
26
|
+
},
|
|
27
|
+
// The overview renders the `account.overview` widget area when it is installed.
|
|
28
|
+
"@frigear-nu/widget": {
|
|
29
|
+
version: ">=0.0.0",
|
|
30
|
+
optional: true
|
|
24
31
|
}
|
|
25
32
|
},
|
|
26
33
|
async setup(options, nuxt) {
|
|
@@ -30,7 +37,10 @@ const module$1 = defineNuxtModule({
|
|
|
30
37
|
registerLocales(nuxt, resolver.resolve("./runtime/i18n/lang"));
|
|
31
38
|
const settings = options.settings ?? {};
|
|
32
39
|
const withMenus = hasNuxtModule("@frigear-nu/menu");
|
|
33
|
-
addImports([
|
|
40
|
+
addImports([
|
|
41
|
+
{ name: "useAccount", from: resolver.resolve("./runtime/app/composables/useAccount") },
|
|
42
|
+
{ name: "useAccountNav", from: resolver.resolve("./runtime/app/composables/useAccountNav") }
|
|
43
|
+
]);
|
|
34
44
|
const profilePath = resolvePagePath(settings.pages?.profile, "/account/profile");
|
|
35
45
|
const securityPath = resolvePagePath(settings.pages?.security, "/account/security");
|
|
36
46
|
const documentsPath = resolvePagePath(settings.pages?.documents, "/account/documents");
|
|
@@ -43,36 +53,32 @@ const module$1 = defineNuxtModule({
|
|
|
43
53
|
pages: { index: indexPath, profile: profilePath, security: securityPath, documents: documentsPath, apiTokens: apiTokensPath },
|
|
44
54
|
documents: settings.documents ?? []
|
|
45
55
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
}
|
|
56
|
+
addModulePages("@frigear-nu/account", resolver, [
|
|
57
|
+
{ name: "account-index", path: indexPath, file: "./runtime/app/pages/index.vue" },
|
|
58
|
+
{ name: "account-profile", path: profilePath, file: "./runtime/app/pages/profile.vue" },
|
|
59
|
+
{ name: "account-documents", path: documentsPath, file: "./runtime/app/pages/documents.vue" },
|
|
60
|
+
{ name: "account-security", path: securityPath, file: "./runtime/app/pages/security.vue" },
|
|
61
|
+
{ name: "account-api-tokens", path: apiTokensPath, file: "./runtime/app/pages/api-tokens.vue" }
|
|
62
|
+
]);
|
|
63
|
+
const prefixes = ["", ...(nuxt.options.i18n?.locales ?? []).map((locale) => typeof locale === "string" ? locale : locale.code).map((code) => `/${code}`)];
|
|
64
|
+
const outside = [indexPath, profilePath, securityPath, documentsPath, apiTokensPath].filter((path) => !!path && path !== "/account" && !path.startsWith("/account/"));
|
|
65
|
+
const areaPatterns = prefixes.flatMap((prefix) => [`${prefix}/account`, `${prefix}/account/**`, ...outside.map((path) => `${prefix}${path}`)]);
|
|
66
|
+
if (settings.layout !== false) {
|
|
67
|
+
addLayout({ src: resolver.resolve("./runtime/app/layouts/account.vue"), filename: "account.vue" }, "account");
|
|
68
|
+
const rules = nuxt.options.routeRules ||= {};
|
|
69
|
+
for (const pattern of areaPatterns) {
|
|
70
|
+
rules[pattern] = { appLayout: "account", ...rules[pattern] };
|
|
71
|
+
}
|
|
62
72
|
}
|
|
63
73
|
if (hasNuxtModule("@frigear-nu/authorization")) {
|
|
64
|
-
const prefixes = ["", ...(nuxt.options.i18n?.locales ?? []).map((locale) => typeof locale === "string" ? locale : locale.code).map((code) => `/${code}`)];
|
|
65
74
|
const rules = nuxt.options.routeRules ||= {};
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
for (const pattern of [`${prefix}/account`, `${prefix}/account/**`, ...guarded.map((path) => `${prefix}${path}`)]) {
|
|
69
|
-
rules[pattern] = { authenticated: true, ...rules[pattern] };
|
|
70
|
-
}
|
|
75
|
+
for (const pattern of areaPatterns) {
|
|
76
|
+
rules[pattern] = { authenticated: true, ...rules[pattern] };
|
|
71
77
|
}
|
|
72
78
|
} else {
|
|
73
79
|
console.warn("[@frigear-nu/account] @frigear-nu/authorization is not installed: the account pages are not guarded. Install it and guard `/account/**` yourself.");
|
|
74
80
|
}
|
|
75
|
-
addOptionalMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "
|
|
81
|
+
addOptionalMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "account" });
|
|
76
82
|
addComponentsDir({
|
|
77
83
|
path: resolver.resolve("./runtime/app/components"),
|
|
78
84
|
priority: -1
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ApiTokenScope, CreatedApiToken } from '../../utils/api-tokens.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
scopes: ApiTokenScope[];
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
|
+
close: (created: false | CreatedApiToken) => any;
|
|
7
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
8
|
+
onClose?: ((created: false | CreatedApiToken) => any) | undefined;
|
|
9
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
10
|
+
declare const _default: typeof __VLS_export;
|
|
11
|
+
export default _default;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
scopes: { type: Array, required: true }
|
|
5
|
+
});
|
|
6
|
+
const emit = defineEmits(["close"]);
|
|
7
|
+
const t = useModuleT("account.apiTokens");
|
|
8
|
+
const { busy, run } = useAction();
|
|
9
|
+
const scopeItems = computed(() => props.scopes.map((scope) => ({
|
|
10
|
+
label: scope.name === "*" ? t("allScopes", "Full access") : scope.name,
|
|
11
|
+
description: scope.description,
|
|
12
|
+
value: scope.name
|
|
13
|
+
})));
|
|
14
|
+
const expiryItems = computed(() => [
|
|
15
|
+
{ label: t("expiry.30", "30 days"), value: 30 },
|
|
16
|
+
{ label: t("expiry.90", "90 days"), value: 90 },
|
|
17
|
+
{ label: t("expiry.365", "1 year"), value: 365 },
|
|
18
|
+
{ label: t("expiry.never", "No expiry"), value: 0 }
|
|
19
|
+
]);
|
|
20
|
+
const schema = z.object({
|
|
21
|
+
name: z.string().trim().min(1, t("name.required", "Give the token a name.")).max(80),
|
|
22
|
+
scopes: z.array(z.string()).min(1, t("scopes.required", "Pick at least one scope.")),
|
|
23
|
+
expiresInDays: z.number()
|
|
24
|
+
});
|
|
25
|
+
const state = reactive({ name: "", scopes: [], expiresInDays: 90 });
|
|
26
|
+
async function onSubmit(event) {
|
|
27
|
+
const created = await run(
|
|
28
|
+
() => $fetch("/api/auth/tokens", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
body: { name: event.data.name, scopes: event.data.scopes, expiresInDays: event.data.expiresInDays || null }
|
|
31
|
+
}),
|
|
32
|
+
{ error: t("createError", "Could not create the token.") }
|
|
33
|
+
);
|
|
34
|
+
if (created) emit("close", created);
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<FormSlideover
|
|
40
|
+
form-id="account-api-token"
|
|
41
|
+
:title="t('create.title', 'Create API token')"
|
|
42
|
+
:description="t('create.description', 'Tokens let scripts and tools such as MCP clients act as you. Give each one only the scopes it needs.')"
|
|
43
|
+
:submit-label="t('create.submit', 'Create API token')"
|
|
44
|
+
:loading="busy"
|
|
45
|
+
@close="emit('close', false)"
|
|
46
|
+
>
|
|
47
|
+
<UForm
|
|
48
|
+
id="account-api-token"
|
|
49
|
+
:schema="schema"
|
|
50
|
+
:state="state"
|
|
51
|
+
class="flex flex-col gap-4"
|
|
52
|
+
@submit="onSubmit"
|
|
53
|
+
>
|
|
54
|
+
<UFormField
|
|
55
|
+
:label="t('name.label', 'Name')"
|
|
56
|
+
:description="t('name.description', 'So you recognise the token later.')"
|
|
57
|
+
name="name"
|
|
58
|
+
required
|
|
59
|
+
>
|
|
60
|
+
<UInput
|
|
61
|
+
v-model="state.name"
|
|
62
|
+
:placeholder="t('name.placeholder', 'e.g. My MCP client')"
|
|
63
|
+
maxlength="80"
|
|
64
|
+
class="w-full"
|
|
65
|
+
/>
|
|
66
|
+
</UFormField>
|
|
67
|
+
<UFormField
|
|
68
|
+
:label="t('scopes.label', 'Scopes')"
|
|
69
|
+
:description="t('scopes.description', 'What the token may do.')"
|
|
70
|
+
name="scopes"
|
|
71
|
+
required
|
|
72
|
+
>
|
|
73
|
+
<USelectMenu
|
|
74
|
+
v-model="state.scopes"
|
|
75
|
+
:items="scopeItems"
|
|
76
|
+
value-key="value"
|
|
77
|
+
multiple
|
|
78
|
+
:placeholder="t('scopes.placeholder', 'Choose what the token may do')"
|
|
79
|
+
class="w-full"
|
|
80
|
+
/>
|
|
81
|
+
</UFormField>
|
|
82
|
+
<UFormField
|
|
83
|
+
:label="t('expiry.label', 'Expires')"
|
|
84
|
+
name="expiresInDays"
|
|
85
|
+
>
|
|
86
|
+
<USelect
|
|
87
|
+
v-model="state.expiresInDays"
|
|
88
|
+
:items="expiryItems"
|
|
89
|
+
value-key="value"
|
|
90
|
+
class="w-full"
|
|
91
|
+
/>
|
|
92
|
+
</UFormField>
|
|
93
|
+
</UForm>
|
|
94
|
+
</FormSlideover>
|
|
95
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ApiTokenScope, CreatedApiToken } from '../../utils/api-tokens.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
scopes: ApiTokenScope[];
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
|
+
close: (created: false | CreatedApiToken) => any;
|
|
7
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
8
|
+
onClose?: ((created: false | CreatedApiToken) => any) | undefined;
|
|
9
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
10
|
+
declare const _default: typeof __VLS_export;
|
|
11
|
+
export default _default;
|
|
@@ -1,43 +1,23 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
const t = useModuleT("account");
|
|
3
3
|
const route = useRoute();
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const stripLocale = (path) => {
|
|
9
|
-
const trimmed = trimSlash(path);
|
|
10
|
-
const first = trimmed.split("/")[1];
|
|
11
|
-
const codes = (nuxtApp.$i18n?.locales?.value ?? []).map((locale) => typeof locale === "string" ? locale : locale.code);
|
|
12
|
-
return first && codes.includes(first) ? trimmed.slice(first.length + 1) || "/" : trimmed;
|
|
13
|
-
};
|
|
14
|
-
const current = computed(() => {
|
|
15
|
-
const path = stripLocale(route.path);
|
|
16
|
-
return (items.value ?? []).find((item) => item.to && !item.external && stripLocale(item.to) === path);
|
|
4
|
+
const { current } = await useAccountNav();
|
|
5
|
+
const title = computed(() => {
|
|
6
|
+
const meta = route.meta.title;
|
|
7
|
+
return typeof meta === "string" && meta || current.value?.label || t("title", "Account");
|
|
17
8
|
});
|
|
18
|
-
const title = computed(() => current.value?.label ?? t("title", "Account"));
|
|
19
9
|
const description = computed(() => current.value?.description);
|
|
10
|
+
useHead({ title });
|
|
20
11
|
</script>
|
|
21
12
|
|
|
22
13
|
<template>
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
>
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
icon="i-lucide-arrow-left"
|
|
33
|
-
:to="pages.index"
|
|
34
|
-
:label="t('backTo', 'Back to account')"
|
|
35
|
-
class="-ml-3 mt-2"
|
|
36
|
-
/>
|
|
37
|
-
|
|
38
|
-
<template #links>
|
|
39
|
-
<AccountHeaderRight />
|
|
40
|
-
</template>
|
|
41
|
-
</UPageHeader>
|
|
42
|
-
</UContainer>
|
|
14
|
+
<PageHeading
|
|
15
|
+
v-if="route.meta.heading !== false"
|
|
16
|
+
:title="title"
|
|
17
|
+
:description="description"
|
|
18
|
+
>
|
|
19
|
+
<template #actions>
|
|
20
|
+
<AccountHeaderRight />
|
|
21
|
+
</template>
|
|
22
|
+
</PageHeading>
|
|
43
23
|
</template>
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The signed-in user's menu: who is signed in, the `account.dropdown` menu (account, admin, sign out), and the
|
|
3
|
+
* colour mode under Appearance. The entry for the area you are in is left out. `block` renders it as a full-width
|
|
4
|
+
* row for a sidebar footer (the admin shell), `collapsed` shrinks that row to the avatar.
|
|
5
|
+
*/
|
|
6
|
+
type __VLS_Props = {
|
|
7
|
+
block?: boolean;
|
|
8
|
+
collapsed?: boolean;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
2
11
|
declare const _default: typeof __VLS_export;
|
|
3
12
|
export default _default;
|
|
@@ -1,69 +1,118 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
block: { type: Boolean, required: false },
|
|
4
|
+
collapsed: { type: Boolean, required: false }
|
|
5
|
+
});
|
|
6
|
+
const t = useModuleT("account");
|
|
3
7
|
const { user } = useUserSession();
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
8
|
+
const route = useRoute();
|
|
9
|
+
const colorMode = useColorMode();
|
|
10
|
+
const { localize, strip } = useLocaleLink();
|
|
11
|
+
const dropdown = typeof useMenu === "function" ? useMenu("account.dropdown").data : ref([]);
|
|
12
|
+
const sessionUser = computed(() => user.value);
|
|
13
|
+
const displayName = computed(() => sessionUser.value?.name || sessionUser.value?.email || String(sessionUser.value?.id ?? t("title", "Account")));
|
|
14
|
+
const avatar = computed(() => ({ src: sessionUser.value?.avatar || void 0, alt: displayName.value }));
|
|
15
|
+
const role = computed(() => sessionUser.value?.role || void 0);
|
|
16
|
+
const roleLabel = computed(() => role.value && role.value !== "user" ? role.value.charAt(0).toUpperCase() + role.value.slice(1) : void 0);
|
|
17
|
+
const isHere = (to) => {
|
|
18
|
+
const target = strip(to);
|
|
19
|
+
const current = strip(route.path);
|
|
20
|
+
return current === target || target !== "/" && current.startsWith(`${target}/`);
|
|
7
21
|
};
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
const toItem = (item) => ({
|
|
23
|
+
label: item.label,
|
|
24
|
+
icon: item.icon,
|
|
25
|
+
to: item.to && !item.external ? localize(item.to) : item.to,
|
|
26
|
+
external: item.external,
|
|
27
|
+
replace: item.external
|
|
28
|
+
});
|
|
29
|
+
const appearance = (label, value, icon) => ({
|
|
30
|
+
label,
|
|
31
|
+
icon,
|
|
32
|
+
type: "checkbox",
|
|
33
|
+
checked: colorMode.preference === value,
|
|
34
|
+
onUpdateChecked: () => {
|
|
35
|
+
colorMode.preference = value;
|
|
21
36
|
}
|
|
22
|
-
|
|
23
|
-
|
|
37
|
+
});
|
|
38
|
+
const items = computed(() => {
|
|
39
|
+
const visible = (dropdown.value ?? []).filter((item) => {
|
|
24
40
|
const roles = item.meta?.roles;
|
|
25
|
-
|
|
41
|
+
if (roles?.length && !(role.value && roles.includes(role.value))) return false;
|
|
42
|
+
return !item.to || item.external || !isHere(item.to);
|
|
26
43
|
});
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
const last = (item) => (item.order ?? 0) >= 99;
|
|
45
|
+
return [
|
|
46
|
+
sessionUser.value ? [{ type: "label", label: displayName.value, slot: "account" }] : [],
|
|
47
|
+
visible.filter((item) => !last(item)).map(toItem),
|
|
48
|
+
[{
|
|
49
|
+
label: t("appearance.label", "Appearance"),
|
|
50
|
+
icon: colorMode.preference === "dark" ? "i-lucide-moon" : colorMode.preference === "light" ? "i-lucide-sun" : "i-lucide-monitor",
|
|
51
|
+
children: [
|
|
52
|
+
appearance(t("appearance.light", "Light"), "light", "i-lucide-sun"),
|
|
53
|
+
appearance(t("appearance.dark", "Dark"), "dark", "i-lucide-moon"),
|
|
54
|
+
appearance(t("appearance.system", "System"), "system", "i-lucide-monitor")
|
|
55
|
+
]
|
|
56
|
+
}],
|
|
57
|
+
visible.filter(last).map(toItem)
|
|
58
|
+
].filter((group) => group.length);
|
|
35
59
|
});
|
|
36
60
|
</script>
|
|
37
61
|
|
|
38
62
|
<template>
|
|
39
63
|
<UDropdownMenu
|
|
40
64
|
:items="items"
|
|
41
|
-
:content="{ align: 'start' }"
|
|
42
|
-
:ui="{ content: 'w-
|
|
65
|
+
:content="{ align: props.block ? 'start' : 'end', side: props.block ? 'top' : 'bottom', collisionPadding: 12 }"
|
|
66
|
+
:ui="{ content: props.block && !props.collapsed ? 'w-(--reka-dropdown-menu-trigger-width) min-w-56' : 'w-56' }"
|
|
43
67
|
>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
<UButton
|
|
69
|
+
v-if="props.block"
|
|
70
|
+
:label="props.collapsed ? void 0 : displayName"
|
|
71
|
+
:avatar="avatar"
|
|
72
|
+
:trailing-icon="props.collapsed ? void 0 : 'i-lucide-chevrons-up-down'"
|
|
73
|
+
:square="props.collapsed"
|
|
74
|
+
:block="!props.collapsed"
|
|
75
|
+
:aria-label="t('accountMenu', 'Account menu')"
|
|
76
|
+
color="neutral"
|
|
77
|
+
variant="ghost"
|
|
78
|
+
class="data-[state=open]:bg-elevated"
|
|
79
|
+
:ui="{ trailingIcon: 'text-dimmed ms-auto', label: 'truncate' }"
|
|
80
|
+
/>
|
|
81
|
+
<UButton
|
|
82
|
+
v-else
|
|
83
|
+
:avatar="avatar"
|
|
84
|
+
:aria-label="t('accountMenu', 'Account menu')"
|
|
85
|
+
color="neutral"
|
|
86
|
+
variant="ghost"
|
|
87
|
+
square
|
|
88
|
+
class="data-[state=open]:bg-elevated"
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
<template #account-label>
|
|
92
|
+
<UUser
|
|
93
|
+
:name="displayName"
|
|
94
|
+
:avatar="avatar"
|
|
95
|
+
size="sm"
|
|
96
|
+
:ui="{ name: 'truncate', description: 'truncate' }"
|
|
55
97
|
>
|
|
56
|
-
<template
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
98
|
+
<template
|
|
99
|
+
v-if="roleLabel || sessionUser?.name && sessionUser?.email"
|
|
100
|
+
#description
|
|
101
|
+
>
|
|
102
|
+
<span class="flex min-w-0 items-center gap-1.5">
|
|
103
|
+
<UBadge
|
|
104
|
+
v-if="roleLabel"
|
|
105
|
+
size="xs"
|
|
106
|
+
variant="subtle"
|
|
107
|
+
:label="roleLabel"
|
|
108
|
+
/>
|
|
109
|
+
<span
|
|
110
|
+
v-if="sessionUser?.name && sessionUser?.email"
|
|
111
|
+
class="truncate"
|
|
112
|
+
>{{ sessionUser.email }}</span>
|
|
113
|
+
</span>
|
|
65
114
|
</template>
|
|
66
|
-
</
|
|
115
|
+
</UUser>
|
|
67
116
|
</template>
|
|
68
117
|
</UDropdownMenu>
|
|
69
118
|
</template>
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The signed-in user's menu: who is signed in, the `account.dropdown` menu (account, admin, sign out), and the
|
|
3
|
+
* colour mode under Appearance. The entry for the area you are in is left out. `block` renders it as a full-width
|
|
4
|
+
* row for a sidebar footer (the admin shell), `collapsed` shrinks that row to the avatar.
|
|
5
|
+
*/
|
|
6
|
+
type __VLS_Props = {
|
|
7
|
+
block?: boolean;
|
|
8
|
+
collapsed?: boolean;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
2
11
|
declare const _default: typeof __VLS_export;
|
|
3
12
|
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** An entry of the account navigation: an item of the `account` menu, or a configured page without the menu module. */
|
|
2
|
+
export interface AccountNavItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
icon?: string;
|
|
7
|
+
to: string;
|
|
8
|
+
external?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The account navigation: the items of the `account` menu (so pages other modules add show up too), or, without
|
|
12
|
+
* `@frigear-nu/menu`, the account pages enabled in `account.settings.pages`. `links` are ready for
|
|
13
|
+
* `UNavigationMenu`/`PageTabs` (localised, `active` set, the overview only active on its own path) and `current` is
|
|
14
|
+
* the item the route belongs to. Await it, so the menu is resolved before the page renders on the server.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useAccountNav(): Promise<{
|
|
17
|
+
items: any;
|
|
18
|
+
links: any;
|
|
19
|
+
current: any;
|
|
20
|
+
}>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function useAccountNav() {
|
|
2
|
+
const t = useModuleT("account");
|
|
3
|
+
const pages = useRuntimeConfig().public.account?.pages;
|
|
4
|
+
const { isActive, strip } = useLocaleLink();
|
|
5
|
+
const menu = typeof useMenu === "function" ? useMenu("account") : void 0;
|
|
6
|
+
const fallback = () => [
|
|
7
|
+
{ id: "account-overview", to: pages?.index, label: t("menu.overview.label", "Overview"), description: t("menu.overview.description", "A summary of your account."), icon: "i-lucide-layout-dashboard" },
|
|
8
|
+
{ id: "profile", to: pages?.profile, label: t("menu.profile.label", "Profile"), description: t("menu.profile.description", "Manage your profile details and email address."), icon: "i-lucide-user-round-pen" },
|
|
9
|
+
{ id: "security", to: pages?.security, label: t("menu.security.label", "Security"), description: t("menu.security.description", "Change your password and manage passkeys."), icon: "i-lucide-shield-user" },
|
|
10
|
+
{ id: "api-tokens", to: pages?.apiTokens, label: t("menu.apiTokens.label", "API tokens"), description: t("menu.apiTokens.description", "Create tokens for scripts and tools."), icon: "i-lucide-key-round" },
|
|
11
|
+
{ id: "documents", to: pages?.documents, label: t("menu.documents.label", "Documents"), description: t("menu.documents.description", "Handbooks and other files for you."), icon: "i-lucide-files" }
|
|
12
|
+
].filter((item) => !!item.to);
|
|
13
|
+
const items = computed(() => menu ? (menu.data.value ?? []).filter((item) => !!item.to).map((item) => ({ id: item.id ?? item.to, label: item.label, description: item.description, icon: item.icon, to: item.to, external: item.external })) : fallback());
|
|
14
|
+
const links = useNavLinks(items, { exact: pages?.index ? [pages.index] : [] });
|
|
15
|
+
const current = computed(() => items.value.filter((item) => !item.external && isActive(item.to, { exact: item.to === pages?.index })).sort((a, b) => strip(b.to).length - strip(a.to).length)[0]);
|
|
16
|
+
if (menu) await menu;
|
|
17
|
+
return { items, links, current };
|
|
18
|
+
}
|
|
@@ -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
|
+
};
|