@frigear-nu/admin 0.0.2 → 0.0.4

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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frigear-nu/admin",
3
3
  "configKey": "admin",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -26,7 +26,7 @@ const module$1 = defineNuxtModule({
26
26
  const dashboardPath = settings.dashboard === false ? void 0 : typeof settings.dashboard === "string" ? settings.dashboard : base;
27
27
  nuxt.options.runtimeConfig.public.admin = { base, dashboard: dashboardPath };
28
28
  addLayout({ src: resolver.resolve("./runtime/app/layouts/admin.vue"), filename: "admin.vue" }, "admin");
29
- for (const name of ["AdminPage", "AdminPageHeader", "AdminSection", "AdminToolbar", "AdminEmpty", "AdminTable", "AdminStatCard", "AdminKeyValue"]) {
29
+ for (const name of ["AdminPage", "AdminPageHeader", "AdminSection", "AdminToolbar", "AdminEmpty", "AdminTable", "AdminStatCard", "AdminKeyValue", "AdminListCard"]) {
30
30
  addComponent({ name, filePath: resolver.resolve(`./runtime/app/components/${name}.vue`) });
31
31
  }
32
32
  const prefixes = ["", ...(nuxt.options.i18n?.locales ?? []).map((locale) => typeof locale === "string" ? locale : locale.code).map((code) => `/${code}`)];
@@ -0,0 +1,24 @@
1
+ type __VLS_Props = {
2
+ title: string;
3
+ description?: string;
4
+ loading?: boolean;
5
+ isEmpty?: boolean;
6
+ empty?: string;
7
+ to?: string;
8
+ toLabel?: string;
9
+ };
10
+ declare var __VLS_1: {}, __VLS_18: {};
11
+ type __VLS_Slots = {} & {
12
+ actions?: (props: typeof __VLS_1) => any;
13
+ } & {
14
+ default?: (props: typeof __VLS_18) => any;
15
+ };
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
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
18
+ declare const _default: typeof __VLS_export;
19
+ export default _default;
20
+ type __VLS_WithSlots<T, S> = T & {
21
+ new (): {
22
+ $slots: S;
23
+ };
24
+ };
@@ -0,0 +1,71 @@
1
+ <script setup>
2
+ defineProps({
3
+ title: { type: String, required: true },
4
+ description: { type: String, required: false },
5
+ loading: { type: Boolean, required: false },
6
+ isEmpty: { type: Boolean, required: false },
7
+ empty: { type: String, required: false },
8
+ to: { type: String, required: false },
9
+ toLabel: { type: String, required: false }
10
+ });
11
+ </script>
12
+
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"
59
+ >
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>
71
+ </template>
@@ -0,0 +1,24 @@
1
+ type __VLS_Props = {
2
+ title: string;
3
+ description?: string;
4
+ loading?: boolean;
5
+ isEmpty?: boolean;
6
+ empty?: string;
7
+ to?: string;
8
+ toLabel?: string;
9
+ };
10
+ declare var __VLS_1: {}, __VLS_18: {};
11
+ type __VLS_Slots = {} & {
12
+ actions?: (props: typeof __VLS_1) => any;
13
+ } & {
14
+ default?: (props: typeof __VLS_18) => any;
15
+ };
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
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
18
+ declare const _default: typeof __VLS_export;
19
+ export default _default;
20
+ type __VLS_WithSlots<T, S> = T & {
21
+ new (): {
22
+ $slots: S;
23
+ };
24
+ };
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="space-y-6">
2
+ <div class="space-y-4">
3
3
  <slot />
4
4
  </div>
5
5
  </template>
@@ -6,19 +6,19 @@ type __VLS_Props = {
6
6
  to: string;
7
7
  };
8
8
  };
9
- declare var __VLS_9: {}, __VLS_17: {}, __VLS_20: {}, __VLS_23: {}, __VLS_25: {}, __VLS_27: {};
9
+ declare var __VLS_1: {}, __VLS_8: {}, __VLS_10: {}, __VLS_12: {}, __VLS_14: {}, __VLS_16: {};
10
10
  type __VLS_Slots = {} & {
11
- headline?: (props: typeof __VLS_9) => any;
11
+ headline?: (props: typeof __VLS_1) => any;
12
12
  } & {
13
- title?: (props: typeof __VLS_17) => any;
13
+ title?: (props: typeof __VLS_8) => any;
14
14
  } & {
15
- description?: (props: typeof __VLS_20) => any;
15
+ description?: (props: typeof __VLS_10) => any;
16
16
  } & {
17
- links?: (props: typeof __VLS_23) => any;
17
+ default?: (props: typeof __VLS_12) => any;
18
18
  } & {
19
- actions?: (props: typeof __VLS_25) => any;
19
+ links?: (props: typeof __VLS_14) => any;
20
20
  } & {
21
- default?: (props: typeof __VLS_27) => any;
21
+ actions?: (props: typeof __VLS_16) => any;
22
22
  };
23
23
  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
24
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -7,19 +7,12 @@ defineProps({
7
7
  </script>
8
8
 
9
9
  <template>
10
- <UPageHeader
11
- :title="title"
12
- :description="description"
13
- :ui="{ root: 'pt-0 pb-6 mb-6', title: 'text-2xl sm:text-3xl' }"
14
- >
15
- <template
16
- v-if="back || $slots.headline"
17
- #headline
18
- >
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">
19
12
  <slot name="headline">
20
13
  <UButton
21
14
  v-if="back"
22
- class="p-0"
15
+ class="-ml-1 p-1"
23
16
  color="neutral"
24
17
  variant="link"
25
18
  size="sm"
@@ -28,31 +21,28 @@ defineProps({
28
21
  :label="back.label"
29
22
  />
30
23
  </slot>
31
- </template>
32
-
33
- <template
34
- v-if="$slots.title"
35
- #title
36
- >
37
- <slot name="title" />
38
- </template>
39
-
40
- <template
41
- v-if="$slots.description"
42
- #description
43
- >
44
- <slot name="description" />
45
- </template>
46
-
47
- <template
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>
37
+ <slot />
38
+ </div>
39
+ <div
48
40
  v-if="$slots.links || $slots.actions"
49
- #links
41
+ class="flex flex-wrap items-center gap-2"
50
42
  >
51
43
  <slot name="links">
52
44
  <slot name="actions" />
53
45
  </slot>
54
- </template>
55
-
56
- <slot />
57
- </UPageHeader>
46
+ </div>
47
+ </header>
58
48
  </template>
@@ -6,19 +6,19 @@ type __VLS_Props = {
6
6
  to: string;
7
7
  };
8
8
  };
9
- declare var __VLS_9: {}, __VLS_17: {}, __VLS_20: {}, __VLS_23: {}, __VLS_25: {}, __VLS_27: {};
9
+ declare var __VLS_1: {}, __VLS_8: {}, __VLS_10: {}, __VLS_12: {}, __VLS_14: {}, __VLS_16: {};
10
10
  type __VLS_Slots = {} & {
11
- headline?: (props: typeof __VLS_9) => any;
11
+ headline?: (props: typeof __VLS_1) => any;
12
12
  } & {
13
- title?: (props: typeof __VLS_17) => any;
13
+ title?: (props: typeof __VLS_8) => any;
14
14
  } & {
15
- description?: (props: typeof __VLS_20) => any;
15
+ description?: (props: typeof __VLS_10) => any;
16
16
  } & {
17
- links?: (props: typeof __VLS_23) => any;
17
+ default?: (props: typeof __VLS_12) => any;
18
18
  } & {
19
- actions?: (props: typeof __VLS_25) => any;
19
+ links?: (props: typeof __VLS_14) => any;
20
20
  } & {
21
- default?: (props: typeof __VLS_27) => any;
21
+ actions?: (props: typeof __VLS_16) => any;
22
22
  };
23
23
  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
24
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -7,14 +7,14 @@ defineProps({
7
7
  </script>
8
8
 
9
9
  <template>
10
- <section class="border-default overflow-hidden rounded-lg border">
10
+ <section class="bg-elevated/50 ring-default ring overflow-hidden rounded-lg">
11
11
  <div
12
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"
13
+ class="border-default flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3.5"
14
14
  >
15
15
  <slot name="header">
16
16
  <div class="min-w-0">
17
- <h2 class="text-sm font-semibold">
17
+ <h2 class="text-highlighted text-sm font-semibold">
18
18
  {{ title }}
19
19
  </h2>
20
20
  <p
@@ -1,11 +1,13 @@
1
1
  type __VLS_Props = {
2
2
  label: string;
3
+ icon?: string;
4
+ to?: string;
3
5
  loading?: boolean;
4
6
  empty?: string;
5
7
  };
6
- declare var __VLS_11: {};
8
+ declare var __VLS_23: {};
7
9
  type __VLS_Slots = {} & {
8
- default?: (props: typeof __VLS_11) => any;
10
+ default?: (props: typeof __VLS_23) => any;
9
11
  };
10
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>;
11
13
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,34 +1,53 @@
1
1
  <script setup>
2
2
  defineProps({
3
3
  label: { type: String, required: true },
4
+ icon: { type: String, required: false },
5
+ to: { type: String, required: false },
4
6
  loading: { type: Boolean, required: false },
5
7
  empty: { type: String, required: false }
6
8
  });
9
+ const NuxtLink = resolveComponent("NuxtLink");
7
10
  </script>
8
11
 
9
12
  <template>
10
- <section class="border-default flex h-full flex-col gap-2 overflow-hidden rounded-lg border p-4">
11
- <h2 class="text-muted text-xs font-medium tracking-wide uppercase">
12
- {{ label }}
13
- </h2>
14
- <div
15
- v-if="loading"
16
- class="space-y-2"
13
+ <component
14
+ :is="to ? NuxtLink : 'section'"
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'"
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"
17
22
  >
18
- <USkeleton class="h-5 w-2/3" />
19
- <USkeleton class="h-4 w-1/3" />
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>
20
51
  </div>
21
- <div
22
- v-else-if="$slots.default"
23
- class="flex flex-wrap items-center gap-2 text-sm"
24
- >
25
- <slot />
26
- </div>
27
- <p
28
- v-else
29
- class="text-muted text-sm"
30
- >
31
- {{ empty }}
32
- </p>
33
- </section>
52
+ </component>
34
53
  </template>
@@ -1,11 +1,13 @@
1
1
  type __VLS_Props = {
2
2
  label: string;
3
+ icon?: string;
4
+ to?: string;
3
5
  loading?: boolean;
4
6
  empty?: string;
5
7
  };
6
- declare var __VLS_11: {};
8
+ declare var __VLS_23: {};
7
9
  type __VLS_Slots = {} & {
8
- default?: (props: typeof __VLS_11) => any;
10
+ default?: (props: typeof __VLS_23) => any;
9
11
  };
10
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>;
11
13
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -9,7 +9,7 @@ const emit = defineEmits(["select"]);
9
9
  </script>
10
10
 
11
11
  <template>
12
- <div class="border-default overflow-hidden rounded-lg border">
12
+ <div class="bg-elevated/50 ring-default ring overflow-hidden rounded-lg">
13
13
  <UTable
14
14
  :data="data"
15
15
  :columns="columns"
@@ -29,77 +29,100 @@ const sidebarOpen = ref(false);
29
29
  watch(() => route.path, () => {
30
30
  sidebarOpen.value = false;
31
31
  });
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
+ }]);
32
37
  </script>
33
38
 
34
39
  <template>
35
- <div class="h-dvh w-full overflow-hidden bg-default">
36
- <UDashboardGroup
37
- unit="rem"
38
- :ui="{ base: 'admin-group relative flex h-full w-full overflow-hidden' }"
40
+ <UDashboardGroup unit="rem">
41
+ <UDashboardSidebar
42
+ id="admin"
43
+ v-model:open="sidebarOpen"
44
+ collapsible
45
+ resizable
46
+ class="bg-elevated/25"
47
+ :ui="{ footer: 'lg:border-t lg:border-default' }"
39
48
  >
40
- <UDashboardSidebar
41
- v-model:open="sidebarOpen"
42
- :default-size="16"
43
- :min-size="12"
44
- :max-size="22"
45
- :ui="{ root: 'min-h-0', footer: 'border-t border-default' }"
46
- >
47
- <template #header>
48
- <NuxtLink
49
- :to="home"
50
- class="flex items-center gap-2 font-semibold"
51
- >
52
- <UIcon name="i-lucide-layout-dashboard" />
53
- <span>{{ t("title", "Administration") }}</span>
54
- </NuxtLink>
55
- </template>
49
+ <template #header="{ collapsed }">
50
+ <NuxtLink
51
+ :to="home"
52
+ class="flex min-w-0 items-center gap-2"
53
+ >
54
+ <span class="bg-primary text-inverted flex size-7 shrink-0 items-center justify-center rounded-lg">
55
+ <UIcon
56
+ name="i-lucide-layout-dashboard"
57
+ class="size-4"
58
+ />
59
+ </span>
60
+ <span
61
+ v-if="!collapsed"
62
+ class="text-highlighted truncate font-semibold"
63
+ >{{ t("title", "Administration") }}</span>
64
+ </NuxtLink>
65
+ </template>
66
+
67
+ <template #default="{ collapsed }">
68
+ <UDashboardSearchButton
69
+ :collapsed="collapsed"
70
+ class="bg-transparent ring-default"
71
+ />
56
72
 
57
73
  <UNavigationMenu
58
- orientation="vertical"
74
+ :collapsed="collapsed"
59
75
  :items="links"
76
+ orientation="vertical"
77
+ tooltip
60
78
  :aria-label="t('nav.label', 'Admin navigation')"
61
79
  />
80
+ </template>
81
+
82
+ <template #footer="{ collapsed }">
83
+ <UButton
84
+ to="/"
85
+ color="neutral"
86
+ variant="ghost"
87
+ icon="i-lucide-arrow-left"
88
+ :label="collapsed ? void 0 : t('backToSite', 'Back to the site')"
89
+ :square="collapsed"
90
+ block
91
+ class="justify-start"
92
+ />
93
+ </template>
94
+ </UDashboardSidebar>
62
95
 
63
- <template #footer>
64
- <UButton
65
- to="/"
66
- color="neutral"
67
- variant="ghost"
68
- icon="i-lucide-arrow-left"
69
- :label="t('backToSite', 'Back to the site')"
70
- block
71
- />
72
- </template>
73
- </UDashboardSidebar>
96
+ <UDashboardSearch :groups="searchGroups" />
74
97
 
75
- <UDashboardPanel :ui="{ body: 'p-0 sm:p-0 gap-0' }">
76
- <template #header>
77
- <UDashboardNavbar :title="title">
78
- <template #leading>
79
- <UDashboardSidebarCollapse />
80
- </template>
81
- <template #right>
82
- <component
83
- :is="optional(LanguageSelect)"
84
- v-if="optional(LanguageSelect) && multiLocale"
85
- />
86
- <UColorModeButton />
87
- <component
88
- :is="optional(AccountMenu)"
89
- v-if="optional(AccountMenu)"
90
- />
91
- </template>
92
- </UDashboardNavbar>
93
- </template>
98
+ <UDashboardPanel id="admin-main">
99
+ <template #header>
100
+ <UDashboardNavbar
101
+ :title="title"
102
+ :ui="{ right: 'gap-2' }"
103
+ >
104
+ <template #leading>
105
+ <UDashboardSidebarCollapse />
106
+ </template>
107
+ <template #right>
108
+ <component
109
+ :is="optional(LanguageSelect)"
110
+ v-if="optional(LanguageSelect) && multiLocale"
111
+ />
112
+ <UColorModeButton />
113
+ <component
114
+ :is="optional(AccountMenu)"
115
+ v-if="optional(AccountMenu)"
116
+ />
117
+ </template>
118
+ </UDashboardNavbar>
119
+ </template>
94
120
 
95
- <template #body>
96
- <UPage :ui="{ root: 'block', center: 'admin-content px-4 sm:px-6' }">
97
- <UPageBody class="mt-0 pt-6 pb-12 space-y-0">
98
- <slot />
99
- </UPageBody>
100
- </UPage>
101
- </template>
102
- </UDashboardPanel>
103
- </UDashboardGroup>
104
- </div>
121
+ <template #body>
122
+ <div class="admin-content mx-auto w-full max-w-7xl">
123
+ <slot />
124
+ </div>
125
+ </template>
126
+ </UDashboardPanel>
127
+ </UDashboardGroup>
105
128
  </template>
@@ -3,6 +3,13 @@ defineOptions({ name: "AdminDashboardPage" });
3
3
  const t = useModuleT("admin");
4
4
  const { data: menu } = await useMenu("admin.nav");
5
5
  const items = computed(() => (menu.value ?? []).filter((item) => item.id !== "admin-dashboard"));
6
+ const [{ data: top }, { data: main }, { data: side }] = await Promise.all([
7
+ useWidgets("admin.dashboard.top"),
8
+ useWidgets("admin.dashboard"),
9
+ useWidgets("admin.dashboard.side")
10
+ ]);
11
+ const hasWidgets = computed(() => !!(top.value?.length || main.value?.length || side.value?.length));
12
+ const stack = "flex flex-col gap-4";
6
13
  </script>
7
14
 
8
15
  <template>
@@ -11,28 +18,47 @@ const items = computed(() => (menu.value ?? []).filter((item) => item.id !== "ad
11
18
  :title="t('dashboard.title', 'Dashboard')"
12
19
  :description="t('dashboard.description', 'Overview and shortcuts for administrators.')"
13
20
  />
14
- <WidgetArea name="admin.dashboard">
15
- <template #empty>
16
- <div
17
- v-if="items.length"
18
- class="grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] gap-4"
19
- >
20
- <UPageCard
21
- v-for="item in items"
22
- :key="item.id"
23
- :title="item.label"
24
- :description="item.description"
25
- :icon="item.icon"
26
- :to="item.to"
27
- />
28
- </div>
29
- <p
30
- v-else
31
- class="text-muted"
32
- >
33
- {{ t("dashboard.empty", "Nothing here yet. Modules add their admin pages and widgets automatically.") }}
34
- </p>
35
- </template>
36
- </WidgetArea>
21
+
22
+ <template v-if="hasWidgets">
23
+ <WidgetArea name="admin.dashboard.top" />
24
+ <div class="grid gap-4 lg:grid-cols-3">
25
+ <WidgetArea
26
+ name="admin.dashboard"
27
+ :grid-class="`${stack} ${side?.length ? 'lg:col-span-2' : 'lg:col-span-3'}`"
28
+ />
29
+ <WidgetArea
30
+ name="admin.dashboard.side"
31
+ :grid-class="stack"
32
+ />
33
+ </div>
34
+ </template>
35
+
36
+ <template v-else>
37
+ <UPageGrid
38
+ v-if="items.length"
39
+ class="lg:grid-cols-3"
40
+ >
41
+ <UPageCard
42
+ v-for="item in items"
43
+ :key="item.id"
44
+ :title="item.label"
45
+ :description="item.description"
46
+ :icon="item.icon"
47
+ :to="item.to"
48
+ variant="subtle"
49
+ :ui="{
50
+ container: 'gap-y-1.5',
51
+ wrapper: 'items-start',
52
+ leading: 'p-2.5 rounded-full bg-primary/10 ring ring-inset ring-primary/25 flex-col'
53
+ }"
54
+ />
55
+ </UPageGrid>
56
+ <p
57
+ v-else
58
+ class="text-muted"
59
+ >
60
+ {{ t("dashboard.empty", "Nothing here yet. Modules add their admin pages and widgets automatically.") }}
61
+ </p>
62
+ </template>
37
63
  </AdminPage>
38
64
  </template>
@@ -8,7 +8,16 @@ import type { WidgetDefinition } from '@frigear-nu/widget/utils';
8
8
  export declare const adminRoute: (nuxt: Nuxt, ...segments: string[]) => string;
9
9
  export declare const ADMIN_NAV_MENU = "admin.nav";
10
10
  export declare const ADMIN_DASHBOARD_AREA = "admin.dashboard";
11
+ /** The stat row above the dashboard content. */
12
+ export declare const ADMIN_DASHBOARD_TOP_AREA = "admin.dashboard.top";
13
+ /** The narrow column next to the main dashboard content. */
14
+ export declare const ADMIN_DASHBOARD_SIDE_AREA = "admin.dashboard.side";
15
+ export type AdminDashboardPosition = 'top' | 'main' | 'side';
11
16
  /** Adds entries to the admin sidebar (the `admin.nav` menu of `@frigear-nu/menu`). Call from a module's `setup()`. */
12
17
  export declare const extendAdminNav: (nuxt: Nuxt, items: MenuItem | MenuItem[]) => void;
13
- /** Adds widgets to the admin dashboard (the `admin.dashboard` area of `@frigear-nu/widget`). Call from a module's `setup()`. */
14
- export declare const extendAdminDashboard: (nuxt: Nuxt, widgets: WidgetDefinition | WidgetDefinition[]) => void;
18
+ /**
19
+ * 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),
21
+ * `side` the narrow column.
22
+ */
23
+ export declare const extendAdminDashboard: (nuxt: Nuxt, widgets: WidgetDefinition | WidgetDefinition[], position?: AdminDashboardPosition) => void;
@@ -6,5 +6,12 @@ export const adminRoute = (nuxt, ...segments) => {
6
6
  };
7
7
  export const ADMIN_NAV_MENU = "admin.nav";
8
8
  export const ADMIN_DASHBOARD_AREA = "admin.dashboard";
9
+ export const ADMIN_DASHBOARD_TOP_AREA = "admin.dashboard.top";
10
+ export const ADMIN_DASHBOARD_SIDE_AREA = "admin.dashboard.side";
11
+ const DASHBOARD_AREAS = {
12
+ top: ADMIN_DASHBOARD_TOP_AREA,
13
+ main: ADMIN_DASHBOARD_AREA,
14
+ side: ADMIN_DASHBOARD_SIDE_AREA
15
+ };
9
16
  export const extendAdminNav = (nuxt, items) => extendMenu(nuxt, ADMIN_NAV_MENU, items);
10
- export const extendAdminDashboard = (nuxt, widgets) => extendWidgets(nuxt, ADMIN_DASHBOARD_AREA, widgets);
17
+ 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.2",
3
+ "version": "0.0.4",
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,9 +30,9 @@
30
30
  "playground"
31
31
  ],
32
32
  "dependencies": {
33
- "@frigear-nu/menu": "0.0.9",
34
- "@frigear-nu/module-kit": "0.1.7",
35
- "@frigear-nu/widget": "0.0.2",
33
+ "@frigear-nu/menu": "0.0.11",
34
+ "@frigear-nu/module-kit": "1.0.1",
35
+ "@frigear-nu/widget": "0.0.4",
36
36
  "@nuxt/kit": "^4.6.0",
37
37
  "nuxt-auth-utils": "^0.5.30"
38
38
  },