@loadbare/app 0.13.0 → 0.15.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.
Files changed (51) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +31 -1
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/build/elements.d.ts +12 -2
  5. package/dist/build/elements.d.ts.map +1 -1
  6. package/dist/build/elements.js +44 -10
  7. package/dist/build/elements.js.map +1 -1
  8. package/dist/core/lb-constants.d.ts +13 -2
  9. package/dist/core/lb-constants.d.ts.map +1 -1
  10. package/dist/core/lb-constants.js +50 -3
  11. package/dist/core/lb-constants.js.map +1 -1
  12. package/dist/core/lb-types.d.ts +52 -8
  13. package/dist/core/lb-types.d.ts.map +1 -1
  14. package/dist/core/lb-types.js.map +1 -1
  15. package/dist/hub/lb-apply.d.ts +22 -3
  16. package/dist/hub/lb-apply.d.ts.map +1 -1
  17. package/dist/hub/lb-apply.js +82 -37
  18. package/dist/hub/lb-apply.js.map +1 -1
  19. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  20. package/dist/hub/lb-hub.browser.js +382 -26
  21. package/dist/hub/lb-hub.browser.js.map +1 -1
  22. package/dist/hub/lb-lifecycle.d.ts +40 -0
  23. package/dist/hub/lb-lifecycle.d.ts.map +1 -0
  24. package/dist/hub/lb-lifecycle.js +140 -0
  25. package/dist/hub/lb-lifecycle.js.map +1 -0
  26. package/dist/server/lb-express.d.ts +13 -3
  27. package/dist/server/lb-express.d.ts.map +1 -1
  28. package/dist/server/lb-express.js +30 -12
  29. package/dist/server/lb-express.js.map +1 -1
  30. package/dist/server/lb-server.d.ts +31 -1
  31. package/dist/server/lb-server.d.ts.map +1 -1
  32. package/dist/server/lb-server.js +112 -2
  33. package/dist/server/lb-server.js.map +1 -1
  34. package/docs/TECHREF-1.0.md +343 -68
  35. package/docs/comparison.md +13 -9
  36. package/docs/possible-ideas.md +152 -26
  37. package/docs/reference/custom-elements.md +203 -88
  38. package/docs/reference/data-binding.md +310 -11
  39. package/docs/reference/page-files.md +76 -3
  40. package/docs/reference/server.md +20 -0
  41. package/docs/reference/widgets.md +35 -12
  42. package/docs/roadmap.md +9 -2
  43. package/docs/testing.md +2 -2
  44. package/package.json +1 -1
  45. package/skills/loadbare-app/SKILL.md +70 -23
  46. package/skills/loadbare-app/references/TECHREF-1.0.md +343 -68
  47. package/skills/loadbare-app/references/custom-elements.md +203 -88
  48. package/skills/loadbare-app/references/data-binding.md +310 -11
  49. package/skills/loadbare-app/references/page-files.md +76 -3
  50. package/skills/loadbare-app/references/server.md +20 -0
  51. package/skills/loadbare-app/references/widgets.md +35 -12
@@ -121,6 +121,7 @@ no row fills.
121
121
  | Element | Receives the value as |
122
122
  |------------------------------------------------------|-----------------------------------------|
123
123
  | `<input>`, `<select>`, `<textarea>` | Its `value`, and `lb-column-value` |
124
+ | `<input type="checkbox">` | Its `checked`, and `lb-column-value` |
124
125
  | A form-associated custom element with `value` | Its `value`, and `lb-column-value` |
125
126
  | Any other custom element | `lb-column-value` only |
126
127
  | Any other element | Its text content, and `lb-column-value` |
@@ -132,12 +133,26 @@ element that is not a control keeps the content the builder placed in it
132
133
  from its element file, and renders `lb-column-value` itself; see
133
134
  [Custom Elements](./custom-elements.md#receiving-a-value).
134
135
 
135
- A checkbox, a radio button and a file input receive nothing, and the hub
136
- reports it to the console.
136
+ A radio button and a file input receive nothing, and the hub reports it to
137
+ the console.
137
138
 
138
139
  The hub hands every value to the browser as the server sent it. What a
139
140
  number, a date or a null looks like is decided in the query.
140
141
 
142
+ ### Checkboxes
143
+
144
+ A checkbox is a control. It is checked when its column is anything but
145
+ `false` or null, and it sends `"true"` or `"false"`:
146
+
147
+ ```html
148
+ <input type="checkbox" lb-column="active" lb-request="lb-row-update" />
149
+ ```
150
+
151
+ Checked follows the rule `lb-show` follows, so a query spells a flag as a
152
+ boolean, and the string `"false"` is checked. Unchecked sends `"false"`
153
+ rather than nothing, so a flag can be set back. The checkbox's `value`
154
+ attribute means nothing to the hub.
155
+
141
156
  ### Rows
142
157
 
143
158
  Write a `<template>` inside the element carrying `lb-query`, holding one
@@ -269,10 +284,9 @@ scaffolding around rows, such as a group's heading or a ghost row. So
269
284
  a query nested in another query's rows need not land again, or land after
270
285
  the outer query, for a new outer row to show its choices.
271
286
 
272
- A custom element the builder shipped absent has not upgraded while its
273
- `lb-show` column is off. When the column first turns on and the element
274
- upgrades, the hub lands the kept answer on it again, so its `lbRowsLanded`
275
- runs. See [Holding rows](./custom-elements.md#holding-rows).
287
+ A custom element under an `lb-show` that is off is filled like any other,
288
+ and its `lbRowsLanded` runs as its rows land. See
289
+ [Holding rows](./custom-elements.md#holding-rows).
276
290
 
277
291
  Nothing is answered from what the hub kept: a request always goes to the
278
292
  server, and a query re-runs only when a response names it. A patch that
@@ -414,7 +428,8 @@ Every request has one shape:
414
428
  ```
415
429
 
416
430
  The hub takes `query`, `key` and `values` from where the element sits, and
417
- sends each when it has one. The server runs the handler for the name and
431
+ sends each when it has one. A request acting on several rows carries `keys`
432
+ in place of `key`; see [Selecting rows](#selecting-rows). The server runs the handler for the name and
418
433
  answers with response items, which land like any others.
419
434
 
420
435
  ### Committing
@@ -461,7 +476,8 @@ carrying `lb-request`:
461
476
  4. Otherwise: every control whose form owner is the element's form owner.
462
477
 
463
478
  The hub gathers from controls only, and skips a control that belongs to a
464
- query nested inside the group. A form owner includes a control outside the
479
+ query nested inside the group, except that a form gathers the rows of a
480
+ query nested in its own; see [A form over a set of rows](#a-form-over-a-set-of-rows). A form owner includes a control outside the
465
481
  form that names it with the HTML `form` attribute, which is how a table row,
466
482
  where a form cannot go, sends one:
467
483
 
@@ -492,6 +508,99 @@ ancestor `lb-query` and row of the element carrying `lb-request` instead.
492
508
  Controls gathered from two different queries are an error, and the hub
493
509
  sends nothing.
494
510
 
511
+ ### A form over a set of rows
512
+
513
+ A form gathers the live rows of a query nested in its own query as well as
514
+ its own controls: a master and its details, entered together. Each nested
515
+ row goes as its key and the values of its own controls, under the nested
516
+ query's name, in `rows`:
517
+
518
+ ```html
519
+ <form lb-query="paychecks" lb-request="lb-row-insert">
520
+ <input lb-column="pay_date" type="date" />
521
+ <table>
522
+ <tbody lb-query="deduction_types">
523
+ <template>
524
+ <tr>
525
+ <td lb-column="label"></td>
526
+ <td><input lb-column="amount" /></td>
527
+ </tr>
528
+ </template>
529
+ </tbody>
530
+ </table>
531
+ <button type="submit">Save</button>
532
+ </form>
533
+ ```
534
+
535
+ ```json
536
+ {
537
+ "name": "lb-row-insert",
538
+ "query": "paychecks",
539
+ "values": { "pay_date": "2026-10-02" },
540
+ "rows": {
541
+ "deduction_types": [
542
+ { "key": "401k", "values": { "amount": "50" } },
543
+ { "key": "dental", "values": { "amount": "" } }
544
+ ]
545
+ }
546
+ }
547
+ ```
548
+
549
+ The list of deduction types lives only in the database: the nested query
550
+ answers it, and the form shows one input per row.
551
+
552
+ - Every live row of the nested query goes, whatever its values. What an
553
+ empty value means, such as no deduction of that type, is the handler's.
554
+ - One level only. A query nested inside a nested row shows the same rows in
555
+ every row around it, so it is not gathered.
556
+ - An insert, an update and a declared request carry `rows`. A delete does
557
+ not.
558
+ - A form whose only controls are in nested rows still sends, for its own
559
+ query, with empty `values`.
560
+ - A live row gathers nothing nested, as before.
561
+
562
+ The handler for the form's query receives `rows` beside `values`, and writes
563
+ the master and its details together; see [crud](./page-files.md#crud).
564
+
565
+ A nested query answers once for the page, so its rows are the same wherever
566
+ they show. The form enters a new master with its details, or edits the
567
+ master the page shows, whose details the nested query reads from the same
568
+ query parm. A form [pointed at a row](#editing-a-row-in-one-form) fills its
569
+ own controls from that row; its nested rows stay the nested query's answer.
570
+
571
+ ### A row not yet entered
572
+
573
+ A row whose key is empty is a new one, not yet entered. It arises two ways:
574
+ a `row` query answers with an empty key, as one showing the master a query
575
+ parm names does when the parm is empty, or New points a form (see
576
+ [Editing a row in one form](#editing-a-row-in-one-form)). Either way the
577
+ element carries `lb-key-value=""`.
578
+
579
+ | Request | From a row not yet entered |
580
+ |------------------|---------------------------------------|
581
+ | `lb-row-insert` | Sent, with no `key` |
582
+ | A declared one | Sent, with no `key` |
583
+ | `lb-row-update` | Not sent: there is nothing to update |
584
+ | `lb-row-delete` | Not sent: there is nothing to delete |
585
+
586
+ An update or delete not sent is the row's state, not a mistake, and nothing
587
+ is reported. So a control that saves its own column with
588
+ `lb-request="lb-row-update"` saves on a row on record, and on a new row only
589
+ holds what a submit will gather:
590
+
591
+ ```html
592
+ <section lb-query="account">
593
+ <form id="account-form">
594
+ <input lb-column="name" lb-request="lb-row-update" />
595
+ </form>
596
+ <button type="submit" form="account-form" lb-request="lb-row-insert">Save new</button>
597
+ </section>
598
+ ```
599
+
600
+ The same holds for a request a custom element sends carrying `key: ""`.
601
+ An update or delete with no row around it at all has no key either, and
602
+ that is reported to the console.
603
+
495
604
  ### Inserting, updating and deleting rows
496
605
 
497
606
  Loadbare provides three requests. Each runs the handler the page declares
@@ -528,6 +637,152 @@ failed insert leaves the entry for the user to correct.
528
637
  Leave `lb-request` off a control inside a form or a live row that sends its
529
638
  own update, or the same edit is written twice.
530
639
 
640
+ ### Editing a row in one form
641
+
642
+ A table, or any other set of rows, that the user edits or adds to has one
643
+ form on the page, naming the same query. It sits anywhere, outside every
644
+ row. Buttons point it with HTML's `commandfor` and `command`:
645
+
646
+ | Command | On a button | The hub |
647
+ |----------------|-------------------------------------|---------------------------------------------|
648
+ | `--lb-edit` | In a live row of the form's query | Fills the form from that row, keyed by it |
649
+ | `--lb-new` | Anywhere | Starts the form empty, for an insert |
650
+ | `--lb-release` | Anywhere | Lets the form go without saving |
651
+
652
+ HTML requires a custom command to begin with `--`.
653
+
654
+ ```html
655
+ <tbody lb-query="paychecks">
656
+ <template>
657
+ <tr>
658
+ <td lb-column="pay_date"></td>
659
+ <td lb-column="gross"></td>
660
+ <td><button type="button" commandfor="paycheck" command="--lb-edit">Edit</button></td>
661
+ </tr>
662
+ </template>
663
+ </tbody>
664
+ <button type="button" commandfor="paycheck" command="--lb-new">New</button>
665
+
666
+ <dialog>
667
+ <form id="paycheck" lb-query="paychecks" lb-request="lb-row-update">
668
+ <input lb-column="pay_date" type="date" />
669
+ <input lb-column="gross" />
670
+ <button type="submit">Save</button>
671
+ <button type="submit" lb-request="lb-row-insert">Save new</button>
672
+ <button type="button" commandfor="paycheck" command="--lb-release">Cancel</button>
673
+ </form>
674
+ </dialog>
675
+ ```
676
+
677
+ Edit fills the form from the row as the hub already holds it, with no
678
+ round trip, including columns no element in the table shows. The user
679
+ edits a row they can see, so the row is on the page. A row that has to be
680
+ fetched to be edited is another page, with that row as its master.
681
+
682
+ The hub stamps the form with the row's key, so the form's request carries
683
+ it, and stamps the row `lb-row-editing`. New stamps an empty key, making the
684
+ form [a row not yet entered](#a-row-not-yet-entered), and resets every
685
+ control to its default. The form's state is in its `lb-key-value`
686
+ stamp:
687
+
688
+ | `lb-key-value` | The form is |
689
+ |----------------|-----------------------------|
690
+ | A key | Editing that row |
691
+ | Empty | Entering a new row |
692
+ | Absent | Not in use |
693
+
694
+ A stylesheet shows Save or Save new by it:
695
+
696
+ ```css
697
+ form[lb-key-value=""] [type="submit"]:not([lb-request]),
698
+ form:not([lb-key-value=""]) [lb-request="lb-row-insert"] {
699
+ display: none;
700
+ }
701
+ ```
702
+
703
+ An `lb-show` in the form reads from the row it was pointed at. With no row,
704
+ every column is null, so `lb-show="!id"` shows only while entering a new
705
+ row.
706
+
707
+ A successful request from the form lets it go, as Cancel does: the form
708
+ empties, its controls return to their defaults, and the stamps go. A failed
709
+ one leaves the form as the user had it, to correct.
710
+
711
+ Inside a `<dialog>`, Edit and New open the dialog modally, and letting go
712
+ closes it. The dialog closing in any way HTML closes one lets the form go:
713
+ Escape, `command="close"`, or `method="dialog"`. A form in no dialog opens
714
+ nothing. It is always showing, as a detail panel beside the table is, or a
715
+ stylesheet shows it while it has a key:
716
+
717
+ ```css
718
+ form:not([lb-key-value]) {
719
+ display: none;
720
+ }
721
+ ```
722
+
723
+ The form names a `rows` query and holds no row template, so the query
724
+ landing again lands nothing on it, and what the user has typed stays.
725
+
726
+ A button inside a form that carries a command is `type="button"`, or it
727
+ submits the form instead.
728
+
729
+ ### Selecting rows
730
+
731
+ A checkbox carrying `lb-select` in a row template selects the live row it
732
+ sits in. It is not a column: it carries no `lb-column`, lands nothing and
733
+ is not gathered. The hub stamps the row `lb-row-selected` while it is
734
+ ticked.
735
+
736
+ A request for the query that names no one row then acts on the rows
737
+ selected, and carries their keys as `keys`:
738
+
739
+ ```html
740
+ <tbody lb-query="transactions">
741
+ <template>
742
+ <tr>
743
+ <td><input type="checkbox" lb-select lb-show="selecting" /></td>
744
+ <td lb-column="description"></td>
745
+ <td lb-column="amount"></td>
746
+ </tr>
747
+ </template>
748
+ </tbody>
749
+
750
+ <form lb-query="transactions" lb-request="lb-row-update">
751
+ <lb-picker lb-query="accounts" lb-column="acct" exp-label="Account" exp-column="name"></lb-picker>
752
+ <button type="submit">Apply</button>
753
+ <button type="button" lb-request="lb-row-delete">Delete</button>
754
+ </form>
755
+ ```
756
+
757
+ ```json
758
+ { "name": "lb-row-update", "query": "transactions", "keys": ["4", "9"], "values": { "acct": "12" } }
759
+ ```
760
+
761
+ | Request | With rows selected | With none ticked |
762
+ |------------------|----------------------------------------|----------------------|
763
+ | `lb-row-update` | Sent with `keys` | Not sent |
764
+ | `lb-row-delete` | Sent with `keys` | Not sent |
765
+ | A declared one | Sent with `keys` | Sent with `keys: []` |
766
+ | `lb-row-insert` | Sent as ever; the selection is not its | Sent as ever |
767
+
768
+ An update or delete with nothing ticked has nothing to act on, and nothing
769
+ is reported. A request from inside a row still names that row alone. The
770
+ server runs the page's `rowUpdate` or `rowDelete` once for each key, as for
771
+ each row alone, and refreshes once; see [crud](./page-files.md#crud).
772
+
773
+ After the request succeeds, the hub clears the selection. A failed one
774
+ leaves it, to try again.
775
+
776
+ Only a checkbox that is showing counts. Turning a selector off with
777
+ `lb-show` also clears its tick, so leaving a selecting mode abandons the
778
+ selection rather than keeping it out of sight. With every selector off, a
779
+ request for the query is an ordinary one again.
780
+
781
+ Which rows are selected, and whether the page is in a selecting mode, is
782
+ interaction state. A mode the URL keeps, such as `?selecting=1`, reaches
783
+ the rows as a column the query computes from the parm, which `lb-show`
784
+ reads.
785
+
531
786
  ### Declared requests
532
787
 
533
788
  A request name the page declares under `handlers` runs the application's
@@ -557,6 +812,7 @@ The hub stamps the element that committed:
557
812
  |----------------------|-----------------------------------------------|
558
813
  | `lb-request-pending` | The round trip is in flight |
559
814
  | `lb-request-error` | The round trip failed, until the next request |
815
+ | `lb-request-refused` | The handler refused it; see [Refusals](#refusals) |
560
816
 
561
817
  `lb-request-error` covers a non-2xx response, a network failure and a
562
818
  timeout alike. The hub aborts a round trip after ten seconds, and sets
@@ -574,6 +830,44 @@ pending button is already disabled and a stylesheet only shows it:
574
830
  }
575
831
  ```
576
832
 
833
+ ### Refusals
834
+
835
+ A handler that refuses a request says why, in words for the user; see
836
+ [Refusing a request](./page-files.md#refusing-a-request). The hub shows
837
+ them where they belong:
838
+
839
+ - A message for the request as a whole is the value of the
840
+ `lb-request-refused` stamp on the element that committed, empty when the
841
+ refusal has none.
842
+ - A message for a column goes on the control the request gathered it from,
843
+ as HTML's own validity: `setCustomValidity`, the `:invalid` pseudo-class,
844
+ and the browser's own bubble for the first, which takes focus.
845
+
846
+ A stylesheet shows the whole-request message with no script:
847
+
848
+ ```css
849
+ form[lb-request-refused]:not([lb-request-refused=""])::after {
850
+ content: attr(lb-request-refused);
851
+ color: firebrick;
852
+ }
853
+ input:invalid {
854
+ outline: 2px solid firebrick;
855
+ }
856
+ ```
857
+
858
+ A refusal is not a failure: `lb-request-error` is not stamped, and nothing
859
+ is reported to the console.
860
+
861
+ A refusal stands until the user edits any control the request gathered,
862
+ resets its form, or sends from the same element again. Then every message
863
+ it put up goes at once, since the next submit is a new question for the
864
+ handler. While a control carries a message the browser holds its form's
865
+ submit, as for any invalid control, so the user edits before trying again.
866
+
867
+ A form-associated custom element takes a column's message when it has
868
+ `setCustomValidity` and `reportValidity`; see
869
+ [Being a control](./custom-elements.md#being-a-control).
870
+
577
871
  ### The request event
578
872
 
579
873
  The hub dispatches every request as a bubbling `lb-request` event from the
@@ -582,9 +876,10 @@ An ancestor may stop the event, and the request is not sent. A custom
582
876
  element may dispatch the event itself; see
583
877
  [Sending a request](./custom-elements.md#sending-a-request).
584
878
 
585
- Once the answer has landed, or the round trip has failed, the hub dispatches
586
- a bubbling `lb-request-done` event from the same element, carrying the
587
- request and what landed because of it, or the error; see
879
+ Once the answer has landed, or the round trip has failed, the hub calls
880
+ `lbRequestDone` on the custom elements from the committing element outward,
881
+ then dispatches a bubbling `lb-request-done` event from the same element.
882
+ Both carry the request and what landed because of it, or the error; see
588
883
  [Hearing how it turned out](./custom-elements.md#hearing-how-it-turned-out).
589
884
 
590
885
  ## The URL
@@ -620,3 +915,7 @@ expansion, and refuses to build:
620
915
  - `lb-show` on a `<template>`, or on a row template's root
621
916
  - `lb-show` with no `lb-query` around it
622
917
  - `lb-show="!"` or `lb-show="!!column"`, which reverse no column
918
+ - `lb-select` on anything but `<input type="checkbox">`, outside a row
919
+ template, or beside `lb-column` or `lb-request`
920
+ - a `command` beginning `--lb-` that is not one of the three commands
921
+ - one of the three commands with no `commandfor`
@@ -137,10 +137,14 @@ name:
137
137
  | `name` | The request name, from `lb-request` |
138
138
  | `query` | The query the request is for |
139
139
  | `key` | The key of the row the request is for |
140
+ | `keys` | The keys of the rows selected, in place of `key` |
141
+ | `rows` | A form's nested rows, by query: each `key` and `values` |
140
142
  | `values` | The gathered control values, by column |
141
143
 
142
144
  The hub sends `query`, `key` and `values` when it finds them; see
143
- [Gathering](./data-binding.md#gathering).
145
+ [Gathering](./data-binding.md#gathering). It sends `keys` for a request that
146
+ acts on the rows the user selected; see
147
+ [Selecting rows](./data-binding.md#selecting-rows).
144
148
 
145
149
  ### onPageEnter
146
150
 
@@ -191,10 +195,34 @@ the query they operate on:
191
195
 
192
196
  | Request name | Runs | `run` receives |
193
197
  |-----------------|-------------|------------------|
194
- | `lb-row-insert` | `rowInsert` | `query`, `values` |
195
- | `lb-row-update` | `rowUpdate` | `query`, `key`, `values` |
198
+ | `lb-row-insert` | `rowInsert` | `query`, `values`, and `rows` from a form |
199
+ | `lb-row-update` | `rowUpdate` | `query`, `key`, `values`, and `rows` from a form |
196
200
  | `lb-row-delete` | `rowDelete` | `query`, `key` |
197
201
 
202
+ An `lb-row-update` or `lb-row-delete` carrying `keys` runs `rowUpdate` or
203
+ `rowDelete` once for each key, with that key, so a handler written for one
204
+ row serves several. The patches the runs return are laid together, and the
205
+ refresh set runs once. A `url()` returned for one of several rows is
206
+ reported and ignored. A declared request receives `keys` as it is.
207
+
208
+ `rows` holds the rows of a query nested in the form the request came from,
209
+ by query name, each with its `key` and the `values` of its own controls; see
210
+ [A form over a set of rows](./data-binding.md#a-form-over-a-set-of-rows).
211
+ The handler writes the master and its details together, deciding what each
212
+ row's values mean, such as an empty amount being no row at all:
213
+
214
+ ```ts
215
+ rowInsert: {
216
+ run: async (ctx, { values, rows }) => {
217
+ const id = await ctx.db.insertPaycheck(values);
218
+ for (const { key, values: row } of rows?.deduction_types ?? []) {
219
+ if (row.amount !== "") await ctx.db.insertDeduction(id, key, row.amount);
220
+ }
221
+ },
222
+ refresh: ["paychecks"],
223
+ },
224
+ ```
225
+
198
226
  ```ts
199
227
  // src/pages/directory.requests.ts
200
228
  import { patch, type Requests } from "@loadbare/app/server";
@@ -333,3 +361,48 @@ Post/Redirect/Get without the redirect. The refresh set does not run, and
333
361
  nothing else `run` returned is sent, since both answered for the URL the page
334
362
  left. The loaded page reads the new parms through `contextFor`; see
335
363
  [the Express server](./server.md#database-layer).
364
+
365
+ ### Refusing a request
366
+
367
+ A handler refuses a request by throwing `refusal()`, with words written for
368
+ the user:
369
+
370
+ ```ts
371
+ import { refusal, type Requests } from "@loadbare/app/server";
372
+
373
+ export const requests: Requests = {
374
+ handlers: {
375
+ enterPaycheck: {
376
+ run: async (ctx, { values }) => {
377
+ if (Number(values!.amount) <= 0) {
378
+ throw refusal({ amount: "Must be more than zero" });
379
+ }
380
+ const overlap = await ctx.db.overlappingSeries(values!);
381
+ if (overlap) {
382
+ throw refusal(
383
+ `That series already has paychecks from ${overlap.from} through ${overlap.to}.`,
384
+ );
385
+ }
386
+ await ctx.db.enterPaycheck(values!);
387
+ },
388
+ refresh: ["paychecks"],
389
+ },
390
+ },
391
+ };
392
+ ```
393
+
394
+ | Call | The browser shows |
395
+ |----------------------------------|--------------------------------------------|
396
+ | `refusal("message")` | The message, for the request as a whole |
397
+ | `refusal({ column: "message" })` | Each message on the control for its column |
398
+ | `refusal("message", { ... })` | Both |
399
+
400
+ Throw it, from the handler or from anything the handler calls, and whatever
401
+ the application has open rolls back as for any throw. A column names a
402
+ control the request gathered. See [Refusals](./data-binding.md#refusals)
403
+ for how the browser shows them.
404
+
405
+ A refusal is the only error whose words reach the browser. Anything else a
406
+ handler throws is a failure, answered with nothing in it, so a stack or a
407
+ database message never reaches a page.
408
+
@@ -72,6 +72,26 @@ path, so the application cannot be hosted under a subpath such as
72
72
  whole path space through unchanged.
73
73
 
74
74
  Leave `express.json()` to `hubRoutes`, which mounts it on its own routes.
75
+ Pass its options through `hubRoutes` instead.
76
+
77
+ ## Options
78
+
79
+ `hubRoutes` takes an optional third argument, an object with one key per
80
+ concern:
81
+
82
+ | Option | Description |
83
+ |--------|----------------------------------------------------------|
84
+ | `json` | Options for `express.json()`, passed to it unchanged |
85
+
86
+ A request body over 100 KB, the `express.json()` default, is refused with a
87
+ 413 before any handler runs, and the server log names the option that raises
88
+ it. Raise it for an application whose pages send a file's text as a value:
89
+
90
+ ```ts
91
+ app.use(hubRoutes(hub, contextFor, { json: { limit: "5mb" } }));
92
+ ```
93
+
94
+ The limit covers every page's requests.
75
95
 
76
96
  ## Running the server
77
97
 
@@ -1,8 +1,8 @@
1
1
  # The Basic Widget Library
2
2
 
3
- Ten widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`, `lb-table`,
4
- `lb-unknown-page`, `lb-confirm`, `lb-form`, `lb-insert-dialog` and
5
- `lb-master`. Every one of them is an ordinary custom element, written
3
+ Eleven widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`,
4
+ `lb-table`, `lb-unknown-page`, `lb-confirm`, `lb-form`, `lb-insert-dialog`,
5
+ `lb-master` and `lb-checkbox`. Every one of them is an ordinary custom element, written
6
6
  against the contracts in [Custom Elements](./custom-elements.md#html) for its
7
7
  definition and [Custom Elements](./custom-elements.md#code) for its class.
8
8
 
@@ -26,7 +26,9 @@ for where a listed package sits in the cascade.
26
26
  `lb-input`, `lb-select`, `lb-options` and `lb-picker` are controls:
27
27
  form-associated custom elements with a `value` property that fire `change`.
28
28
  Each carries `lb-column` and `lb-request` itself, the way an `<input>` does;
29
- see [Being a control](./custom-elements.md#being-a-control).
29
+ see [Being a control](./custom-elements.md#being-a-control). Each passes a
30
+ refusal's message for its column to the native control inside it; see
31
+ [Refusals](./data-binding.md#refusals).
30
32
 
31
33
  ## `lb-input`
32
34
 
@@ -180,14 +182,14 @@ the hub until the user says yes:
180
182
  | `exp-request` | The confirm button's `lb-request` |
181
183
  | `exp-confirm` | The confirm button's text, `Confirm` when absent |
182
184
 
183
- The widget closes the dialog once `lb-request-done` says the request
184
- succeeded, and leaves it open on a refusal.
185
+ The widget closes the dialog once the request succeeds, and leaves it open
186
+ on a refusal.
185
187
 
186
188
  ## `lb-form`
187
189
 
188
190
  A `<form>` over one row's controls. A row with a key saves each control's
189
- column on `change`; a row whose key is empty is new, and the widget stops an
190
- `lb-row-update` that carries no key, so its controls wait for a submit.
191
+ column on `change`; a row whose key is empty is new, and the hub sends no
192
+ update for it, so its controls wait for a submit. A definition only.
191
193
 
192
194
  | Parameter | Fills |
193
195
  |-----------|-----------------|
@@ -210,16 +212,37 @@ answers with a patch holding the new row.
210
212
 
211
213
  ## `lb-master`
212
214
 
213
- A set, one of its rows, and the buttons that act on the set: Copy, Delete,
214
- Save new and Cancel, each shown only for a row on record or a new one. A
215
- definition only, built from `lb-form` and `lb-confirm`. Copy sends `copy`, a
216
- request the page declares.
215
+ A set, one of its rows, and the buttons that act on the set: New, Copy,
216
+ Delete, Save new and Cancel, each shown only for a row on record or a new
217
+ one. A definition only, built from `lb-form` and `lb-confirm`. New sends
218
+ `new` and Copy sends `copy`, requests the page declares; `new` returns
219
+ `url()` emptying the parm that names the row.
217
220
 
218
221
  | Parameter | Fills |
219
222
  |---------------|--------------------------------------------|
220
223
  | `exp-row` | The `lb-query` of the row the form shows |
221
224
  | `exp-form-id` | The form's `id`, `master-form` when absent |
222
225
 
226
+ ## `lb-checkbox`
227
+
228
+ A checkbox and its label as one element, for an application to style. A
229
+ definition only: a native checkbox is already a control (see
230
+ [Checkboxes](./data-binding.md#checkboxes)), so the widget is the markup, a
231
+ `<label>` holding the `<input type="checkbox">` and a `<span>` with the
232
+ text. The column and request go on the checkbox inside, so they arrive as
233
+ expansion attributes.
234
+
235
+ Unlike `lb-input`, it has no script making the tag the control. A value
236
+ that lands before a widget's script has loaded arrives only as the string
237
+ stamp `lb-column-value`, and a script checkbox would read `"false"` as
238
+ checked. The hub sets a native checkbox's `checked` directly.
239
+
240
+ | Parameter | Fills |
241
+ |---------------|-----------------------------------------------|
242
+ | `exp-column` | The checkbox's `lb-column` |
243
+ | `exp-label` | The label's text |
244
+ | `exp-request` | The checkbox's `lb-request`, none when absent |
245
+
223
246
  ## `lb-unknown-page`
224
247
 
225
248
  The chrome's dialog for a URL that names no page, as one tag:
package/docs/roadmap.md CHANGED
@@ -102,6 +102,14 @@ Most valuable against a large imported widget library, where an app uses a
102
102
  small fraction of what ships. Revisit when a real app's `app.css` is big
103
103
  enough to measure.
104
104
 
105
+ ### Options on `createHub`
106
+
107
+ Global hooks, transaction wrapping, error handling and a CSRF token each
108
+ want an option on `createHub`, `hubRoutes`, or both. `hubRoutes` already
109
+ takes a trailing options object, one key per concern. `createHub` takes
110
+ the same shape when its first option arrives, which adds a parameter and
111
+ breaks nothing.
112
+
105
113
  ### Queries stated by the chrome
106
114
 
107
115
  A custom element in the chrome naming a query forces every page to declare
@@ -109,8 +117,7 @@ that query, since the page load covers one page's set. `lb-url` shows the
109
117
  pattern from the framework side, and an application has no equivalent.
110
118
 
111
119
  A chrome-level query set on `createHub` would close this, and it is additive.
112
- It takes its place in the options argument whose shape is a 1.0 blocker in
113
- [TECHREF-1.0](./TECHREF-1.0.md#the-server-api).
120
+ It takes its place in the [options on `createHub`](#options-on-createhub).
114
121
 
115
122
  ### Data binding utilities
116
123
 
package/docs/testing.md CHANGED
@@ -263,8 +263,8 @@ One window per file, which is what running each file in its own process gives
263
263
  for free — registration is global and permanent, so sharing a process across
264
264
  widget files would mean sharing a registry.
265
265
 
266
- What that buys is real upgrades: the constructor, `connectedCallback`, and
267
- `attributeChangedCallback` for attributes already present. That last one is
266
+ What that buys is real upgrades: the constructor, the hub's `lb` methods,
267
+ and `attributeChangedCallback` for attributes already present. That last one is
268
268
  the mechanism by which hydration and refresh are one operation, and it
269
269
  behaves the same in jsdom as in a browser. What it does not buy is layout,
270
270
  painting, or navigation.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loadbare/app",
3
3
  "description": "High performance web app framework for server-bound applications",
4
- "version": "0.13.0",
4
+ "version": "0.15.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",