@jxsuite/studio 0.21.4 → 0.22.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.
- package/dist/studio.js +532 -252
- package/dist/studio.js.map +15 -15
- package/package.json +4 -4
- package/src/canvas/canvas-live-render.js +6 -5
- package/src/canvas/canvas-render.js +4 -3
- package/src/editor/context-menu.js +146 -26
- package/src/editor/insertion-helper.js +4 -3
- package/src/markdown/md-convert.js +7 -0
- package/src/panels/head-panel.js +105 -2
- package/src/panels/right-panel.js +142 -90
- package/src/panels/signals-panel.js +246 -129
- package/src/panels/style-panel.js +44 -1
- package/src/services/code-services.js +12 -5
- package/src/settings/head-editor.js +4 -4
- package/src/tabs/transact.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
|
33
33
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
34
|
-
"@jxsuite/parser": "^0.
|
|
35
|
-
"@jxsuite/runtime": "^0.
|
|
36
|
-
"@jxsuite/schema": "^0.
|
|
34
|
+
"@jxsuite/parser": "^0.22.0",
|
|
35
|
+
"@jxsuite/runtime": "^0.22.0",
|
|
36
|
+
"@jxsuite/schema": "^0.22.0",
|
|
37
37
|
"@spectrum-web-components/accordion": "^1.12.1",
|
|
38
38
|
"@spectrum-web-components/action-bar": "1.12.1",
|
|
39
39
|
"@spectrum-web-components/action-button": "^1.12.1",
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
buildScope,
|
|
14
14
|
defineElement,
|
|
15
15
|
setSkipServerFunctions,
|
|
16
|
-
setSkipContentResolution,
|
|
17
16
|
} from "@jxsuite/runtime";
|
|
18
17
|
import {
|
|
19
18
|
getEffectiveElements,
|
|
@@ -139,7 +138,6 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
|
139
138
|
// failed proxy calls and infinite reactive retries (also covers
|
|
140
139
|
// async custom element connectedCallbacks that run after this function returns)
|
|
141
140
|
setSkipServerFunctions(canvasMode !== "preview");
|
|
142
|
-
setSkipContentResolution(canvasMode !== "preview");
|
|
143
141
|
|
|
144
142
|
let renderDoc =
|
|
145
143
|
canvasMode === "preview"
|
|
@@ -219,9 +217,9 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
|
219
217
|
/** @type {(JxElement | string)[]} */ (renderDoc.$elements),
|
|
220
218
|
);
|
|
221
219
|
|
|
222
|
-
// In content mode (markdown), auto-discover components
|
|
223
|
-
// custom elements that have no explicit $elements registration.
|
|
224
|
-
if (S.mode === "content" && componentRegistry.length > 0) {
|
|
220
|
+
// In content mode (markdown) or when a layout is applied, auto-discover components
|
|
221
|
+
// for custom elements that have no explicit $elements registration.
|
|
222
|
+
if ((S.mode === "content" || layoutWrapped) && componentRegistry.length > 0) {
|
|
225
223
|
const existingRefs = new Set(
|
|
226
224
|
effectiveElements.map((/** @type {JxElement | string} */ e) =>
|
|
227
225
|
typeof e === "string" ? e : e?.$ref,
|
|
@@ -367,6 +365,9 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
|
367
365
|
for (const entry of effectiveHead) {
|
|
368
366
|
if (!entry?.tagName) continue;
|
|
369
367
|
const tag = entry.tagName.toLowerCase();
|
|
368
|
+
// Skip inline scripts — they can contain arbitrary JS/HTML that throws
|
|
369
|
+
// when the browser tries to execute it in the studio context
|
|
370
|
+
if (tag === "script" && !entry.attributes?.src) continue;
|
|
370
371
|
const attrs = { ...entry.attributes };
|
|
371
372
|
for (const key of ["href", "src"]) {
|
|
372
373
|
const val = attrs[key];
|
|
@@ -664,11 +664,12 @@ function applyCanvasMediaOverrides(canvasEl, activeBreakpoints) {
|
|
|
664
664
|
const tab = activeTab.value;
|
|
665
665
|
if (!tab) return;
|
|
666
666
|
const docMedia = getEffectiveMedia(tab.doc.document.$media || {});
|
|
667
|
-
|
|
667
|
+
// Build a set of CSS condition texts that match active breakpoints
|
|
668
|
+
const activeConditions = new Set();
|
|
668
669
|
for (const name of activeBreakpoints) {
|
|
669
|
-
if (docMedia[name])
|
|
670
|
+
if (docMedia[name]) activeConditions.add(docMedia[name]);
|
|
670
671
|
}
|
|
671
|
-
const overrides = collectMediaOverrides(document.styleSheets,
|
|
672
|
+
const overrides = collectMediaOverrides(document.styleSheets, activeConditions);
|
|
672
673
|
applyOverridesToCanvas(canvasEl, overrides);
|
|
673
674
|
}
|
|
674
675
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// ─── Clipboard & Context Menu ─────────────────────────────────────────────────
|
|
2
2
|
import { html } from "lit-html";
|
|
3
3
|
import { ref } from "lit-html/directives/ref.js";
|
|
4
|
+
import { htmlToJx } from "@jxsuite/parser/html-to-jx";
|
|
4
5
|
import { getNodeAtPath, parentElementPath, childIndex } from "../store.js";
|
|
5
6
|
import { activeTab, workspace } from "../workspace/workspace.js";
|
|
6
7
|
import {
|
|
@@ -26,33 +27,135 @@ import { startLayerTitleEdit } from "../panels/layers-panel.js";
|
|
|
26
27
|
* @typedef {JxMutableNode} JxNode
|
|
27
28
|
*/
|
|
28
29
|
|
|
29
|
-
// ─── Clipboard
|
|
30
|
+
// ─── Clipboard helpers ───────────────────────────────────────────────────────
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
const JX_MIME = "web application/jx+json";
|
|
33
|
+
|
|
34
|
+
/** @param {JxNode | string} node */
|
|
35
|
+
function nodeToHtml(node) {
|
|
36
|
+
if (typeof node === "string") return node;
|
|
37
|
+
const tag = node.tagName || "div";
|
|
38
|
+
let attrs = "";
|
|
39
|
+
if (node.attributes) {
|
|
40
|
+
for (const [k, v] of Object.entries(node.attributes)) {
|
|
41
|
+
attrs += v === "" ? ` ${k}` : ` ${k}="${v.replace(/"/g, """)}"`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (node.style) {
|
|
45
|
+
const css = Object.entries(node.style)
|
|
46
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
47
|
+
.join(";");
|
|
48
|
+
if (css) attrs += ` style="${css.replace(/"/g, """)}"`;
|
|
49
|
+
}
|
|
50
|
+
let inner = "";
|
|
51
|
+
if (node.textContent) {
|
|
52
|
+
inner = node.textContent;
|
|
53
|
+
} else if (node.children) {
|
|
54
|
+
inner = node.children.map((c) => nodeToHtml(c)).join("");
|
|
55
|
+
}
|
|
56
|
+
return `<${tag}${attrs}>${inner}</${tag}>`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Write a Jx node to the system clipboard with both jx+json and text/html types.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} json
|
|
63
|
+
*/
|
|
64
|
+
async function writeToClipboard(json) {
|
|
65
|
+
workspace.clipboard = json;
|
|
66
|
+
try {
|
|
67
|
+
await navigator.clipboard.write([
|
|
68
|
+
new ClipboardItem({
|
|
69
|
+
[JX_MIME]: new Blob([JSON.stringify(json)], { type: JX_MIME }),
|
|
70
|
+
"text/html": new Blob([nodeToHtml(json)], { type: "text/html" }),
|
|
71
|
+
}),
|
|
72
|
+
]);
|
|
73
|
+
} catch {
|
|
74
|
+
// Fallback: write as plain text if custom MIME not supported
|
|
75
|
+
try {
|
|
76
|
+
await navigator.clipboard.writeText(JSON.stringify(json));
|
|
77
|
+
} catch {
|
|
78
|
+
// clipboard API unavailable — workspace.clipboard is the fallback
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read from the system clipboard. Returns Jx node(s) or null.
|
|
85
|
+
*
|
|
86
|
+
* @returns {Promise<JxNode[] | null>}
|
|
87
|
+
*/
|
|
88
|
+
async function readFromClipboard() {
|
|
89
|
+
try {
|
|
90
|
+
const items = await navigator.clipboard.read();
|
|
91
|
+
for (const item of items) {
|
|
92
|
+
if (item.types.includes(JX_MIME)) {
|
|
93
|
+
const blob = await item.getType(JX_MIME);
|
|
94
|
+
const json = JSON.parse(await blob.text());
|
|
95
|
+
return [json];
|
|
96
|
+
}
|
|
97
|
+
if (item.types.includes("text/html")) {
|
|
98
|
+
const blob = await item.getType("text/html");
|
|
99
|
+
const htmlStr = await blob.text();
|
|
100
|
+
const nodes = htmlToJx(htmlStr);
|
|
101
|
+
const jxNodes = /** @type {JxNode[]} */ (
|
|
102
|
+
nodes.map((n) => (typeof n === "string" ? { tagName: "p", textContent: n } : n))
|
|
103
|
+
);
|
|
104
|
+
if (jxNodes.length > 0) return jxNodes;
|
|
105
|
+
}
|
|
106
|
+
if (item.types.includes("text/plain")) {
|
|
107
|
+
const blob = await item.getType("text/plain");
|
|
108
|
+
const text = await blob.text();
|
|
109
|
+
// Try parsing as Jx JSON
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(text);
|
|
112
|
+
if (parsed && parsed.tagName) return [parsed];
|
|
113
|
+
} catch {
|
|
114
|
+
// plain text → paragraph node
|
|
115
|
+
}
|
|
116
|
+
if (text.trim()) return [{ tagName: "p", textContent: text.trim() }];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// clipboard API unavailable — use workspace fallback
|
|
121
|
+
if (workspace.clipboard) {
|
|
122
|
+
return [JSON.parse(JSON.stringify(workspace.clipboard))];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Clipboard actions ───────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
export async function copyNode() {
|
|
32
131
|
const tab = activeTab.value;
|
|
33
132
|
if (!tab?.session.selection) return;
|
|
34
133
|
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
35
134
|
if (!node) return;
|
|
36
|
-
|
|
135
|
+
const json = JSON.parse(JSON.stringify(node));
|
|
136
|
+
await writeToClipboard(json);
|
|
37
137
|
statusMessage("Copied");
|
|
38
138
|
}
|
|
39
139
|
|
|
40
|
-
export function cutNode() {
|
|
140
|
+
export async function cutNode() {
|
|
41
141
|
const tab = activeTab.value;
|
|
42
142
|
if (!tab?.session.selection || tab.session.selection.length < 2) return;
|
|
43
143
|
const sel = tab.session.selection;
|
|
44
144
|
const node = getNodeAtPath(tab.doc.document, sel);
|
|
45
145
|
if (!node) return;
|
|
46
|
-
|
|
146
|
+
const json = JSON.parse(JSON.stringify(node));
|
|
147
|
+
await writeToClipboard(json);
|
|
47
148
|
transactDoc(tab, (t) => mutateRemoveNode(t, sel));
|
|
48
149
|
statusMessage("Cut");
|
|
49
150
|
}
|
|
50
151
|
|
|
51
|
-
export function pasteNode() {
|
|
52
|
-
if (!workspace.clipboard) return;
|
|
152
|
+
export async function pasteNode() {
|
|
53
153
|
const tab = activeTab.value;
|
|
54
154
|
if (!tab) return;
|
|
55
|
-
|
|
155
|
+
|
|
156
|
+
const nodes = await readFromClipboard();
|
|
157
|
+
if (!nodes || nodes.length === 0) return;
|
|
158
|
+
|
|
56
159
|
const pPath = tab.session.selection || [];
|
|
57
160
|
const parent = getNodeAtPath(tab.doc.document, pPath);
|
|
58
161
|
if (!parent) return;
|
|
@@ -60,10 +163,18 @@ export function pasteNode() {
|
|
|
60
163
|
if (tab.session.selection && tab.session.selection.length >= 2) {
|
|
61
164
|
const pp = /** @type {JxPath} */ (parentElementPath(tab.session.selection));
|
|
62
165
|
const idx = /** @type {number} */ (childIndex(tab.session.selection));
|
|
63
|
-
transactDoc(tab, (t) =>
|
|
166
|
+
transactDoc(tab, (t) => {
|
|
167
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
168
|
+
mutateInsertNode(t, pp, idx + 1 + i, nodes[i]);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
64
171
|
} else {
|
|
65
172
|
const idx = parent.children ? parent.children.length : 0;
|
|
66
|
-
transactDoc(tab, (t) =>
|
|
173
|
+
transactDoc(tab, (t) => {
|
|
174
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
175
|
+
mutateInsertNode(t, pPath, idx + i, nodes[i]);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
67
178
|
}
|
|
68
179
|
statusMessage("Pasted");
|
|
69
180
|
}
|
|
@@ -214,28 +325,37 @@ export function showContextMenu(e, path, opts = {}) {
|
|
|
214
325
|
danger: true,
|
|
215
326
|
});
|
|
216
327
|
}
|
|
217
|
-
if (
|
|
218
|
-
const clip = workspace.clipboard;
|
|
328
|
+
if (path.length >= 2) {
|
|
219
329
|
items.push({ label: "—" });
|
|
220
330
|
items.push({
|
|
221
331
|
label: "Paste inside",
|
|
222
|
-
action: () => {
|
|
332
|
+
action: async () => {
|
|
333
|
+
const nodes = await readFromClipboard();
|
|
334
|
+
if (!nodes || nodes.length === 0) return;
|
|
223
335
|
const idx = node.children ? node.children.length : 0;
|
|
224
|
-
transactDoc(activeTab.value, (t) =>
|
|
336
|
+
transactDoc(activeTab.value, (t) => {
|
|
337
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
338
|
+
mutateInsertNode(t, path, idx + i, nodes[i]);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
statusMessage("Pasted");
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
items.push({
|
|
345
|
+
label: "Paste after",
|
|
346
|
+
action: async () => {
|
|
347
|
+
const nodes = await readFromClipboard();
|
|
348
|
+
if (!nodes || nodes.length === 0) return;
|
|
349
|
+
const pp = /** @type {JxPath} */ (parentElementPath(path));
|
|
350
|
+
const idx = /** @type {number} */ (childIndex(path));
|
|
351
|
+
transactDoc(activeTab.value, (t) => {
|
|
352
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
353
|
+
mutateInsertNode(t, pp, idx + 1 + i, nodes[i]);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
statusMessage("Pasted");
|
|
225
357
|
},
|
|
226
358
|
});
|
|
227
|
-
if (path.length >= 2) {
|
|
228
|
-
items.push({
|
|
229
|
-
label: "Paste after",
|
|
230
|
-
action: () => {
|
|
231
|
-
const pp = /** @type {JxPath} */ (parentElementPath(path));
|
|
232
|
-
const idx = /** @type {number} */ (childIndex(path));
|
|
233
|
-
transactDoc(activeTab.value, (t) =>
|
|
234
|
-
mutateInsertNode(t, pp, idx + 1, structuredClone(clip)),
|
|
235
|
-
);
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
359
|
}
|
|
240
360
|
|
|
241
361
|
let x = e.clientX,
|
|
@@ -129,7 +129,8 @@ function onMouseMove(e) {
|
|
|
129
129
|
if (!_ctx || !_helper) return;
|
|
130
130
|
|
|
131
131
|
const { getCanvasMode } = _ctx;
|
|
132
|
-
|
|
132
|
+
const mode = getCanvasMode();
|
|
133
|
+
if (mode !== "design" && mode !== "edit") {
|
|
133
134
|
hide();
|
|
134
135
|
return;
|
|
135
136
|
}
|
|
@@ -222,7 +223,7 @@ function showAt(el, edge, path, parentPath, idx) {
|
|
|
222
223
|
// Set CSS anchor on target element
|
|
223
224
|
if (_currentAnchor !== el) {
|
|
224
225
|
clearAnchor();
|
|
225
|
-
el.style.anchorName = "--jx-insert";
|
|
226
|
+
/** @type {any} */ (el.style).anchorName = "--jx-insert";
|
|
226
227
|
_currentAnchor = el;
|
|
227
228
|
}
|
|
228
229
|
|
|
@@ -258,7 +259,7 @@ function hideNow() {
|
|
|
258
259
|
|
|
259
260
|
function clearAnchor() {
|
|
260
261
|
if (_currentAnchor) {
|
|
261
|
-
_currentAnchor.style.anchorName = "";
|
|
262
|
+
/** @type {any} */ (_currentAnchor.style).anchorName = "";
|
|
262
263
|
_currentAnchor = null;
|
|
263
264
|
}
|
|
264
265
|
}
|
|
@@ -843,6 +843,13 @@ function collapsePropsToAttrMap(propsObj) {
|
|
|
843
843
|
mdAttrKey = key.slice(1);
|
|
844
844
|
}
|
|
845
845
|
// Strip @ prefix for media queries (inside style.* paths)
|
|
846
|
+
if (key === "@") {
|
|
847
|
+
// Bare "@" (no media name) — treat contents as base-level style props
|
|
848
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
849
|
+
walk(/** @type {Record<string, unknown>} */ (value), prefix);
|
|
850
|
+
}
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
846
853
|
if (key.startsWith("@--")) {
|
|
847
854
|
mdAttrKey = key.slice(1);
|
|
848
855
|
}
|
package/src/panels/head-panel.js
CHANGED
|
@@ -9,11 +9,44 @@ import { html, nothing } from "lit-html";
|
|
|
9
9
|
import { live } from "lit-html/directives/live.js";
|
|
10
10
|
import { renderFieldRow } from "../ui/field-row.js";
|
|
11
11
|
import { renderMediaPicker } from "../ui/media-picker.js";
|
|
12
|
-
import { debouncedStyleCommit, projectState } from "../store.js";
|
|
12
|
+
import { debouncedStyleCommit, renderOnly, projectState } from "../store.js";
|
|
13
13
|
import { activeTab } from "../workspace/workspace.js";
|
|
14
14
|
import { transactDoc, mutateUpdateFrontmatter } from "../tabs/transact.js";
|
|
15
15
|
import { findContentTypeSchema } from "../utils/studio-utils.js";
|
|
16
16
|
import { isGoogleFontEntry, isGoogleFontPreconnect } from "../utils/google-fonts.js";
|
|
17
|
+
import { invalidateLayoutCache } from "../site-context.js";
|
|
18
|
+
import { getPlatform } from "../platform.js";
|
|
19
|
+
|
|
20
|
+
// ─── Layout picker ──────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/** @type {{ name: string; path: string }[] | null} */
|
|
23
|
+
let layoutEntries = null;
|
|
24
|
+
|
|
25
|
+
async function loadLayoutEntries() {
|
|
26
|
+
try {
|
|
27
|
+
const platform = getPlatform();
|
|
28
|
+
const listing = await platform.listDirectory("layouts");
|
|
29
|
+
layoutEntries = listing
|
|
30
|
+
.filter(
|
|
31
|
+
(/** @type {{ type: string; name: string }} */ f) =>
|
|
32
|
+
f.type === "file" && f.name.endsWith(".json"),
|
|
33
|
+
)
|
|
34
|
+
.map((/** @type {{ type: string; name: string }} */ f) => ({
|
|
35
|
+
name: f.name
|
|
36
|
+
.replace(/\.json$/, "")
|
|
37
|
+
.replace(/[-_]+/g, " ")
|
|
38
|
+
.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
39
|
+
path: `./layouts/${f.name}`,
|
|
40
|
+
}));
|
|
41
|
+
} catch {
|
|
42
|
+
layoutEntries = [];
|
|
43
|
+
}
|
|
44
|
+
renderOnly("leftPanel");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function invalidateLayoutPickerCache() {
|
|
48
|
+
layoutEntries = null;
|
|
49
|
+
}
|
|
17
50
|
|
|
18
51
|
// ─── Field definitions ───────────────────────────────────────────────────
|
|
19
52
|
|
|
@@ -282,9 +315,79 @@ export function renderHeadTemplate({ document: doc, applyMutation, renderLeftPan
|
|
|
282
315
|
const isContent = tab?.doc.mode === "content";
|
|
283
316
|
const frontmatterSection = isContent ? renderFrontmatterSection() : nothing;
|
|
284
317
|
|
|
318
|
+
// Layout field
|
|
319
|
+
const isPage =
|
|
320
|
+
tab?.documentPath &&
|
|
321
|
+
projectState?.isSiteProject &&
|
|
322
|
+
(tab.documentPath.startsWith("pages/") || tab.documentPath.startsWith("./pages/"));
|
|
323
|
+
|
|
324
|
+
/** @type {import("lit-html").TemplateResult | symbol} */
|
|
325
|
+
let layoutSection = nothing;
|
|
326
|
+
if (isPage) {
|
|
327
|
+
if (layoutEntries === null) {
|
|
328
|
+
loadLayoutEntries();
|
|
329
|
+
} else {
|
|
330
|
+
const currentLayout = doc.$layout;
|
|
331
|
+
const defaultLayout = projectState?.projectConfig?.defaults?.layout;
|
|
332
|
+
const displayValue =
|
|
333
|
+
currentLayout === false ? "__none__" : currentLayout ? currentLayout : "__default__";
|
|
334
|
+
const defaultLabel = defaultLayout
|
|
335
|
+
? defaultLayout
|
|
336
|
+
.replace(/^\.\/layouts\//, "")
|
|
337
|
+
.replace(/\.json$/, "")
|
|
338
|
+
.replace(/[-_]+/g, " ")
|
|
339
|
+
.replace(/\b\w/g, (/** @type {string} */ c) => c.toUpperCase())
|
|
340
|
+
: "";
|
|
341
|
+
|
|
342
|
+
layoutSection = html`
|
|
343
|
+
<div class="imports-section">
|
|
344
|
+
<div class="imports-section-header">
|
|
345
|
+
<span class="imports-section-title">Layout</span>
|
|
346
|
+
</div>
|
|
347
|
+
<div class="head-section-body">
|
|
348
|
+
${renderFieldRow({
|
|
349
|
+
prop: "layout",
|
|
350
|
+
label: "Layout",
|
|
351
|
+
hasValue: currentLayout !== undefined,
|
|
352
|
+
onClear: () =>
|
|
353
|
+
applyMutation((/** @type {JxMutableNode} */ d) => {
|
|
354
|
+
delete d.$layout;
|
|
355
|
+
}),
|
|
356
|
+
widget: html`
|
|
357
|
+
<sp-picker
|
|
358
|
+
size="s"
|
|
359
|
+
value=${displayValue}
|
|
360
|
+
@change=${(/** @type {Event} */ e) => {
|
|
361
|
+
const val = /** @type {HTMLInputElement} */ (e.target).value;
|
|
362
|
+
applyMutation((/** @type {JxMutableNode} */ d) => {
|
|
363
|
+
if (val === "__default__") delete d.$layout;
|
|
364
|
+
else if (val === "__none__") d.$layout = false;
|
|
365
|
+
else d.$layout = val;
|
|
366
|
+
});
|
|
367
|
+
invalidateLayoutCache();
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
370
|
+
<sp-menu-item value="__default__"
|
|
371
|
+
>Default${defaultLabel ? ` (${defaultLabel})` : ""}</sp-menu-item
|
|
372
|
+
>
|
|
373
|
+
<sp-menu-item value="__none__">None</sp-menu-item>
|
|
374
|
+
<sp-menu-divider></sp-menu-divider>
|
|
375
|
+
${layoutEntries.map(
|
|
376
|
+
(/** @type {{ name: string; path: string }} */ l) =>
|
|
377
|
+
html`<sp-menu-item value=${l.path}>${l.name}</sp-menu-item>`,
|
|
378
|
+
)}
|
|
379
|
+
</sp-picker>
|
|
380
|
+
`,
|
|
381
|
+
})}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
285
388
|
return html`
|
|
286
389
|
<div class="imports-panel">
|
|
287
|
-
${frontmatterSection}
|
|
390
|
+
${frontmatterSection} ${layoutSection}
|
|
288
391
|
|
|
289
392
|
<!-- Page section -->
|
|
290
393
|
<div class="imports-section">
|