@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.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/components/MAlert.vue.d.ts +27 -0
- package/dist/components/MAppBar.vue.d.ts +24 -0
- package/dist/components/MAvatar.vue.d.ts +9 -0
- package/dist/components/MBadge.vue.d.ts +22 -0
- package/dist/components/MBottomSheet.vue.d.ts +26 -0
- package/dist/components/MBreadcrumbs.vue.d.ts +19 -0
- package/dist/components/MButton.vue.d.ts +32 -0
- package/dist/components/MCalendar.vue.d.ts +23 -0
- package/dist/components/MCard.vue.d.ts +28 -0
- package/dist/components/MChart.vue.d.ts +13 -0
- package/dist/components/MCheckbox.vue.d.ts +26 -0
- package/dist/components/MChip.vue.d.ts +33 -0
- package/dist/components/MCodeEditor.vue.d.ts +35 -0
- package/dist/components/MColorPicker.vue.d.ts +18 -0
- package/dist/components/MCommandPalette.vue.d.ts +29 -0
- package/dist/components/MConfirmDialog.vue.d.ts +23 -0
- package/dist/components/MContainer.vue.d.ts +24 -0
- package/dist/components/MContextMenu.vue.d.ts +35 -0
- package/dist/components/MDataTable.vue.d.ts +83 -0
- package/dist/components/MDatePicker.vue.d.ts +21 -0
- package/dist/components/MDateRangePicker.vue.d.ts +24 -0
- package/dist/components/MDialog.vue.d.ts +30 -0
- package/dist/components/MDivider.vue.d.ts +11 -0
- package/dist/components/MDragDropList.vue.d.ts +40 -0
- package/dist/components/MEmptyState.vue.d.ts +21 -0
- package/dist/components/MExpansionPanel.vue.d.ts +28 -0
- package/dist/components/MFab.vue.d.ts +28 -0
- package/dist/components/MFileUpload.vue.d.ts +25 -0
- package/dist/components/MGrid.vue.d.ts +26 -0
- package/dist/components/MHotkeys.vue.d.ts +16 -0
- package/dist/components/MIcon.vue.d.ts +9 -0
- package/dist/components/MIconButton.vue.d.ts +14 -0
- package/dist/components/MInfiniteScroll.vue.d.ts +34 -0
- package/dist/components/MJsonEditor.vue.d.ts +17 -0
- package/dist/components/MJsonViewer.vue.d.ts +14 -0
- package/dist/components/MKanban.vue.d.ts +53 -0
- package/dist/components/MLoadingOverlay.vue.d.ts +28 -0
- package/dist/components/MMarkdown.vue.d.ts +11 -0
- package/dist/components/MMasonry.vue.d.ts +23 -0
- package/dist/components/MMenu.vue.d.ts +27 -0
- package/dist/components/MMenuItem.vue.d.ts +16 -0
- package/dist/components/MMultiSelect.vue.d.ts +34 -0
- package/dist/components/MNavigationBar.vue.d.ts +18 -0
- package/dist/components/MNavigationDrawer.vue.d.ts +41 -0
- package/dist/components/MNavigationRail.vue.d.ts +32 -0
- package/dist/components/MPagination.vue.d.ts +12 -0
- package/dist/components/MProgressBar.vue.d.ts +13 -0
- package/dist/components/MRadio.vue.d.ts +17 -0
- package/dist/components/MRadioGroup.vue.d.ts +24 -0
- package/dist/components/MRating.vue.d.ts +23 -0
- package/dist/components/MResult.vue.d.ts +20 -0
- package/dist/components/MRichTextEditor.vue.d.ts +17 -0
- package/dist/components/MScheduler.vue.d.ts +35 -0
- package/dist/components/MSegmentedButton.vue.d.ts +24 -0
- package/dist/components/MSelect.vue.d.ts +29 -0
- package/dist/components/MSideSheet.vue.d.ts +28 -0
- package/dist/components/MSkeleton.vue.d.ts +14 -0
- package/dist/components/MSlider.vue.d.ts +24 -0
- package/dist/components/MSnackbar.vue.d.ts +3 -0
- package/dist/components/MSpinner.vue.d.ts +10 -0
- package/dist/components/MSplitter.vue.d.ts +26 -0
- package/dist/components/MSpotlightSearch.vue.d.ts +34 -0
- package/dist/components/MStack.vue.d.ts +30 -0
- package/dist/components/MStatCard.vue.d.ts +24 -0
- package/dist/components/MStepper.vue.d.ts +33 -0
- package/dist/components/MSwitch.vue.d.ts +14 -0
- package/dist/components/MTable.vue.d.ts +73 -0
- package/dist/components/MTabs.vue.d.ts +20 -0
- package/dist/components/MTerminal.vue.d.ts +25 -0
- package/dist/components/MTextField.vue.d.ts +41 -0
- package/dist/components/MTimePicker.vue.d.ts +20 -0
- package/dist/components/MTimeline.vue.d.ts +31 -0
- package/dist/components/MTooltip.vue.d.ts +21 -0
- package/dist/components/MTopAppBar.vue.d.ts +29 -0
- package/dist/components/MTour.vue.d.ts +19 -0
- package/dist/components/MTransferList.vue.d.ts +23 -0
- package/dist/components/MTree.vue.d.ts +68 -0
- package/dist/components/MTreeTable.vue.d.ts +57 -0
- package/dist/components/MVirtualTable.vue.d.ts +40 -0
- package/dist/components/_MContextMenuPanel.vue.d.ts +13 -0
- package/dist/components/_MTreeNode.vue.d.ts +26 -0
- package/dist/composables/useColorPalette.d.ts +11 -0
- package/dist/composables/useFieldBg.d.ts +13 -0
- package/dist/composables/useTheme.d.ts +5 -0
- package/dist/composables/useToast.d.ts +59 -0
- package/dist/index.d.ts +112 -0
- package/dist/m3ui.css +2 -0
- package/dist/m3ui.js +7432 -0
- package/dist/m3ui.js.map +1 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/styles/palettes.css +1253 -0
- package/dist/styles/theme.css +249 -0
- package/package.json +166 -0
- package/src/components/MAlert.vue +69 -0
- package/src/components/MAppBar.vue +40 -0
- package/src/components/MAvatar.vue +21 -0
- package/src/components/MBadge.vue +46 -0
- package/src/components/MBottomSheet.vue +113 -0
- package/src/components/MBreadcrumbs.vue +52 -0
- package/src/components/MButton.vue +111 -0
- package/src/components/MCalendar.vue +173 -0
- package/src/components/MCard.vue +56 -0
- package/src/components/MChart.vue +158 -0
- package/src/components/MCheckbox.vue +48 -0
- package/src/components/MChip.vue +87 -0
- package/src/components/MCodeEditor.vue +179 -0
- package/src/components/MColorPicker.vue +305 -0
- package/src/components/MCommandPalette.vue +213 -0
- package/src/components/MConfirmDialog.vue +43 -0
- package/src/components/MContainer.vue +36 -0
- package/src/components/MContextMenu.vue +66 -0
- package/src/components/MDataTable.vue +376 -0
- package/src/components/MDatePicker.vue +253 -0
- package/src/components/MDateRangePicker.vue +265 -0
- package/src/components/MDialog.vue +90 -0
- package/src/components/MDivider.vue +26 -0
- package/src/components/MDragDropList.vue +111 -0
- package/src/components/MEmptyState.vue +40 -0
- package/src/components/MExpansionPanel.vue +112 -0
- package/src/components/MFab.vue +220 -0
- package/src/components/MFileUpload.vue +206 -0
- package/src/components/MGrid.vue +99 -0
- package/src/components/MHotkeys.vue +122 -0
- package/src/components/MIcon.vue +9 -0
- package/src/components/MIconButton.vue +49 -0
- package/src/components/MInfiniteScroll.vue +68 -0
- package/src/components/MJsonEditor.vue +118 -0
- package/src/components/MJsonViewer.vue +106 -0
- package/src/components/MKanban.vue +147 -0
- package/src/components/MLoadingOverlay.vue +52 -0
- package/src/components/MMarkdown.vue +123 -0
- package/src/components/MMasonry.vue +87 -0
- package/src/components/MMenu.vue +113 -0
- package/src/components/MMenuItem.vue +15 -0
- package/src/components/MMultiSelect.vue +306 -0
- package/src/components/MNavigationBar.vue +62 -0
- package/src/components/MNavigationDrawer.vue +157 -0
- package/src/components/MNavigationRail.vue +80 -0
- package/src/components/MPagination.vue +37 -0
- package/src/components/MProgressBar.vue +200 -0
- package/src/components/MRadio.vue +89 -0
- package/src/components/MRadioGroup.vue +41 -0
- package/src/components/MRating.vue +108 -0
- package/src/components/MResult.vue +62 -0
- package/src/components/MRichTextEditor.vue +199 -0
- package/src/components/MScheduler.vue +225 -0
- package/src/components/MSegmentedButton.vue +75 -0
- package/src/components/MSelect.vue +259 -0
- package/src/components/MSideSheet.vue +112 -0
- package/src/components/MSkeleton.vue +60 -0
- package/src/components/MSlider.vue +188 -0
- package/src/components/MSnackbar.vue +244 -0
- package/src/components/MSpinner.vue +122 -0
- package/src/components/MSplitter.vue +97 -0
- package/src/components/MSpotlightSearch.vue +244 -0
- package/src/components/MStack.vue +67 -0
- package/src/components/MStatCard.vue +56 -0
- package/src/components/MStepper.vue +161 -0
- package/src/components/MSwitch.vue +63 -0
- package/src/components/MTable.vue +404 -0
- package/src/components/MTabs.vue +97 -0
- package/src/components/MTerminal.vue +146 -0
- package/src/components/MTextField.vue +180 -0
- package/src/components/MTimePicker.vue +227 -0
- package/src/components/MTimeline.vue +117 -0
- package/src/components/MTooltip.vue +82 -0
- package/src/components/MTopAppBar.vue +62 -0
- package/src/components/MTour.vue +226 -0
- package/src/components/MTransferList.vue +181 -0
- package/src/components/MTree.vue +164 -0
- package/src/components/MTreeTable.vue +159 -0
- package/src/components/MVirtualTable.vue +155 -0
- package/src/components/_MContextMenuPanel.vue +129 -0
- package/src/components/_MTreeNode.vue +171 -0
- package/src/composables/useColorPalette.ts +60 -0
- package/src/composables/useFieldBg.ts +91 -0
- package/src/composables/useTheme.ts +55 -0
- package/src/composables/useToast.ts +51 -0
- package/src/env.d.ts +1 -0
- package/src/index.ts +119 -0
- package/src/plugin.ts +18 -0
- package/src/styles/palettes.css +1253 -0
- 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>
|