@frigear-nu/admin 0.0.4 → 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.
Files changed (32) hide show
  1. package/README.md +4 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +4 -1
  4. package/dist/runtime/app/components/AdminKeyValue.d.vue.ts +2 -4
  5. package/dist/runtime/app/components/AdminKeyValue.vue +13 -20
  6. package/dist/runtime/app/components/AdminKeyValue.vue.d.ts +2 -4
  7. package/dist/runtime/app/components/AdminListCard.d.vue.ts +3 -3
  8. package/dist/runtime/app/components/AdminListCard.vue +16 -56
  9. package/dist/runtime/app/components/AdminListCard.vue.d.ts +3 -3
  10. package/dist/runtime/app/components/AdminPage.vue +2 -1
  11. package/dist/runtime/app/components/AdminPageHeader.d.vue.ts +6 -8
  12. package/dist/runtime/app/components/AdminPageHeader.vue +28 -33
  13. package/dist/runtime/app/components/AdminPageHeader.vue.d.ts +6 -8
  14. package/dist/runtime/app/components/AdminSection.d.vue.ts +5 -5
  15. package/dist/runtime/app/components/AdminSection.vue +23 -41
  16. package/dist/runtime/app/components/AdminSection.vue.d.ts +5 -5
  17. package/dist/runtime/app/components/AdminStatCard.d.vue.ts +2 -2
  18. package/dist/runtime/app/components/AdminStatCard.vue +11 -38
  19. package/dist/runtime/app/components/AdminStatCard.vue.d.ts +2 -2
  20. package/dist/runtime/app/components/AdminTable.vue +6 -3
  21. package/dist/runtime/app/components/AdminToolbar.d.vue.ts +2 -2
  22. package/dist/runtime/app/components/AdminToolbar.vue +3 -2
  23. package/dist/runtime/app/components/AdminToolbar.vue.d.ts +2 -2
  24. package/dist/runtime/app/layouts/admin.d.vue.ts +2 -2
  25. package/dist/runtime/app/layouts/admin.vue +31 -31
  26. package/dist/runtime/app/layouts/admin.vue.d.ts +2 -2
  27. package/dist/runtime/app/pages/dashboard.vue +12 -11
  28. package/dist/runtime/i18n/lang/da.json +9 -1
  29. package/dist/runtime/i18n/lang/en.json +9 -1
  30. package/dist/runtime/utils/index.d.ts +16 -3
  31. package/dist/runtime/utils/index.js +6 -1
  32. package/package.json +12 -11
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # @frigear-nu/admin
2
2
 
3
- Generic admin area: an `admin` layout with a sidebar (the `admin.nav` menu), a guarded dashboard (the `admin.dashboard` widget area) and `<AdminPageHeader>`. Other modules register their admin pages and widgets into it.
3
+ Generic admin area: an `admin` layout with a grouped sidebar (the `admin.nav` menu: Overview, Manage and Settings, see
4
+ `ADMIN_NAV_GROUPS`) and a guarded dashboard (the `admin.dashboard` widget areas). Other modules register their admin
5
+ pages, nav entries and widgets into it. Pages are built from the `@frigear-nu/ui` page patterns (`PageHeading`,
6
+ `SectionCard`, `TableCard`, …); the `Admin*` components are deprecated aliases over them.
4
7
 
5
8
  See the [documentation](../../apps/docs/content/4.modules/02.admin.md) for usage.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frigear-nu/admin",
3
3
  "configKey": "admin",
4
- "version": "0.0.4",
4
+ "version": "0.1.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { defineNuxtModule, createResolver, addLayout, addComponent, hasNuxtModule, extendPages } from '@nuxt/kit';
2
2
  import { registerModule, registerTailwindSource, registerLocales } from '@frigear-nu/module-kit/utils';
3
3
  import { extendMenu } from '@frigear-nu/menu/utils';
4
+ import { ADMIN_NAV_GROUPS } from '../dist/runtime/utils/index.js';
4
5
 
5
6
  const trimSlash = (path) => path.length > 1 ? path.replace(/\/+$/, "") : path;
6
7
  const module$1 = defineNuxtModule({
@@ -14,6 +15,8 @@ const module$1 = defineNuxtModule({
14
15
  "nuxt-auth-utils": { version: ">=0.0.1" },
15
16
  "@frigear-nu/menu": { version: ">=0.0.0" },
16
17
  "@frigear-nu/widget": { version: ">=0.0.0" },
18
+ // Page pattern components (`PageHeading`, `SectionCard`, ...) and `useNavLinks`.
19
+ "@frigear-nu/ui": { version: ">=0.0.0" },
17
20
  "@frigear-nu/authorization": { version: ">=0.0.0", optional: true }
18
21
  },
19
22
  setup(options, nuxt) {
@@ -63,7 +66,7 @@ const module$1 = defineNuxtModule({
63
66
  });
64
67
  }
65
68
  if (dashboardPath) {
66
- extendMenu(nuxt, "admin.nav", {
69
+ extendMenu(nuxt, ADMIN_NAV_GROUPS.overview, {
67
70
  id: "admin-dashboard",
68
71
  label: "Dashboard",
69
72
  labelKey: "admin.dashboard.title",
@@ -7,11 +7,9 @@ type __VLS_Props = {
7
7
  items: AdminKeyValueItem[];
8
8
  loading?: boolean;
9
9
  };
10
- declare var __VLS_7: string, __VLS_8: {
11
- item: AdminKeyValueItem;
12
- };
10
+ declare var __VLS_10: string, __VLS_11: any;
13
11
  type __VLS_Slots = {} & {
14
- [K in NonNullable<typeof __VLS_7>]?: (props: typeof __VLS_8) => any;
12
+ [K in NonNullable<typeof __VLS_10>]?: (props: typeof __VLS_11) => any;
15
13
  };
16
14
  declare const __VLS_base: 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>;
17
15
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -6,27 +6,20 @@ defineProps({
6
6
  </script>
7
7
 
8
8
  <template>
9
- <dl class="grid grid-cols-[auto_1fr] items-center gap-x-6 gap-y-2 text-sm">
9
+ <DescriptionList
10
+ :items="items"
11
+ :loading="loading"
12
+ align="end"
13
+ empty="–"
14
+ >
10
15
  <template
11
- v-for="item in items"
12
- :key="item.key"
16
+ v-for="(_, name) in $slots"
17
+ #[name]="slotProps"
13
18
  >
14
- <dt class="text-muted">
15
- {{ item.label }}
16
- </dt>
17
- <dd class="text-highlighted min-w-0 text-right wrap-break-word">
18
- <USkeleton
19
- v-if="loading"
20
- class="ml-auto h-4 w-24"
21
- />
22
- <slot
23
- v-else
24
- :name="item.key"
25
- :item="item"
26
- >
27
- {{ item.value ?? "\u2013" }}
28
- </slot>
29
- </dd>
19
+ <slot
20
+ :name="name"
21
+ v-bind="slotProps ?? {}"
22
+ />
30
23
  </template>
31
- </dl>
24
+ </DescriptionList>
32
25
  </template>
@@ -7,11 +7,9 @@ type __VLS_Props = {
7
7
  items: AdminKeyValueItem[];
8
8
  loading?: boolean;
9
9
  };
10
- declare var __VLS_7: string, __VLS_8: {
11
- item: AdminKeyValueItem;
12
- };
10
+ declare var __VLS_10: string, __VLS_11: any;
13
11
  type __VLS_Slots = {} & {
14
- [K in NonNullable<typeof __VLS_7>]?: (props: typeof __VLS_8) => any;
12
+ [K in NonNullable<typeof __VLS_10>]?: (props: typeof __VLS_11) => any;
15
13
  };
16
14
  declare const __VLS_base: 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>;
17
15
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -7,11 +7,11 @@ type __VLS_Props = {
7
7
  to?: string;
8
8
  toLabel?: string;
9
9
  };
10
- declare var __VLS_1: {}, __VLS_18: {};
10
+ declare var __VLS_9: {}, __VLS_11: {};
11
11
  type __VLS_Slots = {} & {
12
- actions?: (props: typeof __VLS_1) => any;
12
+ actions?: (props: typeof __VLS_9) => any;
13
13
  } & {
14
- default?: (props: typeof __VLS_18) => any;
14
+ default?: (props: typeof __VLS_11) => any;
15
15
  };
16
16
  declare const __VLS_base: 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>;
17
17
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -11,61 +11,21 @@ defineProps({
11
11
  </script>
12
12
 
13
13
  <template>
14
- <section class="bg-elevated/50 ring-default flex h-full flex-col overflow-hidden rounded-lg ring">
15
- <div class="border-default flex items-center justify-between gap-3 border-b px-4 py-3">
16
- <div class="min-w-0">
17
- <h2 class="text-highlighted text-sm font-semibold">
18
- {{ title }}
19
- </h2>
20
- <p
21
- v-if="description"
22
- class="text-muted text-xs"
23
- >
24
- {{ description }}
25
- </p>
26
- </div>
27
- <div
28
- v-if="$slots.actions"
29
- class="flex items-center gap-2"
30
- >
31
- <slot name="actions" />
32
- </div>
33
- </div>
34
- <div class="flex-1 px-4">
35
- <div
36
- v-if="loading"
37
- class="space-y-3 py-4"
38
- >
39
- <USkeleton class="h-4 w-2/3" />
40
- <USkeleton class="h-4 w-1/2" />
41
- <USkeleton class="h-4 w-3/5" />
42
- </div>
43
- <p
44
- v-else-if="isEmpty"
45
- class="text-muted py-6 text-center text-sm"
46
- >
47
- {{ empty }}
48
- </p>
49
- <ul
50
- v-else
51
- class="divide-default divide-y text-sm [&>li]:py-2.5"
52
- >
53
- <slot />
54
- </ul>
55
- </div>
56
- <div
57
- v-if="to"
58
- class="border-default border-t px-4 py-2.5"
14
+ <ListCard
15
+ :title="title"
16
+ :description="description"
17
+ :loading="loading"
18
+ :is-empty="isEmpty"
19
+ :empty="empty"
20
+ :to="to"
21
+ :to-label="toLabel"
22
+ >
23
+ <template
24
+ v-if="$slots.actions"
25
+ #actions
59
26
  >
60
- <UButton
61
- :to="to"
62
- variant="link"
63
- color="neutral"
64
- size="xs"
65
- class="p-0"
66
- trailing-icon="i-lucide-arrow-right"
67
- :label="toLabel"
68
- />
69
- </div>
70
- </section>
27
+ <slot name="actions" />
28
+ </template>
29
+ <slot />
30
+ </ListCard>
71
31
  </template>
@@ -7,11 +7,11 @@ type __VLS_Props = {
7
7
  to?: string;
8
8
  toLabel?: string;
9
9
  };
10
- declare var __VLS_1: {}, __VLS_18: {};
10
+ declare var __VLS_9: {}, __VLS_11: {};
11
11
  type __VLS_Slots = {} & {
12
- actions?: (props: typeof __VLS_1) => any;
12
+ actions?: (props: typeof __VLS_9) => any;
13
13
  } & {
14
- default?: (props: typeof __VLS_18) => any;
14
+ default?: (props: typeof __VLS_11) => any;
15
15
  };
16
16
  declare const __VLS_base: 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>;
17
17
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,5 +1,6 @@
1
+ <!-- @deprecated: use a plain `<div class="flex flex-col gap-4">` as the page root -->
1
2
  <template>
2
- <div class="space-y-4">
3
+ <div class="flex flex-col gap-4">
3
4
  <slot />
4
5
  </div>
5
6
  </template>
@@ -6,19 +6,17 @@ type __VLS_Props = {
6
6
  to: string;
7
7
  };
8
8
  };
9
- declare var __VLS_1: {}, __VLS_8: {}, __VLS_10: {}, __VLS_12: {}, __VLS_14: {}, __VLS_16: {};
9
+ declare var __VLS_9: {}, __VLS_12: {}, __VLS_15: {}, __VLS_18: {}, __VLS_20: {};
10
10
  type __VLS_Slots = {} & {
11
- headline?: (props: typeof __VLS_1) => any;
11
+ title?: (props: typeof __VLS_9) => any;
12
12
  } & {
13
- title?: (props: typeof __VLS_8) => any;
13
+ description?: (props: typeof __VLS_12) => any;
14
14
  } & {
15
- description?: (props: typeof __VLS_10) => any;
15
+ default?: (props: typeof __VLS_15) => any;
16
16
  } & {
17
- default?: (props: typeof __VLS_12) => any;
17
+ links?: (props: typeof __VLS_18) => any;
18
18
  } & {
19
- links?: (props: typeof __VLS_14) => any;
20
- } & {
21
- actions?: (props: typeof __VLS_16) => any;
19
+ actions?: (props: typeof __VLS_20) => any;
22
20
  };
23
21
  declare const __VLS_base: 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>;
24
22
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,48 +1,43 @@
1
1
  <script setup>
2
- defineProps({
2
+ const props = defineProps({
3
3
  title: { type: String, required: false },
4
4
  description: { type: String, required: false },
5
5
  back: { type: Object, required: false }
6
6
  });
7
+ const breadcrumb = computed(() => props.back ? [{ label: props.back.label, to: props.back.to }, ...props.title ? [{ label: props.title }] : []] : void 0);
7
8
  </script>
8
9
 
9
10
  <template>
10
- <header class="mb-5 flex flex-wrap items-end justify-between gap-x-4 gap-y-3">
11
- <div class="min-w-0 space-y-1">
12
- <slot name="headline">
13
- <UButton
14
- v-if="back"
15
- class="-ml-1 p-1"
16
- color="neutral"
17
- variant="link"
18
- size="sm"
19
- icon="i-lucide-arrow-left"
20
- :to="back.to"
21
- :label="back.label"
22
- />
23
- </slot>
24
- <h1 class="text-highlighted text-xl font-semibold tracking-tight text-pretty sm:text-2xl">
25
- <slot name="title">
26
- {{ title }}
27
- </slot>
28
- </h1>
29
- <p
30
- v-if="description || $slots.description"
31
- class="text-muted text-sm text-pretty"
32
- >
33
- <slot name="description">
34
- {{ description }}
35
- </slot>
36
- </p>
11
+ <PageHeading
12
+ :title="title"
13
+ :description="description"
14
+ :breadcrumb="breadcrumb"
15
+ >
16
+ <template
17
+ v-if="$slots.title"
18
+ #title
19
+ >
20
+ <slot name="title" />
21
+ </template>
22
+ <template
23
+ v-if="$slots.description"
24
+ #description
25
+ >
26
+ <slot name="description" />
27
+ </template>
28
+ <template
29
+ v-if="$slots.default"
30
+ #default
31
+ >
37
32
  <slot />
38
- </div>
39
- <div
33
+ </template>
34
+ <template
40
35
  v-if="$slots.links || $slots.actions"
41
- class="flex flex-wrap items-center gap-2"
36
+ #actions
42
37
  >
43
38
  <slot name="links">
44
39
  <slot name="actions" />
45
40
  </slot>
46
- </div>
47
- </header>
41
+ </template>
42
+ </PageHeading>
48
43
  </template>
@@ -6,19 +6,17 @@ type __VLS_Props = {
6
6
  to: string;
7
7
  };
8
8
  };
9
- declare var __VLS_1: {}, __VLS_8: {}, __VLS_10: {}, __VLS_12: {}, __VLS_14: {}, __VLS_16: {};
9
+ declare var __VLS_9: {}, __VLS_12: {}, __VLS_15: {}, __VLS_18: {}, __VLS_20: {};
10
10
  type __VLS_Slots = {} & {
11
- headline?: (props: typeof __VLS_1) => any;
11
+ title?: (props: typeof __VLS_9) => any;
12
12
  } & {
13
- title?: (props: typeof __VLS_8) => any;
13
+ description?: (props: typeof __VLS_12) => any;
14
14
  } & {
15
- description?: (props: typeof __VLS_10) => any;
15
+ default?: (props: typeof __VLS_15) => any;
16
16
  } & {
17
- default?: (props: typeof __VLS_12) => any;
17
+ links?: (props: typeof __VLS_18) => any;
18
18
  } & {
19
- links?: (props: typeof __VLS_14) => any;
20
- } & {
21
- actions?: (props: typeof __VLS_16) => any;
19
+ actions?: (props: typeof __VLS_20) => any;
22
20
  };
23
21
  declare const __VLS_base: 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>;
24
22
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -3,15 +3,15 @@ type __VLS_Props = {
3
3
  description?: string;
4
4
  loading?: boolean;
5
5
  };
6
- declare var __VLS_1: {}, __VLS_3: {}, __VLS_15: {}, __VLS_17: {}, __VLS_19: {};
6
+ declare var __VLS_9: {}, __VLS_12: {}, __VLS_14: {}, __VLS_16: {}, __VLS_19: {};
7
7
  type __VLS_Slots = {} & {
8
- header?: (props: typeof __VLS_1) => any;
8
+ header?: (props: typeof __VLS_9) => any;
9
9
  } & {
10
- actions?: (props: typeof __VLS_3) => any;
10
+ actions?: (props: typeof __VLS_12) => any;
11
11
  } & {
12
- flush?: (props: typeof __VLS_15) => any;
12
+ flush?: (props: typeof __VLS_14) => any;
13
13
  } & {
14
- default?: (props: typeof __VLS_17) => any;
14
+ default?: (props: typeof __VLS_16) => any;
15
15
  } & {
16
16
  footer?: (props: typeof __VLS_19) => any;
17
17
  };
@@ -7,49 +7,31 @@ defineProps({
7
7
  </script>
8
8
 
9
9
  <template>
10
- <section class="bg-elevated/50 ring-default ring overflow-hidden rounded-lg">
11
- <div
12
- v-if="title || $slots.header || $slots.actions"
13
- class="border-default flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3.5"
10
+ <SectionCard
11
+ :title="title"
12
+ :description="description"
13
+ :loading="loading"
14
+ :flush="!!$slots.flush"
15
+ >
16
+ <template
17
+ v-if="$slots.header"
18
+ #header
14
19
  >
15
- <slot name="header">
16
- <div class="min-w-0">
17
- <h2 class="text-highlighted text-sm font-semibold">
18
- {{ title }}
19
- </h2>
20
- <p
21
- v-if="description"
22
- class="text-muted text-sm"
23
- >
24
- {{ description }}
25
- </p>
26
- </div>
27
- </slot>
28
- <div
29
- v-if="$slots.actions"
30
- class="flex items-center gap-2"
31
- >
32
- <slot name="actions" />
33
- </div>
34
- </div>
35
- <div :class="$slots.flush || loading ? '' : 'p-4'">
36
- <div
37
- v-if="loading"
38
- class="space-y-2 p-4"
39
- >
40
- <USkeleton class="h-5 w-1/2" />
41
- <USkeleton class="h-5 w-1/3" />
42
- </div>
43
- <template v-else>
44
- <slot name="flush" />
45
- <slot />
46
- </template>
47
- </div>
48
- <div
20
+ <slot name="header" />
21
+ </template>
22
+ <template
23
+ v-if="$slots.actions"
24
+ #actions
25
+ >
26
+ <slot name="actions" />
27
+ </template>
28
+ <slot name="flush" />
29
+ <slot />
30
+ <template
49
31
  v-if="$slots.footer"
50
- class="border-default border-t px-4 py-3"
32
+ #footer
51
33
  >
52
34
  <slot name="footer" />
53
- </div>
54
- </section>
35
+ </template>
36
+ </SectionCard>
55
37
  </template>
@@ -3,15 +3,15 @@ type __VLS_Props = {
3
3
  description?: string;
4
4
  loading?: boolean;
5
5
  };
6
- declare var __VLS_1: {}, __VLS_3: {}, __VLS_15: {}, __VLS_17: {}, __VLS_19: {};
6
+ declare var __VLS_9: {}, __VLS_12: {}, __VLS_14: {}, __VLS_16: {}, __VLS_19: {};
7
7
  type __VLS_Slots = {} & {
8
- header?: (props: typeof __VLS_1) => any;
8
+ header?: (props: typeof __VLS_9) => any;
9
9
  } & {
10
- actions?: (props: typeof __VLS_3) => any;
10
+ actions?: (props: typeof __VLS_12) => any;
11
11
  } & {
12
- flush?: (props: typeof __VLS_15) => any;
12
+ flush?: (props: typeof __VLS_14) => any;
13
13
  } & {
14
- default?: (props: typeof __VLS_17) => any;
14
+ default?: (props: typeof __VLS_16) => any;
15
15
  } & {
16
16
  footer?: (props: typeof __VLS_19) => any;
17
17
  };
@@ -5,9 +5,9 @@ type __VLS_Props = {
5
5
  loading?: boolean;
6
6
  empty?: string;
7
7
  };
8
- declare var __VLS_23: {};
8
+ declare var __VLS_9: {};
9
9
  type __VLS_Slots = {} & {
10
- default?: (props: typeof __VLS_23) => any;
10
+ default?: (props: typeof __VLS_9) => any;
11
11
  };
12
12
  declare const __VLS_base: 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>;
13
13
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -6,48 +6,21 @@ defineProps({
6
6
  loading: { type: Boolean, required: false },
7
7
  empty: { type: String, required: false }
8
8
  });
9
- const NuxtLink = resolveComponent("NuxtLink");
10
9
  </script>
11
10
 
12
11
  <template>
13
- <component
14
- :is="to ? NuxtLink : 'section'"
12
+ <StatCard
13
+ :label="label"
14
+ :icon="icon"
15
15
  :to="to"
16
- class="bg-elevated/50 ring-default flex h-full items-start gap-3 overflow-hidden rounded-lg p-4 ring"
17
- :class="to && 'hover:bg-elevated transition-colors'"
16
+ :loading="loading"
17
+ :empty="empty"
18
18
  >
19
- <span
20
- v-if="icon"
21
- class="bg-primary/10 ring-primary/25 text-primary flex shrink-0 items-center justify-center rounded-full p-2.5 ring ring-inset"
19
+ <template
20
+ v-if="$slots.default"
21
+ #default
22
22
  >
23
- <UIcon
24
- :name="icon"
25
- class="size-5"
26
- />
27
- </span>
28
- <div class="flex min-w-0 flex-1 flex-col gap-1.5">
29
- <h2 class="text-muted text-xs font-normal uppercase">
30
- {{ label }}
31
- </h2>
32
- <div
33
- v-if="loading"
34
- class="space-y-2"
35
- >
36
- <USkeleton class="h-5 w-2/3" />
37
- <USkeleton class="h-4 w-1/3" />
38
- </div>
39
- <div
40
- v-else-if="$slots.default"
41
- class="text-highlighted flex flex-wrap items-center gap-2 text-lg font-semibold"
42
- >
43
- <slot />
44
- </div>
45
- <p
46
- v-else
47
- class="text-muted text-sm"
48
- >
49
- {{ empty }}
50
- </p>
51
- </div>
52
- </component>
23
+ <slot />
24
+ </template>
25
+ </StatCard>
53
26
  </template>
@@ -5,9 +5,9 @@ type __VLS_Props = {
5
5
  loading?: boolean;
6
6
  empty?: string;
7
7
  };
8
- declare var __VLS_23: {};
8
+ declare var __VLS_9: {};
9
9
  type __VLS_Slots = {} & {
10
- default?: (props: typeof __VLS_23) => any;
10
+ default?: (props: typeof __VLS_9) => any;
11
11
  };
12
12
  declare const __VLS_base: 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>;
13
13
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -9,11 +9,12 @@ const emit = defineEmits(["select"]);
9
9
  </script>
10
10
 
11
11
  <template>
12
- <div class="bg-elevated/50 ring-default ring overflow-hidden rounded-lg">
12
+ <TableCard>
13
13
  <UTable
14
14
  :data="data"
15
15
  :columns="columns"
16
16
  :loading="loading"
17
+ :ui="tableUi()"
17
18
  @select="(e, row) => emit('select', e, row)"
18
19
  >
19
20
  <template
@@ -29,11 +30,13 @@ const emit = defineEmits(["select"]);
29
30
  v-if="empty && !$slots.empty"
30
31
  #empty
31
32
  >
32
- <AdminEmpty
33
+ <UEmpty
33
34
  icon="i-lucide-inbox"
34
35
  :title="empty"
36
+ variant="naked"
37
+ class="py-10"
35
38
  />
36
39
  </template>
37
40
  </UTable>
38
- </div>
41
+ </TableCard>
39
42
  </template>
@@ -1,6 +1,6 @@
1
- declare var __VLS_1: {};
1
+ declare var __VLS_8: {};
2
2
  type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_1) => any;
3
+ default?: (props: typeof __VLS_8) => any;
4
4
  };
5
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
6
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,5 +1,6 @@
1
+ <!-- @deprecated: use PageToolbar from @frigear-nu/ui -->
1
2
  <template>
2
- <div class="flex flex-wrap items-center gap-3">
3
+ <PageToolbar>
3
4
  <slot />
4
- </div>
5
+ </PageToolbar>
5
6
  </template>
@@ -1,6 +1,6 @@
1
- declare var __VLS_1: {};
1
+ declare var __VLS_8: {};
2
2
  type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_1) => any;
3
+ default?: (props: typeof __VLS_8) => any;
4
4
  };
5
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
6
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,6 +1,6 @@
1
- declare var __VLS_84: {};
1
+ declare var __VLS_79: {};
2
2
  type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_84) => any;
3
+ default?: (props: typeof __VLS_79) => any;
4
4
  };
5
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
6
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -8,32 +8,31 @@ const AccountMenu = resolveComponent("AccountMenu");
8
8
  const optional = (component) => typeof component === "string" ? void 0 : component;
9
9
  const nuxtApp = useNuxtApp();
10
10
  const multiLocale = (nuxtApp.$i18n?.locales?.value?.length ?? 0) > 1;
11
- const { data: items } = await useMenu("admin.nav");
12
11
  const home = config?.dashboard ?? config?.base ?? "/admin";
13
- const stripLocale = (path) => {
14
- const trimmed = path.length > 1 ? path.replace(/\/+$/, "") : path;
15
- const first = trimmed.split("/")[1];
16
- const codes = (nuxtApp.$i18n?.locales?.value ?? []).map((l) => typeof l === "string" ? l : l.code);
17
- return first && codes.includes(first) ? trimmed.slice(first.length + 1) || "/" : trimmed;
18
- };
19
- const links = computed(() => (items.value ?? []).map((item) => ({
20
- label: item.label,
21
- icon: item.icon,
22
- to: item.to,
23
- external: item.external,
24
- // The dashboard lives at the base itself, so it only matches exactly (ignoring a locale prefix).
25
- active: !!item.to && (item.to === home ? stripLocale(route.path) === home : route.path === item.to || route.path.startsWith(`${item.to}/`))
26
- })));
27
- const title = computed(() => links.value.find((link) => link.active)?.label ?? t("title", "Administration"));
12
+ const { localize } = useLocaleLink();
13
+ const { data: items } = await useMenu("admin.nav.*");
14
+ const groupOf = (group) => group === "nav.overview" ? "overview" : group === "nav.settings" ? "settings" : "manage";
15
+ const byGroup = (name) => (items.value ?? []).filter((item) => groupOf(item.group) === name);
16
+ const navOptions = { exact: [home] };
17
+ const overview = useNavLinks(() => byGroup("overview"), navOptions);
18
+ const manage = useNavLinks(() => byGroup("manage"), navOptions);
19
+ const settings = useNavLinks(() => byGroup("settings"), navOptions);
20
+ const groups = computed(() => [
21
+ { id: "overview", label: void 0, links: overview.value },
22
+ { id: "manage", label: t("nav.groups.manage", "Manage"), links: manage.value },
23
+ { id: "settings", label: t("nav.groups.settings", "Settings"), links: settings.value }
24
+ ].filter((group) => group.links.length));
25
+ const navigation = computed(() => groups.value.map((group) => group.label ? [{ type: "label", label: group.label }, ...group.links] : group.links));
26
+ const title = computed(() => groups.value.flatMap((group) => group.links).find((link) => link.active)?.label ?? t("title", "Administration"));
28
27
  const sidebarOpen = ref(false);
29
28
  watch(() => route.path, () => {
30
29
  sidebarOpen.value = false;
31
30
  });
32
- const searchGroups = computed(() => [{
33
- id: "admin-nav",
34
- label: t("search.goTo", "Go to"),
35
- items: links.value.map(({ label, icon, to }) => ({ id: to ?? label, label, icon, to }))
36
- }]);
31
+ const searchGroups = computed(() => groups.value.map((group) => ({
32
+ id: `admin-nav-${group.id}`,
33
+ label: group.label ?? t("search.goTo", "Go to"),
34
+ items: group.links.map(({ label, icon, to }) => ({ id: String(to ?? label), label, icon, to }))
35
+ })));
37
36
  </script>
38
37
 
39
38
  <template>
@@ -44,11 +43,11 @@ const searchGroups = computed(() => [{
44
43
  collapsible
45
44
  resizable
46
45
  class="bg-elevated/25"
47
- :ui="{ footer: 'lg:border-t lg:border-default' }"
46
+ :ui="{ footer: 'lg:border-t lg:border-default flex-col items-stretch gap-1' }"
48
47
  >
49
48
  <template #header="{ collapsed }">
50
49
  <NuxtLink
51
- :to="home"
50
+ :to="localize(home)"
52
51
  class="flex min-w-0 items-center gap-2"
53
52
  >
54
53
  <span class="bg-primary text-inverted flex size-7 shrink-0 items-center justify-center rounded-lg">
@@ -72,7 +71,7 @@ const searchGroups = computed(() => [{
72
71
 
73
72
  <UNavigationMenu
74
73
  :collapsed="collapsed"
75
- :items="links"
74
+ :items="navigation"
76
75
  orientation="vertical"
77
76
  tooltip
78
77
  :aria-label="t('nav.label', 'Admin navigation')"
@@ -81,7 +80,7 @@ const searchGroups = computed(() => [{
81
80
 
82
81
  <template #footer="{ collapsed }">
83
82
  <UButton
84
- to="/"
83
+ :to="localize('/')"
85
84
  color="neutral"
86
85
  variant="ghost"
87
86
  icon="i-lucide-arrow-left"
@@ -90,6 +89,12 @@ const searchGroups = computed(() => [{
90
89
  block
91
90
  class="justify-start"
92
91
  />
92
+ <component
93
+ :is="optional(AccountMenu)"
94
+ v-if="optional(AccountMenu)"
95
+ block
96
+ :collapsed="collapsed"
97
+ />
93
98
  </template>
94
99
  </UDashboardSidebar>
95
100
 
@@ -109,17 +114,12 @@ const searchGroups = computed(() => [{
109
114
  :is="optional(LanguageSelect)"
110
115
  v-if="optional(LanguageSelect) && multiLocale"
111
116
  />
112
- <UColorModeButton />
113
- <component
114
- :is="optional(AccountMenu)"
115
- v-if="optional(AccountMenu)"
116
- />
117
117
  </template>
118
118
  </UDashboardNavbar>
119
119
  </template>
120
120
 
121
121
  <template #body>
122
- <div class="admin-content mx-auto w-full max-w-7xl">
122
+ <div class="mx-auto w-full max-w-7xl">
123
123
  <slot />
124
124
  </div>
125
125
  </template>
@@ -1,6 +1,6 @@
1
- declare var __VLS_84: {};
1
+ declare var __VLS_79: {};
2
2
  type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_84) => any;
3
+ default?: (props: typeof __VLS_79) => any;
4
4
  };
5
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
6
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,8 +1,9 @@
1
1
  <script setup>
2
2
  defineOptions({ name: "AdminDashboardPage" });
3
3
  const t = useModuleT("admin");
4
- const { data: menu } = await useMenu("admin.nav");
5
- const items = computed(() => (menu.value ?? []).filter((item) => item.id !== "admin-dashboard"));
4
+ const { localize } = useLocaleLink();
5
+ const { data: menu } = await useMenu("admin.nav.*");
6
+ const items = computed(() => (menu.value ?? []).filter((item) => item.group !== "nav.overview" && item.id !== "admin-dashboard"));
6
7
  const [{ data: top }, { data: main }, { data: side }] = await Promise.all([
7
8
  useWidgets("admin.dashboard.top"),
8
9
  useWidgets("admin.dashboard"),
@@ -13,8 +14,8 @@ const stack = "flex flex-col gap-4";
13
14
  </script>
14
15
 
15
16
  <template>
16
- <AdminPage>
17
- <AdminPageHeader
17
+ <div class="flex flex-col gap-4">
18
+ <PageHeading
18
19
  :title="t('dashboard.title', 'Dashboard')"
19
20
  :description="t('dashboard.description', 'Overview and shortcuts for administrators.')"
20
21
  />
@@ -44,7 +45,7 @@ const stack = "flex flex-col gap-4";
44
45
  :title="item.label"
45
46
  :description="item.description"
46
47
  :icon="item.icon"
47
- :to="item.to"
48
+ :to="item.to && !item.external ? localize(item.to) : item.to"
48
49
  variant="subtle"
49
50
  :ui="{
50
51
  container: 'gap-y-1.5',
@@ -53,12 +54,12 @@ const stack = "flex flex-col gap-4";
53
54
  }"
54
55
  />
55
56
  </UPageGrid>
56
- <p
57
+ <UEmpty
57
58
  v-else
58
- class="text-muted"
59
- >
60
- {{ t("dashboard.empty", "Nothing here yet. Modules add their admin pages and widgets automatically.") }}
61
- </p>
59
+ icon="i-lucide-layout-dashboard"
60
+ :title="t('dashboard.empty', 'Nothing here yet. Modules add their admin pages and widgets automatically.')"
61
+ variant="naked"
62
+ />
62
63
  </template>
63
- </AdminPage>
64
+ </div>
64
65
  </template>
@@ -2,7 +2,12 @@
2
2
  "admin": {
3
3
  "title": "Administration",
4
4
  "nav": {
5
- "label": "Admin-navigation"
5
+ "label": "Admin-navigation",
6
+ "groups": {
7
+ "overview": "Overblik",
8
+ "manage": "Administrér",
9
+ "settings": "Indstillinger"
10
+ }
6
11
  },
7
12
  "dashboard": {
8
13
  "title": "Oversigt",
@@ -13,6 +18,9 @@
13
18
  "menu": {
14
19
  "label": "Admin",
15
20
  "description": "Administration"
21
+ },
22
+ "search": {
23
+ "goTo": "Gå til"
16
24
  }
17
25
  }
18
26
  }
@@ -2,7 +2,12 @@
2
2
  "admin": {
3
3
  "title": "Administration",
4
4
  "nav": {
5
- "label": "Admin navigation"
5
+ "label": "Admin navigation",
6
+ "groups": {
7
+ "overview": "Overview",
8
+ "manage": "Manage",
9
+ "settings": "Settings"
10
+ }
6
11
  },
7
12
  "dashboard": {
8
13
  "title": "Dashboard",
@@ -13,6 +18,9 @@
13
18
  "menu": {
14
19
  "label": "Admin",
15
20
  "description": "Administration"
21
+ },
22
+ "search": {
23
+ "goTo": "Go to"
16
24
  }
17
25
  }
18
26
  }
@@ -7,17 +7,30 @@ import type { WidgetDefinition } from '@frigear-nu/widget/utils';
7
7
  */
8
8
  export declare const adminRoute: (nuxt: Nuxt, ...segments: string[]) => string;
9
9
  export declare const ADMIN_NAV_MENU = "admin.nav";
10
+ /**
11
+ * The sidebar groups, as menu paths: Overview (the dashboard), Manage (the modules' pages, the `admin.nav` root)
12
+ * and Settings. Runtime menu sources register with `extendMenuItem(ADMIN_NAV_GROUPS.settings, item)`.
13
+ */
14
+ export declare const ADMIN_NAV_GROUPS: {
15
+ readonly overview: "admin.nav.overview";
16
+ readonly manage: "admin.nav";
17
+ readonly settings: "admin.nav.settings";
18
+ };
19
+ export type AdminNavGroup = keyof typeof ADMIN_NAV_GROUPS;
10
20
  export declare const ADMIN_DASHBOARD_AREA = "admin.dashboard";
11
21
  /** The stat row above the dashboard content. */
12
22
  export declare const ADMIN_DASHBOARD_TOP_AREA = "admin.dashboard.top";
13
23
  /** The narrow column next to the main dashboard content. */
14
24
  export declare const ADMIN_DASHBOARD_SIDE_AREA = "admin.dashboard.side";
15
25
  export type AdminDashboardPosition = 'top' | 'main' | 'side';
16
- /** Adds entries to the admin sidebar (the `admin.nav` menu of `@frigear-nu/menu`). Call from a module's `setup()`. */
17
- export declare const extendAdminNav: (nuxt: Nuxt, items: MenuItem | MenuItem[]) => void;
26
+ /**
27
+ * Adds entries to the admin sidebar (the `admin.nav` menu of `@frigear-nu/menu`), into `group` (default `manage`).
28
+ * Call from a module's `setup()`.
29
+ */
30
+ export declare const extendAdminNav: (nuxt: Nuxt, items: MenuItem | MenuItem[], group?: AdminNavGroup) => void;
18
31
  /**
19
32
  * Adds widgets to the admin dashboard (an area of `@frigear-nu/widget`). Call from a module's `setup()`.
20
- * `position` picks the area: `top` is the stat row (use `AdminStatCard`), `main` the content column (the default),
33
+ * `position` picks the area: `top` is the stat row (use `StatCard`), `main` the content column (the default),
21
34
  * `side` the narrow column.
22
35
  */
23
36
  export declare const extendAdminDashboard: (nuxt: Nuxt, widgets: WidgetDefinition | WidgetDefinition[], position?: AdminDashboardPosition) => void;
@@ -5,6 +5,11 @@ export const adminRoute = (nuxt, ...segments) => {
5
5
  return [base.replace(/\/+$/, ""), ...segments].join("/");
6
6
  };
7
7
  export const ADMIN_NAV_MENU = "admin.nav";
8
+ export const ADMIN_NAV_GROUPS = {
9
+ overview: "admin.nav.overview",
10
+ manage: "admin.nav",
11
+ settings: "admin.nav.settings"
12
+ };
8
13
  export const ADMIN_DASHBOARD_AREA = "admin.dashboard";
9
14
  export const ADMIN_DASHBOARD_TOP_AREA = "admin.dashboard.top";
10
15
  export const ADMIN_DASHBOARD_SIDE_AREA = "admin.dashboard.side";
@@ -13,5 +18,5 @@ const DASHBOARD_AREAS = {
13
18
  main: ADMIN_DASHBOARD_AREA,
14
19
  side: ADMIN_DASHBOARD_SIDE_AREA
15
20
  };
16
- export const extendAdminNav = (nuxt, items) => extendMenu(nuxt, ADMIN_NAV_MENU, items);
21
+ export const extendAdminNav = (nuxt, items, group = "manage") => extendMenu(nuxt, ADMIN_NAV_GROUPS[group], items);
17
22
  export const extendAdminDashboard = (nuxt, widgets, position = "main") => extendWidgets(nuxt, DASHBOARD_AREAS[position], widgets);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frigear-nu/admin",
3
- "version": "0.0.4",
3
+ "version": "0.1.0",
4
4
  "description": "Generic admin area: shell layout, guarded dashboard and navigation that other modules contribute to through @frigear-nu/menu and @frigear-nu/widget.",
5
5
  "homepage": "https://docs.frigear.dev/modules/admin",
6
6
  "repository": {
@@ -30,29 +30,30 @@
30
30
  "playground"
31
31
  ],
32
32
  "dependencies": {
33
- "@frigear-nu/menu": "0.0.11",
34
- "@frigear-nu/module-kit": "1.0.1",
35
- "@frigear-nu/widget": "0.0.4",
36
- "@nuxt/kit": "^4.6.0",
33
+ "@frigear-nu/menu": "0.1.0",
34
+ "@frigear-nu/module-kit": "1.1.0",
35
+ "@frigear-nu/ui": "0.1.0",
36
+ "@frigear-nu/widget": "0.1.0",
37
+ "@nuxt/kit": "^4.6.1",
37
38
  "nuxt-auth-utils": "^0.5.30"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@nuxt/devtools": "^3.4.2",
41
42
  "@nuxt/eslint-config": "^1.17.0",
42
43
  "@nuxt/module-builder": "^1.0.3",
43
- "@nuxt/schema": "^4.6.0",
44
+ "@nuxt/schema": "^4.6.1",
44
45
  "@nuxt/test-utils": "^4.3.2",
45
- "@nuxt/ui": "^4.11.0",
46
- "@types/node": "latest",
46
+ "@nuxt/ui": "^4.11.2",
47
+ "@types/node": "^26.6.5",
47
48
  "changelogen": "^0.6.2",
48
- "eslint": "^10.10.0",
49
+ "eslint": "^10.12.0",
49
50
  "nuxt": "^4.6.1",
50
51
  "typescript": "^6.0.3",
51
- "vitest": "^5.0.1",
52
+ "vitest": "^5.0.2",
52
53
  "vue-tsc": "^3.3.11"
53
54
  },
54
55
  "peerDependencies": {
55
- "@nuxt/ui": "^4.10.0"
56
+ "@nuxt/ui": "^4.11.2"
56
57
  },
57
58
  "scripts": {
58
59
  "postinstall": "nuxt-module-build build --stub && nuxt prepare",