foldkit-form-recipes 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +294 -2
- package/dist/html.d.mts +4 -0
- package/dist/html.d.mts.map +1 -0
- package/dist/html.mjs +57 -0
- package/dist/html.mjs.map +1 -0
- package/dist/image.d.mts +186 -0
- package/dist/image.d.mts.map +1 -0
- package/dist/image.mjs +320 -0
- package/dist/image.mjs.map +1 -0
- package/dist/index.d.mts +153 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1 -0
- package/dist/src-CutVZjTd.mjs +600 -0
- package/dist/src-CutVZjTd.mjs.map +1 -0
- package/dist/ui.d.mts +8 -0
- package/dist/ui.d.mts.map +1 -0
- package/dist/ui.mjs +26 -0
- package/dist/ui.mjs.map +1 -0
- package/package.json +101 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Glenn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,295 @@
|
|
|
1
|
-
#
|
|
1
|
+
# foldkit-form-recipes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Field recipes for [`foldkit-form`](../form/README.md). A recipe is what one kind
|
|
4
|
+
of key means: a **control**, which reads a draft into the key's value and shows a
|
|
5
|
+
stored value as a draft, and a **display**, which reads the value in a list or a
|
|
6
|
+
detail. Both are made from the same options, so a date reads the same in its
|
|
7
|
+
field and in its table cell, and the parsing that is easy to get quietly wrong
|
|
8
|
+
(a date typed month-first, a time the clocks skip) is written once.
|
|
9
|
+
|
|
10
|
+
## What it owns
|
|
11
|
+
|
|
12
|
+
| Fact | Owner |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| The key's schema, and so what a valid value is | your input schema |
|
|
15
|
+
| The drafts, their validity, and the submit | `foldkit-form` |
|
|
16
|
+
| How a draft becomes a value and a value a draft; how a value reads in a list | **this package** |
|
|
17
|
+
| The element that edits it | `foldkit-form-recipes/html` (or your own renderer) |
|
|
18
|
+
|
|
19
|
+
A recipe owns no state and sends no Message, except `image`, whose control
|
|
20
|
+
holds its upload (below). It attaches where any control does: a form's `inputs`, or Entity metadata through `Input.of` and `Display.of`.
|
|
21
|
+
|
|
22
|
+
## Mental model
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
stored value --format--> draft (what the input shows)
|
|
26
|
+
draft (typed) --parse--> value --schema--> valid, or the key's error
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`format` runs only when the form is filled, never on a keystroke, so the caret
|
|
30
|
+
and a composition survive typing. A draft `parse` cannot read is the recipe's
|
|
31
|
+
own error (`Enter a date`); an empty one is the form's (`Required`, or nothing
|
|
32
|
+
for an optional key).
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pnpm add foldkit-form-recipes foldkit-form foldkit-crud
|
|
38
|
+
# and, for the plain renderers:
|
|
39
|
+
pnpm add foldkit-mixins-form
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Sixty seconds
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { Schema } from 'effect'
|
|
46
|
+
import { Form } from 'foldkit-form'
|
|
47
|
+
import { FieldRecipes } from 'foldkit-form-recipes'
|
|
48
|
+
import { renderers } from 'foldkit-form-recipes/html'
|
|
49
|
+
import { FormView } from 'foldkit-mixins-form'
|
|
50
|
+
import { CalendarDate } from 'foldkit/calendar'
|
|
51
|
+
|
|
52
|
+
const due = FieldRecipes.date()
|
|
53
|
+
const startsAt = FieldRecipes.dateTime({ stored: 'instant', zone: 'Europe/London' })
|
|
54
|
+
|
|
55
|
+
const Meeting = Form.make(
|
|
56
|
+
'Meeting',
|
|
57
|
+
Form.input(Schema.Struct({ due: CalendarDate, startsAt: Schema.String })),
|
|
58
|
+
{ inputs: { due: due.control, startsAt: startsAt.control } },
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const View = FormView.define(Meeting, { renderers: renderers() })
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`due.control` and `startsAt.control` are plain `foldkit-form` controls: they do
|
|
65
|
+
no I/O and hold no state. `renderers()` draws them as `input type="date"` and
|
|
66
|
+
`type="datetime-local"`, named by their keys, so a post without scripts carries
|
|
67
|
+
the drafts. Filled with `{ due: Calendar.make(2026, 7, 1), startsAt:
|
|
68
|
+
'2026-07-01T08:30:00.000Z' }`, the inputs read `2026-07-01` and
|
|
69
|
+
`2026-07-01T09:30`, London being an hour ahead in July; typing `2026-07-01T09:30`
|
|
70
|
+
submits `'2026-07-01T08:30:00.000Z'`.
|
|
71
|
+
|
|
72
|
+
## The recipes
|
|
73
|
+
|
|
74
|
+
### `date(options?)`
|
|
75
|
+
|
|
76
|
+
A key whose value is a `CalendarDate`: `Calendar.CalendarDate`, or
|
|
77
|
+
`Calendar.CalendarDateFromIsoString` to store it as text. The draft is the
|
|
78
|
+
`YYYY-MM-DD` a date input writes. Anything else, a partly typed date, a day
|
|
79
|
+
first, or 30 February, reads `Enter a date`.
|
|
80
|
+
|
|
81
|
+
### `dateTime({ stored, zone?, locale? })`
|
|
82
|
+
|
|
83
|
+
A date and a time, stored in one of two forms, chosen explicitly:
|
|
84
|
+
|
|
85
|
+
- **`stored: 'local'`**: the key's value is the wall-clock text a
|
|
86
|
+
`datetime-local` input writes (`2026-10-26T09:30`), in no zone. It is kept as
|
|
87
|
+
typed, so a stored value comes back unchanged. Use it for a time that means a
|
|
88
|
+
clock reading wherever it is read: a shop's opening hour.
|
|
89
|
+
- **`stored: 'instant', zone`**: the value is an ISO instant with an offset, and
|
|
90
|
+
the draft is what the clocks in `zone` (an IANA name such as `Europe/London`)
|
|
91
|
+
read at it. A submitted value is written as `Date.prototype.toISOString` writes
|
|
92
|
+
one; any offset is read. An unknown zone throws when the recipe is made.
|
|
93
|
+
|
|
94
|
+
The zone is a parameter, never the runtime's, so a server and a browser fill the
|
|
95
|
+
same text. To follow the person's own zone, pass it:
|
|
96
|
+
`Intl.DateTimeFormat().resolvedOptions().timeZone`.
|
|
97
|
+
|
|
98
|
+
`locale` (default `en`) is what a display says the value in. It changes no
|
|
99
|
+
draft: the inputs' text is the same in every locale.
|
|
100
|
+
|
|
101
|
+
### Text-shaped keys
|
|
102
|
+
|
|
103
|
+
Each keeps what was typed, unchanged: none trims, lower-cases or rewrites it.
|
|
104
|
+
Their renderers give the input its type, what the browser may fill it with, and
|
|
105
|
+
turn off capitalization and spelling correction where a correction would be a
|
|
106
|
+
wrong address.
|
|
107
|
+
|
|
108
|
+
| Recipe | Takes | Input |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| `email({ autocomplete? })` | one `@`, something either side, no space; else `Enter an email address` | `type="email"`, `autocomplete="email"` |
|
|
111
|
+
| `url({ protocols?, autocomplete? })` | an address that parses with a scheme in `protocols` (default `https:`, `http:`), so `javascript:` is refused | `type="url"` |
|
|
112
|
+
| `tel({ autocomplete? })` | anything: a number is too varied to check without refusing a real one, so leave it to the schema | `type="tel"`, `inputmode="tel"` |
|
|
113
|
+
| `password({ purpose })` | anything | `type="password"`, `autocomplete` `current-password` or `new-password` |
|
|
114
|
+
| `slug(from, { prefix?, through? })` | anything | a text input after `prefix` |
|
|
115
|
+
|
|
116
|
+
A `slug` follows the key `from`, made into a slug by `through` (default
|
|
117
|
+
`FieldRecipes.slugify`: lower case, unaccented, `ß` as `ss` and `ı` as `i`, words
|
|
118
|
+
joined by `-`; a script with no Latin spelling, such as `東京`, leaves nothing,
|
|
119
|
+
so such a title's slug is written by hand), until the
|
|
120
|
+
person writes it; a form filled with a stored slug does not follow, so a
|
|
121
|
+
published address does not move because its title did. Its control is a
|
|
122
|
+
`ControlChange`, which only a form's `inputs` takes.
|
|
123
|
+
|
|
124
|
+
A `password` never shows in a list (its display is hidden) and has no reveal
|
|
125
|
+
toggle: whether the text shows is state the form does not hold, and
|
|
126
|
+
`foldkit-mixins-ui`'s `PasswordInput` keeps it in the application. Keep a
|
|
127
|
+
password out of any saved draft: a form cannot leave a key out of `partial()`,
|
|
128
|
+
so that rule is the application's.
|
|
129
|
+
|
|
130
|
+
### Amounts: `money`, `decimal`, `percent`
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const price = FieldRecipes.money({ currency: 'EUR', unit: 'minor', locale: 'de-DE' })
|
|
134
|
+
const weight = FieldRecipes.decimal({ scale: 3 })
|
|
135
|
+
const discount = FieldRecipes.percent({ unit: 'fraction' })
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
An amount is read and shown as `locale` writes it, and built from its digits, so
|
|
139
|
+
no floating point stands between what was typed and what is stored:
|
|
140
|
+
|
|
141
|
+
- **`money({ currency, unit, locale?, negative? })`**: `unit: 'minor'` stores an
|
|
142
|
+
integer of the currency's smallest unit (`123456` for 1.234,56 €), `'decimal'` a
|
|
143
|
+
decimal text (`'1234.56'`); the key's schema says which, and a value of the other
|
|
144
|
+
form fails the fill rather than being converted. The currency sets the decimals:
|
|
145
|
+
none for JPY, three for KWD. A filled field shows `1.234,56 €`, and typing
|
|
146
|
+
`1.234,56` or `1234,56` reads the same.
|
|
147
|
+
- **`decimal({ scale, locale?, negative? })`**: a number with at most `scale`
|
|
148
|
+
decimals.
|
|
149
|
+
- **`percent({ unit, scale?, locale?, negative? })`**: shown as `12.5%`, stored as
|
|
150
|
+
the share (`unit: 'fraction'`, `0.125`) or the points (`'points'`, `12.5`).
|
|
151
|
+
|
|
152
|
+
What is refused rather than guessed at: grouping other than the locale's (`12,3`
|
|
153
|
+
in `en-US` is not `123`, and `12.3` in `de-DE` is not either), more decimals than
|
|
154
|
+
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,
|
|
156
|
+
a minor amount past what a JavaScript number holds exactly (use `'decimal'` for
|
|
157
|
+
those), and a `decimal` or `percent` a JavaScript number would round. A partial
|
|
158
|
+
amount (`-`, or `1,2` in `en-US`) reads `Enter an amount`: the form checks a key
|
|
159
|
+
as it changes. What a keyboard types for what a format writes is read alike: `¥`
|
|
160
|
+
for `¥`, `'` for the Swiss `’`, full-width digits and punctuation, and the bidi
|
|
161
|
+
marks an rtl locale writes around an amount are not part of it. Nothing else is
|
|
162
|
+
folded: `10²` and `①②` are refused rather than read as `102` and `12`, and so is
|
|
163
|
+
an amount holding an override or a zero-width space, which would show one
|
|
164
|
+
number and mean another. The inputs are text with a numeric keypad (`inputmode`), since `"4."`
|
|
165
|
+
is a fine draft that `type="number"` would refuse to report.
|
|
166
|
+
|
|
167
|
+
### Drawn through `foldkit-mixins-ui`
|
|
168
|
+
|
|
169
|
+
`foldkit-form-recipes/ui`'s `renderers({ input? })` draws the same native inputs
|
|
170
|
+
under `foldkit-mixins-ui`'s Input Slot, for a form drawn with
|
|
171
|
+
`foldkit-mixins-form/ui`'s table:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { renderers as uiRecipes } from 'foldkit-form-recipes/ui'
|
|
175
|
+
import { Style } from 'foldkit-mixins'
|
|
176
|
+
import * as UiForm from 'foldkit-mixins-form/ui'
|
|
177
|
+
import { InputSlots, Recipes } from 'foldkit-mixins-ui'
|
|
178
|
+
|
|
179
|
+
const input = Style.forSlots(InputSlots)(Recipes.Input())
|
|
180
|
+
const UiView = FormView.define(Meeting, {
|
|
181
|
+
renderers: { ...UiForm.renderers({ input }), ...uiRecipes({ input }) },
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The inputs, their names and their Messages are the plain ones; only the look is
|
|
186
|
+
the Input recipe's. A calendar in a popover is not one of them: it holds an open
|
|
187
|
+
month and a focused day, state a renderer has nowhere to keep, so it would be a
|
|
188
|
+
control backed by a Bundle, as `image` is, and is not built.
|
|
189
|
+
|
|
190
|
+
### An image: `foldkit-form-recipes/image`
|
|
191
|
+
|
|
192
|
+
`image({ id, url?, headers?, words?, styles? })` is a key whose value is the
|
|
193
|
+
`HashedAsset` that `foldkit-media`'s upload route answers. Its control is a
|
|
194
|
+
Bundle, because an upload has state of its own:
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
Empty --choose--> Sending --201--> Kept (the value)
|
|
198
|
+
Sending --cancel--> Empty
|
|
199
|
+
Sending --refused--> Refused --retry--> Sending
|
|
200
|
+
Refused --choose--> Sending
|
|
201
|
+
Sending, Refused, Kept --remove--> Empty
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { image } from 'foldkit-form-recipes/image'
|
|
206
|
+
import { HashedAsset } from 'foldkit-media'
|
|
207
|
+
|
|
208
|
+
const cover = image({ id: 'cover' })
|
|
209
|
+
const Post = Form.make('Post', Form.input(Schema.Struct({ cover: HashedAsset })), {
|
|
210
|
+
inputs: { cover: cover.control },
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Choosing a file (dropped, or picked through the drop zone's input) sends it at
|
|
215
|
+
once with `foldkit-media/browser`'s `upload`. The control draws its progress and
|
|
216
|
+
a Cancel button, which ends the send's Subscription and so aborts the request.
|
|
217
|
+
A refusal shows the route's reason (`words.refused`) with Try again, which
|
|
218
|
+
sends the same file again. Only `Kept` is a value: a file on its way, or
|
|
219
|
+
refused, is nothing entered.
|
|
220
|
+
|
|
221
|
+
- **A submit while a file is on its way** waits for it (the control is `busy`
|
|
222
|
+
while `Sending`): the key reads as being checked, the form says
|
|
223
|
+
`words.checking` under it, and the submit goes out with the image once the
|
|
224
|
+
route keeps it. A cancel or a refusal ends the wait with nothing submitted, a
|
|
225
|
+
required key then saying `Required`.
|
|
226
|
+
- **Who may upload** stays the route's `UploadPolicy`, which a page's session
|
|
227
|
+
cookie answers. `headers` is only what the request carries, fixed when the
|
|
228
|
+
recipe is made, so never a secret; a bad header or `url` throws then.
|
|
229
|
+
- **A dropped file that is no image** is refused here, as `Unsupported`, before
|
|
230
|
+
its bytes go anywhere: `accept` filters the picker, not a drop.
|
|
231
|
+
- **For a reader:** the key's label names the group the control is drawn in; the
|
|
232
|
+
status is one live region, said once per state, while the progress moves under
|
|
233
|
+
it; a press that takes its button away moves focus on (Cancel and Remove to
|
|
234
|
+
the file input, Try again to Cancel).
|
|
235
|
+
- **For a stylist:** the root's `data-upload` is the state (`Empty`, `Sending`,
|
|
236
|
+
`Refused`, `Kept`); `styles.field` styles `ImageSlots` and `styles.drop`
|
|
237
|
+
`foldkit-mixins-ui`'s `FileDropSlots`; `words` words it. `id` is the drop
|
|
238
|
+
zone's and its file input's, and where focus is sent, so it is unique on the
|
|
239
|
+
page and an identifier a selector names (`cover`, not `1cover` or `a.b`).
|
|
240
|
+
- **What it needs:** `foldkit-media`, `@foldkit/ui`, `foldkit-mixins` and
|
|
241
|
+
`foldkit-mixins-ui` beside the package's own (`./ui` needs `foldkit-mixins`,
|
|
242
|
+
`foldkit-mixins-ui` and `foldkit-mixins-form`; `./html` `foldkit-mixins-form`).
|
|
243
|
+
- **In `vite dev`,** a Model holding a `File` cannot be kept across a reload, so
|
|
244
|
+
a send on its way is lost by one.
|
|
245
|
+
|
|
246
|
+
## Displays
|
|
247
|
+
|
|
248
|
+
Each recipe's `display` is a `foldkit-crud` Display, for a list or a detail:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
Entity.annotateMembers({ due: Display.of(due.display) })
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`date({ locale: 'de' }).display` reads `make(2026, 3, 5)` as `05.03.2026`; an
|
|
255
|
+
instant reads in its recipe's zone.
|
|
256
|
+
|
|
257
|
+
## Failure and edge semantics
|
|
258
|
+
|
|
259
|
+
- **A time the clocks skip is refused.** In `instant` mode, `2026-03-29T01:30` in
|
|
260
|
+
London names no instant (the clocks go from 01:00 to 02:00), and reads
|
|
261
|
+
`Enter a date and a time that exists`, the words for any draft that names no
|
|
262
|
+
time, a partly typed one too. A browser's `new Date` would move it an hour on
|
|
263
|
+
without saying.
|
|
264
|
+
- **A time the clocks repeat reads as the earlier.** `2026-10-25T01:30` in London
|
|
265
|
+
happens twice; it submits `00:30Z`, the first.
|
|
266
|
+
- **A year under 100 is that year.** `0050-06-01` is not read as 1950, as
|
|
267
|
+
`Date.UTC` would.
|
|
268
|
+
- **A fill with a value of the wrong form throws,** naming the form and the key: a
|
|
269
|
+
`CalendarDate` key given a string, local text in an `instant` key. That is a
|
|
270
|
+
wiring mistake, so it fails at once rather than showing nothing. So does a
|
|
271
|
+
recipe made with an option no format can use.
|
|
272
|
+
- **An empty `Schema.String` key submits `''`.** The empty draft is the form's,
|
|
273
|
+
and a plain string schema accepts it. Give a moment that must be set a schema
|
|
274
|
+
that refuses it: `Schema.String.check(Schema.isMinLength(1))`.
|
|
275
|
+
- **An image control's draft is not stored mid-send.** A `File` cannot be
|
|
276
|
+
kept in a stored draft, so `settled` turns `Sending` and `Refused` into
|
|
277
|
+
`Empty`; a kept image stays.
|
|
278
|
+
- **A late answer is ignored.** Every send has its own attempt number, and an
|
|
279
|
+
event from a send cancelled or replaced changes nothing.
|
|
280
|
+
|
|
281
|
+
## Limits
|
|
282
|
+
|
|
283
|
+
- An email or web address that passes is well-formed, not known to exist: whether
|
|
284
|
+
it receives mail or answers is the server's to find out.
|
|
285
|
+
- No arithmetic across zones and no zone-aware value type: an instant is text,
|
|
286
|
+
and its zone is the recipe's.
|
|
287
|
+
- A date is written and read with a four-digit year, 0000 to 9999: a later year
|
|
288
|
+
is refused, though a date input may take one.
|
|
289
|
+
- `slugify` spells Latin letters only: a title in another script gives an empty
|
|
290
|
+
slug, which is then written by hand.
|
|
291
|
+
- A browser's own date and time inputs are the default renderers; their look and
|
|
292
|
+
their picker are the browser's.
|
|
293
|
+
- `image` is a Bundle control, so it takes no `check` and cannot sit in a row of
|
|
294
|
+
a nested form. There is no `file` recipe: the upload route takes images
|
|
295
|
+
alone.
|
package/dist/html.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.d.mts","names":[],"sources":["../src/html.ts"],"mappings":";;qBAqEa,YAAa,cAAY,UAAU"}
|
package/dist/html.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { t as FieldRecipes } from "./src-CutVZjTd.mjs";
|
|
2
|
+
// src/html.ts
|
|
3
|
+
const { Kinds } = FieldRecipes;
|
|
4
|
+
/** A native input of `type`, holding the draft as typed, with what else the kind asks. */
|
|
5
|
+
const native = (type, extra = () => []) => (context) => {
|
|
6
|
+
const { state, draft, change, blurred, slots, h } = context;
|
|
7
|
+
return h.input(slots.text.attrs([
|
|
8
|
+
...state,
|
|
9
|
+
h.Type(type),
|
|
10
|
+
h.Value(String(draft)),
|
|
11
|
+
h.OnInput(change),
|
|
12
|
+
h.OnBlur(blurred),
|
|
13
|
+
...extra(context)
|
|
14
|
+
]));
|
|
15
|
+
};
|
|
16
|
+
/** What the browser may fill in, and that a keyboard must not capitalize or correct it. */
|
|
17
|
+
const literal = ({ h }, autocomplete) => [
|
|
18
|
+
h.Attribute("autocomplete", autocomplete),
|
|
19
|
+
h.Attribute("autocapitalize", "none"),
|
|
20
|
+
h.Attribute("spellcheck", "false")
|
|
21
|
+
];
|
|
22
|
+
/** The data of a control of `kind`, which the table only hands it. */
|
|
23
|
+
const dataOf = (kind, control) => {
|
|
24
|
+
if (!kind.is(control)) throw new Error(`a ${control.kind} control under another kind's renderer`);
|
|
25
|
+
return control.data;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* An amount's text input: text, since `"4."` and `"-"` are fine drafts that
|
|
29
|
+
* `type="number"` would refuse to report. Its keypad has a minus only when the
|
|
30
|
+
* amount may be negative (`decimal` has none on iOS), and no point for a whole one.
|
|
31
|
+
*/
|
|
32
|
+
const amount = (of) => native("text", (context) => {
|
|
33
|
+
const { negative, scale } = of(context.control);
|
|
34
|
+
return [context.h.InputMode(negative === true ? "text" : scale === 0 ? "numeric" : "decimal"), context.h.Attribute("autocomplete", "off")];
|
|
35
|
+
});
|
|
36
|
+
/** A renderer for each recipe kind. */
|
|
37
|
+
const renderers = () => ({
|
|
38
|
+
[Kinds.Date.kind]: native("date"),
|
|
39
|
+
[Kinds.DateTime.kind]: native("datetime-local"),
|
|
40
|
+
[Kinds.Email.kind]: native("email", (context) => literal(context, dataOf(Kinds.Email, context.control).autocomplete ?? "email")),
|
|
41
|
+
[Kinds.Url.kind]: native("url", (context) => literal(context, dataOf(Kinds.Url, context.control).autocomplete ?? "url")),
|
|
42
|
+
[Kinds.Tel.kind]: native("tel", (context) => [context.h.InputMode("tel"), context.h.Attribute("autocomplete", dataOf(Kinds.Tel, context.control).autocomplete ?? "tel")]),
|
|
43
|
+
[Kinds.Password.kind]: native("password", (context) => literal(context, dataOf(Kinds.Password, context.control).purpose === "new" ? "new-password" : "current-password")),
|
|
44
|
+
[Kinds.Money.kind]: amount((control) => dataOf(Kinds.Money, control)),
|
|
45
|
+
[Kinds.Decimal.kind]: amount((control) => dataOf(Kinds.Decimal, control)),
|
|
46
|
+
[Kinds.Percent.kind]: amount((control) => {
|
|
47
|
+
const data = dataOf(Kinds.Percent, control);
|
|
48
|
+
return {
|
|
49
|
+
negative: data.negative,
|
|
50
|
+
scale: data.scale ?? 2
|
|
51
|
+
};
|
|
52
|
+
}),
|
|
53
|
+
[Kinds.Slug.kind]: (context) => context.h.div(context.slots.group.attrs(), [context.h.span(context.slots.affix.attrs(), [dataOf(Kinds.Slug, context.control).prefix]), native("text", (inner) => literal(inner, "off"))(context)])
|
|
54
|
+
});
|
|
55
|
+
export { renderers };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=html.mjs.map
|
|
@@ -0,0 +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":";;AAWA,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;EAC/B,MAAM,SAAS,OAAO,OAAO,gBAAgB;EAC7C,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
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { FieldRecipe } from "./index.mjs";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
import { BundleControl } from "foldkit-form";
|
|
4
|
+
import { HashedAsset } from "foldkit-media";
|
|
5
|
+
import { UploadOptions, UploadRefusal } from "foldkit-media/browser";
|
|
6
|
+
import { Capability, NamedStyle, Slot, Slots } from "foldkit-mixins";
|
|
7
|
+
import { FileDropSlots } from "foldkit-mixins-ui";
|
|
8
|
+
/**
|
|
9
|
+
* Where the image stands. `Sending` and `Refused` hold the `File` itself, so a
|
|
10
|
+
* retry sends it again; a stored draft cannot hold one, so `settled` empties them.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ImageUpload: import("foldkit/schema").TaggedUnion<{
|
|
13
|
+
readonly Empty: {};
|
|
14
|
+
readonly Sending: {
|
|
15
|
+
file: Schema.instanceOf<File, File>;
|
|
16
|
+
attempt: Schema.Number;
|
|
17
|
+
loaded: Schema.Number;
|
|
18
|
+
total: Schema.Number;
|
|
19
|
+
};
|
|
20
|
+
readonly Refused: {
|
|
21
|
+
file: Schema.instanceOf<File, File>;
|
|
22
|
+
reason: Schema.Literals<readonly ["Forbidden", "Unauthenticated", "TooLarge", "Unsupported", "Malformed", "Network", "Unexpected"]>;
|
|
23
|
+
status: Schema.Number;
|
|
24
|
+
};
|
|
25
|
+
readonly Kept: {
|
|
26
|
+
asset: Schema.refine<HashedAsset, Schema.Struct<{
|
|
27
|
+
readonly key: Schema.String;
|
|
28
|
+
readonly width: Schema.Int;
|
|
29
|
+
readonly height: Schema.Int;
|
|
30
|
+
readonly alpha: Schema.Boolean;
|
|
31
|
+
readonly hash: Schema.OptionFromNullOr<Schema.String>;
|
|
32
|
+
readonly placeholder: Schema.OptionFromNullOr<Schema.TaggedUnion<{
|
|
33
|
+
readonly Color: Schema.TaggedStruct<"Color", {
|
|
34
|
+
readonly value: Schema.String;
|
|
35
|
+
}>;
|
|
36
|
+
readonly LowRes: Schema.TaggedStruct<"LowRes", {}>;
|
|
37
|
+
readonly ThumbHash: Schema.TaggedStruct<"ThumbHash", {
|
|
38
|
+
readonly hash: Schema.String;
|
|
39
|
+
}>;
|
|
40
|
+
}>>;
|
|
41
|
+
readonly focus: Schema.OptionFromOptionalNullOr<Schema.Struct<{
|
|
42
|
+
readonly x: Schema.Finite;
|
|
43
|
+
readonly y: Schema.Finite;
|
|
44
|
+
}>>;
|
|
45
|
+
}>>;
|
|
46
|
+
};
|
|
47
|
+
}>;
|
|
48
|
+
export type ImageUpload = typeof ImageUpload.Type;
|
|
49
|
+
export declare const ImageModel: Schema.Struct<{
|
|
50
|
+
readonly drop: Schema.Struct<{
|
|
51
|
+
readonly id: Schema.String;
|
|
52
|
+
readonly isDragOver: Schema.Boolean;
|
|
53
|
+
}>;
|
|
54
|
+
readonly upload: import("foldkit/schema").TaggedUnion<{
|
|
55
|
+
readonly Empty: {};
|
|
56
|
+
readonly Sending: {
|
|
57
|
+
file: Schema.instanceOf<File, File>;
|
|
58
|
+
attempt: Schema.Number;
|
|
59
|
+
loaded: Schema.Number;
|
|
60
|
+
total: Schema.Number;
|
|
61
|
+
};
|
|
62
|
+
readonly Refused: {
|
|
63
|
+
file: Schema.instanceOf<File, File>;
|
|
64
|
+
reason: Schema.Literals<readonly ["Forbidden", "Unauthenticated", "TooLarge", "Unsupported", "Malformed", "Network", "Unexpected"]>;
|
|
65
|
+
status: Schema.Number;
|
|
66
|
+
};
|
|
67
|
+
readonly Kept: {
|
|
68
|
+
asset: Schema.refine<HashedAsset, Schema.Struct<{
|
|
69
|
+
readonly key: Schema.String;
|
|
70
|
+
readonly width: Schema.Int;
|
|
71
|
+
readonly height: Schema.Int;
|
|
72
|
+
readonly alpha: Schema.Boolean;
|
|
73
|
+
readonly hash: Schema.OptionFromNullOr<Schema.String>;
|
|
74
|
+
readonly placeholder: Schema.OptionFromNullOr<Schema.TaggedUnion<{
|
|
75
|
+
readonly Color: Schema.TaggedStruct<"Color", {
|
|
76
|
+
readonly value: Schema.String;
|
|
77
|
+
}>;
|
|
78
|
+
readonly LowRes: Schema.TaggedStruct<"LowRes", {}>;
|
|
79
|
+
readonly ThumbHash: Schema.TaggedStruct<"ThumbHash", {
|
|
80
|
+
readonly hash: Schema.String;
|
|
81
|
+
}>;
|
|
82
|
+
}>>;
|
|
83
|
+
readonly focus: Schema.OptionFromOptionalNullOr<Schema.Struct<{
|
|
84
|
+
readonly x: Schema.Finite;
|
|
85
|
+
readonly y: Schema.Finite;
|
|
86
|
+
}>>;
|
|
87
|
+
}>>;
|
|
88
|
+
};
|
|
89
|
+
}>;
|
|
90
|
+
/** How many sends were started: each one's events carry its number, so a cancelled one's are ignored. */
|
|
91
|
+
readonly attempts: Schema.Number;
|
|
92
|
+
}>;
|
|
93
|
+
export type ImageModel = typeof ImageModel.Type;
|
|
94
|
+
export declare const ImageMessage: import("foldkit/message").MessageUnion<{
|
|
95
|
+
readonly GotDrop: {
|
|
96
|
+
message: Schema.Union<readonly [Schema.TaggedStruct<"EnteredDragZone", {}>, Schema.TaggedStruct<"LeftDragZone", {}>, Schema.TaggedStruct<"DroppedFiles", {
|
|
97
|
+
readonly files: Schema.NonEmptyArray<Schema.instanceOf<File, File>>;
|
|
98
|
+
}>, Schema.TaggedStruct<"DroppedNonFiles", {}>]>;
|
|
99
|
+
};
|
|
100
|
+
readonly Progressed: {
|
|
101
|
+
attempt: Schema.Number;
|
|
102
|
+
loaded: Schema.Number;
|
|
103
|
+
total: Schema.Number;
|
|
104
|
+
};
|
|
105
|
+
readonly Uploaded: {
|
|
106
|
+
attempt: Schema.Number;
|
|
107
|
+
asset: Schema.refine<HashedAsset, Schema.Struct<{
|
|
108
|
+
readonly key: Schema.String;
|
|
109
|
+
readonly width: Schema.Int;
|
|
110
|
+
readonly height: Schema.Int;
|
|
111
|
+
readonly alpha: Schema.Boolean;
|
|
112
|
+
readonly hash: Schema.OptionFromNullOr<Schema.String>;
|
|
113
|
+
readonly placeholder: Schema.OptionFromNullOr<Schema.TaggedUnion<{
|
|
114
|
+
readonly Color: Schema.TaggedStruct<"Color", {
|
|
115
|
+
readonly value: Schema.String;
|
|
116
|
+
}>;
|
|
117
|
+
readonly LowRes: Schema.TaggedStruct<"LowRes", {}>;
|
|
118
|
+
readonly ThumbHash: Schema.TaggedStruct<"ThumbHash", {
|
|
119
|
+
readonly hash: Schema.String;
|
|
120
|
+
}>;
|
|
121
|
+
}>>;
|
|
122
|
+
readonly focus: Schema.OptionFromOptionalNullOr<Schema.Struct<{
|
|
123
|
+
readonly x: Schema.Finite;
|
|
124
|
+
readonly y: Schema.Finite;
|
|
125
|
+
}>>;
|
|
126
|
+
}>>;
|
|
127
|
+
};
|
|
128
|
+
readonly Failed: {
|
|
129
|
+
attempt: Schema.Number;
|
|
130
|
+
reason: Schema.Literals<readonly ["Forbidden", "Unauthenticated", "TooLarge", "Unsupported", "Malformed", "Network", "Unexpected"]>;
|
|
131
|
+
status: Schema.Number;
|
|
132
|
+
};
|
|
133
|
+
readonly Cancelled: {};
|
|
134
|
+
readonly Retried: {};
|
|
135
|
+
readonly Removed: {};
|
|
136
|
+
/** Focus moved on after a press took away the button pressed. */
|
|
137
|
+
readonly CompletedFocus: {};
|
|
138
|
+
}>;
|
|
139
|
+
export type ImageMessage = typeof ImageMessage.Type;
|
|
140
|
+
/** Every element the control draws, with the drop zone's own two under `FileDropSlots`. */
|
|
141
|
+
export declare const ImageSlots: Slots.Contract<{
|
|
142
|
+
readonly root: Slot.UnnamedSlot<Capability.Capability<"Container", readonly [Capability.Capability<"Interactive", readonly [Capability.Capability<"Base", readonly []>]>]>, readonly Slot.EventName[], readonly Slot.AttributeName[], readonly Slot.RequirementName[], boolean>;
|
|
143
|
+
readonly prompt: Slot.UnnamedSlot<Capability.Capability<"Base", readonly []>, readonly Slot.EventName[], readonly Slot.AttributeName[], readonly Slot.RequirementName[], boolean>;
|
|
144
|
+
readonly status: Slot.UnnamedSlot<Capability.Capability<"Base", readonly []>, readonly Slot.EventName[], readonly Slot.AttributeName[], readonly Slot.RequirementName[], boolean>;
|
|
145
|
+
readonly progress: Slot.UnnamedSlot<Capability.Capability<"Base", readonly []>, readonly Slot.EventName[], readonly Slot.AttributeName[], readonly Slot.RequirementName[], boolean>;
|
|
146
|
+
readonly cancel: Slot.UnnamedSlot<Capability.Capability<"Interactive", readonly [Capability.Capability<"Base", readonly []>]>, readonly Slot.EventName[], readonly Slot.AttributeName[], readonly Slot.RequirementName[], boolean>;
|
|
147
|
+
readonly retry: Slot.UnnamedSlot<Capability.Capability<"Interactive", readonly [Capability.Capability<"Base", readonly []>]>, readonly Slot.EventName[], readonly Slot.AttributeName[], readonly Slot.RequirementName[], boolean>;
|
|
148
|
+
readonly remove: Slot.UnnamedSlot<Capability.Capability<"Interactive", readonly [Capability.Capability<"Base", readonly []>]>, readonly Slot.EventName[], readonly Slot.AttributeName[], readonly Slot.RequirementName[], boolean>;
|
|
149
|
+
}>;
|
|
150
|
+
/**
|
|
151
|
+
* What the control says. `{name}`, `{width}` and `{height}` are filled. Each is
|
|
152
|
+
* said once, in a live region: the progress shows how far a send has gone.
|
|
153
|
+
*/
|
|
154
|
+
export interface ImageWords {
|
|
155
|
+
readonly prompt: string;
|
|
156
|
+
readonly sending: string;
|
|
157
|
+
readonly kept: string;
|
|
158
|
+
readonly cancel: string;
|
|
159
|
+
readonly retry: string;
|
|
160
|
+
readonly remove: string;
|
|
161
|
+
/** Why a send was refused, by the route's reason. */
|
|
162
|
+
readonly refused: { readonly [R in UploadRefusal]: string; };
|
|
163
|
+
}
|
|
164
|
+
export declare const imageWords: ImageWords;
|
|
165
|
+
export interface ImageOptions extends UploadOptions {
|
|
166
|
+
/**
|
|
167
|
+
* The drop zone's `id`, which its file input takes: unique on the page, so
|
|
168
|
+
* two image keys never share one.
|
|
169
|
+
*/
|
|
170
|
+
readonly id: string;
|
|
171
|
+
readonly words?: Partial<Omit<ImageWords, 'refused'>> & {
|
|
172
|
+
readonly refused?: Partial<ImageWords['refused']> | undefined;
|
|
173
|
+
};
|
|
174
|
+
readonly styles?: {
|
|
175
|
+
readonly field?: NamedStyle<typeof ImageSlots> | undefined;
|
|
176
|
+
readonly drop?: NamedStyle<typeof FileDropSlots> | undefined;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* An image key: its value is the `HashedAsset` the upload route kept. The
|
|
181
|
+
* Bundle uploads a file once it is chosen, through `upload` with `url` and
|
|
182
|
+
* `headers`, so who may upload is still the route's `UploadPolicy`. Like every
|
|
183
|
+
* Bundle control it has no `check`, and a row of a nested form cannot hold it.
|
|
184
|
+
*/
|
|
185
|
+
export declare const image: (options: ImageOptions) => FieldRecipe<BundleControl<ImageModel, ImageMessage, HashedAsset>>;
|
|
186
|
+
//# sourceMappingURL=image.d.mts.map
|
|
@@ -0,0 +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;;;;;;;;;qBAuOzB,QAAK,SACP,iBACR,YAAY,cAAc,YAAY,cAAc"}
|