@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.
@@ -0,0 +1,32 @@
1
+ export type {
2
+ CalendarAdapter,
3
+ CalendarAnimationOptions,
4
+ CalendarBadge,
5
+ CalendarDate,
6
+ CalendarDateInput,
7
+ CalendarDay,
8
+ CalendarInstance,
9
+ CalendarMode,
10
+ CalendarOptions,
11
+ CalendarRange,
12
+ CalendarTheme,
13
+ CollapseAnimationOptions,
14
+ DayDecoration,
15
+ HeaderRenderContext,
16
+ HeaderRenderer,
17
+ MonthInput,
18
+ SlideAnimationOptions,
19
+ SubtitleRenderer,
20
+ WeekStartsOn,
21
+ YearMonth,
22
+ } from "./calendar";
23
+ export type {
24
+ GridHandle,
25
+ GridOptions,
26
+ HeaderHandle,
27
+ HeaderOptions,
28
+ NavHandle,
29
+ NavOptions,
30
+ } from "./components";
31
+ export type { CalendarLocale, CalendarLocaleName } from "./locale";
32
+ export type { BuildDaysOptions } from "./utils";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 语言包
3
+ *
4
+ * 只负责日历自身的界面文字;农历、节气、节日等业务文案由适配器提供。
5
+ */
6
+ export interface CalendarLocale {
7
+ /** 语言标签,例如 "zh-CN",仅用于调试与覆盖 */
8
+ name: string;
9
+ /** 一周七天的短名,下标 0 为周日 */
10
+ weekdays: readonly string[];
11
+ /** 一周七天的全名,下标 0 为周日,用于 title 提示 */
12
+ weekdaysLong: readonly string[];
13
+ /** 头部年月标题 */
14
+ monthTitle(year: number, month: number): string;
15
+ /** 上个月按钮的无障碍名称 */
16
+ prevMonth: string;
17
+ /** 下个月按钮的无障碍名称 */
18
+ nextMonth: string;
19
+ /** 折叠按钮的无障碍名称;不传时使用内置文案 */
20
+ collapse?: string;
21
+ /** 展开按钮的无障碍名称;不传时使用内置文案 */
22
+ expand?: string;
23
+ }
24
+
25
+ /** 内置语言包的名称,可直接传给 CalendarOptions.locale */
26
+ export type CalendarLocaleName = "zh-CN" | "en-US";
@@ -0,0 +1,28 @@
1
+ import type { CalendarAdapter, SubtitleRenderer, WeekStartsOn } from "./calendar";
2
+ import type { CalendarLocale } from "./locale";
3
+
4
+ /** buildDays 的入参 */
5
+ export interface BuildDaysOptions<TData = null> {
6
+ /** 公历年 */
7
+ year: number;
8
+ /** 公历月,1-12 */
9
+ month: number;
10
+ /** 一周从哪天开始 */
11
+ weekStartsOn: WeekStartsOn;
12
+ /** 今天 */
13
+ today: Date;
14
+ /** 当前选中的日期,未选中传 null */
15
+ selected: Date | null;
16
+ /** 范围起点 */
17
+ rangeStart?: Date | null;
18
+ /** 范围终点 */
19
+ rangeEnd?: Date | null;
20
+ /** 当前界面语言 */
21
+ locale: CalendarLocale;
22
+ /** 判断某个日期是否不可选;不传表示全部都可用 */
23
+ isDisabled?: (date: Date) => boolean;
24
+ /** 自定义副标题,优先于 adapter */
25
+ subtitle?: SubtitleRenderer;
26
+ /** 日历适配器;不传时就是纯公历 */
27
+ adapter?: CalendarAdapter<TData>;
28
+ }
@@ -0,0 +1,82 @@
1
+ import type { CalendarDateInput, YearMonth } from "../types";
2
+
3
+ /** 日期字符串:YYYY-M-D,分隔符必须一致 */
4
+ const DATE_INPUT_PATTERN = /^(\d{4})([-/])(\d{1,2})\2(\d{1,2})$/;
5
+
6
+ /** 去掉时分秒,只保留本地年月日 */
7
+ export const toDateOnly = function (date: Date): Date {
8
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
9
+ };
10
+
11
+ /**
12
+ * 解析用户传入的日期
13
+ *
14
+ * 支持 Date,以及 `2026-10-01`、`2026-10-1`、`2026/10/01`、`2026/10/1`。
15
+ * 不交给 `new Date(string)`,避免时区/格式歧义。
16
+ */
17
+ export const parseDateInput = function (value: CalendarDateInput, path = "date"): Date {
18
+ if (value instanceof Date) {
19
+ if (Number.isNaN(value.getTime())) {
20
+ throw new TypeError(`[calendar-ui] ${path} 必须是有效的 Date`);
21
+ }
22
+ return toDateOnly(value);
23
+ }
24
+
25
+ if (typeof value !== "string") {
26
+ throw new TypeError(`[calendar-ui] ${path} 必须是 Date 或 YYYY-MM-DD / YYYY/MM/DD 字符串`);
27
+ }
28
+
29
+ const match = DATE_INPUT_PATTERN.exec(value.trim());
30
+ if (!match) {
31
+ throw new TypeError(
32
+ `[calendar-ui] ${path} 必须是 Date 或 YYYY-MM-DD / YYYY/MM/DD 字符串,收到 ${JSON.stringify(value)}`,
33
+ );
34
+ }
35
+
36
+ const year = Number(match[1]);
37
+ const month = Number(match[3]);
38
+ const day = Number(match[4]);
39
+ if (month < 1 || month > 12) {
40
+ throw new RangeError(`[calendar-ui] ${path} 月份必须在 1-12 之间`);
41
+ }
42
+
43
+ const date = new Date(0);
44
+ date.setHours(0, 0, 0, 0);
45
+ date.setFullYear(year, month - 1, day);
46
+ if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) {
47
+ throw new RangeError(`[calendar-ui] ${path} 不是有效日期: ${value}`);
48
+ }
49
+ return date;
50
+ };
51
+
52
+ /** 把日期转成可比较的数字 key:YYYYMMDD */
53
+ export const dateKey = function (date: Date): number {
54
+ return date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate();
55
+ };
56
+
57
+ /** 加减天数,按本地日历计算 */
58
+ export const addDays = function (date: Date, amount: number): Date {
59
+ const next = toDateOnly(date);
60
+ next.setDate(next.getDate() + amount);
61
+ return next;
62
+ };
63
+
64
+ /** 比较两个日期,只比年月日 */
65
+ export const compareDay = function (a: Date, b: Date): number {
66
+ return dateKey(a) - dateKey(b);
67
+ };
68
+
69
+ /** 同一天(只比年月日,忽略时分秒) */
70
+ export const sameDay = function (a: Date, b: Date): boolean {
71
+ return (
72
+ a.getFullYear() === b.getFullYear() &&
73
+ a.getMonth() === b.getMonth() &&
74
+ a.getDate() === b.getDate()
75
+ );
76
+ };
77
+
78
+ /** 把年月规范成合法值,月份越界时自动进位/退位 */
79
+ export const normalizeMonth = function (year: number, month: number): YearMonth {
80
+ const date = new Date(year, month - 1, 1);
81
+ return { year: date.getFullYear(), month: date.getMonth() + 1 };
82
+ };
@@ -0,0 +1,88 @@
1
+ import type { CalendarDate, CalendarDay, DayDecoration, WeekStartsOn } from "../types";
2
+ import type { BuildDaysOptions } from "../types";
3
+ import { compareDay, sameDay } from "./date";
4
+
5
+ /** 某个年月 1 号是星期几(0 为周日) */
6
+ export const firstWeekdayOf = function (year: number, month: number): number {
7
+ return new Date(year, month - 1, 1).getDay();
8
+ };
9
+
10
+ /** 生成 6 行 × 7 列共 42 个公历日期,包含上下月补位格 */
11
+ const buildMonthDates = function (
12
+ year: number,
13
+ month: number,
14
+ weekStartsOn: WeekStartsOn,
15
+ ): CalendarDate[] {
16
+ const offset = (firstWeekdayOf(year, month) - weekStartsOn + 7) % 7;
17
+ const days: CalendarDate[] = [];
18
+
19
+ for (let i = 0; i < 42; i++) {
20
+ const date = new Date(year, month - 1, i - offset + 1);
21
+ days.push({
22
+ year: date.getFullYear(),
23
+ month: date.getMonth() + 1,
24
+ day: date.getDate(),
25
+ date,
26
+ inCurrentMonth: date.getMonth() + 1 === month && date.getFullYear() === year,
27
+ });
28
+ }
29
+ return days;
30
+ };
31
+
32
+ /** 生成 42 个格子的数据;今天/选中每次现算,适配器只负责装饰 */
33
+ export const buildDays = function <TData = null>(
34
+ options: BuildDaysOptions<TData>,
35
+ ): CalendarDay<TData>[] {
36
+ const dates = buildMonthDates(options.year, options.month, options.weekStartsOn);
37
+ const decorations: ReadonlyArray<DayDecoration<TData> | null> = options.adapter
38
+ ? options.adapter.decorateMonth({
39
+ year: options.year,
40
+ month: options.month,
41
+ weekStartsOn: options.weekStartsOn,
42
+ today: options.today,
43
+ locale: options.locale,
44
+ days: dates,
45
+ })
46
+ : [];
47
+
48
+ return dates.map((date, index) => {
49
+ const decoration = decorations[index] ?? null;
50
+ const badge = decoration?.badge ?? null;
51
+ const adapterSubtitle = decoration?.subtitle ?? "";
52
+ const customSubtitle = options.subtitle?.(date);
53
+ const subtitle =
54
+ customSubtitle === null || customSubtitle === undefined ? adapterSubtitle : customSubtitle;
55
+ const key = `${date.year}-${date.month}-${date.day}`;
56
+ const isDisabled = options.isDisabled?.(date.date) ?? false;
57
+ const rangeStart = options.rangeStart ?? null;
58
+ const rangeEnd = options.rangeEnd ?? null;
59
+ const isRangeStart = rangeStart !== null && sameDay(date.date, rangeStart);
60
+ const isRangeEnd = rangeEnd !== null && sameDay(date.date, rangeEnd);
61
+ const isInRange =
62
+ rangeStart !== null &&
63
+ rangeEnd !== null &&
64
+ compareDay(date.date, rangeStart) >= 0 &&
65
+ compareDay(date.date, rangeEnd) <= 0;
66
+
67
+ return {
68
+ ...date,
69
+ isToday: sameDay(date.date, options.today),
70
+ isSelected:
71
+ (options.selected !== null && sameDay(date.date, options.selected)) ||
72
+ isRangeStart ||
73
+ isRangeEnd,
74
+ isDisabled,
75
+ isRangeStart,
76
+ isRangeEnd,
77
+ isInRange,
78
+ subtitle,
79
+ badge,
80
+ data: decoration?.data ?? null,
81
+ ariaLabel:
82
+ decoration?.ariaLabel ??
83
+ [key, badge?.text ?? "", subtitle, isDisabled ? "不可选" : ""]
84
+ .filter((part) => part !== "")
85
+ .join(" "),
86
+ };
87
+ });
88
+ };
@@ -0,0 +1,11 @@
1
+ /** 简单建节点:文本一律走 textContent,避免节假日名里的特殊字符被当成 HTML */
2
+ export const node = function <K extends keyof HTMLElementTagNameMap>(
3
+ tag: K,
4
+ className?: string,
5
+ text?: string,
6
+ ): HTMLElementTagNameMap[K] {
7
+ const el = document.createElement(tag);
8
+ if (className) el.className = className;
9
+ if (text !== undefined) el.textContent = text;
10
+ return el;
11
+ };
@@ -0,0 +1,39 @@
1
+ /** 是否开启了「减少动态效果」;jsdom 等环境没有 matchMedia 时当作未开启 */
2
+ export const prefersReducedMotion = function (): boolean {
3
+ return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
4
+ };
5
+
6
+ /** 把 CSS 里的时长读成毫秒,支持 `200ms` / `0.2s` / 纯数字;非法值回退 */
7
+ export const parseCssDuration = function (value: string, fallback: number): number {
8
+ const match = /^\s*(-?\d*\.?\d+)\s*(ms|s)?\s*$/i.exec(value);
9
+ if (match === null) return fallback;
10
+
11
+ const amount = Number(match[1]);
12
+ if (!Number.isFinite(amount) || amount < 0) return fallback;
13
+ return match[2]?.toLowerCase() === "s" ? amount * 1000 : amount;
14
+ };
15
+
16
+ /** 当前元素上的 CSS 变量优先,CSS 变量没有声明时用 JS 选项 */
17
+ export const readCssDuration = function (
18
+ style: CSSStyleDeclaration,
19
+ name: string,
20
+ fallback: number,
21
+ ): number {
22
+ const value = style.getPropertyValue(name).trim();
23
+ return value === "" ? fallback : parseCssDuration(value, fallback);
24
+ };
25
+
26
+ export const readCssEasing = function (
27
+ style: CSSStyleDeclaration,
28
+ name: string,
29
+ fallback: string,
30
+ ): string {
31
+ const value = style.getPropertyValue(name).trim();
32
+ return value === "" ? fallback : value;
33
+ };
34
+
35
+ /** WAAPI 的 easing 不认非法值会抛错,这里退回到 ease */
36
+ export const normalizeEasing = function (easing: string): string {
37
+ if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return easing;
38
+ return CSS.supports("animation-timing-function", easing) ? easing : "ease";
39
+ };