@danxbot/ui 5.2.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +11269 -11057
- package/dist/index.js.map +1 -1
- package/dist/pure.js +163 -150
- package/dist/pure.js.map +1 -1
- package/dist/types/components/InlineEdit.d.ts +46 -115
- package/dist/types/components/MarkdownEditor.d.ts +22 -2
- package/dist/types/components/Progress.d.ts +24 -0
- package/dist/types/components/ReadMore.d.ts +76 -0
- package/dist/types/components/provenance/ProvenanceDetail.d.ts +9 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/lib/format.d.ts +27 -7
- package/dist/types/pure.d.ts +1 -1
- package/package.json +2 -2
- /package/dist/types/{components/code-editor/state.d.ts → lib/use-controllable.d.ts} +0 -0
package/dist/pure.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pure.js","sources":["../src/lib/format.ts","../src/lib/time/types.ts","../src/lib/time/plain.ts","../src/lib/values/date.ts","../src/lib/time/zone.ts","../src/lib/values/datetime.ts","../src/lib/values/currency.ts","../src/lib/values/phone.ts","../src/lib/values/number.ts","../src/lib/values/boolean.ts"],"sourcesContent":["/* ----------------------------------------------------------------------------\n NUMBERS, TURNED INTO THE STRINGS PEOPLE READ.\n\n These lived inside the components that needed them first — `formatBytes` in\n `chat/Attachment.tsx`, `formatTokens` in `chat/ContextMeter.tsx` — which was\n fine right up until somebody outside a component wanted one. The package\n exports a single entry, so importing `formatBytes` into an application's own\n formatting layer dragged React and every component in this library into a\n layer that is deliberately framework-free.\n\n They are here because they are DECISIONS, not utilities: how much precision\n a reader can act on, and where a rounded figure stops telling the truth. Two\n places rendering the same quantity to different precision is the defect this\n file prevents — see each function for the call it makes.\n\n NOTHING IN THIS FILE MAY IMPORT REACT, directly or transitively. It is the\n whole content of the `@danxbot/ui/pure` entry; `src/pure.ts` states the\n contract and `pure-entry.test.ts` enforces it against both the source graph\n and the built artifact.\n -------------------------------------------------------------------------- */\n\n/** Binary units, because these are file sizes and every OS reports them this way. */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return \"\";\n if (bytes < 1024) return `${bytes} B`;\n const units = [\"KB\", \"MB\", \"GB\", \"TB\"];\n let value = bytes / 1024;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit += 1;\n }\n /* One decimal below 10 and none above: \"1.4 MB\" is useful precision,\n \"847.3 KB\" is noise on a number nobody acts on. */\n return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;\n}\n\n/**\n * 241_600 → \"241.6k\".\n *\n * One decimal all the way to 1M, and exact below 1k. Unlike a file size — where\n * a tenth of a kilobyte is noise on a number nobody acts on — the tenth here is\n * the digit that moves while you work: at 1M capacity, a whole percent is\n * 10_000 tokens, so rounding to \"242k\" holds a figure still for several\n * exchanges and makes a live meter look frozen.\n */\nexport function formatTokens(tokens: number): string {\n if (!Number.isFinite(tokens)) return \"—\";\n if (tokens < 1_000) return String(Math.round(tokens));\n if (tokens < 1_000_000) return `${Number((tokens / 1_000).toFixed(1))}k`;\n return `${Number((tokens / 1_000_000).toFixed(1))}M`;\n}\n","/* ============================================================================\n THE TIME LAYER'S VOCABULARY — deliberately `Temporal`'s, minus what we do\n not need.\n\n WHY WE HAVE OUR OWN AT ALL. `Temporal` is the right answer and it is not\n available: Chrome 144+, Firefox 139+, Node 26+, and **Safari has not shipped\n it in any stable version, desktop or iOS**. Every iOS browser is WebKit, so\n \"modern browsers only\" does not rescue us — global support is ~69% and the\n entire gap is Apple. The polyfill is 20.4 kB gzip and is a real option; it is\n priced on the tracker (CAL-TIME) so reversing this decision is one sentence.\n\n WHY THE SHAPES MIRROR `Temporal` ANYWAY. The day Safari ships it, the swap\n should be mechanical rather than a rewrite. So `PlainDate` has `year` /\n `month` / `day` with a ONE-BASED month, `PlainDateTime` extends it,\n `Instant` is a fixed point on the timeline, and the disambiguation modes are\n spelled exactly as `Temporal.ZonedDateTime.from` spells them. Anywhere a name\n here differs from `Temporal`'s, that is a bug in this file.\n\n WHAT IS DELIBERATELY ABSENT: calendars other than ISO 8601, nanosecond\n precision, `Temporal.Duration`'s full field set, and any notion of a\n `ZonedDateTime` object. A calendar needs an instant, a wall-clock reading, a\n calendar day, and a way to move between them; carrying more would be carrying\n a date library we did not choose to write.\n ========================================================================= */\n\n/**\n * A point on the timeline, as milliseconds since the epoch.\n *\n * NOT a `Date`. A `Date` is a mutable object that prints itself in the host's\n * local zone, and every calendar bug in the survey behind this component\n * involved someone reading a `Date`'s local fields when they meant a specific\n * zone's. A number cannot be misread that way — it has no fields to read.\n */\nexport type Instant = number;\n\n/** An IANA time zone identifier, e.g. `\"America/New_York\"`. */\nexport type TimeZone = string;\n\n/**\n * A calendar day. No time, no zone, no offset — just which square on the wall.\n *\n * `month` IS ONE-BASED, matching `Temporal` and matching how every human writes\n * a date. `Date`'s zero-based month is the single most common off-by-one in\n * date code and it is not reproduced here.\n */\nexport interface PlainDate {\n readonly year: number;\n /** 1–12. */\n readonly month: number;\n /** 1–31. */\n readonly day: number;\n}\n\n/**\n * A wall-clock reading. What a clock on the wall says, with no claim about\n * which wall.\n *\n * This is the type that makes the ambiguity explicit rather than accidental:\n * `2026-11-01T01:30` in New York happened TWICE, and `2026-03-08T02:30` never\n * happened at all. A `PlainDateTime` can represent both, which is precisely why\n * turning one into an `Instant` requires saying what to do about it.\n */\nexport interface PlainDateTime extends PlainDate {\n /** 0–23. */\n readonly hour: number;\n /** 0–59. */\n readonly minute: number;\n /** 0–59. */\n readonly second: number;\n /** 0–999. */\n readonly millisecond: number;\n}\n\n/**\n * How much to move, in CALENDAR units rather than elapsed time.\n *\n * THE DISTINCTION IS THE WHOLE POINT, and getting it wrong is the bug in every\n * library surveyed. \"One day later\" is not \"86,400,000 ms later\": across a\n * spring-forward boundary it is 23 hours, and across a fall-back it is 25. A\n * user dragging an event from Saturday to Sunday expects it to keep its\n * wall-clock time, so the move has to be expressed as `{ days: 1 }` and\n * resolved against the zone — never added as a number.\n *\n * `hours` and `minutes` ARE exact elapsed time, and that is correct: dragging\n * an event down by two hours means two hours of real duration.\n */\nexport interface CalendarDuration {\n readonly days?: number;\n readonly hours?: number;\n readonly minutes?: number;\n}\n\n/**\n * What to do when a wall-clock reading does not name exactly one instant.\n *\n * Spelled as `Temporal.ZonedDateTime.from`'s option of the same name, with the\n * same meanings, so this is one more thing that becomes a deletion later.\n *\n * - `compatible` — what every other date library does, and the default.\n * In a GAP (spring forward) move forward by the size of the gap; in an\n * OVERLAP (fall back) take the first of the two.\n * - `earlier` / `later` — take that side explicitly.\n * - `reject` — throw. For a parser or an import, where a time that never\n * existed is a fact about bad input rather than something to paper over.\n */\nexport type Disambiguation = \"compatible\" | \"earlier\" | \"later\" | \"reject\";\n\n/** Thrown by `reject` disambiguation, and never by anything else. */\nexport class AmbiguousTimeError extends Error {\n constructor(\n readonly wall: PlainDateTime,\n readonly zone: TimeZone,\n readonly kind: \"gap\" | \"overlap\",\n ) {\n super(\n kind === \"gap\"\n ? `${isoDateTimeString(wall)} does not exist in ${zone} — the clock skipped it.`\n : `${isoDateTimeString(wall)} happens twice in ${zone}.`,\n );\n this.name = \"AmbiguousTimeError\";\n }\n}\n\n/** `2026-03-08T02:30:00.000` — used in errors, and by the ISO helpers. */\nexport function isoDateTimeString(wall: PlainDateTime): string {\n const p = (n: number, w = 2) => String(n).padStart(w, \"0\");\n return (\n `${p(wall.year, 4)}-${p(wall.month)}-${p(wall.day)}` +\n `T${p(wall.hour)}:${p(wall.minute)}:${p(wall.second)}.${p(wall.millisecond, 3)}`\n );\n}\n\n/** `2026-03-08`. */\nexport function isoDateString(date: PlainDate): string {\n const p = (n: number, w = 2) => String(n).padStart(w, \"0\");\n return `${p(date.year, 4)}-${p(date.month)}-${p(date.day)}`;\n}\n","import { isoDateString, type PlainDate, type PlainDateTime } from \"./types\";\n\n/* ============================================================================\n CALENDAR ARITHMETIC — no zones, no instants, no offsets.\n\n A month grid is a question about the calendar, not about time: which squares\n does September have, which weekday does the 1st fall on, how many rows. None\n of that depends on a zone, and none of it should reach for one. Keeping this\n file zone-free is what stops a month grid quietly acquiring a dependency on\n the viewer's location.\n\n Every function here goes through UTC internally — NOT because these values\n are UTC (they are not; they have no zone at all) but because UTC is the only\n mode of `Date` with no transitions in it, so the arithmetic cannot be\n disturbed by one. A local-mode `Date` doing the same work would lose an hour\n twice a year and, at midnight-transition zones, land on the wrong DAY.\n ========================================================================= */\n\n/** Days in `month` of `year`, leap years included. */\nexport function daysInMonth(year: number, month: number): number {\n const d = new Date(0);\n d.setUTCFullYear(year, month, 0);\n return d.getUTCDate();\n}\n\nfunction fromUtc(d: Date): PlainDate {\n return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };\n}\n\nfunction toUtc(date: PlainDate): Date {\n const d = new Date(0);\n d.setUTCFullYear(date.year, date.month - 1, date.day);\n return d;\n}\n\n/** `date` moved by `days`, which may be negative, rolling months and years. */\nexport function addDays(date: PlainDate, days: number): PlainDate {\n const d = toUtc(date);\n d.setUTCDate(d.getUTCDate() + days);\n return fromUtc(d);\n}\n\n/**\n * `date` moved by `months`, clamping the day to the target month's length.\n *\n * CLAMPING, NOT ROLLING: one month after the 31st of January is the 28th or\n * 29th of February, never the 3rd of March. Rolling is what `Date.setMonth`\n * does natively and it is why \"next month\" buttons skip February.\n */\nexport function addMonths(date: PlainDate, months: number): PlainDate {\n const total = date.year * 12 + (date.month - 1) + months;\n const year = Math.floor(total / 12);\n const month = (total % 12) + 1;\n return { year, month, day: Math.min(date.day, daysInMonth(year, month)) };\n}\n\n/** Whole days from `a` to `b`; negative when `b` is earlier. */\nexport function daysBetween(a: PlainDate, b: PlainDate): number {\n return Math.round((toUtc(b).getTime() - toUtc(a).getTime()) / 86_400_000);\n}\n\n/** Negative when `a` is earlier, 0 when equal, positive when later. */\nexport function comparePlainDate(a: PlainDate, b: PlainDate): number {\n if (a.year !== b.year) return a.year - b.year;\n if (a.month !== b.month) return a.month - b.month;\n return a.day - b.day;\n}\n\nexport function isSameDate(a: PlainDate, b: PlainDate): boolean {\n return comparePlainDate(a, b) === 0;\n}\n\n/** 0 = Sunday … 6 = Saturday, matching `Date.getUTCDay` and `Intl`'s weekday order. */\nexport function dayOfWeek(date: PlainDate): number {\n return toUtc(date).getUTCDay();\n}\n\n/**\n * The first day of the week `date` falls in.\n *\n * `weekStartsOn` IS A PARAMETER WITH NO DEFAULT ANYWHERE UP THE STACK, because\n * there is no correct global answer: Sunday in the US and Japan, Monday across\n * Europe and by ISO 8601, Saturday in much of the Middle East. A component that\n * picks one silently is wrong for most of the world, so the calendar surfaces\n * it and this function simply obeys.\n */\nexport function startOfWeek(date: PlainDate, weekStartsOn: number): PlainDate {\n const shift = (dayOfWeek(date) - weekStartsOn + 7) % 7;\n return addDays(date, -shift);\n}\n\n/** The 1st of `date`'s month. */\nexport function startOfMonth(date: PlainDate): PlainDate {\n return { year: date.year, month: date.month, day: 1 };\n}\n\n/** The last day of `date`'s month. */\nexport function endOfMonth(date: PlainDate): PlainDate {\n return { year: date.year, month: date.month, day: daysInMonth(date.year, date.month) };\n}\n\n/**\n * Every day a month grid shows, INCLUDING the leading and trailing days that\n * belong to the neighbouring months.\n *\n * Always whole weeks, so the result is a multiple of seven and the grid never\n * has a ragged edge. The count varies — a 28-day February beginning on the\n * week's first day needs four rows, and a 31-day month beginning on its last\n * day needs six. Callers that assume six rows draw an empty one eleven months\n * a year; callers that assume five are simply wrong twice a year.\n */\nexport function monthGridDays(date: PlainDate, weekStartsOn: number): PlainDate[] {\n const first = startOfWeek(startOfMonth(date), weekStartsOn);\n const lastOfMonth = endOfMonth(date);\n const last = addDays(startOfWeek(lastOfMonth, weekStartsOn), 6);\n const out: PlainDate[] = [];\n for (let d = first; comparePlainDate(d, last) <= 0; d = addDays(d, 1)) out.push(d);\n return out;\n}\n\n/** The seven days of the week `date` falls in. */\nexport function weekDays(date: PlainDate, weekStartsOn: number): PlainDate[] {\n const first = startOfWeek(date, weekStartsOn);\n return Array.from({ length: 7 }, (_, i) => addDays(first, i));\n}\n\n/**\n * Parse `YYYY-MM-DD`.\n *\n * FAILS LOUDLY on anything else, including the shapes `Date` accepts and\n * silently reinterprets. `new Date(\"2026-2-30\")` yields the 2nd of March, which\n * is a data error becoming a rendered event on the wrong day with nothing\n * anywhere reporting a problem.\n */\nexport function parsePlainDate(iso: string): PlainDate {\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(iso);\n if (!m) throw new Error(`not an ISO date: ${JSON.stringify(iso)} — expected YYYY-MM-DD`);\n const year = Number(m[1]);\n const month = Number(m[2]);\n const day = Number(m[3]);\n if (month < 1 || month > 12) throw new Error(`month out of range in ${iso}`);\n if (day < 1 || day > daysInMonth(year, month)) {\n throw new Error(`${iso} is not a real date — ${year}-${month} has ${daysInMonth(year, month)} days`);\n }\n return { year, month, day };\n}\n\nexport { isoDateString };\n\n/** A `PlainDateTime` at midnight on `date` — the wall reading, not an instant. */\nexport function atMidnight(date: PlainDate): PlainDateTime {\n return { ...date, hour: 0, minute: 0, second: 0, millisecond: 0 };\n}\n\n/** A `PlainDateTime` at `hour`:`minute` on `date`. */\nexport function atTime(date: PlainDate, hour: number, minute = 0): PlainDateTime {\n return { ...date, hour, minute, second: 0, millisecond: 0 };\n}\n","import { daysInMonth, isoDateString, parsePlainDate } from \"../time/plain\";\nimport type { PlainDate } from \"../time/types\";\n\n/* ----------------------------------------------------------------------------\n A CALENDAR DATE — no time, no zone, just which square on the wall.\n\n Storage form is `YYYY-MM-DD`, the same ISO shape `lib/time` already parses\n and prints (`parsePlainDate` / `isoDateString`). That is not a coincidence —\n an accident date, a policy effective date, a date of birth are all exactly\n the `PlainDate` the calendar layer already models, and reusing its\n leap-year-correct arithmetic here is the whole point of `lib/time` existing\n as a shared layer instead of a calendar-component implementation detail.\n\n THE BUG THIS FILE CANNOT HAVE, BY CONSTRUCTION. The classic defect —\n `new Date(\"2026-09-11\")` parsed as UTC midnight, then printed in a reader\n west of Greenwich, silently showing \"09/10/2026\" — requires a `Date` object\n and an implicit zone somewhere in the path from string to string. Neither\n function below constructs one: `formatDate` reads `year`/`month`/`day` off\n the parsed `PlainDate` and pads them into a string by hand, and `parseDate`\n builds one the same way. There is no step at which a zone could enter.\n\n DISPLAY FORMAT: `MM/DD/YYYY`. Not `Intl`'s locale-formatted `\"Sep 11, 2026\"`\n — this is a fixed, non-localised convention, deliberately: the consuming\n application is a US legal/insurance workflow (demand letters, claim dates,\n policy dates) where every reader is the same audience and a 4-digit year\n with no ambiguity about which field is the day matters more than adapting\n to the viewer's own locale. `MM/DD/YYYY` is the format that convention\n already expects on a claim file, a police report, an EOB. It is also the\n one `parseDate` accepts back, so a value a person types after seeing this\n library's own display round-trips without translation.\n -------------------------------------------------------------------------- */\n\nconst ABSENT = \"—\";\n\nfunction pad(n: number, width = 2): string {\n return String(n).padStart(width, \"0\");\n}\n\n/**\n * `\"2026-09-11\"` → `\"09/11/2026\"`.\n *\n * `null` / `undefined` / `\"\"` render as the em dash — no date entered, the\n * convention the rest of this package's formatters share (`formatTokens`,\n * `formatBoolean`'s \"Unknown\" is the one deliberate exception; see its own\n * docblock for why absence needs a different word there).\n *\n * A non-empty value that is NOT a valid `YYYY-MM-DD` date is returned\n * UNCHANGED rather than folded into the same em dash. Absence and corruption\n * are different facts — silently rendering a malformed stored value as \"no\n * date\" hides a data problem behind a blank field; showing the raw string\n * puts it in front of whoever is looking at the row.\n */\nexport function formatDate(value: string | null | undefined): string {\n if (value == null || value === \"\") return ABSENT;\n let date: PlainDate;\n try {\n date = parsePlainDate(value);\n } catch {\n return value;\n }\n return `${pad(date.month)}/${pad(date.day)}/${date.year}`;\n}\n\nconst US_FORMAT = /^(\\d{1,2})\\/(\\d{1,2})\\/(\\d{4})$/;\nconst ISO_FORMAT = /^(\\d{4})-(\\d{1,2})-(\\d{1,2})$/;\n\n/**\n * A human-typed date → the stored `YYYY-MM-DD`, or `null` if it is not one.\n *\n * Accepts `MM/DD/YYYY` (what `formatDate` prints, and what a person types\n * into a text field) and bare ISO `YYYY-MM-DD` (what a paste from another\n * system, or this library's own storage form, looks like) — nothing looser.\n * `Date`-style leniency (`new Date(\"2026-2-30\")` silently becoming the 2nd of\n * March) is exactly the failure mode `parsePlainDate`'s calendar-length check\n * exists to refuse, so this function refuses it too: `daysInMonth` bounds the\n * day before anything is accepted.\n *\n * Returns `null`, never throws — a form validating a field checks the return\n * value; it should not have to wrap every keystroke in `try`/`catch`.\n */\nexport function parseDate(input: string): string | null {\n const trimmed = input.trim();\n if (trimmed === \"\") return null;\n\n let year: number;\n let month: number;\n let day: number;\n\n const iso = ISO_FORMAT.exec(trimmed);\n if (iso) {\n year = Number(iso[1]);\n month = Number(iso[2]);\n day = Number(iso[3]);\n } else {\n const us = US_FORMAT.exec(trimmed);\n if (!us) return null;\n month = Number(us[1]);\n day = Number(us[2]);\n year = Number(us[3]);\n }\n\n if (month < 1 || month > 12) return null;\n if (day < 1 || day > daysInMonth(year, month)) return null;\n return isoDateString({ year, month, day });\n}\n","import {\n AmbiguousTimeError,\n type Disambiguation,\n type Instant,\n type PlainDate,\n type PlainDateTime,\n type TimeZone,\n} from \"./types\";\n\n/* ============================================================================\n MOVING BETWEEN A WALL CLOCK AND THE TIMELINE.\n\n Everything here rests on ONE primitive — `offsetAt(zone, instant)` — read out\n of `Intl.DateTimeFormat`, which is the only IANA database a browser exposes.\n Everything else is arithmetic on top of it, so there is exactly one place\n where the rules of a zone enter this package.\n\n THE RULE THIS FILE EXISTS TO ENFORCE: a day is not 24 hours. It is 23 in\n New York on 2026-03-08, 25 on 2026-11-01, 23.5 in Lord Howe on 2026-10-04\n (its shift is THIRTY MINUTES, so any branch on \"is the delta 3600000\" is\n already wrong), and in Santiago on 2026-09-06 it is 23 hours that begin at\n 01:00 — `startOfDay` is NOT always midnight. Every one of those was measured\n before this file was written, and all five are in the test corpus.\n\n Kendo's scheduler crashed because two of its own methods disagreed about how\n long a day was; Mozilla's calendar drew an event an hour out plus a phantom\n row; react-big-calendar still shifts an entire week's columns because it\n derives them by adding `n * 86400000`. The shape of all three is the same:\n somewhere, a day was assumed.\n ========================================================================= */\n\nconst HOUR_MS = 3_600_000;\nconst MINUTE_MS = 60_000;\n\n/* Constructing an `Intl.DateTimeFormat` is expensive — tens of microseconds —\n and a month view resolves thousands of instants. The formatter is stateless\n once built, so one per zone is safe to keep forever. */\nconst formatters = new Map<TimeZone, Intl.DateTimeFormat>();\n\nfunction formatterFor(zone: TimeZone): Intl.DateTimeFormat {\n let f = formatters.get(zone);\n if (!f) {\n f = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: zone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n /* `h23` EXPLICITLY, not `hour12: false`. Several ICU versions render\n midnight as hour \"24\" under `hour12: false`, which parses as a valid\n number and silently lands the reading on the wrong day. */\n hourCycle: \"h23\",\n });\n formatters.set(zone, f);\n }\n return f;\n}\n\n/** The wall-clock reading in `zone` at `instant`. */\nexport function instantToPlainDateTime(zone: TimeZone, instant: Instant): PlainDateTime {\n const parts = formatterFor(zone).formatToParts(new Date(instant));\n let year = 0;\n let month = 1;\n let day = 1;\n let hour = 0;\n let minute = 0;\n let second = 0;\n for (const part of parts) {\n switch (part.type) {\n case \"year\":\n year = Number(part.value);\n break;\n case \"month\":\n month = Number(part.value);\n break;\n case \"day\":\n day = Number(part.value);\n break;\n case \"hour\":\n hour = Number(part.value);\n break;\n case \"minute\":\n minute = Number(part.value);\n break;\n case \"second\":\n second = Number(part.value);\n break;\n }\n }\n /* Sub-second precision passes straight through: every offset in the IANA\n database since 1972 is a whole number of minutes, so a zone shift can never\n move the millisecond field. */\n const millisecond = ((instant % 1000) + 1000) % 1000;\n return { year, month, day, hour, minute, second, millisecond };\n}\n\n/**\n * The same wall-clock fields, but as though they were UTC.\n *\n * `Date.UTC` is NOT used directly anywhere in this file: it maps years 0–99\n * onto 1900–1999, so a year-40 date silently becomes 1940. `setUTCFullYear` has\n * no such rule. The calendar will never render year 40, but a helper that is\n * wrong for an input it never receives is a trap left for whoever reuses it.\n */\nfunction wallAsUtcMs(wall: PlainDateTime): number {\n const d = new Date(0);\n d.setUTCFullYear(wall.year, wall.month - 1, wall.day);\n d.setUTCHours(wall.hour, wall.minute, wall.second, wall.millisecond);\n return d.getTime();\n}\n\n/**\n * `zone`'s offset from UTC at `instant`, in milliseconds. East is positive.\n *\n * THE ONE PRIMITIVE. Read by asking the browser what the wall clock says at\n * that instant and subtracting: if it is 07:00 UTC and the clock in New York\n * reads 02:00, the offset is −5 hours. No table, no parsing of a zone name, no\n * assumption that an offset is a whole number of hours — `Pacific/Chatham` sits\n * at +12:45, and `Asia/Kathmandu` at +5:45.\n */\nexport function offsetAt(zone: TimeZone, instant: Instant): number {\n return wallAsUtcMs(instantToPlainDateTime(zone, instant)) - instant;\n}\n\nfunction sameWall(a: PlainDateTime, b: PlainDateTime): boolean {\n return (\n a.year === b.year &&\n a.month === b.month &&\n a.day === b.day &&\n a.hour === b.hour &&\n a.minute === b.minute &&\n a.second === b.second &&\n a.millisecond === b.millisecond\n );\n}\n\n/**\n * Every instant at which `zone`'s clock reads exactly `wall` — zero, one, or\n * two of them, in ascending order.\n *\n * TWO CANDIDATES, BOTH VERIFIED, WHICH IS THE PART THAT IS EASY TO GET WRONG.\n * The naive method — take the offset at a guessed instant and subtract it — is\n * correct in the ordinary case and wrong at exactly the boundaries this\n * component exists to handle. So both plausible offsets are tried, and each\n * candidate is accepted ONLY if reading the clock back at it reproduces the\n * requested wall time. That round-trip is what distinguishes a real reading\n * from one the clock skipped.\n *\n * two results → an OVERLAP; the clock struck this time twice (fall back)\n * one result → the ordinary case\n * none → a GAP; the clock never struck it at all (spring forward)\n */\nfunction instantsFor(zone: TimeZone, wall: PlainDateTime): Instant[] {\n const asUtc = wallAsUtcMs(wall);\n const offsetBefore = offsetAt(zone, asUtc - 24 * HOUR_MS);\n const offsetAfter = offsetAt(zone, asUtc + 24 * HOUR_MS);\n\n const found: Instant[] = [];\n for (const offset of offsetBefore === offsetAfter ? [offsetBefore] : [offsetBefore, offsetAfter]) {\n const candidate = asUtc - offset;\n if (sameWall(instantToPlainDateTime(zone, candidate), wall)) found.push(candidate);\n }\n found.sort((a, b) => a - b);\n /* A zone can in principle be sampled twice at the same offset either side of\n a transition that returns to where it started; de-duplicate rather than\n report one instant as two. */\n return found.filter((t, i) => i === 0 || t !== found[i - 1]);\n}\n\n/**\n * The instant at which `zone`'s clock reads `wall`.\n *\n * See `Disambiguation` for what happens when that is not exactly one instant.\n * The default is `compatible`, which is what `Temporal` and every other date\n * library do, so the surprising cases are the ones you have to ask for.\n */\nexport function plainDateTimeToInstant(\n zone: TimeZone,\n wall: PlainDateTime,\n disambiguation: Disambiguation = \"compatible\",\n): Instant {\n const candidates = instantsFor(zone, wall);\n\n if (candidates.length === 1) return candidates[0];\n\n if (candidates.length > 1) {\n /* OVERLAP — the hour ran twice. */\n if (disambiguation === \"reject\") throw new AmbiguousTimeError(wall, zone, \"overlap\");\n if (disambiguation === \"later\") return candidates[candidates.length - 1];\n /* `compatible` takes the earlier, matching Temporal. */\n return candidates[0];\n }\n\n /* GAP — the clock skipped this reading entirely. */\n if (disambiguation === \"reject\") throw new AmbiguousTimeError(wall, zone, \"gap\");\n\n const asUtc = wallAsUtcMs(wall);\n const offsetBefore = offsetAt(zone, asUtc - 24 * HOUR_MS);\n const offsetAfter = offsetAt(zone, asUtc + 24 * HOUR_MS);\n const gap = offsetAfter - offsetBefore;\n\n /* Both of these are real instants; they differ by the size of the gap. Using\n the PRE-transition offset lands before the jump, the post-transition offset\n lands after it. `compatible` shifts FORWARD by the gap — the same choice\n Temporal makes, and the one that matches what a person means by \"07:30\" on\n a morning when 07:30 did not happen. */\n const earlier = asUtc - offsetBefore;\n const later = asUtc - offsetAfter;\n if (disambiguation === \"earlier\") return Math.min(earlier, later);\n return gap > 0 ? Math.max(earlier, later) : Math.min(earlier, later);\n}\n\n/** The calendar day `instant` falls on, in `zone`. */\nexport function instantToPlainDate(zone: TimeZone, instant: Instant): PlainDate {\n const wall = instantToPlainDateTime(zone, instant);\n return { year: wall.year, month: wall.month, day: wall.day };\n}\n\n/**\n * The first instant of `date` in `zone`.\n *\n * NOT \"midnight\". In `America/Santiago` on 2026-09-06 the clock jumps from\n * 00:00 to 01:00, so the day's first instant reads 01:00 — and a grid that\n * assumes its own origin is midnight puts every event on that day an hour out.\n * `America/Havana` and `Asia/Beirut` do the same thing on their own dates.\n *\n * Implemented by asking for midnight and, when the clock skipped it, walking\n * forward a minute at a time until a reading exists. The walk is bounded: no\n * transition in the IANA database exceeds two hours, so 180 iterations cannot\n * be reached by real data, and the bound exists to fail loudly rather than spin\n * if that ever stops being true.\n */\nexport function startOfDay(zone: TimeZone, date: PlainDate): Instant {\n const midnight: PlainDateTime = { ...date, hour: 0, minute: 0, second: 0, millisecond: 0 };\n const exact = instantsFor(zone, midnight);\n /* An OVERLAP at midnight means the day started twice; the day begins at the\n first of them, not the second. */\n if (exact.length > 0) return exact[0];\n\n for (let minutes = 1; minutes <= 180; minutes++) {\n const candidate = instantsFor(zone, {\n ...date,\n hour: Math.floor(minutes / 60),\n minute: minutes % 60,\n second: 0,\n millisecond: 0,\n });\n if (candidate.length > 0) return candidate[0];\n }\n throw new Error(\n `startOfDay: no instant exists in the first three hours of ${date.year}-${date.month}-${date.day} in ${zone}`,\n );\n}\n\n/** The day after `date` on the calendar, ignoring zones entirely. */\nexport function nextDay(date: PlainDate): PlainDate {\n const d = new Date(0);\n d.setUTCFullYear(date.year, date.month - 1, date.day + 1);\n return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };\n}\n\n/**\n * How long `date` actually lasts in `zone`, in milliseconds.\n *\n * Measured as the distance between one day's first instant and the next day's,\n * which is true by construction for every case — 23 hours, 25 hours, the 30\n * minutes of Lord Howe, and a day that starts at 01:00 — with no special case\n * for any of them. That is the entire reason positions in the grid are a RATIO\n * of this number rather than `hour × rowHeight`.\n */\nexport function dayLengthMs(zone: TimeZone, date: PlainDate): number {\n return startOfDay(zone, nextDay(date)) - startOfDay(zone, date);\n}\n\n/**\n * How many hours `date` lasts in `zone` — 23, 24, 25, or 23.5.\n *\n * Fractional by design. `Australia/Lord_Howe` shifts by thirty minutes, so a\n * signature returning an integer would be lying about a real zone, and any\n * caller branching on `=== 23` is already broken.\n */\nexport function hoursInDay(zone: TimeZone, date: PlainDate): number {\n return dayLengthMs(zone, date) / HOUR_MS;\n}\n\n/**\n * Where `instant` sits within `date`, as a fraction from 0 to 1.\n *\n * THE LOAD-BEARING FUNCTION. A 23-hour day compresses and a 25-hour day\n * stretches with no branch anywhere, because the divisor is the day's real\n * length. Values outside 0–1 mean the instant is not on this day and are\n * returned rather than clamped — the layout core needs to know by how much in\n * order to clip a multi-day span.\n */\nexport function dayFraction(zone: TimeZone, date: PlainDate, instant: Instant): number {\n const from = startOfDay(zone, date);\n return (instant - from) / dayLengthMs(zone, date);\n}\n\n/**\n * The exact inverse of `dayFraction`.\n *\n * IT MUST BE EXACT, and that is not a stylistic preference. Rendering maps an\n * instant to a position and hit-testing maps a position back to an instant; if\n * the two disagree by even a rounding step, an event lands one slot away from\n * where it was dropped. FullCalendar's Australia selection bug is exactly this\n * pair drifting apart. `dayFraction(z, d, instantAtFraction(z, d, f)) === f`\n * for every zone in the corpus, and there is a test that says so.\n */\nexport function instantAtFraction(zone: TimeZone, date: PlainDate, fraction: number): Instant {\n return startOfDay(zone, date) + fraction * dayLengthMs(zone, date);\n}\n\n/**\n * Move `instant` by a CALENDAR duration.\n *\n * Days are applied on the calendar and then re-anchored to the same wall-clock\n * time, so an event dragged from Saturday to Sunday across a transition keeps\n * reading 09:00 rather than becoming 08:00 or 10:00. Hours and minutes are\n * applied afterwards as exact elapsed time, because dragging an event down the\n * grid by two hours does mean two hours.\n *\n * ORDER MATTERS AND IS FIXED: days first, then time. The other order makes\n * \"one day and two hours later\" depend on which side of a transition the\n * intermediate value happened to land on.\n */\nexport function addDuration(\n zone: TimeZone,\n instant: Instant,\n duration: { days?: number; hours?: number; minutes?: number },\n disambiguation: Disambiguation = \"compatible\",\n): Instant {\n const { days = 0, hours = 0, minutes = 0 } = duration;\n\n let result = instant;\n if (days !== 0) {\n const wall = instantToPlainDateTime(zone, instant);\n const shifted = new Date(0);\n shifted.setUTCFullYear(wall.year, wall.month - 1, wall.day + days);\n result = plainDateTimeToInstant(\n zone,\n {\n year: shifted.getUTCFullYear(),\n month: shifted.getUTCMonth() + 1,\n day: shifted.getUTCDate(),\n hour: wall.hour,\n minute: wall.minute,\n second: wall.second,\n millisecond: wall.millisecond,\n },\n disambiguation,\n );\n }\n return result + hours * HOUR_MS + minutes * MINUTE_MS;\n}\n","import { instantToPlainDateTime, plainDateTimeToInstant } from \"../time/zone\";\nimport { daysInMonth } from \"../time/plain\";\nimport type { PlainDateTime } from \"../time/types\";\n\n/* ----------------------------------------------------------------------------\n AN INSTANT — a fixed point on the timeline, rendered through a zone.\n\n THE DISTINCTION FROM `date.ts`, AND WHY IT IS A SEPARATE FILE RATHER THAN AN\n OPTION ON THE SAME ONE. An accident date is a calendar square: everyone who\n reads a claim file agrees it happened on the 11th, regardless of where they\n are sitting. \"When this record was created\" is not — it is one instant, and\n a reader in Los Angeles and a reader in New York see it at different clock\n times on (near a midnight boundary) potentially different DAYS. Collapsing\n the two into one formatter with a \"is this a date-time\" flag is how a\n calendar date ends up accidentally zone-shifted, which is the exact defect\n `date.ts`'s docblock explains `formatDate` cannot have. Keeping them as two\n files with two contracts is what keeps that true.\n\n STORAGE FORM is an ISO instant string (`\"2026-09-11T19:18:43.000Z\"`,\n `\"...43.000000Z\"` — microsecond precision, as a Laravel/Carbon backend\n emits, is accepted and truncated to milliseconds, see `toInstant` below) or\n a raw epoch-millisecond number — `lib/time`'s own `Instant`. Either is\n accepted so a consumer does not have to convert before handing a value in.\n\n DISPLAY FORMAT is `MM/DD/YYYY, h:mm AM/PM` — the SAME `MM/DD/YYYY` `date.ts`\n uses, not `Intl`'s localised `\"Sep 11, 2026, 1:18 PM\"`. Two reasons, not\n one: it keeps a date and a date-time reading consistently across a table\n that shows both, and it means `parseDateTime` can invert exactly what\n `formatDateTime` prints without parsing a localised month name back out of\n a string — the format was chosen so the two functions agree with each\n other, not just with a plausible-looking example.\n\n THE ZONE CONVERSION ITSELF IS NOT REIMPLEMENTED HERE. `instantToPlainDateTime`\n and `plainDateTimeToInstant` are `lib/time/zone.ts`'s own primitives, proven\n against a five-zone DST corpus (`time-dst.test.ts`) that includes half-hour\n offsets, midnight-skipping transitions and hours that happen twice. Writing\n a second, ad-hoc offset calculation here — the way a naive datetime\n formatter usually does — is exactly how the bugs that corpus documents get\n reintroduced a second time in the same codebase.\n -------------------------------------------------------------------------- */\n\nconst ABSENT = \"—\";\n\nfunction pad(n: number, width = 2): string {\n return String(n).padStart(width, \"0\");\n}\n\n/** The reader's own zone, per the host `Intl` implementation — the correct\n * default for an INSTANT, which (unlike a `PlainDate`) has no zone of its\n * own until someone reads it. */\nfunction localZone(): string {\n return Intl.DateTimeFormat().resolvedOptions().timeZone;\n}\n\n/**\n * A stored instant (ISO string or epoch ms) → epoch milliseconds, or `null`.\n *\n * Microsecond-precision ISO strings (`.043210Z`, six fractional digits — what\n * Carbon's default JSON cast emits) are truncated to the three digits\n * `Date.parse` is spec-guaranteed to accept; V8 happens to be lenient with\n * more, but nothing requires every engine this bundles into to be.\n */\nfunction toInstant(value: string | number): number | null {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n const normalized = value.replace(/(\\.\\d{3})\\d+/, \"$1\");\n const ms = Date.parse(normalized);\n return Number.isNaN(ms) ? null : ms;\n}\n\nexport interface FormatDateTimeOptions {\n /**\n * IANA zone to render in, e.g. `\"America/New_York\"`. Omit to use the\n * reader's own zone — the right default for an instant, and the reason\n * this is a parameter at all rather than always-local: a report emailed to\n * someone in a different zone, or a server rendering a PDF, needs to say\n * WHOSE clock the printed time is on.\n */\n zone?: string;\n}\n\n/**\n * A stored instant → `\"09/11/2026, 1:18 PM\"` in `options.zone` (default: the\n * reader's own).\n *\n * `null` / `undefined` / `\"\"` render as the em dash. A non-empty value that\n * does not parse as an instant is returned UNCHANGED — see `date.ts`'s\n * docblock for why absence and corruption get different treatment.\n */\nexport function formatDateTime(value: string | number | null | undefined, options: FormatDateTimeOptions = {}): string {\n if (value == null || value === \"\") return ABSENT;\n const instant = toInstant(value);\n if (instant === null) return String(value);\n\n const zone = options.zone ?? localZone();\n const wall = instantToPlainDateTime(zone, instant);\n const hour12 = ((wall.hour + 11) % 12) + 1;\n const meridiem = wall.hour < 12 ? \"AM\" : \"PM\";\n return `${pad(wall.month)}/${pad(wall.day)}/${wall.year}, ${hour12}:${pad(wall.minute)} ${meridiem}`;\n}\n\n/* `MM/DD/YYYY, h:mm[:ss] AM/PM` — what `formatDateTime` prints, plus optional\n seconds for a value pasted from somewhere more precise. */\nconst TYPED_FORMAT =\n /^(\\d{1,2})\\/(\\d{1,2})\\/(\\d{4})(?:,?\\s+(\\d{1,2}):(\\d{2})(?::(\\d{2}))?\\s*([AaPp][Mm]))?$/;\n\n/* An ISO instant carrying its own offset or `Z` — unambiguous on its own, so\n it never needs a `zone` argument to resolve. */\nconst ISO_WITH_ZONE = /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d+)?)?\\s*(Z|[+-]\\d{2}:?\\d{2})$/;\n\nexport interface ParseDateTimeOptions {\n /**\n * The zone a bare (no AM/PM-less midnight, no offset) typed value's\n * wall-clock reading is IN. Omit to use the reader's own zone — matching\n * `formatDateTime`'s default, so parsing a value typed against that display\n * uses the same zone it was rendered in.\n */\n zone?: string;\n}\n\n/**\n * A human-typed date-time → the stored ISO instant, or `null`.\n *\n * TWO PATHS, DELIBERATELY NOT ONE. An ISO string carrying its own offset or\n * `Z` is unambiguous — `Date.parse` resolves it correctly regardless of what\n * zone this function is running in, and is used directly. Everything else is\n * a WALL-CLOCK reading with no zone of its own (`\"09/11/2026, 1:18 PM\"`), and\n * `Date.parse`'s native handling of that shape resolves it against the\n * HOST's zone — which is wrong the instant `options.zone` names a different\n * one, e.g. a server in UTC parsing a value entered by a reader in\n * `America/New_York`. So the wall-clock path is hand-parsed into fields and\n * handed to `plainDateTimeToInstant`, which resolves it against the\n * REQUESTED zone rather than whatever zone the code happens to be running in.\n *\n * A wall-clock reading that lands in a DST gap or overlap does not throw —\n * `plainDateTimeToInstant`'s default (`\"compatible\"`) disambiguation applies,\n * matching every other date library's default and `Temporal`'s own. See\n * `time-dst.test.ts` for what that resolves to at a transition.\n */\nexport function parseDateTime(input: string, options: ParseDateTimeOptions = {}): string | null {\n const trimmed = input.trim();\n if (trimmed === \"\") return null;\n\n if (ISO_WITH_ZONE.test(trimmed)) {\n const normalized = trimmed.replace(/(\\.\\d{3})\\d+/, \"$1\");\n const ms = Date.parse(normalized);\n return Number.isNaN(ms) ? null : new Date(ms).toISOString();\n }\n\n const m = TYPED_FORMAT.exec(trimmed);\n if (!m) return null;\n\n const month = Number(m[1]);\n const day = Number(m[2]);\n const year = Number(m[3]);\n if (month < 1 || month > 12) return null;\n if (day < 1 || day > daysInMonth(year, month)) return null;\n\n let hour = 0;\n let minute = 0;\n let second = 0;\n if (m[4] !== undefined) {\n const rawHour = Number(m[4]);\n minute = Number(m[5]);\n second = m[6] !== undefined ? Number(m[6]) : 0;\n const meridiem = m[7]!.toUpperCase();\n if (rawHour < 1 || rawHour > 12 || minute > 59 || second > 59) return null;\n hour = meridiem === \"AM\" ? (rawHour === 12 ? 0 : rawHour) : rawHour === 12 ? 12 : rawHour + 12;\n }\n\n const wall: PlainDateTime = { year, month, day, hour, minute, second, millisecond: 0 };\n const zone = options.zone ?? localZone();\n return new Date(plainDateTimeToInstant(zone, wall)).toISOString();\n}\n","/* ----------------------------------------------------------------------------\n USD MONEY.\n\n ALWAYS TWO DECIMAL PLACES, regardless of magnitude — the opposite choice\n from `formatBytes`/`formatTokens`, and deliberately so. Those two round MORE\n aggressively as a number grows because the tenth of a KB or the exact token\n count stop being digits anyone acts on at scale. A dollar figure never\n reaches that point: the gap between $1,234,567.89 and $1,234,568.00 is\n eighty-nine real cents on an invoice or a settlement figure, not noise, no\n matter how many digits come before the decimal point. So precision here is\n fixed, not magnitude-scaled.\n\n ZERO IS NOT ABSENCE. `formatCurrency(0)` is `\"$0.00\"` — a real, recorded\n amount, e.g. a fully-waived fee or a $0 deductible. `null`/`undefined` is\n \"no figure was entered\", rendered as the em dash this package's other\n formatters use for the same fact. Collapsing the two (rendering 0 as \"—\",\n or treating \"—\" as 0 in a sum) is the classic spreadsheet bug this\n distinction exists to prevent.\n -------------------------------------------------------------------------- */\n\nconst ABSENT = \"—\";\n\nconst USD = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\n/**\n * `1234.5` → `\"$1,234.50\"`. `null` / `undefined` → `\"—\"`. `0` → `\"$0.00\"`.\n *\n * `NaN` / `Infinity` are not real amounts either and render as the em dash —\n * grouped with absence rather than with a malformed string, because unlike\n * `formatDate`/`formatPhone`'s malformed-input case, there is no ORIGINAL\n * user-facing string to show back: the caller already has a `number`, and a\n * non-finite one is a computation defect upstream, not a typo to surface.\n */\nexport function formatCurrency(value: number | null | undefined): string {\n if (value == null || !Number.isFinite(value)) return ABSENT;\n return USD.format(value);\n}\n\n/**\n * A human-typed amount → a `number` in dollars, or `null`.\n *\n * Tolerates the punctuation someone actually types or pastes: a leading `$`,\n * thousands commas, surrounding whitespace, and ACCOUNTING-style negatives —\n * `\"($1,234.56)\"` for `-1234.56`, the convention every US invoice and EOB\n * already uses for a credit or a refund. A leading `-` is accepted too.\n * Returns `null` — never `NaN`, never a silent `0` — for anything that is not\n * recognizably a number once that punctuation is stripped, so a caller can\n * reject a bad edit instead of storing garbage.\n */\nexport function parseCurrency(input: string): number | null {\n const trimmed = input.trim();\n if (trimmed === \"\") return null;\n\n const accounting = /^\\((.*)\\)$/.exec(trimmed);\n const negative = accounting !== null;\n const body = accounting ? accounting[1] : trimmed;\n\n const stripped = body.replace(/[$,\\s]/g, \"\");\n if (!/^-?\\d+(\\.\\d+)?$/.test(stripped)) return null;\n\n const magnitude = Number(stripped);\n if (!Number.isFinite(magnitude)) return null;\n return negative ? -Math.abs(magnitude) : magnitude;\n}\n","/* ----------------------------------------------------------------------------\n A US PHONE NUMBER.\n\n STORAGE FORM is ten bare digits — `\"5551234567\"`, no punctuation, no\n country code. Ten digits is a format nothing can misparse: it survives a\n database round-trip, a CSV export, and a search-by-substring without a\n dialect of dashes or parentheses to normalise first. `parsePhone` is the\n only place a country code or punctuation gets stripped; everything\n downstream can assume ten clean digits.\n\n DISPLAY FORM is `\"(555) 123-4567\"` — the standard US convention, and the\n one every reader of a claim file or a client roster already expects. There\n is no attempt at international numbers: this consumer's domain is\n US-only, and a format that silently mis-renders a UK or Australian number\n as if it were an area code is worse than one that plainly does not\n support them.\n -------------------------------------------------------------------------- */\n\nconst ABSENT = \"—\";\n\n/**\n * Ten stored digits → `\"(555) 123-4567\"`. `null` / `undefined` / `\"\"` → `\"—\"`.\n *\n * A non-empty value that is not exactly ten digits after parsing is returned\n * UNCHANGED — the same \"show the corruption, don't hide it behind absence\"\n * rule `formatDate` documents. A stored value should already be `parsePhone`'s\n * output; if it is not, that is a data problem worth seeing in the row, not\n * a blank field that looks like nobody ever entered a number.\n */\nexport function formatPhone(value: string | null | undefined): string {\n if (value == null || value === \"\") return ABSENT;\n const digits = value.replace(/\\D/g, \"\");\n if (digits.length !== 10) return value;\n return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;\n}\n\n/**\n * A human-typed phone number → ten stored digits, or `null`.\n *\n * Strips everything but digits, so `\"(555) 123-4567\"`, `\"555.123.4567\"`,\n * `\"555-123-4567 \"` and `\"5551234567\"` all normalise the same way. A leading\n * US country code (`\"1\"`, making 11 digits total) is dropped — `\"+1 (555)\n * 123-4567\"` is the same number as `\"(555) 123-4567\"` and should be stored\n * identically. Anything that is not exactly 10 digits after that — too few,\n * too many, or an 11-digit number NOT starting with `1` (a foreign number\n * mis-typed into a US field, not a US number with an extra digit) — returns\n * `null` rather than truncating or padding a guess into place.\n */\nexport function parsePhone(input: string): string | null {\n const digits = input.replace(/\\D/g, \"\");\n if (digits.length === 11 && digits.startsWith(\"1\")) return digits.slice(1);\n if (digits.length === 10) return digits;\n return null;\n}\n","/* ----------------------------------------------------------------------------\n A GENERIC NUMBER — a float or an integer, with thousands separators.\n\n NO FIXED PRECISION BY DEFAULT, unlike `formatCurrency`'s always-two-decimals\n rule. Money has a currency-defined minor unit, so \"how many decimals\" is\n never a real question; a generic number's field might be a whole count\n of pages, a percentage to one decimal, or a computed rate to four — there is\n no single right answer, so the caller states it (`decimals`) and the\n default preserves whatever precision the value actually carries (up to 6\n fraction digits, trimmed of trailing zeros) rather than inventing digits\n that were not there or discarding real ones.\n -------------------------------------------------------------------------- */\n\nconst ABSENT = \"—\";\n\nexport interface FormatNumberOptions {\n /**\n * Fixed fraction digits, e.g. `1` for a percentage rendered as `\"42.0\"`.\n * Omit to show the value's own precision, up to 6 fraction digits, with no\n * trailing zeros invented.\n */\n decimals?: number;\n}\n\n/**\n * `1234.5` → `\"1,234.5\"`. `null` / `undefined` → `\"—\"`.\n *\n * `NaN` / `Infinity` render as the em dash too, for the same reason\n * `formatCurrency` groups them with absence rather than showing them back:\n * the caller holds a `number`, not a string a person typed, so there is no\n * original text to preserve.\n */\nexport function formatNumber(value: number | null | undefined, options: FormatNumberOptions = {}): string {\n if (value == null || !Number.isFinite(value)) return ABSENT;\n const { decimals } = options;\n const intlOptions: Intl.NumberFormatOptions =\n decimals === undefined\n ? { maximumFractionDigits: 6 }\n : { minimumFractionDigits: decimals, maximumFractionDigits: decimals };\n return new Intl.NumberFormat(\"en-US\", intlOptions).format(value);\n}\n\n/**\n * A human-typed number → a `number`, or `null`.\n *\n * Strips thousands-separator commas (`\"1,234.5\"` → `1234.5`) before parsing,\n * and accepts a leading `-` for a negative value. Rejects, rather than\n * silently truncating, anything `parseFloat` would otherwise accept a PREFIX\n * of — `\"12abc\"` is not twelve, it is a typo, and `parseFloat` alone would\n * return `12` and hide that.\n */\nexport function parseNumber(input: string): number | null {\n const trimmed = input.trim();\n if (trimmed === \"\") return null;\n const stripped = trimmed.replace(/,/g, \"\");\n if (!/^-?\\d+(\\.\\d+)?$/.test(stripped)) return null;\n const value = Number(stripped);\n return Number.isFinite(value) ? value : null;\n}\n","/* ----------------------------------------------------------------------------\n A TRI-STATE BOOLEAN — `true`, `false`, or nothing recorded.\n\n `\"Unknown\"`, NOT THE EM DASH, is the default for `null`/`undefined` here —\n the one deliberate departure from the convention every other formatter in\n this file follows. The em dash reads correctly as \"no value\" for a date, a\n number, a phone number: a blank field on a form. A boolean field usually\n means something was actually ASKED — \"was the patient transported by\n ambulance,\" \"is there a police report\" — and `null` there means the\n question has not been answered yet, which is a materially different fact\n from \"answered no.\" An em dash sits visually closer to \"false-ish\" than\n \"unanswered\" does; \"Unknown\" does not. `unknownLabel` exists to opt back\n into the em dash (or anything else) for a dense table where every column\n already uses it and a differently-worded cell would stand out for the\n wrong reason — the default is chosen for a single tri-state field read on\n its own, not for a grid.\n -------------------------------------------------------------------------- */\n\nexport interface FormatBooleanOptions {\n /** Word for `true`. Default `\"Yes\"`. */\n trueLabel?: string;\n /** Word for `false`. Default `\"No\"`. */\n falseLabel?: string;\n /** Word for `null` / `undefined`. Default `\"Unknown\"` — see the module doc\n * for why this is not the em dash the rest of this package uses for\n * absence. */\n unknownLabel?: string;\n}\n\n/**\n * `true` → `\"Yes\"`, `false` → `\"No\"`, `null` / `undefined` → `\"Unknown\"`\n * (all three overridable via `options`).\n */\nexport function formatBoolean(value: boolean | null | undefined, options: FormatBooleanOptions = {}): string {\n const { trueLabel = \"Yes\", falseLabel = \"No\", unknownLabel = \"Unknown\" } = options;\n if (value === true) return trueLabel;\n if (value === false) return falseLabel;\n return unknownLabel;\n}\n\nconst TRUE_WORDS = new Set([\"yes\", \"y\", \"true\", \"t\", \"1\"]);\nconst FALSE_WORDS = new Set([\"no\", \"n\", \"false\", \"f\", \"0\"]);\n\n/**\n * A human-typed word → `true`, `false`, or `null` for anything else\n * (including an empty string — a caller cannot distinguish \"typed nothing\"\n * from \"typed a real value\" if both return `false`).\n *\n * Matches case-insensitively against the common spellings a person actually\n * types into a Yes/No field: `yes`/`y`/`true`/`t`/`1` and `no`/`n`/`false`/\n * `f`/`0`. Deliberately does NOT guess at anything outside that list — a\n * typo like `\"yse\"` returning `null` for a caller to reject is correct;\n * returning `false` for it (or any other unrecognised text) would silently\n * record the opposite of an unreadable answer.\n */\nexport function parseBoolean(input: string): boolean | null {\n const normalized = input.trim().toLowerCase();\n if (TRUE_WORDS.has(normalized)) return true;\n if (FALSE_WORDS.has(normalized)) return false;\n return null;\n}\n"],"names":["formatBytes","bytes","units","value","unit","formatTokens","tokens","AmbiguousTimeError","wall","zone","kind","isoDateTimeString","p","w","isoDateString","date","daysInMonth","year","month","d","parsePlainDate","iso","m","day","ABSENT","pad","n","width","formatDate","US_FORMAT","ISO_FORMAT","parseDate","input","trimmed","us","HOUR_MS","formatters","formatterFor","f","instantToPlainDateTime","instant","parts","hour","minute","second","part","millisecond","wallAsUtcMs","offsetAt","sameWall","a","b","instantsFor","asUtc","offsetBefore","offsetAfter","found","offset","candidate","t","i","plainDateTimeToInstant","disambiguation","candidates","gap","earlier","later","localZone","toInstant","normalized","ms","formatDateTime","options","hour12","meridiem","TYPED_FORMAT","ISO_WITH_ZONE","parseDateTime","rawHour","USD","formatCurrency","parseCurrency","accounting","negative","stripped","magnitude","formatPhone","digits","parsePhone","formatNumber","decimals","intlOptions","parseNumber","formatBoolean","trueLabel","falseLabel","unknownLabel","TRUE_WORDS","FALSE_WORDS","parseBoolean"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pure.js","sources":["../src/lib/format.ts","../src/lib/time/types.ts","../src/lib/time/plain.ts","../src/lib/values/date.ts","../src/lib/time/zone.ts","../src/lib/values/datetime.ts","../src/lib/values/currency.ts","../src/lib/values/phone.ts","../src/lib/values/number.ts","../src/lib/values/boolean.ts"],"sourcesContent":["/* ----------------------------------------------------------------------------\n NUMBERS, TURNED INTO THE STRINGS PEOPLE READ.\n\n These lived inside the components that needed them first — `formatBytes` in\n `chat/Attachment.tsx`, `formatTokens` in `chat/ContextMeter.tsx` — which was\n fine right up until somebody outside a component wanted one. The package\n exports a single entry, so importing `formatBytes` into an application's own\n formatting layer dragged React and every component in this library into a\n layer that is deliberately framework-free.\n\n They are here because they are DECISIONS, not utilities: how much precision\n a reader can act on, and where a rounded figure stops telling the truth. Two\n places rendering the same quantity to different precision is the defect this\n file prevents — see each function for the call it makes.\n\n NOTHING IN THIS FILE MAY IMPORT REACT, directly or transitively. It is the\n whole content of the `@danxbot/ui/pure` entry; `src/pure.ts` states the\n contract and `pure-entry.test.ts` enforces it against both the source graph\n and the built artifact.\n -------------------------------------------------------------------------- */\n\n/** Binary units, because these are file sizes and every OS reports them this way. */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return \"\";\n if (bytes < 1024) return `${bytes} B`;\n const units = [\"KB\", \"MB\", \"GB\", \"TB\"];\n let value = bytes / 1024;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit += 1;\n }\n /* One decimal below 10 and none above: \"1.4 MB\" is useful precision,\n \"847.3 KB\" is noise on a number nobody acts on. */\n return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;\n}\n\n/**\n * 241_600 → \"241.6k\". A thin, token-domain-named wrapper — see\n * `formatCompactNumber` below for the shared algorithm and its own reasoning.\n * Kept as its own function (rather than every call site switching to the\n * generic one directly) because \"tokens\" is a real vocabulary word in this\n * package's chat components, and renaming every call site to the generic name\n * would cost real diff for zero behavioral gain.\n */\nexport function formatTokens(tokens: number): string {\n return formatCompactNumber(tokens);\n}\n\n/**\n * A GENERIC large count, abbreviated the way a person reads it aloud:\n * `850` → `\"850\"`, `1_100` → `\"1.1k\"`, `3_400_000` → `\"3.4M\"`,\n * `4_400_000_000` → `\"4.4B\"`. `null` / `undefined` / non-finite → `\"—\"`.\n *\n * One decimal per tier (k/M/B/T), trimmed of a trailing \".0\" (`1_000_000` →\n * `\"1M\"`, not `\"1.0M\"`), and exact below 1,000 — a count small enough to read\n * digit-by-digit gets no abbreviation at all, since \"850\" is not improved by\n * becoming \"0.9k\". This is the same one-decimal-per-tier shape `formatTokens`\n * already used for a token budget; the difference that justified a SEPARATE,\n * generic export rather than just widening that one is scope: `formatTokens`\n * is a token-budget's own vocabulary (a live context meter), while this\n * function is for any large count a screen needs to abbreviate — a field\n * tally, a donut's fraction, a record count — with no token-specific meaning\n * attached to its name.\n *\n * NEGATIVE VALUES keep their sign (`-1_100` → `\"-1.1k\"`) rather than being\n * treated as absent — a generic count utility cannot assume its caller's\n * domain never has one, the way `formatTokens`'s own callers do.\n */\nexport function formatCompactNumber(value: number | null | undefined): string {\n if (value == null || !Number.isFinite(value)) return \"—\";\n const sign = value < 0 ? \"-\" : \"\";\n const abs = Math.abs(value);\n const TIERS: readonly [threshold: number, suffix: string][] = [\n [1_000_000_000_000, \"T\"],\n [1_000_000_000, \"B\"],\n [1_000_000, \"M\"],\n [1_000, \"k\"],\n ];\n for (const [threshold, suffix] of TIERS) {\n if (abs >= threshold) return `${sign}${Number((abs / threshold).toFixed(1))}${suffix}`;\n }\n return `${sign}${Math.round(abs)}`;\n}\n","/* ============================================================================\n THE TIME LAYER'S VOCABULARY — deliberately `Temporal`'s, minus what we do\n not need.\n\n WHY WE HAVE OUR OWN AT ALL. `Temporal` is the right answer and it is not\n available: Chrome 144+, Firefox 139+, Node 26+, and **Safari has not shipped\n it in any stable version, desktop or iOS**. Every iOS browser is WebKit, so\n \"modern browsers only\" does not rescue us — global support is ~69% and the\n entire gap is Apple. The polyfill is 20.4 kB gzip and is a real option; it is\n priced on the tracker (CAL-TIME) so reversing this decision is one sentence.\n\n WHY THE SHAPES MIRROR `Temporal` ANYWAY. The day Safari ships it, the swap\n should be mechanical rather than a rewrite. So `PlainDate` has `year` /\n `month` / `day` with a ONE-BASED month, `PlainDateTime` extends it,\n `Instant` is a fixed point on the timeline, and the disambiguation modes are\n spelled exactly as `Temporal.ZonedDateTime.from` spells them. Anywhere a name\n here differs from `Temporal`'s, that is a bug in this file.\n\n WHAT IS DELIBERATELY ABSENT: calendars other than ISO 8601, nanosecond\n precision, `Temporal.Duration`'s full field set, and any notion of a\n `ZonedDateTime` object. A calendar needs an instant, a wall-clock reading, a\n calendar day, and a way to move between them; carrying more would be carrying\n a date library we did not choose to write.\n ========================================================================= */\n\n/**\n * A point on the timeline, as milliseconds since the epoch.\n *\n * NOT a `Date`. A `Date` is a mutable object that prints itself in the host's\n * local zone, and every calendar bug in the survey behind this component\n * involved someone reading a `Date`'s local fields when they meant a specific\n * zone's. A number cannot be misread that way — it has no fields to read.\n */\nexport type Instant = number;\n\n/** An IANA time zone identifier, e.g. `\"America/New_York\"`. */\nexport type TimeZone = string;\n\n/**\n * A calendar day. No time, no zone, no offset — just which square on the wall.\n *\n * `month` IS ONE-BASED, matching `Temporal` and matching how every human writes\n * a date. `Date`'s zero-based month is the single most common off-by-one in\n * date code and it is not reproduced here.\n */\nexport interface PlainDate {\n readonly year: number;\n /** 1–12. */\n readonly month: number;\n /** 1–31. */\n readonly day: number;\n}\n\n/**\n * A wall-clock reading. What a clock on the wall says, with no claim about\n * which wall.\n *\n * This is the type that makes the ambiguity explicit rather than accidental:\n * `2026-11-01T01:30` in New York happened TWICE, and `2026-03-08T02:30` never\n * happened at all. A `PlainDateTime` can represent both, which is precisely why\n * turning one into an `Instant` requires saying what to do about it.\n */\nexport interface PlainDateTime extends PlainDate {\n /** 0–23. */\n readonly hour: number;\n /** 0–59. */\n readonly minute: number;\n /** 0–59. */\n readonly second: number;\n /** 0–999. */\n readonly millisecond: number;\n}\n\n/**\n * How much to move, in CALENDAR units rather than elapsed time.\n *\n * THE DISTINCTION IS THE WHOLE POINT, and getting it wrong is the bug in every\n * library surveyed. \"One day later\" is not \"86,400,000 ms later\": across a\n * spring-forward boundary it is 23 hours, and across a fall-back it is 25. A\n * user dragging an event from Saturday to Sunday expects it to keep its\n * wall-clock time, so the move has to be expressed as `{ days: 1 }` and\n * resolved against the zone — never added as a number.\n *\n * `hours` and `minutes` ARE exact elapsed time, and that is correct: dragging\n * an event down by two hours means two hours of real duration.\n */\nexport interface CalendarDuration {\n readonly days?: number;\n readonly hours?: number;\n readonly minutes?: number;\n}\n\n/**\n * What to do when a wall-clock reading does not name exactly one instant.\n *\n * Spelled as `Temporal.ZonedDateTime.from`'s option of the same name, with the\n * same meanings, so this is one more thing that becomes a deletion later.\n *\n * - `compatible` — what every other date library does, and the default.\n * In a GAP (spring forward) move forward by the size of the gap; in an\n * OVERLAP (fall back) take the first of the two.\n * - `earlier` / `later` — take that side explicitly.\n * - `reject` — throw. For a parser or an import, where a time that never\n * existed is a fact about bad input rather than something to paper over.\n */\nexport type Disambiguation = \"compatible\" | \"earlier\" | \"later\" | \"reject\";\n\n/** Thrown by `reject` disambiguation, and never by anything else. */\nexport class AmbiguousTimeError extends Error {\n constructor(\n readonly wall: PlainDateTime,\n readonly zone: TimeZone,\n readonly kind: \"gap\" | \"overlap\",\n ) {\n super(\n kind === \"gap\"\n ? `${isoDateTimeString(wall)} does not exist in ${zone} — the clock skipped it.`\n : `${isoDateTimeString(wall)} happens twice in ${zone}.`,\n );\n this.name = \"AmbiguousTimeError\";\n }\n}\n\n/** `2026-03-08T02:30:00.000` — used in errors, and by the ISO helpers. */\nexport function isoDateTimeString(wall: PlainDateTime): string {\n const p = (n: number, w = 2) => String(n).padStart(w, \"0\");\n return (\n `${p(wall.year, 4)}-${p(wall.month)}-${p(wall.day)}` +\n `T${p(wall.hour)}:${p(wall.minute)}:${p(wall.second)}.${p(wall.millisecond, 3)}`\n );\n}\n\n/** `2026-03-08`. */\nexport function isoDateString(date: PlainDate): string {\n const p = (n: number, w = 2) => String(n).padStart(w, \"0\");\n return `${p(date.year, 4)}-${p(date.month)}-${p(date.day)}`;\n}\n","import { isoDateString, type PlainDate, type PlainDateTime } from \"./types\";\n\n/* ============================================================================\n CALENDAR ARITHMETIC — no zones, no instants, no offsets.\n\n A month grid is a question about the calendar, not about time: which squares\n does September have, which weekday does the 1st fall on, how many rows. None\n of that depends on a zone, and none of it should reach for one. Keeping this\n file zone-free is what stops a month grid quietly acquiring a dependency on\n the viewer's location.\n\n Every function here goes through UTC internally — NOT because these values\n are UTC (they are not; they have no zone at all) but because UTC is the only\n mode of `Date` with no transitions in it, so the arithmetic cannot be\n disturbed by one. A local-mode `Date` doing the same work would lose an hour\n twice a year and, at midnight-transition zones, land on the wrong DAY.\n ========================================================================= */\n\n/** Days in `month` of `year`, leap years included. */\nexport function daysInMonth(year: number, month: number): number {\n const d = new Date(0);\n d.setUTCFullYear(year, month, 0);\n return d.getUTCDate();\n}\n\nfunction fromUtc(d: Date): PlainDate {\n return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };\n}\n\nfunction toUtc(date: PlainDate): Date {\n const d = new Date(0);\n d.setUTCFullYear(date.year, date.month - 1, date.day);\n return d;\n}\n\n/** `date` moved by `days`, which may be negative, rolling months and years. */\nexport function addDays(date: PlainDate, days: number): PlainDate {\n const d = toUtc(date);\n d.setUTCDate(d.getUTCDate() + days);\n return fromUtc(d);\n}\n\n/**\n * `date` moved by `months`, clamping the day to the target month's length.\n *\n * CLAMPING, NOT ROLLING: one month after the 31st of January is the 28th or\n * 29th of February, never the 3rd of March. Rolling is what `Date.setMonth`\n * does natively and it is why \"next month\" buttons skip February.\n */\nexport function addMonths(date: PlainDate, months: number): PlainDate {\n const total = date.year * 12 + (date.month - 1) + months;\n const year = Math.floor(total / 12);\n const month = (total % 12) + 1;\n return { year, month, day: Math.min(date.day, daysInMonth(year, month)) };\n}\n\n/** Whole days from `a` to `b`; negative when `b` is earlier. */\nexport function daysBetween(a: PlainDate, b: PlainDate): number {\n return Math.round((toUtc(b).getTime() - toUtc(a).getTime()) / 86_400_000);\n}\n\n/** Negative when `a` is earlier, 0 when equal, positive when later. */\nexport function comparePlainDate(a: PlainDate, b: PlainDate): number {\n if (a.year !== b.year) return a.year - b.year;\n if (a.month !== b.month) return a.month - b.month;\n return a.day - b.day;\n}\n\nexport function isSameDate(a: PlainDate, b: PlainDate): boolean {\n return comparePlainDate(a, b) === 0;\n}\n\n/** 0 = Sunday … 6 = Saturday, matching `Date.getUTCDay` and `Intl`'s weekday order. */\nexport function dayOfWeek(date: PlainDate): number {\n return toUtc(date).getUTCDay();\n}\n\n/**\n * The first day of the week `date` falls in.\n *\n * `weekStartsOn` IS A PARAMETER WITH NO DEFAULT ANYWHERE UP THE STACK, because\n * there is no correct global answer: Sunday in the US and Japan, Monday across\n * Europe and by ISO 8601, Saturday in much of the Middle East. A component that\n * picks one silently is wrong for most of the world, so the calendar surfaces\n * it and this function simply obeys.\n */\nexport function startOfWeek(date: PlainDate, weekStartsOn: number): PlainDate {\n const shift = (dayOfWeek(date) - weekStartsOn + 7) % 7;\n return addDays(date, -shift);\n}\n\n/** The 1st of `date`'s month. */\nexport function startOfMonth(date: PlainDate): PlainDate {\n return { year: date.year, month: date.month, day: 1 };\n}\n\n/** The last day of `date`'s month. */\nexport function endOfMonth(date: PlainDate): PlainDate {\n return { year: date.year, month: date.month, day: daysInMonth(date.year, date.month) };\n}\n\n/**\n * Every day a month grid shows, INCLUDING the leading and trailing days that\n * belong to the neighbouring months.\n *\n * Always whole weeks, so the result is a multiple of seven and the grid never\n * has a ragged edge. The count varies — a 28-day February beginning on the\n * week's first day needs four rows, and a 31-day month beginning on its last\n * day needs six. Callers that assume six rows draw an empty one eleven months\n * a year; callers that assume five are simply wrong twice a year.\n */\nexport function monthGridDays(date: PlainDate, weekStartsOn: number): PlainDate[] {\n const first = startOfWeek(startOfMonth(date), weekStartsOn);\n const lastOfMonth = endOfMonth(date);\n const last = addDays(startOfWeek(lastOfMonth, weekStartsOn), 6);\n const out: PlainDate[] = [];\n for (let d = first; comparePlainDate(d, last) <= 0; d = addDays(d, 1)) out.push(d);\n return out;\n}\n\n/** The seven days of the week `date` falls in. */\nexport function weekDays(date: PlainDate, weekStartsOn: number): PlainDate[] {\n const first = startOfWeek(date, weekStartsOn);\n return Array.from({ length: 7 }, (_, i) => addDays(first, i));\n}\n\n/**\n * Parse `YYYY-MM-DD`.\n *\n * FAILS LOUDLY on anything else, including the shapes `Date` accepts and\n * silently reinterprets. `new Date(\"2026-2-30\")` yields the 2nd of March, which\n * is a data error becoming a rendered event on the wrong day with nothing\n * anywhere reporting a problem.\n */\nexport function parsePlainDate(iso: string): PlainDate {\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(iso);\n if (!m) throw new Error(`not an ISO date: ${JSON.stringify(iso)} — expected YYYY-MM-DD`);\n const year = Number(m[1]);\n const month = Number(m[2]);\n const day = Number(m[3]);\n if (month < 1 || month > 12) throw new Error(`month out of range in ${iso}`);\n if (day < 1 || day > daysInMonth(year, month)) {\n throw new Error(`${iso} is not a real date — ${year}-${month} has ${daysInMonth(year, month)} days`);\n }\n return { year, month, day };\n}\n\nexport { isoDateString };\n\n/** A `PlainDateTime` at midnight on `date` — the wall reading, not an instant. */\nexport function atMidnight(date: PlainDate): PlainDateTime {\n return { ...date, hour: 0, minute: 0, second: 0, millisecond: 0 };\n}\n\n/** A `PlainDateTime` at `hour`:`minute` on `date`. */\nexport function atTime(date: PlainDate, hour: number, minute = 0): PlainDateTime {\n return { ...date, hour, minute, second: 0, millisecond: 0 };\n}\n","import { daysInMonth, isoDateString, parsePlainDate } from \"../time/plain\";\nimport type { PlainDate } from \"../time/types\";\n\n/* ----------------------------------------------------------------------------\n A CALENDAR DATE — no time, no zone, just which square on the wall.\n\n Storage form is `YYYY-MM-DD`, the same ISO shape `lib/time` already parses\n and prints (`parsePlainDate` / `isoDateString`). That is not a coincidence —\n an accident date, a policy effective date, a date of birth are all exactly\n the `PlainDate` the calendar layer already models, and reusing its\n leap-year-correct arithmetic here is the whole point of `lib/time` existing\n as a shared layer instead of a calendar-component implementation detail.\n\n THE BUG THIS FILE CANNOT HAVE, BY CONSTRUCTION. The classic defect —\n `new Date(\"2026-09-11\")` parsed as UTC midnight, then printed in a reader\n west of Greenwich, silently showing \"09/10/2026\" — requires a `Date` object\n and an implicit zone somewhere in the path from string to string. Neither\n function below constructs one: `formatDate` reads `year`/`month`/`day` off\n the parsed `PlainDate` and pads them into a string by hand, and `parseDate`\n builds one the same way. There is no step at which a zone could enter.\n\n DISPLAY FORMAT: `MM/DD/YYYY`. Not `Intl`'s locale-formatted `\"Sep 11, 2026\"`\n — this is a fixed, non-localised convention, deliberately: the consuming\n application is a US legal/insurance workflow (demand letters, claim dates,\n policy dates) where every reader is the same audience and a 4-digit year\n with no ambiguity about which field is the day matters more than adapting\n to the viewer's own locale. `MM/DD/YYYY` is the format that convention\n already expects on a claim file, a police report, an EOB. It is also the\n one `parseDate` accepts back, so a value a person types after seeing this\n library's own display round-trips without translation.\n -------------------------------------------------------------------------- */\n\nconst ABSENT = \"—\";\n\nfunction pad(n: number, width = 2): string {\n return String(n).padStart(width, \"0\");\n}\n\n/**\n * `\"2026-09-11\"` → `\"09/11/2026\"`.\n *\n * `null` / `undefined` / `\"\"` render as the em dash — no date entered, the\n * convention the rest of this package's formatters share (`formatTokens`,\n * `formatBoolean`'s \"Unknown\" is the one deliberate exception; see its own\n * docblock for why absence needs a different word there).\n *\n * A non-empty value that is NOT a valid `YYYY-MM-DD` date is returned\n * UNCHANGED rather than folded into the same em dash. Absence and corruption\n * are different facts — silently rendering a malformed stored value as \"no\n * date\" hides a data problem behind a blank field; showing the raw string\n * puts it in front of whoever is looking at the row.\n */\nexport function formatDate(value: string | null | undefined): string {\n if (value == null || value === \"\") return ABSENT;\n let date: PlainDate;\n try {\n date = parsePlainDate(value);\n } catch {\n return value;\n }\n return `${pad(date.month)}/${pad(date.day)}/${date.year}`;\n}\n\nconst US_FORMAT = /^(\\d{1,2})\\/(\\d{1,2})\\/(\\d{4})$/;\nconst ISO_FORMAT = /^(\\d{4})-(\\d{1,2})-(\\d{1,2})$/;\n\n/**\n * A human-typed date → the stored `YYYY-MM-DD`, or `null` if it is not one.\n *\n * Accepts `MM/DD/YYYY` (what `formatDate` prints, and what a person types\n * into a text field) and bare ISO `YYYY-MM-DD` (what a paste from another\n * system, or this library's own storage form, looks like) — nothing looser.\n * `Date`-style leniency (`new Date(\"2026-2-30\")` silently becoming the 2nd of\n * March) is exactly the failure mode `parsePlainDate`'s calendar-length check\n * exists to refuse, so this function refuses it too: `daysInMonth` bounds the\n * day before anything is accepted.\n *\n * Returns `null`, never throws — a form validating a field checks the return\n * value; it should not have to wrap every keystroke in `try`/`catch`.\n */\nexport function parseDate(input: string): string | null {\n const trimmed = input.trim();\n if (trimmed === \"\") return null;\n\n let year: number;\n let month: number;\n let day: number;\n\n const iso = ISO_FORMAT.exec(trimmed);\n if (iso) {\n year = Number(iso[1]);\n month = Number(iso[2]);\n day = Number(iso[3]);\n } else {\n const us = US_FORMAT.exec(trimmed);\n if (!us) return null;\n month = Number(us[1]);\n day = Number(us[2]);\n year = Number(us[3]);\n }\n\n if (month < 1 || month > 12) return null;\n if (day < 1 || day > daysInMonth(year, month)) return null;\n return isoDateString({ year, month, day });\n}\n","import {\n AmbiguousTimeError,\n type Disambiguation,\n type Instant,\n type PlainDate,\n type PlainDateTime,\n type TimeZone,\n} from \"./types\";\n\n/* ============================================================================\n MOVING BETWEEN A WALL CLOCK AND THE TIMELINE.\n\n Everything here rests on ONE primitive — `offsetAt(zone, instant)` — read out\n of `Intl.DateTimeFormat`, which is the only IANA database a browser exposes.\n Everything else is arithmetic on top of it, so there is exactly one place\n where the rules of a zone enter this package.\n\n THE RULE THIS FILE EXISTS TO ENFORCE: a day is not 24 hours. It is 23 in\n New York on 2026-03-08, 25 on 2026-11-01, 23.5 in Lord Howe on 2026-10-04\n (its shift is THIRTY MINUTES, so any branch on \"is the delta 3600000\" is\n already wrong), and in Santiago on 2026-09-06 it is 23 hours that begin at\n 01:00 — `startOfDay` is NOT always midnight. Every one of those was measured\n before this file was written, and all five are in the test corpus.\n\n Kendo's scheduler crashed because two of its own methods disagreed about how\n long a day was; Mozilla's calendar drew an event an hour out plus a phantom\n row; react-big-calendar still shifts an entire week's columns because it\n derives them by adding `n * 86400000`. The shape of all three is the same:\n somewhere, a day was assumed.\n ========================================================================= */\n\nconst HOUR_MS = 3_600_000;\nconst MINUTE_MS = 60_000;\n\n/* Constructing an `Intl.DateTimeFormat` is expensive — tens of microseconds —\n and a month view resolves thousands of instants. The formatter is stateless\n once built, so one per zone is safe to keep forever. */\nconst formatters = new Map<TimeZone, Intl.DateTimeFormat>();\n\nfunction formatterFor(zone: TimeZone): Intl.DateTimeFormat {\n let f = formatters.get(zone);\n if (!f) {\n f = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: zone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n /* `h23` EXPLICITLY, not `hour12: false`. Several ICU versions render\n midnight as hour \"24\" under `hour12: false`, which parses as a valid\n number and silently lands the reading on the wrong day. */\n hourCycle: \"h23\",\n });\n formatters.set(zone, f);\n }\n return f;\n}\n\n/** The wall-clock reading in `zone` at `instant`. */\nexport function instantToPlainDateTime(zone: TimeZone, instant: Instant): PlainDateTime {\n const parts = formatterFor(zone).formatToParts(new Date(instant));\n let year = 0;\n let month = 1;\n let day = 1;\n let hour = 0;\n let minute = 0;\n let second = 0;\n for (const part of parts) {\n switch (part.type) {\n case \"year\":\n year = Number(part.value);\n break;\n case \"month\":\n month = Number(part.value);\n break;\n case \"day\":\n day = Number(part.value);\n break;\n case \"hour\":\n hour = Number(part.value);\n break;\n case \"minute\":\n minute = Number(part.value);\n break;\n case \"second\":\n second = Number(part.value);\n break;\n }\n }\n /* Sub-second precision passes straight through: every offset in the IANA\n database since 1972 is a whole number of minutes, so a zone shift can never\n move the millisecond field. */\n const millisecond = ((instant % 1000) + 1000) % 1000;\n return { year, month, day, hour, minute, second, millisecond };\n}\n\n/**\n * The same wall-clock fields, but as though they were UTC.\n *\n * `Date.UTC` is NOT used directly anywhere in this file: it maps years 0–99\n * onto 1900–1999, so a year-40 date silently becomes 1940. `setUTCFullYear` has\n * no such rule. The calendar will never render year 40, but a helper that is\n * wrong for an input it never receives is a trap left for whoever reuses it.\n */\nfunction wallAsUtcMs(wall: PlainDateTime): number {\n const d = new Date(0);\n d.setUTCFullYear(wall.year, wall.month - 1, wall.day);\n d.setUTCHours(wall.hour, wall.minute, wall.second, wall.millisecond);\n return d.getTime();\n}\n\n/**\n * `zone`'s offset from UTC at `instant`, in milliseconds. East is positive.\n *\n * THE ONE PRIMITIVE. Read by asking the browser what the wall clock says at\n * that instant and subtracting: if it is 07:00 UTC and the clock in New York\n * reads 02:00, the offset is −5 hours. No table, no parsing of a zone name, no\n * assumption that an offset is a whole number of hours — `Pacific/Chatham` sits\n * at +12:45, and `Asia/Kathmandu` at +5:45.\n */\nexport function offsetAt(zone: TimeZone, instant: Instant): number {\n return wallAsUtcMs(instantToPlainDateTime(zone, instant)) - instant;\n}\n\nfunction sameWall(a: PlainDateTime, b: PlainDateTime): boolean {\n return (\n a.year === b.year &&\n a.month === b.month &&\n a.day === b.day &&\n a.hour === b.hour &&\n a.minute === b.minute &&\n a.second === b.second &&\n a.millisecond === b.millisecond\n );\n}\n\n/**\n * Every instant at which `zone`'s clock reads exactly `wall` — zero, one, or\n * two of them, in ascending order.\n *\n * TWO CANDIDATES, BOTH VERIFIED, WHICH IS THE PART THAT IS EASY TO GET WRONG.\n * The naive method — take the offset at a guessed instant and subtract it — is\n * correct in the ordinary case and wrong at exactly the boundaries this\n * component exists to handle. So both plausible offsets are tried, and each\n * candidate is accepted ONLY if reading the clock back at it reproduces the\n * requested wall time. That round-trip is what distinguishes a real reading\n * from one the clock skipped.\n *\n * two results → an OVERLAP; the clock struck this time twice (fall back)\n * one result → the ordinary case\n * none → a GAP; the clock never struck it at all (spring forward)\n */\nfunction instantsFor(zone: TimeZone, wall: PlainDateTime): Instant[] {\n const asUtc = wallAsUtcMs(wall);\n const offsetBefore = offsetAt(zone, asUtc - 24 * HOUR_MS);\n const offsetAfter = offsetAt(zone, asUtc + 24 * HOUR_MS);\n\n const found: Instant[] = [];\n for (const offset of offsetBefore === offsetAfter ? [offsetBefore] : [offsetBefore, offsetAfter]) {\n const candidate = asUtc - offset;\n if (sameWall(instantToPlainDateTime(zone, candidate), wall)) found.push(candidate);\n }\n found.sort((a, b) => a - b);\n /* A zone can in principle be sampled twice at the same offset either side of\n a transition that returns to where it started; de-duplicate rather than\n report one instant as two. */\n return found.filter((t, i) => i === 0 || t !== found[i - 1]);\n}\n\n/**\n * The instant at which `zone`'s clock reads `wall`.\n *\n * See `Disambiguation` for what happens when that is not exactly one instant.\n * The default is `compatible`, which is what `Temporal` and every other date\n * library do, so the surprising cases are the ones you have to ask for.\n */\nexport function plainDateTimeToInstant(\n zone: TimeZone,\n wall: PlainDateTime,\n disambiguation: Disambiguation = \"compatible\",\n): Instant {\n const candidates = instantsFor(zone, wall);\n\n if (candidates.length === 1) return candidates[0];\n\n if (candidates.length > 1) {\n /* OVERLAP — the hour ran twice. */\n if (disambiguation === \"reject\") throw new AmbiguousTimeError(wall, zone, \"overlap\");\n if (disambiguation === \"later\") return candidates[candidates.length - 1];\n /* `compatible` takes the earlier, matching Temporal. */\n return candidates[0];\n }\n\n /* GAP — the clock skipped this reading entirely. */\n if (disambiguation === \"reject\") throw new AmbiguousTimeError(wall, zone, \"gap\");\n\n const asUtc = wallAsUtcMs(wall);\n const offsetBefore = offsetAt(zone, asUtc - 24 * HOUR_MS);\n const offsetAfter = offsetAt(zone, asUtc + 24 * HOUR_MS);\n const gap = offsetAfter - offsetBefore;\n\n /* Both of these are real instants; they differ by the size of the gap. Using\n the PRE-transition offset lands before the jump, the post-transition offset\n lands after it. `compatible` shifts FORWARD by the gap — the same choice\n Temporal makes, and the one that matches what a person means by \"07:30\" on\n a morning when 07:30 did not happen. */\n const earlier = asUtc - offsetBefore;\n const later = asUtc - offsetAfter;\n if (disambiguation === \"earlier\") return Math.min(earlier, later);\n return gap > 0 ? Math.max(earlier, later) : Math.min(earlier, later);\n}\n\n/** The calendar day `instant` falls on, in `zone`. */\nexport function instantToPlainDate(zone: TimeZone, instant: Instant): PlainDate {\n const wall = instantToPlainDateTime(zone, instant);\n return { year: wall.year, month: wall.month, day: wall.day };\n}\n\n/**\n * The first instant of `date` in `zone`.\n *\n * NOT \"midnight\". In `America/Santiago` on 2026-09-06 the clock jumps from\n * 00:00 to 01:00, so the day's first instant reads 01:00 — and a grid that\n * assumes its own origin is midnight puts every event on that day an hour out.\n * `America/Havana` and `Asia/Beirut` do the same thing on their own dates.\n *\n * Implemented by asking for midnight and, when the clock skipped it, walking\n * forward a minute at a time until a reading exists. The walk is bounded: no\n * transition in the IANA database exceeds two hours, so 180 iterations cannot\n * be reached by real data, and the bound exists to fail loudly rather than spin\n * if that ever stops being true.\n */\nexport function startOfDay(zone: TimeZone, date: PlainDate): Instant {\n const midnight: PlainDateTime = { ...date, hour: 0, minute: 0, second: 0, millisecond: 0 };\n const exact = instantsFor(zone, midnight);\n /* An OVERLAP at midnight means the day started twice; the day begins at the\n first of them, not the second. */\n if (exact.length > 0) return exact[0];\n\n for (let minutes = 1; minutes <= 180; minutes++) {\n const candidate = instantsFor(zone, {\n ...date,\n hour: Math.floor(minutes / 60),\n minute: minutes % 60,\n second: 0,\n millisecond: 0,\n });\n if (candidate.length > 0) return candidate[0];\n }\n throw new Error(\n `startOfDay: no instant exists in the first three hours of ${date.year}-${date.month}-${date.day} in ${zone}`,\n );\n}\n\n/** The day after `date` on the calendar, ignoring zones entirely. */\nexport function nextDay(date: PlainDate): PlainDate {\n const d = new Date(0);\n d.setUTCFullYear(date.year, date.month - 1, date.day + 1);\n return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };\n}\n\n/**\n * How long `date` actually lasts in `zone`, in milliseconds.\n *\n * Measured as the distance between one day's first instant and the next day's,\n * which is true by construction for every case — 23 hours, 25 hours, the 30\n * minutes of Lord Howe, and a day that starts at 01:00 — with no special case\n * for any of them. That is the entire reason positions in the grid are a RATIO\n * of this number rather than `hour × rowHeight`.\n */\nexport function dayLengthMs(zone: TimeZone, date: PlainDate): number {\n return startOfDay(zone, nextDay(date)) - startOfDay(zone, date);\n}\n\n/**\n * How many hours `date` lasts in `zone` — 23, 24, 25, or 23.5.\n *\n * Fractional by design. `Australia/Lord_Howe` shifts by thirty minutes, so a\n * signature returning an integer would be lying about a real zone, and any\n * caller branching on `=== 23` is already broken.\n */\nexport function hoursInDay(zone: TimeZone, date: PlainDate): number {\n return dayLengthMs(zone, date) / HOUR_MS;\n}\n\n/**\n * Where `instant` sits within `date`, as a fraction from 0 to 1.\n *\n * THE LOAD-BEARING FUNCTION. A 23-hour day compresses and a 25-hour day\n * stretches with no branch anywhere, because the divisor is the day's real\n * length. Values outside 0–1 mean the instant is not on this day and are\n * returned rather than clamped — the layout core needs to know by how much in\n * order to clip a multi-day span.\n */\nexport function dayFraction(zone: TimeZone, date: PlainDate, instant: Instant): number {\n const from = startOfDay(zone, date);\n return (instant - from) / dayLengthMs(zone, date);\n}\n\n/**\n * The exact inverse of `dayFraction`.\n *\n * IT MUST BE EXACT, and that is not a stylistic preference. Rendering maps an\n * instant to a position and hit-testing maps a position back to an instant; if\n * the two disagree by even a rounding step, an event lands one slot away from\n * where it was dropped. FullCalendar's Australia selection bug is exactly this\n * pair drifting apart. `dayFraction(z, d, instantAtFraction(z, d, f)) === f`\n * for every zone in the corpus, and there is a test that says so.\n */\nexport function instantAtFraction(zone: TimeZone, date: PlainDate, fraction: number): Instant {\n return startOfDay(zone, date) + fraction * dayLengthMs(zone, date);\n}\n\n/**\n * Move `instant` by a CALENDAR duration.\n *\n * Days are applied on the calendar and then re-anchored to the same wall-clock\n * time, so an event dragged from Saturday to Sunday across a transition keeps\n * reading 09:00 rather than becoming 08:00 or 10:00. Hours and minutes are\n * applied afterwards as exact elapsed time, because dragging an event down the\n * grid by two hours does mean two hours.\n *\n * ORDER MATTERS AND IS FIXED: days first, then time. The other order makes\n * \"one day and two hours later\" depend on which side of a transition the\n * intermediate value happened to land on.\n */\nexport function addDuration(\n zone: TimeZone,\n instant: Instant,\n duration: { days?: number; hours?: number; minutes?: number },\n disambiguation: Disambiguation = \"compatible\",\n): Instant {\n const { days = 0, hours = 0, minutes = 0 } = duration;\n\n let result = instant;\n if (days !== 0) {\n const wall = instantToPlainDateTime(zone, instant);\n const shifted = new Date(0);\n shifted.setUTCFullYear(wall.year, wall.month - 1, wall.day + days);\n result = plainDateTimeToInstant(\n zone,\n {\n year: shifted.getUTCFullYear(),\n month: shifted.getUTCMonth() + 1,\n day: shifted.getUTCDate(),\n hour: wall.hour,\n minute: wall.minute,\n second: wall.second,\n millisecond: wall.millisecond,\n },\n disambiguation,\n );\n }\n return result + hours * HOUR_MS + minutes * MINUTE_MS;\n}\n","import { instantToPlainDateTime, plainDateTimeToInstant } from \"../time/zone\";\nimport { daysInMonth } from \"../time/plain\";\nimport type { PlainDateTime } from \"../time/types\";\n\n/* ----------------------------------------------------------------------------\n AN INSTANT — a fixed point on the timeline, rendered through a zone.\n\n THE DISTINCTION FROM `date.ts`, AND WHY IT IS A SEPARATE FILE RATHER THAN AN\n OPTION ON THE SAME ONE. An accident date is a calendar square: everyone who\n reads a claim file agrees it happened on the 11th, regardless of where they\n are sitting. \"When this record was created\" is not — it is one instant, and\n a reader in Los Angeles and a reader in New York see it at different clock\n times on (near a midnight boundary) potentially different DAYS. Collapsing\n the two into one formatter with a \"is this a date-time\" flag is how a\n calendar date ends up accidentally zone-shifted, which is the exact defect\n `date.ts`'s docblock explains `formatDate` cannot have. Keeping them as two\n files with two contracts is what keeps that true.\n\n STORAGE FORM is an ISO instant string (`\"2026-09-11T19:18:43.000Z\"`,\n `\"...43.000000Z\"` — microsecond precision, as a Laravel/Carbon backend\n emits, is accepted and truncated to milliseconds, see `toInstant` below) or\n a raw epoch-millisecond number — `lib/time`'s own `Instant`. Either is\n accepted so a consumer does not have to convert before handing a value in.\n\n DISPLAY FORMAT is `MM/DD/YYYY, h:mm AM/PM` — the SAME `MM/DD/YYYY` `date.ts`\n uses, not `Intl`'s localised `\"Sep 11, 2026, 1:18 PM\"`. Two reasons, not\n one: it keeps a date and a date-time reading consistently across a table\n that shows both, and it means `parseDateTime` can invert exactly what\n `formatDateTime` prints without parsing a localised month name back out of\n a string — the format was chosen so the two functions agree with each\n other, not just with a plausible-looking example.\n\n THE ZONE CONVERSION ITSELF IS NOT REIMPLEMENTED HERE. `instantToPlainDateTime`\n and `plainDateTimeToInstant` are `lib/time/zone.ts`'s own primitives, proven\n against a five-zone DST corpus (`time-dst.test.ts`) that includes half-hour\n offsets, midnight-skipping transitions and hours that happen twice. Writing\n a second, ad-hoc offset calculation here — the way a naive datetime\n formatter usually does — is exactly how the bugs that corpus documents get\n reintroduced a second time in the same codebase.\n -------------------------------------------------------------------------- */\n\nconst ABSENT = \"—\";\n\nfunction pad(n: number, width = 2): string {\n return String(n).padStart(width, \"0\");\n}\n\n/** The reader's own zone, per the host `Intl` implementation — the correct\n * default for an INSTANT, which (unlike a `PlainDate`) has no zone of its\n * own until someone reads it. */\nfunction localZone(): string {\n return Intl.DateTimeFormat().resolvedOptions().timeZone;\n}\n\n/**\n * A stored instant (ISO string or epoch ms) → epoch milliseconds, or `null`.\n *\n * Microsecond-precision ISO strings (`.043210Z`, six fractional digits — what\n * Carbon's default JSON cast emits) are truncated to the three digits\n * `Date.parse` is spec-guaranteed to accept; V8 happens to be lenient with\n * more, but nothing requires every engine this bundles into to be.\n */\nfunction toInstant(value: string | number): number | null {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n const normalized = value.replace(/(\\.\\d{3})\\d+/, \"$1\");\n const ms = Date.parse(normalized);\n return Number.isNaN(ms) ? null : ms;\n}\n\nexport interface FormatDateTimeOptions {\n /**\n * IANA zone to render in, e.g. `\"America/New_York\"`. Omit to use the\n * reader's own zone — the right default for an instant, and the reason\n * this is a parameter at all rather than always-local: a report emailed to\n * someone in a different zone, or a server rendering a PDF, needs to say\n * WHOSE clock the printed time is on.\n */\n zone?: string;\n}\n\n/**\n * A stored instant → `\"09/11/2026, 1:18 PM\"` in `options.zone` (default: the\n * reader's own).\n *\n * `null` / `undefined` / `\"\"` render as the em dash. A non-empty value that\n * does not parse as an instant is returned UNCHANGED — see `date.ts`'s\n * docblock for why absence and corruption get different treatment.\n */\nexport function formatDateTime(value: string | number | null | undefined, options: FormatDateTimeOptions = {}): string {\n if (value == null || value === \"\") return ABSENT;\n const instant = toInstant(value);\n if (instant === null) return String(value);\n\n const zone = options.zone ?? localZone();\n const wall = instantToPlainDateTime(zone, instant);\n const hour12 = ((wall.hour + 11) % 12) + 1;\n const meridiem = wall.hour < 12 ? \"AM\" : \"PM\";\n return `${pad(wall.month)}/${pad(wall.day)}/${wall.year}, ${hour12}:${pad(wall.minute)} ${meridiem}`;\n}\n\n/* `MM/DD/YYYY, h:mm[:ss] AM/PM` — what `formatDateTime` prints, plus optional\n seconds for a value pasted from somewhere more precise. */\nconst TYPED_FORMAT =\n /^(\\d{1,2})\\/(\\d{1,2})\\/(\\d{4})(?:,?\\s+(\\d{1,2}):(\\d{2})(?::(\\d{2}))?\\s*([AaPp][Mm]))?$/;\n\n/* An ISO instant carrying its own offset or `Z` — unambiguous on its own, so\n it never needs a `zone` argument to resolve. */\nconst ISO_WITH_ZONE = /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d+)?)?\\s*(Z|[+-]\\d{2}:?\\d{2})$/;\n\nexport interface ParseDateTimeOptions {\n /**\n * The zone a bare (no AM/PM-less midnight, no offset) typed value's\n * wall-clock reading is IN. Omit to use the reader's own zone — matching\n * `formatDateTime`'s default, so parsing a value typed against that display\n * uses the same zone it was rendered in.\n */\n zone?: string;\n}\n\n/**\n * A human-typed date-time → the stored ISO instant, or `null`.\n *\n * TWO PATHS, DELIBERATELY NOT ONE. An ISO string carrying its own offset or\n * `Z` is unambiguous — `Date.parse` resolves it correctly regardless of what\n * zone this function is running in, and is used directly. Everything else is\n * a WALL-CLOCK reading with no zone of its own (`\"09/11/2026, 1:18 PM\"`), and\n * `Date.parse`'s native handling of that shape resolves it against the\n * HOST's zone — which is wrong the instant `options.zone` names a different\n * one, e.g. a server in UTC parsing a value entered by a reader in\n * `America/New_York`. So the wall-clock path is hand-parsed into fields and\n * handed to `plainDateTimeToInstant`, which resolves it against the\n * REQUESTED zone rather than whatever zone the code happens to be running in.\n *\n * A wall-clock reading that lands in a DST gap or overlap does not throw —\n * `plainDateTimeToInstant`'s default (`\"compatible\"`) disambiguation applies,\n * matching every other date library's default and `Temporal`'s own. See\n * `time-dst.test.ts` for what that resolves to at a transition.\n */\nexport function parseDateTime(input: string, options: ParseDateTimeOptions = {}): string | null {\n const trimmed = input.trim();\n if (trimmed === \"\") return null;\n\n if (ISO_WITH_ZONE.test(trimmed)) {\n const normalized = trimmed.replace(/(\\.\\d{3})\\d+/, \"$1\");\n const ms = Date.parse(normalized);\n return Number.isNaN(ms) ? null : new Date(ms).toISOString();\n }\n\n const m = TYPED_FORMAT.exec(trimmed);\n if (!m) return null;\n\n const month = Number(m[1]);\n const day = Number(m[2]);\n const year = Number(m[3]);\n if (month < 1 || month > 12) return null;\n if (day < 1 || day > daysInMonth(year, month)) return null;\n\n let hour = 0;\n let minute = 0;\n let second = 0;\n if (m[4] !== undefined) {\n const rawHour = Number(m[4]);\n minute = Number(m[5]);\n second = m[6] !== undefined ? Number(m[6]) : 0;\n const meridiem = m[7]!.toUpperCase();\n if (rawHour < 1 || rawHour > 12 || minute > 59 || second > 59) return null;\n hour = meridiem === \"AM\" ? (rawHour === 12 ? 0 : rawHour) : rawHour === 12 ? 12 : rawHour + 12;\n }\n\n const wall: PlainDateTime = { year, month, day, hour, minute, second, millisecond: 0 };\n const zone = options.zone ?? localZone();\n return new Date(plainDateTimeToInstant(zone, wall)).toISOString();\n}\n","/* ----------------------------------------------------------------------------\n USD MONEY.\n\n ALWAYS TWO DECIMAL PLACES, regardless of magnitude — the opposite choice\n from `formatBytes`/`formatTokens`, and deliberately so. Those two round MORE\n aggressively as a number grows because the tenth of a KB or the exact token\n count stop being digits anyone acts on at scale. A dollar figure never\n reaches that point: the gap between $1,234,567.89 and $1,234,568.00 is\n eighty-nine real cents on an invoice or a settlement figure, not noise, no\n matter how many digits come before the decimal point. So precision here is\n fixed, not magnitude-scaled.\n\n ZERO IS NOT ABSENCE. `formatCurrency(0)` is `\"$0.00\"` — a real, recorded\n amount, e.g. a fully-waived fee or a $0 deductible. `null`/`undefined` is\n \"no figure was entered\", rendered as the em dash this package's other\n formatters use for the same fact. Collapsing the two (rendering 0 as \"—\",\n or treating \"—\" as 0 in a sum) is the classic spreadsheet bug this\n distinction exists to prevent.\n -------------------------------------------------------------------------- */\n\nconst ABSENT = \"—\";\n\nconst USD = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\n/**\n * `1234.5` → `\"$1,234.50\"`. `null` / `undefined` → `\"—\"`. `0` → `\"$0.00\"`.\n *\n * `NaN` / `Infinity` are not real amounts either and render as the em dash —\n * grouped with absence rather than with a malformed string, because unlike\n * `formatDate`/`formatPhone`'s malformed-input case, there is no ORIGINAL\n * user-facing string to show back: the caller already has a `number`, and a\n * non-finite one is a computation defect upstream, not a typo to surface.\n */\nexport function formatCurrency(value: number | null | undefined): string {\n if (value == null || !Number.isFinite(value)) return ABSENT;\n return USD.format(value);\n}\n\n/**\n * A human-typed amount → a `number` in dollars, or `null`.\n *\n * Tolerates the punctuation someone actually types or pastes: a leading `$`,\n * thousands commas, surrounding whitespace, and ACCOUNTING-style negatives —\n * `\"($1,234.56)\"` for `-1234.56`, the convention every US invoice and EOB\n * already uses for a credit or a refund. A leading `-` is accepted too.\n * Returns `null` — never `NaN`, never a silent `0` — for anything that is not\n * recognizably a number once that punctuation is stripped, so a caller can\n * reject a bad edit instead of storing garbage.\n */\nexport function parseCurrency(input: string): number | null {\n const trimmed = input.trim();\n if (trimmed === \"\") return null;\n\n const accounting = /^\\((.*)\\)$/.exec(trimmed);\n const negative = accounting !== null;\n const body = accounting ? accounting[1] : trimmed;\n\n const stripped = body.replace(/[$,\\s]/g, \"\");\n if (!/^-?\\d+(\\.\\d+)?$/.test(stripped)) return null;\n\n const magnitude = Number(stripped);\n if (!Number.isFinite(magnitude)) return null;\n return negative ? -Math.abs(magnitude) : magnitude;\n}\n","/* ----------------------------------------------------------------------------\n A US PHONE NUMBER.\n\n STORAGE FORM is ten bare digits — `\"5551234567\"`, no punctuation, no\n country code. Ten digits is a format nothing can misparse: it survives a\n database round-trip, a CSV export, and a search-by-substring without a\n dialect of dashes or parentheses to normalise first. `parsePhone` is the\n only place a country code or punctuation gets stripped; everything\n downstream can assume ten clean digits.\n\n DISPLAY FORM is `\"(555) 123-4567\"` — the standard US convention, and the\n one every reader of a claim file or a client roster already expects. There\n is no attempt at international numbers: this consumer's domain is\n US-only, and a format that silently mis-renders a UK or Australian number\n as if it were an area code is worse than one that plainly does not\n support them.\n -------------------------------------------------------------------------- */\n\nconst ABSENT = \"—\";\n\n/**\n * Ten stored digits → `\"(555) 123-4567\"`. `null` / `undefined` / `\"\"` → `\"—\"`.\n *\n * A non-empty value that is not exactly ten digits after parsing is returned\n * UNCHANGED — the same \"show the corruption, don't hide it behind absence\"\n * rule `formatDate` documents. A stored value should already be `parsePhone`'s\n * output; if it is not, that is a data problem worth seeing in the row, not\n * a blank field that looks like nobody ever entered a number.\n */\nexport function formatPhone(value: string | null | undefined): string {\n if (value == null || value === \"\") return ABSENT;\n const digits = value.replace(/\\D/g, \"\");\n if (digits.length !== 10) return value;\n return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;\n}\n\n/**\n * A human-typed phone number → ten stored digits, or `null`.\n *\n * Strips everything but digits, so `\"(555) 123-4567\"`, `\"555.123.4567\"`,\n * `\"555-123-4567 \"` and `\"5551234567\"` all normalise the same way. A leading\n * US country code (`\"1\"`, making 11 digits total) is dropped — `\"+1 (555)\n * 123-4567\"` is the same number as `\"(555) 123-4567\"` and should be stored\n * identically. Anything that is not exactly 10 digits after that — too few,\n * too many, or an 11-digit number NOT starting with `1` (a foreign number\n * mis-typed into a US field, not a US number with an extra digit) — returns\n * `null` rather than truncating or padding a guess into place.\n */\nexport function parsePhone(input: string): string | null {\n const digits = input.replace(/\\D/g, \"\");\n if (digits.length === 11 && digits.startsWith(\"1\")) return digits.slice(1);\n if (digits.length === 10) return digits;\n return null;\n}\n","/* ----------------------------------------------------------------------------\n A GENERIC NUMBER — a float or an integer, with thousands separators.\n\n NO FIXED PRECISION BY DEFAULT, unlike `formatCurrency`'s always-two-decimals\n rule. Money has a currency-defined minor unit, so \"how many decimals\" is\n never a real question; a generic number's field might be a whole count\n of pages, a percentage to one decimal, or a computed rate to four — there is\n no single right answer, so the caller states it (`decimals`) and the\n default preserves whatever precision the value actually carries (up to 6\n fraction digits, trimmed of trailing zeros) rather than inventing digits\n that were not there or discarding real ones.\n -------------------------------------------------------------------------- */\n\nconst ABSENT = \"—\";\n\nexport interface FormatNumberOptions {\n /**\n * Fixed fraction digits, e.g. `1` for a percentage rendered as `\"42.0\"`.\n * Omit to show the value's own precision, up to 6 fraction digits, with no\n * trailing zeros invented.\n */\n decimals?: number;\n}\n\n/**\n * `1234.5` → `\"1,234.5\"`. `null` / `undefined` → `\"—\"`.\n *\n * `NaN` / `Infinity` render as the em dash too, for the same reason\n * `formatCurrency` groups them with absence rather than showing them back:\n * the caller holds a `number`, not a string a person typed, so there is no\n * original text to preserve.\n */\nexport function formatNumber(value: number | null | undefined, options: FormatNumberOptions = {}): string {\n if (value == null || !Number.isFinite(value)) return ABSENT;\n const { decimals } = options;\n const intlOptions: Intl.NumberFormatOptions =\n decimals === undefined\n ? { maximumFractionDigits: 6 }\n : { minimumFractionDigits: decimals, maximumFractionDigits: decimals };\n return new Intl.NumberFormat(\"en-US\", intlOptions).format(value);\n}\n\n/**\n * A human-typed number → a `number`, or `null`.\n *\n * Strips thousands-separator commas (`\"1,234.5\"` → `1234.5`) before parsing,\n * and accepts a leading `-` for a negative value. Rejects, rather than\n * silently truncating, anything `parseFloat` would otherwise accept a PREFIX\n * of — `\"12abc\"` is not twelve, it is a typo, and `parseFloat` alone would\n * return `12` and hide that.\n */\nexport function parseNumber(input: string): number | null {\n const trimmed = input.trim();\n if (trimmed === \"\") return null;\n const stripped = trimmed.replace(/,/g, \"\");\n if (!/^-?\\d+(\\.\\d+)?$/.test(stripped)) return null;\n const value = Number(stripped);\n return Number.isFinite(value) ? value : null;\n}\n","/* ----------------------------------------------------------------------------\n A TRI-STATE BOOLEAN — `true`, `false`, or nothing recorded.\n\n `\"Unknown\"`, NOT THE EM DASH, is the default for `null`/`undefined` here —\n the one deliberate departure from the convention every other formatter in\n this file follows. The em dash reads correctly as \"no value\" for a date, a\n number, a phone number: a blank field on a form. A boolean field usually\n means something was actually ASKED — \"was the patient transported by\n ambulance,\" \"is there a police report\" — and `null` there means the\n question has not been answered yet, which is a materially different fact\n from \"answered no.\" An em dash sits visually closer to \"false-ish\" than\n \"unanswered\" does; \"Unknown\" does not. `unknownLabel` exists to opt back\n into the em dash (or anything else) for a dense table where every column\n already uses it and a differently-worded cell would stand out for the\n wrong reason — the default is chosen for a single tri-state field read on\n its own, not for a grid.\n -------------------------------------------------------------------------- */\n\nexport interface FormatBooleanOptions {\n /** Word for `true`. Default `\"Yes\"`. */\n trueLabel?: string;\n /** Word for `false`. Default `\"No\"`. */\n falseLabel?: string;\n /** Word for `null` / `undefined`. Default `\"Unknown\"` — see the module doc\n * for why this is not the em dash the rest of this package uses for\n * absence. */\n unknownLabel?: string;\n}\n\n/**\n * `true` → `\"Yes\"`, `false` → `\"No\"`, `null` / `undefined` → `\"Unknown\"`\n * (all three overridable via `options`).\n */\nexport function formatBoolean(value: boolean | null | undefined, options: FormatBooleanOptions = {}): string {\n const { trueLabel = \"Yes\", falseLabel = \"No\", unknownLabel = \"Unknown\" } = options;\n if (value === true) return trueLabel;\n if (value === false) return falseLabel;\n return unknownLabel;\n}\n\nconst TRUE_WORDS = new Set([\"yes\", \"y\", \"true\", \"t\", \"1\"]);\nconst FALSE_WORDS = new Set([\"no\", \"n\", \"false\", \"f\", \"0\"]);\n\n/**\n * A human-typed word → `true`, `false`, or `null` for anything else\n * (including an empty string — a caller cannot distinguish \"typed nothing\"\n * from \"typed a real value\" if both return `false`).\n *\n * Matches case-insensitively against the common spellings a person actually\n * types into a Yes/No field: `yes`/`y`/`true`/`t`/`1` and `no`/`n`/`false`/\n * `f`/`0`. Deliberately does NOT guess at anything outside that list — a\n * typo like `\"yse\"` returning `null` for a caller to reject is correct;\n * returning `false` for it (or any other unrecognised text) would silently\n * record the opposite of an unreadable answer.\n */\nexport function parseBoolean(input: string): boolean | null {\n const normalized = input.trim().toLowerCase();\n if (TRUE_WORDS.has(normalized)) return true;\n if (FALSE_WORDS.has(normalized)) return false;\n return null;\n}\n"],"names":["formatBytes","bytes","units","value","unit","formatTokens","tokens","formatCompactNumber","sign","abs","TIERS","threshold","suffix","AmbiguousTimeError","wall","zone","kind","isoDateTimeString","p","n","w","isoDateString","date","daysInMonth","year","month","d","parsePlainDate","iso","m","day","ABSENT","pad","width","formatDate","US_FORMAT","ISO_FORMAT","parseDate","input","trimmed","us","HOUR_MS","formatters","formatterFor","f","instantToPlainDateTime","instant","parts","hour","minute","second","part","millisecond","wallAsUtcMs","offsetAt","sameWall","a","b","instantsFor","asUtc","offsetBefore","offsetAfter","found","offset","candidate","t","i","plainDateTimeToInstant","disambiguation","candidates","gap","earlier","later","localZone","toInstant","normalized","ms","formatDateTime","options","hour12","meridiem","TYPED_FORMAT","ISO_WITH_ZONE","parseDateTime","rawHour","USD","formatCurrency","parseCurrency","accounting","negative","stripped","magnitude","formatPhone","digits","parsePhone","formatNumber","decimals","intlOptions","parseNumber","formatBoolean","trueLabel","falseLabel","unknownLabel","TRUE_WORDS","FALSE_WORDS","parseBoolean"],"mappings":"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"}
|
|
@@ -2,168 +2,99 @@ import { type Ref } from "react";
|
|
|
2
2
|
import { useRender } from "@base-ui/react/use-render";
|
|
3
3
|
/**
|
|
4
4
|
* The caller's refusal of a value, with the reason that will be shown.
|
|
5
|
-
*
|
|
6
|
-
* ONE OBJECT RATHER THAN TWO LOOSE RETURNS — `false` plus a separate message
|
|
7
|
-
* prop, say — because those two can disagree: a refusal with no sentence beside
|
|
8
|
-
* it is the mute rejection this whole component exists to prevent, and a
|
|
9
|
-
* sentence with nothing refusing it is a lie. This library already settled that
|
|
10
|
-
* shape three times over: `StatusOption.refusal`, `KanbanLane.refusal` and
|
|
11
|
-
* `SettingRow.readOnly: { reason }`.
|
|
12
|
-
*
|
|
13
|
-
* Written for the person who typed it — "a demand with this name already
|
|
14
|
-
* exists", not "invalid". It is the only thing they will see.
|
|
5
|
+
* Unchanged from 5.2.1 — see `InlineEditOutcome`.
|
|
15
6
|
*/
|
|
16
7
|
export interface InlineEditRefusal {
|
|
17
8
|
refusal: string;
|
|
18
9
|
}
|
|
19
|
-
/**
|
|
20
|
-
* What `onCommit` may report. Returning nothing accepts the value.
|
|
21
|
-
*
|
|
22
|
-
* A REFUSAL IS NOT AN EXCEPTION, and keeping the two apart is the point: a
|
|
23
|
-
* refusal is the caller's rule saying no (shown as its own sentence, the field
|
|
24
|
-
* stays open for a correction), while a thrown error or a rejected promise is
|
|
25
|
-
* the save FAILING (shown as `failureMessage`, the same text kept, retryable).
|
|
26
|
-
* Folding them together would put a stack trace's message in front of an
|
|
27
|
-
* end user, or bury a real outage as a validation note.
|
|
28
|
-
*/
|
|
10
|
+
/** What `onCommit` may report. Returning nothing accepts the value. */
|
|
29
11
|
export type InlineEditOutcome = void | InlineEditRefusal;
|
|
30
12
|
export interface InlineEditProps {
|
|
31
13
|
/**
|
|
32
14
|
* The committed value. Controlled — this component never holds the truth,
|
|
33
|
-
*
|
|
15
|
+
* only the draft while somebody is typing one.
|
|
34
16
|
*/
|
|
35
17
|
value: string;
|
|
36
18
|
/**
|
|
37
|
-
* Commit a new value. Called ONLY when the text actually differs from
|
|
38
|
-
* `value
|
|
19
|
+
* Commit a new value. Called ONLY when the live text actually differs from
|
|
20
|
+
* `value`.
|
|
39
21
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
22
|
+
* `base` is the `value` this control saw at the moment editing BEGAN —
|
|
23
|
+
* captured once and handed back unchanged regardless of how long the edit or
|
|
24
|
+
* the save takes. Compare it against whatever you currently believe is
|
|
25
|
+
* authoritative to tell "this would overwrite a change that landed while I
|
|
26
|
+
* was editing" from "nothing has moved". See the module comment
|
|
27
|
+
* ("THE OVERWRITE-PROTECTION CONTRACT") for the DX-2907 problem this closes.
|
|
43
28
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* reads to the person exactly like the local one; a rejected promise means
|
|
48
|
-
* the write did not happen for a reason nobody can act on by retyping.
|
|
49
|
-
*
|
|
50
|
-
* WHICH RESPONSES THOSE ARE IS YOUR API'S BUSINESS, and worth checking
|
|
51
|
-
* against a real one: the first integration's duplicate-name refusal arrived
|
|
52
|
-
* as HTTP 400, so a handler keyed to 422 would have shown "Could not save"
|
|
53
|
-
* for a name the server had merely declined.
|
|
29
|
+
* May return a promise, in which case the field stays open and pending until
|
|
30
|
+
* it settles. Resolve/return `{ refusal }` to reject the value with a
|
|
31
|
+
* reason; throw or reject to report that the save itself failed.
|
|
54
32
|
*/
|
|
55
|
-
onCommit: (value: string) => InlineEditOutcome | Promise<InlineEditOutcome>;
|
|
33
|
+
onCommit: (value: string, base: string) => InlineEditOutcome | Promise<InlineEditOutcome>;
|
|
56
34
|
/**
|
|
57
35
|
* The refusals that are knowable WITHOUT a request. Return the reason, or
|
|
58
|
-
* `null` to accept.
|
|
59
|
-
*
|
|
60
|
-
* Runs after the unchanged check and before `onCommit`, so a value that fails
|
|
61
|
-
* here never reaches the network — which is the whole point of it being
|
|
62
|
-
* separate: emptiness and length are decidable locally, uniqueness is not,
|
|
63
|
-
* and a client that guesses at the second one drifts from the server.
|
|
64
|
-
*
|
|
65
|
-
* A BARE STRING RATHER THAN `{ refusal }`, unlike `onCommit`'s return, and
|
|
66
|
-
* the difference is not an inconsistency: `onCommit` has to say WHICH of
|
|
67
|
-
* accepted-or-refused happened, so the reason needs a name to sit under.
|
|
68
|
-
* Everything this function returns is the reason, which is the same position
|
|
69
|
-
* `StatusOption.refusal` and `KanbanLane.refusal` are already a bare string
|
|
70
|
-
* in. An empty string throws, for the reason those do.
|
|
71
|
-
*
|
|
72
|
-
* It is shown through exactly the same channel a refusal from `onCommit` is —
|
|
73
|
-
* the person is not told where in the stack their name was turned down.
|
|
36
|
+
* `null` to accept. Runs after the unchanged check and before `onCommit`.
|
|
74
37
|
*/
|
|
75
38
|
validate?: (value: string) => string | null;
|
|
76
39
|
/**
|
|
77
|
-
* The accessible name
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* Name the FIELD ("Demand title"), not the action ("Edit") and not the
|
|
82
|
-
* current value: it is read in a list of controls where the value is
|
|
83
|
-
* announced separately.
|
|
40
|
+
* The accessible name AND accessible value's label, in both states —
|
|
41
|
+
* `role="textbox"` computes its accessible value from its own text content,
|
|
42
|
+
* so this is the one thing that has to be said separately. REQUIRED.
|
|
84
43
|
*/
|
|
85
44
|
label: string;
|
|
86
45
|
/**
|
|
87
|
-
* What the
|
|
88
|
-
* placeholder
|
|
46
|
+
* What the control shows in place of a blank value, and its
|
|
47
|
+
* `aria-placeholder`.
|
|
89
48
|
*
|
|
90
|
-
*
|
|
91
|
-
* punctuation has no name worth announcing, and one whose content is empty
|
|
92
|
-
* collapses to a few pixels — a control that cannot be hit. See
|
|
93
|
-
* `scripts/probe-card-header.mjs` for the zero-width family this belongs to.
|
|
49
|
+
* @default "Empty"
|
|
94
50
|
*/
|
|
95
51
|
placeholder?: string;
|
|
96
|
-
/** Box ramp. Matches `Input`'s names
|
|
52
|
+
/** Box ramp. Matches `Input`'s names. */
|
|
97
53
|
size?: "sm" | "md" | "lg";
|
|
98
54
|
/**
|
|
99
|
-
* Edit as a
|
|
55
|
+
* Edit as a growing multi-line block instead of a single line.
|
|
100
56
|
*
|
|
101
|
-
* THE COMMIT KEY MOVES, because Enter has to insert a newline
|
|
102
|
-
*
|
|
103
|
-
* commits.
|
|
57
|
+
* THE COMMIT KEY MOVES, because Enter has to insert a newline instead: Enter
|
|
58
|
+
* inserts "\n", Cmd/Ctrl+Enter commits, Escape still cancels, blur still
|
|
59
|
+
* commits.
|
|
104
60
|
*/
|
|
105
61
|
multiline?: boolean;
|
|
106
62
|
/**
|
|
107
|
-
* This value cannot be edited at all, AND WHY
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* that refuses in silence is indistinguishable from one that is broken.
|
|
111
|
-
*
|
|
112
|
-
* The display stays focusable and `aria-disabled`, carrying the reason as
|
|
113
|
-
* its description and in a tooltip, so it can be asked.
|
|
63
|
+
* This value cannot be edited at all, AND WHY. `{ reason }`, matching
|
|
64
|
+
* `SettingRow.readOnly` — a control that refuses in silence is
|
|
65
|
+
* indistinguishable from one that is broken.
|
|
114
66
|
*/
|
|
115
67
|
readOnly?: {
|
|
116
68
|
reason: string;
|
|
117
69
|
};
|
|
118
70
|
/**
|
|
119
|
-
* What is shown when the commit REJECTS
|
|
120
|
-
*
|
|
121
|
-
* just typed a name.
|
|
71
|
+
* What is shown when the commit REJECTS (a thrown error or a rejected
|
|
72
|
+
* promise — never a `{ refusal }`, which renders its own text).
|
|
122
73
|
*
|
|
123
74
|
* @default "Could not save. Your text is still here — press Enter to try again."
|
|
124
75
|
*/
|
|
125
76
|
failureMessage?: string;
|
|
126
|
-
/**
|
|
127
|
-
* The rejected error, handed on so it can be logged or reported. Without
|
|
128
|
-
* this the exception is swallowed entirely, which is the silent failure this
|
|
129
|
-
* library treats as a defect rather than as tidiness.
|
|
130
|
-
*/
|
|
77
|
+
/** The rejected error, handed on so it can be logged or reported. */
|
|
131
78
|
onError?: (error: unknown) => void;
|
|
132
79
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
80
|
+
* Fires exactly when editing starts and exactly when it ends — never while a
|
|
81
|
+
* read-only control merely declines to open. Use it to suspend any
|
|
82
|
+
* re-render-from-server logic of your own for as long as someone is
|
|
83
|
+
* actually looking at the field; see the module comment.
|
|
135
84
|
*/
|
|
85
|
+
onEditingChange?: (editing: boolean) => void;
|
|
86
|
+
/** Render as a different element — see `Button`'s own `render` for the shape. */
|
|
136
87
|
render?: useRender.RenderProp;
|
|
137
88
|
/**
|
|
138
|
-
* Applied
|
|
139
|
-
* that changes
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* TYPOGRAPHY REACHES BOTH FORMS AND DOES LAND — `font-medium`, `text-lg`,
|
|
144
|
-
* `tracking-tight` all work. That is worth saying because for one release it
|
|
145
|
-
* silently did not: the shared box declared `[font:inherit]` as a utility,
|
|
146
|
-
* the `font` shorthand resets weight, and Tailwind's emission order gave the
|
|
147
|
-
* shorthand the win, so `font-medium` computed as 400 with no error. The
|
|
148
|
-
* declaration now lives in `@layer components` (`dx-inline-type`), where
|
|
149
|
-
* every utility of equal specificity beats it.
|
|
89
|
+
* Applied in both states, and there is only one of these on purpose — see
|
|
90
|
+
* `box` above: anything that changes the box's geometry has to reach both
|
|
91
|
+
* states or the control shifts on click, and a control cannot shift on click
|
|
92
|
+
* when both states are the same element unless a class only reaches one of
|
|
93
|
+
* them.
|
|
150
94
|
*/
|
|
151
95
|
className?: string;
|
|
152
|
-
/**
|
|
153
|
-
* The refusal/failure message only — NOT the box.
|
|
154
|
-
*
|
|
155
|
-
* A separate prop here where `className` deliberately refuses to have one,
|
|
156
|
-
* and the difference is the coupling: the display and the field must measure
|
|
157
|
-
* identically, so their classes cannot be set apart; the message is out of
|
|
158
|
-
* flow and coupled to neither.
|
|
159
|
-
*
|
|
160
|
-
* What it is FOR: the message is absolutely positioned below the control, so
|
|
161
|
-
* an `overflow: hidden` ancestor clips it and a control at the bottom of a
|
|
162
|
-
* panel points it off the edge. Reposition it (`bottom-full mb-1` to flip
|
|
163
|
-
* above), or drop `absolute` where nothing is fixed-height and the growth is
|
|
164
|
-
* fine. Without this there was no way to do either — reported as exactly that.
|
|
165
|
-
*/
|
|
96
|
+
/** The refusal/failure message only — NOT the box. See 5.2.1's own doc for why it is a separate prop. */
|
|
166
97
|
messageClassName?: string;
|
|
167
98
|
ref?: Ref<HTMLElement>;
|
|
168
99
|
}
|
|
169
|
-
export declare function InlineEdit({ value, onCommit, validate, label, placeholder, size, multiline, readOnly, failureMessage, onError, render, className, messageClassName, ref, }: InlineEditProps): import("react").JSX.Element;
|
|
100
|
+
export declare function InlineEdit({ value, onCommit, validate, label, placeholder, size, multiline, readOnly, failureMessage, onError, onEditingChange, render, className, messageClassName, ref, }: InlineEditProps): import("react").JSX.Element;
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import { type ReactNode } from "react";
|
|
1
|
+
import { type ReactNode, type Ref } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The imperative half of the DX-2930 / DX-2907-problem-137 contract: a
|
|
4
|
+
* consumer that needs to move focus into the surface from code (a "Edit"
|
|
5
|
+
* button, a just-opened panel) gets a NAMED method instead of reaching for
|
|
6
|
+
* `data-md-editable` inside the page — the internal it was reduced to
|
|
7
|
+
* querying for before this existed. `focus()` mirrors what the toolbar's own
|
|
8
|
+
* `apply()` already does after a command: `editor.root.focus()`, the actual
|
|
9
|
+
* contenteditable host, never a proxy element.
|
|
10
|
+
*/
|
|
11
|
+
export interface MarkdownEditorHandle {
|
|
12
|
+
focus: () => void;
|
|
13
|
+
}
|
|
2
14
|
export interface MarkdownEditorProps {
|
|
3
15
|
/** The markdown. Controlled when `onChange` is given. */
|
|
4
16
|
value: string;
|
|
@@ -20,5 +32,13 @@ export interface MarkdownEditorProps {
|
|
|
20
32
|
className?: string;
|
|
21
33
|
/** Height of the editable area. Grows with content when omitted. */
|
|
22
34
|
minHeight?: number | string;
|
|
35
|
+
/**
|
|
36
|
+
* Focus the surface once, on mount — the same one-shot semantics as an
|
|
37
|
+
* `<input autoFocus>`. Ignored on `readOnly`, since there is nothing to
|
|
38
|
+
* focus into.
|
|
39
|
+
*/
|
|
40
|
+
autoFocus?: boolean;
|
|
41
|
+
/** Imperative focus handle — see `MarkdownEditorHandle`. */
|
|
42
|
+
ref?: Ref<MarkdownEditorHandle>;
|
|
23
43
|
}
|
|
24
|
-
export declare function MarkdownEditor({ value, onChange, readOnly, label, hideLabel, placeholder, toolbar, className, minHeight, }: MarkdownEditorProps): import("react").JSX.Element;
|
|
44
|
+
export declare function MarkdownEditor({ value, onChange, readOnly, label, hideLabel, placeholder, toolbar, className, minHeight, autoFocus, ref, }: MarkdownEditorProps): import("react").JSX.Element;
|