@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,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
+ }