webcake-ui-kit 1.1.29 → 1.1.30
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 +7 -2
- package/src/components/date-picker/CalendarPanel.vue +154 -0
- package/src/components/date-picker/CalendarView.vue +104 -0
- package/src/components/date-picker/DatePicker.vue +587 -0
- package/src/components/date-picker/PanelFooter.vue +45 -0
- package/src/components/date-picker/PanelHeader.vue +102 -0
- package/src/components/date-picker/TimePanel.vue +98 -0
- package/src/components/date-picker/calendar-panel.css +36 -0
- package/src/components/date-picker/calendar-view.css +134 -0
- package/src/components/date-picker/calendar.js +116 -0
- package/src/components/date-picker/date-picker.css +95 -0
- package/src/components/date-picker/dayjs.js +34 -0
- package/src/components/date-picker/panel-footer.css +42 -0
- package/src/components/date-picker/panel-header.css +71 -0
- package/src/components/date-picker/time-panel.css +63 -0
- package/src/components/input/input.css +5 -0
- package/src/components/menu/menu.css +0 -1
- package/src/components/message/Message.vue +72 -0
- package/src/components/message/MessageContainer.vue +200 -0
- package/src/components/message/message-container.css +108 -0
- package/src/components/message/message.css +69 -0
- package/src/components/message/message.js +133 -0
- package/src/components/progress/Progress.vue +42 -0
- package/src/components/progress/progress.css +15 -0
- package/src/components/radio/Radio.vue +1 -1
- package/src/components/radio-group/RadioGroup.vue +2 -2
- package/src/components/table/Table.vue +30 -21
- package/src/components/table/table.css +18 -2
- package/src/index.js +5 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="ui-cal-header">
|
|
3
|
+
<div class="ui-cal-header__nav ui-cal-header__nav--prev">
|
|
4
|
+
<WkButton v-if="showYearJump" variant="ghost" size="xs" aria-label="Previous year" @click="$emit('prev-year')">
|
|
5
|
+
<template #icon><WkiChevronsLeft :size="16" /></template>
|
|
6
|
+
</WkButton>
|
|
7
|
+
<WkButton variant="ghost" size="xs" :disabled="!canPrev" aria-label="Previous month" @click="$emit('prev')">
|
|
8
|
+
<template #icon><WkiChevronLeft :size="16" /></template>
|
|
9
|
+
</WkButton>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="ui-cal-header__labels">
|
|
13
|
+
<div v-for="(panel, i) in panels" :key="i" class="ui-cal-header__group">
|
|
14
|
+
<!-- Month: WkSelect when opted-in, otherwise the clickable text label. -->
|
|
15
|
+
<span v-if="showSelectMonth" class="ui-cal-header__select ui-cal-header__select--month">
|
|
16
|
+
<WkSelect
|
|
17
|
+
size="xs"
|
|
18
|
+
:value="String(panel.month)"
|
|
19
|
+
:options="monthOptions"
|
|
20
|
+
@change="m => $emit('set-month', { index: i, month: Number(m) })"
|
|
21
|
+
/>
|
|
22
|
+
</span>
|
|
23
|
+
<button
|
|
24
|
+
v-else
|
|
25
|
+
type="button"
|
|
26
|
+
:class="[
|
|
27
|
+
'ui-cal-header__label',
|
|
28
|
+
showSelectYear && !showSelectMonth && 'ui-cal-header__label_month_not_select'
|
|
29
|
+
]"
|
|
30
|
+
@click="$emit('label-click', { index: i, part: 'month' })"
|
|
31
|
+
>
|
|
32
|
+
{{ monthName(panel.month) }}
|
|
33
|
+
</button>
|
|
34
|
+
|
|
35
|
+
<!-- Year: WkSelect when opted-in, otherwise the clickable text label. -->
|
|
36
|
+
<span v-if="showSelectYear" class="ui-cal-header__select ui-cal-header__select--year">
|
|
37
|
+
<WkSelect
|
|
38
|
+
size="xs"
|
|
39
|
+
:value="String(panel.year)"
|
|
40
|
+
:options="yearOptions"
|
|
41
|
+
@change="y => $emit('set-year', { index: i, year: Number(y) })"
|
|
42
|
+
/>
|
|
43
|
+
</span>
|
|
44
|
+
<button
|
|
45
|
+
v-else
|
|
46
|
+
type="button"
|
|
47
|
+
:class="[
|
|
48
|
+
'ui-cal-header__label',
|
|
49
|
+
!showSelectYear && showSelectMonth && 'ui-cal-header__label_year_not_select'
|
|
50
|
+
]"
|
|
51
|
+
@click="$emit('label-click', { index: i, part: 'year' })"
|
|
52
|
+
>
|
|
53
|
+
{{ panel.year }}
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="ui-cal-header__nav ui-cal-header__nav--next">
|
|
59
|
+
<WkButton variant="ghost" size="xs" :disabled="!canNext" aria-label="Next month" @click="$emit('next')">
|
|
60
|
+
<template #icon><WkiChevronRight :size="16" /></template>
|
|
61
|
+
</WkButton>
|
|
62
|
+
<WkButton v-if="showYearJump" variant="ghost" size="xs" aria-label="Next year" @click="$emit('next-year')">
|
|
63
|
+
<template #icon><WkiChevronsRight :size="16" /></template>
|
|
64
|
+
</WkButton>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<script>
|
|
70
|
+
import WkButton from '../button/Button.vue'
|
|
71
|
+
import WkSelect from '../select/Select.vue'
|
|
72
|
+
import { WkiChevronLeft, WkiChevronRight, WkiChevronsLeft, WkiChevronsRight } from '../../icons'
|
|
73
|
+
|
|
74
|
+
export default {
|
|
75
|
+
name: 'PanelHeader',
|
|
76
|
+
components: { WkButton, WkSelect, WkiChevronLeft, WkiChevronRight, WkiChevronsLeft, WkiChevronsRight },
|
|
77
|
+
props: {
|
|
78
|
+
// One entry per visible month: { month: 0-11, year: Number }.
|
|
79
|
+
panels: { type: Array, default: () => [] },
|
|
80
|
+
// Localized month names as select options: [{ label, value: '0'..'11' }].
|
|
81
|
+
monthOptions: { type: Array, default: () => [] },
|
|
82
|
+
// Selectable years: [{ label, value: 'YYYY' }].
|
|
83
|
+
yearOptions: { type: Array, default: () => [] },
|
|
84
|
+
// Swap the month / year label for a WkSelect dropdown.
|
|
85
|
+
showSelectMonth: { type: Boolean, default: false },
|
|
86
|
+
showSelectYear: { type: Boolean, default: false },
|
|
87
|
+
canPrev: { type: Boolean, default: true },
|
|
88
|
+
canNext: { type: Boolean, default: true },
|
|
89
|
+
// Double-chevron year jump (used by month/year drill views in Phase 2).
|
|
90
|
+
showYearJump: { type: Boolean, default: false }
|
|
91
|
+
},
|
|
92
|
+
emits: ['prev', 'next', 'prev-year', 'next-year', 'set-month', 'set-year', 'label-click'],
|
|
93
|
+
methods: {
|
|
94
|
+
monthName(month) {
|
|
95
|
+
const opt = this.monthOptions[month]
|
|
96
|
+
return opt ? opt.label : month + 1
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<style src="./panel-header.css" scoped></style>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="ui-time-panel">
|
|
3
|
+
<div v-for="col in columns" :key="col.key" class="ui-time-panel__col" role="listbox" :aria-label="col.key">
|
|
4
|
+
<button
|
|
5
|
+
v-for="opt in col.options"
|
|
6
|
+
:key="opt"
|
|
7
|
+
type="button"
|
|
8
|
+
class="ui-time-panel__opt"
|
|
9
|
+
:class="{ 'is-active': opt === col.value, 'is-disabled': col.disabled.indexOf(opt) !== -1 }"
|
|
10
|
+
:disabled="col.disabled.indexOf(opt) !== -1"
|
|
11
|
+
:aria-selected="opt === col.value ? 'true' : 'false'"
|
|
12
|
+
@click="onPick(col.key, opt)"
|
|
13
|
+
>
|
|
14
|
+
{{ pad(opt) }}
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script>
|
|
21
|
+
export default {
|
|
22
|
+
name: 'TimePanel',
|
|
23
|
+
props: {
|
|
24
|
+
// { hour, minute, second } — 24-hour numeric parts.
|
|
25
|
+
value: {
|
|
26
|
+
type: Object,
|
|
27
|
+
default: () => ({ hour: 0, minute: 0, second: 0 })
|
|
28
|
+
},
|
|
29
|
+
showSeconds: { type: Boolean, default: false },
|
|
30
|
+
hourStep: { type: Number, default: 1 },
|
|
31
|
+
minuteStep: { type: Number, default: 1 },
|
|
32
|
+
secondStep: { type: Number, default: 1 },
|
|
33
|
+
// () => { disabledHours?, disabledMinutes?(h), disabledSeconds?(h,m) }
|
|
34
|
+
disabledTime: { type: Function, default: null }
|
|
35
|
+
},
|
|
36
|
+
emits: ['change'],
|
|
37
|
+
computed: {
|
|
38
|
+
rules() {
|
|
39
|
+
return (typeof this.disabledTime === 'function' && this.disabledTime()) || {}
|
|
40
|
+
},
|
|
41
|
+
columns() {
|
|
42
|
+
const v = this.value || {}
|
|
43
|
+
const h = v.hour || 0
|
|
44
|
+
const m = v.minute || 0
|
|
45
|
+
const cols = [
|
|
46
|
+
{
|
|
47
|
+
key: 'hour',
|
|
48
|
+
value: h,
|
|
49
|
+
options: this.range(24, this.hourStep),
|
|
50
|
+
disabled: this.callRule(this.rules.disabledHours)
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: 'minute',
|
|
54
|
+
value: m,
|
|
55
|
+
options: this.range(60, this.minuteStep),
|
|
56
|
+
disabled: this.callRule(this.rules.disabledMinutes, h)
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
if (this.showSeconds) {
|
|
60
|
+
cols.push({
|
|
61
|
+
key: 'second',
|
|
62
|
+
value: v.second || 0,
|
|
63
|
+
options: this.range(60, this.secondStep),
|
|
64
|
+
disabled: this.callRule(this.rules.disabledSeconds, h, m)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
return cols
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
methods: {
|
|
71
|
+
range(count, step) {
|
|
72
|
+
const out = []
|
|
73
|
+
const s = step > 0 ? step : 1
|
|
74
|
+
for (let i = 0; i < count; i += s) out.push(i)
|
|
75
|
+
return out
|
|
76
|
+
},
|
|
77
|
+
callRule(fn, a, b) {
|
|
78
|
+
if (typeof fn !== 'function') return []
|
|
79
|
+
const r = fn(a, b)
|
|
80
|
+
return Array.isArray(r) ? r : []
|
|
81
|
+
},
|
|
82
|
+
pad(n) {
|
|
83
|
+
return n < 10 ? '0' + n : '' + n
|
|
84
|
+
},
|
|
85
|
+
onPick(key, opt) {
|
|
86
|
+
const next = {
|
|
87
|
+
hour: this.value.hour || 0,
|
|
88
|
+
minute: this.value.minute || 0,
|
|
89
|
+
second: this.value.second || 0
|
|
90
|
+
}
|
|
91
|
+
next[key] = opt
|
|
92
|
+
this.$emit('change', next)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<style src="./time-panel.css" scoped></style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
.ui-cal-panel {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--spacing-sm);
|
|
5
|
+
padding: var(--spacing-md);
|
|
6
|
+
background: var(--card);
|
|
7
|
+
font-family: var(--font-family-body);
|
|
8
|
+
border-radius: var(--rounded-xl);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.ui-cal-panel__main {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: var(--spacing-md);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.ui-cal-panel__body {
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: var(--spacing-lg);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Months sit side by side; each grid is 7×36px wide. */
|
|
23
|
+
.ui-cal-panel__months {
|
|
24
|
+
display: flex;
|
|
25
|
+
gap: var(--spacing-md);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.ui-cal-panel__month {
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Time column divided from the calendar. */
|
|
33
|
+
.ui-cal-panel__time {
|
|
34
|
+
padding-left: var(--spacing-xs);
|
|
35
|
+
border-left: 1px solid var(--border-primary);
|
|
36
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
.ui-cal-view {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--spacing-2xs);
|
|
5
|
+
font-family: var(--font-family-body);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Weekday header row — 7 fixed 36px tracks, muted labels. */
|
|
9
|
+
.ui-cal-view__weekdays {
|
|
10
|
+
display: grid;
|
|
11
|
+
grid-template-columns: repeat(7, 32px);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.ui-cal-view__weekday {
|
|
15
|
+
height: 32px;
|
|
16
|
+
width: 32px;
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
color: var(--muted-fg);
|
|
21
|
+
font-size: var(--paragraph-small-font-size);
|
|
22
|
+
line-height: var(--paragraph-small-line-height);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* Day grid — 7 columns, cells butt together horizontally (column-gap 0) so the
|
|
27
|
+
* range band reads as one continuous strip; a small row-gap keeps weeks legible.
|
|
28
|
+
*/
|
|
29
|
+
.ui-cal-view__grid {
|
|
30
|
+
display: grid;
|
|
31
|
+
grid-template-columns: repeat(7, 32px);
|
|
32
|
+
row-gap: 1px;
|
|
33
|
+
column-gap: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.ui-cal-cell {
|
|
37
|
+
position: relative;
|
|
38
|
+
width: 32px;
|
|
39
|
+
height: 32px;
|
|
40
|
+
padding: 0;
|
|
41
|
+
border: 0;
|
|
42
|
+
background: transparent;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
font-family: inherit;
|
|
45
|
+
font-size: var(--paragraph-small-font-size);
|
|
46
|
+
line-height: var(--paragraph-small-line-height);
|
|
47
|
+
color: var(--primary-fg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* Range band layer (::before) sits behind the number. Full-width for interior
|
|
52
|
+
* days so neighbours connect; half-width toward the range on the endpoints.
|
|
53
|
+
*/
|
|
54
|
+
.ui-cal-cell::before {
|
|
55
|
+
content: '';
|
|
56
|
+
position: absolute;
|
|
57
|
+
top: 0px;
|
|
58
|
+
bottom: 0px;
|
|
59
|
+
left: 0;
|
|
60
|
+
right: 0;
|
|
61
|
+
background: transparent;
|
|
62
|
+
z-index: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.ui-cal-cell.is-in-range::before {
|
|
66
|
+
background: var(--accent-1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.ui-cal-cell.is-range-left::before {
|
|
70
|
+
left: 50%;
|
|
71
|
+
right: 0;
|
|
72
|
+
background: var(--brand-100);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.ui-cal-cell.is-range-right::before {
|
|
76
|
+
left: 0;
|
|
77
|
+
right: 50%;
|
|
78
|
+
background: var(--brand-100);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* The visible day chip — rounded, holds hover / selected fills and the number. */
|
|
82
|
+
.ui-cal-cell__inner {
|
|
83
|
+
position: relative;
|
|
84
|
+
z-index: 1;
|
|
85
|
+
display: inline-flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
justify-content: center;
|
|
88
|
+
width: 32px;
|
|
89
|
+
height: 32px;
|
|
90
|
+
border-radius: var(--rounded-lg);
|
|
91
|
+
transition:
|
|
92
|
+
background-color 0.12s ease,
|
|
93
|
+
color 0.12s ease;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.ui-cal-cell:hover:not(.is-disabled):not(.is-selected) .ui-cal-cell__inner {
|
|
97
|
+
background: var(--accent-1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.ui-cal-cell.is-outside {
|
|
101
|
+
color: var(--muted-fg);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.ui-cal-cell.is-today .ui-cal-cell__inner {
|
|
105
|
+
box-shadow: inset 0 0 0 1px var(--border-brand);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.ui-cal-cell.is-selected .ui-cal-cell__inner {
|
|
109
|
+
background: var(--primary-brand-bg);
|
|
110
|
+
color: var(--inverse-fg);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Selected endpoint wins over the today ring. */
|
|
114
|
+
.ui-cal-cell.is-selected.is-today .ui-cal-cell__inner {
|
|
115
|
+
box-shadow: none;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.ui-cal-cell.is-disabled {
|
|
119
|
+
color: var(--muted-fg);
|
|
120
|
+
cursor: not-allowed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.ui-cal-cell.is-disabled .ui-cal-cell__inner {
|
|
124
|
+
background: transparent;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.ui-cal-cell.is-focus:focus-visible {
|
|
128
|
+
outline: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.ui-cal-cell.is-focus:focus-visible .ui-cal-cell__inner {
|
|
132
|
+
outline: 2px solid var(--focus-ring);
|
|
133
|
+
outline-offset: 1px;
|
|
134
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Pure calendar math for the date-picker — no Vue, no component state.
|
|
2
|
+
//
|
|
3
|
+
// Everything here operates on plain values (ISO 'YYYY-MM-DD' strings, Day.js
|
|
4
|
+
// instances, plain option objects) so it can be unit-tested in isolation and
|
|
5
|
+
// reused by the week/month/quarter/year panels in Phase 2. The picker stores
|
|
6
|
+
// its selection as ISO strings internally (stable identity, no deep-reactive
|
|
7
|
+
// Day.js objects in `data`) and only converts to Date/formatted output at the
|
|
8
|
+
// component boundary.
|
|
9
|
+
import dayjs from './dayjs.js'
|
|
10
|
+
|
|
11
|
+
export const ISO = 'YYYY-MM-DD'
|
|
12
|
+
|
|
13
|
+
// Normalize any accepted value (Date | ISO/formatted string | Day.js | number)
|
|
14
|
+
// into a Day.js instance, or null when empty/invalid. `format` is consulted for
|
|
15
|
+
// string parsing so a custom `format` prop round-trips.
|
|
16
|
+
export function toDayjs(value, format) {
|
|
17
|
+
if (value == null || value === '') return null
|
|
18
|
+
if (dayjs.isDayjs(value)) return value.isValid() ? value : null
|
|
19
|
+
if (value instanceof Date) {
|
|
20
|
+
const d = dayjs(value)
|
|
21
|
+
return d.isValid() ? d : null
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === 'number') {
|
|
24
|
+
const d = dayjs(value)
|
|
25
|
+
return d.isValid() ? d : null
|
|
26
|
+
}
|
|
27
|
+
// string: try the explicit format first, then ISO, then loose parse.
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
if (format) {
|
|
30
|
+
const byFmt = dayjs(value, format)
|
|
31
|
+
if (byFmt.isValid()) return byFmt
|
|
32
|
+
}
|
|
33
|
+
const byIso = dayjs(value, ISO)
|
|
34
|
+
if (byIso.isValid()) return byIso
|
|
35
|
+
const loose = dayjs(value)
|
|
36
|
+
return loose.isValid() ? loose : null
|
|
37
|
+
}
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Day.js → canonical ISO date key (drops time; the day grid keys on this).
|
|
42
|
+
export function toISO(value, format) {
|
|
43
|
+
const d = toDayjs(value, format)
|
|
44
|
+
return d ? d.format(ISO) : null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Is `d` (Day.js) outside the allowed window / vetoed by disabledDate(Date)?
|
|
48
|
+
export function isDateDisabled(d, opts) {
|
|
49
|
+
if (!d) return true
|
|
50
|
+
const o = opts || {}
|
|
51
|
+
if (o.minDate && d.isBefore(o.minDate, 'day')) return true
|
|
52
|
+
if (o.maxDate && d.isAfter(o.maxDate, 'day')) return true
|
|
53
|
+
if (typeof o.disabledDate === 'function' && o.disabledDate(d.toDate())) return true
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Locale-aware weekday header labels, rotated to `weekStart` (0 = Sunday).
|
|
58
|
+
// Uses Day.js localeData min-weekday names ('Su','Mo',...).
|
|
59
|
+
export function weekdayLabels(weekStart, locale) {
|
|
60
|
+
const base = (locale ? dayjs().locale(locale) : dayjs()).localeData().weekdaysMin()
|
|
61
|
+
const out = []
|
|
62
|
+
for (let i = 0; i < 7; i++) out.push(base[(i + weekStart) % 7])
|
|
63
|
+
return out
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Build the 6×7 (42-cell) day grid for the month containing `viewDate`.
|
|
67
|
+
// `weekStart` rotates the leading offset. Cells outside the view month are
|
|
68
|
+
// flagged `outside` (rendered muted) but remain selectable unless disabled.
|
|
69
|
+
export function buildDayMatrix(viewDate, weekStart, opts) {
|
|
70
|
+
const ws = weekStart || 0
|
|
71
|
+
const first = viewDate.startOf('month')
|
|
72
|
+
const offset = (first.day() - ws + 7) % 7
|
|
73
|
+
const start = first.subtract(offset, 'day')
|
|
74
|
+
const today = dayjs()
|
|
75
|
+
const month = viewDate.month()
|
|
76
|
+
const cells = []
|
|
77
|
+
for (let i = 0; i < 35; i++) {
|
|
78
|
+
const d = start.add(i, 'day')
|
|
79
|
+
const dow = d.day()
|
|
80
|
+
cells.push({
|
|
81
|
+
iso: d.format(ISO),
|
|
82
|
+
day: d.date(),
|
|
83
|
+
outside: d.month() !== month,
|
|
84
|
+
today: d.isSame(today, 'day'),
|
|
85
|
+
weekend: dow === 0 || dow === 6,
|
|
86
|
+
disabled: isDateDisabled(d, opts)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
return cells
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Order two ISO keys ascending, tolerating either being null.
|
|
93
|
+
export function orderISO(a, b) {
|
|
94
|
+
if (a == null) return { start: b, end: b }
|
|
95
|
+
if (b == null) return { start: a, end: a }
|
|
96
|
+
return a <= b ? { start: a, end: b } : { start: b, end: a }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Position of a cell within an active range [start,end] (both ISO), used to pick
|
|
100
|
+
// the connected-band radius: 'single' | 'left' | 'middle' | 'right' | null.
|
|
101
|
+
export function rangePosition(iso, start, end) {
|
|
102
|
+
if (!start && !end) return null
|
|
103
|
+
const s = start
|
|
104
|
+
const e = end || start
|
|
105
|
+
if (s === e) return iso === s ? 'single' : null
|
|
106
|
+
if (iso === s) return 'left'
|
|
107
|
+
if (iso === e) return 'right'
|
|
108
|
+
if (iso > s && iso < e) return 'middle'
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// True when `iso` sits strictly inside (start,end) — the range fill band.
|
|
113
|
+
export function isWithinRange(iso, start, end) {
|
|
114
|
+
if (!start || !end || start === end) return false
|
|
115
|
+
return iso > start && iso < end
|
|
116
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
.ui-date-picker {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.ui-date-picker__trigger {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: var(--spacing-xs);
|
|
9
|
+
width: 100%;
|
|
10
|
+
min-width: 200px;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
height: 36px;
|
|
13
|
+
padding: 0 var(--spacing-sm);
|
|
14
|
+
background: var(--input);
|
|
15
|
+
border: 1px solid var(--border-primary);
|
|
16
|
+
border-radius: var(--radius);
|
|
17
|
+
color: var(--primary-fg);
|
|
18
|
+
font-family: var(--font-family-body);
|
|
19
|
+
font-size: var(--paragraph-small-font-size);
|
|
20
|
+
line-height: var(--paragraph-small-line-height);
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
transition:
|
|
23
|
+
border-color 0.12s ease,
|
|
24
|
+
box-shadow 0.12s ease;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.ui-date-picker__trigger--sm {
|
|
28
|
+
height: 28px;
|
|
29
|
+
padding: 0 var(--spacing-xs);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.ui-date-picker__trigger--lg {
|
|
33
|
+
height: 44px;
|
|
34
|
+
padding: 0 var(--spacing-md);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.ui-date-picker__trigger:hover:not(.is-disabled) {
|
|
38
|
+
border-color: var(--border-focus);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.ui-date-picker__trigger.is-focus {
|
|
42
|
+
border-color: var(--border-focus);
|
|
43
|
+
box-shadow: 0 0 0 3px var(--focus-ring);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.ui-date-picker__trigger:focus-visible {
|
|
47
|
+
outline: none;
|
|
48
|
+
border-color: var(--border-focus);
|
|
49
|
+
box-shadow: 0 0 0 3px var(--focus-ring);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.ui-date-picker__trigger.is-disabled {
|
|
53
|
+
background: var(--muted-bg);
|
|
54
|
+
color: var(--muted-fg);
|
|
55
|
+
cursor: not-allowed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.ui-date-picker__icon {
|
|
59
|
+
display: inline-flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
color: var(--muted-fg);
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.ui-date-picker__text {
|
|
66
|
+
flex: 1;
|
|
67
|
+
min-width: 0;
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
white-space: nowrap;
|
|
70
|
+
text-overflow: ellipsis;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ui-date-picker__text.is-placeholder {
|
|
74
|
+
color: var(--muted-fg);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ui-date-picker__clear {
|
|
78
|
+
display: inline-flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: center;
|
|
81
|
+
flex-shrink: 0;
|
|
82
|
+
width: 18px;
|
|
83
|
+
height: 18px;
|
|
84
|
+
padding: 0;
|
|
85
|
+
border: 0;
|
|
86
|
+
border-radius: var(--rounded-full);
|
|
87
|
+
background: transparent;
|
|
88
|
+
color: var(--muted-fg);
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.ui-date-picker__clear:hover {
|
|
93
|
+
background: var(--accent-1);
|
|
94
|
+
color: var(--primary-fg);
|
|
95
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Configured Day.js singleton for the date-picker.
|
|
2
|
+
//
|
|
3
|
+
// Day.js ships as a tiny core; every feature the picker needs (custom format
|
|
4
|
+
// parsing, locale-aware week start, week/quarter numbers, inclusive comparisons)
|
|
5
|
+
// lives in an opt-in plugin. We register them ONCE here and re-export the same
|
|
6
|
+
// instance so every date-picker file shares one configured dayjs — importing
|
|
7
|
+
// `dayjs` directly elsewhere would get the unconfigured core.
|
|
8
|
+
//
|
|
9
|
+
// `dayjs.extend` is idempotent (re-extending with the same plugin is a no-op),
|
|
10
|
+
// so this module is safe to import from multiple files under both the Vite
|
|
11
|
+
// (Vue 3) and webpack (Vue 2) bundlers.
|
|
12
|
+
import dayjs from 'dayjs'
|
|
13
|
+
|
|
14
|
+
import customParseFormat from 'dayjs/plugin/customParseFormat.js'
|
|
15
|
+
import localeData from 'dayjs/plugin/localeData.js'
|
|
16
|
+
import weekday from 'dayjs/plugin/weekday.js'
|
|
17
|
+
import weekOfYear from 'dayjs/plugin/weekOfYear.js'
|
|
18
|
+
import isoWeek from 'dayjs/plugin/isoWeek.js'
|
|
19
|
+
import advancedFormat from 'dayjs/plugin/advancedFormat.js'
|
|
20
|
+
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore.js'
|
|
21
|
+
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter.js'
|
|
22
|
+
import quarterOfYear from 'dayjs/plugin/quarterOfYear.js'
|
|
23
|
+
|
|
24
|
+
dayjs.extend(customParseFormat)
|
|
25
|
+
dayjs.extend(localeData)
|
|
26
|
+
dayjs.extend(weekday)
|
|
27
|
+
dayjs.extend(weekOfYear)
|
|
28
|
+
dayjs.extend(isoWeek)
|
|
29
|
+
dayjs.extend(advancedFormat)
|
|
30
|
+
dayjs.extend(isSameOrBefore)
|
|
31
|
+
dayjs.extend(isSameOrAfter)
|
|
32
|
+
dayjs.extend(quarterOfYear)
|
|
33
|
+
|
|
34
|
+
export default dayjs
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
.ui-cal-footer {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
gap: var(--spacing-sm);
|
|
6
|
+
padding-top: var(--spacing-sm);
|
|
7
|
+
border-top: 1px solid var(--border-primary);
|
|
8
|
+
font-family: var(--font-family-body);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.ui-cal-footer__left {
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.ui-cal-footer__right {
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: var(--spacing-xs);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.ui-cal-footer__link {
|
|
23
|
+
border: 0;
|
|
24
|
+
background: transparent;
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
padding: 0;
|
|
27
|
+
font-family: inherit;
|
|
28
|
+
font-size: var(--paragraph-mini-font-size);
|
|
29
|
+
line-height: var(--paragraph-mini-line-height);
|
|
30
|
+
letter-spacing: var(--paragraph-mini-letter-spacing);
|
|
31
|
+
color: var(--primary-brand-fg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.ui-cal-footer__link:hover:not(:disabled) {
|
|
35
|
+
color: var(--primary-brand-hover);
|
|
36
|
+
text-decoration: underline;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.ui-cal-footer__link:disabled {
|
|
40
|
+
color: var(--muted-fg);
|
|
41
|
+
cursor: not-allowed;
|
|
42
|
+
}
|