@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
|
@@ -11,6 +11,8 @@ import { canvasWrap, canvasPanels, updateCanvas } from "../store.js";
|
|
|
11
11
|
import { activeTab } from "../workspace/workspace.js";
|
|
12
12
|
import { view } from "../view.js";
|
|
13
13
|
import { loadMarkdown } from "../files/file-ops.js";
|
|
14
|
+
import { renderWelcome } from "../panels/welcome-screen.js";
|
|
15
|
+
import { projectState } from "../state.js";
|
|
14
16
|
import {
|
|
15
17
|
canvasPanelTemplate,
|
|
16
18
|
applyTransform,
|
|
@@ -44,24 +46,43 @@ import { mediaDisplayName } from "../panels/shared.js";
|
|
|
44
46
|
import { statusMessage } from "../panels/statusbar.js";
|
|
45
47
|
import * as overlaysPanel from "../panels/overlays.js";
|
|
46
48
|
|
|
47
|
-
/** @
|
|
48
|
-
let _ctx = null;
|
|
49
|
+
/** @typedef {import("../panels/canvas-dnd.js").CanvasPanel} CanvasPanel */
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {{
|
|
53
|
+
* filePath: string;
|
|
54
|
+
* originalContent: string;
|
|
55
|
+
* currentContent: string;
|
|
56
|
+
* isMarkdown: boolean;
|
|
57
|
+
* fileStatus: string;
|
|
58
|
+
* originalDoc?: JxMutableNode | null;
|
|
59
|
+
* currentDoc?: JxMutableNode | null;
|
|
60
|
+
* original?: JxMutableNode | null;
|
|
61
|
+
* }} GitDiffState
|
|
62
|
+
*/
|
|
52
63
|
|
|
53
64
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* @param {{
|
|
65
|
+
* @typedef {{
|
|
57
66
|
* getCanvasMode: () => string;
|
|
58
67
|
* setCanvasMode: (mode: string) => void;
|
|
59
68
|
* openFileFromTree: (path: string) => void;
|
|
60
69
|
* exportFile: () => void;
|
|
61
70
|
* closeFunctionEditor: () => void;
|
|
62
|
-
* gitDiffState:
|
|
63
|
-
* setGitDiffState: (state:
|
|
64
|
-
* }}
|
|
71
|
+
* gitDiffState: GitDiffState | null;
|
|
72
|
+
* setGitDiffState: (state: GitDiffState | null) => void;
|
|
73
|
+
* }} CanvasRenderCtx
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/** @type {CanvasRenderCtx | null} */
|
|
77
|
+
let _ctx = null;
|
|
78
|
+
|
|
79
|
+
let _prevStylebookFilter = "";
|
|
80
|
+
let _prevStylebookCustomizedOnly = false;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Initialize the canvas render module.
|
|
84
|
+
*
|
|
85
|
+
* @param {CanvasRenderCtx} ctx
|
|
65
86
|
*/
|
|
66
87
|
export function initCanvasRender(ctx) {
|
|
67
88
|
_ctx = ctx;
|
|
@@ -69,9 +90,17 @@ export function initCanvasRender(ctx) {
|
|
|
69
90
|
|
|
70
91
|
export function renderCanvas() {
|
|
71
92
|
const tab = activeTab.value;
|
|
72
|
-
if (!tab)
|
|
93
|
+
if (!tab) {
|
|
94
|
+
if (!projectState) {
|
|
95
|
+
renderWelcome(canvasWrap);
|
|
96
|
+
} else {
|
|
97
|
+
canvasWrap.textContent = "";
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const ctx = /** @type {CanvasRenderCtx} */ (_ctx);
|
|
73
102
|
const S = { document: tab.doc.document, ui: tab.session.ui, mode: tab.doc.mode };
|
|
74
|
-
const canvasMode =
|
|
103
|
+
const canvasMode = ctx.getCanvasMode();
|
|
75
104
|
|
|
76
105
|
// Advance render generation so stale async renders from the previous cycle bail out
|
|
77
106
|
++view.renderGeneration;
|
|
@@ -92,7 +121,7 @@ export function renderCanvas() {
|
|
|
92
121
|
|
|
93
122
|
// Function editor mode: editing a function body in Monaco (JS)
|
|
94
123
|
if (S.ui.editingFunction) {
|
|
95
|
-
renderFunctionEditor(
|
|
124
|
+
renderFunctionEditor(ctx.closeFunctionEditor);
|
|
96
125
|
return;
|
|
97
126
|
}
|
|
98
127
|
|
|
@@ -203,7 +232,7 @@ export function renderCanvas() {
|
|
|
203
232
|
litRender(
|
|
204
233
|
html`<div class="source-wrap">
|
|
205
234
|
<div class="source-toolbar">
|
|
206
|
-
<sp-action-button size="s" @click=${
|
|
235
|
+
<sp-action-button size="s" @click=${ctx.exportFile}>
|
|
207
236
|
<sp-icon-export slot="icon"></sp-icon-export>
|
|
208
237
|
Export
|
|
209
238
|
</sp-action-button>
|
|
@@ -224,25 +253,30 @@ export function renderCanvas() {
|
|
|
224
253
|
lang === "json" ? JSON.stringify(S.document, null, 2) : S.document?.toString?.() || "";
|
|
225
254
|
const modelUri = monaco.Uri.parse("file:///" + filePath);
|
|
226
255
|
const model = monaco.editor.createModel(content, lang, modelUri);
|
|
227
|
-
view.monacoEditor = monaco.editor.create(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
256
|
+
view.monacoEditor = monaco.editor.create(
|
|
257
|
+
/** @type {HTMLElement} */ (/** @type {unknown} */ (editorContainer)),
|
|
258
|
+
{
|
|
259
|
+
model,
|
|
260
|
+
theme: "vs-dark",
|
|
261
|
+
automaticLayout: true,
|
|
262
|
+
minimap: { enabled: false },
|
|
263
|
+
fontSize: 12,
|
|
264
|
+
fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
|
|
265
|
+
lineNumbers: "on",
|
|
266
|
+
scrollBeyondLastLine: false,
|
|
267
|
+
wordWrap: "on",
|
|
268
|
+
tabSize: 2,
|
|
269
|
+
},
|
|
270
|
+
);
|
|
239
271
|
|
|
240
272
|
// Debounced sync back to state
|
|
241
|
-
/** @type {
|
|
273
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
242
274
|
let debounce;
|
|
243
275
|
view.monacoEditor.onDidChangeModelContent(() => {
|
|
244
|
-
|
|
245
|
-
|
|
276
|
+
const editor = view.monacoEditor;
|
|
277
|
+
if (!editor) return;
|
|
278
|
+
if (editor._ignoreNextChange) {
|
|
279
|
+
editor._ignoreNextChange = false;
|
|
246
280
|
return;
|
|
247
281
|
}
|
|
248
282
|
clearTimeout(debounce);
|
|
@@ -250,7 +284,7 @@ export function renderCanvas() {
|
|
|
250
284
|
const tab = activeTab.value;
|
|
251
285
|
if (lang === "json") {
|
|
252
286
|
try {
|
|
253
|
-
tab.doc.document = JSON.parse(
|
|
287
|
+
tab.doc.document = JSON.parse(editor.getValue());
|
|
254
288
|
tab.doc.dirty = true;
|
|
255
289
|
} catch {
|
|
256
290
|
// Invalid JSON — don't update state
|
|
@@ -265,8 +299,8 @@ export function renderCanvas() {
|
|
|
265
299
|
|
|
266
300
|
// Git diff mode — render original (left) and current (right) side-by-side on panzoom surface
|
|
267
301
|
if (canvasMode === "git-diff") {
|
|
268
|
-
if (!
|
|
269
|
-
|
|
302
|
+
if (!ctx.gitDiffState) {
|
|
303
|
+
ctx.setCanvasMode("design");
|
|
270
304
|
renderCanvas();
|
|
271
305
|
return;
|
|
272
306
|
}
|
|
@@ -276,7 +310,7 @@ export function renderCanvas() {
|
|
|
276
310
|
canvasWrap.style.overflow = "hidden";
|
|
277
311
|
}
|
|
278
312
|
|
|
279
|
-
const gitDiffState =
|
|
313
|
+
const gitDiffState = ctx.gitDiffState;
|
|
280
314
|
const panelWidth = 800;
|
|
281
315
|
|
|
282
316
|
const { tpl: origTpl, panel: origPanel } = canvasPanelTemplate(
|
|
@@ -307,8 +341,8 @@ export function renderCanvas() {
|
|
|
307
341
|
canvasWrap,
|
|
308
342
|
);
|
|
309
343
|
|
|
310
|
-
canvasPanels.push(/** @type {
|
|
311
|
-
canvasPanels.push(/** @type {
|
|
344
|
+
canvasPanels.push(/** @type {CanvasPanel} */ (origPanel));
|
|
345
|
+
canvasPanels.push(/** @type {CanvasPanel} */ (currPanel));
|
|
312
346
|
|
|
313
347
|
/** @param {string} content */
|
|
314
348
|
const parseContent = (content) => {
|
|
@@ -329,8 +363,20 @@ export function renderCanvas() {
|
|
|
329
363
|
parseContent(gitDiffState.originalContent || ""),
|
|
330
364
|
parseContent(gitDiffState.currentContent || ""),
|
|
331
365
|
]).then(([originalDoc, currentDoc]) => {
|
|
332
|
-
renderCanvasIntoPanel(
|
|
333
|
-
|
|
366
|
+
renderCanvasIntoPanel(
|
|
367
|
+
/** @type {CanvasPanel} */ (origPanel),
|
|
368
|
+
new Set(),
|
|
369
|
+
featureToggles,
|
|
370
|
+
originalDoc,
|
|
371
|
+
gitDiffState,
|
|
372
|
+
);
|
|
373
|
+
renderCanvasIntoPanel(
|
|
374
|
+
/** @type {CanvasPanel} */ (currPanel),
|
|
375
|
+
new Set(),
|
|
376
|
+
featureToggles,
|
|
377
|
+
currentDoc,
|
|
378
|
+
gitDiffState,
|
|
379
|
+
);
|
|
334
380
|
});
|
|
335
381
|
|
|
336
382
|
applyTransform();
|
|
@@ -355,15 +401,15 @@ export function renderCanvas() {
|
|
|
355
401
|
<div
|
|
356
402
|
class="content-edit-canvas"
|
|
357
403
|
${ref((el) => {
|
|
358
|
-
panel.scrollContainer = /** @type {HTMLElement | null} */ (el);
|
|
404
|
+
panel.scrollContainer = /** @type {HTMLElement | null} */ (el || null);
|
|
359
405
|
})}
|
|
360
406
|
>
|
|
361
407
|
<div class="content-edit-column" style="max-width:${baseWidth}px">${panelTpl}</div>
|
|
362
408
|
</div>
|
|
363
409
|
`;
|
|
364
410
|
litRender(editTpl, canvasWrap);
|
|
365
|
-
canvasPanels.push(/** @type {
|
|
366
|
-
renderCanvasIntoPanel(panel, new Set(), S.ui.featureToggles);
|
|
411
|
+
canvasPanels.push(/** @type {CanvasPanel} */ (panel));
|
|
412
|
+
renderCanvasIntoPanel(/** @type {CanvasPanel} */ (panel), new Set(), S.ui.featureToggles);
|
|
367
413
|
return;
|
|
368
414
|
}
|
|
369
415
|
|
|
@@ -407,8 +453,8 @@ export function renderCanvas() {
|
|
|
407
453
|
`,
|
|
408
454
|
canvasWrap,
|
|
409
455
|
);
|
|
410
|
-
canvasPanels.push(/** @type {
|
|
411
|
-
renderCanvasIntoPanel(panel, new Set(), featureToggles);
|
|
456
|
+
canvasPanels.push(/** @type {CanvasPanel} */ (panel));
|
|
457
|
+
renderCanvasIntoPanel(/** @type {CanvasPanel} */ (panel), new Set(), featureToggles);
|
|
412
458
|
applyTransform();
|
|
413
459
|
if (modeChanged) {
|
|
414
460
|
observeCenterUntilStable();
|
|
@@ -436,7 +482,6 @@ export function renderCanvas() {
|
|
|
436
482
|
});
|
|
437
483
|
}
|
|
438
484
|
|
|
439
|
-
/** @type {{ tpl: any; panel: any; activeSet: any }[]} */
|
|
440
485
|
const panelEntries = allPanelDefs.map((def) => {
|
|
441
486
|
const label = `${def.displayName} (${def.width}px)`;
|
|
442
487
|
const { tpl, panel } = canvasPanelTemplate(def.name, label, false, def.width);
|
|
@@ -459,8 +504,9 @@ export function renderCanvas() {
|
|
|
459
504
|
);
|
|
460
505
|
|
|
461
506
|
for (const { panel, activeSet } of panelEntries) {
|
|
462
|
-
|
|
463
|
-
|
|
507
|
+
const p = /** @type {CanvasPanel} */ (panel);
|
|
508
|
+
canvasPanels.push(p);
|
|
509
|
+
renderCanvasIntoPanel(p, activeSet, featureToggles);
|
|
464
510
|
}
|
|
465
511
|
|
|
466
512
|
// Highlight active panel header
|
|
@@ -480,13 +526,13 @@ export function renderCanvas() {
|
|
|
480
526
|
* Render document into a single canvas panel. Tries runtime rendering first, falls back to
|
|
481
527
|
* structural preview.
|
|
482
528
|
*
|
|
483
|
-
* @param {
|
|
529
|
+
* @param {CanvasPanel} panel
|
|
484
530
|
* @param {Set<string>} activeBreakpoints
|
|
485
|
-
* @param {
|
|
486
|
-
* @param {
|
|
487
|
-
* not provided.
|
|
488
|
-
* @param {
|
|
489
|
-
* highlighting.
|
|
531
|
+
* @param {Record<string, boolean>} featureToggles
|
|
532
|
+
* @param {JxMutableNode | null} [docOverride] - Optional document to render (for diff mode). Uses
|
|
533
|
+
* active tab doc if not provided.
|
|
534
|
+
* @param {GitDiffState | null} [gitDiffState] - Optional diff state. If provided, computes and
|
|
535
|
+
* applies diff highlighting.
|
|
490
536
|
*/
|
|
491
537
|
function renderCanvasIntoPanel(
|
|
492
538
|
panel,
|
|
@@ -497,15 +543,16 @@ function renderCanvasIntoPanel(
|
|
|
497
543
|
) {
|
|
498
544
|
const gen = view.renderGeneration;
|
|
499
545
|
const tab = activeTab.value;
|
|
500
|
-
const docToRender = docOverride || tab?.doc.document;
|
|
546
|
+
const docToRender = /** @type {JxMutableNode} */ (docOverride || tab?.doc.document);
|
|
547
|
+
const canvas = /** @type {HTMLElement} */ (panel.canvas);
|
|
501
548
|
|
|
502
|
-
renderCanvasLive(gen, docToRender,
|
|
503
|
-
.then((/** @type {
|
|
549
|
+
renderCanvasLive(gen, docToRender, canvas)
|
|
550
|
+
.then((/** @type {Record<string, unknown> | null} */ scope) => {
|
|
504
551
|
// Skip post-render setup if a newer render has started
|
|
505
552
|
if (gen !== view.renderGeneration) return;
|
|
506
553
|
if (scope) {
|
|
507
554
|
updateCanvas({ status: "ready", scope, error: null });
|
|
508
|
-
applyCanvasMediaOverrides(
|
|
555
|
+
applyCanvasMediaOverrides(canvas, activeBreakpoints);
|
|
509
556
|
statusMessage("Runtime render OK", 1500);
|
|
510
557
|
|
|
511
558
|
// Apply diff highlighting if in git-diff mode
|
|
@@ -521,51 +568,50 @@ function renderCanvasIntoPanel(
|
|
|
521
568
|
// Can't iterate WeakMap, so apply styling by walking the canvas
|
|
522
569
|
const { elToPath } = scope;
|
|
523
570
|
if (elToPath instanceof WeakMap) {
|
|
524
|
-
applyDiffHighlightToCanvas(
|
|
571
|
+
applyDiffHighlightToCanvas(canvas, diffMap);
|
|
525
572
|
}
|
|
526
573
|
}
|
|
527
574
|
} else {
|
|
528
575
|
// Fallback to structural preview
|
|
529
576
|
updateCanvas({ status: "ready", scope: null, error: null });
|
|
530
|
-
|
|
531
|
-
renderCanvasNode(docToRender, [],
|
|
577
|
+
canvas.innerHTML = "";
|
|
578
|
+
renderCanvasNode(docToRender, [], canvas, activeBreakpoints, featureToggles);
|
|
532
579
|
}
|
|
533
580
|
try {
|
|
534
|
-
registerPanelDnD(panel);
|
|
535
|
-
} catch (/** @type {
|
|
536
|
-
console.warn("registerPanelDnD failed:", e.message);
|
|
581
|
+
registerPanelDnD(/** @type {CanvasPanel} */ (panel));
|
|
582
|
+
} catch (/** @type {unknown} */ e) {
|
|
583
|
+
console.warn("registerPanelDnD failed:", /** @type {Error} */ (e).message);
|
|
537
584
|
}
|
|
538
|
-
registerPanelEvents(panel);
|
|
585
|
+
registerPanelEvents(/** @type {CanvasPanel} */ (panel));
|
|
539
586
|
renderOverlays();
|
|
540
587
|
updateForcedPseudoPreview();
|
|
541
588
|
|
|
542
589
|
// Process pending inline edit when canvas becomes ready
|
|
543
590
|
const currentTab = activeTab.value;
|
|
544
591
|
if (currentTab?.session.ui?.pendingInlineEdit) {
|
|
545
|
-
const { path, mediaName: mn } = /** @type {
|
|
592
|
+
const { path, mediaName: mn } = /** @type {InlineEditDef} */ (
|
|
546
593
|
currentTab.session.ui.pendingInlineEdit
|
|
547
594
|
);
|
|
548
595
|
currentTab.session.ui.pendingInlineEdit = null;
|
|
549
|
-
const targetPanel =
|
|
550
|
-
canvasPanels.find((/** @type {any} */ p) => p.mediaName === mn) || canvasPanels[0];
|
|
596
|
+
const targetPanel = canvasPanels.find((p) => p.mediaName === mn) || canvasPanels[0];
|
|
551
597
|
if (targetPanel) {
|
|
552
598
|
const el = findCanvasElement(path, targetPanel.canvas);
|
|
553
599
|
if (el) enterComponentInlineEdit(el, path);
|
|
554
600
|
}
|
|
555
601
|
}
|
|
556
602
|
})
|
|
557
|
-
.catch((/** @type {
|
|
603
|
+
.catch((/** @type {unknown} */ err) => {
|
|
558
604
|
if (gen !== view.renderGeneration) return;
|
|
559
|
-
console.warn("renderCanvasLive rejected:", err
|
|
605
|
+
console.warn("renderCanvasLive rejected:", err instanceof Error ? err.message : err);
|
|
560
606
|
updateCanvas({ status: "ready", scope: null, error: null });
|
|
561
|
-
|
|
562
|
-
renderCanvasNode(docToRender, [],
|
|
607
|
+
canvas.innerHTML = "";
|
|
608
|
+
renderCanvasNode(docToRender, [], canvas, activeBreakpoints, featureToggles);
|
|
563
609
|
try {
|
|
564
|
-
registerPanelDnD(panel);
|
|
565
|
-
} catch (/** @type {
|
|
566
|
-
console.warn("registerPanelDnD failed:", e.message);
|
|
610
|
+
registerPanelDnD(/** @type {CanvasPanel} */ (panel));
|
|
611
|
+
} catch (/** @type {unknown} */ e) {
|
|
612
|
+
console.warn("registerPanelDnD failed:", /** @type {Error} */ (e).message);
|
|
567
613
|
}
|
|
568
|
-
registerPanelEvents(panel);
|
|
614
|
+
registerPanelEvents(/** @type {CanvasPanel} */ (panel));
|
|
569
615
|
renderOverlays();
|
|
570
616
|
updateForcedPseudoPreview();
|
|
571
617
|
});
|
|
@@ -15,8 +15,18 @@ import { view } from "../view.js";
|
|
|
15
15
|
import { getLayerSlot } from "../ui/layers.js";
|
|
16
16
|
import { getActivePanel, findCanvasElement } from "./canvas-helpers.js";
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* @type {{
|
|
20
|
+
* getCanvasMode: () => string;
|
|
21
|
+
* getZoom: () => number;
|
|
22
|
+
* setZoomDirect: (zoom: number) => void;
|
|
23
|
+
* renderStylebookOverlays: () => void;
|
|
24
|
+
* }}
|
|
25
|
+
*/
|
|
26
|
+
let _ctx;
|
|
27
|
+
|
|
28
|
+
/** @type {HTMLElement | null} */
|
|
29
|
+
let _zoomIndicatorEl = null;
|
|
20
30
|
|
|
21
31
|
/**
|
|
22
32
|
* Initialize the canvas utils module.
|
|
@@ -35,10 +45,10 @@ export function initCanvasUtils(ctx) {
|
|
|
35
45
|
/**
|
|
36
46
|
* Create the DOM structure for a single canvas panel.
|
|
37
47
|
*
|
|
38
|
-
* @param {
|
|
39
|
-
* @param {
|
|
40
|
-
* @param {
|
|
41
|
-
* @param {
|
|
48
|
+
* @param {string | null} mediaName
|
|
49
|
+
* @param {string | null} label
|
|
50
|
+
* @param {boolean} fullWidth
|
|
51
|
+
* @param {number | null} width
|
|
42
52
|
*/
|
|
43
53
|
export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
|
|
44
54
|
/**
|
|
@@ -55,7 +65,7 @@ export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
|
|
|
55
65
|
* }}
|
|
56
66
|
*/
|
|
57
67
|
const panel = {
|
|
58
|
-
mediaName,
|
|
68
|
+
mediaName: mediaName || "",
|
|
59
69
|
element: null,
|
|
60
70
|
canvas: null,
|
|
61
71
|
overlay: null,
|
|
@@ -201,6 +211,15 @@ export function fitToScreen() {
|
|
|
201
211
|
applyTransform();
|
|
202
212
|
}
|
|
203
213
|
|
|
214
|
+
/** Reset zoom to 100% and re-center horizontally. */
|
|
215
|
+
export function resetZoom() {
|
|
216
|
+
if (!view.panzoomWrap) return;
|
|
217
|
+
_ctx.setZoomDirect(1);
|
|
218
|
+
centerCanvas();
|
|
219
|
+
applyTransform();
|
|
220
|
+
renderZoomIndicator();
|
|
221
|
+
}
|
|
222
|
+
|
|
204
223
|
/** Reset the zoom indicator (clear its content). Called when switching to non-panzoom modes. */
|
|
205
224
|
export function resetZoomIndicator() {
|
|
206
225
|
litRender(nothing, getLayerSlot("popover", "zoom-indicator"));
|
|
@@ -272,15 +291,27 @@ export function renderZoomIndicator() {
|
|
|
272
291
|
const host = getLayerSlot("popover", "zoom-indicator");
|
|
273
292
|
litRender(
|
|
274
293
|
html`
|
|
275
|
-
<div
|
|
294
|
+
<div
|
|
295
|
+
class="zoom-indicator"
|
|
296
|
+
${ref((el) => {
|
|
297
|
+
_zoomIndicatorEl = /** @type {HTMLElement | null} */ (el || null);
|
|
298
|
+
})}
|
|
299
|
+
>
|
|
300
|
+
<span class="zoom-indicator-action" title="Reset to 100%" @click=${resetZoom}>
|
|
301
|
+
<svg
|
|
302
|
+
width="14"
|
|
303
|
+
height="14"
|
|
304
|
+
viewBox="0 0 16 16"
|
|
305
|
+
fill="none"
|
|
306
|
+
stroke="currentColor"
|
|
307
|
+
stroke-width="1.5"
|
|
308
|
+
>
|
|
309
|
+
<circle cx="8" cy="8" r="5.5" />
|
|
310
|
+
<path d="M8 5.5v5M5.5 8h5" />
|
|
311
|
+
</svg>
|
|
312
|
+
</span>
|
|
276
313
|
<span class="zoom-indicator-label">${Math.round(zoom * 100)}%</span>
|
|
277
|
-
<
|
|
278
|
-
quiet
|
|
279
|
-
size="s"
|
|
280
|
-
class="zoom-fit-btn"
|
|
281
|
-
title="Fit to screen"
|
|
282
|
-
@click=${fitToScreen}
|
|
283
|
-
>
|
|
314
|
+
<span class="zoom-indicator-action" title="Fit to screen" @click=${fitToScreen}>
|
|
284
315
|
<svg
|
|
285
316
|
width="14"
|
|
286
317
|
height="14"
|
|
@@ -292,7 +323,7 @@ export function renderZoomIndicator() {
|
|
|
292
323
|
<rect x="2" y="2" width="12" height="12" rx="1" />
|
|
293
324
|
<path d="M2 6h12M6 2v12" />
|
|
294
325
|
</svg>
|
|
295
|
-
</
|
|
326
|
+
</span>
|
|
296
327
|
</div>
|
|
297
328
|
`,
|
|
298
329
|
host,
|
|
@@ -302,12 +333,11 @@ export function renderZoomIndicator() {
|
|
|
302
333
|
|
|
303
334
|
/** Position the zoom indicator relative to canvas-wrap bounds. */
|
|
304
335
|
export function positionZoomIndicator() {
|
|
305
|
-
|
|
306
|
-
if (!indicator) return;
|
|
336
|
+
if (!_zoomIndicatorEl) return;
|
|
307
337
|
const rect = canvasWrap.getBoundingClientRect();
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
338
|
+
_zoomIndicatorEl.style.left = `${rect.left + rect.width / 2}px`;
|
|
339
|
+
_zoomIndicatorEl.style.top = `${rect.bottom - 32}px`;
|
|
340
|
+
_zoomIndicatorEl.style.transform = "translateX(-50%)";
|
|
311
341
|
}
|
|
312
342
|
|
|
313
343
|
/** Toggle "active" class on canvas panel headers based on activeMedia. */
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate scoped CSS from nested style objects (e.g. `table: { width: "100%", thead: { ... } }`).
|
|
3
|
+
* Returns a CSS string with rules scoped under the given selector.
|
|
4
|
+
*
|
|
5
|
+
* @param {Record<string, any>} styleObj - The full style object (flat + nested)
|
|
6
|
+
* @param {string} scope - The scoping selector (e.g. `[data-jx-site]`)
|
|
7
|
+
* @returns {string} Generated CSS text
|
|
8
|
+
*/
|
|
9
|
+
export function buildNestedSiteCSS(styleObj, scope) {
|
|
10
|
+
let css = "";
|
|
11
|
+
|
|
12
|
+
function emit(/** @type {string} */ parentSel, /** @type {Record<string, any>} */ rules) {
|
|
13
|
+
const props = Object.entries(rules)
|
|
14
|
+
.filter(([, val]) => val === null || typeof val !== "object" || Array.isArray(val))
|
|
15
|
+
.map(([p, val]) => `${camelToKebab(p)}: ${val}`)
|
|
16
|
+
.join("; ");
|
|
17
|
+
if (props) css += `${parentSel} { ${props} }\n`;
|
|
18
|
+
for (const [sel, sub] of Object.entries(rules)) {
|
|
19
|
+
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
|
|
20
|
+
const resolved = sel.startsWith("&")
|
|
21
|
+
? sel.replace("&", parentSel)
|
|
22
|
+
: sel.startsWith("[") || sel.startsWith(":") || sel.startsWith(".")
|
|
23
|
+
? `${parentSel}${sel}`
|
|
24
|
+
: `${parentSel} ${sel}`;
|
|
25
|
+
emit(resolved, sub);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const [k, v] of Object.entries(styleObj)) {
|
|
30
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) continue;
|
|
31
|
+
const resolved = k.startsWith("&")
|
|
32
|
+
? k.replace("&", scope)
|
|
33
|
+
: k.startsWith("[") || k.startsWith(":") || k.startsWith(".")
|
|
34
|
+
? `${scope}${k}`
|
|
35
|
+
: `${scope} ${k}`;
|
|
36
|
+
emit(resolved, v);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return css;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert camelCase to kebab-case.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} s
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function camelToKebab(s) {
|
|
49
|
+
return s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
50
|
+
}
|
|
@@ -24,7 +24,7 @@ import { isSlashMenuOpen, showSlashMenu, dismissSlashMenu } from "./slash-menu.j
|
|
|
24
24
|
import { renderBlockActionBar } from "../panels/block-action-bar.js";
|
|
25
25
|
import { defaultDef } from "../panels/shared.js";
|
|
26
26
|
|
|
27
|
-
/** @type {
|
|
27
|
+
/** @type {{ findCanvasElement: Function } | null} */
|
|
28
28
|
let _ctx = null;
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -39,8 +39,8 @@ export function initComponentInlineEdit(ctx) {
|
|
|
39
39
|
/**
|
|
40
40
|
* Enter plaintext inline editing on a canvas element.
|
|
41
41
|
*
|
|
42
|
-
* @param {
|
|
43
|
-
* @param {
|
|
42
|
+
* @param {HTMLElement} el
|
|
43
|
+
* @param {JxPath} path
|
|
44
44
|
*/
|
|
45
45
|
export function enterComponentInlineEdit(el, path) {
|
|
46
46
|
if (view.componentInlineEdit && view.componentInlineEdit.el === el) {
|
|
@@ -56,7 +56,7 @@ export function enterComponentInlineEdit(el, path) {
|
|
|
56
56
|
if (node.children && typeof node.children === "object") return;
|
|
57
57
|
if (tc && typeof tc === "object") return;
|
|
58
58
|
const voids = new Set(["img", "input", "br", "hr", "video", "audio", "source", "embed", "slot"]);
|
|
59
|
-
if (voids.has(node.tagName)) return;
|
|
59
|
+
if (voids.has(node.tagName || "")) return;
|
|
60
60
|
|
|
61
61
|
for (const p of canvasPanels) {
|
|
62
62
|
const boxes = /** @type {NodeListOf<HTMLElement>} */ (
|
|
@@ -96,14 +96,15 @@ export function enterComponentInlineEdit(el, path) {
|
|
|
96
96
|
el.addEventListener("keydown", componentInlineKeydown);
|
|
97
97
|
el.addEventListener("input", componentInlineInput);
|
|
98
98
|
|
|
99
|
-
const outsideHandler = (/** @type {
|
|
99
|
+
const outsideHandler = (/** @type {MouseEvent} */ evt) => {
|
|
100
100
|
if (!view.componentInlineEdit) {
|
|
101
101
|
document.removeEventListener("mousedown", outsideHandler, true);
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
|
-
if (view.componentInlineEdit.el.contains(evt.target)) return;
|
|
104
|
+
if (view.componentInlineEdit.el.contains(/** @type {Node} */ (evt.target))) return;
|
|
105
105
|
if (isSlashMenuOpen()) return;
|
|
106
|
-
if (view.blockActionBarEl && view.blockActionBarEl.contains(evt.target))
|
|
106
|
+
if (view.blockActionBarEl && view.blockActionBarEl.contains(/** @type {Node} */ (evt.target)))
|
|
107
|
+
return;
|
|
107
108
|
document.removeEventListener("mousedown", outsideHandler, true);
|
|
108
109
|
|
|
109
110
|
/** @type {(string | number)[] | null} */
|
|
@@ -185,7 +186,7 @@ export function enterComponentInlineEdit(el, path) {
|
|
|
185
186
|
renderBlockActionBar();
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
/** @param {
|
|
189
|
+
/** @param {KeyboardEvent} e */
|
|
189
190
|
function componentInlineKeydown(e) {
|
|
190
191
|
if (isSlashMenuOpen()) {
|
|
191
192
|
if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(e.key)) return;
|
|
@@ -205,10 +206,10 @@ function splitParagraph() {
|
|
|
205
206
|
if (!view.componentInlineEdit) return;
|
|
206
207
|
const { el, path, mediaName } = view.componentInlineEdit;
|
|
207
208
|
|
|
208
|
-
const sel = /** @type {
|
|
209
|
+
const sel = /** @type {Selection | null} */ (el.ownerDocument.defaultView?.getSelection());
|
|
209
210
|
const fullText = el.textContent || "";
|
|
210
211
|
let offset = fullText.length;
|
|
211
|
-
if (sel.rangeCount) {
|
|
212
|
+
if (sel && sel.rangeCount) {
|
|
212
213
|
const range = sel.getRangeAt(0);
|
|
213
214
|
const preRange = document.createRange();
|
|
214
215
|
preRange.selectNodeContents(el);
|
|
@@ -220,7 +221,7 @@ function splitParagraph() {
|
|
|
220
221
|
const textAfter = fullText.slice(offset);
|
|
221
222
|
|
|
222
223
|
const tag = "p";
|
|
223
|
-
const pPath = /** @type {
|
|
224
|
+
const pPath = /** @type {JxPath} */ (parentElementPath(path));
|
|
224
225
|
const idx = /** @type {number} */ (childIndex(path));
|
|
225
226
|
if (!pPath) return;
|
|
226
227
|
|
|
@@ -266,7 +267,7 @@ function cancelComponentInlineEdit() {
|
|
|
266
267
|
renderOnly("overlays");
|
|
267
268
|
}
|
|
268
269
|
|
|
269
|
-
/** @param {
|
|
270
|
+
/** @param {HTMLElement} el */
|
|
270
271
|
function cleanupComponentInlineEdit(el) {
|
|
271
272
|
el.removeEventListener("keydown", componentInlineKeydown);
|
|
272
273
|
el.removeEventListener("input", componentInlineInput);
|
|
@@ -304,7 +305,7 @@ function componentInlineInput() {
|
|
|
304
305
|
}
|
|
305
306
|
}
|
|
306
307
|
|
|
307
|
-
/** @param {
|
|
308
|
+
/** @param {{ tag: string; label: string; description: string }} cmd */
|
|
308
309
|
function handleComponentSlashSelect(cmd) {
|
|
309
310
|
if (!view.componentInlineEdit) return;
|
|
310
311
|
const { el, path, mediaName } = view.componentInlineEdit;
|