@jxsuite/studio 0.28.2 → 0.28.4
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 +86144 -83971
- package/dist/studio.js.map +229 -219
- package/package.json +5 -5
- package/src/browse/browse-modal.ts +9 -3
- package/src/browse/browse.ts +139 -82
- package/src/canvas/canvas-diff.ts +35 -18
- package/src/canvas/canvas-helpers.ts +55 -21
- package/src/canvas/canvas-live-render.ts +140 -89
- package/src/canvas/canvas-render.ts +127 -82
- package/src/canvas/canvas-utils.ts +68 -42
- package/src/canvas/nested-site-style.ts +15 -7
- package/src/editor/component-inline-edit.ts +90 -51
- package/src/editor/content-inline-edit.ts +75 -72
- package/src/editor/context-menu.ts +125 -71
- package/src/editor/convert-targets.ts +17 -13
- package/src/editor/convert-to-component.ts +51 -28
- package/src/editor/convert-to-repeater.ts +44 -19
- package/src/editor/inline-edit.ts +107 -52
- package/src/editor/inline-format.ts +128 -60
- package/src/editor/insertion-helper.ts +26 -14
- package/src/editor/shortcuts.ts +89 -43
- package/src/editor/slash-menu.ts +41 -26
- package/src/files/components.ts +9 -7
- package/src/files/file-ops.ts +53 -33
- package/src/files/files.ts +246 -143
- package/src/format/constraints.ts +25 -15
- package/src/format/format-host.ts +33 -10
- package/src/github/github-auth.ts +24 -14
- package/src/github/github-publish.ts +20 -14
- package/src/new-project/new-project-modal.ts +27 -18
- package/src/panels/activity-bar.ts +27 -26
- package/src/panels/ai-panel.ts +100 -44
- package/src/panels/block-action-bar.ts +84 -52
- package/src/panels/canvas-dnd.ts +47 -28
- package/src/panels/data-explorer.ts +24 -11
- package/src/panels/dnd.ts +148 -100
- package/src/panels/editors.ts +54 -37
- package/src/panels/elements-panel.ts +23 -13
- package/src/panels/events-panel.ts +48 -38
- package/src/panels/git-panel.ts +95 -60
- package/src/panels/head-panel.ts +138 -84
- package/src/panels/imports-panel.ts +66 -29
- package/src/panels/layers-panel.ts +80 -36
- package/src/panels/left-panel.ts +86 -53
- package/src/panels/overlays.ts +36 -19
- package/src/panels/panel-events.ts +48 -27
- package/src/panels/panel-scheduler.ts +41 -23
- package/src/panels/preview-render.ts +26 -26
- package/src/panels/properties-panel.ts +155 -118
- package/src/panels/pseudo-preview.ts +31 -15
- package/src/panels/quick-search.ts +28 -13
- package/src/panels/right-panel.ts +35 -24
- package/src/panels/shared.ts +46 -29
- package/src/panels/signals-panel.ts +303 -186
- package/src/panels/statusbar.ts +27 -12
- package/src/panels/style-inputs.ts +30 -24
- package/src/panels/style-panel.ts +130 -90
- package/src/panels/style-utils.ts +63 -23
- package/src/panels/stylebook-layers-panel.ts +27 -30
- package/src/panels/stylebook-panel.ts +140 -72
- package/src/panels/tab-strip.ts +21 -10
- package/src/panels/toolbar.ts +52 -40
- package/src/panels/welcome-screen.ts +1 -1
- package/src/platform.ts +2 -1
- package/src/platforms/devserver.ts +221 -105
- package/src/recent-projects.ts +15 -7
- package/src/resize-edges.ts +7 -5
- package/src/services/cem-export.ts +73 -57
- package/src/services/code-services.ts +52 -25
- package/src/services/monaco-setup.ts +5 -5
- package/src/settings/content-types-editor.ts +133 -62
- package/src/settings/css-vars-editor.ts +31 -14
- package/src/settings/defs-editor.ts +125 -63
- package/src/settings/general-settings.ts +11 -6
- package/src/settings/head-editor.ts +30 -15
- package/src/settings/schema-field-ui.ts +75 -35
- package/src/settings/settings-modal.ts +35 -17
- package/src/site-context.ts +80 -47
- package/src/state.ts +98 -65
- package/src/store.ts +42 -23
- package/src/studio.ts +165 -144
- package/src/tabs/tab.ts +51 -45
- package/src/tabs/transact.ts +230 -109
- package/src/types.ts +60 -51
- package/src/ui/button-group.ts +3 -2
- package/src/ui/color-selector.ts +33 -15
- package/src/ui/expression-editor.ts +139 -86
- package/src/ui/field-input.ts +35 -18
- package/src/ui/field-row.ts +1 -1
- package/src/ui/icons.ts +5 -6
- package/src/ui/layers.ts +33 -26
- package/src/ui/media-picker.ts +13 -7
- package/src/ui/panel-resize.ts +39 -20
- package/src/ui/spectrum.ts +4 -2
- package/src/ui/unit-selector.ts +20 -11
- package/src/ui/value-selector.ts +13 -7
- package/src/ui/widgets.ts +34 -18
- package/src/utils/canvas-media.ts +66 -33
- package/src/utils/edit-display.ts +43 -44
- package/src/utils/google-fonts.ts +10 -6
- package/src/utils/inherited-style.ts +22 -8
- package/src/utils/studio-utils.ts +88 -45
- package/src/view.ts +9 -5
- package/src/workspace/workspace.ts +49 -29
package/src/panels/ai-panel.ts
CHANGED
|
@@ -33,17 +33,29 @@ let currentAssistantText = "";
|
|
|
33
33
|
let eventSource = null as EventSource | null;
|
|
34
34
|
let mounted = false;
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
/** Minimal surface of the untyped quikchat library that this panel uses. */
|
|
37
|
+
interface QuikChatInstance {
|
|
38
|
+
messageAddNew: (text: string, sender: string, side: string, role?: string) => number;
|
|
39
|
+
messageAddTypingIndicator: (text: string) => number;
|
|
40
|
+
messageReplaceContent: (id: number, text: string) => void;
|
|
41
|
+
messageAppendContent: (id: number, text: string) => void;
|
|
42
|
+
inputAreaSetEnabled: (enabled: boolean) => void;
|
|
43
|
+
historyImport: (history: unknown[]) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let chatInstance: QuikChatInstance | null = null;
|
|
37
47
|
let chatContainerEl: Element | null = null;
|
|
38
48
|
let _quikChatEl: HTMLElement | null = null;
|
|
39
49
|
let currentStreamMsgId = null as number | null;
|
|
40
50
|
let streamStarted = false;
|
|
41
|
-
|
|
51
|
+
const pendingFileReloads = new Set<string>();
|
|
42
52
|
|
|
43
53
|
// ─── Lifecycle ──────────────────────────────────────────────────────────────
|
|
44
54
|
|
|
45
55
|
export function mountAiPanel() {
|
|
46
|
-
if (mounted)
|
|
56
|
+
if (mounted) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
47
59
|
mounted = true;
|
|
48
60
|
checkAuth();
|
|
49
61
|
}
|
|
@@ -54,7 +66,9 @@ const _g = globalThis as unknown as {
|
|
|
54
66
|
|
|
55
67
|
function rerenderPanel() {
|
|
56
68
|
const { render } = _g.__jxRightPanelRender || {};
|
|
57
|
-
if (render)
|
|
69
|
+
if (render) {
|
|
70
|
+
render();
|
|
71
|
+
}
|
|
58
72
|
requestAnimationFrame(() => mountQuikChat());
|
|
59
73
|
}
|
|
60
74
|
|
|
@@ -66,19 +80,23 @@ export function registerRightPanelRender(fn: () => void) {
|
|
|
66
80
|
|
|
67
81
|
export function mountQuikChat() {
|
|
68
82
|
const container = _quikChatEl;
|
|
69
|
-
if (!container)
|
|
70
|
-
|
|
83
|
+
if (!container) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (chatInstance && chatContainerEl === container) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
71
89
|
|
|
72
90
|
chatInstance = new quikchat(
|
|
73
91
|
container,
|
|
74
|
-
(_chat:
|
|
92
|
+
(_chat: unknown, msg: string) => {
|
|
75
93
|
handleUserSend(msg);
|
|
76
94
|
},
|
|
77
95
|
{
|
|
96
|
+
messagesArea: { alternating: false },
|
|
97
|
+
showTimestamps: false,
|
|
78
98
|
theme: "quikchat-theme-dark",
|
|
79
99
|
titleArea: { show: false },
|
|
80
|
-
showTimestamps: false,
|
|
81
|
-
messagesArea: { alternating: false },
|
|
82
100
|
},
|
|
83
101
|
);
|
|
84
102
|
chatContainerEl = container;
|
|
@@ -86,12 +104,14 @@ export function mountQuikChat() {
|
|
|
86
104
|
replayMessages();
|
|
87
105
|
|
|
88
106
|
if (streaming) {
|
|
89
|
-
chatInstance
|
|
107
|
+
chatInstance?.inputAreaSetEnabled(false);
|
|
90
108
|
}
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
function replayMessages() {
|
|
94
|
-
if (!chatInstance ||
|
|
112
|
+
if (!chatInstance || messages.length === 0) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
95
115
|
for (const msg of messages) {
|
|
96
116
|
if (msg.role === "user") {
|
|
97
117
|
chatInstance.messageAddNew(msg.content, "You", "right", "user");
|
|
@@ -122,9 +142,9 @@ async function checkAuth() {
|
|
|
122
142
|
const result = await plat.aiAuthStatus();
|
|
123
143
|
authStatus = result.authenticated ? "authenticated" : "unauthenticated";
|
|
124
144
|
authError = result.error || "";
|
|
125
|
-
} catch (
|
|
145
|
+
} catch (error) {
|
|
126
146
|
authStatus = "unauthenticated";
|
|
127
|
-
authError = String(
|
|
147
|
+
authError = String(error);
|
|
128
148
|
}
|
|
129
149
|
rerenderPanel();
|
|
130
150
|
}
|
|
@@ -133,9 +153,11 @@ async function checkAuth() {
|
|
|
133
153
|
|
|
134
154
|
/** @param {string} text */
|
|
135
155
|
async function handleUserSend(text: string) {
|
|
136
|
-
if (!text.trim() || streaming)
|
|
156
|
+
if (!text.trim() || streaming) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
137
159
|
|
|
138
|
-
messages.push({ role: "user"
|
|
160
|
+
messages.push({ content: text, role: "user" });
|
|
139
161
|
streaming = true;
|
|
140
162
|
currentAssistantText = "";
|
|
141
163
|
streamStarted = false;
|
|
@@ -157,19 +179,23 @@ async function handleUserSend(text: string) {
|
|
|
157
179
|
} else {
|
|
158
180
|
await plat.aiSendMessage(sessionId, text);
|
|
159
181
|
}
|
|
160
|
-
} catch (
|
|
182
|
+
} catch (error) {
|
|
161
183
|
if (chatInstance && currentStreamMsgId != null) {
|
|
162
|
-
chatInstance.messageReplaceContent(currentStreamMsgId, `Error: ${
|
|
184
|
+
chatInstance.messageReplaceContent(currentStreamMsgId, `Error: ${error}`);
|
|
163
185
|
}
|
|
164
|
-
messages.push({
|
|
186
|
+
messages.push({ content: `Error: ${error}`, role: "assistant" });
|
|
165
187
|
streaming = false;
|
|
166
|
-
if (chatInstance)
|
|
188
|
+
if (chatInstance) {
|
|
189
|
+
chatInstance.inputAreaSetEnabled(true);
|
|
190
|
+
}
|
|
167
191
|
rerenderPanel();
|
|
168
192
|
}
|
|
169
193
|
}
|
|
170
194
|
|
|
171
195
|
function stop() {
|
|
172
|
-
if (!sessionId)
|
|
196
|
+
if (!sessionId) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
173
199
|
const plat = getPlatform();
|
|
174
200
|
plat.aiStopSession(sessionId);
|
|
175
201
|
finishStream();
|
|
@@ -258,24 +284,30 @@ async function connectStream(id: string) {
|
|
|
258
284
|
finishStream();
|
|
259
285
|
});
|
|
260
286
|
|
|
261
|
-
eventSource.
|
|
287
|
+
eventSource.addEventListener("error", () => {
|
|
262
288
|
finishStream();
|
|
263
|
-
};
|
|
289
|
+
});
|
|
264
290
|
}
|
|
265
291
|
|
|
266
292
|
function finishStream() {
|
|
267
|
-
if (!streaming)
|
|
293
|
+
if (!streaming) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
268
296
|
if (currentAssistantText) {
|
|
269
|
-
messages.push({ role: "assistant"
|
|
297
|
+
messages.push({ content: currentAssistantText, role: "assistant" });
|
|
270
298
|
currentAssistantText = "";
|
|
271
299
|
}
|
|
272
300
|
streaming = false;
|
|
273
301
|
currentStreamMsgId = null;
|
|
274
302
|
streamStarted = false;
|
|
275
|
-
if (chatInstance)
|
|
303
|
+
if (chatInstance) {
|
|
304
|
+
chatInstance.inputAreaSetEnabled(true);
|
|
305
|
+
}
|
|
276
306
|
|
|
277
|
-
if (pendingFileReloads.size) {
|
|
278
|
-
for (const fp of pendingFileReloads)
|
|
307
|
+
if (pendingFileReloads.size > 0) {
|
|
308
|
+
for (const fp of pendingFileReloads) {
|
|
309
|
+
reloadFileInTab(fp);
|
|
310
|
+
}
|
|
279
311
|
pendingFileReloads.clear();
|
|
280
312
|
}
|
|
281
313
|
|
|
@@ -289,10 +321,26 @@ function disconnectStream() {
|
|
|
289
321
|
}
|
|
290
322
|
}
|
|
291
323
|
|
|
292
|
-
/**
|
|
293
|
-
|
|
324
|
+
/** SSE payload from the assistant stream: message blocks or a bare content array. */
|
|
325
|
+
interface AssistantMessageData {
|
|
326
|
+
message?: { content?: AssistantBlock[] };
|
|
327
|
+
content?: AssistantBlock[];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
interface AssistantBlock {
|
|
331
|
+
type?: string;
|
|
332
|
+
text?: string;
|
|
333
|
+
name?: string;
|
|
334
|
+
input?: Record<string, unknown>;
|
|
335
|
+
[key: string]: unknown;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** @param {AssistantMessageData} data */
|
|
339
|
+
function handleAssistantMessage(data: AssistantMessageData) {
|
|
294
340
|
const content = data.message?.content || data.content;
|
|
295
|
-
if (!content)
|
|
341
|
+
if (!content) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
296
344
|
|
|
297
345
|
let text = "";
|
|
298
346
|
const toolBlocks = [];
|
|
@@ -300,21 +348,23 @@ function handleAssistantMessage(data: any) {
|
|
|
300
348
|
for (const block of content) {
|
|
301
349
|
if (block.type === "text") {
|
|
302
350
|
text += block.text;
|
|
303
|
-
} else if (block.type === "tool_use") {
|
|
304
|
-
toolBlocks.push({ tool: block.name, input: block.input });
|
|
351
|
+
} else if (block.type === "tool_use" && typeof block.name === "string") {
|
|
352
|
+
toolBlocks.push({ tool: block.name, ...(block.input ? { input: block.input } : {}) });
|
|
305
353
|
if ((block.name === "Edit" || block.name === "Write") && block.input) {
|
|
306
354
|
const fp = block.input.file_path || block.input.path;
|
|
307
|
-
if (fp)
|
|
355
|
+
if (fp) {
|
|
356
|
+
pendingFileReloads.add(String(fp));
|
|
357
|
+
}
|
|
308
358
|
}
|
|
309
359
|
}
|
|
310
360
|
}
|
|
311
361
|
|
|
312
|
-
if (toolBlocks.length) {
|
|
362
|
+
if (toolBlocks.length > 0) {
|
|
313
363
|
if (currentAssistantText) {
|
|
314
|
-
messages.push({ role: "assistant"
|
|
364
|
+
messages.push({ content: currentAssistantText, role: "assistant" });
|
|
315
365
|
}
|
|
316
366
|
for (const t of toolBlocks) {
|
|
317
|
-
messages.push({
|
|
367
|
+
messages.push({ content: "", role: "tool", toolUse: t });
|
|
318
368
|
if (chatInstance) {
|
|
319
369
|
chatInstance.messageAddNew(formatToolLabel(t.tool, t.input), "", "left", "tool");
|
|
320
370
|
}
|
|
@@ -324,7 +374,7 @@ function handleAssistantMessage(data: any) {
|
|
|
324
374
|
currentAssistantText = text;
|
|
325
375
|
if (chatInstance) {
|
|
326
376
|
currentStreamMsgId = chatInstance.messageAddNew(text || "", "", "left", "assistant");
|
|
327
|
-
streamStarted =
|
|
377
|
+
streamStarted = Boolean(text);
|
|
328
378
|
}
|
|
329
379
|
}
|
|
330
380
|
|
|
@@ -337,24 +387,30 @@ function handleAssistantMessage(data: any) {
|
|
|
337
387
|
function formatToolLabel(tool: string, input?: Record<string, unknown>) {
|
|
338
388
|
switch (tool) {
|
|
339
389
|
case "Edit":
|
|
340
|
-
case "Write":
|
|
390
|
+
case "Write": {
|
|
341
391
|
return `📝 ${tool}: ${input?.file_path || input?.path || "file"}`;
|
|
342
|
-
|
|
392
|
+
}
|
|
393
|
+
case "Read": {
|
|
343
394
|
return `📖 Read: ${input?.file_path || input?.path || "file"}`;
|
|
344
|
-
|
|
395
|
+
}
|
|
396
|
+
case "Bash": {
|
|
345
397
|
return `⚡ Run: ${truncate(String(input?.command || ""), 50)}`;
|
|
346
|
-
|
|
398
|
+
}
|
|
399
|
+
case "Glob": {
|
|
347
400
|
return `🔍 Glob: ${input?.pattern || ""}`;
|
|
348
|
-
|
|
401
|
+
}
|
|
402
|
+
case "Grep": {
|
|
349
403
|
return `🔍 Grep: ${truncate(String(input?.pattern || ""), 40)}`;
|
|
350
|
-
|
|
404
|
+
}
|
|
405
|
+
default: {
|
|
351
406
|
return `🔧 ${tool}`;
|
|
407
|
+
}
|
|
352
408
|
}
|
|
353
409
|
}
|
|
354
410
|
|
|
355
411
|
/** @param {string} s @param {number} max */
|
|
356
412
|
function truncate(s: string, max: number) {
|
|
357
|
-
return s.length > max ? s.slice(0, max)
|
|
413
|
+
return s.length > max ? `${s.slice(0, max)}…` : s;
|
|
358
414
|
}
|
|
359
415
|
|
|
360
416
|
// ─── Template ───────────────────────────────────────────────────────────────
|
|
@@ -9,13 +9,13 @@ import { styleMap } from "lit-html/directives/style-map.js";
|
|
|
9
9
|
import { ref } from "lit-html/directives/ref.js";
|
|
10
10
|
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
11
11
|
|
|
12
|
-
import { getNodeAtPath, nodeLabel, parentElementPath
|
|
12
|
+
import { childIndex, childList, getNodeAtPath, nodeLabel, parentElementPath } from "../store";
|
|
13
13
|
import { activeTab } from "../workspace/workspace";
|
|
14
|
-
import {
|
|
14
|
+
import { mutateMoveNode, mutateUpdateProperty, transactDoc } from "../tabs/transact";
|
|
15
15
|
import { view } from "../view";
|
|
16
|
-
import {
|
|
16
|
+
import { getActiveElement, getInlineActions, isEditing } from "../editor/inline-edit";
|
|
17
17
|
import type { InlineAction } from "../editor/inline-edit";
|
|
18
|
-
import {
|
|
18
|
+
import { isTagActiveInSelection, toggleInlineFormat } from "../editor/inline-format";
|
|
19
19
|
import { componentRegistry } from "../files/components";
|
|
20
20
|
import { convertToComponent } from "../editor/convert-to-component";
|
|
21
21
|
import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers";
|
|
@@ -24,6 +24,8 @@ import { showSlashMenu } from "../editor/slash-menu";
|
|
|
24
24
|
import { getConvertTargets } from "../editor/convert-targets";
|
|
25
25
|
|
|
26
26
|
import type { JxPath } from "../state";
|
|
27
|
+
import type { TemplateResult } from "lit-html";
|
|
28
|
+
import type { SlashCommand } from "../editor/convert-targets.js";
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* @type {{
|
|
@@ -53,19 +55,19 @@ export function initBlockActionBar(ctx: {
|
|
|
53
55
|
|
|
54
56
|
/** Pre-built icon templates for inline format buttons (avoids unsafeStatic) */
|
|
55
57
|
const formatIconMap = {
|
|
58
|
+
"sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
|
|
59
|
+
"sp-icon-link": html`<sp-icon-link slot="icon"></sp-icon-link>`,
|
|
56
60
|
"sp-icon-text-bold": html`<sp-icon-text-bold slot="icon"></sp-icon-text-bold>`,
|
|
57
61
|
"sp-icon-text-italic": html`<sp-icon-text-italic slot="icon"></sp-icon-text-italic>`,
|
|
58
|
-
"sp-icon-text-underline": html`<sp-icon-text-underline slot="icon"></sp-icon-text-underline>`,
|
|
59
62
|
"sp-icon-text-strikethrough": html`<sp-icon-text-strikethrough
|
|
60
63
|
slot="icon"
|
|
61
64
|
></sp-icon-text-strikethrough>`,
|
|
65
|
+
"sp-icon-text-subscript": html`<sp-icon-text-subscript slot="icon"></sp-icon-text-subscript>`,
|
|
62
66
|
"sp-icon-text-superscript": html`<sp-icon-text-superscript
|
|
63
67
|
slot="icon"
|
|
64
68
|
></sp-icon-text-superscript>`,
|
|
65
|
-
"sp-icon-text-
|
|
66
|
-
|
|
67
|
-
"sp-icon-link": html`<sp-icon-link slot="icon"></sp-icon-link>`,
|
|
68
|
-
} as Record<string, import("lit-html").TemplateResult>;
|
|
69
|
+
"sp-icon-text-underline": html`<sp-icon-text-underline slot="icon"></sp-icon-text-underline>`,
|
|
70
|
+
} as Record<string, TemplateResult>;
|
|
69
71
|
|
|
70
72
|
/**
|
|
71
73
|
* Prevent the bar from stealing focus from contenteditable
|
|
@@ -73,9 +75,15 @@ const formatIconMap = {
|
|
|
73
75
|
* @param {MouseEvent} e
|
|
74
76
|
*/
|
|
75
77
|
function onBarMousedown(e: MouseEvent) {
|
|
76
|
-
if ((e.target as HTMLElement).closest("sp-textfield"))
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
if ((e.target as HTMLElement).closest("sp-textfield")) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if ((e.target as HTMLElement).closest(".bar-drag-handle")) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if ((e.target as HTMLElement).closest(".bar-tag--interactive")) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
79
87
|
e.preventDefault();
|
|
80
88
|
}
|
|
81
89
|
|
|
@@ -84,28 +92,26 @@ function onBarMousedown(e: MouseEvent) {
|
|
|
84
92
|
* @param {import("../editor/convert-targets.js").SlashCommand[]} targets
|
|
85
93
|
* @param {JxPath} selection
|
|
86
94
|
*/
|
|
87
|
-
function onTagBadgeClick(
|
|
88
|
-
e: MouseEvent,
|
|
89
|
-
targets: import("../editor/convert-targets.js").SlashCommand[],
|
|
90
|
-
selection: JxPath,
|
|
91
|
-
) {
|
|
95
|
+
function onTagBadgeClick(e: MouseEvent, targets: SlashCommand[], selection: JxPath) {
|
|
92
96
|
e.stopPropagation();
|
|
93
97
|
const anchorEl = e.currentTarget as HTMLElement;
|
|
94
98
|
showSlashMenu(anchorEl, "", {
|
|
95
|
-
showFilter: targets.length > 6,
|
|
96
99
|
commands: targets,
|
|
97
100
|
onSelect: (cmd) => {
|
|
98
101
|
transactDoc(activeTab.value, (t) => {
|
|
99
102
|
mutateUpdateProperty(t, selection, "tagName", cmd.tag);
|
|
100
103
|
});
|
|
101
104
|
},
|
|
105
|
+
showFilter: targets.length > 6,
|
|
102
106
|
});
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
/** Saved selection range for format button mousedown→click flow */
|
|
106
110
|
function captureSelectionRange() {
|
|
107
111
|
const sel = window.getSelection();
|
|
108
|
-
if (sel && sel.rangeCount)
|
|
112
|
+
if (sel && sel.rangeCount) {
|
|
113
|
+
view.savedRange = sel.getRangeAt(0).cloneRange();
|
|
114
|
+
}
|
|
109
115
|
}
|
|
110
116
|
|
|
111
117
|
/**
|
|
@@ -133,9 +139,13 @@ function onFormatClick(e: MouseEvent, action: InlineAction) {
|
|
|
133
139
|
|
|
134
140
|
function renderParentSelector() {
|
|
135
141
|
const tab = activeTab.value;
|
|
136
|
-
if (!tab?.session.selection)
|
|
142
|
+
if (!tab?.session.selection) {
|
|
143
|
+
return nothing;
|
|
144
|
+
}
|
|
137
145
|
const pPath = parentElementPath(tab.session.selection);
|
|
138
|
-
if (!pPath)
|
|
146
|
+
if (!pPath) {
|
|
147
|
+
return nothing;
|
|
148
|
+
}
|
|
139
149
|
const parentNode = getNodeAtPath(tab.doc.document, pPath);
|
|
140
150
|
return html`
|
|
141
151
|
<sp-action-button
|
|
@@ -154,12 +164,14 @@ function renderParentSelector() {
|
|
|
154
164
|
|
|
155
165
|
function renderMoveArrows() {
|
|
156
166
|
const tab = activeTab.value;
|
|
157
|
-
if (!tab?.session.selection)
|
|
167
|
+
if (!tab?.session.selection) {
|
|
168
|
+
return nothing;
|
|
169
|
+
}
|
|
158
170
|
const sel = tab.session.selection;
|
|
159
171
|
const idx = childIndex(sel) as number;
|
|
160
172
|
const pPath = parentElementPath(sel);
|
|
161
173
|
const parentNode = pPath ? getNodeAtPath(tab.doc.document, pPath) : null;
|
|
162
|
-
const siblings = parentNode
|
|
174
|
+
const siblings = parentNode ? childList(parentNode) : null;
|
|
163
175
|
return html`
|
|
164
176
|
<sp-action-button
|
|
165
177
|
size="xs"
|
|
@@ -196,12 +208,12 @@ function renderMoveArrows() {
|
|
|
196
208
|
function applyInlineFormat(action: InlineAction) {
|
|
197
209
|
const cmdToTag: Record<string, string> = {
|
|
198
210
|
bold: "strong",
|
|
211
|
+
code: "code",
|
|
199
212
|
italic: "em",
|
|
200
|
-
underline: "u",
|
|
201
213
|
strikethrough: "del",
|
|
202
|
-
superscript: "sup",
|
|
203
214
|
subscript: "sub",
|
|
204
|
-
|
|
215
|
+
superscript: "sup",
|
|
216
|
+
underline: "u",
|
|
205
217
|
};
|
|
206
218
|
|
|
207
219
|
const tag = action.command ? cmdToTag[action.command] : undefined;
|
|
@@ -220,7 +232,9 @@ export function dismissLinkPopover() {
|
|
|
220
232
|
|
|
221
233
|
/** Dismiss the block action bar. */
|
|
222
234
|
export function dismissBlockActionBar() {
|
|
223
|
-
if (view.blockActionBarEl)
|
|
235
|
+
if (view.blockActionBarEl) {
|
|
236
|
+
litRender(nothing, view.blockActionBarEl);
|
|
237
|
+
}
|
|
224
238
|
}
|
|
225
239
|
|
|
226
240
|
/** @param {HTMLElement} anchorBtn */
|
|
@@ -257,17 +271,22 @@ function showLinkPopover(anchorBtn: HTMLElement) {
|
|
|
257
271
|
};
|
|
258
272
|
|
|
259
273
|
const onRemove = () => {
|
|
260
|
-
if (!existingLink?.parentNode)
|
|
274
|
+
if (!existingLink?.parentNode) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
261
277
|
const frag = document.createDocumentFragment();
|
|
262
|
-
while (existingLink.firstChild)
|
|
278
|
+
while (existingLink.firstChild) {
|
|
279
|
+
frag.append(existingLink.firstChild);
|
|
280
|
+
}
|
|
263
281
|
existingLink.parentNode.replaceChild(frag, existingLink);
|
|
264
282
|
litRender(nothing, host);
|
|
265
283
|
renderBlockActionBar();
|
|
266
284
|
};
|
|
267
285
|
|
|
268
286
|
const onKeydown = (e: KeyboardEvent) => {
|
|
269
|
-
if (e.key === "Enter")
|
|
270
|
-
|
|
287
|
+
if (e.key === "Enter") {
|
|
288
|
+
onApply();
|
|
289
|
+
} else if (e.key === "Escape") {
|
|
271
290
|
litRender(nothing, host);
|
|
272
291
|
}
|
|
273
292
|
};
|
|
@@ -278,8 +297,8 @@ function showLinkPopover(anchorBtn: HTMLElement) {
|
|
|
278
297
|
class="link-popover"
|
|
279
298
|
open
|
|
280
299
|
style=${styleMap({
|
|
281
|
-
position: "fixed",
|
|
282
300
|
left: `${rect.left}px`,
|
|
301
|
+
position: "fixed",
|
|
283
302
|
top: `${rect.bottom + 4}px`,
|
|
284
303
|
zIndex: "30",
|
|
285
304
|
})}
|
|
@@ -292,7 +311,9 @@ function showLinkPopover(anchorBtn: HTMLElement) {
|
|
|
292
311
|
@keydown=${onKeydown}
|
|
293
312
|
${ref((el) => {
|
|
294
313
|
_linkField = (el as HTMLInputElement | null) || null;
|
|
295
|
-
if (el)
|
|
314
|
+
if (el) {
|
|
315
|
+
requestAnimationFrame(() => (el as HTMLElement).focus());
|
|
316
|
+
}
|
|
296
317
|
})}
|
|
297
318
|
></sp-textfield>
|
|
298
319
|
<sp-action-button size="xs" @click=${onApply}>
|
|
@@ -310,10 +331,14 @@ function showLinkPopover(anchorBtn: HTMLElement) {
|
|
|
310
331
|
/** Move the selected node up (swap with previous sibling). */
|
|
311
332
|
function moveSelectionUp() {
|
|
312
333
|
const tab = activeTab.value;
|
|
313
|
-
if (!tab?.session.selection || tab.session.selection.length < 2)
|
|
334
|
+
if (!tab?.session.selection || tab.session.selection.length < 2) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
314
337
|
const sel = tab.session.selection;
|
|
315
338
|
const idx = childIndex(sel) as number;
|
|
316
|
-
if (idx <= 0)
|
|
339
|
+
if (idx <= 0) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
317
342
|
const pPath = parentElementPath(sel) as JxPath;
|
|
318
343
|
transactDoc(tab, (t) => mutateMoveNode(t, sel, pPath, idx - 1));
|
|
319
344
|
tab.session.selection = [...pPath, "children", idx - 1];
|
|
@@ -322,20 +347,26 @@ function moveSelectionUp() {
|
|
|
322
347
|
/** Move the selected node down (swap with next sibling). */
|
|
323
348
|
function moveSelectionDown() {
|
|
324
349
|
const tab = activeTab.value;
|
|
325
|
-
if (!tab?.session.selection || tab.session.selection.length < 2)
|
|
350
|
+
if (!tab?.session.selection || tab.session.selection.length < 2) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
326
353
|
const sel = tab.session.selection;
|
|
327
354
|
const idx = childIndex(sel) as number;
|
|
328
355
|
const pPath = parentElementPath(sel) as JxPath;
|
|
329
356
|
const parentNode = getNodeAtPath(tab.doc.document, pPath);
|
|
330
|
-
const siblings = parentNode
|
|
331
|
-
if (
|
|
357
|
+
const siblings = childList(parentNode);
|
|
358
|
+
if (idx >= siblings.length - 1) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
332
361
|
transactDoc(tab, (t) => mutateMoveNode(t, sel, pPath, idx + 2));
|
|
333
362
|
tab.session.selection = [...pPath, "children", idx + 1];
|
|
334
363
|
}
|
|
335
364
|
|
|
336
365
|
/** Render the unified block action bar above the selected element. */
|
|
337
366
|
export function renderBlockActionBar() {
|
|
338
|
-
if (!_ctx)
|
|
367
|
+
if (!_ctx) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
339
370
|
if (!view.blockActionBarEl) {
|
|
340
371
|
view.blockActionBarEl = getLayerSlot("popover", "block-action-bar");
|
|
341
372
|
}
|
|
@@ -353,7 +384,7 @@ export function renderBlockActionBar() {
|
|
|
353
384
|
return;
|
|
354
385
|
}
|
|
355
386
|
|
|
356
|
-
const selection = tab.session
|
|
387
|
+
const { selection } = tab.session;
|
|
357
388
|
const activePanel = getActivePanel();
|
|
358
389
|
if (!activePanel) {
|
|
359
390
|
litRender(nothing, view.blockActionBarEl);
|
|
@@ -382,14 +413,11 @@ export function renderBlockActionBar() {
|
|
|
382
413
|
const isComponent =
|
|
383
414
|
node.tagName?.includes("-") &&
|
|
384
415
|
componentRegistry.some((/** @type {{ tagName: string }} */ c) => c.tagName === node.tagName);
|
|
416
|
+
const children = childList(node);
|
|
385
417
|
const isEmpty =
|
|
386
418
|
!node.textContent &&
|
|
387
|
-
(
|
|
388
|
-
|
|
389
|
-
(Array.isArray(node.children) &&
|
|
390
|
-
node.children.length === 1 &&
|
|
391
|
-
typeof node.children[0] === "object" &&
|
|
392
|
-
node.children[0]?.tagName === "br"));
|
|
419
|
+
(children.length === 0 ||
|
|
420
|
+
(children.length === 1 && typeof children[0] === "object" && children[0]?.tagName === "br"));
|
|
393
421
|
const convertTargets = !isComponent ? getConvertTargets(tag, isEmpty) : [];
|
|
394
422
|
const badgeInteractive = convertTargets.length > 0;
|
|
395
423
|
|
|
@@ -414,17 +442,19 @@ export function renderBlockActionBar() {
|
|
|
414
442
|
? html`<span
|
|
415
443
|
class="bar-drag-handle"
|
|
416
444
|
title="Drag to reorder"
|
|
417
|
-
${ref((
|
|
418
|
-
if (!
|
|
445
|
+
${ref((handleEl) => {
|
|
446
|
+
if (!handleEl) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
419
449
|
if (view.selDragCleanup) {
|
|
420
450
|
view.selDragCleanup();
|
|
421
451
|
view.selDragCleanup = null;
|
|
422
452
|
}
|
|
423
453
|
view.selDragCleanup = draggable({
|
|
424
|
-
element:
|
|
454
|
+
element: handleEl as HTMLElement,
|
|
425
455
|
getInitialData: () => ({
|
|
426
|
-
type: "tree-node",
|
|
427
456
|
path: activeTab.value?.session.selection,
|
|
457
|
+
type: "tree-node",
|
|
428
458
|
}),
|
|
429
459
|
});
|
|
430
460
|
})}
|
|
@@ -469,7 +499,7 @@ export function renderBlockActionBar() {
|
|
|
469
499
|
compact
|
|
470
500
|
emphasized
|
|
471
501
|
selects="multiple"
|
|
472
|
-
selected=${activeValues.length ? JSON.stringify(activeValues) : nothing}
|
|
502
|
+
selected=${activeValues.length > 0 ? JSON.stringify(activeValues) : nothing}
|
|
473
503
|
>
|
|
474
504
|
${actions.map(
|
|
475
505
|
(action) => html`
|
|
@@ -495,7 +525,9 @@ export function renderBlockActionBar() {
|
|
|
495
525
|
// Post-render side effects
|
|
496
526
|
requestAnimationFrame(() => {
|
|
497
527
|
const bar = view.blockActionBarEl?.firstElementChild as HTMLElement | null;
|
|
498
|
-
if (!bar)
|
|
528
|
+
if (!bar) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
499
531
|
// Clamp to window
|
|
500
532
|
const barRect = bar.getBoundingClientRect();
|
|
501
533
|
if (barRect.right > window.innerWidth) {
|