@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.
- package/dist/build/assemble.js +1 -0
- package/dist/build/assemble.js.map +1 -0
- package/dist/build/cli.js +1 -0
- package/dist/build/cli.js.map +1 -0
- package/dist/build/elements.js +1 -0
- package/dist/build/elements.js.map +1 -0
- package/dist/build/expand.js +1 -0
- package/dist/build/expand.js.map +1 -0
- package/dist/build/format.js +1 -0
- package/dist/build/format.js.map +1 -0
- package/dist/build/locations.d.ts +2 -2
- package/dist/build/locations.d.ts.map +1 -1
- package/dist/build/locations.js +3 -4
- package/dist/build/locations.js.map +1 -0
- package/dist/build/origins.js +1 -0
- package/dist/build/origins.js.map +1 -0
- package/dist/build/package-root.js +1 -0
- package/dist/build/package-root.js.map +1 -0
- package/dist/build/pages.d.ts +4 -0
- package/dist/build/pages.d.ts.map +1 -1
- package/dist/build/pages.js +5 -0
- package/dist/build/pages.js.map +1 -0
- package/dist/build/styles.js +1 -0
- package/dist/build/styles.js.map +1 -0
- package/dist/core/lb-constants.js +1 -0
- package/dist/core/lb-constants.js.map +1 -0
- package/dist/core/lb-types.d.ts +1 -1
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js +2 -1
- package/dist/core/lb-types.js.map +1 -0
- package/dist/hub/lb-apply.d.ts +11 -1
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +50 -13
- package/dist/hub/lb-apply.js.map +1 -0
- package/dist/hub/lb-hub.browser.d.ts +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +143 -112
- package/dist/hub/lb-hub.browser.js.map +1 -0
- package/dist/server/lb-express.d.ts +1 -1
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +1 -0
- package/dist/server/lb-express.js.map +1 -0
- package/dist/server/lb-server.d.ts +1 -1
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +2 -1
- package/dist/server/lb-server.js.map +1 -0
- package/docs/TECHREF-1.0.md +158 -43
- package/docs/analysis-accidental-complexity.md +149 -0
- package/docs/reference/custom-elements.md +27 -17
- package/docs/reference/data-binding.md +88 -29
- package/docs/reference/server.md +18 -0
- package/docs/reference/widgets.md +3 -3
- package/docs/theory.md +114 -1
- package/docs/tutorials/072-inserting-into-a-list.md +14 -11
- package/docs/tutorials/080-widget-requests.md +9 -26
- package/package.json +2 -2
- package/dist/tests/assemble.test.d.ts +0 -8
- package/dist/tests/assemble.test.d.ts.map +0 -1
- package/dist/tests/assemble.test.js +0 -210
- package/dist/tests/elements.test.d.ts +0 -8
- package/dist/tests/elements.test.d.ts.map +0 -1
- package/dist/tests/elements.test.js +0 -118
- package/dist/tests/expand.test.d.ts +0 -10
- package/dist/tests/expand.test.d.ts.map +0 -1
- package/dist/tests/expand.test.js +0 -253
- package/dist/tests/fixtures/elements/collision/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/collision/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/imports.js +0 -1
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.js +0 -1
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.js +0 -1
- package/dist/tests/fixtures/elements/manifest/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest/imports.js +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +0 -1
- package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts +0 -5
- package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-not-array/imports.js +0 -1
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.js +0 -1
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts +0 -6
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.js +0 -1
- package/dist/tests/helpers/console.d.ts +0 -20
- package/dist/tests/helpers/console.d.ts.map +0 -1
- package/dist/tests/helpers/console.js +0 -28
- package/dist/tests/helpers/dom.d.ts +0 -18
- package/dist/tests/helpers/dom.d.ts.map +0 -1
- package/dist/tests/helpers/dom.js +0 -22
- package/dist/tests/helpers/hub.d.ts +0 -73
- package/dist/tests/helpers/hub.d.ts.map +0 -1
- package/dist/tests/helpers/hub.js +0 -151
- package/dist/tests/lb-apply.test.d.ts +0 -8
- package/dist/tests/lb-apply.test.d.ts.map +0 -1
- package/dist/tests/lb-apply.test.js +0 -177
- package/dist/tests/lb-express.test.d.ts +0 -14
- package/dist/tests/lb-express.test.d.ts.map +0 -1
- package/dist/tests/lb-express.test.js +0 -243
- package/dist/tests/lb-hub.test.d.ts +0 -14
- package/dist/tests/lb-hub.test.d.ts.map +0 -1
- package/dist/tests/lb-hub.test.js +0 -319
- package/dist/tests/lb-list.test.d.ts +0 -12
- package/dist/tests/lb-list.test.d.ts.map +0 -1
- package/dist/tests/lb-list.test.js +0 -339
- package/dist/tests/lb-server.test.d.ts +0 -9
- package/dist/tests/lb-server.test.d.ts.map +0 -1
- package/dist/tests/lb-server.test.js +0 -546
- package/dist/tests/origins.test.d.ts +0 -10
- package/dist/tests/origins.test.d.ts.map +0 -1
- package/dist/tests/origins.test.js +0 -387
- package/dist/tests/pages.test.d.ts +0 -6
- package/dist/tests/pages.test.d.ts.map +0 -1
- package/dist/tests/pages.test.js +0 -148
- package/dist/tests/styles.test.d.ts +0 -7
- package/dist/tests/styles.test.d.ts.map +0 -1
- package/dist/tests/styles.test.js +0 -76
package/dist/server/lb-server.js
CHANGED
|
@@ -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"]}
|
package/docs/TECHREF-1.0.md
CHANGED
|
@@ -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
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
<
|
|
65
|
-
<template lb-key="id"><
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
</
|
|
69
|
-
</
|
|
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
|
-
- **
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 |
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
395
|
-
particular element, and the
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
436
|
-
scope
|
|
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 —
|
|
268
|
-
`
|
|
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 {
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
[
|
|
288
|
-
|
|
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
|
|
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
|