@jxsuite/studio 6.1.0 → 6.1.1
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/chunks/2020-va2wwra4.js +4 -0
- package/dist/chunks/adapter-f93pafd7.js +4 -0
- package/dist/chunks/{cf-account-picker-n8hr38v7.js → cf-account-picker-af1h381v.js} +3 -3
- package/dist/chunks/cf-settings-125tdm0w.js +4 -0
- package/dist/chunks/components-ax2f1fnn.js +4 -0
- package/dist/chunks/content-source-108aqtk1.js +4 -0
- package/dist/chunks/{convert-to-repeater-7wns8kf1.js → convert-to-repeater-ry5rmppr.js} +3 -3
- package/dist/chunks/editor-pfgdmkb0.js +4 -0
- package/dist/chunks/entry-commands-t0k1q0kx.js +4 -0
- package/dist/chunks/{extension-sections-na7rj3nr.js → extension-sections-6mwh2hf8.js} +3 -3
- package/dist/chunks/file-ops-meej5mqf.js +4 -0
- package/dist/chunks/files-44vp8a2s.js +4 -0
- package/dist/chunks/format-host-7q1petb9.js +4 -0
- package/dist/chunks/fs-events-h2mwnxaz.js +4 -0
- package/dist/chunks/git-panel-zngbymvd.js +4 -0
- package/dist/chunks/{grid-view-7tt4bxw1.js → grid-view-sx9vynsp.js} +15 -15
- package/dist/chunks/icons-p44xhjf0.js +4 -0
- package/dist/chunks/iframe-host-qk3ycq87.js +4 -0
- package/dist/chunks/{index-gybvv7a6.js → index-2veqmnhr.js} +3 -3
- package/dist/chunks/index-wkyfjeqz.js +4 -0
- package/dist/chunks/{javascript-rnvdp3k4.js → javascript-8nbvs12s.js} +3 -3
- package/dist/chunks/jsonMode-s8kt9dsj.js +13 -0
- package/dist/chunks/jx-validate-psgd7vsx.js +4 -0
- package/dist/chunks/layers-panel-1kq2jyfk.js +4 -0
- package/dist/chunks/layers-xd92fg8d.js +4 -0
- package/dist/chunks/lit-html-ac4cwwr4.js +4 -0
- package/dist/chunks/media-picker-0svzkdyb.js +4 -0
- package/dist/chunks/menu-5c55ea3d.js +4 -0
- package/dist/chunks/{monaco-binding-2jnhjjvr.js → monaco-binding-z6r70pdt.js} +3 -3
- package/dist/chunks/monaco-buffer-fz6gq5y7.js +4 -0
- package/dist/chunks/monaco-setup-q4axkgj7.js +4 -0
- package/dist/chunks/notify-0xgtn8q1.js +4 -0
- package/dist/chunks/platform-v1qctbe6.js +4 -0
- package/dist/chunks/publish-panel-ym1fmgq5.js +4 -0
- package/dist/chunks/quick-search-a54e6t5n.js +4 -0
- package/dist/chunks/references-gmqp82fs.js +4 -0
- package/dist/chunks/right-panel-v7t9awbk.js +4 -0
- package/dist/chunks/section-registry-vhyf3zsr.js +4 -0
- package/dist/chunks/studio-0bhx5hz6.js +5 -0
- package/dist/chunks/studio-0k1ddnyy.js +6 -0
- package/dist/chunks/studio-1b04pd0j.js +64 -0
- package/dist/chunks/{studio-5g0ag7c0.js → studio-1q1sh2ax.js} +3 -3
- package/dist/chunks/studio-4dwbj6gb.js +5 -0
- package/dist/chunks/studio-4tanh828.js +5 -0
- package/dist/chunks/{studio-vzacdfsc.js → studio-55h4mstj.js} +3 -3
- package/dist/chunks/studio-5tnvjkz4.js +5 -0
- package/dist/chunks/{studio-rggdb3hm.js → studio-60dqgb0c.js} +4 -4
- package/dist/chunks/studio-61dp09qg.js +9 -0
- package/dist/chunks/studio-63epcrj2.js +5 -0
- package/dist/chunks/studio-74k29dy9.js +5 -0
- package/dist/chunks/{studio-qvasen3f.js → studio-7vg6jcd3.js} +4 -4
- package/dist/chunks/studio-7zx31knw.js +9 -0
- package/dist/chunks/studio-8bxk97km.js +5 -0
- package/dist/chunks/{studio-anyrgzvb.js → studio-8grwxtqk.js} +4 -4
- package/dist/chunks/studio-933e30n4.js +5 -0
- package/dist/chunks/{studio-h0xgswew.js → studio-98wnwhbg.js} +4 -4
- package/dist/chunks/studio-9mccjwjh.js +14 -0
- package/dist/chunks/studio-a2ke1e4t.js +5 -0
- package/dist/chunks/studio-e3a2a6v3.js +4 -0
- package/dist/chunks/{studio-vk9ezads.js → studio-e81q1vb9.js} +4 -4
- package/dist/chunks/{studio-z8w183sh.js → studio-epgrmqqm.js} +4 -4
- package/dist/chunks/studio-g1shz58g.js +5 -0
- package/dist/chunks/studio-gf0fz03c.js +5 -0
- package/dist/chunks/studio-gjtpe62q.js +6 -0
- package/dist/chunks/{studio-y78s8r2n.js → studio-gkskhg7m.js} +4 -4
- package/dist/chunks/studio-gx4dnjzd.js +5 -0
- package/dist/chunks/{studio-pxg4ap1e.js → studio-h46vtzk8.js} +4 -4
- package/dist/chunks/studio-j03zga3y.js +9 -0
- package/dist/chunks/{studio-ar8jcf2y.js → studio-jhpxeckz.js} +4 -4
- package/dist/chunks/{studio-w4c0xmrw.js → studio-k01r7ykp.js} +4 -4
- package/dist/chunks/{studio-cpxkgjrj.js → studio-kggey1jt.js} +4 -4
- package/dist/chunks/{studio-zxp13sss.js → studio-kk7h30ne.js} +6 -6
- package/dist/chunks/{studio-ecaz25vk.js → studio-kt1zqney.js} +4 -4
- package/dist/chunks/{studio-zygnmm01.js → studio-m3cwhvye.js} +4 -4
- package/dist/chunks/{studio-2sqcndak.js → studio-mcayt6hz.js} +4 -4
- package/dist/chunks/{studio-qqz8e5gk.js → studio-myjcghv3.js} +4 -4
- package/dist/chunks/studio-n8bxwc3y.js +4 -0
- package/dist/chunks/{studio-wfjqhhbk.js → studio-ngm8gvpb.js} +4 -4
- package/dist/chunks/{studio-zscd9ty4.js → studio-nj5a01jy.js} +196 -196
- package/dist/chunks/{studio-3bpdkq2s.js → studio-pcgz8heb.js} +4 -4
- package/dist/chunks/{studio-v2bvmbjj.js → studio-pre0skq6.js} +4 -4
- package/dist/chunks/{studio-ghkb32r8.js → studio-qcsq0t15.js} +4 -4
- package/dist/chunks/studio-qkstvve9.js +5 -0
- package/dist/chunks/studio-qv1pwd3c.js +5 -0
- package/dist/chunks/{studio-bmn4mn9j.js → studio-rc6dvxjb.js} +5 -5
- package/dist/chunks/{studio-qbj760tx.js → studio-rv11kfwa.js} +7 -7
- package/dist/chunks/studio-s8dmmz66.js +5 -0
- package/dist/chunks/studio-sp5xzbj4.js +6 -0
- package/dist/chunks/{studio-tyasmc39.js → studio-szsjbj0g.js} +4 -4
- package/dist/chunks/studio-t1xzawhr.js +16 -0
- package/dist/chunks/{studio-skprc6ef.js → studio-t6b6qe31.js} +4 -4
- package/dist/chunks/{studio-ghrhy2y7.js → studio-tdcprbqb.js} +4 -4
- package/dist/chunks/{studio-txjjzrsm.js → studio-vrmcjf9g.js} +5 -5
- package/dist/chunks/studio-vyc0b4he.js +10 -0
- package/dist/chunks/{studio-5g4t8v6w.js → studio-w0cgf6jg.js} +4 -4
- package/dist/chunks/{studio-acdksxx6.js → studio-w3hskzqk.js} +4 -4
- package/dist/chunks/{studio-0x3jyb8r.js → studio-wszjyh0j.js} +4 -4
- package/dist/chunks/studio-xgrgcdyh.js +9 -0
- package/dist/chunks/{studio-qpqa7vk3.js → studio-xz2yxfcb.js} +4 -4
- package/dist/chunks/{studio-77kt594t.js → studio-y6n42tgs.js} +4 -4
- package/dist/chunks/{studio-19x1c75w.js → studio-yb560vxr.js} +4 -4
- package/dist/chunks/{studio-wgh5kxpj.js → studio-yn9f8vtx.js} +4 -4
- package/dist/chunks/{studio-a2138x5k.js → studio-yw05w8f0.js} +5 -5
- package/dist/chunks/{studio-h9c4p2tx.js → studio-z16sgavc.js} +4 -4
- package/dist/chunks/studio-zey7zsdw.js +12 -0
- package/dist/chunks/{studio-g7nse7zb.js → studio-zyqn40w4.js} +4 -4
- package/dist/chunks/tsMode-2frah6h1.js +14 -0
- package/dist/chunks/workspace-zt81g1ms.js +4 -0
- package/dist/chunks/ws-client-n2bh4hkf.js +4 -0
- package/dist/iframe-entry.js +13 -13
- package/dist/manifest.json +62 -62
- package/dist/studio.js +77 -77
- package/dist/workers/ts.worker.js +3 -3
- package/package.json +10 -10
- package/src/canvas/canvas-render.ts +4 -1
- package/src/canvas/canvas-utils.ts +73 -15
- package/src/canvas/iframe-host.ts +67 -21
- package/src/canvas/iframe-inline-edit.ts +22 -4
- package/src/canvas/iframe-interaction.ts +7 -1
- package/src/canvas/iframe-protocol.ts +3 -0
- package/src/canvas/iframe-render.ts +5 -2
- package/src/commands/defaults.ts +4 -1
- package/src/commands/live-context.ts +3 -1
- package/src/editor/shortcuts.ts +55 -27
- package/src/files/media-paths.ts +3 -1
- package/src/panels/editors.ts +3 -1
- package/src/panels/style-panel.ts +36 -11
- package/src/platforms/devserver.ts +47 -12
- package/src/studio.ts +14 -2
- package/src/surfaces/library-pane.ts +6 -0
- package/src/surfaces/rail.json +126 -73
- package/src/surfaces/rail.ts +31 -5
- package/src/tabs/typing.ts +31 -0
- package/src/utils/edit-display.ts +30 -1
- package/src/workspace/workspace.ts +7 -0
- package/dist/chunks/2020-nkxms0ay.js +0 -4
- package/dist/chunks/adapter-m948ab5f.js +0 -4
- package/dist/chunks/cf-settings-356xdtne.js +0 -4
- package/dist/chunks/components-yk3t844e.js +0 -4
- package/dist/chunks/content-source-bawmyc09.js +0 -4
- package/dist/chunks/editor-6sc8nacq.js +0 -4
- package/dist/chunks/entry-commands-8ay9fh81.js +0 -4
- package/dist/chunks/file-ops-gye0qw56.js +0 -4
- package/dist/chunks/files-dmc2xvcq.js +0 -4
- package/dist/chunks/format-host-t76b53f8.js +0 -4
- package/dist/chunks/fs-events-cg4tpz06.js +0 -4
- package/dist/chunks/git-panel-yef7bshy.js +0 -4
- package/dist/chunks/icons-qbh8akb1.js +0 -4
- package/dist/chunks/iframe-host-tmz30gfp.js +0 -4
- package/dist/chunks/index-nag78a9h.js +0 -4
- package/dist/chunks/jsonMode-zhwsprqk.js +0 -13
- package/dist/chunks/jx-validate-3gbp5nkn.js +0 -4
- package/dist/chunks/layers-fvz99859.js +0 -4
- package/dist/chunks/layers-panel-gchmbf9p.js +0 -4
- package/dist/chunks/lit-html-9agdspkt.js +0 -4
- package/dist/chunks/media-picker-rhjvyy00.js +0 -4
- package/dist/chunks/menu-2e4y9j7b.js +0 -4
- package/dist/chunks/monaco-buffer-rp21359g.js +0 -4
- package/dist/chunks/monaco-setup-vhh7gayr.js +0 -4
- package/dist/chunks/notify-h3www560.js +0 -4
- package/dist/chunks/platform-hz34wcd7.js +0 -4
- package/dist/chunks/publish-panel-exsmjzyj.js +0 -4
- package/dist/chunks/quick-search-pgaqeh0m.js +0 -4
- package/dist/chunks/references-2cgdxnb8.js +0 -4
- package/dist/chunks/right-panel-jf856w5w.js +0 -4
- package/dist/chunks/section-registry-jnf8r5gy.js +0 -4
- package/dist/chunks/studio-0f55h9ye.js +0 -5
- package/dist/chunks/studio-0sqdhnew.js +0 -12
- package/dist/chunks/studio-3aey7dd3.js +0 -5
- package/dist/chunks/studio-47y6fbpf.js +0 -5
- package/dist/chunks/studio-4m3826v2.js +0 -5
- package/dist/chunks/studio-4nx2dj1g.js +0 -4
- package/dist/chunks/studio-4xv5s47r.js +0 -9
- package/dist/chunks/studio-5py436sf.js +0 -5
- package/dist/chunks/studio-8f87n763.js +0 -10
- package/dist/chunks/studio-8mb00b84.js +0 -5
- package/dist/chunks/studio-8p51gq2h.js +0 -6
- package/dist/chunks/studio-954ds834.js +0 -5
- package/dist/chunks/studio-9ft45c83.js +0 -9
- package/dist/chunks/studio-a9yzstqj.js +0 -5
- package/dist/chunks/studio-e7xxa0sj.js +0 -5
- package/dist/chunks/studio-fzdywr6p.js +0 -9
- package/dist/chunks/studio-g5fmf0c1.js +0 -5
- package/dist/chunks/studio-geq1ea6k.js +0 -14
- package/dist/chunks/studio-grawhey1.js +0 -5
- package/dist/chunks/studio-jyzj5cxs.js +0 -5
- package/dist/chunks/studio-mgx8yhnz.js +0 -64
- package/dist/chunks/studio-mpb04y1d.js +0 -6
- package/dist/chunks/studio-n17mty64.js +0 -5
- package/dist/chunks/studio-nwh2eqsk.js +0 -6
- package/dist/chunks/studio-q1h883bt.js +0 -16
- package/dist/chunks/studio-q4tv82h7.js +0 -9
- package/dist/chunks/studio-sghy936h.js +0 -4
- package/dist/chunks/studio-tt2nactf.js +0 -5
- package/dist/chunks/studio-xasj0796.js +0 -5
- package/dist/chunks/tsMode-gr0m4dzr.js +0 -14
- package/dist/chunks/workspace-m15qtcqp.js +0 -4
- package/dist/chunks/ws-client-nf61frr1.js +0 -4
|
@@ -563,6 +563,9 @@ export type IframeToParent =
|
|
|
563
563
|
// Inline editing started in the iframe (the parent shows the format toolbar from here). `prop`
|
|
564
564
|
// Names the component-instance prop when the session edits prop-bound text (plain sessions).
|
|
565
565
|
| { kind: "editStart"; path: (string | number)[]; prop?: string }
|
|
566
|
+
// The first keystroke of a typing burst, ahead of the idle tick's commit: the parent marks the tab
|
|
567
|
+
// Dirty now, so Save is enabled the instant there is text a save would keep.
|
|
568
|
+
| { kind: "editInput" }
|
|
566
569
|
// Committed inline-edit content (rich `children` else `textContent`) for the parent to persist.
|
|
567
570
|
| {
|
|
568
571
|
kind: "editCommit";
|
|
@@ -1018,8 +1018,11 @@ export async function renderResolvedDocument(opts: {
|
|
|
1018
1018
|
// Sections feeding back into an ever-growing height. Set every render (the iframe always wants it).
|
|
1019
1019
|
setCanvasViewportTranspose(true);
|
|
1020
1020
|
// De-link `<a href>` in design/edit so clicks select the anchor instead of navigating the iframe;
|
|
1021
|
-
// Preview keeps real links live (mirrors the server-function gate above).
|
|
1022
|
-
|
|
1021
|
+
// Preview keeps real links live (mirrors the server-function gate above). So does Project Styles:
|
|
1022
|
+
// An `<a>` without `href` is not a link, so `:link`, `:any-link` and the UA's link colour could
|
|
1023
|
+
// Never match its specimen, and a project could not see the link styles it was setting there.
|
|
1024
|
+
// Its clicks are refused in the frame instead (`iframe-interaction.ts`).
|
|
1025
|
+
setCanvasDelinkAnchors(opts.mode !== "preview" && opts.mode !== "stylebook");
|
|
1023
1026
|
/* De-popover in the same modes and for the same reason: an OPEN popover is in the top layer,
|
|
1024
1027
|
whose containing block is the viewport — a fiction here, since the frame is sized to its own
|
|
1025
1028
|
content — and which contributes to no ancestor's scrollable overflow, so the artboard could
|
package/src/commands/defaults.ts
CHANGED
|
@@ -236,7 +236,10 @@ export function defaultCommands(deps: CommandDeps): AnyCommand[] {
|
|
|
236
236
|
group: "1_file",
|
|
237
237
|
undo: "none",
|
|
238
238
|
when: documentOpen,
|
|
239
|
-
|
|
239
|
+
// Nothing to save is a disabled button, not a no-op one. A disabled chord is still claimed,
|
|
240
|
+
// So ⌘S on a clean document never falls through to the browser's own Save Page.
|
|
241
|
+
enablement: (ctx) => ctx.document.dirty,
|
|
242
|
+
requires: "unsaved changes",
|
|
240
243
|
run: () => deps.saveDocument(),
|
|
241
244
|
},
|
|
242
245
|
{
|
|
@@ -32,6 +32,7 @@ import { shell } from "../shell";
|
|
|
32
32
|
import { activeTab, workspace } from "../workspace/workspace";
|
|
33
33
|
import { primarySelection } from "../tabs/selection";
|
|
34
34
|
import { canRedo, canUndo } from "../tabs/transact";
|
|
35
|
+
import { hasTyping } from "../tabs/typing";
|
|
35
36
|
import { collabState } from "../collab/collab-state";
|
|
36
37
|
|
|
37
38
|
import { canvasViewForMode, editorKindForMode, emptyContext } from "./context";
|
|
@@ -133,7 +134,8 @@ export function createLiveContext(sources: LiveContextSources): () => CommandCon
|
|
|
133
134
|
ctx.git.dirtyCount = shell.git.status?.files.length ?? 0;
|
|
134
135
|
|
|
135
136
|
ctx.document.open = tab !== null;
|
|
136
|
-
|
|
137
|
+
// Typing still in a source buffer is unsaved work too, though the document has not seen it.
|
|
138
|
+
ctx.document.dirty = tab ? tab.doc.dirty === true || hasTyping(tab.id) : false;
|
|
137
139
|
ctx.document.mode = tab?.doc.mode ?? "";
|
|
138
140
|
ctx.document.canUndo = tab ? canUndo(tab) : false;
|
|
139
141
|
ctx.document.canRedo = tab ? canRedo(tab) : false;
|
package/src/editor/shortcuts.ts
CHANGED
|
@@ -51,16 +51,16 @@ import {
|
|
|
51
51
|
} from "../tabs/transact";
|
|
52
52
|
import {
|
|
53
53
|
applyEditZoom,
|
|
54
|
-
markExplicitZoom,
|
|
55
54
|
requestEditZoom,
|
|
56
55
|
setEditZoom,
|
|
56
|
+
setUserZoom,
|
|
57
|
+
stageZoom,
|
|
57
58
|
} from "../canvas/canvas-utils";
|
|
58
59
|
import { openQuickSearch } from "../panels/quick-search";
|
|
59
60
|
import { inspectorTab } from "../panels/right-panel";
|
|
60
61
|
import { requestClose } from "../panels/tab-strip";
|
|
61
62
|
import { layerHost } from "../ui/layers";
|
|
62
63
|
import { openDialogSurface } from "../surfaces/dialog";
|
|
63
|
-
import { rectOf } from "../utils/geometry";
|
|
64
64
|
import {
|
|
65
65
|
DOCK_IDS,
|
|
66
66
|
requireNavigatorPanelId,
|
|
@@ -78,6 +78,8 @@ import { hasElementSelection, hasSelection, inCanvas, keyScopeStack } from "../c
|
|
|
78
78
|
import { defaultCommands } from "../commands/defaults";
|
|
79
79
|
import { setActiveRegistry } from "../commands/active-registry";
|
|
80
80
|
import { runReported } from "../commands/run-reported";
|
|
81
|
+
import { pointerAnchor } from "../canvas/iframe-host";
|
|
82
|
+
import type { PointerAnchor } from "../canvas/iframe-host";
|
|
81
83
|
import type { CommandContext } from "../commands/context";
|
|
82
84
|
import type { DockId as CommandDockId } from "../commands/defaults";
|
|
83
85
|
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
@@ -314,11 +316,15 @@ function redoDocument(): void {
|
|
|
314
316
|
|
|
315
317
|
// ─── Canvas zoom ──────────────────────────────────────────────────────────────
|
|
316
318
|
|
|
317
|
-
/**
|
|
318
|
-
const ZOOM_MIN = 0.05;
|
|
319
|
-
const ZOOM_MAX = 5;
|
|
319
|
+
/** One chord step. Both zooms clamp inside `canvas-utils`. */
|
|
320
320
|
const ZOOM_STEP = 1.2;
|
|
321
321
|
|
|
322
|
+
/** How long after an Edit zoom step its anchor is measured again: the zoom's frame, then a reflow. */
|
|
323
|
+
const EDIT_ZOOM_SETTLE_MS = 34;
|
|
324
|
+
|
|
325
|
+
/** How long an Edit zoom burst must be idle before its anchor is let go. */
|
|
326
|
+
const EDIT_ZOOM_RELEASE_MS = 300;
|
|
327
|
+
|
|
322
328
|
/** Whether the zoom verbs address the reflowing content zoom rather than the artboard transform. */
|
|
323
329
|
function isContentZoom(ctx: CommandContext): boolean {
|
|
324
330
|
return ctx.canvas.view === "edit";
|
|
@@ -346,8 +352,8 @@ function zoomBy(ctx: CommandContext, factor: number, redraw: () => void): void {
|
|
|
346
352
|
setEditZoom((tab.session.ui.editZoom ?? 1) * factor);
|
|
347
353
|
return;
|
|
348
354
|
}
|
|
349
|
-
|
|
350
|
-
|
|
355
|
+
// Around the stage's middle, like the zoom pod's buttons.
|
|
356
|
+
setUserZoom(stageZoom() * factor);
|
|
351
357
|
redraw();
|
|
352
358
|
}
|
|
353
359
|
|
|
@@ -1074,6 +1080,26 @@ let _stageContext: StageContext | null = null;
|
|
|
1074
1080
|
*/
|
|
1075
1081
|
export function installStageGestures(surface: CanvasSurface): () => void {
|
|
1076
1082
|
const canvasWrap = surface.wrap;
|
|
1083
|
+
/* An Edit zoom burst's anchor: the element under the cursor when the burst began. The frame
|
|
1084
|
+
reflows at every step, so after each one the element is measured again and the scroller takes
|
|
1085
|
+
back whatever it moved; the anchor is let go once the burst has been idle a while. */
|
|
1086
|
+
let zoomAnchor: PointerAnchor | null = null;
|
|
1087
|
+
let zoomRelease: ReturnType<typeof setTimeout> | undefined;
|
|
1088
|
+
let zoomCorrecting = false;
|
|
1089
|
+
const correctEditZoom = () => {
|
|
1090
|
+
const anchor = zoomAnchor;
|
|
1091
|
+
if (!anchor || zoomCorrecting) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
zoomCorrecting = true;
|
|
1095
|
+
void anchor.settle().then((top) => {
|
|
1096
|
+
zoomCorrecting = false;
|
|
1097
|
+
const sc = canvasWrap.querySelector<HTMLElement>('[part="edit-canvas"]');
|
|
1098
|
+
if (top !== null && sc) {
|
|
1099
|
+
sc.scrollTop += top - anchor.top;
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
};
|
|
1077
1103
|
const getContext = () => _stageContext?.(surface) ?? stageless();
|
|
1078
1104
|
const controller = new AbortController();
|
|
1079
1105
|
const { signal } = controller;
|
|
@@ -1093,7 +1119,16 @@ export function installStageGestures(surface: CanvasSurface): () => void {
|
|
|
1093
1119
|
if (e.ctrlKey || e.metaKey) {
|
|
1094
1120
|
e.preventDefault();
|
|
1095
1121
|
const editZoom = paneTab?.session.ui.editZoom ?? 1;
|
|
1096
|
-
|
|
1122
|
+
const canvasEl = surface.panels[0]?.canvas;
|
|
1123
|
+
zoomAnchor ??= canvasEl ? pointerAnchor(canvasEl) : null;
|
|
1124
|
+
requestEditZoom(editZoom * (1 + -e.deltaY * 0.005), surface, e.clientY);
|
|
1125
|
+
// Measured after the zoom's own frame has applied, and the frame has had one to reflow.
|
|
1126
|
+
setTimeout(correctEditZoom, EDIT_ZOOM_SETTLE_MS);
|
|
1127
|
+
clearTimeout(zoomRelease);
|
|
1128
|
+
zoomRelease = setTimeout(() => {
|
|
1129
|
+
correctEditZoom();
|
|
1130
|
+
zoomAnchor = null;
|
|
1131
|
+
}, EDIT_ZOOM_RELEASE_MS);
|
|
1097
1132
|
return;
|
|
1098
1133
|
}
|
|
1099
1134
|
/* The stage's own scroller, by the `part` the stage document draws it with — the same
|
|
@@ -1125,24 +1160,14 @@ export function installStageGestures(surface: CanvasSurface): () => void {
|
|
|
1125
1160
|
}
|
|
1126
1161
|
e.preventDefault();
|
|
1127
1162
|
if (e.ctrlKey || e.metaKey) {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
const newZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, oldZoom * (1 + delta)));
|
|
1137
|
-
const ratio = newZoom / oldZoom;
|
|
1138
|
-
// Adjust pan so the point under cursor stays stationary
|
|
1139
|
-
setPan(cursorX - (cursorX - panX) * ratio, cursorY - (cursorY - panY) * ratio);
|
|
1140
|
-
if (paneTab) {
|
|
1141
|
-
paneTab.session.ui.zoom = newZoom;
|
|
1142
|
-
}
|
|
1143
|
-
// The author chose this zoom, so re-entering Design keeps it instead of auto-fitting.
|
|
1144
|
-
// On THIS stage: the declared fit is a fact about the document under the cursor.
|
|
1145
|
-
markExplicitZoom(surface);
|
|
1163
|
+
/* Zoom toward the cursor, on THIS stage (a lens pane carries its own scale). The author
|
|
1164
|
+
chose this zoom, so it is declared as the document's fit and re-entering Design keeps
|
|
1165
|
+
it instead of auto-fitting. */
|
|
1166
|
+
setUserZoom(stageZoom(surface) * (1 + -e.deltaY * 0.005), surface, {
|
|
1167
|
+
clientX: e.clientX,
|
|
1168
|
+
clientY: e.clientY,
|
|
1169
|
+
});
|
|
1170
|
+
return;
|
|
1146
1171
|
} else if (e.shiftKey) {
|
|
1147
1172
|
// Shift+scroll = horizontal pan
|
|
1148
1173
|
setPan(panX - e.deltaY, panY);
|
|
@@ -1206,7 +1231,10 @@ export function installStageGestures(surface: CanvasSurface): () => void {
|
|
|
1206
1231
|
{ signal },
|
|
1207
1232
|
);
|
|
1208
1233
|
|
|
1209
|
-
return () =>
|
|
1234
|
+
return () => {
|
|
1235
|
+
clearTimeout(zoomRelease);
|
|
1236
|
+
controller.abort();
|
|
1237
|
+
};
|
|
1210
1238
|
}
|
|
1211
1239
|
|
|
1212
1240
|
/** The context a stage answers with before the bootstrap has published a reader. */
|
package/src/files/media-paths.ts
CHANGED
|
@@ -134,5 +134,7 @@ export function previewFileSrc(path: string): string {
|
|
|
134
134
|
if (space === "repo") {
|
|
135
135
|
return withVersion(`${fileBaseUrl}${encodeProjectPath(normalized)}`, versionOf(normalized));
|
|
136
136
|
}
|
|
137
|
-
|
|
137
|
+
/* Encoded per segment for the `src` only. `mediaSiteUrl` is also what an author's document is
|
|
138
|
+
given, and stays as typed; a `#` or `?` in a filename would cut this URL short. */
|
|
139
|
+
return loopbackAssetSrc(`/${encodeProjectPath(mediaSiteUrl(normalized).slice(1))}`);
|
|
138
140
|
}
|
package/src/panels/editors.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { globalEntries, namedFormulaEntries } from "../ui/formula-catalog";
|
|
|
52
52
|
import { notify } from "../services/notify";
|
|
53
53
|
|
|
54
54
|
import type { BufferWrites } from "../services/monaco-buffer";
|
|
55
|
+
import { noteTyping } from "../tabs/typing";
|
|
55
56
|
import type { OxLintDiagnostic } from "../services/code-services";
|
|
56
57
|
import type { JxPrototypeDef } from "@jxsuite/schema/types";
|
|
57
58
|
import type { JxPath } from "../state";
|
|
@@ -500,8 +501,9 @@ async function mountFunctionEditor(
|
|
|
500
501
|
// Stated as a fact rather than left to be read off the timer, because the timer is only ONE of
|
|
501
502
|
// The ways a buffer gets ahead and the other one is invisible to it. `markTyped` rather than
|
|
502
503
|
// `markAhead`: this text is the AUTHOR'S, so it is also the text a close would destroy, and
|
|
503
|
-
// Format-on-open's (below) is not.
|
|
504
|
+
// Format-on-open's (below) is not. Noted for Save too, so it is enabled ahead of the commit.
|
|
504
505
|
writes.markTyped();
|
|
506
|
+
noteTyping(tab.id);
|
|
505
507
|
|
|
506
508
|
writes.arm(BUFFER_COMMIT, 500, () => {
|
|
507
509
|
editor._commitBody?.(editor.getValue());
|
|
@@ -1471,13 +1471,32 @@ function buildEditor(
|
|
|
1471
1471
|
match, and a menu that offers unmatchable states is a menu people stop reading. What the
|
|
1472
1472
|
element already DECLARES and the active selector are unioned in after, so nothing an author has
|
|
1473
1473
|
written can drop out of it. */
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1474
|
+
/* In Project Styles the coordinate is a TAG PATH (`a`, `table th`) and its states nest under it
|
|
1475
|
+
(`a :hover`). The menu offered the ROOT's states instead, and picking one replaced the tag, so
|
|
1476
|
+
`:hover` styled the document element and a project had no way to write `a:hover` at all. Here
|
|
1477
|
+
the states are the tag's own, spelled as the path they write. */
|
|
1478
|
+
const stylebookPath =
|
|
1479
|
+
stylebookSelector && isTagPath(stylebookSelector) ? stylebookSelector : null;
|
|
1480
|
+
const stylebookLeaf = stylebookPath?.split(" ").at(-1) ?? null;
|
|
1481
|
+
const stylebookDeclared = stylebookPath
|
|
1482
|
+
? Object.keys(resolveNestedTagStyle(style, stylebookPath)).filter((k) => isNestedSelector(k))
|
|
1483
|
+
: [];
|
|
1484
|
+
const selectorOptions = stylebookPath
|
|
1485
|
+
? [
|
|
1486
|
+
...new Set([
|
|
1487
|
+
...[...selectorsForNode({ tagName: stylebookLeaf ?? "div" }), ...stylebookDeclared].map(
|
|
1488
|
+
(state) => `${stylebookPath} ${state}`,
|
|
1489
|
+
),
|
|
1490
|
+
...(activeSelector && activeSelector !== stylebookPath ? [activeSelector] : []),
|
|
1491
|
+
]),
|
|
1492
|
+
]
|
|
1493
|
+
: [
|
|
1494
|
+
...new Set([
|
|
1495
|
+
...selectorsForNode(node),
|
|
1496
|
+
...declaredSelectors,
|
|
1497
|
+
...(activeSelector ? [activeSelector] : []),
|
|
1498
|
+
]),
|
|
1499
|
+
];
|
|
1481
1500
|
|
|
1482
1501
|
// ── The Target Line (§6.2) ─────────────────────────────────────────────────
|
|
1483
1502
|
const stylebookTag = stylebookTagOf(stylebookSelector);
|
|
@@ -1486,15 +1505,19 @@ function buildEditor(
|
|
|
1486
1505
|
setTargetLine({
|
|
1487
1506
|
segments: targetSegments(elementLabel, mediaTab, mediaNames.length > 0, schemeLayer),
|
|
1488
1507
|
selector: {
|
|
1489
|
-
|
|
1508
|
+
// The bare tag path IS the base rule in Project Styles, so it reads as the base entry.
|
|
1509
|
+
value: stylebookPath && activeSelector === stylebookPath ? null : activeSelector,
|
|
1490
1510
|
options: selectorOptions,
|
|
1491
|
-
declared:
|
|
1511
|
+
declared: stylebookPath
|
|
1512
|
+
? new Set(stylebookDeclared.map((state) => `${stylebookPath} ${state}`))
|
|
1513
|
+
: declaredSelectors,
|
|
1492
1514
|
onSelect: (value) => {
|
|
1493
1515
|
const current = activeTab.value;
|
|
1494
1516
|
if (!current) {
|
|
1495
1517
|
return;
|
|
1496
1518
|
}
|
|
1497
|
-
|
|
1519
|
+
// Clearing the state in Project Styles returns to the tag, never to the document root.
|
|
1520
|
+
current.session.ui.activeSelector = value ?? stylebookPath;
|
|
1498
1521
|
/* §6.2's own rule: a control that selects a rendering context has to change the rendering,
|
|
1499
1522
|
or it is a control over a label. `:hover` gets away with not doing this because you can
|
|
1500
1523
|
hover the element; `:popover-open` cannot, because a closed popover is not on the screen
|
|
@@ -1523,7 +1546,9 @@ function buildEditor(
|
|
|
1523
1546
|
isNestedSelector(v.trim()) ? "" : 'A selector must start with ":", ".", "&" or "[".',
|
|
1524
1547
|
}).then((value) => {
|
|
1525
1548
|
if (value && activeTab.value) {
|
|
1526
|
-
activeTab.value.session.ui.activeSelector =
|
|
1549
|
+
activeTab.value.session.ui.activeSelector = stylebookPath
|
|
1550
|
+
? `${stylebookPath} ${value.trim()}`
|
|
1551
|
+
: value.trim();
|
|
1527
1552
|
}
|
|
1528
1553
|
});
|
|
1529
1554
|
},
|
|
@@ -98,6 +98,50 @@ export function createDevServerPlatform() {
|
|
|
98
98
|
*/
|
|
99
99
|
let _collabConnection: Promise<WsCollabConnection> | null = null;
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Bind the server to `root` — the binding its rootless routes and its static fallback (every
|
|
103
|
+
* media thumbnail, every canvas image) resolve against.
|
|
104
|
+
*
|
|
105
|
+
* **The binding is one per SERVER, not per tab.** So an empty root posts nothing: a Studio tab
|
|
106
|
+
* opened with no project used to post `{ root: "" }` at boot, which unbound the server for every
|
|
107
|
+
* other tab and turned their images into 404s. And a tab re-claims it (see
|
|
108
|
+
* {@link reclaimBinding}) whenever it could have been taken.
|
|
109
|
+
*/
|
|
110
|
+
async function postActivate(root: string): Promise<void> {
|
|
111
|
+
if (!root) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const res = await fetch("/__studio/activate", {
|
|
115
|
+
body: JSON.stringify({ root }),
|
|
116
|
+
headers: { "Content-Type": "application/json" },
|
|
117
|
+
method: "POST",
|
|
118
|
+
});
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
const body = (await readJson<ErrorBody>(res).catch(() => ({}))) as ErrorBody;
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Could not open ${root}: ${problemDetail(body) ?? `activation failed (${res.status})`}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Re-bind the server to this tab's project. Two events can take the binding away without this tab
|
|
129
|
+
* hearing of it: another Studio tab activating ITS project, and a restarted `bun run dev`, which
|
|
130
|
+
* comes back bound to nothing. So a tab re-claims it when the author comes back to it (focus) and
|
|
131
|
+
* when the reload stream reconnects (a restart). Only a tab with a project claims anything.
|
|
132
|
+
*/
|
|
133
|
+
function reclaimBinding(): void {
|
|
134
|
+
if (!_projectRoot) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
_activation = postActivate(_projectRoot).catch((error: unknown) => {
|
|
138
|
+
console.error("Project re-activation failed:", error);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (typeof window !== "undefined") {
|
|
142
|
+
window.addEventListener("focus", reclaimBinding);
|
|
143
|
+
}
|
|
144
|
+
|
|
101
145
|
/**
|
|
102
146
|
* Prefix a project-relative path with the active project root for server API calls.
|
|
103
147
|
*
|
|
@@ -162,18 +206,7 @@ export function createDevServerPlatform() {
|
|
|
162
206
|
* @param {string} [root]
|
|
163
207
|
*/
|
|
164
208
|
async activate(root?: string) {
|
|
165
|
-
|
|
166
|
-
const res = await fetch("/__studio/activate", {
|
|
167
|
-
body: JSON.stringify({ root: r }),
|
|
168
|
-
headers: { "Content-Type": "application/json" },
|
|
169
|
-
method: "POST",
|
|
170
|
-
});
|
|
171
|
-
if (!res.ok) {
|
|
172
|
-
const body = (await readJson<ErrorBody>(res).catch(() => ({}))) as ErrorBody;
|
|
173
|
-
throw new Error(
|
|
174
|
-
`Could not open ${r}: ${problemDetail(body) ?? `activation failed (${res.status})`}`,
|
|
175
|
-
);
|
|
176
|
-
}
|
|
209
|
+
await postActivate(root ?? _projectRoot);
|
|
177
210
|
},
|
|
178
211
|
|
|
179
212
|
// ─── Project opening ──────────────────────────────────────────────────
|
|
@@ -514,6 +547,8 @@ export function createDevServerPlatform() {
|
|
|
514
547
|
es.addEventListener("open", () => {
|
|
515
548
|
opens += 1;
|
|
516
549
|
if (opens > 1) {
|
|
550
|
+
// A reconnect is usually a restarted server, which comes back bound to no project.
|
|
551
|
+
reclaimBinding();
|
|
517
552
|
options?.onResync?.("reconnect");
|
|
518
553
|
}
|
|
519
554
|
});
|
package/src/studio.ts
CHANGED
|
@@ -200,7 +200,8 @@ import * as chatPanelMod from "./panels/chat-panel";
|
|
|
200
200
|
import { setProjectAdopter } from "./services/project-adoption";
|
|
201
201
|
import { setImportHandoff } from "./services/import-seed";
|
|
202
202
|
import { watchConnection } from "./services/connection";
|
|
203
|
-
import { tabBufferUnsaved } from "./services/monaco-buffer";
|
|
203
|
+
import { commitTabBuffers, tabBufferUnsaved } from "./services/monaco-buffer";
|
|
204
|
+
import { clearTyping } from "./tabs/typing";
|
|
204
205
|
import * as leftPanelMod from "./panels/left-panel";
|
|
205
206
|
import * as tabStrip from "./panels/tab-strip";
|
|
206
207
|
import * as paneContext from "./panels/pane-context";
|
|
@@ -1492,7 +1493,18 @@ registerStudioCommands(
|
|
|
1492
1493
|
// Whether the bytes landed, and a hook that neither supplies one nor reads the answer must not
|
|
1493
1494
|
// Silently forward its own first argument as the tab to save.
|
|
1494
1495
|
saveDocument: async () => {
|
|
1495
|
-
|
|
1496
|
+
const tab = activeTab.value;
|
|
1497
|
+
if (!tab) {
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
// What a source buffer holds goes into the document first, or ⌘S inside the commit's
|
|
1501
|
+
// Debounce would save the text from before the last keystrokes.
|
|
1502
|
+
await commitTabBuffers(tab);
|
|
1503
|
+
await saveFile(tab);
|
|
1504
|
+
// Text that would not parse is still only in the buffer, so it is still unsaved.
|
|
1505
|
+
if (!tabBufferUnsaved(tab)) {
|
|
1506
|
+
clearTyping(tab.id);
|
|
1507
|
+
}
|
|
1496
1508
|
},
|
|
1497
1509
|
},
|
|
1498
1510
|
stageContext,
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
import { reactive } from "../reactivity";
|
|
32
32
|
import { mountSurface, registerSurface } from "../ui/surface";
|
|
33
|
+
import { placeholderBrokenImages } from "../utils/edit-display";
|
|
33
34
|
import libraryDoc from "./library-pane.json";
|
|
34
35
|
import type { JxDocument, JxElement } from "@jxsuite/schema/types";
|
|
35
36
|
import type { JxScope } from "@jxsuite/runtime/types";
|
|
@@ -267,6 +268,10 @@ export function mountLibrarySurface(
|
|
|
267
268
|
actions: LibraryActions,
|
|
268
269
|
): LibrarySurfaceHandle {
|
|
269
270
|
host.replaceChildren();
|
|
271
|
+
// A file that will not load, or a preview image with nothing to bind, draws a placeholder rather
|
|
272
|
+
// Than the browser's broken-image glyph.
|
|
273
|
+
const listening = new AbortController();
|
|
274
|
+
placeholderBrokenImages(host, listening.signal);
|
|
270
275
|
const scope = reactive<LibraryScope>({
|
|
271
276
|
bannerState: "hidden",
|
|
272
277
|
bannerText: "",
|
|
@@ -355,6 +360,7 @@ export function mountLibrarySurface(
|
|
|
355
360
|
},
|
|
356
361
|
dispose() {
|
|
357
362
|
disposed = true;
|
|
363
|
+
listening.abort();
|
|
358
364
|
mounted?.dispose();
|
|
359
365
|
mounted = null;
|
|
360
366
|
},
|