@serhiitupilow/nuxt-table 0.1.3

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/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # @serhiitupilow/nuxt-table
2
+
3
+ Nuxt module that provides a single `NuxtTable` component with:
4
+
5
+ - sorting
6
+ - filtering
7
+ - column visibility manager
8
+ - column resize
9
+ - optional drag-and-drop column reordering
10
+ - class-based styling (no Tailwind classes in component templates)
11
+
12
+ ## Install
13
+
14
+ Use any package manager:
15
+
16
+ ```bash
17
+ npm i @serhiitupilow/nuxt-table
18
+ # or
19
+ yarn add @serhiitupilow/nuxt-table
20
+ # or
21
+ bun add @serhiitupilow/nuxt-table
22
+ # or
23
+ pnpm add @serhiitupilow/nuxt-table
24
+ ```
25
+
26
+ ## Nuxt config
27
+
28
+ ```ts
29
+ export default defineNuxtConfig({
30
+ modules: ["@serhiitupilow/nuxt-table"],
31
+ });
32
+ ```
33
+
34
+ Optional module config:
35
+
36
+ ```ts
37
+ export default defineNuxtConfig({
38
+ modules: ["@serhiitupilow/nuxt-table"],
39
+ nuxtTable: {
40
+ injectDefaultStyles: true,
41
+ },
42
+ });
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ```vue
48
+ <script setup lang="ts">
49
+ import type { NuxtTableColumn } from "@serhiitupilow/nuxt-table/dist/runtime/types/table";
50
+
51
+ const columns: NuxtTableColumn[] = [
52
+ { key: "id", label: "ID", sortable: true, filterable: true },
53
+ { key: "name", label: "Name", sortable: true, filterable: true },
54
+ { key: "status", label: "Status", sortable: true, filterable: true },
55
+ ];
56
+
57
+ const rows = [
58
+ { id: 1, name: "Alice", status: "active" },
59
+ { id: 2, name: "Bob", status: "paused" },
60
+ ];
61
+
62
+ function onColumnOrderChange(payload: {
63
+ order: string[];
64
+ movedKey: string;
65
+ fromIndex: number;
66
+ toIndex: number;
67
+ }) {
68
+ console.log("new order", payload.order);
69
+ }
70
+ </script>
71
+
72
+ <template>
73
+ <NuxtTable
74
+ :columns="columns"
75
+ :rows="rows"
76
+ storage-key="users-table"
77
+ :enable-column-dnd="true"
78
+ @column-order-change="onColumnOrderChange"
79
+ />
80
+ </template>
81
+ ```
82
+
83
+ ## Styling
84
+
85
+ `NuxtTable` uses semantic class names (like `nuxt-table__header-cell`) and receives a `classNames` prop for overrides.
86
+
87
+ You can style globally in your project CSS:
88
+
89
+ ```css
90
+ .nuxt-table__header-cell {
91
+ background: #f8fafc;
92
+ }
93
+ ```
94
+
95
+ Or override class names from props:
96
+
97
+ ```vue
98
+ <NuxtTable
99
+ :columns="columns"
100
+ :rows="rows"
101
+ :class-names="{ table: 'my-table', headerCell: 'my-header-cell' }"
102
+ />
103
+ ```
@@ -0,0 +1,9 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ injectDefaultStyles: boolean;
5
+ }
6
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
7
+
8
+ export { _default as default };
9
+ export type { ModuleOptions };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@serhiitupilow/nuxt-table",
3
+ "configKey": "nuxtTable",
4
+ "version": "0.1.3",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.2",
7
+ "unbuild": "3.6.1"
8
+ }
9
+ }
@@ -0,0 +1,31 @@
1
+ import { defineNuxtModule, createResolver, addComponentsDir, addImportsDir } from '@nuxt/kit';
2
+
3
+ const module$1 = defineNuxtModule({
4
+ meta: {
5
+ name: "@serhiitupilow/nuxt-table",
6
+ configKey: "nuxtTable"
7
+ },
8
+ defaults: {
9
+ injectDefaultStyles: true
10
+ },
11
+ setup(options, nuxt) {
12
+ const resolver = createResolver(import.meta.url);
13
+ addComponentsDir({
14
+ path: resolver.resolve("./runtime/components"),
15
+ prefix: "",
16
+ pathPrefix: false,
17
+ global: true,
18
+ extensions: ["vue"],
19
+ transpile: true
20
+ });
21
+ addImportsDir(resolver.resolve("./runtime/composables"));
22
+ if (options.injectDefaultStyles) {
23
+ const stylesPath = resolver.resolve("./runtime/assets/styles.css");
24
+ if (!nuxt.options.css.includes(stylesPath)) {
25
+ nuxt.options.css.push(stylesPath);
26
+ }
27
+ }
28
+ }
29
+ });
30
+
31
+ export { module$1 as default };
@@ -0,0 +1 @@
1
+ .nuxt-table{display:grid;gap:1rem}.nuxt-table__toolbar{align-items:center;display:flex;gap:.75rem;justify-content:space-between}.nuxt-table__toolbar-title{font-size:1.125rem;font-weight:600;margin:0}.nuxt-table__toolbar-actions{align-items:center;display:flex;gap:.5rem;position:relative}.nuxt-table__toolbar-button{background:#fff;border:1px solid #d1d5db;border-radius:.375rem;cursor:pointer;font-size:.875rem;line-height:1;padding:.5rem .75rem}.nuxt-table__column-manager{background:#fff;border:1px solid #d1d5db;border-radius:.5rem;box-shadow:0 10px 20px rgba(15,23,42,.14);min-width:14rem;padding:.75rem;position:absolute;right:0;top:2.75rem;z-index:30}.nuxt-table__column-manager-title{font-size:.875rem;font-weight:600;margin:0 0 .5rem}.nuxt-table__column-manager-item{align-items:center;cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;margin-bottom:.375rem}.nuxt-table__column-manager-item:last-child{margin-bottom:0}.nuxt-table__wrapper{border:1px solid #d1d5db;border-radius:.5rem;overflow-x:auto}.nuxt-table__table{border-collapse:collapse;min-width:100%;table-layout:fixed;width:100%}.nuxt-table__head{background:#f8fafc}.nuxt-table__header-cell{border-bottom:1px solid #e5e7eb;border-right:1px solid #e5e7eb;padding:.75rem;position:relative;text-align:left;transition:background-color .2s ease,box-shadow .2s ease,transform .2s ease,opacity .2s ease;vertical-align:top}.nuxt-table__header-cell:last-child{border-right:0}.nuxt-table__header-cell--drag-source{box-shadow:inset 0 0 0 1px #9ca3af;opacity:.82}.nuxt-table__header-cell--drag-over{background:#f1f5f9;box-shadow:inset 0 0 0 2px #cbd5e1}.nuxt-table__header-top{align-items:center;display:flex;gap:.5rem;justify-content:space-between;margin-bottom:.5rem}.nuxt-table__header-label{font-weight:600}.nuxt-table__sort-button{background:#fff;border:1px solid #d1d5db;border-radius:.375rem;cursor:pointer;font-size:.75rem;line-height:1;padding:.25rem .5rem}.nuxt-table__filter-input{border:1px solid #d1d5db;border-radius:.375rem;font-size:.875rem;padding:.375rem .5rem;width:100%}.nuxt-table__resize-handle{cursor:col-resize;height:100%;position:absolute;right:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:.5rem}.nuxt-table__body-row{border-bottom:1px solid #e5e7eb}.nuxt-table__body-row:last-child{border-bottom:0}.nuxt-table__body-cell{border-right:1px solid #e5e7eb;padding:.75rem;vertical-align:top}.nuxt-table__body-cell:last-child{border-right:0}.nuxt-table__empty-cell{color:#6b7280;padding:1rem;text-align:center}
@@ -0,0 +1,26 @@
1
+ import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow } from '../types/table.js';
2
+ type __VLS_Props = {
3
+ columns: NuxtTableColumn[];
4
+ rows: TableRow[];
5
+ storageKey?: string;
6
+ rowKey?: string | ((row: TableRow, index: number) => string | number);
7
+ title?: string;
8
+ showToolbar?: boolean;
9
+ enableColumnDnd?: boolean;
10
+ enableColumnResize?: boolean;
11
+ classNames?: Partial<NuxtTableClassNames>;
12
+ };
13
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
+ columnOrderChange: (payload: NuxtTableColumnOrderChange) => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onColumnOrderChange?: ((payload: NuxtTableColumnOrderChange) => any) | undefined;
17
+ }>, {
18
+ storageKey: string;
19
+ rowKey: string | ((row: TableRow, index: number) => string | number);
20
+ title: string;
21
+ showToolbar: boolean;
22
+ enableColumnDnd: boolean;
23
+ enableColumnResize: boolean;
24
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
+ declare const _default: typeof __VLS_export;
26
+ export default _default;
@@ -0,0 +1,190 @@
1
+ <script setup>
2
+ import { computed, toRef } from "vue";
3
+ import { useNuxtTable } from "../composables/useNuxtTable";
4
+ const props = defineProps({
5
+ columns: { type: Array, required: true },
6
+ rows: { type: Array, required: true },
7
+ storageKey: { type: String, required: false, default: "nuxt-table" },
8
+ rowKey: { type: [String, Function], required: false, default: "id" },
9
+ title: { type: String, required: false, default: "Table" },
10
+ showToolbar: { type: Boolean, required: false, default: true },
11
+ enableColumnDnd: { type: Boolean, required: false, default: false },
12
+ enableColumnResize: { type: Boolean, required: false, default: true },
13
+ classNames: { type: Object, required: false }
14
+ });
15
+ const emit = defineEmits(["columnOrderChange"]);
16
+ const defaultClassNames = {
17
+ root: "nuxt-table",
18
+ toolbar: "nuxt-table__toolbar",
19
+ toolbarTitle: "nuxt-table__toolbar-title",
20
+ toolbarActions: "nuxt-table__toolbar-actions",
21
+ toolbarButton: "nuxt-table__toolbar-button",
22
+ columnManager: "nuxt-table__column-manager",
23
+ columnManagerTitle: "nuxt-table__column-manager-title",
24
+ columnManagerItem: "nuxt-table__column-manager-item",
25
+ tableWrapper: "nuxt-table__wrapper",
26
+ table: "nuxt-table__table",
27
+ tableHead: "nuxt-table__head",
28
+ tableBody: "nuxt-table__body",
29
+ bodyRow: "nuxt-table__body-row",
30
+ emptyCell: "nuxt-table__empty-cell",
31
+ headerCell: "nuxt-table__header-cell",
32
+ headerCellDragSource: "nuxt-table__header-cell--drag-source",
33
+ headerCellDragOver: "nuxt-table__header-cell--drag-over",
34
+ headerTop: "nuxt-table__header-top",
35
+ headerLabel: "nuxt-table__header-label",
36
+ sortButton: "nuxt-table__sort-button",
37
+ filterInput: "nuxt-table__filter-input",
38
+ resizeHandle: "nuxt-table__resize-handle",
39
+ bodyCell: "nuxt-table__body-cell"
40
+ };
41
+ const mergedClassNames = computed(() => {
42
+ return {
43
+ ...defaultClassNames,
44
+ ...props.classNames ?? {}
45
+ };
46
+ });
47
+ const {
48
+ orderedColumns,
49
+ visibleColumns,
50
+ sortedRows,
51
+ filters,
52
+ isColumnManagerOpen,
53
+ enabledColumnKeys,
54
+ dragSourceColumnKey,
55
+ dragOverColumnKey,
56
+ getSortDirection,
57
+ toggleSort,
58
+ setFilter,
59
+ clearAllFilters,
60
+ toggleColumn,
61
+ resetColumns,
62
+ onHeaderDragStart,
63
+ onHeaderDragOver,
64
+ onHeaderDragLeave,
65
+ onHeaderDrop,
66
+ onHeaderDragEnd,
67
+ getColumnStyle,
68
+ startColumnResize,
69
+ setHeaderElement,
70
+ resolveDisplayValue,
71
+ resolveRowKey
72
+ } = useNuxtTable({
73
+ columns: toRef(props, "columns"),
74
+ rows: toRef(props, "rows"),
75
+ storageKey: toRef(props, "storageKey"),
76
+ rowKey: toRef(props, "rowKey"),
77
+ enableColumnDnd: toRef(props, "enableColumnDnd"),
78
+ onColumnOrderChange: (payload) => {
79
+ emit("columnOrderChange", payload);
80
+ }
81
+ });
82
+ </script>
83
+
84
+ <template>
85
+ <div :class="mergedClassNames.root">
86
+ <div v-if="props.showToolbar" :class="mergedClassNames.toolbar">
87
+ <h2 :class="mergedClassNames.toolbarTitle">{{ props.title }}</h2>
88
+
89
+ <div :class="mergedClassNames.toolbarActions">
90
+ <button
91
+ type="button"
92
+ :class="mergedClassNames.toolbarButton"
93
+ @click="isColumnManagerOpen = !isColumnManagerOpen"
94
+ >
95
+ Columns
96
+ </button>
97
+ <button
98
+ type="button"
99
+ :class="mergedClassNames.toolbarButton"
100
+ @click="clearAllFilters"
101
+ >
102
+ Clear Filters
103
+ </button>
104
+ <button
105
+ type="button"
106
+ :class="mergedClassNames.toolbarButton"
107
+ @click="resetColumns"
108
+ >
109
+ Reset Columns
110
+ </button>
111
+
112
+ <div
113
+ v-if="isColumnManagerOpen"
114
+ :class="mergedClassNames.columnManager"
115
+ >
116
+ <p :class="mergedClassNames.columnManagerTitle">Enable Columns</p>
117
+ <label
118
+ v-for="column in orderedColumns"
119
+ :key="`manager-${column.key}`"
120
+ :class="mergedClassNames.columnManagerItem"
121
+ >
122
+ <input
123
+ type="checkbox"
124
+ :checked="enabledColumnKeys.includes(column.key)"
125
+ @change="toggleColumn(column.key)"
126
+ >
127
+ <span>{{ column.label }}</span>
128
+ </label>
129
+ </div>
130
+ </div>
131
+ </div>
132
+
133
+ <div :class="mergedClassNames.tableWrapper">
134
+ <table :class="mergedClassNames.table">
135
+ <thead :class="mergedClassNames.tableHead">
136
+ <tr>
137
+ <NuxtTableHeaderCell
138
+ v-for="column in visibleColumns"
139
+ :key="column.key"
140
+ :column="column"
141
+ :filter-value="filters[column.key]"
142
+ :column-style="getColumnStyle(column.key)"
143
+ :sort-direction="getSortDirection(column.key)"
144
+ :is-drag-source="dragSourceColumnKey === column.key"
145
+ :is-drag-over="dragOverColumnKey === column.key"
146
+ :is-dnd-enabled="props.enableColumnDnd"
147
+ :is-resize-enabled="props.enableColumnResize"
148
+ :class-names="mergedClassNames"
149
+ :set-header-element="setHeaderElement"
150
+ @drag-start="onHeaderDragStart"
151
+ @drag-over="onHeaderDragOver"
152
+ @drag-leave="onHeaderDragLeave"
153
+ @drop="onHeaderDrop"
154
+ @drag-end="onHeaderDragEnd"
155
+ @toggle-sort="toggleSort"
156
+ @set-filter="setFilter"
157
+ @resize-start="startColumnResize"
158
+ />
159
+ </tr>
160
+ </thead>
161
+ <tbody :class="mergedClassNames.tableBody">
162
+ <tr
163
+ v-for="(row, rowIndex) in sortedRows"
164
+ :key="resolveRowKey(row, rowIndex)"
165
+ :class="mergedClassNames.bodyRow"
166
+ >
167
+ <NuxtTableBodyCell
168
+ v-for="column in visibleColumns"
169
+ :key="`${resolveRowKey(row, rowIndex)}-${column.key}`"
170
+ :row="row"
171
+ :row-key="resolveRowKey(row, rowIndex)"
172
+ :column="column"
173
+ :value="resolveDisplayValue(row, column)"
174
+ :column-style="getColumnStyle(column.key)"
175
+ :class-names="mergedClassNames"
176
+ />
177
+ </tr>
178
+ <tr v-if="sortedRows.length === 0">
179
+ <td
180
+ :colspan="Math.max(visibleColumns.length, 1)"
181
+ :class="mergedClassNames.emptyCell"
182
+ >
183
+ No rows match the current filters.
184
+ </td>
185
+ </tr>
186
+ </tbody>
187
+ </table>
188
+ </div>
189
+ </div>
190
+ </template>
@@ -0,0 +1,26 @@
1
+ import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow } from '../types/table.js';
2
+ type __VLS_Props = {
3
+ columns: NuxtTableColumn[];
4
+ rows: TableRow[];
5
+ storageKey?: string;
6
+ rowKey?: string | ((row: TableRow, index: number) => string | number);
7
+ title?: string;
8
+ showToolbar?: boolean;
9
+ enableColumnDnd?: boolean;
10
+ enableColumnResize?: boolean;
11
+ classNames?: Partial<NuxtTableClassNames>;
12
+ };
13
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
+ columnOrderChange: (payload: NuxtTableColumnOrderChange) => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onColumnOrderChange?: ((payload: NuxtTableColumnOrderChange) => any) | undefined;
17
+ }>, {
18
+ storageKey: string;
19
+ rowKey: string | ((row: TableRow, index: number) => string | number);
20
+ title: string;
21
+ showToolbar: boolean;
22
+ enableColumnDnd: boolean;
23
+ enableColumnResize: boolean;
24
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
+ declare const _default: typeof __VLS_export;
26
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import type { NuxtTableClassNames, NuxtTableColumn, TableRow } from '../types/table.js';
2
+ type __VLS_Props = {
3
+ row: TableRow;
4
+ column: NuxtTableColumn;
5
+ rowKey: string | number;
6
+ value: unknown;
7
+ columnStyle: Record<string, string | undefined>;
8
+ classNames: NuxtTableClassNames;
9
+ };
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ declare const _default: typeof __VLS_export;
12
+ export default _default;
@@ -0,0 +1,27 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ row: { type: Object, required: true },
4
+ column: { type: Object, required: true },
5
+ rowKey: { type: [String, Number], required: true },
6
+ value: { type: null, required: true },
7
+ columnStyle: { type: Object, required: true },
8
+ classNames: { type: Object, required: true }
9
+ });
10
+ </script>
11
+
12
+ <template>
13
+ <td
14
+ :key="`${props.rowKey}-${props.column.key}`"
15
+ :style="props.columnStyle"
16
+ :class="[props.classNames.bodyCell, props.column.cellClassName]"
17
+ >
18
+ <component
19
+ :is="props.column.cellComponent"
20
+ v-if="props.column.cellComponent"
21
+ :row="props.row"
22
+ :column="props.column"
23
+ :value="props.value"
24
+ />
25
+ <span v-else>{{ props.value }}</span>
26
+ </td>
27
+ </template>
@@ -0,0 +1,12 @@
1
+ import type { NuxtTableClassNames, NuxtTableColumn, TableRow } from '../types/table.js';
2
+ type __VLS_Props = {
3
+ row: TableRow;
4
+ column: NuxtTableColumn;
5
+ rowKey: string | number;
6
+ value: unknown;
7
+ columnStyle: Record<string, string | undefined>;
8
+ classNames: NuxtTableClassNames;
9
+ };
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ declare const _default: typeof __VLS_export;
12
+ export default _default;
@@ -0,0 +1,35 @@
1
+ import type { ComponentPublicInstance } from 'vue';
2
+ import type { NuxtTableClassNames, NuxtTableColumn } from '../types/table.js';
3
+ type __VLS_Props = {
4
+ column: NuxtTableColumn;
5
+ filterValue: unknown;
6
+ columnStyle: Record<string, string | undefined>;
7
+ sortDirection: 'asc' | 'desc' | null;
8
+ isDragSource: boolean;
9
+ isDragOver: boolean;
10
+ isDndEnabled: boolean;
11
+ isResizeEnabled: boolean;
12
+ classNames: NuxtTableClassNames;
13
+ setHeaderElement: (columnKey: string, element: Element | ComponentPublicInstance | null) => void;
14
+ };
15
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
16
+ drop: (columnKey: string) => any;
17
+ toggleSort: (column: NuxtTableColumn) => any;
18
+ setFilter: (columnKey: string, value: unknown) => any;
19
+ dragStart: (columnKey: string) => any;
20
+ dragOver: (columnKey: string) => any;
21
+ dragLeave: (columnKey: string) => any;
22
+ dragEnd: () => any;
23
+ resizeStart: (event: MouseEvent, columnKey: string) => any;
24
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
25
+ onDrop?: ((columnKey: string) => any) | undefined;
26
+ onToggleSort?: ((column: NuxtTableColumn) => any) | undefined;
27
+ onSetFilter?: ((columnKey: string, value: unknown) => any) | undefined;
28
+ onDragStart?: ((columnKey: string) => any) | undefined;
29
+ onDragOver?: ((columnKey: string) => any) | undefined;
30
+ onDragLeave?: ((columnKey: string) => any) | undefined;
31
+ onDragEnd?: (() => any) | undefined;
32
+ onResizeStart?: ((event: MouseEvent, columnKey: string) => any) | undefined;
33
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
34
+ declare const _default: typeof __VLS_export;
35
+ export default _default;
@@ -0,0 +1,71 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ column: { type: Object, required: true },
4
+ filterValue: { type: null, required: true },
5
+ columnStyle: { type: Object, required: true },
6
+ sortDirection: { type: [String, null], required: true },
7
+ isDragSource: { type: Boolean, required: true },
8
+ isDragOver: { type: Boolean, required: true },
9
+ isDndEnabled: { type: Boolean, required: true },
10
+ isResizeEnabled: { type: Boolean, required: true },
11
+ classNames: { type: Object, required: true },
12
+ setHeaderElement: { type: Function, required: true }
13
+ });
14
+ const emit = defineEmits(["dragStart", "dragOver", "dragLeave", "drop", "dragEnd", "toggleSort", "setFilter", "resizeStart"]);
15
+ </script>
16
+
17
+ <template>
18
+ <th
19
+ :ref="(element) => props.setHeaderElement(props.column.key, element)"
20
+ :style="props.columnStyle"
21
+ :class="[
22
+ props.classNames.headerCell,
23
+ props.column.headerClassName,
24
+ props.isDragSource ? props.classNames.headerCellDragSource : '',
25
+ props.isDragOver && !props.isDragSource ? props.classNames.headerCellDragOver : ''
26
+ ]"
27
+ :draggable="props.isDndEnabled"
28
+ @dragstart="props.isDndEnabled ? emit('dragStart', props.column.key) : void 0"
29
+ @dragover.prevent="props.isDndEnabled ? emit('dragOver', props.column.key) : void 0"
30
+ @dragenter.prevent="props.isDndEnabled ? emit('dragOver', props.column.key) : void 0"
31
+ @dragleave="props.isDndEnabled ? emit('dragLeave', props.column.key) : void 0"
32
+ @drop="props.isDndEnabled ? emit('drop', props.column.key) : void 0"
33
+ @dragend="props.isDndEnabled ? emit('dragEnd') : void 0"
34
+ >
35
+ <div :class="props.classNames.headerTop">
36
+ <span :class="props.classNames.headerLabel">{{ props.column.label }}</span>
37
+ <button
38
+ v-if="props.column.sortable"
39
+ type="button"
40
+ :class="props.classNames.sortButton"
41
+ @click="emit('toggleSort', props.column)"
42
+ >
43
+ <span v-if="props.sortDirection === 'asc'">Asc</span>
44
+ <span v-else-if="props.sortDirection === 'desc'">Desc</span>
45
+ <span v-else>Sort</span>
46
+ </button>
47
+ </div>
48
+
49
+ <component
50
+ :is="props.column.filterComponent"
51
+ v-if="props.column.filterComponent"
52
+ :model-value="props.filterValue"
53
+ :column="props.column"
54
+ @update:model-value="emit('setFilter', props.column.key, $event)"
55
+ />
56
+ <input
57
+ v-else-if="props.column.filterable"
58
+ :value="String(props.filterValue ?? '')"
59
+ type="text"
60
+ :class="props.classNames.filterInput"
61
+ :placeholder="`Filter ${props.column.label}`"
62
+ @input="emit('setFilter', props.column.key, $event.target.value)"
63
+ >
64
+
65
+ <div
66
+ v-if="props.isResizeEnabled"
67
+ :class="props.classNames.resizeHandle"
68
+ @mousedown.stop.prevent="emit('resizeStart', $event, props.column.key)"
69
+ />
70
+ </th>
71
+ </template>
@@ -0,0 +1,35 @@
1
+ import type { ComponentPublicInstance } from 'vue';
2
+ import type { NuxtTableClassNames, NuxtTableColumn } from '../types/table.js';
3
+ type __VLS_Props = {
4
+ column: NuxtTableColumn;
5
+ filterValue: unknown;
6
+ columnStyle: Record<string, string | undefined>;
7
+ sortDirection: 'asc' | 'desc' | null;
8
+ isDragSource: boolean;
9
+ isDragOver: boolean;
10
+ isDndEnabled: boolean;
11
+ isResizeEnabled: boolean;
12
+ classNames: NuxtTableClassNames;
13
+ setHeaderElement: (columnKey: string, element: Element | ComponentPublicInstance | null) => void;
14
+ };
15
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
16
+ drop: (columnKey: string) => any;
17
+ toggleSort: (column: NuxtTableColumn) => any;
18
+ setFilter: (columnKey: string, value: unknown) => any;
19
+ dragStart: (columnKey: string) => any;
20
+ dragOver: (columnKey: string) => any;
21
+ dragLeave: (columnKey: string) => any;
22
+ dragEnd: () => any;
23
+ resizeStart: (event: MouseEvent, columnKey: string) => any;
24
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
25
+ onDrop?: ((columnKey: string) => any) | undefined;
26
+ onToggleSort?: ((column: NuxtTableColumn) => any) | undefined;
27
+ onSetFilter?: ((columnKey: string, value: unknown) => any) | undefined;
28
+ onDragStart?: ((columnKey: string) => any) | undefined;
29
+ onDragOver?: ((columnKey: string) => any) | undefined;
30
+ onDragLeave?: ((columnKey: string) => any) | undefined;
31
+ onDragEnd?: (() => any) | undefined;
32
+ onResizeStart?: ((event: MouseEvent, columnKey: string) => any) | undefined;
33
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
34
+ declare const _default: typeof __VLS_export;
35
+ export default _default;
@@ -0,0 +1,34 @@
1
+ import type { ComponentPublicInstance } from 'vue';
2
+ import type { NuxtTableColumn, TableRow, UseNuxtTableOptions } from '../types/table.js';
3
+ export declare function useNuxtTable(options: UseNuxtTableOptions): {
4
+ orderedColumns: import("vue").ComputedRef<NuxtTableColumn[]>;
5
+ visibleColumns: import("vue").ComputedRef<NuxtTableColumn[]>;
6
+ sortedRows: import("vue").ComputedRef<TableRow[]>;
7
+ filters: import("vue").Ref<Record<string, unknown>, Record<string, unknown>>;
8
+ isColumnManagerOpen: import("vue").Ref<boolean, boolean>;
9
+ enabledColumnKeys: import("vue").Ref<string[], string[]>;
10
+ dragSourceColumnKey: import("vue").Ref<string | null, string | null>;
11
+ dragOverColumnKey: import("vue").Ref<string | null, string | null>;
12
+ getSortDirection: (columnKey: string) => "asc" | "desc" | null;
13
+ toggleSort: (column: NuxtTableColumn) => void;
14
+ setFilter: (columnKey: string, value: unknown) => void;
15
+ clearAllFilters: () => void;
16
+ toggleColumn: (columnKey: string) => void;
17
+ resetColumns: () => void;
18
+ onHeaderDragStart: (columnKey: string) => void;
19
+ onHeaderDragOver: (columnKey: string) => void;
20
+ onHeaderDragLeave: (columnKey: string) => void;
21
+ onHeaderDrop: (targetColumnKey: string) => Promise<void>;
22
+ onHeaderDragEnd: () => void;
23
+ getColumnStyle: (columnKey: string) => {
24
+ width?: undefined;
25
+ minWidth?: undefined;
26
+ } | {
27
+ width: string;
28
+ minWidth: string;
29
+ };
30
+ startColumnResize: (event: MouseEvent, columnKey: string) => void;
31
+ setHeaderElement: (columnKey: string, element: Element | ComponentPublicInstance | null) => void;
32
+ resolveDisplayValue: (row: TableRow, column: NuxtTableColumn) => any;
33
+ resolveRowKey: (row: TableRow, index: number) => any;
34
+ };
@@ -0,0 +1,454 @@
1
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
2
+ const MIN_COLUMN_WIDTH = 140;
3
+ export function useNuxtTable(options) {
4
+ const columnOrder = ref([]);
5
+ const enabledColumnKeys = ref([]);
6
+ const sortState = ref(null);
7
+ const filters = ref({});
8
+ const isColumnManagerOpen = ref(false);
9
+ const dragSourceColumnKey = ref(null);
10
+ const dragOverColumnKey = ref(null);
11
+ const hasLoadedPersistence = ref(false);
12
+ const headerElements = ref({});
13
+ const columnWidths = ref({});
14
+ const activeResize = ref(null);
15
+ const availableColumnKeys = computed(() => options.columns.value.map((column) => column.key));
16
+ const columnsByKey = computed(() => {
17
+ return new Map(options.columns.value.map((column) => [column.key, column]));
18
+ });
19
+ const orderedColumns = computed(() => {
20
+ return columnOrder.value.map((columnKey) => columnsByKey.value.get(columnKey)).filter((column) => Boolean(column));
21
+ });
22
+ const visibleColumns = computed(() => {
23
+ return orderedColumns.value.filter((column) => enabledColumnKeys.value.includes(column.key));
24
+ });
25
+ const filteredRows = computed(() => {
26
+ return options.rows.value.filter((row) => {
27
+ return orderedColumns.value.every((column) => {
28
+ const filterValue = filters.value[column.key];
29
+ if (!isFilterActive(filterValue)) {
30
+ return true;
31
+ }
32
+ if (column.filterFn) {
33
+ return column.filterFn(row, filterValue, column);
34
+ }
35
+ const candidate = resolveColumnValue(row, column.filterKey ?? column.key);
36
+ const candidateText = String(candidate ?? "").toLowerCase();
37
+ const filterText = String(filterValue ?? "").toLowerCase();
38
+ return candidateText.includes(filterText);
39
+ });
40
+ });
41
+ });
42
+ const sortedRows = computed(() => {
43
+ if (!sortState.value) {
44
+ return filteredRows.value;
45
+ }
46
+ const activeColumn = columnsByKey.value.get(sortState.value.key);
47
+ if (!activeColumn) {
48
+ return filteredRows.value;
49
+ }
50
+ const directionMultiplier = sortState.value.direction === "asc" ? 1 : -1;
51
+ const accessor = activeColumn.sortKey ?? activeColumn.key;
52
+ return [...filteredRows.value].sort((leftRow, rightRow) => {
53
+ const leftValue = resolveColumnValue(leftRow, accessor);
54
+ const rightValue = resolveColumnValue(rightRow, accessor);
55
+ return compareValues(leftValue, rightValue) * directionMultiplier;
56
+ });
57
+ });
58
+ onMounted(() => {
59
+ initializeColumnState();
60
+ loadPersistedState();
61
+ hasLoadedPersistence.value = true;
62
+ });
63
+ watch(
64
+ () => options.columns.value,
65
+ () => {
66
+ initializeColumnState();
67
+ },
68
+ { deep: true }
69
+ );
70
+ watch(
71
+ [columnOrder, enabledColumnKeys, columnWidths],
72
+ () => {
73
+ if (!hasLoadedPersistence.value || !import.meta.client) {
74
+ return;
75
+ }
76
+ localStorage.setItem(buildStorageKey("order"), JSON.stringify(columnOrder.value));
77
+ localStorage.setItem(buildStorageKey("enabledColumns"), JSON.stringify(enabledColumnKeys.value));
78
+ localStorage.setItem(buildStorageKey("widths"), JSON.stringify(columnWidths.value));
79
+ },
80
+ { deep: true }
81
+ );
82
+ onBeforeUnmount(() => {
83
+ stopResizing();
84
+ });
85
+ function initializeColumnState() {
86
+ const currentKeys = availableColumnKeys.value;
87
+ if (!columnOrder.value.length) {
88
+ columnOrder.value = [...currentKeys];
89
+ } else {
90
+ const currentKeySet = new Set(currentKeys);
91
+ const keptKeys = columnOrder.value.filter((key) => currentKeySet.has(key));
92
+ const newKeys = currentKeys.filter((key) => !keptKeys.includes(key));
93
+ columnOrder.value = [...keptKeys, ...newKeys];
94
+ }
95
+ if (!enabledColumnKeys.value.length) {
96
+ enabledColumnKeys.value = [...currentKeys];
97
+ } else {
98
+ const currentKeySet = new Set(currentKeys);
99
+ const keptEnabledKeys = enabledColumnKeys.value.filter((key) => currentKeySet.has(key));
100
+ const missingEnabledKeys = currentKeys.filter((key) => !keptEnabledKeys.includes(key));
101
+ enabledColumnKeys.value = [...keptEnabledKeys, ...missingEnabledKeys];
102
+ }
103
+ const nextFilters = {};
104
+ for (const key of currentKeys) {
105
+ nextFilters[key] = filters.value[key] ?? "";
106
+ }
107
+ filters.value = nextFilters;
108
+ const nextWidths = {};
109
+ for (const key of currentKeys) {
110
+ const width = columnWidths.value[key];
111
+ if (typeof width === "number" && Number.isFinite(width)) {
112
+ nextWidths[key] = width;
113
+ }
114
+ }
115
+ columnWidths.value = nextWidths;
116
+ }
117
+ function loadPersistedState() {
118
+ if (!import.meta.client) {
119
+ return;
120
+ }
121
+ try {
122
+ const persistedOrder = localStorage.getItem(buildStorageKey("order"));
123
+ if (persistedOrder) {
124
+ const parsedOrder = JSON.parse(persistedOrder);
125
+ if (Array.isArray(parsedOrder)) {
126
+ const validPersistedOrder = parsedOrder.filter((key) => {
127
+ return typeof key === "string" && availableColumnKeys.value.includes(key);
128
+ });
129
+ const missingKeys = availableColumnKeys.value.filter((key) => !validPersistedOrder.includes(key));
130
+ columnOrder.value = [...validPersistedOrder, ...missingKeys];
131
+ }
132
+ }
133
+ const persistedEnabledColumns = localStorage.getItem(buildStorageKey("enabledColumns"));
134
+ if (persistedEnabledColumns) {
135
+ const parsedEnabledColumns = JSON.parse(persistedEnabledColumns);
136
+ if (Array.isArray(parsedEnabledColumns)) {
137
+ enabledColumnKeys.value = parsedEnabledColumns.filter((key) => {
138
+ return typeof key === "string" && availableColumnKeys.value.includes(key);
139
+ });
140
+ }
141
+ }
142
+ const persistedWidths = localStorage.getItem(buildStorageKey("widths"));
143
+ if (persistedWidths) {
144
+ const parsedWidths = JSON.parse(persistedWidths);
145
+ if (parsedWidths && typeof parsedWidths === "object") {
146
+ const nextWidths = {};
147
+ for (const key of availableColumnKeys.value) {
148
+ const width = parsedWidths[key];
149
+ if (typeof width === "number" && Number.isFinite(width) && width >= MIN_COLUMN_WIDTH) {
150
+ nextWidths[key] = width;
151
+ }
152
+ }
153
+ columnWidths.value = nextWidths;
154
+ }
155
+ }
156
+ } catch {
157
+ columnOrder.value = [...availableColumnKeys.value];
158
+ enabledColumnKeys.value = [...availableColumnKeys.value];
159
+ columnWidths.value = {};
160
+ }
161
+ }
162
+ function buildStorageKey(segment) {
163
+ return `${options.storageKey.value}:${segment}`;
164
+ }
165
+ function getSortDirection(columnKey) {
166
+ if (sortState.value?.key !== columnKey) {
167
+ return null;
168
+ }
169
+ return sortState.value.direction;
170
+ }
171
+ function toggleSort(column) {
172
+ if (!column.sortable) {
173
+ return;
174
+ }
175
+ if (!sortState.value || sortState.value.key !== column.key) {
176
+ sortState.value = { key: column.key, direction: "asc" };
177
+ return;
178
+ }
179
+ if (sortState.value.direction === "asc") {
180
+ sortState.value = { key: column.key, direction: "desc" };
181
+ return;
182
+ }
183
+ sortState.value = null;
184
+ }
185
+ function setFilter(columnKey, value) {
186
+ filters.value[columnKey] = value;
187
+ }
188
+ function clearAllFilters() {
189
+ const nextFilters = {};
190
+ for (const key of availableColumnKeys.value) {
191
+ nextFilters[key] = "";
192
+ }
193
+ filters.value = nextFilters;
194
+ }
195
+ function toggleColumn(columnKey) {
196
+ if (enabledColumnKeys.value.includes(columnKey)) {
197
+ if (enabledColumnKeys.value.length === 1) {
198
+ return;
199
+ }
200
+ enabledColumnKeys.value = enabledColumnKeys.value.filter((key) => key !== columnKey);
201
+ return;
202
+ }
203
+ enabledColumnKeys.value = [...enabledColumnKeys.value, columnKey];
204
+ }
205
+ function resetColumns() {
206
+ columnOrder.value = [...availableColumnKeys.value];
207
+ enabledColumnKeys.value = [...availableColumnKeys.value];
208
+ }
209
+ function onHeaderDragStart(columnKey) {
210
+ if (!options.enableColumnDnd.value || activeResize.value) {
211
+ return;
212
+ }
213
+ dragSourceColumnKey.value = columnKey;
214
+ }
215
+ function onHeaderDragOver(columnKey) {
216
+ if (!options.enableColumnDnd.value) {
217
+ return;
218
+ }
219
+ if (!dragSourceColumnKey.value || dragSourceColumnKey.value === columnKey) {
220
+ return;
221
+ }
222
+ dragOverColumnKey.value = columnKey;
223
+ }
224
+ function onHeaderDragLeave(columnKey) {
225
+ if (dragOverColumnKey.value === columnKey) {
226
+ dragOverColumnKey.value = null;
227
+ }
228
+ }
229
+ async function onHeaderDrop(targetColumnKey) {
230
+ if (!options.enableColumnDnd.value) {
231
+ dragSourceColumnKey.value = null;
232
+ dragOverColumnKey.value = null;
233
+ return;
234
+ }
235
+ if (!dragSourceColumnKey.value || dragSourceColumnKey.value === targetColumnKey) {
236
+ dragSourceColumnKey.value = null;
237
+ dragOverColumnKey.value = null;
238
+ return;
239
+ }
240
+ const beforeRects = getHeaderRects();
241
+ const sourceIndex = columnOrder.value.indexOf(dragSourceColumnKey.value);
242
+ const targetIndex = columnOrder.value.indexOf(targetColumnKey);
243
+ if (sourceIndex < 0 || targetIndex < 0) {
244
+ dragSourceColumnKey.value = null;
245
+ dragOverColumnKey.value = null;
246
+ return;
247
+ }
248
+ const nextOrder = [...columnOrder.value];
249
+ const [moved] = nextOrder.splice(sourceIndex, 1);
250
+ if (!moved) {
251
+ dragSourceColumnKey.value = null;
252
+ dragOverColumnKey.value = null;
253
+ return;
254
+ }
255
+ nextOrder.splice(targetIndex, 0, moved);
256
+ columnOrder.value = nextOrder;
257
+ await nextTick();
258
+ animateHeaderReorder(beforeRects);
259
+ const payload = {
260
+ order: [...nextOrder],
261
+ movedKey: moved,
262
+ fromIndex: sourceIndex,
263
+ toIndex: targetIndex
264
+ };
265
+ options.onColumnOrderChange?.(payload);
266
+ dragSourceColumnKey.value = null;
267
+ dragOverColumnKey.value = null;
268
+ }
269
+ function onHeaderDragEnd() {
270
+ dragSourceColumnKey.value = null;
271
+ dragOverColumnKey.value = null;
272
+ }
273
+ function getColumnStyle(columnKey) {
274
+ const width = columnWidths.value[columnKey];
275
+ if (!width) {
276
+ return {};
277
+ }
278
+ const safeWidth = Math.max(MIN_COLUMN_WIDTH, width);
279
+ return {
280
+ width: `${safeWidth}px`,
281
+ minWidth: `${safeWidth}px`
282
+ };
283
+ }
284
+ function startColumnResize(event, columnKey) {
285
+ if (!import.meta.client) {
286
+ return;
287
+ }
288
+ const currentWidth = columnWidths.value[columnKey] ?? headerElements.value[columnKey]?.getBoundingClientRect().width ?? MIN_COLUMN_WIDTH;
289
+ activeResize.value = {
290
+ columnKey,
291
+ startX: event.clientX,
292
+ startWidth: Math.max(MIN_COLUMN_WIDTH, currentWidth)
293
+ };
294
+ window.addEventListener("mousemove", onColumnResizeMove);
295
+ window.addEventListener("mouseup", onColumnResizeEnd);
296
+ }
297
+ function onColumnResizeMove(event) {
298
+ if (!activeResize.value) {
299
+ return;
300
+ }
301
+ const delta = event.clientX - activeResize.value.startX;
302
+ const nextWidth = Math.max(MIN_COLUMN_WIDTH, Math.round(activeResize.value.startWidth + delta));
303
+ columnWidths.value = {
304
+ ...columnWidths.value,
305
+ [activeResize.value.columnKey]: nextWidth
306
+ };
307
+ }
308
+ function onColumnResizeEnd() {
309
+ stopResizing();
310
+ }
311
+ function stopResizing() {
312
+ if (!import.meta.client) {
313
+ return;
314
+ }
315
+ activeResize.value = null;
316
+ window.removeEventListener("mousemove", onColumnResizeMove);
317
+ window.removeEventListener("mouseup", onColumnResizeEnd);
318
+ }
319
+ function setHeaderElement(columnKey, element) {
320
+ headerElements.value[columnKey] = element instanceof HTMLTableCellElement ? element : null;
321
+ }
322
+ function getHeaderRects() {
323
+ const rectMap = /* @__PURE__ */ new Map();
324
+ for (const column of visibleColumns.value) {
325
+ const element = headerElements.value[column.key];
326
+ if (element) {
327
+ rectMap.set(column.key, element.getBoundingClientRect());
328
+ }
329
+ }
330
+ return rectMap;
331
+ }
332
+ function animateHeaderReorder(beforeRects) {
333
+ if (!import.meta.client || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
334
+ return;
335
+ }
336
+ for (const [columnKey, beforeRect] of beforeRects.entries()) {
337
+ const element = headerElements.value[columnKey];
338
+ if (!element) {
339
+ continue;
340
+ }
341
+ const afterRect = element.getBoundingClientRect();
342
+ const deltaX = beforeRect.left - afterRect.left;
343
+ if (Math.abs(deltaX) < 1) {
344
+ continue;
345
+ }
346
+ element.animate(
347
+ [
348
+ { transform: `translateX(${deltaX}px)` },
349
+ { transform: "translateX(0)" }
350
+ ],
351
+ {
352
+ duration: 220,
353
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)"
354
+ }
355
+ );
356
+ }
357
+ }
358
+ function resolveDisplayValue(row, column) {
359
+ const value = resolveColumnValue(row, column.key);
360
+ if (column.formatter) {
361
+ return column.formatter(value, row);
362
+ }
363
+ return value;
364
+ }
365
+ function resolveRowKey(row, index) {
366
+ if (typeof options.rowKey.value === "function") {
367
+ return options.rowKey.value(row, index);
368
+ }
369
+ return row[options.rowKey.value] ?? index;
370
+ }
371
+ function resolveColumnValue(row, resolver) {
372
+ if (typeof resolver === "function") {
373
+ return resolver(row);
374
+ }
375
+ const pathParts = resolver.split(".");
376
+ let currentValue = row;
377
+ for (const pathPart of pathParts) {
378
+ if (currentValue == null) {
379
+ return void 0;
380
+ }
381
+ currentValue = currentValue[pathPart];
382
+ }
383
+ return currentValue;
384
+ }
385
+ function isFilterActive(value) {
386
+ if (value == null) {
387
+ return false;
388
+ }
389
+ if (typeof value === "string") {
390
+ return value.trim().length > 0;
391
+ }
392
+ if (Array.isArray(value)) {
393
+ return value.length > 0;
394
+ }
395
+ if (typeof value === "object") {
396
+ return Object.values(value).some((nestedValue) => isFilterActive(nestedValue));
397
+ }
398
+ return true;
399
+ }
400
+ function compareValues(leftValue, rightValue) {
401
+ if (leftValue == null && rightValue == null) {
402
+ return 0;
403
+ }
404
+ if (leftValue == null) {
405
+ return -1;
406
+ }
407
+ if (rightValue == null) {
408
+ return 1;
409
+ }
410
+ if (typeof leftValue === "number" && typeof rightValue === "number") {
411
+ return leftValue - rightValue;
412
+ }
413
+ const leftDate = leftValue instanceof Date ? leftValue.getTime() : null;
414
+ const rightDate = rightValue instanceof Date ? rightValue.getTime() : null;
415
+ if (leftDate !== null && rightDate !== null) {
416
+ return leftDate - rightDate;
417
+ }
418
+ const leftText = String(leftValue).toLowerCase();
419
+ const rightText = String(rightValue).toLowerCase();
420
+ if (leftText < rightText) {
421
+ return -1;
422
+ }
423
+ if (leftText > rightText) {
424
+ return 1;
425
+ }
426
+ return 0;
427
+ }
428
+ return {
429
+ orderedColumns,
430
+ visibleColumns,
431
+ sortedRows,
432
+ filters,
433
+ isColumnManagerOpen,
434
+ enabledColumnKeys,
435
+ dragSourceColumnKey,
436
+ dragOverColumnKey,
437
+ getSortDirection,
438
+ toggleSort,
439
+ setFilter,
440
+ clearAllFilters,
441
+ toggleColumn,
442
+ resetColumns,
443
+ onHeaderDragStart,
444
+ onHeaderDragOver,
445
+ onHeaderDragLeave,
446
+ onHeaderDrop,
447
+ onHeaderDragEnd,
448
+ getColumnStyle,
449
+ startColumnResize,
450
+ setHeaderElement,
451
+ resolveDisplayValue,
452
+ resolveRowKey
453
+ };
454
+ }
@@ -0,0 +1,2 @@
1
+ export { useNuxtTable } from './composables/useNuxtTable.js';
2
+ export type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow, UseNuxtTableOptions, ValueResolver, } from './types/table.js';
@@ -0,0 +1 @@
1
+ export { useNuxtTable } from "./composables/useNuxtTable.js";
@@ -0,0 +1,56 @@
1
+ import type { Component, Ref } from 'vue';
2
+ export type TableRow = Record<string, any>;
3
+ export type ValueResolver = string | ((row: TableRow) => unknown);
4
+ export interface NuxtTableColumn {
5
+ key: string;
6
+ label: string;
7
+ sortable?: boolean;
8
+ filterable?: boolean;
9
+ sortKey?: ValueResolver;
10
+ filterKey?: ValueResolver;
11
+ formatter?: (value: unknown, row: TableRow) => string;
12
+ filterFn?: (row: TableRow, filterValue: unknown, column: NuxtTableColumn) => boolean;
13
+ cellComponent?: Component;
14
+ filterComponent?: Component;
15
+ headerClassName?: string;
16
+ cellClassName?: string;
17
+ }
18
+ export interface NuxtTableColumnOrderChange {
19
+ order: string[];
20
+ movedKey: string;
21
+ fromIndex: number;
22
+ toIndex: number;
23
+ }
24
+ export interface NuxtTableClassNames {
25
+ root: string;
26
+ toolbar: string;
27
+ toolbarTitle: string;
28
+ toolbarActions: string;
29
+ toolbarButton: string;
30
+ columnManager: string;
31
+ columnManagerTitle: string;
32
+ columnManagerItem: string;
33
+ tableWrapper: string;
34
+ table: string;
35
+ tableHead: string;
36
+ tableBody: string;
37
+ bodyRow: string;
38
+ emptyCell: string;
39
+ headerCell: string;
40
+ headerCellDragSource: string;
41
+ headerCellDragOver: string;
42
+ headerTop: string;
43
+ headerLabel: string;
44
+ sortButton: string;
45
+ filterInput: string;
46
+ resizeHandle: string;
47
+ bodyCell: string;
48
+ }
49
+ export interface UseNuxtTableOptions {
50
+ columns: Ref<NuxtTableColumn[]>;
51
+ rows: Ref<TableRow[]>;
52
+ storageKey: Ref<string>;
53
+ rowKey: Ref<string | ((row: TableRow, index: number) => string | number)>;
54
+ enableColumnDnd: Ref<boolean>;
55
+ onColumnOrderChange?: (payload: NuxtTableColumnOrderChange) => void;
56
+ }
File without changes
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@serhiitupilow/nuxt-table",
3
+ "version": "0.1.3",
4
+ "description": "Nuxt module with a functional table component (sorting, filtering, column visibility, resize, optional DnD)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "./dist/module.mjs",
8
+ "types": "./dist/types.d.mts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types.d.mts",
12
+ "import": "./dist/module.mjs"
13
+ },
14
+ "./runtime": {
15
+ "types": "./dist/runtime/index.d.ts",
16
+ "import": "./dist/runtime/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "build": "nuxt-module-build build",
27
+ "dev": "nuxt-module-build dev",
28
+ "prepack": "nuxt-module-build build"
29
+ },
30
+ "peerDependencies": {
31
+ "nuxt": ">=3.11.0",
32
+ "vue": ">=3.4.0"
33
+ },
34
+ "devDependencies": {
35
+ "@nuxt/kit": "^4.3.1",
36
+ "@nuxt/module-builder": "^1.0.2",
37
+ "nuxt": "^4.3.1",
38
+ "typescript": "^5.9.3",
39
+ "vue": "^3.5.28",
40
+ "vue-tsc": "^3.2.5"
41
+ }
42
+ }