@jxsuite/studio 0.19.0 → 0.21.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.css +98 -98
- package/dist/studio.js +13476 -12330
- package/dist/studio.js.map +152 -148
- package/dist/workers/editor.worker.js +79 -79
- package/dist/workers/json.worker.js +109 -109
- package/dist/workers/ts.worker.js +82 -82
- package/package.json +19 -7
- package/src/browse/browse-modal.js +16 -13
- package/src/browse/browse.js +44 -35
- package/src/canvas/canvas-diff.js +24 -16
- package/src/canvas/canvas-helpers.js +24 -19
- package/src/canvas/canvas-live-render.js +113 -39
- package/src/canvas/canvas-render.js +119 -73
- package/src/canvas/canvas-utils.js +51 -21
- package/src/canvas/nested-site-style.js +50 -0
- package/src/editor/component-inline-edit.js +14 -13
- package/src/editor/content-inline-edit.js +22 -10
- package/src/editor/context-menu.js +28 -14
- package/src/editor/convert-targets.js +60 -0
- package/src/editor/convert-to-component.js +20 -19
- package/src/editor/convert-to-repeater.js +226 -0
- package/src/editor/inline-edit.js +69 -28
- package/src/editor/insertion-helper.js +16 -12
- package/src/editor/shortcuts.js +14 -4
- package/src/editor/slash-menu.js +40 -19
- package/src/files/components.js +1 -1
- package/src/files/file-ops.js +37 -16
- package/src/files/files.js +322 -68
- package/src/github/github-auth.js +122 -0
- package/src/github/github-publish.js +126 -0
- package/src/markdown/md-convert.js +172 -107
- package/src/new-project/new-project-modal.js +204 -0
- package/src/panels/activity-bar.js +27 -25
- package/src/panels/ai-panel.js +406 -0
- package/src/panels/block-action-bar.js +77 -28
- package/src/panels/canvas-dnd.js +8 -2
- package/src/panels/data-explorer.js +19 -13
- package/src/panels/dnd.js +144 -92
- package/src/panels/editors.js +55 -35
- package/src/panels/elements-panel.js +33 -19
- package/src/panels/events-panel.js +15 -10
- package/src/panels/git-panel.js +475 -99
- package/src/panels/head-panel.js +121 -81
- package/src/panels/imports-panel.js +94 -65
- package/src/panels/layers-panel.js +64 -43
- package/src/panels/left-panel.js +105 -48
- package/src/panels/overlays.js +1 -0
- package/src/panels/panel-events.js +45 -30
- package/src/panels/preview-render.js +23 -18
- package/src/panels/properties-panel.js +378 -257
- package/src/panels/pseudo-preview.js +2 -2
- package/src/panels/quick-search.js +11 -9
- package/src/panels/right-panel.js +42 -16
- package/src/panels/shared.js +4 -4
- package/src/panels/signals-panel.js +415 -166
- package/src/panels/statusbar.js +10 -6
- package/src/panels/style-inputs.js +67 -35
- package/src/panels/style-panel.js +239 -134
- package/src/panels/style-utils.js +34 -16
- package/src/panels/stylebook-layers-panel.js +12 -10
- package/src/panels/stylebook-panel.js +134 -66
- package/src/panels/tab-strip.js +5 -2
- package/src/panels/toolbar.js +333 -109
- package/src/panels/welcome-screen.js +121 -0
- package/src/platform.js +2 -4
- package/src/platforms/devserver.js +113 -7
- package/src/resize-edges.js +98 -0
- package/src/services/cem-export.js +12 -7
- package/src/services/code-services.js +30 -12
- package/src/settings/content-types-editor.js +8 -8
- package/src/settings/css-vars-editor.js +30 -24
- package/src/settings/defs-editor.js +12 -7
- package/src/settings/general-settings.js +85 -3
- package/src/settings/head-editor.js +30 -24
- package/src/settings/schema-field-ui.js +58 -39
- package/src/settings/settings-modal.js +12 -9
- package/src/site-context.js +41 -31
- package/src/state.js +70 -29
- package/src/store.js +36 -35
- package/src/studio.js +120 -90
- package/src/tabs/tab.js +67 -43
- package/src/tabs/transact.js +23 -13
- package/src/ui/button-group.js +24 -24
- package/src/ui/color-selector.js +28 -23
- package/src/ui/field-row.js +3 -3
- package/src/ui/icons.js +2 -2
- package/src/ui/layers.js +31 -1
- package/src/ui/media-picker.js +7 -7
- package/src/ui/spectrum.js +5 -1
- package/src/ui/unit-selector.js +18 -16
- package/src/ui/value-selector.js +21 -15
- package/src/ui/widgets.js +22 -19
- package/src/utils/canvas-media.js +5 -4
- package/src/utils/edit-display.js +32 -26
- package/src/utils/google-fonts.js +11 -11
- package/src/utils/inherited-style.js +9 -8
- package/src/utils/studio-utils.js +18 -18
- package/src/view.js +58 -1
- package/src/workspace/workspace.js +29 -7
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ai-panel.js — Claude AI Assistant tab for the right panel
|
|
3
|
+
*
|
|
4
|
+
* Uses QuikChat for the chat UI with Claude Agent SDK streaming via SSE.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { html, nothing } from "lit-html";
|
|
8
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
9
|
+
import quikchat from "quikchat/md";
|
|
10
|
+
import { getPlatform } from "../platform.js";
|
|
11
|
+
import { reloadFileInTab } from "../files/files.js";
|
|
12
|
+
|
|
13
|
+
// ─── State (module-level, persists across tab switches) ─────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @type {{
|
|
17
|
+
* role: string;
|
|
18
|
+
* content: string;
|
|
19
|
+
* toolUse?: { tool: string; input?: Record<string, unknown> };
|
|
20
|
+
* }[]}
|
|
21
|
+
*/
|
|
22
|
+
let messages = [];
|
|
23
|
+
let streaming = false;
|
|
24
|
+
let sessionId = /** @type {string | null} */ (null);
|
|
25
|
+
let authStatus = /** @type {"authenticated" | "unauthenticated" | "checking" | "unknown"} */ (
|
|
26
|
+
"unknown"
|
|
27
|
+
);
|
|
28
|
+
let authError = "";
|
|
29
|
+
let currentAssistantText = "";
|
|
30
|
+
let eventSource = /** @type {EventSource | null} */ (null);
|
|
31
|
+
let mounted = false;
|
|
32
|
+
|
|
33
|
+
/** @type {any} */
|
|
34
|
+
let chatInstance = null;
|
|
35
|
+
/** @type {Element | null} */
|
|
36
|
+
let chatContainerEl = null;
|
|
37
|
+
/** @type {HTMLElement | null} */
|
|
38
|
+
let _quikChatEl = null;
|
|
39
|
+
let currentStreamMsgId = /** @type {number | null} */ (null);
|
|
40
|
+
let streamStarted = false;
|
|
41
|
+
/** @type {Set<string>} */
|
|
42
|
+
let pendingFileReloads = new Set();
|
|
43
|
+
|
|
44
|
+
// ─── Lifecycle ──────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export function mountAiPanel() {
|
|
47
|
+
if (mounted) return;
|
|
48
|
+
mounted = true;
|
|
49
|
+
checkAuth();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function rerenderPanel() {
|
|
53
|
+
const { render } = /** @type {any} */ (/** @type {any} */ (globalThis).__jxRightPanelRender) ||
|
|
54
|
+
{};
|
|
55
|
+
if (render) render();
|
|
56
|
+
requestAnimationFrame(() => mountQuikChat());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function registerRightPanelRender(/** @type {Function} */ fn) {
|
|
60
|
+
/** @type {any} */ (globalThis).__jxRightPanelRender = { render: fn };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── QuikChat Mount ────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export function mountQuikChat() {
|
|
66
|
+
const container = _quikChatEl;
|
|
67
|
+
if (!container) return;
|
|
68
|
+
if (chatInstance && chatContainerEl === container) return;
|
|
69
|
+
|
|
70
|
+
chatInstance = new quikchat(
|
|
71
|
+
container,
|
|
72
|
+
(/** @type {any} */ _chat, /** @type {string} */ msg) => {
|
|
73
|
+
handleUserSend(msg);
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
theme: "quikchat-theme-dark",
|
|
77
|
+
titleArea: { show: false },
|
|
78
|
+
showTimestamps: false,
|
|
79
|
+
messagesArea: { alternating: false },
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
chatContainerEl = container;
|
|
83
|
+
|
|
84
|
+
replayMessages();
|
|
85
|
+
|
|
86
|
+
if (streaming) {
|
|
87
|
+
chatInstance.inputAreaSetEnabled(false);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function replayMessages() {
|
|
92
|
+
if (!chatInstance || !messages.length) return;
|
|
93
|
+
for (const msg of messages) {
|
|
94
|
+
if (msg.role === "user") {
|
|
95
|
+
chatInstance.messageAddNew(msg.content, "You", "right", "user");
|
|
96
|
+
} else if (msg.role === "tool" && msg.toolUse) {
|
|
97
|
+
chatInstance.messageAddNew(
|
|
98
|
+
formatToolLabel(msg.toolUse.tool, msg.toolUse.input),
|
|
99
|
+
"",
|
|
100
|
+
"left",
|
|
101
|
+
"tool",
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
chatInstance.messageAddNew(msg.content, "", "left", "assistant");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (streaming && currentAssistantText) {
|
|
108
|
+
currentStreamMsgId = chatInstance.messageAddNew(currentAssistantText, "", "left", "assistant");
|
|
109
|
+
streamStarted = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Auth ───────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
async function checkAuth() {
|
|
116
|
+
authStatus = "checking";
|
|
117
|
+
rerenderPanel();
|
|
118
|
+
try {
|
|
119
|
+
const plat = getPlatform();
|
|
120
|
+
const result = await plat.aiAuthStatus();
|
|
121
|
+
authStatus = result.authenticated ? "authenticated" : "unauthenticated";
|
|
122
|
+
authError = result.error || "";
|
|
123
|
+
} catch (err) {
|
|
124
|
+
authStatus = "unauthenticated";
|
|
125
|
+
authError = String(err);
|
|
126
|
+
}
|
|
127
|
+
rerenderPanel();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Messaging ──────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/** @param {string} text */
|
|
133
|
+
async function handleUserSend(text) {
|
|
134
|
+
if (!text.trim() || streaming) return;
|
|
135
|
+
|
|
136
|
+
messages.push({ role: "user", content: text });
|
|
137
|
+
streaming = true;
|
|
138
|
+
currentAssistantText = "";
|
|
139
|
+
streamStarted = false;
|
|
140
|
+
|
|
141
|
+
if (chatInstance) {
|
|
142
|
+
currentStreamMsgId = chatInstance.messageAddTypingIndicator("");
|
|
143
|
+
chatInstance.inputAreaSetEnabled(false);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
rerenderPanel();
|
|
147
|
+
|
|
148
|
+
const plat = getPlatform();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
if (!sessionId) {
|
|
152
|
+
const result = await plat.aiCreateSession({ message: text });
|
|
153
|
+
sessionId = result.id;
|
|
154
|
+
await connectStream(/** @type {string} */ (sessionId));
|
|
155
|
+
} else {
|
|
156
|
+
await plat.aiSendMessage(sessionId, text);
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (chatInstance && currentStreamMsgId != null) {
|
|
160
|
+
chatInstance.messageReplaceContent(currentStreamMsgId, `Error: ${err}`);
|
|
161
|
+
}
|
|
162
|
+
messages.push({ role: "assistant", content: `Error: ${err}` });
|
|
163
|
+
streaming = false;
|
|
164
|
+
if (chatInstance) chatInstance.inputAreaSetEnabled(true);
|
|
165
|
+
rerenderPanel();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function stop() {
|
|
170
|
+
if (!sessionId) return;
|
|
171
|
+
const plat = getPlatform();
|
|
172
|
+
plat.aiStopSession(sessionId);
|
|
173
|
+
finishStream();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function newChat() {
|
|
177
|
+
if (sessionId) {
|
|
178
|
+
const plat = getPlatform();
|
|
179
|
+
plat.aiDeleteSession(sessionId);
|
|
180
|
+
}
|
|
181
|
+
disconnectStream();
|
|
182
|
+
messages = [];
|
|
183
|
+
sessionId = null;
|
|
184
|
+
streaming = false;
|
|
185
|
+
currentAssistantText = "";
|
|
186
|
+
currentStreamMsgId = null;
|
|
187
|
+
streamStarted = false;
|
|
188
|
+
pendingFileReloads.clear();
|
|
189
|
+
if (chatInstance) {
|
|
190
|
+
chatInstance.historyImport([]);
|
|
191
|
+
chatInstance.inputAreaSetEnabled(true);
|
|
192
|
+
}
|
|
193
|
+
rerenderPanel();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── SSE Stream ─────────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
async function connectStream(/** @type {string} */ id) {
|
|
199
|
+
disconnectStream();
|
|
200
|
+
const plat = getPlatform();
|
|
201
|
+
const url = await Promise.resolve(plat.aiStreamUrl(id));
|
|
202
|
+
eventSource = new EventSource(url);
|
|
203
|
+
|
|
204
|
+
eventSource.addEventListener("stream_event", (e) => {
|
|
205
|
+
try {
|
|
206
|
+
const data = JSON.parse(e.data);
|
|
207
|
+
const evt = data.event;
|
|
208
|
+
if (evt?.type === "content_block_delta" && evt.delta?.type === "text_delta") {
|
|
209
|
+
const token = evt.delta.text;
|
|
210
|
+
currentAssistantText += token;
|
|
211
|
+
if (chatInstance && currentStreamMsgId != null) {
|
|
212
|
+
if (!streamStarted) {
|
|
213
|
+
chatInstance.messageReplaceContent(currentStreamMsgId, currentAssistantText);
|
|
214
|
+
streamStarted = true;
|
|
215
|
+
} else {
|
|
216
|
+
chatInstance.messageAppendContent(currentStreamMsgId, token);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch {}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
eventSource.addEventListener("assistant", (e) => {
|
|
224
|
+
try {
|
|
225
|
+
handleAssistantMessage(JSON.parse(e.data));
|
|
226
|
+
} catch {}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
eventSource.addEventListener("result", (e) => {
|
|
230
|
+
try {
|
|
231
|
+
const data = JSON.parse(e.data);
|
|
232
|
+
if (data.result && data.is_error) {
|
|
233
|
+
if (chatInstance && currentStreamMsgId != null) {
|
|
234
|
+
chatInstance.messageReplaceContent(currentStreamMsgId, `Error: ${data.result}`);
|
|
235
|
+
}
|
|
236
|
+
currentAssistantText = `Error: ${data.result}`;
|
|
237
|
+
}
|
|
238
|
+
} catch {}
|
|
239
|
+
finishStream();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
eventSource.addEventListener("done", () => {
|
|
243
|
+
finishStream();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
eventSource.addEventListener("error", (e) => {
|
|
247
|
+
try {
|
|
248
|
+
const data = JSON.parse(/** @type {MessageEvent} */ (e).data);
|
|
249
|
+
if (data.error) {
|
|
250
|
+
if (chatInstance && currentStreamMsgId != null) {
|
|
251
|
+
chatInstance.messageReplaceContent(currentStreamMsgId, `Error: ${data.error}`);
|
|
252
|
+
}
|
|
253
|
+
currentAssistantText = `Error: ${data.error}`;
|
|
254
|
+
}
|
|
255
|
+
} catch {}
|
|
256
|
+
finishStream();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
eventSource.onerror = () => {
|
|
260
|
+
finishStream();
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function finishStream() {
|
|
265
|
+
if (!streaming) return;
|
|
266
|
+
if (currentAssistantText) {
|
|
267
|
+
messages.push({ role: "assistant", content: currentAssistantText });
|
|
268
|
+
currentAssistantText = "";
|
|
269
|
+
}
|
|
270
|
+
streaming = false;
|
|
271
|
+
currentStreamMsgId = null;
|
|
272
|
+
streamStarted = false;
|
|
273
|
+
if (chatInstance) chatInstance.inputAreaSetEnabled(true);
|
|
274
|
+
|
|
275
|
+
if (pendingFileReloads.size) {
|
|
276
|
+
for (const fp of pendingFileReloads) reloadFileInTab(fp);
|
|
277
|
+
pendingFileReloads.clear();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
rerenderPanel();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function disconnectStream() {
|
|
284
|
+
if (eventSource) {
|
|
285
|
+
eventSource.close();
|
|
286
|
+
eventSource = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** @param {any} data */
|
|
291
|
+
function handleAssistantMessage(data) {
|
|
292
|
+
const content = data.message?.content || data.content;
|
|
293
|
+
if (!content) return;
|
|
294
|
+
|
|
295
|
+
let text = "";
|
|
296
|
+
const toolBlocks = [];
|
|
297
|
+
|
|
298
|
+
for (const block of content) {
|
|
299
|
+
if (block.type === "text") {
|
|
300
|
+
text += block.text;
|
|
301
|
+
} else if (block.type === "tool_use") {
|
|
302
|
+
toolBlocks.push({ tool: block.name, input: block.input });
|
|
303
|
+
if ((block.name === "Edit" || block.name === "Write") && block.input) {
|
|
304
|
+
const fp = block.input.file_path || block.input.path;
|
|
305
|
+
if (fp) pendingFileReloads.add(String(fp));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (toolBlocks.length) {
|
|
311
|
+
if (currentAssistantText) {
|
|
312
|
+
messages.push({ role: "assistant", content: currentAssistantText });
|
|
313
|
+
}
|
|
314
|
+
for (const t of toolBlocks) {
|
|
315
|
+
messages.push({ role: "tool", content: "", toolUse: t });
|
|
316
|
+
if (chatInstance) {
|
|
317
|
+
chatInstance.messageAddNew(formatToolLabel(t.tool, t.input), "", "left", "tool");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
currentAssistantText = text;
|
|
323
|
+
if (chatInstance) {
|
|
324
|
+
currentStreamMsgId = chatInstance.messageAddNew(text || "", "", "left", "assistant");
|
|
325
|
+
streamStarted = !!text;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── Tool Label Formatting ─────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param {string} tool
|
|
333
|
+
* @param {Record<string, unknown>} [input]
|
|
334
|
+
*/
|
|
335
|
+
function formatToolLabel(tool, input) {
|
|
336
|
+
switch (tool) {
|
|
337
|
+
case "Edit":
|
|
338
|
+
case "Write":
|
|
339
|
+
return `📝 ${tool}: ${input?.file_path || input?.path || "file"}`;
|
|
340
|
+
case "Read":
|
|
341
|
+
return `📖 Read: ${input?.file_path || input?.path || "file"}`;
|
|
342
|
+
case "Bash":
|
|
343
|
+
return `⚡ Run: ${truncate(String(input?.command || ""), 50)}`;
|
|
344
|
+
case "Glob":
|
|
345
|
+
return `🔍 Glob: ${input?.pattern || ""}`;
|
|
346
|
+
case "Grep":
|
|
347
|
+
return `🔍 Grep: ${truncate(String(input?.pattern || ""), 40)}`;
|
|
348
|
+
default:
|
|
349
|
+
return `🔧 ${tool}`;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** @param {string} s @param {number} max */
|
|
354
|
+
function truncate(s, max) {
|
|
355
|
+
return s.length > max ? s.slice(0, max) + "…" : s;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ─── Template ───────────────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
/** @returns {import("lit-html").TemplateResult} */
|
|
361
|
+
export function renderAiPanelTemplate() {
|
|
362
|
+
if (authStatus === "checking" || authStatus === "unknown") {
|
|
363
|
+
return html`<div class="ai-tab-body">
|
|
364
|
+
<div class="ai-status-center">Checking authentication...</div>
|
|
365
|
+
</div>`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (authStatus === "unauthenticated") {
|
|
369
|
+
return html`
|
|
370
|
+
<div class="ai-tab-body">
|
|
371
|
+
<div class="ai-status-center">
|
|
372
|
+
<sp-icon-artboard style="font-size:32px"></sp-icon-artboard>
|
|
373
|
+
<div>Claude authentication required</div>
|
|
374
|
+
<div style="font-size:11px;color:var(--spectrum-global-color-gray-600)">
|
|
375
|
+
Run the following in your terminal:
|
|
376
|
+
</div>
|
|
377
|
+
<code class="ai-code-snippet">npx @anthropic-ai/claude-code login</code>
|
|
378
|
+
${authError
|
|
379
|
+
? html`<sp-help-text variant="negative">${authError}</sp-help-text>`
|
|
380
|
+
: nothing}
|
|
381
|
+
<sp-button size="s" variant="primary" @click=${checkAuth}>Retry</sp-button>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return html`
|
|
388
|
+
<div class="ai-tab-body">
|
|
389
|
+
<div class="ai-toolbar">
|
|
390
|
+
${streaming
|
|
391
|
+
? html`<sp-action-button size="xs" @click=${stop}>Stop</sp-action-button>`
|
|
392
|
+
: nothing}
|
|
393
|
+
<sp-action-button size="xs" quiet @click=${newChat}>
|
|
394
|
+
<sp-icon-add slot="icon"></sp-icon-add>
|
|
395
|
+
New Chat
|
|
396
|
+
</sp-action-button>
|
|
397
|
+
</div>
|
|
398
|
+
<div
|
|
399
|
+
id="ai-quikchat"
|
|
400
|
+
${ref((el) => {
|
|
401
|
+
_quikChatEl = /** @type {HTMLElement | null} */ (el || null);
|
|
402
|
+
})}
|
|
403
|
+
></div>
|
|
404
|
+
</div>
|
|
405
|
+
`;
|
|
406
|
+
}
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import { html, render as litRender, nothing } from "lit-html";
|
|
7
7
|
import { styleMap } from "lit-html/directives/style-map.js";
|
|
8
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
8
9
|
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
9
10
|
|
|
10
11
|
import { getNodeAtPath, nodeLabel, parentElementPath, childIndex } from "../store.js";
|
|
11
12
|
import { activeTab } from "../workspace/workspace.js";
|
|
12
|
-
import { transactDoc, mutateMoveNode } from "../tabs/transact.js";
|
|
13
|
+
import { transactDoc, mutateMoveNode, mutateUpdateProperty } from "../tabs/transact.js";
|
|
13
14
|
import { view } from "../view.js";
|
|
14
15
|
import { isEditing, getActiveElement, getInlineActions } from "../editor/inline-edit.js";
|
|
15
16
|
import { toggleInlineFormat, isTagActiveInSelection } from "../editor/inline-format.js";
|
|
@@ -17,13 +18,15 @@ import { componentRegistry } from "../files/components.js";
|
|
|
17
18
|
import { convertToComponent } from "../editor/convert-to-component.js";
|
|
18
19
|
import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
|
|
19
20
|
import { getLayerSlot } from "../ui/layers.js";
|
|
21
|
+
import { showSlashMenu } from "../editor/slash-menu.js";
|
|
22
|
+
import { getConvertTargets } from "../editor/convert-targets.js";
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* @typedef {import("../state.js").StudioState} StudioState
|
|
23
26
|
*
|
|
24
27
|
* @typedef {import("../state.js").JxPath} JxPath
|
|
25
28
|
*
|
|
26
|
-
* @typedef {
|
|
29
|
+
* @typedef {import("../editor/inline-edit.js").InlineAction} InlineAction
|
|
27
30
|
*/
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -70,9 +73,29 @@ const formatIconMap = /** @type {Record<string, import("lit-html").TemplateResul
|
|
|
70
73
|
function onBarMousedown(e) {
|
|
71
74
|
if (/** @type {HTMLElement} */ (e.target).closest("sp-textfield")) return;
|
|
72
75
|
if (/** @type {HTMLElement} */ (e.target).closest(".bar-drag-handle")) return;
|
|
76
|
+
if (/** @type {HTMLElement} */ (e.target).closest(".bar-tag--interactive")) return;
|
|
73
77
|
e.preventDefault();
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
/**
|
|
81
|
+
* @param {MouseEvent} e
|
|
82
|
+
* @param {import("../editor/convert-targets.js").SlashCommand[]} targets
|
|
83
|
+
* @param {JxPath} selection
|
|
84
|
+
*/
|
|
85
|
+
function onTagBadgeClick(e, targets, selection) {
|
|
86
|
+
e.stopPropagation();
|
|
87
|
+
const anchorEl = /** @type {HTMLElement} */ (e.currentTarget);
|
|
88
|
+
showSlashMenu(anchorEl, "", {
|
|
89
|
+
showFilter: targets.length > 6,
|
|
90
|
+
commands: targets,
|
|
91
|
+
onSelect: (cmd) => {
|
|
92
|
+
transactDoc(activeTab.value, (t) => {
|
|
93
|
+
mutateUpdateProperty(t, selection, "tagName", cmd.tag);
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
/** Saved selection range for format button mousedown→click flow */
|
|
77
100
|
function captureSelectionRange() {
|
|
78
101
|
const sel = window.getSelection();
|
|
@@ -182,7 +205,7 @@ function applyInlineFormat(action) {
|
|
|
182
205
|
code: "code",
|
|
183
206
|
};
|
|
184
207
|
|
|
185
|
-
const tag = cmdToTag[action.command];
|
|
208
|
+
const tag = action.command ? cmdToTag[action.command] : undefined;
|
|
186
209
|
if (tag) {
|
|
187
210
|
const editableRoot = getActiveElement();
|
|
188
211
|
toggleInlineFormat(tag, editableRoot);
|
|
@@ -226,9 +249,11 @@ function showLinkPopover(anchorBtn) {
|
|
|
226
249
|
|
|
227
250
|
const rect = anchorBtn.getBoundingClientRect();
|
|
228
251
|
|
|
252
|
+
/** @type {HTMLInputElement | null} */
|
|
253
|
+
let _linkField = null;
|
|
254
|
+
|
|
229
255
|
const onApply = () => {
|
|
230
|
-
const
|
|
231
|
-
const url = field?.value || "";
|
|
256
|
+
const url = _linkField?.value || "";
|
|
232
257
|
if (existingLink) {
|
|
233
258
|
existingLink.setAttribute("href", url);
|
|
234
259
|
} else if (url) {
|
|
@@ -272,6 +297,10 @@ function showLinkPopover(anchorBtn) {
|
|
|
272
297
|
style="width:200px"
|
|
273
298
|
value=${existingLink?.getAttribute("href") || ""}
|
|
274
299
|
@keydown=${onKeydown}
|
|
300
|
+
${ref((el) => {
|
|
301
|
+
_linkField = /** @type {HTMLInputElement | null} */ (el || null);
|
|
302
|
+
if (el) requestAnimationFrame(() => /** @type {HTMLElement} */ (el).focus());
|
|
303
|
+
})}
|
|
275
304
|
></sp-textfield>
|
|
276
305
|
<sp-action-button size="xs" @click=${onApply}>
|
|
277
306
|
${existingLink ? "Update" : "Apply"}
|
|
@@ -283,10 +312,6 @@ function showLinkPopover(anchorBtn) {
|
|
|
283
312
|
`,
|
|
284
313
|
host,
|
|
285
314
|
);
|
|
286
|
-
|
|
287
|
-
requestAnimationFrame(
|
|
288
|
-
() => /** @type {HTMLElement | null} */ (host.querySelector("sp-textfield"))?.focus(),
|
|
289
|
-
);
|
|
290
315
|
}
|
|
291
316
|
|
|
292
317
|
/** Move the selected node up (swap with previous sibling). */
|
|
@@ -360,6 +385,21 @@ export function renderBlockActionBar() {
|
|
|
360
385
|
? actions.filter((a) => isTagActiveInSelection(a.tag, el)).map((a) => a.tag)
|
|
361
386
|
: [];
|
|
362
387
|
|
|
388
|
+
// Conversion targets for badge click
|
|
389
|
+
const isComponent =
|
|
390
|
+
node.tagName?.includes("-") &&
|
|
391
|
+
componentRegistry.some((/** @type {{ tagName: string }} */ c) => c.tagName === node.tagName);
|
|
392
|
+
const isEmpty =
|
|
393
|
+
!node.textContent &&
|
|
394
|
+
(!node.children ||
|
|
395
|
+
node.children.length === 0 ||
|
|
396
|
+
(Array.isArray(node.children) &&
|
|
397
|
+
node.children.length === 1 &&
|
|
398
|
+
typeof node.children[0] === "object" &&
|
|
399
|
+
node.children[0]?.tagName === "br"));
|
|
400
|
+
const convertTargets = !isComponent ? getConvertTargets(tag, isEmpty) : [];
|
|
401
|
+
const badgeInteractive = convertTargets.length > 0;
|
|
402
|
+
|
|
363
403
|
litRender(
|
|
364
404
|
html`
|
|
365
405
|
<div
|
|
@@ -369,10 +409,34 @@ export function renderBlockActionBar() {
|
|
|
369
409
|
>
|
|
370
410
|
${selection.length >= 2 ? renderParentSelector() : nothing}
|
|
371
411
|
|
|
372
|
-
<span
|
|
412
|
+
<span
|
|
413
|
+
class="bar-tag${badgeInteractive ? " bar-tag--interactive" : ""}"
|
|
414
|
+
@click=${badgeInteractive
|
|
415
|
+
? (/** @type {MouseEvent} */ e) => onTagBadgeClick(e, convertTargets, selection)
|
|
416
|
+
: nothing}
|
|
417
|
+
>${node.$id || (node.tagName ?? "div")}</span
|
|
418
|
+
>
|
|
373
419
|
|
|
374
420
|
${selection.length >= 2
|
|
375
|
-
? html`<span
|
|
421
|
+
? html`<span
|
|
422
|
+
class="bar-drag-handle"
|
|
423
|
+
title="Drag to reorder"
|
|
424
|
+
${ref((el) => {
|
|
425
|
+
if (!el) return;
|
|
426
|
+
if (view.selDragCleanup) {
|
|
427
|
+
view.selDragCleanup();
|
|
428
|
+
view.selDragCleanup = null;
|
|
429
|
+
}
|
|
430
|
+
view.selDragCleanup = draggable({
|
|
431
|
+
element: /** @type {HTMLElement} */ (el),
|
|
432
|
+
getInitialData: () => ({
|
|
433
|
+
type: "tree-node",
|
|
434
|
+
path: activeTab.value?.session.selection,
|
|
435
|
+
}),
|
|
436
|
+
});
|
|
437
|
+
})}
|
|
438
|
+
>⠿</span
|
|
439
|
+
>`
|
|
376
440
|
: nothing}
|
|
377
441
|
${selection.length >= 2 ? renderMoveArrows() : nothing}
|
|
378
442
|
${selection.length >= 2 && node.tagName
|
|
@@ -423,7 +487,7 @@ export function renderBlockActionBar() {
|
|
|
423
487
|
@mousedown=${captureSelectionRange}
|
|
424
488
|
@click=${(/** @type {MouseEvent} */ e) => onFormatClick(e, action)}
|
|
425
489
|
>
|
|
426
|
-
${formatIconMap[action.icon] ?? nothing}
|
|
490
|
+
${action.icon ? (formatIconMap[action.icon] ?? nothing) : nothing}
|
|
427
491
|
</sp-action-button>
|
|
428
492
|
`,
|
|
429
493
|
)}
|
|
@@ -437,27 +501,12 @@ export function renderBlockActionBar() {
|
|
|
437
501
|
|
|
438
502
|
// Post-render side effects
|
|
439
503
|
requestAnimationFrame(() => {
|
|
440
|
-
const bar = view.blockActionBarEl?.firstElementChild;
|
|
504
|
+
const bar = /** @type {HTMLElement | null} */ (view.blockActionBarEl?.firstElementChild);
|
|
441
505
|
if (!bar) return;
|
|
442
506
|
// Clamp to window
|
|
443
507
|
const barRect = bar.getBoundingClientRect();
|
|
444
508
|
if (barRect.right > window.innerWidth) {
|
|
445
509
|
bar.style.left = `${Math.max(0, window.innerWidth - barRect.width)}px`;
|
|
446
510
|
}
|
|
447
|
-
// Attach drag handle
|
|
448
|
-
const currentTab = activeTab.value;
|
|
449
|
-
if (currentTab?.session.selection && currentTab.session.selection.length >= 2) {
|
|
450
|
-
const handle = bar.querySelector(".bar-drag-handle");
|
|
451
|
-
if (handle) {
|
|
452
|
-
if (view.selDragCleanup) {
|
|
453
|
-
view.selDragCleanup();
|
|
454
|
-
view.selDragCleanup = null;
|
|
455
|
-
}
|
|
456
|
-
view.selDragCleanup = draggable({
|
|
457
|
-
element: handle,
|
|
458
|
-
getInitialData: () => ({ type: "tree-node", path: activeTab.value?.session.selection }),
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
511
|
});
|
|
463
512
|
}
|
package/src/panels/canvas-dnd.js
CHANGED
|
@@ -16,11 +16,15 @@ import { effectiveZoom } from "../canvas/canvas-helpers.js";
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* @typedef {{
|
|
19
|
+
* mediaName: string | null;
|
|
20
|
+
* element: HTMLElement | null;
|
|
19
21
|
* canvas: HTMLElement;
|
|
20
|
-
* overlayClk: HTMLElement;
|
|
21
22
|
* overlay: HTMLElement;
|
|
23
|
+
* overlayClk: HTMLElement;
|
|
22
24
|
* viewport: HTMLElement;
|
|
25
|
+
* scrollContainer: HTMLElement | null;
|
|
23
26
|
* dropLine: HTMLElement;
|
|
27
|
+
* _width: number | null;
|
|
24
28
|
* }} CanvasPanel
|
|
25
29
|
*
|
|
26
30
|
* @typedef {(string | number)[]} JxPath
|
|
@@ -56,7 +60,9 @@ export function registerPanelDnD(panel) {
|
|
|
56
60
|
onDrop() {
|
|
57
61
|
_activeDropEl?.classList.remove("canvas-drop-target");
|
|
58
62
|
_activeDropEl = null;
|
|
59
|
-
for (const p of canvasPanels)
|
|
63
|
+
for (const p of canvasPanels) {
|
|
64
|
+
if (p.dropLine) p.dropLine.style.display = "none";
|
|
65
|
+
}
|
|
60
66
|
view.lastDragInput = null;
|
|
61
67
|
for (const el of canvas.querySelectorAll("*")) {
|
|
62
68
|
/** @type {HTMLElement} */ (el).style.pointerEvents = "none";
|