@jxsuite/studio 0.10.1 → 0.10.2
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 +105 -89
- package/dist/studio.js.map +17 -17
- package/package.json +29 -29
- package/src/browse/browse.js +21 -19
- package/src/canvas/canvas-live-render.js +7 -1
- package/src/canvas/canvas-render.js +8 -6
- package/src/files/files.js +2 -0
- package/src/panels/imports-panel.js +2 -2
- package/src/panels/properties-panel.js +3 -3
- package/src/panels/stylebook-panel.js +27 -20
- package/src/settings/{collections-editor.js → content-types-editor.js} +48 -48
- package/src/settings/schema-field-ui.js +1 -1
- package/src/state.js +1 -1
- package/src/studio.js +10 -4
- package/src/utils/studio-utils.js +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,35 +30,35 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
|
32
32
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
33
|
-
"@jxsuite/runtime": "^0.10.
|
|
34
|
-
"@spectrum-web-components/accordion": "^1.12.
|
|
35
|
-
"@spectrum-web-components/action-bar": "1.12.
|
|
36
|
-
"@spectrum-web-components/action-button": "^1.12.
|
|
37
|
-
"@spectrum-web-components/action-group": "^1.12.
|
|
38
|
-
"@spectrum-web-components/checkbox": "^1.12.
|
|
39
|
-
"@spectrum-web-components/color-area": "^1.12.
|
|
40
|
-
"@spectrum-web-components/color-handle": "^1.12.
|
|
41
|
-
"@spectrum-web-components/color-slider": "^1.12.
|
|
42
|
-
"@spectrum-web-components/combobox": "^1.12.
|
|
43
|
-
"@spectrum-web-components/dialog": "^1.12.
|
|
44
|
-
"@spectrum-web-components/divider": "^1.12.
|
|
45
|
-
"@spectrum-web-components/icon": "^1.12.
|
|
46
|
-
"@spectrum-web-components/icons-workflow": "^1.12.
|
|
47
|
-
"@spectrum-web-components/menu": "^1.12.
|
|
48
|
-
"@spectrum-web-components/number-field": "^1.12.
|
|
49
|
-
"@spectrum-web-components/overlay": "^1.12.
|
|
33
|
+
"@jxsuite/runtime": "^0.10.1",
|
|
34
|
+
"@spectrum-web-components/accordion": "^1.12.1",
|
|
35
|
+
"@spectrum-web-components/action-bar": "1.12.1",
|
|
36
|
+
"@spectrum-web-components/action-button": "^1.12.1",
|
|
37
|
+
"@spectrum-web-components/action-group": "^1.12.1",
|
|
38
|
+
"@spectrum-web-components/checkbox": "^1.12.1",
|
|
39
|
+
"@spectrum-web-components/color-area": "^1.12.1",
|
|
40
|
+
"@spectrum-web-components/color-handle": "^1.12.1",
|
|
41
|
+
"@spectrum-web-components/color-slider": "^1.12.1",
|
|
42
|
+
"@spectrum-web-components/combobox": "^1.12.1",
|
|
43
|
+
"@spectrum-web-components/dialog": "^1.12.1",
|
|
44
|
+
"@spectrum-web-components/divider": "^1.12.1",
|
|
45
|
+
"@spectrum-web-components/icon": "^1.12.1",
|
|
46
|
+
"@spectrum-web-components/icons-workflow": "^1.12.1",
|
|
47
|
+
"@spectrum-web-components/menu": "^1.12.1",
|
|
48
|
+
"@spectrum-web-components/number-field": "^1.12.1",
|
|
49
|
+
"@spectrum-web-components/overlay": "^1.12.1",
|
|
50
50
|
"@spectrum-web-components/overlay-trigger": "^0.1.7",
|
|
51
|
-
"@spectrum-web-components/picker": "^1.12.
|
|
52
|
-
"@spectrum-web-components/picker-button": "^1.12.
|
|
53
|
-
"@spectrum-web-components/popover": "^1.12.
|
|
54
|
-
"@spectrum-web-components/search": "^1.12.
|
|
55
|
-
"@spectrum-web-components/swatch": "^1.12.
|
|
56
|
-
"@spectrum-web-components/switch": "^1.12.
|
|
57
|
-
"@spectrum-web-components/table": "^1.12.
|
|
58
|
-
"@spectrum-web-components/tabs": "^1.12.
|
|
59
|
-
"@spectrum-web-components/textfield": "^1.12.
|
|
60
|
-
"@spectrum-web-components/theme": "^1.12.
|
|
61
|
-
"@spectrum-web-components/tooltip": "^1.12.
|
|
51
|
+
"@spectrum-web-components/picker": "^1.12.1",
|
|
52
|
+
"@spectrum-web-components/picker-button": "^1.12.1",
|
|
53
|
+
"@spectrum-web-components/popover": "^1.12.1",
|
|
54
|
+
"@spectrum-web-components/search": "^1.12.1",
|
|
55
|
+
"@spectrum-web-components/swatch": "^1.12.1",
|
|
56
|
+
"@spectrum-web-components/switch": "^1.12.1",
|
|
57
|
+
"@spectrum-web-components/table": "^1.12.1",
|
|
58
|
+
"@spectrum-web-components/tabs": "^1.12.1",
|
|
59
|
+
"@spectrum-web-components/textfield": "^1.12.1",
|
|
60
|
+
"@spectrum-web-components/theme": "^1.12.1",
|
|
61
|
+
"@spectrum-web-components/tooltip": "^1.12.1",
|
|
62
62
|
"lit-html": "^3.3.3",
|
|
63
63
|
"monaco-editor": "^0.55.1",
|
|
64
64
|
"remark-directive": "^4.0.0",
|
package/src/browse/browse.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Displays pages, layouts, components, content, and media in a filterable table grid. Fills the
|
|
5
5
|
* center canvas area as a parallel state to Edit/Design/Preview/Code/Settings. Includes a "New +"
|
|
6
|
-
* button with type-aware entity creation (including
|
|
6
|
+
* button with type-aware entity creation (including content types from project.json).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { html, render as litRender } from "lit-html";
|
|
@@ -153,14 +153,14 @@ const ENTITY_TYPES = [
|
|
|
153
153
|
];
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
|
-
* Build frontmatter YAML from a
|
|
156
|
+
* Build frontmatter YAML from a content type's schema properties.
|
|
157
157
|
*
|
|
158
|
-
* @param {string}
|
|
158
|
+
* @param {string} contentTypeName
|
|
159
159
|
* @returns {string}
|
|
160
160
|
*/
|
|
161
|
-
function buildFrontmatterYaml(
|
|
161
|
+
function buildFrontmatterYaml(contentTypeName) {
|
|
162
162
|
const config = projectState?.projectConfig;
|
|
163
|
-
const col = config?.
|
|
163
|
+
const col = config?.contentTypes?.[contentTypeName];
|
|
164
164
|
if (!col?.schema?.properties) return "title: Untitled\n";
|
|
165
165
|
|
|
166
166
|
let yaml = "";
|
|
@@ -172,22 +172,22 @@ function buildFrontmatterYaml(collectionName) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
* Get
|
|
175
|
+
* Get content-type-derived entity types from project config.
|
|
176
176
|
*
|
|
177
|
-
* @returns {{ key: string; label: string; dir: string; ext: string;
|
|
177
|
+
* @returns {{ key: string; label: string; dir: string; ext: string; contentTypeName: string }[]}
|
|
178
178
|
*/
|
|
179
|
-
function
|
|
179
|
+
function getContentTypeTypes() {
|
|
180
180
|
const config = projectState?.projectConfig;
|
|
181
|
-
if (!config?.
|
|
182
|
-
return Object.entries(config.
|
|
181
|
+
if (!config?.contentTypes) return [];
|
|
182
|
+
return Object.entries(config.contentTypes).map(([name, def]) => {
|
|
183
183
|
const d = /** @type {any} */ (def);
|
|
184
184
|
const dir = d.source ? d.source.replace(/^\.\//, "").split("/")[0] : name;
|
|
185
185
|
return {
|
|
186
|
-
key: `
|
|
186
|
+
key: `contentType:${name}`,
|
|
187
187
|
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
188
188
|
dir,
|
|
189
189
|
ext: ".md",
|
|
190
|
-
|
|
190
|
+
contentTypeName: name,
|
|
191
191
|
};
|
|
192
192
|
});
|
|
193
193
|
}
|
|
@@ -200,9 +200,9 @@ function getCollectionTypes() {
|
|
|
200
200
|
* @param {{ openFile: (path: string) => void }} ctx
|
|
201
201
|
*/
|
|
202
202
|
async function handleNewEntity(typeKey, container, ctx) {
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const allTypes = [...ENTITY_TYPES, ...
|
|
203
|
+
const isContentType = typeKey.startsWith("contentType:");
|
|
204
|
+
const contentTypeName = isContentType ? typeKey.slice("contentType:".length) : null;
|
|
205
|
+
const allTypes = [...ENTITY_TYPES, ...getContentTypeTypes()];
|
|
206
206
|
const typeInfo = allTypes.find((t) => t.key === typeKey);
|
|
207
207
|
if (!typeInfo) return;
|
|
208
208
|
|
|
@@ -217,7 +217,9 @@ async function handleNewEntity(typeKey, container, ctx) {
|
|
|
217
217
|
|
|
218
218
|
let content;
|
|
219
219
|
if (typeInfo.ext === ".md") {
|
|
220
|
-
const frontmatter =
|
|
220
|
+
const frontmatter = contentTypeName
|
|
221
|
+
? buildFrontmatterYaml(contentTypeName)
|
|
222
|
+
: "title: Untitled\n";
|
|
221
223
|
content = `---\n${frontmatter}---\n\n`;
|
|
222
224
|
} else {
|
|
223
225
|
content = JSON.stringify({ tagName: "div", children: [] }, null, "\t");
|
|
@@ -278,7 +280,7 @@ export async function renderBrowse(container, ctx) {
|
|
|
278
280
|
|
|
279
281
|
const files = filteredFiles();
|
|
280
282
|
|
|
281
|
-
const
|
|
283
|
+
const contentTypeTypes = getContentTypeTypes();
|
|
282
284
|
|
|
283
285
|
const filterBar = html`
|
|
284
286
|
<div class="browse-filter-bar">
|
|
@@ -317,8 +319,8 @@ export async function renderBrowse(container, ctx) {
|
|
|
317
319
|
@change=${(/** @type {any} */ e) => handleNewEntity(e.target.value, container, ctx)}
|
|
318
320
|
>
|
|
319
321
|
${ENTITY_TYPES.map((t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`)}
|
|
320
|
-
${
|
|
321
|
-
? html`<sp-menu-divider></sp-menu-divider> ${
|
|
322
|
+
${contentTypeTypes.length
|
|
323
|
+
? html`<sp-menu-divider></sp-menu-divider> ${contentTypeTypes.map(
|
|
322
324
|
(t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
|
|
323
325
|
)}`
|
|
324
326
|
: ""}
|
|
@@ -127,7 +127,13 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
|
127
127
|
console.warn("Studio: failed to import package", entry, e);
|
|
128
128
|
}
|
|
129
129
|
} else if (entry?.$ref) {
|
|
130
|
-
|
|
130
|
+
let href;
|
|
131
|
+
try {
|
|
132
|
+
href = new URL(entry.$ref, docBase).href;
|
|
133
|
+
} catch (/** @type {any} */ urlErr) {
|
|
134
|
+
console.warn("Studio: invalid element URL", { ref: entry.$ref, docBase }, urlErr);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
131
137
|
try {
|
|
132
138
|
await defineElement(href);
|
|
133
139
|
} catch (/** @type {any} */ e) {
|
|
@@ -66,12 +66,15 @@ export function renderCanvas() {
|
|
|
66
66
|
// Advance render generation so stale async renders from the previous cycle bail out
|
|
67
67
|
++view.renderGeneration;
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
69
|
+
// Detect whether this is a mode transition or a content-only re-render
|
|
70
|
+
const modeChanged = canvasMode !== view.prevCanvasMode;
|
|
71
|
+
|
|
72
|
+
// Only clear Lit's internal state on mode transitions (structural panel changes).
|
|
73
|
+
// For content re-renders in the same mode, Lit's template diffing preserves
|
|
74
|
+
// the panel structure. Bailed async renders can't corrupt the DOM because
|
|
75
|
+
// renderCanvasLive uses atomic clear (innerHTML = "" right before appendChild).
|
|
73
76
|
// @ts-ignore
|
|
74
|
-
if (canvasWrap["_$litPart$"]) {
|
|
77
|
+
if (modeChanged && canvasWrap["_$litPart$"]) {
|
|
75
78
|
canvasWrap.textContent = "";
|
|
76
79
|
// @ts-ignore
|
|
77
80
|
delete canvasWrap["_$litPart$"];
|
|
@@ -101,7 +104,6 @@ export function renderCanvas() {
|
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
// Detect whether this is a mode transition or a content-only re-render
|
|
104
|
-
const modeChanged = canvasMode !== view.prevCanvasMode;
|
|
105
107
|
view.prevCanvasMode = canvasMode;
|
|
106
108
|
|
|
107
109
|
// DnD handlers are registered on inner canvas elements that get replaced on every
|
package/src/files/files.js
CHANGED
|
@@ -564,6 +564,8 @@ export async function openFileFromTree(ctx, path) {
|
|
|
564
564
|
if (path.endsWith(".md")) {
|
|
565
565
|
await ctx.loadMarkdown(content, null);
|
|
566
566
|
ctx.S.documentPath = path;
|
|
567
|
+
ctx.S.dirty = false;
|
|
568
|
+
ctx.commit(ctx.S);
|
|
567
569
|
} else {
|
|
568
570
|
const doc = JSON.parse(content);
|
|
569
571
|
const newS = createState(doc);
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* When editing project.json: shows Class Imports, Dependencies (add/remove packages), and
|
|
5
5
|
* per-package component toggles for cherry-picking individual elements. When editing a
|
|
6
|
-
* page/layout/component/
|
|
7
|
-
* toggles.
|
|
6
|
+
* page/layout/component/content type: shows Component Imports ($ref picker) and per-package
|
|
7
|
+
* component toggles.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { html, nothing } from "lit-html";
|
|
@@ -27,7 +27,7 @@ import { renderFieldRow } from "../ui/field-row.js";
|
|
|
27
27
|
import {
|
|
28
28
|
attrLabel,
|
|
29
29
|
inferInputType,
|
|
30
|
-
|
|
30
|
+
findContentTypeSchema,
|
|
31
31
|
friendlyNameToVar,
|
|
32
32
|
camelToLabel,
|
|
33
33
|
parseCemType,
|
|
@@ -210,7 +210,7 @@ function kvRow(
|
|
|
210
210
|
function renderFrontmatterOnlyPanel() {
|
|
211
211
|
const S = getState();
|
|
212
212
|
const fm = S.content?.frontmatter || {};
|
|
213
|
-
const col =
|
|
213
|
+
const col = findContentTypeSchema(S.documentPath, projectState?.projectConfig);
|
|
214
214
|
const schemaProps = col?.schema?.properties;
|
|
215
215
|
const requiredFields = new Set(col?.schema?.required || []);
|
|
216
216
|
|
|
@@ -1276,7 +1276,7 @@ export function renderPropertiesPanelTemplate(ctx) {
|
|
|
1276
1276
|
S.mode === "content"
|
|
1277
1277
|
? (() => {
|
|
1278
1278
|
const fm = S.content?.frontmatter || {};
|
|
1279
|
-
const col =
|
|
1279
|
+
const col = findContentTypeSchema(S.documentPath, projectState?.projectConfig);
|
|
1280
1280
|
const schemaProps = col?.schema?.properties;
|
|
1281
1281
|
const requiredFields = new Set(col?.schema?.required || []);
|
|
1282
1282
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stylebook panel — renders the Settings mode canvas (element catalog, design variables,
|
|
3
|
-
* definitions,
|
|
3
|
+
* definitions, content types). Extracted from studio.js Phase 4e.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { html, render as litRender, nothing } from "lit-html";
|
|
@@ -26,7 +26,7 @@ import { parseMediaEntries, activeBreakpointsForWidth } from "../utils/canvas-me
|
|
|
26
26
|
import { mediaDisplayName } from "./shared.js";
|
|
27
27
|
import { friendlyNameToVar, varDisplayName } from "../utils/studio-utils.js";
|
|
28
28
|
import { renderDefsEditor } from "../settings/defs-editor.js";
|
|
29
|
-
import {
|
|
29
|
+
import { renderContentTypesEditor } from "../settings/content-types-editor.js";
|
|
30
30
|
import stylebookMeta from "../../data/stylebook-meta.json";
|
|
31
31
|
|
|
32
32
|
export { stylebookMeta };
|
|
@@ -67,12 +67,12 @@ export function renderStylebookMode(ctx) {
|
|
|
67
67
|
>
|
|
68
68
|
<sp-tab label="Stylebook" value="stylebook"></sp-tab>
|
|
69
69
|
<sp-tab label="Definitions" value="definitions"></sp-tab>
|
|
70
|
-
<sp-tab label="
|
|
70
|
+
<sp-tab label="Content Types" value="contentTypes"></sp-tab>
|
|
71
71
|
</sp-tabs>
|
|
72
72
|
</div>
|
|
73
73
|
`;
|
|
74
74
|
|
|
75
|
-
if (settingsTab === "definitions" || settingsTab === "
|
|
75
|
+
if (settingsTab === "definitions" || settingsTab === "contentTypes") {
|
|
76
76
|
/** @type {any} */ (canvasWrap).style.overflow = "hidden";
|
|
77
77
|
|
|
78
78
|
litRender(
|
|
@@ -88,7 +88,7 @@ export function renderStylebookMode(ctx) {
|
|
|
88
88
|
canvasWrap.querySelector(".settings-editor-container")
|
|
89
89
|
);
|
|
90
90
|
if (settingsTab === "definitions") renderDefsEditor(container);
|
|
91
|
-
else
|
|
91
|
+
else renderContentTypesEditor(container);
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -261,12 +261,6 @@ export function selectStylebookTag(tag, media) {
|
|
|
261
261
|
},
|
|
262
262
|
});
|
|
263
263
|
renderStylebookOverlays();
|
|
264
|
-
requestAnimationFrame(() => {
|
|
265
|
-
if (canvasPanels.length > 0) {
|
|
266
|
-
const el = findStylebookEl(canvasPanels[0].canvas, tag);
|
|
267
|
-
if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
264
|
}
|
|
271
265
|
|
|
272
266
|
/** Draw selection + hover overlays for stylebook elements */
|
|
@@ -324,7 +318,7 @@ export function renderStylebookOverlays() {
|
|
|
324
318
|
}
|
|
325
319
|
}
|
|
326
320
|
|
|
327
|
-
// ─── Internal helpers
|
|
321
|
+
// ─── Internal helpers (exported for testing) ─────────────────────────────────
|
|
328
322
|
|
|
329
323
|
/**
|
|
330
324
|
* Build a DOM element tree from a stylebook-meta.json entry.
|
|
@@ -332,8 +326,9 @@ export function renderStylebookOverlays() {
|
|
|
332
326
|
* @param {any} entry
|
|
333
327
|
* @param {any} rootStyle
|
|
334
328
|
* @param {any} activeBreakpoints
|
|
329
|
+
* @param {string | null} [parentTag]
|
|
335
330
|
*/
|
|
336
|
-
function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
|
|
331
|
+
export function buildStylebookElement(entry, rootStyle, activeBreakpoints, parentTag = null) {
|
|
337
332
|
const el = document.createElement(entry.tag);
|
|
338
333
|
if (entry.text) el.textContent = entry.text;
|
|
339
334
|
if (entry.attributes) {
|
|
@@ -344,7 +339,9 @@ function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
|
|
|
344
339
|
}
|
|
345
340
|
}
|
|
346
341
|
if (entry.style) el.style.cssText = entry.style;
|
|
347
|
-
const
|
|
342
|
+
const compoundSelector =
|
|
343
|
+
parentTag && parentTag !== entry.tag ? `& ${parentTag} ${entry.tag}` : null;
|
|
344
|
+
const tagStyle = (compoundSelector && rootStyle[compoundSelector]) || rootStyle[`& ${entry.tag}`];
|
|
348
345
|
if (tagStyle) {
|
|
349
346
|
for (const [prop, val] of Object.entries(tagStyle)) {
|
|
350
347
|
if (typeof val === "string" || typeof val === "number") {
|
|
@@ -372,7 +369,7 @@ function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
|
|
|
372
369
|
}
|
|
373
370
|
if (entry.children) {
|
|
374
371
|
for (const child of entry.children) {
|
|
375
|
-
el.appendChild(buildStylebookElement(child, rootStyle, activeBreakpoints));
|
|
372
|
+
el.appendChild(buildStylebookElement(child, rootStyle, activeBreakpoints, entry.tag));
|
|
376
373
|
}
|
|
377
374
|
}
|
|
378
375
|
return el;
|
|
@@ -429,13 +426,19 @@ function hasTagStyle(rootStyle, tag) {
|
|
|
429
426
|
* @param {any} customizedOnly
|
|
430
427
|
* @param {any} activeBreakpoints
|
|
431
428
|
*/
|
|
432
|
-
function renderStylebookElementsIntoCanvas(
|
|
429
|
+
export function renderStylebookElementsIntoCanvas(
|
|
433
430
|
canvasEl,
|
|
434
431
|
rootStyle,
|
|
435
432
|
filter,
|
|
436
433
|
customizedOnly,
|
|
437
434
|
activeBreakpoints,
|
|
438
435
|
) {
|
|
436
|
+
for (const [k, v] of Object.entries(rootStyle)) {
|
|
437
|
+
if (k.startsWith("--") && (typeof v === "string" || typeof v === "number")) {
|
|
438
|
+
canvasEl.style.setProperty(k, String(v));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
439
442
|
/** @type {import("lit-html").TemplateResult[]} */
|
|
440
443
|
const sectionTemplates = [];
|
|
441
444
|
|
|
@@ -462,10 +465,11 @@ function renderStylebookElementsIntoCanvas(
|
|
|
462
465
|
view.stylebookElToTag.set(card, entry.tag);
|
|
463
466
|
elToPath.set(card, ["__sb", entry.tag]);
|
|
464
467
|
for (const child of el.querySelectorAll("*")) {
|
|
465
|
-
const
|
|
468
|
+
const childTag = child.tagName.toLowerCase();
|
|
466
469
|
if (!view.stylebookElToTag.has(child)) {
|
|
467
|
-
|
|
468
|
-
|
|
470
|
+
const compound = childTag === entry.tag ? entry.tag : `${entry.tag} ${childTag}`;
|
|
471
|
+
view.stylebookElToTag.set(child, compound);
|
|
472
|
+
elToPath.set(child, ["__sb", compound]);
|
|
469
473
|
}
|
|
470
474
|
}
|
|
471
475
|
})}
|
|
@@ -473,7 +477,10 @@ function renderStylebookElementsIntoCanvas(
|
|
|
473
477
|
<div
|
|
474
478
|
class="element-card-preview"
|
|
475
479
|
${ref((c) => {
|
|
476
|
-
if (c
|
|
480
|
+
if (c) {
|
|
481
|
+
c.textContent = "";
|
|
482
|
+
c.appendChild(el);
|
|
483
|
+
}
|
|
477
484
|
})}
|
|
478
485
|
></div>
|
|
479
486
|
<div class="element-card-label"><${entry.tag}></div>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Content Types Editor — visual schema builder for project content types.
|
|
3
3
|
*
|
|
4
|
-
* Renders inside the Settings view "
|
|
5
|
-
*
|
|
4
|
+
* Renders inside the Settings view "Content Types" tab. Two-column layout: left column lists
|
|
5
|
+
* content type names, right column edits the selected content type's schema.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { html, render as litRender } from "lit-html";
|
|
@@ -13,11 +13,11 @@ import { fieldCardTpl, addFieldFormTpl, schemaForType } from "./schema-field-ui.
|
|
|
13
13
|
// ─── Module state ─────────────────────────────────────────────────────────────
|
|
14
14
|
|
|
15
15
|
/** @type {string | null} */
|
|
16
|
-
let
|
|
16
|
+
let selectedContentType = null;
|
|
17
17
|
let showAddField = false;
|
|
18
18
|
let newFieldState = { name: "", type: "string", required: false };
|
|
19
|
-
let
|
|
20
|
-
let
|
|
19
|
+
let showNewContentType = false;
|
|
20
|
+
let newContentTypeName = "";
|
|
21
21
|
|
|
22
22
|
// ─── Persistence ──────────────────────────────────────────────────────────────
|
|
23
23
|
|
|
@@ -29,17 +29,17 @@ async function saveProjectConfig() {
|
|
|
29
29
|
|
|
30
30
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
31
31
|
|
|
32
|
-
/** Get the schema object for the selected
|
|
32
|
+
/** Get the schema object for the selected content type. */
|
|
33
33
|
function getSelectedSchema() {
|
|
34
34
|
const config = projectState?.projectConfig;
|
|
35
|
-
return config?.
|
|
35
|
+
return config?.contentTypes?.[/** @type {string} */ (selectedContentType)]?.schema;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// ─── Handlers ─────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
40
|
/** @param {() => void} rerender */
|
|
41
|
-
function
|
|
42
|
-
const slug =
|
|
41
|
+
function handleNewContentType(rerender) {
|
|
42
|
+
const slug = newContentTypeName
|
|
43
43
|
.toLowerCase()
|
|
44
44
|
.replace(/\s+/g, "-")
|
|
45
45
|
.replace(/[^a-z0-9-]/g, "");
|
|
@@ -47,30 +47,30 @@ function handleNewCollection(rerender) {
|
|
|
47
47
|
|
|
48
48
|
const config = projectState?.projectConfig;
|
|
49
49
|
if (!config) return;
|
|
50
|
-
if (!config.
|
|
51
|
-
if (config.
|
|
50
|
+
if (!config.contentTypes) config.contentTypes = {};
|
|
51
|
+
if (config.contentTypes[slug]) return; // already exists
|
|
52
52
|
|
|
53
|
-
config.
|
|
54
|
-
source:
|
|
53
|
+
config.contentTypes[slug] = {
|
|
54
|
+
source: `./content/${slug}/**/*.md`,
|
|
55
55
|
schema: { type: "object", properties: {}, required: [] },
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
selectedContentType = slug;
|
|
59
|
+
showNewContentType = false;
|
|
60
|
+
newContentTypeName = "";
|
|
61
61
|
rerender();
|
|
62
62
|
|
|
63
63
|
// Persist in background
|
|
64
64
|
saveProjectConfig().then(async () => {
|
|
65
65
|
const platform = getPlatform();
|
|
66
|
-
await platform.writeFile(
|
|
66
|
+
await platform.writeFile(`content/${slug}/.gitkeep`, "");
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/** @param {() => void} rerender */
|
|
71
71
|
function handleAddField(rerender) {
|
|
72
72
|
const name = newFieldState.name.trim();
|
|
73
|
-
if (!name || !
|
|
73
|
+
if (!name || !selectedContentType) return;
|
|
74
74
|
|
|
75
75
|
const schema = getSelectedSchema();
|
|
76
76
|
if (!schema) return;
|
|
@@ -270,13 +270,13 @@ function handleChangeNestedType(parentName, childName, newType, rerender) {
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
/** @param {() => void} rerender */
|
|
273
|
-
function
|
|
274
|
-
if (!
|
|
273
|
+
function handleDeleteContentType(rerender) {
|
|
274
|
+
if (!selectedContentType) return;
|
|
275
275
|
const config = projectState?.projectConfig;
|
|
276
|
-
if (!config?.
|
|
276
|
+
if (!config?.contentTypes?.[selectedContentType]) return;
|
|
277
277
|
|
|
278
|
-
delete config.
|
|
279
|
-
|
|
278
|
+
delete config.contentTypes[selectedContentType];
|
|
279
|
+
selectedContentType = null;
|
|
280
280
|
|
|
281
281
|
rerender();
|
|
282
282
|
saveProjectConfig();
|
|
@@ -285,26 +285,26 @@ function handleDeleteCollection(rerender) {
|
|
|
285
285
|
// ─── Render ───────────────────────────────────────────────────────────────────
|
|
286
286
|
|
|
287
287
|
/**
|
|
288
|
-
* Render the
|
|
288
|
+
* Render the content types editor.
|
|
289
289
|
*
|
|
290
290
|
* @param {HTMLElement} container
|
|
291
291
|
*/
|
|
292
|
-
export function
|
|
293
|
-
const rerender = () =>
|
|
292
|
+
export function renderContentTypesEditor(container) {
|
|
293
|
+
const rerender = () => renderContentTypesEditor(container);
|
|
294
294
|
const config = projectState?.projectConfig;
|
|
295
|
-
const
|
|
296
|
-
const
|
|
295
|
+
const contentTypes = config?.contentTypes || {};
|
|
296
|
+
const contentTypeNames = Object.keys(contentTypes);
|
|
297
297
|
|
|
298
|
-
// Left column —
|
|
298
|
+
// Left column — content type list
|
|
299
299
|
const listTpl = html`
|
|
300
300
|
<div class="settings-list-panel">
|
|
301
|
-
${
|
|
301
|
+
${contentTypeNames.map(
|
|
302
302
|
(name) => html`
|
|
303
303
|
<sp-action-button
|
|
304
304
|
size="s"
|
|
305
|
-
?selected=${
|
|
305
|
+
?selected=${selectedContentType === name}
|
|
306
306
|
@click=${() => {
|
|
307
|
-
|
|
307
|
+
selectedContentType = name;
|
|
308
308
|
showAddField = false;
|
|
309
309
|
rerender();
|
|
310
310
|
}}
|
|
@@ -313,25 +313,25 @@ export function renderCollectionsEditor(container) {
|
|
|
313
313
|
</sp-action-button>
|
|
314
314
|
`,
|
|
315
315
|
)}
|
|
316
|
-
${
|
|
316
|
+
${showNewContentType
|
|
317
317
|
? html`
|
|
318
318
|
<div class="settings-inline-form">
|
|
319
319
|
<sp-textfield
|
|
320
320
|
size="s"
|
|
321
|
-
placeholder="
|
|
322
|
-
.value=${
|
|
321
|
+
placeholder="content-type-name"
|
|
322
|
+
.value=${newContentTypeName}
|
|
323
323
|
@input=${(/** @type {any} */ e) => {
|
|
324
|
-
|
|
324
|
+
newContentTypeName = e.target.value;
|
|
325
325
|
}}
|
|
326
326
|
@keydown=${(/** @type {any} */ e) => {
|
|
327
|
-
if (e.key === "Enter")
|
|
327
|
+
if (e.key === "Enter") handleNewContentType(rerender);
|
|
328
328
|
if (e.key === "Escape") {
|
|
329
|
-
|
|
329
|
+
showNewContentType = false;
|
|
330
330
|
rerender();
|
|
331
331
|
}
|
|
332
332
|
}}
|
|
333
333
|
></sp-textfield>
|
|
334
|
-
<sp-action-button size="s" @click=${() =>
|
|
334
|
+
<sp-action-button size="s" @click=${() => handleNewContentType(rerender)}>
|
|
335
335
|
Create
|
|
336
336
|
</sp-action-button>
|
|
337
337
|
</div>
|
|
@@ -341,11 +341,11 @@ export function renderCollectionsEditor(container) {
|
|
|
341
341
|
size="s"
|
|
342
342
|
quiet
|
|
343
343
|
@click=${() => {
|
|
344
|
-
|
|
344
|
+
showNewContentType = true;
|
|
345
345
|
rerender();
|
|
346
346
|
}}
|
|
347
347
|
>
|
|
348
|
-
<sp-icon-add slot="icon"></sp-icon-add> New
|
|
348
|
+
<sp-icon-add slot="icon"></sp-icon-add> New Content Type
|
|
349
349
|
</sp-action-button>
|
|
350
350
|
`}
|
|
351
351
|
</div>
|
|
@@ -353,10 +353,10 @@ export function renderCollectionsEditor(container) {
|
|
|
353
353
|
|
|
354
354
|
// Right column — schema editor
|
|
355
355
|
let editorTpl;
|
|
356
|
-
if (!
|
|
357
|
-
editorTpl = html`<div class="settings-empty-state">Select or create a
|
|
356
|
+
if (!selectedContentType || !contentTypes[selectedContentType]) {
|
|
357
|
+
editorTpl = html`<div class="settings-empty-state">Select or create a content type</div>`;
|
|
358
358
|
} else {
|
|
359
|
-
const col =
|
|
359
|
+
const col = contentTypes[selectedContentType];
|
|
360
360
|
const schema = col.schema || {};
|
|
361
361
|
const properties = schema.properties || {};
|
|
362
362
|
const required = schema.required || [];
|
|
@@ -381,13 +381,13 @@ export function renderCollectionsEditor(container) {
|
|
|
381
381
|
editorTpl = html`
|
|
382
382
|
<div class="settings-editor-panel">
|
|
383
383
|
<div class="settings-editor-header">
|
|
384
|
-
<h3>${
|
|
384
|
+
<h3>${selectedContentType}</h3>
|
|
385
385
|
<sp-field-label size="s">Source: ${col.source || "—"}</sp-field-label>
|
|
386
386
|
<sp-action-button
|
|
387
387
|
size="xs"
|
|
388
388
|
quiet
|
|
389
|
-
title="Delete
|
|
390
|
-
@click=${() =>
|
|
389
|
+
title="Delete content type"
|
|
390
|
+
@click=${() => handleDeleteContentType(rerender)}
|
|
391
391
|
>
|
|
392
392
|
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
393
393
|
</sp-action-button>
|
package/src/state.js
CHANGED
|
@@ -229,7 +229,7 @@ export function createState(doc) {
|
|
|
229
229
|
stylebookTab: "elements", // "elements" | "variables"
|
|
230
230
|
stylebookFilter: "", // search filter text
|
|
231
231
|
stylebookCustomizedOnly: false, // show only customized elements
|
|
232
|
-
settingsTab: "stylebook", // "stylebook" | "definitions" | "
|
|
232
|
+
settingsTab: "stylebook", // "stylebook" | "definitions" | "contentTypes"
|
|
233
233
|
gitStatus: null, // { branch, ahead, behind, files: [] }
|
|
234
234
|
gitBranches: null, // { current, branches: [] }
|
|
235
235
|
gitCommitMessage: "", // commit message input
|