@jxsuite/studio 0.27.0 → 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 +33900 -56705
- package/dist/studio.js.map +128 -396
- package/package.json +4 -10
- 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 +37 -8
- package/src/panels/data-explorer.ts +9 -2
- 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 +4 -1
- 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/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
|
@@ -47,7 +47,12 @@ export function registerPanelDnD(panel: CanvasPanel) {
|
|
|
47
47
|
// driven from the monitor using only the innermost target.
|
|
48
48
|
/** Innermost drop target if it belongs to this panel's canvas, else null */
|
|
49
49
|
const innermostCanvasTarget = (location: {
|
|
50
|
-
current: {
|
|
50
|
+
current: {
|
|
51
|
+
dropTargets: {
|
|
52
|
+
data: Record<string | symbol, unknown>;
|
|
53
|
+
element: Element;
|
|
54
|
+
}[];
|
|
55
|
+
};
|
|
51
56
|
}) => {
|
|
52
57
|
const target = location.current.dropTargets[0];
|
|
53
58
|
if (!target) return null;
|
|
@@ -122,7 +127,7 @@ export function registerPanelDnD(panel: CanvasPanel) {
|
|
|
122
127
|
const isLeaf = VOID_ELEMENTS.has(tag) || !hasElementChildren;
|
|
123
128
|
|
|
124
129
|
const cleanup = dropTargetForElements({
|
|
125
|
-
element: /** @type {HTMLElement} */
|
|
130
|
+
element: /** @type {HTMLElement} */ el,
|
|
126
131
|
canDrop({ source }) {
|
|
127
132
|
const srcPath = source.data.path as JxPath | undefined;
|
|
128
133
|
if (srcPath && isAncestor(srcPath, elPath)) return false;
|
|
@@ -144,13 +149,21 @@ export function registerPanelDnD(panel: CanvasPanel) {
|
|
|
144
149
|
*/
|
|
145
150
|
function getCanvasDropResult(el: HTMLElement, elPath: JxPath, isLeaf: boolean): DropResult {
|
|
146
151
|
if (!view.lastDragInput)
|
|
147
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
instruction: { type: "make-child" },
|
|
154
|
+
referenceEl: el,
|
|
155
|
+
targetPath: elPath,
|
|
156
|
+
};
|
|
148
157
|
const y = view.lastDragInput.clientY;
|
|
149
158
|
|
|
150
159
|
if (elPath.length === 0) {
|
|
151
160
|
const children = Array.from(el.children) as HTMLElement[];
|
|
152
161
|
if (children.length === 0)
|
|
153
|
-
return {
|
|
162
|
+
return {
|
|
163
|
+
instruction: { type: "make-child" },
|
|
164
|
+
referenceEl: el,
|
|
165
|
+
targetPath: elPath,
|
|
166
|
+
};
|
|
154
167
|
return nearestChildEdge(children, y, elPath);
|
|
155
168
|
}
|
|
156
169
|
|
|
@@ -164,10 +177,22 @@ function getCanvasDropResult(el: HTMLElement, elPath: JxPath, isLeaf: boolean):
|
|
|
164
177
|
}
|
|
165
178
|
|
|
166
179
|
if (relY < 0.25)
|
|
167
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
instruction: { type: "reorder-above" },
|
|
182
|
+
referenceEl: el,
|
|
183
|
+
targetPath: elPath,
|
|
184
|
+
};
|
|
168
185
|
if (relY > 0.75)
|
|
169
|
-
return {
|
|
170
|
-
|
|
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
|
+
};
|
|
171
196
|
}
|
|
172
197
|
|
|
173
198
|
/**
|
|
@@ -202,7 +227,11 @@ function nearestChildEdge(children: HTMLElement[], cursorY: number, parentPath:
|
|
|
202
227
|
}
|
|
203
228
|
|
|
204
229
|
const childPath = [...parentPath, "children", closestIdx];
|
|
205
|
-
return {
|
|
230
|
+
return {
|
|
231
|
+
instruction,
|
|
232
|
+
referenceEl: children[closestIdx],
|
|
233
|
+
targetPath: childPath,
|
|
234
|
+
};
|
|
206
235
|
}
|
|
207
236
|
|
|
208
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/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) => {
|
|
@@ -211,7 +211,9 @@ export function registerPanelEvents(panel: CanvasPanel) {
|
|
|
211
211
|
let path = elToPath.get(el);
|
|
212
212
|
if (path) {
|
|
213
213
|
path = bubbleInlinePath(tab?.doc.document, path);
|
|
214
|
-
showContextMenu(e, path, {
|
|
214
|
+
showContextMenu(e, path, {
|
|
215
|
+
onEditComponent: ctx.navigateToComponent,
|
|
216
|
+
});
|
|
215
217
|
return;
|
|
216
218
|
}
|
|
217
219
|
}
|
|
@@ -490,12 +490,15 @@ function renderComponentPropsFieldsTemplate(
|
|
|
490
490
|
}}
|
|
491
491
|
></sp-number-field>`;
|
|
492
492
|
} else if (parsed.kind === "combobox") {
|
|
493
|
-
const options = /** @type {{ options?: string[] }} */
|
|
493
|
+
const options = /** @type {{ options?: string[] }} */ parsed.options as string[];
|
|
494
494
|
widgetTpl = html`<jx-value-selector
|
|
495
495
|
.value=${String(staticVal)}
|
|
496
496
|
size="s"
|
|
497
497
|
placeholder="—"
|
|
498
|
-
.options=${options.map((o) => ({
|
|
498
|
+
.options=${options.map((o) => ({
|
|
499
|
+
value: o,
|
|
500
|
+
label: camelToLabel(o),
|
|
501
|
+
}))}
|
|
499
502
|
@change=${(e: Event & { detail?: { value?: string } }) =>
|
|
500
503
|
onChange(e.detail?.value ?? (e.target as HTMLInputElement).value)}
|
|
501
504
|
></jx-value-selector>`;
|
|
@@ -849,7 +852,10 @@ function renderPageSection(node: JxMutableNode) {
|
|
|
849
852
|
// ─── Layout selection panel ─────────────────────────────────────────────────
|
|
850
853
|
|
|
851
854
|
function renderLayoutSelectionPanel(ctx: { navigateToComponent: (path: string) => void }) {
|
|
852
|
-
const { el, layoutPath } = view.layoutSelection as {
|
|
855
|
+
const { el, layoutPath } = view.layoutSelection as {
|
|
856
|
+
el: HTMLElement;
|
|
857
|
+
layoutPath: string;
|
|
858
|
+
};
|
|
853
859
|
const tagName = el?.tagName?.toLowerCase() || "element";
|
|
854
860
|
const className = el?.className || "";
|
|
855
861
|
const displayPath = layoutPath || "layout";
|
|
@@ -4,6 +4,7 @@ import { classMap } from "lit-html/directives/class-map.js";
|
|
|
4
4
|
import { live } from "lit-html/directives/live.js";
|
|
5
5
|
import { ref } from "lit-html/directives/ref.js";
|
|
6
6
|
import { getPlatform } from "../platform";
|
|
7
|
+
import { loadFormats, documentExtensions, formatByExtension } from "../format/format-host";
|
|
7
8
|
import { openFileInTab } from "../files/files";
|
|
8
9
|
import { getRecentFiles, trackRecentFile } from "../recent-projects";
|
|
9
10
|
import { getLayerSlot } from "../ui/layers";
|
|
@@ -46,7 +47,8 @@ async function doSearch(query: string) {
|
|
|
46
47
|
}
|
|
47
48
|
try {
|
|
48
49
|
const platform = getPlatform();
|
|
49
|
-
|
|
50
|
+
await loadFormats();
|
|
51
|
+
_results = await platform.searchFiles(query.trim().toLowerCase(), documentExtensions());
|
|
50
52
|
_selectedIndex = 0;
|
|
51
53
|
renderOverlay();
|
|
52
54
|
} catch {
|
|
@@ -95,14 +97,9 @@ function selectItem(item: { path: string; name?: string }) {
|
|
|
95
97
|
|
|
96
98
|
function fileIcon(name: string) {
|
|
97
99
|
const ext = name.split(".").pop()?.toLowerCase();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
case "md":
|
|
102
|
-
return html`<sp-icon-file-txt size="s"></sp-icon-file-txt>`;
|
|
103
|
-
default:
|
|
104
|
-
return html`<sp-icon-document size="s"></sp-icon-document>`;
|
|
105
|
-
}
|
|
100
|
+
if (ext === "json") return html`<sp-icon-file-code size="s"></sp-icon-file-code>`;
|
|
101
|
+
if (ext && formatByExtension(ext)) return html`<sp-icon-file-txt size="s"></sp-icon-file-txt>`;
|
|
102
|
+
return html`<sp-icon-document size="s"></sp-icon-document>`;
|
|
106
103
|
}
|
|
107
104
|
|
|
108
105
|
function dirPart(path: string) {
|
|
@@ -149,7 +146,10 @@ function renderOverlay() {
|
|
|
149
146
|
${items.map(
|
|
150
147
|
(item, i) => html`
|
|
151
148
|
<div
|
|
152
|
-
class=${classMap({
|
|
149
|
+
class=${classMap({
|
|
150
|
+
"quick-search-item": true,
|
|
151
|
+
selected: i === _selectedIndex,
|
|
152
|
+
})}
|
|
153
153
|
@click=${() => selectItem(item)}
|
|
154
154
|
@mouseenter=${() => {
|
|
155
155
|
_selectedIndex = i;
|
|
@@ -189,12 +189,16 @@ function _doRender() {
|
|
|
189
189
|
// Only render the active panel's content
|
|
190
190
|
if (tab === "properties") {
|
|
191
191
|
litRender(
|
|
192
|
-
renderPropertiesPanelTemplate({
|
|
192
|
+
renderPropertiesPanelTemplate({
|
|
193
|
+
navigateToComponent: ctx.navigateToComponent,
|
|
194
|
+
}),
|
|
193
195
|
_propsContainer!,
|
|
194
196
|
);
|
|
195
197
|
} else if (tab === "events") {
|
|
196
198
|
litRender(
|
|
197
|
-
eventsSidebarTemplate({
|
|
199
|
+
eventsSidebarTemplate({
|
|
200
|
+
isCustomElementDoc: () => isCustomElementDoc(S),
|
|
201
|
+
}),
|
|
198
202
|
_eventsContainer!,
|
|
199
203
|
);
|
|
200
204
|
} else if (tab === "style") {
|