@jxsuite/studio 0.0.1 → 0.5.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 +47638 -33445
- package/dist/studio.js.map +449 -344
- package/package.json +44 -33
- package/src/browse/browse.js +414 -0
- package/src/editor/context-menu.js +48 -1
- package/src/editor/convert-to-component.js +208 -0
- package/src/editor/inline-edit.js +33 -6
- package/src/editor/shortcuts.js +6 -1
- package/src/files/components.js +4 -2
- package/src/files/file-ops.js +102 -54
- package/src/files/files.js +22 -8
- package/src/markdown/md-convert.js +309 -11
- package/src/panels/activity-bar.js +3 -0
- package/src/panels/head-panel.js +576 -0
- package/src/panels/overlays.js +125 -0
- package/src/panels/right-panel.js +104 -0
- package/src/panels/shared.js +41 -0
- package/src/panels/signals-panel.js +95 -94
- package/src/panels/toolbar.js +217 -0
- package/src/platforms/devserver.js +58 -16
- package/src/settings/collections-editor.js +428 -0
- package/src/settings/defs-editor.js +418 -0
- package/src/settings/schema-field-ui.js +329 -0
- package/src/state.js +99 -2
- package/src/store.js +77 -41
- package/src/studio.js +1523 -1375
- package/src/ui/button-group.js +91 -0
- package/src/ui/color-selector.js +299 -0
- package/src/ui/field-row.js +47 -0
- package/src/ui/media-picker.js +172 -0
- package/src/ui/panel-resize.js +96 -0
- package/src/ui/spectrum.js +36 -2
- package/src/ui/unit-selector.js +106 -0
- package/src/ui/{jx-styled-combobox.js → value-selector.js} +7 -7
- package/src/ui/widgets.js +106 -0
- package/src/utils/inherited-style.js +54 -0
- package/src/utils/studio-utils.js +32 -0
- package/src/view.js +45 -0
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/jxsuite/jx.git",
|
|
9
|
+
"directory": "packages/studio"
|
|
10
|
+
},
|
|
6
11
|
"files": [
|
|
7
12
|
"*.js",
|
|
8
13
|
"src",
|
|
@@ -10,44 +15,50 @@
|
|
|
10
15
|
],
|
|
11
16
|
"type": "module",
|
|
12
17
|
"exports": {
|
|
13
|
-
".": "./studio.js",
|
|
14
|
-
"./platform.js": "./platform.js"
|
|
18
|
+
".": "./src/studio.js",
|
|
19
|
+
"./platform.js": "./src/platform.js"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"provenance": true
|
|
15
23
|
},
|
|
16
24
|
"scripts": {
|
|
17
25
|
"build": "bun build ./src/studio.js --outdir dist --target browser --sourcemap=linked",
|
|
18
26
|
"gen:webdata": "bun run scripts/gen-webdata.js",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
27
|
+
"test": "bun test",
|
|
28
|
+
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
21
29
|
},
|
|
22
30
|
"dependencies": {
|
|
23
31
|
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
|
24
32
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
25
33
|
"@jxsuite/runtime": "workspace:*",
|
|
26
|
-
"@spectrum-web-components/accordion": "^1.
|
|
27
|
-
"@spectrum-web-components/action-bar": "1.
|
|
28
|
-
"@spectrum-web-components/action-button": "^1.
|
|
29
|
-
"@spectrum-web-components/action-group": "^1.
|
|
30
|
-
"@spectrum-web-components/checkbox": "^1.
|
|
31
|
-
"@spectrum-web-components/color-area": "^1.
|
|
32
|
-
"@spectrum-web-components/color-handle": "^1.
|
|
33
|
-
"@spectrum-web-components/color-slider": "^1.
|
|
34
|
-
"@spectrum-web-components/combobox": "^1.
|
|
35
|
-
"@spectrum-web-components/
|
|
36
|
-
"@spectrum-web-components/
|
|
37
|
-
"@spectrum-web-components/
|
|
38
|
-
"@spectrum-web-components/
|
|
39
|
-
"@spectrum-web-components/
|
|
40
|
-
"@spectrum-web-components/
|
|
41
|
-
"@spectrum-web-components/
|
|
42
|
-
"@spectrum-web-components/
|
|
43
|
-
"@spectrum-web-components/
|
|
44
|
-
"@spectrum-web-components/
|
|
45
|
-
"@spectrum-web-components/
|
|
46
|
-
"@spectrum-web-components/
|
|
47
|
-
"@spectrum-web-components/
|
|
48
|
-
"@spectrum-web-components/
|
|
49
|
-
"@spectrum-web-components/
|
|
50
|
-
"@spectrum-web-components/
|
|
34
|
+
"@spectrum-web-components/accordion": "^1.12.0",
|
|
35
|
+
"@spectrum-web-components/action-bar": "1.12.0",
|
|
36
|
+
"@spectrum-web-components/action-button": "^1.12.0",
|
|
37
|
+
"@spectrum-web-components/action-group": "^1.12.0",
|
|
38
|
+
"@spectrum-web-components/checkbox": "^1.12.0",
|
|
39
|
+
"@spectrum-web-components/color-area": "^1.12.0",
|
|
40
|
+
"@spectrum-web-components/color-handle": "^1.12.0",
|
|
41
|
+
"@spectrum-web-components/color-slider": "^1.12.0",
|
|
42
|
+
"@spectrum-web-components/combobox": "^1.12.0",
|
|
43
|
+
"@spectrum-web-components/dialog": "^1.12.0",
|
|
44
|
+
"@spectrum-web-components/divider": "^1.12.0",
|
|
45
|
+
"@spectrum-web-components/icon": "^1.12.0",
|
|
46
|
+
"@spectrum-web-components/icons-workflow": "^1.12.0",
|
|
47
|
+
"@spectrum-web-components/menu": "^1.12.0",
|
|
48
|
+
"@spectrum-web-components/number-field": "^1.12.0",
|
|
49
|
+
"@spectrum-web-components/overlay": "^1.12.0",
|
|
50
|
+
"@spectrum-web-components/overlay-trigger": "^0.1.7",
|
|
51
|
+
"@spectrum-web-components/picker": "^1.12.0",
|
|
52
|
+
"@spectrum-web-components/picker-button": "^1.12.0",
|
|
53
|
+
"@spectrum-web-components/popover": "^1.12.0",
|
|
54
|
+
"@spectrum-web-components/search": "^1.12.0",
|
|
55
|
+
"@spectrum-web-components/swatch": "^1.12.0",
|
|
56
|
+
"@spectrum-web-components/switch": "^1.12.0",
|
|
57
|
+
"@spectrum-web-components/table": "^1.12.0",
|
|
58
|
+
"@spectrum-web-components/tabs": "^1.12.0",
|
|
59
|
+
"@spectrum-web-components/textfield": "^1.12.0",
|
|
60
|
+
"@spectrum-web-components/theme": "^1.12.0",
|
|
61
|
+
"@spectrum-web-components/tooltip": "^1.12.0",
|
|
51
62
|
"lit-html": "^3.3.2",
|
|
52
63
|
"monaco-editor": "^0.55.1",
|
|
53
64
|
"remark-directive": "^4.0.0",
|
|
@@ -56,12 +67,12 @@
|
|
|
56
67
|
"remark-parse": "^11.0.0",
|
|
57
68
|
"remark-stringify": "^11.0.0",
|
|
58
69
|
"unified": "^11.0.5",
|
|
59
|
-
"yaml": "^2.
|
|
70
|
+
"yaml": "^2.9.0"
|
|
60
71
|
},
|
|
61
72
|
"devDependencies": {
|
|
62
73
|
"@happy-dom/global-registrator": "^20.9.0",
|
|
63
|
-
"@webref/css": "^8.5.
|
|
74
|
+
"@webref/css": "^8.5.6",
|
|
64
75
|
"@webref/elements": "^2.7.1",
|
|
65
|
-
"@webref/idl": "^3.
|
|
76
|
+
"@webref/idl": "^3.78.0"
|
|
66
77
|
}
|
|
67
78
|
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manage view — project-level file browser rendered as a Spectrum table.
|
|
3
|
+
*
|
|
4
|
+
* Displays pages, layouts, components, content, and media in a filterable table grid. Fills the
|
|
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 collections from project.json).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { html, render as litRender } from "lit-html";
|
|
10
|
+
import { getPlatform } from "../platform.js";
|
|
11
|
+
import { projectState } from "../store.js";
|
|
12
|
+
import { yamlDefault } from "../settings/schema-field-ui.js";
|
|
13
|
+
import { invalidateMediaCache } from "../ui/media-picker.js";
|
|
14
|
+
|
|
15
|
+
// ─── Category definitions ────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const CATEGORIES = [
|
|
18
|
+
{ key: "all", label: "All" },
|
|
19
|
+
{ key: "pages", label: "Pages", dir: "pages" },
|
|
20
|
+
{ key: "layouts", label: "Layouts", dir: "layouts" },
|
|
21
|
+
{ key: "components", label: "Components", dir: "components" },
|
|
22
|
+
{ key: "content", label: "Content", dir: "content" },
|
|
23
|
+
{ key: "media", label: "Media", dir: "public" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const MEDIA_EXTENSIONS = new Set([
|
|
27
|
+
".jpg",
|
|
28
|
+
".jpeg",
|
|
29
|
+
".png",
|
|
30
|
+
".gif",
|
|
31
|
+
".svg",
|
|
32
|
+
".webp",
|
|
33
|
+
".avif",
|
|
34
|
+
".ico",
|
|
35
|
+
".mp4",
|
|
36
|
+
".webm",
|
|
37
|
+
".mp3",
|
|
38
|
+
".wav",
|
|
39
|
+
".ogg",
|
|
40
|
+
".pdf",
|
|
41
|
+
".woff",
|
|
42
|
+
".woff2",
|
|
43
|
+
".ttf",
|
|
44
|
+
".otf",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
// ─── Module state ────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
let activeCategory = "all";
|
|
50
|
+
let searchQuery = "";
|
|
51
|
+
/** @type {{ name: string; path: string; type: string; category: string; ext: string }[]} */
|
|
52
|
+
let fileCache = [];
|
|
53
|
+
let loading = false;
|
|
54
|
+
/** Track which projectDirs were used for the last load, so we re-scan when they change. */
|
|
55
|
+
let lastProjectDirsKey = "";
|
|
56
|
+
|
|
57
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/** @param {string} name */
|
|
60
|
+
function extOf(name) {
|
|
61
|
+
const dot = name.lastIndexOf(".");
|
|
62
|
+
return dot > 0 ? name.slice(dot).toLowerCase() : "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Map a file path to a display category. Media files override by extension. */
|
|
66
|
+
function categoryFor(/** @type {string} */ dir, /** @type {string} */ ext) {
|
|
67
|
+
if (ext && MEDIA_EXTENSIONS.has(ext)) return "Media";
|
|
68
|
+
if (dir.startsWith("pages")) return "Pages";
|
|
69
|
+
if (dir.startsWith("layouts")) return "Layouts";
|
|
70
|
+
if (dir.startsWith("components")) return "Components";
|
|
71
|
+
if (dir.startsWith("content")) return "Content";
|
|
72
|
+
if (dir.startsWith("public")) return "Media";
|
|
73
|
+
if (dir.startsWith("data")) return "Content";
|
|
74
|
+
if (dir.startsWith("styles")) return "Components";
|
|
75
|
+
return "Other";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Recursively collect all files under a directory.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} dir
|
|
82
|
+
* @param {ReturnType<typeof getPlatform>} platform
|
|
83
|
+
* @returns {Promise<
|
|
84
|
+
* { name: string; path: string; type: string; category: string; ext: string }[]
|
|
85
|
+
* >}
|
|
86
|
+
*/
|
|
87
|
+
async function collectFiles(dir, platform) {
|
|
88
|
+
/** @type {{ name: string; path: string; type: string; category: string; ext: string }[]} */
|
|
89
|
+
const results = [];
|
|
90
|
+
try {
|
|
91
|
+
const entries = await platform.listDirectory(dir);
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (entry.type === "directory") {
|
|
94
|
+
const sub = await collectFiles(entry.path, platform);
|
|
95
|
+
results.push(...sub);
|
|
96
|
+
} else {
|
|
97
|
+
const ext = extOf(entry.name);
|
|
98
|
+
results.push({
|
|
99
|
+
name: entry.name,
|
|
100
|
+
path: entry.path,
|
|
101
|
+
type: ext || "file",
|
|
102
|
+
category: categoryFor(entry.path, ext),
|
|
103
|
+
ext,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Directory may not exist or be inaccessible
|
|
109
|
+
}
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Data loading ────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
async function loadFiles() {
|
|
116
|
+
if (!projectState) return;
|
|
117
|
+
loading = true;
|
|
118
|
+
const platform = getPlatform();
|
|
119
|
+
const dirs = projectState.projectDirs || [];
|
|
120
|
+
lastProjectDirsKey = dirs.join(",");
|
|
121
|
+
const all = await Promise.all(dirs.map((/** @type {string} */ d) => collectFiles(d, platform)));
|
|
122
|
+
fileCache = all.flat();
|
|
123
|
+
fileCache.sort((a, b) => a.path.localeCompare(b.path));
|
|
124
|
+
loading = false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Filtering ───────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
function filteredFiles() {
|
|
130
|
+
let files = fileCache;
|
|
131
|
+
if (activeCategory !== "all") {
|
|
132
|
+
const cat = CATEGORIES.find((c) => c.key === activeCategory);
|
|
133
|
+
if (cat && cat.label) {
|
|
134
|
+
files = files.filter((f) => f.category === cat.label);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (searchQuery) {
|
|
138
|
+
const q = searchQuery.toLowerCase();
|
|
139
|
+
files = files.filter(
|
|
140
|
+
(f) => f.name.toLowerCase().includes(q) || f.path.toLowerCase().includes(q),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return files;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Entity types for "New +" button ────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
const ENTITY_TYPES = [
|
|
149
|
+
{ key: "page", label: "Page", dir: "pages", ext: ".md" },
|
|
150
|
+
{ key: "layout", label: "Layout", dir: "layouts", ext: ".json" },
|
|
151
|
+
{ key: "component", label: "Component", dir: "components", ext: ".json" },
|
|
152
|
+
{ key: "content", label: "Content", dir: "content", ext: ".md" },
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build frontmatter YAML from a collection's schema properties.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} collectionName
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
function buildFrontmatterYaml(collectionName) {
|
|
162
|
+
const config = projectState?.projectConfig;
|
|
163
|
+
const col = config?.collections?.[collectionName];
|
|
164
|
+
if (!col?.schema?.properties) return "title: Untitled\n";
|
|
165
|
+
|
|
166
|
+
let yaml = "";
|
|
167
|
+
for (const [field, def] of Object.entries(col.schema.properties)) {
|
|
168
|
+
const d = /** @type {any} */ (def);
|
|
169
|
+
yaml += `${field}: ${yamlDefault(d.type, d.format)}\n`;
|
|
170
|
+
}
|
|
171
|
+
return yaml || "title: Untitled\n";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get collection-derived entity types from project config.
|
|
176
|
+
*
|
|
177
|
+
* @returns {{ key: string; label: string; dir: string; ext: string; collectionName: string }[]}
|
|
178
|
+
*/
|
|
179
|
+
function getCollectionTypes() {
|
|
180
|
+
const config = projectState?.projectConfig;
|
|
181
|
+
if (!config?.collections) return [];
|
|
182
|
+
return Object.entries(config.collections).map(([name, def]) => {
|
|
183
|
+
const d = /** @type {any} */ (def);
|
|
184
|
+
const dir = d.source ? d.source.replace(/^\.\//, "").split("/")[0] : name;
|
|
185
|
+
return {
|
|
186
|
+
key: `collection:${name}`,
|
|
187
|
+
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
188
|
+
dir,
|
|
189
|
+
ext: ".md",
|
|
190
|
+
collectionName: name,
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Handle creation of a new entity.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} typeKey
|
|
199
|
+
* @param {HTMLElement} container
|
|
200
|
+
* @param {{ openFile: (path: string) => void }} ctx
|
|
201
|
+
*/
|
|
202
|
+
async function handleNewEntity(typeKey, container, ctx) {
|
|
203
|
+
const isCollection = typeKey.startsWith("collection:");
|
|
204
|
+
const collectionName = isCollection ? typeKey.slice("collection:".length) : null;
|
|
205
|
+
const allTypes = [...ENTITY_TYPES, ...getCollectionTypes()];
|
|
206
|
+
const typeInfo = allTypes.find((t) => t.key === typeKey);
|
|
207
|
+
if (!typeInfo) return;
|
|
208
|
+
|
|
209
|
+
const name = prompt(`${typeInfo.label} name:`, "untitled");
|
|
210
|
+
if (!name) return;
|
|
211
|
+
|
|
212
|
+
const slug = name
|
|
213
|
+
.toLowerCase()
|
|
214
|
+
.replace(/\s+/g, "-")
|
|
215
|
+
.replace(/[^a-z0-9-]/g, "");
|
|
216
|
+
const filePath = `${typeInfo.dir}/${slug}${typeInfo.ext}`;
|
|
217
|
+
|
|
218
|
+
let content;
|
|
219
|
+
if (typeInfo.ext === ".md") {
|
|
220
|
+
const frontmatter = collectionName ? buildFrontmatterYaml(collectionName) : "title: Untitled\n";
|
|
221
|
+
content = `---\n${frontmatter}---\n\n`;
|
|
222
|
+
} else {
|
|
223
|
+
content = JSON.stringify({ tagName: "div", children: [] }, null, "\t");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const platform = getPlatform();
|
|
227
|
+
await platform.writeFile(filePath, content);
|
|
228
|
+
invalidateBrowseCache();
|
|
229
|
+
ctx.openFile(filePath);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Upload ─────────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
const UPLOAD_ACCEPT = [
|
|
235
|
+
"image/*",
|
|
236
|
+
"video/*",
|
|
237
|
+
"audio/*",
|
|
238
|
+
".pdf",
|
|
239
|
+
".svg",
|
|
240
|
+
".woff",
|
|
241
|
+
".woff2",
|
|
242
|
+
".ttf",
|
|
243
|
+
".otf",
|
|
244
|
+
].join(",");
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Handle file uploads — writes binary files to public/ directory.
|
|
248
|
+
*
|
|
249
|
+
* @param {FileList | File[]} files
|
|
250
|
+
* @param {HTMLElement} container
|
|
251
|
+
* @param {{ openFile: (path: string) => void }} ctx
|
|
252
|
+
*/
|
|
253
|
+
async function handleUpload(files, container, ctx) {
|
|
254
|
+
const platform = getPlatform();
|
|
255
|
+
for (const file of files) {
|
|
256
|
+
const destPath = `public/${file.name}`;
|
|
257
|
+
await platform.uploadFile(destPath, file);
|
|
258
|
+
}
|
|
259
|
+
invalidateBrowseCache();
|
|
260
|
+
invalidateMediaCache();
|
|
261
|
+
renderBrowse(container, ctx);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ─── Render ──────────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Render the Browse view into the canvas area.
|
|
268
|
+
*
|
|
269
|
+
* @param {HTMLElement} container — the #canvas-wrap element
|
|
270
|
+
* @param {{ openFile: (path: string) => void }} ctx — callbacks from studio.js
|
|
271
|
+
*/
|
|
272
|
+
export async function renderBrowse(container, ctx) {
|
|
273
|
+
// Re-load when projectDirs changed (e.g. project opened after initial render)
|
|
274
|
+
const currentKey = (projectState?.projectDirs || []).join(",");
|
|
275
|
+
if ((!fileCache.length && !loading) || currentKey !== lastProjectDirsKey) {
|
|
276
|
+
await loadFiles();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const files = filteredFiles();
|
|
280
|
+
|
|
281
|
+
const collectionTypes = getCollectionTypes();
|
|
282
|
+
|
|
283
|
+
const filterBar = html`
|
|
284
|
+
<div class="browse-filter-bar">
|
|
285
|
+
<sp-action-group selects="single" size="s" compact>
|
|
286
|
+
${CATEGORIES.map(
|
|
287
|
+
(cat) => html`
|
|
288
|
+
<sp-action-button
|
|
289
|
+
size="s"
|
|
290
|
+
?selected=${activeCategory === cat.key}
|
|
291
|
+
@click=${() => {
|
|
292
|
+
activeCategory = cat.key;
|
|
293
|
+
renderBrowse(container, ctx);
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
${cat.label}
|
|
297
|
+
</sp-action-button>
|
|
298
|
+
`,
|
|
299
|
+
)}
|
|
300
|
+
</sp-action-group>
|
|
301
|
+
<sp-search
|
|
302
|
+
size="s"
|
|
303
|
+
placeholder="Filter files..."
|
|
304
|
+
.value=${searchQuery}
|
|
305
|
+
@input=${(/** @type {any} */ e) => {
|
|
306
|
+
searchQuery = e.target.value;
|
|
307
|
+
renderBrowse(container, ctx);
|
|
308
|
+
}}
|
|
309
|
+
@submit=${(/** @type {Event} */ e) => e.preventDefault()}
|
|
310
|
+
></sp-search>
|
|
311
|
+
<overlay-trigger placement="bottom-start">
|
|
312
|
+
<sp-action-button size="s" slot="trigger">
|
|
313
|
+
<sp-icon-add slot="icon"></sp-icon-add> New
|
|
314
|
+
</sp-action-button>
|
|
315
|
+
<sp-popover slot="click-content" tip>
|
|
316
|
+
<sp-menu
|
|
317
|
+
@change=${(/** @type {any} */ e) => handleNewEntity(e.target.value, container, ctx)}
|
|
318
|
+
>
|
|
319
|
+
${ENTITY_TYPES.map((t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`)}
|
|
320
|
+
${collectionTypes.length
|
|
321
|
+
? html`<sp-menu-divider></sp-menu-divider> ${collectionTypes.map(
|
|
322
|
+
(t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
|
|
323
|
+
)}`
|
|
324
|
+
: ""}
|
|
325
|
+
</sp-menu>
|
|
326
|
+
</sp-popover>
|
|
327
|
+
</overlay-trigger>
|
|
328
|
+
<sp-action-button
|
|
329
|
+
size="s"
|
|
330
|
+
@click=${() => {
|
|
331
|
+
const input = /** @type {HTMLInputElement} */ (
|
|
332
|
+
container.querySelector(".browse-upload-input")
|
|
333
|
+
);
|
|
334
|
+
if (input) input.click();
|
|
335
|
+
}}
|
|
336
|
+
>
|
|
337
|
+
<sp-icon-upload slot="icon"></sp-icon-upload> Upload
|
|
338
|
+
</sp-action-button>
|
|
339
|
+
<input
|
|
340
|
+
type="file"
|
|
341
|
+
multiple
|
|
342
|
+
accept=${UPLOAD_ACCEPT}
|
|
343
|
+
class="browse-upload-input"
|
|
344
|
+
style="display:none"
|
|
345
|
+
@change=${(/** @type {any} */ e) => {
|
|
346
|
+
if (e.target.files?.length) handleUpload(e.target.files, container, ctx);
|
|
347
|
+
e.target.value = "";
|
|
348
|
+
}}
|
|
349
|
+
/>
|
|
350
|
+
</div>
|
|
351
|
+
`;
|
|
352
|
+
|
|
353
|
+
const table = html`
|
|
354
|
+
<sp-table size="m" quiet>
|
|
355
|
+
<sp-table-head>
|
|
356
|
+
<sp-table-head-cell>Name</sp-table-head-cell>
|
|
357
|
+
<sp-table-head-cell>Category</sp-table-head-cell>
|
|
358
|
+
<sp-table-head-cell>Type</sp-table-head-cell>
|
|
359
|
+
<sp-table-head-cell>Path</sp-table-head-cell>
|
|
360
|
+
</sp-table-head>
|
|
361
|
+
<sp-table-body>
|
|
362
|
+
${files.length === 0
|
|
363
|
+
? html`<sp-table-row
|
|
364
|
+
><sp-table-cell
|
|
365
|
+
>${loading ? "Loading..." : "No files found"}</sp-table-cell
|
|
366
|
+
></sp-table-row
|
|
367
|
+
>`
|
|
368
|
+
: files.map(
|
|
369
|
+
(f) => html`
|
|
370
|
+
<sp-table-row
|
|
371
|
+
value=${f.path}
|
|
372
|
+
class="browse-row"
|
|
373
|
+
@click=${() => ctx.openFile(f.path)}
|
|
374
|
+
>
|
|
375
|
+
<sp-table-cell class="browse-name-cell">${f.name}</sp-table-cell>
|
|
376
|
+
<sp-table-cell>${f.category}</sp-table-cell>
|
|
377
|
+
<sp-table-cell>${f.ext || "—"}</sp-table-cell>
|
|
378
|
+
<sp-table-cell class="browse-path-cell">${f.path}</sp-table-cell>
|
|
379
|
+
</sp-table-row>
|
|
380
|
+
`,
|
|
381
|
+
)}
|
|
382
|
+
</sp-table-body>
|
|
383
|
+
</sp-table>
|
|
384
|
+
`;
|
|
385
|
+
|
|
386
|
+
const tpl = html`
|
|
387
|
+
<div
|
|
388
|
+
class="browse-view"
|
|
389
|
+
@dragover=${(/** @type {DragEvent} */ e) => {
|
|
390
|
+
e.preventDefault();
|
|
391
|
+
/** @type {HTMLElement} */ (e.currentTarget).classList.add("browse-drop-active");
|
|
392
|
+
}}
|
|
393
|
+
@dragleave=${(/** @type {DragEvent} */ e) => {
|
|
394
|
+
/** @type {HTMLElement} */ (e.currentTarget).classList.remove("browse-drop-active");
|
|
395
|
+
}}
|
|
396
|
+
@drop=${(/** @type {DragEvent} */ e) => {
|
|
397
|
+
e.preventDefault();
|
|
398
|
+
/** @type {HTMLElement} */ (e.currentTarget).classList.remove("browse-drop-active");
|
|
399
|
+
const files = e.dataTransfer?.files;
|
|
400
|
+
if (files?.length) handleUpload(files, container, ctx);
|
|
401
|
+
}}
|
|
402
|
+
>
|
|
403
|
+
${filterBar}
|
|
404
|
+
<div class="browse-table">${table}</div>
|
|
405
|
+
</div>
|
|
406
|
+
`;
|
|
407
|
+
|
|
408
|
+
litRender(tpl, container);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Force a data reload on next render (e.g., after file creation/deletion). */
|
|
412
|
+
export function invalidateBrowseCache() {
|
|
413
|
+
fileCache = [];
|
|
414
|
+
}
|
|
@@ -6,11 +6,14 @@ import {
|
|
|
6
6
|
insertNode,
|
|
7
7
|
removeNode,
|
|
8
8
|
duplicateNode,
|
|
9
|
+
wrapNode,
|
|
9
10
|
getNodeAtPath,
|
|
10
11
|
parentElementPath,
|
|
11
12
|
childIndex,
|
|
12
13
|
} from "../store.js";
|
|
13
14
|
import { statusMessage } from "../panels/statusbar.js";
|
|
15
|
+
import { convertToComponent } from "./convert-to-component.js";
|
|
16
|
+
import { componentRegistry } from "../files/components.js";
|
|
14
17
|
|
|
15
18
|
/** @type {any} */
|
|
16
19
|
let clipboard = null;
|
|
@@ -68,12 +71,18 @@ document.addEventListener("click", () => {
|
|
|
68
71
|
ctxMenu.removeAttribute("open");
|
|
69
72
|
});
|
|
70
73
|
|
|
74
|
+
/** Dismiss the context menu if open. */
|
|
75
|
+
export function dismissContextMenu() {
|
|
76
|
+
ctxMenu.removeAttribute("open");
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
/**
|
|
72
80
|
* @param {any} e
|
|
73
81
|
* @param {any} path
|
|
74
82
|
* @param {any} S
|
|
83
|
+
* @param {{ onEditComponent?: (path: string) => void }} [opts]
|
|
75
84
|
*/
|
|
76
|
-
export function showContextMenu(e, path, S) {
|
|
85
|
+
export function showContextMenu(e, path, S, opts = {}) {
|
|
77
86
|
e.preventDefault();
|
|
78
87
|
ctxMenu.removeAttribute("open");
|
|
79
88
|
|
|
@@ -91,6 +100,44 @@ export function showContextMenu(e, path, S) {
|
|
|
91
100
|
items.push({ label: "Cut", action: () => cutNode(S) });
|
|
92
101
|
items.push({ label: "Duplicate", action: () => update(duplicateNode(S, S.selection)) });
|
|
93
102
|
items.push({ label: "—" }); // separator
|
|
103
|
+
items.push({
|
|
104
|
+
label: "Insert before",
|
|
105
|
+
action: () => {
|
|
106
|
+
const pp = /** @type {any} */ (parentElementPath(path));
|
|
107
|
+
const idx = /** @type {number} */ (childIndex(path));
|
|
108
|
+
update(insertNode(S, pp, idx, { tagName: "p", children: [] }));
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
items.push({
|
|
112
|
+
label: "Insert after",
|
|
113
|
+
action: () => {
|
|
114
|
+
const pp = /** @type {any} */ (parentElementPath(path));
|
|
115
|
+
const idx = /** @type {number} */ (childIndex(path));
|
|
116
|
+
update(insertNode(S, pp, idx + 1, { tagName: "p", children: [] }));
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
items.push({
|
|
120
|
+
label: "Wrap in Div",
|
|
121
|
+
action: () => update(wrapNode(S, S.selection)),
|
|
122
|
+
});
|
|
123
|
+
if (node.tagName) {
|
|
124
|
+
const isComponent =
|
|
125
|
+
node.tagName.includes("-") &&
|
|
126
|
+
componentRegistry.some((/** @type {any} */ c) => c.tagName === node.tagName);
|
|
127
|
+
if (isComponent && opts.onEditComponent) {
|
|
128
|
+
const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === node.tagName);
|
|
129
|
+
items.push({
|
|
130
|
+
label: "Edit Component",
|
|
131
|
+
action: () => opts.onEditComponent?.(comp.path),
|
|
132
|
+
});
|
|
133
|
+
} else if (!isComponent) {
|
|
134
|
+
items.push({
|
|
135
|
+
label: "Convert to Component",
|
|
136
|
+
action: () => convertToComponent(S),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
items.push({ label: "—" }); // separator
|
|
94
141
|
items.push({ label: "Delete", action: () => update(removeNode(S, S.selection)), danger: true });
|
|
95
142
|
}
|
|
96
143
|
if (clipboard) {
|