@hina-ui/vue 1.7.0 → 1.7.2

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.
Files changed (98) hide show
  1. package/dist/index.js +3086 -444
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/anchor/composables/useScrollSpy.d.ts.map +1 -1
  4. package/dist/types/components/combobox/Combobox.vue.d.ts +1 -0
  5. package/dist/types/components/combobox/Combobox.vue.d.ts.map +1 -1
  6. package/dist/types/components/data-table/DataTable.vue.d.ts +58 -0
  7. package/dist/types/components/data-table/DataTable.vue.d.ts.map +1 -0
  8. package/dist/types/components/data-table/DataTableAction.vue.d.ts +18 -0
  9. package/dist/types/components/data-table/DataTableAction.vue.d.ts.map +1 -0
  10. package/dist/types/components/data-table/DataTableDragPreview.vue.d.ts +24 -0
  11. package/dist/types/components/data-table/DataTableDragPreview.vue.d.ts.map +1 -0
  12. package/dist/types/components/data-table/DataTableEditor.vue.d.ts +24 -0
  13. package/dist/types/components/data-table/DataTableEditor.vue.d.ts.map +1 -0
  14. package/dist/types/components/data-table/DataTableHead.vue.d.ts +32 -0
  15. package/dist/types/components/data-table/DataTableHead.vue.d.ts.map +1 -0
  16. package/dist/types/components/data-table/DataTableRow.vue.d.ts +39 -0
  17. package/dist/types/components/data-table/DataTableRow.vue.d.ts.map +1 -0
  18. package/dist/types/components/data-table/DataTableSelection.vue.d.ts +15 -0
  19. package/dist/types/components/data-table/DataTableSelection.vue.d.ts.map +1 -0
  20. package/dist/types/components/data-table/DataTableText.vue.d.ts +8 -0
  21. package/dist/types/components/data-table/DataTableText.vue.d.ts.map +1 -0
  22. package/dist/types/components/data-table/Slot.d.ts +8 -0
  23. package/dist/types/components/data-table/Slot.d.ts.map +1 -0
  24. package/dist/types/components/data-table/column-sizing-styles.d.ts +4 -0
  25. package/dist/types/components/data-table/column-sizing-styles.d.ts.map +1 -0
  26. package/dist/types/components/data-table/column-sizing.d.ts +22 -0
  27. package/dist/types/components/data-table/column-sizing.d.ts.map +1 -0
  28. package/dist/types/components/data-table/composables/useDataTable.d.ts +80 -0
  29. package/dist/types/components/data-table/composables/useDataTable.d.ts.map +1 -0
  30. package/dist/types/components/data-table/composables/useTableColumns.d.ts +52 -0
  31. package/dist/types/components/data-table/composables/useTableColumns.d.ts.map +1 -0
  32. package/dist/types/components/data-table/composables/useTableDrag.d.ts +53 -0
  33. package/dist/types/components/data-table/composables/useTableDrag.d.ts.map +1 -0
  34. package/dist/types/components/data-table/composables/useTableEditing.d.ts +27 -0
  35. package/dist/types/components/data-table/composables/useTableEditing.d.ts.map +1 -0
  36. package/dist/types/components/data-table/composables/useTableExport.d.ts +3 -0
  37. package/dist/types/components/data-table/composables/useTableExport.d.ts.map +1 -0
  38. package/dist/types/components/data-table/composables/useTableResize.d.ts +42 -0
  39. package/dist/types/components/data-table/composables/useTableResize.d.ts.map +1 -0
  40. package/dist/types/components/data-table/composables/useTableResizeHandles.d.ts +8 -0
  41. package/dist/types/components/data-table/composables/useTableResizeHandles.d.ts.map +1 -0
  42. package/dist/types/components/data-table/composables/useTableText.d.ts +6 -0
  43. package/dist/types/components/data-table/composables/useTableText.d.ts.map +1 -0
  44. package/dist/types/components/data-table/composables/useTableVirtual.d.ts +55 -0
  45. package/dist/types/components/data-table/composables/useTableVirtual.d.ts.map +1 -0
  46. package/dist/types/components/data-table/features.d.ts +16 -0
  47. package/dist/types/components/data-table/features.d.ts.map +1 -0
  48. package/dist/types/components/data-table/types.d.ts +225 -0
  49. package/dist/types/components/data-table/types.d.ts.map +1 -0
  50. package/dist/types/components/data-table/utils.d.ts +10 -0
  51. package/dist/types/components/data-table/utils.d.ts.map +1 -0
  52. package/dist/types/components/form/composables/useForm.d.ts.map +1 -1
  53. package/dist/types/components/table/TableHead.vue.d.ts +2 -2
  54. package/dist/types/components/table/TableHead.vue.d.ts.map +1 -1
  55. package/dist/types/components/table/table.variants.d.ts +1 -1
  56. package/dist/types/components/table/table.variants.d.ts.map +1 -1
  57. package/dist/types/index.d.ts +2 -0
  58. package/dist/types/index.d.ts.map +1 -1
  59. package/dist/types/locale/en-US.d.ts.map +1 -1
  60. package/dist/types/locale/types.d.ts +12 -0
  61. package/dist/types/locale/types.d.ts.map +1 -1
  62. package/dist/types/locale/zh-CN.d.ts.map +1 -1
  63. package/package.json +14 -12
  64. package/src/components/anchor/composables/useScrollSpy.ts +8 -11
  65. package/src/components/combobox/Combobox.vue +11 -1
  66. package/src/components/data-table/DataTable.vue +341 -0
  67. package/src/components/data-table/DataTableAction.vue +15 -0
  68. package/src/components/data-table/DataTableDragPreview.vue +72 -0
  69. package/src/components/data-table/DataTableEditor.vue +59 -0
  70. package/src/components/data-table/DataTableHead.vue +181 -0
  71. package/src/components/data-table/DataTableRow.vue +255 -0
  72. package/src/components/data-table/DataTableSelection.vue +51 -0
  73. package/src/components/data-table/DataTableText.vue +20 -0
  74. package/src/components/data-table/Slot.ts +12 -0
  75. package/src/components/data-table/column-sizing-styles.ts +40 -0
  76. package/src/components/data-table/column-sizing.ts +101 -0
  77. package/src/components/data-table/composables/useDataTable.ts +425 -0
  78. package/src/components/data-table/composables/useTableColumns.ts +251 -0
  79. package/src/components/data-table/composables/useTableDrag.ts +345 -0
  80. package/src/components/data-table/composables/useTableEditing.ts +184 -0
  81. package/src/components/data-table/composables/useTableExport.ts +44 -0
  82. package/src/components/data-table/composables/useTableResize.ts +292 -0
  83. package/src/components/data-table/composables/useTableResizeHandles.ts +72 -0
  84. package/src/components/data-table/composables/useTableText.ts +13 -0
  85. package/src/components/data-table/composables/useTableVirtual.ts +109 -0
  86. package/src/components/data-table/features.ts +32 -0
  87. package/src/components/data-table/types.ts +218 -0
  88. package/src/components/data-table/utils.ts +75 -0
  89. package/src/components/form/composables/useForm.ts +44 -8
  90. package/src/components/table/TableHead.vue +1 -1
  91. package/src/components/table/table.variants.ts +1 -2
  92. package/src/index.ts +25 -0
  93. package/src/locale/en-US.ts +13 -0
  94. package/src/locale/types.ts +13 -0
  95. package/src/locale/zh-CN.ts +13 -0
  96. package/src/styles/data-table.css +330 -0
  97. package/src/styles/tokens.css +1 -0
  98. package/src/styles/typography.css +4 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hina-ui/vue",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "An elegant, restrained Vue 3 component library built on Reka UI and Tailwind CSS v4.",
5
5
  "keywords": [
6
6
  "vue",
@@ -41,9 +41,18 @@
41
41
  "publishConfig": {
42
42
  "access": "public"
43
43
  },
44
+ "scripts": {
45
+ "build": "vite build && vue-tsc -p tsconfig.build.json",
46
+ "typecheck": "vue-tsc --noEmit -p tsconfig.json",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "test:browser": "vitest run -c vitest.browser.config.ts"
50
+ },
44
51
  "dependencies": {
45
52
  "@internationalized/date": "^3.12.4",
46
53
  "@lucide/vue": "^1.37.0",
54
+ "@tanstack/vue-table": "^9.2.4",
55
+ "@tanstack/vue-virtual": "^3.13.36",
47
56
  "@vueuse/core": "^12.8.2",
48
57
  "clsx": "^2.1.1",
49
58
  "motion-v": "^2.2.1",
@@ -51,7 +60,8 @@
51
60
  "reka-ui": "^2.9.10",
52
61
  "shiki": "4.0.2",
53
62
  "tailwind-merge": "^3.6.0",
54
- "tailwind-variants": "^3.3.1"
63
+ "tailwind-variants": "^3.3.1",
64
+ "vue-component-type-helpers": "^3.3.11"
55
65
  },
56
66
  "peerDependencies": {
57
67
  "vue": "^3.5.0"
@@ -59,7 +69,6 @@
59
69
  "devDependencies": {
60
70
  "@floating-ui/dom": "1.7.6",
61
71
  "@tailwindcss/vite": "^4.3.0",
62
- "@tanstack/vue-virtual": "^3.13.36",
63
72
  "@vitejs/plugin-vue": "^6.0.7",
64
73
  "@vitest/browser": "^4.1.11",
65
74
  "@vitest/browser-playwright": "^4.1.11",
@@ -74,12 +83,5 @@
74
83
  "vue": "^3.5.33",
75
84
  "vue-tsc": "^3.3.1"
76
85
  },
77
- "license": "MIT",
78
- "scripts": {
79
- "build": "vite build && vue-tsc -p tsconfig.build.json",
80
- "typecheck": "vue-tsc --noEmit -p tsconfig.json",
81
- "test": "vitest run",
82
- "test:watch": "vitest",
83
- "test:browser": "vitest run -c vitest.browser.config.ts"
84
- }
85
- }
86
+ "license": "MIT"
87
+ }
@@ -11,7 +11,7 @@ export interface SpyEntry {
11
11
  export function useScrollSpy(entries: () => SpyEntry[]) {
12
12
  const visible = shallowRef<ReadonlySet<string>>(new Set())
13
13
  const targets = shallowRef<Array<HTMLElement | null>>([])
14
- const seen = new Set<string>()
14
+ const intersecting = new Set<string>()
15
15
 
16
16
  const covered = computed(() => entries().filter(entry => visible.value.has(entry.id)))
17
17
  const current = computed(() => covered.value[0]?.id)
@@ -23,18 +23,15 @@ export function useScrollSpy(entries: () => SpyEntry[]) {
23
23
  useIntersectionObserver(
24
24
  targets,
25
25
  observed => {
26
- const next = new Set(visible.value)
27
26
  for (const entry of observed) {
28
- if (entry.isIntersecting) {
29
- seen.add(entry.target.id)
30
- next.add(entry.target.id)
31
- } else if (seen.has(entry.target.id)) {
32
- next.delete(entry.target.id)
33
- }
27
+ if (entry.isIntersecting && entry.intersectionRatio > 0) intersecting.add(entry.target.id)
28
+ else intersecting.delete(entry.target.id)
34
29
  }
35
- if (next.size) visible.value = next
30
+ visible.value = intersecting.size
31
+ ? new Set(intersecting)
32
+ : new Set(current.value ? [current.value] : [])
36
33
  },
37
- { rootMargin: '0px 0px -15% 0px' },
34
+ { rootMargin: '0px 0px -15% 0px', threshold: [0, Number.EPSILON] },
38
35
  )
39
36
 
40
37
  function targetOf(id: string) {
@@ -42,7 +39,7 @@ export function useScrollSpy(entries: () => SpyEntry[]) {
42
39
  }
43
40
 
44
41
  function locate() {
45
- seen.clear()
42
+ intersecting.clear()
46
43
  targets.value = entries().map(({ id }) => targetOf(id))
47
44
  }
48
45
 
@@ -5,6 +5,8 @@
5
5
  import { useUiLocale } from '../../locale'
6
6
  import { X } from '@lucide/vue'
7
7
  import InputAction from '../input/InputAction.vue'
8
+ import IconSlot from '../button/IconSlot.vue'
9
+ import { buttonIconBox } from '../button/button.variants'
8
10
  import DisclosureIcon from '../disclosure-icon/DisclosureIcon.vue'
9
11
  import { useFieldControl } from '../form-field/context'
10
12
  import { injectInputGroup } from '../input-group/context'
@@ -28,6 +30,7 @@
28
30
  selectedOption?: T | null
29
31
  placeholder?: string
30
32
  ignoreFilter?: boolean
33
+ loading?: boolean
31
34
  clearable?: boolean
32
35
  variant?: InputVariants['variant']
33
36
  size?: InputVariants['size']
@@ -84,6 +87,7 @@
84
87
  data-hn-combobox
85
88
  :data-invalid="invalid ? '' : undefined"
86
89
  :data-disabled="disabled ? '' : undefined"
90
+ :aria-busy="props.loading || undefined"
87
91
  :class="
88
92
  cn(
89
93
  group ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
@@ -124,7 +128,13 @@
124
128
  :class="cn(inputAdornment(), inputIndicator())"
125
129
  @mousedown.prevent
126
130
  >
127
- <DisclosureIcon :open="open" />
131
+ <IconSlot
132
+ :box-class="buttonIconBox({ size })"
133
+ :swapped="!!props.loading"
134
+ spinner-size="sm"
135
+ >
136
+ <DisclosureIcon :open="open" />
137
+ </IconSlot>
128
138
  </ComboboxTrigger>
129
139
  </div>
130
140
  </ComboboxAnchor>
@@ -0,0 +1,341 @@
1
+ <script setup lang="ts" generic="T extends object">
2
+ import { computed, shallowRef, useId } from 'vue'
3
+ import { ChevronLeft, ChevronRight } from '@lucide/vue'
4
+ import { cn } from '../../lib/cn'
5
+ import { useUiLocale } from '../../locale'
6
+ import Button from '../button/Button.vue'
7
+ import LoadingOverlay from '../loading-overlay/LoadingOverlay.vue'
8
+ import Pagination from '../pagination/Pagination.vue'
9
+ import ScrollArea from '../scroll-area/ScrollArea.vue'
10
+ import TableCell from '../table/TableCell.vue'
11
+ import { tableWrapper } from '../table/table.variants'
12
+ import DataTableHead from './DataTableHead.vue'
13
+ import DataTableRow from './DataTableRow.vue'
14
+ import DataTableDragPreview from './DataTableDragPreview.vue'
15
+ import Slot from './Slot'
16
+ import { aggregate, cssSize } from './utils'
17
+ import { useDataTable } from './composables/useDataTable'
18
+ import { useTableColumns } from './composables/useTableColumns'
19
+ import { useTableDrag } from './composables/useTableDrag'
20
+ import { useTableEditing } from './composables/useTableEditing'
21
+ import { useTableExport } from './composables/useTableExport'
22
+ import { useTableVirtual } from './composables/useTableVirtual'
23
+ import type {
24
+ DataTableEdit,
25
+ DataTableFilter,
26
+ DataTableKey,
27
+ DataTableProps,
28
+ DataTableQuery,
29
+ DataTableReorder,
30
+ DataTableSlots,
31
+ DataTableSort,
32
+ } from './types'
33
+
34
+ defineOptions({ name: 'HnDataTable' })
35
+ const props = withDefaults(defineProps<DataTableProps<T>>(), {
36
+ variant: 'primary',
37
+ hover: true,
38
+ autoResetPage: true,
39
+ selectChildren: true,
40
+ hasNextPage: undefined,
41
+ })
42
+ const slots = defineSlots<DataTableSlots<T>>()
43
+ const page = defineModel<number>('page', { default: 1 })
44
+ const pageSize = defineModel<number>('pageSize', { default: 10 })
45
+ const sorting = defineModel<DataTableSort[]>('sorting', { default: () => [] })
46
+ const filter = defineModel<string>('filter', { default: '' })
47
+ const columnFilters = defineModel<DataTableFilter[]>('columnFilters', { default: () => [] })
48
+ const grouping = defineModel<string[]>('grouping', { default: () => [] })
49
+ const selected = defineModel<DataTableKey[]>('selected', { default: () => [] })
50
+ const expanded = defineModel<DataTableKey[]>('expanded', { default: () => [] })
51
+ const expandedGroups = defineModel<string[]>('expandedGroups', { default: () => [] })
52
+ const hiddenColumns = defineModel<string[]>('hiddenColumns', { default: () => [] })
53
+ const columnOrder = defineModel<string[]>('columnOrder', { default: () => [] })
54
+ const columnWidths = defineModel<Record<string, number>>('columnWidths', { default: () => ({}) })
55
+ const models = {
56
+ page,
57
+ pageSize,
58
+ sorting,
59
+ filter,
60
+ columnFilters,
61
+ grouping,
62
+ selected,
63
+ expanded,
64
+ expandedGroups,
65
+ hiddenColumns,
66
+ columnOrder,
67
+ columnWidths,
68
+ }
69
+ const emit = defineEmits<{
70
+ change: [query: DataTableQuery]
71
+ rowClick: [row: T, event: MouseEvent | KeyboardEvent]
72
+ rowReorder: [change: DataTableReorder<T>]
73
+ 'update:rows': [rows: T[]]
74
+ edit: [edit: DataTableEdit<T>]
75
+ editError: [error: unknown, edit: DataTableEdit<T>]
76
+ rowContextmenu: [row: T, event: MouseEvent]
77
+ }>()
78
+ function onRowContextmenu(row: T, event: MouseEvent) {
79
+ emit('rowContextmenu', row, event)
80
+ }
81
+ const t = useUiLocale()
82
+ const name = useId()
83
+ const area = shallowRef<InstanceType<typeof ScrollArea>>()
84
+ const element = shallowRef<HTMLTableElement>()
85
+ const viewport = computed(() => area.value?.viewport)
86
+ const ctl = useDataTable(
87
+ props,
88
+ models,
89
+ query => emit('change', query),
90
+ (row, event) => emit('rowClick', row, event),
91
+ )
92
+ const controls = computed(() =>
93
+ [
94
+ props.reorderable && 'drag',
95
+ props.selectable && 'select',
96
+ (props.expandable || props.getChildren) && 'expand',
97
+ ].filter((value): value is string => typeof value === 'string'),
98
+ )
99
+ const trailing = computed(() => (props.editMode === 'row' ? 1 : 0))
100
+ const layout = useTableColumns(
101
+ props,
102
+ models,
103
+ ctl,
104
+ element,
105
+ viewport,
106
+ computed(() => controls.value.length),
107
+ trailing,
108
+ )
109
+ const editing = useTableEditing(
110
+ props,
111
+ ctl,
112
+ edit => emit('edit', edit),
113
+ (error, edit) => emit('editError', error, edit),
114
+ element,
115
+ )
116
+ const drag = useTableDrag(props, models, ctl, element, viewport, change => {
117
+ if (!change.parent) emit('update:rows', change.rows)
118
+ emit('rowReorder', change)
119
+ })
120
+ const virtual = useTableVirtual(
121
+ props,
122
+ ctl,
123
+ element,
124
+ viewport,
125
+ () => !!slots.expansion,
126
+ editing.rowError,
127
+ )
128
+ useTableExport(ctl)
129
+ const colspan = computed(() =>
130
+ Math.max(1, ctl.visibleColumns.value.length + controls.value.length + trailing.value),
131
+ )
132
+ const summaryRows = computed(() => ctl.api.getRows('filtered'))
133
+ const hasSummary = computed(
134
+ () =>
135
+ !!slots.summary ||
136
+ ctl.visibleColumns.value.some(
137
+ column => column.footer !== undefined || slots[`footer-${column.key}`],
138
+ ),
139
+ )
140
+ defineExpose({ viewport, element, state: ctl.state, api: ctl.api })
141
+ </script>
142
+ <template>
143
+ <div
144
+ :aria-busy="props.loading || undefined"
145
+ :style="{ height: cssSize(props.height) }"
146
+ :class="
147
+ cn(
148
+ 'flex min-h-0 min-w-0 w-full max-w-full flex-col gap-3',
149
+ props.fill && 'h-full',
150
+ props.class,
151
+ )
152
+ "
153
+ >
154
+ <slot name="toolbar" v-bind="ctl.state.value" />
155
+ <div :class="['relative min-h-0', (props.fill || props.height) && 'flex-1']">
156
+ <ScrollArea
157
+ ref="area"
158
+ :direction="
159
+ props.stickyHeader || props.maxHeight || props.height || props.fill || props.virtualize
160
+ ? 'both'
161
+ : 'horizontal'
162
+ "
163
+ :style="{
164
+ maxHeight: cssSize(
165
+ props.maxHeight ?? (props.virtualize && !props.fill && !props.height ? 400 : undefined),
166
+ ),
167
+ }"
168
+ :class="
169
+ cn(
170
+ tableWrapper({ variant: props.variant, hover: false }),
171
+ layout.constrained.value && '@container',
172
+ (props.fill || props.height) && 'h-full',
173
+ props.stickyHeader && '[&_thead_th]:sticky [&_thead_th]:z-[3]',
174
+ props.stickyFooter &&
175
+ '[&_tfoot_td]:sticky [&_tfoot_td]:bottom-0 [&_tfoot_td]:z-[2] [&_tfoot_td]:bg-(--hn-table-head-bg)',
176
+ props.tableClass,
177
+ )
178
+ "
179
+ >
180
+ <table
181
+ ref="element"
182
+ class="hn-table"
183
+ :style="layout.tableStyle.value"
184
+ :aria-label="props.label"
185
+ :aria-rowcount="
186
+ props.virtualize
187
+ ? virtual.entries.value.length + layout.headerRows.value.length + (hasSummary ? 1 : 0)
188
+ : undefined
189
+ "
190
+ :inert="ctl.blocked.value || undefined"
191
+ >
192
+ <caption v-if="props.caption">
193
+ <span>{{ props.caption }}</span>
194
+ </caption>
195
+ <colgroup>
196
+ <col v-for="control in controls" :key="control" style="width: 48px" />
197
+ <col
198
+ v-for="column in ctl.visibleColumns.value"
199
+ :key="column.key"
200
+ :style="{ width: layout.cellStyle(column).width }"
201
+ />
202
+ <col v-if="trailing" style="width: 72px" />
203
+ </colgroup>
204
+ <DataTableHead
205
+ :config="props"
206
+ :ctl="ctl"
207
+ :models="models"
208
+ :layout="layout"
209
+ :drag="drag"
210
+ :controls="controls"
211
+ :trailing="trailing"
212
+ :slots="slots"
213
+ />
214
+ <tbody>
215
+ <tr
216
+ v-if="
217
+ !ctl.rows.value.length || (!ctl.visibleColumns.value.length && !controls.length)
218
+ "
219
+ >
220
+ <TableCell :colspan="colspan">
221
+ <div class="text-muted flex min-h-32 items-center justify-center px-3 py-6 text-sm">
222
+ <template v-if="props.loading">{{ t.table.loading }}</template>
223
+ <template v-else-if="!ctl.visibleColumns.value.length && !controls.length">
224
+ {{ t.table.noColumns }}
225
+ </template>
226
+ <slot v-else name="empty">{{ props.emptyText ?? t.table.empty }}</slot>
227
+ </div>
228
+ </TableCell>
229
+ </tr>
230
+ <template v-else>
231
+ <tr v-if="virtual.before.value" aria-hidden="true">
232
+ <td
233
+ :colspan="colspan"
234
+ :style="{ height: `${virtual.before.value}px`, padding: 0, border: 0 }"
235
+ />
236
+ </tr>
237
+ <DataTableRow
238
+ v-for="item in virtual.visible.value"
239
+ :key="item.id"
240
+ :item="item"
241
+ :config="props"
242
+ :ctl="ctl"
243
+ :layout="layout"
244
+ :drag="drag"
245
+ :editing="editing"
246
+ :slots="slots"
247
+ :controls="controls"
248
+ :colspan="colspan"
249
+ :name="name"
250
+ :measure="virtual.measure"
251
+ @row-contextmenu="onRowContextmenu"
252
+ />
253
+ <tr v-if="virtual.after.value" aria-hidden="true">
254
+ <td
255
+ :colspan="colspan"
256
+ :style="{ height: `${virtual.after.value}px`, padding: 0, border: 0 }"
257
+ />
258
+ </tr>
259
+ </template>
260
+ </tbody>
261
+ <tfoot v-if="hasSummary">
262
+ <Slot :render="slots.summary" :context="ctl.state.value">
263
+ <tr>
264
+ <TableCell
265
+ v-for="(control, index) in controls"
266
+ :key="control"
267
+ :style="layout.controlStyle('start', index)"
268
+ />
269
+ <TableCell
270
+ v-for="column in ctl.visibleColumns.value"
271
+ :key="column.key"
272
+ :style="layout.cellStyle(column)"
273
+ :align="column.align"
274
+ class="font-medium"
275
+ >
276
+ <Slot
277
+ :render="slots[`footer-${column.key}`]"
278
+ :context="{ column, rows: summaryRows }"
279
+ >
280
+ {{
281
+ typeof column.footer === 'function'
282
+ ? column.footer(summaryRows)
283
+ : column.footer === true
284
+ ? aggregate(summaryRows, column, ctl.valueOf)
285
+ : column.footer
286
+ }}
287
+ </Slot>
288
+ </TableCell>
289
+ <TableCell v-if="trailing" :style="layout.controlStyle('end', 0)" />
290
+ </tr>
291
+ </Slot>
292
+ </tfoot>
293
+ </table>
294
+ </ScrollArea>
295
+ <LoadingOverlay :visible="props.loading" :text="t.table.loading" class="rounded-lg">
296
+ <template v-if="slots.loading" #default><slot name="loading" /></template>
297
+ </LoadingOverlay>
298
+ </div>
299
+ <slot name="footer" v-bind="ctl.state.value">
300
+ <Pagination
301
+ v-if="props.pagination && ctl.knownTotal.value"
302
+ :model-value="ctl.page.value"
303
+ :page-size="ctl.pageSize.value"
304
+ :total="ctl.total.value"
305
+ :item-count="ctl.api.getRows().length"
306
+ :pending="props.loading"
307
+ :disabled="props.disabled"
308
+ @update:model-value="models.page.value = $event"
309
+ @update:page-size="models.pageSize.value = $event"
310
+ />
311
+ <nav
312
+ v-else-if="props.pagination"
313
+ class="flex items-center gap-2"
314
+ :aria-label="t.pagination.navLabel"
315
+ >
316
+ <Button
317
+ variant="ghost"
318
+ size="sm"
319
+ icon-only
320
+ :disabled="ctl.blocked.value || ctl.page.value <= 1"
321
+ :aria-label="t.pagination.prev"
322
+ @click="models.page.value--"
323
+ >
324
+ <ChevronLeft class="rtl:rotate-180" />
325
+ </Button>
326
+ <span class="text-muted text-sm tabular-nums">{{ ctl.page.value }}</span>
327
+ <Button
328
+ variant="ghost"
329
+ size="sm"
330
+ icon-only
331
+ :disabled="ctl.blocked.value || !ctl.canNext.value"
332
+ :aria-label="t.pagination.next"
333
+ @click="models.page.value++"
334
+ >
335
+ <ChevronRight class="rtl:rotate-180" />
336
+ </Button>
337
+ </nav>
338
+ </slot>
339
+ <DataTableDragPreview :drag="drag" :layout="layout" />
340
+ </div>
341
+ </template>
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ import Spinner from '../spinner/Spinner.vue'
3
+ defineProps<{ disabled?: boolean; loading?: boolean }>()
4
+ </script>
5
+ <template>
6
+ <button
7
+ type="button"
8
+ class="hn-table-action hn-focus-ring"
9
+ :disabled="disabled || loading"
10
+ :aria-busy="loading || undefined"
11
+ >
12
+ <Spinner v-if="loading" size="sm" />
13
+ <slot v-else />
14
+ </button>
15
+ </template>
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts" generic="T extends object">
2
+ import { DialogPortal } from 'reka-ui'
3
+ import type { DataTableDrag } from './composables/useTableDrag'
4
+ import type { DataTableLayout } from './composables/useTableColumns'
5
+ defineProps<{ drag: DataTableDrag<T>; layout: DataTableLayout<T> }>()
6
+ </script>
7
+ <template>
8
+ <DialogPortal>
9
+ <div
10
+ v-if="layout.guide.value"
11
+ class="hn-table-drop-line"
12
+ aria-hidden="true"
13
+ data-hn-resize-guide
14
+ :style="{
15
+ left: `${layout.guide.value.x}px`,
16
+ top: `${layout.guide.value.y}px`,
17
+ height: `${layout.guide.value.height}px`,
18
+ width: '2px',
19
+ }"
20
+ />
21
+ <div
22
+ v-if="layout.guide.value"
23
+ class="hn-table-resize-label"
24
+ data-hn-resize-label
25
+ aria-hidden="true"
26
+ :style="{
27
+ left: `${layout.guide.value.labelX}px`,
28
+ top: `${Math.max(4, layout.guide.value.y - 12 - layout.guide.value.columns.length * 18)}px`,
29
+ }"
30
+ >
31
+ <div v-for="column in layout.guide.value.columns" :key="column.key">
32
+ <span>{{ column.label }}</span>
33
+ <span>{{ column.width }}px</span>
34
+ </div>
35
+ </div>
36
+ <template v-if="drag.dragging.value">
37
+ <div
38
+ v-if="drag.dragging.value.marker"
39
+ class="hn-table-drop-line"
40
+ aria-hidden="true"
41
+ data-hn-drop-line
42
+ :style="{
43
+ left: `${drag.dragging.value.marker.x}px`,
44
+ top: `${drag.dragging.value.marker.y}px`,
45
+ width: `${drag.dragging.value.marker.width}px`,
46
+ height: `${drag.dragging.value.marker.height}px`,
47
+ }"
48
+ />
49
+ <div
50
+ class="hn-table-drag-preview"
51
+ aria-hidden="true"
52
+ data-hn-drag-preview
53
+ :style="{
54
+ ...drag.dragging.value.appearance,
55
+ left: `${drag.dragging.value.x - drag.dragging.value.offsetX}px`,
56
+ top: `${drag.dragging.value.y - drag.dragging.value.offsetY}px`,
57
+ width:
58
+ drag.dragging.value.kind === 'column' ? `${drag.dragging.value.width}px` : undefined,
59
+ }"
60
+ >
61
+ <div class="hn-table-drag-label">{{ drag.dragging.value.label }}</div>
62
+ <div
63
+ v-for="(sample, index) in drag.dragging.value.samples"
64
+ :key="index"
65
+ class="hn-table-drag-cell"
66
+ >
67
+ {{ sample }}
68
+ </div>
69
+ </div>
70
+ </template>
71
+ </DialogPortal>
72
+ </template>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts" generic="T extends object">
2
+ import { useId } from 'vue'
3
+ import { Check, X } from '@lucide/vue'
4
+ import { useUiLocale } from '../../locale'
5
+ import Input from '../input/Input.vue'
6
+ import InputGroup from '../input-group/InputGroup.vue'
7
+ import DataTableAction from './DataTableAction.vue'
8
+ import Slot from './Slot'
9
+ import type { DataTableEditorContext, DataTableSlots } from './types'
10
+ defineProps<{ context: DataTableEditorContext<T>; slots: DataTableSlots<T>; cell: boolean }>()
11
+ const t = useUiLocale()
12
+ const errorId = useId()
13
+ </script>
14
+ <template>
15
+ <div class="hn-table-editor" @click.stop @keydown.stop>
16
+ <div class="hn-table-editor-control">
17
+ <InputGroup
18
+ variant="bare"
19
+ size="sm"
20
+ :disabled="context.pending"
21
+ :invalid="!!context.error"
22
+ class="hn-table-editor-field"
23
+ role="group"
24
+ :aria-label="`${t.table.edit}: ${context.column.label}`"
25
+ :aria-describedby="context.error ? errorId : undefined"
26
+ >
27
+ <Slot :render="slots[`editor-${context.column.key}`] ?? slots.editor" :context="context">
28
+ <Input
29
+ :model-value="String(context.value ?? '')"
30
+ :aria-label="`${t.table.edit}: ${context.column.label}`"
31
+ :aria-describedby="context.error ? errorId : undefined"
32
+ @update:model-value="context.updateValue"
33
+ @keydown.enter.prevent="context.commit"
34
+ @keydown.esc.prevent="context.cancel"
35
+ />
36
+ </Slot>
37
+ </InputGroup>
38
+ <template v-if="cell">
39
+ <DataTableAction
40
+ :loading="context.pending"
41
+ :aria-label="t.table.save"
42
+ @click="context.commit"
43
+ >
44
+ <Check />
45
+ </DataTableAction>
46
+ <DataTableAction
47
+ :disabled="context.pending"
48
+ :aria-label="t.table.cancel"
49
+ @click="context.cancel"
50
+ >
51
+ <X />
52
+ </DataTableAction>
53
+ </template>
54
+ </div>
55
+ <p v-if="context.error" :id="errorId" role="alert" class="hn-table-field-error">
56
+ {{ context.error }}
57
+ </p>
58
+ </div>
59
+ </template>