@jxsuite/studio 0.28.2 → 0.28.4

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 (104) hide show
  1. package/dist/studio.js +86144 -83971
  2. package/dist/studio.js.map +229 -219
  3. package/package.json +5 -5
  4. package/src/browse/browse-modal.ts +9 -3
  5. package/src/browse/browse.ts +139 -82
  6. package/src/canvas/canvas-diff.ts +35 -18
  7. package/src/canvas/canvas-helpers.ts +55 -21
  8. package/src/canvas/canvas-live-render.ts +140 -89
  9. package/src/canvas/canvas-render.ts +127 -82
  10. package/src/canvas/canvas-utils.ts +68 -42
  11. package/src/canvas/nested-site-style.ts +15 -7
  12. package/src/editor/component-inline-edit.ts +90 -51
  13. package/src/editor/content-inline-edit.ts +75 -72
  14. package/src/editor/context-menu.ts +125 -71
  15. package/src/editor/convert-targets.ts +17 -13
  16. package/src/editor/convert-to-component.ts +51 -28
  17. package/src/editor/convert-to-repeater.ts +44 -19
  18. package/src/editor/inline-edit.ts +107 -52
  19. package/src/editor/inline-format.ts +128 -60
  20. package/src/editor/insertion-helper.ts +26 -14
  21. package/src/editor/shortcuts.ts +89 -43
  22. package/src/editor/slash-menu.ts +41 -26
  23. package/src/files/components.ts +9 -7
  24. package/src/files/file-ops.ts +53 -33
  25. package/src/files/files.ts +246 -143
  26. package/src/format/constraints.ts +25 -15
  27. package/src/format/format-host.ts +33 -10
  28. package/src/github/github-auth.ts +24 -14
  29. package/src/github/github-publish.ts +20 -14
  30. package/src/new-project/new-project-modal.ts +27 -18
  31. package/src/panels/activity-bar.ts +27 -26
  32. package/src/panels/ai-panel.ts +100 -44
  33. package/src/panels/block-action-bar.ts +84 -52
  34. package/src/panels/canvas-dnd.ts +47 -28
  35. package/src/panels/data-explorer.ts +24 -11
  36. package/src/panels/dnd.ts +148 -100
  37. package/src/panels/editors.ts +54 -37
  38. package/src/panels/elements-panel.ts +23 -13
  39. package/src/panels/events-panel.ts +48 -38
  40. package/src/panels/git-panel.ts +95 -60
  41. package/src/panels/head-panel.ts +138 -84
  42. package/src/panels/imports-panel.ts +66 -29
  43. package/src/panels/layers-panel.ts +80 -36
  44. package/src/panels/left-panel.ts +86 -53
  45. package/src/panels/overlays.ts +36 -19
  46. package/src/panels/panel-events.ts +48 -27
  47. package/src/panels/panel-scheduler.ts +41 -23
  48. package/src/panels/preview-render.ts +26 -26
  49. package/src/panels/properties-panel.ts +155 -118
  50. package/src/panels/pseudo-preview.ts +31 -15
  51. package/src/panels/quick-search.ts +28 -13
  52. package/src/panels/right-panel.ts +35 -24
  53. package/src/panels/shared.ts +46 -29
  54. package/src/panels/signals-panel.ts +303 -186
  55. package/src/panels/statusbar.ts +27 -12
  56. package/src/panels/style-inputs.ts +30 -24
  57. package/src/panels/style-panel.ts +130 -90
  58. package/src/panels/style-utils.ts +63 -23
  59. package/src/panels/stylebook-layers-panel.ts +27 -30
  60. package/src/panels/stylebook-panel.ts +140 -72
  61. package/src/panels/tab-strip.ts +21 -10
  62. package/src/panels/toolbar.ts +52 -40
  63. package/src/panels/welcome-screen.ts +1 -1
  64. package/src/platform.ts +2 -1
  65. package/src/platforms/devserver.ts +221 -105
  66. package/src/recent-projects.ts +15 -7
  67. package/src/resize-edges.ts +7 -5
  68. package/src/services/cem-export.ts +73 -57
  69. package/src/services/code-services.ts +52 -25
  70. package/src/services/monaco-setup.ts +5 -5
  71. package/src/settings/content-types-editor.ts +133 -62
  72. package/src/settings/css-vars-editor.ts +31 -14
  73. package/src/settings/defs-editor.ts +125 -63
  74. package/src/settings/general-settings.ts +11 -6
  75. package/src/settings/head-editor.ts +30 -15
  76. package/src/settings/schema-field-ui.ts +75 -35
  77. package/src/settings/settings-modal.ts +35 -17
  78. package/src/site-context.ts +80 -47
  79. package/src/state.ts +98 -65
  80. package/src/store.ts +42 -23
  81. package/src/studio.ts +165 -144
  82. package/src/tabs/tab.ts +51 -45
  83. package/src/tabs/transact.ts +230 -109
  84. package/src/types.ts +60 -51
  85. package/src/ui/button-group.ts +3 -2
  86. package/src/ui/color-selector.ts +33 -15
  87. package/src/ui/expression-editor.ts +139 -86
  88. package/src/ui/field-input.ts +35 -18
  89. package/src/ui/field-row.ts +1 -1
  90. package/src/ui/icons.ts +5 -6
  91. package/src/ui/layers.ts +33 -26
  92. package/src/ui/media-picker.ts +13 -7
  93. package/src/ui/panel-resize.ts +39 -20
  94. package/src/ui/spectrum.ts +4 -2
  95. package/src/ui/unit-selector.ts +20 -11
  96. package/src/ui/value-selector.ts +13 -7
  97. package/src/ui/widgets.ts +34 -18
  98. package/src/utils/canvas-media.ts +66 -33
  99. package/src/utils/edit-display.ts +43 -44
  100. package/src/utils/google-fonts.ts +10 -6
  101. package/src/utils/inherited-style.ts +22 -8
  102. package/src/utils/studio-utils.ts +88 -45
  103. package/src/view.ts +9 -5
  104. package/src/workspace/workspace.ts +49 -29
@@ -2,9 +2,9 @@
2
2
  /** Elements panel — block/component palette with categorized accordion and search filter. */
3
3
 
4
4
  import { html, nothing } from "lit-html";
5
- import { getNodeAtPath } from "../store";
5
+ import { childList, getNodeAtPath } from "../store";
6
6
  import { activeTab } from "../workspace/workspace";
7
- import { transactDoc, mutateInsertNode } from "../tabs/transact";
7
+ import { mutateInsertNode, transactDoc } from "../tabs/transact";
8
8
  import { view } from "../view";
9
9
  import { getEffectiveElements } from "../site-context";
10
10
  import { componentRegistry } from "../files/components";
@@ -32,16 +32,20 @@ export function renderElementsTemplate(ctx: {
32
32
  const filtered = view.elementsFilter
33
33
  ? elements.filter((/** @type {{ tag: string }} */ e) => e.tag.includes(view.elementsFilter))
34
34
  : elements;
35
- if (filtered.length === 0) return nothing;
35
+ if (filtered.length === 0) {
36
+ return nothing;
37
+ }
36
38
 
37
39
  return html`
38
40
  <sp-accordion-item
39
41
  label=${category}
40
42
  ?open=${!view.elementsCollapsed.has(category)}
41
43
  @sp-accordion-item-toggle=${(e: Event) => {
42
- if ((e.target as HTMLElement & { open: boolean }).open)
44
+ if ((e.target as HTMLElement & { open: boolean }).open) {
43
45
  view.elementsCollapsed.delete(category);
44
- else view.elementsCollapsed.add(category);
46
+ } else {
47
+ view.elementsCollapsed.add(category);
48
+ }
45
49
  }}
46
50
  >
47
51
  ${filtered.map((/** @type {{ tag: string }} */ { tag }) => {
@@ -54,7 +58,7 @@ export function renderElementsTemplate(ctx: {
54
58
  const t = activeTab.value;
55
59
  const parentPath = t?.session.selection || [];
56
60
  const parent = getNodeAtPath(t!.doc.document, parentPath);
57
- const idx = parent?.children ? parent.children.length : 0;
61
+ const idx = childList(parent).length;
58
62
  transactDoc(t!, (tr) =>
59
63
  mutateInsertNode(tr, parentPath, idx, structuredClone(def)),
60
64
  );
@@ -73,9 +77,11 @@ export function renderElementsTemplate(ctx: {
73
77
  const effectiveEls = getEffectiveElements(
74
78
  tab?.doc.document?.$elements as (string | JxElement)[] | undefined,
75
79
  );
76
- const enabledTags: Set<string> = new Set();
80
+ const enabledTags = new Set<string>();
77
81
  for (const entry of effectiveEls) {
78
- if (typeof entry !== "string") continue;
82
+ if (typeof entry !== "string") {
83
+ continue;
84
+ }
79
85
  const comp = componentRegistry.find(
80
86
  (c: ComponentEntry) =>
81
87
  c.source === "npm" && c.modulePath && entry === `${c.package}/${c.modulePath}`,
@@ -84,7 +90,9 @@ export function renderElementsTemplate(ctx: {
84
90
  enabledTags.add(comp.tagName);
85
91
  } else {
86
92
  for (const c of componentRegistry) {
87
- if (c.source === "npm" && c.package === entry) enabledTags.add(c.tagName);
93
+ if (c.source === "npm" && c.package === entry) {
94
+ enabledTags.add(c.tagName);
95
+ }
88
96
  }
89
97
  }
90
98
  }
@@ -105,9 +113,11 @@ export function renderElementsTemplate(ctx: {
105
113
  label="Components"
106
114
  ?open=${!view.elementsCollapsed.has("Components")}
107
115
  @sp-accordion-item-toggle=${(e: Event) => {
108
- if ((e.target as HTMLElement & { open: boolean }).open)
116
+ if ((e.target as HTMLElement & { open: boolean }).open) {
109
117
  view.elementsCollapsed.delete("Components");
110
- else view.elementsCollapsed.add("Components");
118
+ } else {
119
+ view.elementsCollapsed.add("Components");
120
+ }
111
121
  }}
112
122
  >
113
123
  <div class="components-section">
@@ -123,9 +133,8 @@ export function renderElementsTemplate(ctx: {
123
133
  const t = activeTab.value;
124
134
  const parentPath = t?.session.selection || [];
125
135
  const parent = getNodeAtPath(t!.doc.document, parentPath);
126
- const idx = parent?.children ? parent.children.length : 0;
136
+ const idx = childList(parent).length;
127
137
  const instanceDef = {
128
- tagName: comp.tagName,
129
138
  $props: Object.fromEntries(
130
139
  (comp.props || []).map(
131
140
  (/** @type {{ name: string; default?: unknown }} */ p) => [
@@ -134,6 +143,7 @@ export function renderElementsTemplate(ctx: {
134
143
  ],
135
144
  ),
136
145
  ),
146
+ tagName: comp.tagName,
137
147
  };
138
148
  transactDoc(t!, (tr) =>
139
149
  mutateInsertNode(tr, parentPath, idx, structuredClone(instanceDef)),
@@ -3,10 +3,18 @@ import { getNodeAtPath } from "../store";
3
3
  import { html, nothing } from "lit-html";
4
4
  import { live } from "lit-html/directives/live.js";
5
5
  import { activeTab } from "../workspace/workspace";
6
- import { transactDoc, mutateUpdateProperty } from "../tabs/transact";
6
+ import { mutateUpdateProperty, transactDoc } from "../tabs/transact";
7
7
  import { renderExpressionEditor } from "../ui/expression-editor";
8
+ import {
9
+ getEventBinding,
10
+ isExpressionDef,
11
+ isFunctionDef,
12
+ isJsonObject,
13
+ isRef,
14
+ } from "@jxsuite/schema/guards";
8
15
 
9
- import type { JxPrototypeDef } from "@jxsuite/schema/types";
16
+ import type { JsonValue } from "../types";
17
+ import type { CemEvent, JxFunctionDef, JxPrototypeDef } from "@jxsuite/schema/types";
10
18
 
11
19
  export const EVENT_NAMES = [
12
20
  "onclick",
@@ -27,9 +35,13 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
27
35
  const tab = activeTab.value;
28
36
  const selection = tab?.session.selection;
29
37
  const document = tab?.doc.document;
30
- if (!selection) return html`<div class="empty-state">Select an element to edit events</div>`;
38
+ if (!selection) {
39
+ return html`<div class="empty-state">Select an element to edit events</div>`;
40
+ }
31
41
  const node = getNodeAtPath(document!, selection);
32
- if (!node) return html`<div class="empty-state">Node not found</div>`;
42
+ if (!node) {
43
+ return html`<div class="empty-state">Node not found</div>`;
44
+ }
33
45
 
34
46
  const defs = document!.state || {};
35
47
  const functionDefs = Object.entries(defs).filter(
@@ -40,11 +52,12 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
40
52
  // Declared CEM events (custom element docs)
41
53
  let declaredEventsT: unknown = nothing;
42
54
  if (isCustomElementDoc()) {
43
- const allEmits: Record<string, unknown>[] = [];
55
+ const allEmits: (CemEvent & { _fn: string })[] = [];
44
56
  for (const [fnName, d] of Object.entries(defs)) {
45
- if (Array.isArray((d as Record<string, unknown>).emits)) {
46
- for (const ev of (d as Record<string, unknown>).emits as Record<string, unknown>[])
57
+ if (isFunctionDef(d) && Array.isArray(d.emits)) {
58
+ for (const ev of d.emits) {
47
59
  allEmits.push({ ...ev, _fn: fnName });
60
+ }
48
61
  }
49
62
  }
50
63
  if (allEmits.length > 0) {
@@ -56,10 +69,8 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
56
69
  <div class="declared-event-row" title=${ev.description || ""}>
57
70
  <code class="event-code">${ev.name || "(unnamed)"}</code>
58
71
  <span class="event-source">← ${ev._fn}</span>
59
- ${(ev.type as Record<string, unknown>)?.text
60
- ? html`<span class="event-type"
61
- >${(ev.type as Record<string, unknown>).text}</span
62
- >`
72
+ ${isJsonObject(ev.type) && typeof ev.type.text === "string"
73
+ ? html`<span class="event-type">${ev.type.text}</span>`
63
74
  : nothing}
64
75
  </div>
65
76
  `,
@@ -71,12 +82,9 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
71
82
  }
72
83
 
73
84
  // Find existing event bindings
74
- const eventKeys = Object.keys(node).filter((k) => {
75
- if (!k.startsWith("on")) return false;
76
- const v = node[k];
77
- if (!v || typeof v !== "object") return false;
78
- return v.$ref || v.$prototype === "Function" || v.$expression;
79
- });
85
+ const eventKeys = Object.keys(node).filter(
86
+ (k) => k.startsWith("on") && getEventBinding(node, k) !== undefined,
87
+ );
80
88
 
81
89
  return html`
82
90
  <div class="events-panel">
@@ -86,10 +94,14 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
86
94
  ? html` <sp-field-label size="s">Event Bindings</sp-field-label> `
87
95
  : nothing}
88
96
  ${eventKeys.map((evKey) => {
89
- const evVal = node[evKey];
90
- const isInline = evVal.$prototype === "Function";
91
- const isExpression = evVal.$expression != null;
92
- const currentMode = isInline ? "inline" : isExpression ? "$expression" : "ref";
97
+ const evVal = getEventBinding(node, evKey);
98
+ if (!evVal) {
99
+ return nothing;
100
+ }
101
+ const inlineFn: JxFunctionDef | null = isFunctionDef(evVal) ? evVal : null;
102
+ const expression = isExpressionDef(evVal) ? evVal.$expression : null;
103
+ const refValue = isRef(evVal) ? evVal.$ref : null;
104
+ const currentMode = inlineFn ? "inline" : expression ? "$expression" : "ref";
93
105
  return html`
94
106
  <div class="event-binding">
95
107
  <div class="event-row">
@@ -101,8 +113,8 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
101
113
  const newKey = (e.target as HTMLInputElement).value;
102
114
  if (newKey && newKey !== evKey) {
103
115
  transactDoc(activeTab.value, (t) => {
104
- mutateUpdateProperty(t, selection, evKey, undefined);
105
- mutateUpdateProperty(t, selection, newKey, node[evKey]);
116
+ mutateUpdateProperty(t, selection, evKey);
117
+ mutateUpdateProperty(t, selection, newKey, evVal as JsonValue);
106
118
  });
107
119
  }
108
120
  }}
@@ -132,7 +144,7 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
132
144
  }),
133
145
  );
134
146
  } else {
135
- const firstFn = functionDefs[0];
147
+ const [firstFn] = functionDefs;
136
148
  transactDoc(activeTab.value, (t) =>
137
149
  mutateUpdateProperty(
138
150
  t,
@@ -152,14 +164,12 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
152
164
  size="xs"
153
165
  quiet
154
166
  @click=${() =>
155
- transactDoc(activeTab.value, (t) =>
156
- mutateUpdateProperty(t, selection, evKey, undefined),
157
- )}
167
+ transactDoc(activeTab.value, (t) => mutateUpdateProperty(t, selection, evKey))}
158
168
  >
159
169
  <sp-icon-delete slot="icon"></sp-icon-delete>
160
170
  </sp-action-button>
161
171
  </div>
162
- ${isInline
172
+ ${inlineFn
163
173
  ? html`
164
174
  <div class="event-body-row">
165
175
  <sp-textfield
@@ -167,13 +177,13 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
167
177
  multiline
168
178
  grows
169
179
  placeholder="// handler body"
170
- .value=${live(evVal.body || "")}
180
+ .value=${live(inlineFn?.body || "")}
171
181
  @input=${(e: Event) => {
172
182
  transactDoc(activeTab.value, (t) =>
173
183
  mutateUpdateProperty(t, selection, evKey, {
174
184
  $prototype: "Function",
175
185
  body: (e.target as HTMLInputElement).value,
176
- parameters: evVal.parameters || [],
186
+ parameters: inlineFn?.parameters || [],
177
187
  }),
178
188
  );
179
189
  }}
@@ -185,9 +195,9 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
185
195
  title="Open in editor"
186
196
  @click=${() => {
187
197
  tab.session.ui.editingFunction = {
188
- type: "event",
189
- path: selection,
190
198
  eventKey: evKey,
199
+ path: selection,
200
+ type: "event",
191
201
  };
192
202
  }}
193
203
  >
@@ -195,18 +205,18 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
195
205
  </sp-action-button>
196
206
  </div>
197
207
  `
198
- : isExpression
208
+ : expression
199
209
  ? html`
200
210
  <div class="event-body-row">
201
211
  ${renderExpressionEditor(
202
- evVal.$expression,
203
- (newNode: any) =>
212
+ expression,
213
+ (newNode: unknown) =>
204
214
  transactDoc(activeTab.value, (t) =>
205
215
  mutateUpdateProperty(t, selection, evKey, {
206
216
  $expression: newNode,
207
217
  }),
208
218
  ),
209
- { stateDefs: Object.keys(defs), allowEventRef: true },
219
+ { allowEventRef: true, stateDefs: Object.keys(defs) },
210
220
  )}
211
221
  </div>
212
222
  `
@@ -214,7 +224,7 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
214
224
  <sp-picker
215
225
  size="s"
216
226
  class="event-handler"
217
- .value=${live(evVal.$ref || "__none__")}
227
+ .value=${live(refValue || "__none__")}
218
228
  @change=${(e: Event) => {
219
229
  if (
220
230
  (e.target as HTMLInputElement).value &&
@@ -227,7 +237,7 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
227
237
  );
228
238
  } else {
229
239
  transactDoc(activeTab.value, (t) =>
230
- mutateUpdateProperty(t, selection, evKey, undefined),
240
+ mutateUpdateProperty(t, selection, evKey),
231
241
  );
232
242
  }
233
243
  }}
@@ -5,23 +5,24 @@
5
5
  */
6
6
 
7
7
  import { html, nothing } from "lit-html";
8
+ import { errorMessage } from "@jxsuite/schema/parse";
9
+ import type {
10
+ GitBranchesResult,
11
+ GitDiffState,
12
+ GitFileStatus,
13
+ GitStatusResult,
14
+ StudioPlatform,
15
+ } from "../types";
8
16
  import { live } from "lit-html/directives/live.js";
9
17
  import { repeat } from "lit-html/directives/repeat.js";
10
18
  import { getPlatform } from "../platform";
11
19
  import { formatForPath } from "../format/format-host";
12
- import { updateUi, renderOnly, projectState } from "../store";
20
+ import { projectState, renderOnly, updateUi } from "../store";
13
21
  import { activeTab } from "../workspace/workspace";
14
22
  import { view } from "../view";
15
- import { showDialog, showConfirmDialog } from "../ui/layers";
23
+ import { showConfirmDialog, showDialog } from "../ui/layers";
16
24
  import { statusMessage } from "./statusbar";
17
25
  import { publishToGithub } from "../github/github-publish";
18
- import type { StudioPlatform } from "../types";
19
-
20
- interface GitFileEntry {
21
- path: string;
22
- status: string;
23
- staged: boolean;
24
- }
25
26
 
26
27
  interface GitLogEntry {
27
28
  hash: string;
@@ -30,25 +31,21 @@ interface GitLogEntry {
30
31
  date: string;
31
32
  }
32
33
 
34
+ type GitFileEntry = GitFileStatus;
35
+
33
36
  interface GitUiState {
34
- gitStatus?: {
35
- files?: GitFileEntry[];
36
- branch?: string;
37
- ahead?: number;
38
- behind?: number;
39
- isRepo?: boolean;
40
- remotes?: string[];
41
- } | null;
42
- gitBranches?: { current?: string; branches?: string[] } | null;
37
+ gitStatus?: Partial<GitStatusResult> | null;
38
+ gitBranches?: Partial<GitBranchesResult> | null;
43
39
  gitLoading?: boolean;
44
40
  gitError?: string | null;
45
41
  gitCommitMessage?: string;
46
42
  gitLogEntries?: GitLogEntry[] | null;
47
- [key: string]: unknown;
48
43
  }
49
44
 
50
45
  export async function refreshGitStatus() {
51
- if (!projectState) return;
46
+ if (!projectState) {
47
+ return;
48
+ }
52
49
  const plat = getPlatform();
53
50
  updateUi("gitLoading", true);
54
51
  updateUi("gitError", null);
@@ -57,8 +54,8 @@ export async function refreshGitStatus() {
57
54
  updateUi("gitStatus", status);
58
55
  updateUi("gitBranches", branches);
59
56
  _lastUpdated = new Date();
60
- } catch (e) {
61
- updateUi("gitError", (e as Error).message);
57
+ } catch (error) {
58
+ updateUi("gitError", errorMessage(error));
62
59
  } finally {
63
60
  updateUi("gitLoading", false);
64
61
  renderOnly("leftPanel");
@@ -104,7 +101,9 @@ export async function cloneRepository(ctx: { openRecentProject: (root: string) =
104
101
  `,
105
102
  );
106
103
 
107
- if (!url) return;
104
+ if (!url) {
105
+ return;
106
+ }
108
107
 
109
108
  try {
110
109
  statusMessage("Cloning repository...");
@@ -113,14 +112,14 @@ export async function cloneRepository(ctx: { openRecentProject: (root: string) =
113
112
  statusMessage("Clone complete");
114
113
  await ctx.openRecentProject(result.root);
115
114
  }
116
- } catch (e) {
117
- statusMessage(`Clone failed: ${(e as Error).message}`);
115
+ } catch (error) {
116
+ statusMessage(`Clone failed: ${errorMessage(error)}`);
118
117
  }
119
118
  }
120
119
 
121
120
  /** @returns {boolean} */
122
121
  export function platformSupportsClone() {
123
- return !!getPlatform().gitClone;
122
+ return Boolean(getPlatform().gitClone);
124
123
  }
125
124
 
126
125
  /**
@@ -128,14 +127,15 @@ export function platformSupportsClone() {
128
127
  * @param {unknown} [body]
129
128
  */
130
129
  async function gitAction(action: string, body?: unknown) {
131
- const plat = getPlatform() as Record<string, Function> & StudioPlatform;
130
+ const plat = getPlatform() as Record<string, (...args: unknown[]) => Promise<unknown>> &
131
+ StudioPlatform;
132
132
  updateUi("gitLoading", true);
133
133
  updateUi("gitError", null);
134
134
  try {
135
135
  await plat[action](body);
136
136
  await refreshGitStatus();
137
- } catch (e) {
138
- updateUi("gitError", (e as Error).message);
137
+ } catch (error) {
138
+ updateUi("gitError", errorMessage(error));
139
139
  updateUi("gitLoading", false);
140
140
  renderOnly("leftPanel");
141
141
  }
@@ -151,8 +151,8 @@ async function fetchGitLog() {
151
151
  const entries = await plat.gitLog(30);
152
152
  updateUi("gitLogEntries", entries);
153
153
  renderOnly("leftPanel");
154
- } catch (e) {
155
- updateUi("gitError", (e as Error).message);
154
+ } catch (error) {
155
+ updateUi("gitError", errorMessage(error));
156
156
  renderOnly("leftPanel");
157
157
  }
158
158
  }
@@ -161,7 +161,7 @@ async function fetchGitLog() {
161
161
  * @param {{ ui: GitUiState }} S
162
162
  * @param {{
163
163
  * setCanvasMode?: (mode: string) => void;
164
- * setGitDiffState?: (state: unknown) => void;
164
+ * setGitDiffState?: (state: GitDiffState | null) => void;
165
165
  * cloneRepository?: () => void;
166
166
  * }} ctx
167
167
  */
@@ -169,7 +169,7 @@ export function renderGitPanel(
169
169
  S: { ui: GitUiState },
170
170
  ctx: {
171
171
  setCanvasMode?: (mode: string) => void;
172
- setGitDiffState?: (state: unknown) => void;
172
+ setGitDiffState?: (state: GitDiffState | null) => void;
173
173
  cloneRepository?: () => void;
174
174
  },
175
175
  ) {
@@ -231,8 +231,10 @@ export function renderGitPanel(
231
231
 
232
232
  if (!_pollTimer) {
233
233
  _pollTimer = setInterval(() => {
234
- if (view.leftTab === "git" && !S.ui.gitLoading) refreshGitStatus();
235
- }, 30000);
234
+ if (view.leftTab === "git" && !S.ui.gitLoading) {
235
+ refreshGitStatus();
236
+ }
237
+ }, 30_000);
236
238
  }
237
239
 
238
240
  const stagedFiles = status?.files?.filter((f: GitFileEntry) => f.staged) || [];
@@ -242,7 +244,9 @@ export function renderGitPanel(
242
244
  const doCommit = async () => {
243
245
  const tab = activeTab.value;
244
246
  const msg = tab?.session.ui.gitCommitMessage?.trim();
245
- if (!msg) return;
247
+ if (!msg) {
248
+ return;
249
+ }
246
250
  updateUi("gitCommitMessage", "");
247
251
  await gitAction("gitCommit", msg);
248
252
  };
@@ -250,7 +254,9 @@ export function renderGitPanel(
250
254
  const doCommitAndSync = async () => {
251
255
  const tab = activeTab.value;
252
256
  const msg = tab?.session.ui.gitCommitMessage?.trim();
253
- if (!msg) return;
257
+ if (!msg) {
258
+ return;
259
+ }
254
260
  updateUi("gitCommitMessage", "");
255
261
  updateUi("gitLoading", true);
256
262
  updateUi("gitError", null);
@@ -259,8 +265,8 @@ export function renderGitPanel(
259
265
  await plat.gitCommit(msg);
260
266
  await plat.gitPush();
261
267
  await refreshGitStatus();
262
- } catch (e) {
263
- updateUi("gitError", (e as Error).message);
268
+ } catch (error) {
269
+ updateUi("gitError", errorMessage(error));
264
270
  updateUi("gitLoading", false);
265
271
  renderOnly("leftPanel");
266
272
  }
@@ -386,11 +392,16 @@ export function renderGitPanel(
386
392
  const val = (e.target as HTMLInputElement).value;
387
393
  if (val === "__new__") {
388
394
  (e.target as HTMLInputElement).value = branches?.current || "";
395
+ // oxlint-disable-next-line no-alert -- native prompt is the intended quick-input UX here
389
396
  const name = prompt("New branch name:");
390
- if (name?.trim()) await gitAction("gitCreateBranch", name.trim());
397
+ if (name?.trim()) {
398
+ await gitAction("gitCreateBranch", name.trim());
399
+ }
391
400
  return;
392
401
  }
393
- if (val !== branches?.current) await gitAction("gitCheckout", val);
402
+ if (val !== branches?.current) {
403
+ await gitAction("gitCheckout", val);
404
+ }
394
405
  }}
395
406
  >
396
407
  ${(branches?.branches || []).map(
@@ -405,7 +416,9 @@ export function renderGitPanel(
405
416
  // ─── 3. Tabs: Local Changes / History ────────────────────────────────────
406
417
  const switchTab = (tab: string) => {
407
418
  _gitSubTab = tab;
408
- if (tab === "history" && !S.ui.gitLogEntries) fetchGitLog();
419
+ if (tab === "history" && !S.ui.gitLogEntries) {
420
+ fetchGitLog();
421
+ }
409
422
  renderOnly("leftPanel");
410
423
  };
411
424
 
@@ -461,7 +474,9 @@ export function renderGitPanel(
461
474
  const menu = (
462
475
  (e.currentTarget as HTMLElement).parentElement as HTMLElement
463
476
  ).querySelector(".git-split-menu");
464
- if (menu) menu.toggleAttribute("hidden");
477
+ if (menu) {
478
+ menu.toggleAttribute("hidden");
479
+ }
465
480
  }}
466
481
  >
467
482
  <sp-icon-chevron-down slot="icon" size="xs"></sp-icon-chevron-down>
@@ -492,8 +507,12 @@ export function renderGitPanel(
492
507
  const dir = parts.join("/");
493
508
 
494
509
  const onFileClick = async () => {
495
- if (file.status !== "M" && file.status !== "A") return;
496
- if (!file.path.endsWith(".json") && !formatForPath(file.path)) return;
510
+ if (file.status !== "M" && file.status !== "A") {
511
+ return;
512
+ }
513
+ if (!file.path.endsWith(".json") && !formatForPath(file.path)) {
514
+ return;
515
+ }
497
516
 
498
517
  try {
499
518
  const plat = getPlatform();
@@ -507,20 +526,22 @@ export function renderGitPanel(
507
526
  ]);
508
527
 
509
528
  const diffState = {
510
- filePath: file.path,
511
- originalContent,
512
529
  currentContent,
530
+ filePath: file.path,
513
531
  fileStatus: file.status,
532
+ originalContent,
514
533
  };
515
534
 
516
535
  updateUi("gitDiffState", diffState);
517
536
 
518
537
  if (ctx?.setCanvasMode) {
519
- if (ctx.setGitDiffState) ctx.setGitDiffState(diffState);
538
+ if (ctx.setGitDiffState) {
539
+ ctx.setGitDiffState(diffState);
540
+ }
520
541
  ctx.setCanvasMode("git-diff");
521
542
  }
522
- } catch (e) {
523
- updateUi("gitError", `Failed to load diff: ${(e as Error).message}`);
543
+ } catch (error) {
544
+ updateUi("gitError", `Failed to load diff: ${errorMessage(error)}`);
524
545
  } finally {
525
546
  updateUi("gitLoading", false);
526
547
  }
@@ -555,13 +576,17 @@ export function renderGitPanel(
555
576
  quiet
556
577
  title="Discard changes"
557
578
  @click=${async () => {
558
- if (file.status === "U") return;
579
+ if (file.status === "U") {
580
+ return;
581
+ }
559
582
  const confirmed = await showConfirmDialog(
560
583
  "Discard Changes",
561
584
  `Discard changes to ${file.path}?`,
562
585
  { confirmLabel: "Discard", destructive: true },
563
586
  );
564
- if (!confirmed) return;
587
+ if (!confirmed) {
588
+ return;
589
+ }
565
590
  await gitAction("gitDiscard", [file.path]);
566
591
  }}
567
592
  ?disabled=${file.status === "U"}
@@ -585,16 +610,18 @@ export function renderGitPanel(
585
610
 
586
611
  /** Group files by component (parent directory for .json/.class.json, or "Other") */
587
612
  const groupFilesByComponent = (files: GitFileEntry[]) => {
588
- const groups: Map<string, GitFileEntry[]> = new Map();
613
+ const groups = new Map<string, GitFileEntry[]>();
589
614
  for (const f of files) {
590
615
  const parts = f.path.split("/");
591
616
  let component;
592
617
  if (f.path.endsWith(".json") || f.path.endsWith(".class.json") || formatForPath(f.path)) {
593
- component = parts.length > 1 ? `/${parts[parts.length - 2]}` : `/${parts[0]}`;
618
+ component = parts.length > 1 ? `/${parts.at(-2)}` : `/${parts[0]}`;
594
619
  } else {
595
620
  component = "Other";
596
621
  }
597
- if (!groups.has(component)) groups.set(component, []);
622
+ if (!groups.has(component)) {
623
+ groups.set(component, []);
624
+ }
598
625
  (groups.get(component) as GitFileEntry[]).push(f);
599
626
  }
600
627
  return groups;
@@ -708,13 +735,21 @@ function _relativeDate(iso: string) {
708
735
  const d = new Date(iso);
709
736
  const now = Date.now();
710
737
  const diff = now - d.getTime();
711
- const mins = Math.floor(diff / 60000);
712
- if (mins < 1) return "just now";
713
- if (mins < 60) return `${mins}m ago`;
738
+ const mins = Math.floor(diff / 60_000);
739
+ if (mins < 1) {
740
+ return "just now";
741
+ }
742
+ if (mins < 60) {
743
+ return `${mins}m ago`;
744
+ }
714
745
  const hours = Math.floor(mins / 60);
715
- if (hours < 24) return `${hours}h ago`;
746
+ if (hours < 24) {
747
+ return `${hours}h ago`;
748
+ }
716
749
  const days = Math.floor(hours / 24);
717
- if (days < 30) return `${days}d ago`;
750
+ if (days < 30) {
751
+ return `${days}d ago`;
752
+ }
718
753
  return d.toLocaleDateString();
719
754
  }
720
755