@m3ui-vue/m3ui-vue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/dist/components/MAlert.vue.d.ts +27 -0
  4. package/dist/components/MAppBar.vue.d.ts +24 -0
  5. package/dist/components/MAvatar.vue.d.ts +9 -0
  6. package/dist/components/MBadge.vue.d.ts +22 -0
  7. package/dist/components/MBottomSheet.vue.d.ts +26 -0
  8. package/dist/components/MBreadcrumbs.vue.d.ts +19 -0
  9. package/dist/components/MButton.vue.d.ts +32 -0
  10. package/dist/components/MCalendar.vue.d.ts +23 -0
  11. package/dist/components/MCard.vue.d.ts +28 -0
  12. package/dist/components/MChart.vue.d.ts +13 -0
  13. package/dist/components/MCheckbox.vue.d.ts +26 -0
  14. package/dist/components/MChip.vue.d.ts +33 -0
  15. package/dist/components/MCodeEditor.vue.d.ts +35 -0
  16. package/dist/components/MColorPicker.vue.d.ts +18 -0
  17. package/dist/components/MCommandPalette.vue.d.ts +29 -0
  18. package/dist/components/MConfirmDialog.vue.d.ts +23 -0
  19. package/dist/components/MContainer.vue.d.ts +24 -0
  20. package/dist/components/MContextMenu.vue.d.ts +35 -0
  21. package/dist/components/MDataTable.vue.d.ts +83 -0
  22. package/dist/components/MDatePicker.vue.d.ts +21 -0
  23. package/dist/components/MDateRangePicker.vue.d.ts +24 -0
  24. package/dist/components/MDialog.vue.d.ts +30 -0
  25. package/dist/components/MDivider.vue.d.ts +11 -0
  26. package/dist/components/MDragDropList.vue.d.ts +40 -0
  27. package/dist/components/MEmptyState.vue.d.ts +21 -0
  28. package/dist/components/MExpansionPanel.vue.d.ts +28 -0
  29. package/dist/components/MFab.vue.d.ts +28 -0
  30. package/dist/components/MFileUpload.vue.d.ts +25 -0
  31. package/dist/components/MGrid.vue.d.ts +26 -0
  32. package/dist/components/MHotkeys.vue.d.ts +16 -0
  33. package/dist/components/MIcon.vue.d.ts +9 -0
  34. package/dist/components/MIconButton.vue.d.ts +14 -0
  35. package/dist/components/MInfiniteScroll.vue.d.ts +34 -0
  36. package/dist/components/MJsonEditor.vue.d.ts +17 -0
  37. package/dist/components/MJsonViewer.vue.d.ts +14 -0
  38. package/dist/components/MKanban.vue.d.ts +53 -0
  39. package/dist/components/MLoadingOverlay.vue.d.ts +28 -0
  40. package/dist/components/MMarkdown.vue.d.ts +11 -0
  41. package/dist/components/MMasonry.vue.d.ts +23 -0
  42. package/dist/components/MMenu.vue.d.ts +27 -0
  43. package/dist/components/MMenuItem.vue.d.ts +16 -0
  44. package/dist/components/MMultiSelect.vue.d.ts +34 -0
  45. package/dist/components/MNavigationBar.vue.d.ts +18 -0
  46. package/dist/components/MNavigationDrawer.vue.d.ts +41 -0
  47. package/dist/components/MNavigationRail.vue.d.ts +32 -0
  48. package/dist/components/MPagination.vue.d.ts +12 -0
  49. package/dist/components/MProgressBar.vue.d.ts +13 -0
  50. package/dist/components/MRadio.vue.d.ts +17 -0
  51. package/dist/components/MRadioGroup.vue.d.ts +24 -0
  52. package/dist/components/MRating.vue.d.ts +23 -0
  53. package/dist/components/MResult.vue.d.ts +20 -0
  54. package/dist/components/MRichTextEditor.vue.d.ts +17 -0
  55. package/dist/components/MScheduler.vue.d.ts +35 -0
  56. package/dist/components/MSegmentedButton.vue.d.ts +24 -0
  57. package/dist/components/MSelect.vue.d.ts +29 -0
  58. package/dist/components/MSideSheet.vue.d.ts +28 -0
  59. package/dist/components/MSkeleton.vue.d.ts +14 -0
  60. package/dist/components/MSlider.vue.d.ts +24 -0
  61. package/dist/components/MSnackbar.vue.d.ts +3 -0
  62. package/dist/components/MSpinner.vue.d.ts +10 -0
  63. package/dist/components/MSplitter.vue.d.ts +26 -0
  64. package/dist/components/MSpotlightSearch.vue.d.ts +34 -0
  65. package/dist/components/MStack.vue.d.ts +30 -0
  66. package/dist/components/MStatCard.vue.d.ts +24 -0
  67. package/dist/components/MStepper.vue.d.ts +33 -0
  68. package/dist/components/MSwitch.vue.d.ts +14 -0
  69. package/dist/components/MTable.vue.d.ts +73 -0
  70. package/dist/components/MTabs.vue.d.ts +20 -0
  71. package/dist/components/MTerminal.vue.d.ts +25 -0
  72. package/dist/components/MTextField.vue.d.ts +41 -0
  73. package/dist/components/MTimePicker.vue.d.ts +20 -0
  74. package/dist/components/MTimeline.vue.d.ts +31 -0
  75. package/dist/components/MTooltip.vue.d.ts +21 -0
  76. package/dist/components/MTopAppBar.vue.d.ts +29 -0
  77. package/dist/components/MTour.vue.d.ts +19 -0
  78. package/dist/components/MTransferList.vue.d.ts +23 -0
  79. package/dist/components/MTree.vue.d.ts +68 -0
  80. package/dist/components/MTreeTable.vue.d.ts +57 -0
  81. package/dist/components/MVirtualTable.vue.d.ts +40 -0
  82. package/dist/components/_MContextMenuPanel.vue.d.ts +13 -0
  83. package/dist/components/_MTreeNode.vue.d.ts +26 -0
  84. package/dist/composables/useColorPalette.d.ts +11 -0
  85. package/dist/composables/useFieldBg.d.ts +13 -0
  86. package/dist/composables/useTheme.d.ts +5 -0
  87. package/dist/composables/useToast.d.ts +59 -0
  88. package/dist/index.d.ts +112 -0
  89. package/dist/m3ui.css +2 -0
  90. package/dist/m3ui.js +7432 -0
  91. package/dist/m3ui.js.map +1 -0
  92. package/dist/plugin.d.ts +9 -0
  93. package/dist/styles/palettes.css +1253 -0
  94. package/dist/styles/theme.css +249 -0
  95. package/package.json +166 -0
  96. package/src/components/MAlert.vue +69 -0
  97. package/src/components/MAppBar.vue +40 -0
  98. package/src/components/MAvatar.vue +21 -0
  99. package/src/components/MBadge.vue +46 -0
  100. package/src/components/MBottomSheet.vue +113 -0
  101. package/src/components/MBreadcrumbs.vue +52 -0
  102. package/src/components/MButton.vue +111 -0
  103. package/src/components/MCalendar.vue +173 -0
  104. package/src/components/MCard.vue +56 -0
  105. package/src/components/MChart.vue +158 -0
  106. package/src/components/MCheckbox.vue +48 -0
  107. package/src/components/MChip.vue +87 -0
  108. package/src/components/MCodeEditor.vue +179 -0
  109. package/src/components/MColorPicker.vue +305 -0
  110. package/src/components/MCommandPalette.vue +213 -0
  111. package/src/components/MConfirmDialog.vue +43 -0
  112. package/src/components/MContainer.vue +36 -0
  113. package/src/components/MContextMenu.vue +66 -0
  114. package/src/components/MDataTable.vue +376 -0
  115. package/src/components/MDatePicker.vue +253 -0
  116. package/src/components/MDateRangePicker.vue +265 -0
  117. package/src/components/MDialog.vue +90 -0
  118. package/src/components/MDivider.vue +26 -0
  119. package/src/components/MDragDropList.vue +111 -0
  120. package/src/components/MEmptyState.vue +40 -0
  121. package/src/components/MExpansionPanel.vue +112 -0
  122. package/src/components/MFab.vue +220 -0
  123. package/src/components/MFileUpload.vue +206 -0
  124. package/src/components/MGrid.vue +99 -0
  125. package/src/components/MHotkeys.vue +122 -0
  126. package/src/components/MIcon.vue +9 -0
  127. package/src/components/MIconButton.vue +49 -0
  128. package/src/components/MInfiniteScroll.vue +68 -0
  129. package/src/components/MJsonEditor.vue +118 -0
  130. package/src/components/MJsonViewer.vue +106 -0
  131. package/src/components/MKanban.vue +147 -0
  132. package/src/components/MLoadingOverlay.vue +52 -0
  133. package/src/components/MMarkdown.vue +123 -0
  134. package/src/components/MMasonry.vue +87 -0
  135. package/src/components/MMenu.vue +113 -0
  136. package/src/components/MMenuItem.vue +15 -0
  137. package/src/components/MMultiSelect.vue +306 -0
  138. package/src/components/MNavigationBar.vue +62 -0
  139. package/src/components/MNavigationDrawer.vue +157 -0
  140. package/src/components/MNavigationRail.vue +80 -0
  141. package/src/components/MPagination.vue +37 -0
  142. package/src/components/MProgressBar.vue +200 -0
  143. package/src/components/MRadio.vue +89 -0
  144. package/src/components/MRadioGroup.vue +41 -0
  145. package/src/components/MRating.vue +108 -0
  146. package/src/components/MResult.vue +62 -0
  147. package/src/components/MRichTextEditor.vue +199 -0
  148. package/src/components/MScheduler.vue +225 -0
  149. package/src/components/MSegmentedButton.vue +75 -0
  150. package/src/components/MSelect.vue +259 -0
  151. package/src/components/MSideSheet.vue +112 -0
  152. package/src/components/MSkeleton.vue +60 -0
  153. package/src/components/MSlider.vue +188 -0
  154. package/src/components/MSnackbar.vue +244 -0
  155. package/src/components/MSpinner.vue +122 -0
  156. package/src/components/MSplitter.vue +97 -0
  157. package/src/components/MSpotlightSearch.vue +244 -0
  158. package/src/components/MStack.vue +67 -0
  159. package/src/components/MStatCard.vue +56 -0
  160. package/src/components/MStepper.vue +161 -0
  161. package/src/components/MSwitch.vue +63 -0
  162. package/src/components/MTable.vue +404 -0
  163. package/src/components/MTabs.vue +97 -0
  164. package/src/components/MTerminal.vue +146 -0
  165. package/src/components/MTextField.vue +180 -0
  166. package/src/components/MTimePicker.vue +227 -0
  167. package/src/components/MTimeline.vue +117 -0
  168. package/src/components/MTooltip.vue +82 -0
  169. package/src/components/MTopAppBar.vue +62 -0
  170. package/src/components/MTour.vue +226 -0
  171. package/src/components/MTransferList.vue +181 -0
  172. package/src/components/MTree.vue +164 -0
  173. package/src/components/MTreeTable.vue +159 -0
  174. package/src/components/MVirtualTable.vue +155 -0
  175. package/src/components/_MContextMenuPanel.vue +129 -0
  176. package/src/components/_MTreeNode.vue +171 -0
  177. package/src/composables/useColorPalette.ts +60 -0
  178. package/src/composables/useFieldBg.ts +91 -0
  179. package/src/composables/useTheme.ts +55 -0
  180. package/src/composables/useToast.ts +51 -0
  181. package/src/env.d.ts +1 -0
  182. package/src/index.ts +119 -0
  183. package/src/plugin.ts +18 -0
  184. package/src/styles/palettes.css +1253 -0
  185. package/src/styles/theme.css +249 -0
@@ -0,0 +1,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>