@lichta/svelte 2.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zeforc Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @lichta/svelte
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@lichta/svelte.svg)](https://www.npmjs.com/package/@lichta/svelte)
4
+
5
+ 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
+
7
+ 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
+
9
+ ## Cài đặt
10
+
11
+ ```bash
12
+ pnpm add @lichta/svelte
13
+ ```
14
+
15
+ Bạn cần cài `svelte` (peer dependency, `^5.0.0`) nếu dự án chưa có.
16
+
17
+ ## Sử dụng
18
+
19
+ ```svelte
20
+ <script>
21
+ import { Calendar, Formatter } from '@lichta/svelte';
22
+ import '@lichta/core/styles/calendar-base.css';
23
+ import '@lichta/core/styles/calendar-glass.css'; // theme "glass" (tùy chọn)
24
+ </script>
25
+
26
+ <!-- Lịch tháng đầy đủ -->
27
+ <Calendar theme="glass" locale="vi" onSelect={(date, lunar) => console.log(date, lunar)} />
28
+
29
+ <!-- Hiển thị ngày âm lịch cho 1 ngày cụ thể -->
30
+ <Formatter date={new Date()} />
31
+ ```
32
+
33
+ > ⚠️ `Calendar`/`Formatter` chỉ export class CSS (`lich-ta-calendar`, ...) — bạn **phải tự import** file CSS ở trên (hoặc CSS tương đương của riêng bạn), nếu không lịch sẽ hiển thị không có style.
34
+
35
+ ### Custom render cho mỗi ô ngày
36
+
37
+ ```svelte
38
+ <Calendar>
39
+ {#snippet dayCell(cell)}
40
+ <div>
41
+ <span>{cell.solar.getDate()}</span>
42
+ {#if cell.isCurrentMonth}<span>{cell.lunar.day}</span>{/if}
43
+ </div>
44
+ {/snippet}
45
+ </Calendar>
46
+ ```
47
+
48
+ `cell` có kiểu `DayCellData`: `{ solar: Date, lunar: LunarDate, isToday: boolean, isSelected: boolean, isCurrentMonth: boolean }`.
49
+
50
+ ## Props
51
+
52
+ ### `Calendar`
53
+
54
+ | Prop | Kiểu | Mặc định | Mô tả |
55
+ | -------------- | ---------------------------------------- | -------------- | --------------------------------- |
56
+ | `month` | `number` | Tháng hiện tại | Tháng hiển thị ban đầu (1–12) |
57
+ | `year` | `number` | Năm hiện tại | Năm hiển thị ban đầu |
58
+ | `selectedDate` | `Date \| null` | `null` | Ngày được chọn (controlled) |
59
+ | `showLunar` | `boolean` | `true` | Hiện ngày âm lịch dưới ngày dương |
60
+ | `locale` | `'vi' \| 'en' \| 'ja' \| 'ko'` | `'vi'` | Ngôn ngữ nhãn thứ trong tuần |
61
+ | `theme` | `'classic' \| 'glass'` | `'classic'` | Giao diện lịch |
62
+ | `onSelect` | `(date: Date, lunar: LunarDate) => void` | — | Callback khi chọn ngày |
63
+ | `dayCell` | `Snippet<[DayCellData]>` | — | Custom snippet cho mỗi ô ngày |
64
+ | `children` | `Snippet` | — | Nội dung footer tùy chỉnh |
65
+
66
+ ### `Formatter`
67
+
68
+ | Prop | Kiểu | Mặc định | Mô tả |
69
+ | ---------- | --------- | ------------ | -------------------------------- |
70
+ | `date` | `Date` | `new Date()` | Ngày dương lịch cần hiển thị |
71
+ | `children` | `Snippet` | — | Nội dung tùy chỉnh (render prop) |
72
+
73
+ ## Dùng logic thuần (không cần component)
74
+
75
+ 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`:
76
+
77
+ ```typescript
78
+ import { LichTa, getYearDetails, formatTraditional, t } from '@lichta/svelte';
79
+
80
+ const lunar = LichTa.toLunar(10, 2, 2024);
81
+ ```
82
+
83
+ Xem đầy đủ API tại README của [`@lichta/core`](https://www.npmjs.com/package/@lichta/core).
84
+
85
+ ## Theming
86
+
87
+ ```css
88
+ :root {
89
+ --lichta-primary: #d4a373;
90
+ --lichta-bg: #fffcf7;
91
+ --lichta-text: #2c1810;
92
+ --lichta-today-bg: #d4a373;
93
+ --lichta-selected-bg: #a0522d;
94
+ --lichta-lunar-text: #b08968;
95
+ --lichta-radius: 8px;
96
+ --lichta-font: 'Inter', sans-serif;
97
+ }
98
+ ```
99
+
100
+ ## License
101
+
102
+ [MIT](./LICENSE)
@@ -0,0 +1,463 @@
1
+ <script lang="ts">
2
+ import { LichTa, getYearDetails } from '@lichta/core';
3
+ import type { LunarDate } from '@lichta/core';
4
+ import type { Snippet } from 'svelte';
5
+ import { untrack } from 'svelte';
6
+
7
+ import type { Locale } from '@lichta/core';
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
+ /** Theme cho calendar */
37
+ theme?: 'classic' | 'glass';
38
+ }
39
+
40
+ let {
41
+ month = new Date().getMonth() + 1,
42
+ year = new Date().getFullYear(),
43
+ selectedDate = null,
44
+ onSelect,
45
+ showLunar = true,
46
+ locale = 'vi',
47
+ dayCell,
48
+ children,
49
+ theme = 'classic'
50
+ }: Props = $props();
51
+
52
+ // State nội bộ cho navigation (khởi tạo từ props hoặc giá trị mặc định).
53
+ // Cố ý chỉ lấy giá trị ban đầu (untrack) — việc đồng bộ khi prop đổi sau đó
54
+ // do 2 $effect bên dưới đảm nhiệm, để tránh cảnh báo state_referenced_locally.
55
+ let currentMonth = $state(untrack(() => month ?? new Date().getMonth() + 1));
56
+ let currentYear = $state(untrack(() => year ?? new Date().getFullYear()));
57
+
58
+ // Đồng bộ lại state nội bộ khi component cha đổi prop `month`/`year` từ bên ngoài
59
+ // (không chạy lại khi chỉ điều hướng nội bộ qua prevMonth()/nextMonth(), vì effect
60
+ // này chỉ theo dõi `month`/`year`, không theo dõi `currentMonth`/`currentYear`).
61
+ $effect(() => {
62
+ if (month !== undefined) currentMonth = month;
63
+ });
64
+ $effect(() => {
65
+ if (year !== undefined) currentYear = year;
66
+ });
67
+
68
+ // 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
+ );
74
+
75
+ // Tên tháng hiển thị
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
+ ];
90
+
91
+ // Can Chi năm hiển thị
92
+ let yearInfo = $derived(getYearDetails(currentYear));
93
+
94
+ // Ngày hôm nay
95
+ const today = new Date();
96
+ const todayStr = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`;
97
+
98
+ // Tính grid lịch 6x7 = 42 ô
99
+ let calendarGrid = $derived.by((): DayCellData[] => {
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
+ });
172
+
173
+ // Navigation
174
+ function prevMonth() {
175
+ if (currentMonth === 1) {
176
+ currentMonth = 12;
177
+ currentYear -= 1;
178
+ } else {
179
+ currentMonth -= 1;
180
+ }
181
+ }
182
+
183
+ function nextMonth() {
184
+ if (currentMonth === 12) {
185
+ currentMonth = 1;
186
+ currentYear += 1;
187
+ } else {
188
+ currentMonth += 1;
189
+ }
190
+ }
191
+
192
+ function handleDayClick(cell: DayCellData) {
193
+ onSelect?.(cell.solar, cell.lunar);
194
+ }
195
+
196
+ function handleKeydown(event: KeyboardEvent, cell: DayCellData) {
197
+ if (event.key === 'Enter' || event.key === ' ') {
198
+ event.preventDefault();
199
+ handleDayClick(cell);
200
+ }
201
+ }
202
+ </script>
203
+
204
+ <div class={['lich-ta-calendar', `lichta-theme-${theme}`].join(' ')}>
205
+ <!-- Header: Navigation + Tháng/Năm -->
206
+ <div class="lich-ta-calendar-header">
207
+ <button class="lich-ta-calendar-nav" onclick={prevMonth} aria-label="Tháng trước">◀</button>
208
+ <div class="lich-ta-calendar-title">
209
+ <span class="lich-ta-calendar-month-year">
210
+ {monthNames[currentMonth - 1]}, {currentYear}
211
+ </span>
212
+ <span class="lich-ta-calendar-canchi">
213
+ {yearInfo.can}
214
+ {yearInfo.chi}
215
+ </span>
216
+ </div>
217
+ <button class="lich-ta-calendar-nav" onclick={nextMonth} aria-label="Tháng sau">▶</button>
218
+ </div>
219
+
220
+ <!-- Tên thứ -->
221
+ <div class="lich-ta-calendar-weekdays">
222
+ {#each weekDayLabels as label (label)}
223
+ <div class="lich-ta-calendar-weekday">{label}</div>
224
+ {/each}
225
+ </div>
226
+
227
+ <!-- Grid ngày -->
228
+ <div class="lich-ta-calendar-grid">
229
+ {#each calendarGrid as cell (cell.solar.getTime())}
230
+ {#if dayCell}
231
+ {@render dayCell(cell)}
232
+ {:else}
233
+ <button
234
+ class="lich-ta-calendar-day"
235
+ class:is-today={cell.isToday}
236
+ class:is-selected={cell.isSelected}
237
+ class:is-other-month={!cell.isCurrentMonth}
238
+ class:is-first-lunar={cell.lunar.day === 1}
239
+ onclick={() => handleDayClick(cell)}
240
+ onkeydown={(e) => handleKeydown(e, cell)}
241
+ tabindex={cell.isCurrentMonth ? 0 : -1}
242
+ >
243
+ <span class="solar-day">{cell.solar.getDate()}</span>
244
+ {#if showLunar}
245
+ <span class="lunar-day">
246
+ {#if cell.lunar.day === 1}
247
+ {cell.lunar.day}/{cell.lunar.month}{cell.lunar.isLeap ? '*' : ''}
248
+ {:else}
249
+ {cell.lunar.day}
250
+ {/if}
251
+ </span>
252
+ {/if}
253
+ </button>
254
+ {/if}
255
+ {/each}
256
+ </div>
257
+
258
+ <!-- Slot cho nội dung tùy chỉnh bên dưới lịch -->
259
+ {#if children}
260
+ <div class="lich-ta-calendar-footer">
261
+ {@render children()}
262
+ </div>
263
+ {/if}
264
+ </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>
@@ -0,0 +1,35 @@
1
+ import type { LunarDate } from '@lichta/core';
2
+ import type { Snippet } from 'svelte';
3
+ import type { Locale } from '@lichta/core';
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
+ /** Theme cho calendar */
31
+ theme?: 'classic' | 'glass';
32
+ }
33
+ declare const Calendar: import("svelte").Component<Props, {}, "">;
34
+ type Calendar = ReturnType<typeof Calendar>;
35
+ export default Calendar;
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import { LichTa } from '@lichta/core';
3
+ import type { LunarDate } from '@lichta/core';
4
+ import type { Snippet } from 'svelte';
5
+
6
+ // Props nhận vào từ consumer ứng dụng
7
+ interface Props {
8
+ date?: Date;
9
+ children?: Snippet;
10
+ }
11
+ let { date = new Date(), children }: Props = $props();
12
+
13
+ // Khai báo biến reactive tự động tính toán lại khi `date` thay đổi
14
+ let lunar: LunarDate = $derived.by(() => {
15
+ const d = date.getDate();
16
+ const m = date.getMonth() + 1; // Tháng trong JS tính từ 0
17
+ const y = date.getFullYear();
18
+ return LichTa.toLunar(d, m, y);
19
+ });
20
+ </script>
21
+
22
+ <div class="lich-ta-formatter">
23
+ <div class="lunar-display-banner">
24
+ <span class="lunar-date">Ngày {lunar.day} tháng {lunar.month} (Âm lịch)</span>
25
+ <span class="can-chi-details">Năm {lunar.yearCanChi}</span>
26
+ </div>
27
+
28
+ <div class="control-slot-wrapper">
29
+ {@render children?.()}
30
+ </div>
31
+ </div>
32
+
33
+ <style>
34
+ .lich-ta-formatter {
35
+ display: block;
36
+ width: 100%;
37
+ box-sizing: border-box;
38
+ }
39
+ .lunar-display-banner {
40
+ padding: 12px;
41
+ background-color: #fcfaf7;
42
+ border-left: 4px solid #d4a373;
43
+ margin-bottom: 16px;
44
+ }
45
+ .lunar-date {
46
+ display: block;
47
+ font-weight: bold;
48
+ font-size: 1.1rem;
49
+ color: #1a1a1a;
50
+ }
51
+ .can-chi-details {
52
+ display: block;
53
+ font-size: 0.9rem;
54
+ color: #666666;
55
+ margin-top: 4px;
56
+ }
57
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ date?: Date;
4
+ children?: Snippet;
5
+ }
6
+ declare const Formatter: import("svelte").Component<Props, {}, "">;
7
+ type Formatter = ReturnType<typeof Formatter>;
8
+ export default Formatter;
@@ -0,0 +1,4 @@
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';
3
+ export { default as Formatter } from './components/Formatter.svelte';
4
+ export { default as Calendar } from './components/Calendar.svelte';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // ============================================================
2
+ // Lichta Svelte — Public API (Root Entry Point)
3
+ // ============================================================
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';
6
+ // Xuất bản phần Svelte Component
7
+ export { default as Formatter } from './components/Formatter.svelte';
8
+ export { default as Calendar } from './components/Calendar.svelte';
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@lichta/svelte",
3
+ "version": "2.0.0",
4
+ "description": "Svelte components for LichTa",
5
+ "author": "Zeforc Labs | Stridev",
6
+ "license": "MIT",
7
+ "files": [
8
+ "dist",
9
+ "!dist/**/*.test.*",
10
+ "!dist/**/*.spec.*",
11
+ "LICENSE"
12
+ ],
13
+ "sideEffects": [
14
+ "**/*.css"
15
+ ],
16
+ "svelte": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "type": "module",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "svelte": "./dist/index.js",
23
+ "default": "./dist/index.js"
24
+ }
25
+ },
26
+ "peerDependencies": {
27
+ "svelte": "^5.0.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "svelte": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "devDependencies": {
35
+ "@eslint/js": "^10.0.1",
36
+ "@sveltejs/adapter-auto": "^7.0.1",
37
+ "@sveltejs/kit": "^2.63.0",
38
+ "@sveltejs/package": "^2.5.8",
39
+ "@sveltejs/vite-plugin-svelte": "^7.1.2",
40
+ "@types/node": "^22",
41
+ "eslint": "^10.4.1",
42
+ "eslint-config-prettier": "^10.1.8",
43
+ "eslint-plugin-svelte": "^3.19.0",
44
+ "globals": "^17.6.0",
45
+ "prettier": "^3.8.3",
46
+ "prettier-plugin-svelte": "^4.1.0",
47
+ "publint": "^0.3.21",
48
+ "svelte": "^5.56.1",
49
+ "svelte-check": "^4.6.0",
50
+ "typescript": "^6.0.3",
51
+ "typescript-eslint": "^8.60.1",
52
+ "vite": "^8.0.16"
53
+ },
54
+ "dependencies": {
55
+ "@lichta/core": "2.0.0"
56
+ },
57
+ "scripts": {
58
+ "dev": "vite dev",
59
+ "build": "svelte-kit sync && svelte-package && publint",
60
+ "preview": "vite preview",
61
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
62
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
63
+ "lint": "prettier --check . && eslint .",
64
+ "format": "prettier --write ."
65
+ }
66
+ }