@12-apps/ui 6.10.1 → 6.11.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.
@@ -0,0 +1,111 @@
1
+ // src/components/data-display/DataViews/data-views-day-input.tsx
2
+ import { TextField } from "@mui/material";
3
+ import { useEffect, useState } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ var SEGMENT_WIDTH = { year: 4, month: 2, day: 2 };
6
+ function segmentsOf(mask) {
7
+ const tokens = mask.split(/[^A-Za-z]+/).filter(Boolean);
8
+ if (tokens.length !== 3) return ["day", "month", "year"];
9
+ const segments = tokens.map((token) => {
10
+ const head = token.slice(0, 1).toLowerCase();
11
+ if (head === "y" || head === "a") return "year";
12
+ return head === "m" ? "month" : "day";
13
+ });
14
+ return new Set(segments).size === 3 ? segments : ["day", "month", "year"];
15
+ }
16
+ var separatorOf = (mask) => mask.replace(/[A-Za-z]/g, "").slice(0, 1) || "/";
17
+ function isoToMasked(iso, mask) {
18
+ const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
19
+ if (!parts) return "";
20
+ const value = {
21
+ year: parts[1],
22
+ month: parts[2],
23
+ day: parts[3]
24
+ };
25
+ return segmentsOf(mask).map((segment) => value[segment]).join(separatorOf(mask));
26
+ }
27
+ function maskedToIso(text, mask) {
28
+ const segments = segmentsOf(mask);
29
+ const digits = text.replace(/\D/g, "");
30
+ if (digits.length !== 8) return "";
31
+ const value = {};
32
+ let cursor = 0;
33
+ for (const segment of segments) {
34
+ value[segment] = digits.slice(cursor, cursor + SEGMENT_WIDTH[segment]);
35
+ cursor += SEGMENT_WIDTH[segment];
36
+ }
37
+ const year = Number(value.year);
38
+ const month = Number(value.month);
39
+ const day = Number(value.day);
40
+ if (year < 1e3) return "";
41
+ const probe = new Date(Date.UTC(year, month - 1, day));
42
+ if (probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) return "";
43
+ return `${value.year}-${value.month}-${value.day}`;
44
+ }
45
+ function maskDate(raw, mask) {
46
+ const segments = segmentsOf(mask);
47
+ const separator = separatorOf(mask);
48
+ const digits = raw.replace(/\D/g, "").slice(0, 8);
49
+ const out = [];
50
+ let cursor = 0;
51
+ for (const segment of segments) {
52
+ if (cursor >= digits.length) break;
53
+ out.push(digits.slice(cursor, cursor + SEGMENT_WIDTH[segment]));
54
+ cursor += SEGMENT_WIDTH[segment];
55
+ }
56
+ return out.join(separator);
57
+ }
58
+ function dayInputProps(testId, describedBy) {
59
+ return {
60
+ "data-testid": testId,
61
+ maxLength: 10,
62
+ inputMode: "numeric",
63
+ autoComplete: "off",
64
+ ...describedBy ? { "aria-describedby": describedBy } : {}
65
+ };
66
+ }
67
+ function DayBoundInput({
68
+ label,
69
+ value,
70
+ onChange,
71
+ testId,
72
+ error = false,
73
+ describedBy,
74
+ mask
75
+ }) {
76
+ const applied = value ?? "";
77
+ const [text, setText] = useState(() => isoToMasked(applied, mask));
78
+ useEffect(() => {
79
+ setText(
80
+ (current) => maskedToIso(current, mask) === applied ? current : isoToMasked(applied, mask)
81
+ );
82
+ }, [applied, mask]);
83
+ return /* @__PURE__ */ jsx(
84
+ TextField,
85
+ {
86
+ size: "small",
87
+ type: "text",
88
+ label,
89
+ error,
90
+ placeholder: mask,
91
+ value: text,
92
+ onChange: (event) => {
93
+ const next = maskDate(event.target.value, mask);
94
+ setText(next);
95
+ if (next === "") return onChange(void 0);
96
+ const iso = maskedToIso(next, mask);
97
+ if (iso) onChange(iso);
98
+ },
99
+ onBlur: () => setText(isoToMasked(applied, mask)),
100
+ InputLabelProps: { shrink: true },
101
+ inputProps: dayInputProps(testId, describedBy),
102
+ sx: { width: 165 }
103
+ }
104
+ );
105
+ }
106
+
107
+ export {
108
+ isoToMasked,
109
+ DayBoundInput
110
+ };
111
+ //# sourceMappingURL=chunk-FKWNB4LW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/data-display/DataViews/data-views-day-input.tsx"],"sourcesContent":["'use client';\n\n/**\n * A day bound as a MASKED `dd/mm/aaaa` text field, replacing `<input type=\"date\">`.\n *\n * The native control is three separate segments wearing one box. Typing a date\n * means filling a segment, being jumped to the next, and starting over — and\n * which segment comes first is the BROWSER's locale, not ours, so the same\n * field reads `dd/mm/aaaa` for one merchant and `mm/dd/yyyy` for the next with\n * nothing in our code saying which. There is no way to type a date as one\n * continuous run of digits, which is how people actually type dates.\n *\n * A mask gives that back: eight digits straight through, separators inserted as\n * you go, backspace walking back through them.\n *\n * Measured, driving both controls in Chromium with the same eight keystrokes\n * (`01072026`, one `keydown` at a time):\n *\n * native → field ends EMPTY, no filter applied, 0 requests\n * masked → field reads `01/07/2026`, filter applied, 1 request\n *\n * The native result is the whole complaint: you type a date and nothing lands.\n *\n * The second half of the fix is WHEN the bound is written. A partially-typed\n * year is itself a valid date — `2026` passes through `0002`, `0020`, `0202` on\n * the way — so committing on every keystroke would send the grid off to fetch\n * years nobody meant. A bound is written only once the text is a WHOLE, real\n * date, which is what keeps the run above at one request rather than four.\n */\nimport { TextField } from '@mui/material';\nimport React, { useEffect, useState } from 'react';\n\n\n/**\n * THE MASK IS THE ORDER, and both halves of this file read it.\n *\n * The placeholder a host supplies (`copy.filters.dayMask` — `dd/mm/aaaa` for\n * pt-BR, `mm/dd/yyyy` for en-US) is not decoration: it tells the reader which\n * segment comes first. Everything below therefore derives the segment order\n * FROM that same string, so the shape on screen and the shape the parser\n * expects cannot disagree.\n *\n * They did disagree, and the failure was silent in the worst direction. The\n * mask/parse pair was hardcoded day-first (the `Br` in the old names was\n * accurate) while the placeholder followed the copy pack — so under `en-US` the\n * field ASKED for `mm/dd/yyyy`, and a reader who typed exactly that got a day\n * of 12 and a month of 31, which is not a date, which commits nothing. No\n * error, no filter, a full list that reads like an answer. The en-US pack's own\n * comment already said the mask \"is the shape that gets a date entered\n * backwards\"; nothing enforced it.\n */\n\n/** What one segment of a mask is, and how many digits it takes. */\ntype DaySegment = 'year' | 'month' | 'day';\nconst SEGMENT_WIDTH: Readonly<Record<DaySegment, number>> = { year: 4, month: 2, day: 2 };\n\n/**\n * The segments a mask asks for, in its own order.\n *\n * Keyed on each token's FIRST letter so one rule covers every spelling a host\n * might use — `aaaa`/`yyyy` for the year, `mm` for the month, `dd`/`tt` for the\n * day. An unreadable mask falls back to day-first rather than throwing: this\n * runs on every keystroke, and a field that renders is recoverable where one\n * that crashes the grid is not.\n */\nfunction segmentsOf(mask: string): readonly DaySegment[] {\n const tokens = mask.split(/[^A-Za-z]+/).filter(Boolean);\n if (tokens.length !== 3) return ['day', 'month', 'year'];\n const segments = tokens.map((token): DaySegment => {\n const head = token.slice(0, 1).toLowerCase();\n if (head === 'y' || head === 'a') return 'year';\n return head === 'm' ? 'month' : 'day';\n });\n // A mask naming the same segment twice describes no date. Fall back rather\n // than build a parser that can never succeed.\n return new Set(segments).size === 3 ? segments : ['day', 'month', 'year'];\n}\n\n/** The separator the mask uses, defaulting to `/`. */\nconst separatorOf = (mask: string): string => mask.replace(/[A-Za-z]/g, '').slice(0, 1) || '/';\n\n/** `AAAA-MM-DD` (the wire) → the mask's own order (what the reader sees). */\nexport function isoToMasked(iso: string, mask: string): string {\n const parts = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(iso);\n if (!parts) return '';\n const value: Record<DaySegment, string> = {\n year: parts[1] as string,\n month: parts[2] as string,\n day: parts[3] as string,\n };\n return segmentsOf(mask)\n .map((segment) => value[segment])\n .join(separatorOf(mask));\n}\n\n/**\n * The mask's order → `AAAA-MM-DD`, but ONLY for a day that exists.\n *\n * `31/02/2026` parses fine as three numbers and is not a date, and `Date` would\n * silently roll it into March rather than reject it — so the round trip through\n * UTC is the check: if the month or day comes back changed, the input was not a\n * day.\n */\nexport function maskedToIso(text: string, mask: string): string {\n const segments = segmentsOf(mask);\n const digits = text.replace(/\\D/g, '');\n if (digits.length !== 8) return '';\n const value = {} as Record<DaySegment, string>;\n let cursor = 0;\n for (const segment of segments) {\n value[segment] = digits.slice(cursor, cursor + SEGMENT_WIDTH[segment]);\n cursor += SEGMENT_WIDTH[segment];\n }\n const year = Number(value.year);\n const month = Number(value.month);\n const day = Number(value.day);\n if (year < 1000) return '';\n const probe = new Date(Date.UTC(year, month - 1, day));\n if (probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) return '';\n return `${value.year}-${value.month}-${value.day}`;\n}\n\n/**\n * Digits only, capped at the mask's width, with the separators put back as you\n * type.\n *\n * Deriving the whole string from the digits — rather than editing the text in\n * place — is what makes backspace behave. Four digits of `dd/mm/aaaa` render as\n * `06/08`, not `06/08/`, so deleting the first digit of the year removes the\n * trailing separator with it instead of leaving a dead keystroke that changes\n * nothing.\n */\nexport function maskDate(raw: string, mask: string): string {\n const segments = segmentsOf(mask);\n const separator = separatorOf(mask);\n const digits = raw.replace(/\\D/g, '').slice(0, 8);\n const out: string[] = [];\n let cursor = 0;\n for (const segment of segments) {\n if (cursor >= digits.length) break;\n out.push(digits.slice(cursor, cursor + SEGMENT_WIDTH[segment]));\n cursor += SEGMENT_WIDTH[segment];\n }\n return out.join(separator);\n}\n\n/**\n * What has to land on the `<input>` itself rather than on the `TextField`.\n *\n * Anything `TextField` does not recognise is spread onto its root `FormControl`\n * div. `inputMode` went up there once: the attribute sat on a non-editable\n * wrapper, meant nothing, and the phone went on offering the letter keyboard\n * for a field that only takes digits (the `dd/mm/aaaa` hint under a QWERTY\n * layout). Only the `<input>` this reaches decides which keypad opens — and the\n * same is true of `aria-describedby`, which on the wrapper points at a\n * description no screen reader will read out for the field.\n *\n * `numeric` and not `tel`: both raise a keypad, but the phone one is a dialler\n * with `+ * #` on it — keys that cannot occur in a date. A paste carrying\n * separators still lands, because the MASK is what filters the text; the keypad\n * only narrows what a thumb can reach for.\n *\n * Autofill off for the same reason: the browser's saved-value strip covers the\n * keypad to suggest names and addresses, none of which are a date, and none of\n * which the mask would keep.\n */\nfunction dayInputProps(testId: string, describedBy: string | undefined) {\n return {\n 'data-testid': testId,\n maxLength: 10,\n inputMode: 'numeric' as const,\n autoComplete: 'off',\n ...(describedBy ? { 'aria-describedby': describedBy } : {}),\n };\n}\n\n/**\n * One end of a day window. The BOUND stays `AAAA-MM-DD` — the form the backend's\n * inclusive day comparison already speaks, and the form the chip label reads —\n * so only the typing surface changes.\n */\nexport function DayBoundInput({\n label,\n value,\n onChange,\n testId,\n error = false,\n describedBy,\n mask,\n}: {\n label: string;\n /** The applied bound, `AAAA-MM-DD`, or undefined when this end is open. */\n value: string | undefined;\n onChange: (bound: string | undefined) => void;\n testId: string;\n /**\n * Paint the field as rejected. It only ever writes a real day by itself, so\n * this is for a caller judging the PAIR: a window whose end precedes its\n * start is two valid days and one impossible period, and the field it was\n * typed into is the only place to say which half is being complained about.\n */\n error?: boolean;\n /** Id of the element carrying that complaint, so it is read WITH the field. */\n describedBy?: string;\n /**\n * The day mask, which IS the placeholder — so the expected ORDER is on\n * screen before the first keystroke rather than discovered by getting it\n * wrong. `dd/mm/aaaa` is one locale's order and one locale's letters.\n *\n * A PROP rather than a context read: this field has two callers, and only\n * one of them is inside DataViews. `DateRangePicker` mounts it standalone\n * with its own message table, so a context read here throws for every host\n * of that picker — which is exactly what it did.\n */\n mask: string;\n}): React.JSX.Element {\n const applied = value ?? '';\n const [text, setText] = useState(() => isoToMasked(applied, mask));\n\n // The bound is the APPLIED filter; `text` is what is being typed. Re-sync only\n // when the applied value moved somewhere the text does not already mean — a\n // \"Limpar\", a saved view, the other pill resetting the pair. Without that\n // guard the field's own write bounces straight back mid-edit and resets the\n // caret, which is the native control's worst habit reproduced by hand.\n useEffect(() => {\n setText((current) =>\n maskedToIso(current, mask) === applied ? current : isoToMasked(applied, mask),\n );\n }, [applied, mask]);\n\n return (\n <TextField\n size=\"small\"\n type=\"text\"\n label={label}\n error={error}\n placeholder={mask}\n value={text}\n onChange={(event) => {\n const next = maskDate(event.target.value, mask);\n setText(next);\n // Emptying clears the bound. A COMPLETE day applies it. A partial one\n // does NEITHER — it leaves whatever is applied alone, so typing is never\n // fought and a half-typed year never reaches the backend.\n if (next === '') return onChange(undefined);\n const iso = maskedToIso(next, mask);\n if (iso) onChange(iso);\n }}\n // Leaving a half-typed (or impossible) date on screen would show a filter\n // the list is not actually using. On blur the field snaps back to what IS\n // applied; a complete date already equals it, so this only ever tidies.\n onBlur={() => setText(isoToMasked(applied, mask))}\n // The placeholder occupies the space an un-shrunk label would render into.\n InputLabelProps={{ shrink: true }}\n inputProps={dayInputProps(testId, describedBy)}\n sx={{ width: 165 }}\n />\n );\n}\n"],"mappings":"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","names":[]}
@@ -22,7 +22,7 @@ import {
22
22
  } from "../chunk-S4R6CKIG.js";
23
23
  import {
24
24
  DayBoundInput
25
- } from "../chunk-TSG5C7MP.js";
25
+ } from "../chunk-FKWNB4LW.js";
26
26
  import {
27
27
  CategorySelect
28
28
  } from "../chunk-AR5LUIBC.js";
@@ -5,8 +5,8 @@ import {
5
5
  } from "../chunk-KXARS46L.js";
6
6
  import {
7
7
  DayBoundInput,
8
- isoToBr
9
- } from "../chunk-TSG5C7MP.js";
8
+ isoToMasked
9
+ } from "../chunk-FKWNB4LW.js";
10
10
  import "../chunk-PZ5AY32C.js";
11
11
 
12
12
  // src/components/form/DateRangePicker/DateRangePicker.tsx
@@ -104,6 +104,7 @@ function resolveDayRange(draft, maxRangeDays) {
104
104
  return { ok: true, window: { from, to }, days };
105
105
  }
106
106
  function defaultMessages() {
107
+ const dayMask = "dd/mm/yyyy";
107
108
  return {
108
109
  from: "Start date",
109
110
  to: "End date",
@@ -111,9 +112,9 @@ function defaultMessages() {
111
112
  incomplete: "Choose both dates.",
112
113
  reversed: "The end date must be on or after the start date.",
113
114
  overMax: ({ maxRangeDays }) => `Choose a range of at most ${maxRangeDays} days.`,
114
- summary: ({ from, to }) => `${isoToBr(from)} \u2013 ${isoToBr(to)}`,
115
+ summary: ({ from, to }) => `${isoToMasked(from, dayMask)} \u2013 ${isoToMasked(to, dayMask)}`,
115
116
  calendarLabel: "Calendar",
116
- dayMask: "dd/mm/yyyy"
117
+ dayMask
117
118
  };
118
119
  }
119
120
  function statusMessage(status, messages, maxRangeDays) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/form/DateRangePicker/DateRangePicker.tsx","../../src/components/form/DateRangePicker/DateRangePicker.dates.ts","../../src/components/form/DateRangePicker/DateRangePicker.hooks.ts","../../src/components/form/DateRangePicker/DateRangePicker.range.ts","../../src/components/form/DateRangePicker/DateRangePicker.quick.ts","../../src/components/form/DateRangePicker/DateRangePickerFields.tsx","../../src/components/form/DateRangePicker/DateRangePickerQuickList.tsx"],"sourcesContent":["'use client';\n\n/**\n * Three views of ONE range: a calendar, a quick-pick column, and two typed\n * dates. Change any and the other two follow.\n *\n * WHY IT EXISTS: a range calendar alone makes \"this quarter\" cost two clicks,\n * a month of paging, and a mental note of which day a quarter starts on; a list\n * of presets alone cannot express \"the 3rd to the 19th\"; and typed fields alone\n * make you spell out a window you can see. Each of the three is the fastest\n * route to a different range, so the control offers all three over one value\n * rather than picking a favourite.\n *\n * WHAT IT DOES NOT KNOW: anything about the data behind the range. No preset\n * list of a product's own, no ceiling of its own, no notion of whose clock\n * \"today\" runs on — `quickRanges`, `maxRangeDays` and `timeZone` are props, and\n * a consumer that needs its own periods passes them rather than teaching this\n * file about its domain. The calendar is `Calendar` in `selectionMode=\"range\"`,\n * not a second implementation of one.\n *\n * The value is a DRAFT (`{ from, to }`, either end possibly null) rather than a\n * finished window, so a half-picked or impossible range has somewhere to live\n * while it is being fixed. Every `onChange` carries the verdict with it.\n */\nimport { Box, Paper, useMediaQuery } from '@mui/material';\nimport type { SxProps, Theme } from '@mui/material';\nimport React from 'react';\n\nimport { Calendar } from '../Calendar';\n\nimport { toLocalDay } from './DateRangePicker.dates';\nimport { useDateRangePicker } from './DateRangePicker.hooks';\nimport { DEFAULT_QUICK_RANGES } from './DateRangePicker.quick';\nimport type { DateRangeDraft, DateRangePickerProps, WeekStart } from './DateRangePicker.types';\nimport { DateRangeFields } from './DateRangePickerFields';\nimport { DateRangeQuickList } from './DateRangePickerQuickList';\n\nconst CALENDAR_CLASS = 'date-range-picker__calendar';\n\n/** Below this the picker is a phone layout. MUI's own `md`, written out. */\nconst NARROW_QUERY = '(max-width:899.95px)';\n\nconst ROOT_SX: SxProps<Theme> = {\n display: 'inline-flex',\n flexDirection: 'column',\n gap: 2,\n p: 2,\n maxWidth: '100%',\n // `Calendar` renders its own `Paper` with 24px of padding. Nested in this one\n // that reads as a box inside a box, so the inner frame is flattened here\n // through the className hook the component exposes for exactly this.\n [`& .${CALENDAR_CLASS}`]: {\n p: 0,\n boxShadow: 'none',\n backgroundColor: 'transparent',\n backgroundImage: 'none',\n },\n};\n\nconst BODY_SX: SxProps<Theme> = {\n display: 'flex',\n // The quick column sits BESIDE the calendar on a wide screen. Narrow, it\n // moves ABOVE it — `column-reverse`, so the DOM order stays calendar-then-\n // list (the calendar is the primary control and should be reached first by a\n // screen reader and by Tab) while the eye meets the quick ranges first, which\n // is the order the phone mock asks for and the order a thumb wants: nine\n // one-tap answers before a grid of thirty.\n //\n // A side column at 390px would squeeze the day grid to roughly half its width\n // and make the numbers unreadable. Checked at 390px.\n flexDirection: { xs: 'column-reverse', md: 'row' },\n alignItems: { xs: 'stretch', md: 'flex-start' },\n gap: 2,\n maxWidth: '100%',\n};\n\n/**\n * The rule between the calendar and the quick ranges.\n *\n * Two lists of dates side by side with only whitespace between them read as one\n * ragged thing; the rule says which numbers belong to which control. It turns\n * with the layout — a vertical rule when the list is a column beside the\n * calendar, a horizontal one when it has moved above it — because a divider\n * that keeps its axis while the flex direction flips ends up separating nothing.\n */\nconst QUICK_DIVIDER_SX: SxProps<Theme> = {\n borderColor: 'divider',\n borderStyle: 'solid',\n borderWidth: 0,\n borderLeftWidth: { xs: 0, md: '1px' },\n borderBottomWidth: { xs: '1px', md: 0 },\n pl: { xs: 0, md: 2 },\n pb: { xs: 2, md: 0 },\n alignSelf: 'stretch',\n};\n\n/** And the rule above the typed pair, which is a third way in, not a caption. */\nconst FIELDS_DIVIDER_SX: SxProps<Theme> = {\n borderTop: '1px solid',\n borderColor: 'divider',\n pt: 2,\n};\n\ntype Picker = ReturnType<typeof useDateRangePicker>;\n\n/**\n * How many months to draw.\n *\n * A month grid has a floor: seven columns that must stay legible, which is\n * about 280px. Two of them need 580, so on a phone the consumer's\n * `numberOfMonths` is not a preference that can be honoured — it is a request\n * for a layout that does not fit. Capped here rather than left to the consumer,\n * because the consumer chooses for its own reason (the reports dialog wants two\n * because a period often crosses a month boundary) and has no way to know the\n * picker is currently 390px wide.\n *\n * A media query rather than an `sx` breakpoint: this decides what to RENDER,\n * and CSS cannot un-render a second month.\n *\n * The query is a LITERAL, not `theme.breakpoints.down('md')`. The callback form\n * reads the theme from context and throws on a null one, so a consumer that\n * mounts this outside a `ThemeProvider` would crash — which the unit tests here\n * do, and which is a fair thing for a consumer to do.\n */\nfunction useMonthCount(requested: number | undefined): number | undefined {\n const narrow = useMediaQuery(NARROW_QUERY);\n return narrow ? 1 : requested;\n}\n\n/** The grid, in range mode — `Calendar`, never a second implementation of one. */\nfunction RangeCalendar({\n calendarLabel,\n picker,\n value,\n locale,\n weekStartsOn,\n months,\n maxRangeDays,\n}: {\n picker: Picker;\n value: DateRangeDraft;\n locale: string;\n weekStartsOn: WeekStart;\n months: number | undefined;\n maxRangeDays: number | undefined;\n calendarLabel: string;\n}): React.JSX.Element {\n return (\n <Calendar\n key={picker.viewNonce}\n ariaLabel={calendarLabel}\n className={CALENDAR_CLASS}\n selectionMode=\"range\"\n locale={locale}\n // One prop drives the grid's first column AND the day \"this week\" starts\n // on, so a highlighted week can never begin mid-row.\n firstDayOfWeek={weekStartsOn}\n numberOfMonths={months}\n range={{ start: toLocalDay(value.from), end: toLocalDay(value.to) }}\n // BOTH callbacks, and the intermediate one does the work: the other fires\n // only on the click that CLOSES a range, so a picker wired to it alone\n // would not repaint after the first click.\n onIntermediateRangeChange={picker.pickOnCalendar}\n onRangeChange={picker.pickOnCalendar}\n // The same ceiling the quick list is judged against, so the calendar\n // cannot hand back a window the control has just called too long.\n maxRangeLength={maxRangeDays}\n />\n );\n}\n\n/** The quick ranges, inside the rule that separates them from the grid. */\nfunction QuickColumn({\n picker,\n maxRangeDays,\n dataTestId,\n}: {\n picker: Picker;\n maxRangeDays: number | undefined;\n dataTestId: string;\n}): React.JSX.Element | null {\n if (picker.options.length === 0) return null;\n return (\n <Box sx={QUICK_DIVIDER_SX}>\n <DateRangeQuickList\n options={picker.options}\n activeId={picker.activeId}\n onPick={picker.pickQuick}\n reasonFor={(option) =>\n picker.copy.overMax({ maxRangeDays: maxRangeDays ?? option.days, days: option.days })\n }\n label={picker.copy.quickRanges}\n dataTestId={dataTestId}\n />\n </Box>\n );\n}\n\nexport function DateRangePicker({\n value,\n onChange,\n timeZone,\n now,\n maxRangeDays,\n weekStartsOn = 0,\n quickRanges = DEFAULT_QUICK_RANGES,\n locale = 'en-US',\n numberOfMonths,\n messages,\n dataTestId = 'date-range-picker',\n className,\n}: DateRangePickerProps): React.JSX.Element {\n const picker = useDateRangePicker({\n value,\n onChange,\n timeZone,\n now,\n maxRangeDays,\n weekStartsOn,\n quickRanges,\n messages,\n });\n const months = useMonthCount(numberOfMonths);\n\n return (\n <Paper className={className} data-testid={dataTestId} sx={ROOT_SX}>\n <Box sx={BODY_SX}>\n <RangeCalendar\n calendarLabel={picker.copy.calendarLabel}\n picker={picker}\n value={value}\n locale={locale}\n weekStartsOn={weekStartsOn}\n months={months}\n maxRangeDays={maxRangeDays}\n />\n <QuickColumn picker={picker} maxRangeDays={maxRangeDays} dataTestId={dataTestId} />\n </Box>\n <Box sx={FIELDS_DIVIDER_SX}>\n <DateRangeFields\n value={value}\n status={picker.status}\n messages={picker.copy}\n maxRangeDays={maxRangeDays}\n onEdit={picker.editBound}\n statusId={picker.statusId}\n dataTestId={dataTestId}\n />\n </Box>\n </Paper>\n );\n}\n\nexport default DateRangePicker;\n","/**\n * Civil-day arithmetic for the range picker.\n *\n * Two representations meet here and the boundary between them is the whole\n * point of the file:\n *\n * - `AAAA-MM-DD` — what the picker's value, the quick ranges and any caller\n * speak. A day with no instant attached, so it cannot drift.\n * - a LOCAL `Date` at NOON — what `Calendar` deals in. Noon (not midnight) is\n * `Calendar.dates.normalizeDate`'s own rule: a daylight-saving jump at\n * midnight can move a date across a day boundary, and midday never can.\n *\n * The arithmetic itself is `Calendar.dates`' — `addDays` and `startOfWeek` are\n * imported rather than re-written, so a week here starts on the same day the\n * grid draws it.\n *\n * ONE thing needs a time zone: which day is today. Everything after that is\n * calendar arithmetic that has no clock in it.\n */\nimport { addDays, startOfWeek } from '../Calendar/Calendar.dates';\n\nimport type { DayString, WeekStart } from './DateRangePicker.types';\n\nconst NOON = 12;\nconst MS_PER_DAY = 86_400_000;\nconst MONTHS_PER_QUARTER = 3;\nconst DAY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\n/** `[year, month 1-12, day]`, or null when the text is not a day at all. */\nfunction dayParts(day: DayString): [number, number, number] | null {\n const parts = DAY_PATTERN.exec(day);\n if (!parts) return null;\n return [Number(parts[1]), Number(parts[2]), Number(parts[3])];\n}\n\nconst pad = (value: number): string => String(value).padStart(2, '0');\n\n/** `AAAA-MM-DD` from calendar parts. Month is 1-12. */\nfunction formatDay(year: number, month: number, day: number): DayString {\n return `${String(year).padStart(4, '0')}-${pad(month)}-${pad(day)}`;\n}\n\n/** A LOCAL `Date` → the day it falls on, read off the same local getters. */\nexport function toDayString(date: Date): DayString {\n return formatDay(date.getFullYear(), date.getMonth() + 1, date.getDate());\n}\n\n/**\n * `AAAA-MM-DD` → a LOCAL `Date` at noon, or null when the text is not a real\n * day.\n *\n * Deliberately not `new Date(day)`: the spec reads a bare `AAAA-MM-DD` as UTC\n * midnight, so west of Greenwich it lands on the day BEFORE and every date in\n * the picker is off by one. The round-trip check is what rejects `2026-02-31`,\n * which `Date` would otherwise roll silently into March.\n */\nexport function toLocalDay(day: DayString | null | undefined): Date | null {\n if (!day) return null;\n const parts = dayParts(day);\n if (!parts) return null;\n const date = new Date(parts[0], parts[1] - 1, parts[2], NOON);\n return toDayString(date) === day ? date : null;\n}\n\nconst civilFormatters = new Map<string, Intl.DateTimeFormat>();\n\nfunction civilFormatter(timeZone: string): Intl.DateTimeFormat {\n const cached = civilFormatters.get(timeZone);\n if (cached) return cached;\n const created = new Intl.DateTimeFormat('en-US', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n });\n civilFormatters.set(timeZone, created);\n return created;\n}\n\n/**\n * The civil day `now` falls on, on `timeZone`'s clock (the host's when absent).\n *\n * Read through `formatToParts` rather than by adding an offset to an epoch: the\n * zone database already knows about every transition, and reconstructing one by\n * hand is how a picker ends up a day out for exactly one week of the year.\n */\nexport function todayIn(timeZone: string | undefined, now: Date): DayString {\n if (!timeZone) return toDayString(now);\n const parts = civilFormatter(timeZone).formatToParts(now);\n const read = (type: Intl.DateTimeFormatPartTypes): string =>\n parts.find((part) => part.type === type)?.value ?? '';\n return `${read('year')}-${read('month')}-${read('day')}`;\n}\n\n/** The same day `days` later (or earlier). */\nexport function shiftDay(day: DayString, days: number): DayString {\n const date = toLocalDay(day);\n return date ? toDayString(addDays(date, days)) : day;\n}\n\n/** The 1st of `day`'s month. */\nexport function startOfMonthDay(day: DayString): DayString {\n const parts = dayParts(day);\n return parts ? formatDay(parts[0], parts[1], 1) : day;\n}\n\n/**\n * The 1st of `day`'s CALENDAR quarter — Jan/Apr/Jul/Oct. Fiscal quarters are a\n * different thing and belong in a caller's own quick range, not in a default.\n */\nexport function startOfQuarterDay(day: DayString): DayString {\n const parts = dayParts(day);\n if (!parts) return day;\n const month = parts[1] - ((parts[1] - 1) % MONTHS_PER_QUARTER);\n return formatDay(parts[0], month, 1);\n}\n\n/** The 1st of January of `day`'s year. */\nexport function startOfYearDay(day: DayString): DayString {\n const parts = dayParts(day);\n return parts ? formatDay(parts[0], 1, 1) : day;\n}\n\n/** The first day of `day`'s week, on the same week start the grid uses. */\nexport function startOfWeekDay(day: DayString, weekStartsOn: WeekStart): DayString {\n const date = toLocalDay(day);\n return date ? toDayString(startOfWeek(date, weekStartsOn)) : day;\n}\n\n/**\n * How many days a window covers, both ends INCLUDED — so a single day is 1.\n *\n * Measured on UTC epochs rather than on the local dates: the difference between\n * two local noons can be 23 or 25 hours across a transition, and dividing that\n * by a fixed day length is how a 30-day window starts reporting 29.96.\n */\nexport function dayCount(from: DayString, to: DayString): number {\n const start = dayParts(from);\n const end = dayParts(to);\n if (!start || !end) return 0;\n const startMs = Date.UTC(start[0], start[1] - 1, start[2]);\n const endMs = Date.UTC(end[0], end[1] - 1, end[2]);\n return Math.round((endMs - startMs) / MS_PER_DAY) + 1;\n}\n\n/** `AAAA-MM-DD` sorts lexicographically, so this needs no parsing at all. */\nexport function isAfterDay(a: DayString, b: DayString): boolean {\n return a > b;\n}\n","/**\n * Everything the picker derives and every way its range can move, kept out of\n * the component so that file is a layout and this one is the behaviour — the\n * same split `Calendar.hooks.ts` uses next door.\n */\nimport { useId, useMemo, useState } from 'react';\n\nimport { toDayString, todayIn } from './DateRangePicker.dates';\nimport {\n activeQuickRangeId,\n defaultMessages,\n resolveDayRange,\n resolveQuickRanges,\n} from './DateRangePicker.range';\nimport type {\n DateRangeChangeMeta,\n DateRangeDraft,\n DateRangePickerState,\n UseDateRangePickerArgs,\n} from './DateRangePicker.types';\n\nexport function useDateRangePicker({\n value,\n onChange,\n timeZone,\n now,\n maxRangeDays,\n weekStartsOn,\n quickRanges,\n messages,\n}: UseDateRangePickerArgs): DateRangePickerState {\n const statusId = useId();\n // `Calendar` reads which month to open on from the range it is handed, and\n // only when it MOUNTS. Bumping this key remounts it, which is how the other\n // two views move it. It changes for a quick pick and for a typed date and\n // NEVER for a click inside the grid — a calendar that jumped under the\n // pointer picking in it would be unusable.\n const [viewNonce, setViewNonce] = useState(0);\n\n const copy = useMemo(() => ({ ...defaultMessages(), ...messages }), [messages]);\n const today = useMemo(() => todayIn(timeZone, now ?? new Date()), [timeZone, now]);\n const options = useMemo(\n () => resolveQuickRanges(quickRanges, { today, weekStartsOn }, maxRangeDays),\n [quickRanges, today, weekStartsOn, maxRangeDays],\n );\n const status = useMemo(() => resolveDayRange(value, maxRangeDays), [value, maxRangeDays]);\n\n const emit = (\n next: DateRangeDraft,\n source: DateRangeChangeMeta['source'],\n quickRangeId?: string,\n ): void => {\n onChange(next, {\n source,\n ...(quickRangeId ? { quickRangeId } : {}),\n // Judged here, once. A caller left to re-derive \"is this usable\" is a\n // second opinion able to disagree with the message on screen.\n status: resolveDayRange(next, maxRangeDays),\n });\n };\n\n return {\n copy,\n options,\n activeId: activeQuickRangeId(options, value),\n status,\n statusId,\n viewNonce,\n pickQuick: (option) => {\n setViewNonce((nonce) => nonce + 1);\n emit({ from: option.window.from, to: option.window.to }, 'quick', option.id);\n },\n editBound: (which, bound) => {\n setViewNonce((nonce) => nonce + 1);\n emit({ ...value, [which]: bound }, which);\n },\n pickOnCalendar: (range) => {\n emit(\n {\n from: range.start ? toDayString(range.start) : null,\n to: range.end ? toDayString(range.end) : null,\n },\n 'calendar',\n );\n },\n };\n}\n","/**\n * Judging a draft, and saying so in words.\n *\n * The picker never repairs what it is given. A reversed pair is not silently\n * swapped and an over-long one is not clipped to the cap: both are reported,\n * because a control that returns a different range from the one on screen is\n * indistinguishable from one that ignored you. The caller reads\n * {@link DateRangeStatus} and keeps its own confirm button shut.\n */\nimport { isoToBr } from '../../data-display/DataViews/data-views-day-input';\n\nimport { dayCount, isAfterDay } from './DateRangePicker.dates';\nimport type {\n DateRangePickerMessages,\n DateRangeDraft,\n DateRangeStatus,\n QuickRange,\n QuickRangeContext,\n ResolvedQuickRange,\n} from './DateRangePicker.types';\n\n/**\n * Whether a draft is a window this picker will hand over.\n *\n * Exported because the caller needs the same verdict the picker shows: a dialog\n * disabling its \"Apply\" button must not re-derive \"is this legal\" from the two\n * strings, or the two answers drift and the button ends up enabled over a\n * message saying the range is impossible.\n */\nexport function resolveDayRange(\n draft: DateRangeDraft,\n maxRangeDays?: number,\n): DateRangeStatus {\n const { from, to } = draft;\n if (!from || !to) return { ok: false, problem: 'incomplete', days: 0 };\n if (isAfterDay(from, to)) return { ok: false, problem: 'reversed', days: 0 };\n\n const days = dayCount(from, to);\n if (maxRangeDays !== undefined && days > maxRangeDays) {\n return { ok: false, problem: 'over-max', days };\n }\n return { ok: true, window: { from, to }, days };\n}\n\n/**\n * The English defaults.\n *\n * The summary prints the days in the SAME `dd/mm/aaaa` the two fields are\n * masked to, rather than in the host locale's order. One control cannot show\n * the same date two ways: an `en-US` host formatting the summary as `08/05`\n * over a field reading `05/08` leaves the reader to work out which of the two\n * is lying. A host that wants its own order replaces `messages.summary`, which\n * moves both halves of the decision to the same place.\n */\nexport function defaultMessages(): DateRangePickerMessages {\n return {\n from: 'Start date',\n to: 'End date',\n quickRanges: 'Quick ranges',\n incomplete: 'Choose both dates.',\n reversed: 'The end date must be on or after the start date.',\n overMax: ({ maxRangeDays }) => `Choose a range of at most ${maxRangeDays} days.`,\n summary: ({ from, to }) => `${isoToBr(from)} – ${isoToBr(to)}`,\n calendarLabel: 'Calendar',\n dayMask: 'dd/mm/yyyy',\n };\n}\n\n/** The line under the fields: the chosen window, or why there isn't one. */\nexport function statusMessage(\n status: DateRangeStatus,\n messages: DateRangePickerMessages,\n maxRangeDays: number | undefined,\n): string {\n if (status.ok) return messages.summary(status.window);\n if (status.problem === 'incomplete') return messages.incomplete;\n if (status.problem === 'reversed') return messages.reversed;\n return messages.overMax({ maxRangeDays: maxRangeDays ?? status.days, days: status.days });\n}\n\n/** Every quick range with its window, its length, and whether it is over cap. */\nexport function resolveQuickRanges(\n quickRanges: QuickRange[],\n context: QuickRangeContext,\n maxRangeDays: number | undefined,\n): ResolvedQuickRange[] {\n return quickRanges.map((quickRange) => {\n const window = quickRange.resolve(context);\n const days = dayCount(window.from, window.to);\n return {\n ...quickRange,\n window,\n days,\n overMax: maxRangeDays !== undefined && days > maxRangeDays,\n };\n });\n}\n\n/** The quick range the draft currently IS, if it is exactly one of them. */\nexport function activeQuickRangeId(\n options: ResolvedQuickRange[],\n draft: DateRangeDraft,\n): string | undefined {\n return options.find(\n (option) => option.window.from === draft.from && option.window.to === draft.to,\n )?.id;\n}\n","/**\n * The quick-pick column's default list, and how to relabel it.\n *\n * These are DOMAIN-FREE on purpose: every one of them is derivable from \"today\"\n * and the calendar, so the design system can ship them without knowing what the\n * range is for. A product's own periods (a fiscal quarter, \"since the store\n * opened\", the last closed month) are a `quickRanges` array the caller passes —\n * the prop exists so that list never has to be smuggled in here.\n *\n * \"Last N days\" INCLUDES today, which is the reading every dashboard in the\n * wild uses: on the 10th, \"last 7 days\" is the 4th through the 10th, not the\n * 3rd through the 9th.\n */\nimport {\n shiftDay,\n startOfMonthDay,\n startOfQuarterDay,\n startOfWeekDay,\n startOfYearDay,\n} from './DateRangePicker.dates';\nimport type { QuickRange, QuickRangeContext } from './DateRangePicker.types';\n\n/**\n * The ids the built-in list uses. Stable API: a caller keys off these to map a\n * quick pick onto something of its own (a stored preset, an event name), and\n * renaming one is a breaking change in a way that renaming a label is not.\n */\nexport const QUICK_RANGE_IDS = [\n 'today',\n 'yesterday',\n 'this-week',\n 'last-7-days',\n 'this-month',\n 'last-30-days',\n 'this-quarter',\n 'this-year',\n 'last-365-days',\n] as const;\n\nexport type QuickRangeId = (typeof QUICK_RANGE_IDS)[number];\n\n/** English defaults. Every one is replaceable through {@link createQuickRanges}. */\nexport const DEFAULT_QUICK_RANGE_LABELS: Record<QuickRangeId, string> = {\n today: 'Today',\n yesterday: 'Yesterday',\n 'this-week': 'This week',\n 'last-7-days': 'Last 7 days',\n 'this-month': 'This month',\n 'last-30-days': 'Last 30 days',\n 'this-quarter': 'This quarter',\n 'this-year': 'This year',\n 'last-365-days': 'Last 365 days',\n};\n\n/** A window that ends today and reaches back `days` days, today included. */\nconst trailing =\n (days: number) =>\n ({ today }: QuickRangeContext) => ({ from: shiftDay(today, 1 - days), to: today });\n\n/** A window from the start of some period through today. */\nconst sinceStartOf =\n (start: (context: QuickRangeContext) => string) => (context: QuickRangeContext) => ({\n from: start(context),\n to: context.today,\n });\n\nconst RESOLVERS: Record<QuickRangeId, QuickRange['resolve']> = {\n today: trailing(1),\n yesterday: ({ today }) => ({ from: shiftDay(today, -1), to: shiftDay(today, -1) }),\n 'this-week': sinceStartOf(({ today, weekStartsOn }) => startOfWeekDay(today, weekStartsOn)),\n 'last-7-days': trailing(7),\n 'this-month': sinceStartOf(({ today }) => startOfMonthDay(today)),\n 'last-30-days': trailing(30),\n 'this-quarter': sinceStartOf(({ today }) => startOfQuarterDay(today)),\n 'this-year': sinceStartOf(({ today }) => startOfYearDay(today)),\n 'last-365-days': trailing(365),\n};\n\n/**\n * The built-in quick ranges, optionally relabelled.\n *\n * Relabelling rather than rebuilding is the point: a Portuguese host wants\n * \"Este mês\" over the same month-to-date arithmetic, and re-deriving the dates\n * to get the word is how two lists that claim the same thing start disagreeing.\n */\nexport function createQuickRanges(\n labels: Partial<Record<QuickRangeId, string>> = {},\n): QuickRange[] {\n return QUICK_RANGE_IDS.map((id) => ({\n id,\n label: labels[id] ?? DEFAULT_QUICK_RANGE_LABELS[id],\n resolve: RESOLVERS[id],\n }));\n}\n\n/** The list a picker uses when the caller says nothing. */\nexport const DEFAULT_QUICK_RANGES: QuickRange[] = createQuickRanges();\n","/**\n * The two typed ends, and the one line that says what the picker currently has.\n *\n * WHEN A TYPED DATE COMMITS: the moment it is a WHOLE, REAL day — eight digits\n * that name a date that exists. Not on every keystroke (`2026` passes through\n * `0002`, `0020` and `0202`, each a legal year, and committing those makes the\n * calendar jump to the year 20 while you are still typing) and not only on blur\n * (a reader who types a date and looks at the calendar to check it would see\n * the old month until they clicked somewhere else). Half-typed text stays in\n * the field and changes nothing; leaving the field discards it and shows what\n * the range actually is. Enter needs no special case — by the time it can be\n * pressed the date has already committed.\n *\n * The field itself is `DayBoundInput`, the masked `dd/mm/aaaa` control the data\n * views already use. Same package, imported rather than re-implemented: a\n * second masked date field is a second set of rules for what backspace does.\n */\nimport { Box, Typography } from '@mui/material';\nimport React from 'react';\n\nimport { DayBoundInput } from '../../data-display/DataViews/data-views-day-input';\n\nimport { statusMessage } from './DateRangePicker.range';\nimport type {\n DateRangePickerMessages,\n DateRangeDraft,\n DateRangeStatus,\n DayString,\n} from './DateRangePicker.types';\n\nexport interface DateRangeFieldsProps {\n value: DateRangeDraft;\n status: DateRangeStatus;\n messages: DateRangePickerMessages;\n maxRangeDays: number | undefined;\n onEdit: (which: 'from' | 'to', bound: DayString | null) => void;\n /** Id of the status line, so both fields can point at it. */\n statusId: string;\n dataTestId: string;\n}\n\nexport function DateRangeFields({\n value,\n status,\n messages,\n maxRangeDays,\n onEdit,\n statusId,\n dataTestId,\n}: DateRangeFieldsProps): React.JSX.Element {\n // \"Incomplete\" is not an error — it is a range that is not finished yet, and\n // painting it red would make the first click on a two-click control look like\n // a mistake. The pair being impossible is what gets marked.\n const rejected = !status.ok && status.problem !== 'incomplete';\n\n return (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>\n <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>\n <DayBoundInput\n mask={messages.dayMask}\n label={messages.from}\n value={value.from ?? undefined}\n onChange={(bound) => onEdit('from', bound ?? null)}\n testId={`${dataTestId}-from`}\n error={rejected}\n describedBy={statusId}\n />\n <DayBoundInput\n mask={messages.dayMask}\n label={messages.to}\n value={value.to ?? undefined}\n onChange={(bound) => onEdit('to', bound ?? null)}\n testId={`${dataTestId}-to`}\n error={rejected}\n describedBy={statusId}\n />\n </Box>\n <Typography\n id={statusId}\n // A live region, so the reason a range was refused is spoken when it\n // appears rather than only being found by someone who goes looking.\n role=\"status\"\n aria-live=\"polite\"\n variant=\"body2\"\n color={rejected ? 'error.main' : 'text.secondary'}\n data-testid={`${dataTestId}-status`}\n >\n {statusMessage(status, messages, maxRangeDays)}\n </Typography>\n </Box>\n );\n}\n","/**\n * The quick-pick column beside the calendar.\n *\n * These are BUTTONS, not links or list rows: each one sets the range and the\n * pressed one stays visibly pressed, which is what `aria-pressed` says out\n * loud. (A listbox with `aria-selected` would model it too — the rule is to\n * pick one and use it for every entry, so a reader's screen reader describes\n * the whole column the same way.)\n *\n * An over-cap entry is `aria-disabled` and keeps its place in the tab order,\n * with the reason rendered under the label. Removing it, or quietly shortening\n * the window it hands back, both answer \"why can't I have this year?\" with\n * silence.\n */\nimport { Box } from '@mui/material';\nimport type { SxProps, Theme } from '@mui/material';\nimport React, { useId } from 'react';\n\nimport type { ResolvedQuickRange } from './DateRangePicker.types';\n\nconst optionSx = (active: boolean, overMax: boolean): SxProps<Theme> => ({\n display: 'block',\n // Full-width rows in the side column; pills in the narrow row. A fixed full\n // width in both would make the narrow layout nine stacked lines.\n width: { xs: 'auto', md: '100%' },\n // A pill must not be squeezed by its neighbours — the row SCROLLS rather\n // than compressing \"Este trimestre\" into two characters and an ellipsis.\n flex: { xs: '0 0 auto', md: '0 1 auto' },\n textAlign: 'left',\n font: 'inherit',\n fontSize: '0.875rem',\n fontWeight: active ? 600 : 400,\n lineHeight: 1.4,\n px: 1.5,\n py: 0.75,\n whiteSpace: { xs: 'nowrap', md: 'normal' },\n border: '1px solid',\n // Narrow, an entry has to look tappable on its own, with no column and no\n // rule to group it — so the pill carries a visible edge. Beside the calendar\n // the group already reads as a list and a border on every row would be nine\n // boxes where the selected one should be the only marked thing.\n borderColor: active ? 'primary.main' : { xs: 'divider', md: 'transparent' },\n borderRadius: { xs: 999, md: 1 },\n cursor: overMax ? 'not-allowed' : 'pointer',\n color: overMax ? 'text.disabled' : active ? 'primary.main' : 'text.primary',\n backgroundColor: active ? 'action.selected' : 'transparent',\n '&:hover': { backgroundColor: overMax ? 'transparent' : 'action.hover' },\n '&:focus-visible': { outline: '2px solid', outlineColor: 'primary.main', outlineOffset: 1 },\n});\n\nconst REASON_SX: SxProps<Theme> = {\n display: 'block',\n fontSize: '0.6875rem',\n lineHeight: 1.3,\n mt: 0.25,\n color: 'text.disabled',\n};\n\nexport interface DateRangeQuickListProps {\n options: ResolvedQuickRange[];\n /** The entry the current draft equals, if any. */\n activeId: string | undefined;\n onPick: (option: ResolvedQuickRange) => void;\n /** Why an entry is refused, rendered under it. */\n reasonFor: (option: ResolvedQuickRange) => string;\n /** Accessible name of the group. */\n label: string;\n dataTestId: string;\n}\n\nexport function DateRangeQuickList({\n options,\n activeId,\n onPick,\n reasonFor,\n label,\n dataTestId,\n}: DateRangeQuickListProps): React.JSX.Element {\n const reasonPrefix = useId();\n\n return (\n <Box\n role=\"group\"\n aria-label={label}\n data-testid={`${dataTestId}-quick-list`}\n sx={{\n display: 'flex',\n // A column beside the calendar; ONE scrolling row of pills above it\n // once there is no width for both.\n //\n // Scrolling rather than wrapping: nine wrapped pills take three or four\n // lines at 390px, which pushes the calendar most of a screen down and\n // makes the quick ranges — the fastest way to answer — the thing you\n // have to scroll past. One line keeps the calendar in view and costs a\n // horizontal swipe, which is the gesture a chip row already invites.\n flexDirection: { xs: 'row', md: 'column' },\n flexWrap: { xs: 'nowrap', md: 'wrap' },\n overflowX: { xs: 'auto', md: 'visible' },\n // Room for the focus ring, which an `overflow` container would clip.\n px: { xs: 0.25, md: 0 },\n py: { xs: 0.25, md: 0 },\n // The scrollbar is chrome on a phone and noise on a trackpad; the pills\n // themselves are the affordance, and the last one sits half-cut at the\n // edge to say the row continues.\n scrollbarWidth: 'none',\n '&::-webkit-scrollbar': { display: 'none' },\n alignContent: 'flex-start',\n gap: 0.5,\n minWidth: { md: 168 },\n maxWidth: { md: 200 },\n }}\n >\n {options.map((option) => {\n const active = option.id === activeId;\n const reasonId = `${reasonPrefix}-${option.id}`;\n return (\n <Box\n component=\"button\"\n type=\"button\"\n key={option.id}\n aria-pressed={active}\n // `aria-disabled`, never the `disabled` attribute: a disabled button\n // is skipped by the tab order, so the reason under it is unreachable\n // by exactly the reader most likely to need it.\n aria-disabled={option.overMax || undefined}\n aria-describedby={option.overMax ? reasonId : undefined}\n data-testid={`${dataTestId}-quick-${option.id}`}\n onClick={() => {\n if (!option.overMax) onPick(option);\n }}\n sx={optionSx(active, option.overMax)}\n >\n {option.label}\n {option.overMax && (\n <Box component=\"span\" id={reasonId} sx={REASON_SX}>\n {reasonFor(option)}\n </Box>\n )}\n </Box>\n );\n })}\n </Box>\n );\n}\n"],"mappings":"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","names":["Box","Box","useId","jsx","jsxs","jsx","jsxs","Box"]}
1
+ {"version":3,"sources":["../../src/components/form/DateRangePicker/DateRangePicker.tsx","../../src/components/form/DateRangePicker/DateRangePicker.dates.ts","../../src/components/form/DateRangePicker/DateRangePicker.hooks.ts","../../src/components/form/DateRangePicker/DateRangePicker.range.ts","../../src/components/form/DateRangePicker/DateRangePicker.quick.ts","../../src/components/form/DateRangePicker/DateRangePickerFields.tsx","../../src/components/form/DateRangePicker/DateRangePickerQuickList.tsx"],"sourcesContent":["'use client';\n\n/**\n * Three views of ONE range: a calendar, a quick-pick column, and two typed\n * dates. Change any and the other two follow.\n *\n * WHY IT EXISTS: a range calendar alone makes \"this quarter\" cost two clicks,\n * a month of paging, and a mental note of which day a quarter starts on; a list\n * of presets alone cannot express \"the 3rd to the 19th\"; and typed fields alone\n * make you spell out a window you can see. Each of the three is the fastest\n * route to a different range, so the control offers all three over one value\n * rather than picking a favourite.\n *\n * WHAT IT DOES NOT KNOW: anything about the data behind the range. No preset\n * list of a product's own, no ceiling of its own, no notion of whose clock\n * \"today\" runs on — `quickRanges`, `maxRangeDays` and `timeZone` are props, and\n * a consumer that needs its own periods passes them rather than teaching this\n * file about its domain. The calendar is `Calendar` in `selectionMode=\"range\"`,\n * not a second implementation of one.\n *\n * The value is a DRAFT (`{ from, to }`, either end possibly null) rather than a\n * finished window, so a half-picked or impossible range has somewhere to live\n * while it is being fixed. Every `onChange` carries the verdict with it.\n */\nimport { Box, Paper, useMediaQuery } from '@mui/material';\nimport type { SxProps, Theme } from '@mui/material';\nimport React from 'react';\n\nimport { Calendar } from '../Calendar';\n\nimport { toLocalDay } from './DateRangePicker.dates';\nimport { useDateRangePicker } from './DateRangePicker.hooks';\nimport { DEFAULT_QUICK_RANGES } from './DateRangePicker.quick';\nimport type { DateRangeDraft, DateRangePickerProps, WeekStart } from './DateRangePicker.types';\nimport { DateRangeFields } from './DateRangePickerFields';\nimport { DateRangeQuickList } from './DateRangePickerQuickList';\n\nconst CALENDAR_CLASS = 'date-range-picker__calendar';\n\n/** Below this the picker is a phone layout. MUI's own `md`, written out. */\nconst NARROW_QUERY = '(max-width:899.95px)';\n\nconst ROOT_SX: SxProps<Theme> = {\n display: 'inline-flex',\n flexDirection: 'column',\n gap: 2,\n p: 2,\n maxWidth: '100%',\n // `Calendar` renders its own `Paper` with 24px of padding. Nested in this one\n // that reads as a box inside a box, so the inner frame is flattened here\n // through the className hook the component exposes for exactly this.\n [`& .${CALENDAR_CLASS}`]: {\n p: 0,\n boxShadow: 'none',\n backgroundColor: 'transparent',\n backgroundImage: 'none',\n },\n};\n\nconst BODY_SX: SxProps<Theme> = {\n display: 'flex',\n // The quick column sits BESIDE the calendar on a wide screen. Narrow, it\n // moves ABOVE it — `column-reverse`, so the DOM order stays calendar-then-\n // list (the calendar is the primary control and should be reached first by a\n // screen reader and by Tab) while the eye meets the quick ranges first, which\n // is the order the phone mock asks for and the order a thumb wants: nine\n // one-tap answers before a grid of thirty.\n //\n // A side column at 390px would squeeze the day grid to roughly half its width\n // and make the numbers unreadable. Checked at 390px.\n flexDirection: { xs: 'column-reverse', md: 'row' },\n alignItems: { xs: 'stretch', md: 'flex-start' },\n gap: 2,\n maxWidth: '100%',\n};\n\n/**\n * The rule between the calendar and the quick ranges.\n *\n * Two lists of dates side by side with only whitespace between them read as one\n * ragged thing; the rule says which numbers belong to which control. It turns\n * with the layout — a vertical rule when the list is a column beside the\n * calendar, a horizontal one when it has moved above it — because a divider\n * that keeps its axis while the flex direction flips ends up separating nothing.\n */\nconst QUICK_DIVIDER_SX: SxProps<Theme> = {\n borderColor: 'divider',\n borderStyle: 'solid',\n borderWidth: 0,\n borderLeftWidth: { xs: 0, md: '1px' },\n borderBottomWidth: { xs: '1px', md: 0 },\n pl: { xs: 0, md: 2 },\n pb: { xs: 2, md: 0 },\n alignSelf: 'stretch',\n};\n\n/** And the rule above the typed pair, which is a third way in, not a caption. */\nconst FIELDS_DIVIDER_SX: SxProps<Theme> = {\n borderTop: '1px solid',\n borderColor: 'divider',\n pt: 2,\n};\n\ntype Picker = ReturnType<typeof useDateRangePicker>;\n\n/**\n * How many months to draw.\n *\n * A month grid has a floor: seven columns that must stay legible, which is\n * about 280px. Two of them need 580, so on a phone the consumer's\n * `numberOfMonths` is not a preference that can be honoured — it is a request\n * for a layout that does not fit. Capped here rather than left to the consumer,\n * because the consumer chooses for its own reason (the reports dialog wants two\n * because a period often crosses a month boundary) and has no way to know the\n * picker is currently 390px wide.\n *\n * A media query rather than an `sx` breakpoint: this decides what to RENDER,\n * and CSS cannot un-render a second month.\n *\n * The query is a LITERAL, not `theme.breakpoints.down('md')`. The callback form\n * reads the theme from context and throws on a null one, so a consumer that\n * mounts this outside a `ThemeProvider` would crash — which the unit tests here\n * do, and which is a fair thing for a consumer to do.\n */\nfunction useMonthCount(requested: number | undefined): number | undefined {\n const narrow = useMediaQuery(NARROW_QUERY);\n return narrow ? 1 : requested;\n}\n\n/** The grid, in range mode — `Calendar`, never a second implementation of one. */\nfunction RangeCalendar({\n calendarLabel,\n picker,\n value,\n locale,\n weekStartsOn,\n months,\n maxRangeDays,\n}: {\n picker: Picker;\n value: DateRangeDraft;\n locale: string;\n weekStartsOn: WeekStart;\n months: number | undefined;\n maxRangeDays: number | undefined;\n calendarLabel: string;\n}): React.JSX.Element {\n return (\n <Calendar\n key={picker.viewNonce}\n ariaLabel={calendarLabel}\n className={CALENDAR_CLASS}\n selectionMode=\"range\"\n locale={locale}\n // One prop drives the grid's first column AND the day \"this week\" starts\n // on, so a highlighted week can never begin mid-row.\n firstDayOfWeek={weekStartsOn}\n numberOfMonths={months}\n range={{ start: toLocalDay(value.from), end: toLocalDay(value.to) }}\n // BOTH callbacks, and the intermediate one does the work: the other fires\n // only on the click that CLOSES a range, so a picker wired to it alone\n // would not repaint after the first click.\n onIntermediateRangeChange={picker.pickOnCalendar}\n onRangeChange={picker.pickOnCalendar}\n // The same ceiling the quick list is judged against, so the calendar\n // cannot hand back a window the control has just called too long.\n maxRangeLength={maxRangeDays}\n />\n );\n}\n\n/** The quick ranges, inside the rule that separates them from the grid. */\nfunction QuickColumn({\n picker,\n maxRangeDays,\n dataTestId,\n}: {\n picker: Picker;\n maxRangeDays: number | undefined;\n dataTestId: string;\n}): React.JSX.Element | null {\n if (picker.options.length === 0) return null;\n return (\n <Box sx={QUICK_DIVIDER_SX}>\n <DateRangeQuickList\n options={picker.options}\n activeId={picker.activeId}\n onPick={picker.pickQuick}\n reasonFor={(option) =>\n picker.copy.overMax({ maxRangeDays: maxRangeDays ?? option.days, days: option.days })\n }\n label={picker.copy.quickRanges}\n dataTestId={dataTestId}\n />\n </Box>\n );\n}\n\nexport function DateRangePicker({\n value,\n onChange,\n timeZone,\n now,\n maxRangeDays,\n weekStartsOn = 0,\n quickRanges = DEFAULT_QUICK_RANGES,\n locale = 'en-US',\n numberOfMonths,\n messages,\n dataTestId = 'date-range-picker',\n className,\n}: DateRangePickerProps): React.JSX.Element {\n const picker = useDateRangePicker({\n value,\n onChange,\n timeZone,\n now,\n maxRangeDays,\n weekStartsOn,\n quickRanges,\n messages,\n });\n const months = useMonthCount(numberOfMonths);\n\n return (\n <Paper className={className} data-testid={dataTestId} sx={ROOT_SX}>\n <Box sx={BODY_SX}>\n <RangeCalendar\n calendarLabel={picker.copy.calendarLabel}\n picker={picker}\n value={value}\n locale={locale}\n weekStartsOn={weekStartsOn}\n months={months}\n maxRangeDays={maxRangeDays}\n />\n <QuickColumn picker={picker} maxRangeDays={maxRangeDays} dataTestId={dataTestId} />\n </Box>\n <Box sx={FIELDS_DIVIDER_SX}>\n <DateRangeFields\n value={value}\n status={picker.status}\n messages={picker.copy}\n maxRangeDays={maxRangeDays}\n onEdit={picker.editBound}\n statusId={picker.statusId}\n dataTestId={dataTestId}\n />\n </Box>\n </Paper>\n );\n}\n\nexport default DateRangePicker;\n","/**\n * Civil-day arithmetic for the range picker.\n *\n * Two representations meet here and the boundary between them is the whole\n * point of the file:\n *\n * - `AAAA-MM-DD` — what the picker's value, the quick ranges and any caller\n * speak. A day with no instant attached, so it cannot drift.\n * - a LOCAL `Date` at NOON — what `Calendar` deals in. Noon (not midnight) is\n * `Calendar.dates.normalizeDate`'s own rule: a daylight-saving jump at\n * midnight can move a date across a day boundary, and midday never can.\n *\n * The arithmetic itself is `Calendar.dates`' — `addDays` and `startOfWeek` are\n * imported rather than re-written, so a week here starts on the same day the\n * grid draws it.\n *\n * ONE thing needs a time zone: which day is today. Everything after that is\n * calendar arithmetic that has no clock in it.\n */\nimport { addDays, startOfWeek } from '../Calendar/Calendar.dates';\n\nimport type { DayString, WeekStart } from './DateRangePicker.types';\n\nconst NOON = 12;\nconst MS_PER_DAY = 86_400_000;\nconst MONTHS_PER_QUARTER = 3;\nconst DAY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\n/** `[year, month 1-12, day]`, or null when the text is not a day at all. */\nfunction dayParts(day: DayString): [number, number, number] | null {\n const parts = DAY_PATTERN.exec(day);\n if (!parts) return null;\n return [Number(parts[1]), Number(parts[2]), Number(parts[3])];\n}\n\nconst pad = (value: number): string => String(value).padStart(2, '0');\n\n/** `AAAA-MM-DD` from calendar parts. Month is 1-12. */\nfunction formatDay(year: number, month: number, day: number): DayString {\n return `${String(year).padStart(4, '0')}-${pad(month)}-${pad(day)}`;\n}\n\n/** A LOCAL `Date` → the day it falls on, read off the same local getters. */\nexport function toDayString(date: Date): DayString {\n return formatDay(date.getFullYear(), date.getMonth() + 1, date.getDate());\n}\n\n/**\n * `AAAA-MM-DD` → a LOCAL `Date` at noon, or null when the text is not a real\n * day.\n *\n * Deliberately not `new Date(day)`: the spec reads a bare `AAAA-MM-DD` as UTC\n * midnight, so west of Greenwich it lands on the day BEFORE and every date in\n * the picker is off by one. The round-trip check is what rejects `2026-02-31`,\n * which `Date` would otherwise roll silently into March.\n */\nexport function toLocalDay(day: DayString | null | undefined): Date | null {\n if (!day) return null;\n const parts = dayParts(day);\n if (!parts) return null;\n const date = new Date(parts[0], parts[1] - 1, parts[2], NOON);\n return toDayString(date) === day ? date : null;\n}\n\nconst civilFormatters = new Map<string, Intl.DateTimeFormat>();\n\nfunction civilFormatter(timeZone: string): Intl.DateTimeFormat {\n const cached = civilFormatters.get(timeZone);\n if (cached) return cached;\n const created = new Intl.DateTimeFormat('en-US', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n });\n civilFormatters.set(timeZone, created);\n return created;\n}\n\n/**\n * The civil day `now` falls on, on `timeZone`'s clock (the host's when absent).\n *\n * Read through `formatToParts` rather than by adding an offset to an epoch: the\n * zone database already knows about every transition, and reconstructing one by\n * hand is how a picker ends up a day out for exactly one week of the year.\n */\nexport function todayIn(timeZone: string | undefined, now: Date): DayString {\n if (!timeZone) return toDayString(now);\n const parts = civilFormatter(timeZone).formatToParts(now);\n const read = (type: Intl.DateTimeFormatPartTypes): string =>\n parts.find((part) => part.type === type)?.value ?? '';\n return `${read('year')}-${read('month')}-${read('day')}`;\n}\n\n/** The same day `days` later (or earlier). */\nexport function shiftDay(day: DayString, days: number): DayString {\n const date = toLocalDay(day);\n return date ? toDayString(addDays(date, days)) : day;\n}\n\n/** The 1st of `day`'s month. */\nexport function startOfMonthDay(day: DayString): DayString {\n const parts = dayParts(day);\n return parts ? formatDay(parts[0], parts[1], 1) : day;\n}\n\n/**\n * The 1st of `day`'s CALENDAR quarter — Jan/Apr/Jul/Oct. Fiscal quarters are a\n * different thing and belong in a caller's own quick range, not in a default.\n */\nexport function startOfQuarterDay(day: DayString): DayString {\n const parts = dayParts(day);\n if (!parts) return day;\n const month = parts[1] - ((parts[1] - 1) % MONTHS_PER_QUARTER);\n return formatDay(parts[0], month, 1);\n}\n\n/** The 1st of January of `day`'s year. */\nexport function startOfYearDay(day: DayString): DayString {\n const parts = dayParts(day);\n return parts ? formatDay(parts[0], 1, 1) : day;\n}\n\n/** The first day of `day`'s week, on the same week start the grid uses. */\nexport function startOfWeekDay(day: DayString, weekStartsOn: WeekStart): DayString {\n const date = toLocalDay(day);\n return date ? toDayString(startOfWeek(date, weekStartsOn)) : day;\n}\n\n/**\n * How many days a window covers, both ends INCLUDED — so a single day is 1.\n *\n * Measured on UTC epochs rather than on the local dates: the difference between\n * two local noons can be 23 or 25 hours across a transition, and dividing that\n * by a fixed day length is how a 30-day window starts reporting 29.96.\n */\nexport function dayCount(from: DayString, to: DayString): number {\n const start = dayParts(from);\n const end = dayParts(to);\n if (!start || !end) return 0;\n const startMs = Date.UTC(start[0], start[1] - 1, start[2]);\n const endMs = Date.UTC(end[0], end[1] - 1, end[2]);\n return Math.round((endMs - startMs) / MS_PER_DAY) + 1;\n}\n\n/** `AAAA-MM-DD` sorts lexicographically, so this needs no parsing at all. */\nexport function isAfterDay(a: DayString, b: DayString): boolean {\n return a > b;\n}\n","/**\n * Everything the picker derives and every way its range can move, kept out of\n * the component so that file is a layout and this one is the behaviour — the\n * same split `Calendar.hooks.ts` uses next door.\n */\nimport { useId, useMemo, useState } from 'react';\n\nimport { toDayString, todayIn } from './DateRangePicker.dates';\nimport {\n activeQuickRangeId,\n defaultMessages,\n resolveDayRange,\n resolveQuickRanges,\n} from './DateRangePicker.range';\nimport type {\n DateRangeChangeMeta,\n DateRangeDraft,\n DateRangePickerState,\n UseDateRangePickerArgs,\n} from './DateRangePicker.types';\n\nexport function useDateRangePicker({\n value,\n onChange,\n timeZone,\n now,\n maxRangeDays,\n weekStartsOn,\n quickRanges,\n messages,\n}: UseDateRangePickerArgs): DateRangePickerState {\n const statusId = useId();\n // `Calendar` reads which month to open on from the range it is handed, and\n // only when it MOUNTS. Bumping this key remounts it, which is how the other\n // two views move it. It changes for a quick pick and for a typed date and\n // NEVER for a click inside the grid — a calendar that jumped under the\n // pointer picking in it would be unusable.\n const [viewNonce, setViewNonce] = useState(0);\n\n const copy = useMemo(() => ({ ...defaultMessages(), ...messages }), [messages]);\n const today = useMemo(() => todayIn(timeZone, now ?? new Date()), [timeZone, now]);\n const options = useMemo(\n () => resolveQuickRanges(quickRanges, { today, weekStartsOn }, maxRangeDays),\n [quickRanges, today, weekStartsOn, maxRangeDays],\n );\n const status = useMemo(() => resolveDayRange(value, maxRangeDays), [value, maxRangeDays]);\n\n const emit = (\n next: DateRangeDraft,\n source: DateRangeChangeMeta['source'],\n quickRangeId?: string,\n ): void => {\n onChange(next, {\n source,\n ...(quickRangeId ? { quickRangeId } : {}),\n // Judged here, once. A caller left to re-derive \"is this usable\" is a\n // second opinion able to disagree with the message on screen.\n status: resolveDayRange(next, maxRangeDays),\n });\n };\n\n return {\n copy,\n options,\n activeId: activeQuickRangeId(options, value),\n status,\n statusId,\n viewNonce,\n pickQuick: (option) => {\n setViewNonce((nonce) => nonce + 1);\n emit({ from: option.window.from, to: option.window.to }, 'quick', option.id);\n },\n editBound: (which, bound) => {\n setViewNonce((nonce) => nonce + 1);\n emit({ ...value, [which]: bound }, which);\n },\n pickOnCalendar: (range) => {\n emit(\n {\n from: range.start ? toDayString(range.start) : null,\n to: range.end ? toDayString(range.end) : null,\n },\n 'calendar',\n );\n },\n };\n}\n","/**\n * Judging a draft, and saying so in words.\n *\n * The picker never repairs what it is given. A reversed pair is not silently\n * swapped and an over-long one is not clipped to the cap: both are reported,\n * because a control that returns a different range from the one on screen is\n * indistinguishable from one that ignored you. The caller reads\n * {@link DateRangeStatus} and keeps its own confirm button shut.\n */\nimport { isoToMasked } from '../../data-display/DataViews/data-views-day-input';\n\nimport { dayCount, isAfterDay } from './DateRangePicker.dates';\nimport type {\n DateRangePickerMessages,\n DateRangeDraft,\n DateRangeStatus,\n QuickRange,\n QuickRangeContext,\n ResolvedQuickRange,\n} from './DateRangePicker.types';\n\n/**\n * Whether a draft is a window this picker will hand over.\n *\n * Exported because the caller needs the same verdict the picker shows: a dialog\n * disabling its \"Apply\" button must not re-derive \"is this legal\" from the two\n * strings, or the two answers drift and the button ends up enabled over a\n * message saying the range is impossible.\n */\nexport function resolveDayRange(\n draft: DateRangeDraft,\n maxRangeDays?: number,\n): DateRangeStatus {\n const { from, to } = draft;\n if (!from || !to) return { ok: false, problem: 'incomplete', days: 0 };\n if (isAfterDay(from, to)) return { ok: false, problem: 'reversed', days: 0 };\n\n const days = dayCount(from, to);\n if (maxRangeDays !== undefined && days > maxRangeDays) {\n return { ok: false, problem: 'over-max', days };\n }\n return { ok: true, window: { from, to }, days };\n}\n\n/**\n * The English defaults.\n *\n * The summary prints the days in the SAME `dd/mm/aaaa` the two fields are\n * masked to, rather than in the host locale's order. One control cannot show\n * the same date two ways: an `en-US` host formatting the summary as `08/05`\n * over a field reading `05/08` leaves the reader to work out which of the two\n * is lying. A host that wants its own order replaces `messages.summary`, which\n * moves both halves of the decision to the same place.\n */\nexport function defaultMessages(): DateRangePickerMessages {\n // ONE mask, read by both the fields and the summary below. They are the same\n // date in the same control, so a summary formatted in a different order from\n // the boxes it summarises is a control arguing with itself — which is what\n // two independent literals eventually become.\n const dayMask = 'dd/mm/yyyy';\n return {\n from: 'Start date',\n to: 'End date',\n quickRanges: 'Quick ranges',\n incomplete: 'Choose both dates.',\n reversed: 'The end date must be on or after the start date.',\n overMax: ({ maxRangeDays }) => `Choose a range of at most ${maxRangeDays} days.`,\n summary: ({ from, to }) => `${isoToMasked(from, dayMask)} – ${isoToMasked(to, dayMask)}`,\n calendarLabel: 'Calendar',\n dayMask,\n };\n}\n\n/** The line under the fields: the chosen window, or why there isn't one. */\nexport function statusMessage(\n status: DateRangeStatus,\n messages: DateRangePickerMessages,\n maxRangeDays: number | undefined,\n): string {\n if (status.ok) return messages.summary(status.window);\n if (status.problem === 'incomplete') return messages.incomplete;\n if (status.problem === 'reversed') return messages.reversed;\n return messages.overMax({ maxRangeDays: maxRangeDays ?? status.days, days: status.days });\n}\n\n/** Every quick range with its window, its length, and whether it is over cap. */\nexport function resolveQuickRanges(\n quickRanges: QuickRange[],\n context: QuickRangeContext,\n maxRangeDays: number | undefined,\n): ResolvedQuickRange[] {\n return quickRanges.map((quickRange) => {\n const window = quickRange.resolve(context);\n const days = dayCount(window.from, window.to);\n return {\n ...quickRange,\n window,\n days,\n overMax: maxRangeDays !== undefined && days > maxRangeDays,\n };\n });\n}\n\n/** The quick range the draft currently IS, if it is exactly one of them. */\nexport function activeQuickRangeId(\n options: ResolvedQuickRange[],\n draft: DateRangeDraft,\n): string | undefined {\n return options.find(\n (option) => option.window.from === draft.from && option.window.to === draft.to,\n )?.id;\n}\n","/**\n * The quick-pick column's default list, and how to relabel it.\n *\n * These are DOMAIN-FREE on purpose: every one of them is derivable from \"today\"\n * and the calendar, so the design system can ship them without knowing what the\n * range is for. A product's own periods (a fiscal quarter, \"since the store\n * opened\", the last closed month) are a `quickRanges` array the caller passes —\n * the prop exists so that list never has to be smuggled in here.\n *\n * \"Last N days\" INCLUDES today, which is the reading every dashboard in the\n * wild uses: on the 10th, \"last 7 days\" is the 4th through the 10th, not the\n * 3rd through the 9th.\n */\nimport {\n shiftDay,\n startOfMonthDay,\n startOfQuarterDay,\n startOfWeekDay,\n startOfYearDay,\n} from './DateRangePicker.dates';\nimport type { QuickRange, QuickRangeContext } from './DateRangePicker.types';\n\n/**\n * The ids the built-in list uses. Stable API: a caller keys off these to map a\n * quick pick onto something of its own (a stored preset, an event name), and\n * renaming one is a breaking change in a way that renaming a label is not.\n */\nexport const QUICK_RANGE_IDS = [\n 'today',\n 'yesterday',\n 'this-week',\n 'last-7-days',\n 'this-month',\n 'last-30-days',\n 'this-quarter',\n 'this-year',\n 'last-365-days',\n] as const;\n\nexport type QuickRangeId = (typeof QUICK_RANGE_IDS)[number];\n\n/** English defaults. Every one is replaceable through {@link createQuickRanges}. */\nexport const DEFAULT_QUICK_RANGE_LABELS: Record<QuickRangeId, string> = {\n today: 'Today',\n yesterday: 'Yesterday',\n 'this-week': 'This week',\n 'last-7-days': 'Last 7 days',\n 'this-month': 'This month',\n 'last-30-days': 'Last 30 days',\n 'this-quarter': 'This quarter',\n 'this-year': 'This year',\n 'last-365-days': 'Last 365 days',\n};\n\n/** A window that ends today and reaches back `days` days, today included. */\nconst trailing =\n (days: number) =>\n ({ today }: QuickRangeContext) => ({ from: shiftDay(today, 1 - days), to: today });\n\n/** A window from the start of some period through today. */\nconst sinceStartOf =\n (start: (context: QuickRangeContext) => string) => (context: QuickRangeContext) => ({\n from: start(context),\n to: context.today,\n });\n\nconst RESOLVERS: Record<QuickRangeId, QuickRange['resolve']> = {\n today: trailing(1),\n yesterday: ({ today }) => ({ from: shiftDay(today, -1), to: shiftDay(today, -1) }),\n 'this-week': sinceStartOf(({ today, weekStartsOn }) => startOfWeekDay(today, weekStartsOn)),\n 'last-7-days': trailing(7),\n 'this-month': sinceStartOf(({ today }) => startOfMonthDay(today)),\n 'last-30-days': trailing(30),\n 'this-quarter': sinceStartOf(({ today }) => startOfQuarterDay(today)),\n 'this-year': sinceStartOf(({ today }) => startOfYearDay(today)),\n 'last-365-days': trailing(365),\n};\n\n/**\n * The built-in quick ranges, optionally relabelled.\n *\n * Relabelling rather than rebuilding is the point: a Portuguese host wants\n * \"Este mês\" over the same month-to-date arithmetic, and re-deriving the dates\n * to get the word is how two lists that claim the same thing start disagreeing.\n */\nexport function createQuickRanges(\n labels: Partial<Record<QuickRangeId, string>> = {},\n): QuickRange[] {\n return QUICK_RANGE_IDS.map((id) => ({\n id,\n label: labels[id] ?? DEFAULT_QUICK_RANGE_LABELS[id],\n resolve: RESOLVERS[id],\n }));\n}\n\n/** The list a picker uses when the caller says nothing. */\nexport const DEFAULT_QUICK_RANGES: QuickRange[] = createQuickRanges();\n","/**\n * The two typed ends, and the one line that says what the picker currently has.\n *\n * WHEN A TYPED DATE COMMITS: the moment it is a WHOLE, REAL day — eight digits\n * that name a date that exists. Not on every keystroke (`2026` passes through\n * `0002`, `0020` and `0202`, each a legal year, and committing those makes the\n * calendar jump to the year 20 while you are still typing) and not only on blur\n * (a reader who types a date and looks at the calendar to check it would see\n * the old month until they clicked somewhere else). Half-typed text stays in\n * the field and changes nothing; leaving the field discards it and shows what\n * the range actually is. Enter needs no special case — by the time it can be\n * pressed the date has already committed.\n *\n * The field itself is `DayBoundInput`, the masked `dd/mm/aaaa` control the data\n * views already use. Same package, imported rather than re-implemented: a\n * second masked date field is a second set of rules for what backspace does.\n */\nimport { Box, Typography } from '@mui/material';\nimport React from 'react';\n\nimport { DayBoundInput } from '../../data-display/DataViews/data-views-day-input';\n\nimport { statusMessage } from './DateRangePicker.range';\nimport type {\n DateRangePickerMessages,\n DateRangeDraft,\n DateRangeStatus,\n DayString,\n} from './DateRangePicker.types';\n\nexport interface DateRangeFieldsProps {\n value: DateRangeDraft;\n status: DateRangeStatus;\n messages: DateRangePickerMessages;\n maxRangeDays: number | undefined;\n onEdit: (which: 'from' | 'to', bound: DayString | null) => void;\n /** Id of the status line, so both fields can point at it. */\n statusId: string;\n dataTestId: string;\n}\n\nexport function DateRangeFields({\n value,\n status,\n messages,\n maxRangeDays,\n onEdit,\n statusId,\n dataTestId,\n}: DateRangeFieldsProps): React.JSX.Element {\n // \"Incomplete\" is not an error — it is a range that is not finished yet, and\n // painting it red would make the first click on a two-click control look like\n // a mistake. The pair being impossible is what gets marked.\n const rejected = !status.ok && status.problem !== 'incomplete';\n\n return (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>\n <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>\n <DayBoundInput\n mask={messages.dayMask}\n label={messages.from}\n value={value.from ?? undefined}\n onChange={(bound) => onEdit('from', bound ?? null)}\n testId={`${dataTestId}-from`}\n error={rejected}\n describedBy={statusId}\n />\n <DayBoundInput\n mask={messages.dayMask}\n label={messages.to}\n value={value.to ?? undefined}\n onChange={(bound) => onEdit('to', bound ?? null)}\n testId={`${dataTestId}-to`}\n error={rejected}\n describedBy={statusId}\n />\n </Box>\n <Typography\n id={statusId}\n // A live region, so the reason a range was refused is spoken when it\n // appears rather than only being found by someone who goes looking.\n role=\"status\"\n aria-live=\"polite\"\n variant=\"body2\"\n color={rejected ? 'error.main' : 'text.secondary'}\n data-testid={`${dataTestId}-status`}\n >\n {statusMessage(status, messages, maxRangeDays)}\n </Typography>\n </Box>\n );\n}\n","/**\n * The quick-pick column beside the calendar.\n *\n * These are BUTTONS, not links or list rows: each one sets the range and the\n * pressed one stays visibly pressed, which is what `aria-pressed` says out\n * loud. (A listbox with `aria-selected` would model it too — the rule is to\n * pick one and use it for every entry, so a reader's screen reader describes\n * the whole column the same way.)\n *\n * An over-cap entry is `aria-disabled` and keeps its place in the tab order,\n * with the reason rendered under the label. Removing it, or quietly shortening\n * the window it hands back, both answer \"why can't I have this year?\" with\n * silence.\n */\nimport { Box } from '@mui/material';\nimport type { SxProps, Theme } from '@mui/material';\nimport React, { useId } from 'react';\n\nimport type { ResolvedQuickRange } from './DateRangePicker.types';\n\nconst optionSx = (active: boolean, overMax: boolean): SxProps<Theme> => ({\n display: 'block',\n // Full-width rows in the side column; pills in the narrow row. A fixed full\n // width in both would make the narrow layout nine stacked lines.\n width: { xs: 'auto', md: '100%' },\n // A pill must not be squeezed by its neighbours — the row SCROLLS rather\n // than compressing \"Este trimestre\" into two characters and an ellipsis.\n flex: { xs: '0 0 auto', md: '0 1 auto' },\n textAlign: 'left',\n font: 'inherit',\n fontSize: '0.875rem',\n fontWeight: active ? 600 : 400,\n lineHeight: 1.4,\n px: 1.5,\n py: 0.75,\n whiteSpace: { xs: 'nowrap', md: 'normal' },\n border: '1px solid',\n // Narrow, an entry has to look tappable on its own, with no column and no\n // rule to group it — so the pill carries a visible edge. Beside the calendar\n // the group already reads as a list and a border on every row would be nine\n // boxes where the selected one should be the only marked thing.\n borderColor: active ? 'primary.main' : { xs: 'divider', md: 'transparent' },\n borderRadius: { xs: 999, md: 1 },\n cursor: overMax ? 'not-allowed' : 'pointer',\n color: overMax ? 'text.disabled' : active ? 'primary.main' : 'text.primary',\n backgroundColor: active ? 'action.selected' : 'transparent',\n '&:hover': { backgroundColor: overMax ? 'transparent' : 'action.hover' },\n '&:focus-visible': { outline: '2px solid', outlineColor: 'primary.main', outlineOffset: 1 },\n});\n\nconst REASON_SX: SxProps<Theme> = {\n display: 'block',\n fontSize: '0.6875rem',\n lineHeight: 1.3,\n mt: 0.25,\n color: 'text.disabled',\n};\n\nexport interface DateRangeQuickListProps {\n options: ResolvedQuickRange[];\n /** The entry the current draft equals, if any. */\n activeId: string | undefined;\n onPick: (option: ResolvedQuickRange) => void;\n /** Why an entry is refused, rendered under it. */\n reasonFor: (option: ResolvedQuickRange) => string;\n /** Accessible name of the group. */\n label: string;\n dataTestId: string;\n}\n\nexport function DateRangeQuickList({\n options,\n activeId,\n onPick,\n reasonFor,\n label,\n dataTestId,\n}: DateRangeQuickListProps): React.JSX.Element {\n const reasonPrefix = useId();\n\n return (\n <Box\n role=\"group\"\n aria-label={label}\n data-testid={`${dataTestId}-quick-list`}\n sx={{\n display: 'flex',\n // A column beside the calendar; ONE scrolling row of pills above it\n // once there is no width for both.\n //\n // Scrolling rather than wrapping: nine wrapped pills take three or four\n // lines at 390px, which pushes the calendar most of a screen down and\n // makes the quick ranges — the fastest way to answer — the thing you\n // have to scroll past. One line keeps the calendar in view and costs a\n // horizontal swipe, which is the gesture a chip row already invites.\n flexDirection: { xs: 'row', md: 'column' },\n flexWrap: { xs: 'nowrap', md: 'wrap' },\n overflowX: { xs: 'auto', md: 'visible' },\n // Room for the focus ring, which an `overflow` container would clip.\n px: { xs: 0.25, md: 0 },\n py: { xs: 0.25, md: 0 },\n // The scrollbar is chrome on a phone and noise on a trackpad; the pills\n // themselves are the affordance, and the last one sits half-cut at the\n // edge to say the row continues.\n scrollbarWidth: 'none',\n '&::-webkit-scrollbar': { display: 'none' },\n alignContent: 'flex-start',\n gap: 0.5,\n minWidth: { md: 168 },\n maxWidth: { md: 200 },\n }}\n >\n {options.map((option) => {\n const active = option.id === activeId;\n const reasonId = `${reasonPrefix}-${option.id}`;\n return (\n <Box\n component=\"button\"\n type=\"button\"\n key={option.id}\n aria-pressed={active}\n // `aria-disabled`, never the `disabled` attribute: a disabled button\n // is skipped by the tab order, so the reason under it is unreachable\n // by exactly the reader most likely to need it.\n aria-disabled={option.overMax || undefined}\n aria-describedby={option.overMax ? reasonId : undefined}\n data-testid={`${dataTestId}-quick-${option.id}`}\n onClick={() => {\n if (!option.overMax) onPick(option);\n }}\n sx={optionSx(active, option.overMax)}\n >\n {option.label}\n {option.overMax && (\n <Box component=\"span\" id={reasonId} sx={REASON_SX}>\n {reasonFor(option)}\n </Box>\n )}\n </Box>\n );\n })}\n </Box>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAwBA,SAAS,OAAAA,MAAK,OAAO,qBAAqB;;;ACD1C,IAAM,OAAO;AACb,IAAM,aAAa;AACnB,IAAM,qBAAqB;AAC3B,IAAM,cAAc;AAGpB,SAAS,SAAS,KAAiD;AACjE,QAAM,QAAQ,YAAY,KAAK,GAAG;AAClC,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,CAAC,OAAO,MAAM,CAAC,CAAC,GAAG,OAAO,MAAM,CAAC,CAAC,GAAG,OAAO,MAAM,CAAC,CAAC,CAAC;AAC9D;AAEA,IAAM,MAAM,CAAC,UAA0B,OAAO,KAAK,EAAE,SAAS,GAAG,GAAG;AAGpE,SAAS,UAAU,MAAc,OAAe,KAAwB;AACtE,SAAO,GAAG,OAAO,IAAI,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,GAAG,CAAC;AACnE;AAGO,SAAS,YAAY,MAAuB;AACjD,SAAO,UAAU,KAAK,YAAY,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,QAAQ,CAAC;AAC1E;AAWO,SAAS,WAAW,KAAgD;AACzE,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,QAAQ,SAAS,GAAG;AAC1B,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,IAAI,KAAK,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,GAAG,IAAI;AAC5D,SAAO,YAAY,IAAI,MAAM,MAAM,OAAO;AAC5C;AAEA,IAAM,kBAAkB,oBAAI,IAAiC;AAE7D,SAAS,eAAe,UAAuC;AAC7D,QAAM,SAAS,gBAAgB,IAAI,QAAQ;AAC3C,MAAI,OAAQ,QAAO;AACnB,QAAM,UAAU,IAAI,KAAK,eAAe,SAAS;AAAA,IAC/C;AAAA,IACA,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK;AAAA,EACP,CAAC;AACD,kBAAgB,IAAI,UAAU,OAAO;AACrC,SAAO;AACT;AASO,SAAS,QAAQ,UAA8B,KAAsB;AAC1E,MAAI,CAAC,SAAU,QAAO,YAAY,GAAG;AACrC,QAAM,QAAQ,eAAe,QAAQ,EAAE,cAAc,GAAG;AACxD,QAAM,OAAO,CAAC,SACZ,MAAM,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI,GAAG,SAAS;AACrD,SAAO,GAAG,KAAK,MAAM,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,KAAK,KAAK,CAAC;AACxD;AAGO,SAAS,SAAS,KAAgB,MAAyB;AAChE,QAAM,OAAO,WAAW,GAAG;AAC3B,SAAO,OAAO,YAAY,QAAQ,MAAM,IAAI,CAAC,IAAI;AACnD;AAGO,SAAS,gBAAgB,KAA2B;AACzD,QAAM,QAAQ,SAAS,GAAG;AAC1B,SAAO,QAAQ,UAAU,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI;AACpD;AAMO,SAAS,kBAAkB,KAA2B;AAC3D,QAAM,QAAQ,SAAS,GAAG;AAC1B,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,QAAQ,MAAM,CAAC,KAAM,MAAM,CAAC,IAAI,KAAK;AAC3C,SAAO,UAAU,MAAM,CAAC,GAAG,OAAO,CAAC;AACrC;AAGO,SAAS,eAAe,KAA2B;AACxD,QAAM,QAAQ,SAAS,GAAG;AAC1B,SAAO,QAAQ,UAAU,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI;AAC7C;AAGO,SAAS,eAAe,KAAgB,cAAoC;AACjF,QAAM,OAAO,WAAW,GAAG;AAC3B,SAAO,OAAO,YAAY,YAAY,MAAM,YAAY,CAAC,IAAI;AAC/D;AASO,SAAS,SAAS,MAAiB,IAAuB;AAC/D,QAAM,QAAQ,SAAS,IAAI;AAC3B,QAAM,MAAM,SAAS,EAAE;AACvB,MAAI,CAAC,SAAS,CAAC,IAAK,QAAO;AAC3B,QAAM,UAAU,KAAK,IAAI,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;AACzD,QAAM,QAAQ,KAAK,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;AACjD,SAAO,KAAK,OAAO,QAAQ,WAAW,UAAU,IAAI;AACtD;AAGO,SAAS,WAAW,GAAc,GAAuB;AAC9D,SAAO,IAAI;AACb;;;AC/IA,SAAS,OAAO,SAAS,gBAAgB;;;ACwBlC,SAAS,gBACd,OACA,cACiB;AACjB,QAAM,EAAE,MAAM,GAAG,IAAI;AACrB,MAAI,CAAC,QAAQ,CAAC,GAAI,QAAO,EAAE,IAAI,OAAO,SAAS,cAAc,MAAM,EAAE;AACrE,MAAI,WAAW,MAAM,EAAE,EAAG,QAAO,EAAE,IAAI,OAAO,SAAS,YAAY,MAAM,EAAE;AAE3E,QAAM,OAAO,SAAS,MAAM,EAAE;AAC9B,MAAI,iBAAiB,UAAa,OAAO,cAAc;AACrD,WAAO,EAAE,IAAI,OAAO,SAAS,YAAY,KAAK;AAAA,EAChD;AACA,SAAO,EAAE,IAAI,MAAM,QAAQ,EAAE,MAAM,GAAG,GAAG,KAAK;AAChD;AAYO,SAAS,kBAA2C;AAKzD,QAAM,UAAU;AAChB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,SAAS,CAAC,EAAE,aAAa,MAAM,6BAA6B,YAAY;AAAA,IACxE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,YAAY,MAAM,OAAO,CAAC,WAAM,YAAY,IAAI,OAAO,CAAC;AAAA,IACtF,eAAe;AAAA,IACf;AAAA,EACF;AACF;AAGO,SAAS,cACd,QACA,UACA,cACQ;AACR,MAAI,OAAO,GAAI,QAAO,SAAS,QAAQ,OAAO,MAAM;AACpD,MAAI,OAAO,YAAY,aAAc,QAAO,SAAS;AACrD,MAAI,OAAO,YAAY,WAAY,QAAO,SAAS;AACnD,SAAO,SAAS,QAAQ,EAAE,cAAc,gBAAgB,OAAO,MAAM,MAAM,OAAO,KAAK,CAAC;AAC1F;AAGO,SAAS,mBACd,aACA,SACA,cACsB;AACtB,SAAO,YAAY,IAAI,CAAC,eAAe;AACrC,UAAM,SAAS,WAAW,QAAQ,OAAO;AACzC,UAAM,OAAO,SAAS,OAAO,MAAM,OAAO,EAAE;AAC5C,WAAO;AAAA,MACL,GAAG;AAAA,MACH;AAAA,MACA;AAAA,MACA,SAAS,iBAAiB,UAAa,OAAO;AAAA,IAChD;AAAA,EACF,CAAC;AACH;AAGO,SAAS,mBACd,SACA,OACoB;AACpB,SAAO,QAAQ;AAAA,IACb,CAAC,WAAW,OAAO,OAAO,SAAS,MAAM,QAAQ,OAAO,OAAO,OAAO,MAAM;AAAA,EAC9E,GAAG;AACL;;;AD1FO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiD;AAC/C,QAAM,WAAW,MAAM;AAMvB,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,CAAC;AAE5C,QAAM,OAAO,QAAQ,OAAO,EAAE,GAAG,gBAAgB,GAAG,GAAG,SAAS,IAAI,CAAC,QAAQ,CAAC;AAC9E,QAAM,QAAQ,QAAQ,MAAM,QAAQ,UAAU,OAAO,oBAAI,KAAK,CAAC,GAAG,CAAC,UAAU,GAAG,CAAC;AACjF,QAAM,UAAU;AAAA,IACd,MAAM,mBAAmB,aAAa,EAAE,OAAO,aAAa,GAAG,YAAY;AAAA,IAC3E,CAAC,aAAa,OAAO,cAAc,YAAY;AAAA,EACjD;AACA,QAAM,SAAS,QAAQ,MAAM,gBAAgB,OAAO,YAAY,GAAG,CAAC,OAAO,YAAY,CAAC;AAExF,QAAM,OAAO,CACX,MACA,QACA,iBACS;AACT,aAAS,MAAM;AAAA,MACb;AAAA,MACA,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA;AAAA;AAAA,MAGvC,QAAQ,gBAAgB,MAAM,YAAY;AAAA,IAC5C,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU,mBAAmB,SAAS,KAAK;AAAA,IAC3C;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,CAAC,WAAW;AACrB,mBAAa,CAAC,UAAU,QAAQ,CAAC;AACjC,WAAK,EAAE,MAAM,OAAO,OAAO,MAAM,IAAI,OAAO,OAAO,GAAG,GAAG,SAAS,OAAO,EAAE;AAAA,IAC7E;AAAA,IACA,WAAW,CAAC,OAAO,UAAU;AAC3B,mBAAa,CAAC,UAAU,QAAQ,CAAC;AACjC,WAAK,EAAE,GAAG,OAAO,CAAC,KAAK,GAAG,MAAM,GAAG,KAAK;AAAA,IAC1C;AAAA,IACA,gBAAgB,CAAC,UAAU;AACzB;AAAA,QACE;AAAA,UACE,MAAM,MAAM,QAAQ,YAAY,MAAM,KAAK,IAAI;AAAA,UAC/C,IAAI,MAAM,MAAM,YAAY,MAAM,GAAG,IAAI;AAAA,QAC3C;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;AE3DO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKO,IAAM,6BAA2D;AAAA,EACtE,OAAO;AAAA,EACP,WAAW;AAAA,EACX,aAAa;AAAA,EACb,eAAe;AAAA,EACf,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,iBAAiB;AACnB;AAGA,IAAM,WACJ,CAAC,SACD,CAAC,EAAE,MAAM,OAA0B,EAAE,MAAM,SAAS,OAAO,IAAI,IAAI,GAAG,IAAI,MAAM;AAGlF,IAAM,eACJ,CAAC,UAAkD,CAAC,aAAgC;AAAA,EAClF,MAAM,MAAM,OAAO;AAAA,EACnB,IAAI,QAAQ;AACd;AAEF,IAAM,YAAyD;AAAA,EAC7D,OAAO,SAAS,CAAC;AAAA,EACjB,WAAW,CAAC,EAAE,MAAM,OAAO,EAAE,MAAM,SAAS,OAAO,EAAE,GAAG,IAAI,SAAS,OAAO,EAAE,EAAE;AAAA,EAChF,aAAa,aAAa,CAAC,EAAE,OAAO,aAAa,MAAM,eAAe,OAAO,YAAY,CAAC;AAAA,EAC1F,eAAe,SAAS,CAAC;AAAA,EACzB,cAAc,aAAa,CAAC,EAAE,MAAM,MAAM,gBAAgB,KAAK,CAAC;AAAA,EAChE,gBAAgB,SAAS,EAAE;AAAA,EAC3B,gBAAgB,aAAa,CAAC,EAAE,MAAM,MAAM,kBAAkB,KAAK,CAAC;AAAA,EACpE,aAAa,aAAa,CAAC,EAAE,MAAM,MAAM,eAAe,KAAK,CAAC;AAAA,EAC9D,iBAAiB,SAAS,GAAG;AAC/B;AASO,SAAS,kBACd,SAAgD,CAAC,GACnC;AACd,SAAO,gBAAgB,IAAI,CAAC,QAAQ;AAAA,IAClC;AAAA,IACA,OAAO,OAAO,EAAE,KAAK,2BAA2B,EAAE;AAAA,IAClD,SAAS,UAAU,EAAE;AAAA,EACvB,EAAE;AACJ;AAGO,IAAM,uBAAqC,kBAAkB;;;AC/EpE,SAAS,KAAK,kBAAkB;AAwC1B,SACE,KADF;AAhBC,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4C;AAI1C,QAAM,WAAW,CAAC,OAAO,MAAM,OAAO,YAAY;AAElD,SACE,qBAAC,OAAI,IAAI,EAAE,SAAS,QAAQ,eAAe,UAAU,KAAK,EAAE,GAC1D;AAAA,yBAAC,OAAI,IAAI,EAAE,SAAS,QAAQ,UAAU,QAAQ,KAAK,EAAE,GACnD;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,SAAS;AAAA,UACf,OAAO,SAAS;AAAA,UAChB,OAAO,MAAM,QAAQ;AAAA,UACrB,UAAU,CAAC,UAAU,OAAO,QAAQ,SAAS,IAAI;AAAA,UACjD,QAAQ,GAAG,UAAU;AAAA,UACrB,OAAO;AAAA,UACP,aAAa;AAAA;AAAA,MACf;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,SAAS;AAAA,UACf,OAAO,SAAS;AAAA,UAChB,OAAO,MAAM,MAAM;AAAA,UACnB,UAAU,CAAC,UAAU,OAAO,MAAM,SAAS,IAAI;AAAA,UAC/C,QAAQ,GAAG,UAAU;AAAA,UACrB,OAAO;AAAA,UACP,aAAa;AAAA;AAAA,MACf;AAAA,OACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,IAAI;AAAA,QAGJ,MAAK;AAAA,QACL,aAAU;AAAA,QACV,SAAQ;AAAA,QACR,OAAO,WAAW,eAAe;AAAA,QACjC,eAAa,GAAG,UAAU;AAAA,QAEzB,wBAAc,QAAQ,UAAU,YAAY;AAAA;AAAA,IAC/C;AAAA,KACF;AAEJ;;;AC7EA,SAAS,OAAAC,YAAW;AAEpB,SAAgB,SAAAC,cAAa;AAoGnB,SAkBI,OAAAC,MAlBJ,QAAAC,aAAA;AAhGV,IAAM,WAAW,CAAC,QAAiB,aAAsC;AAAA,EACvE,SAAS;AAAA;AAAA;AAAA,EAGT,OAAO,EAAE,IAAI,QAAQ,IAAI,OAAO;AAAA;AAAA;AAAA,EAGhC,MAAM,EAAE,IAAI,YAAY,IAAI,WAAW;AAAA,EACvC,WAAW;AAAA,EACX,MAAM;AAAA,EACN,UAAU;AAAA,EACV,YAAY,SAAS,MAAM;AAAA,EAC3B,YAAY;AAAA,EACZ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,YAAY,EAAE,IAAI,UAAU,IAAI,SAAS;AAAA,EACzC,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,EAKR,aAAa,SAAS,iBAAiB,EAAE,IAAI,WAAW,IAAI,cAAc;AAAA,EAC1E,cAAc,EAAE,IAAI,KAAK,IAAI,EAAE;AAAA,EAC/B,QAAQ,UAAU,gBAAgB;AAAA,EAClC,OAAO,UAAU,kBAAkB,SAAS,iBAAiB;AAAA,EAC7D,iBAAiB,SAAS,oBAAoB;AAAA,EAC9C,WAAW,EAAE,iBAAiB,UAAU,gBAAgB,eAAe;AAAA,EACvE,mBAAmB,EAAE,SAAS,aAAa,cAAc,gBAAgB,eAAe,EAAE;AAC5F;AAEA,IAAM,YAA4B;AAAA,EAChC,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,IAAI;AAAA,EACJ,OAAO;AACT;AAcO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA+C;AAC7C,QAAM,eAAeF,OAAM;AAE3B,SACE,gBAAAC;AAAA,IAACF;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,eAAa,GAAG,UAAU;AAAA,MAC1B,IAAI;AAAA,QACF,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAST,eAAe,EAAE,IAAI,OAAO,IAAI,SAAS;AAAA,QACzC,UAAU,EAAE,IAAI,UAAU,IAAI,OAAO;AAAA,QACrC,WAAW,EAAE,IAAI,QAAQ,IAAI,UAAU;AAAA;AAAA,QAEvC,IAAI,EAAE,IAAI,MAAM,IAAI,EAAE;AAAA,QACtB,IAAI,EAAE,IAAI,MAAM,IAAI,EAAE;AAAA;AAAA;AAAA;AAAA,QAItB,gBAAgB;AAAA,QAChB,wBAAwB,EAAE,SAAS,OAAO;AAAA,QAC1C,cAAc;AAAA,QACd,KAAK;AAAA,QACL,UAAU,EAAE,IAAI,IAAI;AAAA,QACpB,UAAU,EAAE,IAAI,IAAI;AAAA,MACtB;AAAA,MAEC,kBAAQ,IAAI,CAAC,WAAW;AACvB,cAAM,SAAS,OAAO,OAAO;AAC7B,cAAM,WAAW,GAAG,YAAY,IAAI,OAAO,EAAE;AAC7C,eACE,gBAAAG;AAAA,UAACH;AAAA,UAAA;AAAA,YACC,WAAU;AAAA,YACV,MAAK;AAAA,YAEL,gBAAc;AAAA,YAId,iBAAe,OAAO,WAAW;AAAA,YACjC,oBAAkB,OAAO,UAAU,WAAW;AAAA,YAC9C,eAAa,GAAG,UAAU,UAAU,OAAO,EAAE;AAAA,YAC7C,SAAS,MAAM;AACb,kBAAI,CAAC,OAAO,QAAS,QAAO,MAAM;AAAA,YACpC;AAAA,YACA,IAAI,SAAS,QAAQ,OAAO,OAAO;AAAA,YAElC;AAAA,qBAAO;AAAA,cACP,OAAO,WACN,gBAAAE,KAACF,MAAA,EAAI,WAAU,QAAO,IAAI,UAAU,IAAI,WACrC,oBAAU,MAAM,GACnB;AAAA;AAAA;AAAA,UAjBG,OAAO;AAAA,QAmBd;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;;;ANKI,gBAAAI,MA8EE,QAAAC,aA9EF;AA/GJ,IAAM,iBAAiB;AAGvB,IAAM,eAAe;AAErB,IAAM,UAA0B;AAAA,EAC9B,SAAS;AAAA,EACT,eAAe;AAAA,EACf,KAAK;AAAA,EACL,GAAG;AAAA,EACH,UAAU;AAAA;AAAA;AAAA;AAAA,EAIV,CAAC,MAAM,cAAc,EAAE,GAAG;AAAA,IACxB,GAAG;AAAA,IACH,WAAW;AAAA,IACX,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,EACnB;AACF;AAEA,IAAM,UAA0B;AAAA,EAC9B,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUT,eAAe,EAAE,IAAI,kBAAkB,IAAI,MAAM;AAAA,EACjD,YAAY,EAAE,IAAI,WAAW,IAAI,aAAa;AAAA,EAC9C,KAAK;AAAA,EACL,UAAU;AACZ;AAWA,IAAM,mBAAmC;AAAA,EACvC,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,iBAAiB,EAAE,IAAI,GAAG,IAAI,MAAM;AAAA,EACpC,mBAAmB,EAAE,IAAI,OAAO,IAAI,EAAE;AAAA,EACtC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE;AAAA,EACnB,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE;AAAA,EACnB,WAAW;AACb;AAGA,IAAM,oBAAoC;AAAA,EACxC,WAAW;AAAA,EACX,aAAa;AAAA,EACb,IAAI;AACN;AAuBA,SAAS,cAAc,WAAmD;AACxE,QAAM,SAAS,cAAc,YAAY;AACzC,SAAO,SAAS,IAAI;AACtB;AAGA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQsB;AACpB,SACE,gBAAAD;AAAA,IAAC;AAAA;AAAA,MAEC,WAAW;AAAA,MACX,WAAW;AAAA,MACX,eAAc;AAAA,MACd;AAAA,MAGA,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,MAChB,OAAO,EAAE,OAAO,WAAW,MAAM,IAAI,GAAG,KAAK,WAAW,MAAM,EAAE,EAAE;AAAA,MAIlE,2BAA2B,OAAO;AAAA,MAClC,eAAe,OAAO;AAAA,MAGtB,gBAAgB;AAAA;AAAA,IAjBX,OAAO;AAAA,EAkBd;AAEJ;AAGA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AACF,GAI6B;AAC3B,MAAI,OAAO,QAAQ,WAAW,EAAG,QAAO;AACxC,SACE,gBAAAA,KAACE,MAAA,EAAI,IAAI,kBACP,0BAAAF;AAAA,IAAC;AAAA;AAAA,MACC,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO;AAAA,MACjB,QAAQ,OAAO;AAAA,MACf,WAAW,CAAC,WACV,OAAO,KAAK,QAAQ,EAAE,cAAc,gBAAgB,OAAO,MAAM,MAAM,OAAO,KAAK,CAAC;AAAA,MAEtF,OAAO,OAAO,KAAK;AAAA,MACnB;AAAA;AAAA,EACF,GACF;AAEJ;AAEO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf,cAAc;AAAA,EACd,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AACF,GAA4C;AAC1C,QAAM,SAAS,mBAAmB;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,SAAS,cAAc,cAAc;AAE3C,SACE,gBAAAC,MAAC,SAAM,WAAsB,eAAa,YAAY,IAAI,SACxD;AAAA,oBAAAA,MAACC,MAAA,EAAI,IAAI,SACP;AAAA,sBAAAF;AAAA,QAAC;AAAA;AAAA,UACC,eAAe,OAAO,KAAK;AAAA,UAC3B;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA;AAAA,MACF;AAAA,MACA,gBAAAA,KAAC,eAAY,QAAgB,cAA4B,YAAwB;AAAA,OACnF;AAAA,IACA,gBAAAA,KAACE,MAAA,EAAI,IAAI,mBACP,0BAAAF;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,QAAQ,OAAO;AAAA,QACf,UAAU,OAAO;AAAA,QACjB;AAAA,QACA,QAAQ,OAAO;AAAA,QACf,UAAU,OAAO;AAAA,QACjB;AAAA;AAAA,IACF,GACF;AAAA,KACF;AAEJ;","names":["Box","Box","useId","jsx","jsxs","jsx","jsxs","Box"]}
@@ -1,22 +1,26 @@
1
1
  import React from 'react';
2
- /** `AAAA-MM-DD` (the wire) → `dd/mm/aaaa` (what the merchant reads). */
3
- export declare function isoToBr(iso: string): string;
2
+ /** `AAAA-MM-DD` (the wire) → the mask's own order (what the reader sees). */
3
+ export declare function isoToMasked(iso: string, mask: string): string;
4
4
  /**
5
- * `dd/mm/aaaa` → `AAAA-MM-DD`, but ONLY for a day that exists. `31/02/2026`
6
- * parses fine as three numbers and is not a date, and `Date` would silently
7
- * roll it into March rather than reject it — so the round trip through UTC is
8
- * the check: if the month or day comes back changed, the input was not a day.
5
+ * The mask's order → `AAAA-MM-DD`, but ONLY for a day that exists.
6
+ *
7
+ * `31/02/2026` parses fine as three numbers and is not a date, and `Date` would
8
+ * silently roll it into March rather than reject it — so the round trip through
9
+ * UTC is the check: if the month or day comes back changed, the input was not a
10
+ * day.
9
11
  */
10
- export declare function brToIso(text: string): string;
12
+ export declare function maskedToIso(text: string, mask: string): string;
11
13
  /**
12
- * Digits only, capped at eight, with the separators put back as you type.
14
+ * Digits only, capped at the mask's width, with the separators put back as you
15
+ * type.
13
16
  *
14
17
  * Deriving the whole string from the digits — rather than editing the text in
15
- * place — is what makes backspace behave. Four digits render as `06/08`, not
16
- * `06/08/`, so deleting the first digit of the year removes the trailing
17
- * separator with it instead of leaving a dead keystroke that changes nothing.
18
+ * place — is what makes backspace behave. Four digits of `dd/mm/aaaa` render as
19
+ * `06/08`, not `06/08/`, so deleting the first digit of the year removes the
20
+ * trailing separator with it instead of leaving a dead keystroke that changes
21
+ * nothing.
18
22
  */
19
- export declare function maskBrDate(raw: string): string;
23
+ export declare function maskDate(raw: string, mask: string): string;
20
24
  /**
21
25
  * One end of a day window. The BOUND stays `AAAA-MM-DD` — the form the backend's
22
26
  * inclusive day comparison already speaks, and the form the chip label reads —
@@ -1 +1 @@
1
- {"version":3,"file":"data-views-day-input.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/DataViews/data-views-day-input.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,wEAAwE;AACxE,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG3C;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW5C;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAK9C;AAgCD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,MAAM,EACN,KAAa,EACb,WAAW,EACX,IAAI,GACL,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;OASG;IACH,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAyCpB"}
1
+ {"version":3,"file":"data-views-day-input.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/DataViews/data-views-day-input.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAmDnD,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAW7D;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAiB9D;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAY1D;AAgCD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,MAAM,EACN,KAAa,EACb,WAAW,EACX,IAAI,GACL,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;OASG;IACH,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA2CpB"}
@@ -1 +1 @@
1
- {"version":3,"file":"DateRangePicker.range.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/DateRangePicker/DateRangePicker.range.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,cAAc,EACrB,YAAY,CAAC,EAAE,MAAM,GACpB,eAAe,CAUjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,IAAI,uBAAuB,CAYzD;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAC3B,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,uBAAuB,EACjC,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,MAAM,CAKR;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,UAAU,EAAE,EACzB,OAAO,EAAE,iBAAiB,EAC1B,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,kBAAkB,EAAE,CAWtB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,kBAAkB,EAAE,EAC7B,KAAK,EAAE,cAAc,GACpB,MAAM,GAAG,SAAS,CAIpB"}
1
+ {"version":3,"file":"DateRangePicker.range.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/DateRangePicker/DateRangePicker.range.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,cAAc,EACrB,YAAY,CAAC,EAAE,MAAM,GACpB,eAAe,CAUjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,IAAI,uBAAuB,CAiBzD;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAC3B,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,uBAAuB,EACjC,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,MAAM,CAKR;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,UAAU,EAAE,EACzB,OAAO,EAAE,iBAAiB,EAC1B,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,kBAAkB,EAAE,CAWtB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,kBAAkB,EAAE,EAC7B,KAAK,EAAE,cAAc,GACpB,MAAM,GAAG,SAAS,CAIpB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/ui",
3
- "version": "6.10.1",
3
+ "version": "6.11.0",
4
4
  "license": "MIT",
5
5
  "packageManager": "pnpm@10.34.5",
6
6
  "type": "module",
@@ -31,44 +31,117 @@ import { TextField } from '@mui/material';
31
31
  import React, { useEffect, useState } from 'react';
32
32
 
33
33
 
34
- /** `AAAA-MM-DD` (the wire) → `dd/mm/aaaa` (what the merchant reads). */
35
- export function isoToBr(iso: string): string {
34
+ /**
35
+ * THE MASK IS THE ORDER, and both halves of this file read it.
36
+ *
37
+ * The placeholder a host supplies (`copy.filters.dayMask` — `dd/mm/aaaa` for
38
+ * pt-BR, `mm/dd/yyyy` for en-US) is not decoration: it tells the reader which
39
+ * segment comes first. Everything below therefore derives the segment order
40
+ * FROM that same string, so the shape on screen and the shape the parser
41
+ * expects cannot disagree.
42
+ *
43
+ * They did disagree, and the failure was silent in the worst direction. The
44
+ * mask/parse pair was hardcoded day-first (the `Br` in the old names was
45
+ * accurate) while the placeholder followed the copy pack — so under `en-US` the
46
+ * field ASKED for `mm/dd/yyyy`, and a reader who typed exactly that got a day
47
+ * of 12 and a month of 31, which is not a date, which commits nothing. No
48
+ * error, no filter, a full list that reads like an answer. The en-US pack's own
49
+ * comment already said the mask "is the shape that gets a date entered
50
+ * backwards"; nothing enforced it.
51
+ */
52
+
53
+ /** What one segment of a mask is, and how many digits it takes. */
54
+ type DaySegment = 'year' | 'month' | 'day';
55
+ const SEGMENT_WIDTH: Readonly<Record<DaySegment, number>> = { year: 4, month: 2, day: 2 };
56
+
57
+ /**
58
+ * The segments a mask asks for, in its own order.
59
+ *
60
+ * Keyed on each token's FIRST letter so one rule covers every spelling a host
61
+ * might use — `aaaa`/`yyyy` for the year, `mm` for the month, `dd`/`tt` for the
62
+ * day. An unreadable mask falls back to day-first rather than throwing: this
63
+ * runs on every keystroke, and a field that renders is recoverable where one
64
+ * that crashes the grid is not.
65
+ */
66
+ function segmentsOf(mask: string): readonly DaySegment[] {
67
+ const tokens = mask.split(/[^A-Za-z]+/).filter(Boolean);
68
+ if (tokens.length !== 3) return ['day', 'month', 'year'];
69
+ const segments = tokens.map((token): DaySegment => {
70
+ const head = token.slice(0, 1).toLowerCase();
71
+ if (head === 'y' || head === 'a') return 'year';
72
+ return head === 'm' ? 'month' : 'day';
73
+ });
74
+ // A mask naming the same segment twice describes no date. Fall back rather
75
+ // than build a parser that can never succeed.
76
+ return new Set(segments).size === 3 ? segments : ['day', 'month', 'year'];
77
+ }
78
+
79
+ /** The separator the mask uses, defaulting to `/`. */
80
+ const separatorOf = (mask: string): string => mask.replace(/[A-Za-z]/g, '').slice(0, 1) || '/';
81
+
82
+ /** `AAAA-MM-DD` (the wire) → the mask's own order (what the reader sees). */
83
+ export function isoToMasked(iso: string, mask: string): string {
36
84
  const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
37
- return parts ? `${parts[3]}/${parts[2]}/${parts[1]}` : '';
85
+ if (!parts) return '';
86
+ const value: Record<DaySegment, string> = {
87
+ year: parts[1] as string,
88
+ month: parts[2] as string,
89
+ day: parts[3] as string,
90
+ };
91
+ return segmentsOf(mask)
92
+ .map((segment) => value[segment])
93
+ .join(separatorOf(mask));
38
94
  }
39
95
 
40
96
  /**
41
- * `dd/mm/aaaa` → `AAAA-MM-DD`, but ONLY for a day that exists. `31/02/2026`
42
- * parses fine as three numbers and is not a date, and `Date` would silently
43
- * roll it into March rather than reject it — so the round trip through UTC is
44
- * the check: if the month or day comes back changed, the input was not a day.
97
+ * The mask's order → `AAAA-MM-DD`, but ONLY for a day that exists.
98
+ *
99
+ * `31/02/2026` parses fine as three numbers and is not a date, and `Date` would
100
+ * silently roll it into March rather than reject it — so the round trip through
101
+ * UTC is the check: if the month or day comes back changed, the input was not a
102
+ * day.
45
103
  */
46
- export function brToIso(text: string): string {
47
- const parts = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(text);
48
- if (!parts) return '';
49
- const [, dd, mm, yyyy] = parts;
50
- const day = Number(dd);
51
- const month = Number(mm);
52
- const year = Number(yyyy);
104
+ export function maskedToIso(text: string, mask: string): string {
105
+ const segments = segmentsOf(mask);
106
+ const digits = text.replace(/\D/g, '');
107
+ if (digits.length !== 8) return '';
108
+ const value = {} as Record<DaySegment, string>;
109
+ let cursor = 0;
110
+ for (const segment of segments) {
111
+ value[segment] = digits.slice(cursor, cursor + SEGMENT_WIDTH[segment]);
112
+ cursor += SEGMENT_WIDTH[segment];
113
+ }
114
+ const year = Number(value.year);
115
+ const month = Number(value.month);
116
+ const day = Number(value.day);
53
117
  if (year < 1000) return '';
54
118
  const probe = new Date(Date.UTC(year, month - 1, day));
55
119
  if (probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) return '';
56
- return `${yyyy}-${mm}-${dd}`;
120
+ return `${value.year}-${value.month}-${value.day}`;
57
121
  }
58
122
 
59
123
  /**
60
- * Digits only, capped at eight, with the separators put back as you type.
124
+ * Digits only, capped at the mask's width, with the separators put back as you
125
+ * type.
61
126
  *
62
127
  * Deriving the whole string from the digits — rather than editing the text in
63
- * place — is what makes backspace behave. Four digits render as `06/08`, not
64
- * `06/08/`, so deleting the first digit of the year removes the trailing
65
- * separator with it instead of leaving a dead keystroke that changes nothing.
128
+ * place — is what makes backspace behave. Four digits of `dd/mm/aaaa` render as
129
+ * `06/08`, not `06/08/`, so deleting the first digit of the year removes the
130
+ * trailing separator with it instead of leaving a dead keystroke that changes
131
+ * nothing.
66
132
  */
67
- export function maskBrDate(raw: string): string {
133
+ export function maskDate(raw: string, mask: string): string {
134
+ const segments = segmentsOf(mask);
135
+ const separator = separatorOf(mask);
68
136
  const digits = raw.replace(/\D/g, '').slice(0, 8);
69
- if (digits.length <= 2) return digits;
70
- if (digits.length <= 4) return `${digits.slice(0, 2)}/${digits.slice(2)}`;
71
- return `${digits.slice(0, 2)}/${digits.slice(2, 4)}/${digits.slice(4)}`;
137
+ const out: string[] = [];
138
+ let cursor = 0;
139
+ for (const segment of segments) {
140
+ if (cursor >= digits.length) break;
141
+ out.push(digits.slice(cursor, cursor + SEGMENT_WIDTH[segment]));
142
+ cursor += SEGMENT_WIDTH[segment];
143
+ }
144
+ return out.join(separator);
72
145
  }
73
146
 
74
147
  /**
@@ -142,7 +215,7 @@ export function DayBoundInput({
142
215
  mask: string;
143
216
  }): React.JSX.Element {
144
217
  const applied = value ?? '';
145
- const [text, setText] = useState(() => isoToBr(applied));
218
+ const [text, setText] = useState(() => isoToMasked(applied, mask));
146
219
 
147
220
  // The bound is the APPLIED filter; `text` is what is being typed. Re-sync only
148
221
  // when the applied value moved somewhere the text does not already mean — a
@@ -150,8 +223,10 @@ export function DayBoundInput({
150
223
  // guard the field's own write bounces straight back mid-edit and resets the
151
224
  // caret, which is the native control's worst habit reproduced by hand.
152
225
  useEffect(() => {
153
- setText((current) => (brToIso(current) === applied ? current : isoToBr(applied)));
154
- }, [applied]);
226
+ setText((current) =>
227
+ maskedToIso(current, mask) === applied ? current : isoToMasked(applied, mask),
228
+ );
229
+ }, [applied, mask]);
155
230
 
156
231
  return (
157
232
  <TextField
@@ -162,19 +237,19 @@ export function DayBoundInput({
162
237
  placeholder={mask}
163
238
  value={text}
164
239
  onChange={(event) => {
165
- const next = maskBrDate(event.target.value);
240
+ const next = maskDate(event.target.value, mask);
166
241
  setText(next);
167
242
  // Emptying clears the bound. A COMPLETE day applies it. A partial one
168
243
  // does NEITHER — it leaves whatever is applied alone, so typing is never
169
244
  // fought and a half-typed year never reaches the backend.
170
245
  if (next === '') return onChange(undefined);
171
- const iso = brToIso(next);
246
+ const iso = maskedToIso(next, mask);
172
247
  if (iso) onChange(iso);
173
248
  }}
174
249
  // Leaving a half-typed (or impossible) date on screen would show a filter
175
250
  // the list is not actually using. On blur the field snaps back to what IS
176
251
  // applied; a complete date already equals it, so this only ever tidies.
177
- onBlur={() => setText(isoToBr(applied))}
252
+ onBlur={() => setText(isoToMasked(applied, mask))}
178
253
  // The placeholder occupies the space an un-shrunk label would render into.
179
254
  InputLabelProps={{ shrink: true }}
180
255
  inputProps={dayInputProps(testId, describedBy)}
@@ -7,7 +7,7 @@
7
7
  * indistinguishable from one that ignored you. The caller reads
8
8
  * {@link DateRangeStatus} and keeps its own confirm button shut.
9
9
  */
10
- import { isoToBr } from '../../data-display/DataViews/data-views-day-input';
10
+ import { isoToMasked } from '../../data-display/DataViews/data-views-day-input';
11
11
 
12
12
  import { dayCount, isAfterDay } from './DateRangePicker.dates';
13
13
  import type {
@@ -53,6 +53,11 @@ export function resolveDayRange(
53
53
  * moves both halves of the decision to the same place.
54
54
  */
55
55
  export function defaultMessages(): DateRangePickerMessages {
56
+ // ONE mask, read by both the fields and the summary below. They are the same
57
+ // date in the same control, so a summary formatted in a different order from
58
+ // the boxes it summarises is a control arguing with itself — which is what
59
+ // two independent literals eventually become.
60
+ const dayMask = 'dd/mm/yyyy';
56
61
  return {
57
62
  from: 'Start date',
58
63
  to: 'End date',
@@ -60,9 +65,9 @@ export function defaultMessages(): DateRangePickerMessages {
60
65
  incomplete: 'Choose both dates.',
61
66
  reversed: 'The end date must be on or after the start date.',
62
67
  overMax: ({ maxRangeDays }) => `Choose a range of at most ${maxRangeDays} days.`,
63
- summary: ({ from, to }) => `${isoToBr(from)} – ${isoToBr(to)}`,
68
+ summary: ({ from, to }) => `${isoToMasked(from, dayMask)} – ${isoToMasked(to, dayMask)}`,
64
69
  calendarLabel: 'Calendar',
65
- dayMask: 'dd/mm/yyyy',
70
+ dayMask,
66
71
  };
67
72
  }
68
73
 
@@ -1,78 +0,0 @@
1
- // src/components/data-display/DataViews/data-views-day-input.tsx
2
- import { TextField } from "@mui/material";
3
- import { useEffect, useState } from "react";
4
- import { jsx } from "react/jsx-runtime";
5
- function isoToBr(iso) {
6
- const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
7
- return parts ? `${parts[3]}/${parts[2]}/${parts[1]}` : "";
8
- }
9
- function brToIso(text) {
10
- const parts = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(text);
11
- if (!parts) return "";
12
- const [, dd, mm, yyyy] = parts;
13
- const day = Number(dd);
14
- const month = Number(mm);
15
- const year = Number(yyyy);
16
- if (year < 1e3) return "";
17
- const probe = new Date(Date.UTC(year, month - 1, day));
18
- if (probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) return "";
19
- return `${yyyy}-${mm}-${dd}`;
20
- }
21
- function maskBrDate(raw) {
22
- const digits = raw.replace(/\D/g, "").slice(0, 8);
23
- if (digits.length <= 2) return digits;
24
- if (digits.length <= 4) return `${digits.slice(0, 2)}/${digits.slice(2)}`;
25
- return `${digits.slice(0, 2)}/${digits.slice(2, 4)}/${digits.slice(4)}`;
26
- }
27
- function dayInputProps(testId, describedBy) {
28
- return {
29
- "data-testid": testId,
30
- maxLength: 10,
31
- inputMode: "numeric",
32
- autoComplete: "off",
33
- ...describedBy ? { "aria-describedby": describedBy } : {}
34
- };
35
- }
36
- function DayBoundInput({
37
- label,
38
- value,
39
- onChange,
40
- testId,
41
- error = false,
42
- describedBy,
43
- mask
44
- }) {
45
- const applied = value ?? "";
46
- const [text, setText] = useState(() => isoToBr(applied));
47
- useEffect(() => {
48
- setText((current) => brToIso(current) === applied ? current : isoToBr(applied));
49
- }, [applied]);
50
- return /* @__PURE__ */ jsx(
51
- TextField,
52
- {
53
- size: "small",
54
- type: "text",
55
- label,
56
- error,
57
- placeholder: mask,
58
- value: text,
59
- onChange: (event) => {
60
- const next = maskBrDate(event.target.value);
61
- setText(next);
62
- if (next === "") return onChange(void 0);
63
- const iso = brToIso(next);
64
- if (iso) onChange(iso);
65
- },
66
- onBlur: () => setText(isoToBr(applied)),
67
- InputLabelProps: { shrink: true },
68
- inputProps: dayInputProps(testId, describedBy),
69
- sx: { width: 165 }
70
- }
71
- );
72
- }
73
-
74
- export {
75
- isoToBr,
76
- DayBoundInput
77
- };
78
- //# sourceMappingURL=chunk-TSG5C7MP.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/data-display/DataViews/data-views-day-input.tsx"],"sourcesContent":["'use client';\n\n/**\n * A day bound as a MASKED `dd/mm/aaaa` text field, replacing `<input type=\"date\">`.\n *\n * The native control is three separate segments wearing one box. Typing a date\n * means filling a segment, being jumped to the next, and starting over — and\n * which segment comes first is the BROWSER's locale, not ours, so the same\n * field reads `dd/mm/aaaa` for one merchant and `mm/dd/yyyy` for the next with\n * nothing in our code saying which. There is no way to type a date as one\n * continuous run of digits, which is how people actually type dates.\n *\n * A mask gives that back: eight digits straight through, separators inserted as\n * you go, backspace walking back through them.\n *\n * Measured, driving both controls in Chromium with the same eight keystrokes\n * (`01072026`, one `keydown` at a time):\n *\n * native → field ends EMPTY, no filter applied, 0 requests\n * masked → field reads `01/07/2026`, filter applied, 1 request\n *\n * The native result is the whole complaint: you type a date and nothing lands.\n *\n * The second half of the fix is WHEN the bound is written. A partially-typed\n * year is itself a valid date — `2026` passes through `0002`, `0020`, `0202` on\n * the way — so committing on every keystroke would send the grid off to fetch\n * years nobody meant. A bound is written only once the text is a WHOLE, real\n * date, which is what keeps the run above at one request rather than four.\n */\nimport { TextField } from '@mui/material';\nimport React, { useEffect, useState } from 'react';\n\n\n/** `AAAA-MM-DD` (the wire) → `dd/mm/aaaa` (what the merchant reads). */\nexport function isoToBr(iso: string): string {\n const parts = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(iso);\n return parts ? `${parts[3]}/${parts[2]}/${parts[1]}` : '';\n}\n\n/**\n * `dd/mm/aaaa` → `AAAA-MM-DD`, but ONLY for a day that exists. `31/02/2026`\n * parses fine as three numbers and is not a date, and `Date` would silently\n * roll it into March rather than reject it — so the round trip through UTC is\n * the check: if the month or day comes back changed, the input was not a day.\n */\nexport function brToIso(text: string): string {\n const parts = /^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/.exec(text);\n if (!parts) return '';\n const [, dd, mm, yyyy] = parts;\n const day = Number(dd);\n const month = Number(mm);\n const year = Number(yyyy);\n if (year < 1000) return '';\n const probe = new Date(Date.UTC(year, month - 1, day));\n if (probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) return '';\n return `${yyyy}-${mm}-${dd}`;\n}\n\n/**\n * Digits only, capped at eight, with the separators put back as you type.\n *\n * Deriving the whole string from the digits — rather than editing the text in\n * place — is what makes backspace behave. Four digits render as `06/08`, not\n * `06/08/`, so deleting the first digit of the year removes the trailing\n * separator with it instead of leaving a dead keystroke that changes nothing.\n */\nexport function maskBrDate(raw: string): string {\n const digits = raw.replace(/\\D/g, '').slice(0, 8);\n if (digits.length <= 2) return digits;\n if (digits.length <= 4) return `${digits.slice(0, 2)}/${digits.slice(2)}`;\n return `${digits.slice(0, 2)}/${digits.slice(2, 4)}/${digits.slice(4)}`;\n}\n\n/**\n * What has to land on the `<input>` itself rather than on the `TextField`.\n *\n * Anything `TextField` does not recognise is spread onto its root `FormControl`\n * div. `inputMode` went up there once: the attribute sat on a non-editable\n * wrapper, meant nothing, and the phone went on offering the letter keyboard\n * for a field that only takes digits (the `dd/mm/aaaa` hint under a QWERTY\n * layout). Only the `<input>` this reaches decides which keypad opens — and the\n * same is true of `aria-describedby`, which on the wrapper points at a\n * description no screen reader will read out for the field.\n *\n * `numeric` and not `tel`: both raise a keypad, but the phone one is a dialler\n * with `+ * #` on it — keys that cannot occur in a date. A paste carrying\n * separators still lands, because the MASK is what filters the text; the keypad\n * only narrows what a thumb can reach for.\n *\n * Autofill off for the same reason: the browser's saved-value strip covers the\n * keypad to suggest names and addresses, none of which are a date, and none of\n * which the mask would keep.\n */\nfunction dayInputProps(testId: string, describedBy: string | undefined) {\n return {\n 'data-testid': testId,\n maxLength: 10,\n inputMode: 'numeric' as const,\n autoComplete: 'off',\n ...(describedBy ? { 'aria-describedby': describedBy } : {}),\n };\n}\n\n/**\n * One end of a day window. The BOUND stays `AAAA-MM-DD` — the form the backend's\n * inclusive day comparison already speaks, and the form the chip label reads —\n * so only the typing surface changes.\n */\nexport function DayBoundInput({\n label,\n value,\n onChange,\n testId,\n error = false,\n describedBy,\n mask,\n}: {\n label: string;\n /** The applied bound, `AAAA-MM-DD`, or undefined when this end is open. */\n value: string | undefined;\n onChange: (bound: string | undefined) => void;\n testId: string;\n /**\n * Paint the field as rejected. It only ever writes a real day by itself, so\n * this is for a caller judging the PAIR: a window whose end precedes its\n * start is two valid days and one impossible period, and the field it was\n * typed into is the only place to say which half is being complained about.\n */\n error?: boolean;\n /** Id of the element carrying that complaint, so it is read WITH the field. */\n describedBy?: string;\n /**\n * The day mask, which IS the placeholder — so the expected ORDER is on\n * screen before the first keystroke rather than discovered by getting it\n * wrong. `dd/mm/aaaa` is one locale's order and one locale's letters.\n *\n * A PROP rather than a context read: this field has two callers, and only\n * one of them is inside DataViews. `DateRangePicker` mounts it standalone\n * with its own message table, so a context read here throws for every host\n * of that picker — which is exactly what it did.\n */\n mask: string;\n}): React.JSX.Element {\n const applied = value ?? '';\n const [text, setText] = useState(() => isoToBr(applied));\n\n // The bound is the APPLIED filter; `text` is what is being typed. Re-sync only\n // when the applied value moved somewhere the text does not already mean — a\n // \"Limpar\", a saved view, the other pill resetting the pair. Without that\n // guard the field's own write bounces straight back mid-edit and resets the\n // caret, which is the native control's worst habit reproduced by hand.\n useEffect(() => {\n setText((current) => (brToIso(current) === applied ? current : isoToBr(applied)));\n }, [applied]);\n\n return (\n <TextField\n size=\"small\"\n type=\"text\"\n label={label}\n error={error}\n placeholder={mask}\n value={text}\n onChange={(event) => {\n const next = maskBrDate(event.target.value);\n setText(next);\n // Emptying clears the bound. A COMPLETE day applies it. A partial one\n // does NEITHER — it leaves whatever is applied alone, so typing is never\n // fought and a half-typed year never reaches the backend.\n if (next === '') return onChange(undefined);\n const iso = brToIso(next);\n if (iso) onChange(iso);\n }}\n // Leaving a half-typed (or impossible) date on screen would show a filter\n // the list is not actually using. On blur the field snaps back to what IS\n // applied; a complete date already equals it, so this only ever tidies.\n onBlur={() => setText(isoToBr(applied))}\n // The placeholder occupies the space an un-shrunk label would render into.\n InputLabelProps={{ shrink: true }}\n inputProps={dayInputProps(testId, describedBy)}\n sx={{ width: 165 }}\n />\n );\n}\n"],"mappings":";AA6BA,SAAS,iBAAiB;AAC1B,SAAgB,WAAW,gBAAgB;AA8HvC;AA1HG,SAAS,QAAQ,KAAqB;AAC3C,QAAM,QAAQ,4BAA4B,KAAK,GAAG;AAClD,SAAO,QAAQ,GAAG,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,KAAK;AACzD;AAQO,SAAS,QAAQ,MAAsB;AAC5C,QAAM,QAAQ,8BAA8B,KAAK,IAAI;AACrD,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,CAAC,EAAE,IAAI,IAAI,IAAI,IAAI;AACzB,QAAM,MAAM,OAAO,EAAE;AACrB,QAAM,QAAQ,OAAO,EAAE;AACvB,QAAM,OAAO,OAAO,IAAI;AACxB,MAAI,OAAO,IAAM,QAAO;AACxB,QAAM,QAAQ,IAAI,KAAK,KAAK,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;AACrD,MAAI,MAAM,YAAY,MAAM,QAAQ,KAAK,MAAM,WAAW,MAAM,IAAK,QAAO;AAC5E,SAAO,GAAG,IAAI,IAAI,EAAE,IAAI,EAAE;AAC5B;AAUO,SAAS,WAAW,KAAqB;AAC9C,QAAM,SAAS,IAAI,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG,CAAC;AAChD,MAAI,OAAO,UAAU,EAAG,QAAO;AAC/B,MAAI,OAAO,UAAU,EAAG,QAAO,GAAG,OAAO,MAAM,GAAG,CAAC,CAAC,IAAI,OAAO,MAAM,CAAC,CAAC;AACvE,SAAO,GAAG,OAAO,MAAM,GAAG,CAAC,CAAC,IAAI,OAAO,MAAM,GAAG,CAAC,CAAC,IAAI,OAAO,MAAM,CAAC,CAAC;AACvE;AAsBA,SAAS,cAAc,QAAgB,aAAiC;AACtE,SAAO;AAAA,IACL,eAAe;AAAA,IACf,WAAW;AAAA,IACX,WAAW;AAAA,IACX,cAAc;AAAA,IACd,GAAI,cAAc,EAAE,oBAAoB,YAAY,IAAI,CAAC;AAAA,EAC3D;AACF;AAOO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AACF,GA0BsB;AACpB,QAAM,UAAU,SAAS;AACzB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,MAAM,QAAQ,OAAO,CAAC;AAOvD,YAAU,MAAM;AACd,YAAQ,CAAC,YAAa,QAAQ,OAAO,MAAM,UAAU,UAAU,QAAQ,OAAO,CAAE;AAAA,EAClF,GAAG,CAAC,OAAO,CAAC;AAEZ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,MAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA,aAAa;AAAA,MACb,OAAO;AAAA,MACP,UAAU,CAAC,UAAU;AACnB,cAAM,OAAO,WAAW,MAAM,OAAO,KAAK;AAC1C,gBAAQ,IAAI;AAIZ,YAAI,SAAS,GAAI,QAAO,SAAS,MAAS;AAC1C,cAAM,MAAM,QAAQ,IAAI;AACxB,YAAI,IAAK,UAAS,GAAG;AAAA,MACvB;AAAA,MAIA,QAAQ,MAAM,QAAQ,QAAQ,OAAO,CAAC;AAAA,MAEtC,iBAAiB,EAAE,QAAQ,KAAK;AAAA,MAChC,YAAY,cAAc,QAAQ,WAAW;AAAA,MAC7C,IAAI,EAAE,OAAO,IAAI;AAAA;AAAA,EACnB;AAEJ;","names":[]}