@loadbare/app 0.6.0 → 0.7.1

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 (122) hide show
  1. package/dist/build/assemble.js +1 -0
  2. package/dist/build/assemble.js.map +1 -0
  3. package/dist/build/cli.js +1 -0
  4. package/dist/build/cli.js.map +1 -0
  5. package/dist/build/elements.js +1 -0
  6. package/dist/build/elements.js.map +1 -0
  7. package/dist/build/expand.js +1 -0
  8. package/dist/build/expand.js.map +1 -0
  9. package/dist/build/format.js +1 -0
  10. package/dist/build/format.js.map +1 -0
  11. package/dist/build/locations.d.ts +2 -2
  12. package/dist/build/locations.d.ts.map +1 -1
  13. package/dist/build/locations.js +3 -4
  14. package/dist/build/locations.js.map +1 -0
  15. package/dist/build/origins.js +1 -0
  16. package/dist/build/origins.js.map +1 -0
  17. package/dist/build/package-root.js +1 -0
  18. package/dist/build/package-root.js.map +1 -0
  19. package/dist/build/pages.d.ts +4 -0
  20. package/dist/build/pages.d.ts.map +1 -1
  21. package/dist/build/pages.js +5 -0
  22. package/dist/build/pages.js.map +1 -0
  23. package/dist/build/styles.js +1 -0
  24. package/dist/build/styles.js.map +1 -0
  25. package/dist/core/lb-constants.js +1 -0
  26. package/dist/core/lb-constants.js.map +1 -0
  27. package/dist/core/lb-types.d.ts +1 -1
  28. package/dist/core/lb-types.d.ts.map +1 -1
  29. package/dist/core/lb-types.js +2 -1
  30. package/dist/core/lb-types.js.map +1 -0
  31. package/dist/hub/lb-apply.d.ts +11 -1
  32. package/dist/hub/lb-apply.d.ts.map +1 -1
  33. package/dist/hub/lb-apply.js +50 -13
  34. package/dist/hub/lb-apply.js.map +1 -0
  35. package/dist/hub/lb-hub.browser.d.ts +1 -1
  36. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  37. package/dist/hub/lb-hub.browser.js +143 -112
  38. package/dist/hub/lb-hub.browser.js.map +1 -0
  39. package/dist/server/lb-express.d.ts +1 -1
  40. package/dist/server/lb-express.d.ts.map +1 -1
  41. package/dist/server/lb-express.js +1 -0
  42. package/dist/server/lb-express.js.map +1 -0
  43. package/dist/server/lb-server.d.ts +1 -1
  44. package/dist/server/lb-server.d.ts.map +1 -1
  45. package/dist/server/lb-server.js +2 -1
  46. package/dist/server/lb-server.js.map +1 -0
  47. package/docs/TECHREF-1.0.md +158 -43
  48. package/docs/analysis-accidental-complexity.md +149 -0
  49. package/docs/reference/custom-elements.md +27 -17
  50. package/docs/reference/data-binding.md +88 -29
  51. package/docs/reference/server.md +18 -0
  52. package/docs/reference/widgets.md +3 -3
  53. package/docs/theory.md +114 -1
  54. package/docs/tutorials/072-inserting-into-a-list.md +14 -11
  55. package/docs/tutorials/080-widget-requests.md +9 -26
  56. package/package.json +2 -2
  57. package/dist/tests/assemble.test.d.ts +0 -8
  58. package/dist/tests/assemble.test.d.ts.map +0 -1
  59. package/dist/tests/assemble.test.js +0 -210
  60. package/dist/tests/elements.test.d.ts +0 -8
  61. package/dist/tests/elements.test.d.ts.map +0 -1
  62. package/dist/tests/elements.test.js +0 -118
  63. package/dist/tests/expand.test.d.ts +0 -10
  64. package/dist/tests/expand.test.d.ts.map +0 -1
  65. package/dist/tests/expand.test.js +0 -253
  66. package/dist/tests/fixtures/elements/collision/imports.d.ts +0 -3
  67. package/dist/tests/fixtures/elements/collision/imports.d.ts.map +0 -1
  68. package/dist/tests/fixtures/elements/collision/imports.js +0 -1
  69. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts +0 -2
  70. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts.map +0 -1
  71. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.js +0 -1
  72. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts +0 -2
  73. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts.map +0 -1
  74. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.js +0 -1
  75. package/dist/tests/fixtures/elements/manifest/imports.d.ts +0 -3
  76. package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +0 -1
  77. package/dist/tests/fixtures/elements/manifest/imports.js +0 -1
  78. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +0 -3
  79. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +0 -1
  80. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +0 -1
  81. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts +0 -5
  82. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +0 -1
  83. package/dist/tests/fixtures/elements/manifest-not-array/imports.js +0 -1
  84. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts +0 -2
  85. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts.map +0 -1
  86. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.js +0 -1
  87. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts +0 -6
  88. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts.map +0 -1
  89. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.js +0 -1
  90. package/dist/tests/helpers/console.d.ts +0 -20
  91. package/dist/tests/helpers/console.d.ts.map +0 -1
  92. package/dist/tests/helpers/console.js +0 -28
  93. package/dist/tests/helpers/dom.d.ts +0 -18
  94. package/dist/tests/helpers/dom.d.ts.map +0 -1
  95. package/dist/tests/helpers/dom.js +0 -22
  96. package/dist/tests/helpers/hub.d.ts +0 -73
  97. package/dist/tests/helpers/hub.d.ts.map +0 -1
  98. package/dist/tests/helpers/hub.js +0 -151
  99. package/dist/tests/lb-apply.test.d.ts +0 -8
  100. package/dist/tests/lb-apply.test.d.ts.map +0 -1
  101. package/dist/tests/lb-apply.test.js +0 -177
  102. package/dist/tests/lb-express.test.d.ts +0 -14
  103. package/dist/tests/lb-express.test.d.ts.map +0 -1
  104. package/dist/tests/lb-express.test.js +0 -243
  105. package/dist/tests/lb-hub.test.d.ts +0 -14
  106. package/dist/tests/lb-hub.test.d.ts.map +0 -1
  107. package/dist/tests/lb-hub.test.js +0 -319
  108. package/dist/tests/lb-list.test.d.ts +0 -12
  109. package/dist/tests/lb-list.test.d.ts.map +0 -1
  110. package/dist/tests/lb-list.test.js +0 -339
  111. package/dist/tests/lb-server.test.d.ts +0 -9
  112. package/dist/tests/lb-server.test.d.ts.map +0 -1
  113. package/dist/tests/lb-server.test.js +0 -546
  114. package/dist/tests/origins.test.d.ts +0 -10
  115. package/dist/tests/origins.test.d.ts.map +0 -1
  116. package/dist/tests/origins.test.js +0 -387
  117. package/dist/tests/pages.test.d.ts +0 -6
  118. package/dist/tests/pages.test.d.ts.map +0 -1
  119. package/dist/tests/pages.test.js +0 -148
  120. package/dist/tests/styles.test.d.ts +0 -7
  121. package/dist/tests/styles.test.d.ts.map +0 -1
  122. package/dist/tests/styles.test.js +0 -76
@@ -4,7 +4,7 @@
4
4
  // vocabulary the browser writes, this one is the vocabulary the application
5
5
  // writes. The engine below is the whole of Hub on the server. It ships no
6
6
  // HTTP server, no router, and no data layer.
7
- import { LB_RESERVED_PREFIX } from "../core/lb-constants";
7
+ import { LB_RESERVED_PREFIX } from "../core/lb-constants.js";
8
8
  /** A query that answers with one row. */
9
9
  export function row(run) {
10
10
  return { kind: "row", run };
@@ -114,3 +114,4 @@ export function createHub(pages) {
114
114
  },
115
115
  };
116
116
  }
117
+ //# sourceMappingURL=lb-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-server.js","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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","sourcesContent":["// The server-side contract and the engine that runs it.\n//\n// This is the counterpart to core/lb-constants.ts: that file is the\n// vocabulary the browser writes, this one is the vocabulary the application\n// writes. The engine below is the whole of Hub on the server. It ships no\n// HTTP server, no router, and no data layer.\n\nimport type { Patch, Row, HubData, HubResult } from \"../core/lb-types.js\";\nimport { LB_RESERVED_PREFIX } from \"../core/lb-constants.js\";\n\n/**\n * Whatever the application hands the engine for the duration of one request.\n *\n * Hub declares it empty and never reads it. An application fills it in by\n * declaration merging, once, anywhere in its own source:\n *\n * declare module \"@loadbare/app/server\" {\n * interface HubContext {\n * db: Db;\n * }\n * }\n *\n * That is why no type on this page takes a type parameter. The context is a\n * request-scoped handle — an authenticated database connection is the\n * expected case — so it is passed per call rather than held by the engine.\n */\nexport interface HubContext {}\n\n/**\n * A declared query: the request context in, one result out.\n *\n * Cardinality is a property of the name rather than of any one answer, so it\n * is declared here and never inferred from what comes back. One name answers\n * with one shape, always. A page that wants the roster once as a single row\n * and once as a set declares two queries.\n *\n * Write one with `row()` or `list()` below; nothing else builds one.\n */\nexport interface Query {\n readonly kind: \"row\" | \"list\";\n readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;\n}\n\n/** A query that answers with one row. */\nexport function row(run: (ctx: HubContext) => Row | Promise<Row>): Query {\n return { kind: \"row\", run };\n}\n\n/**\n * A query that answers with the entire set, and therefore also the order. A\n * list reconciles to exactly this: a row whose key is not here is gone.\n *\n * There is no wrapper around the array, because the declaration already said\n * this name answers with rows.\n */\nexport function list(run: (ctx: HubContext) => Row[] | Promise<Row[]>): Query {\n return { kind: \"list\", run };\n}\n\n/**\n * Only what changed, returned from a `crud` run rather than from a query.\n * Rows named here arrive or are updated, keys in `drop` are gone, and\n * everything unnamed is left alone — its contents, and its place in whatever\n * order the widget is keeping.\n *\n * `Array.isArray` is what tells a patch from a whole set, so a patch needs no\n * marker of its own and no column name is reserved to carry one.\n */\nexport function patch(change: { rows?: Row[]; drop?: string[] }): Patch {\n return { ...change };\n}\n\n/** The shape of a `<name>.queries.ts` module. */\nexport type Queries = Record<string, Query>;\n\n/**\n * Where the interaction happened, in the binding vocabulary, plus the one\n * value a control may carry.\n *\n * The browser fills these from attributes it already has. It never names a\n * function — only a name the page declared — which is what keeps this from\n * being an RPC endpoint. A value may ride along because a `<select>` has one\n * and there is nowhere else to put it; it is a string from a control, not an\n * argument list.\n */\nexport interface Where {\n list?: string;\n row?: string;\n key?: string;\n cell?: string;\n value?: string;\n}\n\n/**\n * What an action does, and which queries must re-run once it has.\n *\n * `run` may also return results of its own, which are laid over the refreshed\n * ones. That is how a patch reaches the browser: a query answers for its\n * whole set and cannot know why it was re-run, but the action knows exactly\n * what it changed and can say only that.\n */\nexport interface Action {\n run: (\n ctx: HubContext,\n where: Where,\n ) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\n/**\n * One CRUD operation on a declared query. Same shape as `Action` — run, then\n * refresh — but `where` carries only what that operation is typed to carry\n * on the wire, rather than the general `Where`.\n */\nexport interface CrudOp<W> {\n run: (ctx: HubContext, where: W) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\nexport type CellChangeOp = CrudOp<{ key: string; cell: string; value: string }>;\nexport type RowDeleteOp = CrudOp<{ key: string }>;\nexport type RowInsertOp = CrudOp<{ values: Record<string, string> }>;\nexport type RowUpdateOp = CrudOp<{\n key: string;\n values: Record<string, string>;\n}>;\n\n/**\n * The CRUD operations declared for one list, keyed by its name in\n * `Requests.crud`. A name with no entry here permits none of them — the wire\n * cannot reach anything the page has not published, exactly as for a named\n * action.\n *\n * Every key here is the reserved `lb-action` value with the prefix stripped\n * and the rest camel-cased, so the attribute, the wire field and this key\n * are one vocabulary. All four are list operations: each needs a key, and a\n * key exists only on a live row inside a list.\n */\nexport interface Crud {\n cellChange?: CellChangeOp;\n rowDelete?: RowDeleteOp;\n rowInsert?: RowInsertOp;\n rowUpdate?: RowUpdateOp;\n}\n\n/**\n * The shape of a `<name>.requests.ts` module.\n *\n * `onPageEnter` runs once when the page is entered, before any query. It\n * declares no refresh set: entering the page runs the whole query set\n * afterward, so whatever the hook changed is already in the response.\n *\n * `actions` names what this page may be asked to do. A name the page did not\n * declare is refused, so the wire cannot reach anything the page has not\n * published.\n *\n * `crud` is the same rule for the typed CRUD operations, keyed by the list\n * they operate on rather than by a declared name — there is nothing to name,\n * since the row's own binding says what it is.\n */\nexport interface Requests {\n onPageEnter?: (ctx: HubContext) => void | Promise<void>;\n actions?: Record<string, Action>;\n crud?: Record<string, Crud>;\n}\n\n/** A page is three files sharing a basename; two of them are these. */\nexport interface Page {\n queries: Queries;\n requests: Requests;\n}\n\n/**\n * The page registry. The builder populates this automatically from every\n * `.requests.ts`/`.queries.ts` pair it discovers — see docs/reference/builder.md,\n * \"Generating the server-side page registry\".\n */\nexport type Pages = Record<string, Page>;\n\nexport interface Hub {\n /** Entering a page: run its onPageEnter hook, then all of its queries. */\n dataForPage(page: string, ctx: HubContext): Promise<HubData>;\n\n /**\n * An action: run what the page declared under that name, then the refresh\n * set declared with it.\n */\n runAction(\n page: string,\n name: string,\n where: Where,\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Edit one cell: run the list's declared `cellChange`, then its refresh set. */\n runCellChange(\n page: string,\n where: { list: string; key: string; cell: string; value: string },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Drop one row: run the list's declared `rowDelete`, then its refresh set. */\n runRowDelete(\n page: string,\n where: { list: string; key: string },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Add one row: run the list's declared `rowInsert`, then its refresh set. */\n runRowInsert(\n page: string,\n where: { list: string; values: Record<string, string> },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Edit several cells at once: run the list's declared `rowUpdate`, then its refresh set. */\n runRowUpdate(\n page: string,\n where: { list: string; key: string; values: Record<string, string> },\n ctx: HubContext,\n ): Promise<HubData>;\n}\n\n/**\n * Build the engine over a set of pages. The pages are fixed at startup; the\n * context is not, and arrives with each call.\n */\nexport function createHub(pages: Pages): Hub {\n // Names beginning with the reserved prefix are Loadbare's — the hub's\n // own query, the CRUD operations — so an application cannot declare one.\n // Refused at startup, because a name is a fact about the page and not\n // about any request.\n for (const [page, entry] of Object.entries(pages)) {\n const declared = [\n ...Object.keys(entry.queries),\n ...Object.keys(entry.requests.actions ?? {}),\n ];\n for (const name of declared) {\n if (name.startsWith(LB_RESERVED_PREFIX)) {\n throw new Error(\n `loadbare: page '${page}' declares '${name}', but names ` +\n `beginning with '${LB_RESERVED_PREFIX}' are reserved`,\n );\n }\n }\n }\n\n async function run(\n page: string,\n names: string[],\n ctx: HubContext,\n ): Promise<HubData> {\n const data: HubData = {};\n for (const name of names) {\n const query = pages[page]?.queries[name];\n if (!query) {\n console.warn(`loadbare: page '${page}' declares no query '${name}'`);\n continue;\n }\n const result = await query.run(ctx);\n // Cardinality is declared, so an answer that disagrees is a mistake in\n // the query rather than a case to handle. Refused here, because a page\n // is better off missing one name than showing the wrong shape for it.\n if (Array.isArray(result) !== (query.kind === \"list\")) {\n console.warn(\n `loadbare: query '${name}' is declared ${query.kind} but answered ` +\n `with ${Array.isArray(result) ? \"rows\" : \"one row\"}`,\n );\n continue;\n }\n data[name] = result;\n }\n return data;\n }\n\n /**\n * Shared by every operation kind: run what was declared, then its refresh\n * set against the same context, laying what the operation itself stated\n * over the refreshed queries — the narrower answer wins because it is the\n * one that knows what actually changed.\n */\n async function settle<W>(\n page: string,\n declared: CrudOp<W> | Action | undefined,\n where: W,\n ctx: HubContext,\n notFound: string,\n ): Promise<HubData> {\n if (!declared) {\n console.warn(notFound);\n return {};\n }\n const stated = await declared.run(ctx, where as never);\n return { ...(await run(page, declared.refresh, ctx)), ...(stated ?? {}) };\n }\n\n return {\n async dataForPage(page, ctx) {\n const entry = pages[page];\n if (!entry) {\n console.warn(`loadbare: no page '${page}'`);\n return {};\n }\n await entry.requests.onPageEnter?.(ctx);\n return run(page, Object.keys(entry.queries), ctx);\n },\n\n runAction(page, name, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.actions?.[name],\n where,\n ctx,\n `loadbare: page '${page}' declares no action '${name}'`,\n );\n },\n\n runCellChange(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.cellChange,\n { key: where.key, cell: where.cell, value: where.value },\n ctx,\n `loadbare: page '${page}' declares no cellChange for '${where.list}'`,\n );\n },\n\n runRowDelete(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowDelete,\n { key: where.key },\n ctx,\n `loadbare: page '${page}' declares no rowDelete for '${where.list}'`,\n );\n },\n\n runRowInsert(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowInsert,\n { values: where.values },\n ctx,\n `loadbare: page '${page}' declares no rowInsert for '${where.list}'`,\n );\n },\n\n runRowUpdate(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowUpdate,\n { key: where.key, values: where.values },\n ctx,\n `loadbare: page '${page}' declares no rowUpdate for '${where.list}'`,\n );\n },\n };\n}\n"]}
@@ -24,6 +24,13 @@ element, or by whatever the widget does when it lands on a widget.
24
24
  We will then see if a useful solution emerges that Loadbare/app should
25
25
  handle.
26
26
 
27
+ ### Form controls
28
+
29
+ - **Checkboxes and radio buttons are not implemented.** A value does not
30
+ land on one and a form does not gather one. Landing needs a decision on
31
+ what counts as checked, which waits on [Data types](#data-types), and a
32
+ radio group is several elements answering to one cell.
33
+
27
34
  ### Run-time state attributes
28
35
 
29
36
  The hub stamps `lb-row-count`, `lb-pending` and `lb-error` for a
@@ -49,24 +56,21 @@ That decision is firm; what the set contains is not.
49
56
  element at a time and offers no escape hatch. An optional `lbAcceptRow`
50
57
  would sit beside `lbPlaceRow` and `lbRowsLanded`, and this is the most
51
58
  likely first request from a widget author.
52
- - **Give a widget a way to fire its own request.** A custom widget must
53
- reproduce the logic in the hub to fire its own event. This is a bit of a
54
- smell, but was done this way to start because we do not know yet the shape
55
- of how the widget could re-use code from the hub.
56
59
 
57
60
  ### Lists
58
61
 
59
- - **Decide what a nested list means.** A list whose rows each hold a list is
60
- legal by the nesting rule, and produces strange results when the outer list
61
- is updated. Loadbare/app has no vocabulary for it.
62
+ - **Decide how a new row fills a nested list.** A row added to the outer
63
+ list starts with an empty nested list, which fills only when the nested
64
+ list's name lands again. See [Master-detail](#master-detail) for what a
65
+ nested list is for.
62
66
 
63
67
  ```html
64
- <div lb-list="departments">
65
- <template lb-key="id"><section>
66
- <h2 lb-cell="name"></h2>
67
- <ul lb-list="members"><template lb-key="id"><li lb-cell="who"></li></template></ul>
68
- </section></template>
69
- </div>
68
+ <tbody lb-list="accounts">
69
+ <template lb-key="id"><tr>
70
+ <td lb-cell="name"></td>
71
+ <td><select lb-list="statuses"><template lb-key="id"><option lb-cell="label"></option></template></select></td>
72
+ </tr></template>
73
+ </tbody>
70
74
  ```
71
75
 
72
76
  ### The server API
@@ -93,10 +97,14 @@ That decision is firm; what the set contains is not.
93
97
  stays a flag, since it is what locates the file. State the precedence
94
98
  between a flag and a key once. Its key names join the permanent surface,
95
99
  so this lands before 1.0 or not at all.
96
- - **Take a position on the URL space.** A page name is a filename basename,
97
- so `/user/42` resolves to nothing and no page is addressable by row.
98
- Deciding against path parameters is a legitimate answer. Write whichever
99
- one down.
100
+ - **Decide whether a URL carries view parameters.** A URL names a page and
101
+ nothing finer — see [What a URL names](#what-a-url-names) — and a query
102
+ takes no argument from the browser. So a page has nowhere to keep which
103
+ record it shows, a filter, a sort, or a collapsed section: a reload or a
104
+ shared link loses them, and a filter is lost whenever an action re-runs its
105
+ query. Parameters on a page URL, as `<form method="get">` puts its fields
106
+ in the query string, are one answer. Deciding against them, and leaving
107
+ view state to the application, is another. Write whichever one down.
100
108
  - **Decide CSS pairing.** Naming a stylesheet `<tag>.css` beside its
101
109
  definition would let the builder ship only what survives expansion.
102
110
  Recommend deferring the mechanism and reserving the configuration key, so
@@ -376,35 +384,104 @@ custom widgets to update themselves.
376
384
  | lb-cell | Developer | This DOM node displays this column of the row in scope |
377
385
  | lb-key | Developer | Names the column that identifies a row, on the row template inside an lb-list |
378
386
  | lb-key-value | Hub | Stamped on a live row: that row's value of `lb-key` |
379
- | lb-value | Hub | Custom widgets update themselves when this attribute changes |
387
+ | lb-value | Hub | The value that landed on a cell; a widget updates itself from it and a stylesheet selects on it |
380
388
 
381
389
  #### How a value lands
382
390
 
383
- A cell lands one of two ways, and the tag name decides which.
391
+ A cell lands one of three ways, and the element decides which. Every cell
392
+ that receives a value also carries it as `lb-value`.
393
+
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` |
399
+
400
+ A widget owns whatever control it wraps, so it is handed the value and
401
+ renders it itself. A form control shows its state as its `value`, so a
402
+ `<select>` keeps its options. Any other native element has no behavior of its
403
+ own, so its value is its text.
404
+
405
+ A form gathers from the same controls it lands on, so a value read back on
406
+ submit is the one that landed.
384
407
 
385
- | Element | Receives the value as |
386
- |----------------------------------|--------------------------|
387
- | A native element | Its `textContent` |
388
- | A custom element, tag hyphenated | Its `lb-value` attribute |
408
+ An `<input>` of type `checkbox`, `radio`, or `file` receives nothing, not even
409
+ `lb-value`, and the hub reports it to the console. A form gathering one skips it the same way.
389
410
 
390
- A native element has no behavior of its own, so its value is its text. A
391
- widget owns whatever control it wraps, so it is handed the value and renders
392
- it itself.
411
+ A `<select>` with no option for the value shows no selection.
393
412
 
394
- The test is the hyphen in the tag name. The hub holds no knowledge of any
395
- particular element, and the same test decides that the hub leaves a widget's
396
- own clicks alone — see [Requests](#requests).
413
+ Only the hyphen and these three form controls are recognized. The hub holds
414
+ no other knowledge of any particular element, and the hyphen also decides
415
+ that the hub leaves a widget's own clicks alone — see [Requests](#requests).
397
416
 
398
- Nothing an application writes ever sets `lb-value`. Loadbare writes it and
399
- the widget it is written on reads it.
417
+ Nothing an application writes ever sets `lb-value`. Loadbare writes it, and
418
+ a widget or a stylesheet reads it.
400
419
 
401
420
  The value arrives as the query produced it, with no conversion, so the
402
421
  browser decides what a non-string looks like.
403
422
 
423
+ A cell holds one value. A set of values is a list of its own, never an array
424
+ in a cell.
425
+
404
426
  A widget sees `attributeChangedCallback` for an `lb-value` already present
405
427
  when it upgrades, so a widget cannot tell a first landing from a refresh,
406
428
  and does not need to.
407
429
 
430
+ #### Displaying by value
431
+
432
+ A stylesheet selects on `lb-value` to show or hide part of a page according
433
+ to a value that landed. A column the page does not display is bound to a
434
+ hidden element.
435
+
436
+ ```html
437
+ <template lb-key="id">
438
+ <tr>
439
+ <td lb-cell="name"></td>
440
+ <td lb-cell="locked" hidden></td>
441
+ <td><button lb-action="lb-row-delete">Remove</button></td>
442
+ </tr>
443
+ </template>
444
+ ```
445
+
446
+ ```css
447
+ tr:has([lb-cell="locked"][lb-value="true"]) button { display: none; }
448
+ ```
449
+
450
+ `lb-value` holds the string the query sent, so the query decides the spelling
451
+ a selector matches. A hidden control is out of reach, keyboard included; a
452
+ stylesheet cannot disable one, which is a widget's job. Hiding is
453
+ presentation, and the server still refuses what a request may not do.
454
+
455
+ #### Master-detail
456
+
457
+ A master is a row and its detail is a list, as two names answered side by
458
+ side. A cell never holds rows.
459
+
460
+ ```html
461
+ <section lb-row="invoice">
462
+ <h2 lb-cell="number"></h2>
463
+ <span lb-cell="customer"></span>
464
+ </section>
465
+
466
+ <table>
467
+ <tbody lb-list="invoiceLines">
468
+ <template lb-key="id"><tr><td lb-cell="item"></td><td lb-cell="amount"></td></tr></template>
469
+ </tbody>
470
+ </table>
471
+ ```
472
+
473
+ Many masters, each with its own detail, is one list of joined rows: each
474
+ detail row carries its master's columns. A widget's `lbPlaceRow` groups them
475
+ for display, as `lb-table` builds sections and `lb-options` builds
476
+ `<optgroup>`s.
477
+
478
+ A list nested in another list's rows receives the same rows in every outer
479
+ row. That serves a picker offering the same choices on every row, and is
480
+ not a way to show a different detail per row.
481
+
482
+ Which master a page shows is server state reached through `ctx`, since a
483
+ query takes no argument from the browser — see the blocker on view parameters.
484
+
408
485
  ### Requests
409
486
 
410
487
  ---- UNEDITED ----
@@ -422,9 +499,9 @@ name, the same test that decides how a value lands — see
422
499
 
423
500
  | lb-action | Written on |
424
501
  |----------------|---------------------------------------------------|
425
- | lb-row-insert | a `<form>` in a list scope |
502
+ | lb-row-insert | a `<form>`, or a button by its cells, in a list |
426
503
  | lb-row-delete | anything inside a live row |
427
- | lb-row-update | a `<form>` inside a live row |
504
+ | lb-row-update | a `<form>`, or a button by its cells, in a row |
428
505
  | lb-cell-change | a widget wrapping one control |
429
506
  | anything else | must be a named routine in the page's server code |
430
507
 
@@ -432,21 +509,43 @@ The wire format is not visible to the user, but uses the same lb-*
432
509
  attributes minus their prefix, so that it is intelligible when working on
433
510
  Loadbare/app itself.
434
511
 
435
- From a native element the hub sends the action plus whatever binding is in
436
- scope, and never a scalar value.
512
+ The hub scopes every request, whether a native element or a widget
513
+ dispatched it. It reads the scope from the dispatching element before any
514
+ ancestor sees the event, and never overwrites a field the request already
515
+ carries.
516
+
517
+ | `action` | Filled from scope | Required |
518
+ |------------------|--------------------------------|----------------------------|
519
+ | a declared name | `list` or `row`, `key`, `cell` | nothing |
520
+ | `lb-cell-change` | `list`, `key`, `cell` | all three, and `value` |
521
+ | `lb-row-insert` | `list` | `list`, and `values` |
522
+ | `lb-row-delete` | `list`, `key` | both |
523
+ | `lb-row-update` | `list`, `key` | both, and `values` |
524
+
525
+ An element's own `lb-list` or `lb-row` names what it displays, never where
526
+ its request goes. A request belongs to the scope around the element, the
527
+ way a control belongs to the form around it. `list` or `row` comes from the
528
+ nearest ancestor scope, `key` from the nearest live row inside that scope,
529
+ and `cell` from the dispatching element's own `lb-cell`.
530
+ A request missing a required field is not sent. The hub never fills
531
+ `value`; for `lb-row-insert` and `lb-row-update`, `values` is gathered from
532
+ the nearest element holding an `lb-cell` with a control to read, starting at
533
+ the dispatching element and walking up. A `<form>` holds its own cells; a
534
+ button in a `<tr>` for a new row, which cannot be a form, reads the row
535
+ around it. The walk stops at the live row the element sits in and never
536
+ reaches the scope, whose other cells belong to other rows. A request that
537
+ already carries `values` keeps them, and one with no cell to read is not
538
+ sent. A click inside a native element that carries either operation and
539
+ holds cells itself is not sent, since clicking into one of its controls
540
+ would send the row: put the action on a form or on a button.
541
+
542
+ A widget dispatches the action and, where it wraps a control, that control's
543
+ value.
437
544
 
438
545
  ```
439
546
  { action: "lb-row-insert", list: "rosterList", values: {...} }
440
547
  { action: "lb-row-delete", list: "rosterList", key: "42" }
441
548
  { action: "lb-row-update", list: "rosterList", key: "42", values: {...} }
442
- ```
443
-
444
- A widget builds its own request and adds the value of the control it wraps.
445
- `lb-select` and `lb-options` send an action and a value and no binding.
446
- `lb-input` sends the binding as well, and is the only sender of a cell
447
- change.
448
-
449
- ```
450
549
  { action: "lb-cell-change", list: "rosterList", key: "42", cell: "name", value: "Ann" }
451
550
  { action: "selectTab", row: "prefs", cell: "active_tab", value: "two" }
452
551
  ```
@@ -519,6 +618,22 @@ One that declares none gets a console error and nothing on screen. See
519
618
 
520
619
  See also [Navigation Row lb-navigation](#lb-navigation).
521
620
 
621
+ #### What a URL names
622
+
623
+ A URL names a page, a place in the application. It never names a resource,
624
+ and Loadbare/app does not reproduce REST: `/accounts/42` is not a way to
625
+ reach account 42.
626
+
627
+ A page declares what it shows, its queries, and what it allows, its actions.
628
+ Loading a page runs its queries. A request performs an action at a position.
629
+ A row is addressed by `list` and `key`, taken from where the element sits,
630
+ which is how the database already names it. Nothing is fetched by URL, so an
631
+ application designs no endpoints, and the browser reaches only what a page
632
+ publishes.
633
+
634
+ Whether a page URL may carry parameters, so that a link can open the page on
635
+ one record, is undecided — see the blockers.
636
+
522
637
  ### lb-navigation
523
638
 
524
639
  ---- UNEDITED ----
@@ -0,0 +1,149 @@
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.
@@ -264,28 +264,39 @@ no separate hydration path to write.
264
264
  ### Sending a request
265
265
 
266
266
  A widget that owns its own interaction — a `<select>`'s choice rather than a
267
- click — builds its own request and dispatches it as a bubbling
268
- `CustomEvent` named `LB_EVENT_NAME`, carrying one of the `HubRequest` shapes
269
- as its `detail`:
267
+ click — dispatches its own request as a bubbling `CustomEvent` named
268
+ `LB_EVENT_NAME`, carrying the action and, where it wraps a control, that
269
+ control's value as its `detail`:
270
270
 
271
271
  ```ts
272
- import { ATTR_CELL, ATTR_KEY_VALUE, ATTR_LIST, LB_EVENT_NAME } from "@loadbare/app/constants";
272
+ import { LB_EVENT_NAME } from "@loadbare/app/constants";
273
273
  import type { HubRequest } from "@loadbare/app/types";
274
274
 
275
- const detail: HubRequest = {
276
- action: "lb-cell-change",
277
- list: this.closest(`[${ATTR_LIST}]`)!.getAttribute(ATTR_LIST)!,
278
- key: this.closest(`[${ATTR_KEY_VALUE}]`)!.getAttribute(ATTR_KEY_VALUE)!,
279
- cell: this.getAttribute(ATTR_CELL)!,
280
- value: input.value,
281
- };
275
+ const detail: HubRequest = { action: "lb-cell-change", value: input.value };
282
276
  this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
283
277
  ```
284
278
 
285
- Find the scope name and `key` by walking up with `closest()`, the same way the hub
286
- finds the binding for a native element. See
287
- [Data Binding](./data-binding.md#requests) for the request vocabulary and
288
- what each operation carries.
279
+ The hub fills in the scope the widget sits in before any ancestor sees the
280
+ event, and does not send a request missing a field its operation requires.
281
+ See [TECHREF-1.0](../TECHREF-1.0.md#requests) for what each operation is
282
+ filled with.
283
+
284
+ A widget sending `lb-row-insert` or `lb-row-update` doesn't read its own
285
+ controls. It dispatches the bare action from the element holding the cells,
286
+ a `<tr>` as readily as a `<form>`, or from anything inside it, and the hub
287
+ gathers `values` from the nearest element holding a readable `lb-cell`:
288
+
289
+ ```ts
290
+ row.dispatchEvent(
291
+ new CustomEvent(LB_EVENT_NAME, {
292
+ bubbles: true,
293
+ detail: { action: "lb-row-insert" } as HubRequest,
294
+ }),
295
+ );
296
+ ```
297
+
298
+ Values the widget supplies itself are kept, and nothing is gathered over
299
+ them.
289
300
 
290
301
  Let the event bubble, so an ancestor widget can intercept and stop it before
291
302
  the hub sees it. A hand-written widget and a native element carrying
@@ -339,8 +350,7 @@ order. `lb-options.browser.ts` and `lb-table.browser.ts` in
339
350
  [`@loadbare/widgets`](./widgets.md) are two different placements over the
340
351
  same machinery.
341
352
 
342
- A list scope with no row template displays nothing, which is not an error: an
343
- insert form names the list it adds a row to and has no rows of its own.
353
+ A list scope with no row template displays nothing, which is not an error.
344
354
 
345
355
  Style an empty list against `lb-row-count` rather than carrying an empty-state
346
356
  conditional in the widget — see