@jxsuite/studio 0.0.1
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.css +3676 -0
- package/dist/studio.js +188743 -0
- package/dist/studio.js.map +1448 -0
- package/package.json +67 -0
- package/src/editor/context-menu.js +144 -0
- package/src/editor/inline-edit.js +597 -0
- package/src/editor/inline-format.js +572 -0
- package/src/editor/shortcuts.js +275 -0
- package/src/editor/slash-menu.js +167 -0
- package/src/files/components.js +40 -0
- package/src/files/file-ops.js +195 -0
- package/src/files/files.js +569 -0
- package/src/markdown/md-allowlist.js +101 -0
- package/src/markdown/md-convert.js +491 -0
- package/src/panels/activity-bar.js +69 -0
- package/src/panels/data-explorer.js +181 -0
- package/src/panels/events-panel.js +235 -0
- package/src/panels/imports-panel.js +427 -0
- package/src/panels/signals-panel.js +1093 -0
- package/src/panels/statusbar.js +56 -0
- package/src/platform.js +31 -0
- package/src/platforms/devserver.js +293 -0
- package/src/services/cem-export.js +130 -0
- package/src/services/code-services.js +98 -0
- package/src/site-context.js +122 -0
- package/src/state.js +744 -0
- package/src/store.js +332 -0
- package/src/studio.js +7692 -0
- package/src/ui/icons.js +83 -0
- package/src/ui/jx-styled-combobox.js +142 -0
- package/src/ui/spectrum.js +238 -0
- package/src/utils/studio-utils.js +185 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// ─── Data Explorer ──────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
import { html, nothing } from "lit-html";
|
|
4
|
+
|
|
5
|
+
/** Expanded data entries set — persists across renders. */
|
|
6
|
+
const expandedDataKeys = new Set();
|
|
7
|
+
|
|
8
|
+
/** Unwrap a Vue ref (has .value and .__v_isRef) to get the underlying value. */
|
|
9
|
+
function unwrapSignal(/** @type {any} */ value) {
|
|
10
|
+
if (value && typeof value === "object" && value.__v_isRef) return value.value;
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Type label for a signal value in the data explorer. */
|
|
15
|
+
function dataTypeLabel(/** @type {any} */ value) {
|
|
16
|
+
const v = unwrapSignal(value);
|
|
17
|
+
if (v === null) return "null";
|
|
18
|
+
if (v === undefined) return "pending";
|
|
19
|
+
if (Array.isArray(v)) return `Array(${v.length})`;
|
|
20
|
+
if (typeof v === "object") return `{${Object.keys(v).length}}`;
|
|
21
|
+
return typeof v;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render the data explorer tab showing live resolved values.
|
|
26
|
+
*
|
|
27
|
+
* @param {Record<string, any>} state - S.document.state (the $defs definitions)
|
|
28
|
+
* @param {Record<string, any> | null} liveScope - Cached live scope from runtime rendering
|
|
29
|
+
* @param {{
|
|
30
|
+
* renderCanvas: () => void;
|
|
31
|
+
* renderLeftPanel: () => void;
|
|
32
|
+
* defCategory: (def: any) => string;
|
|
33
|
+
* defBadgeLabel: (def: any) => string;
|
|
34
|
+
* }} callbacks
|
|
35
|
+
* @returns {any}
|
|
36
|
+
*/
|
|
37
|
+
export function renderDataExplorerTemplate(state, liveScope, callbacks) {
|
|
38
|
+
const { renderCanvas, renderLeftPanel, defCategory, defBadgeLabel } = callbacks;
|
|
39
|
+
|
|
40
|
+
if (!liveScope) {
|
|
41
|
+
return html`<div class="empty-state">No live data — render the document in preview mode</div>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const defs = state || {};
|
|
45
|
+
const entries = Object.entries(defs);
|
|
46
|
+
|
|
47
|
+
return html`
|
|
48
|
+
<div class="data-explorer-toolbar">
|
|
49
|
+
<sp-action-button
|
|
50
|
+
quiet
|
|
51
|
+
size="s"
|
|
52
|
+
class="data-refresh-btn"
|
|
53
|
+
@click=${() => {
|
|
54
|
+
renderCanvas();
|
|
55
|
+
setTimeout(() => renderLeftPanel(), 200);
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<sp-icon-refresh slot="icon"></sp-icon-refresh>
|
|
59
|
+
Refresh
|
|
60
|
+
</sp-action-button>
|
|
61
|
+
</div>
|
|
62
|
+
${entries.length === 0
|
|
63
|
+
? html`<div class="empty-state">No state defined</div>`
|
|
64
|
+
: entries.map(([name, def]) => {
|
|
65
|
+
const value = liveScope[name];
|
|
66
|
+
const unwrapped = unwrapSignal(value);
|
|
67
|
+
const isExpanded = expandedDataKeys.has(name);
|
|
68
|
+
return html`
|
|
69
|
+
<div class="data-row">
|
|
70
|
+
<div
|
|
71
|
+
class="data-row-header${isExpanded ? " expanded" : ""}"
|
|
72
|
+
@click=${() => {
|
|
73
|
+
if (expandedDataKeys.has(name)) expandedDataKeys.delete(name);
|
|
74
|
+
else expandedDataKeys.add(name);
|
|
75
|
+
renderLeftPanel();
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<span class="signal-badge ${defCategory(def)}">${defBadgeLabel(def)}</span>
|
|
79
|
+
<span class="data-name">${name}</span>
|
|
80
|
+
<span class="data-type${unwrapped === null ? " data-pending" : ""}"
|
|
81
|
+
>${dataTypeLabel(value)}</span
|
|
82
|
+
>
|
|
83
|
+
</div>
|
|
84
|
+
${isExpanded
|
|
85
|
+
? html`<div class="data-tree">${renderDataTreeTemplate(unwrapped, 0)}</div>`
|
|
86
|
+
: nothing}
|
|
87
|
+
</div>
|
|
88
|
+
`;
|
|
89
|
+
})}
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Recursively render a JSON value as a tree view (Lit template).
|
|
95
|
+
*
|
|
96
|
+
* @returns {any}
|
|
97
|
+
*/
|
|
98
|
+
export function renderDataTreeTemplate(
|
|
99
|
+
/** @type {any} */ value,
|
|
100
|
+
/** @type {any} */ depth,
|
|
101
|
+
maxDepth = 5,
|
|
102
|
+
) {
|
|
103
|
+
const indent = `${(depth + 1) * 12}px`;
|
|
104
|
+
|
|
105
|
+
if (depth > maxDepth) {
|
|
106
|
+
return html`<div class="data-leaf data-ellipsis" style="padding-left:${indent}">…</div>`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (value === null || value === undefined) {
|
|
110
|
+
return html`<div class="data-leaf data-null" style="padding-left:${indent}">
|
|
111
|
+
${String(value)}
|
|
112
|
+
</div>`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (typeof value !== "object") {
|
|
116
|
+
const text =
|
|
117
|
+
typeof value === "string" && value.length > 200
|
|
118
|
+
? `"${value.slice(0, 200)}\u2026"`
|
|
119
|
+
: JSON.stringify(value);
|
|
120
|
+
return html`<div class="data-leaf data-${typeof value}" style="padding-left:${indent}">
|
|
121
|
+
${text}
|
|
122
|
+
</div>`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
const cap = 20;
|
|
127
|
+
const items = value.slice(0, cap).map((item, i) => {
|
|
128
|
+
if (item === null || item === undefined || typeof item !== "object") {
|
|
129
|
+
const valText =
|
|
130
|
+
typeof item === "string" && item.length > 80
|
|
131
|
+
? `"${item.slice(0, 80)}\u2026"`
|
|
132
|
+
: JSON.stringify(item);
|
|
133
|
+
return html`<div class="data-branch" style="padding-left:${indent}">
|
|
134
|
+
<span class="data-key">[${i}] </span
|
|
135
|
+
><span class="data-value data-${item === null ? "null" : typeof item}">${valText}</span>
|
|
136
|
+
</div>`;
|
|
137
|
+
}
|
|
138
|
+
const label = Array.isArray(item) ? `Array(${item.length})` : `{${Object.keys(item).length}}`;
|
|
139
|
+
return html`
|
|
140
|
+
<div class="data-branch" style="padding-left:${indent}">
|
|
141
|
+
<span class="data-key">[${i}] </span
|
|
142
|
+
><span class="data-value data-object-label">${label}</span>
|
|
143
|
+
</div>
|
|
144
|
+
${renderDataTreeTemplate(item, depth + 1, maxDepth)}
|
|
145
|
+
`;
|
|
146
|
+
});
|
|
147
|
+
return html`${items}${value.length > cap
|
|
148
|
+
? html`<div class="data-leaf data-ellipsis" style="padding-left:${indent}">
|
|
149
|
+
… ${value.length - cap} more
|
|
150
|
+
</div>`
|
|
151
|
+
: nothing}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Object
|
|
155
|
+
const keys = Object.keys(value);
|
|
156
|
+
const cap = 30;
|
|
157
|
+
const items = keys.slice(0, cap).map((key) => {
|
|
158
|
+
const v = value[key];
|
|
159
|
+
if (v === null || v === undefined || typeof v !== "object") {
|
|
160
|
+
const valText =
|
|
161
|
+
typeof v === "string" && v.length > 80 ? `"${v.slice(0, 80)}\u2026"` : JSON.stringify(v);
|
|
162
|
+
return html`<div class="data-branch" style="padding-left:${indent}">
|
|
163
|
+
<span class="data-key">${key}: </span
|
|
164
|
+
><span class="data-value data-${v === null ? "null" : typeof v}">${valText}</span>
|
|
165
|
+
</div>`;
|
|
166
|
+
}
|
|
167
|
+
const label = Array.isArray(v) ? `Array(${v.length})` : `{${Object.keys(v).length}}`;
|
|
168
|
+
return html`
|
|
169
|
+
<div class="data-branch" style="padding-left:${indent}">
|
|
170
|
+
<span class="data-key">${key}: </span
|
|
171
|
+
><span class="data-value data-object-label">${label}</span>
|
|
172
|
+
</div>
|
|
173
|
+
${renderDataTreeTemplate(v, depth + 1, maxDepth)}
|
|
174
|
+
`;
|
|
175
|
+
});
|
|
176
|
+
return html`${items}${keys.length > cap
|
|
177
|
+
? html`<div class="data-leaf data-ellipsis" style="padding-left:${indent}">
|
|
178
|
+
… ${keys.length - cap} more
|
|
179
|
+
</div>`
|
|
180
|
+
: nothing}`;
|
|
181
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { getNodeAtPath, updateProperty, update } from "../store.js";
|
|
2
|
+
import { html, nothing } from "lit-html";
|
|
3
|
+
import { live } from "lit-html/directives/live.js";
|
|
4
|
+
|
|
5
|
+
export const EVENT_NAMES = [
|
|
6
|
+
"onclick",
|
|
7
|
+
"oninput",
|
|
8
|
+
"onchange",
|
|
9
|
+
"onsubmit",
|
|
10
|
+
"onkeydown",
|
|
11
|
+
"onkeyup",
|
|
12
|
+
"onfocus",
|
|
13
|
+
"onblur",
|
|
14
|
+
"onmouseenter",
|
|
15
|
+
"onmouseleave",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {any} S - Studio state
|
|
20
|
+
* @param {{ isCustomElementDoc: () => boolean; renderCanvas: () => void }} helpers
|
|
21
|
+
*/
|
|
22
|
+
export function eventsSidebarTemplate(S, helpers) {
|
|
23
|
+
const { isCustomElementDoc, renderCanvas } = helpers;
|
|
24
|
+
if (!S.selection) return html`<div class="empty-state">Select an element to edit events</div>`;
|
|
25
|
+
const node = getNodeAtPath(S.document, S.selection);
|
|
26
|
+
if (!node) return html`<div class="empty-state">Node not found</div>`;
|
|
27
|
+
|
|
28
|
+
const defs = S.document.state || {};
|
|
29
|
+
const functionDefs = Object.entries(defs).filter(
|
|
30
|
+
([, d]) => d.$prototype === "Function" || d.$handler,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Declared CEM events (custom element docs)
|
|
34
|
+
/** @type {any} */
|
|
35
|
+
let declaredEventsT = nothing;
|
|
36
|
+
if (isCustomElementDoc()) {
|
|
37
|
+
const allEmits = [];
|
|
38
|
+
for (const [fnName, d] of Object.entries(defs)) {
|
|
39
|
+
if (Array.isArray(d.emits)) {
|
|
40
|
+
for (const ev of d.emits) allEmits.push({ ...ev, _fn: fnName });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (allEmits.length > 0) {
|
|
44
|
+
declaredEventsT = html`
|
|
45
|
+
<div class="events-section">
|
|
46
|
+
<sp-field-label size="s">Declared Events</sp-field-label>
|
|
47
|
+
${allEmits.map(
|
|
48
|
+
(ev) => html`
|
|
49
|
+
<div class="declared-event-row" title=${ev.description || ""}>
|
|
50
|
+
<code class="event-code">${ev.name || "(unnamed)"}</code>
|
|
51
|
+
<span class="event-source">← ${ev._fn}</span>
|
|
52
|
+
${ev.type?.text ? html`<span class="event-type">${ev.type.text}</span>` : nothing}
|
|
53
|
+
</div>
|
|
54
|
+
`,
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
<sp-divider size="s"></sp-divider>
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Find existing event bindings
|
|
63
|
+
const eventKeys = Object.keys(node).filter((k) => {
|
|
64
|
+
if (!k.startsWith("on")) return false;
|
|
65
|
+
const v = node[k];
|
|
66
|
+
if (!v || typeof v !== "object") return false;
|
|
67
|
+
return v.$ref || v.$prototype === "Function";
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return html`
|
|
71
|
+
<div class="events-panel">
|
|
72
|
+
${declaredEventsT}
|
|
73
|
+
<div class="events-section">
|
|
74
|
+
${eventKeys.length > 0
|
|
75
|
+
? html` <sp-field-label size="s">Event Bindings</sp-field-label> `
|
|
76
|
+
: nothing}
|
|
77
|
+
${eventKeys.map((evKey) => {
|
|
78
|
+
const evVal = node[evKey];
|
|
79
|
+
const isInline = evVal.$prototype === "Function";
|
|
80
|
+
return html`
|
|
81
|
+
<div class="event-binding">
|
|
82
|
+
<div class="event-row">
|
|
83
|
+
<sp-picker
|
|
84
|
+
size="s"
|
|
85
|
+
class="event-name"
|
|
86
|
+
.value=${live(evKey)}
|
|
87
|
+
@change=${(/** @type {any} */ e) => {
|
|
88
|
+
const newKey = e.target.value;
|
|
89
|
+
if (newKey && newKey !== evKey) {
|
|
90
|
+
let s = updateProperty(S, S.selection, evKey, undefined);
|
|
91
|
+
s = updateProperty(s, S.selection, newKey, node[evKey]);
|
|
92
|
+
update(s);
|
|
93
|
+
}
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
${[evKey, ...EVENT_NAMES.filter((n) => n !== evKey)].map(
|
|
97
|
+
(n) => html`<sp-menu-item value=${n}>${n}</sp-menu-item>`,
|
|
98
|
+
)}
|
|
99
|
+
</sp-picker>
|
|
100
|
+
<sp-picker
|
|
101
|
+
size="s"
|
|
102
|
+
class="event-mode"
|
|
103
|
+
.value=${live(isInline ? "inline" : "ref")}
|
|
104
|
+
@change=${(/** @type {any} */ e) => {
|
|
105
|
+
if (e.target.value === "inline") {
|
|
106
|
+
update(
|
|
107
|
+
updateProperty(S, S.selection, evKey, {
|
|
108
|
+
$prototype: "Function",
|
|
109
|
+
body: "",
|
|
110
|
+
parameters: [],
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
const firstFn = functionDefs[0];
|
|
115
|
+
update(
|
|
116
|
+
updateProperty(
|
|
117
|
+
S,
|
|
118
|
+
S.selection,
|
|
119
|
+
evKey,
|
|
120
|
+
firstFn ? { $ref: `#/state/${firstFn[0]}` } : { $ref: "" },
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
<sp-menu-item value="inline">inline</sp-menu-item>
|
|
127
|
+
<sp-menu-item value="ref">$ref</sp-menu-item>
|
|
128
|
+
</sp-picker>
|
|
129
|
+
<sp-action-button
|
|
130
|
+
size="xs"
|
|
131
|
+
quiet
|
|
132
|
+
@click=${() => update(updateProperty(S, S.selection, evKey, undefined))}
|
|
133
|
+
>
|
|
134
|
+
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
135
|
+
</sp-action-button>
|
|
136
|
+
</div>
|
|
137
|
+
${isInline
|
|
138
|
+
? html`
|
|
139
|
+
<div class="event-body-row">
|
|
140
|
+
<sp-textfield
|
|
141
|
+
size="s"
|
|
142
|
+
multiline
|
|
143
|
+
grows
|
|
144
|
+
placeholder="// handler body"
|
|
145
|
+
.value=${live(evVal.body || "")}
|
|
146
|
+
@input=${(/** @type {any} */ e) => {
|
|
147
|
+
update(
|
|
148
|
+
updateProperty(S, S.selection, evKey, {
|
|
149
|
+
$prototype: "Function",
|
|
150
|
+
body: e.target.value,
|
|
151
|
+
parameters: evVal.parameters || [],
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
</sp-textfield>
|
|
157
|
+
<sp-action-button
|
|
158
|
+
size="xs"
|
|
159
|
+
quiet
|
|
160
|
+
title="Open in editor"
|
|
161
|
+
@click=${() => {
|
|
162
|
+
S = {
|
|
163
|
+
...S,
|
|
164
|
+
ui: {
|
|
165
|
+
...S.ui,
|
|
166
|
+
editingFunction: {
|
|
167
|
+
type: "event",
|
|
168
|
+
path: S.selection,
|
|
169
|
+
eventKey: evKey,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
renderCanvas();
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
<sp-icon-code slot="icon"></sp-icon-code>
|
|
177
|
+
</sp-action-button>
|
|
178
|
+
</div>
|
|
179
|
+
`
|
|
180
|
+
: html`
|
|
181
|
+
<sp-picker
|
|
182
|
+
size="s"
|
|
183
|
+
class="event-handler"
|
|
184
|
+
.value=${live(evVal.$ref || "__none__")}
|
|
185
|
+
@change=${(/** @type {any} */ e) => {
|
|
186
|
+
if (e.target.value && e.target.value !== "__none__") {
|
|
187
|
+
update(updateProperty(S, S.selection, evKey, { $ref: e.target.value }));
|
|
188
|
+
} else {
|
|
189
|
+
update(updateProperty(S, S.selection, evKey, undefined));
|
|
190
|
+
}
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
<sp-menu-item value="__none__">— none —</sp-menu-item>
|
|
194
|
+
${functionDefs.map(
|
|
195
|
+
([fName]) =>
|
|
196
|
+
html`<sp-menu-item value=${`#/state/${fName}`}>${fName}</sp-menu-item>`,
|
|
197
|
+
)}
|
|
198
|
+
</sp-picker>
|
|
199
|
+
`}
|
|
200
|
+
</div>
|
|
201
|
+
`;
|
|
202
|
+
})}
|
|
203
|
+
<sp-action-button
|
|
204
|
+
size="s"
|
|
205
|
+
quiet
|
|
206
|
+
@click=${() => {
|
|
207
|
+
let evName = "onclick";
|
|
208
|
+
for (const name of EVENT_NAMES) {
|
|
209
|
+
if (!node[name]) {
|
|
210
|
+
evName = name;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (functionDefs.length > 0) {
|
|
215
|
+
update(
|
|
216
|
+
updateProperty(S, S.selection, evName, { $ref: `#/state/${functionDefs[0][0]}` }),
|
|
217
|
+
);
|
|
218
|
+
} else {
|
|
219
|
+
update(
|
|
220
|
+
updateProperty(S, S.selection, evName, {
|
|
221
|
+
$prototype: "Function",
|
|
222
|
+
body: "",
|
|
223
|
+
parameters: [],
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
<sp-icon-add slot="icon"></sp-icon-add>
|
|
230
|
+
Add Event
|
|
231
|
+
</sp-action-button>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
`;
|
|
235
|
+
}
|