@jxsuite/studio 0.28.4 → 0.29.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 (81) hide show
  1. package/dist/studio.js +6769 -5399
  2. package/dist/studio.js.map +86 -79
  3. package/package.json +5 -4
  4. package/src/browse/browse-modal.ts +2 -2
  5. package/src/browse/browse.ts +20 -19
  6. package/src/canvas/canvas-diff.ts +3 -3
  7. package/src/canvas/canvas-helpers.ts +15 -2
  8. package/src/canvas/canvas-live-render.ts +168 -86
  9. package/src/canvas/canvas-patcher.ts +656 -0
  10. package/src/canvas/canvas-perf.ts +68 -0
  11. package/src/canvas/canvas-render.ts +135 -23
  12. package/src/canvas/canvas-subtree-render.ts +113 -0
  13. package/src/canvas/canvas-utils.ts +4 -0
  14. package/src/editor/component-inline-edit.ts +4 -35
  15. package/src/editor/context-menu.ts +92 -74
  16. package/src/editor/convert-to-component.ts +11 -2
  17. package/src/editor/convert-to-repeater.ts +4 -5
  18. package/src/editor/inline-edit.ts +9 -9
  19. package/src/editor/inline-format.ts +11 -11
  20. package/src/editor/merge-tags.ts +120 -0
  21. package/src/editor/shortcuts.ts +4 -4
  22. package/src/files/components.ts +37 -0
  23. package/src/files/file-ops.ts +28 -7
  24. package/src/files/files.ts +25 -21
  25. package/src/github/github-auth.ts +14 -2
  26. package/src/github/github-publish.ts +12 -2
  27. package/src/panels/activity-bar.ts +1 -1
  28. package/src/panels/ai-panel.ts +67 -39
  29. package/src/panels/block-action-bar.ts +72 -1
  30. package/src/panels/canvas-dnd.ts +55 -26
  31. package/src/panels/data-explorer.ts +5 -3
  32. package/src/panels/dnd.ts +12 -15
  33. package/src/panels/editors.ts +5 -23
  34. package/src/panels/elements-panel.ts +2 -12
  35. package/src/panels/events-panel.ts +1 -1
  36. package/src/panels/git-panel.ts +6 -6
  37. package/src/panels/head-panel.ts +1 -1
  38. package/src/panels/layers-panel.ts +177 -147
  39. package/src/panels/preview-render.ts +15 -0
  40. package/src/panels/properties-panel.ts +16 -27
  41. package/src/panels/quick-search.ts +2 -2
  42. package/src/panels/right-panel.ts +2 -6
  43. package/src/panels/shared.ts +3 -0
  44. package/src/panels/signals-panel.ts +24 -22
  45. package/src/panels/statusbar.ts +15 -6
  46. package/src/panels/style-panel.ts +3 -3
  47. package/src/panels/style-utils.ts +3 -3
  48. package/src/panels/stylebook-panel.ts +4 -4
  49. package/src/panels/tab-bar.ts +198 -0
  50. package/src/panels/tab-strip.ts +2 -2
  51. package/src/panels/toolbar.ts +6 -75
  52. package/src/platforms/devserver.ts +60 -34
  53. package/src/reactivity.ts +2 -0
  54. package/src/services/cem-export.ts +23 -2
  55. package/src/services/code-services.ts +16 -2
  56. package/src/services/monaco-setup.ts +2 -1
  57. package/src/settings/content-types-editor.ts +16 -16
  58. package/src/settings/css-vars-editor.ts +2 -2
  59. package/src/settings/defs-editor.ts +14 -14
  60. package/src/settings/general-settings.ts +6 -6
  61. package/src/settings/head-editor.ts +2 -2
  62. package/src/site-context.ts +1 -1
  63. package/src/state.ts +66 -12
  64. package/src/store.ts +15 -3
  65. package/src/studio.ts +73 -33
  66. package/src/tabs/patch-ops.ts +143 -0
  67. package/src/tabs/tab.ts +15 -1
  68. package/src/tabs/transact.ts +454 -27
  69. package/src/types.ts +41 -0
  70. package/src/ui/color-selector.ts +7 -7
  71. package/src/ui/icons.ts +0 -30
  72. package/src/ui/media-picker.ts +2 -2
  73. package/src/ui/panel-resize.ts +6 -1
  74. package/src/ui/unit-selector.ts +2 -2
  75. package/src/ui/value-selector.ts +3 -3
  76. package/src/utils/canvas-media.ts +6 -6
  77. package/src/utils/edit-display.ts +125 -83
  78. package/src/utils/google-fonts.ts +1 -1
  79. package/src/utils/inherited-style.ts +3 -2
  80. package/src/utils/studio-utils.ts +1 -1
  81. package/src/view.ts +4 -1
package/src/types.ts CHANGED
@@ -35,11 +35,18 @@ export interface GitLogEntry {
35
35
  date: string;
36
36
  }
37
37
 
38
+ export interface ComponentSlotMeta {
39
+ name: string;
40
+ description?: string;
41
+ fallback?: (JxMutableNode | string)[];
42
+ }
43
+
38
44
  export interface ComponentMeta {
39
45
  tagName: string;
40
46
  $id?: string | null;
41
47
  path: string;
42
48
  props?: { name: string; type?: string; default?: JsonValue; [k: string]: unknown }[];
49
+ slots?: ComponentSlotMeta[];
43
50
  hasElements?: boolean;
44
51
  }
45
52
 
@@ -132,6 +139,18 @@ export interface StudioPlatform {
132
139
  aiStreamUrl: (id: string) => string | Promise<string>;
133
140
  aiStopSession: (id: string) => Promise<void>;
134
141
  aiDeleteSession: (id: string) => Promise<void>;
142
+ // ─── Multi-window (desktop only; undefined on dev-server) ───────────────────
143
+ /** Open a project in a new window, focusing an existing window if it is already open. */
144
+ openProjectInNewWindow?: (root: string) => Promise<void>;
145
+ /** Open a fresh welcome window. */
146
+ newWindow?: () => Promise<void>;
147
+ /**
148
+ * Point THIS window's backend at a project and return its config. If the project is already open
149
+ * in another window, that window is focused and `deduped` is true (no project is loaded here).
150
+ */
151
+ setWindowProject?: (root: string) => Promise<{ deduped: boolean; config: ProjectConfig | null }>;
152
+ /** The project root this window's backend is currently bound to. */
153
+ getProjectRoot?: () => Promise<{ root: string | null }>;
135
154
  }
136
155
 
137
156
  // ─── Studio Types ───────────────────────────────────────────────────────────
@@ -146,6 +165,28 @@ export interface CanvasPanel {
146
165
  scrollContainer: HTMLElement;
147
166
  dropLine: HTMLElement;
148
167
  _width: number | null;
168
+ /** True when the panel's DOM reflects the current document via a successful live render. */
169
+ ready: boolean;
170
+ /** Breakpoints active for this panel's width (persisted for surgical patch re-application). */
171
+ activeBreakpoints: Set<string> | null;
172
+ /** Render context captured from the last successful live render (null until then). */
173
+ liveCtx: PanelLiveCtx | null;
174
+ /**
175
+ * Effect scope owning the reactive effects created while rendering this panel's content
176
+ * (including child scopes from surgical subtree renders). Stopped when panels are rebuilt.
177
+ */
178
+ renderScope: { stop: () => void; run: <T>(fn: () => T) => T | undefined } | null;
179
+ }
180
+
181
+ /** Per-panel context persisted by a successful live render so patches can re-render subtrees. */
182
+ export interface PanelLiveCtx {
183
+ scope: Record<string, unknown>;
184
+ canvasMode: string;
185
+ layoutWrapped: boolean;
186
+ pageContentPrefix: (string | number)[] | null;
187
+ pageContentOffset: number;
188
+ arrayPaths: Set<string>;
189
+ pathMapper: (created: Node, path: (string | number)[], def: unknown) => void;
149
190
  }
150
191
 
151
192
  export interface DocumentStackEntry {
@@ -58,7 +58,7 @@ function resolveColorForDisplay(val: string | number | undefined) {
58
58
  const m = s.match(/^var\((--[^)]+)\)$/);
59
59
  if (m) {
60
60
  const style = getEffectiveStyle(activeTab.value?.doc.document?.style);
61
- const resolved = style?.[m[1]];
61
+ const resolved = style?.[m[1]!];
62
62
  if (typeof resolved === "string") {
63
63
  return resolved;
64
64
  }
@@ -108,7 +108,7 @@ function matchesColorVar(
108
108
  // ─── JxColorPopover LitElement ──────────────────────────────────────────────
109
109
 
110
110
  export class JxColorPopover extends LitElement {
111
- static properties = {
111
+ static override properties = {
112
112
  color: { type: String },
113
113
  colorVars: { attribute: false },
114
114
  displayColor: { attribute: false, type: String },
@@ -126,12 +126,12 @@ export class JxColorPopover extends LitElement {
126
126
  }
127
127
 
128
128
  /** No shadow DOM — render directly into light DOM for Spectrum theming */
129
- createRenderRoot() {
129
+ override createRenderRoot() {
130
130
  return this;
131
131
  }
132
132
 
133
133
  /** @param {Map<string, unknown>} changed */
134
- willUpdate(changed: Map<string, unknown>) {
134
+ override willUpdate(changed: Map<string, unknown>) {
135
135
  if (changed.has("color")) {
136
136
  const raw = resolveColorForDisplay(this.color);
137
137
  if (!raw || raw === "transparent") {
@@ -175,7 +175,7 @@ export class JxColorPopover extends LitElement {
175
175
  this.dispatchEvent(new CustomEvent("color-change", { bubbles: true, detail: varRef }));
176
176
  }
177
177
 
178
- render() {
178
+ override render() {
179
179
  return html`
180
180
  <div class="color-popover-inner">
181
181
  <sp-color-area
@@ -258,7 +258,7 @@ export function renderColorSelector(
258
258
  <jx-color-popover
259
259
  .color=${value || ""}
260
260
  .colorVars=${colorVars}
261
- @color-change=${(e: CustomEvent) => onChange(e.detail)}
261
+ @color-change=${(e: CustomEvent) => onChange(e.detail as string)}
262
262
  ></jx-color-popover>
263
263
  </sp-popover>
264
264
  </sp-overlay>
@@ -314,7 +314,7 @@ export function renderColorSelector(
314
314
  <jx-color-popover
315
315
  .color=${value || ""}
316
316
  .colorVars=${colorVars}
317
- @color-change=${(e: CustomEvent) => onChange(e.detail)}
317
+ @color-change=${(e: CustomEvent) => onChange(e.detail as string)}
318
318
  ></jx-color-popover>
319
319
  </sp-popover>
320
320
  </sp-overlay>
package/src/ui/icons.ts CHANGED
@@ -3,36 +3,6 @@
3
3
  // Uses Spectrum workflow icons where available; custom SVGs for flex-specific concepts.
4
4
 
5
5
  import { html } from "lit";
6
- import type { TemplateResult } from "lit";
7
-
8
- // Helper for custom filled-rect icons (alignment/justify diagrams) where no Spectrum match exists
9
- const _R = (d: TemplateResult) =>
10
- html`<svg
11
- xmlns="http://www.w3.org/2000/svg"
12
- width="16"
13
- height="16"
14
- viewBox="0 0 24 24"
15
- fill="currentColor"
16
- stroke="none"
17
- >
18
- ${d}
19
- </svg>`;
20
-
21
- // Helper for custom stroke icons
22
- const _S = (d: TemplateResult) =>
23
- html`<svg
24
- xmlns="http://www.w3.org/2000/svg"
25
- width="16"
26
- height="16"
27
- viewBox="0 0 24 24"
28
- fill="none"
29
- stroke="currentColor"
30
- stroke-width="2"
31
- stroke-linecap="round"
32
- stroke-linejoin="round"
33
- >
34
- ${d}
35
- </svg>`;
36
6
 
37
7
  export const icons = {
38
8
  // ─── Arrows — flexDirection ───
@@ -290,7 +290,7 @@ function showMediaPickerPopover(anchorEl: HTMLElement, onCommit: (val: string) =
290
290
  */
291
291
  export function renderMediaPicker(prop: string, value: string, onCommit: (val: string) => void) {
292
292
  // Kick off async load (won't block render)
293
- loadMediaCache();
293
+ void loadMediaCache();
294
294
 
295
295
  const currentValue = value || "";
296
296
  const isImage = IMAGE_EXTENSIONS.has(
@@ -318,7 +318,7 @@ export function renderMediaPicker(prop: string, value: string, onCommit: (val: s
318
318
  quiet
319
319
  title="Browse media"
320
320
  @click=${(e: MouseEvent) => {
321
- loadMediaCache();
321
+ void loadMediaCache();
322
322
  showMediaPickerPopover(e.currentTarget as HTMLElement, onCommit);
323
323
  }}
324
324
  >
@@ -19,7 +19,12 @@ const root = document.documentElement;
19
19
  // ─── Restore saved widths & collapse state ──────────────────────────────────
20
20
 
21
21
  try {
22
- const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
22
+ const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}") as {
23
+ left?: number;
24
+ right?: number;
25
+ leftCollapsed?: boolean;
26
+ rightCollapsed?: boolean;
27
+ };
23
28
  if (saved.left) {
24
29
  root.style.setProperty("--panel-w-left", `${saved.left}px`);
25
30
  }
@@ -42,7 +42,7 @@ export function renderUnitSelector(
42
42
  if (isKeyword) {
43
43
  displayValue = strVal;
44
44
  } else if (match) {
45
- [, displayValue] = match;
45
+ displayValue = match[1]!;
46
46
  } else if (strVal !== "") {
47
47
  const num = Number.parseFloat(strVal);
48
48
  displayValue = Number.isNaN(num) ? strVal : String(num);
@@ -52,7 +52,7 @@ export function renderUnitSelector(
52
52
 
53
53
  // Parse placeholder so inherited values display as "500" not "500px"
54
54
  const placeholderMatch = placeholder.match(UNIT_RE);
55
- const numericPlaceholder = placeholderMatch ? placeholderMatch[1] : placeholder || "0";
55
+ const numericPlaceholder = placeholderMatch ? placeholderMatch[1]! : placeholder || "0";
56
56
 
57
57
  const isExpression = isKeyword || (displayValue !== "" && !isNumericVal(displayValue));
58
58
  const hasUnits = units.length > 0 || keywords.length > 0;
@@ -22,7 +22,7 @@ import { live } from "lit/directives/live.js";
22
22
  type ComboOption = { value: string; label: string; style?: string } | { divider: true };
23
23
 
24
24
  export class JxValueSelector extends LitElement {
25
- static properties = {
25
+ static override properties = {
26
26
  options: { attribute: false },
27
27
  placeholder: { type: String },
28
28
  size: { type: String },
@@ -45,7 +45,7 @@ export class JxValueSelector extends LitElement {
45
45
  }
46
46
 
47
47
  /** No shadow DOM — render directly into light DOM */
48
- createRenderRoot() {
48
+ override createRenderRoot() {
49
49
  return this;
50
50
  }
51
51
 
@@ -111,7 +111,7 @@ export class JxValueSelector extends LitElement {
111
111
  }
112
112
  }
113
113
 
114
- render() {
114
+ override render() {
115
115
  if (this._isPicker) {
116
116
  return html`
117
117
  <sp-picker
@@ -25,15 +25,15 @@ export function parseMediaEntries(mediaDef?: Record<string, string> | null) {
25
25
  for (const [name, query] of Object.entries(mediaDef)) {
26
26
  if (name === "--") {
27
27
  const wm = String(query).match(/^(\d+)\s*px$/);
28
- baseWidth = wm ? Number.parseFloat(wm[1]) : 320;
28
+ baseWidth = wm ? Number.parseFloat(wm[1]!) : 320;
29
29
  continue;
30
30
  }
31
31
  const minMatch = query.match(/min-width:\s*([\d.]+)px/);
32
32
  const maxMatch = query.match(/max-width:\s*([\d.]+)px/);
33
33
  if (minMatch) {
34
- sizes.push({ name, query, type: "min", width: Number.parseFloat(minMatch[1]) });
34
+ sizes.push({ name, query, type: "min", width: Number.parseFloat(minMatch[1]!) });
35
35
  } else if (maxMatch) {
36
- sizes.push({ name, query, type: "max", width: Number.parseFloat(maxMatch[1]) });
36
+ sizes.push({ name, query, type: "max", width: Number.parseFloat(maxMatch[1]!) });
37
37
  } else {
38
38
  features.push({ name, query });
39
39
  }
@@ -164,10 +164,10 @@ export function collectMediaOverrides(
164
164
  continue;
165
165
  }
166
166
  const [, uid] = jxMatch;
167
- if (!overrides.has(uid)) {
168
- overrides.set(uid, new Map());
167
+ if (!overrides.has(uid!)) {
168
+ overrides.set(uid!, new Map());
169
169
  }
170
- const props = overrides.get(uid) as Map<string, string>;
170
+ const props = overrides.get(uid!) as Map<string, string>;
171
171
  for (const prop of mediaRule.style) {
172
172
  props.set(prop, mediaRule.style.getPropertyValue(prop));
173
173
  }
@@ -11,6 +11,94 @@ const mediaTags = new Set(["img", "video", "source", "iframe", "audio"]);
11
11
  const TRANSPARENT_PX =
12
12
  "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
13
13
 
14
+ const textTags = new Set([
15
+ "p",
16
+ "h1",
17
+ "h2",
18
+ "h3",
19
+ "h4",
20
+ "h5",
21
+ "h6",
22
+ "blockquote",
23
+ "li",
24
+ "dt",
25
+ "dd",
26
+ "th",
27
+ "td",
28
+ "span",
29
+ "strong",
30
+ "em",
31
+ "small",
32
+ "mark",
33
+ "code",
34
+ "abbr",
35
+ "q",
36
+ "sub",
37
+ "sup",
38
+ "time",
39
+ "a",
40
+ "button",
41
+ "label",
42
+ "legend",
43
+ "caption",
44
+ "summary",
45
+ "pre",
46
+ "option",
47
+ ]);
48
+
49
+ const containerTags = new Set([
50
+ "div",
51
+ "section",
52
+ "article",
53
+ "aside",
54
+ "header",
55
+ "footer",
56
+ "main",
57
+ "nav",
58
+ "figure",
59
+ "figcaption",
60
+ "details",
61
+ "fieldset",
62
+ "form",
63
+ "ul",
64
+ "ol",
65
+ "dl",
66
+ "table",
67
+ ]);
68
+
69
+ /** All placeholder classes prepareForEditMode may add to an empty element. */
70
+ export const EMPTY_PLACEHOLDER_CLASSES = [
71
+ "empty-text-placeholder",
72
+ "empty-container-placeholder",
73
+ ] as const;
74
+
75
+ /**
76
+ * The empty-placeholder class prepareForEditMode would add for this node, or null. Used by the
77
+ * canvas patcher to keep placeholder classes in sync when patching text without a full render.
78
+ *
79
+ * @param {JxMutableNode} node
80
+ */
81
+ export function computeEmptyPlaceholderClass(node: JxMutableNode): string | null {
82
+ if (!node.tagName || node.textContent || node.innerHTML) {
83
+ return null;
84
+ }
85
+ // Layout-originated nodes ($__layout, set by markLayoutNodes) are read-only in page context;
86
+ // Edit affordances like "Click here to add text" don't apply to them.
87
+ if (node.$__layout) {
88
+ return null;
89
+ }
90
+ if (Array.isArray(node.children) && node.children.length > 0) {
91
+ return null;
92
+ }
93
+ if (textTags.has(node.tagName)) {
94
+ return "empty-text-placeholder";
95
+ }
96
+ if (containerTags.has(node.tagName)) {
97
+ return "empty-container-placeholder";
98
+ }
99
+ return null;
100
+ }
101
+
14
102
  /**
15
103
  * Convert a template string to a displayable expression for edit mode. Replaces ${expr} with ❮ expr
16
104
  * ❯ so the runtime renders it as literal text.
@@ -37,6 +125,27 @@ export function restoreTemplateExpressions(el: HTMLElement) {
37
125
  }
38
126
  }
39
127
 
128
+ /**
129
+ * Build the edit-mode visual for a mapped array: a `<div class="repeater-perimeter">` wrapping a
130
+ * single prepared instance of the map template. Always one element (an empty perimeter when the
131
+ * template is missing) so the array keeps a 1:1 DOM node at its sibling index. The perimeter is an
132
+ * edit-only device — it is never compiled or rendered in preview.
133
+ *
134
+ * @param {JxMutableNode} arrayObj
135
+ * @returns {Record<string, unknown>}
136
+ */
137
+ function arrayToPerimeter(arrayObj: JxMutableNode): Record<string, unknown> {
138
+ const template = arrayObj.map;
139
+ return {
140
+ children:
141
+ template && typeof template === "object"
142
+ ? [prepareForEditMode(template as JxMutableNode)]
143
+ : [],
144
+ className: "repeater-perimeter",
145
+ tagName: "div",
146
+ };
147
+ }
148
+
40
149
  /**
41
150
  * Prepare a document for edit-mode rendering. Replaces template strings with readable literal text,
42
151
  * $prototype:Array with placeholders, and $ref bindings with display labels. Preserves state so the
@@ -51,7 +160,13 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
51
160
  }
52
161
  if (Array.isArray(node)) {
53
162
  // Arrays of nodes round-trip element-wise; the array itself is not a node.
54
- return node.map((n) => prepareForEditMode(n)) as unknown as JxMutableNode;
163
+ return (node as JxMutableNode[]).map((n) => prepareForEditMode(n)) as unknown as JxMutableNode;
164
+ }
165
+
166
+ // A mapped-array node itself → its edit-mode perimeter (e.g. when the patcher re-renders an
167
+ // Array node directly). Members inside a children array funnel here via the children branch.
168
+ if ((node as Record<string, unknown>).$prototype === "Array") {
169
+ return arrayToPerimeter(node) as unknown as JxMutableNode;
55
170
  }
56
171
 
57
172
  const /** @type {Record<string, unknown>} */ obj = node as Record<string, unknown>;
@@ -95,25 +210,16 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
95
210
  out[k] = propsOut;
96
211
  } else if (k === "children") {
97
212
  if (Array.isArray(v)) {
98
- out.children = v.map((c) => prepareForEditMode(c));
213
+ // Each member recurses; array pseudo-elements become a single repeater-perimeter element
214
+ // (via the top-level array case) so they stay a 1:1 DOM node among their siblings.
215
+ out.children = v.map((c) => prepareForEditMode(c as JxMutableNode));
99
216
  } else if (
100
217
  v &&
101
218
  typeof v === "object" &&
102
219
  (v as Record<string, unknown>).$prototype === "Array"
103
220
  ) {
104
- // Wrap the map template in a visual repeater perimeter
105
- const vObj = v as Record<string, unknown>;
106
- const template = vObj.map;
107
- out.children =
108
- template && typeof template === "object"
109
- ? [
110
- {
111
- children: [prepareForEditMode(template as JxMutableNode)],
112
- className: "repeater-perimeter",
113
- tagName: "div",
114
- },
115
- ]
116
- : [];
221
+ // Legacy whole-children repeater a single perimeter as the sole child.
222
+ out.children = [prepareForEditMode(v as JxMutableNode)];
117
223
  } else {
118
224
  out.children = prepareForEditMode(v as JxMutableNode);
119
225
  }
@@ -121,7 +227,7 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
121
227
  // Replace $switch cases with a placeholder showing the first case or a label
122
228
  const caseKeys = Object.keys(v);
123
229
  if (caseKeys.length > 0) {
124
- const firstCase = (v as Record<string, unknown>)[caseKeys[0]];
230
+ const firstCase = (v as Record<string, unknown>)[caseKeys[0]!];
125
231
  out.children =
126
232
  firstCase && typeof firstCase === "object" && !(firstCase as Record<string, unknown>).$ref
127
233
  ? [prepareForEditMode(firstCase as JxMutableNode)]
@@ -216,73 +322,9 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
216
322
  }
217
323
 
218
324
  // Mark empty elements with placeholder classes for design-mode visibility
219
- if (out.tagName && !out.textContent && !out.innerHTML) {
220
- const hasChildren = Array.isArray(out.children) && out.children.length > 0;
221
- if (!hasChildren) {
222
- const tag = out.tagName as string;
223
- const textTags = new Set([
224
- "p",
225
- "h1",
226
- "h2",
227
- "h3",
228
- "h4",
229
- "h5",
230
- "h6",
231
- "blockquote",
232
- "li",
233
- "dt",
234
- "dd",
235
- "th",
236
- "td",
237
- "span",
238
- "strong",
239
- "em",
240
- "small",
241
- "mark",
242
- "code",
243
- "abbr",
244
- "q",
245
- "sub",
246
- "sup",
247
- "time",
248
- "a",
249
- "button",
250
- "label",
251
- "legend",
252
- "caption",
253
- "summary",
254
- "pre",
255
- "option",
256
- ]);
257
- const containerTags = new Set([
258
- "div",
259
- "section",
260
- "article",
261
- "aside",
262
- "header",
263
- "footer",
264
- "main",
265
- "nav",
266
- "figure",
267
- "figcaption",
268
- "details",
269
- "fieldset",
270
- "form",
271
- "ul",
272
- "ol",
273
- "dl",
274
- "table",
275
- ]);
276
- if (textTags.has(tag)) {
277
- out.className = out.className
278
- ? `${out.className} empty-text-placeholder`
279
- : "empty-text-placeholder";
280
- } else if (containerTags.has(tag)) {
281
- out.className = out.className
282
- ? `${out.className} empty-container-placeholder`
283
- : "empty-container-placeholder";
284
- }
285
- }
325
+ const placeholderClass = computeEmptyPlaceholderClass(out as JxMutableNode);
326
+ if (placeholderClass) {
327
+ out.className = out.className ? `${out.className} ${placeholderClass}` : placeholderClass;
286
328
  }
287
329
 
288
330
  // Media elements with missing/dynamic src get a placeholder class
@@ -48,7 +48,7 @@ export function extractFontFamily(href: string) {
48
48
  if (!match) {
49
49
  return "";
50
50
  }
51
- return decodeURIComponent(match[1].replaceAll("+", " "));
51
+ return decodeURIComponent(match[1]!.replaceAll("+", " "));
52
52
  }
53
53
 
54
54
  /**
@@ -61,10 +61,11 @@ export function computeInheritedStyle(
61
61
  if (name === activeTab) {
62
62
  break;
63
63
  }
64
- const selBlock = ((style[`@${name}`] || {}) as Record<string, unknown>)[activeSelector] || {};
64
+ const selBlock = (((style[`@${name}`] || {}) as Record<string, unknown>)[activeSelector] ||
65
+ {}) as JxStyle;
65
66
  for (const [p, v] of Object.entries(selBlock)) {
66
67
  if (typeof v !== "object") {
67
- inherited[p] = v ?? "";
68
+ inherited[p] = (v as string | number) ?? "";
68
69
  }
69
70
  }
70
71
  }
@@ -30,7 +30,7 @@ export function camelToLabel(prop: string) {
30
30
 
31
31
  export function toCamelCase(str: string): string {
32
32
  return str
33
- .replaceAll(/[^a-zA-Z0-9]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
33
+ .replaceAll(/[^a-zA-Z0-9]+(.)?/g, (_, c: string | undefined) => (c ? c.toUpperCase() : ""))
34
34
  .replace(/^[A-Z]/, (c) => c.toLowerCase());
35
35
  }
36
36
 
package/src/view.ts CHANGED
@@ -145,7 +145,10 @@ export function applyPanelCollapse() {
145
145
  app.classList.toggle("left-collapsed", view.leftPanelCollapsed);
146
146
  app.classList.toggle("right-collapsed", view.rightPanelCollapsed);
147
147
  try {
148
- const saved = JSON.parse(localStorage.getItem(COLLAPSE_STORAGE_KEY) || "{}");
148
+ const saved = JSON.parse(localStorage.getItem(COLLAPSE_STORAGE_KEY) || "{}") as Record<
149
+ string,
150
+ unknown
151
+ >;
149
152
  saved.leftCollapsed = view.leftPanelCollapsed;
150
153
  saved.rightCollapsed = view.rightPanelCollapsed;
151
154
  localStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(saved));