@gramlot/gramlot-examples 0.2.12 → 0.2.14

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/README.md CHANGED
@@ -12,7 +12,13 @@ npm install @gramlot/gramlot-examples
12
12
  [![PyPI](https://img.shields.io/pypi/v/gramlot-examples)](https://pypi.org/project/gramlot-examples/)
13
13
  [![npm](https://img.shields.io/npm/v/@gramlot/gramlot-examples)](https://www.npmjs.com/package/@gramlot/gramlot-examples)
14
14
 
15
- Current release: 0.2.12.
15
+ Current release: 0.2.14.
16
+
17
+ [Documentation and learning sequence](https://gramlot-examples.readthedocs.io/en/latest/)
18
+ include every lesson, its Python and JavaScript source, and the gallery guide.
19
+ To run the examples, choose the [Python server](https://gramlot-py-server.readthedocs.io/en/latest/),
20
+ [Node.js/Bun server](https://gramlot-js-server.readthedocs.io/en/latest/010-tutorial.html)
21
+ or [serverless exporter](https://gramlot-js-server.readthedocs.io/en/latest/135-serverless-tutorial.html).
16
22
 
17
23
  The repositories of the family are described in
18
24
  [The Gramlot family](https://github.com/gramlot-org/gramlot/blob/main/docs/public/055-family.md)
@@ -26,9 +32,10 @@ same-name README:
26
32
 
27
33
  - `html_svg/`: thirteen HTML and SVG pages, without binding (`e01`–`e13`);
28
34
  - `binding/`: eleven pages whose DOM follows the Data (`b01`–`b11`);
29
- - `controllers/`: seventeen pages with formulas, controllers, named logic, events, the
35
+ - `controllers/`: nineteen pages with formulas, controllers, named logic, events, the
30
36
  end-to-end story, `dataRpc` calls to the endpoints of the page, `remote` fragments and
31
- `dataRemote` values (`c01`–`c17`).
37
+ `dataRemote` values (`c01`–`c19`). The last two introduce: topics,
38
+ authored references, native ask and parent/child messages. They require core 0.2.18 or later.
32
39
 
33
40
  Each `NN_name.js` exports `Page` and `Logic`; the Python page `NN_name.py` takes its
34
41
  `Logic` from the module beside it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gramlot/gramlot-examples",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Gramlot example pages, the gallery page and its catalogue.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -14,7 +14,7 @@
14
14
  "prepack": "node scripts/build-browser.mjs"
15
15
  },
16
16
  "dependencies": {
17
- "@gramlot/gramlot": ">=0.2.17"
17
+ "@gramlot/gramlot": ">=0.2.18"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@genrojs/bag": ">=0.11.0",
@@ -72,7 +72,7 @@
72
72
  "title": "Cards with icons",
73
73
  "folder": "10_cards_with_icons",
74
74
  "description": "HTML cards with SVG icons and live Source removal.",
75
- "explanation": "Helpers and loops build the cards. Each × button removes its labelled Source node through the browser Builder; Gramlot updates the DOM without reloading. Reload restores the initial cards. No Data binding or server synchronization is involved."
75
+ "explanation": "Helpers and loops build the cards. Each \u00d7 button removes its labelled Source node through the browser Builder; Gramlot updates the DOM without reloading. Reload restores the initial cards. No Data binding or server synchronization is involved."
76
76
  },
77
77
  {
78
78
  "key": "e11",
@@ -299,6 +299,18 @@
299
299
  "folder": "14_data_remote",
300
300
  "description": "Revisit endpoint values with lazy Data; compare a bound read, an explicit snapshot and cache expiry.",
301
301
  "explanation": "The menu path holds a resolver of the endpoint load_menu with cacheTime 5. The ^.menu paragraph reads it: the answer arrives with the Data event. A change of the table loads the menu again."
302
+ },
303
+ {
304
+ "key": "c18",
305
+ "title": "Publish and subscribe",
306
+ "folder": "18_publish_subscribe",
307
+ "description": "Development core 0.2.18: revisit button actions with page topics, self receivers, references and native ask."
308
+ },
309
+ {
310
+ "key": "c19",
311
+ "title": "Parent and child messages",
312
+ "folder": "19_frame_messages",
313
+ "description": "Development core 0.2.18: local-first publish and direct parent/child postMessage exchange."
302
314
  }
303
315
  ]
304
316
  }
@@ -49,30 +49,82 @@
49
49
  }
50
50
  throw new Error(`Invalid ${type} value: ${text || "(empty)"}`);
51
51
  }
52
- var sequence = 0;
52
+ var InspectorStyles = class {
53
+ #document;
54
+ #element = null;
55
+ #rule = null;
56
+ constructor(document2) {
57
+ this.#document = document2;
58
+ }
59
+ snapshot() {
60
+ const computed = this.#document.defaultView.getComputedStyle(this.#document.documentElement);
61
+ const names = /* @__PURE__ */ new Set();
62
+ for (let index = 0; index < computed.length; index++) {
63
+ const name = computed.item(index);
64
+ if (name.startsWith("--gramlot-")) names.add(name);
65
+ }
66
+ for (let index = 0; index < (this.#rule?.style.length ?? 0); index++) names.add(this.#rule.style.item(index));
67
+ return [...names].sort((left, right) => left.localeCompare(right, "en", { numeric: true })).map((name) => ({
68
+ name,
69
+ value: computed.getPropertyValue(name).trim(),
70
+ override: this.#rule?.style.getPropertyValue(name) ?? ""
71
+ }));
72
+ }
73
+ set(name, value) {
74
+ if (typeof name !== "string" || !this.snapshot().some((row) => row.name === name)) {
75
+ throw new Error("Choose an existing --gramlot-* variable on the document root");
76
+ }
77
+ if (typeof value !== "string" || !value.trim() || !this.#document.defaultView.CSS.supports(name, value)) {
78
+ throw new Error("Enter a valid CSS variable value");
79
+ }
80
+ if (!this.#element) {
81
+ const element = this.#document.createElement("style");
82
+ this.#document.head.append(element);
83
+ if (!element.sheet) {
84
+ element.remove();
85
+ throw new Error("The page does not allow the inspector preview stylesheet");
86
+ }
87
+ element.sheet.insertRule(":root {}", 0);
88
+ this.#element = element;
89
+ this.#rule = element.sheet.cssRules[0];
90
+ }
91
+ this.#rule.style.setProperty(name, value, "important");
92
+ }
93
+ reset(name) {
94
+ if (typeof name !== "string" || !name.startsWith("--gramlot-")) throw new Error("Invalid CSS variable name");
95
+ this.#rule?.style.removeProperty(name);
96
+ if (this.#rule?.style.length === 0) this.dispose();
97
+ }
98
+ dispose() {
99
+ this.#element?.remove();
100
+ this.#element = null;
101
+ this.#rule = null;
102
+ }
103
+ };
53
104
  var roots = ["data", "source"];
54
105
  var forbidden = /* @__PURE__ */ new Set(["_meta", "__proto__", "prototype", "constructor"]);
55
- function encodeScalar(value) {
56
- if (!scalarType(value)) throw new TypeError("The inspector edits scalar values only");
57
- const wrapper = new Bag();
58
- wrapper.setItem("value", value);
59
- return wrapper.toTytx("json");
60
- }
61
- function decodeScalar(text) {
62
- const value = Bag.fromTytx(text, "json").getItem("value");
63
- if (!scalarType(value)) throw new TypeError("The inspector edits scalar values only");
64
- return value;
65
- }
66
- function cell(value) {
67
- if (scalarType(value)) return { encoded: encodeScalar(value) };
68
- const display = value instanceof Bag ? `Bag (${value.getNodes().length} nodes)` : value === void 0 ? "undefined" : typeof value === "function" ? "[Function]" : typeof value === "bigint" || typeof value === "symbol" ? String(value) : JSON.stringify(value);
69
- return { display, readonly: true };
70
- }
71
106
  function attachInspectorAgent(gramlot, port) {
72
- if (!(gramlot instanceof Gramlot) || gramlot.state === "disposed") throw new TypeError("Expected a live Gramlot instance");
107
+ if (!gramlot || gramlot.state === "disposed") throw new TypeError("Expected a live Gramlot instance");
108
+ const Bag4 = gramlot.data.constructor;
109
+ const encodeScalar2 = (value) => {
110
+ const wrapper = new Bag4();
111
+ wrapper.setItem("value", value);
112
+ return wrapper.toTytx("json");
113
+ };
114
+ const decodeScalar2 = (text) => {
115
+ const value = Bag4.fromTytx(text, "json").getItem("value");
116
+ if (!scalarType(value)) throw new TypeError("The inspector edits scalar values only");
117
+ return value;
118
+ };
119
+ function cell(value) {
120
+ if (scalarType(value)) return { encoded: encodeScalar2(value) };
121
+ const display = value instanceof Bag4 ? `Bag (${value.getNodes().length} nodes)` : value === void 0 ? "undefined" : typeof value === "function" ? "[Function]" : typeof value === "bigint" || typeof value === "symbol" ? String(value) : JSON.stringify(value);
122
+ return { display, readonly: true };
123
+ }
73
124
  const document2 = gramlot.src.renderer.destination.ownerDocument;
74
125
  const window2 = document2.defaultView;
75
- const subscriber = `inspector-bridge-${++sequence}`;
126
+ const styles = new InspectorStyles(document2);
127
+ const subscriber = `inspector-bridge-${window2.crypto.getRandomValues(new Uint32Array(4)).join("-")}`;
76
128
  const identities = /* @__PURE__ */ new WeakMap();
77
129
  const versions = /* @__PURE__ */ new WeakMap();
78
130
  let nextNode = 0, stopped = false, scheduled = false;
@@ -90,7 +142,7 @@
90
142
  const next = /* @__PURE__ */ new Set([...ancestors, store]);
91
143
  return store.getNodes().map((node) => {
92
144
  const value = node.getValue(true);
93
- const branch = !node.resolver && value instanceof Bag;
145
+ const branch = !node.resolver && value instanceof Bag4;
94
146
  return {
95
147
  id: identity(node),
96
148
  label: node.label,
@@ -171,12 +223,12 @@
171
223
  }
172
224
  for (const [name, encoded] of command.set) {
173
225
  if (!name.trim() || forbidden.has(name) || Object.hasOwn(attrs, name) && !scalarType(attrs[name])) throw new Error(`Cannot edit attribute: ${name}`);
174
- attrs[name] = decodeScalar(encoded);
226
+ attrs[name] = decodeScalar2(encoded);
175
227
  }
176
228
  let value;
177
229
  if (Object.hasOwn(command, "value")) {
178
230
  if (node.resolver || !scalarType(node.getValue(true))) throw new Error("This value is read-only");
179
- value = decodeScalar(command.value);
231
+ value = decodeScalar2(command.value);
180
232
  }
181
233
  if (command.set.length || command.remove.length) node.setAttr(attrs, true, false, false);
182
234
  if (Object.hasOwn(command, "value")) node.setValue(value);
@@ -197,6 +249,14 @@
197
249
  return;
198
250
  }
199
251
  try {
252
+ if (["styles", "setStyle", "resetStyle", "resetStyles"].includes(message.type)) {
253
+ if (gramlot.state === "disposed") throw new Error("Inspected page disconnected");
254
+ if (message.type === "setStyle") styles.set(message.command.name, message.command.value);
255
+ if (message.type === "resetStyle") styles.reset(message.command.name);
256
+ if (message.type === "resetStyles") styles.dispose();
257
+ send({ type: "result", id: message.id, ok: true, styles: styles.snapshot() });
258
+ return;
259
+ }
200
260
  if (message.type === "edit") edit(message.command);
201
261
  else if (message.type === "breakpoint") setBreakpoint(message.command);
202
262
  else if (message.type !== "refresh") throw new Error(`Unknown inspector command: ${message.type}`);
@@ -211,6 +271,7 @@
211
271
  stopped = true;
212
272
  for (const root of roots) bag(root).unsubscribe(subscriber, { any: true });
213
273
  breakpoints.clear();
274
+ styles.dispose();
214
275
  port.removeEventListener("message", receive);
215
276
  window2.removeEventListener("pagehide", dispose);
216
277
  port.close();
@@ -222,12 +283,25 @@
222
283
  flush();
223
284
  return { dispose };
224
285
  }
286
+ var roots2 = ["data", "source"];
287
+ function encodeScalar(value) {
288
+ if (!scalarType(value)) throw new TypeError("The inspector edits scalar values only");
289
+ const wrapper = new Bag();
290
+ wrapper.setItem("value", value);
291
+ return wrapper.toTytx("json");
292
+ }
293
+ function decodeScalar(text) {
294
+ const value = Bag.fromTytx(text, "json").getItem("value");
295
+ if (!scalarType(value)) throw new TypeError("The inspector edits scalar values only");
296
+ return value;
297
+ }
225
298
  var InspectorConnection = class {
226
299
  data = new Bag();
227
300
  source = new Bag();
228
301
  state = "connecting";
229
302
  error = "";
230
303
  breakpoints = [];
304
+ styles = null;
231
305
  #info = /* @__PURE__ */ new WeakMap();
232
306
  #listeners = /* @__PURE__ */ new Set();
233
307
  #pending = /* @__PURE__ */ new Map();
@@ -255,8 +329,9 @@
255
329
  return;
256
330
  }
257
331
  try {
332
+ if (message.styles) this.styles = message.styles;
258
333
  if (message.snapshot) {
259
- for (const root of roots) this.#reconcile(this[root], message.snapshot[root]);
334
+ for (const root of roots2) this.#reconcile(this[root], message.snapshot[root]);
260
335
  this.breakpoints = message.snapshot.breakpoints;
261
336
  this.state = "started";
262
337
  this.error = "";
@@ -277,7 +352,7 @@
277
352
  #reconcile(store, rows) {
278
353
  const labels = new Set(rows.map((row) => row.label));
279
354
  for (const node of store.getNodes()) if (!labels.has(node.label)) store.popNode(node.label);
280
- for (const row of rows) {
355
+ for (const [index, row] of rows.entries()) {
281
356
  let node = store.getNode(row.label);
282
357
  let previous = node ? this.#info.get(node) : null;
283
358
  if (node && previous?.id !== row.id) {
@@ -307,6 +382,8 @@
307
382
  attrsWire: new Map(row.attrs.map(([name, descriptor]) => [name, JSON.stringify(descriptor)]))
308
383
  });
309
384
  if (branch) this.#reconcile(value, row.children);
385
+ const position = store.getNodes().indexOf(node);
386
+ if (position !== index) store.move(position, index);
310
387
  }
311
388
  }
312
389
  request(type, command = {}) {
@@ -328,9 +405,9 @@
328
405
  this.#notify();
329
406
  }
330
407
  };
331
- var gramlot_tools_inspector_default = ":host { all: initial; color-scheme: light; font: 12px/1.4 system-ui, sans-serif; color: #38414b; }\n* { box-sizing: border-box; }\nbutton, input, select { font: inherit; color: inherit; }\nbutton { cursor: pointer; }\nbutton:focus-visible, input:focus-visible, select:focus-visible { outline: 1px solid #3574cb; outline-offset: -1px; }\n.tree { padding: 7px 3px; }\n.properties { background: white; }\n.properties-heading { font-size: 11px; display: flex; align-items: center; justify-content: space-between; height: 29px; padding: 0 8px; background: #f1f3f5; color: #6d7989; border-bottom: 1px solid #d1d7df; }\n.properties-heading button { background: white; border: 1px solid #c5cdda; border-radius: 2px; width: 23px; height: 23px; padding: 0; font-size: 18px; line-height: 20px; }\nfieldset { border: 0; margin: 0; padding: 0; min-width: 0; }\n.property-row { display: grid; grid-template-columns: minmax(95px, 30%) minmax(0, 1fr); min-height: 20px; border-bottom: 1px solid #d1d7df; font-size: 11px; }\n.property-name { display: flex; align-items: center; justify-content: flex-end; background: #888; color: white; font-weight: 700; padding: 1px 6px; border-right: 1px solid #d1d7df; overflow-wrap: anywhere; text-align: right; }\n.property-value { display: flex; align-items: center; min-width: 0; }\n.property-row input { border: 0; border-radius: 0; min-width: 0; width: 100%; height: 19px; padding: 1px 5px; background: transparent; }\n.property-name input { color: white; text-align: right; padding: 0; }\n.property-value input { flex: 1; }\n.property-value input[type=checkbox] { flex: none; width: 14px; height: 14px; margin: 2px 6px; }\n.property-value select { max-width: 82px; height: 20px; padding: 0; border: 0; border-left: 1px solid #d1d7df; background: #f1f3f5; font-size: 10px; }\n.remove { margin-left: auto; flex: none; width: 22px; height: 20px; padding: 3px; border: 0; background: transparent; color: #7c8797; opacity: 0; }\n.remove svg { width: 13px; height: 13px; }\n.property-row:hover .remove, .property-row:focus-within .remove { opacity: 1; }\n.remove:hover { color: #b82323; background: #fbe9e9; }\n:disabled { cursor: default; opacity: .65; }\n.path { flex: none; padding: 5px 9px; border-top: 1px solid #c9cfd6; background: #f5f7f9; color: #697687; font-size: 11px; min-height: 27px; overflow-wrap: anywhere; }\n.error, .edit-status, .breakpoint-status, .breakpoint-line { font-size: 11px; margin: 0; padding: 3px 8px; overflow-wrap: anywhere; }\n.error, .edit-status, .breakpoint-status { color: #a12121; }\n.breakpoint-line { background: #fff2ed; color: #854127; border-bottom: 1px solid #d1d7df; }\n.error:empty, .edit-status:empty, .breakpoint-status:empty, .breakpoint-line:empty { display: none; }\n.breakpoint-dialog { width: min(340px, 90vw); color: #38414b; border: 1px solid #bbc3cf; border-radius: 4px; padding: 12px; box-shadow: 0 8px 28px #0003; font: 12px/1.4 system-ui, sans-serif; }\n.breakpoint-dialog::backdrop { background: #0002; }\n.breakpoint-dialog h2 { font-size: 13px; margin: 0 0 10px; overflow-wrap: anywhere; }\n.breakpoint-dialog select, .breakpoint-dialog input { width: 100%; border: 1px solid #bbc3cf; padding: 4px; margin: 0 0 7px; background: white; }\n.dialog-actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 7px; }\n.dialog-actions button { padding: 3px 10px; border: 1px solid #bbc3cf; border-radius: 3px; background: #f1f3f5; }\n.heading-label { flex: 1; }\n.properties-heading .breakpoint-count { width: auto; padding: 0 5px; margin-right: 5px; font-size: 11px; color: #b04a35; }\n.breakpoint-entry { display: flex; align-items: center; gap: 6px; padding: 4px 0; border-bottom: 1px solid #d1d7df; }\n.breakpoint-entry span { flex: 1; overflow-wrap: anywhere; }\n.breakpoint-entry button { background: transparent; border: 0; font-size: 17px; }\n[hidden] { display: none !important; }\n.targetbar { flex: none; height: 28px; padding: 2px 6px; background: #edf2fa; border-bottom: 1px solid #dadce0; }\n.targetbar select { width: 100%; height: 23px; border: 0; background: transparent; font-size: 11px; }\n";
408
+ var gramlot_tools_inspector_default = ":host { all: initial; color-scheme: light; font: 12px/1.4 system-ui, sans-serif; color: #38414b; }\n* { box-sizing: border-box; }\nbutton, input, select { font: inherit; color: inherit; }\nbutton { cursor: pointer; }\nbutton:focus-visible, input:focus-visible, select:focus-visible { outline: 1px solid #3574cb; outline-offset: -1px; }\n.tree { padding: 7px 3px; }\n.properties { background: white; }\n.properties-heading { font-size: 11px; display: flex; align-items: center; justify-content: space-between; height: 29px; padding: 0 8px; background: #f1f3f5; color: #6d7989; border-bottom: 1px solid #d1d7df; }\n.properties-heading button { background: white; border: 1px solid #c5cdda; border-radius: 2px; width: 23px; height: 23px; padding: 0; font-size: 18px; line-height: 20px; }\nfieldset { border: 0; margin: 0; padding: 0; min-width: 0; }\n.property-row { display: grid; grid-template-columns: minmax(95px, 30%) minmax(0, 1fr); min-height: 20px; border-bottom: 1px solid #d1d7df; font-size: 11px; }\n.property-name { display: flex; align-items: center; justify-content: flex-end; background: #888; color: white; font-weight: 700; padding: 1px 6px; border-right: 1px solid #d1d7df; overflow-wrap: anywhere; text-align: right; }\n.property-value { display: flex; align-items: center; min-width: 0; }\n.property-row input { border: 0; border-radius: 0; min-width: 0; width: 100%; height: 19px; padding: 1px 5px; background: transparent; }\n.property-name input { color: white; text-align: right; padding: 0; }\n.property-value input { flex: 1; }\n.property-value input[type=checkbox] { flex: none; width: 14px; height: 14px; margin: 2px 6px; }\n.property-value select { max-width: 82px; height: 20px; padding: 0; border: 0; border-left: 1px solid #d1d7df; background: #f1f3f5; font-size: 10px; }\n.remove { margin-left: auto; flex: none; width: 22px; height: 20px; padding: 3px; border: 0; background: transparent; color: #7c8797; opacity: 0; }\n.remove svg { width: 13px; height: 13px; }\n.property-row:hover .remove, .property-row:focus-within .remove { opacity: 1; }\n.remove:hover { color: #b82323; background: #fbe9e9; }\n:disabled { cursor: default; opacity: .65; }\n.path { flex: none; padding: 5px 9px; border-top: 1px solid #c9cfd6; background: #f5f7f9; color: #697687; font-size: 11px; min-height: 27px; overflow-wrap: anywhere; }\n.error, .edit-status, .breakpoint-status, .breakpoint-line { font-size: 11px; margin: 0; padding: 3px 8px; overflow-wrap: anywhere; }\n.error, .edit-status, .breakpoint-status { color: #a12121; }\n.breakpoint-line { background: #fff2ed; color: #854127; border-bottom: 1px solid #d1d7df; }\n.error:empty, .edit-status:empty, .breakpoint-status:empty, .breakpoint-line:empty { display: none; }\n.breakpoint-dialog { width: min(340px, 90vw); color: #38414b; border: 1px solid #bbc3cf; border-radius: 4px; padding: 12px; box-shadow: 0 8px 28px #0003; font: 12px/1.4 system-ui, sans-serif; }\n.breakpoint-dialog::backdrop { background: #0002; }\n.breakpoint-dialog h2 { font-size: 13px; margin: 0 0 10px; overflow-wrap: anywhere; }\n.breakpoint-dialog select, .breakpoint-dialog input { width: 100%; border: 1px solid #bbc3cf; padding: 4px; margin: 0 0 7px; background: white; }\n.dialog-actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 7px; }\n.dialog-actions button { padding: 3px 10px; border: 1px solid #bbc3cf; border-radius: 3px; background: #f1f3f5; }\n.heading-label { flex: 1; }\n.properties-heading .breakpoint-count { width: auto; padding: 0 5px; margin-right: 5px; font-size: 11px; color: #b04a35; }\n.breakpoint-entry { display: flex; align-items: center; gap: 6px; padding: 4px 0; border-bottom: 1px solid #d1d7df; }\n.breakpoint-entry span { flex: 1; overflow-wrap: anywhere; }\n.breakpoint-entry button { background: transparent; border: 0; font-size: 17px; }\n[hidden] { display: none !important; }\n.targetbar { flex: none; height: 28px; padding: 2px 6px; background: #edf2fa; border-bottom: 1px solid #dadce0; }\n.targetbar select { width: 100%; height: 23px; border: 0; background: transparent; font-size: 11px; }\n.styles-panel { flex: 1; min-height: 0; overflow: auto; background: #fff; font-size: 11px; }\n.styles-toolbar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 6px 8px; background: #f1f3f5; border-bottom: 1px solid #d1d7df; }\n.styles-toolbar span { flex: 1; font-weight: 600; }\n.styles-toolbar button, .styles-toolbar a { padding: 3px 7px; border: 1px solid #c5cdda; border-radius: 3px; background: white; }\n.styles-toolbar a { color: inherit; text-decoration: none; }\n.styles-toolbar a[aria-disabled=true] { cursor: default; opacity: .65; }\n.styles-toolbar a:focus-visible { outline: 1px solid #3574cb; outline-offset: -1px; }\n.styles-help, .styles-empty, .styles-status { margin: 0; padding: 7px 9px; color: #697687; }\n.styles-status { color: #a12121; }\n.styles-status:empty { display: none; }\n.styles-group h3 { margin: 0; padding: 6px 9px; font-size: 11px; background: #edf2fa; border-block: 1px solid #d1d7df; }\n.style-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 25px 23px; align-items: center; gap: 8px; min-width: 0; padding: 3px 9px; border-bottom: 1px solid #e8ebef; }\n.style-row label { min-width: 0; font: 11px/1.4 ui-monospace, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.style-row input { min-width: 0; width: 100%; height: 23px; padding: 2px 5px; border: 1px solid #c5cdda; border-radius: 3px; background: white; }\n.style-row input[type=color] { width: 25px; padding: 1px; cursor: pointer; }\n.style-row button { grid-column: 4; width: 23px; height: 23px; padding: 0; border: 1px solid #c5cdda; border-radius: 3px; background: #f5f7f9; font-size: 15px; }\n\n:host([embedded]) { display: block; height: 100%; min-height: 0; }\n:host([embedded]) > div { height: 100%; }\n.embedded-panel { display: flex; flex-direction: column; height: 100%; min-height: 0; overflow: hidden; }\n";
332
409
  var gramlot_poc_tree_default = "/* Copyright 2025-2026 Softwell S.r.l. - SPDX-License-Identifier: Apache-2.0\n * Inspector subset of the DevTools storeTree stylesheet and inspector.css variables. */\n:host { display:block; font:11px/18px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#38414b; }\n:host([hidden]) { display:none; }\nul { list-style:none; margin:0; padding-left:17px; }\n.tree-root { padding-left:16px; }\n.tree-root > ul { padding-left:2px; }\nsummary, .leaf { position:relative; }\nli { line-height:18px; }\nsummary { display:flex; align-items:center; gap:7px; padding:0 5px; cursor:pointer; user-select:none; list-style:none; }\nsummary::-webkit-details-marker { display:none; }\nsummary::before { content:''; width:5px; height:5px; border-right:1.5px solid #78818a; border-bottom:1.5px solid #78818a; transform:rotate(-45deg); flex:none; margin:0 3px; }\ndetails[open] > summary::before { transform:rotate(45deg); }\n.leaf { display:flex; align-items:center; gap:7px; padding:0 5px 0 23px; cursor:pointer; }\nsummary:hover,.leaf:hover { background:#f0f3f6; }\n.selected,.selected:hover { background:transparent; color:#315dbd; font-weight:700; border-radius:2px; }\n.caption { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n/* Reserve a left gutter; showing an action must not shift the node label. */\n.actions { position:absolute; left:-14px; top:0; display:inline-flex; opacity:0; pointer-events:none; }\nsummary:hover > .actions,.leaf:hover > .actions,summary:focus-visible > .actions,.leaf:focus-visible > .actions,\n.actions:has(button:focus-visible),.actions:has(button.active) { opacity:1; pointer-events:auto; }\n.actions button { border:0; background:transparent; color:#80868b; font:inherit; font-size:11px; line-height:18px; width:14px; height:18px; padding:0; cursor:pointer; border-radius:3px; }\n.actions button:hover { background:#e1e5ed; }\n.actions button[data-action=breakpoint] { font-size:0; }\n.actions button[data-action=breakpoint]::before { content:''; display:inline-block; box-sizing:border-box; width:8px; height:8px; border:1.5px solid currentColor; border-radius:50%; vertical-align:middle; }\n.actions button.active { color:#b3261e; }\n.actions button.active.conditional { color:#f9ab00; }\n.actions button.active[data-action=breakpoint]::before { background:currentColor; }\n.actions button:focus-visible,summary:focus-visible,.leaf:focus-visible { outline:2px solid #356f9f; outline-offset:-2px; }\n";
333
- var sequence2 = 0;
410
+ var sequence = 0;
334
411
  var GramlotPocTree = class extends HTMLElement {
335
412
  constructor() {
336
413
  super();
@@ -346,7 +423,7 @@
346
423
  this._nodes = /* @__PURE__ */ new Map();
347
424
  this._rowActions = [];
348
425
  this._pendingRender = null;
349
- this._subId = `gramlot-poc-tree-${++sequence2}`;
426
+ this._subId = `gramlot-poc-tree-${++sequence}`;
350
427
  }
351
428
  get storeBag() {
352
429
  return this._store;
@@ -664,6 +741,10 @@
664
741
  #breakpointTests = /* @__PURE__ */ new Map();
665
742
  #trees = {};
666
743
  #breakpointTarget = null;
744
+ #styles = null;
745
+ #styleNames = "";
746
+ #styleRequest = 0;
747
+ #styleDownloadUrl = null;
667
748
  constructor() {
668
749
  super();
669
750
  this.#mount = this.ownerDocument.createElement("div");
@@ -697,6 +778,7 @@
697
778
  this.#ui.data.setItem("breakpointStatus", "");
698
779
  this.#subscribe();
699
780
  this.#refresh();
781
+ if (this.#ui.data.getItem("tab") === "styles") this.#loadStyles();
700
782
  }
701
783
  }
702
784
  get targets() {
@@ -739,6 +821,7 @@
739
821
  this.#ui.data.setItem("breakpointDirty", false);
740
822
  this.#subscribe();
741
823
  this.#refresh();
824
+ if (this.#ui.data.getItem("tab") === "styles") this.#loadStyles();
742
825
  }
743
826
  }
744
827
  #renderTargets() {
@@ -825,14 +908,47 @@
825
908
  }
826
909
  selectRoot(node) {
827
910
  const choose = () => {
828
- ui.data.setItem("root", node.getAttr("data-root"));
829
- ui.data.setItem("selection", null);
830
- inspector.#snapshot = null;
911
+ const tab = node.getAttr("data-root");
912
+ ui.data.setItem("tab", tab);
913
+ if (tab !== "styles") {
914
+ ui.data.setItem("root", tab);
915
+ ui.data.setItem("selection", null);
916
+ inspector.#snapshot = null;
917
+ }
831
918
  inspector.#refresh();
919
+ if (tab === "styles") inspector.#loadStyles();
832
920
  };
833
921
  if (ui.data.getItem("editor.dirty")) inspector.#applyEdit(choose);
834
922
  else choose();
835
923
  }
924
+ refreshStyles() {
925
+ inspector.#loadStyles();
926
+ }
927
+ styleChanged(node) {
928
+ ui.data.setItem(`styles.rows.${node.getAttr("data-row")}.dirty`, true);
929
+ }
930
+ setStyle(node) {
931
+ const row = ui.data.getItem(`styles.rows.${node.getAttr("data-row")}`);
932
+ if (node.getAttr("data-field") === "color") row.setItem("dirty", false);
933
+ else if (!row.getItem("dirty")) return;
934
+ inspector.#loadStyles("setStyle", { name: row.getItem("name"), value: row.getItem(node.getAttr("data-field")) });
935
+ }
936
+ styleKey(node, event) {
937
+ if (event.key === "Enter" && !event.isComposing) {
938
+ event.preventDefault();
939
+ this.setStyle(node);
940
+ }
941
+ if (event.key === "Escape") {
942
+ event.preventDefault();
943
+ inspector.#loadStyles();
944
+ }
945
+ }
946
+ resetStyle(node) {
947
+ inspector.#loadStyles("resetStyle", { name: ui.data.getItem(`styles.rows.${node.getAttr("data-row")}.name`) });
948
+ }
949
+ resetStyles() {
950
+ inspector.#loadStyles("resetStyles");
951
+ }
836
952
  selectNode(node, event) {
837
953
  const choose = () => {
838
954
  const entry = inspector.#nodes.find((item) => item.node === event.detail.node);
@@ -895,6 +1011,9 @@
895
1011
  ui.src.logicRegistry.register(Logic, { group: "inspector", resource: "gramlot-tools-inspector" });
896
1012
  for (const [key, value] of Object.entries({
897
1013
  root: "data",
1014
+ tab: "data",
1015
+ stylesActive: false,
1016
+ treeActive: true,
898
1017
  dataActive: true,
899
1018
  sourceActive: false,
900
1019
  targetId: "",
@@ -914,14 +1033,15 @@
914
1033
  ui.data.setItem("editor", new Bag({ dirty: false, disabled: true, status: "", mode: "", condition: "" }));
915
1034
  ui.data.setItem("breakpoints", new Bag());
916
1035
  ui.data.setItem("breakpointDirty", false);
1036
+ ui.data.setItem("styles", new Bag({ status: "", busy: false, noChanges: true, noDownload: true, downloadUrl: null, rows: new Bag() }));
917
1037
  const builder = new GramlotBuilder();
918
1038
  const root = builder.root;
919
1039
  root.style(gramlot_tools_inspector_default);
920
- const palette = root.webcomponent({ code: "gramlot-poc-palette", node_label: "palette", title: "Gramlot inspector", connect_onclose: "inspector.close" });
1040
+ const palette = this.hasAttribute("embedded") ? root.section({ node_label: "palette", class: "embedded-panel", "aria-label": "Gramlot inspector" }) : root.webcomponent({ code: "gramlot-poc-palette", node_label: "palette", title: "Gramlot inspector", connect_onclose: "inspector.close" });
921
1041
  const targetbar = palette.div({ node_label: "targetbar", class: "targetbar", hidden: "^singleTarget" });
922
1042
  targetbar.select({ node_label: "targets", value: "^targetId", disabled: "^busy", "aria-label": "Inspected page", connect_onchange: "inspector.selectTarget" });
923
1043
  const toolbar = palette.webcomponent({ code: "gramlot-poc-tabs", node_label: "tabs", role: "tablist", "aria-label": "Inspected root" });
924
- for (const name of ["Data", "Source"]) toolbar.button(name, {
1044
+ for (const name of ["Data", "Source", "Styles"]) toolbar.button(name, {
925
1045
  node_label: name.toLowerCase(),
926
1046
  type: "button",
927
1047
  role: "tab",
@@ -930,7 +1050,7 @@
930
1050
  connect_onclick: "inspector.selectRoot"
931
1051
  });
932
1052
  palette.p("^error", { role: "alert", class: "error" });
933
- const content = palette.webcomponent({ code: "gramlot-poc-splitter", node_label: "content" });
1053
+ const content = palette.webcomponent({ code: "gramlot-poc-splitter", node_label: "content", hidden: "^stylesActive" });
934
1054
  const trees = content.div({ node_label: "tree", slot: "top", class: "tree" });
935
1055
  for (const name of ["data", "source"]) trees.webcomponent({
936
1056
  code: "gramlot-poc-tree",
@@ -964,7 +1084,17 @@
964
1084
  const editor = properties.fieldset({ node_label: "editor", disabled: "^editor.disabled" });
965
1085
  editor.div({ node_label: "rows", class: "rows" });
966
1086
  properties.p("^editor.status", { role: "status", class: "edit-status" });
967
- const footer = palette.footer({ class: "path" });
1087
+ const styles = palette.section({ node_label: "styles", class: "styles-panel", hidden: "^treeActive", "aria-label": "CSS variables" });
1088
+ const stylebar = styles.div({ class: "styles-toolbar" });
1089
+ stylebar.span("Document variables");
1090
+ stylebar.button("Refresh", { type: "button", disabled: "^styles.busy", connect_onclick: "inspector.refreshStyles" });
1091
+ stylebar.button("Reset all", { type: "button", disabled: "^styles.noChanges", connect_onclick: "inspector.resetStyles" });
1092
+ stylebar.a("Download CSS", { href: "^styles.downloadUrl", download: "theme-overrides.css", "aria-disabled": "^styles.noDownload" });
1093
+ styles.p("Temporary preview \xB7 Enter or leave a field to apply. Closing the connection clears previews.", { class: "styles-help" });
1094
+ styles.p("^styles.status", { role: "status", class: "styles-status" });
1095
+ styles.p("No --gramlot-* variables on this document root. Load a Gramlot theme, then Refresh.", { class: "styles-empty", node_label: "empty" });
1096
+ styles.div({ node_label: "rows", class: "styles-rows" });
1097
+ const footer = palette.footer({ class: "path", hidden: "^stylesActive" });
968
1098
  footer.span("Path: ");
969
1099
  footer.em("^path");
970
1100
  const breakpoint = palette.dialog({ node_label: "breakpoint", "aria-label": "Breakpoint", class: "breakpoint-dialog", connect_oncancel: "inspector.closeBreakpoint" });
@@ -1012,6 +1142,14 @@
1012
1142
  } });
1013
1143
  }
1014
1144
  #unsubscribe() {
1145
+ ++this.#styleRequest;
1146
+ this.#styles?.dispose();
1147
+ this.#styles = null;
1148
+ if (this.#ui) {
1149
+ this.#renderStyles([]);
1150
+ this.#ui.data.setItem("styles.busy", false);
1151
+ this.#ui.data.setItem("styles.status", "");
1152
+ }
1015
1153
  this.#connectionRelease?.();
1016
1154
  this.#connectionRelease = null;
1017
1155
  ++this.#generation;
@@ -1038,6 +1176,12 @@
1038
1176
  try {
1039
1177
  this.#nodes = [];
1040
1178
  const root = ui.data.getItem("root");
1179
+ const tab = ui.data.getItem("tab");
1180
+ ui.data.setItem("stylesActive", tab === "styles");
1181
+ ui.data.setItem("treeActive", tab !== "styles");
1182
+ for (const name of ["data", "source", "styles"]) {
1183
+ ui.src.source.getNode(`main.palette.tabs.${name}`).setAttr({ "aria-selected": String(tab === name) });
1184
+ }
1041
1185
  if (this.#connection) {
1042
1186
  const configurations = new Bag();
1043
1187
  for (const [index, record] of this.#connection.breakpoints.entries()) configurations.setItem(`b${index}`, new Bag(record));
@@ -1050,7 +1194,6 @@
1050
1194
  ui.data.setItem("dataActive", root === "data");
1051
1195
  ui.data.setItem("sourceActive", root === "source");
1052
1196
  for (const name of ["data", "source"]) {
1053
- ui.src.source.getNode(`main.palette.tabs.${name}`).setAttr({ "aria-selected": String(root === name) });
1054
1197
  const tree = this.#trees[name];
1055
1198
  tree.storeBag = this.#connection || this.#target ? this.#targetBag(name) : null;
1056
1199
  const breakpoints = ui.data.getItem("breakpoints").getNodes().filter((node) => node.value.getItem("root") === name);
@@ -1070,6 +1213,132 @@
1070
1213
  ui.data.setItem("error", error.message);
1071
1214
  }
1072
1215
  }
1216
+ async #loadStyles(type = "styles", command = {}) {
1217
+ const ui = this.#ui;
1218
+ const request = ++this.#styleRequest;
1219
+ ui.data.setItem("styles.busy", true);
1220
+ ui.data.setItem("styles.downloadUrl", null);
1221
+ ui.data.setItem("styles.noDownload", true);
1222
+ ui.data.setItem("styles.status", "");
1223
+ try {
1224
+ if (!this.#state() || this.#state() === "disposed" || this.#connection && this.#state() !== "started") {
1225
+ throw new Error("Select a connected page before editing CSS variables");
1226
+ }
1227
+ let rows;
1228
+ if (this.#connection) {
1229
+ const connection = this.#connection;
1230
+ await connection.request(type, command);
1231
+ rows = connection.styles;
1232
+ } else {
1233
+ this.#styles ??= new InspectorStyles(this.#target.src.renderer.destination.ownerDocument);
1234
+ if (type === "setStyle") this.#styles.set(command.name, command.value);
1235
+ if (type === "resetStyle") this.#styles.reset(command.name);
1236
+ if (type === "resetStyles") this.#styles.dispose();
1237
+ rows = this.#styles.snapshot();
1238
+ }
1239
+ if (this.#ui !== ui || request !== this.#styleRequest) return;
1240
+ this.#renderStyles(rows, type, command);
1241
+ } catch (error) {
1242
+ if (this.#ui === ui && request === this.#styleRequest) ui.data.setItem("styles.status", error.message);
1243
+ } finally {
1244
+ if (this.#ui === ui && request === this.#styleRequest) {
1245
+ ui.data.setItem("styles.busy", false);
1246
+ ui.data.setItem("styles.downloadUrl", this.#styleDownloadUrl);
1247
+ ui.data.setItem("styles.noDownload", !this.#styleDownloadUrl);
1248
+ }
1249
+ }
1250
+ }
1251
+ #renderStyles(rows, type = "styles", command = {}) {
1252
+ const ui = this.#ui;
1253
+ const browser = this.ownerDocument.defaultView;
1254
+ if (this.#styleDownloadUrl) browser.URL.revokeObjectURL(this.#styleDownloadUrl);
1255
+ this.#styleDownloadUrl = null;
1256
+ const overrides = rows.filter((row) => row.override);
1257
+ if (overrides.length) {
1258
+ const declarations = overrides.map((row) => ` ${browser.CSS.escape(row.name)}: ${row.override} !important;`).join("\n");
1259
+ const css = `/* Gramlot overrides. Load after the theme stylesheet. */
1260
+ :root {
1261
+ ${declarations}
1262
+ }
1263
+ `;
1264
+ this.#styleDownloadUrl = browser.URL.createObjectURL(new browser.Blob([css], { type: "text/css;charset=utf-8" }));
1265
+ }
1266
+ ui.data.setItem("styles.downloadUrl", ui.data.getItem("styles.busy") ? null : this.#styleDownloadUrl);
1267
+ ui.data.setItem("styles.noDownload", ui.data.getItem("styles.busy") || !this.#styleDownloadUrl);
1268
+ const names = JSON.stringify(rows.map((row) => row.name));
1269
+ const rebuild = names !== this.#styleNames;
1270
+ this.#styleNames = names;
1271
+ const builder = rebuild ? new GramlotBuilder() : null;
1272
+ const groups = /* @__PURE__ */ new Map();
1273
+ if (rebuild) {
1274
+ ui.src.source.getNode("main.palette.styles.rows").setValue(ui.src.prepareSource(new GramlotBuilder().source));
1275
+ ui.data.setItem("styles.rows", new Bag());
1276
+ }
1277
+ for (const [index, record] of rows.entries()) {
1278
+ const id = `v${index}`, path = `styles.rows.${id}`;
1279
+ const color = /^#[\da-f]{6}$/i.test(record.value) ? record.value : /^#[\da-f]{3}$/i.test(record.value) ? "#" + [...record.value.slice(1)].map((char) => char + char).join("") : null;
1280
+ const fields = {
1281
+ name: record.name,
1282
+ text: record.override || record.value,
1283
+ computed: record.value,
1284
+ dirty: false,
1285
+ color: color ?? "#000000",
1286
+ noColor: color === null,
1287
+ unchanged: !record.override
1288
+ };
1289
+ if (!rebuild && ui.data.getItem(`${path}.dirty`) && !["styles", "resetStyles"].includes(type)) {
1290
+ const acknowledged = record.name === command.name && (type === "resetStyle" || ui.data.getItem(`${path}.text`) === command.value);
1291
+ if (!acknowledged) {
1292
+ delete fields.text;
1293
+ delete fields.dirty;
1294
+ }
1295
+ }
1296
+ if (rebuild) ui.data.setItem(path, new Bag(fields));
1297
+ else for (const [key, value] of Object.entries(fields)) ui.data.setItem(`${path}.${key}`, value);
1298
+ if (!rebuild) continue;
1299
+ const group = record.name.includes("-font-") ? "Typography" : record.name.includes("-space-") ? "Spacing" : /-(radius|page-width)/.test(record.name) ? "Dimensions" : record.name.endsWith("-shadow") ? "Effects" : color ? "Colors" : "Other";
1300
+ if (!groups.has(group)) {
1301
+ const section = builder.root.section({ class: "styles-group" });
1302
+ section.h3(group);
1303
+ groups.set(group, section);
1304
+ }
1305
+ const row = groups.get(group).div({ class: "style-row" });
1306
+ row.html_label(record.name, { for: `${this.#subscriber}-${id}`, title: record.name });
1307
+ row.input({
1308
+ id: `${this.#subscriber}-${id}`,
1309
+ value: `^${path}.text`,
1310
+ live: true,
1311
+ "aria-label": `${record.name} value`,
1312
+ title: `^${path}.computed`,
1313
+ "data-row": id,
1314
+ "data-field": "text",
1315
+ connect_oninput: "inspector.styleChanged",
1316
+ connect_onchange: "inspector.setStyle",
1317
+ connect_onkeydown: "inspector.styleKey",
1318
+ spellcheck: "false"
1319
+ });
1320
+ row.input({
1321
+ type: "color",
1322
+ value: `^${path}.color`,
1323
+ hidden: `^${path}.noColor`,
1324
+ "aria-label": `${record.name} color`,
1325
+ "data-row": id,
1326
+ "data-field": "color",
1327
+ connect_onchange: "inspector.setStyle"
1328
+ });
1329
+ row.button("\u21B6", {
1330
+ type: "button",
1331
+ "aria-label": `Reset ${record.name}`,
1332
+ title: "Reset variable",
1333
+ disabled: `^${path}.unchanged`,
1334
+ "data-row": id,
1335
+ connect_onclick: "inspector.resetStyle"
1336
+ });
1337
+ }
1338
+ if (rebuild) ui.src.source.getNode("main.palette.styles.rows").setValue(ui.src.prepareSource(builder.source));
1339
+ ui.data.setItem("styles.noChanges", !rows.some((row) => row.override));
1340
+ ui.src.source.getNode("main.palette.styles.empty").setAttr({ hidden: rows.length !== 0 });
1341
+ }
1073
1342
  #index(bag, labels) {
1074
1343
  for (const node of bag.getNodes()) {
1075
1344
  const path = [...labels, node.label];
@@ -1163,8 +1432,8 @@
1163
1432
  const label = line.div({ class: "property-name" });
1164
1433
  if (fresh) label.input({ value: "^.name", live: true, "aria-label": "New attribute name", "data-row": row.label, connect_oninput: "inspector.editChanged" });
1165
1434
  else label.span(name);
1166
- const cell2 = line.div({ class: "property-value" });
1167
- cell2.input({
1435
+ const cell = line.div({ class: "property-value" });
1436
+ cell.input({
1168
1437
  value: "^.text",
1169
1438
  type: "^.inputType",
1170
1439
  live: true,
@@ -1177,11 +1446,11 @@
1177
1446
  connect_onkeydown: "inspector.editKey"
1178
1447
  });
1179
1448
  if (fresh) {
1180
- const type = cell2.select({ value: "^.type", "aria-label": "New attribute type", "data-row": row.label, connect_onchange: "inspector.typeChanged" });
1449
+ const type = cell.select({ value: "^.type", "aria-label": "New attribute type", "data-row": row.label, connect_onchange: "inspector.typeChanged" });
1181
1450
  for (const value of editorTypes) type.option(value, { value });
1182
1451
  }
1183
1452
  if (!primary) {
1184
- const button = cell2.button({
1453
+ const button = cell.button({
1185
1454
  type: "button",
1186
1455
  disabled: locked,
1187
1456
  "aria-label": `Remove ${name || "new attribute"}`,