@polymarbot/nuxt-layer-shadcn-ui 0.9.6 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,9 @@
1
+ import type { Component } from 'vue'
1
2
  import type { DropdownItem } from '../Dropdown/types'
2
3
 
3
4
  export interface AdminLayoutSidebarMenuItem {
4
5
  label: string
5
- icon?: string
6
+ icon?: string | Component
6
7
  href?: string
7
8
  command?: () => void
8
9
  group?: string
@@ -14,7 +15,7 @@ export interface AdminLayoutSidebarMenuItem {
14
15
  export interface AdminLayoutSidebarDropdownProfile {
15
16
  title?: string
16
17
  subtitle?: string
17
- icon?: string
18
+ icon?: string | Component
18
19
  image?: string
19
20
  }
20
21
 
@@ -54,13 +54,9 @@ const hasDescription = computed(() => Boolean(slots.default || props.description
54
54
  <ShadcnAlert :class="mergedClass">
55
55
  <slot name="icon">
56
56
  <Icon
57
- v-if="typeof icon === 'string' && icon"
57
+ v-if="icon"
58
58
  :name="icon"
59
59
  />
60
- <component
61
- :is="icon"
62
- v-else-if="icon"
63
- />
64
60
  <Icon
65
61
  v-else-if="defaultIconName"
66
62
  :name="defaultIconName"
@@ -1,6 +1,8 @@
1
+ import type { Component } from 'vue'
2
+
1
3
  export interface BreadcrumbItem {
2
4
  label?: string
3
- icon?: string
5
+ icon?: string | Component
4
6
  href?: string
5
7
  target?: string
6
8
  }
@@ -1,4 +1,5 @@
1
1
  import type { ButtonVariants } from '../../shadcn/button'
2
+ import type { Component } from 'vue'
2
3
  import type { RouteLocationRaw } from 'vue-router'
3
4
 
4
5
  export type ButtonVariant = ButtonVariants['variant']
@@ -10,7 +11,7 @@ export interface ButtonProps {
10
11
  loading?: boolean
11
12
  disabled?: boolean
12
13
  rounded?: boolean
13
- icon?: string
14
+ icon?: string | Component
14
15
  iconPosition?: 'start' | 'end'
15
16
  href?: string
16
17
  to?: RouteLocationRaw
@@ -3,7 +3,6 @@ import { cva } from 'class-variance-authority'
3
3
  import type { DropdownActionItem } from './types'
4
4
 
5
5
  const props = defineProps<{
6
- /** Icon name (lucide kebab-case) or a Vue component. */
7
6
  icon: DropdownActionItem['icon']
8
7
  /** Override icon color independently of the surrounding item color. */
9
8
  iconColor?: DropdownActionItem['iconColor']
@@ -29,13 +28,8 @@ const colorClass = computed(() => iconColorVariants({ color: props.iconColor }))
29
28
 
30
29
  <template>
31
30
  <Icon
32
- v-if="typeof icon === 'string'"
31
+ v-if="icon"
33
32
  :name="icon"
34
33
  :class="colorClass"
35
34
  />
36
- <component
37
- :is="icon"
38
- v-else-if="icon"
39
- :class="cn('size-4', colorClass)"
40
- />
41
35
  </template>
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { Coffee } from 'lucide-vue-next'
2
3
  import Icon from './index.vue'
3
4
 
4
5
  const commonIcons = [
@@ -75,6 +76,30 @@ export const CommonIcons: Story = {
75
76
  }),
76
77
  }
77
78
 
79
+ export const FromComponent: Story = {
80
+ parameters: {
81
+ ...noControls,
82
+ docs: {
83
+ source: {
84
+ code: `
85
+ <script setup lang="ts">
86
+ import { Coffee } from 'lucide-vue-next'
87
+ </script>
88
+
89
+ <template>
90
+ <Icon :name="Coffee" class="size-6" />
91
+ </template>
92
+ `.trim(),
93
+ },
94
+ },
95
+ },
96
+ render: () => ({
97
+ components: { Icon },
98
+ setup: () => ({ Coffee }),
99
+ template: '<Icon :name="Coffee" class="size-6" />',
100
+ }),
101
+ }
102
+
78
103
  export const Sizes: Story = {
79
104
  parameters: {
80
105
  ...noControls,
@@ -14,6 +14,7 @@ function toPascalCase (str: string): string {
14
14
  }
15
15
 
16
16
  const iconComponent = computed<Component | undefined>(() => {
17
+ if (typeof props.name !== 'string') return props.name
17
18
  const pascalName = toPascalCase(props.name)
18
19
  return (icons as Record<string, Component>)[pascalName]
19
20
  })
@@ -1,5 +1,7 @@
1
+ import type { Component } from 'vue'
2
+
1
3
  export interface IconProps {
2
- /** Icon name in kebab-case (e.g. "mail", "arrow-left", "circle-question-mark") */
3
- name: string
4
+ /** Lucide icon name in kebab-case (e.g. "arrow-left") or a Vue component. */
5
+ name: string | Component
4
6
  class?: ClassValue
5
7
  }
@@ -1,9 +1,9 @@
1
- import type { VNode } from 'vue'
1
+ import type { Component, VNode } from 'vue'
2
2
 
3
3
  export type ModalContentType = 'default' | 'success' | 'info' | 'help' | 'warn' | 'danger' | 'error'
4
4
 
5
5
  export interface ModalContentProps {
6
6
  type?: ModalContentType
7
- icon?: string
7
+ icon?: string | Component
8
8
  content?: string | VNode | HTMLElement
9
9
  }
@@ -34,7 +34,7 @@ function getTriggerClass (item: TabsItem) {
34
34
  const showsOnlyIcon = !!item.icon && (props.iconOnly || !item.title)
35
35
  return cn(
36
36
  props.rounded && 'rounded-full',
37
- showsOnlyIcon && 'aspect-square flex-none px-0',
37
+ showsOnlyIcon && 'px-0 aspect-square flex-none',
38
38
  props.triggerClass,
39
39
  )
40
40
  }
@@ -62,13 +62,9 @@ function getTriggerClass (item: TabsItem) {
62
62
  :active="activeValue === item.value"
63
63
  >
64
64
  <Icon
65
- v-if="item.icon && typeof item.icon === 'string'"
65
+ v-if="item.icon"
66
66
  :name="item.icon"
67
67
  />
68
- <component
69
- :is="item.icon"
70
- v-else-if="item.icon"
71
- />
72
68
  <span v-if="item.title && !iconOnly">
73
69
  {{ item.title }}
74
70
  </span>
@@ -0,0 +1,21 @@
1
+ {
2
+ "drag": {
3
+ "title": "Click or drag file to this area to upload",
4
+ "titleMultiple": "Click or drag files to this area to upload"
5
+ },
6
+ "empty": "No files",
7
+ "error": {
8
+ "oversize": "{files} exceeds the {size} size limit."
9
+ },
10
+ "hint": {
11
+ "max": "Up to {max} files can be selected.",
12
+ "maxSize": "Max size per file: {size}.",
13
+ "multiple": "Multiple files can be selected.",
14
+ "single": "Only a single file can be selected."
15
+ },
16
+ "preview": "Preview",
17
+ "remove": "Remove",
18
+ "upload": "Upload",
19
+ "uploadDirectory": "Upload Directory",
20
+ "uploading": "Uploading"
21
+ }
@@ -0,0 +1,381 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import EventLog from '#storybook/EventLog.vue'
3
+ import type { UploadFile, UploadVariant } from './types'
4
+ import Upload from './index.vue'
5
+
6
+ const variants: UploadVariant[] = [ 'button', 'box', 'drag' ]
7
+
8
+ const sampleImage = 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200&h=200&fit=crop'
9
+ const sampleImage2 = 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200&h=200&fit=crop'
10
+
11
+ const sampleImages: UploadFile[] = [
12
+ { uid: 's-img-1', name: 'avatar.png', url: sampleImage, status: 'done' },
13
+ { uid: 's-img-2', name: 'cover.png', url: sampleImage2, status: 'done' },
14
+ ]
15
+
16
+ const meta = {
17
+ title: 'UI/Upload',
18
+ component: Upload,
19
+ argTypes: {
20
+ variant: { control: 'select', options: variants },
21
+ accept: { control: 'text' },
22
+ disabled: { control: 'boolean' },
23
+ readonly: { control: 'boolean' },
24
+ invalid: { control: 'boolean' },
25
+ multiple: { control: 'boolean' },
26
+ maxCount: { control: 'number' },
27
+ maxSize: { control: 'number' },
28
+ directory: { control: 'boolean' },
29
+ text: { control: 'text' },
30
+ icon: { control: 'text' },
31
+ },
32
+ args: {
33
+ variant: 'button',
34
+ accept: '',
35
+ disabled: false,
36
+ readonly: false,
37
+ invalid: false,
38
+ multiple: false,
39
+ maxCount: undefined,
40
+ maxSize: undefined,
41
+ directory: false,
42
+ text: '',
43
+ icon: '',
44
+ },
45
+ // fileList carries `raw: File` after a user picks a file, which is not
46
+ // structured-cloneable — Storybook's `updateArgs` drops such values, so
47
+ // binding fileList through args makes uploads appear to do nothing. Use a
48
+ // local ref instead, optionally seeded from `args.fileList`.
49
+ render: args => ({
50
+ components: { Upload },
51
+ setup () {
52
+ const initial = Array.isArray(args.fileList) ? args.fileList as UploadFile[] : []
53
+ const fileList = ref<UploadFile[]>([ ...initial ])
54
+ return { args, fileList }
55
+ },
56
+ template: `
57
+ <div class="max-w-2xl">
58
+ <Upload v-bind="args" v-model:fileList="fileList" />
59
+ </div>
60
+ `,
61
+ }),
62
+ } satisfies Meta<typeof Upload>
63
+
64
+ export default meta
65
+ type Story = StoryObj<typeof meta>
66
+
67
+ const noControls = { controls: { disable: true }} satisfies Story['parameters']
68
+
69
+ export const Default: Story = {}
70
+
71
+ // --- Variant showcase ---
72
+
73
+ export const VariantButton: Story = {
74
+ parameters: noControls,
75
+ args: { variant: 'button' },
76
+ }
77
+
78
+ export const VariantBox: Story = {
79
+ parameters: noControls,
80
+ args: {
81
+ variant: 'box',
82
+ accept: 'image/*',
83
+ multiple: true,
84
+ maxCount: 5,
85
+ fileList: sampleImages,
86
+ },
87
+ }
88
+
89
+ export const VariantDrag: Story = {
90
+ parameters: noControls,
91
+ args: { variant: 'drag' },
92
+ }
93
+
94
+ export const AcceptImagesOnly: Story = {
95
+ parameters: noControls,
96
+ args: {
97
+ variant: 'drag',
98
+ accept: 'image/*',
99
+ multiple: true,
100
+ },
101
+ }
102
+
103
+ // --- Selection behavior (all three variants side by side) ---
104
+
105
+ export const Multiple: Story = {
106
+ parameters: noControls,
107
+ render: () => ({
108
+ components: { Upload },
109
+ setup: () => ({ variants }),
110
+ template: `
111
+ <div class="max-w-2xl space-y-6">
112
+ <Upload v-for="v in variants" :key="v" :variant="v" multiple />
113
+ </div>
114
+ `,
115
+ }),
116
+ }
117
+
118
+ export const MaxCount: Story = {
119
+ parameters: noControls,
120
+ render: () => ({
121
+ components: { Upload },
122
+ setup: () => ({ variants }),
123
+ template: `
124
+ <div class="max-w-2xl space-y-6">
125
+ <Upload v-for="v in variants" :key="v" :variant="v" multiple :maxCount="3" />
126
+ </div>
127
+ `,
128
+ }),
129
+ }
130
+
131
+ export const MaxSize: Story = {
132
+ parameters: noControls,
133
+ render: () => ({
134
+ components: { Upload },
135
+ setup () {
136
+ const errorMessage = ref('')
137
+ const onError = (e: unknown) => {
138
+ errorMessage.value = e instanceof Error ? e.message : String(e)
139
+ }
140
+ return { variants, errorMessage, onError }
141
+ },
142
+ template: `
143
+ <div class="max-w-2xl space-y-6">
144
+ <Upload
145
+ v-for="v in variants"
146
+ :key="v"
147
+ :variant="v"
148
+ multiple
149
+ :maxSize="50 * 1024"
150
+ @error="onError"
151
+ />
152
+ <p v-if="errorMessage" class="text-sm text-danger">
153
+ {{ errorMessage }}
154
+ </p>
155
+ </div>
156
+ `,
157
+ }),
158
+ }
159
+
160
+ export const Directory: Story = {
161
+ parameters: noControls,
162
+ render: () => ({
163
+ components: { Upload },
164
+ setup: () => ({ variants }),
165
+ template: `
166
+ <div class="max-w-2xl space-y-6">
167
+ <Upload v-for="v in variants" :key="v" :variant="v" directory :maxCount="5" />
168
+ </div>
169
+ `,
170
+ }),
171
+ }
172
+
173
+ // --- States ---
174
+
175
+ export const Disabled: Story = {
176
+ parameters: noControls,
177
+ render: () => ({
178
+ components: { Upload },
179
+ setup () {
180
+ const fileList = ref<UploadFile[]>([ ...sampleImages ])
181
+ return { variants, fileList }
182
+ },
183
+ template: `
184
+ <div class="max-w-2xl space-y-6">
185
+ <Upload
186
+ v-for="v in variants"
187
+ :key="v"
188
+ :variant="v"
189
+ disabled
190
+ multiple
191
+ :fileList="fileList"
192
+ />
193
+ </div>
194
+ `,
195
+ }),
196
+ }
197
+
198
+ export const Readonly: Story = {
199
+ parameters: noControls,
200
+ render: () => ({
201
+ components: { Upload },
202
+ setup () {
203
+ const fileList = ref<UploadFile[]>([ ...sampleImages ])
204
+ return { variants, fileList }
205
+ },
206
+ template: `
207
+ <div class="max-w-2xl space-y-6">
208
+ <Upload
209
+ v-for="v in variants"
210
+ :key="v"
211
+ :variant="v"
212
+ readonly
213
+ multiple
214
+ :fileList="fileList"
215
+ />
216
+ </div>
217
+ `,
218
+ }),
219
+ }
220
+
221
+ export const ReadonlyEmpty: Story = {
222
+ parameters: noControls,
223
+ render: () => ({
224
+ components: { Upload },
225
+ setup: () => ({ variants }),
226
+ template: `
227
+ <div class="max-w-2xl space-y-6">
228
+ <Upload v-for="v in variants" :key="v" :variant="v" readonly />
229
+ </div>
230
+ `,
231
+ }),
232
+ }
233
+
234
+ export const Invalid: Story = {
235
+ parameters: noControls,
236
+ args: {
237
+ variant: 'drag',
238
+ invalid: true,
239
+ multiple: true,
240
+ fileList: [
241
+ { uid: 'i-1', name: 'gitmoji (1).md', status: 'done' },
242
+ { uid: 'i-2', name: 'cd973fbf55b88a9ebaec4f01821c552a.png', status: 'done' },
243
+ ],
244
+ },
245
+ }
246
+
247
+ // --- Custom behaviors ---
248
+
249
+ export const BeforeUpload: Story = {
250
+ parameters: {
251
+ ...noControls,
252
+ docs: {
253
+ source: {
254
+ code: `
255
+ <template>
256
+ <Upload
257
+ v-model:fileList="fileList"
258
+ variant="drag"
259
+ multiple
260
+ :beforeUpload="beforeUpload"
261
+ />
262
+ </template>
263
+
264
+ <script setup lang="ts">
265
+ const fileList = ref<UploadFile[]>([])
266
+
267
+ function beforeUpload (file: File) {
268
+ // Reject files larger than 1MB
269
+ if (file.size > 1024 * 1024) {
270
+ alert(\`\${file.name} is too large (>1MB)\`)
271
+ return false
272
+ }
273
+ // Optionally return a Promise<File | Blob> to replace the file
274
+ return true
275
+ }
276
+ </script>
277
+ `.trim(),
278
+ },
279
+ },
280
+ },
281
+ render: () => ({
282
+ components: { Upload },
283
+ setup () {
284
+ const fileList = ref<UploadFile[]>([])
285
+ function beforeUpload (file: File) {
286
+ if (file.size > 1024 * 1024) {
287
+ alert(`${file.name} is too large (>1MB)`)
288
+ return false
289
+ }
290
+ return true
291
+ }
292
+ return { fileList, beforeUpload }
293
+ },
294
+ template: `
295
+ <div class="max-w-2xl">
296
+ <Upload
297
+ v-model:fileList="fileList"
298
+ variant="drag"
299
+ multiple
300
+ :beforeUpload="beforeUpload"
301
+ />
302
+ </div>
303
+ `,
304
+ }),
305
+ }
306
+
307
+ export const CustomUpload: Story = {
308
+ parameters: {
309
+ ...noControls,
310
+ docs: {
311
+ source: {
312
+ code: `
313
+ <template>
314
+ <Upload
315
+ v-model:fileList="fileList"
316
+ variant="box"
317
+ accept="image/*"
318
+ multiple
319
+ :maxCount="5"
320
+ :upload="upload"
321
+ />
322
+ </template>
323
+
324
+ <script setup lang="ts">
325
+ const fileList = ref<UploadFile[]>([])
326
+
327
+ async function upload (files: (File | Blob)[]) {
328
+ // Simulate network latency
329
+ await new Promise(resolve => setTimeout(resolve, 1500))
330
+ // throw new Error('Upload failed') would mark items as error
331
+ }
332
+ </script>
333
+ `.trim(),
334
+ },
335
+ },
336
+ },
337
+ render: () => ({
338
+ components: { Upload },
339
+ setup () {
340
+ const fileList = ref<UploadFile[]>([])
341
+ async function upload () {
342
+ await new Promise<void>(resolve => setTimeout(resolve, 1500))
343
+ }
344
+ return { fileList, upload }
345
+ },
346
+ template: `
347
+ <div class="max-w-2xl">
348
+ <Upload
349
+ v-model:fileList="fileList"
350
+ variant="box"
351
+ accept="image/*"
352
+ multiple
353
+ :maxCount="5"
354
+ :upload="upload"
355
+ />
356
+ </div>
357
+ `,
358
+ }),
359
+ }
360
+
361
+ export const EventHandling: Story = {
362
+ parameters: noControls,
363
+ render: () => ({
364
+ components: { Upload, EventLog },
365
+ setup: () => ({ fileList: ref<UploadFile[]>([]) }),
366
+ template: `
367
+ <EventLog v-slot="{ record }">
368
+ <Upload
369
+ v-model:fileList="fileList"
370
+ variant="drag"
371
+ multiple
372
+ @update:fileList="(v) => record('update:fileList', v.map(f => f.name))"
373
+ @change="(v) => record('change', v.map(f => f.name))"
374
+ @remove="(f) => record('remove', f.name)"
375
+ @preview="(f) => record('preview', f.name)"
376
+ @error="(e) => record('error', String(e))"
377
+ />
378
+ </EventLog>
379
+ `,
380
+ }),
381
+ }