@jxsuite/studio 0.33.0 → 0.34.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/iframe-entry.js +6230 -0
- package/dist/iframe-entry.js.map +35 -0
- package/dist/studio.js +10754 -11060
- package/dist/studio.js.map +83 -72
- package/package.json +7 -7
- package/src/browse/browse.ts +11 -4
- package/src/canvas/canvas-helpers.ts +2 -56
- package/src/canvas/canvas-live-render.ts +102 -435
- package/src/canvas/canvas-origin.ts +66 -0
- package/src/canvas/canvas-patcher.ts +63 -403
- package/src/canvas/canvas-render.ts +70 -212
- package/src/canvas/canvas-utils.ts +37 -65
- package/src/canvas/iframe-channel.ts +154 -0
- package/src/canvas/iframe-drop.ts +484 -0
- package/src/canvas/iframe-entry.ts +600 -0
- package/src/canvas/iframe-host.ts +1373 -0
- package/src/canvas/iframe-inline-edit.ts +367 -0
- package/src/canvas/iframe-insert.ts +164 -0
- package/src/canvas/iframe-interaction.ts +176 -0
- package/src/canvas/iframe-keys.ts +85 -0
- package/src/canvas/iframe-overlay.ts +218 -0
- package/src/canvas/iframe-patch.ts +363 -0
- package/src/canvas/iframe-protocol.ts +361 -0
- package/src/canvas/iframe-render.ts +458 -0
- package/src/canvas/iframe-slash.ts +114 -0
- package/src/canvas/iframe-subtree.ts +113 -0
- package/src/canvas/path-mapping.ts +86 -0
- package/src/canvas/serialize-scope.ts +65 -0
- package/src/editor/canvas-context-menu.ts +40 -0
- package/src/editor/canvas-slash-bridge.ts +21 -0
- package/src/editor/context-menu.ts +2 -1
- package/src/editor/inline-edit-apply.ts +183 -0
- package/src/editor/inline-edit.ts +99 -21
- package/src/editor/inline-link.ts +89 -0
- package/src/editor/insert-zone-action.ts +35 -0
- package/src/editor/merge-tags.ts +26 -2
- package/src/editor/repeater-scope.ts +144 -0
- package/src/editor/shortcuts.ts +14 -28
- package/src/editor/slash-menu.ts +73 -42
- package/src/files/files.ts +2 -1
- package/src/page-params.ts +383 -0
- package/src/panels/ai-panel.ts +5 -7
- package/src/panels/block-action-bar.ts +296 -138
- package/src/panels/canvas-dnd-bridge.ts +397 -0
- package/src/panels/component-preview.ts +56 -0
- package/src/panels/dnd.ts +41 -17
- package/src/panels/drag-ghost.ts +62 -0
- package/src/panels/editors.ts +1 -1
- package/src/panels/overlays.ts +10 -125
- package/src/panels/properties-panel.ts +210 -0
- package/src/panels/right-panel.ts +0 -2
- package/src/panels/signals-panel.ts +136 -22
- package/src/panels/stylebook-doc.ts +373 -0
- package/src/panels/stylebook-panel.ts +46 -689
- package/src/panels/tab-bar.ts +159 -13
- package/src/panels/toolbar.ts +3 -2
- package/src/platforms/devserver.ts +15 -0
- package/src/services/monaco-setup.ts +12 -0
- package/src/services/render-critic.ts +9 -9
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/store.ts +4 -62
- package/src/studio.ts +90 -40
- package/src/tabs/doc-op-apply.ts +89 -0
- package/src/tabs/patch-ops.ts +6 -2
- package/src/tabs/tab.ts +23 -4
- package/src/tabs/transact.ts +2 -74
- package/src/types.ts +14 -18
- package/src/ui/jx-theme.ts +63 -0
- package/src/ui/media-picker.ts +6 -4
- package/src/ui/spectrum.ts +5 -0
- package/src/utils/canvas-media.ts +0 -137
- package/src/utils/edit-display.ts +23 -3
- package/src/utils/geometry.ts +43 -0
- package/src/utils/link-target.ts +93 -0
- package/src/utils/strip-events.ts +54 -0
- package/src/view.ts +0 -23
- package/src/workspace/workspace.ts +14 -1
- package/src/canvas/canvas-subtree-render.ts +0 -113
- package/src/editor/component-inline-edit.ts +0 -349
- package/src/editor/content-inline-edit.ts +0 -207
- package/src/editor/insertion-helper.ts +0 -308
- package/src/panels/canvas-dnd.ts +0 -329
- package/src/panels/panel-events.ts +0 -306
- package/src/panels/preview-render.ts +0 -132
- package/src/panels/pseudo-preview.ts +0 -75
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { html, render as litRender, nothing } from "lit-html";
|
|
8
|
-
import { errorMessage } from "@jxsuite/schema/parse";
|
|
9
8
|
import { ref } from "lit-html/directives/ref.js";
|
|
10
9
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
11
10
|
|
|
@@ -24,23 +23,16 @@ import {
|
|
|
24
23
|
resetZoomIndicator,
|
|
25
24
|
updateActivePanelHeaders,
|
|
26
25
|
} from "./canvas-utils";
|
|
27
|
-
import {
|
|
26
|
+
import { parseMediaEntries } from "../utils/canvas-media";
|
|
27
|
+
import { getEffectiveMedia, getEffectiveStyle } from "../site-context";
|
|
28
28
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} from "../utils/canvas-media";
|
|
34
|
-
import { getEffectiveMedia } from "../site-context";
|
|
35
|
-
import { renderCanvasLive } from "./canvas-live-render";
|
|
29
|
+
commitActiveEditSession,
|
|
30
|
+
mountIframeCanvas,
|
|
31
|
+
postStyleUpdateToStylebookHosts,
|
|
32
|
+
} from "./iframe-host";
|
|
36
33
|
import { canvasPerf } from "./canvas-perf";
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import { registerPanelEvents } from "../panels/panel-events";
|
|
40
|
-
import { computeDocumentDiff } from "./canvas-diff";
|
|
41
|
-
import { updateForcedPseudoPreview } from "../panels/pseudo-preview";
|
|
42
|
-
import { enterComponentInlineEdit } from "../editor/component-inline-edit";
|
|
43
|
-
import { refreshStylebookStyles, renderStylebookMode } from "../panels/stylebook-panel";
|
|
34
|
+
import { renderStylebookMode } from "../panels/stylebook-panel";
|
|
35
|
+
import { transposeStylebookStyle } from "../panels/stylebook-doc";
|
|
44
36
|
import { dismissBlockActionBar, dismissLinkPopover } from "../panels/block-action-bar";
|
|
45
37
|
import { dismissContextMenu } from "../editor/context-menu";
|
|
46
38
|
import { dismissSlashMenu } from "../editor/slash-menu";
|
|
@@ -49,8 +41,7 @@ import { mediaDisplayName } from "../panels/shared";
|
|
|
49
41
|
import { statusMessage } from "../panels/statusbar";
|
|
50
42
|
import * as overlaysPanel from "../panels/overlays";
|
|
51
43
|
|
|
52
|
-
import type { CanvasPanel } from "../
|
|
53
|
-
import type { GitDiffState, InlineEditDef } from "../types";
|
|
44
|
+
import type { CanvasPanel, GitDiffState } from "../types";
|
|
54
45
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
55
46
|
import type { Tab } from "../tabs/tab.js";
|
|
56
47
|
|
|
@@ -201,7 +192,11 @@ export function renderCanvas() {
|
|
|
201
192
|
mode: tab.doc.mode,
|
|
202
193
|
ui: tab.session.ui,
|
|
203
194
|
};
|
|
204
|
-
|
|
195
|
+
// Base mode drives the host surface (panel structure, panzoom vs centered column). The preview
|
|
196
|
+
// Toggle changes only what the iframe renders — resolveCanvasDocument reads the effective mode
|
|
197
|
+
// Via its own getCanvasMode. Flipping the toggle therefore re-renders panels in place (no
|
|
198
|
+
// ModeChanged teardown, no pan reset).
|
|
199
|
+
const { canvasMode } = S.ui;
|
|
205
200
|
|
|
206
201
|
// Advance render generation so stale async renders from the previous cycle bail out
|
|
207
202
|
view.renderGeneration += 1;
|
|
@@ -252,21 +247,32 @@ export function renderCanvas() {
|
|
|
252
247
|
return;
|
|
253
248
|
}
|
|
254
249
|
|
|
255
|
-
// Stylebook fast-path: re-
|
|
250
|
+
// Stylebook fast-path: a style edit re-applies IN PLACE via the bridge (the iframe re-runs the
|
|
251
|
+
// Runtime's style applier on the specimen root — real @media, no re-render, no iframe reload).
|
|
252
|
+
// Filter/Customized changes fall through to the full rebuild (they change which specimens exist),
|
|
253
|
+
// As does a zero-host post (no stylebook iframe live yet).
|
|
256
254
|
if (canvasMode === "stylebook" && !modeChanged) {
|
|
257
255
|
const curFilter = tab.session.ui.stylebookFilter || "";
|
|
258
256
|
const curCustomized = Boolean(tab.session.ui.stylebookCustomizedOnly);
|
|
259
257
|
const filterChanged =
|
|
260
258
|
curFilter !== _prevStylebookFilter || curCustomized !== _prevStylebookCustomizedOnly;
|
|
261
259
|
if (!filterChanged) {
|
|
262
|
-
|
|
263
|
-
|
|
260
|
+
const style = transposeStylebookStyle(getEffectiveStyle(tab.doc.document?.style));
|
|
261
|
+
if (postStyleUpdateToStylebookHosts(style as Record<string, unknown>) > 0) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
// Detect whether this is a mode transition or a content-only re-render
|
|
268
268
|
view.prevCanvasMode = canvasMode;
|
|
269
269
|
|
|
270
|
+
// Best-effort commit of a live inline-edit session BEFORE lit rebuilds the panel DOM. Covers
|
|
271
|
+
// Keyboard-driven tab switches / mode changes where no parent pointerdown preceded the render —
|
|
272
|
+
// The endEdit posts ahead of the new render on the FIFO channel, so the resulting editCommit
|
|
273
|
+
// Still routes to the tab the session belonged to (the host's tabId flips only on renderComplete).
|
|
274
|
+
commitActiveEditSession();
|
|
275
|
+
|
|
270
276
|
// DnD handlers are registered on inner canvas elements that get replaced on every
|
|
271
277
|
// Content render, so always clean them up.
|
|
272
278
|
for (const fn of view.canvasDndCleanups) {
|
|
@@ -315,7 +321,7 @@ export function renderCanvas() {
|
|
|
315
321
|
canvasWrap.style.overflow = "";
|
|
316
322
|
canvasWrap.style.overflow = "";
|
|
317
323
|
|
|
318
|
-
// Clear zoom indicator (only re-rendered by design/
|
|
324
|
+
// Clear zoom indicator (only re-rendered by design/stylebook)
|
|
319
325
|
resetZoomIndicator();
|
|
320
326
|
|
|
321
327
|
// Dismiss open popovers/toolbars that are no longer relevant
|
|
@@ -332,9 +338,7 @@ export function renderCanvas() {
|
|
|
332
338
|
renderStylebookMode({
|
|
333
339
|
applyTransform,
|
|
334
340
|
canvasPanelTemplate,
|
|
335
|
-
effectiveZoom,
|
|
336
341
|
observeCenterUntilStable,
|
|
337
|
-
overlayBoxDescriptor,
|
|
338
342
|
renderZoomIndicator,
|
|
339
343
|
updateActivePanelHeaders,
|
|
340
344
|
});
|
|
@@ -377,7 +381,7 @@ export function renderCanvas() {
|
|
|
377
381
|
});
|
|
378
382
|
view.monacoEditor = monaco.editor.create(editorContainer as unknown as HTMLElement, {
|
|
379
383
|
automaticLayout: true,
|
|
380
|
-
fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
|
|
384
|
+
fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace",
|
|
381
385
|
fontSize: 12,
|
|
382
386
|
lineNumbers: "on",
|
|
383
387
|
minimap: { enabled: false },
|
|
@@ -506,14 +510,12 @@ export function renderCanvas() {
|
|
|
506
510
|
]).then(([originalDoc, currentDoc]) => {
|
|
507
511
|
renderCanvasIntoPanel(
|
|
508
512
|
origPanel as unknown as CanvasPanel,
|
|
509
|
-
new Set<string>(),
|
|
510
513
|
featureToggles,
|
|
511
514
|
originalDoc,
|
|
512
515
|
gitDiffState,
|
|
513
516
|
);
|
|
514
517
|
renderCanvasIntoPanel(
|
|
515
518
|
currPanel as unknown as CanvasPanel,
|
|
516
|
-
new Set<string>(),
|
|
517
519
|
featureToggles,
|
|
518
520
|
currentDoc,
|
|
519
521
|
gitDiffState,
|
|
@@ -534,12 +536,17 @@ export function renderCanvas() {
|
|
|
534
536
|
canvasWrap.style.padding = "0";
|
|
535
537
|
canvasWrap.style.overflow = "hidden";
|
|
536
538
|
|
|
537
|
-
// Remove zoom indicator left over from design
|
|
539
|
+
// Remove zoom indicator left over from design mode
|
|
538
540
|
resetZoomIndicator();
|
|
539
541
|
}
|
|
540
542
|
|
|
541
543
|
const { baseWidth } = parseMediaEntries(getEffectiveMedia(S.document.$media));
|
|
542
544
|
const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
|
|
545
|
+
// A component-definition doc (root tag is a custom element) is a fragment, not a page: it should
|
|
546
|
+
// Hug its content rather than have the column fill+stretch to the viewport (dead scroll space).
|
|
547
|
+
const rootTag = (S.document as { tagName?: unknown }).tagName;
|
|
548
|
+
const isComponentDoc = typeof rootTag === "string" && rootTag.includes("-");
|
|
549
|
+
const columnClass = isComponentDoc ? "content-edit-column is-component" : "content-edit-column";
|
|
543
550
|
const editTpl = html`
|
|
544
551
|
<div
|
|
545
552
|
class="content-edit-canvas"
|
|
@@ -547,16 +554,16 @@ export function renderCanvas() {
|
|
|
547
554
|
panel.scrollContainer = (el as HTMLElement) || null;
|
|
548
555
|
})}
|
|
549
556
|
>
|
|
550
|
-
<div class
|
|
557
|
+
<div class=${columnClass} style="max-width:${baseWidth}px">${panelTpl}</div>
|
|
551
558
|
</div>
|
|
552
559
|
`;
|
|
553
560
|
litRender(editTpl, canvasWrap);
|
|
554
561
|
canvasPanels.push(panel as unknown as CanvasPanel);
|
|
555
|
-
renderCanvasIntoPanel(panel as unknown as CanvasPanel,
|
|
562
|
+
renderCanvasIntoPanel(panel as unknown as CanvasPanel, S.ui.featureToggles);
|
|
556
563
|
return;
|
|
557
564
|
}
|
|
558
565
|
|
|
559
|
-
//
|
|
566
|
+
// Design mode (also hosts preview-toggle renders) — set up panzoom surface
|
|
560
567
|
if (modeChanged) {
|
|
561
568
|
canvasWrap.style.padding = "0";
|
|
562
569
|
canvasWrap.style.overflow = "hidden";
|
|
@@ -599,7 +606,7 @@ export function renderCanvas() {
|
|
|
599
606
|
canvasWrap,
|
|
600
607
|
);
|
|
601
608
|
canvasPanels.push(panel as unknown as CanvasPanel);
|
|
602
|
-
renderCanvasIntoPanel(panel as unknown as CanvasPanel,
|
|
609
|
+
renderCanvasIntoPanel(panel as unknown as CanvasPanel, featureToggles);
|
|
603
610
|
applyTransform();
|
|
604
611
|
if (modeChanged) {
|
|
605
612
|
observeCenterUntilStable();
|
|
@@ -612,7 +619,6 @@ export function renderCanvas() {
|
|
|
612
619
|
// Descending for max-width — matching the direction of the design's media queries).
|
|
613
620
|
const allPanelDefs = [
|
|
614
621
|
{
|
|
615
|
-
activeSet: activeBreakpointsForWidth(sizeBreakpoints, baseWidth),
|
|
616
622
|
displayName: mediaDisplayName("--"),
|
|
617
623
|
name: "base",
|
|
618
624
|
width: baseWidth,
|
|
@@ -620,7 +626,6 @@ export function renderCanvas() {
|
|
|
620
626
|
];
|
|
621
627
|
for (const bp of sizeBreakpoints) {
|
|
622
628
|
allPanelDefs.push({
|
|
623
|
-
activeSet: activeBreakpointsForWidth(sizeBreakpoints, bp.width),
|
|
624
629
|
displayName: mediaDisplayName(bp.name),
|
|
625
630
|
name: bp.name,
|
|
626
631
|
width: bp.width,
|
|
@@ -630,7 +635,7 @@ export function renderCanvas() {
|
|
|
630
635
|
const panelEntries = allPanelDefs.map((def) => {
|
|
631
636
|
const label = `${def.displayName} (${def.width}px)`;
|
|
632
637
|
const { tpl, panel } = canvasPanelTemplate(def.name, label, false, def.width);
|
|
633
|
-
return {
|
|
638
|
+
return { panel, tpl };
|
|
634
639
|
});
|
|
635
640
|
|
|
636
641
|
litRender(
|
|
@@ -651,13 +656,13 @@ export function renderCanvas() {
|
|
|
651
656
|
);
|
|
652
657
|
|
|
653
658
|
for (let i = 0; i < panelEntries.length; i++) {
|
|
654
|
-
const { panel
|
|
659
|
+
const { panel } = panelEntries[i]!;
|
|
655
660
|
const p = panel as CanvasPanel;
|
|
656
661
|
canvasPanels.push(p);
|
|
657
662
|
if (i === 0) {
|
|
658
|
-
renderCanvasIntoPanel(p,
|
|
663
|
+
renderCanvasIntoPanel(p, featureToggles);
|
|
659
664
|
} else {
|
|
660
|
-
setTimeout(() => renderCanvasIntoPanel(p,
|
|
665
|
+
setTimeout(() => renderCanvasIntoPanel(p, featureToggles), 0);
|
|
661
666
|
}
|
|
662
667
|
}
|
|
663
668
|
|
|
@@ -675,23 +680,24 @@ export function renderCanvas() {
|
|
|
675
680
|
}
|
|
676
681
|
|
|
677
682
|
/**
|
|
678
|
-
* Render document into a single canvas panel
|
|
679
|
-
*
|
|
683
|
+
* Render a document into a single canvas panel via the iframe canvas: the document renders inside a
|
|
684
|
+
* same-runtime iframe served from the real origin (the iframe holds the render context, stamps
|
|
685
|
+
* `data-jx-path`/`data-jx-layout`, and draws its own overlays). Git-diff/preview overrides render
|
|
686
|
+
* but stay un-patchable (`ready` only flips for the real tab document).
|
|
680
687
|
*
|
|
681
688
|
* @param {CanvasPanel} panel
|
|
682
|
-
* @param {
|
|
683
|
-
*
|
|
689
|
+
* @param {Record<string, boolean>} _featureToggles - Accepted for call-site symmetry (the iframe
|
|
690
|
+
* render needs no structural-preview fallback); unused.
|
|
684
691
|
* @param {JxMutableNode | null} [docOverride] - Optional document to render (for diff mode). Uses
|
|
685
692
|
* active tab doc if not provided.
|
|
686
|
-
* @param {GitDiffState | null} [
|
|
687
|
-
*
|
|
693
|
+
* @param {GitDiffState | null} [_gitDiffState] - Accepted for call-site symmetry; the iframe path
|
|
694
|
+
* does not apply parent-side diff highlighting.
|
|
688
695
|
*/
|
|
689
696
|
function renderCanvasIntoPanel(
|
|
690
697
|
panel: CanvasPanel,
|
|
691
|
-
|
|
692
|
-
featureToggles: Record<string, boolean>,
|
|
698
|
+
_featureToggles: Record<string, boolean>,
|
|
693
699
|
docOverride: JxMutableNode | null = null,
|
|
694
|
-
|
|
700
|
+
_gitDiffState: GitDiffState | null = null,
|
|
695
701
|
) {
|
|
696
702
|
const gen = view.renderGeneration;
|
|
697
703
|
const tab = activeTab.value;
|
|
@@ -700,179 +706,31 @@ function renderCanvasIntoPanel(
|
|
|
700
706
|
|
|
701
707
|
canvasPerf.panelRenders += 1;
|
|
702
708
|
panel.ready = false;
|
|
703
|
-
panel.liveCtx = null;
|
|
704
|
-
panel.activeBreakpoints = activeBreakpoints;
|
|
705
709
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
710
|
+
// Overrides (git-diff docs) mount with a null tab identity: their iframes must never route doc
|
|
711
|
+
// Mutations anywhere. The real doc carries its tab id so edit/drop messages route to THAT tab.
|
|
712
|
+
void mountIframeCanvas(
|
|
713
|
+
gen,
|
|
714
|
+
docToRender,
|
|
715
|
+
canvas,
|
|
716
|
+
panel._width,
|
|
717
|
+
docOverride ? null : (tab?.id ?? null),
|
|
718
|
+
)
|
|
719
|
+
.then(() => {
|
|
720
|
+
if (gen === view.renderGeneration) {
|
|
721
|
+
// Mark the panel patchable once the real document is mounted (not a diff/preview override)
|
|
722
|
+
// So classifyOps admits surgical patches; the iframe holds the render context, so this
|
|
723
|
+
// Panel needs no parent-side render scope.
|
|
717
724
|
panel.ready = !docOverride;
|
|
718
|
-
scheduleStyleTagSweep();
|
|
719
|
-
|
|
720
|
-
// Apply diff highlighting if in git-diff mode
|
|
721
|
-
if (gitDiffState && docOverride) {
|
|
722
|
-
// Determine which document is original and which is current
|
|
723
|
-
const isOriginal = docOverride === (gitDiffState.originalDoc || gitDiffState.original);
|
|
724
|
-
const _tab = activeTab.value;
|
|
725
|
-
const origDoc = isOriginal ? docOverride : gitDiffState.currentDoc || _tab?.doc.document;
|
|
726
|
-
const currDoc = isOriginal ? gitDiffState.currentDoc || _tab?.doc.document : docOverride;
|
|
727
|
-
|
|
728
|
-
const { byPath: diffMap } = computeDocumentDiff(origDoc, currDoc);
|
|
729
|
-
|
|
730
|
-
// Can't iterate WeakMap, so apply styling by walking the canvas
|
|
731
|
-
const { elToPath } = scope;
|
|
732
|
-
if (elToPath instanceof WeakMap) {
|
|
733
|
-
applyDiffHighlightToCanvas(canvas, diffMap);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
} else {
|
|
737
|
-
// Fallback to structural preview
|
|
738
725
|
updateCanvas({ error: null, scope: null, status: "ready" });
|
|
739
|
-
|
|
740
|
-
renderCanvasNode(docToRender, [], canvas, activeBreakpoints, featureToggles);
|
|
741
|
-
}
|
|
742
|
-
try {
|
|
743
|
-
registerPanelDnD(panel as unknown as CanvasPanel);
|
|
744
|
-
} catch (dndError) {
|
|
745
|
-
console.warn("registerPanelDnD failed:", errorMessage(dndError));
|
|
746
|
-
}
|
|
747
|
-
registerPanelEvents(panel as unknown as CanvasPanel);
|
|
748
|
-
renderOverlays();
|
|
749
|
-
updateForcedPseudoPreview();
|
|
750
|
-
|
|
751
|
-
// Process pending inline edit when canvas becomes ready
|
|
752
|
-
const currentTab = activeTab.value;
|
|
753
|
-
if (currentTab?.session.ui?.pendingInlineEdit) {
|
|
754
|
-
const { path, mediaName: mn } = currentTab.session.ui.pendingInlineEdit as InlineEditDef;
|
|
755
|
-
currentTab.session.ui.pendingInlineEdit = null;
|
|
756
|
-
const targetPanel = canvasPanels.find((p) => p.mediaName === mn) || canvasPanels[0];
|
|
757
|
-
if (targetPanel) {
|
|
758
|
-
const el = findCanvasElement(path, targetPanel.canvas);
|
|
759
|
-
if (el) {
|
|
760
|
-
enterComponentInlineEdit(el, path);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
726
|
+
statusMessage("Iframe render OK", 1500);
|
|
763
727
|
}
|
|
764
728
|
})
|
|
765
729
|
.catch((error: unknown) => {
|
|
766
|
-
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
console.warn("renderCanvasLive rejected:", error instanceof Error ? error.message : error);
|
|
770
|
-
updateCanvas({ error: null, scope: null, status: "ready" });
|
|
771
|
-
canvas.innerHTML = "";
|
|
772
|
-
renderCanvasNode(docToRender, [], canvas, activeBreakpoints, featureToggles);
|
|
773
|
-
try {
|
|
774
|
-
registerPanelDnD(panel as unknown as CanvasPanel);
|
|
775
|
-
} catch (dndError) {
|
|
776
|
-
console.warn("registerPanelDnD failed:", errorMessage(dndError));
|
|
777
|
-
}
|
|
778
|
-
registerPanelEvents(panel as unknown as CanvasPanel);
|
|
779
|
-
renderOverlays();
|
|
780
|
-
updateForcedPseudoPreview();
|
|
730
|
+
console.warn("mountIframeCanvas failed:", error instanceof Error ? error.message : error);
|
|
781
731
|
});
|
|
782
732
|
}
|
|
783
733
|
|
|
784
|
-
/**
|
|
785
|
-
* Apply diff highlighting to canvas elements based on elToPath mapping. Walks the canvas DOM and
|
|
786
|
-
* applies classes based on diff status.
|
|
787
|
-
*
|
|
788
|
-
* @param {HTMLElement} canvas
|
|
789
|
-
* @param {Map<string, "added" | "removed" | "modified">} diffMap
|
|
790
|
-
*/
|
|
791
|
-
function applyDiffHighlightToCanvas(
|
|
792
|
-
canvas: HTMLElement,
|
|
793
|
-
diffMap: Map<string, "added" | "removed" | "modified">,
|
|
794
|
-
) {
|
|
795
|
-
if (!diffMap || diffMap.size === 0) {
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// Walk all elements in canvas and check their data attributes or other markers
|
|
800
|
-
const walkCanvas = (el: HTMLElement, /** @type {string} */ path = "") => {
|
|
801
|
-
const pathKey = path || "/";
|
|
802
|
-
|
|
803
|
-
if (diffMap.has(pathKey)) {
|
|
804
|
-
const status = diffMap.get(pathKey);
|
|
805
|
-
if (status === "added") {
|
|
806
|
-
el.classList.add("element-diff-added");
|
|
807
|
-
} else if (status === "removed") {
|
|
808
|
-
el.classList.add("element-diff-removed");
|
|
809
|
-
} else if (status === "modified") {
|
|
810
|
-
el.classList.add("element-diff-modified");
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// Check for child elements (heuristic: children array markers)
|
|
815
|
-
let childIdx = 0;
|
|
816
|
-
for (const child of el.children) {
|
|
817
|
-
const childPath =
|
|
818
|
-
pathKey === "/" ? `children/${childIdx}` : `${pathKey}/children/${childIdx}`;
|
|
819
|
-
walkCanvas(child as HTMLElement, childPath);
|
|
820
|
-
childIdx += 1;
|
|
821
|
-
}
|
|
822
|
-
};
|
|
823
|
-
|
|
824
|
-
walkCanvas(canvas, "");
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
/**
|
|
828
|
-
* Apply media query overrides as inline styles on matching canvas elements. Needed because the
|
|
829
|
-
* runtime renders base styles as inline — @media CSS rules in the injected stylesheet can't win
|
|
830
|
-
* against inline specificity.
|
|
831
|
-
*
|
|
832
|
-
* @param {Element} canvasEl
|
|
833
|
-
* @param {Set<string>} activeBreakpoints
|
|
834
|
-
*/
|
|
835
|
-
export function applyCanvasMediaOverrides(canvasEl: Element, activeBreakpoints: Set<string>) {
|
|
836
|
-
if (activeBreakpoints.size === 0) {
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
const tab = activeTab.value;
|
|
840
|
-
if (!tab) {
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
const docMedia = getEffectiveMedia(tab.doc.document.$media || {});
|
|
844
|
-
// Build a set of CSS condition texts that match active breakpoints
|
|
845
|
-
const activeConditions = new Set<string>();
|
|
846
|
-
for (const name of activeBreakpoints) {
|
|
847
|
-
if (docMedia[name]) {
|
|
848
|
-
activeConditions.add(docMedia[name]);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
const overrides = collectMediaOverrides(document.styleSheets, activeConditions);
|
|
852
|
-
applyOverridesToCanvas(canvasEl, overrides);
|
|
853
|
-
}
|
|
854
|
-
|
|
855
734
|
export function renderOverlays() {
|
|
856
735
|
overlaysPanel.render();
|
|
857
736
|
}
|
|
858
|
-
|
|
859
|
-
// ─── Style-tag hygiene ────────────────────────────────────────────────────────
|
|
860
|
-
|
|
861
|
-
let _sweepTimer: ReturnType<typeof setTimeout> | undefined;
|
|
862
|
-
|
|
863
|
-
/**
|
|
864
|
-
* Remove scoped <style> tags whose owner elements are no longer in the document. The runtime emits
|
|
865
|
-
* one tag per styled element (nested selectors / media rules) and full re-renders orphan the
|
|
866
|
-
* previous tree's tags. Debounced so in-flight panel renders finish attaching first.
|
|
867
|
-
*/
|
|
868
|
-
function scheduleStyleTagSweep() {
|
|
869
|
-
clearTimeout(_sweepTimer);
|
|
870
|
-
_sweepTimer = setTimeout(() => {
|
|
871
|
-
for (const tag of document.head.querySelectorAll("style[data-jx-owner]")) {
|
|
872
|
-
const uid = (tag as HTMLElement).dataset.jxOwner;
|
|
873
|
-
if (uid && !document.querySelector(`[data-jx="${uid}"]`)) {
|
|
874
|
-
tag.remove();
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
}, 250);
|
|
878
|
-
}
|
|
@@ -16,13 +16,13 @@ import { activeTab } from "../workspace/workspace";
|
|
|
16
16
|
import { view } from "../view";
|
|
17
17
|
import { getLayerSlot } from "../ui/layers";
|
|
18
18
|
import { findCanvasElement, getActivePanel, panelMediaToActiveMedia } from "./canvas-helpers";
|
|
19
|
+
import { rectOf } from "../utils/geometry";
|
|
19
20
|
import type { TemplateResult } from "lit-html";
|
|
20
21
|
|
|
21
22
|
let _ctx: {
|
|
22
23
|
getCanvasMode: () => string;
|
|
23
24
|
getZoom: () => number;
|
|
24
25
|
setZoomDirect: (zoom: number) => void;
|
|
25
|
-
renderStylebookOverlays: () => void;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
let _zoomIndicatorEl: HTMLElement | null = null;
|
|
@@ -34,14 +34,12 @@ let _zoomIndicatorEl: HTMLElement | null = null;
|
|
|
34
34
|
* getCanvasMode: () => string;
|
|
35
35
|
* getZoom: () => number;
|
|
36
36
|
* setZoomDirect: (zoom: number) => void;
|
|
37
|
-
* renderStylebookOverlays: () => void;
|
|
38
37
|
* }} ctx
|
|
39
38
|
*/
|
|
40
39
|
export function initCanvasUtils(ctx: {
|
|
41
40
|
getCanvasMode: () => string;
|
|
42
41
|
getZoom: () => number;
|
|
43
42
|
setZoomDirect: (zoom: number) => void;
|
|
44
|
-
renderStylebookOverlays: () => void;
|
|
45
43
|
}) {
|
|
46
44
|
_ctx = ctx;
|
|
47
45
|
}
|
|
@@ -64,14 +62,9 @@ export function canvasPanelTemplate(
|
|
|
64
62
|
// Which lit runs synchronously during render — before any consumer reads them.
|
|
65
63
|
const panel = {
|
|
66
64
|
_width: width || null,
|
|
67
|
-
activeBreakpoints: null,
|
|
68
65
|
canvas: null,
|
|
69
|
-
dropLine: null,
|
|
70
66
|
element: null,
|
|
71
|
-
liveCtx: null,
|
|
72
67
|
mediaName: mediaName || "",
|
|
73
|
-
overlay: null,
|
|
74
|
-
overlayClk: null,
|
|
75
68
|
ready: false,
|
|
76
69
|
renderScope: null,
|
|
77
70
|
scrollContainer: null,
|
|
@@ -117,32 +110,6 @@ export function canvasPanelTemplate(
|
|
|
117
110
|
}
|
|
118
111
|
})}
|
|
119
112
|
></div>
|
|
120
|
-
<div
|
|
121
|
-
class="canvas-panel-overlay"
|
|
122
|
-
${ref((el) => {
|
|
123
|
-
if (el) {
|
|
124
|
-
panel.overlay = el as HTMLElement;
|
|
125
|
-
}
|
|
126
|
-
})}
|
|
127
|
-
>
|
|
128
|
-
<div
|
|
129
|
-
class="canvas-drop-indicator"
|
|
130
|
-
style="display:none"
|
|
131
|
-
${ref((el) => {
|
|
132
|
-
if (el) {
|
|
133
|
-
panel.dropLine = el as HTMLElement;
|
|
134
|
-
}
|
|
135
|
-
})}
|
|
136
|
-
></div>
|
|
137
|
-
</div>
|
|
138
|
-
<div
|
|
139
|
-
class="canvas-panel-click"
|
|
140
|
-
${ref((el) => {
|
|
141
|
-
if (el) {
|
|
142
|
-
panel.overlayClk = el as HTMLElement;
|
|
143
|
-
}
|
|
144
|
-
})}
|
|
145
|
-
></div>
|
|
146
113
|
</div>
|
|
147
114
|
</div>
|
|
148
115
|
`;
|
|
@@ -196,10 +163,9 @@ export function applyTransform() {
|
|
|
196
163
|
const zoom = _ctx.getZoom();
|
|
197
164
|
view.panzoomWrap.style.transform = `translate(${view.panX}px, ${view.panY}px) scale(${zoom})`;
|
|
198
165
|
renderZoomIndicator();
|
|
166
|
+
// Overlays live INSIDE the scaled panzoom-wrap (iframe hosts draw there), so no per-mode redraw
|
|
167
|
+
// Is needed here — the flush only re-anchors the fixed block-action-bar.
|
|
199
168
|
renderOnly("overlays");
|
|
200
|
-
if (_ctx.getCanvasMode() === "stylebook") {
|
|
201
|
-
_ctx.renderStylebookOverlays();
|
|
202
|
-
}
|
|
203
169
|
}
|
|
204
170
|
|
|
205
171
|
/** Calculate zoom + pan to fit all panels within the viewport. */
|
|
@@ -218,7 +184,7 @@ export function fitToScreen() {
|
|
|
218
184
|
totalPanelWidth += gap * Math.max(0, canvasPanels.length - 1) + padding;
|
|
219
185
|
|
|
220
186
|
const zoom = _ctx.getZoom();
|
|
221
|
-
const wrapRect = view.panzoomWrap
|
|
187
|
+
const wrapRect = rectOf(view.panzoomWrap);
|
|
222
188
|
const unscaledHeight = wrapRect.height / zoom;
|
|
223
189
|
const maxPanelHeight = unscaledHeight + padding;
|
|
224
190
|
|
|
@@ -258,8 +224,8 @@ export function resetZoomIndicator() {
|
|
|
258
224
|
* @param {{ scrollContainer?: HTMLElement | null }} [panel]
|
|
259
225
|
*/
|
|
260
226
|
function _panToEl(el: HTMLElement, panel?: { scrollContainer?: HTMLElement | null }) {
|
|
261
|
-
const wrapRect = canvasWrap
|
|
262
|
-
const elRect = el
|
|
227
|
+
const wrapRect = rectOf(canvasWrap);
|
|
228
|
+
const elRect = rectOf(el);
|
|
263
229
|
const elCenterY = elRect.top + elRect.height / 2 - wrapRect.top;
|
|
264
230
|
const vpCenterY = wrapRect.height / 2;
|
|
265
231
|
const offsetY = vpCenterY - elCenterY;
|
|
@@ -270,23 +236,39 @@ function _panToEl(el: HTMLElement, panel?: { scrollContainer?: HTMLElement | nul
|
|
|
270
236
|
top: panel.scrollContainer.scrollTop - offsetY,
|
|
271
237
|
});
|
|
272
238
|
} else {
|
|
273
|
-
|
|
274
|
-
const targetY = startY + offsetY;
|
|
275
|
-
const start = performance.now();
|
|
276
|
-
const duration = 250;
|
|
277
|
-
const step = (now: number) => {
|
|
278
|
-
const t = Math.min((now - start) / duration, 1);
|
|
279
|
-
const ease = t * (2 - t);
|
|
280
|
-
view.panY = startY + (targetY - startY) * ease;
|
|
281
|
-
applyTransform();
|
|
282
|
-
if (t < 1) {
|
|
283
|
-
requestAnimationFrame(step);
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
requestAnimationFrame(step);
|
|
239
|
+
animatePanBy(offsetY);
|
|
287
240
|
}
|
|
288
241
|
}
|
|
289
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Pan the panzoom canvas vertically so a PARENT-VIEWPORT rect is centered — for callers whose
|
|
245
|
+
* target lives inside an iframe (no parent DOM element to measure; the host converts the measured
|
|
246
|
+
* iframe rect and passes it here).
|
|
247
|
+
*/
|
|
248
|
+
export function panToParentRect(rect: { top: number; height: number }) {
|
|
249
|
+
const wrapRect = rectOf(canvasWrap);
|
|
250
|
+
const elCenterY = rect.top + rect.height / 2 - wrapRect.top;
|
|
251
|
+
animatePanBy(wrapRect.height / 2 - elCenterY);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Animate `view.panY` by `offsetY` with the shared 250ms ease-out. */
|
|
255
|
+
function animatePanBy(offsetY: number) {
|
|
256
|
+
const startY = view.panY;
|
|
257
|
+
const targetY = startY + offsetY;
|
|
258
|
+
const start = performance.now();
|
|
259
|
+
const duration = 250;
|
|
260
|
+
const step = (now: number) => {
|
|
261
|
+
const t = Math.min((now - start) / duration, 1);
|
|
262
|
+
const ease = t * (2 - t);
|
|
263
|
+
view.panY = startY + (targetY - startY) * ease;
|
|
264
|
+
applyTransform();
|
|
265
|
+
if (t < 1) {
|
|
266
|
+
requestAnimationFrame(step);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
requestAnimationFrame(step);
|
|
270
|
+
}
|
|
271
|
+
|
|
290
272
|
/**
|
|
291
273
|
* Pan the canvas vertically so the element at `path` is centered in the viewport.
|
|
292
274
|
*
|
|
@@ -304,16 +286,6 @@ export function panToElement(path: (string | number)[]) {
|
|
|
304
286
|
_panToEl(el, panel);
|
|
305
287
|
}
|
|
306
288
|
|
|
307
|
-
/**
|
|
308
|
-
* Pan the canvas vertically to center a specific DOM element (e.g. stylebook elements).
|
|
309
|
-
*
|
|
310
|
-
* @param {HTMLElement} el
|
|
311
|
-
*/
|
|
312
|
-
export function panToCanvasEl(el: HTMLElement) {
|
|
313
|
-
const panel = getActivePanel();
|
|
314
|
-
_panToEl(el, panel ?? undefined);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
289
|
/**
|
|
318
290
|
* Render the floating zoom indicator at the bottom center of canvas-wrap. Uses position: fixed,
|
|
319
291
|
* computed from canvas-wrap bounds.
|
|
@@ -368,7 +340,7 @@ export function positionZoomIndicator() {
|
|
|
368
340
|
if (!_zoomIndicatorEl) {
|
|
369
341
|
return;
|
|
370
342
|
}
|
|
371
|
-
const rect = canvasWrap
|
|
343
|
+
const rect = rectOf(canvasWrap);
|
|
372
344
|
_zoomIndicatorEl.style.left = `${rect.left + rect.width / 2}px`;
|
|
373
345
|
_zoomIndicatorEl.style.top = `${rect.bottom - 32}px`;
|
|
374
346
|
_zoomIndicatorEl.style.transform = "translateX(-50%)";
|