This commit is contained in:
Nadar
2026-03-17 13:24:22 +03:00
commit 82e5ac9d81
554 changed files with 29637 additions and 0 deletions

View File

@@ -0,0 +1,216 @@
<template>
<div :class="cn.b()">
<div :class="cn.e('header')">
<UiButton
icon="chevron-left"
size="small"
type="ghost"
color="secondary"
@click="previousMonth"
/>
<p :class="cn.e('title')">
{{ currentMonth }}
</p>
<UiButton
icon="chevron-right"
size="small"
type="ghost"
color="secondary"
@click="nextMonth"
/>
</div>
<div :class="cn.e('week-days')">
<div
v-for="dayOfWeek in daysOfWeek"
:key="dayOfWeek"
>
{{ dayOfWeek }}
</div>
</div>
<div :class="cn.e('days')">
<div
v-for="day in monthDays"
:key="day.date"
:class="getDayClasses(day)"
@click="selectDay(day)"
>
{{ day.day }}
</div>
</div>
<UiButton
type="outlined"
color="secondary"
:class="cn.e('apply-button')"
@click="apply"
>
{{ $t('apply') }}
</UiButton>
</div>
</template>
<script setup>
// TODO: TypeScript
import { computed, reactive, ref, toRef, watch } from 'vue'
import dayjs from 'dayjs'
import UiButton from '../button/index.vue'
import useClassname from '../../composables/use-classname'
const props = defineProps({
allowRangeSelection: {
type: Boolean,
default: true,
},
modelValue: {
type: Array,
default: () => [],
},
})
const emit = defineEmits(['update:modelValue'])
const cn = useClassname('ui-calendar')
const modelValue = toRef(props, 'modelValue')
const today = dayjs()
const dateRange = reactive({
start: null,
end: null,
})
const currentDate = ref(today)
const daysOfWeek = dayjs.weekdaysMin()
const currentMonth = computed(() => currentDate.value.format('MMMM, YYYY'))
const monthDays = computed(() => {
const daysInMonth = currentDate.value.daysInMonth()
const leadingDays = currentDate.value.startOf('month').day()
const days = []
for (let i = 0; i < leadingDays; i++) {
const date = currentDate.value
.subtract(1, 'month')
.endOf('month')
.subtract(leadingDays - i - 1, 'day')
days.push(date)
}
for (let i = 0; i < daysInMonth; i++) {
const date = currentDate.value.date(i + 1)
days.push(date)
}
const num = 7 - (days.length % 7 || 7)
for (let i = 0; i < num; i++) {
const date = currentDate.value.add(1, 'month').date(i + 1)
days.push(date)
}
return days.map(formatDateObject)
})
function formatDateObject(date) {
return {
raw: date,
day: date.date(),
month: date.get('month'),
isToday: date.isToday(),
isCurrentMonth: date.isSame(currentDate.value, 'month'),
isPeriod:
!!dateRange.start
&& !!dateRange.end
&& date.isBetween(dateRange.start, dateRange.end, 'day', '[]'),
isPeriodStart: !!dateRange.start && date.isSame(dateRange.start, 'day'),
isPeriodEnd: !!dateRange.end && date.isSame(dateRange.end, 'day'),
}
}
function nextMonth() {
currentDate.value = currentDate.value.add(1, 'month')
}
function previousMonth() {
currentDate.value = currentDate.value.subtract(1, 'month')
}
function selectDay(day) {
let { start, end } = dateRange
if (props.allowRangeSelection) {
const isSameStartDay = day.raw.isSame(start, 'day')
if (isSameStartDay) {
start = null
end = null
}
else if (!start || end) {
start = day.raw
end = null
}
else {
end = day.raw
if (dayjs(start).isAfter(dayjs(end), 'day'))
[start, end] = [end, start]
}
}
else {
start = day.raw
end = null
}
dateRange.start = start
dateRange.end = end
}
watch(
modelValue,
(value) => {
const [start, end] = value
dateRange.start = !!start && dayjs(start)
dateRange.end = !!end && dayjs(end)
},
{ immediate: true },
)
function getDayClasses(day) {
const classes = [cn.e('cell')]
if (day.isCurrentMonth)
classes.push(cn.em('cell', 'current-month'))
if (day.isPeriod)
classes.push(cn.em('cell', 'period'))
if (day.isPeriodStart)
classes.push(cn.em('cell', 'period-start'))
else if (day.isPeriodEnd)
classes.push(cn.em('cell', 'period-end'))
if (day.isToday)
classes.push(cn.em('cell', 'current-day'))
return classes
}
function apply() {
const value = []
if (dateRange.start)
value.push(dateRange.start.startOf('day').valueOf())
if (dateRange.end)
value.push(dateRange.end.startOf('day').valueOf())
emit('update:modelValue', value)
}
</script>
<style lang="scss">
@use 'styles';
</style>

View File

@@ -0,0 +1,140 @@
@use '../../styles/variables' as *;
@use '../../styles/mixins' as *;
.ui-calendar {
display: inline-block;
width: fit-content;
background-color: var(--calendar-background);
color: var(--calendar-color);
padding: 16px 16px 24px 16px;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
i {
&:hover {
cursor: pointer;
}
}
}
&__title {
@include txt-i-sb;
text-transform: capitalize;
}
&__week-days,
&__days {
display: grid;
grid-template-columns: repeat(7, 32px);
color: var(--calendar-day-color);
div {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
}
&__week-days {
@include txt-s;
grid-auto-rows: 24px;
text-transform: capitalize;
}
&__days {
@include txt-i-m;
grid-auto-rows: 32px;
}
&__apply-button {
width: 100%;
margin-top: 16px;
}
&__cell {
transition: 0.2s ease-out;
transition-property: background-color;
&:not(&--period-start, &--period-end, &--period):hover {
background: var(--calendar-day-background-hover-color);
border-radius: 8px;
}
&--current-month {
color: var(--calendar-current-month-day-color);
}
&--current-day {
color: var(--calendar-today-color);
}
&--period {
background-color: var(--calendar-period-background);
}
&--period-start,
&--period-end {
border-radius: 8px;
position: relative;
color: var(--calendar-period-start-end-color);
z-index: 2;
&::before,
&::after {
content: '';
position: absolute;
top: 0;
height: 100%;
z-index: -1;
}
&::before {
width: 50%;
background-color: var(--calendar-period-background);
}
&::after {
width: 100%;
background-color: var(--calendar-period-start-end-background);
border-radius: inherit;
left: 0;
}
}
&--period-start {
&::before {
right: 0;
}
}
&--period-end {
&::before {
left: 0;
}
}
}
/* prettier-ignore */
@include element-variant(
'calendar',
'',
(
'color': $clr-black,
'background': $clr-white,
'day-color': $clr-grey-500,
'current-month-day-color': $clr-black,
'today-color': $clr-cyan-500,
'arrow-color': $clr-grey-600,
'period-start-end-color': $clr-white,
'period-start-end-background': $clr-cyan-500,
'period-background': $clr-grey-100,
'day-background-hover-color': $clr-grey-100
)
);
}