@polymarbot/nuxt-layer-shadcn-ui 0.3.8 → 0.3.9
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.
|
@@ -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>
|
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.9",
|
|
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": "96386de30028e3a1fdeaa66d31149a6153909daa"
|
|
46
46
|
}
|