@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.
@@ -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
+ }