@lichta/react 2.0.1 → 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 CHANGED
@@ -1,6 +1,9 @@
1
1
  # @lichta/react
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@lichta/react.svg)](https://www.npmjs.com/package/@lichta/react)
4
+ [![npm version](https://img.shields.io/npm/v/@lichta/core.svg)](https://www.npmjs.com/package/@lichta/core)
5
+
6
+ 🚀 **[Demo trực tiếp](https://lichta.zeneo.app/)**
4
7
 
5
8
  Component `Calendar` (lịch tháng có ngày âm lịch) cho React, đóng gói trên [`@lichta/core`](https://www.npmjs.com/package/@lichta/core).
6
9
 
@@ -63,6 +66,46 @@ export default function App() {
63
66
 
64
67
  > Lưu ý: `month`/`year` hiện chỉ dùng làm giá trị khởi tạo state nội bộ (uncontrolled) — thay đổi prop sau khi mount chưa tự động cập nhật lại lịch đang hiển thị.
65
68
 
69
+ ## DatePicker
70
+
71
+ Component `DatePicker` (input + popover chọn ngày, dựng trên `Calendar` ở trên).
72
+
73
+ ```tsx
74
+ import { DatePicker } from '@lichta/react';
75
+ import '@lichta/core/styles/calendar-base.css';
76
+ import '@lichta/core/styles/calendar-glass.css'; // theme "glass" (tùy chọn)
77
+ import '@lichta/core/styles/datepicker-base.css';
78
+ import '@lichta/core/styles/datepicker-glass.css'; // theme "glass" (tùy chọn)
79
+
80
+ export default function App() {
81
+ return (
82
+ <DatePicker
83
+ theme="glass"
84
+ locale="vi"
85
+ onSelect={(date, lunar) => console.log(date, lunar)}
86
+ />
87
+ );
88
+ }
89
+ ```
90
+
91
+ > ⚠️ 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.
92
+
93
+ ### Props
94
+
95
+ | Prop | Kiểu | Mặc định | Mô tả |
96
+ |---|---|---|---|
97
+ | `value` | `Date \| null` | `null` | Ngày đang chọn (uncontrolled — component tự quản lý sau lần mount đầu) |
98
+ | `onSelect` | `(date: Date, lunar: LunarDate) => void` | — | Callback khi chọn ngày trong popover |
99
+ | `placeholder` | `string` | `'Chọn ngày'` | Placeholder khi chưa chọn ngày |
100
+ | `locale` | `'vi' \| 'en' \| 'ja' \| 'ko'` | `'vi'` | Ngôn ngữ hiển thị trong popover |
101
+ | `theme` | `'classic' \| 'glass'` | `'classic'` | Giao diện input + popover |
102
+ | `showLunar` | `boolean` | `true` | Hiện ngày âm lịch trong popover |
103
+ | `format` | `(date: Date) => string` | `dd/MM/yyyy` | Hàm format ngày hiển thị trên input |
104
+ | `disabled` | `boolean` | `false` | Vô hiệu hoá input |
105
+ | `className` | `string` | `''` | Class CSS bổ sung cho container |
106
+
107
+ Popover tự đóng khi: chọn xong 1 ngày, click ra ngoài, hoặc nhấn phím `Escape`.
108
+
66
109
  ## Theming
67
110
 
68
111
  ```css
@@ -78,6 +121,8 @@ export default function App() {
78
121
  }
79
122
  ```
80
123
 
124
+ `DatePicker` dùng chung biến CSS `--lichta-*` ở trên — không cần custom class riêng.
125
+
81
126
  ## License
82
127
 
83
128
  [MIT](./LICENSE)
package/dist/index.d.mts CHANGED
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
- import { LunarDate, Locale } from '@lichta/core';
2
+ import { LunarDate, Locale, CalendarDayCell } from '@lichta/core';
3
3
 
4
4
  interface CalendarProps {
5
5
  month?: number;
6
6
  year?: number;
7
+ /** Ngày được chọn (controlled). Bỏ qua thì Calendar tự quản lý selection nội bộ (uncontrolled). */
8
+ selectedDate?: Date | null;
7
9
  onSelect?: (date: Date, lunar: LunarDate) => void;
8
10
  showLunar?: boolean;
9
11
  locale?: Locale;
@@ -12,13 +14,27 @@ interface CalendarProps {
12
14
  children?: React.ReactNode;
13
15
  renderDay?: (cell: DayCellData) => React.ReactNode;
14
16
  }
15
- interface DayCellData {
16
- solar: Date;
17
- lunar: LunarDate;
18
- isToday: boolean;
19
- isSelected: boolean;
20
- isCurrentMonth: boolean;
21
- }
17
+ type DayCellData = CalendarDayCell;
22
18
  declare const Calendar: React.FC<CalendarProps>;
23
19
 
24
- export { Calendar, type CalendarProps };
20
+ interface DatePickerProps {
21
+ /** Ngày đang chọn (uncontrolled — component tự quản lý sau lần mount đầu) */
22
+ value?: Date | null;
23
+ /** Gọi khi người dùng chọn 1 ngày trong popover */
24
+ onSelect?: (date: Date, lunar: LunarDate) => void;
25
+ /** Placeholder khi chưa chọn ngày nào */
26
+ placeholder?: string;
27
+ /** Ngôn ngữ hiển thị Can Chi/thứ trong tuần */
28
+ locale?: Locale;
29
+ /** Theme cho input + popover */
30
+ theme?: 'classic' | 'glass';
31
+ /** Hiển thị ngày âm lịch trong popover */
32
+ showLunar?: boolean;
33
+ /** Hàm format ngày hiển thị trên input, mặc định dd/MM/yyyy */
34
+ format?: (date: Date) => string;
35
+ disabled?: boolean;
36
+ className?: string;
37
+ }
38
+ declare const DatePicker: React.FC<DatePickerProps>;
39
+
40
+ export { Calendar, type CalendarProps, DatePicker, type DatePickerProps };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
- import { LunarDate, Locale } from '@lichta/core';
2
+ import { LunarDate, Locale, CalendarDayCell } from '@lichta/core';
3
3
 
4
4
  interface CalendarProps {
5
5
  month?: number;
6
6
  year?: number;
7
+ /** Ngày được chọn (controlled). Bỏ qua thì Calendar tự quản lý selection nội bộ (uncontrolled). */
8
+ selectedDate?: Date | null;
7
9
  onSelect?: (date: Date, lunar: LunarDate) => void;
8
10
  showLunar?: boolean;
9
11
  locale?: Locale;
@@ -12,13 +14,27 @@ interface CalendarProps {
12
14
  children?: React.ReactNode;
13
15
  renderDay?: (cell: DayCellData) => React.ReactNode;
14
16
  }
15
- interface DayCellData {
16
- solar: Date;
17
- lunar: LunarDate;
18
- isToday: boolean;
19
- isSelected: boolean;
20
- isCurrentMonth: boolean;
21
- }
17
+ type DayCellData = CalendarDayCell;
22
18
  declare const Calendar: React.FC<CalendarProps>;
23
19
 
24
- export { Calendar, type CalendarProps };
20
+ interface DatePickerProps {
21
+ /** Ngày đang chọn (uncontrolled — component tự quản lý sau lần mount đầu) */
22
+ value?: Date | null;
23
+ /** Gọi khi người dùng chọn 1 ngày trong popover */
24
+ onSelect?: (date: Date, lunar: LunarDate) => void;
25
+ /** Placeholder khi chưa chọn ngày nào */
26
+ placeholder?: string;
27
+ /** Ngôn ngữ hiển thị Can Chi/thứ trong tuần */
28
+ locale?: Locale;
29
+ /** Theme cho input + popover */
30
+ theme?: 'classic' | 'glass';
31
+ /** Hiển thị ngày âm lịch trong popover */
32
+ showLunar?: boolean;
33
+ /** Hàm format ngày hiển thị trên input, mặc định dd/MM/yyyy */
34
+ format?: (date: Date) => string;
35
+ disabled?: boolean;
36
+ className?: string;
37
+ }
38
+ declare const DatePicker: React.FC<DatePickerProps>;
39
+
40
+ export { Calendar, type CalendarProps, DatePicker, type DatePickerProps };
package/dist/index.js CHANGED
@@ -30,7 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- Calendar: () => Calendar
33
+ Calendar: () => Calendar,
34
+ DatePicker: () => DatePicker
34
35
  });
35
36
  module.exports = __toCommonJS(index_exports);
36
37
 
@@ -38,29 +39,10 @@ module.exports = __toCommonJS(index_exports);
38
39
  var import_react = __toESM(require("react"));
39
40
  var import_core = require("@lichta/core");
40
41
  var import_jsx_runtime = require("react/jsx-runtime");
41
- var monthNames = [
42
- "Th\xE1ng 1",
43
- "Th\xE1ng 2",
44
- "Th\xE1ng 3",
45
- "Th\xE1ng 4",
46
- "Th\xE1ng 5",
47
- "Th\xE1ng 6",
48
- "Th\xE1ng 7",
49
- "Th\xE1ng 8",
50
- "Th\xE1ng 9",
51
- "Th\xE1ng 10",
52
- "Th\xE1ng 11",
53
- "Th\xE1ng 12"
54
- ];
55
- var weekDayLabelsByLocale = {
56
- vi: ["CN", "T2", "T3", "T4", "T5", "T6", "T7"],
57
- en: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
58
- ja: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
59
- ko: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
60
- };
61
42
  var Calendar = ({
62
43
  month = (/* @__PURE__ */ new Date()).getMonth() + 1,
63
44
  year = (/* @__PURE__ */ new Date()).getFullYear(),
45
+ selectedDate: selectedDateProp,
64
46
  onSelect,
65
47
  showLunar = true,
66
48
  locale = "vi",
@@ -71,66 +53,20 @@ var Calendar = ({
71
53
  }) => {
72
54
  const [currentMonth, setCurrentMonth] = (0, import_react.useState)(month);
73
55
  const [currentYear, setCurrentYear] = (0, import_react.useState)(year);
74
- const [selectedDate, setSelectedDate] = (0, import_react.useState)(null);
56
+ const [internalSelectedDate, setInternalSelectedDate] = (0, import_react.useState)(null);
57
+ const selectedDate = selectedDateProp !== void 0 ? selectedDateProp : internalSelectedDate;
58
+ (0, import_react.useEffect)(() => {
59
+ setCurrentMonth(month);
60
+ }, [month]);
61
+ (0, import_react.useEffect)(() => {
62
+ setCurrentYear(year);
63
+ }, [year]);
75
64
  const yearInfo = (0, import_react.useMemo)(() => (0, import_core.getYearDetails)(currentYear), [currentYear]);
76
- const weekDayLabels = weekDayLabelsByLocale[locale];
77
- const calendarGrid = (0, import_react.useMemo)(() => {
78
- const grid = [];
79
- const firstDay = new Date(currentYear, currentMonth - 1, 1);
80
- const lastDay = new Date(currentYear, currentMonth, 0);
81
- const startingDayOfWeek = firstDay.getDay();
82
- const daysInMonth = lastDay.getDate();
83
- const today = /* @__PURE__ */ new Date();
84
- const todayStr = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`;
85
- let selStr = "";
86
- if (selectedDate) {
87
- selStr = `${selectedDate.getFullYear()}-${selectedDate.getMonth() + 1}-${selectedDate.getDate()}`;
88
- }
89
- const prevMonthLastDay = new Date(currentYear, currentMonth - 1, 0).getDate();
90
- for (let i = startingDayOfWeek - 1; i >= 0; i--) {
91
- const d = prevMonthLastDay - i;
92
- const m = currentMonth - 1 < 1 ? 12 : currentMonth - 1;
93
- const y = currentMonth - 1 < 1 ? currentYear - 1 : currentYear;
94
- const solar = new Date(y, m - 1, d);
95
- const lunar = import_core.LichTa.toLunar(d, m, y);
96
- const dateStr = `${y}-${m}-${d}`;
97
- grid.push({
98
- solar,
99
- lunar,
100
- isToday: dateStr === todayStr,
101
- isSelected: dateStr === selStr,
102
- isCurrentMonth: false
103
- });
104
- }
105
- for (let d = 1; d <= daysInMonth; d++) {
106
- const solar = new Date(currentYear, currentMonth - 1, d);
107
- const lunar = import_core.LichTa.toLunar(d, currentMonth, currentYear);
108
- const dateStr = `${currentYear}-${currentMonth}-${d}`;
109
- grid.push({
110
- solar,
111
- lunar,
112
- isToday: dateStr === todayStr,
113
- isSelected: dateStr === selStr,
114
- isCurrentMonth: true
115
- });
116
- }
117
- const remaining = 42 - grid.length;
118
- for (let d = 1; d <= remaining; d++) {
119
- const m = currentMonth + 1 > 12 ? 1 : currentMonth + 1;
120
- const y = currentMonth + 1 > 12 ? currentYear + 1 : currentYear;
121
- const solar = new Date(y, m - 1, d);
122
- const lunar = import_core.LichTa.toLunar(d, m, y);
123
- const dateStr = `${y}-${m}-${d}`;
124
- grid.push({
125
- solar,
126
- lunar,
127
- isToday: dateStr === todayStr,
128
- isSelected: dateStr === selStr,
129
- isCurrentMonth: false
130
- });
131
- }
132
- return grid;
133
- }, [currentMonth, currentYear, selectedDate]);
65
+ const { weekDays: weekDayLabels, solarMonthNames: monthNames } = (0, import_core.t)(locale);
66
+ const calendarGrid = (0, import_react.useMemo)(
67
+ () => (0, import_core.getCalendarGrid)(currentMonth, currentYear, selectedDate),
68
+ [currentMonth, currentYear, selectedDate]
69
+ );
134
70
  const prevMonth = () => {
135
71
  if (currentMonth === 1) {
136
72
  setCurrentMonth(12);
@@ -148,7 +84,9 @@ var Calendar = ({
148
84
  }
149
85
  };
150
86
  const handleDayClick = (cell) => {
151
- setSelectedDate(cell.solar);
87
+ if (selectedDateProp === void 0) {
88
+ setInternalSelectedDate(cell.solar);
89
+ }
152
90
  if (onSelect) {
153
91
  onSelect(cell.solar, cell.lunar);
154
92
  }
@@ -204,7 +142,162 @@ var Calendar = ({
204
142
  children && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lich-ta-calendar-footer", children })
205
143
  ] });
206
144
  };
145
+
146
+ // src/DatePicker.tsx
147
+ var import_react2 = require("react");
148
+ var import_core2 = require("@lichta/core");
149
+ var import_jsx_runtime2 = require("react/jsx-runtime");
150
+ function defaultFormat(date) {
151
+ const dd = String(date.getDate()).padStart(2, "0");
152
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
153
+ return `${dd}/${mm}/${date.getFullYear()}`;
154
+ }
155
+ var DatePicker = ({
156
+ value = null,
157
+ onSelect,
158
+ placeholder = "Ch\u1ECDn ng\xE0y",
159
+ locale = "vi",
160
+ theme = "classic",
161
+ showLunar = true,
162
+ format = defaultFormat,
163
+ disabled = false,
164
+ className = ""
165
+ }) => {
166
+ const [selected, setSelected] = (0, import_react2.useState)(value);
167
+ const [isOpen, setIsOpen] = (0, import_react2.useState)(false);
168
+ const anchor = selected ?? /* @__PURE__ */ new Date();
169
+ const [viewMonth, setViewMonth] = (0, import_react2.useState)(anchor.getMonth() + 1);
170
+ const [viewYear, setViewYear] = (0, import_react2.useState)(anchor.getFullYear());
171
+ const rootRef = (0, import_react2.useRef)(null);
172
+ (0, import_react2.useEffect)(() => {
173
+ const next = value ?? null;
174
+ setSelected(next);
175
+ if (next) {
176
+ setViewMonth(next.getMonth() + 1);
177
+ setViewYear(next.getFullYear());
178
+ }
179
+ }, [value]);
180
+ (0, import_react2.useEffect)(() => {
181
+ if (!isOpen) return;
182
+ function handlePointerDown(event) {
183
+ if (rootRef.current && !rootRef.current.contains(event.target)) {
184
+ setIsOpen(false);
185
+ }
186
+ }
187
+ function handleKeydown(event) {
188
+ if (event.key === "Escape") setIsOpen(false);
189
+ }
190
+ document.addEventListener("mousedown", handlePointerDown);
191
+ document.addEventListener("keydown", handleKeydown);
192
+ return () => {
193
+ document.removeEventListener("mousedown", handlePointerDown);
194
+ document.removeEventListener("keydown", handleKeydown);
195
+ };
196
+ }, [isOpen]);
197
+ const yearInfo = (0, import_react2.useMemo)(() => (0, import_core2.getYearDetails)(viewYear), [viewYear]);
198
+ const { weekDays: weekDayLabels, solarMonthNames: monthNames } = (0, import_core2.t)(locale);
199
+ const grid = (0, import_react2.useMemo)(
200
+ () => (0, import_core2.getCalendarGrid)(viewMonth, viewYear, selected),
201
+ [viewMonth, viewYear, selected]
202
+ );
203
+ function prevMonth() {
204
+ if (viewMonth === 1) {
205
+ setViewMonth(12);
206
+ setViewYear((y) => y - 1);
207
+ } else {
208
+ setViewMonth((m) => m - 1);
209
+ }
210
+ }
211
+ function nextMonth() {
212
+ if (viewMonth === 12) {
213
+ setViewMonth(1);
214
+ setViewYear((y) => y + 1);
215
+ } else {
216
+ setViewMonth((m) => m + 1);
217
+ }
218
+ }
219
+ function handleDaySelect(cell) {
220
+ setSelected(cell.solar);
221
+ setIsOpen(false);
222
+ onSelect?.(cell.solar, cell.lunar);
223
+ }
224
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ref: rootRef, className: `lich-ta-datepicker lichta-theme-${theme} ${className}`, children: [
225
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
226
+ "input",
227
+ {
228
+ type: "text",
229
+ className: "lich-ta-datepicker-input",
230
+ readOnly: true,
231
+ disabled,
232
+ placeholder,
233
+ value: selected ? format(selected) : "",
234
+ "aria-haspopup": "dialog",
235
+ "aria-expanded": isOpen,
236
+ onClick: () => !disabled && setIsOpen((open) => !open)
237
+ }
238
+ ),
239
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lich-ta-datepicker-popover", role: "dialog", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lich-ta-datepicker-calendar", children: [
240
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lich-ta-datepicker-calendar-header", children: [
241
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
242
+ "button",
243
+ {
244
+ type: "button",
245
+ className: "lich-ta-datepicker-calendar-nav",
246
+ onClick: prevMonth,
247
+ "aria-label": "Th\xE1ng tr\u01B0\u1EDBc",
248
+ children: "\u25C0"
249
+ }
250
+ ),
251
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lich-ta-datepicker-calendar-title", children: [
252
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "lich-ta-datepicker-calendar-month-year", children: [
253
+ monthNames[viewMonth - 1],
254
+ ", ",
255
+ viewYear
256
+ ] }),
257
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "lich-ta-datepicker-calendar-canchi", children: [
258
+ yearInfo.can,
259
+ " ",
260
+ yearInfo.chi
261
+ ] })
262
+ ] }),
263
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
264
+ "button",
265
+ {
266
+ type: "button",
267
+ className: "lich-ta-datepicker-calendar-nav",
268
+ onClick: nextMonth,
269
+ "aria-label": "Th\xE1ng sau",
270
+ children: "\u25B6"
271
+ }
272
+ )
273
+ ] }),
274
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lich-ta-datepicker-calendar-weekdays", children: weekDayLabels.map((label) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lich-ta-datepicker-calendar-weekday", children: label }, label)) }),
275
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lich-ta-datepicker-calendar-grid", children: grid.map((cell, idx) => {
276
+ let classes = "lich-ta-datepicker-calendar-day";
277
+ if (cell.isToday) classes += " is-today";
278
+ if (cell.isSelected) classes += " is-selected";
279
+ if (!cell.isCurrentMonth) classes += " is-other-month";
280
+ if (cell.lunar.day === 1) classes += " is-first-lunar";
281
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
282
+ "button",
283
+ {
284
+ type: "button",
285
+ className: classes,
286
+ onClick: () => handleDaySelect(cell),
287
+ tabIndex: cell.isCurrentMonth ? 0 : -1,
288
+ children: [
289
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "solar-day", children: cell.solar.getDate() }),
290
+ showLunar && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "lunar-day", children: cell.lunar.day === 1 ? `${cell.lunar.day}/${cell.lunar.month}${cell.lunar.isLeap ? "*" : ""}` : cell.lunar.day })
291
+ ]
292
+ },
293
+ idx
294
+ );
295
+ }) })
296
+ ] }) })
297
+ ] });
298
+ };
207
299
  // Annotate the CommonJS export names for ESM import in node:
208
300
  0 && (module.exports = {
209
- Calendar
301
+ Calendar,
302
+ DatePicker
210
303
  });
package/dist/index.mjs CHANGED
@@ -1,30 +1,11 @@
1
1
  // src/Calendar.tsx
2
- import React, { useState, useMemo } from "react";
3
- import { LichTa, getYearDetails } from "@lichta/core";
2
+ import React, { useState, useMemo, useEffect } from "react";
3
+ import { getYearDetails, getCalendarGrid, t } from "@lichta/core";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
- var monthNames = [
6
- "Th\xE1ng 1",
7
- "Th\xE1ng 2",
8
- "Th\xE1ng 3",
9
- "Th\xE1ng 4",
10
- "Th\xE1ng 5",
11
- "Th\xE1ng 6",
12
- "Th\xE1ng 7",
13
- "Th\xE1ng 8",
14
- "Th\xE1ng 9",
15
- "Th\xE1ng 10",
16
- "Th\xE1ng 11",
17
- "Th\xE1ng 12"
18
- ];
19
- var weekDayLabelsByLocale = {
20
- vi: ["CN", "T2", "T3", "T4", "T5", "T6", "T7"],
21
- en: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
22
- ja: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
23
- ko: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
24
- };
25
5
  var Calendar = ({
26
6
  month = (/* @__PURE__ */ new Date()).getMonth() + 1,
27
7
  year = (/* @__PURE__ */ new Date()).getFullYear(),
8
+ selectedDate: selectedDateProp,
28
9
  onSelect,
29
10
  showLunar = true,
30
11
  locale = "vi",
@@ -35,66 +16,20 @@ var Calendar = ({
35
16
  }) => {
36
17
  const [currentMonth, setCurrentMonth] = useState(month);
37
18
  const [currentYear, setCurrentYear] = useState(year);
38
- const [selectedDate, setSelectedDate] = useState(null);
19
+ const [internalSelectedDate, setInternalSelectedDate] = useState(null);
20
+ const selectedDate = selectedDateProp !== void 0 ? selectedDateProp : internalSelectedDate;
21
+ useEffect(() => {
22
+ setCurrentMonth(month);
23
+ }, [month]);
24
+ useEffect(() => {
25
+ setCurrentYear(year);
26
+ }, [year]);
39
27
  const yearInfo = useMemo(() => getYearDetails(currentYear), [currentYear]);
40
- const weekDayLabels = weekDayLabelsByLocale[locale];
41
- const calendarGrid = useMemo(() => {
42
- const grid = [];
43
- const firstDay = new Date(currentYear, currentMonth - 1, 1);
44
- const lastDay = new Date(currentYear, currentMonth, 0);
45
- const startingDayOfWeek = firstDay.getDay();
46
- const daysInMonth = lastDay.getDate();
47
- const today = /* @__PURE__ */ new Date();
48
- const todayStr = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`;
49
- let selStr = "";
50
- if (selectedDate) {
51
- selStr = `${selectedDate.getFullYear()}-${selectedDate.getMonth() + 1}-${selectedDate.getDate()}`;
52
- }
53
- const prevMonthLastDay = new Date(currentYear, currentMonth - 1, 0).getDate();
54
- for (let i = startingDayOfWeek - 1; i >= 0; i--) {
55
- const d = prevMonthLastDay - i;
56
- const m = currentMonth - 1 < 1 ? 12 : currentMonth - 1;
57
- const y = currentMonth - 1 < 1 ? currentYear - 1 : currentYear;
58
- const solar = new Date(y, m - 1, d);
59
- const lunar = LichTa.toLunar(d, m, y);
60
- const dateStr = `${y}-${m}-${d}`;
61
- grid.push({
62
- solar,
63
- lunar,
64
- isToday: dateStr === todayStr,
65
- isSelected: dateStr === selStr,
66
- isCurrentMonth: false
67
- });
68
- }
69
- for (let d = 1; d <= daysInMonth; d++) {
70
- const solar = new Date(currentYear, currentMonth - 1, d);
71
- const lunar = LichTa.toLunar(d, currentMonth, currentYear);
72
- const dateStr = `${currentYear}-${currentMonth}-${d}`;
73
- grid.push({
74
- solar,
75
- lunar,
76
- isToday: dateStr === todayStr,
77
- isSelected: dateStr === selStr,
78
- isCurrentMonth: true
79
- });
80
- }
81
- const remaining = 42 - grid.length;
82
- for (let d = 1; d <= remaining; d++) {
83
- const m = currentMonth + 1 > 12 ? 1 : currentMonth + 1;
84
- const y = currentMonth + 1 > 12 ? currentYear + 1 : currentYear;
85
- const solar = new Date(y, m - 1, d);
86
- const lunar = LichTa.toLunar(d, m, y);
87
- const dateStr = `${y}-${m}-${d}`;
88
- grid.push({
89
- solar,
90
- lunar,
91
- isToday: dateStr === todayStr,
92
- isSelected: dateStr === selStr,
93
- isCurrentMonth: false
94
- });
95
- }
96
- return grid;
97
- }, [currentMonth, currentYear, selectedDate]);
28
+ const { weekDays: weekDayLabels, solarMonthNames: monthNames } = t(locale);
29
+ const calendarGrid = useMemo(
30
+ () => getCalendarGrid(currentMonth, currentYear, selectedDate),
31
+ [currentMonth, currentYear, selectedDate]
32
+ );
98
33
  const prevMonth = () => {
99
34
  if (currentMonth === 1) {
100
35
  setCurrentMonth(12);
@@ -112,7 +47,9 @@ var Calendar = ({
112
47
  }
113
48
  };
114
49
  const handleDayClick = (cell) => {
115
- setSelectedDate(cell.solar);
50
+ if (selectedDateProp === void 0) {
51
+ setInternalSelectedDate(cell.solar);
52
+ }
116
53
  if (onSelect) {
117
54
  onSelect(cell.solar, cell.lunar);
118
55
  }
@@ -168,6 +105,161 @@ var Calendar = ({
168
105
  children && /* @__PURE__ */ jsx("div", { className: "lich-ta-calendar-footer", children })
169
106
  ] });
170
107
  };
108
+
109
+ // src/DatePicker.tsx
110
+ import { useState as useState2, useRef, useEffect as useEffect2, useMemo as useMemo2 } from "react";
111
+ import { getYearDetails as getYearDetails2, getCalendarGrid as getCalendarGrid2, t as t2 } from "@lichta/core";
112
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
113
+ function defaultFormat(date) {
114
+ const dd = String(date.getDate()).padStart(2, "0");
115
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
116
+ return `${dd}/${mm}/${date.getFullYear()}`;
117
+ }
118
+ var DatePicker = ({
119
+ value = null,
120
+ onSelect,
121
+ placeholder = "Ch\u1ECDn ng\xE0y",
122
+ locale = "vi",
123
+ theme = "classic",
124
+ showLunar = true,
125
+ format = defaultFormat,
126
+ disabled = false,
127
+ className = ""
128
+ }) => {
129
+ const [selected, setSelected] = useState2(value);
130
+ const [isOpen, setIsOpen] = useState2(false);
131
+ const anchor = selected ?? /* @__PURE__ */ new Date();
132
+ const [viewMonth, setViewMonth] = useState2(anchor.getMonth() + 1);
133
+ const [viewYear, setViewYear] = useState2(anchor.getFullYear());
134
+ const rootRef = useRef(null);
135
+ useEffect2(() => {
136
+ const next = value ?? null;
137
+ setSelected(next);
138
+ if (next) {
139
+ setViewMonth(next.getMonth() + 1);
140
+ setViewYear(next.getFullYear());
141
+ }
142
+ }, [value]);
143
+ useEffect2(() => {
144
+ if (!isOpen) return;
145
+ function handlePointerDown(event) {
146
+ if (rootRef.current && !rootRef.current.contains(event.target)) {
147
+ setIsOpen(false);
148
+ }
149
+ }
150
+ function handleKeydown(event) {
151
+ if (event.key === "Escape") setIsOpen(false);
152
+ }
153
+ document.addEventListener("mousedown", handlePointerDown);
154
+ document.addEventListener("keydown", handleKeydown);
155
+ return () => {
156
+ document.removeEventListener("mousedown", handlePointerDown);
157
+ document.removeEventListener("keydown", handleKeydown);
158
+ };
159
+ }, [isOpen]);
160
+ const yearInfo = useMemo2(() => getYearDetails2(viewYear), [viewYear]);
161
+ const { weekDays: weekDayLabels, solarMonthNames: monthNames } = t2(locale);
162
+ const grid = useMemo2(
163
+ () => getCalendarGrid2(viewMonth, viewYear, selected),
164
+ [viewMonth, viewYear, selected]
165
+ );
166
+ function prevMonth() {
167
+ if (viewMonth === 1) {
168
+ setViewMonth(12);
169
+ setViewYear((y) => y - 1);
170
+ } else {
171
+ setViewMonth((m) => m - 1);
172
+ }
173
+ }
174
+ function nextMonth() {
175
+ if (viewMonth === 12) {
176
+ setViewMonth(1);
177
+ setViewYear((y) => y + 1);
178
+ } else {
179
+ setViewMonth((m) => m + 1);
180
+ }
181
+ }
182
+ function handleDaySelect(cell) {
183
+ setSelected(cell.solar);
184
+ setIsOpen(false);
185
+ onSelect?.(cell.solar, cell.lunar);
186
+ }
187
+ return /* @__PURE__ */ jsxs2("div", { ref: rootRef, className: `lich-ta-datepicker lichta-theme-${theme} ${className}`, children: [
188
+ /* @__PURE__ */ jsx2(
189
+ "input",
190
+ {
191
+ type: "text",
192
+ className: "lich-ta-datepicker-input",
193
+ readOnly: true,
194
+ disabled,
195
+ placeholder,
196
+ value: selected ? format(selected) : "",
197
+ "aria-haspopup": "dialog",
198
+ "aria-expanded": isOpen,
199
+ onClick: () => !disabled && setIsOpen((open) => !open)
200
+ }
201
+ ),
202
+ isOpen && /* @__PURE__ */ jsx2("div", { className: "lich-ta-datepicker-popover", role: "dialog", children: /* @__PURE__ */ jsxs2("div", { className: "lich-ta-datepicker-calendar", children: [
203
+ /* @__PURE__ */ jsxs2("div", { className: "lich-ta-datepicker-calendar-header", children: [
204
+ /* @__PURE__ */ jsx2(
205
+ "button",
206
+ {
207
+ type: "button",
208
+ className: "lich-ta-datepicker-calendar-nav",
209
+ onClick: prevMonth,
210
+ "aria-label": "Th\xE1ng tr\u01B0\u1EDBc",
211
+ children: "\u25C0"
212
+ }
213
+ ),
214
+ /* @__PURE__ */ jsxs2("div", { className: "lich-ta-datepicker-calendar-title", children: [
215
+ /* @__PURE__ */ jsxs2("span", { className: "lich-ta-datepicker-calendar-month-year", children: [
216
+ monthNames[viewMonth - 1],
217
+ ", ",
218
+ viewYear
219
+ ] }),
220
+ /* @__PURE__ */ jsxs2("span", { className: "lich-ta-datepicker-calendar-canchi", children: [
221
+ yearInfo.can,
222
+ " ",
223
+ yearInfo.chi
224
+ ] })
225
+ ] }),
226
+ /* @__PURE__ */ jsx2(
227
+ "button",
228
+ {
229
+ type: "button",
230
+ className: "lich-ta-datepicker-calendar-nav",
231
+ onClick: nextMonth,
232
+ "aria-label": "Th\xE1ng sau",
233
+ children: "\u25B6"
234
+ }
235
+ )
236
+ ] }),
237
+ /* @__PURE__ */ jsx2("div", { className: "lich-ta-datepicker-calendar-weekdays", children: weekDayLabels.map((label) => /* @__PURE__ */ jsx2("div", { className: "lich-ta-datepicker-calendar-weekday", children: label }, label)) }),
238
+ /* @__PURE__ */ jsx2("div", { className: "lich-ta-datepicker-calendar-grid", children: grid.map((cell, idx) => {
239
+ let classes = "lich-ta-datepicker-calendar-day";
240
+ if (cell.isToday) classes += " is-today";
241
+ if (cell.isSelected) classes += " is-selected";
242
+ if (!cell.isCurrentMonth) classes += " is-other-month";
243
+ if (cell.lunar.day === 1) classes += " is-first-lunar";
244
+ return /* @__PURE__ */ jsxs2(
245
+ "button",
246
+ {
247
+ type: "button",
248
+ className: classes,
249
+ onClick: () => handleDaySelect(cell),
250
+ tabIndex: cell.isCurrentMonth ? 0 : -1,
251
+ children: [
252
+ /* @__PURE__ */ jsx2("span", { className: "solar-day", children: cell.solar.getDate() }),
253
+ showLunar && /* @__PURE__ */ jsx2("span", { className: "lunar-day", children: cell.lunar.day === 1 ? `${cell.lunar.day}/${cell.lunar.month}${cell.lunar.isLeap ? "*" : ""}` : cell.lunar.day })
254
+ ]
255
+ },
256
+ idx
257
+ );
258
+ }) })
259
+ ] }) })
260
+ ] });
261
+ };
171
262
  export {
172
- Calendar
263
+ Calendar,
264
+ DatePicker
173
265
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lichta/react",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "React components for LichTa",
5
5
  "author": "Zeforc Labs | Stridev",
6
6
  "license": "MIT",
@@ -12,7 +12,7 @@
12
12
  "LICENSE"
13
13
  ],
14
14
  "dependencies": {
15
- "@lichta/core": "2.0.1"
15
+ "@lichta/core": "2.1.0"
16
16
  },
17
17
  "peerDependencies": {
18
18
  "react": ">=18.0.0",