@loadbare/app 0.8.0 → 0.8.2

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 (47) hide show
  1. package/dist/build/skills-cli.d.ts +12 -0
  2. package/dist/build/skills-cli.d.ts.map +1 -0
  3. package/dist/build/skills-cli.js +81 -0
  4. package/dist/build/skills-cli.js.map +1 -0
  5. package/dist/build/skills.d.ts +47 -0
  6. package/dist/build/skills.d.ts.map +1 -0
  7. package/dist/build/skills.js +124 -0
  8. package/dist/build/skills.js.map +1 -0
  9. package/dist/core/lb-constants.d.ts +2 -0
  10. package/dist/core/lb-constants.d.ts.map +1 -1
  11. package/dist/core/lb-constants.js +18 -4
  12. package/dist/core/lb-constants.js.map +1 -1
  13. package/dist/hub/lb-apply.d.ts +10 -0
  14. package/dist/hub/lb-apply.d.ts.map +1 -1
  15. package/dist/hub/lb-apply.js +15 -1
  16. package/dist/hub/lb-apply.js.map +1 -1
  17. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  18. package/dist/hub/lb-hub.browser.js +116 -35
  19. package/dist/hub/lb-hub.browser.js.map +1 -1
  20. package/dist/server/lb-express.d.ts +13 -5
  21. package/dist/server/lb-express.d.ts.map +1 -1
  22. package/dist/server/lb-express.js +26 -7
  23. package/dist/server/lb-express.js.map +1 -1
  24. package/dist/server/lb-server.d.ts +3 -0
  25. package/dist/server/lb-server.d.ts.map +1 -1
  26. package/dist/server/lb-server.js.map +1 -1
  27. package/docs/TECHREF-1.0.md +93 -22
  28. package/docs/comparison.md +8 -7
  29. package/docs/reference/chrome.md +5 -3
  30. package/docs/reference/data-binding.md +28 -0
  31. package/docs/reference/server.md +11 -0
  32. package/docs/reference/widgets.md +8 -4
  33. package/docs/roadmap.md +16 -0
  34. package/docs/theory.md +8 -1
  35. package/docs/tutorials/010-pages-and-navigation.md +2 -1
  36. package/package.json +8 -4
  37. package/skills/loadbare-app/SKILL.md +275 -0
  38. package/skills/loadbare-app/references/TECHREF-1.0.md +1260 -0
  39. package/skills/loadbare-app/references/builder.md +134 -0
  40. package/skills/loadbare-app/references/chrome.md +160 -0
  41. package/skills/loadbare-app/references/css.md +44 -0
  42. package/skills/loadbare-app/references/custom-elements.md +397 -0
  43. package/skills/loadbare-app/references/data-binding.md +485 -0
  44. package/skills/loadbare-app/references/overview.md +38 -0
  45. package/skills/loadbare-app/references/page-files.md +194 -0
  46. package/skills/loadbare-app/references/server.md +153 -0
  47. package/skills/loadbare-app/references/widgets.md +178 -0
@@ -0,0 +1,194 @@
1
+ # Page Files
2
+
3
+ A page is a set of files sharing one base name. The application writes the
4
+ HTML, and adds queries and requests when the page shows data.
5
+
6
+ | File | Holds |
7
+ |----------------------|-------------------------------|
8
+ | `<name>.page.html` | The page's HTML |
9
+ | `<name>.queries.ts` | The data the page displays |
10
+ | `<name>.requests.ts` | What the page does when asked |
11
+
12
+ Name the landing page `index.page.html`. A bare `/` resolves to `index`, and
13
+ every other path names the page of the same name.
14
+
15
+ Put the files anywhere under `src/`. The builder pairs them by base name, not
16
+ by directory; `src/pages/` is the convention.
17
+
18
+ ## HTML
19
+
20
+ Write the page as a fragment. The fragment will land in `<main>`, which
21
+ is supplied by [chrome.html](./chrome.md).
22
+
23
+ ```html
24
+ <!-- src/pages/about.page.html -->
25
+ <h1>About</h1>
26
+ <p>This is the about page.</p>
27
+
28
+ <div lb-row="visits">
29
+ <p>This page has been visited <span lb-cell="count"></span> times.</p>
30
+ </div>
31
+ ```
32
+
33
+ Bind elements to data with `lb-list` or `lb-row`, `lb-cell`, and the rest of the
34
+ attribute vocabulary in [Data Binding](./data-binding.md).
35
+
36
+ A page that displays no data needs no other file.
37
+
38
+ ## Queries
39
+
40
+ Export `queries` from `<name>.queries.ts`. Each key is a name the HTML binds
41
+ to with `lb-list` or `lb-row`, and each value takes the request context and returns
42
+ that query's result:
43
+
44
+ ```ts
45
+ // src/pages/about.queries.ts
46
+ import { row, type Queries } from "@loadbare/app/server";
47
+
48
+ export const queries: Queries = {
49
+ visits: row(async (ctx) => ({ count: String(await ctx.db.visitCount()) })),
50
+ };
51
+ ```
52
+
53
+ Declare each query with `row()` or `list()`. Cardinality is a property of the
54
+ name rather than of any one answer, so one name answers with one shape,
55
+ always, and `createHub` refuses an answer that disagrees. A page that needs
56
+ the same data as one row and as a set declares two queries.
57
+
58
+ The hub hands each cell to the browser untouched and takes no position on
59
+ its type, so what a number, a date or a null looks like is decided here, in
60
+ the query. Formatting it here means the browser displays a value it never
61
+ computes.
62
+
63
+ Declare a query that answers with many rows using `list()`, and return the
64
+ array itself:
65
+
66
+ ```ts
67
+ // src/pages/directory.queries.ts
68
+ import { list, type Queries } from "@loadbare/app/server";
69
+
70
+ export const queries: Queries = {
71
+ directory: list((ctx) => ctx.db.directory()),
72
+ };
73
+ ```
74
+
75
+ Give every row a column that identifies it, and name that column with
76
+ `lb-key` in the HTML. Return the rows in the order the page shows them.
77
+
78
+ Return the full result every time. Sending only what changed is a request's job —
79
+ see [refresh and patch](#refresh-and-patch).
80
+
81
+ ## Requests, actions, CRUD
82
+
83
+ Export `requests` from `<name>.requests.ts`. It holds three keys, each optional:
84
+
85
+ | Key | Runs |
86
+ |---------------|------------------------------------------------------|
87
+ | `onPageEnter` | Before the page's queries, on entering the page |
88
+ | `actions` | What the page may be asked to do, by name |
89
+ | `crud` | The three operations a list permits on its rows |
90
+
91
+ ### onPageEnter
92
+
93
+ ```ts
94
+ // src/pages/about.requests.ts
95
+ import { type Requests } from "@loadbare/app/server";
96
+
97
+ export const requests: Requests = {
98
+ onPageEnter: (ctx) => ctx.db.recordVisit(),
99
+ };
100
+ ```
101
+
102
+ Declare no refresh set here. The page's queries run afterward.
103
+
104
+ ### actions
105
+
106
+ Declare an action under the name the HTML gives `lb-action`. Pair what it
107
+ does with the queries to re-run once it has:
108
+
109
+ ```ts
110
+ export const requests: Requests = {
111
+ actions: {
112
+ resetVisits: {
113
+ run: (ctx) => ctx.db.resetVisits(),
114
+ refresh: ["visits"],
115
+ },
116
+ },
117
+ };
118
+ ```
119
+
120
+ Declare every action the page allows. A name the page does not declare is
121
+ refused.
122
+
123
+ Read where the interaction happened from `run`'s second argument, which
124
+ carries `list` or `row`, whichever attribute scoped the element, plus `key`,
125
+ `cell` and `value` when the element that dispatched the request had them.
126
+
127
+ ### crud
128
+
129
+ Declare CRUD operations under `crud`, keyed by the list they operate on. All
130
+ three are list operations: each needs a key, and a key exists only on a live
131
+ row inside a list, so a single-row scope is read-only and a declared action is
132
+ the only thing it can send. Each operation takes the binding its trigger
133
+ supplies:
134
+
135
+ | Operation | The page writes | `run` receives |
136
+ |-------------|------------------------------------------------------------------------|-----------------|
137
+ | `rowDelete` | `lb-action="lb-row-delete"` | `key` |
138
+ | `rowInsert` | `<form lb-action="lb-row-insert">` | `values` |
139
+ | `rowUpdate` | `<form lb-action="lb-row-update">` or `<lb-input lb-action="lb-row-update">` | `key`, `values` |
140
+
141
+ Write `rowUpdate` to set the columns `values` names and leave every other
142
+ column as it is. A form sends the cells it holds, and a widget cell sends
143
+ itself alone. Check the names in `values` against the columns the list lets
144
+ the page edit.
145
+
146
+ The operation names are reserved: a name beginning with `lb-` cannot be
147
+ declared under `actions` or as a query, and `createHub` refuses a page that
148
+ tries.
149
+
150
+ ```ts
151
+ // src/pages/directory.requests.ts
152
+ import { patch, type Requests } from "@loadbare/app/server";
153
+
154
+ export const requests: Requests = {
155
+ crud: {
156
+ directory: {
157
+ rowInsert: {
158
+ run: async (ctx, { values }) => {
159
+ const entry = await ctx.db.addDirectoryEntry(values);
160
+ return { directory: patch({ rows: [entry] }) };
161
+ },
162
+ refresh: [],
163
+ },
164
+ },
165
+ },
166
+ };
167
+ ```
168
+
169
+ Declare every operation the list permits. An operation a list does not
170
+ declare is refused, and a name with no `crud` entry permits none.
171
+
172
+ ### refresh and patch
173
+
174
+ List in `refresh` every query whose whole answer the operation changed.
175
+
176
+ Return a result from `run` to state a narrower change than re-running a query
177
+ would. What `run` returns is laid over the refreshed queries:
178
+
179
+ | Result | States |
180
+ |--------------------------|---------------------------------------------|
181
+ | `[...]` | The entire set, and its order |
182
+ | `patch({ rows: [...] })` | These rows arrive or change; the rest stand |
183
+ | `patch({ drop: [...] })` | These keys are gone; the rest stand |
184
+
185
+ Return a patch for a change the operation knows the extent of — one row added,
186
+ one row dropped, one row edited — and leave `refresh` empty. Re-run the query
187
+ instead when membership or order changed in a way the operation cannot name:
188
+
189
+ ```ts
190
+ resetRoster: {
191
+ run: (ctx) => ctx.db.resetMembers(),
192
+ refresh: ["roster"],
193
+ },
194
+ ```
@@ -0,0 +1,153 @@
1
+ # The Express Server
2
+
3
+ Loadbare ships no server. The application writes an ordinary Express app and
4
+ serves four things from it: the client script, the stylesheet, the data
5
+ channel, and the one HTML document.
6
+
7
+ Express is a peer dependency. The application installs it.
8
+
9
+ ## A complete server
10
+
11
+ Here is a minimal but complete server for a typical app:
12
+
13
+ ```ts
14
+ // server.ts
15
+ import path from "node:path";
16
+ import { readFileSync } from "node:fs";
17
+ import express, { type Request } from "express";
18
+ import { hubRoutes } from "@loadbare/app/express";
19
+ import type { HubContext } from "@loadbare/app/server";
20
+ import { hub } from "./dist/pages";
21
+ import { openDb } from "./src/database";
22
+
23
+ const DIST = path.resolve("dist");
24
+ const app = express();
25
+
26
+ app.get("/client.js", (_req, res) => res.sendFile(path.join(DIST, "client.js")));
27
+ app.get("/app.css", (_req, res) => res.sendFile(path.join(DIST, "app.css")));
28
+
29
+ function contextFor(_req: Request): HubContext {
30
+ return { db: openDb() };
31
+ }
32
+ app.use(hubRoutes(hub, contextFor));
33
+
34
+ app.get(/.*/, (_req, res) =>
35
+ res.type("html").send(readFileSync(path.join(DIST, "app.html"), "utf-8")),
36
+ );
37
+
38
+ app.listen(8787);
39
+ ```
40
+
41
+ ## What the server serves
42
+
43
+ | Required | Serves |
44
+ |------------------------------|-------------------------------|
45
+ | `/client.js` | `dist/client.js` |
46
+ | `/app.css` | `dist/app.css` |
47
+ | `hubRoutes(hub, contextFor)` | The hub's own route |
48
+ | Every other GET | `dist/app.html` |
49
+
50
+ Everything else is optional:
51
+
52
+ | Optional | Description |
53
+ |-------------------------------|----------------------------------------------|
54
+ | Middleware before `hubRoutes` | Sessions, authentication, logging |
55
+ | The application's own routes | Uploads, webhooks, anything outside Loadbare |
56
+ | An error handler | Express sends its own 500 without one |
57
+
58
+
59
+ ## Rules for writing the server
60
+
61
+ Register the static routes and `hubRoutes` before the catch-all.
62
+
63
+ Register authentication before `hubRoutes`.
64
+
65
+ Serve `dist/app.html` for every route the application does not claim,
66
+ including a path that names no page. See [`chrome.html`](./chrome.md) for the
67
+ `<dialog lb-unknown-page>` that announces that case to the user.
68
+
69
+ Give the server the origin root. The hub reaches its own route by absolute
70
+ path, so the application cannot be hosted under a subpath such as
71
+ `example.com/myapp/`, and anything proxying in front of the server passes the
72
+ whole path space through unchanged.
73
+
74
+ Leave `express.json()` to `hubRoutes`, which mounts it on its own routes.
75
+
76
+ ## Running the server
77
+
78
+ Run the server under a TypeScript-capable runner. The builder writes
79
+ `dist/pages.ts`, which exports `hub`, as TypeScript:
80
+
81
+ ```json
82
+ {
83
+ "scripts": {
84
+ "dev": "loadbare-app-build --watch & tsx server.ts"
85
+ }
86
+ }
87
+ ```
88
+
89
+ Node 22.18 and later also runs it with no runner at all, since Node strips
90
+ TypeScript types itself. `dist/pages.ts` imports each `.requests.ts` and
91
+ `.queries.ts` file by its full name, extension included, because Node looks
92
+ for exactly the path an import names.
93
+
94
+ Enable `allowImportingTsExtensions` in the application's `tsconfig.json`
95
+ when `tsc` type-checks the server. Without it, `tsc` rejects the `.ts`
96
+ extensions in `dist/pages.ts`:
97
+
98
+ ```json
99
+ {
100
+ "compilerOptions": {
101
+ "noEmit": true,
102
+ "allowImportingTsExtensions": true
103
+ }
104
+ }
105
+ ```
106
+
107
+ Restart the server after adding or changing a `.requests.ts` or `.queries.ts`
108
+ file.
109
+
110
+ ## Database layer
111
+
112
+ Loadbare ships no data layer. The application opens its own database and hands
113
+ it to Loadbare as the request context.
114
+
115
+ The application writes `contextFor` and passes it to `hubRoutes`. Loadbare
116
+ calls it on every data request:
117
+
118
+ ```ts
119
+ // server.ts
120
+ function contextFor(req: Request): HubContext {
121
+ return { db: openDb(req.session.userId) };
122
+ }
123
+ ```
124
+
125
+ Open the handle in `contextFor` rather than once at startup, so that each
126
+ request works through a database opened for the caller it authenticated.
127
+
128
+ Declare what the context holds, once, anywhere in the application's own
129
+ source. Next to the database module is the natural place:
130
+
131
+ ```ts
132
+ // src/database.ts
133
+ declare module "@loadbare/app/server" {
134
+ interface HubContext {
135
+ db: Db;
136
+ }
137
+ }
138
+ ```
139
+
140
+ The query string the browser is showing arrives on every data request, so
141
+ `req.query` holds the page's query parms. Put on the context whatever a query
142
+ reads from them, and treat them as user input:
143
+
144
+ ```ts
145
+ function contextFor(req: Request): HubContext {
146
+ const { team } = req.query;
147
+ return { db: openDb(), team: typeof team === "string" ? team : "" };
148
+ }
149
+ ```
150
+
151
+ Add a field for anything else a request needs — the authenticated user, a
152
+ request id, a feature flag set. Queries and requests read them from `ctx`; see
153
+ [page files](./page-files.md).
@@ -0,0 +1,178 @@
1
+ # The Basic Widget Library
2
+
3
+ Seven widgets: `lb-input`, `lb-select`, `lb-list`, `lb-options`, `lb-table`,
4
+ `lb-picker`, `lb-unknown-page`. Every one of them is written against the same two contracts
5
+ documented elsewhere — [Custom Elements](./custom-elements.md#html) for its
6
+ definition, [Custom Elements](./custom-elements.md#code) for its class —
7
+ nothing here is special-cased machinery.
8
+
9
+ They ship compiled, in `@loadbare/widgets`, a package the builder resolves the
10
+ way it resolves anyone else's — it declares `"loadbare": { "widgets": "./dist" }`
11
+ and the builder scans that. Install it and list it:
12
+
13
+ ```
14
+ npm install @loadbare/widgets
15
+ ```
16
+
17
+ ```ts
18
+ // src/imports.ts
19
+ export default ["@loadbare/widgets"];
20
+ ```
21
+
22
+ See [Widgets from packages](./builder.md#widgets-from-packages) for what
23
+ listing a package does, and [The Builder](./builder.md#where-the-builder-looks)
24
+ for where a listed package sits in the cascade.
25
+
26
+ ## `lb-input`
27
+
28
+ Wraps an `<input>`. `lb-value` sets the input's `.value`. The widget sends
29
+ nothing on its own: `lb-action` names what the input's `change` sends. The
30
+ reserved `lb-row-update` saves the input's own cell; any other name sends that
31
+ action. Either carries the input's value, and the hub adds the scope the
32
+ input sits in.
33
+
34
+ An input inside an `lb-row-insert` or `lb-row-update` form leaves the attribute off.
35
+ The form reads every `lb-cell` in it on submit and sends one request for all
36
+ of them, so an input that also sent its own would write the same edit twice.
37
+
38
+ | Parameter | Fills |
39
+ | ---------- | ----- |
40
+ | `exp-label` | the visible `<label>` text |
41
+ | `exp-readonly` | the input's `readonly` attribute |
42
+
43
+ | Attribute | Asks for |
44
+ | ---------- | ----- |
45
+ | `lb-action` | what to send on `change`; `lb-row-update` to save the cell's own edit |
46
+
47
+ ## `lb-select`
48
+
49
+ Wraps a `<select>` whose `<option>`s the author writes directly inside (via
50
+ `lb-slot`). `lb-value` sets the select's `.value`; a `change` sends the
51
+ action named by `lb-action`, with the select's `.value` as the request's
52
+ `value` — the choice is the interaction, so this is the case where an
53
+ action carries a value.
54
+
55
+ | Parameter | Fills |
56
+ | ---------- | ----- |
57
+ | `exp-label` | the visible `<label>` text |
58
+
59
+ Requires `lb-action` or `lb-query-parm` — a change with neither logs and
60
+ sends nothing. With `lb-query-parm` the hub writes the choice into the URL.
61
+
62
+ ## `lb-options`
63
+
64
+ A `<select>` whose `<option>`s come from a query instead of being written by
65
+ hand. The author supplies the row template inside the widget (via
66
+ `lb-slot`), same as any list widget:
67
+
68
+ ```html
69
+ <lb-options lb-list="statuses" exp-label="Status" lb-action="setStatus">
70
+ <template lb-key="id" data-group="category">
71
+ <option lb-cell="label"></option>
72
+ </template>
73
+ </lb-options>
74
+ ```
75
+
76
+ | Parameter | Fills |
77
+ | ---------- | ----- |
78
+ | `exp-label` | the visible `<label>` text |
79
+
80
+ - The row's key becomes the option's `value`: the `lb-key-value` the hub
81
+ stamps on each option is copied across, so the page names the key column
82
+ once, with `lb-key`.
83
+ - `data-group` on the row template sections the options into `<optgroup>`s,
84
+ one per distinct value, created and removed as rows arrive and leave.
85
+ - `lb-value` selects the option with that key, including one that arrives
86
+ after the value did.
87
+ - A `change` sends the action named by `lb-action`, value from the
88
+ select's `.value`. With `lb-query-parm` instead, the hub writes the
89
+ choice into the URL.
90
+
91
+ `lb-picker` is this same class with its row template supplied by the
92
+ definition instead of the page — see below.
93
+
94
+ ## `lb-table`
95
+
96
+ A `<table>` that supplies its own scaffolding; the author supplies the
97
+ heading row, the row template, and optionally a footer, each as a
98
+ `<template>` matched to a destination:
99
+
100
+ ```html
101
+ <lb-table lb-list="ledger" exp-caption="Ledger">
102
+ <template lb-template="head">
103
+ <tr><th>Date</th><th>Amount</th></tr>
104
+ </template>
105
+ <template lb-key="id" data-sort="date" data-group="month">
106
+ <tr><td lb-cell="date"></td><td lb-cell="amount"></td></tr>
107
+ </template>
108
+ <template lb-template="foot">
109
+ <tr lb-row="ledger-total"><td>Total</td><td lb-cell="total"></td></tr>
110
+ </template>
111
+ </lb-table>
112
+ ```
113
+
114
+ | Parameter | Fills |
115
+ | ---------- | ----- |
116
+ | `exp-caption` | the `<caption>` text |
117
+
118
+ | Destination | Fills |
119
+ | ------------ | ----- |
120
+ | `head` (`lb-template="head"`) | the `<thead>` content |
121
+ | `foot` (`lb-template="foot"`) | the `<tfoot>` content |
122
+ | slot (no `lb-template`) | the row template, via `lb-slot` on `<tbody>` |
123
+
124
+ - `data-group` on the row template sections rows under a derived heading row,
125
+ one per distinct value, whose `colSpan` matches the row's own column
126
+ count. `data-sort` orders rows within a section (or the whole body, with no
127
+ grouping) by comparing each row's cell text.
128
+ - The `foot` destination is not delivered through `lbPlaceRow` — it's an
129
+ ordinary scope carrying its own `lb-row`, resolved by name like any
130
+ other on the page. A grand total is a second query over the same
131
+ data, not a row the hub hands the table.
132
+
133
+ ## `lb-picker`
134
+
135
+ `lb-options`, with the row template supplied by the definition instead of
136
+ the page — for when every row is one option and nothing else varies:
137
+
138
+ ```html
139
+ <lb-picker
140
+ lb-list="statuses"
141
+ exp-label="Status"
142
+ exp-key="id"
143
+ exp-cell="label"
144
+ exp-group="category"
145
+ lb-action="setStatus"
146
+ ></lb-picker>
147
+ ```
148
+
149
+ | Parameter | Fills |
150
+ | ---------- | ----- |
151
+ | `exp-label` | the visible `<label>` text |
152
+ | `exp-key` | the row template's `lb-key` |
153
+ | `exp-cell` | the option's `lb-cell` |
154
+ | `exp-group` | the row template's `data-group` |
155
+
156
+ Behavior — grouping, key-as-value, the action on change — is inherited
157
+ whole from `lb-options`; a page author who needs a second element in the
158
+ row, or an option built from two columns, writes `lb-options` and its own
159
+ `<template>` instead.
160
+
161
+ ## `lb-unknown-page`
162
+
163
+ The chrome's dialog for a URL that names no page, as one tag:
164
+
165
+ ```html
166
+ <lb-hub>
167
+ <nav>...</nav>
168
+ <main></main>
169
+ <lb-unknown-page></lb-unknown-page>
170
+ </lb-hub>
171
+ ```
172
+
173
+ It expands to a `<dialog lb-unknown-page>` scoped to the hub's own
174
+ `lb-navigation` query, so `page-label` and `page-uri` land in it the way any
175
+ cell lands anywhere, and the hub opens it on a miss. It takes no parameters
176
+ and, so far, shows both cells rather than choosing between them. See
177
+ [`chrome.html`](./chrome.md#where-the-page-is) for the query, and for the same
178
+ dialog written by hand.