@gramlot/gramlot-examples 0.3.3 → 0.3.4

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 (32) hide show
  1. package/README.md +6 -3
  2. package/package.json +4 -3
  3. package/src/gramlot_examples/catalog.json +54 -0
  4. package/src/gramlot_examples/gallery/dist/frame.js +548 -36
  5. package/src/gramlot_examples/gallery/dist/gallery.js +2387 -16677
  6. package/src/gramlot_examples/gallery/dist/inputs.js +12 -6
  7. package/src/gramlot_examples/gallery/dist/inspector.js +547 -35
  8. package/src/gramlot_examples/gallery/dist/layout.js +847 -28
  9. package/src/gramlot_examples/gallery/dist/notices.json +0 -24
  10. package/src/gramlot_examples/gallery/dist/theme-styles.js +1 -1
  11. package/src/gramlot_examples/gallery/gallery-page.js +66 -45
  12. package/src/gramlot_examples/gallery/gallery.css +61 -38
  13. package/src/gramlot_examples/gallery/guided-demo.js +151 -0
  14. package/src/gramlot_examples/gallery/page.js +1 -1
  15. package/src/gramlot_examples/index.js +1 -0
  16. package/src/gramlot_examples/pages/layout/01_border.js +80 -0
  17. package/src/gramlot_examples/pages/layout/01_border.md +23 -0
  18. package/src/gramlot_examples/pages/layout/02_nested.js +99 -0
  19. package/src/gramlot_examples/pages/layout/02_nested.md +23 -0
  20. package/src/gramlot_examples/pages/layout/03_rounded.js +111 -0
  21. package/src/gramlot_examples/pages/layout/03_rounded.md +25 -0
  22. package/src/gramlot_examples/pages/layout/04_drawer_handles.js +140 -0
  23. package/src/gramlot_examples/pages/layout/04_drawer_handles.md +33 -0
  24. package/src/gramlot_examples/pages/layout/05_nested_lab.js +251 -0
  25. package/src/gramlot_examples/pages/layout/05_nested_lab.md +25 -0
  26. package/src/gramlot_examples/pages/layout/06_border_events.js +118 -0
  27. package/src/gramlot_examples/pages/layout/06_border_events.md +25 -0
  28. package/src/gramlot_examples/pages/layout/07_tabs.js +300 -0
  29. package/src/gramlot_examples/pages/layout/07_tabs.md +37 -0
  30. package/src/gramlot_examples/pages/layout/08_stack.js +142 -0
  31. package/src/gramlot_examples/pages/layout/08_stack.md +27 -0
  32. package/src/gramlot_examples/pages/layout/README.md +15 -0
@@ -52,6 +52,239 @@
52
52
  function readParameterEnums(node) {
53
53
  return Object.fromEntries(Object.entries(node._getMeta("gramlot_component")?.parameters ?? {}).filter(([, spec]) => spec.enum).map(([name, spec]) => [name, spec.enum]));
54
54
  }
55
+ var InspectorHistory = class {
56
+ #undo = [];
57
+ #redo = [];
58
+ #references = /* @__PURE__ */ new WeakMap();
59
+ constructor(gramlot) {
60
+ this.gramlot = gramlot;
61
+ this.Source = gramlot.src.source.constructor;
62
+ this.Bag = gramlot.data.constructor;
63
+ }
64
+ get state() {
65
+ return { undo: this.#undo.at(-1)?.label ?? "", redo: this.#redo.at(-1)?.label ?? "" };
66
+ }
67
+ clear() {
68
+ this.#undo = [];
69
+ this.#redo = [];
70
+ }
71
+ #ref(node) {
72
+ if (!this.#references.has(node)) this.#references.set(node, { node });
73
+ return this.#references.get(node);
74
+ }
75
+ #bind(ref, node) {
76
+ ref.node = node;
77
+ this.#references.set(node, ref);
78
+ }
79
+ #capture(value) {
80
+ if (!(value instanceof this.Bag)) return value;
81
+ return value.getNodes().map((node) => [
82
+ this.#ref(node),
83
+ node.nodeTag,
84
+ node.resolver,
85
+ Object.entries(node.getAttr()).sort(([a], [b]) => a.localeCompare(b)),
86
+ this.#capture(node.getValue(true))
87
+ ]);
88
+ }
89
+ #equal(a, b) {
90
+ return Object.is(a, b) || Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => this.#equal(v, b[i]));
91
+ }
92
+ #commit(label, capture, redo, undo) {
93
+ const before = capture();
94
+ let error;
95
+ try {
96
+ redo();
97
+ } catch (caught) {
98
+ error = caught;
99
+ }
100
+ const after = capture();
101
+ if (!this.#equal(before, after)) {
102
+ this.#undo.push({ label, capture, before, after, undo, redo });
103
+ this.#redo = [];
104
+ }
105
+ if (error) throw error;
106
+ }
107
+ #step(from, to, direction, expected, result) {
108
+ const action = from.at(-1);
109
+ if (!action) return;
110
+ if (this.gramlot.state === "disposed") throw new Error("Inspected page disconnected");
111
+ if (!this.#equal(action.capture(), action[expected])) throw new Error("The target changed outside the inspector. Undo/Redo would overwrite those changes.");
112
+ let error;
113
+ try {
114
+ action[direction]();
115
+ } catch (caught) {
116
+ error = caught;
117
+ }
118
+ if (this.#equal(action.capture(), action[result])) {
119
+ from.pop();
120
+ to.push(action);
121
+ } else if (!error) error = new Error("The application changed the target during Undo/Redo.");
122
+ if (error) throw error;
123
+ }
124
+ undo() {
125
+ this.#step(this.#undo, this.#redo, "undo", "after", "before");
126
+ }
127
+ redo() {
128
+ this.#step(this.#redo, this.#undo, "redo", "before", "after");
129
+ }
130
+ edit(node, attrs, value, changeAttrs, changeValue) {
131
+ const ref = this.#ref(node), oldAttrs = { ...node.getAttr() }, oldValue = node.getValue(true);
132
+ const capture = () => [ref.node.parentBag, this.#captureNode(ref.node)];
133
+ const write = (attributes, content) => {
134
+ if (changeAttrs) ref.node.setAttr(attributes, true, false, false);
135
+ if (changeValue) ref.node.setValue(content);
136
+ };
137
+ this.#commit("Edit value or attributes", capture, () => write(attrs, value), () => write(oldAttrs, oldValue));
138
+ }
139
+ #captureNode(node) {
140
+ return [node.resolver, Object.entries(node.getAttr()).sort(([a], [b]) => a.localeCompare(b)), this.#capture(node.getValue(true))];
141
+ }
142
+ #sourceCapture = () => this.#capture(this.gramlot.src.source);
143
+ #node(path) {
144
+ if (typeof path !== "string" || !path) throw new Error("Select a Source node");
145
+ const node = this.gramlot.src.source.getNode(path);
146
+ if (!node || node.resolver) throw new Error("Source node is missing or deferred");
147
+ return node;
148
+ }
149
+ #removable(node) {
150
+ if (node.parentBag === this.gramlot.src.source) throw new Error("The page Source root cannot be removed or moved");
151
+ }
152
+ #saved(node) {
153
+ return { ref: this.#ref(node), label: node.label, tag: node.nodeTag, value: node.getValue(true), attrs: { ...node.getAttr() } };
154
+ }
155
+ #restore(saved, bag, index) {
156
+ if (bag.getNode(saved.label)) throw new Error(`Source label already exists: ${saved.label}`);
157
+ try {
158
+ bag.setItem(saved.label, saved.value, saved.attrs, index, false, false, "inspector", false, true, null, saved.tag);
159
+ } finally {
160
+ const inserted = bag.getNode(saved.label);
161
+ if (inserted) this.#bind(saved.ref, inserted);
162
+ }
163
+ }
164
+ #parentState(parent) {
165
+ return { value: parent.getValue(true), attrs: { ...parent.getAttr() } };
166
+ }
167
+ #setParent(parent, state) {
168
+ parent.setValue(state.value, true, state.attrs, false);
169
+ parent.setAttr(state.attrs, true, false, false);
170
+ }
171
+ childFields(parent) {
172
+ const specifications = parent._getMeta("gramlot_component")?.childParameters ?? {};
173
+ return Object.entries(specifications).filter(([, spec]) => spec.required).map(([name, spec]) => {
174
+ const children = parent.getValue(true);
175
+ const used = spec.unique && children instanceof this.Source ? children.getNodes().map((child) => child.getAttr(name)) : [];
176
+ return { name, choices: spec.enum.filter((value) => !used.includes(value)) };
177
+ });
178
+ }
179
+ validateChild(parent, attrs, moving = null) {
180
+ const specifications = parent._getMeta("gramlot_component")?.childParameters ?? {};
181
+ for (const [name, spec] of Object.entries(specifications)) {
182
+ const value = attrs[name];
183
+ if (spec.required && value == null) throw new Error(`Choose ${name} before inserting the child`);
184
+ if (spec.enum && value != null && !spec.enum.includes(value)) throw new Error(`Invalid ${name}: ${value}`);
185
+ const children = parent.getValue(true);
186
+ if (spec.unique && children instanceof this.Source && children.getNodes().some((child) => child !== moving && child.getAttr(name) === value)) {
187
+ throw new Error(`${name} is already in use: ${value}`);
188
+ }
189
+ }
190
+ }
191
+ catalog(path) {
192
+ const node = this.#node(path), builder = node.builder;
193
+ return Object.entries(builder.schema).filter(([, info]) => !info._meta?.data_element && !info._meta?.subbuilder && !info._meta?.container).map(([tag, info]) => ({ tag, kind: info._meta?.gramlot_component ? "component" : "html", fields: this.childFields(node) }));
194
+ }
195
+ insert(path, requestedTag, attrs = {}) {
196
+ const parent = this.#node(path), builder = parent.builder;
197
+ const tag = builder.schemaTag(requestedTag);
198
+ if (!this.catalog(path).some((entry) => entry.tag === tag)) throw new Error("Choose an available component or HTML tag");
199
+ this.validateChild(parent, attrs);
200
+ const previous = this.#parentState(parent), parentRef = this.#ref(parent);
201
+ let saved, promoted;
202
+ const redo = () => {
203
+ if (saved) {
204
+ if (!(previous.value instanceof this.Source)) this.#setParent(parentRef.node, promoted);
205
+ this.#restore(saved, parentRef.node.getValue(true), parentRef.node.getValue(true).getNodes().length);
206
+ return;
207
+ }
208
+ const existing = previous.value instanceof this.Source ? new Set(previous.value.getNodes()) : /* @__PURE__ */ new Set();
209
+ try {
210
+ builder.commandOnNode(parentRef.node, tag, null, attrs);
211
+ } finally {
212
+ const body = parentRef.node.getValue(true);
213
+ if (body instanceof this.Source) {
214
+ const added = body.getNodes().find((node) => !existing.has(node));
215
+ if (added) {
216
+ saved = this.#saved(added);
217
+ promoted = this.#parentState(parentRef.node);
218
+ }
219
+ }
220
+ }
221
+ };
222
+ const undo = () => {
223
+ if (saved) saved.ref.node.parentBag.popNode(saved.ref.node.label, "inspector");
224
+ if (!(previous.value instanceof this.Source)) this.#setParent(parentRef.node, previous);
225
+ };
226
+ this.#commit("Add element", this.#sourceCapture, redo, undo);
227
+ return `${path}.${saved.label}`;
228
+ }
229
+ remove(path) {
230
+ const node = this.#node(path);
231
+ this.#removable(node);
232
+ const saved = this.#saved(node), bag = node.parentBag, index = bag.getNodes().indexOf(node);
233
+ this.#commit(
234
+ "Remove branch",
235
+ this.#sourceCapture,
236
+ () => saved.ref.node.parentBag.popNode(saved.label, "inspector"),
237
+ () => this.#restore(saved, bag, index)
238
+ );
239
+ return path.slice(0, path.lastIndexOf("."));
240
+ }
241
+ move(path, destination, placement) {
242
+ if (!["before", "inside", "after"].includes(placement)) throw new Error("Invalid drop position");
243
+ const node = this.#node(path), target = this.#node(destination);
244
+ this.#removable(node);
245
+ if (path === destination || destination.startsWith(path + ".")) throw new Error("A branch cannot be moved into itself");
246
+ const parent = placement === "inside" ? target : target.parentBag.parentNode;
247
+ if (!parent) throw new Error("Drop inside the page Source root");
248
+ const builder = parent.builder;
249
+ if (node.builder !== builder) throw new Error("Branches cannot cross builder dialects");
250
+ this.validateChild(parent, node.getAttr(), node);
251
+ const parentRef = this.#ref(parent);
252
+ const previous = this.#parentState(parent), origin = node.parentBag, oldIndex = origin.getNodes().indexOf(node);
253
+ const body = previous.value;
254
+ if (body instanceof this.Source && body !== origin && body.getNode(node.label)) throw new Error(`Source label already exists: ${node.label}`);
255
+ const children = body instanceof this.Source ? body.getNodes().filter((child) => child !== node) : [];
256
+ if (parent.nodeTag) {
257
+ builder.validateParent(node.nodeTag, parent.nodeTag);
258
+ builder.validateChildren(parent.nodeTag, [...children.map((child) => child.nodeTag), node.nodeTag]);
259
+ }
260
+ let index = placement === "inside" ? children.length : children.indexOf(target) + (placement === "after" ? 1 : 0);
261
+ if (index < 0) throw new Error("Drop target changed");
262
+ if (body === origin) {
263
+ const ref = this.#ref(node);
264
+ this.#commit("Move branch", this.#sourceCapture, () => origin.move(origin.getNodes().indexOf(ref.node), index), () => origin.move(origin.getNodes().indexOf(ref.node), oldIndex));
265
+ } else {
266
+ const saved = this.#saved(node);
267
+ const destinationBag = body instanceof this.Source ? body : new this.Source(null, builder);
268
+ let promoted;
269
+ this.#commit("Move branch", this.#sourceCapture, () => {
270
+ if (!(body instanceof this.Source)) {
271
+ if (promoted) this.#setParent(parentRef.node, promoted);
272
+ else {
273
+ builder.promoteNodeValue(parentRef.node, body, destinationBag);
274
+ promoted = this.#parentState(parentRef.node);
275
+ }
276
+ }
277
+ saved.ref.node.parentBag.popNode(saved.label, "inspector");
278
+ this.#restore(saved, destinationBag, index);
279
+ }, () => {
280
+ saved.ref.node.parentBag.popNode(saved.label, "inspector");
281
+ this.#restore(saved, origin, oldIndex);
282
+ if (!(body instanceof this.Source)) this.#setParent(parentRef.node, previous);
283
+ });
284
+ }
285
+ return `${placement === "inside" ? destination : destination.slice(0, destination.lastIndexOf("."))}.${node.label}`;
286
+ }
287
+ };
55
288
  var InspectorStyles = class {
56
289
  #document;
57
290
  #element = null;
@@ -127,6 +360,7 @@
127
360
  const document2 = gramlot.src.renderer.destination.ownerDocument;
128
361
  const window2 = document2.defaultView;
129
362
  const styles = new InspectorStyles(document2);
363
+ const history = new InspectorHistory(gramlot);
130
364
  const subscriber = `inspector-bridge-${window2.crypto.getRandomValues(new Uint32Array(4)).join("-")}`;
131
365
  const identities = /* @__PURE__ */ new WeakMap();
132
366
  const versions = /* @__PURE__ */ new WeakMap();
@@ -161,7 +395,8 @@
161
395
  const snapshot = () => ({
162
396
  data: describe(bag("data"), "data"),
163
397
  source: describe(bag("source"), "source"),
164
- breakpoints: [...breakpoints.values()].map(({ test, ...record }) => record)
398
+ breakpoints: [...breakpoints.values()].map(({ test, ...record }) => record),
399
+ history: history.state
165
400
  });
166
401
  const send = (data) => {
167
402
  if (!stopped) port.postMessage(data);
@@ -234,8 +469,7 @@
234
469
  if (node.resolver || !scalarType(node.getValue(true))) throw new Error("This value is read-only");
235
470
  value = decodeScalar2(command.value);
236
471
  }
237
- if (command.set.length || command.remove.length) node.setAttr(attrs, true, false, false);
238
- if (Object.hasOwn(command, "value")) node.setValue(value);
472
+ history.edit(node, attrs, value, !!(command.set.length || command.remove.length), Object.hasOwn(command, "value"));
239
473
  };
240
474
  const setBreakpoint = (command) => {
241
475
  bag(command.root);
@@ -261,10 +495,27 @@
261
495
  send({ type: "result", id: message.id, ok: true, styles: styles.snapshot() });
262
496
  return;
263
497
  }
264
- if (message.type === "edit") edit(message.command);
498
+ let selection;
499
+ if (message.type === "catalog") {
500
+ send({ type: "result", id: message.id, ok: true, catalog: history.catalog(message.command.path) });
501
+ return;
502
+ }
503
+ if (["insert", "remove", "move"].includes(message.type)) {
504
+ const command = message.command, node = bag("source").getNode(command.path);
505
+ if (!node || identity(node) !== command.nodeId || (versions.get(node) ?? 0) !== command.revision) throw new Error("Source node changed. Select it again.");
506
+ if (message.type === "insert") selection = history.insert(command.path, command.tag, command.attrs);
507
+ if (message.type === "remove") selection = history.remove(command.path);
508
+ if (message.type === "move") {
509
+ const target = bag("source").getNode(command.destination);
510
+ if (!target || identity(target) !== command.targetId || (versions.get(target) ?? 0) !== command.targetRevision) throw new Error("Drop target changed. Try again.");
511
+ selection = history.move(command.path, command.destination, command.placement);
512
+ }
513
+ } else if (message.type === "undo") history.undo();
514
+ else if (message.type === "redo") history.redo();
515
+ else if (message.type === "edit") edit(message.command);
265
516
  else if (message.type === "breakpoint") setBreakpoint(message.command);
266
517
  else if (message.type !== "refresh") throw new Error(`Unknown inspector command: ${message.type}`);
267
- send({ type: "result", id: message.id, ok: true, snapshot: snapshot() });
518
+ send({ type: "result", id: message.id, ok: true, selection, snapshot: snapshot() });
268
519
  } catch (error) {
269
520
  send({ type: "result", id: message.id, ok: false, error: error.message });
270
521
  }
@@ -275,6 +526,7 @@
275
526
  stopped = true;
276
527
  for (const root of roots) bag(root).unsubscribe(subscriber, { any: true });
277
528
  breakpoints.clear();
529
+ history.clear();
278
530
  styles.dispose();
279
531
  port.removeEventListener("message", receive);
280
532
  window2.removeEventListener("pagehide", dispose);
@@ -306,6 +558,7 @@
306
558
  error = "";
307
559
  breakpoints = [];
308
560
  styles = null;
561
+ history = { undo: "", redo: "" };
309
562
  #info = /* @__PURE__ */ new WeakMap();
310
563
  #listeners = /* @__PURE__ */ new Set();
311
564
  #pending = /* @__PURE__ */ new Map();
@@ -337,6 +590,7 @@
337
590
  if (message.snapshot) {
338
591
  for (const root of roots2) this.#reconcile(this[root], message.snapshot[root]);
339
592
  this.breakpoints = message.snapshot.breakpoints;
593
+ this.history = message.snapshot.history;
340
594
  this.state = "started";
341
595
  this.error = "";
342
596
  } else if (message.type === "error") this.error = message.error;
@@ -347,7 +601,7 @@
347
601
  const pending = this.#pending.get(message.id);
348
602
  if (pending) {
349
603
  this.#pending.delete(message.id);
350
- if (message.ok && !this.error) pending.resolve();
604
+ if (message.ok && !this.error) pending.resolve(message);
351
605
  else pending.reject(new Error(message.error || this.error));
352
606
  }
353
607
  }
@@ -410,8 +664,8 @@
410
664
  this.#notify();
411
665
  }
412
666
  };
413
- 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 { display: inline-flex; align-items: center; justify-content: center; 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\n.property-value select.enum-value { flex: 1; min-width: 0; max-width: none; width: 100%; border: 0; background: white; font: inherit; padding: 1px 5px; }\n\n/* Presentation changes preserve the inspector runtime and selection. */\n:host([hidden]) { display: none; }\n:host([docked]) { display: block; height: 100%; min-height: 0; }\n:host([docked][hidden]) { display: none; }\n:host([docked]) > div { height: 100%; }\n:host([docked]) .targetbar select { min-height: 40px; font-size: 16px; }\n:host([docked]) .property-row { min-height: 36px; font-size: 13px; }\n:host([docked]) .property-row input { min-height: 35px; font-size: 16px; }\n:host([docked]) .remove { opacity: 1; min-height: 35px; width: 32px; }\n\n:host([docked]) { --tree-action-size: 28px; --tree-font-size: 14px; --tree-row-height: 40px; --separator-size: 14px; }\n:host([docked]) .targetbar { height: 44px; }\n:host([docked]) gramlot-poc-tabs > button { height: 44px; font-size: 14px; }\n:host([docked]) .properties-heading { height: 40px; font-size: 13px; }\n:host([docked]) .properties-heading button { width: 36px; height: 36px; }\n:host([docked]) .property-value select { height: 35px; font-size: 14px; }\n";
414
- 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:var(--tree-font-size,11px)/var(--tree-row-height,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:calc(var(--tree-action-size,14px) + 2px); }\n.tree-root > ul { padding-left:2px; }\nsummary, .leaf { position:relative; }\nli { line-height:var(--tree-row-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:calc(-1 * var(--tree-action-size,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:var(--tree-row-height,18px); width:var(--tree-action-size,14px); height:var(--tree-row-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\n@media (hover: none) { .actions { opacity:1; pointer-events:auto; } }\n";
667
+ 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 { display: inline-flex; align-items: center; justify-content: center; 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\n.property-value select.enum-value { flex: 1; min-width: 0; max-width: none; width: 100%; border: 0; background: white; font: inherit; padding: 1px 5px; }\n\n/* Presentation changes preserve the inspector runtime and selection. */\n:host([hidden]) { display: none; }\n:host([docked]) { display: block; height: 100%; min-height: 0; }\n:host([docked][hidden]) { display: none; }\n:host([docked]) > div { height: 100%; }\n:host([docked]) .targetbar select { min-height: 40px; font-size: 16px; }\n:host([docked]) .property-row { min-height: 36px; font-size: 13px; }\n:host([docked]) .property-row input { min-height: 35px; font-size: 16px; }\n:host([docked]) .remove { opacity: 1; min-height: 35px; width: 32px; }\n\n:host([docked]) { --tree-action-size: 28px; --tree-font-size: 14px; --tree-row-height: 40px; --separator-size: 14px; }\n:host([docked]) .targetbar { height: 44px; }\n:host([docked]) gramlot-poc-tabs > button { height: 44px; font-size: 14px; }\n:host([docked]) .properties-heading { height: 40px; font-size: 13px; }\n:host([docked]) .properties-heading button { width: 36px; height: 36px; }\n:host([docked]) .property-value select { height: 35px; font-size: 14px; }\n\n.history-toolbar { flex:none; display:flex; align-items:center; gap:6px; padding:4px 8px; border-bottom:1px solid #d1d7df; background:#f6f8fb; font:11px/1.4 system-ui,sans-serif; }\n.history-toolbar button { flex:none; padding:3px 6px; border:1px solid #c5cdda; border-radius:3px; background:white; cursor:pointer; }\n.history-toolbar button:disabled { opacity:.45; cursor:default; }\n.history-toolbar span { min-width:0; color:#a12121; overflow-wrap:anywhere; }\n";
668
+ 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:var(--tree-font-size,11px)/var(--tree-row-height,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:calc(var(--tree-action-size,14px) + 2px); }\n.tree-root > ul { padding-left:2px; }\nsummary, .leaf { position:relative; }\nli { line-height:var(--tree-row-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:calc(-1 * var(--tree-action-size,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:var(--tree-row-height,18px); width:var(--tree-action-size,14px); height:var(--tree-row-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\n@media (hover: none) { .actions { opacity:1; pointer-events:auto; } }\n\n.actions-end { position:static; margin-left:auto; flex:none; }\n[data-drop=inside] { outline:2px solid #356f9f; outline-offset:-2px; background:#e7f0fc; }\n[data-drop=before] { box-shadow:inset 0 2px #356f9f; }\n[data-drop=after] { box-shadow:inset 0 -2px #356f9f; }\n";
415
669
  var sequence = 0;
416
670
  var GramlotPocTree = class extends HTMLElement {
417
671
  constructor() {
@@ -427,6 +681,9 @@
427
681
  this._expanded = /* @__PURE__ */ new Set();
428
682
  this._nodes = /* @__PURE__ */ new Map();
429
683
  this._rowActions = [];
684
+ this.dragEnabled = false;
685
+ this.fixedPaths = [];
686
+ this._drag = null;
430
687
  this._pendingRender = null;
431
688
  this._subId = `gramlot-poc-tree-${++sequence}`;
432
689
  }
@@ -454,6 +711,12 @@
454
711
  this._selectedPath = path;
455
712
  if (this.isConnected) this._markSelection();
456
713
  }
714
+ revealPath(path) {
715
+ for (const details of this._root.querySelectorAll("details")) {
716
+ if (path.startsWith(details.dataset.path + ".")) details.open = true;
717
+ }
718
+ this.selectedPath = path;
719
+ }
457
720
  get rowActions() {
458
721
  return this._rowActions;
459
722
  }
@@ -483,12 +746,22 @@
483
746
  }
484
747
  _render() {
485
748
  this._pendingRender = null;
749
+ const focused = this.shadowRoot.activeElement;
750
+ const focusedPath = focused?.closest("[data-node-path]")?.dataset.nodePath;
751
+ const focusedAction = focused?.dataset.action;
486
752
  for (const details of this._root.querySelectorAll("details")) {
487
753
  if (details.open) this._expanded.add(details.dataset.path);
488
754
  else this._expanded.delete(details.dataset.path);
489
755
  }
490
756
  this._nodes.clear();
491
757
  this._root.replaceChildren(...this._store ? [this._buildLevel(this._store, "", /* @__PURE__ */ new Set())] : []);
758
+ if (focusedPath) {
759
+ let path = focusedPath;
760
+ while (path && !this._nodes.has(path)) path = path.includes(".") ? path.slice(0, path.lastIndexOf(".")) : "";
761
+ const row = [...this._root.querySelectorAll("[data-node-path]")].find((item) => item.dataset.nodePath === path);
762
+ const action = focusedAction && row && [...row.querySelectorAll("button")].find((button) => button.dataset.action === focusedAction);
763
+ (action || row)?.focus({ preventScroll: true });
764
+ }
492
765
  if (this._selectedPath !== null && !this._nodes.has(this._selectedPath)) {
493
766
  this._selectedPath = null;
494
767
  this._notifySelection();
@@ -531,35 +804,91 @@
531
804
  row.append(caption);
532
805
  row.addEventListener("click", () => this._select(path));
533
806
  this._actions(row, path, node);
807
+ this._dragEvents(row, path, node);
534
808
  list.append(item);
535
809
  }
536
810
  return list;
537
811
  }
538
812
  _actions(row, path, node) {
539
813
  if (!this._rowActions.length) return;
540
- const actions = this.ownerDocument.createElement("span");
541
- actions.className = "actions";
542
- for (const action of this._rowActions) {
543
- const button = this.ownerDocument.createElement("button");
544
- button.type = "button";
545
- button.textContent = action.icon ?? action.label;
546
- button.title = action.label;
547
- button.setAttribute("aria-label", action.label);
548
- button.dataset.action = action.id;
549
- button.classList.toggle("active", action.activePaths?.includes(path) ?? false);
550
- button.classList.toggle("conditional", action.conditionalPaths?.includes(path) ?? false);
551
- button.addEventListener("click", (event) => {
552
- event.preventDefault();
553
- event.stopPropagation();
554
- this.dispatchEvent(new this.ownerDocument.defaultView.CustomEvent("tree-action", {
555
- bubbles: true,
556
- composed: true,
557
- detail: { action: action.id, path, node }
558
- }));
559
- });
560
- actions.append(button);
814
+ for (const position of ["start", "end"]) {
815
+ const actions = this.ownerDocument.createElement("span");
816
+ actions.className = `actions actions-${position}`;
817
+ for (const action of this._rowActions) {
818
+ if ((action.position ?? "start") !== position || action.hiddenPaths?.includes(path)) continue;
819
+ const button = this.ownerDocument.createElement("button");
820
+ button.type = "button";
821
+ button.textContent = action.icon ?? action.label;
822
+ button.title = action.label;
823
+ button.setAttribute("aria-label", action.label);
824
+ button.dataset.action = action.id;
825
+ button.classList.toggle("active", action.activePaths?.includes(path) ?? false);
826
+ button.classList.toggle("conditional", action.conditionalPaths?.includes(path) ?? false);
827
+ button.addEventListener("click", (event) => {
828
+ event.preventDefault();
829
+ event.stopPropagation();
830
+ this.dispatchEvent(new this.ownerDocument.defaultView.CustomEvent("tree-action", {
831
+ bubbles: true,
832
+ composed: true,
833
+ detail: { action: action.id, path, node }
834
+ }));
835
+ });
836
+ actions.append(button);
837
+ }
838
+ if (actions.childElementCount) row.append(actions);
561
839
  }
562
- row.append(actions);
840
+ }
841
+ _clearDrop() {
842
+ for (const row of this._root.querySelectorAll("[data-drop]")) delete row.dataset.drop;
843
+ }
844
+ _dragEvents(row, path, node) {
845
+ if (!this.dragEnabled) return;
846
+ row.draggable = !this.fixedPaths.includes(path);
847
+ row.addEventListener("dragstart", (event) => {
848
+ if (!row.draggable || event.target.closest("button")) {
849
+ event.preventDefault();
850
+ return;
851
+ }
852
+ event.stopPropagation();
853
+ this._drag = { path, node };
854
+ event.dataTransfer.effectAllowed = "move";
855
+ event.dataTransfer.setData("application/x-gramlot-source", this._subId);
856
+ });
857
+ const placement = (event) => {
858
+ const rect = row.getBoundingClientRect();
859
+ const fraction = (event.clientY - rect.top) / rect.height;
860
+ return this.fixedPaths.includes(path) ? "inside" : fraction < 0.25 ? "before" : fraction > 0.75 ? "after" : "inside";
861
+ };
862
+ const valid = () => this._drag && this._drag.path !== path && !path.startsWith(this._drag.path + ".");
863
+ row.addEventListener("dragover", (event) => {
864
+ if (!valid()) return;
865
+ event.preventDefault();
866
+ event.stopPropagation();
867
+ event.dataTransfer.dropEffect = "move";
868
+ this._clearDrop();
869
+ row.dataset.drop = placement(event);
870
+ });
871
+ row.addEventListener("dragleave", () => {
872
+ delete row.dataset.drop;
873
+ });
874
+ row.addEventListener("drop", (event) => {
875
+ event.preventDefault();
876
+ event.stopPropagation();
877
+ this._clearDrop();
878
+ if (!valid() || event.dataTransfer.getData("application/x-gramlot-source") !== this._subId) return;
879
+ const source = this._drag;
880
+ this._drag = null;
881
+ if (this._store.getNode(source.path) !== source.node) return;
882
+ this.dispatchEvent(new this.ownerDocument.defaultView.CustomEvent("tree-drop", {
883
+ bubbles: true,
884
+ composed: true,
885
+ detail: { path: source.path, destination: path, placement: placement(event) }
886
+ }));
887
+ });
888
+ row.addEventListener("dragend", () => {
889
+ this._drag = null;
890
+ this._clearDrop();
891
+ });
563
892
  }
564
893
  _markSelection() {
565
894
  for (const row of this._root.querySelectorAll("[data-node-path]")) {
@@ -736,6 +1065,9 @@
736
1065
  }
737
1066
  }
738
1067
  #target = null;
1068
+ #history = null;
1069
+ #insertTarget = null;
1070
+ #catalog = [];
739
1071
  #connection = null;
740
1072
  #connectionRelease = null;
741
1073
  #targets = [];
@@ -979,6 +1311,67 @@
979
1311
  inspector.#applyEdit(choose);
980
1312
  } else choose();
981
1313
  }
1314
+ sourceAction(node, event) {
1315
+ if (event.detail.action === "breakpoint") {
1316
+ this.openBreakpoint(node, event);
1317
+ return;
1318
+ }
1319
+ const run = () => {
1320
+ if (event.detail.action === "insert") inspector.#openInsert(event.detail.path);
1321
+ if (event.detail.action === "remove") {
1322
+ try {
1323
+ inspector.#runCommand("remove", inspector.#sourceCommand(event.detail.path));
1324
+ } catch (error) {
1325
+ ui.data.setItem("status", error.message);
1326
+ }
1327
+ }
1328
+ };
1329
+ if (ui.data.getItem("editor.dirty")) inspector.#applyEdit(run);
1330
+ else run();
1331
+ }
1332
+ sourceDrop(node, event) {
1333
+ const run = () => {
1334
+ try {
1335
+ const { path, destination, placement } = event.detail;
1336
+ const command = inspector.#sourceCommand(path);
1337
+ const target = inspector.#sourceCommand(destination);
1338
+ inspector.#runCommand("move", { ...command, destination, placement, targetId: target.nodeId, targetRevision: target.revision });
1339
+ } catch (error) {
1340
+ ui.data.setItem("status", error.message);
1341
+ }
1342
+ };
1343
+ if (ui.data.getItem("editor.dirty")) inspector.#applyEdit(run);
1344
+ else run();
1345
+ }
1346
+ insertKind() {
1347
+ inspector.#insertChoices();
1348
+ }
1349
+ addElement() {
1350
+ inspector.#runCommand("insert", { ...inspector.#insertTarget, tag: ui.data.getItem("insert.tag"), attrs: Object.fromEntries((inspector.#catalog[0]?.fields ?? []).map(({ name }) => [name, ui.data.getItem(`insert.attrs.${name}`)])) });
1351
+ }
1352
+ closeInsert() {
1353
+ inspector.#dialog("insert").close();
1354
+ inspector.#insertTarget = null;
1355
+ }
1356
+ undo() {
1357
+ if (ui.data.getItem("editor.dirty")) inspector.#applyEdit(() => inspector.#runCommand("undo"));
1358
+ else inspector.#runCommand("undo");
1359
+ }
1360
+ redo() {
1361
+ if (ui.data.getItem("editor.dirty")) inspector.#applyEdit(() => inspector.#runCommand("redo"));
1362
+ else inspector.#runCommand("redo");
1363
+ }
1364
+ historyKey(node, event) {
1365
+ if (!(event.metaKey || event.ctrlKey) || event.altKey || event.target.closest("input,textarea,select,[contenteditable]")) return;
1366
+ if (event.key.toLowerCase() === "z") {
1367
+ event.preventDefault();
1368
+ event.shiftKey ? this.redo() : this.undo();
1369
+ }
1370
+ if (event.key.toLowerCase() === "y") {
1371
+ event.preventDefault();
1372
+ this.redo();
1373
+ }
1374
+ }
982
1375
  openBreakpoint(node, event) {
983
1376
  if (event.detail.action !== "breakpoint") return;
984
1377
  ui.data.setItem("breakpointDirty", false);
@@ -1048,6 +1441,8 @@
1048
1441
  breakpointCount: "\u25CF 0",
1049
1442
  noBreakpoints: true
1050
1443
  })) ui.data.setItem(key, value);
1444
+ ui.data.setItem("history", new Bag({ noUndo: true, noRedo: true, undo: "", redo: "" }));
1445
+ ui.data.setItem("insert", new Bag({ kind: "html", tag: "", html: true, component: false, status: "" }));
1051
1446
  ui.data.setItem("editor", new Bag({ dirty: false, disabled: true, status: "", mode: "", condition: "" }));
1052
1447
  ui.data.setItem("breakpoints", new Bag());
1053
1448
  ui.data.setItem("breakpointDirty", false);
@@ -1055,7 +1450,7 @@
1055
1450
  const builder = new GramlotBuilder();
1056
1451
  const root = builder.root;
1057
1452
  root.style(gramlot_tools_inspector_default);
1058
- 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", docked: this.hasAttribute("docked") ? "" : null, connect_onclose: "inspector.close" });
1453
+ const palette = this.hasAttribute("embedded") ? root.section({ node_label: "palette", class: "embedded-panel", "aria-label": "Gramlot inspector", connect_onkeydown: "inspector.historyKey" }) : root.webcomponent({ code: "gramlot-poc-palette", node_label: "palette", title: "Gramlot inspector", docked: this.hasAttribute("docked") ? "" : null, connect_onclose: "inspector.close", connect_onkeydown: "inspector.historyKey" });
1059
1454
  const targetbar = palette.div({ node_label: "targetbar", class: "targetbar", hidden: "^singleTarget" });
1060
1455
  targetbar.select({ node_label: "targets", value: "^targetId", disabled: "^busy", "aria-label": "Inspected page", connect_onchange: "inspector.selectTarget" });
1061
1456
  const toolbar = palette.webcomponent({ code: "gramlot-poc-tabs", node_label: "tabs", role: "tablist", "aria-label": "Inspected root" });
@@ -1067,6 +1462,10 @@
1067
1462
  "aria-selected": "false",
1068
1463
  connect_onclick: "inspector.selectRoot"
1069
1464
  });
1465
+ const historybar = palette.div({ class: "history-toolbar", hidden: "^stylesActive" });
1466
+ historybar.html_button("\u21B6 Undo", { type: "button", disabled: "^history.noUndo", title: "^history.undo", connect_onclick: "inspector.undo" });
1467
+ historybar.html_button("\u21B7 Redo", { type: "button", disabled: "^history.noRedo", title: "^history.redo", connect_onclick: "inspector.redo" });
1468
+ historybar.span("^status", { role: "status" });
1070
1469
  palette.p("^error", { role: "alert", class: "error" });
1071
1470
  const content = palette.webcomponent({ code: "gramlot-poc-splitter", node_label: "content", hidden: "^stylesActive" });
1072
1471
  const trees = content.div({ node_label: "tree", slot: "top", class: "tree" });
@@ -1076,7 +1475,8 @@
1076
1475
  "data-root": name,
1077
1476
  hidden: name === "data" ? "^sourceActive" : "^dataActive",
1078
1477
  connect_onchange: "inspector.selectNode",
1079
- "connect_ontree-action": "inspector.openBreakpoint"
1478
+ "connect_ontree-action": "inspector.sourceAction",
1479
+ "connect_ontree-drop": "inspector.sourceDrop"
1080
1480
  });
1081
1481
  const properties = content.section({ node_label: "properties", slot: "bottom", class: "properties", "aria-label": "Selected node properties" });
1082
1482
  const heading = properties.div({ class: "properties-heading" });
@@ -1128,6 +1528,19 @@
1128
1528
  list.h2("Active breakpoints");
1129
1529
  list.div({ node_label: "entries" });
1130
1530
  list.div({ class: "dialog-actions" }).html_button("Close", { type: "button", connect_onclick: "inspector.closeBreakpoints" });
1531
+ const insert = palette.dialog({ node_label: "insert", "aria-label": "Add Source element", class: "breakpoint-dialog", connect_oncancel: "inspector.closeInsert" });
1532
+ insert.h2("Add a child element");
1533
+ const kind = insert.select({ value: "^insert.kind", "aria-label": "Element kind", connect_onchange: "inspector.insertKind" });
1534
+ kind.option("HTML tag", { value: "html" });
1535
+ kind.option("Widget or container", { value: "component" });
1536
+ insert.select({ node_label: "components", value: "^insert.tag", hidden: "^insert.html", "aria-label": "Widget or container" });
1537
+ insert.input({ value: "^insert.tag", live: true, hidden: "^insert.component", list: `${this.#subscriber}-tags`, "aria-label": "HTML tag", placeholder: "div, span\u2026" });
1538
+ insert.datalist({ node_label: "tags", id: `${this.#subscriber}-tags` });
1539
+ insert.div({ node_label: "fields" });
1540
+ insert.p("^insert.status", { role: "status" });
1541
+ const insertActions = insert.div({ class: "dialog-actions" });
1542
+ insertActions.html_button("Cancel", { type: "button", connect_onclick: "inspector.closeInsert" });
1543
+ insertActions.html_button("Add", { type: "button", disabled: "^busy", connect_onclick: "inspector.addElement" });
1131
1544
  ui.src.startSource(builder.source);
1132
1545
  for (const name of ["data", "source"]) this.#trees[name] = ui.dom.getDomNode(ui.src.source.getNode(`main.palette.content.tree.${name}`));
1133
1546
  this.#renderTargets();
@@ -1153,6 +1566,7 @@
1153
1566
  return;
1154
1567
  }
1155
1568
  if (!this.#target) return;
1569
+ this.#history = new InspectorHistory(this.#target);
1156
1570
  this.#subscriptions = [this.#target.data, this.#target.src.source];
1157
1571
  for (const [index, bag] of this.#subscriptions.entries()) bag.subscribe(this.#subscriber, { any: (event) => {
1158
1572
  this.#checkBreakpoints(index === 0 ? "data" : "source", event);
@@ -1160,6 +1574,10 @@
1160
1574
  } });
1161
1575
  }
1162
1576
  #unsubscribe() {
1577
+ this.#history?.clear();
1578
+ this.#history = null;
1579
+ this.#insertTarget = null;
1580
+ if (this.#ui) this.#dialog("insert").close();
1163
1581
  ++this.#styleRequest;
1164
1582
  this.#styles?.dispose();
1165
1583
  this.#styles = null;
@@ -1206,6 +1624,11 @@
1206
1624
  ui.data.setItem("breakpoints", configurations);
1207
1625
  ui.data.setItem("breakpointError", this.#connection.breakpoints.filter((record) => record.error).map((record) => `${record.root}: ${record.path}: ${record.error}`).join("; "));
1208
1626
  }
1627
+ const history = this.#connection ? this.#connection.history : this.#history?.state;
1628
+ for (const action of ["undo", "redo"]) {
1629
+ ui.data.setItem(`history.${action}`, history?.[action] ?? "");
1630
+ ui.data.setItem(`history.no${action === "undo" ? "Undo" : "Redo"}`, !history?.[action] || ui.data.getItem("busy") || this.#state() !== "started");
1631
+ }
1209
1632
  const count = ui.data.getItem("breakpoints").getNodes().length;
1210
1633
  ui.data.setItem("breakpointCount", `\u25CF ${count}`);
1211
1634
  ui.data.setItem("noBreakpoints", count === 0);
@@ -1215,13 +1638,18 @@
1215
1638
  const tree = this.#trees[name];
1216
1639
  tree.storeBag = this.#connection || this.#target ? this.#targetBag(name) : null;
1217
1640
  const breakpoints = ui.data.getItem("breakpoints").getNodes().filter((node) => node.value.getItem("root") === name);
1641
+ tree.dragEnabled = name === "source";
1642
+ tree.fixedPaths = tree.storeBag?.getNodes().map((node) => node.label) ?? [];
1218
1643
  tree.rowActions = [{
1219
1644
  id: "breakpoint",
1220
1645
  icon: "\u25CB",
1221
1646
  label: "Breakpoint",
1222
1647
  activePaths: breakpoints.map((node) => node.value.getItem("path")),
1223
1648
  conditionalPaths: breakpoints.filter((node) => node.value.getItem("condition").trim()).map((node) => node.value.getItem("path"))
1224
- }];
1649
+ }, ...name === "source" ? [
1650
+ { id: "insert", icon: "+", label: "Add child element", position: "end" },
1651
+ { id: "remove", icon: "\u2212", label: "Remove branch", position: "end", hiddenPaths: tree.fixedPaths }
1652
+ ] : []];
1225
1653
  }
1226
1654
  if (this.#target || this.#connection) this.#index(this.#bag(), []);
1227
1655
  this.#trees[root].selectedPath = this.#selected()?.path.join(".") ?? null;
@@ -1591,8 +2019,7 @@ ${declarations}
1591
2019
  });
1592
2020
  return false;
1593
2021
  }
1594
- if (changed) target.setAttr(attrs, true, false, false);
1595
- if (!Object.is(value, this.#snapshot.value)) target.setValue(value);
2022
+ this.#history.edit(target, attrs, value, changed, !Object.is(value, this.#snapshot.value));
1596
2023
  this.#acceptEdit(target);
1597
2024
  this.#refresh();
1598
2025
  this.#ui.data.setItem("editor.status", "");
@@ -1603,6 +2030,91 @@ ${declarations}
1603
2030
  return false;
1604
2031
  }
1605
2032
  }
2033
+ #sourceCommand(path) {
2034
+ const node = this.#targetBag("source").getNode(path);
2035
+ if (!node) throw new Error("Source node no longer exists");
2036
+ const info = this.#connection?.nodeInfo(node);
2037
+ return { path, nodeId: info?.id, revision: info?.revision, ...this.#connection ? {} : { node } };
2038
+ }
2039
+ async #openInsert(path) {
2040
+ if (this.#ui.data.getItem("busy")) return;
2041
+ const generation = this.#generation;
2042
+ try {
2043
+ this.#insertTarget = this.#sourceCommand(path);
2044
+ this.#catalog = this.#connection ? (await this.#connection.request("catalog", { path })).catalog : this.#history.catalog(path);
2045
+ if (!this.#ui || generation !== this.#generation) return;
2046
+ this.#ui.data.setItem("insert.kind", "html");
2047
+ this.#ui.data.setItem("insert.status", "");
2048
+ this.#insertChoices();
2049
+ const builder = new GramlotBuilder();
2050
+ this.#ui.data.setItem("insert.attrs", new Bag());
2051
+ for (const { name, choices } of this.#catalog[0]?.fields ?? []) {
2052
+ builder.root.html_label(name);
2053
+ const field = builder.root.select({ value: `^insert.attrs.${name}`, "aria-label": name });
2054
+ field.option(`Choose ${name}\u2026`, { value: "", disabled: true });
2055
+ for (const value of choices) field.option(value, { value });
2056
+ this.#ui.data.setItem(`insert.attrs.${name}`, "");
2057
+ }
2058
+ this.#ui.src.source.getNode("main.palette.insert.fields").setValue(this.#ui.src.prepareSource(builder.source));
2059
+ this.#dialog("insert").showModal();
2060
+ } catch (error) {
2061
+ if (this.#ui && generation === this.#generation) this.#ui.data.setItem("status", error.message);
2062
+ }
2063
+ }
2064
+ #insertChoices() {
2065
+ const html = this.#ui.data.getItem("insert.kind") === "html";
2066
+ this.#ui.data.setItem("insert.html", html);
2067
+ this.#ui.data.setItem("insert.component", !html);
2068
+ const choices = this.#catalog.filter((item) => item.kind === (html ? "html" : "component"));
2069
+ const builder = new GramlotBuilder();
2070
+ for (const item of choices) builder.root.option(item.tag, { value: item.tag });
2071
+ this.#ui.src.source.getNode(`main.palette.insert.${html ? "tags" : "components"}`).setValue(this.#ui.src.prepareSource(builder.source));
2072
+ this.#ui.data.setItem("insert.tag", html ? "" : choices[0]?.tag ?? "");
2073
+ }
2074
+ async #runCommand(type, command = {}) {
2075
+ const ui = this.#ui;
2076
+ if (ui.data.getItem("busy")) return;
2077
+ const generation = this.#generation;
2078
+ ui.data.setItem("busy", true);
2079
+ ui.data.setItem("status", "");
2080
+ ui.data.setItem("insert.status", "");
2081
+ try {
2082
+ let selection;
2083
+ if (this.#connection) ({ selection } = await this.#connection.request(type, command));
2084
+ else {
2085
+ if (command.path && this.#targetBag("source").getNode(command.path) !== command.node) throw new Error("Source node changed. Select it again.");
2086
+ if (type === "insert") selection = this.#history.insert(command.path, command.tag, command.attrs);
2087
+ if (type === "remove") selection = this.#history.remove(command.path);
2088
+ if (type === "move") selection = this.#history.move(command.path, command.destination, command.placement);
2089
+ if (type === "undo") this.#history.undo();
2090
+ if (type === "redo") this.#history.redo();
2091
+ }
2092
+ if (!this.#ui || generation !== this.#generation) return;
2093
+ if (type === "insert") this.#dialog("insert").close();
2094
+ this.#snapshot = null;
2095
+ ui.data.setItem("editor.dirty", false);
2096
+ this.#refresh();
2097
+ if (selection) {
2098
+ ui.data.setItem("root", "source");
2099
+ ui.data.setItem("tab", "source");
2100
+ this.#refresh();
2101
+ const entry = this.#nodes.find((item) => item.path.join(".") === selection);
2102
+ ui.data.setItem("selection", entry?.id ?? null);
2103
+ this.#trees.source.revealPath(selection);
2104
+ this.#refresh();
2105
+ }
2106
+ } catch (error) {
2107
+ if (this.#ui && generation === this.#generation) {
2108
+ ui.data.setItem("status", error.message);
2109
+ ui.data.setItem("insert.status", error.message);
2110
+ }
2111
+ } finally {
2112
+ if (this.#ui && generation === this.#generation) {
2113
+ ui.data.setItem("busy", false);
2114
+ this.#refresh();
2115
+ }
2116
+ }
2117
+ }
1606
2118
  #findBreakpoint(root, path) {
1607
2119
  return this.#ui.data.getItem("breakpoints").getNodes().find((node) => {
1608
2120
  const value = node.getValue(true);