@jxsuite/studio 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/studio.js +1229 -477
- package/dist/studio.js.map +41 -39
- package/package.json +6 -5
- package/src/browse/browse-modal.js +16 -13
- package/src/browse/browse.js +19 -16
- package/src/canvas/canvas-live-render.js +19 -0
- package/src/canvas/canvas-utils.js +13 -6
- package/src/editor/context-menu.js +27 -13
- package/src/editor/convert-targets.js +60 -0
- package/src/editor/convert-to-component.js +9 -10
- package/src/editor/convert-to-repeater.js +226 -0
- package/src/editor/inline-edit.js +3 -0
- package/src/editor/shortcuts.js +10 -2
- package/src/editor/slash-menu.js +36 -17
- package/src/files/files.js +244 -16
- package/src/github/github-publish.js +26 -15
- package/src/panels/activity-bar.js +5 -5
- package/src/panels/ai-panel.js +10 -3
- package/src/panels/block-action-bar.js +73 -24
- package/src/panels/git-panel.js +7 -2
- package/src/panels/imports-panel.js +6 -1
- package/src/panels/left-panel.js +20 -1
- package/src/panels/properties-panel.js +16 -11
- package/src/panels/quick-search.js +4 -4
- package/src/panels/right-panel.js +16 -14
- package/src/panels/signals-panel.js +120 -0
- package/src/panels/statusbar.js +6 -2
- package/src/panels/style-panel.js +6 -2
- package/src/panels/tab-strip.js +5 -2
- package/src/settings/content-types-editor.js +1 -1
- package/src/settings/settings-modal.js +12 -9
- package/src/store.js +15 -9
- package/src/studio.js +17 -9
- package/src/ui/layers.js +31 -1
- package/src/utils/edit-display.js +1 -6
- package/src/utils/studio-utils.js +11 -11
- package/src/workspace/workspace.js +19 -0
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import { html, render as litRender, nothing } from "lit-html";
|
|
7
7
|
import { styleMap } from "lit-html/directives/style-map.js";
|
|
8
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
8
9
|
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
9
10
|
|
|
10
11
|
import { getNodeAtPath, nodeLabel, parentElementPath, childIndex } from "../store.js";
|
|
11
12
|
import { activeTab } from "../workspace/workspace.js";
|
|
12
|
-
import { transactDoc, mutateMoveNode } from "../tabs/transact.js";
|
|
13
|
+
import { transactDoc, mutateMoveNode, mutateUpdateProperty } from "../tabs/transact.js";
|
|
13
14
|
import { view } from "../view.js";
|
|
14
15
|
import { isEditing, getActiveElement, getInlineActions } from "../editor/inline-edit.js";
|
|
15
16
|
import { toggleInlineFormat, isTagActiveInSelection } from "../editor/inline-format.js";
|
|
@@ -17,6 +18,8 @@ import { componentRegistry } from "../files/components.js";
|
|
|
17
18
|
import { convertToComponent } from "../editor/convert-to-component.js";
|
|
18
19
|
import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
|
|
19
20
|
import { getLayerSlot } from "../ui/layers.js";
|
|
21
|
+
import { showSlashMenu } from "../editor/slash-menu.js";
|
|
22
|
+
import { getConvertTargets } from "../editor/convert-targets.js";
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* @typedef {import("../state.js").StudioState} StudioState
|
|
@@ -70,9 +73,29 @@ const formatIconMap = /** @type {Record<string, import("lit-html").TemplateResul
|
|
|
70
73
|
function onBarMousedown(e) {
|
|
71
74
|
if (/** @type {HTMLElement} */ (e.target).closest("sp-textfield")) return;
|
|
72
75
|
if (/** @type {HTMLElement} */ (e.target).closest(".bar-drag-handle")) return;
|
|
76
|
+
if (/** @type {HTMLElement} */ (e.target).closest(".bar-tag--interactive")) return;
|
|
73
77
|
e.preventDefault();
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
/**
|
|
81
|
+
* @param {MouseEvent} e
|
|
82
|
+
* @param {import("../editor/convert-targets.js").SlashCommand[]} targets
|
|
83
|
+
* @param {JxPath} selection
|
|
84
|
+
*/
|
|
85
|
+
function onTagBadgeClick(e, targets, selection) {
|
|
86
|
+
e.stopPropagation();
|
|
87
|
+
const anchorEl = /** @type {HTMLElement} */ (e.currentTarget);
|
|
88
|
+
showSlashMenu(anchorEl, "", {
|
|
89
|
+
showFilter: targets.length > 6,
|
|
90
|
+
commands: targets,
|
|
91
|
+
onSelect: (cmd) => {
|
|
92
|
+
transactDoc(activeTab.value, (t) => {
|
|
93
|
+
mutateUpdateProperty(t, selection, "tagName", cmd.tag);
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
/** Saved selection range for format button mousedown→click flow */
|
|
77
100
|
function captureSelectionRange() {
|
|
78
101
|
const sel = window.getSelection();
|
|
@@ -226,9 +249,11 @@ function showLinkPopover(anchorBtn) {
|
|
|
226
249
|
|
|
227
250
|
const rect = anchorBtn.getBoundingClientRect();
|
|
228
251
|
|
|
252
|
+
/** @type {HTMLInputElement | null} */
|
|
253
|
+
let _linkField = null;
|
|
254
|
+
|
|
229
255
|
const onApply = () => {
|
|
230
|
-
const
|
|
231
|
-
const url = field?.value || "";
|
|
256
|
+
const url = _linkField?.value || "";
|
|
232
257
|
if (existingLink) {
|
|
233
258
|
existingLink.setAttribute("href", url);
|
|
234
259
|
} else if (url) {
|
|
@@ -272,6 +297,10 @@ function showLinkPopover(anchorBtn) {
|
|
|
272
297
|
style="width:200px"
|
|
273
298
|
value=${existingLink?.getAttribute("href") || ""}
|
|
274
299
|
@keydown=${onKeydown}
|
|
300
|
+
${ref((el) => {
|
|
301
|
+
_linkField = /** @type {HTMLInputElement | null} */ (el || null);
|
|
302
|
+
if (el) requestAnimationFrame(() => /** @type {HTMLElement} */ (el).focus());
|
|
303
|
+
})}
|
|
275
304
|
></sp-textfield>
|
|
276
305
|
<sp-action-button size="xs" @click=${onApply}>
|
|
277
306
|
${existingLink ? "Update" : "Apply"}
|
|
@@ -283,10 +312,6 @@ function showLinkPopover(anchorBtn) {
|
|
|
283
312
|
`,
|
|
284
313
|
host,
|
|
285
314
|
);
|
|
286
|
-
|
|
287
|
-
requestAnimationFrame(
|
|
288
|
-
() => /** @type {HTMLElement | null} */ (host.querySelector("sp-textfield"))?.focus(),
|
|
289
|
-
);
|
|
290
315
|
}
|
|
291
316
|
|
|
292
317
|
/** Move the selected node up (swap with previous sibling). */
|
|
@@ -360,6 +385,21 @@ export function renderBlockActionBar() {
|
|
|
360
385
|
? actions.filter((a) => isTagActiveInSelection(a.tag, el)).map((a) => a.tag)
|
|
361
386
|
: [];
|
|
362
387
|
|
|
388
|
+
// Conversion targets for badge click
|
|
389
|
+
const isComponent =
|
|
390
|
+
node.tagName?.includes("-") &&
|
|
391
|
+
componentRegistry.some((/** @type {{ tagName: string }} */ c) => c.tagName === node.tagName);
|
|
392
|
+
const isEmpty =
|
|
393
|
+
!node.textContent &&
|
|
394
|
+
(!node.children ||
|
|
395
|
+
node.children.length === 0 ||
|
|
396
|
+
(Array.isArray(node.children) &&
|
|
397
|
+
node.children.length === 1 &&
|
|
398
|
+
typeof node.children[0] === "object" &&
|
|
399
|
+
node.children[0]?.tagName === "br"));
|
|
400
|
+
const convertTargets = !isComponent ? getConvertTargets(tag, isEmpty) : [];
|
|
401
|
+
const badgeInteractive = convertTargets.length > 0;
|
|
402
|
+
|
|
363
403
|
litRender(
|
|
364
404
|
html`
|
|
365
405
|
<div
|
|
@@ -369,10 +409,34 @@ export function renderBlockActionBar() {
|
|
|
369
409
|
>
|
|
370
410
|
${selection.length >= 2 ? renderParentSelector() : nothing}
|
|
371
411
|
|
|
372
|
-
<span
|
|
412
|
+
<span
|
|
413
|
+
class="bar-tag${badgeInteractive ? " bar-tag--interactive" : ""}"
|
|
414
|
+
@click=${badgeInteractive
|
|
415
|
+
? (/** @type {MouseEvent} */ e) => onTagBadgeClick(e, convertTargets, selection)
|
|
416
|
+
: nothing}
|
|
417
|
+
>${node.$id || (node.tagName ?? "div")}</span
|
|
418
|
+
>
|
|
373
419
|
|
|
374
420
|
${selection.length >= 2
|
|
375
|
-
? html`<span
|
|
421
|
+
? html`<span
|
|
422
|
+
class="bar-drag-handle"
|
|
423
|
+
title="Drag to reorder"
|
|
424
|
+
${ref((el) => {
|
|
425
|
+
if (!el) return;
|
|
426
|
+
if (view.selDragCleanup) {
|
|
427
|
+
view.selDragCleanup();
|
|
428
|
+
view.selDragCleanup = null;
|
|
429
|
+
}
|
|
430
|
+
view.selDragCleanup = draggable({
|
|
431
|
+
element: /** @type {HTMLElement} */ (el),
|
|
432
|
+
getInitialData: () => ({
|
|
433
|
+
type: "tree-node",
|
|
434
|
+
path: activeTab.value?.session.selection,
|
|
435
|
+
}),
|
|
436
|
+
});
|
|
437
|
+
})}
|
|
438
|
+
>⠿</span
|
|
439
|
+
>`
|
|
376
440
|
: nothing}
|
|
377
441
|
${selection.length >= 2 ? renderMoveArrows() : nothing}
|
|
378
442
|
${selection.length >= 2 && node.tagName
|
|
@@ -444,20 +508,5 @@ export function renderBlockActionBar() {
|
|
|
444
508
|
if (barRect.right > window.innerWidth) {
|
|
445
509
|
bar.style.left = `${Math.max(0, window.innerWidth - barRect.width)}px`;
|
|
446
510
|
}
|
|
447
|
-
// Attach drag handle
|
|
448
|
-
const currentTab = activeTab.value;
|
|
449
|
-
if (currentTab?.session.selection && currentTab.session.selection.length >= 2) {
|
|
450
|
-
const handle = /** @type {HTMLElement | null} */ (bar.querySelector(".bar-drag-handle"));
|
|
451
|
-
if (handle) {
|
|
452
|
-
if (view.selDragCleanup) {
|
|
453
|
-
view.selDragCleanup();
|
|
454
|
-
view.selDragCleanup = null;
|
|
455
|
-
}
|
|
456
|
-
view.selDragCleanup = draggable({
|
|
457
|
-
element: handle,
|
|
458
|
-
getInitialData: () => ({ type: "tree-node", path: activeTab.value?.session.selection }),
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
511
|
});
|
|
463
512
|
}
|
package/src/panels/git-panel.js
CHANGED
|
@@ -10,7 +10,7 @@ import { getPlatform } from "../platform.js";
|
|
|
10
10
|
import { updateUi, renderOnly, projectState } from "../store.js";
|
|
11
11
|
import { activeTab } from "../workspace/workspace.js";
|
|
12
12
|
import { view } from "../view.js";
|
|
13
|
-
import { showDialog } from "../ui/layers.js";
|
|
13
|
+
import { showDialog, showConfirmDialog } from "../ui/layers.js";
|
|
14
14
|
import { statusMessage } from "./statusbar.js";
|
|
15
15
|
import { publishToGithub } from "../github/github-publish.js";
|
|
16
16
|
|
|
@@ -531,7 +531,12 @@ export function renderGitPanel(S, ctx) {
|
|
|
531
531
|
title="Discard changes"
|
|
532
532
|
@click=${async () => {
|
|
533
533
|
if (file.status === "U") return;
|
|
534
|
-
|
|
534
|
+
const confirmed = await showConfirmDialog(
|
|
535
|
+
"Discard Changes",
|
|
536
|
+
`Discard changes to ${file.path}?`,
|
|
537
|
+
{ confirmLabel: "Discard", destructive: true },
|
|
538
|
+
);
|
|
539
|
+
if (!confirmed) return;
|
|
535
540
|
await gitAction("gitDiscard", [file.path]);
|
|
536
541
|
}}
|
|
537
542
|
?disabled=${file.status === "U"}
|
|
@@ -12,6 +12,7 @@ import { componentRegistry, computeRelativePath } from "../files/components.js";
|
|
|
12
12
|
import { projectState } from "../store.js";
|
|
13
13
|
import { updateSiteConfig } from "../site-context.js";
|
|
14
14
|
import { getPlatform } from "../platform.js";
|
|
15
|
+
import { showConfirmDialog } from "../ui/layers.js";
|
|
15
16
|
|
|
16
17
|
/** @typedef {import("../files/components.js").ComponentEntry} ComponentEntry */
|
|
17
18
|
|
|
@@ -185,7 +186,11 @@ function renderSiteLevelImports(renderLeftPanel) {
|
|
|
185
186
|
size="xs"
|
|
186
187
|
title="Remove package"
|
|
187
188
|
@click=${async () => {
|
|
188
|
-
|
|
189
|
+
const confirmed = await showConfirmDialog("Remove Package", `Remove ${pkg}?`, {
|
|
190
|
+
confirmLabel: "Remove",
|
|
191
|
+
destructive: true,
|
|
192
|
+
});
|
|
193
|
+
if (!confirmed) return;
|
|
189
194
|
try {
|
|
190
195
|
const platform = getPlatform();
|
|
191
196
|
await platform.removePackage(pkg);
|
package/src/panels/left-panel.js
CHANGED
|
@@ -40,6 +40,7 @@ import { selectStylebookTag, stylebookMeta } from "./stylebook-panel.js";
|
|
|
40
40
|
* registerElementsDnD: () => void;
|
|
41
41
|
* registerComponentsDnD: () => void;
|
|
42
42
|
* setupTreeKeyboard: (tree: HTMLElement) => void;
|
|
43
|
+
* registerFileTreeDnD: (ctx: { renderLeftPanel: () => void }) => void;
|
|
43
44
|
* setGitDiffState: (state: import("../canvas/canvas-render.js").GitDiffState | null) => void;
|
|
44
45
|
* cloneRepository?: () => void;
|
|
45
46
|
* }} LeftPanelCtx
|
|
@@ -53,6 +54,16 @@ let _scope = null;
|
|
|
53
54
|
|
|
54
55
|
let _rendering = false;
|
|
55
56
|
let _scheduled = false;
|
|
57
|
+
let _hasFocus = false;
|
|
58
|
+
|
|
59
|
+
function _onFocusIn() {
|
|
60
|
+
_hasFocus = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _onFocusOut() {
|
|
64
|
+
_hasFocus = false;
|
|
65
|
+
render();
|
|
66
|
+
}
|
|
56
67
|
|
|
57
68
|
/**
|
|
58
69
|
* Mount the left panel orchestrator.
|
|
@@ -61,6 +72,8 @@ let _scheduled = false;
|
|
|
61
72
|
*/
|
|
62
73
|
export function mount(ctx) {
|
|
63
74
|
_ctx = ctx;
|
|
75
|
+
leftPanel.addEventListener("focusin", _onFocusIn);
|
|
76
|
+
leftPanel.addEventListener("focusout", _onFocusOut);
|
|
64
77
|
_scope = effectScope();
|
|
65
78
|
_scope.run(() => {
|
|
66
79
|
effect(() => {
|
|
@@ -75,7 +88,9 @@ export function mount(ctx) {
|
|
|
75
88
|
void tab.session.ui.gitLoading;
|
|
76
89
|
void tab.session.ui.gitError;
|
|
77
90
|
}
|
|
78
|
-
|
|
91
|
+
if (!_hasFocus) {
|
|
92
|
+
render();
|
|
93
|
+
}
|
|
79
94
|
});
|
|
80
95
|
});
|
|
81
96
|
}
|
|
@@ -84,6 +99,9 @@ export function unmount() {
|
|
|
84
99
|
_scope?.stop();
|
|
85
100
|
_scope = null;
|
|
86
101
|
_ctx = null;
|
|
102
|
+
leftPanel.removeEventListener("focusin", _onFocusIn);
|
|
103
|
+
leftPanel.removeEventListener("focusout", _onFocusOut);
|
|
104
|
+
_hasFocus = false;
|
|
87
105
|
}
|
|
88
106
|
|
|
89
107
|
export function render() {
|
|
@@ -132,6 +150,7 @@ function _render() {
|
|
|
132
150
|
litRender(html`<div class="panel-body">${ctx.renderFilesTemplate()}</div>`, leftPanel);
|
|
133
151
|
const tree = /** @type {HTMLElement | null} */ (leftPanel.querySelector(".file-tree"));
|
|
134
152
|
if (tree) ctx.setupTreeKeyboard(tree);
|
|
153
|
+
ctx.registerFileTreeDnD({ renderLeftPanel: render });
|
|
135
154
|
return;
|
|
136
155
|
}
|
|
137
156
|
|
|
@@ -97,14 +97,19 @@ function bindableFieldRow(
|
|
|
97
97
|
const staticVal = isBound ? "" : (rawValue ?? "");
|
|
98
98
|
const staticTpl =
|
|
99
99
|
type === "textarea"
|
|
100
|
-
? html`<sp-textfield
|
|
100
|
+
? html`<sp-textfield
|
|
101
|
+
multiline
|
|
102
|
+
size="s"
|
|
103
|
+
.value=${live(staticVal)}
|
|
104
|
+
@input=${onInput}
|
|
105
|
+
></sp-textfield>`
|
|
101
106
|
: type === "checkbox"
|
|
102
107
|
? html`<sp-checkbox
|
|
103
108
|
?checked=${!!staticVal}
|
|
104
109
|
@change=${(/** @type {Event} */ e) =>
|
|
105
110
|
onChange(/** @type {HTMLInputElement} */ (e.target).checked)}
|
|
106
111
|
></sp-checkbox>`
|
|
107
|
-
: html`<sp-textfield size="s" value=${staticVal} @input=${onInput}></sp-textfield>`;
|
|
112
|
+
: html`<sp-textfield size="s" .value=${live(staticVal)} @input=${onInput}></sp-textfield>`;
|
|
108
113
|
|
|
109
114
|
const boundTpl = html`
|
|
110
115
|
<sp-picker
|
|
@@ -189,7 +194,7 @@ function kvRow(
|
|
|
189
194
|
<sp-textfield
|
|
190
195
|
size="s"
|
|
191
196
|
class="kv-key"
|
|
192
|
-
value=${key}
|
|
197
|
+
.value=${live(key)}
|
|
193
198
|
@input=${(/** @type {Event} */ e) => {
|
|
194
199
|
currentKey = /** @type {HTMLInputElement} */ (e.target).value;
|
|
195
200
|
commit();
|
|
@@ -210,7 +215,7 @@ function kvRow(
|
|
|
210
215
|
<sp-textfield
|
|
211
216
|
size="s"
|
|
212
217
|
class="kv-val"
|
|
213
|
-
value=${value}
|
|
218
|
+
.value=${live(value)}
|
|
214
219
|
placeholder=${placeholder}
|
|
215
220
|
@input=${(/** @type {Event} */ e) => {
|
|
216
221
|
currentVal = /** @type {HTMLInputElement} */ (e.target).value;
|
|
@@ -316,7 +321,7 @@ function renderSwitchFieldsTemplate(
|
|
|
316
321
|
<div class="field-row" style="display:flex;align-items:center;gap:4px;margin-bottom:3px">
|
|
317
322
|
<input
|
|
318
323
|
class="field-input"
|
|
319
|
-
value=${caseName}
|
|
324
|
+
.value=${live(caseName)}
|
|
320
325
|
style="flex:1"
|
|
321
326
|
@input=${(/** @type {Event} */ e) => {
|
|
322
327
|
clearTimeout(debounce);
|
|
@@ -476,7 +481,7 @@ function renderComponentPropsFieldsTemplate(
|
|
|
476
481
|
widgetTpl = html`<sp-textfield
|
|
477
482
|
size="s"
|
|
478
483
|
placeholder="YYYY-MM-DD"
|
|
479
|
-
value=${staticVal}
|
|
484
|
+
.value=${live(staticVal)}
|
|
480
485
|
@input=${(/** @type {Event} */ e) => {
|
|
481
486
|
clearTimeout(debounce);
|
|
482
487
|
debounce = setTimeout(
|
|
@@ -495,7 +500,7 @@ function renderComponentPropsFieldsTemplate(
|
|
|
495
500
|
} else if (parsed.kind === "number") {
|
|
496
501
|
widgetTpl = html`<sp-number-field
|
|
497
502
|
size="s"
|
|
498
|
-
value=${staticVal}
|
|
503
|
+
.value=${live(staticVal)}
|
|
499
504
|
@input=${(/** @type {Event} */ e) => {
|
|
500
505
|
clearTimeout(debounce);
|
|
501
506
|
debounce = setTimeout(
|
|
@@ -519,7 +524,7 @@ function renderComponentPropsFieldsTemplate(
|
|
|
519
524
|
} else {
|
|
520
525
|
widgetTpl = html`<sp-textfield
|
|
521
526
|
size="s"
|
|
522
|
-
value=${staticVal}
|
|
527
|
+
.value=${live(staticVal)}
|
|
523
528
|
@input=${(/** @type {Event} */ e) => {
|
|
524
529
|
clearTimeout(debounce);
|
|
525
530
|
debounce = setTimeout(
|
|
@@ -612,7 +617,7 @@ function renderMediaFieldsTemplate(/** @type {JxMutableNode} */ node) {
|
|
|
612
617
|
class="field-input"
|
|
613
618
|
style="width:70px;flex:none"
|
|
614
619
|
placeholder="320px"
|
|
615
|
-
value=${media["--"] || ""}
|
|
620
|
+
.value=${live(media["--"] || "")}
|
|
616
621
|
@input=${(/** @type {Event} */ e) => {
|
|
617
622
|
clearTimeout(baseDebounce);
|
|
618
623
|
baseDebounce = setTimeout(() => {
|
|
@@ -716,7 +721,7 @@ function mediaBreakpointRowTemplate(/** @type {string} */ name, /** @type {strin
|
|
|
716
721
|
<div style="display:flex;align-items:center;gap:4px;margin-bottom:2px">
|
|
717
722
|
<input
|
|
718
723
|
class="field-input"
|
|
719
|
-
value=${mediaDisplayName(name)}
|
|
724
|
+
.value=${live(mediaDisplayName(name))}
|
|
720
725
|
style="flex:1;font-weight:600;font-size:12px"
|
|
721
726
|
@input=${(/** @type {Event} */ e) => {
|
|
722
727
|
const newKey = friendlyNameToMedia(/** @type {HTMLInputElement} */ (e.target).value);
|
|
@@ -755,7 +760,7 @@ function mediaBreakpointRowTemplate(/** @type {string} */ name, /** @type {strin
|
|
|
755
760
|
<div style="display:flex;gap:4px;align-items:center">
|
|
756
761
|
<input
|
|
757
762
|
class="field-input bp-query-input"
|
|
758
|
-
value=${query}
|
|
763
|
+
.value=${live(query)}
|
|
759
764
|
style="flex:1"
|
|
760
765
|
@input=${(/** @type {Event} */ e) => {
|
|
761
766
|
clearTimeout(debounceTimer);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { html, render as litRender, nothing } from "lit-html";
|
|
2
2
|
import { classMap } from "lit-html/directives/class-map.js";
|
|
3
3
|
import { live } from "lit-html/directives/live.js";
|
|
4
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
4
5
|
import { getPlatform } from "../platform.js";
|
|
5
6
|
import { openFileInTab } from "../files/files.js";
|
|
6
7
|
import { getRecentFiles, trackRecentFile } from "../recent-projects.js";
|
|
@@ -28,10 +29,6 @@ export function openQuickSearch() {
|
|
|
28
29
|
_results = [];
|
|
29
30
|
_selectedIndex = 0;
|
|
30
31
|
renderOverlay();
|
|
31
|
-
requestAnimationFrame(() => {
|
|
32
|
-
const input = getContainer().querySelector(".quick-search-input");
|
|
33
|
-
if (input) /** @type {HTMLInputElement} */ (input).focus();
|
|
34
|
-
});
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
export function closeQuickSearch() {
|
|
@@ -136,6 +133,9 @@ function renderOverlay() {
|
|
|
136
133
|
.value=${live(_query)}
|
|
137
134
|
@input=${onInput}
|
|
138
135
|
@keydown=${onKeydown}
|
|
136
|
+
${ref((el) => {
|
|
137
|
+
if (el) requestAnimationFrame(() => /** @type {HTMLInputElement} */ (el).focus());
|
|
138
|
+
})}
|
|
139
139
|
/>
|
|
140
140
|
<div class="quick-search-results">
|
|
141
141
|
${items.length === 0 && _query.trim()
|
|
@@ -39,6 +39,16 @@ let _scope = null;
|
|
|
39
39
|
|
|
40
40
|
let _rendering = false;
|
|
41
41
|
let _scheduled = false;
|
|
42
|
+
let _hasFocus = false;
|
|
43
|
+
|
|
44
|
+
function _onFocusIn() {
|
|
45
|
+
_hasFocus = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _onFocusOut() {
|
|
49
|
+
_hasFocus = false;
|
|
50
|
+
render();
|
|
51
|
+
}
|
|
42
52
|
|
|
43
53
|
/**
|
|
44
54
|
* Mount the right panel.
|
|
@@ -49,6 +59,8 @@ export function mount(ctx) {
|
|
|
49
59
|
_ctx = ctx;
|
|
50
60
|
mountAiPanel();
|
|
51
61
|
registerRightPanelRender(render);
|
|
62
|
+
rightPanel.addEventListener("focusin", _onFocusIn);
|
|
63
|
+
rightPanel.addEventListener("focusout", _onFocusOut);
|
|
52
64
|
_scope = effectScope();
|
|
53
65
|
_scope.run(() => {
|
|
54
66
|
effect(() => {
|
|
@@ -66,20 +78,7 @@ export function mount(ctx) {
|
|
|
66
78
|
void tab.session.ui.styleFilterActive;
|
|
67
79
|
void tab.session.ui.inspectorSections;
|
|
68
80
|
|
|
69
|
-
|
|
70
|
-
const activeTag = document.activeElement?.tagName;
|
|
71
|
-
const rightHasFocus =
|
|
72
|
-
!colorPopoverOpen &&
|
|
73
|
-
rightPanel.contains(document.activeElement) &&
|
|
74
|
-
(activeTag === "INPUT" ||
|
|
75
|
-
activeTag === "TEXTAREA" ||
|
|
76
|
-
activeTag === "SP-TEXTFIELD" ||
|
|
77
|
-
activeTag === "SP-NUMBER-FIELD" ||
|
|
78
|
-
activeTag === "SP-PICKER" ||
|
|
79
|
-
activeTag === "SP-COMBOBOX" ||
|
|
80
|
-
activeTag === "SP-SEARCH");
|
|
81
|
-
|
|
82
|
-
if (!rightHasFocus) {
|
|
81
|
+
if (!_hasFocus && !isColorPopoverOpen()) {
|
|
83
82
|
render();
|
|
84
83
|
}
|
|
85
84
|
});
|
|
@@ -90,6 +89,9 @@ export function unmount() {
|
|
|
90
89
|
_scope?.stop();
|
|
91
90
|
_scope = null;
|
|
92
91
|
_ctx = null;
|
|
92
|
+
rightPanel.removeEventListener("focusin", _onFocusIn);
|
|
93
|
+
rightPanel.removeEventListener("focusout", _onFocusOut);
|
|
94
|
+
_hasFocus = false;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
export function render() {
|
|
@@ -8,6 +8,7 @@ import { html, nothing } from "lit-html";
|
|
|
8
8
|
import { classMap } from "lit-html/directives/class-map.js";
|
|
9
9
|
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
10
10
|
import { styleMap } from "lit-html/directives/style-map.js";
|
|
11
|
+
import { projectState } from "../state.js";
|
|
11
12
|
import { activeTab } from "../workspace/workspace.js";
|
|
12
13
|
import {
|
|
13
14
|
transactDoc,
|
|
@@ -59,6 +60,11 @@ import { fetchPluginSchema, pluginSchemaCache } from "../services/code-services.
|
|
|
59
60
|
* parameters?: Record<string, unknown>[];
|
|
60
61
|
* emits?: Record<string, unknown>[];
|
|
61
62
|
* fields?: unknown;
|
|
63
|
+
* contentType?: string;
|
|
64
|
+
* id?: unknown;
|
|
65
|
+
* filter?: Record<string, unknown>;
|
|
66
|
+
* sort?: { field?: string; order?: string };
|
|
67
|
+
* limit?: number;
|
|
62
68
|
* }} SignalDef
|
|
63
69
|
*
|
|
64
70
|
* @typedef {{ name: string; type?: { text?: string }; description?: string; optional?: boolean }} CemParameter
|
|
@@ -730,6 +736,9 @@ function renderDataSourceFields(
|
|
|
730
736
|
)}
|
|
731
737
|
`;
|
|
732
738
|
}
|
|
739
|
+
if (proto === "ContentEntry" || proto === "ContentCollection") {
|
|
740
|
+
return renderContentPrototypeFields(name, def, proto);
|
|
741
|
+
}
|
|
733
742
|
if (proto === "Set" || proto === "Map" || proto === "FormData") {
|
|
734
743
|
const fieldName = proto === "FormData" ? "fields" : "default";
|
|
735
744
|
const fieldLabel = proto === "FormData" ? "Fields" : "Default";
|
|
@@ -751,6 +760,117 @@ function renderDataSourceFields(
|
|
|
751
760
|
return renderExternalPrototypeEditorTemplate(S, name, def, ctx);
|
|
752
761
|
}
|
|
753
762
|
|
|
763
|
+
/** ContentEntry/ContentCollection fields for signal editor */
|
|
764
|
+
function renderContentPrototypeFields(
|
|
765
|
+
/** @type {string} */ name,
|
|
766
|
+
/** @type {SignalDef} */ def,
|
|
767
|
+
/** @type {string} */ proto,
|
|
768
|
+
) {
|
|
769
|
+
const contentTypes = projectState?.projectConfig?.contentTypes;
|
|
770
|
+
const typeNames = contentTypes ? Object.keys(contentTypes) : [];
|
|
771
|
+
const currentType = def.contentType || "";
|
|
772
|
+
const schema = /** @type {ContentTypeSchema | undefined} */ (
|
|
773
|
+
currentType && contentTypes?.[currentType]?.schema
|
|
774
|
+
) ||
|
|
775
|
+
undefined;
|
|
776
|
+
|
|
777
|
+
const contentTypeField = typeNames.length
|
|
778
|
+
? html`
|
|
779
|
+
<div class="signal-field-row">
|
|
780
|
+
<label class="signal-field-label">Content Type</label>
|
|
781
|
+
<sp-picker
|
|
782
|
+
size="s"
|
|
783
|
+
value=${currentType}
|
|
784
|
+
style="width:100%"
|
|
785
|
+
@change=${(/** @type {Event} */ e) => {
|
|
786
|
+
const v = /** @type {HTMLSelectElement} */ (e.target).value;
|
|
787
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { contentType: v }));
|
|
788
|
+
}}
|
|
789
|
+
>
|
|
790
|
+
${typeNames.map((t) => html`<sp-menu-item value=${t}>${t}</sp-menu-item>`)}
|
|
791
|
+
</sp-picker>
|
|
792
|
+
</div>
|
|
793
|
+
`
|
|
794
|
+
: signalFieldRow("Content Type", currentType, (/** @type {string} */ v) =>
|
|
795
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { contentType: v })),
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
/** @type {import("lit-html").TemplateResult | symbol} */
|
|
799
|
+
let entryFields = nothing;
|
|
800
|
+
if (proto === "ContentEntry") {
|
|
801
|
+
const idVal = def.id;
|
|
802
|
+
const idStr =
|
|
803
|
+
idVal && typeof idVal === "object" && /** @type {Record<string, unknown>} */ (idVal).$ref
|
|
804
|
+
? /** @type {string} */ (/** @type {Record<string, unknown>} */ (idVal).$ref)
|
|
805
|
+
: typeof idVal === "string"
|
|
806
|
+
? idVal
|
|
807
|
+
: "";
|
|
808
|
+
entryFields = signalFieldRow("ID", idStr, (/** @type {string} */ v) => {
|
|
809
|
+
const val = v.startsWith("#/") ? { $ref: v } : v;
|
|
810
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { id: val }));
|
|
811
|
+
});
|
|
812
|
+
} else {
|
|
813
|
+
const filterStr = def.filter ? JSON.stringify(def.filter) : "";
|
|
814
|
+
const sortStr = def.sort ? `${def.sort.field || ""} ${def.sort.order || ""}`.trim() : "";
|
|
815
|
+
const limitStr = def.limit != null ? String(def.limit) : "";
|
|
816
|
+
entryFields = html`
|
|
817
|
+
${signalFieldRow("Filter", filterStr, (/** @type {string} */ v) => {
|
|
818
|
+
try {
|
|
819
|
+
const parsed = v ? JSON.parse(v) : undefined;
|
|
820
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { filter: parsed }));
|
|
821
|
+
} catch {}
|
|
822
|
+
})}
|
|
823
|
+
${signalFieldRow("Sort", sortStr, (/** @type {string} */ v) => {
|
|
824
|
+
if (!v) {
|
|
825
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { sort: undefined }));
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const [field, order] = v.split(" ");
|
|
829
|
+
transactDoc(activeTab.value, (t) =>
|
|
830
|
+
mutateUpdateDef(t, name, { sort: { field, order: order || "asc" } }),
|
|
831
|
+
);
|
|
832
|
+
})}
|
|
833
|
+
${signalFieldRow("Limit", limitStr, (/** @type {string} */ v) => {
|
|
834
|
+
const num = v ? parseInt(v, 10) : undefined;
|
|
835
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { limit: num }));
|
|
836
|
+
})}
|
|
837
|
+
`;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** @type {import("lit-html").TemplateResult | symbol} */
|
|
841
|
+
let schemaFields = nothing;
|
|
842
|
+
if (schema?.properties) {
|
|
843
|
+
const fields = Object.entries(schema.properties);
|
|
844
|
+
const required = new Set(schema.required || []);
|
|
845
|
+
schemaFields = html`
|
|
846
|
+
<div style="margin-top:8px;padding:6px 0;border-top:1px solid var(--border-dim)">
|
|
847
|
+
<div style="font-size:11px;color:var(--fg-dim);margin-bottom:4px;font-weight:500">
|
|
848
|
+
Fields (${currentType})
|
|
849
|
+
</div>
|
|
850
|
+
${fields.map(
|
|
851
|
+
([field, fieldDef]) => html`
|
|
852
|
+
<div style="font-size:11px;padding:2px 0;display:flex;gap:4px;align-items:center">
|
|
853
|
+
<code style="color:var(--fg-default)">${field}</code>
|
|
854
|
+
<span style="color:var(--fg-dim)">${/** @type {any} */ (fieldDef).type || ""}</span>
|
|
855
|
+
${required.has(field)
|
|
856
|
+
? html`<span style="color:var(--color-accent,#f36f32);font-size:9px">req</span>`
|
|
857
|
+
: nothing}
|
|
858
|
+
</div>
|
|
859
|
+
`,
|
|
860
|
+
)}
|
|
861
|
+
</div>
|
|
862
|
+
`;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return html`
|
|
866
|
+
${contentTypeField}
|
|
867
|
+
${signalFieldRow("Prototype", proto, (/** @type {string} */ v) =>
|
|
868
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { $prototype: v })),
|
|
869
|
+
)}
|
|
870
|
+
${entryFields} ${schemaFields}
|
|
871
|
+
`;
|
|
872
|
+
}
|
|
873
|
+
|
|
754
874
|
/** Function fields for signal editor */
|
|
755
875
|
function renderFunctionFields(
|
|
756
876
|
/** @type {SignalsPanelState} */ S,
|
package/src/panels/statusbar.js
CHANGED
|
@@ -38,11 +38,14 @@ export function mountStatusbar() {
|
|
|
38
38
|
renderStatusbar();
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
statusbarEl?.addEventListener("click", _onStatusbarClick);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export function unmountStatusbar() {
|
|
44
46
|
_scope?.stop();
|
|
45
47
|
_scope = null;
|
|
48
|
+
statusbarEl?.removeEventListener("click", _onStatusbarClick);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
// ─── Statusbar ───────────────────────────────────────────────────────────────
|
|
@@ -81,7 +84,8 @@ export function renderStatusbar() {
|
|
|
81
84
|
statusbarEl.innerHTML = parts.join(" | ") || "Jx Studio";
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
|
|
87
|
+
/** @param {Event} e */
|
|
88
|
+
function _onStatusbarClick(e) {
|
|
85
89
|
const target = /** @type {HTMLElement} */ (e.target);
|
|
86
90
|
if (!target.classList.contains("sb-path-seg")) return;
|
|
87
91
|
const pathStr = target.dataset.path;
|
|
@@ -93,7 +97,7 @@ statusbarEl?.addEventListener("click", (e) => {
|
|
|
93
97
|
} catch {
|
|
94
98
|
// ignore
|
|
95
99
|
}
|
|
96
|
-
}
|
|
100
|
+
}
|
|
97
101
|
|
|
98
102
|
/**
|
|
99
103
|
* Show a temporary status message.
|
|
@@ -872,7 +872,7 @@ export function _fieldRow(
|
|
|
872
872
|
? html`<sp-textfield
|
|
873
873
|
multiline
|
|
874
874
|
size="s"
|
|
875
|
-
value=${value ?? ""}
|
|
875
|
+
.value=${live(value ?? "")}
|
|
876
876
|
@input=${onInput}
|
|
877
877
|
></sp-textfield>`
|
|
878
878
|
: type === "checkbox"
|
|
@@ -881,7 +881,11 @@ export function _fieldRow(
|
|
|
881
881
|
@change=${(/** @type {Event} */ e) =>
|
|
882
882
|
onChange(/** @type {HTMLInputElement} */ (e.target).checked)}
|
|
883
883
|
></sp-checkbox>`
|
|
884
|
-
: html`<sp-textfield
|
|
884
|
+
: html`<sp-textfield
|
|
885
|
+
size="s"
|
|
886
|
+
.value=${live(value ?? "")}
|
|
887
|
+
@input=${onInput}
|
|
888
|
+
></sp-textfield>`;
|
|
885
889
|
return html`
|
|
886
890
|
<div class="field-row">
|
|
887
891
|
<sp-field-label size="s">${label}</sp-field-label>
|