@jxsuite/studio 0.16.1 → 0.18.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.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Head editor — structured form for managing $head entries (link, meta, script, style tags) with
3
+ * Monaco editor for script/style bodies.
4
+ */
5
+
6
+ import { html, render as litRender, nothing } from "lit-html";
7
+ import { projectState } from "../store.js";
8
+ import { updateSiteConfig } from "../site-context.js";
9
+
10
+ /** @param {HTMLElement} container */
11
+ export function renderHeadEditor(container) {
12
+ const config = projectState.projectConfig || {};
13
+ const headEntries = config.$head || [];
14
+
15
+ const save = () => {
16
+ updateSiteConfig({ $head: headEntries });
17
+ };
18
+
19
+ const addEntry = (/** @type {string} */ tag) => {
20
+ /** @type {any} */
21
+ const entry = { tag };
22
+ if (tag === "link") {
23
+ entry.rel = "stylesheet";
24
+ entry.href = "";
25
+ } else if (tag === "meta") {
26
+ entry.name = "";
27
+ entry.content = "";
28
+ } else if (tag === "script") {
29
+ entry.src = "";
30
+ entry.body = "";
31
+ } else if (tag === "style") {
32
+ entry.body = "";
33
+ }
34
+ headEntries.push(entry);
35
+ save();
36
+ renderHeadEditor(container);
37
+ };
38
+
39
+ const removeEntry = (/** @type {number} */ idx) => {
40
+ headEntries.splice(idx, 1);
41
+ save();
42
+ renderHeadEditor(container);
43
+ };
44
+
45
+ const updateEntry = (
46
+ /** @type {number} */ idx,
47
+ /** @type {string} */ key,
48
+ /** @type {string} */ val,
49
+ ) => {
50
+ headEntries[idx][key] = val;
51
+ save();
52
+ };
53
+
54
+ const tpl = html`
55
+ <div class="settings-section">
56
+ <h3 class="settings-section-title">Head</h3>
57
+ <p class="settings-field-desc">
58
+ Manage global &lt;head&gt; tags — stylesheets, meta tags, scripts, and inline styles.
59
+ </p>
60
+
61
+ <div class="head-entries">
62
+ ${headEntries.map(
63
+ (/** @type {any} */ entry, /** @type {number} */ idx) => html`
64
+ <div class="head-entry">
65
+ <div class="head-entry-header">
66
+ <span class="head-entry-tag">&lt;${entry.tag}&gt;</span>
67
+ <sp-action-button quiet size="s" @click=${() => removeEntry(idx)}>
68
+ <sp-icon-delete slot="icon"></sp-icon-delete>
69
+ </sp-action-button>
70
+ </div>
71
+ <div class="head-entry-fields">${renderEntryFields(entry, idx, updateEntry)}</div>
72
+ </div>
73
+ `,
74
+ )}
75
+ </div>
76
+
77
+ <div class="head-add-actions" style="margin-top:12px;display:flex;gap:8px">
78
+ <sp-action-button size="s" @click=${() => addEntry("link")}> + Link </sp-action-button>
79
+ <sp-action-button size="s" @click=${() => addEntry("meta")}> + Meta </sp-action-button>
80
+ <sp-action-button size="s" @click=${() => addEntry("script")}> + Script </sp-action-button>
81
+ <sp-action-button size="s" @click=${() => addEntry("style")}> + Style </sp-action-button>
82
+ </div>
83
+ </div>
84
+ `;
85
+
86
+ litRender(tpl, container);
87
+ }
88
+
89
+ /**
90
+ * @param {any} entry
91
+ * @param {number} idx
92
+ * @param {(idx: number, key: string, val: string) => void} updateEntry
93
+ */
94
+ function renderEntryFields(entry, idx, updateEntry) {
95
+ /** @type {any} */
96
+ let debounce;
97
+ const onFieldChange = (/** @type {string} */ key) => (/** @type {any} */ e) => {
98
+ clearTimeout(debounce);
99
+ debounce = setTimeout(() => {
100
+ updateEntry(idx, key, e.target.value);
101
+ }, 300);
102
+ };
103
+
104
+ switch (entry.tag) {
105
+ case "link":
106
+ return html`
107
+ <div class="settings-field-row">
108
+ <sp-textfield
109
+ size="s"
110
+ label="rel"
111
+ .value=${entry.rel || ""}
112
+ @change=${onFieldChange("rel")}
113
+ ></sp-textfield>
114
+ <sp-textfield
115
+ size="s"
116
+ label="href"
117
+ .value=${entry.href || ""}
118
+ @change=${onFieldChange("href")}
119
+ style="flex:1"
120
+ ></sp-textfield>
121
+ </div>
122
+ `;
123
+ case "meta":
124
+ return html`
125
+ <div class="settings-field-row">
126
+ <sp-textfield
127
+ size="s"
128
+ label="name"
129
+ .value=${entry.name || ""}
130
+ @change=${onFieldChange("name")}
131
+ ></sp-textfield>
132
+ <sp-textfield
133
+ size="s"
134
+ label="content"
135
+ .value=${entry.content || ""}
136
+ @change=${onFieldChange("content")}
137
+ style="flex:1"
138
+ ></sp-textfield>
139
+ </div>
140
+ `;
141
+ case "script":
142
+ return html`
143
+ <div class="settings-field-row">
144
+ <sp-textfield
145
+ size="s"
146
+ label="src"
147
+ .value=${entry.src || ""}
148
+ @change=${onFieldChange("src")}
149
+ placeholder="URL or leave empty for inline"
150
+ style="flex:1"
151
+ ></sp-textfield>
152
+ </div>
153
+ ${!entry.src
154
+ ? html`
155
+ <div class="head-entry-body">
156
+ <label class="settings-field-label">Script body</label>
157
+ <textarea
158
+ class="head-code-editor"
159
+ .value=${entry.body || ""}
160
+ @input=${onFieldChange("body")}
161
+ rows="6"
162
+ spellcheck="false"
163
+ ></textarea>
164
+ </div>
165
+ `
166
+ : nothing}
167
+ `;
168
+ case "style":
169
+ return html`
170
+ <div class="head-entry-body">
171
+ <label class="settings-field-label">Style body</label>
172
+ <textarea
173
+ class="head-code-editor"
174
+ .value=${entry.body || ""}
175
+ @input=${onFieldChange("body")}
176
+ rows="8"
177
+ spellcheck="false"
178
+ ></textarea>
179
+ </div>
180
+ `;
181
+ default:
182
+ return nothing;
183
+ }
184
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Settings modal — site-wide project settings (CSS variables, definitions, content types, head,
3
+ * general). Modeled after VS Code / Obsidian settings panels: left sidebar nav + right content
4
+ * area.
5
+ */
6
+
7
+ import { html, render as litRender } from "lit-html";
8
+ import { ref } from "lit-html/directives/ref.js";
9
+ import { renderDefsEditor } from "./defs-editor.js";
10
+ import { renderContentTypesEditor } from "./content-types-editor.js";
11
+ import { renderCssVarsEditor } from "./css-vars-editor.js";
12
+ import { renderHeadEditor } from "./head-editor.js";
13
+ import { renderGeneralSettings } from "./general-settings.js";
14
+
15
+ /** @type {HTMLElement | null} */
16
+ let _host = null;
17
+
18
+ /** @type {string} */
19
+ let _activeSection = "general";
20
+
21
+ const sections = [
22
+ { key: "general", label: "General", icon: "sp-icon-properties" },
23
+ { key: "head", label: "Head", icon: "sp-icon-file-single-web-page" },
24
+ { key: "cssVars", label: "CSS Variables", icon: "sp-icon-brush" },
25
+ { key: "definitions", label: "Definitions", icon: "sp-icon-data" },
26
+ { key: "contentTypes", label: "Content Types", icon: "sp-icon-view-grid" },
27
+ ];
28
+
29
+ export function openSettingsModal() {
30
+ if (_host) return;
31
+
32
+ _host = document.createElement("div");
33
+ _host.style.display = "contents";
34
+ const themeRoot = document.querySelector("sp-theme") || document.body;
35
+ themeRoot.appendChild(_host);
36
+ _activeSection = "general";
37
+ renderModal();
38
+ }
39
+
40
+ export function closeSettingsModal() {
41
+ if (!_host) return;
42
+ _host.remove();
43
+ _host = null;
44
+ }
45
+
46
+ function renderModal() {
47
+ if (!_host) return;
48
+
49
+ const onNavClick = (/** @type {string} */ key) => {
50
+ _activeSection = key;
51
+ renderModal();
52
+ renderActiveSection();
53
+ };
54
+
55
+ const tpl = html`
56
+ <sp-underlay open @close=${closeSettingsModal}></sp-underlay>
57
+ <div
58
+ class="settings-modal"
59
+ @keydown=${(/** @type {KeyboardEvent} */ e) => {
60
+ if (e.key === "Escape") closeSettingsModal();
61
+ }}
62
+ >
63
+ <div class="settings-modal-header">
64
+ <h2 class="settings-modal-title">Settings</h2>
65
+ <sp-action-button quiet size="s" @click=${closeSettingsModal} title="Close">
66
+ <sp-icon-close slot="icon"></sp-icon-close>
67
+ </sp-action-button>
68
+ </div>
69
+ <div class="settings-modal-body">
70
+ <nav class="settings-modal-nav">
71
+ ${sections.map(
72
+ (s) => html`
73
+ <button
74
+ class="settings-nav-item${_activeSection === s.key ? " active" : ""}"
75
+ @click=${() => onNavClick(s.key)}
76
+ >
77
+ ${s.label}
78
+ </button>
79
+ `,
80
+ )}
81
+ </nav>
82
+ <div
83
+ class="settings-modal-content"
84
+ ${ref((el) => {
85
+ if (el) requestAnimationFrame(() => renderActiveSection());
86
+ })}
87
+ ></div>
88
+ </div>
89
+ </div>
90
+ `;
91
+
92
+ litRender(tpl, _host);
93
+ }
94
+
95
+ function renderActiveSection() {
96
+ if (!_host) return;
97
+ const container = _host.querySelector(".settings-modal-content");
98
+ if (!container) return;
99
+
100
+ switch (_activeSection) {
101
+ case "general":
102
+ renderGeneralSettings(/** @type {HTMLElement} */ (container));
103
+ break;
104
+ case "head":
105
+ renderHeadEditor(/** @type {HTMLElement} */ (container));
106
+ break;
107
+ case "cssVars":
108
+ renderCssVarsEditor(/** @type {HTMLElement} */ (container));
109
+ break;
110
+ case "definitions":
111
+ renderDefsEditor(/** @type {HTMLElement} */ (container));
112
+ break;
113
+ case "contentTypes":
114
+ renderContentTypesEditor(/** @type {HTMLElement} */ (container));
115
+ break;
116
+ }
117
+ }
package/src/studio.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  updateUi,
17
17
  } from "./store.js";
18
18
 
19
- import { activeTab, openTab } from "./workspace/workspace.js";
19
+ import { activeTab, openTab, replaceAllTabs } from "./workspace/workspace.js";
20
20
  import { transactDoc, mutateUpdateDef, mutateUpdateProperty } from "./tabs/transact.js";
21
21
  import { effect } from "./reactivity.js";
22
22
 
@@ -35,19 +35,14 @@ import {
35
35
  setStatusbarRenderer,
36
36
  mountStatusbar,
37
37
  } from "./panels/statusbar.js";
38
- import {
39
- openFile,
40
- loadMarkdown,
41
- saveFile,
42
- exportFile,
43
- serializeDocument,
44
- } from "./files/file-ops.js";
38
+ import { loadMarkdown, saveFile, exportFile, serializeDocument } from "./files/file-ops.js";
45
39
  import {
46
40
  loadProject as _loadProject,
47
41
  openProject as _openProject,
48
42
  renderFilesTemplate as _renderFilesTemplate,
49
43
  openFileInTab,
50
44
  setupTreeKeyboard,
45
+ loadDirectory,
51
46
  } from "./files/files.js";
52
47
  import { renderImportsTemplate } from "./panels/imports-panel.js";
53
48
  import { renderHeadTemplate } from "./panels/head-panel.js";
@@ -86,6 +81,8 @@ import { renderBlockActionBar, initBlockActionBar } from "./panels/block-action-
86
81
  import { initCssData } from "./panels/style-utils.js";
87
82
  import { updateForcedPseudoPreview } from "./panels/pseudo-preview.js";
88
83
  import { initPanelEvents } from "./panels/panel-events.js";
84
+ import { initQuickSearch } from "./panels/quick-search.js";
85
+ import { addRecentProject } from "./recent-projects.js";
89
86
 
90
87
  // ─── Globals ──────────────────────────────────────────────────────────────────
91
88
  // These mutable variables are local to studio.js for now. As sections are extracted
@@ -260,7 +257,7 @@ toolbarPanel.mount(toolbarEl, {
260
257
  navigateBack: () => navigateBack(),
261
258
  closeFunctionEditor: () => closeFunctionEditor(),
262
259
  openProject: () => openProject(),
263
- openFile: () => openFile(),
260
+ openRecentProject: (/** @type {string} */ root) => openRecentProject(root),
264
261
  saveFile: () => saveFile(),
265
262
  parseMediaEntries,
266
263
  getCanvasMode,
@@ -269,6 +266,8 @@ toolbarPanel.mount(toolbarEl, {
269
266
  safeRenderRightPanel: () => safeRenderRightPanel(),
270
267
  });
271
268
 
269
+ initQuickSearch();
270
+
272
271
  tabStrip.mount(/** @type {HTMLElement} */ (document.querySelector("#tab-strip")));
273
272
 
274
273
  overlaysPanel.mount({
@@ -543,6 +542,56 @@ function openProject() {
543
542
  renderLeftPanel,
544
543
  });
545
544
  }
545
+ async function openRecentProject(/** @type {string} */ root) {
546
+ try {
547
+ const platform = getPlatform();
548
+ platform.projectRoot = root;
549
+ const content = await platform.readFile("project.json");
550
+ const config = JSON.parse(content);
551
+
552
+ replaceAllTabs({ id: "initial", document: { tagName: "div", children: [] } });
553
+
554
+ setProjectState({
555
+ ...projectState,
556
+ projectRoot: root,
557
+ isSiteProject: true,
558
+ projectConfig: config,
559
+ name: config.name || root.split("/").pop(),
560
+ dirs: new Map(),
561
+ expanded: new Set(),
562
+ selectedPath: null,
563
+ searchQuery: "",
564
+ });
565
+
566
+ await loadDirectory(".");
567
+ await loadComponentRegistry();
568
+
569
+ const conventionalDirs = [
570
+ "pages",
571
+ "layouts",
572
+ "components",
573
+ "content",
574
+ "data",
575
+ "public",
576
+ "styles",
577
+ ];
578
+ const entries = projectState.dirs.get(".") || [];
579
+ for (const e of entries) {
580
+ if (e.type === "directory" && conventionalDirs.includes(e.name)) {
581
+ projectState.expanded.add(e.path || e.name);
582
+ await loadDirectory(e.path || e.name);
583
+ }
584
+ }
585
+
586
+ addRecentProject(projectState.name, root);
587
+ view.leftTab = "files";
588
+ renderActivityBar();
589
+ renderLeftPanel();
590
+ statusMessage(`Opened project: ${projectState.name}`);
591
+ } catch (/** @type {any} */ e) {
592
+ statusMessage(`Error: ${e.message}`);
593
+ }
594
+ }
546
595
  function renderFilesTemplate() {
547
596
  return _renderFilesTemplate({ openProject, openFileFromTree, renderLeftPanel });
548
597
  }
@@ -114,6 +114,7 @@ import { IconDistributeHorizontalCenter } from "@spectrum-web-components/icons-w
114
114
  import { IconTextBaselineShift } from "@spectrum-web-components/icons-workflow/src/elements/IconTextBaselineShift.js";
115
115
  import { IconFlipVertical } from "@spectrum-web-components/icons-workflow/src/elements/IconFlipVertical.js";
116
116
  import { IconRemove } from "@spectrum-web-components/icons-workflow/src/elements/IconRemove.js";
117
+ import { IconFullScreen } from "@spectrum-web-components/icons-workflow/src/elements/IconFullScreen.js";
117
118
  import { IconViewColumn } from "@spectrum-web-components/icons-workflow/src/elements/IconViewColumn.js";
118
119
  import { IconBox } from "@spectrum-web-components/icons-workflow/src/elements/IconBox.js";
119
120
  import { IconVisibility } from "@spectrum-web-components/icons-workflow/src/elements/IconVisibility.js";
@@ -244,6 +245,7 @@ const components = [
244
245
  ["sp-icon-text-baseline-shift", IconTextBaselineShift],
245
246
  ["sp-icon-flip-vertical", IconFlipVertical],
246
247
  ["sp-icon-remove", IconRemove],
248
+ ["sp-icon-full-screen", IconFullScreen],
247
249
  ["sp-icon-download", IconDownload],
248
250
  ["sp-icon-checkmark", IconCheckmark],
249
251
  ["sp-icon-view-column", IconViewColumn],