foldkit-form-recipes 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -4
- package/dist/html.d.mts.map +1 -1
- package/dist/html.mjs +3 -2
- package/dist/html.mjs.map +1 -1
- package/dist/image.d.mts.map +1 -1
- package/dist/image.mjs +11 -5
- package/dist/image.mjs.map +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-CutVZjTd.mjs → src-C-VSTaPM.mjs} +109 -30
- package/dist/src-C-VSTaPM.mjs.map +1 -0
- package/package.json +20 -20
- package/dist/src-CutVZjTd.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -91,6 +91,16 @@ A date and a time, stored in one of two forms, chosen explicitly:
|
|
|
91
91
|
read at it. A submitted value is written as `Date.prototype.toISOString` writes
|
|
92
92
|
one; any offset is read. An unknown zone throws when the recipe is made.
|
|
93
93
|
|
|
94
|
+
A stored instant is shown only as a draft that submits it again unchanged. The
|
|
95
|
+
later of a repeated hour (New York's second `01:30` on 2026-11-01) is shown with
|
|
96
|
+
its offset, `2026-11-01T01:30-05:00`, which `html` draws as text, since a
|
|
97
|
+
date-time input cannot hold it; the reading alone would submit the earlier. A
|
|
98
|
+
fill throws for an instant finer than a millisecond, or one naming no real date
|
|
99
|
+
and time (`2026-02-30T10:00:00Z`, which `Date.parse` moves into March), since
|
|
100
|
+
showing either would change it. An instant in year 0 is kept, and drawn as text
|
|
101
|
+
too: a date-time input takes no year before 1. `html`'s date-time input steps by
|
|
102
|
+
a millisecond, so seconds and milliseconds can be edited in it.
|
|
103
|
+
|
|
94
104
|
The zone is a parameter, never the runtime's, so a server and a browser fill the
|
|
95
105
|
same text. To follow the person's own zone, pass it:
|
|
96
106
|
`Intl.DateTimeFormat().resolvedOptions().timeZone`.
|
|
@@ -147,12 +157,16 @@ no floating point stands between what was typed and what is stored:
|
|
|
147
157
|
- **`decimal({ scale, locale?, negative? })`**: a number with at most `scale`
|
|
148
158
|
decimals.
|
|
149
159
|
- **`percent({ unit, scale?, locale?, negative? })`**: shown as `12.5%`, stored as
|
|
150
|
-
the share (`unit: 'fraction'`, `0.125`) or the points (`'points'`, `12.5`).
|
|
160
|
+
the share (`unit: 'fraction'`, `0.125`) or the points (`'points'`, `12.5`). It
|
|
161
|
+
groups as the locale's percentages do, which is not always as its numbers do:
|
|
162
|
+
`mr` writes `123,450%` but `1,23,450`.
|
|
151
163
|
|
|
152
164
|
What is refused rather than guessed at: grouping other than the locale's (`12,3`
|
|
153
165
|
in `en-US` is not `123`, and `12.3` in `de-DE` is not either), more decimals than
|
|
154
166
|
the scale (never rounded), a minus unless `negative: true`, a space inside an
|
|
155
|
-
amount where the locale groups with none, a symbol anywhere but at either end,
|
|
167
|
+
amount where the locale groups with none, a symbol anywhere but at either end, a
|
|
168
|
+
minus after the amount but where the locale writes it there (`fy`'s money:
|
|
169
|
+
`US$ 1.234,56-`),
|
|
156
170
|
a minor amount past what a JavaScript number holds exactly (use `'decimal'` for
|
|
157
171
|
those), and a `decimal` or `percent` a JavaScript number would round. A partial
|
|
158
172
|
amount (`-`, or `1,2` in `en-US`) reads `Enter an amount`: the form checks a key
|
|
@@ -261,8 +275,10 @@ instant reads in its recipe's zone.
|
|
|
261
275
|
`Enter a date and a time that exists`, the words for any draft that names no
|
|
262
276
|
time, a partly typed one too. A browser's `new Date` would move it an hour on
|
|
263
277
|
without saying.
|
|
264
|
-
- **A time the clocks repeat reads as the earlier
|
|
265
|
-
happens twice; it submits `00:30Z`, the first
|
|
278
|
+
- **A time the clocks repeat reads as the earlier,** unless an offset says which.
|
|
279
|
+
`2026-10-25T01:30` in London happens twice; it submits `00:30Z`, the first, and
|
|
280
|
+
`2026-10-25T01:30+00:00` submits `01:30Z`, the second. An offset the zone's
|
|
281
|
+
clocks do not have then is refused.
|
|
266
282
|
- **A year under 100 is that year.** `0050-06-01` is not read as 1950, as
|
|
267
283
|
`Date.UTC` would.
|
|
268
284
|
- **A fill with a value of the wrong form throws,** naming the form and the key: a
|
package/dist/html.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.d.mts","names":[],"sources":["../src/html.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"html.d.mts","names":[],"sources":["../src/html.ts"],"mappings":";;qBAuEa,YAAa,cAAY,UAAU"}
|
package/dist/html.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { t as FieldRecipes } from "./src-
|
|
1
|
+
import { n as readWall, t as FieldRecipes } from "./src-C-VSTaPM.mjs";
|
|
2
|
+
import { Option } from "effect";
|
|
2
3
|
// src/html.ts
|
|
3
4
|
const { Kinds } = FieldRecipes;
|
|
4
5
|
/** A native input of `type`, holding the draft as typed, with what else the kind asks. */
|
|
@@ -36,7 +37,7 @@ const amount = (of) => native("text", (context) => {
|
|
|
36
37
|
/** A renderer for each recipe kind. */
|
|
37
38
|
const renderers = () => ({
|
|
38
39
|
[Kinds.Date.kind]: native("date"),
|
|
39
|
-
[Kinds.DateTime.kind]: native("datetime-local"),
|
|
40
|
+
[Kinds.DateTime.kind]: (context) => context.draft === "" || Option.exists(readWall(String(context.draft)), (wall) => wall.year > 0) ? native("datetime-local", ({ h }) => [h.Step("0.001")])(context) : native("text")(context),
|
|
40
41
|
[Kinds.Email.kind]: native("email", (context) => literal(context, dataOf(Kinds.Email, context.control).autocomplete ?? "email")),
|
|
41
42
|
[Kinds.Url.kind]: native("url", (context) => literal(context, dataOf(Kinds.Url, context.control).autocomplete ?? "url")),
|
|
42
43
|
[Kinds.Tel.kind]: native("tel", (context) => [context.h.InputMode("tel"), context.h.Attribute("autocomplete", dataOf(Kinds.Tel, context.control).autocomplete ?? "tel")]),
|
package/dist/html.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.mjs","names":[],"sources":["../src/html.ts"],"sourcesContent":["/**\n * The plain renderers of the recipes' kinds, for `foldkit-mixins-form`'s\n * `FormView.define(form, { renderers })`, beside its own: each a native input\n * the browser already knows how to edit, named by its key so a post without\n * scripts carries it.\n */\nimport type { Control } from 'foldkit-form'\nimport type { Renderer, Renderers, RenderContext } from 'foldkit-mixins-form'\nimport type { Attribute } from 'foldkit/html'\nimport { FieldRecipes } from './index.js'\n\nconst { Kinds } = FieldRecipes\n\n/** A native input of `type`, holding the draft as typed, with what else the kind asks. */\nconst native =\n <Message>(\n type: string,\n extra: (context: RenderContext<Message>) => ReadonlyArray<Attribute<Message>> = () => [],\n ): Renderer<Message> =>\n context => {\n const { state, draft, change, blurred, slots, h } = context\n return h.input(\n slots.text.attrs([\n ...state,\n h.Type(type),\n h.Value(String(draft)),\n h.OnInput(change),\n h.OnBlur(blurred),\n ...extra(context),\n ]),\n )\n }\n\n/** What the browser may fill in, and that a keyboard must not capitalize or correct it. */\nconst literal = <Message>(\n { h }: RenderContext<Message>,\n autocomplete: string,\n): ReadonlyArray<Attribute<Message>> => [\n h.Attribute('autocomplete', autocomplete),\n h.Attribute('autocapitalize', 'none'),\n h.Attribute('spellcheck', 'false'),\n]\n\n/** The data of a control of `kind`, which the table only hands it. */\nconst dataOf = <Data>(\n kind: { readonly is: (control: Control) => control is Control<Data> },\n control: Control,\n): Data => {\n if (!kind.is(control)) throw new Error(`a ${control.kind} control under another kind's renderer`)\n return control.data\n}\n\n/**\n * An amount's text input: text, since `\"4.\"` and `\"-\"` are fine drafts that\n * `type=\"number\"` would refuse to report. Its keypad has a minus only when the\n * amount may be negative (`decimal` has none on iOS), and no point for a whole one.\n */\nconst amount = <Message>(\n of: (control: Control) => { readonly negative?: boolean | undefined; readonly scale: number },\n): Renderer<Message> =>\n native('text', context => {\n const { negative, scale } = of(context.control)\n return [\n context.h.InputMode(negative === true ? 'text' : scale === 0 ? 'numeric' : 'decimal'),\n context.h.Attribute('autocomplete', 'off'),\n ]\n })\n\n/** A renderer for each recipe kind. */\nexport const renderers = <Message>(): Renderers<Message> => ({\n [Kinds.Date.kind]: native('date'),\n [Kinds.DateTime.kind]: native('datetime-local'),\n [Kinds.Email.kind]: native('email', context =>\n literal(context, dataOf(Kinds.Email, context.control).autocomplete ?? 'email'),\n ),\n [Kinds.Url.kind]: native('url', context =>\n literal(context, dataOf(Kinds.Url, context.control).autocomplete ?? 'url'),\n ),\n [Kinds.Tel.kind]: native('tel', context => [\n context.h.InputMode('tel'),\n context.h.Attribute('autocomplete', dataOf(Kinds.Tel, context.control).autocomplete ?? 'tel'),\n ]),\n [Kinds.Password.kind]: native('password', context =>\n literal(\n context,\n dataOf(Kinds.Password, context.control).purpose === 'new'\n ? 'new-password'\n : 'current-password',\n ),\n ),\n [Kinds.Money.kind]: amount(control => dataOf(Kinds.Money, control)),\n [Kinds.Decimal.kind]: amount(control => dataOf(Kinds.Decimal, control)),\n [Kinds.Percent.kind]: amount(control => {\n const data = dataOf(Kinds.Percent, control)\n return { negative: data.negative, scale: data.scale ?? 2 }\n }),\n // The address before the slug is text beside its input, in the field's affix.\n [Kinds.Slug.kind]: context =>\n context.h.div(context.slots.group.attrs(), [\n context.h.span(context.slots.affix.attrs(), [dataOf(Kinds.Slug, context.control).prefix]),\n native<Message>('text', inner => literal(inner, 'off'))(context),\n ]),\n})\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"html.mjs","names":[],"sources":["../src/html.ts"],"sourcesContent":["/**\n * The plain renderers of the recipes' kinds, for `foldkit-mixins-form`'s\n * `FormView.define(form, { renderers })`, beside its own: each a native input\n * the browser already knows how to edit, named by its key so a post without\n * scripts carries it.\n */\nimport type { Control } from 'foldkit-form'\nimport type { Renderer, Renderers, RenderContext } from 'foldkit-mixins-form'\nimport type { Attribute } from 'foldkit/html'\nimport { Option } from 'effect'\nimport { FieldRecipes } from './index.js'\nimport { readWall } from './wall.js'\n\nconst { Kinds } = FieldRecipes\n\n/** A native input of `type`, holding the draft as typed, with what else the kind asks. */\nconst native =\n <Message>(\n type: string,\n extra: (context: RenderContext<Message>) => ReadonlyArray<Attribute<Message>> = () => [],\n ): Renderer<Message> =>\n context => {\n const { state, draft, change, blurred, slots, h } = context\n return h.input(\n slots.text.attrs([\n ...state,\n h.Type(type),\n h.Value(String(draft)),\n h.OnInput(change),\n h.OnBlur(blurred),\n ...extra(context),\n ]),\n )\n }\n\n/** What the browser may fill in, and that a keyboard must not capitalize or correct it. */\nconst literal = <Message>(\n { h }: RenderContext<Message>,\n autocomplete: string,\n): ReadonlyArray<Attribute<Message>> => [\n h.Attribute('autocomplete', autocomplete),\n h.Attribute('autocapitalize', 'none'),\n h.Attribute('spellcheck', 'false'),\n]\n\n/** The data of a control of `kind`, which the table only hands it. */\nconst dataOf = <Data>(\n kind: { readonly is: (control: Control) => control is Control<Data> },\n control: Control,\n): Data => {\n if (!kind.is(control)) throw new Error(`a ${control.kind} control under another kind's renderer`)\n return control.data\n}\n\n/**\n * An amount's text input: text, since `\"4.\"` and `\"-\"` are fine drafts that\n * `type=\"number\"` would refuse to report. Its keypad has a minus only when the\n * amount may be negative (`decimal` has none on iOS), and no point for a whole one.\n */\nconst amount = <Message>(\n of: (control: Control) => { readonly negative?: boolean | undefined; readonly scale: number },\n): Renderer<Message> =>\n native('text', context => {\n const { negative, scale } = of(context.control)\n return [\n context.h.InputMode(negative === true ? 'text' : scale === 0 ? 'numeric' : 'decimal'),\n context.h.Attribute('autocomplete', 'off'),\n ]\n })\n\n/** A renderer for each recipe kind. */\nexport const renderers = <Message>(): Renderers<Message> => ({\n [Kinds.Date.kind]: native('date'),\n // A date-time input cannot hold a draft with an offset (the later of a repeated\n // hour) or of year 0: it would show nothing, so such a draft is shown as text.\n // It steps by a millisecond, the finest a draft holds: its default minute would\n // offer no seconds, and refuse a value that has them.\n [Kinds.DateTime.kind]: context =>\n context.draft === '' || Option.exists(readWall(String(context.draft)), wall => wall.year > 0)\n ? native<Message>('datetime-local', ({ h }) => [h.Step('0.001')])(context)\n : native<Message>('text')(context),\n [Kinds.Email.kind]: native('email', context =>\n literal(context, dataOf(Kinds.Email, context.control).autocomplete ?? 'email'),\n ),\n [Kinds.Url.kind]: native('url', context =>\n literal(context, dataOf(Kinds.Url, context.control).autocomplete ?? 'url'),\n ),\n [Kinds.Tel.kind]: native('tel', context => [\n context.h.InputMode('tel'),\n context.h.Attribute('autocomplete', dataOf(Kinds.Tel, context.control).autocomplete ?? 'tel'),\n ]),\n [Kinds.Password.kind]: native('password', context =>\n literal(\n context,\n dataOf(Kinds.Password, context.control).purpose === 'new'\n ? 'new-password'\n : 'current-password',\n ),\n ),\n [Kinds.Money.kind]: amount(control => dataOf(Kinds.Money, control)),\n [Kinds.Decimal.kind]: amount(control => dataOf(Kinds.Decimal, control)),\n [Kinds.Percent.kind]: amount(control => {\n const data = dataOf(Kinds.Percent, control)\n return { negative: data.negative, scale: data.scale ?? 2 }\n }),\n // The address before the slug is text beside its input, in the field's affix.\n [Kinds.Slug.kind]: context =>\n context.h.div(context.slots.group.attrs(), [\n context.h.span(context.slots.affix.attrs(), [dataOf(Kinds.Slug, context.control).prefix]),\n native<Message>('text', inner => literal(inner, 'off'))(context),\n ]),\n})\n"],"mappings":";;;AAaA,MAAM,EAAE,UAAU;;AAGlB,MAAM,UAEF,MACA,cAAsF,CAAC,OAEzF,YAAW;CACT,MAAM,EAAE,OAAO,OAAO,QAAQ,SAAS,OAAO,MAAM;CACpD,OAAO,EAAE,MACP,MAAM,KAAK,MAAM;EACf,GAAG;EACH,EAAE,KAAK,IAAI;EACX,EAAE,MAAM,OAAO,KAAK,CAAC;EACrB,EAAE,QAAQ,MAAM;EAChB,EAAE,OAAO,OAAO;EAChB,GAAG,MAAM,OAAO;CAClB,CAAC,CACH;AACF;;AAGF,MAAM,WACJ,EAAE,KACF,iBACsC;CACtC,EAAE,UAAU,gBAAgB,YAAY;CACxC,EAAE,UAAU,kBAAkB,MAAM;CACpC,EAAE,UAAU,cAAc,OAAO;AACnC;;AAGA,MAAM,UACJ,MACA,YACS;CACT,IAAI,CAAC,KAAK,GAAG,OAAO,GAAG,MAAM,IAAI,MAAM,KAAK,QAAQ,KAAK,uCAAuC;CAChG,OAAO,QAAQ;AACjB;;;;;;AAOA,MAAM,UACJ,OAEA,OAAO,SAAQ,YAAW;CACxB,MAAM,EAAE,UAAU,UAAU,GAAG,QAAQ,OAAO;CAC9C,OAAO,CACL,QAAQ,EAAE,UAAU,aAAa,OAAO,SAAS,UAAU,IAAI,YAAY,SAAS,GACpF,QAAQ,EAAE,UAAU,gBAAgB,KAAK,CAC3C;AACF,CAAC;;AAGH,MAAa,mBAAgD;EAC1D,MAAM,KAAK,OAAO,OAAO,MAAM;EAK/B,MAAM,SAAS,QAAO,YACrB,QAAQ,UAAU,MAAM,OAAO,OAAO,SAAS,OAAO,QAAQ,KAAK,CAAC,IAAG,SAAQ,KAAK,OAAO,CAAC,IACxF,OAAgB,mBAAmB,EAAE,QAAQ,CAAC,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,IACvE,OAAgB,MAAM,CAAC,CAAC,OAAO;EACpC,MAAM,MAAM,OAAO,OAAO,UAAS,YAClC,QAAQ,SAAS,OAAO,MAAM,OAAO,QAAQ,OAAO,CAAC,CAAC,gBAAgB,OAAO,CAC/E;EACC,MAAM,IAAI,OAAO,OAAO,QAAO,YAC9B,QAAQ,SAAS,OAAO,MAAM,KAAK,QAAQ,OAAO,CAAC,CAAC,gBAAgB,KAAK,CAC3E;EACC,MAAM,IAAI,OAAO,OAAO,QAAO,YAAW,CACzC,QAAQ,EAAE,UAAU,KAAK,GACzB,QAAQ,EAAE,UAAU,gBAAgB,OAAO,MAAM,KAAK,QAAQ,OAAO,CAAC,CAAC,gBAAgB,KAAK,CAC9F,CAAC;EACA,MAAM,SAAS,OAAO,OAAO,aAAY,YACxC,QACE,SACA,OAAO,MAAM,UAAU,QAAQ,OAAO,CAAC,CAAC,YAAY,QAChD,iBACA,kBACN,CACF;EACC,MAAM,MAAM,OAAO,QAAO,YAAW,OAAO,MAAM,OAAO,OAAO,CAAC;EACjE,MAAM,QAAQ,OAAO,QAAO,YAAW,OAAO,MAAM,SAAS,OAAO,CAAC;EACrE,MAAM,QAAQ,OAAO,QAAO,YAAW;EACtC,MAAM,OAAO,OAAO,MAAM,SAAS,OAAO;EAC1C,OAAO;GAAE,UAAU,KAAK;GAAU,OAAO,KAAK,SAAS;EAAE;CAC3D,CAAC;EAEA,MAAM,KAAK,QAAO,YACjB,QAAQ,EAAE,IAAI,QAAQ,MAAM,MAAM,MAAM,GAAG,CACzC,QAAQ,EAAE,KAAK,QAAQ,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,MAAM,MAAM,QAAQ,OAAO,CAAC,CAAC,MAAM,CAAC,GACxF,OAAgB,SAAQ,UAAS,QAAQ,OAAO,KAAK,CAAC,CAAC,CAAC,OAAO,CACjE,CAAC;AACL"}
|
package/dist/image.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.d.mts","names":[],"sources":["../src/image.ts"],"mappings":";;;;;;;;;;;qBA+Ca,sCAAW;;;IAGpB,MAAI,OAAA,WAAA,MAAA;IACJ,SAAO,OAAA;IACP,QAAM,OAAA;IACN,OAAK,OAAA;;;IAEI,MAAI,OAAA,WAAA,MAAA;IAAc,QAAM,OAAA;IAAiB,QAAM,OAAA;;;IAClD,OAAK,OAAA,OAAA,aAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;YAEH,qBAAqB,YAAY;qBAEhC,YAAU,OAAA;;;;;;;;MAVnB,MAAI,OAAA,WAAA,MAAA;MACJ,SAAO,OAAA;MACP,QAAM,OAAA;MACN,OAAK,OAAA;;;MAEI,MAAI,OAAA,WAAA,MAAA;MAAc,QAAM,OAAA;MAAiB,QAAM,OAAA;;;MAClD,OAAK,OAAA,OAAA,aAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;YAUH,oBAAoB,WAAW;qBAY9B,wCAAY;;IACZ,SAAO,OAAA,gBAAA,OAAA,qCAAA,OAAA,kCAAA,OAAA;;;;;IACJ,SAAO,OAAA;IAAiB,QAAM,OAAA;IAAiB,OAAK,OAAA;;;IACtD,SAAO,OAAA;IAAiB,OAAK,OAAA,OAAA,aAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;IAC/B,SAAO,OAAA;IAAiB,QAAM,OAAA;IAAiB,QAAM,OAAA;;;;;;;;YAOrD,sBAAsB,aAAa;;qBAGlC,YAAU,MAAA;;;;;;;;;;;;;iBAcN;WACN;WACA;WACA;WACA;WACA;WACA;;WAEA,qBAAqB,KAAK;;qBAGxB,YAAY;iBAkBR,qBAAqB;;;;;WAK3B;WACA,QAAQ,QAAQ,KAAK;aACnB,UAAU,QAAQ;;WAEpB;aACE,QAAQ,kBAAkB;aAC1B,OAAO,kBAAkB;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"image.d.mts","names":[],"sources":["../src/image.ts"],"mappings":";;;;;;;;;;;qBA+Ca,sCAAW;;;IAGpB,MAAI,OAAA,WAAA,MAAA;IACJ,SAAO,OAAA;IACP,QAAM,OAAA;IACN,OAAK,OAAA;;;IAEI,MAAI,OAAA,WAAA,MAAA;IAAc,QAAM,OAAA;IAAiB,QAAM,OAAA;;;IAClD,OAAK,OAAA,OAAA,aAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;YAEH,qBAAqB,YAAY;qBAEhC,YAAU,OAAA;;;;;;;;MAVnB,MAAI,OAAA,WAAA,MAAA;MACJ,SAAO,OAAA;MACP,QAAM,OAAA;MACN,OAAK,OAAA;;;MAEI,MAAI,OAAA,WAAA,MAAA;MAAc,QAAM,OAAA;MAAiB,QAAM,OAAA;;;MAClD,OAAK,OAAA,OAAA,aAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;YAUH,oBAAoB,WAAW;qBAY9B,wCAAY;;IACZ,SAAO,OAAA,gBAAA,OAAA,qCAAA,OAAA,kCAAA,OAAA;;;;;IACJ,SAAO,OAAA;IAAiB,QAAM,OAAA;IAAiB,OAAK,OAAA;;;IACtD,SAAO,OAAA;IAAiB,OAAK,OAAA,OAAA,aAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;IAC/B,SAAO,OAAA;IAAiB,QAAM,OAAA;IAAiB,QAAM,OAAA;;;;;;;;YAOrD,sBAAsB,aAAa;;qBAGlC,YAAU,MAAA;;;;;;;;;;;;;iBAcN;WACN;WACA;WACA;WACA;WACA;WACA;;WAEA,qBAAqB,KAAK;;qBAGxB,YAAY;iBAkBR,qBAAqB;;;;;WAK3B;WACA,QAAQ,QAAQ,KAAK;aACnB,UAAU,QAAQ;;WAEpB;aACE,QAAQ,kBAAkB;aAC1B,OAAO,kBAAkB;;;;;;;;;qBA6OzB,QAAK,SACP,iBACR,YAAY,cAAc,YAAY,cAAc"}
|
package/dist/image.mjs
CHANGED
|
@@ -116,6 +116,12 @@ const imageWords = {
|
|
|
116
116
|
Unexpected: "The upload of {name} failed."
|
|
117
117
|
}
|
|
118
118
|
};
|
|
119
|
+
/**
|
|
120
|
+
* Whether the control sends `file` at all. `accept` filters the picker, not a
|
|
121
|
+
* drop, so what is no image is refused here, before its bytes go anywhere, and
|
|
122
|
+
* a retry asks again.
|
|
123
|
+
*/
|
|
124
|
+
const sendable = (file) => file.type.startsWith("image/");
|
|
119
125
|
/** The facts and the Model that holds them, whatever was in flight. */
|
|
120
126
|
const keep = (model, asset) => modifyFields(model, { upload: () => ImageUpload.Kept({ asset }) });
|
|
121
127
|
/** Nothing chosen, whatever was. */
|
|
@@ -139,7 +145,7 @@ const update = (model, message) => ImageMessage.match(message, {
|
|
|
139
145
|
return { model: Option.match(Option.fromNullishOr(dropped.outMessage), {
|
|
140
146
|
onNone: () => next,
|
|
141
147
|
onSome: UiFileDrop.OutMessage.match({
|
|
142
|
-
ReceivedFiles: ({ files }) => files[0]
|
|
148
|
+
ReceivedFiles: ({ files }) => sendable(files[0]) ? send(next, files[0]) : modifyFields(next, { upload: () => ImageUpload.Refused({
|
|
143
149
|
file: files[0],
|
|
144
150
|
reason: "Unsupported",
|
|
145
151
|
status: 0
|
|
@@ -163,10 +169,10 @@ const update = (model, message) => ImageMessage.match(message, {
|
|
|
163
169
|
model: emptied(model),
|
|
164
170
|
commands: [focus(`#${model.drop.id}`)]
|
|
165
171
|
}) }, () => ({ model })),
|
|
166
|
-
Retried: () => ImageUpload.matchOrElse(model.upload, { Refused: ({ file }) => ({
|
|
172
|
+
Retried: () => ImageUpload.matchOrElse(model.upload, { Refused: ({ file }) => sendable(file) ? {
|
|
167
173
|
model: send(model, file),
|
|
168
174
|
commands: [focus(`#${model.drop.id}-cancel`)]
|
|
169
|
-
}
|
|
175
|
+
} : { model } }, () => ({ model })),
|
|
170
176
|
Removed: () => ImageUpload.matchOrElse(model.upload, { Empty: () => ({ model }) }, () => ({
|
|
171
177
|
model: emptied(model),
|
|
172
178
|
commands: [focus(`#${model.drop.id}`)]
|
|
@@ -254,8 +260,8 @@ const view = (options, words) => Submodel.defineView((model, h) => {
|
|
|
254
260
|
h.Value(String(percentOf(loaded, total))),
|
|
255
261
|
h.AriaLabelledBy(`${id}-status`)
|
|
256
262
|
]), []), button("cancel", ImageMessage.Cancelled())],
|
|
257
|
-
Refused: () => [
|
|
258
|
-
button("retry", ImageMessage.Retried()),
|
|
263
|
+
Refused: ({ file }) => [
|
|
264
|
+
...sendable(file) ? [button("retry", ImageMessage.Retried())] : [],
|
|
259
265
|
button("remove", ImageMessage.Removed()),
|
|
260
266
|
drop
|
|
261
267
|
],
|
package/dist/image.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.mjs","names":[],"sources":["../src/image.ts"],"sourcesContent":["/**\n * `foldkit-form-recipes/image` — an image key, uploaded where it is chosen.\n *\n * The key's value is the `HashedAsset` the upload route answers. The control\n * is a Bundle: a drop zone, then the upload with its progress, which can be\n * cancelled, and a refusal, which can be retried. Only facts the route kept\n * become the value; a file on its way, or refused, is nothing entered.\n */\nimport { Effect, Match, Option, Schema, Stream } from 'effect'\nimport * as UiFileDrop from '@foldkit/ui/fileDrop'\nimport { Bundle } from 'foldkit-bundle'\nimport { Display } from 'foldkit-crud'\nimport { fillWords, Input, type BundleControl } from 'foldkit-form'\nimport { HashedAsset } from 'foldkit-media'\nimport { upload, UploadRefusal, type UploadOptions } from 'foldkit-media/browser'\nimport { Capability, Slot, Slots, SlotView, type NamedStyle } from 'foldkit-mixins'\nimport { FileDrop, type FileDropSlots } from 'foldkit-mixins-ui'\nimport * as Command from 'foldkit/command'\nimport * as Dom from 'foldkit/dom'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { defineTaggedUnion } from 'foldkit/schema'\nimport { modifyFields } from 'foldkit/struct'\nimport * as Submodel from 'foldkit/submodel'\nimport * as Subscription from 'foldkit/subscription'\nimport type { FieldRecipe } from './index.js'\n\n/** The request headers a browser sets itself, and drops from a script (Fetch, \"forbidden request-header\"). */\nconst forbidden =\n /^(?:accept-charset|accept-encoding|access-control-request-headers|access-control-request-method|connection|content-length|cookie2?|date|dnt|expect|host|keep-alive|origin|referer|set-cookie|te|trailer|transfer-encoding|upgrade|via|proxy-.*|sec-.*)$/i\n\n/** Whether `url` is an address, relative or not; `URL.canParse` is too new for some browsers. */\nconst parses = (url: string): boolean => {\n try {\n new URL(url, 'http://localhost')\n return true\n } catch {\n return false\n }\n}\n\n/** A chosen file, held while it is sent. */\nconst FileSchema = Schema.instanceOf(File)\n\n/**\n * Where the image stands. `Sending` and `Refused` hold the `File` itself, so a\n * retry sends it again; a stored draft cannot hold one, so `settled` empties them.\n */\nexport const ImageUpload = defineTaggedUnion({\n Empty: {},\n Sending: {\n file: FileSchema,\n attempt: Schema.Number,\n loaded: Schema.Number,\n total: Schema.Number,\n },\n Refused: { file: FileSchema, reason: UploadRefusal, status: Schema.Number },\n Kept: { asset: HashedAsset },\n})\nexport type ImageUpload = typeof ImageUpload.Type\n\nexport const ImageModel = Schema.Struct({\n drop: UiFileDrop.Model,\n upload: ImageUpload,\n /** How many sends were started: each one's events carry its number, so a cancelled one's are ignored. */\n attempts: Schema.Number,\n})\nexport type ImageModel = typeof ImageModel.Type\n\n// Upstream's own Message schema types its files as `Schema.Schema<File>`, whose\n// decoding services are unknown, and a Bundle's Message must need none. This is\n// the same four Messages over a `File` codec; the drop's `update` takes them as they are.\nconst DropMessage = Schema.Union([\n Schema.TaggedStruct('EnteredDragZone', {}),\n Schema.TaggedStruct('LeftDragZone', {}),\n Schema.TaggedStruct('DroppedFiles', { files: Schema.NonEmptyArray(FileSchema) }),\n Schema.TaggedStruct('DroppedNonFiles', {}),\n])\n\nexport const ImageMessage = defineMessageUnion({\n GotDrop: { message: DropMessage },\n Progressed: { attempt: Schema.Number, loaded: Schema.Number, total: Schema.Number },\n Uploaded: { attempt: Schema.Number, asset: HashedAsset },\n Failed: { attempt: Schema.Number, reason: UploadRefusal, status: Schema.Number },\n Cancelled: {},\n Retried: {},\n Removed: {},\n /** Focus moved on after a press took away the button pressed. */\n CompletedFocus: {},\n})\nexport type ImageMessage = typeof ImageMessage.Type\n\n/** Every element the control draws, with the drop zone's own two under `FileDropSlots`. */\nexport const ImageSlots = Slots.define({\n root: Slot.make({ capability: Capability.Container }),\n prompt: Slot.make({ capability: Capability.Base }),\n status: Slot.make({ capability: Capability.Base }),\n progress: Slot.make({ capability: Capability.Base }),\n cancel: Slot.make({ capability: Capability.Interactive }),\n retry: Slot.make({ capability: Capability.Interactive }),\n remove: Slot.make({ capability: Capability.Interactive }),\n})\n\n/**\n * What the control says. `{name}`, `{width}` and `{height}` are filled. Each is\n * said once, in a live region: the progress shows how far a send has gone.\n */\nexport interface ImageWords {\n readonly prompt: string\n readonly sending: string\n readonly kept: string\n readonly cancel: string\n readonly retry: string\n readonly remove: string\n /** Why a send was refused, by the route's reason. */\n readonly refused: { readonly [R in UploadRefusal]: string }\n}\n\nexport const imageWords: ImageWords = {\n prompt: 'Drop an image here, or choose one',\n sending: 'Sending {name}',\n kept: 'An image of {width} × {height}',\n cancel: 'Cancel',\n retry: 'Try again',\n remove: 'Remove',\n refused: {\n Forbidden: 'You may not upload images here.',\n Unauthenticated: 'Sign in again to upload.',\n TooLarge: '{name} is too large.',\n Unsupported: '{name} is not an image this site takes.',\n Malformed: 'The upload of {name} broke off.',\n Network: 'The upload of {name} did not reach the server.',\n Unexpected: 'The upload of {name} failed.',\n },\n}\n\nexport interface ImageOptions extends UploadOptions {\n /**\n * The drop zone's `id`, which its file input takes: unique on the page, so\n * two image keys never share one.\n */\n readonly id: string\n readonly words?: Partial<Omit<ImageWords, 'refused'>> & {\n readonly refused?: Partial<ImageWords['refused']> | undefined\n }\n readonly styles?: {\n readonly field?: NamedStyle<typeof ImageSlots> | undefined\n readonly drop?: NamedStyle<typeof FileDropSlots> | undefined\n }\n}\n\n/** The facts and the Model that holds them, whatever was in flight. */\nconst keep = (model: ImageModel, asset: HashedAsset): ImageModel =>\n modifyFields(model, { upload: () => ImageUpload.Kept({ asset }) })\n\n/** Nothing chosen, whatever was. */\nconst emptied = (model: ImageModel): ImageModel =>\n modifyFields(model, { upload: () => ImageUpload.Empty() })\n\n/** A new send of `file`: the next attempt, from nothing sent. */\nconst send = (model: ImageModel, file: File): ImageModel =>\n modifyFields(model, {\n attempts: attempts => attempts + 1,\n upload: () =>\n ImageUpload.Sending({ file, attempt: model.attempts + 1, loaded: 0, total: file.size }),\n })\n\n/** The Model when `attempt` is the send in flight, for that send's events. */\nconst sending = (\n model: ImageModel,\n attempt: number,\n next: (upload: Extract<ImageUpload, { readonly _tag: 'Sending' }>) => ImageModel,\n): ImageModel =>\n ImageUpload.matchOrElse(\n model.upload,\n { Sending: upload => (upload.attempt === attempt ? next(upload) : model) },\n () => model,\n )\n\nconst update = (\n model: ImageModel,\n message: ImageMessage,\n): {\n readonly model: ImageModel\n readonly commands?: ReadonlyArray<Command.Command<ImageMessage>>\n} =>\n ImageMessage.match(message, {\n GotDrop: ({ message }) => {\n const dropped = UiFileDrop.update(model.drop, message)\n const next = modifyFields(model, { drop: () => dropped.model })\n return {\n model: Option.match(Option.fromNullishOr(dropped.outMessage), {\n onNone: () => next,\n onSome: UiFileDrop.OutMessage.match({\n // `accept` filters the picker, not a drop: what is no image is refused here,\n // before its bytes go anywhere.\n ReceivedFiles: ({ files }) =>\n files[0].type.startsWith('image/')\n ? send(next, files[0])\n : modifyFields(next, {\n upload: () =>\n ImageUpload.Refused({ file: files[0], reason: 'Unsupported', status: 0 }),\n }),\n RejectedNonFiles: () => next,\n }),\n }),\n }\n },\n Progressed: ({ attempt, loaded, total }) => ({\n model: sending(model, attempt, upload =>\n modifyFields(model, { upload: () => ImageUpload.Sending({ ...upload, loaded, total }) }),\n ),\n }),\n Uploaded: ({ attempt, asset }) => ({\n model: sending(model, attempt, () => keep(model, asset)),\n }),\n Failed: ({ attempt, reason, status }) => ({\n model: sending(model, attempt, ({ file }) =>\n modifyFields(model, { upload: () => ImageUpload.Refused({ file, reason, status }) }),\n ),\n }),\n // Each press takes away the button pressed, so focus goes where the reader goes next.\n Cancelled: () =>\n ImageUpload.matchOrElse(\n model.upload,\n { Sending: () => ({ model: emptied(model), commands: [focus(`#${model.drop.id}`)] }) },\n () => ({ model }),\n ),\n Retried: () =>\n ImageUpload.matchOrElse(\n model.upload,\n {\n Refused: ({ file }) => ({\n model: send(model, file),\n commands: [focus(`#${model.drop.id}-cancel`)],\n }),\n },\n () => ({ model }),\n ),\n Removed: () =>\n ImageUpload.matchOrElse(model.upload, { Empty: () => ({ model }) }, () => ({\n model: emptied(model),\n commands: [focus(`#${model.drop.id}`)],\n })),\n CompletedFocus: () => ({ model }),\n })\n\nconst Focus = Command.define('FieldRecipes.Image.Focus', {\n args: { selector: Schema.String },\n messages: [ImageMessage.CompletedFocus],\n // Nothing to focus is nothing to do: the control may be gone by the next draw.\n execute: ({ selector }) =>\n Dom.focus(selector).pipe(Effect.ignore, Effect.as(ImageMessage.CompletedFocus())),\n})\nconst focus = (selector: string) => Focus({ selector })\n\n/** The send in flight, which is what the upload Subscription follows. */\nconst inFlight = Schema.Option(Schema.Struct({ attempt: Schema.Number, file: FileSchema }))\n\nconst subscriptions = (options: UploadOptions) =>\n Subscription.make<ImageModel, ImageMessage>()(entry => ({\n upload: entry(\n { sending: inFlight },\n {\n modelToDependencies: model => ({\n sending: ImageUpload.matchOrElse(\n model.upload,\n { Sending: ({ attempt, file }) => Option.some({ attempt, file }) },\n () => Option.none(),\n ),\n }),\n // A change of attempt ends this stream, and ending it aborts the request.\n dependenciesToStream: ({ sending }) =>\n Option.match(sending, {\n onNone: () => Stream.empty,\n onSome: ({ attempt, file }) =>\n upload(file, options).pipe(\n Stream.map(event =>\n Match.valueTags(event, {\n Progressed: ({ loaded, total }) =>\n ImageMessage.Progressed({ attempt, loaded, total }),\n Uploaded: ({ asset }) => ImageMessage.Uploaded({ attempt, asset }),\n }),\n ),\n Stream.catch(failed =>\n Stream.make(\n ImageMessage.Failed({ attempt, reason: failed.reason, status: failed.status }),\n ),\n ),\n ),\n }),\n },\n ),\n }))\n\nconst percentOf = (loaded: number, total: number) =>\n total === 0 ? 0 : Math.min(100, Math.round((loaded / total) * 100))\n\nconst view = (options: ImageOptions, words: ImageWords) =>\n Submodel.defineView<ImageModel, ImageMessage>((model, h) => {\n const slots = SlotView.buildersFor(\n ImageSlots,\n options.styles?.field === undefined ? [] : [options.styles.field.mixin],\n { input: model, h },\n )\n const { id } = model.drop\n const button = (slot: 'cancel' | 'retry' | 'remove', message: ImageMessage) =>\n h.button(slots[slot].attrs([h.Id(`${id}-${slot}`), h.Type('button'), h.OnClick(message)]), [\n words[slot],\n ])\n const drop = h.submodel({\n slotId: model.drop.id,\n model: model.drop,\n view: UiFileDrop.view,\n viewInputs: {\n accept: ['image/*'],\n toView: attributes => {\n const resolved = FileDrop.resolve(\n attributes,\n options.styles?.drop === undefined ? [] : [options.styles.drop.mixin],\n { h },\n )\n return h.label(resolved.root, [\n h.span(slots.prompt.attrs(), [words.prompt]),\n h.input(resolved.input),\n ])\n },\n },\n toParentMessage: message => ImageMessage.GotDrop({ message }),\n })\n // One live region, drawn in every state and first, so a reader hears each\n // change of state once, and never the percentages that move under it.\n const said = ImageUpload.match(model.upload, {\n Empty: () => '',\n Sending: ({ file }) => fillWords(words.sending, { name: file.name }),\n Refused: ({ file, reason }) => fillWords(words.refused[reason], { name: file.name }),\n Kept: ({ asset }) => fillWords(words.kept, { width: asset.width, height: asset.height }),\n })\n const status = h.p(\n slots.status.attrs([h.Id(`${id}-status`), h.Role('status'), h.AriaLive('polite')]),\n [said],\n )\n return h.div(slots.root.attrs([h.DataAttribute('upload', model.upload._tag)]), [\n status,\n ...ImageUpload.match(model.upload, {\n Empty: () => [drop],\n Sending: ({ loaded, total }) => [\n h.progress(\n slots.progress.attrs([\n h.Max('100'),\n h.Value(String(percentOf(loaded, total))),\n h.AriaLabelledBy(`${id}-status`),\n ]),\n [],\n ),\n button('cancel', ImageMessage.Cancelled()),\n ],\n Refused: () => [\n button('retry', ImageMessage.Retried()),\n button('remove', ImageMessage.Removed()),\n drop,\n ],\n Kept: () => [button('remove', ImageMessage.Removed())],\n }),\n ])\n })\n\nconst ImageDisplay = Display.kind<{}>('FieldRecipes.Image', {\n text: (_, value) =>\n Schema.is(HashedAsset)(value) ? `${value.width} × ${value.height}` : String(value),\n})\n\n/**\n * An image key: its value is the `HashedAsset` the upload route kept. The\n * Bundle uploads a file once it is chosen, through `upload` with `url` and\n * `headers`, so who may upload is still the route's `UploadPolicy`. Like every\n * Bundle control it has no `check`, and a row of a nested form cannot hold it.\n */\nexport const image = (\n options: ImageOptions,\n): FieldRecipe<BundleControl<ImageModel, ImageMessage, HashedAsset>> => {\n const words: ImageWords = {\n ...imageWords,\n ...options.words,\n refused: { ...imageWords.refused, ...options.words?.refused },\n }\n // What a browser refuses would throw at the first upload, inside the\n // Subscription, and take the runtime with it; what it drops would go unsent\n // without a word. Both are refused here, when the recipe is made.\n // The id is also a focus selector (`#cover`), so it is an identifier CSS takes.\n if (!/^[A-Za-z_][\\w-]*$/.test(options.id))\n throw new Error(`image: \"${options.id}\" is not an id a selector can name, such as \"cover\"`)\n for (const [name, value] of Object.entries(options.headers ?? {}))\n if (\n !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name) ||\n forbidden.test(name) ||\n /[\\r\\n\\0]|[^\\x00-\\xff]/.test(value)\n )\n throw new Error(`image: \"${name}\" is not a header a browser sends as given`)\n if (options.url !== undefined && !parses(options.url))\n throw new Error(`image: \"${options.url}\" is not an address`)\n const uploadOptions: UploadOptions = { url: options.url, headers: options.headers }\n const bundle = Bundle.make({\n name: 'Image',\n Model: ImageModel,\n Message: ImageMessage,\n init: () => ({\n model: {\n drop: UiFileDrop.init({ id: options.id }),\n upload: ImageUpload.Empty(),\n attempts: 0,\n },\n }),\n update,\n subscriptions: () => subscriptions(uploadOptions),\n view: view(options, words),\n })\n return {\n control: Input.bundle('FieldRecipes.Image', {\n bundle,\n value: model =>\n ImageUpload.matchOrElse(model.upload, { Kept: ({ asset }) => asset }, () => undefined),\n fill: keep,\n // A submit made while a file is on its way waits for the route's answer.\n busy: model => ImageUpload.matchOrElse(model.upload, { Sending: () => true }, () => false),\n // A stored draft holds no File: what was on its way, or refused, is gone with it.\n settled: model =>\n modifyFields(model, {\n drop: drop => (drop.isDragOver ? { ...drop, isDragOver: false } : drop),\n upload: upload =>\n ImageUpload.matchOrElse(upload, { Kept: () => upload }, () => ImageUpload.Empty()),\n }),\n }),\n display: ImageDisplay.of({}),\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"image.mjs","names":[],"sources":["../src/image.ts"],"sourcesContent":["/**\n * `foldkit-form-recipes/image` — an image key, uploaded where it is chosen.\n *\n * The key's value is the `HashedAsset` the upload route answers. The control\n * is a Bundle: a drop zone, then the upload with its progress, which can be\n * cancelled, and a refusal, which can be retried. Only facts the route kept\n * become the value; a file on its way, or refused, is nothing entered.\n */\nimport { Effect, Match, Option, Schema, Stream } from 'effect'\nimport * as UiFileDrop from '@foldkit/ui/fileDrop'\nimport { Bundle } from 'foldkit-bundle'\nimport { Display } from 'foldkit-crud'\nimport { fillWords, Input, type BundleControl } from 'foldkit-form'\nimport { HashedAsset } from 'foldkit-media'\nimport { upload, UploadRefusal, type UploadOptions } from 'foldkit-media/browser'\nimport { Capability, Slot, Slots, SlotView, type NamedStyle } from 'foldkit-mixins'\nimport { FileDrop, type FileDropSlots } from 'foldkit-mixins-ui'\nimport * as Command from 'foldkit/command'\nimport * as Dom from 'foldkit/dom'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { defineTaggedUnion } from 'foldkit/schema'\nimport { modifyFields } from 'foldkit/struct'\nimport * as Submodel from 'foldkit/submodel'\nimport * as Subscription from 'foldkit/subscription'\nimport type { FieldRecipe } from './index.js'\n\n/** The request headers a browser sets itself, and drops from a script (Fetch, \"forbidden request-header\"). */\nconst forbidden =\n /^(?:accept-charset|accept-encoding|access-control-request-headers|access-control-request-method|connection|content-length|cookie2?|date|dnt|expect|host|keep-alive|origin|referer|set-cookie|te|trailer|transfer-encoding|upgrade|via|proxy-.*|sec-.*)$/i\n\n/** Whether `url` is an address, relative or not; `URL.canParse` is too new for some browsers. */\nconst parses = (url: string): boolean => {\n try {\n new URL(url, 'http://localhost')\n return true\n } catch {\n return false\n }\n}\n\n/** A chosen file, held while it is sent. */\nconst FileSchema = Schema.instanceOf(File)\n\n/**\n * Where the image stands. `Sending` and `Refused` hold the `File` itself, so a\n * retry sends it again; a stored draft cannot hold one, so `settled` empties them.\n */\nexport const ImageUpload = defineTaggedUnion({\n Empty: {},\n Sending: {\n file: FileSchema,\n attempt: Schema.Number,\n loaded: Schema.Number,\n total: Schema.Number,\n },\n Refused: { file: FileSchema, reason: UploadRefusal, status: Schema.Number },\n Kept: { asset: HashedAsset },\n})\nexport type ImageUpload = typeof ImageUpload.Type\n\nexport const ImageModel = Schema.Struct({\n drop: UiFileDrop.Model,\n upload: ImageUpload,\n /** How many sends were started: each one's events carry its number, so a cancelled one's are ignored. */\n attempts: Schema.Number,\n})\nexport type ImageModel = typeof ImageModel.Type\n\n// Upstream's own Message schema types its files as `Schema.Schema<File>`, whose\n// decoding services are unknown, and a Bundle's Message must need none. This is\n// the same four Messages over a `File` codec; the drop's `update` takes them as they are.\nconst DropMessage = Schema.Union([\n Schema.TaggedStruct('EnteredDragZone', {}),\n Schema.TaggedStruct('LeftDragZone', {}),\n Schema.TaggedStruct('DroppedFiles', { files: Schema.NonEmptyArray(FileSchema) }),\n Schema.TaggedStruct('DroppedNonFiles', {}),\n])\n\nexport const ImageMessage = defineMessageUnion({\n GotDrop: { message: DropMessage },\n Progressed: { attempt: Schema.Number, loaded: Schema.Number, total: Schema.Number },\n Uploaded: { attempt: Schema.Number, asset: HashedAsset },\n Failed: { attempt: Schema.Number, reason: UploadRefusal, status: Schema.Number },\n Cancelled: {},\n Retried: {},\n Removed: {},\n /** Focus moved on after a press took away the button pressed. */\n CompletedFocus: {},\n})\nexport type ImageMessage = typeof ImageMessage.Type\n\n/** Every element the control draws, with the drop zone's own two under `FileDropSlots`. */\nexport const ImageSlots = Slots.define({\n root: Slot.make({ capability: Capability.Container }),\n prompt: Slot.make({ capability: Capability.Base }),\n status: Slot.make({ capability: Capability.Base }),\n progress: Slot.make({ capability: Capability.Base }),\n cancel: Slot.make({ capability: Capability.Interactive }),\n retry: Slot.make({ capability: Capability.Interactive }),\n remove: Slot.make({ capability: Capability.Interactive }),\n})\n\n/**\n * What the control says. `{name}`, `{width}` and `{height}` are filled. Each is\n * said once, in a live region: the progress shows how far a send has gone.\n */\nexport interface ImageWords {\n readonly prompt: string\n readonly sending: string\n readonly kept: string\n readonly cancel: string\n readonly retry: string\n readonly remove: string\n /** Why a send was refused, by the route's reason. */\n readonly refused: { readonly [R in UploadRefusal]: string }\n}\n\nexport const imageWords: ImageWords = {\n prompt: 'Drop an image here, or choose one',\n sending: 'Sending {name}',\n kept: 'An image of {width} × {height}',\n cancel: 'Cancel',\n retry: 'Try again',\n remove: 'Remove',\n refused: {\n Forbidden: 'You may not upload images here.',\n Unauthenticated: 'Sign in again to upload.',\n TooLarge: '{name} is too large.',\n Unsupported: '{name} is not an image this site takes.',\n Malformed: 'The upload of {name} broke off.',\n Network: 'The upload of {name} did not reach the server.',\n Unexpected: 'The upload of {name} failed.',\n },\n}\n\nexport interface ImageOptions extends UploadOptions {\n /**\n * The drop zone's `id`, which its file input takes: unique on the page, so\n * two image keys never share one.\n */\n readonly id: string\n readonly words?: Partial<Omit<ImageWords, 'refused'>> & {\n readonly refused?: Partial<ImageWords['refused']> | undefined\n }\n readonly styles?: {\n readonly field?: NamedStyle<typeof ImageSlots> | undefined\n readonly drop?: NamedStyle<typeof FileDropSlots> | undefined\n }\n}\n\n/**\n * Whether the control sends `file` at all. `accept` filters the picker, not a\n * drop, so what is no image is refused here, before its bytes go anywhere, and\n * a retry asks again.\n */\nconst sendable = (file: File): boolean => file.type.startsWith('image/')\n\n/** The facts and the Model that holds them, whatever was in flight. */\nconst keep = (model: ImageModel, asset: HashedAsset): ImageModel =>\n modifyFields(model, { upload: () => ImageUpload.Kept({ asset }) })\n\n/** Nothing chosen, whatever was. */\nconst emptied = (model: ImageModel): ImageModel =>\n modifyFields(model, { upload: () => ImageUpload.Empty() })\n\n/** A new send of `file`: the next attempt, from nothing sent. */\nconst send = (model: ImageModel, file: File): ImageModel =>\n modifyFields(model, {\n attempts: attempts => attempts + 1,\n upload: () =>\n ImageUpload.Sending({ file, attempt: model.attempts + 1, loaded: 0, total: file.size }),\n })\n\n/** The Model when `attempt` is the send in flight, for that send's events. */\nconst sending = (\n model: ImageModel,\n attempt: number,\n next: (upload: Extract<ImageUpload, { readonly _tag: 'Sending' }>) => ImageModel,\n): ImageModel =>\n ImageUpload.matchOrElse(\n model.upload,\n { Sending: upload => (upload.attempt === attempt ? next(upload) : model) },\n () => model,\n )\n\nconst update = (\n model: ImageModel,\n message: ImageMessage,\n): {\n readonly model: ImageModel\n readonly commands?: ReadonlyArray<Command.Command<ImageMessage>>\n} =>\n ImageMessage.match(message, {\n GotDrop: ({ message }) => {\n const dropped = UiFileDrop.update(model.drop, message)\n const next = modifyFields(model, { drop: () => dropped.model })\n return {\n model: Option.match(Option.fromNullishOr(dropped.outMessage), {\n onNone: () => next,\n onSome: UiFileDrop.OutMessage.match({\n ReceivedFiles: ({ files }) =>\n sendable(files[0])\n ? send(next, files[0])\n : modifyFields(next, {\n upload: () =>\n ImageUpload.Refused({ file: files[0], reason: 'Unsupported', status: 0 }),\n }),\n RejectedNonFiles: () => next,\n }),\n }),\n }\n },\n Progressed: ({ attempt, loaded, total }) => ({\n model: sending(model, attempt, upload =>\n modifyFields(model, { upload: () => ImageUpload.Sending({ ...upload, loaded, total }) }),\n ),\n }),\n Uploaded: ({ attempt, asset }) => ({\n model: sending(model, attempt, () => keep(model, asset)),\n }),\n Failed: ({ attempt, reason, status }) => ({\n model: sending(model, attempt, ({ file }) =>\n modifyFields(model, { upload: () => ImageUpload.Refused({ file, reason, status }) }),\n ),\n }),\n // Each press takes away the button pressed, so focus goes where the reader goes next.\n Cancelled: () =>\n ImageUpload.matchOrElse(\n model.upload,\n { Sending: () => ({ model: emptied(model), commands: [focus(`#${model.drop.id}`)] }) },\n () => ({ model }),\n ),\n Retried: () =>\n ImageUpload.matchOrElse(\n model.upload,\n {\n Refused: ({ file }) =>\n sendable(file)\n ? { model: send(model, file), commands: [focus(`#${model.drop.id}-cancel`)] }\n : { model },\n },\n () => ({ model }),\n ),\n Removed: () =>\n ImageUpload.matchOrElse(model.upload, { Empty: () => ({ model }) }, () => ({\n model: emptied(model),\n commands: [focus(`#${model.drop.id}`)],\n })),\n CompletedFocus: () => ({ model }),\n })\n\nconst Focus = Command.define('FieldRecipes.Image.Focus', {\n args: { selector: Schema.String },\n messages: [ImageMessage.CompletedFocus],\n // Nothing to focus is nothing to do: the control may be gone by the next draw.\n execute: ({ selector }) =>\n Dom.focus(selector).pipe(Effect.ignore, Effect.as(ImageMessage.CompletedFocus())),\n})\nconst focus = (selector: string) => Focus({ selector })\n\n/** The send in flight, which is what the upload Subscription follows. */\nconst inFlight = Schema.Option(Schema.Struct({ attempt: Schema.Number, file: FileSchema }))\n\nconst subscriptions = (options: UploadOptions) =>\n Subscription.make<ImageModel, ImageMessage>()(entry => ({\n upload: entry(\n { sending: inFlight },\n {\n modelToDependencies: model => ({\n sending: ImageUpload.matchOrElse(\n model.upload,\n { Sending: ({ attempt, file }) => Option.some({ attempt, file }) },\n () => Option.none(),\n ),\n }),\n // A change of attempt ends this stream, and ending it aborts the request.\n dependenciesToStream: ({ sending }) =>\n Option.match(sending, {\n onNone: () => Stream.empty,\n onSome: ({ attempt, file }) =>\n upload(file, options).pipe(\n Stream.map(event =>\n Match.valueTags(event, {\n Progressed: ({ loaded, total }) =>\n ImageMessage.Progressed({ attempt, loaded, total }),\n Uploaded: ({ asset }) => ImageMessage.Uploaded({ attempt, asset }),\n }),\n ),\n Stream.catch(failed =>\n Stream.make(\n ImageMessage.Failed({ attempt, reason: failed.reason, status: failed.status }),\n ),\n ),\n ),\n }),\n },\n ),\n }))\n\nconst percentOf = (loaded: number, total: number) =>\n total === 0 ? 0 : Math.min(100, Math.round((loaded / total) * 100))\n\nconst view = (options: ImageOptions, words: ImageWords) =>\n Submodel.defineView<ImageModel, ImageMessage>((model, h) => {\n const slots = SlotView.buildersFor(\n ImageSlots,\n options.styles?.field === undefined ? [] : [options.styles.field.mixin],\n { input: model, h },\n )\n const { id } = model.drop\n const button = (slot: 'cancel' | 'retry' | 'remove', message: ImageMessage) =>\n h.button(slots[slot].attrs([h.Id(`${id}-${slot}`), h.Type('button'), h.OnClick(message)]), [\n words[slot],\n ])\n const drop = h.submodel({\n slotId: model.drop.id,\n model: model.drop,\n view: UiFileDrop.view,\n viewInputs: {\n accept: ['image/*'],\n toView: attributes => {\n const resolved = FileDrop.resolve(\n attributes,\n options.styles?.drop === undefined ? [] : [options.styles.drop.mixin],\n { h },\n )\n return h.label(resolved.root, [\n h.span(slots.prompt.attrs(), [words.prompt]),\n h.input(resolved.input),\n ])\n },\n },\n toParentMessage: message => ImageMessage.GotDrop({ message }),\n })\n // One live region, drawn in every state and first, so a reader hears each\n // change of state once, and never the percentages that move under it.\n const said = ImageUpload.match(model.upload, {\n Empty: () => '',\n Sending: ({ file }) => fillWords(words.sending, { name: file.name }),\n Refused: ({ file, reason }) => fillWords(words.refused[reason], { name: file.name }),\n Kept: ({ asset }) => fillWords(words.kept, { width: asset.width, height: asset.height }),\n })\n const status = h.p(\n slots.status.attrs([h.Id(`${id}-status`), h.Role('status'), h.AriaLive('polite')]),\n [said],\n )\n return h.div(slots.root.attrs([h.DataAttribute('upload', model.upload._tag)]), [\n status,\n ...ImageUpload.match(model.upload, {\n Empty: () => [drop],\n Sending: ({ loaded, total }) => [\n h.progress(\n slots.progress.attrs([\n h.Max('100'),\n h.Value(String(percentOf(loaded, total))),\n h.AriaLabelledBy(`${id}-status`),\n ]),\n [],\n ),\n button('cancel', ImageMessage.Cancelled()),\n ],\n // A file refused before it was sent would be refused again: no retry for it.\n Refused: ({ file }) => [\n ...(sendable(file) ? [button('retry', ImageMessage.Retried())] : []),\n button('remove', ImageMessage.Removed()),\n drop,\n ],\n Kept: () => [button('remove', ImageMessage.Removed())],\n }),\n ])\n })\n\nconst ImageDisplay = Display.kind<{}>('FieldRecipes.Image', {\n text: (_, value) =>\n Schema.is(HashedAsset)(value) ? `${value.width} × ${value.height}` : String(value),\n})\n\n/**\n * An image key: its value is the `HashedAsset` the upload route kept. The\n * Bundle uploads a file once it is chosen, through `upload` with `url` and\n * `headers`, so who may upload is still the route's `UploadPolicy`. Like every\n * Bundle control it has no `check`, and a row of a nested form cannot hold it.\n */\nexport const image = (\n options: ImageOptions,\n): FieldRecipe<BundleControl<ImageModel, ImageMessage, HashedAsset>> => {\n const words: ImageWords = {\n ...imageWords,\n ...options.words,\n refused: { ...imageWords.refused, ...options.words?.refused },\n }\n // What a browser refuses would throw at the first upload, inside the\n // Subscription, and take the runtime with it; what it drops would go unsent\n // without a word. Both are refused here, when the recipe is made.\n // The id is also a focus selector (`#cover`), so it is an identifier CSS takes.\n if (!/^[A-Za-z_][\\w-]*$/.test(options.id))\n throw new Error(`image: \"${options.id}\" is not an id a selector can name, such as \"cover\"`)\n for (const [name, value] of Object.entries(options.headers ?? {}))\n if (\n !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name) ||\n forbidden.test(name) ||\n /[\\r\\n\\0]|[^\\x00-\\xff]/.test(value)\n )\n throw new Error(`image: \"${name}\" is not a header a browser sends as given`)\n if (options.url !== undefined && !parses(options.url))\n throw new Error(`image: \"${options.url}\" is not an address`)\n const uploadOptions: UploadOptions = { url: options.url, headers: options.headers }\n const bundle = Bundle.make({\n name: 'Image',\n Model: ImageModel,\n Message: ImageMessage,\n init: () => ({\n model: {\n drop: UiFileDrop.init({ id: options.id }),\n upload: ImageUpload.Empty(),\n attempts: 0,\n },\n }),\n update,\n subscriptions: () => subscriptions(uploadOptions),\n view: view(options, words),\n })\n return {\n control: Input.bundle('FieldRecipes.Image', {\n bundle,\n value: model =>\n ImageUpload.matchOrElse(model.upload, { Kept: ({ asset }) => asset }, () => undefined),\n fill: keep,\n // A submit made while a file is on its way waits for the route's answer.\n busy: model => ImageUpload.matchOrElse(model.upload, { Sending: () => true }, () => false),\n // A stored draft holds no File: what was on its way, or refused, is gone with it.\n settled: model =>\n modifyFields(model, {\n drop: drop => (drop.isDragOver ? { ...drop, isDragOver: false } : drop),\n upload: upload =>\n ImageUpload.matchOrElse(upload, { Kept: () => upload }, () => ImageUpload.Empty()),\n }),\n }),\n display: ImageDisplay.of({}),\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;iBAqBiB,YAAY,UAAU,eAAe,gBAAgB;;WAE3D,SAAS;;WAET,SAAS;;;iBAIH;WACN;;;;;;;;YA2CC;WACG;;WACA;WAA4B;;KAEtC,eAAe,SAAS;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;iBAqBiB,YAAY,UAAU,eAAe,gBAAgB;;WAE3D,SAAS;;WAET,SAAS;;;iBAIH;WACN;;;;;;;;YA2CC;WACG;;WACA;WAA4B;;KAEtC,eAAe,SAAS;;iBAmFZ;WACN;;;iBAeM,gBAAgB;WACtB,YAAY;;;iBAqBN;WACN;;;iBAMM;WACN;;cAwBL,UAAO;;iBAaI;WACN;;;;;;;;iBASM,qBAAqB,QAAQ;;WAEnC;WACA;;KAGN,YAAY;WAA0B;;;iBA8E1B,uBAAuB,QAAQ;WACrC;;;;;;;iBA8EM,uBAAuB,QAAQ;WACrC;WACA;;qBA+DE;;;IAGT,6BAAI,YAAA;IACJ,iCAAQ,YAAA;IACR,8BAAK,YAAA;IACL,4BAAG,YAAA;IACH,4BAAG,YAAA;IACH,iCAAQ,YAAA;IACR,6BAAI,YAAA;IACJ,8BAAK,YAAA;IACL,gCAAO,YAAA;IACP,gCAAO,YAAA;;;;IAIP,6BAAI,YAAA;IACJ,iCAAQ,YAAA;IACR,8BAAK,YAAA;IACL,gCAAO,YAAA;IACP,gCAAO,YAAA;;;;;;;EAQO,OAAA,UAAA,WAAc,YAAY,QAAQ;;;;;;EAU9B,WAAA,SAAA,SAAS,WAAS,YAAY,QAAQ;;;;;EASzC,QAAA,UAAA,aAAgB,YAAY,QAAQ;;EAMtC,MAAA,UAAA,YAAe,YAAY,QAAQ;;EAWnC,MAAA,UAAA,aAAgB,YAAY,QAAQ;;;;;;EAU/B,WAAA,SAAA,YAAU,YAAY,QAAQ;;;;;;EAW1C,OAAA,cAAM;aACQ;aAA0B,WAAW;QACxD,YAAY;;;;;;;;;;EA0BE,QAAA,SAAA,iBAAe,YAAY,QAAQ;;EAejC,UAAA,SAAA,mBAAiB,YAAY,QAAQ;;EAMrC,UAAA,SAAA,mBAAiB,YAAY,QAAQ"}
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as FieldRecipes } from "./src-
|
|
1
|
+
export { t as FieldRecipes } from "./src-C-VSTaPM.mjs";
|
|
@@ -10,6 +10,7 @@ import { isCalendarDate } from "foldkit/calendar";
|
|
|
10
10
|
* design: a draft that could mean two things is refused, never guessed at.
|
|
11
11
|
*/
|
|
12
12
|
const minus = /^[-−]/;
|
|
13
|
+
const minusLast = /[-−]$/;
|
|
13
14
|
const apostrophe = /^['’]$/;
|
|
14
15
|
const apostrophes = /['’]/g;
|
|
15
16
|
const spaces = /[\s\u00a0\u202f]/;
|
|
@@ -19,6 +20,21 @@ const allSpaces = /[\s\u00a0\u202f]/g;
|
|
|
19
20
|
const bidi = /[\u061c\u200e\u200f\u2066-\u2069]/g;
|
|
20
21
|
/** The full-width forms a keyboard in East Asia types: `12,3`. Only these are folded. */
|
|
21
22
|
const fullWidth = /[\uff01-\uff5e\uffe0-\uffe6]/g;
|
|
23
|
+
/** `text` without a minus at its start, or at its end where the locale writes one there. */
|
|
24
|
+
const unsigned = (text, minusAfter) => {
|
|
25
|
+
if (minus.test(text)) return {
|
|
26
|
+
signed: true,
|
|
27
|
+
text: text.slice(1).trim()
|
|
28
|
+
};
|
|
29
|
+
if (minusAfter && minusLast.test(text)) return {
|
|
30
|
+
signed: true,
|
|
31
|
+
text: text.slice(0, -1).trim()
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
signed: false,
|
|
35
|
+
text
|
|
36
|
+
};
|
|
37
|
+
};
|
|
22
38
|
const symbolsCache = /* @__PURE__ */ new Map();
|
|
23
39
|
/**
|
|
24
40
|
* The symbols of `options` in `locale`, Latin digits always: a draft is typed on
|
|
@@ -39,7 +55,6 @@ const symbolsOf = (locale, options) => {
|
|
|
39
55
|
const of = (type) => parts.find((part) => part.type === type)?.value;
|
|
40
56
|
const whole = new Intl.NumberFormat(locale, {
|
|
41
57
|
...options,
|
|
42
|
-
...options.style === "percent" ? { style: "decimal" } : {},
|
|
43
58
|
numberingSystem: "latn",
|
|
44
59
|
useGrouping: true,
|
|
45
60
|
minimumFractionDigits: 0,
|
|
@@ -49,7 +64,12 @@ const symbolsOf = (locale, options) => {
|
|
|
49
64
|
group: (of("group") ?? ",").normalize("NFKC"),
|
|
50
65
|
decimal: (of("decimal") ?? ".").normalize("NFKC"),
|
|
51
66
|
affixes: parts.filter((part) => part.type === "currency" || part.type === "percentSign").map((part) => part.value.normalize("NFKC")),
|
|
52
|
-
|
|
67
|
+
minusAfter: parts.findIndex((part) => part.type === "minusSign") > parts.findIndex((part) => part.type === "integer"),
|
|
68
|
+
grouped: (digits) => whole.formatToParts(options.style === "percent" ? decimalText(shifted({
|
|
69
|
+
negative: false,
|
|
70
|
+
whole: digits,
|
|
71
|
+
fraction: ""
|
|
72
|
+
}, 2)) : digits).filter((part) => part.type === "integer" || part.type === "group").map((part) => part.value).join("").normalize("NFKC")
|
|
53
73
|
};
|
|
54
74
|
symbolsCache.set(key, made);
|
|
55
75
|
return made;
|
|
@@ -59,20 +79,22 @@ const symbolsOf = (locale, options) => {
|
|
|
59
79
|
* none for text that is not an amount, has more than `scale` fraction digits,
|
|
60
80
|
* groups its digits other than the locale does (`12,3` in `en-US` is no
|
|
61
81
|
* thousand), or is negative where `negative` is false. A currency symbol or a
|
|
62
|
-
* percent sign is taken at either end only, and a minus before or after it
|
|
82
|
+
* percent sign is taken at either end only, and a minus before or after it, or
|
|
83
|
+
* last where the locale writes it last.
|
|
63
84
|
*/
|
|
64
85
|
const readDigits = (draft, locale, options, scale, negative) => {
|
|
65
86
|
const symbols = symbolsOf(locale, options);
|
|
66
87
|
let text = draft.replace(fullWidth, (form) => form.normalize("NFKC")).replace(bidi, "").trim();
|
|
67
|
-
|
|
68
|
-
|
|
88
|
+
const outer = unsigned(text, symbols.minusAfter);
|
|
89
|
+
text = outer.text;
|
|
69
90
|
const affix = symbols.affixes.find((held) => text.startsWith(held) || text.endsWith(held));
|
|
70
91
|
if (affix !== void 0) text = (text.startsWith(affix) ? text.slice(affix.length) : text.slice(0, -affix.length)).trim();
|
|
71
|
-
|
|
72
|
-
signed
|
|
73
|
-
text
|
|
74
|
-
}
|
|
75
|
-
|
|
92
|
+
const inner = outer.signed ? {
|
|
93
|
+
signed: true,
|
|
94
|
+
text
|
|
95
|
+
} : unsigned(text, symbols.minusAfter);
|
|
96
|
+
text = inner.text;
|
|
97
|
+
if (inner.signed && !negative) return Option.none();
|
|
76
98
|
if (spaces.test(symbols.group)) text = text.replace(allSpaces, symbols.group);
|
|
77
99
|
else if (spaces.test(text)) return Option.none();
|
|
78
100
|
if (apostrophe.test(symbols.group)) text = text.replace(apostrophes, symbols.group);
|
|
@@ -83,7 +105,7 @@ const readDigits = (draft, locale, options, scale, negative) => {
|
|
|
83
105
|
if (!/^\d*$/.test(digits)) return Option.none();
|
|
84
106
|
if (digits !== whole && symbols.grouped(digits) !== whole) return Option.none();
|
|
85
107
|
return Option.some({
|
|
86
|
-
negative: signed,
|
|
108
|
+
negative: inner.signed,
|
|
87
109
|
whole: digits.replace(/^0+(?=\d)/, "") || "0",
|
|
88
110
|
fraction: fraction.padEnd(scale, "0")
|
|
89
111
|
});
|
|
@@ -189,6 +211,7 @@ const formatterOf = (zone) => {
|
|
|
189
211
|
const made = new Intl.DateTimeFormat("en-US", {
|
|
190
212
|
timeZone: zone,
|
|
191
213
|
hourCycle: "h23",
|
|
214
|
+
era: "short",
|
|
192
215
|
year: "numeric",
|
|
193
216
|
month: "numeric",
|
|
194
217
|
day: "numeric",
|
|
@@ -213,7 +236,7 @@ const knownZone = (zone, who) => {
|
|
|
213
236
|
const wallOf = (instant, zone) => {
|
|
214
237
|
const parts = Object.fromEntries(formatterOf(zone).formatToParts(new Date(instant)).map((part) => [part.type, part.value]));
|
|
215
238
|
return {
|
|
216
|
-
year: Number(parts["year"]),
|
|
239
|
+
year: parts["era"] === "BC" ? 1 - Number(parts["year"]) : Number(parts["year"]),
|
|
217
240
|
month: Number(parts["month"]),
|
|
218
241
|
day: Number(parts["day"]),
|
|
219
242
|
hour: Number(parts["hour"]),
|
|
@@ -234,6 +257,56 @@ const instantOf = (wall, zone) => {
|
|
|
234
257
|
const instants = [...new Set([local - day, local + day].map((around) => utcOf(wallOf(around, zone)) - around))].map((offset) => local - offset).filter((instant) => utcOf(wallOf(instant, zone)) === local);
|
|
235
258
|
return instants.length === 0 ? Option.none() : Option.some(Math.min(...instants));
|
|
236
259
|
};
|
|
260
|
+
const offsetText = /^([+-])(\d{2}):(\d{2})$/;
|
|
261
|
+
/** The milliseconds `Z` or `±HH:MM` is ahead of UTC, or none for an offset past ±23:59. */
|
|
262
|
+
const offsetOf = (text) => {
|
|
263
|
+
if (text === "Z") return Option.some(0);
|
|
264
|
+
const parts = offsetText.exec(text);
|
|
265
|
+
if (parts === null) return Option.none();
|
|
266
|
+
const [, sign, hours = "", minutes = ""] = parts;
|
|
267
|
+
if (Number(hours) > 23 || Number(minutes) > 59) return Option.none();
|
|
268
|
+
return Option.some((sign === "-" ? -1 : 1) * (Number(hours) * 60 + Number(minutes)) * 6e4);
|
|
269
|
+
};
|
|
270
|
+
const writeOffset = (offset) => {
|
|
271
|
+
const minutes = Math.abs(offset) / 6e4;
|
|
272
|
+
return `${offset < 0 ? "-" : "+"}${two$1(Math.floor(minutes / 60))}:${two$1(minutes % 60)}`;
|
|
273
|
+
};
|
|
274
|
+
const isoInstant = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?)(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})$/;
|
|
275
|
+
/**
|
|
276
|
+
* The instant stored ISO text names, or none for text that is not one, names
|
|
277
|
+
* no real date and time, or is finer than a millisecond: a reading holds no
|
|
278
|
+
* finer, so showing such an instant would change it.
|
|
279
|
+
*/
|
|
280
|
+
const readInstant = (text) => {
|
|
281
|
+
const parts = isoInstant.exec(text);
|
|
282
|
+
if (parts === null) return Option.none();
|
|
283
|
+
const [, reading = "", fraction = "", offset = ""] = parts;
|
|
284
|
+
if (/[1-9]/.test(fraction.slice(3))) return Option.none();
|
|
285
|
+
return Option.flatMap(readWall(fraction === "" ? reading : `${reading}.${fraction.slice(0, 3)}`), (wall) => Option.map(offsetOf(offset), (ahead) => utcOf(wall) - ahead));
|
|
286
|
+
};
|
|
287
|
+
const withOffset = /^(.*)([+-]\d{2}:\d{2})$/;
|
|
288
|
+
/**
|
|
289
|
+
* The instant a draft names in `zone`. A reading alone names the one instant
|
|
290
|
+
* the clocks read so, or the earlier of two they repeat; one with an offset
|
|
291
|
+
* (`2026-11-01T01:30-05:00`) names the instant of that offset, when the clocks
|
|
292
|
+
* read so then. None for a time they skip, or an offset they do not have there.
|
|
293
|
+
*/
|
|
294
|
+
const instantIn = (draft, zone) => {
|
|
295
|
+
const parts = withOffset.exec(draft);
|
|
296
|
+
if (parts === null) return Option.flatMap(readWall(draft), (wall) => instantOf(wall, zone));
|
|
297
|
+
const [, reading = "", offset = ""] = parts;
|
|
298
|
+
return Option.flatMap(readWall(reading), (wall) => Option.filter(Option.map(offsetOf(offset), (ahead) => utcOf(wall) - ahead), (instant) => utcOf(wallOf(instant, zone)) === utcOf(wall)));
|
|
299
|
+
};
|
|
300
|
+
/**
|
|
301
|
+
* The draft of an instant in `zone`: what the clocks read, with the offset
|
|
302
|
+
* only where the reading alone would name another instant, the later of a
|
|
303
|
+
* repeated hour, so that it reads back as the instant it shows.
|
|
304
|
+
*/
|
|
305
|
+
const draftIn = (instant, zone) => {
|
|
306
|
+
const wall = wallOf(instant, zone);
|
|
307
|
+
const reading = writeWall(wall);
|
|
308
|
+
return Option.contains(instantIn(reading, zone), instant) ? reading : `${reading}${writeOffset(utcOf(wall) - instant)}`;
|
|
309
|
+
};
|
|
237
310
|
// src/index.ts
|
|
238
311
|
/**
|
|
239
312
|
* `foldkit-form-recipes` — field recipes for `foldkit-form`.
|
|
@@ -281,7 +354,6 @@ const shown = {
|
|
|
281
354
|
dateStyle: "medium",
|
|
282
355
|
timeStyle: "short"
|
|
283
356
|
};
|
|
284
|
-
const instantText = /^\d{4,}-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d+)?)?(?:Z|[+-]\d\d:\d\d)$/;
|
|
285
357
|
const local = {
|
|
286
358
|
parse: (draft) => Option.as(readWall(draft), draft),
|
|
287
359
|
format: (value) => {
|
|
@@ -296,21 +368,28 @@ const local = {
|
|
|
296
368
|
}).format(utcOf(wall))
|
|
297
369
|
})
|
|
298
370
|
};
|
|
299
|
-
const instant = (zone) =>
|
|
300
|
-
parse
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
|
|
371
|
+
const instant = (zone) => {
|
|
372
|
+
const parse = (draft) => Option.map(Option.filter(instantIn(draft, zone), (at) => new Date(at).getUTCFullYear() <= 9999), (at) => new Date(at).toISOString());
|
|
373
|
+
return {
|
|
374
|
+
parse,
|
|
375
|
+
format: (value) => Option.match(Option.flatMap(typeof value === "string" ? readInstant(value) : Option.none(), (at) => {
|
|
376
|
+
const draft = draftIn(at, zone);
|
|
377
|
+
return Option.contains(parse(draft), new Date(at).toISOString()) ? Option.some(draft) : Option.none();
|
|
378
|
+
}), {
|
|
379
|
+
onSome: (draft) => draft,
|
|
380
|
+
onNone: () => {
|
|
381
|
+
throw new Error(`an instant is ISO text with an offset, naming a real date and time to the millisecond that ${zone} can show, not ${JSON.stringify(value)}`);
|
|
382
|
+
}
|
|
383
|
+
}),
|
|
384
|
+
text: (value, locale) => Option.match(readInstant(value), {
|
|
385
|
+
onNone: () => value,
|
|
386
|
+
onSome: (at) => dateFormat(locale, {
|
|
387
|
+
...shown,
|
|
388
|
+
timeZone: zone
|
|
389
|
+
}).format(at)
|
|
390
|
+
})
|
|
391
|
+
};
|
|
392
|
+
};
|
|
314
393
|
const modeOf = (data) => Match.value(data).pipe(Match.discriminatorsExhaustive("stored")({
|
|
315
394
|
local: () => local,
|
|
316
395
|
instant: ({ zone }) => instant(zone)
|
|
@@ -595,6 +674,6 @@ const FieldRecipes = {
|
|
|
595
674
|
};
|
|
596
675
|
}
|
|
597
676
|
};
|
|
598
|
-
export { FieldRecipes as t };
|
|
677
|
+
export { readWall as n, FieldRecipes as t };
|
|
599
678
|
|
|
600
|
-
//# sourceMappingURL=src-
|
|
679
|
+
//# sourceMappingURL=src-C-VSTaPM.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src-C-VSTaPM.mjs","names":["two"],"sources":["../src/number.ts","../src/wall.ts","../src/index.ts"],"sourcesContent":["/**\n * Reading and writing amounts as a locale writes them, without floating point:\n * a typed amount becomes its digits and sign, and an amount is written from a\n * decimal text, which `Intl.NumberFormat` formats exactly. Conservative by\n * design: a draft that could mean two things is refused, never guessed at.\n */\nimport { Option } from 'effect'\n\n/** An amount read from a draft: its sign, whole digits, and exactly `scale` fraction digits. */\nexport interface Digits {\n readonly negative: boolean\n readonly whole: string\n readonly fraction: string\n}\n\n/** What a locale writes around and between the digits of one kind of amount. */\ninterface Symbols {\n readonly group: string\n readonly decimal: string\n /** Text that is not part of the amount: a currency symbol, a percent sign. */\n readonly affixes: ReadonlyArray<string>\n /** Whether the format writes a minus after the digits, as `fy`'s money does: `€ 12,00-`. */\n readonly minusAfter: boolean\n /** Whole digits grouped as this kind of amount groups them, to compare a grouped draft against. */\n readonly grouped: (digits: string) => string\n}\n\nconst minus = /^[-−]/\nconst minusLast = /[-−]$/\nconst apostrophe = /^['’]$/\nconst apostrophes = /['’]/g\nconst spaces = /[\\s\\u00a0\\u202f]/\nconst allSpaces = /[\\s\\u00a0\\u202f]/g\n/** Marks a format writes and a person does not type: bidi marks around an amount in rtl locales. */\n/** The marks a format writes around an amount in an rtl locale: the bidi marks and isolates. */\nconst bidi = /[\\u061c\\u200e\\u200f\\u2066-\\u2069]/g\n/** The full-width forms a keyboard in East Asia types: `12,3`. Only these are folded. */\nconst fullWidth = /[\\uff01-\\uff5e\\uffe0-\\uffe6]/g\n\n/** `text` without a minus at its start, or at its end where the locale writes one there. */\nconst unsigned = (\n text: string,\n minusAfter: boolean,\n): { readonly signed: boolean; readonly text: string } => {\n if (minus.test(text)) return { signed: true, text: text.slice(1).trim() }\n if (minusAfter && minusLast.test(text)) return { signed: true, text: text.slice(0, -1).trim() }\n return { signed: false, text }\n}\n\nconst symbolsCache = new Map<string, Symbols>()\n\n/**\n * The symbols of `options` in `locale`, Latin digits always: a draft is typed on\n * a keyboard. All of them from the format the field writes with: a currency or a\n * percentage can group, and separate decimals, otherwise than a plain number\n * (`de-AT`'s euros group with `.`, its plain numbers with a space).\n */\nconst symbolsOf = (locale: string, options: Intl.NumberFormatOptions): Symbols => {\n const key = JSON.stringify([locale, options])\n const known = symbolsCache.get(key)\n if (known !== undefined) return known\n const parts = new Intl.NumberFormat(locale, {\n ...options,\n numberingSystem: 'latn',\n // One decimal, whatever the field's own: a yen amount still has a decimal separator.\n minimumFractionDigits: 1,\n maximumFractionDigits: 1,\n }).formatToParts(-1234567.5)\n const of = (type: Intl.NumberFormatPartTypes) => parts.find(part => part.type === type)?.value\n // Grouped by the field's own format, which for a percentage is not always a\n // number's (`mr` writes `123,450%` but `1,23,450`).\n const whole = new Intl.NumberFormat(locale, {\n ...options,\n numberingSystem: 'latn',\n useGrouping: true,\n minimumFractionDigits: 0,\n maximumFractionDigits: 0,\n })\n // In their compatibility forms, as a draft is read: the full-width `¥` a\n // format writes is the `¥` a keyboard types.\n const made: Symbols = {\n group: (of('group') ?? ',').normalize('NFKC'),\n decimal: (of('decimal') ?? '.').normalize('NFKC'),\n affixes: parts\n .filter(part => part.type === 'currency' || part.type === 'percentSign')\n .map(part => part.value.normalize('NFKC')),\n minusAfter:\n parts.findIndex(part => part.type === 'minusSign') >\n parts.findIndex(part => part.type === 'integer'),\n grouped: digits =>\n whole\n // A percentage's format scales what it is given by 100, so it is given the\n // share, as exact text (see `writeAmount`).\n .formatToParts(\n (options.style === 'percent'\n ? decimalText(shifted({ negative: false, whole: digits, fraction: '' }, 2))\n : digits) as unknown as number,\n )\n .filter(part => part.type === 'integer' || part.type === 'group')\n .map(part => part.value)\n .join('')\n .normalize('NFKC'),\n }\n symbolsCache.set(key, made)\n return made\n}\n\n/**\n * The digits a draft names, in `locale` for amounts formatted with `options`:\n * none for text that is not an amount, has more than `scale` fraction digits,\n * groups its digits other than the locale does (`12,3` in `en-US` is no\n * thousand), or is negative where `negative` is false. A currency symbol or a\n * percent sign is taken at either end only, and a minus before or after it, or\n * last where the locale writes it last.\n */\nexport const readDigits = (\n draft: string,\n locale: string,\n options: Intl.NumberFormatOptions,\n scale: number,\n negative: boolean,\n): Option.Option<Digits> => {\n const symbols = symbolsOf(locale, options)\n // Only the full-width forms are folded: NFKC of the whole draft would also read\n // `10²` as `102` and `①②` as `12`, a guess where a refusal is due.\n // Any other invisible character, an override that shows `21` as `12`, stays in\n // and is no digit, so the amount is refused.\n let text = draft\n .replace(fullWidth, form => form.normalize('NFKC'))\n .replace(bidi, '')\n .trim()\n // A minus, then at most one symbol at one end, then a minus inside the symbol.\n const outer = unsigned(text, symbols.minusAfter)\n text = outer.text\n const affix = symbols.affixes.find(held => text.startsWith(held) || text.endsWith(held))\n if (affix !== undefined)\n text = (text.startsWith(affix) ? text.slice(affix.length) : text.slice(0, -affix.length)).trim()\n const inner = outer.signed ? { signed: true, text } : unsigned(text, symbols.minusAfter)\n text = inner.text\n if (inner.signed && !negative) return Option.none()\n // A locale that groups with a space groups with any space a person types; in\n // one that does not, a space inside an amount is no part of it.\n if (spaces.test(symbols.group)) text = text.replace(allSpaces, symbols.group)\n else if (spaces.test(text)) return Option.none()\n // Swiss grouping is an apostrophe, `’` or `'` as the runtime's ICU writes it\n // (ICU 78 moved to `'`), and a person types either.\n if (apostrophe.test(symbols.group)) text = text.replace(apostrophes, symbols.group)\n const [whole = '', fraction = '', ...rest] = text.split(symbols.decimal)\n if (rest.length > 0 || (whole === '' && fraction === '')) return Option.none()\n if (!/^\\d*$/.test(fraction) || fraction.length > scale) return Option.none()\n const digits = whole.split(symbols.group).join('')\n if (!/^\\d*$/.test(digits)) return Option.none()\n // Grouped only as the locale groups that number, or not at all.\n if (digits !== whole && symbols.grouped(digits) !== whole) return Option.none()\n return Option.some({\n negative: inner.signed,\n whole: digits.replace(/^0+(?=\\d)/, '') || '0',\n fraction: fraction.padEnd(scale, '0'),\n })\n}\n\n/** Digits as a decimal text `Intl.NumberFormat` reads exactly: `-1234.50`. Zero has no sign. */\nexport const decimalText = ({ negative, whole, fraction }: Digits): string => {\n const unsigned = fraction === '' ? whole : `${whole}.${fraction}`\n return negative && /[1-9]/.test(unsigned) ? `-${unsigned}` : unsigned\n}\n\nconst decimalPattern = /^(-?)(\\d+)(?:\\.(\\d+))?$/\n\n/** The digits of a decimal text, with exactly `scale` fraction digits, or none for text with more. */\nexport const digitsOf = (text: string, scale: number): Option.Option<Digits> => {\n const parts = decimalPattern.exec(text)\n if (parts === null) return Option.none()\n const [, sign, whole = '0', fraction = ''] = parts\n if (fraction.replace(/0+$/, '').length > scale) return Option.none()\n return Option.some({\n negative: sign === '-',\n whole: whole.replace(/^0+(?=\\d)/, ''),\n fraction: fraction.slice(0, scale).padEnd(scale, '0'),\n })\n}\n\n/** Moves the decimal point `places` to the left (`12.5`, 2 → `0.125`), as text. */\nexport const shifted = (digits: Digits, places: number): Digits => {\n const all = (digits.whole + digits.fraction).padStart(digits.fraction.length + places + 1, '0')\n const point = all.length - digits.fraction.length - places\n return {\n negative: digits.negative,\n whole: all.slice(0, point).replace(/^0+(?=\\d)/, ''),\n fraction: all.slice(point),\n }\n}\n\nconst formatCache = new Map<string, Intl.NumberFormat>()\n\n/** Writes a decimal text as `locale` writes amounts formatted with `options`. */\nexport const writeAmount = (\n text: string,\n locale: string,\n options: Intl.NumberFormatOptions,\n): string => {\n const key = JSON.stringify([locale, options])\n const known = formatCache.get(key)\n const format = known ?? new Intl.NumberFormat(locale, { ...options, numberingSystem: 'latn' })\n if (known === undefined) formatCache.set(key, format)\n // A string is formatted exactly (ECMA-402 2023), with no rounding through a\n // double; the ES2022 library this builds against types only number and bigint.\n return format.format(text as unknown as number)\n}\n","/**\n * Wall-clock readings and the instants they name in a time zone. A reading in a\n * zone names no instant when the clocks skip it, and two when they repeat it;\n * the zone is always a parameter, never the runtime's.\n */\nimport { Option } from 'effect'\nimport { isCalendarDate } from 'foldkit/calendar'\n\n/** A date and a time of day, in no zone. */\nexport interface Wall {\n readonly year: number\n readonly month: number\n readonly day: number\n readonly hour: number\n readonly minute: number\n readonly second: number\n readonly millisecond: number\n}\n\n// What a `datetime-local` input writes: minutes, and seconds only with a step under a minute.\nconst naive = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{1,3}))?)?$/\n\n/** The reading a naive text names, or none for text that is not one, or names no real time. */\nexport const readWall = (text: string): Option.Option<Wall> => {\n const parts = naive.exec(text)\n if (parts === null) return Option.none()\n const [year, month, day, hour, minute, second = '0', fraction = ''] = parts.slice(1)\n const wall: Wall = {\n year: Number(year),\n month: Number(month),\n day: Number(day),\n hour: Number(hour),\n minute: Number(minute),\n second: Number(second),\n millisecond: Number(fraction.padEnd(3, '0')),\n }\n const real =\n isCalendarDate({ year: wall.year, month: wall.month, day: wall.day }) &&\n wall.hour < 24 &&\n wall.minute < 60 &&\n wall.second < 60\n return real ? Option.some(wall) : Option.none()\n}\n\nconst two = (value: number) => String(value).padStart(2, '0')\n\n/** A reading as a `datetime-local` input shows it: seconds only when it has them. */\nexport const writeWall = (wall: Wall): string => {\n const date = `${String(wall.year).padStart(4, '0')}-${two(wall.month)}-${two(wall.day)}`\n const seconds =\n wall.second === 0 && wall.millisecond === 0\n ? ''\n : `:${two(wall.second)}${wall.millisecond === 0 ? '' : `.${String(wall.millisecond).padStart(3, '0')}`}`\n return `${date}T${two(wall.hour)}:${two(wall.minute)}${seconds}`\n}\n\n/**\n * The reading as if it were in UTC, in milliseconds: what offsets are measured\n * from. Set field by field, since `Date.UTC` reads a year under 100 as 19xx.\n */\nexport const utcOf = (wall: Wall): number => {\n const at = new Date(0)\n at.setUTCFullYear(wall.year, wall.month - 1, wall.day)\n at.setUTCHours(wall.hour, wall.minute, wall.second, wall.millisecond)\n return at.getTime()\n}\n\nconst dateFormats = new Map<string, Intl.DateTimeFormat>()\n\n/** One formatter per locale and options: a display runs per cell, and making one is the slow part. */\nexport const dateFormat = (\n locale: string,\n options: Intl.DateTimeFormatOptions,\n): Intl.DateTimeFormat => {\n const key = JSON.stringify([locale, options])\n const known = dateFormats.get(key)\n if (known !== undefined) return known\n const made = new Intl.DateTimeFormat(locale, options)\n dateFormats.set(key, made)\n return made\n}\n\n// One formatter per zone: making one is the slow part.\nconst formatters = new Map<string, Intl.DateTimeFormat>()\nconst formatterOf = (zone: string): Intl.DateTimeFormat => {\n const known = formatters.get(zone)\n if (known !== undefined) return known\n const made = new Intl.DateTimeFormat('en-US', {\n timeZone: zone,\n hourCycle: 'h23',\n // The era too: the formatter writes ISO year 0 as year 1 BC.\n era: 'short',\n year: 'numeric',\n month: 'numeric',\n day: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n second: 'numeric',\n fractionalSecondDigits: 3,\n })\n formatters.set(zone, made)\n return made\n}\n\n/** Refuses a zone the runtime does not know, naming it. */\nexport const knownZone = (zone: string, who: string): string => {\n try {\n formatterOf(zone)\n } catch {\n throw new Error(`${who}: \"${zone}\" is not a time zone this runtime knows`)\n }\n return zone\n}\n\n/** What the clocks in `zone` read at an instant. */\nexport const wallOf = (instant: number, zone: string): Wall => {\n const parts = Object.fromEntries(\n formatterOf(zone)\n .formatToParts(new Date(instant))\n .map(part => [part.type, part.value]),\n )\n return {\n year: parts['era'] === 'BC' ? 1 - Number(parts['year']) : Number(parts['year']),\n month: Number(parts['month']),\n day: Number(parts['day']),\n hour: Number(parts['hour']),\n minute: Number(parts['minute']),\n second: Number(parts['second']),\n millisecond: Number(parts['fractionalSecond']),\n }\n}\n\nconst day = 86_400_000\n\n/**\n * The instant at which the clocks in `zone` read `wall`: none in a gap the\n * clocks skip (a spring change), and the earlier of the two in an hour they\n * repeat (an autumn one). The offsets a day either side are the only ones that\n * can apply, since no zone changes its offset twice in two days.\n */\nexport const instantOf = (wall: Wall, zone: string): Option.Option<number> => {\n const local = utcOf(wall)\n const offsets = new Set(\n [local - day, local + day].map(around => utcOf(wallOf(around, zone)) - around),\n )\n const instants = [...offsets]\n .map(offset => local - offset)\n .filter(instant => utcOf(wallOf(instant, zone)) === local)\n return instants.length === 0 ? Option.none() : Option.some(Math.min(...instants))\n}\n\nconst offsetText = /^([+-])(\\d{2}):(\\d{2})$/\n\n/** The milliseconds `Z` or `±HH:MM` is ahead of UTC, or none for an offset past ±23:59. */\nconst offsetOf = (text: string): Option.Option<number> => {\n if (text === 'Z') return Option.some(0)\n const parts = offsetText.exec(text)\n if (parts === null) return Option.none()\n const [, sign, hours = '', minutes = ''] = parts\n if (Number(hours) > 23 || Number(minutes) > 59) return Option.none()\n return Option.some((sign === '-' ? -1 : 1) * (Number(hours) * 60 + Number(minutes)) * 60_000)\n}\n\nconst writeOffset = (offset: number): string => {\n const minutes = Math.abs(offset) / 60_000\n return `${offset < 0 ? '-' : '+'}${two(Math.floor(minutes / 60))}:${two(minutes % 60)}`\n}\n\n// The ISO form alone, offset and all: `Date.parse` takes much else, in the runtime's\n// zone, and moves a day that does not exist (`02-30`) into the next month.\nconst isoInstant = /^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(?::\\d{2})?)(?:\\.(\\d+))?(Z|[+-]\\d{2}:\\d{2})$/\n\n/**\n * The instant stored ISO text names, or none for text that is not one, names\n * no real date and time, or is finer than a millisecond: a reading holds no\n * finer, so showing such an instant would change it.\n */\nexport const readInstant = (text: string): Option.Option<number> => {\n const parts = isoInstant.exec(text)\n if (parts === null) return Option.none()\n const [, reading = '', fraction = '', offset = ''] = parts\n if (/[1-9]/.test(fraction.slice(3))) return Option.none()\n return Option.flatMap(\n readWall(fraction === '' ? reading : `${reading}.${fraction.slice(0, 3)}`),\n wall => Option.map(offsetOf(offset), ahead => utcOf(wall) - ahead),\n )\n}\n\nconst withOffset = /^(.*)([+-]\\d{2}:\\d{2})$/\n\n/**\n * The instant a draft names in `zone`. A reading alone names the one instant\n * the clocks read so, or the earlier of two they repeat; one with an offset\n * (`2026-11-01T01:30-05:00`) names the instant of that offset, when the clocks\n * read so then. None for a time they skip, or an offset they do not have there.\n */\nexport const instantIn = (draft: string, zone: string): Option.Option<number> => {\n const parts = withOffset.exec(draft)\n if (parts === null) return Option.flatMap(readWall(draft), wall => instantOf(wall, zone))\n const [, reading = '', offset = ''] = parts\n return Option.flatMap(readWall(reading), wall =>\n Option.filter(\n Option.map(offsetOf(offset), ahead => utcOf(wall) - ahead),\n instant => utcOf(wallOf(instant, zone)) === utcOf(wall),\n ),\n )\n}\n\n/**\n * The draft of an instant in `zone`: what the clocks read, with the offset\n * only where the reading alone would name another instant, the later of a\n * repeated hour, so that it reads back as the instant it shows.\n */\nexport const draftIn = (instant: number, zone: string): string => {\n const wall = wallOf(instant, zone)\n const reading = writeWall(wall)\n return Option.contains(instantIn(reading, zone), instant)\n ? reading\n : `${reading}${writeOffset(utcOf(wall) - instant)}`\n}\n","/**\n * `foldkit-form-recipes` — field recipes for `foldkit-form`.\n *\n * A recipe is the meaning of one kind of key: a control that reads a draft into\n * the key's value and shows a value as a draft, and a display that reads the\n * value in a list or a detail. Both are values made from the same options, so\n * a date reads the same in its field and in its table cell. This entry draws\n * nothing; `./html` holds the renderers.\n */\nimport { Match, Option } from 'effect'\nimport { Display } from 'foldkit-crud'\nimport { Input, type Control, type ControlChange } from 'foldkit-form'\nimport { isCalendarDate, type CalendarDate } from 'foldkit/calendar'\nimport { decimalText, digitsOf, readDigits, shifted, writeAmount, type Digits } from './number.js'\nimport { dateFormat, draftIn, instantIn, knownZone, readInstant, readWall, utcOf } from './wall.js'\n\n/**\n * One kind of key: how it is edited, and how it reads. Most recipes' control is\n * a `Control`; one that follows another key (`slug`) is a `ControlChange`, which\n * only a form's `inputs` takes.\n */\nexport interface FieldRecipe<C extends Control<any> | ControlChange = Control> {\n /** For a form's `inputs`, or (a `Control`) for Entity metadata through `Input.of`. */\n readonly control: C\n /** For Entity metadata through `Display.of`: how a list or a detail shows the value. */\n readonly display: Display\n}\n\n/** What a display says a date or a time in: a BCP 47 locale. Default `en`. */\nexport interface Locale {\n readonly locale?: string | undefined\n}\n\nconst two = (value: number) => String(value).padStart(2, '0')\n\nconst isoDate = /^(\\d{4})-(\\d{2})-(\\d{2})$/\n\n/** The calendar date `YYYY-MM-DD` names, or `undefined` for text that names none. */\nconst readDate = (text: string): CalendarDate | undefined => {\n const parts = isoDate.exec(text)\n if (parts === null) return undefined\n const date = { year: Number(parts[1]), month: Number(parts[2]), day: Number(parts[3]) }\n return isCalendarDate(date) ? date : undefined\n}\n\nconst writeDate = (date: CalendarDate): string =>\n `${String(date.year).padStart(4, '0')}-${two(date.month)}-${two(date.day)}`\n\nconst DateKind = Input.kind<Locale>('FieldRecipes.Date', {\n draft: 'text',\n parse: draft => readDate(draft),\n format: value => {\n if (isCalendarDate(value)) return writeDate(value)\n throw new Error(`a date is a CalendarDate, not ${JSON.stringify(value)}`)\n },\n unparsed: 'Enter a date',\n})\n\nconst DateDisplay = Display.kind<Locale>('FieldRecipes.Date', {\n text: (data, value) =>\n isCalendarDate(value)\n ? dateFormat(data.locale ?? 'en', { dateStyle: 'medium', timeZone: 'UTC' }).format(\n utcOf({ ...value, hour: 0, minute: 0, second: 0, millisecond: 0 }),\n )\n : String(value),\n})\n\n/**\n * How a moment is stored. `local` is the wall-clock text a `datetime-local`\n * input writes (`2026-10-26T09:30`), in no zone: the key's value is that text.\n * `instant` is an ISO instant (`2026-10-26T09:30:00.000Z`), read and shown as\n * the clocks in `zone` (an IANA name) read it.\n */\nexport type Stored =\n | { readonly stored: 'local' }\n | { readonly stored: 'instant'; readonly zone: string }\n\ntype DateTimeData = Stored & Locale\n\n/** How one stored form reads a typed reading, shows a value, and says it in a list. */\ninterface Mode {\n readonly parse: (draft: string) => Option.Option<string>\n readonly format: (value: unknown) => string\n readonly text: (value: string, locale: string) => string\n}\n\nconst shown = { dateStyle: 'medium', timeStyle: 'short' } as const\n\nconst local: Mode = {\n // The text as typed, once it names a real time: a stored value comes back unchanged.\n parse: draft => Option.as(readWall(draft), draft),\n format: value => {\n if (typeof value === 'string' && Option.isSome(readWall(value))) return value\n throw new Error(\n `a local date and time is text such as 2026-10-26T09:30, not ${JSON.stringify(value)}`,\n )\n },\n text: (value, locale) =>\n Option.match(readWall(value), {\n onNone: () => value,\n onSome: wall => dateFormat(locale, { ...shown, timeZone: 'UTC' }).format(utcOf(wall)),\n }),\n}\n\nconst instant = (zone: string): Mode => {\n const parse = (draft: string) =>\n Option.map(\n // An instant whose UTC year is past 9999 would be written `+010000-…`, which\n // no form reads back: refused, as a date past 9999 is.\n Option.filter(instantIn(draft, zone), at => new Date(at).getUTCFullYear() <= 9999),\n at => new Date(at).toISOString(),\n )\n return {\n parse,\n // Shown only as a draft that submits the same instant unchanged, or refused.\n format: value =>\n Option.match(\n Option.flatMap(typeof value === 'string' ? readInstant(value) : Option.none(), at => {\n const draft = draftIn(at, zone)\n return Option.contains(parse(draft), new Date(at).toISOString())\n ? Option.some(draft)\n : Option.none()\n }),\n {\n onSome: draft => draft,\n onNone: () => {\n throw new Error(\n `an instant is ISO text with an offset, naming a real date and time to the millisecond that ${zone} can show, not ${JSON.stringify(value)}`,\n )\n },\n },\n ),\n text: (value, locale) =>\n Option.match(readInstant(value), {\n onNone: () => value,\n onSome: at => dateFormat(locale, { ...shown, timeZone: zone }).format(at),\n }),\n }\n}\n\nconst modeOf = (data: Stored): Mode =>\n Match.value(data).pipe(\n Match.discriminatorsExhaustive('stored')({\n local: () => local,\n instant: ({ zone }) => instant(zone),\n }),\n )\n\nconst DateTimeKind = Input.kind<DateTimeData>('FieldRecipes.DateTime', {\n draft: 'text',\n parse: (draft, data) => Option.getOrUndefined(modeOf(data).parse(draft)),\n format: (value, data) => modeOf(data).format(value),\n unparsed: 'Enter a date and a time that exists',\n})\n\nconst DateTimeDisplay = Display.kind<DateTimeData>('FieldRecipes.DateTime', {\n text: (data, value) => modeOf(data).text(String(value), data.locale ?? 'en'),\n})\n\n/** What the browser may fill a text-shaped key with: an `autocomplete` token. */\nexport interface Autofill {\n readonly autocomplete?: string | undefined\n}\n\n// One `@`, something either side, no space: an address, without guessing at\n// what a server will deliver to. Whether it exists is the server's to say.\n// No space, no control or invisible character, and one `@` with something either side.\nconst address = /^[^\\s@\\p{C}]+@[^\\s@\\p{C}]+$/u\n\nconst EmailKind = Input.kind<Autofill>('FieldRecipes.Email', {\n draft: 'text',\n parse: draft => (address.test(draft) ? draft : undefined),\n unparsed: 'Enter an email address',\n})\n\n/** Which schemes a web address may have. Default `https:` and `http:`. */\nexport interface Schemes extends Autofill {\n readonly protocols?: ReadonlyArray<string> | undefined\n}\n\nconst UrlKind = Input.kind<Schemes>('FieldRecipes.Url', {\n draft: 'text',\n // Kept as typed, once it parses with an allowed scheme: a reader's spelling is\n // not rewritten, and `javascript:` is not a link.\n parse: (draft, data) => {\n if (!URL.canParse(draft)) return undefined\n return (data.protocols ?? ['https:', 'http:']).includes(new URL(draft).protocol)\n ? draft\n : undefined\n },\n unparsed: 'Enter a web address, starting https://',\n})\n\n// A telephone number is too varied to check here without refusing a real one;\n// the schema may, and the kind gives the keypad and the autofill.\nconst TelKind = Input.kind<Autofill>('FieldRecipes.Tel', { draft: 'text' })\n\n/** Whether the password is one the person has (`current`) or is choosing (`new`). */\nexport interface Purpose {\n readonly purpose: 'current' | 'new'\n}\n\nconst PasswordKind = Input.kind<Purpose>('FieldRecipes.Password', { draft: 'text' })\n\n/** A slug's address before it: `/blog/`. */\nexport interface Prefix {\n readonly prefix: string\n}\n\nconst SlugKind = Input.kind<Prefix>('FieldRecipes.Slug', { draft: 'text' })\n\n// Letters that decompose to no Latin letter, written as an address writes them.\nconst unaccented: Readonly<Record<string, string>> = {\n ı: 'i',\n ß: 'ss',\n æ: 'ae',\n œ: 'oe',\n ø: 'o',\n ł: 'l',\n đ: 'd',\n ð: 'd',\n þ: 'th',\n ħ: 'h',\n ŧ: 't',\n ŋ: 'n',\n ə: 'e',\n ĸ: 'k',\n}\nconst spelled = new RegExp(`[${Object.keys(unaccented).join('')}]`, 'g')\n\nconst slugify = (text: string): string =>\n text\n .toLowerCase()\n .replace(spelled, letter => unaccented[letter] ?? letter)\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n\nconst text = Display.text()\n\n/** Whether an amount may be below zero. Default not: a minus is then refused, not dropped. */\nexport interface Signed {\n readonly negative?: boolean | undefined\n}\n\n/**\n * How an amount of money is stored. `minor` is an integer of the currency's\n * smallest unit (cents: `123456` for $1,234.56); `decimal` is a decimal text\n * (`'1234.56'`). The key's schema says which it holds; a fill with a value of\n * the other form throws, rather than converting it.\n */\nexport interface MoneyOptions extends Locale, Signed {\n /** An ISO 4217 code: `USD`, `EUR`, `JPY`. */\n readonly currency: string\n readonly unit: 'minor' | 'decimal'\n}\n\ntype MoneyData = MoneyOptions & { readonly scale: number }\n\nconst currencyOf = (data: MoneyData) => ({ style: 'currency', currency: data.currency }) as const\n\n/** Digits of an integer number of minor units, or none for one that is not a safe integer. */\nconst minorDigits = (value: number, scale: number): Option.Option<Digits> => {\n if (!Number.isSafeInteger(value)) return Option.none()\n const all = String(Math.abs(value)).padStart(scale + 1, '0')\n return Option.some({\n negative: value < 0,\n whole: all.slice(0, all.length - scale),\n fraction: all.slice(all.length - scale),\n })\n}\n\n/** The decimal text a stored amount of money stands for, or none for a value of the wrong form. */\nconst moneyText = (value: unknown, data: MoneyData): Option.Option<string> =>\n Option.map(\n Match.value(data.unit).pipe(\n Match.when('minor', () =>\n typeof value === 'number' ? minorDigits(value, data.scale) : Option.none(),\n ),\n Match.when('decimal', () =>\n typeof value === 'string' ? digitsOf(value, data.scale) : Option.none(),\n ),\n Match.exhaustive,\n ),\n decimalText,\n )\n\n/** The value a typed amount of money stands for, in the stored form. */\nconst moneyValue = (digits: Digits, data: MoneyData): Option.Option<unknown> =>\n Match.value(data.unit).pipe(\n Match.when('decimal', () => Option.some<unknown>(decimalText(digits))),\n Match.when('minor', () => {\n const sign = digits.negative ? '-' : ''\n const minor = Number(BigInt(`${sign}${digits.whole}${digits.fraction}`))\n return Number.isSafeInteger(minor) ? Option.some<unknown>(minor) : Option.none()\n }),\n Match.exhaustive,\n )\n\nconst MoneyKind = Input.kind<MoneyData>('FieldRecipes.Money', {\n draft: 'text',\n parse: (draft, data) =>\n Option.getOrUndefined(\n Option.flatMap(\n readDigits(\n draft,\n data.locale ?? 'en',\n currencyOf(data),\n data.scale,\n data.negative === true,\n ),\n digits => moneyValue(digits, data),\n ),\n ),\n format: (value, data) =>\n Option.match(moneyText(value, data), {\n onNone: () => {\n throw new Error(\n `an amount of ${data.currency} in ${data.unit} units is ${data.unit === 'minor' ? 'an integer' : 'decimal text'} of at most ${data.scale} decimals, not ${JSON.stringify(value)}`,\n )\n },\n onSome: amount => writeAmount(amount, data.locale ?? 'en', currencyOf(data)),\n }),\n unparsed: 'Enter an amount',\n})\n\nconst MoneyDisplay = Display.kind<MoneyData>('FieldRecipes.Money', {\n text: (data, value) =>\n Option.match(moneyText(value, data), {\n onNone: () => String(value),\n onSome: amount => writeAmount(amount, data.locale ?? 'en', currencyOf(data)),\n }),\n})\n\n/** A number with at most `scale` decimals, kept as a number. */\nexport interface DecimalOptions extends Locale, Signed {\n readonly scale: number\n}\n\n/** Throws, when a recipe is made, for a scale no format can write. */\nconst scaleOf = (recipe: string, scale: number, most: number): void => {\n if (!Number.isInteger(scale) || scale < 0 || scale > most)\n throw new Error(\n `FieldRecipes.${recipe}: scale is a whole number from 0 to ${most}, not ${scale}`,\n )\n}\n\n/**\n * A number's shortest decimal text, the one `String` writes, never in exponent\n * form. `toFixed` would print the double's binary expansion instead past 17\n * significant digits: `1234.1` at 13 decimals is `1234.0999999999999`.\n */\nconst plainOf = (value: number): string => {\n const written = String(value)\n const parts = /^(-?)(\\d+)(?:\\.(\\d+))?e([+-]\\d+)$/.exec(written)\n if (parts === null) return written\n const [, sign = '', whole = '', fraction = '', exponent = '0'] = parts\n const digits = whole + fraction\n const point = whole.length + Number(exponent)\n if (point <= 0) return `${sign}0.${'0'.repeat(-point)}${digits}`\n if (point >= digits.length) return sign + digits + '0'.repeat(point - digits.length)\n return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`\n}\n\n/** A number's decimal text with `scale` decimals, or none when it has more. */\nconst fixed = (value: unknown, scale: number): Option.Option<string> =>\n typeof value === 'number' && Number.isFinite(value)\n ? Option.map(digitsOf(plainOf(value), scale), decimalText)\n : Option.none()\n\n/**\n * A decimal text as a number, or none when no number holds it exactly: past\n * about 15 significant digits a double rounds, and the typed amount would not\n * be what is stored.\n */\nconst exact = (text: string, scale: number): Option.Option<number> => {\n const value = Number(text)\n return Option.contains(fixed(value, scale), text) ? Option.some(value) : Option.none()\n}\n\nconst decimalOf = (data: DecimalOptions) => ({ maximumFractionDigits: data.scale }) as const\n\nconst DecimalKind = Input.kind<DecimalOptions>('FieldRecipes.Decimal', {\n draft: 'text',\n parse: (draft, data) =>\n Option.getOrUndefined(\n Option.flatMap(\n readDigits(draft, data.locale ?? 'en', decimalOf(data), data.scale, data.negative === true),\n digits => exact(decimalText(digits), data.scale),\n ),\n ),\n format: (value, data) =>\n Option.match(fixed(value, data.scale), {\n onNone: () => {\n throw new Error(`a number of at most ${data.scale} decimals, not ${JSON.stringify(value)}`)\n },\n onSome: amount => writeAmount(amount, data.locale ?? 'en', decimalOf(data)),\n }),\n unparsed: 'Enter a number',\n})\n\nconst DecimalDisplay = Display.kind<DecimalOptions>('FieldRecipes.Decimal', {\n text: (data, value) =>\n Option.match(fixed(value, data.scale), {\n onNone: () => String(value),\n onSome: amount => writeAmount(amount, data.locale ?? 'en', decimalOf(data)),\n }),\n})\n\n/**\n * A percentage, drawn as `12.5%`. `fraction` stores it as the share (`0.125`),\n * `points` as the number before the sign (`12.5`). `scale` is the decimals of\n * the percentage as shown (default 2).\n */\nexport interface PercentOptions extends Locale, Signed {\n readonly unit: 'fraction' | 'points'\n readonly scale?: number | undefined\n}\n\nconst percentOf = (data: PercentOptions) =>\n ({ style: 'percent', maximumFractionDigits: data.scale ?? 2 }) as const\n\n/** The share a stored percentage stands for, as decimal text, or none for one that would round. */\nconst shareText = (value: unknown, data: PercentOptions): Option.Option<string> => {\n const scale = data.scale ?? 2\n return Match.value(data.unit).pipe(\n Match.when('fraction', () => fixed(value, scale + 2)),\n Match.when('points', () =>\n Option.map(\n Option.flatMap(fixed(value, scale), points => digitsOf(points, scale)),\n points => decimalText(shifted(points, 2)),\n ),\n ),\n Match.exhaustive,\n )\n}\n\nconst PercentKind = Input.kind<PercentOptions>('FieldRecipes.Percent', {\n draft: 'text',\n parse: (draft, data) =>\n Option.getOrUndefined(\n Option.flatMap(\n readDigits(\n draft,\n data.locale ?? 'en',\n percentOf(data),\n data.scale ?? 2,\n data.negative === true,\n ),\n points =>\n Match.value(data.unit).pipe(\n Match.when('points', () => exact(decimalText(points), data.scale ?? 2)),\n Match.when('fraction', () =>\n exact(decimalText(shifted(points, 2)), (data.scale ?? 2) + 2),\n ),\n Match.exhaustive,\n ),\n ),\n ),\n format: (value, data) =>\n Option.match(shareText(value, data), {\n onNone: () => {\n throw new Error(\n `a percentage of at most ${data.scale ?? 2} decimals, not ${JSON.stringify(value)}`,\n )\n },\n onSome: share => writeAmount(share, data.locale ?? 'en', percentOf(data)),\n }),\n unparsed: 'Enter a percentage',\n})\n\nconst PercentDisplay = Display.kind<PercentOptions>('FieldRecipes.Percent', {\n text: (data, value) =>\n Option.match(shareText(value, data), {\n onNone: () => String(value),\n onSome: share => writeAmount(share, data.locale ?? 'en', percentOf(data)),\n }),\n})\n\nexport const FieldRecipes = {\n /** The control kinds, so a renderer table is keyed by them and `is` narrows a control. */\n Kinds: {\n Date: DateKind,\n DateTime: DateTimeKind,\n Email: EmailKind,\n Url: UrlKind,\n Tel: TelKind,\n Password: PasswordKind,\n Slug: SlugKind,\n Money: MoneyKind,\n Decimal: DecimalKind,\n Percent: PercentKind,\n },\n /** The display kinds, likewise. */\n Displays: {\n Date: DateDisplay,\n DateTime: DateTimeDisplay,\n Money: MoneyDisplay,\n Decimal: DecimalDisplay,\n Percent: PercentDisplay,\n },\n\n /**\n * A calendar date: a key whose value is a `CalendarDate` (`Calendar.CalendarDate`,\n * or `Calendar.CalendarDateFromIsoString` to store it as text), edited as the\n * `YYYY-MM-DD` a date input writes. A partly typed date reads `Enter a date`.\n */\n date: (options: Locale = {}): FieldRecipe<Control<Locale>> => ({\n control: DateKind.of(options),\n display: DateDisplay.of(options),\n }),\n\n /**\n * A date and a time, stored as `options.stored` says (see `Stored`). With\n * `instant`, a time the zone's clocks skip is refused, and one they repeat\n * reads as the earlier of the two. An unknown zone throws here.\n */\n dateTime: (options: Stored & Locale): FieldRecipe<Control<DateTimeData>> => {\n if (options.stored === 'instant') knownZone(options.zone, 'FieldRecipes.dateTime')\n return { control: DateTimeKind.of(options), display: DateTimeDisplay.of(options) }\n },\n\n /**\n * An email address: one `@` with something either side and no space, kept as\n * typed (neither trimmed nor lower-cased: a local part may be case-sensitive).\n */\n email: (options: Autofill = {}): FieldRecipe<Control<Autofill>> => ({\n control: EmailKind.of(options),\n display: text,\n }),\n\n /** A web address with one of `options.protocols`, kept as typed. */\n url: (options: Schemes = {}): FieldRecipe<Control<Schemes>> => {\n // A scheme as `URL` writes one, or nothing could ever match it.\n for (const protocol of options.protocols ?? [])\n if (!/^[a-z][a-z0-9+.-]*:$/.test(protocol))\n throw new Error(\n `FieldRecipes.url: \"${protocol}\" is not a scheme as URL writes it, such as \"https:\"`,\n )\n return { control: UrlKind.of(options), display: text }\n },\n\n /** A telephone number, as typed: the phone's keypad and the browser's autofill, no check. */\n tel: (options: Autofill = {}): FieldRecipe<Control<Autofill>> => ({\n control: TelKind.of(options),\n display: text,\n }),\n\n /**\n * A password, drawn masked and named for the browser's password manager by\n * `purpose`. It never shows in a list. Keep it out of any saved draft: a form\n * cannot leave a key out of `partial()`, so that rule is the application's.\n */\n password: (options: Purpose): FieldRecipe<Control<Purpose>> => ({\n control: PasswordKind.of(options),\n display: Display.hidden(),\n }),\n\n /**\n * An address that follows the key `from` (made into a slug) until the person\n * writes it, shown after `prefix`. A form filled with a stored slug does not\n * follow it: a published address must not move because its title did.\n */\n slug: (\n from: string,\n options: { readonly prefix?: string; readonly through?: (text: string) => string } = {},\n ): FieldRecipe<ControlChange> => ({\n control: {\n change: (resolved, inputKey) => {\n const following = Input.following(from, options.through ?? slugify).change(\n resolved,\n inputKey,\n )\n return Object.freeze({\n ...SlugKind.of({ prefix: options.prefix ?? '/' }),\n follows: following.follows,\n })\n },\n },\n display: text,\n }),\n\n /** Text as an address: lower case, unaccented, words joined by `-`. What `slug` follows with. */\n slugify,\n\n /**\n * An amount of money in `currency`, shown and read as `locale` writes it\n * (`$1,234.56`, `1.234,56 €`, `¥1,235`), with the currency's own decimals: none\n * for JPY, three for KWD. More decimals than that are refused, not rounded, and\n * grouping other than the locale's (`12,3`) is refused, not read as `123`. A\n * code that is not three capital letters throws here.\n */\n money: (options: MoneyOptions): FieldRecipe<Control<MoneyData>> => {\n // The shape only: `Intl.supportedValuesOf('currency')` lists current tender,\n // which leaves out `XAU` and `CLF` and differs between ICU releases.\n if (!/^[A-Z]{3}$/.test(options.currency))\n throw new Error(`FieldRecipes.money: \"${options.currency}\" is not an ISO 4217 code`)\n const scale =\n new Intl.NumberFormat(options.locale ?? 'en', {\n style: 'currency',\n currency: options.currency,\n }).resolvedOptions().maximumFractionDigits ?? 2\n const data = { ...options, scale }\n return { control: MoneyKind.of(data), display: MoneyDisplay.of(data) }\n },\n\n /** A number with at most `scale` decimals, read and shown in `locale`'s separators. */\n decimal: (options: DecimalOptions): FieldRecipe<Control<DecimalOptions>> => {\n scaleOf('decimal', options.scale, 20)\n return { control: DecimalKind.of(options), display: DecimalDisplay.of(options) }\n },\n\n /** A percentage, stored as a share or as points (see `PercentOptions`). */\n percent: (options: PercentOptions): FieldRecipe<Control<PercentOptions>> => {\n // A share keeps two more decimals than the points it is shown as.\n scaleOf('percent', options.scale ?? 2, 18)\n return { control: PercentKind.of(options), display: PercentDisplay.of(options) }\n },\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foldkit-form-recipes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Field recipes for foldkit-form: dates, times, amounts, text-shaped keys and an uploaded image, each a control, a display and a renderer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"foldkit",
|
|
@@ -53,18 +53,18 @@
|
|
|
53
53
|
"LICENSE"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"foldkit-
|
|
57
|
-
"foldkit-
|
|
58
|
-
"foldkit-
|
|
56
|
+
"foldkit-bundle": "0.9.0",
|
|
57
|
+
"foldkit-crud": "0.12.0",
|
|
58
|
+
"foldkit-form": "0.9.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@foldkit/ui": "^0.
|
|
62
|
-
"effect": "^4.
|
|
63
|
-
"foldkit": "^0.
|
|
64
|
-
"foldkit-
|
|
65
|
-
"foldkit-mixins": "0.
|
|
66
|
-
"foldkit-
|
|
67
|
-
"foldkit-mixins
|
|
61
|
+
"@foldkit/ui": "^0.170.0",
|
|
62
|
+
"effect": "^4.1.0",
|
|
63
|
+
"foldkit": "^0.170.0",
|
|
64
|
+
"foldkit-mixins-ui": "0.22.0",
|
|
65
|
+
"foldkit-mixins-form": "0.11.0",
|
|
66
|
+
"foldkit-media": "0.4.0",
|
|
67
|
+
"foldkit-mixins": "0.21.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependenciesMeta": {
|
|
70
70
|
"@foldkit/ui": {
|
|
@@ -84,18 +84,18 @@
|
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
|
-
"@effect/platform-browser": "^4.0
|
|
88
|
-
"@foldkit/ui": "0.
|
|
89
|
-
"effect": "^4.0
|
|
90
|
-
"foldkit": "0.
|
|
87
|
+
"@effect/platform-browser": "^4.1.0",
|
|
88
|
+
"@foldkit/ui": "0.170.0",
|
|
89
|
+
"effect": "^4.1.0",
|
|
90
|
+
"foldkit": "0.170.0",
|
|
91
91
|
"jsdom": "30.1.3",
|
|
92
92
|
"tsdown": "^0.23.1",
|
|
93
93
|
"typescript": "7.0.2",
|
|
94
|
-
"foldkit-entity": "0.
|
|
95
|
-
"foldkit-
|
|
96
|
-
"foldkit-mixins-
|
|
97
|
-
"foldkit-
|
|
98
|
-
"foldkit-mixins": "0.
|
|
94
|
+
"foldkit-entity": "0.14.0",
|
|
95
|
+
"foldkit-mixins-form": "0.11.0",
|
|
96
|
+
"foldkit-mixins-ui": "0.22.0",
|
|
97
|
+
"foldkit-media": "0.4.0",
|
|
98
|
+
"foldkit-mixins": "0.21.0"
|
|
99
99
|
},
|
|
100
100
|
"scripts": {
|
|
101
101
|
"build": "tsdown",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"src-CutVZjTd.mjs","names":["two"],"sources":["../src/number.ts","../src/wall.ts","../src/index.ts"],"sourcesContent":["/**\n * Reading and writing amounts as a locale writes them, without floating point:\n * a typed amount becomes its digits and sign, and an amount is written from a\n * decimal text, which `Intl.NumberFormat` formats exactly. Conservative by\n * design: a draft that could mean two things is refused, never guessed at.\n */\nimport { Option } from 'effect'\n\n/** An amount read from a draft: its sign, whole digits, and exactly `scale` fraction digits. */\nexport interface Digits {\n readonly negative: boolean\n readonly whole: string\n readonly fraction: string\n}\n\n/** What a locale writes around and between the digits of one kind of amount. */\ninterface Symbols {\n readonly group: string\n readonly decimal: string\n /** Text that is not part of the amount: a currency symbol, a percent sign. */\n readonly affixes: ReadonlyArray<string>\n /** Whole digits grouped as this kind of amount groups them, to compare a grouped draft against. */\n readonly grouped: (digits: string) => string\n}\n\nconst minus = /^[-−]/\nconst apostrophe = /^['’]$/\nconst apostrophes = /['’]/g\nconst spaces = /[\\s\\u00a0\\u202f]/\nconst allSpaces = /[\\s\\u00a0\\u202f]/g\n/** Marks a format writes and a person does not type: bidi marks around an amount in rtl locales. */\n/** The marks a format writes around an amount in an rtl locale: the bidi marks and isolates. */\nconst bidi = /[\\u061c\\u200e\\u200f\\u2066-\\u2069]/g\n/** The full-width forms a keyboard in East Asia types: `12,3`. Only these are folded. */\nconst fullWidth = /[\\uff01-\\uff5e\\uffe0-\\uffe6]/g\n\nconst symbolsCache = new Map<string, Symbols>()\n\n/**\n * The symbols of `options` in `locale`, Latin digits always: a draft is typed on\n * a keyboard. All of them from the format the field writes with: a currency or a\n * percentage can group, and separate decimals, otherwise than a plain number\n * (`de-AT`'s euros group with `.`, its plain numbers with a space).\n */\nconst symbolsOf = (locale: string, options: Intl.NumberFormatOptions): Symbols => {\n const key = JSON.stringify([locale, options])\n const known = symbolsCache.get(key)\n if (known !== undefined) return known\n const parts = new Intl.NumberFormat(locale, {\n ...options,\n numberingSystem: 'latn',\n // One decimal, whatever the field's own: a yen amount still has a decimal separator.\n minimumFractionDigits: 1,\n maximumFractionDigits: 1,\n }).formatToParts(-1234567.5)\n const of = (type: Intl.NumberFormatPartTypes) => parts.find(part => part.type === type)?.value\n // A percentage's whole digits are grouped as a number: its format would scale them.\n const whole = new Intl.NumberFormat(locale, {\n ...options,\n ...(options.style === 'percent' ? { style: 'decimal' } : {}),\n numberingSystem: 'latn',\n useGrouping: true,\n minimumFractionDigits: 0,\n maximumFractionDigits: 0,\n })\n // In their compatibility forms, as a draft is read: the full-width `¥` a\n // format writes is the `¥` a keyboard types.\n const made: Symbols = {\n group: (of('group') ?? ',').normalize('NFKC'),\n decimal: (of('decimal') ?? '.').normalize('NFKC'),\n affixes: parts\n .filter(part => part.type === 'currency' || part.type === 'percentSign')\n .map(part => part.value.normalize('NFKC')),\n grouped: digits =>\n whole\n .formatToParts(BigInt(digits))\n .filter(part => part.type === 'integer' || part.type === 'group')\n .map(part => part.value)\n .join('')\n .normalize('NFKC'),\n }\n symbolsCache.set(key, made)\n return made\n}\n\n/**\n * The digits a draft names, in `locale` for amounts formatted with `options`:\n * none for text that is not an amount, has more than `scale` fraction digits,\n * groups its digits other than the locale does (`12,3` in `en-US` is no\n * thousand), or is negative where `negative` is false. A currency symbol or a\n * percent sign is taken at either end only, and a minus before or after it.\n */\nexport const readDigits = (\n draft: string,\n locale: string,\n options: Intl.NumberFormatOptions,\n scale: number,\n negative: boolean,\n): Option.Option<Digits> => {\n const symbols = symbolsOf(locale, options)\n // Only the full-width forms are folded: NFKC of the whole draft would also read\n // `10²` as `102` and `①②` as `12`, a guess where a refusal is due.\n // Any other invisible character, an override that shows `21` as `12`, stays in\n // and is no digit, so the amount is refused.\n let text = draft\n .replace(fullWidth, form => form.normalize('NFKC'))\n .replace(bidi, '')\n .trim()\n // A minus, then at most one symbol at one end, then a minus after a leading symbol.\n let signed = minus.test(text)\n if (signed) text = text.slice(1).trim()\n const affix = symbols.affixes.find(held => text.startsWith(held) || text.endsWith(held))\n if (affix !== undefined)\n text = (text.startsWith(affix) ? text.slice(affix.length) : text.slice(0, -affix.length)).trim()\n if (!signed && minus.test(text)) {\n signed = true\n text = text.slice(1).trim()\n }\n if (signed && !negative) return Option.none()\n // A locale that groups with a space groups with any space a person types; in\n // one that does not, a space inside an amount is no part of it.\n if (spaces.test(symbols.group)) text = text.replace(allSpaces, symbols.group)\n else if (spaces.test(text)) return Option.none()\n // Swiss grouping is an apostrophe, `’` or `'` as the runtime's ICU writes it\n // (ICU 78 moved to `'`), and a person types either.\n if (apostrophe.test(symbols.group)) text = text.replace(apostrophes, symbols.group)\n const [whole = '', fraction = '', ...rest] = text.split(symbols.decimal)\n if (rest.length > 0 || (whole === '' && fraction === '')) return Option.none()\n if (!/^\\d*$/.test(fraction) || fraction.length > scale) return Option.none()\n const digits = whole.split(symbols.group).join('')\n if (!/^\\d*$/.test(digits)) return Option.none()\n // Grouped only as the locale groups that number, or not at all.\n if (digits !== whole && symbols.grouped(digits) !== whole) return Option.none()\n return Option.some({\n negative: signed,\n whole: digits.replace(/^0+(?=\\d)/, '') || '0',\n fraction: fraction.padEnd(scale, '0'),\n })\n}\n\n/** Digits as a decimal text `Intl.NumberFormat` reads exactly: `-1234.50`. Zero has no sign. */\nexport const decimalText = ({ negative, whole, fraction }: Digits): string => {\n const unsigned = fraction === '' ? whole : `${whole}.${fraction}`\n return negative && /[1-9]/.test(unsigned) ? `-${unsigned}` : unsigned\n}\n\nconst decimalPattern = /^(-?)(\\d+)(?:\\.(\\d+))?$/\n\n/** The digits of a decimal text, with exactly `scale` fraction digits, or none for text with more. */\nexport const digitsOf = (text: string, scale: number): Option.Option<Digits> => {\n const parts = decimalPattern.exec(text)\n if (parts === null) return Option.none()\n const [, sign, whole = '0', fraction = ''] = parts\n if (fraction.replace(/0+$/, '').length > scale) return Option.none()\n return Option.some({\n negative: sign === '-',\n whole: whole.replace(/^0+(?=\\d)/, ''),\n fraction: fraction.slice(0, scale).padEnd(scale, '0'),\n })\n}\n\n/** Moves the decimal point `places` to the left (`12.5`, 2 → `0.125`), as text. */\nexport const shifted = (digits: Digits, places: number): Digits => {\n const all = (digits.whole + digits.fraction).padStart(digits.fraction.length + places + 1, '0')\n const point = all.length - digits.fraction.length - places\n return {\n negative: digits.negative,\n whole: all.slice(0, point).replace(/^0+(?=\\d)/, ''),\n fraction: all.slice(point),\n }\n}\n\nconst formatCache = new Map<string, Intl.NumberFormat>()\n\n/** Writes a decimal text as `locale` writes amounts formatted with `options`. */\nexport const writeAmount = (\n text: string,\n locale: string,\n options: Intl.NumberFormatOptions,\n): string => {\n const key = JSON.stringify([locale, options])\n const known = formatCache.get(key)\n const format = known ?? new Intl.NumberFormat(locale, { ...options, numberingSystem: 'latn' })\n if (known === undefined) formatCache.set(key, format)\n // A string is formatted exactly (ECMA-402 2023), with no rounding through a\n // double; the ES2022 library this builds against types only number and bigint.\n return format.format(text as unknown as number)\n}\n","/**\n * Wall-clock readings and the instants they name in a time zone. A reading in a\n * zone names no instant when the clocks skip it, and two when they repeat it;\n * the zone is always a parameter, never the runtime's.\n */\nimport { Option } from 'effect'\nimport { isCalendarDate } from 'foldkit/calendar'\n\n/** A date and a time of day, in no zone. */\nexport interface Wall {\n readonly year: number\n readonly month: number\n readonly day: number\n readonly hour: number\n readonly minute: number\n readonly second: number\n readonly millisecond: number\n}\n\n// What a `datetime-local` input writes: minutes, and seconds only with a step under a minute.\nconst naive = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{1,3}))?)?$/\n\n/** The reading a naive text names, or none for text that is not one, or names no real time. */\nexport const readWall = (text: string): Option.Option<Wall> => {\n const parts = naive.exec(text)\n if (parts === null) return Option.none()\n const [year, month, day, hour, minute, second = '0', fraction = ''] = parts.slice(1)\n const wall: Wall = {\n year: Number(year),\n month: Number(month),\n day: Number(day),\n hour: Number(hour),\n minute: Number(minute),\n second: Number(second),\n millisecond: Number(fraction.padEnd(3, '0')),\n }\n const real =\n isCalendarDate({ year: wall.year, month: wall.month, day: wall.day }) &&\n wall.hour < 24 &&\n wall.minute < 60 &&\n wall.second < 60\n return real ? Option.some(wall) : Option.none()\n}\n\nconst two = (value: number) => String(value).padStart(2, '0')\n\n/** A reading as a `datetime-local` input shows it: seconds only when it has them. */\nexport const writeWall = (wall: Wall): string => {\n const date = `${String(wall.year).padStart(4, '0')}-${two(wall.month)}-${two(wall.day)}`\n const seconds =\n wall.second === 0 && wall.millisecond === 0\n ? ''\n : `:${two(wall.second)}${wall.millisecond === 0 ? '' : `.${String(wall.millisecond).padStart(3, '0')}`}`\n return `${date}T${two(wall.hour)}:${two(wall.minute)}${seconds}`\n}\n\n/**\n * The reading as if it were in UTC, in milliseconds: what offsets are measured\n * from. Set field by field, since `Date.UTC` reads a year under 100 as 19xx.\n */\nexport const utcOf = (wall: Wall): number => {\n const at = new Date(0)\n at.setUTCFullYear(wall.year, wall.month - 1, wall.day)\n at.setUTCHours(wall.hour, wall.minute, wall.second, wall.millisecond)\n return at.getTime()\n}\n\nconst dateFormats = new Map<string, Intl.DateTimeFormat>()\n\n/** One formatter per locale and options: a display runs per cell, and making one is the slow part. */\nexport const dateFormat = (\n locale: string,\n options: Intl.DateTimeFormatOptions,\n): Intl.DateTimeFormat => {\n const key = JSON.stringify([locale, options])\n const known = dateFormats.get(key)\n if (known !== undefined) return known\n const made = new Intl.DateTimeFormat(locale, options)\n dateFormats.set(key, made)\n return made\n}\n\n// One formatter per zone: making one is the slow part.\nconst formatters = new Map<string, Intl.DateTimeFormat>()\nconst formatterOf = (zone: string): Intl.DateTimeFormat => {\n const known = formatters.get(zone)\n if (known !== undefined) return known\n const made = new Intl.DateTimeFormat('en-US', {\n timeZone: zone,\n hourCycle: 'h23',\n year: 'numeric',\n month: 'numeric',\n day: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n second: 'numeric',\n fractionalSecondDigits: 3,\n })\n formatters.set(zone, made)\n return made\n}\n\n/** Refuses a zone the runtime does not know, naming it. */\nexport const knownZone = (zone: string, who: string): string => {\n try {\n formatterOf(zone)\n } catch {\n throw new Error(`${who}: \"${zone}\" is not a time zone this runtime knows`)\n }\n return zone\n}\n\n/** What the clocks in `zone` read at an instant. */\nexport const wallOf = (instant: number, zone: string): Wall => {\n const parts = Object.fromEntries(\n formatterOf(zone)\n .formatToParts(new Date(instant))\n .map(part => [part.type, part.value]),\n )\n return {\n year: Number(parts['year']),\n month: Number(parts['month']),\n day: Number(parts['day']),\n hour: Number(parts['hour']),\n minute: Number(parts['minute']),\n second: Number(parts['second']),\n millisecond: Number(parts['fractionalSecond']),\n }\n}\n\nconst day = 86_400_000\n\n/**\n * The instant at which the clocks in `zone` read `wall`: none in a gap the\n * clocks skip (a spring change), and the earlier of the two in an hour they\n * repeat (an autumn one). The offsets a day either side are the only ones that\n * can apply, since no zone changes its offset twice in two days.\n */\nexport const instantOf = (wall: Wall, zone: string): Option.Option<number> => {\n const local = utcOf(wall)\n const offsets = new Set(\n [local - day, local + day].map(around => utcOf(wallOf(around, zone)) - around),\n )\n const instants = [...offsets]\n .map(offset => local - offset)\n .filter(instant => utcOf(wallOf(instant, zone)) === local)\n return instants.length === 0 ? Option.none() : Option.some(Math.min(...instants))\n}\n","/**\n * `foldkit-form-recipes` — field recipes for `foldkit-form`.\n *\n * A recipe is the meaning of one kind of key: a control that reads a draft into\n * the key's value and shows a value as a draft, and a display that reads the\n * value in a list or a detail. Both are values made from the same options, so\n * a date reads the same in its field and in its table cell. This entry draws\n * nothing; `./html` holds the renderers.\n */\nimport { Match, Option } from 'effect'\nimport { Display } from 'foldkit-crud'\nimport { Input, type Control, type ControlChange } from 'foldkit-form'\nimport { isCalendarDate, type CalendarDate } from 'foldkit/calendar'\nimport { decimalText, digitsOf, readDigits, shifted, writeAmount, type Digits } from './number.js'\nimport { dateFormat, instantOf, knownZone, readWall, utcOf, wallOf, writeWall } from './wall.js'\n\n/**\n * One kind of key: how it is edited, and how it reads. Most recipes' control is\n * a `Control`; one that follows another key (`slug`) is a `ControlChange`, which\n * only a form's `inputs` takes.\n */\nexport interface FieldRecipe<C extends Control<any> | ControlChange = Control> {\n /** For a form's `inputs`, or (a `Control`) for Entity metadata through `Input.of`. */\n readonly control: C\n /** For Entity metadata through `Display.of`: how a list or a detail shows the value. */\n readonly display: Display\n}\n\n/** What a display says a date or a time in: a BCP 47 locale. Default `en`. */\nexport interface Locale {\n readonly locale?: string | undefined\n}\n\nconst two = (value: number) => String(value).padStart(2, '0')\n\nconst isoDate = /^(\\d{4})-(\\d{2})-(\\d{2})$/\n\n/** The calendar date `YYYY-MM-DD` names, or `undefined` for text that names none. */\nconst readDate = (text: string): CalendarDate | undefined => {\n const parts = isoDate.exec(text)\n if (parts === null) return undefined\n const date = { year: Number(parts[1]), month: Number(parts[2]), day: Number(parts[3]) }\n return isCalendarDate(date) ? date : undefined\n}\n\nconst writeDate = (date: CalendarDate): string =>\n `${String(date.year).padStart(4, '0')}-${two(date.month)}-${two(date.day)}`\n\nconst DateKind = Input.kind<Locale>('FieldRecipes.Date', {\n draft: 'text',\n parse: draft => readDate(draft),\n format: value => {\n if (isCalendarDate(value)) return writeDate(value)\n throw new Error(`a date is a CalendarDate, not ${JSON.stringify(value)}`)\n },\n unparsed: 'Enter a date',\n})\n\nconst DateDisplay = Display.kind<Locale>('FieldRecipes.Date', {\n text: (data, value) =>\n isCalendarDate(value)\n ? dateFormat(data.locale ?? 'en', { dateStyle: 'medium', timeZone: 'UTC' }).format(\n utcOf({ ...value, hour: 0, minute: 0, second: 0, millisecond: 0 }),\n )\n : String(value),\n})\n\n/**\n * How a moment is stored. `local` is the wall-clock text a `datetime-local`\n * input writes (`2026-10-26T09:30`), in no zone: the key's value is that text.\n * `instant` is an ISO instant (`2026-10-26T09:30:00.000Z`), read and shown as\n * the clocks in `zone` (an IANA name) read it.\n */\nexport type Stored =\n | { readonly stored: 'local' }\n | { readonly stored: 'instant'; readonly zone: string }\n\ntype DateTimeData = Stored & Locale\n\n/** How one stored form reads a typed reading, shows a value, and says it in a list. */\ninterface Mode {\n readonly parse: (draft: string) => Option.Option<string>\n readonly format: (value: unknown) => string\n readonly text: (value: string, locale: string) => string\n}\n\nconst shown = { dateStyle: 'medium', timeStyle: 'short' } as const\n// The ISO form alone, offset and all: `Date.parse` takes much else, in the runtime's zone.\nconst instantText = /^\\d{4,}-\\d\\d-\\d\\dT\\d\\d:\\d\\d(?::\\d\\d(?:\\.\\d+)?)?(?:Z|[+-]\\d\\d:\\d\\d)$/\n\nconst local: Mode = {\n // The text as typed, once it names a real time: a stored value comes back unchanged.\n parse: draft => Option.as(readWall(draft), draft),\n format: value => {\n if (typeof value === 'string' && Option.isSome(readWall(value))) return value\n throw new Error(\n `a local date and time is text such as 2026-10-26T09:30, not ${JSON.stringify(value)}`,\n )\n },\n text: (value, locale) =>\n Option.match(readWall(value), {\n onNone: () => value,\n onSome: wall => dateFormat(locale, { ...shown, timeZone: 'UTC' }).format(utcOf(wall)),\n }),\n}\n\nconst instant = (zone: string): Mode => ({\n parse: draft =>\n Option.map(\n // An instant whose UTC year is past 9999 would be written `+010000-…`, which\n // no form reads back: refused, as a date past 9999 is.\n Option.filter(\n Option.flatMap(readWall(draft), wall => instantOf(wall, zone)),\n at => new Date(at).getUTCFullYear() <= 9999,\n ),\n at => new Date(at).toISOString(),\n ),\n format: value => {\n const at = typeof value === 'string' && instantText.test(value) ? Date.parse(value) : Number.NaN\n if (!Number.isNaN(at)) return writeWall(wallOf(at, zone))\n throw new Error(`an instant is ISO text with an offset, not ${JSON.stringify(value)}`)\n },\n text: (value, locale) => {\n const at = instantText.test(value) ? Date.parse(value) : Number.NaN\n return Number.isNaN(at) ? value : dateFormat(locale, { ...shown, timeZone: zone }).format(at)\n },\n})\n\nconst modeOf = (data: Stored): Mode =>\n Match.value(data).pipe(\n Match.discriminatorsExhaustive('stored')({\n local: () => local,\n instant: ({ zone }) => instant(zone),\n }),\n )\n\nconst DateTimeKind = Input.kind<DateTimeData>('FieldRecipes.DateTime', {\n draft: 'text',\n parse: (draft, data) => Option.getOrUndefined(modeOf(data).parse(draft)),\n format: (value, data) => modeOf(data).format(value),\n unparsed: 'Enter a date and a time that exists',\n})\n\nconst DateTimeDisplay = Display.kind<DateTimeData>('FieldRecipes.DateTime', {\n text: (data, value) => modeOf(data).text(String(value), data.locale ?? 'en'),\n})\n\n/** What the browser may fill a text-shaped key with: an `autocomplete` token. */\nexport interface Autofill {\n readonly autocomplete?: string | undefined\n}\n\n// One `@`, something either side, no space: an address, without guessing at\n// what a server will deliver to. Whether it exists is the server's to say.\n// No space, no control or invisible character, and one `@` with something either side.\nconst address = /^[^\\s@\\p{C}]+@[^\\s@\\p{C}]+$/u\n\nconst EmailKind = Input.kind<Autofill>('FieldRecipes.Email', {\n draft: 'text',\n parse: draft => (address.test(draft) ? draft : undefined),\n unparsed: 'Enter an email address',\n})\n\n/** Which schemes a web address may have. Default `https:` and `http:`. */\nexport interface Schemes extends Autofill {\n readonly protocols?: ReadonlyArray<string> | undefined\n}\n\nconst UrlKind = Input.kind<Schemes>('FieldRecipes.Url', {\n draft: 'text',\n // Kept as typed, once it parses with an allowed scheme: a reader's spelling is\n // not rewritten, and `javascript:` is not a link.\n parse: (draft, data) => {\n if (!URL.canParse(draft)) return undefined\n return (data.protocols ?? ['https:', 'http:']).includes(new URL(draft).protocol)\n ? draft\n : undefined\n },\n unparsed: 'Enter a web address, starting https://',\n})\n\n// A telephone number is too varied to check here without refusing a real one;\n// the schema may, and the kind gives the keypad and the autofill.\nconst TelKind = Input.kind<Autofill>('FieldRecipes.Tel', { draft: 'text' })\n\n/** Whether the password is one the person has (`current`) or is choosing (`new`). */\nexport interface Purpose {\n readonly purpose: 'current' | 'new'\n}\n\nconst PasswordKind = Input.kind<Purpose>('FieldRecipes.Password', { draft: 'text' })\n\n/** A slug's address before it: `/blog/`. */\nexport interface Prefix {\n readonly prefix: string\n}\n\nconst SlugKind = Input.kind<Prefix>('FieldRecipes.Slug', { draft: 'text' })\n\n// Letters that decompose to no Latin letter, written as an address writes them.\nconst unaccented: Readonly<Record<string, string>> = {\n ı: 'i',\n ß: 'ss',\n æ: 'ae',\n œ: 'oe',\n ø: 'o',\n ł: 'l',\n đ: 'd',\n ð: 'd',\n þ: 'th',\n ħ: 'h',\n ŧ: 't',\n ŋ: 'n',\n ə: 'e',\n ĸ: 'k',\n}\nconst spelled = new RegExp(`[${Object.keys(unaccented).join('')}]`, 'g')\n\nconst slugify = (text: string): string =>\n text\n .toLowerCase()\n .replace(spelled, letter => unaccented[letter] ?? letter)\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n\nconst text = Display.text()\n\n/** Whether an amount may be below zero. Default not: a minus is then refused, not dropped. */\nexport interface Signed {\n readonly negative?: boolean | undefined\n}\n\n/**\n * How an amount of money is stored. `minor` is an integer of the currency's\n * smallest unit (cents: `123456` for $1,234.56); `decimal` is a decimal text\n * (`'1234.56'`). The key's schema says which it holds; a fill with a value of\n * the other form throws, rather than converting it.\n */\nexport interface MoneyOptions extends Locale, Signed {\n /** An ISO 4217 code: `USD`, `EUR`, `JPY`. */\n readonly currency: string\n readonly unit: 'minor' | 'decimal'\n}\n\ntype MoneyData = MoneyOptions & { readonly scale: number }\n\nconst currencyOf = (data: MoneyData) => ({ style: 'currency', currency: data.currency }) as const\n\n/** Digits of an integer number of minor units, or none for one that is not a safe integer. */\nconst minorDigits = (value: number, scale: number): Option.Option<Digits> => {\n if (!Number.isSafeInteger(value)) return Option.none()\n const all = String(Math.abs(value)).padStart(scale + 1, '0')\n return Option.some({\n negative: value < 0,\n whole: all.slice(0, all.length - scale),\n fraction: all.slice(all.length - scale),\n })\n}\n\n/** The decimal text a stored amount of money stands for, or none for a value of the wrong form. */\nconst moneyText = (value: unknown, data: MoneyData): Option.Option<string> =>\n Option.map(\n Match.value(data.unit).pipe(\n Match.when('minor', () =>\n typeof value === 'number' ? minorDigits(value, data.scale) : Option.none(),\n ),\n Match.when('decimal', () =>\n typeof value === 'string' ? digitsOf(value, data.scale) : Option.none(),\n ),\n Match.exhaustive,\n ),\n decimalText,\n )\n\n/** The value a typed amount of money stands for, in the stored form. */\nconst moneyValue = (digits: Digits, data: MoneyData): Option.Option<unknown> =>\n Match.value(data.unit).pipe(\n Match.when('decimal', () => Option.some<unknown>(decimalText(digits))),\n Match.when('minor', () => {\n const sign = digits.negative ? '-' : ''\n const minor = Number(BigInt(`${sign}${digits.whole}${digits.fraction}`))\n return Number.isSafeInteger(minor) ? Option.some<unknown>(minor) : Option.none()\n }),\n Match.exhaustive,\n )\n\nconst MoneyKind = Input.kind<MoneyData>('FieldRecipes.Money', {\n draft: 'text',\n parse: (draft, data) =>\n Option.getOrUndefined(\n Option.flatMap(\n readDigits(\n draft,\n data.locale ?? 'en',\n currencyOf(data),\n data.scale,\n data.negative === true,\n ),\n digits => moneyValue(digits, data),\n ),\n ),\n format: (value, data) =>\n Option.match(moneyText(value, data), {\n onNone: () => {\n throw new Error(\n `an amount of ${data.currency} in ${data.unit} units is ${data.unit === 'minor' ? 'an integer' : 'decimal text'} of at most ${data.scale} decimals, not ${JSON.stringify(value)}`,\n )\n },\n onSome: amount => writeAmount(amount, data.locale ?? 'en', currencyOf(data)),\n }),\n unparsed: 'Enter an amount',\n})\n\nconst MoneyDisplay = Display.kind<MoneyData>('FieldRecipes.Money', {\n text: (data, value) =>\n Option.match(moneyText(value, data), {\n onNone: () => String(value),\n onSome: amount => writeAmount(amount, data.locale ?? 'en', currencyOf(data)),\n }),\n})\n\n/** A number with at most `scale` decimals, kept as a number. */\nexport interface DecimalOptions extends Locale, Signed {\n readonly scale: number\n}\n\n/** Throws, when a recipe is made, for a scale no format can write. */\nconst scaleOf = (recipe: string, scale: number, most: number): void => {\n if (!Number.isInteger(scale) || scale < 0 || scale > most)\n throw new Error(\n `FieldRecipes.${recipe}: scale is a whole number from 0 to ${most}, not ${scale}`,\n )\n}\n\n/**\n * A number's shortest decimal text, the one `String` writes, never in exponent\n * form. `toFixed` would print the double's binary expansion instead past 17\n * significant digits: `1234.1` at 13 decimals is `1234.0999999999999`.\n */\nconst plainOf = (value: number): string => {\n const written = String(value)\n const parts = /^(-?)(\\d+)(?:\\.(\\d+))?e([+-]\\d+)$/.exec(written)\n if (parts === null) return written\n const [, sign = '', whole = '', fraction = '', exponent = '0'] = parts\n const digits = whole + fraction\n const point = whole.length + Number(exponent)\n if (point <= 0) return `${sign}0.${'0'.repeat(-point)}${digits}`\n if (point >= digits.length) return sign + digits + '0'.repeat(point - digits.length)\n return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`\n}\n\n/** A number's decimal text with `scale` decimals, or none when it has more. */\nconst fixed = (value: unknown, scale: number): Option.Option<string> =>\n typeof value === 'number' && Number.isFinite(value)\n ? Option.map(digitsOf(plainOf(value), scale), decimalText)\n : Option.none()\n\n/**\n * A decimal text as a number, or none when no number holds it exactly: past\n * about 15 significant digits a double rounds, and the typed amount would not\n * be what is stored.\n */\nconst exact = (text: string, scale: number): Option.Option<number> => {\n const value = Number(text)\n return Option.contains(fixed(value, scale), text) ? Option.some(value) : Option.none()\n}\n\nconst decimalOf = (data: DecimalOptions) => ({ maximumFractionDigits: data.scale }) as const\n\nconst DecimalKind = Input.kind<DecimalOptions>('FieldRecipes.Decimal', {\n draft: 'text',\n parse: (draft, data) =>\n Option.getOrUndefined(\n Option.flatMap(\n readDigits(draft, data.locale ?? 'en', decimalOf(data), data.scale, data.negative === true),\n digits => exact(decimalText(digits), data.scale),\n ),\n ),\n format: (value, data) =>\n Option.match(fixed(value, data.scale), {\n onNone: () => {\n throw new Error(`a number of at most ${data.scale} decimals, not ${JSON.stringify(value)}`)\n },\n onSome: amount => writeAmount(amount, data.locale ?? 'en', decimalOf(data)),\n }),\n unparsed: 'Enter a number',\n})\n\nconst DecimalDisplay = Display.kind<DecimalOptions>('FieldRecipes.Decimal', {\n text: (data, value) =>\n Option.match(fixed(value, data.scale), {\n onNone: () => String(value),\n onSome: amount => writeAmount(amount, data.locale ?? 'en', decimalOf(data)),\n }),\n})\n\n/**\n * A percentage, drawn as `12.5%`. `fraction` stores it as the share (`0.125`),\n * `points` as the number before the sign (`12.5`). `scale` is the decimals of\n * the percentage as shown (default 2).\n */\nexport interface PercentOptions extends Locale, Signed {\n readonly unit: 'fraction' | 'points'\n readonly scale?: number | undefined\n}\n\nconst percentOf = (data: PercentOptions) =>\n ({ style: 'percent', maximumFractionDigits: data.scale ?? 2 }) as const\n\n/** The share a stored percentage stands for, as decimal text, or none for one that would round. */\nconst shareText = (value: unknown, data: PercentOptions): Option.Option<string> => {\n const scale = data.scale ?? 2\n return Match.value(data.unit).pipe(\n Match.when('fraction', () => fixed(value, scale + 2)),\n Match.when('points', () =>\n Option.map(\n Option.flatMap(fixed(value, scale), points => digitsOf(points, scale)),\n points => decimalText(shifted(points, 2)),\n ),\n ),\n Match.exhaustive,\n )\n}\n\nconst PercentKind = Input.kind<PercentOptions>('FieldRecipes.Percent', {\n draft: 'text',\n parse: (draft, data) =>\n Option.getOrUndefined(\n Option.flatMap(\n readDigits(\n draft,\n data.locale ?? 'en',\n percentOf(data),\n data.scale ?? 2,\n data.negative === true,\n ),\n points =>\n Match.value(data.unit).pipe(\n Match.when('points', () => exact(decimalText(points), data.scale ?? 2)),\n Match.when('fraction', () =>\n exact(decimalText(shifted(points, 2)), (data.scale ?? 2) + 2),\n ),\n Match.exhaustive,\n ),\n ),\n ),\n format: (value, data) =>\n Option.match(shareText(value, data), {\n onNone: () => {\n throw new Error(\n `a percentage of at most ${data.scale ?? 2} decimals, not ${JSON.stringify(value)}`,\n )\n },\n onSome: share => writeAmount(share, data.locale ?? 'en', percentOf(data)),\n }),\n unparsed: 'Enter a percentage',\n})\n\nconst PercentDisplay = Display.kind<PercentOptions>('FieldRecipes.Percent', {\n text: (data, value) =>\n Option.match(shareText(value, data), {\n onNone: () => String(value),\n onSome: share => writeAmount(share, data.locale ?? 'en', percentOf(data)),\n }),\n})\n\nexport const FieldRecipes = {\n /** The control kinds, so a renderer table is keyed by them and `is` narrows a control. */\n Kinds: {\n Date: DateKind,\n DateTime: DateTimeKind,\n Email: EmailKind,\n Url: UrlKind,\n Tel: TelKind,\n Password: PasswordKind,\n Slug: SlugKind,\n Money: MoneyKind,\n Decimal: DecimalKind,\n Percent: PercentKind,\n },\n /** The display kinds, likewise. */\n Displays: {\n Date: DateDisplay,\n DateTime: DateTimeDisplay,\n Money: MoneyDisplay,\n Decimal: DecimalDisplay,\n Percent: PercentDisplay,\n },\n\n /**\n * A calendar date: a key whose value is a `CalendarDate` (`Calendar.CalendarDate`,\n * or `Calendar.CalendarDateFromIsoString` to store it as text), edited as the\n * `YYYY-MM-DD` a date input writes. A partly typed date reads `Enter a date`.\n */\n date: (options: Locale = {}): FieldRecipe<Control<Locale>> => ({\n control: DateKind.of(options),\n display: DateDisplay.of(options),\n }),\n\n /**\n * A date and a time, stored as `options.stored` says (see `Stored`). With\n * `instant`, a time the zone's clocks skip is refused, and one they repeat\n * reads as the earlier of the two. An unknown zone throws here.\n */\n dateTime: (options: Stored & Locale): FieldRecipe<Control<DateTimeData>> => {\n if (options.stored === 'instant') knownZone(options.zone, 'FieldRecipes.dateTime')\n return { control: DateTimeKind.of(options), display: DateTimeDisplay.of(options) }\n },\n\n /**\n * An email address: one `@` with something either side and no space, kept as\n * typed (neither trimmed nor lower-cased: a local part may be case-sensitive).\n */\n email: (options: Autofill = {}): FieldRecipe<Control<Autofill>> => ({\n control: EmailKind.of(options),\n display: text,\n }),\n\n /** A web address with one of `options.protocols`, kept as typed. */\n url: (options: Schemes = {}): FieldRecipe<Control<Schemes>> => {\n // A scheme as `URL` writes one, or nothing could ever match it.\n for (const protocol of options.protocols ?? [])\n if (!/^[a-z][a-z0-9+.-]*:$/.test(protocol))\n throw new Error(\n `FieldRecipes.url: \"${protocol}\" is not a scheme as URL writes it, such as \"https:\"`,\n )\n return { control: UrlKind.of(options), display: text }\n },\n\n /** A telephone number, as typed: the phone's keypad and the browser's autofill, no check. */\n tel: (options: Autofill = {}): FieldRecipe<Control<Autofill>> => ({\n control: TelKind.of(options),\n display: text,\n }),\n\n /**\n * A password, drawn masked and named for the browser's password manager by\n * `purpose`. It never shows in a list. Keep it out of any saved draft: a form\n * cannot leave a key out of `partial()`, so that rule is the application's.\n */\n password: (options: Purpose): FieldRecipe<Control<Purpose>> => ({\n control: PasswordKind.of(options),\n display: Display.hidden(),\n }),\n\n /**\n * An address that follows the key `from` (made into a slug) until the person\n * writes it, shown after `prefix`. A form filled with a stored slug does not\n * follow it: a published address must not move because its title did.\n */\n slug: (\n from: string,\n options: { readonly prefix?: string; readonly through?: (text: string) => string } = {},\n ): FieldRecipe<ControlChange> => ({\n control: {\n change: (resolved, inputKey) => {\n const following = Input.following(from, options.through ?? slugify).change(\n resolved,\n inputKey,\n )\n return Object.freeze({\n ...SlugKind.of({ prefix: options.prefix ?? '/' }),\n follows: following.follows,\n })\n },\n },\n display: text,\n }),\n\n /** Text as an address: lower case, unaccented, words joined by `-`. What `slug` follows with. */\n slugify,\n\n /**\n * An amount of money in `currency`, shown and read as `locale` writes it\n * (`$1,234.56`, `1.234,56 €`, `¥1,235`), with the currency's own decimals: none\n * for JPY, three for KWD. More decimals than that are refused, not rounded, and\n * grouping other than the locale's (`12,3`) is refused, not read as `123`. A\n * code that is not three capital letters throws here.\n */\n money: (options: MoneyOptions): FieldRecipe<Control<MoneyData>> => {\n // The shape only: `Intl.supportedValuesOf('currency')` lists current tender,\n // which leaves out `XAU` and `CLF` and differs between ICU releases.\n if (!/^[A-Z]{3}$/.test(options.currency))\n throw new Error(`FieldRecipes.money: \"${options.currency}\" is not an ISO 4217 code`)\n const scale =\n new Intl.NumberFormat(options.locale ?? 'en', {\n style: 'currency',\n currency: options.currency,\n }).resolvedOptions().maximumFractionDigits ?? 2\n const data = { ...options, scale }\n return { control: MoneyKind.of(data), display: MoneyDisplay.of(data) }\n },\n\n /** A number with at most `scale` decimals, read and shown in `locale`'s separators. */\n decimal: (options: DecimalOptions): FieldRecipe<Control<DecimalOptions>> => {\n scaleOf('decimal', options.scale, 20)\n return { control: DecimalKind.of(options), display: DecimalDisplay.of(options) }\n },\n\n /** A percentage, stored as a share or as points (see `PercentOptions`). */\n percent: (options: PercentOptions): FieldRecipe<Control<PercentOptions>> => {\n // A share keeps two more decimals than the points it is shown as.\n scaleOf('percent', options.scale ?? 2, 18)\n return { control: PercentKind.of(options), display: PercentDisplay.of(options) }\n },\n}\n"],"mappings":";;;;;;;;;;;AAyBA,MAAM,QAAQ;AACd,MAAM,aAAa;AACnB,MAAM,cAAc;AACpB,MAAM,SAAS;AACf,MAAM,YAAY;;;AAGlB,MAAM,OAAO;;AAEb,MAAM,YAAY;AAElB,MAAM,+BAAe,IAAI,IAAqB;;;;;;;AAQ9C,MAAM,aAAa,QAAgB,YAA+C;CAChF,MAAM,MAAM,KAAK,UAAU,CAAC,QAAQ,OAAO,CAAC;CAC5C,MAAM,QAAQ,aAAa,IAAI,GAAG;CAClC,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,MAAM,QAAQ,IAAI,KAAK,aAAa,QAAQ;EAC1C,GAAG;EACH,iBAAiB;EAEjB,uBAAuB;EACvB,uBAAuB;CACzB,CAAC,CAAC,CAAC,cAAc,UAAU;CAC3B,MAAM,MAAM,SAAqC,MAAM,MAAK,SAAQ,KAAK,SAAS,IAAI,CAAC,EAAE;CAEzF,MAAM,QAAQ,IAAI,KAAK,aAAa,QAAQ;EAC1C,GAAG;EACH,GAAI,QAAQ,UAAU,YAAY,EAAE,OAAO,UAAU,IAAI,CAAC;EAC1D,iBAAiB;EACjB,aAAa;EACb,uBAAuB;EACvB,uBAAuB;CACzB,CAAC;CAGD,MAAM,OAAgB;EACpB,QAAQ,GAAG,OAAO,KAAK,IAAA,CAAK,UAAU,MAAM;EAC5C,UAAU,GAAG,SAAS,KAAK,IAAA,CAAK,UAAU,MAAM;EAChD,SAAS,MACN,QAAO,SAAQ,KAAK,SAAS,cAAc,KAAK,SAAS,aAAa,CAAC,CACvE,KAAI,SAAQ,KAAK,MAAM,UAAU,MAAM,CAAC;EAC3C,UAAS,WACP,MACG,cAAc,OAAO,MAAM,CAAC,CAAC,CAC7B,QAAO,SAAQ,KAAK,SAAS,aAAa,KAAK,SAAS,OAAO,CAAC,CAChE,KAAI,SAAQ,KAAK,KAAK,CAAC,CACvB,KAAK,EAAE,CAAC,CACR,UAAU,MAAM;CACvB;CACA,aAAa,IAAI,KAAK,IAAI;CAC1B,OAAO;AACT;;;;;;;;AASA,MAAa,cACX,OACA,QACA,SACA,OACA,aAC0B;CAC1B,MAAM,UAAU,UAAU,QAAQ,OAAO;CAKzC,IAAI,OAAO,MACR,QAAQ,YAAW,SAAQ,KAAK,UAAU,MAAM,CAAC,CAAC,CAClD,QAAQ,MAAM,EAAE,CAAC,CACjB,KAAK;CAER,IAAI,SAAS,MAAM,KAAK,IAAI;CAC5B,IAAI,QAAQ,OAAO,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK;CACtC,MAAM,QAAQ,QAAQ,QAAQ,MAAK,SAAQ,KAAK,WAAW,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC;CACvF,IAAI,UAAU,KAAA,GACZ,QAAQ,KAAK,WAAW,KAAK,IAAI,KAAK,MAAM,MAAM,MAAM,IAAI,KAAK,MAAM,GAAG,CAAC,MAAM,MAAM,EAAA,CAAG,KAAK;CACjG,IAAI,CAAC,UAAU,MAAM,KAAK,IAAI,GAAG;EAC/B,SAAS;EACT,OAAO,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK;CAC5B;CACA,IAAI,UAAU,CAAC,UAAU,OAAO,OAAO,KAAK;CAG5C,IAAI,OAAO,KAAK,QAAQ,KAAK,GAAG,OAAO,KAAK,QAAQ,WAAW,QAAQ,KAAK;MACvE,IAAI,OAAO,KAAK,IAAI,GAAG,OAAO,OAAO,KAAK;CAG/C,IAAI,WAAW,KAAK,QAAQ,KAAK,GAAG,OAAO,KAAK,QAAQ,aAAa,QAAQ,KAAK;CAClF,MAAM,CAAC,QAAQ,IAAI,WAAW,IAAI,GAAG,QAAQ,KAAK,MAAM,QAAQ,OAAO;CACvE,IAAI,KAAK,SAAS,KAAM,UAAU,MAAM,aAAa,IAAK,OAAO,OAAO,KAAK;CAC7E,IAAI,CAAC,QAAQ,KAAK,QAAQ,KAAK,SAAS,SAAS,OAAO,OAAO,OAAO,KAAK;CAC3E,MAAM,SAAS,MAAM,MAAM,QAAQ,KAAK,CAAC,CAAC,KAAK,EAAE;CACjD,IAAI,CAAC,QAAQ,KAAK,MAAM,GAAG,OAAO,OAAO,KAAK;CAE9C,IAAI,WAAW,SAAS,QAAQ,QAAQ,MAAM,MAAM,OAAO,OAAO,OAAO,KAAK;CAC9E,OAAO,OAAO,KAAK;EACjB,UAAU;EACV,OAAO,OAAO,QAAQ,aAAa,EAAE,KAAK;EAC1C,UAAU,SAAS,OAAO,OAAO,GAAG;CACtC,CAAC;AACH;;AAGA,MAAa,eAAe,EAAE,UAAU,OAAO,eAA+B;CAC5E,MAAM,WAAW,aAAa,KAAK,QAAQ,GAAG,MAAM,GAAG;CACvD,OAAO,YAAY,QAAQ,KAAK,QAAQ,IAAI,IAAI,aAAa;AAC/D;AAEA,MAAM,iBAAiB;;AAGvB,MAAa,YAAY,MAAc,UAAyC;CAC9E,MAAM,QAAQ,eAAe,KAAK,IAAI;CACtC,IAAI,UAAU,MAAM,OAAO,OAAO,KAAK;CACvC,MAAM,GAAG,MAAM,QAAQ,KAAK,WAAW,MAAM;CAC7C,IAAI,SAAS,QAAQ,OAAO,EAAE,CAAC,CAAC,SAAS,OAAO,OAAO,OAAO,KAAK;CACnE,OAAO,OAAO,KAAK;EACjB,UAAU,SAAS;EACnB,OAAO,MAAM,QAAQ,aAAa,EAAE;EACpC,UAAU,SAAS,MAAM,GAAG,KAAK,CAAC,CAAC,OAAO,OAAO,GAAG;CACtD,CAAC;AACH;;AAGA,MAAa,WAAW,QAAgB,WAA2B;CACjE,MAAM,OAAO,OAAO,QAAQ,OAAO,SAAA,CAAU,SAAS,OAAO,SAAS,SAAS,SAAS,GAAG,GAAG;CAC9F,MAAM,QAAQ,IAAI,SAAS,OAAO,SAAS,SAAS;CACpD,OAAO;EACL,UAAU,OAAO;EACjB,OAAO,IAAI,MAAM,GAAG,KAAK,CAAC,CAAC,QAAQ,aAAa,EAAE;EAClD,UAAU,IAAI,MAAM,KAAK;CAC3B;AACF;AAEA,MAAM,8BAAc,IAAI,IAA+B;;AAGvD,MAAa,eACX,MACA,QACA,YACW;CACX,MAAM,MAAM,KAAK,UAAU,CAAC,QAAQ,OAAO,CAAC;CAC5C,MAAM,QAAQ,YAAY,IAAI,GAAG;CACjC,MAAM,SAAS,SAAS,IAAI,KAAK,aAAa,QAAQ;EAAE,GAAG;EAAS,iBAAiB;CAAO,CAAC;CAC7F,IAAI,UAAU,KAAA,GAAW,YAAY,IAAI,KAAK,MAAM;CAGpD,OAAO,OAAO,OAAO,IAAyB;AAChD;;;;;;;ACvKA,MAAM,QAAQ;;AAGd,MAAa,YAAY,SAAsC;CAC7D,MAAM,QAAQ,MAAM,KAAK,IAAI;CAC7B,IAAI,UAAU,MAAM,OAAO,OAAO,KAAK;CACvC,MAAM,CAAC,MAAM,OAAO,KAAK,MAAM,QAAQ,SAAS,KAAK,WAAW,MAAM,MAAM,MAAM,CAAC;CACnF,MAAM,OAAa;EACjB,MAAM,OAAO,IAAI;EACjB,OAAO,OAAO,KAAK;EACnB,KAAK,OAAO,GAAG;EACf,MAAM,OAAO,IAAI;EACjB,QAAQ,OAAO,MAAM;EACrB,QAAQ,OAAO,MAAM;EACrB,aAAa,OAAO,SAAS,OAAO,GAAG,GAAG,CAAC;CAC7C;CAMA,OAJE,eAAe;EAAE,MAAM,KAAK;EAAM,OAAO,KAAK;EAAO,KAAK,KAAK;CAAI,CAAC,KACpE,KAAK,OAAO,MACZ,KAAK,SAAS,MACd,KAAK,SAAS,KACF,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK;AAChD;AAEA,MAAMA,SAAO,UAAkB,OAAO,KAAK,CAAC,CAAC,SAAS,GAAG,GAAG;;AAG5D,MAAa,aAAa,SAAuB;CAC/C,MAAM,OAAO,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,GAAGA,MAAI,KAAK,KAAK,EAAE,GAAGA,MAAI,KAAK,GAAG;CACrF,MAAM,UACJ,KAAK,WAAW,KAAK,KAAK,gBAAgB,IACtC,KACA,IAAIA,MAAI,KAAK,MAAM,IAAI,KAAK,gBAAgB,IAAI,KAAK,IAAI,OAAO,KAAK,WAAW,CAAC,CAAC,SAAS,GAAG,GAAG;CACvG,OAAO,GAAG,KAAK,GAAGA,MAAI,KAAK,IAAI,EAAE,GAAGA,MAAI,KAAK,MAAM,IAAI;AACzD;;;;;AAMA,MAAa,SAAS,SAAuB;CAC3C,MAAM,qBAAK,IAAI,KAAK,CAAC;CACrB,GAAG,eAAe,KAAK,MAAM,KAAK,QAAQ,GAAG,KAAK,GAAG;CACrD,GAAG,YAAY,KAAK,MAAM,KAAK,QAAQ,KAAK,QAAQ,KAAK,WAAW;CACpE,OAAO,GAAG,QAAQ;AACpB;AAEA,MAAM,8BAAc,IAAI,IAAiC;;AAGzD,MAAa,cACX,QACA,YACwB;CACxB,MAAM,MAAM,KAAK,UAAU,CAAC,QAAQ,OAAO,CAAC;CAC5C,MAAM,QAAQ,YAAY,IAAI,GAAG;CACjC,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,MAAM,OAAO,IAAI,KAAK,eAAe,QAAQ,OAAO;CACpD,YAAY,IAAI,KAAK,IAAI;CACzB,OAAO;AACT;AAGA,MAAM,6BAAa,IAAI,IAAiC;AACxD,MAAM,eAAe,SAAsC;CACzD,MAAM,QAAQ,WAAW,IAAI,IAAI;CACjC,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,MAAM,OAAO,IAAI,KAAK,eAAe,SAAS;EAC5C,UAAU;EACV,WAAW;EACX,MAAM;EACN,OAAO;EACP,KAAK;EACL,MAAM;EACN,QAAQ;EACR,QAAQ;EACR,wBAAwB;CAC1B,CAAC;CACD,WAAW,IAAI,MAAM,IAAI;CACzB,OAAO;AACT;;AAGA,MAAa,aAAa,MAAc,QAAwB;CAC9D,IAAI;EACF,YAAY,IAAI;CAClB,QAAQ;EACN,MAAM,IAAI,MAAM,GAAG,IAAI,KAAK,KAAK,wCAAwC;CAC3E;CACA,OAAO;AACT;;AAGA,MAAa,UAAU,SAAiB,SAAuB;CAC7D,MAAM,QAAQ,OAAO,YACnB,YAAY,IAAI,CAAC,CACd,cAAc,IAAI,KAAK,OAAO,CAAC,CAAC,CAChC,KAAI,SAAQ,CAAC,KAAK,MAAM,KAAK,KAAK,CAAC,CACxC;CACA,OAAO;EACL,MAAM,OAAO,MAAM,OAAO;EAC1B,OAAO,OAAO,MAAM,QAAQ;EAC5B,KAAK,OAAO,MAAM,MAAM;EACxB,MAAM,OAAO,MAAM,OAAO;EAC1B,QAAQ,OAAO,MAAM,SAAS;EAC9B,QAAQ,OAAO,MAAM,SAAS;EAC9B,aAAa,OAAO,MAAM,mBAAmB;CAC/C;AACF;AAEA,MAAM,MAAM;;;;;;;AAQZ,MAAa,aAAa,MAAY,SAAwC;CAC5E,MAAM,QAAQ,MAAM,IAAI;CAIxB,MAAM,WAAW,CAAC,GAAG,IAHD,IAClB,CAAC,QAAQ,KAAK,QAAQ,GAAG,CAAC,CAAC,KAAI,WAAU,MAAM,OAAO,QAAQ,IAAI,CAAC,IAAI,MAAM,CAEpD,CAAC,CAAC,CAC1B,KAAI,WAAU,QAAQ,MAAM,CAAC,CAC7B,QAAO,YAAW,MAAM,OAAO,SAAS,IAAI,CAAC,MAAM,KAAK;CAC3D,OAAO,SAAS,WAAW,IAAI,OAAO,KAAK,IAAI,OAAO,KAAK,KAAK,IAAI,GAAG,QAAQ,CAAC;AAClF;;;;;;;;;;;AClHA,MAAM,OAAO,UAAkB,OAAO,KAAK,CAAC,CAAC,SAAS,GAAG,GAAG;AAE5D,MAAM,UAAU;;AAGhB,MAAM,YAAY,SAA2C;CAC3D,MAAM,QAAQ,QAAQ,KAAK,IAAI;CAC/B,IAAI,UAAU,MAAM,OAAO,KAAA;CAC3B,MAAM,OAAO;EAAE,MAAM,OAAO,MAAM,EAAE;EAAG,OAAO,OAAO,MAAM,EAAE;EAAG,KAAK,OAAO,MAAM,EAAE;CAAE;CACtF,OAAO,eAAe,IAAI,IAAI,OAAO,KAAA;AACvC;AAEA,MAAM,aAAa,SACjB,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,GAAG,IAAI,KAAK,KAAK,EAAE,GAAG,IAAI,KAAK,GAAG;AAE1E,MAAM,WAAW,MAAM,KAAa,qBAAqB;CACvD,OAAO;CACP,QAAO,UAAS,SAAS,KAAK;CAC9B,SAAQ,UAAS;EACf,IAAI,eAAe,KAAK,GAAG,OAAO,UAAU,KAAK;EACjD,MAAM,IAAI,MAAM,iCAAiC,KAAK,UAAU,KAAK,GAAG;CAC1E;CACA,UAAU;AACZ,CAAC;AAED,MAAM,cAAc,QAAQ,KAAa,qBAAqB,EAC5D,OAAO,MAAM,UACX,eAAe,KAAK,IAChB,WAAW,KAAK,UAAU,MAAM;CAAE,WAAW;CAAU,UAAU;AAAM,CAAC,CAAC,CAAC,OACxE,MAAM;CAAE,GAAG;CAAO,MAAM;CAAG,QAAQ;CAAG,QAAQ;CAAG,aAAa;AAAE,CAAC,CACnE,IACA,OAAO,KAAK,EACpB,CAAC;AAqBD,MAAM,QAAQ;CAAE,WAAW;CAAU,WAAW;AAAQ;AAExD,MAAM,cAAc;AAEpB,MAAM,QAAc;CAElB,QAAO,UAAS,OAAO,GAAG,SAAS,KAAK,GAAG,KAAK;CAChD,SAAQ,UAAS;EACf,IAAI,OAAO,UAAU,YAAY,OAAO,OAAO,SAAS,KAAK,CAAC,GAAG,OAAO;EACxE,MAAM,IAAI,MACR,+DAA+D,KAAK,UAAU,KAAK,GACrF;CACF;CACA,OAAO,OAAO,WACZ,OAAO,MAAM,SAAS,KAAK,GAAG;EAC5B,cAAc;EACd,SAAQ,SAAQ,WAAW,QAAQ;GAAE,GAAG;GAAO,UAAU;EAAM,CAAC,CAAC,CAAC,OAAO,MAAM,IAAI,CAAC;CACtF,CAAC;AACL;AAEA,MAAM,WAAW,UAAwB;CACvC,QAAO,UACL,OAAO,IAGL,OAAO,OACL,OAAO,QAAQ,SAAS,KAAK,IAAG,SAAQ,UAAU,MAAM,IAAI,CAAC,IAC7D,OAAM,IAAI,KAAK,EAAE,CAAC,CAAC,eAAe,KAAK,IACzC,IACA,OAAM,IAAI,KAAK,EAAE,CAAC,CAAC,YAAY,CACjC;CACF,SAAQ,UAAS;EACf,MAAM,KAAK,OAAO,UAAU,YAAY,YAAY,KAAK,KAAK,IAAI,KAAK,MAAM,KAAK,IAAI;EACtF,IAAI,CAAC,OAAO,MAAM,EAAE,GAAG,OAAO,UAAU,OAAO,IAAI,IAAI,CAAC;EACxD,MAAM,IAAI,MAAM,8CAA8C,KAAK,UAAU,KAAK,GAAG;CACvF;CACA,OAAO,OAAO,WAAW;EACvB,MAAM,KAAK,YAAY,KAAK,KAAK,IAAI,KAAK,MAAM,KAAK,IAAI;EACzD,OAAO,OAAO,MAAM,EAAE,IAAI,QAAQ,WAAW,QAAQ;GAAE,GAAG;GAAO,UAAU;EAAK,CAAC,CAAC,CAAC,OAAO,EAAE;CAC9F;AACF;AAEA,MAAM,UAAU,SACd,MAAM,MAAM,IAAI,CAAC,CAAC,KAChB,MAAM,yBAAyB,QAAQ,CAAC,CAAC;CACvC,aAAa;CACb,UAAU,EAAE,WAAW,QAAQ,IAAI;AACrC,CAAC,CACH;AAEF,MAAM,eAAe,MAAM,KAAmB,yBAAyB;CACrE,OAAO;CACP,QAAQ,OAAO,SAAS,OAAO,eAAe,OAAO,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;CACvE,SAAS,OAAO,SAAS,OAAO,IAAI,CAAC,CAAC,OAAO,KAAK;CAClD,UAAU;AACZ,CAAC;AAED,MAAM,kBAAkB,QAAQ,KAAmB,yBAAyB,EAC1E,OAAO,MAAM,UAAU,OAAO,IAAI,CAAC,CAAC,KAAK,OAAO,KAAK,GAAG,KAAK,UAAU,IAAI,EAC7E,CAAC;AAUD,MAAM,UAAU;AAEhB,MAAM,YAAY,MAAM,KAAe,sBAAsB;CAC3D,OAAO;CACP,QAAO,UAAU,QAAQ,KAAK,KAAK,IAAI,QAAQ,KAAA;CAC/C,UAAU;AACZ,CAAC;AAOD,MAAM,UAAU,MAAM,KAAc,oBAAoB;CACtD,OAAO;CAGP,QAAQ,OAAO,SAAS;EACtB,IAAI,CAAC,IAAI,SAAS,KAAK,GAAG,OAAO,KAAA;EACjC,QAAQ,KAAK,aAAa,CAAC,UAAU,OAAO,EAAA,CAAG,SAAS,IAAI,IAAI,KAAK,CAAC,CAAC,QAAQ,IAC3E,QACA,KAAA;CACN;CACA,UAAU;AACZ,CAAC;AAID,MAAM,UAAU,MAAM,KAAe,oBAAoB,EAAE,OAAO,OAAO,CAAC;AAO1E,MAAM,eAAe,MAAM,KAAc,yBAAyB,EAAE,OAAO,OAAO,CAAC;AAOnF,MAAM,WAAW,MAAM,KAAa,qBAAqB,EAAE,OAAO,OAAO,CAAC;AAG1E,MAAM,aAA+C;CACnD,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;AACL;AACA,MAAM,UAAU,IAAI,OAAO,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC,KAAK,EAAE,EAAE,IAAI,GAAG;AAEvE,MAAM,WAAW,SACf,KACG,YAAY,CAAC,CACb,QAAQ,UAAS,WAAU,WAAW,WAAW,MAAM,CAAC,CACxD,UAAU,MAAM,CAAC,CACjB,QAAQ,oBAAoB,EAAE,CAAC,CAC/B,YAAY,CAAC,CACb,QAAQ,eAAe,GAAG,CAAC,CAC3B,QAAQ,YAAY,EAAE;AAE3B,MAAM,OAAO,QAAQ,KAAK;AAqB1B,MAAM,cAAc,UAAqB;CAAE,OAAO;CAAY,UAAU,KAAK;AAAS;;AAGtF,MAAM,eAAe,OAAe,UAAyC;CAC3E,IAAI,CAAC,OAAO,cAAc,KAAK,GAAG,OAAO,OAAO,KAAK;CACrD,MAAM,MAAM,OAAO,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,SAAS,QAAQ,GAAG,GAAG;CAC3D,OAAO,OAAO,KAAK;EACjB,UAAU,QAAQ;EAClB,OAAO,IAAI,MAAM,GAAG,IAAI,SAAS,KAAK;EACtC,UAAU,IAAI,MAAM,IAAI,SAAS,KAAK;CACxC,CAAC;AACH;;AAGA,MAAM,aAAa,OAAgB,SACjC,OAAO,IACL,MAAM,MAAM,KAAK,IAAI,CAAC,CAAC,KACrB,MAAM,KAAK,eACT,OAAO,UAAU,WAAW,YAAY,OAAO,KAAK,KAAK,IAAI,OAAO,KAAK,CAC3E,GACA,MAAM,KAAK,iBACT,OAAO,UAAU,WAAW,SAAS,OAAO,KAAK,KAAK,IAAI,OAAO,KAAK,CACxE,GACA,MAAM,UACR,GACA,WACF;;AAGF,MAAM,cAAc,QAAgB,SAClC,MAAM,MAAM,KAAK,IAAI,CAAC,CAAC,KACrB,MAAM,KAAK,iBAAiB,OAAO,KAAc,YAAY,MAAM,CAAC,CAAC,GACrE,MAAM,KAAK,eAAe;CACxB,MAAM,OAAO,OAAO,WAAW,MAAM;CACrC,MAAM,QAAQ,OAAO,OAAO,GAAG,OAAO,OAAO,QAAQ,OAAO,UAAU,CAAC;CACvE,OAAO,OAAO,cAAc,KAAK,IAAI,OAAO,KAAc,KAAK,IAAI,OAAO,KAAK;AACjF,CAAC,GACD,MAAM,UACR;AAEF,MAAM,YAAY,MAAM,KAAgB,sBAAsB;CAC5D,OAAO;CACP,QAAQ,OAAO,SACb,OAAO,eACL,OAAO,QACL,WACE,OACA,KAAK,UAAU,MACf,WAAW,IAAI,GACf,KAAK,OACL,KAAK,aAAa,IACpB,IACA,WAAU,WAAW,QAAQ,IAAI,CACnC,CACF;CACF,SAAS,OAAO,SACd,OAAO,MAAM,UAAU,OAAO,IAAI,GAAG;EACnC,cAAc;GACZ,MAAM,IAAI,MACR,gBAAgB,KAAK,SAAS,MAAM,KAAK,KAAK,YAAY,KAAK,SAAS,UAAU,eAAe,eAAe,cAAc,KAAK,MAAM,iBAAiB,KAAK,UAAU,KAAK,GAChL;EACF;EACA,SAAQ,WAAU,YAAY,QAAQ,KAAK,UAAU,MAAM,WAAW,IAAI,CAAC;CAC7E,CAAC;CACH,UAAU;AACZ,CAAC;AAED,MAAM,eAAe,QAAQ,KAAgB,sBAAsB,EACjE,OAAO,MAAM,UACX,OAAO,MAAM,UAAU,OAAO,IAAI,GAAG;CACnC,cAAc,OAAO,KAAK;CAC1B,SAAQ,WAAU,YAAY,QAAQ,KAAK,UAAU,MAAM,WAAW,IAAI,CAAC;AAC7E,CAAC,EACL,CAAC;;AAQD,MAAM,WAAW,QAAgB,OAAe,SAAuB;CACrE,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,MACnD,MAAM,IAAI,MACR,gBAAgB,OAAO,sCAAsC,KAAK,QAAQ,OAC5E;AACJ;;;;;;AAOA,MAAM,WAAW,UAA0B;CACzC,MAAM,UAAU,OAAO,KAAK;CAC5B,MAAM,QAAQ,oCAAoC,KAAK,OAAO;CAC9D,IAAI,UAAU,MAAM,OAAO;CAC3B,MAAM,GAAG,OAAO,IAAI,QAAQ,IAAI,WAAW,IAAI,WAAW,OAAO;CACjE,MAAM,SAAS,QAAQ;CACvB,MAAM,QAAQ,MAAM,SAAS,OAAO,QAAQ;CAC5C,IAAI,SAAS,GAAG,OAAO,GAAG,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,IAAI;CACxD,IAAI,SAAS,OAAO,QAAQ,OAAO,OAAO,SAAS,IAAI,OAAO,QAAQ,OAAO,MAAM;CACnF,OAAO,GAAG,OAAO,OAAO,MAAM,GAAG,KAAK,EAAE,GAAG,OAAO,MAAM,KAAK;AAC/D;;AAGA,MAAM,SAAS,OAAgB,UAC7B,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAC9C,OAAO,IAAI,SAAS,QAAQ,KAAK,GAAG,KAAK,GAAG,WAAW,IACvD,OAAO,KAAK;;;;;;AAOlB,MAAM,SAAS,MAAc,UAAyC;CACpE,MAAM,QAAQ,OAAO,IAAI;CACzB,OAAO,OAAO,SAAS,MAAM,OAAO,KAAK,GAAG,IAAI,IAAI,OAAO,KAAK,KAAK,IAAI,OAAO,KAAK;AACvF;AAEA,MAAM,aAAa,UAA0B,EAAE,uBAAuB,KAAK,MAAM;AAEjF,MAAM,cAAc,MAAM,KAAqB,wBAAwB;CACrE,OAAO;CACP,QAAQ,OAAO,SACb,OAAO,eACL,OAAO,QACL,WAAW,OAAO,KAAK,UAAU,MAAM,UAAU,IAAI,GAAG,KAAK,OAAO,KAAK,aAAa,IAAI,IAC1F,WAAU,MAAM,YAAY,MAAM,GAAG,KAAK,KAAK,CACjD,CACF;CACF,SAAS,OAAO,SACd,OAAO,MAAM,MAAM,OAAO,KAAK,KAAK,GAAG;EACrC,cAAc;GACZ,MAAM,IAAI,MAAM,uBAAuB,KAAK,MAAM,iBAAiB,KAAK,UAAU,KAAK,GAAG;EAC5F;EACA,SAAQ,WAAU,YAAY,QAAQ,KAAK,UAAU,MAAM,UAAU,IAAI,CAAC;CAC5E,CAAC;CACH,UAAU;AACZ,CAAC;AAED,MAAM,iBAAiB,QAAQ,KAAqB,wBAAwB,EAC1E,OAAO,MAAM,UACX,OAAO,MAAM,MAAM,OAAO,KAAK,KAAK,GAAG;CACrC,cAAc,OAAO,KAAK;CAC1B,SAAQ,WAAU,YAAY,QAAQ,KAAK,UAAU,MAAM,UAAU,IAAI,CAAC;AAC5E,CAAC,EACL,CAAC;AAYD,MAAM,aAAa,UAChB;CAAE,OAAO;CAAW,uBAAuB,KAAK,SAAS;AAAE;;AAG9D,MAAM,aAAa,OAAgB,SAAgD;CACjF,MAAM,QAAQ,KAAK,SAAS;CAC5B,OAAO,MAAM,MAAM,KAAK,IAAI,CAAC,CAAC,KAC5B,MAAM,KAAK,kBAAkB,MAAM,OAAO,QAAQ,CAAC,CAAC,GACpD,MAAM,KAAK,gBACT,OAAO,IACL,OAAO,QAAQ,MAAM,OAAO,KAAK,IAAG,WAAU,SAAS,QAAQ,KAAK,CAAC,IACrE,WAAU,YAAY,QAAQ,QAAQ,CAAC,CAAC,CAC1C,CACF,GACA,MAAM,UACR;AACF;AAEA,MAAM,cAAc,MAAM,KAAqB,wBAAwB;CACrE,OAAO;CACP,QAAQ,OAAO,SACb,OAAO,eACL,OAAO,QACL,WACE,OACA,KAAK,UAAU,MACf,UAAU,IAAI,GACd,KAAK,SAAS,GACd,KAAK,aAAa,IACpB,IACA,WACE,MAAM,MAAM,KAAK,IAAI,CAAC,CAAC,KACrB,MAAM,KAAK,gBAAgB,MAAM,YAAY,MAAM,GAAG,KAAK,SAAS,CAAC,CAAC,GACtE,MAAM,KAAK,kBACT,MAAM,YAAY,QAAQ,QAAQ,CAAC,CAAC,IAAI,KAAK,SAAS,KAAK,CAAC,CAC9D,GACA,MAAM,UACR,CACJ,CACF;CACF,SAAS,OAAO,SACd,OAAO,MAAM,UAAU,OAAO,IAAI,GAAG;EACnC,cAAc;GACZ,MAAM,IAAI,MACR,2BAA2B,KAAK,SAAS,EAAE,iBAAiB,KAAK,UAAU,KAAK,GAClF;EACF;EACA,SAAQ,UAAS,YAAY,OAAO,KAAK,UAAU,MAAM,UAAU,IAAI,CAAC;CAC1E,CAAC;CACH,UAAU;AACZ,CAAC;AAED,MAAM,iBAAiB,QAAQ,KAAqB,wBAAwB,EAC1E,OAAO,MAAM,UACX,OAAO,MAAM,UAAU,OAAO,IAAI,GAAG;CACnC,cAAc,OAAO,KAAK;CAC1B,SAAQ,UAAS,YAAY,OAAO,KAAK,UAAU,MAAM,UAAU,IAAI,CAAC;AAC1E,CAAC,EACL,CAAC;AAED,MAAa,eAAe;;CAE1B,OAAO;EACL,MAAM;EACN,UAAU;EACV,OAAO;EACP,KAAK;EACL,KAAK;EACL,UAAU;EACV,MAAM;EACN,OAAO;EACP,SAAS;EACT,SAAS;CACX;;CAEA,UAAU;EACR,MAAM;EACN,UAAU;EACV,OAAO;EACP,SAAS;EACT,SAAS;CACX;;;;;;CAOA,OAAO,UAAkB,CAAC,OAAqC;EAC7D,SAAS,SAAS,GAAG,OAAO;EAC5B,SAAS,YAAY,GAAG,OAAO;CACjC;;;;;;CAOA,WAAW,YAAiE;EAC1E,IAAI,QAAQ,WAAW,WAAW,UAAU,QAAQ,MAAM,uBAAuB;EACjF,OAAO;GAAE,SAAS,aAAa,GAAG,OAAO;GAAG,SAAS,gBAAgB,GAAG,OAAO;EAAE;CACnF;;;;;CAMA,QAAQ,UAAoB,CAAC,OAAuC;EAClE,SAAS,UAAU,GAAG,OAAO;EAC7B,SAAS;CACX;;CAGA,MAAM,UAAmB,CAAC,MAAqC;EAE7D,KAAK,MAAM,YAAY,QAAQ,aAAa,CAAC,GAC3C,IAAI,CAAC,uBAAuB,KAAK,QAAQ,GACvC,MAAM,IAAI,MACR,sBAAsB,SAAS,qDACjC;EACJ,OAAO;GAAE,SAAS,QAAQ,GAAG,OAAO;GAAG,SAAS;EAAK;CACvD;;CAGA,MAAM,UAAoB,CAAC,OAAuC;EAChE,SAAS,QAAQ,GAAG,OAAO;EAC3B,SAAS;CACX;;;;;;CAOA,WAAW,aAAqD;EAC9D,SAAS,aAAa,GAAG,OAAO;EAChC,SAAS,QAAQ,OAAO;CAC1B;;;;;;CAOA,OACE,MACA,UAAqF,CAAC,OACtD;EAChC,SAAS,EACP,SAAS,UAAU,aAAa;GAC9B,MAAM,YAAY,MAAM,UAAU,MAAM,QAAQ,WAAW,OAAO,CAAC,CAAC,OAClE,UACA,QACF;GACA,OAAO,OAAO,OAAO;IACnB,GAAG,SAAS,GAAG,EAAE,QAAQ,QAAQ,UAAU,IAAI,CAAC;IAChD,SAAS,UAAU;GACrB,CAAC;EACH,EACF;EACA,SAAS;CACX;;CAGA;;;;;;;;CASA,QAAQ,YAA2D;EAGjE,IAAI,CAAC,aAAa,KAAK,QAAQ,QAAQ,GACrC,MAAM,IAAI,MAAM,wBAAwB,QAAQ,SAAS,0BAA0B;EACrF,MAAM,QACJ,IAAI,KAAK,aAAa,QAAQ,UAAU,MAAM;GAC5C,OAAO;GACP,UAAU,QAAQ;EACpB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,yBAAyB;EAChD,MAAM,OAAO;GAAE,GAAG;GAAS;EAAM;EACjC,OAAO;GAAE,SAAS,UAAU,GAAG,IAAI;GAAG,SAAS,aAAa,GAAG,IAAI;EAAE;CACvE;;CAGA,UAAU,YAAkE;EAC1E,QAAQ,WAAW,QAAQ,OAAO,EAAE;EACpC,OAAO;GAAE,SAAS,YAAY,GAAG,OAAO;GAAG,SAAS,eAAe,GAAG,OAAO;EAAE;CACjF;;CAGA,UAAU,YAAkE;EAE1E,QAAQ,WAAW,QAAQ,SAAS,GAAG,EAAE;EACzC,OAAO;GAAE,SAAS,YAAY,GAAG,OAAO;GAAG,SAAS,eAAe,GAAG,OAAO;EAAE;CACjF;AACF"}
|