@jxsuite/studio 1.0.0 → 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 +53069 -31746
- package/dist/studio.js.map +109 -74
- package/package.json +11 -9
- 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 +131 -5
- 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/inline-edit-apply.ts +36 -1
- package/src/editor/inline-edit.ts +58 -1
- package/src/editor/shortcuts.ts +41 -12
- package/src/files/file-ops.ts +14 -0
- package/src/files/files.ts +53 -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/panels/block-action-bar.ts +8 -3
- package/src/panels/chat-panel.ts +98 -0
- package/src/panels/data-grid.ts +9 -387
- 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 +115 -23
- package/src/panels/statement-editor.ts +710 -0
- 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/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/document-assistant.ts +81 -11
- package/src/services/gated-registry.ts +72 -0
- package/src/services/live-preview.ts +0 -0
- package/src/services/preview-eval.ts +68 -0
- package/src/services/project-adoption.ts +31 -0
- package/src/site-context.ts +2 -0
- package/src/store.ts +4 -0
- package/src/studio.ts +83 -52
- package/src/tabs/patch-ops.ts +25 -0
- package/src/tabs/tab.ts +39 -1
- package/src/tabs/transact.ts +60 -13
- package/src/types.ts +12 -0
- 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/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/utils/preview-format.ts +26 -0
- package/src/view.ts +4 -4
- package/src/workspace/workspace.ts +14 -0
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "../tabs/transact";
|
|
23
23
|
import { inferInputType, propLabel } from "../utils/studio-utils";
|
|
24
24
|
import { renderFieldRow } from "../ui/field-row";
|
|
25
|
+
import { renderDynamicSlot } from "../ui/dynamic-slot";
|
|
25
26
|
import { parseMediaEntries } from "../utils/canvas-media";
|
|
26
27
|
import { getEffectiveMedia, getEffectiveStyle } from "../site-context";
|
|
27
28
|
import { computeInheritedStyle } from "../utils/inherited-style";
|
|
@@ -41,6 +42,7 @@ import { widgetForType } from "./style-inputs";
|
|
|
41
42
|
|
|
42
43
|
import type { Tab } from "../tabs/tab";
|
|
43
44
|
import type { JxPath } from "../state";
|
|
45
|
+
import type { JsonValue } from "../types";
|
|
44
46
|
import type { JxMutableNode, JxStyle } from "@jxsuite/schema/types";
|
|
45
47
|
|
|
46
48
|
interface CssLonghand {
|
|
@@ -96,17 +98,29 @@ function renderStyleRow(
|
|
|
96
98
|
isWarning: boolean,
|
|
97
99
|
gridMode: boolean,
|
|
98
100
|
inheritedValue: string | undefined,
|
|
101
|
+
templateSignals: string[] = [],
|
|
102
|
+
fieldKey: string = prop,
|
|
99
103
|
) {
|
|
100
104
|
const type = inferInputType(entry);
|
|
101
105
|
const hasVal = value !== undefined && value !== "";
|
|
102
106
|
const placeholder = !hasVal && inheritedValue ? String(inheritedValue) : "";
|
|
103
107
|
const spanVal = gridMode && (entry as Record<string, unknown>).$span === 2 ? 2 : undefined;
|
|
108
|
+
// Style values are schema-legal at two rungs: literal and ${} template (no $ref in JxStyle).
|
|
109
|
+
const slot = renderDynamicSlot({
|
|
110
|
+
caps: ["literal", "template"],
|
|
111
|
+
fieldKey,
|
|
112
|
+
onChange: (v?: JsonValue) => onCommit(v === undefined || v === "" ? undefined : String(v)),
|
|
113
|
+
staticWidget: widgetForType(type, entry, prop, value, onCommit, { placeholder }),
|
|
114
|
+
stateDefs: templateSignals,
|
|
115
|
+
value,
|
|
116
|
+
});
|
|
104
117
|
return renderFieldRow({
|
|
105
118
|
prop,
|
|
106
119
|
label: propLabel(entry, prop),
|
|
107
120
|
hasValue: hasVal,
|
|
108
121
|
onClear: onDelete,
|
|
109
|
-
widget:
|
|
122
|
+
widget: slot.widget,
|
|
123
|
+
labelExtra: slot.modeButton,
|
|
110
124
|
...(spanVal != null && { span: spanVal }),
|
|
111
125
|
warning: isWarning,
|
|
112
126
|
});
|
|
@@ -287,6 +301,14 @@ function styleSidebarTemplate(
|
|
|
287
301
|
const tab = activeTab.value!;
|
|
288
302
|
const sel = tab.session.selection as JxPath;
|
|
289
303
|
const style = effectiveStyle || node.style || {};
|
|
304
|
+
// Signals seeding the ${} template default when a style value escalates (no $ref in JxStyle).
|
|
305
|
+
const templateSignals = Object.entries(tab.doc.document.state || {})
|
|
306
|
+
.filter(
|
|
307
|
+
([, d]) =>
|
|
308
|
+
!(d as Record<string, unknown>)?.$handler &&
|
|
309
|
+
(d as Record<string, unknown>)?.$prototype !== "Function",
|
|
310
|
+
)
|
|
311
|
+
.map(([defName]) => defName);
|
|
290
312
|
const { sizeBreakpoints } = parseMediaEntries(getEffectiveMedia(tab.doc.document.$media));
|
|
291
313
|
const mediaNames = sizeBreakpoints.map((bp) => bp.name);
|
|
292
314
|
const mediaTab = activeMediaTab || null;
|
|
@@ -660,6 +682,8 @@ function styleSidebarTemplate(
|
|
|
660
682
|
isWarning,
|
|
661
683
|
sec.$layout === "grid",
|
|
662
684
|
inheritedStyle[prop] as string | undefined,
|
|
685
|
+
templateSignals,
|
|
686
|
+
`style|${sel.join("/")}|${mediaTab ?? ""}|${activeSelector ?? ""}|${prop}`,
|
|
663
687
|
),
|
|
664
688
|
);
|
|
665
689
|
}
|
|
@@ -43,7 +43,6 @@ interface StylebookCtx {
|
|
|
43
43
|
) => { tpl: TemplateResult; panel: CanvasPanel };
|
|
44
44
|
applyTransform: () => void;
|
|
45
45
|
observeCenterUntilStable: () => void;
|
|
46
|
-
renderZoomIndicator: () => void;
|
|
47
46
|
updateActivePanelHeaders: () => void;
|
|
48
47
|
}
|
|
49
48
|
|
|
@@ -182,7 +181,6 @@ export function renderStylebookMode(ctx: StylebookCtx) {
|
|
|
182
181
|
|
|
183
182
|
ctx.applyTransform();
|
|
184
183
|
ctx.observeCenterUntilStable();
|
|
185
|
-
ctx.renderZoomIndicator();
|
|
186
184
|
}
|
|
187
185
|
|
|
188
186
|
/**
|
package/src/panels/tab-bar.ts
CHANGED
|
@@ -13,12 +13,14 @@ import { html, render as litRender, nothing } from "lit-html";
|
|
|
13
13
|
import { projectState, updateUi } from "../store";
|
|
14
14
|
import { effect, effectScope } from "../reactivity";
|
|
15
15
|
import { activeTab } from "../workspace/workspace";
|
|
16
|
+
import { applyTransform, fitToScreen, resetZoom, setEditZoom } from "../canvas/canvas-utils";
|
|
16
17
|
import { getEffectiveLayoutPath, getEffectiveMedia } from "../site-context";
|
|
17
18
|
import { dynamicRouteParams, loadParamValues, pagePathsDef } from "../page-params";
|
|
19
|
+
import { componentPropEntries, isComponentDoc } from "../component-props";
|
|
18
20
|
import { mediaDisplayName } from "./shared";
|
|
19
21
|
import type { ParamValues } from "../page-params";
|
|
20
22
|
import type { Tab } from "../tabs/tab";
|
|
21
|
-
import type { DocumentStackEntry, FunctionEditDef } from "../types";
|
|
23
|
+
import type { DocumentStackEntry, FormulaEditDef, FunctionEditDef, JsonValue } from "../types";
|
|
22
24
|
import type { EffectScope } from "@vue/reactivity";
|
|
23
25
|
import type { TemplateResult } from "lit-html";
|
|
24
26
|
|
|
@@ -26,6 +28,7 @@ interface TabBarCtx {
|
|
|
26
28
|
navigateBack: () => void;
|
|
27
29
|
navigateToLevel: (level: number) => void;
|
|
28
30
|
closeFunctionEditor: () => void;
|
|
31
|
+
closeFormulaWorkspace: () => void;
|
|
29
32
|
exportFile: () => void;
|
|
30
33
|
getCanvasMode: () => string;
|
|
31
34
|
parseMediaEntries: (media: Record<string, string> | null | undefined) => {
|
|
@@ -67,11 +70,15 @@ export function mount(host: HTMLElement, ctx: TabBarCtx) {
|
|
|
67
70
|
void tab.documentPath;
|
|
68
71
|
void tab.session.documentStack.length;
|
|
69
72
|
void tab.session.ui.canvasMode;
|
|
73
|
+
void tab.session.ui.editZoom;
|
|
74
|
+
void tab.session.ui.editingFormula;
|
|
70
75
|
void tab.session.ui.editingFunction;
|
|
71
76
|
void tab.session.ui.featureToggles;
|
|
72
77
|
void tab.session.ui.preview;
|
|
73
78
|
void tab.session.ui.previewParams;
|
|
79
|
+
void tab.session.ui.previewProps;
|
|
74
80
|
void tab.session.ui.showLayout;
|
|
81
|
+
void tab.session.ui.zoom;
|
|
75
82
|
}
|
|
76
83
|
render();
|
|
77
84
|
});
|
|
@@ -110,9 +117,13 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
110
117
|
};
|
|
111
118
|
const canvasMode = ctx.getCanvasMode();
|
|
112
119
|
const editing = S.ui.editingFunction as FunctionEditDef | null;
|
|
120
|
+
const formulaEditing = S.ui.editingFormula as FormulaEditDef | null;
|
|
121
|
+
// Any full-screen canvas takeover (function editor or formula workspace) suppresses the zoom
|
|
122
|
+
// Widget, the view-settings cluster, and the Export action below.
|
|
123
|
+
const takeover = Boolean(editing) || Boolean(formulaEditing);
|
|
113
124
|
const hasStack = S.documentStack && S.documentStack.length > 0;
|
|
114
125
|
|
|
115
|
-
// ── Left region: navigation context (function editor
|
|
126
|
+
// ── Left region: navigation context (function editor > formula workspace > document stack) ──
|
|
116
127
|
let navTpl: TemplateResult | typeof nothing = nothing;
|
|
117
128
|
if (editing) {
|
|
118
129
|
const docName = S.documentPath?.split("/").pop() || S.document?.tagName || "document";
|
|
@@ -128,6 +139,27 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
128
139
|
<span class="breadcrumb-item current">${funcLabel}</span>
|
|
129
140
|
</div>
|
|
130
141
|
`;
|
|
142
|
+
} else if (formulaEditing) {
|
|
143
|
+
const docName = S.documentPath?.split("/").pop() || S.document?.tagName || "document";
|
|
144
|
+
const formulaLabel =
|
|
145
|
+
formulaEditing.type === "def"
|
|
146
|
+
? `fx ${formulaEditing.defName}`
|
|
147
|
+
: `fx ${formulaEditing.eventKey}`;
|
|
148
|
+
navTpl = html`
|
|
149
|
+
<div class="breadcrumb">
|
|
150
|
+
<sp-action-button
|
|
151
|
+
size="s"
|
|
152
|
+
title="Close formula workspace"
|
|
153
|
+
@click=${ctx.closeFormulaWorkspace}
|
|
154
|
+
>
|
|
155
|
+
<sp-icon-back slot="icon"></sp-icon-back>
|
|
156
|
+
Back
|
|
157
|
+
</sp-action-button>
|
|
158
|
+
<span class="breadcrumb-item">${docName}</span>
|
|
159
|
+
<span class="breadcrumb-sep"> › </span>
|
|
160
|
+
<span class="breadcrumb-item current">${formulaLabel}</span>
|
|
161
|
+
</div>
|
|
162
|
+
`;
|
|
131
163
|
} else if (hasStack) {
|
|
132
164
|
navTpl = html`
|
|
133
165
|
<div class="breadcrumb">
|
|
@@ -150,6 +182,82 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
150
182
|
`;
|
|
151
183
|
}
|
|
152
184
|
|
|
185
|
+
// ── Left region: zoom widget (every mode with a zoom concept) ──
|
|
186
|
+
// Gates on the EFFECTIVE mode: edit drives the content-reflow `editZoom`, while design /
|
|
187
|
+
// Stylebook / git-diff / preview all render on the panzoom surface and drive `ui.zoom` (with the
|
|
188
|
+
// Panzoom-only fit-to-screen action).
|
|
189
|
+
let zoomTpl: TemplateResult | typeof nothing = nothing;
|
|
190
|
+
if (!takeover && canvasMode === "edit") {
|
|
191
|
+
const editZoom = S.ui.editZoom ?? 1;
|
|
192
|
+
zoomTpl = html`
|
|
193
|
+
<sp-action-group compact size="s" class="tb-zoom">
|
|
194
|
+
<sp-action-button
|
|
195
|
+
size="s"
|
|
196
|
+
title="Zoom out (Ctrl+-)"
|
|
197
|
+
@click=${() => setEditZoom((tab.session.ui.editZoom ?? 1) / 1.2)}
|
|
198
|
+
>
|
|
199
|
+
−
|
|
200
|
+
</sp-action-button>
|
|
201
|
+
<sp-action-button
|
|
202
|
+
size="s"
|
|
203
|
+
class="tb-zoom-label"
|
|
204
|
+
title="Reset to 100% (Ctrl+0)"
|
|
205
|
+
@click=${() => setEditZoom(1)}
|
|
206
|
+
>
|
|
207
|
+
${Math.round(editZoom * 100)}%
|
|
208
|
+
</sp-action-button>
|
|
209
|
+
<sp-action-button
|
|
210
|
+
size="s"
|
|
211
|
+
title="Zoom in (Ctrl+=)"
|
|
212
|
+
@click=${() => setEditZoom((tab.session.ui.editZoom ?? 1) * 1.2)}
|
|
213
|
+
>
|
|
214
|
+
+
|
|
215
|
+
</sp-action-button>
|
|
216
|
+
</sp-action-group>
|
|
217
|
+
`;
|
|
218
|
+
} else if (
|
|
219
|
+
!takeover &&
|
|
220
|
+
(canvasMode === "design" ||
|
|
221
|
+
canvasMode === "stylebook" ||
|
|
222
|
+
canvasMode === "git-diff" ||
|
|
223
|
+
canvasMode === "preview")
|
|
224
|
+
) {
|
|
225
|
+
const zoom = S.ui.zoom ?? 1;
|
|
226
|
+
const setPanZoom = (next: number) => {
|
|
227
|
+
tab.session.ui.zoom = Math.min(5, Math.max(0.05, next));
|
|
228
|
+
applyTransform();
|
|
229
|
+
};
|
|
230
|
+
zoomTpl = html`
|
|
231
|
+
<sp-action-group compact size="s" class="tb-zoom">
|
|
232
|
+
<sp-action-button
|
|
233
|
+
size="s"
|
|
234
|
+
title="Zoom out (Ctrl+-)"
|
|
235
|
+
@click=${() => setPanZoom((tab.session.ui.zoom ?? 1) / 1.2)}
|
|
236
|
+
>
|
|
237
|
+
−
|
|
238
|
+
</sp-action-button>
|
|
239
|
+
<sp-action-button
|
|
240
|
+
size="s"
|
|
241
|
+
class="tb-zoom-label"
|
|
242
|
+
title="Reset to 100% (Ctrl+0)"
|
|
243
|
+
@click=${() => resetZoom()}
|
|
244
|
+
>
|
|
245
|
+
${Math.round(zoom * 100)}%
|
|
246
|
+
</sp-action-button>
|
|
247
|
+
<sp-action-button
|
|
248
|
+
size="s"
|
|
249
|
+
title="Zoom in (Ctrl+=)"
|
|
250
|
+
@click=${() => setPanZoom((tab.session.ui.zoom ?? 1) * 1.2)}
|
|
251
|
+
>
|
|
252
|
+
+
|
|
253
|
+
</sp-action-button>
|
|
254
|
+
<sp-action-button size="s" title="Fit to screen" @click=${() => fitToScreen()}>
|
|
255
|
+
Fit
|
|
256
|
+
</sp-action-button>
|
|
257
|
+
</sp-action-group>
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
|
|
153
261
|
// ── Right region: view settings cluster (edit/design base modes only) ──
|
|
154
262
|
// Gates on the BASE mode (not the effective mode): the cluster stays visible while the preview
|
|
155
263
|
// Toggle is on so it can be toggled back off.
|
|
@@ -160,10 +268,11 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
160
268
|
(S.documentPath.startsWith("pages/") || S.documentPath.startsWith("./pages/")),
|
|
161
269
|
);
|
|
162
270
|
let settingsTpl: TemplateResult | typeof nothing = nothing;
|
|
163
|
-
if (!
|
|
271
|
+
if (!takeover && (baseMode === "edit" || baseMode === "design")) {
|
|
164
272
|
const canPreview = tab.capabilities.modes.includes("preview");
|
|
165
273
|
const hasLayout = isPage && Boolean(getEffectiveLayoutPath(S.document?.$layout));
|
|
166
|
-
|
|
274
|
+
// Pages get route-param pickers; component docs get test-prop fields (M6) — same slot.
|
|
275
|
+
const pickersTpl = isPage ? paramPickersTpl(tab) : propFieldsTpl(tab);
|
|
167
276
|
const previewTpl = canPreview
|
|
168
277
|
? html`
|
|
169
278
|
<sp-action-button
|
|
@@ -230,7 +339,7 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
230
339
|
|
|
231
340
|
// ── Right region: mode actions (Code-mode Export) ──
|
|
232
341
|
const exportTpl =
|
|
233
|
-
!
|
|
342
|
+
!takeover && canvasMode === "source"
|
|
234
343
|
? html`
|
|
235
344
|
<sp-action-button size="s" @click=${ctx.exportFile}>
|
|
236
345
|
<sp-icon-export slot="icon"></sp-icon-export>
|
|
@@ -241,7 +350,7 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
241
350
|
|
|
242
351
|
return html`
|
|
243
352
|
<div class="tab-bar">
|
|
244
|
-
${navTpl}
|
|
353
|
+
${navTpl} ${zoomTpl}
|
|
245
354
|
<div class="tb-spacer"></div>
|
|
246
355
|
${settingsTpl} ${togglesTpl} ${exportTpl}
|
|
247
356
|
</div>
|
|
@@ -342,3 +451,63 @@ function paramPickersTpl(tab: Tab): TemplateResult | typeof nothing {
|
|
|
342
451
|
)}
|
|
343
452
|
`;
|
|
344
453
|
}
|
|
454
|
+
|
|
455
|
+
// ── Component test-prop fields (M6) ──────────────────────────────────────────
|
|
456
|
+
// The previewParams mirror for component docs: one small field per prop entry (the doc's
|
|
457
|
+
// Plain-data state entries), committed on change so typing never re-renders the bar mid-edit. A
|
|
458
|
+
// Value parses as JSON when it can (numbers, booleans, arrays) and falls back to the raw string;
|
|
459
|
+
// Clearing a field removes the override so the prop returns to its authored default.
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* @param {string} raw
|
|
463
|
+
* @returns {JsonValue}
|
|
464
|
+
*/
|
|
465
|
+
function parsePropValue(raw: string): JsonValue {
|
|
466
|
+
try {
|
|
467
|
+
return JSON.parse(raw) as JsonValue;
|
|
468
|
+
} catch {
|
|
469
|
+
return raw;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* @param {Tab} tab
|
|
475
|
+
* @returns {TemplateResult | typeof nothing}
|
|
476
|
+
*/
|
|
477
|
+
function propFieldsTpl(tab: Tab): TemplateResult | typeof nothing {
|
|
478
|
+
const doc = tab.doc.document;
|
|
479
|
+
if (!isComponentDoc(doc)) {
|
|
480
|
+
return nothing;
|
|
481
|
+
}
|
|
482
|
+
const entries = componentPropEntries(doc);
|
|
483
|
+
if (entries.length === 0) {
|
|
484
|
+
return nothing;
|
|
485
|
+
}
|
|
486
|
+
const { previewProps } = tab.session.ui;
|
|
487
|
+
const display = (v: JsonValue | undefined) =>
|
|
488
|
+
v === undefined ? "" : typeof v === "string" ? v : JSON.stringify(v);
|
|
489
|
+
return html`
|
|
490
|
+
${entries.map(
|
|
491
|
+
({ name }) => html`
|
|
492
|
+
<sp-textfield
|
|
493
|
+
size="s"
|
|
494
|
+
quiet
|
|
495
|
+
class="tab-bar-prop"
|
|
496
|
+
placeholder=${name}
|
|
497
|
+
title=${`Test value for ${name}`}
|
|
498
|
+
.value=${display(previewProps?.[name])}
|
|
499
|
+
@change=${(e: Event) => {
|
|
500
|
+
const raw = (e.target as HTMLInputElement).value;
|
|
501
|
+
const next = { ...tab.session.ui.previewProps };
|
|
502
|
+
if (raw === "") {
|
|
503
|
+
delete next[name];
|
|
504
|
+
} else {
|
|
505
|
+
next[name] = parsePropValue(raw);
|
|
506
|
+
}
|
|
507
|
+
updateUi("previewProps", Object.keys(next).length > 0 ? next : null);
|
|
508
|
+
}}
|
|
509
|
+
></sp-textfield>
|
|
510
|
+
`,
|
|
511
|
+
)}
|
|
512
|
+
`;
|
|
513
|
+
}
|
package/src/panels/tab-strip.ts
CHANGED
|
@@ -11,14 +11,18 @@ import { classMap } from "lit-html/directives/class-map.js";
|
|
|
11
11
|
import { repeat } from "lit-html/directives/repeat.js";
|
|
12
12
|
import { effect, effectScope } from "../reactivity";
|
|
13
13
|
import { activateTab, closeTab, workspace } from "../workspace/workspace";
|
|
14
|
+
import { gridTabLabel } from "../grid/grid-source";
|
|
14
15
|
import type { Tab } from "../tabs/tab";
|
|
15
16
|
import { showConfirmDialog } from "../ui/layers";
|
|
17
|
+
import { collabState } from "../collab/collab-state";
|
|
16
18
|
import type { EffectScope } from "@vue/reactivity";
|
|
17
19
|
|
|
18
20
|
let _host: HTMLElement | null = null;
|
|
19
21
|
|
|
20
22
|
let _scope: EffectScope | null = null;
|
|
21
23
|
|
|
24
|
+
let _lastActiveId: string | null = null;
|
|
25
|
+
|
|
22
26
|
/**
|
|
23
27
|
* Mount the tab strip into the given host element.
|
|
24
28
|
*
|
|
@@ -26,6 +30,7 @@ let _scope: EffectScope | null = null;
|
|
|
26
30
|
*/
|
|
27
31
|
export function mount(host: HTMLElement) {
|
|
28
32
|
_host = host;
|
|
33
|
+
_lastActiveId = null;
|
|
29
34
|
_scope = effectScope();
|
|
30
35
|
_scope.run(() => {
|
|
31
36
|
effect(() => {
|
|
@@ -52,13 +57,14 @@ function render() {
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
if (workspace.tabOrder.length === 0) {
|
|
60
|
+
_lastActiveId = null;
|
|
55
61
|
litRender(nothing, _host);
|
|
56
62
|
return;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
litRender(
|
|
60
66
|
html`
|
|
61
|
-
<div class="tab-strip">
|
|
67
|
+
<div class="tab-strip" @wheel=${onWheel}>
|
|
62
68
|
${repeat(
|
|
63
69
|
workspace.tabOrder,
|
|
64
70
|
(id) => id,
|
|
@@ -80,7 +86,7 @@ function render() {
|
|
|
80
86
|
void requestClose(id);
|
|
81
87
|
}
|
|
82
88
|
}}
|
|
83
|
-
title=${tab.documentPath || "Untitled"}
|
|
89
|
+
title=${tab.documentPath || gridTabLabel(tab.id) || "Untitled"}
|
|
84
90
|
>
|
|
85
91
|
<span class="tab-strip-label">${label}</span>
|
|
86
92
|
${isDirty ? html`<span class="tab-strip-dirty">●</span>` : nothing}
|
|
@@ -101,10 +107,40 @@ function render() {
|
|
|
101
107
|
`,
|
|
102
108
|
_host,
|
|
103
109
|
);
|
|
110
|
+
|
|
111
|
+
if (workspace.activeTabId !== _lastActiveId) {
|
|
112
|
+
_lastActiveId = workspace.activeTabId;
|
|
113
|
+
_host
|
|
114
|
+
.querySelector(".tab-strip-tab.active")
|
|
115
|
+
?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Scroll the strip horizontally from wheel motion so overflowed tabs stay reachable — a plain mouse
|
|
121
|
+
* wheel only emits deltaY, which does nothing in an overflow-x container. The scrollbar is hidden
|
|
122
|
+
* by design, so the wheel is the primary way to reach off-screen tabs.
|
|
123
|
+
*
|
|
124
|
+
* @param {WheelEvent} e
|
|
125
|
+
*/
|
|
126
|
+
function onWheel(e: WheelEvent) {
|
|
127
|
+
if (e.ctrlKey) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const el = e.currentTarget as HTMLElement;
|
|
131
|
+
if (el.scrollWidth <= el.clientWidth) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
|
|
135
|
+
if (!delta) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
el.scrollLeft += delta;
|
|
104
140
|
}
|
|
105
141
|
|
|
106
142
|
/**
|
|
107
|
-
* Derive a short label from the tab's documentPath.
|
|
143
|
+
* Derive a short label from the tab's documentPath (virtual grid tabs label from their id).
|
|
108
144
|
*
|
|
109
145
|
* @param {Tab} tab
|
|
110
146
|
* @returns {string}
|
|
@@ -112,14 +148,34 @@ function render() {
|
|
|
112
148
|
function tabLabel(tab: Tab) {
|
|
113
149
|
const path = tab.documentPath;
|
|
114
150
|
if (!path) {
|
|
115
|
-
return "Untitled";
|
|
151
|
+
return gridTabLabel(tab.id) ?? "Untitled";
|
|
116
152
|
}
|
|
117
153
|
const parts = path.split("/");
|
|
118
154
|
return parts.at(-1);
|
|
119
155
|
}
|
|
120
156
|
|
|
121
157
|
/**
|
|
122
|
-
*
|
|
158
|
+
* True when closing this tab would lose unsaved work the user must be warned about: the tab is
|
|
159
|
+
* dirty AND either it is not co-edited, or this client is the last active collaborator on the doc
|
|
160
|
+
* (no other peer is focused on its path). When peers remain, the shared session lives on and the
|
|
161
|
+
* edits are still on the server, so closing is safe.
|
|
162
|
+
*
|
|
163
|
+
* @param {Tab} tab
|
|
164
|
+
*/
|
|
165
|
+
export function shouldWarnOnClose(tab: Tab): boolean {
|
|
166
|
+
if (!tab.doc.dirty) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const state = collabState(tab);
|
|
170
|
+
if (!state.active) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
const peersHere = state.peers.filter((p) => p.state?.focusedPath === tab.documentPath);
|
|
174
|
+
return peersHere.length === 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Close a tab, prompting if closing would lose unsaved work.
|
|
123
179
|
*
|
|
124
180
|
* @param {string} id
|
|
125
181
|
*/
|
|
@@ -128,7 +184,7 @@ async function requestClose(id: string) {
|
|
|
128
184
|
if (!tab) {
|
|
129
185
|
return;
|
|
130
186
|
}
|
|
131
|
-
if (tab
|
|
187
|
+
if (shouldWarnOnClose(tab)) {
|
|
132
188
|
const confirmed = await showConfirmDialog(
|
|
133
189
|
"Unsaved Changes",
|
|
134
190
|
`"${tabLabel(tab)}" has unsaved changes. Close without saving?`,
|
package/src/panels/toolbar.ts
CHANGED
|
@@ -43,6 +43,19 @@ let _rootEl: HTMLElement | null = null;
|
|
|
43
43
|
|
|
44
44
|
let _ctx: ToolbarCtx | null = null;
|
|
45
45
|
|
|
46
|
+
/** Test override for the mac CSD layout — happy-dom forbids redefining navigator.platform. */
|
|
47
|
+
let _isMacOverride: boolean | null = null;
|
|
48
|
+
|
|
49
|
+
/** Force (or restore, with null) the mac/non-mac window-control layout detection. */
|
|
50
|
+
export function setMacPlatformForTests(value: boolean | null): void {
|
|
51
|
+
_isMacOverride = value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True on macOS — picks the CSD window-control order (close-first, toolbar-leading). */
|
|
55
|
+
function isMacPlatform(): boolean {
|
|
56
|
+
return _isMacOverride ?? navigator.platform.startsWith("Mac");
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
let _scope: EffectScope | null = null;
|
|
47
60
|
|
|
48
61
|
const toolbarIconMap = {
|
|
@@ -59,6 +72,7 @@ const toolbarIconMap = {
|
|
|
59
72
|
"sp-icon-redo": html`<sp-icon-redo slot="icon"></sp-icon-redo>`,
|
|
60
73
|
"sp-icon-save-floppy": html`<sp-icon-save-floppy slot="icon"></sp-icon-save-floppy>`,
|
|
61
74
|
"sp-icon-undo": html`<sp-icon-undo slot="icon"></sp-icon-undo>`,
|
|
75
|
+
"sp-icon-view-grid": html`<sp-icon-view-grid slot="icon"></sp-icon-view-grid>`,
|
|
62
76
|
"sp-icon-view-list": html`<sp-icon-view-list slot="icon"></sp-icon-view-list>`,
|
|
63
77
|
} as Record<string, TemplateResult>;
|
|
64
78
|
|
|
@@ -308,13 +322,33 @@ function minimalToolbarTemplate(ctx: ToolbarCtx) {
|
|
|
308
322
|
? html`<sp-icon-rail-right-open slot="icon"></sp-icon-rail-right-open>`
|
|
309
323
|
: html`<sp-icon-rail-right-close slot="icon"></sp-icon-rail-right-close>`}
|
|
310
324
|
</sp-action-button>
|
|
311
|
-
${csdTpl}
|
|
325
|
+
${chatToggleTpl()} ${csdTpl}
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Toggle for the persistent AI chat sidebar (selected = open). Shared by both layouts. */
|
|
330
|
+
function chatToggleTpl() {
|
|
331
|
+
return html`
|
|
332
|
+
<sp-action-button
|
|
333
|
+
quiet
|
|
334
|
+
size="s"
|
|
335
|
+
title="Toggle Assistant"
|
|
336
|
+
?selected=${!view.chatPanelCollapsed}
|
|
337
|
+
@click=${() => {
|
|
338
|
+
view.chatPanelCollapsed = !view.chatPanelCollapsed;
|
|
339
|
+
applyPanelCollapse();
|
|
340
|
+
render();
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
<sp-icon-chat slot="icon"></sp-icon-chat>
|
|
344
|
+
</sp-action-button>
|
|
312
345
|
`;
|
|
313
346
|
}
|
|
314
347
|
|
|
315
348
|
const modes = [
|
|
316
349
|
{ iconTag: "sp-icon-edit", key: "edit", label: "Edit" },
|
|
317
350
|
{ iconTag: "sp-icon-artboard", key: "design", label: "Design" },
|
|
351
|
+
{ iconTag: "sp-icon-view-grid", key: "grid", label: "Grid" },
|
|
318
352
|
{ iconTag: "sp-icon-code", key: "source", label: "Code" },
|
|
319
353
|
{ iconTag: "sp-icon-brush", key: "stylebook", label: "Stylebook" },
|
|
320
354
|
];
|
|
@@ -399,7 +433,7 @@ function toolbarTemplate() {
|
|
|
399
433
|
};
|
|
400
434
|
}
|
|
401
435
|
).__jxPlatform?.windowControls;
|
|
402
|
-
const isMac =
|
|
436
|
+
const isMac = isMacPlatform();
|
|
403
437
|
const csdTpl = windowControls
|
|
404
438
|
? isMac
|
|
405
439
|
? html`
|
|
@@ -545,6 +579,6 @@ function toolbarTemplate() {
|
|
|
545
579
|
? html`<sp-icon-rail-right-open slot="icon"></sp-icon-rail-right-open>`
|
|
546
580
|
: html`<sp-icon-rail-right-close slot="icon"></sp-icon-rail-right-close>`}
|
|
547
581
|
</sp-action-button>
|
|
548
|
-
${isMac ? nothing : csdTpl}
|
|
582
|
+
${chatToggleTpl()} ${isMac ? nothing : csdTpl}
|
|
549
583
|
`;
|
|
550
584
|
}
|