@jxsuite/studio 0.1.0 → 0.5.1

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 (40) hide show
  1. package/dist/studio.js +50941 -34749
  2. package/dist/studio.js.map +461 -345
  3. package/package.json +46 -35
  4. package/src/browse/browse.js +414 -0
  5. package/src/editor/context-menu.js +48 -1
  6. package/src/editor/convert-to-component.js +208 -0
  7. package/src/editor/inline-edit.js +33 -6
  8. package/src/editor/shortcuts.js +6 -1
  9. package/src/files/components.js +4 -2
  10. package/src/files/file-ops.js +102 -54
  11. package/src/files/files.js +22 -8
  12. package/src/markdown/md-convert.js +309 -11
  13. package/src/panels/activity-bar.js +3 -0
  14. package/src/panels/head-panel.js +576 -0
  15. package/src/panels/overlays.js +133 -0
  16. package/src/panels/right-panel.js +130 -0
  17. package/src/panels/shared.js +41 -0
  18. package/src/panels/signals-panel.js +95 -94
  19. package/src/panels/statusbar.js +15 -1
  20. package/src/panels/toolbar.js +223 -0
  21. package/src/platforms/devserver.js +58 -16
  22. package/src/settings/collections-editor.js +428 -0
  23. package/src/settings/defs-editor.js +418 -0
  24. package/src/settings/schema-field-ui.js +329 -0
  25. package/src/state.js +99 -2
  26. package/src/store.js +112 -41
  27. package/src/studio.js +1551 -1565
  28. package/src/ui/button-group.js +91 -0
  29. package/src/ui/color-selector.js +299 -0
  30. package/src/ui/field-row.js +47 -0
  31. package/src/ui/media-picker.js +172 -0
  32. package/src/ui/panel-resize.js +96 -0
  33. package/src/ui/spectrum.js +36 -2
  34. package/src/ui/unit-selector.js +106 -0
  35. package/src/ui/{jx-styled-combobox.js → value-selector.js} +7 -7
  36. package/src/ui/widgets.js +106 -0
  37. package/src/utils/canvas-media.js +151 -0
  38. package/src/utils/inherited-style.js +54 -0
  39. package/src/utils/studio-utils.js +32 -0
  40. package/src/view.js +68 -0
@@ -0,0 +1,208 @@
1
+ // ─── Convert to Component ─────────────────────────────────────────────────────
2
+ import { html, render as litRender } from "lit-html";
3
+ import { update, getNodeAtPath, applyMutation, parentElementPath, childIndex } from "../store.js";
4
+ import {
5
+ computeRelativePath,
6
+ loadComponentRegistry,
7
+ componentRegistry,
8
+ } from "../files/components.js";
9
+ import { getPlatform } from "../platform.js";
10
+ import { statusMessage } from "../panels/statusbar.js";
11
+
12
+ const VALID_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
13
+
14
+ /**
15
+ * Convert the currently selected element into a reusable component.
16
+ *
17
+ * @param {any} S - Current studio state
18
+ */
19
+ export async function convertToComponent(S) {
20
+ if (!S.selection || S.selection.length < 2) return;
21
+
22
+ const node = getNodeAtPath(S.document, S.selection);
23
+ if (!node || !node.tagName) return;
24
+
25
+ const defaultName = deriveDefaultName(node);
26
+ const name = await promptComponentName(defaultName);
27
+ if (!name) return;
28
+
29
+ // Extract component definition
30
+ const componentDef = extractComponentDef(node);
31
+ componentDef.tagName = name;
32
+
33
+ // Compute paths
34
+ const componentFile = "components/" + name + ".json";
35
+ const refPath = computeRelativePath(S.documentPath, componentFile);
36
+
37
+ // Single atomic mutation: replace node + add $elements ref
38
+ const selectionPath = S.selection;
39
+ const newState = applyMutation(S, (doc) => {
40
+ // Navigate to parent's children array and replace the node
41
+ const pp = parentElementPath(selectionPath) ?? [];
42
+ const idx = childIndex(selectionPath);
43
+ let parent = doc;
44
+ for (const seg of pp) parent = parent[seg];
45
+ parent.children[idx] = { tagName: name };
46
+
47
+ // Ensure $elements exists and add the $ref
48
+ if (!doc.$elements) doc.$elements = [];
49
+ const alreadyReferenced = doc.$elements.some(
50
+ (/** @type {any} */ el) => el && el.$ref === refPath,
51
+ );
52
+ if (!alreadyReferenced) {
53
+ doc.$elements.push({ $ref: refPath });
54
+ }
55
+ });
56
+
57
+ update(newState);
58
+
59
+ // Write component file and refresh registry
60
+ try {
61
+ const platform = getPlatform();
62
+ await platform.writeFile(componentFile, JSON.stringify(componentDef, null, 2));
63
+ await loadComponentRegistry();
64
+ statusMessage(`Converted to <${name}>`);
65
+ } catch (/** @type {any} */ err) {
66
+ statusMessage(`Error saving component: ${err.message}`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Derive a default tag name from a node.
72
+ *
73
+ * @param {any} node
74
+ * @returns {string}
75
+ */
76
+ function deriveDefaultName(node) {
77
+ if (node.$id && node.$id.includes("-")) return node.$id.toLowerCase();
78
+ const tag = (node.tagName ?? "div").toLowerCase();
79
+ return tag.includes("-") ? tag : "jx-" + tag;
80
+ }
81
+
82
+ /**
83
+ * Deep clone a node and strip page-specific keys.
84
+ *
85
+ * @param {any} node
86
+ * @returns {any}
87
+ */
88
+ function extractComponentDef(node) {
89
+ const clone = structuredClone(node);
90
+ delete clone.$id;
91
+ delete clone.$layout;
92
+ delete clone.$paths;
93
+ return clone;
94
+ }
95
+
96
+ /**
97
+ * Validate a component name against naming rules and existing registry.
98
+ *
99
+ * @param {string} val
100
+ * @returns {{ valid: boolean; error: string }}
101
+ */
102
+ function validateName(val) {
103
+ val = val.trim().toLowerCase();
104
+ if (!val.includes("-")) {
105
+ return { valid: false, error: "Name must contain a hyphen (e.g. my-component)" };
106
+ }
107
+ if (!VALID_NAME.test(val)) {
108
+ return { valid: false, error: "Lowercase letters, digits, and hyphens only" };
109
+ }
110
+ const exists = componentRegistry.some((/** @type {any} */ c) => c.tagName === val);
111
+ if (exists) {
112
+ return { valid: false, error: `Component <${val}> already exists` };
113
+ }
114
+ return { valid: true, error: "" };
115
+ }
116
+
117
+ /**
118
+ * Show a naming dialog using Lit-rendered sp-dialog-wrapper.
119
+ *
120
+ * @param {string} defaultName
121
+ * @returns {Promise<string | null>}
122
+ */
123
+ function promptComponentName(defaultName) {
124
+ return new Promise((resolve) => {
125
+ let value = defaultName;
126
+ let error = "";
127
+ let resolved = false;
128
+
129
+ const host = document.createElement("div");
130
+ const themeRoot = document.querySelector("sp-theme") || document.body;
131
+ themeRoot.appendChild(host);
132
+
133
+ function cleanup() {
134
+ if (resolved) return;
135
+ resolved = true;
136
+ host.remove();
137
+ }
138
+
139
+ function confirm() {
140
+ const result = validateName(value);
141
+ if (!result.valid) {
142
+ error = result.error;
143
+ renderDialog();
144
+ return;
145
+ }
146
+ cleanup();
147
+ resolve(value.trim().toLowerCase());
148
+ }
149
+
150
+ function cancel() {
151
+ cleanup();
152
+ resolve(null);
153
+ }
154
+
155
+ function onInput(/** @type {Event} */ e) {
156
+ value = /** @type {any} */ (e.target).value || "";
157
+ const result = validateName(value);
158
+ error = result.valid ? "" : result.error;
159
+ renderDialog();
160
+ }
161
+
162
+ function onKeydown(/** @type {KeyboardEvent} */ e) {
163
+ if (e.key === "Enter") confirm();
164
+ }
165
+
166
+ function renderDialog() {
167
+ litRender(
168
+ html`
169
+ <sp-dialog-wrapper
170
+ open
171
+ underlay
172
+ headline="Convert to Component"
173
+ confirm-label="Convert"
174
+ cancel-label="Cancel"
175
+ size="s"
176
+ @confirm=${confirm}
177
+ @cancel=${cancel}
178
+ @close=${cancel}
179
+ >
180
+ <p>Enter a hyphenated tag name for the new component.</p>
181
+ <sp-textfield
182
+ placeholder="my-component"
183
+ value=${value}
184
+ ?negative=${!!error}
185
+ @input=${onInput}
186
+ @keydown=${onKeydown}
187
+ >
188
+ <sp-help-text slot="negative-help-text">${error}</sp-help-text>
189
+ </sp-textfield>
190
+ </sp-dialog-wrapper>
191
+ `,
192
+ host,
193
+ );
194
+ }
195
+
196
+ renderDialog();
197
+
198
+ // Focus the textfield after Spectrum renders
199
+ requestAnimationFrame(() => {
200
+ const tf = /** @type {any} */ (host.querySelector("sp-textfield"));
201
+ if (tf) {
202
+ tf.focus();
203
+ const input = tf.shadowRoot?.querySelector("input");
204
+ if (input) input.select();
205
+ }
206
+ });
207
+ });
208
+ }
@@ -94,8 +94,8 @@ let activePath = null; // JSON path to the active element
94
94
  let commitFn = null; // function(path, newChildren, newTextContent) to commit changes
95
95
  /** @type {((path: any[], beforeChildren: any, afterChildren: any) => void) | null} */
96
96
  let splitFn = null; // function(path, beforeChildren, afterChildren) to split paragraph
97
- /** @type {((path: any[], elementDef: any) => void) | null} */
98
- let insertFn = null; // function(path, elementDef) to insert after current block
97
+ /** @type {((path: any[], elementDef: any, commitData?: any) => void) | null} */
98
+ let insertFn = null; // function(path, elementDef, commitData?) to insert after current block
99
99
  /** @type {(() => void) | null} */
100
100
  let endFn = null; // function() called when editing stops
101
101
 
@@ -337,14 +337,26 @@ function handleEnterKey() {
337
337
 
338
338
  // Stop editing before mutating state (which will re-render)
339
339
  const path = [...activePath];
340
+ const split = splitFn;
341
+
340
342
  activeEl.contentEditable = "false";
341
343
  activeEl.removeEventListener("keydown", handleKeydown);
342
344
  activeEl.removeEventListener("input", handleInput);
343
345
  activeEl.removeEventListener("blur", handleBlur);
344
346
  activeEl.removeEventListener("paste", handlePaste);
345
347
  activeEl = null;
348
+ activePath = null;
349
+ commitFn = null;
350
+ splitFn = null;
351
+ insertFn = null;
352
+
353
+ if (endFn) {
354
+ const fn = endFn;
355
+ endFn = null;
356
+ fn();
357
+ }
346
358
 
347
- splitFn(path, beforeChildren, afterChildren);
359
+ split(path, beforeChildren, afterChildren);
348
360
  }
349
361
 
350
362
  // ─── Content sync: DOM → Jx ────────────────────────────────────────────
@@ -582,16 +594,31 @@ function handleSlashSelect(cmd) {
582
594
  }
583
595
  }
584
596
 
585
- commitChanges();
597
+ // Compute commit data inline instead of calling commitChanges() — avoids a separate
598
+ // update() call that would race with the insertFn update() (two concurrent async renders).
599
+ normalizeInlineContent(activeEl);
600
+ const commitResult = elementToJx(activeEl);
586
601
 
587
602
  const path = [...activePath];
603
+ const insert = insertFn;
604
+
588
605
  activeEl.contentEditable = "false";
589
606
  activeEl.removeEventListener("keydown", handleKeydown);
590
607
  activeEl.removeEventListener("input", handleInput);
591
608
  activeEl.removeEventListener("blur", handleBlur);
592
609
  activeEl.removeEventListener("paste", handlePaste);
593
610
  activeEl = null;
611
+ activePath = null;
612
+ commitFn = null;
613
+ splitFn = null;
614
+ insertFn = null;
615
+
616
+ if (endFn) {
617
+ const fn = endFn;
618
+ endFn = null;
619
+ fn();
620
+ }
594
621
 
595
- // Delegate to studio.js callback which builds the element def and inserts it
596
- insertFn(path, cmd);
622
+ // Pass commit data so onInsert can batch commit + insert into a single update()
623
+ insert(path, cmd, commitResult);
597
624
  }
@@ -115,7 +115,12 @@ export function initShortcuts(getContext) {
115
115
  const mod = e.ctrlKey || e.metaKey;
116
116
 
117
117
  // Don't intercept when typing in inputs or contenteditable
118
- if (e.target instanceof HTMLElement && e.target.matches("input, textarea, select")) {
118
+ if (
119
+ e.target instanceof HTMLElement &&
120
+ e.target.matches(
121
+ "input, textarea, select, sp-textfield, sp-search, sp-number-field, sp-picker",
122
+ )
123
+ ) {
119
124
  if (mod && e.key === "s") {
120
125
  e.preventDefault();
121
126
  saveFile();
@@ -23,9 +23,11 @@ export async function loadComponentRegistry() {
23
23
  */
24
24
  export function computeRelativePath(fromDocPath, toCompPath) {
25
25
  if (!fromDocPath) return `./${toCompPath}`;
26
- const fromDir = fromDocPath.substring(0, fromDocPath.lastIndexOf("/"));
26
+ const from = fromDocPath.replaceAll("\\", "/");
27
+ const to = toCompPath.replaceAll("\\", "/");
28
+ const fromDir = from.substring(0, from.lastIndexOf("/"));
27
29
  const fromParts = fromDir.split("/").filter(Boolean);
28
- const toParts = toCompPath.split("/").filter(Boolean);
30
+ const toParts = to.split("/").filter(Boolean);
29
31
  let common = 0;
30
32
  while (
31
33
  common < fromParts.length &&
@@ -6,16 +6,14 @@
6
6
  */
7
7
 
8
8
  import { unified } from "unified";
9
- import remarkParse from "remark-parse";
10
9
  import remarkStringify from "remark-stringify";
11
- import remarkFrontmatter from "remark-frontmatter";
12
- import remarkGfm from "remark-gfm";
13
10
  import remarkDirective from "remark-directive";
14
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
15
- import { mdToJx, jxToMd } from "../markdown/md-convert.js";
11
+ import { stringify as stringifyYaml } from "yaml";
12
+ import { jxToMd, jxDocToMd } from "../markdown/md-convert.js";
16
13
  import { createState } from "../store.js";
17
14
  import { locateDocument } from "../services/code-services.js";
18
15
  import { statusMessage } from "../panels/statusbar.js";
16
+ import { getPlatform } from "../platform.js";
19
17
 
20
18
  /**
21
19
  * Open a file via the File System Access API (or fallback input).
@@ -35,7 +33,7 @@ export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar })
35
33
  const text = await file.text();
36
34
 
37
35
  if (handle.name.endsWith(".md")) {
38
- const newState = loadMarkdown(text, handle);
36
+ const newState = await loadMarkdown(text, handle);
39
37
  commit(newState);
40
38
  } else {
41
39
  const doc = JSON.parse(text);
@@ -59,7 +57,7 @@ export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar })
59
57
  const text = await file.text();
60
58
 
61
59
  if (file.name.endsWith(".md")) {
62
- const newState = loadMarkdown(text, null);
60
+ const newState = await loadMarkdown(text, null);
63
61
  commit(newState);
64
62
  } else {
65
63
  const doc = JSON.parse(text);
@@ -80,33 +78,46 @@ export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar })
80
78
  /**
81
79
  * Parse a markdown string into a Jx state object (pure — no side effects).
82
80
  *
81
+ * All markdown goes through `transpileJxMarkdown()`. Content documents (no hyphenated `tagName`)
82
+ * are wrapped in a `{ tagName: "div", $id: "content" }` root to match the studio's content
83
+ * contract.
84
+ *
83
85
  * @param {any} source Markdown text
84
86
  * @param {any} fileHandle File handle (or null)
85
- * @returns {any} A new state object ready for commit()
87
+ * @returns {Promise<any>} A new state object ready for commit()
86
88
  */
87
- export function loadMarkdown(source, fileHandle) {
88
- const processor = unified()
89
- .use(remarkParse)
90
- .use(remarkFrontmatter, ["yaml"])
91
- .use(remarkGfm)
92
- .use(remarkDirective);
93
-
94
- const mdast = processor.parse(source);
95
-
96
- // Extract frontmatter from the first YAML node
97
- let frontmatter = {};
98
- const yamlNode = mdast.children.find((n) => n.type === "yaml");
99
- if (yamlNode) {
100
- try {
101
- frontmatter = parseYaml(yamlNode.value) ?? {};
102
- } catch {}
89
+ export async function loadMarkdown(source, fileHandle) {
90
+ const { transpileJxMarkdown } = await import("@jxsuite/parser/transpile");
91
+ const doc = /** @type {any} */ (transpileJxMarkdown(source));
92
+
93
+ const isComponent = doc.tagName && String(doc.tagName).includes("-");
94
+
95
+ if (isComponent) {
96
+ const newState = /** @type {any} */ (createState(doc));
97
+ newState.sourceFormat = "md";
98
+ newState.rawMarkdown = source;
99
+ newState.fileHandle = fileHandle;
100
+ newState.dirty = false;
101
+ return newState;
103
102
  }
104
103
 
105
- const jxTree = mdToJx(mdast);
104
+ // Content markdown — children form the root-level document body
105
+ const contentDoc = {
106
+ children: doc.children ?? [],
107
+ };
108
+
109
+ // Extract frontmatter keys (everything except children) as content metadata
110
+ /** @type {Record<string, any>} */
111
+ const frontmatter = {};
112
+ for (const [key, value] of Object.entries(doc)) {
113
+ if (key !== "children") frontmatter[key] = value;
114
+ }
106
115
 
107
- const newState = createState(jxTree);
116
+ const newState = /** @type {any} */ (createState(contentDoc));
117
+ newState.sourceFormat = "md";
108
118
  newState.mode = "content";
109
119
  newState.content = { frontmatter };
120
+ newState.rawMarkdown = source;
110
121
  newState.fileHandle = fileHandle;
111
122
  newState.dirty = false;
112
123
  return newState;
@@ -130,52 +141,66 @@ async function loadCompanionJS(handle, state) {
130
141
  }
131
142
 
132
143
  /**
133
- * Save the current document to disk (or download as fallback).
144
+ * Save the current document back to its source location.
134
145
  *
135
146
  * @param {{ S: any; commit: (s: any) => void; renderToolbar: () => void }} ctx
136
147
  */
137
148
  export async function saveFile({ S, commit, renderToolbar }) {
138
149
  try {
139
- const isContent = S.mode === "content";
140
- let output, mimeType, ext, description;
141
-
142
- if (isContent) {
143
- const mdast = jxToMd(S.document);
144
- const md = unified()
145
- .use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
146
- .stringify(mdast);
147
-
148
- const fm = S.content?.frontmatter;
149
- const hasFrontmatter = fm && Object.keys(fm).length > 0;
150
- output = hasFrontmatter ? `---\n${stringifyYaml(fm).trim()}\n---\n\n${md}` : md;
151
- mimeType = "text/markdown";
152
- ext = ".md";
153
- description = "Markdown Content";
154
- } else {
155
- output = JSON.stringify(S.document, null, 2);
156
- mimeType = "application/json";
157
- ext = ".json";
158
- description = "Jx Component";
159
- }
150
+ const output = serializeDocument(S);
160
151
 
161
- if (S.fileHandle && "createWritable" in S.fileHandle) {
152
+ if (S.documentPath) {
153
+ // Project file — save via platform
154
+ const platform = getPlatform();
155
+ await platform.writeFile(S.documentPath, output);
156
+ commit({ ...S, dirty: false });
157
+ renderToolbar();
158
+ statusMessage("Saved");
159
+ } else if (S.fileHandle && "createWritable" in S.fileHandle) {
160
+ // Standalone file opened via FS Access API
162
161
  const writable = await S.fileHandle.createWritable();
163
162
  await writable.write(output);
164
163
  await writable.close();
165
164
  commit({ ...S, dirty: false });
166
165
  renderToolbar();
167
166
  statusMessage("Saved");
168
- } else if ("showSaveFilePicker" in window) {
167
+ } else {
168
+ statusMessage("No save target — use Export");
169
+ }
170
+ } catch (/** @type {any} */ e) {
171
+ if (e.name !== "AbortError") statusMessage(`Save error: ${e.message}`);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Export the current document to a new location (Save As / download).
177
+ *
178
+ * @param {{ S: any; commit: (s: any) => void; renderToolbar: () => void }} ctx
179
+ */
180
+ export async function exportFile({ S, commit, renderToolbar }) {
181
+ try {
182
+ const isContent = S.mode === "content";
183
+ const output = serializeDocument(S);
184
+ const mimeType = isContent ? "text/markdown" : "application/json";
185
+ const ext = isContent ? ".md" : ".json";
186
+ const description = isContent ? "Markdown Content" : "Jx Component";
187
+
188
+ if ("showSaveFilePicker" in window) {
189
+ const suggestedName = S.documentPath
190
+ ? S.documentPath.split("/").pop()
191
+ : isContent
192
+ ? "content.md"
193
+ : "component.json";
169
194
  const handle = await /** @type {any} */ (window).showSaveFilePicker({
170
- suggestedName: isContent ? "content.md" : "component.json",
195
+ suggestedName,
171
196
  types: [{ description, accept: { [mimeType]: [ext] } }],
172
197
  });
173
198
  const writable = await handle.createWritable();
174
199
  await writable.write(output);
175
200
  await writable.close();
176
- commit({ ...S, fileHandle: handle, dirty: false });
201
+ commit({ ...S, dirty: false });
177
202
  renderToolbar();
178
- statusMessage(`Saved as ${handle.name}`);
203
+ statusMessage(`Exported as ${handle.name}`);
179
204
  } else {
180
205
  // Fallback: download
181
206
  const blob = new Blob([output], { type: mimeType });
@@ -190,6 +215,29 @@ export async function saveFile({ S, commit, renderToolbar }) {
190
215
  statusMessage("Downloaded");
191
216
  }
192
217
  } catch (/** @type {any} */ e) {
193
- if (e.name !== "AbortError") statusMessage(`Save error: ${e.message}`);
218
+ if (e.name !== "AbortError") statusMessage(`Export error: ${e.message}`);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Serialize the current document to its output format (JSON or Markdown).
224
+ *
225
+ * @param {any} S
226
+ * @returns {string}
227
+ */
228
+ function serializeDocument(S) {
229
+ if (S.sourceFormat === "md") {
230
+ return jxDocToMd(S.document);
231
+ }
232
+ if (S.mode === "content") {
233
+ const mdast = jxToMd(S.document);
234
+ const md = unified()
235
+ .use(remarkDirective)
236
+ .use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
237
+ .stringify(mdast);
238
+ const fm = S.content?.frontmatter;
239
+ const hasFrontmatter = fm && Object.keys(fm).length > 0;
240
+ return hasFrontmatter ? `---\n${stringifyYaml(fm).trim()}\n---\n\n${md}` : md;
194
241
  }
242
+ return JSON.stringify(S.document, null, 2);
195
243
  }
@@ -8,6 +8,7 @@
8
8
  import { html, render as litRender, nothing } from "lit-html";
9
9
  import { unified } from "unified";
10
10
  import remarkStringify from "remark-stringify";
11
+ import remarkDirective from "remark-directive";
11
12
  import { stringify as stringifyYaml } from "yaml";
12
13
  import { jxToMd } from "../markdown/md-convert.js";
13
14
  import { createState, projectState, setProjectState } from "../store.js";
@@ -105,14 +106,26 @@ export async function openProject({ S, commit, renderActivityBar, renderLeftPane
105
106
  await loadDirectory(".");
106
107
  await loadComponentRegistry();
107
108
 
108
- // Auto-expand key directories
109
+ // Auto-expand key directories and populate projectDirs for Browse view
110
+ const conventionalDirs = [
111
+ "pages",
112
+ "layouts",
113
+ "components",
114
+ "content",
115
+ "data",
116
+ "public",
117
+ "styles",
118
+ ];
109
119
  const entries = projectState.dirs.get(".") || [];
120
+ const foundDirs = [];
110
121
  for (const e of entries) {
111
- if (e.type === "directory" && ["pages", "components", "layouts"].includes(e.name)) {
122
+ if (e.type === "directory" && conventionalDirs.includes(e.name)) {
123
+ foundDirs.push(e.name);
112
124
  projectState.expanded.add(e.path || e.name);
113
125
  await loadDirectory(e.path || e.name);
114
126
  }
115
127
  }
128
+ projectState.projectDirs = foundDirs;
116
129
 
117
130
  commit({ ...S, ui: { ...S.ui, leftTab: "files" } });
118
131
  renderActivityBar();
@@ -468,8 +481,9 @@ async function createNewFile(dirPath = ".", /** @type {() => void} */ renderLeft
468
481
  async function renameFile(/** @type {any} */ entry, /** @type {() => void} */ renderLeftPanel) {
469
482
  const newName = prompt("New name:", entry.name);
470
483
  if (!newName || newName === entry.name) return;
471
- const parentDir = entry.path.includes("/")
472
- ? entry.path.substring(0, entry.path.lastIndexOf("/"))
484
+ const entryPath = entry.path.replaceAll("\\", "/");
485
+ const parentDir = entryPath.includes("/")
486
+ ? entryPath.substring(0, entryPath.lastIndexOf("/"))
473
487
  : ".";
474
488
  const newPath = parentDir === "." ? newName : `${parentDir}/${newName}`;
475
489
  try {
@@ -491,9 +505,8 @@ async function deleteFile(/** @type {any} */ entry, /** @type {() => void} */ re
491
505
  try {
492
506
  const platform = getPlatform();
493
507
  await platform.deleteFile(entry.path);
494
- const parentDir = entry.path.includes("/")
495
- ? entry.path.substring(0, entry.path.lastIndexOf("/"))
496
- : ".";
508
+ const delPath = entry.path.replaceAll("\\", "/");
509
+ const parentDir = delPath.includes("/") ? delPath.substring(0, delPath.lastIndexOf("/")) : ".";
497
510
  await loadDirectory(parentDir);
498
511
  if (projectState.selectedPath === entry.path) {
499
512
  projectState.selectedPath = null;
@@ -528,6 +541,7 @@ export async function openFileFromTree(ctx, path) {
528
541
  if (isContent) {
529
542
  const mdast = jxToMd(ctx.S.document);
530
543
  const md = unified()
544
+ .use(remarkDirective)
531
545
  .use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
532
546
  .stringify(mdast);
533
547
  const fm = ctx.S.content?.frontmatter;
@@ -548,7 +562,7 @@ export async function openFileFromTree(ctx, path) {
548
562
  if (!content) return;
549
563
 
550
564
  if (path.endsWith(".md")) {
551
- ctx.loadMarkdown(content, null);
565
+ await ctx.loadMarkdown(content, null);
552
566
  ctx.S.documentPath = path;
553
567
  } else {
554
568
  const doc = JSON.parse(content);