@loadbare/app 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +17 -17
  2. package/dist/build/assemble.d.ts +7 -1
  3. package/dist/build/assemble.d.ts.map +1 -1
  4. package/dist/build/assemble.js +81 -11
  5. package/dist/build/assemble.js.map +1 -1
  6. package/dist/build/cli.d.ts +3 -0
  7. package/dist/build/cli.d.ts.map +1 -1
  8. package/dist/build/cli.js +6 -1
  9. package/dist/build/cli.js.map +1 -1
  10. package/dist/build/declared.d.ts +25 -0
  11. package/dist/build/declared.d.ts.map +1 -0
  12. package/dist/build/declared.js +62 -0
  13. package/dist/build/declared.js.map +1 -0
  14. package/dist/build/pages.d.ts +6 -0
  15. package/dist/build/pages.d.ts.map +1 -1
  16. package/dist/build/pages.js +32 -12
  17. package/dist/build/pages.js.map +1 -1
  18. package/dist/core/lb-types.d.ts +6 -3
  19. package/dist/core/lb-types.d.ts.map +1 -1
  20. package/dist/core/lb-types.js.map +1 -1
  21. package/dist/hub/lb-apply.d.ts.map +1 -1
  22. package/dist/hub/lb-apply.js +10 -3
  23. package/dist/hub/lb-apply.js.map +1 -1
  24. package/dist/server/lb-server.d.ts +5 -2
  25. package/dist/server/lb-server.d.ts.map +1 -1
  26. package/dist/server/lb-server.js +12 -3
  27. package/dist/server/lb-server.js.map +1 -1
  28. package/docs/TECHREF-1.0.md +33 -85
  29. package/docs/comparison.md +695 -482
  30. package/docs/departures.md +63 -0
  31. package/docs/miswarts.md +140 -0
  32. package/docs/opinions.md +155 -0
  33. package/docs/reference/builder.md +9 -2
  34. package/docs/reference/chrome.md +2 -1
  35. package/docs/reference/css.md +163 -0
  36. package/docs/reference/custom-elements.md +1 -1
  37. package/docs/reference/data-binding.md +44 -10
  38. package/docs/reference/page-files.md +38 -5
  39. package/docs/reference/widgets.md +24 -2
  40. package/docs/roadmap.md +55 -43
  41. package/docs/testing.md +62 -28
  42. package/docs/theory.md +348 -108
  43. package/package.json +1 -1
  44. package/skills/loadbare-app/SKILL.md +16 -9
  45. package/skills/loadbare-app/references/TECHREF-1.0.md +33 -85
  46. package/skills/loadbare-app/references/builder.md +9 -2
  47. package/skills/loadbare-app/references/chrome.md +2 -1
  48. package/skills/loadbare-app/references/css.md +163 -0
  49. package/skills/loadbare-app/references/custom-elements.md +1 -1
  50. package/skills/loadbare-app/references/data-binding.md +44 -10
  51. package/skills/loadbare-app/references/miswarts.md +140 -0
  52. package/skills/loadbare-app/references/page-files.md +38 -5
  53. package/skills/loadbare-app/references/widgets.md +24 -2
  54. package/docs/possible-ideas.md +0 -314
  55. package/docs/theory-old.md +0 -839
  56. package/docs/what-does-loadbare-extend.md +0 -124
@@ -96,7 +96,7 @@ export const queries: Queries = {
96
96
 
97
97
  | Declared with | Answers with |
98
98
  |----------------------------|-------------------------------------------|
99
- | `row(key, run)` | One row, an object |
99
+ | `row(key, run)` | One row, an object, or `null` for none |
100
100
  | `rows(key, run, options)` | All its rows, an array of objects |
101
101
 
102
102
  `options` are a `rows` query's order and column types. `order` is the
@@ -229,8 +229,9 @@ export const requests: Requests = {
229
229
  };
230
230
  ```
231
231
 
232
- Declare every request the page allows. The server refuses a name the page
233
- does not declare.
232
+ Declare every request the page allows. The builder refuses markup naming a
233
+ request the page does not declare, and the server refuses one that arrives
234
+ anyway; see [The markup checks](./data-binding.md#the-markup-checks).
234
235
 
235
236
  ### crud
236
237
 
@@ -293,8 +294,9 @@ export const requests: Requests = {
293
294
  };
294
295
  ```
295
296
 
296
- Declare every request the query permits. The server refuses a request a
297
- query does not declare, and a query with no `crud` entry permits none.
297
+ Declare every request the query permits. The builder refuses markup sending
298
+ a request a query does not declare, and the server refuses one that arrives
299
+ anyway. A query with no `crud` entry permits none.
298
300
 
299
301
  Write `rowUpdate` to set the columns `values` names and leave every other
300
302
  column as it is. A form or a live row sends every control it gathers, and a
@@ -303,6 +305,36 @@ columns the page may edit.
303
305
 
304
306
  Every value in `values` is a string, as a control's value is.
305
307
 
308
+ Two users writing the same row overlap only on the columns their requests
309
+ carry, and the last write wins. A single control sends its own column, so
310
+ two users editing different columns of a row do not overwrite each other.
311
+ A form sends every control it gathers, so two forms saved over one row
312
+ overlap on all of their columns.
313
+
314
+ A write whose outcome matters states its precondition in its own SQL, and
315
+ refuses when no row was updated:
316
+
317
+ ```ts
318
+ handlers: {
319
+ approve: {
320
+ run: async (ctx, { key }) => {
321
+ const updated = await ctx.db.run(
322
+ "UPDATE invoice SET status = 'approved' WHERE id = $1 AND status = 'pending'",
323
+ [key],
324
+ );
325
+ if (updated === 0) {
326
+ throw refusal("Someone else has already decided this invoice.");
327
+ }
328
+ },
329
+ refresh: ["invoice"],
330
+ },
331
+ },
332
+ ```
333
+
334
+ The request's name says what the user meant, and the precondition follows
335
+ from it, so no earlier value is needed. See
336
+ [Refusing a request](#refusing-a-request).
337
+
306
338
  ### refresh and patch
307
339
 
308
340
  List in `refresh` every query whose whole answer the request changed.
@@ -317,6 +349,7 @@ queries:
317
349
  | Answer | States |
318
350
  |--------------------------|---------------------------------------------|
319
351
  | `{ ... }` | The row of a `row` query |
352
+ | `null` | A `row` query found no row |
320
353
  | `[...]` | All rows of a `rows` query, and their order |
321
354
  | `patch({ rows: [...] })` | These rows arrive or change; the rest stand |
322
355
  | `patch({ drop: [...] })` | These keys are gone; the rest stand |
@@ -1,8 +1,8 @@
1
1
  # The Basic Widget Library
2
2
 
3
- Eleven widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`,
3
+ Thirteen widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`,
4
4
  `lb-table`, `lb-unknown-page`, `lb-confirm`, `lb-form`, `lb-insert-dialog`,
5
- `lb-master` and `lb-checkbox`. Every one of them is an ordinary custom element, written
5
+ `lb-master`, `lb-checkbox`, `lb-number` and `lb-date`. Every one of them is an ordinary custom element, written
6
6
  against the contracts in [Custom Elements](./custom-elements.md#html) for its
7
7
  definition and [Custom Elements](./custom-elements.md#code) for its class.
8
8
 
@@ -258,6 +258,28 @@ checked. The hub sets a native checkbox's `checked` directly.
258
258
  | `exp-label` | The label's text |
259
259
  | `exp-request` | The checkbox's `lb-request`, none when absent |
260
260
 
261
+ ## `lb-number` and `lb-date`
262
+
263
+ A number shown by the browser's `Intl.NumberFormat`, and a date or time
264
+ shown by `Intl.DateTimeFormat`. Neither is a control: `lb-column` goes on
265
+ the tag, and the widget renders the `lb-column-value` the hub stamps; see
266
+ [Receiving a value](./custom-elements.md#receiving-a-value).
267
+
268
+ ```html
269
+ <lb-number lb-column="balance" exp-style="currency" exp-currency="USD"></lb-number>
270
+ <lb-date lb-column="due" exp-date-style="medium"></lb-date>
271
+ ```
272
+
273
+ Each parameter is one of Intl's options in kebab-case, as `exp-currency-sign`
274
+ for `currencySign`, plus `exp-locale`. A value that is no number or date
275
+ shows as it arrived, and a date alone, `YYYY-MM-DD`, shows as that day in
276
+ every time zone. The stamp keeps the raw value, so a stylesheet selects on
277
+ it:
278
+
279
+ ```css
280
+ lb-number[lb-column-value^="-"] { color: var(--negative); }
281
+ ```
282
+
261
283
  ## `lb-unknown-page`
262
284
 
263
285
  The chrome's dialog for a URL that names no page, as one tag:
@@ -1,314 +0,0 @@
1
- # Possible Ideas
2
-
3
- > **LLM-authored, not yet revised by a person.** Drafted by Claude on
4
- > 2026-10-02 against `@loadbare/app` 0.12.0, from a session in ef. Nothing
5
- > here is decided or planned.
6
-
7
- > **Status, 2026-10-04.** The lifecycle in the hub's terms was adopted, on
8
- > custom elements rather than on behaviors: see
9
- > [When its code runs](./reference/custom-elements.md#when-its-code-runs).
10
- > Behavior by attribute, and expansion that replaces the tag, were set aside.
11
- > What follows is kept as the record of how the decision was reached.
12
-
13
- ## Why these are being considered
14
-
15
- A custom element is the platform's one way to attach code to markup, and
16
- widget work keeps running into what that costs.
17
-
18
- - No decomposition. Code that concerns one row has nowhere to live but a
19
- wrapper around the whole table, so `lb-table` owns everything inside it.
20
- That is how page-specific behavior ends up in a shared widget: the COA
21
- page's ghost row per section grew because row-level code could attach
22
- only to the table. It is the overfit the README's test guards against.
23
- - The browser's lifecycle is not the hub's. A moved row is disconnected and
24
- connected, the copy a move leaves behind is constructed, and an `lb-show`
25
- toggle adopts twice, so `connectedCallback` must be written to run again.
26
- - Clutter. The page's source is the widget's dialect of named templates
27
- rather than HTML, and the shipped page keeps a wrapper tag around what it
28
- expanded.
29
- - Plumbing through the wrapper. The tag carrying the code is not the
30
- control: `WrappedControl` forwards `value`, fires `change` again, and needs
31
- `ElementInternals` to join a form. Focus is searched for inside the
32
- wrapper, and `autofocus` on a wrapped `<input>` is lost.
33
- - Content models reach forms too. A `<form>` cannot wrap a `<tr>`, so the
34
- ghost row joins its controls to a hidden form outside the table by the
35
- `form` attribute. A form cannot hold a form, so `lb-insert-dialog` is
36
- written once outside every row instead of beside the control it serves.
37
- - Ids repeat in a row template. `lb-confirm` finds its dialog by position,
38
- because `commandfor`, HTML's own way to open one with no script, works
39
- only by id.
40
- - Upgrade timing is the hub's problem. An element shipped inside an
41
- `lb-show` template, or whose definition loads late, has not upgraded, so
42
- the hub keeps `waiting()`, lands kept answers again once it upgrades, and
43
- stamps `lb-column-value` for a control to take up on first connect.
44
- - A few lines cost a wrapper. `lb-options` has a script mostly to copy
45
- `lb-group-value` into an `<optgroup>`'s `label` and each key into an
46
- option's `value`, and for that the whole `<select>` sits inside a custom
47
- element.
48
- - Parameters cannot carry structure. Expansion parameters are words, so
49
- when groups became a template, `lb-picker` lost `exp-group` and a grouped
50
- picker went back to a hand-written `lb-options`.
51
- - CSS and accessibility see an element HTML does not define. A wrapper tag
52
- has no role, shifts child and `:nth-child` selectors, and has no place in
53
- a table. The usual remedy, `display: contents`, has a history of
54
- accessibility bugs.
55
- - One element, one class. A ghost row that should also save as it is typed
56
- in needs a widget combining the two.
57
-
58
- ## Code on elements the parser constrains
59
-
60
- Loadbare's data vocabulary is attributes, and an attribute goes on any
61
- element: `<tr lb-query>`, `<option lb-column>`, `<tbody lb-show>`. Behavior
62
- is different. The platform attaches code to markup through a custom element
63
- tag, and a tag must survive the parser. In a table, a `<select>`, or a `<p>`
64
- it does not: an unexpected tag is dropped, moved out of the table, or closes
65
- the paragraph.
66
-
67
- So code that concerns one row lives on a wrapper around the whole table.
68
- `lb-table` owns the `<table>`, and the page writes its contents as named
69
- templates (`head`, `foot`, `ghost`) that the builder joins back into a
70
- table. Each new need is another named destination, and the page's source
71
- reads as `lb-table`'s shape rather than a table's. The shipped HTML is
72
- honest; the source is a dialect.
73
-
74
- Two ways to put code on the element itself follow, and only the second
75
- survives.
76
-
77
- ### Customized built-ins
78
-
79
- ```html
80
- <tr is="lb-ghost-row">…</tr>
81
- ```
82
-
83
- The platform's own answer: a class extending `HTMLTableRowElement`, which
84
- the parser treats as a `<tr>`. WebKit has declined to ship it, and every
85
- browser on iOS and iPadOS is WebKit, so a polyfill, usually a
86
- `MutationObserver`, would be the code path on every iPhone and iPad for
87
- good. That rules it out.
88
-
89
- ### Behavior by attribute
90
-
91
- ```html
92
- <tr lb-behavior="ghost-row">…</tr>
93
- ```
94
-
95
- Code bound to any element by an attribute, as Stimulus does with
96
- `data-controller`. It fits the parser everywhere and needs no polyfill.
97
-
98
- - What usually drives it is a `MutationObserver`, which is incidental: the
99
- code runs because something noticed. The hub, though, already knows every
100
- time it puts an element in the page: it shows a page, clones a row, builds
101
- a group, and moves an element in and out of an `lb-show` template. A
102
- behavior attached and detached at those moments would be explicit.
103
- - Elements the hub does not add (a widget's own scaffolding, anything a
104
- script creates) would get no behavior, which may be the right rule rather
105
- than a limitation.
106
- - No form association of its own, and mostly none needed: a behavior on a
107
- native control leaves it a native control, which joins its form, takes
108
- focus and honors `autofocus` as it always did. Only a control whose UI is
109
- built from scratch needs `ElementInternals`, and stays an autonomous
110
- custom element.
111
- - The platform has discussed a registry for custom attributes, which would
112
- be this idea built in. It has not shipped. Without it the browser has no
113
- part in a behavior: everything below is the hub's.
114
-
115
- #### A lifecycle in the hub's terms
116
-
117
- A custom element's lifecycle is the browser's, and it speaks of insertion:
118
- connected, disconnected, adopted. The hub's work rarely lines up with it. A
119
- moved row is disconnected and connected, the copy a move leaves behind is
120
- constructed, an `lb-show` toggle adopts twice, which is why a custom element
121
- must write `connectedCallback` to run again.
122
-
123
- A behavior's lifecycle would be calls the hub makes from its own code, at
124
- moments it already knows, named for what the hub did:
125
-
126
- | The hub | A behavior hears |
127
- | -------------------------------------------------------------- | ----------------------------------------------------------- |
128
- | Shows a page, places a cloned row, builds a group, first shows an element shipped absent | `attached()`, once, after the element is placed and filled |
129
- | Lands new values on the element's row | `landed(row)` |
130
- | Moves a row the order put elsewhere | `moved()`; the instance is kept |
131
- | Leaves a copy of a moved row behind | Nothing; the copy gets no behavior |
132
- | Turns `lb-show` off, then on | `hidden()`, then `shown()`; the instance is kept |
133
- | Removes a row, once its animations end | `detached()`, once |
134
- | Finishes a request issued from inside the element | `requestDone(detail)` |
135
-
136
- - `attached` and `detached` pair exactly, so setup and teardown need no
137
- guard against running twice.
138
- - The calls are the code-side twin of the stamps a stylesheet already sees
139
- (`lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-leaving`):
140
- one vocabulary for CSS and code, in the hub's relational terms.
141
- - The hub keeps one instance per element and behavior name. An element may
142
- carry several, `lb-behavior="ghost-row autosave"`, which a tag cannot.
143
- - A behavior is a class in a file the builder finds by name, as it finds
144
- `<tag>.browser.ts`, and a name with no file is a build error.
145
-
146
- What the hub would have to define and keep: whether `attached` comes before
147
- or after the rows inside a host land, and the order of calls among nested
148
- behaviors. A custom element gets its ordering from the platform; here it is
149
- loadbare's to state.
150
-
151
- A rule for choosing: a custom element makes a new kind of element (its own
152
- markup, expansion, or a control built from scratch); a behavior adds code to
153
- an element HTML already has.
154
-
155
- ### Where it would land
156
-
157
- Behavior by attribute lets a page write a real `<table lb-query>` with code
158
- on the rows that need it, and leaves `lb-table` to supply only what is truly
159
- the table's. It keeps the parser and the browser matrix out of it, at the
160
- cost of a lifecycle the hub would define.
161
-
162
- ## Expansion that replaces the tag
163
-
164
- Today a custom element's tag does two jobs: at build time it is expanded,
165
- and at run time it carries code. Because the code needs the tag, the tag
166
- ships as a wrapper around what it expanded, and the wrapper is the source of
167
- most widget friction: focus searched for inside it, values forwarded through
168
- it, `autofocus` lost, and no place in a table.
169
-
170
- With behaviors carrying the code, expansion is free to stop shipping the
171
- tag.
172
-
173
- ```html
174
- <!-- page -->
175
- <lb-field exp-label="Name" lb-column="name"></lb-field>
176
-
177
- <!-- what ships -->
178
- <!-- lb-field exp-label="Name" lb-column="name" -->
179
- <label lb-behavior="lb-field">Name <input lb-column="name" /></label>
180
- ```
181
-
182
- - **The tag becomes a comment.** The shipped DOM keeps a trail back to the
183
- source at no run-time cost. A comment is legal anywhere, tables included,
184
- and is invisible to CSS, `:nth-child` and the hub's counting.
185
- - **The builder puts the behavior on the root.** When
186
- `<name>.behavior.ts` exists, the expanded root gets `lb-behavior="<name>"`;
187
- the author never writes it. `[lb-behavior~="lb-field"]` then serves CSS
188
- and `querySelector` as the tag selector `lb-field` does now.
189
- - **Every widget inherits one base class.** A `Behavior` holds `this.el`,
190
- has empty methods for every call in the hub's lifecycle, and types
191
- `landed(row)` and `requestDone(detail)`. A widget overrides only what it
192
- uses, as it extends `HTMLElement` or `WrappedControl` today.
193
-
194
- The file convention decides which path an element takes:
195
-
196
- | The element has | The builder | At run time |
197
- | ------------------------------------------ | -------------------------------------------------- | ---------------------- |
198
- | An element file, no script | Replaces the tag, leaves a comment | Plain HTML |
199
- | An element file and `<name>.behavior.ts` | Replaces the tag, puts `lb-behavior` on the root | The hub's lifecycle |
200
- | `<name>.browser.ts` | Keeps the tag, as today | The browser's lifecycle |
201
-
202
- The third row is for controls built from scratch, the only elements that
203
- still need `ElementInternals`.
204
-
205
- ### A flag to keep the tag
206
-
207
- Considered and set aside. It gives every widget two DOM shapes, so its CSS
208
- and code must work in both. It cannot apply where replacing pays off most,
209
- since a kept tag still has no place in a table. And what people would keep
210
- the tag for, a name to find and style the widget by and a trail to its
211
- source, the `lb-behavior` attribute and the comment already give.
212
-
213
- ### To settle
214
-
215
- 1. **One root.** A behavior attaches to one element, so an element file with
216
- a behavior has exactly one top-level element, and anything more is a
217
- build error.
218
- 2. **Where the tag's attributes go.** The wrapper used to keep them; once
219
- the tag is gone each needs a destination. The natural default is the
220
- root, and `lb-field` breaks it: its root is the `<label>`, but
221
- `lb-column` belongs on the `<input>` inside while `lb-show` belongs on the
222
- label. Either the element file marks a target for data attributes, or
223
- the page passes them as parameters (`exp-column`, written into the file as
224
- `lb-column="{{column}}"`), which works today but reads less naturally
225
- than `lb-column`. This wants worked examples before a rule.
226
-
227
- ## The case for behaviors
228
-
229
- Today's model is simple to state: a widget is a name, and a name is one or
230
- both of a `.html` and a `.browser.ts`. One name is one element the
231
- developer writes, so what a widget does is easy to predict. Most of what
232
- that costs is paid inside Loadbare, in service of the simple model. The one
233
- place it leaks to the developer is the lifecycle, where a widget author must
234
- follow strict rules to get the behavior they expect.
235
-
236
- Behaviors would simplify the hub, but they seem to spread code that a
237
- custom element keeps in one file. The containment comes from the element
238
- file, though, not from the tag.
239
-
240
- ### What one file bundles today
241
-
242
- `lb-table`'s script does three jobs, joined by where they sit rather than by
243
- any data they share:
244
-
245
- | Job | Reads and writes |
246
- | -------------------------- | -------------------------------------------------- |
247
- | Table scaffolding | Caption, head and foot destinations; expansion only |
248
- | The ghost row | The ghost row's controls, its form, and focus |
249
- | Scrolling to the saved row | The list's rows and their stamps |
250
-
251
- It is one file, and three concerns in one class, none reusable without the
252
- others. A `<ul>` cannot have a ghost row without becoming an `lb-table`.
253
-
254
- ### Two units
255
-
256
- - A widget is markup. Its element file decides what elements exist, at
257
- build time, as now.
258
- - A behavior is what one element does. Its code attaches to the element
259
- that carries it, and runs on the hub's lifecycle.
260
-
261
- An element file may write `lb-behavior` on its own inner elements:
262
-
263
- ```html
264
- <!-- lb-table.html -->
265
- <table lb-behavior="scroll-to-saved">
266
- <thead lb-exp-template="head"></thead>
267
- <tbody lb-exp-slot></tbody>
268
- <tbody lb-exp-template="ghost" lb-behavior="ghost-row"></tbody>
269
- </table>
270
- ```
271
-
272
- `lb-table` stays the one place to read, and its element file says which code
273
- runs on which element. The code is split into files by job, and the widget
274
- assembles them. A page that wants a ghost row in a `<ul>` writes
275
- `lb-behavior="ghost-row"` on it.
276
-
277
- ### Where code goes
278
-
279
- Code goes on the element it reads and writes.
280
-
281
- - The ghost row's code touches the ghost row's controls and form, so it is a
282
- behavior on the ghost row.
283
- - Scrolling touches the list's rows, so it is a behavior on the list.
284
- - Code that seems to need two elements is the sign to stop. Behaviors never
285
- call each other. They speak only through what widgets use today:
286
- attributes the hub stamps, and bubbling events such as `lb-request-done`.
287
- The scrolling behavior knows nothing of the ghost row; it reads
288
- `lb-row-requested`.
289
-
290
- The rule also refuses the overfit that started this: a ghost row per
291
- section would be a behavior on the table reaching into every section.
292
-
293
- ### Where the leak moves
294
-
295
- Today the leak is the browser's lifecycle: `connectedCallback` written to
296
- run again, and a value stamp adopted on first connect. With behaviors it
297
- becomes three rules Loadbare states:
298
-
299
- 1. Only elements the hub places get behaviors.
300
- 2. A behavior is attached and detached exactly once.
301
- 3. Behaviors speak only through stamps and events.
302
-
303
- They are fewer, and in the hub's terms rather than the browser's. The cost
304
- moves to Loadbare's design: the ordering the platform gives custom elements
305
- must be stated, such as whether a host's behavior attaches before or after
306
- its rows land, and the order among nested behaviors.
307
-
308
- ### Both, not one
309
-
310
- Custom elements stay. A control built from scratch still needs
311
- `ElementInternals`, and a tag with no code is expansion alone. Behaviors
312
- cover what a tag does badly: code on an element HTML already has, inside
313
- content models that forbid a tag. The developer's question becomes whether
314
- they are making a new kind of element, or giving an existing element a job.