@jxsuite/studio 0.23.2 → 0.25.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.
- package/dist/studio.js +2311 -718
- package/dist/studio.js.map +121 -104
- package/package.json +8 -7
- package/src/browse/{browse-modal.js → browse-modal.ts} +9 -10
- package/src/browse/{browse.js → browse.ts} +109 -84
- package/src/canvas/{canvas-diff.js → canvas-diff.ts} +42 -35
- package/src/canvas/{canvas-helpers.js → canvas-helpers.ts} +25 -19
- package/src/canvas/{canvas-live-render.js → canvas-live-render.ts} +53 -63
- package/src/canvas/{canvas-render.js → canvas-render.ts} +110 -121
- package/src/canvas/{canvas-utils.js → canvas-utils.ts} +48 -44
- package/src/canvas/{nested-site-style.js → nested-site-style.ts} +3 -3
- package/src/editor/{component-inline-edit.js → component-inline-edit.ts} +45 -33
- package/src/editor/{content-inline-edit.js → content-inline-edit.ts} +27 -45
- package/src/editor/{context-menu.js → context-menu.ts} +38 -36
- package/src/editor/{convert-targets.js → convert-targets.ts} +15 -12
- package/src/editor/{convert-to-component.js → convert-to-component.ts} +25 -26
- package/src/editor/{convert-to-repeater.js → convert-to-repeater.ts} +60 -40
- package/src/editor/{inline-edit.js → inline-edit.ts} +91 -55
- package/src/editor/{inline-format.js → inline-format.ts} +46 -51
- package/src/editor/{insertion-helper.js → insertion-helper.ts} +47 -63
- package/src/editor/{shortcuts.js → shortcuts.ts} +45 -31
- package/src/editor/{slash-menu.js → slash-menu.ts} +34 -35
- package/src/files/{components.js → components.ts} +12 -15
- package/src/files/{file-ops.js → file-ops.ts} +36 -29
- package/src/files/{files.js → files.ts} +102 -84
- package/src/github/{github-auth.js → github-auth.ts} +3 -3
- package/src/github/{github-publish.js → github-publish.ts} +20 -18
- package/src/markdown/{md-allowlist.js → md-allowlist.ts} +5 -2
- package/src/markdown/{md-convert.js → md-convert.ts} +146 -169
- package/src/new-project/{new-project-modal.js → new-project-modal.ts} +16 -18
- package/src/panels/{activity-bar.js → activity-bar.ts} +26 -34
- package/src/panels/{ai-panel.js → ai-panel.ts} +30 -30
- package/src/panels/{block-action-bar.js → block-action-bar.ts} +64 -73
- package/src/panels/{canvas-dnd.js → canvas-dnd.ts} +44 -49
- package/src/panels/{data-explorer.js → data-explorer.ts} +21 -15
- package/src/panels/dnd.ts +417 -0
- package/src/panels/{editors.js → editors.ts} +43 -50
- package/src/panels/{elements-panel.js → elements-panel.ts} +30 -27
- package/src/panels/{events-panel.js → events-panel.ts} +33 -25
- package/src/panels/{git-panel.js → git-panel.ts} +97 -83
- package/src/panels/{head-panel.js → head-panel.ts} +117 -139
- package/src/panels/{imports-panel.js → imports-panel.ts} +69 -89
- package/src/panels/{layers-panel.js → layers-panel.ts} +61 -57
- package/src/panels/{left-panel.js → left-panel.ts} +56 -63
- package/src/panels/{overlays.js → overlays.ts} +28 -36
- package/src/panels/{panel-events.js → panel-events.ts} +53 -51
- package/src/panels/{preview-render.js → preview-render.ts} +20 -15
- package/src/panels/{properties-panel.js → properties-panel.ts} +212 -246
- package/src/panels/{pseudo-preview.js → pseudo-preview.ts} +9 -8
- package/src/panels/{quick-search.js → quick-search.ts} +16 -17
- package/src/panels/{right-panel.js → right-panel.ts} +41 -59
- package/src/panels/{shared.js → shared.ts} +8 -6
- package/src/panels/{signals-panel.js → signals-panel.ts} +322 -362
- package/src/panels/{statusbar.js → statusbar.ts} +14 -14
- package/src/panels/{style-inputs.js → style-inputs.ts} +66 -48
- package/src/panels/{style-panel.js → style-panel.ts} +235 -290
- package/src/panels/{style-utils.js → style-utils.ts} +32 -31
- package/src/panels/{stylebook-layers-panel.js → stylebook-layers-panel.ts} +22 -14
- package/src/panels/{stylebook-panel.js → stylebook-panel.ts} +127 -140
- package/src/panels/{tab-strip.js → tab-strip.ts} +12 -14
- package/src/panels/{toolbar.js → toolbar.ts} +68 -70
- package/src/panels/{welcome-screen.js → welcome-screen.ts} +14 -16
- package/src/{platform.js → platform.ts} +5 -3
- package/src/platforms/{devserver.js → devserver.ts} +53 -44
- package/src/{recent-projects.js → recent-projects.ts} +17 -12
- package/src/{resize-edges.js → resize-edges.ts} +18 -13
- package/src/services/{cem-export.js → cem-export.ts} +14 -7
- package/src/services/{code-services.js → code-services.ts} +29 -20
- package/src/services/{monaco-setup.js → monaco-setup.ts} +2 -3
- package/src/settings/{content-types-editor.js → content-types-editor.ts} +88 -67
- package/src/settings/{css-vars-editor.js → css-vars-editor.ts} +78 -51
- package/src/settings/{defs-editor.js → defs-editor.ts} +70 -56
- package/src/settings/{general-settings.js → general-settings.ts} +29 -28
- package/src/settings/{head-editor.js → head-editor.ts} +37 -35
- package/src/settings/{schema-field-ui.js → schema-field-ui.ts} +86 -76
- package/src/settings/{settings-modal.js → settings-modal.ts} +14 -16
- package/src/{site-context.js → site-context.ts} +35 -32
- package/src/{state.js → state.ts} +95 -78
- package/src/{store.js → store.ts} +60 -63
- package/src/{studio.js → studio.ts} +117 -119
- package/src/tabs/tab.ts +196 -0
- package/src/tabs/{transact.js → transact.ts} +107 -60
- package/src/types.ts +185 -0
- package/src/ui/{button-group.js → button-group.ts} +21 -21
- package/src/ui/{color-selector.js → color-selector.ts} +44 -37
- package/src/ui/{expression-editor.js → expression-editor.ts} +65 -40
- package/src/ui/{field-row.js → field-row.ts} +19 -2
- package/src/ui/{icons.js → icons.ts} +2 -2
- package/src/ui/{layers.js → layers.ts} +33 -24
- package/src/ui/{media-picker.js → media-picker.ts} +13 -9
- package/src/ui/{panel-resize.js → panel-resize.ts} +9 -4
- package/src/ui/{spectrum.js → spectrum.ts} +4 -4
- package/src/ui/{unit-selector.js → unit-selector.ts} +16 -19
- package/src/ui/{value-selector.js → value-selector.ts} +28 -28
- package/src/ui/{widgets.js → widgets.ts} +62 -15
- package/src/utils/{canvas-media.js → canvas-media.ts} +29 -14
- package/src/utils/edit-display.ts +298 -0
- package/src/utils/{google-fonts.js → google-fonts.ts} +14 -13
- package/src/utils/{inherited-style.js → inherited-style.ts} +15 -10
- package/src/utils/{studio-utils.js → studio-utils.ts} +35 -23
- package/src/{view.js → view.ts} +53 -55
- package/src/workspace/{workspace.js → workspace.ts} +44 -33
- package/src/panels/dnd.js +0 -426
- package/src/tabs/tab.js +0 -192
- package/src/utils/edit-display.js +0 -203
- /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
|
|
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
|
|
17
|
-
import { createState, projectState, setProjectState, requireProjectState } from "../store
|
|
18
|
-
import { getPlatform } from "../platform
|
|
19
|
-
import { statusMessage } from "../panels/statusbar
|
|
20
|
-
import { loadComponentRegistry } from "./components
|
|
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
|
|
35
|
-
import { loadMarkdown } from "./file-ops
|
|
36
|
-
import { view } from "../view
|
|
37
|
-
import { addRecentProject, trackRecentFile } from "../recent-projects
|
|
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 =
|
|
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(
|
|
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({
|
|
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 (
|
|
159
|
-
statusMessage(`Error: ${
|
|
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(
|
|
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=${(
|
|
277
|
-
requireProjectState().searchQuery =
|
|
290
|
+
@input=${(e: Event) => {
|
|
291
|
+
requireProjectState().searchQuery = (e.target as HTMLInputElement).value;
|
|
278
292
|
renderLeftPanel();
|
|
279
293
|
}}
|
|
280
|
-
@submit=${(
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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 (
|
|
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=${(
|
|
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(
|
|
365
|
-
tree.addEventListener("keydown", (
|
|
366
|
-
const items =
|
|
367
|
-
const focused =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
443
|
+
const tree = document.querySelector(".file-tree") as HTMLElement | null;
|
|
433
444
|
if (!tree) return;
|
|
434
445
|
|
|
435
|
-
const items =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
524
|
-
const targetDirPath =
|
|
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,
|
|
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 (
|
|
571
|
-
statusMessage(`Error: ${
|
|
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
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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 =
|
|
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 = ".",
|
|
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 (
|
|
683
|
-
statusMessage(`Error: ${
|
|
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=${(
|
|
712
|
-
value =
|
|
721
|
+
@input=${(e: Event) => {
|
|
722
|
+
value = (e.target as HTMLInputElement).value || "";
|
|
713
723
|
}}
|
|
714
|
-
@keydown=${(
|
|
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 =
|
|
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
|
-
|
|
735
|
-
|
|
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 (
|
|
757
|
-
statusMessage(`Error: ${
|
|
766
|
+
} catch (e) {
|
|
767
|
+
statusMessage(`Error: ${(e as Error).message}`);
|
|
758
768
|
}
|
|
759
769
|
}
|
|
760
770
|
|
|
761
771
|
async function deleteFile(
|
|
762
|
-
|
|
763
|
-
|
|
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 (
|
|
782
|
-
statusMessage(`Error: ${
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
820
|
-
statusMessage(`Save error: ${
|
|
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 (
|
|
848
|
-
statusMessage(`Error: ${
|
|
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 (
|
|
898
|
-
statusMessage(`Error: ${
|
|
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
|
|
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
|
-
|
|
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
|
|
6
|
-
import { authenticateGithub } from "./github-auth
|
|
7
|
-
import { getPlatform } from "../platform
|
|
8
|
-
import { refreshGitStatus } from "../panels/git-panel
|
|
9
|
-
import { statusMessage } from "../panels/statusbar
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
let
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
119
|
-
statusMessage(`Push failed: ${
|
|
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
|
|