@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.
Files changed (104) hide show
  1. package/dist/studio.js +86144 -83971
  2. package/dist/studio.js.map +229 -219
  3. package/package.json +5 -5
  4. package/src/browse/browse-modal.ts +9 -3
  5. package/src/browse/browse.ts +139 -82
  6. package/src/canvas/canvas-diff.ts +35 -18
  7. package/src/canvas/canvas-helpers.ts +55 -21
  8. package/src/canvas/canvas-live-render.ts +140 -89
  9. package/src/canvas/canvas-render.ts +127 -82
  10. package/src/canvas/canvas-utils.ts +68 -42
  11. package/src/canvas/nested-site-style.ts +15 -7
  12. package/src/editor/component-inline-edit.ts +90 -51
  13. package/src/editor/content-inline-edit.ts +75 -72
  14. package/src/editor/context-menu.ts +125 -71
  15. package/src/editor/convert-targets.ts +17 -13
  16. package/src/editor/convert-to-component.ts +51 -28
  17. package/src/editor/convert-to-repeater.ts +44 -19
  18. package/src/editor/inline-edit.ts +107 -52
  19. package/src/editor/inline-format.ts +128 -60
  20. package/src/editor/insertion-helper.ts +26 -14
  21. package/src/editor/shortcuts.ts +89 -43
  22. package/src/editor/slash-menu.ts +41 -26
  23. package/src/files/components.ts +9 -7
  24. package/src/files/file-ops.ts +53 -33
  25. package/src/files/files.ts +246 -143
  26. package/src/format/constraints.ts +25 -15
  27. package/src/format/format-host.ts +33 -10
  28. package/src/github/github-auth.ts +24 -14
  29. package/src/github/github-publish.ts +20 -14
  30. package/src/new-project/new-project-modal.ts +27 -18
  31. package/src/panels/activity-bar.ts +27 -26
  32. package/src/panels/ai-panel.ts +100 -44
  33. package/src/panels/block-action-bar.ts +84 -52
  34. package/src/panels/canvas-dnd.ts +47 -28
  35. package/src/panels/data-explorer.ts +24 -11
  36. package/src/panels/dnd.ts +148 -100
  37. package/src/panels/editors.ts +54 -37
  38. package/src/panels/elements-panel.ts +23 -13
  39. package/src/panels/events-panel.ts +48 -38
  40. package/src/panels/git-panel.ts +95 -60
  41. package/src/panels/head-panel.ts +138 -84
  42. package/src/panels/imports-panel.ts +66 -29
  43. package/src/panels/layers-panel.ts +80 -36
  44. package/src/panels/left-panel.ts +86 -53
  45. package/src/panels/overlays.ts +36 -19
  46. package/src/panels/panel-events.ts +48 -27
  47. package/src/panels/panel-scheduler.ts +41 -23
  48. package/src/panels/preview-render.ts +26 -26
  49. package/src/panels/properties-panel.ts +155 -118
  50. package/src/panels/pseudo-preview.ts +31 -15
  51. package/src/panels/quick-search.ts +28 -13
  52. package/src/panels/right-panel.ts +35 -24
  53. package/src/panels/shared.ts +46 -29
  54. package/src/panels/signals-panel.ts +303 -186
  55. package/src/panels/statusbar.ts +27 -12
  56. package/src/panels/style-inputs.ts +30 -24
  57. package/src/panels/style-panel.ts +130 -90
  58. package/src/panels/style-utils.ts +63 -23
  59. package/src/panels/stylebook-layers-panel.ts +27 -30
  60. package/src/panels/stylebook-panel.ts +140 -72
  61. package/src/panels/tab-strip.ts +21 -10
  62. package/src/panels/toolbar.ts +52 -40
  63. package/src/panels/welcome-screen.ts +1 -1
  64. package/src/platform.ts +2 -1
  65. package/src/platforms/devserver.ts +221 -105
  66. package/src/recent-projects.ts +15 -7
  67. package/src/resize-edges.ts +7 -5
  68. package/src/services/cem-export.ts +73 -57
  69. package/src/services/code-services.ts +52 -25
  70. package/src/services/monaco-setup.ts +5 -5
  71. package/src/settings/content-types-editor.ts +133 -62
  72. package/src/settings/css-vars-editor.ts +31 -14
  73. package/src/settings/defs-editor.ts +125 -63
  74. package/src/settings/general-settings.ts +11 -6
  75. package/src/settings/head-editor.ts +30 -15
  76. package/src/settings/schema-field-ui.ts +75 -35
  77. package/src/settings/settings-modal.ts +35 -17
  78. package/src/site-context.ts +80 -47
  79. package/src/state.ts +98 -65
  80. package/src/store.ts +42 -23
  81. package/src/studio.ts +165 -144
  82. package/src/tabs/tab.ts +51 -45
  83. package/src/tabs/transact.ts +230 -109
  84. package/src/types.ts +60 -51
  85. package/src/ui/button-group.ts +3 -2
  86. package/src/ui/color-selector.ts +33 -15
  87. package/src/ui/expression-editor.ts +139 -86
  88. package/src/ui/field-input.ts +35 -18
  89. package/src/ui/field-row.ts +1 -1
  90. package/src/ui/icons.ts +5 -6
  91. package/src/ui/layers.ts +33 -26
  92. package/src/ui/media-picker.ts +13 -7
  93. package/src/ui/panel-resize.ts +39 -20
  94. package/src/ui/spectrum.ts +4 -2
  95. package/src/ui/unit-selector.ts +20 -11
  96. package/src/ui/value-selector.ts +13 -7
  97. package/src/ui/widgets.ts +34 -18
  98. package/src/utils/canvas-media.ts +66 -33
  99. package/src/utils/edit-display.ts +43 -44
  100. package/src/utils/google-fonts.ts +10 -6
  101. package/src/utils/inherited-style.ts +22 -8
  102. package/src/utils/studio-utils.ts +88 -45
  103. package/src/view.ts +9 -5
  104. package/src/workspace/workspace.ts +49 -29
@@ -33,17 +33,29 @@ let currentAssistantText = "";
33
33
  let eventSource = null as EventSource | null;
34
34
  let mounted = false;
35
35
 
36
- let chatInstance: any = null;
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
- let pendingFileReloads: Set<string> = new Set();
51
+ const pendingFileReloads = new Set<string>();
42
52
 
43
53
  // ─── Lifecycle ──────────────────────────────────────────────────────────────
44
54
 
45
55
  export function mountAiPanel() {
46
- if (mounted) return;
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) 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) return;
70
- if (chatInstance && chatContainerEl === container) return;
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: any, msg: string) => {
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.inputAreaSetEnabled(false);
107
+ chatInstance?.inputAreaSetEnabled(false);
90
108
  }
91
109
  }
92
110
 
93
111
  function replayMessages() {
94
- if (!chatInstance || !messages.length) return;
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 (err) {
145
+ } catch (error) {
126
146
  authStatus = "unauthenticated";
127
- authError = String(err);
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) return;
156
+ if (!text.trim() || streaming) {
157
+ return;
158
+ }
137
159
 
138
- messages.push({ role: "user", content: text });
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 (err) {
182
+ } catch (error) {
161
183
  if (chatInstance && currentStreamMsgId != null) {
162
- chatInstance.messageReplaceContent(currentStreamMsgId, `Error: ${err}`);
184
+ chatInstance.messageReplaceContent(currentStreamMsgId, `Error: ${error}`);
163
185
  }
164
- messages.push({ role: "assistant", content: `Error: ${err}` });
186
+ messages.push({ content: `Error: ${error}`, role: "assistant" });
165
187
  streaming = false;
166
- if (chatInstance) chatInstance.inputAreaSetEnabled(true);
188
+ if (chatInstance) {
189
+ chatInstance.inputAreaSetEnabled(true);
190
+ }
167
191
  rerenderPanel();
168
192
  }
169
193
  }
170
194
 
171
195
  function stop() {
172
- if (!sessionId) return;
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.onerror = () => {
287
+ eventSource.addEventListener("error", () => {
262
288
  finishStream();
263
- };
289
+ });
264
290
  }
265
291
 
266
292
  function finishStream() {
267
- if (!streaming) return;
293
+ if (!streaming) {
294
+ return;
295
+ }
268
296
  if (currentAssistantText) {
269
- messages.push({ role: "assistant", content: currentAssistantText });
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) chatInstance.inputAreaSetEnabled(true);
303
+ if (chatInstance) {
304
+ chatInstance.inputAreaSetEnabled(true);
305
+ }
276
306
 
277
- if (pendingFileReloads.size) {
278
- for (const fp of pendingFileReloads) reloadFileInTab(fp);
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
- /** @param {any} data */
293
- function handleAssistantMessage(data: any) {
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) return;
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) pendingFileReloads.add(String(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", content: currentAssistantText });
364
+ messages.push({ content: currentAssistantText, role: "assistant" });
315
365
  }
316
366
  for (const t of toolBlocks) {
317
- messages.push({ role: "tool", content: "", toolUse: t });
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 = !!text;
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
- case "Read":
392
+ }
393
+ case "Read": {
343
394
  return `📖 Read: ${input?.file_path || input?.path || "file"}`;
344
- case "Bash":
395
+ }
396
+ case "Bash": {
345
397
  return `⚡ Run: ${truncate(String(input?.command || ""), 50)}`;
346
- case "Glob":
398
+ }
399
+ case "Glob": {
347
400
  return `🔍 Glob: ${input?.pattern || ""}`;
348
- case "Grep":
401
+ }
402
+ case "Grep": {
349
403
  return `🔍 Grep: ${truncate(String(input?.pattern || ""), 40)}`;
350
- default:
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) + "…" : s;
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, childIndex } from "../store";
12
+ import { childIndex, childList, getNodeAtPath, nodeLabel, parentElementPath } from "../store";
13
13
  import { activeTab } from "../workspace/workspace";
14
- import { transactDoc, mutateMoveNode, mutateUpdateProperty } from "../tabs/transact";
14
+ import { mutateMoveNode, mutateUpdateProperty, transactDoc } from "../tabs/transact";
15
15
  import { view } from "../view";
16
- import { isEditing, getActiveElement, getInlineActions } from "../editor/inline-edit";
16
+ import { getActiveElement, getInlineActions, isEditing } from "../editor/inline-edit";
17
17
  import type { InlineAction } from "../editor/inline-edit";
18
- import { toggleInlineFormat, isTagActiveInSelection } from "../editor/inline-format";
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-subscript": html`<sp-icon-text-subscript slot="icon"></sp-icon-text-subscript>`,
66
- "sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
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")) return;
77
- if ((e.target as HTMLElement).closest(".bar-drag-handle")) return;
78
- if ((e.target as HTMLElement).closest(".bar-tag--interactive")) return;
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) view.savedRange = sel.getRangeAt(0).cloneRange();
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) return nothing;
142
+ if (!tab?.session.selection) {
143
+ return nothing;
144
+ }
137
145
  const pPath = parentElementPath(tab.session.selection);
138
- if (!pPath) return nothing;
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) return nothing;
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?.children;
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
- code: "code",
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) litRender(nothing, 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) return;
274
+ if (!existingLink?.parentNode) {
275
+ return;
276
+ }
261
277
  const frag = document.createDocumentFragment();
262
- while (existingLink.firstChild) frag.appendChild(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") onApply();
270
- else if (e.key === "Escape") {
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) requestAnimationFrame(() => (el as HTMLElement).focus());
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) return;
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) return;
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) return;
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?.children;
331
- if (!siblings || idx >= siblings.length - 1) return;
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) return;
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.selection;
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
- (!node.children ||
388
- node.children.length === 0 ||
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((el) => {
418
- if (!el) return;
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: el as HTMLElement,
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) return;
528
+ if (!bar) {
529
+ return;
530
+ }
499
531
  // Clamp to window
500
532
  const barRect = bar.getBoundingClientRect();
501
533
  if (barRect.right > window.innerWidth) {