@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
@@ -1,3 +1,5 @@
1
+ /// <reference lib="dom" />
2
+ /// <reference lib="dom.iterable" />
1
3
  /**
2
4
  * File tree management — project loading, file tree rendering, and file CRUD.
3
5
  *
@@ -10,14 +12,14 @@ import { classMap } from "lit-html/directives/class-map.js";
10
12
  import { ref } from "lit-html/directives/ref.js";
11
13
  import { unified } from "unified";
12
14
  import remarkStringify from "remark-stringify";
13
- import { renderPopover, showDialog, showConfirmDialog } from "../ui/layers.js";
15
+ import { renderPopover, showDialog, showConfirmDialog } from "../ui/layers";
14
16
  import remarkDirective from "remark-directive";
15
17
  import { stringify as stringifyYaml } from "yaml";
16
- import { jxToMd } from "../markdown/md-convert.js";
17
- import { createState, projectState, setProjectState, requireProjectState } from "../store.js";
18
- import { getPlatform } from "../platform.js";
19
- import { statusMessage } from "../panels/statusbar.js";
20
- import { loadComponentRegistry } from "./components.js";
18
+ import { jxToMd } from "../markdown/md-convert";
19
+ import { createState, projectState, setProjectState, requireProjectState } from "../store";
20
+ import { getPlatform } from "../platform";
21
+ import { statusMessage } from "../panels/statusbar";
22
+ import { loadComponentRegistry } from "./components";
21
23
  import {
22
24
  draggable,
23
25
  dropTargetForElements,
@@ -31,25 +33,27 @@ import {
31
33
  renameTab,
32
34
  replaceAllTabs,
33
35
  activeTab,
34
- } from "../workspace/workspace.js";
35
- import { loadMarkdown } from "./file-ops.js";
36
- import { view } from "../view.js";
37
- import { addRecentProject, trackRecentFile } from "../recent-projects.js";
36
+ } from "../workspace/workspace";
37
+ import { loadMarkdown } from "./file-ops";
38
+ import { view } from "../view";
39
+ import { addRecentProject, trackRecentFile } from "../recent-projects";
40
+
41
+ import type { JxElement } from "@jxsuite/schema/types";
38
42
 
39
43
  // ─── File icon map ────────────────────────────────────────────────────────────
40
44
 
41
- const fileIconMap = /** @type {Record<string, import("lit-html").TemplateResult>} */ ({
45
+ const fileIconMap = {
42
46
  "sp-icon-folder-open": html`<sp-icon-folder-open></sp-icon-folder-open>`,
43
47
  "sp-icon-folder": html`<sp-icon-folder></sp-icon-folder>`,
44
48
  "sp-icon-file-code": html`<sp-icon-file-code></sp-icon-file-code>`,
45
49
  "sp-icon-file-txt": html`<sp-icon-file-txt></sp-icon-file-txt>`,
46
50
  "sp-icon-image": html`<sp-icon-image></sp-icon-image>`,
47
51
  "sp-icon-document": html`<sp-icon-document></sp-icon-document>`,
48
- });
52
+ } as Record<string, import("lit-html").TemplateResult>;
49
53
 
50
54
  // ─── File management ──────────────────────────────────────────────────────────
51
55
 
52
- export async function loadDirectory(/** @type {string} */ dirPath) {
56
+ export async function loadDirectory(dirPath: string) {
53
57
  if (!projectState) return;
54
58
  try {
55
59
  const platform = getPlatform();
@@ -102,7 +106,13 @@ export async function loadProject() {
102
106
  * renderLeftPanel: () => void;
103
107
  * }} ctx
104
108
  */
105
- export async function openProject({ renderActivityBar, renderLeftPanel }) {
109
+ export async function openProject({
110
+ renderActivityBar,
111
+ renderLeftPanel,
112
+ }: {
113
+ renderActivityBar: () => void;
114
+ renderLeftPanel: () => void;
115
+ }) {
106
116
  try {
107
117
  const platform = getPlatform();
108
118
  const result = await platform.openProject();
@@ -155,8 +165,8 @@ export async function openProject({ renderActivityBar, renderLeftPanel }) {
155
165
  statusMessage(`Opened project: ${requireProjectState().name}`);
156
166
 
157
167
  await openHomePage();
158
- } catch (/** @type {unknown} */ e) {
159
- statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
168
+ } catch (e) {
169
+ statusMessage(`Error: ${(e as Error).message}`);
160
170
  }
161
171
  }
162
172
 
@@ -174,7 +184,7 @@ export async function openHomePage() {
174
184
 
175
185
  // ─── File tree templates ──────────────────────────────────────────────────────
176
186
 
177
- function fileTypeIconTpl(/** @type {string} */ name, /** @type {string} */ type) {
187
+ function fileTypeIconTpl(name: string, type: string) {
178
188
  let tag;
179
189
  if (type === "directory") {
180
190
  tag = projectState?.expanded?.has(name) ? "sp-icon-folder-open" : "sp-icon-folder";
@@ -223,6 +233,10 @@ export function renderFilesTemplate({
223
233
  openProject: openProjectFn,
224
234
  openFileFromTree: openFileFn,
225
235
  renderLeftPanel,
236
+ }: {
237
+ openProject: () => void;
238
+ openFileFromTree: (path: string) => void;
239
+ renderLeftPanel: () => void;
226
240
  }) {
227
241
  if (!projectState) {
228
242
  return html`<div class="file-tree-empty">No project loaded</div>`;
@@ -273,11 +287,11 @@ export function renderFilesTemplate({
273
287
  quiet
274
288
  placeholder="Filter files…"
275
289
  value=${requireProjectState().searchQuery}
276
- @input=${(/** @type {Event} */ e) => {
277
- requireProjectState().searchQuery = /** @type {HTMLInputElement} */ (e.target).value;
290
+ @input=${(e: Event) => {
291
+ requireProjectState().searchQuery = (e.target as HTMLInputElement).value;
278
292
  renderLeftPanel();
279
293
  }}
280
- @submit=${(/** @type {Event} */ e) => e.preventDefault()}
294
+ @submit=${(e: Event) => e.preventDefault()}
281
295
  ></sp-search>
282
296
  </div>
283
297
  <div class="file-tree" role="tree" aria-label="Project files">
@@ -288,10 +302,10 @@ export function renderFilesTemplate({
288
302
 
289
303
  /** @returns {import("lit-html").TemplateResult | import("lit-html").TemplateResult[]} */
290
304
  function renderTreeLevelTemplate(
291
- /** @type {string} */ dirPath,
292
- /** @type {number} */ depth,
293
- /** @type {{ openFileFn: (path: string) => void; renderLeftPanel: () => void }} */ ctx,
294
- ) {
305
+ dirPath: string,
306
+ depth: number,
307
+ ctx: { openFileFn: (path: string) => void; renderLeftPanel: () => void },
308
+ ): import("lit-html").TemplateResult | import("lit-html").TemplateResult[] {
295
309
  const entries = requireProjectState().dirs.get(dirPath);
296
310
  if (!entries) {
297
311
  loadDirectory(dirPath).then(() => ctx.renderLeftPanel());
@@ -329,7 +343,7 @@ function renderTreeLevelTemplate(
329
343
  data-path=${entry.path}
330
344
  data-type=${entry.type}
331
345
  aria-expanded=${isDir ? String(isExpanded) : nothing}
332
- @click=${async (/** @type {MouseEvent} */ e) => {
346
+ @click=${async (e: MouseEvent) => {
333
347
  e.stopPropagation();
334
348
  if (isDir) {
335
349
  if (isExpanded) requireProjectState().expanded.delete(entry.path);
@@ -342,7 +356,7 @@ function renderTreeLevelTemplate(
342
356
  ctx.openFileFn(entry.path);
343
357
  }
344
358
  }}
345
- @contextmenu=${(/** @type {MouseEvent} */ e) => {
359
+ @contextmenu=${(e: MouseEvent) => {
346
360
  e.preventDefault();
347
361
  e.stopPropagation();
348
362
  showFileContextMenu(e, entry, ctx);
@@ -361,10 +375,10 @@ function renderTreeLevelTemplate(
361
375
  });
362
376
  }
363
377
 
364
- export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
365
- tree.addEventListener("keydown", (/** @type {KeyboardEvent} */ e) => {
366
- const items = /** @type {HTMLElement[]} */ ([...tree.querySelectorAll(".file-tree-item")]);
367
- const focused = /** @type {HTMLElement | null} */ (tree.querySelector(".file-tree-item:focus"));
378
+ export function setupTreeKeyboard(tree: HTMLElement) {
379
+ tree.addEventListener("keydown", (e: KeyboardEvent) => {
380
+ const items = [...tree.querySelectorAll(".file-tree-item")] as HTMLElement[];
381
+ const focused = tree.querySelector(".file-tree-item:focus") as HTMLElement | null;
368
382
  if (!focused || items.length === 0) return;
369
383
 
370
384
  const idx = items.indexOf(focused);
@@ -379,22 +393,20 @@ export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
379
393
  break;
380
394
  case "ArrowRight":
381
395
  if (focused.dataset.type === "directory") {
382
- const path = /** @type {string} */ (focused.dataset.path);
396
+ const path = focused.dataset.path as string;
383
397
  if (!requireProjectState().expanded.has(path)) {
384
398
  requireProjectState().expanded.add(path);
385
399
  loadDirectory(path).then(() => {
386
400
  const panel = tree.closest(".panel-body");
387
401
  if (panel)
388
- /** @type {HTMLElement | null} */ (
389
- panel.querySelector(".file-tree-item:focus")
390
- )?.click();
402
+ (panel.querySelector(".file-tree-item:focus") as HTMLElement | null)?.click();
391
403
  });
392
404
  }
393
405
  }
394
406
  break;
395
407
  case "ArrowLeft":
396
408
  if (focused.dataset.type === "directory") {
397
- const path = /** @type {string} */ (focused.dataset.path);
409
+ const path = focused.dataset.path as string;
398
410
  if (requireProjectState().expanded.has(path)) {
399
411
  requireProjectState().expanded.delete(path);
400
412
  // renderLeftPanel will be called by the caller who sets up keyboard
@@ -415,24 +427,23 @@ export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
415
427
  if (first) first.setAttribute("tabindex", "0");
416
428
  }
417
429
 
418
- /** @type {(() => void)[]} */
419
- let _fileTreeDndCleanups = [];
430
+ let _fileTreeDndCleanups: (() => void)[] = [];
420
431
 
421
432
  /**
422
433
  * Register drag-and-drop on file tree items. Called after each file tree render.
423
434
  *
424
435
  * @param {{ renderLeftPanel: () => void }} ctx
425
436
  */
426
- export function registerFileTreeDnD({ renderLeftPanel }) {
437
+ export function registerFileTreeDnD({ renderLeftPanel }: { renderLeftPanel: () => void }) {
427
438
  // Clean up previous registrations
428
439
  for (const fn of _fileTreeDndCleanups) fn();
429
440
  _fileTreeDndCleanups = [];
430
441
 
431
442
  requestAnimationFrame(() => {
432
- const tree = /** @type {HTMLElement | null} */ (document.querySelector(".file-tree"));
443
+ const tree = document.querySelector(".file-tree") as HTMLElement | null;
433
444
  if (!tree) return;
434
445
 
435
- const items = /** @type {NodeListOf<HTMLElement>} */ (tree.querySelectorAll(".file-tree-item"));
446
+ const items = tree.querySelectorAll(".file-tree-item") as NodeListOf<HTMLElement>;
436
447
 
437
448
  items.forEach((row) => {
438
449
  const path = row.dataset.path;
@@ -460,7 +471,7 @@ export function registerFileTreeDnD({ renderLeftPanel }) {
460
471
  element: row,
461
472
  canDrop({ source }) {
462
473
  if (source.data.type !== "file-tree") return false;
463
- const srcPath = /** @type {string} */ (source.data.path);
474
+ const srcPath = source.data.path as string;
464
475
  if (srcPath === path) return false;
465
476
  if (srcPath.startsWith(path + "/")) return false;
466
477
  const srcParent = parentDir(srcPath);
@@ -494,7 +505,7 @@ export function registerFileTreeDnD({ renderLeftPanel }) {
494
505
  element: tree,
495
506
  canDrop({ source }) {
496
507
  if (source.data.type !== "file-tree") return false;
497
- const srcPath = /** @type {string} */ (source.data.path);
508
+ const srcPath = source.data.path as string;
498
509
  return parentDir(srcPath) !== ".";
499
510
  },
500
511
  onDragEnter() {
@@ -520,14 +531,14 @@ export function registerFileTreeDnD({ renderLeftPanel }) {
520
531
  if (source.data.type !== "file-tree") return;
521
532
  if (target.data.type !== "file-tree-target") return;
522
533
 
523
- const srcPath = /** @type {string} */ (source.data.path);
524
- const targetDirPath = /** @type {string} */ (target.data.targetDir);
534
+ const srcPath = source.data.path as string;
535
+ const targetDirPath = target.data.targetDir as string;
525
536
  const fileName = srcPath.split("/").pop();
526
537
  const newPath = targetDirPath === "." ? fileName : `${targetDirPath}/${fileName}`;
527
538
 
528
539
  if (newPath === srcPath) return;
529
540
 
530
- moveFileEntry(srcPath, /** @type {string} */ (newPath), renderLeftPanel);
541
+ moveFileEntry(srcPath, newPath!, renderLeftPanel);
531
542
  },
532
543
  });
533
544
  _fileTreeDndCleanups.push(monitorCleanup);
@@ -541,7 +552,7 @@ export function registerFileTreeDnD({ renderLeftPanel }) {
541
552
  * @param {string} newPath
542
553
  * @param {() => void} renderLeftPanel
543
554
  */
544
- async function moveFileEntry(oldPath, newPath, renderLeftPanel) {
555
+ async function moveFileEntry(oldPath: string, newPath: string, renderLeftPanel: () => void) {
545
556
  const platform = getPlatform();
546
557
  try {
547
558
  await platform.renameFile(oldPath, newPath);
@@ -567,13 +578,13 @@ async function moveFileEntry(oldPath, newPath, renderLeftPanel) {
567
578
 
568
579
  renderLeftPanel();
569
580
  statusMessage(`Moved to ${newPath}`);
570
- } catch (/** @type {unknown} */ e) {
571
- statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
581
+ } catch (e) {
582
+ statusMessage(`Error: ${(e as Error).message}`);
572
583
  }
573
584
  }
574
585
 
575
586
  /** @param {string} path @returns {string} */
576
- function parentDir(path) {
587
+ function parentDir(path: string) {
577
588
  const normalized = path.replaceAll("\\", "/");
578
589
  const lastSlash = normalized.lastIndexOf("/");
579
590
  return lastSlash === -1 ? "." : normalized.substring(0, lastSlash);
@@ -581,8 +592,7 @@ function parentDir(path) {
581
592
 
582
593
  // ─── Context menu ─────────────────────────────────────────────────────────────
583
594
 
584
- /** @type {ReturnType<typeof renderPopover> | null} */
585
- let _fileCtxHandle = null;
595
+ let _fileCtxHandle: ReturnType<typeof renderPopover> | null = null;
586
596
 
587
597
  function dismissFileContextMenu() {
588
598
  if (_fileCtxHandle) {
@@ -592,9 +602,9 @@ function dismissFileContextMenu() {
592
602
  }
593
603
 
594
604
  function showFileContextMenu(
595
- /** @type {MouseEvent} */ e,
596
- /** @type {{ name: string; path: string; type: string }} */ entry,
597
- /** @type {{ openFileFn: (path: string) => void; renderLeftPanel: () => void }} */ ctx,
605
+ e: MouseEvent,
606
+ entry: { name: string; path: string; type: string },
607
+ ctx: { openFileFn: (path: string) => void; renderLeftPanel: () => void },
598
608
  ) {
599
609
  e.preventDefault();
600
610
  dismissFileContextMenu();
@@ -630,7 +640,7 @@ function showFileContextMenu(
630
640
  ${ref((el) => {
631
641
  if (!el) return;
632
642
  requestAnimationFrame(() => {
633
- const popover = /** @type {HTMLElement} */ (el);
643
+ const popover = el as HTMLElement;
634
644
  const menuRect = popover.getBoundingClientRect();
635
645
  if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
636
646
  if (y + menuRect.height > window.innerHeight)
@@ -666,7 +676,7 @@ function showFileContextMenu(
666
676
 
667
677
  // ─── File CRUD ────────────────────────────────────────────────────────────────
668
678
 
669
- async function createNewFile(dirPath = ".", /** @type {() => void} */ renderLeftPanel) {
679
+ async function createNewFile(dirPath = ".", renderLeftPanel: () => void) {
670
680
  const name = prompt("File name:", "untitled.json");
671
681
  if (!name) return;
672
682
  const path = dirPath === "." ? name : `${dirPath}/${name}`;
@@ -679,15 +689,15 @@ async function createNewFile(dirPath = ".", /** @type {() => void} */ renderLeft
679
689
  await loadDirectory(dirPath);
680
690
  renderLeftPanel();
681
691
  statusMessage(`Created ${path}`);
682
- } catch (/** @type {unknown} */ e) {
683
- statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
692
+ } catch (e) {
693
+ statusMessage(`Error: ${(e as Error).message}`);
684
694
  }
685
695
  }
686
696
 
687
697
  /** @param {string} currentName @returns {Promise<string | null>} */
688
- function showRenameFileDialog(currentName) {
698
+ function showRenameFileDialog(currentName: string): Promise<string | null> {
689
699
  let value = currentName;
690
- return showDialog((done) => {
700
+ return showDialog<string | null>((done) => {
691
701
  function confirm() {
692
702
  const trimmed = value.trim();
693
703
  if (!trimmed) return;
@@ -708,10 +718,10 @@ function showRenameFileDialog(currentName) {
708
718
  <sp-textfield
709
719
  style="width:100%"
710
720
  value=${currentName}
711
- @input=${(/** @type {Event} */ e) => {
712
- value = /** @type {HTMLInputElement} */ (e.target).value || "";
721
+ @input=${(e: Event) => {
722
+ value = (e.target as HTMLInputElement).value || "";
713
723
  }}
714
- @keydown=${(/** @type {KeyboardEvent} */ e) => {
724
+ @keydown=${(e: KeyboardEvent) => {
715
725
  if (e.key === "Enter") confirm();
716
726
  }}
717
727
  ></sp-textfield>
@@ -719,7 +729,7 @@ function showRenameFileDialog(currentName) {
719
729
  `;
720
730
  requestAnimationFrame(() => {
721
731
  const layer = document.getElementById("layer-dialog");
722
- const tf = /** @type {HTMLElement | null} */ (layer?.querySelector("sp-textfield"));
732
+ const tf = layer?.querySelector("sp-textfield") as HTMLElement | null;
723
733
  if (tf) {
724
734
  tf.focus();
725
735
  const input = tf.shadowRoot?.querySelector("input");
@@ -731,8 +741,8 @@ function showRenameFileDialog(currentName) {
731
741
  }
732
742
 
733
743
  async function renameFile(
734
- /** @type {{ name: string; path: string; type: string }} */ entry,
735
- /** @type {() => void} */ renderLeftPanel,
744
+ entry: { name: string; path: string; type: string },
745
+ renderLeftPanel: () => void,
736
746
  ) {
737
747
  const newName = await showRenameFileDialog(entry.name);
738
748
  if (!newName || newName === entry.name) return;
@@ -753,14 +763,14 @@ async function renameFile(
753
763
  }
754
764
  renderLeftPanel();
755
765
  statusMessage(`Renamed to ${newName}`);
756
- } catch (/** @type {unknown} */ e) {
757
- statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
766
+ } catch (e) {
767
+ statusMessage(`Error: ${(e as Error).message}`);
758
768
  }
759
769
  }
760
770
 
761
771
  async function deleteFile(
762
- /** @type {{ name: string; path: string; type: string }} */ entry,
763
- /** @type {() => void} */ renderLeftPanel,
772
+ entry: { name: string; path: string; type: string },
773
+ renderLeftPanel: () => void,
764
774
  ) {
765
775
  const confirmed = await showConfirmDialog("Delete File", `Delete "${entry.name}"?`, {
766
776
  confirmLabel: "Delete",
@@ -778,8 +788,8 @@ async function deleteFile(
778
788
  }
779
789
  renderLeftPanel();
780
790
  statusMessage(`Deleted ${entry.name}`);
781
- } catch (/** @type {unknown} */ e) {
782
- statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
791
+ } catch (e) {
792
+ statusMessage(`Error: ${(e as Error).message}`);
783
793
  }
784
794
  }
785
795
 
@@ -796,7 +806,15 @@ async function deleteFile(
796
806
  * }} ctx
797
807
  * @param {string} path
798
808
  */
799
- export async function openFileFromTree(ctx, path) {
809
+ export async function openFileFromTree(
810
+ ctx: {
811
+ S: import("../state.js").StudioState;
812
+ commit: (s: import("../state.js").StudioState) => void;
813
+ render: () => void;
814
+ loadMarkdown: (source: string, handle: unknown) => void;
815
+ },
816
+ path: string,
817
+ ) {
800
818
  const platform = getPlatform();
801
819
  // Auto-save current dirty document
802
820
  if (ctx.S.dirty && ctx.S.documentPath) {
@@ -804,11 +822,11 @@ export async function openFileFromTree(ctx, path) {
804
822
  const isContent = ctx.S.mode === "content";
805
823
  let output;
806
824
  if (isContent) {
807
- const mdast = jxToMd(/** @type {JxElement} */ (ctx.S.document));
825
+ const mdast = jxToMd(ctx.S.document as JxElement);
808
826
  const md = unified()
809
827
  .use(remarkDirective)
810
828
  .use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
811
- .stringify(/** @type {import("mdast").Root} */ (/** @type {unknown} */ (mdast)));
829
+ .stringify(mdast as unknown as import("mdast").Root);
812
830
  const fm = ctx.S.content?.frontmatter;
813
831
  const hasFrontmatter = fm && Object.keys(fm).length > 0;
814
832
  output = hasFrontmatter ? `---\n${stringifyYaml(fm).trim()}\n---\n\n${md}` : md;
@@ -816,8 +834,8 @@ export async function openFileFromTree(ctx, path) {
816
834
  output = JSON.stringify(ctx.S.document, null, 2);
817
835
  }
818
836
  await platform.writeFile(ctx.S.documentPath, output);
819
- } catch (/** @type {unknown} */ e) {
820
- statusMessage(`Save error: ${/** @type {Error} */ (e).message}`);
837
+ } catch (e) {
838
+ statusMessage(`Save error: ${(e as Error).message}`);
821
839
  }
822
840
  }
823
841
 
@@ -844,8 +862,8 @@ export async function openFileFromTree(ctx, path) {
844
862
 
845
863
  ctx.render();
846
864
  statusMessage(`Opened ${path}`);
847
- } catch (/** @type {unknown} */ e) {
848
- statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
865
+ } catch (e) {
866
+ statusMessage(`Error: ${(e as Error).message}`);
849
867
  }
850
868
  }
851
869
 
@@ -854,7 +872,7 @@ export async function openFileFromTree(ctx, path) {
854
872
  *
855
873
  * @param {string} path
856
874
  */
857
- export async function openFileInTab(path) {
875
+ export async function openFileInTab(path: string) {
858
876
  for (const [id, tab] of workspace.tabs.entries()) {
859
877
  if (tab.documentPath === path) {
860
878
  activateTab(id);
@@ -882,7 +900,7 @@ export async function openFileInTab(path) {
882
900
  id,
883
901
  documentPath: path,
884
902
  document,
885
- frontmatter,
903
+ ...(frontmatter != null && { frontmatter }),
886
904
  sourceFormat: path.endsWith(".md") ? "md" : null,
887
905
  });
888
906
  requireProjectState().selectedPath = path;
@@ -894,8 +912,8 @@ export async function openFileInTab(path) {
894
912
  }
895
913
 
896
914
  statusMessage(`Opened ${path.split("/").pop()}`);
897
- } catch (/** @type {unknown} */ e) {
898
- statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
915
+ } catch (e) {
916
+ statusMessage(`Error: ${(e as Error).message}`);
899
917
  }
900
918
  }
901
919
 
@@ -905,7 +923,7 @@ export async function openFileInTab(path) {
905
923
  *
906
924
  * @param {string} path
907
925
  */
908
- export async function reloadFileInTab(path) {
926
+ export async function reloadFileInTab(path: string) {
909
927
  for (const [, tab] of workspace.tabs.entries()) {
910
928
  if (tab.documentPath === path) {
911
929
  const platform = getPlatform();
@@ -1,3 +1,4 @@
1
+ /// <reference lib="dom" />
1
2
  /**
2
3
  * GitHub OAuth Device Flow authentication for Jx Studio.
3
4
  *
@@ -6,7 +7,7 @@
6
7
  */
7
8
 
8
9
  import { html } from "lit-html";
9
- import { showDialog } from "../ui/layers.js";
10
+ import { showDialog } from "../ui/layers";
10
11
 
11
12
  const CLIENT_ID = "Ov23liYVlMFpgjOEPXJH";
12
13
  const STORAGE_KEY = "jx_github_token";
@@ -45,8 +46,7 @@ export async function authenticateGithub() {
45
46
 
46
47
  return showDialog((done) => {
47
48
  let cancelled = false;
48
- /** @type {ReturnType<typeof setTimeout> | null} */
49
- let pollTimer = null;
49
+ let pollTimer: ReturnType<typeof setTimeout> | null = null;
50
50
 
51
51
  const poll = async () => {
52
52
  if (cancelled) return;
@@ -1,12 +1,13 @@
1
+ /// <reference lib="dom" />
1
2
  /** Publish a local project to GitHub — creates a new repo and pushes. */
2
3
 
3
4
  import { html } from "lit-html";
4
5
  import { ref } from "lit-html/directives/ref.js";
5
- import { showDialog } from "../ui/layers.js";
6
- import { authenticateGithub } from "./github-auth.js";
7
- import { getPlatform } from "../platform.js";
8
- import { refreshGitStatus } from "../panels/git-panel.js";
9
- import { statusMessage } from "../panels/statusbar.js";
6
+ import { showDialog } from "../ui/layers";
7
+ import { authenticateGithub } from "./github-auth";
8
+ import { getPlatform } from "../platform";
9
+ import { refreshGitStatus } from "../panels/git-panel";
10
+ import { statusMessage } from "../panels/statusbar";
10
11
 
11
12
  /**
12
13
  * Full "Publish to GitHub" flow: 1. Authenticate (or reuse stored token) 2. Prompt for repo name /
@@ -15,17 +16,18 @@ import { statusMessage } from "../panels/statusbar.js";
15
16
  * @param {{ projectName: string }} opts
16
17
  * @returns {Promise<boolean>} True if published successfully
17
18
  */
18
- export async function publishToGithub({ projectName }) {
19
+ export async function publishToGithub({ projectName }: { projectName: string }) {
19
20
  const token = await authenticateGithub();
20
21
  if (!token) return false;
21
22
 
22
- const repoOpts = await showDialog((done) => {
23
- /** @type {HTMLInputElement | null} */
24
- let _nameInput = null;
25
- /** @type {HTMLInputElement | null} */
26
- let _descInput = null;
27
- /** @type {HTMLInputElement | null} */
28
- let _privateToggle = null;
23
+ const repoOpts = await showDialog<{
24
+ name: string;
25
+ description: string;
26
+ isPrivate: boolean;
27
+ } | null>((done) => {
28
+ let _nameInput: HTMLInputElement | null = null;
29
+ let _descInput: HTMLInputElement | null = null;
30
+ let _privateToggle: HTMLInputElement | null = null;
29
31
 
30
32
  return html`
31
33
  <sp-dialog-wrapper
@@ -51,7 +53,7 @@ export async function publishToGithub({ projectName }) {
51
53
  value="${projectName}"
52
54
  placeholder="my-project"
53
55
  ${ref((el) => {
54
- _nameInput = /** @type {HTMLInputElement | null} */ (el || null);
56
+ _nameInput = (el as HTMLInputElement | null) || null;
55
57
  })}
56
58
  ></sp-textfield>
57
59
 
@@ -61,7 +63,7 @@ export async function publishToGithub({ projectName }) {
61
63
  name="repo-desc"
62
64
  placeholder="A brief description"
63
65
  ${ref((el) => {
64
- _descInput = /** @type {HTMLInputElement | null} */ (el || null);
66
+ _descInput = (el as HTMLInputElement | null) || null;
65
67
  })}
66
68
  ></sp-textfield>
67
69
 
@@ -70,7 +72,7 @@ export async function publishToGithub({ projectName }) {
70
72
  name="repo-private"
71
73
  checked
72
74
  ${ref((el) => {
73
- _privateToggle = /** @type {HTMLInputElement | null} */ (el || null);
75
+ _privateToggle = (el as HTMLInputElement | null) || null;
74
76
  })}
75
77
  >Private repository</sp-switch
76
78
  >
@@ -115,8 +117,8 @@ export async function publishToGithub({ projectName }) {
115
117
  statusMessage("Pushing to GitHub…");
116
118
  try {
117
119
  await platform.gitPush({ setUpstream: true });
118
- } catch (/** @type {unknown} */ e) {
119
- statusMessage(`Push failed: ${/** @type {Error} */ (e).message}`);
120
+ } catch (e) {
121
+ statusMessage(`Push failed: ${(e as Error).message}`);
120
122
  return false;
121
123
  }
122
124
 
@@ -49,7 +49,10 @@ export const MD_TEXT_ONLY = new Set(["code"]);
49
49
  * { block: boolean; inline: boolean; directive: boolean; only: Set<string> | null }
50
50
  * >}
51
51
  */
52
- export const MD_NESTING = {
52
+ export const MD_NESTING: Record<
53
+ string,
54
+ { block: boolean; inline: boolean; directive: boolean; only: Set<string> | null }
55
+ > = {
53
56
  _root: { block: true, inline: false, directive: true, only: null },
54
57
  h1: { block: false, inline: true, directive: false, only: null },
55
58
  h2: { block: false, inline: true, directive: false, only: null },
@@ -82,7 +85,7 @@ export const MD_NESTING = {
82
85
  * @param {string} childTag - Proposed child element tag
83
86
  * @returns {boolean}
84
87
  */
85
- export function isValidChild(parentTag, childTag) {
88
+ export function isValidChild(parentTag: string, childTag: string) {
86
89
  const rule = MD_NESTING[parentTag];
87
90
  if (!rule) return true; // directive components allow anything
88
91