@jxsuite/studio 0.37.1 → 1.1.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 +622 -49
- package/dist/iframe-entry.js.map +15 -12
- package/dist/studio.css +1333 -0
- package/dist/studio.js +103709 -81001
- package/dist/studio.js.map +201 -157
- package/package.json +47 -45
- package/src/account-status.ts +39 -0
- package/src/browse/browse.ts +10 -14
- package/src/canvas/canvas-live-render.ts +21 -2
- package/src/canvas/canvas-render.ts +35 -16
- package/src/canvas/canvas-utils.ts +118 -72
- package/src/canvas/iframe-entry.ts +20 -0
- package/src/canvas/iframe-eval.ts +155 -0
- package/src/canvas/iframe-host.ts +132 -6
- package/src/canvas/iframe-inline-edit.ts +107 -1
- package/src/canvas/iframe-protocol.ts +43 -3
- package/src/canvas/iframe-render.ts +18 -0
- package/src/collab/collab-session.ts +23 -8
- package/src/collab/collab-state.ts +6 -3
- package/src/component-props.ts +104 -0
- package/src/editor/context-menu.ts +1 -1
- package/src/editor/inline-edit-apply.ts +36 -1
- package/src/editor/inline-edit.ts +58 -1
- package/src/editor/repeater-scope.ts +8 -13
- package/src/editor/shortcuts.ts +41 -12
- package/src/files/file-ops.ts +14 -0
- package/src/files/files.ts +56 -1
- package/src/format/format-host.ts +63 -1
- package/src/grid/cell-editors.ts +288 -0
- package/src/grid/cell-popovers.ts +108 -0
- package/src/grid/csv-codec.ts +122 -0
- package/src/grid/edit-buffer.ts +490 -0
- package/src/grid/grid-controller.ts +417 -0
- package/src/grid/grid-layout.ts +83 -0
- package/src/grid/grid-open.ts +167 -0
- package/src/grid/grid-panel.ts +302 -0
- package/src/grid/grid-source.ts +210 -0
- package/src/grid/grid-view.ts +367 -0
- package/src/grid/schema-columns.ts +261 -0
- package/src/grid/sources/connector-source.ts +217 -0
- package/src/grid/sources/content-source.ts +542 -0
- package/src/grid/sources/csv-file-source.ts +247 -0
- package/src/grid/tabulator-tables.d.ts +120 -0
- package/src/new-project/add-repo-modal.ts +183 -0
- package/src/new-project/new-project-modal.ts +22 -3
- package/src/page-params.ts +34 -8
- package/src/panels/ai-chat/chat-markdown.ts +2 -2
- package/src/panels/ai-panel.ts +61 -4
- package/src/panels/block-action-bar.ts +8 -3
- package/src/panels/chat-panel.ts +98 -0
- package/src/panels/data-grid.ts +241 -0
- package/src/panels/editors.ts +43 -17
- package/src/panels/events-panel.ts +137 -44
- package/src/panels/formula-workspace.ts +379 -0
- package/src/panels/frontmatter-fields.ts +231 -0
- package/src/panels/frontmatter-panel.ts +132 -0
- package/src/panels/git-panel.ts +1 -1
- package/src/panels/head-panel.ts +11 -175
- package/src/panels/properties-panel.ts +154 -144
- package/src/panels/right-panel.ts +9 -47
- package/src/panels/signals-panel.ts +217 -460
- package/src/panels/statement-editor.ts +710 -0
- package/src/panels/statusbar.ts +1 -1
- package/src/panels/style-panel.ts +25 -1
- package/src/panels/stylebook-panel.ts +0 -2
- package/src/panels/tab-bar.ts +175 -6
- package/src/panels/tab-strip.ts +62 -6
- package/src/panels/toolbar.ts +37 -3
- package/src/panels/welcome-screen.ts +50 -0
- package/src/platform-errors.ts +30 -0
- package/src/platforms/cloud.ts +172 -89
- package/src/platforms/devserver.ts +172 -0
- package/src/services/ai-project-tools.ts +541 -0
- package/src/services/ai-session-store.ts +28 -0
- package/src/services/ai-system-prompt.ts +194 -24
- package/src/services/ai-tools.ts +88 -21
- package/src/services/automation.ts +16 -0
- package/src/services/context-resolver.ts +73 -0
- package/src/services/data-service.ts +155 -0
- package/src/services/document-assistant.ts +81 -11
- package/src/services/gated-registry.ts +72 -0
- package/src/services/live-preview.ts +0 -0
- package/src/services/monaco-setup.ts +75 -26
- package/src/services/preview-eval.ts +68 -0
- package/src/services/project-adoption.ts +31 -0
- package/src/settings/contributed-section.ts +406 -0
- package/src/settings/extension-sections.ts +145 -0
- package/src/settings/schema-field-ui.ts +4 -2
- package/src/settings/settings-modal.ts +101 -42
- package/src/site-context.ts +12 -1
- package/src/store.ts +4 -0
- package/src/studio.ts +107 -52
- package/src/tabs/patch-ops.ts +25 -0
- package/src/tabs/tab.ts +39 -1
- package/src/tabs/transact.ts +61 -14
- package/src/types.ts +132 -1
- package/src/ui/dynamic-slot.ts +272 -0
- package/src/ui/expression-editor.ts +423 -125
- package/src/ui/field-row.ts +5 -0
- package/src/ui/form-controls.ts +322 -0
- package/src/ui/formula-catalog.ts +557 -0
- package/src/ui/formula-chips.ts +216 -0
- package/src/ui/formula-palette.ts +211 -0
- package/src/ui/layers.ts +40 -0
- package/src/ui/media-picker.ts +1 -1
- package/src/ui/panel-resize.ts +15 -1
- package/src/ui/progress-modal.ts +2 -2
- package/src/ui/schema-form.ts +524 -0
- package/src/utils/preview-format.ts +26 -0
- package/src/utils/studio-utils.ts +4 -3
- package/src/view.ts +4 -4
- package/src/workspace/workspace.ts +14 -0
- package/src/settings/content-types-editor.ts +0 -599
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,63 +39,65 @@
|
|
|
39
39
|
"@atlaskit/pragmatic-drag-and-drop": "^2.0.1",
|
|
40
40
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.0.0",
|
|
41
41
|
"@jxsuite/ai": "^0.33.0",
|
|
42
|
-
"@jxsuite/collab": "^0.
|
|
43
|
-
"@jxsuite/create": "^
|
|
44
|
-
"@jxsuite/
|
|
45
|
-
"@jxsuite/
|
|
46
|
-
"@jxsuite/
|
|
47
|
-
"@jxsuite/
|
|
48
|
-
"@
|
|
49
|
-
"@spectrum-web-components/
|
|
50
|
-
"@spectrum-web-components/action-
|
|
51
|
-
"@spectrum-web-components/action-
|
|
52
|
-
"@spectrum-web-components/
|
|
53
|
-
"@spectrum-web-components/
|
|
54
|
-
"@spectrum-web-components/
|
|
55
|
-
"@spectrum-web-components/
|
|
56
|
-
"@spectrum-web-components/color-
|
|
57
|
-
"@spectrum-web-components/color-
|
|
58
|
-
"@spectrum-web-components/
|
|
59
|
-
"@spectrum-web-components/
|
|
60
|
-
"@spectrum-web-components/
|
|
61
|
-
"@spectrum-web-components/
|
|
62
|
-
"@spectrum-web-components/
|
|
63
|
-
"@spectrum-web-components/
|
|
64
|
-
"@spectrum-web-components/
|
|
65
|
-
"@spectrum-web-components/icons-
|
|
66
|
-
"@spectrum-web-components/
|
|
67
|
-
"@spectrum-web-components/
|
|
68
|
-
"@spectrum-web-components/
|
|
42
|
+
"@jxsuite/collab": "^0.4.0",
|
|
43
|
+
"@jxsuite/create": "^1.1.0",
|
|
44
|
+
"@jxsuite/formulas": "^0.0.2",
|
|
45
|
+
"@jxsuite/markup": "^0.3.0",
|
|
46
|
+
"@jxsuite/protocol": "^0.5.0",
|
|
47
|
+
"@jxsuite/runtime": "^1.1.0",
|
|
48
|
+
"@jxsuite/schema": "^1.1.0",
|
|
49
|
+
"@spectrum-web-components/accordion": "^1.12.2",
|
|
50
|
+
"@spectrum-web-components/action-bar": "1.12.2",
|
|
51
|
+
"@spectrum-web-components/action-button": "^1.12.2",
|
|
52
|
+
"@spectrum-web-components/action-group": "^1.12.2",
|
|
53
|
+
"@spectrum-web-components/base": "^1.12.2",
|
|
54
|
+
"@spectrum-web-components/button": "^1.12.2",
|
|
55
|
+
"@spectrum-web-components/checkbox": "^1.12.2",
|
|
56
|
+
"@spectrum-web-components/color-area": "^1.12.2",
|
|
57
|
+
"@spectrum-web-components/color-handle": "^1.12.2",
|
|
58
|
+
"@spectrum-web-components/color-slider": "^1.12.2",
|
|
59
|
+
"@spectrum-web-components/combobox": "^1.12.2",
|
|
60
|
+
"@spectrum-web-components/dialog": "^1.12.2",
|
|
61
|
+
"@spectrum-web-components/divider": "^1.12.2",
|
|
62
|
+
"@spectrum-web-components/field-label": "^1.12.2",
|
|
63
|
+
"@spectrum-web-components/help-text": "^1.12.2",
|
|
64
|
+
"@spectrum-web-components/icon": "^1.12.2",
|
|
65
|
+
"@spectrum-web-components/icons-ui": "^1.12.2",
|
|
66
|
+
"@spectrum-web-components/icons-workflow": "^1.12.2",
|
|
67
|
+
"@spectrum-web-components/menu": "^1.12.2",
|
|
68
|
+
"@spectrum-web-components/number-field": "^1.12.2",
|
|
69
|
+
"@spectrum-web-components/overlay": "^1.12.2",
|
|
69
70
|
"@spectrum-web-components/overlay-trigger": "^0.1.7",
|
|
70
|
-
"@spectrum-web-components/picker": "^1.12.
|
|
71
|
-
"@spectrum-web-components/picker-button": "^1.12.
|
|
72
|
-
"@spectrum-web-components/popover": "^1.12.
|
|
73
|
-
"@spectrum-web-components/progress-circle": "^1.12.
|
|
74
|
-
"@spectrum-web-components/search": "^1.12.
|
|
75
|
-
"@spectrum-web-components/swatch": "^1.12.
|
|
76
|
-
"@spectrum-web-components/switch": "^1.12.
|
|
77
|
-
"@spectrum-web-components/table": "^1.12.
|
|
78
|
-
"@spectrum-web-components/tabs": "^1.12.
|
|
79
|
-
"@spectrum-web-components/textfield": "^1.12.
|
|
80
|
-
"@spectrum-web-components/theme": "^1.12.
|
|
81
|
-
"@spectrum-web-components/toast": "^1.12.
|
|
82
|
-
"@spectrum-web-components/tooltip": "^1.12.
|
|
83
|
-
"@spectrum-web-components/underlay": "^1.12.
|
|
84
|
-
"@vue/reactivity": "3.5.
|
|
71
|
+
"@spectrum-web-components/picker": "^1.12.2",
|
|
72
|
+
"@spectrum-web-components/picker-button": "^1.12.2",
|
|
73
|
+
"@spectrum-web-components/popover": "^1.12.2",
|
|
74
|
+
"@spectrum-web-components/progress-circle": "^1.12.2",
|
|
75
|
+
"@spectrum-web-components/search": "^1.12.2",
|
|
76
|
+
"@spectrum-web-components/swatch": "^1.12.2",
|
|
77
|
+
"@spectrum-web-components/switch": "^1.12.2",
|
|
78
|
+
"@spectrum-web-components/table": "^1.12.2",
|
|
79
|
+
"@spectrum-web-components/tabs": "^1.12.2",
|
|
80
|
+
"@spectrum-web-components/textfield": "^1.12.2",
|
|
81
|
+
"@spectrum-web-components/theme": "^1.12.2",
|
|
82
|
+
"@spectrum-web-components/toast": "^1.12.2",
|
|
83
|
+
"@spectrum-web-components/tooltip": "^1.12.2",
|
|
84
|
+
"@spectrum-web-components/underlay": "^1.12.2",
|
|
85
|
+
"@vue/reactivity": "3.5.40",
|
|
85
86
|
"ajv": "^8.20.0",
|
|
86
87
|
"ajv-formats": "^3.0.1",
|
|
87
88
|
"lit": "^3.3.3",
|
|
88
89
|
"lit-html": "^3.3.3",
|
|
89
90
|
"monaco-editor": "^0.55.1",
|
|
91
|
+
"tabulator-tables": "^6.5",
|
|
90
92
|
"y-monaco": "^0.1.6",
|
|
91
93
|
"yaml": "^2.9.0"
|
|
92
94
|
},
|
|
93
95
|
"devDependencies": {
|
|
94
96
|
"@happy-dom/global-registrator": "^20.10.6",
|
|
95
97
|
"@types/mdast": "^4.0.4",
|
|
96
|
-
"@webref/css": "^8.
|
|
98
|
+
"@webref/css": "^8.7.0",
|
|
97
99
|
"@webref/elements": "^2.7.1",
|
|
98
|
-
"@webref/idl": "^3.
|
|
100
|
+
"@webref/idl": "^3.82.0"
|
|
99
101
|
},
|
|
100
102
|
"//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
|
|
101
103
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account onboarding status — a synchronous render cache over the optional
|
|
3
|
+
* `platform.getAccountStatus` PAL member (cloud GitHub-App installations). Hydrated once at boot
|
|
4
|
+
* (studio.ts) like the project-list cache; the welcome screen reads it synchronously to prompt
|
|
5
|
+
* "install the GitHub App" when the user has no repository access yet.
|
|
6
|
+
*/
|
|
7
|
+
import { getPlatform, hasPlatform } from "./platform";
|
|
8
|
+
import type { AccountStatus } from "./types";
|
|
9
|
+
|
|
10
|
+
let cache: AccountStatus | null = null;
|
|
11
|
+
|
|
12
|
+
/** Refresh the cache from the platform; null when unsupported, failing, or unknown. */
|
|
13
|
+
export async function hydrateAccountStatus(): Promise<void> {
|
|
14
|
+
if (!hasPlatform() || typeof getPlatform().getAccountStatus !== "function") {
|
|
15
|
+
cache = null;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
cache = (await getPlatform().getAccountStatus?.()) ?? null;
|
|
20
|
+
} catch {
|
|
21
|
+
// Onboarding prompts are progressive enhancement; unknown status never nags.
|
|
22
|
+
cache = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Synchronous snapshot for render paths (never awaits); null = unknown. */
|
|
27
|
+
export function getAccountStatus(): AccountStatus | null {
|
|
28
|
+
return cache;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True when the account verifiably has no repository access yet and we know where to fix it. */
|
|
32
|
+
export function needsAppInstall(): boolean {
|
|
33
|
+
return cache !== null && cache.installations.length === 0 && Boolean(cache.appInstallUrl);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Reset seam for tests. */
|
|
37
|
+
export function resetAccountStatus(): void {
|
|
38
|
+
cache = null;
|
|
39
|
+
}
|
package/src/browse/browse.ts
CHANGED
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
} from "../format/format-host";
|
|
35
35
|
|
|
36
36
|
import type { ComponentEntry } from "../files/components";
|
|
37
|
-
import type {
|
|
37
|
+
import type { ContentSectionEntry } from "../types";
|
|
38
|
+
import type { JxDocument } from "@jxsuite/schema/types";
|
|
38
39
|
|
|
39
40
|
// ─── Category definitions ────────────────────────────────────────────────────
|
|
40
41
|
|
|
@@ -184,19 +185,16 @@ async function collectFiles(
|
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
/**
|
|
187
|
-
* Match a file path against project
|
|
188
|
-
* Returns the content type name (capitalized) or null if no match.
|
|
188
|
+
* Match a file path against the project `content` section's source directories to find its content
|
|
189
|
+
* type name. Returns the content type name (capitalized) or null if no match.
|
|
189
190
|
*
|
|
190
191
|
* @param {string} filePath
|
|
191
192
|
* @returns {string | null}
|
|
192
193
|
*/
|
|
193
194
|
function contentTypeFor(filePath: string) {
|
|
194
195
|
const config = projectState?.projectConfig;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
for (const [name, def] of Object.entries(config.contentTypes)) {
|
|
199
|
-
const d = def as ContentTypeDef;
|
|
196
|
+
const content = (config?.content ?? {}) as Record<string, ContentSectionEntry>;
|
|
197
|
+
for (const [name, d] of Object.entries(content)) {
|
|
200
198
|
if (!d.source) {
|
|
201
199
|
continue;
|
|
202
200
|
}
|
|
@@ -264,7 +262,8 @@ function getEntityTypes() {
|
|
|
264
262
|
*/
|
|
265
263
|
function buildFrontmatterYaml(contentTypeName: string) {
|
|
266
264
|
const config = projectState?.projectConfig;
|
|
267
|
-
const
|
|
265
|
+
const content = (config?.content ?? {}) as Record<string, ContentSectionEntry>;
|
|
266
|
+
const col = content[contentTypeName];
|
|
268
267
|
if (!col?.schema?.properties) {
|
|
269
268
|
return "title: Untitled\n";
|
|
270
269
|
}
|
|
@@ -284,11 +283,8 @@ function buildFrontmatterYaml(contentTypeName: string) {
|
|
|
284
283
|
*/
|
|
285
284
|
function getContentTypeTypes() {
|
|
286
285
|
const config = projectState?.projectConfig;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
return Object.entries(config.contentTypes).map(([name, def]) => {
|
|
291
|
-
const d = def as ContentTypeDef;
|
|
286
|
+
const content = (config?.content ?? {}) as Record<string, ContentSectionEntry>;
|
|
287
|
+
return Object.entries(content).map(([name, d]) => {
|
|
292
288
|
const dir = d.source ? d.source.replace(/^\.\//, "").replace(/\/$/, "") : name;
|
|
293
289
|
return {
|
|
294
290
|
contentTypeName: name,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
resolveParamBoundState,
|
|
27
27
|
substitutePreviewParams,
|
|
28
28
|
} from "../page-params";
|
|
29
|
+
import { isComponentDoc, substitutePreviewProps } from "../component-props";
|
|
29
30
|
|
|
30
31
|
import type { JxElement, JxMutableNode } from "@jxsuite/schema/types";
|
|
31
32
|
import type { ComponentEntry } from "../files/components.js";
|
|
@@ -182,6 +183,17 @@ export async function resolveCanvasDocument(doc: JxMutableNode): Promise<{
|
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
185
|
|
|
186
|
+
// Component definition docs (non-page): seed chosen test-prop values into the render doc's state
|
|
187
|
+
// So a non-instantiated component previews with real data — templates, dataScope snapshots, and
|
|
188
|
+
// Live/snapshot expression previews all see the values (M6, the previewParams mirror). Pure
|
|
189
|
+
// Rebuild for the same reason as substitutePreviewParams above.
|
|
190
|
+
if (!isPage && tab && isComponentDoc(renderDoc)) {
|
|
191
|
+
const { previewProps } = tab.session.ui;
|
|
192
|
+
if (previewProps && Object.keys(previewProps).length > 0) {
|
|
193
|
+
renderDoc = substitutePreviewProps(renderDoc, previewProps);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
185
197
|
const arrayPaths = new Set<string>();
|
|
186
198
|
if (canvasMode === "design" || canvasMode === "edit") {
|
|
187
199
|
(function findArrayPaths(node: JxMutableNode, path: (string | number)[]) {
|
|
@@ -219,9 +231,16 @@ export async function resolveCanvasDocument(doc: JxMutableNode): Promise<{
|
|
|
219
231
|
})(doc, []);
|
|
220
232
|
}
|
|
221
233
|
|
|
222
|
-
// Component auto-discovery (content mode or layout
|
|
234
|
+
// Component auto-discovery (content mode, wrapped layout, or a layout opened on its own) —
|
|
235
|
+
// Mirrors the legacy render path. A directly-opened layout is neither a page (so it never
|
|
236
|
+
// Sets layoutWrapped) nor content mode (a plain .json file skips the mode-setting parse),
|
|
237
|
+
// So it needs its own gate; scoping to layouts/ avoids the single-component-edit path, where
|
|
238
|
+
// The doc's own root tag would otherwise inject a $ref to itself.
|
|
239
|
+
const isLayoutDoc =
|
|
240
|
+
S.documentPath != null &&
|
|
241
|
+
(S.documentPath.startsWith("layouts/") || S.documentPath.startsWith("./layouts/"));
|
|
223
242
|
const effectiveElements = getEffectiveElements(renderDoc.$elements as (JxElement | string)[]);
|
|
224
|
-
if ((S.mode === "content" || layoutWrapped) && componentRegistry.length > 0) {
|
|
243
|
+
if ((S.mode === "content" || layoutWrapped || isLayoutDoc) && componentRegistry.length > 0) {
|
|
225
244
|
const existingRefs = new Set(
|
|
226
245
|
effectiveElements.map((e: JxElement | string) => (typeof e === "string" ? e : e?.$ref)),
|
|
227
246
|
);
|
|
@@ -15,15 +15,15 @@ import { attachCursorStyles } from "../collab/monaco-cursors";
|
|
|
15
15
|
import type { AwarenessLike } from "../collab/monaco-cursors";
|
|
16
16
|
import { view } from "../view";
|
|
17
17
|
import { parseSourceForPath, serializeDocument } from "../files/file-ops";
|
|
18
|
+
import { detachGridPanel, gridPanelMounted, renderGridMode } from "../grid/grid-panel";
|
|
18
19
|
import { formatByName, formatForPath } from "../format/format-host";
|
|
19
20
|
import { renderWelcome } from "../panels/welcome-screen";
|
|
20
21
|
import { projectState } from "../state";
|
|
21
22
|
import {
|
|
23
|
+
applyEditZoom,
|
|
22
24
|
applyTransform,
|
|
23
25
|
canvasPanelTemplate,
|
|
24
26
|
observeCenterUntilStable,
|
|
25
|
-
renderZoomIndicator,
|
|
26
|
-
resetZoomIndicator,
|
|
27
27
|
updateActivePanelHeaders,
|
|
28
28
|
} from "./canvas-utils";
|
|
29
29
|
import { parseMediaEntries } from "../utils/canvas-media";
|
|
@@ -40,6 +40,7 @@ import { dismissBlockActionBar, dismissLinkPopover } from "../panels/block-actio
|
|
|
40
40
|
import { dismissContextMenu } from "../editor/context-menu";
|
|
41
41
|
import { dismissSlashMenu } from "../editor/slash-menu";
|
|
42
42
|
import { renderFunctionEditor } from "../panels/editors";
|
|
43
|
+
import { renderFormulaWorkspace } from "../panels/formula-workspace";
|
|
43
44
|
import { mediaDisplayName } from "../panels/shared";
|
|
44
45
|
import { statusMessage } from "../panels/statusbar";
|
|
45
46
|
import * as overlaysPanel from "../panels/overlays";
|
|
@@ -174,6 +175,7 @@ function resetCanvasView() {
|
|
|
174
175
|
view.functionEditor.dispose();
|
|
175
176
|
view.functionEditor = null;
|
|
176
177
|
}
|
|
178
|
+
detachGridPanel();
|
|
177
179
|
disposeSourceCollab();
|
|
178
180
|
if (view.monacoEditor) {
|
|
179
181
|
view.monacoEditor.getModel()?.dispose();
|
|
@@ -206,7 +208,6 @@ function resetCanvasView() {
|
|
|
206
208
|
canvasWrap.style.flexDirection = "";
|
|
207
209
|
canvasWrap.style.display = "";
|
|
208
210
|
canvasWrap.style.overflow = "";
|
|
209
|
-
resetZoomIndicator();
|
|
210
211
|
dismissBlockActionBar();
|
|
211
212
|
dismissLinkPopover();
|
|
212
213
|
dismissContextMenu();
|
|
@@ -266,6 +267,13 @@ export function renderCanvas() {
|
|
|
266
267
|
view.functionEditor = null;
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
// Formula workspace mode: full-screen structured editing of an $expression (the function editor
|
|
271
|
+
// Takes precedence when both targets are set).
|
|
272
|
+
if (S.ui.editingFormula) {
|
|
273
|
+
renderFormulaWorkspace();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
269
277
|
// Source mode: update existing Monaco editor without recreating. Don't replace the buffer while
|
|
270
278
|
// The user is actively typing in it — that would reformat under the cursor (the source view is
|
|
271
279
|
// The editing surface here, mirroring the panel draft-state behaviour).
|
|
@@ -287,6 +295,12 @@ export function renderCanvas() {
|
|
|
287
295
|
return;
|
|
288
296
|
}
|
|
289
297
|
|
|
298
|
+
// Grid fast-path: the grid panel runs its own effect scope (toolbar + engine stay live), so a
|
|
299
|
+
// Same-tab re-render while the panel is mounted needs nothing from the canvas pipeline.
|
|
300
|
+
if (canvasMode === "grid" && gridPanelMounted(tab)) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
290
304
|
// Stylebook fast-path: a style edit re-applies IN PLACE via the bridge (the iframe re-runs the
|
|
291
305
|
// Runtime's style applier on the specimen root — real @media, no re-render, no iframe reload).
|
|
292
306
|
// Filter/Customized changes fall through to the full rebuild (they change which specimens exist),
|
|
@@ -344,6 +358,9 @@ export function renderCanvas() {
|
|
|
344
358
|
view.centerObserver = null;
|
|
345
359
|
}
|
|
346
360
|
|
|
361
|
+
// Destroy the grid panel if switching away from grid mode
|
|
362
|
+
detachGridPanel();
|
|
363
|
+
|
|
347
364
|
// Dispose Monaco editor if switching away from source mode
|
|
348
365
|
disposeSourceCollab();
|
|
349
366
|
if (view.monacoEditor) {
|
|
@@ -362,9 +379,6 @@ export function renderCanvas() {
|
|
|
362
379
|
canvasWrap.style.overflow = "";
|
|
363
380
|
canvasWrap.style.overflow = "";
|
|
364
381
|
|
|
365
|
-
// Clear zoom indicator (only re-rendered by design/stylebook)
|
|
366
|
-
resetZoomIndicator();
|
|
367
|
-
|
|
368
382
|
// Dismiss open popovers/toolbars that are no longer relevant
|
|
369
383
|
dismissBlockActionBar();
|
|
370
384
|
dismissLinkPopover();
|
|
@@ -380,12 +394,20 @@ export function renderCanvas() {
|
|
|
380
394
|
applyTransform,
|
|
381
395
|
canvasPanelTemplate,
|
|
382
396
|
observeCenterUntilStable,
|
|
383
|
-
renderZoomIndicator,
|
|
384
397
|
updateActivePanelHeaders,
|
|
385
398
|
});
|
|
386
399
|
return;
|
|
387
400
|
}
|
|
388
401
|
|
|
402
|
+
// Grid mode: spreadsheet editor over the tab's grid source (mirrors the Monaco/stylebook
|
|
403
|
+
// Non-iframe-editor pattern; the panel owns its own reactivity from here).
|
|
404
|
+
if (canvasMode === "grid") {
|
|
405
|
+
canvasWrap.style.padding = "0";
|
|
406
|
+
canvasWrap.style.display = "block";
|
|
407
|
+
renderGridMode(canvasWrap, tab);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
389
411
|
// Source mode: create Monaco editor instead of canvas
|
|
390
412
|
if (canvasMode === "source") {
|
|
391
413
|
canvasWrap.style.padding = "0";
|
|
@@ -599,18 +621,15 @@ export function renderCanvas() {
|
|
|
599
621
|
if (modeChanged) {
|
|
600
622
|
observeCenterUntilStable();
|
|
601
623
|
}
|
|
602
|
-
renderZoomIndicator();
|
|
603
624
|
return;
|
|
604
625
|
}
|
|
605
626
|
|
|
606
|
-
// Edit (content) mode — centered column, no panzoom
|
|
627
|
+
// Edit (content) mode — centered column, no panzoom; `ui.editZoom` reflows content at the zoomed
|
|
628
|
+
// Effective width while the on-screen footprint stays fixed (browser-page-zoom semantics).
|
|
607
629
|
if (canvasMode === "edit") {
|
|
608
630
|
if (modeChanged) {
|
|
609
631
|
canvasWrap.style.padding = "0";
|
|
610
632
|
canvasWrap.style.overflow = "hidden";
|
|
611
|
-
|
|
612
|
-
// Remove zoom indicator left over from design mode
|
|
613
|
-
resetZoomIndicator();
|
|
614
633
|
}
|
|
615
634
|
|
|
616
635
|
const { baseWidth } = parseMediaEntries(getEffectiveMedia(S.document.$media));
|
|
@@ -633,6 +652,10 @@ export function renderCanvas() {
|
|
|
633
652
|
litRender(editTpl, canvasWrap);
|
|
634
653
|
canvasPanels.push(panel as unknown as CanvasPanel);
|
|
635
654
|
renderCanvasIntoPanel(panel as unknown as CanvasPanel, S.ui.featureToggles);
|
|
655
|
+
// The column must exist in the DOM before the zoom's live width measurement — so the zoom is
|
|
656
|
+
// Applied after the render rather than baked into the template (the panel mounts fluid and is
|
|
657
|
+
// Immediately re-fitted; the iframe hasn't painted yet, so nothing visibly jumps).
|
|
658
|
+
applyEditZoom();
|
|
636
659
|
return;
|
|
637
660
|
}
|
|
638
661
|
|
|
@@ -684,7 +707,6 @@ export function renderCanvas() {
|
|
|
684
707
|
if (modeChanged) {
|
|
685
708
|
observeCenterUntilStable();
|
|
686
709
|
}
|
|
687
|
-
renderZoomIndicator();
|
|
688
710
|
return;
|
|
689
711
|
}
|
|
690
712
|
|
|
@@ -747,9 +769,6 @@ export function renderCanvas() {
|
|
|
747
769
|
if (modeChanged) {
|
|
748
770
|
observeCenterUntilStable();
|
|
749
771
|
}
|
|
750
|
-
|
|
751
|
-
// Floating zoom indicator
|
|
752
|
-
renderZoomIndicator();
|
|
753
772
|
}
|
|
754
773
|
|
|
755
774
|
/**
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* template creation, centering, transform application, zoom indicator, and fit-to-screen.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { html,
|
|
7
|
+
import { html, nothing } from "lit-html";
|
|
8
8
|
import type { CanvasPanel } from "../types";
|
|
9
9
|
import { ref } from "lit-html/directives/ref.js";
|
|
10
10
|
import { classMap } from "lit-html/directives/class-map.js";
|
|
@@ -14,7 +14,6 @@ import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
|
14
14
|
import { canvasPanels, canvasWrap, renderOnly, updateUi } from "../store";
|
|
15
15
|
import { activeTab } from "../workspace/workspace";
|
|
16
16
|
import { view } from "../view";
|
|
17
|
-
import { getLayerSlot } from "../ui/layers";
|
|
18
17
|
import { findCanvasElement, getActivePanel, panelMediaToActiveMedia } from "./canvas-helpers";
|
|
19
18
|
import { rectOf } from "../utils/geometry";
|
|
20
19
|
import type { TemplateResult } from "lit-html";
|
|
@@ -25,8 +24,6 @@ let _ctx: {
|
|
|
25
24
|
setZoomDirect: (zoom: number) => void;
|
|
26
25
|
};
|
|
27
26
|
|
|
28
|
-
let _zoomIndicatorEl: HTMLElement | null = null;
|
|
29
|
-
|
|
30
27
|
/**
|
|
31
28
|
* Initialize the canvas utils module.
|
|
32
29
|
*
|
|
@@ -162,12 +159,127 @@ export function applyTransform() {
|
|
|
162
159
|
}
|
|
163
160
|
const zoom = _ctx.getZoom();
|
|
164
161
|
view.panzoomWrap.style.transform = `translate(${view.panX}px, ${view.panY}px) scale(${zoom})`;
|
|
165
|
-
renderZoomIndicator();
|
|
166
162
|
// 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.
|
|
163
|
+
// Is needed here — the flush only re-anchors the fixed block-action-bar. The tab-bar zoom widget
|
|
164
|
+
// Tracks `ui.zoom` reactively, so no explicit indicator refresh is needed either.
|
|
165
|
+
renderOnly("overlays");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Edit-mode content zoom ──────────────────────────────────────────────────
|
|
169
|
+
// Browser-page-zoom semantics: the canvas footprint stays fixed while the CONTENT reflows at the
|
|
170
|
+
// Zoomed effective width — unlike design mode's panzoom, which visually scales a fixed layout.
|
|
171
|
+
|
|
172
|
+
export const EDIT_ZOOM_MIN = 0.25;
|
|
173
|
+
|
|
174
|
+
export const EDIT_ZOOM_MAX = 3;
|
|
175
|
+
|
|
176
|
+
/** Clamp an edit-zoom value to the supported range. */
|
|
177
|
+
export function clampEditZoom(zoom: number): number {
|
|
178
|
+
return Math.min(EDIT_ZOOM_MAX, Math.max(EDIT_ZOOM_MIN, zoom));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Apply the active tab's edit-mode content zoom to the single edit panel.
|
|
183
|
+
*
|
|
184
|
+
* Mechanism: a parent-side transform on the iframe never reflows the iframe's internal document (it
|
|
185
|
+
* is its own layout viewport), so genuine reflow requires resizing the iframe's REAL CSS width to
|
|
186
|
+
* `renderWidth / editZoom` — and a compensating `scale(editZoom)` on the canvas element brings the
|
|
187
|
+
* rendered footprint back to exactly `renderWidth`. The overlay layer (the iframe's DOM sibling
|
|
188
|
+
* inside the canvas element) inherits the transform, so overlay boxes stay drawn at scale 1 (D-2),
|
|
189
|
+
* and `hostDragGeometry`'s empirical `rect.width / clientWidth` evaluates to exactly `editZoom`
|
|
190
|
+
* with no bridge changes.
|
|
191
|
+
*
|
|
192
|
+
* `renderWidth` is measured live from `.content-edit-column` (already `min(baseWidth, available)`
|
|
193
|
+
* via normal block layout) — the nominal `$media["--"]` width would overflow a narrow studio
|
|
194
|
+
* window.
|
|
195
|
+
*
|
|
196
|
+
* HARD INVARIANT: never trigger a canvas re-render from here (`renderCanvas`/`mountIframeCanvas`) —
|
|
197
|
+
* a re-render rebuilds the iframe DOM and would destroy a live inline-edit session. Live zoom is
|
|
198
|
+
* bare style writes only; the iframe's own ResizeObserver re-posts `contentHeight` after the
|
|
199
|
+
* reflow, which also finalizes the viewport height.
|
|
200
|
+
*/
|
|
201
|
+
export function applyEditZoom() {
|
|
202
|
+
if (_ctx.getCanvasMode() !== "edit") {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const [panel] = canvasPanels;
|
|
206
|
+
if (!panel?.canvas || !panel.viewport) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const canvasEl = panel.canvas;
|
|
210
|
+
const iframe = canvasEl.querySelector("iframe");
|
|
211
|
+
const editZoom = activeTab.value?.session.ui.editZoom ?? 1;
|
|
212
|
+
if (editZoom === 1) {
|
|
213
|
+
// Exactly today's fluid behavior — no inline width, no transform, auto viewport height.
|
|
214
|
+
panel._width = null;
|
|
215
|
+
canvasEl.style.width = "";
|
|
216
|
+
canvasEl.style.transform = "";
|
|
217
|
+
canvasEl.style.transformOrigin = "";
|
|
218
|
+
if (iframe) {
|
|
219
|
+
iframe.style.width = "100%";
|
|
220
|
+
}
|
|
221
|
+
panel.viewport.style.height = "";
|
|
222
|
+
} else {
|
|
223
|
+
const column = canvasWrap.querySelector<HTMLElement>(".content-edit-column");
|
|
224
|
+
if (!column) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// The column width is parent-driven (width:100%, max-width:baseWidth), so it is independent of
|
|
228
|
+
// The canvas content — measuring it mid-zoom is stable. No rounding: layoutWidth * editZoom
|
|
229
|
+
// Must equal renderWidth exactly.
|
|
230
|
+
const renderWidth = rectOf(column).width;
|
|
231
|
+
if (renderWidth <= 0) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const layoutWidth = renderWidth / editZoom;
|
|
235
|
+
panel._width = layoutWidth;
|
|
236
|
+
canvasEl.style.width = `${layoutWidth}px`;
|
|
237
|
+
canvasEl.style.transform = `scale(${editZoom})`;
|
|
238
|
+
canvasEl.style.transformOrigin = "top left";
|
|
239
|
+
if (iframe) {
|
|
240
|
+
iframe.style.width = `${layoutWidth}px`;
|
|
241
|
+
// Transforms don't affect an ancestor's auto-height, so the viewport (the white "page"
|
|
242
|
+
// Surface) needs its height pinned to the SCALED content height. offsetHeight is the
|
|
243
|
+
// Pre-measurement approximation; the next contentHeight message writes the exact value.
|
|
244
|
+
panel.viewport.style.height = `${iframe.offsetHeight * editZoom}px`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Re-anchor the fixed block-action-bar over the rescaled canvas.
|
|
168
248
|
renderOnly("overlays");
|
|
169
249
|
}
|
|
170
250
|
|
|
251
|
+
/** Set the active tab's edit zoom (clamped) and apply it synchronously. */
|
|
252
|
+
export function setEditZoom(zoom: number) {
|
|
253
|
+
const tab = activeTab.value;
|
|
254
|
+
if (!tab) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
tab.session.ui.editZoom = clampEditZoom(zoom);
|
|
258
|
+
applyEditZoom();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let _editZoomRaf = 0;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Wheel-rate edit-zoom setter: the reactive `editZoom` write lands immediately (the tab-bar label
|
|
265
|
+
* tracks it), but the DOM work — an iframe width resize, i.e. a real reflow — is coalesced to one
|
|
266
|
+
* `applyEditZoom()` per animation frame so a fast ctrl+scroll burst doesn't thrash layout.
|
|
267
|
+
*/
|
|
268
|
+
export function requestEditZoom(zoom: number) {
|
|
269
|
+
const tab = activeTab.value;
|
|
270
|
+
if (!tab) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
tab.session.ui.editZoom = clampEditZoom(zoom);
|
|
274
|
+
if (_editZoomRaf) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
_editZoomRaf = requestAnimationFrame(() => {
|
|
278
|
+
_editZoomRaf = 0;
|
|
279
|
+
applyEditZoom();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
171
283
|
/** Calculate zoom + pan to fit all panels within the viewport. */
|
|
172
284
|
export function fitToScreen() {
|
|
173
285
|
if (!view.panzoomWrap) {
|
|
@@ -209,12 +321,6 @@ export function resetZoom() {
|
|
|
209
321
|
_ctx.setZoomDirect(1);
|
|
210
322
|
centerCanvas();
|
|
211
323
|
applyTransform();
|
|
212
|
-
renderZoomIndicator();
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** Reset the zoom indicator (clear its content). Called when switching to non-panzoom modes. */
|
|
216
|
-
export function resetZoomIndicator() {
|
|
217
|
-
litRender(nothing, getLayerSlot("popover", "zoom-indicator"));
|
|
218
324
|
}
|
|
219
325
|
|
|
220
326
|
/**
|
|
@@ -286,66 +392,6 @@ export function panToElement(path: (string | number)[]) {
|
|
|
286
392
|
_panToEl(el, panel);
|
|
287
393
|
}
|
|
288
394
|
|
|
289
|
-
/**
|
|
290
|
-
* Render the floating zoom indicator at the bottom center of canvas-wrap. Uses position: fixed,
|
|
291
|
-
* computed from canvas-wrap bounds.
|
|
292
|
-
*/
|
|
293
|
-
export function renderZoomIndicator() {
|
|
294
|
-
const zoom = _ctx.getZoom();
|
|
295
|
-
const host = getLayerSlot("popover", "zoom-indicator");
|
|
296
|
-
litRender(
|
|
297
|
-
html`
|
|
298
|
-
<div
|
|
299
|
-
class="zoom-indicator"
|
|
300
|
-
${ref((el: Element | undefined) => {
|
|
301
|
-
_zoomIndicatorEl = (el as HTMLElement) || null;
|
|
302
|
-
})}
|
|
303
|
-
>
|
|
304
|
-
<span class="zoom-indicator-action" title="Reset to 100%" @click=${resetZoom}>
|
|
305
|
-
<svg
|
|
306
|
-
width="14"
|
|
307
|
-
height="14"
|
|
308
|
-
viewBox="0 0 16 16"
|
|
309
|
-
fill="none"
|
|
310
|
-
stroke="currentColor"
|
|
311
|
-
stroke-width="1.5"
|
|
312
|
-
>
|
|
313
|
-
<circle cx="8" cy="8" r="5.5" />
|
|
314
|
-
<path d="M8 5.5v5M5.5 8h5" />
|
|
315
|
-
</svg>
|
|
316
|
-
</span>
|
|
317
|
-
<span class="zoom-indicator-label">${Math.round(zoom * 100)}%</span>
|
|
318
|
-
<span class="zoom-indicator-action" title="Fit to screen" @click=${fitToScreen}>
|
|
319
|
-
<svg
|
|
320
|
-
width="14"
|
|
321
|
-
height="14"
|
|
322
|
-
viewBox="0 0 16 16"
|
|
323
|
-
fill="none"
|
|
324
|
-
stroke="currentColor"
|
|
325
|
-
stroke-width="1.5"
|
|
326
|
-
>
|
|
327
|
-
<rect x="2" y="2" width="12" height="12" rx="1" />
|
|
328
|
-
<path d="M2 6h12M6 2v12" />
|
|
329
|
-
</svg>
|
|
330
|
-
</span>
|
|
331
|
-
</div>
|
|
332
|
-
`,
|
|
333
|
-
host,
|
|
334
|
-
);
|
|
335
|
-
positionZoomIndicator();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/** Position the zoom indicator relative to canvas-wrap bounds. */
|
|
339
|
-
export function positionZoomIndicator() {
|
|
340
|
-
if (!_zoomIndicatorEl) {
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
const rect = rectOf(canvasWrap);
|
|
344
|
-
_zoomIndicatorEl.style.left = `${rect.left + rect.width / 2}px`;
|
|
345
|
-
_zoomIndicatorEl.style.top = `${rect.bottom - 32}px`;
|
|
346
|
-
_zoomIndicatorEl.style.transform = "translateX(-50%)";
|
|
347
|
-
}
|
|
348
|
-
|
|
349
395
|
/** Toggle "active" class on canvas panel headers based on activeMedia. */
|
|
350
396
|
export function updateActivePanelHeaders() {
|
|
351
397
|
const activeMedia = activeTab.value?.session.ui.activeMedia ?? null;
|