@jxsuite/studio 0.31.0 → 0.32.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.
package/dist/studio.js CHANGED
@@ -195544,6 +195544,7 @@ function findContentTypeSchema(documentPath, projectConfig) {
195544
195544
  if (!documentPath || !projectConfig?.contentTypes) {
195545
195545
  return null;
195546
195546
  }
195547
+ const docPath = documentPath.replaceAll("\\", "/");
195547
195548
  for (const [name, def3] of Object.entries(projectConfig.contentTypes)) {
195548
195549
  if (!def3.source || !def3.schema) {
195549
195550
  continue;
@@ -195551,12 +195552,12 @@ function findContentTypeSchema(documentPath, projectConfig) {
195551
195552
  const src = def3.source.replace(/^\.\//, "").replace(/\/$/, "");
195552
195553
  const hasExt = src.includes(".") && !src.endsWith("/");
195553
195554
  if (hasExt) {
195554
- if (documentPath === src || documentPath.endsWith(`/${src}`)) {
195555
+ if (docPath === src || docPath.endsWith(`/${src}`)) {
195555
195556
  return { name, schema: def3.schema };
195556
195557
  }
195557
195558
  } else {
195558
195559
  const ext = def3.format === "json" ? ".json" : formatByName(def3.format)?.extensions[0] ?? defaultContentFormat()?.extensions[0] ?? ".json";
195559
- if (documentPath.startsWith(`${src}/`) && documentPath.endsWith(ext)) {
195560
+ if (docPath.startsWith(`${src}/`) && docPath.endsWith(ext)) {
195560
195561
  return { name, schema: def3.schema };
195561
195562
  }
195562
195563
  }
@@ -201543,6 +201544,7 @@ async function loadMediaCache() {
201543
201544
  m.path = m.path.replace(/^\/public\//, "/");
201544
201545
  }
201545
201546
  mediaCacheLoaded = true;
201547
+ renderOnly("leftPanel", "rightPanel");
201546
201548
  }
201547
201549
  function invalidateMediaCache() {
201548
201550
  mediaCache = [];
@@ -203717,45 +203719,108 @@ var init_file_ops = __esm(() => {
203717
203719
  });
203718
203720
 
203719
203721
  // src/recent-projects.ts
203720
- function getRecentProjects() {
203722
+ function backend() {
203723
+ if (!hasPlatform()) {
203724
+ return null;
203725
+ }
203726
+ const platform3 = getPlatform();
203727
+ return platform3.getRecentProjects && platform3.saveRecentProjects ? platform3 : null;
203728
+ }
203729
+ function loadFromLocalStorage() {
203721
203730
  try {
203722
203731
  const raw = localStorage.getItem(STORAGE_KEY);
203723
203732
  if (!raw) {
203724
203733
  return [];
203725
203734
  }
203726
- return JSON.parse(raw).toSorted((a, b) => b.timestamp - a.timestamp);
203735
+ const parsed = JSON.parse(raw);
203736
+ return Array.isArray(parsed) ? parsed : [];
203727
203737
  } catch {
203728
203738
  return [];
203729
203739
  }
203730
203740
  }
203741
+ function currentList() {
203742
+ return backend() ? cache : loadFromLocalStorage();
203743
+ }
203744
+ function commit(list2) {
203745
+ cache = list2;
203746
+ const store = backend();
203747
+ if (store) {
203748
+ store.saveRecentProjects(list2);
203749
+ } else {
203750
+ try {
203751
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(list2));
203752
+ } catch {}
203753
+ }
203754
+ }
203755
+ async function hydrateRecentProjects() {
203756
+ const store = backend();
203757
+ if (!store) {
203758
+ return;
203759
+ }
203760
+ try {
203761
+ cache = await store.getRecentProjects();
203762
+ } catch {
203763
+ cache = [];
203764
+ }
203765
+ }
203766
+ function getRecentProjects() {
203767
+ return currentList().toSorted((a, b) => b.timestamp - a.timestamp);
203768
+ }
203731
203769
  function addRecentProject(name, root) {
203732
- const projects = getRecentProjects().filter((p) => p.root !== root);
203770
+ const projects = currentList().filter((p) => p.root !== root);
203733
203771
  projects.unshift({ name, root, timestamp: Date.now() });
203734
203772
  if (projects.length > MAX_RECENT) {
203735
203773
  projects.length = MAX_RECENT;
203736
203774
  }
203737
- localStorage.setItem(STORAGE_KEY, JSON.stringify(projects));
203775
+ commit(projects);
203776
+ }
203777
+ function removeRecentProject(root) {
203778
+ commit(currentList().filter((p) => p.root !== root));
203738
203779
  }
203739
- function getRecentFiles() {
203780
+ function clearRecentProjects() {
203781
+ cache = [];
203782
+ const store = backend();
203783
+ if (store) {
203784
+ store.saveRecentProjects([]);
203785
+ } else {
203786
+ localStorage.removeItem(STORAGE_KEY);
203787
+ }
203788
+ }
203789
+ function loadRecentFiles() {
203740
203790
  try {
203741
203791
  const raw = localStorage.getItem(FILES_STORAGE_KEY);
203742
203792
  if (!raw) {
203743
203793
  return [];
203744
203794
  }
203745
- return JSON.parse(raw).toSorted((a, b) => b.timestamp - a.timestamp);
203795
+ const parsed = JSON.parse(raw);
203796
+ return Array.isArray(parsed) ? parsed : [];
203746
203797
  } catch {
203747
203798
  return [];
203748
203799
  }
203749
203800
  }
203801
+ function getRecentFiles(root) {
203802
+ const all = loadRecentFiles().toSorted((a, b) => b.timestamp - a.timestamp);
203803
+ return root == null ? all : all.filter((f) => f.root === root);
203804
+ }
203750
203805
  function trackRecentFile(file) {
203751
- const recent = getRecentFiles().filter((f) => f.path !== file.path);
203752
- recent.unshift({ name: file.name, path: file.path, timestamp: Date.now() });
203753
- if (recent.length > MAX_RECENT_FILES) {
203754
- recent.length = MAX_RECENT_FILES;
203806
+ const all = loadRecentFiles().filter((f) => !(f.root === file.root && f.path === file.path));
203807
+ all.unshift({ name: file.name, path: file.path, root: file.root, timestamp: Date.now() });
203808
+ const perRoot = new Map;
203809
+ const kept = [];
203810
+ for (const f of all.toSorted((a, b) => b.timestamp - a.timestamp)) {
203811
+ const n2 = (perRoot.get(f.root) ?? 0) + 1;
203812
+ perRoot.set(f.root, n2);
203813
+ if (n2 <= MAX_RECENT_FILES) {
203814
+ kept.push(f);
203815
+ }
203755
203816
  }
203756
- localStorage.setItem(FILES_STORAGE_KEY, JSON.stringify(recent));
203817
+ localStorage.setItem(FILES_STORAGE_KEY, JSON.stringify(kept));
203757
203818
  }
203758
- var STORAGE_KEY = "jx-studio-recent-projects", FILES_STORAGE_KEY = "jx-studio-recent-files", MAX_RECENT = 8, MAX_RECENT_FILES = 10;
203819
+ var STORAGE_KEY = "jx-studio-recent-projects", FILES_STORAGE_KEY = "jx-studio-recent-files", MAX_RECENT = 8, MAX_RECENT_FILES = 10, cache;
203820
+ var init_recent_projects = __esm(() => {
203821
+ init_platform3();
203822
+ cache = [];
203823
+ });
203759
203824
 
203760
203825
  // ../../node_modules/lit-html/development/directives/repeat.js
203761
203826
  var generateMap = (list2, start2, end) => {
@@ -217800,7 +217865,7 @@ function startLayerTitleEdit(path, rerender) {
217800
217865
  input.remove();
217801
217866
  label.style.display = "";
217802
217867
  };
217803
- const commit = () => {
217868
+ const commit2 = () => {
217804
217869
  if (committed) {
217805
217870
  return;
217806
217871
  }
@@ -217818,7 +217883,7 @@ function startLayerTitleEdit(path, rerender) {
217818
217883
  cleanup();
217819
217884
  rerender();
217820
217885
  };
217821
- input.addEventListener("blur", commit);
217886
+ input.addEventListener("blur", commit2);
217822
217887
  input.addEventListener("keydown", (e) => {
217823
217888
  if (e.key === "Enter") {
217824
217889
  e.preventDefault();
@@ -218565,7 +218630,7 @@ var init_ensure_deps = __esm(() => {
218565
218630
  });
218566
218631
 
218567
218632
  // src/version.ts
218568
- var APP_NAME = "Jx Studio", VERSION = "0.31.0", BUILD_DATE = "2026-06-24T16:30:57.146Z", GIT_COMMIT = "1abc394", LINKS;
218633
+ var APP_NAME = "Jx Studio", VERSION = "0.32.0", BUILD_DATE = "2026-06-24T21:38:38.074Z", GIT_COMMIT = "d2ad6ce", LINKS;
218569
218634
  var init_version = __esm(() => {
218570
218635
  LINKS = {
218571
218636
  github: "https://github.com/jxsuite/jx",
@@ -218859,6 +218924,7 @@ async function loadProject() {
218859
218924
  selectedPath: null
218860
218925
  });
218861
218926
  if (info.isSiteProject) {
218927
+ addRecentProject(requireProjectState().name, meta.root);
218862
218928
  await ensureDependenciesInstalled();
218863
218929
  await loadDirectory(".");
218864
218930
  await loadComponentRegistry();
@@ -219571,7 +219637,11 @@ async function openFileInTab(path) {
219571
219637
  sourceFormat: format3?.name ?? null
219572
219638
  });
219573
219639
  requireProjectState().selectedPath = path;
219574
- trackRecentFile({ name: path.split("/").pop() || path, path });
219640
+ trackRecentFile({
219641
+ name: path.split("/").pop() || path,
219642
+ path,
219643
+ root: requireProjectState().projectRoot
219644
+ });
219575
219645
  statusMessage(`Opened ${path.split("/").pop()}`);
219576
219646
  } catch (error) {
219577
219647
  statusMessage(`Error: ${errorMessage(error)}`);
@@ -219628,6 +219698,7 @@ var init_files2 = __esm(() => {
219628
219698
  init_file_ops();
219629
219699
  init_format_host();
219630
219700
  init_view3();
219701
+ init_recent_projects();
219631
219702
  fileIconMap = {
219632
219703
  "sp-icon-document": html3`<sp-icon-document></sp-icon-document>`,
219633
219704
  "sp-icon-file-code": html3`<sp-icon-file-code></sp-icon-file-code>`,
@@ -220516,17 +220587,17 @@ var desc = (obj, name, descriptor) => {
220516
220587
  };
220517
220588
 
220518
220589
  // ../../node_modules/@lit/reactive-element/development/decorators/query.js
220519
- function query(selector, cache) {
220590
+ function query(selector, cache2) {
220520
220591
  return (protoOrTarget, nameOrContext, descriptor) => {
220521
220592
  const doQuery = (el) => {
220522
220593
  const result = el.renderRoot?.querySelector(selector) ?? null;
220523
- if (DEV_MODE6 && result === null && cache && !el.hasUpdated) {
220594
+ if (DEV_MODE6 && result === null && cache2 && !el.hasUpdated) {
220524
220595
  const name = typeof nameOrContext === "object" ? nameOrContext.name : nameOrContext;
220525
220596
  issueWarning5("", `@query'd field ${JSON.stringify(String(name))} with the 'cache' ` + `flag set for selector '${selector}' has been accessed before ` + `the first update and returned null. This is expected if the ` + `renderRoot tree has not been provided beforehand (e.g. via ` + `Declarative Shadow DOM). Therefore the value hasn't been cached.`);
220526
220597
  }
220527
220598
  return result;
220528
220599
  };
220529
- if (cache) {
220600
+ if (cache2) {
220530
220601
  const { get, set } = typeof nameOrContext === "object" ? protoOrTarget : descriptor ?? (() => {
220531
220602
  const key = DEV_MODE6 ? Symbol(`${String(nameOrContext)} (@query() cache)`) : Symbol();
220532
220603
  return {
@@ -224224,8 +224295,8 @@ function hasFixedPositionAncestor(element2, stopNode) {
224224
224295
  }
224225
224296
  return getComputedStyle3(parentNode).position === "fixed" || hasFixedPositionAncestor(parentNode, stopNode);
224226
224297
  }
224227
- function getClippingElementAncestors(element2, cache) {
224228
- const cachedResult = cache.get(element2);
224298
+ function getClippingElementAncestors(element2, cache2) {
224299
+ const cachedResult = cache2.get(element2);
224229
224300
  if (cachedResult) {
224230
224301
  return cachedResult;
224231
224302
  }
@@ -224247,7 +224318,7 @@ function getClippingElementAncestors(element2, cache) {
224247
224318
  }
224248
224319
  currentNode = getParentNode(currentNode);
224249
224320
  }
224250
- cache.set(element2, result);
224321
+ cache2.set(element2, result);
224251
224322
  return result;
224252
224323
  }
224253
224324
  function getClippingRect(_ref) {
@@ -224524,14 +224595,14 @@ var noOffsets, SCROLLBAR_MAX = 25, absoluteOrFixed, getElementRects = async func
224524
224595
  }
224525
224596
  };
224526
224597
  }, platform3, offset2, shift3, flip2, size3, arrow2, computePosition2 = (reference, floating, options) => {
224527
- const cache = new Map;
224598
+ const cache2 = new Map;
224528
224599
  const mergedOptions = {
224529
224600
  platform: platform3,
224530
224601
  ...options
224531
224602
  };
224532
224603
  const platformWithCache = {
224533
224604
  ...mergedOptions.platform,
224534
- _c: cache
224605
+ _c: cache2
224535
224606
  };
224536
224607
  return computePosition(reference, floating, {
224537
224608
  ...mergedOptions,
@@ -254247,6 +254318,8 @@ init_format_host();
254247
254318
 
254248
254319
  // src/panels/welcome-screen.ts
254249
254320
  init_lit_html();
254321
+ init_recent_projects();
254322
+ init_store();
254250
254323
  init_git_panel();
254251
254324
  var _ctx4 = null;
254252
254325
  function initWelcome(ctx) {
@@ -254310,16 +254383,39 @@ function renderWelcome(host) {
254310
254383
 
254311
254384
  ${recent.length > 0 ? html3`
254312
254385
  <div class="welcome-section">
254313
- <h2 class="welcome-section-title">Recent</h2>
254386
+ <div class="welcome-section-header">
254387
+ <h2 class="welcome-section-title">Recent</h2>
254388
+ <button
254389
+ class="welcome-clear"
254390
+ @click=${() => {
254391
+ clearRecentProjects();
254392
+ renderOnly("canvas");
254393
+ }}
254394
+ >
254395
+ Clear
254396
+ </button>
254397
+ </div>
254314
254398
  ${recent.map((p) => html3`
254315
- <button
254316
- class="welcome-recent"
254317
- @click=${() => ctx.openRecentProject(p.root)}
254318
- title=${p.root}
254319
- >
254320
- <span class="welcome-recent-name">${p.name}</span>
254321
- <span class="welcome-recent-path">${shortenPath(p.root)}</span>
254322
- </button>
254399
+ <div class="welcome-recent-row">
254400
+ <button
254401
+ class="welcome-recent"
254402
+ @click=${() => ctx.openRecentProject(p.root)}
254403
+ title=${p.root}
254404
+ >
254405
+ <span class="welcome-recent-name">${p.name}</span>
254406
+ <span class="welcome-recent-path">${shortenPath(p.root)}</span>
254407
+ </button>
254408
+ <button
254409
+ class="welcome-recent-remove"
254410
+ title="Remove from recent"
254411
+ @click=${() => {
254412
+ removeRecentProject(p.root);
254413
+ renderOnly("canvas");
254414
+ }}
254415
+ >
254416
+
254417
+ </button>
254418
+ </div>
254323
254419
  `)}
254324
254420
  </div>
254325
254421
  ` : nothing}
@@ -256744,15 +256840,15 @@ function isShallowEqual(a, b) {
256744
256840
  }
256745
256841
  function stable() {
256746
256842
  var isEqual2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : isShallowEqual;
256747
- var cache = null;
256843
+ var cache2 = null;
256748
256844
  return function(value) {
256749
- if (cache && isEqual2(cache.value, value)) {
256750
- return cache.value;
256845
+ if (cache2 && isEqual2(cache2.value, value)) {
256846
+ return cache2.value;
256751
256847
  }
256752
- cache = {
256848
+ cache2 = {
256753
256849
  value
256754
256850
  };
256755
- return cache.value;
256851
+ return cache2.value;
256756
256852
  };
256757
256853
  }
256758
256854
 
@@ -260646,11 +260742,11 @@ function renderMetaFieldRow(field, head, applyMutation) {
260646
260742
  })
260647
260743
  });
260648
260744
  }
260649
- const commit = (v) => applyMutation((d2) => upsertMeta(d2, field.attr, field.key, v.trim()));
260745
+ const commit2 = (v) => applyMutation((d2) => upsertMeta(d2, field.attr, field.key, v.trim()));
260650
260746
  const placeholder = field.key === "viewport" ? "width=device-width, initial-scale=1" : `${field.label}…`;
260651
- const widget = field.multiline ? spTextArea(`head:${field.key}`, val, commit, {
260747
+ const widget = field.multiline ? spTextArea(`head:${field.key}`, val, commit2, {
260652
260748
  placeholder: `${field.label}…`
260653
- }) : spTextField(`head:${field.key}`, val, commit, { placeholder });
260749
+ }) : spTextField(`head:${field.key}`, val, commit2, { placeholder });
260654
260750
  return renderFieldRow({
260655
260751
  hasValue: Boolean(val),
260656
260752
  label: field.label,
@@ -287020,9 +287116,12 @@ init_class_map();
287020
287116
  init_live();
287021
287117
  init_ref();
287022
287118
  init_platform3();
287119
+ init_store();
287023
287120
  init_format_host();
287024
287121
  init_files2();
287122
+ init_recent_projects();
287025
287123
  init_layers();
287124
+ var _ctx12 = null;
287026
287125
  var _open = false;
287027
287126
  var _query = "";
287028
287127
  var _results = [];
@@ -287031,7 +287130,9 @@ var _debounceTimer = 0;
287031
287130
  function getContainer() {
287032
287131
  return getLayerSlot("popover", "quick-search");
287033
287132
  }
287034
- function initQuickSearch() {}
287133
+ function initQuickSearch(ctx) {
287134
+ _ctx12 = ctx ?? null;
287135
+ }
287035
287136
  function openQuickSearch() {
287036
287137
  _open = true;
287037
287138
  _query = "";
@@ -287043,6 +287144,28 @@ function closeQuickSearch() {
287043
287144
  _open = false;
287044
287145
  renderOverlay();
287045
287146
  }
287147
+ function scopeRoot() {
287148
+ return projectState ? projectState.projectRoot ?? null : null;
287149
+ }
287150
+ function currentItems() {
287151
+ const q = _query.trim();
287152
+ if (!projectState) {
287153
+ const needle = q.toLowerCase();
287154
+ const projects = getRecentProjects().filter((p2) => !needle || p2.name.toLowerCase().includes(needle) || p2.root.toLowerCase().includes(needle));
287155
+ return {
287156
+ items: projects.map((p2) => ({ kind: "project", name: p2.name, root: p2.root })),
287157
+ showingRecent: !q
287158
+ };
287159
+ }
287160
+ if (!q) {
287161
+ const recent = getRecentFiles(scopeRoot() ?? undefined);
287162
+ return {
287163
+ items: recent.map((f) => ({ kind: "file", name: f.name, path: f.path })),
287164
+ showingRecent: true
287165
+ };
287166
+ }
287167
+ return { items: _results, showingRecent: false };
287168
+ }
287046
287169
  async function doSearch(query3) {
287047
287170
  if (!query3.trim()) {
287048
287171
  _results = [];
@@ -287053,7 +287176,12 @@ async function doSearch(query3) {
287053
287176
  try {
287054
287177
  const platform4 = getPlatform();
287055
287178
  await loadFormats();
287056
- _results = await platform4.searchFiles(query3.trim().toLowerCase(), documentExtensions());
287179
+ const hits = await platform4.searchFiles(query3.trim().toLowerCase(), documentExtensions());
287180
+ _results = hits.map((h3) => ({
287181
+ kind: "file",
287182
+ name: h3.name ?? h3.path.split("/").pop() ?? "",
287183
+ path: h3.path
287184
+ }));
287057
287185
  _selectedIndex = 0;
287058
287186
  renderOverlay();
287059
287187
  } catch {
@@ -287064,11 +287192,14 @@ async function doSearch(query3) {
287064
287192
  function onInput(e20) {
287065
287193
  _query = e20.target.value;
287066
287194
  clearTimeout(_debounceTimer);
287067
- _debounceTimer = setTimeout(() => doSearch(_query), 150);
287195
+ if (projectState) {
287196
+ _debounceTimer = setTimeout(() => doSearch(_query), 150);
287197
+ }
287198
+ _selectedIndex = 0;
287068
287199
  renderOverlay();
287069
287200
  }
287070
287201
  function onKeydown2(e20) {
287071
- const items = _query.trim() ? _results : getRecentFiles();
287202
+ const { items } = currentItems();
287072
287203
  switch (e20.key) {
287073
287204
  case "ArrowDown": {
287074
287205
  e20.preventDefault();
@@ -287101,9 +287232,12 @@ function onKeydown2(e20) {
287101
287232
  }
287102
287233
  function selectItem(item) {
287103
287234
  closeQuickSearch();
287104
- const { path } = item;
287105
- trackRecentFile({ name: path.split("/").pop() || "", path });
287106
- openFileInTab(path);
287235
+ if (item.kind === "project") {
287236
+ _ctx12?.openRecentProject(item.root);
287237
+ return;
287238
+ }
287239
+ trackRecentFile({ name: item.name, path: item.path, root: scopeRoot() ?? "" });
287240
+ openFileInTab(item.path);
287107
287241
  }
287108
287242
  function fileIcon(name) {
287109
287243
  const ext = name.split(".").pop()?.toLowerCase();
@@ -287120,22 +287254,31 @@ function dirPart(path) {
287120
287254
  parts2.pop();
287121
287255
  return parts2.length > 0 ? parts2.join("/") : "";
287122
287256
  }
287257
+ function shortenPath2(path) {
287258
+ if (path.startsWith("/home/")) {
287259
+ return `~/${path.split("/").slice(3).join("/")}`;
287260
+ }
287261
+ return path;
287262
+ }
287123
287263
  function renderOverlay() {
287124
287264
  const container = getContainer();
287125
287265
  if (!_open) {
287126
287266
  render2(nothing, container);
287127
287267
  return;
287128
287268
  }
287129
- const recentFiles = getRecentFiles();
287130
- const showRecent = !_query.trim();
287131
- const items = showRecent ? recentFiles : _results;
287269
+ const hasProject = projectState != null;
287270
+ const { items, showingRecent } = currentItems();
287271
+ const hasQuery = _query.trim().length > 0;
287272
+ const placeholder = hasProject ? "Search project files…" : "Open a recent project…";
287273
+ const sectionLabel = hasProject ? "Recently opened" : "Recent projects";
287274
+ const emptyHint = hasProject ? "Type to search project files" : "No recent projects — open one to get started";
287132
287275
  const tpl = html3`
287133
287276
  <div class="quick-search-overlay" @click=${closeQuickSearch}>
287134
287277
  <div class="quick-search-panel" @click=${(e20) => e20.stopPropagation()}>
287135
287278
  <input
287136
287279
  class="quick-search-input"
287137
287280
  type="text"
287138
- placeholder="Search project files…"
287281
+ placeholder=${placeholder}
287139
287282
  .value=${live(_query)}
287140
287283
  @input=${onInput}
287141
287284
  @keydown=${onKeydown2}
@@ -287146,29 +287289,31 @@ function renderOverlay() {
287146
287289
  })}
287147
287290
  />
287148
287291
  <div class="quick-search-results">
287149
- ${items.length === 0 && _query.trim() ? html3`<div class="quick-search-empty">No results</div>` : nothing}
287150
- ${items.length === 0 && !_query.trim() && recentFiles.length === 0 ? html3`<div class="quick-search-empty">Type to search project files</div>` : nothing}
287151
- ${showRecent && recentFiles.length > 0 ? html3`<div class="quick-search-section-label">Recently opened</div>` : nothing}
287152
- ${items.map((item, i7) => html3`
287292
+ ${items.length === 0 && hasQuery ? html3`<div class="quick-search-empty">No results</div>` : nothing}
287293
+ ${items.length === 0 && !hasQuery ? html3`<div class="quick-search-empty">${emptyHint}</div>` : nothing}
287294
+ ${showingRecent && items.length > 0 ? html3`<div class="quick-search-section-label">${sectionLabel}</div>` : nothing}
287295
+ ${items.map((item, i7) => {
287296
+ const icon = item.kind === "project" ? html3`<sp-icon-folder-open size="s"></sp-icon-folder-open>` : fileIcon(item.name);
287297
+ const pathText = item.kind === "project" ? shortenPath2(item.root) : dirPart(item.path);
287298
+ return html3`
287153
287299
  <div
287154
287300
  class=${classMap({
287155
- "quick-search-item": true,
287156
- selected: i7 === _selectedIndex
287157
- })}
287301
+ "quick-search-item": true,
287302
+ selected: i7 === _selectedIndex
287303
+ })}
287158
287304
  @click=${() => selectItem(item)}
287159
287305
  @mouseenter=${() => {
287160
- _selectedIndex = i7;
287161
- renderOverlay();
287162
- }}
287306
+ _selectedIndex = i7;
287307
+ renderOverlay();
287308
+ }}
287163
287309
  >
287164
- <span class="quick-search-icon"
287165
- >${fileIcon(item.name || item.path.split("/").pop() || "")}</span
287166
- >
287167
- <span class="quick-search-name">${item.name || item.path.split("/").pop()}</span>
287168
- <span class="quick-search-path">${dirPart(item.path)}</span>
287169
- ${showRecent ? html3`<span class="quick-search-badge">recent</span>` : nothing}
287310
+ <span class="quick-search-icon">${icon}</span>
287311
+ <span class="quick-search-name">${item.name}</span>
287312
+ <span class="quick-search-path">${pathText}</span>
287313
+ ${showingRecent ? html3`<span class="quick-search-badge">recent</span>` : nothing}
287170
287314
  </div>
287171
- `)}
287315
+ `;
287316
+ })}
287172
287317
  </div>
287173
287318
  </div>
287174
287319
  </div>
@@ -287734,6 +287879,7 @@ init_transact();
287734
287879
  init_reactivity();
287735
287880
  init_workspace2();
287736
287881
  init_view3();
287882
+ init_recent_projects();
287737
287883
  init_platform3();
287738
287884
  init_git_panel();
287739
287885
 
@@ -288645,7 +288791,7 @@ var _dirDerived = true;
288645
288791
 
288646
288792
  // src/panels/toolbar.ts
288647
288793
  var _rootEl = null;
288648
- var _ctx12 = null;
288794
+ var _ctx13 = null;
288649
288795
  var _scope4 = null;
288650
288796
  var toolbarIconMap = {
288651
288797
  "sp-icon-artboard": html3`<sp-icon-artboard slot="icon"></sp-icon-artboard>`,
@@ -288665,14 +288811,14 @@ var toolbarIconMap = {
288665
288811
  };
288666
288812
  function tbBtnTpl(label, onClick, iconTag) {
288667
288813
  return html3`
288668
- <sp-action-button size="s" @click=${onClick}>
288669
- ${iconTag ? toolbarIconMap[iconTag] : nothing} ${label}
288814
+ <sp-action-button size="s" title=${label} @click=${onClick}>
288815
+ ${iconTag ? toolbarIconMap[iconTag] : nothing}<span class="tb-label">${label}</span>
288670
288816
  </sp-action-button>
288671
288817
  `;
288672
288818
  }
288673
288819
  function mount5(rootEl, ctx) {
288674
288820
  _rootEl = rootEl;
288675
- _ctx12 = ctx;
288821
+ _ctx13 = ctx;
288676
288822
  if (globalThis.__jxPlatform?.windowControls) {
288677
288823
  rootEl.classList.add("electrobun-webkit-app-region-drag");
288678
288824
  }
@@ -288698,7 +288844,7 @@ function mount5(rootEl, ctx) {
288698
288844
  });
288699
288845
  }
288700
288846
  function render6() {
288701
- if (!_rootEl || !_ctx12) {
288847
+ if (!_rootEl || !_ctx13) {
288702
288848
  return;
288703
288849
  }
288704
288850
  try {
@@ -288709,13 +288855,13 @@ function render6() {
288709
288855
  }
288710
288856
  async function handleNewProject() {
288711
288857
  const result = await openNewProjectModal();
288712
- if (result && _ctx12) {
288713
- await _ctx12.openRecentProject(result.root);
288858
+ if (result && _ctx13) {
288859
+ await _ctx13.openRecentProject(result.root);
288714
288860
  }
288715
288861
  }
288716
- function minimalToolbarTemplate(ctx) {
288862
+ function recentMenuTpl(ctx) {
288717
288863
  const recentProjects = getRecentProjects();
288718
- const recentProjectsTpl = html3`
288864
+ return html3`
288719
288865
  <overlay-trigger placement="bottom-start" triggered-by="click">
288720
288866
  <sp-action-button size="s" slot="trigger" title="Recent projects" class="tb-split-trigger">
288721
288867
  <sp-icon-chevron-down slot="icon"></sp-icon-chevron-down>
@@ -288726,17 +288872,45 @@ function minimalToolbarTemplate(ctx) {
288726
288872
  const val = e20.target.value;
288727
288873
  if (val === "__new__") {
288728
288874
  handleNewProject();
288875
+ } else if (val === "__clear__") {
288876
+ clearRecentProjects();
288877
+ render6();
288729
288878
  } else {
288730
288879
  ctx.openRecentProject(val);
288731
288880
  }
288732
288881
  }}
288733
288882
  >
288734
288883
  <sp-menu-item value="__new__">New Project…</sp-menu-item>
288735
- ${recentProjects.length > 0 ? html3`<sp-menu-divider></sp-menu-divider> ${recentProjects.map((p2) => html3`<sp-menu-item value=${p2.root}>${p2.name}</sp-menu-item>`)}` : nothing}
288884
+ ${recentProjects.length > 0 ? html3`
288885
+ <sp-menu-divider></sp-menu-divider>
288886
+ ${recentProjects.map((p2) => html3`
288887
+ <sp-menu-item value=${p2.root} title=${p2.root}>
288888
+ ${p2.name}
288889
+ <sp-action-button
288890
+ slot="end"
288891
+ quiet
288892
+ size="s"
288893
+ title="Remove from recent"
288894
+ @click=${(e20) => {
288895
+ e20.stopPropagation();
288896
+ removeRecentProject(p2.root);
288897
+ render6();
288898
+ }}
288899
+ >
288900
+ <sp-icon-close slot="icon"></sp-icon-close>
288901
+ </sp-action-button>
288902
+ </sp-menu-item>
288903
+ `)}
288904
+ <sp-menu-divider></sp-menu-divider>
288905
+ <sp-menu-item value="__clear__">Clear recent projects</sp-menu-item>
288906
+ ` : nothing}
288736
288907
  </sp-menu>
288737
288908
  </sp-popover>
288738
288909
  </overlay-trigger>
288739
288910
  `;
288911
+ }
288912
+ function minimalToolbarTemplate(ctx) {
288913
+ const recentProjectsTpl = recentMenuTpl(ctx);
288740
288914
  const windowControls = globalThis.__jxPlatform?.windowControls;
288741
288915
  const csdTpl = windowControls ? html3`
288742
288916
  <sp-action-group class="window-controls" size="s">
@@ -288769,33 +288943,44 @@ function minimalToolbarTemplate(ctx) {
288769
288943
  ` : nothing;
288770
288944
  return html3`
288771
288945
  <div class="tb-split-btn">
288772
- <sp-action-button size="s" class="tb-split-main" @click=${ctx.openProject}>
288773
- ${toolbarIconMap["sp-icon-folder-open"]} Open Project
288946
+ <sp-action-button
288947
+ size="s"
288948
+ class="tb-split-main"
288949
+ title="Open Project"
288950
+ @click=${ctx.openProject}
288951
+ >
288952
+ ${toolbarIconMap["sp-icon-folder-open"]}<span class="tb-label">Open Project</span>
288774
288953
  </sp-action-button>
288775
288954
  ${recentProjectsTpl}
288776
288955
  </div>
288777
288956
  ${tbBtnTpl("Manage", openBrowseModal, "sp-icon-view-list")}
288778
- <sp-action-button size="s" disabled>
288779
- ${toolbarIconMap["sp-icon-save-floppy"]} Save
288957
+ <sp-action-button size="s" title="Save" disabled>
288958
+ ${toolbarIconMap["sp-icon-save-floppy"]}<span class="tb-label">Save</span>
288780
288959
  </sp-action-button>
288781
288960
  <sp-action-group compact size="s">
288782
- <sp-action-button size="s" disabled>
288783
- ${toolbarIconMap["sp-icon-undo"]} Undo
288961
+ <sp-action-button size="s" title="Undo" disabled>
288962
+ ${toolbarIconMap["sp-icon-undo"]}<span class="tb-label">Undo</span>
288784
288963
  </sp-action-button>
288785
- <sp-action-button size="s" disabled>
288786
- ${toolbarIconMap["sp-icon-redo"]} Redo
288964
+ <sp-action-button size="s" title="Redo" disabled>
288965
+ ${toolbarIconMap["sp-icon-redo"]}<span class="tb-label">Redo</span>
288787
288966
  </sp-action-button>
288788
288967
  </sp-action-group>
288789
288968
  <div class="tb-spacer"></div>
288790
- <sp-action-button class="tb-search-trigger" size="s" quiet @click=${openQuickSearch}>
288969
+ <sp-action-button
288970
+ class="tb-search-trigger"
288971
+ size="s"
288972
+ quiet
288973
+ title="Search files (⌘P)"
288974
+ @click=${openQuickSearch}
288975
+ >
288791
288976
  <sp-icon-search slot="icon"></sp-icon-search>
288792
288977
  <span class="tb-search-label">Search files… <kbd>⌘P</kbd></span>
288793
288978
  </sp-action-button>
288794
288979
  <div class="tb-spacer"></div>
288795
288980
  <sp-action-group selects="single" size="s" compact>
288796
288981
  ${modes.map((m3) => html3`
288797
- <sp-action-button size="s" disabled ?selected=${m3.key === "design"}>
288798
- ${toolbarIconMap[m3.iconTag]}${m3.label}
288982
+ <sp-action-button size="s" title=${m3.label} disabled ?selected=${m3.key === "design"}>
288983
+ ${toolbarIconMap[m3.iconTag]}<span class="tb-label">${m3.label}</span>
288799
288984
  </sp-action-button>
288800
288985
  `)}
288801
288986
  </sp-action-group>
@@ -288823,10 +289008,10 @@ var modes = [
288823
289008
  ];
288824
289009
  function toolbarTemplate() {
288825
289010
  const tab = activeTab.value;
288826
- if (!_ctx12) {
289011
+ if (!_ctx13) {
288827
289012
  return html3``;
288828
289013
  }
288829
- const ctx = _ctx12;
289014
+ const ctx = _ctx13;
288830
289015
  if (!tab) {
288831
289016
  return minimalToolbarTemplate(ctx);
288832
289017
  }
@@ -288847,6 +289032,7 @@ function toolbarTemplate() {
288847
289032
  ${modes.map((m3) => html3`
288848
289033
  <sp-action-button
288849
289034
  size="s"
289035
+ title=${m3.label}
288850
289036
  ?selected=${canvasMode === m3.key}
288851
289037
  ?disabled=${!allowedModes.has(m3.key)}
288852
289038
  @click=${() => {
@@ -288874,7 +289060,7 @@ function toolbarTemplate() {
288874
289060
  ctx.safeRenderRightPanel();
288875
289061
  }}
288876
289062
  >
288877
- ${toolbarIconMap[m3.iconTag]}${m3.label}
289063
+ ${toolbarIconMap[m3.iconTag]}<span class="tb-label">${m3.label}</span>
288878
289064
  </sp-action-button>
288879
289065
  `)}
288880
289066
  </sp-action-group>
@@ -288942,63 +289128,63 @@ function toolbarTemplate() {
288942
289128
  </sp-action-button>
288943
289129
  </sp-action-group>
288944
289130
  ` : nothing;
288945
- const recentProjects = getRecentProjects();
288946
- const recentProjectsTpl = html3`
288947
- <overlay-trigger placement="bottom-start" triggered-by="click">
288948
- <sp-action-button size="s" slot="trigger" title="Recent projects" class="tb-split-trigger">
288949
- <sp-icon-chevron-down slot="icon"></sp-icon-chevron-down>
288950
- </sp-action-button>
288951
- <sp-popover slot="click-content" tip>
288952
- <sp-menu
288953
- @change=${(e20) => {
288954
- const val = e20.target.value;
288955
- if (val === "__new__") {
288956
- handleNewProject();
288957
- } else {
288958
- ctx.openRecentProject(val);
288959
- }
288960
- }}
288961
- >
288962
- <sp-menu-item value="__new__">New Project…</sp-menu-item>
288963
- ${recentProjects.length > 0 ? html3`<sp-menu-divider></sp-menu-divider> ${recentProjects.map((p2) => html3`<sp-menu-item value=${p2.root}>${p2.name}</sp-menu-item>`)}` : nothing}
288964
- </sp-menu>
288965
- </sp-popover>
288966
- </overlay-trigger>
288967
- `;
289131
+ const recentProjectsTpl = recentMenuTpl(ctx);
288968
289132
  return html3`
288969
289133
  ${isMac2 ? csdTpl : nothing}
288970
289134
  <div class="tb-split-btn">
288971
- <sp-action-button size="s" class="tb-split-main" @click=${ctx.openProject}>
288972
- ${toolbarIconMap["sp-icon-folder-open"]} Open Project
289135
+ <sp-action-button
289136
+ size="s"
289137
+ class="tb-split-main"
289138
+ title="Open Project"
289139
+ @click=${ctx.openProject}
289140
+ >
289141
+ ${toolbarIconMap["sp-icon-folder-open"]}<span class="tb-label">Open Project</span>
288973
289142
  </sp-action-button>
288974
289143
  ${recentProjectsTpl}
288975
289144
  </div>
288976
289145
  ${tbBtnTpl("Manage", openBrowseModal, "sp-icon-view-list")}
288977
- <sp-action-button size="s" ?disabled=${!canSave} @click=${ctx.saveFile}>
288978
- ${toolbarIconMap["sp-icon-save-floppy"]} Save
289146
+ <sp-action-button size="s" title="Save" ?disabled=${!canSave} @click=${ctx.saveFile}>
289147
+ ${toolbarIconMap["sp-icon-save-floppy"]}<span class="tb-label">Save</span>
288979
289148
  </sp-action-button>
288980
289149
  <sp-action-group compact size="s">
288981
- <sp-action-button size="s" ?disabled=${!canUndo} @click=${() => undo(activeTab.value)}>
288982
- ${toolbarIconMap["sp-icon-undo"]} Undo
289150
+ <sp-action-button
289151
+ size="s"
289152
+ title="Undo"
289153
+ ?disabled=${!canUndo}
289154
+ @click=${() => undo(activeTab.value)}
289155
+ >
289156
+ ${toolbarIconMap["sp-icon-undo"]}<span class="tb-label">Undo</span>
288983
289157
  </sp-action-button>
288984
- <sp-action-button size="s" ?disabled=${!canRedo} @click=${() => redo(activeTab.value)}>
288985
- ${toolbarIconMap["sp-icon-redo"]} Redo
289158
+ <sp-action-button
289159
+ size="s"
289160
+ title="Redo"
289161
+ ?disabled=${!canRedo}
289162
+ @click=${() => redo(activeTab.value)}
289163
+ >
289164
+ ${toolbarIconMap["sp-icon-redo"]}<span class="tb-label">Redo</span>
288986
289165
  </sp-action-button>
288987
289166
  </sp-action-group>
288988
289167
  <div class="tb-spacer"></div>
288989
- <sp-action-button class="tb-search-trigger" size="s" quiet @click=${openQuickSearch}>
289168
+ <sp-action-button
289169
+ class="tb-search-trigger"
289170
+ size="s"
289171
+ quiet
289172
+ title="Search files (⌘P)"
289173
+ @click=${openQuickSearch}
289174
+ >
288990
289175
  <sp-icon-search slot="icon"></sp-icon-search>
288991
289176
  <span class="tb-search-label">Search files… <kbd>⌘P</kbd></span>
288992
289177
  </sp-action-button>
288993
289178
  ${(activeTab.value?.session.ui.gitStatus?.behind ?? 0) > 0 ? html3`<sp-action-button
288994
289179
  size="s"
289180
+ title="Sync Project"
288995
289181
  @click=${async () => {
288996
289182
  await getPlatform().gitPull();
288997
289183
  await refreshGitStatus();
288998
289184
  }}
288999
289185
  >
289000
289186
  <sp-icon-download slot="icon"></sp-icon-download>
289001
- Sync Project
289187
+ <span class="tb-label">Sync Project</span>
289002
289188
  </sp-action-button>` : nothing}
289003
289189
  <div class="tb-spacer"></div>
289004
289190
  ${modeSwitcherTpl}
@@ -292537,7 +292723,7 @@ function kvRow(key, value, onChange, onDelete, datalistId = null) {
292537
292723
  let debounceTimer;
292538
292724
  let currentKey = key;
292539
292725
  let currentVal = value;
292540
- const commit = () => {
292726
+ const commit2 = () => {
292541
292727
  clearTimeout(debounceTimer);
292542
292728
  debounceTimer = setTimeout(() => onChange(currentKey, currentVal), 400);
292543
292729
  };
@@ -292550,7 +292736,7 @@ function kvRow(key, value, onChange, onDelete, datalistId = null) {
292550
292736
  .value=${live(key)}
292551
292737
  @input=${(e20) => {
292552
292738
  currentKey = e20.target.value;
292553
- commit();
292739
+ commit2();
292554
292740
  }}
292555
292741
  @change=${datalistId === "css-props" ? (e20) => {
292556
292742
  const el = e20.target.closest(".kv-row")?.querySelector(".kv-val");
@@ -292566,7 +292752,7 @@ function kvRow(key, value, onChange, onDelete, datalistId = null) {
292566
292752
  placeholder=${placeholder}
292567
292753
  @input=${(e20) => {
292568
292754
  currentVal = e20.target.value;
292569
- commit();
292755
+ commit2();
292570
292756
  }}
292571
292757
  ></sp-textfield>
292572
292758
  <sp-action-button size="xs" quiet @click=${onDelete}>
@@ -294455,11 +294641,11 @@ function renderAiPanelTemplate() {
294455
294641
  }
294456
294642
 
294457
294643
  // src/panels/right-panel.ts
294458
- var _ctx13 = null;
294644
+ var _ctx14 = null;
294459
294645
  var _scope5 = null;
294460
294646
  var _scheduler = null;
294461
294647
  function mount6(ctx) {
294462
- _ctx13 = ctx;
294648
+ _ctx14 = ctx;
294463
294649
  mountAiPanel();
294464
294650
  registerRightPanelRender(render7);
294465
294651
  _scheduler = createPanelScheduler({
@@ -294511,11 +294697,11 @@ function _ensureContainers() {
294511
294697
  _assistantContainer.style.cssText = "display:flex;flex-direction:column;overflow:hidden";
294512
294698
  }
294513
294699
  function _doRender() {
294514
- if (!_ctx13) {
294700
+ if (!_ctx14) {
294515
294701
  return;
294516
294702
  }
294517
294703
  try {
294518
- const ctx = _ctx13;
294704
+ const ctx = _ctx14;
294519
294705
  const aTab = activeTab.value;
294520
294706
  if (!aTab) {
294521
294707
  rightPanel.textContent = "";
@@ -294597,7 +294783,7 @@ function _doRender() {
294597
294783
  console.error("right-panel render error:", error);
294598
294784
  }
294599
294785
  requestAnimationFrame(() => mountQuikChat());
294600
- _ctx13.updateForcedPseudoPreview();
294786
+ _ctx14.updateForcedPseudoPreview();
294601
294787
  }
294602
294788
 
294603
294789
  // src/panels/left-panel.ts
@@ -294819,11 +295005,11 @@ function renderElementsTemplate(ctx) {
294819
295005
  }
294820
295006
 
294821
295007
  // src/panels/left-panel.ts
294822
- var _ctx14 = null;
295008
+ var _ctx15 = null;
294823
295009
  var _scope6 = null;
294824
295010
  var _scheduler2 = null;
294825
295011
  function mount7(ctx) {
294826
- _ctx14 = ctx;
295012
+ _ctx15 = ctx;
294827
295013
  _scheduler2 = createPanelScheduler({ render: _doRender2, root: leftPanel });
294828
295014
  _scheduler2.bindFocus();
294829
295015
  _scope6 = effectScope();
@@ -294847,7 +295033,7 @@ function render8() {
294847
295033
  _scheduler2?.schedule();
294848
295034
  }
294849
295035
  function _doRender2() {
294850
- if (!_ctx14) {
295036
+ if (!_ctx15) {
294851
295037
  return;
294852
295038
  }
294853
295039
  try {
@@ -294879,7 +295065,7 @@ function buildHeadDoc(doc, fm) {
294879
295065
  };
294880
295066
  }
294881
295067
  function _render() {
294882
- const ctx = _ctx14;
295068
+ const ctx = _ctx15;
294883
295069
  const tab = view.leftTab;
294884
295070
  if (tab === "files") {
294885
295071
  render2(html3`<div class="panel-body">${ctx.renderFilesTemplate()}</div>`, leftPanel);
@@ -295087,11 +295273,11 @@ init_reactivity();
295087
295273
  init_workspace2();
295088
295274
  init_site_context();
295089
295275
  var _host2 = null;
295090
- var _ctx15 = null;
295276
+ var _ctx16 = null;
295091
295277
  var _scope8 = null;
295092
295278
  function mount9(host, ctx) {
295093
295279
  _host2 = host;
295094
- _ctx15 = ctx;
295280
+ _ctx16 = ctx;
295095
295281
  _scope8 = effectScope();
295096
295282
  _scope8.run(() => {
295097
295283
  effect(() => {
@@ -295110,11 +295296,11 @@ function mount9(host, ctx) {
295110
295296
  });
295111
295297
  }
295112
295298
  function render10() {
295113
- if (!_host2 || !_ctx15) {
295299
+ if (!_host2 || !_ctx16) {
295114
295300
  return;
295115
295301
  }
295116
295302
  try {
295117
- render2(tabBarTemplate(_ctx15), _host2);
295303
+ render2(tabBarTemplate(_ctx16), _host2);
295118
295304
  } catch (error) {
295119
295305
  console.error("tab-bar render error:", error);
295120
295306
  }
@@ -295206,7 +295392,9 @@ function tabBarTemplate(ctx) {
295206
295392
  </div>
295207
295393
  `;
295208
295394
  }
295395
+
295209
295396
  // src/studio.ts
295397
+ init_recent_projects();
295210
295398
  function getCanvasMode() {
295211
295399
  return activeTab.value?.session.ui.canvasMode ?? "design";
295212
295400
  }
@@ -295380,7 +295568,7 @@ mount5(toolbarEl, {
295380
295568
  setCanvasMode
295381
295569
  });
295382
295570
  initLayers();
295383
- initQuickSearch();
295571
+ initQuickSearch({ openRecentProject: (root2) => openRecentProject(root2) });
295384
295572
  mount8(document.querySelector("#tab-strip"));
295385
295573
  mount9(document.querySelector("#tab-bar"), {
295386
295574
  closeFunctionEditor: () => closeFunctionEditor(),
@@ -295650,6 +295838,10 @@ if (_projectParam) {
295650
295838
  render();
295651
295839
  ensureFsSync();
295652
295840
  }
295841
+ hydrateRecentProjects().then(() => {
295842
+ render6();
295843
+ render();
295844
+ });
295653
295845
  function renderLeftPanel() {
295654
295846
  render8();
295655
295847
  }
@@ -295720,6 +295912,9 @@ async function openRecentProject(root2) {
295720
295912
  ensureFsSync();
295721
295913
  maybePromptJxsuiteUpdate(root2);
295722
295914
  } catch (error) {
295915
+ removeRecentProject(root2);
295916
+ render6();
295917
+ render();
295723
295918
  statusMessage(`Error: ${errorMessage(error)}`);
295724
295919
  }
295725
295920
  }
@@ -295787,5 +295982,5 @@ effect(() => {
295787
295982
  }
295788
295983
  });
295789
295984
 
295790
- //# debugId=BA04C2B472069B1A64756E2164756E21
295985
+ //# debugId=AE7238B52581CB6F64756E2164756E21
295791
295986
  //# sourceMappingURL=studio.js.map