@loadbare/app 0.11.0 → 0.13.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 (56) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +61 -1
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/build/cli.js +2 -1
  5. package/dist/build/cli.js.map +1 -1
  6. package/dist/build/elements.d.ts +9 -1
  7. package/dist/build/elements.d.ts.map +1 -1
  8. package/dist/build/elements.js +50 -0
  9. package/dist/build/elements.js.map +1 -1
  10. package/dist/build/origins.d.ts +2 -0
  11. package/dist/build/origins.d.ts.map +1 -1
  12. package/dist/build/origins.js +1 -1
  13. package/dist/build/origins.js.map +1 -1
  14. package/dist/core/lb-constants.d.ts +26 -1
  15. package/dist/core/lb-constants.d.ts.map +1 -1
  16. package/dist/core/lb-constants.js +79 -0
  17. package/dist/core/lb-constants.js.map +1 -1
  18. package/dist/core/lb-types.d.ts +58 -18
  19. package/dist/core/lb-types.d.ts.map +1 -1
  20. package/dist/core/lb-types.js +74 -11
  21. package/dist/core/lb-types.js.map +1 -1
  22. package/dist/hub/lb-apply.d.ts +44 -16
  23. package/dist/hub/lb-apply.d.ts.map +1 -1
  24. package/dist/hub/lb-apply.js +694 -49
  25. package/dist/hub/lb-apply.js.map +1 -1
  26. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  27. package/dist/hub/lb-hub.browser.js +168 -36
  28. package/dist/hub/lb-hub.browser.js.map +1 -1
  29. package/dist/server/lb-express.d.ts +6 -4
  30. package/dist/server/lb-express.d.ts.map +1 -1
  31. package/dist/server/lb-express.js +32 -14
  32. package/dist/server/lb-express.js.map +1 -1
  33. package/dist/server/lb-server.d.ts +49 -10
  34. package/dist/server/lb-server.d.ts.map +1 -1
  35. package/dist/server/lb-server.js +87 -28
  36. package/dist/server/lb-server.js.map +1 -1
  37. package/docs/TECHREF-1.0.md +364 -72
  38. package/docs/comparison.md +16 -10
  39. package/docs/possible-ideas.md +188 -0
  40. package/docs/reference/chrome.md +22 -1
  41. package/docs/reference/custom-elements.md +90 -29
  42. package/docs/reference/data-binding.md +132 -7
  43. package/docs/reference/page-files.md +48 -11
  44. package/docs/reference/server.md +4 -0
  45. package/docs/reference/widgets.md +94 -24
  46. package/docs/roadmap.md +10 -6
  47. package/docs/testing.md +21 -10
  48. package/package.json +2 -3
  49. package/skills/loadbare-app/SKILL.md +85 -12
  50. package/skills/loadbare-app/references/TECHREF-1.0.md +364 -72
  51. package/skills/loadbare-app/references/chrome.md +22 -1
  52. package/skills/loadbare-app/references/custom-elements.md +90 -29
  53. package/skills/loadbare-app/references/data-binding.md +132 -7
  54. package/skills/loadbare-app/references/page-files.md +48 -11
  55. package/skills/loadbare-app/references/server.md +4 -0
  56. package/skills/loadbare-app/references/widgets.md +94 -24
@@ -48,7 +48,8 @@ page touches that page's files.
48
48
  ## HTML
49
49
 
50
50
  Write the page as a fragment. The fragment lands in `<main>`, which
51
- [chrome.html](./chrome.md) supplies.
51
+ [chrome.html](./chrome.md) supplies. A page carries no `<main>` and no
52
+ `<lb-hub>` of its own; the builder rejects one that does.
52
53
 
53
54
  ```html
54
55
  <!-- src/pages/about.page.html -->
@@ -89,14 +90,18 @@ export const queries: Queries = {
89
90
  page: "directory",
90
91
  count: String(await ctx.db.visitCount()),
91
92
  })),
92
- directory: rows("id", (ctx) => ctx.db.directory()),
93
+ directory: rows("id", (ctx) => ctx.db.directory(), { order: "name" }),
93
94
  };
94
95
  ```
95
96
 
96
- | Declared with | Answers with |
97
- |-------------------|-------------------------------------------|
98
- | `row(key, run)` | One row, an object |
99
- | `rows(key, run)` | All its rows, an array of objects, in order |
97
+ | Declared with | Answers with |
98
+ |----------------------------|-------------------------------------------|
99
+ | `row(key, run)` | One row, an object |
100
+ | `rows(key, run, options)` | All its rows, an array of objects |
101
+
102
+ `options` are a `rows` query's order: `order`, the columns the hub places
103
+ its rows by, and `serverSortedByUrl`, set when the query reads the user's
104
+ order parm itself. See [Order](./data-binding.md#order).
100
105
 
101
106
  `key` names the column that identifies a row. Give every row that column,
102
107
  including a `row` query's: an aggregate row answers with a constant key. The
@@ -109,8 +114,8 @@ page that needs the same data as one row and as a set declares two queries.
109
114
  The hub hands each value to the browser untouched, so what a number, a date
110
115
  or a null looks like is decided here, in the query.
111
116
 
112
- Return the full answer every time, and the rows in the order the page shows
113
- them. Sending only what changed is a request's job — see
117
+ Return the full answer every time. With no `order`, the rows show in the
118
+ order they are returned. Sending only what changed is a request's job — see
114
119
  [refresh and patch](#refresh-and-patch).
115
120
 
116
121
  ## Requests
@@ -120,7 +125,7 @@ optional:
120
125
 
121
126
  | Key | Runs |
122
127
  |---------------|--------------------------------------------------------|
123
- | `onPageEnter` | Before the page's queries, when the page loads |
128
+ | `onPageEnter` | Before the page's queries, when the page loads; may move the URL |
124
129
  | `handlers` | The page's declared requests, by request name |
125
130
  | `crud` | The requests Loadbare provides, by query name |
126
131
 
@@ -150,6 +155,12 @@ export const requests: Requests = {
150
155
 
151
156
  Declare no refresh set here. The page's queries run afterward.
152
157
 
158
+ It may return `url()` instead, to move the page to other query parms before
159
+ anything shows, as restoring the filters and order the user last had does.
160
+ The page loads there in the same round trip, and that load does not follow
161
+ `onPageEnter` again. A `url()` leading to the parms already in hand is
162
+ ignored. See [Moving the URL](#moving-the-url).
163
+
153
164
  ### handlers
154
165
 
155
166
  Declare a request under the name the HTML gives `lb-request`. Pair what it
@@ -223,6 +234,9 @@ Every value in `values` is a string, as a control's value is.
223
234
  ### refresh and patch
224
235
 
225
236
  List in `refresh` every query whose whole answer the request changed.
237
+ Never list one only to fill what the request's answer creates, such as the
238
+ picker in a new row: the hub fills that from the query's last answer. See
239
+ [What arrives later](./data-binding.md#what-arrives-later).
226
240
 
227
241
  Return answers from `run` to state a narrower change than re-running a query
228
242
  would. What `run` returns is keyed by query name and laid over the refreshed
@@ -236,8 +250,31 @@ queries:
236
250
  | `patch({ drop: [...] })` | These keys are gone; the rest stand |
237
251
 
238
252
  Return a patch for a change the request knows the extent of — one row added,
239
- one row dropped, one row edited — and leave `refresh` empty. Re-run the query
240
- instead when membership or order changed in a way the request cannot name:
253
+ one row dropped, one row edited — and leave `refresh` empty. A refreshed
254
+ `rows` query sends every row to say what one row could.
255
+
256
+ A change that seems to need a refresh usually has a patch:
257
+
258
+ - A row whose position changes: the hub places it by the query's
259
+ [order](./data-binding.md#order).
260
+ - A group that appears or goes: the hub makes a group with its first row
261
+ and removes it with its last, so no row stands in for an empty one.
262
+ - A row whose other columns change with the edit: re-read the row and patch
263
+ it.
264
+ - Rows the database removes with a deleted one: select their keys before the
265
+ delete and drop them too.
266
+
267
+ An update or a delete always has a patch, since it names its row by key and
268
+ removing a row never reorders the rest. `createHub` refuses at startup a
269
+ `crud` `rowUpdate` or `rowDelete` on a `rows` query whose `refresh` names
270
+ that same query. An insert may refresh its own query: where a new row goes
271
+ depends on whether its host places it, which the server cannot see.
272
+
273
+ Each request's `refresh` is its own, and every query in it needs its own
274
+ reason. A list shared by several requests is a warning sign.
275
+
276
+ Re-run the query when membership or order changed in a way the request
277
+ cannot name:
241
278
 
242
279
  ```ts
243
280
  resetRoster: {
@@ -91,6 +91,10 @@ TypeScript types itself. `dist/pages.ts` imports each `.requests.ts` and
91
91
  `.queries.ts` file by its full name, extension included, because Node looks
92
92
  for exactly the path an import names.
93
93
 
94
+ The `hub` it exports comes from `createHub`, the only implementation of
95
+ the `Hub` interface. That interface may gain members in any release, so an
96
+ application never implements it.
97
+
94
98
  Enable `allowImportingTsExtensions` in the application's `tsconfig.json`
95
99
  when `tsc` type-checks the server. Without it, `tsc` rejects the `.ts`
96
100
  extensions in `dist/pages.ts`:
@@ -1,7 +1,8 @@
1
1
  # The Basic Widget Library
2
2
 
3
- Six widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`, `lb-table`,
4
- `lb-unknown-page`. Every one of them is an ordinary custom element, written
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
5
6
  against the contracts in [Custom Elements](./custom-elements.md#html) for its
6
7
  definition and [Custom Elements](./custom-elements.md#code) for its class.
7
8
 
@@ -43,6 +44,7 @@ write the same edit twice.
43
44
  | Parameter | Fills |
44
45
  |----------------|----------------------------------|
45
46
  | `exp-label` | The visible `<label>` text |
47
+ | `exp-type` | The input's `type`, `text` when absent |
46
48
  | `exp-readonly` | The input's `readonly` attribute |
47
49
 
48
50
  ## `lb-select`
@@ -67,9 +69,7 @@ writes the row template inside the widget:
67
69
 
68
70
  ```html
69
71
  <lb-options lb-query="statuses" lb-column="status" lb-request="lb-row-update" exp-label="Status">
70
- <template data-group="category">
71
- <option lb-column="label"></option>
72
- </template>
72
+ <template><option lb-column="label"></option></template>
73
73
  </lb-options>
74
74
  ```
75
75
 
@@ -79,9 +79,15 @@ writes the row template inside the widget:
79
79
 
80
80
  - Each option's `value` is its row's key, the `lb-key-value` the hub stamps
81
81
  on the live row, so the widget holds a key and shows a label.
82
- - `data-group` on the row template sections the options into `<optgroup>`s,
83
- one per distinct value of that column, added and removed as rows arrive
84
- and leave.
82
+ - A group template holding an `<optgroup>` puts the options in groups, one
83
+ per run of the query's order's leading term. The widget labels each
84
+ `<optgroup>` from the `lb-group-value` the hub stamps on it:
85
+
86
+ ```html
87
+ <template lb-group>
88
+ <optgroup><template><option lb-column="label"></option></template></optgroup>
89
+ </template>
90
+ ```
85
91
  - Its `value` selects the option with that key, including an option that
86
92
  arrives after the value did.
87
93
 
@@ -100,7 +106,6 @@ one option showing one column:
100
106
  lb-request="lb-row-update"
101
107
  exp-label="Status"
102
108
  exp-column="label"
103
- exp-group="category"
104
109
  ></lb-picker>
105
110
  ```
106
111
 
@@ -108,10 +113,9 @@ one option showing one column:
108
113
  |--------------|-----------------------------------------|
109
114
  | `exp-label` | The visible `<label>` text |
110
115
  | `exp-column` | The column each option shows |
111
- | `exp-group` | The row template's `data-group` |
112
116
 
113
- An option built from two columns, or a row with a second element, is
114
- `lb-options` with the page's own row template.
117
+ An option built from two columns, a row with a second element, or options
118
+ in groups, is `lb-options` with the page's own templates.
115
119
 
116
120
  ## `lb-table`
117
121
 
@@ -123,11 +127,14 @@ row, the row template, and optionally a footer, each as a `<template>`:
123
127
  <template lb-exp-template="head">
124
128
  <tr><th>Date</th><th>Amount</th></tr>
125
129
  </template>
126
- <template data-sort="date" data-group="month">
127
- <tr><td lb-column="date"></td><td lb-column="amount"></td></tr>
130
+ <template lb-group>
131
+ <tr><th colspan="2" lb-column="month"></th></tr>
132
+ <template>
133
+ <tr><td lb-column="date"></td><td lb-column="amount"></td></tr>
134
+ </template>
128
135
  </template>
129
136
  <template lb-exp-template="foot">
130
- <tr lb-query="ledger-total"><td>Total</td><td lb-column="total"></td></tr>
137
+ <tr><td>Total</td><td lb-sum="amount"></td></tr>
131
138
  </template>
132
139
  </lb-table>
133
140
  ```
@@ -140,15 +147,78 @@ row, the row template, and optionally a footer, each as a `<template>`:
140
147
  |-----------------------------------|-----------------------------------------|
141
148
  | `<template lb-exp-template="head">` | The `<thead>` |
142
149
  | `<template lb-exp-template="foot">` | The `<tfoot>` |
143
- | Any other `<template>` | The row template, in the `<tbody>` |
144
-
145
- - `data-group` on the row template sections rows under a heading row, one
146
- per distinct value of that column, spanning every column of the row.
147
- - `data-sort` orders rows within a section, or within the whole body with no
148
- grouping, by the text of that column.
149
- - The footer is not a row of `ledger`. Its `<tr>` names a `row` query of its
150
- own, which lands on the `<tr>` itself. A grand total is a second query over
151
- the same data.
150
+ | Any other `<template>` | The row or group template, in the `<tbody>` |
151
+
152
+ - The hub places the rows by the query's order and builds its groups. The
153
+ rows land in the one `<tbody>`, so a group here is a heading row with its
154
+ rows after it.
155
+ - The footer is not a row of `ledger`. An aggregate there covers every row;
156
+ a figure that is not a sum of the rows, such as a budget, is a `<tr>`
157
+ naming a `row` query of its own.
158
+ - A `<template lb-exp-template="ghost">` is a blank row for entering a new
159
+ one, in a `<tbody>` of its own. The widget joins its controls to a form
160
+ outside the table, since a `<form>` cannot wrap a `<tr>`, and puts the
161
+ cursor back in it once an insert from it succeeds.
162
+ - After rows land, the widget scrolls the row the page's own request created
163
+ or moved into view.
164
+
165
+ ## `lb-confirm`
166
+
167
+ A button that asks before it sends. The page writes the question inside the
168
+ tag, and the request goes on the dialog's confirm button, so nothing reaches
169
+ the hub until the user says yes:
170
+
171
+ ```html
172
+ <lb-confirm exp-label="Delete" exp-request="lb-row-delete">
173
+ Delete <b lb-column="name"></b>?
174
+ </lb-confirm>
175
+ ```
176
+
177
+ | Parameter | Fills |
178
+ |---------------|--------------------------------------------------|
179
+ | `exp-label` | The text of the button that opens the dialog |
180
+ | `exp-request` | The confirm button's `lb-request` |
181
+ | `exp-confirm` | The confirm button's text, `Confirm` when absent |
182
+
183
+ The widget closes the dialog once `lb-request-done` says the request
184
+ succeeded, and leaves it open on a refusal.
185
+
186
+ ## `lb-form`
187
+
188
+ 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
+
192
+ | Parameter | Fills |
193
+ |-----------|-----------------|
194
+ | `exp-id` | The form's `id` |
195
+
196
+ ## `lb-insert-dialog`
197
+
198
+ A row entered in a modal dialog, written once outside every row and opened
199
+ with `commandfor` and `command="show-modal"`. Save is the form's
200
+ `lb-row-insert`; on success the widget chooses the new row in the nearest
201
+ `lb-options` around the button that opened it, and closes. The handler
202
+ answers with a patch holding the new row.
203
+
204
+ | Parameter | Fills |
205
+ |-------------|---------------------------------------------------|
206
+ | `exp-id` | The dialog's `id` |
207
+ | `exp-title` | The dialog's heading |
208
+ | `exp-query` | The form's `lb-query`, which it inserts into |
209
+ | `exp-save` | The Save button's text, `Save` when absent |
210
+
211
+ ## `lb-master`
212
+
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.
217
+
218
+ | Parameter | Fills |
219
+ |---------------|--------------------------------------------|
220
+ | `exp-row` | The `lb-query` of the row the form shows |
221
+ | `exp-form-id` | The form's `id`, `master-form` when absent |
152
222
 
153
223
  ## `lb-unknown-page`
154
224
 
package/docs/roadmap.md CHANGED
@@ -29,12 +29,6 @@ expanding row). [Theory](./theory.md) already flags this as possibly load-bearin
29
29
  disallowed. Worth a decision-in-principle the first time a master-detail
30
30
  page is built, even before the mechanism is needed elsewhere.
31
31
 
32
- ### Pending appearance
33
-
34
- A value that hasn't arrived yet is probably derivable from an absent
35
- `lb-column-value` rather than needing a signal of its own. Not yet needed because
36
- nothing currently produces that gap in practice — revisit if one does.
37
-
38
32
  ### Events while a request is in flight
39
33
 
40
34
  The hub ignores a commit on an element carrying `lb-request-pending`.
@@ -108,6 +102,16 @@ Most valuable against a large imported widget library, where an app uses a
108
102
  small fraction of what ships. Revisit when a real app's `app.css` is big
109
103
  enough to measure.
110
104
 
105
+ ### Queries stated by the chrome
106
+
107
+ A custom element in the chrome naming a query forces every page to declare
108
+ that query, since the page load covers one page's set. `lb-url` shows the
109
+ pattern from the framework side, and an application has no equivalent.
110
+
111
+ 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).
114
+
111
115
  ### Data binding utilities
112
116
 
113
117
  A custom element that finds its own nearest ancestor `lb-query`,
package/docs/testing.md CHANGED
@@ -86,6 +86,9 @@ anything ships:
86
86
  `lb-url-unknown` off a `<dialog>` or outside `<lb-hub>`
87
87
  - `lb-show` on a `<template>`, on a row template's root, or with no row
88
88
  around it
89
+ - a chrome without exactly one `<lb-hub>` and one empty `<main>` inside it,
90
+ and a page carrying either
91
+ - a widget script declaring an `lb` method Loadbare does not define
89
92
 
90
93
  A page file's `<title>` is lifted out and stamped as `lb-page-title`.
91
94
 
@@ -119,6 +122,13 @@ The rest of the build — `assemble`, `elements`, `pages`, `styles`,
119
122
  `package-css` — is tested the same way and in the same tier, since none of it
120
123
  needs a browser either.
121
124
 
125
+ **The command.** `loadbare-app-build` itself runs in a child process against
126
+ a temporary project, which carries a stand-in `@loadbare/app` pointing at
127
+ the hub's source so the test needs no prior build. It is tested for its
128
+ default `src` and `dist`, every file it writes, `--minify`, a failed build's
129
+ exit code, and `--watch` rebuilding on a change, skipping a file it does not
130
+ watch, and surviving a failed build.
131
+
122
132
  ## Tier 2 — The engine
123
133
 
124
134
  `createHub` takes a plain object and returns an object. Nothing in
@@ -163,14 +173,14 @@ evidence here.
163
173
  the row around it, and the columns inside it are its own query's
164
174
  - a query nothing names is reported and skipped; several elements naming one
165
175
  query are all filled
166
- - all rows decide membership and order, so a key that did not arrive is gone
167
- - a patch disturbs only what it names, in contents and in position
176
+ - all rows decide membership, so a key that did not arrive is gone
177
+ - a patch disturbs only what it names, in contents, and a row moves only
178
+ when the order puts it somewhere else
168
179
  - `lb-query-row-count` is counted from the DOM after reconciliation, so all
169
180
  rows and a patch ending in the same state report the same number
170
- - `lbPlaceRow` is called for a fresh row always, and for an existing row
171
- only under all rows. That is today's behavior, not a decision — a patch
172
- therefore never re-places a row whose sort key changed. The test states
173
- what is true now, and is the one that flips if that changes.
181
+ - the order places all rows and a patch alike, groups come and go with
182
+ their rows to any depth, aggregates follow every landing, and every change
183
+ leaves a stamp a stylesheet can see (`lb-order.test.ts`)
174
184
  - `lb-show` moves an element into a template and back, and landing reaches
175
185
  inside it
176
186
 
@@ -223,6 +233,8 @@ What the hub is tested for here:
223
233
  `lb-request-error` replaces it on failure, and the next request clears
224
234
  it; `aria-busy` comes and goes with `lb-request-pending`, and a commit on
225
235
  a pending element is ignored
236
+ - a round trip the server never answers is aborted at the ten-second
237
+ deadline, under mocked timers, and fails the way any round trip fails
226
238
  - a successful insert gathered from a form resets the form
227
239
  - a path with no page reports, lands `lb-page-unknown`, and opens the
228
240
  `lb-url-unknown` dialog; two pages for one stub report and take the first
@@ -235,7 +247,7 @@ What the hub is tested for here:
235
247
  - an `lb-url` item in a server answer writes the URL and lands the page
236
248
  loaded there
237
249
  - `hidden` comes off the body once a page has landed, including the page
238
- that failed to load
250
+ that failed to load, and a link whose page fails to load marks no element
239
251
 
240
252
  Widgets are small and their logic is local, so jsdom carries them: a value
241
253
  reaches the control the widget owns, the widget is a form-associated control
@@ -266,9 +278,8 @@ No browser test runner is installed and none of the following exists. They
266
278
  are recorded here as the shape of the work, not as coverage:
267
279
 
268
280
  1. a cold load fills `<main>` with no empty flash
269
- 2. the ten-second deadline aborts a request that never answers
270
- 3. the wizard never displays a step the server has not confirmed
271
- 4. the view-source invariant
281
+ 2. the wizard never displays a step the server has not confirmed
282
+ 3. the view-source invariant
272
283
 
273
284
  The last is the one worth a browser. Loadbare's central claim is that no
274
285
  markup exists in the DOM that is not in view-source, and that the difference
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.11.0",
4
+ "version": "0.13.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -22,8 +22,7 @@
22
22
  "./constants": "./dist/core/lb-constants.js",
23
23
  "./types": "./dist/core/lb-types.js",
24
24
  "./server": "./dist/server/lb-server.js",
25
- "./express": "./dist/server/lb-express.js",
26
- "./build": "./dist/build/elements.js"
25
+ "./express": "./dist/server/lb-express.js"
27
26
  },
28
27
  "scripts": {
29
28
  "prepublishOnly": "npm run typecheck && npm run test && npm run build",
@@ -159,7 +159,9 @@ row template lands on the element itself. A `rows` query with no row
159
159
  template lands nothing, which is what an insert form naming its query is. A
160
160
  condition is `lb-show="<column>"`: write every possibility into the page and
161
161
  let a column decide which is present. The column is a boolean or null; the
162
- string `"false"` counts as present.
162
+ string `"false"` counts as present. `lb-show="!<column>"` is present when
163
+ the column is not, so one column decides both sides and the query never
164
+ sends a flag and its opposite.
163
165
 
164
166
  **`{{placeholder}}` is build time only.** It reads an `exp-` attribute on
165
167
  the tag, never data. Write it as an entire attribute value or an entire
@@ -174,9 +176,34 @@ from it.
174
176
 
175
177
  **A column never holds rows.** Master-detail is a `row` query and a `rows`
176
178
  query under two names. Many masters with their details is one `rows` query
177
- of joined rows, grouped for display by a custom element's `lbPlaceRow`. A
179
+ of joined rows, ordered by the master first and shown in groups. A
178
180
  query nested inside another query's row template receives the same rows in
179
- every outer row; use it for a picker, never for per-row detail.
181
+ every outer row; use it for a picker, never for per-row detail. A new outer
182
+ row is filled from the nested query's last answer, whatever order they land
183
+ in.
184
+
185
+ **A `rows` query declares its order, and the hub places every row by it.**
186
+ `rows("id", run, { order: "category_order,name" })`; a column after `-` sorts
187
+ descending, and values compare by JSON type, so a numeric sort column is a
188
+ JSON number. The parm `lb-order-<query>` replaces the order, and a control
189
+ writing it inside `lb-query="lb-url"` re-sorts with no round trip. A query
190
+ that pages or limits reads the parm itself and declares
191
+ `serverSortedByUrl: true`. Never sort in a widget, and never send a
192
+ placeholder row to hold a place.
193
+
194
+ **Groups are templates.** A `<template lb-group>` holds a heading and one
195
+ nested `<template>`, the next level or the row template; the nth breaks on
196
+ the order's nth term, to any depth. Contents land before the nested
197
+ template, so put it inside the heading's element for a container and beside
198
+ the heading for a heading row. `<tbody>` and `<optgroup>` do not nest. The
199
+ heading is filled from the group's first row. `lb-count`, `lb-sum="col"`,
200
+ `lb-avg`, `lb-min` and `lb-max` total the nearest group, or the whole list
201
+ outside any.
202
+
203
+ **Animate with CSS.** The hub stamps `lb-row-created`, `lb-row-changed`,
204
+ `lb-row-moved`, and `lb-row-requested` for this page's request, and marks a
205
+ leaving row or group `lb-row-leaving` or `lb-group-leaving`, removing it once
206
+ its animations end. Scrolling to a row is script, in `lbRowsLanded`.
180
207
 
181
208
  **Every request has one shape.** `lb-request` names the request, and the
182
209
  hub sends `{ name, query, key, values }` as present: `query` from the
@@ -201,6 +228,12 @@ elsewhere with the HTML `form` attribute. Checkboxes, radio buttons and
201
228
  file inputs are not controls here: they receive no value and are not
202
229
  gathered.
203
230
 
231
+ **A form that adds to a set chosen elsewhere is written once, outside every
232
+ row.** A form cannot sit inside another form or in a table row, so a dialog
233
+ that creates an account while a posting's picker is choosing one goes at the
234
+ end of the page, and each row opens it with `commandfor`. The dialog learns
235
+ the new key from `lb-request-done`.
236
+
204
237
  **Three requests are Loadbare's.** `lb-row-insert` needs `query` and
205
238
  `values`, `lb-row-update` needs `query`, `key` and `values`, and
206
239
  `lb-row-delete` needs `query` and `key`. The page permits each under
@@ -216,8 +249,28 @@ page may edit.
216
249
 
217
250
  **Return a patch when the change has a known extent.** `patch({ rows })`
218
251
  for rows added or edited, `patch({ drop })` for keys removed, with
219
- `refresh: []`. List a query in `refresh` only when its membership or order
220
- changed in a way the handler cannot name.
252
+ `refresh: []`. A refreshed `rows` query sends every row to say what one row
253
+ could. List a query in `refresh` only when its membership changed in a way
254
+ the handler cannot name. Never list one only to fill the
255
+ picker in a row the request adds: its answer did not change, and the hub
256
+ fills a new row from the last one.
257
+
258
+ **The usual reasons for a refresh each have a patch.**
259
+ - A row whose position changes: the hub places it by the query's order.
260
+ - A group that appears or goes: the hub makes it with its first row and
261
+ removes it with its last.
262
+ - A row whose other columns change with the edit: re-read the row and patch
263
+ it.
264
+ - Rows the database removes with a deleted one: select their keys before
265
+ the delete and drop them too.
266
+
267
+ An update or a delete names its row by key, and removing a row never
268
+ reorders the rest, so each always has a patch: `createHub` refuses at
269
+ startup a `rowUpdate` or `rowDelete` whose `refresh` names its own `rows`
270
+ query. An insert may refresh its own query, though a patch of the new row
271
+ lands in its place by the order.
272
+ Each handler's `refresh` is its own, and every query in it needs its own
273
+ reason. A list shared by several handlers is a warning sign.
221
274
 
222
275
  **A page's queries are its view model, not a data layer.** Each answers
223
276
  one page's elements in the form they show: formatted dates and amounts, and
@@ -247,6 +300,12 @@ puts parms onto `ctx`, and a query reads them there. Read them from
247
300
  request. A parm is user input, so validate it where it is read. Do not
248
301
  keep a selection in server state set by a handler.
249
302
 
303
+ **Where focus starts is the hub's.** On entering a page, once its queries
304
+ have landed, the hub focuses the first control in `<main>` the user can
305
+ operate; a change of query parm leaves focus where it is. Order the page so
306
+ that control is the one to start in, and write no `autofocus` and no script
307
+ that focuses on load.
308
+
250
309
  **A handler moves the URL with `url()`.** After an insert, only the server
251
310
  knows the new key; after a delete, only the server knows the parm should
252
311
  go. Have `run` return `url({ acct: String(id) })`, or `url({ acct: "" })`
@@ -286,12 +345,25 @@ on `change`. Any other custom element keeps its content and receives the
286
345
  column as its `lb-column-value` attribute.
287
346
 
288
347
  **A custom element connects many times.** It is moved, never rebuilt: the
289
- hub places every live row again on each set of all rows, and parks an
290
- `lb-show` element in a template while it is off, so `connectedCallback` runs
291
- on every move. Listen on the element itself in the constructor, look a
348
+ hub moves a live row the order puts elsewhere, and parks an `lb-show`
349
+ element in a template while it is off, so `connectedCallback` runs on every
350
+ move. Listen on the element itself in the constructor, look a
292
351
  child up when it is needed, and in a control take up `lb-column-value` once,
293
352
  on first connect, since a value can land before the element upgrades.
294
353
 
354
+ **The hub and a custom element talk in four ways, one per kind of
355
+ message.** State the hub gives an element is an attribute stamp, or `value`
356
+ on a control. Work the hub needs done while landing is an optional `lb`
357
+ method: `lbRowsLanded`. A moment is a bubbling event: an
358
+ element sends `lb-request`, and the hub answers on the same element with
359
+ `lb-request-done`, carrying the request and the response items that landed,
360
+ or the error. Listen for `lb-request-done` on whichever ancestor needs the
361
+ outcome, such as a dialog choosing the row its form just created.
362
+
363
+ **A widget that holds rows may sit under `lb-show`.** Rows that land while
364
+ it is away are placed by the hub, and its `lbRowsLanded` runs once it first
365
+ appears, so hide a table with `lb-show`, not with a stylesheet rule.
366
+
295
367
  **Name a custom element script `<tag>.browser.ts`.** A plain `<tag>.ts`
296
368
  stays on the server and the tag goes unregistered.
297
369
 
@@ -318,12 +390,13 @@ parser loses the tag before the build can report it.
318
390
 
319
391
  Reach for one only when plain HTML cannot do the job. A set of rows, a form
320
392
  and a condition need none. A custom element exists to be a control of its
321
- own, or to place and scaffold rows through the two hooks the hub calls,
322
- `lbPlaceRow` and `lbRowsLanded` (the `RowsHost` interface).
393
+ own, or to act on rows once they land through the hook the hub calls,
394
+ `lbRowsLanded` (the `RowsHost` interface).
323
395
 
324
396
  Before writing one, check `@loadbare/widgets`: `lb-input`, `lb-select`,
325
- `lb-options`, `lb-picker`, `lb-table`, `lb-unknown-page`. The first four
326
- are form-associated controls that carry `lb-column` and `lb-request`
397
+ `lb-options`, `lb-picker`, `lb-table`, `lb-unknown-page`, `lb-confirm`,
398
+ `lb-form`, `lb-insert-dialog`, `lb-master`. The first four are
399
+ form-associated controls that carry `lb-column` and `lb-request`
327
400
  themselves. Install the package and list it in `src/imports.ts`:
328
401
 
329
402
  ```ts