@jxsuite/studio 0.19.0 → 0.21.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 +13476 -12330
- package/dist/studio.js.map +152 -148
- 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 +19 -7
- package/src/browse/browse-modal.js +16 -13
- package/src/browse/browse.js +44 -35
- package/src/canvas/canvas-diff.js +24 -16
- package/src/canvas/canvas-helpers.js +24 -19
- package/src/canvas/canvas-live-render.js +113 -39
- package/src/canvas/canvas-render.js +119 -73
- package/src/canvas/canvas-utils.js +51 -21
- 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 +28 -14
- package/src/editor/convert-targets.js +60 -0
- package/src/editor/convert-to-component.js +20 -19
- package/src/editor/convert-to-repeater.js +226 -0
- package/src/editor/inline-edit.js +69 -28
- package/src/editor/insertion-helper.js +16 -12
- package/src/editor/shortcuts.js +14 -4
- package/src/editor/slash-menu.js +40 -19
- package/src/files/components.js +1 -1
- package/src/files/file-ops.js +37 -16
- package/src/files/files.js +322 -68
- package/src/github/github-auth.js +122 -0
- package/src/github/github-publish.js +126 -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 +27 -25
- package/src/panels/ai-panel.js +406 -0
- package/src/panels/block-action-bar.js +77 -28
- 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 +475 -99
- package/src/panels/head-panel.js +121 -81
- package/src/panels/imports-panel.js +94 -65
- package/src/panels/layers-panel.js +64 -43
- package/src/panels/left-panel.js +105 -48
- 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 +378 -257
- package/src/panels/pseudo-preview.js +2 -2
- package/src/panels/quick-search.js +11 -9
- package/src/panels/right-panel.js +42 -16
- package/src/panels/shared.js +4 -4
- package/src/panels/signals-panel.js +415 -166
- package/src/panels/statusbar.js +10 -6
- package/src/panels/style-inputs.js +67 -35
- package/src/panels/style-panel.js +239 -134
- 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/tab-strip.js +5 -2
- 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 +8 -8
- 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/settings/settings-modal.js +12 -9
- package/src/site-context.js +41 -31
- package/src/state.js +70 -29
- package/src/store.js +36 -35
- package/src/studio.js +120 -90
- 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/layers.js +31 -1
- 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 +32 -26
- package/src/utils/google-fonts.js +11 -11
- package/src/utils/inherited-style.js +9 -8
- package/src/utils/studio-utils.js +18 -18
- package/src/view.js +58 -1
- package/src/workspace/workspace.js +29 -7
|
@@ -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 {
|
|
@@ -46,6 +56,9 @@ const EDITABLE_BLOCKS = new Set([
|
|
|
46
56
|
"td",
|
|
47
57
|
"th",
|
|
48
58
|
"blockquote",
|
|
59
|
+
"span",
|
|
60
|
+
"a",
|
|
61
|
+
"label",
|
|
49
62
|
]);
|
|
50
63
|
|
|
51
64
|
// ─── Context-aware inline scoping ─────────────────────────────────────────
|
|
@@ -60,7 +73,7 @@ const EDITABLE_BLOCKS = new Set([
|
|
|
60
73
|
*/
|
|
61
74
|
export function isInlineInContext(childTag, parentTag) {
|
|
62
75
|
if (!parentTag) return INLINE_TAGS.has(childTag);
|
|
63
|
-
const parentDef = /** @type {Record<string,
|
|
76
|
+
const parentDef = /** @type {Record<string, ElementDef>} */ (elementsMeta.$defs)[parentTag];
|
|
64
77
|
if (!parentDef || !parentDef.$inlineChildren) return false;
|
|
65
78
|
return parentDef.$inlineChildren.includes(childTag);
|
|
66
79
|
}
|
|
@@ -70,15 +83,15 @@ export function isInlineInContext(childTag, parentTag) {
|
|
|
70
83
|
* look up h1's actions).
|
|
71
84
|
*
|
|
72
85
|
* @param {string} tag
|
|
73
|
-
* @returns {
|
|
86
|
+
* @returns {InlineAction[] | null}
|
|
74
87
|
*/
|
|
75
88
|
export function getInlineActions(tag) {
|
|
76
|
-
const def = /** @type {Record<string,
|
|
89
|
+
const def = /** @type {Record<string, ElementDef>} */ (elementsMeta.$defs)[tag];
|
|
77
90
|
if (!def) return null;
|
|
78
91
|
let actions = def.$inlineActions;
|
|
79
92
|
if (typeof actions === "string") {
|
|
80
|
-
const refDef = /** @type {Record<string,
|
|
81
|
-
actions = refDef?.$inlineActions ??
|
|
93
|
+
const refDef = /** @type {Record<string, ElementDef>} */ (elementsMeta.$defs)[actions];
|
|
94
|
+
actions = refDef?.$inlineActions ?? undefined;
|
|
82
95
|
}
|
|
83
96
|
if (!Array.isArray(actions)) return null;
|
|
84
97
|
return actions;
|
|
@@ -88,13 +101,26 @@ export function getInlineActions(tag) {
|
|
|
88
101
|
|
|
89
102
|
/** @type {HTMLElement | null} */
|
|
90
103
|
let activeEl = null; // currently contenteditable element
|
|
91
|
-
/** @type {
|
|
104
|
+
/** @type {JxPath | null} */
|
|
92
105
|
let activePath = null; // JSON path to the active element
|
|
93
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* @type {((
|
|
108
|
+
* path: JxPath,
|
|
109
|
+
* children: (JxMutableNode | string)[] | null,
|
|
110
|
+
* textContent: string | null,
|
|
111
|
+
* ) => void)
|
|
112
|
+
* | null}
|
|
113
|
+
*/
|
|
94
114
|
let commitFn = null; // function(path, newChildren, newTextContent) to commit changes
|
|
95
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* @type {((path: JxPath, beforeChildren: JxContentResult, afterChildren: JxContentResult) => void)
|
|
117
|
+
* | null}
|
|
118
|
+
*/
|
|
96
119
|
let splitFn = null; // function(path, beforeChildren, afterChildren) to split paragraph
|
|
97
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* @type {((path: JxPath, elementDef: SlashCommand, commitData?: JxContentResult) => void)
|
|
122
|
+
* | null}
|
|
123
|
+
*/
|
|
98
124
|
let insertFn = null; // function(path, elementDef, commitData?) to insert after current block
|
|
99
125
|
/** @type {(() => void) | null} */
|
|
100
126
|
let endFn = null; // function() called when editing stops
|
|
@@ -113,8 +139,8 @@ export function isEditableBlock(el) {
|
|
|
113
139
|
* Check if a node is an inline child. When parentNode is provided, uses context-aware scoping from
|
|
114
140
|
* metadata. Without parent, uses the fallback INLINE_TAGS set.
|
|
115
141
|
*
|
|
116
|
-
* @param {
|
|
117
|
-
* @param {
|
|
142
|
+
* @param {JxMutableNode} node
|
|
143
|
+
* @param {JxMutableNode} [parentNode]
|
|
118
144
|
* @returns {boolean}
|
|
119
145
|
*/
|
|
120
146
|
export function isInlineElement(node, parentNode) {
|
|
@@ -131,11 +157,21 @@ export function isInlineElement(node, parentNode) {
|
|
|
131
157
|
* Start inline editing on a canvas element.
|
|
132
158
|
*
|
|
133
159
|
* @param {HTMLElement} el - The canvas DOM element to edit
|
|
134
|
-
* @param {
|
|
135
|
-
* @param {
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
160
|
+
* @param {JxPath} path - JSON path to the element
|
|
161
|
+
* @param {{
|
|
162
|
+
* onCommit: (
|
|
163
|
+
* path: JxPath,
|
|
164
|
+
* children: (JxMutableNode | string)[] | null,
|
|
165
|
+
* textContent: string | null,
|
|
166
|
+
* ) => void;
|
|
167
|
+
* onSplit: (
|
|
168
|
+
* path: JxPath,
|
|
169
|
+
* beforeChildren: JxContentResult,
|
|
170
|
+
* afterChildren: JxContentResult,
|
|
171
|
+
* ) => void;
|
|
172
|
+
* onInsert: (path: JxPath, elementDef: SlashCommand, commitData?: JxContentResult) => void;
|
|
173
|
+
* onEnd: () => void;
|
|
174
|
+
* }} callbacks
|
|
139
175
|
*/
|
|
140
176
|
export function startEditing(el, path, callbacks) {
|
|
141
177
|
if (activeEl) stopEditing();
|
|
@@ -373,8 +409,8 @@ function commitChanges() {
|
|
|
373
409
|
* Normalize a node's children array: merge adjacent text nodes and fold all-text children into
|
|
374
410
|
* textContent. Returns `{ textContent }` or `{ children }`.
|
|
375
411
|
*
|
|
376
|
-
* @param {{ children?:
|
|
377
|
-
* @returns {
|
|
412
|
+
* @param {{ children?: (JxMutableNode | string)[] }} node
|
|
413
|
+
* @returns {JxContentResult}
|
|
378
414
|
*/
|
|
379
415
|
export function normalizeChildren(node) {
|
|
380
416
|
if (!Array.isArray(node.children) || node.children.length === 0) return { textContent: "" };
|
|
@@ -394,7 +430,7 @@ export function normalizeChildren(node) {
|
|
|
394
430
|
}
|
|
395
431
|
|
|
396
432
|
// Step 2: If all children are text, fold into textContent
|
|
397
|
-
if (merged.every((/** @type {
|
|
433
|
+
if (merged.every((/** @type {JxMutableNode | string} */ c) => typeof c === "string")) {
|
|
398
434
|
return { textContent: merged.join("") };
|
|
399
435
|
}
|
|
400
436
|
|
|
@@ -406,7 +442,7 @@ export function normalizeChildren(node) {
|
|
|
406
442
|
* for plain text or { children } for rich content.
|
|
407
443
|
*
|
|
408
444
|
* @param {HTMLElement} el
|
|
409
|
-
* @returns {
|
|
445
|
+
* @returns {JxContentResult}
|
|
410
446
|
*/
|
|
411
447
|
function elementToJx(el) {
|
|
412
448
|
const nodes = el.childNodes;
|
|
@@ -418,7 +454,7 @@ function elementToJx(el) {
|
|
|
418
454
|
}
|
|
419
455
|
|
|
420
456
|
// Mixed content → children array
|
|
421
|
-
/** @type {
|
|
457
|
+
/** @type {(JxMutableNode | string)[]} */
|
|
422
458
|
const children = [];
|
|
423
459
|
for (const child of nodes) {
|
|
424
460
|
const jsx = domNodeToJx(child);
|
|
@@ -433,7 +469,7 @@ function elementToJx(el) {
|
|
|
433
469
|
* Convert a DOM node to a Jx element definition.
|
|
434
470
|
*
|
|
435
471
|
* @param {Node} node
|
|
436
|
-
* @returns {
|
|
472
|
+
* @returns {JxMutableNode | string | null}
|
|
437
473
|
*/
|
|
438
474
|
function domNodeToJx(node) {
|
|
439
475
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
@@ -446,7 +482,7 @@ function domNodeToJx(node) {
|
|
|
446
482
|
|
|
447
483
|
const el = /** @type {HTMLElement} */ (node);
|
|
448
484
|
const tag = el.tagName.toLowerCase();
|
|
449
|
-
/** @type {
|
|
485
|
+
/** @type {JxMutableNode} */
|
|
450
486
|
const result = { tagName: tag };
|
|
451
487
|
|
|
452
488
|
// Map browser execCommand output to our tag conventions
|
|
@@ -475,7 +511,7 @@ function domNodeToJx(node) {
|
|
|
475
511
|
result.children = [];
|
|
476
512
|
for (const child of childNodes) {
|
|
477
513
|
const jsx = domNodeToJx(child);
|
|
478
|
-
if (jsx) result.children.push(jsx);
|
|
514
|
+
if (jsx) result.children.push(/** @type {JxMutableNode} */ (jsx));
|
|
479
515
|
}
|
|
480
516
|
}
|
|
481
517
|
|
|
@@ -486,7 +522,7 @@ function domNodeToJx(node) {
|
|
|
486
522
|
* Convert a DocumentFragment to a Jx-compatible structure. Returns { textContent } or { children }.
|
|
487
523
|
*
|
|
488
524
|
* @param {DocumentFragment} frag
|
|
489
|
-
* @returns {
|
|
525
|
+
* @returns {JxContentResult}
|
|
490
526
|
*/
|
|
491
527
|
function fragmentToJx(frag) {
|
|
492
528
|
const nodes = frag.childNodes;
|
|
@@ -495,14 +531,19 @@ function fragmentToJx(frag) {
|
|
|
495
531
|
return { textContent: nodes[0].textContent };
|
|
496
532
|
}
|
|
497
533
|
|
|
498
|
-
/** @type {
|
|
534
|
+
/** @type {(JxMutableNode | string)[]} */
|
|
499
535
|
const children = [];
|
|
500
536
|
for (const child of nodes) {
|
|
501
537
|
const jsx = domNodeToJx(child);
|
|
502
538
|
if (jsx) children.push(jsx);
|
|
503
539
|
}
|
|
504
540
|
|
|
505
|
-
if (
|
|
541
|
+
if (
|
|
542
|
+
children.length === 1 &&
|
|
543
|
+
typeof children[0] !== "string" &&
|
|
544
|
+
children[0].tagName === "span" &&
|
|
545
|
+
children[0].textContent != null
|
|
546
|
+
) {
|
|
506
547
|
return { textContent: children[0].textContent };
|
|
507
548
|
}
|
|
508
549
|
|
|
@@ -560,7 +601,7 @@ function updateSlashMenu() {
|
|
|
560
601
|
sharedShowSlashMenu(activeEl, filter, { onSelect: handleSlashSelect });
|
|
561
602
|
}
|
|
562
603
|
|
|
563
|
-
/** @param {
|
|
604
|
+
/** @param {SlashCommand} cmd */
|
|
564
605
|
function handleSlashSelect(cmd) {
|
|
565
606
|
if (!activeEl || !insertFn || !activePath) return;
|
|
566
607
|
|
|
@@ -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,9 +15,10 @@ 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
|
+
import { showConfirmDialog } from "../ui/layers.js";
|
|
21
22
|
|
|
22
23
|
/** @typedef {import("../state.js").JxPath} JxPath */
|
|
23
24
|
|
|
@@ -132,6 +133,7 @@ export function initShortcuts(getContext) {
|
|
|
132
133
|
if (isEditing()) {
|
|
133
134
|
if (mod && e.key === "s") {
|
|
134
135
|
e.preventDefault();
|
|
136
|
+
stopEditing();
|
|
135
137
|
saveFile();
|
|
136
138
|
}
|
|
137
139
|
if (mod && e.key === "w") {
|
|
@@ -142,6 +144,7 @@ export function initShortcuts(getContext) {
|
|
|
142
144
|
if (componentInlineEdit) {
|
|
143
145
|
if (mod && e.key === "s") {
|
|
144
146
|
e.preventDefault();
|
|
147
|
+
stopEditing();
|
|
145
148
|
saveFile();
|
|
146
149
|
}
|
|
147
150
|
if (mod && e.key === "w") {
|
|
@@ -158,9 +161,16 @@ export function initShortcuts(getContext) {
|
|
|
158
161
|
const tab = workspace.tabs.get(workspace.activeTabId);
|
|
159
162
|
if (tab?.doc.dirty) {
|
|
160
163
|
const name = tab.documentPath?.split("/").pop() || "Untitled";
|
|
161
|
-
|
|
164
|
+
showConfirmDialog(
|
|
165
|
+
"Unsaved Changes",
|
|
166
|
+
`"${name}" has unsaved changes. Close without saving?`,
|
|
167
|
+
{ confirmLabel: "Close", destructive: true },
|
|
168
|
+
).then((confirmed) => {
|
|
169
|
+
if (confirmed && workspace.activeTabId) closeTab(workspace.activeTabId);
|
|
170
|
+
});
|
|
171
|
+
} else {
|
|
172
|
+
closeTab(workspace.activeTabId);
|
|
162
173
|
}
|
|
163
|
-
closeTab(workspace.activeTabId);
|
|
164
174
|
}
|
|
165
175
|
break;
|
|
166
176
|
case "o":
|
|
@@ -266,7 +276,7 @@ export function initShortcuts(getContext) {
|
|
|
266
276
|
e.preventDefault();
|
|
267
277
|
if (tab?.session.selection) {
|
|
268
278
|
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
269
|
-
if (node?.children?.length > 0) {
|
|
279
|
+
if ((node?.children?.length ?? 0) > 0) {
|
|
270
280
|
activeTab.value.session.selection = [...tab.session.selection, "children", 0];
|
|
271
281
|
}
|
|
272
282
|
}
|
package/src/editor/slash-menu.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { html, render as litRender, nothing } from "lit-html";
|
|
11
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
11
12
|
import { getLayerSlot } from "../ui/layers.js";
|
|
12
13
|
|
|
13
14
|
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
@@ -30,18 +31,30 @@ const SLASH_COMMANDS = [
|
|
|
30
31
|
{ label: "Section", tag: "section", description: "Section container" },
|
|
31
32
|
];
|
|
32
33
|
|
|
34
|
+
/** @typedef {{ label: string; tag: string; description: string }} SlashCommand */
|
|
35
|
+
|
|
33
36
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
34
37
|
|
|
35
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* @type {{
|
|
40
|
+
* onSelect: (cmd: SlashCommand) => void;
|
|
41
|
+
* showFilter?: boolean;
|
|
42
|
+
* commands?: SlashCommand[];
|
|
43
|
+
* } | null}
|
|
44
|
+
*/
|
|
36
45
|
let callbacks = null;
|
|
37
46
|
let activeIdx = 0;
|
|
38
|
-
/** @type {
|
|
47
|
+
/** @type {SlashCommand[]} */
|
|
39
48
|
let filteredItems = [];
|
|
40
49
|
let open = false;
|
|
41
50
|
/** @type {HTMLElement | null} */
|
|
42
51
|
let _anchorEl = null;
|
|
43
52
|
/** @type {DOMRect | null} */
|
|
44
53
|
let _anchorRect = null;
|
|
54
|
+
/** @type {HTMLInputElement | null} */
|
|
55
|
+
let _filterEl = null;
|
|
56
|
+
/** @type {HTMLElement | null} */
|
|
57
|
+
let _popoverEl = null;
|
|
45
58
|
|
|
46
59
|
/** @returns {HTMLElement} */
|
|
47
60
|
function getHost() {
|
|
@@ -60,18 +73,23 @@ export function isSlashMenuOpen() {
|
|
|
60
73
|
*
|
|
61
74
|
* @param {HTMLElement} anchorEl — the element being edited (for positioning)
|
|
62
75
|
* @param {string} filter — current typed filter text (after the "/")
|
|
63
|
-
* @param {{
|
|
76
|
+
* @param {{
|
|
77
|
+
* onSelect: (cmd: SlashCommand) => void;
|
|
78
|
+
* showFilter?: boolean;
|
|
79
|
+
* commands?: SlashCommand[];
|
|
80
|
+
* }} cbs
|
|
64
81
|
*/
|
|
65
82
|
export function showSlashMenu(anchorEl, filter, cbs) {
|
|
66
83
|
callbacks = cbs;
|
|
67
84
|
_anchorEl = anchorEl;
|
|
68
85
|
_anchorRect = anchorEl.getBoundingClientRect();
|
|
69
86
|
|
|
87
|
+
const source = cbs.commands || SLASH_COMMANDS;
|
|
70
88
|
filteredItems = filter
|
|
71
|
-
?
|
|
89
|
+
? source.filter(
|
|
72
90
|
(c) => c.label.toLowerCase().includes(filter) || c.tag.toLowerCase().includes(filter),
|
|
73
91
|
)
|
|
74
|
-
:
|
|
92
|
+
: source;
|
|
75
93
|
|
|
76
94
|
if (!filteredItems.length && !cbs.showFilter) {
|
|
77
95
|
dismissSlashMenu();
|
|
@@ -92,10 +110,7 @@ export function showSlashMenu(anchorEl, filter, cbs) {
|
|
|
92
110
|
|
|
93
111
|
if (cbs.showFilter) {
|
|
94
112
|
requestAnimationFrame(() => {
|
|
95
|
-
|
|
96
|
-
getHost().querySelector(".slash-filter")
|
|
97
|
-
);
|
|
98
|
-
if (input) input.focus();
|
|
113
|
+
if (_filterEl) _filterEl.focus();
|
|
99
114
|
});
|
|
100
115
|
}
|
|
101
116
|
}
|
|
@@ -106,6 +121,8 @@ export function dismissSlashMenu() {
|
|
|
106
121
|
callbacks = null;
|
|
107
122
|
_anchorEl = null;
|
|
108
123
|
_anchorRect = null;
|
|
124
|
+
_filterEl = null;
|
|
125
|
+
_popoverEl = null;
|
|
109
126
|
filteredItems = [];
|
|
110
127
|
document.removeEventListener("keydown", onKeydown, true);
|
|
111
128
|
document.removeEventListener("mousedown", onOutsideClick, true);
|
|
@@ -125,6 +142,9 @@ function render(anchorEl, showFilter) {
|
|
|
125
142
|
html`
|
|
126
143
|
<sp-popover
|
|
127
144
|
open
|
|
145
|
+
${ref((el) => {
|
|
146
|
+
_popoverEl = /** @type {HTMLElement | null} */ (el || null);
|
|
147
|
+
})}
|
|
128
148
|
style="position:fixed;left:${rect.left}px;top:${rect.bottom +
|
|
129
149
|
4}px;z-index:9999;max-height:320px;overflow-y:auto"
|
|
130
150
|
>
|
|
@@ -135,6 +155,9 @@ function render(anchorEl, showFilter) {
|
|
|
135
155
|
placeholder="Filter…"
|
|
136
156
|
autocomplete="off"
|
|
137
157
|
style="display:block;width:100%;box-sizing:border-box;padding:6px 10px;border:none;border-bottom:1px solid var(--border, #444);outline:none;font-size:13px;background:transparent;color:inherit"
|
|
158
|
+
${ref((el) => {
|
|
159
|
+
_filterEl = /** @type {HTMLInputElement | null} */ (el || null);
|
|
160
|
+
})}
|
|
138
161
|
@input=${onFilterInput}
|
|
139
162
|
/>`
|
|
140
163
|
: nothing}
|
|
@@ -167,14 +190,12 @@ function render(anchorEl, showFilter) {
|
|
|
167
190
|
|
|
168
191
|
/** @param {MouseEvent} e */
|
|
169
192
|
function onOutsideClick(e) {
|
|
170
|
-
|
|
171
|
-
const popover = host.querySelector("sp-popover");
|
|
172
|
-
if (popover && !popover.contains(/** @type {Node} */ (e.target))) {
|
|
193
|
+
if (_popoverEl && !_popoverEl.contains(/** @type {Node} */ (e.target))) {
|
|
173
194
|
dismissSlashMenu();
|
|
174
195
|
}
|
|
175
196
|
}
|
|
176
197
|
|
|
177
|
-
/** @param {
|
|
198
|
+
/** @param {SlashCommand} cmd */
|
|
178
199
|
function select(cmd) {
|
|
179
200
|
const cbs = callbacks;
|
|
180
201
|
dismissSlashMenu();
|
|
@@ -186,21 +207,21 @@ function onFilterInput(e) {
|
|
|
186
207
|
const input = /** @type {HTMLInputElement} */ (e.target);
|
|
187
208
|
const filter = input.value.toLowerCase();
|
|
188
209
|
|
|
210
|
+
const source = callbacks?.commands || SLASH_COMMANDS;
|
|
189
211
|
filteredItems = filter
|
|
190
|
-
?
|
|
212
|
+
? source.filter(
|
|
191
213
|
(c) => c.label.toLowerCase().includes(filter) || c.tag.toLowerCase().includes(filter),
|
|
192
214
|
)
|
|
193
|
-
:
|
|
215
|
+
: source;
|
|
194
216
|
|
|
195
217
|
activeIdx = 0;
|
|
196
218
|
if (_anchorEl) render(_anchorEl, true);
|
|
197
219
|
|
|
198
220
|
// Re-focus input after re-render
|
|
199
221
|
requestAnimationFrame(() => {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
el.selectionStart = el.selectionEnd = el.value.length;
|
|
222
|
+
if (_filterEl && _filterEl !== document.activeElement) {
|
|
223
|
+
_filterEl.focus();
|
|
224
|
+
_filterEl.selectionStart = _filterEl.selectionEnd = _filterEl.value.length;
|
|
204
225
|
}
|
|
205
226
|
});
|
|
206
227
|
}
|
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;
|