@kispace-io/core 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/dist/api/base-classes.d.ts +7 -0
  2. package/dist/api/base-classes.d.ts.map +1 -0
  3. package/dist/api/constants.d.ts +2 -0
  4. package/dist/api/constants.d.ts.map +1 -0
  5. package/dist/api/index.d.ts +6 -0
  6. package/dist/api/index.d.ts.map +1 -0
  7. package/dist/api/index.js +80 -0
  8. package/dist/api/index.js.map +1 -0
  9. package/dist/api/services.d.ts +27 -0
  10. package/dist/api/services.d.ts.map +1 -0
  11. package/dist/api/types.d.ts +11 -0
  12. package/dist/api/types.d.ts.map +1 -0
  13. package/dist/commands/files.d.ts +2 -0
  14. package/dist/commands/files.d.ts.map +1 -0
  15. package/dist/commands/global.d.ts +1 -0
  16. package/dist/commands/global.d.ts.map +1 -0
  17. package/dist/commands/index.d.ts +1 -0
  18. package/dist/commands/index.d.ts.map +1 -0
  19. package/dist/commands/version-info.d.ts +2 -0
  20. package/dist/commands/version-info.d.ts.map +1 -0
  21. package/dist/components/index.d.ts +1 -0
  22. package/dist/components/index.d.ts.map +1 -0
  23. package/dist/components/k-app-selector.d.ts +17 -0
  24. package/dist/components/k-app-selector.d.ts.map +1 -0
  25. package/dist/components/k-app-switcher.d.ts +13 -0
  26. package/dist/components/k-app-switcher.d.ts.map +1 -0
  27. package/dist/components/k-command.d.ts +31 -0
  28. package/dist/components/k-command.d.ts.map +1 -0
  29. package/dist/components/k-extensions.d.ts +32 -0
  30. package/dist/components/k-extensions.d.ts.map +1 -0
  31. package/dist/components/k-fastviews.d.ts +34 -0
  32. package/dist/components/k-fastviews.d.ts.map +1 -0
  33. package/dist/components/k-filebrowser.d.ts +40 -0
  34. package/dist/components/k-filebrowser.d.ts.map +1 -0
  35. package/dist/components/k-language-selector.d.ts +12 -0
  36. package/dist/components/k-language-selector.d.ts.map +1 -0
  37. package/dist/components/k-log-terminal.d.ts +36 -0
  38. package/dist/components/k-log-terminal.d.ts.map +1 -0
  39. package/dist/components/k-part-name.d.ts +12 -0
  40. package/dist/components/k-part-name.d.ts.map +1 -0
  41. package/dist/components/k-tasks.d.ts +13 -0
  42. package/dist/components/k-tasks.d.ts.map +1 -0
  43. package/dist/components/k-workspace-name.d.ts +14 -0
  44. package/dist/components/k-workspace-name.d.ts.map +1 -0
  45. package/dist/contributions/default-ui-contributions.d.ts +2 -0
  46. package/dist/contributions/default-ui-contributions.d.ts.map +1 -0
  47. package/dist/contributions/index.d.ts +1 -0
  48. package/dist/contributions/index.d.ts.map +1 -0
  49. package/dist/contributions/marketplace-catalog-contributions.d.ts +2 -0
  50. package/dist/contributions/marketplace-catalog-contributions.d.ts.map +1 -0
  51. package/dist/core/app-host-config.d.ts +7 -0
  52. package/dist/core/app-host-config.d.ts.map +1 -0
  53. package/dist/core/apploader.d.ts +214 -0
  54. package/dist/core/apploader.d.ts.map +1 -0
  55. package/dist/core/appstate.d.ts +12 -0
  56. package/dist/core/appstate.d.ts.map +1 -0
  57. package/dist/core/commandregistry.d.ts +79 -0
  58. package/dist/core/commandregistry.d.ts.map +1 -0
  59. package/dist/core/config.d.ts +15 -0
  60. package/dist/core/config.d.ts.map +1 -0
  61. package/dist/core/constants.d.ts +21 -0
  62. package/dist/core/constants.d.ts.map +1 -0
  63. package/dist/core/contributionregistry.d.ts +49 -0
  64. package/dist/core/contributionregistry.d.ts.map +1 -0
  65. package/dist/core/di.d.ts +18 -0
  66. package/dist/core/di.d.ts.map +1 -0
  67. package/dist/core/dialogservice.d.ts +33 -0
  68. package/dist/core/dialogservice.d.ts.map +1 -0
  69. package/dist/core/editorregistry.d.ts +73 -0
  70. package/dist/core/editorregistry.d.ts.map +1 -0
  71. package/dist/core/esmsh-service.d.ts +40 -0
  72. package/dist/core/esmsh-service.d.ts.map +1 -0
  73. package/dist/core/events.d.ts +7 -0
  74. package/dist/core/events.d.ts.map +1 -0
  75. package/dist/core/events.js +63 -0
  76. package/dist/core/events.js.map +1 -0
  77. package/dist/core/extensionregistry.d.ts +98 -0
  78. package/dist/core/extensionregistry.d.ts.map +1 -0
  79. package/dist/core/filesys.d.ts +139 -0
  80. package/dist/core/filesys.d.ts.map +1 -0
  81. package/dist/core/i18n.d.ts +50 -0
  82. package/dist/core/i18n.d.ts.map +1 -0
  83. package/dist/core/index.d.ts +1 -0
  84. package/dist/core/index.d.ts.map +1 -0
  85. package/dist/core/k-utils.d.ts +2 -0
  86. package/dist/core/k-utils.d.ts.map +1 -0
  87. package/dist/core/keybindings.d.ts +67 -0
  88. package/dist/core/keybindings.d.ts.map +1 -0
  89. package/dist/core/logger.d.ts +44 -0
  90. package/dist/core/logger.d.ts.map +1 -0
  91. package/dist/core/marketplaceregistry.d.ts +25 -0
  92. package/dist/core/marketplaceregistry.d.ts.map +1 -0
  93. package/dist/core/packageinfoservice.d.ts +16 -0
  94. package/dist/core/packageinfoservice.d.ts.map +1 -0
  95. package/dist/core/persistenceservice.d.ts +6 -0
  96. package/dist/core/persistenceservice.d.ts.map +1 -0
  97. package/dist/core/settingsservice.d.ts +19 -0
  98. package/dist/core/settingsservice.d.ts.map +1 -0
  99. package/dist/core/signals.d.ts +3 -0
  100. package/dist/core/signals.d.ts.map +1 -0
  101. package/dist/core/taskservice.d.ts +20 -0
  102. package/dist/core/taskservice.d.ts.map +1 -0
  103. package/dist/core/toast.d.ts +4 -0
  104. package/dist/core/toast.d.ts.map +1 -0
  105. package/dist/core/tree-utils.d.ts +16 -0
  106. package/dist/core/tree-utils.d.ts.map +1 -0
  107. package/dist/dialogs/confirm-dialog.d.ts +14 -0
  108. package/dist/dialogs/confirm-dialog.d.ts.map +1 -0
  109. package/dist/dialogs/index.d.ts +5 -0
  110. package/dist/dialogs/index.d.ts.map +1 -0
  111. package/dist/dialogs/info-dialog.d.ts +13 -0
  112. package/dist/dialogs/info-dialog.d.ts.map +1 -0
  113. package/dist/dialogs/navigable-info-dialog.d.ts +33 -0
  114. package/dist/dialogs/navigable-info-dialog.d.ts.map +1 -0
  115. package/dist/dialogs/prompt-dialog.d.ts +21 -0
  116. package/dist/dialogs/prompt-dialog.d.ts.map +1 -0
  117. package/dist/externals/lit.d.ts +20 -0
  118. package/dist/externals/lit.d.ts.map +1 -0
  119. package/dist/externals/lit.js +15 -0
  120. package/dist/externals/lit.js.map +1 -0
  121. package/dist/externals/third-party.d.ts +7 -0
  122. package/dist/externals/third-party.d.ts.map +1 -0
  123. package/dist/externals/third-party.js +2 -0
  124. package/dist/externals/third-party.js.map +1 -0
  125. package/dist/externals/webawesome.d.ts +1 -0
  126. package/dist/externals/webawesome.d.ts.map +1 -0
  127. package/dist/externals/webawesome.js +52 -0
  128. package/dist/externals/webawesome.js.map +1 -0
  129. package/dist/i18n/extensions.json.d.ts +42 -0
  130. package/dist/i18n/fastviews.json.d.ts +13 -0
  131. package/dist/i18n/filebrowser.json.d.ts +35 -0
  132. package/dist/i18n/index.d.ts +2 -0
  133. package/dist/i18n/index.d.ts.map +1 -0
  134. package/dist/i18n/logterminal.json.d.ts +45 -0
  135. package/dist/i18n/partname.json.d.ts +15 -0
  136. package/dist/i18n/tasks.json.d.ts +15 -0
  137. package/dist/i18n/workspace.json.d.ts +15 -0
  138. package/dist/index.d.ts +2 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +80 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/k-icon-BZC7dQV0.js +492 -0
  143. package/dist/k-icon-BZC7dQV0.js.map +1 -0
  144. package/dist/k-nocontent-Bh_yToGh.js +48 -0
  145. package/dist/k-nocontent-Bh_yToGh.js.map +1 -0
  146. package/dist/k-resizable-grid-Ch3iWZaL.js +3157 -0
  147. package/dist/k-resizable-grid-Ch3iWZaL.js.map +1 -0
  148. package/dist/k-standard-layout-CQ1VZoxa.js +5011 -0
  149. package/dist/k-standard-layout-CQ1VZoxa.js.map +1 -0
  150. package/dist/layouts/k-standard-layout.d.ts +16 -0
  151. package/dist/layouts/k-standard-layout.d.ts.map +1 -0
  152. package/dist/parts/index.d.ts +1 -0
  153. package/dist/parts/index.d.ts.map +1 -0
  154. package/dist/parts/index.js +53 -0
  155. package/dist/parts/index.js.map +1 -0
  156. package/dist/parts/k-app.d.ts +11 -0
  157. package/dist/parts/k-app.d.ts.map +1 -0
  158. package/dist/parts/k-container.d.ts +4 -0
  159. package/dist/parts/k-container.d.ts.map +1 -0
  160. package/dist/parts/k-contextmenu.d.ts +38 -0
  161. package/dist/parts/k-contextmenu.d.ts.map +1 -0
  162. package/dist/parts/k-dialog-content.d.ts +9 -0
  163. package/dist/parts/k-dialog-content.d.ts.map +1 -0
  164. package/dist/parts/k-element.d.ts +36 -0
  165. package/dist/parts/k-element.d.ts.map +1 -0
  166. package/dist/parts/k-part.d.ts +96 -0
  167. package/dist/parts/k-part.d.ts.map +1 -0
  168. package/dist/parts/k-resizable-grid.d.ts +31 -0
  169. package/dist/parts/k-resizable-grid.d.ts.map +1 -0
  170. package/dist/parts/k-tabs.d.ts +74 -0
  171. package/dist/parts/k-tabs.d.ts.map +1 -0
  172. package/dist/parts/k-toolbar.d.ts +21 -0
  173. package/dist/parts/k-toolbar.d.ts.map +1 -0
  174. package/dist/widgets/index.d.ts +1 -0
  175. package/dist/widgets/index.d.ts.map +1 -0
  176. package/dist/widgets/index.js +3 -0
  177. package/dist/widgets/index.js.map +1 -0
  178. package/dist/widgets/k-icon.d.ts +10 -0
  179. package/dist/widgets/k-icon.d.ts.map +1 -0
  180. package/dist/widgets/k-nocontent.d.ts +13 -0
  181. package/dist/widgets/k-nocontent.d.ts.map +1 -0
  182. package/dist/widgets/k-widget.d.ts +25 -0
  183. package/dist/widgets/k-widget.d.ts.map +1 -0
  184. package/package.json +81 -0
  185. package/src/api/base-classes.ts +10 -0
  186. package/src/api/constants.ts +3 -0
  187. package/src/api/index.ts +31 -0
  188. package/src/api/services.ts +52 -0
  189. package/src/api/types.ts +46 -0
  190. package/src/commands/files.ts +829 -0
  191. package/src/commands/global.ts +225 -0
  192. package/src/commands/index.ts +4 -0
  193. package/src/commands/version-info.ts +214 -0
  194. package/src/components/index.ts +10 -0
  195. package/src/components/k-app-selector.ts +233 -0
  196. package/src/components/k-app-switcher.ts +126 -0
  197. package/src/components/k-command.ts +236 -0
  198. package/src/components/k-extensions.ts +615 -0
  199. package/src/components/k-fastviews.ts +314 -0
  200. package/src/components/k-filebrowser.ts +442 -0
  201. package/src/components/k-language-selector.ts +166 -0
  202. package/src/components/k-log-terminal.ts +337 -0
  203. package/src/components/k-part-name.ts +54 -0
  204. package/src/components/k-tasks.ts +267 -0
  205. package/src/components/k-workspace-name.ts +56 -0
  206. package/src/contributions/default-ui-contributions.ts +51 -0
  207. package/src/contributions/index.ts +3 -0
  208. package/src/contributions/marketplace-catalog-contributions.ts +6 -0
  209. package/src/core/app-host-config.ts +23 -0
  210. package/src/core/apploader.ts +630 -0
  211. package/src/core/appstate.ts +15 -0
  212. package/src/core/commandregistry.ts +210 -0
  213. package/src/core/config.ts +29 -0
  214. package/src/core/constants.ts +27 -0
  215. package/src/core/contributionregistry.ts +77 -0
  216. package/src/core/di.ts +54 -0
  217. package/src/core/dialogservice.ts +266 -0
  218. package/src/core/editorregistry.ts +303 -0
  219. package/src/core/esmsh-service.ts +404 -0
  220. package/src/core/events.ts +68 -0
  221. package/src/core/extensionregistry.ts +399 -0
  222. package/src/core/filesys.ts +618 -0
  223. package/src/core/i18n.ts +221 -0
  224. package/src/core/index.ts +51 -0
  225. package/src/core/k-utils.ts +11 -0
  226. package/src/core/keybindings.ts +274 -0
  227. package/src/core/logger.ts +187 -0
  228. package/src/core/marketplaceregistry.ts +197 -0
  229. package/src/core/packageinfoservice.ts +56 -0
  230. package/src/core/persistenceservice.ts +15 -0
  231. package/src/core/settingsservice.ts +70 -0
  232. package/src/core/signals.ts +18 -0
  233. package/src/core/taskservice.ts +72 -0
  234. package/src/core/toast.ts +11 -0
  235. package/src/core/tree-utils.ts +24 -0
  236. package/src/dialogs/confirm-dialog.ts +72 -0
  237. package/src/dialogs/index.ts +4 -0
  238. package/src/dialogs/info-dialog.ts +67 -0
  239. package/src/dialogs/navigable-info-dialog.ts +256 -0
  240. package/src/dialogs/prompt-dialog.ts +123 -0
  241. package/src/externals/lit.ts +26 -0
  242. package/src/externals/third-party.ts +9 -0
  243. package/src/externals/webawesome.ts +54 -0
  244. package/src/i18n/extensions.json +39 -0
  245. package/src/i18n/fastviews.json +10 -0
  246. package/src/i18n/filebrowser.json +33 -0
  247. package/src/i18n/index.ts +25 -0
  248. package/src/i18n/logterminal.json +42 -0
  249. package/src/i18n/partname.json +12 -0
  250. package/src/i18n/tasks.json +12 -0
  251. package/src/i18n/workspace.json +12 -0
  252. package/src/icons/icons.txt +3 -0
  253. package/src/icons/js.svg +6 -0
  254. package/src/icons/jupyter.svg +18 -0
  255. package/src/icons/python.svg +15 -0
  256. package/src/index.ts +3 -0
  257. package/src/layouts/k-standard-layout.ts +174 -0
  258. package/src/parts/index.ts +6 -0
  259. package/src/parts/k-app.ts +29 -0
  260. package/src/parts/k-container.ts +4 -0
  261. package/src/parts/k-contextmenu.ts +245 -0
  262. package/src/parts/k-dialog-content.ts +31 -0
  263. package/src/parts/k-element.ts +100 -0
  264. package/src/parts/k-part.ts +158 -0
  265. package/src/parts/k-resizable-grid.ts +366 -0
  266. package/src/parts/k-tabs.ts +574 -0
  267. package/src/parts/k-toolbar.ts +158 -0
  268. package/src/vite-env.d.ts +2 -0
  269. package/src/widgets/index.ts +2 -0
  270. package/src/widgets/k-icon.ts +39 -0
  271. package/src/widgets/k-nocontent.ts +40 -0
  272. package/src/widgets/k-widget.ts +90 -0
@@ -0,0 +1,574 @@
1
+ import {customElement, state} from "lit/decorators.js";
2
+ import {css, html, nothing} from "lit";
3
+ import {KContainer} from "./k-container";
4
+ import {contributionRegistry, ContributionChangeEvent, TabContribution, TOPIC_CONTRIBUTEIONS_CHANGED} from "../core/contributionregistry";
5
+ import {when} from "lit/directives/when.js";
6
+ import {repeat} from "lit/directives/repeat.js";
7
+ import '../widgets/k-icon';
8
+ import {createRef, ref} from "lit/directives/ref.js";
9
+ import {subscribe} from "../core/events";
10
+ import {KPart} from "./k-part";
11
+ import {KToolbar} from "./k-toolbar";
12
+ import {KContextMenu} from "./k-contextmenu";
13
+ import {MouseButton, EDITOR_AREA_MAIN} from "../core/constants";
14
+ import {activePartSignal} from "../core/appstate";
15
+ import {confirmDialog} from "../dialogs";
16
+ import {appLoaderService} from "../core/apploader";
17
+
18
+ /**
19
+ * KTabs - A dynamic tab container component
20
+ *
21
+ * Architecture:
22
+ * - Fixed layout (VS Code style) - each tab is registered to a specific container
23
+ * - Tabs are created/destroyed as needed (no instance reuse)
24
+ * - Support for both static (views) and dynamic (editors) tabs
25
+ *
26
+ * Lifecycle:
27
+ * 1. doInitUI(): Load contributions, activate first tab
28
+ * 2. render(): Create tab UI from contributions
29
+ * 3. open/closeTab(): Dynamic tab operations
30
+ */
31
+ @customElement('k-tabs')
32
+ export class KTabs extends KContainer {
33
+ /** Tab contributions for this container */
34
+ @state()
35
+ private contributions: TabContribution[] = [];
36
+
37
+ /** Reference to the underlying wa-tab-group element */
38
+ private tabGroup = createRef()
39
+
40
+ /** Cached container ID (this element's 'id' attribute) */
41
+ private containerId: string | null = null;
42
+
43
+ /** Map to track ResizeObservers for cleanup */
44
+ private resizeObservers = new WeakMap<HTMLElement, ResizeObserver>();
45
+
46
+ // ============= Lifecycle Methods =============
47
+
48
+ protected doBeforeUI() {
49
+ this.containerId = this.getAttribute("id");
50
+ if (!this.containerId) {
51
+ throw new Error("k-tabs requires an 'id' attribute to function");
52
+ }
53
+
54
+ this.loadAndResolveContributions();
55
+ }
56
+
57
+ protected doInitUI() {
58
+ this.updateComplete.then(() => {
59
+ this.activateNextAvailableTab();
60
+
61
+ if (!this.tabGroup.value) return;
62
+
63
+ // @ts-ignore
64
+ this.tabGroup.value.addEventListener("wa-tab-show", (event: CustomEvent) => {
65
+ const tabPanel = this.getTabPanel(event.detail.name);
66
+ if (tabPanel) {
67
+ // Update toolbar from component's renderToolbar() method
68
+ this.updateToolbarFromComponent(tabPanel);
69
+ // Update toolbar height variable for calc() positioning
70
+ requestAnimationFrame(() => {
71
+ this.updateToolbarHeightVariable(tabPanel);
72
+ this.setupToolbarResizeObserver(tabPanel);
73
+ });
74
+ this.dispatchEvent(new CustomEvent('tab-shown', {detail: tabPanel}));
75
+ }
76
+ });
77
+
78
+ // Listen for toolbar update requests from components
79
+ this.tabGroup.value.addEventListener("part-toolbar-changed", (event: Event) => {
80
+ const component = event.target as HTMLElement;
81
+ const tabPanel = component.closest('wa-tab-panel') as HTMLElement | null;
82
+ if (tabPanel) {
83
+ this.updateToolbarFromComponent(tabPanel);
84
+ // Update toolbar height variable for calc() positioning
85
+ requestAnimationFrame(() => this.updateToolbarHeightVariable(tabPanel));
86
+ }
87
+ });
88
+
89
+ // Listen for context menu update requests from components
90
+ this.tabGroup.value.addEventListener("part-contextmenu-changed", (event: Event) => {
91
+ const component = event.target as HTMLElement;
92
+ const tabPanel = component.closest('wa-tab-panel') as HTMLElement | null;
93
+ if (tabPanel) {
94
+ this.updateContextMenuFromComponent(tabPanel);
95
+ }
96
+ });
97
+
98
+ // Update active part signal when clicking anywhere in tab content or tab title
99
+ this.tabGroup.value.addEventListener('click', (event: Event) => {
100
+ const target = event.target as HTMLElement;
101
+
102
+ // Handle clicks on tab titles
103
+ const tab = target.closest('wa-tab');
104
+ if (tab) {
105
+ const panelName = tab.getAttribute('panel');
106
+ if (panelName) {
107
+ const tabPanel = this.getTabPanel(panelName);
108
+ if (tabPanel) {
109
+ const contentDiv = tabPanel.querySelector('.tab-content');
110
+ if (contentDiv && contentDiv.firstElementChild) {
111
+ const part = contentDiv.firstElementChild;
112
+ if (part instanceof KPart) {
113
+ activePartSignal.set(part);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ return;
119
+ }
120
+
121
+ // Handle clicks on tab content
122
+ const scroller = target.closest('wa-scroller.tab-content');
123
+ if (!scroller) return;
124
+
125
+ const tabPanel = scroller.closest('wa-tab-panel') as HTMLElement;
126
+ if (!tabPanel) return;
127
+
128
+ const contentDiv = tabPanel.querySelector('.tab-content');
129
+ if (contentDiv && contentDiv.firstElementChild) {
130
+ const part = contentDiv.firstElementChild;
131
+ if (part instanceof KPart) {
132
+ activePartSignal.set(part);
133
+ }
134
+ }
135
+ });
136
+
137
+ // Automatically wire up context menus for all tab content
138
+ this.tabGroup.value.addEventListener('contextmenu', (event: Event) => {
139
+ const mouseEvent = event as MouseEvent;
140
+ const scroller = (mouseEvent.target as HTMLElement).closest('wa-scroller.tab-content');
141
+ if (!scroller) return;
142
+
143
+ mouseEvent.preventDefault();
144
+
145
+ const tabPanel = scroller.closest('wa-tab-panel') as HTMLElement;
146
+ if (!tabPanel) return;
147
+
148
+ // Wait for selection to update before showing context menu
149
+ requestAnimationFrame(() => {
150
+ this.updateContextMenuFromComponent(tabPanel);
151
+
152
+ const contextMenu = tabPanel.querySelector('k-contextmenu') as KContextMenu;
153
+ if (contextMenu) {
154
+ contextMenu.show({ x: mouseEvent.clientX, y: mouseEvent.clientY }, mouseEvent);
155
+ }
156
+ });
157
+ });
158
+ });
159
+
160
+ subscribe(TOPIC_CONTRIBUTEIONS_CHANGED, (event: ContributionChangeEvent) => {
161
+ if (!this.containerId || event.target !== this.containerId) return;
162
+
163
+ this.loadAndResolveContributions();
164
+ this.requestUpdate();
165
+
166
+ this.updateComplete.then(() => {
167
+ this.activateNextAvailableTab();
168
+ });
169
+ });
170
+ }
171
+
172
+ updated(changedProperties: Map<string, any>) {
173
+ super.updated(changedProperties);
174
+
175
+ if (changedProperties.has('contributions')) {
176
+ const isEditorArea = this.containerId === EDITOR_AREA_MAIN;
177
+ this.contributions.forEach(contribution => {
178
+ const tabPanel = this.getTabPanel(contribution.name);
179
+ if (!tabPanel) return;
180
+
181
+ const contentDiv = tabPanel.querySelector('.tab-content');
182
+ if (contentDiv && contentDiv.firstElementChild) {
183
+ const part = contentDiv.firstElementChild;
184
+ if (part instanceof KPart) {
185
+ part.tabContribution = contribution;
186
+ part.isEditor = isEditorArea;
187
+ }
188
+ }
189
+
190
+ requestAnimationFrame(() => this.updateToolbarHeightVariable(tabPanel));
191
+ });
192
+ }
193
+ }
194
+
195
+ // ============= Public API Methods =============
196
+
197
+ has(key: string): boolean {
198
+ if (!this.tabGroup.value) return false;
199
+ return !!this.getTabPanel(key);
200
+ }
201
+
202
+ activate(key: string): void {
203
+ if (!this.tabGroup.value) return;
204
+ this.tabGroup.value.setAttribute("active", key);
205
+ }
206
+
207
+ public getActiveEditor(): string | null {
208
+ if (!this.tabGroup.value) return null;
209
+ return this.tabGroup.value.getAttribute("active");
210
+ }
211
+
212
+ open(contribution: TabContribution): void {
213
+ // Check if contribution already exists, if so just activate it
214
+ const existing = this.contributions.find(c => c.name === contribution.name);
215
+ if (existing) {
216
+ this.activate(contribution.name);
217
+ return;
218
+ }
219
+
220
+ this.contributions.push(contribution);
221
+ this.requestUpdate();
222
+
223
+ this.updateComplete.then(() => {
224
+ this.activate(contribution.name);
225
+ // Update toolbar after component is rendered
226
+ const tabPanel = this.getTabPanel(contribution.name);
227
+ if (tabPanel) {
228
+ const contentDiv = tabPanel.querySelector('.tab-content');
229
+ if (contentDiv && contentDiv.firstElementChild) {
230
+ const part = contentDiv.firstElementChild;
231
+ if (part instanceof KPart) {
232
+ part.tabContribution = contribution;
233
+ part.isEditor = this.containerId === EDITOR_AREA_MAIN;
234
+ }
235
+ }
236
+
237
+ // Give component time to initialize
238
+ requestAnimationFrame(() => {
239
+ this.updateToolbarFromComponent(tabPanel);
240
+ this.updateToolbarHeightVariable(tabPanel);
241
+ this.setupToolbarResizeObserver(tabPanel);
242
+ });
243
+ }
244
+ });
245
+ }
246
+
247
+ handleTabAuxClick(event: MouseEvent, contribution: TabContribution): void {
248
+ if (event.button === MouseButton.MIDDLE && contribution.closable) {
249
+ this.closeTab(event, contribution.name);
250
+ }
251
+ }
252
+
253
+ async closeTab(event: Event, tabName: string): Promise<void> {
254
+ event.stopPropagation();
255
+
256
+ if (this.isDirty(tabName) && !await confirmDialog("Unsaved changes will be lost: Do you really want to close?")) {
257
+ return;
258
+ }
259
+
260
+ const tabPanel = this.getTabPanel(tabName);
261
+ if (!tabPanel) return;
262
+
263
+ const contribution = this.contributions.find(c => c.name === tabName);
264
+ if (!contribution) return;
265
+
266
+ this.cleanupTabInstance(tabPanel);
267
+
268
+ const index = this.contributions.indexOf(contribution);
269
+ if (index > -1) {
270
+ this.contributions.splice(index, 1);
271
+ }
272
+
273
+ this.dispatchEvent(new CustomEvent('tab-closed', {detail: tabPanel}));
274
+
275
+ this.requestUpdate();
276
+
277
+ this.updateComplete.then(() => {
278
+ this.activateNextAvailableTab();
279
+ });
280
+ }
281
+
282
+ markDirty(name: string, dirty: boolean): void {
283
+ const tab = this.getTab(name);
284
+ tab!.classList.toggle("part-dirty", dirty);
285
+ }
286
+
287
+ isDirty(name: string): boolean {
288
+ const tab = this.getTab(name);
289
+ return tab!.classList.contains("part-dirty");
290
+ }
291
+
292
+ // ============= Private Helper Methods =============
293
+
294
+ /**
295
+ * Loads tab contributions from the registry.
296
+ */
297
+ private loadAndResolveContributions(): void {
298
+ this.contributions = contributionRegistry.getContributions(this.containerId!) as TabContribution[];
299
+ this.requestUpdate();
300
+ }
301
+
302
+ /**
303
+ * Cleans up a tab instance when the tab is closed.
304
+ *
305
+ * Cleanup Process:
306
+ * 1. Disconnect ResizeObserver if one exists
307
+ * 2. Call component's close() method if available (disposes resources)
308
+ * 3. DOM element is removed by caller (closeTab method)
309
+ */
310
+ private cleanupTabInstance(tabPanel: HTMLElement): void {
311
+ // Clean up ResizeObserver
312
+ const observer = this.resizeObservers.get(tabPanel);
313
+ if (observer) {
314
+ observer.disconnect();
315
+ this.resizeObservers.delete(tabPanel);
316
+ }
317
+
318
+ // Explicitly close the component inside the tab before removing
319
+ // This allows components to dispose resources (e.g., Monaco editor models, event listeners)
320
+ const contentDiv = tabPanel.querySelector('.tab-content');
321
+ if (contentDiv && contentDiv.firstElementChild) {
322
+ const component = contentDiv.firstElementChild;
323
+ if ('close' in component && typeof component.close === 'function') {
324
+ component.close();
325
+ }
326
+ }
327
+ }
328
+
329
+ private activateNextAvailableTab(): void {
330
+ // Guard: Component might not be fully initialized yet
331
+ if (!this.tabGroup.value) return;
332
+
333
+ const allRemainingTabs = this.tabGroup.value.querySelectorAll("wa-tab");
334
+ if (allRemainingTabs.length > 0) {
335
+ const newActive = allRemainingTabs.item(0).getAttribute("panel");
336
+ if (newActive) {
337
+ this.tabGroup.value.setAttribute("active", newActive);
338
+ }
339
+ } else {
340
+ this.tabGroup.value.removeAttribute("active");
341
+ }
342
+ }
343
+
344
+ private getTabPanel(name: string): HTMLElement | null {
345
+ if (!this.tabGroup.value) return null;
346
+ return this.tabGroup.value.querySelector(`wa-tab-panel[name='${name}']`) as HTMLElement | null;
347
+ }
348
+
349
+ private getTab(name: string): HTMLElement | null {
350
+ if (!this.tabGroup.value) return null;
351
+ return this.tabGroup.value.querySelector(`wa-tab[panel='${name}']`) as HTMLElement | null;
352
+ }
353
+
354
+ /**
355
+ * Updates the toolbar for a tab panel by querying the component for its toolbar content.
356
+ * This allows KPart components to provide their own toolbar items directly.
357
+ */
358
+ private updateToolbarFromComponent(tabPanel: HTMLElement): void {
359
+ const contentDiv = tabPanel.querySelector('.tab-content');
360
+ if (!contentDiv || !contentDiv.firstElementChild) return;
361
+
362
+ const component = contentDiv.firstElementChild;
363
+ if (!(component instanceof KPart)) return;
364
+
365
+ // Check if component has renderToolbar method
366
+ if (!component['renderToolbar']) return;
367
+
368
+ // Query for k-toolbar directly since there's only one per tab panel
369
+ const toolbar = tabPanel.querySelector('k-toolbar') as KToolbar | null;
370
+ if (toolbar) {
371
+ // Pass a bound render function to maintain component context
372
+ toolbar.partToolbarRenderer = () => component['renderToolbar']();
373
+ toolbar.requestUpdate();
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Updates the context menu for a tab panel by querying the component for its context menu content.
379
+ * This allows KPart components to provide their own context menu items directly.
380
+ */
381
+ private updateContextMenuFromComponent(tabPanel: HTMLElement): void {
382
+ const contentDiv = tabPanel.querySelector('.tab-content');
383
+ if (!contentDiv || !contentDiv.firstElementChild) return;
384
+
385
+ const component = contentDiv.firstElementChild;
386
+ if (!(component instanceof KPart)) return;
387
+
388
+ // Check if component has renderContextMenu method
389
+ if (!component['renderContextMenu']) return;
390
+
391
+ // Query for k-contextmenu directly since there's only one per tab panel
392
+ const contextMenu = tabPanel.querySelector('k-contextmenu') as KContextMenu | null;
393
+ if (contextMenu) {
394
+ // Pass a bound render function to maintain component context
395
+ contextMenu.partContextMenuRenderer = () => component['renderContextMenu']();
396
+ contextMenu.requestUpdate();
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Updates the toolbar height CSS variable for calc() positioning.
402
+ */
403
+ private updateToolbarHeightVariable(tabPanel: HTMLElement): void {
404
+ const toolbar = tabPanel.querySelector('.tab-toolbar') as HTMLElement | null;
405
+ if (!toolbar) return;
406
+
407
+ const toolbarHeight = toolbar.offsetHeight;
408
+ tabPanel.style.setProperty('--toolbar-height', `${toolbarHeight}px`);
409
+ }
410
+
411
+ /**
412
+ * Sets up a ResizeObserver to update toolbar height variable when toolbar size changes.
413
+ * Reuses existing observer if one already exists for this tab panel.
414
+ */
415
+ private setupToolbarResizeObserver(tabPanel: HTMLElement): void {
416
+ // Check if observer already exists
417
+ if (this.resizeObservers.has(tabPanel)) return;
418
+
419
+ const toolbar = tabPanel.querySelector('.tab-toolbar') as HTMLElement | null;
420
+ if (!toolbar) return;
421
+
422
+ const observer = new ResizeObserver(() => {
423
+ this.updateToolbarHeightVariable(tabPanel);
424
+ });
425
+ observer.observe(toolbar);
426
+ this.resizeObservers.set(tabPanel, observer);
427
+ }
428
+
429
+ // ============= Render Method =============
430
+
431
+ render() {
432
+ const currentApp = appLoaderService.getCurrentApp();
433
+
434
+ return html`
435
+ <wa-tab-group ${ref(this.tabGroup)}>
436
+ ${when(
437
+ this.contributions.length === 0,
438
+ () => html`
439
+ <div class="empty-state">
440
+ ${when(
441
+ currentApp,
442
+ () => html`
443
+ <div class="empty-content">
444
+ <h2 class="empty-title">${currentApp!.name}</h2>
445
+ ${when(
446
+ currentApp!.description,
447
+ () => html`<p class="empty-description">${currentApp!.description}</p>`
448
+ )}
449
+ </div>
450
+ `,
451
+ () => html`
452
+ <wa-icon name="folder-open" class="empty-icon"></wa-icon>
453
+ `
454
+ )}
455
+ </div>
456
+ `,
457
+ () => repeat(
458
+ this.contributions,
459
+ (c) => c.name,
460
+ (c) => html`
461
+ <wa-tab panel="${c.name}"
462
+ @auxclick="${(e: MouseEvent) => this.handleTabAuxClick(e, c)}">
463
+ <k-icon name="${c.icon!}"></k-icon>
464
+ ${c.label}
465
+ ${when(c.closable, () => html`
466
+ <wa-icon name="xmark" label="Close" @click="${(e: Event) => this.closeTab(e, c.name)}"></wa-icon>
467
+ `)}
468
+ </wa-tab>
469
+ <wa-tab-panel name="${c.name}">
470
+ <k-toolbar id="toolbar:${c.editorId ?? c.name}"
471
+ class="tab-toolbar"
472
+ ?is-editor="${this.containerId === EDITOR_AREA_MAIN}"></k-toolbar>
473
+ <wa-scroller class="tab-content" orientation="vertical">
474
+ ${c.component ? c.component(c.name) : nothing}
475
+ </wa-scroller>
476
+ <k-contextmenu id="contextmenu:${c.name}"
477
+ ?is-editor="${this.containerId === EDITOR_AREA_MAIN}"></k-contextmenu>
478
+ </wa-tab-panel>
479
+ `
480
+ )
481
+ )}
482
+ </wa-tab-group>
483
+ `;
484
+ }
485
+
486
+ static styles = css`
487
+ :host {
488
+ height: 100%;
489
+ width: 100%;
490
+ }
491
+
492
+ wa-tab-group {
493
+ height: 100%;
494
+ width: 100%;
495
+ }
496
+
497
+ wa-tab-group::part(base) {
498
+ display: grid;
499
+ grid-template-rows: auto minmax(0, 1fr);
500
+ height: 100%;
501
+ width: 100%;
502
+ }
503
+
504
+ wa-tab-panel[active] {
505
+ display: grid;
506
+ grid-template-rows: minmax(0, 1fr);
507
+ height: 100%;
508
+ width: 100%;
509
+ overflow: hidden;
510
+ position: relative;
511
+ }
512
+
513
+ .tab-content {
514
+ position: absolute;
515
+ top: calc(var(--toolbar-height, 0px));
516
+ right: 0;
517
+ left: 0;
518
+ height: calc(100% - var(--toolbar-height, 0px));
519
+ }
520
+
521
+ wa-tab::part(base) {
522
+ padding: 3px 0.5rem;
523
+ }
524
+
525
+ wa-tab-panel {
526
+ --padding: 0px;
527
+ }
528
+
529
+ .part-dirty::part(base) {
530
+ font-style: italic;
531
+ color: var(--wa-color-danger-fill-loud)
532
+ }
533
+
534
+ .empty-state {
535
+ display: flex;
536
+ align-items: center;
537
+ justify-content: center;
538
+ width: 100%;
539
+ height: 100%;
540
+ grid-row: 2;
541
+ }
542
+
543
+ .empty-content {
544
+ display: flex;
545
+ flex-direction: column;
546
+ align-items: center;
547
+ justify-content: center;
548
+ text-align: center;
549
+ padding: 2rem;
550
+ gap: 0.75rem;
551
+ opacity: 0.3;
552
+ }
553
+
554
+ .empty-title {
555
+ margin: 0;
556
+ font-size: 1.5rem;
557
+ font-weight: 500;
558
+ color: var(--wa-color-text-quiet);
559
+ }
560
+
561
+ .empty-description {
562
+ margin: 0;
563
+ font-size: 1rem;
564
+ color: var(--wa-color-text-quiet);
565
+ max-width: 500px;
566
+ }
567
+
568
+ .empty-icon {
569
+ font-size: 6rem;
570
+ opacity: 0.2;
571
+ color: var(--wa-color-text-quiet);
572
+ }
573
+ `
574
+ }