@polymarbot/nuxt-layer-shadcn-ui 0.1.2 → 0.1.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/app/assets/styles/colors.css +24 -24
- package/app/assets/styles/globals.css +2 -29
- package/app/assets/styles/index.stories.ts +10 -5
- package/app/assets/styles/utilities.css +0 -3
- package/app/components/ui/AdminLayout/Navbar.vue +22 -0
- package/app/components/ui/AdminLayout/SidebarDropdown.vue +102 -0
- package/app/components/ui/AdminLayout/SidebarMenus.vue +244 -0
- package/app/components/ui/AdminLayout/index.stories.ts +199 -0
- package/app/components/ui/AdminLayout/index.vue +78 -0
- package/app/components/ui/AdminLayout/types.ts +42 -0
- package/nuxt.config.ts +28 -4
- package/package.json +1 -2
- package/app/assets/styles/animate.css +0 -71
- package/app/assets/styles/transition.css +0 -34
- package/i18n.config.ts +0 -19
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
/* Reference: https://www.shadcn-vue.com/themes */
|
|
3
3
|
|
|
4
4
|
:root {
|
|
5
|
-
/* Background
|
|
6
|
-
--background: oklch(
|
|
7
|
-
--background-surface: oklch(
|
|
5
|
+
/* Background */
|
|
6
|
+
--background: oklch(0.98 0 0);
|
|
7
|
+
--background-surface: oklch(1 0 0);
|
|
8
8
|
--background-muted: oklch(0.95 0 0);
|
|
9
9
|
--background-accent: oklch(0.93 0 0);
|
|
10
10
|
|
|
11
|
-
/* Foreground
|
|
11
|
+
/* Foreground */
|
|
12
12
|
--foreground: oklch(0.145 0 0);
|
|
13
13
|
--foreground-muted: oklch(0.556 0 0);
|
|
14
14
|
--foreground-accent: oklch(0.35 0 0);
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
--input: oklch(0.922 0 0);
|
|
23
23
|
--ring: oklch(0.708 0 0);
|
|
24
24
|
|
|
25
|
-
/*
|
|
25
|
+
/* Status */
|
|
26
26
|
--status-foreground: oklch(0.985 0 0);
|
|
27
27
|
--success: oklch(0.58 0.17 163.225);
|
|
28
28
|
--info: oklch(0.58 0.15 221.723);
|
|
@@ -32,13 +32,14 @@
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
.dark {
|
|
35
|
-
/* Background
|
|
35
|
+
/* Background — muted/accent go brighter than surface here; they're an
|
|
36
|
+
interaction axis, not a deeper elevation level than cards. */
|
|
36
37
|
--background: oklch(0.12 0 0);
|
|
37
38
|
--background-surface: oklch(0.18 0 0);
|
|
38
39
|
--background-muted: oklch(0.25 0 0);
|
|
39
40
|
--background-accent: oklch(0.3 0 0);
|
|
40
41
|
|
|
41
|
-
/* Foreground
|
|
42
|
+
/* Foreground */
|
|
42
43
|
--foreground: oklch(0.985 0 0);
|
|
43
44
|
--foreground-muted: oklch(0.708 0 0);
|
|
44
45
|
--foreground-accent: oklch(0.85 0 0);
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
--input: oklch(1 0 0 / 15%);
|
|
53
54
|
--ring: oklch(0.439 0 0);
|
|
54
55
|
|
|
55
|
-
/*
|
|
56
|
+
/* Status */
|
|
56
57
|
--status-foreground: oklch(0.145 0 0);
|
|
57
58
|
--success: oklch(0.696 0.17 162.48);
|
|
58
59
|
--info: oklch(0.685 0.121 220.7);
|
|
@@ -67,10 +68,10 @@
|
|
|
67
68
|
--color-background: var(--background);
|
|
68
69
|
--color-foreground: var(--foreground);
|
|
69
70
|
|
|
70
|
-
/* Components
|
|
71
|
-
--color-card: var(--background);
|
|
71
|
+
/* Components */
|
|
72
|
+
--color-card: var(--background-surface);
|
|
72
73
|
--color-card-foreground: var(--foreground);
|
|
73
|
-
--color-popover: var(--background);
|
|
74
|
+
--color-popover: var(--background-surface);
|
|
74
75
|
--color-popover-foreground: var(--foreground);
|
|
75
76
|
|
|
76
77
|
/* Brand */
|
|
@@ -85,24 +86,13 @@
|
|
|
85
86
|
--color-accent: var(--background-accent);
|
|
86
87
|
--color-accent-foreground: var(--foreground-accent);
|
|
87
88
|
--color-destructive: var(--danger);
|
|
88
|
-
--color-destructive-foreground: var(--status-foreground);
|
|
89
89
|
|
|
90
90
|
/* Form & Border */
|
|
91
91
|
--color-border: var(--border);
|
|
92
92
|
--color-input: var(--input);
|
|
93
93
|
--color-ring: var(--ring);
|
|
94
94
|
|
|
95
|
-
/*
|
|
96
|
-
--color-sidebar: var(--background-surface);
|
|
97
|
-
--color-sidebar-foreground: var(--foreground);
|
|
98
|
-
--color-sidebar-primary: var(--primary);
|
|
99
|
-
--color-sidebar-primary-foreground: var(--primary-foreground);
|
|
100
|
-
--color-sidebar-accent: var(--background-accent);
|
|
101
|
-
--color-sidebar-accent-foreground: var(--foreground-accent);
|
|
102
|
-
--color-sidebar-border: var(--border);
|
|
103
|
-
--color-sidebar-ring: var(--ring);
|
|
104
|
-
|
|
105
|
-
/* Semantic status */
|
|
95
|
+
/* Status */
|
|
106
96
|
--color-success: var(--success);
|
|
107
97
|
--color-success-foreground: var(--status-foreground);
|
|
108
98
|
--color-info: var(--info);
|
|
@@ -113,4 +103,14 @@
|
|
|
113
103
|
--color-warn-foreground: var(--status-foreground);
|
|
114
104
|
--color-danger: var(--danger);
|
|
115
105
|
--color-danger-foreground: var(--status-foreground);
|
|
116
|
-
|
|
106
|
+
|
|
107
|
+
/* Sidebar */
|
|
108
|
+
--color-sidebar: var(--background);
|
|
109
|
+
--color-sidebar-foreground: var(--foreground);
|
|
110
|
+
--color-sidebar-primary: var(--primary);
|
|
111
|
+
--color-sidebar-primary-foreground: var(--primary-foreground);
|
|
112
|
+
--color-sidebar-accent: var(--background-accent);
|
|
113
|
+
--color-sidebar-accent-foreground: var(--foreground-accent);
|
|
114
|
+
--color-sidebar-border: var(--border);
|
|
115
|
+
--color-sidebar-ring: var(--ring);
|
|
116
|
+
}
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
@import 'tw-animate-css';
|
|
3
3
|
@import './colors.css';
|
|
4
4
|
@import './utilities.css';
|
|
5
|
-
@import './animate.css';
|
|
6
|
-
@import './transition.css';
|
|
7
5
|
|
|
8
|
-
/* Ensure Tailwind scans
|
|
9
|
-
@source '
|
|
6
|
+
/* Ensure Tailwind scans the layer's own source files */
|
|
7
|
+
@source '../../..';
|
|
10
8
|
|
|
11
9
|
/* Dark mode configuration for Tailwind v4 */
|
|
12
10
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
@@ -14,30 +12,5 @@
|
|
|
14
12
|
@layer base {
|
|
15
13
|
* {
|
|
16
14
|
@apply border-border outline-ring/50;
|
|
17
|
-
box-sizing: border-box;
|
|
18
|
-
}
|
|
19
|
-
html {
|
|
20
|
-
font-size: 16px;
|
|
21
|
-
}
|
|
22
|
-
body {
|
|
23
|
-
@apply bg-background text-foreground;
|
|
24
|
-
margin: 0;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/* Prevent accidental text selection on touch devices */
|
|
29
|
-
@media (pointer: coarse) {
|
|
30
|
-
body {
|
|
31
|
-
-webkit-user-select: none;
|
|
32
|
-
user-select: none;
|
|
33
|
-
}
|
|
34
|
-
input,
|
|
35
|
-
textarea,
|
|
36
|
-
[contenteditable='true'],
|
|
37
|
-
pre,
|
|
38
|
-
code,
|
|
39
|
-
.selectable {
|
|
40
|
-
-webkit-user-select: text;
|
|
41
|
-
user-select: text;
|
|
42
15
|
}
|
|
43
16
|
}
|
|
@@ -6,20 +6,23 @@ type Swatch = {
|
|
|
6
6
|
bg: string
|
|
7
7
|
fg?: string
|
|
8
8
|
border?: boolean
|
|
9
|
+
span?: string
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
type Group = {
|
|
12
13
|
title: string
|
|
13
14
|
description?: string
|
|
14
15
|
swatches: Swatch[]
|
|
16
|
+
cols?: string
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
const groups: Group[] = [
|
|
18
20
|
{
|
|
19
21
|
title: 'Surface',
|
|
20
|
-
description: '
|
|
22
|
+
description: 'Two independent axes — elevation (background / card / popover) and interaction (muted / accent).',
|
|
23
|
+
cols: 'grid-cols-1 sm:grid-cols-2',
|
|
21
24
|
swatches: [
|
|
22
|
-
{ name: 'background', token: '--color-background', bg: 'bg-background', fg: 'text-foreground', border: true },
|
|
25
|
+
{ name: 'background', token: '--color-background', bg: 'bg-background', fg: 'text-foreground', border: true, span: 'col-span-2' },
|
|
23
26
|
{ name: 'card', token: '--color-card', bg: 'bg-card', fg: 'text-card-foreground', border: true },
|
|
24
27
|
{ name: 'popover', token: '--color-popover', bg: 'bg-popover', fg: 'text-popover-foreground', border: true },
|
|
25
28
|
{ name: 'muted', token: '--color-muted', bg: 'bg-muted', fg: 'text-muted-foreground' },
|
|
@@ -43,7 +46,6 @@ const groups: Group[] = [
|
|
|
43
46
|
{ name: 'help', token: '--color-help', bg: 'bg-help', fg: 'text-help-foreground' },
|
|
44
47
|
{ name: 'warn', token: '--color-warn', bg: 'bg-warn', fg: 'text-warn-foreground' },
|
|
45
48
|
{ name: 'danger', token: '--color-danger', bg: 'bg-danger', fg: 'text-danger-foreground' },
|
|
46
|
-
{ name: 'destructive', token: '--color-destructive', bg: 'bg-destructive', fg: 'text-destructive-foreground' },
|
|
47
49
|
],
|
|
48
50
|
},
|
|
49
51
|
{
|
|
@@ -94,7 +96,7 @@ export const Palette: Story = {
|
|
|
94
96
|
<h3 class="text-lg font-medium">{{ group.title }}</h3>
|
|
95
97
|
<p v-if="group.description" class="mt-1 text-sm text-muted-foreground">{{ group.description }}</p>
|
|
96
98
|
</header>
|
|
97
|
-
<div class="grid grid-cols-1
|
|
99
|
+
<div :class="['grid gap-3', group.cols || 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4']">
|
|
98
100
|
<div
|
|
99
101
|
v-for="swatch in group.swatches"
|
|
100
102
|
:key="swatch.name"
|
|
@@ -103,6 +105,7 @@ export const Palette: Story = {
|
|
|
103
105
|
swatch.bg,
|
|
104
106
|
swatch.fg,
|
|
105
107
|
swatch.border ? 'border border-border' : '',
|
|
108
|
+
swatch.span,
|
|
106
109
|
]"
|
|
107
110
|
>
|
|
108
111
|
<span class="font-mono text-xs font-medium">{{ swatch.name }}</span>
|
|
@@ -157,7 +160,7 @@ export const LightVsDark: Story = {
|
|
|
157
160
|
},
|
|
158
161
|
},
|
|
159
162
|
render: () => ({
|
|
160
|
-
setup: () => ({ groups }),
|
|
163
|
+
setup: () => ({ groups: groups.filter(g => g.title !== 'Sidebar') }),
|
|
161
164
|
template: `
|
|
162
165
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
163
166
|
<div class="rounded-lg border border-border bg-background p-6">
|
|
@@ -174,6 +177,7 @@ export const LightVsDark: Story = {
|
|
|
174
177
|
swatch.bg,
|
|
175
178
|
swatch.fg || 'text-foreground',
|
|
176
179
|
swatch.border ? 'border-border' : 'border-transparent',
|
|
180
|
+
swatch.span,
|
|
177
181
|
]"
|
|
178
182
|
>
|
|
179
183
|
<span class="font-mono text-xs">{{ swatch.name }}</span>
|
|
@@ -197,6 +201,7 @@ export const LightVsDark: Story = {
|
|
|
197
201
|
swatch.bg,
|
|
198
202
|
swatch.fg || 'text-foreground',
|
|
199
203
|
swatch.border ? 'border-border' : 'border-transparent',
|
|
204
|
+
swatch.span,
|
|
200
205
|
]"
|
|
201
206
|
>
|
|
202
207
|
<span class="font-mono text-xs">{{ swatch.name }}</span>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { SidebarTrigger } from '../../shadcn/sidebar'
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<header
|
|
7
|
+
class="
|
|
8
|
+
flex h-14 shrink-0 items-center gap-2 border-b border-sidebar-border
|
|
9
|
+
bg-card px-4 text-card-foreground transition-[width,height] ease-linear
|
|
10
|
+
group-has-data-[collapsible=icon]/sidebar-wrapper:h-12
|
|
11
|
+
"
|
|
12
|
+
>
|
|
13
|
+
<SidebarTrigger class="-ml-1" />
|
|
14
|
+
<Divider
|
|
15
|
+
type="vertical"
|
|
16
|
+
class="mr-2 h-4!"
|
|
17
|
+
/>
|
|
18
|
+
<div class="flex flex-1 items-center">
|
|
19
|
+
<slot />
|
|
20
|
+
</div>
|
|
21
|
+
</header>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { AdminLayoutSidebarDropdownProfile, AdminLayoutSidebarDropdownMenuItem } from './types'
|
|
3
|
+
import { ChevronsUpDown } from 'lucide-vue-next'
|
|
4
|
+
import {
|
|
5
|
+
SidebarMenu,
|
|
6
|
+
SidebarMenuButton,
|
|
7
|
+
SidebarMenuItem,
|
|
8
|
+
useSidebar,
|
|
9
|
+
} from '../../shadcn/sidebar'
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
profile?: AdminLayoutSidebarDropdownProfile | null
|
|
13
|
+
menuItems?: AdminLayoutSidebarDropdownMenuItem[]
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const { isMobile } = useSidebar()
|
|
17
|
+
|
|
18
|
+
// Transform 'profile' items to Dropdown's custom-label items with slot 'profile'.
|
|
19
|
+
// Other items pass through unchanged.
|
|
20
|
+
const dropdownItems = computed<DropdownItem[]>(() =>
|
|
21
|
+
(props.menuItems ?? []).map(item => {
|
|
22
|
+
if (item.type === 'profile') {
|
|
23
|
+
const { type: _, ...rest } = item
|
|
24
|
+
return { type: 'custom-label', slot: 'profile', ...rest }
|
|
25
|
+
}
|
|
26
|
+
return item
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<SidebarMenu>
|
|
33
|
+
<SidebarMenuItem>
|
|
34
|
+
<Dropdown
|
|
35
|
+
:menus="dropdownItems"
|
|
36
|
+
trigger="click"
|
|
37
|
+
:side="isMobile ? 'bottom' : 'right'"
|
|
38
|
+
align="start"
|
|
39
|
+
:sideOffset="4"
|
|
40
|
+
class="min-w-56 rounded-lg"
|
|
41
|
+
>
|
|
42
|
+
<SidebarMenuButton
|
|
43
|
+
size="lg"
|
|
44
|
+
class="
|
|
45
|
+
data-[state=open]:bg-sidebar-accent
|
|
46
|
+
data-[state=open]:text-sidebar-accent-foreground
|
|
47
|
+
"
|
|
48
|
+
>
|
|
49
|
+
<Avatar
|
|
50
|
+
:image="profile?.icon ? undefined : (profile?.image || undefined)"
|
|
51
|
+
:fallbackLabel="profile?.icon ? undefined : profile?.title?.charAt(0)?.toUpperCase()"
|
|
52
|
+
shape="circle"
|
|
53
|
+
class="size-8"
|
|
54
|
+
>
|
|
55
|
+
<Icon
|
|
56
|
+
v-if="profile?.icon"
|
|
57
|
+
:name="profile.icon"
|
|
58
|
+
class="size-4"
|
|
59
|
+
/>
|
|
60
|
+
</Avatar>
|
|
61
|
+
<div class="grid flex-1 text-left text-sm/tight">
|
|
62
|
+
<span class="truncate font-medium">
|
|
63
|
+
{{ profile?.title }}
|
|
64
|
+
</span>
|
|
65
|
+
<span class="truncate text-xs">
|
|
66
|
+
{{ profile?.subtitle }}
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
<ChevronsUpDown class="ml-auto size-4" />
|
|
70
|
+
</SidebarMenuButton>
|
|
71
|
+
|
|
72
|
+
<template #profile="{ item }">
|
|
73
|
+
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
74
|
+
<Avatar
|
|
75
|
+
:image="item.icon ? undefined : (item.image || undefined)"
|
|
76
|
+
:fallbackLabel="item.icon ? undefined : item.title?.charAt(0)?.toUpperCase()"
|
|
77
|
+
shape="circle"
|
|
78
|
+
class="size-8"
|
|
79
|
+
>
|
|
80
|
+
<Icon
|
|
81
|
+
v-if="item.icon"
|
|
82
|
+
:name="item.icon"
|
|
83
|
+
class="size-4"
|
|
84
|
+
/>
|
|
85
|
+
</Avatar>
|
|
86
|
+
<div class="grid flex-1 text-left text-sm/tight">
|
|
87
|
+
<span class="truncate font-semibold">
|
|
88
|
+
{{ item.title }}
|
|
89
|
+
</span>
|
|
90
|
+
<span
|
|
91
|
+
v-if="item.subtitle"
|
|
92
|
+
class="truncate text-xs"
|
|
93
|
+
>
|
|
94
|
+
{{ item.subtitle }}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</template>
|
|
99
|
+
</Dropdown>
|
|
100
|
+
</SidebarMenuItem>
|
|
101
|
+
</SidebarMenu>
|
|
102
|
+
</template>
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { AdminLayoutSidebarMenuItem } from './types'
|
|
3
|
+
import {
|
|
4
|
+
Collapsible,
|
|
5
|
+
CollapsibleContent,
|
|
6
|
+
CollapsibleTrigger,
|
|
7
|
+
} from '../../shadcn/collapsible'
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuItem,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from '../../shadcn/dropdown-menu'
|
|
14
|
+
import {
|
|
15
|
+
SidebarGroup,
|
|
16
|
+
SidebarGroupLabel,
|
|
17
|
+
SidebarMenu,
|
|
18
|
+
SidebarMenuAction,
|
|
19
|
+
SidebarMenuButton,
|
|
20
|
+
SidebarMenuItem,
|
|
21
|
+
SidebarMenuSub,
|
|
22
|
+
SidebarMenuSubButton,
|
|
23
|
+
SidebarMenuSubItem,
|
|
24
|
+
} from '../../shadcn/sidebar'
|
|
25
|
+
|
|
26
|
+
const props = defineProps<{
|
|
27
|
+
menus: AdminLayoutSidebarMenuItem[]
|
|
28
|
+
}>()
|
|
29
|
+
|
|
30
|
+
const route = useRoute()
|
|
31
|
+
|
|
32
|
+
const isExternal = (href: string) => /^https?:\/\//.test(href)
|
|
33
|
+
|
|
34
|
+
const navGroups = computed(() => {
|
|
35
|
+
const groups: { label?: string, items: AdminLayoutSidebarMenuItem[] }[] = []
|
|
36
|
+
const groupMap = new Map<string | undefined, AdminLayoutSidebarMenuItem[]>()
|
|
37
|
+
|
|
38
|
+
for (const item of props.menus) {
|
|
39
|
+
const key = item.group
|
|
40
|
+
if (!groupMap.has(key)) {
|
|
41
|
+
const items: AdminLayoutSidebarMenuItem[] = []
|
|
42
|
+
groupMap.set(key, items)
|
|
43
|
+
groups.push({ label: key, items })
|
|
44
|
+
}
|
|
45
|
+
groupMap.get(key)!.push(item)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return groups
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
function isActive (href?: string): boolean {
|
|
52
|
+
if (!href || isExternal(href)) return false
|
|
53
|
+
return route.path.startsWith(href)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isLink (item: AdminLayoutSidebarMenuItem): boolean {
|
|
57
|
+
return !!item.href
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hasActiveChild (item: AdminLayoutSidebarMenuItem): boolean {
|
|
61
|
+
return item.children?.some(child => isActive(child.href)) ?? false
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<SidebarGroup
|
|
67
|
+
v-for="(group, groupIndex) in navGroups"
|
|
68
|
+
:key="groupIndex"
|
|
69
|
+
>
|
|
70
|
+
<SidebarGroupLabel v-if="group.label">
|
|
71
|
+
{{ group.label }}
|
|
72
|
+
</SidebarGroupLabel>
|
|
73
|
+
<SidebarMenu>
|
|
74
|
+
<template
|
|
75
|
+
v-for="item in group.items"
|
|
76
|
+
:key="item.label"
|
|
77
|
+
>
|
|
78
|
+
<!-- Collapsible item with children -->
|
|
79
|
+
<Collapsible
|
|
80
|
+
v-if="item.children"
|
|
81
|
+
asChild
|
|
82
|
+
:defaultOpen="item.expanded ?? hasActiveChild(item)"
|
|
83
|
+
class="group/collapsible"
|
|
84
|
+
>
|
|
85
|
+
<SidebarMenuItem>
|
|
86
|
+
<CollapsibleTrigger asChild>
|
|
87
|
+
<SidebarMenuButton :tooltip="item.label">
|
|
88
|
+
<Icon
|
|
89
|
+
v-if="item.icon"
|
|
90
|
+
:name="item.icon"
|
|
91
|
+
/>
|
|
92
|
+
<span>{{ item.label }}</span>
|
|
93
|
+
<Icon
|
|
94
|
+
name="chevron-right"
|
|
95
|
+
class="
|
|
96
|
+
ml-auto transition-transform duration-200
|
|
97
|
+
group-data-[state=open]/collapsible:rotate-90
|
|
98
|
+
"
|
|
99
|
+
/>
|
|
100
|
+
</SidebarMenuButton>
|
|
101
|
+
</CollapsibleTrigger>
|
|
102
|
+
<CollapsibleContent>
|
|
103
|
+
<SidebarMenuSub>
|
|
104
|
+
<SidebarMenuSubItem
|
|
105
|
+
v-for="child in item.children"
|
|
106
|
+
:key="child.label"
|
|
107
|
+
>
|
|
108
|
+
<SidebarMenuSubButton
|
|
109
|
+
:asChild="isLink(child)"
|
|
110
|
+
:isActive="isActive(child.href)"
|
|
111
|
+
>
|
|
112
|
+
<WebLink
|
|
113
|
+
v-if="isLink(child)"
|
|
114
|
+
:href="child.href"
|
|
115
|
+
unstyled
|
|
116
|
+
>
|
|
117
|
+
<span>{{ child.label }}</span>
|
|
118
|
+
</WebLink>
|
|
119
|
+
<span
|
|
120
|
+
v-else
|
|
121
|
+
@click="child.command?.()"
|
|
122
|
+
>
|
|
123
|
+
{{ child.label }}
|
|
124
|
+
</span>
|
|
125
|
+
</SidebarMenuSubButton>
|
|
126
|
+
</SidebarMenuSubItem>
|
|
127
|
+
</SidebarMenuSub>
|
|
128
|
+
</CollapsibleContent>
|
|
129
|
+
</SidebarMenuItem>
|
|
130
|
+
</Collapsible>
|
|
131
|
+
|
|
132
|
+
<!-- Item with actions dropdown -->
|
|
133
|
+
<SidebarMenuItem v-else-if="item.actions">
|
|
134
|
+
<SidebarMenuButton
|
|
135
|
+
:asChild="isLink(item)"
|
|
136
|
+
:isActive="isActive(item.href)"
|
|
137
|
+
:tooltip="item.label"
|
|
138
|
+
@click="!isLink(item) ? item.command?.() : undefined"
|
|
139
|
+
>
|
|
140
|
+
<WebLink
|
|
141
|
+
v-if="isLink(item)"
|
|
142
|
+
:href="item.href"
|
|
143
|
+
:externalIcon="false"
|
|
144
|
+
unstyled
|
|
145
|
+
>
|
|
146
|
+
<Icon
|
|
147
|
+
v-if="item.icon"
|
|
148
|
+
:name="item.icon"
|
|
149
|
+
/>
|
|
150
|
+
<span>{{ item.label }}</span>
|
|
151
|
+
<Icon
|
|
152
|
+
v-if="isExternal(item.href!)"
|
|
153
|
+
name="external-link"
|
|
154
|
+
class="ml-auto size-3.5 text-sidebar-foreground/50"
|
|
155
|
+
/>
|
|
156
|
+
</WebLink>
|
|
157
|
+
<template v-else>
|
|
158
|
+
<Icon
|
|
159
|
+
v-if="item.icon"
|
|
160
|
+
:name="item.icon"
|
|
161
|
+
/>
|
|
162
|
+
<span>{{ item.label }}</span>
|
|
163
|
+
</template>
|
|
164
|
+
</SidebarMenuButton>
|
|
165
|
+
<DropdownMenu>
|
|
166
|
+
<DropdownMenuTrigger asChild>
|
|
167
|
+
<SidebarMenuAction showOnHover>
|
|
168
|
+
<Icon name="ellipsis" />
|
|
169
|
+
</SidebarMenuAction>
|
|
170
|
+
</DropdownMenuTrigger>
|
|
171
|
+
<DropdownMenuContent
|
|
172
|
+
side="right"
|
|
173
|
+
align="start"
|
|
174
|
+
class="min-w-48 rounded-lg"
|
|
175
|
+
>
|
|
176
|
+
<DropdownMenuItem
|
|
177
|
+
v-for="action in item.actions"
|
|
178
|
+
:key="action.label"
|
|
179
|
+
@click="action.href ? undefined : action.command?.()"
|
|
180
|
+
>
|
|
181
|
+
<WebLink
|
|
182
|
+
v-if="isLink(action)"
|
|
183
|
+
:href="action.href"
|
|
184
|
+
unstyled
|
|
185
|
+
class="flex items-center gap-2"
|
|
186
|
+
>
|
|
187
|
+
<Icon
|
|
188
|
+
v-if="action.icon"
|
|
189
|
+
:name="action.icon"
|
|
190
|
+
class="text-muted-foreground"
|
|
191
|
+
/>
|
|
192
|
+
<span>{{ action.label }}</span>
|
|
193
|
+
</WebLink>
|
|
194
|
+
<template v-else>
|
|
195
|
+
<Icon
|
|
196
|
+
v-if="action.icon"
|
|
197
|
+
:name="action.icon"
|
|
198
|
+
class="text-muted-foreground"
|
|
199
|
+
/>
|
|
200
|
+
<span>{{ action.label }}</span>
|
|
201
|
+
</template>
|
|
202
|
+
</DropdownMenuItem>
|
|
203
|
+
</DropdownMenuContent>
|
|
204
|
+
</DropdownMenu>
|
|
205
|
+
</SidebarMenuItem>
|
|
206
|
+
|
|
207
|
+
<!-- Simple item -->
|
|
208
|
+
<SidebarMenuItem v-else>
|
|
209
|
+
<SidebarMenuButton
|
|
210
|
+
:asChild="isLink(item)"
|
|
211
|
+
:isActive="isActive(item.href)"
|
|
212
|
+
:tooltip="item.label"
|
|
213
|
+
@click="!isLink(item) ? item.command?.() : undefined"
|
|
214
|
+
>
|
|
215
|
+
<WebLink
|
|
216
|
+
v-if="isLink(item)"
|
|
217
|
+
:href="item.href"
|
|
218
|
+
:externalIcon="false"
|
|
219
|
+
unstyled
|
|
220
|
+
>
|
|
221
|
+
<Icon
|
|
222
|
+
v-if="item.icon"
|
|
223
|
+
:name="item.icon"
|
|
224
|
+
/>
|
|
225
|
+
<span>{{ item.label }}</span>
|
|
226
|
+
<Icon
|
|
227
|
+
v-if="isExternal(item.href!)"
|
|
228
|
+
name="external-link"
|
|
229
|
+
class="ml-auto size-3.5 text-sidebar-foreground/50"
|
|
230
|
+
/>
|
|
231
|
+
</WebLink>
|
|
232
|
+
<template v-else>
|
|
233
|
+
<Icon
|
|
234
|
+
v-if="item.icon"
|
|
235
|
+
:name="item.icon"
|
|
236
|
+
/>
|
|
237
|
+
<span>{{ item.label }}</span>
|
|
238
|
+
</template>
|
|
239
|
+
</SidebarMenuButton>
|
|
240
|
+
</SidebarMenuItem>
|
|
241
|
+
</template>
|
|
242
|
+
</SidebarMenu>
|
|
243
|
+
</SidebarGroup>
|
|
244
|
+
</template>
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import type { AdminLayoutSidebarDropdownProfile, AdminLayoutSidebarMenuItem, AdminLayoutSidebarDropdownMenuItem } from './types'
|
|
3
|
+
import AdminLayout from './index.vue'
|
|
4
|
+
import Avatar from '../Avatar/index.vue'
|
|
5
|
+
import Breadcrumb from '../Breadcrumb/index.vue'
|
|
6
|
+
import Button from '../Button/index.vue'
|
|
7
|
+
import Card from '../Card/index.vue'
|
|
8
|
+
|
|
9
|
+
const menus: AdminLayoutSidebarMenuItem[] = [
|
|
10
|
+
{
|
|
11
|
+
label: 'Dashboard',
|
|
12
|
+
icon: 'layout-dashboard',
|
|
13
|
+
href: '#',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: 'Inbox',
|
|
17
|
+
icon: 'inbox',
|
|
18
|
+
href: '#inbox',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
label: 'Documentation',
|
|
22
|
+
icon: 'book-open',
|
|
23
|
+
group: 'Platform',
|
|
24
|
+
expanded: true,
|
|
25
|
+
children: [
|
|
26
|
+
{ label: 'Introduction', href: '#intro' },
|
|
27
|
+
{ label: 'Get Started', href: '#get-started' },
|
|
28
|
+
{ label: 'Tutorials', href: '#tutorials' },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: 'Settings',
|
|
33
|
+
icon: 'settings',
|
|
34
|
+
group: 'Platform',
|
|
35
|
+
children: [
|
|
36
|
+
{ label: 'General', href: '#general' },
|
|
37
|
+
{ label: 'Team', href: '#team' },
|
|
38
|
+
{ label: 'Billing', href: '#billing' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: 'Design Engineering',
|
|
43
|
+
icon: 'frame',
|
|
44
|
+
href: '#design',
|
|
45
|
+
group: 'Projects',
|
|
46
|
+
actions: [
|
|
47
|
+
{ label: 'View Project', icon: 'folder', href: '#design' },
|
|
48
|
+
{ label: 'Share Project', icon: 'forward', command: () => {} },
|
|
49
|
+
{ label: 'Delete Project', icon: 'trash-2', command: () => {} },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: 'Sales & Marketing',
|
|
54
|
+
icon: 'pie-chart',
|
|
55
|
+
href: '#sales',
|
|
56
|
+
group: 'Projects',
|
|
57
|
+
actions: [
|
|
58
|
+
{ label: 'View Project', icon: 'folder', href: '#sales' },
|
|
59
|
+
{ label: 'Delete Project', icon: 'trash-2', command: () => {} },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: 'shadcn-vue Docs',
|
|
64
|
+
icon: 'book-open-text',
|
|
65
|
+
href: 'https://www.shadcn-vue.com',
|
|
66
|
+
group: 'Links',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: 'Print Page',
|
|
70
|
+
icon: 'printer',
|
|
71
|
+
group: 'Links',
|
|
72
|
+
command: () => {},
|
|
73
|
+
},
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
const menuItems: AdminLayoutSidebarDropdownMenuItem[] = [
|
|
77
|
+
{ type: 'profile', title: 'Demo User', subtitle: 'demo@example.com' },
|
|
78
|
+
{ type: 'action', label: 'Account', icon: 'badge-check', command: () => {} },
|
|
79
|
+
{ type: 'action', label: 'Billing', icon: 'credit-card', command: () => {} },
|
|
80
|
+
{ type: 'action', label: 'Notifications', icon: 'bell', command: () => {} },
|
|
81
|
+
{ type: 'separator' },
|
|
82
|
+
{ type: 'action', label: 'Sign Out', icon: 'log-out', command: () => {} },
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
const profile: AdminLayoutSidebarDropdownProfile = {
|
|
86
|
+
title: 'Demo User',
|
|
87
|
+
subtitle: 'demo@example.com',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const meta = {
|
|
91
|
+
title: 'UI/AdminLayout',
|
|
92
|
+
component: AdminLayout,
|
|
93
|
+
argTypes: {
|
|
94
|
+
variant: {
|
|
95
|
+
control: 'select',
|
|
96
|
+
options: [ 'sidebar', 'floating', 'inset' ],
|
|
97
|
+
},
|
|
98
|
+
collapsible: {
|
|
99
|
+
control: 'select',
|
|
100
|
+
options: [ 'icon', 'offcanvas', 'none' ],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
args: {
|
|
104
|
+
menus,
|
|
105
|
+
footerDropdown: { profile, menuItems },
|
|
106
|
+
variant: 'sidebar',
|
|
107
|
+
collapsible: 'icon',
|
|
108
|
+
},
|
|
109
|
+
render: args => ({
|
|
110
|
+
components: { AdminLayout, Avatar, Breadcrumb, Button, Card },
|
|
111
|
+
setup () {
|
|
112
|
+
const breadcrumb = [
|
|
113
|
+
{ label: 'Dashboard', href: '#' },
|
|
114
|
+
{ label: 'Overview' },
|
|
115
|
+
]
|
|
116
|
+
return { args, breadcrumb }
|
|
117
|
+
},
|
|
118
|
+
template: `
|
|
119
|
+
<AdminLayout v-bind="args">
|
|
120
|
+
<template #navbar-left>
|
|
121
|
+
<Breadcrumb :model="breadcrumb" />
|
|
122
|
+
</template>
|
|
123
|
+
<template #navbar-right>
|
|
124
|
+
<Button variant="ghost" size="icon" icon="search" aria-label="Search" />
|
|
125
|
+
<Button variant="ghost" size="icon" icon="bell" aria-label="Notifications" />
|
|
126
|
+
<Avatar label="DU" size="small" />
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<div class="space-y-6 p-6">
|
|
130
|
+
<div class="flex items-center justify-between">
|
|
131
|
+
<div>
|
|
132
|
+
<h1 class="text-2xl font-semibold tracking-tight">Overview</h1>
|
|
133
|
+
<p class="text-sm text-muted-foreground">Welcome back, here is what is happening today.</p>
|
|
134
|
+
</div>
|
|
135
|
+
<Button icon="plus">New Project</Button>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="grid gap-4 md:grid-cols-3">
|
|
139
|
+
<Card title="Total Revenue">
|
|
140
|
+
<div class="text-2xl font-semibold">$45,231.89</div>
|
|
141
|
+
<p class="text-xs text-muted-foreground">+20.1% from last month</p>
|
|
142
|
+
</Card>
|
|
143
|
+
<Card title="Subscriptions">
|
|
144
|
+
<div class="text-2xl font-semibold">+2,350</div>
|
|
145
|
+
<p class="text-xs text-muted-foreground">+180.1% from last month</p>
|
|
146
|
+
</Card>
|
|
147
|
+
<Card title="Active Users">
|
|
148
|
+
<div class="text-2xl font-semibold">+12,234</div>
|
|
149
|
+
<p class="text-xs text-muted-foreground">+19% from last month</p>
|
|
150
|
+
</Card>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<Card title="Recent Activity">
|
|
154
|
+
<ul class="divide-y divide-border text-sm">
|
|
155
|
+
<li class="flex items-center justify-between py-3">
|
|
156
|
+
<span>Alice updated the design system</span>
|
|
157
|
+
<span class="text-xs text-muted-foreground">2 min ago</span>
|
|
158
|
+
</li>
|
|
159
|
+
<li class="flex items-center justify-between py-3">
|
|
160
|
+
<span>Bob deployed v1.4.0 to production</span>
|
|
161
|
+
<span class="text-xs text-muted-foreground">1 hour ago</span>
|
|
162
|
+
</li>
|
|
163
|
+
<li class="flex items-center justify-between py-3">
|
|
164
|
+
<span>Charlie opened a new issue</span>
|
|
165
|
+
<span class="text-xs text-muted-foreground">3 hours ago</span>
|
|
166
|
+
</li>
|
|
167
|
+
</ul>
|
|
168
|
+
</Card>
|
|
169
|
+
</div>
|
|
170
|
+
</AdminLayout>
|
|
171
|
+
`,
|
|
172
|
+
}),
|
|
173
|
+
decorators: [
|
|
174
|
+
() => ({
|
|
175
|
+
template: `
|
|
176
|
+
<div
|
|
177
|
+
class="relative h-[700px] overflow-hidden rounded-lg border border-border
|
|
178
|
+
[&_[data-slot=sidebar]>div:last-child]:absolute!
|
|
179
|
+
[&_[data-slot=sidebar]>div:last-child]:h-full!"
|
|
180
|
+
>
|
|
181
|
+
<story />
|
|
182
|
+
</div>
|
|
183
|
+
`,
|
|
184
|
+
}),
|
|
185
|
+
],
|
|
186
|
+
} satisfies Meta<typeof AdminLayout>
|
|
187
|
+
|
|
188
|
+
export default meta
|
|
189
|
+
type Story = StoryObj<typeof meta>
|
|
190
|
+
|
|
191
|
+
export const Default: Story = {}
|
|
192
|
+
|
|
193
|
+
export const Floating: Story = {
|
|
194
|
+
args: { variant: 'floating' },
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const Inset: Story = {
|
|
198
|
+
args: { variant: 'inset' },
|
|
199
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { AdminLayoutProps } from './types'
|
|
3
|
+
import Navbar from './Navbar.vue'
|
|
4
|
+
import SidebarDropdown from './SidebarDropdown.vue'
|
|
5
|
+
import SidebarMenus from './SidebarMenus.vue'
|
|
6
|
+
import {
|
|
7
|
+
Sidebar,
|
|
8
|
+
SidebarContent,
|
|
9
|
+
SidebarFooter,
|
|
10
|
+
SidebarHeader,
|
|
11
|
+
SidebarInset,
|
|
12
|
+
SidebarProvider,
|
|
13
|
+
SidebarRail,
|
|
14
|
+
} from '../../shadcn/sidebar'
|
|
15
|
+
|
|
16
|
+
withDefaults(defineProps<AdminLayoutProps>(), {
|
|
17
|
+
headerDropdown: undefined,
|
|
18
|
+
footerDropdown: undefined,
|
|
19
|
+
variant: 'sidebar',
|
|
20
|
+
collapsible: 'icon',
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<!-- Wrap with a real DOM element to avoid Vue <Transition> warning.
|
|
26
|
+
SidebarProvider's root is reka-ui TooltipProvider (renderless), which
|
|
27
|
+
Nuxt's layoutTransition cannot animate. `display: contents` keeps
|
|
28
|
+
layout unaffected. -->
|
|
29
|
+
<div class="contents">
|
|
30
|
+
<SidebarProvider>
|
|
31
|
+
<Sidebar
|
|
32
|
+
:variant="variant"
|
|
33
|
+
:collapsible="collapsible"
|
|
34
|
+
>
|
|
35
|
+
<SidebarHeader>
|
|
36
|
+
<slot name="header">
|
|
37
|
+
<SidebarDropdown
|
|
38
|
+
v-if="headerDropdown"
|
|
39
|
+
:profile="headerDropdown.profile"
|
|
40
|
+
:menuItems="headerDropdown.menuItems"
|
|
41
|
+
/>
|
|
42
|
+
</slot>
|
|
43
|
+
</SidebarHeader>
|
|
44
|
+
|
|
45
|
+
<SidebarContent>
|
|
46
|
+
<SidebarMenus :menus="menus" />
|
|
47
|
+
</SidebarContent>
|
|
48
|
+
|
|
49
|
+
<SidebarFooter>
|
|
50
|
+
<slot name="footer">
|
|
51
|
+
<SidebarDropdown
|
|
52
|
+
v-if="footerDropdown"
|
|
53
|
+
:profile="footerDropdown.profile"
|
|
54
|
+
:menuItems="footerDropdown.menuItems"
|
|
55
|
+
/>
|
|
56
|
+
</slot>
|
|
57
|
+
</SidebarFooter>
|
|
58
|
+
|
|
59
|
+
<SidebarRail />
|
|
60
|
+
</Sidebar>
|
|
61
|
+
|
|
62
|
+
<SidebarInset class="min-w-0">
|
|
63
|
+
<Navbar>
|
|
64
|
+
<div class="flex flex-1 items-center gap-3">
|
|
65
|
+
<slot name="navbar-left" />
|
|
66
|
+
</div>
|
|
67
|
+
<div class="flex items-center gap-3">
|
|
68
|
+
<slot name="navbar-right" />
|
|
69
|
+
</div>
|
|
70
|
+
</Navbar>
|
|
71
|
+
|
|
72
|
+
<div class="flex-1 overflow-auto">
|
|
73
|
+
<slot />
|
|
74
|
+
</div>
|
|
75
|
+
</SidebarInset>
|
|
76
|
+
</SidebarProvider>
|
|
77
|
+
</div>
|
|
78
|
+
</template>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DropdownItem } from '../Dropdown/types'
|
|
2
|
+
|
|
3
|
+
export interface AdminLayoutSidebarMenuItem {
|
|
4
|
+
label: string
|
|
5
|
+
icon?: string
|
|
6
|
+
href?: string
|
|
7
|
+
command?: () => void
|
|
8
|
+
group?: string
|
|
9
|
+
expanded?: boolean
|
|
10
|
+
children?: AdminLayoutSidebarMenuItem[]
|
|
11
|
+
actions?: AdminLayoutSidebarMenuItem[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AdminLayoutSidebarDropdownProfile {
|
|
15
|
+
title?: string
|
|
16
|
+
subtitle?: string
|
|
17
|
+
icon?: string
|
|
18
|
+
image?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Menu item for the SidebarDropdown.
|
|
23
|
+
*
|
|
24
|
+
* Union of Dropdown's DropdownItem with an extra 'profile' variant that renders
|
|
25
|
+
* a profile header (avatar + title + subtitle) as a label inside the menu.
|
|
26
|
+
*/
|
|
27
|
+
export type AdminLayoutSidebarDropdownMenuItem
|
|
28
|
+
= | DropdownItem
|
|
29
|
+
| ({ type: 'profile' } & AdminLayoutSidebarDropdownProfile)
|
|
30
|
+
|
|
31
|
+
export interface AdminLayoutSidebarDropdownConfig {
|
|
32
|
+
profile?: AdminLayoutSidebarDropdownProfile | null
|
|
33
|
+
menuItems?: AdminLayoutSidebarDropdownMenuItem[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AdminLayoutProps {
|
|
37
|
+
menus: AdminLayoutSidebarMenuItem[]
|
|
38
|
+
headerDropdown?: AdminLayoutSidebarDropdownConfig
|
|
39
|
+
footerDropdown?: AdminLayoutSidebarDropdownConfig
|
|
40
|
+
variant?: 'sidebar' | 'floating' | 'inset'
|
|
41
|
+
collapsible?: 'offcanvas' | 'icon' | 'none'
|
|
42
|
+
}
|
package/nuxt.config.ts
CHANGED
|
@@ -15,11 +15,35 @@ export default defineNuxtConfig({
|
|
|
15
15
|
// Package is declared as peerDependency so consumers own the version.
|
|
16
16
|
modules: [ '@nuxtjs/i18n' ],
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// and merged
|
|
18
|
+
// Lazy-loaded messages. @nuxtjs/i18n v9+ lazy-loads every locale file by
|
|
19
|
+
// default; each layer's `langDir` resolves per-layer, so the files under
|
|
20
|
+
// this layer's directory stay scoped to this layer and are merged with
|
|
21
|
+
// the consumer's own locale files per matching `code`. `langDir` must be
|
|
22
|
+
// relative (absolute paths do not survive the build) — v10's default
|
|
23
|
+
// `restructureDir: 'i18n'` roots resolution at `<layer>/i18n/`.
|
|
21
24
|
i18n: {
|
|
22
|
-
|
|
25
|
+
langDir: 'messages',
|
|
26
|
+
locales: [
|
|
27
|
+
{ code: 'ar', file: 'ar.json' },
|
|
28
|
+
{ code: 'de', file: 'de.json' },
|
|
29
|
+
{ code: 'en', file: 'en.json' },
|
|
30
|
+
{ code: 'es', file: 'es.json' },
|
|
31
|
+
{ code: 'fr', file: 'fr.json' },
|
|
32
|
+
{ code: 'hi', file: 'hi.json' },
|
|
33
|
+
{ code: 'id', file: 'id.json' },
|
|
34
|
+
{ code: 'it', file: 'it.json' },
|
|
35
|
+
{ code: 'ja', file: 'ja.json' },
|
|
36
|
+
{ code: 'ko', file: 'ko.json' },
|
|
37
|
+
{ code: 'nl', file: 'nl.json' },
|
|
38
|
+
{ code: 'pl', file: 'pl.json' },
|
|
39
|
+
{ code: 'pt', file: 'pt.json' },
|
|
40
|
+
{ code: 'ru', file: 'ru.json' },
|
|
41
|
+
{ code: 'th', file: 'th.json' },
|
|
42
|
+
{ code: 'tr', file: 'tr.json' },
|
|
43
|
+
{ code: 'vi', file: 'vi.json' },
|
|
44
|
+
{ code: 'zh-CN', file: 'zh-CN.json' },
|
|
45
|
+
{ code: 'zh-TW', file: 'zh-TW.json' },
|
|
46
|
+
],
|
|
23
47
|
},
|
|
24
48
|
|
|
25
49
|
// Register the layer's global stylesheet. Consumers extending this layer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polymarbot/nuxt-layer-shadcn-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Nuxt layer providing shadcn-vue based UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"app",
|
|
13
13
|
"i18n",
|
|
14
|
-
"i18n.config.ts",
|
|
15
14
|
"nuxt.config.ts"
|
|
16
15
|
],
|
|
17
16
|
"publishConfig": {
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
@keyframes slideIn {
|
|
2
|
-
from {
|
|
3
|
-
transform: translateX(calc(100% + var(--viewport-padding)));
|
|
4
|
-
}
|
|
5
|
-
to {
|
|
6
|
-
transform: translateX(0);
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
@keyframes hide {
|
|
11
|
-
from {
|
|
12
|
-
opacity: 1;
|
|
13
|
-
}
|
|
14
|
-
to {
|
|
15
|
-
opacity: 0;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
@keyframes swipeOut {
|
|
20
|
-
from {
|
|
21
|
-
transform: translateX(var(--radix-toast-swipe-end-x));
|
|
22
|
-
}
|
|
23
|
-
to {
|
|
24
|
-
transform: translateX(calc(100% + var(--viewport-padding)));
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
@keyframes shake {
|
|
29
|
-
0%, 100% {
|
|
30
|
-
transform: translateX(0);
|
|
31
|
-
}
|
|
32
|
-
10%, 30%, 50%, 70%, 90% {
|
|
33
|
-
transform: translateX(-4px);
|
|
34
|
-
}
|
|
35
|
-
20%, 40%, 60%, 80% {
|
|
36
|
-
transform: translateX(4px);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
@keyframes pop {
|
|
41
|
-
from {
|
|
42
|
-
transform: scale(0);
|
|
43
|
-
opacity: 0;
|
|
44
|
-
}
|
|
45
|
-
to {
|
|
46
|
-
transform: scale(1);
|
|
47
|
-
opacity: 1;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
@layer utilities {
|
|
52
|
-
.animate-slideIn {
|
|
53
|
-
animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.animate-hide {
|
|
57
|
-
animation: hide 100ms ease-in;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.animate-swipeOut {
|
|
61
|
-
animation: swipeOut 100ms ease-out;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.animate-shake {
|
|
65
|
-
animation: shake 0.5s ease-in-out;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.animate-pop {
|
|
69
|
-
animation: pop 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/* Layout transition (fade only) */
|
|
2
|
-
.layout-enter-active,
|
|
3
|
-
.layout-leave-active {
|
|
4
|
-
transition: opacity .2s;
|
|
5
|
-
}
|
|
6
|
-
.layout-enter-from,
|
|
7
|
-
.layout-leave-to {
|
|
8
|
-
opacity: 0;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/* Page transition */
|
|
12
|
-
.page-enter-active,
|
|
13
|
-
.page-leave-active {
|
|
14
|
-
transition: all .2s;
|
|
15
|
-
}
|
|
16
|
-
.page-enter-from,
|
|
17
|
-
.page-leave-to {
|
|
18
|
-
opacity: 0;
|
|
19
|
-
transform: translateX(30px);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/* Tab transition */
|
|
23
|
-
.tab-enter-active,
|
|
24
|
-
.tab-leave-active {
|
|
25
|
-
transition: all .2s;
|
|
26
|
-
}
|
|
27
|
-
.tab-enter-from {
|
|
28
|
-
opacity: 0;
|
|
29
|
-
transform: translateX(30px);
|
|
30
|
-
}
|
|
31
|
-
.tab-leave-to {
|
|
32
|
-
opacity: 0;
|
|
33
|
-
transform: translateX(-30px);
|
|
34
|
-
}
|
package/i18n.config.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vue I18n config loaded by `@nuxtjs/i18n` (configured via `nuxt.config.ts`).
|
|
3
|
-
*
|
|
4
|
-
* Pre-merged messages live in `i18n/messages/<locale>.json` (generated by
|
|
5
|
-
* `pnpm i18n:build`). This file auto-loads every JSON in that directory so
|
|
6
|
-
* adding a new language file "just works" without touching this config.
|
|
7
|
-
*
|
|
8
|
-
* Consumers that extend this Nuxt layer inherit these messages; their own
|
|
9
|
-
* translations are merged on top by `@nuxtjs/i18n`.
|
|
10
|
-
*/
|
|
11
|
-
const files = import.meta.glob('./i18n/messages/*.json', { eager: true, import: 'default' })
|
|
12
|
-
|
|
13
|
-
const messages: Record<string, unknown> = {}
|
|
14
|
-
for (const [ filepath, data ] of Object.entries(files)) {
|
|
15
|
-
const code = filepath.split('/').pop()!.replace('.json', '')
|
|
16
|
-
messages[code] = data
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export default defineI18nConfig(() => ({ messages }))
|