@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.
Files changed (35) hide show
  1. package/dist/module.cjs +5 -0
  2. package/dist/module.d.mts +25 -0
  3. package/dist/module.d.ts +25 -0
  4. package/dist/module.json +12 -0
  5. package/dist/module.mjs +52 -0
  6. package/dist/runtime/components/Breadcrumbs.vue +78 -0
  7. package/dist/runtime/components/DataTable.vue +334 -0
  8. package/dist/runtime/components/Pagination.vue +154 -0
  9. package/dist/runtime/components/Table.vue +18 -0
  10. package/dist/runtime/components/TableBody.vue +15 -0
  11. package/dist/runtime/components/TableCell.vue +20 -0
  12. package/dist/runtime/components/TableHead.vue +44 -0
  13. package/dist/runtime/components/TableHeader.vue +15 -0
  14. package/dist/runtime/components/TableRow.vue +25 -0
  15. package/dist/runtime/components/index.d.ts +0 -0
  16. package/dist/runtime/components/index.js +9 -0
  17. package/dist/runtime/composables/index.d.ts +0 -0
  18. package/dist/runtime/composables/index.js +20 -0
  19. package/dist/runtime/composables/useBreadcrumbs.d.ts +0 -0
  20. package/dist/runtime/composables/useBreadcrumbs.js +75 -0
  21. package/dist/runtime/composables/useI18nUI.d.ts +0 -0
  22. package/dist/runtime/composables/useI18nUI.js +105 -0
  23. package/dist/runtime/composables/usePagination.d.ts +0 -0
  24. package/dist/runtime/composables/usePagination.js +114 -0
  25. package/dist/runtime/composables/useSoaveSeoMeta.d.ts +0 -0
  26. package/dist/runtime/composables/useSoaveSeoMeta.js +130 -0
  27. package/dist/runtime/composables/useTypedRoute.d.ts +0 -0
  28. package/dist/runtime/composables/useTypedRoute.js +22 -0
  29. package/dist/runtime/plugins/ui-provider.client.d.ts +0 -0
  30. package/dist/runtime/plugins/ui-provider.client.js +10 -0
  31. package/dist/runtime/plugins/ui-provider.server.d.ts +0 -0
  32. package/dist/runtime/plugins/ui-provider.server.js +10 -0
  33. package/dist/types.d.mts +7 -0
  34. package/dist/types.d.ts +7 -0
  35. package/package.json +52 -0
@@ -0,0 +1,5 @@
1
+ module.exports = function(...args) {
2
+ return import('./module.mjs').then(m => m.default.call(this, ...args))
3
+ }
4
+ const _meta = module.exports.meta = require('./module.json')
5
+ module.exports.getMeta = () => Promise.resolve(_meta)
@@ -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 };
@@ -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 };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@soave/nuxt-ui",
3
+ "configKey": "soaveUI",
4
+ "compatibility": {
5
+ "nuxt": "^3.0.0 || ^4.0.0"
6
+ },
7
+ "version": "0.1.0",
8
+ "builder": {
9
+ "@nuxt/module-builder": "0.8.4",
10
+ "unbuild": "2.0.0"
11
+ }
12
+ }
@@ -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">&laquo;</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">&lsaquo;</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">&hellip;</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">&rsaquo;</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">&raquo;</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>