v-uixy 1.3.1 → 1.5.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/package.json +1 -1
- package/templates/assets/css/main.css +4 -0
- package/templates/components/Alert/Alert.types.ts +4 -0
- package/templates/components/Alert/AlertItem.component.vue +2 -4
- package/templates/components/Alert/index.ts +6 -1
- package/templates/components/Avatar/Avatar.component.vue +7 -3
- package/templates/components/Avatar/Avatar.styles.ts +3 -3
- package/templates/components/Breadcrumb/Breadcrumb.component.vue +116 -0
- package/templates/components/Breadcrumb/Breadcrumb.styles.ts +40 -0
- package/templates/components/Breadcrumb/Breadcrumb.types.ts +30 -0
- package/templates/components/Breadcrumb/index.ts +17 -0
- package/templates/components/Button/Button.styles.ts +3 -3
- package/templates/components/Calendar/Calendar.component.vue +76 -17
- package/templates/components/Calendar/Calendar.styles.ts +15 -3
- package/templates/components/Calendar/Calendar.types.ts +9 -0
- package/templates/components/Chart/Chart.component.vue +308 -0
- package/templates/components/Chart/Chart.types.ts +18 -0
- package/templates/components/Chart/index.ts +13 -0
- package/templates/components/Checkbox/Checkbox.styles.ts +1 -1
- package/templates/components/ColorPicker/ColorPicker.component.vue +4 -4
- package/templates/components/Command/Command.component.vue +6 -6
- package/templates/components/Command/Command.types.ts +1 -0
- package/templates/components/DatePicker/DatePicker.component.vue +219 -0
- package/templates/components/DatePicker/DatePicker.styles.ts +74 -0
- package/templates/components/DatePicker/DatePicker.types.ts +18 -0
- package/templates/components/DatePicker/DatePicker.utils.ts +46 -0
- package/templates/components/DatePicker/TimeWheel.vue +145 -0
- package/templates/components/DatePicker/TimeWheelColumn.vue +147 -0
- package/templates/components/DatePicker/index.ts +11 -0
- package/templates/components/DateRangePicker/DateRangePicker.component.vue +304 -0
- package/templates/components/DateRangePicker/DateRangePicker.types.ts +23 -0
- package/templates/components/DateRangePicker/index.ts +11 -0
- package/templates/components/FileUpload/FileUpload.component.vue +306 -0
- package/templates/components/FileUpload/FileUpload.styles.ts +74 -0
- package/templates/components/FileUpload/FileUpload.types.ts +31 -0
- package/templates/components/FileUpload/index.ts +15 -0
- package/templates/components/Input/Input.component.vue +27 -27
- package/templates/components/Input/Input.styles.ts +5 -5
- package/templates/components/Input/Input.types.ts +1 -0
- package/templates/components/InputOTP/InputOTP.component.vue +4 -2
- package/templates/components/InputOTP/InputOTP.types.ts +1 -0
- package/templates/components/Modal/Modal.component.vue +28 -4
- package/templates/components/Modal/Modal.types.ts +3 -1
- package/templates/components/Popover/Popover.component.vue +33 -2
- package/templates/components/Popover/Popover.types.ts +1 -0
- package/templates/components/SegmentedControl/SegmentedControl.component.vue +85 -0
- package/templates/components/SegmentedControl/SegmentedControl.styles.ts +55 -0
- package/templates/components/SegmentedControl/SegmentedControl.types.ts +15 -0
- package/templates/components/SegmentedControl/index.ts +11 -0
- package/templates/components/Select/Select.component.vue +253 -108
- package/templates/components/Select/Select.styles.ts +6 -6
- package/templates/components/Select/Select.types.ts +12 -0
- package/templates/components/Select/SelectTreeItem.vue +154 -0
- package/templates/components/Select/composables/useSelect.composable.ts +260 -42
- package/templates/components/Tabs/Tabs.component.vue +11 -2
- package/templates/components/Tabs/Tabs.types.ts +1 -0
- package/templates/components/Textarea/Textarea.component.vue +23 -32
- package/templates/components/Textarea/Textarea.styles.ts +4 -4
- package/templates/components/Textarea/Textarea.types.ts +1 -0
- package/templates/components/TimePicker/TimePicker.component.vue +21 -18
- package/templates/components/TimePicker/TimePicker.styles.ts +1 -1
- package/templates/components/TimePicker/TimePicker.types.ts +1 -0
- package/templates/components.json +37 -1
- package/templates/composables/usePosition.ts +17 -2
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative w-full h-full">
|
|
3
|
+
<svg
|
|
4
|
+
width="100%"
|
|
5
|
+
height="100%"
|
|
6
|
+
:viewBox="`0 0 ${vbWidth} ${vbHeight}`"
|
|
7
|
+
preserveAspectRatio="xMidYMid meet"
|
|
8
|
+
role="img"
|
|
9
|
+
aria-label="Pixel chart"
|
|
10
|
+
class="block select-none pointer-events-none overflow-visible"
|
|
11
|
+
shape-rendering="crispEdges"
|
|
12
|
+
>
|
|
13
|
+
<rect
|
|
14
|
+
v-for="cell in renderCells"
|
|
15
|
+
:key="`base-${cell.key}`"
|
|
16
|
+
:x="cell.x"
|
|
17
|
+
:y="cell.y"
|
|
18
|
+
:width="CELL"
|
|
19
|
+
:height="CELL"
|
|
20
|
+
class="fill-gray-200 dark:fill-gray-900"
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<rect
|
|
24
|
+
v-for="cell in renderCells"
|
|
25
|
+
:key="`color-${cell.key}`"
|
|
26
|
+
:x="cell.x"
|
|
27
|
+
:y="cell.y"
|
|
28
|
+
:width="CELL"
|
|
29
|
+
:height="CELL"
|
|
30
|
+
:class="cell.cls"
|
|
31
|
+
:fill="cell.fill"
|
|
32
|
+
:style="{
|
|
33
|
+
opacity: cell.filled ? 1 : 0,
|
|
34
|
+
transition: props.animate
|
|
35
|
+
? `opacity ${REVEAL_DURATION}s ease-out`
|
|
36
|
+
: 'none',
|
|
37
|
+
}"
|
|
38
|
+
/>
|
|
39
|
+
</svg>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script setup lang="ts">
|
|
44
|
+
import { computed, onUnmounted, ref, watch } from "vue";
|
|
45
|
+
import type { UixyChartProps, UixyChartSeries } from "./Chart.types";
|
|
46
|
+
|
|
47
|
+
const CELL = 12;
|
|
48
|
+
const GAP = 2;
|
|
49
|
+
|
|
50
|
+
const COLUMN_STEP = 0.05;
|
|
51
|
+
const ROW_STEP = 0.04;
|
|
52
|
+
const REVEAL_DURATION = 0.18;
|
|
53
|
+
|
|
54
|
+
const TOP_HEADROOM = 0.15;
|
|
55
|
+
const FADE_ROWS = 2;
|
|
56
|
+
|
|
57
|
+
const DEFAULT_CLASSES = [
|
|
58
|
+
"fill-gray-500 dark:fill-gray-600",
|
|
59
|
+
"fill-gray-800 dark:fill-gray-300",
|
|
60
|
+
"fill-gray-900 dark:fill-gray-200",
|
|
61
|
+
"fill-black dark:fill-white",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const props = withDefaults(defineProps<UixyChartProps>(), {
|
|
65
|
+
rows: 8,
|
|
66
|
+
animate: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
type Paint = { fill?: string; cls?: string };
|
|
70
|
+
|
|
71
|
+
type Cell = {
|
|
72
|
+
key: string;
|
|
73
|
+
x: number;
|
|
74
|
+
y: number;
|
|
75
|
+
filled: boolean;
|
|
76
|
+
fill?: string;
|
|
77
|
+
cls?: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const parseHex = (hex: string) => {
|
|
81
|
+
let h = hex.replace("#", "").trim();
|
|
82
|
+
if (h.length === 3)
|
|
83
|
+
h = h
|
|
84
|
+
.split("")
|
|
85
|
+
.map((c) => c + c)
|
|
86
|
+
.join("");
|
|
87
|
+
const n = parseInt(h, 16);
|
|
88
|
+
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const toHex = (r: number, g: number, b: number) => {
|
|
92
|
+
const channel = (v: number) =>
|
|
93
|
+
Math.max(0, Math.min(255, Math.round(v)))
|
|
94
|
+
.toString(16)
|
|
95
|
+
.padStart(2, "0");
|
|
96
|
+
return `#${channel(r)}${channel(g)}${channel(b)}`;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const mix = (a: string, b: string, t: number) => {
|
|
100
|
+
const from = parseHex(a);
|
|
101
|
+
const to = parseHex(b);
|
|
102
|
+
return toHex(
|
|
103
|
+
from.r + (to.r - from.r) * t,
|
|
104
|
+
from.g + (to.g - from.g) * t,
|
|
105
|
+
from.b + (to.b - from.b) * t,
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const peak = computed(() => props.color ?? "#000000");
|
|
110
|
+
const base = computed(
|
|
111
|
+
() =>
|
|
112
|
+
props.baseColor ??
|
|
113
|
+
(props.color ? mix(props.color, "#000000", 0.4) : "#000000"),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const paintForDepth = (depth: number): Paint => {
|
|
117
|
+
if (!props.color) {
|
|
118
|
+
const index = depth <= 0 ? 0 : depth > FADE_ROWS ? 3 : depth;
|
|
119
|
+
return { cls: DEFAULT_CLASSES[index] };
|
|
120
|
+
}
|
|
121
|
+
if (depth <= 0) return { fill: peak.value };
|
|
122
|
+
if (depth > FADE_ROWS) return { fill: base.value };
|
|
123
|
+
return { fill: mix(peak.value, base.value, depth / (FADE_ROWS + 1)) };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const basePaint = (): Paint =>
|
|
127
|
+
props.color ? { fill: base.value } : { cls: DEFAULT_CLASSES[3] };
|
|
128
|
+
|
|
129
|
+
const rows = computed(() => Math.max(1, Math.floor(props.rows)));
|
|
130
|
+
|
|
131
|
+
const series = computed<UixyChartSeries[]>(() => {
|
|
132
|
+
if (props.series?.length) return props.series;
|
|
133
|
+
if (props.data?.length)
|
|
134
|
+
return [{ label: "data", values: props.data.map((d) => d.value) }];
|
|
135
|
+
return [];
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const columnCount = computed(() =>
|
|
139
|
+
Math.max(0, ...series.value.map((s) => s.values.length)),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const columnTotals = computed(() =>
|
|
143
|
+
Array.from({ length: columnCount.value }, (_, c) =>
|
|
144
|
+
series.value.reduce((sum, s) => sum + Math.max(0, s.values[c] ?? 0), 0),
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const usableRows = computed(() =>
|
|
149
|
+
Math.max(1, Math.floor(rows.value * (1 - TOP_HEADROOM))),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const scale = computed(
|
|
153
|
+
() => usableRows.value / Math.max(1, Math.max(0, ...columnTotals.value)),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const barHeights = computed(() =>
|
|
157
|
+
columnTotals.value.map((total) =>
|
|
158
|
+
Math.min(usableRows.value, Math.round(total * scale.value)),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const vbWidth = computed(
|
|
163
|
+
() => columnCount.value * CELL + Math.max(0, columnCount.value - 1) * GAP,
|
|
164
|
+
);
|
|
165
|
+
const vbHeight = computed(() => rows.value * CELL + (rows.value - 1) * GAP);
|
|
166
|
+
|
|
167
|
+
type Scheduled = {
|
|
168
|
+
apply: () => void;
|
|
169
|
+
done: boolean;
|
|
170
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let prevHeights: number[] = [];
|
|
174
|
+
let prevPaint: Record<string, Paint> = {};
|
|
175
|
+
let scheduled: Scheduled[] = [];
|
|
176
|
+
|
|
177
|
+
const renderCells = ref<Cell[]>([]);
|
|
178
|
+
|
|
179
|
+
const flushScheduled = () => {
|
|
180
|
+
scheduled.forEach((s) => {
|
|
181
|
+
if (s.timer) clearTimeout(s.timer);
|
|
182
|
+
if (!s.done) s.apply();
|
|
183
|
+
});
|
|
184
|
+
scheduled = [];
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const ensureCells = () => {
|
|
188
|
+
const needed = columnCount.value * rows.value;
|
|
189
|
+
if (renderCells.value.length === needed) return;
|
|
190
|
+
|
|
191
|
+
const cells: Cell[] = [];
|
|
192
|
+
for (let col = 0; col < columnCount.value; col++) {
|
|
193
|
+
const x = col * (CELL + GAP);
|
|
194
|
+
for (let row = 0; row < rows.value; row++) {
|
|
195
|
+
const y = vbHeight.value - (row + 1) * CELL - row * GAP;
|
|
196
|
+
cells.push({
|
|
197
|
+
key: `${col}-${row}`,
|
|
198
|
+
x,
|
|
199
|
+
y,
|
|
200
|
+
filled: false,
|
|
201
|
+
cls: DEFAULT_CLASSES[3],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
renderCells.value = cells;
|
|
207
|
+
prevHeights = [];
|
|
208
|
+
prevPaint = {};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
type CellTarget = {
|
|
212
|
+
cell: Cell;
|
|
213
|
+
filled: boolean;
|
|
214
|
+
paint: Paint;
|
|
215
|
+
waveStep: number;
|
|
216
|
+
changed: boolean;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const build = () => {
|
|
220
|
+
flushScheduled();
|
|
221
|
+
ensureCells();
|
|
222
|
+
|
|
223
|
+
const heights = barHeights.value;
|
|
224
|
+
const nextPaint: Record<string, Paint> = {};
|
|
225
|
+
|
|
226
|
+
const columns = Array.from({ length: columnCount.value }, (_, col) => {
|
|
227
|
+
const height = heights[col] ?? 0;
|
|
228
|
+
const prevHeight = prevHeights[col] ?? 0;
|
|
229
|
+
const rising = height >= prevHeight;
|
|
230
|
+
let columnChanged = height !== prevHeight;
|
|
231
|
+
|
|
232
|
+
const targets: CellTarget[] = [];
|
|
233
|
+
for (let row = 0; row < rows.value; row++) {
|
|
234
|
+
const cell = renderCells.value[col * rows.value + row]!;
|
|
235
|
+
const filled = row < height;
|
|
236
|
+
const paint = filled
|
|
237
|
+
? paintForDepth(height - 1 - row)
|
|
238
|
+
: (prevPaint[cell.key] ?? basePaint());
|
|
239
|
+
nextPaint[cell.key] = paint;
|
|
240
|
+
|
|
241
|
+
const changed =
|
|
242
|
+
cell.filled !== filled ||
|
|
243
|
+
cell.fill !== paint.fill ||
|
|
244
|
+
cell.cls !== paint.cls;
|
|
245
|
+
if (changed) columnChanged = true;
|
|
246
|
+
|
|
247
|
+
targets.push({
|
|
248
|
+
cell,
|
|
249
|
+
filled,
|
|
250
|
+
paint,
|
|
251
|
+
waveStep: rising ? row : rows.value - 1 - row,
|
|
252
|
+
changed,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { columnChanged, targets };
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
let activeColumn = 0;
|
|
260
|
+
columns.forEach(({ columnChanged, targets }) => {
|
|
261
|
+
const columnDelay = activeColumn * COLUMN_STEP;
|
|
262
|
+
if (columnChanged) activeColumn++;
|
|
263
|
+
|
|
264
|
+
targets.forEach(({ cell, filled, paint, waveStep, changed }) => {
|
|
265
|
+
if (!changed) return;
|
|
266
|
+
|
|
267
|
+
const apply = () => {
|
|
268
|
+
cell.filled = filled;
|
|
269
|
+
cell.fill = paint.fill;
|
|
270
|
+
cell.cls = paint.cls;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const delay = props.animate ? columnDelay + waveStep * ROW_STEP : 0;
|
|
274
|
+
if (delay <= 0) {
|
|
275
|
+
apply();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const record: Scheduled = { apply, done: false };
|
|
280
|
+
record.timer = setTimeout(() => {
|
|
281
|
+
record.done = true;
|
|
282
|
+
apply();
|
|
283
|
+
}, delay * 1000);
|
|
284
|
+
scheduled.push(record);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
prevHeights = heights.slice();
|
|
289
|
+
prevPaint = nextPaint;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
watch(
|
|
293
|
+
[
|
|
294
|
+
barHeights,
|
|
295
|
+
() => props.color,
|
|
296
|
+
() => props.baseColor,
|
|
297
|
+
() => props.animate,
|
|
298
|
+
rows,
|
|
299
|
+
columnCount,
|
|
300
|
+
],
|
|
301
|
+
build,
|
|
302
|
+
{ immediate: true },
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
onUnmounted(() => {
|
|
306
|
+
scheduled.forEach((s) => s.timer && clearTimeout(s.timer));
|
|
307
|
+
});
|
|
308
|
+
</script>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type UixyChartSeries = {
|
|
2
|
+
label: string;
|
|
3
|
+
values: number[];
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type UixyChartData = {
|
|
7
|
+
label: string;
|
|
8
|
+
value: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface UixyChartProps {
|
|
12
|
+
series?: UixyChartSeries[];
|
|
13
|
+
data?: UixyChartData[];
|
|
14
|
+
color?: string;
|
|
15
|
+
baseColor?: string;
|
|
16
|
+
rows?: number;
|
|
17
|
+
animate?: boolean;
|
|
18
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<uixy-card class="p-2 flex flex-col gap-4">
|
|
3
3
|
<div
|
|
4
4
|
ref="svRef"
|
|
5
|
-
class="relative cursor-crosshair overflow-hidden rounded-1 w-64 h-48"
|
|
5
|
+
class="relative cursor-crosshair overflow-hidden rounded-1 min-w-64 h-48"
|
|
6
6
|
:style="svBackgroundStyle"
|
|
7
7
|
@mousedown="onSvMouseDown"
|
|
8
8
|
@touchstart.prevent="onSvTouchStart"
|
|
@@ -69,11 +69,11 @@
|
|
|
69
69
|
|
|
70
70
|
const rgbToHex8 = (r: number, g: number, b: number, a: number) =>
|
|
71
71
|
`#${componentToHex(r)}${componentToHex(g)}${componentToHex(
|
|
72
|
-
b
|
|
72
|
+
b,
|
|
73
73
|
)}${componentToHex(clamp(a, 0, 255))}`;
|
|
74
74
|
|
|
75
75
|
const hexToRgbA = (
|
|
76
|
-
hex?: string
|
|
76
|
+
hex?: string,
|
|
77
77
|
): {
|
|
78
78
|
r: number;
|
|
79
79
|
g: number;
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
const svRef = ref<HTMLElement | null>(null);
|
|
163
163
|
|
|
164
164
|
const rgb = computed(() =>
|
|
165
|
-
hsvToRgb(hue.value, sat.value / 100, val.value / 100)
|
|
165
|
+
hsvToRgb(hue.value, sat.value / 100, val.value / 100),
|
|
166
166
|
);
|
|
167
167
|
|
|
168
168
|
const updatingFromModel = ref(false);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<uixy-modal v-model="open">
|
|
2
|
+
<uixy-modal v-model="open" :persistent="props.persistent">
|
|
3
3
|
<uixy-card :class="commandStyles($attrs.class as string)">
|
|
4
4
|
<div class="flex items-center gap-2 p-3">
|
|
5
5
|
<label :for="id">
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
/>
|
|
23
23
|
</div>
|
|
24
24
|
<uixy-separator variant="horizontal" />
|
|
25
|
-
<div class="scrollbar max-h-
|
|
25
|
+
<div class="scrollbar max-h-80 overflow-y-auto">
|
|
26
26
|
<template v-for="option in filteredOptions" :key="option.label">
|
|
27
27
|
<template v-if="option.items.length > 0">
|
|
28
28
|
<ul class="flex flex-col gap-2 p-2">
|
|
@@ -79,13 +79,13 @@
|
|
|
79
79
|
props.options.map((option) => ({
|
|
80
80
|
label: option.label,
|
|
81
81
|
items: option.items.filter((item) =>
|
|
82
|
-
item.label.toLowerCase().includes(input.value?.toLowerCase() ?? "")
|
|
82
|
+
item.label.toLowerCase().includes(input.value?.toLowerCase() ?? ""),
|
|
83
83
|
),
|
|
84
|
-
}))
|
|
84
|
+
})),
|
|
85
85
|
);
|
|
86
86
|
|
|
87
87
|
const isEmpty = computed(() =>
|
|
88
|
-
filteredOptions.value.every((option) => option.items.length === 0)
|
|
88
|
+
filteredOptions.value.every((option) => option.items.length === 0),
|
|
89
89
|
);
|
|
90
90
|
|
|
91
91
|
const handleClose = () => {
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
};
|
|
94
94
|
|
|
95
95
|
const placeholder = computed(
|
|
96
|
-
() => props.placeholder || "Search or type a command..."
|
|
96
|
+
() => props.placeholder || "Search or type a command...",
|
|
97
97
|
);
|
|
98
98
|
|
|
99
99
|
const emptyText = computed(() => props.emptyText || "No results found.");
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex w-full flex-col gap-1">
|
|
3
|
+
<label v-if="props.label" :class="labelStyles({ status })">
|
|
4
|
+
{{ props.label }}
|
|
5
|
+
</label>
|
|
6
|
+
|
|
7
|
+
<div
|
|
8
|
+
ref="triggerRef"
|
|
9
|
+
role="button"
|
|
10
|
+
:tabindex="props.disabled ? -1 : 0"
|
|
11
|
+
:aria-expanded="active"
|
|
12
|
+
:class="triggerStyles({ status, open: active })"
|
|
13
|
+
@click="onTriggerClick"
|
|
14
|
+
@keydown.enter.prevent="onTriggerClick"
|
|
15
|
+
@keydown.space.prevent="onTriggerClick"
|
|
16
|
+
>
|
|
17
|
+
<uixy-icon
|
|
18
|
+
:name="props.icon ?? 'calendar'"
|
|
19
|
+
:class="triggerIconStyles({ disabled: props.disabled })"
|
|
20
|
+
/>
|
|
21
|
+
<span
|
|
22
|
+
:class="
|
|
23
|
+
triggerValueStyles({ placeholder: !model, disabled: props.disabled })
|
|
24
|
+
"
|
|
25
|
+
>
|
|
26
|
+
{{ displayValue }}
|
|
27
|
+
</span>
|
|
28
|
+
<uixy-icon
|
|
29
|
+
v-if="model && !props.disabled"
|
|
30
|
+
name="close"
|
|
31
|
+
class="h-4 w-4 shrink-0 cursor-pointer text-gray-400 hover:text-black dark:text-gray-600 dark:hover:text-white"
|
|
32
|
+
@click.stop="clear"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div v-if="!props.hideHelper && helperText" class="h-4">
|
|
37
|
+
<p :class="helperStyles({ status })">{{ helperText }}</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<teleport to="body">
|
|
41
|
+
<animate-presence>
|
|
42
|
+
<motion.div
|
|
43
|
+
v-if="active"
|
|
44
|
+
:initial="{ opacity: 0 }"
|
|
45
|
+
:animate="{ opacity: 1 }"
|
|
46
|
+
:exit="{ opacity: 0 }"
|
|
47
|
+
:transition="{ duration: 0.15, ease: 'easeInOut' }"
|
|
48
|
+
:style="positionStyles"
|
|
49
|
+
>
|
|
50
|
+
<div ref="refElement">
|
|
51
|
+
<motion.div
|
|
52
|
+
:initial="{ y: 4, scale: 0.96, opacity: 0 }"
|
|
53
|
+
:animate="{ y: 0, scale: 1, opacity: 1 }"
|
|
54
|
+
:exit="{ opacity: 0, scale: 0.96 }"
|
|
55
|
+
:transition="{ duration: 0.15, ease: 'easeInOut' }"
|
|
56
|
+
:class="panelStyles()"
|
|
57
|
+
>
|
|
58
|
+
<div class="flex items-stretch gap-3">
|
|
59
|
+
<uixy-calendar
|
|
60
|
+
:model-value="model"
|
|
61
|
+
:year="viewYear"
|
|
62
|
+
:month="viewMonth"
|
|
63
|
+
:start-of-week="props.startOfWeek"
|
|
64
|
+
:is-date-disabled="props.isDateDisabled"
|
|
65
|
+
@update:model-value="onDayPick"
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
<div v-if="props.withTime" class="flex items-center">
|
|
69
|
+
<uixy-time-wheel
|
|
70
|
+
v-model="timeParts"
|
|
71
|
+
:hour-cycle="24"
|
|
72
|
+
:with-seconds="props.withSeconds"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div :class="footerStyles()">
|
|
78
|
+
<uixy-button variant="tertiary" size="sm" @click="clear">
|
|
79
|
+
Clear
|
|
80
|
+
</uixy-button>
|
|
81
|
+
<uixy-button variant="primary" size="sm" @click="handleLeave">
|
|
82
|
+
Done
|
|
83
|
+
</uixy-button>
|
|
84
|
+
</div>
|
|
85
|
+
</motion.div>
|
|
86
|
+
</div>
|
|
87
|
+
</motion.div>
|
|
88
|
+
</animate-presence>
|
|
89
|
+
</teleport>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<script setup lang="ts">
|
|
94
|
+
import { AnimatePresence, motion } from "motion-v";
|
|
95
|
+
import { usePosition } from "~/composables";
|
|
96
|
+
import { UixyIcon } from "../Icon";
|
|
97
|
+
import { UixyButton } from "../Button";
|
|
98
|
+
import { UixyCalendar } from "../Calendar";
|
|
99
|
+
import UixyTimeWheel from "./TimeWheel.vue";
|
|
100
|
+
import type { UixyDatePickerProps } from "./DatePicker.types";
|
|
101
|
+
import {
|
|
102
|
+
triggerStyles,
|
|
103
|
+
triggerValueStyles,
|
|
104
|
+
triggerIconStyles,
|
|
105
|
+
labelStyles,
|
|
106
|
+
helperStyles,
|
|
107
|
+
panelStyles,
|
|
108
|
+
footerStyles,
|
|
109
|
+
} from "./DatePicker.styles";
|
|
110
|
+
import {
|
|
111
|
+
formatDate,
|
|
112
|
+
timePartsOf,
|
|
113
|
+
withTimeParts,
|
|
114
|
+
type UixyTimeParts,
|
|
115
|
+
} from "./DatePicker.utils";
|
|
116
|
+
|
|
117
|
+
const props = defineProps<UixyDatePickerProps>();
|
|
118
|
+
|
|
119
|
+
const model = defineModel<Date | null>();
|
|
120
|
+
|
|
121
|
+
const {
|
|
122
|
+
active,
|
|
123
|
+
styles: positionStyles,
|
|
124
|
+
refElement,
|
|
125
|
+
handleOpen,
|
|
126
|
+
handleLeave,
|
|
127
|
+
} = usePosition({ direction: props.direction ?? "bottom" });
|
|
128
|
+
|
|
129
|
+
const triggerRef = ref<HTMLElement>();
|
|
130
|
+
|
|
131
|
+
const draftTime = ref<UixyTimeParts>(timePartsOf(model.value));
|
|
132
|
+
|
|
133
|
+
const status = computed(() => {
|
|
134
|
+
if (props.disabled) return "disabled";
|
|
135
|
+
|
|
136
|
+
return props.status ?? "default";
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const helperText = computed(() => {
|
|
140
|
+
if (status.value === "error") return props.errorText ?? "";
|
|
141
|
+
|
|
142
|
+
return props.helperText ?? "";
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const displayValue = computed(() => {
|
|
146
|
+
if (!model.value) return props.placeholder ?? "Select date";
|
|
147
|
+
|
|
148
|
+
return formatDate(model.value, {
|
|
149
|
+
withTime: props.withTime,
|
|
150
|
+
withSeconds: props.withSeconds,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const viewBase = computed(() => model.value ?? new Date());
|
|
155
|
+
|
|
156
|
+
const viewYear = computed(() => viewBase.value.getFullYear());
|
|
157
|
+
|
|
158
|
+
const viewMonth = computed(() => viewBase.value.getMonth());
|
|
159
|
+
|
|
160
|
+
const timeParts = computed<UixyTimeParts>({
|
|
161
|
+
get: () => (model.value ? timePartsOf(model.value) : draftTime.value),
|
|
162
|
+
set: (parts) => {
|
|
163
|
+
draftTime.value = parts;
|
|
164
|
+
|
|
165
|
+
if (!model.value) return;
|
|
166
|
+
|
|
167
|
+
model.value = withTimeParts(model.value, parts);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const onTriggerClick = (event: MouseEvent | KeyboardEvent) => {
|
|
172
|
+
if (props.disabled) return;
|
|
173
|
+
|
|
174
|
+
handleOpen(event as MouseEvent);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const onDayPick = (date: Date | null | undefined) => {
|
|
178
|
+
if (!date) return;
|
|
179
|
+
|
|
180
|
+
model.value = withTimeParts(date, draftTime.value);
|
|
181
|
+
|
|
182
|
+
if (!props.withTime && props.closeOnSelect) handleLeave();
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const clear = () => {
|
|
186
|
+
model.value = null;
|
|
187
|
+
|
|
188
|
+
draftTime.value = { hour: 0, minute: 0, second: 0 };
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
watch(active, (isActive) => {
|
|
192
|
+
if (isActive) draftTime.value = timePartsOf(model.value);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const onDocClick = (event: MouseEvent) => {
|
|
196
|
+
if (!active.value) return;
|
|
197
|
+
|
|
198
|
+
const target = event.target as Node;
|
|
199
|
+
|
|
200
|
+
const clickedInside =
|
|
201
|
+
refElement.value?.contains(target) || triggerRef.value?.contains(target);
|
|
202
|
+
|
|
203
|
+
if (!clickedInside) handleLeave();
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
207
|
+
if (active.value && event.key === "Escape") handleLeave();
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
onMounted(() => {
|
|
211
|
+
window.addEventListener("click", onDocClick);
|
|
212
|
+
window.addEventListener("keydown", onKeyDown);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
onUnmounted(() => {
|
|
216
|
+
window.removeEventListener("click", onDocClick);
|
|
217
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
218
|
+
});
|
|
219
|
+
</script>
|