@lichta/svelte 2.0.0 → 2.1.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 +34 -0
- package/dist/components/Calendar.svelte +7 -305
- package/dist/components/Calendar.svelte.d.ts +2 -8
- package/dist/components/DatePicker.svelte +184 -0
- package/dist/components/DatePicker.svelte.d.ts +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
# @lichta/svelte
|
|
2
2
|
|
|
3
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
|
|
@@ -70,6 +75,35 @@ Bạn cần cài `svelte` (peer dependency, `^5.0.0`) nếu dự án chưa có.
|
|
|
70
75
|
| `date` | `Date` | `new Date()` | Ngày dương lịch cần hiển thị |
|
|
71
76
|
| `children` | `Snippet` | — | Nội dung tùy chỉnh (render prop) |
|
|
72
77
|
|
|
78
|
+
### `DatePicker`
|
|
79
|
+
|
|
80
|
+
```svelte
|
|
81
|
+
<script>
|
|
82
|
+
import { DatePicker } from '@lichta/svelte';
|
|
83
|
+
import '@lichta/core/styles/calendar-base.css';
|
|
84
|
+
import '@lichta/core/styles/calendar-glass.css'; // theme "glass" (tùy chọn)
|
|
85
|
+
import '@lichta/core/styles/datepicker-base.css';
|
|
86
|
+
import '@lichta/core/styles/datepicker-glass.css'; // theme "glass" (tùy chọn)
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<DatePicker theme="glass" locale="vi" onSelect={(date, lunar) => console.log(date, lunar)} />
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
> ⚠️ 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.
|
|
93
|
+
|
|
94
|
+
| Prop | Kiểu | Mặc định | Mô tả |
|
|
95
|
+
| ----------- | ----------------------------------------- | -------------- | ----------------------------------------------------------------------- |
|
|
96
|
+
| `value` | `Date \| null` | `null` | Ngày đang chọn (uncontrolled — component tự quản lý sau lần mount đầu) |
|
|
97
|
+
| `onSelect` | `(date: Date, lunar: LunarDate) => void` | — | Callback khi chọn ngày trong popover |
|
|
98
|
+
| `placeholder` | `string` | `'Chọn ngày'` | Placeholder khi chưa chọn ngày |
|
|
99
|
+
| `locale` | `'vi' \| 'en' \| 'ja' \| 'ko'` | `'vi'` | Ngôn ngữ hiển thị trong popover |
|
|
100
|
+
| `theme` | `'classic' \| 'glass'` | `'classic'` | Giao diện input + popover |
|
|
101
|
+
| `showLunar` | `boolean` | `true` | Hiện ngày âm lịch trong popover |
|
|
102
|
+
| `format` | `(date: Date) => string` | `dd/MM/yyyy` | Hàm format ngày hiển thị trên input |
|
|
103
|
+
| `disabled` | `boolean` | `false` | Vô hiệu hoá input |
|
|
104
|
+
|
|
105
|
+
Popover tự đóng khi: chọn xong 1 ngày, click ra ngoài, hoặc nhấn phím `Escape`.
|
|
106
|
+
|
|
73
107
|
## Dùng logic thuần (không cần component)
|
|
74
108
|
|
|
75
109
|
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,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import type { LunarDate } from '@lichta/core';
|
|
2
|
+
import { getYearDetails, getCalendarGrid, 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
|
|
|
@@ -9,13 +9,7 @@
|
|
|
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 */
|
|
@@ -66,109 +60,16 @@
|
|
|
66
60
|
});
|
|
67
61
|
|
|
68
62
|
// 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
|
-
);
|
|
63
|
+
const weekDayLabels = $derived(t(locale).weekDays);
|
|
74
64
|
|
|
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
|
-
];
|
|
65
|
+
// Tên tháng dương lịch cho phần header điều hướng
|
|
66
|
+
const monthNames = $derived(t(locale).solarMonthNames);
|
|
90
67
|
|
|
91
68
|
// Can Chi năm hiển thị
|
|
92
69
|
let yearInfo = $derived(getYearDetails(currentYear));
|
|
93
70
|
|
|
94
|
-
// Ngày hôm nay
|
|
95
|
-
const today = new Date();
|
|
96
|
-
const todayStr = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`;
|
|
97
|
-
|
|
98
71
|
// 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
|
-
});
|
|
72
|
+
let calendarGrid = $derived(getCalendarGrid(currentMonth, currentYear, selectedDate));
|
|
172
73
|
|
|
173
74
|
// Navigation
|
|
174
75
|
function prevMonth() {
|
|
@@ -262,202 +163,3 @@
|
|
|
262
163
|
</div>
|
|
263
164
|
{/if}
|
|
264
165
|
</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
3
|
import type { Locale } 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;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getYearDetails, getCalendarGrid, t } from '@lichta/core';
|
|
3
|
+
import type { LunarDate, Locale, CalendarDayCell } 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
|
+
placeholder?: string;
|
|
11
|
+
locale?: Locale;
|
|
12
|
+
theme?: 'classic' | 'glass';
|
|
13
|
+
showLunar?: boolean;
|
|
14
|
+
format?: (date: Date) => string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function defaultFormat(date: Date): string {
|
|
19
|
+
const dd = String(date.getDate()).padStart(2, '0');
|
|
20
|
+
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
21
|
+
return `${dd}/${mm}/${date.getFullYear()}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
value = null,
|
|
26
|
+
onSelect,
|
|
27
|
+
placeholder = 'Chọn ngày',
|
|
28
|
+
locale = 'vi',
|
|
29
|
+
theme = 'classic',
|
|
30
|
+
showLunar = true,
|
|
31
|
+
format = defaultFormat,
|
|
32
|
+
disabled = false
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
// Writable $derived (Svelte 5): mặc định bám theo prop `value`, nhưng có thể
|
|
36
|
+
// gán lại trực tiếp (xem handleDaySelect) — giá trị gán tay giữ nguyên cho tới
|
|
37
|
+
// khi `value` đổi thì tính lại từ đầu.
|
|
38
|
+
let selected = $derived<Date | null>(value ?? null);
|
|
39
|
+
let isOpen = $state(false);
|
|
40
|
+
let rootEl: HTMLElement | undefined = $state();
|
|
41
|
+
|
|
42
|
+
// State điều hướng tháng/năm hiển thị trong popover, tách khỏi `selected` để
|
|
43
|
+
// prevMonth()/nextMonth() điều hướng được độc lập (không phụ thuộc ngày đã chọn).
|
|
44
|
+
const initialAnchor = value ?? new Date();
|
|
45
|
+
let viewMonth = $state(initialAnchor.getMonth() + 1);
|
|
46
|
+
let viewYear = $state(initialAnchor.getFullYear());
|
|
47
|
+
|
|
48
|
+
// Đồng bộ lại view khi component cha đổi prop `value` từ bên ngoài.
|
|
49
|
+
$effect(() => {
|
|
50
|
+
if (value) {
|
|
51
|
+
viewMonth = value.getMonth() + 1;
|
|
52
|
+
viewYear = value.getFullYear();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const weekDayLabels = $derived(t(locale).weekDays);
|
|
57
|
+
const monthNames = $derived(t(locale).solarMonthNames);
|
|
58
|
+
let yearInfo = $derived(getYearDetails(viewYear));
|
|
59
|
+
let grid = $derived(getCalendarGrid(viewMonth, viewYear, selected));
|
|
60
|
+
const displayValue = $derived(selected ? format(selected) : '');
|
|
61
|
+
|
|
62
|
+
function toggleOpen() {
|
|
63
|
+
if (!disabled) isOpen = !isOpen;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function prevMonth() {
|
|
67
|
+
if (viewMonth === 1) {
|
|
68
|
+
viewMonth = 12;
|
|
69
|
+
viewYear -= 1;
|
|
70
|
+
} else {
|
|
71
|
+
viewMonth -= 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function nextMonth() {
|
|
76
|
+
if (viewMonth === 12) {
|
|
77
|
+
viewMonth = 1;
|
|
78
|
+
viewYear += 1;
|
|
79
|
+
} else {
|
|
80
|
+
viewMonth += 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleDaySelect(cell: CalendarDayCell) {
|
|
85
|
+
selected = cell.solar;
|
|
86
|
+
isOpen = false;
|
|
87
|
+
onSelect?.(cell.solar, cell.lunar);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handlePointerDown(event: MouseEvent) {
|
|
91
|
+
if (rootEl && !rootEl.contains(event.target as Node)) {
|
|
92
|
+
isOpen = false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
97
|
+
if (event.key === 'Escape') isOpen = false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
$effect(() => {
|
|
101
|
+
if (!isOpen) return;
|
|
102
|
+
document.addEventListener('mousedown', handlePointerDown);
|
|
103
|
+
document.addEventListener('keydown', handleKeydown);
|
|
104
|
+
return () => {
|
|
105
|
+
document.removeEventListener('mousedown', handlePointerDown);
|
|
106
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<div bind:this={rootEl} class={['lich-ta-datepicker', `lichta-theme-${theme}`].join(' ')}>
|
|
112
|
+
<input
|
|
113
|
+
type="text"
|
|
114
|
+
class="lich-ta-datepicker-input"
|
|
115
|
+
readonly
|
|
116
|
+
{disabled}
|
|
117
|
+
{placeholder}
|
|
118
|
+
value={displayValue}
|
|
119
|
+
aria-haspopup="dialog"
|
|
120
|
+
aria-expanded={isOpen}
|
|
121
|
+
onclick={toggleOpen}
|
|
122
|
+
/>
|
|
123
|
+
{#if isOpen}
|
|
124
|
+
<div class="lich-ta-datepicker-popover" role="dialog">
|
|
125
|
+
<div class="lich-ta-datepicker-calendar">
|
|
126
|
+
<div class="lich-ta-datepicker-calendar-header">
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
class="lich-ta-datepicker-calendar-nav"
|
|
130
|
+
onclick={prevMonth}
|
|
131
|
+
aria-label="Tháng trước">◀</button
|
|
132
|
+
>
|
|
133
|
+
<div class="lich-ta-datepicker-calendar-title">
|
|
134
|
+
<span class="lich-ta-datepicker-calendar-month-year">
|
|
135
|
+
{monthNames[viewMonth - 1]}, {viewYear}
|
|
136
|
+
</span>
|
|
137
|
+
<span class="lich-ta-datepicker-calendar-canchi">
|
|
138
|
+
{yearInfo.can}
|
|
139
|
+
{yearInfo.chi}
|
|
140
|
+
</span>
|
|
141
|
+
</div>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
class="lich-ta-datepicker-calendar-nav"
|
|
145
|
+
onclick={nextMonth}
|
|
146
|
+
aria-label="Tháng sau">▶</button
|
|
147
|
+
>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div class="lich-ta-datepicker-calendar-weekdays">
|
|
151
|
+
{#each weekDayLabels as label (label)}
|
|
152
|
+
<div class="lich-ta-datepicker-calendar-weekday">{label}</div>
|
|
153
|
+
{/each}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="lich-ta-datepicker-calendar-grid">
|
|
157
|
+
{#each grid as cell (cell.solar.getTime())}
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
class="lich-ta-datepicker-calendar-day"
|
|
161
|
+
class:is-today={cell.isToday}
|
|
162
|
+
class:is-selected={cell.isSelected}
|
|
163
|
+
class:is-other-month={!cell.isCurrentMonth}
|
|
164
|
+
class:is-first-lunar={cell.lunar.day === 1}
|
|
165
|
+
tabindex={cell.isCurrentMonth ? 0 : -1}
|
|
166
|
+
onclick={() => handleDaySelect(cell)}
|
|
167
|
+
>
|
|
168
|
+
<span class="solar-day">{cell.solar.getDate()}</span>
|
|
169
|
+
{#if showLunar}
|
|
170
|
+
<span class="lunar-day">
|
|
171
|
+
{#if cell.lunar.day === 1}
|
|
172
|
+
{cell.lunar.day}/{cell.lunar.month}{cell.lunar.isLeap ? '*' : ''}
|
|
173
|
+
{:else}
|
|
174
|
+
{cell.lunar.day}
|
|
175
|
+
{/if}
|
|
176
|
+
</span>
|
|
177
|
+
{/if}
|
|
178
|
+
</button>
|
|
179
|
+
{/each}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
{/if}
|
|
184
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LunarDate, Locale } 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
|
+
placeholder?: string;
|
|
8
|
+
locale?: Locale;
|
|
9
|
+
theme?: 'classic' | 'glass';
|
|
10
|
+
showLunar?: boolean;
|
|
11
|
+
format?: (date: Date) => string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
declare const DatePicker: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type DatePicker = ReturnType<typeof DatePicker>;
|
|
16
|
+
export default DatePicker;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { LichTa, jdFromDate, jdToDate, getYearDetails, getDayCanChi, getMonthCan
|
|
|
2
2
|
export type { LunarDate, SolarDate, Locale } 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
|
@@ -6,3 +6,4 @@ export { LichTa, jdFromDate, jdToDate, getYearDetails, getDayCanChi, getMonthCan
|
|
|
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.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Svelte components for LichTa",
|
|
5
5
|
"author": "Zeforc Labs | Stridev",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,15 +52,23 @@
|
|
|
52
52
|
"vite": "^8.0.16"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@lichta/core": "2.
|
|
55
|
+
"@lichta/core": "2.1.0"
|
|
56
56
|
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/zeforc/lichta.git"
|
|
60
|
+
},
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/zeforc/lichta/issues"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/zeforc/lichta#readme",
|
|
57
65
|
"scripts": {
|
|
58
66
|
"dev": "vite dev",
|
|
59
67
|
"build": "svelte-kit sync && svelte-package && publint",
|
|
60
68
|
"preview": "vite preview",
|
|
61
69
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
62
70
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
63
|
-
"lint": "
|
|
71
|
+
"lint": "eslint .",
|
|
64
72
|
"format": "prettier --write ."
|
|
65
73
|
}
|
|
66
74
|
}
|