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