@jxsuite/studio 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/iframe-entry.js +6230 -0
  2. package/dist/iframe-entry.js.map +35 -0
  3. package/dist/studio.js +10754 -11060
  4. package/dist/studio.js.map +83 -72
  5. package/package.json +7 -7
  6. package/src/browse/browse.ts +11 -4
  7. package/src/canvas/canvas-helpers.ts +2 -56
  8. package/src/canvas/canvas-live-render.ts +102 -435
  9. package/src/canvas/canvas-origin.ts +66 -0
  10. package/src/canvas/canvas-patcher.ts +63 -403
  11. package/src/canvas/canvas-render.ts +70 -212
  12. package/src/canvas/canvas-utils.ts +37 -65
  13. package/src/canvas/iframe-channel.ts +154 -0
  14. package/src/canvas/iframe-drop.ts +484 -0
  15. package/src/canvas/iframe-entry.ts +600 -0
  16. package/src/canvas/iframe-host.ts +1373 -0
  17. package/src/canvas/iframe-inline-edit.ts +367 -0
  18. package/src/canvas/iframe-insert.ts +164 -0
  19. package/src/canvas/iframe-interaction.ts +176 -0
  20. package/src/canvas/iframe-keys.ts +85 -0
  21. package/src/canvas/iframe-overlay.ts +218 -0
  22. package/src/canvas/iframe-patch.ts +363 -0
  23. package/src/canvas/iframe-protocol.ts +361 -0
  24. package/src/canvas/iframe-render.ts +458 -0
  25. package/src/canvas/iframe-slash.ts +114 -0
  26. package/src/canvas/iframe-subtree.ts +113 -0
  27. package/src/canvas/path-mapping.ts +86 -0
  28. package/src/canvas/serialize-scope.ts +65 -0
  29. package/src/editor/canvas-context-menu.ts +40 -0
  30. package/src/editor/canvas-slash-bridge.ts +21 -0
  31. package/src/editor/context-menu.ts +2 -1
  32. package/src/editor/inline-edit-apply.ts +183 -0
  33. package/src/editor/inline-edit.ts +99 -21
  34. package/src/editor/inline-link.ts +89 -0
  35. package/src/editor/insert-zone-action.ts +35 -0
  36. package/src/editor/merge-tags.ts +26 -2
  37. package/src/editor/repeater-scope.ts +144 -0
  38. package/src/editor/shortcuts.ts +14 -28
  39. package/src/editor/slash-menu.ts +73 -42
  40. package/src/files/files.ts +2 -1
  41. package/src/page-params.ts +383 -0
  42. package/src/panels/ai-panel.ts +5 -7
  43. package/src/panels/block-action-bar.ts +296 -138
  44. package/src/panels/canvas-dnd-bridge.ts +397 -0
  45. package/src/panels/component-preview.ts +56 -0
  46. package/src/panels/dnd.ts +41 -17
  47. package/src/panels/drag-ghost.ts +62 -0
  48. package/src/panels/editors.ts +1 -1
  49. package/src/panels/overlays.ts +10 -125
  50. package/src/panels/properties-panel.ts +210 -0
  51. package/src/panels/right-panel.ts +0 -2
  52. package/src/panels/signals-panel.ts +136 -22
  53. package/src/panels/stylebook-doc.ts +373 -0
  54. package/src/panels/stylebook-panel.ts +46 -689
  55. package/src/panels/tab-bar.ts +159 -13
  56. package/src/panels/toolbar.ts +3 -2
  57. package/src/platforms/devserver.ts +15 -0
  58. package/src/services/monaco-setup.ts +12 -0
  59. package/src/services/render-critic.ts +9 -9
  60. package/src/settings/css-vars-editor.ts +2 -2
  61. package/src/store.ts +4 -62
  62. package/src/studio.ts +90 -40
  63. package/src/tabs/doc-op-apply.ts +89 -0
  64. package/src/tabs/patch-ops.ts +6 -2
  65. package/src/tabs/tab.ts +23 -4
  66. package/src/tabs/transact.ts +2 -74
  67. package/src/types.ts +14 -18
  68. package/src/ui/jx-theme.ts +63 -0
  69. package/src/ui/media-picker.ts +6 -4
  70. package/src/ui/spectrum.ts +5 -0
  71. package/src/utils/canvas-media.ts +0 -137
  72. package/src/utils/edit-display.ts +23 -3
  73. package/src/utils/geometry.ts +43 -0
  74. package/src/utils/link-target.ts +93 -0
  75. package/src/utils/strip-events.ts +54 -0
  76. package/src/view.ts +0 -23
  77. package/src/workspace/workspace.ts +14 -1
  78. package/src/canvas/canvas-subtree-render.ts +0 -113
  79. package/src/editor/component-inline-edit.ts +0 -349
  80. package/src/editor/content-inline-edit.ts +0 -207
  81. package/src/editor/insertion-helper.ts +0 -308
  82. package/src/panels/canvas-dnd.ts +0 -329
  83. package/src/panels/panel-events.ts +0 -306
  84. package/src/panels/preview-render.ts +0 -132
  85. package/src/panels/pseudo-preview.ts +0 -75
@@ -0,0 +1,62 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Cross-frame drag GHOST (Phase 4c, commit 5). A single parent-realm `position:fixed` layer that
4
+ * follows the raw pointer 1:1 during a canvas drag. It lives OUTSIDE the scaled `panzoom-wrap`
5
+ * (D-3) — a sibling of `#canvas-wrap` on `<body>` — so it tracks `clientX`/`clientY` directly and
6
+ * is NEVER subject to the canvas zoom transform (drawing it in the scaled overlay would shrink/grow
7
+ * it with zoom). pragmatic's native drag image is suppressed per source via
8
+ * `disableNativeDragPreview`, so this is the only drag affordance the user sees.
9
+ *
10
+ * One ghost is reused across drags + panels: {@link setDragGhost} shows it with a label and moves
11
+ * it to a cursor; {@link clearDragGhost} hides it. The element is created lazily on first show so
12
+ * the module has no import-time DOM side effect (and stays inert in tests until used).
13
+ */
14
+
15
+ let ghostEl: HTMLElement | null = null;
16
+
17
+ /** Lazily create (once) the fixed ghost element appended to `<body>`. */
18
+ function ensureGhost(doc: Document): HTMLElement {
19
+ if (ghostEl?.isConnected) {
20
+ return ghostEl;
21
+ }
22
+ const el = doc.createElement("div");
23
+ el.className = "jx-drag-ghost";
24
+ // Fixed + non-interactive + above the canvas; offset slightly from the cursor so it doesn't sit
25
+ // Under the pointer and block hit-testing visuals. Positioned via left/top on each move.
26
+ el.style.cssText =
27
+ "position:fixed;left:0;top:0;z-index:9999;pointer-events:none;display:none;" +
28
+ "padding:2px 8px;border-radius:4px;font-size:12px;line-height:1.4;white-space:nowrap;" +
29
+ "background:var(--accent,#3b82f6);color:#fff;box-shadow:0 2px 8px rgba(0,0,0,0.25);" +
30
+ "transform:translate(8px,8px)";
31
+ doc.body.append(el);
32
+ ghostEl = el;
33
+ return el;
34
+ }
35
+
36
+ /**
37
+ * Show the ghost with `label`, positioned at the raw pointer (`x`,`y` in parent-viewport px). Safe
38
+ * to call every pointermove; reuses the one element.
39
+ */
40
+ export function setDragGhost(label: string, x: number, y: number, doc: Document = document): void {
41
+ const el = ensureGhost(doc);
42
+ el.textContent = label;
43
+ el.style.left = `${x}px`;
44
+ el.style.top = `${y}px`;
45
+ el.style.display = "block";
46
+ }
47
+
48
+ /** Move the ghost to a new pointer position without changing its label (no-op when hidden). */
49
+ export function moveDragGhost(x: number, y: number): void {
50
+ if (!ghostEl || ghostEl.style.display === "none") {
51
+ return;
52
+ }
53
+ ghostEl.style.left = `${x}px`;
54
+ ghostEl.style.top = `${y}px`;
55
+ }
56
+
57
+ /** Hide the ghost (between drags). The element is retained for reuse. */
58
+ export function clearDragGhost(): void {
59
+ if (ghostEl) {
60
+ ghostEl.style.display = "none";
61
+ }
62
+ }
@@ -102,7 +102,7 @@ export function renderFunctionEditor() {
102
102
 
103
103
  view.functionEditor = monaco.editor.create(editorContainer as unknown as HTMLElement, {
104
104
  automaticLayout: true,
105
- fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
105
+ fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace",
106
106
  fontSize: 12,
107
107
  language: "javascript",
108
108
  lineNumbers: "on",
@@ -1,29 +1,15 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Overlays panel — renders hover/selection overlay boxes on canvas panels. Delegates block action
4
- * bar rendering to studio.js via ctx callback.
3
+ * Overlays panel — the iframe canvas owns hit-testing and draws its own hover/selection boxes (from
4
+ * posted rects, inside each host's overlay layer), so this panel only keeps panel-header
5
+ * highlighting in sync and delegates the block-action-bar render on tracked session changes.
5
6
  */
6
7
 
7
- import { html, render as litRender, nothing } from "lit-html";
8
- import { styleMap } from "lit-html/directives/style-map.js";
9
- import { canvasPanels, pathsEqual } from "../store";
10
8
  import { effect, effectScope } from "../reactivity";
11
9
  import { activeTab } from "../workspace/workspace";
12
- import { view } from "../view";
13
- import { effectiveZoom, findCanvasElement, getActivePanel } from "../canvas/canvas-helpers";
14
- import { layoutElements } from "../canvas/canvas-live-render";
10
+ import { updateActivePanelHeaders } from "../canvas/canvas-utils";
15
11
  import type { EffectScope } from "@vue/reactivity";
16
12
 
17
- interface OverlayBox {
18
- cls: string;
19
- top: string;
20
- left: string;
21
- width: string;
22
- height: string;
23
- border?: string;
24
- isLayout?: boolean;
25
- }
26
-
27
13
  interface OverlaysCtx {
28
14
  getCanvasMode: () => string;
29
15
  isEditing: () => boolean;
@@ -50,10 +36,12 @@ export function mount(ctx: OverlaysCtx) {
50
36
  if (!tab) {
51
37
  return;
52
38
  }
53
- // Track selection, hover, and mode
39
+ // Track selection, hover, mode, and the active panel (a hit in another breakpoint panel — or
40
+ // A header click — re-anchors the block action bar even when the selection path is unchanged).
54
41
  void tab.session.selection;
55
42
  void tab.session.hover;
56
43
  void tab.doc.mode;
44
+ void tab.session.ui.activeMedia;
57
45
  render();
58
46
  });
59
47
  });
@@ -84,113 +72,10 @@ function _flush() {
84
72
  if (!tab) {
85
73
  return;
86
74
  }
87
- const { selection, hover } = tab.session;
88
- const { stylebookTab } = tab.session.ui;
89
- const canvasMode = _ctx.getCanvasMode();
90
-
91
- if (canvasMode !== "design" && canvasMode !== "edit" && canvasMode !== "stylebook") {
92
- for (const p of canvasPanels) {
93
- litRender(nothing, p.overlay);
94
- p.overlayClk.style.pointerEvents = "none";
95
- }
96
- if (view.selDragCleanup) {
97
- view.selDragCleanup();
98
- view.selDragCleanup = null;
99
- }
100
- return;
101
- }
102
-
103
- if (canvasMode === "stylebook") {
104
- const enable = stylebookTab === "elements";
105
- for (const p of canvasPanels) {
106
- p.overlayClk.style.pointerEvents = enable ? "" : "none";
107
- }
108
- return;
109
- }
110
-
111
- for (const p of canvasPanels) {
112
- p.overlayClk.style.pointerEvents = view.componentInlineEdit || _ctx.isEditing() ? "none" : "";
113
- }
114
75
 
115
- if (view.selDragCleanup) {
116
- view.selDragCleanup();
117
- view.selDragCleanup = null;
118
- }
119
-
120
- for (const p of canvasPanels) {
121
- const boxes: OverlayBox[] = [];
122
-
123
- // Batch layout reads: read viewport geometry once per panel
124
- if (!p.viewport) {
125
- continue;
126
- }
127
- const vpRect = p.viewport.getBoundingClientRect();
128
- const { scrollTop } = p.viewport;
129
- const { scrollLeft } = p.viewport;
130
- const scale = effectiveZoom();
131
-
132
- if (hover && !pathsEqual(hover, selection)) {
133
- const el = findCanvasElement(hover, p.canvas);
134
- if (el) {
135
- const elRect = el.getBoundingClientRect();
136
- const desc: OverlayBox = {
137
- cls: "overlay-box overlay-hover",
138
- height: `${elRect.height / scale}px`,
139
- left: `${(elRect.left - vpRect.left + scrollLeft) / scale}px`,
140
- top: `${(elRect.top - vpRect.top + scrollTop) / scale}px`,
141
- width: `${elRect.width / scale}px`,
142
- };
143
- if (layoutElements.has(el)) {
144
- desc.isLayout = true;
145
- }
146
- boxes.push(desc);
147
- }
148
- }
149
-
150
- if (selection && p === getActivePanel()) {
151
- const el = findCanvasElement(selection, p.canvas);
152
- if (el) {
153
- const elRect = el.getBoundingClientRect();
154
- const desc: OverlayBox = {
155
- cls: "overlay-box overlay-selection",
156
- height: `${elRect.height / scale}px`,
157
- left: `${(elRect.left - vpRect.left + scrollLeft) / scale}px`,
158
- top: `${(elRect.top - vpRect.top + scrollTop) / scale}px`,
159
- width: `${elRect.width / scale}px`,
160
- };
161
- if (view.componentInlineEdit || _ctx.isEditing()) {
162
- desc.border = "none";
163
- }
164
- if (layoutElements.has(el)) {
165
- desc.isLayout = true;
166
- }
167
- boxes.push(desc);
168
- }
169
- }
170
-
171
- litRender(
172
- html`
173
- ${p.dropLine}
174
- ${boxes.map(
175
- (b) => html`
176
- <div
177
- class="${b.cls}${b.isLayout ? " overlay-layout" : ""}"
178
- style=${styleMap({
179
- border: b.border,
180
- height: b.height,
181
- left: b.left,
182
- top: b.top,
183
- width: b.width,
184
- })}
185
- >
186
- ${b.isLayout ? html`<span class="overlay-layout-badge">Layout</span>` : nothing}
187
- </div>
188
- `,
189
- )}
190
- `,
191
- p.overlay,
192
- );
193
- }
76
+ // Header highlighting follows hit-driven activation immediately (it otherwise only refreshes on
77
+ // Full canvas renders).
78
+ updateActivePanelHeaders();
194
79
 
195
80
  _ctx.renderBlockActionBar();
196
81
  }
@@ -29,6 +29,8 @@ import {
29
29
  inferInputType,
30
30
  parseCemType,
31
31
  } from "../utils/studio-utils";
32
+ import { classifyHref, composeHref } from "../utils/link-target";
33
+ import type { LinkKind } from "../utils/link-target";
32
34
  import { collectCssParts, isCustomElementDoc } from "./signals-panel";
33
35
  import { mediaDisplayName } from "./shared";
34
36
  import { getCssInitialMap } from "./style-utils";
@@ -800,6 +802,202 @@ function isPageDocument(documentPath: string | undefined | null) {
800
802
  return documentPath.startsWith("pages/") || documentPath.startsWith("./pages/");
801
803
  }
802
804
 
805
+ // ─── Page-route enumeration (for the Link-target Internal picker) ─────────────
806
+
807
+ /** @type {string[] | null} — cached list of internal routes derived from the pages/ tree. */
808
+ let pageRouteEntries: string[] | null = null;
809
+
810
+ /**
811
+ * Derive a site route from a page file path relative to `pages/`, following the file-based routing
812
+ * convention: `index.json` → the directory route, `[slug].json` → `:slug`, all others drop their
813
+ * extension. Directory routes get a trailing slash (`/about/`); the root is `/`.
814
+ *
815
+ * @param {string} relPath — path relative to `pages/`, forward-slashed (e.g. "blog/[slug].json").
816
+ * @returns {string}
817
+ */
818
+ function routeForPagePath(relPath: string): string {
819
+ const withoutExt = relPath.replace(/\.[^./]+$/, "");
820
+ const segments = withoutExt
821
+ .split("/")
822
+ .map((seg) => (seg.startsWith("[") ? `:${seg.slice(1, -1)}` : seg));
823
+ const isIndex = segments.at(-1) === "index";
824
+ if (isIndex) {
825
+ segments.pop();
826
+ }
827
+ const body = segments.join("/");
828
+ if (!body) {
829
+ return "/";
830
+ }
831
+ // Dynamic routes keep no trailing slash; static routes are directory-style (trailing slash).
832
+ return isIndex || body.includes(":") ? `/${body}${isIndex ? "/" : ""}` : `/${body}/`;
833
+ }
834
+
835
+ /** Recursively walk the pages/ tree and populate {@link pageRouteEntries} with derived routes. */
836
+ async function loadPageRouteEntries() {
837
+ const platform = getPlatform();
838
+ const routes: string[] = [];
839
+ const docExts = new Set([".json", ".md", ".html"]);
840
+ async function walk(dir: string, rel: string) {
841
+ let listing: DirEntry[];
842
+ try {
843
+ listing = await platform.listDirectory(dir);
844
+ } catch {
845
+ return;
846
+ }
847
+ for (const entry of listing) {
848
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
849
+ if (entry.type === "directory") {
850
+ await walk(entry.path ?? `${dir}/${entry.name}`, childRel);
851
+ } else if (docExts.has(entry.name.slice(entry.name.lastIndexOf(".")))) {
852
+ routes.push(routeForPagePath(childRel));
853
+ }
854
+ }
855
+ }
856
+ await walk("pages", "");
857
+ pageRouteEntries = [...new Set(routes)].toSorted((a, b) => a.localeCompare(b));
858
+ renderOnly("rightPanel");
859
+ }
860
+
861
+ export function invalidatePageRouteCache() {
862
+ pageRouteEntries = null;
863
+ }
864
+
865
+ /**
866
+ * Composite Link-target control for an anchor's `href` — a kind selector (Internal / External /
867
+ * Anchor / Email / Phone) plus the matching input, backed by classifyHref/composeHref so edits
868
+ * round-trip. Internal targets render an sp-picker of page routes enumerated from the pages/ tree.
869
+ *
870
+ * @param {JxMutableNode} node
871
+ * @param {JxPath} path
872
+ */
873
+ function renderLinkTargetField(node: JxMutableNode, path: JxPath) {
874
+ const raw = typeof node.attributes?.href === "string" ? node.attributes.href : "";
875
+ const { kind, value } = classifyHref(raw);
876
+
877
+ const commit = (nextKind: LinkKind, nextValue: string) => {
878
+ const composed = composeHref(nextKind, nextValue);
879
+ transactDoc(activeTab.value!, (t) =>
880
+ mutateUpdateAttribute(t, path, "href", composed || undefined),
881
+ );
882
+ };
883
+
884
+ const kindOptions: { value: LinkKind; label: string }[] = [
885
+ { label: "Internal Page", value: "internal" },
886
+ { label: "External URL", value: "external" },
887
+ { label: "Anchor", value: "anchor" },
888
+ { label: "Email", value: "mailto" },
889
+ { label: "Phone", value: "tel" },
890
+ ];
891
+
892
+ const kindSelector = html`
893
+ <sp-picker
894
+ class="link-target-kind"
895
+ size="s"
896
+ value=${kind}
897
+ @change=${(e: Event) => {
898
+ const nextKind = (e.target as HTMLInputElement).value as LinkKind;
899
+ // Switching kind reinterprets the current value under the new kind.
900
+ commit(nextKind, value);
901
+ }}
902
+ >
903
+ ${kindOptions.map((o) => html`<sp-menu-item value=${o.value}>${o.label}</sp-menu-item>`)}
904
+ </sp-picker>
905
+ `;
906
+
907
+ let valueInput;
908
+ if (kind === "internal") {
909
+ if (pageRouteEntries === null) {
910
+ void loadPageRouteEntries();
911
+ }
912
+ const routes = pageRouteEntries ?? [];
913
+ const knownValue = value !== "" && !routes.includes(value);
914
+ valueInput = html`
915
+ <sp-picker
916
+ class="link-target-value"
917
+ size="s"
918
+ value=${value}
919
+ @change=${(e: Event) => commit("internal", (e.target as HTMLInputElement).value)}
920
+ >
921
+ ${knownValue ? html`<sp-menu-item value=${value}>${value}</sp-menu-item>` : nothing}
922
+ ${routes.map((r) => html`<sp-menu-item value=${r}>${r}</sp-menu-item>`)}
923
+ </sp-picker>
924
+ `;
925
+ } else {
926
+ const placeholder =
927
+ kind === "mailto"
928
+ ? "name@example.com"
929
+ : kind === "tel"
930
+ ? "+15551234567"
931
+ : kind === "anchor"
932
+ ? "section-id"
933
+ : "https://example.com";
934
+ valueInput = html`
935
+ <sp-textfield
936
+ class="link-target-value"
937
+ size="s"
938
+ placeholder=${placeholder}
939
+ .value=${live(value)}
940
+ @input=${debouncedStyleCommit("link:href", 400, (e: Event) =>
941
+ commit(kind, (e.target as HTMLInputElement).value),
942
+ )}
943
+ ></sp-textfield>
944
+ `;
945
+ }
946
+
947
+ return renderFieldRow({
948
+ hasValue: raw !== "",
949
+ label: "Link",
950
+ onClear: () => transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, "href")),
951
+ prop: "href",
952
+ widget: html`<div class="link-target-field">${kindSelector}${valueInput}</div>`,
953
+ });
954
+ }
955
+
956
+ /**
957
+ * Real enum picker (sp-picker) for the anchor `target` attribute, replacing the generic
958
+ * jx-value-selector so the four browsing-context keywords are offered as a dropdown.
959
+ *
960
+ * @param {JxMutableNode} node
961
+ * @param {JxPath} path
962
+ * @param {HtmlMetaEntry} entry
963
+ */
964
+ function renderTargetField(node: JxMutableNode, path: JxPath, entry: HtmlMetaEntry) {
965
+ const options = Array.isArray(entry.enum) ? (entry.enum as string[]) : [];
966
+ const current = typeof node.attributes?.target === "string" ? node.attributes.target : "";
967
+ return renderFieldRow({
968
+ hasValue: current !== "",
969
+ label: attrLabel(entry, "target"),
970
+ onClear: () => transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, "target")),
971
+ prop: "target",
972
+ widget: html`
973
+ <sp-picker
974
+ class="link-target-window"
975
+ size="s"
976
+ value=${current}
977
+ @change=${(e: Event) =>
978
+ transactDoc(activeTab.value!, (t) =>
979
+ mutateUpdateAttribute(
980
+ t,
981
+ path,
982
+ "target",
983
+ (e.target as HTMLInputElement).value || undefined,
984
+ ),
985
+ )}
986
+ >
987
+ ${options.map((o) => html`<sp-menu-item value=${o}>${o}</sp-menu-item>`)}
988
+ </sp-picker>
989
+ `,
990
+ });
991
+ }
992
+
993
+ /**
994
+ * True when an attribute value is a binding (a `$ref` object or a template string containing
995
+ * `${…}`), so the Link-target special-case must fall back to the raw widget to keep it editable.
996
+ */
997
+ function isBoundAttrValue(value: unknown): boolean {
998
+ return isRef(value) || (typeof value === "string" && value.includes("${"));
999
+ }
1000
+
803
1001
  function renderPageSection(node: JxMutableNode) {
804
1002
  const tab = activeTab.value;
805
1003
  if (!isPageDocument(tab!.documentPath)) {
@@ -968,6 +1166,18 @@ export function renderPropertiesPanelTemplate(ctx: {
968
1166
  const type = inferInputType(entry);
969
1167
  const hasVal = value !== undefined && value !== "";
970
1168
 
1169
+ // Enhanced Link handling: only for anchors (a/area) with a plain (non-binding) value. Bindings
1170
+ // ($ref objects or ${…} template strings) fall through to the raw widget to stay editable.
1171
+ const isAnchor = tagName === "a" || tagName === "area";
1172
+ if (isAnchor && !isBoundAttrValue(value)) {
1173
+ if (attr === "href") {
1174
+ return renderLinkTargetField(node, path);
1175
+ }
1176
+ if (attr === "target") {
1177
+ return renderTargetField(node, path, entry);
1178
+ }
1179
+ }
1180
+
971
1181
  if (entry.type === "boolean") {
972
1182
  return renderFieldRow({
973
1183
  hasValue: hasVal,
@@ -31,7 +31,6 @@ interface RightPanelCtx {
31
31
  navigateToComponent: (path: string) => void;
32
32
  getCanvasMode: () => string;
33
33
  renderCanvas: () => void;
34
- updateForcedPseudoPreview: () => void;
35
34
  }
36
35
 
37
36
  let _ctx: RightPanelCtx | null = null;
@@ -223,5 +222,4 @@ function _doRender() {
223
222
  console.error("right-panel render error:", error);
224
223
  }
225
224
  requestAnimationFrame(() => mountQuikChat());
226
- _ctx.updateForcedPseudoPreview();
227
225
  }