@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,226 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
3
+ import MButton from './MButton.vue'
4
+ import MIcon from './MIcon.vue'
5
+
6
+ export interface TourStep {
7
+ target: string
8
+ title: string
9
+ content: string
10
+ placement?: 'top' | 'bottom' | 'left' | 'right'
11
+ }
12
+
13
+ const props = withDefaults(
14
+ defineProps<{
15
+ modelValue: boolean
16
+ steps: TourStep[]
17
+ }>(),
18
+ {},
19
+ )
20
+
21
+ const emit = defineEmits<{
22
+ 'update:modelValue': [boolean]
23
+ finish: []
24
+ }>()
25
+
26
+ const currentStep = ref(0)
27
+ const tooltipStyle = ref<Record<string, string>>({})
28
+ const arrowStyle = ref<Record<string, string>>({})
29
+ const placement = ref<'top' | 'bottom' | 'left' | 'right'>('bottom')
30
+
31
+ const step = computed(() => props.steps[currentStep.value])
32
+ const isFirst = computed(() => currentStep.value === 0)
33
+ const isLast = computed(() => currentStep.value === props.steps.length - 1)
34
+
35
+ function positionTooltip() {
36
+ if (!step.value) return
37
+ const el = document.querySelector(step.value.target) as HTMLElement | null
38
+ if (!el) return
39
+
40
+ const rect = el.getBoundingClientRect()
41
+ const pad = 12
42
+ const arrowSize = 8
43
+ const p = step.value.placement ?? 'bottom'
44
+ placement.value = p
45
+
46
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' })
47
+
48
+ const s: Record<string, string> = { position: 'fixed' }
49
+ const a: Record<string, string> = { position: 'absolute' }
50
+
51
+ switch (p) {
52
+ case 'bottom':
53
+ s.top = `${rect.bottom + pad}px`
54
+ s.left = `${rect.left + rect.width / 2}px`
55
+ s.transform = 'translateX(-50%)'
56
+ a.top = `-${arrowSize}px`
57
+ a.left = '50%'
58
+ a.transform = 'translateX(-50%)'
59
+ a.borderBottom = `${arrowSize}px solid var(--color-surface-container-high)`
60
+ a.borderLeft = `${arrowSize}px solid transparent`
61
+ a.borderRight = `${arrowSize}px solid transparent`
62
+ break
63
+ case 'top':
64
+ s.bottom = `${window.innerHeight - rect.top + pad}px`
65
+ s.left = `${rect.left + rect.width / 2}px`
66
+ s.transform = 'translateX(-50%)'
67
+ a.bottom = `-${arrowSize}px`
68
+ a.left = '50%'
69
+ a.transform = 'translateX(-50%)'
70
+ a.borderTop = `${arrowSize}px solid var(--color-surface-container-high)`
71
+ a.borderLeft = `${arrowSize}px solid transparent`
72
+ a.borderRight = `${arrowSize}px solid transparent`
73
+ break
74
+ case 'left':
75
+ s.top = `${rect.top + rect.height / 2}px`
76
+ s.right = `${window.innerWidth - rect.left + pad}px`
77
+ s.transform = 'translateY(-50%)'
78
+ a.top = '50%'
79
+ a.right = `-${arrowSize}px`
80
+ a.transform = 'translateY(-50%)'
81
+ a.borderLeft = `${arrowSize}px solid var(--color-surface-container-high)`
82
+ a.borderTop = `${arrowSize}px solid transparent`
83
+ a.borderBottom = `${arrowSize}px solid transparent`
84
+ break
85
+ case 'right':
86
+ s.top = `${rect.top + rect.height / 2}px`
87
+ s.left = `${rect.right + pad}px`
88
+ s.transform = 'translateY(-50%)'
89
+ a.top = '50%'
90
+ a.left = `-${arrowSize}px`
91
+ a.transform = 'translateY(-50%)'
92
+ a.borderRight = `${arrowSize}px solid var(--color-surface-container-high)`
93
+ a.borderTop = `${arrowSize}px solid transparent`
94
+ a.borderBottom = `${arrowSize}px solid transparent`
95
+ break
96
+ }
97
+
98
+ tooltipStyle.value = s
99
+ arrowStyle.value = a
100
+ }
101
+
102
+ function highlightTarget() {
103
+ if (!step.value) return
104
+ document.querySelectorAll('.m3-tour-highlight').forEach((el) => el.classList.remove('m3-tour-highlight'))
105
+ const el = document.querySelector(step.value.target)
106
+ el?.classList.add('m3-tour-highlight')
107
+ }
108
+
109
+ function clearHighlight() {
110
+ document.querySelectorAll('.m3-tour-highlight').forEach((el) => el.classList.remove('m3-tour-highlight'))
111
+ }
112
+
113
+ function goNext() {
114
+ if (isLast.value) {
115
+ close()
116
+ emit('finish')
117
+ } else {
118
+ currentStep.value++
119
+ }
120
+ }
121
+
122
+ function goPrev() {
123
+ if (!isFirst.value) currentStep.value--
124
+ }
125
+
126
+ function close() {
127
+ clearHighlight()
128
+ currentStep.value = 0
129
+ emit('update:modelValue', false)
130
+ }
131
+
132
+ watch([() => props.modelValue, currentStep], () => {
133
+ if (props.modelValue) {
134
+ nextTick(() => {
135
+ highlightTarget()
136
+ positionTooltip()
137
+ })
138
+ }
139
+ })
140
+
141
+ watch(() => props.modelValue, (v) => {
142
+ if (!v) clearHighlight()
143
+ })
144
+
145
+ onBeforeUnmount(clearHighlight)
146
+ </script>
147
+
148
+ <template>
149
+ <Teleport to="body">
150
+ <Transition name="m3-tour">
151
+ <div v-if="modelValue && step" class="fixed inset-0 z-[100]">
152
+ <!-- Overlay -->
153
+ <div class="absolute inset-0 bg-black/40" @click="close" />
154
+
155
+ <!-- Tooltip -->
156
+ <div
157
+ class="z-[101] w-80 rounded-xl bg-surface-container-high p-5 shadow-elevation-3"
158
+ :style="tooltipStyle"
159
+ >
160
+ <!-- Arrow -->
161
+ <div class="h-0 w-0" :style="arrowStyle" />
162
+
163
+ <!-- Step indicator -->
164
+ <div class="mb-2 flex items-center justify-between">
165
+ <span class="text-label-small text-on-surface-variant">
166
+ {{ currentStep + 1 }} / {{ steps.length }}
167
+ </span>
168
+ <button
169
+ type="button"
170
+ class="flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-on-surface-variant transition-colors hover:bg-on-surface/8"
171
+ @click="close"
172
+ >
173
+ <MIcon name="close" :size="16" />
174
+ </button>
175
+ </div>
176
+
177
+ <h3 class="mb-1 text-title-medium font-medium text-on-surface">{{ step.title }}</h3>
178
+ <p class="mb-4 text-body-medium text-on-surface-variant">{{ step.content }}</p>
179
+
180
+ <!-- Progress dots -->
181
+ <div class="mb-4 flex justify-center gap-1.5">
182
+ <div
183
+ v-for="(_, i) in steps"
184
+ :key="i"
185
+ class="h-1.5 rounded-full transition-all duration-200"
186
+ :class="i === currentStep ? 'w-6 bg-primary' : 'w-1.5 bg-outline-variant'"
187
+ />
188
+ </div>
189
+
190
+ <!-- Actions -->
191
+ <div class="flex justify-between">
192
+ <MButton
193
+ v-if="!isFirst"
194
+ variant="text"
195
+ @click="goPrev"
196
+ >
197
+ Anterior
198
+ </MButton>
199
+ <span v-else />
200
+ <MButton @click="goNext">
201
+ {{ isLast ? 'Finalizar' : 'Siguiente' }}
202
+ </MButton>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </Transition>
207
+ </Teleport>
208
+ </template>
209
+
210
+ <style>
211
+ .m3-tour-highlight {
212
+ position: relative;
213
+ z-index: 101 !important;
214
+ box-shadow: 0 0 0 4px var(--color-primary), 0 0 0 9999px rgba(0, 0, 0, 0.4);
215
+ border-radius: 8px;
216
+ }
217
+
218
+ .m3-tour-enter-active,
219
+ .m3-tour-leave-active {
220
+ transition: opacity 0.2s ease;
221
+ }
222
+ .m3-tour-enter-from,
223
+ .m3-tour-leave-to {
224
+ opacity: 0;
225
+ }
226
+ </style>
@@ -0,0 +1,181 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+ import MIcon from './MIcon.vue'
4
+ import MIconButton from './MIconButton.vue'
5
+ import MCheckbox from './MCheckbox.vue'
6
+
7
+ export interface TransferItem {
8
+ value: string | number
9
+ label: string
10
+ icon?: string
11
+ }
12
+
13
+ const props = withDefaults(defineProps<{
14
+ modelValue: (string | number)[]
15
+ items: TransferItem[]
16
+ sourceTitle?: string
17
+ targetTitle?: string
18
+ filterable?: boolean
19
+ }>(), { sourceTitle: 'Disponibles', targetTitle: 'Seleccionados', filterable: false })
20
+
21
+ const emit = defineEmits<{ 'update:modelValue': [(string | number)[]] }>()
22
+
23
+ const checkedSource = ref<Set<string | number>>(new Set())
24
+ const checkedTarget = ref<Set<string | number>>(new Set())
25
+ const sourceSearch = ref('')
26
+ const targetSearch = ref('')
27
+
28
+ const sourceItems = computed(() => {
29
+ const selected = new Set(props.modelValue)
30
+ let list = props.items.filter(i => !selected.has(i.value))
31
+ if (sourceSearch.value) {
32
+ const q = sourceSearch.value.toLowerCase()
33
+ list = list.filter(i => i.label.toLowerCase().includes(q))
34
+ }
35
+ return list
36
+ })
37
+
38
+ const targetItems = computed(() => {
39
+ const selected = new Set(props.modelValue)
40
+ let list = props.items.filter(i => selected.has(i.value))
41
+ if (targetSearch.value) {
42
+ const q = targetSearch.value.toLowerCase()
43
+ list = list.filter(i => i.label.toLowerCase().includes(q))
44
+ }
45
+ return list
46
+ })
47
+
48
+ function toggleSource(value: string | number) {
49
+ const s = new Set(checkedSource.value)
50
+ s.has(value) ? s.delete(value) : s.add(value)
51
+ checkedSource.value = s
52
+ }
53
+ function toggleTarget(value: string | number) {
54
+ const s = new Set(checkedTarget.value)
55
+ s.has(value) ? s.delete(value) : s.add(value)
56
+ checkedTarget.value = s
57
+ }
58
+
59
+ function moveRight() {
60
+ const next = [...props.modelValue, ...checkedSource.value]
61
+ emit('update:modelValue', next)
62
+ checkedSource.value = new Set()
63
+ }
64
+ function moveLeft() {
65
+ const remove = checkedTarget.value
66
+ emit('update:modelValue', props.modelValue.filter(v => !remove.has(v)))
67
+ checkedTarget.value = new Set()
68
+ }
69
+ function moveAllRight() {
70
+ const all = sourceItems.value.map(i => i.value)
71
+ emit('update:modelValue', [...props.modelValue, ...all])
72
+ checkedSource.value = new Set()
73
+ }
74
+ function moveAllLeft() {
75
+ const keep = targetItems.value.map(i => i.value)
76
+ emit('update:modelValue', props.modelValue.filter(v => !new Set(keep).has(v)))
77
+ checkedTarget.value = new Set()
78
+ }
79
+ </script>
80
+
81
+ <template>
82
+ <div class="flex items-stretch gap-2">
83
+ <!-- Source list -->
84
+ <div class="flex min-w-0 flex-1 flex-col overflow-hidden rounded-lg border border-outline-variant">
85
+ <div class="flex items-center justify-between border-b border-outline-variant bg-surface-container px-3 py-2">
86
+ <span class="text-label-large font-medium text-on-surface">{{ sourceTitle }}</span>
87
+ <span class="text-label-small text-on-surface-variant">{{ sourceItems.length }}</span>
88
+ </div>
89
+ <div v-if="filterable" class="border-b border-outline-variant px-3 py-2">
90
+ <input
91
+ v-model="sourceSearch"
92
+ type="text"
93
+ placeholder="Buscar..."
94
+ class="w-full bg-transparent text-body-medium text-on-surface outline-none placeholder:text-on-surface-variant/50"
95
+ />
96
+ </div>
97
+ <div class="flex-1 overflow-y-auto" style="max-height: 240px">
98
+ <button
99
+ v-for="item in sourceItems"
100
+ :key="item.value"
101
+ type="button"
102
+ class="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-on-surface/4"
103
+ @click="toggleSource(item.value)"
104
+ >
105
+ <MCheckbox :model-value="checkedSource.has(item.value)" @update:model-value="toggleSource(item.value)" />
106
+ <MIcon v-if="item.icon" :name="item.icon" :size="18" class="shrink-0 text-on-surface-variant" />
107
+ <span class="flex-1 truncate text-body-medium text-on-surface">{{ item.label }}</span>
108
+ </button>
109
+ <p v-if="!sourceItems.length" class="px-3 py-4 text-center text-body-small text-on-surface-variant">
110
+ Sin elementos
111
+ </p>
112
+ </div>
113
+ </div>
114
+
115
+ <!-- Transfer buttons -->
116
+ <div class="flex flex-col items-center justify-center gap-1">
117
+ <MIconButton
118
+ icon="keyboard_double_arrow_right"
119
+ label="Mover todos a la derecha"
120
+ :size="36"
121
+ :disabled="!sourceItems.length"
122
+ @click="moveAllRight"
123
+ />
124
+ <MIconButton
125
+ icon="chevron_right"
126
+ label="Mover seleccionados a la derecha"
127
+ variant="tonal"
128
+ :size="36"
129
+ :disabled="!checkedSource.size"
130
+ @click="moveRight"
131
+ />
132
+ <MIconButton
133
+ icon="chevron_left"
134
+ label="Mover seleccionados a la izquierda"
135
+ variant="tonal"
136
+ :size="36"
137
+ :disabled="!checkedTarget.size"
138
+ @click="moveLeft"
139
+ />
140
+ <MIconButton
141
+ icon="keyboard_double_arrow_left"
142
+ label="Mover todos a la izquierda"
143
+ :size="36"
144
+ :disabled="!targetItems.length"
145
+ @click="moveAllLeft"
146
+ />
147
+ </div>
148
+
149
+ <!-- Target list -->
150
+ <div class="flex min-w-0 flex-1 flex-col overflow-hidden rounded-lg border border-outline-variant">
151
+ <div class="flex items-center justify-between border-b border-outline-variant bg-surface-container px-3 py-2">
152
+ <span class="text-label-large font-medium text-on-surface">{{ targetTitle }}</span>
153
+ <span class="text-label-small text-on-surface-variant">{{ targetItems.length }}</span>
154
+ </div>
155
+ <div v-if="filterable" class="border-b border-outline-variant px-3 py-2">
156
+ <input
157
+ v-model="targetSearch"
158
+ type="text"
159
+ placeholder="Buscar..."
160
+ class="w-full bg-transparent text-body-medium text-on-surface outline-none placeholder:text-on-surface-variant/50"
161
+ />
162
+ </div>
163
+ <div class="flex-1 overflow-y-auto" style="max-height: 240px">
164
+ <button
165
+ v-for="item in targetItems"
166
+ :key="item.value"
167
+ type="button"
168
+ class="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-on-surface/4"
169
+ @click="toggleTarget(item.value)"
170
+ >
171
+ <MCheckbox :model-value="checkedTarget.has(item.value)" @update:model-value="toggleTarget(item.value)" />
172
+ <MIcon v-if="item.icon" :name="item.icon" :size="18" class="shrink-0 text-on-surface-variant" />
173
+ <span class="flex-1 truncate text-body-medium text-on-surface">{{ item.label }}</span>
174
+ </button>
175
+ <p v-if="!targetItems.length" class="px-3 py-4 text-center text-body-small text-on-surface-variant">
176
+ Sin elementos
177
+ </p>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </template>
@@ -0,0 +1,164 @@
1
+ <script setup lang="ts">
2
+ import { computed, provide, ref, type Ref } from 'vue'
3
+ import MTreeNode from './_MTreeNode.vue'
4
+ import MIcon from './MIcon.vue'
5
+
6
+ // ── Public types ────────────────────────────────────────────────────────────
7
+
8
+ export interface TreeNode {
9
+ id: string | number
10
+ label: string
11
+ icon?: string
12
+ children?: TreeNode[]
13
+ disabled?: boolean
14
+ [key: string]: unknown
15
+ }
16
+
17
+ /** Shape injected into every _MTreeNode via provide/inject. */
18
+ export interface TreeContext {
19
+ selected: Ref<string | number | null>
20
+ checkedSet: Ref<Set<string | number>>
21
+ expandedIds: Ref<Set<string | number>>
22
+ checkable: Ref<boolean>
23
+ selectNode: (node: TreeNode) => void
24
+ toggleExpand: (id: string | number) => void
25
+ toggleCheck: (node: TreeNode) => void
26
+ getDescendantIds: (node: TreeNode) => (string | number)[]
27
+ getLeafIds: (node: TreeNode) => (string | number)[]
28
+ }
29
+
30
+ // ── Props & emits ───────────────────────────────────────────────────────────
31
+
32
+ const props = withDefaults(
33
+ defineProps<{
34
+ nodes: TreeNode[]
35
+ /** Currently selected node id (single-select). */
36
+ selected?: string | number | null
37
+ /** Checked node ids (checkable multi-select). */
38
+ checked?: (string | number)[]
39
+ /** Show checkboxes with cascade selection. */
40
+ checkable?: boolean
41
+ /**
42
+ * Which nodes start expanded.
43
+ * 'all' | 'none' | array of ids (default: 'none').
44
+ */
45
+ defaultExpanded?: (string | number)[] | 'all' | 'none'
46
+ emptyText?: string
47
+ }>(),
48
+ {
49
+ selected: null,
50
+ checked: () => [],
51
+ checkable: false,
52
+ defaultExpanded: 'none',
53
+ emptyText: 'Sin elementos',
54
+ },
55
+ )
56
+
57
+ const emit = defineEmits<{
58
+ 'update:selected': [string | number | null]
59
+ 'update:checked': [(string | number)[]]
60
+ 'node-click': [TreeNode]
61
+ }>()
62
+
63
+ // ── Helpers ─────────────────────────────────────────────────────────────────
64
+
65
+ function getDescendantIds(node: TreeNode): (string | number)[] {
66
+ return [node.id, ...(node.children ?? []).flatMap(getDescendantIds)]
67
+ }
68
+
69
+ function getLeafIds(node: TreeNode): (string | number)[] {
70
+ if (!node.children?.length) return [node.id]
71
+ return node.children.flatMap(getLeafIds)
72
+ }
73
+
74
+ function getAllIds(nodes: TreeNode[]): (string | number)[] {
75
+ return nodes.flatMap((n) => getDescendantIds(n))
76
+ }
77
+
78
+ // ── Expand state ────────────────────────────────────────────────────────────
79
+
80
+ function buildInitialExpanded(): Set<string | number> {
81
+ if (props.defaultExpanded === 'all') return new Set(getAllIds(props.nodes))
82
+ if (props.defaultExpanded === 'none') return new Set()
83
+ return new Set(props.defaultExpanded)
84
+ }
85
+
86
+ const expandedIds = ref<Set<string | number>>(buildInitialExpanded())
87
+
88
+ function toggleExpand(id: string | number) {
89
+ const next = new Set(expandedIds.value)
90
+ if (next.has(id)) next.delete(id)
91
+ else next.add(id)
92
+ expandedIds.value = next
93
+ }
94
+
95
+ // ── Selection ───────────────────────────────────────────────────────────────
96
+
97
+ const selectedRef = computed(() => props.selected ?? null)
98
+
99
+ function selectNode(node: TreeNode) {
100
+ emit('update:selected', selectedRef.value === node.id ? null : node.id)
101
+ emit('node-click', node)
102
+ }
103
+
104
+ // ── Checkable ───────────────────────────────────────────────────────────────
105
+
106
+ const checkedSet = computed(() => new Set(props.checked))
107
+
108
+ function toggleCheck(node: TreeNode) {
109
+ const leafIds = getLeafIds(node)
110
+ const allLeafsChecked = leafIds.every((id) => checkedSet.value.has(id))
111
+ const next = new Set(props.checked)
112
+ if (allLeafsChecked) {
113
+ // Remove leaf ids + clean up any stale branch ids
114
+ getDescendantIds(node).forEach((id) => next.delete(id))
115
+ } else {
116
+ leafIds.forEach((id) => next.add(id))
117
+ }
118
+ emit('update:checked', [...next])
119
+ }
120
+
121
+ // ── Provide context ─────────────────────────────────────────────────────────
122
+
123
+ provide<TreeContext>('m-tree', {
124
+ selected: selectedRef,
125
+ checkedSet,
126
+ expandedIds,
127
+ checkable: computed(() => props.checkable),
128
+ selectNode,
129
+ toggleExpand,
130
+ toggleCheck,
131
+ getDescendantIds,
132
+ getLeafIds,
133
+ })
134
+
135
+ // ── Expose expand/collapse utilities ────────────────────────────────────────
136
+
137
+ function expandAll() { expandedIds.value = new Set(getAllIds(props.nodes)) }
138
+ function collapseAll() { expandedIds.value = new Set() }
139
+
140
+ defineExpose({ expandAll, collapseAll })
141
+ </script>
142
+
143
+ <template>
144
+ <div role="tree" class="flex flex-col">
145
+ <template v-if="nodes.length">
146
+ <MTreeNode
147
+ v-for="node in nodes"
148
+ :key="node.id"
149
+ :node="node"
150
+ :depth="0"
151
+ >
152
+ <!-- Forward all slots down the recursive tree -->
153
+ <template v-for="(_, name) in $slots" #[name]="slotProps">
154
+ <slot :name="name" v-bind="slotProps ?? {}" />
155
+ </template>
156
+ </MTreeNode>
157
+ </template>
158
+
159
+ <div v-else class="flex flex-col items-center gap-2 py-10 text-on-surface-variant">
160
+ <MIcon name="account_tree" :size="32" class="opacity-30" />
161
+ <p class="text-body-medium">{{ emptyText }}</p>
162
+ </div>
163
+ </div>
164
+ </template>