Skip to content

Commit 7a27265

Browse files
committed
feat: allow docks-tabs to have a custom extensible toolbar
1 parent e711081 commit 7a27265

12 files changed

Lines changed: 160 additions & 60 deletions

File tree

packages/app/src/dashboard-layout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ contributionRegistry.registerContribution("dashboard-views-toolbar-bottom", {
7777

7878
contributionRegistry.registerContribution("dashboard-views-toolbar-bottom", {
7979
label: "Settings",
80-
icon: "gear",
80+
icon: "docks settings",
8181
command: "open_settings"
8282
});
8383

packages/core/src/commands/global.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,13 @@ registerAll({
121121
title: "Extensions",
122122
data: {},
123123
key: "system.extensions",
124-
icon: "puzzle-piece",
124+
icon: "docks extensions",
125125
state: {},
126126
component: (id: string) => html`<docks-extensions id="${id}"></docks-extensions>`,
127127
}
128128
editorRegistry.loadEditor(editorInput, "extensions-editor").then()
129129
}
130130
},
131-
contribution: {
132-
target: TOOLBAR_MAIN_RIGHT,
133-
icon: "puzzle-piece",
134-
label: "Open Extensions",
135-
}
136131
})
137132

138133
registerAll({

packages/core/src/contributions/default-ui-contributions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
contributionRegistry.registerContribution(SIDEBAR_MAIN, {
1919
name: VIEW_FILEBROWSER,
2020
label: "Workspace",
21-
icon: "folder-open",
21+
icon: "folder",
2222
component: (id: string) => html`<docks-filebrowser id="${id}"></docks-filebrowser>`
2323
});
2424

packages/core/src/core/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export const TOOLBAR_BOTTOM = "app-toolbars-bottom"
66
export const TOOLBAR_BOTTOM_CENTER = "app-toolbars-bottom-center"
77
export const TOOLBAR_BOTTOM_END = "app-toolbars-bottom-end"
88

9+
/** Commands/HTML at the end of the main sidebar activity rail (wa-tab-group nav slot). */
10+
export const SIDEBAR_MAIN_TOOLBAR = "sidebar-main-toolbar"
11+
912
export const SYSTEM_VIEWS = "system-views"
1013
export const SYSTEM_LAYOUTS = "system.layouts"
1114

packages/core/src/core/extensionregistry.ts

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,8 @@ class ExtensionRegistry {
198198
logger.info(`Registered extension from URL: ${id}`);
199199
}
200200

201-
this.enable(id, false);
202-
201+
await this.enableAsync(id, false);
202+
203203
logger.info(`Successfully enabled extension from URL: ${finalUrl}`);
204204
return id;
205205
} catch (error) {
@@ -248,11 +248,11 @@ class ExtensionRegistry {
248248
return
249249
}
250250
logger.debug(`Loading extension: ${extensionId}`)
251-
this.load(extensionId).then(() => {
252-
this.updateEnablement(extensionId, true, informUser)
253-
}).catch(_e => {
254-
logger.error(`Could not load extension: ${extensionId}: ${_e}`)
255-
})
251+
this.load(extensionId)
252+
.then(() => this.updateEnablementAsync(extensionId, true, informUser))
253+
.catch(_e => {
254+
logger.error(`Could not load extension: ${extensionId}: ${_e}`)
255+
})
256256
}
257257

258258
/** Like enable() but returns a Promise that resolves when the extension is loaded. Use when the caller must wait for commands/contributions to be registered (e.g. before rendering the app). */
@@ -263,7 +263,7 @@ class ExtensionRegistry {
263263
logger.debug(`Loading extension: ${extensionId}`)
264264
try {
265265
await this.load(extensionId)
266-
this.updateEnablement(extensionId, true, informUser)
266+
await this.updateEnablementAsync(extensionId, true, informUser)
267267
} catch (e) {
268268
logger.error(`Could not load extension: ${extensionId}: ${e}`)
269269
throw e
@@ -341,17 +341,17 @@ class ExtensionRegistry {
341341
}
342342
})
343343

344-
// Mark as loaded BEFORE executing the module
345-
this.loadedExtensions.add(extensionId)
346-
347344
if (module?.default instanceof Function) {
348345
try {
349-
module.default(uiContext.getProxy())
346+
const activationResult = module.default(uiContext.getProxy())
347+
await Promise.resolve(activationResult)
350348
} catch (error) {
351349
logger.error(`Error executing extension function for ${extensionId}: ${error}`)
352350
throw error
353351
}
354352
}
353+
354+
this.loadedExtensions.add(extensionId)
355355

356356
} catch (error) {
357357
// If loading failed, remove from loaded set
@@ -371,28 +371,8 @@ class ExtensionRegistry {
371371
if (!this.isEnabled(extensionId)) {
372372
return
373373
}
374-
this.updateEnablement(extensionId, false, informUser)
375-
}
376-
377-
private updateEnablement(extensionId: string, enabled: boolean, informUser: boolean) {
378-
this.checkExtensionsConfig().then(() => {
379-
const extension = this.extensionsSettings?.find(e => e.id == extensionId)
380-
if (extension) {
381-
extension.enabled = enabled
382-
} else {
383-
this.extensionsSettings?.push({id: extensionId, enabled: enabled})
384-
}
385-
appSettings.set(KEY_EXTENSIONS_CONFIG, this.extensionsSettings).then(() => {
386-
if (informUser) {
387-
const extObj = this.extensions[extensionId]
388-
if (enabled) {
389-
toastInfo(extObj.name + " enabled.")
390-
} else {
391-
toastInfo(extObj.name + " disabled " + " - Please restart to take effect")
392-
}
393-
}
394-
publish(TOPIC_EXTENSIONS_CHANGED, this.extensionsSettings)
395-
})
374+
void this.updateEnablementAsync(extensionId, false, informUser).catch((e) => {
375+
logger.error(`Could not persist disable for extension ${extensionId}: ${e}`)
396376
})
397377
}
398378

Lines changed: 17 additions & 0 deletions
Loading

packages/core/src/icons/icons.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Provenance — bundled icons in this folder (licence, author, source).
2+
# Used for compliance review and attribution; resolver: registerIconLibrary('docks') in externals/webawesome.ts
3+
14
- python.svg: Licence=MIT, Author=Garuda Technology, Source=https://www.svgrepo.com/svg/452091/python
25
- js.svg: Licence=MIT, Author=Garuda Technology, Source=https://www.svgrepo.com/svg/452045/js
36
- jupyter.svg: Licence=Logo License, Author=gilbarbara, Source=https://www.svgrepo.com/svg/353949/jupyter
@@ -7,3 +10,5 @@
710
- layout-standard-full.svg: Licence=Project-internal, Author=Eclipse Docks Team
811
- mark-github.svg: Licence=MIT, Author=GitHub, Source=https://github.com/primer/octicons (icons/mark-github-16.svg)
912
- file-plus.svg: Licence=Project-internal, Author=Eclipse Docks Team
13+
- settings.svg: Licence=Project-internal, Author=Eclipse Docks Team, Note=Settings; duotone brand tracks + success knobs (sliders motif)
14+
- extensions.svg: Licence=Project-internal, Author=Eclipse Docks Team, Note=Extensions; 2×2 tile grid like docks favicon (packages/app/public/favicon.svg); one success tile, three brand tiles
Lines changed: 8 additions & 0 deletions
Loading

packages/core/src/layouts/standard-layout.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,11 @@ export class DocksStandardLayout extends DocksContainer {
139139
id="left-sidebar-split"
140140
orientation="vertical"
141141
sizes="50%, 50%">
142-
<docks-tabs id="${SIDEBAR_MAIN}"></docks-tabs>
143-
<docks-tabs id="${SIDEBAR_MAIN_BOTTOM}"></docks-tabs>
142+
<docks-tabs id="${SIDEBAR_MAIN}" placement="start" icon-only with-toolbar></docks-tabs>
143+
<docks-tabs id="${SIDEBAR_MAIN_BOTTOM}" placement="start" icon-only></docks-tabs>
144144
</docks-resizable-grid>
145145
`
146-
: html`<docks-tabs id="${SIDEBAR_MAIN}"></docks-tabs>`
146+
: html`<docks-tabs id="${SIDEBAR_MAIN}" placement="start" icon-only with-toolbar></docks-tabs>`
147147
}
148148
`
149149
: nothing

packages/core/src/parts/tabs.ts

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {customElement, property, state} from "lit/decorators.js";
22
import {css, html, nothing} from "lit";
33
import {DocksContainer} from "./container";
4+
import {appLoaderService} from "../core/apploader";
45
import {contributionRegistry, ContributionChangeEvent, TabContribution, TOPIC_CONTRIBUTEIONS_CHANGED} from "../core/contributionregistry";
56
import {when} from "lit/directives/when.js";
67
import {repeat} from "lit/directives/repeat.js";
@@ -12,8 +13,6 @@ import {MouseButton, EDITOR_AREA_MAIN} from "../core/constants";
1213
import {activePartSignal, activeEditorSignal, partDirtySignal} from "../core/appstate";
1314
import {watchSignal} from "../core/signals";
1415
import {confirmDialog} from "../dialogs";
15-
import {appLoaderService} from "../core/apploader";
16-
1716
/**
1817
* DocksTabs - A dynamic tab container component
1918
*
@@ -32,6 +31,16 @@ export class DocksTabs extends DocksContainer {
3231
@property({reflect: true})
3332
placement: "top" | "bottom" | "start" | "end" = "top";
3433

34+
/** When true, tab contributions show icons only (labels remain on title + wa-icon for a11y). */
35+
@property({type: Boolean, reflect: true, attribute: 'icon-only'})
36+
iconOnly: boolean = false;
37+
38+
/**
39+
* When true, render a `docks-toolbar` in the tab group `nav` slot after tabs (extensions contribute via id `${containerId}-toolbar`).
40+
*/
41+
@property({type: Boolean, reflect: true, attribute: 'with-toolbar'})
42+
withToolbar: boolean = false;
43+
3544
/** Tab contributions for this container */
3645
@state()
3746
private contributions: TabContribution[] = [];
@@ -60,11 +69,14 @@ export class DocksTabs extends DocksContainer {
6069
this.updateComplete.then(() => this.ensureTabGroupListenersAndActivate());
6170

6271
subscribe(TOPIC_CONTRIBUTEIONS_CHANGED, (event: ContributionChangeEvent) => {
63-
if (!this.containerId || event.target !== this.containerId) return;
64-
72+
if (!this.containerId) return;
73+
const navToolbarId = `${this.containerId}-toolbar`;
74+
if (event.target === navToolbarId) return;
75+
if (event.target !== this.containerId) return;
76+
6577
this.loadAndResolveContributions();
6678
this.requestUpdate();
67-
79+
6880
this.updateComplete.then(() => {
6981
this.activateNextAvailableTab();
7082
});
@@ -326,6 +338,11 @@ export class DocksTabs extends DocksContainer {
326338
return label.slice(0, startLen) + ellipsis + label.slice(-(take - startLen));
327339
}
328340

341+
/** Aligns `docks-toolbar` with `placement`: side rails → vertical, top/bottom tab strip → horizontal. */
342+
private withToolbarOrientation(): "horizontal" | "vertical" {
343+
return this.placement === "start" || this.placement === "end" ? "vertical" : "horizontal";
344+
}
345+
329346
private renderEmptyState() {
330347
const currentApp = appLoaderService.getCurrentApp();
331348
return html`
@@ -351,6 +368,7 @@ export class DocksTabs extends DocksContainer {
351368
if (this.contributions.length === 0) {
352369
return this.renderEmptyState();
353370
}
371+
const navToolbarId = this.containerId ? `${this.containerId}-toolbar` : '';
354372
return html`
355373
<wa-tab-group ${ref(this.tabGroup)} placement=${this.placement}>
356374
${repeat(
@@ -364,7 +382,7 @@ export class DocksTabs extends DocksContainer {
364382
title="${fullLabel}"
365383
@auxclick="${(e: MouseEvent) => this.handleTabAuxClick(e, c)}">
366384
${icon(c.icon, { label: fullLabel })}
367-
${shortLabel}
385+
${this.iconOnly ? nothing : shortLabel}
368386
${when(c.closable, () => html`
369387
<wa-icon name="xmark" label="Close" @click="${(e: Event) => this.closeTab(e, c.name)}"></wa-icon>
370388
`)}
@@ -375,6 +393,18 @@ export class DocksTabs extends DocksContainer {
375393
`;
376394
}
377395
)}
396+
${this.withToolbar && navToolbarId
397+
? html`
398+
<div class="nav-toolbar-spacer" slot="nav" aria-hidden="true"></div>
399+
<docks-toolbar
400+
slot="nav"
401+
id=${navToolbarId}
402+
orientation=${this.withToolbarOrientation()}
403+
align="center"
404+
size="large"
405+
></docks-toolbar>
406+
`
407+
: nothing}
378408
</wa-tab-group>
379409
`;
380410
}
@@ -388,15 +418,22 @@ export class DocksTabs extends DocksContainer {
388418
wa-tab-group {
389419
height: 100%;
390420
width: 100%;
421+
min-height: 0;
391422
}
392423
393-
wa-tab-group::part(base) {
424+
:host(:is([placement="top"], [placement="bottom"])) wa-tab-group::part(base) {
394425
display: grid;
395426
grid-template-rows: auto minmax(0, 1fr);
396427
height: 100%;
397428
width: 100%;
398429
}
399430
431+
:host(:is([placement="start"], [placement="end"])) wa-tab-group::part(base) {
432+
height: 100%;
433+
width: 100%;
434+
min-height: 0;
435+
}
436+
400437
wa-tab-panel[active] {
401438
display: grid;
402439
grid-template-rows: minmax(0, 1fr);
@@ -416,6 +453,56 @@ export class DocksTabs extends DocksContainer {
416453
padding: 3px 0.5rem;
417454
}
418455
456+
:host([icon-only]) wa-tab::part(base) {
457+
justify-content: center;
458+
}
459+
460+
:host([icon-only]:is([placement="top"], [placement="bottom"])) wa-tab::part(base) {
461+
padding: var(--wa-space-s);
462+
}
463+
464+
:host([icon-only]:is([placement="start"], [placement="end"])) wa-tab::part(base) {
465+
padding-inline: 0;
466+
padding-block: var(--wa-space-s);
467+
}
468+
469+
:host([icon-only]) wa-tab wa-icon {
470+
font-size: var(--wa-font-size-l);
471+
}
472+
473+
:host([icon-only]:is([placement="start"], [placement="end"])) wa-tab-group::part(nav),
474+
:host([icon-only]:is([placement="start"], [placement="end"])) wa-tab-group::part(tabs) {
475+
padding: 0;
476+
margin: 0;
477+
}
478+
479+
:host([icon-only]:is([placement="start"], [placement="end"])) wa-tab-group::part(nav) {
480+
flex: 0 0 auto;
481+
}
482+
483+
:host([with-toolbar]) .nav-toolbar-spacer {
484+
flex: 1 1 auto;
485+
min-height: 0;
486+
min-width: 0;
487+
pointer-events: none;
488+
}
489+
490+
:host([with-toolbar]:is([placement="start"], [placement="end"])) wa-tab-group::part(nav) {
491+
display: grid;
492+
grid-template-rows: 1fr;
493+
height: 100%;
494+
min-height: 0;
495+
}
496+
497+
:host([with-toolbar]:is([placement="start"], [placement="end"])) wa-tab-group::part(tabs) {
498+
display: flex;
499+
flex-direction: column;
500+
min-height: 0;
501+
height: 100%;
502+
flex: 1 1 auto;
503+
align-self: stretch;
504+
}
505+
419506
wa-tab-panel {
420507
--padding: 0px;
421508
}
@@ -431,7 +518,6 @@ export class DocksTabs extends DocksContainer {
431518
justify-content: center;
432519
width: 100%;
433520
height: 100%;
434-
grid-row: 2;
435521
}
436522
437523
.empty-content {

0 commit comments

Comments
 (0)