@polymarbot/nuxt-layer-shadcn-ui 0.1.3 → 0.1.5

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
+ }
@@ -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>
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymarbot/nuxt-layer-shadcn-ui",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Nuxt layer providing shadcn-vue based UI components",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -41,5 +41,6 @@
41
41
  "vue": "^3",
42
42
  "vue-i18n": "^11",
43
43
  "vue-router": "^4 || ^5"
44
- }
44
+ },
45
+ "gitHead": "614507b8e3682b8d8d3ee10e9b26155b60b6923b"
45
46
  }