@jxsuite/studio 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,332 @@
1
+ /**
2
+ * DnD registration functions — extracted from studio.js (Phase 4). Registers drag-and-drop behavior
3
+ * on layer rows, component cards, and element cards.
4
+ */
5
+
6
+ import {
7
+ draggable,
8
+ dropTargetForElements,
9
+ monitorForElements,
10
+ } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
11
+ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
12
+ import {
13
+ attachInstruction,
14
+ extractInstruction,
15
+ } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
16
+
17
+ import {
18
+ getState,
19
+ update,
20
+ leftPanel,
21
+ moveNode,
22
+ insertNode,
23
+ applyMutation,
24
+ getNodeAtPath,
25
+ parentElementPath,
26
+ childIndex,
27
+ isAncestor,
28
+ } from "../store.js";
29
+ import { view } from "../view.js";
30
+ import { componentRegistry, computeRelativePath } from "../files/components.js";
31
+ import { renderComponentPreview } from "./stylebook-panel.js";
32
+ import { defaultDef, unsafeTags } from "./shared.js";
33
+
34
+ /** Register DnD on layer rows — called from left-panel.js after render */
35
+ export function registerLayersDnD() {
36
+ requestAnimationFrame(() => {
37
+ const container = /** @type {any} */ (leftPanel)?.querySelector(".layers-container");
38
+ if (!container) return;
39
+
40
+ container.querySelectorAll("[data-dnd-row]").forEach(
41
+ /** @param {any} row */ (row) => {
42
+ const rowPath = /** @type {string} */ (row.dataset.path)
43
+ .split("/")
44
+ .map((/** @type {any} */ s) => (/^\d+$/.test(s) ? parseInt(s) : s));
45
+ const rowDepth = parseInt(/** @type {string} */ (row.dataset.dndDepth)) || 0;
46
+ const isVoid = row.hasAttribute("data-dnd-void");
47
+
48
+ const cleanup = combine(
49
+ draggable({
50
+ element: row,
51
+ canDrag(/** @type {any} */ { element: _el, input }) {
52
+ const target = /** @type {HTMLElement} */ (
53
+ document.elementFromPoint(input.clientX, input.clientY)
54
+ );
55
+ if (target?.closest(".layer-actions")) return false;
56
+ return true;
57
+ },
58
+ getInitialData() {
59
+ return { type: "tree-node", path: rowPath };
60
+ },
61
+ onDragStart() {
62
+ row.classList.add("dragging");
63
+ view.layerDragSourceHeight = row.offsetHeight;
64
+ },
65
+ onDrop() {
66
+ row.classList.remove("dragging");
67
+ },
68
+ }),
69
+ dropTargetForElements({
70
+ element: row,
71
+ canDrop(/** @type {any} */ { source }) {
72
+ const srcPath = source.data.path;
73
+ if (srcPath && isAncestor(srcPath, rowPath)) return false;
74
+ return true;
75
+ },
76
+ getData(/** @type {any} */ { input, element }) {
77
+ return attachInstruction(
78
+ { path: rowPath },
79
+ /** @type {any} */ ({
80
+ input,
81
+ element,
82
+ currentLevel: rowDepth,
83
+ indentPerLevel: 16,
84
+ block: isVoid ? ["make-child"] : [],
85
+ }),
86
+ );
87
+ },
88
+ onDragEnter(/** @type {any} */ { self }) {
89
+ showLayerDropGap(row, self.data, container);
90
+ },
91
+ onDrag(/** @type {any} */ { self }) {
92
+ showLayerDropGap(row, self.data, container);
93
+ },
94
+ onDragLeave() {
95
+ clearLayerDropGap(container);
96
+ },
97
+ onDrop() {
98
+ clearLayerDropGap(container);
99
+ },
100
+ }),
101
+ );
102
+ view.dndCleanups.push(cleanup);
103
+ },
104
+ );
105
+
106
+ // Global monitor
107
+ const monitorCleanup = monitorForElements({
108
+ onDrop(/** @type {any} */ { source, location }) {
109
+ clearLayerDropGap(container);
110
+ const target = location.current.dropTargets[0];
111
+ if (!target) return;
112
+ const instruction = extractInstruction(target.data);
113
+ if (!instruction || instruction.type === "instruction-blocked") return;
114
+ const srcData = source.data;
115
+ const targetPath = target.data.path;
116
+ applyDropInstruction(instruction, srcData, targetPath);
117
+ },
118
+ });
119
+ view.dndCleanups.push(monitorCleanup);
120
+ });
121
+ }
122
+
123
+ /** Register DnD on component rows — called from renderLeftPanel when tab=components */
124
+ export function registerComponentsDnD() {
125
+ requestAnimationFrame(() => {
126
+ const container = /** @type {any} */ (leftPanel)?.querySelector(".components-section");
127
+ if (!container) return;
128
+
129
+ container.querySelectorAll("[data-component-tag]").forEach(
130
+ /** @param {any} row */ (row) => {
131
+ const tagName = row.dataset.componentTag;
132
+ if (!tagName) return;
133
+ const comp = componentRegistry.find(/** @param {any} c */ (c) => c.tagName === tagName);
134
+ if (!comp) return;
135
+
136
+ // Fill preview with live rendered component
137
+ const preview = row.querySelector(".element-card-preview");
138
+ if (preview && !preview.querySelector(tagName)) {
139
+ renderComponentPreview(comp).then((/** @type {any} */ el) => {
140
+ preview.textContent = "";
141
+ preview.appendChild(el);
142
+ });
143
+ }
144
+
145
+ const instanceDef = {
146
+ tagName: comp.tagName,
147
+ $props: Object.fromEntries(
148
+ comp.props.map((/** @type {any} */ p) => [
149
+ p.name,
150
+ p.default !== undefined ? p.default : "",
151
+ ]),
152
+ ),
153
+ };
154
+ const cleanup = draggable({
155
+ element: row,
156
+ getInitialData() {
157
+ return { type: "block", fragment: structuredClone(instanceDef) };
158
+ },
159
+ });
160
+ view.dndCleanups.push(cleanup);
161
+ },
162
+ );
163
+ });
164
+ }
165
+
166
+ /** Register DnD on element (HTML block) rows */
167
+ export function registerElementsDnD() {
168
+ requestAnimationFrame(() => {
169
+ const container = /** @type {any} */ (leftPanel)?.querySelector(".panel-body");
170
+ if (!container) return;
171
+ container.querySelectorAll("[data-block-tag]").forEach(
172
+ /** @param {any} row */ (row) => {
173
+ const tag = row.dataset.blockTag;
174
+ const preview = row.querySelector(".element-card-preview");
175
+ if (preview && !preview.firstChild) {
176
+ const el = document.createElement(unsafeTags.has(tag) ? "span" : tag);
177
+ el.textContent = tag;
178
+ preview.appendChild(el);
179
+ }
180
+ const def = defaultDef(tag);
181
+ const cleanup = draggable({
182
+ element: row,
183
+ getInitialData() {
184
+ return { type: "block", fragment: structuredClone(def) };
185
+ },
186
+ });
187
+ view.dndCleanups.push(cleanup);
188
+ },
189
+ );
190
+ });
191
+ }
192
+
193
+ /**
194
+ * @param {any} rowEl
195
+ * @param {any} data
196
+ * @param {any} container
197
+ */
198
+ export function showLayerDropGap(rowEl, data, container) {
199
+ const instruction = extractInstruction(data);
200
+
201
+ // Clear previous drop-target highlight
202
+ if (view._currentDropTargetRow && view._currentDropTargetRow !== rowEl) {
203
+ view._currentDropTargetRow.classList.remove("drop-target");
204
+ }
205
+
206
+ if (!instruction || instruction.type === "instruction-blocked") {
207
+ clearLayerDropGap(container);
208
+ return;
209
+ }
210
+
211
+ if (instruction.type === "make-child") {
212
+ clearLayerDropGap(container);
213
+ rowEl.classList.add("drop-target");
214
+ view._currentDropTargetRow = rowEl;
215
+ return;
216
+ }
217
+
218
+ rowEl.classList.remove("drop-target");
219
+ view._currentDropTargetRow = rowEl;
220
+
221
+ // Shift rows to create gap
222
+ const rows = Array.from(container.querySelectorAll(".layers-tree .layer-row"));
223
+ const targetIdx = rows.indexOf(rowEl);
224
+ const gap = view.layerDragSourceHeight;
225
+
226
+ for (let i = 0; i < rows.length; i++) {
227
+ if (/** @type {any} */ (rows[i]).classList.contains("dragging")) continue;
228
+ if (instruction.type === "reorder-above") {
229
+ /** @type {any} */ (rows[i]).style.transform = i >= targetIdx ? `translateY(${gap}px)` : "";
230
+ } else {
231
+ /** @type {any} */ (rows[i]).style.transform = i > targetIdx ? `translateY(${gap}px)` : "";
232
+ }
233
+ }
234
+ }
235
+
236
+ /** @param {any} container */
237
+ export function clearLayerDropGap(container) {
238
+ if (view._currentDropTargetRow) {
239
+ view._currentDropTargetRow.classList.remove("drop-target");
240
+ view._currentDropTargetRow = null;
241
+ }
242
+ const rows = container.querySelectorAll(".layers-tree .layer-row");
243
+ for (const r of rows) /** @type {any} */ (r).style.transform = "";
244
+ }
245
+
246
+ /**
247
+ * Apply a DnD instruction to the state
248
+ *
249
+ * @param {any} instruction
250
+ * @param {any} srcData
251
+ * @param {any} targetPath
252
+ */
253
+ export function applyDropInstruction(instruction, srcData, targetPath) {
254
+ const S = getState();
255
+ if (srcData.type === "tree-node") {
256
+ const fromPath = srcData.path;
257
+ const targetParent = /** @type {any} */ (parentElementPath(targetPath));
258
+ const targetIdx = /** @type {number} */ (childIndex(targetPath));
259
+
260
+ switch (instruction.type) {
261
+ case "reorder-above":
262
+ update(moveNode(S, fromPath, targetParent, targetIdx));
263
+ break;
264
+ case "reorder-below":
265
+ update(moveNode(S, fromPath, targetParent, targetIdx + 1));
266
+ break;
267
+ case "make-child": {
268
+ const target = getNodeAtPath(S.document, targetPath);
269
+ const len = target?.children?.length || 0;
270
+ update(moveNode(S, fromPath, targetPath, len));
271
+ break;
272
+ }
273
+ }
274
+ } else if (srcData.type === "block") {
275
+ const targetParent = /** @type {any} */ (parentElementPath(targetPath));
276
+ const targetIdx = /** @type {number} */ (childIndex(targetPath));
277
+
278
+ switch (instruction.type) {
279
+ case "reorder-above":
280
+ update(insertNode(S, targetParent, targetIdx, structuredClone(srcData.fragment)));
281
+ break;
282
+ case "reorder-below":
283
+ update(insertNode(S, targetParent, targetIdx + 1, structuredClone(srcData.fragment)));
284
+ break;
285
+ case "make-child": {
286
+ const target = getNodeAtPath(S.document, targetPath);
287
+ const len = target?.children?.length || 0;
288
+ update(insertNode(S, targetPath, len, structuredClone(srcData.fragment)));
289
+ break;
290
+ }
291
+ }
292
+
293
+ // Auto-import to $elements if the dropped block is a custom component
294
+ const tag = srcData.fragment?.tagName;
295
+ if (tag && tag.includes("-")) {
296
+ const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === tag);
297
+ if (comp) {
298
+ const currentS = getState();
299
+ const elements = currentS.document.$elements || [];
300
+ if (comp.source === "npm") {
301
+ const specifier = comp.modulePath ? `${comp.package}/${comp.modulePath}` : comp.package;
302
+ const alreadyImported = elements.some(
303
+ (/** @type {any} */ e) => e === specifier || e === comp.package,
304
+ );
305
+ if (!alreadyImported) {
306
+ update(
307
+ applyMutation(currentS, (/** @type {any} */ doc) => {
308
+ if (!doc.$elements) doc.$elements = [];
309
+ doc.$elements.push(specifier);
310
+ }),
311
+ );
312
+ }
313
+ } else {
314
+ const alreadyImported = elements.some(
315
+ (/** @type {any} */ e) =>
316
+ e.$ref &&
317
+ (e.$ref === `./${comp.path}` || e.$ref.endsWith(comp.path.split("/").pop())),
318
+ );
319
+ if (!alreadyImported) {
320
+ const relPath = computeRelativePath(currentS.documentPath, comp.path);
321
+ update(
322
+ applyMutation(currentS, (/** @type {any} */ doc) => {
323
+ if (!doc.$elements) doc.$elements = [];
324
+ doc.$elements.push({ $ref: relPath });
325
+ }),
326
+ );
327
+ }
328
+ }
329
+ }
330
+ }
331
+ }
332
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Editor panels — extracted from studio.js (Phase 4g). Monaco-based function editor (JS mode) and
3
+ * completion provider for state scope variables.
4
+ */
5
+
6
+ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
7
+ import { html, render as litRender, nothing } from "lit-html";
8
+ import { ref } from "lit-html/directives/ref.js";
9
+
10
+ import {
11
+ getState,
12
+ update,
13
+ renderOnly,
14
+ canvasWrap,
15
+ canvasPanels,
16
+ updateDef,
17
+ updateProperty,
18
+ getNodeAtPath,
19
+ } from "../store.js";
20
+ import { view } from "../view.js";
21
+ import { codeService, setLintMarkers, getFunctionArgs } from "../services/code-services.js";
22
+
23
+ function getFunctionBody(/** @type {any} */ editing) {
24
+ const S = getState();
25
+ if (editing.type === "def") {
26
+ return S.document.state?.[editing.defName]?.body || "";
27
+ } else if (editing.type === "event") {
28
+ const node = getNodeAtPath(S.document, editing.path);
29
+ return node?.[editing.eventKey]?.body || "";
30
+ }
31
+ return "";
32
+ }
33
+
34
+ export function renderFunctionEditor() {
35
+ const S = getState();
36
+ const editing = S.ui.editingFunction;
37
+
38
+ // If editor already exists and matches current target, just sync value
39
+ if (view.functionEditor && view.functionEditor._editingTarget === JSON.stringify(editing)) {
40
+ const body = getFunctionBody(editing);
41
+ const currentVal = view.functionEditor.getValue();
42
+ if (currentVal !== body) {
43
+ view.functionEditor._ignoreNextChange = true;
44
+ view.functionEditor.setValue(body);
45
+ }
46
+ return;
47
+ }
48
+
49
+ // Dispose previous editors
50
+ if (view.functionEditor) {
51
+ view.functionEditor.dispose();
52
+ view.functionEditor = null;
53
+ }
54
+ if (view.monacoEditor) {
55
+ view.monacoEditor.dispose();
56
+ view.monacoEditor = null;
57
+ }
58
+
59
+ // Clean up canvas DnD and event handlers
60
+ for (const fn of view.canvasDndCleanups) fn();
61
+ view.canvasDndCleanups = [];
62
+ for (const fn of view.canvasEventCleanups) fn();
63
+ view.canvasEventCleanups = [];
64
+ canvasPanels.length = 0;
65
+
66
+ litRender(nothing, canvasWrap);
67
+ canvasWrap.style.padding = "0";
68
+
69
+ // Toolbar breadcrumb handles context display — re-render it
70
+ renderOnly("toolbar");
71
+
72
+ // Editor container
73
+ /** @type {HTMLDivElement | null} */
74
+ let editorContainer = null;
75
+ litRender(
76
+ html`<div
77
+ class="source-editor"
78
+ ${ref((el) => {
79
+ if (el) editorContainer = /** @type {HTMLDivElement} */ (el);
80
+ })}
81
+ ></div>`,
82
+ canvasWrap,
83
+ );
84
+
85
+ const body = getFunctionBody(editing);
86
+ const args = getFunctionArgs(editing, S);
87
+
88
+ view.functionEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
89
+ value: body,
90
+ language: "javascript",
91
+ theme: "vs-dark",
92
+ automaticLayout: true,
93
+ minimap: { enabled: false },
94
+ fontSize: 12,
95
+ fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
96
+ lineNumbers: "on",
97
+ scrollBeyondLastLine: false,
98
+ wordWrap: "on",
99
+ tabSize: 2,
100
+ });
101
+ view.functionEditor._editingTarget = JSON.stringify(editing);
102
+
103
+ // Format on open — show pretty-printed code, then run initial lint
104
+ codeService("format", { code: body, args }).then((result) => {
105
+ if (result?.code != null && view.functionEditor) {
106
+ view.functionEditor._ignoreNextChange = true;
107
+ view.functionEditor.setValue(result.code);
108
+ }
109
+ });
110
+ codeService("lint", { code: body, args }).then((result) => {
111
+ if (result?.diagnostics && view.functionEditor)
112
+ setLintMarkers(view.functionEditor, result.diagnostics);
113
+ });
114
+
115
+ // Debounced sync back to state + lint on edit
116
+ /** @type {any} */
117
+ let syncDebounce;
118
+ /** @type {any} */
119
+ let lintDebounce;
120
+ let lintGen = 0;
121
+ view.functionEditor.onDidChangeModelContent(() => {
122
+ if (view.functionEditor._ignoreNextChange) {
123
+ view.functionEditor._ignoreNextChange = false;
124
+ return;
125
+ }
126
+
127
+ clearTimeout(syncDebounce);
128
+ syncDebounce = setTimeout(() => {
129
+ const S = getState();
130
+ const newBody = view.functionEditor.getValue();
131
+ if (editing.type === "def") {
132
+ update(updateDef(S, editing.defName, { body: newBody }));
133
+ } else if (editing.type === "event") {
134
+ const node = getNodeAtPath(S.document, editing.path);
135
+ const current = node?.[editing.eventKey] || {};
136
+ update(
137
+ updateProperty(S, editing.path, editing.eventKey, {
138
+ ...current,
139
+ $prototype: "Function",
140
+ body: newBody,
141
+ }),
142
+ );
143
+ }
144
+ renderOnly("leftPanel");
145
+ }, 500);
146
+
147
+ clearTimeout(lintDebounce);
148
+ lintDebounce = setTimeout(() => {
149
+ const gen = ++lintGen;
150
+ const currentCode = view.functionEditor.getValue();
151
+ codeService("lint", { code: currentCode, args }).then((result) => {
152
+ if (gen !== lintGen) return;
153
+ if (result?.diagnostics && view.functionEditor)
154
+ setLintMarkers(view.functionEditor, result.diagnostics);
155
+ });
156
+ }, 750);
157
+ });
158
+ }
159
+
160
+ // Register Monaco JS completion provider for state scope variables (once)
161
+ export function registerFunctionCompletions() {
162
+ if (view._completionRegistered) return;
163
+ view._completionRegistered = true;
164
+ monaco.languages.registerCompletionItemProvider("javascript", {
165
+ triggerCharacters: ["."],
166
+ provideCompletionItems(model, position) {
167
+ const S = getState();
168
+ const defs = S?.document?.state || {};
169
+ const word = model.getWordUntilPosition(position);
170
+ const range = {
171
+ startLineNumber: position.lineNumber,
172
+ endLineNumber: position.lineNumber,
173
+ startColumn: word.startColumn,
174
+ endColumn: word.endColumn,
175
+ };
176
+
177
+ const suggestions = Object.entries(defs).map(([key, def]) => {
178
+ let kind = monaco.languages.CompletionItemKind.Variable;
179
+ if (
180
+ /** @type {any} */ (def)?.$prototype === "Function" ||
181
+ /** @type {any} */ (def)?.$handler
182
+ )
183
+ kind = monaco.languages.CompletionItemKind.Function;
184
+ else if (/** @type {any} */ (def)?.$prototype)
185
+ kind = monaco.languages.CompletionItemKind.Property;
186
+ return {
187
+ label: `state.${key}`,
188
+ kind,
189
+ insertText: `state.${key}`,
190
+ range,
191
+ };
192
+ });
193
+ return { suggestions };
194
+ },
195
+ });
196
+ }
@@ -0,0 +1,148 @@
1
+ /** Elements panel — block/component palette with categorized accordion and search filter. */
2
+
3
+ import { html, nothing } from "lit-html";
4
+ import { getState, update, getNodeAtPath, insertNode } from "../store.js";
5
+ import { view } from "../view.js";
6
+ import { getEffectiveElements } from "../site-context.js";
7
+ import { componentRegistry } from "../files/components.js";
8
+
9
+ /**
10
+ * @param {{ webdata: any; defaultDef: (tag: string) => any; rerender: () => void }} ctx
11
+ * @returns {import("lit-html").TemplateResult}
12
+ */
13
+ export function renderElementsTemplate(ctx) {
14
+ const S = getState();
15
+
16
+ const categories = Object.entries(ctx.webdata.elements).map(
17
+ (/** @type {any} */ [category, elements]) => {
18
+ const filtered = view.elementsFilter
19
+ ? elements.filter((/** @type {any} */ e) => e.tag.includes(view.elementsFilter))
20
+ : elements;
21
+ if (filtered.length === 0) return nothing;
22
+
23
+ return html`
24
+ <sp-accordion-item
25
+ label=${category}
26
+ ?open=${!view.elementsCollapsed.has(category)}
27
+ @sp-accordion-item-toggle=${(/** @type {any} */ e) => {
28
+ if (e.target.open) view.elementsCollapsed.delete(category);
29
+ else view.elementsCollapsed.add(category);
30
+ }}
31
+ >
32
+ ${filtered.map((/** @type {any} */ { tag }) => {
33
+ const def = ctx.defaultDef(tag);
34
+ return html`
35
+ <div
36
+ class="element-card"
37
+ data-block-tag=${tag}
38
+ @click=${() => {
39
+ const s = getState();
40
+ const parentPath = s.selection || [];
41
+ const parent = getNodeAtPath(s.document, parentPath);
42
+ const idx = parent?.children ? parent.children.length : 0;
43
+ update(insertNode(s, parentPath, idx, structuredClone(def)));
44
+ }}
45
+ >
46
+ <div class="element-card-preview"></div>
47
+ <div class="element-card-label">&lt;${tag}&gt;</div>
48
+ </div>
49
+ `;
50
+ })}
51
+ </sp-accordion-item>
52
+ `;
53
+ },
54
+ );
55
+
56
+ const effectiveEls = getEffectiveElements(S.document?.$elements);
57
+ /** @type {Set<string>} */
58
+ const enabledTags = new Set();
59
+ for (const entry of effectiveEls) {
60
+ if (typeof entry !== "string") continue;
61
+ const comp = componentRegistry.find(
62
+ (/** @type {any} */ c) =>
63
+ c.source === "npm" && c.modulePath && entry === `${c.package}/${c.modulePath}`,
64
+ );
65
+ if (comp) {
66
+ enabledTags.add(comp.tagName);
67
+ } else {
68
+ for (const c of componentRegistry) {
69
+ if (c.source === "npm" && c.package === entry) enabledTags.add(c.tagName);
70
+ }
71
+ }
72
+ }
73
+ const compsFiltered =
74
+ componentRegistry.length > 0
75
+ ? componentRegistry
76
+ .filter((/** @type {any} */ c) => c.source !== "npm" || enabledTags.has(c.tagName))
77
+ .filter(
78
+ (/** @type {any} */ c) =>
79
+ !view.elementsFilter || c.tagName.toLowerCase().includes(view.elementsFilter),
80
+ )
81
+ : [];
82
+
83
+ const componentsAccordion =
84
+ compsFiltered.length > 0
85
+ ? html`
86
+ <sp-accordion-item
87
+ label="Components"
88
+ ?open=${!view.elementsCollapsed.has("Components")}
89
+ @sp-accordion-item-toggle=${(/** @type {any} */ e) => {
90
+ if (e.target.open) view.elementsCollapsed.delete("Components");
91
+ else view.elementsCollapsed.add("Components");
92
+ }}
93
+ >
94
+ <div class="components-section">
95
+ ${compsFiltered.map(
96
+ (/** @type {any} */ comp) => html`
97
+ <div
98
+ class="element-card"
99
+ data-component-tag=${comp.tagName}
100
+ title=${comp.source === "npm"
101
+ ? `${comp.package}: <${comp.tagName}>`
102
+ : comp.path}
103
+ @click=${() => {
104
+ const s = getState();
105
+ const parentPath = s.selection || [];
106
+ const parent = getNodeAtPath(s.document, parentPath);
107
+ const idx = parent?.children ? parent.children.length : 0;
108
+ const instanceDef = {
109
+ tagName: comp.tagName,
110
+ $props: Object.fromEntries(
111
+ (comp.props || []).map((/** @type {any} */ p) => [
112
+ p.name,
113
+ p.default !== undefined ? p.default : "",
114
+ ]),
115
+ ),
116
+ };
117
+ update(insertNode(s, parentPath, idx, structuredClone(instanceDef)));
118
+ }}
119
+ >
120
+ <div class="element-card-preview">
121
+ <span style="color:var(--fg-dim);font-size:11px;font-style:italic"
122
+ >&lt;${comp.tagName}&gt;</span
123
+ >
124
+ </div>
125
+ <div class="element-card-label">${comp.tagName}</div>
126
+ </div>
127
+ `,
128
+ )}
129
+ </div>
130
+ </sp-accordion-item>
131
+ `
132
+ : nothing;
133
+
134
+ return html`
135
+ <sp-search
136
+ size="s"
137
+ placeholder="Filter elements…"
138
+ value=${view.elementsFilter}
139
+ @input=${(/** @type {any} */ e) => {
140
+ view.elementsFilter = e.target.value.toLowerCase();
141
+ ctx.rerender();
142
+ }}
143
+ ></sp-search>
144
+ <sp-accordion class="elements-list" allow-multiple
145
+ >${componentsAccordion}${categories}</sp-accordion
146
+ >
147
+ `;
148
+ }