@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.
@@ -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>
@@ -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: () => ({
@@ -76,6 +76,7 @@ function clearInput () {
76
76
  align="inline-end"
77
77
  >
78
78
  <InputGroupButton
79
+ type="button"
79
80
  size="icon-xs"
80
81
  @click="clearInput"
81
82
  >
@@ -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]:border-ring data-[state=open]:ring-[3px]
161
- data-[state=open]:ring-ring/50
161
+ data-[state=open]:ring-[3px]
162
162
  "
163
163
  >
164
164
  <span
165
165
  class="
166
- line-clamp-1 flex flex-1 items-center gap-2 px-3 py-1 text-base
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) p-0">
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 size-4 shrink-0',
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.8",
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": "3377868984c2e309b5be71fa54f5f6f354b4ffa6"
45
+ "gitHead": "aba25283d8cf93bf3223b3b59da5ff8016615f98"
46
46
  }