@m3ui-vue/m3ui-vue 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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/dist/components/MAlert.vue.d.ts +27 -0
  4. package/dist/components/MAppBar.vue.d.ts +24 -0
  5. package/dist/components/MAvatar.vue.d.ts +9 -0
  6. package/dist/components/MBadge.vue.d.ts +22 -0
  7. package/dist/components/MBottomSheet.vue.d.ts +26 -0
  8. package/dist/components/MBreadcrumbs.vue.d.ts +19 -0
  9. package/dist/components/MButton.vue.d.ts +32 -0
  10. package/dist/components/MCalendar.vue.d.ts +23 -0
  11. package/dist/components/MCard.vue.d.ts +28 -0
  12. package/dist/components/MChart.vue.d.ts +13 -0
  13. package/dist/components/MCheckbox.vue.d.ts +26 -0
  14. package/dist/components/MChip.vue.d.ts +33 -0
  15. package/dist/components/MCodeEditor.vue.d.ts +35 -0
  16. package/dist/components/MColorPicker.vue.d.ts +18 -0
  17. package/dist/components/MCommandPalette.vue.d.ts +29 -0
  18. package/dist/components/MConfirmDialog.vue.d.ts +23 -0
  19. package/dist/components/MContainer.vue.d.ts +24 -0
  20. package/dist/components/MContextMenu.vue.d.ts +35 -0
  21. package/dist/components/MDataTable.vue.d.ts +83 -0
  22. package/dist/components/MDatePicker.vue.d.ts +21 -0
  23. package/dist/components/MDateRangePicker.vue.d.ts +24 -0
  24. package/dist/components/MDialog.vue.d.ts +30 -0
  25. package/dist/components/MDivider.vue.d.ts +11 -0
  26. package/dist/components/MDragDropList.vue.d.ts +40 -0
  27. package/dist/components/MEmptyState.vue.d.ts +21 -0
  28. package/dist/components/MExpansionPanel.vue.d.ts +28 -0
  29. package/dist/components/MFab.vue.d.ts +28 -0
  30. package/dist/components/MFileUpload.vue.d.ts +25 -0
  31. package/dist/components/MGrid.vue.d.ts +26 -0
  32. package/dist/components/MHotkeys.vue.d.ts +16 -0
  33. package/dist/components/MIcon.vue.d.ts +9 -0
  34. package/dist/components/MIconButton.vue.d.ts +14 -0
  35. package/dist/components/MInfiniteScroll.vue.d.ts +34 -0
  36. package/dist/components/MJsonEditor.vue.d.ts +17 -0
  37. package/dist/components/MJsonViewer.vue.d.ts +14 -0
  38. package/dist/components/MKanban.vue.d.ts +53 -0
  39. package/dist/components/MLoadingOverlay.vue.d.ts +28 -0
  40. package/dist/components/MMarkdown.vue.d.ts +11 -0
  41. package/dist/components/MMasonry.vue.d.ts +23 -0
  42. package/dist/components/MMenu.vue.d.ts +27 -0
  43. package/dist/components/MMenuItem.vue.d.ts +16 -0
  44. package/dist/components/MMultiSelect.vue.d.ts +34 -0
  45. package/dist/components/MNavigationBar.vue.d.ts +18 -0
  46. package/dist/components/MNavigationDrawer.vue.d.ts +41 -0
  47. package/dist/components/MNavigationRail.vue.d.ts +32 -0
  48. package/dist/components/MPagination.vue.d.ts +12 -0
  49. package/dist/components/MProgressBar.vue.d.ts +13 -0
  50. package/dist/components/MRadio.vue.d.ts +17 -0
  51. package/dist/components/MRadioGroup.vue.d.ts +24 -0
  52. package/dist/components/MRating.vue.d.ts +23 -0
  53. package/dist/components/MResult.vue.d.ts +20 -0
  54. package/dist/components/MRichTextEditor.vue.d.ts +17 -0
  55. package/dist/components/MScheduler.vue.d.ts +35 -0
  56. package/dist/components/MSegmentedButton.vue.d.ts +24 -0
  57. package/dist/components/MSelect.vue.d.ts +29 -0
  58. package/dist/components/MSideSheet.vue.d.ts +28 -0
  59. package/dist/components/MSkeleton.vue.d.ts +14 -0
  60. package/dist/components/MSlider.vue.d.ts +24 -0
  61. package/dist/components/MSnackbar.vue.d.ts +3 -0
  62. package/dist/components/MSpinner.vue.d.ts +10 -0
  63. package/dist/components/MSplitter.vue.d.ts +26 -0
  64. package/dist/components/MSpotlightSearch.vue.d.ts +34 -0
  65. package/dist/components/MStack.vue.d.ts +30 -0
  66. package/dist/components/MStatCard.vue.d.ts +24 -0
  67. package/dist/components/MStepper.vue.d.ts +33 -0
  68. package/dist/components/MSwitch.vue.d.ts +14 -0
  69. package/dist/components/MTable.vue.d.ts +73 -0
  70. package/dist/components/MTabs.vue.d.ts +20 -0
  71. package/dist/components/MTerminal.vue.d.ts +25 -0
  72. package/dist/components/MTextField.vue.d.ts +41 -0
  73. package/dist/components/MTimePicker.vue.d.ts +20 -0
  74. package/dist/components/MTimeline.vue.d.ts +31 -0
  75. package/dist/components/MTooltip.vue.d.ts +21 -0
  76. package/dist/components/MTopAppBar.vue.d.ts +29 -0
  77. package/dist/components/MTour.vue.d.ts +19 -0
  78. package/dist/components/MTransferList.vue.d.ts +23 -0
  79. package/dist/components/MTree.vue.d.ts +68 -0
  80. package/dist/components/MTreeTable.vue.d.ts +57 -0
  81. package/dist/components/MVirtualTable.vue.d.ts +40 -0
  82. package/dist/components/_MContextMenuPanel.vue.d.ts +13 -0
  83. package/dist/components/_MTreeNode.vue.d.ts +26 -0
  84. package/dist/composables/useColorPalette.d.ts +11 -0
  85. package/dist/composables/useFieldBg.d.ts +13 -0
  86. package/dist/composables/useTheme.d.ts +5 -0
  87. package/dist/composables/useToast.d.ts +59 -0
  88. package/dist/index.d.ts +112 -0
  89. package/dist/m3ui.css +2 -0
  90. package/dist/m3ui.js +7432 -0
  91. package/dist/m3ui.js.map +1 -0
  92. package/dist/plugin.d.ts +9 -0
  93. package/dist/styles/palettes.css +1253 -0
  94. package/dist/styles/theme.css +249 -0
  95. package/package.json +166 -0
  96. package/src/components/MAlert.vue +69 -0
  97. package/src/components/MAppBar.vue +40 -0
  98. package/src/components/MAvatar.vue +21 -0
  99. package/src/components/MBadge.vue +46 -0
  100. package/src/components/MBottomSheet.vue +113 -0
  101. package/src/components/MBreadcrumbs.vue +52 -0
  102. package/src/components/MButton.vue +111 -0
  103. package/src/components/MCalendar.vue +173 -0
  104. package/src/components/MCard.vue +56 -0
  105. package/src/components/MChart.vue +158 -0
  106. package/src/components/MCheckbox.vue +48 -0
  107. package/src/components/MChip.vue +87 -0
  108. package/src/components/MCodeEditor.vue +179 -0
  109. package/src/components/MColorPicker.vue +305 -0
  110. package/src/components/MCommandPalette.vue +213 -0
  111. package/src/components/MConfirmDialog.vue +43 -0
  112. package/src/components/MContainer.vue +36 -0
  113. package/src/components/MContextMenu.vue +66 -0
  114. package/src/components/MDataTable.vue +376 -0
  115. package/src/components/MDatePicker.vue +253 -0
  116. package/src/components/MDateRangePicker.vue +265 -0
  117. package/src/components/MDialog.vue +90 -0
  118. package/src/components/MDivider.vue +26 -0
  119. package/src/components/MDragDropList.vue +111 -0
  120. package/src/components/MEmptyState.vue +40 -0
  121. package/src/components/MExpansionPanel.vue +112 -0
  122. package/src/components/MFab.vue +220 -0
  123. package/src/components/MFileUpload.vue +206 -0
  124. package/src/components/MGrid.vue +99 -0
  125. package/src/components/MHotkeys.vue +122 -0
  126. package/src/components/MIcon.vue +9 -0
  127. package/src/components/MIconButton.vue +49 -0
  128. package/src/components/MInfiniteScroll.vue +68 -0
  129. package/src/components/MJsonEditor.vue +118 -0
  130. package/src/components/MJsonViewer.vue +106 -0
  131. package/src/components/MKanban.vue +147 -0
  132. package/src/components/MLoadingOverlay.vue +52 -0
  133. package/src/components/MMarkdown.vue +123 -0
  134. package/src/components/MMasonry.vue +87 -0
  135. package/src/components/MMenu.vue +113 -0
  136. package/src/components/MMenuItem.vue +15 -0
  137. package/src/components/MMultiSelect.vue +306 -0
  138. package/src/components/MNavigationBar.vue +62 -0
  139. package/src/components/MNavigationDrawer.vue +157 -0
  140. package/src/components/MNavigationRail.vue +80 -0
  141. package/src/components/MPagination.vue +37 -0
  142. package/src/components/MProgressBar.vue +200 -0
  143. package/src/components/MRadio.vue +89 -0
  144. package/src/components/MRadioGroup.vue +41 -0
  145. package/src/components/MRating.vue +108 -0
  146. package/src/components/MResult.vue +62 -0
  147. package/src/components/MRichTextEditor.vue +199 -0
  148. package/src/components/MScheduler.vue +225 -0
  149. package/src/components/MSegmentedButton.vue +75 -0
  150. package/src/components/MSelect.vue +259 -0
  151. package/src/components/MSideSheet.vue +112 -0
  152. package/src/components/MSkeleton.vue +60 -0
  153. package/src/components/MSlider.vue +188 -0
  154. package/src/components/MSnackbar.vue +244 -0
  155. package/src/components/MSpinner.vue +122 -0
  156. package/src/components/MSplitter.vue +97 -0
  157. package/src/components/MSpotlightSearch.vue +244 -0
  158. package/src/components/MStack.vue +67 -0
  159. package/src/components/MStatCard.vue +56 -0
  160. package/src/components/MStepper.vue +161 -0
  161. package/src/components/MSwitch.vue +63 -0
  162. package/src/components/MTable.vue +404 -0
  163. package/src/components/MTabs.vue +97 -0
  164. package/src/components/MTerminal.vue +146 -0
  165. package/src/components/MTextField.vue +180 -0
  166. package/src/components/MTimePicker.vue +227 -0
  167. package/src/components/MTimeline.vue +117 -0
  168. package/src/components/MTooltip.vue +82 -0
  169. package/src/components/MTopAppBar.vue +62 -0
  170. package/src/components/MTour.vue +226 -0
  171. package/src/components/MTransferList.vue +181 -0
  172. package/src/components/MTree.vue +164 -0
  173. package/src/components/MTreeTable.vue +159 -0
  174. package/src/components/MVirtualTable.vue +155 -0
  175. package/src/components/_MContextMenuPanel.vue +129 -0
  176. package/src/components/_MTreeNode.vue +171 -0
  177. package/src/composables/useColorPalette.ts +60 -0
  178. package/src/composables/useFieldBg.ts +91 -0
  179. package/src/composables/useTheme.ts +55 -0
  180. package/src/composables/useToast.ts +51 -0
  181. package/src/env.d.ts +1 -0
  182. package/src/index.ts +119 -0
  183. package/src/plugin.ts +18 -0
  184. package/src/styles/palettes.css +1253 -0
  185. package/src/styles/theme.css +249 -0
@@ -0,0 +1,43 @@
1
+ <script setup lang="ts">
2
+ import MDialog from './MDialog.vue'
3
+ import MButton from './MButton.vue'
4
+
5
+ withDefaults(
6
+ defineProps<{
7
+ modelValue: boolean
8
+ title: string
9
+ message: string
10
+ confirmLabel?: string
11
+ cancelLabel?: string
12
+ danger?: boolean
13
+ loading?: boolean
14
+ }>(),
15
+ {
16
+ confirmLabel: 'Confirmar',
17
+ cancelLabel: 'Cancelar',
18
+ danger: false,
19
+ loading: false,
20
+ },
21
+ )
22
+
23
+ const emit = defineEmits<{ 'update:modelValue': [boolean]; confirm: [] }>()
24
+ </script>
25
+
26
+ <template>
27
+ <MDialog
28
+ :model-value="modelValue"
29
+ :title="title"
30
+ max-width="max-w-sm"
31
+ @update:model-value="emit('update:modelValue', $event)"
32
+ >
33
+ <p class="text-body-medium text-on-surface-variant">{{ message }}</p>
34
+ <template #actions>
35
+ <MButton variant="text" :disabled="loading" @click="emit('update:modelValue', false)">
36
+ {{ cancelLabel }}
37
+ </MButton>
38
+ <MButton :color="danger ? 'error' : 'primary'" :loading="loading" @click="emit('confirm')">
39
+ {{ confirmLabel }}
40
+ </MButton>
41
+ </template>
42
+ </MDialog>
43
+ </template>
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'
7
+ fluid?: boolean
8
+ centered?: boolean
9
+ padding?: boolean
10
+ }>(),
11
+ { maxWidth: 'lg', fluid: false, centered: true, padding: true },
12
+ )
13
+
14
+ const maxWidthClasses: Record<string, string> = {
15
+ xs: 'max-w-screen-xs',
16
+ sm: 'max-w-screen-sm',
17
+ md: 'max-w-screen-md',
18
+ lg: 'max-w-screen-lg',
19
+ xl: 'max-w-screen-xl',
20
+ '2xl': 'max-w-screen-2xl',
21
+ full: 'max-w-full',
22
+ }
23
+
24
+ const classes = computed(() => [
25
+ 'w-full',
26
+ props.fluid ? 'max-w-full' : maxWidthClasses[props.maxWidth],
27
+ props.centered && 'mx-auto',
28
+ props.padding && 'px-4 sm:px-6 lg:px-8',
29
+ ])
30
+ </script>
31
+
32
+ <template>
33
+ <div :class="classes">
34
+ <slot />
35
+ </div>
36
+ </template>
@@ -0,0 +1,66 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import MContextMenuPanel from './_MContextMenuPanel.vue'
4
+
5
+ export interface ContextMenuItem {
6
+ label?: string
7
+ icon?: string
8
+ shortcut?: string
9
+ disabled?: boolean
10
+ danger?: boolean
11
+ divider?: boolean
12
+ children?: ContextMenuItem[]
13
+ onClick?: () => void
14
+ }
15
+
16
+ defineProps<{ items: ContextMenuItem[] }>()
17
+
18
+ const visible = ref(false)
19
+ const position = ref({ x: 0, y: 0 })
20
+
21
+ function show(e: MouseEvent) {
22
+ e.preventDefault()
23
+ e.stopPropagation()
24
+ showAt(e.clientX, e.clientY)
25
+ }
26
+
27
+ function showAt(x: number, y: number) {
28
+ position.value = { x, y }
29
+ visible.value = true
30
+ }
31
+
32
+ function hide() {
33
+ visible.value = false
34
+ }
35
+
36
+ defineExpose({ show, showAt, hide })
37
+ </script>
38
+
39
+ <template>
40
+ <slot :show="show" />
41
+
42
+ <Teleport to="body">
43
+ <Transition
44
+ enter-active-class="transition-opacity duration-100"
45
+ enter-from-class="opacity-0"
46
+ enter-to-class="opacity-100"
47
+ leave-active-class="transition-opacity duration-75"
48
+ leave-from-class="opacity-100"
49
+ leave-to-class="opacity-0"
50
+ >
51
+ <div
52
+ v-if="visible"
53
+ class="fixed inset-0 z-[200]"
54
+ @mousedown.self="hide"
55
+ @contextmenu.prevent
56
+ >
57
+ <MContextMenuPanel
58
+ :items="items"
59
+ :x="position.x"
60
+ :y="position.y"
61
+ @close="hide"
62
+ />
63
+ </div>
64
+ </Transition>
65
+ </Teleport>
66
+ </template>
@@ -0,0 +1,376 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, useSlots, watch } from 'vue'
3
+ import MCheckbox from './MCheckbox.vue'
4
+ import MIcon from './MIcon.vue'
5
+ import MIconButton from './MIconButton.vue'
6
+ import MPagination from './MPagination.vue'
7
+ import MChip from './MChip.vue'
8
+
9
+ export interface DataTableColumn {
10
+ key: string
11
+ label: string
12
+ sortable?: boolean
13
+ filterable?: boolean
14
+ resizable?: boolean
15
+ width?: string
16
+ minWidth?: string
17
+ align?: 'left' | 'center' | 'right'
18
+ pinned?: 'left' | 'right'
19
+ hidden?: boolean
20
+ }
21
+
22
+ export interface DataTableGroup {
23
+ key: string
24
+ label: string
25
+ }
26
+
27
+ const SKEL = [65, 80, 50, 75, 90, 55, 70, 85, 60, 78]
28
+
29
+ const props = withDefaults(defineProps<{
30
+ columns: DataTableColumn[]
31
+ rows: Record<string, any>[]
32
+ loading?: boolean
33
+ emptyText?: string
34
+ rowKey?: string
35
+ selectable?: boolean
36
+ modelValue?: Record<string, any>[]
37
+ perPage?: number
38
+ searchable?: boolean
39
+ expandable?: boolean
40
+ striped?: boolean
41
+ dense?: boolean
42
+ stickyHeader?: boolean
43
+ groupBy?: string
44
+ columnToggle?: boolean
45
+ exportable?: boolean
46
+ }>(), {
47
+ loading: false,
48
+ emptyText: 'Sin resultados',
49
+ rowKey: 'id',
50
+ selectable: false,
51
+ modelValue: () => [],
52
+ perPage: 10,
53
+ searchable: true,
54
+ expandable: false,
55
+ striped: false,
56
+ dense: false,
57
+ stickyHeader: false,
58
+ columnToggle: false,
59
+ exportable: false,
60
+ })
61
+
62
+ const emit = defineEmits<{
63
+ 'update:modelValue': [Record<string, any>[]]
64
+ rowClick: [Record<string, any>]
65
+ }>()
66
+
67
+ const slots = useSlots()
68
+ const hasActions = computed(() => !!slots['row-actions'])
69
+ const hasExpand = computed(() => props.expandable && !!slots['row-expand'])
70
+
71
+ const search = ref('')
72
+ const sortKey = ref('')
73
+ const sortDir = ref<'asc' | 'desc' | ''>('')
74
+ const internalPage = ref(1)
75
+ const expanded = ref<Set<any>>(new Set())
76
+ const hiddenCols = ref<Set<string>>(new Set())
77
+ const colWidths = ref<Record<string, number>>({})
78
+ const showColMenu = ref(false)
79
+
80
+ const visibleColumns = computed(() =>
81
+ props.columns.filter(c => !c.hidden && !hiddenCols.value.has(c.key))
82
+ )
83
+
84
+ function toggleSort(key: string) {
85
+ if (sortKey.value !== key) { sortKey.value = key; sortDir.value = 'asc' }
86
+ else if (sortDir.value === 'asc') sortDir.value = 'desc'
87
+ else { sortKey.value = ''; sortDir.value = '' }
88
+ }
89
+
90
+ const processedRows = computed(() => {
91
+ let result = props.rows
92
+ if (search.value.trim()) {
93
+ const q = search.value.toLowerCase()
94
+ result = result.filter(row =>
95
+ visibleColumns.value.some(col => {
96
+ const val = row[col.key]
97
+ return val != null && String(val).toLowerCase().includes(q)
98
+ })
99
+ )
100
+ }
101
+ if (sortKey.value && sortDir.value) {
102
+ const key = sortKey.value, dir = sortDir.value
103
+ result = [...result].sort((a, b) => {
104
+ const cmp = String(a[key] ?? '').localeCompare(String(b[key] ?? ''), undefined, { numeric: true, sensitivity: 'base' })
105
+ return dir === 'asc' ? cmp : -cmp
106
+ })
107
+ }
108
+ return result
109
+ })
110
+
111
+ const groupedRows = computed(() => {
112
+ if (!props.groupBy) return null
113
+ const map = new Map<string, Record<string, any>[]>()
114
+ for (const row of processedRows.value) {
115
+ const key = String(row[props.groupBy] ?? 'Sin grupo')
116
+ if (!map.has(key)) map.set(key, [])
117
+ map.get(key)!.push(row)
118
+ }
119
+ return map
120
+ })
121
+
122
+ const totalCount = computed(() => processedRows.value.length)
123
+ const visibleRows = computed(() => {
124
+ const start = (internalPage.value - 1) * props.perPage
125
+ return processedRows.value.slice(start, start + props.perPage)
126
+ })
127
+
128
+ watch([search, sortKey, sortDir], () => { internalPage.value = 1 })
129
+
130
+ const selected = computed({
131
+ get: () => props.modelValue ?? [],
132
+ set: (val) => emit('update:modelValue', val),
133
+ })
134
+ function rowId(row: Record<string, any>) { return row[props.rowKey] }
135
+ function isSelected(row: Record<string, any>) { return selected.value.some(r => rowId(r) === rowId(row)) }
136
+ function toggleRow(row: Record<string, any>) {
137
+ if (isSelected(row)) selected.value = selected.value.filter(r => rowId(r) !== rowId(row))
138
+ else selected.value = [...selected.value, row]
139
+ }
140
+ const allOnPageSelected = computed(() => visibleRows.value.length > 0 && visibleRows.value.every(r => isSelected(r)))
141
+ const someOnPageSelected = computed(() => visibleRows.value.some(r => isSelected(r)) && !allOnPageSelected.value)
142
+ function toggleAll() {
143
+ if (allOnPageSelected.value) selected.value = selected.value.filter(r => !visibleRows.value.some(v => rowId(v) === rowId(r)))
144
+ else selected.value = [...selected.value, ...visibleRows.value.filter(r => !isSelected(r))]
145
+ }
146
+
147
+ function toggleExpand(row: Record<string, any>) {
148
+ const id = rowId(row)
149
+ const next = new Set(expanded.value)
150
+ next.has(id) ? next.delete(id) : next.add(id)
151
+ expanded.value = next
152
+ }
153
+ function isExpanded(row: Record<string, any>) { return expanded.value.has(rowId(row)) }
154
+
155
+ const extraCols = computed(() =>
156
+ (props.selectable ? 1 : 0) + (hasActions.value ? 1 : 0) + (hasExpand.value ? 1 : 0)
157
+ )
158
+ function alignClass(a?: string) { return a === 'center' ? 'text-center' : a === 'right' ? 'text-right' : 'text-left' }
159
+ function skelWidth(ri: number, ci: number) { return `${SKEL[(ri * 3 + ci) % SKEL.length]}%` }
160
+
161
+ let resizeCol: string | null = null
162
+ let resizeStart = 0
163
+ let resizeInitial = 0
164
+
165
+ function onResizeDown(e: PointerEvent, col: DataTableColumn) {
166
+ e.preventDefault()
167
+ resizeCol = col.key
168
+ resizeStart = e.clientX
169
+ resizeInitial = colWidths.value[col.key] ?? 150
170
+ window.addEventListener('pointermove', onResizeMove)
171
+ window.addEventListener('pointerup', onResizeUp)
172
+ }
173
+ function onResizeMove(e: PointerEvent) {
174
+ if (!resizeCol) return
175
+ const w = Math.max(60, resizeInitial + e.clientX - resizeStart)
176
+ colWidths.value = { ...colWidths.value, [resizeCol]: w }
177
+ }
178
+ function onResizeUp() {
179
+ resizeCol = null
180
+ window.removeEventListener('pointermove', onResizeMove)
181
+ window.removeEventListener('pointerup', onResizeUp)
182
+ }
183
+
184
+ function exportCSV() {
185
+ const cols = visibleColumns.value
186
+ const header = cols.map(c => c.label).join(',')
187
+ const body = processedRows.value.map(row =>
188
+ cols.map(c => {
189
+ const v = String(row[c.key] ?? '')
190
+ return v.includes(',') || v.includes('"') ? `"${v.replace(/"/g, '""')}"` : v
191
+ }).join(',')
192
+ ).join('\n')
193
+ const blob = new Blob([`${header}\n${body}`], { type: 'text/csv' })
194
+ const url = URL.createObjectURL(blob)
195
+ const a = document.createElement('a')
196
+ a.href = url; a.download = 'data.csv'; a.click()
197
+ URL.revokeObjectURL(url)
198
+ }
199
+
200
+ function colStyle(col: DataTableColumn) {
201
+ const w = colWidths.value[col.key]
202
+ if (w) return { width: `${w}px`, minWidth: col.minWidth }
203
+ return { width: col.width, minWidth: col.minWidth }
204
+ }
205
+ </script>
206
+
207
+ <template>
208
+ <div class="flex flex-col overflow-hidden rounded-sm border border-outline-variant">
209
+
210
+ <!-- Toolbar -->
211
+ <div
212
+ v-if="searchable || columnToggle || exportable || $slots.toolbar"
213
+ class="flex flex-wrap items-center gap-3 border-b border-outline-variant bg-surface-container-lowest px-4 py-2.5"
214
+ >
215
+ <div v-if="searchable" class="flex min-w-48 flex-1 items-center gap-2 rounded-full border border-outline-variant bg-surface-container px-3 py-1.5 transition-[border-color,box-shadow] duration-150 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30">
216
+ <MIcon name="search" :size="16" class="shrink-0 text-on-surface-variant" />
217
+ <input v-model="search" type="text" placeholder="Buscar..." class="w-full bg-transparent text-body-medium text-on-surface outline-none placeholder:text-on-surface-variant" />
218
+ <button v-if="search" class="text-on-surface-variant transition-colors hover:text-on-surface" @click="search = ''">
219
+ <MIcon name="close" :size="14" />
220
+ </button>
221
+ </div>
222
+
223
+ <slot name="toolbar" />
224
+
225
+ <Transition enter-active-class="transition-[opacity,transform] duration-150" enter-from-class="opacity-0 scale-90" leave-active-class="transition-[opacity,transform] duration-100" leave-to-class="opacity-0 scale-90">
226
+ <span v-if="selectable && selected.length > 0" class="rounded-full bg-primary/12 px-3 py-1 text-label-small font-medium text-primary">
227
+ {{ selected.length }} seleccionado{{ selected.length !== 1 ? 's' : '' }}
228
+ </span>
229
+ </Transition>
230
+
231
+ <!-- Column toggle -->
232
+ <div v-if="columnToggle" class="relative">
233
+ <MIconButton icon="view_column" label="Columnas" :size="36" @click="showColMenu = !showColMenu" />
234
+ <div v-if="showColMenu" class="absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg bg-surface-container py-2 shadow-elevation-3">
235
+ <label v-for="col in columns" :key="col.key" class="flex cursor-pointer items-center gap-2 px-3 py-1.5 hover:bg-on-surface/4">
236
+ <MCheckbox
237
+ :model-value="!hiddenCols.has(col.key)"
238
+ @update:model-value="hiddenCols.has(col.key) ? hiddenCols.delete(col.key) : hiddenCols.add(col.key)"
239
+ />
240
+ <span class="text-body-small text-on-surface">{{ col.label }}</span>
241
+ </label>
242
+ </div>
243
+ </div>
244
+
245
+ <MIconButton v-if="exportable" icon="download" label="Exportar CSV" :size="36" @click="exportCSV" />
246
+ </div>
247
+
248
+ <!-- Table -->
249
+ <div class="overflow-x-auto">
250
+ <table class="w-full border-collapse">
251
+ <thead :class="stickyHeader ? 'sticky top-0 z-[1]' : ''">
252
+ <tr class="bg-surface-container-high">
253
+ <th v-if="hasExpand" class="w-10 px-2" :class="dense ? 'py-2' : 'py-3'" />
254
+ <th v-if="selectable" class="w-12 px-4" :class="dense ? 'py-2' : 'py-3'">
255
+ <MCheckbox :model-value="allOnPageSelected" :indeterminate="someOnPageSelected" @update:model-value="toggleAll" />
256
+ </th>
257
+ <th
258
+ v-for="col in visibleColumns"
259
+ :key="col.key"
260
+ :style="colStyle(col)"
261
+ :class="[
262
+ 'relative whitespace-nowrap text-label-medium font-medium text-on-surface-variant',
263
+ dense ? 'px-3 py-2' : 'px-4 py-3',
264
+ alignClass(col.align),
265
+ col.sortable ? 'cursor-pointer select-none hover:text-on-surface transition-colors duration-100' : '',
266
+ ]"
267
+ @click="col.sortable ? toggleSort(col.key) : undefined"
268
+ >
269
+ <span class="inline-flex items-center gap-1">
270
+ {{ col.label }}
271
+ <span v-if="col.sortable" class="inline-flex">
272
+ <MIcon v-if="sortKey === col.key && sortDir === 'asc'" name="arrow_upward" :size="14" class="text-primary" />
273
+ <MIcon v-else-if="sortKey === col.key && sortDir === 'desc'" name="arrow_downward" :size="14" class="text-primary" />
274
+ <MIcon v-else name="unfold_more" :size="14" class="opacity-30" />
275
+ </span>
276
+ </span>
277
+ <!-- Resize handle -->
278
+ <div
279
+ v-if="col.resizable"
280
+ class="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/30"
281
+ @pointerdown="onResizeDown($event, col)"
282
+ />
283
+ </th>
284
+ <th v-if="hasActions" class="w-1 px-4" :class="dense ? 'py-2' : 'py-3'" />
285
+ </tr>
286
+ </thead>
287
+
288
+ <tbody>
289
+ <!-- Loading -->
290
+ <template v-if="loading">
291
+ <tr v-for="ri in perPage" :key="`sk-${ri}`" class="border-t border-outline-variant">
292
+ <td v-if="hasExpand" :class="dense ? 'px-2 py-2' : 'px-2 py-3'" />
293
+ <td v-if="selectable" :class="dense ? 'px-4 py-2' : 'px-4 py-3.5'">
294
+ <div class="h-4 w-4 animate-pulse rounded bg-on-surface/10" />
295
+ </td>
296
+ <td v-for="(col, ci) in visibleColumns" :key="col.key" :class="dense ? 'px-3 py-2' : 'px-4 py-3.5'">
297
+ <div class="h-4 animate-pulse rounded-full bg-on-surface/10" :style="{ width: skelWidth(ri, ci) }" />
298
+ </td>
299
+ <td v-if="hasActions" :class="dense ? 'px-4 py-2' : 'px-4 py-3.5'">
300
+ <div class="ml-auto h-4 w-16 animate-pulse rounded-full bg-on-surface/10" />
301
+ </td>
302
+ </tr>
303
+ </template>
304
+
305
+ <!-- Empty -->
306
+ <template v-else-if="visibleRows.length === 0">
307
+ <tr>
308
+ <td :colspan="visibleColumns.length + extraCols" class="border-t border-outline-variant px-4 py-14 text-center">
309
+ <slot name="empty">
310
+ <MIcon name="search_off" :size="36" class="mb-2 text-on-surface-variant opacity-30" />
311
+ <p class="text-body-medium text-on-surface-variant">{{ emptyText }}</p>
312
+ </slot>
313
+ </td>
314
+ </tr>
315
+ </template>
316
+
317
+ <!-- Data rows -->
318
+ <template v-else>
319
+ <template v-for="row in visibleRows" :key="rowId(row)">
320
+ <tr
321
+ :class="[
322
+ 'border-t border-outline-variant transition-colors duration-100',
323
+ 'hover:bg-on-surface/[0.04]',
324
+ selectable && isSelected(row) ? 'bg-primary/[0.06]' : '',
325
+ striped ? 'even:bg-surface-container-lowest' : '',
326
+ selectable ? 'cursor-pointer' : '',
327
+ ]"
328
+ @click="selectable ? toggleRow(row) : emit('rowClick', row)"
329
+ >
330
+ <td v-if="hasExpand" class="px-2" :class="dense ? 'py-1' : 'py-2'" @click.stop>
331
+ <MIconButton
332
+ icon="expand_more"
333
+ label="Expandir"
334
+ :size="28"
335
+ :class="isExpanded(row) ? 'rotate-180' : ''"
336
+ class="transition-transform duration-200"
337
+ @click="toggleExpand(row)"
338
+ />
339
+ </td>
340
+ <td v-if="selectable" :class="dense ? 'px-4 py-1' : 'px-4 py-3'" @click.stop="toggleRow(row)">
341
+ <MCheckbox :model-value="isSelected(row)" @update:model-value="toggleRow(row)" />
342
+ </td>
343
+ <td
344
+ v-for="col in visibleColumns"
345
+ :key="col.key"
346
+ :class="['text-body-medium text-on-surface', alignClass(col.align), dense ? 'px-3 py-1.5' : 'px-4 py-3']"
347
+ >
348
+ <slot :name="`cell-${col.key}`" :row="row" :value="row[col.key]" :col="col">
349
+ {{ row[col.key] ?? '—' }}
350
+ </slot>
351
+ </td>
352
+ <td v-if="hasActions" class="text-right" :class="dense ? 'px-4 py-1' : 'px-4 py-3'" @click.stop>
353
+ <slot name="row-actions" :row="row" />
354
+ </td>
355
+ </tr>
356
+ <!-- Expanded content -->
357
+ <tr v-if="hasExpand && isExpanded(row)">
358
+ <td :colspan="visibleColumns.length + extraCols" class="border-t border-outline-variant/50 bg-surface-container-lowest px-6 py-4">
359
+ <slot name="row-expand" :row="row" />
360
+ </td>
361
+ </tr>
362
+ </template>
363
+ </template>
364
+ </tbody>
365
+ </table>
366
+ </div>
367
+
368
+ <!-- Footer -->
369
+ <div class="flex items-center justify-between gap-4 border-t border-outline-variant bg-surface-container-lowest px-4 py-2">
370
+ <span class="text-label-small text-on-surface-variant">
371
+ {{ totalCount }} registro{{ totalCount !== 1 ? 's' : '' }}
372
+ </span>
373
+ <MPagination :page="internalPage" :per-page="perPage" :total="totalCount" @update:page="internalPage = $event" />
374
+ </div>
375
+ </div>
376
+ </template>