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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "v-uixy",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "private": false,
5
5
  "author": {
6
6
  "name": "Alan Haber",
@@ -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
  },
@@ -13,7 +13,7 @@ export const iconStyles = styles("h-3 w-3", {
13
13
  },
14
14
  });
15
15
 
16
- export const labelStyles = styles("text-sm", {
16
+ export const labelStyles = styles("text-xs", {
17
17
  disabled: {
18
18
  true: "text-gray-500 dark:text-gray-600",
19
19
  false: "",
@@ -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>
@@ -0,0 +1,3 @@
1
+ export interface UixyColorPickerProps {
2
+ disabled?: boolean;
3
+ }
@@ -0,0 +1,4 @@
1
+ import UixyColorPicker from "./ColorPicker.component.vue";
2
+ import type { UixyColorPickerProps } from "./ColorPicker.types";
3
+
4
+ export { UixyColorPicker, type UixyColorPickerProps };
@@ -27,19 +27,29 @@
27
27
  {{ props.label }}
28
28
  </label>
29
29
  </div>
30
- <div v-if="!props.hideHelper" class="h-4">
31
- <animate-presence mode="wait" :initial="false">
32
- <motion.div
33
- :initial="{ opacity: 0, y: '-4px' }"
34
- :animate="{ opacity: 1, y: 0 }"
35
- :exit="{ opacity: 0, y: '-4px' }"
36
- :transition="{ duration: 0.125 }"
37
- :key="status"
38
- >
39
- <p :class="helperStyles({ status })">{{ text }}</p>
40
- </motion.div>
41
- </animate-presence>
42
- </div>
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
 
@@ -45,7 +45,7 @@ export const iconStyles = styles(
45
45
  );
46
46
 
47
47
  export const labelStyles = styles(
48
- "transition-colors ease-in-out duration-150 text-sm font-500",
48
+ "transition-colors ease-in-out duration-150 text-xs font-500",
49
49
  {
50
50
  status: {
51
51
  default: "",
@@ -11,7 +11,6 @@ export interface UixyInputProps {
11
11
  helperText?: string;
12
12
  errorText?: string;
13
13
  autoFocus?: boolean;
14
- hideHelper?: boolean;
15
14
  }
16
15
 
17
16
  export interface UixyInputEmits {
@@ -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>
@@ -0,0 +1,10 @@
1
+ export type UixyPieChartData = {
2
+ label: string;
3
+ value: number;
4
+ color: string;
5
+ };
6
+
7
+ export interface UixyPieChartProps {
8
+ data: UixyPieChartData[];
9
+ thickness?: number;
10
+ }
@@ -0,0 +1,4 @@
1
+ import UixyPieChart from "./PieChart.component.vue";
2
+ import type { UixyPieChartProps, UixyPieChartData } from "./PieChart.types";
3
+
4
+ export { UixyPieChart, type UixyPieChartData, type UixyPieChartProps };
@@ -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
- <div v-if="!props.hideHelper" class="h-4">
94
- <animate-presence mode="wait" :initial="false">
95
- <motion.div
96
- :initial="{ opacity: 0, y: '-4px' }"
97
- :animate="{ opacity: 1, y: 0 }"
98
- :exit="{ opacity: 0, y: '-4px' }"
99
- :transition="{ duration: 0.125 }"
100
- :key="status"
101
- >
102
- <p :class="helperStyles({ status })">{{ text }}</p>
103
- </motion.div>
104
- </animate-presence>
105
- </div>
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
 
@@ -27,7 +27,7 @@ export const iconStyles = styles(
27
27
  );
28
28
 
29
29
  export const labelStyles = styles(
30
- "transition-colors ease-in-out duration-150 text-sm font-500",
30
+ "transition-colors ease-in-out duration-150 text-xs font-500",
31
31
  {
32
32
  status: {
33
33
  default: "",
@@ -26,6 +26,5 @@ export type UixySelectProps = {
26
26
  helperText?: string;
27
27
  errorText?: string;
28
28
  autoFocus?: boolean;
29
- hideHelper?: boolean;
30
29
  max?: number;
31
30
  } & (UixyMultipleSelectProps | UixySingleSelectProps);
@@ -79,4 +79,10 @@
79
79
  setScale(1);
80
80
  }
81
81
  );
82
+
83
+ onUnmounted(async () => {
84
+ await nextTick();
85
+
86
+ setScale(1);
87
+ });
82
88
  </script>
@@ -19,28 +19,34 @@
19
19
  {{ props.label }}
20
20
  </label>
21
21
  </div>
22
- <div class="h-4" v-if="!props.hideHelper">
23
- <animate-presence mode="wait" :initial="false">
24
- <motion.div
25
- :initial="{ opacity: 0, y: '-4px' }"
26
- :animate="{ opacity: 1, y: 0 }"
27
- :exit="{ opacity: 0, y: '-4px' }"
28
- :transition="{ duration: 0.125 }"
29
- :key="status"
30
- >
31
- <p
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
- </motion.div>
42
- </animate-presence>
43
- </div>
46
+ </motion.p>
47
+ </animate-presence>
48
+ </motion.div>
49
+ </animate-presence>
44
50
  </div>
45
51
  </template>
46
52
 
@@ -21,7 +21,7 @@ export const textareaStyles = styles(
21
21
  );
22
22
 
23
23
  export const labelStyles = styles(
24
- "transition-colors ease-in-out duration-150 text-sm font-500",
24
+ "transition-colors ease-in-out duration-150 text-xs font-500",
25
25
  {
26
26
  status: {
27
27
  default: "",
@@ -10,5 +10,4 @@ export interface UixyTextareaProps {
10
10
  noResize?: boolean;
11
11
  rows?: number;
12
12
  maxLength?: number;
13
- hideHelper?: boolean;
14
13
  }
@@ -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;