@aquiferre/ui-kit 0.3.0 → 0.4.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.
@@ -0,0 +1,563 @@
1
+ <template>
2
+ <div ref="rootRef" class="relative" :data-testid="baseTestId">
3
+ <!-- Trigger -->
4
+ <div
5
+ ref="triggerRef"
6
+ class="input flex items-center gap-1 cursor-pointer select-none"
7
+ :class="[disabled ? 'opacity-60 cursor-not-allowed' : '']"
8
+ @click="toggle"
9
+ >
10
+ <span v-if="hasValue" class="flex-1 min-w-0 truncate text-input-text text-xs">{{ displayText }}</span>
11
+ <span v-else class="flex-1 min-w-0"></span>
12
+ <button
13
+ v-if="clearable && hasValue && !disabled"
14
+ type="button"
15
+ class="text-text-tertiary hover:text-text-primary text-xs px-1"
16
+ @click.stop="clear"
17
+ tabindex="-1"
18
+ >&#x2715;</button>
19
+ <svg class="w-3.5 h-3.5 text-text-tertiary flex-shrink-0" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
20
+ <rect x="2" y="3" width="12" height="11" rx="1.5" />
21
+ <path d="M2 6.5h12" />
22
+ <path d="M5 1.5v3M11 1.5v3" />
23
+ </svg>
24
+ </div>
25
+
26
+ <!-- Panel -->
27
+ <Teleport to="body">
28
+ <div
29
+ v-if="open"
30
+ class="fixed card shadow-lg p-3"
31
+ :style="panelStyle"
32
+ @mousedown.stop
33
+ @click.stop
34
+ >
35
+ <!-- Calendar mode -->
36
+ <div v-if="pickerStyle === 'calendar'">
37
+ <div class="flex items-center justify-between mb-2">
38
+ <div class="relative">
39
+ <button type="button" class="btn-sm btn-ghost text-xs font-semibold" @click="yearDropdownOpen = !yearDropdownOpen">
40
+ {{ viewYear }}年 &#x25BE;
41
+ </button>
42
+ <div
43
+ v-if="yearDropdownOpen"
44
+ class="absolute top-full left-0 mt-1 card shadow-lg z-30 p-2 min-w-[160px]"
45
+ @mousedown.prevent
46
+ >
47
+ <div class="flex items-center justify-between mb-1.5 px-1">
48
+ <button type="button" class="btn-icon text-text-secondary hover:text-text-primary" @click="decadeStart -= 10">&#x25C0;</button>
49
+ <span class="text-xs text-text-secondary">{{ decadeStart }}&mdash;{{ decadeStart + 9 }}</span>
50
+ <button type="button" class="btn-icon text-text-secondary hover:text-text-primary" @click="decadeStart += 10">&#x25B6;</button>
51
+ </div>
52
+ <div class="grid grid-cols-2 gap-1">
53
+ <button
54
+ v-for="y in decadeYears" :key="y" type="button"
55
+ class="px-2 py-1 text-xs rounded-md transition-colors"
56
+ :class="y === viewYear ? 'bg-accent-primary text-white' : 'text-text-primary hover:bg-bg-hover'"
57
+ @click="selectYear(y)"
58
+ >{{ y }}</button>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ <div class="flex items-center gap-1">
63
+ <button type="button" class="btn-icon text-text-secondary hover:text-text-primary" @click="prevMonth">&#x25C0;</button>
64
+ <span class="text-xs font-semibold text-text-primary min-w-[3rem] text-center">{{ viewMonth }}月</span>
65
+ <button type="button" class="btn-icon text-text-secondary hover:text-text-primary" @click="nextMonth">&#x25B6;</button>
66
+ </div>
67
+ </div>
68
+ <div class="grid grid-cols-7 gap-0.5 mb-1">
69
+ <div v-for="d in weekDays" :key="d" class="text-center text-xs text-text-tertiary py-0.5 font-medium">{{ d }}</div>
70
+ </div>
71
+ <div class="grid grid-cols-7 gap-0.5">
72
+ <button
73
+ v-for="(day, i) in calendarDays" :key="i" type="button"
74
+ class="h-7 w-7 text-xs rounded-md transition-colors mx-auto"
75
+ :class="dayClass(day)"
76
+ :disabled="isDisabled(day.dateStr)"
77
+ @click="selectDate(day)"
78
+ >{{ day.day }}</button>
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Grid mode -->
83
+ <div v-else class="flex gap-3">
84
+ <div>
85
+ <div class="flex items-center justify-between mb-1.5 px-1">
86
+ <button type="button" class="btn-icon text-text-secondary hover:text-text-primary" @click="decadeStart -= 10">&#x25C0;</button>
87
+ <span class="text-xs text-text-secondary">{{ decadeStart }}&mdash;{{ decadeStart + 9 }}</span>
88
+ <button type="button" class="btn-icon text-text-secondary hover:text-text-primary" @click="decadeStart += 10">&#x25B6;</button>
89
+ </div>
90
+ <div class="grid grid-cols-2 gap-1">
91
+ <button
92
+ v-for="y in decadeYears" :key="y" type="button"
93
+ class="px-2 py-1 text-xs rounded-md transition-colors min-w-[3rem]"
94
+ :class="y === viewYear ? 'bg-accent-primary text-white' : 'bg-bg-secondary text-text-primary hover:bg-bg-hover'"
95
+ @click="selectYear(y)"
96
+ >{{ y }}</button>
97
+ </div>
98
+ </div>
99
+ <div class="w-px bg-border-primary" />
100
+ <div>
101
+ <div class="mb-1.5 px-1"><span class="text-xs text-text-secondary">月份</span></div>
102
+ <div class="grid grid-cols-2 gap-1">
103
+ <button
104
+ v-for="m in 12" :key="m" type="button"
105
+ class="px-2 py-1 text-xs rounded-md transition-colors min-w-[2.5rem]"
106
+ :class="m === viewMonth ? 'bg-accent-primary text-white' : 'bg-bg-secondary text-text-primary hover:bg-bg-hover'"
107
+ @click="selectMonth(m)"
108
+ >{{ m }}月</button>
109
+ </div>
110
+ </div>
111
+ <div class="w-px bg-border-primary" />
112
+ <div>
113
+ <div class="grid grid-cols-7 gap-0.5 mb-1">
114
+ <div v-for="d in weekDays" :key="d" class="text-center text-xs text-text-tertiary py-0.5 font-medium">{{ d }}</div>
115
+ </div>
116
+ <div class="grid grid-cols-7 gap-0.5">
117
+ <button
118
+ v-for="(day, i) in calendarDays" :key="i" type="button"
119
+ class="h-7 w-7 text-xs rounded-md transition-colors mx-auto"
120
+ :class="dayClass(day)"
121
+ :disabled="isDisabled(day.dateStr)"
122
+ @click="selectDate(day)"
123
+ >{{ day.day }}</button>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <!-- Time section -->
129
+ <div v-if="mode === 'datetime' || mode === 'time'" class="flex items-center justify-center gap-3 mt-3 pt-3 border-t border-border-primary">
130
+ <div v-for="col in timeColumns" :key="col.key" class="flex flex-col items-center gap-0.5">
131
+ <button type="button" class="text-text-tertiary hover:text-text-primary text-xs leading-none" @click="incTime(col.key)">&#x25B2;</button>
132
+ <input
133
+ v-if="editingTime === col.key"
134
+ ref="timeInputRefs"
135
+ :value="pad(col.val)"
136
+ class="input text-center text-xs w-10 py-0.5"
137
+ :maxlength="2"
138
+ @blur="finishTimeEdit(col.key, $event)"
139
+ @keydown.enter="finishTimeEdit(col.key, $event)"
140
+ />
141
+ <button v-else type="button" class="text-sm font-mono font-semibold text-text-primary hover:bg-bg-hover rounded px-2 py-0.5" @click="startTimeEdit(col.key)">{{ pad(col.val) }}</button>
142
+ <button type="button" class="text-text-tertiary hover:text-text-primary text-xs leading-none" @click="decTime(col.key)">&#x25BC;</button>
143
+ </div>
144
+ </div>
145
+
146
+ <!-- Confirm button -->
147
+ <div v-if="mode !== 'date'" class="flex justify-end mt-3 pt-2 border-t border-border-primary">
148
+ <button type="button" class="btn-sm btn-primary" @click="confirm">确认</button>
149
+ </div>
150
+ </div>
151
+ </Teleport>
152
+ </div>
153
+ </template>
154
+
155
+ <script setup lang="ts">
156
+ import { ref, computed, watch, onMounted, onUnmounted, nextTick, useAttrs } from 'vue'
157
+ import type { DateTimePreset } from '../types'
158
+
159
+ const props = withDefaults(defineProps<{
160
+ modelValue: string | [string, string]
161
+ mode?: 'date' | 'time' | 'datetime'
162
+ pickerStyle?: 'calendar' | 'grid'
163
+ range?: boolean
164
+ minuteStep?: number
165
+ secondStep?: number
166
+ disabledDate?: (date: string) => boolean
167
+ presets?: DateTimePreset[]
168
+ placeholder?: string
169
+ disabled?: boolean
170
+ clearable?: boolean
171
+ testId?: string
172
+ }>(), {
173
+ mode: 'date',
174
+ pickerStyle: 'calendar',
175
+ range: false,
176
+ minuteStep: 1,
177
+ secondStep: 1,
178
+ placeholder: '',
179
+ disabled: false,
180
+ clearable: true,
181
+ })
182
+
183
+ const emit = defineEmits<{
184
+ 'update:modelValue': [value: string | [string, string]]
185
+ 'change': [value: string | [string, string]]
186
+ 'clear': []
187
+ }>()
188
+
189
+ const attrs = useAttrs()
190
+ const baseTestId = computed(() => props.testId ?? (attrs['data-testid'] as string | undefined))
191
+
192
+ const rootRef = ref<HTMLElement | null>(null)
193
+ const triggerRef = ref<HTMLElement | null>(null)
194
+ const timeInputRefs = ref<HTMLInputElement[]>([])
195
+ const open = ref(false)
196
+ const yearDropdownOpen = ref(false)
197
+
198
+ const now = new Date()
199
+ const viewYear = ref(now.getFullYear())
200
+ const viewMonth = ref(now.getMonth() + 1)
201
+ const decadeStart = ref(Math.floor(now.getFullYear() / 10) * 10)
202
+
203
+ const selectedDate = ref('')
204
+ const rangeStart = ref('')
205
+ const rangeEnd = ref('')
206
+ const rangePending = ref(false)
207
+
208
+ const hours = ref(0)
209
+ const minutes = ref(0)
210
+ const seconds = ref(0)
211
+ const editingTime = ref<'h' | 'm' | 's' | null>(null)
212
+
213
+ const weekDays = ['日', '一', '二', '三', '四', '五', '六']
214
+
215
+ const decadeYears = computed(() => Array.from({ length: 10 }, (_, i) => decadeStart.value + i))
216
+
217
+ const hasValue = computed(() => {
218
+ if (props.range) {
219
+ const v = props.modelValue as [string, string]
220
+ return Array.isArray(v) && v[0] !== '' && v[1] !== ''
221
+ }
222
+ return typeof props.modelValue === 'string' && props.modelValue !== ''
223
+ })
224
+
225
+ const displayText = computed(() => {
226
+ if (props.mode === 'time') {
227
+ if (typeof props.modelValue === 'string' && props.modelValue) return props.modelValue
228
+ return ''
229
+ }
230
+ if (props.range) {
231
+ const v = props.modelValue as [string, string]
232
+ if (Array.isArray(v) && v[0] && v[1]) return `${fmtDisplay(v[0])} → ${fmtDisplay(v[1])}`
233
+ return ''
234
+ }
235
+ if (typeof props.modelValue === 'string' && props.modelValue) return fmtDisplay(props.modelValue)
236
+ return ''
237
+ })
238
+
239
+ function fmtDisplay(val: string): string {
240
+ if (props.mode === 'date') return val.substring(0, 10)
241
+ const d = val.substring(0, 10)
242
+ const t = val.includes('T') ? val.substring(11, 19) : '00:00:00'
243
+ return `${d} ${t}`
244
+ }
245
+
246
+ function pad(n: number): string { return String(n).padStart(2, '0') }
247
+
248
+ interface CalendarDay { day: number; dateStr: string; currentMonth: boolean }
249
+
250
+ const calendarDays = computed<CalendarDay[]>(() => {
251
+ const y = viewYear.value, m = viewMonth.value
252
+ const first = new Date(y, m - 1, 1)
253
+ const startDow = first.getDay()
254
+ const dim = daysInMonth(y, m)
255
+ const prevDim = daysInMonth(y, m - 1 || 12)
256
+ const days: CalendarDay[] = []
257
+
258
+ const pm = m - 1 || 12, py = m - 1 === 0 ? y - 1 : y
259
+ for (let i = startDow - 1; i >= 0; i--) {
260
+ const d = prevDim - i
261
+ days.push({ day: d, dateStr: `${py}-${pad(pm)}-${pad(d)}`, currentMonth: false })
262
+ }
263
+ for (let d = 1; d <= dim; d++) {
264
+ days.push({ day: d, dateStr: `${y}-${pad(m)}-${pad(d)}`, currentMonth: true })
265
+ }
266
+ const nm = m + 1 > 12 ? 1 : m + 1, ny = m + 1 > 12 ? y + 1 : y
267
+ const rem = 42 - days.length
268
+ for (let d = 1; d <= rem; d++) {
269
+ days.push({ day: d, dateStr: `${ny}-${pad(nm)}-${pad(d)}`, currentMonth: false })
270
+ }
271
+ return days
272
+ })
273
+
274
+ function daysInMonth(y: number, m: number): number {
275
+ if (m <= 0) { m += 12; y-- }
276
+ if (m > 12) { m -= 12; y++ }
277
+ return new Date(y, m, 0).getDate()
278
+ }
279
+
280
+ function dayClass(day: CalendarDay) {
281
+ const cls: string[] = []
282
+ if (!day.currentMonth) cls.push('text-text-tertiary')
283
+ else cls.push('text-text-primary')
284
+
285
+ if (isSelected(day.dateStr)) {
286
+ cls.push('bg-accent-primary', 'text-white')
287
+ } else if (isRangeStart(day.dateStr)) {
288
+ cls.push('bg-accent-primary', 'text-white', 'rounded-l-md')
289
+ } else if (isRangeEnd(day.dateStr)) {
290
+ cls.push('bg-accent-primary', 'text-white', 'rounded-r-md')
291
+ } else if (isInRange(day.dateStr)) {
292
+ cls.push('bg-accent-primary/15')
293
+ } else if (isToday(day.dateStr)) {
294
+ cls.push('ring-1', 'ring-accent-primary')
295
+ } else if (day.currentMonth) {
296
+ cls.push('hover:bg-bg-hover')
297
+ }
298
+ return cls
299
+ }
300
+
301
+ function isSelected(ds: string) {
302
+ if (props.range) return false
303
+ return selectedDate.value === ds
304
+ }
305
+
306
+ function isRangeStart(ds: string) {
307
+ if (!props.range || !rangeStart.value) return false
308
+ return ds === rangeStart.value && rangePending.value
309
+ }
310
+
311
+ function isRangeEnd(ds: string) {
312
+ if (!props.range || !rangeEnd.value) return false
313
+ if (rangePending.value) return false
314
+ return ds === rangeEnd.value
315
+ }
316
+
317
+ function isInRange(ds: string) {
318
+ if (!props.range || !rangeStart.value || !rangeEnd.value || rangePending.value) return false
319
+ return ds > rangeStart.value && ds < rangeEnd.value
320
+ }
321
+
322
+ function isToday(ds: string) {
323
+ const t = new Date()
324
+ return ds === `${t.getFullYear()}-${pad(t.getMonth() + 1)}-${pad(t.getDate())}`
325
+ }
326
+
327
+ function isDisabled(ds: string) {
328
+ return props.disabledDate ? props.disabledDate(ds) : false
329
+ }
330
+
331
+ const timeColumns = computed(() => [
332
+ { key: 'h' as const, val: hours.value },
333
+ { key: 'm' as const, val: minutes.value },
334
+ { key: 's' as const, val: seconds.value },
335
+ ])
336
+
337
+ const panelPos = ref({ top: 0, left: 0 })
338
+
339
+ const panelStyle = computed(() => ({
340
+ top: `${panelPos.value.top}px`,
341
+ left: `${panelPos.value.left}px`,
342
+ zIndex: 9999,
343
+ }))
344
+
345
+ function updatePosition() {
346
+ if (!triggerRef.value) return
347
+ const rect = triggerRef.value.getBoundingClientRect()
348
+ panelPos.value = { top: rect.bottom + 4, left: rect.left }
349
+ }
350
+
351
+ function prevMonth() {
352
+ if (viewMonth.value === 1) { viewMonth.value = 12; viewYear.value-- }
353
+ else viewMonth.value--
354
+ }
355
+
356
+ function nextMonth() {
357
+ if (viewMonth.value === 12) { viewMonth.value = 1; viewYear.value++ }
358
+ else viewMonth.value++
359
+ }
360
+
361
+ function selectYear(y: number) {
362
+ viewYear.value = y
363
+ yearDropdownOpen.value = false
364
+ }
365
+
366
+ function selectMonth(m: number) {
367
+ viewMonth.value = m
368
+ }
369
+
370
+ function selectDate(day: CalendarDay) {
371
+ if (isDisabled(day.dateStr)) return
372
+
373
+ if (props.range) {
374
+ if (!rangePending.value || rangeEnd.value) {
375
+ rangeStart.value = day.dateStr
376
+ rangeEnd.value = ''
377
+ rangePending.value = true
378
+ } else {
379
+ if (day.dateStr < rangeStart.value) {
380
+ rangeEnd.value = rangeStart.value
381
+ rangeStart.value = day.dateStr
382
+ } else {
383
+ rangeEnd.value = day.dateStr
384
+ }
385
+ rangePending.value = false
386
+ emitRange()
387
+ if (props.mode === 'date') { open.value = false; emit('change', props.modelValue) }
388
+ }
389
+ } else {
390
+ selectedDate.value = day.dateStr
391
+ if (props.mode === 'date') {
392
+ emitValue(day.dateStr)
393
+ open.value = false
394
+ emit('change', props.modelValue)
395
+ }
396
+ }
397
+ }
398
+
399
+ function emitValue(dateStr: string) {
400
+ let v: string
401
+ if (props.mode === 'date') v = dateStr
402
+ else if (props.mode === 'time') v = `${pad(hours.value)}:${pad(minutes.value)}:${pad(seconds.value)}`
403
+ else v = `${dateStr}T${pad(hours.value)}:${pad(minutes.value)}:${pad(seconds.value)}`
404
+ emit('update:modelValue', v)
405
+ }
406
+
407
+ function emitRange() {
408
+ const s = buildFull(rangeStart.value)
409
+ const e = buildFull(rangeEnd.value)
410
+ emit('update:modelValue', [s, e])
411
+ }
412
+
413
+ function buildFull(ds: string) {
414
+ if (props.mode === 'date') return ds
415
+ if (props.mode === 'time') return `${pad(hours.value)}:${pad(minutes.value)}:${pad(seconds.value)}`
416
+ return `${ds}T${pad(hours.value)}:${pad(minutes.value)}:${pad(seconds.value)}`
417
+ }
418
+
419
+ function confirm() {
420
+ if (props.range) {
421
+ if (!rangeStart.value) rangeStart.value = selectedDate.value || todayStr()
422
+ if (!rangeEnd.value) rangeEnd.value = rangeStart.value
423
+ emitRange()
424
+ } else {
425
+ const ds = selectedDate.value || todayStr()
426
+ emitValue(ds)
427
+ }
428
+ open.value = false
429
+ emit('change', props.modelValue)
430
+ }
431
+
432
+ function clear() {
433
+ if (props.range) emit('update:modelValue', ['', ''])
434
+ else emit('update:modelValue', props.mode === 'time' ? '' : '')
435
+ selectedDate.value = ''
436
+ rangeStart.value = ''
437
+ rangeEnd.value = ''
438
+ rangePending.value = false
439
+ open.value = false
440
+ emit('clear')
441
+ }
442
+
443
+ function toggle() {
444
+ if (props.disabled) return
445
+ open.value = !open.value
446
+ if (open.value) {
447
+ initFromModel()
448
+ yearDropdownOpen.value = false
449
+ nextTick(updatePosition)
450
+ } else {
451
+ editingTime.value = null
452
+ }
453
+ }
454
+
455
+ function todayStr() {
456
+ const t = new Date()
457
+ return `${t.getFullYear()}-${pad(t.getMonth() + 1)}-${pad(t.getDate())}`
458
+ }
459
+
460
+ function initFromModel() {
461
+ const v = props.modelValue
462
+ if (props.range && Array.isArray(v)) {
463
+ const [s, e] = v as [string, string]
464
+ if (s) {
465
+ const p = parseDatePart(s)
466
+ viewYear.value = p.y; viewMonth.value = p.m
467
+ rangeStart.value = p.ds
468
+ const pe = parseDatePart(e)
469
+ rangeEnd.value = pe.ds
470
+ rangePending.value = false
471
+ if (props.mode !== 'date') { setTimeFromStr(s) }
472
+ }
473
+ } else if (typeof v === 'string' && v) {
474
+ if (props.mode === 'time') {
475
+ setTimeFromStr(v)
476
+ } else {
477
+ const p = parseDatePart(v)
478
+ viewYear.value = p.y; viewMonth.value = p.m
479
+ selectedDate.value = p.ds
480
+ if (props.mode === 'datetime') setTimeFromStr(v)
481
+ }
482
+ } else {
483
+ const t = new Date()
484
+ viewYear.value = t.getFullYear()
485
+ viewMonth.value = t.getMonth() + 1
486
+ hours.value = t.getHours()
487
+ minutes.value = t.getMinutes()
488
+ seconds.value = t.getSeconds()
489
+ selectedDate.value = todayStr()
490
+ rangeStart.value = ''
491
+ rangeEnd.value = ''
492
+ rangePending.value = false
493
+ }
494
+ decadeStart.value = Math.floor(viewYear.value / 10) * 10
495
+ }
496
+
497
+ function parseDatePart(s: string) {
498
+ const ds = s.substring(0, 10)
499
+ const parts = ds.split('-')
500
+ return { y: +parts[0], m: +parts[1], ds }
501
+ }
502
+
503
+ function setTimeFromStr(s: string) {
504
+ const idx = s.indexOf('T')
505
+ const ts = idx >= 0 ? s.substring(idx + 1) : (s.includes(':') ? s : '00:00:00')
506
+ const p = ts.split(':')
507
+ hours.value = +p[0] || 0
508
+ minutes.value = +p[1] || 0
509
+ seconds.value = +p[2] || 0
510
+ }
511
+
512
+ function incTime(key: 'h' | 'm' | 's') {
513
+ if (key === 'h') hours.value = (hours.value + 1) % 24
514
+ else if (key === 'm') minutes.value = (minutes.value + props.minuteStep) % 60
515
+ else seconds.value = (seconds.value + props.secondStep) % 60
516
+ }
517
+
518
+ function decTime(key: 'h' | 'm' | 's') {
519
+ if (key === 'h') hours.value = (hours.value + 23) % 24
520
+ else if (key === 'm') minutes.value = (minutes.value - props.minuteStep + 60) % 60
521
+ else seconds.value = (seconds.value - props.secondStep + 60) % 60
522
+ }
523
+
524
+ function startTimeEdit(key: 'h' | 'm' | 's') {
525
+ editingTime.value = key
526
+ nextTick(() => {
527
+ const idx = key === 'h' ? 0 : key === 'm' ? 1 : 2
528
+ const inp = timeInputRefs.value[idx]
529
+ if (inp) { inp.focus(); inp.select() }
530
+ })
531
+ }
532
+
533
+ function finishTimeEdit(key: 'h' | 'm' | 's', e: Event) {
534
+ editingTime.value = null
535
+ const val = parseInt((e.target as HTMLInputElement).value, 10)
536
+ if (isNaN(val)) return
537
+ if (key === 'h') hours.value = Math.max(0, Math.min(23, val))
538
+ else if (key === 'm') minutes.value = Math.max(0, Math.min(59, Math.round(val / props.minuteStep) * props.minuteStep))
539
+ else seconds.value = Math.max(0, Math.min(59, Math.round(val / props.secondStep) * props.secondStep))
540
+ }
541
+
542
+ function handleOutside(e: MouseEvent) {
543
+ if (rootRef.value && !rootRef.value.contains(e.target as Node)) {
544
+ open.value = false
545
+ editingTime.value = null
546
+ }
547
+ }
548
+
549
+ onMounted(() => {
550
+ document.addEventListener('mousedown', handleOutside)
551
+ window.addEventListener('scroll', updatePosition, true)
552
+ window.addEventListener('resize', updatePosition)
553
+ })
554
+ onUnmounted(() => {
555
+ document.removeEventListener('mousedown', handleOutside)
556
+ window.removeEventListener('scroll', updatePosition, true)
557
+ window.removeEventListener('resize', updatePosition)
558
+ })
559
+
560
+ watch(() => props.modelValue, () => {
561
+ if (open.value) initFromModel()
562
+ }, { deep: true })
563
+ </script>
package/index.ts CHANGED
@@ -14,5 +14,6 @@ export { default as UserPicker } from './components/UserPicker.vue'
14
14
  export { default as SearchSelect } from './components/SearchSelect.vue'
15
15
  export { default as SideMenu } from './components/SideMenu.vue'
16
16
  export { default as TopNavbar } from './components/TopNavbar.vue'
17
+ export { default as DateTimePicker } from './components/DateTimePicker.vue'
17
18
  export { useToast } from './composables/useToast'
18
- export type { Column, SelectOption, MenuItem, MenuGroup, NavModule, DropdownItem, ToastType } from './types'
19
+ export type { Column, SelectOption, MenuItem, MenuGroup, NavModule, DropdownItem, ToastType, DateTimePreset } from './types'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquiferre/ui-kit",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/types.ts CHANGED
@@ -45,3 +45,8 @@ export interface DropdownItem {
45
45
  }
46
46
 
47
47
  export type ToastType = 'success' | 'error' | 'warning' | 'info'
48
+
49
+ export interface DateTimePreset {
50
+ label: string
51
+ value: () => string | [string, string]
52
+ }