@loadbare/app 0.16.0 → 0.18.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 (46) hide show
  1. package/README.md +10 -14
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +15 -1
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/core/lb-constants.d.ts +6 -2
  6. package/dist/core/lb-constants.d.ts.map +1 -1
  7. package/dist/core/lb-constants.js +38 -8
  8. package/dist/core/lb-constants.js.map +1 -1
  9. package/dist/core/lb-types.d.ts +57 -20
  10. package/dist/core/lb-types.d.ts.map +1 -1
  11. package/dist/core/lb-types.js +122 -11
  12. package/dist/core/lb-types.js.map +1 -1
  13. package/dist/hub/lb-apply.d.ts +38 -26
  14. package/dist/hub/lb-apply.d.ts.map +1 -1
  15. package/dist/hub/lb-apply.js +655 -471
  16. package/dist/hub/lb-apply.js.map +1 -1
  17. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  18. package/dist/hub/lb-hub.browser.js +77 -59
  19. package/dist/hub/lb-hub.browser.js.map +1 -1
  20. package/dist/hub/lb-lifecycle.d.ts +1 -2
  21. package/dist/hub/lb-lifecycle.d.ts.map +1 -1
  22. package/dist/hub/lb-lifecycle.js +9 -3
  23. package/dist/hub/lb-lifecycle.js.map +1 -1
  24. package/dist/server/lb-server.d.ts +29 -12
  25. package/dist/server/lb-server.d.ts.map +1 -1
  26. package/dist/server/lb-server.js +79 -19
  27. package/dist/server/lb-server.js.map +1 -1
  28. package/docs/TECHREF-1.0.md +167 -53
  29. package/docs/comparison.md +9 -8
  30. package/docs/reference/chrome.md +8 -5
  31. package/docs/reference/custom-elements.md +25 -7
  32. package/docs/reference/data-binding.md +125 -22
  33. package/docs/reference/page-files.md +56 -11
  34. package/docs/reference/widgets.md +16 -1
  35. package/docs/roadmap.md +8 -0
  36. package/docs/testing.md +6 -2
  37. package/docs/theory-old.md +839 -0
  38. package/docs/theory.md +113 -839
  39. package/package.json +1 -1
  40. package/skills/loadbare-app/SKILL.md +32 -20
  41. package/skills/loadbare-app/references/TECHREF-1.0.md +167 -53
  42. package/skills/loadbare-app/references/chrome.md +8 -5
  43. package/skills/loadbare-app/references/custom-elements.md +25 -7
  44. package/skills/loadbare-app/references/data-binding.md +125 -22
  45. package/skills/loadbare-app/references/page-files.md +56 -11
  46. package/skills/loadbare-app/references/widgets.md +16 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  A page shows server data by naming queries and columns in its HTML, and asks
4
4
  the server to change that data by naming requests. The developer writes
5
- seven `lb-` attributes. The server declares every query a page may show and
5
+ nine `lb-` attributes. The server declares every query a page may show and
6
6
  every request it may send, and refuses anything it has not declared.
7
7
 
8
8
  Everything is a query. A query is a name for rows, of kind `row` or `rows`,
@@ -46,6 +46,8 @@ each with its key, and permits the three requests on `roster`; see
46
46
  | `lb-query` | Any element | The query whose rows land in it |
47
47
  | `lb-column` | Any element | The column it shows, and the column it gathers |
48
48
  | `lb-show` | Any element but `<template>` | The column that decides whether it is present |
49
+ | `lb-order` | An element with `lb-query` | The choice holding the user's order; see [Order](#order) |
50
+ | `lb-filter` | An element with `lb-query` | The columns a choice filters; see [Filters](#filters) |
49
51
  | `lb-request` | Any element | The request it sends when it commits |
50
52
  | `lb-url-link` | An `<a>` | A link between pages; see [chrome](./chrome.md#links) |
51
53
  | `lb-url-push` | An element with `lb-request` | A URL change that pushes a history entry |
@@ -59,7 +61,7 @@ builder writes a stamp, and a stylesheet or a custom element reads it.
59
61
  | `lb-column-value` | The value the hub set from a column |
60
62
  | `lb-key-value` | The key of a live row, or of a `row` landed on an element |
61
63
  | `lb-row-live` | The element is a live row |
62
- | `lb-query-row-count` | The number of live rows an element holds |
64
+ | `lb-query-row-count` | The number of live rows an element shows |
63
65
  | `lb-request-pending` | The element's request is in flight |
64
66
  | `lb-request-error` | The element's last request failed |
65
67
  | `lb-page` | The stub of a page, on its `<template>` |
@@ -170,7 +172,8 @@ places it immediately before the template:
170
172
  The hub matches each row to a live row by its key. All rows decide
171
173
  membership: a live row whose key did not arrive leaves. A patch changes only
172
174
  the rows it names, and every other live row keeps its content. The hub
173
- places every live row by the query's order; see [Order](#order).
175
+ places every live row by the query's order; see [Order](#order). A row that
176
+ does not pass the user's filter is `hidden`; see [Filters](#filters).
174
177
 
175
178
  A live row whose values arrive as it already shows them is not filled
176
179
  again. A control in it that the user has typed into and not committed keeps
@@ -180,6 +183,11 @@ whose values changed is filled whole, and its controls show what arrived.
180
183
  A key is unique within a query. Two rows with one key in the same answer,
181
184
  or two live rows showing one key, are reported on the console.
182
185
 
186
+ When the focus is in a live row that placing moves, the hub puts it back on
187
+ the same element and scrolls it into view. When that row leaves, or a
188
+ filter hides it, the focus goes to the first element the user can operate
189
+ in the first row showing.
190
+
183
191
  A live row's root counts as a column when it carries `lb-column`, which is
184
192
  how an `<option>`, whose content is text, shows the column it is.
185
193
 
@@ -196,19 +204,83 @@ commas, each descending when written after `-`:
196
204
  roster: rows("id", (ctx) => ctx.db.members(), { order: "team,name" }),
197
205
  ```
198
206
 
199
- The query parm `lb-order-<query>` replaces it for that query, so a control
200
- inside `lb-query="lb-url"` writing that parm re-sorts the rows the hub
201
- already has, with no round trip. With no order, rows show in the order they
202
- arrived, and a patch's new rows go last.
207
+ With no order, rows show in the order they arrived, and a patch's new rows
208
+ go last.
209
+
210
+ The user's order replaces the server's. Write `lb-order` on the element
211
+ carrying `lb-query`, naming a column of [`lb-choices`](#choices). That
212
+ column holds an order written the same way, and while it is empty the
213
+ server's order places the rows:
214
+
215
+ ```html
216
+ <div lb-query="lb-choices">
217
+ <select lb-column="roster_order" lb-request="lb-row-update">
218
+ <option value="">By team</option>
219
+ <option value="name">By name</option>
220
+ <option value="-joined">Newest first</option>
221
+ </select>
222
+ </div>
223
+
224
+ <ul lb-query="roster" lb-order="roster_order">
225
+ <template><li lb-column="name"></li></template>
226
+ </ul>
227
+ ```
203
228
 
204
229
  Values compare by their JSON type: numbers numerically, strings as the
205
230
  user's language orders them, `false` before `true`, and `null` last. A
206
- number sent as a string sorts as a string. Rows that compare equal keep the
207
- order they arrived in. A row moves only when the order puts it somewhere
231
+ number sent as a string sorts as a string, unless the query declares the
232
+ column's [type](./page-files.md#queries): a column declared `number` sorts
233
+ numerically whatever JSON type its values arrive as, and a value that is no
234
+ number sorts with the nulls. Rows that compare equal keep the order they
235
+ arrived in. A row moves only when the order puts it somewhere
208
236
  else.
209
237
 
210
- A query that pages or limits its rows reads the parm itself and declares
211
- `serverSortedByUrl: true`, so a change of order loads the page again.
238
+ ### Filters
239
+
240
+ Write `lb-filter` on the element carrying `lb-query`, naming the columns of
241
+ its rows the user may filter, separated by commas. The column of
242
+ [`lb-choices`](#choices) of the same name holds the filter for each:
243
+
244
+ ```html
245
+ <div lb-query="lb-choices">
246
+ <input lb-column="name" lb-request="lb-row-update" placeholder="Name" />
247
+ </div>
248
+
249
+ <ul lb-query="roster" lb-filter="name, team">
250
+ <template><li lb-column="name"></li></template>
251
+ </ul>
252
+ ```
253
+
254
+ A row that does not pass every filter is `hidden`, and a ticked `lb-select`
255
+ in it is cleared. A group whose rows are all hidden is hidden too. An empty
256
+ choice filters nothing.
257
+
258
+ A filter is one or more terms, each an operator followed by a value:
259
+
260
+ | Operator | A column passes when it |
261
+ |----------|------------------------------------------|
262
+ | `=` | Equals the value |
263
+ | `!=` | Does not equal the value |
264
+ | `<` | Is less than the value |
265
+ | `<=` | Is at most the value |
266
+ | `>` | Is greater than the value |
267
+ | `>=` | Is at least the value |
268
+ | `^=` | Begins with the value |
269
+ | `$=` | Ends with the value |
270
+ | `*=` | Contains the value |
271
+
272
+ Text with no operator is `*=`. A term begins wherever whitespace comes
273
+ before an operator, so a value may hold spaces. Every term must pass, which
274
+ is how a range is written: `>=2026-01-01 <=2026-03-31`. A term with no
275
+ value is left out, so a filter being typed filters nothing until it has
276
+ one.
277
+
278
+ `^=`, `$=` and `*=` read the column as text and ignore case. The others
279
+ compare as the order compares, with the value read as the column's type: a
280
+ number for a number, `true` or `false` for a boolean, and text otherwise.
281
+ The column's type is the one its query declares, if it declares one, and
282
+ its JSON type otherwise. A value that is not of the column's type, and a
283
+ column that is null or missing, pass only `!=`.
212
284
 
213
285
  ### Groups
214
286
 
@@ -240,7 +312,8 @@ A heading is filled from its group's first row, so it may show a column
240
312
  other than the one the group breaks on. The hub stamps each top-level
241
313
  element of a group with `lb-group-live`, `lb-group-column` and
242
314
  `lb-group-value`. A row creates its group, and a group leaves with its last
243
- row.
315
+ row. When the order changes, a group whose run goes on is kept and moved,
316
+ and a group whose run ended leaves.
244
317
 
245
318
  ### Aggregates
246
319
 
@@ -254,7 +327,8 @@ or of the whole list outside any group:
254
327
 
255
328
  A sum and an average are exact, read from JSON numbers and from strings
256
329
  holding a plain decimal. `null` and empty values are left out. The result
257
- lands unformatted, and covers the rows the browser holds.
330
+ lands unformatted, and covers the rows showing: a row a filter hides is
331
+ left out.
258
332
 
259
333
  ### What happened to a row
260
334
 
@@ -264,15 +338,18 @@ Every change to a list is one a stylesheet can see:
264
338
  | ------------------ | ------------------------------------------------------ |
265
339
  | `lb-row-created` | Created when the hub last touched the row |
266
340
  | `lb-row-changed` | Its values changed when the hub last touched it |
267
- | `lb-row-moved` | The order put it somewhere else when last touched |
341
+ | `lb-row-moved` | Its values changed, and it now stands elsewhere |
268
342
  | `lb-row-requested` | `created`, `changed` or `moved` by this page's request |
269
343
  | `lb-row-leaving` | A row on its way out |
270
344
  | `lb-group-leaving` | A group on its way out |
271
345
 
272
346
  A row that leaves loses `lb-row-live`, takes `inert`, and is removed once
273
- its animations finish, at once when there are none. A moved row moves, and
274
- a copy stays behind where it was, leaving. `lb-row-requested` is cleared at
275
- every landing; the others when the hub next touches the row.
347
+ its animations finish, at once when there are none. `lb-row-requested` is
348
+ cleared at every landing; the others when the hub next touches the row.
349
+
350
+ A row is moved when its values changed and the rows whose values did not
351
+ change now stand around it differently. A row it passed is not stamped,
352
+ and a change of the user's order, which changes no values, stamps none.
276
353
 
277
354
  ```css
278
355
  [lb-row-created] { animation: arrive 200ms; }
@@ -293,8 +370,9 @@ A custom element under an `lb-show` that is off is filled like any other,
293
370
  and its `lbRowsLanded` runs as its rows land. See
294
371
  [Holding rows](./custom-elements.md#holding-rows).
295
372
 
296
- Nothing is answered from what the hub kept: a request always goes to the
297
- server, and a query re-runs only when a response names it. A patch that
373
+ A request always goes to the server, and a query re-runs only when a
374
+ response names it. A [choice](#choices) is the one thing landed from what
375
+ the hub kept. A patch that
298
376
  arrives before all of a query's rows is not kept, since it is not the whole
299
377
  of anything.
300
378
 
@@ -305,8 +383,8 @@ insert form above: it sits inside `lb-query="roster"` so its request is for
305
383
  ### Counting rows
306
384
 
307
385
  The hub stamps every element that holds a row template with
308
- `lb-query-row-count`, the number of live rows it holds. An empty query is a
309
- stylesheet rule:
386
+ `lb-query-row-count`, the number of live rows it shows. A row a filter
387
+ hides is not counted. An empty query is a stylesheet rule:
310
388
 
311
389
  ```html
312
390
  <div lb-query="roster">
@@ -906,13 +984,36 @@ answers it with no round trip:
906
984
  See [The URL](./chrome.md#the-url) for its columns, links, history, and the
907
985
  unknown-page dialog.
908
986
 
987
+ ## Choices
988
+
989
+ The hub serves a second query, `lb-choices`: one row keyed by `lb-path`,
990
+ with a column for each choice the user made for how rows show, an
991
+ [order](#order) or a [filter](#filters). The application names the
992
+ columns. A control inside `lb-query="lb-choices"` that sends
993
+ `lb-row-update` makes a choice, and an element anywhere may make one by
994
+ dispatching the `lb-request` event with `lb-row-update` for `lb-choices`,
995
+ which the hub keys by the path; see
996
+ [Sending a request](./custom-elements.md#sending-a-request).
997
+
998
+ A choice joins the ones already made, and shows with no round trip: every
999
+ element whose `lb-order` or `lb-filter` reads a column that changed lands
1000
+ its query's last answer again, as a round trip carrying the choice would
1001
+ have. Then the hub sends the server every choice made on the page, as
1002
+ strings, in `lb-row-update` for `lb-choices`. No element waits on it, and
1003
+ the page's [`onChoices`](./page-files.md#onchoices) runs, when it has one.
1004
+
1005
+ Choices hold until the page changes, and a change of query parm keeps them.
1006
+ On entering a page, every column named under `lb-query="lb-choices"` lands
1007
+ empty, unless the page's `onPageEnter` restores them. Choices are not query
1008
+ parms, and the URL does not carry them.
1009
+
909
1010
  ## The markup checks
910
1011
 
911
1012
  The developer writes `lb-` attributes in markup, and script never assigns
912
1013
  them. The builder checks the markup of the chrome and of every page, after
913
1014
  expansion, and refuses to build:
914
1015
 
915
- - an `lb-` attribute that is not one of the seven the developer writes
1016
+ - an `lb-` attribute that is not one of the nine the developer writes
916
1017
  - `lb-request` naming an `lb-` request other than the three Loadbare provides
917
1018
  - `lb-url-link` on anything but an `<a>`
918
1019
  - `lb-url-push` on an element without `lb-request`
@@ -920,6 +1021,8 @@ expansion, and refuses to build:
920
1021
  - `lb-show` on a `<template>`, or on a row template's root
921
1022
  - `lb-show` with no `lb-query` around it
922
1023
  - `lb-show="!"` or `lb-show="!!column"`, which reverse no column
1024
+ - `lb-order` or `lb-filter` on an element not carrying `lb-query`, or naming
1025
+ no column
923
1026
  - `lb-select` on anything but `<input type="checkbox">`, outside a row
924
1027
  template, or beside `lb-column` or `lb-request`
925
1028
  - a `command` beginning `--lb-` that is not one of the three commands
@@ -99,9 +99,23 @@ export const queries: Queries = {
99
99
  | `row(key, run)` | One row, an object |
100
100
  | `rows(key, run, options)` | All its rows, an array of objects |
101
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).
102
+ `options` are a `rows` query's order and column types. `order` is the
103
+ columns the hub places its rows by, and the user's order, a choice, replaces
104
+ it. See [Order](./data-binding.md#order). `types` names a type for any of
105
+ its columns, `number`, `boolean` or `text`, and the hub sorts and filters
106
+ that column read as its type:
107
+
108
+ ```ts
109
+ payroll: rows("id", (ctx) => ctx.db.payroll(), {
110
+ order: "-amount",
111
+ types: { amount: "number" },
112
+ }),
113
+ ```
114
+
115
+ Declare a type for a column whose values arrive as some other JSON type,
116
+ such as a Postgres `numeric` sent as a string. A column with no type
117
+ compares by its JSON type. The order and the types go to the browser once,
118
+ with the answer that loads the page.
105
119
 
106
120
  `key` names the column that identifies a row. Give every row that column,
107
121
  including a `row` query's: an aggregate row answers with a constant key. The
@@ -120,12 +134,13 @@ order they are returned. Sending only what changed is a request's job — see
120
134
 
121
135
  ## Requests
122
136
 
123
- Export `requests` from `<stub>.requests.ts`. It holds three keys, each
137
+ Export `requests` from `<stub>.requests.ts`. It holds four keys, each
124
138
  optional:
125
139
 
126
140
  | Key | Runs |
127
141
  |---------------|--------------------------------------------------------|
128
- | `onPageEnter` | Before the page's queries, when the page loads; may move the URL |
142
+ | `onPageEnter` | Before the page's queries, when the page loads; may move the URL or restore choices |
143
+ | `onChoices` | Each time the user makes a choice |
129
144
  | `handlers` | The page's declared requests, by request name |
130
145
  | `crud` | The requests Loadbare provides, by query name |
131
146
 
@@ -160,11 +175,40 @@ export const requests: Requests = {
160
175
  Declare no refresh set here. The page's queries run afterward.
161
176
 
162
177
  It may return `url()` instead, to move the page to other query parms before
163
- anything shows, as restoring the filters and order the user last had does.
164
- The page loads there in the same round trip, and that load does not follow
178
+ anything shows, as restoring the parms the user last had does. The page
179
+ loads there in the same round trip, and that load does not follow
165
180
  `onPageEnter` again. A `url()` leading to the parms already in hand is
166
181
  ignored. See [Moving the URL](#moving-the-url).
167
182
 
183
+ It may return `choices()`, alone or beside `url()`, to restore the user's
184
+ [choices](./data-binding.md#choices) for the page:
185
+
186
+ ```ts
187
+ // src/pages/directory.requests.ts
188
+ import { choices, url, type Requests } from "@loadbare/app/server";
189
+
190
+ export const requests: Requests = {
191
+ onPageEnter: async (ctx) => {
192
+ const saved = await ctx.db.savedView("directory");
193
+ if (saved) return { ...url(saved.parms), ...choices(saved.choices) };
194
+ },
195
+ onChoices: (ctx, chosen) => ctx.db.saveChoices("directory", chosen),
196
+ };
197
+ ```
198
+
199
+ The page load answers `lb-choices` first, so the rows land by the choices.
200
+ When `url()` moves the page, the choices arrive with the load at the new
201
+ URL. `choices()`, like `url()`, takes columns whose values are strings, at
202
+ least one of them; anything else is reported on the server console and
203
+ ignored.
204
+
205
+ ### onChoices
206
+
207
+ `onChoices` runs each time the user makes a choice, with every choice made
208
+ on the page, by column, as strings. It is how an application keeps them for
209
+ `onPageEnter` to restore, and answers nothing, by convention. Whatever it
210
+ returns lands. A page without it is not told.
211
+
168
212
  ### handlers
169
213
 
170
214
  Declare a request under the name the HTML gives `lb-request`. Pair what it
@@ -293,10 +337,11 @@ A change that seems to need a refresh usually has a patch:
293
337
  delete and drop them too.
294
338
 
295
339
  An update or a delete always has a patch, since it names its row by key and
296
- removing a row never reorders the rest. `createHub` refuses at startup a
297
- `crud` `rowUpdate` or `rowDelete` on a `rows` query whose `refresh` names
298
- that same query. An insert may refresh its own query: where a new row goes
299
- depends on whether its host places it, which the server cannot see.
340
+ removing a row never reorders the rest. So does an insert, which has the new
341
+ row's key once it has written it, and the hub places the new row by the
342
+ query's order. `createHub` refuses at startup a `crud` `rowInsert`,
343
+ `rowUpdate` or `rowDelete` on a `rows` query whose `refresh` names that same
344
+ query.
300
345
 
301
346
  Each request's `refresh` is its own, and every query in it needs its own
302
347
  reason. A list shared by several requests is a warning sign.
@@ -78,6 +78,7 @@ writes the row template inside the widget:
78
78
  | Parameter | Fills |
79
79
  |-------------|----------------------------|
80
80
  | `exp-label` | The visible `<label>` text |
81
+ | `exp-size` | The `<select>`'s `size`, an open list rather than a drop-down |
81
82
 
82
83
  - Each option's `value` is its row's key, the `lb-key-value` the hub stamps
83
84
  on the live row, so the widget holds a key and shows a label.
@@ -90,6 +91,9 @@ writes the row template inside the widget:
90
91
  <optgroup><template><option lb-column="label"></option></template></optgroup>
91
92
  </template>
92
93
  ```
94
+ - A heading option marked `data-group-label` in the `<optgroup>`, `hidden`
95
+ and `disabled`, labels the group with its text instead, so the label can
96
+ be a column of the group's first row.
93
97
  - Its `value` selects the option with that key, including an option that
94
98
  arrives after the value did.
95
99
 
@@ -154,7 +158,11 @@ row, the row template, and optionally a footer, each as a `<template>`:
154
158
  - The hub places the rows by the query's order and builds its groups. The
155
159
  rows land in the one `<tbody>`, so a group here is a heading row with its
156
160
  rows after it.
157
- - The footer is not a row of `ledger`. An aggregate there covers every row;
161
+ - `lb-order` and `lb-filter` go on the `lb-table` itself, beside
162
+ `lb-query`; see [Order](./data-binding.md#order) and
163
+ [Filters](./data-binding.md#filters).
164
+ - The footer is not a row of `ledger`. An aggregate there covers every row
165
+ showing;
158
166
  a figure that is not a sum of the rows, such as a budget, is a `<tr>`
159
167
  naming a `row` query of its own.
160
168
  - A `<template lb-exp-template="ghost">` is a blank row for entering a new
@@ -163,6 +171,9 @@ row, the row template, and optionally a footer, each as a `<template>`:
163
171
  cursor back in it once an insert from it succeeds.
164
172
  - After rows land, the widget scrolls the row the page's own request created
165
173
  or moved into view.
174
+ - Text selected in a cell whose `lb-column` the table's `lb-filter` names
175
+ becomes that column's filter in `lb-choices`: `=` for the whole text,
176
+ `^=` from its start, `$=` up to its end, `*=` otherwise.
166
177
 
167
178
  ## `lb-confirm`
168
179
 
@@ -203,6 +214,10 @@ with `commandfor` and `command="show-modal"`. Save is the form's
203
214
  `lb-options` around the button that opened it, and closes. The handler
204
215
  answers with a patch holding the new row.
205
216
 
217
+ While open, the button that opened it carries `insert-dialog-opener` and the
218
+ widget carries `anchored`, for a stylesheet to place the dialog beside the
219
+ button with CSS anchor positioning.
220
+
206
221
  | Parameter | Fills |
207
222
  |-------------|---------------------------------------------------|
208
223
  | `exp-id` | The dialog's `id` |