@nuvui/date-picker 0.0.0-stage → 0.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -2
  3. package/dist/components/calendar/calendar.cjs +163 -0
  4. package/dist/components/calendar/calendar.d.cts +17 -0
  5. package/dist/components/calendar/calendar.d.ts +17 -0
  6. package/dist/components/calendar/calendar.js +163 -0
  7. package/dist/components/calendar/index.cjs +3 -0
  8. package/dist/components/calendar/index.d.cts +2 -0
  9. package/dist/components/calendar/index.d.ts +2 -0
  10. package/dist/components/calendar/index.js +2 -0
  11. package/dist/components/date-picker/date-picker.cjs +340 -0
  12. package/dist/components/date-picker/date-picker.d.cts +169 -0
  13. package/dist/components/date-picker/date-picker.d.ts +169 -0
  14. package/dist/components/date-picker/date-picker.js +339 -0
  15. package/dist/components/date-picker/index.cjs +4 -0
  16. package/dist/components/date-picker/index.d.cts +2 -0
  17. package/dist/components/date-picker/index.d.ts +2 -0
  18. package/dist/components/date-picker/index.js +2 -0
  19. package/dist/components/date-picker/popup.cjs +43 -0
  20. package/dist/components/date-picker/popup.js +43 -0
  21. package/dist/css/calendar.css +291 -0
  22. package/dist/css/date-picker.css +24 -0
  23. package/dist/index.cjs +13 -0
  24. package/dist/index.d.cts +6 -0
  25. package/dist/index.d.ts +6 -0
  26. package/dist/index.js +4 -0
  27. package/dist/locale.cjs +9 -0
  28. package/dist/locale.d.cts +1 -0
  29. package/dist/locale.d.ts +1 -0
  30. package/dist/locale.js +1 -0
  31. package/dist/styles.css +314 -0
  32. package/dist/utils/cx.cjs +6 -0
  33. package/dist/utils/cx.js +6 -0
  34. package/dist/utils/dates.cjs +59 -0
  35. package/dist/utils/dates.js +54 -0
  36. package/dist/utils/use-controlled.cjs +31 -0
  37. package/dist/utils/use-controlled.js +31 -0
  38. package/dist/utils/use-date-text.cjs +83 -0
  39. package/dist/utils/use-date-text.js +83 -0
  40. package/dist/utils/use-is-phone.cjs +18 -0
  41. package/dist/utils/use-is-phone.js +18 -0
  42. package/package.json +118 -3
@@ -0,0 +1,59 @@
1
+ let _date_fns_tz = require("@date-fns/tz");
2
+ let react_day_picker = require("react-day-picker");
3
+ let date_fns = require("date-fns");
4
+ let date_fns_locale_en_US = require("date-fns/locale/en-US");
5
+ //#region src/utils/dates.ts
6
+ function inZone(date, timeZone) {
7
+ if (!timeZone) return date;
8
+ if (date instanceof _date_fns_tz.TZDate && date.timeZone === timeZone) return date;
9
+ return new _date_fns_tz.TZDate(date, timeZone);
10
+ }
11
+ /**
12
+ * A date as `yyyy-MM-dd`, which is what a form submits. `toISOString` would
13
+ * be wrong here: it gives the day in UTC, which is the day before or after
14
+ * for anyone far enough from Greenwich.
15
+ */
16
+ function isoDate(date, timeZone) {
17
+ return date ? (0, date_fns.format)(inZone(date, timeZone), "yyyy-MM-dd") : "";
18
+ }
19
+ /** The pattern a locale writes a short date in, such as `MM/dd/yyyy`. */
20
+ function shortPattern(locale) {
21
+ return (locale ?? date_fns_locale_en_US.enUS).formatLong.date({ width: "short" });
22
+ }
23
+ function formatDate(date, pattern, locale, timeZone) {
24
+ return (0, date_fns.format)(inZone(date, timeZone), pattern, { locale });
25
+ }
26
+ /**
27
+ * Reads a date someone typed. Gives `undefined` for text that isn't a date
28
+ * in the pattern, a day that doesn't exist included.
29
+ */
30
+ function parseDate(text, pattern, locale, timeZone) {
31
+ const typed = text.trim();
32
+ if (!typed) return void 0;
33
+ const midnight = (0, date_fns.startOfDay)(timeZone ? _date_fns_tz.TZDate.tz(timeZone) : /* @__PURE__ */ new Date());
34
+ const date = (0, date_fns.parse)(typed, pattern, midnight, { locale });
35
+ if (!(0, date_fns.isValid)(date)) return void 0;
36
+ if (date.getFullYear() < 100 && !/\d{3}/.test(typed)) {
37
+ const short = (pattern === "P" ? shortPattern(locale) : pattern).replace(/y+/, "yy");
38
+ const again = (0, date_fns.parse)(typed, short, midnight, { locale });
39
+ return (0, date_fns.isValid)(again) ? again : void 0;
40
+ }
41
+ return date;
42
+ }
43
+ function disabledDays(min, max, disabledDates, timeZone) {
44
+ return [
45
+ ...min ? [{ before: inZone(min, timeZone) }] : [],
46
+ ...max ? [{ after: inZone(max, timeZone) }] : [],
47
+ ...Array.isArray(disabledDates) ? disabledDates : disabledDates === void 0 ? [] : [disabledDates]
48
+ ];
49
+ }
50
+ function isDisabled(date, matchers) {
51
+ return matchers.length > 0 && (0, react_day_picker.dateMatchModifiers)(date, matchers);
52
+ }
53
+ //#endregion
54
+ exports.disabledDays = disabledDays;
55
+ exports.formatDate = formatDate;
56
+ exports.isDisabled = isDisabled;
57
+ exports.isoDate = isoDate;
58
+ exports.parseDate = parseDate;
59
+ exports.shortPattern = shortPattern;
@@ -0,0 +1,54 @@
1
+ import { TZDate } from "@date-fns/tz";
2
+ import { dateMatchModifiers } from "react-day-picker";
3
+ import { format, isValid, parse, startOfDay } from "date-fns";
4
+ import { enUS } from "date-fns/locale/en-US";
5
+ //#region src/utils/dates.ts
6
+ function inZone(date, timeZone) {
7
+ if (!timeZone) return date;
8
+ if (date instanceof TZDate && date.timeZone === timeZone) return date;
9
+ return new TZDate(date, timeZone);
10
+ }
11
+ /**
12
+ * A date as `yyyy-MM-dd`, which is what a form submits. `toISOString` would
13
+ * be wrong here: it gives the day in UTC, which is the day before or after
14
+ * for anyone far enough from Greenwich.
15
+ */
16
+ function isoDate(date, timeZone) {
17
+ return date ? format(inZone(date, timeZone), "yyyy-MM-dd") : "";
18
+ }
19
+ /** The pattern a locale writes a short date in, such as `MM/dd/yyyy`. */
20
+ function shortPattern(locale) {
21
+ return (locale ?? enUS).formatLong.date({ width: "short" });
22
+ }
23
+ function formatDate(date, pattern, locale, timeZone) {
24
+ return format(inZone(date, timeZone), pattern, { locale });
25
+ }
26
+ /**
27
+ * Reads a date someone typed. Gives `undefined` for text that isn't a date
28
+ * in the pattern, a day that doesn't exist included.
29
+ */
30
+ function parseDate(text, pattern, locale, timeZone) {
31
+ const typed = text.trim();
32
+ if (!typed) return void 0;
33
+ const midnight = startOfDay(timeZone ? TZDate.tz(timeZone) : /* @__PURE__ */ new Date());
34
+ const date = parse(typed, pattern, midnight, { locale });
35
+ if (!isValid(date)) return void 0;
36
+ if (date.getFullYear() < 100 && !/\d{3}/.test(typed)) {
37
+ const short = (pattern === "P" ? shortPattern(locale) : pattern).replace(/y+/, "yy");
38
+ const again = parse(typed, short, midnight, { locale });
39
+ return isValid(again) ? again : void 0;
40
+ }
41
+ return date;
42
+ }
43
+ function disabledDays(min, max, disabledDates, timeZone) {
44
+ return [
45
+ ...min ? [{ before: inZone(min, timeZone) }] : [],
46
+ ...max ? [{ after: inZone(max, timeZone) }] : [],
47
+ ...Array.isArray(disabledDates) ? disabledDates : disabledDates === void 0 ? [] : [disabledDates]
48
+ ];
49
+ }
50
+ function isDisabled(date, matchers) {
51
+ return matchers.length > 0 && dateMatchModifiers(date, matchers);
52
+ }
53
+ //#endregion
54
+ export { disabledDays, formatDate, isDisabled, isoDate, parseDate, shortPattern };
@@ -0,0 +1,31 @@
1
+ let react = require("react");
2
+ //#region src/utils/use-controlled.ts
3
+ /**
4
+ * State that the parent may hold. With `value` passed, that's the state and
5
+ * `onChange` is only told what was asked for. Without it, the state is kept
6
+ * here, starting from `defaultValue`.
7
+ *
8
+ * `value` being `undefined` can't be what says the parent holds the state,
9
+ * because for a date `undefined` is a value: no date picked. So the caller
10
+ * says it outright with `controlled`.
11
+ */
12
+ function useControlled(controlled, value, defaultValue, onChange) {
13
+ const [inner, setInner] = (0, react.useState)(defaultValue);
14
+ const current = controlled ? value : inner;
15
+ const latest = (0, react.useRef)({
16
+ current,
17
+ controlled,
18
+ onChange
19
+ });
20
+ latest.current = {
21
+ current,
22
+ controlled,
23
+ onChange
24
+ };
25
+ return [current, (0, react.useCallback)((next) => {
26
+ if (!latest.current.controlled) setInner(next);
27
+ latest.current.onChange?.(next);
28
+ }, [])];
29
+ }
30
+ //#endregion
31
+ exports.useControlled = useControlled;
@@ -0,0 +1,31 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ //#region src/utils/use-controlled.ts
3
+ /**
4
+ * State that the parent may hold. With `value` passed, that's the state and
5
+ * `onChange` is only told what was asked for. Without it, the state is kept
6
+ * here, starting from `defaultValue`.
7
+ *
8
+ * `value` being `undefined` can't be what says the parent holds the state,
9
+ * because for a date `undefined` is a value: no date picked. So the caller
10
+ * says it outright with `controlled`.
11
+ */
12
+ function useControlled(controlled, value, defaultValue, onChange) {
13
+ const [inner, setInner] = useState(defaultValue);
14
+ const current = controlled ? value : inner;
15
+ const latest = useRef({
16
+ current,
17
+ controlled,
18
+ onChange
19
+ });
20
+ latest.current = {
21
+ current,
22
+ controlled,
23
+ onChange
24
+ };
25
+ return [current, useCallback((next) => {
26
+ if (!latest.current.controlled) setInner(next);
27
+ latest.current.onChange?.(next);
28
+ }, [])];
29
+ }
30
+ //#endregion
31
+ export { useControlled };
@@ -0,0 +1,83 @@
1
+ const require_dates = require("./dates.cjs");
2
+ let react = require("react");
3
+ let react_dom = require("react-dom");
4
+ //#region src/utils/use-date-text.ts
5
+ /**
6
+ * The text of a field a date can be typed into.
7
+ *
8
+ * While someone types, the text is theirs: a half-written date is left
9
+ * alone. It becomes the field's date the moment it reads as one in full,
10
+ * and again when they leave the field or press Enter, where a looser form
11
+ * such as "1/5/26" is taken too and rewritten in the pattern. Text that
12
+ * isn't a date by then stays as typed, the date is cleared, and the input
13
+ * is marked invalid, with the browser's own validation told why.
14
+ */
15
+ function useDateText({ value, onCommit, problem, invalidMessage, pattern, locale, timeZone, input }) {
16
+ const [draft, setDraft] = (0, react.useState)(null);
17
+ const [error, setErrorState] = (0, react.useState)(void 0);
18
+ function setError(message) {
19
+ setErrorState(message);
20
+ input.current?.setCustomValidity(message ?? "");
21
+ }
22
+ const formatted = value ? require_dates.formatDate(value, pattern, locale, timeZone) : "";
23
+ const text = draft ?? formatted;
24
+ const day = require_dates.isoDate(value ?? void 0, timeZone);
25
+ const typedDay = (0, react.useRef)(day);
26
+ (0, react.useEffect)(() => {
27
+ if (day && day !== typedDay.current) {
28
+ setDraft(null);
29
+ setErrorState(void 0);
30
+ input.current?.setCustomValidity("");
31
+ }
32
+ typedDay.current = day;
33
+ }, [day, input]);
34
+ function read(typed) {
35
+ if (!typed.trim()) return { date: null };
36
+ const date = require_dates.parseDate(typed, pattern, locale, timeZone);
37
+ if (!date) return {
38
+ date: null,
39
+ error: invalidMessage
40
+ };
41
+ const reason = problem(date);
42
+ return reason ? {
43
+ date: null,
44
+ error: reason
45
+ } : { date };
46
+ }
47
+ function commit(date) {
48
+ typedDay.current = require_dates.isoDate(date ?? void 0, timeZone);
49
+ if (typedDay.current !== day) onCommit(date);
50
+ }
51
+ function onChange(event) {
52
+ const typed = event.target.value;
53
+ setDraft(typed);
54
+ setError(void 0);
55
+ const result = read(typed);
56
+ if (!typed.trim()) commit(null);
57
+ else if (result.date && require_dates.formatDate(result.date, pattern, locale, timeZone) === typed) commit(result.date);
58
+ }
59
+ function settle() {
60
+ if (draft === null) return;
61
+ const result = read(draft);
62
+ setError(result.error);
63
+ setDraft(result.error ? draft : null);
64
+ commit(result.date);
65
+ }
66
+ function onKeyDown(event) {
67
+ if (event.key === "Enter") (0, react_dom.flushSync)(settle);
68
+ }
69
+ function clearDraft() {
70
+ setDraft(null);
71
+ setError(void 0);
72
+ }
73
+ return {
74
+ text,
75
+ error,
76
+ onChange,
77
+ onBlur: settle,
78
+ onKeyDown,
79
+ clearDraft
80
+ };
81
+ }
82
+ //#endregion
83
+ exports.useDateText = useDateText;
@@ -0,0 +1,83 @@
1
+ import { formatDate, isoDate, parseDate } from "./dates.js";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { flushSync } from "react-dom";
4
+ //#region src/utils/use-date-text.ts
5
+ /**
6
+ * The text of a field a date can be typed into.
7
+ *
8
+ * While someone types, the text is theirs: a half-written date is left
9
+ * alone. It becomes the field's date the moment it reads as one in full,
10
+ * and again when they leave the field or press Enter, where a looser form
11
+ * such as "1/5/26" is taken too and rewritten in the pattern. Text that
12
+ * isn't a date by then stays as typed, the date is cleared, and the input
13
+ * is marked invalid, with the browser's own validation told why.
14
+ */
15
+ function useDateText({ value, onCommit, problem, invalidMessage, pattern, locale, timeZone, input }) {
16
+ const [draft, setDraft] = useState(null);
17
+ const [error, setErrorState] = useState(void 0);
18
+ function setError(message) {
19
+ setErrorState(message);
20
+ input.current?.setCustomValidity(message ?? "");
21
+ }
22
+ const formatted = value ? formatDate(value, pattern, locale, timeZone) : "";
23
+ const text = draft ?? formatted;
24
+ const day = isoDate(value ?? void 0, timeZone);
25
+ const typedDay = useRef(day);
26
+ useEffect(() => {
27
+ if (day && day !== typedDay.current) {
28
+ setDraft(null);
29
+ setErrorState(void 0);
30
+ input.current?.setCustomValidity("");
31
+ }
32
+ typedDay.current = day;
33
+ }, [day, input]);
34
+ function read(typed) {
35
+ if (!typed.trim()) return { date: null };
36
+ const date = parseDate(typed, pattern, locale, timeZone);
37
+ if (!date) return {
38
+ date: null,
39
+ error: invalidMessage
40
+ };
41
+ const reason = problem(date);
42
+ return reason ? {
43
+ date: null,
44
+ error: reason
45
+ } : { date };
46
+ }
47
+ function commit(date) {
48
+ typedDay.current = isoDate(date ?? void 0, timeZone);
49
+ if (typedDay.current !== day) onCommit(date);
50
+ }
51
+ function onChange(event) {
52
+ const typed = event.target.value;
53
+ setDraft(typed);
54
+ setError(void 0);
55
+ const result = read(typed);
56
+ if (!typed.trim()) commit(null);
57
+ else if (result.date && formatDate(result.date, pattern, locale, timeZone) === typed) commit(result.date);
58
+ }
59
+ function settle() {
60
+ if (draft === null) return;
61
+ const result = read(draft);
62
+ setError(result.error);
63
+ setDraft(result.error ? draft : null);
64
+ commit(result.date);
65
+ }
66
+ function onKeyDown(event) {
67
+ if (event.key === "Enter") flushSync(settle);
68
+ }
69
+ function clearDraft() {
70
+ setDraft(null);
71
+ setError(void 0);
72
+ }
73
+ return {
74
+ text,
75
+ error,
76
+ onChange,
77
+ onBlur: settle,
78
+ onKeyDown,
79
+ clearDraft
80
+ };
81
+ }
82
+ //#endregion
83
+ export { useDateText };
@@ -0,0 +1,18 @@
1
+ let react = require("react");
2
+ //#region src/utils/use-is-phone.ts
3
+ const wide = "(min-width: 40rem)";
4
+ function subscribe(onChange) {
5
+ const query = window.matchMedia(wide);
6
+ query.addEventListener("change", onChange);
7
+ return () => query.removeEventListener("change", onChange);
8
+ }
9
+ /**
10
+ * Whether the screen is narrow enough that the calendar should open as a
11
+ * sheet. On the server it's `false`: nothing is open there, and the answer
12
+ * is only needed once something is.
13
+ */
14
+ function useIsPhone() {
15
+ return (0, react.useSyncExternalStore)(subscribe, () => !window.matchMedia(wide).matches, () => false);
16
+ }
17
+ //#endregion
18
+ exports.useIsPhone = useIsPhone;
@@ -0,0 +1,18 @@
1
+ import { useSyncExternalStore } from "react";
2
+ //#region src/utils/use-is-phone.ts
3
+ const wide = "(min-width: 40rem)";
4
+ function subscribe(onChange) {
5
+ const query = window.matchMedia(wide);
6
+ query.addEventListener("change", onChange);
7
+ return () => query.removeEventListener("change", onChange);
8
+ }
9
+ /**
10
+ * Whether the screen is narrow enough that the calendar should open as a
11
+ * sheet. On the server it's `false`: nothing is open there, and the answer
12
+ * is only needed once something is.
13
+ */
14
+ function useIsPhone() {
15
+ return useSyncExternalStore(subscribe, () => !window.matchMedia(wide).matches, () => false);
16
+ }
17
+ //#endregion
18
+ export { useIsPhone };
package/package.json CHANGED
@@ -1,6 +1,121 @@
1
1
  {
2
2
  "name": "@nuvui/date-picker",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "A calendar, a date field and a date range field for @nuvui/react, built on react-day-picker.",
5
+ "license": "MIT",
6
+ "author": "Nuvraxis",
7
+ "homepage": "https://nuvui.nuvraxis.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Nuvraxis/nuvui.git",
11
+ "directory": "packages/date-picker"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/Nuvraxis/nuvui/issues"
15
+ },
16
+ "type": "module",
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "sideEffects": [
21
+ "**/*.css"
22
+ ],
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "main": "./dist/index.cjs",
27
+ "module": "./dist/index.js",
28
+ "types": "./dist/index.d.cts",
29
+ "exports": {
30
+ ".": {
31
+ "import": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "require": {
36
+ "types": "./dist/index.d.cts",
37
+ "default": "./dist/index.cjs"
38
+ }
39
+ },
40
+ "./calendar": {
41
+ "import": {
42
+ "types": "./dist/components/calendar/index.d.ts",
43
+ "default": "./dist/components/calendar/index.js"
44
+ },
45
+ "require": {
46
+ "types": "./dist/components/calendar/index.d.cts",
47
+ "default": "./dist/components/calendar/index.cjs"
48
+ }
49
+ },
50
+ "./date-picker": {
51
+ "import": {
52
+ "types": "./dist/components/date-picker/index.d.ts",
53
+ "default": "./dist/components/date-picker/index.js"
54
+ },
55
+ "require": {
56
+ "types": "./dist/components/date-picker/index.d.cts",
57
+ "default": "./dist/components/date-picker/index.cjs"
58
+ }
59
+ },
60
+ "./locale": {
61
+ "import": {
62
+ "types": "./dist/locale.d.ts",
63
+ "default": "./dist/locale.js"
64
+ },
65
+ "require": {
66
+ "types": "./dist/locale.d.cts",
67
+ "default": "./dist/locale.cjs"
68
+ }
69
+ },
70
+ "./styles.css": "./dist/styles.css",
71
+ "./css/*.css": "./dist/css/*.css",
72
+ "./package.json": "./package.json"
73
+ },
74
+ "peerDependencies": {
75
+ "@nuvui/react": "^0.1.0",
76
+ "react": "^18.2.0 || ^19.0.0",
77
+ "react-dom": "^18.2.0 || ^19.0.0"
78
+ },
79
+ "dependencies": {
80
+ "@date-fns/tz": "^1.5.0",
81
+ "date-fns": "^4.4.0",
82
+ "react-day-picker": "^10.0.2"
83
+ },
84
+ "devDependencies": {
85
+ "@arethetypeswrong/cli": "^0.18.5",
86
+ "@nuvui/react": "0.1.0",
87
+ "@nuvui/tooling": "0.0.0",
88
+ "@nuvui/typescript-config": "0.0.0",
89
+ "@size-limit/preset-small-lib": "14.1.0",
90
+ "@types/node": "^24.0.0",
91
+ "@types/react": "^19.3.0",
92
+ "@types/react-dom": "^19.3.0",
93
+ "@vitest/browser-playwright": "^5.0.3",
94
+ "axe-core": "^4.13.0",
95
+ "playwright": "1.63.0",
96
+ "publint": "^0.3.25",
97
+ "react": "^19.3.0",
98
+ "react-dom": "^19.3.0",
99
+ "sass-embedded": "^1.105.1",
100
+ "size-limit": "14.1.0",
101
+ "stylelint": "^17.16.0",
102
+ "tsdown": "0.23.0",
103
+ "typescript": "^7.0.2",
104
+ "vitest": "^5.0.3",
105
+ "vitest-browser-react": "^2.3.0"
106
+ },
107
+ "publishConfig": {
108
+ "access": "public",
109
+ "provenance": true
110
+ },
111
+ "scripts": {
112
+ "build": "tsdown && node scripts/check-dist.mjs",
113
+ "dev": "tsdown --watch src",
114
+ "lint": "stylelint \"src/**/*.scss\"",
115
+ "typecheck": "tsc --noEmit",
116
+ "test": "vitest run",
117
+ "test:watch": "vitest",
118
+ "check:package": "publint && pnpm pack --out node_modules/.cache/attw/package.tgz && attw node_modules/.cache/attw/package.tgz",
119
+ "size": "size-limit"
120
+ }
6
121
  }