sherick-ui 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 +55 -199
- package/dist/cjs/components/Breadcrumb.cjs +47 -0
- package/dist/cjs/components/Breadcrumb.cjs.map +1 -0
- package/dist/cjs/components/Calendar.cjs +298 -0
- package/dist/cjs/components/Calendar.cjs.map +1 -0
- package/dist/cjs/components/Combobox.cjs +1 -1
- package/dist/cjs/components/Combobox.cjs.map +1 -1
- package/dist/cjs/components/Command.cjs +81 -0
- package/dist/cjs/components/Command.cjs.map +1 -0
- package/dist/cjs/components/CommandPalette.cjs +52 -0
- package/dist/cjs/components/CommandPalette.cjs.map +1 -0
- package/dist/cjs/components/DatePicker.cjs +107 -0
- package/dist/cjs/components/DatePicker.cjs.map +1 -0
- package/dist/cjs/components/DateRangePicker.cjs +165 -0
- package/dist/cjs/components/DateRangePicker.cjs.map +1 -0
- package/dist/cjs/components/FileUpload.cjs +203 -0
- package/dist/cjs/components/FileUpload.cjs.map +1 -0
- package/dist/cjs/components/Media.cjs +27 -0
- package/dist/cjs/components/Media.cjs.map +1 -0
- package/dist/cjs/components/Pagination.cjs +115 -0
- package/dist/cjs/components/Pagination.cjs.map +1 -0
- package/dist/cjs/components/Select.cjs +1 -1
- package/dist/cjs/components/Select.cjs.map +1 -1
- package/dist/cjs/components/Stepper.cjs +40 -0
- package/dist/cjs/components/Stepper.cjs.map +1 -0
- package/dist/cjs/components/Tabs.cjs +1 -1
- package/dist/cjs/components/Tabs.cjs.map +1 -1
- package/dist/cjs/components/TreeView.cjs +247 -0
- package/dist/cjs/components/TreeView.cjs.map +1 -0
- package/dist/cjs/components/date-family.cjs +191 -0
- package/dist/cjs/components/date-family.cjs.map +1 -0
- package/dist/cjs/components/date-field.cjs +90 -0
- package/dist/cjs/components/date-field.cjs.map +1 -0
- package/dist/cjs/components/fileUpload.rules.cjs +121 -0
- package/dist/cjs/components/fileUpload.rules.cjs.map +1 -0
- package/dist/cjs/components/tree.cjs +95 -0
- package/dist/cjs/components/tree.cjs.map +1 -0
- package/dist/cjs/components/ui.common.cjs +27 -7
- package/dist/cjs/components/ui.common.cjs.map +1 -1
- package/dist/cjs/components/ui.motion.cjs +3 -3
- package/dist/cjs/components/ui.motion.cjs.map +1 -1
- package/dist/cjs/dev.cjs +5 -0
- package/dist/cjs/dev.cjs.map +1 -1
- package/dist/cjs/index.cjs +22 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/components/Breadcrumb.js +43 -0
- package/dist/esm/components/Breadcrumb.js.map +1 -0
- package/dist/esm/components/Calendar.js +294 -0
- package/dist/esm/components/Calendar.js.map +1 -0
- package/dist/esm/components/Combobox.js +2 -2
- package/dist/esm/components/Combobox.js.map +1 -1
- package/dist/esm/components/Command.js +77 -0
- package/dist/esm/components/Command.js.map +1 -0
- package/dist/esm/components/CommandPalette.js +48 -0
- package/dist/esm/components/CommandPalette.js.map +1 -0
- package/dist/esm/components/DatePicker.js +103 -0
- package/dist/esm/components/DatePicker.js.map +1 -0
- package/dist/esm/components/DateRangePicker.js +161 -0
- package/dist/esm/components/DateRangePicker.js.map +1 -0
- package/dist/esm/components/FileUpload.js +199 -0
- package/dist/esm/components/FileUpload.js.map +1 -0
- package/dist/esm/components/Media.js +25 -0
- package/dist/esm/components/Media.js.map +1 -0
- package/dist/esm/components/Pagination.js +111 -0
- package/dist/esm/components/Pagination.js.map +1 -0
- package/dist/esm/components/Select.js +2 -2
- package/dist/esm/components/Select.js.map +1 -1
- package/dist/esm/components/Stepper.js +36 -0
- package/dist/esm/components/Stepper.js.map +1 -0
- package/dist/esm/components/Tabs.js +2 -2
- package/dist/esm/components/Tabs.js.map +1 -1
- package/dist/esm/components/TreeView.js +243 -0
- package/dist/esm/components/TreeView.js.map +1 -0
- package/dist/esm/components/date-family.js +173 -0
- package/dist/esm/components/date-family.js.map +1 -0
- package/dist/esm/components/date-field.js +84 -0
- package/dist/esm/components/date-field.js.map +1 -0
- package/dist/esm/components/fileUpload.rules.js +114 -0
- package/dist/esm/components/fileUpload.rules.js.map +1 -0
- package/dist/esm/components/tree.js +88 -0
- package/dist/esm/components/tree.js.map +1 -0
- package/dist/esm/components/ui.common.js +23 -8
- package/dist/esm/components/ui.common.js.map +1 -1
- package/dist/esm/components/ui.motion.js +3 -3
- package/dist/esm/components/ui.motion.js.map +1 -1
- package/dist/esm/dev.js +1 -1
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/styles.css +362 -20
- package/dist/theme.css +15 -9
- package/dist/types/dev.d.cts +8 -3
- package/dist/types/dev.d.ts +8 -3
- package/dist/types/index.d.cts +600 -66
- package/dist/types/index.d.ts +600 -66
- package/package.json +1 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/* The date family's own calendar arithmetic, labels and Intl formatting.
|
|
2
|
+
====================================================================
|
|
3
|
+
`Calendar`, `DatePicker` and `DateRangePicker` share one definition of what a date *is*, so a
|
|
4
|
+
calendar grid, a native `type=date` input and a range span all agree.
|
|
5
|
+
|
|
6
|
+
A `CalendarDate` is a zero-padded Gregorian civil date (`YYYY-MM-DD`, years `0001`–`9999`) and
|
|
7
|
+
carries no time. Every arithmetic step below reads and writes calendar fields directly, through
|
|
8
|
+
the pure helpers in this module, rather than going through a timestamp: a local-midnight
|
|
9
|
+
`Date` would shift a day under a daylight-saving transition, and `Date.UTC` silently maps a
|
|
10
|
+
year below 100 onto `1900 + year`. The only `Date` objects here are the ones `Intl` needs to
|
|
11
|
+
*format* a value, and they are built through `setUTCFullYear` so year `0001` survives.
|
|
12
|
+
|
|
13
|
+
This module is private to the date family. The public date types are re-exported from
|
|
14
|
+
`Calendar.tsx`, which is the family's public entry. */
|
|
15
|
+
/** Inclusive year bounds of the value format: `0001-01-01` through `9999-12-31`. */
|
|
16
|
+
const MIN_CALENDAR_YEAR = 1;
|
|
17
|
+
const MAX_CALENDAR_YEAR = 9999;
|
|
18
|
+
const DAYS_PER_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
19
|
+
const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
20
|
+
const pad = (value, length) => String(value).padStart(length, "0");
|
|
21
|
+
const isLeapYear = (year) => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
22
|
+
const daysInMonth = (year, month) => month === 1 && isLeapYear(year) ? 29 : DAYS_PER_MONTH[month];
|
|
23
|
+
/**
|
|
24
|
+
* The only way a value becomes a date. A value that is not zero-padded, not a real civil date
|
|
25
|
+
* (including a day that does not exist in its month) or outside the year bounds is not a date at
|
|
26
|
+
* all, so callers never have to reason about a half-valid value.
|
|
27
|
+
*/
|
|
28
|
+
const parseCalendarDate = (value) => {
|
|
29
|
+
if (typeof value !== "string")
|
|
30
|
+
return null;
|
|
31
|
+
const match = DATE_PATTERN.exec(value);
|
|
32
|
+
if (!match)
|
|
33
|
+
return null;
|
|
34
|
+
const year = Number(match[1]);
|
|
35
|
+
const month = Number(match[2]) - 1;
|
|
36
|
+
const day = Number(match[3]);
|
|
37
|
+
if (year < MIN_CALENDAR_YEAR || year > MAX_CALENDAR_YEAR)
|
|
38
|
+
return null;
|
|
39
|
+
if (month < 0 || month > 11)
|
|
40
|
+
return null;
|
|
41
|
+
if (day < 1 || day > daysInMonth(year, month))
|
|
42
|
+
return null;
|
|
43
|
+
return { year, month, day };
|
|
44
|
+
};
|
|
45
|
+
const toCalendarDate = ({ year, month, day }) => `${pad(year, 4)}-${pad(month + 1, 2)}-${pad(day, 2)}`;
|
|
46
|
+
/** A calendar date, or `null` when the input is not one. */
|
|
47
|
+
const normalizeCalendarDate = (value) => {
|
|
48
|
+
const parts = parseCalendarDate(value);
|
|
49
|
+
return parts ? toCalendarDate(parts) : null;
|
|
50
|
+
};
|
|
51
|
+
const normalizeDateRange = (range) => ({
|
|
52
|
+
start: normalizeCalendarDate(range === null || range === void 0 ? void 0 : range.start),
|
|
53
|
+
end: normalizeCalendarDate(range === null || range === void 0 ? void 0 : range.end),
|
|
54
|
+
});
|
|
55
|
+
/* Zero-padded dates order lexicographically exactly as they order in time, so comparison needs no
|
|
56
|
+
parsing. */
|
|
57
|
+
const compareCalendarDates = (left, right) => left === right ? 0 : left < right ? -1 : 1;
|
|
58
|
+
/** Day of week, `0` for Sunday. Sakamoto's algorithm: pure integer arithmetic, so it is exact for
|
|
59
|
+
* the whole year range rather than only for the years a `Date` round-trip happens to preserve. */
|
|
60
|
+
const dayOfWeek = ({ year, month, day }) => {
|
|
61
|
+
const offsets = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
|
|
62
|
+
const adjustedYear = month < 2 ? year - 1 : year;
|
|
63
|
+
return ((adjustedYear +
|
|
64
|
+
Math.floor(adjustedYear / 4) -
|
|
65
|
+
Math.floor(adjustedYear / 100) +
|
|
66
|
+
Math.floor(adjustedYear / 400) +
|
|
67
|
+
offsets[month] +
|
|
68
|
+
day) %
|
|
69
|
+
7);
|
|
70
|
+
};
|
|
71
|
+
const addCalendarDays = ({ year, month, day }, delta) => {
|
|
72
|
+
let nextYear = year;
|
|
73
|
+
let nextMonth = month;
|
|
74
|
+
let nextDay = day + delta;
|
|
75
|
+
while (nextDay < 1) {
|
|
76
|
+
nextMonth -= 1;
|
|
77
|
+
if (nextMonth < 0) {
|
|
78
|
+
nextMonth = 11;
|
|
79
|
+
nextYear -= 1;
|
|
80
|
+
}
|
|
81
|
+
nextDay += daysInMonth(nextYear, nextMonth);
|
|
82
|
+
}
|
|
83
|
+
while (nextDay > daysInMonth(nextYear, nextMonth)) {
|
|
84
|
+
nextDay -= daysInMonth(nextYear, nextMonth);
|
|
85
|
+
nextMonth += 1;
|
|
86
|
+
if (nextMonth > 11) {
|
|
87
|
+
nextMonth = 0;
|
|
88
|
+
nextYear += 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { year: nextYear, month: nextMonth, day: nextDay };
|
|
92
|
+
};
|
|
93
|
+
/** Moves by whole months and **clamps** the day to the target month, which is what keeps
|
|
94
|
+
* `2024-01-31` a real date when the grid walks into February. The result never leaves the year
|
|
95
|
+
* bounds, so month and year navigation cannot escape `0001`–`9999`. */
|
|
96
|
+
const addCalendarMonths = ({ year, month, day }, delta) => {
|
|
97
|
+
const minIndex = MIN_CALENDAR_YEAR * 12;
|
|
98
|
+
const maxIndex = MAX_CALENDAR_YEAR * 12 + 11;
|
|
99
|
+
const index = Math.min(maxIndex, Math.max(minIndex, year * 12 + month + delta));
|
|
100
|
+
const nextYear = Math.floor(index / 12);
|
|
101
|
+
const nextMonth = index % 12;
|
|
102
|
+
return { year: nextYear, month: nextMonth, day: Math.min(day, daysInMonth(nextYear, nextMonth)) };
|
|
103
|
+
};
|
|
104
|
+
/** Walks an inclusive span once and reports whether any date in it matches `predicate`. */
|
|
105
|
+
const rangeIncludesDate = (from, to, predicate) => {
|
|
106
|
+
const last = toCalendarDate(to);
|
|
107
|
+
let cursor = from;
|
|
108
|
+
while (!predicate(toCalendarDate(cursor))) {
|
|
109
|
+
if (toCalendarDate(cursor) === last)
|
|
110
|
+
return false;
|
|
111
|
+
cursor = addCalendarDays(cursor, 1);
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
};
|
|
115
|
+
/** Today in UTC calendar fields — the value an application overrides through `today` when its own
|
|
116
|
+
* civil day differs from UTC. */
|
|
117
|
+
const todayCalendarDate = () => {
|
|
118
|
+
const now = new Date();
|
|
119
|
+
return toCalendarDate({
|
|
120
|
+
year: now.getUTCFullYear(),
|
|
121
|
+
month: now.getUTCMonth(),
|
|
122
|
+
day: now.getUTCDate(),
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
/* A `Date` only so `Intl` can format; the calendar fields are set through `setUTCFullYear`, which
|
|
126
|
+
keeps year `0001` intact instead of folding it into the 1900s. */
|
|
127
|
+
const utcDate = ({ year, month, day }) => {
|
|
128
|
+
const date = new Date(0);
|
|
129
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
130
|
+
date.setUTCFullYear(year, month, day);
|
|
131
|
+
return date;
|
|
132
|
+
};
|
|
133
|
+
/* 2024-01-07 is a Sunday, which is what anchors the weekday headings: asking `Intl` for a weekday
|
|
134
|
+
name needs a date, and a fixed one keeps the headings independent of the visitor's clock. */
|
|
135
|
+
const WEEKDAY_ANCHOR = { year: 2024, month: 0, day: 7 };
|
|
136
|
+
/**
|
|
137
|
+
* The locale's month, weekday and full-date wording, with the Gregorian calendar and UTC fixed.
|
|
138
|
+
* Values stay Gregorian and zero-padded whatever the locale is, so a locale changes how a date is
|
|
139
|
+
* written and announced, never what it means.
|
|
140
|
+
*/
|
|
141
|
+
const createCalendarFormat = (locale) => {
|
|
142
|
+
const shared = { timeZone: "UTC", calendar: "gregory" };
|
|
143
|
+
const monthYear = new Intl.DateTimeFormat(locale, { ...shared, month: "long", year: "numeric" });
|
|
144
|
+
const fullDate = new Intl.DateTimeFormat(locale, { ...shared, dateStyle: "full" });
|
|
145
|
+
const dayNumber = new Intl.NumberFormat(locale, { useGrouping: false });
|
|
146
|
+
const shortWeekday = new Intl.DateTimeFormat(locale, { ...shared, weekday: "short" });
|
|
147
|
+
const longWeekday = new Intl.DateTimeFormat(locale, { ...shared, weekday: "long" });
|
|
148
|
+
const weekdayNames = Array.from({ length: 7 }, (_, index) => {
|
|
149
|
+
const date = utcDate(addCalendarDays(WEEKDAY_ANCHOR, index));
|
|
150
|
+
return { short: shortWeekday.format(date), long: longWeekday.format(date) };
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
formatMonthYear: (parts) => monthYear.format(utcDate(parts)),
|
|
154
|
+
formatDayNumber: (day) => dayNumber.format(day),
|
|
155
|
+
weekdayNames,
|
|
156
|
+
formatDate: (parts) => fullDate.format(utcDate(parts)),
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
const defaultCalendarLabels = {
|
|
160
|
+
previousMonth: "Previous month",
|
|
161
|
+
nextMonth: "Next month",
|
|
162
|
+
today: "Today",
|
|
163
|
+
selectDate: "Select a date",
|
|
164
|
+
selectRange: "Select a range",
|
|
165
|
+
unavailableRange: "That range includes an unavailable date",
|
|
166
|
+
};
|
|
167
|
+
const resolveCalendarLabels = (labels) => ({
|
|
168
|
+
...defaultCalendarLabels,
|
|
169
|
+
...labels,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export { MAX_CALENDAR_YEAR, MIN_CALENDAR_YEAR, addCalendarDays, addCalendarMonths, compareCalendarDates, createCalendarFormat, dayOfWeek, daysInMonth, defaultCalendarLabels, isLeapYear, normalizeCalendarDate, normalizeDateRange, parseCalendarDate, rangeIncludesDate, resolveCalendarLabels, toCalendarDate, todayCalendarDate };
|
|
173
|
+
//# sourceMappingURL=date-family.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-family.js","sources":["../../../src/components/date-family.ts"],"sourcesContent":["/* The date family's own calendar arithmetic, labels and Intl formatting.\n ====================================================================\n `Calendar`, `DatePicker` and `DateRangePicker` share one definition of what a date *is*, so a\n calendar grid, a native `type=date` input and a range span all agree.\n\n A `CalendarDate` is a zero-padded Gregorian civil date (`YYYY-MM-DD`, years `0001`–`9999`) and\n carries no time. Every arithmetic step below reads and writes calendar fields directly, through\n the pure helpers in this module, rather than going through a timestamp: a local-midnight\n `Date` would shift a day under a daylight-saving transition, and `Date.UTC` silently maps a\n year below 100 onto `1900 + year`. The only `Date` objects here are the ones `Intl` needs to\n *format* a value, and they are built through `setUTCFullYear` so year `0001` survives.\n\n This module is private to the date family. The public date types are re-exported from\n `Calendar.tsx`, which is the family's public entry. */\n\nexport type CalendarDate = string;\n\nexport interface DateRange {\n start: CalendarDate | null;\n end: CalendarDate | null;\n}\n\n/** A parsed civil date. `month` is 0-based, as every `Intl` and `Date` API expects. */\nexport interface DateParts {\n year: number;\n month: number;\n day: number;\n}\n\n/** Inclusive year bounds of the value format: `0001-01-01` through `9999-12-31`. */\nexport const MIN_CALENDAR_YEAR = 1;\nexport const MAX_CALENDAR_YEAR = 9999;\n\nconst DAYS_PER_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\nconst DATE_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\nconst pad = (value: number, length: number) => String(value).padStart(length, \"0\");\n\nexport const isLeapYear = (year: number) =>\n year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);\n\nexport const daysInMonth = (year: number, month: number) =>\n month === 1 && isLeapYear(year) ? 29 : DAYS_PER_MONTH[month];\n\n/**\n * The only way a value becomes a date. A value that is not zero-padded, not a real civil date\n * (including a day that does not exist in its month) or outside the year bounds is not a date at\n * all, so callers never have to reason about a half-valid value.\n */\nexport const parseCalendarDate = (value: CalendarDate | null | undefined): DateParts | null => {\n if (typeof value !== \"string\") return null;\n\n const match = DATE_PATTERN.exec(value);\n if (!match) return null;\n\n const year = Number(match[1]);\n const month = Number(match[2]) - 1;\n const day = Number(match[3]);\n\n if (year < MIN_CALENDAR_YEAR || year > MAX_CALENDAR_YEAR) return null;\n if (month < 0 || month > 11) return null;\n if (day < 1 || day > daysInMonth(year, month)) return null;\n\n return { year, month, day };\n};\n\nexport const isValidCalendarDate = (value: CalendarDate | null | undefined) =>\n parseCalendarDate(value) !== null;\n\nexport const toCalendarDate = ({ year, month, day }: DateParts): CalendarDate =>\n `${pad(year, 4)}-${pad(month + 1, 2)}-${pad(day, 2)}`;\n\n/** A calendar date, or `null` when the input is not one. */\nexport const normalizeCalendarDate = (value: CalendarDate | null | undefined): CalendarDate | null => {\n const parts = parseCalendarDate(value);\n return parts ? toCalendarDate(parts) : null;\n};\n\nexport const normalizeDateRange = (range: DateRange | null | undefined): DateRange => ({\n start: normalizeCalendarDate(range?.start),\n end: normalizeCalendarDate(range?.end),\n});\n\nexport const emptyDateRange = (): DateRange => ({ start: null, end: null });\n\n/* Zero-padded dates order lexicographically exactly as they order in time, so comparison needs no\n parsing. */\nexport const compareCalendarDates = (left: CalendarDate, right: CalendarDate) =>\n left === right ? 0 : left < right ? -1 : 1;\n\n/** Day of week, `0` for Sunday. Sakamoto's algorithm: pure integer arithmetic, so it is exact for\n * the whole year range rather than only for the years a `Date` round-trip happens to preserve. */\nexport const dayOfWeek = ({ year, month, day }: DateParts): number => {\n const offsets = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];\n const adjustedYear = month < 2 ? year - 1 : year;\n\n return (\n (adjustedYear +\n Math.floor(adjustedYear / 4) -\n Math.floor(adjustedYear / 100) +\n Math.floor(adjustedYear / 400) +\n offsets[month] +\n day) %\n 7\n );\n};\n\nexport const addCalendarDays = ({ year, month, day }: DateParts, delta: number): DateParts => {\n let nextYear = year;\n let nextMonth = month;\n let nextDay = day + delta;\n\n while (nextDay < 1) {\n nextMonth -= 1;\n if (nextMonth < 0) {\n nextMonth = 11;\n nextYear -= 1;\n }\n nextDay += daysInMonth(nextYear, nextMonth);\n }\n\n while (nextDay > daysInMonth(nextYear, nextMonth)) {\n nextDay -= daysInMonth(nextYear, nextMonth);\n nextMonth += 1;\n if (nextMonth > 11) {\n nextMonth = 0;\n nextYear += 1;\n }\n }\n\n return { year: nextYear, month: nextMonth, day: nextDay };\n};\n\n/** Moves by whole months and **clamps** the day to the target month, which is what keeps\n * `2024-01-31` a real date when the grid walks into February. The result never leaves the year\n * bounds, so month and year navigation cannot escape `0001`–`9999`. */\nexport const addCalendarMonths = ({ year, month, day }: DateParts, delta: number): DateParts => {\n const minIndex = MIN_CALENDAR_YEAR * 12;\n const maxIndex = MAX_CALENDAR_YEAR * 12 + 11;\n const index = Math.min(maxIndex, Math.max(minIndex, year * 12 + month + delta));\n const nextYear = Math.floor(index / 12);\n const nextMonth = index % 12;\n\n return { year: nextYear, month: nextMonth, day: Math.min(day, daysInMonth(nextYear, nextMonth)) };\n};\n\n/** Walks an inclusive span once and reports whether any date in it matches `predicate`. */\nexport const rangeIncludesDate = (\n from: DateParts,\n to: DateParts,\n predicate: (date: CalendarDate) => boolean\n): boolean => {\n const last = toCalendarDate(to);\n let cursor = from;\n\n while (!predicate(toCalendarDate(cursor))) {\n if (toCalendarDate(cursor) === last) return false;\n cursor = addCalendarDays(cursor, 1);\n }\n\n return true;\n};\n\n/** Today in UTC calendar fields — the value an application overrides through `today` when its own\n * civil day differs from UTC. */\nexport const todayCalendarDate = (): CalendarDate => {\n const now = new Date();\n return toCalendarDate({\n year: now.getUTCFullYear(),\n month: now.getUTCMonth(),\n day: now.getUTCDate(),\n });\n};\n\n/* A `Date` only so `Intl` can format; the calendar fields are set through `setUTCFullYear`, which\n keeps year `0001` intact instead of folding it into the 1900s. */\nconst utcDate = ({ year, month, day }: DateParts) => {\n const date = new Date(0);\n date.setUTCHours(0, 0, 0, 0);\n date.setUTCFullYear(year, month, day);\n return date;\n};\n\nexport interface CalendarFormat {\n /** The visible and announced month caption, e.g. `January 2024`. */\n formatMonthYear: (parts: DateParts) => string;\n /** The day number as the locale writes digits. */\n formatDayNumber: (day: number) => string;\n /** Weekday headings, indexed from Sunday. `short` is shown, `long` is the heading's name. */\n weekdayNames: { short: string; long: string }[];\n /** The day's full accessible name, e.g. `Monday, January 15, 2024`. */\n formatDate: (parts: DateParts) => string;\n}\n\n/* 2024-01-07 is a Sunday, which is what anchors the weekday headings: asking `Intl` for a weekday\n name needs a date, and a fixed one keeps the headings independent of the visitor's clock. */\nconst WEEKDAY_ANCHOR: DateParts = { year: 2024, month: 0, day: 7 };\n\n/**\n * The locale's month, weekday and full-date wording, with the Gregorian calendar and UTC fixed.\n * Values stay Gregorian and zero-padded whatever the locale is, so a locale changes how a date is\n * written and announced, never what it means.\n */\nexport const createCalendarFormat = (locale: string): CalendarFormat => {\n const shared = { timeZone: \"UTC\", calendar: \"gregory\" } as const;\n const monthYear = new Intl.DateTimeFormat(locale, { ...shared, month: \"long\", year: \"numeric\" });\n const fullDate = new Intl.DateTimeFormat(locale, { ...shared, dateStyle: \"full\" });\n const dayNumber = new Intl.NumberFormat(locale, { useGrouping: false });\n const shortWeekday = new Intl.DateTimeFormat(locale, { ...shared, weekday: \"short\" });\n const longWeekday = new Intl.DateTimeFormat(locale, { ...shared, weekday: \"long\" });\n\n const weekdayNames = Array.from({ length: 7 }, (_, index) => {\n const date = utcDate(addCalendarDays(WEEKDAY_ANCHOR, index));\n return { short: shortWeekday.format(date), long: longWeekday.format(date) };\n });\n\n return {\n formatMonthYear: (parts) => monthYear.format(utcDate(parts)),\n formatDayNumber: (day) => dayNumber.format(day),\n weekdayNames,\n formatDate: (parts) => fullDate.format(utcDate(parts)),\n };\n};\n\nexport interface CalendarLabels {\n /** The control that moves the grid to the previous month. */\n previousMonth: string;\n /** The control that moves the grid to the next month. */\n nextMonth: string;\n /** The control that navigates to today without selecting it. */\n today: string;\n /** Describes a single-date grid to assistive technology. */\n selectDate: string;\n /** Describes a range grid to assistive technology. */\n selectRange: string;\n /** Reported when a date — or a range — cannot be selected or is not acceptable. */\n unavailableRange: string;\n}\n\nexport const defaultCalendarLabels: CalendarLabels = {\n previousMonth: \"Previous month\",\n nextMonth: \"Next month\",\n today: \"Today\",\n selectDate: \"Select a date\",\n selectRange: \"Select a range\",\n unavailableRange: \"That range includes an unavailable date\",\n};\n\nexport const resolveCalendarLabels = (labels?: Partial<CalendarLabels>): CalendarLabels => ({\n ...defaultCalendarLabels,\n ...labels,\n});\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import { Popover } from '@base-ui/react/popover';
|
|
4
|
+
import { CalendarDays } from 'lucide-react';
|
|
5
|
+
import { useRef, useEffect } from 'react';
|
|
6
|
+
import { cn } from '../libs/utils.js';
|
|
7
|
+
import { density, shape, text, stateLayer, state, focusRingWithin, material, overlay, stacking } from './ui.common.js';
|
|
8
|
+
import { motionInkPress, motionFeedback, motionPresenceAnchored } from './ui.motion.js';
|
|
9
|
+
|
|
10
|
+
/* The parts the two date pickers share.
|
|
11
|
+
====================================
|
|
12
|
+
A picker is a native `type=date` field with a named control beside it that opens the calendar,
|
|
13
|
+
so the field's surface, its trailing control and the popup that holds the grid have to be one
|
|
14
|
+
definition rather than two that drift. This module is private to the date family: nothing here
|
|
15
|
+
is published, and neither picker adds a shell of its own.
|
|
16
|
+
|
|
17
|
+
`DatePicker` and `DateRangePicker` compose Base `Field` for the label/description/error
|
|
18
|
+
relationship and Base `Popover` for the popup; what lives here is only the visual and
|
|
19
|
+
structural arrangement those compositions hold in common. */
|
|
20
|
+
/**
|
|
21
|
+
* The one field surface the date family uses: a borderless matte field whose focus treatment
|
|
22
|
+
* wraps the whole row, exactly as the library's other composite fields do.
|
|
23
|
+
*
|
|
24
|
+
* The row carries `group/field` and its own `data-disabled` marker so the trailing control's dim
|
|
25
|
+
* is neutralised inside an already dimmed field — a disabled field dims once, at the field.
|
|
26
|
+
*/
|
|
27
|
+
const dateFieldRowClassName = (disabled) => cn("group/field flex min-w-0 w-full items-center ps-5 pe-3", density.normal, shape.control, material.control, motionFeedback, focusRingWithin, state.field.invalid, !disabled && state.field.hover, !disabled && state.field.focusWithin, !disabled && state.field.invalidHover, !disabled && state.field.invalidFocusWithin, disabled ? state.disabledDescendant : state.text);
|
|
28
|
+
/** Native date entry remains intact. Its trailing browser affordance sits outside the clipped
|
|
29
|
+
* input slot, leaving one visible calendar trigger in every engine (Firefox does not expose
|
|
30
|
+
* a picker-indicator pseudo-element). The composite row owns focus, not the clipped input. */
|
|
31
|
+
const dateInputClassName = "block w-[calc(100%+2.5rem)] min-w-0 bg-transparent py-3 text-inherit outline-none disabled:cursor-not-allowed";
|
|
32
|
+
/* The 36×44 target holds a concentric 32px hover surface, leaving air inside the 48px field.
|
|
33
|
+
Only the icon presses: the adjacent native text entry never activates this button. */
|
|
34
|
+
const datePartClassName = `group inline-flex shrink-0 items-center justify-center ${density.part} ${shape.circle} ${text.medium} ${motionFeedback} ${stateLayer.quiet} before:inset-x-0.5 before:inset-y-1.5 ${state.enabled} ${state.disabledPart} [&:not([data-disabled]):not(:disabled)]:hover:text-sherick-ink`;
|
|
35
|
+
/**
|
|
36
|
+
* The named control that opens the calendar. It is a Base `Popover.Trigger` rendered as a native
|
|
37
|
+
* button, so Base owns the relationship it publishes on the field (`aria-haspopup`,
|
|
38
|
+
* `aria-expanded`, `aria-controls`) and the pointer and keyboard activation that opens the
|
|
39
|
+
* surface; the button itself only carries the type it needs to stay out of form submission.
|
|
40
|
+
*/
|
|
41
|
+
const DateFieldTrigger = ({ label, disabled = false }) => (jsx(Popover.Trigger, { disabled: disabled, render: jsx("button", { type: "button", "aria-label": label, disabled: disabled, className: cn(datePartClassName) }), children: jsx("span", { className: cn("inline-flex size-5 items-center justify-center [&>svg]:size-5", !disabled && motionInkPress), children: jsx(CalendarDays, { "aria-hidden": "true" }) }) }));
|
|
42
|
+
/**
|
|
43
|
+
* The calendar's popup: the same shell `Popover.Content` renders, composed straight from Base's
|
|
44
|
+
* public parts because the grid needs one thing the published shell does not offer — `initialFocus`
|
|
45
|
+
* aimed at the day the reader is on, so the grid is usable from the moment it opens.
|
|
46
|
+
*
|
|
47
|
+
* Base owns the portal, the placement and collision handling, the outside-interaction and Escape
|
|
48
|
+
* dismissal and the focus restoration; the surface is non-modal, so the rest of the page stays
|
|
49
|
+
* reachable. Sherick owns only the sheet and its presence.
|
|
50
|
+
*/
|
|
51
|
+
const DateCalendarPopup = ({ title, children, anchor }) => {
|
|
52
|
+
const popupRef = useRef(null);
|
|
53
|
+
return (jsx(Popover.Portal, { children: jsx(Popover.Positioner, { anchor: anchor, side: "bottom", align: "start", sideOffset: 8, className: cn(stacking.float), children: jsxs(Popover.Popup, { ref: popupRef,
|
|
54
|
+
/* The roving tab stop is the calendar's own active day, so this is exactly the cell a
|
|
55
|
+
keyboard reader would land on. `null` leaves Base's default focus behaviour in place
|
|
56
|
+
if the grid has no tab stop to give. */
|
|
57
|
+
initialFocus: () => { var _a, _b; return (_b = (_a = popupRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('[data-day][tabindex="0"]')) !== null && _b !== void 0 ? _b : null; }, className: cn("w-max max-w-[min(24rem,var(--available-width))] max-h-[var(--available-height)] overflow-y-auto p-3 [overflow-wrap:anywhere]", overlay.popup, motionPresenceAnchored), children: [jsx(Popover.Title, { className: cn("sr-only"), children: title }), children] }) }) }));
|
|
58
|
+
};
|
|
59
|
+
/** Base Field controls entry and validity, but native reset emits no input/change event.
|
|
60
|
+
* Bridge only the date family's domain state, after the owning form's cancelable reset. */
|
|
61
|
+
const useDateFormReset = (inputRef, formId, restore) => {
|
|
62
|
+
const latestRestore = useRef(restore);
|
|
63
|
+
useEffect(() => { latestRestore.current = restore; }, [restore]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
var _a;
|
|
66
|
+
const form = (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.form;
|
|
67
|
+
if (!form)
|
|
68
|
+
return;
|
|
69
|
+
const reset = (event) => {
|
|
70
|
+
// A microtask can run between native listeners, before React's delegated onReset.
|
|
71
|
+
// The next task observes cancellation after the entire native dispatch/default action.
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
var _a;
|
|
74
|
+
if (!event.defaultPrevented && ((_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.form) === form)
|
|
75
|
+
latestRestore.current();
|
|
76
|
+
}, 0);
|
|
77
|
+
};
|
|
78
|
+
form.addEventListener("reset", reset);
|
|
79
|
+
return () => form.removeEventListener("reset", reset);
|
|
80
|
+
}, [inputRef, formId]);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export { DateCalendarPopup, DateFieldTrigger, dateFieldRowClassName, dateInputClassName, useDateFormReset };
|
|
84
|
+
//# sourceMappingURL=date-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-field.js","sources":["../../../src/components/date-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { Popover as BasePopover } from \"@base-ui/react/popover\";\nimport { CalendarDays } from \"lucide-react\";\nimport React, { useEffect, useRef, type ReactNode } from \"react\";\nimport { cn } from \"@/libs/utils\";\nimport { density, focusRingWithin, material, overlay, shape, stacking, state, stateLayer, text } from \"./ui.common\";\nimport { motionFeedback, motionInkPress, motionPresenceAnchored } from \"./ui.motion\";\n\n/* The parts the two date pickers share.\n ====================================\n A picker is a native `type=date` field with a named control beside it that opens the calendar,\n so the field's surface, its trailing control and the popup that holds the grid have to be one\n definition rather than two that drift. This module is private to the date family: nothing here\n is published, and neither picker adds a shell of its own.\n\n `DatePicker` and `DateRangePicker` compose Base `Field` for the label/description/error\n relationship and Base `Popover` for the popup; what lives here is only the visual and\n structural arrangement those compositions hold in common. */\n\n/**\n * The one field surface the date family uses: a borderless matte field whose focus treatment\n * wraps the whole row, exactly as the library's other composite fields do.\n *\n * The row carries `group/field` and its own `data-disabled` marker so the trailing control's dim\n * is neutralised inside an already dimmed field — a disabled field dims once, at the field.\n */\nexport const dateFieldRowClassName = (disabled: boolean) =>\n cn(\n \"group/field flex min-w-0 w-full items-center ps-5 pe-3\",\n density.normal,\n shape.control,\n material.control,\n motionFeedback,\n focusRingWithin,\n state.field.invalid,\n !disabled && state.field.hover,\n !disabled && state.field.focusWithin,\n !disabled && state.field.invalidHover,\n !disabled && state.field.invalidFocusWithin,\n disabled ? state.disabledDescendant : state.text\n );\n\n/** Native date entry remains intact. Its trailing browser affordance sits outside the clipped\n * input slot, leaving one visible calendar trigger in every engine (Firefox does not expose\n * a picker-indicator pseudo-element). The composite row owns focus, not the clipped input. */\nexport const dateInputClassName =\n \"block w-[calc(100%+2.5rem)] min-w-0 bg-transparent py-3 text-inherit outline-none disabled:cursor-not-allowed\";\n\n/* The 36×44 target holds a concentric 32px hover surface, leaving air inside the 48px field.\n Only the icon presses: the adjacent native text entry never activates this button. */\nconst datePartClassName = `group inline-flex shrink-0 items-center justify-center ${density.part} ${shape.circle} ${text.medium} ${motionFeedback} ${stateLayer.quiet} before:inset-x-0.5 before:inset-y-1.5 ${state.enabled} ${state.disabledPart} [&:not([data-disabled]):not(:disabled)]:hover:text-sherick-ink`;\n\nexport interface DateFieldTriggerProps {\n /** The control's accessible name. It has no visible label, so it has to be named. */\n label: string;\n disabled?: boolean;\n}\n\n/**\n * The named control that opens the calendar. It is a Base `Popover.Trigger` rendered as a native\n * button, so Base owns the relationship it publishes on the field (`aria-haspopup`,\n * `aria-expanded`, `aria-controls`) and the pointer and keyboard activation that opens the\n * surface; the button itself only carries the type it needs to stay out of form submission.\n */\nexport const DateFieldTrigger = ({ label, disabled = false }: DateFieldTriggerProps) => (\n <BasePopover.Trigger\n disabled={disabled}\n render={<button type=\"button\" aria-label={label} disabled={disabled} className={cn(datePartClassName)} />}\n >\n <span className={cn(\"inline-flex size-5 items-center justify-center [&>svg]:size-5\", !disabled && motionInkPress)}>\n <CalendarDays aria-hidden=\"true\" />\n </span>\n </BasePopover.Trigger>\n);\n\nexport interface DateCalendarPopupProps {\n /** Names the popup, through Base's own Title part. */\n title: string;\n children: ReactNode;\n anchor: React.ComponentProps<typeof BasePopover.Positioner>[\"anchor\"];\n}\n\n/**\n * The calendar's popup: the same shell `Popover.Content` renders, composed straight from Base's\n * public parts because the grid needs one thing the published shell does not offer — `initialFocus`\n * aimed at the day the reader is on, so the grid is usable from the moment it opens.\n *\n * Base owns the portal, the placement and collision handling, the outside-interaction and Escape\n * dismissal and the focus restoration; the surface is non-modal, so the rest of the page stays\n * reachable. Sherick owns only the sheet and its presence.\n */\nexport const DateCalendarPopup = ({ title, children, anchor }: DateCalendarPopupProps) => {\n const popupRef = useRef<HTMLDivElement | null>(null);\n\n return (\n <BasePopover.Portal>\n <BasePopover.Positioner\n anchor={anchor}\n side=\"bottom\"\n align=\"start\"\n sideOffset={8}\n className={cn(stacking.float)}\n >\n <BasePopover.Popup\n ref={popupRef}\n /* The roving tab stop is the calendar's own active day, so this is exactly the cell a\n keyboard reader would land on. `null` leaves Base's default focus behaviour in place\n if the grid has no tab stop to give. */\n initialFocus={() =>\n popupRef.current?.querySelector<HTMLElement>('[data-day][tabindex=\"0\"]') ?? null\n }\n className={cn(\n \"w-max max-w-[min(24rem,var(--available-width))] max-h-[var(--available-height)] overflow-y-auto p-3 [overflow-wrap:anywhere]\",\n overlay.popup,\n motionPresenceAnchored\n )}\n >\n <BasePopover.Title className={cn(\"sr-only\")}>{title}</BasePopover.Title>\n {children}\n </BasePopover.Popup>\n </BasePopover.Positioner>\n </BasePopover.Portal>\n );\n};\n\n/** Base Field controls entry and validity, but native reset emits no input/change event.\n * Bridge only the date family's domain state, after the owning form's cancelable reset. */\nexport const useDateFormReset = (\n inputRef: React.RefObject<HTMLInputElement | null>,\n formId: string | undefined,\n restore: () => void\n) => {\n const latestRestore = useRef(restore);\n useEffect(() => { latestRestore.current = restore; }, [restore]);\n useEffect(() => {\n const form = inputRef.current?.form;\n if (!form) return;\n const reset = (event: Event) => {\n // A microtask can run between native listeners, before React's delegated onReset.\n // The next task observes cancellation after the entire native dispatch/default action.\n setTimeout(() => {\n if (!event.defaultPrevented && inputRef.current?.form === form) latestRestore.current();\n }, 0);\n };\n form.addEventListener(\"reset\", reset);\n return () => form.removeEventListener(\"reset\", reset);\n }, [inputRef, formId]);\n};\n"],"names":["_jsx","BasePopover","_jsxs"],"mappings":"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C,CAAA,CAAA,CAAA,CAAI,CAAA;AAGpD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;;AAE+F,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACxF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,kBAAkB,CAAA,CAAA,CAC7B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAEF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACwF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* File selection rules for `FileUpload`.
|
|
2
|
+
|
|
3
|
+
The native chooser and the drop zone are two doors into one policy: the same accept, the same
|
|
4
|
+
per-file size and the same total count decide both, so a file the picker would refuse cannot
|
|
5
|
+
slip in through a drop. Nothing here reads a browser global — every rule works from the
|
|
6
|
+
metadata a `File` already carries — so the module is safe to evaluate during SSR and can be
|
|
7
|
+
probed without a document.
|
|
8
|
+
|
|
9
|
+
`FileUpload.tsx` composes the field around these rules; it owns no rule of its own. */
|
|
10
|
+
/** The identity of one selection entry: the fields that make two `File`s the same choice. */
|
|
11
|
+
const fileKey = (file) => `${file.name}\u0000${file.size}\u0000${file.type}\u0000${file.lastModified}`;
|
|
12
|
+
/** The per-file byte limit as a whole number, or `undefined` when the caller set none. */
|
|
13
|
+
const resolveMaxSize = (maxSize) => maxSize !== undefined && Number.isFinite(maxSize) && maxSize >= 0 ? Math.floor(maxSize) : undefined;
|
|
14
|
+
/**
|
|
15
|
+
* The total file limit, or `undefined` when the selection is unbounded. A single-file field
|
|
16
|
+
* holds exactly one: the picker is told nothing about it, because that limit is what makes a
|
|
17
|
+
* second file a replacement rather than an addition.
|
|
18
|
+
*/
|
|
19
|
+
const resolveMaxFiles = (maxFiles, multiple) => {
|
|
20
|
+
if (!multiple)
|
|
21
|
+
return 1;
|
|
22
|
+
return maxFiles !== undefined && Number.isFinite(maxFiles) && maxFiles >= 0
|
|
23
|
+
? Math.floor(maxFiles)
|
|
24
|
+
: undefined;
|
|
25
|
+
};
|
|
26
|
+
/** A byte limit as the copy spells it: `2 MB`, never `2097152`. */
|
|
27
|
+
const formatBytes = (bytes) => {
|
|
28
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
29
|
+
let value = bytes;
|
|
30
|
+
let unit = 0;
|
|
31
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
32
|
+
value /= 1024;
|
|
33
|
+
unit += 1;
|
|
34
|
+
}
|
|
35
|
+
return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;
|
|
36
|
+
};
|
|
37
|
+
const acceptTokens = (accept) => (accept !== null && accept !== void 0 ? accept : "")
|
|
38
|
+
.split(",")
|
|
39
|
+
.map((token) => token.trim().toLowerCase())
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
/** An extension token starts with a dot; anything else has to name a media type. */
|
|
42
|
+
const isAcceptToken = (token) => (token.startsWith(".") ? token.length > 1 : token.includes("/"));
|
|
43
|
+
/**
|
|
44
|
+
* Whether a file satisfies the native `accept` restriction. An extension token matches the file
|
|
45
|
+
* name, a media type matches the reported MIME type, and a `type/*` wildcard matches a whole
|
|
46
|
+
* family of them. A file that reports no MIME type at all can satisfy an extension but can never
|
|
47
|
+
* establish a MIME-only match, which is exactly what the platform does. A malformed `accept`
|
|
48
|
+
* value is ignored entirely rather than taken as a restriction that refuses everything, again
|
|
49
|
+
* mirroring the platform: a typo must not close the field.
|
|
50
|
+
*/
|
|
51
|
+
const matchesAccept = (file, accept) => {
|
|
52
|
+
const tokens = acceptTokens(accept);
|
|
53
|
+
if (tokens.length === 0 || !tokens.every(isAcceptToken))
|
|
54
|
+
return true;
|
|
55
|
+
const name = file.name.toLowerCase();
|
|
56
|
+
const type = file.type.toLowerCase();
|
|
57
|
+
return tokens.some((token) => {
|
|
58
|
+
if (token.startsWith("."))
|
|
59
|
+
return name.endsWith(token);
|
|
60
|
+
if (token.endsWith("/*"))
|
|
61
|
+
return type !== "" && type.startsWith(token.slice(0, -1));
|
|
62
|
+
return type !== "" && type === token;
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
const countMessage = (file, limit, multiple) => multiple
|
|
66
|
+
? `${file.name} was not added: at most ${limit} file${limit === 1 ? "" : "s"} can be selected.`
|
|
67
|
+
: `${file.name} was not added: this field holds one file.`;
|
|
68
|
+
/**
|
|
69
|
+
* Applies the whole policy to one batch, from either door.
|
|
70
|
+
*
|
|
71
|
+
* Each file is judged type first, then size, then count — the caller's order of severity — and a
|
|
72
|
+
* refusal never removes a file that was already accepted, so the valid part of a mixed batch
|
|
73
|
+
* still enters. A file the selection already holds is not a failure, it is the same choice
|
|
74
|
+
* arriving twice, so it is neither reported nor counted against the limit.
|
|
75
|
+
*
|
|
76
|
+
* A single-file field starts from an empty selection: the first accepted file replaces what was
|
|
77
|
+
* there and everything after it is a count failure.
|
|
78
|
+
*/
|
|
79
|
+
const selectFiles = ({ incoming, current, limits, }) => {
|
|
80
|
+
const maxSize = resolveMaxSize(limits.maxSize);
|
|
81
|
+
const maxFiles = resolveMaxFiles(limits.maxFiles, limits.multiple);
|
|
82
|
+
const files = limits.multiple ? [...current] : [];
|
|
83
|
+
const keys = new Set(current.map(fileKey));
|
|
84
|
+
const rejections = [];
|
|
85
|
+
let accepted = 0;
|
|
86
|
+
for (const file of incoming) {
|
|
87
|
+
if (!matchesAccept(file, limits.accept)) {
|
|
88
|
+
rejections.push({ file, code: "type", message: `${file.name} is not an accepted file type.` });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (maxSize !== undefined && file.size > maxSize) {
|
|
92
|
+
rejections.push({
|
|
93
|
+
file,
|
|
94
|
+
code: "size",
|
|
95
|
+
message: `${file.name} is larger than the ${formatBytes(maxSize)} limit.`,
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const key = fileKey(file);
|
|
100
|
+
if (keys.has(key))
|
|
101
|
+
continue;
|
|
102
|
+
if (maxFiles !== undefined && files.length >= maxFiles) {
|
|
103
|
+
rejections.push({ file, code: "count", message: countMessage(file, maxFiles, limits.multiple) });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
keys.add(key);
|
|
107
|
+
files.push(file);
|
|
108
|
+
accepted += 1;
|
|
109
|
+
}
|
|
110
|
+
return { files, rejections, accepted };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export { fileKey, formatBytes, matchesAccept, resolveMaxFiles, resolveMaxSize, selectFiles };
|
|
114
|
+
//# sourceMappingURL=fileUpload.rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileUpload.rules.js","sources":["../../../src/components/fileUpload.rules.ts"],"sourcesContent":["/* File selection rules for `FileUpload`.\n\n The native chooser and the drop zone are two doors into one policy: the same accept, the same\n per-file size and the same total count decide both, so a file the picker would refuse cannot\n slip in through a drop. Nothing here reads a browser global — every rule works from the\n metadata a `File` already carries — so the module is safe to evaluate during SSR and can be\n probed without a document.\n\n `FileUpload.tsx` composes the field around these rules; it owns no rule of its own. */\n\nexport type RejectionCode = \"type\" | \"size\" | \"count\";\n\n/** One file a selection operation refused, with the reason it was refused. */\nexport interface FileRejection {\n file: File;\n code: RejectionCode;\n message: string;\n}\n\nexport interface FileSelectionLimits {\n /** The native `accept` value, comma-separated MIME types and extensions. */\n accept?: string;\n /** Per-file byte limit, already normalized. */\n maxSize?: number;\n /** Total file limit, already normalized. */\n maxFiles?: number;\n /** Whether the field holds several files or replaces its selection with one. */\n multiple: boolean;\n}\n\nexport interface FileSelection {\n /** The selection the operation leaves behind. */\n files: File[];\n /** Everything the batch refused, in the order it arrived. */\n rejections: FileRejection[];\n /** How many of the incoming files entered the selection. */\n accepted: number;\n}\n\n/** The identity of one selection entry: the fields that make two `File`s the same choice. */\nexport const fileKey = (file: File) =>\n `${file.name}\\u0000${file.size}\\u0000${file.type}\\u0000${file.lastModified}`;\n\n/** The per-file byte limit as a whole number, or `undefined` when the caller set none. */\nexport const resolveMaxSize = (maxSize?: number) =>\n maxSize !== undefined && Number.isFinite(maxSize) && maxSize >= 0 ? Math.floor(maxSize) : undefined;\n\n/**\n * The total file limit, or `undefined` when the selection is unbounded. A single-file field\n * holds exactly one: the picker is told nothing about it, because that limit is what makes a\n * second file a replacement rather than an addition.\n */\nexport const resolveMaxFiles = (maxFiles: number | undefined, multiple: boolean) => {\n if (!multiple) return 1;\n return maxFiles !== undefined && Number.isFinite(maxFiles) && maxFiles >= 0\n ? Math.floor(maxFiles)\n : undefined;\n};\n\n/** A byte limit as the copy spells it: `2 MB`, never `2097152`. */\nexport const formatBytes = (bytes: number) => {\n const units = [\"B\", \"KB\", \"MB\", \"GB\"];\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit += 1;\n }\n return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;\n};\n\nconst acceptTokens = (accept?: string) =>\n (accept ?? \"\")\n .split(\",\")\n .map((token) => token.trim().toLowerCase())\n .filter(Boolean);\n\n/** An extension token starts with a dot; anything else has to name a media type. */\nconst isAcceptToken = (token: string) => (token.startsWith(\".\") ? token.length > 1 : token.includes(\"/\"));\n\n/**\n * Whether a file satisfies the native `accept` restriction. An extension token matches the file\n * name, a media type matches the reported MIME type, and a `type/*` wildcard matches a whole\n * family of them. A file that reports no MIME type at all can satisfy an extension but can never\n * establish a MIME-only match, which is exactly what the platform does. A malformed `accept`\n * value is ignored entirely rather than taken as a restriction that refuses everything, again\n * mirroring the platform: a typo must not close the field.\n */\nexport const matchesAccept = (file: File, accept?: string) => {\n const tokens = acceptTokens(accept);\n if (tokens.length === 0 || !tokens.every(isAcceptToken)) return true;\n\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n\n return tokens.some((token) => {\n if (token.startsWith(\".\")) return name.endsWith(token);\n if (token.endsWith(\"/*\")) return type !== \"\" && type.startsWith(token.slice(0, -1));\n return type !== \"\" && type === token;\n });\n};\n\nconst countMessage = (file: File, limit: number, multiple: boolean) =>\n multiple\n ? `${file.name} was not added: at most ${limit} file${limit === 1 ? \"\" : \"s\"} can be selected.`\n : `${file.name} was not added: this field holds one file.`;\n\n/**\n * Applies the whole policy to one batch, from either door.\n *\n * Each file is judged type first, then size, then count — the caller's order of severity — and a\n * refusal never removes a file that was already accepted, so the valid part of a mixed batch\n * still enters. A file the selection already holds is not a failure, it is the same choice\n * arriving twice, so it is neither reported nor counted against the limit.\n *\n * A single-file field starts from an empty selection: the first accepted file replaces what was\n * there and everything after it is a count failure.\n */\nexport const selectFiles = ({\n incoming,\n current,\n limits,\n}: {\n incoming: File[];\n current: File[];\n limits: FileSelectionLimits;\n}): FileSelection => {\n const maxSize = resolveMaxSize(limits.maxSize);\n const maxFiles = resolveMaxFiles(limits.maxFiles, limits.multiple);\n\n const files = limits.multiple ? [...current] : [];\n const keys = new Set(current.map(fileKey));\n const rejections: FileRejection[] = [];\n let accepted = 0;\n\n for (const file of incoming) {\n if (!matchesAccept(file, limits.accept)) {\n rejections.push({ file, code: \"type\", message: `${file.name} is not an accepted file type.` });\n continue;\n }\n\n if (maxSize !== undefined && file.size > maxSize) {\n rejections.push({\n file,\n code: \"size\",\n message: `${file.name} is larger than the ${formatBytes(maxSize)} limit.`,\n });\n continue;\n }\n\n const key = fileKey(file);\n if (keys.has(key)) continue;\n\n if (maxFiles !== undefined && files.length >= maxFiles) {\n rejections.push({ file, code: \"count\", message: countMessage(file, maxFiles, limits.multiple) });\n continue;\n }\n\n keys.add(key);\n files.push(file);\n accepted += 1;\n }\n\n return { files, rejections, accepted };\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a typed sequence stays open for extension.
|
|
3
|
+
*
|
|
4
|
+
* A node label is a word rather than a document, so a search only spans a few characters: long
|
|
5
|
+
* enough that typing quickly reads as one search, short enough that the next deliberate
|
|
6
|
+
* character starts a new one instead of extending a stale one.
|
|
7
|
+
*/
|
|
8
|
+
const typeaheadWindow = 500;
|
|
9
|
+
/** Whether a node holds children the tree can show. */
|
|
10
|
+
const isBranch = (item) => { var _a, _b; return ((_b = (_a = item.children) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0; };
|
|
11
|
+
/**
|
|
12
|
+
* The nodes in the order the tree presents them: a document-order walk that descends into an
|
|
13
|
+
* expanded branch and leaves a collapsed one alone.
|
|
14
|
+
*
|
|
15
|
+
* This is the one place that decides what "visible" means, and the keyboard, the typeahead and
|
|
16
|
+
* the roving tab stop all read it rather than walking the tree again — so they can never
|
|
17
|
+
* disagree about which node is where.
|
|
18
|
+
*/
|
|
19
|
+
const flattenTree = (items, expanded) => {
|
|
20
|
+
const rows = [];
|
|
21
|
+
const collect = (nodes, depth, ancestors, ancestorDisabled) => {
|
|
22
|
+
var _a, _b;
|
|
23
|
+
for (const item of nodes) {
|
|
24
|
+
const expandedHere = isBranch(item) && expanded.has(item.value);
|
|
25
|
+
rows.push({
|
|
26
|
+
item,
|
|
27
|
+
depth,
|
|
28
|
+
parent: (_a = ancestors[ancestors.length - 1]) !== null && _a !== void 0 ? _a : null,
|
|
29
|
+
ancestors,
|
|
30
|
+
expanded: expandedHere,
|
|
31
|
+
ancestorDisabled,
|
|
32
|
+
});
|
|
33
|
+
if (expandedHere) {
|
|
34
|
+
collect((_b = item.children) !== null && _b !== void 0 ? _b : [], depth + 1, [...ancestors, item.value], ancestorDisabled || item.disabled === true);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
collect(items, 0, [], false);
|
|
39
|
+
return rows;
|
|
40
|
+
};
|
|
41
|
+
/** The nearest of `ancestors`, deepest first, that the tree still shows. */
|
|
42
|
+
const nearestVisible = (ancestors, visible) => {
|
|
43
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
44
|
+
if (visible.has(ancestors[index]))
|
|
45
|
+
return ancestors[index];
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The APG typeahead search: the next *visible* node whose label starts with the characters typed
|
|
51
|
+
* so far, starting after `from` and wrapping at the end of the tree.
|
|
52
|
+
*
|
|
53
|
+
* A search moves focus and nothing else. Selection is the user's decision, so typing a letter can
|
|
54
|
+
* never change what is chosen, and a node that a typeahead lands on is not selected by arriving.
|
|
55
|
+
*/
|
|
56
|
+
const matchTypeahead = (rows, pattern, from) => {
|
|
57
|
+
const query = pattern.toLowerCase();
|
|
58
|
+
if (query.length === 0 || rows.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
const start = rows.findIndex((row) => row.item.value === from);
|
|
61
|
+
for (let step = 1; step <= rows.length; step += 1) {
|
|
62
|
+
const row = rows[(start + step + rows.length) % rows.length];
|
|
63
|
+
if (row.item.label.toLowerCase().startsWith(query))
|
|
64
|
+
return row;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Advances the typeahead buffer by one keystroke.
|
|
70
|
+
*
|
|
71
|
+
* Characters typed in quick succession are one search — `d`, `o`, `c` looks for a label starting
|
|
72
|
+
* with "doc" — and the *same* character typed again cycles through the nodes that start with it
|
|
73
|
+
* rather than searching for a two-letter word that almost certainly does not exist. A repeat
|
|
74
|
+
* neither clears nor extends the buffer, so the next different character still widens the
|
|
75
|
+
* single-character search it was part of.
|
|
76
|
+
*/
|
|
77
|
+
const typeaheadStep = (buffer, key, now) => {
|
|
78
|
+
if (buffer.text.length === 0 || now - buffer.time >= typeaheadWindow) {
|
|
79
|
+
return { buffer: { text: key, time: now }, pattern: key };
|
|
80
|
+
}
|
|
81
|
+
if (buffer.text === key) {
|
|
82
|
+
return { buffer: { text: buffer.text, time: now }, pattern: key };
|
|
83
|
+
}
|
|
84
|
+
return { buffer: { text: buffer.text + key, time: now }, pattern: buffer.text + key };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export { flattenTree, isBranch, matchTypeahead, nearestVisible, typeaheadStep, typeaheadWindow };
|
|
88
|
+
//# sourceMappingURL=tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.js","sources":["../../../src/components/tree.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\n/**\n * The finite static tree a `TreeView` presents.\n *\n * A node is data, never markup: the tree renders the roles, the nesting and the indentation\n * itself, so every node in it is reachable the same way whether it is a leaf or a branch.\n * `value` identifies a node throughout the tree and is unique; `label` is what the node is\n * called — its accessible name and what a typeahead search matches.\n */\nexport interface TreeViewItem {\n value: string;\n label: string;\n icon?: ReactNode;\n disabled?: boolean;\n children?: TreeViewItem[];\n}\n\n/** One node, as the tree currently shows it. */\nexport interface TreeRow {\n item: TreeViewItem;\n /** How deep the node sits, counted from the roots. */\n depth: number;\n /** The node that contains it, or `null` at the top level. */\n parent: string | null;\n /** Its ancestors, outermost first. */\n ancestors: string[];\n /** Whether this node's own children are shown. */\n expanded: boolean;\n /**\n * Whether a node above this one is disabled. `aria-disabled` is inherited down a subtree, and\n * this tree deliberately does not mean that: a node below an unavailable branch is its own\n * node, so the row has to be able to say it is not itself unavailable.\n */\n ancestorDisabled: boolean;\n}\n\n/** The characters a typeahead search has accumulated, and when the last one arrived. */\nexport interface TypeaheadBuffer {\n text: string;\n time: number;\n}\n\n/**\n * How long a typed sequence stays open for extension.\n *\n * A node label is a word rather than a document, so a search only spans a few characters: long\n * enough that typing quickly reads as one search, short enough that the next deliberate\n * character starts a new one instead of extending a stale one.\n */\nexport const typeaheadWindow = 500;\n\n/** Whether a node holds children the tree can show. */\nexport const isBranch = (item: TreeViewItem) => (item.children?.length ?? 0) > 0;\n\n/**\n * The nodes in the order the tree presents them: a document-order walk that descends into an\n * expanded branch and leaves a collapsed one alone.\n *\n * This is the one place that decides what \"visible\" means, and the keyboard, the typeahead and\n * the roving tab stop all read it rather than walking the tree again — so they can never\n * disagree about which node is where.\n */\nexport const flattenTree = (items: TreeViewItem[], expanded: ReadonlySet<string>): TreeRow[] => {\n const rows: TreeRow[] = [];\n\n const collect = (\n nodes: TreeViewItem[],\n depth: number,\n ancestors: string[],\n ancestorDisabled: boolean\n ) => {\n for (const item of nodes) {\n const expandedHere = isBranch(item) && expanded.has(item.value);\n rows.push({\n item,\n depth,\n parent: ancestors[ancestors.length - 1] ?? null,\n ancestors,\n expanded: expandedHere,\n ancestorDisabled,\n });\n if (expandedHere) {\n collect(\n item.children ?? [],\n depth + 1,\n [...ancestors, item.value],\n ancestorDisabled || item.disabled === true\n );\n }\n }\n };\n\n collect(items, 0, [], false);\n return rows;\n};\n\n/** The nearest of `ancestors`, deepest first, that the tree still shows. */\nexport const nearestVisible = (\n ancestors: string[],\n visible: ReadonlySet<string>\n): string | null => {\n for (let index = ancestors.length - 1; index >= 0; index -= 1) {\n if (visible.has(ancestors[index])) return ancestors[index];\n }\n return null;\n};\n\n/**\n * The APG typeahead search: the next *visible* node whose label starts with the characters typed\n * so far, starting after `from` and wrapping at the end of the tree.\n *\n * A search moves focus and nothing else. Selection is the user's decision, so typing a letter can\n * never change what is chosen, and a node that a typeahead lands on is not selected by arriving.\n */\nexport const matchTypeahead = (\n rows: TreeRow[],\n pattern: string,\n from: string | null\n): TreeRow | null => {\n const query = pattern.toLowerCase();\n if (query.length === 0 || rows.length === 0) return null;\n\n const start = rows.findIndex((row) => row.item.value === from);\n for (let step = 1; step <= rows.length; step += 1) {\n const row = rows[(start + step + rows.length) % rows.length];\n if (row.item.label.toLowerCase().startsWith(query)) return row;\n }\n return null;\n};\n\n/**\n * Advances the typeahead buffer by one keystroke.\n *\n * Characters typed in quick succession are one search — `d`, `o`, `c` looks for a label starting\n * with \"doc\" — and the *same* character typed again cycles through the nodes that start with it\n * rather than searching for a two-letter word that almost certainly does not exist. A repeat\n * neither clears nor extends the buffer, so the next different character still widens the\n * single-character search it was part of.\n */\nexport const typeaheadStep = (\n buffer: TypeaheadBuffer,\n key: string,\n now: number\n): { buffer: TypeaheadBuffer; pattern: string } => {\n if (buffer.text.length === 0 || now - buffer.time >= typeaheadWindow) {\n return { buffer: { text: key, time: now }, pattern: key };\n }\n\n if (buffer.text === key) {\n return { buffer: { text: buffer.text, time: now }, pattern: key };\n }\n\n return { buffer: { text: buffer.text + key, time: now }, pattern: buffer.text + key };\n};\n"],"names":[],"mappings":"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"}
|