@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,244 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useToast } from "../composables/useToast";
|
|
4
|
+
import MIcon from "./MIcon.vue";
|
|
5
|
+
|
|
6
|
+
const { toasts, position, dismiss } = useToast();
|
|
7
|
+
|
|
8
|
+
const isTop = computed(() => position.value.startsWith("top"));
|
|
9
|
+
|
|
10
|
+
const containerClass = computed(() => {
|
|
11
|
+
const base = "pointer-events-none fixed z-[300] flex flex-col";
|
|
12
|
+
switch (position.value) {
|
|
13
|
+
case "top-left":
|
|
14
|
+
return `${base} top-4 left-4 items-start`;
|
|
15
|
+
case "top-center":
|
|
16
|
+
return `${base} top-4 left-1/2 -translate-x-1/2 items-center`;
|
|
17
|
+
case "top-right":
|
|
18
|
+
return `${base} top-4 right-4 items-end`;
|
|
19
|
+
case "bottom-left":
|
|
20
|
+
return `${base} bottom-4 left-4 items-start`;
|
|
21
|
+
case "bottom-right":
|
|
22
|
+
return `${base} bottom-4 right-4 items-end`;
|
|
23
|
+
default:
|
|
24
|
+
return `${base} bottom-4 left-1/2 -translate-x-1/2 items-center`;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
type VariantStyle = {
|
|
29
|
+
container: string;
|
|
30
|
+
icon: string;
|
|
31
|
+
iconName: string;
|
|
32
|
+
action: string;
|
|
33
|
+
close: string;
|
|
34
|
+
progress: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const variantStyles: Record<string, VariantStyle> = {
|
|
38
|
+
info: {
|
|
39
|
+
// secondary-container tokens are adaptive light/dark out of the box
|
|
40
|
+
container:
|
|
41
|
+
"bg-secondary-container text-on-secondary-container ring-1 ring-inset ring-on-secondary-container/8",
|
|
42
|
+
icon: "text-on-secondary-container/70",
|
|
43
|
+
iconName: "info",
|
|
44
|
+
action: "text-on-secondary-container hover:bg-on-secondary-container/10",
|
|
45
|
+
close: "text-on-secondary-container/60 hover:bg-on-secondary-container/10",
|
|
46
|
+
progress: "bg-on-secondary-container/25",
|
|
47
|
+
},
|
|
48
|
+
success: {
|
|
49
|
+
container:
|
|
50
|
+
"bg-[#dcfce7] text-[#14532d] ring-1 ring-inset ring-[#14532d]/10 dark:bg-[#052e16] dark:text-[#bbf7d0] dark:ring-white/8",
|
|
51
|
+
icon: "text-[#16a34a] dark:text-[#4ade80]",
|
|
52
|
+
iconName: "check_circle",
|
|
53
|
+
action: "text-[#166534] hover:bg-[#14532d]/10 dark:text-[#86efac] dark:hover:bg-white/10",
|
|
54
|
+
close: "text-[#14532d]/50 hover:bg-[#14532d]/10 dark:text-[#bbf7d0]/50 dark:hover:bg-white/10",
|
|
55
|
+
progress: "bg-[#16a34a]/35 dark:bg-[#4ade80]/30",
|
|
56
|
+
},
|
|
57
|
+
warning: {
|
|
58
|
+
container:
|
|
59
|
+
"bg-[#fefce8] text-[#713f12] ring-1 ring-inset ring-[#713f12]/10 dark:bg-[#2d1a00] dark:text-[#fde68a] dark:ring-white/8",
|
|
60
|
+
icon: "text-[#d97706] dark:text-[#fcd34d]",
|
|
61
|
+
iconName: "warning",
|
|
62
|
+
action: "text-[#92400e] hover:bg-[#713f12]/10 dark:text-[#fcd34d] dark:hover:bg-white/10",
|
|
63
|
+
close: "text-[#713f12]/50 hover:bg-[#713f12]/10 dark:text-[#fde68a]/50 dark:hover:bg-white/10",
|
|
64
|
+
progress: "bg-[#d97706]/35 dark:bg-[#fcd34d]/30",
|
|
65
|
+
},
|
|
66
|
+
error: {
|
|
67
|
+
// error-container tokens are adaptive light/dark out of the box
|
|
68
|
+
container:
|
|
69
|
+
"bg-error-container text-on-error-container ring-1 ring-inset ring-on-error-container/8",
|
|
70
|
+
icon: "text-error dark:text-[#fca5a5]",
|
|
71
|
+
iconName: "error",
|
|
72
|
+
action: "text-on-error-container hover:bg-on-error-container/10",
|
|
73
|
+
close: "text-on-error-container/60 hover:bg-on-error-container/10",
|
|
74
|
+
progress: "bg-on-error-container/25",
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getVariantStyle = (variant: string): VariantStyle =>
|
|
79
|
+
variantStyles[variant] ?? variantStyles.info!;
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<div :class="containerClass">
|
|
84
|
+
<TransitionGroup :name="isTop ? 'm3-toast-top' : 'm3-toast-bot'">
|
|
85
|
+
<div v-for="t in toasts" :key="t.id" class="toast-row w-full min-w-64 max-w-xs">
|
|
86
|
+
<div
|
|
87
|
+
class="toast-inner pointer-events-auto relative flex items-center gap-3 overflow-hidden rounded-2xl px-4 py-4 shadow-elevation-2"
|
|
88
|
+
:class="getVariantStyle(t.variant).container"
|
|
89
|
+
>
|
|
90
|
+
<MIcon
|
|
91
|
+
:name="getVariantStyle(t.variant).iconName"
|
|
92
|
+
:size="20"
|
|
93
|
+
class="shrink-0"
|
|
94
|
+
:class="getVariantStyle(t.variant).icon"
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
<p class="flex-1 text-body-medium leading-snug">{{ t.message }}</p>
|
|
98
|
+
|
|
99
|
+
<div class="flex shrink-0 items-center gap-0.5">
|
|
100
|
+
<button
|
|
101
|
+
v-if="t.action"
|
|
102
|
+
type="button"
|
|
103
|
+
class="cursor-pointer rounded px-2 py-1 text-label-medium font-semibold transition-colors"
|
|
104
|
+
:class="getVariantStyle(t.variant).action"
|
|
105
|
+
@click="
|
|
106
|
+
() => {
|
|
107
|
+
t.action!.onClick();
|
|
108
|
+
dismiss(t.id);
|
|
109
|
+
}
|
|
110
|
+
"
|
|
111
|
+
>
|
|
112
|
+
{{ t.action.label }}
|
|
113
|
+
</button>
|
|
114
|
+
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full transition-colors"
|
|
118
|
+
:class="getVariantStyle(t.variant).close"
|
|
119
|
+
aria-label="Cerrar"
|
|
120
|
+
@click="dismiss(t.id)"
|
|
121
|
+
>
|
|
122
|
+
<MIcon name="close" :size="18" />
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- Countdown progress bar -->
|
|
127
|
+
<div
|
|
128
|
+
v-if="t.duration > 0"
|
|
129
|
+
class="absolute right-0 bottom-0 left-0 h-0.5 origin-left"
|
|
130
|
+
:class="getVariantStyle(t.variant).progress"
|
|
131
|
+
:style="{ animation: `m3-toast-progress ${t.duration}ms linear forwards` }"
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</TransitionGroup>
|
|
136
|
+
</div>
|
|
137
|
+
</template>
|
|
138
|
+
|
|
139
|
+
<style scoped>
|
|
140
|
+
/*
|
|
141
|
+
.toast-row is a grid container — animating grid-template-rows: 1fr → 0fr
|
|
142
|
+
collapses height smoothly without position:absolute, so sibling toasts
|
|
143
|
+
shift up gracefully instead of jumping.
|
|
144
|
+
*/
|
|
145
|
+
.toast-row {
|
|
146
|
+
display: grid;
|
|
147
|
+
grid-template-rows: 1fr;
|
|
148
|
+
padding-bottom: 8px;
|
|
149
|
+
}
|
|
150
|
+
.toast-row > .toast-inner {
|
|
151
|
+
min-height: 0; /* required for 0fr collapse */
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ─── Bottom toasts ─────────────────────────────────────────────── */
|
|
155
|
+
.m3-toast-bot-enter-active {
|
|
156
|
+
transition:
|
|
157
|
+
grid-template-rows 220ms cubic-bezier(0.2, 0, 0, 1),
|
|
158
|
+
padding-bottom 220ms cubic-bezier(0.2, 0, 0, 1);
|
|
159
|
+
overflow: hidden;
|
|
160
|
+
}
|
|
161
|
+
.m3-toast-bot-enter-active > .toast-inner {
|
|
162
|
+
transition:
|
|
163
|
+
opacity 180ms ease,
|
|
164
|
+
transform 220ms cubic-bezier(0.2, 0, 0, 1);
|
|
165
|
+
}
|
|
166
|
+
.m3-toast-bot-enter-from {
|
|
167
|
+
grid-template-rows: 0fr;
|
|
168
|
+
padding-bottom: 0;
|
|
169
|
+
}
|
|
170
|
+
.m3-toast-bot-enter-from > .toast-inner {
|
|
171
|
+
opacity: 0;
|
|
172
|
+
transform: translateY(20px) scale(0.94);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.m3-toast-bot-leave-active {
|
|
176
|
+
transition:
|
|
177
|
+
grid-template-rows 300ms cubic-bezier(0.2, 0, 0, 1),
|
|
178
|
+
padding-bottom 300ms cubic-bezier(0.2, 0, 0, 1);
|
|
179
|
+
overflow: hidden;
|
|
180
|
+
}
|
|
181
|
+
.m3-toast-bot-leave-active > .toast-inner {
|
|
182
|
+
transition:
|
|
183
|
+
opacity 180ms ease,
|
|
184
|
+
transform 180ms ease;
|
|
185
|
+
}
|
|
186
|
+
.m3-toast-bot-leave-to {
|
|
187
|
+
grid-template-rows: 0fr;
|
|
188
|
+
padding-bottom: 0;
|
|
189
|
+
}
|
|
190
|
+
.m3-toast-bot-leave-to > .toast-inner {
|
|
191
|
+
opacity: 0;
|
|
192
|
+
transform: scale(0.92);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* ─── Top toasts ────────────────────────────────────────────────── */
|
|
196
|
+
.m3-toast-top-enter-active {
|
|
197
|
+
transition:
|
|
198
|
+
grid-template-rows 220ms cubic-bezier(0.2, 0, 0, 1),
|
|
199
|
+
padding-bottom 220ms cubic-bezier(0.2, 0, 0, 1);
|
|
200
|
+
overflow: hidden;
|
|
201
|
+
}
|
|
202
|
+
.m3-toast-top-enter-active > .toast-inner {
|
|
203
|
+
transition:
|
|
204
|
+
opacity 180ms ease,
|
|
205
|
+
transform 220ms cubic-bezier(0.2, 0, 0, 1);
|
|
206
|
+
}
|
|
207
|
+
.m3-toast-top-enter-from {
|
|
208
|
+
grid-template-rows: 0fr;
|
|
209
|
+
padding-bottom: 0;
|
|
210
|
+
}
|
|
211
|
+
.m3-toast-top-enter-from > .toast-inner {
|
|
212
|
+
opacity: 0;
|
|
213
|
+
transform: translateY(-20px) scale(0.94);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.m3-toast-top-leave-active {
|
|
217
|
+
transition:
|
|
218
|
+
grid-template-rows 300ms cubic-bezier(0.2, 0, 0, 1),
|
|
219
|
+
padding-bottom 300ms cubic-bezier(0.2, 0, 0, 1);
|
|
220
|
+
overflow: hidden;
|
|
221
|
+
}
|
|
222
|
+
.m3-toast-top-leave-active > .toast-inner {
|
|
223
|
+
transition:
|
|
224
|
+
opacity 180ms ease,
|
|
225
|
+
transform 180ms ease;
|
|
226
|
+
}
|
|
227
|
+
.m3-toast-top-leave-to {
|
|
228
|
+
grid-template-rows: 0fr;
|
|
229
|
+
padding-bottom: 0;
|
|
230
|
+
}
|
|
231
|
+
.m3-toast-top-leave-to > .toast-inner {
|
|
232
|
+
opacity: 0;
|
|
233
|
+
transform: scale(0.92);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@keyframes m3-toast-progress {
|
|
237
|
+
from {
|
|
238
|
+
transform: scaleX(1);
|
|
239
|
+
}
|
|
240
|
+
to {
|
|
241
|
+
transform: scaleX(0);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
</style>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
size?: number;
|
|
7
|
+
wavy?: boolean;
|
|
8
|
+
}>(),
|
|
9
|
+
{ size: 20, wavy: false },
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const STROKE = 3;
|
|
13
|
+
const BUMPS = 9;
|
|
14
|
+
|
|
15
|
+
// amp fraction of r = 0.25 → max radius = r * 1.25
|
|
16
|
+
// Constrain so that max_r + STROKE/2 ≤ size/2 - 1 (1px margin from edge)
|
|
17
|
+
const r = computed(() => (props.size / 2 - 1 - STROKE / 2) / 1.25);
|
|
18
|
+
const cx = computed(() => props.size / 2);
|
|
19
|
+
|
|
20
|
+
// Build the full bumpy-circle path and its total length.
|
|
21
|
+
const wavyData = computed(() => {
|
|
22
|
+
const CX = cx.value;
|
|
23
|
+
const R = r.value;
|
|
24
|
+
const amp = R * 0.08;
|
|
25
|
+
const segs = BUMPS * 24; // smooth curve
|
|
26
|
+
|
|
27
|
+
const pts: string[] = [];
|
|
28
|
+
let len = 0;
|
|
29
|
+
let px = 0,
|
|
30
|
+
py = 0;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i <= segs; i++) {
|
|
33
|
+
const theta = (2 * Math.PI * i) / segs - Math.PI / 2;
|
|
34
|
+
const rr = R + amp * Math.sin(BUMPS * theta);
|
|
35
|
+
const x = CX + rr * Math.cos(theta);
|
|
36
|
+
const y = CX + rr * Math.sin(theta);
|
|
37
|
+
if (i > 0) len += Math.sqrt((x - px) ** 2 + (y - py) ** 2);
|
|
38
|
+
pts.push(`${i === 0 ? "M" : "L"}${x.toFixed(2)},${y.toFixed(2)}`);
|
|
39
|
+
px = x;
|
|
40
|
+
py = y;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Visible arc ~58% of the circumference, gap fills the rest.
|
|
44
|
+
const visible = len * 0.58;
|
|
45
|
+
const gap = len - visible;
|
|
46
|
+
const dash = `${visible.toFixed(1)} ${gap.toFixed(1)}`;
|
|
47
|
+
|
|
48
|
+
// The wave "travels" by shifting dashoffset over exactly one full length,
|
|
49
|
+
// so the crests slide around the path independently of the rotation.
|
|
50
|
+
return { path: pts.join("") + "Z", dash, len: len.toFixed(1) };
|
|
51
|
+
});
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<span
|
|
56
|
+
class="inline-flex shrink-0 items-center justify-center"
|
|
57
|
+
:style="{ width: `${size}px`, height: `${size}px` }"
|
|
58
|
+
role="status"
|
|
59
|
+
aria-label="Cargando"
|
|
60
|
+
>
|
|
61
|
+
<!-- Standard circular spinner -->
|
|
62
|
+
<span
|
|
63
|
+
v-if="!wavy"
|
|
64
|
+
class="block h-full w-full animate-spin rounded-full border-2 border-current border-t-transparent"
|
|
65
|
+
/>
|
|
66
|
+
|
|
67
|
+
<!-- Wavy spinner (M3 Expressive): the whole shape rotates AND the wave
|
|
68
|
+
travels along the stroke via dashoffset, giving the snake-like flow. -->
|
|
69
|
+
<svg
|
|
70
|
+
v-else
|
|
71
|
+
:width="size"
|
|
72
|
+
:height="size"
|
|
73
|
+
:viewBox="`0 0 ${size} ${size}`"
|
|
74
|
+
fill="none"
|
|
75
|
+
class="animate-[m3-wavy-spin_2.8s_linear_infinite]"
|
|
76
|
+
:style="`transform-origin: ${cx}px ${cx}px`"
|
|
77
|
+
>
|
|
78
|
+
<path
|
|
79
|
+
:d="wavyData.path"
|
|
80
|
+
stroke="currentColor"
|
|
81
|
+
:stroke-width="STROKE"
|
|
82
|
+
stroke-linecap="round"
|
|
83
|
+
:stroke-dasharray="wavyData.dash"
|
|
84
|
+
class="animate-[m3-wavy-travel_2s_linear_infinite]"
|
|
85
|
+
:style="{ '--m3-wave-len': wavyData.len }"
|
|
86
|
+
/>
|
|
87
|
+
</svg>
|
|
88
|
+
</span>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<style>
|
|
92
|
+
/* The SVG element rotates the whole bumpy circle. */
|
|
93
|
+
@keyframes m3-wavy-spin {
|
|
94
|
+
from {
|
|
95
|
+
transform: rotate(0deg);
|
|
96
|
+
}
|
|
97
|
+
to {
|
|
98
|
+
transform: rotate(360deg);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* The stroke's dashoffset slides by one full path length, so the crests
|
|
103
|
+
appear to crawl along the circle — the "snake" motion of M3 Expressive.
|
|
104
|
+
Negative direction makes the wave travel forward relative to the spin. */
|
|
105
|
+
@keyframes m3-wavy-travel {
|
|
106
|
+
from {
|
|
107
|
+
stroke-dashoffset: 0;
|
|
108
|
+
}
|
|
109
|
+
to {
|
|
110
|
+
stroke-dashoffset: calc(var(--m3-wave-len) * -1px);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@media (prefers-reduced-motion: reduce) {
|
|
115
|
+
.animate-\[m3-wavy-spin_2\.8s_linear_infinite\] {
|
|
116
|
+
animation: m3-wavy-spin 2.8s linear infinite;
|
|
117
|
+
}
|
|
118
|
+
.animate-\[m3-wavy-travel_2s_linear_infinite\] {
|
|
119
|
+
animation: none !important;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onBeforeUnmount } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
direction?: 'horizontal' | 'vertical'
|
|
7
|
+
initialSplit?: number
|
|
8
|
+
min?: number
|
|
9
|
+
max?: number
|
|
10
|
+
}>(),
|
|
11
|
+
{ direction: 'horizontal', initialSplit: 50, min: 10, max: 90 },
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const split = ref(props.initialSplit)
|
|
15
|
+
const dragging = ref(false)
|
|
16
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
17
|
+
|
|
18
|
+
const isHorizontal = computed(() => props.direction === 'horizontal')
|
|
19
|
+
|
|
20
|
+
const panelAStyle = computed(() =>
|
|
21
|
+
isHorizontal.value
|
|
22
|
+
? { width: `${split.value}%` }
|
|
23
|
+
: { height: `${split.value}%` },
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const panelBStyle = computed(() =>
|
|
27
|
+
isHorizontal.value
|
|
28
|
+
? { width: `${100 - split.value}%` }
|
|
29
|
+
: { height: `${100 - split.value}%` },
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
function onPointerDown(e: PointerEvent) {
|
|
33
|
+
dragging.value = true
|
|
34
|
+
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function onPointerMove(e: PointerEvent) {
|
|
38
|
+
if (!dragging.value || !containerRef.value) return
|
|
39
|
+
|
|
40
|
+
const rect = containerRef.value.getBoundingClientRect()
|
|
41
|
+
let pct: number
|
|
42
|
+
|
|
43
|
+
if (isHorizontal.value) {
|
|
44
|
+
pct = ((e.clientX - rect.left) / rect.width) * 100
|
|
45
|
+
} else {
|
|
46
|
+
pct = ((e.clientY - rect.top) / rect.height) * 100
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
split.value = Math.min(props.max, Math.max(props.min, pct))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function onPointerUp() {
|
|
53
|
+
dragging.value = false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onBeforeUnmount(() => {
|
|
57
|
+
dragging.value = false
|
|
58
|
+
})
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<div
|
|
63
|
+
ref="containerRef"
|
|
64
|
+
class="flex overflow-hidden"
|
|
65
|
+
:class="[
|
|
66
|
+
isHorizontal ? 'flex-row' : 'flex-col',
|
|
67
|
+
dragging && 'select-none',
|
|
68
|
+
]"
|
|
69
|
+
style="height: 100%"
|
|
70
|
+
>
|
|
71
|
+
<div class="overflow-auto" :style="panelAStyle">
|
|
72
|
+
<slot name="first" />
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div
|
|
76
|
+
class="z-10 flex shrink-0 items-center justify-center transition-colors"
|
|
77
|
+
:class="[
|
|
78
|
+
isHorizontal
|
|
79
|
+
? 'w-2 cursor-col-resize flex-col'
|
|
80
|
+
: 'h-2 cursor-row-resize flex-row',
|
|
81
|
+
dragging ? 'bg-primary/20' : 'bg-outline-variant/40 hover:bg-primary/12',
|
|
82
|
+
]"
|
|
83
|
+
@pointerdown="onPointerDown"
|
|
84
|
+
@pointermove="onPointerMove"
|
|
85
|
+
@pointerup="onPointerUp"
|
|
86
|
+
>
|
|
87
|
+
<div
|
|
88
|
+
class="rounded-full bg-outline"
|
|
89
|
+
:class="isHorizontal ? 'h-6 w-1' : 'h-1 w-6'"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="overflow-auto" :style="panelBStyle">
|
|
94
|
+
<slot name="second" />
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</template>
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
import MSpinner from './MSpinner.vue'
|
|
5
|
+
|
|
6
|
+
export interface SpotlightResult {
|
|
7
|
+
id: string | number
|
|
8
|
+
title: string
|
|
9
|
+
description?: string
|
|
10
|
+
icon?: string
|
|
11
|
+
category?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(
|
|
15
|
+
defineProps<{
|
|
16
|
+
modelValue: boolean
|
|
17
|
+
results?: SpotlightResult[]
|
|
18
|
+
placeholder?: string
|
|
19
|
+
loading?: boolean
|
|
20
|
+
noResultsText?: string
|
|
21
|
+
hotkey?: string
|
|
22
|
+
debounce?: number
|
|
23
|
+
}>(),
|
|
24
|
+
{
|
|
25
|
+
results: () => [],
|
|
26
|
+
placeholder: 'Buscar...',
|
|
27
|
+
loading: false,
|
|
28
|
+
noResultsText: 'No se encontraron resultados',
|
|
29
|
+
hotkey: '/',
|
|
30
|
+
debounce: 0,
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits<{
|
|
35
|
+
'update:modelValue': [boolean]
|
|
36
|
+
search: [string]
|
|
37
|
+
select: [SpotlightResult]
|
|
38
|
+
}>()
|
|
39
|
+
|
|
40
|
+
const query = ref('')
|
|
41
|
+
const activeIndex = ref(0)
|
|
42
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
43
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
44
|
+
|
|
45
|
+
const hasQuery = computed(() => query.value.trim().length > 0)
|
|
46
|
+
|
|
47
|
+
const grouped = computed(() => {
|
|
48
|
+
const map = new Map<string, SpotlightResult[]>()
|
|
49
|
+
for (const r of props.results) {
|
|
50
|
+
const cat = r.category ?? ''
|
|
51
|
+
if (!map.has(cat)) map.set(cat, [])
|
|
52
|
+
map.get(cat)!.push(r)
|
|
53
|
+
}
|
|
54
|
+
return map
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
function close() {
|
|
58
|
+
query.value = ''
|
|
59
|
+
activeIndex.value = 0
|
|
60
|
+
emit('update:modelValue', false)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function selectResult(result: SpotlightResult) {
|
|
64
|
+
emit('select', result)
|
|
65
|
+
close()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function emitSearch() {
|
|
69
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
70
|
+
if (props.debounce > 0) {
|
|
71
|
+
debounceTimer = setTimeout(() => emit('search', query.value), props.debounce)
|
|
72
|
+
} else {
|
|
73
|
+
emit('search', query.value)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onKeydown(e: KeyboardEvent) {
|
|
78
|
+
const len = props.results.length
|
|
79
|
+
if (e.key === 'ArrowDown') {
|
|
80
|
+
e.preventDefault()
|
|
81
|
+
activeIndex.value = len ? (activeIndex.value + 1) % len : 0
|
|
82
|
+
scrollToActive()
|
|
83
|
+
} else if (e.key === 'ArrowUp') {
|
|
84
|
+
e.preventDefault()
|
|
85
|
+
activeIndex.value = len ? (activeIndex.value - 1 + len) % len : 0
|
|
86
|
+
scrollToActive()
|
|
87
|
+
} else if (e.key === 'Enter' && len) {
|
|
88
|
+
e.preventDefault()
|
|
89
|
+
selectResult(props.results[activeIndex.value]!)
|
|
90
|
+
} else if (e.key === 'Escape') {
|
|
91
|
+
close()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function scrollToActive() {
|
|
96
|
+
nextTick(() => {
|
|
97
|
+
const el = document.querySelector('[data-spot-active="true"]')
|
|
98
|
+
el?.scrollIntoView({ block: 'nearest' })
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function onGlobalKeydown(e: KeyboardEvent) {
|
|
103
|
+
const tag = (e.target as HTMLElement).tagName
|
|
104
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement).isContentEditable) return
|
|
105
|
+
if (e.key === props.hotkey && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
emit('update:modelValue', true)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
watch(
|
|
112
|
+
() => props.modelValue,
|
|
113
|
+
(open) => {
|
|
114
|
+
if (open) {
|
|
115
|
+
document.body.style.overflow = 'hidden'
|
|
116
|
+
nextTick(() => inputRef.value?.focus())
|
|
117
|
+
} else {
|
|
118
|
+
document.body.style.overflow = ''
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
watch(query, () => {
|
|
124
|
+
activeIndex.value = 0
|
|
125
|
+
emitSearch()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
onMounted(() => document.addEventListener('keydown', onGlobalKeydown))
|
|
129
|
+
onBeforeUnmount(() => {
|
|
130
|
+
document.removeEventListener('keydown', onGlobalKeydown)
|
|
131
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
132
|
+
})
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<template>
|
|
136
|
+
<Teleport to="body">
|
|
137
|
+
<Transition name="m3-spot">
|
|
138
|
+
<div
|
|
139
|
+
v-if="modelValue"
|
|
140
|
+
class="fixed inset-0 z-50 flex items-start justify-center bg-black/50 pt-[12vh]"
|
|
141
|
+
@click.self="close"
|
|
142
|
+
>
|
|
143
|
+
<div class="spot-box flex w-full max-w-xl flex-col overflow-hidden rounded-2xl bg-surface-container-high shadow-elevation-3">
|
|
144
|
+
<!-- Search bar -->
|
|
145
|
+
<div class="flex items-center gap-3 px-5 py-1">
|
|
146
|
+
<MIcon name="search" :size="24" class="shrink-0 text-primary" />
|
|
147
|
+
<input
|
|
148
|
+
ref="inputRef"
|
|
149
|
+
v-model="query"
|
|
150
|
+
type="text"
|
|
151
|
+
:placeholder="placeholder"
|
|
152
|
+
class="h-14 flex-1 bg-transparent text-title-medium text-on-surface outline-none placeholder:text-on-surface-variant/50"
|
|
153
|
+
@keydown="onKeydown"
|
|
154
|
+
/>
|
|
155
|
+
<MSpinner v-if="loading" :size="20" class="shrink-0 text-primary" />
|
|
156
|
+
<button
|
|
157
|
+
v-else-if="hasQuery"
|
|
158
|
+
type="button"
|
|
159
|
+
class="flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-full text-on-surface-variant hover:bg-on-surface/8"
|
|
160
|
+
@click="query = ''"
|
|
161
|
+
>
|
|
162
|
+
<MIcon name="close" :size="18" />
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<!-- Results -->
|
|
167
|
+
<div v-if="hasQuery" class="max-h-96 overflow-y-auto border-t border-outline-variant">
|
|
168
|
+
<template v-if="results.length">
|
|
169
|
+
<template v-for="[category, items] in grouped" :key="category">
|
|
170
|
+
<p v-if="category" class="px-5 pt-4 pb-1 text-label-small font-medium tracking-wide text-on-surface-variant uppercase">
|
|
171
|
+
{{ category }}
|
|
172
|
+
</p>
|
|
173
|
+
<button
|
|
174
|
+
v-for="item in items"
|
|
175
|
+
:key="item.id"
|
|
176
|
+
type="button"
|
|
177
|
+
:data-spot-active="results.indexOf(item) === activeIndex || undefined"
|
|
178
|
+
class="flex w-full cursor-pointer items-center gap-3 px-5 py-3 text-left transition-colors"
|
|
179
|
+
:class="results.indexOf(item) === activeIndex ? 'bg-primary/12' : 'hover:bg-on-surface/4'"
|
|
180
|
+
@click="selectResult(item)"
|
|
181
|
+
@pointerenter="activeIndex = results.indexOf(item)"
|
|
182
|
+
>
|
|
183
|
+
<div
|
|
184
|
+
v-if="item.icon"
|
|
185
|
+
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary-container"
|
|
186
|
+
>
|
|
187
|
+
<MIcon :name="item.icon" :size="20" class="text-on-primary-container" />
|
|
188
|
+
</div>
|
|
189
|
+
<div class="min-w-0 flex-1">
|
|
190
|
+
<p class="truncate text-body-medium text-on-surface">{{ item.title }}</p>
|
|
191
|
+
<p v-if="item.description" class="truncate text-body-small text-on-surface-variant">
|
|
192
|
+
{{ item.description }}
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
<MIcon name="arrow_forward" :size="16" class="shrink-0 text-on-surface-variant/40" />
|
|
196
|
+
</button>
|
|
197
|
+
</template>
|
|
198
|
+
</template>
|
|
199
|
+
<div v-else-if="!loading" class="flex flex-col items-center gap-2 py-10">
|
|
200
|
+
<MIcon name="search_off" :size="40" class="text-on-surface-variant/40" />
|
|
201
|
+
<p class="text-body-medium text-on-surface-variant">{{ noResultsText }}</p>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<!-- Hints -->
|
|
206
|
+
<div class="flex items-center gap-4 border-t border-outline-variant px-5 py-2">
|
|
207
|
+
<span class="flex items-center gap-1 text-label-small text-on-surface-variant">
|
|
208
|
+
<kbd class="rounded bg-surface-container px-1 py-0.5">↑↓</kbd> navegar
|
|
209
|
+
</span>
|
|
210
|
+
<span class="flex items-center gap-1 text-label-small text-on-surface-variant">
|
|
211
|
+
<kbd class="rounded bg-surface-container px-1 py-0.5">↵</kbd> abrir
|
|
212
|
+
</span>
|
|
213
|
+
<span class="flex items-center gap-1 text-label-small text-on-surface-variant">
|
|
214
|
+
<kbd class="rounded bg-surface-container px-1 py-0.5">esc</kbd> cerrar
|
|
215
|
+
</span>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</Transition>
|
|
220
|
+
</Teleport>
|
|
221
|
+
</template>
|
|
222
|
+
|
|
223
|
+
<style scoped>
|
|
224
|
+
.m3-spot-enter-active,
|
|
225
|
+
.m3-spot-leave-active {
|
|
226
|
+
transition: opacity 0.15s ease;
|
|
227
|
+
}
|
|
228
|
+
.m3-spot-enter-from,
|
|
229
|
+
.m3-spot-leave-to {
|
|
230
|
+
opacity: 0;
|
|
231
|
+
}
|
|
232
|
+
.m3-spot-enter-active .spot-box,
|
|
233
|
+
.m3-spot-leave-active .spot-box {
|
|
234
|
+
transition: transform 0.15s ease, opacity 0.15s ease;
|
|
235
|
+
}
|
|
236
|
+
.m3-spot-enter-from .spot-box {
|
|
237
|
+
transform: scale(0.96) translateY(-8px);
|
|
238
|
+
opacity: 0;
|
|
239
|
+
}
|
|
240
|
+
.m3-spot-leave-to .spot-box {
|
|
241
|
+
transform: scale(0.98);
|
|
242
|
+
opacity: 0;
|
|
243
|
+
}
|
|
244
|
+
</style>
|