@polymarbot/nuxt-layer-shadcn-ui 0.3.8 → 0.3.10
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/components/ui/DataTable/CellSlot.ts +40 -0
- package/app/components/ui/DataTable/index.stories.ts +45 -11
- package/app/components/ui/DataTable/index.vue +20 -16
- package/app/components/ui/Input/index.stories.ts +0 -17
- package/app/components/ui/Input/index.vue +1 -0
- package/app/components/ui/Pagination/index.stories.ts +0 -27
- package/app/components/ui/Popover/index.stories.ts +0 -24
- package/app/components/ui/RadioGroup/index.stories.ts +0 -17
- package/app/components/ui/Select/index.vue +7 -6
- package/app/components/ui/Tabs/index.stories.ts +0 -17
- package/app/components/ui/Textarea/index.stories.ts +0 -17
- package/package.json +2 -2
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Wraps a scoped table-cell slot. Branches by `slotFn` state:
|
|
2
|
+
// - missing → render `#default` (no custom slot; show normal value)
|
|
3
|
+
// - returns non-empty VNodes → render those
|
|
4
|
+
// - returns empty (only Comment / empty Text / empty Fragment) → render
|
|
5
|
+
// `#empty` (slot is authoritative — don't fall back to the value)
|
|
6
|
+
//
|
|
7
|
+
// Empty detection catches `v-if` / `{{ value }}` returning nothing — but a
|
|
8
|
+
// rendered element with no content (e.g. `<div/>`) still counts as non-empty.
|
|
9
|
+
|
|
10
|
+
import { Comment, Fragment, Text, type FunctionalComponent, type VNode } from 'vue'
|
|
11
|
+
|
|
12
|
+
function isEmptyVNodes (vnodes: VNode[] | undefined): boolean {
|
|
13
|
+
if (!vnodes?.length) return true
|
|
14
|
+
return vnodes.every(vnode => {
|
|
15
|
+
if (vnode.type === Comment) return true
|
|
16
|
+
if (vnode.type === Text) {
|
|
17
|
+
return typeof vnode.children === 'string'
|
|
18
|
+
? vnode.children.trim() === ''
|
|
19
|
+
: !vnode.children
|
|
20
|
+
}
|
|
21
|
+
if (vnode.type === Fragment) return isEmptyVNodes(vnode.children as VNode[])
|
|
22
|
+
return false
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const CellSlot: FunctionalComponent<{
|
|
27
|
+
slotFn?: (scope: Record<string, unknown>) => VNode[]
|
|
28
|
+
scope: Record<string, unknown>
|
|
29
|
+
}> = (props, { slots }) => {
|
|
30
|
+
if (props.slotFn) {
|
|
31
|
+
const vnodes = props.slotFn(props.scope)
|
|
32
|
+
if (!isEmptyVNodes(vnodes)) return vnodes
|
|
33
|
+
return slots.empty?.()
|
|
34
|
+
}
|
|
35
|
+
return slots.default?.()
|
|
36
|
+
}
|
|
37
|
+
CellSlot.props = [ 'slotFn', 'scope' ]
|
|
38
|
+
CellSlot.inheritAttrs = false
|
|
39
|
+
|
|
40
|
+
export default CellSlot
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import Tag from '../Tag/index.vue'
|
|
2
3
|
import type { DataTableColumn } from './types'
|
|
3
4
|
import DataTable from './index.vue'
|
|
4
5
|
|
|
@@ -191,22 +192,15 @@ export const ColumnTypes: Story = {
|
|
|
191
192
|
export const CustomSlots: Story = {
|
|
192
193
|
parameters: noControls,
|
|
193
194
|
render: () => ({
|
|
194
|
-
components: { DataTable: DataTable as any },
|
|
195
|
+
components: { DataTable: DataTable as any, Tag },
|
|
195
196
|
setup: () => ({ data: sampleData, slotColumns }),
|
|
196
197
|
template: `
|
|
197
198
|
<div class="w-full">
|
|
198
199
|
<DataTable :data="data" :columns="slotColumns">
|
|
199
200
|
<template #status="{ value }">
|
|
200
|
-
<
|
|
201
|
-
:class="[
|
|
202
|
-
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
|
|
203
|
-
value === 'active'
|
|
204
|
-
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
|
205
|
-
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200',
|
|
206
|
-
]"
|
|
207
|
-
>
|
|
201
|
+
<Tag :color="value === 'active' ? 'success' : 'default'">
|
|
208
202
|
{{ value }}
|
|
209
|
-
</
|
|
203
|
+
</Tag>
|
|
210
204
|
</template>
|
|
211
205
|
<template #amount="{ value }">
|
|
212
206
|
<span class="font-mono font-medium">\${{ Number(value).toFixed(2) }}</span>
|
|
@@ -217,6 +211,46 @@ export const CustomSlots: Story = {
|
|
|
217
211
|
}),
|
|
218
212
|
}
|
|
219
213
|
|
|
214
|
+
export const SlotEmptyFallback: Story = {
|
|
215
|
+
parameters: noControls,
|
|
216
|
+
render: () => ({
|
|
217
|
+
components: { DataTable: DataTable as any, Tag },
|
|
218
|
+
setup () {
|
|
219
|
+
const data = [
|
|
220
|
+
{ name: 'Empty value (no slot)', email: '', status: 'active', action: 'Edit' },
|
|
221
|
+
{ name: 'Empty slot output', email: 'demo@example.com', status: 'pending', action: 'Edit' },
|
|
222
|
+
{ name: 'Column type=\'empty\'', email: 'demo@example.com', status: 'active', action: '' },
|
|
223
|
+
]
|
|
224
|
+
const columns: DataTableColumn[] = [
|
|
225
|
+
{ field: 'name', title: 'Demo case', minWidth: '200px' },
|
|
226
|
+
{ field: 'email', title: 'Email', minWidth: '200px' },
|
|
227
|
+
{ field: 'status', title: 'Status', width: '120px' },
|
|
228
|
+
{ field: 'action', title: 'Action', width: '120px', type: 'empty' },
|
|
229
|
+
]
|
|
230
|
+
return { data, columns }
|
|
231
|
+
},
|
|
232
|
+
template: `
|
|
233
|
+
<div class="w-full">
|
|
234
|
+
<DataTable :data="data" :columns="columns">
|
|
235
|
+
<template #status="{ value }">
|
|
236
|
+
<Tag
|
|
237
|
+
v-if="value === 'active' || value === 'inactive'"
|
|
238
|
+
:color="value === 'active' ? 'success' : 'default'"
|
|
239
|
+
>
|
|
240
|
+
{{ value }}
|
|
241
|
+
</Tag>
|
|
242
|
+
</template>
|
|
243
|
+
<template #action="{ value }">
|
|
244
|
+
<button v-if="value" class="text-sm text-primary underline">
|
|
245
|
+
{{ value }}
|
|
246
|
+
</button>
|
|
247
|
+
</template>
|
|
248
|
+
</DataTable>
|
|
249
|
+
</div>
|
|
250
|
+
`,
|
|
251
|
+
}),
|
|
252
|
+
}
|
|
253
|
+
|
|
220
254
|
export const EmptyState: Story = {
|
|
221
255
|
parameters: noControls,
|
|
222
256
|
render: () => ({
|
|
@@ -233,7 +267,7 @@ export const EmptyState: Story = {
|
|
|
233
267
|
export const FooterSlot: Story = {
|
|
234
268
|
parameters: noControls,
|
|
235
269
|
render: () => ({
|
|
236
|
-
components: { DataTable: DataTable as any },
|
|
270
|
+
components: { DataTable: DataTable as any, Tag },
|
|
237
271
|
setup: () => ({ data: sampleData, slotColumns }),
|
|
238
272
|
template: `
|
|
239
273
|
<div class="w-full">
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
TableHeader,
|
|
12
12
|
TableRow,
|
|
13
13
|
} from '../../shadcn/table'
|
|
14
|
+
import CellSlot from './CellSlot'
|
|
14
15
|
import type {
|
|
15
16
|
DataTableColumn,
|
|
16
17
|
DataTableProps,
|
|
@@ -255,6 +256,7 @@ function buildColumnStyle (column: DataTableColumn): Record<string, string> {
|
|
|
255
256
|
const headerCellClass = 'h-auto bg-border px-4 py-3 text-xs font-normal text-foreground'
|
|
256
257
|
const headerDividerClass = 'relative after:absolute after:top-1/2 after:right-0 after:h-4 after:w-px after:-translate-y-1/2 after:bg-muted-foreground/25'
|
|
257
258
|
const stickyLeftClass = 'sticky left-0 z-10'
|
|
259
|
+
const emptyCellClass = 'h-0.5 w-2.5 bg-muted-foreground/50 inline-block rounded-full align-middle'
|
|
258
260
|
|
|
259
261
|
const selectionColumnClass = '[&:has([role=checkbox])]:pr-4'
|
|
260
262
|
const selectionColumnStyle = { width: '1%' }
|
|
@@ -383,24 +385,26 @@ const selectionColumnShadowDir = computed<FrozenShadow | undefined>(() =>
|
|
|
383
385
|
:style="buildColumnStyle(column)"
|
|
384
386
|
:data-shadow="buildFrozenShadow(column)"
|
|
385
387
|
>
|
|
386
|
-
<
|
|
387
|
-
:
|
|
388
|
-
:
|
|
389
|
-
:row="row"
|
|
390
|
-
:value="get(row, column.field)"
|
|
391
|
-
:index="index"
|
|
388
|
+
<CellSlot
|
|
389
|
+
:slotFn="$slots[column.field]"
|
|
390
|
+
:scope="{ column, row, value: get(row, column.field), index }"
|
|
392
391
|
>
|
|
393
|
-
<
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
392
|
+
<template #default>
|
|
393
|
+
<span
|
|
394
|
+
v-if="!formatCellValue(get(row, column.field), column) && column.type !== 'empty'"
|
|
395
|
+
:class="emptyCellClass"
|
|
396
|
+
/>
|
|
397
|
+
<template v-else>
|
|
398
|
+
{{ formatCellValue(get(row, column.field), column) }}
|
|
399
|
+
</template>
|
|
400
|
+
</template>
|
|
401
|
+
<template
|
|
402
|
+
v-if="column.type !== 'empty'"
|
|
403
|
+
#empty
|
|
404
|
+
>
|
|
405
|
+
<span :class="emptyCellClass" />
|
|
402
406
|
</template>
|
|
403
|
-
</
|
|
407
|
+
</CellSlot>
|
|
404
408
|
</TableCell>
|
|
405
409
|
</TableRow>
|
|
406
410
|
</template>
|
|
@@ -35,23 +35,6 @@ const noControls = { controls: { disable: true }} satisfies Story['parameters']
|
|
|
35
35
|
|
|
36
36
|
export const Default: Story = {}
|
|
37
37
|
|
|
38
|
-
export const Controlled: Story = {
|
|
39
|
-
parameters: noControls,
|
|
40
|
-
render: () => ({
|
|
41
|
-
components: { Input },
|
|
42
|
-
setup () {
|
|
43
|
-
const value = ref('')
|
|
44
|
-
return { value }
|
|
45
|
-
},
|
|
46
|
-
template: `
|
|
47
|
-
<div class="max-w-sm space-y-2">
|
|
48
|
-
<Input v-model="value" placeholder="Enter your name..." />
|
|
49
|
-
<div class="text-sm text-muted-foreground">Value: {{ value }}</div>
|
|
50
|
-
</div>
|
|
51
|
-
`,
|
|
52
|
-
}),
|
|
53
|
-
}
|
|
54
|
-
|
|
55
38
|
export const WithPrefix: Story = {
|
|
56
39
|
parameters: noControls,
|
|
57
40
|
render: () => ({
|
|
@@ -38,33 +38,6 @@ const noControls = { controls: { disable: true }} satisfies Story['parameters']
|
|
|
38
38
|
|
|
39
39
|
export const Default: Story = {}
|
|
40
40
|
|
|
41
|
-
export const Controlled: Story = {
|
|
42
|
-
parameters: noControls,
|
|
43
|
-
render: () => ({
|
|
44
|
-
components: { Pagination },
|
|
45
|
-
setup () {
|
|
46
|
-
const page = ref(1)
|
|
47
|
-
const pageSize = ref(10)
|
|
48
|
-
return { page, pageSize }
|
|
49
|
-
},
|
|
50
|
-
template: `
|
|
51
|
-
<div>
|
|
52
|
-
<Pagination
|
|
53
|
-
:page="page"
|
|
54
|
-
:total="85"
|
|
55
|
-
:pageSize="pageSize"
|
|
56
|
-
:pageSizeOptions="[10, 20, 50]"
|
|
57
|
-
@update:page="page = $event"
|
|
58
|
-
@update:pageSize="v => { pageSize = v; page = 1 }"
|
|
59
|
-
/>
|
|
60
|
-
<div class="mt-2 text-sm text-muted-foreground">
|
|
61
|
-
Page: {{ page }}, Size: {{ pageSize }}
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
`,
|
|
65
|
-
}),
|
|
66
|
-
}
|
|
67
|
-
|
|
68
41
|
export const Sizes: Story = {
|
|
69
42
|
parameters: noControls,
|
|
70
43
|
render: () => ({
|
|
@@ -47,27 +47,3 @@ export const WithForm: Story = {
|
|
|
47
47
|
`,
|
|
48
48
|
}),
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
export const Controlled: Story = {
|
|
52
|
-
parameters: noControls,
|
|
53
|
-
render: () => ({
|
|
54
|
-
components: { Popover, Button },
|
|
55
|
-
setup () {
|
|
56
|
-
const open = ref(false)
|
|
57
|
-
return { open }
|
|
58
|
-
},
|
|
59
|
-
template: `
|
|
60
|
-
<div class="flex items-center gap-2">
|
|
61
|
-
<Popover v-model:open="open">
|
|
62
|
-
<template #trigger>
|
|
63
|
-
<Button variant="outline">Controlled Popover</Button>
|
|
64
|
-
</template>
|
|
65
|
-
<p class="text-sm">This popover is controlled externally.</p>
|
|
66
|
-
</Popover>
|
|
67
|
-
<Button variant="ghost" size="sm" @click="open = !open">
|
|
68
|
-
Toggle ({{ open ? 'Open' : 'Closed' }})
|
|
69
|
-
</Button>
|
|
70
|
-
</div>
|
|
71
|
-
`,
|
|
72
|
-
}),
|
|
73
|
-
}
|
|
@@ -43,23 +43,6 @@ const noControls = { controls: { disable: true }} satisfies Story['parameters']
|
|
|
43
43
|
|
|
44
44
|
export const Default: Story = {}
|
|
45
45
|
|
|
46
|
-
export const Controlled: Story = {
|
|
47
|
-
parameters: noControls,
|
|
48
|
-
render: () => ({
|
|
49
|
-
components: { RadioGroup },
|
|
50
|
-
setup () {
|
|
51
|
-
const selected = ref('option1')
|
|
52
|
-
return { options, selected }
|
|
53
|
-
},
|
|
54
|
-
template: `
|
|
55
|
-
<div class="space-y-2">
|
|
56
|
-
<RadioGroup v-model="selected" :items="options" />
|
|
57
|
-
<div class="text-sm text-muted-foreground">Selected: {{ selected }}</div>
|
|
58
|
-
</div>
|
|
59
|
-
`,
|
|
60
|
-
}),
|
|
61
|
-
}
|
|
62
|
-
|
|
63
46
|
export const Horizontal: Story = {
|
|
64
47
|
parameters: noControls,
|
|
65
48
|
render: () => ({
|
|
@@ -156,16 +156,16 @@ function handleClear (event: MouseEvent) {
|
|
|
156
156
|
:data-disabled="disabled || undefined"
|
|
157
157
|
:data-state="open ? 'open' : 'closed'"
|
|
158
158
|
class="
|
|
159
|
+
data-[state=open]:border-ring data-[state=open]:ring-ring/50
|
|
159
160
|
cursor-pointer
|
|
160
|
-
data-[state=open]:
|
|
161
|
-
data-[state=open]:ring-ring/50
|
|
161
|
+
data-[state=open]:ring-[3px]
|
|
162
162
|
"
|
|
163
163
|
>
|
|
164
164
|
<span
|
|
165
165
|
class="
|
|
166
|
-
|
|
167
|
-
whitespace-nowrap
|
|
166
|
+
gap-2 px-3 py-1 text-base
|
|
168
167
|
md:text-sm
|
|
168
|
+
line-clamp-1 flex flex-1 items-center whitespace-nowrap
|
|
169
169
|
"
|
|
170
170
|
>
|
|
171
171
|
<template v-if="multiple && selectedOptions.length > 0">
|
|
@@ -196,6 +196,7 @@ function handleClear (event: MouseEvent) {
|
|
|
196
196
|
align="inline-end"
|
|
197
197
|
>
|
|
198
198
|
<InputGroupButton
|
|
199
|
+
type="button"
|
|
199
200
|
size="icon-xs"
|
|
200
201
|
@click="handleClear"
|
|
201
202
|
>
|
|
@@ -210,7 +211,7 @@ function handleClear (event: MouseEvent) {
|
|
|
210
211
|
</InputGroupAddon>
|
|
211
212
|
</InputGroup>
|
|
212
213
|
</PopoverTrigger>
|
|
213
|
-
<PopoverContent class="w-(--reka-popover-trigger-width)
|
|
214
|
+
<PopoverContent class="p-0 w-(--reka-popover-trigger-width)">
|
|
214
215
|
<Command
|
|
215
216
|
:modelValue="commandModelValue"
|
|
216
217
|
:filterFunction="commandFilterFunction"
|
|
@@ -250,7 +251,7 @@ function handleClear (event: MouseEvent) {
|
|
|
250
251
|
<Icon
|
|
251
252
|
name="check"
|
|
252
253
|
:class="cn(
|
|
253
|
-
'ml-auto
|
|
254
|
+
'size-4 ml-auto shrink-0',
|
|
254
255
|
isSelected(option.value) ? 'opacity-100' : 'opacity-0',
|
|
255
256
|
)"
|
|
256
257
|
/>
|
|
@@ -61,23 +61,6 @@ const noControls = { controls: { disable: true }} satisfies Story['parameters']
|
|
|
61
61
|
|
|
62
62
|
export const Default: Story = {}
|
|
63
63
|
|
|
64
|
-
export const Controlled: Story = {
|
|
65
|
-
parameters: noControls,
|
|
66
|
-
render: () => ({
|
|
67
|
-
components: { Tabs },
|
|
68
|
-
setup () {
|
|
69
|
-
const value = ref<string>('account')
|
|
70
|
-
return { items, value }
|
|
71
|
-
},
|
|
72
|
-
template: `
|
|
73
|
-
<div class="max-w-md space-y-3">
|
|
74
|
-
<Tabs v-model="value" :items="items" />
|
|
75
|
-
<div class="text-sm text-muted-foreground">Active: {{ value }}</div>
|
|
76
|
-
</div>
|
|
77
|
-
`,
|
|
78
|
-
}),
|
|
79
|
-
}
|
|
80
|
-
|
|
81
64
|
export const IconOnly: Story = {
|
|
82
65
|
parameters: noControls,
|
|
83
66
|
render: () => ({
|
|
@@ -40,23 +40,6 @@ const noControls = { controls: { disable: true }} satisfies Story['parameters']
|
|
|
40
40
|
|
|
41
41
|
export const Default: Story = {}
|
|
42
42
|
|
|
43
|
-
export const Controlled: Story = {
|
|
44
|
-
parameters: noControls,
|
|
45
|
-
render: () => ({
|
|
46
|
-
components: { Textarea },
|
|
47
|
-
setup () {
|
|
48
|
-
const value = ref('')
|
|
49
|
-
return { value }
|
|
50
|
-
},
|
|
51
|
-
template: `
|
|
52
|
-
<div class="max-w-sm space-y-2">
|
|
53
|
-
<Textarea v-model="value" placeholder="Type your message here..." />
|
|
54
|
-
<div class="text-sm text-muted-foreground">Value: {{ value }}</div>
|
|
55
|
-
</div>
|
|
56
|
-
`,
|
|
57
|
-
}),
|
|
58
|
-
}
|
|
59
|
-
|
|
60
43
|
export const WithRows: Story = {
|
|
61
44
|
parameters: noControls,
|
|
62
45
|
render: () => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polymarbot/nuxt-layer-shadcn-ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Nuxt layer providing shadcn-vue based UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"vue-i18n": "^11",
|
|
43
43
|
"vue-router": "^4 || ^5"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "aba25283d8cf93bf3223b3b59da5ff8016615f98"
|
|
46
46
|
}
|