@jxsuite/studio 0.23.1 → 0.24.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.
Files changed (105) hide show
  1. package/dist/studio.js +2231 -715
  2. package/dist/studio.js.map +121 -104
  3. package/package.json +8 -7
  4. package/src/browse/{browse-modal.js → browse-modal.ts} +9 -10
  5. package/src/browse/{browse.js → browse.ts} +110 -85
  6. package/src/canvas/{canvas-diff.js → canvas-diff.ts} +42 -35
  7. package/src/canvas/{canvas-helpers.js → canvas-helpers.ts} +25 -19
  8. package/src/canvas/{canvas-live-render.js → canvas-live-render.ts} +53 -63
  9. package/src/canvas/{canvas-render.js → canvas-render.ts} +110 -121
  10. package/src/canvas/{canvas-utils.js → canvas-utils.ts} +48 -44
  11. package/src/canvas/{nested-site-style.js → nested-site-style.ts} +3 -3
  12. package/src/editor/{component-inline-edit.js → component-inline-edit.ts} +45 -33
  13. package/src/editor/{content-inline-edit.js → content-inline-edit.ts} +27 -45
  14. package/src/editor/{context-menu.js → context-menu.ts} +38 -36
  15. package/src/editor/{convert-targets.js → convert-targets.ts} +15 -12
  16. package/src/editor/{convert-to-component.js → convert-to-component.ts} +25 -26
  17. package/src/editor/{convert-to-repeater.js → convert-to-repeater.ts} +60 -40
  18. package/src/editor/{inline-edit.js → inline-edit.ts} +91 -55
  19. package/src/editor/{inline-format.js → inline-format.ts} +46 -51
  20. package/src/editor/{insertion-helper.js → insertion-helper.ts} +47 -63
  21. package/src/editor/{shortcuts.js → shortcuts.ts} +45 -31
  22. package/src/editor/{slash-menu.js → slash-menu.ts} +34 -35
  23. package/src/files/{components.js → components.ts} +12 -15
  24. package/src/files/{file-ops.js → file-ops.ts} +36 -29
  25. package/src/files/{files.js → files.ts} +102 -84
  26. package/src/github/{github-auth.js → github-auth.ts} +3 -3
  27. package/src/github/{github-publish.js → github-publish.ts} +20 -18
  28. package/src/markdown/{md-allowlist.js → md-allowlist.ts} +5 -2
  29. package/src/markdown/{md-convert.js → md-convert.ts} +146 -169
  30. package/src/new-project/{new-project-modal.js → new-project-modal.ts} +16 -18
  31. package/src/panels/{activity-bar.js → activity-bar.ts} +26 -34
  32. package/src/panels/{ai-panel.js → ai-panel.ts} +30 -30
  33. package/src/panels/{block-action-bar.js → block-action-bar.ts} +64 -73
  34. package/src/panels/{canvas-dnd.js → canvas-dnd.ts} +44 -49
  35. package/src/panels/{data-explorer.js → data-explorer.ts} +21 -15
  36. package/src/panels/dnd.ts +417 -0
  37. package/src/panels/{editors.js → editors.ts} +43 -50
  38. package/src/panels/{elements-panel.js → elements-panel.ts} +30 -27
  39. package/src/panels/{events-panel.js → events-panel.ts} +33 -25
  40. package/src/panels/{git-panel.js → git-panel.ts} +97 -83
  41. package/src/panels/{head-panel.js → head-panel.ts} +117 -139
  42. package/src/panels/{imports-panel.js → imports-panel.ts} +69 -89
  43. package/src/panels/{layers-panel.js → layers-panel.ts} +61 -57
  44. package/src/panels/{left-panel.js → left-panel.ts} +56 -63
  45. package/src/panels/{overlays.js → overlays.ts} +28 -36
  46. package/src/panels/{panel-events.js → panel-events.ts} +53 -51
  47. package/src/panels/{preview-render.js → preview-render.ts} +20 -15
  48. package/src/panels/{properties-panel.js → properties-panel.ts} +212 -246
  49. package/src/panels/{pseudo-preview.js → pseudo-preview.ts} +9 -8
  50. package/src/panels/{quick-search.js → quick-search.ts} +16 -17
  51. package/src/panels/{right-panel.js → right-panel.ts} +41 -59
  52. package/src/panels/{shared.js → shared.ts} +7 -5
  53. package/src/panels/{signals-panel.js → signals-panel.ts} +322 -362
  54. package/src/panels/{statusbar.js → statusbar.ts} +14 -14
  55. package/src/panels/{style-inputs.js → style-inputs.ts} +66 -48
  56. package/src/panels/{style-panel.js → style-panel.ts} +235 -290
  57. package/src/panels/{style-utils.js → style-utils.ts} +32 -31
  58. package/src/panels/{stylebook-layers-panel.js → stylebook-layers-panel.ts} +22 -14
  59. package/src/panels/{stylebook-panel.js → stylebook-panel.ts} +127 -140
  60. package/src/panels/{tab-strip.js → tab-strip.ts} +12 -14
  61. package/src/panels/{toolbar.js → toolbar.ts} +68 -70
  62. package/src/panels/{welcome-screen.js → welcome-screen.ts} +14 -16
  63. package/src/{platform.js → platform.ts} +5 -3
  64. package/src/platforms/{devserver.js → devserver.ts} +53 -44
  65. package/src/{recent-projects.js → recent-projects.ts} +17 -12
  66. package/src/{resize-edges.js → resize-edges.ts} +18 -13
  67. package/src/services/{cem-export.js → cem-export.ts} +14 -7
  68. package/src/services/{code-services.js → code-services.ts} +29 -20
  69. package/src/services/{monaco-setup.js → monaco-setup.ts} +2 -3
  70. package/src/settings/{content-types-editor.js → content-types-editor.ts} +88 -67
  71. package/src/settings/{css-vars-editor.js → css-vars-editor.ts} +78 -51
  72. package/src/settings/{defs-editor.js → defs-editor.ts} +70 -56
  73. package/src/settings/{general-settings.js → general-settings.ts} +29 -28
  74. package/src/settings/{head-editor.js → head-editor.ts} +37 -35
  75. package/src/settings/{schema-field-ui.js → schema-field-ui.ts} +86 -76
  76. package/src/settings/{settings-modal.js → settings-modal.ts} +14 -16
  77. package/src/{site-context.js → site-context.ts} +35 -32
  78. package/src/{state.js → state.ts} +95 -78
  79. package/src/{store.js → store.ts} +60 -63
  80. package/src/{studio.js → studio.ts} +117 -119
  81. package/src/tabs/tab.ts +196 -0
  82. package/src/tabs/{transact.js → transact.ts} +107 -60
  83. package/src/types.ts +185 -0
  84. package/src/ui/{button-group.js → button-group.ts} +21 -21
  85. package/src/ui/{color-selector.js → color-selector.ts} +44 -37
  86. package/src/ui/{expression-editor.js → expression-editor.ts} +65 -40
  87. package/src/ui/{field-row.js → field-row.ts} +19 -2
  88. package/src/ui/{icons.js → icons.ts} +2 -2
  89. package/src/ui/{layers.js → layers.ts} +33 -24
  90. package/src/ui/{media-picker.js → media-picker.ts} +13 -9
  91. package/src/ui/{panel-resize.js → panel-resize.ts} +9 -4
  92. package/src/ui/{spectrum.js → spectrum.ts} +4 -4
  93. package/src/ui/{unit-selector.js → unit-selector.ts} +16 -19
  94. package/src/ui/{value-selector.js → value-selector.ts} +28 -28
  95. package/src/ui/{widgets.js → widgets.ts} +62 -15
  96. package/src/utils/{canvas-media.js → canvas-media.ts} +29 -14
  97. package/src/utils/{edit-display.js → edit-display.ts} +23 -20
  98. package/src/utils/{google-fonts.js → google-fonts.ts} +14 -13
  99. package/src/utils/{inherited-style.js → inherited-style.ts} +15 -10
  100. package/src/utils/{studio-utils.js → studio-utils.ts} +38 -26
  101. package/src/{view.js → view.ts} +53 -55
  102. package/src/workspace/{workspace.js → workspace.ts} +44 -33
  103. package/src/panels/dnd.js +0 -426
  104. package/src/tabs/tab.js +0 -192
  105. /package/src/{reactivity.js → reactivity.ts} +0 -0
@@ -1,14 +1,15 @@
1
+ /// <reference lib="dom" />
1
2
  /**
2
3
  * New Project modal — guides the user through creating a new Jx project. Fields mirror the CLI
3
4
  * scaffolder: name, description, url, adapter.
4
5
  */
5
6
 
6
7
  import { html } from "lit-html";
7
- import { openModal } from "../ui/layers.js";
8
- import { getPlatform } from "../platform.js";
8
+ import { openModal } from "../ui/layers";
9
+ import { getPlatform } from "../platform";
10
+ import type { ProjectConfig } from "@jxsuite/schema/types";
9
11
 
10
- /** @type {ReturnType<typeof openModal> | null} */
11
- let _handle = null;
12
+ let _handle: ReturnType<typeof openModal> | null = null;
12
13
 
13
14
  /**
14
15
  * @type {{
@@ -21,14 +22,12 @@ let _handle = null;
21
22
  */
22
23
  let _form = { name: "", description: "", url: "", adapter: "static", directory: "" };
23
24
 
24
- /** @type {string} */
25
- let _error = "";
25
+ let _error: string = "";
26
26
 
27
- /** @type {boolean} */
28
- let _creating = false;
27
+ let _creating: boolean = false;
29
28
 
30
29
  /** @type {((result: { root: string; config: object } | null) => void) | null} */
31
- let _resolve = null;
30
+ let _resolve: ((result: { root: string; config: ProjectConfig } | null) => void) | null = null;
32
31
 
33
32
  /**
34
33
  * Open the New Project modal. Returns a promise that resolves with the created project info (or
@@ -36,7 +35,7 @@ let _resolve = null;
36
35
  *
37
36
  * @returns {Promise<{ root: string; config: object } | null>}
38
37
  */
39
- export function openNewProjectModal() {
38
+ export function openNewProjectModal(): Promise<{ root: string; config: ProjectConfig } | null> {
40
39
  if (_handle) return Promise.resolve(null);
41
40
  _form = { name: "", description: "", url: "", adapter: "static", directory: "" };
42
41
  _error = "";
@@ -60,9 +59,8 @@ export function closeNewProjectModal() {
60
59
 
61
60
  function renderModal() {
62
61
  const onInput =
63
- (/** @type {"name" | "description" | "url" | "adapter" | "directory"} */ field) =>
64
- (/** @type {Event} */ e) => {
65
- _form[field] = /** @type {HTMLInputElement} */ (e.target).value;
62
+ (field: "name" | "description" | "url" | "adapter" | "directory") => (e: Event) => {
63
+ _form[field] = (e.target as HTMLInputElement).value;
66
64
  if (field === "name" && !_form.directory) {
67
65
  // Auto-derive directory slug from name while user hasn't manually typed one
68
66
  _dirDerived = true;
@@ -77,8 +75,8 @@ function renderModal() {
77
75
  renderModal();
78
76
  };
79
77
 
80
- const onAdapterChange = (/** @type {Event} */ e) => {
81
- _form.adapter = /** @type {HTMLInputElement} */ (e.target).value;
78
+ const onAdapterChange = (e: Event) => {
79
+ _form.adapter = (e.target as HTMLInputElement).value;
82
80
  renderModal();
83
81
  };
84
82
 
@@ -111,9 +109,9 @@ function renderModal() {
111
109
  _resolve(result);
112
110
  _resolve = null;
113
111
  }
114
- } catch (/** @type {unknown} */ e) {
112
+ } catch (e) {
115
113
  _creating = false;
116
- _error = /** @type {Error} */ (e).message;
114
+ _error = (e as Error).message;
117
115
  renderModal();
118
116
  }
119
117
  };
@@ -122,7 +120,7 @@ function renderModal() {
122
120
  <sp-underlay open @close=${closeNewProjectModal}></sp-underlay>
123
121
  <div
124
122
  class="new-project-modal"
125
- @keydown=${(/** @type {KeyboardEvent} */ e) => {
123
+ @keydown=${(e: KeyboardEvent) => {
126
124
  if (e.key === "Escape") closeNewProjectModal();
127
125
  }}
128
126
  >
@@ -1,15 +1,15 @@
1
+ /// <reference lib="dom" />
1
2
  /** Activity bar — tab icons for switching left panel views. */
2
3
 
3
4
  import { html, render as litRender, nothing } from "lit-html";
4
- import { activityBar, renderOnly } from "../store.js";
5
- import { effect, effectScope } from "../reactivity.js";
6
- import { activeTab } from "../workspace/workspace.js";
7
- import { view, applyPanelCollapse } from "../view.js";
8
- import { openSettingsModal } from "../settings/settings-modal.js";
9
- import { refreshGitStatus } from "./git-panel.js";
5
+ import { activityBar, renderOnly } from "../store";
6
+ import { effect, effectScope } from "../reactivity";
7
+ import { activeTab } from "../workspace/workspace";
8
+ import { view, applyPanelCollapse } from "../view";
9
+ import { openSettingsModal } from "../settings/settings-modal";
10
+ import { refreshGitStatus } from "./git-panel";
10
11
 
11
- /** @type {import("@vue/reactivity").EffectScope | null} */
12
- let _scope = null;
12
+ let _scope: import("@vue/reactivity").EffectScope | null = null;
13
13
 
14
14
  export function mount() {
15
15
  _scope = effectScope();
@@ -32,7 +32,7 @@ export function unmount() {
32
32
  _scope = null;
33
33
  }
34
34
 
35
- const gitBranchIcon = (/** @type {string} */ s) => html`
35
+ const gitBranchIcon = (s: string) => html`
36
36
  <svg
37
37
  slot="icon"
38
38
  xmlns="http://www.w3.org/2000/svg"
@@ -56,35 +56,27 @@ const gitBranchIcon = (/** @type {string} */ s) => html`
56
56
  * @param {string} tag
57
57
  * @param {string} [size]
58
58
  */
59
- export function tabIcon(tag, size) {
60
- /** @type {Record<string, (s: string) => import("lit-html").TemplateResult>} */
61
- const m = {
62
- "sp-icon-folder": (/** @type {string} */ s) =>
63
- html`<sp-icon-folder slot="icon" size=${s}></sp-icon-folder>`,
64
- "sp-icon-layers": (/** @type {string} */ s) =>
65
- html`<sp-icon-layers slot="icon" size=${s}></sp-icon-layers>`,
66
- "sp-icon-view-grid": (/** @type {string} */ s) =>
59
+ export function tabIcon(tag: string, size?: string) {
60
+ const m: Record<string, (s: string) => import("lit-html").TemplateResult> = {
61
+ "sp-icon-folder": (s: string) => html`<sp-icon-folder slot="icon" size=${s}></sp-icon-folder>`,
62
+ "sp-icon-layers": (s: string) => html`<sp-icon-layers slot="icon" size=${s}></sp-icon-layers>`,
63
+ "sp-icon-view-grid": (s: string) =>
67
64
  html`<sp-icon-view-grid slot="icon" size=${s}></sp-icon-view-grid>`,
68
- "sp-icon-brackets": (/** @type {string} */ s) =>
65
+ "sp-icon-brackets": (s: string) =>
69
66
  html`<sp-icon-brackets slot="icon" size=${s}></sp-icon-brackets>`,
70
- "sp-icon-data": (/** @type {string} */ s) =>
71
- html`<sp-icon-data slot="icon" size=${s}></sp-icon-data>`,
72
- "sp-icon-properties": (/** @type {string} */ s) =>
67
+ "sp-icon-data": (s: string) => html`<sp-icon-data slot="icon" size=${s}></sp-icon-data>`,
68
+ "sp-icon-properties": (s: string) =>
73
69
  html`<sp-icon-properties slot="icon" size=${s}></sp-icon-properties>`,
74
- "sp-icon-event": (/** @type {string} */ s) =>
75
- html`<sp-icon-event slot="icon" size=${s}></sp-icon-event>`,
76
- "sp-icon-brush": (/** @type {string} */ s) =>
77
- html`<sp-icon-brush slot="icon" size=${s}></sp-icon-brush>`,
78
- "sp-icon-file-single-web-page": (/** @type {string} */ s) =>
70
+ "sp-icon-event": (s: string) => html`<sp-icon-event slot="icon" size=${s}></sp-icon-event>`,
71
+ "sp-icon-brush": (s: string) => html`<sp-icon-brush slot="icon" size=${s}></sp-icon-brush>`,
72
+ "sp-icon-file-single-web-page": (s: string) =>
79
73
  html`<sp-icon-file-single-web-page slot="icon" size=${s}></sp-icon-file-single-web-page>`,
80
- "sp-icon-view-all-tags": (/** @type {string} */ s) =>
74
+ "sp-icon-view-all-tags": (s: string) =>
81
75
  html`<sp-icon-view-all-tags slot="icon" size=${s}></sp-icon-view-all-tags>`,
82
- "sp-icon-artboard": (/** @type {string} */ s) =>
76
+ "sp-icon-artboard": (s: string) =>
83
77
  html`<sp-icon-artboard slot="icon" size=${s}></sp-icon-artboard>`,
84
- "sp-icon-chat": (/** @type {string} */ s) =>
85
- html`<sp-icon-chat slot="icon" size=${s}></sp-icon-chat>`,
86
- "sp-icon-box": (/** @type {string} */ s) =>
87
- html`<sp-icon-box slot="icon" size=${s}></sp-icon-box>`,
78
+ "sp-icon-chat": (s: string) => html`<sp-icon-chat slot="icon" size=${s}></sp-icon-chat>`,
79
+ "sp-icon-box": (s: string) => html`<sp-icon-box slot="icon" size=${s}></sp-icon-box>`,
88
80
  "sp-icon-git-branch": gitBranchIcon,
89
81
  };
90
82
  const fn = m[tag];
@@ -110,8 +102,8 @@ export function renderActivityBar() {
110
102
  selected=${view.leftPanelCollapsed ? "" : leftTab}
111
103
  direction="vertical"
112
104
  quiet
113
- @change=${(/** @type {Event} */ e) => {
114
- const clicked = /** @type {HTMLElement & { selected: string }} */ (e.target).selected;
105
+ @change=${(e: Event) => {
106
+ const clicked = (e.target as HTMLElement & { selected: string }).selected;
115
107
  if (clicked === view.leftTab && !view.leftPanelCollapsed) {
116
108
  view.leftPanelCollapsed = true;
117
109
  applyPanelCollapse();
@@ -1,3 +1,4 @@
1
+ /// <reference lib="dom" />
1
2
  /**
2
3
  * Ai-panel.js — Claude AI Assistant tab for the right panel
3
4
  *
@@ -7,8 +8,8 @@
7
8
  import { html, nothing } from "lit-html";
8
9
  import { ref } from "lit-html/directives/ref.js";
9
10
  import quikchat from "quikchat/md";
10
- import { getPlatform } from "../platform.js";
11
- import { reloadFileInTab } from "../files/files.js";
11
+ import { getPlatform } from "../platform";
12
+ import { reloadFileInTab } from "../files/files";
12
13
 
13
14
  // ─── State (module-level, persists across tab switches) ─────────────────────
14
15
 
@@ -19,27 +20,25 @@ import { reloadFileInTab } from "../files/files.js";
19
20
  * toolUse?: { tool: string; input?: Record<string, unknown> };
20
21
  * }[]}
21
22
  */
22
- let messages = [];
23
+ let messages: {
24
+ role: string;
25
+ content: string;
26
+ toolUse?: { tool: string; input?: Record<string, unknown> };
27
+ }[] = [];
23
28
  let streaming = false;
24
- let sessionId = /** @type {string | null} */ (null);
25
- let authStatus = /** @type {"authenticated" | "unauthenticated" | "checking" | "unknown"} */ (
26
- "unknown"
27
- );
29
+ let sessionId = null as string | null;
30
+ let authStatus = "unknown" as "authenticated" | "unauthenticated" | "checking" | "unknown";
28
31
  let authError = "";
29
32
  let currentAssistantText = "";
30
- let eventSource = /** @type {EventSource | null} */ (null);
33
+ let eventSource = null as EventSource | null;
31
34
  let mounted = false;
32
35
 
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);
36
+ let chatInstance: any = null;
37
+ let chatContainerEl: Element | null = null;
38
+ let _quikChatEl: HTMLElement | null = null;
39
+ let currentStreamMsgId = null as number | null;
40
40
  let streamStarted = false;
41
- /** @type {Set<string>} */
42
- let pendingFileReloads = new Set();
41
+ let pendingFileReloads: Set<string> = new Set();
43
42
 
44
43
  // ─── Lifecycle ──────────────────────────────────────────────────────────────
45
44
 
@@ -49,15 +48,16 @@ export function mountAiPanel() {
49
48
  checkAuth();
50
49
  }
51
50
 
51
+ const _g = globalThis as unknown as { __jxRightPanelRender?: { render: () => void } };
52
+
52
53
  function rerenderPanel() {
53
- const { render } = /** @type {any} */ (/** @type {any} */ (globalThis).__jxRightPanelRender) ||
54
- {};
54
+ const { render } = _g.__jxRightPanelRender || {};
55
55
  if (render) render();
56
56
  requestAnimationFrame(() => mountQuikChat());
57
57
  }
58
58
 
59
- export function registerRightPanelRender(/** @type {Function} */ fn) {
60
- /** @type {any} */ (globalThis).__jxRightPanelRender = { render: fn };
59
+ export function registerRightPanelRender(fn: () => void) {
60
+ _g.__jxRightPanelRender = { render: fn };
61
61
  }
62
62
 
63
63
  // ─── QuikChat Mount ────────────────────────────────────────────────────────
@@ -69,7 +69,7 @@ export function mountQuikChat() {
69
69
 
70
70
  chatInstance = new quikchat(
71
71
  container,
72
- (/** @type {any} */ _chat, /** @type {string} */ msg) => {
72
+ (_chat: any, msg: string) => {
73
73
  handleUserSend(msg);
74
74
  },
75
75
  {
@@ -130,7 +130,7 @@ async function checkAuth() {
130
130
  // ─── Messaging ──────────────────────────────────────────────────────────────
131
131
 
132
132
  /** @param {string} text */
133
- async function handleUserSend(text) {
133
+ async function handleUserSend(text: string) {
134
134
  if (!text.trim() || streaming) return;
135
135
 
136
136
  messages.push({ role: "user", content: text });
@@ -151,7 +151,7 @@ async function handleUserSend(text) {
151
151
  if (!sessionId) {
152
152
  const result = await plat.aiCreateSession({ message: text });
153
153
  sessionId = result.id;
154
- await connectStream(/** @type {string} */ (sessionId));
154
+ await connectStream(sessionId!);
155
155
  } else {
156
156
  await plat.aiSendMessage(sessionId, text);
157
157
  }
@@ -195,7 +195,7 @@ function newChat() {
195
195
 
196
196
  // ─── SSE Stream ─────────────────────────────────────────────────────────────
197
197
 
198
- async function connectStream(/** @type {string} */ id) {
198
+ async function connectStream(id: string) {
199
199
  disconnectStream();
200
200
  const plat = getPlatform();
201
201
  const url = await Promise.resolve(plat.aiStreamUrl(id));
@@ -245,7 +245,7 @@ async function connectStream(/** @type {string} */ id) {
245
245
 
246
246
  eventSource.addEventListener("error", (e) => {
247
247
  try {
248
- const data = JSON.parse(/** @type {MessageEvent} */ (e).data);
248
+ const data = JSON.parse((e as MessageEvent).data);
249
249
  if (data.error) {
250
250
  if (chatInstance && currentStreamMsgId != null) {
251
251
  chatInstance.messageReplaceContent(currentStreamMsgId, `Error: ${data.error}`);
@@ -288,7 +288,7 @@ function disconnectStream() {
288
288
  }
289
289
 
290
290
  /** @param {any} data */
291
- function handleAssistantMessage(data) {
291
+ function handleAssistantMessage(data: any) {
292
292
  const content = data.message?.content || data.content;
293
293
  if (!content) return;
294
294
 
@@ -332,7 +332,7 @@ function handleAssistantMessage(data) {
332
332
  * @param {string} tool
333
333
  * @param {Record<string, unknown>} [input]
334
334
  */
335
- function formatToolLabel(tool, input) {
335
+ function formatToolLabel(tool: string, input?: Record<string, unknown>) {
336
336
  switch (tool) {
337
337
  case "Edit":
338
338
  case "Write":
@@ -351,7 +351,7 @@ function formatToolLabel(tool, input) {
351
351
  }
352
352
 
353
353
  /** @param {string} s @param {number} max */
354
- function truncate(s, max) {
354
+ function truncate(s: string, max: number) {
355
355
  return s.length > max ? s.slice(0, max) + "…" : s;
356
356
  }
357
357
 
@@ -398,7 +398,7 @@ export function renderAiPanelTemplate() {
398
398
  <div
399
399
  id="ai-quikchat"
400
400
  ${ref((el) => {
401
- _quikChatEl = /** @type {HTMLElement | null} */ (el || null);
401
+ _quikChatEl = (el as HTMLElement | null) || null;
402
402
  })}
403
403
  ></div>
404
404
  </div>
@@ -1,3 +1,4 @@
1
+ /// <reference lib="dom" />
1
2
  /**
2
3
  * Block action bar — extracted from studio.js (Phase 4h). Floating toolbar above selected elements
3
4
  * with parent selector, move arrows, drag handle, component actions, and inline formatting.
@@ -8,26 +9,21 @@ import { styleMap } from "lit-html/directives/style-map.js";
8
9
  import { ref } from "lit-html/directives/ref.js";
9
10
  import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
10
11
 
11
- import { getNodeAtPath, nodeLabel, parentElementPath, childIndex } from "../store.js";
12
- import { activeTab } from "../workspace/workspace.js";
13
- import { transactDoc, mutateMoveNode, mutateUpdateProperty } from "../tabs/transact.js";
14
- import { view } from "../view.js";
15
- import { isEditing, getActiveElement, getInlineActions } from "../editor/inline-edit.js";
16
- import { toggleInlineFormat, isTagActiveInSelection } from "../editor/inline-format.js";
17
- import { componentRegistry } from "../files/components.js";
18
- import { convertToComponent } from "../editor/convert-to-component.js";
19
- import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
20
- import { getLayerSlot } from "../ui/layers.js";
21
- import { showSlashMenu } from "../editor/slash-menu.js";
22
- import { getConvertTargets } from "../editor/convert-targets.js";
23
-
24
- /**
25
- * @typedef {import("../state.js").StudioState} StudioState
26
- *
27
- * @typedef {import("../state.js").JxPath} JxPath
28
- *
29
- * @typedef {import("../editor/inline-edit.js").InlineAction} InlineAction
30
- */
12
+ import { getNodeAtPath, nodeLabel, parentElementPath, childIndex } from "../store";
13
+ import { activeTab } from "../workspace/workspace";
14
+ import { transactDoc, mutateMoveNode, mutateUpdateProperty } from "../tabs/transact";
15
+ import { view } from "../view";
16
+ import { isEditing, getActiveElement, getInlineActions } from "../editor/inline-edit";
17
+ import type { InlineAction } from "../editor/inline-edit";
18
+ import { toggleInlineFormat, isTagActiveInSelection } from "../editor/inline-format";
19
+ import { componentRegistry } from "../files/components";
20
+ import { convertToComponent } from "../editor/convert-to-component";
21
+ import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers";
22
+ import { getLayerSlot } from "../ui/layers";
23
+ import { showSlashMenu } from "../editor/slash-menu";
24
+ import { getConvertTargets } from "../editor/convert-targets";
25
+
26
+ import type { JxPath } from "../state";
31
27
 
32
28
  /**
33
29
  * @type {{
@@ -35,7 +31,8 @@ import { getConvertTargets } from "../editor/convert-targets.js";
35
31
  * navigateToComponent: (path: string) => void;
36
32
  * } | null}
37
33
  */
38
- let _ctx = null;
34
+ let _ctx: { getCanvasMode: () => string; navigateToComponent: (path: string) => void } | null =
35
+ null;
39
36
 
40
37
  /**
41
38
  * Initialize the block action bar module.
@@ -45,12 +42,15 @@ let _ctx = null;
45
42
  * navigateToComponent: (path: string) => void;
46
43
  * }} ctx
47
44
  */
48
- export function initBlockActionBar(ctx) {
45
+ export function initBlockActionBar(ctx: {
46
+ getCanvasMode: () => string;
47
+ navigateToComponent: (path: string) => void;
48
+ }) {
49
49
  _ctx = ctx;
50
50
  }
51
51
 
52
52
  /** Pre-built icon templates for inline format buttons (avoids unsafeStatic) */
53
- const formatIconMap = /** @type {Record<string, import("lit-html").TemplateResult>} */ ({
53
+ const formatIconMap = {
54
54
  "sp-icon-text-bold": html`<sp-icon-text-bold slot="icon"></sp-icon-text-bold>`,
55
55
  "sp-icon-text-italic": html`<sp-icon-text-italic slot="icon"></sp-icon-text-italic>`,
56
56
  "sp-icon-text-underline": html`<sp-icon-text-underline slot="icon"></sp-icon-text-underline>`,
@@ -63,17 +63,17 @@ const formatIconMap = /** @type {Record<string, import("lit-html").TemplateResul
63
63
  "sp-icon-text-subscript": html`<sp-icon-text-subscript slot="icon"></sp-icon-text-subscript>`,
64
64
  "sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
65
65
  "sp-icon-link": html`<sp-icon-link slot="icon"></sp-icon-link>`,
66
- });
66
+ } as Record<string, import("lit-html").TemplateResult>;
67
67
 
68
68
  /**
69
69
  * Prevent the bar from stealing focus from contenteditable
70
70
  *
71
71
  * @param {MouseEvent} e
72
72
  */
73
- function onBarMousedown(e) {
74
- if (/** @type {HTMLElement} */ (e.target).closest("sp-textfield")) return;
75
- if (/** @type {HTMLElement} */ (e.target).closest(".bar-drag-handle")) return;
76
- if (/** @type {HTMLElement} */ (e.target).closest(".bar-tag--interactive")) return;
73
+ function onBarMousedown(e: MouseEvent) {
74
+ if ((e.target as HTMLElement).closest("sp-textfield")) return;
75
+ if ((e.target as HTMLElement).closest(".bar-drag-handle")) return;
76
+ if ((e.target as HTMLElement).closest(".bar-tag--interactive")) return;
77
77
  e.preventDefault();
78
78
  }
79
79
 
@@ -82,9 +82,13 @@ function onBarMousedown(e) {
82
82
  * @param {import("../editor/convert-targets.js").SlashCommand[]} targets
83
83
  * @param {JxPath} selection
84
84
  */
85
- function onTagBadgeClick(e, targets, selection) {
85
+ function onTagBadgeClick(
86
+ e: MouseEvent,
87
+ targets: import("../editor/convert-targets.js").SlashCommand[],
88
+ selection: JxPath,
89
+ ) {
86
90
  e.stopPropagation();
87
- const anchorEl = /** @type {HTMLElement} */ (e.currentTarget);
91
+ const anchorEl = e.currentTarget as HTMLElement;
88
92
  showSlashMenu(anchorEl, "", {
89
93
  showFilter: targets.length > 6,
90
94
  commands: targets,
@@ -106,24 +110,18 @@ function captureSelectionRange() {
106
110
  * @param {MouseEvent} e
107
111
  * @param {InlineAction} action
108
112
  */
109
- function onFormatClick(e, action) {
113
+ function onFormatClick(e: MouseEvent, action: InlineAction) {
110
114
  e.stopPropagation();
111
115
  if (action.command === "link") {
112
- showLinkPopover(
113
- /** @type {HTMLElement} */ (
114
- /** @type {HTMLElement} */ (e.target).closest("sp-action-button")
115
- ),
116
- );
116
+ showLinkPopover((e.target as HTMLElement).closest("sp-action-button") as HTMLElement);
117
117
  } else if (view.savedRange) {
118
118
  const sel = window.getSelection();
119
119
  const anchor = view.savedRange.startContainer;
120
120
  const editableRoot = (
121
- anchor?.nodeType === Node.ELEMENT_NODE
122
- ? /** @type {Element} */ (anchor)
123
- : anchor?.parentElement
121
+ anchor?.nodeType === Node.ELEMENT_NODE ? (anchor as Element) : anchor?.parentElement
124
122
  )?.closest("[contenteditable]");
125
123
  if (editableRoot) {
126
- /** @type {HTMLElement} */ (editableRoot).focus();
124
+ (editableRoot as HTMLElement).focus();
127
125
  sel?.removeAllRanges();
128
126
  sel?.addRange(view.savedRange);
129
127
  applyInlineFormat(action);
@@ -142,9 +140,9 @@ function renderParentSelector() {
142
140
  size="xs"
143
141
  quiet
144
142
  title="Select parent: ${nodeLabel(parentNode)}"
145
- @click=${(/** @type {MouseEvent} */ e) => {
143
+ @click=${(e: MouseEvent) => {
146
144
  e.stopPropagation();
147
- activeTab.value.session.selection = pPath;
145
+ activeTab.value!.session.selection = pPath;
148
146
  }}
149
147
  >
150
148
  <sp-icon-back slot="icon"></sp-icon-back>
@@ -156,9 +154,9 @@ function renderMoveArrows() {
156
154
  const tab = activeTab.value;
157
155
  if (!tab?.session.selection) return nothing;
158
156
  const sel = tab.session.selection;
159
- const idx = /** @type {number} */ (childIndex(sel));
157
+ const idx = childIndex(sel) as number;
160
158
  const pPath = parentElementPath(sel);
161
- const parentNode = getNodeAtPath(tab.doc.document, /** @type {JxPath} */ (pPath));
159
+ const parentNode = pPath ? getNodeAtPath(tab.doc.document, pPath) : null;
162
160
  const siblings = parentNode?.children;
163
161
  return html`
164
162
  <sp-action-button
@@ -166,7 +164,7 @@ function renderMoveArrows() {
166
164
  quiet
167
165
  title="Move up"
168
166
  ?disabled=${idx <= 0}
169
- @click=${(/** @type {MouseEvent} */ e) => {
167
+ @click=${(e: MouseEvent) => {
170
168
  e.stopPropagation();
171
169
  moveSelectionUp();
172
170
  }}
@@ -178,7 +176,7 @@ function renderMoveArrows() {
178
176
  quiet
179
177
  title="Move down"
180
178
  ?disabled=${!siblings || idx >= siblings.length - 1}
181
- @click=${(/** @type {MouseEvent} */ e) => {
179
+ @click=${(e: MouseEvent) => {
182
180
  e.stopPropagation();
183
181
  moveSelectionDown();
184
182
  }}
@@ -193,9 +191,8 @@ function renderMoveArrows() {
193
191
  *
194
192
  * @param {InlineAction} action
195
193
  */
196
- function applyInlineFormat(action) {
197
- /** @type {Record<string, string>} */
198
- const cmdToTag = {
194
+ function applyInlineFormat(action: InlineAction) {
195
+ const cmdToTag: Record<string, string> = {
199
196
  bold: "strong",
200
197
  italic: "em",
201
198
  underline: "u",
@@ -225,22 +222,17 @@ export function dismissBlockActionBar() {
225
222
  }
226
223
 
227
224
  /** @param {HTMLElement} anchorBtn */
228
- function showLinkPopover(anchorBtn) {
225
+ function showLinkPopover(anchorBtn: HTMLElement) {
229
226
  const host = getLayerSlot("popover", "link-popover");
230
227
  litRender(nothing, host);
231
228
 
232
229
  const sel = window.getSelection();
233
- /** @type {HTMLAnchorElement | null} */
234
- let existingLink = null;
230
+ let existingLink: HTMLAnchorElement | null = null;
235
231
  if (sel?.rangeCount) {
236
- /** @type {Node | null} */
237
- let node = sel.anchorNode;
232
+ let node: Node | null = sel.anchorNode;
238
233
  while (node && node !== document.body) {
239
- if (
240
- node.nodeType === Node.ELEMENT_NODE &&
241
- /** @type {Element} */ (node).tagName.toLowerCase() === "a"
242
- ) {
243
- existingLink = /** @type {HTMLAnchorElement} */ (node);
234
+ if (node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName.toLowerCase() === "a") {
235
+ existingLink = node as HTMLAnchorElement;
244
236
  break;
245
237
  }
246
238
  node = node.parentNode;
@@ -249,8 +241,7 @@ function showLinkPopover(anchorBtn) {
249
241
 
250
242
  const rect = anchorBtn.getBoundingClientRect();
251
243
 
252
- /** @type {HTMLInputElement | null} */
253
- let _linkField = null;
244
+ let _linkField: HTMLInputElement | null = null;
254
245
 
255
246
  const onApply = () => {
256
247
  const url = _linkField?.value || "";
@@ -272,7 +263,7 @@ function showLinkPopover(anchorBtn) {
272
263
  renderBlockActionBar();
273
264
  };
274
265
 
275
- const onKeydown = (/** @type {KeyboardEvent} */ e) => {
266
+ const onKeydown = (e: KeyboardEvent) => {
276
267
  if (e.key === "Enter") onApply();
277
268
  else if (e.key === "Escape") {
278
269
  litRender(nothing, host);
@@ -298,8 +289,8 @@ function showLinkPopover(anchorBtn) {
298
289
  value=${existingLink?.getAttribute("href") || ""}
299
290
  @keydown=${onKeydown}
300
291
  ${ref((el) => {
301
- _linkField = /** @type {HTMLInputElement | null} */ (el || null);
302
- if (el) requestAnimationFrame(() => /** @type {HTMLElement} */ (el).focus());
292
+ _linkField = (el as HTMLInputElement | null) || null;
293
+ if (el) requestAnimationFrame(() => (el as HTMLElement).focus());
303
294
  })}
304
295
  ></sp-textfield>
305
296
  <sp-action-button size="xs" @click=${onApply}>
@@ -319,9 +310,9 @@ function moveSelectionUp() {
319
310
  const tab = activeTab.value;
320
311
  if (!tab?.session.selection || tab.session.selection.length < 2) return;
321
312
  const sel = tab.session.selection;
322
- const idx = /** @type {number} */ (childIndex(sel));
313
+ const idx = childIndex(sel) as number;
323
314
  if (idx <= 0) return;
324
- const pPath = /** @type {JxPath} */ (parentElementPath(sel));
315
+ const pPath = parentElementPath(sel) as JxPath;
325
316
  transactDoc(tab, (t) => mutateMoveNode(t, sel, pPath, idx - 1));
326
317
  tab.session.selection = [...pPath, "children", idx - 1];
327
318
  }
@@ -331,8 +322,8 @@ function moveSelectionDown() {
331
322
  const tab = activeTab.value;
332
323
  if (!tab?.session.selection || tab.session.selection.length < 2) return;
333
324
  const sel = tab.session.selection;
334
- const idx = /** @type {number} */ (childIndex(sel));
335
- const pPath = /** @type {JxPath} */ (parentElementPath(sel));
325
+ const idx = childIndex(sel) as number;
326
+ const pPath = parentElementPath(sel) as JxPath;
336
327
  const parentNode = getNodeAtPath(tab.doc.document, pPath);
337
328
  const siblings = parentNode?.children;
338
329
  if (!siblings || idx >= siblings.length - 1) return;
@@ -412,7 +403,7 @@ export function renderBlockActionBar() {
412
403
  <span
413
404
  class="bar-tag${badgeInteractive ? " bar-tag--interactive" : ""}"
414
405
  @click=${badgeInteractive
415
- ? (/** @type {MouseEvent} */ e) => onTagBadgeClick(e, convertTargets, selection)
406
+ ? (e: MouseEvent) => onTagBadgeClick(e, convertTargets, selection)
416
407
  : nothing}
417
408
  >${node.$id || (node.tagName ?? "div")}</span
418
409
  >
@@ -428,7 +419,7 @@ export function renderBlockActionBar() {
428
419
  view.selDragCleanup = null;
429
420
  }
430
421
  view.selDragCleanup = draggable({
431
- element: /** @type {HTMLElement} */ (el),
422
+ element: el as HTMLElement,
432
423
  getInitialData: () => ({
433
424
  type: "tree-node",
434
425
  path: activeTab.value?.session.selection,
@@ -455,7 +446,7 @@ export function renderBlockActionBar() {
455
446
  size="xs"
456
447
  quiet
457
448
  title="Edit Component"
458
- @click=${() => _ctx?.navigateToComponent(/** @type {string} */ (comp?.path))}
449
+ @click=${() => _ctx?.navigateToComponent(comp?.path as string)}
459
450
  ><sp-icon-edit slot="icon" size="xs"></sp-icon-edit
460
451
  ></sp-action-button>`;
461
452
  }
@@ -485,7 +476,7 @@ export function renderBlockActionBar() {
485
476
  value=${action.tag}
486
477
  title="${action.label}${action.shortcut ? ` (${action.shortcut})` : ""}"
487
478
  @mousedown=${captureSelectionRange}
488
- @click=${(/** @type {MouseEvent} */ e) => onFormatClick(e, action)}
479
+ @click=${(e: MouseEvent) => onFormatClick(e, action)}
489
480
  >
490
481
  ${action.icon ? (formatIconMap[action.icon] ?? nothing) : nothing}
491
482
  </sp-action-button>
@@ -501,7 +492,7 @@ export function renderBlockActionBar() {
501
492
 
502
493
  // Post-render side effects
503
494
  requestAnimationFrame(() => {
504
- const bar = /** @type {HTMLElement | null} */ (view.blockActionBarEl?.firstElementChild);
495
+ const bar = view.blockActionBarEl?.firstElementChild as HTMLElement | null;
505
496
  if (!bar) return;
506
497
  // Clamp to window
507
498
  const barRect = bar.getBoundingClientRect();