@loadbare/app 0.9.0 → 0.11.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 (82) hide show
  1. package/README.md +3 -3
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +107 -90
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/build/expand.d.ts +6 -1
  6. package/dist/build/expand.d.ts.map +1 -1
  7. package/dist/build/expand.js +112 -26
  8. package/dist/build/expand.js.map +1 -1
  9. package/dist/build/locations.d.ts +2 -3
  10. package/dist/build/locations.d.ts.map +1 -1
  11. package/dist/build/locations.js +2 -3
  12. package/dist/build/locations.js.map +1 -1
  13. package/dist/build/pages.d.ts +3 -4
  14. package/dist/build/pages.d.ts.map +1 -1
  15. package/dist/build/pages.js +3 -4
  16. package/dist/build/pages.js.map +1 -1
  17. package/dist/core/lb-constants.d.ts +27 -24
  18. package/dist/core/lb-constants.d.ts.map +1 -1
  19. package/dist/core/lb-constants.js +103 -168
  20. package/dist/core/lb-constants.js.map +1 -1
  21. package/dist/core/lb-types.d.ts +64 -77
  22. package/dist/core/lb-types.d.ts.map +1 -1
  23. package/dist/core/lb-types.js +40 -7
  24. package/dist/core/lb-types.js.map +1 -1
  25. package/dist/hub/lb-apply.d.ts +47 -37
  26. package/dist/hub/lb-apply.d.ts.map +1 -1
  27. package/dist/hub/lb-apply.js +195 -199
  28. package/dist/hub/lb-apply.js.map +1 -1
  29. package/dist/hub/lb-hub.browser.d.ts +1 -1
  30. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  31. package/dist/hub/lb-hub.browser.js +410 -449
  32. package/dist/hub/lb-hub.browser.js.map +1 -1
  33. package/dist/server/lb-express.d.ts +5 -5
  34. package/dist/server/lb-express.d.ts.map +1 -1
  35. package/dist/server/lb-express.js +35 -66
  36. package/dist/server/lb-express.js.map +1 -1
  37. package/dist/server/lb-server.d.ts +77 -135
  38. package/dist/server/lb-server.d.ts.map +1 -1
  39. package/dist/server/lb-server.js +132 -79
  40. package/dist/server/lb-server.js.map +1 -1
  41. package/docs/TECHREF-1.0.md +908 -585
  42. package/docs/comparison.md +243 -185
  43. package/docs/prior-art.md +15 -14
  44. package/docs/reference/builder.md +9 -3
  45. package/docs/reference/chrome.md +107 -56
  46. package/docs/reference/custom-elements.md +291 -173
  47. package/docs/reference/data-binding.md +381 -374
  48. package/docs/reference/overview.md +12 -10
  49. package/docs/reference/page-files.md +164 -99
  50. package/docs/reference/server.md +2 -2
  51. package/docs/reference/widgets.md +104 -110
  52. package/docs/roadmap.md +32 -39
  53. package/docs/terms-of-art.md +57 -0
  54. package/docs/testing.md +97 -68
  55. package/docs/theory.md +92 -58
  56. package/docs/tutorials/010-pages-and-navigation.md +20 -12
  57. package/docs/tutorials/020-css.md +6 -3
  58. package/docs/tutorials/030-html-decomposition.md +9 -7
  59. package/docs/tutorials/040-displaying-data.md +30 -13
  60. package/docs/tutorials/{050-actions.md → 050-requests.md} +25 -15
  61. package/docs/tutorials/060-custom-element-code.md +17 -16
  62. package/docs/tutorials/065-conditional-rendering.md +34 -23
  63. package/docs/tutorials/070-displaying-a-list.md +29 -21
  64. package/docs/tutorials/072-inserting-into-a-list.md +24 -16
  65. package/docs/tutorials/074-deleting-from-a-list.md +9 -7
  66. package/docs/tutorials/076-updating-a-list-item.md +11 -10
  67. package/docs/tutorials/080-widget-requests.md +71 -43
  68. package/docs/tutorials/090-using-widget-libraries.md +22 -22
  69. package/docs/what-does-loadbare-extend.md +124 -0
  70. package/package.json +1 -1
  71. package/skills/loadbare-app/SKILL.md +201 -123
  72. package/skills/loadbare-app/references/TECHREF-1.0.md +908 -585
  73. package/skills/loadbare-app/references/builder.md +9 -3
  74. package/skills/loadbare-app/references/chrome.md +107 -56
  75. package/skills/loadbare-app/references/custom-elements.md +291 -173
  76. package/skills/loadbare-app/references/data-binding.md +381 -374
  77. package/skills/loadbare-app/references/overview.md +12 -10
  78. package/skills/loadbare-app/references/page-files.md +164 -99
  79. package/skills/loadbare-app/references/server.md +2 -2
  80. package/skills/loadbare-app/references/widgets.md +104 -110
  81. package/docs/analysis-accidental-complexity.md +0 -149
  82. package/docs/analysis-closed-set.md +0 -210
@@ -1,13 +1,12 @@
1
1
  # The Basic Widget Library
2
2
 
3
- Seven widgets: `lb-input`, `lb-select`, `lb-list`, `lb-options`, `lb-table`,
4
- `lb-picker`, `lb-unknown-page`. Every one of them is written against the same two contracts
5
- documented elsewhere — [Custom Elements](./custom-elements.md#html) for its
6
- definition, [Custom Elements](./custom-elements.md#code) for its class —
7
- nothing here is special-cased machinery.
3
+ Six widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`, `lb-table`,
4
+ `lb-unknown-page`. Every one of them is an ordinary custom element, written
5
+ against the contracts in [Custom Elements](./custom-elements.md#html) for its
6
+ definition and [Custom Elements](./custom-elements.md#code) for its class.
8
7
 
9
8
  They ship compiled, in `@loadbare/widgets`, a package the builder resolves the
10
- way it resolves anyone else's — it declares `"loadbare": { "widgets": "./dist" }`
9
+ way it resolves any other: it declares `"loadbare": { "widgets": "./dist" }`
11
10
  and the builder scans that. Install it and list it:
12
11
 
13
12
  ```
@@ -23,140 +22,133 @@ See [Widgets from packages](./builder.md#widgets-from-packages) for what
23
22
  listing a package does, and [The Builder](./builder.md#where-the-builder-looks)
24
23
  for where a listed package sits in the cascade.
25
24
 
25
+ `lb-input`, `lb-select`, `lb-options` and `lb-picker` are controls:
26
+ form-associated custom elements with a `value` property that fire `change`.
27
+ Each carries `lb-column` and `lb-request` itself, the way an `<input>` does;
28
+ see [Being a control](./custom-elements.md#being-a-control).
29
+
26
30
  ## `lb-input`
27
31
 
28
- Wraps an `<input>`. `lb-value` sets the input's `.value`. The widget sends
29
- nothing on its own: `lb-action` names what the input's `change` sends. The
30
- reserved `lb-row-update` saves the input's own cell; any other name sends that
31
- action. Either carries the input's value, and the hub adds the scope the
32
- input sits in.
32
+ Wraps an `<input>`. The hub sets its `value` from the column `lb-column`
33
+ names, and `lb-request` names what the widget sends on `change`:
33
34
 
34
- An input inside an `lb-row-insert` or `lb-row-update` form leaves the attribute off.
35
- The form reads every `lb-cell` in it on submit and sends one request for all
36
- of them, so an input that also sent its own would write the same edit twice.
35
+ ```html
36
+ <lb-input lb-column="name" lb-request="lb-row-update" exp-label="Name"></lb-input>
37
+ ```
37
38
 
38
- | Parameter | Fills |
39
- | ---------- | ----- |
40
- | `exp-label` | the visible `<label>` text |
41
- | `exp-readonly` | the input's `readonly` attribute |
39
+ Inside a form, leave `lb-request` off. The form gathers the widget on submit
40
+ like any other control, so a widget that also sent its own request would
41
+ write the same edit twice.
42
42
 
43
- | Attribute | Asks for |
44
- | ---------- | ----- |
45
- | `lb-action` | what to send on `change`; `lb-row-update` to save the cell's own edit |
43
+ | Parameter | Fills |
44
+ |----------------|----------------------------------|
45
+ | `exp-label` | The visible `<label>` text |
46
+ | `exp-readonly` | The input's `readonly` attribute |
46
47
 
47
48
  ## `lb-select`
48
49
 
49
- Wraps a `<select>` whose `<option>`s the author writes directly inside (via
50
- `lb-slot`). `lb-value` sets the select's `.value`; a `change` sends the
51
- action named by `lb-action`, with the select's `.value` as the request's
52
- `value` — the choice is the interaction, so this is the case where an
53
- action carries a value.
50
+ Wraps a `<select>` whose `<option>`s the page writes inside the tag:
54
51
 
55
- | Parameter | Fills |
56
- | ---------- | ----- |
57
- | `exp-label` | the visible `<label>` text |
52
+ ```html
53
+ <lb-select lb-column="team" lb-request="lb-row-update" exp-label="Team">
54
+ <option value="">No team</option>
55
+ <option value="Engines">Engines</option>
56
+ </lb-select>
57
+ ```
58
58
 
59
- Requires `lb-action` or `lb-query-parm` — a change with neither logs and
60
- sends nothing. With `lb-query-parm` the hub writes the choice into the URL.
59
+ | Parameter | Fills |
60
+ |-------------|----------------------------|
61
+ | `exp-label` | The visible `<label>` text |
61
62
 
62
63
  ## `lb-options`
63
64
 
64
- A `<select>` whose `<option>`s come from a query instead of being written by
65
- hand. The author supplies the row template inside the widget (via
66
- `lb-slot`), same as any list widget:
65
+ A `<select>` whose `<option>`s are the rows of the query it names. The page
66
+ writes the row template inside the widget:
67
67
 
68
68
  ```html
69
- <lb-options lb-list="statuses" exp-label="Status" lb-action="setStatus">
70
- <template lb-key="id" data-group="category">
71
- <option lb-cell="label"></option>
69
+ <lb-options lb-query="statuses" lb-column="status" lb-request="lb-row-update" exp-label="Status">
70
+ <template data-group="category">
71
+ <option lb-column="label"></option>
72
72
  </template>
73
73
  </lb-options>
74
74
  ```
75
75
 
76
- | Parameter | Fills |
77
- | ---------- | ----- |
78
- | `exp-label` | the visible `<label>` text |
76
+ | Parameter | Fills |
77
+ |-------------|----------------------------|
78
+ | `exp-label` | The visible `<label>` text |
79
79
 
80
- - The row's key becomes the option's `value`: the `lb-key-value` the hub
81
- stamps on each option is copied across, so the page names the key column
82
- once, with `lb-key`.
80
+ - Each option's `value` is its row's key, the `lb-key-value` the hub stamps
81
+ on the live row, so the widget holds a key and shows a label.
83
82
  - `data-group` on the row template sections the options into `<optgroup>`s,
84
- one per distinct value, created and removed as rows arrive and leave.
85
- - `lb-value` selects the option with that key, including one that arrives
86
- after the value did.
87
- - A `change` sends the action named by `lb-action`, value from the
88
- select's `.value`. With `lb-query-parm` instead, the hub writes the
89
- choice into the URL.
83
+ one per distinct value of that column, added and removed as rows arrive
84
+ and leave.
85
+ - Its `value` selects the option with that key, including an option that
86
+ arrives after the value did.
87
+
88
+ `lb-picker` is the same class with the row template supplied by its
89
+ definition.
90
+
91
+ ## `lb-picker`
92
+
93
+ `lb-options`, with the row template built in, for a list where every row is
94
+ one option showing one column:
95
+
96
+ ```html
97
+ <lb-picker
98
+ lb-query="statuses"
99
+ lb-column="status"
100
+ lb-request="lb-row-update"
101
+ exp-label="Status"
102
+ exp-column="label"
103
+ exp-group="category"
104
+ ></lb-picker>
105
+ ```
90
106
 
91
- `lb-picker` is this same class with its row template supplied by the
92
- definition instead of the page — see below.
107
+ | Parameter | Fills |
108
+ |--------------|-----------------------------------------|
109
+ | `exp-label` | The visible `<label>` text |
110
+ | `exp-column` | The column each option shows |
111
+ | `exp-group` | The row template's `data-group` |
112
+
113
+ An option built from two columns, or a row with a second element, is
114
+ `lb-options` with the page's own row template.
93
115
 
94
116
  ## `lb-table`
95
117
 
96
- A `<table>` that supplies its own scaffolding; the author supplies the
97
- heading row, the row template, and optionally a footer, each as a
98
- `<template>` matched to a destination:
118
+ A `<table>` that supplies its own scaffolding. The page supplies the heading
119
+ row, the row template, and optionally a footer, each as a `<template>`:
99
120
 
100
121
  ```html
101
- <lb-table lb-list="ledger" exp-caption="Ledger">
102
- <template lb-template="head">
122
+ <lb-table lb-query="ledger" exp-caption="Ledger">
123
+ <template lb-exp-template="head">
103
124
  <tr><th>Date</th><th>Amount</th></tr>
104
125
  </template>
105
- <template lb-key="id" data-sort="date" data-group="month">
106
- <tr><td lb-cell="date"></td><td lb-cell="amount"></td></tr>
126
+ <template data-sort="date" data-group="month">
127
+ <tr><td lb-column="date"></td><td lb-column="amount"></td></tr>
107
128
  </template>
108
- <template lb-template="foot">
109
- <tr lb-row="ledger-total"><td>Total</td><td lb-cell="total"></td></tr>
129
+ <template lb-exp-template="foot">
130
+ <tr lb-query="ledger-total"><td>Total</td><td lb-column="total"></td></tr>
110
131
  </template>
111
132
  </lb-table>
112
133
  ```
113
134
 
114
- | Parameter | Fills |
115
- | ---------- | ----- |
116
- | `exp-caption` | the `<caption>` text |
117
-
118
- | Destination | Fills |
119
- | ------------ | ----- |
120
- | `head` (`lb-template="head"`) | the `<thead>` content |
121
- | `foot` (`lb-template="foot"`) | the `<tfoot>` content |
122
- | slot (no `lb-template`) | the row template, via `lb-slot` on `<tbody>` |
123
-
124
- - `data-group` on the row template sections rows under a derived heading row,
125
- one per distinct value, whose `colSpan` matches the row's own column
126
- count. `data-sort` orders rows within a section (or the whole body, with no
127
- grouping) by comparing each row's cell text.
128
- - The `foot` destination is not delivered through `lbPlaceRow` — it's an
129
- ordinary scope carrying its own `lb-row`, resolved by name like any
130
- other on the page. A grand total is a second query over the same
131
- data, not a row the hub hands the table.
132
-
133
- ## `lb-picker`
134
-
135
- `lb-options`, with the row template supplied by the definition instead of
136
- the page — for when every row is one option and nothing else varies:
137
-
138
- ```html
139
- <lb-picker
140
- lb-list="statuses"
141
- exp-label="Status"
142
- exp-key="id"
143
- exp-cell="label"
144
- exp-group="category"
145
- lb-action="setStatus"
146
- ></lb-picker>
147
- ```
135
+ | Parameter | Fills |
136
+ |---------------|----------------------|
137
+ | `exp-caption` | The `<caption>` text |
148
138
 
149
- | Parameter | Fills |
150
- | ---------- | ----- |
151
- | `exp-label` | the visible `<label>` text |
152
- | `exp-key` | the row template's `lb-key` |
153
- | `exp-cell` | the option's `lb-cell` |
154
- | `exp-group` | the row template's `data-group` |
139
+ | Template | Fills |
140
+ |-----------------------------------|-----------------------------------------|
141
+ | `<template lb-exp-template="head">` | The `<thead>` |
142
+ | `<template lb-exp-template="foot">` | The `<tfoot>` |
143
+ | Any other `<template>` | The row template, in the `<tbody>` |
155
144
 
156
- Behavior — grouping, key-as-value, the action on change — is inherited
157
- whole from `lb-options`; a page author who needs a second element in the
158
- row, or an option built from two columns, writes `lb-options` and its own
159
- `<template>` instead.
145
+ - `data-group` on the row template sections rows under a heading row, one
146
+ per distinct value of that column, spanning every column of the row.
147
+ - `data-sort` orders rows within a section, or within the whole body with no
148
+ grouping, by the text of that column.
149
+ - The footer is not a row of `ledger`. Its `<tr>` names a `row` query of its
150
+ own, which lands on the `<tr>` itself. A grand total is a second query over
151
+ the same data.
160
152
 
161
153
  ## `lb-unknown-page`
162
154
 
@@ -170,9 +162,11 @@ The chrome's dialog for a URL that names no page, as one tag:
170
162
  </lb-hub>
171
163
  ```
172
164
 
173
- It expands to a `<dialog lb-unknown-page>` scoped to the hub's own
174
- `lb-navigation` query, so `page-label` and `page-uri` land in it the way any
175
- cell lands anywhere, and the hub opens it on a miss. It takes no parameters
176
- and, so far, shows both cells rather than choosing between them. See
177
- [`chrome.html`](./chrome.md#where-the-page-is) for the query, and for the same
178
- dialog written by hand.
165
+ It expands to a `<dialog lb-url-unknown lb-query="lb-url">` showing
166
+ `lb-path`, and the hub opens it when no page's stub matches the path. See
167
+ [An unknown page](./chrome.md#an-unknown-page) for the same dialog written
168
+ by hand.
169
+
170
+ | Parameter | Fills |
171
+ |-------------------|------------------------------------------|
172
+ | `exp-button-text` | The text of the button that closes it, `OK` by default |
@@ -1,149 +0,0 @@
1
- # Assessing the accidental complexity claim
2
-
3
- [Theory](./theory.md) claims that Loadbare/app carries less accidental
4
- complexity than React, Angular, or the hypermedia libraries. This document
5
- tests that claim against [TECHREF-1.0](./TECHREF-1.0.md), which is the
6
- authoritative statement of what 1.0 means.
7
-
8
- Written 2026-09-07, against `@loadbare/app` 0.6.0.
9
-
10
- Brooks separates the difficulty of the user's problem from the work the tool
11
- demands. The second kind is accidental, and the test applied here is whether
12
- Loadbare removes such work or relocates it somewhere the author still pays
13
- for it.
14
-
15
- ## Where the claim holds
16
-
17
- The whole owned surface fits in the technical reference's cross-reference
18
- section.
19
-
20
- | Owned | Count |
21
- |------------------|-------|
22
- | `lb-*` attributes | 15 |
23
- | `lb*` methods | 3 |
24
- | Builder flags | 4 |
25
- | Reserved filenames| 8 |
26
- | Reserved tags | 1 |
27
- | Reserved events | 1 |
28
-
29
- React reaches that size before an application adds a router, a data layer, or
30
- a bundler configuration, and each of those carries a surface of its own.
31
- Loadbare/app asks for no configuration file today.
32
-
33
- The server half is the strongest part of the argument. A page is markup, a
34
- queries file, and a requests file. The application designs no endpoints,
35
- writes no route table, and picks no serialization contract. Four lines of
36
- Express carry the data channel. Neither the fat frameworks nor the
37
- hypermedia libraries remove that category of work; htmx in particular leaves
38
- the developer designing every endpoint and every fragment it answers with.
39
-
40
- Building the HTML once removes reconciliation, keys, memoization, and effect
41
- dependencies together. That is the largest single deletion in the design,
42
- and it is the one the hypermedia libraries do not make either, since they
43
- ship markup at run time and carry swap semantics to place it.
44
-
45
- ## What the technical reference already knows
46
-
47
- Most of what an assessment finds is already on the blocker list. Each
48
- concern below adds weight to an open item rather than naming a new one.
49
-
50
- | Concern | Blocker |
51
- |--------------------------------------------|--------------------------------------------|
52
- | Values carry no type, so every application formats its own dates and money | Data types |
53
- | No page is reachable by row | Take a position on the URL space |
54
- | A widget receives one cell at a time | Whether a widget may receive a whole row |
55
- | A widget repeats hub code to fire a request| Give a widget a way to fire its own request|
56
- | Master-detail is unspecified | Decide what a nested list means |
57
- | Every page redeclares the chrome's queries | Give the chrome a way to state its own queries |
58
-
59
- The technical reference's own sample query calls `String()` on a count by
60
- hand, which is the data-type blocker showing up in the documentation.
61
-
62
- Two [roadmap](./roadmap.md) items carry the same weight as these and appear
63
- in the first real application. Concurrent writers on one list have no
64
- version or conflict story. Per-keystroke validation has no home, and the
65
- roadmap expects it to sit in the widget while the server stays authoritative
66
- for the same field.
67
-
68
- Five sections of the technical reference are still marked `UNEDITED`:
69
- Binding, Requests, Links, Widgets, and Widget authoring. Those are the
70
- mechanisms an author touches on every page.
71
-
72
- ## Positions that carry a cost
73
-
74
- Two constraints appear in the body of the technical reference as current
75
- behavior rather than on the blocker list, so the project has taken a position
76
- on each. Naming the cost is still fair.
77
-
78
- The hub reaches its endpoint by absolute path, so an application cannot be
79
- hosted under a subpath such as `example.com/myapp/`. A deployment that wants
80
- several applications behind one host gives each one an origin.
81
-
82
- Every route answers 200 with the same document, so the browser detects an
83
- unknown page after the fact and the chrome's `lb-unknown-page` dialog reports
84
- it. A crawler or a monitor that reads status codes sees a healthy response
85
- for a path the application does not have.
86
-
87
- ## The gap recorded nowhere
88
-
89
- Loadbare/app offers no way to display or style an element according to the
90
- value that landed in it.
91
-
92
- A cell lands on a custom element as the `lb-value` attribute, which a
93
- stylesheet can select. A cell lands on a native element as its
94
- `textContent`, which no selector reaches. The hub stamps `lb-pending`,
95
- `lb-error`, and `lb-row-count`, which cover a request in flight, a request
96
- that failed, and an empty list. Nothing covers a row whose `status` column
97
- reads `overdue`.
98
-
99
- An application that wants this writes a widget, and a widget that fires a
100
- request pays the cost the widget-protocol blocker already names. So the
101
- missing piece pushes the author toward the mechanism that is itself
102
- unfinished.
103
-
104
- This appears in neither the blockers, the roadmap's open questions, nor the
105
- decided-against section. It is the one finding here that the technical
106
- reference does not already record.
107
-
108
- ## The general form of the query gap
109
-
110
- The URL-space blocker names one consequence of a broader constraint, and
111
- stating the constraint directly is more useful than stating the consequence.
112
-
113
- A query takes no argument from the browser. Its signature is `(ctx) => Row`
114
- or `(ctx) => Row[]`, and `ctx` is what the application built from the Express
115
- request. The hub's own request carries `page=<name>`. The hub reads
116
- `location.pathname`, which drops the query string, so a path segment and a
117
- search parameter are both invisible to the server.
118
-
119
- An application that shows one selected record therefore holds the selection
120
- in server state. A click fires an action, the handler records the selection
121
- where `ctx` reaches it, and the refreshed query reads it back. That works
122
- today and needs no new mechanism. It puts selection in the same territory as
123
- the roadmap's concurrent-writers question, and it means a reload or a shared
124
- link does not carry the record.
125
-
126
- ## Comparison with the hypermedia libraries
127
-
128
- Theory rejects the existing tools for combining interpolation with
129
- conditional and list rendering. The distinction is narrower than that.
130
- Loadbare/app interpolates at build time, with defaults and a substitution
131
- grammar, and renders lists at run time through `<template lb-key>`. What the
132
- design rules out is conditionals and interpolation after the build.
133
-
134
- Loadbare/app also adds a builder, a filename grammar, and a slot and template
135
- system, where htmx asks for no build step. Against React and Angular the
136
- surface comparison is decisive. Against htmx it is close, and the server
137
- half is where Loadbare/app wins instead.
138
-
139
- ## Verdict
140
-
141
- The claim holds on the server, and it holds on the client for everything the
142
- reconciliation layer used to cost.
143
-
144
- On the rest of the client it currently holds partly by not doing several
145
- things database applications need, and the technical reference lists most of
146
- them itself. Whether the claim survives 1.0 depends on how the URL space,
147
- the row hook, the chrome queries, and value-driven display are answered, and
148
- an answer of "decided against" counts as an answer only where an application
149
- can still reach the behavior some other way.
@@ -1,210 +0,0 @@
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 one cell from 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
- ### Done: `lb-cell-change` folded into `lb-row-update`
79
-
80
- A cell change is an update whose `values` holds one entry. SQL has one
81
- UPDATE whether it sets one column or many. On the server, `cellChange` and
82
- `rowUpdate` merged into `rowUpdate(key, values)`, where a column absent from
83
- `values` is left as it is.
84
-
85
- The fold needed one rule beyond this proposal. Without it, a widget sending
86
- `lb-row-update` gathers its whole record, and every other editable cell in
87
- the row goes with it. The rule: an element carrying `lb-cell` is a record
88
- of one cell, the way a control has a value and a form has values. Its
89
- `values` holds that cell alone, from the `value` the widget sent or else its
90
- control, so a widget never writes its own column name into the request.
91
-
92
- ### Reordering is not an operation
93
-
94
- Position is not a relational concept; order is a column. A drag is an update
95
- to an ordering column such as `display_order`, which the wire already
96
- carries in `values`. No move operation is needed, and no field for "where
97
- the row went".
98
-
99
- ## 4. Position: taken from the page, never written in the request
100
-
101
- - An element's own attributes say what it displays. Its ancestors say where
102
- it belongs.
103
- - A nested scope begins a new scope.
104
- - A request is an action plus a position. The hub supplies the position;
105
- only an interaction supplies a value.
106
-
107
- These are the rules [Theory](./theory.md) already states. They are listed
108
- here because closure depends on them.
109
-
110
- ## 5. Landing: every combination defined
111
-
112
- | Result → target | list scope | row scope | no scope yet |
113
- | --------------- | ------------ | --------------------------------- | -------------------------- |
114
- | whole list | reconcile | error: one name has one shape | stored |
115
- | patch | upsert, drop | error | applied to the stored copy |
116
- | row | error | fill | stored |
117
-
118
- A result lands on every scope bound to its name, at any depth of nesting.
119
- 0.7.3 already does this: a patch to `groups` reaches a picker in every row of
120
- an outer table, and the outer list's reconciliation does not mistake the
121
- picker's rows for its own.
122
-
123
- ### The missing rule: timing
124
-
125
- In 0.7.3 a scope that appears after its name has landed stays empty. A row
126
- added to an outer list by a patch starts with every nested list empty, and a
127
- widget's scaffolding built during landing, such as a ghost row holding a
128
- picker, starts empty too. Pages work around it by ordering their queries so
129
- the outer list lands first, which only covers the first load.
130
-
131
- The closed rule:
132
-
133
- > **The hub holds each name's current value. A scope shows that value
134
- > whenever it appears.**
135
-
136
- In relational terms a name is a relation variable, results are assignments to
137
- it, and scopes are views of it. Whether a scope existed before or after its
138
- data arrived stops mattering.
139
-
140
- It also settles several things that were not worked on directly:
141
-
142
- - The blocker in [TECHREF-1.0](./TECHREF-1.0.md), "Decide how a new row
143
- fills a nested list".
144
- - Pickers in newly created rows, and pickers in ghost rows built after the
145
- first load.
146
- - Query order in a page's queries file stops mattering.
147
- - **What a partial patch row means.** A patch row upserts by key, and a
148
- column it does not name is left unchanged, as an SQL UPDATE leaves it.
149
- The reference is silent on this today.
150
- - "No scope for a name" becomes an ordinary state rather than a console
151
- warning, since a scope may simply not exist yet.
152
-
153
- #### Mechanism
154
-
155
- - The hub fills a cloned row, nested scopes included, before inserting it.
156
- This extends the rule it already follows of filling before insertion.
157
- - After each landing, it sweeps for scopes that have never received data.
158
- For a list, an absent `lb-row-count` already marks one. A row scope would
159
- need an equivalent mark.
160
- - A widget that builds scopes outside a landing, at some later time, would
161
- need a `MutationObserver`. No widget built so far does that.
162
-
163
- #### What would prove it wrong
164
-
165
- A case where the stored copy and the document legitimately disagree. The
166
- only candidate is a control holding an edit not yet sent, and landing already
167
- overwrites that in 0.7.3, so the rule introduces nothing new.
168
-
169
- ## 6. State attributes: four, stamped by the hub, read by CSS
170
-
171
- `lb-value`, `lb-row-count`, `lb-pending`, `lb-error`.
172
-
173
- ## 7. Outside the algebra, deliberately bounded
174
-
175
- - **Composition at build time:** `lb-slot`, `lb-template`.
176
- - **The host channel:** `lb-page`, `lb-nav-link`, `lb-unknown-page`, and the
177
- hub's own `lb-navigation` row. That row already shows the hub's own state
178
- landing through the same three forms.
179
- - **The widget protocol:** the `lb-request` event, which an ancestor may
180
- stop, and the `lbPlaceRow` and `lbRowsLanded` hooks.
181
-
182
- ## What this changes in 0.7.3
183
-
184
- 1. The hub holds each name's current value and fills scopes that appear
185
- late.
186
- 2. `lb-cell-change` folds into `lb-row-update`, with an element carrying
187
- `lb-cell` sending itself alone, and the server's `cellChange` and
188
- `rowUpdate` merge. Done.
189
- 3. The reference states the patch row rule: upsert by key, unnamed columns
190
- unchanged.
191
-
192
- Everything else is the current vocabulary, now justified as closed.
193
-
194
- ## What the evidence has not settled
195
-
196
- Everything above is backed by pages already built. These are not:
197
-
198
- 1. **Parameters and view state:** a filter, a selection, a collapsed
199
- section. The likely closed answer is that URL query parameters become
200
- columns of the `lb-navigation` row and reach queries as arguments, as
201
- `<form method="get">` puts its fields in the query string. No page needs
202
- it yet, so this is a prediction rather than evidence.
203
- 2. **Checkboxes and data types:** probably HTML's presence-or-absence
204
- convention, still undecided.
205
- 3. **Concurrent writers:** staleness is a property of the stored copy, so
206
- the rule in section 5 makes this question easier to state without
207
- answering it.
208
- 4. **Error content:** `lb-error` records that a round trip failed, not why.
209
- A closed answer would deliver the message as a result like any other,
210
- perhaps as a hub row. Untested.