@soave/nuxt-ui 0.1.0
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/dist/module.cjs +5 -0
- package/dist/module.d.mts +25 -0
- package/dist/module.d.ts +25 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +52 -0
- package/dist/runtime/components/Breadcrumbs.vue +78 -0
- package/dist/runtime/components/DataTable.vue +334 -0
- package/dist/runtime/components/Pagination.vue +154 -0
- package/dist/runtime/components/Table.vue +18 -0
- package/dist/runtime/components/TableBody.vue +15 -0
- package/dist/runtime/components/TableCell.vue +20 -0
- package/dist/runtime/components/TableHead.vue +44 -0
- package/dist/runtime/components/TableHeader.vue +15 -0
- package/dist/runtime/components/TableRow.vue +25 -0
- package/dist/runtime/components/index.d.ts +0 -0
- package/dist/runtime/components/index.js +9 -0
- package/dist/runtime/composables/index.d.ts +0 -0
- package/dist/runtime/composables/index.js +20 -0
- package/dist/runtime/composables/useBreadcrumbs.d.ts +0 -0
- package/dist/runtime/composables/useBreadcrumbs.js +75 -0
- package/dist/runtime/composables/useI18nUI.d.ts +0 -0
- package/dist/runtime/composables/useI18nUI.js +105 -0
- package/dist/runtime/composables/usePagination.d.ts +0 -0
- package/dist/runtime/composables/usePagination.js +114 -0
- package/dist/runtime/composables/useSoaveSeoMeta.d.ts +0 -0
- package/dist/runtime/composables/useSoaveSeoMeta.js +130 -0
- package/dist/runtime/composables/useTypedRoute.d.ts +0 -0
- package/dist/runtime/composables/useTypedRoute.js +22 -0
- package/dist/runtime/plugins/ui-provider.client.d.ts +0 -0
- package/dist/runtime/plugins/ui-provider.client.js +10 -0
- package/dist/runtime/plugins/ui-provider.server.d.ts +0 -0
- package/dist/runtime/plugins/ui-provider.server.js +10 -0
- package/dist/types.d.mts +7 -0
- package/dist/types.d.ts +7 -0
- package/package.json +52 -0
package/dist/module.cjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface SoaveNuxtModuleOptions {
|
|
4
|
+
prefix?: string;
|
|
5
|
+
global?: boolean;
|
|
6
|
+
i18n?: {
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
default_locale?: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
declare const _default: _nuxt_schema.NuxtModule<SoaveNuxtModuleOptions, SoaveNuxtModuleOptions, false>;
|
|
12
|
+
|
|
13
|
+
declare module "@nuxt/schema" {
|
|
14
|
+
interface PublicRuntimeConfig {
|
|
15
|
+
soaveUI: {
|
|
16
|
+
i18n: {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
default_locale: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { _default as default };
|
|
25
|
+
export type { SoaveNuxtModuleOptions };
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface SoaveNuxtModuleOptions {
|
|
4
|
+
prefix?: string;
|
|
5
|
+
global?: boolean;
|
|
6
|
+
i18n?: {
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
default_locale?: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
declare const _default: _nuxt_schema.NuxtModule<SoaveNuxtModuleOptions, SoaveNuxtModuleOptions, false>;
|
|
12
|
+
|
|
13
|
+
declare module "@nuxt/schema" {
|
|
14
|
+
interface PublicRuntimeConfig {
|
|
15
|
+
soaveUI: {
|
|
16
|
+
i18n: {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
default_locale: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { _default as default };
|
|
25
|
+
export type { SoaveNuxtModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addImportsDir, addComponentsDir, addPlugin } from '@nuxt/kit';
|
|
2
|
+
|
|
3
|
+
const module = defineNuxtModule({
|
|
4
|
+
meta: {
|
|
5
|
+
name: "@soave/nuxt-ui",
|
|
6
|
+
configKey: "soaveUI",
|
|
7
|
+
compatibility: {
|
|
8
|
+
nuxt: "^3.0.0 || ^4.0.0"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
defaults: {
|
|
12
|
+
prefix: "",
|
|
13
|
+
global: true,
|
|
14
|
+
i18n: {
|
|
15
|
+
enabled: false,
|
|
16
|
+
default_locale: "en"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
async setup(options, nuxt) {
|
|
20
|
+
const resolver = createResolver(import.meta.url);
|
|
21
|
+
nuxt.options.build.transpile.push("@soave/ui");
|
|
22
|
+
addImportsDir(resolver.resolve("./runtime/composables"));
|
|
23
|
+
await addComponentsDir({
|
|
24
|
+
path: resolver.resolve("./runtime/components"),
|
|
25
|
+
prefix: options.prefix,
|
|
26
|
+
global: options.global
|
|
27
|
+
});
|
|
28
|
+
await addComponentsDir({
|
|
29
|
+
path: resolver.resolve("../../core/components/ui"),
|
|
30
|
+
prefix: options.prefix,
|
|
31
|
+
global: options.global
|
|
32
|
+
});
|
|
33
|
+
addPlugin({
|
|
34
|
+
src: resolver.resolve("./runtime/plugins/ui-provider.client"),
|
|
35
|
+
mode: "client"
|
|
36
|
+
});
|
|
37
|
+
addPlugin({
|
|
38
|
+
src: resolver.resolve("./runtime/plugins/ui-provider.server"),
|
|
39
|
+
mode: "server"
|
|
40
|
+
});
|
|
41
|
+
nuxt.options.runtimeConfig.public.soaveUI = {
|
|
42
|
+
i18n: options.i18n
|
|
43
|
+
};
|
|
44
|
+
nuxt.hook("prepare:types", ({ references }) => {
|
|
45
|
+
references.push({
|
|
46
|
+
path: resolver.resolve("./types.d.ts")
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export { module as default };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav :aria-label="aria_label" :class="cn(base_classes, props.class)">
|
|
3
|
+
<ol class="flex items-center gap-2">
|
|
4
|
+
<li
|
|
5
|
+
v-for="(item, index) in items"
|
|
6
|
+
:key="index"
|
|
7
|
+
class="flex items-center gap-2"
|
|
8
|
+
>
|
|
9
|
+
<component
|
|
10
|
+
:is="item.href && !item.current ? 'a' : 'span'"
|
|
11
|
+
:href="item.href"
|
|
12
|
+
:class="item_classes(item)"
|
|
13
|
+
:aria-current="item.current ? 'page' : undefined"
|
|
14
|
+
:aria-disabled="item.disabled ? 'true' : undefined"
|
|
15
|
+
>
|
|
16
|
+
<slot name="icon" :item="item" :index="index">
|
|
17
|
+
<component
|
|
18
|
+
v-if="item.icon"
|
|
19
|
+
:is="item.icon"
|
|
20
|
+
class="h-4 w-4"
|
|
21
|
+
/>
|
|
22
|
+
</slot>
|
|
23
|
+
<slot name="item" :item="item" :index="index">
|
|
24
|
+
{{ item.label }}
|
|
25
|
+
</slot>
|
|
26
|
+
</component>
|
|
27
|
+
|
|
28
|
+
<slot v-if="index < items.length - 1" name="separator">
|
|
29
|
+
<span
|
|
30
|
+
:class="separator_classes"
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
>
|
|
33
|
+
{{ separator }}
|
|
34
|
+
</span>
|
|
35
|
+
</slot>
|
|
36
|
+
</li>
|
|
37
|
+
</ol>
|
|
38
|
+
</nav>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<script setup lang="ts">
|
|
42
|
+
import { computed } from "vue"
|
|
43
|
+
import { cn } from "@soave/ui"
|
|
44
|
+
import type { BreadcrumbItem } from "../../types"
|
|
45
|
+
|
|
46
|
+
export interface BreadcrumbsProps {
|
|
47
|
+
items: BreadcrumbItem[]
|
|
48
|
+
separator?: string
|
|
49
|
+
aria_label?: string
|
|
50
|
+
class?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const props = withDefaults(defineProps<BreadcrumbsProps>(), {
|
|
54
|
+
separator: "/",
|
|
55
|
+
aria_label: "Breadcrumb"
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const base_classes = "flex"
|
|
59
|
+
|
|
60
|
+
const separator_classes = computed(() =>
|
|
61
|
+
cn("text-muted-foreground select-none")
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const item_classes = (item: BreadcrumbItem): string => {
|
|
65
|
+
const base = "text-sm transition-colors"
|
|
66
|
+
const current_state = item.current
|
|
67
|
+
? "text-foreground font-medium pointer-events-none"
|
|
68
|
+
: "text-muted-foreground hover:text-foreground"
|
|
69
|
+
const disabled_state = item.disabled && !item.current
|
|
70
|
+
? "opacity-50 pointer-events-none"
|
|
71
|
+
: ""
|
|
72
|
+
const link_state = item.href && !item.current
|
|
73
|
+
? "hover:underline underline-offset-4"
|
|
74
|
+
: ""
|
|
75
|
+
|
|
76
|
+
return cn(base, current_state, disabled_state, link_state)
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="cn('space-y-4', props.class)">
|
|
3
|
+
<!-- Header slot for search, filters, etc. -->
|
|
4
|
+
<slot name="header" />
|
|
5
|
+
|
|
6
|
+
<!-- Table -->
|
|
7
|
+
<Table>
|
|
8
|
+
<TableHeader>
|
|
9
|
+
<TableRow>
|
|
10
|
+
<!-- Selection checkbox column -->
|
|
11
|
+
<TableHead v-if="selectable" class="w-12">
|
|
12
|
+
<input
|
|
13
|
+
type="checkbox"
|
|
14
|
+
:checked="is_all_selected"
|
|
15
|
+
:indeterminate="is_some_selected && !is_all_selected"
|
|
16
|
+
class="h-4 w-4 rounded border-gray-300"
|
|
17
|
+
aria-label="Select all rows"
|
|
18
|
+
@change="toggleSelectAll"
|
|
19
|
+
/>
|
|
20
|
+
</TableHead>
|
|
21
|
+
|
|
22
|
+
<!-- Column headers -->
|
|
23
|
+
<TableHead
|
|
24
|
+
v-for="column in columns"
|
|
25
|
+
:key="String(column.key)"
|
|
26
|
+
:sortable="sortable && column.sortable !== false"
|
|
27
|
+
:sort_direction="getSortDirection(column.key)"
|
|
28
|
+
:class="column.width ? `w-[${column.width}]` : ''"
|
|
29
|
+
:style="{ textAlign: column.align || 'left' }"
|
|
30
|
+
@sort="handleSort(column.key)"
|
|
31
|
+
>
|
|
32
|
+
<slot :name="`header-${String(column.key)}`" :column="column">
|
|
33
|
+
{{ column.label }}
|
|
34
|
+
</slot>
|
|
35
|
+
</TableHead>
|
|
36
|
+
|
|
37
|
+
<!-- Actions column -->
|
|
38
|
+
<TableHead v-if="$slots.actions" class="w-12">
|
|
39
|
+
<span class="sr-only">Actions</span>
|
|
40
|
+
</TableHead>
|
|
41
|
+
</TableRow>
|
|
42
|
+
</TableHeader>
|
|
43
|
+
|
|
44
|
+
<TableBody>
|
|
45
|
+
<!-- Empty state -->
|
|
46
|
+
<TableRow v-if="paginated_data.length === 0">
|
|
47
|
+
<TableCell
|
|
48
|
+
:colspan="column_count"
|
|
49
|
+
class="h-24 text-center"
|
|
50
|
+
>
|
|
51
|
+
<slot name="empty">
|
|
52
|
+
No results found.
|
|
53
|
+
</slot>
|
|
54
|
+
</TableCell>
|
|
55
|
+
</TableRow>
|
|
56
|
+
|
|
57
|
+
<!-- Data rows -->
|
|
58
|
+
<TableRow
|
|
59
|
+
v-for="(row, row_index) in paginated_data"
|
|
60
|
+
:key="getRowKey(row, row_index)"
|
|
61
|
+
:selected="isRowSelected(row)"
|
|
62
|
+
>
|
|
63
|
+
<!-- Selection checkbox -->
|
|
64
|
+
<TableCell v-if="selectable">
|
|
65
|
+
<input
|
|
66
|
+
type="checkbox"
|
|
67
|
+
:checked="isRowSelected(row)"
|
|
68
|
+
class="h-4 w-4 rounded border-gray-300"
|
|
69
|
+
:aria-label="`Select row ${row_index + 1}`"
|
|
70
|
+
@change="toggleRowSelection(row)"
|
|
71
|
+
/>
|
|
72
|
+
</TableCell>
|
|
73
|
+
|
|
74
|
+
<!-- Data cells -->
|
|
75
|
+
<TableCell
|
|
76
|
+
v-for="column in columns"
|
|
77
|
+
:key="String(column.key)"
|
|
78
|
+
:style="{ textAlign: column.align || 'left' }"
|
|
79
|
+
>
|
|
80
|
+
<slot
|
|
81
|
+
:name="`cell-${String(column.key)}`"
|
|
82
|
+
:row="row"
|
|
83
|
+
:value="getCellValue(row, column)"
|
|
84
|
+
:column="column"
|
|
85
|
+
:index="row_index"
|
|
86
|
+
>
|
|
87
|
+
{{ getCellValue(row, column) }}
|
|
88
|
+
</slot>
|
|
89
|
+
</TableCell>
|
|
90
|
+
|
|
91
|
+
<!-- Actions cell -->
|
|
92
|
+
<TableCell v-if="$slots.actions">
|
|
93
|
+
<slot name="actions" :row="row" :index="row_index" />
|
|
94
|
+
</TableCell>
|
|
95
|
+
</TableRow>
|
|
96
|
+
</TableBody>
|
|
97
|
+
</Table>
|
|
98
|
+
|
|
99
|
+
<!-- Pagination -->
|
|
100
|
+
<div v-if="pagination && pagination_state.total_pages > 1" class="flex items-center justify-between">
|
|
101
|
+
<div class="text-sm text-muted-foreground">
|
|
102
|
+
<slot name="pagination-info" :state="pagination_state">
|
|
103
|
+
Showing {{ start_item }} to {{ end_item }} of {{ pagination_state.total_items }} results
|
|
104
|
+
</slot>
|
|
105
|
+
</div>
|
|
106
|
+
<Pagination
|
|
107
|
+
:state="pagination_state"
|
|
108
|
+
:pages="pagination_pages"
|
|
109
|
+
@page="handlePageChange"
|
|
110
|
+
@previous="handlePreviousPage"
|
|
111
|
+
@next="handleNextPage"
|
|
112
|
+
@first="handleFirstPage"
|
|
113
|
+
@last="handleLastPage"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- Footer slot -->
|
|
118
|
+
<slot name="footer" />
|
|
119
|
+
</div>
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<script setup lang="ts" generic="T extends Record<string, unknown>">
|
|
123
|
+
import { computed, ref, watch } from "vue"
|
|
124
|
+
import { cn } from "@soave/ui"
|
|
125
|
+
import { usePagination } from "../composables/usePagination"
|
|
126
|
+
import type { TableColumn, TableSortState } from "../../types"
|
|
127
|
+
import Table from "./Table.vue"
|
|
128
|
+
import TableHeader from "./TableHeader.vue"
|
|
129
|
+
import TableBody from "./TableBody.vue"
|
|
130
|
+
import TableRow from "./TableRow.vue"
|
|
131
|
+
import TableHead from "./TableHead.vue"
|
|
132
|
+
import TableCell from "./TableCell.vue"
|
|
133
|
+
import Pagination from "./Pagination.vue"
|
|
134
|
+
|
|
135
|
+
export interface DataTableProps<T> {
|
|
136
|
+
columns: TableColumn<T>[]
|
|
137
|
+
data: T[]
|
|
138
|
+
row_key?: keyof T | ((row: T) => string | number)
|
|
139
|
+
sortable?: boolean
|
|
140
|
+
selectable?: boolean
|
|
141
|
+
pagination?: boolean
|
|
142
|
+
per_page?: number
|
|
143
|
+
class?: string
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const props = withDefaults(defineProps<DataTableProps<T>>(), {
|
|
147
|
+
sortable: false,
|
|
148
|
+
selectable: false,
|
|
149
|
+
pagination: false,
|
|
150
|
+
per_page: 10
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const emit = defineEmits<{
|
|
154
|
+
sort: [state: TableSortState]
|
|
155
|
+
select: [rows: T[]]
|
|
156
|
+
"page-change": [page: number]
|
|
157
|
+
}>()
|
|
158
|
+
|
|
159
|
+
// Sort state
|
|
160
|
+
const sort_state = ref<TableSortState | null>(null)
|
|
161
|
+
|
|
162
|
+
// Selected rows
|
|
163
|
+
const selected_rows = ref<Set<T>>(new Set())
|
|
164
|
+
|
|
165
|
+
// Pagination
|
|
166
|
+
const {
|
|
167
|
+
state: pagination_state,
|
|
168
|
+
pages: pagination_pages,
|
|
169
|
+
setPage,
|
|
170
|
+
nextPage,
|
|
171
|
+
previousPage,
|
|
172
|
+
firstPage,
|
|
173
|
+
lastPage,
|
|
174
|
+
setTotalItems
|
|
175
|
+
} = usePagination({
|
|
176
|
+
per_page: props.per_page
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Watch data changes to update total items
|
|
180
|
+
watch(
|
|
181
|
+
() => props.data,
|
|
182
|
+
(new_data) => {
|
|
183
|
+
setTotalItems(new_data.length)
|
|
184
|
+
},
|
|
185
|
+
{ immediate: true }
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// Computed
|
|
189
|
+
const sorted_data = computed<T[]>(() => {
|
|
190
|
+
if (!sort_state.value || !props.sortable) {
|
|
191
|
+
return props.data
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const { column, direction } = sort_state.value
|
|
195
|
+
const sorted = [...props.data].sort((a, b) => {
|
|
196
|
+
const a_value = a[column as keyof T]
|
|
197
|
+
const b_value = b[column as keyof T]
|
|
198
|
+
|
|
199
|
+
if (a_value === b_value) return 0
|
|
200
|
+
if (a_value === null || a_value === undefined) return 1
|
|
201
|
+
if (b_value === null || b_value === undefined) return -1
|
|
202
|
+
|
|
203
|
+
const comparison = a_value < b_value ? -1 : 1
|
|
204
|
+
return direction === "asc" ? comparison : -comparison
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
return sorted
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const paginated_data = computed<T[]>(() => {
|
|
211
|
+
if (!props.pagination) {
|
|
212
|
+
return sorted_data.value
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const start = (pagination_state.value.current_page - 1) * pagination_state.value.per_page
|
|
216
|
+
const end = start + pagination_state.value.per_page
|
|
217
|
+
return sorted_data.value.slice(start, end)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const column_count = computed(() => {
|
|
221
|
+
let count = props.columns.length
|
|
222
|
+
if (props.selectable) count++
|
|
223
|
+
return count
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const is_all_selected = computed(() => {
|
|
227
|
+
return props.data.length > 0 && selected_rows.value.size === props.data.length
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const is_some_selected = computed(() => {
|
|
231
|
+
return selected_rows.value.size > 0
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const start_item = computed(() => {
|
|
235
|
+
return (pagination_state.value.current_page - 1) * pagination_state.value.per_page + 1
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const end_item = computed(() => {
|
|
239
|
+
return Math.min(
|
|
240
|
+
pagination_state.value.current_page * pagination_state.value.per_page,
|
|
241
|
+
pagination_state.value.total_items
|
|
242
|
+
)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// Methods
|
|
246
|
+
const getRowKey = (row: T, index: number): string | number => {
|
|
247
|
+
if (!props.row_key) {
|
|
248
|
+
return index
|
|
249
|
+
}
|
|
250
|
+
if (typeof props.row_key === "function") {
|
|
251
|
+
return props.row_key(row)
|
|
252
|
+
}
|
|
253
|
+
return String(row[props.row_key])
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const getCellValue = (row: T, column: TableColumn<T>): unknown => {
|
|
257
|
+
const value = row[column.key as keyof T]
|
|
258
|
+
if (column.render) {
|
|
259
|
+
return column.render(value, row)
|
|
260
|
+
}
|
|
261
|
+
return value
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const getSortDirection = (column_key: keyof T | string): "asc" | "desc" | null => {
|
|
265
|
+
if (!sort_state.value || sort_state.value.column !== column_key) {
|
|
266
|
+
return null
|
|
267
|
+
}
|
|
268
|
+
return sort_state.value.direction
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const handleSort = (column_key: keyof T | string): void => {
|
|
272
|
+
if (!props.sortable) return
|
|
273
|
+
|
|
274
|
+
let direction: "asc" | "desc" = "asc"
|
|
275
|
+
|
|
276
|
+
if (sort_state.value && sort_state.value.column === column_key) {
|
|
277
|
+
direction = sort_state.value.direction === "asc" ? "desc" : "asc"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
sort_state.value = {
|
|
281
|
+
column: String(column_key),
|
|
282
|
+
direction
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
emit("sort", sort_state.value)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const isRowSelected = (row: T): boolean => {
|
|
289
|
+
return selected_rows.value.has(row)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const toggleRowSelection = (row: T): void => {
|
|
293
|
+
if (selected_rows.value.has(row)) {
|
|
294
|
+
selected_rows.value.delete(row)
|
|
295
|
+
} else {
|
|
296
|
+
selected_rows.value.add(row)
|
|
297
|
+
}
|
|
298
|
+
emit("select", Array.from(selected_rows.value))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const toggleSelectAll = (): void => {
|
|
302
|
+
if (is_all_selected.value) {
|
|
303
|
+
selected_rows.value.clear()
|
|
304
|
+
} else {
|
|
305
|
+
selected_rows.value = new Set(props.data)
|
|
306
|
+
}
|
|
307
|
+
emit("select", Array.from(selected_rows.value))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const handlePageChange = (page: number): void => {
|
|
311
|
+
setPage(page)
|
|
312
|
+
emit("page-change", page)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const handlePreviousPage = (): void => {
|
|
316
|
+
previousPage()
|
|
317
|
+
emit("page-change", pagination_state.value.current_page)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const handleNextPage = (): void => {
|
|
321
|
+
nextPage()
|
|
322
|
+
emit("page-change", pagination_state.value.current_page)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const handleFirstPage = (): void => {
|
|
326
|
+
firstPage()
|
|
327
|
+
emit("page-change", pagination_state.value.current_page)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const handleLastPage = (): void => {
|
|
331
|
+
lastPage()
|
|
332
|
+
emit("page-change", pagination_state.value.current_page)
|
|
333
|
+
}
|
|
334
|
+
</script>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav
|
|
3
|
+
:aria-label="aria_label"
|
|
4
|
+
:class="cn('flex items-center justify-center gap-1', props.class)"
|
|
5
|
+
role="navigation"
|
|
6
|
+
>
|
|
7
|
+
<!-- First Page Button -->
|
|
8
|
+
<button
|
|
9
|
+
v-if="show_first_last"
|
|
10
|
+
type="button"
|
|
11
|
+
:class="button_classes"
|
|
12
|
+
:disabled="!state.has_previous"
|
|
13
|
+
:aria-label="first_label"
|
|
14
|
+
@click="emit('first')"
|
|
15
|
+
>
|
|
16
|
+
<slot name="first-icon">
|
|
17
|
+
<span aria-hidden="true">«</span>
|
|
18
|
+
</slot>
|
|
19
|
+
</button>
|
|
20
|
+
|
|
21
|
+
<!-- Previous Page Button -->
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
:class="button_classes"
|
|
25
|
+
:disabled="!state.has_previous"
|
|
26
|
+
:aria-label="previous_label"
|
|
27
|
+
@click="emit('previous')"
|
|
28
|
+
>
|
|
29
|
+
<slot name="previous-icon">
|
|
30
|
+
<span aria-hidden="true">‹</span>
|
|
31
|
+
</slot>
|
|
32
|
+
</button>
|
|
33
|
+
|
|
34
|
+
<!-- Page Numbers -->
|
|
35
|
+
<template v-for="(page, index) in pages" :key="index">
|
|
36
|
+
<span
|
|
37
|
+
v-if="page === 'ellipsis'"
|
|
38
|
+
:class="ellipsis_classes"
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
>
|
|
41
|
+
<slot name="ellipsis">…</slot>
|
|
42
|
+
</span>
|
|
43
|
+
<button
|
|
44
|
+
v-else
|
|
45
|
+
type="button"
|
|
46
|
+
:class="page_button_classes(page)"
|
|
47
|
+
:aria-label="`${page_label} ${page}`"
|
|
48
|
+
:aria-current="page === state.current_page ? 'page' : undefined"
|
|
49
|
+
@click="emit('page', page)"
|
|
50
|
+
>
|
|
51
|
+
{{ page }}
|
|
52
|
+
</button>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<!-- Next Page Button -->
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
:class="button_classes"
|
|
59
|
+
:disabled="!state.has_next"
|
|
60
|
+
:aria-label="next_label"
|
|
61
|
+
@click="emit('next')"
|
|
62
|
+
>
|
|
63
|
+
<slot name="next-icon">
|
|
64
|
+
<span aria-hidden="true">›</span>
|
|
65
|
+
</slot>
|
|
66
|
+
</button>
|
|
67
|
+
|
|
68
|
+
<!-- Last Page Button -->
|
|
69
|
+
<button
|
|
70
|
+
v-if="show_first_last"
|
|
71
|
+
type="button"
|
|
72
|
+
:class="button_classes"
|
|
73
|
+
:disabled="!state.has_next"
|
|
74
|
+
:aria-label="last_label"
|
|
75
|
+
@click="emit('last')"
|
|
76
|
+
>
|
|
77
|
+
<slot name="last-icon">
|
|
78
|
+
<span aria-hidden="true">»</span>
|
|
79
|
+
</slot>
|
|
80
|
+
</button>
|
|
81
|
+
</nav>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<script setup lang="ts">
|
|
85
|
+
import { cn } from "@soave/ui"
|
|
86
|
+
import type { PaginationState } from "../../types"
|
|
87
|
+
|
|
88
|
+
export interface PaginationProps {
|
|
89
|
+
state: PaginationState
|
|
90
|
+
pages: (number | "ellipsis")[]
|
|
91
|
+
size?: "sm" | "md" | "lg"
|
|
92
|
+
show_first_last?: boolean
|
|
93
|
+
aria_label?: string
|
|
94
|
+
previous_label?: string
|
|
95
|
+
next_label?: string
|
|
96
|
+
first_label?: string
|
|
97
|
+
last_label?: string
|
|
98
|
+
page_label?: string
|
|
99
|
+
class?: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const props = withDefaults(defineProps<PaginationProps>(), {
|
|
103
|
+
size: "md",
|
|
104
|
+
show_first_last: true,
|
|
105
|
+
aria_label: "Pagination",
|
|
106
|
+
previous_label: "Go to previous page",
|
|
107
|
+
next_label: "Go to next page",
|
|
108
|
+
first_label: "Go to first page",
|
|
109
|
+
last_label: "Go to last page",
|
|
110
|
+
page_label: "Go to page"
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const emit = defineEmits<{
|
|
114
|
+
page: [page: number]
|
|
115
|
+
previous: []
|
|
116
|
+
next: []
|
|
117
|
+
first: []
|
|
118
|
+
last: []
|
|
119
|
+
}>()
|
|
120
|
+
|
|
121
|
+
const size_classes = {
|
|
122
|
+
sm: "h-8 min-w-8 text-xs",
|
|
123
|
+
md: "h-9 min-w-9 text-sm",
|
|
124
|
+
lg: "h-10 min-w-10 text-base"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const button_classes = cn(
|
|
128
|
+
"inline-flex items-center justify-center rounded-md font-medium",
|
|
129
|
+
"transition-colors focus-visible:outline-none focus-visible:ring-2",
|
|
130
|
+
"focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
131
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
132
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
133
|
+
size_classes[props.size]
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const ellipsis_classes = cn(
|
|
137
|
+
"flex items-center justify-center",
|
|
138
|
+
"text-muted-foreground",
|
|
139
|
+
size_classes[props.size]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const page_button_classes = (page: number): string => {
|
|
143
|
+
const is_current = page === props.state.current_page
|
|
144
|
+
return cn(
|
|
145
|
+
"inline-flex items-center justify-center rounded-md font-medium",
|
|
146
|
+
"transition-colors focus-visible:outline-none focus-visible:ring-2",
|
|
147
|
+
"focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
148
|
+
size_classes[props.size],
|
|
149
|
+
is_current
|
|
150
|
+
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
151
|
+
: "border border-input bg-background hover:bg-accent hover:text-accent-foreground"
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
</script>
|