@charcuterie/ui 2.18.0 → 3.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 +10 -0
- package/dist/Badge/Badge.d.ts +33 -3
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +20 -2
- package/dist/Badge/Badge.js.map +1 -1
- package/dist/Board/Board.d.ts +119 -0
- package/dist/Board/Board.d.ts.map +1 -0
- package/dist/Board/Board.js +165 -0
- package/dist/Board/Board.js.map +1 -0
- package/dist/Board/BoardCard.d.ts +120 -0
- package/dist/Board/BoardCard.d.ts.map +1 -0
- package/dist/Board/BoardCard.js +77 -0
- package/dist/Board/BoardCard.js.map +1 -0
- package/dist/Board/BoardLaneList.d.ts +103 -0
- package/dist/Board/BoardLaneList.d.ts.map +1 -0
- package/dist/Board/BoardLaneList.js +69 -0
- package/dist/Board/BoardLaneList.js.map +1 -0
- package/dist/Board/boardMove.d.ts +116 -0
- package/dist/Board/boardMove.d.ts.map +1 -0
- package/dist/Board/boardMove.js +116 -0
- package/dist/Board/boardMove.js.map +1 -0
- package/dist/Board/useBoardDrag.d.ts +44 -0
- package/dist/Board/useBoardDrag.d.ts.map +1 -0
- package/dist/Board/useBoardDrag.js +237 -0
- package/dist/Board/useBoardDrag.js.map +1 -0
- package/dist/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/Checkbox/Checkbox.js +15 -4
- package/dist/Checkbox/Checkbox.js.map +1 -1
- package/dist/DataTable/DataTable.d.ts +159 -0
- package/dist/DataTable/DataTable.d.ts.map +1 -0
- package/dist/DataTable/DataTable.js +197 -0
- package/dist/DataTable/DataTable.js.map +1 -0
- package/dist/DatePicker/DateGrid.d.ts +79 -0
- package/dist/DatePicker/DateGrid.d.ts.map +1 -0
- package/dist/DatePicker/DateGrid.js +220 -0
- package/dist/DatePicker/DateGrid.js.map +1 -0
- package/dist/DatePicker/DatePicker.d.ts +138 -0
- package/dist/DatePicker/DatePicker.d.ts.map +1 -0
- package/dist/DatePicker/DatePicker.js +384 -0
- package/dist/DatePicker/DatePicker.js.map +1 -0
- package/dist/DatePicker/datePresets.d.ts +29 -0
- package/dist/DatePicker/datePresets.d.ts.map +1 -0
- package/dist/DatePicker/datePresets.js +28 -0
- package/dist/DatePicker/datePresets.js.map +1 -0
- package/dist/DatePicker/parseDateInput.d.ts +106 -0
- package/dist/DatePicker/parseDateInput.d.ts.map +1 -0
- package/dist/DatePicker/parseDateInput.js +419 -0
- package/dist/DatePicker/parseDateInput.js.map +1 -0
- package/dist/DatePicker/plainDate.d.ts +167 -0
- package/dist/DatePicker/plainDate.d.ts.map +1 -0
- package/dist/DatePicker/plainDate.js +272 -0
- package/dist/DatePicker/plainDate.js.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts +147 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
- package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.js +379 -0
- package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.js +303 -0
- package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
- package/dist/Select/Select.d.ts +36 -1
- package/dist/Select/Select.d.ts.map +1 -1
- package/dist/Select/Select.js +38 -2
- package/dist/Select/Select.js.map +1 -1
- package/dist/categoricalStyles.d.ts +53 -0
- package/dist/categoricalStyles.d.ts.map +1 -0
- package/dist/categoricalStyles.js +195 -0
- package/dist/categoricalStyles.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Badge/Badge.mdx +46 -0
- package/src/Badge/Badge.stories.tsx +98 -4
- package/src/Badge/Badge.test.tsx +157 -2
- package/src/Badge/Badge.tsx +49 -3
- package/src/Board/Board.mdx +199 -0
- package/src/Board/Board.stories.tsx +707 -0
- package/src/Board/Board.test.tsx +310 -0
- package/src/Board/Board.tsx +333 -0
- package/src/Board/BoardCard.tsx +290 -0
- package/src/Board/BoardLaneList.tsx +275 -0
- package/src/Board/boardMove.test.ts +213 -0
- package/src/Board/boardMove.ts +225 -0
- package/src/Board/useBoardDrag.ts +413 -0
- package/src/Checkbox/Checkbox.tsx +34 -3
- package/src/DataTable/DataTable.mdx +123 -0
- package/src/DataTable/DataTable.stories.tsx +411 -0
- package/src/DataTable/DataTable.test.tsx +386 -0
- package/src/DataTable/DataTable.tsx +497 -0
- package/src/DatePicker/DateGrid.tsx +467 -0
- package/src/DatePicker/DatePicker.mdx +170 -0
- package/src/DatePicker/DatePicker.stories.tsx +268 -0
- package/src/DatePicker/DatePicker.test.tsx +455 -0
- package/src/DatePicker/DatePicker.tsx +852 -0
- package/src/DatePicker/datePresets.ts +34 -0
- package/src/DatePicker/parseDateInput.test.ts +187 -0
- package/src/DatePicker/parseDateInput.ts +629 -0
- package/src/DatePicker/plainDate.test.ts +375 -0
- package/src/DatePicker/plainDate.ts +465 -0
- package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
- package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
- package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
- package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
- package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
- package/src/MarkdownEditor/markdownCommands.ts +566 -0
- package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
- package/src/MarkdownEditor/markdownSpans.ts +458 -0
- package/src/Select/Select.mdx +44 -0
- package/src/Select/Select.stories.tsx +55 -0
- package/src/Select/Select.test.tsx +89 -1
- package/src/Select/Select.tsx +58 -2
- package/src/argTypes.storyHelpers.ts +39 -0
- package/src/categoricalStyles.ts +244 -0
- package/src/index.ts +89 -0
- package/src/sourceRules.test.ts +52 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A calendar date, and deliberately not an instant.
|
|
3
|
+
*
|
|
4
|
+
* ### The bug this file exists to make impossible
|
|
5
|
+
*
|
|
6
|
+
* A due date is not a moment. "This is due on the 19th" is true in
|
|
7
|
+
* Denver and true in Tokyo, and it does not become the 18th because
|
|
8
|
+
* a laptop crossed a meridian. The classic date-picker defect is
|
|
9
|
+
* that the value is a `Date` — which is an instant, a count of
|
|
10
|
+
* milliseconds — and every read of it goes through the *browser's*
|
|
11
|
+
* timezone. Pick the 19th at 23:30 in Denver, store
|
|
12
|
+
* `new Date("2026-08-19").toISOString()`, and the server, the next
|
|
13
|
+
* device, and the same device after a flight all disagree about
|
|
14
|
+
* which day you meant. It is a one-day error, it only appears near
|
|
15
|
+
* midnight or after travel, and it is invisible in every test
|
|
16
|
+
* written at noon.
|
|
17
|
+
*
|
|
18
|
+
* So the value here is a **plain date**: a year, a month, and a day,
|
|
19
|
+
* with no time, no offset, and no zone. It is stored as an ISO
|
|
20
|
+
* `YYYY-MM-DD` string — which is exactly `<input type="date">`'s own
|
|
21
|
+
* `value` format, so a consumer can move between this component and
|
|
22
|
+
* the native control without a migration.
|
|
23
|
+
*
|
|
24
|
+
* ### There is no `Date` arithmetic in here at all
|
|
25
|
+
*
|
|
26
|
+
* Not "carefully-written `Date` arithmetic" — none. Every operation
|
|
27
|
+
* below is integer maths on a **day number**: the count of days
|
|
28
|
+
* since 1970-01-01, computed with Howard Hinnant's `days_from_civil`
|
|
29
|
+
* / `civil_from_days` (public domain, the algorithm C++20's
|
|
30
|
+
* `<chrono>` and Rust's `chrono` both use). Adding a day is `+ 1`.
|
|
31
|
+
* That is a proleptic Gregorian calendar with no DST, no leap
|
|
32
|
+
* seconds, and no zone, which is what a calendar date actually is.
|
|
33
|
+
*
|
|
34
|
+
* A `Date` appears in exactly two places, both of them named and
|
|
35
|
+
* both of them fenced:
|
|
36
|
+
*
|
|
37
|
+
* - `getLocalPlainDate`, which is the **one** conversion from an
|
|
38
|
+
* instant to a calendar date, and which requires you to say
|
|
39
|
+
* which zone's calendar you mean.
|
|
40
|
+
* - `formatPlainDate` and its callers, which hand `Intl` a date
|
|
41
|
+
* built at UTC midnight and format it with `timeZone: "UTC"`, so
|
|
42
|
+
* the formatter cannot shift the day either.
|
|
43
|
+
*
|
|
44
|
+
* `plainDate.test.ts` re-runs the whole suite under four `TZ`
|
|
45
|
+
* values, including UTC+14 and UTC-11, and asserts every answer is
|
|
46
|
+
* byte-identical.
|
|
47
|
+
*
|
|
48
|
+
* ### Why not `Temporal`
|
|
49
|
+
*
|
|
50
|
+
* `Temporal.PlainDate` is this type, done properly, and it is
|
|
51
|
+
* available in the Node this repo builds on. It is not available in
|
|
52
|
+
* every browser a published package has to run in, and the polyfill
|
|
53
|
+
* is ~50 KB gz — an order of magnitude more than this entire
|
|
54
|
+
* component. The functions below are the ~80 lines of it a date
|
|
55
|
+
* picker needs. When `Temporal` is baseline, this file becomes a
|
|
56
|
+
* thin adapter and the public API — ISO strings — does not change,
|
|
57
|
+
* which is the reason the API is ISO strings.
|
|
58
|
+
*/
|
|
59
|
+
/** Year, month (1-12), day (1-31). No time, no zone. */
|
|
60
|
+
export type PlainDate = {
|
|
61
|
+
day: number;
|
|
62
|
+
month: number;
|
|
63
|
+
year: number;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* A start and an end, either of which may be absent — because a
|
|
67
|
+
* half-picked range is a real state a user spends time in, not an
|
|
68
|
+
* error. `start` with a null `end` is "I have clicked once".
|
|
69
|
+
*/
|
|
70
|
+
export type DateRange = {
|
|
71
|
+
end: null | string;
|
|
72
|
+
start: null | string;
|
|
73
|
+
};
|
|
74
|
+
export declare const getIsLeapYear: (year: number) => boolean;
|
|
75
|
+
export declare const getDaysInMonth: (year: number, month: number) => number;
|
|
76
|
+
export declare const getIsValidPlainDate: ({ day, month, year, }: PlainDate) => boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Days since 1970-01-01, by Hinnant's `days_from_civil`.
|
|
79
|
+
*
|
|
80
|
+
* Every other operation in this file is expressed through this and
|
|
81
|
+
* its inverse, which is why "add a month, then add 40 days, then
|
|
82
|
+
* ask what weekday it is" cannot drift: there is one representation
|
|
83
|
+
* and it is an integer.
|
|
84
|
+
*/
|
|
85
|
+
export declare const getDayNumber: ({ day, month, year, }: PlainDate) => number;
|
|
86
|
+
/** The inverse — Hinnant's `civil_from_days`. */
|
|
87
|
+
export declare const getPlainDateFromDayNumber: (dayNumber: number) => PlainDate;
|
|
88
|
+
export declare const addDays: (date: PlainDate, days: number) => PlainDate;
|
|
89
|
+
/**
|
|
90
|
+
* Month arithmetic clamps the day rather than overflowing, which is
|
|
91
|
+
* the answer every calendar app gives and the one `Date` does not:
|
|
92
|
+
* `new Date(2026, 0, 31)` plus a month is **3 March**, because 31
|
|
93
|
+
* February rolls forward. One month after 31 January is 28
|
|
94
|
+
* February.
|
|
95
|
+
*/
|
|
96
|
+
export declare const addMonths: (date: PlainDate, months: number) => PlainDate;
|
|
97
|
+
export declare const comparePlainDates: (first: PlainDate, second: PlainDate) => number;
|
|
98
|
+
export declare const getIsSameDay: (first: PlainDate, second: PlainDate) => boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Whole days from `from` to `to`. Docket's staleness threshold is
|
|
101
|
+
* counted in days, and this is the subtraction that counts it —
|
|
102
|
+
* with no hour-of-day and no DST to make "14 days" occasionally mean
|
|
103
|
+
* 13.958.
|
|
104
|
+
*/
|
|
105
|
+
export declare const getDaysBetween: (from: PlainDate, to: PlainDate) => number;
|
|
106
|
+
export declare const clampPlainDate: (date: PlainDate, minimum?: PlainDate, maximum?: PlainDate) => PlainDate;
|
|
107
|
+
export declare const getIsWithinRange: (date: PlainDate, minimum?: PlainDate, maximum?: PlainDate) => boolean;
|
|
108
|
+
/**
|
|
109
|
+
* 0 is Sunday, 6 is Saturday — the numbering `Intl`'s `weekday`
|
|
110
|
+
* option and `Date.prototype.getDay` both use, so the two never
|
|
111
|
+
* need reconciling at a call site.
|
|
112
|
+
*
|
|
113
|
+
* Day number 0 (1970-01-01) was a Thursday, which is the `+ 4`.
|
|
114
|
+
*/
|
|
115
|
+
export declare const getWeekday: (date: PlainDate) => number;
|
|
116
|
+
export declare const toIsoDate: (date: PlainDate) => string;
|
|
117
|
+
/**
|
|
118
|
+
* `null` rather than a throw, and `null` rather than a "best
|
|
119
|
+
* effort" date. A string that is not a calendar date has no nearest
|
|
120
|
+
* calendar date, and inventing one is the silent-guess failure this
|
|
121
|
+
* component is built to avoid.
|
|
122
|
+
*/
|
|
123
|
+
export declare const parseIsoDate: (isoDate: null | string | undefined) => null | PlainDate;
|
|
124
|
+
export type PlainDateFormatOptions = Intl.DateTimeFormatOptions & {
|
|
125
|
+
locale?: string;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* `timeZone: "UTC"` is forced, and it is not configurable.
|
|
129
|
+
*
|
|
130
|
+
* The instant above is UTC midnight, so formatting it in any other
|
|
131
|
+
* zone renders the previous or the next day — which is the same
|
|
132
|
+
* one-day error as storing an instant, arriving through the display
|
|
133
|
+
* layer instead. There is no legitimate reason to format a calendar
|
|
134
|
+
* date in a zone: the date *is* the answer.
|
|
135
|
+
*/
|
|
136
|
+
export declare const formatPlainDate: (date: PlainDate, { locale, ...options }?: PlainDateFormatOptions) => string;
|
|
137
|
+
/**
|
|
138
|
+
* The one instant-to-calendar-date conversion in the library, and it
|
|
139
|
+
* makes you name the zone.
|
|
140
|
+
*
|
|
141
|
+
* Defaulting `timeZone` to the device's own resolved zone is right
|
|
142
|
+
* for "what is today on this screen"; passing one explicitly is
|
|
143
|
+
* right for "what is today for this household". Either way the
|
|
144
|
+
* question is asked out loud, which is the difference between this
|
|
145
|
+
* and `new Date().toISOString().slice(0, 10)` — a line that appears
|
|
146
|
+
* in three of the fleet's repos and is wrong for everybody west of
|
|
147
|
+
* Greenwich after 17:00.
|
|
148
|
+
*/
|
|
149
|
+
export declare const getLocalPlainDate: (now: Date, timeZone?: string) => PlainDate;
|
|
150
|
+
export declare const getFirstDayOfWeek: (locale?: string) => number;
|
|
151
|
+
/**
|
|
152
|
+
* The seven weekday names, rotated so index 0 is the locale's own
|
|
153
|
+
* first day. A fixed sample week is formatted rather than a
|
|
154
|
+
* hand-written table, so every locale the browser knows is covered
|
|
155
|
+
* and nothing here needs translating.
|
|
156
|
+
*/
|
|
157
|
+
export declare const getWeekdayNames: ({ firstDayOfWeek, locale, weekday, }: {
|
|
158
|
+
firstDayOfWeek: number;
|
|
159
|
+
locale?: string;
|
|
160
|
+
weekday: "long" | "narrow" | "short";
|
|
161
|
+
}) => string[];
|
|
162
|
+
/** January-first month names, for the parser and the header. */
|
|
163
|
+
export declare const getMonthNames: ({ locale, month, }: {
|
|
164
|
+
locale?: string;
|
|
165
|
+
month: "long" | "short";
|
|
166
|
+
}) => string[];
|
|
167
|
+
//# sourceMappingURL=plainDate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plainDate.d.ts","sourceRoot":"","sources":["../../src/DatePicker/plainDate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A calendar date, and deliberately not an instant.
|
|
3
|
+
*
|
|
4
|
+
* ### The bug this file exists to make impossible
|
|
5
|
+
*
|
|
6
|
+
* A due date is not a moment. "This is due on the 19th" is true in
|
|
7
|
+
* Denver and true in Tokyo, and it does not become the 18th because
|
|
8
|
+
* a laptop crossed a meridian. The classic date-picker defect is
|
|
9
|
+
* that the value is a `Date` — which is an instant, a count of
|
|
10
|
+
* milliseconds — and every read of it goes through the *browser's*
|
|
11
|
+
* timezone. Pick the 19th at 23:30 in Denver, store
|
|
12
|
+
* `new Date("2026-08-19").toISOString()`, and the server, the next
|
|
13
|
+
* device, and the same device after a flight all disagree about
|
|
14
|
+
* which day you meant. It is a one-day error, it only appears near
|
|
15
|
+
* midnight or after travel, and it is invisible in every test
|
|
16
|
+
* written at noon.
|
|
17
|
+
*
|
|
18
|
+
* So the value here is a **plain date**: a year, a month, and a day,
|
|
19
|
+
* with no time, no offset, and no zone. It is stored as an ISO
|
|
20
|
+
* `YYYY-MM-DD` string — which is exactly `<input type="date">`'s own
|
|
21
|
+
* `value` format, so a consumer can move between this component and
|
|
22
|
+
* the native control without a migration.
|
|
23
|
+
*
|
|
24
|
+
* ### There is no `Date` arithmetic in here at all
|
|
25
|
+
*
|
|
26
|
+
* Not "carefully-written `Date` arithmetic" — none. Every operation
|
|
27
|
+
* below is integer maths on a **day number**: the count of days
|
|
28
|
+
* since 1970-01-01, computed with Howard Hinnant's `days_from_civil`
|
|
29
|
+
* / `civil_from_days` (public domain, the algorithm C++20's
|
|
30
|
+
* `<chrono>` and Rust's `chrono` both use). Adding a day is `+ 1`.
|
|
31
|
+
* That is a proleptic Gregorian calendar with no DST, no leap
|
|
32
|
+
* seconds, and no zone, which is what a calendar date actually is.
|
|
33
|
+
*
|
|
34
|
+
* A `Date` appears in exactly two places, both of them named and
|
|
35
|
+
* both of them fenced:
|
|
36
|
+
*
|
|
37
|
+
* - `getLocalPlainDate`, which is the **one** conversion from an
|
|
38
|
+
* instant to a calendar date, and which requires you to say
|
|
39
|
+
* which zone's calendar you mean.
|
|
40
|
+
* - `formatPlainDate` and its callers, which hand `Intl` a date
|
|
41
|
+
* built at UTC midnight and format it with `timeZone: "UTC"`, so
|
|
42
|
+
* the formatter cannot shift the day either.
|
|
43
|
+
*
|
|
44
|
+
* `plainDate.test.ts` re-runs the whole suite under four `TZ`
|
|
45
|
+
* values, including UTC+14 and UTC-11, and asserts every answer is
|
|
46
|
+
* byte-identical.
|
|
47
|
+
*
|
|
48
|
+
* ### Why not `Temporal`
|
|
49
|
+
*
|
|
50
|
+
* `Temporal.PlainDate` is this type, done properly, and it is
|
|
51
|
+
* available in the Node this repo builds on. It is not available in
|
|
52
|
+
* every browser a published package has to run in, and the polyfill
|
|
53
|
+
* is ~50 KB gz — an order of magnitude more than this entire
|
|
54
|
+
* component. The functions below are the ~80 lines of it a date
|
|
55
|
+
* picker needs. When `Temporal` is baseline, this file becomes a
|
|
56
|
+
* thin adapter and the public API — ISO strings — does not change,
|
|
57
|
+
* which is the reason the API is ISO strings.
|
|
58
|
+
*/
|
|
59
|
+
const MONTH_LENGTHS = [
|
|
60
|
+
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
|
|
61
|
+
];
|
|
62
|
+
const ISO_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
63
|
+
export const getIsLeapYear = (year) => (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
64
|
+
export const getDaysInMonth = (year, month) => month === 2 && getIsLeapYear(year)
|
|
65
|
+
? 29
|
|
66
|
+
: (MONTH_LENGTHS[month - 1] ?? 30);
|
|
67
|
+
export const getIsValidPlainDate = ({ day, month, year, }) => Number.isInteger(year) &&
|
|
68
|
+
Number.isInteger(month) &&
|
|
69
|
+
Number.isInteger(day) &&
|
|
70
|
+
year >= 1 &&
|
|
71
|
+
year <= 9999 &&
|
|
72
|
+
month >= 1 &&
|
|
73
|
+
month <= 12 &&
|
|
74
|
+
day >= 1 &&
|
|
75
|
+
day <= getDaysInMonth(year, month);
|
|
76
|
+
/**
|
|
77
|
+
* Days since 1970-01-01, by Hinnant's `days_from_civil`.
|
|
78
|
+
*
|
|
79
|
+
* Every other operation in this file is expressed through this and
|
|
80
|
+
* its inverse, which is why "add a month, then add 40 days, then
|
|
81
|
+
* ask what weekday it is" cannot drift: there is one representation
|
|
82
|
+
* and it is an integer.
|
|
83
|
+
*/
|
|
84
|
+
export const getDayNumber = ({ day, month, year, }) => {
|
|
85
|
+
const shiftedYear = year - (month <= 2 ? 1 : 0);
|
|
86
|
+
const era = Math.floor(shiftedYear / 400);
|
|
87
|
+
const yearOfEra = shiftedYear - era * 400;
|
|
88
|
+
const dayOfYear = Math.floor((153 * (month + (month > 2 ? -3 : 9)) + 2) / 5) +
|
|
89
|
+
day -
|
|
90
|
+
1;
|
|
91
|
+
const dayOfEra = yearOfEra * 365 +
|
|
92
|
+
Math.floor(yearOfEra / 4) -
|
|
93
|
+
Math.floor(yearOfEra / 100) +
|
|
94
|
+
dayOfYear;
|
|
95
|
+
return era * 146097 + dayOfEra - 719468;
|
|
96
|
+
};
|
|
97
|
+
/** The inverse — Hinnant's `civil_from_days`. */
|
|
98
|
+
export const getPlainDateFromDayNumber = (dayNumber) => {
|
|
99
|
+
const shifted = dayNumber + 719468;
|
|
100
|
+
const era = Math.floor(shifted / 146097);
|
|
101
|
+
const dayOfEra = shifted - era * 146097;
|
|
102
|
+
const yearOfEra = Math.floor((dayOfEra -
|
|
103
|
+
Math.floor(dayOfEra / 1460) +
|
|
104
|
+
Math.floor(dayOfEra / 36524) -
|
|
105
|
+
Math.floor(dayOfEra / 146096)) /
|
|
106
|
+
365);
|
|
107
|
+
const dayOfYear = dayOfEra -
|
|
108
|
+
(365 * yearOfEra +
|
|
109
|
+
Math.floor(yearOfEra / 4) -
|
|
110
|
+
Math.floor(yearOfEra / 100));
|
|
111
|
+
const monthPrime = Math.floor((5 * dayOfYear + 2) / 153);
|
|
112
|
+
const day = dayOfYear - Math.floor((153 * monthPrime + 2) / 5) + 1;
|
|
113
|
+
const month = monthPrime + (monthPrime < 10 ? 3 : -9);
|
|
114
|
+
return {
|
|
115
|
+
day,
|
|
116
|
+
month,
|
|
117
|
+
year: yearOfEra + era * 400 + (month <= 2 ? 1 : 0),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
export const addDays = (date, days) => getPlainDateFromDayNumber(getDayNumber(date) + days);
|
|
121
|
+
/**
|
|
122
|
+
* Month arithmetic clamps the day rather than overflowing, which is
|
|
123
|
+
* the answer every calendar app gives and the one `Date` does not:
|
|
124
|
+
* `new Date(2026, 0, 31)` plus a month is **3 March**, because 31
|
|
125
|
+
* February rolls forward. One month after 31 January is 28
|
|
126
|
+
* February.
|
|
127
|
+
*/
|
|
128
|
+
export const addMonths = (date, months) => {
|
|
129
|
+
const monthIndex = date.year * 12 + (date.month - 1) + months;
|
|
130
|
+
const year = Math.floor(monthIndex / 12);
|
|
131
|
+
const month = monthIndex - year * 12 + 1;
|
|
132
|
+
return {
|
|
133
|
+
day: Math.min(date.day, getDaysInMonth(year, month)),
|
|
134
|
+
month,
|
|
135
|
+
year,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
export const comparePlainDates = (first, second) => getDayNumber(first) - getDayNumber(second);
|
|
139
|
+
export const getIsSameDay = (first, second) => comparePlainDates(first, second) === 0;
|
|
140
|
+
/**
|
|
141
|
+
* Whole days from `from` to `to`. Docket's staleness threshold is
|
|
142
|
+
* counted in days, and this is the subtraction that counts it —
|
|
143
|
+
* with no hour-of-day and no DST to make "14 days" occasionally mean
|
|
144
|
+
* 13.958.
|
|
145
|
+
*/
|
|
146
|
+
export const getDaysBetween = (from, to) => getDayNumber(to) - getDayNumber(from);
|
|
147
|
+
export const clampPlainDate = (date, minimum, maximum) => {
|
|
148
|
+
if (minimum && comparePlainDates(date, minimum) < 0) {
|
|
149
|
+
return minimum;
|
|
150
|
+
}
|
|
151
|
+
if (maximum && comparePlainDates(date, maximum) > 0) {
|
|
152
|
+
return maximum;
|
|
153
|
+
}
|
|
154
|
+
return date;
|
|
155
|
+
};
|
|
156
|
+
export const getIsWithinRange = (date, minimum, maximum) => (!minimum || comparePlainDates(date, minimum) >= 0) &&
|
|
157
|
+
(!maximum || comparePlainDates(date, maximum) <= 0);
|
|
158
|
+
/**
|
|
159
|
+
* 0 is Sunday, 6 is Saturday — the numbering `Intl`'s `weekday`
|
|
160
|
+
* option and `Date.prototype.getDay` both use, so the two never
|
|
161
|
+
* need reconciling at a call site.
|
|
162
|
+
*
|
|
163
|
+
* Day number 0 (1970-01-01) was a Thursday, which is the `+ 4`.
|
|
164
|
+
*/
|
|
165
|
+
export const getWeekday = (date) => {
|
|
166
|
+
const dayNumber = getDayNumber(date);
|
|
167
|
+
return ((((dayNumber % 7) + 7) % 7) + 4) % 7;
|
|
168
|
+
};
|
|
169
|
+
const pad = (value, length) => String(value).padStart(length, "0");
|
|
170
|
+
export const toIsoDate = (date) => `${pad(date.year, 4)}-${pad(date.month, 2)}-${pad(date.day, 2)}`;
|
|
171
|
+
/**
|
|
172
|
+
* `null` rather than a throw, and `null` rather than a "best
|
|
173
|
+
* effort" date. A string that is not a calendar date has no nearest
|
|
174
|
+
* calendar date, and inventing one is the silent-guess failure this
|
|
175
|
+
* component is built to avoid.
|
|
176
|
+
*/
|
|
177
|
+
export const parseIsoDate = (isoDate) => {
|
|
178
|
+
if (!isoDate) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const match = ISO_DATE_PATTERN.exec(isoDate);
|
|
182
|
+
if (!match) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const date = {
|
|
186
|
+
day: Number(match[3]),
|
|
187
|
+
month: Number(match[2]),
|
|
188
|
+
year: Number(match[1]),
|
|
189
|
+
};
|
|
190
|
+
return getIsValidPlainDate(date) ? date : null;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* A `Date` pinned at UTC midnight, for `Intl` and for nothing else.
|
|
194
|
+
*
|
|
195
|
+
* `Date.UTC` is not used, because it maps years 0-99 onto 1900-1999
|
|
196
|
+
* — a two-digit year silently becomes the twentieth century inside
|
|
197
|
+
* the formatter, which is a bug the *parser* takes an explicit
|
|
198
|
+
* position on and the formatter has no business re-deciding.
|
|
199
|
+
*/
|
|
200
|
+
const toUtcInstant = (date) => {
|
|
201
|
+
const instant = new Date(0);
|
|
202
|
+
instant.setUTCFullYear(date.year, date.month - 1, date.day);
|
|
203
|
+
instant.setUTCHours(0, 0, 0, 0);
|
|
204
|
+
return instant;
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* `timeZone: "UTC"` is forced, and it is not configurable.
|
|
208
|
+
*
|
|
209
|
+
* The instant above is UTC midnight, so formatting it in any other
|
|
210
|
+
* zone renders the previous or the next day — which is the same
|
|
211
|
+
* one-day error as storing an instant, arriving through the display
|
|
212
|
+
* layer instead. There is no legitimate reason to format a calendar
|
|
213
|
+
* date in a zone: the date *is* the answer.
|
|
214
|
+
*/
|
|
215
|
+
export const formatPlainDate = (date, { locale, ...options } = {}) => new Intl.DateTimeFormat(locale, {
|
|
216
|
+
...options,
|
|
217
|
+
timeZone: "UTC",
|
|
218
|
+
}).format(toUtcInstant(date));
|
|
219
|
+
/**
|
|
220
|
+
* The one instant-to-calendar-date conversion in the library, and it
|
|
221
|
+
* makes you name the zone.
|
|
222
|
+
*
|
|
223
|
+
* Defaulting `timeZone` to the device's own resolved zone is right
|
|
224
|
+
* for "what is today on this screen"; passing one explicitly is
|
|
225
|
+
* right for "what is today for this household". Either way the
|
|
226
|
+
* question is asked out loud, which is the difference between this
|
|
227
|
+
* and `new Date().toISOString().slice(0, 10)` — a line that appears
|
|
228
|
+
* in three of the fleet's repos and is wrong for everybody west of
|
|
229
|
+
* Greenwich after 17:00.
|
|
230
|
+
*/
|
|
231
|
+
export const getLocalPlainDate = (now, timeZone) => {
|
|
232
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
233
|
+
day: "2-digit",
|
|
234
|
+
month: "2-digit",
|
|
235
|
+
timeZone,
|
|
236
|
+
year: "numeric",
|
|
237
|
+
}).formatToParts(now);
|
|
238
|
+
const read = (type) => Number(parts.find((part) => part.type === type)?.value ??
|
|
239
|
+
"0");
|
|
240
|
+
return {
|
|
241
|
+
day: read("day"),
|
|
242
|
+
month: read("month"),
|
|
243
|
+
year: read("year"),
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
export const getFirstDayOfWeek = (locale) => {
|
|
247
|
+
const resolved = new Intl.Locale(locale ??
|
|
248
|
+
new Intl.DateTimeFormat().resolvedOptions().locale);
|
|
249
|
+
const firstDay = resolved.getWeekInfo?.().firstDay ??
|
|
250
|
+
resolved.weekInfo?.firstDay;
|
|
251
|
+
// `getWeekInfo` numbers Monday 1 through Sunday 7; this file
|
|
252
|
+
// numbers Sunday 0 through Saturday 6.
|
|
253
|
+
return firstDay === undefined ? 0 : firstDay % 7;
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* The seven weekday names, rotated so index 0 is the locale's own
|
|
257
|
+
* first day. A fixed sample week is formatted rather than a
|
|
258
|
+
* hand-written table, so every locale the browser knows is covered
|
|
259
|
+
* and nothing here needs translating.
|
|
260
|
+
*/
|
|
261
|
+
export const getWeekdayNames = ({ firstDayOfWeek, locale, weekday, }) => {
|
|
262
|
+
// 2026-08-16 is a Sunday, so `+ index` walks Sunday to Saturday.
|
|
263
|
+
const sundayDayNumber = getDayNumber({
|
|
264
|
+
day: 16,
|
|
265
|
+
month: 8,
|
|
266
|
+
year: 2026,
|
|
267
|
+
});
|
|
268
|
+
return Array.from({ length: 7 }, (_unused, index) => formatPlainDate(getPlainDateFromDayNumber(sundayDayNumber + ((firstDayOfWeek + index) % 7)), { locale, weekday }));
|
|
269
|
+
};
|
|
270
|
+
/** January-first month names, for the parser and the header. */
|
|
271
|
+
export const getMonthNames = ({ locale, month, }) => Array.from({ length: 12 }, (_unused, index) => formatPlainDate({ day: 1, month: index + 1, year: 2026 }, { locale, month }));
|
|
272
|
+
//# sourceMappingURL=plainDate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plainDate.js","sourceRoot":"","sources":["../../src/DatePicker/plainDate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { SlotProps } from "../slotProps.ts";
|
|
3
|
+
export type MarkdownImageUpload = {
|
|
4
|
+
/** Alt text. Defaults to the file's name when omitted. */
|
|
5
|
+
alt?: string;
|
|
6
|
+
/** Where the image now lives. Inserted verbatim. */
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The toolbar's glyphs, by action.
|
|
11
|
+
*
|
|
12
|
+
* **No default, and none shipped** — this package ships no icons,
|
|
13
|
+
* and a `⋯`/`▨` default renders as nothing where the font lacks it
|
|
14
|
+
* ([decision](../../../../docs/decisions/2026-07-29-ship-no-icons-and-no-symbol-glyphs.md)).
|
|
15
|
+
* With no icons the toolbar is nine word buttons, which works
|
|
16
|
+
* everywhere and collapses into the overflow menu early; an app
|
|
17
|
+
* that brings lucide gets a compact bar and keeps the words as the
|
|
18
|
+
* accessible names.
|
|
19
|
+
*/
|
|
20
|
+
export type MarkdownEditorIcons = Partial<Record<"bold" | "bulletedList" | "code" | "heading" | "italic" | "link" | "numberedList" | "quote" | "taskList", ReactNode>>;
|
|
21
|
+
export type MarkdownEditorProps = SlotProps & {
|
|
22
|
+
className?: string;
|
|
23
|
+
/**
|
|
24
|
+
* **Initial** only. This control is uncontrolled, and the long
|
|
25
|
+
* reason is in the doc comment below — it is the whole design.
|
|
26
|
+
*/
|
|
27
|
+
defaultValue?: string;
|
|
28
|
+
/** See `MarkdownEditorIcons`. */
|
|
29
|
+
icons?: MarkdownEditorIcons;
|
|
30
|
+
isDisabled?: boolean;
|
|
31
|
+
isReadOnly?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The accessible name, as `aria-label`. Omit it inside a `Field`,
|
|
34
|
+
* which supplies a real `<label htmlFor>` — same contract as
|
|
35
|
+
* `Select`.
|
|
36
|
+
*/
|
|
37
|
+
label?: string;
|
|
38
|
+
onChange?: (value: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* A failed upload, after the placeholder has been taken back out
|
|
41
|
+
* of the text. Pair it with a `Toast`; this component does not
|
|
42
|
+
* own one.
|
|
43
|
+
*/
|
|
44
|
+
onUploadError?: (file: File, error: unknown) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Paste or drop an image and this runs. **The component does not
|
|
47
|
+
* own storage** — it hands you a `File`, you put it somewhere,
|
|
48
|
+
* and the URL you return is inserted at the caret as
|
|
49
|
+
* ``. Docket keeps its own blob storage precisely so
|
|
50
|
+
* the URL stays stable and theirs.
|
|
51
|
+
*
|
|
52
|
+
* Omit it and image paste is simply not offered: a `<textarea>`
|
|
53
|
+
* ignores a pasted image on its own, which is the correct
|
|
54
|
+
* fallback rather than a broken one.
|
|
55
|
+
*/
|
|
56
|
+
onUploadImage?: (file: File) => Promise<MarkdownImageUpload>;
|
|
57
|
+
placeholder?: string;
|
|
58
|
+
/** The editing surface's accessible name in the toolbar's label. */
|
|
59
|
+
toolbarLabel?: string;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* A live hybrid markdown editor whose stored value is markdown and
|
|
63
|
+
* only ever markdown.
|
|
64
|
+
*
|
|
65
|
+
* Docket asked for this one and the requirement is unusually sharp,
|
|
66
|
+
* because the tracker it replaces destroyed data by storing HTML in
|
|
67
|
+
* a field it later tried to read back as text. So the constraint
|
|
68
|
+
* here is not "prefer markdown" — it is that **an HTML tag must not
|
|
69
|
+
* be able to reach the stored string**, and the design is chosen to
|
|
70
|
+
* make that mechanical rather than policed:
|
|
71
|
+
*
|
|
72
|
+
* - the editing surface is a `<textarea>`, which takes
|
|
73
|
+
* `text/plain` off the clipboard and nothing else. Copy a
|
|
74
|
+
* styled paragraph out of a browser and the `text/html` flavour
|
|
75
|
+
* is simply not read;
|
|
76
|
+
* - every command in `markdownCommands.ts` returns markdown, and
|
|
77
|
+
* there is no serialiser anywhere in the component;
|
|
78
|
+
* - the painted layer emits `{ kind, text }` spans that React
|
|
79
|
+
* renders as **text nodes**. No `innerHTML`, no sanitiser, and
|
|
80
|
+
* nothing to get wrong — a `<script>` in the source is four
|
|
81
|
+
* coloured characters on screen.
|
|
82
|
+
*
|
|
83
|
+
* `MarkdownEditor.test.tsx` round-trips a value full of the
|
|
84
|
+
* characters that tempt escaping and asserts no tag ever appears.
|
|
85
|
+
*
|
|
86
|
+
* ## Why a `<textarea>` and not an editor core
|
|
87
|
+
*
|
|
88
|
+
* Charcuterie is published, so a dependency here is a permanent
|
|
89
|
+
* cost to every consumer of the package — measured, for a markdown
|
|
90
|
+
* setup: CodeMirror 6 at 176 KB gzipped, Lexical at 148 KB, TipTap
|
|
91
|
+
* at 238 KB. This component adds **nothing**. The full argument,
|
|
92
|
+
* with the numbers and with what the choice gives up, is
|
|
93
|
+
* `docs/decisions/2026-08-19-the-markdown-editor-is-a-textarea-with-a-painted-layer.md`.
|
|
94
|
+
*
|
|
95
|
+
* The accessibility half of that argument is worth repeating here,
|
|
96
|
+
* because it is the half that is hard to buy back later. A
|
|
97
|
+
* `<textarea>` arrives with caret movement, word-wise selection,
|
|
98
|
+
* `Home`/`End`, IME composition, spellcheck, autocorrect, the
|
|
99
|
+
* platform's own undo stack, `role="textbox"` and
|
|
100
|
+
* `aria-multiline="true"` — all of it, in every browser, tested by
|
|
101
|
+
* the vendors. A `contenteditable` starts with none of those and
|
|
102
|
+
* re-earns each one, and the ones it never fully re-earns are IME
|
|
103
|
+
* and undo. This library's central claim is that an agent and a
|
|
104
|
+
* keyboard can drive every component; starting from the platform's
|
|
105
|
+
* own text control is the cheapest way to keep it.
|
|
106
|
+
*
|
|
107
|
+
* ## It is uncontrolled, and that is not a shortcut
|
|
108
|
+
*
|
|
109
|
+
* `defaultValue` in, `onChange` out — the same contract as `Select`
|
|
110
|
+
* ([decision](../../../../docs/decisions/2026-07-31-select-is-uncontrolled-because-the-platform-owns-the-value.md)),
|
|
111
|
+
* and here it protects something specific. A controlled React
|
|
112
|
+
* `<textarea>` round-trips every keystroke through render, and any
|
|
113
|
+
* programmatic `value` assignment **wipes the browser's undo
|
|
114
|
+
* stack**: Ctrl+Z after a toolbar click then skips the paragraph
|
|
115
|
+
* you typed before it, or does nothing at all.
|
|
116
|
+
*
|
|
117
|
+
* So the textarea is never handed a `value`. Its text is mirrored
|
|
118
|
+
* into state for *painting* only, and every programmatic edit — a
|
|
119
|
+
* toolbar action, a shortcut, an image insertion — is applied as a
|
|
120
|
+
* range replacement through `document.execCommand("insertText")`,
|
|
121
|
+
* which is deprecated, is the only API that appends to the native
|
|
122
|
+
* undo stack, and is exactly why `getMinimalEdit` exists.
|
|
123
|
+
*
|
|
124
|
+
* A consumer that must swap the whole document — Docket opening a
|
|
125
|
+
* different task — remounts with a `key`. That is one line, and it
|
|
126
|
+
* is honest about the fact that the old undo history belonged to
|
|
127
|
+
* the old document.
|
|
128
|
+
*
|
|
129
|
+
* ## Composition, and the one component that did not fit
|
|
130
|
+
*
|
|
131
|
+
* The toolbar is this package's `Toolbar`, so the row collapses
|
|
132
|
+
* into a menu **by measurement** rather than at a breakpoint —
|
|
133
|
+
* which is what the Narrow View actually needs, since a lane in a
|
|
134
|
+
* three-up board is narrow on a 4K display. `Field` wraps it
|
|
135
|
+
* through `SlotProps`, so a label, a description and an error wire
|
|
136
|
+
* themselves to the textarea.
|
|
137
|
+
*
|
|
138
|
+
* `FileDropZone` does **not** wrap it, and that is the interesting
|
|
139
|
+
* one. That component is a `<label>` around a file input covering
|
|
140
|
+
* its whole area — correct for "drop a disc image here", and wrong
|
|
141
|
+
* for a text surface, where the drop has to land *at a character
|
|
142
|
+
* offset* and the area has to keep taking clicks as caret
|
|
143
|
+
* placement. So the editor handles its own drop, and `FileDropZone`
|
|
144
|
+
* stays the right answer for the attachment list beside it.
|
|
145
|
+
*/
|
|
146
|
+
export declare const MarkdownEditor: ({ className, defaultValue, icons, isDisabled, isReadOnly, label, onChange, onUploadError, onUploadImage, placeholder, toolbarLabel, ...receivedSlotProps }: MarkdownEditorProps) => ReactNode;
|
|
147
|
+
//# sourceMappingURL=MarkdownEditor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MarkdownEditor.d.ts","sourceRoot":"","sources":["../../src/MarkdownEditor/MarkdownEditor.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,SAAS,EACV,MAAM,OAAO,CAAA;AAGd,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAwBhD,MAAM,MAAM,mBAAmB,GAAG;IAChC,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,CACvC,MAAM,CACF,MAAM,GACN,cAAc,GACd,MAAM,GACN,SAAS,GACT,QAAQ,GACR,MAAM,GACN,cAAc,GACd,OAAO,GACP,UAAU,EACZ,SAAS,CACV,CACF,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iCAAiC;IACjC,KAAK,CAAC,EAAE,mBAAmB,CAAA;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IACpD;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,CACd,IAAI,EAAE,IAAI,KACP,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AA+DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoFG;AACH,eAAO,MAAM,cAAc,GAAI,4JAa5B,mBAAmB,KAAG,SAilBxB,CAAA"}
|