@jxsuite/studio 0.26.2 → 0.28.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 (60) hide show
  1. package/dist/studio.js +34349 -57015
  2. package/dist/studio.js.map +130 -398
  3. package/package.json +5 -11
  4. package/src/browse/browse.ts +62 -22
  5. package/src/canvas/canvas-live-render.ts +57 -55
  6. package/src/canvas/canvas-render.ts +50 -21
  7. package/src/editor/context-menu.ts +4 -1
  8. package/src/editor/convert-to-component.ts +8 -2
  9. package/src/editor/inline-edit.ts +7 -2
  10. package/src/editor/insertion-helper.ts +12 -3
  11. package/src/editor/slash-menu.ts +5 -1
  12. package/src/files/file-ops.ts +121 -102
  13. package/src/files/files.ts +53 -35
  14. package/src/format/constraints.ts +55 -0
  15. package/src/format/format-host.ts +212 -0
  16. package/src/new-project/new-project-modal.ts +18 -3
  17. package/src/panels/ai-panel.ts +3 -1
  18. package/src/panels/block-action-bar.ts +4 -2
  19. package/src/panels/canvas-dnd.ts +78 -28
  20. package/src/panels/data-explorer.ts +9 -2
  21. package/src/panels/dnd.ts +14 -3
  22. package/src/panels/editors.ts +1 -1
  23. package/src/panels/git-panel.ts +18 -8
  24. package/src/panels/head-panel.ts +12 -5
  25. package/src/panels/left-panel.ts +7 -4
  26. package/src/panels/panel-events.ts +3 -1
  27. package/src/panels/properties-panel.ts +9 -3
  28. package/src/panels/quick-search.ts +10 -10
  29. package/src/panels/right-panel.ts +6 -2
  30. package/src/panels/shared.ts +6 -1
  31. package/src/panels/signals-panel.ts +35 -10
  32. package/src/panels/style-inputs.ts +5 -1
  33. package/src/panels/style-panel.ts +6 -2
  34. package/src/panels/stylebook-layers-panel.ts +12 -3
  35. package/src/panels/stylebook-panel.ts +24 -8
  36. package/src/panels/toolbar.ts +16 -3
  37. package/src/platforms/devserver.ts +32 -3
  38. package/src/resize-edges.ts +12 -2
  39. package/src/services/cem-export.ts +9 -3
  40. package/src/settings/content-types-editor.ts +7 -2
  41. package/src/settings/defs-editor.ts +6 -1
  42. package/src/settings/general-settings.ts +3 -1
  43. package/src/settings/schema-field-ui.ts +11 -3
  44. package/src/settings/settings-modal.ts +4 -1
  45. package/src/state.ts +5 -1
  46. package/src/studio.ts +27 -16
  47. package/src/tabs/tab.ts +29 -6
  48. package/src/tabs/transact.ts +9 -2
  49. package/src/types.ts +15 -6
  50. package/src/ui/button-group.ts +6 -1
  51. package/src/ui/expression-editor.ts +14 -3
  52. package/src/ui/layers.ts +5 -1
  53. package/src/ui/media-picker.ts +192 -43
  54. package/src/ui/unit-selector.ts +6 -1
  55. package/src/utils/canvas-media.ts +1 -1
  56. package/src/utils/google-fonts.ts +4 -1
  57. package/src/utils/studio-utils.ts +7 -1
  58. package/src/view.ts +8 -2
  59. package/src/markdown/md-allowlist.ts +0 -104
  60. package/src/markdown/md-convert.ts +0 -846
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Format-host — the studio's view of the project's format registry.
3
+ *
4
+ * Format classes are auto-discovered server-side from the project imports map
5
+ * (specs/extensions.md); the studio introspects them via the PAL (`listFormats`) and invokes
6
+ * parse/serialize capabilities via `formatAction` (POST /__studio/format in the dev server, RPC on
7
+ * desktop). The studio itself holds zero format knowledge — `.json` is the single native built-in.
8
+ */
9
+
10
+ import { getPlatform } from "../platform";
11
+ import type { JxMutableNode } from "@jxsuite/schema/types";
12
+
13
+ export interface StudioFormatHints {
14
+ icon?: string;
15
+ modes?: string[];
16
+ documentMode?: {
17
+ default?: "content" | "component";
18
+ componentWhen?: { frontmatterKey?: string; matches?: string };
19
+ };
20
+ newFileTemplate?: string;
21
+ elements?: {
22
+ block?: string[];
23
+ inline?: string[];
24
+ void?: string[];
25
+ textOnly?: string[];
26
+ nesting?: Record<
27
+ string,
28
+ {
29
+ block?: boolean;
30
+ inline?: boolean;
31
+ directive?: boolean;
32
+ only?: string[];
33
+ }
34
+ >;
35
+ };
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ export interface StudioFormat {
40
+ name: string;
41
+ extensions: string[];
42
+ mediaType: string | null;
43
+ documentKinds: string[];
44
+ exportTarget: boolean;
45
+ remote: boolean;
46
+ studio: StudioFormatHints | null;
47
+ capabilities: Record<string, { identifier: string; timing: string[] }>;
48
+ }
49
+
50
+ let _formats: StudioFormat[] = [];
51
+ let _loaded: Promise<StudioFormat[]> | null = null;
52
+
53
+ /** Load (and cache) the project's format registry from the platform. */
54
+ export function loadFormats(): Promise<StudioFormat[]> {
55
+ if (!_loaded) {
56
+ _loaded = (async () => {
57
+ try {
58
+ const platform = getPlatform() as {
59
+ listFormats?: () => Promise<StudioFormat[]>;
60
+ };
61
+ _formats = (await platform.listFormats?.()) ?? [];
62
+ } catch {
63
+ _formats = [];
64
+ }
65
+ return _formats;
66
+ })();
67
+ }
68
+ return _loaded;
69
+ }
70
+
71
+ /** Invalidate the cached registry (call on project switch). */
72
+ export function refreshFormats() {
73
+ _loaded = null;
74
+ _formats = [];
75
+ }
76
+
77
+ /** Seed the registry directly (tests and hosts that preload format metadata). */
78
+ export function setFormats(formats: StudioFormat[]) {
79
+ _formats = formats;
80
+ _loaded = Promise.resolve(formats);
81
+ }
82
+
83
+ /** The last-loaded registry (synchronous access for render paths). */
84
+ export function getFormats(): StudioFormat[] {
85
+ return _formats;
86
+ }
87
+
88
+ /** Look up the format claiming a file extension (".md" or "md"), optionally by capability. */
89
+ export function formatByExtension(ext: string, capability?: string): StudioFormat | undefined {
90
+ const norm = ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`;
91
+ return _formats.find(
92
+ (f) => f.extensions.includes(norm) && (!capability || f.capabilities[capability]),
93
+ );
94
+ }
95
+
96
+ /** Look up a format by its import name. */
97
+ export function formatByName(name: string | null | undefined): StudioFormat | undefined {
98
+ if (!name) return undefined;
99
+ return _formats.find((f) => f.name === name);
100
+ }
101
+
102
+ /** The format claiming a file path's extension, if any. */
103
+ export function formatForPath(path: string | null | undefined): StudioFormat | undefined {
104
+ if (!path) return undefined;
105
+ const dot = path.lastIndexOf(".");
106
+ if (dot === -1) return undefined;
107
+ return formatByExtension(path.slice(dot));
108
+ }
109
+
110
+ /** Registered extensions, optionally filtered by document kind. Never includes ".json". */
111
+ export function documentExtensions(kind?: string): string[] {
112
+ const exts = new Set<string>();
113
+ for (const f of _formats) {
114
+ if (kind && !f.documentKinds.includes(kind)) continue;
115
+ for (const e of f.extensions) exts.add(e);
116
+ }
117
+ exts.delete(".json");
118
+ return [...exts];
119
+ }
120
+
121
+ /** The default format for new content documents (first content-kind serializer). */
122
+ export function defaultContentFormat(): StudioFormat | undefined {
123
+ return _formats.find((f) => f.capabilities.serialize && f.documentKinds.includes("content"));
124
+ }
125
+
126
+ async function formatAction(payload: Record<string, unknown>): Promise<unknown> {
127
+ const platform = getPlatform() as {
128
+ formatAction?: (payload: Record<string, unknown>) => Promise<unknown>;
129
+ };
130
+ if (!platform.formatAction) {
131
+ throw new Error("Platform does not support format actions");
132
+ }
133
+ return platform.formatAction(payload);
134
+ }
135
+
136
+ /** Parse source text into a Jx document via the named format's parse capability. */
137
+ export async function formatParse(
138
+ name: string,
139
+ source: string,
140
+ options?: Record<string, unknown>,
141
+ ): Promise<JxMutableNode> {
142
+ return (await formatAction({
143
+ format: name,
144
+ action: "parse",
145
+ source,
146
+ options,
147
+ })) as JxMutableNode;
148
+ }
149
+
150
+ /** Serialize a Jx document to source text via the named format's serialize capability. */
151
+ export async function formatSerialize(
152
+ name: string,
153
+ doc: Record<string, unknown>,
154
+ options?: Record<string, unknown>,
155
+ ): Promise<string> {
156
+ return (await formatAction({
157
+ format: name,
158
+ action: "serialize",
159
+ doc,
160
+ options,
161
+ })) as string;
162
+ }
163
+
164
+ /**
165
+ * Split a parsed format document into { document, frontmatter, mode } per the format's
166
+ * `$studio.documentMode` hints. Component documents (e.g. a frontmatter `tagName` with a hyphen)
167
+ * keep their full shape; content documents separate frontmatter metadata from the body children.
168
+ */
169
+ export function splitFormatDocument(format: StudioFormat | undefined, doc: JxMutableNode) {
170
+ const hints = format?.studio?.documentMode;
171
+ const componentWhen = hints?.componentWhen;
172
+ if (componentWhen?.frontmatterKey) {
173
+ const value = doc[componentWhen.frontmatterKey];
174
+ const pattern = componentWhen.matches ? new RegExp(componentWhen.matches) : /./;
175
+ if (typeof value === "string" && pattern.test(value)) {
176
+ return {
177
+ document: doc,
178
+ frontmatter: {} as Record<string, unknown>,
179
+ mode: "component",
180
+ };
181
+ }
182
+ }
183
+ if (hints?.default === "component") {
184
+ return {
185
+ document: doc,
186
+ frontmatter: {} as Record<string, unknown>,
187
+ mode: "component",
188
+ };
189
+ }
190
+
191
+ // Content document — children form the root-level body; other keys are frontmatter
192
+ const children = (doc.children as unknown[]) ?? [];
193
+ if (children.length === 0) children.push({ tagName: "p", children: [] });
194
+
195
+ const documentKeys = new Set(["state", "imports"]);
196
+ const contentDoc: Record<string, unknown> = { children };
197
+ const frontmatter: Record<string, unknown> = {};
198
+ for (const [key, value] of Object.entries(doc)) {
199
+ if (key === "children") continue;
200
+ if (documentKeys.has(key)) {
201
+ contentDoc[key] = value;
202
+ } else {
203
+ frontmatter[key] = value;
204
+ }
205
+ }
206
+
207
+ return {
208
+ document: contentDoc as JxMutableNode,
209
+ frontmatter,
210
+ mode: "content",
211
+ };
212
+ }
@@ -20,7 +20,13 @@ let _handle: ReturnType<typeof openModal> | null = null;
20
20
  * directory: string;
21
21
  * }}
22
22
  */
23
- let _form = { name: "", description: "", url: "", adapter: "static", directory: "" };
23
+ let _form = {
24
+ name: "",
25
+ description: "",
26
+ url: "",
27
+ adapter: "static",
28
+ directory: "",
29
+ };
24
30
 
25
31
  let _error: string = "";
26
32
 
@@ -35,9 +41,18 @@ let _resolve: ((result: { root: string; config: ProjectConfig } | null) => void)
35
41
  *
36
42
  * @returns {Promise<{ root: string; config: object } | null>}
37
43
  */
38
- export function openNewProjectModal(): Promise<{ root: string; config: ProjectConfig } | null> {
44
+ export function openNewProjectModal(): Promise<{
45
+ root: string;
46
+ config: ProjectConfig;
47
+ } | null> {
39
48
  if (_handle) return Promise.resolve(null);
40
- _form = { name: "", description: "", url: "", adapter: "static", directory: "" };
49
+ _form = {
50
+ name: "",
51
+ description: "",
52
+ url: "",
53
+ adapter: "static",
54
+ directory: "",
55
+ };
41
56
  _error = "";
42
57
  _creating = false;
43
58
 
@@ -48,7 +48,9 @@ export function mountAiPanel() {
48
48
  checkAuth();
49
49
  }
50
50
 
51
- const _g = globalThis as unknown as { __jxRightPanelRender?: { render: () => void } };
51
+ const _g = globalThis as unknown as {
52
+ __jxRightPanelRender?: { render: () => void };
53
+ };
52
54
 
53
55
  function rerenderPanel() {
54
56
  const { render } = _g.__jxRightPanelRender || {};
@@ -31,8 +31,10 @@ import type { JxPath } from "../state";
31
31
  * navigateToComponent: (path: string) => void;
32
32
  * } | null}
33
33
  */
34
- let _ctx: { getCanvasMode: () => string; navigateToComponent: (path: string) => void } | null =
35
- null;
34
+ let _ctx: {
35
+ getCanvasMode: () => string;
36
+ navigateToComponent: (path: string) => void;
37
+ } | null = null;
36
38
 
37
39
  /**
38
40
  * Initialize the block action bar module.
@@ -42,6 +42,26 @@ export function registerPanelDnD(panel: CanvasPanel) {
42
42
  const { canvas, dropLine } = panel;
43
43
  const allEls = canvas.querySelectorAll("*");
44
44
 
45
+ // Drop-target callbacks fire on EVERY target in the stack (innermost → outermost),
46
+ // and every canvas element is a drop target — so the indicator and the drop are
47
+ // driven from the monitor using only the innermost target.
48
+ /** Innermost drop target if it belongs to this panel's canvas, else null */
49
+ const innermostCanvasTarget = (location: {
50
+ current: {
51
+ dropTargets: {
52
+ data: Record<string | symbol, unknown>;
53
+ element: Element;
54
+ }[];
55
+ };
56
+ }) => {
57
+ const target = location.current.dropTargets[0];
58
+ if (!target) return null;
59
+ const tEl = target.element as HTMLElement;
60
+ const tPath = target.data.path;
61
+ if (!canvas.contains(tEl) || !Array.isArray(tPath)) return null;
62
+ return { el: tEl, path: tPath as JxPath, isLeaf: !!target.data._isVoid };
63
+ };
64
+
45
65
  const monitorCleanup = monitorForElements({
46
66
  onDragStart({ location }) {
47
67
  view.lastDragInput = location.current.input;
@@ -52,8 +72,34 @@ export function registerPanelDnD(panel: CanvasPanel) {
52
72
  },
53
73
  onDrag({ location }) {
54
74
  view.lastDragInput = location.current.input;
75
+ const target = innermostCanvasTarget(location);
76
+ if (target) {
77
+ if (_activeDropEl && _activeDropEl !== target.el) {
78
+ _activeDropEl.classList.remove("canvas-drop-target");
79
+ }
80
+ _activeDropEl = target.el;
81
+ showCanvasDropIndicator(target.el, target.path, target.isLeaf, panel);
82
+ } else if (location.current.dropTargets.length > 0) {
83
+ // Pointer is over a non-canvas target (e.g. a layer row) — hide this panel's
84
+ // indicator. When over dead space (no targets at all) keep the last indicator
85
+ // visible so it persists for the whole drag.
86
+ if (_activeDropEl && canvas.contains(_activeDropEl)) {
87
+ _activeDropEl.classList.remove("canvas-drop-target");
88
+ _activeDropEl = null;
89
+ }
90
+ dropLine.style.display = "none";
91
+ }
55
92
  },
56
- onDrop() {
93
+ onDrop({ source, location }) {
94
+ const target = innermostCanvasTarget(location);
95
+ if (target) {
96
+ const { instruction, targetPath } = getCanvasDropResult(
97
+ target.el,
98
+ target.path,
99
+ target.isLeaf,
100
+ );
101
+ applyDropInstruction(instruction, source.data, targetPath);
102
+ }
57
103
  _activeDropEl?.classList.remove("canvas-drop-target");
58
104
  _activeDropEl = null;
59
105
  for (const p of canvasPanels) {
@@ -81,7 +127,7 @@ export function registerPanelDnD(panel: CanvasPanel) {
81
127
  const isLeaf = VOID_ELEMENTS.has(tag) || !hasElementChildren;
82
128
 
83
129
  const cleanup = dropTargetForElements({
84
- element: /** @type {HTMLElement} */ (el),
130
+ element: /** @type {HTMLElement} */ el,
85
131
  canDrop({ source }) {
86
132
  const srcPath = source.data.path as JxPath | undefined;
87
133
  if (srcPath && isAncestor(srcPath, elPath)) return false;
@@ -90,26 +136,6 @@ export function registerPanelDnD(panel: CanvasPanel) {
90
136
  getData() {
91
137
  return { path: elPath, _isVoid: isLeaf };
92
138
  },
93
- onDragEnter({ location }) {
94
- view.lastDragInput = location.current.input;
95
- if (_activeDropEl && _activeDropEl !== el) {
96
- _activeDropEl.classList.remove("canvas-drop-target");
97
- }
98
- _activeDropEl = el as HTMLElement;
99
- showCanvasDropIndicator(el as HTMLElement, elPath, isLeaf, panel);
100
- },
101
- onDrag({ location }) {
102
- view.lastDragInput = location.current.input;
103
- showCanvasDropIndicator(el as HTMLElement, elPath, isLeaf, panel);
104
- },
105
- onDragLeave() {},
106
- onDrop({ source }) {
107
- dropLine.style.display = "none";
108
- (el as HTMLElement).classList.remove("canvas-drop-target");
109
- _activeDropEl = null;
110
- const { instruction, targetPath } = getCanvasDropResult(el as HTMLElement, elPath, isLeaf);
111
- applyDropInstruction(instruction, source.data, targetPath);
112
- },
113
139
  });
114
140
  view.canvasDndCleanups.push(cleanup);
115
141
  }
@@ -123,13 +149,21 @@ export function registerPanelDnD(panel: CanvasPanel) {
123
149
  */
124
150
  function getCanvasDropResult(el: HTMLElement, elPath: JxPath, isLeaf: boolean): DropResult {
125
151
  if (!view.lastDragInput)
126
- return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
152
+ return {
153
+ instruction: { type: "make-child" },
154
+ referenceEl: el,
155
+ targetPath: elPath,
156
+ };
127
157
  const y = view.lastDragInput.clientY;
128
158
 
129
159
  if (elPath.length === 0) {
130
160
  const children = Array.from(el.children) as HTMLElement[];
131
161
  if (children.length === 0)
132
- return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
162
+ return {
163
+ instruction: { type: "make-child" },
164
+ referenceEl: el,
165
+ targetPath: elPath,
166
+ };
133
167
  return nearestChildEdge(children, y, elPath);
134
168
  }
135
169
 
@@ -143,10 +177,22 @@ function getCanvasDropResult(el: HTMLElement, elPath: JxPath, isLeaf: boolean):
143
177
  }
144
178
 
145
179
  if (relY < 0.25)
146
- return { instruction: { type: "reorder-above" }, referenceEl: el, targetPath: elPath };
180
+ return {
181
+ instruction: { type: "reorder-above" },
182
+ referenceEl: el,
183
+ targetPath: elPath,
184
+ };
147
185
  if (relY > 0.75)
148
- return { instruction: { type: "reorder-below" }, referenceEl: el, targetPath: elPath };
149
- return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
186
+ return {
187
+ instruction: { type: "reorder-below" },
188
+ referenceEl: el,
189
+ targetPath: elPath,
190
+ };
191
+ return {
192
+ instruction: { type: "make-child" },
193
+ referenceEl: el,
194
+ targetPath: elPath,
195
+ };
150
196
  }
151
197
 
152
198
  /**
@@ -181,7 +227,11 @@ function nearestChildEdge(children: HTMLElement[], cursorY: number, parentPath:
181
227
  }
182
228
 
183
229
  const childPath = [...parentPath, "children", closestIdx];
184
- return { instruction, referenceEl: children[closestIdx], targetPath: childPath };
230
+ return {
231
+ instruction,
232
+ referenceEl: children[closestIdx],
233
+ targetPath: childPath,
234
+ };
185
235
  }
186
236
 
187
237
  /**
@@ -77,7 +77,10 @@ export function renderDataExplorerTemplate(
77
77
  return html`
78
78
  <div class="data-row">
79
79
  <div
80
- class=${classMap({ "data-row-header": true, expanded: isExpanded })}
80
+ class=${classMap({
81
+ "data-row-header": true,
82
+ expanded: isExpanded,
83
+ })}
81
84
  @click=${() => {
82
85
  if (expandedDataKeys.has(name)) expandedDataKeys.delete(name);
83
86
  else expandedDataKeys.add(name);
@@ -86,7 +89,11 @@ export function renderDataExplorerTemplate(
86
89
  >
87
90
  <span class="signal-badge ${defCategory(def)}">${defBadgeLabel(def)}</span>
88
91
  <span class="data-name">${name}</span>
89
- <span class=${classMap({ "data-type": true, "data-pending": unwrapped === null })}
92
+ <span
93
+ class=${classMap({
94
+ "data-type": true,
95
+ "data-pending": unwrapped === null,
96
+ })}
90
97
  >${dataTypeLabel(value)}</span
91
98
  >
92
99
  </div>
package/src/panels/dnd.ts CHANGED
@@ -115,9 +115,8 @@ export function registerLayersDnD() {
115
115
  onDrag({ self }: DragSelfArgs) {
116
116
  showLayerDropGap(row, self.data, container);
117
117
  },
118
- onDragLeave() {
119
- clearLayerDropGap(container);
120
- },
118
+ // No onDragLeave clear — the gap persists while the pointer crosses dead space
119
+ // between rows; the monitor clears it when the drag moves to a non-tree target.
121
120
  onDrop() {
122
121
  clearLayerDropGap(container);
123
122
  },
@@ -128,6 +127,12 @@ export function registerLayersDnD() {
128
127
 
129
128
  // Global monitor
130
129
  const monitorCleanup = monitorForElements({
130
+ onDropTargetChange({ location }: DragMonitorDropArgs) {
131
+ // Clear the layer gap when the drag moves onto a non-tree target (e.g. a canvas
132
+ // element). When there is no target at all, keep the gap so it persists.
133
+ const inner = location.current.dropTargets[0];
134
+ if (inner && !extractInstruction(inner.data)) clearLayerDropGap(container);
135
+ },
131
136
  onDrop({ source, location }: DragMonitorDropArgs) {
132
137
  clearLayerDropGap(container);
133
138
  const target = location.current.dropTargets[0];
@@ -321,6 +326,10 @@ export function applyDropInstruction(
321
326
  const fromPath = srcData.path as JxPath;
322
327
  const targetParent = parentElementPath(targetPath) as JxPath;
323
328
  const targetIdx = childIndex(targetPath) as number;
329
+ // Reordering requires a parent and a numeric index; root-level paths can't be
330
+ // reordered around.
331
+ if (instruction.type !== "make-child" && (!targetParent || typeof targetIdx !== "number"))
332
+ return;
324
333
 
325
334
  switch (instruction.type) {
326
335
  case "reorder-above":
@@ -341,6 +350,8 @@ export function applyDropInstruction(
341
350
  } else if (srcData.type === "block") {
342
351
  const targetParent = parentElementPath(targetPath) as JxPath;
343
352
  const targetIdx = childIndex(targetPath) as number;
353
+ if (instruction.type !== "make-child" && (!targetParent || typeof targetIdx !== "number"))
354
+ return;
344
355
 
345
356
  switch (instruction.type) {
346
357
  case "reorder-above":
@@ -104,7 +104,7 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
104
104
 
105
105
  const body = getFunctionBody(editing);
106
106
  const args = getFunctionArgs(
107
- /** @type {EditingTarget} */ (editing as EditingTarget),
107
+ /** @type {EditingTarget} */ editing as EditingTarget,
108
108
  activeTab.value?.doc.document,
109
109
  );
110
110
 
@@ -8,6 +8,7 @@ import { html, nothing } from "lit-html";
8
8
  import { live } from "lit-html/directives/live.js";
9
9
  import { repeat } from "lit-html/directives/repeat.js";
10
10
  import { getPlatform } from "../platform";
11
+ import { formatForPath } from "../format/format-host";
11
12
  import { updateUi, renderOnly, projectState } from "../store";
12
13
  import { activeTab } from "../workspace/workspace";
13
14
  import { view } from "../view";
@@ -191,7 +192,9 @@ export function renderGitPanel(
191
192
 
192
193
  if (!status && !loading) {
193
194
  refreshGitStatus();
194
- return html`<div class="git-panel"><div class="git-loading">Loading...</div></div>`;
195
+ return html`<div class="git-panel">
196
+ <div class="git-loading">Loading...</div>
197
+ </div>`;
195
198
  }
196
199
 
197
200
  if (status && !status.isRepo) {
@@ -213,7 +216,10 @@ export function renderGitPanel(
213
216
  </sp-action-button>
214
217
  <sp-action-button
215
218
  size="m"
216
- @click=${() => publishToGithub({ projectName: projectState?.name || "my-project" })}
219
+ @click=${() =>
220
+ publishToGithub({
221
+ projectName: projectState?.name || "my-project",
222
+ })}
217
223
  ?disabled=${loading}
218
224
  >
219
225
  <sp-icon-share slot="icon"></sp-icon-share>
@@ -266,7 +272,10 @@ export function renderGitPanel(
266
272
  ? "Up to date"
267
273
  : `${status?.ahead ? `${status.ahead} ahead` : ""}${status?.ahead && status?.behind ? ", " : ""}${status?.behind ? `${status.behind} behind` : ""}`;
268
274
  const lastUpdatedStr = _lastUpdated
269
- ? _lastUpdated.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
275
+ ? _lastUpdated.toLocaleTimeString([], {
276
+ hour: "2-digit",
277
+ minute: "2-digit",
278
+ })
270
279
  : "";
271
280
 
272
281
  const hasRemotes = (status?.remotes?.length ?? 0) > 0;
@@ -332,7 +341,10 @@ export function renderGitPanel(
332
341
  </div>
333
342
  <sp-action-button
334
343
  size="s"
335
- @click=${() => publishToGithub({ projectName: projectState?.name || "my-project" })}
344
+ @click=${() =>
345
+ publishToGithub({
346
+ projectName: projectState?.name || "my-project",
347
+ })}
336
348
  ?disabled=${loading}
337
349
  >
338
350
  <sp-icon-share slot="icon"></sp-icon-share>
@@ -481,7 +493,7 @@ export function renderGitPanel(
481
493
 
482
494
  const onFileClick = async () => {
483
495
  if (file.status !== "M" && file.status !== "A") return;
484
- if (!file.path.endsWith(".md") && !file.path.endsWith(".json")) return;
496
+ if (!file.path.endsWith(".json") && !formatForPath(file.path)) return;
485
497
 
486
498
  try {
487
499
  const plat = getPlatform();
@@ -494,12 +506,10 @@ export function renderGitPanel(
494
506
  plat.readFile(file.path),
495
507
  ]);
496
508
 
497
- const isMarkdown = file.path.endsWith(".md");
498
509
  const diffState = {
499
510
  filePath: file.path,
500
511
  originalContent,
501
512
  currentContent,
502
- isMarkdown,
503
513
  fileStatus: file.status,
504
514
  };
505
515
 
@@ -579,7 +589,7 @@ export function renderGitPanel(
579
589
  for (const f of files) {
580
590
  const parts = f.path.split("/");
581
591
  let component;
582
- if (f.path.endsWith(".json") || f.path.endsWith(".class.json") || f.path.endsWith(".md")) {
592
+ if (f.path.endsWith(".json") || f.path.endsWith(".class.json") || formatForPath(f.path)) {
583
593
  component = parts.length > 1 ? `/${parts[parts.length - 2]}` : `/${parts[0]}`;
584
594
  } else {
585
595
  component = "Other";
@@ -74,7 +74,12 @@ const PAGE_FIELDS: MetaField[] = [
74
74
 
75
75
  const OG_FIELDS: MetaField[] = [
76
76
  { label: "Title", attr: "property", key: "og:title" },
77
- { label: "Description", attr: "property", key: "og:description", multiline: true },
77
+ {
78
+ label: "Description",
79
+ attr: "property",
80
+ key: "og:description",
81
+ multiline: true,
82
+ },
78
83
  { label: "Image", attr: "property", key: "og:image", media: true },
79
84
  { label: "Type", attr: "property", key: "og:type" },
80
85
  ];
@@ -243,7 +248,9 @@ function renderMetaFieldRow(
243
248
  const placeholder =
244
249
  field.key === "viewport" ? "width=device-width, initial-scale=1" : `${field.label}…`;
245
250
  const widget = field.multiline
246
- ? spTextArea(`head:${field.key}`, val, commit, { placeholder: `${field.label}…` })
251
+ ? spTextArea(`head:${field.key}`, val, commit, {
252
+ placeholder: `${field.label}…`,
253
+ })
247
254
  : spTextField(`head:${field.key}`, val, commit, { placeholder });
248
255
 
249
256
  return renderFieldRow({
@@ -522,7 +529,7 @@ function renderFrontmatterSection() {
522
529
  const fields = [];
523
530
  if (schemaProps) {
524
531
  for (const [field, fieldSchema] of Object.entries(
525
- /** @type {Record<string, FmSchemaEntry>} */ (schemaProps),
532
+ /** @type {Record<string, FmSchemaEntry>} */ schemaProps,
526
533
  )) {
527
534
  if (RESERVED_FM_KEYS.has(field)) continue;
528
535
  fields.push({ field, entry: fieldSchema, value: fm[field] as JsonValue });
@@ -532,7 +539,7 @@ function renderFrontmatterSection() {
532
539
  fields.push({
533
540
  field,
534
541
  entry: { type: typeof value === "boolean" ? "boolean" : "string" },
535
- value: /** @type {JsonValue} */ (value),
542
+ value: /** @type {JsonValue} */ value,
536
543
  });
537
544
  }
538
545
  } else {
@@ -541,7 +548,7 @@ function renderFrontmatterSection() {
541
548
  fields.push({
542
549
  field,
543
550
  entry: { type: typeof value === "boolean" ? "boolean" : "string" },
544
- value: /** @type {JsonValue} */ (value),
551
+ value: /** @type {JsonValue} */ value,
545
552
  });
546
553
  }
547
554
  }
@@ -170,7 +170,7 @@ function _render() {
170
170
  * content?: { frontmatter?: Record<string, unknown> };
171
171
  * documentPath?: string;
172
172
  * }}
173
- */ ({
173
+ */ {
174
174
  ui: aTab.session.ui,
175
175
  document: aTab.doc.document,
176
176
  mode: aTab.doc.mode,
@@ -178,7 +178,7 @@ function _render() {
178
178
  canvas: aTab.session.canvas,
179
179
  content: aTab.doc.content,
180
180
  documentPath: aTab.documentPath,
181
- });
181
+ };
182
182
 
183
183
  /** @type {TemplateResult | typeof nothing} */
184
184
  let content;
@@ -226,12 +226,15 @@ function _render() {
226
226
  const tab = activeTab.value!;
227
227
  const fm = (tab.doc.content?.frontmatter ?? {}) as Record<string, unknown>;
228
228
  const fmHead = fm.$head as unknown[] | undefined;
229
- const tmp = { title: fm.title, $head: fmHead ? [...fmHead] : undefined };
229
+ const tmp = {
230
+ title: fm.title,
231
+ $head: fmHead ? [...fmHead] : undefined,
232
+ };
230
233
  fn(tmp);
231
234
  if (tmp.title !== fm.title)
232
235
  mutateUpdateFrontmatter(tab, "title", tmp.title as JsonValue);
233
236
  const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
234
- mutateUpdateFrontmatter(tab, "$head", /** @type {JsonValue} */ (newHead));
237
+ mutateUpdateFrontmatter(tab, "$head", /** @type {JsonValue} */ newHead);
235
238
  render();
236
239
  }
237
240
  : (fn: (doc: object) => void) => {