@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
package/dist/client.js ADDED
@@ -0,0 +1,522 @@
1
+ "use strict";
2
+ (() => {
3
+ // core/lb-constants.ts
4
+ var ATTR_QUERY = "lb-query";
5
+ var ATTR_KEY = "lb-key";
6
+ var ATTR_CELL = "lb-cell";
7
+ var ATTR_VALUE = "lb-value";
8
+ var ATTR_GROUP = "lb-group";
9
+ var ATTR_SORT = "lb-sort";
10
+ var ATTR_ROW_COUNT = "data-rows";
11
+ var LB_EVENT_NAME = "lb-request";
12
+ var ATTR_ACTION = "lb-action";
13
+ var ATTR_DELETE = "lb-delete";
14
+ var ATTR_INSERT = "lb-insert";
15
+ var ATTR_UPDATE = "lb-update";
16
+ var ATTR_NAV_LINK = "lb-nav-link";
17
+ var PAGE_TEMPLATE_PREFIX = "page-";
18
+ var HUB_TAG_NAME = "lb-hub";
19
+ var ATTR_PENDING = "data-lb-pending";
20
+ var ATTR_ERROR = "data-lb-error";
21
+ var LB_DATA_ENDPOINT = "/lb/data";
22
+ var LB_REQUEST_ENDPOINT = "/lb";
23
+ var LB_REQUEST_TIMEOUT_MS = 1e4;
24
+
25
+ // widgets/lb-input.ts
26
+ var LbInput = class extends HTMLElement {
27
+ static observedAttributes = [ATTR_VALUE];
28
+ attributeChangedCallback(_name, _old, value) {
29
+ const input = this.querySelector("input");
30
+ if (!input) {
31
+ console.error(`lb-input: no <input> to receive the value`);
32
+ return;
33
+ }
34
+ input.value = value;
35
+ }
36
+ connectedCallback() {
37
+ this.addEventListener("change", () => {
38
+ const input = this.querySelector("input");
39
+ if (!input) {
40
+ console.error(`lb-input: change with no <input>, ignoring`);
41
+ return;
42
+ }
43
+ const query = this.closest(`[${ATTR_QUERY}]`)?.getAttribute(ATTR_QUERY);
44
+ const key = this.closest(`[${ATTR_KEY}]`)?.getAttribute(ATTR_KEY);
45
+ const cell = this.getAttribute(ATTR_CELL);
46
+ if (!query || !key || !cell) {
47
+ console.error(
48
+ `lb-input: change with no query/key/cell coordinates, ignoring`
49
+ );
50
+ return;
51
+ }
52
+ const detail = {
53
+ op: "cell-change",
54
+ query,
55
+ key,
56
+ cell,
57
+ value: input.value
58
+ };
59
+ this.dispatchEvent(
60
+ new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
61
+ );
62
+ });
63
+ }
64
+ };
65
+ customElements.define("lb-input", LbInput);
66
+
67
+ // widgets/lb-select.ts
68
+ var LbSelect = class extends HTMLElement {
69
+ static observedAttributes = [ATTR_VALUE];
70
+ attributeChangedCallback(_name, _old, value) {
71
+ const select = this.querySelector("select");
72
+ if (!select) {
73
+ console.error(`lb-select: no <select> to receive the value`);
74
+ return;
75
+ }
76
+ select.value = value;
77
+ }
78
+ connectedCallback() {
79
+ this.addEventListener("change", () => {
80
+ const select = this.querySelector("select");
81
+ if (!select) {
82
+ console.error(`lb-select: change with no <select>, ignoring`);
83
+ return;
84
+ }
85
+ const name = this.getAttribute(ATTR_ACTION);
86
+ if (!name) {
87
+ console.error(`lb-select: no ${ATTR_ACTION}, nothing to send`);
88
+ return;
89
+ }
90
+ const detail = {
91
+ op: "action",
92
+ name,
93
+ value: select.value
94
+ };
95
+ this.dispatchEvent(
96
+ new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
97
+ );
98
+ });
99
+ }
100
+ };
101
+ customElements.define("lb-select", LbSelect);
102
+
103
+ // core/lb-types.ts
104
+ function isProjection(result) {
105
+ const op = result.op;
106
+ return op === "rows" || op === "patch";
107
+ }
108
+
109
+ // hub/lb-apply.ts
110
+ function land(el, value) {
111
+ if (el.localName.includes("-")) el.setAttribute(ATTR_VALUE, value);
112
+ else el.textContent = value;
113
+ }
114
+ function applyTuple(root, cells) {
115
+ for (const [cell, value] of Object.entries(cells)) {
116
+ const selector = `[${ATTR_CELL}="${cell}"]`;
117
+ if (root.matches(selector)) land(root, value);
118
+ for (const el of root.querySelectorAll(selector)) land(el, value);
119
+ }
120
+ }
121
+ function landRows(scope, query, result) {
122
+ const host = scope;
123
+ if (typeof host.acceptRows !== "function") {
124
+ console.error(
125
+ `lb-hub: query '${query}' returned rows, but <${scope.localName}> is not a list widget`
126
+ );
127
+ return;
128
+ }
129
+ host.acceptRows(result);
130
+ }
131
+ function applyData(root, data) {
132
+ for (const [query, result] of Object.entries(data)) {
133
+ const scopes = root.querySelectorAll(`[${ATTR_QUERY}="${query}"]`);
134
+ if (scopes.length === 0) {
135
+ console.warn(`lb-hub: no scope for query '${query}', skipping`);
136
+ continue;
137
+ }
138
+ for (const scope of scopes) {
139
+ if (isProjection(result)) landRows(scope, query, result);
140
+ else applyTuple(scope, result);
141
+ }
142
+ }
143
+ }
144
+
145
+ // hub/lb-rows.ts
146
+ var before = (row, _tuple, template) => {
147
+ template.parentElement.insertBefore(row, template);
148
+ };
149
+ function templateIn(scope) {
150
+ const template = scope.querySelector("template");
151
+ if (!template) {
152
+ console.error(
153
+ `lb-rows: <${scope.localName}> has no <template> to clone`
154
+ );
155
+ return null;
156
+ }
157
+ if (!template.getAttribute(ATTR_KEY)) {
158
+ console.error(
159
+ `lb-rows: the <template> in <${scope.localName}> has no ${ATTR_KEY} naming the column that identifies a row`
160
+ );
161
+ return null;
162
+ }
163
+ return template;
164
+ }
165
+ function showing(scope, template) {
166
+ const rows = /* @__PURE__ */ new Map();
167
+ for (const el of scope.querySelectorAll(`[${ATTR_KEY}]`)) {
168
+ if (el !== template) rows.set(el.getAttribute(ATTR_KEY), el);
169
+ }
170
+ return rows;
171
+ }
172
+ function applyRows(scope, result, place = before) {
173
+ const template = templateIn(scope);
174
+ if (!template) return;
175
+ const keyCell = template.getAttribute(ATTR_KEY);
176
+ const rows = showing(scope, template);
177
+ const upsert = (tuple) => {
178
+ const key = tuple[keyCell];
179
+ if (key === void 0) {
180
+ console.error(
181
+ `lb-rows: a tuple for <${scope.localName}> has no '${keyCell}' cell`
182
+ );
183
+ return null;
184
+ }
185
+ let row = rows.get(key);
186
+ const fresh = row === void 0;
187
+ if (!row) {
188
+ row = template.content.firstElementChild.cloneNode(true);
189
+ row.setAttribute(ATTR_KEY, key);
190
+ rows.set(key, row);
191
+ }
192
+ applyTuple(row, tuple);
193
+ if (fresh || result.op === "rows") place(row, tuple, template);
194
+ return key;
195
+ };
196
+ if (result.op === "rows") {
197
+ const arrived = /* @__PURE__ */ new Set();
198
+ for (const tuple of result.rows) {
199
+ const key = upsert(tuple);
200
+ if (key !== null) arrived.add(key);
201
+ }
202
+ for (const [key, row] of rows) if (!arrived.has(key)) row.remove();
203
+ } else {
204
+ for (const tuple of result.rows ?? []) upsert(tuple);
205
+ for (const key of result.drop ?? []) rows.get(key)?.remove();
206
+ }
207
+ scope.setAttribute(ATTR_ROW_COUNT, String(showing(scope, template).size));
208
+ }
209
+
210
+ // widgets/lb-list.ts
211
+ var LbList = class extends HTMLElement {
212
+ acceptRows(result) {
213
+ applyRows(this, result);
214
+ }
215
+ };
216
+ customElements.define("lb-list", LbList);
217
+
218
+ // widgets/lb-options.ts
219
+ var LbOptions = class extends HTMLElement {
220
+ acceptRows(result) {
221
+ applyRows(this, result, this.place);
222
+ for (const option of this.querySelectorAll(`option[${ATTR_KEY}]`)) {
223
+ option.setAttribute("value", option.getAttribute(ATTR_KEY));
224
+ }
225
+ for (const group of this.querySelectorAll("optgroup")) {
226
+ if (!group.querySelector("option")) group.remove();
227
+ }
228
+ }
229
+ place = (row, tuple, template) => {
230
+ const select = this.querySelector("select");
231
+ const groupCell = template.getAttribute(ATTR_GROUP);
232
+ if (!groupCell) {
233
+ select.insertBefore(row, template);
234
+ return;
235
+ }
236
+ this.groupFor(select, template, tuple[groupCell] ?? "").append(row);
237
+ };
238
+ /** The group with this label, or a new one in front of the template. */
239
+ groupFor(select, template, label) {
240
+ for (const group2 of select.querySelectorAll("optgroup")) {
241
+ if (group2.label === label) return group2;
242
+ }
243
+ const group = document.createElement("optgroup");
244
+ group.label = label;
245
+ select.insertBefore(group, template);
246
+ return group;
247
+ }
248
+ connectedCallback() {
249
+ this.addEventListener("change", () => {
250
+ const select = this.querySelector("select");
251
+ if (!select) {
252
+ console.error(`lb-options: change with no <select>, ignoring`);
253
+ return;
254
+ }
255
+ const name = this.getAttribute(ATTR_ACTION);
256
+ if (!name) {
257
+ console.error(`lb-options: no ${ATTR_ACTION}, nothing to send`);
258
+ return;
259
+ }
260
+ const detail = { op: "action", name, value: select.value };
261
+ this.dispatchEvent(
262
+ new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
263
+ );
264
+ });
265
+ }
266
+ };
267
+ customElements.define("lb-options", LbOptions);
268
+
269
+ // widgets/lb-table.ts
270
+ var GROUP_ROW = "data-group";
271
+ var LbTable = class extends HTMLElement {
272
+ acceptRows(result) {
273
+ applyRows(this, result, this.place);
274
+ for (const heading of this.querySelectorAll(`tr[${GROUP_ROW}]`)) {
275
+ if (this.section(heading).rows.length === 0) heading.remove();
276
+ }
277
+ }
278
+ /**
279
+ * Group first, then sort within the group.
280
+ *
281
+ * Inserting a row before the first one that sorts after it keeps the
282
+ * section ordered, whatever else is in it — which is what lets a whole set
283
+ * and a single patched row take the same path.
284
+ */
285
+ place = (row, tuple, template) => {
286
+ const groupCell = template.getAttribute(ATTR_GROUP);
287
+ const sortCell = template.getAttribute(ATTR_SORT);
288
+ const heading = groupCell ? this.headingFor(template, tuple[groupCell] ?? "") : null;
289
+ const { rows, end } = this.section(heading);
290
+ const value = sortCell ? tuple[sortCell] ?? "" : "";
291
+ const after = sortCell ? rows.find((other) => other !== row && cellOf(other, sortCell) > value) : void 0;
292
+ template.parentElement.insertBefore(row, after ?? end);
293
+ };
294
+ /**
295
+ * The rows of one section, and the node it ends at.
296
+ *
297
+ * A section runs from its heading to the next heading, or to the template
298
+ * if there is none after it. Without grouping there is one section and it
299
+ * is the whole body — the heading row the page wrote is skipped along the
300
+ * way, because it carries no key and is therefore not a row.
301
+ */
302
+ section(heading) {
303
+ const template = this.querySelector("template");
304
+ const rows = [];
305
+ let node = heading ? heading.nextElementSibling : template.parentElement.firstElementChild;
306
+ while (node && node !== template && !node.hasAttribute(GROUP_ROW)) {
307
+ if (node.hasAttribute(ATTR_KEY)) rows.push(node);
308
+ node = node.nextElementSibling;
309
+ }
310
+ return { rows, end: node ?? template };
311
+ }
312
+ /** The section with this label, or a new one in front of the template. */
313
+ headingFor(template, label) {
314
+ const body = template.parentElement;
315
+ for (const heading2 of body.querySelectorAll(`tr[${GROUP_ROW}]`)) {
316
+ if (heading2.getAttribute(GROUP_ROW) === label) return heading2;
317
+ }
318
+ const heading = document.createElement("tr");
319
+ heading.setAttribute(GROUP_ROW, label);
320
+ const cell = document.createElement("th");
321
+ cell.setAttribute("scope", "rowgroup");
322
+ cell.colSpan = template.content.firstElementChild?.childElementCount ?? 1;
323
+ cell.textContent = label;
324
+ heading.append(cell);
325
+ body.insertBefore(heading, template);
326
+ return heading;
327
+ }
328
+ };
329
+ function cellOf(row, cell) {
330
+ const selector = `[${ATTR_CELL}="${cell}"]`;
331
+ const el = row.matches(selector) ? row : row.querySelector(selector);
332
+ return el?.textContent ?? "";
333
+ }
334
+ customElements.define("lb-table", LbTable);
335
+
336
+ // widgets/lb-picker.ts
337
+ var LbPicker = class extends LbOptions {
338
+ };
339
+ customElements.define("lb-picker", LbPicker);
340
+
341
+ // hub/lb-hub.ts
342
+ function coordinates(el) {
343
+ const query = el.closest(`[${ATTR_QUERY}]`)?.getAttribute(ATTR_QUERY);
344
+ const key = el.closest(`[${ATTR_KEY}]`)?.getAttribute(ATTR_KEY);
345
+ const cell = el.getAttribute(ATTR_CELL);
346
+ return {
347
+ ...query === null || query === void 0 ? {} : { query },
348
+ ...key === null || key === void 0 ? {} : { key },
349
+ ...cell === null ? {} : { cell }
350
+ };
351
+ }
352
+ function controlValue(el) {
353
+ const control = el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement ? el : el.querySelector("input, select, textarea");
354
+ return control?.value;
355
+ }
356
+ function gatherValues(form) {
357
+ const values = {};
358
+ for (const cell of form.querySelectorAll(`[${ATTR_CELL}]`)) {
359
+ const name = cell.getAttribute(ATTR_CELL);
360
+ const value = controlValue(cell);
361
+ if (value === void 0) {
362
+ console.warn(
363
+ `lb-hub: lb-cell '${name}' has no control to read, ignoring it`,
364
+ cell
365
+ );
366
+ continue;
367
+ }
368
+ values[name] = value;
369
+ }
370
+ return values;
371
+ }
372
+ function pageNameFor(path) {
373
+ const name = path.replace(/^\/+|\/+$/g, "");
374
+ return name === "" ? "counter" : name;
375
+ }
376
+ async function fetchData(url, init) {
377
+ const controller = new AbortController();
378
+ const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);
379
+ try {
380
+ const res = await fetch(url, { ...init, signal: controller.signal });
381
+ if (!res.ok) {
382
+ throw new Error(`lb-hub: server responded ${res.status} for ${url}`);
383
+ }
384
+ return await res.json();
385
+ } finally {
386
+ clearTimeout(timeout);
387
+ }
388
+ }
389
+ var LbHub = class extends HTMLElement {
390
+ main;
391
+ /**
392
+ * The page currently in <main>. Hooks are declared per page, so a request
393
+ * has to say which page it came from. The page rides on the URL rather
394
+ * than in the request, leaving the operation set closed.
395
+ */
396
+ page = "";
397
+ connectedCallback() {
398
+ this.main = this.querySelector("main");
399
+ this.addEventListener(LB_EVENT_NAME, (e) => {
400
+ const request = e.detail;
401
+ if (!request?.op) {
402
+ console.warn(`lb-hub: event without an op, ignoring`, request);
403
+ return;
404
+ }
405
+ const origin = e.target;
406
+ origin?.removeAttribute(ATTR_ERROR);
407
+ origin?.setAttribute(ATTR_PENDING, "");
408
+ const url = `${LB_REQUEST_ENDPOINT}?page=${encodeURIComponent(this.page)}`;
409
+ void fetchData(url, {
410
+ method: "POST",
411
+ headers: { "content-type": "application/json" },
412
+ body: JSON.stringify(request)
413
+ }).then((data) => applyData(this.main, data)).catch((err) => {
414
+ console.error(`lb-hub: request failed`, err);
415
+ origin?.setAttribute(ATTR_ERROR, "");
416
+ }).finally(() => origin?.removeAttribute(ATTR_PENDING));
417
+ });
418
+ this.addEventListener("click", (e) => {
419
+ const el = e.target?.closest(`[${ATTR_ACTION}]`);
420
+ if (!el || el.localName.includes("-")) return;
421
+ e.preventDefault();
422
+ const detail = {
423
+ op: "action",
424
+ name: el.getAttribute(ATTR_ACTION),
425
+ ...coordinates(el)
426
+ };
427
+ el.dispatchEvent(
428
+ new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
429
+ );
430
+ });
431
+ this.addEventListener("click", (e) => {
432
+ const el = e.target?.closest(`[${ATTR_DELETE}]`);
433
+ if (!el || el.localName.includes("-")) return;
434
+ e.preventDefault();
435
+ const { query, key } = coordinates(el);
436
+ if (!query || !key) {
437
+ console.warn(`lb-hub: lb-delete with no query/key coordinates, ignoring`, el);
438
+ return;
439
+ }
440
+ const detail = { op: "tuple-delete", query, key };
441
+ el.dispatchEvent(
442
+ new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
443
+ );
444
+ });
445
+ this.addEventListener("submit", (e) => {
446
+ const form = e.target?.closest(`[${ATTR_INSERT}]`);
447
+ if (!form) return;
448
+ e.preventDefault();
449
+ const { query } = coordinates(form);
450
+ if (!query) {
451
+ console.warn(`lb-hub: lb-insert with no lb-query, ignoring`, form);
452
+ return;
453
+ }
454
+ const detail = {
455
+ op: "tuple-insert",
456
+ query,
457
+ values: gatherValues(form)
458
+ };
459
+ form.dispatchEvent(
460
+ new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
461
+ );
462
+ });
463
+ this.addEventListener("submit", (e) => {
464
+ const form = e.target?.closest(`[${ATTR_UPDATE}]`);
465
+ if (!form) return;
466
+ e.preventDefault();
467
+ const { query, key } = coordinates(form);
468
+ if (!query || !key) {
469
+ console.warn(
470
+ `lb-hub: lb-update with no query/key coordinates, ignoring`,
471
+ form
472
+ );
473
+ return;
474
+ }
475
+ const detail = {
476
+ op: "tuple-update",
477
+ query,
478
+ key,
479
+ values: gatherValues(form)
480
+ };
481
+ form.dispatchEvent(
482
+ new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
483
+ );
484
+ });
485
+ this.addEventListener("click", (e) => {
486
+ const link = e.target?.closest(`a[${ATTR_NAV_LINK}]`);
487
+ if (!link) return;
488
+ e.preventDefault();
489
+ const path = new URL(link.href).pathname;
490
+ history.pushState(null, "", path);
491
+ void this.navigate(path);
492
+ });
493
+ addEventListener("popstate", () => void this.navigate(location.pathname));
494
+ void this.navigate(location.pathname);
495
+ }
496
+ /**
497
+ * Navigation is the one operation that replaces host DOM. The host is
498
+ * already in the document, so selecting a page is getElementById() and
499
+ * there is no fetching mechanism on the host channel.
500
+ */
501
+ async navigate(path) {
502
+ const page = pageNameFor(path);
503
+ const template = document.getElementById(PAGE_TEMPLATE_PREFIX + page);
504
+ if (!(template instanceof HTMLTemplateElement)) {
505
+ console.error(`lb-hub: no page host for '${page}'`);
506
+ return;
507
+ }
508
+ let data = {};
509
+ try {
510
+ data = await fetchData(
511
+ `${LB_DATA_ENDPOINT}?page=${encodeURIComponent(page)}`
512
+ );
513
+ } catch (err) {
514
+ console.error(`lb-hub: failed to load data for '${page}'`, err);
515
+ }
516
+ this.page = page;
517
+ this.main.replaceChildren(template.content.cloneNode(true));
518
+ applyData(this.main, data);
519
+ }
520
+ };
521
+ customElements.define(HUB_TAG_NAME, LbHub);
522
+ })();
@@ -0,0 +1,23 @@
1
+ export declare const ATTR_QUERY = "lb-query";
2
+ export declare const ATTR_KEY = "lb-key";
3
+ export declare const ATTR_CELL = "lb-cell";
4
+ export declare const ATTR_VALUE = "lb-value";
5
+ export declare const ATTR_GROUP = "lb-group";
6
+ export declare const ATTR_SORT = "lb-sort";
7
+ export declare const ATTR_ROW_COUNT = "data-rows";
8
+ export declare const LB_EVENT_NAME = "lb-request";
9
+ export declare const ATTR_ACTION = "lb-action";
10
+ export declare const ATTR_DELETE = "lb-delete";
11
+ export declare const ATTR_INSERT = "lb-insert";
12
+ export declare const ATTR_UPDATE = "lb-update";
13
+ export declare const ATTR_SLOT = "lb-slot";
14
+ export declare const ATTR_TEMPLATE = "lb-template";
15
+ export declare const ATTR_NAV_LINK = "lb-nav-link";
16
+ export declare const PAGE_TEMPLATE_PREFIX = "page-";
17
+ export declare const HUB_TAG_NAME = "lb-hub";
18
+ export declare const ATTR_PENDING = "data-lb-pending";
19
+ export declare const ATTR_ERROR = "data-lb-error";
20
+ export declare const LB_DATA_ENDPOINT = "/lb/data";
21
+ export declare const LB_REQUEST_ENDPOINT = "/lb";
22
+ export declare const LB_REQUEST_TIMEOUT_MS = 10000;
23
+ //# sourceMappingURL=lb-constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-constants.d.ts","sourceRoot":"","sources":["../../core/lb-constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,UAAU,aAAa,CAAC;AACrC,eAAO,MAAM,QAAQ,WAAW,CAAC;AACjC,eAAO,MAAM,SAAS,YAAY,CAAC;AACnC,eAAO,MAAM,UAAU,aAAa,CAAC;AAMrC,eAAO,MAAM,UAAU,aAAa,CAAC;AAKrC,eAAO,MAAM,SAAS,YAAY,CAAC;AAOnC,eAAO,MAAM,cAAc,cAAc,CAAC;AAK1C,eAAO,MAAM,aAAa,eAAe,CAAC;AAM1C,eAAO,MAAM,WAAW,cAAc,CAAC;AAMvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAKvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAOvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAMvC,eAAO,MAAM,SAAS,YAAY,CAAC;AAYnC,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAK3C,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAG3C,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAQ5C,eAAO,MAAM,YAAY,WAAW,CAAC;AAQrC,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAO9C,eAAO,MAAM,UAAU,kBAAkB,CAAC;AAG1C,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAC3C,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAIzC,eAAO,MAAM,qBAAqB,QAAS,CAAC"}
@@ -0,0 +1,95 @@
1
+ // Every hub attribute name lives here. Nothing writes one as a string
2
+ // literal — not a widget, not a page host, not a test. See docs/guide.md,
3
+ // "Attribute vocabulary".
4
+ // --- Addressing (docs/data-binding.md) ---------------------------------
5
+ export const ATTR_QUERY = "lb-query";
6
+ export const ATTR_KEY = "lb-key";
7
+ export const ATTR_CELL = "lb-cell";
8
+ export const ATTR_VALUE = "lb-value";
9
+ // Named on a row template, and read by a list widget that groups its rows
10
+ // rather than by the hub. Grouping is placement, and placement is the
11
+ // widget's — but the name is shared, because a grouped <select> and a table
12
+ // with section headings are asking the same thing of the same data.
13
+ export const ATTR_GROUP = "lb-group";
14
+ // Named on a row template beside lb-group, and read by the same kind of
15
+ // widget for the same reason. Sorting and grouping are one question — where
16
+ // does this row go? — and both answers are local.
17
+ export const ATTR_SORT = "lb-sort";
18
+ // Stamped on a list widget by the shared row machinery: how many rows it is
19
+ // showing. It is HTML's namespace rather than Loadbare App's because nothing
20
+ // addresses it — it is derived from rows the hub already knows, exactly as
21
+ // the section heading in lb-table is. It exists so that "no members yet"
22
+ // is a stylesheet rule rather than a conditional in every list widget.
23
+ export const ATTR_ROW_COUNT = "data-rows";
24
+ // --- Requests (docs/requests.md) ---------------------------------------
25
+ // One event name, used by a widget reaching the server and by a widget
26
+ // reaching an ancestor widget.
27
+ export const LB_EVENT_NAME = "lb-request";
28
+ // Names a declared action. On a native element, the hub turns a click into
29
+ // the request; on a widget, the widget builds it from whatever interaction
30
+ // it owns. Either way the name must appear in the page's hooks or it is
31
+ // refused.
32
+ export const ATTR_ACTION = "lb-action";
33
+ // Marks a native element as deleting the tuple it is inside — the CRUD
34
+ // counterpart to lb-action. It needs no declared name: tuple-delete takes
35
+ // only the row's own lb-query/lb-key coordinates, which is all the server
36
+ // needs to know which row and whether the operation is permitted on it.
37
+ export const ATTR_DELETE = "lb-delete";
38
+ // Marks a <form> as building a new tuple on submit. The hub gathers every
39
+ // lb-cell inside it into a values map — no lb-key, because there is no row
40
+ // yet — and the form's own lb-query says which query it is inserting into.
41
+ export const ATTR_INSERT = "lb-insert";
42
+ // Marks a <form> as committing several cells of one existing tuple at once
43
+ // on submit — the batch counterpart to cell-change. The hub gathers the
44
+ // same values map lb-insert does, plus the row's own lb-key: a form that is
45
+ // a list's row template root, or sits inside one, has both from its
46
+ // ancestors exactly as a delete button does.
47
+ export const ATTR_UPDATE = "lb-update";
48
+ // --- Expansion (docs/expansion.md) -------------------------------------
49
+ // Marks the element in a definition whose children receive the authored
50
+ // inner content. An attribute rather than an element because HTML content
51
+ // models discard foreign elements inside <select> and <table>.
52
+ export const ATTR_SLOT = "lb-slot";
53
+ // One name in two positions. In a definition it marks a destination; on an
54
+ // authored <template> it names the destination that template is for, and the
55
+ // template is unwrapped into it.
56
+ //
57
+ // It exists because the parser, not the content model, is the obstacle. A
58
+ // <thead> written inside <lb-table> on a page is not in a table, so the
59
+ // tokenizer drops the tag and keeps the text. A <template> survives anywhere
60
+ // and its contents parse as if they were in a table, so wrapping the markup
61
+ // is what gets it to expansion intact — and unwrapping it is what puts real
62
+ // elements in the shipped document.
63
+ export const ATTR_TEMPLATE = "lb-template";
64
+ // --- Host channel (docs/bundling.md) -----------------------------------
65
+ // Only anchors carrying this are intercepted, so external links are never
66
+ // hijacked.
67
+ export const ATTR_NAV_LINK = "lb-nav-link";
68
+ // A page host ships as <template id="page-<name>">.
69
+ export const PAGE_TEMPLATE_PREFIX = "page-";
70
+ // The hub's own tag. It is exempt from the rule every other LB-* tag
71
+ // follows: expansion does not require it to have a definition (build/expand.ts),
72
+ // and the client build does not require it to have a script of its own
73
+ // (build/elements.ts) — it is the framework's live element, always present,
74
+ // registered once by the package's main entry point, not a widget an
75
+ // application discovers or declares.
76
+ export const HUB_TAG_NAME = "lb-hub";
77
+ // --- Request lifecycle --------------------------------------------------
78
+ // Stamped on the element that dispatched a request — the button, form, or
79
+ // widget itself — for the duration of its round trip. It carries no
80
+ // meaning beyond "in flight": a stylesheet dims the element, or a custom
81
+ // button widget watches it to disable itself, but the hub itself only sets
82
+ // and clears it.
83
+ export const ATTR_PENDING = "data-lb-pending";
84
+ // Stamped on the same element if that round trip failed — a non-2xx
85
+ // response, a thrown application error, or a timeout, all alike. Cleared at
86
+ // the start of that element's next request. What (if anything) an
87
+ // application shows for it is undecided; this is only the fact that it
88
+ // happened.
89
+ export const ATTR_ERROR = "data-lb-error";
90
+ // --- Wire --------------------------------------------------------------
91
+ export const LB_DATA_ENDPOINT = "/lb/data";
92
+ export const LB_REQUEST_ENDPOINT = "/lb";
93
+ // A client-side deadline for one round trip. fetch() has no deadline of its
94
+ // own; past this the request is aborted and treated as a failure.
95
+ export const LB_REQUEST_TIMEOUT_MS = 10_000;