@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
- <span
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
- </span>
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
- <slot
387
- :name="column.field"
388
- :column="column"
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
- <span
394
- v-if="!formatCellValue(get(row, column.field), column) && column.type !== 'empty'"
395
- class="
396
- h-0.5 w-2.5 bg-muted-foreground/50 inline-block rounded-full
397
- align-middle
398
- "
399
- />
400
- <template v-else>
401
- {{ formatCellValue(get(row, column.field), column) }}
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
- </slot>
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.8",
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": "3377868984c2e309b5be71fa54f5f6f354b4ffa6"
45
+ "gitHead": "96386de30028e3a1fdeaa66d31149a6153909daa"
46
46
  }