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.
Files changed (64) hide show
  1. package/package.json +1 -1
  2. package/templates/assets/css/main.css +4 -0
  3. package/templates/components/Alert/Alert.types.ts +4 -0
  4. package/templates/components/Alert/AlertItem.component.vue +2 -4
  5. package/templates/components/Alert/index.ts +6 -1
  6. package/templates/components/Avatar/Avatar.component.vue +7 -3
  7. package/templates/components/Avatar/Avatar.styles.ts +3 -3
  8. package/templates/components/Breadcrumb/Breadcrumb.component.vue +116 -0
  9. package/templates/components/Breadcrumb/Breadcrumb.styles.ts +40 -0
  10. package/templates/components/Breadcrumb/Breadcrumb.types.ts +30 -0
  11. package/templates/components/Breadcrumb/index.ts +17 -0
  12. package/templates/components/Button/Button.styles.ts +3 -3
  13. package/templates/components/Calendar/Calendar.component.vue +76 -17
  14. package/templates/components/Calendar/Calendar.styles.ts +15 -3
  15. package/templates/components/Calendar/Calendar.types.ts +9 -0
  16. package/templates/components/Chart/Chart.component.vue +308 -0
  17. package/templates/components/Chart/Chart.types.ts +18 -0
  18. package/templates/components/Chart/index.ts +13 -0
  19. package/templates/components/Checkbox/Checkbox.styles.ts +1 -1
  20. package/templates/components/ColorPicker/ColorPicker.component.vue +4 -4
  21. package/templates/components/Command/Command.component.vue +6 -6
  22. package/templates/components/Command/Command.types.ts +1 -0
  23. package/templates/components/DatePicker/DatePicker.component.vue +219 -0
  24. package/templates/components/DatePicker/DatePicker.styles.ts +74 -0
  25. package/templates/components/DatePicker/DatePicker.types.ts +18 -0
  26. package/templates/components/DatePicker/DatePicker.utils.ts +46 -0
  27. package/templates/components/DatePicker/TimeWheel.vue +145 -0
  28. package/templates/components/DatePicker/TimeWheelColumn.vue +147 -0
  29. package/templates/components/DatePicker/index.ts +11 -0
  30. package/templates/components/DateRangePicker/DateRangePicker.component.vue +304 -0
  31. package/templates/components/DateRangePicker/DateRangePicker.types.ts +23 -0
  32. package/templates/components/DateRangePicker/index.ts +11 -0
  33. package/templates/components/FileUpload/FileUpload.component.vue +306 -0
  34. package/templates/components/FileUpload/FileUpload.styles.ts +74 -0
  35. package/templates/components/FileUpload/FileUpload.types.ts +31 -0
  36. package/templates/components/FileUpload/index.ts +15 -0
  37. package/templates/components/Input/Input.component.vue +27 -27
  38. package/templates/components/Input/Input.styles.ts +5 -5
  39. package/templates/components/Input/Input.types.ts +1 -0
  40. package/templates/components/InputOTP/InputOTP.component.vue +4 -2
  41. package/templates/components/InputOTP/InputOTP.types.ts +1 -0
  42. package/templates/components/Modal/Modal.component.vue +28 -4
  43. package/templates/components/Modal/Modal.types.ts +3 -1
  44. package/templates/components/Popover/Popover.component.vue +33 -2
  45. package/templates/components/Popover/Popover.types.ts +1 -0
  46. package/templates/components/SegmentedControl/SegmentedControl.component.vue +85 -0
  47. package/templates/components/SegmentedControl/SegmentedControl.styles.ts +55 -0
  48. package/templates/components/SegmentedControl/SegmentedControl.types.ts +15 -0
  49. package/templates/components/SegmentedControl/index.ts +11 -0
  50. package/templates/components/Select/Select.component.vue +253 -108
  51. package/templates/components/Select/Select.styles.ts +6 -6
  52. package/templates/components/Select/Select.types.ts +12 -0
  53. package/templates/components/Select/SelectTreeItem.vue +154 -0
  54. package/templates/components/Select/composables/useSelect.composable.ts +260 -42
  55. package/templates/components/Tabs/Tabs.component.vue +11 -2
  56. package/templates/components/Tabs/Tabs.types.ts +1 -0
  57. package/templates/components/Textarea/Textarea.component.vue +23 -32
  58. package/templates/components/Textarea/Textarea.styles.ts +4 -4
  59. package/templates/components/Textarea/Textarea.types.ts +1 -0
  60. package/templates/components/TimePicker/TimePicker.component.vue +21 -18
  61. package/templates/components/TimePicker/TimePicker.styles.ts +1 -1
  62. package/templates/components/TimePicker/TimePicker.types.ts +1 -0
  63. package/templates/components.json +37 -1
  64. 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
+ }
@@ -0,0 +1,13 @@
1
+ import UixyChart from "./Chart.component.vue";
2
+ import type {
3
+ UixyChartData,
4
+ UixyChartProps,
5
+ UixyChartSeries,
6
+ } from "./Chart.types";
7
+
8
+ export {
9
+ UixyChart,
10
+ type UixyChartData,
11
+ type UixyChartProps,
12
+ type UixyChartSeries,
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-xs", {
16
+ export const labelStyles = styles("text-sm", {
17
17
  disabled: {
18
18
  true: "text-gray-500 dark:text-gray-600",
19
19
  false: "",
@@ -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-[320px] overflow-y-auto">
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.");
@@ -16,4 +16,5 @@ export interface UixyCommandProps {
16
16
  options: UixyCommandItems[];
17
17
  placeholder?: string;
18
18
  emptyText?: string;
19
+ persistent?: boolean;
19
20
  }
@@ -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>