@loadbare/app 0.8.2 → 0.10.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 (80) hide show
  1. package/README.md +3 -3
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +74 -66
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/build/expand.d.ts.map +1 -1
  6. package/dist/build/expand.js +20 -19
  7. package/dist/build/expand.js.map +1 -1
  8. package/dist/build/locations.d.ts +2 -3
  9. package/dist/build/locations.d.ts.map +1 -1
  10. package/dist/build/locations.js +2 -3
  11. package/dist/build/locations.js.map +1 -1
  12. package/dist/build/pages.d.ts +3 -4
  13. package/dist/build/pages.d.ts.map +1 -1
  14. package/dist/build/pages.js +3 -4
  15. package/dist/build/pages.js.map +1 -1
  16. package/dist/core/lb-constants.d.ts +25 -23
  17. package/dist/core/lb-constants.d.ts.map +1 -1
  18. package/dist/core/lb-constants.js +95 -158
  19. package/dist/core/lb-constants.js.map +1 -1
  20. package/dist/core/lb-types.d.ts +73 -75
  21. package/dist/core/lb-types.d.ts.map +1 -1
  22. package/dist/core/lb-types.js +58 -5
  23. package/dist/core/lb-types.js.map +1 -1
  24. package/dist/hub/lb-apply.d.ts +47 -37
  25. package/dist/hub/lb-apply.d.ts.map +1 -1
  26. package/dist/hub/lb-apply.js +174 -193
  27. package/dist/hub/lb-apply.js.map +1 -1
  28. package/dist/hub/lb-hub.browser.d.ts +1 -1
  29. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  30. package/dist/hub/lb-hub.browser.js +419 -415
  31. package/dist/hub/lb-hub.browser.js.map +1 -1
  32. package/dist/server/lb-express.d.ts +20 -13
  33. package/dist/server/lb-express.d.ts.map +1 -1
  34. package/dist/server/lb-express.js +50 -52
  35. package/dist/server/lb-express.js.map +1 -1
  36. package/dist/server/lb-server.d.ts +81 -116
  37. package/dist/server/lb-server.d.ts.map +1 -1
  38. package/dist/server/lb-server.js +151 -48
  39. package/dist/server/lb-server.js.map +1 -1
  40. package/docs/TECHREF-1.0.md +893 -558
  41. package/docs/comparison.md +222 -185
  42. package/docs/prior-art.md +15 -14
  43. package/docs/reference/builder.md +9 -3
  44. package/docs/reference/chrome.md +107 -56
  45. package/docs/reference/custom-elements.md +199 -173
  46. package/docs/reference/data-binding.md +375 -370
  47. package/docs/reference/overview.md +12 -10
  48. package/docs/reference/page-files.md +161 -86
  49. package/docs/reference/server.md +13 -7
  50. package/docs/reference/widgets.md +104 -110
  51. package/docs/roadmap.md +36 -31
  52. package/docs/terms-of-art.md +57 -0
  53. package/docs/testing.md +97 -68
  54. package/docs/theory.md +92 -58
  55. package/docs/tutorials/010-pages-and-navigation.md +20 -12
  56. package/docs/tutorials/020-css.md +6 -3
  57. package/docs/tutorials/030-html-decomposition.md +9 -7
  58. package/docs/tutorials/040-displaying-data.md +30 -13
  59. package/docs/tutorials/{050-actions.md → 050-requests.md} +25 -15
  60. package/docs/tutorials/060-custom-element-code.md +17 -16
  61. package/docs/tutorials/065-conditional-rendering.md +34 -23
  62. package/docs/tutorials/070-displaying-a-list.md +29 -21
  63. package/docs/tutorials/072-inserting-into-a-list.md +24 -16
  64. package/docs/tutorials/074-deleting-from-a-list.md +9 -7
  65. package/docs/tutorials/076-updating-a-list-item.md +11 -10
  66. package/docs/tutorials/080-widget-requests.md +71 -43
  67. package/docs/tutorials/090-using-widget-libraries.md +22 -22
  68. package/package.json +1 -1
  69. package/skills/loadbare-app/SKILL.md +178 -111
  70. package/skills/loadbare-app/references/TECHREF-1.0.md +893 -558
  71. package/skills/loadbare-app/references/builder.md +9 -3
  72. package/skills/loadbare-app/references/chrome.md +107 -56
  73. package/skills/loadbare-app/references/custom-elements.md +199 -173
  74. package/skills/loadbare-app/references/data-binding.md +375 -370
  75. package/skills/loadbare-app/references/overview.md +12 -10
  76. package/skills/loadbare-app/references/page-files.md +161 -86
  77. package/skills/loadbare-app/references/server.md +13 -7
  78. package/skills/loadbare-app/references/widgets.md +104 -110
  79. package/docs/analysis-accidental-complexity.md +0 -149
  80. package/docs/analysis-closed-set.md +0 -210
@@ -1,16 +1,14 @@
1
1
  # Loadbare/app and current front-end frameworks
2
2
 
3
3
  > LLM-authored, not yet revised by a person. Drafted by Claude on
4
- > 2026-09-16 against the working tree of `@loadbare/app` 0.7.3, which
5
- > includes `lb-show`. Statements about other frameworks are from general
4
+ > 2026-09-16 and brought up to date with the working tree that follows
5
+ > `@loadbare/app` 0.9.0. Statements about other frameworks are from general
6
6
  > knowledge of those projects as of 2026 and carry no per-claim sources.
7
7
 
8
8
  This document states how `@loadbare/app` differs from the frameworks and
9
9
  libraries most used for front-end development today. It repeats material
10
- from [Theory](./theory.md), [TECHREF-1.0](./TECHREF-1.0.md),
11
- [Prior art](./prior-art.md) and
12
- [Assessing the accidental complexity claim](./analysis-accidental-complexity.md)
13
- so that it can be read on its own. Where this document and
10
+ from [Theory](./theory.md), [TECHREF-1.0](./TECHREF-1.0.md) and
11
+ [Prior art](./prior-art.md) so that it can be read on its own. Where this document and
14
12
  [Theory](./theory.md) disagree, Theory is authoritative.
15
13
 
16
14
  Loadbare/app has not reached 1.0. The [Blockers](./TECHREF-1.0.md#blockers)
@@ -50,10 +48,11 @@ Loadbare/app is a framework for server-bound database applications. The
50
48
  browser receives one HTML document containing every page as a `<template>`,
51
49
  one script and one stylesheet. After that, only JSON rows cross the wire.
52
50
  HTML attributes bind elements to named server queries, and one attribute,
53
- `lb-action`, turns a click or a submit into a request. The server answers
54
- every request with rows, sets of rows, or patches to sets of rows, and the
55
- browser-side runtime, called the hub, writes those values into the elements
56
- bound to them.
51
+ `lb-request`, turns a click, a change or a submit into a request. The
52
+ server answers every request with response items: one row, a set of rows,
53
+ or a patch to a set of rows, each named by its query. The browser-side
54
+ runtime, called the hub, lands those values on the elements that name the
55
+ query.
57
56
 
58
57
  Compared with the client component frameworks (React, Vue, Angular, Svelte,
59
58
  Solid), Loadbare/app has no component tree rendered at run time, no
@@ -62,7 +61,8 @@ template expressions.
62
61
 
63
62
  Compared with the server-integrated meta-frameworks (Next.js, Nuxt,
64
63
  SvelteKit, React Router 7, Astro), Loadbare/app has no server rendering and
65
- no hydration, ships no server of its own, and has no route parameters.
64
+ no hydration, ships no server of its own, and has no route parameters:
65
+ a path names a page, and the query parms carry everything finer.
66
66
 
67
67
  Compared with the hypermedia libraries (htmx, Hotwire Turbo, Unpoly),
68
68
  Loadbare/app sends JSON rather than HTML after the first load, requires a
@@ -131,22 +131,25 @@ The hub holds no application data. There is no store, no signal, no
131
131
  observable, and no client cache of query results.
132
132
 
133
133
  - A value that has landed exists in the DOM: as `textContent` or `value`, and
134
- as the `lb-value` attribute.
135
- - A list exists as the DOM rows the hub cloned, each stamped with
136
- `lb-key-value`.
134
+ as the `lb-column-value` stamp.
135
+ - The rows of a `rows` query exist as the live rows the hub cloned, each
136
+ stamped with `lb-key-value`.
137
+ - The URL is a row too: the hub serves the query `lb-url`, whose columns are
138
+ the path, the page label and the query parms.
137
139
  - An element hidden by `lb-show` exists inside a `<template>` standing where
138
140
  it stood.
139
141
  - The hub keeps two pieces of state of its own: the name of the current page,
140
- and, until an insert completes, a record of which controls it read.
142
+ and, until an insert completes, the form it gathered from.
141
143
 
142
- The server is the source of truth for everything else, including view state.
143
- A query takes no argument from the browser; its only input is `ctx`, which
144
- the application builds from the Express request. Which record a page shows,
145
- or which step of a wizard is current, is held on the server where `ctx`
146
- reaches it. A reload runs the page's queries again and shows the same state.
144
+ The server is the source of truth for everything else. A query takes no
145
+ argument from the browser; its only input is `ctx`, which the application
146
+ builds with `contextFor` from the Express request and the query parms.
147
+ Which record a page shows, a filter or a date range is a query parm, so a
148
+ reload, a bookmark or a mailed link runs the page's queries again and shows
149
+ the same state.
147
150
 
148
- A URL names a page and nothing finer. Whether a page URL may carry
149
- parameters is an open blocker in TECHREF-1.0.
151
+ A URL's path names a page and nothing finer. Everything finer is a query
152
+ parm.
150
153
 
151
154
  ### Elsewhere
152
155
 
@@ -179,20 +182,22 @@ At run time the hub performs a closed set of operations:
179
182
 
180
183
  | Trigger | Operation |
181
184
  |----------------------------|----------------------------------------------------------------------------|
182
- | Navigation | Clone the page's `<template>` into `<main>` |
183
- | A value for a native element | Set `textContent`, and set `lb-value` |
184
- | A value for a form control | Set `value`, and set `lb-value` |
185
- | A value for a custom element | Set `lb-value` |
186
- | A new key in a list | Clone the row template, stamp `lb-key-value`, fill it, insert it |
187
- | A whole list | Place every row in the order given, remove rows whose keys did not arrive |
185
+ | Navigation | Clone the page's `<template>` into `<main>`, land `lb-url` |
186
+ | A value for a control | Set `value`, and stamp `lb-column-value` |
187
+ | A value for another native element | Set `textContent`, and stamp `lb-column-value` |
188
+ | A value for a custom element that is not a control | Stamp `lb-column-value` |
189
+ | A `row` with no row template | Land it on the element itself, stamp `lb-key-value` |
190
+ | A new key through a row template | Clone the row template, stamp `lb-key-value`, fill it, insert it |
191
+ | All rows | Place every row in the order given, remove rows whose keys did not arrive |
188
192
  | A patch | Upsert the rows named, remove the keys named, leave other rows in place |
189
- | After any list result | Stamp `lb-row-count` |
193
+ | After rows land | Stamp `lb-query-row-count` |
190
194
  | An `lb-show` column | Move the element into, or out of, a `<template lb-show>` |
191
- | A request starts or ends | Set or remove `lb-pending` and `aria-busy`; set `lb-error` on failure |
192
- | An insert succeeds | Reset the controls it gathered to their defaults |
195
+ | A request starts or ends | Set or remove `lb-request-pending` and `aria-busy`; set `lb-request-error` on failure |
196
+ | An insert from a form succeeds | Reset the form |
193
197
 
194
198
  The hub finds targets with `querySelectorAll` on the attributes written in
195
- the HTML. No tree is compared with a previous tree. A response names what
199
+ the HTML. Each response item names its query, its kind and its key, so the
200
+ markup names none of the three beyond the query. No tree is compared with a previous tree. A response names what
196
201
  changed, so the hub does not determine what changed. Between interactions
197
202
  the hub runs no timers, observers or polling loops.
198
203
 
@@ -224,7 +229,7 @@ values its queries return, serialized as JSON.
224
229
 
225
230
  The HTML written in `.html` files is the HTML that ships, after expansion.
226
231
  Attribute values in the `lb-` namespace are names: a query name, a column
227
- name, or an action name. No attribute value is an expression.
232
+ name, or a request name. No attribute value is an expression.
228
233
 
229
234
  There are two kinds of placeholder, and they run at different times.
230
235
 
@@ -232,7 +237,7 @@ There are two kinds of placeholder, and they run at different times.
232
237
  a whole attribute value or a whole text node. The tag supplies it with an
233
238
  `exp-name` attribute. An unsupplied placeholder with no default drops the
234
239
  attribute. The value becomes permanent in the shipped HTML.
235
- - Run time. Data lands through `lb-cell`, `lb-list` with a keyed
240
+ - Run time. Data lands through `lb-column`, `lb-query` with a row
236
241
  `<template>`, and `lb-show`. There is no run-time interpolation into
237
242
  attribute values or text.
238
243
 
@@ -241,8 +246,8 @@ listed as decided against in the [Roadmap](./roadmap.md#decided-against).
241
246
 
242
247
  | Task | Loadbare/app | React (JSX) | Vue | Angular | Svelte | Alpine.js |
243
248
  |-------------------|-----------------------------------------------|--------------------------------------|--------------------------------------|---------------------------------------|-------------------------------|--------------------------------------------|
244
- | Show a value | `<span lb-cell="name">` | `{row.name}` | `{{ row.name }}` | `{{ row.name }}` | `{row.name}` | `<span x-text="row.name">` |
245
- | Keyed list | `<template lb-key="id">` inside `lb-list` | `rows.map(r => <li key={r.id}>)` | `<li v-for="r in rows" :key="r.id">` | `@for (r of rows; track r.id)` | `{#each rows as r (r.id)}` | `<template x-for="r in rows" :key="r.id">` |
249
+ | Show a value | `<span lb-column="name">` | `{row.name}` | `{{ row.name }}` | `{{ row.name }}` | `{row.name}` | `<span x-text="row.name">` |
250
+ | Keyed list | `<template>` inside `lb-query`, key from the server | `rows.map(r => <li key={r.id}>)` | `<li v-for="r in rows" :key="r.id">` | `@for (r of rows; track r.id)` | `{#each rows as r (r.id)}` | `<template x-for="r in rows" :key="r.id">` |
246
251
  | Condition | `lb-show="column"` | `{cond && <X />}` | `v-if`, `v-show` | `@if (cond) { }` | `{#if cond}` | `x-if`, `x-show` |
247
252
  | Expression in markup | Not supported | Any JavaScript expression | JavaScript expressions | Template expressions | JavaScript expressions | JavaScript expressions |
248
253
  | Build-time constant | `exp-label="Name"` into `{{label}}` | A prop | A prop | An input | A prop | Not applicable |
@@ -296,20 +301,24 @@ without `.browser` is not bundled, and the build error names it.
296
301
  - Light DOM throughout. Loadbare/app uses no shadow DOM.
297
302
  - No base class. A widget extends `HTMLElement`.
298
303
  - No props at run time. Build-time parameters arrive as `exp-` attributes.
299
- Data arrives as the `lb-value` attribute, which the widget observes through
304
+ Data arrives one of two ways. A form-associated custom element with a
305
+ `value` property is a control: the hub sets its `value`, gathers it back
306
+ like an `<input>`, and commits it on `change`. Any other custom element
307
+ receives the `lb-column-value` stamp, which it observes through
300
308
  `attributeChangedCallback`. The browser calls that callback for an
301
309
  attribute already present at upgrade, so first render and refresh are one
302
310
  code path.
303
- - Composition at build time. `lb-slot` marks where authored content goes;
304
- `lb-template` names additional destinations filled by
305
- `<template lb-template="name">`. A definition may use other widgets; a
311
+ - Composition at build time. `lb-exp-slot` marks where authored content
312
+ goes; `lb-exp-template` names additional destinations filled by
313
+ `<template lb-exp-template="name">`. A definition may use other widgets; a
306
314
  cycle is a build error.
307
- - A widget sends a request by dispatching a bubbling `lb-request`
308
- `CustomEvent`. The hub adds the scope (`list` or `row`, `key`, `cell`). An
309
- ancestor may stop the event.
310
- - A list widget may implement `lbPlaceRow(el, row, template)` to decide where
311
- a row goes, and `lbRowsLanded()` to build scaffolding such as group
312
- headings. The hub does the cloning, matching, removal and counting.
315
+ - A widget may send a request by dispatching a bubbling `lb-request`
316
+ `CustomEvent`. The hub completes it with `query`, `key` and `values` from
317
+ where the element sits. An ancestor may stop the event.
318
+ - A custom element carrying `lb-query` and a row template may implement
319
+ `lbPlaceRow(el, row, template)` to decide where a row goes, and
320
+ `lbRowsLanded()` to build scaffolding such as group headings. The hub does
321
+ the cloning, matching, removal and counting.
313
322
  - Method names beginning with `lb` on a custom element are reserved.
314
323
 
315
324
  Widget libraries are npm packages listed in `imports.ts`. The builder scans
@@ -330,14 +339,15 @@ The following follow from the builder and hub code.
330
339
  - A `.browser.ts` file is bundled by esbuild and may import any package
331
340
  esbuild resolves. A custom element may mount another framework's component
332
341
  tree inside itself.
333
- - The hub does not turn a click on a hyphenated element into a request. A
334
- third-party element carrying `lb-action` sends nothing unless it dispatches
335
- `lb-request` itself.
336
- - A bound value lands on a third-party element as `lb-value`. The element
337
- displays it only if it observes that attribute.
338
- - Gathering for `lb-row-insert` and `lb-row-update` reads `<input>`, `<select>`
339
- and `<textarea>` elements found with `querySelectorAll`, which does not
340
- enter a shadow root.
342
+ - A third-party form-associated custom element with a `value` property that
343
+ fires `change` is a control, and takes `lb-column` and `lb-request` as an
344
+ `<input>` does. Any other third-party element carrying `lb-request`
345
+ commits on `click`.
346
+ - A value lands on a third-party element that is not a control as the
347
+ `lb-column-value` stamp. The element displays it only if it observes that
348
+ attribute.
349
+ - Gathering reads controls found in the light DOM and through a form's
350
+ owner, which does not enter a shadow root.
341
351
 
342
352
  ### Elsewhere
343
353
 
@@ -366,27 +376,29 @@ the query string the browser is showing.
366
376
 
367
377
  - An empty body asks for the page's whole query set. The server runs the
368
378
  page's `onPageEnter`, then every query the page declares.
369
- - A body with an `action` runs that action and its refresh set.
370
- - A request without an action answers 400. A `run` that throws answers 500.
379
+ - A body carrying a request, `{ name, query, key, values }`, runs the handler
380
+ its name picks and then its refresh set.
381
+ - A request without a name answers 400. A `run` that throws answers 500.
371
382
 
372
- Every response has one shape: an object keyed by query name.
383
+ Every response has one shape: an array of response items. Each names its
384
+ query, the kind and key the server declared, and one of these:
373
385
 
374
- | Value | Means |
375
- |------------------|-----------------------------------------------------|
376
- | An object | One row, for a name declared with `row()` |
377
- | An array | The whole set, and its order, for a `list()` |
378
- | `{ rows, drop }` | A patch to a `list()`: upsert these, remove these keys |
386
+ | Member | Means |
387
+ |------------------|-------------------------------------------------------|
388
+ | `row` | One row, for a query declared with `row()` |
389
+ | `rows` | All rows, and their order, for a query declared with `rows()` |
390
+ | `patch` | `{ rows, drop }` for a `rows` query: upsert these, remove these keys |
379
391
 
380
- A cell holds one value. A set of values is a list of its own. A row never
381
- contains rows. Master-detail is a row and a list answered under two names.
382
- Many masters with their details is one list of joined rows, grouped for
383
- display by a widget's `lbPlaceRow`.
392
+ A column holds one value. A set of values is a query of its own. A row
393
+ never contains rows. Master-detail is a row and rows answered under two
394
+ names. Many masters with their details is one `rows` query of joined rows,
395
+ grouped for display by a custom element's `lbPlaceRow`.
384
396
 
385
397
  The application designs no endpoints, routes or serialization format. The
386
398
  names in the markup are the keys in the page's `.queries.ts` and
387
- `.requests.ts` files. The browser can reach only the queries, actions and
388
- CRUD operations a page declares; any other name answers `{}` and logs a
389
- server warning.
399
+ `.requests.ts` files. The browser can reach only the queries, declared
400
+ requests and `crud` entries a page declares; any other name answers `[]` and
401
+ logs a server warning.
390
402
 
391
403
  Values are not converted. The hub hands each value to the browser as the
392
404
  query produced it. Formatting a date or an amount is done in the query.
@@ -411,48 +423,54 @@ Data types are an open blocker in TECHREF-1.0.
411
423
 
412
424
  ### Loadbare/app
413
425
 
414
- `lb-action` names what an interaction asks for. A value beginning with `lb-`
415
- is one of three reserved operations; any other value is an action the page
416
- declares.
417
-
418
- | `lb-action` | Written on | Carries | Server key under `crud` |
419
- |------------------|---------------------------------------------|---------------------------------|-------------------------|
420
- | `lb-row-insert` | A `<form>`, or a button in a `<tr>`, in a list | `list`, `values` | `rowInsert` |
421
- | `lb-row-update` | A `<form>`, a button in a live row, or a widget cell | `list`, `key`, `values` | `rowUpdate` |
422
- | `lb-row-delete` | Anything inside a live row | `list`, `key` | `rowDelete` |
423
- | Any other name | Any element | Scope in effect, and a widget's `value` | Under `actions` |
424
-
425
- - Position comes from the document. `list` or `row` comes from the nearest
426
- ancestor scope, `key` from the nearest live row, `cell` from the element's
427
- own `lb-cell`. The author writes the action and not the position.
428
- - Values are gathered from the nearest `<form>`, `<tr>` or live row around
429
- the element, the way a button submits its form owner. A widget carrying
430
- `lb-cell` sends its own cell alone, so one `rowUpdate` answers a form and a
431
- per-field edit.
432
- - A native element sends on click, a form on submit. A widget sends its own.
433
- - A declared action carries no argument list.
434
- - A single-row scope is read-only for the three operations, because each needs
435
- a key and a key exists only on a live row in a list.
426
+ `lb-request` names what an interaction asks for. A name beginning with
427
+ `lb-` is one of three requests Loadbare provides; any other name is a
428
+ request the page declares.
429
+
430
+ | `lb-request` | Needs | Server key |
431
+ |------------------|--------------------------|--------------------------------|
432
+ | `lb-row-insert` | `query`, `values` | `crud.<query>.rowInsert` |
433
+ | `lb-row-update` | `query`, `key`, `values` | `crud.<query>.rowUpdate` |
434
+ | `lb-row-delete` | `query`, `key` | `crud.<query>.rowDelete` |
435
+ | Any other name | Whatever is present | `handlers.<name>` |
436
+
437
+ - An element commits the way HTML says it does: a form on `submit`, a
438
+ control on `change`, anything else on `click`. A submit button commits
439
+ with its form, and its own `lb-request` wins over the form's, the way
440
+ `formaction` wins over `action`.
441
+ - Position comes from the document. `query` is the nearest ancestor
442
+ `lb-query` of what was gathered, and `key` comes from the nearest ancestor
443
+ row of that query. The author writes the request name and not the
444
+ position.
445
+ - Values are gathered from controls: the element alone when it carries
446
+ `lb-column`, a form's controls, a live row's, or the controls whose form
447
+ owner is the element's form owner, the HTML `form` attribute included. A
448
+ control carrying `lb-column` and `lb-request` sends its own column alone,
449
+ so one `rowUpdate` answers a form and a per-field edit.
450
+ - Every request has one shape, `{ name, query, key, values }`, and a
451
+ declared request receives the same fields.
452
+ - The hub does not send a request Loadbare provides that lacks what the
453
+ table lists, and says why in the console.
436
454
 
437
455
  On the server, each entry has `run` and `refresh`. `run` does the work and
438
- may return results, such as a `patch()`. `refresh` names queries to run
439
- again. What `run` returns is laid over the refreshed results.
456
+ may return results, such as a `patch()`, or a new row for the URL with
457
+ `url()`. `refresh` names queries to run again. What `run` returns is laid
458
+ over the refreshed results.
440
459
 
441
- The hub stamps the dispatching element:
460
+ The hub stamps the element that committed:
442
461
 
443
- | Attribute | Means |
444
- |----------------|------------------------------------------------------------|
445
- | `lb-pending` | The round trip is in flight; `aria-busy="true"` is set with it |
446
- | `lb-error` | The last round trip from this element failed |
462
+ | Stamp | Means |
463
+ |----------------------|------------------------------------------------------------|
464
+ | `lb-request-pending` | The round trip is in flight; `aria-busy="true"` is set with it |
465
+ | `lb-request-error` | The last round trip from this element failed |
447
466
 
448
- A native button or form performed again while it carries `lb-pending` is
449
- ignored. A widget is not held back. A round trip has a ten-second deadline,
450
- after which it is aborted and treated as a failure. `lb-error` records that
451
- a round trip failed, and carries no message.
467
+ A commit on an element carrying `lb-request-pending` is ignored. A round
468
+ trip has a ten-second deadline, after which it is aborted and treated as a
469
+ failure. `lb-request-error` records that a round trip failed, and carries
470
+ no message.
452
471
 
453
- After a successful `lb-row-insert`, the hub resets each control it gathered
454
- to its default, as `form.reset()` does, unless the control was edited during
455
- the round trip. A failed insert resets nothing.
472
+ After a successful `lb-row-insert` gathered from a form, the hub resets the
473
+ form. A failed insert resets nothing.
456
474
 
457
475
  The browser displays only values the server returned. There is no
458
476
  optimistic update. Per-keystroke validation is an open question in the
@@ -482,24 +500,30 @@ roadmap.
482
500
  - The path `/members` names the page `members`. The path `/` names `index`.
483
501
  - There are no route parameters, nested routes or per-page layouts. The
484
502
  chrome, `chrome.html`, is the one document surrounding every page.
485
- - An anchor carrying `lb-nav-link` navigates inside the application: the hub
486
- calls `history.pushState`, clones the page template into `<main>`, then
487
- fetches the page's data. An anchor without the attribute is an ordinary
488
- link. `popstate` navigates back and forward.
503
+ - An anchor carrying `lb-url-link` navigates inside the application on a
504
+ plain primary click: the hub calls `history.pushState`, clones the page
505
+ template into `<main>`, then fetches the page's data. An anchor without
506
+ the attribute is an ordinary link. `popstate` navigates back and forward.
489
507
  - Navigation fetches no HTML and no code. Every page's markup is already in
490
508
  the document.
491
509
  - The server answers every GET with the same `app.html` and status 200. A
492
510
  path that names no page is detected in the browser, which opens a
493
- `<dialog lb-unknown-page>` if the chrome has one.
494
- - The hub publishes its own row, `lb-navigation`, with `page-uri` and
495
- `page-label` (the text of the matching `lb-nav-link` anchor), bound like
496
- any server row.
497
- - The hub reads `location.pathname` only. A query string does not reach the
498
- server.
511
+ `<dialog lb-url-unknown>` if the chrome has one.
512
+ - The hub serves its own query, `lb-url`, of kind `row` and keyed by
513
+ `lb-path`. Its columns are `lb-path`, `lb-page-label` (the page file's
514
+ `<title>`, or its stub), `lb-page-unknown`, and one column per query parm,
515
+ bound like any server row.
516
+ - A control inside `lb-query="lb-url"` carrying `lb-request="lb-row-update"`
517
+ writes a query parm. The hub answers that request itself: it replaces the
518
+ history entry, or pushes one when the element carries `lb-url-push`, and
519
+ reloads the page's queries. A handler moves the URL by returning
520
+ `url({ ... })`, and the page loads there in the same round trip.
521
+ - The query string rides on every round trip, and the server hands its parms
522
+ to `contextFor`.
499
523
  - The application must be served from the origin root. The hub reaches
500
524
  `/lb`, `/client.js` and `/app.css` by absolute path.
501
- - The hub does not change `document.title`, move focus, or restore scroll
502
- position on navigation.
525
+ - The hub sets `document.title` from `lb-page-label`. It does not move focus
526
+ or restore scroll position on navigation.
503
527
 
504
528
  ### Elsewhere
505
529
 
@@ -551,20 +575,25 @@ Checked at build time, as errors:
551
575
  containing a capital
552
576
  - slot and template errors: two slots, two templates of one name, a template
553
577
  naming a missing destination, content in a definition with no slot
554
- - `lb-cell` outside every scope, `lb-key` on anything but a list's row
555
- template, `lb-list` and `lb-row` on one element
556
- - `lb-show` on a `<template>`, on a row template's root, or with no row
557
- around it
558
- - `lb-unknown-page` on anything but a `<dialog>` inside `<lb-hub>`
578
+ - an `lb-` attribute in markup that is not one a developer writes (`lb-query`,
579
+ `lb-column`, `lb-show`, `lb-request`, `lb-url-link`, `lb-url-push`,
580
+ `lb-url-unknown`); the rest are stamps, and script never assigns them
581
+ - an `lb-request` value beginning with `lb-` that is not one of the three
582
+ requests Loadbare provides
583
+ - `lb-url-link` on anything but an `<a>`, `lb-url-push` without
584
+ `lb-request`, `lb-url-unknown` on anything but a `<dialog>` inside
585
+ `<lb-hub>`
586
+ - `lb-show` on a `<template>`, on a row template's root, or with no
587
+ `lb-query` around it
559
588
  - a `.queries.ts` or `.requests.ts` with no matching page
560
589
 
561
- Checked when the server starts: a query or action name beginning with `lb-`.
590
+ Checked when the server starts: a query or declared request name beginning
591
+ with `lb-`.
562
592
 
563
- Not checked at build time: that an `lb-list` or `lb-row` name matches a
564
- declared query, that an `lb-action` matches a declared action or CRUD entry,
565
- that an `lb-` attribute is one Loadbare defines, that the chrome contains one
566
- `<lb-hub>` and one empty `<main>`. The first two are a roadmap item; the
567
- last two are listed under the builder blockers. A language server is a
593
+ Not checked at build time: that an `lb-query` name matches a declared query,
594
+ that an `lb-request` matches a declared request or `crud` entry, that the
595
+ chrome contains one `<lb-hub>` and one empty `<main>`. The first two are a
596
+ roadmap item; the last is listed under the builder blockers. A language server is a
568
597
  roadmap item.
569
598
 
570
599
  ### Elsewhere
@@ -598,9 +627,10 @@ Nothing is scoped, renamed, added or removed.
598
627
  with a tag is a roadmap item.
599
628
  - Light DOM means every selector reaches every element, in the chrome and in
600
629
  widgets.
601
- - The hub stamps attributes a stylesheet can select on: `lb-value`,
602
- `lb-pending`, `lb-error`, `lb-row-count`. An empty list is styled with
603
- `[lb-row-count="0"]`.
630
+ - The hub stamps attributes a stylesheet can select on: `lb-column-value`,
631
+ `lb-key-value`, `lb-request-pending`, `lb-request-error`,
632
+ `lb-query-row-count`. An empty `rows` query is styled with
633
+ `[lb-query-row-count="0"]`.
604
634
 
605
635
  ### Elsewhere
606
636
 
@@ -638,8 +668,9 @@ app.get(/.*/, (_req, res) => res.sendFile(path.join(DIST, "app.html")));
638
668
 
639
669
  These lines do not change as pages are added.
640
670
 
641
- `contextFor(req)` returns `ctx`, which Loadbare passes to every query and
642
- request and never reads. Its type is declared by the application through
671
+ `contextFor(req, parms)` returns `ctx`, which Loadbare passes to every query
672
+ and handler and never reads. `parms` are the query parms of the URL the
673
+ browser is showing. Its type is declared by the application through
643
674
  TypeScript declaration merging on `HubContext`. Authentication, sessions,
644
675
  logging, database access, and any ORM or business-logic layer are the
645
676
  application's, placed in Express middleware and in `ctx`.
@@ -648,10 +679,10 @@ A page's server half:
648
679
 
649
680
  | File | Exports | Contents |
650
681
  |----------------------|------------|--------------------------------------------------------------|
651
- | `<name>.queries.ts` | `queries` | Named queries, each built with `row()` or `list()` |
652
- | `<name>.requests.ts` | `requests` | `onPageEnter`, `actions` by name, `crud` by list name |
682
+ | `<name>.queries.ts` | `queries` | Named queries, each built with `row(key, run)` or `rows(key, run)` |
683
+ | `<name>.requests.ts` | `requests` | `onPageEnter`, `handlers` by request name, `crud` by query name |
653
684
 
654
- A query's signature is `(ctx) => Row` or `(ctx) => Row[]`. An answer whose
685
+ A query's `run` is `(ctx) => Row` or `(ctx) => Row[]`. An answer whose
655
686
  shape disagrees with its declaration is dropped with a warning.
656
687
 
657
688
  The data channel carries no CSRF token. Where an options argument for
@@ -729,22 +760,24 @@ TECHREF-1.0 lists every name Loadbare/app owns in one cross-reference.
729
760
 
730
761
  | Owned | Count | Names |
731
762
  |-------------------------------------|-------|---------------------------------------------------------------|
732
- | `lb-*` HTML attributes | 16 | `lb-list`, `lb-row`, `lb-cell`, `lb-show`, `lb-key`, `lb-key-value`, `lb-value`, `lb-action`, `lb-nav-link`, `lb-pending`, `lb-error`, `lb-row-count`, `lb-unknown-page`, `lb-slot`, `lb-template`, `lb-page` |
733
- | Reserved `lb-action` values | 3 | `lb-row-insert`, `lb-row-update`, `lb-row-delete` |
763
+ | `lb-*` attributes a developer writes | 7 | `lb-query`, `lb-column`, `lb-show`, `lb-request`, `lb-url-link`, `lb-url-push`, `lb-url-unknown` |
764
+ | `lb-*` stamps | 7 | `lb-column-value`, `lb-key-value`, `lb-query-row-count`, `lb-request-pending`, `lb-request-error`, `lb-page`, `lb-page-title` |
765
+ | `lb-*` build-time attributes | 2 | `lb-exp-slot`, `lb-exp-template` |
766
+ | Requests Loadbare provides | 3 | `lb-row-insert`, `lb-row-update`, `lb-row-delete` |
734
767
  | `lb*` methods on custom elements | 2 | `lbPlaceRow`, `lbRowsLanded` |
735
768
  | Attribute namespace for expansion | 1 | `exp-*` |
736
769
  | Reserved tags | 1 | `<lb-hub>` |
737
770
  | Reserved events | 1 | `lb-request` |
738
- | Synthetic rows | 1 | `lb-navigation` |
771
+ | Queries the hub serves | 1 | `lb-url`, with columns `lb-path`, `lb-page-label`, `lb-page-unknown` |
739
772
  | Builder flags | 4 | `--src`, `--out`, `--watch`, `--minify` |
740
773
  | Reserved file names and patterns | 8 | `chrome.html`, `imports.ts`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `<tag>.html`, `<tag>.browser.ts`, `*.css` |
741
774
  | Reserved `package.json` keys | 1 | `loadbare.widgets` |
742
- | Server functions | 5 | `row`, `list`, `patch`, `createHub`, `hubRoutes` |
743
- | Browser exports | 2 | `applyRow`, `applyData` |
775
+ | Server functions | 6 | `row`, `rows`, `patch`, `url`, `createHub`, `hubRoutes` |
776
+ | Browser exports | 2 | `applyRow`, `applyResponse` |
744
777
 
745
778
  The technical reference, TECHREF-1.0.md, is 1,181 lines. Theory states that
746
779
  the vocabulary fits in an LLM's context window, and discloses that Anthropic
747
- models writing `.requests.ts` files consistently refresh a list query after
780
+ models writing `.requests.ts` files consistently refresh a `rows` query after
748
781
  returning a patch for it.
749
782
 
750
783
  The other projects in the comparison set do not publish a comparable
@@ -757,12 +790,12 @@ cross-reference, so no count is given for them here.
757
790
  - Pages and widgets are native HTML in light DOM. Loadbare/app adds no ARIA
758
791
  roles and no elements of its own except `<lb-hub>`, `<template>` for
759
792
  absent branches, and the custom elements an application writes.
760
- - `aria-busy="true"` is set and removed with `lb-pending`.
793
+ - `aria-busy="true"` is set and removed with `lb-request-pending`.
761
794
  - An element hidden by `lb-show` is inside template content, so it is not in
762
795
  the accessibility tree.
763
796
  - A widget's markup is in the document and in view-source.
764
- - On navigation the hub does not move focus, announce the new page, or change
765
- `document.title`.
797
+ - On navigation the hub sets `document.title` from the page's `<title>`. It
798
+ does not move focus or announce the new page.
766
799
  - Checkboxes and radio buttons are not implemented for landing or gathering.
767
800
 
768
801
  ### Elsewhere
@@ -808,7 +841,7 @@ The documentation describes no testing approach for applications.
808
841
 
809
842
  ### Loadbare/app
810
843
 
811
- Loadbare/app is at 0.7.3 and has not reached 1.0. TECHREF-1.0 lists items
844
+ Loadbare/app is at 0.9.0 and has not reached 1.0. TECHREF-1.0 lists items
812
845
  to decide before 1.0 because they would be breaking changes afterward.
813
846
  Theory states that 1.0 is scoped so that foreseeable features can be added
814
847
  without breaking existing code.
@@ -853,14 +886,14 @@ Loadbare's history begins with Andromeda in 2003, whose data layer is now
853
886
  Each item is current behavior or an open item named in TECHREF-1.0 or the
854
887
  Roadmap.
855
888
 
856
- | Area | Status in 0.7.3 |
889
+ | Area | Status in the working tree after 0.9.0 |
857
890
  |-------------------------------|------------------------------------------------------------------------|
858
891
  | Nested data | A row never holds rows. Trees are sent as joined rows. |
859
- | Record URLs and view parameters | A URL names a page. Parameters are an open blocker. |
860
- | Query arguments from the browser | None. Selection is server state reached through `ctx`. |
892
+ | Record URLs and view parameters | A path names a page. A record or a filter is a query parm, `/accounts?acct=23`. |
893
+ | Query arguments from the browser | None. Query parms reach queries through `contextFor` and `ctx`. |
861
894
  | Checkboxes, radio buttons, file inputs | Not landed and not gathered. Open blocker. |
862
895
  | Data types and formatting | No conversion. Queries format values. Open blocker. |
863
- | Error messages | `lb-error` carries no message. |
896
+ | Error messages | `lb-request-error` carries no message. |
864
897
  | Field validation | No mechanism. Per-keystroke validation is a roadmap question. |
865
898
  | Optimistic updates | None. |
866
899
  | Concurrent writers | No version or conflict check. Roadmap question. |
@@ -870,7 +903,7 @@ Roadmap.
870
903
  | Subpath hosting | Not possible. The hub uses absolute paths. |
871
904
  | Chrome-level queries | A widget in the chrome bound to a query requires every page to declare it. Open blocker. |
872
905
  | A widget receiving a whole row | Not available. Open blocker. |
873
- | A nested list per outer row | A nested list receives the same rows in every outer row. A new outer row's nested list stays empty until its name lands again. Open blocker. |
906
+ | A nested `rows` query per outer row | A nested query receives the same rows in every outer row. A new outer row's nested query stays empty until its name lands again. Open blocker. |
874
907
  | Configurable request deadline | Fixed at ten seconds. |
875
908
  | Internationalization | Roadmap item. |
876
909
  | Lazy page loading | Roadmap item. |
@@ -893,14 +926,14 @@ one set of accidental complexity for another, and that the system is
893
926
  The table lists requirements an application may have, and whether each
894
927
  approach provides them without additional libraries.
895
928
 
896
- | Requirement | Loadbare/app 0.7.3 | Client component + API | Meta-frameworks | Hypermedia | Server-driven stateful |
929
+ | Requirement | Loadbare/app 0.9 | Client component + API | Meta-frameworks | Hypermedia | Server-driven stateful |
897
930
  |-----------------------------------------------|--------------------|------------------------|-----------------|------------|------------------------|
898
931
  | Pages bound to relational query results | yes | application code | application code | server templates | server templates |
899
932
  | No endpoint or route design | yes | no | partly (actions, loaders) | no | partly (routes, events) |
900
933
  | No client-side state management | yes | no | no | yes | yes |
901
934
  | No build step | no | no | no | yes | depends on stack |
902
935
  | Server-rendered HTML for crawlers | no | no | yes | yes | yes |
903
- | Route parameters and record URLs | no | yes | yes | yes | yes |
936
+ | Route parameters and record URLs | query parms only | yes | yes | yes | yes |
904
937
  | Optimistic updates | no | yes | yes | no | partly |
905
938
  | Offline or local-first operation | no | with libraries | with libraries | no | no |
906
939
  | Real-time server push | no | with libraries | with libraries | extensions, Turbo Streams over WebSocket | yes |
@@ -921,7 +954,7 @@ approach provides them without additional libraries.
921
954
  | Components | Light-DOM custom elements | Framework components | Framework components | Server partials; Stimulus controllers | Server components |
922
955
  | Wire | One POST endpoint, JSON rows | Application-designed | Framework-defined | HTML over HTTP | WebSocket or snapshot POSTs |
923
956
  | Mutation refresh | Declared `refresh` names; patches | Application code or query invalidation | Revalidation | Response HTML | Re-render |
924
- | Routing | Flat page names; no parameters | Router library | File-based, with parameters and layouts | Server routes | Server routes |
957
+ | Routing | Flat page names; query parms | Router library | File-based, with parameters and layouts | Server routes | Server routes |
925
958
  | Build | `loadbare-app-build`, esbuild | Vite and others | Vite, Turbopack, others | None required | Stack-specific |
926
959
  | CSS | Concatenated, unscoped | Varies | Varies, scoped options | None | None built in |
927
960
  | Server | Application's Express app | Application's API | Framework server or adapters | Any | Phoenix, Laravel, ASP.NET Core |
@@ -936,18 +969,19 @@ row, so the invoice is refreshed.
936
969
 
937
970
  ```html
938
971
  <!-- invoice.page.html -->
939
- <section lb-row="invoice">
940
- <h2 lb-cell="number"></h2>
941
- <span lb-cell="customer"></span>
972
+ <title>Invoice</title>
973
+ <section lb-query="invoice">
974
+ <h2 lb-column="number"></h2>
975
+ <span lb-column="customer"></span>
942
976
  </section>
943
977
 
944
978
  <table>
945
- <tbody lb-list="invoiceLines">
946
- <template lb-key="id">
979
+ <tbody lb-query="invoiceLines">
980
+ <template>
947
981
  <tr>
948
- <td lb-cell="item"></td>
949
- <td lb-cell="amount"></td>
950
- <td><button lb-action="lb-row-delete">Remove</button></td>
982
+ <td lb-column="item"></td>
983
+ <td lb-column="amount"></td>
984
+ <td><button lb-request="lb-row-delete">Remove</button></td>
951
985
  </tr>
952
986
  </template>
953
987
  </tbody>
@@ -956,11 +990,11 @@ row, so the invoice is refreshed.
956
990
 
957
991
  ```ts
958
992
  // invoice.queries.ts
959
- import { list, row, type Queries } from "@loadbare/app/server";
993
+ import { row, rows, type Queries } from "@loadbare/app/server";
960
994
 
961
995
  export const queries: Queries = {
962
- invoice: row((ctx) => ctx.db.currentInvoice()),
963
- invoiceLines: list((ctx) => ctx.db.currentInvoiceLines()),
996
+ invoice: row("id", (ctx) => ctx.db.invoice(ctx.invoiceId)),
997
+ invoiceLines: rows("id", (ctx) => ctx.db.invoiceLines(ctx.invoiceId)),
964
998
  };
965
999
  ```
966
1000
 
@@ -972,9 +1006,9 @@ export const requests: Requests = {
972
1006
  crud: {
973
1007
  invoiceLines: {
974
1008
  rowDelete: {
975
- run: async (ctx, where) => {
976
- await ctx.db.deleteLine(where.key);
977
- return { invoiceLines: patch({ drop: [where.key] }) };
1009
+ run: async (ctx, { key }) => {
1010
+ await ctx.db.deleteLine(key);
1011
+ return { invoiceLines: patch({ drop: [key] }) };
978
1012
  },
979
1013
  refresh: ["invoice"],
980
1014
  },
@@ -983,11 +1017,13 @@ export const requests: Requests = {
983
1017
  };
984
1018
  ```
985
1019
 
986
- Which invoice is current is held on the server and reached through `ctx`.
987
- The button carries `lb-pending` and `aria-busy` while the request is in
988
- flight. The request sent is
989
- `{ "action": "lb-row-delete", "list": "invoiceLines", "key": "42" }`. The
990
- response carries the refreshed `invoice` row and a patch dropping key `42`.
1020
+ Which invoice is current is a query parm, `/invoice?id=7`, which
1021
+ `contextFor` puts on `ctx` as `invoiceId`. The button carries
1022
+ `lb-request-pending` and `aria-busy` while the request is in flight. The
1023
+ request sent is
1024
+ `{ "name": "lb-row-delete", "query": "invoiceLines", "key": "42" }`. The
1025
+ response carries two response items: the refreshed `invoice` row and a
1026
+ patch dropping key `42`.
991
1027
 
992
1028
  ### Next.js (App Router)
993
1029
 
@@ -1097,28 +1133,29 @@ Each equivalent is approximate.
1097
1133
  | Hub | Client runtime; router plus data layer |
1098
1134
  | Chrome (`chrome.html`) | Root layout (`app/layout.tsx`, `+layout.svelte`); application shell |
1099
1135
  | Page (`<name>.page.html`) | Route component; page template |
1100
- | Query (`row()`, `list()`) | Loader (React Router 7); load function (SvelteKit); data read in a Server Component |
1136
+ | Query (`row()`, `rows()`) | Loader (React Router 7); load function (SvelteKit); data read in a Server Component |
1101
1137
  | `onPageEnter` | LiveView `mount`; code at the top of a loader |
1102
- | Declared action | Route action (React Router 7); form action (SvelteKit); Server Function; LiveView `handle_event` |
1138
+ | Declared request (`handlers`) | Route action (React Router 7); form action (SvelteKit); Server Function; LiveView `handle_event` |
1103
1139
  | `crud` operation | Resource controller action (Rails); REST verb on a row |
1104
1140
  | `refresh` | Revalidation; `invalidateQueries`; `revalidatePath` |
1105
1141
  | `patch({ rows, drop })` | Keyed partial update; Turbo Stream `append`/`replace`/`remove`; LiveView `stream_insert`/`stream_delete` |
1106
1142
  | Landing | Render and commit; swap; patch |
1107
- | `lb-list`, `lb-row`, `lb-cell` | IE4 `DATASRC` and `DATAFLD`; `v-for` and `{{ }}`; `x-for` and `x-text` |
1108
- | `lb-key` | `key` (React, Vue); `track` (Angular); keyed `each` (Svelte) |
1143
+ | `lb-query`, `lb-column` | IE4 `DATASRC` and `DATAFLD`; `v-for` and `{{ }}`; `x-for` and `x-text` |
1144
+ | Key declared by `row()`/`rows()` | `key` (React, Vue); `track` (Angular); keyed `each` (Svelte) |
1109
1145
  | `lb-show` | `v-if`, `@if`, `{#if}`, `x-if` |
1110
- | `lb-value` | A prop; an observed attribute |
1111
- | `lb-action` | `hx-post`; `phx-click`; `wire:click`; `<form action>` |
1146
+ | `lb-column-value` | A prop; an observed attribute |
1147
+ | `lb-request` | `hx-post`; `phx-click`; `wire:click`; `<form action>` |
1112
1148
  | `lb-request` event | `htmx:beforeRequest`; a component `emit` |
1113
- | `lb-pending` | `htmx-request` class; `phx-click-loading`; `useFormStatus().pending`; `wire:loading` |
1114
- | `lb-error` | `htmx:responseError`; an error boundary |
1115
- | `lb-row-count` | An empty-state conditional in a template |
1116
- | `lb-nav-link` | `<Link>`; `hx-boost`; Turbo Drive |
1117
- | `lb-navigation` | `useLocation`; `page.url` (SvelteKit); `$route` (Vue Router) |
1149
+ | `lb-request-pending` | `htmx-request` class; `phx-click-loading`; `useFormStatus().pending`; `wire:loading` |
1150
+ | `lb-request-error` | `htmx:responseError`; an error boundary |
1151
+ | `lb-query-row-count` | An empty-state conditional in a template |
1152
+ | `lb-url-link` | `<Link>`; `hx-boost`; Turbo Drive |
1153
+ | `lb-url` | `useLocation` and `useSearchParams`; `page.url` (SvelteKit); `$route` (Vue Router) |
1154
+ | `url()` from a handler | A redirect after POST; `redirect()` in a Server Function |
1118
1155
  | Widget | Component; custom element |
1119
1156
  | Expansion | Server-side include; partial; build-time component render (Astro) |
1120
1157
  | `exp-` parameter | A prop fixed at build time |
1121
- | `lb-slot`, `lb-template` | Default slot and named slots; `ng-content` with `select` |
1158
+ | `lb-exp-slot`, `lb-exp-template` | Default slot and named slots; `ng-content` with `select` |
1122
1159
  | `lbPlaceRow`, `lbRowsLanded` | Custom list rendering; a render prop |
1123
1160
  | `ctx` / `HubContext` | `event.locals` (SvelteKit); loader `context` (React Router 7); request-scoped dependency injection |
1124
1161
  | `imports.ts` | Installing and registering a component library |