@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,200 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
value?: number;
|
|
7
|
+
indeterminate?: boolean;
|
|
8
|
+
color?: "primary" | "secondary" | "tertiary" | "error";
|
|
9
|
+
variant?: "linear" | "wavy";
|
|
10
|
+
label?: string;
|
|
11
|
+
}>(),
|
|
12
|
+
{
|
|
13
|
+
color: "primary",
|
|
14
|
+
variant: "linear",
|
|
15
|
+
},
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const isIndeterminate = computed(() => props.indeterminate || props.value === undefined);
|
|
19
|
+
const clampedValue = computed(() => Math.min(100, Math.max(0, props.value ?? 0)));
|
|
20
|
+
|
|
21
|
+
const colorMap: Record<
|
|
22
|
+
"primary" | "secondary" | "tertiary" | "error",
|
|
23
|
+
{ bar: string; track: string; text: string }
|
|
24
|
+
> = {
|
|
25
|
+
primary: { bar: "bg-primary", track: "bg-primary-container", text: "text-primary" },
|
|
26
|
+
secondary: { bar: "bg-secondary", track: "bg-secondary-container", text: "text-secondary" },
|
|
27
|
+
tertiary: { bar: "bg-tertiary", track: "bg-tertiary-container", text: "text-tertiary" },
|
|
28
|
+
error: { bar: "bg-error", track: "bg-error-container", text: "text-error" },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ── Wave geometry ────────────────────────────────────────────────────────
|
|
32
|
+
// Smooth sine wave sampled as a single polyline path.
|
|
33
|
+
// Period = 20px (one full up-down cycle). We render a wide strip so that
|
|
34
|
+
// translating by exactly one period gives a seamless infinite scroll.
|
|
35
|
+
const PERIOD = 20; // px per full sine cycle
|
|
36
|
+
const AMP = 2.5; // amplitude (bar is 8px tall, mid at 4)
|
|
37
|
+
const MID = 4;
|
|
38
|
+
const VIEW_H = 8;
|
|
39
|
+
const PERIODS = 80; // total cycles → 1600px strip
|
|
40
|
+
const STEP = 1; // px sampling resolution
|
|
41
|
+
|
|
42
|
+
const waveWidth = PERIOD * PERIODS;
|
|
43
|
+
|
|
44
|
+
const wavePath = (() => {
|
|
45
|
+
let d = "";
|
|
46
|
+
for (let x = 0; x <= waveWidth; x += STEP) {
|
|
47
|
+
const y = MID - AMP * Math.sin((x / PERIOD) * Math.PI * 2);
|
|
48
|
+
d += (x === 0 ? "M" : "L") + x + "," + y.toFixed(2) + " ";
|
|
49
|
+
}
|
|
50
|
+
return d.trim();
|
|
51
|
+
})();
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<div class="flex flex-col gap-1">
|
|
56
|
+
<span v-if="label" class="text-label-small text-on-surface-variant">{{ label }}</span>
|
|
57
|
+
|
|
58
|
+
<!-- ── Linear variant ────────────────────────────────────────────────── -->
|
|
59
|
+
<div
|
|
60
|
+
v-if="variant === 'linear'"
|
|
61
|
+
class="relative h-1 w-full overflow-hidden rounded-full"
|
|
62
|
+
:class="colorMap[color].track"
|
|
63
|
+
role="progressbar"
|
|
64
|
+
:aria-valuenow="isIndeterminate ? undefined : clampedValue"
|
|
65
|
+
aria-valuemin="0"
|
|
66
|
+
aria-valuemax="100"
|
|
67
|
+
>
|
|
68
|
+
<div
|
|
69
|
+
v-if="!isIndeterminate"
|
|
70
|
+
class="h-full rounded-full transition-[width] duration-300 ease-in-out"
|
|
71
|
+
:class="colorMap[color].bar"
|
|
72
|
+
:style="{ width: `${clampedValue}%` }"
|
|
73
|
+
/>
|
|
74
|
+
<div
|
|
75
|
+
v-else
|
|
76
|
+
class="absolute inset-y-0 w-2/5 rounded-full animate-[m3-progress-indeterminate_1.6s_ease-in-out_infinite]"
|
|
77
|
+
:class="colorMap[color].bar"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- ── Wavy variant ───────────────────────────────────────────────────── -->
|
|
82
|
+
<div
|
|
83
|
+
v-else
|
|
84
|
+
class="relative h-2 w-full overflow-visible"
|
|
85
|
+
role="progressbar"
|
|
86
|
+
:aria-valuenow="isIndeterminate ? undefined : clampedValue"
|
|
87
|
+
aria-valuemin="0"
|
|
88
|
+
aria-valuemax="100"
|
|
89
|
+
>
|
|
90
|
+
<!-- DETERMINATE -->
|
|
91
|
+
<template v-if="!isIndeterminate">
|
|
92
|
+
<!-- Active (wavy) portion: clipped to value%, but the wave keeps flowing -->
|
|
93
|
+
<div
|
|
94
|
+
class="absolute inset-0 overflow-hidden"
|
|
95
|
+
:style="{
|
|
96
|
+
clipPath: `inset(0 ${100 - clampedValue}% 0 0)`,
|
|
97
|
+
transition: 'clip-path 300ms ease',
|
|
98
|
+
}"
|
|
99
|
+
>
|
|
100
|
+
<div
|
|
101
|
+
class="absolute top-0 left-0 h-full animate-[m3-wave-flow_0.8s_linear_infinite]"
|
|
102
|
+
:class="colorMap[color].text"
|
|
103
|
+
:style="{ width: `${waveWidth}px` }"
|
|
104
|
+
>
|
|
105
|
+
<svg
|
|
106
|
+
:width="waveWidth"
|
|
107
|
+
:height="VIEW_H"
|
|
108
|
+
:viewBox="`0 0 ${waveWidth} ${VIEW_H}`"
|
|
109
|
+
class="h-full"
|
|
110
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
111
|
+
>
|
|
112
|
+
<path
|
|
113
|
+
:d="wavePath"
|
|
114
|
+
fill="none"
|
|
115
|
+
stroke="currentColor"
|
|
116
|
+
stroke-width="3"
|
|
117
|
+
stroke-linecap="round"
|
|
118
|
+
/>
|
|
119
|
+
</svg>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Inactive (straight track) portion -->
|
|
124
|
+
<div
|
|
125
|
+
class="absolute inset-y-0 right-0 flex items-center"
|
|
126
|
+
:class="colorMap[color].track"
|
|
127
|
+
:style="{ left: `calc(${clampedValue}% + 4px)`, transition: 'left 300ms ease' }"
|
|
128
|
+
style="border-radius: 9999px; height: 4px; top: 50%; transform: translateY(-50%)"
|
|
129
|
+
/>
|
|
130
|
+
|
|
131
|
+
<!-- Stop indicator (dot at the end of the track) -->
|
|
132
|
+
<div
|
|
133
|
+
class="absolute rounded-full"
|
|
134
|
+
:class="colorMap[color].bar"
|
|
135
|
+
:style="{
|
|
136
|
+
right: '0',
|
|
137
|
+
top: '50%',
|
|
138
|
+
transform: 'translateY(-50%)',
|
|
139
|
+
width: '4px',
|
|
140
|
+
height: '4px',
|
|
141
|
+
}"
|
|
142
|
+
/>
|
|
143
|
+
</template>
|
|
144
|
+
|
|
145
|
+
<!-- INDETERMINATE -->
|
|
146
|
+
<div v-else class="absolute inset-0 overflow-hidden rounded-full">
|
|
147
|
+
<div
|
|
148
|
+
class="absolute top-0 left-0 h-full animate-[m3-wave-flow_0.9s_linear_infinite]"
|
|
149
|
+
:class="colorMap[color].text"
|
|
150
|
+
:style="{ width: `${waveWidth}px` }"
|
|
151
|
+
>
|
|
152
|
+
<svg
|
|
153
|
+
:width="waveWidth"
|
|
154
|
+
:height="VIEW_H"
|
|
155
|
+
:viewBox="`0 0 ${waveWidth} ${VIEW_H}`"
|
|
156
|
+
class="h-full"
|
|
157
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
158
|
+
>
|
|
159
|
+
<path
|
|
160
|
+
:d="wavePath"
|
|
161
|
+
fill="none"
|
|
162
|
+
stroke="currentColor"
|
|
163
|
+
stroke-width="3"
|
|
164
|
+
stroke-linecap="round"
|
|
165
|
+
/>
|
|
166
|
+
</svg>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</template>
|
|
172
|
+
|
|
173
|
+
<style>
|
|
174
|
+
/* Scroll exactly one period (20px) so the loop is perfectly seamless. */
|
|
175
|
+
@keyframes m3-wave-flow {
|
|
176
|
+
from {
|
|
177
|
+
transform: translateX(0);
|
|
178
|
+
}
|
|
179
|
+
to {
|
|
180
|
+
transform: translateX(-20px);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@keyframes m3-progress-indeterminate {
|
|
185
|
+
0% {
|
|
186
|
+
left: -40%;
|
|
187
|
+
}
|
|
188
|
+
100% {
|
|
189
|
+
left: 100%;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@media (prefers-reduced-motion: reduce) {
|
|
194
|
+
.animate-\[m3-wave-flow_1\.2s_linear_infinite\],
|
|
195
|
+
.animate-\[m3-wave-flow_0\.9s_linear_infinite\],
|
|
196
|
+
.animate-\[m3-progress-indeterminate_1\.6s_ease-in-out_infinite\] {
|
|
197
|
+
animation: none !important;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
</style>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, useId } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
modelValue: unknown;
|
|
7
|
+
value: unknown;
|
|
8
|
+
label?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
color?: "primary" | "secondary" | "tertiary" | "error";
|
|
11
|
+
}>(),
|
|
12
|
+
{ disabled: false, color: "primary" },
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{ "update:modelValue": [unknown] }>();
|
|
16
|
+
const id = useId();
|
|
17
|
+
const isChecked = computed(() => props.modelValue === props.value);
|
|
18
|
+
|
|
19
|
+
// Ring + dot color when checked, applied via currentColor on the SVG.
|
|
20
|
+
const checkedColor: Record<string, string> = {
|
|
21
|
+
primary: "text-primary",
|
|
22
|
+
secondary: "text-secondary",
|
|
23
|
+
tertiary: "text-tertiary",
|
|
24
|
+
error: "text-error",
|
|
25
|
+
};
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<label
|
|
30
|
+
:for="id"
|
|
31
|
+
class="inline-flex items-center gap-3 select-none"
|
|
32
|
+
:class="disabled ? 'cursor-not-allowed opacity-[0.38]' : 'cursor-pointer'"
|
|
33
|
+
>
|
|
34
|
+
<span class="relative flex h-5 w-5 shrink-0">
|
|
35
|
+
<input
|
|
36
|
+
:id="id"
|
|
37
|
+
type="radio"
|
|
38
|
+
class="sr-only"
|
|
39
|
+
:checked="isChecked"
|
|
40
|
+
:disabled="disabled"
|
|
41
|
+
@change="emit('update:modelValue', value)"
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
<!--
|
|
45
|
+
SVG radio: vector circles sharing center (10,10) stay round + concentric
|
|
46
|
+
at any zoom. Outer ring uses r=8 (not 9) so the 2px stroke (7..9) leaves
|
|
47
|
+
~1px of clearance to the viewBox edge — prevents the border getting
|
|
48
|
+
clipped at certain zoom levels.
|
|
49
|
+
-->
|
|
50
|
+
<svg
|
|
51
|
+
viewBox="0 0 20 20"
|
|
52
|
+
class="h-full w-full transition-colors duration-150"
|
|
53
|
+
:class="isChecked ? checkedColor[color] : 'text-on-surface-variant'"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
>
|
|
56
|
+
<circle cx="10" cy="10" r="8" fill="none" stroke="currentColor" stroke-width="2" />
|
|
57
|
+
<!--
|
|
58
|
+
Dot scaled via CSS, but the transform-origin is pinned to the circle's
|
|
59
|
+
own bounding box center (transform-box: fill-box). Without this, the SVG
|
|
60
|
+
element origin is (0,0) of the viewBox, so scale() grows from a corner
|
|
61
|
+
and the dot visibly slides to the center. fill-box fixes the origin to
|
|
62
|
+
the dot itself, so it grows symmetrically in place.
|
|
63
|
+
-->
|
|
64
|
+
<circle
|
|
65
|
+
class="m3-radio-dot"
|
|
66
|
+
:class="{ 'is-checked': isChecked }"
|
|
67
|
+
cx="10"
|
|
68
|
+
cy="10"
|
|
69
|
+
r="4.5"
|
|
70
|
+
fill="currentColor"
|
|
71
|
+
/>
|
|
72
|
+
</svg>
|
|
73
|
+
</span>
|
|
74
|
+
|
|
75
|
+
<span v-if="label" class="text-body-large text-on-surface">{{ label }}</span>
|
|
76
|
+
</label>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<style scoped>
|
|
80
|
+
.m3-radio-dot {
|
|
81
|
+
transform: scale(0);
|
|
82
|
+
transform-box: fill-box;
|
|
83
|
+
transform-origin: center;
|
|
84
|
+
transition: transform 150ms ease;
|
|
85
|
+
}
|
|
86
|
+
.m3-radio-dot.is-checked {
|
|
87
|
+
transform: scale(1);
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import MRadio from './MRadio.vue'
|
|
3
|
+
|
|
4
|
+
interface Option {
|
|
5
|
+
label: string
|
|
6
|
+
value: unknown
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<{
|
|
11
|
+
modelValue: unknown
|
|
12
|
+
options: Option[]
|
|
13
|
+
label?: string
|
|
14
|
+
direction?: 'column' | 'row'
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
color?: 'primary' | 'secondary' | 'tertiary' | 'error'
|
|
17
|
+
}>(), { direction: 'column', disabled: false, color: 'primary' })
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{ 'update:modelValue': [unknown] }>()
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div class="flex flex-col gap-2">
|
|
24
|
+
<span v-if="label" class="text-label-large text-on-surface-variant">{{ label }}</span>
|
|
25
|
+
<div
|
|
26
|
+
class="flex gap-4"
|
|
27
|
+
:class="direction === 'row' ? 'flex-row flex-wrap' : 'flex-col'"
|
|
28
|
+
>
|
|
29
|
+
<MRadio
|
|
30
|
+
v-for="opt in options"
|
|
31
|
+
:key="String(opt.value)"
|
|
32
|
+
:model-value="modelValue"
|
|
33
|
+
:value="opt.value"
|
|
34
|
+
:label="opt.label"
|
|
35
|
+
:color="color"
|
|
36
|
+
:disabled="disabled || !!opt.disabled"
|
|
37
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(defineProps<{
|
|
6
|
+
modelValue: number
|
|
7
|
+
max?: number
|
|
8
|
+
size?: number
|
|
9
|
+
readonly?: boolean
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
color?: string
|
|
12
|
+
icon?: string
|
|
13
|
+
halfIncrements?: boolean
|
|
14
|
+
}>(), { max: 5, size: 28, color: 'primary', icon: 'star', halfIncrements: false })
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{ 'update:modelValue': [number] }>()
|
|
17
|
+
|
|
18
|
+
const hovered = ref(-1)
|
|
19
|
+
|
|
20
|
+
const colorClass = computed(() => {
|
|
21
|
+
const map: Record<string, string> = {
|
|
22
|
+
primary: 'text-primary',
|
|
23
|
+
secondary: 'text-secondary',
|
|
24
|
+
tertiary: 'text-tertiary',
|
|
25
|
+
error: 'text-error',
|
|
26
|
+
}
|
|
27
|
+
return map[props.color] || ''
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const customStyle = computed(() => {
|
|
31
|
+
if (['primary', 'secondary', 'tertiary', 'error'].includes(props.color)) return undefined
|
|
32
|
+
return { color: props.color }
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
function valueAt(index: number, e?: MouseEvent) {
|
|
36
|
+
if (!props.halfIncrements) return index + 1
|
|
37
|
+
if (!e) return index + 1
|
|
38
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
39
|
+
const half = (e.clientX - rect.left) < rect.width / 2
|
|
40
|
+
return half ? index + 0.5 : index + 1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function onClick(index: number, e: MouseEvent) {
|
|
44
|
+
if (props.readonly || props.disabled) return
|
|
45
|
+
const v = valueAt(index, e)
|
|
46
|
+
emit('update:modelValue', v === props.modelValue ? 0 : v)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function onMove(index: number, e: MouseEvent) {
|
|
50
|
+
if (props.readonly || props.disabled) return
|
|
51
|
+
hovered.value = valueAt(index, e)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function onLeave() {
|
|
55
|
+
hovered.value = -1
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function iconName(index: number) {
|
|
59
|
+
const active = hovered.value >= 0 ? hovered.value : props.modelValue
|
|
60
|
+
if (index + 1 <= active) return props.icon
|
|
61
|
+
if (props.halfIncrements && index + 0.5 <= active) return props.icon + '_half'
|
|
62
|
+
return props.icon + '_border' // outlined variant not available for all icons
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isFilled(index: number) {
|
|
66
|
+
const active = hovered.value >= 0 ? hovered.value : props.modelValue
|
|
67
|
+
return index + 1 <= active || (props.halfIncrements && index + 0.5 <= active)
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<template>
|
|
72
|
+
<div
|
|
73
|
+
class="inline-flex items-center gap-0.5"
|
|
74
|
+
:class="disabled ? 'opacity-[0.38]' : ''"
|
|
75
|
+
@mouseleave="onLeave"
|
|
76
|
+
>
|
|
77
|
+
<button
|
|
78
|
+
v-for="i in max"
|
|
79
|
+
:key="i"
|
|
80
|
+
type="button"
|
|
81
|
+
class="relative inline-flex items-center justify-center rounded-full p-0.5 transition-transform duration-100"
|
|
82
|
+
:class="[
|
|
83
|
+
readonly || disabled ? 'cursor-default' : 'cursor-pointer hover:scale-110',
|
|
84
|
+
]"
|
|
85
|
+
:style="customStyle"
|
|
86
|
+
:disabled="disabled"
|
|
87
|
+
@click="onClick(i - 1, $event)"
|
|
88
|
+
@mousemove="onMove(i - 1, $event)"
|
|
89
|
+
>
|
|
90
|
+
<!-- Filled star -->
|
|
91
|
+
<MIcon
|
|
92
|
+
v-if="isFilled(i - 1)"
|
|
93
|
+
:name="icon"
|
|
94
|
+
:size="size"
|
|
95
|
+
:class="colorClass"
|
|
96
|
+
:style="customStyle"
|
|
97
|
+
style="font-variation-settings: 'FILL' 1"
|
|
98
|
+
/>
|
|
99
|
+
<!-- Empty star -->
|
|
100
|
+
<MIcon
|
|
101
|
+
v-else
|
|
102
|
+
:name="icon"
|
|
103
|
+
:size="size"
|
|
104
|
+
class="text-on-surface-variant/40"
|
|
105
|
+
/>
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
</template>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(defineProps<{
|
|
6
|
+
status: 'success' | 'error' | 'warning' | 'info' | '404' | '403' | '500'
|
|
7
|
+
title?: string
|
|
8
|
+
description?: string
|
|
9
|
+
}>(), {})
|
|
10
|
+
|
|
11
|
+
const config = computed(() => {
|
|
12
|
+
switch (props.status) {
|
|
13
|
+
case 'success': return { icon: 'check_circle', bg: 'bg-success-container', text: 'text-on-success-container', defaultTitle: 'Operación exitosa', defaultDesc: 'La acción se completó correctamente.' }
|
|
14
|
+
case 'error': return { icon: 'error', bg: 'bg-error-container', text: 'text-on-error-container', defaultTitle: 'Algo salió mal', defaultDesc: 'Ocurrió un error inesperado. Inténtalo de nuevo.' }
|
|
15
|
+
case 'warning': return { icon: 'warning', bg: 'bg-tertiary-container', text: 'text-on-tertiary-container', defaultTitle: 'Atención', defaultDesc: 'Hay algo que requiere tu atención.' }
|
|
16
|
+
case 'info': return { icon: 'info', bg: 'bg-primary-container', text: 'text-on-primary-container', defaultTitle: 'Información', defaultDesc: '' }
|
|
17
|
+
case '404': return { icon: 'search_off', bg: 'bg-surface-container-high', text: 'text-on-surface-variant', defaultTitle: 'Página no encontrada', defaultDesc: 'La página que buscas no existe o fue movida.' }
|
|
18
|
+
case '403': return { icon: 'lock', bg: 'bg-error-container', text: 'text-on-error-container', defaultTitle: 'Acceso denegado', defaultDesc: 'No tienes permisos para ver este recurso.' }
|
|
19
|
+
case '500': return { icon: 'cloud_off', bg: 'bg-error-container', text: 'text-on-error-container', defaultTitle: 'Error del servidor', defaultDesc: 'El servidor no pudo procesar la solicitud.' }
|
|
20
|
+
default: return { icon: 'info', bg: 'bg-surface-container-high', text: 'text-on-surface-variant', defaultTitle: '', defaultDesc: '' }
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const httpCode = computed(() => {
|
|
25
|
+
if (props.status === '404' || props.status === '403' || props.status === '500') return props.status
|
|
26
|
+
return null
|
|
27
|
+
})
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="flex flex-col items-center justify-center gap-4 py-14 text-center">
|
|
32
|
+
<!-- HTTP code -->
|
|
33
|
+
<span v-if="httpCode" class="text-display-small font-medium text-on-surface-variant/30">
|
|
34
|
+
{{ httpCode }}
|
|
35
|
+
</span>
|
|
36
|
+
|
|
37
|
+
<!-- Icon -->
|
|
38
|
+
<div class="flex h-20 w-20 items-center justify-center rounded-full" :class="[config.bg, config.text]">
|
|
39
|
+
<MIcon :name="config.icon" :size="40" />
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Title -->
|
|
43
|
+
<h2 class="text-headline-small font-medium text-on-surface">
|
|
44
|
+
{{ title ?? config.defaultTitle }}
|
|
45
|
+
</h2>
|
|
46
|
+
|
|
47
|
+
<!-- Description -->
|
|
48
|
+
<p v-if="description ?? config.defaultDesc" class="max-w-md text-body-large text-on-surface-variant">
|
|
49
|
+
{{ description ?? config.defaultDesc }}
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<!-- Actions slot -->
|
|
53
|
+
<div v-if="$slots.actions" class="mt-2 flex flex-wrap items-center justify-center gap-3">
|
|
54
|
+
<slot name="actions" />
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Extra content -->
|
|
58
|
+
<div v-if="$slots.default" class="mt-2">
|
|
59
|
+
<slot />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|