@saasmakers/ui 2.0.68 → 2.0.69
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/app/components/bases/BaseChip.vue +125 -0
- package/app/components/fields/FieldCombobox.vue +56 -211
- package/app/components/fields/FieldDate.vue +755 -0
- package/app/components/fields/FieldSelect.vue +2 -2
- package/app/composables/useChipNavigation.ts +178 -0
- package/app/composables/useRelativeDate.ts +97 -0
- package/app/i18n/relativeDate.ts +138 -0
- package/app/types/bases.d.ts +26 -0
- package/app/types/fields.d.ts +24 -0
- package/app/types/global.d.ts +6 -0
- package/app/utils/dateField.ts +104 -0
- package/app/utils/layerIcons.ts +1 -0
- package/package.json +1 -1
- package/uno.config.ts +2 -0
|
@@ -385,11 +385,11 @@ function selectOption(event: MouseEvent, value: string) {
|
|
|
385
385
|
:key="option.value"
|
|
386
386
|
class="flex cursor-pointer items-center outline-none"
|
|
387
387
|
:class="{
|
|
388
|
-
'base-border-b
|
|
388
|
+
'base-border-b base-bg-hover px-3 last:border-b-0': computedColumns.length < 2,
|
|
389
389
|
'h-10': size === 'sm' && computedColumns.length < 2,
|
|
390
390
|
'h-12': size === 'base' && computedColumns.length < 2,
|
|
391
391
|
'h-14': size === 'lg' && computedColumns.length < 2,
|
|
392
|
-
'px-2 py-1
|
|
392
|
+
'base-bg-hover px-2 py-1 last:mb-2': computedColumns.length >= 2,
|
|
393
393
|
'font-medium underline': option.value === selectedOption?.value,
|
|
394
394
|
'text-gray-900 dark:text-gray-100': option.value === selectedOption?.value,
|
|
395
395
|
'bg-white dark:bg-gray-900': selectedOption && option.value !== selectedOption.value,
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
export type ChipNavigationDirection = 'down' | 'left' | 'right' | 'up'
|
|
2
|
+
|
|
3
|
+
export default function useChipNavigation(options: {
|
|
4
|
+
containerRef: Ref<HTMLElement | undefined>
|
|
5
|
+
count: MaybeRefOrGetter<number>
|
|
6
|
+
role?: string
|
|
7
|
+
shouldIgnoreHorizontal?: (direction: 'left' | 'right') => boolean
|
|
8
|
+
}) {
|
|
9
|
+
const highlightedIndex = ref(0)
|
|
10
|
+
const chipRole = options.role ?? 'option'
|
|
11
|
+
|
|
12
|
+
const chipCount = computed(() => {
|
|
13
|
+
return toValue(options.count)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
watch(chipCount, () => {
|
|
17
|
+
const lastIndex = Math.max(0, chipCount.value - 1)
|
|
18
|
+
|
|
19
|
+
if (highlightedIndex.value > lastIndex) {
|
|
20
|
+
highlightedIndex.value = lastIndex
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
function getChipElements() {
|
|
25
|
+
if (!options.containerRef.value) {
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Array.from(options.containerRef.value.querySelectorAll<HTMLElement>(`[role="${chipRole}"]`))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getChipRects() {
|
|
33
|
+
return getChipElements().map((element, index) => ({
|
|
34
|
+
index,
|
|
35
|
+
rect: element.getBoundingClientRect(),
|
|
36
|
+
}))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function moveHighlight(direction: ChipNavigationDirection) {
|
|
40
|
+
const chipRects = getChipRects()
|
|
41
|
+
|
|
42
|
+
if (chipRects.length === 0) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const currentIndex = Math.min(highlightedIndex.value, chipRects.length - 1)
|
|
47
|
+
const current = chipRects[currentIndex]!
|
|
48
|
+
|
|
49
|
+
if (direction === 'left' || direction === 'right') {
|
|
50
|
+
const sameRowChips = chipRects.filter((chip) => {
|
|
51
|
+
return Math.abs(chip.rect.top - current.rect.top) <= Math.min(chip.rect.height, current.rect.height) / 2
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
if (direction === 'right') {
|
|
55
|
+
const rightChips = sameRowChips
|
|
56
|
+
.filter(chip => chip.rect.left > current.rect.left + 1)
|
|
57
|
+
.sort((first, second) => first.rect.left - second.rect.left)
|
|
58
|
+
|
|
59
|
+
if (rightChips.length > 0) {
|
|
60
|
+
highlightedIndex.value = rightChips[0]!.index
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const nextRowChips = chipRects
|
|
64
|
+
.filter(chip => chip.rect.top > current.rect.bottom - 1)
|
|
65
|
+
.sort((first, second) => first.rect.top - second.rect.top || first.rect.left - second.rect.left)
|
|
66
|
+
|
|
67
|
+
if (nextRowChips.length > 0) {
|
|
68
|
+
const nextRowTop = nextRowChips[0]!.rect.top
|
|
69
|
+
const firstInNextRow = nextRowChips.find(chip => Math.abs(chip.rect.top - nextRowTop) <= 4)
|
|
70
|
+
|
|
71
|
+
if (firstInNextRow) {
|
|
72
|
+
highlightedIndex.value = firstInNextRow.index
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const leftChips = sameRowChips
|
|
79
|
+
.filter(chip => chip.rect.right < current.rect.right - 1)
|
|
80
|
+
.sort((first, second) => second.rect.left - first.rect.left)
|
|
81
|
+
|
|
82
|
+
if (leftChips.length > 0) {
|
|
83
|
+
highlightedIndex.value = leftChips[0]!.index
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const previousRowChips = chipRects
|
|
87
|
+
.filter(chip => chip.rect.bottom < current.rect.top + 1)
|
|
88
|
+
.sort((first, second) => second.rect.top - first.rect.top || second.rect.left - first.rect.left)
|
|
89
|
+
|
|
90
|
+
if (previousRowChips.length > 0) {
|
|
91
|
+
const previousRowTop = previousRowChips[0]!.rect.top
|
|
92
|
+
|
|
93
|
+
const lastInPreviousRow = previousRowChips
|
|
94
|
+
.filter(chip => Math.abs(chip.rect.top - previousRowTop) <= 4)
|
|
95
|
+
.sort((first, second) => second.rect.left - first.rect.left)[0]
|
|
96
|
+
|
|
97
|
+
if (lastInPreviousRow) {
|
|
98
|
+
highlightedIndex.value = lastInPreviousRow.index
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const currentCenterX = current.rect.left + current.rect.width / 2
|
|
106
|
+
|
|
107
|
+
const rowChips = chipRects.filter((chip) => {
|
|
108
|
+
if (direction === 'down') {
|
|
109
|
+
return chip.rect.top > current.rect.top + 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return chip.rect.bottom < current.rect.bottom - 1
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
if (rowChips.length === 0) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const targetRowTop = direction === 'down'
|
|
120
|
+
? Math.min(...rowChips.map(chip => chip.rect.top))
|
|
121
|
+
: Math.max(...rowChips.map(chip => chip.rect.top))
|
|
122
|
+
|
|
123
|
+
const targetRowChips = rowChips.filter(chip => Math.abs(chip.rect.top - targetRowTop) <= 4)
|
|
124
|
+
|
|
125
|
+
let closestIndex: number | undefined
|
|
126
|
+
let closestDistance: number | undefined
|
|
127
|
+
|
|
128
|
+
for (const chip of targetRowChips) {
|
|
129
|
+
const centerX = chip.rect.left + chip.rect.width / 2
|
|
130
|
+
const distance = Math.abs(centerX - currentCenterX)
|
|
131
|
+
|
|
132
|
+
if (closestDistance === undefined || distance < closestDistance) {
|
|
133
|
+
closestDistance = distance
|
|
134
|
+
closestIndex = chip.index
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (closestIndex !== undefined) {
|
|
139
|
+
highlightedIndex.value = closestIndex
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
nextTick(() => {
|
|
144
|
+
getChipElements()[highlightedIndex.value]?.scrollIntoView({
|
|
145
|
+
block: 'nearest',
|
|
146
|
+
inline: 'nearest',
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function onArrowKey(event: KeyboardEvent, direction: ChipNavigationDirection) {
|
|
152
|
+
if (chipCount.value === 0) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if ((direction === 'left' || direction === 'right') && options.shouldIgnoreHorizontal?.(direction)) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
event.preventDefault()
|
|
161
|
+
moveHighlight(direction)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function onHighlight(index: number) {
|
|
165
|
+
highlightedIndex.value = index
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resetHighlight(index = 0) {
|
|
169
|
+
highlightedIndex.value = index
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
highlightedIndex,
|
|
174
|
+
onArrowKey,
|
|
175
|
+
onHighlight,
|
|
176
|
+
resetHighlight,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { getRelativeDateMessage } from '../i18n/relativeDate'
|
|
2
|
+
import { getDayDiff, resolveLocaleCode, startOfLocalDay } from '../utils/dateField'
|
|
3
|
+
|
|
4
|
+
export default function useRelativeDate(
|
|
5
|
+
date: MaybeRefOrGetter<Date | null | undefined>,
|
|
6
|
+
options?: {
|
|
7
|
+
today?: MaybeRefOrGetter<Date>
|
|
8
|
+
},
|
|
9
|
+
) {
|
|
10
|
+
const { locale } = useI18n()
|
|
11
|
+
const resolvedDate = computed(() => coerceDate(toValue(date)))
|
|
12
|
+
|
|
13
|
+
const resolvedToday = computed(() => {
|
|
14
|
+
const today = coerceDate(toValue(options?.today)) ?? new Date()
|
|
15
|
+
|
|
16
|
+
return startOfLocalDay(today)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const dayDiff = computed(() => {
|
|
20
|
+
const currentDate = resolvedDate.value
|
|
21
|
+
|
|
22
|
+
if (!currentDate) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return getDayDiff(resolvedToday.value, currentDate)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const isOverdue = computed(() => {
|
|
30
|
+
const diff = dayDiff.value
|
|
31
|
+
|
|
32
|
+
return diff !== null && diff < 0
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const relativeLabel = computed(() => {
|
|
36
|
+
const diff = dayDiff.value
|
|
37
|
+
const currentDate = resolvedDate.value
|
|
38
|
+
|
|
39
|
+
if (diff === null || !currentDate) {
|
|
40
|
+
return ''
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (diff === 0) {
|
|
44
|
+
return getRelativeDateMessage(locale.value, 'today')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (diff === 1) {
|
|
48
|
+
return getRelativeDateMessage(locale.value, 'tomorrow')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (diff === -1) {
|
|
52
|
+
return getRelativeDateMessage(locale.value, 'yesterday')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (diff < -1) {
|
|
56
|
+
return getRelativeDateMessage(locale.value, 'overdue', Math.abs(diff))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (diff >= 2 && diff <= 6) {
|
|
60
|
+
return new Intl.DateTimeFormat(resolveLocaleCode(locale.value), { weekday: 'long' }).format(currentDate)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return getRelativeDateMessage(locale.value, 'inDays', diff)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const absoluteLabel = computed(() => {
|
|
67
|
+
const currentDate = resolvedDate.value
|
|
68
|
+
|
|
69
|
+
if (!currentDate) {
|
|
70
|
+
return ''
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return currentDate.toLocaleDateString(resolveLocaleCode(locale.value), {
|
|
74
|
+
day: 'numeric',
|
|
75
|
+
month: 'long',
|
|
76
|
+
year: 'numeric',
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
absoluteLabel,
|
|
82
|
+
isOverdue,
|
|
83
|
+
relativeLabel,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function coerceDate(value: unknown): Date | null {
|
|
88
|
+
if (!value) {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (value instanceof Date) {
|
|
93
|
+
return Number.isNaN(value.getTime()) ? null : value
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { Locale } from '@saasmakers/shared'
|
|
2
|
+
import { resolveLocaleCode } from '../utils/dateField'
|
|
3
|
+
|
|
4
|
+
export type RelativeDateKey = 'inDays' | 'overdue' | 'today' | 'tomorrow' | 'yesterday'
|
|
5
|
+
|
|
6
|
+
export interface RelativeDateMessages {
|
|
7
|
+
inDays: string
|
|
8
|
+
overdue: string
|
|
9
|
+
today: string
|
|
10
|
+
tomorrow: string
|
|
11
|
+
yesterday: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const relativeDateMessages: Record<Locale, RelativeDateMessages> = {
|
|
15
|
+
de: {
|
|
16
|
+
inDays: 'In {count} Tag | In {count} Tagen',
|
|
17
|
+
overdue: 'Überfällig | {count} Tag überfällig | {count} Tage überfällig',
|
|
18
|
+
today: 'Heute',
|
|
19
|
+
tomorrow: 'Morgen',
|
|
20
|
+
yesterday: 'Gestern',
|
|
21
|
+
},
|
|
22
|
+
en: {
|
|
23
|
+
inDays: 'In {count} day | In {count} days',
|
|
24
|
+
overdue: 'Overdue | {count} day overdue | {count} days overdue',
|
|
25
|
+
today: 'Today',
|
|
26
|
+
tomorrow: 'Tomorrow',
|
|
27
|
+
yesterday: 'Yesterday',
|
|
28
|
+
},
|
|
29
|
+
es: {
|
|
30
|
+
inDays: 'En {count} día | En {count} días',
|
|
31
|
+
overdue: 'Atrasado | {count} día de retraso | {count} días de retraso',
|
|
32
|
+
today: 'Hoy',
|
|
33
|
+
tomorrow: 'Mañana',
|
|
34
|
+
yesterday: 'Ayer',
|
|
35
|
+
},
|
|
36
|
+
fr: {
|
|
37
|
+
inDays: 'Dans {count} j | Dans {count} j',
|
|
38
|
+
overdue: 'En retard | En retard de {count} j | En retard de {count} j',
|
|
39
|
+
today: 'Aujourd\'hui',
|
|
40
|
+
tomorrow: 'Demain',
|
|
41
|
+
yesterday: 'Hier',
|
|
42
|
+
},
|
|
43
|
+
id: {
|
|
44
|
+
inDays: 'Dalam {count} hari | Dalam {count} hari',
|
|
45
|
+
overdue: 'Terlambat | Terlambat {count} hari | Terlambat {count} hari',
|
|
46
|
+
today: 'Hari ini',
|
|
47
|
+
tomorrow: 'Besok',
|
|
48
|
+
yesterday: 'Kemarin',
|
|
49
|
+
},
|
|
50
|
+
it: {
|
|
51
|
+
inDays: 'Tra {count} giorno | Tra {count} giorni',
|
|
52
|
+
overdue: 'In ritardo | In ritardo di {count} g | In ritardo di {count} g',
|
|
53
|
+
today: 'Oggi',
|
|
54
|
+
tomorrow: 'Domani',
|
|
55
|
+
yesterday: 'Ieri',
|
|
56
|
+
},
|
|
57
|
+
ja: {
|
|
58
|
+
inDays: 'あと{count}日 | あと{count}日',
|
|
59
|
+
overdue: '期限切れ | {count}日遅れ | {count}日遅れ',
|
|
60
|
+
today: '今日',
|
|
61
|
+
tomorrow: '明日',
|
|
62
|
+
yesterday: '昨日',
|
|
63
|
+
},
|
|
64
|
+
ko: {
|
|
65
|
+
inDays: '{count}일 후 | {count}일 후',
|
|
66
|
+
overdue: '기한 지남 | {count}일 지남 | {count}일 지남',
|
|
67
|
+
today: '오늘',
|
|
68
|
+
tomorrow: '내일',
|
|
69
|
+
yesterday: '어제',
|
|
70
|
+
},
|
|
71
|
+
nl: {
|
|
72
|
+
inDays: 'Over {count} dag | Over {count} dagen',
|
|
73
|
+
overdue: 'Te laat | {count} dag te laat | {count} dagen te laat',
|
|
74
|
+
today: 'Vandaag',
|
|
75
|
+
tomorrow: 'Morgen',
|
|
76
|
+
yesterday: 'Gisteren',
|
|
77
|
+
},
|
|
78
|
+
pl: {
|
|
79
|
+
inDays: 'Za {count} dzień | Za {count} dni',
|
|
80
|
+
overdue: 'Po terminie | {count} dzień po terminie | {count} dni po terminie',
|
|
81
|
+
today: 'Dziś',
|
|
82
|
+
tomorrow: 'Jutro',
|
|
83
|
+
yesterday: 'Wczoraj',
|
|
84
|
+
},
|
|
85
|
+
pt: {
|
|
86
|
+
inDays: 'Daqui a {count} dia | Daqui a {count} dias',
|
|
87
|
+
overdue: 'Em atraso | {count} dia de atraso | {count} dias de atraso',
|
|
88
|
+
today: 'Hoje',
|
|
89
|
+
tomorrow: 'Amanhã',
|
|
90
|
+
yesterday: 'Ontem',
|
|
91
|
+
},
|
|
92
|
+
'pt-BR': {
|
|
93
|
+
inDays: 'Em {count} dia | Em {count} dias',
|
|
94
|
+
overdue: 'Atrasado | {count} dia de atraso | {count} dias de atraso',
|
|
95
|
+
today: 'Hoje',
|
|
96
|
+
tomorrow: 'Amanhã',
|
|
97
|
+
yesterday: 'Ontem',
|
|
98
|
+
},
|
|
99
|
+
vi: {
|
|
100
|
+
inDays: 'Trong {count} ngày | Trong {count} ngày',
|
|
101
|
+
overdue: 'Quá hạn | Quá hạn {count} ngày | Quá hạn {count} ngày',
|
|
102
|
+
today: 'Hôm nay',
|
|
103
|
+
tomorrow: 'Ngày mai',
|
|
104
|
+
yesterday: 'Hôm qua',
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveRelativeDateLocale(localeValue: unknown): Locale {
|
|
109
|
+
const localeCode = resolveLocaleCode(localeValue)
|
|
110
|
+
|
|
111
|
+
if (localeCode in relativeDateMessages) {
|
|
112
|
+
return localeCode as Locale
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const languageCode = localeCode.split('-')[0]
|
|
116
|
+
|
|
117
|
+
if (languageCode && languageCode in relativeDateMessages) {
|
|
118
|
+
return languageCode as Locale
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return 'en'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getRelativeDateMessage(localeValue: unknown, key: RelativeDateKey, count?: number) {
|
|
125
|
+
const messages = relativeDateMessages[resolveRelativeDateLocale(localeValue)]
|
|
126
|
+
const rawMessage = messages[key]
|
|
127
|
+
|
|
128
|
+
if (key === 'today' || key === 'tomorrow' || key === 'yesterday' || count === undefined) {
|
|
129
|
+
return rawMessage
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const forms = rawMessage.split('|').map(form => form.trim())
|
|
133
|
+
const template = forms.length === 1
|
|
134
|
+
? forms[0]
|
|
135
|
+
: count === 1 ? forms[1] ?? forms[0] : forms[2] ?? forms[1] ?? forms[0]
|
|
136
|
+
|
|
137
|
+
return template?.replace(/\{count\}/g, String(count)) ?? rawMessage
|
|
138
|
+
}
|
package/app/types/bases.d.ts
CHANGED
|
@@ -109,6 +109,32 @@ export type BaseCharacterCharacter
|
|
|
109
109
|
|
|
110
110
|
export type BaseCharacterSize = 'base' | 'lg' | 'sm' | 'xs'
|
|
111
111
|
|
|
112
|
+
export interface BaseChip {
|
|
113
|
+
active?: boolean
|
|
114
|
+
activeHighlight?: BaseChipActiveHighlight
|
|
115
|
+
ariaLabel?: string
|
|
116
|
+
ariaSelected?: boolean
|
|
117
|
+
as?: BaseChipAs
|
|
118
|
+
disabled?: boolean
|
|
119
|
+
hoverable?: boolean
|
|
120
|
+
icon?: string
|
|
121
|
+
iconLeading?: boolean
|
|
122
|
+
iconSize?: BaseSize
|
|
123
|
+
id?: string
|
|
124
|
+
maxWidth?: boolean
|
|
125
|
+
normalCase?: boolean
|
|
126
|
+
removable?: boolean
|
|
127
|
+
role?: string
|
|
128
|
+
text?: BaseTextText
|
|
129
|
+
variant?: BaseChipVariant
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type BaseChipActiveHighlight = 'gray' | 'indigo'
|
|
133
|
+
|
|
134
|
+
export type BaseChipAs = 'button' | 'div'
|
|
135
|
+
|
|
136
|
+
export type BaseChipVariant = 'dashed' | 'filled' | 'outline'
|
|
137
|
+
|
|
112
138
|
export interface BaseChart {
|
|
113
139
|
data?: ChartistBarChartData | ChartistLineChartData | ChartistPieChartData
|
|
114
140
|
fullWidth?: boolean
|
package/app/types/fields.d.ts
CHANGED
|
@@ -56,6 +56,30 @@ export interface FieldCombobox<Item = Record<string, unknown>> {
|
|
|
56
56
|
validation?: VuelidateValidation
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
export interface FieldDate {
|
|
60
|
+
background?: FieldBackground
|
|
61
|
+
description?: BaseTextText
|
|
62
|
+
disabled?: boolean
|
|
63
|
+
hideError?: boolean
|
|
64
|
+
label?: BaseTextText
|
|
65
|
+
labelIcon?: string
|
|
66
|
+
min?: Date
|
|
67
|
+
modelValue: Date | null
|
|
68
|
+
placeholder?: string
|
|
69
|
+
presets?: FieldDatePreset[]
|
|
70
|
+
required?: boolean
|
|
71
|
+
size?: FieldSize
|
|
72
|
+
validation?: VuelidateValidation
|
|
73
|
+
weekStartsOn?: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface FieldDatePreset {
|
|
77
|
+
date: Date
|
|
78
|
+
id: string
|
|
79
|
+
label: string
|
|
80
|
+
sub?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
export interface FieldDays {
|
|
60
84
|
disabled?: boolean
|
|
61
85
|
modelValue: number[]
|
package/app/types/global.d.ts
CHANGED
|
@@ -33,6 +33,10 @@ declare global {
|
|
|
33
33
|
type BaseCharacter = import('./bases').BaseCharacter
|
|
34
34
|
type BaseCharacterCharacter = import('./bases').BaseCharacterCharacter
|
|
35
35
|
type BaseCharacterSize = import('./bases').BaseCharacterSize
|
|
36
|
+
type BaseChip = import('./bases').BaseChip
|
|
37
|
+
type BaseChipActiveHighlight = import('./bases').BaseChipActiveHighlight
|
|
38
|
+
type BaseChipAs = import('./bases').BaseChipAs
|
|
39
|
+
type BaseChipVariant = import('./bases').BaseChipVariant
|
|
36
40
|
type BaseChart = import('./bases').BaseChart
|
|
37
41
|
type BaseChartType = import('./bases').BaseChartType
|
|
38
42
|
type BaseCircle = import('./bases').BaseCircle
|
|
@@ -96,6 +100,8 @@ declare global {
|
|
|
96
100
|
type FieldBackground = import('./fields').FieldBackground
|
|
97
101
|
type FieldCheckbox = import('./fields').FieldCheckbox
|
|
98
102
|
type FieldCombobox<Item = Record<string, unknown>> = import('./fields').FieldCombobox<Item>
|
|
103
|
+
type FieldDate = import('./fields').FieldDate
|
|
104
|
+
type FieldDatePreset = import('./fields').FieldDatePreset
|
|
99
105
|
type FieldDays = import('./fields').FieldDays
|
|
100
106
|
type FieldEmojis = import('./fields').FieldEmojis
|
|
101
107
|
type FieldInput = import('./fields').FieldInput
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export interface CalendarCell {
|
|
2
|
+
date: Date
|
|
3
|
+
inMonth: boolean
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function buildCalendarGrid(params: {
|
|
7
|
+
viewMonth: Date
|
|
8
|
+
weekStartsOn?: number
|
|
9
|
+
}): CalendarCell[] {
|
|
10
|
+
const weekStartsOn = params.weekStartsOn ?? 1
|
|
11
|
+
const year = params.viewMonth.getFullYear()
|
|
12
|
+
const month = params.viewMonth.getMonth()
|
|
13
|
+
const firstOfMonth = new Date(year, month, 1)
|
|
14
|
+
const firstWeekday = firstOfMonth.getDay()
|
|
15
|
+
const offset = (firstWeekday - weekStartsOn + 7) % 7
|
|
16
|
+
const gridStart = new Date(year, month, 1 - offset)
|
|
17
|
+
|
|
18
|
+
return Array.from({ length: 42 }, (_, index) => {
|
|
19
|
+
const date = new Date(gridStart)
|
|
20
|
+
|
|
21
|
+
date.setDate(gridStart.getDate() + index)
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
date,
|
|
25
|
+
inMonth: date.getMonth() === month,
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatMonthLabel(date: Date, locale: string) {
|
|
31
|
+
const formatted = new Intl.DateTimeFormat(locale, {
|
|
32
|
+
month: 'long',
|
|
33
|
+
year: 'numeric',
|
|
34
|
+
}).format(date)
|
|
35
|
+
|
|
36
|
+
return formatted.charAt(0).toUpperCase() + formatted.slice(1)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatPresetSub(date: Date, locale: string) {
|
|
40
|
+
return new Intl.DateTimeFormat(locale, {
|
|
41
|
+
day: 'numeric',
|
|
42
|
+
weekday: 'short',
|
|
43
|
+
}).format(date)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getDayDiff(from: Date, to: Date) {
|
|
47
|
+
const millisecondsPerDay = 86_400_000
|
|
48
|
+
|
|
49
|
+
return Math.round((startOfLocalDay(to).getTime() - startOfLocalDay(from).getTime()) / millisecondsPerDay)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getNextMonday(from: Date) {
|
|
53
|
+
const weekday = from.getDay()
|
|
54
|
+
const daysUntilNextMonday = ((1 - weekday + 7) % 7) || 7
|
|
55
|
+
const nextMonday = new Date(from)
|
|
56
|
+
|
|
57
|
+
nextMonday.setDate(from.getDate() + daysUntilNextMonday)
|
|
58
|
+
|
|
59
|
+
return startOfLocalDay(nextMonday)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getNextSaturday(from: Date) {
|
|
63
|
+
const weekday = from.getDay()
|
|
64
|
+
const daysUntilSaturday = weekday === 0 ? 6 : (6 - weekday + 7) % 7 || 7
|
|
65
|
+
const nextSaturday = new Date(from)
|
|
66
|
+
|
|
67
|
+
nextSaturday.setDate(from.getDate() + daysUntilSaturday)
|
|
68
|
+
|
|
69
|
+
return startOfLocalDay(nextSaturday)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isBeforeDay(date: Date, min?: Date) {
|
|
73
|
+
if (!min) {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return startOfLocalDay(date).getTime() < startOfLocalDay(min).getTime()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isSameDay(first: Date, second: Date) {
|
|
81
|
+
return startOfLocalDay(first).getTime() === startOfLocalDay(second).getTime()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function resolveLocaleCode(localeValue: unknown): string {
|
|
85
|
+
if (typeof localeValue === 'string') {
|
|
86
|
+
return localeValue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (localeValue && typeof localeValue === 'object') {
|
|
90
|
+
if ('code' in localeValue && typeof localeValue.code === 'string') {
|
|
91
|
+
return localeValue.code
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ('language' in localeValue && typeof localeValue.language === 'string') {
|
|
95
|
+
return localeValue.language.split('-')[0] ?? 'en'
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return 'en'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function startOfLocalDay(date: Date) {
|
|
103
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
|
104
|
+
}
|
package/app/utils/layerIcons.ts
CHANGED
|
@@ -13,6 +13,7 @@ export const layerIcons = {
|
|
|
13
13
|
arrowRight: 'solar:alt-arrow-right-bold',
|
|
14
14
|
arrowUp: 'solar:alt-arrow-up-bold',
|
|
15
15
|
back: 'hugeicons:arrow-turn-backward',
|
|
16
|
+
calendar: 'hugeicons:calendar-03',
|
|
16
17
|
check: 'mdi:check',
|
|
17
18
|
checkCircle: 'hugeicons:checkmark-circle-02',
|
|
18
19
|
chevronLeft: 'hugeicons:arrow-left-01',
|
package/package.json
CHANGED
package/uno.config.ts
CHANGED
|
@@ -37,6 +37,8 @@ export const shortcuts: Record<string, string> = {
|
|
|
37
37
|
'base-bg': 'bg-white dark:bg-gray-900',
|
|
38
38
|
'base-box': 'border shadow-sm base-border rounded-lg',
|
|
39
39
|
'base-highlight-indigo': 'border border-indigo-600/35 bg-indigo-600/[0.08] shadow-indigo-600/15 dark:border-indigo-400/35 dark:bg-indigo-400/[0.08] dark:shadow-indigo-400/15',
|
|
40
|
+
'base-bg-hover': 'hover:bg-gray-50 dark:hover:bg-white/5',
|
|
41
|
+
'base-control-hover': 'base-bg-hover hover:text-gray-900 dark:hover:text-gray-100',
|
|
40
42
|
'base-surface': 'border base-border base-bg shadow-sm',
|
|
41
43
|
'field-disabled': 'opacity-50 cursor-not-allowed',
|
|
42
44
|
'field-uppercase': 'uppercase [&_*]:uppercase [&_input::placeholder]:uppercase',
|