@jxsuite/studio 0.19.0 → 0.20.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.css +98 -98
- package/dist/studio.js +12577 -12183
- package/dist/studio.js.map +146 -144
- package/dist/workers/editor.worker.js +79 -79
- package/dist/workers/json.worker.js +109 -109
- package/dist/workers/ts.worker.js +82 -82
- package/package.json +17 -6
- package/src/browse/browse.js +25 -19
- package/src/canvas/canvas-diff.js +24 -16
- package/src/canvas/canvas-helpers.js +24 -19
- package/src/canvas/canvas-live-render.js +94 -39
- package/src/canvas/canvas-render.js +119 -73
- package/src/canvas/canvas-utils.js +38 -15
- package/src/canvas/nested-site-style.js +50 -0
- package/src/editor/component-inline-edit.js +14 -13
- package/src/editor/content-inline-edit.js +22 -10
- package/src/editor/context-menu.js +1 -1
- package/src/editor/convert-to-component.js +12 -10
- package/src/editor/inline-edit.js +66 -28
- package/src/editor/insertion-helper.js +16 -12
- package/src/editor/shortcuts.js +4 -2
- package/src/editor/slash-menu.js +6 -4
- package/src/files/components.js +1 -1
- package/src/files/file-ops.js +37 -16
- package/src/files/files.js +78 -52
- package/src/github/github-auth.js +122 -0
- package/src/github/github-publish.js +115 -0
- package/src/markdown/md-convert.js +172 -107
- package/src/new-project/new-project-modal.js +204 -0
- package/src/panels/activity-bar.js +22 -20
- package/src/panels/ai-panel.js +399 -0
- package/src/panels/block-action-bar.js +5 -5
- package/src/panels/canvas-dnd.js +8 -2
- package/src/panels/data-explorer.js +19 -13
- package/src/panels/dnd.js +144 -92
- package/src/panels/editors.js +55 -35
- package/src/panels/elements-panel.js +33 -19
- package/src/panels/events-panel.js +15 -10
- package/src/panels/git-panel.js +469 -98
- package/src/panels/head-panel.js +121 -81
- package/src/panels/imports-panel.js +88 -64
- package/src/panels/layers-panel.js +64 -43
- package/src/panels/left-panel.js +85 -47
- package/src/panels/overlays.js +1 -0
- package/src/panels/panel-events.js +45 -30
- package/src/panels/preview-render.js +23 -18
- package/src/panels/properties-panel.js +365 -249
- package/src/panels/pseudo-preview.js +2 -2
- package/src/panels/quick-search.js +7 -5
- package/src/panels/right-panel.js +26 -2
- package/src/panels/shared.js +4 -4
- package/src/panels/signals-panel.js +295 -166
- package/src/panels/statusbar.js +4 -4
- package/src/panels/style-inputs.js +67 -35
- package/src/panels/style-panel.js +233 -132
- package/src/panels/style-utils.js +34 -16
- package/src/panels/stylebook-layers-panel.js +12 -10
- package/src/panels/stylebook-panel.js +134 -66
- package/src/panels/toolbar.js +333 -109
- package/src/panels/welcome-screen.js +121 -0
- package/src/platform.js +2 -4
- package/src/platforms/devserver.js +113 -7
- package/src/resize-edges.js +98 -0
- package/src/services/cem-export.js +12 -7
- package/src/services/code-services.js +30 -12
- package/src/settings/content-types-editor.js +7 -7
- package/src/settings/css-vars-editor.js +30 -24
- package/src/settings/defs-editor.js +12 -7
- package/src/settings/general-settings.js +85 -3
- package/src/settings/head-editor.js +30 -24
- package/src/settings/schema-field-ui.js +58 -39
- package/src/site-context.js +41 -31
- package/src/state.js +70 -29
- package/src/store.js +21 -26
- package/src/studio.js +103 -81
- package/src/tabs/tab.js +67 -43
- package/src/tabs/transact.js +23 -13
- package/src/ui/button-group.js +24 -24
- package/src/ui/color-selector.js +28 -23
- package/src/ui/field-row.js +3 -3
- package/src/ui/icons.js +2 -2
- package/src/ui/media-picker.js +7 -7
- package/src/ui/spectrum.js +5 -1
- package/src/ui/unit-selector.js +18 -16
- package/src/ui/value-selector.js +21 -15
- package/src/ui/widgets.js +22 -19
- package/src/utils/canvas-media.js +5 -4
- package/src/utils/edit-display.js +31 -20
- package/src/utils/google-fonts.js +11 -11
- package/src/utils/inherited-style.js +9 -8
- package/src/utils/studio-utils.js +7 -7
- package/src/view.js +58 -1
- package/src/workspace/workspace.js +10 -7
|
@@ -21,7 +21,7 @@ import { startLayerTitleEdit } from "../panels/layers-panel.js";
|
|
|
21
21
|
*
|
|
22
22
|
* @typedef {import("../state.js").JxPath} JxPath
|
|
23
23
|
*
|
|
24
|
-
* @typedef {
|
|
24
|
+
* @typedef {JxMutableNode} JxNode
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
// ─── Clipboard ────────────────────────────────────────────────────────────────
|
|
@@ -39,15 +39,17 @@ export async function convertToComponent() {
|
|
|
39
39
|
transact(tab, (doc) => {
|
|
40
40
|
// Navigate to parent's children array and replace the node
|
|
41
41
|
const pp = parentElementPath(selectionPath) ?? [];
|
|
42
|
-
const idx = childIndex(selectionPath);
|
|
42
|
+
const idx = /** @type {number} */ (childIndex(selectionPath));
|
|
43
43
|
let parent = doc;
|
|
44
44
|
for (const seg of pp) parent = parent[seg];
|
|
45
|
+
if (!parent.children) parent.children = [];
|
|
45
46
|
parent.children[idx] = { tagName: name };
|
|
46
47
|
|
|
47
48
|
// Ensure $elements exists and add the $ref
|
|
48
49
|
if (!doc.$elements) doc.$elements = [];
|
|
49
50
|
const alreadyReferenced = doc.$elements.some(
|
|
50
|
-
(/** @type {
|
|
51
|
+
(/** @type {JxMutableNode | string | { $ref: string }} */ el) =>
|
|
52
|
+
el && typeof el === "object" && "$ref" in el && el.$ref === refPath,
|
|
51
53
|
);
|
|
52
54
|
if (!alreadyReferenced) {
|
|
53
55
|
doc.$elements.push({ $ref: refPath });
|
|
@@ -60,15 +62,15 @@ export async function convertToComponent() {
|
|
|
60
62
|
await platform.writeFile(componentFile, JSON.stringify(componentDef, null, 2));
|
|
61
63
|
await loadComponentRegistry();
|
|
62
64
|
statusMessage(`Converted to <${name}>`);
|
|
63
|
-
} catch (/** @type {
|
|
64
|
-
statusMessage(`Error saving component: ${err.message}`);
|
|
65
|
+
} catch (/** @type {unknown} */ err) {
|
|
66
|
+
statusMessage(`Error saving component: ${/** @type {Error} */ (err).message}`);
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
71
|
* Derive a default tag name from a node.
|
|
70
72
|
*
|
|
71
|
-
* @param {
|
|
73
|
+
* @param {JxMutableNode} node
|
|
72
74
|
* @returns {string}
|
|
73
75
|
*/
|
|
74
76
|
function deriveDefaultName(node) {
|
|
@@ -80,8 +82,8 @@ function deriveDefaultName(node) {
|
|
|
80
82
|
/**
|
|
81
83
|
* Deep clone a node and strip page-specific keys.
|
|
82
84
|
*
|
|
83
|
-
* @param {
|
|
84
|
-
* @returns {
|
|
85
|
+
* @param {JxMutableNode} node
|
|
86
|
+
* @returns {JxMutableNode}
|
|
85
87
|
*/
|
|
86
88
|
function extractComponentDef(node) {
|
|
87
89
|
const clone = structuredClone(node);
|
|
@@ -105,7 +107,7 @@ function validateName(val) {
|
|
|
105
107
|
if (!VALID_NAME.test(val)) {
|
|
106
108
|
return { valid: false, error: "Lowercase letters, digits, and hyphens only" };
|
|
107
109
|
}
|
|
108
|
-
const exists = componentRegistry.some((/** @type {
|
|
110
|
+
const exists = componentRegistry.some((/** @type {JxMutableNode} */ c) => c.tagName === val);
|
|
109
111
|
if (exists) {
|
|
110
112
|
return { valid: false, error: `Component <${val}> already exists` };
|
|
111
113
|
}
|
|
@@ -134,7 +136,7 @@ function promptComponentName(defaultName) {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
function onInput(/** @type {Event} */ e) {
|
|
137
|
-
value = /** @type {
|
|
139
|
+
value = /** @type {HTMLInputElement} */ (e.target).value || "";
|
|
138
140
|
const result = validateName(value);
|
|
139
141
|
error = result.valid ? "" : result.error;
|
|
140
142
|
rerender();
|
|
@@ -179,7 +181,7 @@ function promptComponentName(defaultName) {
|
|
|
179
181
|
|
|
180
182
|
requestAnimationFrame(() => {
|
|
181
183
|
const layer = document.getElementById("layer-dialog");
|
|
182
|
-
const tf = /** @type {
|
|
184
|
+
const tf = /** @type {HTMLElement | null} */ (layer?.querySelector("sp-textfield"));
|
|
183
185
|
if (tf) {
|
|
184
186
|
tf.focus();
|
|
185
187
|
const input = tf.shadowRoot?.querySelector("input");
|
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
* text formatting, Enter for new paragraphs, and slash commands for inserting elements.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{ tag: string; label: string; icon?: string; command?: string; shortcut?: string }} InlineAction
|
|
10
|
+
*
|
|
11
|
+
* @typedef {{ $inlineChildren?: string[]; $inlineActions?: InlineAction[] | string }} ElementDef
|
|
12
|
+
*
|
|
13
|
+
* @typedef {{ label: string; tag: string; description: string }} SlashCommand
|
|
14
|
+
*
|
|
15
|
+
* @typedef {{ textContent?: string | null; children?: (JxMutableNode | string)[] }} JxContentResult
|
|
16
|
+
*/
|
|
17
|
+
|
|
8
18
|
import elementsMeta from "../../data/elements-meta.json";
|
|
9
19
|
import { toggleInlineFormat, normalizeInlineContent } from "./inline-format.js";
|
|
10
20
|
import {
|
|
@@ -60,7 +70,7 @@ const EDITABLE_BLOCKS = new Set([
|
|
|
60
70
|
*/
|
|
61
71
|
export function isInlineInContext(childTag, parentTag) {
|
|
62
72
|
if (!parentTag) return INLINE_TAGS.has(childTag);
|
|
63
|
-
const parentDef = /** @type {Record<string,
|
|
73
|
+
const parentDef = /** @type {Record<string, ElementDef>} */ (elementsMeta.$defs)[parentTag];
|
|
64
74
|
if (!parentDef || !parentDef.$inlineChildren) return false;
|
|
65
75
|
return parentDef.$inlineChildren.includes(childTag);
|
|
66
76
|
}
|
|
@@ -70,15 +80,15 @@ export function isInlineInContext(childTag, parentTag) {
|
|
|
70
80
|
* look up h1's actions).
|
|
71
81
|
*
|
|
72
82
|
* @param {string} tag
|
|
73
|
-
* @returns {
|
|
83
|
+
* @returns {InlineAction[] | null}
|
|
74
84
|
*/
|
|
75
85
|
export function getInlineActions(tag) {
|
|
76
|
-
const def = /** @type {Record<string,
|
|
86
|
+
const def = /** @type {Record<string, ElementDef>} */ (elementsMeta.$defs)[tag];
|
|
77
87
|
if (!def) return null;
|
|
78
88
|
let actions = def.$inlineActions;
|
|
79
89
|
if (typeof actions === "string") {
|
|
80
|
-
const refDef = /** @type {Record<string,
|
|
81
|
-
actions = refDef?.$inlineActions ??
|
|
90
|
+
const refDef = /** @type {Record<string, ElementDef>} */ (elementsMeta.$defs)[actions];
|
|
91
|
+
actions = refDef?.$inlineActions ?? undefined;
|
|
82
92
|
}
|
|
83
93
|
if (!Array.isArray(actions)) return null;
|
|
84
94
|
return actions;
|
|
@@ -88,13 +98,26 @@ export function getInlineActions(tag) {
|
|
|
88
98
|
|
|
89
99
|
/** @type {HTMLElement | null} */
|
|
90
100
|
let activeEl = null; // currently contenteditable element
|
|
91
|
-
/** @type {
|
|
101
|
+
/** @type {JxPath | null} */
|
|
92
102
|
let activePath = null; // JSON path to the active element
|
|
93
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* @type {((
|
|
105
|
+
* path: JxPath,
|
|
106
|
+
* children: (JxMutableNode | string)[] | null,
|
|
107
|
+
* textContent: string | null,
|
|
108
|
+
* ) => void)
|
|
109
|
+
* | null}
|
|
110
|
+
*/
|
|
94
111
|
let commitFn = null; // function(path, newChildren, newTextContent) to commit changes
|
|
95
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* @type {((path: JxPath, beforeChildren: JxContentResult, afterChildren: JxContentResult) => void)
|
|
114
|
+
* | null}
|
|
115
|
+
*/
|
|
96
116
|
let splitFn = null; // function(path, beforeChildren, afterChildren) to split paragraph
|
|
97
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* @type {((path: JxPath, elementDef: SlashCommand, commitData?: JxContentResult) => void)
|
|
119
|
+
* | null}
|
|
120
|
+
*/
|
|
98
121
|
let insertFn = null; // function(path, elementDef, commitData?) to insert after current block
|
|
99
122
|
/** @type {(() => void) | null} */
|
|
100
123
|
let endFn = null; // function() called when editing stops
|
|
@@ -113,8 +136,8 @@ export function isEditableBlock(el) {
|
|
|
113
136
|
* Check if a node is an inline child. When parentNode is provided, uses context-aware scoping from
|
|
114
137
|
* metadata. Without parent, uses the fallback INLINE_TAGS set.
|
|
115
138
|
*
|
|
116
|
-
* @param {
|
|
117
|
-
* @param {
|
|
139
|
+
* @param {JxMutableNode} node
|
|
140
|
+
* @param {JxMutableNode} [parentNode]
|
|
118
141
|
* @returns {boolean}
|
|
119
142
|
*/
|
|
120
143
|
export function isInlineElement(node, parentNode) {
|
|
@@ -131,11 +154,21 @@ export function isInlineElement(node, parentNode) {
|
|
|
131
154
|
* Start inline editing on a canvas element.
|
|
132
155
|
*
|
|
133
156
|
* @param {HTMLElement} el - The canvas DOM element to edit
|
|
134
|
-
* @param {
|
|
135
|
-
* @param {
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
157
|
+
* @param {JxPath} path - JSON path to the element
|
|
158
|
+
* @param {{
|
|
159
|
+
* onCommit: (
|
|
160
|
+
* path: JxPath,
|
|
161
|
+
* children: (JxMutableNode | string)[] | null,
|
|
162
|
+
* textContent: string | null,
|
|
163
|
+
* ) => void;
|
|
164
|
+
* onSplit: (
|
|
165
|
+
* path: JxPath,
|
|
166
|
+
* beforeChildren: JxContentResult,
|
|
167
|
+
* afterChildren: JxContentResult,
|
|
168
|
+
* ) => void;
|
|
169
|
+
* onInsert: (path: JxPath, elementDef: SlashCommand, commitData?: JxContentResult) => void;
|
|
170
|
+
* onEnd: () => void;
|
|
171
|
+
* }} callbacks
|
|
139
172
|
*/
|
|
140
173
|
export function startEditing(el, path, callbacks) {
|
|
141
174
|
if (activeEl) stopEditing();
|
|
@@ -373,8 +406,8 @@ function commitChanges() {
|
|
|
373
406
|
* Normalize a node's children array: merge adjacent text nodes and fold all-text children into
|
|
374
407
|
* textContent. Returns `{ textContent }` or `{ children }`.
|
|
375
408
|
*
|
|
376
|
-
* @param {{ children?:
|
|
377
|
-
* @returns {
|
|
409
|
+
* @param {{ children?: (JxMutableNode | string)[] }} node
|
|
410
|
+
* @returns {JxContentResult}
|
|
378
411
|
*/
|
|
379
412
|
export function normalizeChildren(node) {
|
|
380
413
|
if (!Array.isArray(node.children) || node.children.length === 0) return { textContent: "" };
|
|
@@ -394,7 +427,7 @@ export function normalizeChildren(node) {
|
|
|
394
427
|
}
|
|
395
428
|
|
|
396
429
|
// Step 2: If all children are text, fold into textContent
|
|
397
|
-
if (merged.every((/** @type {
|
|
430
|
+
if (merged.every((/** @type {JxMutableNode | string} */ c) => typeof c === "string")) {
|
|
398
431
|
return { textContent: merged.join("") };
|
|
399
432
|
}
|
|
400
433
|
|
|
@@ -406,7 +439,7 @@ export function normalizeChildren(node) {
|
|
|
406
439
|
* for plain text or { children } for rich content.
|
|
407
440
|
*
|
|
408
441
|
* @param {HTMLElement} el
|
|
409
|
-
* @returns {
|
|
442
|
+
* @returns {JxContentResult}
|
|
410
443
|
*/
|
|
411
444
|
function elementToJx(el) {
|
|
412
445
|
const nodes = el.childNodes;
|
|
@@ -418,7 +451,7 @@ function elementToJx(el) {
|
|
|
418
451
|
}
|
|
419
452
|
|
|
420
453
|
// Mixed content → children array
|
|
421
|
-
/** @type {
|
|
454
|
+
/** @type {(JxMutableNode | string)[]} */
|
|
422
455
|
const children = [];
|
|
423
456
|
for (const child of nodes) {
|
|
424
457
|
const jsx = domNodeToJx(child);
|
|
@@ -433,7 +466,7 @@ function elementToJx(el) {
|
|
|
433
466
|
* Convert a DOM node to a Jx element definition.
|
|
434
467
|
*
|
|
435
468
|
* @param {Node} node
|
|
436
|
-
* @returns {
|
|
469
|
+
* @returns {JxMutableNode | string | null}
|
|
437
470
|
*/
|
|
438
471
|
function domNodeToJx(node) {
|
|
439
472
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
@@ -446,7 +479,7 @@ function domNodeToJx(node) {
|
|
|
446
479
|
|
|
447
480
|
const el = /** @type {HTMLElement} */ (node);
|
|
448
481
|
const tag = el.tagName.toLowerCase();
|
|
449
|
-
/** @type {
|
|
482
|
+
/** @type {JxMutableNode} */
|
|
450
483
|
const result = { tagName: tag };
|
|
451
484
|
|
|
452
485
|
// Map browser execCommand output to our tag conventions
|
|
@@ -475,7 +508,7 @@ function domNodeToJx(node) {
|
|
|
475
508
|
result.children = [];
|
|
476
509
|
for (const child of childNodes) {
|
|
477
510
|
const jsx = domNodeToJx(child);
|
|
478
|
-
if (jsx) result.children.push(jsx);
|
|
511
|
+
if (jsx) result.children.push(/** @type {JxMutableNode} */ (jsx));
|
|
479
512
|
}
|
|
480
513
|
}
|
|
481
514
|
|
|
@@ -486,7 +519,7 @@ function domNodeToJx(node) {
|
|
|
486
519
|
* Convert a DocumentFragment to a Jx-compatible structure. Returns { textContent } or { children }.
|
|
487
520
|
*
|
|
488
521
|
* @param {DocumentFragment} frag
|
|
489
|
-
* @returns {
|
|
522
|
+
* @returns {JxContentResult}
|
|
490
523
|
*/
|
|
491
524
|
function fragmentToJx(frag) {
|
|
492
525
|
const nodes = frag.childNodes;
|
|
@@ -495,14 +528,19 @@ function fragmentToJx(frag) {
|
|
|
495
528
|
return { textContent: nodes[0].textContent };
|
|
496
529
|
}
|
|
497
530
|
|
|
498
|
-
/** @type {
|
|
531
|
+
/** @type {(JxMutableNode | string)[]} */
|
|
499
532
|
const children = [];
|
|
500
533
|
for (const child of nodes) {
|
|
501
534
|
const jsx = domNodeToJx(child);
|
|
502
535
|
if (jsx) children.push(jsx);
|
|
503
536
|
}
|
|
504
537
|
|
|
505
|
-
if (
|
|
538
|
+
if (
|
|
539
|
+
children.length === 1 &&
|
|
540
|
+
typeof children[0] !== "string" &&
|
|
541
|
+
children[0].tagName === "span" &&
|
|
542
|
+
children[0].textContent != null
|
|
543
|
+
) {
|
|
506
544
|
return { textContent: children[0].textContent };
|
|
507
545
|
}
|
|
508
546
|
|
|
@@ -560,7 +598,7 @@ function updateSlashMenu() {
|
|
|
560
598
|
sharedShowSlashMenu(activeEl, filter, { onSelect: handleSlashSelect });
|
|
561
599
|
}
|
|
562
600
|
|
|
563
|
-
/** @param {
|
|
601
|
+
/** @param {SlashCommand} cmd */
|
|
564
602
|
function handleSlashSelect(cmd) {
|
|
565
603
|
if (!activeEl || !insertFn || !activePath) return;
|
|
566
604
|
|
|
@@ -34,16 +34,18 @@ import { transactDoc, mutateInsertNode } from "../tabs/transact.js";
|
|
|
34
34
|
/**
|
|
35
35
|
* @typedef {Object} InsertionHelperContext
|
|
36
36
|
* @property {() => string} getCanvasMode - Returns the active canvas mode.
|
|
37
|
-
* @property {(fn: Function) =>
|
|
37
|
+
* @property {(fn: Function) => unknown} withPanelPointerEvents - Executes fn with pointer-events
|
|
38
38
|
* temporarily enabled on the canvas.
|
|
39
39
|
* @property {() => number} effectiveZoom - Returns the current zoom scale factor.
|
|
40
40
|
* @property {(tag: string) => Object} defaultDef - Creates a default element definition for a tag.
|
|
41
|
-
* @property {(path:
|
|
42
|
-
* null for root.
|
|
43
|
-
* @property {(path:
|
|
41
|
+
* @property {(path: JxPath) => JxPath | null} parentElementPath - Returns the parent element path,
|
|
42
|
+
* or null for root.
|
|
43
|
+
* @property {(path: JxPath) => string | number} childIndex - Returns the child index within the
|
|
44
44
|
* parent.
|
|
45
|
-
* @property {(doc:
|
|
46
|
-
*
|
|
45
|
+
* @property {(doc: JxMutableNode, path: JxPath) => JxMutableNode | null} getNodeAtPath - Retrieves
|
|
46
|
+
* the node at a document path.
|
|
47
|
+
* @property {WeakMap<Element, JxPath>} elToPath - Maps rendered DOM elements to their document
|
|
48
|
+
* paths.
|
|
47
49
|
* @property {CanvasPanel} panel - The active canvas panel.
|
|
48
50
|
*/
|
|
49
51
|
|
|
@@ -58,7 +60,7 @@ let _helper = null;
|
|
|
58
60
|
/** @type {HTMLElement | null} */
|
|
59
61
|
let _currentAnchor = null;
|
|
60
62
|
|
|
61
|
-
/** @type {{ edge: string; path:
|
|
63
|
+
/** @type {{ edge: string; path: JxPath; parentPath: JxPath; idx: number } | null} */
|
|
62
64
|
let _insertionPoint = null;
|
|
63
65
|
|
|
64
66
|
/** @type {AbortController | null} */
|
|
@@ -133,7 +135,9 @@ function onMouseMove(e) {
|
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
const { panel, withPanelPointerEvents, elToPath } = _ctx;
|
|
136
|
-
const el =
|
|
138
|
+
const el = /** @type {HTMLElement | null} */ (
|
|
139
|
+
withPanelPointerEvents(() => document.elementFromPoint(e.clientX, e.clientY))
|
|
140
|
+
);
|
|
137
141
|
|
|
138
142
|
if (!el || !panel.canvas.contains(el)) {
|
|
139
143
|
hide();
|
|
@@ -208,8 +212,8 @@ function onMouseMove(e) {
|
|
|
208
212
|
/**
|
|
209
213
|
* @param {HTMLElement} el
|
|
210
214
|
* @param {string} edge
|
|
211
|
-
* @param {
|
|
212
|
-
* @param {
|
|
215
|
+
* @param {JxPath} path
|
|
216
|
+
* @param {JxPath} parentPath
|
|
213
217
|
* @param {number} idx
|
|
214
218
|
*/
|
|
215
219
|
function showAt(el, edge, path, parentPath, idx) {
|
|
@@ -275,8 +279,8 @@ function onHelperClick(/** @type {MouseEvent} */ e) {
|
|
|
275
279
|
}
|
|
276
280
|
|
|
277
281
|
/**
|
|
278
|
-
* @param {
|
|
279
|
-
* @param {{ edge: string; path:
|
|
282
|
+
* @param {{ label: string; tag: string; description?: string }} cmd
|
|
283
|
+
* @param {{ edge: string; path: JxPath; parentPath: JxPath; idx: number }} point
|
|
280
284
|
*/
|
|
281
285
|
function onSlashSelect(cmd, point) {
|
|
282
286
|
if (!_ctx) return;
|
package/src/editor/shortcuts.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
undo as tabUndo,
|
|
16
16
|
redo as tabRedo,
|
|
17
17
|
} from "../tabs/transact.js";
|
|
18
|
-
import { isEditing } from "./inline-edit.js";
|
|
18
|
+
import { isEditing, stopEditing } from "./inline-edit.js";
|
|
19
19
|
import { copyNode, cutNode, pasteNode } from "./context-menu.js";
|
|
20
20
|
import { openQuickSearch } from "../panels/quick-search.js";
|
|
21
21
|
|
|
@@ -132,6 +132,7 @@ export function initShortcuts(getContext) {
|
|
|
132
132
|
if (isEditing()) {
|
|
133
133
|
if (mod && e.key === "s") {
|
|
134
134
|
e.preventDefault();
|
|
135
|
+
stopEditing();
|
|
135
136
|
saveFile();
|
|
136
137
|
}
|
|
137
138
|
if (mod && e.key === "w") {
|
|
@@ -142,6 +143,7 @@ export function initShortcuts(getContext) {
|
|
|
142
143
|
if (componentInlineEdit) {
|
|
143
144
|
if (mod && e.key === "s") {
|
|
144
145
|
e.preventDefault();
|
|
146
|
+
stopEditing();
|
|
145
147
|
saveFile();
|
|
146
148
|
}
|
|
147
149
|
if (mod && e.key === "w") {
|
|
@@ -266,7 +268,7 @@ export function initShortcuts(getContext) {
|
|
|
266
268
|
e.preventDefault();
|
|
267
269
|
if (tab?.session.selection) {
|
|
268
270
|
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
269
|
-
if (node?.children?.length > 0) {
|
|
271
|
+
if ((node?.children?.length ?? 0) > 0) {
|
|
270
272
|
activeTab.value.session.selection = [...tab.session.selection, "children", 0];
|
|
271
273
|
}
|
|
272
274
|
}
|
package/src/editor/slash-menu.js
CHANGED
|
@@ -30,12 +30,14 @@ const SLASH_COMMANDS = [
|
|
|
30
30
|
{ label: "Section", tag: "section", description: "Section container" },
|
|
31
31
|
];
|
|
32
32
|
|
|
33
|
+
/** @typedef {{ label: string; tag: string; description: string }} SlashCommand */
|
|
34
|
+
|
|
33
35
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
34
36
|
|
|
35
|
-
/** @type {{ onSelect: (cmd:
|
|
37
|
+
/** @type {{ onSelect: (cmd: SlashCommand) => void; showFilter?: boolean } | null} */
|
|
36
38
|
let callbacks = null;
|
|
37
39
|
let activeIdx = 0;
|
|
38
|
-
/** @type {
|
|
40
|
+
/** @type {SlashCommand[]} */
|
|
39
41
|
let filteredItems = [];
|
|
40
42
|
let open = false;
|
|
41
43
|
/** @type {HTMLElement | null} */
|
|
@@ -60,7 +62,7 @@ export function isSlashMenuOpen() {
|
|
|
60
62
|
*
|
|
61
63
|
* @param {HTMLElement} anchorEl — the element being edited (for positioning)
|
|
62
64
|
* @param {string} filter — current typed filter text (after the "/")
|
|
63
|
-
* @param {{ onSelect: (cmd:
|
|
65
|
+
* @param {{ onSelect: (cmd: SlashCommand) => void; showFilter?: boolean }} cbs
|
|
64
66
|
*/
|
|
65
67
|
export function showSlashMenu(anchorEl, filter, cbs) {
|
|
66
68
|
callbacks = cbs;
|
|
@@ -174,7 +176,7 @@ function onOutsideClick(e) {
|
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
/** @param {
|
|
179
|
+
/** @param {SlashCommand} cmd */
|
|
178
180
|
function select(cmd) {
|
|
179
181
|
const cbs = callbacks;
|
|
180
182
|
dismissSlashMenu();
|
package/src/files/components.js
CHANGED
|
@@ -7,7 +7,7 @@ import { projectState } from "../store.js";
|
|
|
7
7
|
* @typedef {{
|
|
8
8
|
* tagName: string;
|
|
9
9
|
* path: string;
|
|
10
|
-
* props?: { name: string; type?: string; [k: string]:
|
|
10
|
+
* props?: { name: string; type?: string; [k: string]: unknown }[];
|
|
11
11
|
* source?: string;
|
|
12
12
|
* package?: string;
|
|
13
13
|
* modulePath?: string;
|
package/src/files/file-ops.js
CHANGED
|
@@ -7,18 +7,22 @@
|
|
|
7
7
|
import { unified } from "unified";
|
|
8
8
|
import remarkStringify from "remark-stringify";
|
|
9
9
|
import remarkDirective from "remark-directive";
|
|
10
|
+
import remarkGfm from "remark-gfm";
|
|
10
11
|
import { stringify as stringifyYaml } from "yaml";
|
|
11
12
|
import { jxToMd, jxDocToMd } from "../markdown/md-convert.js";
|
|
12
13
|
import { locateDocument } from "../services/code-services.js";
|
|
13
14
|
import { statusMessage } from "../panels/statusbar.js";
|
|
14
15
|
import { getPlatform } from "../platform.js";
|
|
15
16
|
import { activeTab, openTab } from "../workspace/workspace.js";
|
|
17
|
+
import { isEditing, stopEditing } from "../editor/inline-edit.js";
|
|
16
18
|
|
|
17
19
|
/** Open a file via the File System Access API (or fallback input). */
|
|
18
20
|
export async function openFile() {
|
|
19
21
|
try {
|
|
20
22
|
if ("showOpenFilePicker" in window) {
|
|
21
|
-
const [handle] = await /** @type {
|
|
23
|
+
const [handle] = await /** @type {{ showOpenFilePicker: Function }} */ (
|
|
24
|
+
/** @type {unknown} */ (window)
|
|
25
|
+
).showOpenFilePicker({
|
|
22
26
|
types: [
|
|
23
27
|
{ description: "Jx Component", accept: { "application/json": [".json"] } },
|
|
24
28
|
{ description: "Markdown Content", accept: { "text/markdown": [".md"] } },
|
|
@@ -66,8 +70,9 @@ export async function openFile() {
|
|
|
66
70
|
};
|
|
67
71
|
input.click();
|
|
68
72
|
}
|
|
69
|
-
} catch (/** @type {
|
|
70
|
-
if (e.name !== "AbortError")
|
|
73
|
+
} catch (/** @type {unknown} */ e) {
|
|
74
|
+
if (/** @type {Error} */ (e).name !== "AbortError")
|
|
75
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
|
|
@@ -75,11 +80,11 @@ export async function openFile() {
|
|
|
75
80
|
* Parse a markdown string into document + frontmatter (pure — no side effects).
|
|
76
81
|
*
|
|
77
82
|
* @param {string} source Markdown text
|
|
78
|
-
* @returns {Promise<{ document:
|
|
83
|
+
* @returns {Promise<{ document: JxMutableNode; frontmatter: Record<string, unknown> }>}
|
|
79
84
|
*/
|
|
80
85
|
export async function loadMarkdown(source) {
|
|
81
86
|
const { transpileJxMarkdown } = await import("@jxsuite/parser/transpile");
|
|
82
|
-
const doc = /** @type {
|
|
87
|
+
const doc = /** @type {JxMutableNode} */ (transpileJxMarkdown(source));
|
|
83
88
|
|
|
84
89
|
const isComponent = doc.tagName && String(doc.tagName).includes("-");
|
|
85
90
|
|
|
@@ -88,9 +93,11 @@ export async function loadMarkdown(source) {
|
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
// Content markdown — children form the root-level document body
|
|
91
|
-
const
|
|
96
|
+
const children = doc.children ?? [];
|
|
97
|
+
if (children.length === 0) children.push({ tagName: "p", children: [] });
|
|
98
|
+
const contentDoc = { children };
|
|
92
99
|
|
|
93
|
-
/** @type {Record<string,
|
|
100
|
+
/** @type {Record<string, unknown>} */
|
|
94
101
|
const frontmatter = {};
|
|
95
102
|
for (const [key, value] of Object.entries(doc)) {
|
|
96
103
|
if (key !== "children") frontmatter[key] = value;
|
|
@@ -101,6 +108,7 @@ export async function loadMarkdown(source) {
|
|
|
101
108
|
|
|
102
109
|
/** Save the current document back to its source location. */
|
|
103
110
|
export async function saveFile() {
|
|
111
|
+
if (isEditing()) stopEditing();
|
|
104
112
|
const tab = activeTab.value;
|
|
105
113
|
if (!tab) return;
|
|
106
114
|
try {
|
|
@@ -112,7 +120,15 @@ export async function saveFile() {
|
|
|
112
120
|
tab.doc.dirty = false;
|
|
113
121
|
statusMessage("Saved");
|
|
114
122
|
} else if (tab.fileHandle && "createWritable" in tab.fileHandle) {
|
|
115
|
-
const writable =
|
|
123
|
+
const writable =
|
|
124
|
+
await /**
|
|
125
|
+
* @type {{
|
|
126
|
+
* createWritable: () => Promise<{
|
|
127
|
+
* write: (s: string) => Promise<void>;
|
|
128
|
+
* close: () => Promise<void>;
|
|
129
|
+
* }>;
|
|
130
|
+
* }}
|
|
131
|
+
*/ (tab.fileHandle).createWritable();
|
|
116
132
|
await writable.write(output);
|
|
117
133
|
await writable.close();
|
|
118
134
|
tab.doc.dirty = false;
|
|
@@ -120,8 +136,9 @@ export async function saveFile() {
|
|
|
120
136
|
} else {
|
|
121
137
|
statusMessage("No save target — use Export");
|
|
122
138
|
}
|
|
123
|
-
} catch (/** @type {
|
|
124
|
-
if (e.name !== "AbortError")
|
|
139
|
+
} catch (/** @type {unknown} */ e) {
|
|
140
|
+
if (/** @type {Error} */ (e).name !== "AbortError")
|
|
141
|
+
statusMessage(`Save error: ${/** @type {Error} */ (e).message}`);
|
|
125
142
|
}
|
|
126
143
|
}
|
|
127
144
|
|
|
@@ -142,7 +159,9 @@ export async function exportFile() {
|
|
|
142
159
|
: isContent
|
|
143
160
|
? "content.md"
|
|
144
161
|
: "component.json";
|
|
145
|
-
const handle = await /** @type {
|
|
162
|
+
const handle = await /** @type {{ showSaveFilePicker: Function }} */ (
|
|
163
|
+
/** @type {unknown} */ (window)
|
|
164
|
+
).showSaveFilePicker({
|
|
146
165
|
suggestedName,
|
|
147
166
|
types: [{ description, accept: { [mimeType]: [ext] } }],
|
|
148
167
|
});
|
|
@@ -163,8 +182,9 @@ export async function exportFile() {
|
|
|
163
182
|
tab.doc.dirty = false;
|
|
164
183
|
statusMessage("Downloaded");
|
|
165
184
|
}
|
|
166
|
-
} catch (/** @type {
|
|
167
|
-
if (e.name !== "AbortError")
|
|
185
|
+
} catch (/** @type {unknown} */ e) {
|
|
186
|
+
if (/** @type {Error} */ (e).name !== "AbortError")
|
|
187
|
+
statusMessage(`Export error: ${/** @type {Error} */ (e).message}`);
|
|
168
188
|
}
|
|
169
189
|
}
|
|
170
190
|
|
|
@@ -178,14 +198,15 @@ export function serializeDocument(tab) {
|
|
|
178
198
|
if (tab.doc.sourceFormat === "md") {
|
|
179
199
|
const fm = tab.doc.content?.frontmatter || {};
|
|
180
200
|
const fullDoc = { ...fm, children: tab.doc.document.children ?? [] };
|
|
181
|
-
return jxDocToMd(fullDoc);
|
|
201
|
+
return jxDocToMd(/** @type {JxMutableNode} */ (fullDoc));
|
|
182
202
|
}
|
|
183
203
|
if (tab.doc.mode === "content") {
|
|
184
|
-
const mdast = jxToMd(tab.doc.document);
|
|
204
|
+
const mdast = jxToMd(/** @type {JxElement} */ (tab.doc.document));
|
|
185
205
|
const md = unified()
|
|
206
|
+
.use(remarkGfm)
|
|
186
207
|
.use(remarkDirective)
|
|
187
208
|
.use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
|
|
188
|
-
.stringify(mdast);
|
|
209
|
+
.stringify(/** @type {import("mdast").Root} */ (/** @type {unknown} */ (mdast)));
|
|
189
210
|
const fm = tab.doc.content?.frontmatter;
|
|
190
211
|
const hasFrontmatter = fm && Object.keys(fm).length > 0;
|
|
191
212
|
return hasFrontmatter ? `---\n${stringifyYaml(fm).trim()}\n---\n\n${md}` : md;
|