@datametria/vue-components 2.3.1 → 2.4.1

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 (126) hide show
  1. package/README.md +625 -594
  2. package/dist/index.es.js +1962 -1887
  3. package/dist/index.umd.js +6 -6
  4. package/dist/src/components/DatametriaForm.vue.d.ts +1 -1
  5. package/dist/src/components/DatametriaInput.vue.d.ts +9 -1
  6. package/dist/src/components/DatametriaSelect.vue.d.ts +10 -1
  7. package/dist/vue-components.css +1 -1
  8. package/package.json +103 -102
  9. package/src/components/DatametriaAlert.vue +151 -133
  10. package/src/components/DatametriaAutocomplete.vue +250 -229
  11. package/src/components/DatametriaAvatar.vue +256 -238
  12. package/src/components/DatametriaBadge.vue +101 -96
  13. package/src/components/DatametriaBreadcrumb.vue +132 -128
  14. package/src/components/DatametriaButton.vue +191 -173
  15. package/src/components/DatametriaCard.vue +84 -66
  16. package/src/components/DatametriaCheckbox.vue +197 -193
  17. package/src/components/DatametriaChip.vue +159 -141
  18. package/src/components/DatametriaContainer.vue +70 -52
  19. package/src/components/DatametriaDataTable.vue +318 -300
  20. package/src/components/DatametriaDatePicker.vue +396 -378
  21. package/src/components/DatametriaDialog.vue +297 -293
  22. package/src/components/DatametriaDivider.vue +105 -98
  23. package/src/components/DatametriaDropdown.vue +356 -350
  24. package/src/components/DatametriaEmpty.vue +155 -151
  25. package/src/components/DatametriaFileUpload.vue +413 -395
  26. package/src/components/DatametriaFloatingBar.vue +144 -126
  27. package/src/components/DatametriaForm.vue +174 -156
  28. package/src/components/DatametriaFormItem.vue +183 -179
  29. package/src/components/DatametriaGrid.vue +55 -37
  30. package/src/components/DatametriaInput.vue +314 -263
  31. package/src/components/DatametriaMenu.vue +618 -600
  32. package/src/components/DatametriaModal.vue +147 -129
  33. package/src/components/DatametriaNavbar.vue +277 -223
  34. package/src/components/DatametriaPagination.vue +375 -371
  35. package/src/components/DatametriaPasswordInput.vue +446 -426
  36. package/src/components/DatametriaPopconfirm.vue +240 -234
  37. package/src/components/DatametriaProgress.vue +228 -224
  38. package/src/components/DatametriaRadio.vue +151 -147
  39. package/src/components/DatametriaResult.vue +135 -131
  40. package/src/components/DatametriaSelect.vue +311 -211
  41. package/src/components/DatametriaSidebar.vue +294 -222
  42. package/src/components/DatametriaSkeleton.vue +257 -234
  43. package/src/components/DatametriaSlider.vue +409 -391
  44. package/src/components/DatametriaSortableTable.vue +826 -802
  45. package/src/components/DatametriaSpinner.vue +114 -110
  46. package/src/components/DatametriaSteps.vue +318 -312
  47. package/src/components/DatametriaSwitch.vue +146 -142
  48. package/src/components/DatametriaTabPane.vue +94 -76
  49. package/src/components/DatametriaTable.vue +118 -100
  50. package/src/components/DatametriaTabs.vue +315 -297
  51. package/src/components/DatametriaTextarea.vue +213 -195
  52. package/src/components/DatametriaTimePicker.vue +317 -299
  53. package/src/components/DatametriaToast.vue +176 -176
  54. package/src/components/DatametriaTooltip.vue +421 -400
  55. package/src/components/DatametriaTree.vue +126 -122
  56. package/src/components/DatametriaTreeNode.vue +176 -172
  57. package/src/components/DatametriaUpload.vue +379 -361
  58. package/src/components/__tests__/DatametriaAlert.test.js +35 -35
  59. package/src/components/__tests__/DatametriaAlert.test.ts +190 -190
  60. package/src/components/__tests__/DatametriaAvatar.test.ts +151 -151
  61. package/src/components/__tests__/DatametriaBadge.test.js +29 -29
  62. package/src/components/__tests__/DatametriaBadge.test.ts +167 -167
  63. package/src/components/__tests__/DatametriaBreadcrumb.test.ts +187 -0
  64. package/src/components/__tests__/DatametriaButton.test.js +30 -30
  65. package/src/components/__tests__/DatametriaButton.test.ts +283 -283
  66. package/src/components/__tests__/DatametriaCard.test.ts +201 -201
  67. package/src/components/__tests__/DatametriaCheckbox.test.ts +204 -0
  68. package/src/components/__tests__/DatametriaChip.test.js +38 -38
  69. package/src/components/__tests__/DatametriaContainer.test.ts +52 -52
  70. package/src/components/__tests__/DatametriaDialog.test.ts +338 -0
  71. package/src/components/__tests__/DatametriaDivider.test.ts +54 -54
  72. package/src/components/__tests__/DatametriaDropdown.test.ts +357 -0
  73. package/src/components/__tests__/DatametriaEmpty.test.ts +261 -0
  74. package/src/components/__tests__/DatametriaFileUpload.test.ts +290 -290
  75. package/src/components/__tests__/DatametriaFloatingBar.test.ts +137 -137
  76. package/src/components/__tests__/DatametriaForm.test.ts +96 -0
  77. package/src/components/__tests__/DatametriaFormItem.test.ts +58 -0
  78. package/src/components/__tests__/DatametriaGrid.test.ts +31 -31
  79. package/src/components/__tests__/DatametriaInput.test.ts +72 -72
  80. package/src/components/__tests__/DatametriaMenu.test.ts +366 -366
  81. package/src/components/__tests__/DatametriaModal.test.ts +86 -86
  82. package/src/components/__tests__/DatametriaNavbar.test.js +48 -48
  83. package/src/components/__tests__/DatametriaNavbar.test.ts +203 -203
  84. package/src/components/__tests__/DatametriaPasswordInput.test.js +305 -305
  85. package/src/components/__tests__/DatametriaRadio.test.ts +195 -0
  86. package/src/components/__tests__/DatametriaSelect.test.ts +77 -77
  87. package/src/components/__tests__/DatametriaSidebar.test.ts +169 -169
  88. package/src/components/__tests__/DatametriaSlider.test.ts +261 -261
  89. package/src/components/__tests__/DatametriaSortableTable.test.js +168 -168
  90. package/src/components/__tests__/DatametriaSpinner.test.ts +156 -156
  91. package/src/components/__tests__/DatametriaSteps.test.ts +211 -0
  92. package/src/components/__tests__/DatametriaSwitch.test.ts +129 -0
  93. package/src/components/__tests__/DatametriaTabPane.test.ts +205 -0
  94. package/src/components/__tests__/DatametriaTable.test.ts +97 -97
  95. package/src/components/__tests__/DatametriaTabs.test.ts +232 -232
  96. package/src/components/__tests__/DatametriaToast.test.js +48 -48
  97. package/src/components/__tests__/DatametriaToast.test.ts +99 -99
  98. package/src/components/__tests__/DatametriaTree.test.ts +376 -0
  99. package/src/components/__tests__/index.test.ts +48 -0
  100. package/src/composables/useAccessibilityScale.ts +94 -94
  101. package/src/composables/useBreakpoints.ts +82 -82
  102. package/src/composables/useHapticFeedback.ts +439 -439
  103. package/src/composables/useRipple.ts +218 -218
  104. package/src/composables/useTheme.ts +5 -1
  105. package/src/index.ts +84 -84
  106. package/src/stories/Variants.stories.js +95 -95
  107. package/src/styles/design-tokens.css +623 -623
  108. package/src/theme/ThemeProvider.vue +96 -96
  109. package/src/theme/__tests__/ThemeProvider.test.ts +208 -208
  110. package/src/theme/__tests__/constants.test.ts +31 -31
  111. package/src/theme/__tests__/presets.test.ts +166 -166
  112. package/src/theme/__tests__/tokens.test.ts +155 -155
  113. package/src/theme/__tests__/types.test.ts +153 -153
  114. package/src/theme/__tests__/useTheme.test.ts +146 -146
  115. package/src/theme/constants.ts +14 -14
  116. package/src/theme/index.ts +12 -12
  117. package/src/theme/presets/datametria.ts +94 -94
  118. package/src/theme/presets/default.ts +94 -94
  119. package/src/theme/presets/index.ts +8 -8
  120. package/src/theme/tokens/colors.ts +28 -28
  121. package/src/theme/tokens/index.ts +47 -47
  122. package/src/theme/tokens/spacing.ts +21 -21
  123. package/src/theme/tokens/typography.ts +35 -35
  124. package/src/theme/types.ts +111 -111
  125. package/src/theme/useTheme.ts +28 -28
  126. package/src/types/index.ts +55 -55
@@ -1,806 +1,830 @@
1
- <template>
2
- <div class="datametria-sortable-table">
3
- <!-- Search Bar -->
4
- <div v-if="searchable" class="datametria-sortable-table__search">
5
- <input
6
- v-model="searchQuery"
7
- type="text"
8
- placeholder="Buscar..."
9
- class="datametria-sortable-table__search-input"
10
- aria-label="Buscar na tabela"
11
- />
12
- </div>
13
-
14
- <!-- Table -->
15
- <div class="datametria-sortable-table__wrapper">
16
- <table class="datametria-sortable-table__table" role="table">
17
- <thead class="datametria-sortable-table__thead">
18
- <tr role="row">
19
- <!-- Selection Column -->
20
- <th
21
- v-if="selectable"
22
- class="datametria-sortable-table__th datametria-sortable-table__th--checkbox"
23
- role="columnheader"
24
- >
25
- <input
26
- type="checkbox"
27
- :checked="isAllSelected"
28
- :indeterminate="isIndeterminate"
29
- aria-label="Selecionar todas as linhas"
30
- @change="toggleSelectAll"
31
- />
32
- </th>
33
-
34
- <!-- Data Columns -->
35
- <th
36
- v-for="column in columns"
37
- :key="column.key"
38
- class="datametria-sortable-table__th"
39
- :class="{ 'datametria-sortable-table__th--sortable': column.sortable !== false }"
40
- :style="{ width: column.width }"
41
- role="columnheader"
42
- :aria-sort="sortKey === column.key ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none'"
43
- @click="column.sortable !== false && toggleSort(column.key)"
44
- @keydown.enter="column.sortable !== false && toggleSort(column.key)"
45
- @keydown.space.prevent="column.sortable !== false && toggleSort(column.key)"
46
- :tabindex="column.sortable !== false ? 0 : undefined"
47
- >
48
- <div class="datametria-sortable-table__th-content">
49
- <span>{{ column.label }}</span>
50
- <span v-if="column.sortable !== false" class="datametria-sortable-table__sort-icon" aria-hidden="true">
51
- <span v-if="sortKey === column.key">
52
- {{ sortOrder === 'asc' ? '↑' : '↓' }}
53
- </span>
54
- <span v-else class="datametria-sortable-table__sort-icon--inactive">↕</span>
55
- </span>
56
- </div>
57
-
58
- <!-- Column Filter -->
59
- <div v-if="filterable && column.filterable !== false" @click.stop>
60
- <!-- Select Filter -->
61
- <select
62
- v-if="column.filterType === 'select'"
63
- v-model="columnFilters[column.key]"
64
- :aria-label="`Filtrar por ${column.label}`"
65
- class="datametria-sortable-table__filter-input"
66
- @keydown.stop
67
- >
68
- <option value="">Todos</option>
69
- <option
70
- v-for="option in getFilterOptions(column)"
71
- :key="option.value"
72
- :value="option.value"
73
- >
74
- {{ option.label }}
75
- </option>
76
- </select>
77
-
78
- <!-- Multi-Select Filter -->
79
- <div
80
- v-else-if="column.filterType === 'multiselect'"
81
- class="datametria-sortable-table__multiselect"
82
- >
83
- <button
84
- type="button"
85
- class="datametria-sortable-table__multiselect-trigger"
86
- @click="toggleMultiselect(column.key)"
87
- >
88
- {{ getMultiselectLabel(column) }}
89
- </button>
90
- <div
91
- v-if="activeMultiselect === column.key"
92
- class="datametria-sortable-table__multiselect-dropdown"
93
- >
94
- <label
95
- v-for="option in getFilterOptions(column)"
96
- :key="option.value"
97
- class="datametria-sortable-table__multiselect-option"
98
- >
99
- <input
100
- type="checkbox"
101
- :value="option.value"
102
- :checked="isMultiselectChecked(column.key, option.value)"
103
- @change="toggleMultiselectOption(column.key, option.value)"
104
- />
105
- {{ option.label }}
106
- </label>
107
- </div>
108
- </div>
109
-
110
- <!-- Text/Date Filter -->
111
- <input
112
- v-else
113
- v-model="columnFilters[column.key]"
114
- :type="column.filterType || getFilterInputType(column)"
115
- :placeholder="`Filtrar ${column.label}...`"
116
- :aria-label="`Filtrar por ${column.label}`"
117
- class="datametria-sortable-table__filter-input"
118
- @keydown.stop
119
- />
120
- </div>
121
- </th>
122
- </tr>
123
- </thead>
124
-
125
- <tbody class="datametria-sortable-table__tbody">
126
- <tr
127
- v-for="(row, index) in paginatedData"
128
- :key="row.id || index"
129
- class="datametria-sortable-table__tr"
130
- :class="{ 'datametria-sortable-table__tr--selected': selectedRows.has(row.id || index) }"
131
- role="row"
132
- >
133
- <!-- Selection Column -->
134
- <td
135
- v-if="selectable"
136
- class="datametria-sortable-table__td datametria-sortable-table__td--checkbox"
137
- role="cell"
138
- >
139
- <input
140
- type="checkbox"
141
- :checked="selectedRows.has(row.id || index)"
142
- :aria-label="`Selecionar linha ${index + 1}`"
143
- @change="toggleSelectRow(row.id || index)"
144
- />
145
- </td>
146
-
147
- <!-- Data Columns -->
148
- <td
149
- v-for="column in columns"
150
- :key="column.key"
151
- class="datametria-sortable-table__td"
152
- role="cell"
153
- >
154
- <slot :name="`cell-${column.key}`" :row="row" :value="row[column.key]">
155
- {{ row[column.key] }}
156
- </slot>
157
- </td>
158
- </tr>
159
- </tbody>
160
- </table>
161
-
162
- <!-- Empty State -->
163
- <div v-if="filteredData.length === 0" class="datametria-sortable-table__empty" role="status">
164
- <slot name="empty">Nenhum dado encontrado</slot>
165
- </div>
166
- </div>
167
-
168
- <!-- Pagination -->
169
- <div
170
- v-if="paginated && filteredData.length > 0"
171
- class="datametria-sortable-table__pagination"
172
- role="navigation"
173
- aria-label="Paginação da tabela"
174
- >
175
- <div class="datametria-sortable-table__pagination-info" aria-live="polite">
176
- Mostrando {{ startIndex + 1 }} - {{ endIndex }} de {{ filteredData.length }} registros
177
- <span v-if="selectedRows.size > 0">({{ selectedRows.size }} selecionados)</span>
178
- </div>
179
-
180
- <div class="datametria-sortable-table__pagination-controls">
181
- <select
182
- v-model.number="currentPageSize"
183
- class="datametria-sortable-table__page-size"
184
- aria-label="Itens por página"
185
- >
186
- <option v-for="size in pageSizeOptions" :key="size" :value="size">
187
- {{ size }} por página
188
- </option>
189
- </select>
190
-
191
- <button
192
- class="datametria-sortable-table__pagination-btn"
193
- :disabled="currentPage === 1"
194
- aria-label="Primeira página"
195
- @click="currentPage = 1"
196
- >
197
- ««
198
- </button>
199
- <button
200
- class="datametria-sortable-table__pagination-btn"
201
- :disabled="currentPage === 1"
202
- aria-label="Página anterior"
203
- @click="currentPage--"
204
- >
205
-
206
- </button>
207
-
208
- <span class="datametria-sortable-table__pagination-pages" aria-current="page">
209
- Página {{ currentPage }} de {{ totalPages }}
210
- </span>
211
-
212
- <button
213
- class="datametria-sortable-table__pagination-btn"
214
- :disabled="currentPage === totalPages"
215
- aria-label="Próxima página"
216
- @click="currentPage++"
217
- >
218
-
219
- </button>
220
- <button
221
- class="datametria-sortable-table__pagination-btn"
222
- :disabled="currentPage === totalPages"
223
- aria-label="Última página"
224
- @click="currentPage = totalPages"
225
- >
226
- »»
227
- </button>
228
- </div>
229
- </div>
230
- </div>
231
- </template>
232
-
233
- <script setup lang="ts">
234
- import { ref, computed, watch } from 'vue'
235
- import type { SortableTableProps } from '../types'
236
-
237
- const props = withDefaults(defineProps<SortableTableProps>(), {
238
- selectable: false,
239
- searchable: true,
240
- filterable: true,
241
- paginated: true,
242
- pageSize: 10,
243
- pageSizeOptions: () => [5, 10, 25, 50, 100]
244
- })
245
-
246
- // Validação em desenvolvimento
247
- if (process.env.NODE_ENV === 'development') {
248
- if (!props.columns || props.columns.length === 0) {
249
- console.warn('[DatametriaSortableTable] No columns provided')
250
- }
251
- if (!props.data) {
252
- console.warn('[DatametriaSortableTable] No data provided')
253
- }
254
- }
255
-
256
- const emit = defineEmits<{
257
- 'selection-change': [selectedIds: (string | number)[]]
258
- }>()
259
-
260
- // Detect filter input type based on column data
261
- const getFilterInputType = (column: any) => {
262
- if (!props.data || props.data.length === 0) return 'text'
263
-
264
- const sampleValue = props.data[0][column.key]
265
-
266
- // Check if it's a date
267
- if (sampleValue instanceof Date) return 'date'
268
- if (typeof sampleValue === 'string' && !isNaN(Date.parse(sampleValue))) {
269
- // Check if it looks like a date string (YYYY-MM-DD, DD/MM/YYYY, etc)
270
- const datePattern = /^\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/
271
- if (datePattern.test(sampleValue)) return 'date'
272
- }
273
-
274
- return 'text'
275
- }
276
-
277
- // Get filter options (auto-generate if 'auto')
278
- const getFilterOptions = (column: any) => {
279
- if (column.filterOptions === 'auto') {
280
- const uniqueValues = new Set<string>()
281
- props.data.forEach(row => {
282
- const value = row[column.key]
283
- if (value !== null && value !== undefined && value !== '') {
284
- uniqueValues.add(String(value))
285
- }
286
- })
287
- return Array.from(uniqueValues)
288
- .sort()
289
- .map(value => ({ value, label: value }))
290
- }
291
- return column.filterOptions || []
292
- }
293
-
294
- // Multi-select methods
295
- const toggleMultiselect = (key: string) => {
296
- activeMultiselect.value = activeMultiselect.value === key ? null : key
297
- }
298
-
299
- const getMultiselectLabel = (column: any) => {
300
- const selected = columnFilters.value[column.key] as string[] || []
301
- if (selected.length === 0) return 'Todos'
302
- if (selected.length === 1) {
303
- const options = getFilterOptions(column)
304
- const option = options.find((o: any) => o.value === selected[0])
305
- return option?.label || selected[0]
306
- }
307
- return `${selected.length} selecionados`
308
- }
309
-
310
- const isMultiselectChecked = (key: string, value: string) => {
311
- const selected = columnFilters.value[key] as string[] || []
312
- return selected.includes(value)
313
- }
314
-
315
- const toggleMultiselectOption = (key: string, value: string) => {
316
- const selected = (columnFilters.value[key] as string[]) || []
317
- const index = selected.indexOf(value)
318
-
319
- if (index > -1) {
320
- columnFilters.value[key] = selected.filter(v => v !== value)
321
- } else {
322
- columnFilters.value[key] = [...selected, value]
323
- }
324
- }
325
-
326
- // Search
327
- const searchQuery = ref('')
328
-
329
- // Sorting
330
- const sortKey = ref<string>('')
331
- const sortOrder = ref<'asc' | 'desc'>('asc')
332
-
333
- // Filtering
334
- const columnFilters = ref<Record<string, string | string[]>>({})
335
- const activeMultiselect = ref<string | null>(null)
336
-
337
- // Selection
338
- const selectedRows = ref<Set<string | number>>(new Set())
339
-
340
- // Pagination
341
- const currentPage = ref(1)
342
- const currentPageSize = ref(props.pageSize)
343
-
344
- // Computed: Filtered Data
345
- const filteredData = computed(() => {
346
- let result = [...props.data]
347
-
348
- // Global search
349
- if (searchQuery.value) {
350
- const query = searchQuery.value.toLowerCase()
351
- result = result.filter(row =>
352
- Object.values(row).some(value =>
353
- String(value).toLowerCase().includes(query)
354
- )
355
- )
356
- }
357
-
358
- // Column filters
359
- Object.entries(columnFilters.value).forEach(([key, filterValue]) => {
360
- if (filterValue) {
361
- // Multi-select filter
362
- if (Array.isArray(filterValue) && filterValue.length > 0) {
363
- result = result.filter(row =>
364
- filterValue.includes(String(row[key]))
365
- )
366
- }
367
- // Text/Date/Select filter
368
- else if (typeof filterValue === 'string') {
369
- const query = filterValue.toLowerCase()
370
- result = result.filter(row =>
371
- String(row[key]).toLowerCase().includes(query)
372
- )
373
- }
374
- }
375
- })
376
-
377
- // Sorting
378
- if (sortKey.value) {
379
- result.sort((a, b) => {
380
- let aVal = a[sortKey.value]
381
- let bVal = b[sortKey.value]
382
-
383
- // Handle null/undefined
384
- if (aVal == null && bVal == null) return 0
385
- if (aVal == null) return 1
386
- if (bVal == null) return -1
387
-
388
- // Convert dates to timestamps for proper comparison
389
- if (aVal instanceof Date) aVal = aVal.getTime()
390
- if (bVal instanceof Date) bVal = bVal.getTime()
391
-
392
- // Try to parse as date if string looks like a date
393
- if (typeof aVal === 'string' && !isNaN(Date.parse(aVal))) {
394
- const datePattern = /^\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/
395
- if (datePattern.test(aVal)) {
396
- aVal = new Date(aVal).getTime()
397
- }
398
- }
399
- if (typeof bVal === 'string' && !isNaN(Date.parse(bVal))) {
400
- const datePattern = /^\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/
401
- if (datePattern.test(bVal)) {
402
- bVal = new Date(bVal).getTime()
403
- }
404
- }
405
-
406
- // Numeric comparison
407
- if (typeof aVal === 'number' && typeof bVal === 'number') {
408
- return sortOrder.value === 'asc' ? aVal - bVal : bVal - aVal
409
- }
410
-
411
- // String comparison (case-insensitive)
412
- const aStr = String(aVal).toLowerCase()
413
- const bStr = String(bVal).toLowerCase()
414
-
415
- if (aStr === bStr) return 0
416
- const comparison = aStr > bStr ? 1 : -1
417
- return sortOrder.value === 'asc' ? comparison : -comparison
418
- })
419
- }
420
-
421
- return result
422
- })
423
-
424
- // Computed: Pagination
425
- const totalPages = computed(() =>
426
- Math.ceil(filteredData.value.length / currentPageSize.value)
427
- )
428
-
429
- const startIndex = computed(() =>
430
- (currentPage.value - 1) * currentPageSize.value
431
- )
432
-
433
- const endIndex = computed(() =>
434
- Math.min(startIndex.value + currentPageSize.value, filteredData.value.length)
435
- )
436
-
437
- const paginatedData = computed(() => {
438
- if (!props.paginated) return filteredData.value
439
- return filteredData.value.slice(startIndex.value, endIndex.value)
440
- })
441
-
442
- // Computed: Selection
443
- const isAllSelected = computed(() =>
444
- paginatedData.value.length > 0 &&
445
- paginatedData.value.every(row => selectedRows.value.has(row.id || props.data.indexOf(row)))
446
- )
447
-
448
- const isIndeterminate = computed(() =>
449
- selectedRows.value.size > 0 && !isAllSelected.value
450
- )
451
-
452
- // Methods: Sorting
453
- const toggleSort = (key: string) => {
454
- if (sortKey.value === key) {
455
- sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
456
- } else {
457
- sortKey.value = key
458
- sortOrder.value = 'asc'
459
- }
460
- }
461
-
462
- // Methods: Selection
463
- const toggleSelectRow = (id: string | number) => {
464
- if (selectedRows.value.has(id)) {
465
- selectedRows.value.delete(id)
466
- } else {
467
- selectedRows.value.add(id)
468
- }
469
- emit('selection-change', Array.from(selectedRows.value))
470
- }
471
-
472
- const toggleSelectAll = () => {
473
- if (isAllSelected.value) {
474
- paginatedData.value.forEach(row => {
475
- selectedRows.value.delete(row.id || props.data.indexOf(row))
476
- })
477
- } else {
478
- paginatedData.value.forEach(row => {
479
- selectedRows.value.add(row.id || props.data.indexOf(row))
480
- })
481
- }
482
- emit('selection-change', Array.from(selectedRows.value))
483
- }
484
-
485
- // Watch: Reset page on filter change
486
- watch([searchQuery, columnFilters], () => {
487
- currentPage.value = 1
488
- }, { deep: true })
489
-
490
- // Watch: Reset page if current page exceeds total pages
491
- watch(totalPages, (newTotal) => {
492
- if (currentPage.value > newTotal && newTotal > 0) {
493
- currentPage.value = newTotal
494
- }
495
- })
496
- </script>
497
-
498
- <style scoped>
499
- .datametria-sortable-table {
500
- width: 100%;
501
- background: var(--dm-neutral-50, #ffffff);
502
- border-radius: var(--dm-radius-md, 0.375rem);
503
- box-shadow: var(--dm-shadow-sm, 0 1px 3px rgba(0, 0, 0, 0.1));
504
- }
505
-
506
- .datametria-sortable-table__search {
507
- padding: var(--dm-spacing-4, 1rem);
508
- border-bottom: 1px solid var(--dm-neutral-100, #f3f4f6);
509
- }
510
-
511
- .datametria-sortable-table__search-input {
512
- width: 100%;
513
- padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-4, 1rem);
514
- border: 1px solid var(--dm-neutral-200, #e5e7eb);
515
- border-radius: var(--dm-radius-md, 0.375rem);
516
- font-size: var(--dm-font-size-sm, 0.875rem);
517
- transition: all 0.2s ease;
518
- }
519
-
520
- .datametria-sortable-table__search-input:focus {
521
- outline: none;
522
- border-color: var(--dm-primary, #0072CE);
523
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
524
- }
525
-
526
- .datametria-sortable-table__wrapper {
527
- overflow-x: auto;
528
- }
529
-
530
- .datametria-sortable-table__table {
531
- width: 100%;
532
- border-collapse: collapse;
533
- }
534
-
535
- .datametria-sortable-table__thead {
536
- background: var(--dm-neutral-50, #f9fafb);
537
- border-bottom: 2px solid var(--dm-neutral-200, #e5e7eb);
538
- }
539
-
540
- .datametria-sortable-table__th {
541
- padding: var(--dm-spacing-3, 0.75rem) var(--dm-spacing-4, 1rem);
542
- text-align: left;
543
- font-size: var(--dm-font-size-sm, 0.875rem);
544
- font-weight: var(--dm-font-weight-semibold, 600);
545
- color: var(--dm-neutral-700, #374151);
546
- position: relative;
547
- }
548
-
549
- .datametria-sortable-table__th--sortable {
550
- cursor: pointer;
551
- user-select: none;
552
- }
553
-
554
- .datametria-sortable-table__th--sortable:hover {
555
- background: var(--dm-neutral-100, #f3f4f6);
556
- }
557
-
558
- .datametria-sortable-table__th--sortable:focus-visible {
559
- outline: none;
560
- box-shadow: inset 0 0 0 2px var(--dm-primary, #0072CE);
561
- }
562
-
563
- .datametria-sortable-table__th--checkbox {
564
- width: 40px;
565
- padding: var(--dm-spacing-3, 0.75rem) var(--dm-spacing-2, 0.5rem);
566
- }
567
-
568
- .datametria-sortable-table__th-content {
569
- display: flex;
570
- align-items: center;
571
- gap: var(--dm-spacing-2, 0.5rem);
572
- }
573
-
574
- .datametria-sortable-table__sort-icon {
575
- font-size: var(--dm-font-size-sm, 0.875rem);
576
- color: var(--dm-primary, #0072CE);
577
- }
578
-
579
- .datametria-sortable-table__sort-icon--inactive {
580
- color: var(--dm-neutral-400, #9ca3af);
581
- opacity: 0.5;
582
- }
583
-
584
- .datametria-sortable-table__filter-input {
585
- width: 100%;
586
- margin-top: var(--dm-spacing-2, 0.5rem);
587
- padding: var(--dm-spacing-2, 0.5rem);
588
- border: 1px solid var(--dm-neutral-200, #e5e7eb);
589
- border-radius: var(--dm-radius-md, 0.375rem);
590
- font-size: var(--dm-font-size-sm, 0.875rem);
591
- font-weight: 400;
592
- transition: all 0.2s ease;
593
- }
594
-
595
- .datametria-sortable-table__filter-input:focus {
596
- outline: none;
597
- border-color: var(--dm-primary, #0072CE);
598
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
599
- }
600
-
601
- .datametria-sortable-table__multiselect {
602
- position: relative;
603
- margin-top: var(--dm-spacing-2, 0.5rem);
604
- }
605
-
606
- .datametria-sortable-table__multiselect-trigger {
607
- width: 100%;
608
- padding: var(--dm-spacing-2, 0.5rem);
609
- border: 1px solid var(--dm-neutral-200, #e5e7eb);
610
- border-radius: var(--dm-radius-md, 0.375rem);
611
- font-size: var(--dm-font-size-sm, 0.875rem);
612
- font-weight: 400;
613
- text-align: left;
614
- background: var(--dm-neutral-50, #ffffff);
615
- cursor: pointer;
616
- transition: all 0.2s ease;
617
- }
618
-
619
- .datametria-sortable-table__multiselect-trigger:hover {
620
- border-color: var(--dm-primary, #0072CE);
621
- }
622
-
623
- .datametria-sortable-table__multiselect-trigger:focus {
624
- outline: none;
625
- border-color: var(--dm-primary, #0072CE);
626
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
627
- }
628
-
629
- .datametria-sortable-table__multiselect-dropdown {
630
- position: absolute;
631
- top: 100%;
632
- left: 0;
633
- right: 0;
634
- margin-top: var(--dm-spacing-1, 0.25rem);
635
- max-height: 200px;
636
- overflow-y: auto;
637
- background: var(--dm-neutral-50, #ffffff);
638
- border: 1px solid var(--dm-neutral-200, #e5e7eb);
639
- border-radius: var(--dm-radius-md, 0.375rem);
640
- box-shadow: var(--dm-shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.1));
641
- z-index: 10;
642
- }
643
-
644
- .datametria-sortable-table__multiselect-option {
645
- display: flex;
646
- align-items: center;
647
- gap: var(--dm-spacing-2, 0.5rem);
648
- padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-3, 0.75rem);
649
- font-size: var(--dm-font-size-sm, 0.875rem);
650
- cursor: pointer;
651
- transition: background-color 0.2s ease;
652
- }
653
-
654
- .datametria-sortable-table__multiselect-option:hover {
655
- background: var(--dm-neutral-100, #f3f4f6);
656
- }
657
-
658
- .datametria-sortable-table__multiselect-option input[type="checkbox"] {
659
- margin: 0;
660
- }
661
-
662
- .datametria-sortable-table__tbody {
663
- background: var(--dm-neutral-50, #ffffff);
664
- }
665
-
666
- .datametria-sortable-table__tr {
667
- border-bottom: 1px solid var(--dm-neutral-100, #f3f4f6);
668
- transition: background-color 0.2s ease;
669
- }
670
-
671
- .datametria-sortable-table__tr:hover {
672
- background: var(--dm-neutral-50, #f9fafb);
673
- }
674
-
675
- .datametria-sortable-table__tr--selected {
676
- background: color-mix(in srgb, var(--dm-primary, #0072CE) 10%, transparent);
677
- }
678
-
679
- .datametria-sortable-table__td {
680
- padding: var(--dm-spacing-3, 0.75rem) var(--dm-spacing-4, 1rem);
681
- font-size: var(--dm-font-size-sm, 0.875rem);
682
- color: var(--dm-neutral-900, #111827);
683
- }
684
-
685
- .datametria-sortable-table__td--checkbox {
686
- width: 40px;
687
- padding: var(--dm-spacing-3, 0.75rem) var(--dm-spacing-2, 0.5rem);
688
- }
689
-
690
- .datametria-sortable-table__empty {
691
- padding: var(--dm-spacing-4, 1rem);
692
- text-align: center;
693
- color: var(--dm-neutral-500, #6b7280);
694
- font-size: var(--dm-font-size-sm, 0.875rem);
695
- }
696
-
697
- .datametria-sortable-table__pagination {
698
- display: flex;
699
- justify-content: space-between;
700
- align-items: center;
701
- padding: var(--dm-spacing-4, 1rem);
702
- border-top: 1px solid var(--dm-neutral-200, #e5e7eb);
703
- flex-wrap: wrap;
704
- gap: var(--dm-spacing-4, 1rem);
705
- }
706
-
707
- .datametria-sortable-table__pagination-info {
708
- font-size: var(--dm-font-size-sm, 0.875rem);
709
- color: var(--dm-neutral-600, #4b5563);
710
- }
711
-
712
- .datametria-sortable-table__pagination-controls {
713
- display: flex;
714
- align-items: center;
715
- gap: var(--dm-spacing-2, 0.5rem);
716
- }
717
-
718
- .datametria-sortable-table__page-size {
719
- padding: var(--dm-spacing-2, 0.5rem);
720
- border: 1px solid var(--dm-neutral-200, #e5e7eb);
721
- border-radius: var(--dm-radius-md, 0.375rem);
722
- font-size: var(--dm-font-size-sm, 0.875rem);
723
- cursor: pointer;
724
- transition: all 0.2s ease;
725
- background: var(--dm-neutral-50, #ffffff);
726
- color: var(--dm-neutral-900, #111827);
727
- }
728
-
729
- .datametria-sortable-table__page-size:focus {
730
- outline: none;
731
- border-color: var(--dm-primary, #0072CE);
732
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
733
- }
734
-
735
- .datametria-sortable-table__pagination-btn {
736
- padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-3, 0.75rem);
737
- border: 1px solid var(--dm-neutral-200, #e5e7eb);
738
- border-radius: var(--dm-radius-md, 0.375rem);
739
- background: var(--dm-neutral-50, #ffffff);
740
- color: var(--dm-neutral-900, #111827);
741
- font-size: var(--dm-font-size-sm, 0.875rem);
742
- cursor: pointer;
743
- transition: all 0.2s ease;
744
- }
745
-
746
- .datametria-sortable-table__pagination-btn:hover:not(:disabled) {
747
- background: var(--dm-neutral-100, #f3f4f6);
748
- border-color: var(--dm-primary, #0072CE);
749
- }
750
-
751
- .datametria-sortable-table__pagination-btn:focus-visible {
752
- outline: none;
753
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
754
- }
755
-
756
- .datametria-sortable-table__pagination-btn:disabled {
757
- opacity: 0.5;
758
- cursor: not-allowed;
759
- }
760
-
761
- .datametria-sortable-table__pagination-pages {
762
- padding: 0 var(--dm-spacing-2, 0.5rem);
763
- font-size: var(--dm-font-size-sm, 0.875rem);
764
- color: var(--dm-neutral-600, #4b5563);
765
- }
766
-
767
- input[type="checkbox"] {
768
- width: 16px;
769
- height: 16px;
770
- cursor: pointer;
771
- accent-color: var(--dm-primary, #0072CE);
772
- }
773
-
774
- input[type="checkbox"]:focus-visible {
775
- outline: 2px solid var(--dm-primary, #0072CE);
776
- outline-offset: 2px;
777
- }
778
-
779
- /* Mobile Responsiveness */
780
- @media (max-width: 640px) {
781
- .datametria-sortable-table__pagination {
782
- flex-direction: column;
783
- align-items: stretch;
784
- }
785
-
786
- .datametria-sortable-table__pagination-controls {
787
- justify-content: center;
788
- }
789
-
790
- .datametria-sortable-table__th,
791
- .datametria-sortable-table__td {
792
- padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-3, 0.75rem);
793
- font-size: calc(var(--dm-font-size-sm, 0.875rem) * 0.9);
1
+ <template>
2
+ <div class="datametria-sortable-table">
3
+ <!-- Search Bar -->
4
+ <div v-if="searchable" class="datametria-sortable-table__search">
5
+ <input
6
+ v-model="searchQuery"
7
+ type="text"
8
+ placeholder="Buscar..."
9
+ class="datametria-sortable-table__search-input"
10
+ aria-label="Buscar na tabela"
11
+ />
12
+ </div>
13
+
14
+ <!-- Table -->
15
+ <div class="datametria-sortable-table__wrapper">
16
+ <table class="datametria-sortable-table__table" role="table">
17
+ <thead class="datametria-sortable-table__thead">
18
+ <tr role="row">
19
+ <!-- Selection Column -->
20
+ <th
21
+ v-if="selectable"
22
+ class="datametria-sortable-table__th datametria-sortable-table__th--checkbox"
23
+ role="columnheader"
24
+ >
25
+ <input
26
+ type="checkbox"
27
+ :checked="isAllSelected"
28
+ :indeterminate="isIndeterminate"
29
+ aria-label="Selecionar todas as linhas"
30
+ @change="toggleSelectAll"
31
+ />
32
+ </th>
33
+
34
+ <!-- Data Columns -->
35
+ <th
36
+ v-for="column in columns"
37
+ :key="column.key"
38
+ class="datametria-sortable-table__th"
39
+ :class="{ 'datametria-sortable-table__th--sortable': column.sortable !== false }"
40
+ :style="{ width: column.width }"
41
+ role="columnheader"
42
+ :aria-sort="sortKey === column.key ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none'"
43
+ @click="column.sortable !== false && toggleSort(column.key)"
44
+ @keydown.enter="column.sortable !== false && toggleSort(column.key)"
45
+ @keydown.space.prevent="column.sortable !== false && toggleSort(column.key)"
46
+ :tabindex="column.sortable !== false ? 0 : undefined"
47
+ >
48
+ <div class="datametria-sortable-table__th-content">
49
+ <span>{{ column.label }}</span>
50
+ <span v-if="column.sortable !== false" class="datametria-sortable-table__sort-icon" aria-hidden="true">
51
+ <span v-if="sortKey === column.key">
52
+ {{ sortOrder === 'asc' ? '↑' : '↓' }}
53
+ </span>
54
+ <span v-else class="datametria-sortable-table__sort-icon--inactive">↕</span>
55
+ </span>
56
+ </div>
57
+
58
+ <!-- Column Filter -->
59
+ <div v-if="filterable && column.filterable !== false" @click.stop>
60
+ <!-- Select Filter -->
61
+ <select
62
+ v-if="column.filterType === 'select'"
63
+ v-model="columnFilters[column.key]"
64
+ :aria-label="`Filtrar por ${column.label}`"
65
+ class="datametria-sortable-table__filter-input"
66
+ @keydown.stop
67
+ >
68
+ <option value="">Todos</option>
69
+ <option
70
+ v-for="option in getFilterOptions(column)"
71
+ :key="option.value"
72
+ :value="option.value"
73
+ >
74
+ {{ option.label }}
75
+ </option>
76
+ </select>
77
+
78
+ <!-- Multi-Select Filter -->
79
+ <div
80
+ v-else-if="column.filterType === 'multiselect'"
81
+ class="datametria-sortable-table__multiselect"
82
+ >
83
+ <button
84
+ type="button"
85
+ class="datametria-sortable-table__multiselect-trigger"
86
+ @click="toggleMultiselect(column.key)"
87
+ >
88
+ {{ getMultiselectLabel(column) }}
89
+ </button>
90
+ <div
91
+ v-if="activeMultiselect === column.key"
92
+ class="datametria-sortable-table__multiselect-dropdown"
93
+ >
94
+ <label
95
+ v-for="option in getFilterOptions(column)"
96
+ :key="option.value"
97
+ class="datametria-sortable-table__multiselect-option"
98
+ >
99
+ <input
100
+ type="checkbox"
101
+ :value="option.value"
102
+ :checked="isMultiselectChecked(column.key, option.value)"
103
+ @change="toggleMultiselectOption(column.key, option.value)"
104
+ />
105
+ {{ option.label }}
106
+ </label>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- Text/Date Filter -->
111
+ <input
112
+ v-else
113
+ v-model="columnFilters[column.key]"
114
+ :type="column.filterType || getFilterInputType(column)"
115
+ :placeholder="`Filtrar ${column.label}...`"
116
+ :aria-label="`Filtrar por ${column.label}`"
117
+ class="datametria-sortable-table__filter-input"
118
+ @keydown.stop
119
+ />
120
+ </div>
121
+ </th>
122
+ </tr>
123
+ </thead>
124
+
125
+ <tbody class="datametria-sortable-table__tbody">
126
+ <tr
127
+ v-for="(row, index) in paginatedData"
128
+ :key="row.id || index"
129
+ class="datametria-sortable-table__tr"
130
+ :class="{ 'datametria-sortable-table__tr--selected': selectedRows.has(row.id || index) }"
131
+ role="row"
132
+ >
133
+ <!-- Selection Column -->
134
+ <td
135
+ v-if="selectable"
136
+ class="datametria-sortable-table__td datametria-sortable-table__td--checkbox"
137
+ role="cell"
138
+ >
139
+ <input
140
+ type="checkbox"
141
+ :checked="selectedRows.has(row.id || index)"
142
+ :aria-label="`Selecionar linha ${index + 1}`"
143
+ @change="toggleSelectRow(row.id || index)"
144
+ />
145
+ </td>
146
+
147
+ <!-- Data Columns -->
148
+ <td
149
+ v-for="column in columns"
150
+ :key="column.key"
151
+ class="datametria-sortable-table__td"
152
+ role="cell"
153
+ >
154
+ <slot :name="`cell-${column.key}`" :row="row" :value="row[column.key]">
155
+ {{ row[column.key] }}
156
+ </slot>
157
+ </td>
158
+ </tr>
159
+ </tbody>
160
+ </table>
161
+
162
+ <!-- Empty State -->
163
+ <div v-if="filteredData.length === 0" class="datametria-sortable-table__empty" role="status">
164
+ <slot name="empty">Nenhum dado encontrado</slot>
165
+ </div>
166
+ </div>
167
+
168
+ <!-- Pagination -->
169
+ <div
170
+ v-if="paginated && filteredData.length > 0"
171
+ class="datametria-sortable-table__pagination"
172
+ role="navigation"
173
+ aria-label="Paginação da tabela"
174
+ >
175
+ <div class="datametria-sortable-table__pagination-info" aria-live="polite">
176
+ Mostrando {{ startIndex + 1 }} - {{ endIndex }} de {{ filteredData.length }} registros
177
+ <span v-if="selectedRows.size > 0">({{ selectedRows.size }} selecionados)</span>
178
+ </div>
179
+
180
+ <div class="datametria-sortable-table__pagination-controls">
181
+ <select
182
+ v-model.number="currentPageSize"
183
+ class="datametria-sortable-table__page-size"
184
+ aria-label="Itens por página"
185
+ >
186
+ <option v-for="size in pageSizeOptions" :key="size" :value="size">
187
+ {{ size }} por página
188
+ </option>
189
+ </select>
190
+
191
+ <button
192
+ class="datametria-sortable-table__pagination-btn"
193
+ :disabled="currentPage === 1"
194
+ aria-label="Primeira página"
195
+ @click="currentPage = 1"
196
+ >
197
+ ««
198
+ </button>
199
+ <button
200
+ class="datametria-sortable-table__pagination-btn"
201
+ :disabled="currentPage === 1"
202
+ aria-label="Página anterior"
203
+ @click="currentPage--"
204
+ >
205
+
206
+ </button>
207
+
208
+ <span class="datametria-sortable-table__pagination-pages" aria-current="page">
209
+ Página {{ currentPage }} de {{ totalPages }}
210
+ </span>
211
+
212
+ <button
213
+ class="datametria-sortable-table__pagination-btn"
214
+ :disabled="currentPage === totalPages"
215
+ aria-label="Próxima página"
216
+ @click="currentPage++"
217
+ >
218
+
219
+ </button>
220
+ <button
221
+ class="datametria-sortable-table__pagination-btn"
222
+ :disabled="currentPage === totalPages"
223
+ aria-label="Última página"
224
+ @click="currentPage = totalPages"
225
+ >
226
+ »»
227
+ </button>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </template>
232
+
233
+ <script setup lang="ts">
234
+ import { ref, computed, watch } from 'vue'
235
+ import type { SortableTableProps } from '../types'
236
+
237
+ const props = withDefaults(defineProps<SortableTableProps>(), {
238
+ selectable: false,
239
+ searchable: true,
240
+ filterable: true,
241
+ paginated: true,
242
+ pageSize: 10,
243
+ pageSizeOptions: () => [5, 10, 25, 50, 100]
244
+ })
245
+
246
+ // Validação em desenvolvimento
247
+ if (process.env.NODE_ENV === 'development') {
248
+ if (!props.columns || props.columns.length === 0) {
249
+ console.warn('[DatametriaSortableTable] No columns provided')
250
+ }
251
+ if (!props.data) {
252
+ console.warn('[DatametriaSortableTable] No data provided')
253
+ }
254
+ }
255
+
256
+ const emit = defineEmits<{
257
+ 'selection-change': [selectedIds: (string | number)[]]
258
+ }>()
259
+
260
+ // Detect filter input type based on column data
261
+ const getFilterInputType = (column: any) => {
262
+ if (!props.data || props.data.length === 0) return 'text'
263
+
264
+ const sampleValue = props.data[0][column.key]
265
+
266
+ // Check if it's a date
267
+ if (sampleValue instanceof Date) return 'date'
268
+ if (typeof sampleValue === 'string' && !isNaN(Date.parse(sampleValue))) {
269
+ // Check if it looks like a date string (YYYY-MM-DD, DD/MM/YYYY, etc)
270
+ const datePattern = /^\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/
271
+ if (datePattern.test(sampleValue)) return 'date'
272
+ }
273
+
274
+ return 'text'
275
+ }
276
+
277
+ // Get filter options (auto-generate if 'auto')
278
+ const getFilterOptions = (column: any) => {
279
+ if (column.filterOptions === 'auto') {
280
+ const uniqueValues = new Set<string>()
281
+ props.data.forEach(row => {
282
+ const value = row[column.key]
283
+ if (value !== null && value !== undefined && value !== '') {
284
+ uniqueValues.add(String(value))
285
+ }
286
+ })
287
+ return Array.from(uniqueValues)
288
+ .sort()
289
+ .map(value => ({ value, label: value }))
290
+ }
291
+ return column.filterOptions || []
292
+ }
293
+
294
+ // Multi-select methods
295
+ const toggleMultiselect = (key: string) => {
296
+ activeMultiselect.value = activeMultiselect.value === key ? null : key
297
+ }
298
+
299
+ const getMultiselectLabel = (column: any) => {
300
+ const selected = columnFilters.value[column.key] as string[] || []
301
+ if (selected.length === 0) return 'Todos'
302
+ if (selected.length === 1) {
303
+ const options = getFilterOptions(column)
304
+ const option = options.find((o: any) => o.value === selected[0])
305
+ return option?.label || selected[0]
306
+ }
307
+ return `${selected.length} selecionados`
308
+ }
309
+
310
+ const isMultiselectChecked = (key: string, value: string) => {
311
+ const selected = columnFilters.value[key] as string[] || []
312
+ return selected.includes(value)
313
+ }
314
+
315
+ const toggleMultiselectOption = (key: string, value: string) => {
316
+ const selected = (columnFilters.value[key] as string[]) || []
317
+ const index = selected.indexOf(value)
318
+
319
+ if (index > -1) {
320
+ columnFilters.value[key] = selected.filter(v => v !== value)
321
+ } else {
322
+ columnFilters.value[key] = [...selected, value]
323
+ }
324
+ }
325
+
326
+ // Search
327
+ const searchQuery = ref('')
328
+
329
+ // Sorting
330
+ const sortKey = ref<string>('')
331
+ const sortOrder = ref<'asc' | 'desc'>('asc')
332
+
333
+ // Filtering
334
+ const columnFilters = ref<Record<string, string | string[]>>({})
335
+ const activeMultiselect = ref<string | null>(null)
336
+
337
+ // Selection
338
+ const selectedRows = ref<Set<string | number>>(new Set())
339
+
340
+ // Pagination
341
+ const currentPage = ref(1)
342
+ const currentPageSize = ref(props.pageSize)
343
+
344
+ // Computed: Filtered Data
345
+ const filteredData = computed(() => {
346
+ let result = [...props.data]
347
+
348
+ // Global search
349
+ if (searchQuery.value) {
350
+ const query = searchQuery.value.toLowerCase()
351
+ result = result.filter(row =>
352
+ Object.values(row).some(value =>
353
+ String(value).toLowerCase().includes(query)
354
+ )
355
+ )
356
+ }
357
+
358
+ // Column filters
359
+ Object.entries(columnFilters.value).forEach(([key, filterValue]) => {
360
+ if (filterValue) {
361
+ // Multi-select filter
362
+ if (Array.isArray(filterValue) && filterValue.length > 0) {
363
+ result = result.filter(row =>
364
+ filterValue.includes(String(row[key]))
365
+ )
366
+ }
367
+ // Text/Date/Select filter
368
+ else if (typeof filterValue === 'string') {
369
+ const query = filterValue.toLowerCase()
370
+ result = result.filter(row =>
371
+ String(row[key]).toLowerCase().includes(query)
372
+ )
373
+ }
374
+ }
375
+ })
376
+
377
+ // Sorting
378
+ if (sortKey.value) {
379
+ result.sort((a, b) => {
380
+ let aVal = a[sortKey.value]
381
+ let bVal = b[sortKey.value]
382
+
383
+ // Handle null/undefined
384
+ if (aVal == null && bVal == null) return 0
385
+ if (aVal == null) return 1
386
+ if (bVal == null) return -1
387
+
388
+ // Convert dates to timestamps for proper comparison
389
+ if (aVal instanceof Date) aVal = aVal.getTime()
390
+ if (bVal instanceof Date) bVal = bVal.getTime()
391
+
392
+ // Try to parse as date if string looks like a date
393
+ if (typeof aVal === 'string' && !isNaN(Date.parse(aVal))) {
394
+ const datePattern = /^\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/
395
+ if (datePattern.test(aVal)) {
396
+ aVal = new Date(aVal).getTime()
397
+ }
398
+ }
399
+ if (typeof bVal === 'string' && !isNaN(Date.parse(bVal))) {
400
+ const datePattern = /^\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/
401
+ if (datePattern.test(bVal)) {
402
+ bVal = new Date(bVal).getTime()
403
+ }
404
+ }
405
+
406
+ // Numeric comparison
407
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
408
+ return sortOrder.value === 'asc' ? aVal - bVal : bVal - aVal
409
+ }
410
+
411
+ // String comparison (case-insensitive)
412
+ const aStr = String(aVal).toLowerCase()
413
+ const bStr = String(bVal).toLowerCase()
414
+
415
+ if (aStr === bStr) return 0
416
+ const comparison = aStr > bStr ? 1 : -1
417
+ return sortOrder.value === 'asc' ? comparison : -comparison
418
+ })
419
+ }
420
+
421
+ return result
422
+ })
423
+
424
+ // Computed: Pagination
425
+ const totalPages = computed(() =>
426
+ Math.ceil(filteredData.value.length / currentPageSize.value)
427
+ )
428
+
429
+ const startIndex = computed(() =>
430
+ (currentPage.value - 1) * currentPageSize.value
431
+ )
432
+
433
+ const endIndex = computed(() =>
434
+ Math.min(startIndex.value + currentPageSize.value, filteredData.value.length)
435
+ )
436
+
437
+ const paginatedData = computed(() => {
438
+ if (!props.paginated) return filteredData.value
439
+ return filteredData.value.slice(startIndex.value, endIndex.value)
440
+ })
441
+
442
+ // Computed: Selection
443
+ const isAllSelected = computed(() =>
444
+ paginatedData.value.length > 0 &&
445
+ paginatedData.value.every(row => selectedRows.value.has(row.id || props.data.indexOf(row)))
446
+ )
447
+
448
+ const isIndeterminate = computed(() =>
449
+ selectedRows.value.size > 0 && !isAllSelected.value
450
+ )
451
+
452
+ // Methods: Sorting
453
+ const toggleSort = (key: string) => {
454
+ if (sortKey.value === key) {
455
+ sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
456
+ } else {
457
+ sortKey.value = key
458
+ sortOrder.value = 'asc'
459
+ }
460
+ }
461
+
462
+ // Methods: Selection
463
+ const toggleSelectRow = (id: string | number) => {
464
+ if (selectedRows.value.has(id)) {
465
+ selectedRows.value.delete(id)
466
+ } else {
467
+ selectedRows.value.add(id)
468
+ }
469
+ emit('selection-change', Array.from(selectedRows.value))
470
+ }
471
+
472
+ const toggleSelectAll = () => {
473
+ if (isAllSelected.value) {
474
+ paginatedData.value.forEach(row => {
475
+ selectedRows.value.delete(row.id || props.data.indexOf(row))
476
+ })
477
+ } else {
478
+ paginatedData.value.forEach(row => {
479
+ selectedRows.value.add(row.id || props.data.indexOf(row))
480
+ })
481
+ }
482
+ emit('selection-change', Array.from(selectedRows.value))
483
+ }
484
+
485
+ // Watch: Reset page on filter change
486
+ watch([searchQuery, columnFilters], () => {
487
+ currentPage.value = 1
488
+ }, { deep: true })
489
+
490
+ // Watch: Reset page if current page exceeds total pages
491
+ watch(totalPages, (newTotal) => {
492
+ if (currentPage.value > newTotal && newTotal > 0) {
493
+ currentPage.value = newTotal
494
+ }
495
+ })
496
+ </script>
497
+
498
+ <style scoped>
499
+ .datametria-sortable-table {
500
+ width: 100%;
501
+ background: var(--dm-bg-color, #ffffff);
502
+ border-radius: var(--dm-radius-md, 0.375rem);
503
+ box-shadow: var(--dm-shadow-sm, 0 1px 3px rgba(0, 0, 0, 0.1));
504
+ }
505
+
506
+ .datametria-sortable-table__search {
507
+ padding: var(--dm-spacing-4, 1rem);
508
+ border-bottom: 1px solid var(--dm-border-color, #e5e7eb);
509
+ }
510
+
511
+ .datametria-sortable-table__search-input {
512
+ width: 100%;
513
+ padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-4, 1rem);
514
+ border: 1px solid var(--dm-border-color, #e5e7eb);
515
+ border-radius: var(--dm-radius-md, 0.375rem);
516
+ font-size: var(--dm-font-size-sm, 0.875rem);
517
+ background: var(--dm-bg-color, #ffffff);
518
+ color: var(--dm-text-primary, #111827);
519
+ transition: all 0.2s ease;
520
+ }
521
+
522
+ .datametria-sortable-table__search-input:focus {
523
+ outline: none;
524
+ border-color: var(--dm-primary, #0072CE);
525
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
526
+ }
527
+
528
+ .datametria-sortable-table__wrapper {
529
+ overflow-x: auto;
530
+ }
531
+
532
+ .datametria-sortable-table__table {
533
+ width: 100%;
534
+ border-collapse: collapse;
535
+ }
536
+
537
+ .datametria-sortable-table__thead {
538
+ background: var(--dm-bg-secondary, #f9fafb);
539
+ border-bottom: 2px solid var(--dm-border-color, #e5e7eb);
540
+ }
541
+
542
+ .datametria-sortable-table__th {
543
+ padding: var(--dm-spacing-3, 0.75rem) var(--dm-spacing-4, 1rem);
544
+ text-align: left;
545
+ font-size: var(--dm-font-size-sm, 0.875rem);
546
+ font-weight: var(--dm-font-weight-semibold, 600);
547
+ color: var(--dm-text-primary, #374151);
548
+ position: relative;
549
+ }
550
+
551
+ .datametria-sortable-table__th--sortable {
552
+ cursor: pointer;
553
+ user-select: none;
554
+ }
555
+
556
+ .datametria-sortable-table__th--sortable:hover {
557
+ background: var(--dm-bg-hover, #f3f4f6);
558
+ }
559
+
560
+ .datametria-sortable-table__th--sortable:focus-visible {
561
+ outline: none;
562
+ box-shadow: inset 0 0 0 2px var(--dm-primary, #0072CE);
563
+ }
564
+
565
+ .datametria-sortable-table__th--checkbox {
566
+ width: 40px;
567
+ padding: var(--dm-spacing-3, 0.75rem) var(--dm-spacing-2, 0.5rem);
568
+ }
569
+
570
+ .datametria-sortable-table__th-content {
571
+ display: flex;
572
+ align-items: center;
573
+ gap: var(--dm-spacing-2, 0.5rem);
574
+ }
575
+
576
+ .datametria-sortable-table__sort-icon {
577
+ font-size: var(--dm-font-size-sm, 0.875rem);
578
+ color: var(--dm-primary, #0072CE);
579
+ }
580
+
581
+ .datametria-sortable-table__sort-icon--inactive {
582
+ color: var(--dm-text-secondary, #9ca3af);
583
+ opacity: 0.5;
584
+ }
585
+
586
+ .datametria-sortable-table__filter-input {
587
+ width: 100%;
588
+ margin-top: var(--dm-spacing-2, 0.5rem);
589
+ padding: var(--dm-spacing-2, 0.5rem);
590
+ border: 1px solid var(--dm-border-color, #e5e7eb);
591
+ border-radius: var(--dm-radius-md, 0.375rem);
592
+ font-size: var(--dm-font-size-sm, 0.875rem);
593
+ font-weight: 400;
594
+ background: var(--dm-bg-color, #ffffff);
595
+ color: var(--dm-text-primary, #111827);
596
+ transition: all 0.2s ease;
597
+ }
598
+
599
+ .datametria-sortable-table__filter-input:focus {
600
+ outline: none;
601
+ border-color: var(--dm-primary, #0072CE);
602
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
603
+ }
604
+
605
+ .datametria-sortable-table__multiselect {
606
+ position: relative;
607
+ margin-top: var(--dm-spacing-2, 0.5rem);
608
+ }
609
+
610
+ .datametria-sortable-table__multiselect-trigger {
611
+ width: 100%;
612
+ padding: var(--dm-spacing-2, 0.5rem);
613
+ border: 1px solid var(--dm-border-color, #e5e7eb);
614
+ border-radius: var(--dm-radius-md, 0.375rem);
615
+ font-size: var(--dm-font-size-sm, 0.875rem);
616
+ font-weight: 400;
617
+ text-align: left;
618
+ background: var(--dm-bg-color, #ffffff);
619
+ color: var(--dm-text-primary, #111827);
620
+ cursor: pointer;
621
+ transition: all 0.2s ease;
622
+ }
623
+
624
+ .datametria-sortable-table__multiselect-trigger:hover {
625
+ border-color: var(--dm-primary, #0072CE);
626
+ }
627
+
628
+ .datametria-sortable-table__multiselect-trigger:focus {
629
+ outline: none;
630
+ border-color: var(--dm-primary, #0072CE);
631
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
632
+ }
633
+
634
+ .datametria-sortable-table__multiselect-dropdown {
635
+ position: absolute;
636
+ top: 100%;
637
+ left: 0;
638
+ right: 0;
639
+ margin-top: var(--dm-spacing-1, 0.25rem);
640
+ max-height: 200px;
641
+ overflow-y: auto;
642
+ background: var(--dm-bg-color, #ffffff);
643
+ border: 1px solid var(--dm-border-color, #e5e7eb);
644
+ border-radius: var(--dm-radius-md, 0.375rem);
645
+ box-shadow: var(--dm-shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.1));
646
+ z-index: 10;
647
+ }
648
+
649
+ .datametria-sortable-table__multiselect-option {
650
+ display: flex;
651
+ align-items: center;
652
+ gap: var(--dm-spacing-2, 0.5rem);
653
+ padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-3, 0.75rem);
654
+ font-size: var(--dm-font-size-sm, 0.875rem);
655
+ color: var(--dm-text-primary, #111827);
656
+ cursor: pointer;
657
+ transition: background-color 0.2s ease;
658
+ }
659
+
660
+ .datametria-sortable-table__multiselect-option:hover {
661
+ background: var(--dm-bg-hover, #f3f4f6);
662
+ }
663
+
664
+ .datametria-sortable-table__multiselect-option input[type="checkbox"] {
665
+ margin: 0;
666
+ }
667
+
668
+ .datametria-sortable-table__tbody {
669
+ background: var(--dm-bg-color, #ffffff);
670
+ }
671
+
672
+ .datametria-sortable-table__tr {
673
+ border-bottom: 1px solid var(--dm-border-color, #e5e7eb);
674
+ transition: background-color 0.2s ease;
675
+ }
676
+
677
+ .datametria-sortable-table__tr:hover {
678
+ background: var(--dm-bg-hover, #f9fafb);
679
+ }
680
+
681
+ .datametria-sortable-table__tr--selected {
682
+ background: color-mix(in srgb, var(--dm-primary, #0072CE) 10%, transparent);
683
+ }
684
+
685
+ .datametria-sortable-table__td {
686
+ padding: var(--dm-spacing-3, 0.75rem) var(--dm-spacing-4, 1rem);
687
+ font-size: var(--dm-font-size-sm, 0.875rem);
688
+ color: var(--dm-text-primary, #111827);
689
+ }
690
+
691
+ .datametria-sortable-table__td--checkbox {
692
+ width: 40px;
693
+ padding: var(--dm-spacing-3, 0.75rem) var(--dm-spacing-2, 0.5rem);
694
+ }
695
+
696
+ .datametria-sortable-table__empty {
697
+ padding: var(--dm-spacing-4, 1rem);
698
+ text-align: center;
699
+ color: var(--dm-text-secondary, #6b7280);
700
+ font-size: var(--dm-font-size-sm, 0.875rem);
701
+ }
702
+
703
+ .datametria-sortable-table__pagination {
704
+ display: flex;
705
+ justify-content: space-between;
706
+ align-items: center;
707
+ padding: var(--dm-spacing-4, 1rem);
708
+ border-top: 1px solid var(--dm-border-color, #e5e7eb);
709
+ flex-wrap: wrap;
710
+ gap: var(--dm-spacing-4, 1rem);
711
+ }
712
+
713
+ .datametria-sortable-table__pagination-info {
714
+ font-size: var(--dm-font-size-sm, 0.875rem);
715
+ color: var(--dm-text-secondary, #4b5563);
716
+ }
717
+
718
+ .datametria-sortable-table__pagination-controls {
719
+ display: flex;
720
+ align-items: center;
721
+ gap: var(--dm-spacing-2, 0.5rem);
722
+ }
723
+
724
+ .datametria-sortable-table__page-size {
725
+ padding: var(--dm-spacing-2, 0.5rem);
726
+ border: 1px solid var(--dm-border-color, #e5e7eb);
727
+ border-radius: var(--dm-radius-md, 0.375rem);
728
+ font-size: var(--dm-font-size-sm, 0.875rem);
729
+ cursor: pointer;
730
+ transition: all 0.2s ease;
731
+ background: var(--dm-bg-color, #ffffff);
732
+ color: var(--dm-text-primary, #111827);
733
+ }
734
+
735
+ .datametria-sortable-table__page-size:focus {
736
+ outline: none;
737
+ border-color: var(--dm-primary, #0072CE);
738
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
739
+ }
740
+
741
+ .datametria-sortable-table__pagination-btn {
742
+ padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-3, 0.75rem);
743
+ border: 1px solid var(--dm-border-color, #e5e7eb);
744
+ border-radius: var(--dm-radius-md, 0.375rem);
745
+ background: var(--dm-bg-color, #ffffff);
746
+ color: var(--dm-text-primary, #111827);
747
+ font-size: var(--dm-font-size-sm, 0.875rem);
748
+ cursor: pointer;
749
+ transition: all 0.2s ease;
750
+ }
751
+
752
+ .datametria-sortable-table__pagination-btn:hover:not(:disabled) {
753
+ background: var(--dm-bg-hover, #f3f4f6);
754
+ border-color: var(--dm-primary, #0072CE);
755
+ }
756
+
757
+ .datametria-sortable-table__pagination-btn:focus-visible {
758
+ outline: none;
759
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
760
+ }
761
+
762
+ .datametria-sortable-table__pagination-btn:disabled {
763
+ opacity: 0.5;
764
+ cursor: not-allowed;
765
+ }
766
+
767
+ .datametria-sortable-table__pagination-pages {
768
+ padding: 0 var(--dm-spacing-2, 0.5rem);
769
+ font-size: var(--dm-font-size-sm, 0.875rem);
770
+ color: var(--dm-text-secondary, #4b5563);
771
+ }
772
+
773
+ input[type="checkbox"] {
774
+ width: 16px;
775
+ height: 16px;
776
+ cursor: pointer;
777
+ accent-color: var(--dm-primary, #0072CE);
778
+ }
779
+
780
+ input[type="checkbox"]:focus-visible {
781
+ outline: 2px solid var(--dm-primary, #0072CE);
782
+ outline-offset: 2px;
783
+ }
784
+
785
+ /* Mobile Responsiveness */
786
+ @media (max-width: 640px) {
787
+ .datametria-sortable-table__pagination {
788
+ flex-direction: column;
789
+ align-items: stretch;
790
+ }
791
+
792
+ .datametria-sortable-table__pagination-controls {
793
+ justify-content: center;
794
+ }
795
+
796
+ .datametria-sortable-table__th,
797
+ .datametria-sortable-table__td {
798
+ padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-3, 0.75rem);
799
+ font-size: calc(var(--dm-font-size-sm, 0.875rem) * 0.9);
800
+ }
801
+ }
802
+
803
+ /* Reduced Motion */
804
+ @media (prefers-reduced-motion: reduce) {
805
+ .datametria-sortable-table__tr,
806
+ .datametria-sortable-table__pagination-btn,
807
+ .datametria-sortable-table__search-input,
808
+ .datametria-sortable-table__filter-input {
809
+ transition: none;
810
+ }
811
+ }
812
+
813
+ /* Dark Mode Support - Hybrid Approach */
814
+
815
+ /* Fallback automático (sem JS) */
816
+ @media (prefers-color-scheme: dark) {
817
+ .datametria-sortable-table {
818
+ background: var(--dm-bg-color-dark, #1e1e1e);
819
+ color: var(--dm-text-primary-dark, #e0e0e0);
820
+ border-color: var(--dm-border-color-dark, #404040);
794
821
  }
795
822
  }
796
823
 
797
- /* Reduced Motion */
798
- @media (prefers-reduced-motion: reduce) {
799
- .datametria-sortable-table__tr,
800
- .datametria-sortable-table__pagination-btn,
801
- .datametria-sortable-table__search-input,
802
- .datametria-sortable-table__filter-input {
803
- transition: none;
804
- }
824
+ /* Controle manual via useTheme() */
825
+ [data-theme="dark"] .datametria-sortable-table {
826
+ background: var(--dm-bg-color-dark, #1e1e1e);
827
+ color: var(--dm-text-primary-dark, #e0e0e0);
828
+ border-color: var(--dm-border-color-dark, #404040);
805
829
  }
806
- </style>
830
+ </style>