v-uixy 1.2.0 → 1.3.1
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/package.json +1 -1
- package/templates/assets/css/main.css +4 -0
- package/templates/components/Badge/Badge.styles.ts +2 -2
- package/templates/components/Checkbox/Checkbox.styles.ts +1 -1
- package/templates/components/ColorPicker/ColorPicker.component.vue +287 -0
- package/templates/components/ColorPicker/ColorPicker.types.ts +3 -0
- package/templates/components/ColorPicker/index.ts +4 -0
- package/templates/components/Input/Input.component.vue +23 -13
- package/templates/components/Input/Input.styles.ts +1 -1
- package/templates/components/Input/Input.types.ts +0 -1
- package/templates/components/PieChart/PieChart.component.vue +218 -0
- package/templates/components/PieChart/PieChart.types.ts +10 -0
- package/templates/components/PieChart/index.ts +4 -0
- package/templates/components/Select/Select.component.vue +23 -14
- package/templates/components/Select/Select.styles.ts +1 -1
- package/templates/components/Select/Select.types.ts +0 -1
- package/templates/components/Sheet/Sheet.component.vue +6 -0
- package/templates/components/Textarea/Textarea.component.vue +20 -14
- package/templates/components/Textarea/Textarea.styles.ts +1 -1
- package/templates/components/Textarea/Textarea.types.ts +0 -1
- package/templates/components/index.ts +35 -0
- package/templates/components.json +14 -2
- package/templates/composables/useInputValidation.ts +89 -0
package/package.json
CHANGED
|
@@ -164,6 +164,10 @@
|
|
|
164
164
|
@apply cursor-default border-gray-500 dark:border-gray-500;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
.slider-color-picker::-webkit-slider-thumb {
|
|
168
|
+
@apply relative z-10 size-4.5 cursor-pointer appearance-none rounded-full border-2 border-solid border-gray-900 bg-gray-100;
|
|
169
|
+
}
|
|
170
|
+
|
|
167
171
|
.disable-transitions * {
|
|
168
172
|
transition: none !important;
|
|
169
173
|
}
|
|
@@ -5,9 +5,9 @@ export const badgeStyles = styles(
|
|
|
5
5
|
{
|
|
6
6
|
variant: {
|
|
7
7
|
primary:
|
|
8
|
-
"bg-gray-800 hover:bg-gray-700 dark:hover:bg-gray-200 dark:bg-gray-300 text-gray-100 dark:text-black dark:disabled:bg-gray-400 dark:disabled:text-gray-500 disabled:text-gray-600",
|
|
8
|
+
"bg-gray-800 hover:bg-gray-700 border border-transparent dark:hover:bg-gray-200 dark:bg-gray-300 text-gray-100 dark:text-black dark:disabled:bg-gray-400 dark:disabled:text-gray-500 disabled:text-gray-600",
|
|
9
9
|
secondary:
|
|
10
|
-
"bg-gray-300 hover:bg-gray-200 dark:bg-gray-900 dark:hover:bg-gray-700 dark:text-gray-100 text-black dark:disabled:bg-gray-600 dark:disabled:text-gray-700 disabled:text-gray-500",
|
|
10
|
+
"bg-gray-300 hover:bg-gray-200 border border-transparent dark:bg-gray-900 dark:hover:bg-gray-700 dark:text-gray-100 text-black dark:disabled:bg-gray-600 dark:disabled:text-gray-700 disabled:text-gray-500",
|
|
11
11
|
tertiary:
|
|
12
12
|
"bg-transparent border border-solid border-gray-300 dark:border-gray-800 hover:bg-gray-200 dark:hover:bg-gray-800 text-black dark:text-gray-100 dark:disabled:border-gray-900 dark:disabled:text-gray-500 dark:disabled:bg-gray-800 disabled:bg-gray-300 disabled:text-gray-500 disabled:border-gray-400",
|
|
13
13
|
},
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<uixy-card class="p-2 flex flex-col gap-4">
|
|
3
|
+
<div
|
|
4
|
+
ref="svRef"
|
|
5
|
+
class="relative cursor-crosshair overflow-hidden rounded-1 w-64 h-48"
|
|
6
|
+
:style="svBackgroundStyle"
|
|
7
|
+
@mousedown="onSvMouseDown"
|
|
8
|
+
@touchstart.prevent="onSvTouchStart"
|
|
9
|
+
>
|
|
10
|
+
<div
|
|
11
|
+
class="pointer-events-none absolute -mt-2 -ml-2 size-4 rounded-full border-2 shadow-sm transition-colors border-white"
|
|
12
|
+
:style="svHandleStyle"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="flex flex-col gap-4">
|
|
16
|
+
<input
|
|
17
|
+
type="range"
|
|
18
|
+
min="0"
|
|
19
|
+
max="360"
|
|
20
|
+
step="1"
|
|
21
|
+
v-model.number="hue"
|
|
22
|
+
:disabled="props.disabled"
|
|
23
|
+
class="h-2.5 w-full appearance-none rounded-full outline-none slider-color-picker border border-gray-300 dark:border-gray-900"
|
|
24
|
+
:style="hueSliderStyle"
|
|
25
|
+
/>
|
|
26
|
+
<input
|
|
27
|
+
type="range"
|
|
28
|
+
min="0"
|
|
29
|
+
max="100"
|
|
30
|
+
step="1"
|
|
31
|
+
v-model.number="alpha"
|
|
32
|
+
:disabled="props.disabled"
|
|
33
|
+
class="h-2.5 w-full appearance-none rounded-full outline-none slider-color-picker border border-gray-300 dark:border-gray-900"
|
|
34
|
+
:style="alphaSliderStyle"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="flex gap-2 items-center mt-2 justify-between">
|
|
38
|
+
<uixy-card class="p-1">
|
|
39
|
+
<div
|
|
40
|
+
class="w-12 h-4 rounded-1 shadow-sm"
|
|
41
|
+
:style="{ backgroundColor: model }"
|
|
42
|
+
></div>
|
|
43
|
+
</uixy-card>
|
|
44
|
+
<p class="text-xs font-500">{{ model }}</p>
|
|
45
|
+
</div>
|
|
46
|
+
</uixy-card>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script setup lang="ts">
|
|
50
|
+
import type { UixyColorPickerProps } from "./ColorPicker.types";
|
|
51
|
+
import { UixyCard } from "../Card";
|
|
52
|
+
|
|
53
|
+
const props = defineProps<UixyColorPickerProps>();
|
|
54
|
+
|
|
55
|
+
const model = defineModel<string>({ default: "#ff0000" });
|
|
56
|
+
|
|
57
|
+
const hue = ref(0);
|
|
58
|
+
const sat = ref(100);
|
|
59
|
+
const val = ref(100);
|
|
60
|
+
const alpha = ref(100);
|
|
61
|
+
|
|
62
|
+
const clamp = (n: number, min: number, max: number) =>
|
|
63
|
+
Math.min(max, Math.max(min, n));
|
|
64
|
+
|
|
65
|
+
const componentToHex = (c: number) => c.toString(16).padStart(2, "0");
|
|
66
|
+
|
|
67
|
+
const rgbToHex = (r: number, g: number, b: number) =>
|
|
68
|
+
`#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}`;
|
|
69
|
+
|
|
70
|
+
const rgbToHex8 = (r: number, g: number, b: number, a: number) =>
|
|
71
|
+
`#${componentToHex(r)}${componentToHex(g)}${componentToHex(
|
|
72
|
+
b
|
|
73
|
+
)}${componentToHex(clamp(a, 0, 255))}`;
|
|
74
|
+
|
|
75
|
+
const hexToRgbA = (
|
|
76
|
+
hex?: string
|
|
77
|
+
): {
|
|
78
|
+
r: number;
|
|
79
|
+
g: number;
|
|
80
|
+
b: number;
|
|
81
|
+
a: number;
|
|
82
|
+
hasAlpha: boolean;
|
|
83
|
+
} | null => {
|
|
84
|
+
if (!hex) return null;
|
|
85
|
+
const cleaned = hex.trim().replace(/^#/, "");
|
|
86
|
+
if (cleaned.length === 6) {
|
|
87
|
+
const r = parseInt(cleaned.slice(0, 2), 16);
|
|
88
|
+
const g = parseInt(cleaned.slice(2, 4), 16);
|
|
89
|
+
const b = parseInt(cleaned.slice(4, 6), 16);
|
|
90
|
+
return { r, g, b, a: 255, hasAlpha: false } as const;
|
|
91
|
+
}
|
|
92
|
+
if (cleaned.length === 8) {
|
|
93
|
+
const r = parseInt(cleaned.slice(0, 2), 16);
|
|
94
|
+
const g = parseInt(cleaned.slice(2, 4), 16);
|
|
95
|
+
const b = parseInt(cleaned.slice(4, 6), 16);
|
|
96
|
+
const a = parseInt(cleaned.slice(6, 8), 16);
|
|
97
|
+
return { r, g, b, a, hasAlpha: true } as const;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const hsvToRgb = (h: number, s: number, v: number) => {
|
|
103
|
+
const c = v * s;
|
|
104
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
105
|
+
const m = v - c;
|
|
106
|
+
|
|
107
|
+
let r1 = 0,
|
|
108
|
+
g1 = 0,
|
|
109
|
+
b1 = 0;
|
|
110
|
+
|
|
111
|
+
if (h >= 0 && h < 60) {
|
|
112
|
+
r1 = c;
|
|
113
|
+
g1 = x;
|
|
114
|
+
b1 = 0;
|
|
115
|
+
} else if (h < 120) {
|
|
116
|
+
r1 = x;
|
|
117
|
+
g1 = c;
|
|
118
|
+
b1 = 0;
|
|
119
|
+
} else if (h < 180) {
|
|
120
|
+
r1 = 0;
|
|
121
|
+
g1 = c;
|
|
122
|
+
b1 = x;
|
|
123
|
+
} else if (h < 240) {
|
|
124
|
+
r1 = 0;
|
|
125
|
+
g1 = x;
|
|
126
|
+
b1 = c;
|
|
127
|
+
} else if (h < 300) {
|
|
128
|
+
r1 = x;
|
|
129
|
+
g1 = 0;
|
|
130
|
+
b1 = c;
|
|
131
|
+
} else {
|
|
132
|
+
r1 = c;
|
|
133
|
+
g1 = 0;
|
|
134
|
+
b1 = x;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
r: Math.round((r1 + m) * 255),
|
|
138
|
+
g: Math.round((g1 + m) * 255),
|
|
139
|
+
b: Math.round((b1 + m) * 255),
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const rgbToHsv = (r: number, g: number, b: number) => {
|
|
144
|
+
r /= 255;
|
|
145
|
+
g /= 255;
|
|
146
|
+
b /= 255;
|
|
147
|
+
const max = Math.max(r, g, b),
|
|
148
|
+
min = Math.min(r, g, b);
|
|
149
|
+
const d = max - min;
|
|
150
|
+
let h = 0;
|
|
151
|
+
if (d === 0) h = 0;
|
|
152
|
+
else if (max === r) h = ((g - b) / d) % 6;
|
|
153
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
154
|
+
else h = (r - g) / d + 4;
|
|
155
|
+
h = Math.round(h * 60);
|
|
156
|
+
if (h < 0) h += 360;
|
|
157
|
+
const s = max === 0 ? 0 : d / max;
|
|
158
|
+
const v = max;
|
|
159
|
+
return { h, s, v };
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const svRef = ref<HTMLElement | null>(null);
|
|
163
|
+
|
|
164
|
+
const rgb = computed(() =>
|
|
165
|
+
hsvToRgb(hue.value, sat.value / 100, val.value / 100)
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const updatingFromModel = ref(false);
|
|
169
|
+
|
|
170
|
+
watch([hue, sat, val, alpha], () => {
|
|
171
|
+
if (updatingFromModel.value) return;
|
|
172
|
+
const a = Math.round((alpha.value / 100) * 255);
|
|
173
|
+
if (a === 255) {
|
|
174
|
+
model.value = rgbToHex(rgb.value.r, rgb.value.g, rgb.value.b);
|
|
175
|
+
} else {
|
|
176
|
+
model.value = rgbToHex8(rgb.value.r, rgb.value.g, rgb.value.b, a);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const applyModel = (value?: string) => {
|
|
181
|
+
const parsed = hexToRgbA(value ?? model.value ?? "#ff0000");
|
|
182
|
+
if (!parsed) return;
|
|
183
|
+
const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b);
|
|
184
|
+
updatingFromModel.value = true;
|
|
185
|
+
hue.value = hsv.h;
|
|
186
|
+
sat.value = Math.round(hsv.s * 100);
|
|
187
|
+
val.value = Math.round(hsv.v * 100);
|
|
188
|
+
alpha.value = clamp(Math.round((parsed.a / 255) * 100), 0, 100);
|
|
189
|
+
|
|
190
|
+
let normalized: string;
|
|
191
|
+
if (!parsed.hasAlpha || parsed.a === 255) {
|
|
192
|
+
normalized = rgbToHex(parsed.r, parsed.g, parsed.b);
|
|
193
|
+
} else {
|
|
194
|
+
normalized = rgbToHex8(parsed.r, parsed.g, parsed.b, parsed.a);
|
|
195
|
+
}
|
|
196
|
+
if (model.value !== normalized) model.value = normalized;
|
|
197
|
+
updatingFromModel.value = false;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
onMounted(() => applyModel());
|
|
201
|
+
watch(model, (v) => applyModel(v));
|
|
202
|
+
|
|
203
|
+
const svBackgroundStyle = computed(() => ({
|
|
204
|
+
background: `linear-gradient(to top, black, transparent), linear-gradient(to right, white, hsl(${hue.value}, 100%, 50%))`,
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
const svHandleStyle = computed(() => ({
|
|
208
|
+
left: `${sat.value}%`,
|
|
209
|
+
top: `${100 - val.value}%`,
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
const hueSliderStyle = computed(() => ({
|
|
213
|
+
background:
|
|
214
|
+
"linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)",
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
const alphaSliderStyle = computed(() => {
|
|
218
|
+
const { r, g, b } = rgb.value;
|
|
219
|
+
return {
|
|
220
|
+
background: `linear-gradient(to right, rgba(${r}, ${g}, ${b}, 0), rgba(${r}, ${g}, ${b}, 1))`,
|
|
221
|
+
} as Record<string, string>;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const updateSvFromEvent = (e: MouseEvent) => {
|
|
225
|
+
if (!svRef.value) return;
|
|
226
|
+
const rect = svRef.value.getBoundingClientRect();
|
|
227
|
+
const x = clamp(((e.clientX - rect.left) / rect.width) * 100, 0, 100);
|
|
228
|
+
const y = clamp(((e.clientY - rect.top) / rect.height) * 100, 0, 100);
|
|
229
|
+
sat.value = Math.round(x);
|
|
230
|
+
val.value = Math.round(100 - y);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const updateSvFromTouch = (e: TouchEvent) => {
|
|
234
|
+
if (!svRef.value) return;
|
|
235
|
+
const t = e.touches?.[0] ?? e.changedTouches?.[0];
|
|
236
|
+
if (!t) return;
|
|
237
|
+
const rect = svRef.value.getBoundingClientRect();
|
|
238
|
+
const x = clamp(((t.clientX - rect.left) / rect.width) * 100, 0, 100);
|
|
239
|
+
const y = clamp(((t.clientY - rect.top) / rect.height) * 100, 0, 100);
|
|
240
|
+
sat.value = Math.round(x);
|
|
241
|
+
val.value = Math.round(100 - y);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
let dragging = false;
|
|
245
|
+
|
|
246
|
+
const onSvMouseDown = (e: MouseEvent) => {
|
|
247
|
+
if (props.disabled) return;
|
|
248
|
+
|
|
249
|
+
dragging = true;
|
|
250
|
+
|
|
251
|
+
updateSvFromEvent(e);
|
|
252
|
+
|
|
253
|
+
window.addEventListener("mousemove", onMouseMove);
|
|
254
|
+
window.addEventListener("mouseup", onMouseUp);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
258
|
+
if (!dragging) return;
|
|
259
|
+
|
|
260
|
+
updateSvFromEvent(e);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const onMouseUp = () => {
|
|
264
|
+
dragging = false;
|
|
265
|
+
|
|
266
|
+
window.removeEventListener("mousemove", onMouseMove);
|
|
267
|
+
window.removeEventListener("mouseup", onMouseUp);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const onSvTouchStart = (e: TouchEvent) => {
|
|
271
|
+
if (props.disabled) return;
|
|
272
|
+
|
|
273
|
+
updateSvFromTouch(e);
|
|
274
|
+
|
|
275
|
+
window.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
276
|
+
window.addEventListener("touchend", onTouchEnd);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const onTouchMove = (e: TouchEvent) => {
|
|
280
|
+
updateSvFromTouch(e);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const onTouchEnd = () => {
|
|
284
|
+
window.removeEventListener("touchmove", onTouchMove);
|
|
285
|
+
window.removeEventListener("touchend", onTouchEnd);
|
|
286
|
+
};
|
|
287
|
+
</script>
|
|
@@ -27,19 +27,29 @@
|
|
|
27
27
|
{{ props.label }}
|
|
28
28
|
</label>
|
|
29
29
|
</div>
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
<animate-presence mode="wait" :initial="false">
|
|
31
|
+
<motion.div
|
|
32
|
+
v-if="!!text"
|
|
33
|
+
class="overflow-hidden"
|
|
34
|
+
:initial="{ height: 0 }"
|
|
35
|
+
:animate="{ height: 'auto' }"
|
|
36
|
+
:exit="{ height: 0 }"
|
|
37
|
+
:transition="{ duration: 0.1, ease: 'easeInOut' }"
|
|
38
|
+
>
|
|
39
|
+
<animate-presence mode="wait" :initial="false">
|
|
40
|
+
<motion.p
|
|
41
|
+
:key="status + text"
|
|
42
|
+
:class="helperStyles({ status })"
|
|
43
|
+
:initial="{ opacity: 0, y: -4 }"
|
|
44
|
+
:animate="{ opacity: 1, y: 0 }"
|
|
45
|
+
:exit="{ opacity: 0, y: 4 }"
|
|
46
|
+
:transition="{ duration: 0.12, ease: 'easeInOut', delay: 0.1 }"
|
|
47
|
+
>
|
|
48
|
+
{{ text }}
|
|
49
|
+
</motion.p>
|
|
50
|
+
</animate-presence>
|
|
51
|
+
</motion.div>
|
|
52
|
+
</animate-presence>
|
|
43
53
|
</div>
|
|
44
54
|
</template>
|
|
45
55
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="wrapperRef" class="relative w-full aspect-square">
|
|
3
|
+
<svg
|
|
4
|
+
ref="svgRef"
|
|
5
|
+
width="100%"
|
|
6
|
+
height="100%"
|
|
7
|
+
:viewBox="`0 0 ${size} ${size}`"
|
|
8
|
+
role="img"
|
|
9
|
+
aria-label="Pie chart"
|
|
10
|
+
class="block select-none pointer-events-none"
|
|
11
|
+
>
|
|
12
|
+
<motion.g
|
|
13
|
+
:initial="{ transform: `rotate(0, ${center}, ${center})` }"
|
|
14
|
+
:animate="{ transform: `rotate(0, ${center}, ${center})` }"
|
|
15
|
+
:transition="groupTransition"
|
|
16
|
+
>
|
|
17
|
+
<circle
|
|
18
|
+
:cx="center"
|
|
19
|
+
:cy="center"
|
|
20
|
+
:r="radius"
|
|
21
|
+
fill="none"
|
|
22
|
+
class="stroke-gray-300 dark:stroke-gray-800"
|
|
23
|
+
:stroke-width="thickness"
|
|
24
|
+
vector-effect="non-scaling-stroke"
|
|
25
|
+
shape-rendering="geometricPrecision"
|
|
26
|
+
/>
|
|
27
|
+
<template v-if="isReady">
|
|
28
|
+
<motion.circle
|
|
29
|
+
v-for="seg in segmentsTimed"
|
|
30
|
+
:key="seg.key"
|
|
31
|
+
:initial="{
|
|
32
|
+
strokeDasharray: `0 ${circumference}`,
|
|
33
|
+
strokeDashoffset: seg.offset,
|
|
34
|
+
opacity: 0.8,
|
|
35
|
+
}"
|
|
36
|
+
:animate="{
|
|
37
|
+
strokeDasharray: `${seg.length} ${circumference}`,
|
|
38
|
+
strokeDashoffset: seg.offset,
|
|
39
|
+
opacity: 1,
|
|
40
|
+
}"
|
|
41
|
+
:transition="transitionFor(seg)"
|
|
42
|
+
:cx="center"
|
|
43
|
+
:cy="center"
|
|
44
|
+
:r="radius"
|
|
45
|
+
fill="none"
|
|
46
|
+
:stroke="seg.color"
|
|
47
|
+
:stroke-width="thickness - 2"
|
|
48
|
+
stroke-linecap="butt"
|
|
49
|
+
vector-effect="non-scaling-stroke"
|
|
50
|
+
shape-rendering="geometricPrecision"
|
|
51
|
+
/>
|
|
52
|
+
</template>
|
|
53
|
+
</motion.g>
|
|
54
|
+
</svg>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
import { computed, onMounted, onUnmounted, ref, toRefs, watch } from "vue";
|
|
60
|
+
import { motion } from "motion-v";
|
|
61
|
+
import type { UixyPieChartProps } from "./PieChart.types";
|
|
62
|
+
|
|
63
|
+
type SegBase = { key: string; length: number; offset: number; color: string };
|
|
64
|
+
type SegTimed = SegBase & { dur: number; delay: number };
|
|
65
|
+
|
|
66
|
+
const INITIAL_TOTAL_DURATION = 1.2;
|
|
67
|
+
const UPDATE_DURATION = 0.45;
|
|
68
|
+
const OVERLAP = 0.02;
|
|
69
|
+
const SIZE_EPSILON = 0.5;
|
|
70
|
+
const EASING = "linear";
|
|
71
|
+
|
|
72
|
+
const props = withDefaults(defineProps<UixyPieChartProps>(), {
|
|
73
|
+
thickness: 18,
|
|
74
|
+
animate: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const wrapperRef = ref<HTMLElement | null>(null);
|
|
78
|
+
const svgRef = ref<SVGSVGElement | null>(null);
|
|
79
|
+
const measuredSize = ref<number>(180);
|
|
80
|
+
const roRef = ref<ResizeObserver | null>(null);
|
|
81
|
+
const isReady = ref(false);
|
|
82
|
+
const initialTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
|
83
|
+
|
|
84
|
+
const size = computed(() => measuredSize.value);
|
|
85
|
+
const center = computed(() => measuredSize.value / 2);
|
|
86
|
+
const radius = computed(() =>
|
|
87
|
+
Math.max(1, measuredSize.value / 2 - props.thickness / 2 - 2)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const circumference = computed(() => 2 * Math.PI * radius.value);
|
|
91
|
+
|
|
92
|
+
const groupTransition = computed(() =>
|
|
93
|
+
!isReady.value || isInitial.value
|
|
94
|
+
? { duration: 0 }
|
|
95
|
+
: { duration: 0.35, easing: EASING }
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const total = computed(() =>
|
|
99
|
+
props.data.reduce((acc, d) => acc + Math.max(0, d.value), 0)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const segmentsBase = computed<SegBase[]>(() => {
|
|
103
|
+
const totalVal = total.value || 1;
|
|
104
|
+
let acc = 0;
|
|
105
|
+
const list: SegBase[] = [];
|
|
106
|
+
|
|
107
|
+
props.data.forEach((d, i) => {
|
|
108
|
+
const frac = Math.max(0, d.value) / totalVal;
|
|
109
|
+
const baseLen = frac * circumference.value;
|
|
110
|
+
const drawLen = Math.max(0, baseLen);
|
|
111
|
+
|
|
112
|
+
if (drawLen > 0) {
|
|
113
|
+
list.push({
|
|
114
|
+
key: `seg-${i}-${d.color ?? "c"}`,
|
|
115
|
+
length: drawLen,
|
|
116
|
+
offset: -acc,
|
|
117
|
+
color: d.color || "#999999",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
acc += baseLen;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return list;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const segmentsTimed = computed<SegTimed[]>(() => {
|
|
128
|
+
const totalDur = Math.max(0.01, INITIAL_TOTAL_DURATION);
|
|
129
|
+
let running = 0;
|
|
130
|
+
|
|
131
|
+
return segmentsBase.value.map((s) => {
|
|
132
|
+
const dur = (s.length / Math.max(1e-6, circumference.value)) * totalDur;
|
|
133
|
+
const delay = Math.max(0, running - OVERLAP);
|
|
134
|
+
const segT: SegTimed = { ...s, dur, delay };
|
|
135
|
+
running += dur;
|
|
136
|
+
return segT;
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const isInitial = ref(true);
|
|
141
|
+
|
|
142
|
+
watch(
|
|
143
|
+
() => ({ ready: isReady.value }),
|
|
144
|
+
({ ready }) => {
|
|
145
|
+
if (!ready) return;
|
|
146
|
+
|
|
147
|
+
if (initialTimer.value) return;
|
|
148
|
+
|
|
149
|
+
const totalDur = INITIAL_TOTAL_DURATION;
|
|
150
|
+
|
|
151
|
+
initialTimer.value = setTimeout(() => {
|
|
152
|
+
isInitial.value = false;
|
|
153
|
+
if (initialTimer.value) {
|
|
154
|
+
clearTimeout(initialTimer.value);
|
|
155
|
+
initialTimer.value = null;
|
|
156
|
+
}
|
|
157
|
+
}, Math.ceil(totalDur * 1000) + 16);
|
|
158
|
+
},
|
|
159
|
+
{ immediate: true }
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
onMounted(() => {
|
|
163
|
+
startResizeObserver();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
onUnmounted(() => {
|
|
167
|
+
stopResizeObserver();
|
|
168
|
+
|
|
169
|
+
if (initialTimer.value) {
|
|
170
|
+
clearTimeout(initialTimer.value);
|
|
171
|
+
initialTimer.value = null;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const transitionFor = (seg: SegTimed) => {
|
|
176
|
+
if (isInitial.value)
|
|
177
|
+
return { delay: seg.delay, duration: seg.dur, easing: EASING } as const;
|
|
178
|
+
|
|
179
|
+
return { delay: 0, duration: UPDATE_DURATION, easing: EASING };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const measure = () => {
|
|
183
|
+
const el = wrapperRef.value ?? svgRef.value;
|
|
184
|
+
|
|
185
|
+
if (!el) return;
|
|
186
|
+
|
|
187
|
+
const rect = el.getBoundingClientRect();
|
|
188
|
+
const next = Math.max(1, Math.min(rect.width || 0, rect.height || 0));
|
|
189
|
+
|
|
190
|
+
if (Math.abs(next - measuredSize.value) > SIZE_EPSILON)
|
|
191
|
+
measuredSize.value = next;
|
|
192
|
+
|
|
193
|
+
if (!isReady.value && next > 1) isReady.value = true;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const startResizeObserver = () => {
|
|
197
|
+
if (roRef.value) return;
|
|
198
|
+
|
|
199
|
+
roRef.value = new ResizeObserver(measure);
|
|
200
|
+
|
|
201
|
+
const el = wrapperRef.value ?? svgRef.value;
|
|
202
|
+
|
|
203
|
+
if (el) roRef.value.observe(el);
|
|
204
|
+
|
|
205
|
+
requestAnimationFrame(measure);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const stopResizeObserver = () => {
|
|
209
|
+
if (!roRef.value) return;
|
|
210
|
+
|
|
211
|
+
const el = wrapperRef.value ?? svgRef.value;
|
|
212
|
+
|
|
213
|
+
if (el) roRef.value.unobserve(el);
|
|
214
|
+
|
|
215
|
+
roRef.value.disconnect();
|
|
216
|
+
roRef.value = null;
|
|
217
|
+
};
|
|
218
|
+
</script>
|
|
@@ -52,7 +52,6 @@
|
|
|
52
52
|
icon="search"
|
|
53
53
|
iconPositon="right"
|
|
54
54
|
placeholder="Search..."
|
|
55
|
-
hide-helper
|
|
56
55
|
auto-focus
|
|
57
56
|
/>
|
|
58
57
|
</div>
|
|
@@ -90,19 +89,29 @@
|
|
|
90
89
|
{{ props.label }}
|
|
91
90
|
</div>
|
|
92
91
|
</div>
|
|
93
|
-
<
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
92
|
+
<animate-presence mode="wait" :initial="false">
|
|
93
|
+
<motion.div
|
|
94
|
+
v-if="!!text"
|
|
95
|
+
class="overflow-hidden"
|
|
96
|
+
:initial="{ height: 0 }"
|
|
97
|
+
:animate="{ height: 'auto' }"
|
|
98
|
+
:exit="{ height: 0 }"
|
|
99
|
+
:transition="{ duration: 0.1, ease: 'easeInOut' }"
|
|
100
|
+
>
|
|
101
|
+
<animate-presence mode="wait" :initial="false">
|
|
102
|
+
<motion.p
|
|
103
|
+
:key="status + text"
|
|
104
|
+
:class="helperStyles({ status })"
|
|
105
|
+
:initial="{ opacity: 0, y: -4 }"
|
|
106
|
+
:animate="{ opacity: 1, y: 0 }"
|
|
107
|
+
:exit="{ opacity: 0, y: 4 }"
|
|
108
|
+
:transition="{ duration: 0.12, ease: 'easeInOut', delay: 0.1 }"
|
|
109
|
+
>
|
|
110
|
+
{{ text }}
|
|
111
|
+
</motion.p>
|
|
112
|
+
</animate-presence>
|
|
113
|
+
</motion.div>
|
|
114
|
+
</animate-presence>
|
|
106
115
|
</div>
|
|
107
116
|
</template>
|
|
108
117
|
|
|
@@ -19,28 +19,34 @@
|
|
|
19
19
|
{{ props.label }}
|
|
20
20
|
</label>
|
|
21
21
|
</div>
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
<animate-presence mode="wait" :initial="false">
|
|
23
|
+
<motion.div
|
|
24
|
+
v-if="!!text"
|
|
25
|
+
class="overflow-hidden"
|
|
26
|
+
:initial="{ height: 0 }"
|
|
27
|
+
:animate="{ height: 'auto' }"
|
|
28
|
+
:exit="{ height: 0 }"
|
|
29
|
+
:transition="{ duration: 0.1, ease: 'easeInOut' }"
|
|
30
|
+
>
|
|
31
|
+
<animate-presence mode="wait" :initial="false">
|
|
32
|
+
<motion.p
|
|
33
|
+
:key="status + text"
|
|
32
34
|
:class="
|
|
33
35
|
helperStyles({
|
|
34
36
|
status,
|
|
35
37
|
align: props.alignText ?? 'left',
|
|
36
38
|
})
|
|
37
39
|
"
|
|
40
|
+
:initial="{ opacity: 0, y: -4 }"
|
|
41
|
+
:animate="{ opacity: 1, y: 0 }"
|
|
42
|
+
:exit="{ opacity: 0, y: 4 }"
|
|
43
|
+
:transition="{ duration: 0.12, ease: 'easeInOut', delay: 0.1 }"
|
|
38
44
|
>
|
|
39
45
|
{{ text }}
|
|
40
|
-
</p>
|
|
41
|
-
</
|
|
42
|
-
</
|
|
43
|
-
</
|
|
46
|
+
</motion.p>
|
|
47
|
+
</animate-presence>
|
|
48
|
+
</motion.div>
|
|
49
|
+
</animate-presence>
|
|
44
50
|
</div>
|
|
45
51
|
</template>
|
|
46
52
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export * from "./Accordion";
|
|
2
|
+
export * from "./Alert";
|
|
3
|
+
export * from "./Avatar";
|
|
4
|
+
export * from "./Badge";
|
|
5
|
+
export * from "./Button";
|
|
6
|
+
export * from "./Calendar";
|
|
7
|
+
export * from "./Card";
|
|
8
|
+
export * from "./Checkbox";
|
|
9
|
+
export * from "./Command";
|
|
10
|
+
export * from "./Icon";
|
|
11
|
+
export * from "./IconButton";
|
|
12
|
+
export * from "./Input";
|
|
13
|
+
export * from "./InputOTP";
|
|
14
|
+
export * from "./Link";
|
|
15
|
+
export * from "./Loader";
|
|
16
|
+
export * from "./Modal";
|
|
17
|
+
export * from "./PieChart";
|
|
18
|
+
export * from "./Popover";
|
|
19
|
+
export * from "./Radio";
|
|
20
|
+
export * from "./Scale";
|
|
21
|
+
export * from "./Select";
|
|
22
|
+
export * from "./Separator";
|
|
23
|
+
export * from "./Sheet";
|
|
24
|
+
export * from "./Skeleton";
|
|
25
|
+
export * from "./Slider";
|
|
26
|
+
export * from "./Switch";
|
|
27
|
+
export * from "./TableOfContents";
|
|
28
|
+
export * from "./Tabs";
|
|
29
|
+
export * from "./Textarea";
|
|
30
|
+
export * from "./Theme";
|
|
31
|
+
export * from "./TimePicker";
|
|
32
|
+
export * from "./Toast";
|
|
33
|
+
export * from "./Toggle";
|
|
34
|
+
export * from "./Tooltip";
|
|
35
|
+
export * from "./ColorPicker";
|
|
@@ -48,6 +48,12 @@
|
|
|
48
48
|
"packages": ["motion-v"],
|
|
49
49
|
"composables": []
|
|
50
50
|
},
|
|
51
|
+
{
|
|
52
|
+
"name": "ColorPicker",
|
|
53
|
+
"related-components": ["Card"],
|
|
54
|
+
"packages": [],
|
|
55
|
+
"composables": []
|
|
56
|
+
},
|
|
51
57
|
{
|
|
52
58
|
"name": "Command",
|
|
53
59
|
"related-components": ["Modal", "Card", "Link", "Icon", "Separator"],
|
|
@@ -70,7 +76,7 @@
|
|
|
70
76
|
"name": "Input",
|
|
71
77
|
"related-components": ["Icon"],
|
|
72
78
|
"packages": ["motion-v"],
|
|
73
|
-
"composables": []
|
|
79
|
+
"composables": ["useInputValidation"]
|
|
74
80
|
},
|
|
75
81
|
{
|
|
76
82
|
"name": "InputOTP",
|
|
@@ -96,6 +102,12 @@
|
|
|
96
102
|
"packages": ["motion-v"],
|
|
97
103
|
"composables": []
|
|
98
104
|
},
|
|
105
|
+
{
|
|
106
|
+
"name": "PieChart",
|
|
107
|
+
"related-components": [],
|
|
108
|
+
"packages": ["motion-v"],
|
|
109
|
+
"composables": []
|
|
110
|
+
},
|
|
99
111
|
{
|
|
100
112
|
"name": "Popover",
|
|
101
113
|
"related-components": [],
|
|
@@ -118,7 +130,7 @@
|
|
|
118
130
|
"name": "Select",
|
|
119
131
|
"related-components": ["Badge", "Icon", "Input"],
|
|
120
132
|
"packages": ["motion-v"],
|
|
121
|
-
"composables": []
|
|
133
|
+
"composables": ["useInputValidation"]
|
|
122
134
|
},
|
|
123
135
|
{
|
|
124
136
|
"name": "Separator",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ref, computed, type Ref } from "vue";
|
|
2
|
+
|
|
3
|
+
export type UseValidationProps = {
|
|
4
|
+
validation: ((value: string) => true | string)[];
|
|
5
|
+
base?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function useInputValidation<
|
|
9
|
+
T extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement
|
|
10
|
+
>({ validation, base }: UseValidationProps) {
|
|
11
|
+
const value = ref(base ?? "");
|
|
12
|
+
const touched = ref(false);
|
|
13
|
+
const error = ref<string | undefined>(undefined);
|
|
14
|
+
const elRef: Ref<T | null> = ref(null);
|
|
15
|
+
|
|
16
|
+
const onInput = (e: Event) => {
|
|
17
|
+
const target = e.target as T | null;
|
|
18
|
+
if (error.value) error.value = undefined;
|
|
19
|
+
if (target) value.value = target.value;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const onBlur = () => {
|
|
23
|
+
touched.value = true;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const valid = computed(
|
|
27
|
+
() => !error.value && validation.every((v) => v(value.value) === true)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const status = computed<"valid" | "error" | "default">(() => {
|
|
31
|
+
if (!touched.value) return "default";
|
|
32
|
+
if (error.value || !valid.value) return "error";
|
|
33
|
+
if (valid.value) return "valid";
|
|
34
|
+
|
|
35
|
+
return "default";
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const parsedValidation = computed(() => {
|
|
39
|
+
const found = validation.find((v) => v(value.value) !== true);
|
|
40
|
+
return found as ((v: string) => string) | undefined;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const errorText = computed(() =>
|
|
44
|
+
touched.value
|
|
45
|
+
? error.value || parsedValidation.value?.(value.value)
|
|
46
|
+
: undefined
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const reset = (hard = false) => {
|
|
50
|
+
touched.value = !hard;
|
|
51
|
+
error.value = undefined;
|
|
52
|
+
value.value = "";
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const setError = async (message: string) => {
|
|
56
|
+
error.value = message;
|
|
57
|
+
touched.value = true;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const bindings = computed<Record<string, any>>(() => ({
|
|
61
|
+
ref: elRef,
|
|
62
|
+
modelValue: value.value,
|
|
63
|
+
["onUpdate:modelValue"]: (v: string) => {
|
|
64
|
+
value.value = v;
|
|
65
|
+
},
|
|
66
|
+
status: status.value,
|
|
67
|
+
errorText: errorText.value,
|
|
68
|
+
onInput,
|
|
69
|
+
onBlur,
|
|
70
|
+
"aria-invalid": status.value === "error",
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
ref: elRef,
|
|
75
|
+
value,
|
|
76
|
+
touched,
|
|
77
|
+
errorText,
|
|
78
|
+
status,
|
|
79
|
+
valid,
|
|
80
|
+
error,
|
|
81
|
+
reset,
|
|
82
|
+
setError,
|
|
83
|
+
onInput,
|
|
84
|
+
onBlur,
|
|
85
|
+
bindings,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default useInputValidation;
|