@loadbare/app 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +95 -0
  3. package/dist/build/assemble.d.ts +31 -0
  4. package/dist/build/assemble.d.ts.map +1 -0
  5. package/dist/build/assemble.js +53 -0
  6. package/dist/build/cli.d.ts +30 -0
  7. package/dist/build/cli.d.ts.map +1 -0
  8. package/dist/build/cli.js +107 -0
  9. package/dist/build/elements.d.ts +61 -0
  10. package/dist/build/elements.d.ts.map +1 -0
  11. package/dist/build/elements.js +158 -0
  12. package/dist/build/expand.d.ts +45 -0
  13. package/dist/build/expand.d.ts.map +1 -0
  14. package/dist/build/expand.js +386 -0
  15. package/dist/build/format.d.ts +28 -0
  16. package/dist/build/format.d.ts.map +1 -0
  17. package/dist/build/format.js +42 -0
  18. package/dist/build/locations.d.ts +87 -0
  19. package/dist/build/locations.d.ts.map +1 -0
  20. package/dist/build/locations.js +173 -0
  21. package/dist/build/package-root.d.ts +9 -0
  22. package/dist/build/package-root.d.ts.map +1 -0
  23. package/dist/build/package-root.js +24 -0
  24. package/dist/build/pages.d.ts +25 -0
  25. package/dist/build/pages.d.ts.map +1 -0
  26. package/dist/build/pages.js +54 -0
  27. package/dist/build/styles.d.ts +13 -0
  28. package/dist/build/styles.d.ts.map +1 -0
  29. package/dist/build/styles.js +18 -0
  30. package/dist/client.js +522 -0
  31. package/dist/core/lb-constants.d.ts +23 -0
  32. package/dist/core/lb-constants.d.ts.map +1 -0
  33. package/dist/core/lb-constants.js +95 -0
  34. package/dist/core/lb-types.d.ts +88 -0
  35. package/dist/core/lb-types.d.ts.map +1 -0
  36. package/dist/core/lb-types.js +5 -0
  37. package/dist/demo-static/src/widgets/app-box.d.ts +15 -0
  38. package/dist/demo-static/src/widgets/app-box.d.ts.map +1 -0
  39. package/dist/demo-static/src/widgets/app-box.js +19 -0
  40. package/dist/hub/lb-apply.d.ts +13 -0
  41. package/dist/hub/lb-apply.d.ts.map +1 -0
  42. package/dist/hub/lb-apply.js +77 -0
  43. package/dist/hub/lb-hub.d.ts +2 -0
  44. package/dist/hub/lb-hub.d.ts.map +1 -0
  45. package/dist/hub/lb-hub.js +242 -0
  46. package/dist/hub/lb-rows.d.ts +18 -0
  47. package/dist/hub/lb-rows.d.ts.map +1 -0
  48. package/dist/hub/lb-rows.js +106 -0
  49. package/dist/server/lb-express.d.ts +28 -0
  50. package/dist/server/lb-express.d.ts.map +1 -0
  51. package/dist/server/lb-express.js +77 -0
  52. package/dist/server/lb-server.d.ts +174 -0
  53. package/dist/server/lb-server.d.ts.map +1 -0
  54. package/dist/server/lb-server.js +79 -0
  55. package/dist/tests/assemble.test.d.ts +8 -0
  56. package/dist/tests/assemble.test.d.ts.map +1 -0
  57. package/dist/tests/assemble.test.js +51 -0
  58. package/dist/tests/elements.test.d.ts +8 -0
  59. package/dist/tests/elements.test.d.ts.map +1 -0
  60. package/dist/tests/elements.test.js +111 -0
  61. package/dist/tests/expand.test.d.ts +10 -0
  62. package/dist/tests/expand.test.d.ts.map +1 -0
  63. package/dist/tests/expand.test.js +226 -0
  64. package/dist/tests/fixtures/elements/collision/elements.d.ts +5 -0
  65. package/dist/tests/fixtures/elements/collision/elements.d.ts.map +1 -0
  66. package/dist/tests/fixtures/elements/collision/elements.js +3 -0
  67. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts +2 -0
  68. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts.map +1 -0
  69. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.js +1 -0
  70. package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts +2 -0
  71. package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts.map +1 -0
  72. package/dist/tests/fixtures/elements/local/widgets/app-box.js +1 -0
  73. package/dist/tests/fixtures/elements/manifest/elements.d.ts +5 -0
  74. package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +1 -0
  75. package/dist/tests/fixtures/elements/manifest/elements.js +3 -0
  76. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +5 -0
  77. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +1 -0
  78. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +3 -0
  79. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +5 -0
  80. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +1 -0
  81. package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +3 -0
  82. package/dist/tests/golden.test.d.ts +19 -0
  83. package/dist/tests/golden.test.d.ts.map +1 -0
  84. package/dist/tests/golden.test.js +60 -0
  85. package/dist/tests/helpers/console.d.ts +20 -0
  86. package/dist/tests/helpers/console.d.ts.map +1 -0
  87. package/dist/tests/helpers/console.js +28 -0
  88. package/dist/tests/helpers/dom.d.ts +18 -0
  89. package/dist/tests/helpers/dom.d.ts.map +1 -0
  90. package/dist/tests/helpers/dom.js +22 -0
  91. package/dist/tests/helpers/window.d.ts +43 -0
  92. package/dist/tests/helpers/window.d.ts.map +1 -0
  93. package/dist/tests/helpers/window.js +78 -0
  94. package/dist/tests/lb-apply.test.d.ts +8 -0
  95. package/dist/tests/lb-apply.test.d.ts.map +1 -0
  96. package/dist/tests/lb-apply.test.js +153 -0
  97. package/dist/tests/lb-express.test.d.ts +14 -0
  98. package/dist/tests/lb-express.test.d.ts.map +1 -0
  99. package/dist/tests/lb-express.test.js +238 -0
  100. package/dist/tests/lb-input.test.d.ts +9 -0
  101. package/dist/tests/lb-input.test.d.ts.map +1 -0
  102. package/dist/tests/lb-input.test.js +78 -0
  103. package/dist/tests/lb-list.test.d.ts +12 -0
  104. package/dist/tests/lb-list.test.d.ts.map +1 -0
  105. package/dist/tests/lb-list.test.js +44 -0
  106. package/dist/tests/lb-options.test.d.ts +10 -0
  107. package/dist/tests/lb-options.test.d.ts.map +1 -0
  108. package/dist/tests/lb-options.test.js +121 -0
  109. package/dist/tests/lb-picker.test.d.ts +14 -0
  110. package/dist/tests/lb-picker.test.d.ts.map +1 -0
  111. package/dist/tests/lb-picker.test.js +59 -0
  112. package/dist/tests/lb-rows.test.d.ts +12 -0
  113. package/dist/tests/lb-rows.test.d.ts.map +1 -0
  114. package/dist/tests/lb-rows.test.js +336 -0
  115. package/dist/tests/lb-select.test.d.ts +9 -0
  116. package/dist/tests/lb-select.test.d.ts.map +1 -0
  117. package/dist/tests/lb-select.test.js +71 -0
  118. package/dist/tests/lb-server.test.d.ts +9 -0
  119. package/dist/tests/lb-server.test.d.ts.map +1 -0
  120. package/dist/tests/lb-server.test.js +495 -0
  121. package/dist/tests/lb-table.test.d.ts +15 -0
  122. package/dist/tests/lb-table.test.d.ts.map +1 -0
  123. package/dist/tests/lb-table.test.js +205 -0
  124. package/dist/tests/pages.test.d.ts +6 -0
  125. package/dist/tests/pages.test.d.ts.map +1 -0
  126. package/dist/tests/pages.test.js +98 -0
  127. package/dist/tests/styles.test.d.ts +7 -0
  128. package/dist/tests/styles.test.d.ts.map +1 -0
  129. package/dist/tests/styles.test.js +73 -0
  130. package/dist/widgets/index.d.ts +7 -0
  131. package/dist/widgets/index.d.ts.map +1 -0
  132. package/dist/widgets/index.js +6 -0
  133. package/dist/widgets/lb-input.d.ts +2 -0
  134. package/dist/widgets/lb-input.d.ts.map +1 -0
  135. package/dist/widgets/lb-input.js +48 -0
  136. package/dist/widgets/lb-list.d.ts +2 -0
  137. package/dist/widgets/lb-list.d.ts.map +1 -0
  138. package/dist/widgets/lb-list.js +17 -0
  139. package/dist/widgets/lb-options.d.ts +26 -0
  140. package/dist/widgets/lb-options.d.ts.map +1 -0
  141. package/dist/widgets/lb-options.js +72 -0
  142. package/dist/widgets/lb-picker.d.ts +2 -0
  143. package/dist/widgets/lb-picker.d.ts.map +1 -0
  144. package/dist/widgets/lb-picker.js +25 -0
  145. package/dist/widgets/lb-select.d.ts +2 -0
  146. package/dist/widgets/lb-select.d.ts.map +1 -0
  147. package/dist/widgets/lb-select.js +43 -0
  148. package/dist/widgets/lb-table.d.ts +2 -0
  149. package/dist/widgets/lb-table.d.ts.map +1 -0
  150. package/dist/widgets/lb-table.js +113 -0
  151. package/docs/application-chrome.md +36 -0
  152. package/docs/building-html-pages.md +130 -0
  153. package/docs/getting-started.md +120 -0
  154. package/docs/guide.md +1164 -0
  155. package/docs/hosting.md +218 -0
  156. package/docs/latent-risks.md +20 -0
  157. package/docs/theory.md +226 -0
  158. package/package.json +85 -0
  159. package/widgets/index.ts +6 -0
  160. package/widgets/lb-input.html +1 -0
  161. package/widgets/lb-input.ts +64 -0
  162. package/widgets/lb-list.html +1 -0
  163. package/widgets/lb-list.ts +21 -0
  164. package/widgets/lb-options.html +4 -0
  165. package/widgets/lb-options.ts +88 -0
  166. package/widgets/lb-picker.html +7 -0
  167. package/widgets/lb-picker.ts +27 -0
  168. package/widgets/lb-select.html +4 -0
  169. package/widgets/lb-select.ts +55 -0
  170. package/widgets/lb-table.html +8 -0
  171. package/widgets/lb-table.ts +126 -0
@@ -0,0 +1,88 @@
1
+ /// <reference lib="dom" />
2
+
3
+ import {
4
+ ATTR_ACTION,
5
+ ATTR_GROUP,
6
+ ATTR_KEY,
7
+ LB_EVENT_NAME,
8
+ } from "../core/lb-constants";
9
+ import type {
10
+ Projection,
11
+ HubRequest,
12
+ HubRowHost,
13
+ } from "../core/lb-types";
14
+ import { applyRows, type Place } from "../hub/lb-rows";
15
+
16
+ /**
17
+ * A `<select>` whose choices come from a query, and the case that shows why
18
+ * placement belongs to the widget rather than to the framework.
19
+ *
20
+ * The rows arrive flat and keyed. Nothing on the wire knows about
21
+ * `<optgroup>`, and nothing needs to: a group is a column like any other, and
22
+ * turning a repeated column value into an element is exactly the local
23
+ * knowledge a widget has and the hub does not. A table wanting section
24
+ * headings would read the same `lb-group` and build something else.
25
+ *
26
+ * The identity of a row is the value of the option, so `lb-key` supplies
27
+ * both and the page declares it once.
28
+ *
29
+ * Exported because `lb-picker` is this widget with its row template
30
+ * supplied by the definition instead of by the page. Nothing about the
31
+ * behavior changes, so nothing about the behavior is written twice.
32
+ */
33
+ export class LbOptions extends HTMLElement implements HubRowHost {
34
+ acceptRows(result: Projection) {
35
+ applyRows(this, result, this.place);
36
+ // The identity of the row is the value of the option, so nothing on the
37
+ // page has to say so twice.
38
+ for (const option of this.querySelectorAll(`option[${ATTR_KEY}]`)) {
39
+ option.setAttribute("value", option.getAttribute(ATTR_KEY)!);
40
+ }
41
+ // A group is derived, so it goes when its last row does.
42
+ for (const group of this.querySelectorAll("optgroup")) {
43
+ if (!group.querySelector("option")) group.remove();
44
+ }
45
+ }
46
+
47
+ private place: Place = (row, tuple, template) => {
48
+ const select = this.querySelector("select")!;
49
+ const groupCell = template.getAttribute(ATTR_GROUP);
50
+ if (!groupCell) {
51
+ select.insertBefore(row, template);
52
+ return;
53
+ }
54
+ this.groupFor(select, template, tuple[groupCell] ?? "").append(row);
55
+ };
56
+
57
+ /** The group with this label, or a new one in front of the template. */
58
+ private groupFor(select: Element, template: Element, label: string): Element {
59
+ for (const group of select.querySelectorAll("optgroup")) {
60
+ if (group.label === label) return group;
61
+ }
62
+ const group = document.createElement("optgroup");
63
+ group.label = label;
64
+ select.insertBefore(group, template);
65
+ return group;
66
+ }
67
+
68
+ connectedCallback() {
69
+ this.addEventListener("change", () => {
70
+ const select = this.querySelector("select");
71
+ if (!select) {
72
+ console.error(`lb-options: change with no <select>, ignoring`);
73
+ return;
74
+ }
75
+ const name = this.getAttribute(ATTR_ACTION);
76
+ if (!name) {
77
+ console.error(`lb-options: no ${ATTR_ACTION}, nothing to send`);
78
+ return;
79
+ }
80
+ const detail: HubRequest = { op: "action", name, value: select.value };
81
+ this.dispatchEvent(
82
+ new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),
83
+ );
84
+ });
85
+ }
86
+ }
87
+
88
+ customElements.define("lb-options", LbOptions);
@@ -0,0 +1,7 @@
1
+ <label
2
+ >{{label}}
3
+ <select>
4
+ <template lb-key="{{key}}" lb-group="{{group}}">
5
+ <option lb-cell="{{cell}}"></option>
6
+ </template></select
7
+ ></label>
@@ -0,0 +1,27 @@
1
+ /// <reference lib="dom" />
2
+
3
+ import { LbOptions } from "./lb-options";
4
+
5
+ /**
6
+ * The same `<select>` as `lb-options`, with the row template supplied by
7
+ * the definition instead of by the page.
8
+ *
9
+ * A row template is markup, so writing it is the page author's job — until
10
+ * every row is one option and the only thing that varies is which columns it
11
+ * reads. Those are words, and words are parameters: `exp-key`, `exp-cell` and
12
+ * `exp-group` fill the placeholders in the template the definition already
13
+ * has, at build time, and the page writes one tag with no children.
14
+ *
15
+ * It is the same trade as anywhere else in expansion. A page author who wants
16
+ * a second element in the row, or an option built from two columns, writes
17
+ * `lb-options` and the template themselves. This one is for when they do
18
+ * not.
19
+ *
20
+ * The behavior is inherited whole, because it is the same behavior: rows land
21
+ * in groups, the key becomes the value, and a change sends the declared
22
+ * action. A distinct class exists only because `customElements.define`
23
+ * requires one constructor per name.
24
+ */
25
+ class LbPicker extends LbOptions {}
26
+
27
+ customElements.define("lb-picker", LbPicker);
@@ -0,0 +1,4 @@
1
+ <label
2
+ >{{label}}
3
+ <select lb-slot></select
4
+ ></label>
@@ -0,0 +1,55 @@
1
+ /// <reference lib="dom" />
2
+
3
+ import {
4
+ ATTR_ACTION,
5
+ ATTR_VALUE,
6
+ LB_EVENT_NAME,
7
+ } from "../core/lb-constants";
8
+ import type { HubRequest } from "../core/lb-types";
9
+
10
+ /**
11
+ * A leaf cell wrapping a <select>. It receives its value like any other cell,
12
+ * and on change it sends the action the page declared for it — the request
13
+ * vocabulary and the addressing vocabulary running in both directions on one
14
+ * widget.
15
+ *
16
+ * The choice is the interaction, so this is the case where an action carries
17
+ * a value. The server still decides what that choice means.
18
+ */
19
+ class LbSelect extends HTMLElement {
20
+ static observedAttributes = [ATTR_VALUE];
21
+
22
+ attributeChangedCallback(_name: string, _old: string, value: string) {
23
+ const select = this.querySelector("select");
24
+ if (!select) {
25
+ console.error(`lb-select: no <select> to receive the value`);
26
+ return;
27
+ }
28
+ select.value = value;
29
+ }
30
+
31
+ connectedCallback() {
32
+ this.addEventListener("change", () => {
33
+ const select = this.querySelector("select");
34
+ if (!select) {
35
+ console.error(`lb-select: change with no <select>, ignoring`);
36
+ return;
37
+ }
38
+ const name = this.getAttribute(ATTR_ACTION);
39
+ if (!name) {
40
+ console.error(`lb-select: no ${ATTR_ACTION}, nothing to send`);
41
+ return;
42
+ }
43
+ const detail: HubRequest = {
44
+ op: "action",
45
+ name,
46
+ value: select.value,
47
+ };
48
+ this.dispatchEvent(
49
+ new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),
50
+ );
51
+ });
52
+ }
53
+ }
54
+
55
+ customElements.define("lb-select", LbSelect);
@@ -0,0 +1,8 @@
1
+ <table>
2
+ <caption>
3
+ {{caption}}
4
+ </caption>
5
+ <thead lb-template="head"></thead>
6
+ <tbody lb-slot></tbody>
7
+ <tfoot lb-template="foot"></tfoot>
8
+ </table>
@@ -0,0 +1,126 @@
1
+ /// <reference lib="dom" />
2
+
3
+ import {
4
+ ATTR_CELL,
5
+ ATTR_GROUP,
6
+ ATTR_KEY,
7
+ ATTR_SORT,
8
+ } from "../core/lb-constants";
9
+ import type { Projection, HubRowHost } from "../core/lb-types";
10
+ import { applyRows, type Place } from "../hub/lb-rows";
11
+
12
+ /**
13
+ * A table that owns its own scaffolding, and the case that shows what a list
14
+ * widget is for once the repeater exists.
15
+ *
16
+ * `lb-list` goes around a table the page author wrote. This one supplies
17
+ * the table, and the page writes what only the page knows: the heading row
18
+ * and the row template. That division is forced rather than chosen — a
19
+ * `<thead>` written inside a custom element is not inside a table, so the
20
+ * parser drops the tag and keeps the text. A `<template>` survives anywhere,
21
+ * so the heading row arrives wrapped and expansion unwraps it into the
22
+ * `lb-template` destination beside the slot.
23
+ *
24
+ * What it adds at run time is placement, and only placement. `lb-sort` names
25
+ * the column rows are ordered by; `lb-group` names the column they are
26
+ * sectioned by, and is the same attribute a grouped `<select>` reads, because
27
+ * a table with section headings and an `<optgroup>` are asking the same thing
28
+ * of the same data.
29
+ *
30
+ * The `foot` destination is the head's argument a second time and needs no
31
+ * code here. A `<tfoot>` is a place on the page, not a row the hub delivers,
32
+ * so what lands in it is an ordinary scope carrying its own `lb-query` — a
33
+ * grand total is a second projection of the same table, and the hub resolves
34
+ * it by name like any other. See demo/pages/ledger.html.
35
+ */
36
+
37
+ /**
38
+ * The heading row of a section, carrying the value it stands for.
39
+ *
40
+ * The widget's own scaffolding, so it is HTML's namespace rather than
41
+ * Loadbare App's: it is derived from rows the hub knows and is invisible to the hub
42
+ * itself, exactly as the `<optgroup>` in `lb-options` is. Nothing
43
+ * addresses it, so it carries no `lb-key` and no name from the vocabulary.
44
+ */
45
+ const GROUP_ROW = "data-group";
46
+
47
+ class LbTable extends HTMLElement implements HubRowHost {
48
+ acceptRows(result: Projection) {
49
+ applyRows(this, result, this.place);
50
+ // A section is derived, so it goes when its last row does.
51
+ for (const heading of this.querySelectorAll(`tr[${GROUP_ROW}]`)) {
52
+ if (this.section(heading).rows.length === 0) heading.remove();
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Group first, then sort within the group.
58
+ *
59
+ * Inserting a row before the first one that sorts after it keeps the
60
+ * section ordered, whatever else is in it — which is what lets a whole set
61
+ * and a single patched row take the same path.
62
+ */
63
+ private place: Place = (row, tuple, template) => {
64
+ const groupCell = template.getAttribute(ATTR_GROUP);
65
+ const sortCell = template.getAttribute(ATTR_SORT);
66
+ const heading = groupCell
67
+ ? this.headingFor(template, tuple[groupCell] ?? "")
68
+ : null;
69
+ const { rows, end } = this.section(heading);
70
+
71
+ const value = sortCell ? (tuple[sortCell] ?? "") : "";
72
+ const after = sortCell
73
+ ? rows.find((other) => other !== row && cellOf(other, sortCell) > value)
74
+ : undefined;
75
+ template.parentElement!.insertBefore(row, after ?? end);
76
+ };
77
+
78
+ /**
79
+ * The rows of one section, and the node it ends at.
80
+ *
81
+ * A section runs from its heading to the next heading, or to the template
82
+ * if there is none after it. Without grouping there is one section and it
83
+ * is the whole body — the heading row the page wrote is skipped along the
84
+ * way, because it carries no key and is therefore not a row.
85
+ */
86
+ private section(heading: Element | null): { rows: Element[]; end: Element } {
87
+ const template = this.querySelector("template")!;
88
+ const rows: Element[] = [];
89
+ let node = heading
90
+ ? heading.nextElementSibling
91
+ : template.parentElement!.firstElementChild;
92
+ while (node && node !== template && !node.hasAttribute(GROUP_ROW)) {
93
+ if (node.hasAttribute(ATTR_KEY)) rows.push(node);
94
+ node = node.nextElementSibling;
95
+ }
96
+ return { rows, end: node ?? template };
97
+ }
98
+
99
+ /** The section with this label, or a new one in front of the template. */
100
+ private headingFor(template: HTMLTemplateElement, label: string): Element {
101
+ const body = template.parentElement!;
102
+ for (const heading of body.querySelectorAll(`tr[${GROUP_ROW}]`)) {
103
+ if (heading.getAttribute(GROUP_ROW) === label) return heading;
104
+ }
105
+ const heading = document.createElement("tr");
106
+ heading.setAttribute(GROUP_ROW, label);
107
+ const cell = document.createElement("th");
108
+ cell.setAttribute("scope", "rowgroup");
109
+ // How wide the table is, from the row the page author wrote. Nothing
110
+ // else knows, and nothing had to be told twice.
111
+ cell.colSpan = template.content.firstElementChild?.childElementCount ?? 1;
112
+ cell.textContent = label;
113
+ heading.append(cell);
114
+ body.insertBefore(heading, template);
115
+ return heading;
116
+ }
117
+ }
118
+
119
+ /** What a live row is showing in one column. */
120
+ function cellOf(row: Element, cell: string): string {
121
+ const selector = `[${ATTR_CELL}="${cell}"]`;
122
+ const el = row.matches(selector) ? row : row.querySelector(selector);
123
+ return el?.textContent ?? "";
124
+ }
125
+
126
+ customElements.define("lb-table", LbTable);