@loadbare/app 0.7.2 → 0.7.4

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.
@@ -10,8 +10,8 @@ to be true, then revise the code, docs, and tests to ensure it is true.
10
10
  We cannot declare 1.0 until we determine if these blockers can be
11
11
  added later w/o breaking changes. If we are reasonably confident
12
12
  that they can be decided later w/o breaking changes, then we rename
13
- them to "Out of Scope". Otherwise, they must be resolved before
14
- we declare 1.0.
13
+ them to "Out of Scope" or move them to the roadmap. Otherwise,
14
+ they must be resolved before we declare 1.0.
15
15
 
16
16
  ### Data types
17
17
 
@@ -59,6 +59,15 @@ That decision is firm; what the set contains is not.
59
59
 
60
60
  ### Lists
61
61
 
62
+ Imagine we have an HTML `<select>` that displays a fixed list for all
63
+ rows in a table, such as `customer_type` for a table of customers. When
64
+ all customers can be any customer type, our system as written today
65
+ is fine, because all HTML `<select>` elements get the same list.
66
+
67
+ But it may be that the list of allowed values is dependent on other values
68
+ in the row.
69
+
70
+
62
71
  - **Decide how a new row fills a nested list.** A row added to the outer
63
72
  list starts with an empty nested list, which fills only when the nested
64
73
  list's name lands again. See [Master-detail](#master-detail) for what a
@@ -210,7 +219,7 @@ the whole of `--src` is one flat namespace, as stated in
210
219
  Three kinds of file hold HTML.
211
220
 
212
221
  | File | Holds | Found in |
213
- |--------------------|--------------------------------------|--------------|
222
+ | ------------------ | ------------------------------------ | ------------ |
214
223
  | `chrome.html` | The one HTML document | `--src` |
215
224
  | `<stub>.page.html` | One page's markup, as a fragment | `--src` |
216
225
  | `<tag-name>.html` | One widget definition, as a fragment | Every origin |
@@ -277,13 +286,13 @@ A definition states a default with `{{name|default}}`, as in
277
286
  `{{button-label|OK}}`. Whitespace around the name and around the default is
278
287
  discarded. The default is a literal.
279
288
 
280
- | The attribute is assigned | Expansion defines | Result |
281
- |---------------------------|--------------------|----------------------------------|
282
- | `exp-name="text"` | `{{name}}` | `text` |
283
- | `exp-name=""` | `{{name}}` | An empty string |
284
- | Nothing | `{{name|default}}` | `default` |
285
- | Nothing | `{{name}}` | Nothing, and the attribute drops |
286
- | `exp-name` | No `{{name}}` | A build error |
289
+ | The attribute is assigned | Expansion defines | Result |
290
+ | ------------------------- | ------------------- | -------------------------------- |
291
+ | `exp-name="text"` | `{{name}}` | `text` |
292
+ | `exp-name=""` | `{{name}}` | An empty string |
293
+ | Nothing | `{{name\|default}}` | `default` |
294
+ | Nothing | `{{name}}` | Nothing, and the attribute drops |
295
+ | `exp-name` | No `{{name}}` | A build error |
287
296
 
288
297
  Attributes without the `exp-` prefix are ignored during expansion.
289
298
 
@@ -370,20 +379,19 @@ These are build errors:
370
379
 
371
380
  ### Binding
372
381
 
373
- ---- UNEDITED ----
374
-
375
382
  Binding attributes determine how the hub updates the DOM, either by
376
383
  directly updating a plain HTML element or instructing
377
384
  custom widgets to update themselves.
378
385
 
379
386
 
380
- | Attribute | Assigned By | Behavior |
381
- |--------------|-------------|-------------------------------------------------------------------------------------------|
382
- | lb-list | Developer | Scopes DOM children to a named set of rows; a nested lb-list or lb-row begins a new scope |
383
- | lb-row | Developer | Scopes DOM children to one named row; a nested lb-list or lb-row begins a new scope |
384
- | lb-cell | Developer | This DOM node displays this column of the row in scope |
385
- | lb-key | Developer | Names the column that identifies a row, on the row template inside an lb-list |
386
- | lb-key-value | Hub | Stamped on a live row: that row's value of `lb-key` |
387
+ | Attribute | Assigned By | Behavior |
388
+ | ------------ | ----------- | ----------------------------------------------------------------------------------------------- |
389
+ | lb-list | Developer | Scopes DOM children to a named set of rows; a nested lb-list or lb-row begins a new scope |
390
+ | lb-row | Developer | Scopes DOM children to one named row; a nested lb-list or lb-row begins a new scope |
391
+ | lb-cell | Developer | This DOM node displays this column of the row in scope |
392
+ | lb-show | Developer | This DOM node is present when this column of the row in scope is neither null nor false |
393
+ | lb-key | Developer | Names the column that identifies a row, on the row template inside an lb-list |
394
+ | lb-key-value | Hub | Stamped on a live row: that row's value of `lb-key` |
387
395
  | lb-value | Hub | The value that landed on a cell; a widget updates itself from it and a stylesheet selects on it |
388
396
 
389
397
  #### How a value lands
@@ -391,11 +399,11 @@ custom widgets to update themselves.
391
399
  A cell lands one of three ways, and the element decides which. Every cell
392
400
  that receives a value also carries it as `lb-value`.
393
401
 
394
- | Element | Receives the value as |
395
- |-----------------------------------------------------|--------------------------------------|
396
- | A custom element, tag hyphenated | Its `lb-value` attribute |
397
- | `<select>`, `<textarea>`, or `<input>` | Its `value`, and `lb-value` |
398
- | Any other native element | Its `textContent`, and `lb-value` |
402
+ | Element | Receives the value as |
403
+ | -------------------------------------- | --------------------------------- |
404
+ | A custom element, tag hyphenated | Its `lb-value` attribute |
405
+ | `<select>`, `<textarea>`, or `<input>` | Its `value`, and `lb-value` |
406
+ | Any other native element | Its `textContent`, and `lb-value` |
399
407
 
400
408
  A widget owns whatever control it wraps, so it is handed the value and
401
409
  renders it itself. A form control shows its state as its `value`, so a
@@ -433,30 +441,44 @@ A widget sees `attributeChangedCallback` for an `lb-value` already present
433
441
  when it upgrades, so a widget cannot tell a first landing from a refresh,
434
442
  and does not need to.
435
443
 
436
- #### Displaying by value
444
+ #### Displaying by condition
437
445
 
438
- A stylesheet selects on `lb-value` to show or hide part of a page according
439
- to a value that landed. A column the page does not display is bound to a
440
- hidden element.
446
+ `lb-show` names the column that decides whether an element is present.
447
+ `null` and `false` are off, and every other value is on. No string is read,
448
+ so `"false"` is on, and a query spells a condition as a boolean or a null.
441
449
 
442
450
  ```html
443
451
  <template lb-key="id">
444
452
  <tr>
445
453
  <td lb-cell="name"></td>
446
- <td lb-cell="locked" hidden></td>
447
- <td><button lb-action="lb-row-delete">Remove</button></td>
454
+ <td><button lb-action="lb-row-delete" lb-show="removable">Remove</button></td>
448
455
  </tr>
449
456
  </template>
450
457
  ```
451
458
 
452
- ```css
453
- tr:has([lb-cell="locked"][lb-value="true"]) button { display: none; }
454
- ```
459
+ It binds as `lb-cell` does, to the row on the nearest scoped ancestor, and on
460
+ an element that is itself a scope the column belongs to the row around it. A row
461
+ that does not carry the column leaves the element as it is.
455
462
 
456
- `lb-value` holds the string the query sent, so the query decides the spelling
457
- a selector matches. A hidden control is out of reach, keyboard included; a
458
- stylesheet cannot disable one, which is a widget's job. Hiding is
459
- presentation, and the server still refuses what a request may not do.
463
+ An element that is off is moved into a `<template lb-show="column">` standing
464
+ where it stood, and moved back out when the column turns on. It is not
465
+ rendered, focused, clicked, announced or gathered. It is moved and never
466
+ rebuilt, so a widget keeps its instance. Landing reaches into that template,
467
+ and nothing else does, so the element and every cell and scope inside it
468
+ return current. A custom element sees `disconnectedCallback` then
469
+ `adoptedCallback` going in, and `adoptedCallback` then `connectedCallback`
470
+ coming out, and still receives `lb-value` while it is away.
471
+
472
+ The builder ships every `lb-show` element already inside its template, so
473
+ nothing conditional shows until its row lands. An absent element's template
474
+ keeps its place among its siblings, and a position selector counts it.
475
+
476
+ These are build errors: `lb-show` on a row template's root, with no row
477
+ around it (outside every scope, on a scope with none around it, or in a list
478
+ scope outside its row template), or on a `<template>`.
479
+
480
+ Hiding is presentation, and the server still refuses what a request may not
481
+ do.
460
482
 
461
483
  #### Master-detail
462
484
 
@@ -504,10 +526,10 @@ name, the same test that decides how a value lands — see
504
526
  [How a value lands](#how-a-value-lands).
505
527
 
506
528
  | lb-action | Written on |
507
- |----------------|---------------------------------------------------|
508
- | lb-row-insert | a `<form>`, or a button by its cells, in a list |
529
+ | -------------- | ------------------------------------------------- |
530
+ | lb-row-insert | a `<form>`, or a button in a `<tr>`, in a list |
509
531
  | lb-row-delete | anything inside a live row |
510
- | lb-row-update | a `<form>`, or a button by its cells, in a row |
532
+ | lb-row-update | a `<form>`, or a button in a live row |
511
533
  | lb-cell-change | a widget wrapping one control |
512
534
  | anything else | must be a named routine in the page's server code |
513
535
 
@@ -520,13 +542,13 @@ dispatched it. It reads the scope from the dispatching element before any
520
542
  ancestor sees the event, and never overwrites a field the request already
521
543
  carries.
522
544
 
523
- | `action` | Filled from scope | Required |
524
- |------------------|--------------------------------|----------------------------|
525
- | a declared name | `list` or `row`, `key`, `cell` | nothing |
526
- | `lb-cell-change` | `list`, `key`, `cell` | all three, and `value` |
527
- | `lb-row-insert` | `list` | `list`, and `values` |
528
- | `lb-row-delete` | `list`, `key` | both |
529
- | `lb-row-update` | `list`, `key` | both, and `values` |
545
+ | `action` | Filled from scope | Required |
546
+ | ---------------- | ------------------------------ | ---------------------- |
547
+ | a declared name | `list` or `row`, `key`, `cell` | nothing |
548
+ | `lb-cell-change` | `list`, `key`, `cell` | all three, and `value` |
549
+ | `lb-row-insert` | `list` | `list`, and `values` |
550
+ | `lb-row-delete` | `list`, `key` | both |
551
+ | `lb-row-update` | `list`, `key` | both, and `values` |
530
552
 
531
553
  An element's own `lb-list` or `lb-row` names what it displays, never where
532
554
  its request goes. A request belongs to the scope around the element, the
@@ -534,14 +556,21 @@ way a control belongs to the form around it. `list` or `row` comes from the
534
556
  nearest ancestor scope, `key` from the nearest live row inside that scope,
535
557
  and `cell` from the dispatching element's own `lb-cell`.
536
558
  A request missing a required field is not sent. The hub never fills
537
- `value`; for `lb-row-insert` and `lb-row-update`, `values` is gathered from
538
- the nearest element holding an `lb-cell` with a control to read, starting at
539
- the dispatching element and walking up. A `<form>` holds its own cells; a
540
- button in a `<tr>` for a new row, which cannot be a form, reads the row
541
- around it. The walk stops at the live row the element sits in and never
542
- reaches the scope, whose other cells belong to other rows. A request that
543
- already carries `values` keeps them, and one with no cell to read is not
544
- sent. A click inside a native element that carries either operation and
559
+ `value`. An `lb-row-insert` or `lb-row-update` gathers the row it belongs
560
+ to: `values` holds every `lb-cell` with a control to read in the nearest
561
+ `<form>`, `<tr>` or live row around the dispatching element, itself
562
+ included, inside its scope. The cells are found the way a row lands, so a
563
+ cell inside a scope nested in the row is that scope's and is not gathered,
564
+ while an element carrying a scope and `lb-cell` both is the row's cell.
565
+ This is a button's form owner: what else sits
566
+ beside the element never changes what is sent. A `<tr>` counts because a
567
+ form cannot go around a table row's controls, so a new row in a table is its
568
+ own form; a live row counts because it is the row the key names. The scope
569
+ itself is never the row, since its cells belong to other rows. A request
570
+ from an element in no form or row is not sent, and the hub reports that its
571
+ cells belong in a `<form>`. A request that already carries `values` keeps
572
+ them, and one whose row has no cell to read is not sent. A click inside a
573
+ native element that carries either operation and
545
574
  holds cells itself is not sent, since clicking into one of its controls
546
575
  would send the row: put the action on a form or on a button.
547
576
 
@@ -617,7 +646,7 @@ other link, so leaving the application is the default and staying in it is
617
646
  the opt-in.
618
647
 
619
648
  | Attribute | Assigned By | Behavior |
620
- |-------------|-------------|-----------------------------------------------------------------------------------------|
649
+ | ----------- | ----------- | --------------------------------------------------------------------------------------- |
621
650
  | lb-nav-link | Developer | On an `<a>`: the hub shows the page the anchor's path names, without loading a document |
622
651
 
623
652
  The attribute takes no value. The hub looks for the nearest ancestor
@@ -670,7 +699,7 @@ Current page, published by the hub as a row. Can be bound anywhere just
670
699
  like a server-produced result.
671
700
 
672
701
  | Cell | Holds |
673
- |--------------|---------------------------------------------------------------------|
702
+ | ------------ | ------------------------------------------------------------------- |
674
703
  | `page-label` | The text of the link to the current page, empty if no link names it |
675
704
  | `page-uri` | The path as the browser has it, such as `/members` |
676
705
 
@@ -792,7 +821,7 @@ client script, the stylesheet, the hub's data channel, and the one HTML
792
821
  document.
793
822
 
794
823
  | Handle | With |
795
- |------------------------------|---------------------|
824
+ | ---------------------------- | ------------------- |
796
825
  | `/client.js` | `dist/client.js` |
797
826
  | `/app.css` | `dist/app.css` |
798
827
  | `hubRoutes(hub, contextFor)` | The hub's own route |
@@ -881,7 +910,7 @@ as explained in the next section.
881
910
  It has three optional keys.
882
911
 
883
912
  | Key | Keyed by | Answers |
884
- |---------------|-----------------------|----------------------------------------------------|
913
+ | ------------- | --------------------- | -------------------------------------------------- |
885
914
  | `actions` | The `lb-action` value | Anything the page chooses to declare |
886
915
  | `crud` | A query name | The four reserved `lb-action` values |
887
916
  | `onPageEnter` | Nothing | Runs once on entering the page, before its queries |
@@ -938,7 +967,7 @@ are one vocabulary. All four operate on a list, because each needs a key and
938
967
  a key exists only on a live row.
939
968
 
940
969
  | `lb-action` | Key under `crud` | `where` carries |
941
- |------------------|------------------|------------------------|
970
+ | ---------------- | ---------------- | ---------------------- |
942
971
  | `lb-cell-change` | `cellChange` | `key`, `cell`, `value` |
943
972
  | `lb-row-delete` | `rowDelete` | `key` |
944
973
  | `lb-row-insert` | `rowInsert` | `values` |
@@ -981,21 +1010,31 @@ The hub stamps these on the element that dispatched a request, which is the
981
1010
  widget itself when a widget fired it. A widget observes them and reacts; it
982
1011
  must name them in `observedAttributes` to see them change.
983
1012
 
984
- | Attribute | Written on | Holds |
985
- |----------------|-------------------------|-------------------------------------------------|
986
- | `lb-pending` | The dispatching element | The round trip is in flight |
1013
+ | Attribute | Written on | Holds |
1014
+ | -------------- | ----------------------- | --------------------------------------------------------------------------------------- |
1015
+ | `lb-pending` | The dispatching element | The round trip is in flight |
987
1016
  | `lb-error` | The dispatching element | The last round trip failed, cleared on the next — see [The round trip](#the-round-trip) |
988
- | `lb-row-count` | A list scope | How many rows the scope is showing |
1017
+ | `lb-row-count` | A list scope | How many rows the scope is showing |
1018
+
1019
+ They are also stamped on plain HTML, where a stylesheet is the only
1020
+ consumer: dim a pending button, mark a failed one, and style an empty list
1021
+ against `lb-row-count` rather than carrying an empty-state element.
1022
+
1023
+ The hub reads one of them. A native `lb-action` element, a button or a
1024
+ form, that is performed again while it carries `lb-pending` is ignored: the
1025
+ click or submit is cancelled and nothing is sent. A pending native action is
1026
+ therefore disabled in fact, and a stylesheet only has to show it. A widget is
1027
+ not held back this way, because a widget that sends on change must have its
1028
+ latest value sent rather than dropped.
989
1029
 
990
- The hub writes all three and never reads them. They are also stamped on
991
- plain HTML, where a stylesheet is the only consumer: dim a pending button,
992
- mark a failed one, and style an empty list against `lb-row-count` rather than
993
- carrying an empty-state element.
1030
+ Alongside `lb-pending` the hub sets `aria-busy="true"` on the same element and
1031
+ removes it when the round trip settles, so assistive technology hears the
1032
+ state a stylesheet shows.
994
1033
 
995
1034
  #### Row hooks
996
1035
 
997
1036
  | Name | Implemented By | Behavior |
998
- |---------------------------------|----------------|-------------------------------------------------------------|
1037
+ | ------------------------------- | -------------- | ----------------------------------------------------------- |
999
1038
  | `applyRow(root, row)` | Loadbare | Fills one scope from one row |
1000
1039
  | `lbPlaceRow(el, row, template)` | Developer | Optional on a list scope: where a row goes |
1001
1040
  | `lbRowsLanded()` | Developer | Optional on a list scope: scaffolding derived from the rows |
@@ -1020,7 +1059,7 @@ that defines it. The definition lives there and only there.
1020
1059
  Loadbare/app owns every flag in this table.
1021
1060
 
1022
1061
  | Flag | Default | Names | Defined in |
1023
- |------------|---------|-------------------------------------------|---------------------------------------------------|
1062
+ | ---------- | ------- | ----------------------------------------- | ------------------------------------------------- |
1024
1063
  | `--src` | `src` | The application's own tree, scanned whole | [What the builder scans](#what-the-builder-scans) |
1025
1064
  | `--out` | `dist` | Where the builder writes | [Running the builder](#running-the-builder) |
1026
1065
  | `--watch` | off | Rebuild on change under `--src` | [Running the builder](#running-the-builder) |
@@ -1033,7 +1072,7 @@ named carries its meaning wherever it sits in the `--src` tree, so an
1033
1072
  application must not use one of these names for anything else.
1034
1073
 
1035
1074
  | File | How many | Holds | Defined in |
1036
- |-------------------------|------------------------|----------------------------------------------------------|---------------------------------------------------------|
1075
+ | ----------------------- | ---------------------- | -------------------------------------------------------- | ------------------------------------------------------- |
1037
1076
  | `chrome.html` | Exactly one | The application's one HTML document | [Chrome](#chrome) |
1038
1077
  | `imports.ts` | Zero or one | Default-exports an array of widget library package names | [Imported widget libraries](#imported-widget-libraries) |
1039
1078
  | `<stub>.page.html` | One per page | One page's markup, as a fragment | [Pages](#pages) |
@@ -1051,7 +1090,7 @@ only when some origin has a stylesheet, `pages.ts` only when some page has a
1051
1090
  `.queries.ts` or a `.requests.ts`.
1052
1091
 
1053
1092
  | Path | Written | Holds | Read by | Defined in |
1054
- |--------------------|-------------------|--------------------------------------------------------------------|------------|---------------------------------------------|
1093
+ | ------------------ | ----------------- | ------------------------------------------------------------------ | ---------- | ------------------------------------------- |
1055
1094
  | `/app.html` | Always | The chrome, built | The server | [Chrome](#chrome) |
1056
1095
  | `/client.js` | Always | `<lb-hub>` and every widget the application uses | The chrome | [Chrome](#chrome) |
1057
1096
  | `/client-entry.ts` | Always | The generated entry `client.js` is bundled from | Nothing | [Running the builder](#running-the-builder) |
@@ -1064,7 +1103,7 @@ Loadbare/app owns every key in this table. A widget library declares them.
1064
1103
  An application never does.
1065
1104
 
1066
1105
  | Key | Value | Means | Defined in |
1067
- |--------------------|---------------------------|------------------------------------------------------------------------------|---------------------------------------------------------|
1106
+ | ------------------ | ------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- |
1068
1107
  | `loadbare.widgets` | A path inside the package | Where this package's widgets are. Absent means the whole installed directory | [Imported widget libraries](#imported-widget-libraries) |
1069
1108
 
1070
1109
  ### Reserved namespaces
@@ -1072,11 +1111,11 @@ An application never does.
1072
1111
  Loadbare/app owns every namespace in this table. Ownership of a name and
1073
1112
  ownership of its behavior are stated separately, because they differ.
1074
1113
 
1075
- | Namespace | Applies to | Loadbare owns | Defined in |
1076
- |-----------|----------------------------|----------------------------------------------|---------------------------------------------------|
1077
- | `lb-*` | HTML attributes | The names and their behavior | [The lb-* namespace](#the-lb--namespace) |
1078
- | `exp-*` | HTML attributes | The behavior; widget authors pick the values | [Build time parameters](#build-time-parameters) |
1079
- | `lb*` | Methods on custom elements | The names and their behavior | [Row hooks](#row-hooks) |
1114
+ | Namespace | Applies to | Loadbare owns | Defined in |
1115
+ | --------- | -------------------------- | -------------------------------------------- | ----------------------------------------------- |
1116
+ | `lb-*` | HTML attributes | The names and their behavior | [The lb-* namespace](#the-lb--namespace) |
1117
+ | `exp-*` | HTML attributes | The behavior; widget authors pick the values | [Build time parameters](#build-time-parameters) |
1118
+ | `lb*` | Methods on custom elements | The names and their behavior | [Row hooks](#row-hooks) |
1080
1119
 
1081
1120
  #### The lb-* namespace
1082
1121
 
@@ -1090,23 +1129,24 @@ actions on the wire — see [Page Queries](#page-queries).
1090
1129
 
1091
1130
  Each attribute is defined in one section, and this table says which.
1092
1131
 
1093
- | Attribute | Written by | Defined in |
1094
- |------------------|-------------|---------------------------------------------------|
1095
- | `lb-list` | Developer | [Binding](#binding) |
1096
- | `lb-row` | Developer | [Binding](#binding) |
1097
- | `lb-cell` | Developer | [Binding](#binding) |
1098
- | `lb-key` | Developer | [Binding](#binding) |
1099
- | `lb-key-value` | Hub | [Binding](#binding) |
1100
- | `lb-value` | Hub | [How a value lands](#how-a-value-lands) |
1101
- | `lb-action` | Developer | [Requests](#requests) |
1102
- | `lb-nav-link` | Developer | [Links](#links) |
1103
- | `lb-pending` | Hub | [Request state](#request-state) |
1104
- | `lb-error` | Hub | [Request state](#request-state) |
1105
- | `lb-row-count` | Hub | [Request state](#request-state) |
1106
- | `lb-unknown-page`| Developer | [Chrome](#chrome) |
1107
- | `lb-slot` | Widget author | [Slots and templates](#slots-and-templates) |
1108
- | `lb-template` | Widget author | [Slots and templates](#slots-and-templates) |
1109
- | `lb-page` | Builder | [Pages](#pages) |
1132
+ | Attribute | Written by | Defined in |
1133
+ | ----------------- | ------------- | --------------------------------------------------- |
1134
+ | `lb-list` | Developer | [Binding](#binding) |
1135
+ | `lb-row` | Developer | [Binding](#binding) |
1136
+ | `lb-cell` | Developer | [Binding](#binding) |
1137
+ | `lb-show` | Developer | [Displaying by condition](#displaying-by-condition) |
1138
+ | `lb-key` | Developer | [Binding](#binding) |
1139
+ | `lb-key-value` | Hub | [Binding](#binding) |
1140
+ | `lb-value` | Hub | [How a value lands](#how-a-value-lands) |
1141
+ | `lb-action` | Developer | [Requests](#requests) |
1142
+ | `lb-nav-link` | Developer | [Links](#links) |
1143
+ | `lb-pending` | Hub | [Request state](#request-state) |
1144
+ | `lb-error` | Hub | [Request state](#request-state) |
1145
+ | `lb-row-count` | Hub | [Request state](#request-state) |
1146
+ | `lb-unknown-page` | Developer | [Chrome](#chrome) |
1147
+ | `lb-slot` | Widget author | [Slots and templates](#slots-and-templates) |
1148
+ | `lb-template` | Widget author | [Slots and templates](#slots-and-templates) |
1149
+ | `lb-page` | Builder | [Pages](#pages) |
1110
1150
 
1111
1151
  An attribute the builder does not recognize is left alone today. Refusing
1112
1152
  one is on the list of validations still to land — see
@@ -1118,7 +1158,7 @@ Loadbare/app owns every tag in this table. An application writes them and
1118
1158
  never defines them.
1119
1159
 
1120
1160
  | Tag | Written in | Means | Defined in |
1121
- |------------|------------|--------------------------------|-------------------|
1161
+ | ---------- | ---------- | ------------------------------ | ----------------- |
1122
1162
  | `<lb-hub>` | The chrome | The application's live element | [Chrome](#chrome) |
1123
1163
 
1124
1164
  ### Reserved events
@@ -1127,7 +1167,7 @@ Loadbare/app owns every DOM event in this table, both the name and what its
1127
1167
  `detail` carries.
1128
1168
 
1129
1169
  | Event | Dispatched from | Bubbles | Cancelable | Defined in |
1130
- |--------------|------------------------|---------|------------|-----------------------------------------|
1170
+ | ------------ | ---------------------- | ------- | ---------- | --------------------------------------- |
1131
1171
  | `lb-request` | The element that acted | Yes | No | [The request event](#the-request-event) |
1132
1172
 
1133
1173
  ### Reserved attributes
@@ -1136,6 +1176,6 @@ Loadbare/app owns every attribute in this table, both the name and its
1136
1176
  behavior.
1137
1177
 
1138
1178
  | Attribute | Written on | Takes a value | Defined in |
1139
- |-------------------|-------------------------------------|---------------|-------------------|
1179
+ | ----------------- | ----------------------------------- | ------------- | ----------------- |
1140
1180
  | `lb-unknown-page` | A `<dialog>` in chrome | No | [Chrome](#chrome) |
1141
1181
  | `lb-page` | A `<template>`, by the builder only | Yes | [Pages](#pages) |
@@ -0,0 +1,203 @@
1
+ # A closed set for the Loadbare/app vocabulary
2
+
3
+ > **LLM-authored, not yet revised by a person.** Drafted by Claude on
4
+ > 2026-09-13, from a design session against `@loadbare/app` 0.7.3 while
5
+ > building a chart of accounts page. Treat it as a proposal, not as the
6
+ > author's statement.
7
+
8
+ ## The question
9
+
10
+ Relational algebra is closed: every operation takes relations and returns a
11
+ relation. CRUD is closed in the same way on the write side. Closure is what
12
+ makes both easy to program against. Combinations do not explode, the cases
13
+ are finite, and nothing falls outside the model.
14
+
15
+ Loadbare/app maps three data shapes (cell, row, list) and three relational
16
+ operations (insert, update, delete) into HTML. This document asks whether
17
+ recent iterations have shown enough to state a **closed** vocabulary and set
18
+ of behaviors: one where every combination of shape, operation, position,
19
+ nesting and timing is defined, so application code never meets an edge case
20
+ outside the model.
21
+
22
+ Author note: a page can of course define invalid combinations, but their
23
+ lack of validity must be intelligibly reported as a failure to stick to the
24
+ closed set.
25
+
26
+ "Closed" is tested four ways:
27
+
28
+ 1. **Shapes are closed.** Everything a page receives is one of the shapes,
29
+ or a patch on a list.
30
+ 2. **Operations are closed over shapes.** Each operation yields a defined
31
+ result in the same vocabulary.
32
+ 3. **Composition is closed.** Nesting, many scopes bound to one name, scopes
33
+ that appear late, and several names in one response all stay inside the
34
+ rules.
35
+ 4. **Anything outside the spine is justified.** It either reduces to the
36
+ spine or is a separate, deliberately bounded axis.
37
+
38
+ ## The model in one sentence
39
+
40
+ > **A page holds named relations. Every read and every write answers with
41
+ > new values for those names. Every scope is a view of one name.**
42
+
43
+ Every rule below follows from it.
44
+
45
+ ## 1. Shapes: three, plus identity
46
+
47
+ | Shape | Attribute | Relational |
48
+ | -------- | ------------------------------------------- | ----------------------------------------------------------------- |
49
+ | list | `lb-list` | a relation, as a variable that holds rows |
50
+ | row | `lb-row` | a single row, read-only; a writable single row is a list of one |
51
+ | cell | `lb-cell` | a column value |
52
+ | identity | `lb-key` written, `lb-key-value` stamped | the primary key |
53
+
54
+ ## 2. What every answer is made of: three forms
55
+
56
+ Every query, CRUD operation, declared action and refresh answers with
57
+ `{ name: result }`, and each result is one of:
58
+
59
+ - **a whole list** (an array), which replaces the relation, membership and
60
+ order included;
61
+ - **a patch** (`{ rows, drop }`), which upserts rows by key and drops keys;
62
+ - **a row** (an object), which replaces the single row.
63
+
64
+ This is the closure property. No operation answers with HTML, a redirect, or
65
+ instructions. Declared actions are open-ended, like stored procedures, but
66
+ their results are still in these three forms, so the landing side stays
67
+ closed.
68
+
69
+ ## 3. Operations: three, plus one open door
70
+
71
+ | Operation | Takes from position | Carries | Answers |
72
+ | ---------------- | ------------------- | -------------------------------------------- | ------------ |
73
+ | `lb-row-insert` | list | values, gathered from the record | patch rows |
74
+ | `lb-row-update` | list, key | values, gathered or supplied by a widget | patch rows |
75
+ | `lb-row-delete` | list, key | nothing | patch drop |
76
+ | a declared name | list or row, key, cell | value | any names |
77
+
78
+ ### Proposed: fold `lb-cell-change` into `lb-row-update`
79
+
80
+ A cell change is an update whose `values` holds one entry, and the hub
81
+ already keeps `values` a widget supplies. SQL has one UPDATE whether it sets
82
+ one column or many. On the server, `cellChange` and `rowUpdate` merge into
83
+ one `update(key, values)`, where a column absent from `values` is left as it
84
+ is.
85
+
86
+ ### Reordering is not an operation
87
+
88
+ Position is not a relational concept; order is a column. A drag is an update
89
+ to an ordering column such as `display_order`, which the wire already
90
+ carries in `values`. No move operation is needed, and no field for "where
91
+ the row went".
92
+
93
+ ## 4. Position: taken from the page, never written in the request
94
+
95
+ - An element's own attributes say what it displays. Its ancestors say where
96
+ it belongs.
97
+ - A nested scope begins a new scope.
98
+ - A request is an action plus a position. The hub supplies the position;
99
+ only an interaction supplies a value.
100
+
101
+ These are the rules [Theory](./theory.md) already states. They are listed
102
+ here because closure depends on them.
103
+
104
+ ## 5. Landing: every combination defined
105
+
106
+ | Result → target | list scope | row scope | no scope yet |
107
+ | --------------- | ------------ | --------------------------------- | -------------------------- |
108
+ | whole list | reconcile | error: one name has one shape | stored |
109
+ | patch | upsert, drop | error | applied to the stored copy |
110
+ | row | error | fill | stored |
111
+
112
+ A result lands on every scope bound to its name, at any depth of nesting.
113
+ 0.7.3 already does this: a patch to `groups` reaches a picker in every row of
114
+ an outer table, and the outer list's reconciliation does not mistake the
115
+ picker's rows for its own.
116
+
117
+ ### The missing rule: timing
118
+
119
+ In 0.7.3 a scope that appears after its name has landed stays empty. A row
120
+ added to an outer list by a patch starts with every nested list empty, and a
121
+ widget's scaffolding built during landing, such as a ghost row holding a
122
+ picker, starts empty too. Pages work around it by ordering their queries so
123
+ the outer list lands first, which only covers the first load.
124
+
125
+ The closed rule:
126
+
127
+ > **The hub holds each name's current value. A scope shows that value
128
+ > whenever it appears.**
129
+
130
+ In relational terms a name is a relation variable, results are assignments to
131
+ it, and scopes are views of it. Whether a scope existed before or after its
132
+ data arrived stops mattering.
133
+
134
+ It also settles several things that were not worked on directly:
135
+
136
+ - The blocker in [TECHREF-1.0](./TECHREF-1.0.md), "Decide how a new row
137
+ fills a nested list".
138
+ - Pickers in newly created rows, and pickers in ghost rows built after the
139
+ first load.
140
+ - Query order in a page's queries file stops mattering.
141
+ - **What a partial patch row means.** A patch row upserts by key, and a
142
+ column it does not name is left unchanged, as an SQL UPDATE leaves it.
143
+ The reference is silent on this today.
144
+ - "No scope for a name" becomes an ordinary state rather than a console
145
+ warning, since a scope may simply not exist yet.
146
+
147
+ #### Mechanism
148
+
149
+ - The hub fills a cloned row, nested scopes included, before inserting it.
150
+ This extends the rule it already follows of filling before insertion.
151
+ - After each landing, it sweeps for scopes that have never received data.
152
+ For a list, an absent `lb-row-count` already marks one. A row scope would
153
+ need an equivalent mark.
154
+ - A widget that builds scopes outside a landing, at some later time, would
155
+ need a `MutationObserver`. No widget built so far does that.
156
+
157
+ #### What would prove it wrong
158
+
159
+ A case where the stored copy and the document legitimately disagree. The
160
+ only candidate is a control holding an edit not yet sent, and landing already
161
+ overwrites that in 0.7.3, so the rule introduces nothing new.
162
+
163
+ ## 6. State attributes: four, stamped by the hub, read by CSS
164
+
165
+ `lb-value`, `lb-row-count`, `lb-pending`, `lb-error`.
166
+
167
+ ## 7. Outside the algebra, deliberately bounded
168
+
169
+ - **Composition at build time:** `lb-slot`, `lb-template`.
170
+ - **The host channel:** `lb-page`, `lb-nav-link`, `lb-unknown-page`, and the
171
+ hub's own `lb-navigation` row. That row already shows the hub's own state
172
+ landing through the same three forms.
173
+ - **The widget protocol:** the `lb-request` event, which an ancestor may
174
+ stop, and the `lbPlaceRow` and `lbRowsLanded` hooks.
175
+
176
+ ## What this changes in 0.7.3
177
+
178
+ 1. The hub holds each name's current value and fills scopes that appear
179
+ late.
180
+ 2. `lb-cell-change` folds into `lb-row-update`, with widget-supplied
181
+ `values`, and the server's `cellChange` and `rowUpdate` merge.
182
+ 3. The reference states the patch row rule: upsert by key, unnamed columns
183
+ unchanged.
184
+
185
+ Everything else is the current vocabulary, now justified as closed.
186
+
187
+ ## What the evidence has not settled
188
+
189
+ Everything above is backed by pages already built. These are not:
190
+
191
+ 1. **Parameters and view state:** a filter, a selection, a collapsed
192
+ section. The likely closed answer is that URL query parameters become
193
+ columns of the `lb-navigation` row and reach queries as arguments, as
194
+ `<form method="get">` puts its fields in the query string. No page needs
195
+ it yet, so this is a prediction rather than evidence.
196
+ 2. **Checkboxes and data types:** probably HTML's presence-or-absence
197
+ convention, still undecided.
198
+ 3. **Concurrent writers:** staleness is a property of the stored copy, so
199
+ the rule in section 5 makes this question easier to state without
200
+ answering it.
201
+ 4. **Error content:** `lb-error` records that a round trip failed, not why.
202
+ A closed answer would deliver the message as a result like any other,
203
+ perhaps as a hub row. Untested.