@eclipse-lyra/core 0.7.6 → 0.7.7

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/api/index.js +28 -29
  2. package/dist/api/services.d.ts +0 -4
  3. package/dist/api/services.d.ts.map +1 -1
  4. package/dist/api/types.d.ts +1 -1
  5. package/dist/api/types.d.ts.map +1 -1
  6. package/dist/components/fastviews.d.ts +1 -1
  7. package/dist/components/index.d.ts.map +1 -1
  8. package/dist/components/{app-switcher.d.ts → layout-switcher.d.ts} +5 -4
  9. package/dist/components/layout-switcher.d.ts.map +1 -0
  10. package/dist/{standard-layout-Efok-voU.js → config-BiRvaEoO.js} +243 -454
  11. package/dist/config-BiRvaEoO.js.map +1 -0
  12. package/dist/contributions/default-layout-contributions.d.ts +1 -0
  13. package/dist/contributions/default-layout-contributions.d.ts.map +1 -0
  14. package/dist/contributions/index.d.ts.map +1 -1
  15. package/dist/core/apploader.d.ts +40 -30
  16. package/dist/core/apploader.d.ts.map +1 -1
  17. package/dist/core/constants.d.ts +1 -0
  18. package/dist/core/constants.d.ts.map +1 -1
  19. package/dist/core/contributionregistry.d.ts +9 -0
  20. package/dist/core/contributionregistry.d.ts.map +1 -1
  21. package/dist/core/index.d.ts.map +1 -1
  22. package/dist/icon-DN6fp0dg.js.map +1 -1
  23. package/dist/index.js +28 -29
  24. package/dist/parts/contextmenu.d.ts +1 -1
  25. package/dist/parts/index.js +1 -1
  26. package/dist/parts/resizable-grid.d.ts +1 -1
  27. package/dist/{resizable-grid-BP9wOk_x.js → resizable-grid-oWYRVx30.js} +315 -94
  28. package/dist/resizable-grid-oWYRVx30.js.map +1 -0
  29. package/dist/vite-plugin-resolve-deps.d.ts +18 -0
  30. package/dist/vite-plugin-resolve-deps.d.ts.map +1 -0
  31. package/dist/widgets/icon.d.ts +1 -1
  32. package/package.json +8 -1
  33. package/src/api/services.ts +0 -4
  34. package/src/api/types.ts +1 -0
  35. package/src/commands/version-info.ts +24 -10
  36. package/src/components/index.ts +1 -1
  37. package/src/components/layout-switcher.ts +83 -0
  38. package/src/contributions/default-layout-contributions.ts +10 -0
  39. package/src/contributions/default-ui-contributions.ts +1 -1
  40. package/src/contributions/index.ts +1 -0
  41. package/src/contributions/marketplace-catalog-contributions.ts +1 -1
  42. package/src/core/apploader.ts +182 -99
  43. package/src/core/constants.ts +1 -0
  44. package/src/core/contributionregistry.ts +7 -0
  45. package/src/core/index.ts +0 -1
  46. package/src/vite-env.d.ts +9 -0
  47. package/src/vite-plugin-resolve-deps.ts +112 -0
  48. package/dist/components/app-selector.d.ts +0 -17
  49. package/dist/components/app-selector.d.ts.map +0 -1
  50. package/dist/components/app-switcher.d.ts.map +0 -1
  51. package/dist/core/app-host-config.d.ts +0 -7
  52. package/dist/core/app-host-config.d.ts.map +0 -1
  53. package/dist/core/packageinfoservice.d.ts +0 -16
  54. package/dist/core/packageinfoservice.d.ts.map +0 -1
  55. package/dist/resizable-grid-BP9wOk_x.js.map +0 -1
  56. package/dist/standard-layout-Efok-voU.js.map +0 -1
  57. package/src/components/app-selector.ts +0 -233
  58. package/src/components/app-switcher.ts +0 -126
  59. package/src/core/app-host-config.ts +0 -23
  60. package/src/core/packageinfoservice.ts +0 -56
@@ -15,6 +15,7 @@ const TOOLBAR_BOTTOM = "app-toolbars-bottom";
15
15
  const TOOLBAR_BOTTOM_CENTER = "app-toolbars-bottom-center";
16
16
  const TOOLBAR_BOTTOM_END = "app-toolbars-bottom-end";
17
17
  const SYSTEM_VIEWS = "system-views";
18
+ const SYSTEM_LAYOUTS = "system.layouts";
18
19
  const EDITOR_AREA_MAIN = "editor-area-main";
19
20
  const SIDEBAR_MAIN = "sidebar-main";
20
21
  const SIDEBAR_MAIN_BOTTOM = "sidebar-main-bottom";
@@ -40,6 +41,7 @@ const constants = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePro
40
41
  SIDEBAR_AUXILIARY,
41
42
  SIDEBAR_MAIN,
42
43
  SIDEBAR_MAIN_BOTTOM,
44
+ SYSTEM_LAYOUTS,
43
45
  SYSTEM_VIEWS,
44
46
  TOOLBAR_BOTTOM,
45
47
  TOOLBAR_BOTTOM_CENTER,
@@ -275,7 +277,7 @@ class TaskService {
275
277
  }
276
278
  const taskService = new TaskService();
277
279
  rootContext.put("taskService", taskService);
278
- const logger$2 = createLogger("EsmShService");
280
+ const logger$3 = createLogger("EsmShService");
279
281
  const _EsmShService = class _EsmShService {
280
282
  isEsmShUrl(url) {
281
283
  try {
@@ -492,7 +494,7 @@ const _EsmShService = class _EsmShService {
492
494
  }
493
495
  const parsed = this.parseSource(source);
494
496
  if (!parsed) {
495
- logger$2.warn(`Could not parse source identifier: ${source}`);
497
+ logger$3.warn(`Could not parse source identifier: ${source}`);
496
498
  return source;
497
499
  }
498
500
  return this.buildEsmShUrl(parsed, options);
@@ -1193,7 +1195,7 @@ _LyraDialogContent.styles = [
1193
1195
  `
1194
1196
  ];
1195
1197
  let LyraDialogContent = _LyraDialogContent;
1196
- const logger$1 = createLogger("DialogService");
1198
+ const logger$2 = createLogger("DialogService");
1197
1199
  const DIALOG_CONTRIBUTION_TARGET = "dialogs";
1198
1200
  const OK_BUTTON = {
1199
1201
  id: "ok",
@@ -1234,26 +1236,26 @@ class DialogService {
1234
1236
  this.contributions.clear();
1235
1237
  for (const contribution of contributions) {
1236
1238
  if (!contribution.id) {
1237
- logger$1.warn("Dialog contribution missing id, skipping");
1239
+ logger$2.warn("Dialog contribution missing id, skipping");
1238
1240
  continue;
1239
1241
  }
1240
1242
  if (!contribution.component) {
1241
- logger$1.warn(`Dialog contribution "${contribution.id}" has no component function, skipping`);
1243
+ logger$2.warn(`Dialog contribution "${contribution.id}" has no component function, skipping`);
1242
1244
  continue;
1243
1245
  }
1244
1246
  if (!contribution.onButton) {
1245
- logger$1.warn(`Dialog contribution "${contribution.id}" has no onButton callback, skipping`);
1247
+ logger$2.warn(`Dialog contribution "${contribution.id}" has no onButton callback, skipping`);
1246
1248
  continue;
1247
1249
  }
1248
1250
  this.contributions.set(contribution.id, contribution);
1249
- logger$1.debug(`Loaded dialog contribution: ${contribution.id}`);
1251
+ logger$2.debug(`Loaded dialog contribution: ${contribution.id}`);
1250
1252
  }
1251
- logger$1.info(`Loaded ${this.contributions.size} dialog contributions`);
1253
+ logger$2.info(`Loaded ${this.contributions.size} dialog contributions`);
1252
1254
  }
1253
1255
  async open(dialogId, state2) {
1254
1256
  const contribution = this.contributions.get(dialogId);
1255
1257
  if (!contribution) {
1256
- logger$1.error(`Dialog "${dialogId}" not found`);
1258
+ logger$2.error(`Dialog "${dialogId}" not found`);
1257
1259
  throw new Error(`Dialog "${dialogId}" not found`);
1258
1260
  }
1259
1261
  return new Promise((resolve) => {
@@ -1268,7 +1270,7 @@ class DialogService {
1268
1270
  await dialogContentElement.dispose();
1269
1271
  } catch (error) {
1270
1272
  const errorMessage = error instanceof Error ? error.message : String(error);
1271
- logger$1.error(`Error disposing dialog content for "${dialogId}": ${errorMessage}`);
1273
+ logger$2.error(`Error disposing dialog content for "${dialogId}": ${errorMessage}`);
1272
1274
  }
1273
1275
  }
1274
1276
  try {
@@ -1276,7 +1278,7 @@ class DialogService {
1276
1278
  await contribution.onButton("close", result, stateWithClose);
1277
1279
  } catch (error) {
1278
1280
  const errorMessage = error instanceof Error ? error.message : String(error);
1279
- logger$1.error(`Error executing close callback for dialog "${dialogId}": ${errorMessage}`);
1281
+ logger$2.error(`Error executing close callback for dialog "${dialogId}": ${errorMessage}`);
1280
1282
  }
1281
1283
  render(html``, container);
1282
1284
  resolve();
@@ -1290,7 +1292,7 @@ class DialogService {
1290
1292
  }
1291
1293
  } catch (error) {
1292
1294
  const errorMessage = error instanceof Error ? error.message : String(error);
1293
- logger$1.error(`Error executing button callback for dialog "${dialogId}": ${errorMessage}`);
1295
+ logger$2.error(`Error executing button callback for dialog "${dialogId}": ${errorMessage}`);
1294
1296
  cleanup();
1295
1297
  }
1296
1298
  };
@@ -1523,6 +1525,152 @@ class LyraPart extends LyraContainer {
1523
1525
  __decorateClass$9([
1524
1526
  property()
1525
1527
  ], LyraPart.prototype, "dirty");
1528
+ const logger$1 = createLogger("MarketplaceRegistry");
1529
+ const TOPIC_MARKETPLACE_CHANGED = "events/marketplaceregistry/changed";
1530
+ const KEY_CATALOG_URLS = "marketplace.catalogUrls";
1531
+ class MarketplaceRegistry {
1532
+ constructor() {
1533
+ this.catalogUrls = [];
1534
+ this.loadingPromises = /* @__PURE__ */ new Map();
1535
+ this.loadCatalogUrls().then(() => {
1536
+ this.refreshCatalogs().catch((err) => {
1537
+ logger$1.error(`Failed to refresh catalogs on init: ${err.message}`);
1538
+ });
1539
+ });
1540
+ }
1541
+ async loadCatalogUrls() {
1542
+ try {
1543
+ const urls = await appSettings.get(KEY_CATALOG_URLS);
1544
+ this.catalogUrls = Array.isArray(urls) ? urls : [];
1545
+ logger$1.debug(`Loaded ${this.catalogUrls.length} catalog URLs`);
1546
+ } catch (error) {
1547
+ logger$1.error(`Failed to load catalog URLs: ${error}`);
1548
+ this.catalogUrls = [];
1549
+ }
1550
+ }
1551
+ async saveCatalogUrls() {
1552
+ await appSettings.set(KEY_CATALOG_URLS, this.catalogUrls);
1553
+ publish(TOPIC_MARKETPLACE_CHANGED, { type: "catalogs", urls: this.catalogUrls });
1554
+ }
1555
+ async addCatalogUrl(url) {
1556
+ if (!this.isValidUrl(url)) {
1557
+ throw new Error(`Invalid catalog URL: ${url}`);
1558
+ }
1559
+ if (this.catalogUrls.includes(url)) {
1560
+ logger$1.debug(`Catalog URL already exists: ${url}`);
1561
+ return;
1562
+ }
1563
+ this.catalogUrls.push(url);
1564
+ await this.saveCatalogUrls();
1565
+ logger$1.info(`Added catalog URL: ${url}`);
1566
+ try {
1567
+ await this.refreshCatalogs();
1568
+ } catch (error) {
1569
+ logger$1.warn(`Failed to refresh catalogs immediately after adding: ${error}`);
1570
+ }
1571
+ }
1572
+ async removeCatalogUrl(url) {
1573
+ const index = this.catalogUrls.indexOf(url);
1574
+ if (index === -1) {
1575
+ return;
1576
+ }
1577
+ this.catalogUrls.splice(index, 1);
1578
+ await this.saveCatalogUrls();
1579
+ logger$1.info(`Removed catalog URL: ${url}`);
1580
+ }
1581
+ getCatalogUrls() {
1582
+ return [...this.catalogUrls];
1583
+ }
1584
+ isValidUrl(url) {
1585
+ try {
1586
+ const parsed = new URL(url);
1587
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
1588
+ } catch {
1589
+ return false;
1590
+ }
1591
+ }
1592
+ async fetchCatalog(url) {
1593
+ const existingPromise = this.loadingPromises.get(url);
1594
+ if (existingPromise) {
1595
+ return existingPromise;
1596
+ }
1597
+ const fetchPromise = (async () => {
1598
+ try {
1599
+ logger$1.debug(`Fetching catalog from: ${url}`);
1600
+ const response = await fetch(url, {
1601
+ method: "GET",
1602
+ headers: {
1603
+ "Accept": "application/json"
1604
+ }
1605
+ });
1606
+ if (!response.ok) {
1607
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1608
+ }
1609
+ const data = await response.json();
1610
+ if (!data.extensions || !Array.isArray(data.extensions)) {
1611
+ throw new Error("Invalid catalog format: extensions array is required");
1612
+ }
1613
+ const catalog = {
1614
+ name: data.name,
1615
+ description: data.description,
1616
+ extensions: data.extensions || []
1617
+ };
1618
+ const extCount = catalog.extensions?.length || 0;
1619
+ logger$1.debug(`Successfully fetched catalog from ${url}: ${extCount} extensions`);
1620
+ return catalog;
1621
+ } catch (error) {
1622
+ logger$1.error(`Failed to fetch catalog from ${url}: ${error}`);
1623
+ throw error;
1624
+ } finally {
1625
+ this.loadingPromises.delete(url);
1626
+ }
1627
+ })();
1628
+ this.loadingPromises.set(url, fetchPromise);
1629
+ return fetchPromise;
1630
+ }
1631
+ async refreshCatalogs() {
1632
+ logger$1.info(`Refreshing ${this.catalogUrls.length} catalogs...`);
1633
+ const promises = this.catalogUrls.map(
1634
+ (url) => this.fetchCatalog(url).catch((error) => {
1635
+ logger$1.warn(`Failed to refresh catalog ${url}: ${error.message}`);
1636
+ return null;
1637
+ })
1638
+ );
1639
+ const catalogs = await Promise.allSettled(promises);
1640
+ catalogs.forEach((result, index) => {
1641
+ if (result.status === "fulfilled" && result.value) {
1642
+ const catalog = result.value;
1643
+ if (catalog.extensions) {
1644
+ catalog.extensions.forEach((marketplaceExt) => {
1645
+ if (!extensionRegistry.getExtensions().find((e) => e.id === marketplaceExt.id)) {
1646
+ const extension = {
1647
+ ...marketplaceExt,
1648
+ external: true
1649
+ };
1650
+ extensionRegistry.registerExtension(extension);
1651
+ logger$1.debug(`Registered marketplace extension: ${marketplaceExt.id}`);
1652
+ }
1653
+ });
1654
+ }
1655
+ }
1656
+ });
1657
+ publish(TOPIC_MARKETPLACE_CHANGED, { type: "refreshed" });
1658
+ logger$1.info("Catalog refresh completed");
1659
+ }
1660
+ getMarketplaceExtension(extensionId) {
1661
+ const extension = extensionRegistry.getExtensions().find((e) => e.id === extensionId);
1662
+ if (extension && extension.external) {
1663
+ return extension;
1664
+ }
1665
+ return void 0;
1666
+ }
1667
+ isMarketplaceExtension(extensionId) {
1668
+ const extension = extensionRegistry.getExtensions().find((e) => e.id === extensionId);
1669
+ return extension !== void 0 && extension.external === true;
1670
+ }
1671
+ }
1672
+ const marketplaceRegistry = new MarketplaceRegistry();
1673
+ rootContext.put("marketplaceRegistry", marketplaceRegistry);
1526
1674
  const logger = createLogger("AppLoader");
1527
1675
  function getErrorMessage(error) {
1528
1676
  return error instanceof Error ? error.message : String(error);
@@ -1564,13 +1712,27 @@ const _AppLoaderService = class _AppLoaderService {
1564
1712
  * @param options - Optional configuration for registration and auto-starting
1565
1713
  */
1566
1714
  registerApp(app, options) {
1567
- if (this.apps.has(app.id)) {
1568
- logger.warn(`App '${app.id}' is already registered. Overwriting.`);
1569
- }
1570
- this.apps.set(app.id, app);
1571
- logger.info(`Registered app: ${app.name} (${app.id}) v${app.version}`);
1572
- if (options?.defaultAppId) {
1573
- this.defaultAppId = options.defaultAppId;
1715
+ if (options?.hostConfig === true && typeof __RESOLVED_PACKAGE_INFO__ !== "undefined") {
1716
+ const resolved = __RESOLVED_PACKAGE_INFO__;
1717
+ if (app.name === void 0) app.name = resolved.name;
1718
+ if (app.version === void 0) app.version = resolved.version;
1719
+ if (app.description === void 0) app.description = resolved.description;
1720
+ if (app.dependencies === void 0) app.dependencies = resolved.dependencies;
1721
+ if (app.marketplaceCatalogUrls === void 0) app.marketplaceCatalogUrls = resolved.marketplaceCatalogUrls;
1722
+ }
1723
+ app.name = app.name ?? "app";
1724
+ app.version = app.version ?? "0.0.0";
1725
+ if (this.apps.has(app.name)) {
1726
+ logger.warn(`App '${app.name}' is already registered. Overwriting.`);
1727
+ }
1728
+ if (app.marketplaceCatalogUrls?.length) {
1729
+ app.marketplaceCatalogUrls.forEach((url) => marketplaceRegistry.addCatalogUrl(url).catch(() => {
1730
+ }));
1731
+ }
1732
+ this.apps.set(app.name, app);
1733
+ logger.info(`Registered app: ${app.name} v${app.version}`);
1734
+ if (options?.defaultAppName) {
1735
+ this.defaultAppName = options.defaultAppName;
1574
1736
  }
1575
1737
  if (options?.container) {
1576
1738
  this.container = options.container;
@@ -1600,10 +1762,10 @@ const _AppLoaderService = class _AppLoaderService {
1600
1762
  throw new Error(`Module at ${url} does not have a default export`);
1601
1763
  }
1602
1764
  const app = module.default;
1603
- if (!app.id || !app.name || !app.version) {
1604
- throw new Error(`Module at ${url} does not export a valid AppDefinition`);
1765
+ if (!app.name || !app.version) {
1766
+ throw new Error(`Module at ${url} does not export a valid AppDefinition (name and version required)`);
1605
1767
  }
1606
- logger.info(`Successfully loaded app definition from URL: ${app.name} (${app.id})`);
1768
+ logger.info(`Successfully loaded app definition from URL: ${app.name}`);
1607
1769
  return app;
1608
1770
  } catch (error) {
1609
1771
  logger.error(`Failed to load app from URL ${url}: ${getErrorMessage(error)}`);
@@ -1613,7 +1775,7 @@ const _AppLoaderService = class _AppLoaderService {
1613
1775
  /**
1614
1776
  * Start the application loader.
1615
1777
  * Checks URL parameters for app=URL, loads that extension or app if found.
1616
- * URL parameter has higher precedence than defaultAppId.
1778
+ * URL parameter has higher precedence than defaultAppName.
1617
1779
  * Then loads the default app or first registered app.
1618
1780
  * This method is idempotent - calling it multiple times only starts once.
1619
1781
  */
@@ -1650,7 +1812,8 @@ const _AppLoaderService = class _AppLoaderService {
1650
1812
  try {
1651
1813
  const app = await this.loadAppFromUrl(appUrl);
1652
1814
  this.registerApp(app);
1653
- await this.loadApp(app.id, this.container);
1815
+ if (!app.name) throw new Error("App from URL has no name after registration");
1816
+ await this.loadApp(app.name, this.container);
1654
1817
  logger.info(`Successfully loaded app from URL: ${appUrl}`);
1655
1818
  return;
1656
1819
  } catch (appError) {
@@ -1673,17 +1836,25 @@ const _AppLoaderService = class _AppLoaderService {
1673
1836
  }
1674
1837
  await this.loadApp(appToLoad, this.container);
1675
1838
  }
1839
+ /**
1840
+ * Resolve a path/URL segment to an app name (map key). Matches app.path, app.name, or name ending with /segment.
1841
+ */
1842
+ findAppNameBySegment(segment) {
1843
+ if (this.apps.has(segment)) return segment;
1844
+ for (const app of this.apps.values()) {
1845
+ if (app.path === segment || app.name && app.name.endsWith("/" + segment)) return app.name ?? void 0;
1846
+ }
1847
+ return void 0;
1848
+ }
1676
1849
  /**
1677
1850
  * Load and initialize an application.
1678
- *
1679
- * @param appId - Application identifier (must be already registered)
1851
+ * @param appName - Application name (must be already registered)
1680
1852
  * @param container - Optional DOM element to render into (if provided, auto-renders after loading)
1681
- * @returns Promise that resolves when app is initialized and rendered
1682
1853
  */
1683
- async loadApp(appId, container) {
1684
- const app = this.apps.get(appId);
1854
+ async loadApp(appName, container) {
1855
+ const app = this.apps.get(appName);
1685
1856
  if (!app) {
1686
- throw new Error(`App '${appId}' not found. Make sure it's registered.`);
1857
+ throw new Error(`App '${appName}' not found. Make sure it's registered.`);
1687
1858
  }
1688
1859
  logger.info(`Loading app: ${app.name}...`);
1689
1860
  if (this.currentApp) {
@@ -1731,17 +1902,18 @@ const _AppLoaderService = class _AppLoaderService {
1731
1902
  }
1732
1903
  this.currentApp = app;
1733
1904
  logger.info(`App ${app.name} loaded successfully`);
1905
+ this.preferredLayoutId = await this.getPreferredLayoutId();
1734
1906
  this.updateDocumentMetadata(app);
1735
1907
  if (container) {
1736
1908
  this.renderApp(container);
1737
1909
  }
1738
- window.dispatchEvent(new CustomEvent("app-loaded", { detail: { appId: app.id } }));
1910
+ window.dispatchEvent(new CustomEvent("app-loaded", { detail: { appName: app.name } }));
1739
1911
  }
1740
1912
  /**
1741
1913
  * Updates document title and favicon from app metadata
1742
1914
  */
1743
1915
  updateDocumentMetadata(app) {
1744
- document.title = app.name;
1916
+ document.title = app.name ?? "";
1745
1917
  if (app.metadata?.favicon) {
1746
1918
  const faviconPath = app.metadata.favicon;
1747
1919
  let link = document.querySelector("link[rel*='icon']");
@@ -1756,30 +1928,45 @@ const _AppLoaderService = class _AppLoaderService {
1756
1928
  }
1757
1929
  /**
1758
1930
  * Render the current application to the DOM.
1759
- *
1931
+ * Resolves the layout by layoutId (default 'standard'), renders its component, then calls layout.onShow if defined.
1932
+ *
1760
1933
  * @param container - DOM element to render into
1761
1934
  */
1762
1935
  renderApp(container) {
1763
1936
  if (!this.currentApp) {
1764
1937
  throw new Error("No app loaded. Call loadApp() first.");
1765
1938
  }
1766
- const r = this.currentApp.component;
1939
+ const layoutId = this.preferredLayoutId ?? this.currentApp.layoutId ?? "standard";
1940
+ const layouts = contributionRegistry.getContributions(SYSTEM_LAYOUTS);
1941
+ let layout = layouts.find((c) => c.id === layoutId);
1942
+ if (!layout) {
1943
+ logger.warn(`Layout '${layoutId}' not found, falling back to 'standard'`);
1944
+ layout = layouts.find((c) => c.id === "standard");
1945
+ }
1946
+ if (!layout) {
1947
+ throw new Error(`No layout found for layoutId '${layoutId}' and no 'standard' layout registered.`);
1948
+ }
1949
+ const r = layout.component;
1950
+ container.innerHTML = "";
1767
1951
  if (typeof r === "string") {
1768
- const el = document.createElement(r);
1769
- container.innerHTML = "";
1770
- container.appendChild(el);
1952
+ container.appendChild(document.createElement(r));
1771
1953
  } else if (r && typeof r === "object" && "tag" in r) {
1772
1954
  const el = document.createElement(r.tag);
1773
1955
  for (const [key, value] of Object.entries(r.attributes ?? {})) {
1774
1956
  el.setAttribute(key, value);
1775
1957
  }
1776
- container.innerHTML = "";
1777
1958
  container.appendChild(el);
1778
1959
  } else if (typeof r === "function") {
1779
- const template = r();
1780
- render(template, container);
1960
+ render(r(), container);
1781
1961
  } else {
1782
- render(html`<lyra-standard-layout></lyra-standard-layout>`, container);
1962
+ throw new Error(`Layout '${layout.id}' has invalid component.`);
1963
+ }
1964
+ if (layout.onShow) {
1965
+ requestAnimationFrame(() => {
1966
+ void Promise.resolve(layout.onShow()).catch(
1967
+ (err) => logger.error(`Layout onShow failed for '${layout.id}': ${getErrorMessage(err)}`)
1968
+ );
1969
+ });
1783
1970
  }
1784
1971
  logger.info(`Rendered ${this.currentApp.name}`);
1785
1972
  }
@@ -1815,74 +2002,110 @@ const _AppLoaderService = class _AppLoaderService {
1815
2002
  }
1816
2003
  try {
1817
2004
  await appSettings.set(_AppLoaderService.PREFERRED_APP_KEY, appId);
1818
- this.defaultAppId = appId;
2005
+ this.defaultAppName = appId;
1819
2006
  logger.info(`Set preferred app to: ${appId}`);
1820
2007
  } catch (error) {
1821
- logger.error(`Failed to persist preferred app ID: ${getErrorMessage(error)}`);
2008
+ logger.error(`Failed to persist preferred app: ${getErrorMessage(error)}`);
2009
+ throw error;
2010
+ }
2011
+ }
2012
+ getRegisteredLayouts() {
2013
+ return contributionRegistry.getContributions(SYSTEM_LAYOUTS);
2014
+ }
2015
+ getCurrentLayoutId() {
2016
+ return this.preferredLayoutId ?? this.currentApp?.layoutId ?? "standard";
2017
+ }
2018
+ async getPreferredLayoutId() {
2019
+ try {
2020
+ return await appSettings.get(_AppLoaderService.PREFERRED_LAYOUT_KEY);
2021
+ } catch (error) {
2022
+ logger.debug(`Failed to get preferred layout ID: ${getErrorMessage(error)}`);
2023
+ return void 0;
2024
+ }
2025
+ }
2026
+ async setPreferredLayoutId(layoutId) {
2027
+ const layouts = this.getRegisteredLayouts();
2028
+ if (!layouts.some((l) => l.id === layoutId)) {
2029
+ throw new Error(`Layout '${layoutId}' not found.`);
2030
+ }
2031
+ try {
2032
+ await appSettings.set(_AppLoaderService.PREFERRED_LAYOUT_KEY, layoutId);
2033
+ this.preferredLayoutId = layoutId;
2034
+ logger.info(`Set preferred layout to: ${layoutId}`);
2035
+ if (this.currentApp && this.container) {
2036
+ this.renderApp(this.container);
2037
+ }
2038
+ window.dispatchEvent(new CustomEvent("layout-changed", { detail: { layoutId } }));
2039
+ } catch (error) {
2040
+ logger.error(`Failed to persist preferred layout: ${getErrorMessage(error)}`);
1822
2041
  throw error;
1823
2042
  }
1824
2043
  }
1825
2044
  /**
1826
2045
  * Select which app to load based on priority:
1827
2046
  * 1. appId URL parameter (?appId=...)
1828
- * 2. App ID from current page URL path (/geospace)
1829
- * 3. App ID extracted from app URL parameter (?app=...)
2047
+ * 2. App from current page URL path (/geospace)
2048
+ * 3. App from app URL parameter (?app=...)
1830
2049
  * 4. App registered by extension
1831
- * 5. Preferred app ID from settings
1832
- * 6. Default app ID
2050
+ * 5. Preferred app from settings
2051
+ * 6. Default app
1833
2052
  * 7. First registered app
1834
2053
  */
1835
2054
  async selectAppToLoad(options) {
1836
2055
  const { appIdFromUrl, appIdFromPath, appIdFromAppUrl, appsBeforeExtension } = options;
1837
2056
  if (appIdFromUrl) {
1838
- if (this.apps.has(appIdFromUrl)) {
1839
- logger.info(`Loading app specified by URL parameter 'appId': ${appIdFromUrl}`);
1840
- return appIdFromUrl;
2057
+ const name = this.findAppNameBySegment(appIdFromUrl) ?? appIdFromUrl;
2058
+ if (this.apps.has(name)) {
2059
+ logger.info(`Loading app specified by URL parameter 'appId': ${name}`);
2060
+ return name;
1841
2061
  }
1842
- logger.warn(`App ID '${appIdFromUrl}' from URL parameter not found`);
2062
+ logger.warn(`App '${appIdFromUrl}' from URL parameter not found`);
1843
2063
  }
1844
2064
  if (appIdFromPath) {
1845
- if (this.apps.has(appIdFromPath)) {
2065
+ const name = this.findAppNameBySegment(appIdFromPath);
2066
+ if (name) {
1846
2067
  logger.info(`Loading app from URL path: ${appIdFromPath}`);
1847
- return appIdFromPath;
2068
+ return name;
1848
2069
  }
1849
- logger.debug(`App ID '${appIdFromPath}' from URL path not found, continuing search`);
2070
+ logger.debug(`App for path '${appIdFromPath}' not found, continuing search`);
1850
2071
  }
1851
2072
  if (appIdFromAppUrl) {
1852
- if (this.apps.has(appIdFromAppUrl)) {
1853
- logger.info(`Loading app using ID extracted from app URL path: ${appIdFromAppUrl}`);
1854
- return appIdFromAppUrl;
2073
+ const name = this.findAppNameBySegment(appIdFromAppUrl) ?? appIdFromAppUrl;
2074
+ if (this.apps.has(name)) {
2075
+ logger.info(`Loading app using segment from app URL path: ${name}`);
2076
+ return name;
1855
2077
  }
1856
2078
  }
1857
2079
  if (this.apps.size > appsBeforeExtension) {
1858
2080
  const newlyRegisteredApps = Array.from(this.apps.values()).slice(appsBeforeExtension);
1859
2081
  if (newlyRegisteredApps.length > 0) {
1860
2082
  const app = newlyRegisteredApps[0];
1861
- logger.info(`Loading app registered by extension: ${app.name} (${app.id})`);
1862
- return app.id;
2083
+ logger.info(`Loading app registered by extension: ${app.name}`);
2084
+ return app.name;
1863
2085
  }
1864
2086
  }
1865
- const preferredAppId = await this.getPreferredAppId();
1866
- if (preferredAppId && this.apps.has(preferredAppId)) {
1867
- logger.info(`Loading preferred app from settings: ${preferredAppId}`);
1868
- return preferredAppId;
2087
+ const preferred = await this.getPreferredAppId();
2088
+ if (preferred && this.apps.has(preferred)) {
2089
+ logger.info(`Loading preferred app from settings: ${preferred}`);
2090
+ return preferred;
1869
2091
  }
1870
- if (this.defaultAppId) {
1871
- if (this.apps.has(this.defaultAppId)) {
1872
- return this.defaultAppId;
1873
- }
1874
- logger.warn(`Default app '${this.defaultAppId}' not found`);
2092
+ if (this.defaultAppName && this.apps.has(this.defaultAppName)) {
2093
+ return this.defaultAppName;
2094
+ }
2095
+ if (this.defaultAppName) {
2096
+ logger.warn(`Default app '${this.defaultAppName}' not found`);
1875
2097
  }
1876
2098
  const registeredApps = this.getRegisteredApps();
1877
2099
  if (registeredApps.length > 0) {
1878
2100
  const app = registeredApps[0];
1879
- logger.info(`Loading first registered app: ${app.name} (${app.id})`);
1880
- return app.id;
2101
+ logger.info(`Loading first registered app: ${app.name}`);
2102
+ return app.name;
1881
2103
  }
1882
2104
  return void 0;
1883
2105
  }
1884
2106
  };
1885
- _AppLoaderService.PREFERRED_APP_KEY = "preferredAppId";
2107
+ _AppLoaderService.PREFERRED_APP_KEY = "preferredAppName";
2108
+ _AppLoaderService.PREFERRED_LAYOUT_KEY = "preferredLayoutId";
1886
2109
  let AppLoaderService = _AppLoaderService;
1887
2110
  const appLoaderService = new AppLoaderService();
1888
2111
  rootContext.put("appLoaderService", appLoaderService);
@@ -3775,7 +3998,6 @@ LyraResizableGrid = __decorateClass([
3775
3998
  ], LyraResizableGrid);
3776
3999
  export {
3777
4000
  COMMAND_SAVE as C,
3778
- DIALOG_CONTRIBUTION_TARGET as D,
3779
4001
  EDITOR_AREA_MAIN as E,
3780
4002
  HIDE_DOT_RESOURCE as H,
3781
4003
  LyraContainer as L,
@@ -3788,26 +4010,25 @@ export {
3788
4010
  LyraPart as c,
3789
4011
  SIDEBAR_MAIN as d,
3790
4012
  SIDEBAR_MAIN_BOTTOM as e,
3791
- SYSTEM_VIEWS as f,
3792
- TOOLBAR_BOTTOM_CENTER as g,
3793
- TOOLBAR_BOTTOM_END as h,
3794
- TOOLBAR_MAIN as i,
3795
- TOOLBAR_MAIN_CENTER as j,
3796
- TOOLBAR_MAIN_RIGHT as k,
3797
- TOPIC_SETTINGS_CHANGED as l,
3798
- appLoaderService as m,
3799
- appSettings as n,
3800
- confirmDialog as o,
3801
- esmShService as p,
3802
- extensionRegistry as q,
3803
- infoDialog as r,
3804
- navigableInfoDialog as s,
3805
- persistenceService as t,
3806
- promptDialog as u,
3807
- taskService as v,
3808
- TOPIC_EXTENSIONS_CHANGED as w,
3809
- CLOSE_BUTTON as x,
3810
- dialogService as y,
3811
- constants as z
4013
+ SYSTEM_LAYOUTS as f,
4014
+ SYSTEM_VIEWS as g,
4015
+ TOOLBAR_BOTTOM_CENTER as h,
4016
+ TOOLBAR_BOTTOM_END as i,
4017
+ TOOLBAR_MAIN as j,
4018
+ TOOLBAR_MAIN_CENTER as k,
4019
+ TOOLBAR_MAIN_RIGHT as l,
4020
+ TOPIC_SETTINGS_CHANGED as m,
4021
+ appLoaderService as n,
4022
+ appSettings as o,
4023
+ confirmDialog as p,
4024
+ esmShService as q,
4025
+ extensionRegistry as r,
4026
+ infoDialog as s,
4027
+ navigableInfoDialog as t,
4028
+ persistenceService as u,
4029
+ promptDialog as v,
4030
+ taskService as w,
4031
+ TOPIC_EXTENSIONS_CHANGED as x,
4032
+ constants as y
3812
4033
  };
3813
- //# sourceMappingURL=resizable-grid-BP9wOk_x.js.map
4034
+ //# sourceMappingURL=resizable-grid-oWYRVx30.js.map