@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,159 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
|
|
5
|
+
export interface TreeTableColumn {
|
|
6
|
+
key: string
|
|
7
|
+
label: string
|
|
8
|
+
width?: string
|
|
9
|
+
align?: 'left' | 'center' | 'right'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TreeTableRow {
|
|
13
|
+
[key: string]: any
|
|
14
|
+
children?: TreeTableRow[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(defineProps<{
|
|
18
|
+
columns: TreeTableColumn[]
|
|
19
|
+
rows: TreeTableRow[]
|
|
20
|
+
rowKey?: string
|
|
21
|
+
defaultExpanded?: boolean
|
|
22
|
+
indent?: number
|
|
23
|
+
dense?: boolean
|
|
24
|
+
}>(), {
|
|
25
|
+
rowKey: 'id',
|
|
26
|
+
defaultExpanded: false,
|
|
27
|
+
indent: 24,
|
|
28
|
+
dense: false,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits<{ rowClick: [TreeTableRow] }>()
|
|
32
|
+
|
|
33
|
+
const expanded = ref<Set<any>>(new Set(
|
|
34
|
+
props.defaultExpanded ? collectIds(props.rows) : []
|
|
35
|
+
))
|
|
36
|
+
|
|
37
|
+
function collectIds(rows: TreeTableRow[]): any[] {
|
|
38
|
+
const ids: any[] = []
|
|
39
|
+
for (const r of rows) {
|
|
40
|
+
if (r.children?.length) {
|
|
41
|
+
ids.push(r[props.rowKey])
|
|
42
|
+
ids.push(...collectIds(r.children))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return ids
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toggleExpand(row: TreeTableRow) {
|
|
49
|
+
const id = row[props.rowKey]
|
|
50
|
+
const next = new Set(expanded.value)
|
|
51
|
+
next.has(id) ? next.delete(id) : next.add(id)
|
|
52
|
+
expanded.value = next
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isExpanded(row: TreeTableRow) { return expanded.value.has(row[props.rowKey]) }
|
|
56
|
+
|
|
57
|
+
interface FlatRow {
|
|
58
|
+
row: TreeTableRow
|
|
59
|
+
depth: number
|
|
60
|
+
hasChildren: boolean
|
|
61
|
+
isExpanded: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const flatRows = computed(() => {
|
|
65
|
+
const result: FlatRow[] = []
|
|
66
|
+
function walk(rows: TreeTableRow[], depth: number) {
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
const hasChildren = !!row.children?.length
|
|
69
|
+
const exp = isExpanded(row)
|
|
70
|
+
result.push({ row, depth, hasChildren, isExpanded: exp })
|
|
71
|
+
if (hasChildren && exp) walk(row.children!, depth + 1)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
walk(props.rows, 0)
|
|
75
|
+
return result
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
function expandAll() {
|
|
79
|
+
expanded.value = new Set(collectIds(props.rows))
|
|
80
|
+
}
|
|
81
|
+
function collapseAll() {
|
|
82
|
+
expanded.value = new Set()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function alignClass(a?: string) { return a === 'center' ? 'text-center' : a === 'right' ? 'text-right' : 'text-left' }
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<div class="flex flex-col overflow-hidden rounded-sm border border-outline-variant">
|
|
90
|
+
<!-- Toolbar -->
|
|
91
|
+
<div v-if="$slots.toolbar" class="flex items-center gap-2 border-b border-outline-variant bg-surface-container-lowest px-4 py-2">
|
|
92
|
+
<slot name="toolbar" :expand-all="expandAll" :collapse-all="collapseAll" />
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="overflow-x-auto">
|
|
96
|
+
<table class="w-full border-collapse">
|
|
97
|
+
<thead>
|
|
98
|
+
<tr class="bg-surface-container-high">
|
|
99
|
+
<th
|
|
100
|
+
v-for="(col, ci) in columns"
|
|
101
|
+
:key="col.key"
|
|
102
|
+
:style="col.width ? { width: col.width } : undefined"
|
|
103
|
+
:class="[
|
|
104
|
+
'whitespace-nowrap text-label-medium font-medium text-on-surface-variant',
|
|
105
|
+
dense ? 'px-3 py-2' : 'px-4 py-3',
|
|
106
|
+
alignClass(col.align),
|
|
107
|
+
]"
|
|
108
|
+
>
|
|
109
|
+
{{ col.label }}
|
|
110
|
+
</th>
|
|
111
|
+
</tr>
|
|
112
|
+
</thead>
|
|
113
|
+
<tbody>
|
|
114
|
+
<tr
|
|
115
|
+
v-for="(item, i) in flatRows"
|
|
116
|
+
:key="item.row[rowKey] ?? i"
|
|
117
|
+
class="border-t border-outline-variant transition-colors duration-100 hover:bg-on-surface/[0.04]"
|
|
118
|
+
:class="item.depth > 0 ? 'bg-surface-container-lowest/50' : ''"
|
|
119
|
+
@click="emit('rowClick', item.row)"
|
|
120
|
+
>
|
|
121
|
+
<td
|
|
122
|
+
v-for="(col, ci) in columns"
|
|
123
|
+
:key="col.key"
|
|
124
|
+
:class="['text-body-medium text-on-surface', alignClass(col.align), dense ? 'px-3 py-1.5' : 'px-4 py-3']"
|
|
125
|
+
>
|
|
126
|
+
<!-- First column gets indent + expand icon -->
|
|
127
|
+
<div v-if="ci === 0" class="flex items-center gap-1" :style="{ paddingLeft: `${item.depth * indent}px` }">
|
|
128
|
+
<button
|
|
129
|
+
v-if="item.hasChildren"
|
|
130
|
+
type="button"
|
|
131
|
+
class="flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-full text-on-surface-variant transition-transform duration-200 hover:bg-on-surface/8"
|
|
132
|
+
:class="item.isExpanded ? 'rotate-90' : ''"
|
|
133
|
+
@click.stop="toggleExpand(item.row)"
|
|
134
|
+
>
|
|
135
|
+
<MIcon name="chevron_right" :size="18" />
|
|
136
|
+
</button>
|
|
137
|
+
<span v-else class="w-6 shrink-0" />
|
|
138
|
+
<slot :name="`cell-${col.key}`" :row="item.row" :value="item.row[col.key]" :depth="item.depth">
|
|
139
|
+
<span :class="item.hasChildren ? 'font-medium' : ''">{{ item.row[col.key] ?? '—' }}</span>
|
|
140
|
+
</slot>
|
|
141
|
+
</div>
|
|
142
|
+
<template v-else>
|
|
143
|
+
<slot :name="`cell-${col.key}`" :row="item.row" :value="item.row[col.key]" :depth="item.depth">
|
|
144
|
+
{{ item.row[col.key] ?? '—' }}
|
|
145
|
+
</slot>
|
|
146
|
+
</template>
|
|
147
|
+
</td>
|
|
148
|
+
</tr>
|
|
149
|
+
<tr v-if="!flatRows.length">
|
|
150
|
+
<td :colspan="columns.length" class="border-t border-outline-variant px-4 py-10 text-center">
|
|
151
|
+
<MIcon name="account_tree" :size="36" class="mb-2 text-on-surface-variant opacity-30" />
|
|
152
|
+
<p class="text-body-medium text-on-surface-variant">Sin datos</p>
|
|
153
|
+
</td>
|
|
154
|
+
</tr>
|
|
155
|
+
</tbody>
|
|
156
|
+
</table>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</template>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
|
|
5
|
+
export interface VTableColumn {
|
|
6
|
+
key: string
|
|
7
|
+
label: string
|
|
8
|
+
width?: string
|
|
9
|
+
align?: 'left' | 'center' | 'right'
|
|
10
|
+
sortable?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<{
|
|
14
|
+
columns: VTableColumn[]
|
|
15
|
+
rows: Record<string, any>[]
|
|
16
|
+
rowHeight?: number
|
|
17
|
+
rowKey?: string
|
|
18
|
+
overscan?: number
|
|
19
|
+
maxHeight?: string
|
|
20
|
+
}>(), {
|
|
21
|
+
rowHeight: 44,
|
|
22
|
+
rowKey: 'id',
|
|
23
|
+
overscan: 5,
|
|
24
|
+
maxHeight: '500px',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits<{ rowClick: [Record<string, any>] }>()
|
|
28
|
+
|
|
29
|
+
const scrollEl = ref<HTMLElement>()
|
|
30
|
+
const scrollTop = ref(0)
|
|
31
|
+
const containerH = ref(400)
|
|
32
|
+
|
|
33
|
+
const sortKey = ref('')
|
|
34
|
+
const sortDir = ref<'asc' | 'desc' | ''>('')
|
|
35
|
+
|
|
36
|
+
function toggleSort(key: string) {
|
|
37
|
+
if (sortKey.value !== key) { sortKey.value = key; sortDir.value = 'asc' }
|
|
38
|
+
else if (sortDir.value === 'asc') sortDir.value = 'desc'
|
|
39
|
+
else { sortKey.value = ''; sortDir.value = '' }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sortedRows = computed(() => {
|
|
43
|
+
if (!sortKey.value || !sortDir.value) return props.rows
|
|
44
|
+
const key = sortKey.value, dir = sortDir.value
|
|
45
|
+
return [...props.rows].sort((a, b) => {
|
|
46
|
+
const cmp = String(a[key] ?? '').localeCompare(String(b[key] ?? ''), undefined, { numeric: true, sensitivity: 'base' })
|
|
47
|
+
return dir === 'asc' ? cmp : -cmp
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const totalHeight = computed(() => sortedRows.value.length * props.rowHeight)
|
|
52
|
+
|
|
53
|
+
const visibleRange = computed(() => {
|
|
54
|
+
const start = Math.max(0, Math.floor(scrollTop.value / props.rowHeight) - props.overscan)
|
|
55
|
+
const end = Math.min(
|
|
56
|
+
sortedRows.value.length,
|
|
57
|
+
Math.ceil((scrollTop.value + containerH.value) / props.rowHeight) + props.overscan
|
|
58
|
+
)
|
|
59
|
+
return { start, end }
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const visibleRows = computed(() =>
|
|
63
|
+
sortedRows.value.slice(visibleRange.value.start, visibleRange.value.end).map((row, i) => ({
|
|
64
|
+
row,
|
|
65
|
+
index: visibleRange.value.start + i,
|
|
66
|
+
top: (visibleRange.value.start + i) * props.rowHeight,
|
|
67
|
+
}))
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
function onScroll() {
|
|
71
|
+
if (!scrollEl.value) return
|
|
72
|
+
scrollTop.value = scrollEl.value.scrollTop
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let ro: ResizeObserver | null = null
|
|
76
|
+
onMounted(() => {
|
|
77
|
+
if (scrollEl.value) {
|
|
78
|
+
containerH.value = scrollEl.value.clientHeight
|
|
79
|
+
ro = new ResizeObserver((entries) => { containerH.value = entries[0]!.contentRect.height })
|
|
80
|
+
ro.observe(scrollEl.value)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
onBeforeUnmount(() => ro?.disconnect())
|
|
84
|
+
|
|
85
|
+
function alignClass(a?: string) { return a === 'center' ? 'text-center' : a === 'right' ? 'text-right' : 'text-left' }
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<div class="flex flex-col overflow-hidden rounded-sm border border-outline-variant">
|
|
90
|
+
<!-- Header -->
|
|
91
|
+
<div class="flex bg-surface-container-high">
|
|
92
|
+
<div
|
|
93
|
+
v-for="col in columns"
|
|
94
|
+
:key="col.key"
|
|
95
|
+
:style="{ width: col.width || 'auto', flex: col.width ? 'none' : '1' }"
|
|
96
|
+
:class="[
|
|
97
|
+
'px-4 py-3 text-label-medium font-medium text-on-surface-variant whitespace-nowrap',
|
|
98
|
+
alignClass(col.align),
|
|
99
|
+
col.sortable ? 'cursor-pointer select-none hover:text-on-surface transition-colors' : '',
|
|
100
|
+
]"
|
|
101
|
+
@click="col.sortable ? toggleSort(col.key) : undefined"
|
|
102
|
+
>
|
|
103
|
+
<span class="inline-flex items-center gap-1">
|
|
104
|
+
{{ col.label }}
|
|
105
|
+
<span v-if="col.sortable" class="inline-flex">
|
|
106
|
+
<MIcon v-if="sortKey === col.key && sortDir === 'asc'" name="arrow_upward" :size="14" class="text-primary" />
|
|
107
|
+
<MIcon v-else-if="sortKey === col.key && sortDir === 'desc'" name="arrow_downward" :size="14" class="text-primary" />
|
|
108
|
+
<MIcon v-else name="unfold_more" :size="14" class="opacity-30" />
|
|
109
|
+
</span>
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<!-- Virtual scroll body -->
|
|
115
|
+
<div
|
|
116
|
+
ref="scrollEl"
|
|
117
|
+
class="overflow-y-auto"
|
|
118
|
+
:style="{ maxHeight }"
|
|
119
|
+
@scroll="onScroll"
|
|
120
|
+
>
|
|
121
|
+
<div class="relative" :style="{ height: `${totalHeight}px` }">
|
|
122
|
+
<div
|
|
123
|
+
v-for="{ row, index, top } in visibleRows"
|
|
124
|
+
:key="row[rowKey] ?? index"
|
|
125
|
+
class="absolute left-0 right-0 flex border-t border-outline-variant transition-colors duration-75 hover:bg-on-surface/[0.04]"
|
|
126
|
+
:class="index % 2 === 0 ? '' : 'bg-surface-container-lowest/50'"
|
|
127
|
+
:style="{ top: `${top}px`, height: `${rowHeight}px` }"
|
|
128
|
+
@click="emit('rowClick', row)"
|
|
129
|
+
>
|
|
130
|
+
<div
|
|
131
|
+
v-for="col in columns"
|
|
132
|
+
:key="col.key"
|
|
133
|
+
class="flex items-center overflow-hidden px-4 text-body-medium text-on-surface"
|
|
134
|
+
:style="{ width: col.width || 'auto', flex: col.width ? 'none' : '1' }"
|
|
135
|
+
:class="alignClass(col.align)"
|
|
136
|
+
>
|
|
137
|
+
<slot :name="`cell-${col.key}`" :row="row" :value="row[col.key]">
|
|
138
|
+
<span class="truncate">{{ row[col.key] ?? '—' }}</span>
|
|
139
|
+
</slot>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<!-- Footer -->
|
|
146
|
+
<div class="border-t border-outline-variant bg-surface-container-lowest px-4 py-2">
|
|
147
|
+
<span class="text-label-small text-on-surface-variant">
|
|
148
|
+
{{ sortedRows.length.toLocaleString() }} filas
|
|
149
|
+
<template v-if="visibleRange.end - visibleRange.start < sortedRows.length">
|
|
150
|
+
· mostrando {{ visibleRange.start + 1 }}–{{ visibleRange.end }}
|
|
151
|
+
</template>
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</template>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, nextTick } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
import MContextMenuPanel from './_MContextMenuPanel.vue'
|
|
5
|
+
import type { ContextMenuItem } from './MContextMenu.vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
items: ContextMenuItem[]
|
|
9
|
+
x: number
|
|
10
|
+
y: number
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const emit = defineEmits<{ close: [] }>()
|
|
14
|
+
|
|
15
|
+
const panel = ref<HTMLElement | null>(null)
|
|
16
|
+
const panelX = ref(props.x)
|
|
17
|
+
const panelY = ref(props.y)
|
|
18
|
+
const activeIndex = ref<number | null>(null)
|
|
19
|
+
const subPos = ref({ x: 0, y: 0 })
|
|
20
|
+
|
|
21
|
+
onMounted(async () => {
|
|
22
|
+
await nextTick()
|
|
23
|
+
if (!panel.value) return
|
|
24
|
+
const el = panel.value
|
|
25
|
+
panelX.value = Math.min(props.x, window.innerWidth - el.offsetWidth - 8)
|
|
26
|
+
panelY.value = Math.min(props.y, window.innerHeight - el.offsetHeight - 8)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
function onItemMouseEnter(index: number, item: ContextMenuItem, e: MouseEvent) {
|
|
30
|
+
if (item.divider || item.disabled) {
|
|
31
|
+
activeIndex.value = null
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
if (!item.children?.length) {
|
|
35
|
+
activeIndex.value = null
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
activeIndex.value = index
|
|
40
|
+
const itemEl = e.currentTarget as HTMLElement
|
|
41
|
+
const itemRect = itemEl.getBoundingClientRect()
|
|
42
|
+
const panelRect = panel.value!.getBoundingClientRect()
|
|
43
|
+
|
|
44
|
+
let x = panelRect.right
|
|
45
|
+
let y = itemRect.top
|
|
46
|
+
if (x + 220 > window.innerWidth) x = panelRect.left - 220
|
|
47
|
+
if (y + 300 > window.innerHeight) y = Math.max(8, window.innerHeight - 300)
|
|
48
|
+
|
|
49
|
+
subPos.value = { x, y }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function onItemClick(item: ContextMenuItem) {
|
|
53
|
+
if (item.disabled || item.divider || item.children?.length) return
|
|
54
|
+
item.onClick?.()
|
|
55
|
+
emit('close')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onPanelMouseLeave(e: MouseEvent) {
|
|
59
|
+
// Don't close if the mouse moved to another context menu panel (sibling sub-panel)
|
|
60
|
+
const related = e.relatedTarget as Element | null
|
|
61
|
+
if (related?.closest('.m3-ctx-panel')) return
|
|
62
|
+
activeIndex.value = null
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<div
|
|
68
|
+
ref="panel"
|
|
69
|
+
class="m3-ctx-panel absolute z-[201] min-w-[200px] rounded-sm bg-surface-container shadow-elevation-2"
|
|
70
|
+
:style="{ left: `${panelX}px`, top: `${panelY}px` }"
|
|
71
|
+
@mouseleave="onPanelMouseLeave"
|
|
72
|
+
>
|
|
73
|
+
<div class="overflow-hidden rounded-sm py-1">
|
|
74
|
+
<template v-for="(item, i) in items" :key="i">
|
|
75
|
+
<hr v-if="item.divider" class="my-1 border-outline-variant" />
|
|
76
|
+
|
|
77
|
+
<div
|
|
78
|
+
v-else
|
|
79
|
+
class="relative flex cursor-default select-none items-center gap-3 px-4 py-2.5 text-body-large"
|
|
80
|
+
:class="[
|
|
81
|
+
item.disabled
|
|
82
|
+
? 'cursor-not-allowed opacity-38 text-on-surface'
|
|
83
|
+
: item.danger
|
|
84
|
+
? 'cursor-pointer text-error hover:bg-error/8'
|
|
85
|
+
: 'cursor-pointer text-on-surface hover:bg-on-surface/8',
|
|
86
|
+
activeIndex === i && !item.disabled
|
|
87
|
+
? (item.danger ? 'bg-error/8' : 'bg-on-surface/8')
|
|
88
|
+
: '',
|
|
89
|
+
]"
|
|
90
|
+
@mouseenter="onItemMouseEnter(i, item, $event)"
|
|
91
|
+
@click="onItemClick(item)"
|
|
92
|
+
>
|
|
93
|
+
<MIcon
|
|
94
|
+
v-if="item.icon"
|
|
95
|
+
:name="item.icon"
|
|
96
|
+
:size="18"
|
|
97
|
+
class="shrink-0"
|
|
98
|
+
:class="item.danger ? 'text-error' : 'text-on-surface-variant'"
|
|
99
|
+
/>
|
|
100
|
+
<span v-else class="w-[18px] shrink-0" />
|
|
101
|
+
|
|
102
|
+
<span class="flex-1">{{ item.label }}</span>
|
|
103
|
+
|
|
104
|
+
<span v-if="item.shortcut" class="text-label-small text-on-surface-variant">
|
|
105
|
+
{{ item.shortcut }}
|
|
106
|
+
</span>
|
|
107
|
+
|
|
108
|
+
<MIcon
|
|
109
|
+
v-if="item.children?.length"
|
|
110
|
+
name="chevron_right"
|
|
111
|
+
:size="18"
|
|
112
|
+
class="shrink-0 text-on-surface-variant"
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<!-- Sub-panel: sibling in the same Teleport layer.
|
|
120
|
+
No <Transition> wrapper — MContextMenuPanel is a fragment and cannot be
|
|
121
|
+
animated by Vue's Transition (produces a console warning). -->
|
|
122
|
+
<MContextMenuPanel
|
|
123
|
+
v-if="activeIndex !== null && items[activeIndex]?.children?.length"
|
|
124
|
+
:items="items[activeIndex]!.children!"
|
|
125
|
+
:x="subPos.x"
|
|
126
|
+
:y="subPos.y"
|
|
127
|
+
@close="emit('close')"
|
|
128
|
+
/>
|
|
129
|
+
</template>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject } from 'vue'
|
|
3
|
+
import MTreeNode from './_MTreeNode.vue'
|
|
4
|
+
import MCheckbox from './MCheckbox.vue'
|
|
5
|
+
import MIcon from './MIcon.vue'
|
|
6
|
+
import type { TreeContext, TreeNode } from './MTree.vue'
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{ node: TreeNode; depth: number }>()
|
|
9
|
+
|
|
10
|
+
const tree = inject<TreeContext>('m-tree')!
|
|
11
|
+
|
|
12
|
+
const hasChildren = computed(() => !!props.node.children?.length)
|
|
13
|
+
const isExpanded = computed(() => tree.expandedIds.value.has(props.node.id))
|
|
14
|
+
const isSelected = computed(() => tree.selected.value === props.node.id)
|
|
15
|
+
|
|
16
|
+
// Use only leaf ids for checkbox visual state (branch ids are never stored in checkedSet)
|
|
17
|
+
const leafIds = computed(() => tree.getLeafIds(props.node))
|
|
18
|
+
const checkedLeafCount = computed(() => leafIds.value.filter(id => tree.checkedSet.value.has(id)).length)
|
|
19
|
+
const isChecked = computed(() => leafIds.value.length > 0 && checkedLeafCount.value === leafIds.value.length)
|
|
20
|
+
const isIndeterminate = computed(() => checkedLeafCount.value > 0 && !isChecked.value)
|
|
21
|
+
|
|
22
|
+
function onRowClick() {
|
|
23
|
+
if (props.node.disabled) return
|
|
24
|
+
tree.selectNode(props.node)
|
|
25
|
+
if (hasChildren.value) tree.toggleExpand(props.node.id)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function onChevronClick(e: MouseEvent) {
|
|
29
|
+
e.stopPropagation()
|
|
30
|
+
if (props.node.disabled) return
|
|
31
|
+
tree.toggleExpand(props.node.id)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function onCheck() {
|
|
35
|
+
if (props.node.disabled) return
|
|
36
|
+
tree.toggleCheck(props.node)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Height transition hooks ────────────────────────────────────────────────
|
|
40
|
+
function onEnter(el: Element) {
|
|
41
|
+
const e = el as HTMLElement
|
|
42
|
+
e.style.height = '0'
|
|
43
|
+
e.style.opacity = '0'
|
|
44
|
+
e.style.overflow = 'hidden'
|
|
45
|
+
e.offsetHeight // force reflow
|
|
46
|
+
e.style.transition = 'height 200ms cubic-bezier(0.4,0,0.2,1), opacity 150ms ease'
|
|
47
|
+
e.style.height = e.scrollHeight + 'px'
|
|
48
|
+
e.style.opacity = '1'
|
|
49
|
+
}
|
|
50
|
+
function onAfterEnter(el: Element) {
|
|
51
|
+
const e = el as HTMLElement
|
|
52
|
+
e.style.height = ''
|
|
53
|
+
e.style.overflow = ''
|
|
54
|
+
e.style.transition = ''
|
|
55
|
+
e.style.opacity = ''
|
|
56
|
+
}
|
|
57
|
+
function onLeave(el: Element) {
|
|
58
|
+
const e = el as HTMLElement
|
|
59
|
+
e.style.height = e.scrollHeight + 'px'
|
|
60
|
+
e.style.overflow = 'hidden'
|
|
61
|
+
e.offsetHeight // force reflow
|
|
62
|
+
e.style.transition = 'height 180ms cubic-bezier(0.4,0,0.2,1), opacity 120ms ease'
|
|
63
|
+
e.style.height = '0'
|
|
64
|
+
e.style.opacity = '0'
|
|
65
|
+
}
|
|
66
|
+
function onAfterLeave(el: Element) {
|
|
67
|
+
const e = el as HTMLElement
|
|
68
|
+
e.style.height = ''
|
|
69
|
+
e.style.overflow = ''
|
|
70
|
+
e.style.transition = ''
|
|
71
|
+
e.style.opacity = ''
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<div role="treeitem" :aria-expanded="hasChildren ? isExpanded : undefined" :aria-selected="isSelected">
|
|
77
|
+
<!-- ── Row ─────────────────────────────────────────────────────────────── -->
|
|
78
|
+
<div
|
|
79
|
+
:class="[
|
|
80
|
+
'flex items-center gap-1 rounded-sm py-1 pr-2 transition-colors duration-100 select-none',
|
|
81
|
+
node.disabled
|
|
82
|
+
? 'cursor-not-allowed opacity-38'
|
|
83
|
+
: 'cursor-pointer',
|
|
84
|
+
!node.disabled && isSelected
|
|
85
|
+
? 'bg-primary/[0.10]'
|
|
86
|
+
: !node.disabled
|
|
87
|
+
? 'hover:bg-on-surface/[0.04]'
|
|
88
|
+
: '',
|
|
89
|
+
]"
|
|
90
|
+
@click="onRowClick"
|
|
91
|
+
>
|
|
92
|
+
<!-- Indent spacer (no lines for depth 0, lines provided by parent's border-l) -->
|
|
93
|
+
<div class="flex w-6 shrink-0 items-center justify-center">
|
|
94
|
+
<!-- Chevron for branch nodes -->
|
|
95
|
+
<button
|
|
96
|
+
v-if="hasChildren"
|
|
97
|
+
type="button"
|
|
98
|
+
class="flex h-5 w-5 items-center justify-center rounded text-on-surface-variant transition-transform duration-200"
|
|
99
|
+
:class="isExpanded ? 'rotate-90' : ''"
|
|
100
|
+
:disabled="node.disabled || undefined"
|
|
101
|
+
@click="onChevronClick"
|
|
102
|
+
>
|
|
103
|
+
<MIcon name="chevron_right" :size="16" />
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Checkbox (checkable mode) -->
|
|
108
|
+
<div v-if="tree.checkable.value" class="shrink-0" @click.stop="onCheck">
|
|
109
|
+
<MCheckbox
|
|
110
|
+
:model-value="isChecked"
|
|
111
|
+
:indeterminate="isIndeterminate"
|
|
112
|
+
:disabled="node.disabled"
|
|
113
|
+
@update:model-value="onCheck"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- Node icon -->
|
|
118
|
+
<MIcon
|
|
119
|
+
v-if="node.icon"
|
|
120
|
+
:name="node.icon"
|
|
121
|
+
:size="16"
|
|
122
|
+
class="shrink-0 transition-colors"
|
|
123
|
+
:class="isSelected ? 'text-primary' : 'text-on-surface-variant'"
|
|
124
|
+
/>
|
|
125
|
+
|
|
126
|
+
<!-- Label -->
|
|
127
|
+
<span
|
|
128
|
+
class="min-w-0 flex-1 truncate text-body-medium transition-colors"
|
|
129
|
+
:class="isSelected ? 'font-medium text-primary' : 'text-on-surface'"
|
|
130
|
+
>
|
|
131
|
+
<slot name="label" :node="node">{{ node.label }}</slot>
|
|
132
|
+
</span>
|
|
133
|
+
|
|
134
|
+
<!-- Check count badge (branch + checkable) -->
|
|
135
|
+
<span
|
|
136
|
+
v-if="hasChildren && tree.checkable.value"
|
|
137
|
+
class="shrink-0 text-label-small tabular-nums text-on-surface-variant"
|
|
138
|
+
>
|
|
139
|
+
{{ checkedLeafCount }}/{{ leafIds.length }}
|
|
140
|
+
</span>
|
|
141
|
+
|
|
142
|
+
<!-- Optional trailing slot -->
|
|
143
|
+
<slot name="trailing" :node="node" />
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<!-- ── Children ───────────────────────────────────────────────────────── -->
|
|
147
|
+
<Transition
|
|
148
|
+
@enter="onEnter"
|
|
149
|
+
@after-enter="onAfterEnter"
|
|
150
|
+
@leave="onLeave"
|
|
151
|
+
@after-leave="onAfterLeave"
|
|
152
|
+
>
|
|
153
|
+
<div
|
|
154
|
+
v-if="isExpanded && hasChildren"
|
|
155
|
+
class="ml-3 border-l border-outline-variant pl-2"
|
|
156
|
+
>
|
|
157
|
+
<MTreeNode
|
|
158
|
+
v-for="child in node.children"
|
|
159
|
+
:key="child.id"
|
|
160
|
+
:node="child"
|
|
161
|
+
:depth="depth + 1"
|
|
162
|
+
>
|
|
163
|
+
<!-- @vue-ignore -->
|
|
164
|
+
<template v-for="(_, name) in $slots" #[name]="sp">
|
|
165
|
+
<slot :name="name" v-bind="sp ?? {}" />
|
|
166
|
+
</template>
|
|
167
|
+
</MTreeNode>
|
|
168
|
+
</div>
|
|
169
|
+
</Transition>
|
|
170
|
+
</div>
|
|
171
|
+
</template>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ref, watchEffect } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface Palette {
|
|
4
|
+
id: string
|
|
5
|
+
label: string
|
|
6
|
+
seed: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const palettes: Palette[] = [
|
|
10
|
+
{ id: 'purple', label: 'Morado', seed: '#6750A4' },
|
|
11
|
+
{ id: 'indigo', label: 'Índigo', seed: '#4355B9' },
|
|
12
|
+
{ id: 'navy', label: 'Marino', seed: '#354BA0' },
|
|
13
|
+
{ id: 'blue', label: 'Azul', seed: '#005AC1' },
|
|
14
|
+
{ id: 'cyan', label: 'Cian', seed: '#006874' },
|
|
15
|
+
{ id: 'teal', label: 'Teal', seed: '#006B5F' },
|
|
16
|
+
{ id: 'green', label: 'Verde', seed: '#386A20' },
|
|
17
|
+
{ id: 'lime', label: 'Lima', seed: '#4C6706' },
|
|
18
|
+
{ id: 'olive', label: 'Oliva', seed: '#636118' },
|
|
19
|
+
{ id: 'amber', label: 'Ámbar', seed: '#785900' },
|
|
20
|
+
{ id: 'sand', label: 'Arena', seed: '#715C2E' },
|
|
21
|
+
{ id: 'orange', label: 'Naranja', seed: '#8B5000' },
|
|
22
|
+
{ id: 'deep-orange', label: 'Naranja oscuro', seed: '#96480A' },
|
|
23
|
+
{ id: 'brown', label: 'Marrón', seed: '#6E4C32' },
|
|
24
|
+
{ id: 'red', label: 'Rojo', seed: '#B82000' },
|
|
25
|
+
{ id: 'coral', label: 'Coral', seed: '#A03530' },
|
|
26
|
+
{ id: 'crimson', label: 'Carmesí', seed: '#9C4068' },
|
|
27
|
+
{ id: 'pink', label: 'Rosa', seed: '#9C4057' },
|
|
28
|
+
{ id: 'violet', label: 'Violeta', seed: '#7C39A4' },
|
|
29
|
+
{ id: 'slate', label: 'Pizarra', seed: '#4A6269' },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const current = ref(localStorage.getItem('m3-palette') ?? 'purple')
|
|
33
|
+
|
|
34
|
+
export function useColorPalette() {
|
|
35
|
+
watchEffect(() => {
|
|
36
|
+
const id = current.value
|
|
37
|
+
localStorage.setItem('m3-palette', id)
|
|
38
|
+
|
|
39
|
+
if (id === 'purple') {
|
|
40
|
+
document.documentElement.removeAttribute('data-palette')
|
|
41
|
+
} else {
|
|
42
|
+
document.documentElement.setAttribute('data-palette', id)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function set(id: string) {
|
|
47
|
+
document.documentElement.classList.add('theme-transitioning')
|
|
48
|
+
void document.documentElement.offsetHeight
|
|
49
|
+
current.value = id
|
|
50
|
+
setTimeout(() => document.documentElement.classList.remove('theme-transitioning'), 300)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { palette: current, palettes, set }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Apply on module load so palette is visible before any component mounts
|
|
57
|
+
const saved = localStorage.getItem('m3-palette')
|
|
58
|
+
if (saved && saved !== 'purple') {
|
|
59
|
+
document.documentElement.setAttribute('data-palette', saved)
|
|
60
|
+
}
|