@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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format-host — the studio's view of the project's format registry.
|
|
3
|
+
*
|
|
4
|
+
* Format classes are auto-discovered server-side from the project imports map
|
|
5
|
+
* (specs/extensions.md); the studio introspects them via the PAL (`listFormats`) and invokes
|
|
6
|
+
* parse/serialize capabilities via `formatAction` (POST /__studio/format in the dev server, RPC on
|
|
7
|
+
* desktop). The studio itself holds zero format knowledge — `.json` is the single native built-in.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getPlatform } from "../platform";
|
|
11
|
+
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
12
|
+
|
|
13
|
+
export interface StudioFormatHints {
|
|
14
|
+
icon?: string;
|
|
15
|
+
modes?: string[];
|
|
16
|
+
documentMode?: {
|
|
17
|
+
default?: "content" | "component";
|
|
18
|
+
componentWhen?: { frontmatterKey?: string; matches?: string };
|
|
19
|
+
};
|
|
20
|
+
newFileTemplate?: string;
|
|
21
|
+
elements?: {
|
|
22
|
+
block?: string[];
|
|
23
|
+
inline?: string[];
|
|
24
|
+
void?: string[];
|
|
25
|
+
textOnly?: string[];
|
|
26
|
+
nesting?: Record<
|
|
27
|
+
string,
|
|
28
|
+
{
|
|
29
|
+
block?: boolean;
|
|
30
|
+
inline?: boolean;
|
|
31
|
+
directive?: boolean;
|
|
32
|
+
only?: string[];
|
|
33
|
+
}
|
|
34
|
+
>;
|
|
35
|
+
};
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface StudioFormat {
|
|
40
|
+
name: string;
|
|
41
|
+
extensions: string[];
|
|
42
|
+
mediaType: string | null;
|
|
43
|
+
documentKinds: string[];
|
|
44
|
+
exportTarget: boolean;
|
|
45
|
+
remote: boolean;
|
|
46
|
+
studio: StudioFormatHints | null;
|
|
47
|
+
capabilities: Record<string, { identifier: string; timing: string[] }>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let _formats: StudioFormat[] = [];
|
|
51
|
+
let _loaded: Promise<StudioFormat[]> | null = null;
|
|
52
|
+
|
|
53
|
+
/** Load (and cache) the project's format registry from the platform. */
|
|
54
|
+
export function loadFormats(): Promise<StudioFormat[]> {
|
|
55
|
+
if (!_loaded) {
|
|
56
|
+
_loaded = (async () => {
|
|
57
|
+
try {
|
|
58
|
+
const platform = getPlatform() as {
|
|
59
|
+
listFormats?: () => Promise<StudioFormat[]>;
|
|
60
|
+
};
|
|
61
|
+
_formats = (await platform.listFormats?.()) ?? [];
|
|
62
|
+
} catch {
|
|
63
|
+
_formats = [];
|
|
64
|
+
}
|
|
65
|
+
return _formats;
|
|
66
|
+
})();
|
|
67
|
+
}
|
|
68
|
+
return _loaded;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Invalidate the cached registry (call on project switch). */
|
|
72
|
+
export function refreshFormats() {
|
|
73
|
+
_loaded = null;
|
|
74
|
+
_formats = [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Seed the registry directly (tests and hosts that preload format metadata). */
|
|
78
|
+
export function setFormats(formats: StudioFormat[]) {
|
|
79
|
+
_formats = formats;
|
|
80
|
+
_loaded = Promise.resolve(formats);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The last-loaded registry (synchronous access for render paths). */
|
|
84
|
+
export function getFormats(): StudioFormat[] {
|
|
85
|
+
return _formats;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Look up the format claiming a file extension (".md" or "md"), optionally by capability. */
|
|
89
|
+
export function formatByExtension(ext: string, capability?: string): StudioFormat | undefined {
|
|
90
|
+
const norm = ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`;
|
|
91
|
+
return _formats.find(
|
|
92
|
+
(f) => f.extensions.includes(norm) && (!capability || f.capabilities[capability]),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Look up a format by its import name. */
|
|
97
|
+
export function formatByName(name: string | null | undefined): StudioFormat | undefined {
|
|
98
|
+
if (!name) return undefined;
|
|
99
|
+
return _formats.find((f) => f.name === name);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The format claiming a file path's extension, if any. */
|
|
103
|
+
export function formatForPath(path: string | null | undefined): StudioFormat | undefined {
|
|
104
|
+
if (!path) return undefined;
|
|
105
|
+
const dot = path.lastIndexOf(".");
|
|
106
|
+
if (dot === -1) return undefined;
|
|
107
|
+
return formatByExtension(path.slice(dot));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Registered extensions, optionally filtered by document kind. Never includes ".json". */
|
|
111
|
+
export function documentExtensions(kind?: string): string[] {
|
|
112
|
+
const exts = new Set<string>();
|
|
113
|
+
for (const f of _formats) {
|
|
114
|
+
if (kind && !f.documentKinds.includes(kind)) continue;
|
|
115
|
+
for (const e of f.extensions) exts.add(e);
|
|
116
|
+
}
|
|
117
|
+
exts.delete(".json");
|
|
118
|
+
return [...exts];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The default format for new content documents (first content-kind serializer). */
|
|
122
|
+
export function defaultContentFormat(): StudioFormat | undefined {
|
|
123
|
+
return _formats.find((f) => f.capabilities.serialize && f.documentKinds.includes("content"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function formatAction(payload: Record<string, unknown>): Promise<unknown> {
|
|
127
|
+
const platform = getPlatform() as {
|
|
128
|
+
formatAction?: (payload: Record<string, unknown>) => Promise<unknown>;
|
|
129
|
+
};
|
|
130
|
+
if (!platform.formatAction) {
|
|
131
|
+
throw new Error("Platform does not support format actions");
|
|
132
|
+
}
|
|
133
|
+
return platform.formatAction(payload);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Parse source text into a Jx document via the named format's parse capability. */
|
|
137
|
+
export async function formatParse(
|
|
138
|
+
name: string,
|
|
139
|
+
source: string,
|
|
140
|
+
options?: Record<string, unknown>,
|
|
141
|
+
): Promise<JxMutableNode> {
|
|
142
|
+
return (await formatAction({
|
|
143
|
+
format: name,
|
|
144
|
+
action: "parse",
|
|
145
|
+
source,
|
|
146
|
+
options,
|
|
147
|
+
})) as JxMutableNode;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Serialize a Jx document to source text via the named format's serialize capability. */
|
|
151
|
+
export async function formatSerialize(
|
|
152
|
+
name: string,
|
|
153
|
+
doc: Record<string, unknown>,
|
|
154
|
+
options?: Record<string, unknown>,
|
|
155
|
+
): Promise<string> {
|
|
156
|
+
return (await formatAction({
|
|
157
|
+
format: name,
|
|
158
|
+
action: "serialize",
|
|
159
|
+
doc,
|
|
160
|
+
options,
|
|
161
|
+
})) as string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Split a parsed format document into { document, frontmatter, mode } per the format's
|
|
166
|
+
* `$studio.documentMode` hints. Component documents (e.g. a frontmatter `tagName` with a hyphen)
|
|
167
|
+
* keep their full shape; content documents separate frontmatter metadata from the body children.
|
|
168
|
+
*/
|
|
169
|
+
export function splitFormatDocument(format: StudioFormat | undefined, doc: JxMutableNode) {
|
|
170
|
+
const hints = format?.studio?.documentMode;
|
|
171
|
+
const componentWhen = hints?.componentWhen;
|
|
172
|
+
if (componentWhen?.frontmatterKey) {
|
|
173
|
+
const value = doc[componentWhen.frontmatterKey];
|
|
174
|
+
const pattern = componentWhen.matches ? new RegExp(componentWhen.matches) : /./;
|
|
175
|
+
if (typeof value === "string" && pattern.test(value)) {
|
|
176
|
+
return {
|
|
177
|
+
document: doc,
|
|
178
|
+
frontmatter: {} as Record<string, unknown>,
|
|
179
|
+
mode: "component",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (hints?.default === "component") {
|
|
184
|
+
return {
|
|
185
|
+
document: doc,
|
|
186
|
+
frontmatter: {} as Record<string, unknown>,
|
|
187
|
+
mode: "component",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Content document — children form the root-level body; other keys are frontmatter
|
|
192
|
+
const children = (doc.children as unknown[]) ?? [];
|
|
193
|
+
if (children.length === 0) children.push({ tagName: "p", children: [] });
|
|
194
|
+
|
|
195
|
+
const documentKeys = new Set(["state", "imports"]);
|
|
196
|
+
const contentDoc: Record<string, unknown> = { children };
|
|
197
|
+
const frontmatter: Record<string, unknown> = {};
|
|
198
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
199
|
+
if (key === "children") continue;
|
|
200
|
+
if (documentKeys.has(key)) {
|
|
201
|
+
contentDoc[key] = value;
|
|
202
|
+
} else {
|
|
203
|
+
frontmatter[key] = value;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
document: contentDoc as JxMutableNode,
|
|
209
|
+
frontmatter,
|
|
210
|
+
mode: "content",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -20,7 +20,13 @@ let _handle: ReturnType<typeof openModal> | null = null;
|
|
|
20
20
|
* directory: string;
|
|
21
21
|
* }}
|
|
22
22
|
*/
|
|
23
|
-
let _form = {
|
|
23
|
+
let _form = {
|
|
24
|
+
name: "",
|
|
25
|
+
description: "",
|
|
26
|
+
url: "",
|
|
27
|
+
adapter: "static",
|
|
28
|
+
directory: "",
|
|
29
|
+
};
|
|
24
30
|
|
|
25
31
|
let _error: string = "";
|
|
26
32
|
|
|
@@ -35,9 +41,18 @@ let _resolve: ((result: { root: string; config: ProjectConfig } | null) => void)
|
|
|
35
41
|
*
|
|
36
42
|
* @returns {Promise<{ root: string; config: object } | null>}
|
|
37
43
|
*/
|
|
38
|
-
export function openNewProjectModal(): Promise<{
|
|
44
|
+
export function openNewProjectModal(): Promise<{
|
|
45
|
+
root: string;
|
|
46
|
+
config: ProjectConfig;
|
|
47
|
+
} | null> {
|
|
39
48
|
if (_handle) return Promise.resolve(null);
|
|
40
|
-
_form = {
|
|
49
|
+
_form = {
|
|
50
|
+
name: "",
|
|
51
|
+
description: "",
|
|
52
|
+
url: "",
|
|
53
|
+
adapter: "static",
|
|
54
|
+
directory: "",
|
|
55
|
+
};
|
|
41
56
|
_error = "";
|
|
42
57
|
_creating = false;
|
|
43
58
|
|
package/src/panels/ai-panel.ts
CHANGED
|
@@ -48,7 +48,9 @@ export function mountAiPanel() {
|
|
|
48
48
|
checkAuth();
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const _g = globalThis as unknown as {
|
|
51
|
+
const _g = globalThis as unknown as {
|
|
52
|
+
__jxRightPanelRender?: { render: () => void };
|
|
53
|
+
};
|
|
52
54
|
|
|
53
55
|
function rerenderPanel() {
|
|
54
56
|
const { render } = _g.__jxRightPanelRender || {};
|
|
@@ -31,8 +31,10 @@ import type { JxPath } from "../state";
|
|
|
31
31
|
* navigateToComponent: (path: string) => void;
|
|
32
32
|
* } | null}
|
|
33
33
|
*/
|
|
34
|
-
let _ctx: {
|
|
35
|
-
|
|
34
|
+
let _ctx: {
|
|
35
|
+
getCanvasMode: () => string;
|
|
36
|
+
navigateToComponent: (path: string) => void;
|
|
37
|
+
} | null = null;
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* Initialize the block action bar module.
|
package/src/panels/canvas-dnd.ts
CHANGED
|
@@ -42,6 +42,26 @@ export function registerPanelDnD(panel: CanvasPanel) {
|
|
|
42
42
|
const { canvas, dropLine } = panel;
|
|
43
43
|
const allEls = canvas.querySelectorAll("*");
|
|
44
44
|
|
|
45
|
+
// Drop-target callbacks fire on EVERY target in the stack (innermost → outermost),
|
|
46
|
+
// and every canvas element is a drop target — so the indicator and the drop are
|
|
47
|
+
// driven from the monitor using only the innermost target.
|
|
48
|
+
/** Innermost drop target if it belongs to this panel's canvas, else null */
|
|
49
|
+
const innermostCanvasTarget = (location: {
|
|
50
|
+
current: {
|
|
51
|
+
dropTargets: {
|
|
52
|
+
data: Record<string | symbol, unknown>;
|
|
53
|
+
element: Element;
|
|
54
|
+
}[];
|
|
55
|
+
};
|
|
56
|
+
}) => {
|
|
57
|
+
const target = location.current.dropTargets[0];
|
|
58
|
+
if (!target) return null;
|
|
59
|
+
const tEl = target.element as HTMLElement;
|
|
60
|
+
const tPath = target.data.path;
|
|
61
|
+
if (!canvas.contains(tEl) || !Array.isArray(tPath)) return null;
|
|
62
|
+
return { el: tEl, path: tPath as JxPath, isLeaf: !!target.data._isVoid };
|
|
63
|
+
};
|
|
64
|
+
|
|
45
65
|
const monitorCleanup = monitorForElements({
|
|
46
66
|
onDragStart({ location }) {
|
|
47
67
|
view.lastDragInput = location.current.input;
|
|
@@ -52,8 +72,34 @@ export function registerPanelDnD(panel: CanvasPanel) {
|
|
|
52
72
|
},
|
|
53
73
|
onDrag({ location }) {
|
|
54
74
|
view.lastDragInput = location.current.input;
|
|
75
|
+
const target = innermostCanvasTarget(location);
|
|
76
|
+
if (target) {
|
|
77
|
+
if (_activeDropEl && _activeDropEl !== target.el) {
|
|
78
|
+
_activeDropEl.classList.remove("canvas-drop-target");
|
|
79
|
+
}
|
|
80
|
+
_activeDropEl = target.el;
|
|
81
|
+
showCanvasDropIndicator(target.el, target.path, target.isLeaf, panel);
|
|
82
|
+
} else if (location.current.dropTargets.length > 0) {
|
|
83
|
+
// Pointer is over a non-canvas target (e.g. a layer row) — hide this panel's
|
|
84
|
+
// indicator. When over dead space (no targets at all) keep the last indicator
|
|
85
|
+
// visible so it persists for the whole drag.
|
|
86
|
+
if (_activeDropEl && canvas.contains(_activeDropEl)) {
|
|
87
|
+
_activeDropEl.classList.remove("canvas-drop-target");
|
|
88
|
+
_activeDropEl = null;
|
|
89
|
+
}
|
|
90
|
+
dropLine.style.display = "none";
|
|
91
|
+
}
|
|
55
92
|
},
|
|
56
|
-
onDrop() {
|
|
93
|
+
onDrop({ source, location }) {
|
|
94
|
+
const target = innermostCanvasTarget(location);
|
|
95
|
+
if (target) {
|
|
96
|
+
const { instruction, targetPath } = getCanvasDropResult(
|
|
97
|
+
target.el,
|
|
98
|
+
target.path,
|
|
99
|
+
target.isLeaf,
|
|
100
|
+
);
|
|
101
|
+
applyDropInstruction(instruction, source.data, targetPath);
|
|
102
|
+
}
|
|
57
103
|
_activeDropEl?.classList.remove("canvas-drop-target");
|
|
58
104
|
_activeDropEl = null;
|
|
59
105
|
for (const p of canvasPanels) {
|
|
@@ -81,7 +127,7 @@ export function registerPanelDnD(panel: CanvasPanel) {
|
|
|
81
127
|
const isLeaf = VOID_ELEMENTS.has(tag) || !hasElementChildren;
|
|
82
128
|
|
|
83
129
|
const cleanup = dropTargetForElements({
|
|
84
|
-
element: /** @type {HTMLElement} */
|
|
130
|
+
element: /** @type {HTMLElement} */ el,
|
|
85
131
|
canDrop({ source }) {
|
|
86
132
|
const srcPath = source.data.path as JxPath | undefined;
|
|
87
133
|
if (srcPath && isAncestor(srcPath, elPath)) return false;
|
|
@@ -90,26 +136,6 @@ export function registerPanelDnD(panel: CanvasPanel) {
|
|
|
90
136
|
getData() {
|
|
91
137
|
return { path: elPath, _isVoid: isLeaf };
|
|
92
138
|
},
|
|
93
|
-
onDragEnter({ location }) {
|
|
94
|
-
view.lastDragInput = location.current.input;
|
|
95
|
-
if (_activeDropEl && _activeDropEl !== el) {
|
|
96
|
-
_activeDropEl.classList.remove("canvas-drop-target");
|
|
97
|
-
}
|
|
98
|
-
_activeDropEl = el as HTMLElement;
|
|
99
|
-
showCanvasDropIndicator(el as HTMLElement, elPath, isLeaf, panel);
|
|
100
|
-
},
|
|
101
|
-
onDrag({ location }) {
|
|
102
|
-
view.lastDragInput = location.current.input;
|
|
103
|
-
showCanvasDropIndicator(el as HTMLElement, elPath, isLeaf, panel);
|
|
104
|
-
},
|
|
105
|
-
onDragLeave() {},
|
|
106
|
-
onDrop({ source }) {
|
|
107
|
-
dropLine.style.display = "none";
|
|
108
|
-
(el as HTMLElement).classList.remove("canvas-drop-target");
|
|
109
|
-
_activeDropEl = null;
|
|
110
|
-
const { instruction, targetPath } = getCanvasDropResult(el as HTMLElement, elPath, isLeaf);
|
|
111
|
-
applyDropInstruction(instruction, source.data, targetPath);
|
|
112
|
-
},
|
|
113
139
|
});
|
|
114
140
|
view.canvasDndCleanups.push(cleanup);
|
|
115
141
|
}
|
|
@@ -123,13 +149,21 @@ export function registerPanelDnD(panel: CanvasPanel) {
|
|
|
123
149
|
*/
|
|
124
150
|
function getCanvasDropResult(el: HTMLElement, elPath: JxPath, isLeaf: boolean): DropResult {
|
|
125
151
|
if (!view.lastDragInput)
|
|
126
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
instruction: { type: "make-child" },
|
|
154
|
+
referenceEl: el,
|
|
155
|
+
targetPath: elPath,
|
|
156
|
+
};
|
|
127
157
|
const y = view.lastDragInput.clientY;
|
|
128
158
|
|
|
129
159
|
if (elPath.length === 0) {
|
|
130
160
|
const children = Array.from(el.children) as HTMLElement[];
|
|
131
161
|
if (children.length === 0)
|
|
132
|
-
return {
|
|
162
|
+
return {
|
|
163
|
+
instruction: { type: "make-child" },
|
|
164
|
+
referenceEl: el,
|
|
165
|
+
targetPath: elPath,
|
|
166
|
+
};
|
|
133
167
|
return nearestChildEdge(children, y, elPath);
|
|
134
168
|
}
|
|
135
169
|
|
|
@@ -143,10 +177,22 @@ function getCanvasDropResult(el: HTMLElement, elPath: JxPath, isLeaf: boolean):
|
|
|
143
177
|
}
|
|
144
178
|
|
|
145
179
|
if (relY < 0.25)
|
|
146
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
instruction: { type: "reorder-above" },
|
|
182
|
+
referenceEl: el,
|
|
183
|
+
targetPath: elPath,
|
|
184
|
+
};
|
|
147
185
|
if (relY > 0.75)
|
|
148
|
-
return {
|
|
149
|
-
|
|
186
|
+
return {
|
|
187
|
+
instruction: { type: "reorder-below" },
|
|
188
|
+
referenceEl: el,
|
|
189
|
+
targetPath: elPath,
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
instruction: { type: "make-child" },
|
|
193
|
+
referenceEl: el,
|
|
194
|
+
targetPath: elPath,
|
|
195
|
+
};
|
|
150
196
|
}
|
|
151
197
|
|
|
152
198
|
/**
|
|
@@ -181,7 +227,11 @@ function nearestChildEdge(children: HTMLElement[], cursorY: number, parentPath:
|
|
|
181
227
|
}
|
|
182
228
|
|
|
183
229
|
const childPath = [...parentPath, "children", closestIdx];
|
|
184
|
-
return {
|
|
230
|
+
return {
|
|
231
|
+
instruction,
|
|
232
|
+
referenceEl: children[closestIdx],
|
|
233
|
+
targetPath: childPath,
|
|
234
|
+
};
|
|
185
235
|
}
|
|
186
236
|
|
|
187
237
|
/**
|
|
@@ -77,7 +77,10 @@ export function renderDataExplorerTemplate(
|
|
|
77
77
|
return html`
|
|
78
78
|
<div class="data-row">
|
|
79
79
|
<div
|
|
80
|
-
class=${classMap({
|
|
80
|
+
class=${classMap({
|
|
81
|
+
"data-row-header": true,
|
|
82
|
+
expanded: isExpanded,
|
|
83
|
+
})}
|
|
81
84
|
@click=${() => {
|
|
82
85
|
if (expandedDataKeys.has(name)) expandedDataKeys.delete(name);
|
|
83
86
|
else expandedDataKeys.add(name);
|
|
@@ -86,7 +89,11 @@ export function renderDataExplorerTemplate(
|
|
|
86
89
|
>
|
|
87
90
|
<span class="signal-badge ${defCategory(def)}">${defBadgeLabel(def)}</span>
|
|
88
91
|
<span class="data-name">${name}</span>
|
|
89
|
-
<span
|
|
92
|
+
<span
|
|
93
|
+
class=${classMap({
|
|
94
|
+
"data-type": true,
|
|
95
|
+
"data-pending": unwrapped === null,
|
|
96
|
+
})}
|
|
90
97
|
>${dataTypeLabel(value)}</span
|
|
91
98
|
>
|
|
92
99
|
</div>
|
package/src/panels/dnd.ts
CHANGED
|
@@ -115,9 +115,8 @@ export function registerLayersDnD() {
|
|
|
115
115
|
onDrag({ self }: DragSelfArgs) {
|
|
116
116
|
showLayerDropGap(row, self.data, container);
|
|
117
117
|
},
|
|
118
|
-
onDragLeave
|
|
119
|
-
|
|
120
|
-
},
|
|
118
|
+
// No onDragLeave clear — the gap persists while the pointer crosses dead space
|
|
119
|
+
// between rows; the monitor clears it when the drag moves to a non-tree target.
|
|
121
120
|
onDrop() {
|
|
122
121
|
clearLayerDropGap(container);
|
|
123
122
|
},
|
|
@@ -128,6 +127,12 @@ export function registerLayersDnD() {
|
|
|
128
127
|
|
|
129
128
|
// Global monitor
|
|
130
129
|
const monitorCleanup = monitorForElements({
|
|
130
|
+
onDropTargetChange({ location }: DragMonitorDropArgs) {
|
|
131
|
+
// Clear the layer gap when the drag moves onto a non-tree target (e.g. a canvas
|
|
132
|
+
// element). When there is no target at all, keep the gap so it persists.
|
|
133
|
+
const inner = location.current.dropTargets[0];
|
|
134
|
+
if (inner && !extractInstruction(inner.data)) clearLayerDropGap(container);
|
|
135
|
+
},
|
|
131
136
|
onDrop({ source, location }: DragMonitorDropArgs) {
|
|
132
137
|
clearLayerDropGap(container);
|
|
133
138
|
const target = location.current.dropTargets[0];
|
|
@@ -321,6 +326,10 @@ export function applyDropInstruction(
|
|
|
321
326
|
const fromPath = srcData.path as JxPath;
|
|
322
327
|
const targetParent = parentElementPath(targetPath) as JxPath;
|
|
323
328
|
const targetIdx = childIndex(targetPath) as number;
|
|
329
|
+
// Reordering requires a parent and a numeric index; root-level paths can't be
|
|
330
|
+
// reordered around.
|
|
331
|
+
if (instruction.type !== "make-child" && (!targetParent || typeof targetIdx !== "number"))
|
|
332
|
+
return;
|
|
324
333
|
|
|
325
334
|
switch (instruction.type) {
|
|
326
335
|
case "reorder-above":
|
|
@@ -341,6 +350,8 @@ export function applyDropInstruction(
|
|
|
341
350
|
} else if (srcData.type === "block") {
|
|
342
351
|
const targetParent = parentElementPath(targetPath) as JxPath;
|
|
343
352
|
const targetIdx = childIndex(targetPath) as number;
|
|
353
|
+
if (instruction.type !== "make-child" && (!targetParent || typeof targetIdx !== "number"))
|
|
354
|
+
return;
|
|
344
355
|
|
|
345
356
|
switch (instruction.type) {
|
|
346
357
|
case "reorder-above":
|
package/src/panels/editors.ts
CHANGED
|
@@ -104,7 +104,7 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
|
|
|
104
104
|
|
|
105
105
|
const body = getFunctionBody(editing);
|
|
106
106
|
const args = getFunctionArgs(
|
|
107
|
-
/** @type {EditingTarget} */
|
|
107
|
+
/** @type {EditingTarget} */ editing as EditingTarget,
|
|
108
108
|
activeTab.value?.doc.document,
|
|
109
109
|
);
|
|
110
110
|
|
package/src/panels/git-panel.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { html, nothing } from "lit-html";
|
|
|
8
8
|
import { live } from "lit-html/directives/live.js";
|
|
9
9
|
import { repeat } from "lit-html/directives/repeat.js";
|
|
10
10
|
import { getPlatform } from "../platform";
|
|
11
|
+
import { formatForPath } from "../format/format-host";
|
|
11
12
|
import { updateUi, renderOnly, projectState } from "../store";
|
|
12
13
|
import { activeTab } from "../workspace/workspace";
|
|
13
14
|
import { view } from "../view";
|
|
@@ -191,7 +192,9 @@ export function renderGitPanel(
|
|
|
191
192
|
|
|
192
193
|
if (!status && !loading) {
|
|
193
194
|
refreshGitStatus();
|
|
194
|
-
return html`<div class="git-panel"
|
|
195
|
+
return html`<div class="git-panel">
|
|
196
|
+
<div class="git-loading">Loading...</div>
|
|
197
|
+
</div>`;
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
if (status && !status.isRepo) {
|
|
@@ -213,7 +216,10 @@ export function renderGitPanel(
|
|
|
213
216
|
</sp-action-button>
|
|
214
217
|
<sp-action-button
|
|
215
218
|
size="m"
|
|
216
|
-
@click=${() =>
|
|
219
|
+
@click=${() =>
|
|
220
|
+
publishToGithub({
|
|
221
|
+
projectName: projectState?.name || "my-project",
|
|
222
|
+
})}
|
|
217
223
|
?disabled=${loading}
|
|
218
224
|
>
|
|
219
225
|
<sp-icon-share slot="icon"></sp-icon-share>
|
|
@@ -266,7 +272,10 @@ export function renderGitPanel(
|
|
|
266
272
|
? "Up to date"
|
|
267
273
|
: `${status?.ahead ? `${status.ahead} ahead` : ""}${status?.ahead && status?.behind ? ", " : ""}${status?.behind ? `${status.behind} behind` : ""}`;
|
|
268
274
|
const lastUpdatedStr = _lastUpdated
|
|
269
|
-
? _lastUpdated.toLocaleTimeString([], {
|
|
275
|
+
? _lastUpdated.toLocaleTimeString([], {
|
|
276
|
+
hour: "2-digit",
|
|
277
|
+
minute: "2-digit",
|
|
278
|
+
})
|
|
270
279
|
: "";
|
|
271
280
|
|
|
272
281
|
const hasRemotes = (status?.remotes?.length ?? 0) > 0;
|
|
@@ -332,7 +341,10 @@ export function renderGitPanel(
|
|
|
332
341
|
</div>
|
|
333
342
|
<sp-action-button
|
|
334
343
|
size="s"
|
|
335
|
-
@click=${() =>
|
|
344
|
+
@click=${() =>
|
|
345
|
+
publishToGithub({
|
|
346
|
+
projectName: projectState?.name || "my-project",
|
|
347
|
+
})}
|
|
336
348
|
?disabled=${loading}
|
|
337
349
|
>
|
|
338
350
|
<sp-icon-share slot="icon"></sp-icon-share>
|
|
@@ -481,7 +493,7 @@ export function renderGitPanel(
|
|
|
481
493
|
|
|
482
494
|
const onFileClick = async () => {
|
|
483
495
|
if (file.status !== "M" && file.status !== "A") return;
|
|
484
|
-
if (!file.path.endsWith(".
|
|
496
|
+
if (!file.path.endsWith(".json") && !formatForPath(file.path)) return;
|
|
485
497
|
|
|
486
498
|
try {
|
|
487
499
|
const plat = getPlatform();
|
|
@@ -494,12 +506,10 @@ export function renderGitPanel(
|
|
|
494
506
|
plat.readFile(file.path),
|
|
495
507
|
]);
|
|
496
508
|
|
|
497
|
-
const isMarkdown = file.path.endsWith(".md");
|
|
498
509
|
const diffState = {
|
|
499
510
|
filePath: file.path,
|
|
500
511
|
originalContent,
|
|
501
512
|
currentContent,
|
|
502
|
-
isMarkdown,
|
|
503
513
|
fileStatus: file.status,
|
|
504
514
|
};
|
|
505
515
|
|
|
@@ -579,7 +589,7 @@ export function renderGitPanel(
|
|
|
579
589
|
for (const f of files) {
|
|
580
590
|
const parts = f.path.split("/");
|
|
581
591
|
let component;
|
|
582
|
-
if (f.path.endsWith(".json") || f.path.endsWith(".class.json") || f.path
|
|
592
|
+
if (f.path.endsWith(".json") || f.path.endsWith(".class.json") || formatForPath(f.path)) {
|
|
583
593
|
component = parts.length > 1 ? `/${parts[parts.length - 2]}` : `/${parts[0]}`;
|
|
584
594
|
} else {
|
|
585
595
|
component = "Other";
|
package/src/panels/head-panel.ts
CHANGED
|
@@ -74,7 +74,12 @@ const PAGE_FIELDS: MetaField[] = [
|
|
|
74
74
|
|
|
75
75
|
const OG_FIELDS: MetaField[] = [
|
|
76
76
|
{ label: "Title", attr: "property", key: "og:title" },
|
|
77
|
-
{
|
|
77
|
+
{
|
|
78
|
+
label: "Description",
|
|
79
|
+
attr: "property",
|
|
80
|
+
key: "og:description",
|
|
81
|
+
multiline: true,
|
|
82
|
+
},
|
|
78
83
|
{ label: "Image", attr: "property", key: "og:image", media: true },
|
|
79
84
|
{ label: "Type", attr: "property", key: "og:type" },
|
|
80
85
|
];
|
|
@@ -243,7 +248,9 @@ function renderMetaFieldRow(
|
|
|
243
248
|
const placeholder =
|
|
244
249
|
field.key === "viewport" ? "width=device-width, initial-scale=1" : `${field.label}…`;
|
|
245
250
|
const widget = field.multiline
|
|
246
|
-
? spTextArea(`head:${field.key}`, val, commit, {
|
|
251
|
+
? spTextArea(`head:${field.key}`, val, commit, {
|
|
252
|
+
placeholder: `${field.label}…`,
|
|
253
|
+
})
|
|
247
254
|
: spTextField(`head:${field.key}`, val, commit, { placeholder });
|
|
248
255
|
|
|
249
256
|
return renderFieldRow({
|
|
@@ -522,7 +529,7 @@ function renderFrontmatterSection() {
|
|
|
522
529
|
const fields = [];
|
|
523
530
|
if (schemaProps) {
|
|
524
531
|
for (const [field, fieldSchema] of Object.entries(
|
|
525
|
-
/** @type {Record<string, FmSchemaEntry>} */
|
|
532
|
+
/** @type {Record<string, FmSchemaEntry>} */ schemaProps,
|
|
526
533
|
)) {
|
|
527
534
|
if (RESERVED_FM_KEYS.has(field)) continue;
|
|
528
535
|
fields.push({ field, entry: fieldSchema, value: fm[field] as JsonValue });
|
|
@@ -532,7 +539,7 @@ function renderFrontmatterSection() {
|
|
|
532
539
|
fields.push({
|
|
533
540
|
field,
|
|
534
541
|
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
535
|
-
value: /** @type {JsonValue} */
|
|
542
|
+
value: /** @type {JsonValue} */ value,
|
|
536
543
|
});
|
|
537
544
|
}
|
|
538
545
|
} else {
|
|
@@ -541,7 +548,7 @@ function renderFrontmatterSection() {
|
|
|
541
548
|
fields.push({
|
|
542
549
|
field,
|
|
543
550
|
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
544
|
-
value: /** @type {JsonValue} */
|
|
551
|
+
value: /** @type {JsonValue} */ value,
|
|
545
552
|
});
|
|
546
553
|
}
|
|
547
554
|
}
|
package/src/panels/left-panel.ts
CHANGED
|
@@ -170,7 +170,7 @@ function _render() {
|
|
|
170
170
|
* content?: { frontmatter?: Record<string, unknown> };
|
|
171
171
|
* documentPath?: string;
|
|
172
172
|
* }}
|
|
173
|
-
*/
|
|
173
|
+
*/ {
|
|
174
174
|
ui: aTab.session.ui,
|
|
175
175
|
document: aTab.doc.document,
|
|
176
176
|
mode: aTab.doc.mode,
|
|
@@ -178,7 +178,7 @@ function _render() {
|
|
|
178
178
|
canvas: aTab.session.canvas,
|
|
179
179
|
content: aTab.doc.content,
|
|
180
180
|
documentPath: aTab.documentPath,
|
|
181
|
-
}
|
|
181
|
+
};
|
|
182
182
|
|
|
183
183
|
/** @type {TemplateResult | typeof nothing} */
|
|
184
184
|
let content;
|
|
@@ -226,12 +226,15 @@ function _render() {
|
|
|
226
226
|
const tab = activeTab.value!;
|
|
227
227
|
const fm = (tab.doc.content?.frontmatter ?? {}) as Record<string, unknown>;
|
|
228
228
|
const fmHead = fm.$head as unknown[] | undefined;
|
|
229
|
-
const tmp = {
|
|
229
|
+
const tmp = {
|
|
230
|
+
title: fm.title,
|
|
231
|
+
$head: fmHead ? [...fmHead] : undefined,
|
|
232
|
+
};
|
|
230
233
|
fn(tmp);
|
|
231
234
|
if (tmp.title !== fm.title)
|
|
232
235
|
mutateUpdateFrontmatter(tab, "title", tmp.title as JsonValue);
|
|
233
236
|
const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
|
|
234
|
-
mutateUpdateFrontmatter(tab, "$head", /** @type {JsonValue} */
|
|
237
|
+
mutateUpdateFrontmatter(tab, "$head", /** @type {JsonValue} */ newHead);
|
|
235
238
|
render();
|
|
236
239
|
}
|
|
237
240
|
: (fn: (doc: object) => void) => {
|