@sanguo-dev/calendar-ui 0.0.0-stage → 0.0.2
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/README.md +194 -3
- package/dist/index.cjs +1079 -0
- package/dist/index.d.cts +286 -0
- package/dist/index.d.mts +286 -0
- package/dist/index.mjs +1074 -0
- package/package.json +61 -6
- package/src/calendar.ts +785 -0
- package/src/components/day.ts +79 -0
- package/src/components/grid.ts +181 -0
- package/src/components/header.ts +69 -0
- package/src/components/nav.ts +20 -0
- package/src/components/weekdays.ts +19 -0
- package/src/index.ts +26 -0
- package/src/locales/en-US.ts +28 -0
- package/src/locales/index.ts +19 -0
- package/src/locales/zh-CN.ts +13 -0
- package/src/style.css +388 -0
- package/src/types/calendar.ts +267 -0
- package/src/types/components.ts +69 -0
- package/src/types/index.ts +32 -0
- package/src/types/locale.ts +26 -0
- package/src/types/utils.ts +28 -0
- package/src/utils/date.ts +82 -0
- package/src/utils/days.ts +88 -0
- package/src/utils/dom.ts +11 -0
- package/src/utils/motion.ts +39 -0
package/src/style.css
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/* calendar-ui 的默认皮肤:所有颜色/尺寸都走 CSS 变量,外部改 .cui 即可换主题 */
|
|
2
|
+
.cui {
|
|
3
|
+
--cui-bg: #ffffff;
|
|
4
|
+
--cui-fg: #1f2328;
|
|
5
|
+
--cui-muted: #8b949e;
|
|
6
|
+
--cui-border: #e6e8eb;
|
|
7
|
+
--cui-accent: #2563eb;
|
|
8
|
+
--cui-accent-fg: #ffffff;
|
|
9
|
+
--cui-rest: #e5484d;
|
|
10
|
+
--cui-hover: #f3f4f6;
|
|
11
|
+
--cui-radius: 10px;
|
|
12
|
+
|
|
13
|
+
color-scheme: light;
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
width: 100%;
|
|
16
|
+
max-width: 420px;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
padding: 12px;
|
|
19
|
+
border: 1px solid var(--cui-border);
|
|
20
|
+
border-radius: var(--cui-radius);
|
|
21
|
+
background: var(--cui-bg);
|
|
22
|
+
color: var(--cui-fg);
|
|
23
|
+
font-family: system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
24
|
+
font-size: 14px;
|
|
25
|
+
line-height: 1.4;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* 双月范围视图 */
|
|
29
|
+
.cui--range {
|
|
30
|
+
max-width: 900px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.cui__months {
|
|
34
|
+
display: grid;
|
|
35
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
36
|
+
gap: 16px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.cui__month {
|
|
40
|
+
min-width: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@media (max-width: 720px) {
|
|
44
|
+
.cui__months {
|
|
45
|
+
grid-template-columns: minmax(0, 1fr);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* 显式深色 */
|
|
50
|
+
.cui[data-theme="dark"] {
|
|
51
|
+
--cui-bg: #161b22;
|
|
52
|
+
--cui-fg: #e6edf3;
|
|
53
|
+
--cui-muted: #8b949e;
|
|
54
|
+
--cui-border: #30363d;
|
|
55
|
+
--cui-accent: #1f6feb;
|
|
56
|
+
--cui-accent-fg: #ffffff;
|
|
57
|
+
--cui-rest: #ff7b72;
|
|
58
|
+
--cui-hover: #21262d;
|
|
59
|
+
|
|
60
|
+
color-scheme: dark;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* auto:优先跟随最近的 data-theme="dark" */
|
|
64
|
+
[data-theme="dark"] .cui[data-theme="auto"] {
|
|
65
|
+
--cui-bg: #161b22;
|
|
66
|
+
--cui-fg: #e6edf3;
|
|
67
|
+
--cui-muted: #8b949e;
|
|
68
|
+
--cui-border: #30363d;
|
|
69
|
+
--cui-accent: #1f6feb;
|
|
70
|
+
--cui-accent-fg: #ffffff;
|
|
71
|
+
--cui-rest: #ff7b72;
|
|
72
|
+
--cui-hover: #21262d;
|
|
73
|
+
|
|
74
|
+
color-scheme: dark;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* auto 且没有 data-theme 时跟随系统 */
|
|
78
|
+
@media (prefers-color-scheme: dark) {
|
|
79
|
+
.cui[data-theme="auto"] {
|
|
80
|
+
--cui-bg: #161b22;
|
|
81
|
+
--cui-fg: #e6edf3;
|
|
82
|
+
--cui-muted: #8b949e;
|
|
83
|
+
--cui-border: #30363d;
|
|
84
|
+
--cui-accent: #1f6feb;
|
|
85
|
+
--cui-accent-fg: #ffffff;
|
|
86
|
+
--cui-rest: #ff7b72;
|
|
87
|
+
--cui-hover: #21262d;
|
|
88
|
+
|
|
89
|
+
color-scheme: dark;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* 外层明确 light 时,系统深色不覆盖 */
|
|
93
|
+
[data-theme="light"] .cui[data-theme="auto"] {
|
|
94
|
+
--cui-bg: #ffffff;
|
|
95
|
+
--cui-fg: #1f2328;
|
|
96
|
+
--cui-muted: #8b949e;
|
|
97
|
+
--cui-border: #e6e8eb;
|
|
98
|
+
--cui-accent: #2563eb;
|
|
99
|
+
--cui-accent-fg: #ffffff;
|
|
100
|
+
--cui-rest: #e5484d;
|
|
101
|
+
--cui-hover: #f3f4f6;
|
|
102
|
+
|
|
103
|
+
color-scheme: light;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.cui *,
|
|
108
|
+
.cui *::before,
|
|
109
|
+
.cui *::after {
|
|
110
|
+
box-sizing: border-box;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* 头部:上个月 / 年月标题 / 下个月 */
|
|
114
|
+
.cui__header {
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: space-between;
|
|
118
|
+
gap: 8px;
|
|
119
|
+
padding: 4px 2px 10px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.cui__title {
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
font-size: 15px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.cui__nav {
|
|
128
|
+
width: 30px;
|
|
129
|
+
height: 30px;
|
|
130
|
+
border: 1px solid var(--cui-border);
|
|
131
|
+
border-radius: 8px;
|
|
132
|
+
background: transparent;
|
|
133
|
+
color: var(--cui-fg);
|
|
134
|
+
font-size: 16px;
|
|
135
|
+
line-height: 1;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.cui__nav:hover {
|
|
140
|
+
background: var(--cui-hover);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.cui__nav:disabled {
|
|
144
|
+
opacity: 0.45;
|
|
145
|
+
cursor: not-allowed;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.cui__nav:disabled:hover {
|
|
149
|
+
background: transparent;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.cui__nav:focus-visible,
|
|
153
|
+
.cui__day:focus-visible {
|
|
154
|
+
outline: 2px solid var(--cui-accent);
|
|
155
|
+
outline-offset: 1px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* 星期表头 */
|
|
159
|
+
.cui__weekdays {
|
|
160
|
+
display: grid;
|
|
161
|
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
162
|
+
gap: 4px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* 6 行日期;折叠时按行收起,节点仍然复用 */
|
|
166
|
+
.cui__grid {
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
gap: 0;
|
|
170
|
+
contain: layout paint;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.cui__week {
|
|
174
|
+
display: grid;
|
|
175
|
+
min-height: 0;
|
|
176
|
+
margin-bottom: 4px;
|
|
177
|
+
overflow: hidden;
|
|
178
|
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
179
|
+
gap: 4px;
|
|
180
|
+
opacity: 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.cui__week:last-child {
|
|
184
|
+
margin-bottom: 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* 折叠动画由 JS 按真实行高驱动(见 components/grid.ts),这里只负责最终状态 */
|
|
188
|
+
.cui__week.is-collapsed-hidden {
|
|
189
|
+
height: 0;
|
|
190
|
+
margin-bottom: 0;
|
|
191
|
+
opacity: 0;
|
|
192
|
+
pointer-events: none;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.cui__week[hidden] {
|
|
196
|
+
display: none;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.cui__viewport {
|
|
200
|
+
position: relative;
|
|
201
|
+
min-width: 0;
|
|
202
|
+
overflow: hidden;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.cui__grid.is-month-clone {
|
|
206
|
+
position: absolute;
|
|
207
|
+
inset: 0;
|
|
208
|
+
pointer-events: none;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* 只在动画期间提升为合成层,动画结束后随类名一起移除 */
|
|
212
|
+
.cui__grid.is-month-clone,
|
|
213
|
+
.cui__grid.is-month-in-next,
|
|
214
|
+
.cui__grid.is-month-in-prev {
|
|
215
|
+
will-change: transform;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.cui--no-animation .cui__grid,
|
|
219
|
+
.cui--no-animation .cui__week {
|
|
220
|
+
transition: none;
|
|
221
|
+
animation: none;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@media (prefers-reduced-motion: reduce) {
|
|
225
|
+
.cui__grid,
|
|
226
|
+
.cui__week {
|
|
227
|
+
transition: none;
|
|
228
|
+
animation: none;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.cui__weekday {
|
|
233
|
+
padding: 4px 0;
|
|
234
|
+
color: var(--cui-muted);
|
|
235
|
+
font-size: 12px;
|
|
236
|
+
text-align: center;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* 日期格 */
|
|
240
|
+
.cui__day {
|
|
241
|
+
position: relative;
|
|
242
|
+
display: flex;
|
|
243
|
+
flex-direction: column;
|
|
244
|
+
align-items: center;
|
|
245
|
+
justify-content: center;
|
|
246
|
+
gap: 2px;
|
|
247
|
+
min-height: 46px;
|
|
248
|
+
padding: 4px 2px;
|
|
249
|
+
border: 1px solid transparent;
|
|
250
|
+
border-radius: 8px;
|
|
251
|
+
background: transparent;
|
|
252
|
+
color: inherit;
|
|
253
|
+
font: inherit;
|
|
254
|
+
cursor: pointer;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.cui__day:hover {
|
|
258
|
+
background: var(--cui-hover);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.cui__day-num {
|
|
262
|
+
font-size: 15px;
|
|
263
|
+
font-weight: 500;
|
|
264
|
+
line-height: 1.1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* 周末:日期数字固定用节假日红(--cui-rest),不由节假日数据决定 */
|
|
268
|
+
.cui__day.is-weekend .cui__day-num {
|
|
269
|
+
color: var(--cui-rest);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* 选中时数字跟随反白色,避免红底红字 */
|
|
273
|
+
.cui__day.is-selected .cui__day-num {
|
|
274
|
+
color: currentColor;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.cui__day-sub {
|
|
278
|
+
max-width: 100%;
|
|
279
|
+
overflow: hidden;
|
|
280
|
+
color: var(--cui-muted);
|
|
281
|
+
font-size: 11px;
|
|
282
|
+
white-space: nowrap;
|
|
283
|
+
text-overflow: ellipsis;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* 左上角的休 / 班标记 */
|
|
287
|
+
.cui__day-tag {
|
|
288
|
+
position: absolute;
|
|
289
|
+
top: 3px;
|
|
290
|
+
left: 4px;
|
|
291
|
+
font-size: 10px;
|
|
292
|
+
font-weight: 600;
|
|
293
|
+
line-height: 1;
|
|
294
|
+
pointer-events: none;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.cui__day-tag[hidden] {
|
|
298
|
+
display: none;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.cui__day-tag[data-tone="rest"] {
|
|
302
|
+
color: var(--cui-rest);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.cui__day-tag[data-tone="work"] {
|
|
306
|
+
color: var(--cui-accent);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.cui__day.is-selected .cui__day-tag {
|
|
310
|
+
color: currentColor;
|
|
311
|
+
opacity: 0.9;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* 状态 */
|
|
315
|
+
.cui__day.is-outside {
|
|
316
|
+
opacity: 0.35;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.cui__day.is-disabled,
|
|
320
|
+
.cui__day:disabled {
|
|
321
|
+
opacity: 0.35;
|
|
322
|
+
cursor: not-allowed;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.cui__day.is-disabled:hover,
|
|
326
|
+
.cui__day:disabled:hover {
|
|
327
|
+
background: transparent;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.cui__day[data-tone="rest"] .cui__day-sub {
|
|
331
|
+
color: var(--cui-rest);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.cui__day.is-today {
|
|
335
|
+
border-color: var(--cui-accent);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.cui__day.is-selected {
|
|
339
|
+
background: var(--cui-accent);
|
|
340
|
+
color: var(--cui-accent-fg);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.cui__day.is-in-range:not(.is-selected) {
|
|
344
|
+
background: color-mix(in srgb, var(--cui-accent) 14%, transparent);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.cui__day.is-in-range:hover:not(.is-selected) {
|
|
348
|
+
background: color-mix(in srgb, var(--cui-accent) 22%, transparent);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.cui__day.is-selected .cui__day-sub {
|
|
352
|
+
color: currentColor;
|
|
353
|
+
opacity: 0.85;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* 折叠开关:始终只有一个按钮,箭头随状态切换 */
|
|
357
|
+
.cui__collapse {
|
|
358
|
+
display: flex;
|
|
359
|
+
width: 100%;
|
|
360
|
+
align-items: center;
|
|
361
|
+
justify-content: center;
|
|
362
|
+
margin-top: 8px;
|
|
363
|
+
padding: 2px 0;
|
|
364
|
+
border: 0;
|
|
365
|
+
border-radius: 6px;
|
|
366
|
+
background: transparent;
|
|
367
|
+
color: var(--cui-muted);
|
|
368
|
+
font: inherit;
|
|
369
|
+
font-size: 14px;
|
|
370
|
+
line-height: 1;
|
|
371
|
+
cursor: pointer;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.cui__collapse:hover {
|
|
375
|
+
color: var(--cui-fg);
|
|
376
|
+
background: var(--cui-hover);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.cui__collapse:focus-visible {
|
|
380
|
+
outline: 2px solid var(--cui-accent);
|
|
381
|
+
outline-offset: 1px;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* 隐藏状态:类选择器里的 display 会盖掉 [hidden] 的默认样式,这里显式补上 */
|
|
385
|
+
.cui[hidden],
|
|
386
|
+
.cui__header[hidden] {
|
|
387
|
+
display: none;
|
|
388
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { CalendarLocale, CalendarLocaleName } from "./locale";
|
|
2
|
+
|
|
3
|
+
/** 一周从哪天开始:0 为周日,1 为周一 */
|
|
4
|
+
export type WeekStartsOn = 0 | 1;
|
|
5
|
+
|
|
6
|
+
/** 主题:light / dark 显式指定,auto 跟随最近的 data-theme 或系统偏好 */
|
|
7
|
+
export type CalendarTheme = "light" | "dark" | "auto";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 外部传入的日期
|
|
11
|
+
*
|
|
12
|
+
* 支持 Date,或本地日期字符串:
|
|
13
|
+
* `2026-10-01`、`2026-10-1`、`2026/10/01`、`2026/10/1`。
|
|
14
|
+
*/
|
|
15
|
+
export type CalendarDateInput = Date | string;
|
|
16
|
+
|
|
17
|
+
/** 选择模式:单选 / 范围 */
|
|
18
|
+
export type CalendarMode = "single" | "range";
|
|
19
|
+
|
|
20
|
+
/** 范围选择结果,start/end 为 null 表示还没选完 */
|
|
21
|
+
export interface CalendarRange {
|
|
22
|
+
start: Date | null;
|
|
23
|
+
end: Date | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 自定义副标题:返回 null/undefined 时回退到 adapter 的 subtitle */
|
|
27
|
+
export type SubtitleRenderer = (day: CalendarDate) => string | null | undefined;
|
|
28
|
+
|
|
29
|
+
/** 翻月滑动动画;传了 slide 就开启,不传就是直接切换 */
|
|
30
|
+
export interface SlideAnimationOptions {
|
|
31
|
+
/** 动画时长,单位 ms,默认 220 */
|
|
32
|
+
duration?: number;
|
|
33
|
+
/** 缓动函数,默认 "ease" */
|
|
34
|
+
easing?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 折叠配置;传了 collapse 就显示底部折叠箭头并开启过渡 */
|
|
38
|
+
export interface CollapseAnimationOptions {
|
|
39
|
+
/** 折叠时长,单位 ms,默认 200 */
|
|
40
|
+
duration?: number;
|
|
41
|
+
/** 缓动函数,默认 "ease" */
|
|
42
|
+
easing?: string;
|
|
43
|
+
/** 初始是否折叠,默认 false(展开) */
|
|
44
|
+
defaultCollapsed?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 动画配置;整块不传时不做任何动画,也不显示折叠箭头 */
|
|
48
|
+
export interface CalendarAnimationOptions {
|
|
49
|
+
/** 翻月滑动;`{}` 表示用默认时长和缓动 */
|
|
50
|
+
slide?: SlideAnimationOptions;
|
|
51
|
+
/** 折叠;`{}` 表示启用折叠并默认展开 */
|
|
52
|
+
collapse?: CollapseAnimationOptions;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 规范后的年月 */
|
|
56
|
+
export interface YearMonth {
|
|
57
|
+
year: number;
|
|
58
|
+
/** 1-12 */
|
|
59
|
+
month: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 日历格的公历日期部分 */
|
|
63
|
+
export interface CalendarDate {
|
|
64
|
+
/** 公历年 */
|
|
65
|
+
year: number;
|
|
66
|
+
/** 公历月,1-12 */
|
|
67
|
+
month: number;
|
|
68
|
+
/** 公历日 */
|
|
69
|
+
day: number;
|
|
70
|
+
/** 该格对应的日期对象(本地时区) */
|
|
71
|
+
date: Date;
|
|
72
|
+
/** 是否属于当前展示的月份(false 表示上一月/下一月的补位格) */
|
|
73
|
+
inCurrentMonth: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 格子左上角的小标记,例如「休」「班」 */
|
|
77
|
+
export interface CalendarBadge {
|
|
78
|
+
/** 显示文字 */
|
|
79
|
+
text: string;
|
|
80
|
+
/** 传给样式的 data-tone,核心不解释具体含义 */
|
|
81
|
+
tone?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 适配器对单个格子的装饰 */
|
|
85
|
+
export interface DayDecoration<TData = unknown> {
|
|
86
|
+
/** 格子副标题,例如节气、节日、农历日 */
|
|
87
|
+
subtitle?: string;
|
|
88
|
+
/** 左上角小标记;不传则不显示 */
|
|
89
|
+
badge?: CalendarBadge;
|
|
90
|
+
/** 透传给 onSelect 的数据,核心不解释 */
|
|
91
|
+
data?: TData;
|
|
92
|
+
/** 覆盖默认 aria-label;不传时由日期、标记、副标题拼接 */
|
|
93
|
+
ariaLabel?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 每次渲染月视图时传给适配器的上下文 */
|
|
97
|
+
export interface MonthInput {
|
|
98
|
+
/** 公历年 */
|
|
99
|
+
year: number;
|
|
100
|
+
/** 公历月,1-12 */
|
|
101
|
+
month: number;
|
|
102
|
+
/** 一周从哪天开始 */
|
|
103
|
+
weekStartsOn: WeekStartsOn;
|
|
104
|
+
/** 今天 */
|
|
105
|
+
today: Date;
|
|
106
|
+
/** 当前界面语言 */
|
|
107
|
+
locale: CalendarLocale;
|
|
108
|
+
/** 固定 42 个日期,包含上下月补位格 */
|
|
109
|
+
days: readonly CalendarDate[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 日历适配器
|
|
114
|
+
*
|
|
115
|
+
* 核心只认识这个接口,不依赖任何具体的农历/节假日实现。
|
|
116
|
+
* 第三方可以实现它接入自己的日历数据。
|
|
117
|
+
*/
|
|
118
|
+
export interface CalendarAdapter<TData = unknown> {
|
|
119
|
+
/** 适配器标识,用于调试和排查 */
|
|
120
|
+
readonly id: string;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 生成当前月的 42 个格子的装饰
|
|
124
|
+
*
|
|
125
|
+
* 返回数组按索引和 input.days 对齐;没有装饰的位置返回 null。
|
|
126
|
+
* 每次换月、选中、重新渲染都会调用,适配器应自行缓存重计算。
|
|
127
|
+
*/
|
|
128
|
+
decorateMonth(input: MonthInput): ReadonlyArray<DayDecoration<TData> | null>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 日历中的一格 */
|
|
132
|
+
export interface CalendarDay<TData = null> extends CalendarDate {
|
|
133
|
+
/** 是否今天 */
|
|
134
|
+
isToday: boolean;
|
|
135
|
+
/** 是否当前选中 */
|
|
136
|
+
isSelected: boolean;
|
|
137
|
+
/** 是否被 minDate / maxDate / disabledDates 禁止 */
|
|
138
|
+
isDisabled: boolean;
|
|
139
|
+
/** 是否范围起点 */
|
|
140
|
+
isRangeStart: boolean;
|
|
141
|
+
/** 是否范围终点 */
|
|
142
|
+
isRangeEnd: boolean;
|
|
143
|
+
/** 是否在已选范围内(含起终点) */
|
|
144
|
+
isInRange: boolean;
|
|
145
|
+
/** 格子副标题,例如节气、节日、农历日 */
|
|
146
|
+
subtitle: string;
|
|
147
|
+
/** 左上角小标记 */
|
|
148
|
+
badge: CalendarBadge | null;
|
|
149
|
+
/** 适配器透传的数据 */
|
|
150
|
+
data: TData | null;
|
|
151
|
+
/** 无障碍名称 */
|
|
152
|
+
ariaLabel: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 自定义头部能拿到的上下文 */
|
|
156
|
+
export interface HeaderRenderContext {
|
|
157
|
+
/** 当前展示的年 */
|
|
158
|
+
year: number;
|
|
159
|
+
/** 当前展示的月,1-12 */
|
|
160
|
+
month: number;
|
|
161
|
+
/** 当前语言包 */
|
|
162
|
+
locale: CalendarLocale;
|
|
163
|
+
/** 切到上个月 */
|
|
164
|
+
prevMonth(): void;
|
|
165
|
+
/** 切到下个月 */
|
|
166
|
+
nextMonth(): void;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 自定义头部渲染函数:返回要放进头部的元素,每次换月都会重新调用 */
|
|
170
|
+
export type HeaderRenderer = (context: HeaderRenderContext) => HTMLElement;
|
|
171
|
+
|
|
172
|
+
/** 创建日历时的配置 */
|
|
173
|
+
export interface CalendarOptions<TData = null> {
|
|
174
|
+
/** 初始日期,默认今天;超出约束时夹到最近的可选日期 */
|
|
175
|
+
initialDate?: CalendarDateInput;
|
|
176
|
+
/** 最小可选日期(包含) */
|
|
177
|
+
minDate?: CalendarDateInput;
|
|
178
|
+
/** 最大可选日期(包含) */
|
|
179
|
+
maxDate?: CalendarDateInput;
|
|
180
|
+
/** 禁止选中的日期,按本地年月日比较 */
|
|
181
|
+
disabledDates?: readonly CalendarDateInput[];
|
|
182
|
+
/** 选择模式,默认 "single" */
|
|
183
|
+
mode?: CalendarMode;
|
|
184
|
+
/** 范围模式下显示的月数,默认 1;设为 2 时显示相邻两个月 */
|
|
185
|
+
months?: 1 | 2;
|
|
186
|
+
/** 范围变化时触发 */
|
|
187
|
+
onRangeChange?: (range: CalendarRange) => void;
|
|
188
|
+
/** 一周从哪天开始,默认 1(周一) */
|
|
189
|
+
weekStartsOn?: WeekStartsOn;
|
|
190
|
+
/** 界面语言:内置 "zh-CN"(默认)/ "en-US",也可直接传自定义语言包 */
|
|
191
|
+
locale?: CalendarLocale | CalendarLocaleName;
|
|
192
|
+
/** 主题,默认 "auto":跟随最近的 data-theme,没有则跟随系统 */
|
|
193
|
+
theme?: CalendarTheme;
|
|
194
|
+
/**
|
|
195
|
+
* 动画配置
|
|
196
|
+
*
|
|
197
|
+
* 不传时翻月直接切换、不显示折叠箭头。传了 `slide` 才开启滑动,
|
|
198
|
+
* 传了 `collapse` 才显示折叠箭头。
|
|
199
|
+
*/
|
|
200
|
+
animation?: CalendarAnimationOptions;
|
|
201
|
+
/** 自定义副标题;优先于 adapter.subtitle,返回 null/undefined 时回退 */
|
|
202
|
+
subtitle?: SubtitleRenderer;
|
|
203
|
+
/** 是否显示头部,默认 true */
|
|
204
|
+
header?: boolean;
|
|
205
|
+
/** 自定义头部,替换默认的 ‹ 标题 › */
|
|
206
|
+
renderHeader?: HeaderRenderer;
|
|
207
|
+
/** 选中某一天时触发 */
|
|
208
|
+
onSelect?: (day: CalendarDay<TData>) => void;
|
|
209
|
+
/** 切换月份时触发 */
|
|
210
|
+
onMonthChange?: (year: number, month: number) => void;
|
|
211
|
+
/** 日历适配器;不传时就是纯公历 */
|
|
212
|
+
adapter?: CalendarAdapter<TData>;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 返回的实例 */
|
|
216
|
+
export interface CalendarInstance {
|
|
217
|
+
/** 日历根元素,方便外部定位或再挂样式 */
|
|
218
|
+
readonly el: HTMLElement;
|
|
219
|
+
/** 跳到某年某月 */
|
|
220
|
+
setMonth(year: number, month: number): void;
|
|
221
|
+
/** 当前展示的年月 */
|
|
222
|
+
getMonth(): YearMonth;
|
|
223
|
+
/** 切到上个月 */
|
|
224
|
+
prevMonth(): void;
|
|
225
|
+
/** 切到下个月 */
|
|
226
|
+
nextMonth(): void;
|
|
227
|
+
/** 选中某个日期 */
|
|
228
|
+
select(date: CalendarDateInput): void;
|
|
229
|
+
/** 当前已选范围;单选模式返回 { start: null, end: null } */
|
|
230
|
+
getRange(): CalendarRange;
|
|
231
|
+
/** 设置范围;start 为空时清空,end 省略表示只选了起点 */
|
|
232
|
+
setRange(start: CalendarDateInput | null, end?: CalendarDateInput | null): void;
|
|
233
|
+
/** 清空范围 */
|
|
234
|
+
clearRange(): void;
|
|
235
|
+
/** 当前选中的日期 */
|
|
236
|
+
getSelected(): Date | null;
|
|
237
|
+
/** 显示整个日历 */
|
|
238
|
+
show(): void;
|
|
239
|
+
/** 隐藏整个日历 */
|
|
240
|
+
hide(): void;
|
|
241
|
+
/** 切换整个日历的显示状态 */
|
|
242
|
+
toggle(): void;
|
|
243
|
+
/** 整个日历当前是否可见 */
|
|
244
|
+
isVisible(): boolean;
|
|
245
|
+
/** 显示头部 */
|
|
246
|
+
showHeader(): void;
|
|
247
|
+
/** 隐藏头部 */
|
|
248
|
+
hideHeader(): void;
|
|
249
|
+
/** 切换头部的显示状态 */
|
|
250
|
+
toggleHeader(): void;
|
|
251
|
+
/** 头部当前是否可见 */
|
|
252
|
+
isHeaderVisible(): boolean;
|
|
253
|
+
/** 当前最小可选日期,未设置时为 null */
|
|
254
|
+
getMinDate(): Date | null;
|
|
255
|
+
/** 设置最小可选日期,传 null 取消 */
|
|
256
|
+
setMinDate(date: CalendarDateInput | null): void;
|
|
257
|
+
/** 当前最大可选日期,未设置时为 null */
|
|
258
|
+
getMaxDate(): Date | null;
|
|
259
|
+
/** 设置最大可选日期,传 null 取消 */
|
|
260
|
+
setMaxDate(date: CalendarDateInput | null): void;
|
|
261
|
+
/** 当前主题 */
|
|
262
|
+
getTheme(): CalendarTheme;
|
|
263
|
+
/** 切换主题,只改 data-theme,不重建 DOM */
|
|
264
|
+
setTheme(theme: CalendarTheme): void;
|
|
265
|
+
/** 移除 DOM 与事件监听 */
|
|
266
|
+
destroy(): void;
|
|
267
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CalendarDay, HeaderRenderer } from "./calendar";
|
|
2
|
+
import type { CalendarLocale } from "./locale";
|
|
3
|
+
|
|
4
|
+
/** 头部左右切换按钮的入参 */
|
|
5
|
+
export interface NavOptions {
|
|
6
|
+
/** 按钮上显示的字符,例如 ‹ › */
|
|
7
|
+
label: string;
|
|
8
|
+
/** 无障碍名称,例如「上个月」 */
|
|
9
|
+
ariaLabel: string;
|
|
10
|
+
onClick: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 头部左右切换按钮的句柄 */
|
|
14
|
+
export interface NavHandle {
|
|
15
|
+
el: HTMLButtonElement;
|
|
16
|
+
/** 设置禁用状态,边界月份时使用 */
|
|
17
|
+
setDisabled(disabled: boolean): void;
|
|
18
|
+
destroy(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 头部的入参 */
|
|
22
|
+
export interface HeaderOptions {
|
|
23
|
+
/** 初始展示的年 */
|
|
24
|
+
year: number;
|
|
25
|
+
/** 初始展示的月 */
|
|
26
|
+
month: number;
|
|
27
|
+
/** 界面文案 */
|
|
28
|
+
locale: CalendarLocale;
|
|
29
|
+
/** 自定义标题文本;不传时使用 locale.monthTitle */
|
|
30
|
+
title?: (year: number, month: number) => string;
|
|
31
|
+
/** 上个月 */
|
|
32
|
+
onPrev: () => void;
|
|
33
|
+
/** 下个月 */
|
|
34
|
+
onNext: () => void;
|
|
35
|
+
/** 自定义渲染函数,提供时替换默认的 ‹ 标题 › */
|
|
36
|
+
renderer?: HeaderRenderer;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 头部的句柄 */
|
|
40
|
+
export interface HeaderHandle {
|
|
41
|
+
el: HTMLElement;
|
|
42
|
+
/** 换月时更新标题或重绘自定义头部 */
|
|
43
|
+
update(year: number, month: number): void;
|
|
44
|
+
/** 更新左右按钮的禁用状态 */
|
|
45
|
+
setNavDisabled(prevDisabled: boolean, nextDisabled: boolean): void;
|
|
46
|
+
destroy(): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 日期网格的入参 */
|
|
50
|
+
export interface GridOptions<TData = null> {
|
|
51
|
+
/** 点中某一格时触发 */
|
|
52
|
+
onSelect: (day: CalendarDay<TData>) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 日期网格的句柄 */
|
|
56
|
+
export interface GridHandle<TData = null> {
|
|
57
|
+
el: HTMLElement;
|
|
58
|
+
/** 换月:更新格子文本与状态(节点复用,不重新创建) */
|
|
59
|
+
render(days: CalendarDay<TData>[]): void;
|
|
60
|
+
/** 只更新选中/今天等状态类,不动文本 */
|
|
61
|
+
updateState(days: CalendarDay<TData>[]): void;
|
|
62
|
+
/**
|
|
63
|
+
* 折叠时只显示指定行;传 null 显示全部 6 行
|
|
64
|
+
*
|
|
65
|
+
* animate 为 false 时直接切换(翻月时使用),避免和位移动画同时触发布局。
|
|
66
|
+
*/
|
|
67
|
+
setVisibleRows(rows: readonly number[] | null, animate?: boolean): void;
|
|
68
|
+
destroy(): void;
|
|
69
|
+
}
|