@eclipse-docks/core 0.7.126 → 0.7.130

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 (106) hide show
  1. package/dist/api/index.js +4 -4
  2. package/dist/api/services.d.ts +4 -0
  3. package/dist/api/services.d.ts.map +1 -1
  4. package/dist/{api-B3RXoez_.js → api-oPg3Bv1S.js} +422 -293
  5. package/dist/api-oPg3Bv1S.js.map +1 -0
  6. package/dist/components/attribution.d.ts +1 -0
  7. package/dist/components/attribution.d.ts.map +1 -1
  8. package/dist/components/command.d.ts +4 -5
  9. package/dist/components/command.d.ts.map +1 -1
  10. package/dist/components/extensions.d.ts +3 -0
  11. package/dist/components/extensions.d.ts.map +1 -1
  12. package/dist/components/filebrowser.d.ts.map +1 -1
  13. package/dist/components/language-selector.d.ts.map +1 -1
  14. package/dist/components/layout-switcher.d.ts +1 -1
  15. package/dist/components/layout-switcher.d.ts.map +1 -1
  16. package/dist/components/npm-packages.d.ts +1 -0
  17. package/dist/components/npm-packages.d.ts.map +1 -1
  18. package/dist/components/release-history.d.ts +9 -2
  19. package/dist/components/release-history.d.ts.map +1 -1
  20. package/dist/components/tasks.d.ts.map +1 -1
  21. package/dist/core/apploader.d.ts +2 -9
  22. package/dist/core/apploader.d.ts.map +1 -1
  23. package/dist/core/contribution-names.d.ts +0 -2
  24. package/dist/core/contribution-names.d.ts.map +1 -1
  25. package/dist/core/contribution-targets.d.ts +1 -0
  26. package/dist/core/contribution-targets.d.ts.map +1 -1
  27. package/dist/core/contributionregistry.d.ts +3 -0
  28. package/dist/core/contributionregistry.d.ts.map +1 -1
  29. package/dist/core/dialogservice.d.ts +3 -1
  30. package/dist/core/dialogservice.d.ts.map +1 -1
  31. package/dist/core/dropdown-item.d.ts +43 -0
  32. package/dist/core/dropdown-item.d.ts.map +1 -0
  33. package/dist/core/dropdown-menu-utils.d.ts +5 -0
  34. package/dist/core/dropdown-menu-utils.d.ts.map +1 -1
  35. package/dist/core/layout-panels.d.ts +25 -0
  36. package/dist/core/layout-panels.d.ts.map +1 -0
  37. package/dist/dialogs/navigable-info-dialog.d.ts.map +1 -1
  38. package/dist/dialogs/version-info-dialog.d.ts +18 -2
  39. package/dist/dialogs/version-info-dialog.d.ts.map +1 -1
  40. package/dist/externals/lit.js +1 -1
  41. package/dist/externals/webawesome.js +1 -1
  42. package/dist/index.js +4 -4
  43. package/dist/layouts/standard-layout.d.ts +9 -1
  44. package/dist/layouts/standard-layout.d.ts.map +1 -1
  45. package/dist/parts/contextmenu.d.ts +5 -2
  46. package/dist/parts/contextmenu.d.ts.map +1 -1
  47. package/dist/parts/index.d.ts.map +1 -1
  48. package/dist/parts/index.js +1 -1
  49. package/dist/parts/part.d.ts +4 -0
  50. package/dist/parts/part.d.ts.map +1 -1
  51. package/dist/parts/resizable-grid.d.ts +10 -3
  52. package/dist/parts/resizable-grid.d.ts.map +1 -1
  53. package/dist/parts/tabs.d.ts +6 -0
  54. package/dist/parts/tabs.d.ts.map +1 -1
  55. package/dist/{parts-CsoNFVTw.js → parts-CAt8aeU8.js} +1361 -975
  56. package/dist/parts-CAt8aeU8.js.map +1 -0
  57. package/dist/{webawesome-Y1_HnKa_.js → webawesome-BGXn04Mw.js} +18 -10
  58. package/dist/webawesome-BGXn04Mw.js.map +1 -0
  59. package/dist/{widget-DbLusgAH.js → widget-BqQtioK1.js} +11 -5
  60. package/dist/widget-BqQtioK1.js.map +1 -0
  61. package/dist/widgets/index.js +1 -1
  62. package/dist/{widgets-DnnSZhsi.js → widgets-C9V42Mqq.js} +2 -2
  63. package/dist/{widgets-DnnSZhsi.js.map → widgets-C9V42Mqq.js.map} +1 -1
  64. package/package.json +1 -1
  65. package/src/api/services.ts +20 -0
  66. package/src/commands/filebrowser-contributions.ts +0 -16
  67. package/src/components/attribution.ts +2 -0
  68. package/src/components/command.ts +45 -119
  69. package/src/components/extensions.ts +20 -3
  70. package/src/components/filebrowser.ts +15 -12
  71. package/src/components/language-selector.ts +0 -1
  72. package/src/components/layout-switcher.ts +39 -26
  73. package/src/components/npm-packages.ts +2 -0
  74. package/src/components/release-history.ts +29 -40
  75. package/src/components/tasks.ts +0 -1
  76. package/src/contributions/default-layout-contributions.ts +7 -43
  77. package/src/contributions/default-ui-contributions.ts +4 -4
  78. package/src/core/apploader.ts +7 -54
  79. package/src/core/contribution-names.ts +0 -3
  80. package/src/core/contribution-targets.ts +1 -0
  81. package/src/core/contributionregistry.ts +19 -3
  82. package/src/core/dialogservice.ts +76 -19
  83. package/src/core/dropdown-item.ts +187 -0
  84. package/src/core/dropdown-menu-utils.ts +59 -0
  85. package/src/core/layout-panels.ts +121 -0
  86. package/src/dialogs/navigable-info-dialog.ts +5 -2
  87. package/src/dialogs/version-info-dialog.ts +163 -21
  88. package/src/icons/icons.txt +4 -2
  89. package/src/icons/layout-standard-bottom-panel.svg +4 -4
  90. package/src/icons/layout-standard-full.svg +4 -4
  91. package/src/icons/layout-standard-left-aux.svg +8 -0
  92. package/src/icons/layout-standard-left-sidebar.svg +5 -0
  93. package/src/icons/layout-standard-right-aux.svg +8 -0
  94. package/src/icons/layout-standard-right-sidebar.svg +5 -0
  95. package/src/layouts/standard-layout.ts +81 -5
  96. package/src/parts/contextmenu.ts +39 -44
  97. package/src/parts/index.ts +4 -1
  98. package/src/parts/part.ts +26 -0
  99. package/src/parts/resizable-grid.ts +125 -57
  100. package/src/parts/tabs.ts +103 -0
  101. package/dist/api-B3RXoez_.js.map +0 -1
  102. package/dist/parts-CsoNFVTw.js.map +0 -1
  103. package/dist/webawesome-Y1_HnKa_.js.map +0 -1
  104. package/dist/widget-DbLusgAH.js.map +0 -1
  105. package/src/icons/layout-standard-bottom-sidebar.svg +0 -6
  106. package/src/icons/layout-standard.svg +0 -6
@@ -1,19 +1,19 @@
1
1
  import { n as defaultLogger, o as rootContext, s as uiContext, t as createLogger } from "./logger-DT0_NW3S.js";
2
- import { C as toastError, _ as getContributionDisabled, a as TOPIC_COMMAND_REGISTERED, b as contributionTargetMappingRegistry, f as activeTasksSignal, g as contributionRegistry, h as TOPIC_CONTRIBUTEIONS_CHANGED, l as activeEditorSignal, n as watchSignal, o as commandRegistry, p as partDirtySignal, t as DocksWidget, u as activePartSignal, v as getContributionVisible, w as toastInfo } from "./widget-DbLusgAH.js";
2
+ import { T as toastInfo, _ as getContributionDisabled, a as TOPIC_COMMAND_REGISTERED, f as activeTasksSignal, g as contributionRegistry, h as TOPIC_CONTRIBUTEIONS_CHANGED, l as activeEditorSignal, n as watchSignal, o as commandRegistry, p as partDirtySignal, t as DocksWidget, u as activePartSignal, v as getContributionVisible, w as toastError, x as contributionTargetMappingRegistry, y as readReactiveBoolean } from "./widget-BqQtioK1.js";
3
3
  import { n as subscribe, t as publish } from "./events-B_ZxkU_G.js";
4
4
  import { a as File, i as Directory, l as TOPIC_WORKSPACE_CHANGED, m as persistenceService, o as FileContentType, p as workspaceService } from "./fs-access-DTXPsV1W.js";
5
- import "./webawesome-Y1_HnKa_.js";
5
+ import "./webawesome-BGXn04Mw.js";
6
6
  import { css, html, nothing, render } from "lit";
7
7
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
8
8
  import DOMPurify from "dompurify";
9
9
  import { marked } from "marked";
10
10
  import { customElement, property, state } from "lit/decorators.js";
11
11
  import _decorate from "@oxc-project/runtime/helpers/decorate";
12
+ import { ifDefined } from "lit/directives/if-defined.js";
12
13
  import { styleMap } from "lit/directives/style-map.js";
13
14
  import { createRef, ref } from "lit/directives/ref.js";
14
15
  import { when } from "lit/directives/when.js";
15
16
  import { repeat } from "lit/directives/repeat.js";
16
- import { ifDefined } from "lit/directives/if-defined.js";
17
17
  //#region \0rolldown/runtime.js
18
18
  var __defProp = Object.defineProperty;
19
19
  var __exportAll = (all, no_symbols) => {
@@ -45,6 +45,7 @@ var EDITOR_AREA_MAIN = "editor-area-main";
45
45
  var SIDEBAR_MAIN = "sidebar-main";
46
46
  var SIDEBAR_MAIN_BOTTOM = "sidebar-main-bottom";
47
47
  var SIDEBAR_AUXILIARY = "sidebar-auxiliary";
48
+ var SIDEBAR_AUXILIARY_BOTTOM = "sidebar-auxiliary-bottom";
48
49
  var PANEL_BOTTOM = "panel-bottom";
49
50
  //#endregion
50
51
  //#region src/core/constants.ts
@@ -56,6 +57,7 @@ var constants_exports = /* @__PURE__ */ __exportAll({
56
57
  MouseButton: () => MouseButton,
57
58
  PANEL_BOTTOM: () => PANEL_BOTTOM,
58
59
  SIDEBAR_AUXILIARY: () => SIDEBAR_AUXILIARY,
60
+ SIDEBAR_AUXILIARY_BOTTOM: () => SIDEBAR_AUXILIARY_BOTTOM,
59
61
  SIDEBAR_MAIN: () => SIDEBAR_MAIN,
60
62
  SIDEBAR_MAIN_BOTTOM: () => SIDEBAR_MAIN_BOTTOM,
61
63
  SIDEBAR_MAIN_TOOLBAR: () => SIDEBAR_MAIN_TOOLBAR,
@@ -271,7 +273,7 @@ var taskService = new TaskService();
271
273
  rootContext.put("taskService", taskService);
272
274
  //#endregion
273
275
  //#region src/core/esmsh-service.ts
274
- var logger$3 = createLogger("EsmShService");
276
+ var logger$4 = createLogger("EsmShService");
275
277
  var esmShService = new class EsmShService {
276
278
  static {
277
279
  this.ESM_SH_BASE = "https://esm.sh";
@@ -435,7 +437,7 @@ var esmShService = new class EsmShService {
435
437
  if (this.isHttpUrl(source)) return source;
436
438
  const parsed = this.parseSource(source);
437
439
  if (!parsed) {
438
- logger$3.warn(`Could not parse source identifier: ${source}`);
440
+ logger$4.warn(`Could not parse source identifier: ${source}`);
439
441
  return source;
440
442
  }
441
443
  return this.buildEsmShUrl(parsed, options);
@@ -1080,7 +1082,7 @@ var DocksDialogContent = class extends DocksElement {
1080
1082
  };
1081
1083
  //#endregion
1082
1084
  //#region src/core/dialogservice.ts
1083
- var logger$2 = createLogger("DialogService");
1085
+ var logger$3 = createLogger("DialogService");
1084
1086
  var DIALOG_CONTRIBUTION_TARGET = "dialogs";
1085
1087
  var OK_BUTTON = {
1086
1088
  id: "ok",
@@ -1106,6 +1108,20 @@ function getDialogContainer() {
1106
1108
  }
1107
1109
  return dialogContainer;
1108
1110
  }
1111
+ function findWaDialog(root) {
1112
+ if (root instanceof Element && root.tagName.toLowerCase() === "wa-dialog") return root;
1113
+ const children = root instanceof Document || root instanceof DocumentFragment || root instanceof Element ? Array.from(root.children) : root instanceof ShadowRoot ? Array.from(root.children) : [];
1114
+ for (const child of children) {
1115
+ if (child instanceof Element && child.tagName.toLowerCase() === "wa-dialog") return child;
1116
+ if (child instanceof Element && child.shadowRoot) {
1117
+ const nested = findWaDialog(child.shadowRoot);
1118
+ if (nested) return nested;
1119
+ }
1120
+ const nested = findWaDialog(child);
1121
+ if (nested) return nested;
1122
+ }
1123
+ return null;
1124
+ }
1109
1125
  var DialogService = class {
1110
1126
  constructor() {
1111
1127
  this.contributions = /* @__PURE__ */ new Map();
@@ -1126,15 +1142,15 @@ var DialogService = class {
1126
1142
  this.contributions.clear();
1127
1143
  for (const contribution of contributions) {
1128
1144
  if (!contribution.id) {
1129
- logger$2.warn("Dialog contribution missing id, skipping");
1145
+ logger$3.warn("Dialog contribution missing id, skipping");
1130
1146
  continue;
1131
1147
  }
1132
1148
  if (!contribution.component) {
1133
- logger$2.warn(`Dialog contribution "${contribution.id}" has no component function, skipping`);
1149
+ logger$3.warn(`Dialog contribution "${contribution.id}" has no component function, skipping`);
1134
1150
  continue;
1135
1151
  }
1136
- if (!contribution.onButton) {
1137
- logger$2.warn(`Dialog contribution "${contribution.id}" has no onButton callback, skipping`);
1152
+ if (!contribution.onButton && !contribution.selfContained) {
1153
+ logger$3.warn(`Dialog contribution "${contribution.id}" has no onButton callback, skipping`);
1138
1154
  continue;
1139
1155
  }
1140
1156
  this.contributions.set(contribution.id, contribution);
@@ -1143,50 +1159,68 @@ var DialogService = class {
1143
1159
  async open(dialogId, state) {
1144
1160
  const contribution = this.contributions.get(dialogId);
1145
1161
  if (!contribution) {
1146
- logger$2.error(`Dialog "${dialogId}" not found`);
1162
+ logger$3.error(`Dialog "${dialogId}" not found`);
1147
1163
  throw new Error(`Dialog "${dialogId}" not found`);
1148
1164
  }
1149
1165
  return new Promise((resolve) => {
1150
1166
  const container = getDialogContainer();
1151
- let isOpen = true;
1167
+ let closed = false;
1152
1168
  let dialogContentElement = null;
1153
1169
  const cleanup = async () => {
1154
- if (!isOpen) return;
1155
- isOpen = false;
1170
+ if (closed) return;
1171
+ closed = true;
1156
1172
  if (dialogContentElement) try {
1157
1173
  await dialogContentElement.dispose();
1158
1174
  } catch (error) {
1159
1175
  const errorMessage = error instanceof Error ? error.message : String(error);
1160
- logger$2.error(`Error disposing dialog content for "${dialogId}": ${errorMessage}`);
1176
+ logger$3.error(`Error disposing dialog content for "${dialogId}": ${errorMessage}`);
1161
1177
  }
1162
1178
  try {
1163
1179
  const result = dialogContentElement ? dialogContentElement.getResult() : void 0;
1164
- await contribution.onButton("close", result, stateWithClose);
1180
+ if (contribution.onButton) await contribution.onButton("close", result, stateWithClose);
1165
1181
  } catch (error) {
1166
1182
  const errorMessage = error instanceof Error ? error.message : String(error);
1167
- logger$2.error(`Error executing close callback for dialog "${dialogId}": ${errorMessage}`);
1183
+ logger$3.error(`Error executing close callback for dialog "${dialogId}": ${errorMessage}`);
1168
1184
  }
1169
1185
  render(html``, container);
1170
1186
  resolve();
1171
1187
  };
1188
+ const closeDialog = () => {
1189
+ if (closed) return;
1190
+ const dialog = findWaDialog(container);
1191
+ if (dialog && dialog.open !== false) {
1192
+ dialog.open = false;
1193
+ return;
1194
+ }
1195
+ cleanup();
1196
+ };
1197
+ const handleAfterHide = () => {
1198
+ cleanup();
1199
+ };
1172
1200
  const handleButtonClick = async (buttonId) => {
1201
+ if (!contribution.onButton) return;
1173
1202
  try {
1174
1203
  const result = dialogContentElement ? dialogContentElement.getResult() : void 0;
1175
- if (await contribution.onButton(buttonId, result, stateWithClose) !== false) cleanup();
1204
+ if (await contribution.onButton(buttonId, result, stateWithClose) !== false) closeDialog();
1176
1205
  } catch (error) {
1177
1206
  const errorMessage = error instanceof Error ? error.message : String(error);
1178
- logger$2.error(`Error executing button callback for dialog "${dialogId}": ${errorMessage}`);
1179
- cleanup();
1207
+ logger$3.error(`Error executing button callback for dialog "${dialogId}": ${errorMessage}`);
1208
+ closeDialog();
1180
1209
  }
1181
1210
  };
1182
1211
  const buttons = contribution.buttons && contribution.buttons.length > 0 ? contribution.buttons : [OK_BUTTON];
1183
- if (state && typeof state === "object") state.close = cleanup;
1184
1212
  const stateWithClose = {
1185
1213
  ...state,
1186
- close: cleanup
1214
+ close: closeDialog,
1215
+ onAfterHide: handleAfterHide
1187
1216
  };
1188
- render(html`
1189
- <wa-dialog label="${state && typeof state === "object" && typeof state.label === "string" ? state.label : contribution.label || dialogId}" open @wa-request-close=${cleanup}>
1217
+ if (state && typeof state === "object") {
1218
+ state.close = closeDialog;
1219
+ state.onAfterHide = handleAfterHide;
1220
+ }
1221
+ const dialogLabel = state && typeof state === "object" && typeof state.label === "string" ? state.label : contribution.label || dialogId;
1222
+ render(contribution.selfContained ? contribution.component(stateWithClose) : html`
1223
+ <wa-dialog label="${dialogLabel}" open @wa-after-hide=${handleAfterHide}>
1190
1224
  <style>
1191
1225
  .dialog-service-content {
1192
1226
  display: flex;
@@ -1218,13 +1252,14 @@ var DialogService = class {
1218
1252
  @dialog-cancel=${() => {
1219
1253
  const cancelButton = buttons.find((b) => b.id === "cancel");
1220
1254
  if (cancelButton) handleButtonClick(cancelButton.id);
1221
- else cleanup();
1255
+ else closeDialog();
1222
1256
  }}>
1223
1257
  ${contribution.component(state)}
1224
1258
 
1225
1259
  <div class="dialog-service-footer">
1226
1260
  ${buttons.map((button) => html`
1227
1261
  <wa-button
1262
+ data-dialog-button="${button.id}"
1228
1263
  variant="${button.variant || "default"}"
1229
1264
  ?disabled=${button.disabled}
1230
1265
  @click=${() => handleButtonClick(button.id)}
@@ -1747,7 +1782,7 @@ async function deleteIndexedDbWorkspaceData(directory) {
1747
1782
  }
1748
1783
  //#endregion
1749
1784
  //#region src/core/marketplaceregistry.ts
1750
- var logger$1 = createLogger("MarketplaceRegistry");
1785
+ var logger$2 = createLogger("MarketplaceRegistry");
1751
1786
  var TOPIC_MARKETPLACE_CHANGED = "events/marketplaceregistry/changed";
1752
1787
  var KEY_CATALOG_URLS = "marketplace.catalogUrls";
1753
1788
  var MarketplaceRegistry = class {
@@ -1756,7 +1791,7 @@ var MarketplaceRegistry = class {
1756
1791
  this.loadingPromises = /* @__PURE__ */ new Map();
1757
1792
  this.loadCatalogUrls().then(() => {
1758
1793
  this.refreshCatalogs().catch((err) => {
1759
- logger$1.error(`Failed to refresh catalogs on init: ${err.message}`);
1794
+ logger$2.error(`Failed to refresh catalogs on init: ${err.message}`);
1760
1795
  });
1761
1796
  });
1762
1797
  }
@@ -1765,7 +1800,7 @@ var MarketplaceRegistry = class {
1765
1800
  const urls = await appSettings.get(KEY_CATALOG_URLS);
1766
1801
  this.catalogUrls = Array.isArray(urls) ? urls : [];
1767
1802
  } catch (error) {
1768
- logger$1.error(`Failed to load catalog URLs: ${error}`);
1803
+ logger$2.error(`Failed to load catalog URLs: ${error}`);
1769
1804
  this.catalogUrls = [];
1770
1805
  }
1771
1806
  }
@@ -1779,28 +1814,28 @@ var MarketplaceRegistry = class {
1779
1814
  async addCatalogUrl(url) {
1780
1815
  if (!this.isValidUrl(url)) throw new Error(`Invalid catalog URL: ${url}`);
1781
1816
  if (this.catalogUrls.includes(url)) {
1782
- logger$1.debug(`Catalog URL already exists: ${url}`);
1817
+ logger$2.debug(`Catalog URL already exists: ${url}`);
1783
1818
  return;
1784
1819
  }
1785
1820
  this.catalogUrls.push(url);
1786
1821
  await this.saveCatalogUrls();
1787
- logger$1.debug(`Added catalog URL: ${url}`);
1822
+ logger$2.debug(`Added catalog URL: ${url}`);
1788
1823
  try {
1789
1824
  await this.refreshCatalogs();
1790
1825
  } catch (error) {
1791
- logger$1.warn(`Failed to refresh catalogs immediately after adding: ${error}`);
1826
+ logger$2.warn(`Failed to refresh catalogs immediately after adding: ${error}`);
1792
1827
  }
1793
1828
  }
1794
1829
  async addCatalogUrls(urls) {
1795
1830
  let added = 0;
1796
1831
  for (const url of urls) {
1797
1832
  if (!this.isValidUrl(url)) {
1798
- logger$1.warn(`Skipping invalid catalog URL: ${url}`);
1833
+ logger$2.warn(`Skipping invalid catalog URL: ${url}`);
1799
1834
  continue;
1800
1835
  }
1801
1836
  if (this.catalogUrls.includes(url)) continue;
1802
1837
  this.catalogUrls.push(url);
1803
- logger$1.debug(`Added catalog URL: ${url}`);
1838
+ logger$2.debug(`Added catalog URL: ${url}`);
1804
1839
  added++;
1805
1840
  }
1806
1841
  if (added === 0) return;
@@ -1808,7 +1843,7 @@ var MarketplaceRegistry = class {
1808
1843
  try {
1809
1844
  await this.refreshCatalogs();
1810
1845
  } catch (error) {
1811
- logger$1.warn(`Failed to refresh catalogs after adding URLs: ${error}`);
1846
+ logger$2.warn(`Failed to refresh catalogs after adding URLs: ${error}`);
1812
1847
  }
1813
1848
  }
1814
1849
  async removeCatalogUrl(url) {
@@ -1816,7 +1851,7 @@ var MarketplaceRegistry = class {
1816
1851
  if (index === -1) return;
1817
1852
  this.catalogUrls.splice(index, 1);
1818
1853
  await this.saveCatalogUrls();
1819
- logger$1.info(`Removed catalog URL: ${url}`);
1854
+ logger$2.info(`Removed catalog URL: ${url}`);
1820
1855
  }
1821
1856
  getCatalogUrls() {
1822
1857
  return [...this.catalogUrls];
@@ -1847,7 +1882,7 @@ var MarketplaceRegistry = class {
1847
1882
  extensions: data.extensions || []
1848
1883
  };
1849
1884
  } catch (error) {
1850
- logger$1.error(`Failed to fetch catalog from ${url}: ${error}`);
1885
+ logger$2.error(`Failed to fetch catalog from ${url}: ${error}`);
1851
1886
  throw error;
1852
1887
  } finally {
1853
1888
  this.loadingPromises.delete(url);
@@ -1858,7 +1893,7 @@ var MarketplaceRegistry = class {
1858
1893
  }
1859
1894
  async refreshCatalogs() {
1860
1895
  const promises = this.catalogUrls.map((url) => this.fetchCatalog(url).catch((error) => {
1861
- logger$1.warn(`Failed to refresh catalog ${url}: ${error.message}`);
1896
+ logger$2.warn(`Failed to refresh catalog ${url}: ${error.message}`);
1862
1897
  return null;
1863
1898
  }));
1864
1899
  const catalogs = await Promise.allSettled(promises);
@@ -1878,8 +1913,8 @@ var MarketplaceRegistry = class {
1878
1913
  });
1879
1914
  }
1880
1915
  });
1881
- logger$1.debug(`Refreshed ${this.catalogUrls.length} catalogs, ${registeredCount} extensions registered`);
1882
- if (registeredCount > 0) logger$1.info(`Marketplace: ${registeredCount} new extension(s) available`);
1916
+ logger$2.debug(`Refreshed ${this.catalogUrls.length} catalogs, ${registeredCount} extensions registered`);
1917
+ if (registeredCount > 0) logger$2.info(`Marketplace: ${registeredCount} new extension(s) available`);
1883
1918
  publish(TOPIC_MARKETPLACE_CHANGED, { type: "refreshed" });
1884
1919
  }
1885
1920
  getMarketplaceExtension(extensionId) {
@@ -1908,16 +1943,10 @@ rootContext.put("marketplaceRegistry", marketplaceRegistry);
1908
1943
  * - Application: App-specific code, gs-* components, contributions
1909
1944
  * - App Loader: Bridge between framework and application
1910
1945
  */
1911
- var logger = createLogger("AppLoader");
1946
+ var logger$1 = createLogger("AppLoader");
1912
1947
  function getLayoutIdFromApp(app) {
1913
- if (!app) return "standard";
1914
- const l = app.layout ?? app.layoutId;
1915
- return typeof l === "object" ? l.id : l ?? "standard";
1916
- }
1917
- function propsToAttributes(props) {
1918
- const out = {};
1919
- for (const [k, v] of Object.entries(props)) out[k] = typeof v === "boolean" ? v ? "true" : "false" : v;
1920
- return out;
1948
+ const layout = app?.layout ?? "standard";
1949
+ return typeof layout === "object" ? layout.id : layout;
1921
1950
  }
1922
1951
  /**
1923
1952
  * Extracts error message from an error object.
@@ -1946,9 +1975,6 @@ var appLoaderService = new class AppLoaderService {
1946
1975
  static {
1947
1976
  this.PREFERRED_APP_KEY = "preferredAppName";
1948
1977
  }
1949
- static {
1950
- this.PREFERRED_LAYOUT_KEY = "preferredLayoutId";
1951
- }
1952
1978
  /**
1953
1979
  * Register an application with the framework.
1954
1980
  * Optionally starts the apploader automatically after registration.
@@ -1967,7 +1993,8 @@ var appLoaderService = new class AppLoaderService {
1967
1993
  }
1968
1994
  app.name = app.name ?? "app";
1969
1995
  app.version = app.version ?? "0.0.0";
1970
- if (this.apps.has(app.name)) logger.warn(`App '${app.name}' is already registered. Overwriting.`);
1996
+ app.layout = app.layout ?? "standard";
1997
+ if (this.apps.has(app.name)) logger$1.warn(`App '${app.name}' is already registered. Overwriting.`);
1971
1998
  if (app.marketplaceCatalogUrls?.length) marketplaceRegistry.addCatalogUrls(app.marketplaceCatalogUrls).catch(() => {});
1972
1999
  this.apps.set(app.name, app);
1973
2000
  if (options?.defaultAppName) this.defaultAppName = options.defaultAppName;
@@ -1984,13 +2011,13 @@ var appLoaderService = new class AppLoaderService {
1984
2011
  */
1985
2012
  async start() {
1986
2013
  if (this.started) {
1987
- logger.debug("AppLoader already started");
2014
+ logger$1.debug("AppLoader already started");
1988
2015
  return;
1989
2016
  }
1990
2017
  this.started = true;
1991
2018
  const appIdFromUrl = new URLSearchParams(window.location.search).get("appId");
1992
2019
  const appIdFromPath = extractAppIdFromPath();
1993
- if (appIdFromPath) logger.info(`Extracted app ID from current page path: ${appIdFromPath}`);
2020
+ if (appIdFromPath) logger$1.info(`Extracted app ID from current page path: ${appIdFromPath}`);
1994
2021
  const appToLoad = await this.selectAppToLoad({
1995
2022
  appIdFromUrl,
1996
2023
  appIdFromPath
@@ -2018,10 +2045,10 @@ var appLoaderService = new class AppLoaderService {
2018
2045
  if (!app) throw new Error(`App '${appName}' not found. Make sure it's registered.`);
2019
2046
  this.dispatchLoadProgress("Starting…");
2020
2047
  if (this.currentApp) {
2021
- logger.info(`Disposing current app: ${this.currentApp.name}`);
2048
+ logger$1.info(`Disposing current app: ${this.currentApp.name}`);
2022
2049
  if (this.currentApp.dispose) await this.currentApp.dispose();
2023
2050
  if (this.currentApp.extensions && this.currentApp.extensions.length > 0) {
2024
- logger.info(`Disabling ${this.currentApp.extensions.length} extensions...`);
2051
+ logger$1.info(`Disabling ${this.currentApp.extensions.length} extensions...`);
2025
2052
  this.currentApp.extensions.forEach((extId) => {
2026
2053
  extensionRegistry.disable(extId);
2027
2054
  });
@@ -2037,19 +2064,19 @@ var appLoaderService = new class AppLoaderService {
2037
2064
  });
2038
2065
  }
2039
2066
  if (app.contributions) {
2040
- logger.info("Registering app contributions...");
2067
+ logger$1.info("Registering app contributions...");
2041
2068
  if (app.contributions.ui) {
2042
2069
  app.contributions.ui.forEach((contribution) => {
2043
2070
  const target = contribution.target;
2044
2071
  if (target) contributionRegistry.registerContribution(target, contribution);
2045
2072
  });
2046
- logger.info(`Registered ${app.contributions.ui.length} UI contributions`);
2073
+ logger$1.info(`Registered ${app.contributions.ui.length} UI contributions`);
2047
2074
  }
2048
2075
  if (app.contributions.extensions) {
2049
2076
  app.contributions.extensions.forEach((extension) => {
2050
2077
  extensionRegistry.registerExtension(extension);
2051
2078
  });
2052
- logger.info(`Registered ${app.contributions.extensions.length} app extensions`);
2079
+ logger$1.info(`Registered ${app.contributions.extensions.length} app extensions`);
2053
2080
  }
2054
2081
  }
2055
2082
  const extensionsSet = new Set(app.extensions || []);
@@ -2058,19 +2085,18 @@ var appLoaderService = new class AppLoaderService {
2058
2085
  if (app.extensions.length > 0) {
2059
2086
  this.dispatchLoadProgress("Enabling extensions…");
2060
2087
  await Promise.all(app.extensions.map((extId) => extensionRegistry.enableAsync(extId).catch((e) => {
2061
- logger.error(`Failed to load extension ${extId}: ${getErrorMessage(e)}`);
2088
+ logger$1.error(`Failed to load extension ${extId}: ${getErrorMessage(e)}`);
2062
2089
  })));
2063
2090
  }
2064
2091
  this.dispatchLoadProgress("Loading extensions…");
2065
2092
  await extensionRegistry.loadEnabledExtensions();
2066
2093
  if (app.initialize) {
2067
2094
  this.dispatchLoadProgress("Initializing…");
2068
- logger.info(`Initializing ${app.name}...`);
2095
+ logger$1.info(`Initializing ${app.name}...`);
2069
2096
  await app.initialize();
2070
2097
  }
2071
2098
  this.currentApp = app;
2072
- logger.info(`App ${app.name} loaded successfully`);
2073
- this.preferredLayoutId = await this.getPreferredLayoutId();
2099
+ logger$1.info(`App ${app.name} loaded successfully`);
2074
2100
  this.updateDocumentMetadata(app);
2075
2101
  if (container) {
2076
2102
  this.dispatchLoadProgress("Rendering layout…");
@@ -2103,19 +2129,17 @@ var appLoaderService = new class AppLoaderService {
2103
2129
  */
2104
2130
  renderApp(container) {
2105
2131
  if (!this.currentApp) throw new Error("No app loaded. Call loadApp() first.");
2106
- const layoutId = this.preferredLayoutId ?? getLayoutIdFromApp(this.currentApp);
2132
+ const layoutId = getLayoutIdFromApp(this.currentApp);
2107
2133
  const layouts = contributionRegistry.getContributions(SYSTEM_LAYOUTS);
2108
2134
  let layout = layouts.find((c) => c.id === layoutId);
2109
2135
  if (!layout) {
2110
- logger.warn(`Layout '${layoutId}' not found, falling back to 'standard'`);
2136
+ logger$1.warn(`Layout '${layoutId}' not found, falling back to 'standard'`);
2111
2137
  layout = layouts.find((c) => c.id === "standard");
2112
2138
  }
2113
2139
  if (!layout) throw new Error(`No layout found for layoutId '${layoutId}' and no 'standard' layout registered.`);
2114
2140
  const r = layout.component;
2115
2141
  let effectiveAttributes = {};
2116
2142
  if (r && typeof r === "object" && "tag" in r && r.attributes) effectiveAttributes = { ...r.attributes };
2117
- const appLayout = this.currentApp?.layout;
2118
- if (typeof appLayout === "object" && appLayout.id === layoutId && appLayout.props) Object.assign(effectiveAttributes, propsToAttributes(appLayout.props));
2119
2143
  container.innerHTML = "";
2120
2144
  if (typeof r === "string") {
2121
2145
  const el = document.createElement(r);
@@ -2128,7 +2152,7 @@ var appLoaderService = new class AppLoaderService {
2128
2152
  } else if (typeof r === "function") render(r(), container);
2129
2153
  else throw new Error(`Layout '${layout.id}' has invalid component.`);
2130
2154
  if (layout.onShow) requestAnimationFrame(() => {
2131
- Promise.resolve(layout.onShow()).catch((err) => logger.error(`Layout onShow failed for '${layout.id}': ${getErrorMessage(err)}`));
2155
+ Promise.resolve(layout.onShow()).catch((err) => logger$1.error(`Layout onShow failed for '${layout.id}': ${getErrorMessage(err)}`));
2132
2156
  });
2133
2157
  }
2134
2158
  /**
@@ -2150,7 +2174,7 @@ var appLoaderService = new class AppLoaderService {
2150
2174
  try {
2151
2175
  return await appSettings.get(AppLoaderService.PREFERRED_APP_KEY);
2152
2176
  } catch (error) {
2153
- logger.debug(`Failed to get preferred app ID from settings: ${getErrorMessage(error)}`);
2177
+ logger$1.debug(`Failed to get preferred app ID from settings: ${getErrorMessage(error)}`);
2154
2178
  return;
2155
2179
  }
2156
2180
  }
@@ -2162,9 +2186,9 @@ var appLoaderService = new class AppLoaderService {
2162
2186
  try {
2163
2187
  await appSettings.set(AppLoaderService.PREFERRED_APP_KEY, appId);
2164
2188
  this.defaultAppName = appId;
2165
- logger.info(`Set preferred app to: ${appId}`);
2189
+ logger$1.info(`Set preferred app to: ${appId}`);
2166
2190
  } catch (error) {
2167
- logger.error(`Failed to persist preferred app: ${getErrorMessage(error)}`);
2191
+ logger$1.error(`Failed to persist preferred app: ${getErrorMessage(error)}`);
2168
2192
  throw error;
2169
2193
  }
2170
2194
  }
@@ -2172,28 +2196,7 @@ var appLoaderService = new class AppLoaderService {
2172
2196
  return contributionRegistry.getContributions(SYSTEM_LAYOUTS);
2173
2197
  }
2174
2198
  getCurrentLayoutId() {
2175
- return this.preferredLayoutId ?? getLayoutIdFromApp(this.currentApp);
2176
- }
2177
- async getPreferredLayoutId() {
2178
- try {
2179
- return await appSettings.get(AppLoaderService.PREFERRED_LAYOUT_KEY);
2180
- } catch (error) {
2181
- logger.debug(`Failed to get preferred layout ID: ${getErrorMessage(error)}`);
2182
- return;
2183
- }
2184
- }
2185
- async setPreferredLayoutId(layoutId) {
2186
- if (!this.getRegisteredLayouts().some((l) => l.id === layoutId)) throw new Error(`Layout '${layoutId}' not found.`);
2187
- try {
2188
- await appSettings.set(AppLoaderService.PREFERRED_LAYOUT_KEY, layoutId);
2189
- this.preferredLayoutId = layoutId;
2190
- logger.info(`Set preferred layout to: ${layoutId}`);
2191
- if (this.currentApp && this.container) this.renderApp(this.container);
2192
- window.dispatchEvent(new CustomEvent("layout-changed", { detail: { layoutId } }));
2193
- } catch (error) {
2194
- logger.error(`Failed to persist preferred layout: ${getErrorMessage(error)}`);
2195
- throw error;
2196
- }
2199
+ return getLayoutIdFromApp(this.currentApp);
2197
2200
  }
2198
2201
  /**
2199
2202
  * Select which app to load based on priority:
@@ -2208,26 +2211,26 @@ var appLoaderService = new class AppLoaderService {
2208
2211
  if (appIdFromUrl) {
2209
2212
  const name = this.findAppNameBySegment(appIdFromUrl) ?? appIdFromUrl;
2210
2213
  if (this.apps.has(name)) {
2211
- logger.info(`Loading app specified by URL parameter 'appId': ${name}`);
2214
+ logger$1.info(`Loading app specified by URL parameter 'appId': ${name}`);
2212
2215
  return name;
2213
2216
  }
2214
- logger.warn(`App '${appIdFromUrl}' from URL parameter not found`);
2217
+ logger$1.warn(`App '${appIdFromUrl}' from URL parameter not found`);
2215
2218
  }
2216
2219
  if (appIdFromPath) {
2217
2220
  const name = this.findAppNameBySegment(appIdFromPath);
2218
2221
  if (name) {
2219
- logger.info(`Loading app from URL path: ${appIdFromPath}`);
2222
+ logger$1.info(`Loading app from URL path: ${appIdFromPath}`);
2220
2223
  return name;
2221
2224
  }
2222
- logger.debug(`App for path '${appIdFromPath}' not found, continuing search`);
2225
+ logger$1.debug(`App for path '${appIdFromPath}' not found, continuing search`);
2223
2226
  }
2224
2227
  const preferred = await this.getPreferredAppId();
2225
2228
  if (preferred && this.apps.has(preferred)) {
2226
- logger.info(`Loading preferred app from settings: ${preferred}`);
2229
+ logger$1.info(`Loading preferred app from settings: ${preferred}`);
2227
2230
  return preferred;
2228
2231
  }
2229
2232
  if (this.defaultAppName && this.apps.has(this.defaultAppName)) return this.defaultAppName;
2230
- if (this.defaultAppName) logger.warn(`Default app '${this.defaultAppName}' not found`);
2233
+ if (this.defaultAppName) logger$1.warn(`Default app '${this.defaultAppName}' not found`);
2231
2234
  const registeredApps = this.getRegisteredApps();
2232
2235
  if (registeredApps.length > 0) return registeredApps[0].name;
2233
2236
  }
@@ -2642,7 +2645,9 @@ var DocksNavigableInfoDialogContent = class DocksNavigableInfoDialogContent exte
2642
2645
  action.callback();
2643
2646
  }
2644
2647
  handleClose() {
2645
- if (this.closest("wa-dialog") && this.resolveCallback) this.resolveCallback();
2648
+ if (this.resolveCallback) this.resolveCallback();
2649
+ const dialog = this.closest("wa-dialog");
2650
+ if (dialog && dialog.open !== false) dialog.open = false;
2646
2651
  }
2647
2652
  static {
2648
2653
  this.styles = [...DocksDialogContent.styles, css`
@@ -2983,300 +2988,665 @@ function filebrowserDialog(mode = "either") {
2983
2988
  });
2984
2989
  }
2985
2990
  //#endregion
2986
- //#region src/dialogs/version-info-dialog.ts
2987
- var CLOSE_PRIMARY = {
2988
- ...CLOSE_BUTTON,
2989
- variant: "primary"
2990
- };
2991
- var DocksVersionInfoDialogContent = class DocksVersionInfoDialogContent extends DocksDialogContent {
2992
- static {
2993
- this.styles = [...DocksDialogContent.styles, css`
2994
- :host {
2995
- display: block;
2996
- }
2997
-
2998
- .dialog-body {
2999
- display: flex;
3000
- flex-direction: column;
3001
- gap: 0.75rem;
3002
- min-width: 32rem;
3003
- height: 600px;
3004
- }
3005
-
3006
- .dialog-body docks-tabs {
3007
- flex: 1;
3008
- min-height: 0;
3009
- }
3010
- `];
3011
- }
3012
- firstUpdated(changed) {
3013
- super.firstUpdated(changed);
3014
- const dialog = this.closest("wa-dialog");
3015
- if (dialog) {
3016
- dialog.style.setProperty("--width", "600px");
3017
- dialog.setAttribute("light-dismiss", "");
3018
- }
3019
- }
3020
- render() {
3021
- const app = appLoaderService.getCurrentApp();
3022
- return html`
3023
- <div class="dialog-body">
3024
- ${app?.description ? html`<small>${app.description}</small>` : ""}
3025
- <docks-tabs id=${ABOUT_TABS}></docks-tabs>
3026
- </div>
3027
- `;
3028
- }
3029
- };
3030
- DocksVersionInfoDialogContent = _decorate([customElement("docks-version-info-dialog-content")], DocksVersionInfoDialogContent);
3031
- contributionRegistry.registerContribution(DIALOG_CONTRIBUTION_TARGET, {
3032
- id: "version-info",
3033
- label: "About",
3034
- buttons: [CLOSE_PRIMARY],
3035
- component: () => html`<docks-version-info-dialog-content></docks-version-info-dialog-content>`,
3036
- onButton: async () => true
3037
- });
3038
- function versionInfoDialog() {
3039
- const app = appLoaderService.getCurrentApp();
3040
- return dialogService.open("version-info", { label: `About ${app?.name ?? ""} - ${app?.version ?? "0.0.0"}` });
3041
- }
2991
+ //#region src/core/contribution-names.ts
2992
+ var VIEW_FILEBROWSER = "view.filebrowser";
2993
+ var VIEW_LOG_TERMINAL = "view.logTerminal";
2994
+ var TOOLBAR_INFO = "toolbar.info";
2995
+ var TOOLBAR_FAST_VIEWS = "toolbar.fastViews";
2996
+ var TOOLBAR_LANGUAGE_SELECTOR = "toolbar.languageSelector";
2997
+ /** Main toolbar (right): layout picker (`docks-layout-switcher`). */
2998
+ var TOOLBAR_LAYOUT_SWITCHER = "toolbar.layoutSwitcher";
2999
+ /** Main toolbar (right): dark/light theme toggle (`switch_theme`). */
3000
+ var TOOLBAR_THEME_SWITCHER = "toolbar.themeSwitcher";
3001
+ /** Main toolbar (right): document fullscreen toggle (`fullscreen`). */
3002
+ var TOOLBAR_FULLSCREEN = "toolbar.fullscreen";
3003
+ /** Bottom-center toolbar: focused part title / active part indicator (`docks-part-name`). */
3004
+ var TOOLBAR_ACTIVE_PART_NAME = "toolbar.activePartName";
3005
+ /** Bottom-center toolbar: background tasks / progress (`docks-tasks`). */
3006
+ var TOOLBAR_TASKS = "toolbar.tasks";
3007
+ /** Main sidebar header toolbar: open extensions (`open_extensions` command). */
3008
+ var SIDEBAR_TOOLBAR_EXTENSIONS = "sidebar.toolbar.extensions";
3009
+ /** Default file-type → icon mappings on `system.icons`. */
3010
+ var SYSTEM_DEFAULT_FILE_ICONS = "system.icons.default";
3011
+ var ABOUT_TAB_RELEASE = "about.tab.release";
3012
+ var ABOUT_TAB_PACKAGES = "about.tab.packages";
3013
+ var ABOUT_TAB_ATTRIBUTIONS = "about.tab.attributions";
3014
+ var ATTRIBUTION_ECLIPSE_DOCKS = "attribution.eclipse-docks";
3015
+ var CTXMENU_FILEBROWSER_RENAME = "contextmenu.filebrowser.rename";
3016
+ var CTXMENU_FILEBROWSER_DELETE = "contextmenu.filebrowser.delete";
3042
3017
  //#endregion
3043
- //#region src/core/icon-utils.ts
3044
- /**
3045
- * Parses an icon spec string into `{ library, name }` for use with `wa-icon`.
3046
- *
3047
- * Icon specs support an optional icon library prefix followed by the icon name,
3048
- * separated by whitespace, e.g. `"docks mark-github"`.
3049
- *
3050
- * - When the spec contains **no whitespace**, it is treated as the icon name only
3051
- * and `library` is omitted so the default Web Awesome / Font Awesome library is used.
3052
- * - When the spec contains **whitespace**, the **last segment** is treated as
3053
- * the icon name and every prior segment (joined by a single space) becomes `library`.
3054
- *
3055
- * Prefer {@link icon} for rendering; use parseIconSpec only when you need the parsed parts.
3056
- */
3057
- function parseIconSpec(spec) {
3058
- const trimmed = (spec ?? "").trim();
3059
- if (!trimmed) return { name: "" };
3060
- const parts = trimmed.split(/\s+/);
3061
- if (parts.length <= 1) return { name: trimmed };
3062
- return {
3063
- name: parts.pop(),
3064
- library: parts.join(" ")
3065
- };
3066
- }
3067
- /**
3068
- * Returns a Lit template that renders a `wa-icon` for the given spec.
3069
- * Use this for all icon rendering so specs (e.g. contribution `icon` fields) stay consistent.
3070
- * Handles undefined/null/empty spec internally; call sites may pass values directly.
3071
- */
3072
- function icon(spec, options) {
3073
- const { name: iconName, library } = parseIconSpec(spec ?? "");
3074
- return html`<wa-icon library=${library ?? nothing} name=${iconName} label=${options?.label ?? nothing} slot=${options?.slot ?? nothing}></wa-icon>`;
3075
- }
3018
+ //#region src/parts/container.ts
3019
+ var DocksContainer = class extends DocksElement {};
3076
3020
  //#endregion
3077
- //#region src/parts/toolbar.ts
3078
- var RESIZE_DEBOUNCE_MS = 150;
3079
- function renderButtonGroup(slotName, orientation, contributions, isToolbarItem, contributionCreator) {
3080
- const label = `Toolbar ${slotName ?? "default"}`;
3081
- const items = contributions.filter((c) => c.slot === slotName && isToolbarItem(c) && getContributionVisible(c));
3082
- return html`
3083
- <wa-button-group orientation=${orientation} label=${label}>
3084
- ${slotName === "start" ? html`<slot name="start"></slot>` : slotName === "end" ? html`<slot name="end"></slot>` : html`<slot></slot>`}
3085
- ${items.map(contributionCreator)}
3086
- </wa-button-group>
3087
- `;
3088
- }
3089
- var DocksToolbar = class DocksToolbar extends DocksElement {
3021
+ //#region src/parts/part.ts
3022
+ var DocksPart = class DocksPart extends DocksContainer {
3090
3023
  constructor(..._args) {
3091
3024
  super(..._args);
3092
- this.position = "start";
3093
- this.orientation = "horizontal";
3094
- this.align = "start";
3095
- this.size = "s";
3096
- this.scopeTokens = [];
3097
- this.partToolbarContent = void 0;
3098
- this.partToolbarRenderer = void 0;
3099
- this.contributions = [];
3100
- this.compact = false;
3101
- this.resizeObserver = null;
3102
- this.resizeDebounceTimer = null;
3103
- this.overflowCheckScheduled = false;
3104
- this.onResize = () => {
3105
- if (this.resizeDebounceTimer !== null) clearTimeout(this.resizeDebounceTimer);
3106
- this.resizeDebounceTimer = setTimeout(() => {
3107
- this.resizeDebounceTimer = null;
3108
- this.updateCompactFromSpace();
3109
- }, RESIZE_DEBOUNCE_MS);
3025
+ this.scrollMode = "scroller";
3026
+ this.dirty = false;
3027
+ this.isEditor = false;
3028
+ this.embedded = false;
3029
+ this.onContentContextMenu = async (event) => {
3030
+ const contextMenu = this.renderRoot.querySelector("docks-contextmenu");
3031
+ if (!contextMenu) return;
3032
+ if (await contextMenu.show({
3033
+ x: event.clientX,
3034
+ y: event.clientY
3035
+ }, event)) event.preventDefault();
3110
3036
  };
3111
3037
  }
3112
- updateCompactFromSpace() {
3113
- const toolbarItems = this.shadowRoot?.querySelector(".toolbar-items");
3114
- if (!toolbarItems) return;
3115
- const trimmed = toolbarItems.scrollWidth > toolbarItems.clientWidth;
3116
- if (this.compact !== trimmed) this.compact = trimmed;
3117
- }
3118
- scheduleOverflowCheck() {
3119
- if (this.overflowCheckScheduled) return;
3120
- this.overflowCheckScheduled = true;
3121
- requestAnimationFrame(() => {
3122
- this.overflowCheckScheduled = false;
3123
- this.updateCompactFromSpace();
3124
- });
3125
- }
3126
- connectedCallback() {
3127
- super.connectedCallback();
3128
- this.resizeObserver = new ResizeObserver(this.onResize);
3129
- this.resizeObserver.observe(this);
3130
- }
3131
- disconnectedCallback() {
3132
- this.resizeObserver?.disconnect();
3133
- this.resizeObserver = null;
3134
- if (this.resizeDebounceTimer !== null) {
3135
- clearTimeout(this.resizeDebounceTimer);
3136
- this.resizeDebounceTimer = null;
3137
- }
3138
- super.disconnectedCallback();
3139
- }
3140
- updated(changedProperties) {
3141
- super.updated?.(changedProperties);
3142
- if (!this.compact) this.scheduleOverflowCheck();
3143
- }
3144
- willUpdate(changedProperties) {
3145
- super.willUpdate?.(changedProperties);
3146
- if (changedProperties.has("scopeTokens")) this.refreshContributions();
3147
- }
3148
- attributeChangedCallback(name, old, value) {
3149
- super.attributeChangedCallback(name, old, value);
3150
- if (name === "id" && old !== value) this.refreshContributions();
3038
+ getCommandStack() {
3039
+ return this.commandStack;
3151
3040
  }
3152
- doBeforeUI() {
3153
- this.refreshContributions();
3154
- subscribe(TOPIC_CONTRIBUTEIONS_CHANGED, (event) => {
3155
- const id = this.getAttribute("id");
3156
- if (!id) return;
3157
- if (this.matchesTarget(id, event.target)) this.refreshContributions();
3158
- });
3041
+ /**
3042
+ * Override this method to provide toolbar content for this part.
3043
+ * This is a lightweight alternative to registering toolbar contributions
3044
+ * for actions that are scoped to this part instance.
3045
+ *
3046
+ * IMPORTANT: Event handlers MUST use arrow functions to preserve the component's 'this' context.
3047
+ * The toolbar template is rendered in a different component (docks-toolbar), so direct method
3048
+ * references lose their binding.
3049
+ *
3050
+ * ✅ Correct:
3051
+ * @click=${() => this.myMethod()}
3052
+ * @click=${(e) => this.handleClick(e)}
3053
+ *
3054
+ * ❌ Wrong (this will be bound to the toolbar, not your component):
3055
+ * @click=${this.myMethod}
3056
+ *
3057
+ * Example:
3058
+ * ```typescript
3059
+ * protected renderToolbar() {
3060
+ * return html`
3061
+ * <wa-button @click=${() => this.save()} title="Save">
3062
+ * <wa-icon name="save"></wa-icon>
3063
+ * </wa-button>
3064
+ * `;
3065
+ * }
3066
+ * ```
3067
+ *
3068
+ * @returns TemplateResult with toolbar items, or nothing if no toolbar needed
3069
+ */
3070
+ renderToolbar() {
3071
+ return nothing;
3159
3072
  }
3160
- refreshContributions() {
3161
- const id = this.getAttribute("id");
3162
- if (!id) {
3163
- this.contributions = [];
3164
- return;
3073
+ /**
3074
+ * Activates the tab that contains this part by walking up the DOM to the first
3075
+ * docks-tabs ancestor and activating the tab panel that contains this part.
3076
+ * Crosses Shadow DOM boundaries (e.g. wa-tab-group) via getRootNode().host.
3077
+ * No-op if this part is not inside a docks-tabs / wa-tab-panel.
3078
+ */
3079
+ activateContainingTab() {
3080
+ let el = this;
3081
+ let panelName = null;
3082
+ let tabsEl = null;
3083
+ while (el) {
3084
+ const tag = el.tagName?.toLowerCase();
3085
+ if (tag === "wa-tab-panel") panelName = el.getAttribute("name");
3086
+ if (tag === "docks-tabs") {
3087
+ tabsEl = el;
3088
+ break;
3089
+ }
3090
+ const parent = el.parentElement;
3091
+ if (parent) el = parent;
3092
+ else {
3093
+ const root = el.getRootNode();
3094
+ el = root instanceof ShadowRoot ? root.host : null;
3095
+ }
3165
3096
  }
3166
- this.loadContributions(id);
3097
+ if (tabsEl && panelName != null && panelName !== "") tabsEl.activate(panelName);
3167
3098
  }
3168
- matchesTarget(id, target) {
3169
- if (target === id) return true;
3170
- if (!id.includes(":")) return false;
3171
- const [prefix] = id.split(":");
3172
- if (target === `${prefix}:*`) return true;
3173
- const targetParts = target.split(":");
3174
- if (targetParts.length === 2) {
3175
- const categoryToken = targetParts[1];
3176
- if (this.scopeTokens.includes(categoryToken)) return id.startsWith(`${prefix}:`);
3177
- }
3178
- return false;
3099
+ /**
3100
+ * Override this method to provide context menu content for this part.
3101
+ * This is a lightweight alternative to registering context menu contributions
3102
+ * for actions that are scoped to this part instance.
3103
+ *
3104
+ * IMPORTANT: Event handlers MUST use arrow functions to preserve the component's 'this' context.
3105
+ * The context menu is rendered in a different component (contextmenu), so direct method
3106
+ * references lose their binding.
3107
+ *
3108
+ * ✅ Correct:
3109
+ * @click=${() => this.myMethod()}
3110
+ * @click=${(e) => this.handleClick(e)}
3111
+ *
3112
+ * ❌ Wrong (this will be bound to the context menu, not your component):
3113
+ * @click=${this.myMethod}
3114
+ *
3115
+ * Example:
3116
+ * ```typescript
3117
+ * protected renderContextMenu() {
3118
+ * return html`
3119
+ * <wa-dropdown-item @click=${() => this.open()}>
3120
+ * <wa-icon name="folder-open"></wa-icon>
3121
+ * Open
3122
+ * </wa-dropdown-item>
3123
+ * <wa-divider></wa-divider>
3124
+ * <wa-dropdown-item @click=${() => this.delete()}>
3125
+ * <wa-icon name="trash"></wa-icon>
3126
+ * Delete
3127
+ * </wa-dropdown-item>
3128
+ * `;
3129
+ * }
3130
+ * ```
3131
+ *
3132
+ * @returns TemplateResult with context menu items, or nothing if no context menu needed
3133
+ */
3134
+ renderContextMenu() {
3135
+ return nothing;
3179
3136
  }
3180
- loadContributions(id) {
3181
- const specific = contributionRegistry.getContributions(id);
3182
- if (!id.includes(":")) {
3183
- this.contributions = specific;
3184
- return;
3185
- }
3186
- const [prefix] = id.split(":");
3187
- const wildcardId = `${prefix}:*`;
3188
- const wildcard = contributionRegistry.getContributions(wildcardId);
3189
- const categoryMatches = [];
3190
- for (const category of this.scopeTokens) {
3191
- const categoryId = `${prefix}:${category}`;
3192
- const matches = contributionRegistry.getContributions(categoryId);
3193
- categoryMatches.push(...matches);
3194
- }
3195
- this.contributions = [
3196
- ...wildcard,
3197
- ...categoryMatches,
3198
- ...specific
3199
- ];
3137
+ renderContent() {
3138
+ return nothing;
3200
3139
  }
3201
- isToolbarItem(contribution) {
3202
- return "command" in contribution || "component" in contribution;
3140
+ /** Part-level toolbar contributions (`toolbar:${partId}`). */
3141
+ getToolbarTarget() {
3142
+ const contributionKey = this.tabContribution?.editorId ?? this.id ?? this.tabContribution?.name;
3143
+ return contributionKey ? `toolbar:${contributionKey}` : void 0;
3203
3144
  }
3204
- contributionCreator(contribution) {
3205
- if ("command" in contribution) {
3206
- const commandContribution = contribution;
3207
- const showLabel = !this.compact && !!commandContribution.showLabel;
3208
- return html`
3209
- <wa-button @click=${() => void this.executeCommand(commandContribution.command, commandContribution.params || {})}
3210
- title=${commandContribution.label}
3211
- ?disabled="${getContributionDisabled(commandContribution)}"
3212
- appearance="plain" size=${this.size}>
3213
- ${icon(commandContribution.icon, { label: commandContribution.label })}
3214
- ${showLabel ? commandContribution.label : ""}
3215
- </wa-button>
3216
- `;
3217
- }
3218
- if ("component" in contribution) {
3219
- const contents = contribution.component;
3220
- if (contents instanceof Function) return contents();
3221
- return unsafeHTML(contents);
3222
- }
3223
- return html`<span>unknown contribution type: ${typeof contribution}</span>`;
3145
+ /** Part-level context-menu contributions (`contextmenu:${partId}`). */
3146
+ getContextMenuTarget() {
3147
+ const contributionKey = this.tabContribution?.editorId ?? this.id ?? this.tabContribution?.name;
3148
+ return contributionKey ? `contextmenu:${contributionKey}` : void 0;
3149
+ }
3150
+ syncIsEditorCapability() {
3151
+ const next = this.save !== DocksPart.prototype.save;
3152
+ if (next === this.isEditor) return;
3153
+ this.isEditor = next;
3154
+ }
3155
+ maybeActivateForCoupledEditors() {
3156
+ const coupled = this.tabContribution?.coupledEditors;
3157
+ if (!coupled?.length) return;
3158
+ const active = activeEditorSignal.get();
3159
+ if (!(active instanceof DocksPart)) return;
3160
+ const editorId = active.tabContribution?.editorId;
3161
+ if (!editorId || !coupled.includes(editorId)) return;
3162
+ this.activateContainingTab();
3224
3163
  }
3225
3164
  render() {
3226
- const partContent = this.partToolbarRenderer ? this.partToolbarRenderer() : this.partToolbarContent ? this.partToolbarContent : "";
3227
- const flexDir = this.orientation === "vertical" ? "column" : "row";
3228
- const alignMap = {
3229
- start: "flex-start",
3230
- center: "center",
3231
- end: "flex-end"
3232
- };
3233
- const bindCreator = this.contributionCreator.bind(this);
3234
- const bindIsItem = this.isToolbarItem.bind(this);
3165
+ const toolbarTarget = this.getToolbarTarget();
3166
+ const contextMenuTarget = this.getContextMenuTarget();
3167
+ const toolbarEnabled = this.tabContribution?.toolbar !== false;
3168
+ const contextMenuEnabled = this.tabContribution?.contextMenu !== false;
3169
+ const scrollMode = this.scrollMode;
3170
+ const scopeTokens = this.isEditor ? ["system.editors", ".system.editors"] : [];
3171
+ const content = this.renderContent();
3172
+ const contentNode = scrollMode === "scroller" ? html`
3173
+ <wa-scroller class="part-content-scroll" orientation="vertical">
3174
+ <div class="part-content-inner">${content}</div>
3175
+ </wa-scroller>
3176
+ ` : html`<div class="part-content-inner">${content}</div>`;
3235
3177
  return html`
3236
- <div class="toolbar-items" style=${styleMap({
3237
- "flex-direction": flexDir,
3238
- "align-items": alignMap[this.align],
3239
- "justify-content": this.position
3240
- })}>
3241
- ${renderButtonGroup("start", this.orientation, this.contributions, bindIsItem, bindCreator)}
3242
- ${partContent}
3243
- ${renderButtonGroup(void 0, this.orientation, this.contributions, bindIsItem, bindCreator)}
3244
- ${renderButtonGroup("end", this.orientation, this.contributions, bindIsItem, bindCreator)}
3178
+ <div class="part-shell">
3179
+ ${toolbarEnabled ? html`
3180
+ <docks-toolbar
3181
+ class="part-toolbar"
3182
+ id=${ifDefined(toolbarTarget)}
3183
+ .scopeTokens=${scopeTokens}
3184
+ .partToolbarRenderer=${() => this.renderToolbar()}>
3185
+ </docks-toolbar>
3186
+ ` : nothing}
3187
+ <div class="part-content ${scrollMode === "native" ? "native-scroll" : ""}" @contextmenu=${contextMenuEnabled ? this.onContentContextMenu : void 0}>
3188
+ ${contentNode}
3189
+ </div>
3190
+ ${contextMenuEnabled ? html`
3191
+ <docks-contextmenu
3192
+ id=${ifDefined(contextMenuTarget)}
3193
+ .scopeTokens=${scopeTokens}
3194
+ .partContextMenuRenderer=${() => this.renderContextMenu()}>
3195
+ </docks-contextmenu>
3196
+ ` : nothing}
3245
3197
  </div>
3246
3198
  `;
3247
3199
  }
3200
+ updated(_changedProperties) {
3201
+ super.updated(_changedProperties);
3202
+ this.syncIsEditorCapability();
3203
+ if (_changedProperties.has("tabContribution")) this.maybeActivateForCoupledEditors();
3204
+ if (_changedProperties.has("dirty")) {
3205
+ if (_changedProperties.get("dirty") !== void 0) this.dispatchEvent(new CustomEvent("dirty", {
3206
+ detail: this.dirty,
3207
+ bubbles: true
3208
+ }));
3209
+ }
3210
+ }
3211
+ doClose() {}
3212
+ disconnectedCallback() {
3213
+ super.disconnectedCallback();
3214
+ }
3215
+ close() {
3216
+ this.doClose();
3217
+ }
3218
+ connectedCallback() {
3219
+ super.connectedCallback();
3220
+ this.syncIsEditorCapability();
3221
+ queueMicrotask(() => this.syncIsEditorCapability());
3222
+ this.watch(activeEditorSignal, () => this.maybeActivateForCoupledEditors());
3223
+ }
3224
+ save() {}
3225
+ isDirty() {
3226
+ return this.dirty;
3227
+ }
3228
+ markDirty(dirty) {
3229
+ this.dirty = dirty;
3230
+ partDirtySignal.set(null);
3231
+ partDirtySignal.set(this);
3232
+ activePartSignal.set(null);
3233
+ activePartSignal.set(this);
3234
+ }
3248
3235
  static {
3249
- this.styles = css`
3236
+ this.baseStyles = css`
3250
3237
  :host {
3251
- display: flex;
3252
- flex-direction: row;
3253
- --wa-form-control-padding-inline: var(--wa-space-2xs);
3238
+ display: block;
3254
3239
  }
3255
3240
 
3256
- :host([orientation="vertical"]) {
3257
- flex-direction: column;
3241
+ .part-shell {
3242
+ display: grid;
3243
+ grid-template-rows: auto minmax(0, 1fr);
3244
+ height: 100%;
3245
+ width: 100%;
3246
+ position: relative;
3247
+ overflow: hidden;
3258
3248
  }
3259
3249
 
3260
- .toolbar-items {
3261
- display: flex;
3262
- flex: 1;
3263
- gap: var(--wa-space-2xs);
3250
+ .part-content {
3251
+ min-height: 0;
3252
+ overflow: hidden;
3253
+ position: relative;
3254
+ }
3255
+
3256
+ .part-content.native-scroll {
3257
+ overflow: auto;
3258
+ }
3259
+
3260
+ .part-content-scroll {
3261
+ width: 100%;
3262
+ height: 100%;
3263
+ }
3264
+
3265
+ .part-content-inner {
3266
+ height: 100%;
3267
+ min-height: 100%;
3268
+ }
3269
+
3270
+ :host([embedded]) {
3271
+ height: auto;
3272
+ }
3273
+
3274
+ :host([embedded]) .part-shell {
3275
+ height: auto;
3276
+ overflow: visible;
3277
+ grid-template-rows: auto;
3278
+ }
3279
+
3280
+ :host([embedded]) .part-content {
3281
+ overflow: visible;
3282
+ min-height: 0;
3283
+ }
3284
+
3285
+ :host([embedded]) .part-content-inner {
3286
+ height: auto;
3287
+ min-height: 0;
3288
+ }
3289
+
3290
+ .part-toolbar {
3291
+ min-height: 0;
3264
3292
  }
3265
3293
  `;
3266
3294
  }
3295
+ static finalizeStyles(styles) {
3296
+ const own = super.finalizeStyles(styles);
3297
+ return [DocksPart.baseStyles, ...own];
3298
+ }
3267
3299
  };
3268
- _decorate([property()], DocksToolbar.prototype, "position", void 0);
3269
- _decorate([property({ reflect: true })], DocksToolbar.prototype, "orientation", void 0);
3270
- _decorate([property({ reflect: true })], DocksToolbar.prototype, "align", void 0);
3271
- _decorate([property({ reflect: true })], DocksToolbar.prototype, "size", void 0);
3272
- _decorate([property({ attribute: false })], DocksToolbar.prototype, "scopeTokens", void 0);
3273
- _decorate([property({ attribute: false })], DocksToolbar.prototype, "partToolbarContent", void 0);
3274
- _decorate([property({ attribute: false })], DocksToolbar.prototype, "partToolbarRenderer", void 0);
3275
- _decorate([state()], DocksToolbar.prototype, "contributions", void 0);
3276
- _decorate([state()], DocksToolbar.prototype, "compact", void 0);
3277
- DocksToolbar = _decorate([customElement("docks-toolbar")], DocksToolbar);
3300
+ _decorate([property()], DocksPart.prototype, "dirty", void 0);
3301
+ _decorate([property({ attribute: false })], DocksPart.prototype, "tabContribution", void 0);
3302
+ _decorate([property({
3303
+ type: Boolean,
3304
+ attribute: false
3305
+ })], DocksPart.prototype, "isEditor", void 0);
3306
+ _decorate([property({
3307
+ type: Boolean,
3308
+ reflect: true
3309
+ })], DocksPart.prototype, "embedded", void 0);
3310
+ //#endregion
3311
+ //#region src/components/release-history.ts
3312
+ var logger = createLogger("ReleaseHistory");
3313
+ async function loadReleases(releaseHistory) {
3314
+ if (!releaseHistory) return [];
3315
+ if (typeof releaseHistory !== "function") return releaseHistory;
3316
+ try {
3317
+ return await releaseHistory();
3318
+ } catch (error) {
3319
+ logger.error(`Failed to load release history from app: ${error instanceof Error ? error.message : String(error)}`);
3320
+ return [];
3321
+ }
3322
+ }
3323
+ function semverEqual(a, b) {
3324
+ return a.replace(/^v/i, "") === b.replace(/^v/i, "");
3325
+ }
3326
+ function isNewerThan(releaseTag, currentVersion) {
3327
+ const currentParts = currentVersion.replace(/^v/i, "").split(".").map(Number);
3328
+ const releaseParts = releaseTag.replace(/^v/i, "").split(".").map(Number);
3329
+ for (let i = 0; i < Math.max(currentParts.length, releaseParts.length); i++) {
3330
+ const current = currentParts[i] || 0;
3331
+ const releasePart = releaseParts[i] || 0;
3332
+ if (releasePart > current) return true;
3333
+ if (releasePart < current) return false;
3334
+ }
3335
+ return false;
3336
+ }
3337
+ var DocksReleaseHistory = class DocksReleaseHistory extends DocksPart {
3338
+ constructor(..._args) {
3339
+ super(..._args);
3340
+ this.scrollMode = "none";
3341
+ this.appVersion = "0.0.0";
3342
+ this.releases = [];
3343
+ this.currentReleaseIndex = 0;
3344
+ }
3345
+ static {
3346
+ this.styles = [css`
3347
+ .release-panel {
3348
+ display: flex;
3349
+ flex-direction: column;
3350
+ gap: 0.5rem;
3351
+ padding: 0.75rem;
3352
+ box-sizing: border-box;
3353
+ }
3354
+
3355
+ .release-body {
3356
+ white-space: normal;
3357
+ }
3358
+ `];
3359
+ }
3360
+ doInitUI() {
3361
+ this.load();
3362
+ }
3363
+ async load() {
3364
+ const app = appLoaderService.getCurrentApp();
3365
+ this.appVersion = app?.version ?? "0.0.0";
3366
+ this.releases = await loadReleases(app?.releaseHistory);
3367
+ const currentIndex = this.releases.findIndex((release) => semverEqual(release.tag_name, this.appVersion));
3368
+ this.currentReleaseIndex = currentIndex >= 0 ? currentIndex : 0;
3369
+ this.notifyChanged();
3370
+ }
3371
+ getNavState() {
3372
+ return {
3373
+ hasMultiple: this.releases.length > 1,
3374
+ atOldest: this.currentReleaseIndex >= this.releases.length - 1,
3375
+ atNewest: this.currentReleaseIndex <= 0
3376
+ };
3377
+ }
3378
+ showOlder() {
3379
+ if (this.currentReleaseIndex < this.releases.length - 1) {
3380
+ this.currentReleaseIndex += 1;
3381
+ this.notifyChanged();
3382
+ }
3383
+ }
3384
+ showNewer() {
3385
+ if (this.currentReleaseIndex > 0) {
3386
+ this.currentReleaseIndex -= 1;
3387
+ this.notifyChanged();
3388
+ }
3389
+ }
3390
+ notifyChanged() {
3391
+ this.dispatchEvent(new CustomEvent("release-history-changed", {
3392
+ bubbles: true,
3393
+ composed: true
3394
+ }));
3395
+ }
3396
+ renderContent() {
3397
+ const release = this.releases[this.currentReleaseIndex];
3398
+ if (!release) return html`<div class="release-panel">No release history available.</div>`;
3399
+ const isCurrent = semverEqual(release.tag_name, this.appVersion);
3400
+ const showUpdateHint = !isCurrent && isNewerThan(release.tag_name, this.appVersion);
3401
+ const bodyHtml = release.body ? parseMarkdownHtml(release.body) : "";
3402
+ return html`
3403
+ <div class="release-panel">
3404
+ <p>
3405
+ <strong>Version:</strong> ${release.tag_name}${isCurrent ? " (Current)" : ""}
3406
+ </p>
3407
+ <p>
3408
+ <strong>Released:</strong>
3409
+ <wa-format-date
3410
+ .date=${release.published_at}
3411
+ month="short"
3412
+ day="numeric"
3413
+ year="numeric"
3414
+ ></wa-format-date>
3415
+ </p>
3416
+ ${showUpdateHint ? html`
3417
+ <wa-callout variant="warning">
3418
+ <wa-icon slot="icon" name="triangle-exclamation"></wa-icon>
3419
+ A newer release is available than the version you are running.
3420
+ Use <strong>Update Available</strong> in the toolbar when it appears.
3421
+ </wa-callout>
3422
+ ` : ""}
3423
+ ${bodyHtml ? html`<div class="release-body">${unsafeHTML(bodyHtml)}</div>` : ""}
3424
+ </div>
3425
+ `;
3426
+ }
3427
+ };
3428
+ _decorate([state()], DocksReleaseHistory.prototype, "appVersion", void 0);
3429
+ _decorate([state()], DocksReleaseHistory.prototype, "releases", void 0);
3430
+ _decorate([state()], DocksReleaseHistory.prototype, "currentReleaseIndex", void 0);
3431
+ DocksReleaseHistory = _decorate([customElement("docks-release-history")], DocksReleaseHistory);
3432
+ //#endregion
3433
+ //#region src/dialogs/version-info-dialog.ts
3434
+ var EMPTY_NAV = {
3435
+ hasMultiple: false,
3436
+ atOldest: true,
3437
+ atNewest: true
3438
+ };
3439
+ var DocksVersionInfoDialog = class DocksVersionInfoDialog extends DocksDialogContent {
3440
+ constructor(..._args) {
3441
+ super(..._args);
3442
+ this.label = "";
3443
+ this.activeTab = null;
3444
+ this.releaseNav = EMPTY_NAV;
3445
+ this.onReleaseHistoryChanged = () => {
3446
+ this.syncReleaseNav();
3447
+ };
3448
+ }
3449
+ static {
3450
+ this.styles = [...DocksDialogContent.styles, css`
3451
+ :host {
3452
+ display: block;
3453
+ }
3454
+
3455
+ wa-dialog {
3456
+ --width: 600px;
3457
+ }
3458
+
3459
+ wa-dialog::part(body) {
3460
+ display: flex;
3461
+ flex-direction: column;
3462
+ min-height: 0;
3463
+ overflow: hidden;
3464
+ }
3465
+
3466
+ .dialog-body {
3467
+ display: flex;
3468
+ flex-direction: column;
3469
+ gap: 0.75rem;
3470
+ min-width: 32rem;
3471
+ height: min(70vh, 36rem);
3472
+ min-height: min(70vh, 36rem);
3473
+ overflow: hidden;
3474
+ }
3475
+
3476
+ .dialog-body docks-tabs {
3477
+ flex: 1 1 auto;
3478
+ min-height: 0;
3479
+ overflow: hidden;
3480
+ }
3481
+
3482
+ .dialog-footer {
3483
+ display: flex;
3484
+ gap: 0.5rem;
3485
+ justify-content: flex-end;
3486
+ width: 100%;
3487
+ }
3488
+ `];
3489
+ }
3490
+ firstUpdated(changed) {
3491
+ super.firstUpdated(changed);
3492
+ this.addEventListener("release-history-changed", this.onReleaseHistoryChanged);
3493
+ this.updateComplete.then(() => {
3494
+ this.attachTabListener();
3495
+ this.syncActiveTab();
3496
+ this.syncReleaseNav();
3497
+ });
3498
+ }
3499
+ disconnectedCallback() {
3500
+ this.removeEventListener("release-history-changed", this.onReleaseHistoryChanged);
3501
+ super.disconnectedCallback();
3502
+ }
3503
+ attachTabListener() {
3504
+ const tabGroup = this.getAboutTabs()?.shadowRoot?.querySelector("wa-tab-group");
3505
+ if (!tabGroup || tabGroup.hasAttribute("data-version-info-listener")) return;
3506
+ tabGroup.setAttribute("data-version-info-listener", "");
3507
+ tabGroup.addEventListener("wa-tab-show", (event) => {
3508
+ const name = event.detail?.name;
3509
+ if (name) this.activeTab = name;
3510
+ });
3511
+ }
3512
+ getAboutTabs() {
3513
+ return this.renderRoot.querySelector("docks-tabs");
3514
+ }
3515
+ getReleaseHistory() {
3516
+ const part = this.getAboutTabs()?.getTabPart(ABOUT_TAB_RELEASE);
3517
+ return part instanceof DocksReleaseHistory ? part : null;
3518
+ }
3519
+ syncActiveTab() {
3520
+ const tabGroup = this.getAboutTabs()?.shadowRoot?.querySelector("wa-tab-group");
3521
+ this.activeTab = tabGroup?.getAttribute("active") || tabGroup?.active || null;
3522
+ }
3523
+ syncReleaseNav() {
3524
+ this.releaseNav = this.getReleaseHistory()?.getNavState() ?? EMPTY_NAV;
3525
+ }
3526
+ handleClose() {
3527
+ const dialog = this.renderRoot.querySelector("wa-dialog");
3528
+ if (dialog && dialog.open !== false) {
3529
+ dialog.open = false;
3530
+ return;
3531
+ }
3532
+ this.close?.();
3533
+ }
3534
+ handleAfterHide() {
3535
+ this.onAfterHide?.();
3536
+ }
3537
+ showOlderRelease() {
3538
+ this.getReleaseHistory()?.showOlder();
3539
+ }
3540
+ showNewerRelease() {
3541
+ this.getReleaseHistory()?.showNewer();
3542
+ }
3543
+ render() {
3544
+ const app = appLoaderService.getCurrentApp();
3545
+ const showReleaseNav = this.activeTab === "about.tab.release" && this.releaseNav.hasMultiple;
3546
+ return html`
3547
+ <wa-dialog
3548
+ label=${this.label}
3549
+ open
3550
+ light-dismiss
3551
+ with-footer
3552
+ @wa-after-hide=${this.handleAfterHide}
3553
+ >
3554
+ <div class="dialog-body">
3555
+ ${app?.description ? html`<small>${app.description}</small>` : ""}
3556
+ <docks-tabs id=${ABOUT_TABS} fill-height></docks-tabs>
3557
+ </div>
3558
+ <div slot="footer" class="dialog-footer">
3559
+ ${showReleaseNav ? html`
3560
+ <wa-button
3561
+ variant="default"
3562
+ ?disabled=${this.releaseNav.atOldest}
3563
+ @click=${this.showOlderRelease}
3564
+ >
3565
+ ← Previous
3566
+ </wa-button>
3567
+ <wa-button
3568
+ variant="default"
3569
+ ?disabled=${this.releaseNav.atNewest}
3570
+ @click=${this.showNewerRelease}
3571
+ >
3572
+ Next →
3573
+ </wa-button>
3574
+ ` : nothing}
3575
+ <wa-button variant="primary" @click=${this.handleClose}>Close</wa-button>
3576
+ </div>
3577
+ </wa-dialog>
3578
+ `;
3579
+ }
3580
+ };
3581
+ _decorate([property({ type: String })], DocksVersionInfoDialog.prototype, "label", void 0);
3582
+ _decorate([property({ attribute: false })], DocksVersionInfoDialog.prototype, "close", void 0);
3583
+ _decorate([property({ attribute: false })], DocksVersionInfoDialog.prototype, "onAfterHide", void 0);
3584
+ _decorate([state()], DocksVersionInfoDialog.prototype, "activeTab", void 0);
3585
+ _decorate([state()], DocksVersionInfoDialog.prototype, "releaseNav", void 0);
3586
+ DocksVersionInfoDialog = _decorate([customElement("docks-version-info-dialog")], DocksVersionInfoDialog);
3587
+ contributionRegistry.registerContribution(DIALOG_CONTRIBUTION_TARGET, {
3588
+ id: "version-info",
3589
+ label: "About",
3590
+ selfContained: true,
3591
+ component: (state) => html`
3592
+ <docks-version-info-dialog
3593
+ .label=${state?.label ?? ""}
3594
+ .close=${state?.close}
3595
+ .onAfterHide=${state?.onAfterHide}
3596
+ ></docks-version-info-dialog>
3597
+ `
3598
+ });
3599
+ function versionInfoDialog() {
3600
+ const app = appLoaderService.getCurrentApp();
3601
+ return dialogService.open("version-info", { label: `About ${app?.name ?? ""} - ${app?.version ?? "0.0.0"}` });
3602
+ }
3278
3603
  //#endregion
3279
3604
  //#region src/core/dropdown-menu-utils.ts
3605
+ function isOpenDropdown(element) {
3606
+ if (element.localName !== "wa-dropdown") return false;
3607
+ const dropdown = element;
3608
+ return dropdown.open === true || dropdown.hasAttribute("open");
3609
+ }
3610
+ function elementsFromEventPath(event) {
3611
+ const fromComposedPath = event.composedPath().filter((node) => node instanceof Element);
3612
+ if (fromComposedPath.length > 0) return fromComposedPath;
3613
+ if (!(event.target instanceof Element)) return [];
3614
+ const path = [];
3615
+ let current = event.target;
3616
+ while (current) {
3617
+ path.push(current);
3618
+ const root = current.getRootNode();
3619
+ if (root instanceof ShadowRoot) {
3620
+ current = root.host;
3621
+ continue;
3622
+ }
3623
+ current = current.parentElement;
3624
+ }
3625
+ return path;
3626
+ }
3627
+ function elementTargetsOpenDropdown(element) {
3628
+ if (isOpenDropdown(element)) return true;
3629
+ if (element.localName !== "wa-dropdown-item") return false;
3630
+ const dropdown = element.closest("wa-dropdown");
3631
+ if (dropdown && isOpenDropdown(dropdown)) return true;
3632
+ const root = element.getRootNode();
3633
+ return root instanceof ShadowRoot && root.host instanceof Element && isOpenDropdown(root.host);
3634
+ }
3635
+ /** True when the event occurred inside a wa-dropdown that is currently open. */
3636
+ function eventTargetsOpenDropdown(event) {
3637
+ return elementsFromEventPath(event).some(elementTargetsOpenDropdown);
3638
+ }
3639
+ /** Prevents the browser context menu on open Docks popup lists (context menus, toolbar dropdowns). */
3640
+ function suppressNativeContextMenuInOpenDropdowns(event) {
3641
+ if (!event.cancelable || !eventTargetsOpenDropdown(event)) return;
3642
+ event.preventDefault();
3643
+ }
3644
+ var openDropdownContextMenuGuardInstalled = false;
3645
+ function installOpenDropdownNativeContextMenuGuard() {
3646
+ if (openDropdownContextMenuGuardInstalled || typeof document === "undefined") return;
3647
+ openDropdownContextMenuGuardInstalled = true;
3648
+ document.addEventListener("contextmenu", suppressNativeContextMenuInOpenDropdowns, { capture: true });
3649
+ }
3280
3650
  function visitElementTree(root, visit) {
3281
3651
  if (root instanceof Element) {
3282
3652
  visit(root);
@@ -3299,213 +3669,354 @@ function closeSiblingSubmenus(activeItem, menuRoot) {
3299
3669
  });
3300
3670
  }
3301
3671
  //#endregion
3302
- //#region src/components/command.ts
3303
- var DocksCommand = class DocksCommand extends DocksWidget {
3672
+ //#region src/core/icon-utils.ts
3673
+ /**
3674
+ * Parses an icon spec string into `{ library, name }` for use with `wa-icon`.
3675
+ *
3676
+ * Icon specs support an optional icon library prefix followed by the icon name,
3677
+ * separated by whitespace, e.g. `"docks mark-github"`.
3678
+ *
3679
+ * - When the spec contains **no whitespace**, it is treated as the icon name only
3680
+ * and `library` is omitted so the default Web Awesome / Font Awesome library is used.
3681
+ * - When the spec contains **whitespace**, the **last segment** is treated as
3682
+ * the icon name and every prior segment (joined by a single space) becomes `library`.
3683
+ *
3684
+ * Prefer {@link icon} for rendering; use parseIconSpec only when you need the parsed parts.
3685
+ */
3686
+ function parseIconSpec(spec) {
3687
+ const trimmed = (spec ?? "").trim();
3688
+ if (!trimmed) return { name: "" };
3689
+ const parts = trimmed.split(/\s+/);
3690
+ if (parts.length <= 1) return { name: trimmed };
3691
+ return {
3692
+ name: parts.pop(),
3693
+ library: parts.join(" ")
3694
+ };
3695
+ }
3696
+ /**
3697
+ * Returns a Lit template that renders a `wa-icon` for the given spec.
3698
+ * Use this for all icon rendering so specs (e.g. contribution `icon` fields) stay consistent.
3699
+ * Handles undefined/null/empty spec internally; call sites may pass values directly.
3700
+ */
3701
+ function icon(spec, options) {
3702
+ const { name: iconName, library } = parseIconSpec(spec ?? "");
3703
+ return html`<wa-icon library=${library ?? nothing} name=${iconName} label=${options?.label ?? nothing} slot=${options?.slot ?? nothing}></wa-icon>`;
3704
+ }
3705
+ //#endregion
3706
+ //#region src/parts/toolbar.ts
3707
+ var RESIZE_DEBOUNCE_MS = 150;
3708
+ function renderButtonGroup(slotName, orientation, contributions, isToolbarItem, contributionCreator) {
3709
+ const label = `Toolbar ${slotName ?? "default"}`;
3710
+ const items = contributions.filter((c) => c.slot === slotName && isToolbarItem(c) && getContributionVisible(c));
3711
+ return html`
3712
+ <wa-button-group orientation=${orientation} label=${label}>
3713
+ ${slotName === "start" ? html`<slot name="start"></slot>` : slotName === "end" ? html`<slot name="end"></slot>` : html`<slot></slot>`}
3714
+ ${items.map(contributionCreator)}
3715
+ </wa-button-group>
3716
+ `;
3717
+ }
3718
+ var DocksToolbar = class DocksToolbar extends DocksElement {
3304
3719
  constructor(..._args) {
3305
3720
  super(..._args);
3306
- this.cmd = "";
3307
- this.title = "";
3308
- this.label = false;
3309
- this.disabled = false;
3310
- this.appearance = "plain";
3311
- this.variant = "neutral";
3721
+ this.position = "start";
3722
+ this.orientation = "horizontal";
3723
+ this.align = "start";
3312
3724
  this.size = "s";
3313
- this.params = {};
3314
- this.withCaret = true;
3315
- this.placement = "bottom-start";
3316
- this.dropdownContributions = [];
3317
- }
3318
- closeDropdown(dropdown) {
3319
- if (dropdown && dropdown.open !== void 0) dropdown.open = false;
3320
- }
3321
- closeParentDropdown() {
3322
- this.closeDropdown(this.closest("wa-dropdown"));
3323
- }
3324
- handleClick(event) {
3325
- if (this.disabled) return;
3326
- if (event) event.stopPropagation();
3327
- this.closeParentDropdown();
3328
- if (this.action) {
3329
- this.action(event);
3330
- return;
3331
- }
3332
- if (this.cmd) this.executeCommand(this.cmd, this.params);
3725
+ this.scopeTokens = [];
3726
+ this.partToolbarContent = void 0;
3727
+ this.partToolbarRenderer = void 0;
3728
+ this.contributions = [];
3729
+ this.compact = false;
3730
+ this.resizeObserver = null;
3731
+ this.resizeDebounceTimer = null;
3732
+ this.overflowCheckScheduled = false;
3733
+ this.onResize = () => {
3734
+ if (this.resizeDebounceTimer !== null) clearTimeout(this.resizeDebounceTimer);
3735
+ this.resizeDebounceTimer = setTimeout(() => {
3736
+ this.resizeDebounceTimer = null;
3737
+ this.updateCompactFromSpace();
3738
+ }, RESIZE_DEBOUNCE_MS);
3739
+ };
3333
3740
  }
3334
- handleSelect(event) {
3335
- this.closeDropdown(event.target);
3741
+ updateCompactFromSpace() {
3742
+ const toolbarItems = this.shadowRoot?.querySelector(".toolbar-items");
3743
+ if (!toolbarItems) return;
3744
+ const trimmed = toolbarItems.scrollWidth > toolbarItems.clientWidth;
3745
+ if (this.compact !== trimmed) this.compact = trimmed;
3336
3746
  }
3337
- isInDropdown() {
3338
- return !!this.closest("wa-dropdown, wa-dropdown-menu");
3747
+ scheduleOverflowCheck() {
3748
+ if (this.overflowCheckScheduled) return;
3749
+ this.overflowCheckScheduled = true;
3750
+ requestAnimationFrame(() => {
3751
+ this.overflowCheckScheduled = false;
3752
+ this.updateCompactFromSpace();
3753
+ });
3339
3754
  }
3340
- getKeybinding() {
3341
- if (!this.cmd || this.action) return null;
3342
- const keybindings = keyBindingManager.getKeyBindingsForCommand(this.cmd);
3343
- return keybindings.length > 0 ? keybindings[0] : null;
3755
+ connectedCallback() {
3756
+ super.connectedCallback();
3757
+ this.resizeObserver = new ResizeObserver(this.onResize);
3758
+ this.resizeObserver.observe(this);
3344
3759
  }
3345
- doBeforeUI() {
3346
- if (this.dropdown) {
3347
- this.dropdownContributions = contributionRegistry.getContributions(this.dropdown);
3348
- subscribe(TOPIC_CONTRIBUTEIONS_CHANGED, (event) => {
3349
- if (this.dropdown && event.target === this.dropdown) this.dropdownContributions = event.contributions;
3350
- });
3760
+ disconnectedCallback() {
3761
+ this.resizeObserver?.disconnect();
3762
+ this.resizeObserver = null;
3763
+ if (this.resizeDebounceTimer !== null) {
3764
+ clearTimeout(this.resizeDebounceTimer);
3765
+ this.resizeDebounceTimer = null;
3351
3766
  }
3767
+ super.disconnectedCallback();
3768
+ }
3769
+ updated(changedProperties) {
3770
+ super.updated?.(changedProperties);
3771
+ if (!this.compact) this.scheduleOverflowCheck();
3352
3772
  }
3353
3773
  willUpdate(changedProperties) {
3354
3774
  super.willUpdate?.(changedProperties);
3355
- if (this.dropdown && changedProperties.has("dropdown")) this.dropdownContributions = contributionRegistry.getContributions(this.dropdown);
3775
+ if (changedProperties.has("scopeTokens")) this.refreshContributions();
3356
3776
  }
3357
- renderContribution(contribution, slot) {
3358
- if ("command" in contribution) {
3359
- const commandContribution = contribution;
3360
- if (!getContributionVisible(commandContribution)) return nothing;
3361
- const disabled = getContributionDisabled(commandContribution);
3362
- return html`
3363
- <docks-command
3364
- slot=${slot ?? nothing}
3365
- cmd="${commandContribution.command}"
3366
- icon="${commandContribution.icon || ""}"
3367
- .params=${commandContribution.params || {}}
3368
- ?disabled="${disabled}">
3369
- ${commandContribution.label}
3370
- </docks-command>
3371
- `;
3372
- }
3373
- if ("component" in contribution) {
3374
- const contents = contribution.component;
3375
- if (contents instanceof Function) return contents();
3376
- return unsafeHTML(contents);
3377
- }
3378
- return nothing;
3777
+ attributeChangedCallback(name, old, value) {
3778
+ super.attributeChangedCallback(name, old, value);
3779
+ if (name === "id" && old !== value) this.refreshContributions();
3379
3780
  }
3380
- render() {
3381
- const keybinding = this.getKeybinding();
3382
- if (this.isInDropdown() && this.dropdown) return html`
3383
- <wa-dropdown-item ?disabled=${this.disabled}>
3384
- ${icon(this.icon, {
3385
- label: this.title,
3386
- slot: "icon"
3387
- })}
3388
- <slot></slot>
3389
- ${this.dropdownContributions.map((c) => this.renderContribution(c, "submenu"))}
3390
- </wa-dropdown-item>
3391
- `;
3392
- if (this.isInDropdown()) return html`
3393
- <wa-dropdown-item
3394
- ?disabled=${this.disabled}
3395
- @click=${(e) => this.handleClick(e)}>
3396
- ${icon(this.icon, {
3397
- label: this.title,
3398
- slot: "icon"
3399
- })}
3400
- <slot></slot>
3401
- ${keybinding ? html`<span class="keybinding">${keybinding}</span>` : ""}
3402
- </wa-dropdown-item>
3403
- `;
3404
- if (this.dropdown) return html`
3405
- <wa-dropdown
3406
- placement=${this.placement}
3407
- @wa-select=${(e) => this.handleSelect(e)}>
3408
- <wa-button
3409
- slot="trigger"
3410
- appearance=${this.appearance}
3411
- variant=${this.variant}
3412
- size=${this.size}
3413
- ?disabled=${this.disabled}
3414
- ?with-caret=${this.withCaret}
3415
- title=${keybinding ? `${this.title} (${keybinding})` : this.title}>
3416
- ${icon(this.icon, {
3417
- label: this.title,
3418
- slot: "start"
3419
- })}
3420
- <slot></slot>
3421
- ${this.label ? this.title : nothing}
3422
- </wa-button>
3423
-
3424
- ${this.title ? html`
3425
- <h6 style="padding: var(--wa-space-xs) var(--wa-space-s); margin: 0; color: var(--wa-color-neutral-50); font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;">
3426
- ${this.title}
3427
- </h6>
3428
- ` : nothing}
3429
-
3430
- ${this.dropdownContributions.map((c) => this.renderContribution(c))}
3431
-
3432
- ${this.cmd ? html`
3433
- <wa-divider></wa-divider>
3434
- <docks-command
3435
- cmd="${this.cmd}"
3436
- icon="${this.icon || ""}"
3437
- .params=${this.params}
3438
- ?disabled=${this.disabled}>
3439
- <slot></slot>
3440
- ${this.title}
3441
- </docks-command>
3442
- ` : nothing}
3443
- </wa-dropdown>
3444
- `;
3781
+ doBeforeUI() {
3782
+ this.refreshContributions();
3783
+ subscribe(TOPIC_CONTRIBUTEIONS_CHANGED, (event) => {
3784
+ const id = this.getAttribute("id");
3785
+ if (!id) return;
3786
+ if (this.matchesTarget(id, event.target)) this.refreshContributions();
3787
+ });
3788
+ }
3789
+ refreshContributions() {
3790
+ const id = this.getAttribute("id");
3791
+ if (!id) {
3792
+ this.contributions = [];
3793
+ return;
3794
+ }
3795
+ this.loadContributions(id);
3796
+ }
3797
+ matchesTarget(id, target) {
3798
+ if (target === id) return true;
3799
+ if (!id.includes(":")) return false;
3800
+ const [prefix] = id.split(":");
3801
+ if (target === `${prefix}:*`) return true;
3802
+ const targetParts = target.split(":");
3803
+ if (targetParts.length === 2) {
3804
+ const categoryToken = targetParts[1];
3805
+ if (this.scopeTokens.includes(categoryToken)) return id.startsWith(`${prefix}:`);
3806
+ }
3807
+ return false;
3808
+ }
3809
+ loadContributions(id) {
3810
+ const specific = contributionRegistry.getContributions(id);
3811
+ if (!id.includes(":")) {
3812
+ this.contributions = specific;
3813
+ return;
3814
+ }
3815
+ const [prefix] = id.split(":");
3816
+ const wildcardId = `${prefix}:*`;
3817
+ const wildcard = contributionRegistry.getContributions(wildcardId);
3818
+ const categoryMatches = [];
3819
+ for (const category of this.scopeTokens) {
3820
+ const categoryId = `${prefix}:${category}`;
3821
+ const matches = contributionRegistry.getContributions(categoryId);
3822
+ categoryMatches.push(...matches);
3823
+ }
3824
+ this.contributions = [
3825
+ ...wildcard,
3826
+ ...categoryMatches,
3827
+ ...specific
3828
+ ];
3829
+ }
3830
+ isToolbarItem(contribution) {
3831
+ return "command" in contribution || "component" in contribution;
3832
+ }
3833
+ contributionCreator(contribution) {
3834
+ if ("command" in contribution) {
3835
+ const commandContribution = contribution;
3836
+ const showLabel = !this.compact && !!commandContribution.showLabel;
3837
+ return html`
3838
+ <wa-button @click=${() => void this.executeCommand(commandContribution.command, commandContribution.params || {})}
3839
+ title=${commandContribution.label}
3840
+ ?disabled="${getContributionDisabled(commandContribution)}"
3841
+ appearance="plain" size=${this.size}>
3842
+ ${icon(commandContribution.icon, { label: commandContribution.label })}
3843
+ ${showLabel ? commandContribution.label : ""}
3844
+ </wa-button>
3845
+ `;
3846
+ }
3847
+ if ("component" in contribution) {
3848
+ const contents = contribution.component;
3849
+ if (contents instanceof Function) return contents();
3850
+ return unsafeHTML(contents);
3851
+ }
3852
+ return html`<span>unknown contribution type: ${typeof contribution}</span>`;
3853
+ }
3854
+ render() {
3855
+ const partContent = this.partToolbarRenderer ? this.partToolbarRenderer() : this.partToolbarContent ? this.partToolbarContent : "";
3856
+ const flexDir = this.orientation === "vertical" ? "column" : "row";
3857
+ const alignMap = {
3858
+ start: "flex-start",
3859
+ center: "center",
3860
+ end: "flex-end"
3861
+ };
3862
+ const bindCreator = this.contributionCreator.bind(this);
3863
+ const bindIsItem = this.isToolbarItem.bind(this);
3445
3864
  return html`
3446
- <wa-button
3447
- appearance=${this.appearance}
3448
- variant=${this.variant}
3449
- size=${this.size}
3450
- ?disabled=${this.disabled}
3451
- title=${keybinding ? `${this.title} (${keybinding})` : this.title}
3452
- @click=${(e) => this.handleClick(e)}>
3453
- ${icon(this.icon, {
3454
- label: this.title,
3455
- slot: "start"
3456
- })}
3457
- <slot></slot>
3458
- </wa-button>
3865
+ <div class="toolbar-items" style=${styleMap({
3866
+ "flex-direction": flexDir,
3867
+ "align-items": alignMap[this.align],
3868
+ "justify-content": this.position
3869
+ })}>
3870
+ ${renderButtonGroup("start", this.orientation, this.contributions, bindIsItem, bindCreator)}
3871
+ ${partContent}
3872
+ ${renderButtonGroup(void 0, this.orientation, this.contributions, bindIsItem, bindCreator)}
3873
+ ${renderButtonGroup("end", this.orientation, this.contributions, bindIsItem, bindCreator)}
3874
+ </div>
3459
3875
  `;
3460
3876
  }
3461
3877
  static {
3462
3878
  this.styles = css`
3463
3879
  :host {
3464
- display: inline-block;
3880
+ display: flex;
3881
+ flex-direction: row;
3882
+ --wa-form-control-padding-inline: var(--wa-space-2xs);
3465
3883
  }
3466
3884
 
3467
- .keybinding {
3468
- margin-left: auto;
3469
- padding: 2px 6px;
3470
- background: var(--wa-color-neutral-15);
3471
- border: 1px solid var(--wa-color-neutral-25);
3472
- border-radius: 3px;
3473
- font-size: 10px;
3474
- font-family: monospace;
3475
- opacity: 0.7;
3885
+ :host([orientation="vertical"]) {
3886
+ flex-direction: column;
3476
3887
  }
3477
3888
 
3478
- :host-context(.wa-light) .keybinding {
3479
- background: var(--wa-color-neutral-85);
3480
- border: 1px solid var(--wa-color-neutral-75);
3889
+ .toolbar-items {
3890
+ display: flex;
3891
+ flex: 1;
3892
+ gap: var(--wa-space-2xs);
3481
3893
  }
3482
3894
  `;
3483
3895
  }
3484
3896
  };
3485
- _decorate([property()], DocksCommand.prototype, "cmd", void 0);
3486
- _decorate([property({
3487
- type: Object,
3488
- attribute: false
3489
- })], DocksCommand.prototype, "action", void 0);
3490
- _decorate([property()], DocksCommand.prototype, "title", void 0);
3491
- _decorate([property()], DocksCommand.prototype, "label", void 0);
3492
- _decorate([property()], DocksCommand.prototype, "icon", void 0);
3493
- _decorate([property({ type: Boolean })], DocksCommand.prototype, "disabled", void 0);
3494
- _decorate([property()], DocksCommand.prototype, "appearance", void 0);
3495
- _decorate([property()], DocksCommand.prototype, "variant", void 0);
3496
- _decorate([property()], DocksCommand.prototype, "size", void 0);
3497
- _decorate([property({
3498
- type: Object,
3499
- attribute: false
3500
- })], DocksCommand.prototype, "params", void 0);
3501
- _decorate([property()], DocksCommand.prototype, "dropdown", void 0);
3502
- _decorate([property({
3503
- type: Boolean,
3504
- attribute: "with-caret"
3505
- })], DocksCommand.prototype, "withCaret", void 0);
3506
- _decorate([property()], DocksCommand.prototype, "placement", void 0);
3507
- _decorate([state()], DocksCommand.prototype, "dropdownContributions", void 0);
3508
- DocksCommand = _decorate([customElement("docks-command")], DocksCommand);
3897
+ _decorate([property()], DocksToolbar.prototype, "position", void 0);
3898
+ _decorate([property({ reflect: true })], DocksToolbar.prototype, "orientation", void 0);
3899
+ _decorate([property({ reflect: true })], DocksToolbar.prototype, "align", void 0);
3900
+ _decorate([property({ reflect: true })], DocksToolbar.prototype, "size", void 0);
3901
+ _decorate([property({ attribute: false })], DocksToolbar.prototype, "scopeTokens", void 0);
3902
+ _decorate([property({ attribute: false })], DocksToolbar.prototype, "partToolbarContent", void 0);
3903
+ _decorate([property({ attribute: false })], DocksToolbar.prototype, "partToolbarRenderer", void 0);
3904
+ _decorate([state()], DocksToolbar.prototype, "contributions", void 0);
3905
+ _decorate([state()], DocksToolbar.prototype, "compact", void 0);
3906
+ DocksToolbar = _decorate([customElement("docks-toolbar")], DocksToolbar);
3907
+ //#endregion
3908
+ //#region src/core/dropdown-item.ts
3909
+ /**
3910
+ * Shared render helpers for {@link https://webawesome.com/docs/components/dropdown-item | wa-dropdown-item}
3911
+ * rows inside any Docks popup list.
3912
+ *
3913
+ * **Docks surfaces** (distinct UX and contribution targets):
3914
+ * - **Toolbar dropdown** — anchored to a toolbar control (`docks-command`, `docks-toolbar`).
3915
+ * Part-level registry targets use the `toolbar:` prefix (e.g. `toolbar:view.filebrowser`).
3916
+ * - **Context menu** — opened at the pointer (`docks-contextmenu`).
3917
+ * Part-level registry targets use the `contextmenu:` prefix (e.g. `contextmenu:view.filebrowser`).
3918
+ *
3919
+ * A part can expose both surfaces with the same part id but different prefixes, registering
3920
+ * different contributions on each. Feature-scoped targets (e.g. `filebrowser.create`) may also
3921
+ * be referenced explicitly from either surface when several controls share the same item set.
3922
+ *
3923
+ * Command-backed items rely on the parent {@link https://webawesome.com/docs/components/dropdown | wa-dropdown}
3924
+ * `wa-select` event (see {@link handleDropdownWaSelect}) so the popup closes cleanly before
3925
+ * native dialogs such as `showDirectoryPicker` open.
3926
+ */
3927
+ function parseDropdownItemParams(item) {
3928
+ const raw = item.getAttribute("data-params");
3929
+ if (!raw) return {};
3930
+ try {
3931
+ return JSON.parse(raw);
3932
+ } catch {
3933
+ return {};
3934
+ }
3935
+ }
3936
+ function getDropdownItemCommand(item) {
3937
+ return item.getAttribute("data-cmd") || item.getAttribute("value") || void 0;
3938
+ }
3939
+ /** Handles `wa-select` from a parent `wa-dropdown` and runs the backing command. */
3940
+ function handleDropdownWaSelect(event) {
3941
+ const item = event.detail?.item;
3942
+ if (!item || item.disabled || item.hasAttribute("disabled")) return;
3943
+ const cmd = getDropdownItemCommand(item);
3944
+ if (!cmd) return;
3945
+ commandRegistry.execute(cmd, commandRegistry.createExecutionContext(parseDropdownItemParams(item)));
3946
+ }
3947
+ function onDropdownActionClick(options) {
3948
+ return (event) => {
3949
+ if (readReactiveBoolean(options.disabled, false)) return;
3950
+ event.stopPropagation();
3951
+ options.action?.(event);
3952
+ };
3953
+ }
3954
+ function keybindingDetails(cmd) {
3955
+ if (!cmd) return nothing;
3956
+ const bindings = keyBindingManager.getKeyBindingsForCommand(cmd);
3957
+ if (bindings.length === 0) return nothing;
3958
+ return html`<span slot="details">${bindings[0]}</span>`;
3959
+ }
3960
+ /** Renders a native {@link https://webawesome.com/docs/components/dropdown-item | wa-dropdown-item}. */
3961
+ function renderDropdownItem(options) {
3962
+ const iconSpec = options.icon ?? "";
3963
+ const paramsJson = options.params && Object.keys(options.params).length > 0 ? JSON.stringify(options.params) : void 0;
3964
+ return html`
3965
+ <wa-dropdown-item
3966
+ slot=${options.slot ?? nothing}
3967
+ variant=${options.variant ?? "default"}
3968
+ value=${options.cmd ?? nothing}
3969
+ data-cmd=${options.cmd ?? nothing}
3970
+ data-params=${paramsJson ?? nothing}
3971
+ ?disabled=${readReactiveBoolean(options.disabled, false)}
3972
+ @click=${options.action ? onDropdownActionClick(options) : nothing}>
3973
+ ${iconSpec ? icon(iconSpec, {
3974
+ label: options.title ?? options.label,
3975
+ slot: "icon"
3976
+ }) : nothing}
3977
+ ${options.label}
3978
+ ${keybindingDetails(options.cmd)}
3979
+ </wa-dropdown-item>
3980
+ `;
3981
+ }
3982
+ function renderCommandContribution(contribution, options) {
3983
+ if (!getContributionVisible(contribution)) return nothing;
3984
+ return renderDropdownItem({
3985
+ cmd: contribution.command,
3986
+ icon: contribution.icon,
3987
+ label: contribution.label,
3988
+ params: contribution.params,
3989
+ disabled: contribution.disabled,
3990
+ slot: options?.slot
3991
+ });
3992
+ }
3993
+ function renderDropdownContribution(contribution, options) {
3994
+ if ("command" in contribution) return renderCommandContribution(contribution, options);
3995
+ if ("component" in contribution) {
3996
+ if (!getContributionVisible(contribution)) return nothing;
3997
+ const contents = contribution.component;
3998
+ if (contents instanceof Function) return contents();
3999
+ return html`${unsafeHTML(contents)}`;
4000
+ }
4001
+ return nothing;
4002
+ }
4003
+ /** Renders all visible contributions registered for a contribution target. */
4004
+ function renderDropdownContributions(target, options) {
4005
+ return html`${contributionRegistry.getContributions(target).map((c) => renderDropdownContribution(c, options)).filter((item) => item !== nothing)}`;
4006
+ }
4007
+ /** Renders a {@link wa-dropdown-item} with nested items from a contribution target in {@code slot="submenu"}. */
4008
+ function renderDropdownSubmenu(options) {
4009
+ return html`
4010
+ <wa-dropdown-item ?disabled=${readReactiveBoolean(options.disabled, false)}>
4011
+ ${options.icon ? icon(options.icon, {
4012
+ label: options.title ?? options.label,
4013
+ slot: "icon"
4014
+ }) : nothing}
4015
+ ${options.label}
4016
+ ${renderDropdownContributions(options.contributionTarget, { slot: "submenu" })}
4017
+ </wa-dropdown-item>
4018
+ `;
4019
+ }
3509
4020
  //#endregion
3510
4021
  //#region src/parts/contextmenu.ts
3511
4022
  var DocksContextMenu = class DocksContextMenu extends DocksElement {
@@ -3515,11 +4026,14 @@ var DocksContextMenu = class DocksContextMenu extends DocksElement {
3515
4026
  this.partContextMenuRenderer = void 0;
3516
4027
  this.contributions = [];
3517
4028
  this.isOpen = false;
3518
- this.position = {
4029
+ this.menuPosition = {
3519
4030
  x: 0,
3520
4031
  y: 0
3521
4032
  };
3522
- this.anchorRef = createRef();
4033
+ this.virtualAnchor = { getBoundingClientRect: () => {
4034
+ const { x, y } = this.menuPosition;
4035
+ return new DOMRect(x, y, 0, 0);
4036
+ } };
3523
4037
  this.dropdownRef = createRef();
3524
4038
  this.boundHandleDocumentPointerDown = this.handleDocumentPointerDown.bind(this);
3525
4039
  this.handleSubmenuOpening = (event) => {
@@ -3623,410 +4137,128 @@ var DocksContextMenu = class DocksContextMenu extends DocksElement {
3623
4137
  while (element) {
3624
4138
  const shadowRoot = element.shadowRoot;
3625
4139
  if (shadowRoot) {
3626
- const shadowElement = shadowRoot.elementFromPoint(x, y);
3627
- if (shadowElement && shadowElement !== element) {
3628
- element = shadowElement;
3629
- continue;
3630
- }
3631
- }
3632
- break;
3633
- }
3634
- return element;
3635
- }
3636
- getHostPart() {
3637
- const root = this.getRootNode();
3638
- if (root instanceof ShadowRoot && root.host instanceof HTMLElement) return root.host;
3639
- return null;
3640
- }
3641
- /**
3642
- * Triggers a click on the element under the cursor to update selection before showing context menu.
3643
- */
3644
- triggerClickUnderCursor(mouseEvent) {
3645
- const elementUnderCursor = this.getElementFromPoint(mouseEvent.clientX, mouseEvent.clientY);
3646
- if (elementUnderCursor) {
3647
- const clickEvent = new MouseEvent("click", {
3648
- bubbles: true,
3649
- cancelable: true,
3650
- view: window,
3651
- clientX: mouseEvent.clientX,
3652
- clientY: mouseEvent.clientY,
3653
- screenX: mouseEvent.screenX,
3654
- screenY: mouseEvent.screenY,
3655
- button: 0,
3656
- buttons: 0,
3657
- detail: 1,
3658
- which: 1
3659
- });
3660
- elementUnderCursor.dispatchEvent(clickEvent);
3661
- }
3662
- }
3663
- /** Opens the menu at `position`. Returns false when there is nothing to show (no thin empty panel). */
3664
- async show(position, mouseEvent) {
3665
- if (!this.hasMenuBody()) return false;
3666
- if (mouseEvent) {
3667
- this.triggerClickUnderCursor(mouseEvent);
3668
- await this.getHostPart()?.updateComplete;
3669
- }
3670
- if (this.isOpen) {
3671
- this.isOpen = false;
3672
- await this.updateComplete;
3673
- }
3674
- this.position = position;
3675
- this.isOpen = true;
3676
- await this.updateComplete;
3677
- document.addEventListener("pointerdown", this.boundHandleDocumentPointerDown, { capture: true });
3678
- return true;
3679
- }
3680
- onClose() {
3681
- if (!this.isOpen) return;
3682
- this.isOpen = false;
3683
- document.removeEventListener("pointerdown", this.boundHandleDocumentPointerDown, { capture: true });
3684
- }
3685
- renderContribution(contribution) {
3686
- if ("command" in contribution) {
3687
- const commandContribution = contribution;
3688
- if (!getContributionVisible(commandContribution)) return nothing;
3689
- const disabled = getContributionDisabled(commandContribution);
3690
- return html`
3691
- <docks-command
3692
- cmd="${commandContribution.command}"
3693
- icon="${commandContribution.icon ?? ""}"
3694
- .params=${commandContribution.params ?? {}}
3695
- ?disabled="${disabled}">
3696
- ${commandContribution.label}
3697
- </docks-command>
3698
- `;
3699
- } else if ("component" in contribution) {
3700
- const contents = contribution.component;
3701
- if (contents instanceof Function) return contents();
3702
- return unsafeHTML(contents);
3703
- }
3704
- return nothing;
3705
- }
3706
- render() {
3707
- const partContent = this.partContextMenuRenderer ? this.partContextMenuRenderer() : nothing;
3708
- return html`
3709
- <wa-dropdown
3710
- ${ref(this.dropdownRef)}
3711
- ?open=${this.isOpen}
3712
- @submenu-opening=${this.handleSubmenuOpening}
3713
- @wa-after-hide=${this.onClose}>
3714
-
3715
- <div
3716
- slot="trigger"
3717
- ${ref(this.anchorRef)}
3718
- style="position: fixed;
3719
- left: ${this.position.x}px;
3720
- top: ${this.position.y}px;
3721
- width: 1px;
3722
- height: 1px;
3723
- pointer-events: none;">
3724
- </div>
3725
-
3726
- ${partContent}
3727
- ${this.contributions.map((c) => this.renderContribution(c))}
3728
- </wa-dropdown>
3729
- `;
3730
- }
3731
- static {
3732
- this.styles = css`
3733
- :host {
3734
- position: fixed;
3735
- top: 0;
3736
- left: 0;
3737
- width: 0;
3738
- height: 0;
3739
- pointer-events: none;
3740
- z-index: 10000;
3741
- }
3742
-
3743
- wa-dropdown {
3744
- pointer-events: none;
3745
- min-width: 200px;
3746
- }
3747
-
3748
- wa-dropdown:state(open) {
3749
- pointer-events: auto;
3750
- }
3751
-
3752
- wa-dropdown::part(menu) {
3753
- min-width: 200px;
3754
- }
3755
- `;
3756
- }
3757
- };
3758
- _decorate([property({ attribute: false })], DocksContextMenu.prototype, "scopeTokens", void 0);
3759
- _decorate([property({ attribute: false })], DocksContextMenu.prototype, "partContextMenuRenderer", void 0);
3760
- _decorate([state()], DocksContextMenu.prototype, "contributions", void 0);
3761
- _decorate([state()], DocksContextMenu.prototype, "isOpen", void 0);
3762
- _decorate([state()], DocksContextMenu.prototype, "position", void 0);
3763
- DocksContextMenu = _decorate([customElement("docks-contextmenu")], DocksContextMenu);
3764
- //#endregion
3765
- //#region src/parts/container.ts
3766
- var DocksContainer = class extends DocksElement {};
3767
- //#endregion
3768
- //#region src/parts/part.ts
3769
- var DocksPart = class DocksPart extends DocksContainer {
3770
- constructor(..._args) {
3771
- super(..._args);
3772
- this.scrollMode = "scroller";
3773
- this.dirty = false;
3774
- this.isEditor = false;
3775
- this.onContentContextMenu = async (event) => {
3776
- const contextMenu = this.renderRoot.querySelector("docks-contextmenu");
3777
- if (!contextMenu) return;
3778
- if (await contextMenu.show({
3779
- x: event.clientX,
3780
- y: event.clientY
3781
- }, event)) event.preventDefault();
3782
- };
3783
- }
3784
- getCommandStack() {
3785
- return this.commandStack;
3786
- }
3787
- /**
3788
- * Override this method to provide toolbar content for this part.
3789
- * This is a lightweight alternative to registering toolbar contributions
3790
- * for actions that are scoped to this part instance.
3791
- *
3792
- * IMPORTANT: Event handlers MUST use arrow functions to preserve the component's 'this' context.
3793
- * The toolbar template is rendered in a different component (docks-toolbar), so direct method
3794
- * references lose their binding.
3795
- *
3796
- * ✅ Correct:
3797
- * @click=${() => this.myMethod()}
3798
- * @click=${(e) => this.handleClick(e)}
3799
- *
3800
- * ❌ Wrong (this will be bound to the toolbar, not your component):
3801
- * @click=${this.myMethod}
3802
- *
3803
- * Example:
3804
- * ```typescript
3805
- * protected renderToolbar() {
3806
- * return html`
3807
- * <wa-button @click=${() => this.save()} title="Save">
3808
- * <wa-icon name="save"></wa-icon>
3809
- * </wa-button>
3810
- * `;
3811
- * }
3812
- * ```
3813
- *
3814
- * @returns TemplateResult with toolbar items, or nothing if no toolbar needed
3815
- */
3816
- renderToolbar() {
3817
- return nothing;
3818
- }
3819
- /**
3820
- * Activates the tab that contains this part by walking up the DOM to the first
3821
- * docks-tabs ancestor and activating the tab panel that contains this part.
3822
- * Crosses Shadow DOM boundaries (e.g. wa-tab-group) via getRootNode().host.
3823
- * No-op if this part is not inside a docks-tabs / wa-tab-panel.
3824
- */
3825
- activateContainingTab() {
3826
- let el = this;
3827
- let panelName = null;
3828
- let tabsEl = null;
3829
- while (el) {
3830
- const tag = el.tagName?.toLowerCase();
3831
- if (tag === "wa-tab-panel") panelName = el.getAttribute("name");
3832
- if (tag === "docks-tabs") {
3833
- tabsEl = el;
3834
- break;
3835
- }
3836
- const parent = el.parentElement;
3837
- if (parent) el = parent;
3838
- else {
3839
- const root = el.getRootNode();
3840
- el = root instanceof ShadowRoot ? root.host : null;
3841
- }
3842
- }
3843
- if (tabsEl && panelName != null && panelName !== "") tabsEl.activate(panelName);
3844
- }
3845
- /**
3846
- * Override this method to provide context menu content for this part.
3847
- * This is a lightweight alternative to registering context menu contributions
3848
- * for actions that are scoped to this part instance.
3849
- *
3850
- * IMPORTANT: Event handlers MUST use arrow functions to preserve the component's 'this' context.
3851
- * The context menu is rendered in a different component (contextmenu), so direct method
3852
- * references lose their binding.
3853
- *
3854
- * ✅ Correct:
3855
- * @click=${() => this.myMethod()}
3856
- * @click=${(e) => this.handleClick(e)}
3857
- *
3858
- * ❌ Wrong (this will be bound to the context menu, not your component):
3859
- * @click=${this.myMethod}
3860
- *
3861
- * Example:
3862
- * ```typescript
3863
- * protected renderContextMenu() {
3864
- * return html`
3865
- * <wa-dropdown-item @click=${() => this.open()}>
3866
- * <wa-icon name="folder-open"></wa-icon>
3867
- * Open
3868
- * </wa-dropdown-item>
3869
- * <wa-divider></wa-divider>
3870
- * <wa-dropdown-item @click=${() => this.delete()}>
3871
- * <wa-icon name="trash"></wa-icon>
3872
- * Delete
3873
- * </wa-dropdown-item>
3874
- * `;
3875
- * }
3876
- * ```
3877
- *
3878
- * @returns TemplateResult with context menu items, or nothing if no context menu needed
3879
- */
3880
- renderContextMenu() {
3881
- return nothing;
3882
- }
3883
- renderContent() {
3884
- return nothing;
3885
- }
3886
- getToolbarTarget() {
3887
- const contributionKey = this.tabContribution?.editorId ?? this.id ?? this.tabContribution?.name;
3888
- return contributionKey ? `toolbar:${contributionKey}` : void 0;
3889
- }
3890
- getContextMenuTarget() {
3891
- const contributionKey = this.tabContribution?.editorId ?? this.id ?? this.tabContribution?.name;
3892
- return contributionKey ? `contextmenu:${contributionKey}` : void 0;
3893
- }
3894
- syncIsEditorCapability() {
3895
- const next = this.save !== DocksPart.prototype.save;
3896
- if (next === this.isEditor) return;
3897
- this.isEditor = next;
3898
- }
3899
- maybeActivateForCoupledEditors() {
3900
- const coupled = this.tabContribution?.coupledEditors;
3901
- if (!coupled?.length) return;
3902
- const active = activeEditorSignal.get();
3903
- if (!(active instanceof DocksPart)) return;
3904
- const editorId = active.tabContribution?.editorId;
3905
- if (!editorId || !coupled.includes(editorId)) return;
3906
- this.activateContainingTab();
3907
- }
3908
- render() {
3909
- const toolbarTarget = this.getToolbarTarget();
3910
- const contextMenuTarget = this.getContextMenuTarget();
3911
- const toolbarEnabled = this.tabContribution?.toolbar !== false;
3912
- const contextMenuEnabled = this.tabContribution?.contextMenu !== false;
3913
- const scrollMode = this.scrollMode;
3914
- const scopeTokens = this.isEditor ? ["system.editors", ".system.editors"] : [];
3915
- const content = this.renderContent();
3916
- const contentNode = scrollMode === "scroller" ? html`
3917
- <wa-scroller class="part-content-scroll" orientation="vertical">
3918
- <div class="part-content-inner">${content}</div>
3919
- </wa-scroller>
3920
- ` : html`<div class="part-content-inner">${content}</div>`;
3921
- return html`
3922
- <div class="part-shell">
3923
- ${toolbarEnabled ? html`
3924
- <docks-toolbar
3925
- class="part-toolbar"
3926
- id=${ifDefined(toolbarTarget)}
3927
- .scopeTokens=${scopeTokens}
3928
- .partToolbarRenderer=${() => this.renderToolbar()}>
3929
- </docks-toolbar>
3930
- ` : nothing}
3931
- <div class="part-content ${scrollMode === "native" ? "native-scroll" : ""}" @contextmenu=${contextMenuEnabled ? this.onContentContextMenu : void 0}>
3932
- ${contentNode}
3933
- </div>
3934
- ${contextMenuEnabled ? html`
3935
- <docks-contextmenu
3936
- id=${ifDefined(contextMenuTarget)}
3937
- .scopeTokens=${scopeTokens}
3938
- .partContextMenuRenderer=${() => this.renderContextMenu()}>
3939
- </docks-contextmenu>
3940
- ` : nothing}
3941
- </div>
3942
- `;
4140
+ const shadowElement = shadowRoot.elementFromPoint(x, y);
4141
+ if (shadowElement && shadowElement !== element) {
4142
+ element = shadowElement;
4143
+ continue;
4144
+ }
4145
+ }
4146
+ break;
4147
+ }
4148
+ return element;
3943
4149
  }
3944
- updated(_changedProperties) {
3945
- super.updated(_changedProperties);
3946
- this.syncIsEditorCapability();
3947
- if (_changedProperties.has("tabContribution")) this.maybeActivateForCoupledEditors();
3948
- if (_changedProperties.has("dirty")) {
3949
- if (_changedProperties.get("dirty") !== void 0) this.dispatchEvent(new CustomEvent("dirty", {
3950
- detail: this.dirty,
3951
- bubbles: true
3952
- }));
4150
+ getHostPart() {
4151
+ const root = this.getRootNode();
4152
+ if (root instanceof ShadowRoot && root.host instanceof HTMLElement) return root.host;
4153
+ return null;
4154
+ }
4155
+ /**
4156
+ * Triggers a click on the element under the cursor to update selection before showing context menu.
4157
+ */
4158
+ triggerClickUnderCursor(mouseEvent) {
4159
+ const elementUnderCursor = this.getElementFromPoint(mouseEvent.clientX, mouseEvent.clientY);
4160
+ if (elementUnderCursor) {
4161
+ const clickEvent = new MouseEvent("click", {
4162
+ bubbles: true,
4163
+ cancelable: true,
4164
+ view: window,
4165
+ clientX: mouseEvent.clientX,
4166
+ clientY: mouseEvent.clientY,
4167
+ screenX: mouseEvent.screenX,
4168
+ screenY: mouseEvent.screenY,
4169
+ button: 0,
4170
+ buttons: 0,
4171
+ detail: 1,
4172
+ which: 1
4173
+ });
4174
+ elementUnderCursor.dispatchEvent(clickEvent);
3953
4175
  }
3954
4176
  }
3955
- doClose() {}
3956
- disconnectedCallback() {
3957
- super.disconnectedCallback();
4177
+ /** Opens the menu at `position`. Returns false when there is nothing to show (no thin empty panel). */
4178
+ async show(position, mouseEvent) {
4179
+ if (!this.hasMenuBody()) return false;
4180
+ if (mouseEvent) {
4181
+ this.triggerClickUnderCursor(mouseEvent);
4182
+ await this.getHostPart()?.updateComplete;
4183
+ }
4184
+ if (this.isOpen) {
4185
+ this.isOpen = false;
4186
+ await this.updateComplete;
4187
+ }
4188
+ this.menuPosition = position;
4189
+ this.isOpen = true;
4190
+ await this.updateComplete;
4191
+ this.syncVirtualPopupAnchor();
4192
+ document.addEventListener("pointerdown", this.boundHandleDocumentPointerDown, { capture: true });
4193
+ return true;
3958
4194
  }
3959
- close() {
3960
- this.doClose();
4195
+ /** Points wa-dropdown's internal wa-popup at the pointer via a VirtualElement anchor. */
4196
+ syncVirtualPopupAnchor() {
4197
+ const popup = this.dropdownRef.value?.shadowRoot?.querySelector("wa-popup");
4198
+ if (!popup) return;
4199
+ popup.anchor = this.virtualAnchor;
4200
+ popup.reposition();
3961
4201
  }
3962
- connectedCallback() {
3963
- super.connectedCallback();
3964
- this.syncIsEditorCapability();
3965
- queueMicrotask(() => this.syncIsEditorCapability());
3966
- this.watch(activeEditorSignal, () => this.maybeActivateForCoupledEditors());
4202
+ onClose() {
4203
+ if (!this.isOpen) return;
4204
+ this.isOpen = false;
4205
+ document.removeEventListener("pointerdown", this.boundHandleDocumentPointerDown, { capture: true });
3967
4206
  }
3968
- save() {}
3969
- isDirty() {
3970
- return this.dirty;
4207
+ updated(changedProperties) {
4208
+ super.updated(changedProperties);
4209
+ if (changedProperties.has("isOpen") && this.isOpen) this.syncVirtualPopupAnchor();
3971
4210
  }
3972
- markDirty(dirty) {
3973
- this.dirty = dirty;
3974
- partDirtySignal.set(null);
3975
- partDirtySignal.set(this);
3976
- activePartSignal.set(null);
3977
- activePartSignal.set(this);
4211
+ renderContribution(contribution) {
4212
+ return renderDropdownContribution(contribution);
4213
+ }
4214
+ render() {
4215
+ const partContent = this.partContextMenuRenderer ? this.partContextMenuRenderer() : nothing;
4216
+ return html`
4217
+ <wa-dropdown
4218
+ ${ref(this.dropdownRef)}
4219
+ ?open=${this.isOpen}
4220
+ @submenu-opening=${this.handleSubmenuOpening}
4221
+ @wa-select=${handleDropdownWaSelect}
4222
+ @wa-after-hide=${this.onClose}>
4223
+ <span slot="trigger" hidden aria-hidden="true"></span>
4224
+
4225
+ ${partContent}
4226
+ ${this.contributions.map((c) => this.renderContribution(c))}
4227
+ </wa-dropdown>
4228
+ `;
3978
4229
  }
3979
4230
  static {
3980
- this.baseStyles = css`
4231
+ this.styles = css`
3981
4232
  :host {
3982
- display: block;
3983
- }
3984
-
3985
- .part-shell {
3986
- display: grid;
3987
- grid-template-rows: auto minmax(0, 1fr);
3988
- height: 100%;
3989
- width: 100%;
3990
- position: relative;
3991
- overflow: hidden;
3992
- }
3993
-
3994
- .part-content {
3995
- min-height: 0;
3996
- overflow: hidden;
3997
- position: relative;
3998
- }
3999
-
4000
- .part-content.native-scroll {
4001
- overflow: auto;
4233
+ position: fixed;
4234
+ top: 0;
4235
+ left: 0;
4236
+ width: 0;
4237
+ height: 0;
4238
+ pointer-events: none;
4239
+ z-index: 10000;
4002
4240
  }
4003
4241
 
4004
- .part-content-scroll {
4005
- width: 100%;
4006
- height: 100%;
4242
+ wa-dropdown {
4243
+ pointer-events: none;
4244
+ min-width: 200px;
4007
4245
  }
4008
4246
 
4009
- .part-content-inner {
4010
- height: 100%;
4011
- min-height: 100%;
4247
+ wa-dropdown:state(open) {
4248
+ pointer-events: auto;
4012
4249
  }
4013
-
4014
- .part-toolbar {
4015
- min-height: 0;
4250
+
4251
+ wa-dropdown::part(menu) {
4252
+ min-width: 200px;
4016
4253
  }
4017
4254
  `;
4018
4255
  }
4019
- static finalizeStyles(styles) {
4020
- const own = super.finalizeStyles(styles);
4021
- return [DocksPart.baseStyles, ...own];
4022
- }
4023
4256
  };
4024
- _decorate([property()], DocksPart.prototype, "dirty", void 0);
4025
- _decorate([property({ attribute: false })], DocksPart.prototype, "tabContribution", void 0);
4026
- _decorate([property({
4027
- type: Boolean,
4028
- attribute: false
4029
- })], DocksPart.prototype, "isEditor", void 0);
4257
+ _decorate([property({ attribute: false })], DocksContextMenu.prototype, "scopeTokens", void 0);
4258
+ _decorate([property({ attribute: false })], DocksContextMenu.prototype, "partContextMenuRenderer", void 0);
4259
+ _decorate([state()], DocksContextMenu.prototype, "contributions", void 0);
4260
+ _decorate([state()], DocksContextMenu.prototype, "isOpen", void 0);
4261
+ DocksContextMenu = _decorate([customElement("docks-contextmenu")], DocksContextMenu);
4030
4262
  //#endregion
4031
4263
  //#region src/parts/tabs.ts
4032
4264
  var _DocksTabs;
@@ -4040,6 +4272,8 @@ var DocksTabs = class DocksTabs extends DocksContainer {
4040
4272
  this.iconOnly = false;
4041
4273
  this.withToolbar = false;
4042
4274
  this.hideTabs = false;
4275
+ this.fitContent = false;
4276
+ this.fillHeight = false;
4043
4277
  this.itemSize = "m";
4044
4278
  this.contributions = [];
4045
4279
  this.tabGroup = createRef();
@@ -4092,6 +4326,11 @@ var DocksTabs = class DocksTabs extends DocksContainer {
4092
4326
  if (!this.tabGroup.value) return null;
4093
4327
  return this.tabGroup.value.getAttribute("active");
4094
4328
  }
4329
+ getTabPart(tabName) {
4330
+ const tabPanel = this.getTabPanel(tabName);
4331
+ if (!tabPanel) return null;
4332
+ return this.getPartFromPanel(tabPanel);
4333
+ }
4095
4334
  open(contribution) {
4096
4335
  if (this.contributions.find((c) => c.name === contribution.name)) {
4097
4336
  this.activate(contribution.name);
@@ -4341,6 +4580,95 @@ var DocksTabs = class DocksTabs extends DocksContainer {
4341
4580
  width: 100%;
4342
4581
  }
4343
4582
 
4583
+ :host([fit-content]) {
4584
+ display: flex;
4585
+ flex-direction: column;
4586
+ height: auto;
4587
+ max-height: 100%;
4588
+ min-height: 0;
4589
+ }
4590
+
4591
+ :host([fit-content]) wa-tab-group {
4592
+ display: flex;
4593
+ flex-direction: column;
4594
+ flex: 1 1 auto;
4595
+ min-height: 0;
4596
+ height: auto;
4597
+ max-height: 100%;
4598
+ }
4599
+
4600
+ :host([fit-content]) wa-tab-group::part(base) {
4601
+ display: flex;
4602
+ flex-direction: column;
4603
+ flex: 1 1 auto;
4604
+ min-height: 0;
4605
+ max-height: 100%;
4606
+ height: auto;
4607
+ }
4608
+
4609
+ :host([fit-content]) wa-tab-group::part(body) {
4610
+ flex: 1 1 auto;
4611
+ min-height: 0;
4612
+ height: auto;
4613
+ overflow-x: hidden;
4614
+ overflow-y: auto;
4615
+ }
4616
+
4617
+ :host([fit-content]) wa-tab-panel[active] {
4618
+ display: block;
4619
+ height: auto;
4620
+ overflow: visible;
4621
+ }
4622
+
4623
+ :host([fit-content]) wa-tab-panel > * {
4624
+ height: auto;
4625
+ min-height: 0;
4626
+ }
4627
+
4628
+ :host([fill-height]) {
4629
+ display: flex;
4630
+ flex-direction: column;
4631
+ height: 100%;
4632
+ min-height: 0;
4633
+ max-height: 100%;
4634
+ }
4635
+
4636
+ :host([fill-height]) wa-tab-group {
4637
+ display: flex;
4638
+ flex-direction: column;
4639
+ flex: 1 1 auto;
4640
+ min-height: 0;
4641
+ height: 100%;
4642
+ max-height: 100%;
4643
+ }
4644
+
4645
+ :host([fill-height]) wa-tab-group::part(base) {
4646
+ display: flex;
4647
+ flex-direction: column;
4648
+ flex: 1 1 auto;
4649
+ min-height: 0;
4650
+ max-height: 100%;
4651
+ height: 100%;
4652
+ }
4653
+
4654
+ :host([fill-height]) wa-tab-group::part(body) {
4655
+ flex: 1 1 auto;
4656
+ min-height: 0;
4657
+ overflow-x: hidden;
4658
+ overflow-y: auto;
4659
+ }
4660
+
4661
+ :host([fill-height]) wa-tab-panel[active] {
4662
+ display: block;
4663
+ height: auto;
4664
+ overflow: visible;
4665
+ }
4666
+
4667
+ :host([fill-height]) wa-tab-panel > * {
4668
+ height: auto;
4669
+ min-height: 0;
4670
+ }
4671
+
4344
4672
  wa-tab-group {
4345
4673
  height: 100%;
4346
4674
  width: 100%;
@@ -4568,6 +4896,16 @@ _decorate([property({
4568
4896
  reflect: true,
4569
4897
  attribute: "hide-tabs"
4570
4898
  })], DocksTabs.prototype, "hideTabs", void 0);
4899
+ _decorate([property({
4900
+ type: Boolean,
4901
+ reflect: true,
4902
+ attribute: "fit-content"
4903
+ })], DocksTabs.prototype, "fitContent", void 0);
4904
+ _decorate([property({
4905
+ type: Boolean,
4906
+ reflect: true,
4907
+ attribute: "fill-height"
4908
+ })], DocksTabs.prototype, "fillHeight", void 0);
4571
4909
  _decorate([property({
4572
4910
  reflect: true,
4573
4911
  attribute: "item-size"
@@ -4607,9 +4945,9 @@ var DocksResizableGrid = class DocksResizableGrid extends DocksElement {
4607
4945
  this.gridChildren = [];
4608
4946
  this.resizing = null;
4609
4947
  this.resizeOverlay = null;
4610
- this.childrenLoaded = false;
4611
4948
  this.childStylesApplied = false;
4612
- this.settingsLoaded = false;
4949
+ this.refreshScheduled = false;
4950
+ this.layoutGeneration = 0;
4613
4951
  this.handleResize = (e) => {
4614
4952
  if (!this.resizing) return;
4615
4953
  const delta = (this.orientation === "horizontal" ? e.clientX : e.clientY) - this.resizing.startPos;
@@ -4659,56 +4997,108 @@ var DocksResizableGrid = class DocksResizableGrid extends DocksElement {
4659
4997
  return this;
4660
4998
  }
4661
4999
  doBeforeUI() {
4662
- if (!this.childrenLoaded) {
4663
- this.mutationObserver = new MutationObserver(() => {
4664
- if (!this.childrenLoaded) this.loadChildren();
4665
- });
4666
- this.mutationObserver.observe(this, {
4667
- childList: true,
4668
- subtree: false
4669
- });
4670
- this.loadChildren();
4671
- }
5000
+ this.mutationObserver = new MutationObserver(() => {
5001
+ this.scheduleRefreshGridLayout();
5002
+ });
5003
+ this.mutationObserver.observe(this, {
5004
+ childList: true,
5005
+ subtree: false
5006
+ });
5007
+ this.scheduleRefreshGridLayout();
5008
+ }
5009
+ scheduleRefreshGridLayout() {
5010
+ if (this.refreshScheduled) return;
5011
+ this.refreshScheduled = true;
5012
+ queueMicrotask(() => {
5013
+ this.refreshScheduled = false;
5014
+ this.refreshGridLayout();
5015
+ });
4672
5016
  }
4673
- async loadChildren() {
4674
- const potentialChildren = Array.from(this.children).filter((child) => child.tagName !== "STYLE" && child.tagName !== "SCRIPT" && !child.classList.contains("resize-handle"));
4675
- if (potentialChildren.length === 0) return;
4676
- this.childrenLoaded = true;
4677
- if (this.mutationObserver) {
4678
- this.mutationObserver.disconnect();
4679
- this.mutationObserver = void 0;
4680
- }
4681
- this.gridChildren = potentialChildren;
4682
- if (!this.settingsLoaded) {
4683
- this.settingsLoaded = true;
4684
- const persisted = await this.getDialogSetting();
4685
- if (persisted && Array.isArray(persisted.sizes) && persisted.sizes.length === this.gridChildren.length) {
4686
- this.gridSizes = persisted.sizes;
4687
- this.requestUpdate();
4688
- return;
5017
+ getGridChildrenFromDom() {
5018
+ return Array.from(this.children).filter((child) => child.tagName !== "STYLE" && child.tagName !== "SCRIPT" && !child.classList.contains("resize-handle"));
5019
+ }
5020
+ static isNestedResizableGrid(child) {
5021
+ return child.tagName.toLowerCase() === "docks-resizable-grid";
5022
+ }
5023
+ applyChildLayoutStyles() {
5024
+ this.gridChildren.forEach((child, index) => {
5025
+ child.style.overflow = "hidden";
5026
+ child.style.height = "100%";
5027
+ child.style.width = "100%";
5028
+ child.style.gridColumn = this.orientation === "horizontal" ? `${index * 2 + 1}` : "1";
5029
+ child.style.gridRow = this.orientation === "vertical" ? `${index * 2 + 1}` : "1";
5030
+ if (!_DocksResizableGrid.isNestedResizableGrid(child)) {
5031
+ child.style.display = "flex";
5032
+ child.style.flexDirection = "column";
4689
5033
  }
5034
+ });
5035
+ }
5036
+ resolveGridSizes(childCount) {
5037
+ const parsedSizes = this.sizes?.split(",").map((s) => s.trim()).filter(Boolean) ?? [];
5038
+ if (parsedSizes.length === childCount) return parsedSizes;
5039
+ const equalSize = `${100 / childCount}%`;
5040
+ return Array.from({ length: childCount }, () => equalSize);
5041
+ }
5042
+ resolvePersistedSizes(persisted, childCount) {
5043
+ if (!persisted || typeof persisted !== "object") return null;
5044
+ const record = persisted;
5045
+ const byCount = record.sizesByCount?.[String(childCount)];
5046
+ if (Array.isArray(byCount) && byCount.length === childCount && byCount.every((size) => typeof size === "string")) return byCount;
5047
+ if (Array.isArray(record.sizes) && record.sizes.length === childCount && record.sizes.every((size) => typeof size === "string")) return record.sizes;
5048
+ return null;
5049
+ }
5050
+ async refreshGridLayout() {
5051
+ const generation = ++this.layoutGeneration;
5052
+ const potentialChildren = this.getGridChildrenFromDom();
5053
+ if (potentialChildren.length === 0) {
5054
+ this.gridChildren = [];
5055
+ this.gridSizes = [];
5056
+ this.childStylesApplied = false;
5057
+ this.requestUpdate();
5058
+ return;
4690
5059
  }
4691
- if (this.sizes) this.gridSizes = this.sizes.split(",").map((s) => s.trim());
4692
- else {
4693
- const equalSize = `${100 / this.gridChildren.length}%`;
4694
- this.gridSizes = this.gridChildren.map(() => equalSize);
5060
+ const identityChanged = potentialChildren.length !== this.gridChildren.length || potentialChildren.some((child, index) => child !== this.gridChildren[index]);
5061
+ this.gridChildren = potentialChildren;
5062
+ const persisted = await this.getDialogSetting();
5063
+ if (generation !== this.layoutGeneration) return;
5064
+ const restoredSizes = this.resolvePersistedSizes(persisted, this.gridChildren.length);
5065
+ if (restoredSizes) {
5066
+ const sizesChanged = restoredSizes.join("|") !== this.gridSizes.join("|");
5067
+ this.gridSizes = restoredSizes;
5068
+ if (identityChanged || sizesChanged) this.childStylesApplied = false;
5069
+ this.requestUpdate();
5070
+ return;
4695
5071
  }
5072
+ const nextSizes = this.resolveGridSizes(this.gridChildren.length);
5073
+ const sizesChanged = nextSizes.join("|") !== this.gridSizes.join("|");
5074
+ this.gridSizes = nextSizes;
5075
+ if (identityChanged || sizesChanged) this.childStylesApplied = false;
4696
5076
  this.requestUpdate();
4697
5077
  }
5078
+ doInitUI() {
5079
+ this.scheduleRefreshGridLayout();
5080
+ }
4698
5081
  async saveSizes() {
4699
5082
  if (this.gridSizes.length === 0) return;
5083
+ const persisted = await this.getDialogSetting();
5084
+ const existingByCount = persisted && typeof persisted === "object" ? persisted.sizesByCount ?? {} : {};
4700
5085
  await this.setDialogSetting({
4701
5086
  sizes: this.gridSizes,
5087
+ sizesByCount: {
5088
+ ...existingByCount,
5089
+ [String(this.gridSizes.length)]: this.gridSizes
5090
+ },
4702
5091
  orientation: this.orientation
4703
5092
  });
4704
5093
  }
4705
5094
  updated(changedProperties) {
4706
5095
  super.updated(changedProperties);
4707
- if (changedProperties.has("gridChildren") && !this.childStylesApplied && this.gridChildren.length > 0) {
5096
+ if (changedProperties.has("sizes")) this.scheduleRefreshGridLayout();
5097
+ if (!this.childStylesApplied && this.gridChildren.length > 0) {
4708
5098
  this.childStylesApplied = true;
4709
5099
  /**
4710
5100
  * Direct style manipulation is intentionally used here.
4711
- *
5101
+ *
4712
5102
  * Reasoning:
4713
5103
  * - Grid positioning (gridColumn/gridRow) must be computed dynamically based on
4714
5104
  * the number of children and orientation at runtime
@@ -4718,16 +5108,9 @@ var DocksResizableGrid = class DocksResizableGrid extends DocksElement {
4718
5108
  * (e.g., child at index 0 → column 1, index 1 → column 3, etc.)
4719
5109
  * - This is a layout container whose primary job is to programmatically position
4720
5110
  * its children within a CSS grid system
5111
+ * - Nested resizable grids manage their own display mode and must not receive flex
4721
5112
  */
4722
- this.gridChildren.forEach((child, index) => {
4723
- child.style.overflow = "hidden";
4724
- child.style.height = "100%";
4725
- child.style.width = "100%";
4726
- child.style.gridColumn = this.orientation === "horizontal" ? `${index * 2 + 1}` : "1";
4727
- child.style.gridRow = this.orientation === "vertical" ? `${index * 2 + 1}` : "1";
4728
- child.style.display = "flex";
4729
- child.style.flexDirection = "column";
4730
- });
5113
+ this.applyChildLayoutStyles();
4731
5114
  }
4732
5115
  }
4733
5116
  startResize(e, handleIndex) {
@@ -4843,6 +5226,9 @@ _decorate([state()], DocksResizableGrid.prototype, "gridSizes", void 0);
4843
5226
  _decorate([state()], DocksResizableGrid.prototype, "gridChildren", void 0);
4844
5227
  DocksResizableGrid = _DocksResizableGrid = _decorate([customElement("docks-resizable-grid")], DocksResizableGrid);
4845
5228
  //#endregion
4846
- export { constants_exports as A, SYSTEM_VIEWS as B, esmShService as C, COMMAND_SAVE as D, appSettings as E, SIDEBAR_MAIN as F, TOOLBAR_MAIN_CENTER as G, TOOLBAR_BOTTOM_CENTER as H, SIDEBAR_MAIN_BOTTOM as I, TOOLBAR_MAIN_RIGHT as K, SIDEBAR_MAIN_TOOLBAR as L, EDITOR_AREA_MAIN as M, PANEL_BOTTOM as N, HIDE_DOT_RESOURCE as O, SIDEBAR_AUXILIARY as P, SYSTEM_ATTRIBUTIONS as R, extensionRegistry as S, TOPIC_SETTINGS_CHANGED as T, TOOLBAR_BOTTOM_END as U, TOOLBAR_BOTTOM as V, TOOLBAR_MAIN as W, DocksDialogContent as _, versionInfoDialog as a, sanitizeHtml as b, confirmDialog as c, formDialogRequired as d, validateFormDialogFields as f, dialogService as g, deleteIndexedDbWorkspaceData as h, parseIconSpec as i, ABOUT_TABS as j, MouseButton as k, infoDialog as l, appLoaderService as m, DocksContainer as n, filebrowserDialog as o, promptDialog as p, icon as r, navigableInfoDialog as s, DocksPart as t, formDialog as u, DocksElement as v, taskService as w, TOPIC_EXTENSIONS_CHANGED as x, parseMarkdownHtml as y, SYSTEM_LAYOUTS as z };
5229
+ //#region src/parts/index.ts
5230
+ installOpenDropdownNativeContextMenuGuard();
5231
+ //#endregion
5232
+ export { HIDE_DOT_RESOURCE as $, filebrowserDialog as A, dialogService as B, TOOLBAR_INFO as C, TOOLBAR_THEME_SWITCHER as D, TOOLBAR_TASKS as E, formDialogRequired as F, keyBindingManager as G, DocksElement as H, validateFormDialogFields as I, esmShService as J, TOPIC_EXTENSIONS_CHANGED as K, promptDialog as L, confirmDialog as M, infoDialog as N, VIEW_FILEBROWSER as O, formDialog as P, COMMAND_SAVE as Q, appLoaderService as R, TOOLBAR_FULLSCREEN as S, TOOLBAR_LAYOUT_SWITCHER as T, parseMarkdownHtml as U, DocksDialogContent as V, sanitizeHtml as W, TOPIC_SETTINGS_CHANGED as X, taskService as Y, appSettings as Z, CTXMENU_FILEBROWSER_RENAME as _, TOOLBAR_MAIN_CENTER as _t, renderDropdownItem as a, SIDEBAR_AUXILIARY as at, TOOLBAR_ACTIVE_PART_NAME as b, parseIconSpec as c, SIDEBAR_MAIN_BOTTOM as ct, DocksContainer as d, SYSTEM_LAYOUTS as dt, MouseButton as et, ABOUT_TAB_ATTRIBUTIONS as f, SYSTEM_VIEWS as ft, CTXMENU_FILEBROWSER_DELETE as g, TOOLBAR_MAIN as gt, ATTRIBUTION_ECLIPSE_DOCKS as h, TOOLBAR_BOTTOM_END as ht, renderDropdownContributions as i, PANEL_BOTTOM as it, navigableInfoDialog as j, VIEW_LOG_TERMINAL as k, versionInfoDialog as l, SIDEBAR_MAIN_TOOLBAR as lt, ABOUT_TAB_RELEASE as m, TOOLBAR_BOTTOM_CENTER as mt, renderCommandContribution as n, ABOUT_TABS as nt, renderDropdownSubmenu as o, SIDEBAR_AUXILIARY_BOTTOM as ot, ABOUT_TAB_PACKAGES as p, TOOLBAR_BOTTOM as pt, extensionRegistry as q, renderDropdownContribution as r, EDITOR_AREA_MAIN as rt, icon as s, SIDEBAR_MAIN as st, handleDropdownWaSelect as t, constants_exports as tt, DocksPart as u, SYSTEM_ATTRIBUTIONS as ut, SIDEBAR_TOOLBAR_EXTENSIONS as v, TOOLBAR_MAIN_RIGHT as vt, TOOLBAR_LANGUAGE_SELECTOR as w, TOOLBAR_FAST_VIEWS as x, SYSTEM_DEFAULT_FILE_ICONS as y, deleteIndexedDbWorkspaceData as z };
4847
5233
 
4848
- //# sourceMappingURL=parts-CsoNFVTw.js.map
5234
+ //# sourceMappingURL=parts-CAt8aeU8.js.map