@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.
@@ -2,13 +2,13 @@
2
2
  /* Reference: https://www.shadcn-vue.com/themes */
3
3
 
4
4
  :root {
5
- /* Background scale — base / surface / muted / accent */
6
- --background: oklch(1 0 0);
7
- --background-surface: oklch(0.98 0 0);
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 scale — base / muted / accent */
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
- /* Semantic status */
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 scale */
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 scale */
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
- /* Semantic status */
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 — surface elevation */
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
- /* Sidebar — inherits from main theme */
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 all source files in this app */
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: 'Page, card and popover backgrounds pair each with its matching foreground.',
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 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
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>
@@ -21,6 +21,3 @@
21
21
  to { height: 0; }
22
22
  }
23
23
  }
24
-
25
- /* Safe area utilities */
26
- @utility pb-safe { padding-bottom: env(safe-area-inset-bottom); }
@@ -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
- // Explicit absolute path so the layer's messages load regardless of the
19
- // consumer's vueI18n setting (each layer's vueI18n is resolved per-layer
20
- // and merged by @nuxtjs/i18n).
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
- vueI18n: join(currentDir, 'i18n.config.ts'),
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.2",
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 }))