@jxsuite/studio 0.23.2 → 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 +2230 -714
  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} +109 -84
  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} +35 -23
  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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.23.2",
3
+ "version": "0.24.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -15,14 +15,15 @@
15
15
  ],
16
16
  "type": "module",
17
17
  "exports": {
18
- ".": "./src/studio.js",
19
- "./platform.js": "./src/platform.js"
18
+ ".": "./src/studio.ts",
19
+ "./platform": "./src/platform.ts",
20
+ "./types": "./src/types.ts"
20
21
  },
21
22
  "publishConfig": {
22
23
  "provenance": true
23
24
  },
24
25
  "scripts": {
25
- "build": "bun build ./src/studio.js --outdir dist --target browser --sourcemap=linked && bun build ./node_modules/monaco-editor/esm/vs/editor/editor.worker.js --outdir dist/workers --target browser --entry-naming editor.worker.js && bun build ./node_modules/monaco-editor/esm/vs/language/json/json.worker.js --outdir dist/workers --target browser --entry-naming json.worker.js && bun build ./node_modules/monaco-editor/esm/vs/language/typescript/ts.worker.js --outdir dist/workers --target browser --entry-naming ts.worker.js",
26
+ "build": "bun build ./src/studio.ts --outdir dist --target browser --sourcemap=linked && bun build ./node_modules/monaco-editor/esm/vs/editor/editor.worker.js --outdir dist/workers --target browser --entry-naming editor.worker.js && bun build ./node_modules/monaco-editor/esm/vs/language/json/json.worker.js --outdir dist/workers --target browser --entry-naming json.worker.js && bun build ./node_modules/monaco-editor/esm/vs/language/typescript/ts.worker.js --outdir dist/workers --target browser --entry-naming ts.worker.js",
26
27
  "gen:webdata": "bun run scripts/gen-webdata.js",
27
28
  "test": "bun test --isolate",
28
29
  "test:coverage": "bun test --isolate --coverage",
@@ -31,9 +32,9 @@
31
32
  "dependencies": {
32
33
  "@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
33
34
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
34
- "@jxsuite/parser": "^0.23.2",
35
- "@jxsuite/runtime": "^0.23.2",
36
- "@jxsuite/schema": "^0.23.2",
35
+ "@jxsuite/parser": "^0.24.0",
36
+ "@jxsuite/runtime": "^0.24.0",
37
+ "@jxsuite/schema": "^0.24.0",
37
38
  "@spectrum-web-components/accordion": "^1.12.1",
38
39
  "@spectrum-web-components/action-bar": "1.12.1",
39
40
  "@spectrum-web-components/action-button": "^1.12.1",
@@ -1,3 +1,4 @@
1
+ /// <reference lib="dom" />
1
2
  /**
2
3
  * Browse modal — fullscreen overlay for the project file browser (Manage view). Opens via toolbar
3
4
  * button; selecting a file closes the modal and opens it in the editor.
@@ -5,20 +6,18 @@
5
6
 
6
7
  import { html } from "lit-html";
7
8
  import { ref } from "lit-html/directives/ref.js";
8
- import { renderBrowse } from "./browse.js";
9
- import { openFileInTab } from "../files/files.js";
10
- import { openModal } from "../ui/layers.js";
9
+ import { renderBrowse } from "./browse";
10
+ import { openFileInTab } from "../files/files";
11
+ import { openModal } from "../ui/layers";
11
12
 
12
- /** @type {ReturnType<typeof openModal> | null} */
13
- let _handle = null;
13
+ let _handle: ReturnType<typeof openModal> | null = null;
14
14
 
15
- /** @type {((e: KeyboardEvent) => void) | null} */
16
- let _escHandler = null;
15
+ let _escHandler: ((e: KeyboardEvent) => void) | null = null;
17
16
 
18
17
  export function openBrowseModal() {
19
18
  if (_handle) return;
20
19
 
21
- _escHandler = (/** @type {KeyboardEvent} */ e) => {
20
+ _escHandler = (e: KeyboardEvent) => {
22
21
  if (e.key === "Escape") closeBrowseModal();
23
22
  };
24
23
  document.addEventListener("keydown", _escHandler, true);
@@ -37,8 +36,8 @@ export function openBrowseModal() {
37
36
  ${ref((el) => {
38
37
  if (el) {
39
38
  requestAnimationFrame(() => {
40
- renderBrowse(/** @type {HTMLElement} */ (el), {
41
- openFile: (/** @type {string} */ path) => {
39
+ renderBrowse(el as HTMLElement, {
40
+ openFile: (path: string) => {
42
41
  closeBrowseModal();
43
42
  openFileInTab(path);
44
43
  },
@@ -1,3 +1,4 @@
1
+ /// <reference lib="dom" />
1
2
  /**
2
3
  * Manage view — project-level file browser with grid and table views.
3
4
  *
@@ -9,18 +10,20 @@
9
10
  import { html, render as litRender, nothing } from "lit-html";
10
11
  import { ref } from "lit-html/directives/ref.js";
11
12
  import { repeat } from "lit-html/directives/repeat.js";
12
- import { getPlatform } from "../platform.js";
13
- import { projectState } from "../store.js";
14
- import { yamlDefault } from "../settings/schema-field-ui.js";
15
- import { invalidateMediaCache } from "../ui/media-picker.js";
16
- import { statusMessage } from "../panels/statusbar.js";
17
- import { componentRegistry } from "../files/components.js";
18
-
19
- /** @typedef {import("../files/components.js").ComponentEntry} ComponentEntry */
20
- import { showDialog, renderPopover } from "../ui/layers.js";
21
- import { renderComponentPreview } from "../panels/stylebook-panel.js";
13
+ import { getPlatform } from "../platform";
14
+ import { projectState } from "../store";
15
+ import { yamlDefault } from "../settings/schema-field-ui";
16
+ import { invalidateMediaCache } from "../ui/media-picker";
17
+ import { statusMessage } from "../panels/statusbar";
18
+ import { componentRegistry } from "../files/components";
19
+
20
+ import { showDialog, renderPopover } from "../ui/layers";
21
+ import { renderComponentPreview } from "../panels/stylebook-panel";
22
22
  import { renderNode, buildScope, setSkipServerFunctions } from "@jxsuite/runtime";
23
- import { loadMarkdown } from "../files/file-ops.js";
23
+ import { loadMarkdown } from "../files/file-ops";
24
+
25
+ import type { ComponentEntry } from "../files/components";
26
+ import type { ContentTypeDef } from "@jxsuite/schema/types";
24
27
 
25
28
  // ─── Category definitions ────────────────────────────────────────────────────
26
29
 
@@ -58,10 +61,8 @@ const MEDIA_EXTENSIONS = new Set([
58
61
 
59
62
  let activeCategory = "all";
60
63
  let searchQuery = "";
61
- /** @type {"grid" | "table"} */
62
- let viewMode = "grid";
63
- /** @type {{ name: string; path: string; type: string; category: string; ext: string }[]} */
64
- let fileCache = [];
64
+ let viewMode: "grid" | "table" = "grid";
65
+ let fileCache: { name: string; path: string; type: string; category: string; ext: string }[] = [];
65
66
  let loading = false;
66
67
  /** Track which projectDirs were used for the last load, so we re-scan when they change. */
67
68
  let lastProjectDirsKey = "";
@@ -69,7 +70,7 @@ let lastProjectDirsKey = "";
69
70
  // ─── Helpers ─────────────────────────────────────────────────────────────────
70
71
 
71
72
  /** @param {string} name */
72
- function extOf(name) {
73
+ function extOf(name: string) {
73
74
  const dot = name.lastIndexOf(".");
74
75
  return dot > 0 ? name.slice(dot).toLowerCase() : "";
75
76
  }
@@ -86,12 +87,12 @@ const IMAGE_EXTENSIONS = new Set([
86
87
  ]);
87
88
 
88
89
  /** @param {string} ext */
89
- function isImage(ext) {
90
+ function isImage(ext: string) {
90
91
  return IMAGE_EXTENSIONS.has(ext);
91
92
  }
92
93
 
93
94
  /** Map a file path to a display category. Media files override by extension. */
94
- function categoryFor(/** @type {string} */ dir, /** @type {string} */ ext) {
95
+ function categoryFor(dir: string, ext: string) {
95
96
  if (ext && MEDIA_EXTENSIONS.has(ext)) return "Media";
96
97
  if (dir.startsWith("pages")) return "Pages";
97
98
  if (dir.startsWith("layouts")) return "Layouts";
@@ -112,9 +113,11 @@ function categoryFor(/** @type {string} */ dir, /** @type {string} */ ext) {
112
113
  * { name: string; path: string; type: string; category: string; ext: string }[]
113
114
  * >}
114
115
  */
115
- async function collectFiles(dir, platform) {
116
- /** @type {{ name: string; path: string; type: string; category: string; ext: string }[]} */
117
- const results = [];
116
+ async function collectFiles(
117
+ dir: string,
118
+ platform: ReturnType<typeof getPlatform>,
119
+ ): Promise<{ name: string; path: string; type: string; category: string; ext: string }[]> {
120
+ const results: { name: string; path: string; type: string; category: string; ext: string }[] = [];
118
121
  try {
119
122
  const entries = await platform.listDirectory(dir);
120
123
  for (const entry of entries) {
@@ -148,11 +151,11 @@ async function collectFiles(dir, platform) {
148
151
  * @param {string} filePath
149
152
  * @returns {string | null}
150
153
  */
151
- function contentTypeFor(filePath) {
154
+ function contentTypeFor(filePath: string) {
152
155
  const config = projectState?.projectConfig;
153
156
  if (!config?.contentTypes) return null;
154
157
  for (const [name, def] of Object.entries(config.contentTypes)) {
155
- const d = /** @type {ContentTypeDef} */ (def);
158
+ const d = def as ContentTypeDef;
156
159
  if (!d.source) continue;
157
160
  const prefix = d.source.replace(/^\.\//, "").replace(/\/$/, "");
158
161
  if (filePath.startsWith(prefix + "/") || filePath === prefix) {
@@ -170,7 +173,7 @@ async function loadFiles() {
170
173
  const platform = getPlatform();
171
174
  const dirs = projectState.projectDirs || [];
172
175
  lastProjectDirsKey = dirs.join(",");
173
- const all = await Promise.all(dirs.map((/** @type {string} */ d) => collectFiles(d, platform)));
176
+ const all = await Promise.all(dirs.map((d: string) => collectFiles(d, platform)));
174
177
  fileCache = all.flat();
175
178
  fileCache.sort((a, b) => a.path.localeCompare(b.path));
176
179
  loading = false;
@@ -210,7 +213,7 @@ const ENTITY_TYPES = [
210
213
  * @param {string} contentTypeName
211
214
  * @returns {string}
212
215
  */
213
- function buildFrontmatterYaml(contentTypeName) {
216
+ function buildFrontmatterYaml(contentTypeName: string) {
214
217
  const config = projectState?.projectConfig;
215
218
  const col = config?.contentTypes?.[contentTypeName];
216
219
  if (!col?.schema?.properties) return "title: Untitled\n";
@@ -232,7 +235,7 @@ function getContentTypeTypes() {
232
235
  const config = projectState?.projectConfig;
233
236
  if (!config?.contentTypes) return [];
234
237
  return Object.entries(config.contentTypes).map(([name, def]) => {
235
- const d = /** @type {ContentTypeDef} */ (def);
238
+ const d = def as ContentTypeDef;
236
239
  const dir = d.source ? d.source.replace(/^\.\//, "").replace(/\/$/, "") : name;
237
240
  return {
238
241
  key: `contentType:${name}`,
@@ -251,7 +254,11 @@ function getContentTypeTypes() {
251
254
  * @param {HTMLElement} container
252
255
  * @param {{ openFile: (path: string) => void }} ctx
253
256
  */
254
- async function handleNewEntity(typeKey, container, ctx) {
257
+ async function handleNewEntity(
258
+ typeKey: string,
259
+ container: HTMLElement,
260
+ ctx: { openFile: (path: string) => void },
261
+ ) {
255
262
  const isContentType = typeKey.startsWith("contentType:");
256
263
  const contentTypeName = isContentType ? typeKey.slice("contentType:".length) : null;
257
264
  const allTypes = [...ENTITY_TYPES, ...getContentTypeTypes()];
@@ -304,7 +311,11 @@ const UPLOAD_ACCEPT = [
304
311
  * @param {HTMLElement} container
305
312
  * @param {{ openFile: (path: string) => void }} ctx
306
313
  */
307
- async function handleUpload(files, container, ctx) {
314
+ async function handleUpload(
315
+ files: FileList | File[],
316
+ container: HTMLElement,
317
+ ctx: { openFile: (path: string) => void },
318
+ ) {
308
319
  const platform = getPlatform();
309
320
  for (const file of files) {
310
321
  const destPath = `public/${file.name}`;
@@ -317,8 +328,7 @@ async function handleUpload(files, container, ctx) {
317
328
 
318
329
  // ─── Context menu ───────────────────────────────────────────────────────────
319
330
 
320
- /** @type {ReturnType<typeof renderPopover> | null} */
321
- let _browseCtxHandle = null;
331
+ let _browseCtxHandle: ReturnType<typeof renderPopover> | null = null;
322
332
 
323
333
  function dismissBrowseContextMenu() {
324
334
  if (_browseCtxHandle) {
@@ -333,7 +343,12 @@ function dismissBrowseContextMenu() {
333
343
  * @param {HTMLElement} container
334
344
  * @param {{ openFile: (path: string) => void }} ctx
335
345
  */
336
- function showBrowseContextMenu(e, file, container, ctx) {
346
+ function showBrowseContextMenu(
347
+ e: MouseEvent,
348
+ file: { name: string; path: string; ext: string },
349
+ container: HTMLElement,
350
+ ctx: { openFile: (path: string) => void },
351
+ ) {
337
352
  e.preventDefault();
338
353
  e.stopPropagation();
339
354
  dismissBrowseContextMenu();
@@ -368,7 +383,7 @@ function showBrowseContextMenu(e, file, container, ctx) {
368
383
  ${ref((el) => {
369
384
  if (!el) return;
370
385
  requestAnimationFrame(() => {
371
- const popover = /** @type {HTMLElement} */ (el);
386
+ const popover = el as HTMLElement;
372
387
  const menuRect = popover.getBoundingClientRect();
373
388
  if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
374
389
  if (y + menuRect.height > window.innerHeight)
@@ -408,7 +423,11 @@ function showBrowseContextMenu(e, file, container, ctx) {
408
423
  * @param {HTMLElement} container
409
424
  * @param {{ openFile: (path: string) => void }} ctx
410
425
  */
411
- async function browseRenameFile(file, container, ctx) {
426
+ async function browseRenameFile(
427
+ file: { name: string; path: string; ext: string },
428
+ container: HTMLElement,
429
+ ctx: { openFile: (path: string) => void },
430
+ ) {
412
431
  const newName = await showRenameDialog(file.name);
413
432
  if (!newName || newName === file.name) return;
414
433
  const filePath = file.path.replaceAll("\\", "/");
@@ -420,8 +439,8 @@ async function browseRenameFile(file, container, ctx) {
420
439
  invalidateBrowseCache();
421
440
  renderBrowse(container, ctx);
422
441
  statusMessage(`Renamed to ${newName}`);
423
- } catch (/** @type {unknown} */ e) {
424
- statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
442
+ } catch (e) {
443
+ statusMessage(`Error: ${(e as Error).message}`);
425
444
  }
426
445
  }
427
446
 
@@ -430,7 +449,11 @@ async function browseRenameFile(file, container, ctx) {
430
449
  * @param {HTMLElement} container
431
450
  * @param {{ openFile: (path: string) => void }} ctx
432
451
  */
433
- async function browseDuplicateFile(file, container, ctx) {
452
+ async function browseDuplicateFile(
453
+ file: { name: string; path: string; ext: string },
454
+ container: HTMLElement,
455
+ ctx: { openFile: (path: string) => void },
456
+ ) {
434
457
  const filePath = file.path.replaceAll("\\", "/");
435
458
  const parentDir = filePath.includes("/") ? filePath.substring(0, filePath.lastIndexOf("/")) : ".";
436
459
  const baseName = file.name.replace(/(\.[^.]+)$/, "");
@@ -444,8 +467,8 @@ async function browseDuplicateFile(file, container, ctx) {
444
467
  invalidateBrowseCache();
445
468
  renderBrowse(container, ctx);
446
469
  statusMessage(`Duplicated as ${copyName}`);
447
- } catch (/** @type {unknown} */ e) {
448
- statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
470
+ } catch (e) {
471
+ statusMessage(`Error: ${(e as Error).message}`);
449
472
  }
450
473
  }
451
474
 
@@ -454,7 +477,11 @@ async function browseDuplicateFile(file, container, ctx) {
454
477
  * @param {HTMLElement} container
455
478
  * @param {{ openFile: (path: string) => void }} ctx
456
479
  */
457
- async function browseDeleteFile(file, container, ctx) {
480
+ async function browseDeleteFile(
481
+ file: { name: string; path: string; ext: string },
482
+ container: HTMLElement,
483
+ ctx: { openFile: (path: string) => void },
484
+ ) {
458
485
  const confirmed = await showDeleteDialog(file.name);
459
486
  if (!confirmed) return;
460
487
  try {
@@ -463,8 +490,8 @@ async function browseDeleteFile(file, container, ctx) {
463
490
  invalidateBrowseCache();
464
491
  renderBrowse(container, ctx);
465
492
  statusMessage(`Deleted ${file.name}`);
466
- } catch (/** @type {unknown} */ e) {
467
- statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
493
+ } catch (e) {
494
+ statusMessage(`Error: ${(e as Error).message}`);
468
495
  }
469
496
  }
470
497
 
@@ -474,10 +501,10 @@ async function browseDeleteFile(file, container, ctx) {
474
501
  * @param {string} currentName
475
502
  * @returns {Promise<string | null>}
476
503
  */
477
- function showRenameDialog(currentName) {
504
+ function showRenameDialog(currentName: string): Promise<string | null> {
478
505
  let value = currentName;
479
506
 
480
- return showDialog((done) => {
507
+ return showDialog<string | null>((done) => {
481
508
  function confirm() {
482
509
  const trimmed = value.trim();
483
510
  if (!trimmed) return;
@@ -499,10 +526,10 @@ function showRenameDialog(currentName) {
499
526
  <sp-textfield
500
527
  style="width:100%"
501
528
  value=${value}
502
- @input=${(/** @type {Event} */ e) => {
503
- value = /** @type {HTMLInputElement} */ (e.target).value || "";
529
+ @input=${(e: Event) => {
530
+ value = (e.target as HTMLInputElement).value || "";
504
531
  }}
505
- @keydown=${(/** @type {KeyboardEvent} */ e) => {
532
+ @keydown=${(e: KeyboardEvent) => {
506
533
  if (e.key === "Enter") confirm();
507
534
  }}
508
535
  ></sp-textfield>
@@ -511,7 +538,7 @@ function showRenameDialog(currentName) {
511
538
 
512
539
  requestAnimationFrame(() => {
513
540
  const layer = document.getElementById("layer-dialog");
514
- const tf = /** @type {HTMLElement | null} */ (layer?.querySelector("sp-textfield"));
541
+ const tf = layer?.querySelector("sp-textfield") as HTMLElement | null;
515
542
  if (tf) {
516
543
  tf.focus();
517
544
  const input = tf.shadowRoot?.querySelector("input");
@@ -527,7 +554,7 @@ function showRenameDialog(currentName) {
527
554
  * @param {string} fileName
528
555
  * @returns {Promise<boolean>}
529
556
  */
530
- function showDeleteDialog(fileName) {
557
+ function showDeleteDialog(fileName: string) {
531
558
  return showDialog(
532
559
  (done) => html`
533
560
  <sp-dialog-wrapper
@@ -549,8 +576,7 @@ function showDeleteDialog(fileName) {
549
576
 
550
577
  // ─── Grid view helpers ──────────────────────────────────────────────────────
551
578
 
552
- /** @type {Map<string, HTMLElement>} */
553
- const _previewCache = new Map();
579
+ const _previewCache: Map<string, HTMLElement> = new Map();
554
580
 
555
581
  /**
556
582
  * Render a live preview for a page or layout file (JSON or Markdown).
@@ -558,7 +584,7 @@ const _previewCache = new Map();
558
584
  * @param {string} filePath
559
585
  * @returns {Promise<HTMLElement | null>}
560
586
  */
561
- async function renderDocPreview(filePath) {
587
+ async function renderDocPreview(filePath: string) {
562
588
  try {
563
589
  const platform = getPlatform();
564
590
  const content = await platform.readFile(filePath);
@@ -572,7 +598,7 @@ async function renderDocPreview(filePath) {
572
598
  }
573
599
  const scope = buildScope(doc.state || {});
574
600
  const el = renderNode(doc, scope);
575
- return /** @type {HTMLElement | null} */ (el);
601
+ return el as HTMLElement | null;
576
602
  } catch {
577
603
  return null;
578
604
  }
@@ -584,22 +610,18 @@ async function renderDocPreview(filePath) {
584
610
  * @param {Element} el — the .element-card-preview div
585
611
  * @param {{ path: string; category: string }} file
586
612
  */
587
- async function loadPreview(el, file) {
613
+ async function loadPreview(el: Element, file: { path: string; category: string }) {
588
614
  // Already populated
589
615
  if (el.firstElementChild) return;
590
616
 
591
- /** @type {HTMLElement | undefined} */
592
- let preview = _previewCache.get(file.path);
617
+ let preview: HTMLElement | undefined = _previewCache.get(file.path);
593
618
  if (!preview) {
594
619
  try {
595
- const comp = componentRegistry.find(
596
- (/** @type {ComponentEntry} */ c) => c.path === file.path,
597
- );
620
+ const comp = componentRegistry.find((c: ComponentEntry) => c.path === file.path);
598
621
  if (comp) {
599
- preview = /** @type {HTMLElement | undefined} */ (await renderComponentPreview(comp));
622
+ preview = (await renderComponentPreview(comp)) as HTMLElement | undefined;
600
623
  } else {
601
- preview = /** @type {HTMLElement | undefined} */ (await renderDocPreview(file.path)) ||
602
- undefined;
624
+ preview = ((await renderDocPreview(file.path)) as HTMLElement | undefined) || undefined;
603
625
  }
604
626
  if (preview) _previewCache.set(file.path, /** @type {HTMLElement} */ (preview));
605
627
  } catch {
@@ -616,7 +638,11 @@ async function loadPreview(el, file) {
616
638
  * @param {HTMLElement} container
617
639
  * @param {{ openFile: (path: string) => void }} ctx
618
640
  */
619
- function renderCard(file, container, ctx) {
641
+ function renderCard(
642
+ file: { name: string; path: string; type: string; category: string; ext: string },
643
+ container: HTMLElement,
644
+ ctx: { openFile: (path: string) => void },
645
+ ) {
620
646
  const isImg = isImage(file.ext);
621
647
  const needsPreview =
622
648
  file.category === "Components" ||
@@ -628,13 +654,12 @@ function renderCard(file, container, ctx) {
628
654
  <div
629
655
  class="element-card"
630
656
  @click=${() => ctx.openFile(file.path)}
631
- @contextmenu=${(/** @type {MouseEvent} */ e) =>
632
- showBrowseContextMenu(e, file, container, ctx)}
657
+ @contextmenu=${(e: MouseEvent) => showBrowseContextMenu(e, file, container, ctx)}
633
658
  >
634
659
  <div
635
660
  class="element-card-preview"
636
661
  ${needsPreview
637
- ? ref((/** @type {Element | undefined} */ el) => {
662
+ ? ref((el: Element | undefined) => {
638
663
  if (el) loadPreview(el, file);
639
664
  })
640
665
  : nothing}
@@ -661,7 +686,10 @@ function renderCard(file, container, ctx) {
661
686
  * @param {HTMLElement} container — the #canvas-wrap element
662
687
  * @param {{ openFile: (path: string) => void }} ctx — callbacks from studio.js
663
688
  */
664
- export async function renderBrowse(container, ctx) {
689
+ export async function renderBrowse(
690
+ container: HTMLElement,
691
+ ctx: { openFile: (path: string) => void },
692
+ ) {
665
693
  // Re-load when projectDirs changed (e.g. project opened after initial render)
666
694
  const currentKey = (projectState?.projectDirs || []).join(",");
667
695
  if ((!fileCache.length && !loading) || currentKey !== lastProjectDirsKey) {
@@ -693,11 +721,11 @@ export async function renderBrowse(container, ctx) {
693
721
  size="s"
694
722
  placeholder="Filter files..."
695
723
  .value=${searchQuery}
696
- @input=${(/** @type {Event} */ e) => {
697
- searchQuery = /** @type {HTMLInputElement} */ (e.target).value;
724
+ @input=${(e: Event) => {
725
+ searchQuery = (e.target as HTMLInputElement).value;
698
726
  renderBrowse(container, ctx);
699
727
  }}
700
- @submit=${(/** @type {Event} */ e) => e.preventDefault()}
728
+ @submit=${(e: Event) => e.preventDefault()}
701
729
  ></sp-search>
702
730
  <overlay-trigger placement="bottom-start" triggered-by="click">
703
731
  <sp-action-button size="s" slot="trigger">
@@ -705,8 +733,8 @@ export async function renderBrowse(container, ctx) {
705
733
  </sp-action-button>
706
734
  <sp-popover slot="click-content" tip>
707
735
  <sp-menu
708
- @change=${(/** @type {Event} */ e) =>
709
- handleNewEntity(/** @type {HTMLSelectElement} */ (e.target).value, container, ctx)}
736
+ @change=${(e: Event) =>
737
+ handleNewEntity((e.target as HTMLSelectElement).value, container, ctx)}
710
738
  >
711
739
  ${ENTITY_TYPES.map((t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`)}
712
740
  ${contentTypeTypes.length
@@ -720,9 +748,7 @@ export async function renderBrowse(container, ctx) {
720
748
  <sp-action-button
721
749
  size="s"
722
750
  @click=${() => {
723
- const input = /** @type {HTMLInputElement} */ (
724
- container.querySelector(".browse-upload-input")
725
- );
751
+ const input = container.querySelector(".browse-upload-input") as HTMLInputElement;
726
752
  if (input) input.click();
727
753
  }}
728
754
  >
@@ -734,8 +760,8 @@ export async function renderBrowse(container, ctx) {
734
760
  accept=${UPLOAD_ACCEPT}
735
761
  class="browse-upload-input"
736
762
  style="display:none"
737
- @change=${(/** @type {Event} */ e) => {
738
- const input = /** @type {HTMLInputElement} */ (e.target);
763
+ @change=${(e: Event) => {
764
+ const input = e.target as HTMLInputElement;
739
765
  if (input.files?.length) handleUpload(input.files, container, ctx);
740
766
  input.value = "";
741
767
  }}
@@ -790,8 +816,7 @@ export async function renderBrowse(container, ctx) {
790
816
  class="browse-row"
791
817
  style=${isImage(f.ext) ? "cursor:default" : ""}
792
818
  @click=${isImage(f.ext) ? nothing : () => ctx.openFile(f.path)}
793
- @contextmenu=${(/** @type {MouseEvent} */ e) =>
794
- showBrowseContextMenu(e, f, container, ctx)}
819
+ @contextmenu=${(e: MouseEvent) => showBrowseContextMenu(e, f, container, ctx)}
795
820
  >
796
821
  <sp-table-cell class="browse-name-cell"
797
822
  >${isImage(f.ext)
@@ -824,16 +849,16 @@ export async function renderBrowse(container, ctx) {
824
849
  const tpl = html`
825
850
  <div
826
851
  class="browse-view"
827
- @dragover=${(/** @type {DragEvent} */ e) => {
852
+ @dragover=${(e: DragEvent) => {
828
853
  e.preventDefault();
829
- /** @type {HTMLElement} */ (e.currentTarget).classList.add("browse-drop-active");
854
+ (e.currentTarget as HTMLElement).classList.add("browse-drop-active");
830
855
  }}
831
- @dragleave=${(/** @type {DragEvent} */ e) => {
832
- /** @type {HTMLElement} */ (e.currentTarget).classList.remove("browse-drop-active");
856
+ @dragleave=${(e: DragEvent) => {
857
+ (e.currentTarget as HTMLElement).classList.remove("browse-drop-active");
833
858
  }}
834
- @drop=${(/** @type {DragEvent} */ e) => {
859
+ @drop=${(e: DragEvent) => {
835
860
  e.preventDefault();
836
- /** @type {HTMLElement} */ (e.currentTarget).classList.remove("browse-drop-active");
861
+ (e.currentTarget as HTMLElement).classList.remove("browse-drop-active");
837
862
  const droppedFiles = e.dataTransfer?.files;
838
863
  if (droppedFiles?.length) handleUpload(droppedFiles, container, ctx);
839
864
  }}