lichta 1.0.0-beta
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 +245 -0
- package/dist/components/Calendar.svelte +437 -0
- package/dist/components/Calendar.svelte.d.ts +33 -0
- package/dist/components/Formatter.svelte +57 -0
- package/dist/components/Formatter.svelte.d.ts +8 -0
- package/dist/constants/earthly-branches.d.ts +31 -0
- package/dist/constants/earthly-branches.js +42 -0
- package/dist/constants/five-elements.d.ts +9 -0
- package/dist/constants/five-elements.js +9 -0
- package/dist/constants/heavenly-stems.d.ts +19 -0
- package/dist/constants/heavenly-stems.js +24 -0
- package/dist/constants/i18n.d.ts +59 -0
- package/dist/constants/i18n.js +65 -0
- package/dist/core/feng-shui.d.ts +55 -0
- package/dist/core/feng-shui.js +116 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +4 -0
- package/dist/core/lunar.d.ts +73 -0
- package/dist/core/lunar.js +335 -0
- package/dist/core/types.d.ts +17 -0
- package/dist/core/types.js +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +14 -0
- package/dist/utils/format.d.ts +74 -0
- package/dist/utils/format.js +150 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +2 -0
- package/package.json +94 -0
package/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# 🌙 LichTa
|
|
2
|
+
|
|
3
|
+
**Thư viện chuyển đổi Dương lịch ↔ Âm lịch Việt Nam cho JavaScript/TypeScript.**
|
|
4
|
+
|
|
5
|
+
Hỗ trợ Svelte 5 components ăn liền, đồng thời export pure TypeScript core cho React, Vue, Angular, và vanilla JS.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/lichta)
|
|
8
|
+
[](https://github.com/juststridev/lichta/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ✨ Tính năng
|
|
13
|
+
|
|
14
|
+
- 🔄 **Chuyển đổi Solar ↔ Lunar** — Thuật toán Hồ Ngọc Đức (phạm vi 1800–2199)
|
|
15
|
+
- 🐉 **Can Chi** — Tính Can Chi cho năm, tháng, ngày, giờ
|
|
16
|
+
- 🔮 **Ngũ Hành & Mệnh** — Tính mệnh theo năm sinh
|
|
17
|
+
- ⏰ **Giờ Hoàng Đạo** — 6 giờ tốt trong ngày
|
|
18
|
+
- 📅 **Calendar Component** — Lịch tháng Svelte 5 với ngày âm lịch
|
|
19
|
+
- 🎨 **Format** — Hiển thị ngày âm lịch truyền thống (Mùng Một tháng Giêng...)
|
|
20
|
+
- 🌐 **i18n** — Hỗ trợ Việt–Anh–Nhật–Hàn (vi, en, ja, ko)
|
|
21
|
+
- 📦 **Multi-framework** — Svelte 5, React, Vue, Angular, vanilla JS
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 📦 Cài đặt
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install lichta
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🚀 Sử dụng
|
|
34
|
+
|
|
35
|
+
### Core Logic (mọi framework)
|
|
36
|
+
|
|
37
|
+
Import từ `lichta/core` để sử dụng **pure TypeScript** — không phụ thuộc Svelte:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { LichTa, getYearDetails } from 'lichta/core';
|
|
41
|
+
|
|
42
|
+
// Dương lịch → Âm lịch
|
|
43
|
+
const lunar = LichTa.toLunar(10, 2, 2024);
|
|
44
|
+
// → {
|
|
45
|
+
// day: 1, month: 1, year: 2024,
|
|
46
|
+
// isLeap: false,
|
|
47
|
+
// yearCanChi: 'Giáp Thìn',
|
|
48
|
+
// monthCanChi: 'Bính Dần',
|
|
49
|
+
// dayCanChi: '...',
|
|
50
|
+
// jd: 2460350
|
|
51
|
+
// }
|
|
52
|
+
|
|
53
|
+
// Âm lịch → Dương lịch
|
|
54
|
+
const solar = LichTa.toSolar(1, 1, 2024, false);
|
|
55
|
+
// → { day: 10, month: 2, year: 2024 }
|
|
56
|
+
|
|
57
|
+
// Múi giờ tùy chỉnh (mặc định GMT+7)
|
|
58
|
+
const lunarChina = LichTa.toLunar(10, 2, 2024, 8); // GMT+8
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Can Chi & Phong Thủy
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { getYearDetails, getDayCanChi, getMonthCanChi, getHourCanChi, getAuspiciousHours } from 'lichta/core';
|
|
65
|
+
|
|
66
|
+
// Can Chi + Mệnh theo năm
|
|
67
|
+
getYearDetails(2024);
|
|
68
|
+
// → { can: 'Giáp', chi: 'Thìn', menh: 'Hỏa',
|
|
69
|
+
// fullString: 'Giáp Thìn - Mệnh Hỏa' }
|
|
70
|
+
|
|
71
|
+
// Can Chi ngày (cần Julian Day Number)
|
|
72
|
+
const lunar = LichTa.toLunar(10, 2, 2024);
|
|
73
|
+
getDayCanChi(lunar.jd); // → 'Giáp Tý'
|
|
74
|
+
|
|
75
|
+
// Can Chi tháng
|
|
76
|
+
getMonthCanChi(1, 2024); // → tháng Giêng năm Giáp Thìn
|
|
77
|
+
|
|
78
|
+
// Can Chi giờ
|
|
79
|
+
getHourCanChi(8, lunar.jd); // → giờ Thìn (7h-9h)
|
|
80
|
+
|
|
81
|
+
// Giờ Hoàng Đạo
|
|
82
|
+
getAuspiciousHours(lunar.jd);
|
|
83
|
+
// → ['Tý', 'Sửu', 'Mão', 'Ngọ', 'Mùi', 'Dậu']
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Format & Hiển thị
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { formatTraditional, formatLunarDate, getMonthName, getDayName } from 'lichta/utils';
|
|
90
|
+
|
|
91
|
+
const lunar = LichTa.toLunar(10, 2, 2024);
|
|
92
|
+
|
|
93
|
+
// Format truyền thống
|
|
94
|
+
formatTraditional(lunar);
|
|
95
|
+
// → 'Mùng Một tháng Giêng năm Giáp Thìn'
|
|
96
|
+
|
|
97
|
+
// Format theo pattern
|
|
98
|
+
formatLunarDate(lunar, 'dd/MM/yyyy'); // → '01/01/2024'
|
|
99
|
+
formatLunarDate(lunar, 'CC'); // → 'Giáp Thìn'
|
|
100
|
+
formatLunarDate(lunar, 'Ngày DC'); // → 'Ngày Giáp Tý'
|
|
101
|
+
|
|
102
|
+
// Tên truyền thống
|
|
103
|
+
getMonthName(1); // → 'Giêng'
|
|
104
|
+
getMonthName(12); // → 'Chạp'
|
|
105
|
+
getDayName(1); // → 'Mùng Một'
|
|
106
|
+
getDayName(15); // → 'Rằm'
|
|
107
|
+
getDayName(30); // → 'Ba Mươi'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Format tokens:**
|
|
111
|
+
|
|
112
|
+
| Token | Mô tả | Ví dụ |
|
|
113
|
+
|--------|-------------------------------|--------------|
|
|
114
|
+
| `dd` | Ngày 2 chữ số | 01, 15 |
|
|
115
|
+
| `d` | Ngày | 1, 15 |
|
|
116
|
+
| `MM` | Tháng 2 chữ số | 01, 12 |
|
|
117
|
+
| `M` | Tháng | 1, 12 |
|
|
118
|
+
| `yyyy` | Năm 4 chữ số | 2024 |
|
|
119
|
+
| `yy` | Năm 2 chữ số cuối | 24 |
|
|
120
|
+
| `CC` | Can Chi năm | Giáp Thìn |
|
|
121
|
+
| `DC` | Can Chi ngày | Giáp Tý |
|
|
122
|
+
| `MC` | Can Chi tháng | Bính Dần |
|
|
123
|
+
| `L` | "Nhuận" nếu tháng nhuận | Nhuận |
|
|
124
|
+
|
|
125
|
+
### Svelte 5 Components
|
|
126
|
+
|
|
127
|
+
```svelte
|
|
128
|
+
<script>
|
|
129
|
+
import { Calendar, Formatter } from 'lichta';
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<!-- Lịch tháng đầy đủ -->
|
|
133
|
+
<Calendar
|
|
134
|
+
showLunar={true}
|
|
135
|
+
locale="vi"
|
|
136
|
+
onSelect={(date, lunar) => console.log(date, lunar)}
|
|
137
|
+
/>
|
|
138
|
+
|
|
139
|
+
<!-- Hiển thị ngày âm lịch -->
|
|
140
|
+
<Formatter date={new Date()} />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Calendar Props
|
|
144
|
+
|
|
145
|
+
| Prop | Type | Mặc định | Mô tả |
|
|
146
|
+
|----------------|-----------------------------------------------|-------------------|-------------------------------|
|
|
147
|
+
| `month` | `number` | Tháng hiện tại | Tháng hiển thị (1-12) |
|
|
148
|
+
| `year` | `number` | Năm hiện tại | Năm hiển thị |
|
|
149
|
+
| `selectedDate` | `Date \| null` | `null` | Ngày được chọn |
|
|
150
|
+
| `onSelect` | `(date: Date, lunar: LunarDate) => void` | — | Callback khi chọn ngày |
|
|
151
|
+
| `showLunar` | `boolean` | `true` | Hiển thị ngày âm lịch |
|
|
152
|
+
| `locale` | `'vi' \| 'en' \| 'ja' \| 'ko'` | `'vi'` | Ngôn ngữ |
|
|
153
|
+
| `dayCell` | `Snippet` | — | Custom snippet cho ô ngày |
|
|
154
|
+
|
|
155
|
+
#### Theming
|
|
156
|
+
|
|
157
|
+
Calendar hỗ trợ CSS custom properties:
|
|
158
|
+
|
|
159
|
+
```css
|
|
160
|
+
:root {
|
|
161
|
+
--lichta-primary: #d4a373;
|
|
162
|
+
--lichta-bg: #fffcf7;
|
|
163
|
+
--lichta-text: #2c1810;
|
|
164
|
+
--lichta-today-bg: #d4a373;
|
|
165
|
+
--lichta-selected-bg: #a0522d;
|
|
166
|
+
--lichta-lunar-text: #b08968;
|
|
167
|
+
--lichta-radius: 8px;
|
|
168
|
+
--lichta-font: 'Inter', sans-serif;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### i18n
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { t, getZodiacAnimal } from 'lichta';
|
|
176
|
+
|
|
177
|
+
// Lấy bộ dịch
|
|
178
|
+
const vi = t('vi');
|
|
179
|
+
vi.heavenlyStems; // ['Giáp', 'Ất', ...]
|
|
180
|
+
vi.earthlyBranches; // ['Tý', 'Sửu', ...]
|
|
181
|
+
vi.fiveElements; // ['Kim', 'Mộc', 'Thủy', 'Hỏa', 'Thổ']
|
|
182
|
+
vi.monthNames; // ['Giêng', 'Hai', ..., 'Chạp']
|
|
183
|
+
|
|
184
|
+
const en = t('en');
|
|
185
|
+
en.fiveElements; // ['Metal', 'Wood', 'Water', 'Fire', 'Earth']
|
|
186
|
+
en.zodiacAnimals; // ['Rat', 'Ox', 'Tiger', ...]
|
|
187
|
+
|
|
188
|
+
const ja = t('ja');
|
|
189
|
+
ja.weekDays; // ['日', '月', '火', '水', '木', '金', '土']
|
|
190
|
+
|
|
191
|
+
const ko = t('ko');
|
|
192
|
+
ko.fiveElements; // ['금', '목', '수', '화', '토']
|
|
193
|
+
|
|
194
|
+
// Con giáp theo năm
|
|
195
|
+
getZodiacAnimal(0, 'vi'); // → 'Chuột'
|
|
196
|
+
getZodiacAnimal(4, 'en'); // → 'Dragon'
|
|
197
|
+
getZodiacAnimal(0, 'ja'); // → '鼠'
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 📁 Import Paths
|
|
203
|
+
|
|
204
|
+
| Path | Nội dung | Dùng cho |
|
|
205
|
+
|-----------------|-----------------------------------|-------------------------------|
|
|
206
|
+
| `lichta` | Toàn bộ API + Svelte components | Svelte 5 projects |
|
|
207
|
+
| `lichta/core` | Pure TypeScript logic | React, Vue, Angular, vanilla |
|
|
208
|
+
| `lichta/utils` | Format utilities | Mọi framework |
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// Svelte 5 — import tất cả
|
|
212
|
+
import { LichTa, Calendar, Formatter, formatTraditional } from 'lichta';
|
|
213
|
+
|
|
214
|
+
// React / Vue / Angular — chỉ import core logic
|
|
215
|
+
import { LichTa, getYearDetails } from 'lichta/core';
|
|
216
|
+
import { formatTraditional, getDayName } from 'lichta/utils';
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 📐 Thuật toán
|
|
222
|
+
|
|
223
|
+
LichTa sử dụng thuật toán chuyển đổi âm lịch của **Hồ Ngọc Đức** (Đại học Leipzig), dựa trên:
|
|
224
|
+
|
|
225
|
+
- **Julian Day Number** — Hệ tọa độ ngày liên tục cho tính toán thiên văn
|
|
226
|
+
- **Điểm Sóc (New Moon)** — Xác định mùng 1 âm lịch
|
|
227
|
+
- **Trung Khí (Solar Terms)** — Xác định tháng và tháng nhuận
|
|
228
|
+
- **Jean Meeus, "Astronomical Algorithms"** — Công thức thiên văn chính xác
|
|
229
|
+
|
|
230
|
+
**Phạm vi**: 1800 – 2199 | **Múi giờ mặc định**: GMT+7 (Việt Nam)
|
|
231
|
+
|
|
232
|
+
Xem chi tiết trong [docs/ALGORITHM.md](./docs/ALGORITHM.md).
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 📄 License
|
|
237
|
+
|
|
238
|
+
[MIT](./LICENSE)
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 🙏 Credits
|
|
243
|
+
|
|
244
|
+
- **Hồ Ngọc Đức** — Thuật toán gốc ([informatik.uni-leipzig.de](http://www.informatik.uni-leipzig.de/~duc/amlich/))
|
|
245
|
+
- **Jean Meeus** — *Astronomical Algorithms* (1998)
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { LichTa } from '../core/lunar.js';
|
|
3
|
+
import { getYearDetails } from '../core/feng-shui.js';
|
|
4
|
+
import type { LunarDate } from '../core/types.js';
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
|
|
7
|
+
import type { Locale } from '../constants/i18n.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Dữ liệu cho mỗi ô ngày trong lưới lịch
|
|
11
|
+
*/
|
|
12
|
+
interface DayCellData {
|
|
13
|
+
solar: Date;
|
|
14
|
+
lunar: LunarDate;
|
|
15
|
+
isToday: boolean;
|
|
16
|
+
isSelected: boolean;
|
|
17
|
+
isCurrentMonth: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
/** Tháng hiển thị (1-12). Mặc định: tháng hiện tại */
|
|
22
|
+
month?: number;
|
|
23
|
+
/** Năm hiển thị. Mặc định: năm hiện tại */
|
|
24
|
+
year?: number;
|
|
25
|
+
/** Ngày được chọn */
|
|
26
|
+
selectedDate?: Date | null;
|
|
27
|
+
/** Callback khi chọn ngày */
|
|
28
|
+
onSelect?: (date: Date, lunar: LunarDate) => void;
|
|
29
|
+
/** Hiển thị ngày âm lịch bên dưới ngày dương */
|
|
30
|
+
showLunar?: boolean;
|
|
31
|
+
/** Locale (vi | en | ja | ko) */
|
|
32
|
+
locale?: Locale;
|
|
33
|
+
/** Custom snippet cho mỗi ô ngày */
|
|
34
|
+
dayCell?: Snippet<[DayCellData]>;
|
|
35
|
+
children?: Snippet;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let {
|
|
39
|
+
month = new Date().getMonth() + 1,
|
|
40
|
+
year = new Date().getFullYear(),
|
|
41
|
+
selectedDate = null,
|
|
42
|
+
onSelect,
|
|
43
|
+
showLunar = true,
|
|
44
|
+
locale = 'vi',
|
|
45
|
+
dayCell,
|
|
46
|
+
children,
|
|
47
|
+
}: Props = $props();
|
|
48
|
+
|
|
49
|
+
// State nội bộ cho navigation (khởi tạo từ props hoặc giá trị mặc định)
|
|
50
|
+
let currentMonth = $state(month ?? new Date().getMonth() + 1);
|
|
51
|
+
let currentYear = $state(year ?? new Date().getFullYear());
|
|
52
|
+
|
|
53
|
+
// Tên thứ trong tuần
|
|
54
|
+
const weekDayLabels = $derived(
|
|
55
|
+
locale === 'vi'
|
|
56
|
+
? ['CN', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7']
|
|
57
|
+
: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Tên tháng hiển thị
|
|
61
|
+
const monthNames = [
|
|
62
|
+
'Tháng 1', 'Tháng 2', 'Tháng 3', 'Tháng 4', 'Tháng 5', 'Tháng 6',
|
|
63
|
+
'Tháng 7', 'Tháng 8', 'Tháng 9', 'Tháng 10', 'Tháng 11', 'Tháng 12',
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// Can Chi năm hiển thị
|
|
67
|
+
let yearInfo = $derived(getYearDetails(currentYear));
|
|
68
|
+
|
|
69
|
+
// Ngày hôm nay
|
|
70
|
+
const today = new Date();
|
|
71
|
+
const todayStr = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`;
|
|
72
|
+
|
|
73
|
+
// Tính grid lịch 6x7 = 42 ô
|
|
74
|
+
let calendarGrid = $derived.by((): DayCellData[] => {
|
|
75
|
+
const grid: DayCellData[] = [];
|
|
76
|
+
|
|
77
|
+
// Ngày đầu tiên của tháng
|
|
78
|
+
const firstDay = new Date(currentYear, currentMonth - 1, 1);
|
|
79
|
+
// Thứ của ngày đầu tiên (0=CN, 1=T2, ...)
|
|
80
|
+
const startDayOfWeek = firstDay.getDay();
|
|
81
|
+
// Số ngày trong tháng
|
|
82
|
+
const daysInMonth = new Date(currentYear, currentMonth, 0).getDate();
|
|
83
|
+
|
|
84
|
+
// Padding ngày tháng trước
|
|
85
|
+
const prevMonthDays = new Date(currentYear, currentMonth - 1, 0).getDate();
|
|
86
|
+
for (let i = startDayOfWeek - 1; i >= 0; i--) {
|
|
87
|
+
const d = prevMonthDays - i;
|
|
88
|
+
const m = currentMonth - 1 <= 0 ? 12 : currentMonth - 1;
|
|
89
|
+
const y = currentMonth - 1 <= 0 ? currentYear - 1 : currentYear;
|
|
90
|
+
const solar = new Date(y, m - 1, d);
|
|
91
|
+
const lunar = LichTa.toLunar(d, m, y);
|
|
92
|
+
const dateStr = `${y}-${m}-${d}`;
|
|
93
|
+
const selStr = selectedDate
|
|
94
|
+
? `${selectedDate.getFullYear()}-${selectedDate.getMonth() + 1}-${selectedDate.getDate()}`
|
|
95
|
+
: '';
|
|
96
|
+
|
|
97
|
+
grid.push({
|
|
98
|
+
solar,
|
|
99
|
+
lunar,
|
|
100
|
+
isToday: dateStr === todayStr,
|
|
101
|
+
isSelected: dateStr === selStr,
|
|
102
|
+
isCurrentMonth: false,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Ngày trong tháng hiện tại
|
|
107
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
108
|
+
const solar = new Date(currentYear, currentMonth - 1, d);
|
|
109
|
+
const lunar = LichTa.toLunar(d, currentMonth, currentYear);
|
|
110
|
+
const dateStr = `${currentYear}-${currentMonth}-${d}`;
|
|
111
|
+
const selStr = selectedDate
|
|
112
|
+
? `${selectedDate.getFullYear()}-${selectedDate.getMonth() + 1}-${selectedDate.getDate()}`
|
|
113
|
+
: '';
|
|
114
|
+
|
|
115
|
+
grid.push({
|
|
116
|
+
solar,
|
|
117
|
+
lunar,
|
|
118
|
+
isToday: dateStr === todayStr,
|
|
119
|
+
isSelected: dateStr === selStr,
|
|
120
|
+
isCurrentMonth: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Padding ngày tháng sau
|
|
125
|
+
const remaining = 42 - grid.length;
|
|
126
|
+
for (let d = 1; d <= remaining; d++) {
|
|
127
|
+
const m = currentMonth + 1 > 12 ? 1 : currentMonth + 1;
|
|
128
|
+
const y = currentMonth + 1 > 12 ? currentYear + 1 : currentYear;
|
|
129
|
+
const solar = new Date(y, m - 1, d);
|
|
130
|
+
const lunar = LichTa.toLunar(d, m, y);
|
|
131
|
+
const dateStr = `${y}-${m}-${d}`;
|
|
132
|
+
const selStr = selectedDate
|
|
133
|
+
? `${selectedDate.getFullYear()}-${selectedDate.getMonth() + 1}-${selectedDate.getDate()}`
|
|
134
|
+
: '';
|
|
135
|
+
|
|
136
|
+
grid.push({
|
|
137
|
+
solar,
|
|
138
|
+
lunar,
|
|
139
|
+
isToday: dateStr === todayStr,
|
|
140
|
+
isSelected: dateStr === selStr,
|
|
141
|
+
isCurrentMonth: false,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return grid;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Navigation
|
|
149
|
+
function prevMonth() {
|
|
150
|
+
if (currentMonth === 1) {
|
|
151
|
+
currentMonth = 12;
|
|
152
|
+
currentYear -= 1;
|
|
153
|
+
} else {
|
|
154
|
+
currentMonth -= 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function nextMonth() {
|
|
159
|
+
if (currentMonth === 12) {
|
|
160
|
+
currentMonth = 1;
|
|
161
|
+
currentYear += 1;
|
|
162
|
+
} else {
|
|
163
|
+
currentMonth += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function handleDayClick(cell: DayCellData) {
|
|
168
|
+
onSelect?.(cell.solar, cell.lunar);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function handleKeydown(event: KeyboardEvent, cell: DayCellData) {
|
|
172
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
handleDayClick(cell);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<div class="lich-ta-calendar">
|
|
180
|
+
<!-- Header: Navigation + Tháng/Năm -->
|
|
181
|
+
<div class="lich-ta-calendar-header">
|
|
182
|
+
<button class="lich-ta-calendar-nav" onclick={prevMonth} aria-label="Tháng trước">◀</button>
|
|
183
|
+
<div class="lich-ta-calendar-title">
|
|
184
|
+
<span class="lich-ta-calendar-month-year">
|
|
185
|
+
{monthNames[currentMonth - 1]}, {currentYear}
|
|
186
|
+
</span>
|
|
187
|
+
<span class="lich-ta-calendar-canchi">
|
|
188
|
+
{yearInfo.can} {yearInfo.chi}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
<button class="lich-ta-calendar-nav" onclick={nextMonth} aria-label="Tháng sau">▶</button>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<!-- Tên thứ -->
|
|
195
|
+
<div class="lich-ta-calendar-weekdays">
|
|
196
|
+
{#each weekDayLabels as label}
|
|
197
|
+
<div class="lich-ta-calendar-weekday">{label}</div>
|
|
198
|
+
{/each}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<!-- Grid ngày -->
|
|
202
|
+
<div class="lich-ta-calendar-grid">
|
|
203
|
+
{#each calendarGrid as cell}
|
|
204
|
+
{#if dayCell}
|
|
205
|
+
{@render dayCell(cell)}
|
|
206
|
+
{:else}
|
|
207
|
+
<button
|
|
208
|
+
class="lich-ta-calendar-day"
|
|
209
|
+
class:is-today={cell.isToday}
|
|
210
|
+
class:is-selected={cell.isSelected}
|
|
211
|
+
class:is-other-month={!cell.isCurrentMonth}
|
|
212
|
+
class:is-first-lunar={cell.lunar.day === 1}
|
|
213
|
+
onclick={() => handleDayClick(cell)}
|
|
214
|
+
onkeydown={(e) => handleKeydown(e, cell)}
|
|
215
|
+
tabindex={cell.isCurrentMonth ? 0 : -1}
|
|
216
|
+
>
|
|
217
|
+
<span class="solar-day">{cell.solar.getDate()}</span>
|
|
218
|
+
{#if showLunar}
|
|
219
|
+
<span class="lunar-day">
|
|
220
|
+
{#if cell.lunar.day === 1}
|
|
221
|
+
{cell.lunar.day}/{cell.lunar.month}{cell.lunar.isLeap ? '*' : ''}
|
|
222
|
+
{:else}
|
|
223
|
+
{cell.lunar.day}
|
|
224
|
+
{/if}
|
|
225
|
+
</span>
|
|
226
|
+
{/if}
|
|
227
|
+
</button>
|
|
228
|
+
{/if}
|
|
229
|
+
{/each}
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<!-- Slot cho nội dung tùy chỉnh bên dưới lịch -->
|
|
233
|
+
{#if children}
|
|
234
|
+
<div class="lich-ta-calendar-footer">
|
|
235
|
+
{@render children()}
|
|
236
|
+
</div>
|
|
237
|
+
{/if}
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<style>
|
|
241
|
+
.lich-ta-calendar {
|
|
242
|
+
--lichta-primary: #d4a373;
|
|
243
|
+
--lichta-primary-light: #f5e6d3;
|
|
244
|
+
--lichta-bg: #fffcf7;
|
|
245
|
+
--lichta-surface: #ffffff;
|
|
246
|
+
--lichta-text: #2c1810;
|
|
247
|
+
--lichta-text-muted: #8b7355;
|
|
248
|
+
--lichta-text-dim: #c4b5a0;
|
|
249
|
+
--lichta-border: #e8ddd0;
|
|
250
|
+
--lichta-today-bg: #d4a373;
|
|
251
|
+
--lichta-today-text: #ffffff;
|
|
252
|
+
--lichta-selected-bg: #a0522d;
|
|
253
|
+
--lichta-selected-text: #ffffff;
|
|
254
|
+
--lichta-hover-bg: #f5e6d3;
|
|
255
|
+
--lichta-lunar-text: #b08968;
|
|
256
|
+
--lichta-first-lunar: #c2185b;
|
|
257
|
+
--lichta-radius: 8px;
|
|
258
|
+
--lichta-font: inherit;
|
|
259
|
+
|
|
260
|
+
font-family: var(--lichta-font);
|
|
261
|
+
background: var(--lichta-bg);
|
|
262
|
+
border: 1px solid var(--lichta-border);
|
|
263
|
+
border-radius: calc(var(--lichta-radius) * 2);
|
|
264
|
+
padding: 16px;
|
|
265
|
+
max-width: 420px;
|
|
266
|
+
width: 100%;
|
|
267
|
+
box-sizing: border-box;
|
|
268
|
+
user-select: none;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Header */
|
|
272
|
+
.lich-ta-calendar-header {
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
justify-content: space-between;
|
|
276
|
+
margin-bottom: 16px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.lich-ta-calendar-nav {
|
|
280
|
+
background: none;
|
|
281
|
+
border: 1px solid var(--lichta-border);
|
|
282
|
+
border-radius: var(--lichta-radius);
|
|
283
|
+
width: 36px;
|
|
284
|
+
height: 36px;
|
|
285
|
+
display: flex;
|
|
286
|
+
align-items: center;
|
|
287
|
+
justify-content: center;
|
|
288
|
+
cursor: pointer;
|
|
289
|
+
color: var(--lichta-text);
|
|
290
|
+
font-size: 0.85rem;
|
|
291
|
+
transition: all 0.15s ease;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.lich-ta-calendar-nav:hover {
|
|
295
|
+
background: var(--lichta-hover-bg);
|
|
296
|
+
border-color: var(--lichta-primary);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.lich-ta-calendar-title {
|
|
300
|
+
text-align: center;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.lich-ta-calendar-month-year {
|
|
304
|
+
display: block;
|
|
305
|
+
font-size: 1.1rem;
|
|
306
|
+
font-weight: 600;
|
|
307
|
+
color: var(--lichta-text);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.lich-ta-calendar-canchi {
|
|
311
|
+
display: block;
|
|
312
|
+
font-size: 0.8rem;
|
|
313
|
+
color: var(--lichta-text-muted);
|
|
314
|
+
margin-top: 2px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Weekdays */
|
|
318
|
+
.lich-ta-calendar-weekdays {
|
|
319
|
+
display: grid;
|
|
320
|
+
grid-template-columns: repeat(7, 1fr);
|
|
321
|
+
margin-bottom: 4px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.lich-ta-calendar-weekday {
|
|
325
|
+
text-align: center;
|
|
326
|
+
font-size: 0.75rem;
|
|
327
|
+
font-weight: 600;
|
|
328
|
+
color: var(--lichta-text-muted);
|
|
329
|
+
padding: 6px 0;
|
|
330
|
+
text-transform: uppercase;
|
|
331
|
+
letter-spacing: 0.05em;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* Grid */
|
|
335
|
+
.lich-ta-calendar-grid {
|
|
336
|
+
display: grid;
|
|
337
|
+
grid-template-columns: repeat(7, 1fr);
|
|
338
|
+
gap: 2px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.lich-ta-calendar-day {
|
|
342
|
+
display: flex;
|
|
343
|
+
flex-direction: column;
|
|
344
|
+
align-items: center;
|
|
345
|
+
justify-content: center;
|
|
346
|
+
padding: 6px 2px;
|
|
347
|
+
min-height: 48px;
|
|
348
|
+
border: none;
|
|
349
|
+
border-radius: var(--lichta-radius);
|
|
350
|
+
background: transparent;
|
|
351
|
+
cursor: pointer;
|
|
352
|
+
transition: all 0.15s ease;
|
|
353
|
+
font-family: inherit;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.lich-ta-calendar-day:hover {
|
|
357
|
+
background: var(--lichta-hover-bg);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.lich-ta-calendar-day:focus-visible {
|
|
361
|
+
outline: 2px solid var(--lichta-primary);
|
|
362
|
+
outline-offset: 1px;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.solar-day {
|
|
366
|
+
font-size: 0.95rem;
|
|
367
|
+
font-weight: 500;
|
|
368
|
+
color: var(--lichta-text);
|
|
369
|
+
line-height: 1.2;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.lunar-day {
|
|
373
|
+
font-size: 0.65rem;
|
|
374
|
+
color: var(--lichta-lunar-text);
|
|
375
|
+
line-height: 1.2;
|
|
376
|
+
margin-top: 1px;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* States */
|
|
380
|
+
.is-other-month .solar-day {
|
|
381
|
+
color: var(--lichta-text-dim);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.is-other-month .lunar-day {
|
|
385
|
+
color: var(--lichta-text-dim);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.is-today {
|
|
389
|
+
background: var(--lichta-today-bg);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.is-today .solar-day {
|
|
393
|
+
color: var(--lichta-today-text);
|
|
394
|
+
font-weight: 700;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.is-today .lunar-day {
|
|
398
|
+
color: var(--lichta-today-text);
|
|
399
|
+
opacity: 0.85;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.is-today:hover {
|
|
403
|
+
background: var(--lichta-today-bg);
|
|
404
|
+
opacity: 0.9;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.is-selected {
|
|
408
|
+
background: var(--lichta-selected-bg);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.is-selected .solar-day {
|
|
412
|
+
color: var(--lichta-selected-text);
|
|
413
|
+
font-weight: 700;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.is-selected .lunar-day {
|
|
417
|
+
color: var(--lichta-selected-text);
|
|
418
|
+
opacity: 0.85;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.is-first-lunar .lunar-day {
|
|
422
|
+
color: var(--lichta-first-lunar);
|
|
423
|
+
font-weight: 600;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.is-today.is-first-lunar .lunar-day,
|
|
427
|
+
.is-selected.is-first-lunar .lunar-day {
|
|
428
|
+
color: var(--lichta-today-text);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* Footer */
|
|
432
|
+
.lich-ta-calendar-footer {
|
|
433
|
+
margin-top: 12px;
|
|
434
|
+
padding-top: 12px;
|
|
435
|
+
border-top: 1px solid var(--lichta-border);
|
|
436
|
+
}
|
|
437
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { LunarDate } from '../core/types.js';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { Locale } from '../constants/i18n.js';
|
|
4
|
+
/**
|
|
5
|
+
* Dữ liệu cho mỗi ô ngày trong lưới lịch
|
|
6
|
+
*/
|
|
7
|
+
interface DayCellData {
|
|
8
|
+
solar: Date;
|
|
9
|
+
lunar: LunarDate;
|
|
10
|
+
isToday: boolean;
|
|
11
|
+
isSelected: boolean;
|
|
12
|
+
isCurrentMonth: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface Props {
|
|
15
|
+
/** Tháng hiển thị (1-12). Mặc định: tháng hiện tại */
|
|
16
|
+
month?: number;
|
|
17
|
+
/** Năm hiển thị. Mặc định: năm hiện tại */
|
|
18
|
+
year?: number;
|
|
19
|
+
/** Ngày được chọn */
|
|
20
|
+
selectedDate?: Date | null;
|
|
21
|
+
/** Callback khi chọn ngày */
|
|
22
|
+
onSelect?: (date: Date, lunar: LunarDate) => void;
|
|
23
|
+
/** Hiển thị ngày âm lịch bên dưới ngày dương */
|
|
24
|
+
showLunar?: boolean;
|
|
25
|
+
/** Locale (vi | en | ja | ko) */
|
|
26
|
+
locale?: Locale;
|
|
27
|
+
/** Custom snippet cho mỗi ô ngày */
|
|
28
|
+
dayCell?: Snippet<[DayCellData]>;
|
|
29
|
+
children?: Snippet;
|
|
30
|
+
}
|
|
31
|
+
declare const Calendar: import("svelte").Component<Props, {}, "">;
|
|
32
|
+
type Calendar = ReturnType<typeof Calendar>;
|
|
33
|
+
export default Calendar;
|