@lichta/svelte 2.0.1 → 2.2.0
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 +40 -1
- package/dist/components/Calendar.svelte +35 -320
- package/dist/components/Calendar.svelte.d.ts +9 -9
- package/dist/components/DatePicker.svelte +188 -0
- package/dist/components/DatePicker.svelte.d.ts +20 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
# @lichta/svelte
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@lichta/svelte)
|
|
4
|
+
[](https://www.npmjs.com/package/@lichta/core)
|
|
5
|
+
|
|
6
|
+
🚀 **[Demo trực tiếp](https://lichta.zeneo.app/)**
|
|
4
7
|
|
|
5
8
|
Component `Calendar`/`Formatter` (lịch tháng có ngày âm lịch) cho Svelte 5, đóng gói trên [`@lichta/core`](https://www.npmjs.com/package/@lichta/core).
|
|
6
9
|
|
|
10
|
+
Package này gồm 3 component: `Calendar` (lịch tháng), `Formatter` (hiển thị ngày âm cho 1 ngày cụ thể), `DatePicker` (input + popover chọn ngày, dựng trên `Calendar`).
|
|
11
|
+
|
|
7
12
|
Khác với `@lichta/react`/`@lichta/vue`, package này **re-export toàn bộ API của `@lichta/core`** (`LichTa`, `getYearDetails`, `formatTraditional`, `t`, ...) — bạn không cần cài `@lichta/core` riêng, chỉ cần import thẳng từ `@lichta/svelte`.
|
|
8
13
|
|
|
9
14
|
## Cài đặt
|
|
@@ -58,8 +63,11 @@ Bạn cần cài `svelte` (peer dependency, `^5.0.0`) nếu dự án chưa có.
|
|
|
58
63
|
| `selectedDate` | `Date \| null` | `null` | Ngày được chọn (controlled) |
|
|
59
64
|
| `showLunar` | `boolean` | `true` | Hiện ngày âm lịch dưới ngày dương |
|
|
60
65
|
| `locale` | `'vi' \| 'en' \| 'ja' \| 'ko'` | `'vi'` | Ngôn ngữ nhãn thứ trong tuần |
|
|
66
|
+
| `firstDayOfWeek` | `0 \| 1` | `0` | Ngày bắt đầu tuần: `0` = Chủ Nhật, `1` = Thứ Hai |
|
|
67
|
+
| `showWeekNumber` | `boolean` | `false` | Hiện số tuần (ISO-8601) ở đầu mỗi hàng |
|
|
61
68
|
| `theme` | `'classic' \| 'glass'` | `'classic'` | Giao diện lịch |
|
|
62
69
|
| `onSelect` | `(date: Date, lunar: LunarDate) => void` | — | Callback khi chọn ngày |
|
|
70
|
+
| `onMonthChange` | `(month: number, year: number) => void` | — | Callback khi điều hướng tháng qua nút prev/next trong header |
|
|
63
71
|
| `dayCell` | `Snippet<[DayCellData]>` | — | Custom snippet cho mỗi ô ngày |
|
|
64
72
|
| `children` | `Snippet` | — | Nội dung footer tùy chỉnh |
|
|
65
73
|
|
|
@@ -70,6 +78,37 @@ Bạn cần cài `svelte` (peer dependency, `^5.0.0`) nếu dự án chưa có.
|
|
|
70
78
|
| `date` | `Date` | `new Date()` | Ngày dương lịch cần hiển thị |
|
|
71
79
|
| `children` | `Snippet` | — | Nội dung tùy chỉnh (render prop) |
|
|
72
80
|
|
|
81
|
+
### `DatePicker`
|
|
82
|
+
|
|
83
|
+
```svelte
|
|
84
|
+
<script>
|
|
85
|
+
import { DatePicker } from '@lichta/svelte';
|
|
86
|
+
import '@lichta/core/styles/calendar-base.css';
|
|
87
|
+
import '@lichta/core/styles/calendar-glass.css'; // theme "glass" (tùy chọn)
|
|
88
|
+
import '@lichta/core/styles/datepicker-base.css';
|
|
89
|
+
import '@lichta/core/styles/datepicker-glass.css'; // theme "glass" (tùy chọn)
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<DatePicker theme="glass" locale="vi" onSelect={(date, lunar) => console.log(date, lunar)} />
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> ⚠️ Cần import cả CSS của `Calendar` (dùng trong popover) lẫn `DatePicker` (input + popover chrome) — thiếu 1 trong 2 sẽ hiển thị thiếu style.
|
|
96
|
+
|
|
97
|
+
| Prop | Kiểu | Mặc định | Mô tả |
|
|
98
|
+
| ----------- | ----------------------------------------- | -------------- | ----------------------------------------------------------------------- |
|
|
99
|
+
| `value` | `Date \| null` | `null` | Ngày đang chọn (uncontrolled — component tự quản lý sau lần mount đầu) |
|
|
100
|
+
| `onSelect` | `(date: Date, lunar: LunarDate) => void` | — | Callback khi chọn ngày trong popover |
|
|
101
|
+
| `onMonthChange` | `(month: number, year: number) => void` | — | Callback khi điều hướng tháng qua nút prev/next trong popover |
|
|
102
|
+
| `placeholder` | `string` | `'Chọn ngày'` | Placeholder khi chưa chọn ngày |
|
|
103
|
+
| `locale` | `'vi' \| 'en' \| 'ja' \| 'ko'` | `'vi'` | Ngôn ngữ hiển thị trong popover |
|
|
104
|
+
| `firstDayOfWeek` | `0 \| 1` | `0` | Ngày bắt đầu tuần: `0` = Chủ Nhật, `1` = Thứ Hai |
|
|
105
|
+
| `theme` | `'classic' \| 'glass'` | `'classic'` | Giao diện input + popover |
|
|
106
|
+
| `showLunar` | `boolean` | `true` | Hiện ngày âm lịch trong popover |
|
|
107
|
+
| `format` | `(date: Date) => string` | `dd/MM/yyyy` | Hàm format ngày hiển thị trên input |
|
|
108
|
+
| `disabled` | `boolean` | `false` | Vô hiệu hoá input |
|
|
109
|
+
|
|
110
|
+
Popover tự đóng khi: chọn xong 1 ngày, click ra ngoài, hoặc nhấn phím `Escape`.
|
|
111
|
+
|
|
73
112
|
## Dùng logic thuần (không cần component)
|
|
74
113
|
|
|
75
114
|
Vì `@lichta/svelte` re-export toàn bộ `@lichta/core`, bạn có thể import thẳng logic tính toán từ đây thay vì cài thêm `@lichta/core`:
|
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import type { LunarDate } from '@lichta/core';
|
|
2
|
+
import { getYearDetails, getCalendarGrid, getWeekDayLabels, t } from '@lichta/core';
|
|
3
|
+
import type { LunarDate, CalendarDayCell } from '@lichta/core';
|
|
4
4
|
import type { Snippet } from 'svelte';
|
|
5
5
|
import { untrack } from 'svelte';
|
|
6
6
|
|
|
7
|
-
import type { Locale } from '@lichta/core';
|
|
7
|
+
import type { Locale, FirstDayOfWeek } from '@lichta/core';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Dữ liệu cho mỗi ô ngày trong lưới lịch
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
solar: Date;
|
|
14
|
-
lunar: LunarDate;
|
|
15
|
-
isToday: boolean;
|
|
16
|
-
isSelected: boolean;
|
|
17
|
-
isCurrentMonth: boolean;
|
|
18
|
-
}
|
|
12
|
+
type DayCellData = CalendarDayCell;
|
|
19
13
|
|
|
20
14
|
interface Props {
|
|
21
15
|
/** Tháng hiển thị (1-12). Mặc định: tháng hiện tại */
|
|
@@ -26,10 +20,16 @@
|
|
|
26
20
|
selectedDate?: Date | null;
|
|
27
21
|
/** Callback khi chọn ngày */
|
|
28
22
|
onSelect?: (date: Date, lunar: LunarDate) => void;
|
|
23
|
+
/** Callback khi user điều hướng tháng qua nút prev/next trong header */
|
|
24
|
+
onMonthChange?: (month: number, year: number) => void;
|
|
29
25
|
/** Hiển thị ngày âm lịch bên dưới ngày dương */
|
|
30
26
|
showLunar?: boolean;
|
|
31
27
|
/** Locale (vi | en | ja | ko) */
|
|
32
28
|
locale?: Locale;
|
|
29
|
+
/** Ngày bắt đầu tuần: 0 = Chủ Nhật (mặc định), 1 = Thứ Hai */
|
|
30
|
+
firstDayOfWeek?: FirstDayOfWeek;
|
|
31
|
+
/** Hiển thị số tuần (ISO-8601) ở đầu mỗi hàng */
|
|
32
|
+
showWeekNumber?: boolean;
|
|
33
33
|
/** Custom snippet cho mỗi ô ngày */
|
|
34
34
|
dayCell?: Snippet<[DayCellData]>;
|
|
35
35
|
children?: Snippet;
|
|
@@ -42,8 +42,11 @@
|
|
|
42
42
|
year = new Date().getFullYear(),
|
|
43
43
|
selectedDate = null,
|
|
44
44
|
onSelect,
|
|
45
|
+
onMonthChange,
|
|
45
46
|
showLunar = true,
|
|
46
47
|
locale = 'vi',
|
|
48
|
+
firstDayOfWeek = 0,
|
|
49
|
+
showWeekNumber = false,
|
|
47
50
|
dayCell,
|
|
48
51
|
children,
|
|
49
52
|
theme = 'classic'
|
|
@@ -66,127 +69,32 @@
|
|
|
66
69
|
});
|
|
67
70
|
|
|
68
71
|
// Tên thứ trong tuần
|
|
69
|
-
const weekDayLabels = $derived(
|
|
70
|
-
locale === 'vi'
|
|
71
|
-
? ['CN', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7']
|
|
72
|
-
: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
73
|
-
);
|
|
72
|
+
const weekDayLabels = $derived(getWeekDayLabels(locale, firstDayOfWeek));
|
|
74
73
|
|
|
75
|
-
// Tên tháng
|
|
76
|
-
const monthNames =
|
|
77
|
-
'Tháng 1',
|
|
78
|
-
'Tháng 2',
|
|
79
|
-
'Tháng 3',
|
|
80
|
-
'Tháng 4',
|
|
81
|
-
'Tháng 5',
|
|
82
|
-
'Tháng 6',
|
|
83
|
-
'Tháng 7',
|
|
84
|
-
'Tháng 8',
|
|
85
|
-
'Tháng 9',
|
|
86
|
-
'Tháng 10',
|
|
87
|
-
'Tháng 11',
|
|
88
|
-
'Tháng 12'
|
|
89
|
-
];
|
|
74
|
+
// Tên tháng dương lịch cho phần header điều hướng
|
|
75
|
+
const monthNames = $derived(t(locale).solarMonthNames);
|
|
90
76
|
|
|
91
77
|
// Can Chi năm hiển thị
|
|
92
78
|
let yearInfo = $derived(getYearDetails(currentYear));
|
|
93
79
|
|
|
94
|
-
// Ngày hôm nay
|
|
95
|
-
const today = new Date();
|
|
96
|
-
const todayStr = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`;
|
|
97
|
-
|
|
98
80
|
// Tính grid lịch 6x7 = 42 ô
|
|
99
|
-
let calendarGrid = $derived
|
|
100
|
-
const grid: DayCellData[] = [];
|
|
101
|
-
|
|
102
|
-
// Ngày đầu tiên của tháng
|
|
103
|
-
const firstDay = new Date(currentYear, currentMonth - 1, 1);
|
|
104
|
-
// Thứ của ngày đầu tiên (0=CN, 1=T2, ...)
|
|
105
|
-
const startDayOfWeek = firstDay.getDay();
|
|
106
|
-
// Số ngày trong tháng
|
|
107
|
-
const daysInMonth = new Date(currentYear, currentMonth, 0).getDate();
|
|
108
|
-
|
|
109
|
-
// Padding ngày tháng trước
|
|
110
|
-
const prevMonthDays = new Date(currentYear, currentMonth - 1, 0).getDate();
|
|
111
|
-
for (let i = startDayOfWeek - 1; i >= 0; i--) {
|
|
112
|
-
const d = prevMonthDays - i;
|
|
113
|
-
const m = currentMonth - 1 <= 0 ? 12 : currentMonth - 1;
|
|
114
|
-
const y = currentMonth - 1 <= 0 ? currentYear - 1 : currentYear;
|
|
115
|
-
const solar = new Date(y, m - 1, d);
|
|
116
|
-
const lunar = LichTa.toLunar(d, m, y);
|
|
117
|
-
const dateStr = `${y}-${m}-${d}`;
|
|
118
|
-
const selStr = selectedDate
|
|
119
|
-
? `${selectedDate.getFullYear()}-${selectedDate.getMonth() + 1}-${selectedDate.getDate()}`
|
|
120
|
-
: '';
|
|
121
|
-
|
|
122
|
-
grid.push({
|
|
123
|
-
solar,
|
|
124
|
-
lunar,
|
|
125
|
-
isToday: dateStr === todayStr,
|
|
126
|
-
isSelected: dateStr === selStr,
|
|
127
|
-
isCurrentMonth: false
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Ngày trong tháng hiện tại
|
|
132
|
-
for (let d = 1; d <= daysInMonth; d++) {
|
|
133
|
-
const solar = new Date(currentYear, currentMonth - 1, d);
|
|
134
|
-
const lunar = LichTa.toLunar(d, currentMonth, currentYear);
|
|
135
|
-
const dateStr = `${currentYear}-${currentMonth}-${d}`;
|
|
136
|
-
const selStr = selectedDate
|
|
137
|
-
? `${selectedDate.getFullYear()}-${selectedDate.getMonth() + 1}-${selectedDate.getDate()}`
|
|
138
|
-
: '';
|
|
139
|
-
|
|
140
|
-
grid.push({
|
|
141
|
-
solar,
|
|
142
|
-
lunar,
|
|
143
|
-
isToday: dateStr === todayStr,
|
|
144
|
-
isSelected: dateStr === selStr,
|
|
145
|
-
isCurrentMonth: true
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Padding ngày tháng sau
|
|
150
|
-
const remaining = 42 - grid.length;
|
|
151
|
-
for (let d = 1; d <= remaining; d++) {
|
|
152
|
-
const m = currentMonth + 1 > 12 ? 1 : currentMonth + 1;
|
|
153
|
-
const y = currentMonth + 1 > 12 ? currentYear + 1 : currentYear;
|
|
154
|
-
const solar = new Date(y, m - 1, d);
|
|
155
|
-
const lunar = LichTa.toLunar(d, m, y);
|
|
156
|
-
const dateStr = `${y}-${m}-${d}`;
|
|
157
|
-
const selStr = selectedDate
|
|
158
|
-
? `${selectedDate.getFullYear()}-${selectedDate.getMonth() + 1}-${selectedDate.getDate()}`
|
|
159
|
-
: '';
|
|
160
|
-
|
|
161
|
-
grid.push({
|
|
162
|
-
solar,
|
|
163
|
-
lunar,
|
|
164
|
-
isToday: dateStr === todayStr,
|
|
165
|
-
isSelected: dateStr === selStr,
|
|
166
|
-
isCurrentMonth: false
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return grid;
|
|
171
|
-
});
|
|
81
|
+
let calendarGrid = $derived(getCalendarGrid(currentMonth, currentYear, selectedDate, firstDayOfWeek));
|
|
172
82
|
|
|
173
83
|
// Navigation
|
|
174
84
|
function prevMonth() {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
85
|
+
const newMonth = currentMonth === 1 ? 12 : currentMonth - 1;
|
|
86
|
+
const newYear = currentMonth === 1 ? currentYear - 1 : currentYear;
|
|
87
|
+
currentMonth = newMonth;
|
|
88
|
+
currentYear = newYear;
|
|
89
|
+
onMonthChange?.(newMonth, newYear);
|
|
181
90
|
}
|
|
182
91
|
|
|
183
92
|
function nextMonth() {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
93
|
+
const newMonth = currentMonth === 12 ? 1 : currentMonth + 1;
|
|
94
|
+
const newYear = currentMonth === 12 ? currentYear + 1 : currentYear;
|
|
95
|
+
currentMonth = newMonth;
|
|
96
|
+
currentYear = newYear;
|
|
97
|
+
onMonthChange?.(newMonth, newYear);
|
|
190
98
|
}
|
|
191
99
|
|
|
192
100
|
function handleDayClick(cell: DayCellData) {
|
|
@@ -201,7 +109,7 @@
|
|
|
201
109
|
}
|
|
202
110
|
</script>
|
|
203
111
|
|
|
204
|
-
<div class={['lich-ta-calendar', `lichta-theme-${theme}
|
|
112
|
+
<div class={['lich-ta-calendar', `lichta-theme-${theme}`, showWeekNumber ? 'lich-ta-calendar--with-week-number' : ''].filter(Boolean).join(' ')}>
|
|
205
113
|
<!-- Header: Navigation + Tháng/Năm -->
|
|
206
114
|
<div class="lich-ta-calendar-header">
|
|
207
115
|
<button class="lich-ta-calendar-nav" onclick={prevMonth} aria-label="Tháng trước">◀</button>
|
|
@@ -219,6 +127,9 @@
|
|
|
219
127
|
|
|
220
128
|
<!-- Tên thứ -->
|
|
221
129
|
<div class="lich-ta-calendar-weekdays">
|
|
130
|
+
{#if showWeekNumber}
|
|
131
|
+
<div class="lich-ta-calendar-week-number" aria-hidden="true"></div>
|
|
132
|
+
{/if}
|
|
222
133
|
{#each weekDayLabels as label (label)}
|
|
223
134
|
<div class="lich-ta-calendar-weekday">{label}</div>
|
|
224
135
|
{/each}
|
|
@@ -226,7 +137,10 @@
|
|
|
226
137
|
|
|
227
138
|
<!-- Grid ngày -->
|
|
228
139
|
<div class="lich-ta-calendar-grid">
|
|
229
|
-
{#each calendarGrid as cell (cell.solar.getTime())}
|
|
140
|
+
{#each calendarGrid as cell, idx (cell.solar.getTime())}
|
|
141
|
+
{#if showWeekNumber && idx % 7 === 0}
|
|
142
|
+
<div class="lich-ta-calendar-week-number">{cell.weekNumber}</div>
|
|
143
|
+
{/if}
|
|
230
144
|
{#if dayCell}
|
|
231
145
|
{@render dayCell(cell)}
|
|
232
146
|
{:else}
|
|
@@ -262,202 +176,3 @@
|
|
|
262
176
|
</div>
|
|
263
177
|
{/if}
|
|
264
178
|
</div>
|
|
265
|
-
|
|
266
|
-
<style>
|
|
267
|
-
.lich-ta-calendar {
|
|
268
|
-
--lichta-primary: #d4a373;
|
|
269
|
-
--lichta-primary-light: #f5e6d3;
|
|
270
|
-
--lichta-bg: #fffcf7;
|
|
271
|
-
--lichta-surface: #ffffff;
|
|
272
|
-
--lichta-text: #2c1810;
|
|
273
|
-
--lichta-text-muted: #8b7355;
|
|
274
|
-
--lichta-text-dim: #c4b5a0;
|
|
275
|
-
--lichta-border: #e8ddd0;
|
|
276
|
-
--lichta-today-bg: #d4a373;
|
|
277
|
-
--lichta-today-text: #ffffff;
|
|
278
|
-
--lichta-selected-bg: #a0522d;
|
|
279
|
-
--lichta-selected-text: #ffffff;
|
|
280
|
-
--lichta-hover-bg: #f5e6d3;
|
|
281
|
-
--lichta-lunar-text: #b08968;
|
|
282
|
-
--lichta-first-lunar: #c2185b;
|
|
283
|
-
--lichta-radius: 8px;
|
|
284
|
-
--lichta-font: inherit;
|
|
285
|
-
|
|
286
|
-
font-family: var(--lichta-font);
|
|
287
|
-
background: var(--lichta-bg);
|
|
288
|
-
border: 1px solid var(--lichta-border);
|
|
289
|
-
border-radius: calc(var(--lichta-radius) * 2);
|
|
290
|
-
padding: 16px;
|
|
291
|
-
max-width: 420px;
|
|
292
|
-
width: 100%;
|
|
293
|
-
box-sizing: border-box;
|
|
294
|
-
user-select: none;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/* Header */
|
|
298
|
-
.lich-ta-calendar-header {
|
|
299
|
-
display: flex;
|
|
300
|
-
align-items: center;
|
|
301
|
-
justify-content: space-between;
|
|
302
|
-
margin-bottom: 16px;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
.lich-ta-calendar-nav {
|
|
306
|
-
background: none;
|
|
307
|
-
border: 1px solid var(--lichta-border);
|
|
308
|
-
border-radius: var(--lichta-radius);
|
|
309
|
-
width: 36px;
|
|
310
|
-
height: 36px;
|
|
311
|
-
display: flex;
|
|
312
|
-
align-items: center;
|
|
313
|
-
justify-content: center;
|
|
314
|
-
cursor: pointer;
|
|
315
|
-
color: var(--lichta-text);
|
|
316
|
-
font-size: 0.85rem;
|
|
317
|
-
transition: all 0.15s ease;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
.lich-ta-calendar-nav:hover {
|
|
321
|
-
background: var(--lichta-hover-bg);
|
|
322
|
-
border-color: var(--lichta-primary);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
.lich-ta-calendar-title {
|
|
326
|
-
text-align: center;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
.lich-ta-calendar-month-year {
|
|
330
|
-
display: block;
|
|
331
|
-
font-size: 1.1rem;
|
|
332
|
-
font-weight: 600;
|
|
333
|
-
color: var(--lichta-text);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
.lich-ta-calendar-canchi {
|
|
337
|
-
display: block;
|
|
338
|
-
font-size: 0.8rem;
|
|
339
|
-
color: var(--lichta-text-muted);
|
|
340
|
-
margin-top: 2px;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/* Weekdays */
|
|
344
|
-
.lich-ta-calendar-weekdays {
|
|
345
|
-
display: grid;
|
|
346
|
-
grid-template-columns: repeat(7, 1fr);
|
|
347
|
-
margin-bottom: 4px;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
.lich-ta-calendar-weekday {
|
|
351
|
-
text-align: center;
|
|
352
|
-
font-size: 0.75rem;
|
|
353
|
-
font-weight: 600;
|
|
354
|
-
color: var(--lichta-text-muted);
|
|
355
|
-
padding: 6px 0;
|
|
356
|
-
text-transform: uppercase;
|
|
357
|
-
letter-spacing: 0.05em;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/* Grid */
|
|
361
|
-
.lich-ta-calendar-grid {
|
|
362
|
-
display: grid;
|
|
363
|
-
grid-template-columns: repeat(7, 1fr);
|
|
364
|
-
gap: 2px;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
.lich-ta-calendar-day {
|
|
368
|
-
display: flex;
|
|
369
|
-
flex-direction: column;
|
|
370
|
-
align-items: center;
|
|
371
|
-
justify-content: center;
|
|
372
|
-
padding: 6px 2px;
|
|
373
|
-
min-height: 48px;
|
|
374
|
-
border: none;
|
|
375
|
-
border-radius: var(--lichta-radius);
|
|
376
|
-
background: transparent;
|
|
377
|
-
cursor: pointer;
|
|
378
|
-
transition: all 0.15s ease;
|
|
379
|
-
font-family: inherit;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
.lich-ta-calendar-day:hover {
|
|
383
|
-
background: var(--lichta-hover-bg);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
.lich-ta-calendar-day:focus-visible {
|
|
387
|
-
outline: 2px solid var(--lichta-primary);
|
|
388
|
-
outline-offset: 1px;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
.solar-day {
|
|
392
|
-
font-size: 0.95rem;
|
|
393
|
-
font-weight: 500;
|
|
394
|
-
color: var(--lichta-text);
|
|
395
|
-
line-height: 1.2;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
.lunar-day {
|
|
399
|
-
font-size: 0.65rem;
|
|
400
|
-
color: var(--lichta-lunar-text);
|
|
401
|
-
line-height: 1.2;
|
|
402
|
-
margin-top: 1px;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/* States */
|
|
406
|
-
.is-other-month .solar-day {
|
|
407
|
-
color: var(--lichta-text-dim);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
.is-other-month .lunar-day {
|
|
411
|
-
color: var(--lichta-text-dim);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
.is-today {
|
|
415
|
-
background: var(--lichta-today-bg);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
.is-today .solar-day {
|
|
419
|
-
color: var(--lichta-today-text);
|
|
420
|
-
font-weight: 700;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
.is-today .lunar-day {
|
|
424
|
-
color: var(--lichta-today-text);
|
|
425
|
-
opacity: 0.85;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
.is-today:hover {
|
|
429
|
-
background: var(--lichta-today-bg);
|
|
430
|
-
opacity: 0.9;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
.is-selected {
|
|
434
|
-
background: var(--lichta-selected-bg);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
.is-selected .solar-day {
|
|
438
|
-
color: var(--lichta-selected-text);
|
|
439
|
-
font-weight: 700;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
.is-selected .lunar-day {
|
|
443
|
-
color: var(--lichta-selected-text);
|
|
444
|
-
opacity: 0.85;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
.is-first-lunar .lunar-day {
|
|
448
|
-
color: var(--lichta-first-lunar);
|
|
449
|
-
font-weight: 600;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
.is-today.is-first-lunar .lunar-day,
|
|
453
|
-
.is-selected.is-first-lunar .lunar-day {
|
|
454
|
-
color: var(--lichta-today-text);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/* Footer */
|
|
458
|
-
.lich-ta-calendar-footer {
|
|
459
|
-
margin-top: 12px;
|
|
460
|
-
padding-top: 12px;
|
|
461
|
-
border-top: 1px solid var(--lichta-border);
|
|
462
|
-
}
|
|
463
|
-
</style>
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import type { LunarDate } from '@lichta/core';
|
|
1
|
+
import type { LunarDate, CalendarDayCell } from '@lichta/core';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import type { Locale } from '@lichta/core';
|
|
3
|
+
import type { Locale, FirstDayOfWeek } from '@lichta/core';
|
|
4
4
|
/**
|
|
5
5
|
* Dữ liệu cho mỗi ô ngày trong lưới lịch
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
solar: Date;
|
|
9
|
-
lunar: LunarDate;
|
|
10
|
-
isToday: boolean;
|
|
11
|
-
isSelected: boolean;
|
|
12
|
-
isCurrentMonth: boolean;
|
|
13
|
-
}
|
|
7
|
+
type DayCellData = CalendarDayCell;
|
|
14
8
|
interface Props {
|
|
15
9
|
/** Tháng hiển thị (1-12). Mặc định: tháng hiện tại */
|
|
16
10
|
month?: number;
|
|
@@ -20,10 +14,16 @@ interface Props {
|
|
|
20
14
|
selectedDate?: Date | null;
|
|
21
15
|
/** Callback khi chọn ngày */
|
|
22
16
|
onSelect?: (date: Date, lunar: LunarDate) => void;
|
|
17
|
+
/** Callback khi user điều hướng tháng qua nút prev/next trong header */
|
|
18
|
+
onMonthChange?: (month: number, year: number) => void;
|
|
23
19
|
/** Hiển thị ngày âm lịch bên dưới ngày dương */
|
|
24
20
|
showLunar?: boolean;
|
|
25
21
|
/** Locale (vi | en | ja | ko) */
|
|
26
22
|
locale?: Locale;
|
|
23
|
+
/** Ngày bắt đầu tuần: 0 = Chủ Nhật (mặc định), 1 = Thứ Hai */
|
|
24
|
+
firstDayOfWeek?: FirstDayOfWeek;
|
|
25
|
+
/** Hiển thị số tuần (ISO-8601) ở đầu mỗi hàng */
|
|
26
|
+
showWeekNumber?: boolean;
|
|
27
27
|
/** Custom snippet cho mỗi ô ngày */
|
|
28
28
|
dayCell?: Snippet<[DayCellData]>;
|
|
29
29
|
children?: Snippet;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getYearDetails, getCalendarGrid, getWeekDayLabels, t } from '@lichta/core';
|
|
3
|
+
import type { LunarDate, Locale, CalendarDayCell, FirstDayOfWeek } from '@lichta/core';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Ngày đang chọn (uncontrolled — component tự quản lý sau lần mount đầu) */
|
|
7
|
+
value?: Date | null;
|
|
8
|
+
/** Gọi khi người dùng chọn 1 ngày trong popover */
|
|
9
|
+
onSelect?: (date: Date, lunar: LunarDate) => void;
|
|
10
|
+
/** Callback khi user điều hướng tháng qua nút prev/next trong popover */
|
|
11
|
+
onMonthChange?: (month: number, year: number) => void;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
locale?: Locale;
|
|
14
|
+
/** Ngày bắt đầu tuần: 0 = Chủ Nhật (mặc định), 1 = Thứ Hai */
|
|
15
|
+
firstDayOfWeek?: FirstDayOfWeek;
|
|
16
|
+
theme?: 'classic' | 'glass';
|
|
17
|
+
showLunar?: boolean;
|
|
18
|
+
format?: (date: Date) => string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function defaultFormat(date: Date): string {
|
|
23
|
+
const dd = String(date.getDate()).padStart(2, '0');
|
|
24
|
+
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
25
|
+
return `${dd}/${mm}/${date.getFullYear()}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
value = null,
|
|
30
|
+
onSelect,
|
|
31
|
+
onMonthChange,
|
|
32
|
+
placeholder = 'Chọn ngày',
|
|
33
|
+
locale = 'vi',
|
|
34
|
+
firstDayOfWeek = 0,
|
|
35
|
+
theme = 'classic',
|
|
36
|
+
showLunar = true,
|
|
37
|
+
format = defaultFormat,
|
|
38
|
+
disabled = false
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
// Writable $derived (Svelte 5): mặc định bám theo prop `value`, nhưng có thể
|
|
42
|
+
// gán lại trực tiếp (xem handleDaySelect) — giá trị gán tay giữ nguyên cho tới
|
|
43
|
+
// khi `value` đổi thì tính lại từ đầu.
|
|
44
|
+
let selected = $derived<Date | null>(value ?? null);
|
|
45
|
+
let isOpen = $state(false);
|
|
46
|
+
let rootEl: HTMLElement | undefined = $state();
|
|
47
|
+
|
|
48
|
+
// State điều hướng tháng/năm hiển thị trong popover, tách khỏi `selected` để
|
|
49
|
+
// prevMonth()/nextMonth() điều hướng được độc lập (không phụ thuộc ngày đã chọn).
|
|
50
|
+
const initialAnchor = value ?? new Date();
|
|
51
|
+
let viewMonth = $state(initialAnchor.getMonth() + 1);
|
|
52
|
+
let viewYear = $state(initialAnchor.getFullYear());
|
|
53
|
+
|
|
54
|
+
// Đồng bộ lại view khi component cha đổi prop `value` từ bên ngoài.
|
|
55
|
+
$effect(() => {
|
|
56
|
+
if (value) {
|
|
57
|
+
viewMonth = value.getMonth() + 1;
|
|
58
|
+
viewYear = value.getFullYear();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const weekDayLabels = $derived(getWeekDayLabels(locale, firstDayOfWeek));
|
|
63
|
+
const monthNames = $derived(t(locale).solarMonthNames);
|
|
64
|
+
let yearInfo = $derived(getYearDetails(viewYear));
|
|
65
|
+
let grid = $derived(getCalendarGrid(viewMonth, viewYear, selected, firstDayOfWeek));
|
|
66
|
+
const displayValue = $derived(selected ? format(selected) : '');
|
|
67
|
+
|
|
68
|
+
function toggleOpen() {
|
|
69
|
+
if (!disabled) isOpen = !isOpen;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function prevMonth() {
|
|
73
|
+
const newMonth = viewMonth === 1 ? 12 : viewMonth - 1;
|
|
74
|
+
const newYear = viewMonth === 1 ? viewYear - 1 : viewYear;
|
|
75
|
+
viewMonth = newMonth;
|
|
76
|
+
viewYear = newYear;
|
|
77
|
+
onMonthChange?.(newMonth, newYear);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function nextMonth() {
|
|
81
|
+
const newMonth = viewMonth === 12 ? 1 : viewMonth + 1;
|
|
82
|
+
const newYear = viewMonth === 12 ? viewYear + 1 : viewYear;
|
|
83
|
+
viewMonth = newMonth;
|
|
84
|
+
viewYear = newYear;
|
|
85
|
+
onMonthChange?.(newMonth, newYear);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function handleDaySelect(cell: CalendarDayCell) {
|
|
89
|
+
selected = cell.solar;
|
|
90
|
+
isOpen = false;
|
|
91
|
+
onSelect?.(cell.solar, cell.lunar);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handlePointerDown(event: MouseEvent) {
|
|
95
|
+
if (rootEl && !rootEl.contains(event.target as Node)) {
|
|
96
|
+
isOpen = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
101
|
+
if (event.key === 'Escape') isOpen = false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
$effect(() => {
|
|
105
|
+
if (!isOpen) return;
|
|
106
|
+
document.addEventListener('mousedown', handlePointerDown);
|
|
107
|
+
document.addEventListener('keydown', handleKeydown);
|
|
108
|
+
return () => {
|
|
109
|
+
document.removeEventListener('mousedown', handlePointerDown);
|
|
110
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<div bind:this={rootEl} class={['lich-ta-datepicker', `lichta-theme-${theme}`].join(' ')}>
|
|
116
|
+
<input
|
|
117
|
+
type="text"
|
|
118
|
+
class="lich-ta-datepicker-input"
|
|
119
|
+
readonly
|
|
120
|
+
{disabled}
|
|
121
|
+
{placeholder}
|
|
122
|
+
value={displayValue}
|
|
123
|
+
aria-haspopup="dialog"
|
|
124
|
+
aria-expanded={isOpen}
|
|
125
|
+
onclick={toggleOpen}
|
|
126
|
+
/>
|
|
127
|
+
{#if isOpen}
|
|
128
|
+
<div class="lich-ta-datepicker-popover" role="dialog">
|
|
129
|
+
<div class="lich-ta-datepicker-calendar">
|
|
130
|
+
<div class="lich-ta-datepicker-calendar-header">
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
class="lich-ta-datepicker-calendar-nav"
|
|
134
|
+
onclick={prevMonth}
|
|
135
|
+
aria-label="Tháng trước">◀</button
|
|
136
|
+
>
|
|
137
|
+
<div class="lich-ta-datepicker-calendar-title">
|
|
138
|
+
<span class="lich-ta-datepicker-calendar-month-year">
|
|
139
|
+
{monthNames[viewMonth - 1]}, {viewYear}
|
|
140
|
+
</span>
|
|
141
|
+
<span class="lich-ta-datepicker-calendar-canchi">
|
|
142
|
+
{yearInfo.can}
|
|
143
|
+
{yearInfo.chi}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
class="lich-ta-datepicker-calendar-nav"
|
|
149
|
+
onclick={nextMonth}
|
|
150
|
+
aria-label="Tháng sau">▶</button
|
|
151
|
+
>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="lich-ta-datepicker-calendar-weekdays">
|
|
155
|
+
{#each weekDayLabels as label (label)}
|
|
156
|
+
<div class="lich-ta-datepicker-calendar-weekday">{label}</div>
|
|
157
|
+
{/each}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="lich-ta-datepicker-calendar-grid">
|
|
161
|
+
{#each grid as cell (cell.solar.getTime())}
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
class="lich-ta-datepicker-calendar-day"
|
|
165
|
+
class:is-today={cell.isToday}
|
|
166
|
+
class:is-selected={cell.isSelected}
|
|
167
|
+
class:is-other-month={!cell.isCurrentMonth}
|
|
168
|
+
class:is-first-lunar={cell.lunar.day === 1}
|
|
169
|
+
tabindex={cell.isCurrentMonth ? 0 : -1}
|
|
170
|
+
onclick={() => handleDaySelect(cell)}
|
|
171
|
+
>
|
|
172
|
+
<span class="solar-day">{cell.solar.getDate()}</span>
|
|
173
|
+
{#if showLunar}
|
|
174
|
+
<span class="lunar-day">
|
|
175
|
+
{#if cell.lunar.day === 1}
|
|
176
|
+
{cell.lunar.day}/{cell.lunar.month}{cell.lunar.isLeap ? '*' : ''}
|
|
177
|
+
{:else}
|
|
178
|
+
{cell.lunar.day}
|
|
179
|
+
{/if}
|
|
180
|
+
</span>
|
|
181
|
+
{/if}
|
|
182
|
+
</button>
|
|
183
|
+
{/each}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
{/if}
|
|
188
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { LunarDate, Locale, FirstDayOfWeek } from '@lichta/core';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Ngày đang chọn (uncontrolled — component tự quản lý sau lần mount đầu) */
|
|
4
|
+
value?: Date | null;
|
|
5
|
+
/** Gọi khi người dùng chọn 1 ngày trong popover */
|
|
6
|
+
onSelect?: (date: Date, lunar: LunarDate) => void;
|
|
7
|
+
/** Callback khi user điều hướng tháng qua nút prev/next trong popover */
|
|
8
|
+
onMonthChange?: (month: number, year: number) => void;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
locale?: Locale;
|
|
11
|
+
/** Ngày bắt đầu tuần: 0 = Chủ Nhật (mặc định), 1 = Thứ Hai */
|
|
12
|
+
firstDayOfWeek?: FirstDayOfWeek;
|
|
13
|
+
theme?: 'classic' | 'glass';
|
|
14
|
+
showLunar?: boolean;
|
|
15
|
+
format?: (date: Date) => string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
declare const DatePicker: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type DatePicker = ReturnType<typeof DatePicker>;
|
|
20
|
+
export default DatePicker;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { LichTa, jdFromDate, jdToDate, getYearDetails, getDayCanChi, getMonthCanChi, getHourCanChi, getAuspiciousHours, formatLunarDate, formatTraditional, getMonthName, getDayName, t, getZodiacAnimal } from '@lichta/core';
|
|
2
|
-
export type { LunarDate, SolarDate, Locale } from '@lichta/core';
|
|
1
|
+
export { LichTa, jdFromDate, jdToDate, getYearDetails, getDayCanChi, getMonthCanChi, getHourCanChi, getAuspiciousHours, formatLunarDate, formatTraditional, getMonthName, getDayName, getCalendarGrid, getISOWeekNumber, t, getZodiacAnimal } from '@lichta/core';
|
|
2
|
+
export type { LunarDate, SolarDate, Locale, CalendarDayCell, FirstDayOfWeek } from '@lichta/core';
|
|
3
3
|
export { default as Formatter } from './components/Formatter.svelte';
|
|
4
4
|
export { default as Calendar } from './components/Calendar.svelte';
|
|
5
|
+
export { default as DatePicker } from './components/DatePicker.svelte';
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Lichta Svelte — Public API (Root Entry Point)
|
|
3
3
|
// ============================================================
|
|
4
4
|
// Re-export phần lõi từ @lichta/core
|
|
5
|
-
export { LichTa, jdFromDate, jdToDate, getYearDetails, getDayCanChi, getMonthCanChi, getHourCanChi, getAuspiciousHours, formatLunarDate, formatTraditional, getMonthName, getDayName, t, getZodiacAnimal } from '@lichta/core';
|
|
5
|
+
export { LichTa, jdFromDate, jdToDate, getYearDetails, getDayCanChi, getMonthCanChi, getHourCanChi, getAuspiciousHours, formatLunarDate, formatTraditional, getMonthName, getDayName, getCalendarGrid, getISOWeekNumber, t, getZodiacAnimal } from '@lichta/core';
|
|
6
6
|
// Xuất bản phần Svelte Component
|
|
7
7
|
export { default as Formatter } from './components/Formatter.svelte';
|
|
8
8
|
export { default as Calendar } from './components/Calendar.svelte';
|
|
9
|
+
export { default as DatePicker } from './components/DatePicker.svelte';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lichta/svelte",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Svelte components for LichTa",
|
|
5
5
|
"author": "Zeforc Labs | Stridev",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"vite": "^8.0.16"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@lichta/core": "2.0
|
|
55
|
+
"@lichta/core": "2.2.0"
|
|
56
56
|
},
|
|
57
57
|
"repository": {
|
|
58
58
|
"type": "git",
|