@jxsuite/studio 0.26.2 → 0.28.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 +34349 -57015
- package/dist/studio.js.map +130 -398
- package/package.json +5 -11
- package/src/browse/browse.ts +62 -22
- package/src/canvas/canvas-live-render.ts +57 -55
- package/src/canvas/canvas-render.ts +50 -21
- package/src/editor/context-menu.ts +4 -1
- package/src/editor/convert-to-component.ts +8 -2
- package/src/editor/inline-edit.ts +7 -2
- package/src/editor/insertion-helper.ts +12 -3
- package/src/editor/slash-menu.ts +5 -1
- package/src/files/file-ops.ts +121 -102
- package/src/files/files.ts +53 -35
- package/src/format/constraints.ts +55 -0
- package/src/format/format-host.ts +212 -0
- package/src/new-project/new-project-modal.ts +18 -3
- package/src/panels/ai-panel.ts +3 -1
- package/src/panels/block-action-bar.ts +4 -2
- package/src/panels/canvas-dnd.ts +78 -28
- package/src/panels/data-explorer.ts +9 -2
- package/src/panels/dnd.ts +14 -3
- package/src/panels/editors.ts +1 -1
- package/src/panels/git-panel.ts +18 -8
- package/src/panels/head-panel.ts +12 -5
- package/src/panels/left-panel.ts +7 -4
- package/src/panels/panel-events.ts +3 -1
- package/src/panels/properties-panel.ts +9 -3
- package/src/panels/quick-search.ts +10 -10
- package/src/panels/right-panel.ts +6 -2
- package/src/panels/shared.ts +6 -1
- package/src/panels/signals-panel.ts +35 -10
- package/src/panels/style-inputs.ts +5 -1
- package/src/panels/style-panel.ts +6 -2
- package/src/panels/stylebook-layers-panel.ts +12 -3
- package/src/panels/stylebook-panel.ts +24 -8
- package/src/panels/toolbar.ts +16 -3
- package/src/platforms/devserver.ts +32 -3
- package/src/resize-edges.ts +12 -2
- package/src/services/cem-export.ts +9 -3
- package/src/settings/content-types-editor.ts +7 -2
- package/src/settings/defs-editor.ts +6 -1
- package/src/settings/general-settings.ts +3 -1
- package/src/settings/schema-field-ui.ts +11 -3
- package/src/settings/settings-modal.ts +4 -1
- package/src/state.ts +5 -1
- package/src/studio.ts +27 -16
- package/src/tabs/tab.ts +29 -6
- package/src/tabs/transact.ts +9 -2
- package/src/types.ts +15 -6
- package/src/ui/button-group.ts +6 -1
- package/src/ui/expression-editor.ts +14 -3
- package/src/ui/layers.ts +5 -1
- package/src/ui/media-picker.ts +192 -43
- package/src/ui/unit-selector.ts +6 -1
- package/src/utils/canvas-media.ts +1 -1
- package/src/utils/google-fonts.ts +4 -1
- package/src/utils/studio-utils.ts +7 -1
- package/src/view.ts +8 -2
- package/src/markdown/md-allowlist.ts +0 -104
- package/src/markdown/md-convert.ts +0 -846
package/src/studio.ts
CHANGED
|
@@ -40,7 +40,8 @@ import {
|
|
|
40
40
|
setStatusbarRenderer,
|
|
41
41
|
mountStatusbar,
|
|
42
42
|
} from "./panels/statusbar";
|
|
43
|
-
import {
|
|
43
|
+
import { parseSourceForPath, saveFile, exportFile, serializeDocument } from "./files/file-ops";
|
|
44
|
+
import { loadFormats, formatForPath, documentExtensions } from "./format/format-host";
|
|
44
45
|
import {
|
|
45
46
|
loadProject as _loadProject,
|
|
46
47
|
openProject as _openProject,
|
|
@@ -168,7 +169,7 @@ async function navigateBack() {
|
|
|
168
169
|
if (tab.doc.dirty && tab.documentPath) {
|
|
169
170
|
try {
|
|
170
171
|
const platform = getPlatform();
|
|
171
|
-
await platform.writeFile(tab.documentPath, serializeDocument(tab));
|
|
172
|
+
await platform.writeFile(tab.documentPath, await serializeDocument(tab));
|
|
172
173
|
} catch (e) {
|
|
173
174
|
const err = e as Error;
|
|
174
175
|
statusMessage(`Save error: ${err.message}`);
|
|
@@ -198,7 +199,7 @@ async function navigateToLevel(targetIndex: number) {
|
|
|
198
199
|
if (tab.doc.dirty && tab.documentPath) {
|
|
199
200
|
try {
|
|
200
201
|
const platform = getPlatform();
|
|
201
|
-
await platform.writeFile(tab.documentPath, serializeDocument(tab));
|
|
202
|
+
await platform.writeFile(tab.documentPath, await serializeDocument(tab));
|
|
202
203
|
} catch (e) {
|
|
203
204
|
const err = e as Error;
|
|
204
205
|
statusMessage(`Save error: ${err.message}`);
|
|
@@ -222,9 +223,8 @@ async function navigateToLevel(targetIndex: number) {
|
|
|
222
223
|
async function closeFunctionEditor() {
|
|
223
224
|
const tab = activeTab.value;
|
|
224
225
|
const editing =
|
|
225
|
-
/** @type {{ type: string; defName?: string; path?: JxPath; eventKey?: string } | null} */
|
|
226
|
-
|
|
227
|
-
);
|
|
226
|
+
/** @type {{ type: string; defName?: string; path?: JxPath; eventKey?: string } | null} */ tab
|
|
227
|
+
?.session.ui.editingFunction;
|
|
228
228
|
if (!editing || !tab) return;
|
|
229
229
|
if (view.functionEditor) {
|
|
230
230
|
const currentCode = view.functionEditor.getValue();
|
|
@@ -237,7 +237,7 @@ async function closeFunctionEditor() {
|
|
|
237
237
|
const current = node?.[editing.eventKey as string] || {};
|
|
238
238
|
transactDoc(tab, (t) =>
|
|
239
239
|
mutateUpdateProperty(t, editing.path as JxPath, editing.eventKey as string, {
|
|
240
|
-
.../** @type {object} */
|
|
240
|
+
.../** @type {object} */ current,
|
|
241
241
|
$prototype: "Function",
|
|
242
242
|
body: bodyToStore,
|
|
243
243
|
}),
|
|
@@ -531,7 +531,12 @@ if (_projectParam) {
|
|
|
531
531
|
// When opening project.json, default to home page instead
|
|
532
532
|
if (fileRelPath === "project.json" || fileRelPath.endsWith("/project.json")) {
|
|
533
533
|
let opened = false;
|
|
534
|
-
|
|
534
|
+
await loadFormats();
|
|
535
|
+
const homeCandidates = [
|
|
536
|
+
...documentExtensions("page").map((ext) => `pages/index${ext}`),
|
|
537
|
+
"pages/index.json",
|
|
538
|
+
];
|
|
539
|
+
for (const candidate of homeCandidates) {
|
|
535
540
|
try {
|
|
536
541
|
await platform.readFile(candidate);
|
|
537
542
|
fileRelPath = candidate;
|
|
@@ -544,12 +549,14 @@ if (_projectParam) {
|
|
|
544
549
|
|
|
545
550
|
const content = await platform.readFile(fileRelPath);
|
|
546
551
|
if (content) {
|
|
547
|
-
let parsedDoc, frontmatter;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
552
|
+
let parsedDoc, frontmatter, parsedMode;
|
|
553
|
+
await loadFormats();
|
|
554
|
+
const fileFormat = formatForPath(fileRelPath);
|
|
555
|
+
if (fileFormat) {
|
|
556
|
+
const result = await parseSourceForPath(fileRelPath, content);
|
|
551
557
|
parsedDoc = result.document;
|
|
552
558
|
frontmatter = result.frontmatter;
|
|
559
|
+
parsedMode = result.mode;
|
|
553
560
|
} else {
|
|
554
561
|
parsedDoc = JSON.parse(content);
|
|
555
562
|
}
|
|
@@ -560,10 +567,10 @@ if (_projectParam) {
|
|
|
560
567
|
documentPath: fileRelPath,
|
|
561
568
|
document: parsedDoc,
|
|
562
569
|
...(frontmatter != null && { frontmatter }),
|
|
563
|
-
sourceFormat:
|
|
570
|
+
sourceFormat: fileFormat?.name ?? null,
|
|
564
571
|
});
|
|
565
572
|
|
|
566
|
-
if (
|
|
573
|
+
if (parsedMode === "content" && activeTab.value) activeTab.value.doc.mode = "content";
|
|
567
574
|
if (fileRelPath === "project.json" && activeTab.value) {
|
|
568
575
|
activeTab.value.session.ui.canvasMode = "stylebook";
|
|
569
576
|
}
|
|
@@ -671,7 +678,11 @@ async function openRecentProject(root: string) {
|
|
|
671
678
|
}
|
|
672
679
|
}
|
|
673
680
|
function renderFilesTemplate() {
|
|
674
|
-
return _renderFilesTemplate({
|
|
681
|
+
return _renderFilesTemplate({
|
|
682
|
+
openProject,
|
|
683
|
+
openFileFromTree,
|
|
684
|
+
renderLeftPanel,
|
|
685
|
+
});
|
|
675
686
|
}
|
|
676
687
|
function openFileFromTree(path: string) {
|
|
677
688
|
return openFileInTab(path);
|
|
@@ -718,7 +729,7 @@ function scheduleAutosave() {
|
|
|
718
729
|
if (t?.fileHandle && t.doc.dirty && "createWritable" in t.fileHandle) {
|
|
719
730
|
try {
|
|
720
731
|
const writable = await t.fileHandle.createWritable();
|
|
721
|
-
await writable.write(serializeDocument(t));
|
|
732
|
+
await writable.write(await serializeDocument(t));
|
|
722
733
|
await writable.close();
|
|
723
734
|
t.doc.dirty = false;
|
|
724
735
|
statusMessage("Auto-saved");
|
package/src/tabs/tab.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
import { reactive, effectScope } from "../reactivity";
|
|
3
|
+
import { formatByName, formatForPath } from "../format/format-host";
|
|
3
4
|
import type {
|
|
4
5
|
GitDiffState,
|
|
5
6
|
InlineEditDef,
|
|
@@ -47,7 +48,11 @@ export interface Tab {
|
|
|
47
48
|
documentPath: string | null;
|
|
48
49
|
fileHandle: FileSystemFileHandle | null;
|
|
49
50
|
capabilities: { modes: string[] };
|
|
50
|
-
scope: {
|
|
51
|
+
scope: {
|
|
52
|
+
stop(): void;
|
|
53
|
+
run<T>(fn: () => T): T | undefined;
|
|
54
|
+
[k: string]: unknown;
|
|
55
|
+
};
|
|
51
56
|
doc: {
|
|
52
57
|
document: JxMutableNode;
|
|
53
58
|
content: { frontmatter: Record<string, unknown> };
|
|
@@ -152,8 +157,7 @@ export function createTab({
|
|
|
152
157
|
document,
|
|
153
158
|
sourceFormat,
|
|
154
159
|
content: { frontmatter: frontmatter || {} },
|
|
155
|
-
mode:
|
|
156
|
-
sourceFormat === "md" ? "content" : documentPath?.endsWith(".md") ? "content" : "component",
|
|
160
|
+
mode: inferDocumentMode(documentPath, sourceFormat),
|
|
157
161
|
handlersSource: null,
|
|
158
162
|
dirty: false,
|
|
159
163
|
}),
|
|
@@ -163,7 +167,12 @@ export function createTab({
|
|
|
163
167
|
clipboard: null,
|
|
164
168
|
documentStack: [],
|
|
165
169
|
ui: createDefaultUi(),
|
|
166
|
-
canvas: {
|
|
170
|
+
canvas: {
|
|
171
|
+
status: "idle",
|
|
172
|
+
scope: null,
|
|
173
|
+
error: null,
|
|
174
|
+
pendingInlineEdit: null,
|
|
175
|
+
},
|
|
167
176
|
}),
|
|
168
177
|
history: reactive({
|
|
169
178
|
snapshots: [{ document: structuredClone(document), selection: null }],
|
|
@@ -181,11 +190,25 @@ export function createTab({
|
|
|
181
190
|
*/
|
|
182
191
|
function inferModes(documentPath: string | null | undefined, sourceFormat: string | null) {
|
|
183
192
|
if (documentPath === "project.json") return ["stylebook", "source"];
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
const format = formatByName(sourceFormat) ?? formatForPath(documentPath);
|
|
194
|
+
if (format) return format.studio?.modes ?? ["edit", "design", "preview", "source"];
|
|
186
195
|
return ALL_MODES;
|
|
187
196
|
}
|
|
188
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Document mode for a new tab: format-class documents default to their $studio.documentMode
|
|
200
|
+
* (content unless promoted to component); JSON is component.
|
|
201
|
+
*
|
|
202
|
+
* @param {string | null | undefined} documentPath
|
|
203
|
+
* @param {string | null} sourceFormat
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
function inferDocumentMode(documentPath: string | null | undefined, sourceFormat: string | null) {
|
|
207
|
+
const format = formatByName(sourceFormat) ?? formatForPath(documentPath);
|
|
208
|
+
if (format) return format.studio?.documentMode?.default ?? "content";
|
|
209
|
+
return "component";
|
|
210
|
+
}
|
|
211
|
+
|
|
189
212
|
/**
|
|
190
213
|
* Dispose a tab — stops its effectScope, killing all effects created within it.
|
|
191
214
|
*
|
package/src/tabs/transact.ts
CHANGED
|
@@ -100,6 +100,7 @@ export function mutateInsertNode(
|
|
|
100
100
|
nodeDef: JxMutableNode,
|
|
101
101
|
) {
|
|
102
102
|
const parent = getNodeAtPath(tab.doc.document, parentPath);
|
|
103
|
+
if (!parent) return;
|
|
103
104
|
if (!parent.children) parent.children = [];
|
|
104
105
|
parent.children.splice(index, 0, structuredClone(nodeDef));
|
|
105
106
|
}
|
|
@@ -144,7 +145,10 @@ export function mutateWrapNode(tab: Tab, path: JxPath, wrapperTag: string = "div
|
|
|
144
145
|
if (!node) return;
|
|
145
146
|
const elemPath = parentElementPath(path) as JxPath;
|
|
146
147
|
const idx = childIndex(path) as number;
|
|
147
|
-
const wrapper = {
|
|
148
|
+
const wrapper = {
|
|
149
|
+
tagName: wrapperTag,
|
|
150
|
+
children: [structuredClone(toRaw(node))],
|
|
151
|
+
};
|
|
148
152
|
(getNodeAtPath(tab.doc.document, elemPath).children as JxMutableNode[]).splice(idx, 1, wrapper);
|
|
149
153
|
tab.session.selection = [...elemPath, "children", idx];
|
|
150
154
|
}
|
|
@@ -159,9 +163,12 @@ export function mutateMoveNode(tab: Tab, fromPath: JxPath, toParentPath: JxPath,
|
|
|
159
163
|
const doc = tab.doc.document;
|
|
160
164
|
const fromParentPath = parentElementPath(fromPath) as JxPath;
|
|
161
165
|
const fromIdx = childIndex(fromPath) as number;
|
|
166
|
+
if (!fromParentPath || typeof fromIdx !== "number") return;
|
|
162
167
|
const fromParent = getNodeAtPath(doc, fromParentPath);
|
|
163
|
-
const [node] = (fromParent.children as JxMutableNode[]).splice(fromIdx, 1);
|
|
164
168
|
const toParent = getNodeAtPath(doc, toParentPath);
|
|
169
|
+
if (!fromParent || !Array.isArray(fromParent.children) || !toParent) return;
|
|
170
|
+
const [node] = (fromParent.children as JxMutableNode[]).splice(fromIdx, 1);
|
|
171
|
+
if (node === undefined) return;
|
|
165
172
|
if (!toParent.children) toParent.children = [];
|
|
166
173
|
let adjustedIndex = toIndex;
|
|
167
174
|
if (fromParent === toParent && fromIdx < toIndex) adjustedIndex--;
|
package/src/types.ts
CHANGED
|
@@ -67,7 +67,11 @@ export interface StudioPlatform {
|
|
|
67
67
|
} | null>;
|
|
68
68
|
probeRootProject(): Promise<{
|
|
69
69
|
meta: { root: string; name: string };
|
|
70
|
-
info: {
|
|
70
|
+
info: {
|
|
71
|
+
isSiteProject: boolean;
|
|
72
|
+
projectConfig?: ProjectConfig | null;
|
|
73
|
+
directories?: string[];
|
|
74
|
+
};
|
|
71
75
|
} | null>;
|
|
72
76
|
listDirectory(dir: string): Promise<DirEntry[]>;
|
|
73
77
|
readFile(path: string): Promise<string>;
|
|
@@ -81,11 +85,17 @@ export interface StudioPlatform {
|
|
|
81
85
|
removePackage(name: string): Promise<unknown>;
|
|
82
86
|
listPackages(): Promise<PackageInfo[]>;
|
|
83
87
|
codeService(action: string, payload: unknown): Promise<CodeServiceResult | null>;
|
|
84
|
-
resolveSiteContext(
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
resolveSiteContext(filePath: string): Promise<{
|
|
89
|
+
sitePath: string | null;
|
|
90
|
+
projectConfig?: ProjectConfig;
|
|
91
|
+
fileRelPath?: string;
|
|
92
|
+
}>;
|
|
87
93
|
locateFile(name: string): Promise<string | null>;
|
|
88
|
-
searchFiles(query: string): Promise<DirEntry[]>;
|
|
94
|
+
searchFiles(query: string, extensions?: string[]): Promise<DirEntry[]>;
|
|
95
|
+
/** List the project's registered format classes (auto-discovered from imports). */
|
|
96
|
+
listFormats?(): Promise<unknown[]>;
|
|
97
|
+
/** Invoke a format capability (parse/serialize) — { format, action, source?, doc?, options? }. */
|
|
98
|
+
formatAction?(payload: Record<string, unknown>): Promise<unknown>;
|
|
89
99
|
fetchPluginSchema(src: string, prototype?: string, base?: string): Promise<unknown>;
|
|
90
100
|
gitStatus(): Promise<GitStatusResult>;
|
|
91
101
|
gitBranches(): Promise<GitBranchesResult>;
|
|
@@ -156,7 +166,6 @@ export interface GitDiffState {
|
|
|
156
166
|
filePath: string;
|
|
157
167
|
originalContent: string;
|
|
158
168
|
currentContent: string;
|
|
159
|
-
isMarkdown: boolean;
|
|
160
169
|
fileStatus: string;
|
|
161
170
|
originalDoc?: unknown;
|
|
162
171
|
currentDoc?: unknown;
|
package/src/ui/button-group.ts
CHANGED
|
@@ -40,7 +40,12 @@ export function renderButtonGroup(
|
|
|
40
40
|
const extraSelected = hasExtra && extra.includes(String(value));
|
|
41
41
|
|
|
42
42
|
return html`
|
|
43
|
-
<div
|
|
43
|
+
<div
|
|
44
|
+
class=${classMap({
|
|
45
|
+
"button-group-combo": true,
|
|
46
|
+
"has-overflow": hasExtra,
|
|
47
|
+
})}
|
|
48
|
+
>
|
|
44
49
|
<sp-action-group size="s" compact>
|
|
45
50
|
${values.map(
|
|
46
51
|
(v: string) => html`
|
|
@@ -31,7 +31,10 @@ const OPERATOR_GROUPS = [
|
|
|
31
31
|
{ label: "Arithmetic", ops: ["+", "-", "*", "/", "%"] },
|
|
32
32
|
{ label: "Comparison", ops: ["===", "!==", "<", "<=", ">", ">="] },
|
|
33
33
|
{ label: "Logical", ops: ["&&", "||"] },
|
|
34
|
-
{
|
|
34
|
+
{
|
|
35
|
+
label: "Array methods",
|
|
36
|
+
ops: ["push", "pop", "shift", "unshift", "splice"],
|
|
37
|
+
},
|
|
35
38
|
{ label: "Aggregate", ops: ["reduce", "map", "filter"] },
|
|
36
39
|
];
|
|
37
40
|
|
|
@@ -304,7 +307,12 @@ function renderLiteralEditor(operand: any, onChange: (newVal: any) => void) {
|
|
|
304
307
|
function renderOperandEditor(
|
|
305
308
|
operand: unknown,
|
|
306
309
|
onChange: (newOperand: unknown) => void,
|
|
307
|
-
opts: {
|
|
310
|
+
opts: {
|
|
311
|
+
stateDefs: string[];
|
|
312
|
+
allowEventRef: boolean;
|
|
313
|
+
depth: number;
|
|
314
|
+
mustBeRef?: boolean;
|
|
315
|
+
},
|
|
308
316
|
): import("lit-html").TemplateResult {
|
|
309
317
|
if (opts.mustBeRef) {
|
|
310
318
|
const refVal = ((operand as Record<string, unknown> | null)?.$ref as string) ?? "";
|
|
@@ -337,7 +345,10 @@ function renderOperandEditor(
|
|
|
337
345
|
(r) => onChange({ $ref: r }),
|
|
338
346
|
opts,
|
|
339
347
|
)
|
|
340
|
-
: renderExpressionEditor(operand, onChange, {
|
|
348
|
+
: renderExpressionEditor(operand, onChange, {
|
|
349
|
+
...opts,
|
|
350
|
+
depth: opts.depth + 1,
|
|
351
|
+
})}
|
|
341
352
|
</div>
|
|
342
353
|
`;
|
|
343
354
|
}
|
package/src/ui/layers.ts
CHANGED
|
@@ -48,7 +48,11 @@ export function showDialog<T>(
|
|
|
48
48
|
export function showConfirmDialog(
|
|
49
49
|
headline: string,
|
|
50
50
|
message: string | import("lit-html").TemplateResult,
|
|
51
|
-
opts: {
|
|
51
|
+
opts: {
|
|
52
|
+
confirmLabel?: string;
|
|
53
|
+
cancelLabel?: string;
|
|
54
|
+
destructive?: boolean;
|
|
55
|
+
} = {},
|
|
52
56
|
) {
|
|
53
57
|
const { confirmLabel = "Confirm", cancelLabel = "Cancel", destructive = false } = opts;
|
|
54
58
|
return showDialog(
|
package/src/ui/media-picker.ts
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* files from the project's public/ directory, with thumbnail previews for images.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { html, nothing } from "lit-html";
|
|
9
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
10
10
|
import { live } from "lit-html/directives/live.js";
|
|
11
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
11
12
|
import { getPlatform } from "../platform";
|
|
12
13
|
import { debouncedStyleCommit } from "../store";
|
|
14
|
+
import { getLayerSlot } from "./layers";
|
|
13
15
|
|
|
14
16
|
// ─── Media file cache ────────────────────────────────────────────────────────
|
|
15
17
|
|
|
@@ -92,6 +94,184 @@ export function invalidateMediaCache() {
|
|
|
92
94
|
mediaCacheLoaded = false;
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
// ─── Popover state ───────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/** @type {((val: string) => void) | null} */
|
|
100
|
+
let _popoverOnCommit: ((val: string) => void) | null = null;
|
|
101
|
+
|
|
102
|
+
/** @type {HTMLElement | null} */
|
|
103
|
+
let _popoverAnchorEl: HTMLElement | null = null;
|
|
104
|
+
|
|
105
|
+
/** @type {HTMLInputElement | null} */
|
|
106
|
+
let _popoverFilterEl: HTMLInputElement | null = null;
|
|
107
|
+
|
|
108
|
+
let _popoverFilter = "";
|
|
109
|
+
|
|
110
|
+
function dismissMediaPickerPopover() {
|
|
111
|
+
_popoverFilter = "";
|
|
112
|
+
_popoverOnCommit = null;
|
|
113
|
+
_popoverAnchorEl = null;
|
|
114
|
+
_popoverFilterEl = null;
|
|
115
|
+
document.removeEventListener("keydown", onPopoverKeydown, true);
|
|
116
|
+
document.removeEventListener("mousedown", onPopoverOutsideClick, true);
|
|
117
|
+
litRender(nothing, getLayerSlot("popover", "media-picker"));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** @param {KeyboardEvent} e */
|
|
121
|
+
function onPopoverKeydown(e: KeyboardEvent) {
|
|
122
|
+
if (e.key === "Escape") {
|
|
123
|
+
dismissMediaPickerPopover();
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
e.stopPropagation();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @param {MouseEvent} e */
|
|
130
|
+
function onPopoverOutsideClick(e: MouseEvent) {
|
|
131
|
+
const host = getLayerSlot("popover", "media-picker");
|
|
132
|
+
if (!host.contains(e.target as Node)) {
|
|
133
|
+
dismissMediaPickerPopover();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderMediaPickerPopover() {
|
|
138
|
+
const host = getLayerSlot("popover", "media-picker");
|
|
139
|
+
const rect = _popoverAnchorEl?.getBoundingClientRect();
|
|
140
|
+
if (!rect) return;
|
|
141
|
+
|
|
142
|
+
const query = _popoverFilter.toLowerCase();
|
|
143
|
+
const filtered = query
|
|
144
|
+
? mediaCache.filter(
|
|
145
|
+
(m) => m.path.toLowerCase().includes(query) || m.name.toLowerCase().includes(query),
|
|
146
|
+
)
|
|
147
|
+
: mediaCache;
|
|
148
|
+
const options = filtered.slice(0, 50);
|
|
149
|
+
|
|
150
|
+
// Compute initial position below the anchor
|
|
151
|
+
let left = rect.left;
|
|
152
|
+
let top = rect.bottom + 4;
|
|
153
|
+
|
|
154
|
+
// Estimate popover dimensions for viewport clamping before first paint
|
|
155
|
+
const estimatedWidth = 280;
|
|
156
|
+
const estimatedHeight = Math.min(options.length * 36 + 48, 360);
|
|
157
|
+
|
|
158
|
+
if (left + estimatedWidth > window.innerWidth - 8) {
|
|
159
|
+
left = Math.max(8, window.innerWidth - estimatedWidth - 8);
|
|
160
|
+
}
|
|
161
|
+
if (top + estimatedHeight > window.innerHeight - 8) {
|
|
162
|
+
top = Math.max(8, rect.top - estimatedHeight - 4);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let _popoverEl: HTMLElement | null = null;
|
|
166
|
+
|
|
167
|
+
litRender(
|
|
168
|
+
html`
|
|
169
|
+
<sp-popover
|
|
170
|
+
open
|
|
171
|
+
${ref((el) => {
|
|
172
|
+
_popoverEl = (el as HTMLElement | undefined) || null;
|
|
173
|
+
})}
|
|
174
|
+
style="position:fixed;left:${left}px;top:${top}px;z-index:30;max-height:360px;overflow-y:auto;min-width:240px"
|
|
175
|
+
>
|
|
176
|
+
<input
|
|
177
|
+
class="media-picker-filter"
|
|
178
|
+
type="text"
|
|
179
|
+
placeholder="Search images…"
|
|
180
|
+
autocomplete="off"
|
|
181
|
+
style="display:block;width:100%;box-sizing:border-box;padding:6px 10px;border:none;border-bottom:1px solid var(--border, #444);outline:none;font-size:13px;background:transparent;color:inherit"
|
|
182
|
+
${ref((el) => {
|
|
183
|
+
_popoverFilterEl = (el as HTMLInputElement | null) || null;
|
|
184
|
+
})}
|
|
185
|
+
@input=${(e: Event) => {
|
|
186
|
+
_popoverFilter = (e.target as HTMLInputElement).value;
|
|
187
|
+
renderMediaPickerPopover();
|
|
188
|
+
}}
|
|
189
|
+
@click=${(e: MouseEvent) => e.stopPropagation()}
|
|
190
|
+
/>
|
|
191
|
+
<sp-menu
|
|
192
|
+
style="min-width:220px"
|
|
193
|
+
@change=${(e: Event) => {
|
|
194
|
+
_popoverOnCommit?.((e.target as HTMLInputElement).value);
|
|
195
|
+
dismissMediaPickerPopover();
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
${options.length
|
|
199
|
+
? options.map(
|
|
200
|
+
(m) => html`
|
|
201
|
+
<sp-menu-item value=${m.path}>
|
|
202
|
+
${m.isImage
|
|
203
|
+
? html`<img
|
|
204
|
+
slot="icon"
|
|
205
|
+
src=${m.path}
|
|
206
|
+
alt=""
|
|
207
|
+
style="width:24px;height:24px;object-fit:cover;border-radius:2px"
|
|
208
|
+
/>`
|
|
209
|
+
: nothing}
|
|
210
|
+
${m.name}
|
|
211
|
+
</sp-menu-item>
|
|
212
|
+
`,
|
|
213
|
+
)
|
|
214
|
+
: html`<sp-menu-item disabled>No matches</sp-menu-item>`}
|
|
215
|
+
${filtered.length > 50
|
|
216
|
+
? html`<sp-menu-item disabled>…${filtered.length - 50} more</sp-menu-item>`
|
|
217
|
+
: nothing}
|
|
218
|
+
</sp-menu>
|
|
219
|
+
</sp-popover>
|
|
220
|
+
`,
|
|
221
|
+
host,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Fine-tune position after render using actual measured dimensions
|
|
225
|
+
requestAnimationFrame(() => {
|
|
226
|
+
if (_popoverEl) {
|
|
227
|
+
const popoverRect = _popoverEl.getBoundingClientRect();
|
|
228
|
+
let adjLeft = popoverRect.left;
|
|
229
|
+
let adjTop = popoverRect.top;
|
|
230
|
+
let needsAdjust = false;
|
|
231
|
+
|
|
232
|
+
if (popoverRect.right > window.innerWidth - 4) {
|
|
233
|
+
adjLeft = Math.max(4, window.innerWidth - popoverRect.width - 4);
|
|
234
|
+
needsAdjust = true;
|
|
235
|
+
}
|
|
236
|
+
if (popoverRect.bottom > window.innerHeight - 4) {
|
|
237
|
+
adjTop = Math.max(4, window.innerHeight - popoverRect.height - 4);
|
|
238
|
+
needsAdjust = true;
|
|
239
|
+
}
|
|
240
|
+
if (popoverRect.left < 4) {
|
|
241
|
+
adjLeft = 4;
|
|
242
|
+
needsAdjust = true;
|
|
243
|
+
}
|
|
244
|
+
if (popoverRect.top < 4) {
|
|
245
|
+
adjTop = 4;
|
|
246
|
+
needsAdjust = true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (needsAdjust) {
|
|
250
|
+
_popoverEl.style.left = `${adjLeft}px`;
|
|
251
|
+
_popoverEl.style.top = `${adjTop}px`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (_popoverFilterEl) _popoverFilterEl.focus();
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {HTMLElement} anchorEl
|
|
261
|
+
* @param {(val: string) => void} onCommit
|
|
262
|
+
*/
|
|
263
|
+
function showMediaPickerPopover(anchorEl: HTMLElement, onCommit: (val: string) => void) {
|
|
264
|
+
dismissMediaPickerPopover();
|
|
265
|
+
_popoverOnCommit = onCommit;
|
|
266
|
+
_popoverAnchorEl = anchorEl;
|
|
267
|
+
_popoverFilter = "";
|
|
268
|
+
renderMediaPickerPopover();
|
|
269
|
+
document.addEventListener("keydown", onPopoverKeydown, true);
|
|
270
|
+
requestAnimationFrame(() => {
|
|
271
|
+
document.addEventListener("mousedown", onPopoverOutsideClick, true);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
95
275
|
// ─── Render ──────────────────────────────────────────────────────────────────
|
|
96
276
|
|
|
97
277
|
/**
|
|
@@ -111,17 +291,6 @@ export function renderMediaPicker(prop: string, value: string, onCommit: (val: s
|
|
|
111
291
|
currentValue.slice(currentValue.lastIndexOf(".")).toLowerCase(),
|
|
112
292
|
);
|
|
113
293
|
|
|
114
|
-
// Filter media options based on current input
|
|
115
|
-
const query = currentValue.toLowerCase();
|
|
116
|
-
const filtered = query
|
|
117
|
-
? mediaCache.filter(
|
|
118
|
-
(m) => m.path.toLowerCase().includes(query) || m.name.toLowerCase().includes(query),
|
|
119
|
-
)
|
|
120
|
-
: mediaCache;
|
|
121
|
-
|
|
122
|
-
// Limit displayed options
|
|
123
|
-
const options = filtered.slice(0, 20);
|
|
124
|
-
|
|
125
294
|
return html`
|
|
126
295
|
<div class="media-picker">
|
|
127
296
|
${isImage && currentValue
|
|
@@ -138,37 +307,17 @@ export function renderMediaPicker(prop: string, value: string, onCommit: (val: s
|
|
|
138
307
|
></sp-textfield>
|
|
139
308
|
${mediaCache.length > 0
|
|
140
309
|
? html`
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
(m) => html`
|
|
153
|
-
<sp-menu-item value=${m.path}>
|
|
154
|
-
${m.isImage
|
|
155
|
-
? html`<img
|
|
156
|
-
slot="icon"
|
|
157
|
-
src=${m.path}
|
|
158
|
-
alt=""
|
|
159
|
-
style="width:24px;height:24px;object-fit:cover;border-radius:2px"
|
|
160
|
-
/>`
|
|
161
|
-
: nothing}
|
|
162
|
-
${m.name}
|
|
163
|
-
</sp-menu-item>
|
|
164
|
-
`,
|
|
165
|
-
)}
|
|
166
|
-
${filtered.length > 20
|
|
167
|
-
? html`<sp-menu-item disabled>...${filtered.length - 20} more</sp-menu-item>`
|
|
168
|
-
: nothing}
|
|
169
|
-
</sp-menu>
|
|
170
|
-
</sp-popover>
|
|
171
|
-
</overlay-trigger>
|
|
310
|
+
<sp-action-button
|
|
311
|
+
size="xs"
|
|
312
|
+
quiet
|
|
313
|
+
title="Browse media"
|
|
314
|
+
@click=${(e: MouseEvent) => {
|
|
315
|
+
loadMediaCache();
|
|
316
|
+
showMediaPickerPopover(e.currentTarget as HTMLElement, onCommit);
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
<sp-icon-image slot="icon"></sp-icon-image>
|
|
320
|
+
</sp-action-button>
|
|
172
321
|
`
|
|
173
322
|
: nothing}
|
|
174
323
|
</div>
|
package/src/ui/unit-selector.ts
CHANGED
|
@@ -66,7 +66,12 @@ export function renderUnitSelector(
|
|
|
66
66
|
|
|
67
67
|
return html`
|
|
68
68
|
<div class="style-input-number-unit">
|
|
69
|
-
<div
|
|
69
|
+
<div
|
|
70
|
+
class=${classMap({
|
|
71
|
+
"input-group": true,
|
|
72
|
+
"is-expression": isExpression,
|
|
73
|
+
})}
|
|
74
|
+
>
|
|
70
75
|
<sp-textfield
|
|
71
76
|
size="s"
|
|
72
77
|
placeholder=${numericPlaceholder}
|
|
@@ -83,7 +83,7 @@ export function applyCanvasStyle(
|
|
|
83
83
|
const mediaName = key.slice(1);
|
|
84
84
|
if (mediaName === "--") continue;
|
|
85
85
|
if (activeBreakpoints.has(mediaName) || featureToggles[mediaName]) {
|
|
86
|
-
for (const [prop, v] of Object.entries(/** @type {Record<string, unknown>} */
|
|
86
|
+
for (const [prop, v] of Object.entries(/** @type {Record<string, unknown>} */ val)) {
|
|
87
87
|
if (typeof v === "string" || typeof v === "number") {
|
|
88
88
|
try {
|
|
89
89
|
if (prop.startsWith("--")) el.style.setProperty(prop, String(v));
|
|
@@ -73,7 +73,10 @@ export function ensureGoogleFontPreconnects(head: JxHeadEntry[]) {
|
|
|
73
73
|
e?.attributes?.href === origin,
|
|
74
74
|
);
|
|
75
75
|
if (!exists) {
|
|
76
|
-
const attrs: Record<string, string | boolean> = {
|
|
76
|
+
const attrs: Record<string, string | boolean> = {
|
|
77
|
+
rel: "preconnect",
|
|
78
|
+
href: origin,
|
|
79
|
+
};
|
|
77
80
|
if (origin === "https://fonts.gstatic.com") attrs.crossorigin = "";
|
|
78
81
|
head.push({ tagName: "link", attributes: attrs });
|
|
79
82
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* These are all side-effect-free functions used by style/properties/events panels.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { formatByName, defaultContentFormat } from "../format/format-host";
|
|
7
8
|
import type { ProjectConfig, ContentTypeDef } from "@jxsuite/schema/types";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -155,7 +156,12 @@ export function findContentTypeSchema(
|
|
|
155
156
|
return { name, schema: def.schema };
|
|
156
157
|
}
|
|
157
158
|
} else {
|
|
158
|
-
const ext =
|
|
159
|
+
const ext =
|
|
160
|
+
def.format === "json"
|
|
161
|
+
? ".json"
|
|
162
|
+
: (formatByName(def.format)?.extensions[0] ??
|
|
163
|
+
defaultContentFormat()?.extensions[0] ??
|
|
164
|
+
".json");
|
|
159
165
|
if (documentPath.startsWith(src + "/") && documentPath.endsWith(ext)) {
|
|
160
166
|
return { name, schema: def.schema };
|
|
161
167
|
}
|
package/src/view.ts
CHANGED
|
@@ -16,7 +16,9 @@ interface ViewState {
|
|
|
16
16
|
panY: number;
|
|
17
17
|
prevCanvasMode: string | null;
|
|
18
18
|
monacoEditor:
|
|
19
|
-
| (import("monaco-editor").editor.IStandaloneCodeEditor & {
|
|
19
|
+
| (import("monaco-editor").editor.IStandaloneCodeEditor & {
|
|
20
|
+
_ignoreNextChange?: boolean;
|
|
21
|
+
})
|
|
20
22
|
| null;
|
|
21
23
|
functionEditor:
|
|
22
24
|
| (import("monaco-editor").editor.IStandaloneCodeEditor & {
|
|
@@ -42,7 +44,11 @@ interface ViewState {
|
|
|
42
44
|
forcedAttrEl: HTMLElement | null;
|
|
43
45
|
elementsCollapsed: Set<string>;
|
|
44
46
|
elementsFilter: string;
|
|
45
|
-
lastDragInput: {
|
|
47
|
+
lastDragInput: {
|
|
48
|
+
clientX: number;
|
|
49
|
+
clientY: number;
|
|
50
|
+
[k: string]: unknown;
|
|
51
|
+
} | null;
|
|
46
52
|
_currentDropTargetRow: HTMLElement | null;
|
|
47
53
|
layerDragSourceHeight: number;
|
|
48
54
|
savedRange: Range | null;
|