diff --git a/.eslintrc.json b/.eslintrc.json index 664dadea3..c423bc586 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -107,7 +107,10 @@ "rxjs/no-unsafe-takeuntil": [ "error", { - "alias": ["takeUntil", "takeUntilDestroyed"] + "alias": [ + "takeUntil", + "takeUntilDestroyed" + ] } ] } @@ -120,7 +123,17 @@ "plugin:@angular-eslint/template/recommended", "plugin:@angular-eslint/template/accessibility" ], - "rules": {} + "rules": { + "@angular-eslint/template/label-has-associated-control": [ + "error", + { + "controlComponents": [ + "sci-checkbox", + "sci-toggle-button" + ] + } + ] + } } ] } diff --git a/README.md b/README.md index c2e4a0f69..dc56b4096 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,7 @@ SCION Workbench enables the creation of Angular web applications that require a - [**Getting Started**][link-getting-started]\ Follow these steps to install the SCION Workbench in your project and start with a basic introduction to the SCION Workbench. -- [**How To Guides**][link-howto]\ - Get answers to the most common questions when developing an application with the SCION Workbench. - -#### Workbench Applications - -- [**SCION Workbench Demo**][link-demo-app]\ - See a live demo of the SCION Workbench. +#### Workbench Demo Applications - [**SCION Workbench Testing App**][link-testing-app]\ Visit our technical testing application to explore the workbench and experiment with its features. @@ -35,6 +29,9 @@ SCION Workbench enables the creation of Angular web applications that require a - [**About SCION Workbench**][link-overview]\ Learn more about the SCION Workbench. +- [**How To Guides**][link-howto]\ + Get answers to the most common questions when developing an application with the SCION Workbench. + - [**Workbench and Microfrontends**][link-microfrontend-integration]\ Consider adopting a microfrontend architecture with SCION Workbench. diff --git a/apps/workbench-client-testing-app/src/app/app.component.html b/apps/workbench-client-testing-app/src/app/app.component.html index 188379a05..687ba960f 100644 --- a/apps/workbench-client-testing-app/src/app/app.component.html +++ b/apps/workbench-client-testing-app/src/app/app.component.html @@ -3,8 +3,8 @@ - has-focus - {{appSymbolicName}} + has-focus + {{appSymbolicName}} diff --git a/apps/workbench-client-testing-app/src/app/app.component.scss b/apps/workbench-client-testing-app/src/app/app.component.scss index b35fa2c1a..bc10d851b 100644 --- a/apps/workbench-client-testing-app/src/app/app.component.scss +++ b/apps/workbench-client-testing-app/src/app/app.component.scss @@ -1,4 +1,4 @@ -@use '@scion/components.internal' as sci-ɵcomponents; +@use '@scion/components.internal/design' as sci-design; :host { display: grid; @@ -16,11 +16,15 @@ padding: .25em; > span.chip.has-focus { - @include sci-ɵcomponents.theme-chip(var(--sci-color-primary-darker), var(--sci-color-background), var(--sci-color-P900)); + @include sci-design.style-chip(var(--sci-color-accent), var(--sci-color-background-primary), var(--sci-color-accent)); } > span.chip.app-symbolic-name { - @include sci-ɵcomponents.theme-chip(var(--app-color), var(--app-color), var(--sci-color-background)); + @include sci-design.style-chip(var(--sci-color-text), var(--sci-color-background-primary), var(--sci-color-text)); + + &.has-focus { + @include sci-design.style-chip(var(--sci-color-accent), var(--sci-color-background-primary), var(--sci-color-accent)); + } } } diff --git a/apps/workbench-client-testing-app/src/app/app.component.ts b/apps/workbench-client-testing-app/src/app/app.component.ts index 587d33948..981c10edf 100644 --- a/apps/workbench-client-testing-app/src/app/app.component.ts +++ b/apps/workbench-client-testing-app/src/app/app.component.ts @@ -8,12 +8,14 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {Component, HostBinding, Inject, Optional} from '@angular/core'; -import {APP_IDENTITY, FocusMonitor, MicrofrontendPlatformClient, PlatformPropertyService} from '@scion/microfrontend-platform'; -import {AsyncPipe, NgIf} from '@angular/common'; +import {Component, inject, Inject, Optional} from '@angular/core'; +import {APP_IDENTITY, FocusMonitor, MicrofrontendPlatformClient} from '@scion/microfrontend-platform'; +import {AsyncPipe, DOCUMENT, NgIf} from '@angular/common'; import {SciViewportComponent} from '@scion/components/viewport'; import {RouterOutlet} from '@angular/router'; import {A11yModule} from '@angular/cdk/a11y'; +import {WorkbenchThemeMonitor} from '@scion/workbench-client'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ selector: 'app-root', @@ -30,17 +32,27 @@ import {A11yModule} from '@angular/cdk/a11y'; }) export class AppComponent { + public readonly focusMonitor = inject(FocusMonitor, {optional: true}); // only available if running in the workbench context public readonly workbenchContextActive = MicrofrontendPlatformClient.isConnected(); public appSymbolicName: string; - @HostBinding('style.--app-color') - public appColor: string; - - constructor(@Inject(APP_IDENTITY) @Optional() symbolicName: string, // not available if not running in the workbench context - @Optional() propertyService: PlatformPropertyService, // not available if not running in the workbench context - @Optional() public focusMonitor: FocusMonitor) { // not available if not running in the workbench context + constructor(@Inject(APP_IDENTITY) @Optional() symbolicName: string) { // only available if running in the workbench context this.appSymbolicName = symbolicName; - this.appColor = propertyService?.get(symbolicName).color; + this.installWorkbenchThemeSwitcher(); + } + + private installWorkbenchThemeSwitcher(): void { + const documentRoot = inject(DOCUMENT).documentElement; + inject(WorkbenchThemeMonitor, {optional: true})?.theme$ // only available if running in the workbench context + .pipe(takeUntilDestroyed()) + .subscribe(theme => { + if (theme) { + documentRoot.setAttribute('sci-theme', theme); + } + else { + documentRoot.removeAttribute('sci-theme'); + } + }); } } diff --git a/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html b/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html index 266faaa60..1eda144db 100644 --- a/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html +++ b/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html @@ -55,7 +55,7 @@ - + Open message box diff --git a/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.scss b/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.scss index ddf1017b7..7c1c6d66b 100644 --- a/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.scss +++ b/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.scss @@ -13,23 +13,27 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; } } > output.close-action { - border: 1px solid var(--sci-color-accent); - background-color: var(--sci-color-A100); - border-radius: 3px; + border: 1px solid var(--sci-color-positive); + background-color: var(--sci-color-background-positive); + color: var(--sci-color-positive); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } > output.open-error { - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } diff --git a/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.html b/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.html index a786aaaac..f9b70220d 100644 --- a/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.html +++ b/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.html @@ -48,7 +48,7 @@ - + Show notification diff --git a/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.scss b/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.scss index 6f5390642..989413bac 100644 --- a/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.scss +++ b/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.scss @@ -12,16 +12,18 @@ > section { display: grid; grid-row-gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; } } > output.error { - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } diff --git a/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.html b/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.html index 27785d498..f2c22ac89 100644 --- a/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.html +++ b/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.html @@ -74,7 +74,7 @@ - + Open popup diff --git a/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.scss b/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.scss index b8f9c228c..39238efba 100644 --- a/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.scss +++ b/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.scss @@ -12,8 +12,8 @@ > section { display: grid; grid-row-gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; } @@ -27,16 +27,20 @@ } > output.return-value { - border: 1px solid var(--sci-color-accent); - background-color: var(--sci-color-A100); - border-radius: 3px; + border: 1px solid var(--sci-color-positive); + background-color: var(--sci-color-background-positive); + color: var(--sci-color-positive); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } > output.popup-error { - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } diff --git a/apps/workbench-client-testing-app/src/app/popup-page/popup-page.component.html b/apps/workbench-client-testing-app/src/app/popup-page/popup-page.component.html index 30d8efa03..2564f3f89 100644 --- a/apps/workbench-client-testing-app/src/app/popup-page/popup-page.component.html +++ b/apps/workbench-client-testing-app/src/app/popup-page/popup-page.component.html @@ -123,6 +123,6 @@ - Close + Close Close (with error) diff --git a/apps/workbench-client-testing-app/src/app/popup-page/popup-page.component.scss b/apps/workbench-client-testing-app/src/app/popup-page/popup-page.component.scss index 682663a15..6fff93d03 100644 --- a/apps/workbench-client-testing-app/src/app/popup-page/popup-page.component.scss +++ b/apps/workbench-client-testing-app/src/app/popup-page/popup-page.component.scss @@ -1,4 +1,4 @@ -@use '@scion/components.internal' as sci-ɵcomponents; +@use '@scion/components.internal/design' as sci-design; :host { display: flex; @@ -22,8 +22,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -73,7 +73,7 @@ } &.return-value input { - @include sci-ɵcomponents.theme-input-field(); + @include sci-design.style-input-field(); } } } diff --git a/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.scss b/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.scss index 4cd9bd472..596d36f47 100644 --- a/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.scss +++ b/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.scss @@ -11,8 +11,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -22,19 +22,21 @@ } > output.register-response { - border: 1px solid var(--sci-color-accent); - background-color: var(--sci-color-A100); - border-radius: 3px; + border: 1px solid var(--sci-color-positive); + background-color: var(--sci-color-background-positive); + color: var(--sci-color-positive); + border-radius: var(--sci-corner); padding: 1em; - white-space: pre; font-family: monospace; } > output.register-error { - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } } diff --git a/apps/workbench-client-testing-app/src/app/register-workbench-intention-page/register-workbench-intention-page.component.scss b/apps/workbench-client-testing-app/src/app/register-workbench-intention-page/register-workbench-intention-page.component.scss index dfdd5fe76..06892e592 100644 --- a/apps/workbench-client-testing-app/src/app/register-workbench-intention-page/register-workbench-intention-page.component.scss +++ b/apps/workbench-client-testing-app/src/app/register-workbench-intention-page/register-workbench-intention-page.component.scss @@ -12,8 +12,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -23,17 +23,21 @@ } > output.intention-id { - border: 1px solid var(--sci-color-accent); - background-color: var(--sci-color-A100); - border-radius: 3px; + border: 1px solid var(--sci-color-positive); + background-color: var(--sci-color-background-positive); + color: var(--sci-color-positive); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } > output.register-error { - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } } diff --git a/apps/workbench-client-testing-app/src/app/router-page/router-page.component.html b/apps/workbench-client-testing-app/src/app/router-page/router-page.component.html index 04df10636..e9562eb6d 100644 --- a/apps/workbench-client-testing-app/src/app/router-page/router-page.component.html +++ b/apps/workbench-client-testing-app/src/app/router-page/router-page.component.html @@ -43,7 +43,7 @@ - Navigate + Navigate {{navigateError}} diff --git a/apps/workbench-client-testing-app/src/app/router-page/router-page.component.scss b/apps/workbench-client-testing-app/src/app/router-page/router-page.component.scss index adaa0e48f..440d8f0a0 100644 --- a/apps/workbench-client-testing-app/src/app/router-page/router-page.component.scss +++ b/apps/workbench-client-testing-app/src/app/router-page/router-page.component.scss @@ -11,8 +11,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -22,10 +22,12 @@ } > output.navigate-error { - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } } diff --git a/apps/workbench-client-testing-app/src/app/test-pages/angular-zone-test-page/angular-zone-test-page.component.scss b/apps/workbench-client-testing-app/src/app/test-pages/angular-zone-test-page/angular-zone-test-page.component.scss index e3f6700ea..823929e1a 100644 --- a/apps/workbench-client-testing-app/src/app/test-pages/angular-zone-test-page/angular-zone-test-page.component.scss +++ b/apps/workbench-client-testing-app/src/app/test-pages/angular-zone-test-page/angular-zone-test-page.component.scss @@ -23,18 +23,20 @@ padding: 1em; border-width: 1px; border-style: solid; + border-radius: var(--sci-corner); text-align: center; + font-family: monospace; &[data-zone="inside-angular"] { - color: #004d00; - background-color: #dff5df; - border-color: #004d00; + color: var(--sci-color-positive); + background-color: var(--sci-color-background-positive); + border-color: var(--sci-color-positive); } &[data-zone="outside-angular"] { - color: var(--sci-color-W900); - background-color: var(--sci-color-W50); - border-color: var(--sci-color-W900); + color: var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + border-color: var(--sci-color-negative); } } } diff --git a/apps/workbench-client-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.scss b/apps/workbench-client-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.scss index eccc81472..0d82832f7 100644 --- a/apps/workbench-client-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.scss +++ b/apps/workbench-client-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.scss @@ -8,8 +8,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; } } diff --git a/apps/workbench-client-testing-app/src/app/unregister-workbench-capability-page/unregister-workbench-capability-page.component.scss b/apps/workbench-client-testing-app/src/app/unregister-workbench-capability-page/unregister-workbench-capability-page.component.scss index e63f6583d..26f664140 100644 --- a/apps/workbench-client-testing-app/src/app/unregister-workbench-capability-page/unregister-workbench-capability-page.component.scss +++ b/apps/workbench-client-testing-app/src/app/unregister-workbench-capability-page/unregister-workbench-capability-page.component.scss @@ -11,16 +11,18 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; } > output.unregister-error { - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } > output.unregistered { diff --git a/apps/workbench-client-testing-app/src/app/view-page/view-page.component.scss b/apps/workbench-client-testing-app/src/app/view-page/view-page.component.scss index 62b04707f..820f4712c 100644 --- a/apps/workbench-client-testing-app/src/app/view-page/view-page.component.scss +++ b/apps/workbench-client-testing-app/src/app/view-page/view-page.component.scss @@ -8,8 +8,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; } diff --git a/apps/workbench-client-testing-app/src/app/workbench-client/workbench-client.provider.ts b/apps/workbench-client-testing-app/src/app/workbench-client/workbench-client.provider.ts index 5d9d0ea75..d867ec63b 100644 --- a/apps/workbench-client-testing-app/src/app/workbench-client/workbench-client.provider.ts +++ b/apps/workbench-client-testing-app/src/app/workbench-client/workbench-client.provider.ts @@ -10,7 +10,7 @@ import {APP_INITIALIZER, EnvironmentProviders, inject, makeEnvironmentProviders, NgZone} from '@angular/core'; import {APP_IDENTITY, ContextService, FocusMonitor, IntentClient, ManifestService, MessageClient, ObservableDecorator, OutletRouter, PlatformPropertyService, PreferredSizeService} from '@scion/microfrontend-platform'; -import {WorkbenchClient, WorkbenchMessageBoxService, WorkbenchNotificationService, WorkbenchPopup, WorkbenchPopupService, WorkbenchRouter, WorkbenchView} from '@scion/workbench-client'; +import {WorkbenchClient, WorkbenchMessageBoxService, WorkbenchNotificationService, WorkbenchPopup, WorkbenchPopupService, WorkbenchRouter, WorkbenchThemeMonitor, WorkbenchView} from '@scion/workbench-client'; import {NgZoneObservableDecorator} from './ng-zone-observable-decorator'; import {Beans} from '@scion/toolkit/bean-manager'; import {environment} from '../../environments/environment'; @@ -44,6 +44,7 @@ export function provideWorkbenchClient(): EnvironmentProviders | [] { {provide: WorkbenchPopup, useFactory: () => Beans.opt(WorkbenchPopup)}, {provide: WorkbenchMessageBoxService, useFactory: () => Beans.get(WorkbenchMessageBoxService)}, {provide: WorkbenchNotificationService, useFactory: () => Beans.get(WorkbenchNotificationService)}, + {provide: WorkbenchThemeMonitor, useFactory: () => Beans.get(WorkbenchThemeMonitor)}, ]); } diff --git a/apps/workbench-client-testing-app/src/styles.scss b/apps/workbench-client-testing-app/src/styles.scss index 3c8f44103..9c9fb30e6 100644 --- a/apps/workbench-client-testing-app/src/styles.scss +++ b/apps/workbench-client-testing-app/src/styles.scss @@ -1,38 +1,42 @@ -@use '@scion/components.internal/theme' with ( - $theme: ( - accent: ( - 50: #E8F5E9, - 100: #C8E6C9, - 200: #A5D6A7, - 300: #81C784, - 400: #66BB6A, - 500: #4CAF50, - 600: #43A047, - 700: #388E3C, - 800: #2E7D32, - 900: #1B5E20, - default: 800, - lighter: #60bf4cff, - darker: #327f1fff, - ), - ) - ); +@use '@angular/cdk'; +@use '@scion/components'; +@use '@scion/components.internal/design' as sci-design; -* { - box-sizing: border-box; -} +@import url('https://fonts.googleapis.com/css?family=Roboto:normal,bold,italic,bolditalic|Roboto+Mono'); +@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded'); html { + all: unset; font-size: 14px; // defines 1rem as 14px width: 100vw; height: 100vh; } body { + all: unset; font-family: Roboto, Arial, sans-serif; - color: var(--sci-color-foreground); - background-color: var(--sci-color-background); - margin: 0; + color: var(--sci-color-text); + background-color: unset; // Do not set the background color in microfrontends to inherit it from the embedding context of the workbench, such as view or dialog. width: 100vw; height: 100vh; } + +* { + box-sizing: border-box; +} + +a { + @include sci-design.style-link(); +} + +button[class*="material-icons"], button[class*="material-symbols"] { + @include sci-design.style-mat-icon-button(); +} + +button:not([class*="material-icons"]):not([class*="material-symbols"]) { + @include sci-design.style-button(); +} + +// Install Angular CDK styles +@include cdk.a11y-visually-hidden(); +@include cdk.overlay(); diff --git a/apps/workbench-testing-app/src/app/app.component.scss b/apps/workbench-testing-app/src/app/app.component.scss index b8271cbfc..54605eb26 100644 --- a/apps/workbench-testing-app/src/app/app.component.scss +++ b/apps/workbench-testing-app/src/app/app.component.scss @@ -6,6 +6,7 @@ > app-header { flex: none; + border-bottom: 1px solid var(--sci-color-border); } > main { diff --git a/apps/workbench-testing-app/src/app/header/header.component.html b/apps/workbench-testing-app/src/app/header/header.component.html index f653999f4..17f653977 100644 --- a/apps/workbench-testing-app/src/app/header/header.component.html +++ b/apps/workbench-testing-app/src/app/header/header.component.html @@ -1,3 +1,9 @@ + + dark_mode + + light_mode + + - + more_vert diff --git a/apps/workbench-testing-app/src/app/header/header.component.scss b/apps/workbench-testing-app/src/app/header/header.component.scss index 30b6c0574..be1695f9f 100644 --- a/apps/workbench-testing-app/src/app/header/header.component.scss +++ b/apps/workbench-testing-app/src/app/header/header.component.scss @@ -1,18 +1,29 @@ :host { display: flex; - place-content: flex-end; - background: #333333; + align-items: center; padding: .5em; + > div.theme-switcher { + flex: auto; + display: flex; + gap: .25em; + align-items: center; + user-select: none; + } + > div.toggle-buttons { + flex: none; display: flex; - border-radius: .25em; + border-radius: var(--sci-corner); + border: 1px solid var(--sci-color-border); overflow: hidden; > button { border: none; border-radius: 0; user-select: none; + background-color: var(--sci-color-background-primary); + color: var(--sci-color-text); &:active, &:focus { box-shadow: none; @@ -20,21 +31,13 @@ } } - > button:not(:first-child):not(.active) { - border-left: 1px solid var(--sci-color-primary-darker); - } - - > button.active + button { - border-left: none; + > button:not(:first-child) { + border-left: 1px solid var(--sci-color-border); } > button.active { - background-color: var(--sci-color-accent-lighter); - color: var(--sci-color-background); + background-color: var(--sci-color-accent); + color: var(--sci-color-accent-inverse); } } - - > button.menu-button { - color: var(--sci-color-background); - } } diff --git a/apps/workbench-testing-app/src/app/header/header.component.ts b/apps/workbench-testing-app/src/app/header/header.component.ts index 5cf90782e..b2d86bdf4 100644 --- a/apps/workbench-testing-app/src/app/header/header.component.ts +++ b/apps/workbench-testing-app/src/app/header/header.component.ts @@ -16,6 +16,10 @@ import {WorkbenchStartupQueryParams} from '../workbench/workbench-startup-query- import {Router} from '@angular/router'; import {MenuService} from '../menu/menu.service'; import {WorkbenchRouter, WorkbenchService} from '@scion/workbench'; +import {SciMaterialIconDirective} from '@scion/components.internal/material-icon'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {SciToggleButtonComponent} from '@scion/components.internal/toggle-button'; @Component({ selector: 'app-header', @@ -26,16 +30,21 @@ import {WorkbenchRouter, WorkbenchService} from '@scion/workbench'; imports: [ NgFor, AsyncPipe, + ReactiveFormsModule, + SciMaterialIconDirective, + SciToggleButtonComponent, ], }) export class HeaderComponent { protected readonly PerspectiveData = PerspectiveData; + protected readonly lightThemeActiveFormControl = new FormControl(true); constructor(private _router: Router, private _wbRouter: WorkbenchRouter, private _menuService: MenuService, protected workbenchService: WorkbenchService) { + this.installThemeSwitcher(); } protected async onPerspectiveActivate(id: string): Promise { @@ -146,4 +155,26 @@ export class HeaderComponent { href.searchParams.append(WorkbenchStartupQueryParams.STANDALONE_QUERY_PARAM, `${options.standalone}`); window.open(href); } + + protected onActivateLightTheme(): void { + this.lightThemeActiveFormControl.setValue(true); + } + + protected onActivateDarkTheme(): void { + this.lightThemeActiveFormControl.setValue(false); + } + + private installThemeSwitcher(): void { + this.workbenchService.theme$ + .pipe(takeUntilDestroyed()) + .subscribe(theme => { + this.lightThemeActiveFormControl.setValue(theme === 'scion-light', {emitEvent: false}); + }); + + this.lightThemeActiveFormControl.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(lightTheme => { + this.workbenchService.switchTheme(lightTheme ? 'scion-light' : 'scion-dark').then(); + }); + } } diff --git a/apps/workbench-testing-app/src/app/host-popup-page/host-popup-page.component.scss b/apps/workbench-testing-app/src/app/host-popup-page/host-popup-page.component.scss index f20958af4..460f8c83d 100644 --- a/apps/workbench-testing-app/src/app/host-popup-page/host-popup-page.component.scss +++ b/apps/workbench-testing-app/src/app/host-popup-page/host-popup-page.component.scss @@ -1,4 +1,4 @@ -@use '@scion/components.internal' as sci-ɵcomponents; +@use '@scion/components.internal/design' as sci-design; :host { display: flex; @@ -18,8 +18,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -69,7 +69,7 @@ } &.return-value input { - @include sci-ɵcomponents.theme-input-field(); + @include sci-design.style-input-field(); } } } diff --git a/apps/workbench-testing-app/src/app/inspect-message-box-provider/inspect-message-box.component.scss b/apps/workbench-testing-app/src/app/inspect-message-box-provider/inspect-message-box.component.scss index f13c5fd14..d3126fa62 100644 --- a/apps/workbench-testing-app/src/app/inspect-message-box-provider/inspect-message-box.component.scss +++ b/apps/workbench-testing-app/src/app/inspect-message-box-provider/inspect-message-box.component.scss @@ -3,8 +3,8 @@ grid-row-gap: 1em; > sci-form-field section.input { - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; max-height: 200px; diff --git a/apps/workbench-testing-app/src/app/inspect-notification-provider/inspect-notification.component.scss b/apps/workbench-testing-app/src/app/inspect-notification-provider/inspect-notification.component.scss index f13c5fd14..d3126fa62 100644 --- a/apps/workbench-testing-app/src/app/inspect-notification-provider/inspect-notification.component.scss +++ b/apps/workbench-testing-app/src/app/inspect-notification-provider/inspect-notification.component.scss @@ -3,8 +3,8 @@ grid-row-gap: 1em; > sci-form-field section.input { - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; max-height: 200px; diff --git a/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.html b/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.html index a62da2685..e608d70a1 100644 --- a/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.html +++ b/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.html @@ -13,7 +13,7 @@ - + Navigate diff --git a/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.scss b/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.scss index 7e2ee9ba9..05e8cc1f9 100644 --- a/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.scss +++ b/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.scss @@ -14,8 +14,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -32,9 +32,11 @@ > output.navigate-error { flex: none; - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } diff --git a/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.html b/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.html index 0ba4797d4..3271d2bc3 100644 --- a/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.html +++ b/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.html @@ -33,7 +33,7 @@ - + Navigate diff --git a/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.scss b/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.scss index 7e2ee9ba9..05e8cc1f9 100644 --- a/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.scss +++ b/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.scss @@ -14,8 +14,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -32,9 +32,11 @@ > output.navigate-error { flex: none; - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } diff --git a/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.html b/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.html index 2ee60ef79..2136a6d36 100644 --- a/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.html +++ b/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.html @@ -28,7 +28,7 @@ - + Navigate diff --git a/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.scss b/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.scss index 7e2ee9ba9..05e8cc1f9 100644 --- a/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.scss +++ b/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.scss @@ -14,8 +14,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -32,9 +32,11 @@ > output.navigate-error { flex: none; - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } diff --git a/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.html b/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.html index 533a7a7bd..7aaa11c47 100644 --- a/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.html +++ b/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.html @@ -44,7 +44,7 @@ - + Register diff --git a/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.scss b/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.scss index f50550e8f..242c1f2f0 100644 --- a/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.scss +++ b/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.scss @@ -14,8 +14,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -32,9 +32,11 @@ > output.register-error { flex: none; - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } diff --git a/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.html b/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.html index 89e035169..e8fe2fc01 100644 --- a/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.html +++ b/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.html @@ -28,7 +28,7 @@ - + Register diff --git a/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.scss b/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.scss index 6a738e7d8..48a512dc4 100644 --- a/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.scss +++ b/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.scss @@ -13,8 +13,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { diff --git a/apps/workbench-testing-app/src/app/menu/menu.component.html b/apps/workbench-testing-app/src/app/menu/menu.component.html index 0c477ad26..c9ecf51a4 100644 --- a/apps/workbench-testing-app/src/app/menu/menu.component.html +++ b/apps/workbench-testing-app/src/app/menu/menu.component.html @@ -7,6 +7,6 @@ {{menuItem.text}} - + diff --git a/apps/workbench-testing-app/src/app/menu/menu.component.scss b/apps/workbench-testing-app/src/app/menu/menu.component.scss index 0c6d2615b..8f48e4699 100644 --- a/apps/workbench-testing-app/src/app/menu/menu.component.scss +++ b/apps/workbench-testing-app/src/app/menu/menu.component.scss @@ -1,28 +1,42 @@ :host { display: flex; flex-direction: column; - border: 1px solid var(--sci-color-P400); - background-color: var(--sci-color-P50); - box-shadow: 8px 8px 9px -9px rgba(var(--sci-color-primary-rgb), .2); + border: 1px solid var(--sci-color-border); + background-color: var(--sci-color-background-elevation); + border-radius: var(--sci-corner); + gap: 1px; // space for top/bottom borders when hovering the menu item + overflow: hidden; > button.menu-item { all: unset; padding: .6em 1.5em; font-size: smaller; user-select: none; + position: relative; // positioning context for separator pseudo element &:hover { - background-color: rgba(var(--sci-color-primary-rgb), .0375); - box-shadow: 0 0 0 1px var(--sci-color-P400); + background-color: var(--sci-color-background-elevation-hover); + box-shadow: 0 0 0 1px var(--sci-color-border); } &.disabled { opacity: .5; pointer-events: none; } + + // separator + &:has(+ hr):after { + position: absolute; + left: 0; + right: 0; + bottom: -1px; + height: 1px; + background-color: var(--sci-color-border); + content: ''; + } } - > div.separator { - border-bottom: 1px solid var(--sci-color-P400); + > hr { + display: none; // remove from flex container to not render the gap between flex items } } diff --git a/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html b/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html index 0b29317cf..9407accac 100644 --- a/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html +++ b/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html @@ -67,7 +67,7 @@ - + Open message box diff --git a/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.scss b/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.scss index ddf1017b7..7c1c6d66b 100644 --- a/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.scss +++ b/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.scss @@ -13,23 +13,27 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; } } > output.close-action { - border: 1px solid var(--sci-color-accent); - background-color: var(--sci-color-A100); - border-radius: 3px; + border: 1px solid var(--sci-color-positive); + background-color: var(--sci-color-background-positive); + color: var(--sci-color-positive); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } > output.open-error { - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } diff --git a/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.html b/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.html index 116eed0d8..21817efd4 100644 --- a/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.html +++ b/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.html @@ -57,7 +57,7 @@ - + Show notification diff --git a/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.scss b/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.scss index d2b5fb4c9..28eba375e 100644 --- a/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.scss +++ b/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.scss @@ -13,8 +13,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; } } diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.html b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.html index 38f6fb9e9..622f11c47 100644 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.html +++ b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.html @@ -9,9 +9,9 @@ - add + add - clear + clear @@ -31,7 +31,7 @@ - remove + remove diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.ts b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.ts index c37149829..51ab2d63e 100644 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.ts +++ b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.ts @@ -16,13 +16,20 @@ import {noop} from 'rxjs'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; import {MAIN_AREA} from '@scion/workbench'; +import {SciMaterialIconDirective} from '@scion/components.internal/material-icon'; @Component({ selector: 'app-perspective-page-parts', templateUrl: './perspective-page-parts.component.html', styleUrls: ['./perspective-page-parts.component.scss'], standalone: true, - imports: [CommonModule, ReactiveFormsModule, SciFormFieldComponent, SciCheckboxComponent], + imports: [ + CommonModule, + ReactiveFormsModule, + SciFormFieldComponent, + SciCheckboxComponent, + SciMaterialIconDirective + ], providers: [ {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => PerspectivePagePartsComponent)}, {provide: NG_VALIDATORS, multi: true, useExisting: forwardRef(() => PerspectivePagePartsComponent)}, diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.html b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.html index a9dcb5429..2dd52047a 100644 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.html +++ b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.html @@ -9,9 +9,9 @@ - add + add - clear + clear @@ -26,7 +26,7 @@ - remove + remove diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.ts b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.ts index 6aaee79b7..b422fd118 100644 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.ts +++ b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.ts @@ -16,13 +16,20 @@ import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {PerspectivePagePartEntry} from '../perspective-page-parts/perspective-page-parts.component'; +import {SciMaterialIconDirective} from '@scion/components.internal/material-icon'; @Component({ selector: 'app-perspective-page-views', templateUrl: './perspective-page-views.component.html', styleUrls: ['./perspective-page-views.component.scss'], standalone: true, - imports: [CommonModule, ReactiveFormsModule, SciCheckboxComponent, SciFormFieldComponent], + imports: [ + CommonModule, + ReactiveFormsModule, + SciCheckboxComponent, + SciFormFieldComponent, + SciMaterialIconDirective, + ], providers: [ {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => PerspectivePageViewsComponent)}, {provide: NG_VALIDATORS, multi: true, useExisting: forwardRef(() => PerspectivePageViewsComponent)}, diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.html b/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.html index 7d23f1b7b..a8c88d53a 100644 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.html +++ b/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.html @@ -19,7 +19,7 @@ - + Register diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.scss b/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.scss index b9762d9e2..46a42ce96 100644 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.scss +++ b/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.scss @@ -15,8 +15,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -33,9 +33,11 @@ > output.register-error { flex: none; - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } diff --git a/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.html b/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.html index 0f502573d..d3b0efe13 100644 --- a/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.html +++ b/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.html @@ -115,7 +115,7 @@ - + Open popup diff --git a/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.scss b/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.scss index aaf96a6cf..f0937b6f6 100644 --- a/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.scss +++ b/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.scss @@ -1,4 +1,4 @@ -@use '@scion/components.internal' as sci-ɵcomponents; +@use '@scion/components.internal/design' as sci-design; :host { display: flex; @@ -15,8 +15,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; } @@ -26,7 +26,7 @@ } &.input input { - @include sci-ɵcomponents.theme-input-field(); + @include sci-design.style-input-field(); } } } @@ -36,16 +36,20 @@ } > output.return-value { - border: 1px solid var(--sci-color-accent); - background-color: var(--sci-color-A100); - border-radius: 3px; + border: 1px solid var(--sci-color-positive); + background-color: var(--sci-color-background-positive); + color: var(--sci-color-positive); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } > output.popup-error { - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } diff --git a/apps/workbench-testing-app/src/app/popup-page/popup-page.component.html b/apps/workbench-testing-app/src/app/popup-page/popup-page.component.html index e773cbc3c..85911ae4e 100644 --- a/apps/workbench-testing-app/src/app/popup-page/popup-page.component.html +++ b/apps/workbench-testing-app/src/app/popup-page/popup-page.component.html @@ -77,6 +77,6 @@ - Close + Close Close (with error) diff --git a/apps/workbench-testing-app/src/app/popup-page/popup-page.component.scss b/apps/workbench-testing-app/src/app/popup-page/popup-page.component.scss index 2f599ac4c..2b10daeb3 100644 --- a/apps/workbench-testing-app/src/app/popup-page/popup-page.component.scss +++ b/apps/workbench-testing-app/src/app/popup-page/popup-page.component.scss @@ -1,4 +1,4 @@ -@use '@scion/components.internal' as sci-ɵcomponents; +@use '@scion/components.internal/design' as sci-design; :host { display: flex; @@ -17,8 +17,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -38,7 +38,7 @@ } &.return-value input { - @include sci-ɵcomponents.theme-input-field(); + @include sci-design.style-input-field(); } } } diff --git a/apps/workbench-testing-app/src/app/router-page/router-page.component.html b/apps/workbench-testing-app/src/app/router-page/router-page.component.html index 724542e56..fa49e866e 100644 --- a/apps/workbench-testing-app/src/app/router-page/router-page.component.html +++ b/apps/workbench-testing-app/src/app/router-page/router-page.component.html @@ -65,7 +65,7 @@ - + Navigate via Router diff --git a/apps/workbench-testing-app/src/app/router-page/router-page.component.scss b/apps/workbench-testing-app/src/app/router-page/router-page.component.scss index adaa0e48f..440d8f0a0 100644 --- a/apps/workbench-testing-app/src/app/router-page/router-page.component.scss +++ b/apps/workbench-testing-app/src/app/router-page/router-page.component.scss @@ -11,8 +11,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -22,10 +22,12 @@ } > output.navigate-error { - border: 1px solid var(--sci-color-warn); - background-color: var(--sci-color-W100); - border-radius: 3px; + border: 1px solid var(--sci-color-negative); + background-color: var(--sci-color-background-negative); + color: var(--sci-color-negative); + border-radius: var(--sci-corner); padding: 1em; + font-family: monospace; } } } diff --git a/apps/workbench-testing-app/src/app/start-page/start-page.component.scss b/apps/workbench-testing-app/src/app/start-page/start-page.component.scss index caf547d7c..31eb85623 100644 --- a/apps/workbench-testing-app/src/app/start-page/start-page.component.scss +++ b/apps/workbench-testing-app/src/app/start-page/start-page.component.scss @@ -13,7 +13,6 @@ > span.about { flex: none; - color: var(--sci-color-accent-darker); white-space: pre-line; align-self: center; text-align: center; @@ -37,16 +36,16 @@ } > a { - background-color: var(--sci-color-accent-lighter); - border: 1px solid var(--sci-color-accent-darker); - border-radius: 2px; + background-color: var(--sci-color-accent); + border: 1px solid var(--sci-color-accent); + border-radius: var(--sci-corner); padding: 1em; display: flex; flex-direction: column; justify-content: center; align-items: center; text-decoration: none; - color: var(--sci-color-background); + color: var(--sci-color-accent-inverse); font-size: 24px; text-align: center; cursor: pointer; @@ -57,8 +56,8 @@ font-size: .5em; } - &.microfrontend { - background-color: #484848; + &.microfrontend.devtools { + background-color: var(--devtools-app-color); border-color: unset; } diff --git a/apps/workbench-testing-app/src/app/start-page/start-page.component.ts b/apps/workbench-testing-app/src/app/start-page/start-page.component.ts index 8f0eb1e67..a16d38b8c 100644 --- a/apps/workbench-testing-app/src/app/start-page/start-page.component.ts +++ b/apps/workbench-testing-app/src/app/start-page/start-page.component.ts @@ -8,9 +8,9 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, Optional, ViewChild} from '@angular/core'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Optional, ViewChild} from '@angular/core'; import {WorkbenchModuleConfig, WorkbenchRouteData, WorkbenchRouterLinkDirective, WorkbenchView} from '@scion/workbench'; -import {Capability, IntentClient, ManifestService, PlatformPropertyService} from '@scion/microfrontend-platform'; +import {Capability, IntentClient, ManifestService} from '@scion/microfrontend-platform'; import {Observable, of} from 'rxjs'; import {WorkbenchCapabilities, WorkbenchPopupService, WorkbenchRouter, WorkbenchViewCapability} from '@scion/workbench-client'; import {filterArray, sortArray} from '@scion/toolkit/operators'; @@ -63,8 +63,6 @@ export default class StartPageComponent { @Optional() private _workbenchPopupService: WorkbenchPopupService, // not available when starting the workbench standalone @Optional() private _intentClient: IntentClient, // not available when starting the workbench standalone @Optional() private _manifestService: ManifestService, // not available when starting the workbench standalone - @Optional() private _propertyService: PlatformPropertyService, // not available when starting the workbench standalone - private _host: ElementRef, private _formBuilder: NonNullableFormBuilder, router: Router, cd: ChangeDetectorRef, @@ -95,11 +93,6 @@ export default class StartPageComponent { ); } - // Set configured app colors as CSS variables. - if (workbenchModuleConfig.microfrontendPlatform) { - this.setAppColorCssVariables(); - } - this.markForCheckOnUrlChange(router, cd); this.installFilterFieldDisplayTextSynchronizer(); } @@ -147,21 +140,6 @@ export default class StartPageComponent { }); } - /** - * Sets configured app colors as CSS variables: - * - * --workbench-client-testing-app1-color - * --workbench-client-testing-app2-color - * - * Colors are defined in platform properties in {@link environment.microfrontendPlatformConfig}. - */ - private setAppColorCssVariables(): void { - this._manifestService.applications.forEach(app => { - const appColor = this._propertyService.get<{color: string} | null>(app.symbolicName, null)?.color; - appColor && this._host.nativeElement.style.setProperty(`--${app.symbolicName}-color`, appColor); - }); - } - /** * Returns `true` if in the context of the welcome page. */ diff --git a/apps/workbench-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.scss b/apps/workbench-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.scss index eccc81472..0d82832f7 100644 --- a/apps/workbench-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.scss +++ b/apps/workbench-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.scss @@ -8,8 +8,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; } } diff --git a/apps/workbench-testing-app/src/app/test-pages/navigation-test-page/navigation-test-page.component.ts b/apps/workbench-testing-app/src/app/test-pages/navigation-test-page/navigation-test-page.component.ts index 2426749cb..cd09e4c28 100644 --- a/apps/workbench-testing-app/src/app/test-pages/navigation-test-page/navigation-test-page.component.ts +++ b/apps/workbench-testing-app/src/app/test-pages/navigation-test-page/navigation-test-page.component.ts @@ -27,9 +27,6 @@ export class NavigationTestPageComponent { if (params.has('title')) { view.title = params.get('title'); } - if (params.has('heading')) { - view.heading = params.get('heading'); - } if (params.has('cssClass')) { view.cssClass = params.get('cssClass')!; } diff --git a/apps/workbench-testing-app/src/app/view-page/view-page.component.scss b/apps/workbench-testing-app/src/app/view-page/view-page.component.scss index 56f7c830d..e4b20cb59 100644 --- a/apps/workbench-testing-app/src/app/view-page/view-page.component.scss +++ b/apps/workbench-testing-app/src/app/view-page/view-page.component.scss @@ -1,4 +1,4 @@ -@use '@scion/components.internal' as sci-ɵcomponents; +@use '@scion/components.internal/design' as sci-design; :host { display: flex; @@ -10,8 +10,8 @@ display: flex; flex-direction: column; gap: .5em; - border: 1px solid var(--sci-color-P400); - border-radius: 5px; + border: 1px solid var(--sci-color-border); + border-radius: var(--sci-corner); padding: 1em; > header { @@ -26,7 +26,7 @@ } &.part-actions input { - @include sci-ɵcomponents.theme-input-field(); + @include sci-design.style-input-field(); } } } diff --git a/apps/workbench-testing-app/src/app/workbench/workbench.component.html b/apps/workbench-testing-app/src/app/workbench/workbench.component.html index 72d4b9241..f94840a65 100644 --- a/apps/workbench-testing-app/src/app/workbench/workbench.component.html +++ b/apps/workbench-testing-app/src/app/workbench/workbench.component.html @@ -1,6 +1,6 @@ - + add diff --git a/apps/workbench-testing-app/src/app/workbench/workbench.component.ts b/apps/workbench-testing-app/src/app/workbench/workbench.component.ts index ac4b3cc72..1dba914b4 100644 --- a/apps/workbench-testing-app/src/app/workbench/workbench.component.ts +++ b/apps/workbench-testing-app/src/app/workbench/workbench.component.ts @@ -16,6 +16,7 @@ import {combineLatest} from 'rxjs'; import {AsyncPipe, NgIf} from '@angular/common'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {WorkbenchModule, WorkbenchPart, WorkbenchRouter, WorkbenchService} from '@scion/workbench'; +import {SciMaterialIconDirective} from '@scion/components.internal/material-icon'; @Component({ selector: 'app-workbench', @@ -26,6 +27,7 @@ import {WorkbenchModule, WorkbenchPart, WorkbenchRouter, WorkbenchService} from NgIf, AsyncPipe, WorkbenchModule, + SciMaterialIconDirective, ], }) export class WorkbenchComponent implements OnDestroy { diff --git a/apps/workbench-testing-app/src/environments/environment.ci.ts b/apps/workbench-testing-app/src/environments/environment.ci.ts index fd7677ef6..dd1524222 100644 --- a/apps/workbench-testing-app/src/environments/environment.ci.ts +++ b/apps/workbench-testing-app/src/environments/environment.ci.ts @@ -23,14 +23,6 @@ const microfrontendPlatformConfig: MicrofrontendPlatformConfig = { {symbolicName: 'workbench-client-testing-app1', manifestUrl: 'http://localhost:4201/assets/manifest-app1.json', intentionRegisterApiDisabled: false}, {symbolicName: 'workbench-client-testing-app2', manifestUrl: 'http://localhost:4202/assets/manifest-app2.json', intentionRegisterApiDisabled: false}, ], - properties: { - 'workbench-client-testing-app1': { - color: '#314d8c', - }, - 'workbench-client-testing-app2': { - color: '#2c78f7', - }, - }, }; export const environment = { diff --git a/apps/workbench-testing-app/src/environments/environment.ts b/apps/workbench-testing-app/src/environments/environment.ts index 52d74b9fd..51e71928d 100644 --- a/apps/workbench-testing-app/src/environments/environment.ts +++ b/apps/workbench-testing-app/src/environments/environment.ts @@ -33,14 +33,6 @@ const microfrontendPlatformConfig: MicrofrontendPlatformConfig = { {symbolicName: 'workbench-client-testing-app2', manifestUrl: 'http://localhost:4202/assets/manifest-app2.json', intentionRegisterApiDisabled: false}, {symbolicName: 'devtools', manifestUrl: 'https://scion-microfrontend-platform-devtools-v1-0-0.vercel.app/assets/manifest.json', intentionCheckDisabled: true, scopeCheckDisabled: true}, ], - properties: { - 'workbench-client-testing-app1': { - color: '#314d8c', - }, - 'workbench-client-testing-app2': { - color: '#2c78f7', - }, - }, }; export const environment = { diff --git a/apps/workbench-testing-app/src/environments/environment.vercel.ts b/apps/workbench-testing-app/src/environments/environment.vercel.ts index 855f743cd..fd92f08f3 100644 --- a/apps/workbench-testing-app/src/environments/environment.vercel.ts +++ b/apps/workbench-testing-app/src/environments/environment.vercel.ts @@ -24,14 +24,6 @@ const microfrontendPlatformConfig: MicrofrontendPlatformConfig = { {symbolicName: 'workbench-client-testing-app2', manifestUrl: 'https://scion-workbench-client-testing-app2.vercel.app/assets/manifest-app2.json', intentionRegisterApiDisabled: false}, {symbolicName: 'devtools', manifestUrl: 'https://scion-microfrontend-platform-devtools-v1-0-0.vercel.app/assets/manifest.json', intentionCheckDisabled: true, scopeCheckDisabled: true}, ], - properties: { - 'workbench-client-testing-app1': { - color: '#314d8c', - }, - 'workbench-client-testing-app2': { - color: '#2c78f7', - }, - }, }; export const environment = { diff --git a/apps/workbench-testing-app/src/styles.scss b/apps/workbench-testing-app/src/styles.scss index 3feb877a9..a44515aec 100644 --- a/apps/workbench-testing-app/src/styles.scss +++ b/apps/workbench-testing-app/src/styles.scss @@ -1,45 +1,43 @@ @use '@scion/workbench'; -@use '@scion/components.internal/theme' with ( - $theme: ( - accent: ( - 50: #E8F5E9, - 100: #C8E6C9, - 200: #A5D6A7, - 300: #81C784, - 400: #66BB6A, - 500: #4CAF50, - 600: #43A047, - 700: #388E3C, - 800: #2E7D32, - 900: #1B5E20, - default: 800, - lighter: #60bf4cff, - darker: #327f1fff, - ), - ) - ); +@use '@scion/components.internal/design' as sci-design; -* { - box-sizing: border-box; -} +@import url('https://fonts.googleapis.com/css?family=Roboto:normal,bold,italic,bolditalic|Roboto+Mono'); +@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded'); html { + all: unset; font-size: 14px; // defines 1rem as 14px width: 100vw; height: 100vh; } body { + all: unset; font-family: Roboto, Arial, sans-serif; - color: var(--sci-color-foreground); - background-color: var(--sci-color-background); - margin: 0; + color: var(--sci-color-text); + background-color: var(--sci-color-background-primary); width: 100vw; height: 100vh; } -// TODO [#110] Move to theme. -input:disabled, select:disabled { - background-color: rgb(0, 0, 0); - opacity: .05; +* { + box-sizing: border-box; +} + +a { + @include sci-design.style-link(); +} + +button[class*="material-icons"], button[class*="material-symbols"] { + @include sci-design.style-mat-icon-button(); +} + +button:not([class*="material-icons"]):not([class*="material-symbols"]) { + @include sci-design.style-button(); +} + +:root { + --workbench-client-testing-app1-color: rgb(0, 49, 83); + --workbench-client-testing-app2-color: rgb(0, 112, 187); + --devtools-app-color: rgb(72, 72, 72); } diff --git a/docs/site/announcements.md b/docs/site/announcements.md index 8b24c811e..f10a41b05 100644 --- a/docs/site/announcements.md +++ b/docs/site/announcements.md @@ -7,8 +7,14 @@ On this page you will find the latest news about the development of the SCION Workbench. -- **2023-05: Support Perspectives**\ - A perspective is an arrangement of views around the main area. Multiple perspectives are supported. Different perspectives provide a different perspective on the application while sharing the main area. Only one perspective can be active at a time. +- **2023-10: Theming of SCION Workbench**\ + SCION Workbench has introduced design tokens for applications to control the look of the workbench. + +- **2023-10: Reworked Tab Design**\ + SCION Workbench has a new tab design for improved user experience and a modern and consistent look. + +- **2023-05: Introduction of Perspectives**\ + SCION Workbench now supports the definition of one or more view arrangements, referred to as perspectives. Perspectives can be switched. Perspectives share the same main area, if any. - **2023-05: Support Arranging Views Around the Main Area**\ The workbench has a main area and a peripheral area for placing views. The main area is the primary place for views to interact with the application. The peripheral area arranges views around the main area. Peripheral views can be used to provide entry points to the application, tools or context-sensitive assistance to support the user's workflow. diff --git a/docs/site/features.md b/docs/site/features.md index 4e2d492e4..e362ddb3a 100644 --- a/docs/site/features.md +++ b/docs/site/features.md @@ -15,8 +15,8 @@ This page gives you an overview of existing and planned workbench features. Deve |Feature|Category|Status|Note |-|-|-|-| |Workbench Layout|layout|[![][done]](#)|Layout for the flexible arrangement of views side-by-side or stacked, all personalizable by the user via drag & drop. -|Compact Layout|layout|[![][planned]](#)|Compact presentation of views around the main area, similar to activities known from Visual Studio Code or IntelliJ. -|Perspective|layout|[![][done]](#)|Arrangement of views around the main area. Multiple perspectives are supported. Different perspectives provide a different perspective on the application while sharing the main area. Only one perspective can be active at a time. [#305](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/305). +|Activity Layout|layout|[![][progress]](#)|Compact presentation of views around the main area, similar to activities known from Visual Studio Code or IntelliJ. +|Perspective|layout|[![][done]](#)|Multiple layouts, called perspectives, are supported. Perspectives can be switched with one perspective active at a time. Perspectives share the same main area, if any. [#305](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/305). |View|layout|[![][done]](#)|Visual component for displaying content stacked or side-by-side in the workbench layout. |Multi-Window|layout|[![][done]](#)|Views can be opened in new browser windows. |Part Actions|layout|[![][done]](#)|Actions that are displayed in the tabbar of a part. Actions can stick to a view, so they are only visible if the view is active. @@ -24,7 +24,7 @@ This page gives you an overview of existing and planned workbench features. Deve |Persistent Navigation|navigation|[![][done]](#)|The arrangement of the views is added to the browser URL or local storage, enabling persistent navigation. |Start Page|layout|[![][done]](#)|A start page can be used to display content when all views are closed. |Microfrontend Support|microfrontend|[![][done]](#)|Microfrontends can be opened in views. Embedded microfrontends can interact with the workbench using a framework-angostic workbench API. The documentation is still missing. [#304](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/304). -|Theming|customization|[![][planned]](#)|A custom theme can be applied to change the look of the workbench. [#110](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/110) +|Theming|customization|[![][done]](#)|An application can define a custom theme to change the default look of the SCION Workbench. [#110](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/110) |Responsive Design|layout|[![][planned]](#)|The workbench adapts its layout to the current display size and device. [#112](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/112) |Electron/Edge Webview 2|env|[![][planned]](#)|The workbench can be used in desktop applications built with [Electron](https://www.electronjs.org/) and/or [Microsoft Edge WebView2](https://docs.microsoft.com/en-us/microsoft-edge/webview2/) to support window arrangements. [#306](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/306) |Localization (l10n)|env|[![][planned]](#)|The workbench allows the localization of built-in texts such as texts in context menus and manifest entries. [#255](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/255) @@ -32,8 +32,9 @@ This page gives you an overview of existing and planned workbench features. Deve |Message Box|control|[![][done]](#)|The workbench allows displaying content in a message box. The message box can be either view or application modal. |Notification Ribbon|control|[![][done]](#)|The workbench allows showing content in notifications ribbons. Notifications slide in at the upper-right corner. Multiple notifications are displayed one below the other. |Popup|control|[![][done]](#)|The workbench allows displaying content in a popup overlay. +|Dialog|control|[![][progress]](#)|The workbench allows displaying content in a dialog overlay. |Developer guide|doc|[![][planned]](#)|Developer Guide describing the workbench layout, its conceptsm fundamental APIs and built-in microfrontend support. [#304](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/304) -|Viewtab|customization|[![][done]](#)|The built-in viewtab can be replaced with a custom viewtab implementation, e.g., to add additional functionality. +|Tab|customization|[![][done]](#)|The built-in viewtab can be replaced with a custom viewtab implementation, e.g., to add additional functionality. [done]: /docs/site/images/icon-done.svg [progress]: /docs/site/images/icon-in-progress.svg diff --git a/docs/site/howto/how-to-theme-workbench.md b/docs/site/howto/how-to-theme-workbench.md new file mode 100644 index 000000000..8c2983fbe --- /dev/null +++ b/docs/site/howto/how-to-theme-workbench.md @@ -0,0 +1,225 @@ + + +| SCION Workbench | [Projects Overview][menu-projects-overview] | [Changelog][menu-changelog] | [Contributing][menu-contributing] | [Sponsoring][menu-sponsoring] | +| --- | --- | --- | --- | --- | + +## [SCION Workbench][menu-home] > [How To Guides][menu-how-to] > Theming + +SCION Workbench provides a set of design tokens to enable consistent design of the workbench. Design tokens are provided by the `@scion/workbench` SCSS module. + +An application can define a custom theme to change the default look of the SCION Workbench. Multiple themes are supported. A theme is a collection of design tokens, defining specific design aspects such as colors, spacings, etc. A design token can have a different value per theme. + +An application typically loads the SCSS module `@scion/workbench` in the `styles.scss` file. + +```scss +@use '@scion/workbench'; +``` + +### Themes +SCION provides a light and a dark theme, `scion-light` and `scion-dark`. Custom themes can be passed to the module under the `$themes` map entry, replacing the built-in themes. A custom theme can define only a subset of the available design tokens, with unspecified tokens inherited from the built-in theme of the same color scheme. The color scheme of a theme is determined by the `color-scheme` token. + +```scss +@use '@scion/workbench' with ( + $themes: ( + dark: ( + color-scheme: dark, + --sci-color-gray-50: #1D1D1D, + --sci-color-gray-75: #262626, + --sci-color-gray-100: #323232, + --sci-color-gray-200: #3F3F3F, + --sci-color-gray-300: #545454, + --sci-color-gray-400: #707070, + --sci-color-gray-500: #909090, + --sci-color-gray-600: #B2B2B2, + --sci-color-gray-700: #D1D1D1, + --sci-color-gray-800: #EBEBEB, + --sci-color-gray-900: #FFFFFF, + --sci-color-accent: blueviolet, + --sci-workbench-tab-height: 3.5rem, + --sci-workbench-view-background-color: var(--sci-color-gray-100), + --sci-workbench-part-bar-background-color: var(--sci-color-gray-300), + ), + light: ( + color-scheme: light, + --sci-color-gray-50: #FFFFFF, + --sci-color-gray-75: #FDFDFD, + --sci-color-gray-100: #F8F8F8, + --sci-color-gray-200: #E6E6E6, + --sci-color-gray-300: #D5D5D5, + --sci-color-gray-400: #B1B1B1, + --sci-color-gray-500: #909090, + --sci-color-gray-600: #6D6D6D, + --sci-color-gray-700: #464646, + --sci-color-gray-800: #222222, + --sci-color-gray-900: #000000, + --sci-color-accent: blueviolet, + ), + ) +); +``` + +### Theme Selection +A theme is selected based on the user's OS color scheme preference, or selected manually using the `WorkbenchService`. + +```ts +inject(WorkbenchService).switchTheme('dark'); + +``` +The selected theme is stored in workbench storage and will be selected when loading the application the next time. + +### Design Tokens +SCION Workbench supports the following design tokens: + + + Static Color Tokens + + +Colors that have a fixed color value across all themes. + +[Static Color Tokens](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-toolkit/master/projects/scion/components/design/colors/_scion-static-colors.scss) + + + + + Named Color Tokens + + +Predefined set of named colors as palette of tints and shades. + +[Named Color Tokens (light theme)](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-toolkit/master/projects/scion/components/design/colors/_scion-light-colors.scss), [Named Color Tokens (dark theme)](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-toolkit/master/projects/scion/components/design/colors/_scion-dark-colors.scss) + + + + + Semantic Tokens + + +Tokens for a particular usage. + +[Semantic Tokens (light theme)](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-toolkit/master/projects/scion/components/design/themes/_scion-light-theme.scss), [Semantic Tokens (dark theme)](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-toolkit/master/projects/scion/components/design/themes/_scion-dark-theme.scss) + + + + + Workbench-specific Tokens + + +Tokens specific to the SCION Workbench. + +[Workbench-specific Tokens (light theme)](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-workbench/master/projects/scion/workbench/design/_workbench-light-theme-design-tokens.scss), [Workbench-specific Tokens (dark theme)](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-workbench/master/projects/scion/workbench/design/_workbench-dark-theme-design-tokens.scss) + + + +### Examples + +The following listings illustrate how to customize the look of the SCION Workbench. + + + + Change of Background Color + + +```scss +@use '@scion/workbench' with ( + $themes: ( + scion-dark: ( + --sci-workbench-view-background-color: var(--sci-color-background-primary), + --sci-workbench-view-peripheral-background-color: var(--sci-color-gray-75), + --sci-workbench-part-bar-background-color: rgb(144, 144, 144), + --sci-workbench-part-peripheral-bar-background-color: var(--sci-color-gray-100), + ), + scion-light: ( + --sci-workbench-view-background-color: var(--sci-color-background-primary), + --sci-workbench-view-peripheral-background-color: var(--sci-color-gray-100), + --sci-workbench-part-bar-background-color: var(--sci-color-gray-500), + --sci-workbench-part-peripheral-bar-background-color: var(--sci-color-gray-100), + ), + ) +); +``` + + + + Change of Tab Size + + +```scss +@use '@scion/workbench' with ( + $themes: ( + scion-dark: ( + --sci-workbench-tab-height: 3.5rem, + --sci-workbench-tab-max-width: 15rem, + ), + scion-light: ( + --sci-workbench-tab-height: 3.5rem, + --sci-workbench-tab-max-width: 15rem, + ), + ) +); +``` + + + + Change of Accent Color + + +```scss +@use '@scion/workbench' with ( + $themes: ( + scion-dark: ( + --sci-color-accent: blueviolet, + ), + scion-light: ( + --sci-color-accent: blueviolet, + ), + ) +); +``` + + + + Change of Tab Corner Radius + + +```scss +@use '@scion/workbench' with ( + $themes: ( + scion-dark: ( + --sci-workbench-tab-border-radius: 0, + ), + scion-light: ( + --sci-workbench-tab-border-radius: 0, + ), + ) +); +``` + + + + Change of Overall Corner Radius + + +```scss +@use '@scion/workbench' with ( + $themes: ( + scion-dark: ( + --sci-corner: 3px, + --sci-corner-small: 2px, + ), + scion-light: ( + --sci-corner: 3px, + --sci-corner-small: 2px, + ), + ) +); +``` + + + +[menu-how-to]: /docs/site/howto/how-to.md + +[menu-home]: /README.md +[menu-projects-overview]: /docs/site/projects-overview.md +[menu-changelog]: /docs/site/changelog.md +[menu-contributing]: /CONTRIBUTING.md +[menu-sponsoring]: /docs/site/sponsoring.md diff --git a/docs/site/howto/how-to.md b/docs/site/howto/how-to.md index b7f4e63b8..03195a04b 100644 --- a/docs/site/howto/how-to.md +++ b/docs/site/howto/how-to.md @@ -14,10 +14,10 @@ We are working on a comprehensive guide that explains the features and concepts #### Installation - [How to install the SCION Workbench](how-to-install-workbench.md) -#### Layout and Perspective -- [How to configure a start page](how-to-configure-start-page.md) +#### Layout - [How to define an initial layout](how-to-define-initial-layout.md) - [How to provide a perspective](how-to-provide-perspective.md) +- [How to configure a start page](how-to-configure-start-page.md) - [How to query, switch and reset perspectives](how-to-perspective.md) @@ -30,6 +30,9 @@ We are working on a comprehensive guide that explains the features and concepts - [How to prevent a view from being closed](how-to-prevent-view-closing.md) - [How to provide a part action](how-to-provide-part-action.md) +#### Theming +- [How to theme the SCION Workbench](how-to-theme-workbench.md) + #### Miscellaneous - [How to open a popup](how-to-open-popup.md) diff --git a/package-lock.json b/package-lock.json index 0042d8cf9..944d55cba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,10 @@ "@angular/platform-browser": "16.1.6", "@angular/platform-browser-dynamic": "16.1.6", "@angular/router": "16.1.6", - "@scion/components": "16.1.0", - "@scion/components.internal": "16.2.0", - "@scion/microfrontend-platform": "1.0.0", - "@scion/toolkit": "1.4.0", + "@scion/components": "16.2.0", + "@scion/components.internal": "16.3.0", + "@scion/microfrontend-platform": "1.1.0", + "@scion/toolkit": "1.4.1", "rxjs": "7.8.1", "tslib": "2.5.3", "zone.js": "0.13.0" @@ -4509,8 +4509,9 @@ } }, "node_modules/@scion/components": { - "version": "16.1.0", - "license": "EPL-2.0", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@scion/components/-/components-16.2.0.tgz", + "integrity": "sha512-T+qb2BO/DKcoloBfRAP+9CFWg35sAC3iOlRVsI9A7d249/ROSOAUjtpjDNlw4/8FPCVNFpgRZGyBWjOQpgGZmg==", "dependencies": { "tslib": "^2.5.0" }, @@ -4523,9 +4524,9 @@ } }, "node_modules/@scion/components.internal": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@scion/components.internal/-/components.internal-16.2.0.tgz", - "integrity": "sha512-Xb6SWdQdvL8+jrLx6xnsWCjhVEqt+LESlnWzoHiFiKfzc0au9rf8vSRLD3/41Qq3DXosFwOmmVoaEMr2++sDSg==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@scion/components.internal/-/components.internal-16.3.0.tgz", + "integrity": "sha512-MgMqXu0XCFEhQB+YmPZmo07uh8wXWQ/tTGW6SP3CFecOmi18ByQ7E16s1D0bmi4SGUivYTtPxDazD5qCdVlyqA==", "dependencies": { "tslib": "^2.5.0" }, @@ -4539,8 +4540,9 @@ } }, "node_modules/@scion/microfrontend-platform": { - "version": "1.0.0", - "license": "EPL-2.0", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@scion/microfrontend-platform/-/microfrontend-platform-1.1.0.tgz", + "integrity": "sha512-BIpOi9Dyrw/MgtbbX9rxlRhbHWQtWZpuQV9TtImoy0VTMD0sMF4qfeMIIUQig0b5iBxQ0ueispR77PMk0zKz2A==", "dependencies": { "tslib": "^2.3.0" }, @@ -4550,13 +4552,14 @@ } }, "node_modules/@scion/toolkit": { - "version": "1.4.0", - "license": "EPL-2.0", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@scion/toolkit/-/toolkit-1.4.1.tgz", + "integrity": "sha512-6rjvQoQPggGuMalrXWXnSCzW6EODIGO0XH1h7/KQDqDU/IyMBQoIbT2f/DW8Ff3ncsrLJsFOgrG9U/8oga5wYg==", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.5.0" }, "peerDependencies": { - "rxjs": "^7.5.0" + "rxjs": "^7.8.0" } }, "node_modules/@sigstore/bundle": { diff --git a/package.json b/package.json index bb33b016b..2d85d125c 100644 --- a/package.json +++ b/package.json @@ -73,10 +73,10 @@ "@angular/platform-browser": "16.1.6", "@angular/platform-browser-dynamic": "16.1.6", "@angular/router": "16.1.6", - "@scion/components": "16.1.0", - "@scion/components.internal": "16.2.0", - "@scion/microfrontend-platform": "1.0.0", - "@scion/toolkit": "1.4.0", + "@scion/components": "16.2.0", + "@scion/components.internal": "16.3.0", + "@scion/microfrontend-platform": "1.1.0", + "@scion/toolkit": "1.4.1", "rxjs": "7.8.1", "tslib": "2.5.3", "zone.js": "0.13.0" diff --git a/projects/scion/e2e-testing/src/@scion/components.internal/checkbox.po/checkbox.po.ts b/projects/scion/e2e-testing/src/@scion/components.internal/checkbox.po/checkbox.po.ts index b01d0724a..2daef3b9b 100644 --- a/projects/scion/e2e-testing/src/@scion/components.internal/checkbox.po/checkbox.po.ts +++ b/projects/scion/e2e-testing/src/@scion/components.internal/checkbox.po/checkbox.po.ts @@ -15,15 +15,16 @@ import {Locator} from '@playwright/test'; */ export class SciCheckboxPO { - private _locator: Locator; + private _inputLocator: Locator; - constructor(private _sciCheckboxLocator: Locator) { - this._locator = this._sciCheckboxLocator.locator('input[type="checkbox"]'); + constructor(private _locator: Locator) { + this._inputLocator = this._locator.locator('input[type="checkbox"]'); } public async toggle(check: boolean): Promise { const isChecked = await this.isChecked(); + // We cannot use `Locator.check` or `Locator.uncheck` because the checkbox is not visible. // Ensure the value of the checkbox to be `false` when it is unchecked. await this._locator.click(); await this._locator.click(); @@ -34,6 +35,6 @@ export class SciCheckboxPO { } public async isChecked(): Promise { - return this._locator.isChecked(); + return this._inputLocator.isChecked(); } } diff --git a/projects/scion/e2e-testing/src/app.po.ts b/projects/scion/e2e-testing/src/app.po.ts index 18e4bfe66..9d7547f85 100644 --- a/projects/scion/e2e-testing/src/app.po.ts +++ b/projects/scion/e2e-testing/src/app.po.ts @@ -295,6 +295,14 @@ export class AppPO { await newAppPO.waitUntilWorkbenchStarted(); return newAppPO; } + + /** + * Sets given design token on the workbench HTML element. + */ + public async setDesignToken(name: string, value: string): Promise { + const pageFunction = (workbenchElement: HTMLElement, token: {name: string; value: string}): void => workbenchElement.style.setProperty(token.name, token.value); + await this.workbenchLocator.evaluate(pageFunction, {name, value}); + } } /** diff --git a/projects/scion/e2e-testing/src/view-tab.po.ts b/projects/scion/e2e-testing/src/view-tab.po.ts index 45512352d..85c7746a9 100644 --- a/projects/scion/e2e-testing/src/view-tab.po.ts +++ b/projects/scion/e2e-testing/src/view-tab.po.ts @@ -23,6 +23,16 @@ export class ViewTabPO { */ public readonly part: PartPO; + /** + * Locates the title of the view tab. + */ + public readonly titleLocator = this._locator.locator('.e2e-title'); + + /** + * Locates the heading of the view tab. + */ + public readonly headingLocator = this._locator.locator('.e2e-heading'); + constructor(private readonly _locator: Locator, part: PartPO) { this.part = part; } @@ -55,15 +65,15 @@ export class ViewTabPO { } public getTitle(): Promise { - return waitUntilStable(() => this._locator.locator('.e2e-title').innerText()); + return waitUntilStable(() => this.titleLocator.innerText()); } public getHeading(): Promise { - return waitUntilStable(() => this._locator.locator('.e2e-heading').innerText()); + return waitUntilStable(() => this.headingLocator.innerText()); } public isDirty(): Promise { - return waitUntilStable(() => hasCssClass(this._locator, 'dirty')); + return waitUntilStable(() => hasCssClass(this._locator, 'e2e-dirty')); } public async isClosable(): Promise { diff --git a/projects/scion/e2e-testing/src/workbench-client/router.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/router.e2e-spec.ts index 336210c61..5ec4cebaa 100644 --- a/projects/scion/e2e-testing/src/workbench-client/router.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/router.e2e-spec.ts @@ -1025,6 +1025,7 @@ test.describe('Workbench Router', () => { test('should set view properties upon initial view tab navigation', async ({appPO, microfrontendNavigator}) => { await appPO.navigateTo({microfrontendSupport: true}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); // register testee view const registerCapabilityPagePO = await microfrontendNavigator.openInNewTab(RegisterWorkbenchCapabilityPagePO, 'app1'); @@ -1054,6 +1055,7 @@ test.describe('Workbench Router', () => { test('should set view properties upon initial view tab navigation when replacing an existing workbench view', async ({appPO, microfrontendNavigator, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: true}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); // register testee view const registerCapabilityPagePO = await microfrontendNavigator.openInNewTab(RegisterWorkbenchCapabilityPagePO, 'app1'); @@ -1088,6 +1090,7 @@ test.describe('Workbench Router', () => { test('should set view properties when navigating in the current view tab', async ({appPO, microfrontendNavigator}) => { await appPO.navigateTo({microfrontendSupport: true}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); // register testee-1 view const registerCapabilityPagePO = await microfrontendNavigator.openInNewTab(RegisterWorkbenchCapabilityPagePO, 'app1'); @@ -1147,6 +1150,7 @@ test.describe('Workbench Router', () => { test('should not set view properties when performing self navigation, e.g., when updating view params', async ({appPO, microfrontendNavigator}) => { await appPO.navigateTo({microfrontendSupport: true}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); // register testee-1 view const registerCapabilityPagePO = await microfrontendNavigator.openInNewTab(RegisterWorkbenchCapabilityPagePO, 'app1'); @@ -2165,6 +2169,7 @@ test.describe('Workbench Router', () => { test('should substitute named parameter in title/heading property of view capability', async ({appPO, microfrontendNavigator}) => { await appPO.navigateTo({microfrontendSupport: true}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); // register testee microfrontend const registerCapabilityPagePO = await microfrontendNavigator.openInNewTab(RegisterWorkbenchCapabilityPagePO, 'app1'); @@ -2197,6 +2202,7 @@ test.describe('Workbench Router', () => { test('should substitute multiple named parameters in title/heading property of view capability', async ({appPO, microfrontendNavigator}) => { await appPO.navigateTo({microfrontendSupport: true}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); // register testee microfrontend const registerCapabilityPagePO = await microfrontendNavigator.openInNewTab(RegisterWorkbenchCapabilityPagePO, 'app1'); diff --git a/projects/scion/e2e-testing/src/workbench-client/view-properties.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/view-properties.e2e-spec.ts index 285270862..c45e6f004 100644 --- a/projects/scion/e2e-testing/src/workbench-client/view-properties.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/view-properties.e2e-spec.ts @@ -59,6 +59,7 @@ test.describe('Workbench View Properties', () => { test('should set view heading via Observable in view constructor', async ({appPO, microfrontendNavigator}) => { await appPO.navigateTo({microfrontendSupport: true}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); // Register the test page as view. const registerCapabilityPagePO = await microfrontendNavigator.openInNewTab(RegisterWorkbenchCapabilityPagePO, 'app1'); diff --git a/projects/scion/e2e-testing/src/workbench-client/view.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/view.e2e-spec.ts index fae7f00dc..701c8e452 100644 --- a/projects/scion/e2e-testing/src/workbench-client/view.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/view.e2e-spec.ts @@ -166,6 +166,7 @@ test.describe('Workbench View', () => { test('should allow updating the viewtab heading', async ({appPO, microfrontendNavigator}) => { await appPO.navigateTo({microfrontendSupport: true}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const viewPagePO = await microfrontendNavigator.openInNewTab(ViewPagePO, 'app1'); const viewTabPO = viewPagePO.view.viewTab; diff --git a/projects/scion/e2e-testing/src/workbench/router.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/router.e2e-spec.ts index 60c218a83..c0806233f 100644 --- a/projects/scion/e2e-testing/src/workbench/router.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/router.e2e-spec.ts @@ -111,7 +111,7 @@ test.describe('Workbench Router', () => { // open test view 1 await routerPagePO.enterPath('test-pages/navigation-test-page'); - await routerPagePO.enterMatrixParams({cssClass: 'e2e-test-view-1', title: 'view-1-title', heading: 'view-1-heading'}); + await routerPagePO.enterMatrixParams({cssClass: 'e2e-test-view-1', title: 'view-1-title'}); await routerPagePO.enterTarget('blank'); await routerPagePO.clickNavigate(); @@ -119,7 +119,7 @@ test.describe('Workbench Router', () => { await routerPagePO.viewTabPO.click(); await routerPagePO.enterPath('test-pages/navigation-test-page'); await routerPagePO.enterTarget('blank'); - await routerPagePO.enterMatrixParams({cssClass: 'e2e-test-view-2', title: 'view-2-title', heading: 'view-2-heading'}); + await routerPagePO.enterMatrixParams({cssClass: 'e2e-test-view-2', title: 'view-2-title'}); await routerPagePO.clickNavigate(); // reload the application @@ -128,12 +128,10 @@ test.describe('Workbench Router', () => { const viewTab1 = appPO.view({cssClass: 'e2e-test-view-1'}).viewTab; await expect(await viewTab1.isActive()).toBe(false); await expect(await viewTab1.getTitle()).toEqual('view-1-title'); - await expect(await viewTab1.getHeading()).toEqual('view-1-heading'); const viewTab2 = appPO.view({cssClass: 'e2e-test-view-2'}).viewTab; await expect(await viewTab2.isActive()).toBe(true); await expect(await viewTab2.getTitle()).toEqual('view-2-title'); - await expect(await viewTab2.getHeading()).toEqual('view-2-heading'); await expect(await consoleLogs.get({severity: 'error'})).toEqual([]); }); diff --git a/projects/scion/e2e-testing/src/workbench/view-route-data.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/view-route-data.e2e-spec.ts index aad8b1535..f4651af4b 100644 --- a/projects/scion/e2e-testing/src/workbench/view-route-data.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/view-route-data.e2e-spec.ts @@ -23,6 +23,7 @@ test.describe('View Route Data', () => { test('should resolve view properties from route data of route "feature-a"', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPagePO = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPagePO.enterPath(`${basePath}/feature-a`); @@ -38,6 +39,7 @@ test.describe('View Route Data', () => { test('should resolve view properties from route data of route "feature-b"', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPagePO = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPagePO.enterPath(`${basePath}/feature-b`); @@ -54,6 +56,7 @@ test.describe('View Route Data', () => { test('should resolve view properties from route data of route "feature-c"', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPagePO = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPagePO.enterPath(`${basePath}/feature-c`); @@ -69,6 +72,7 @@ test.describe('View Route Data', () => { test('should resolve view properties from route data of route "feature-d"', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPagePO = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPagePO.enterPath(`${basePath}/feature-d`); @@ -90,6 +94,7 @@ test.describe('View Route Data', () => { test('should resolve view properties from route data of route "feature-a"', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPagePO = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPagePO.enterPath(`${basePath}/feature-a`); @@ -105,6 +110,7 @@ test.describe('View Route Data', () => { test('should resolve view properties from route data of route "feature-b"', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPagePO = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPagePO.enterPath(`${basePath}/feature-b`); @@ -121,6 +127,7 @@ test.describe('View Route Data', () => { test('should resolve view properties from route data of route "feature-c"', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPagePO = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPagePO.enterPath(`${basePath}/feature-c`); @@ -136,6 +143,7 @@ test.describe('View Route Data', () => { test('should resolve view properties from route data of route "feature-d"', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPagePO = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPagePO.enterPath(`${basePath}/feature-d`); diff --git a/projects/scion/e2e-testing/src/workbench/view.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/view.e2e-spec.ts index deaceecf2..073c0e963 100644 --- a/projects/scion/e2e-testing/src/workbench/view.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/view.e2e-spec.ts @@ -28,6 +28,8 @@ test.describe('Workbench View', () => { test('should allow updating the view tab heading', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); + const viewPagePO = await workbenchNavigator.openInNewTab(ViewPagePO); await viewPagePO.enterHeading('HEADING'); @@ -37,6 +39,33 @@ test.describe('Workbench View', () => { await expect(await viewPagePO.viewTabPO.getHeading()).toEqual('heading'); }); + test('should not display the view tab heading (by default)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + const viewPagePO = await workbenchNavigator.openInNewTab(ViewPagePO); + await viewPagePO.enterHeading('heading'); + await expect(viewPagePO.viewTabPO.headingLocator).not.toBeVisible(); + }); + + test('should not display the view tab heading if the tab height < 3.5rem', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.4rem'); + + const viewPagePO = await workbenchNavigator.openInNewTab(ViewPagePO); + await viewPagePO.enterHeading('heading'); + await expect(viewPagePO.viewTabPO.headingLocator).not.toBeVisible(); + }); + + test('should display the view tab heading if the tab height >= 3.5rem', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); + + const viewPagePO = await workbenchNavigator.openInNewTab(ViewPagePO); + await viewPagePO.enterHeading('heading'); + await expect(viewPagePO.viewTabPO.headingLocator).toBeVisible(); + await expect(await viewPagePO.viewTabPO.getHeading()).toEqual('heading'); + }); + test('should allow to mark the view dirty', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); const viewPagePO = await workbenchNavigator.openInNewTab(ViewPagePO); @@ -127,6 +156,8 @@ test.describe('Workbench View', () => { test('should not unset the heading when the navigation resolves to the same route, e.g., when updating matrix params or route params', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); + await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); + const routerPagePO = await workbenchNavigator.openInNewTab(RouterPagePO); const viewPagePO = await workbenchNavigator.openInNewTab(ViewPagePO); const viewTabPO = viewPagePO.viewTabPO; diff --git a/projects/scion/workbench-client/src/lib/theme/workbench-theme-monitor.ts b/projects/scion/workbench-client/src/lib/theme/workbench-theme-monitor.ts new file mode 100644 index 000000000..d572bf7f0 --- /dev/null +++ b/projects/scion/workbench-client/src/lib/theme/workbench-theme-monitor.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2018-2023 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Observable} from 'rxjs'; + +/** + * Enables an application to monitor the workbench theme. + */ +export abstract class WorkbenchThemeMonitor { + + /** + * Emits the name of the current workbench theme. + * + * Upon subscription, emits the name of the current theme, and then continuously emits when switching the theme. It never completes. + */ + public abstract readonly theme$: Observable; +} diff --git "a/projects/scion/workbench-client/src/lib/theme/\311\265workbench-theme-monitor.ts" "b/projects/scion/workbench-client/src/lib/theme/\311\265workbench-theme-monitor.ts" new file mode 100644 index 000000000..d5b443f30 --- /dev/null +++ "b/projects/scion/workbench-client/src/lib/theme/\311\265workbench-theme-monitor.ts" @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018-2023 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Observable} from 'rxjs'; +import {Beans} from '@scion/toolkit/bean-manager'; +import {ContextService} from '@scion/microfrontend-platform'; +import {WorkbenchThemeMonitor} from './workbench-theme-monitor'; + +/** + * @inheritDoc + */ +export class ɵWorkbenchThemeMonitor implements WorkbenchThemeMonitor { + + /** + * @inheritDoc + */ + public theme$: Observable = Beans.get(ContextService).observe$(ɵTHEME_CONTEXT_KEY); +} + +/** + * Context key to retrieve the theme for microfrontends embedded in a workbench router outlet. + * + * @docs-private Not public API, intended for internal use only. + * @ignore + * @see {@link ContextService} + */ +export const ɵTHEME_CONTEXT_KEY = 'ɵworkbench.theme'; diff --git a/projects/scion/workbench-client/src/lib/workbench-client.ts b/projects/scion/workbench-client/src/lib/workbench-client.ts index f218becf0..3617c0509 100644 --- a/projects/scion/workbench-client/src/lib/workbench-client.ts +++ b/projects/scion/workbench-client/src/lib/workbench-client.ts @@ -16,6 +16,8 @@ import {WorkbenchPopupService} from './popup/workbench-popup-service'; import {WorkbenchPopupInitializer} from './popup/workbench-popup-initializer'; import {WorkbenchMessageBoxService} from './message-box/workbench-message-box-service'; import {WorkbenchNotificationService} from './notification/workbench-notification-service'; +import {WorkbenchThemeMonitor} from './theme/workbench-theme-monitor'; +import {ɵWorkbenchThemeMonitor} from './theme/ɵworkbench-theme-monitor'; /** * **SCION Workbench Client provides core API for a web app to interact with SCION Workbench and other microfrontends.** @@ -105,6 +107,7 @@ export class WorkbenchClient { Beans.register(WorkbenchPopupService); Beans.register(WorkbenchMessageBoxService); Beans.register(WorkbenchNotificationService); + Beans.register(WorkbenchThemeMonitor, {useClass: ɵWorkbenchThemeMonitor}); Beans.registerInitializer({useClass: WorkbenchViewInitializer}); Beans.registerInitializer({useClass: WorkbenchPopupInitializer}); await MicrofrontendPlatformClient.connect(symbolicName, connectOptions); diff --git a/projects/scion/workbench-client/src/public-api.ts b/projects/scion/workbench-client/src/public-api.ts index 23bd15688..ec7403051 100644 --- a/projects/scion/workbench-client/src/public-api.ts +++ b/projects/scion/workbench-client/src/public-api.ts @@ -32,3 +32,5 @@ export {WorkbenchMessageBoxConfig} from './lib/message-box/workbench-message-box export {WorkbenchNotificationCapability} from './lib/notification/workbench-notification-capability'; export {WorkbenchNotificationService} from './lib/notification/workbench-notification-service'; export {WorkbenchNotificationConfig} from './lib/notification/workbench-notification.config'; +export {WorkbenchThemeMonitor} from './lib/theme/workbench-theme-monitor'; +export {ɵTHEME_CONTEXT_KEY} from './lib/theme/ɵworkbench-theme-monitor'; diff --git a/projects/scion/workbench/@scion/workbench/_index.scss b/projects/scion/workbench/@scion/workbench/_index.scss index 9d627fa52..b8602692b 100644 --- a/projects/scion/workbench/@scion/workbench/_index.scss +++ b/projects/scion/workbench/@scion/workbench/_index.scss @@ -1 +1 @@ -@forward '../../index'; +@forward '../../_index'; diff --git a/projects/scion/workbench/_index.scss b/projects/scion/workbench/_index.scss index 2156bea7d..1d7d6a5ec 100644 --- a/projects/scion/workbench/_index.scss +++ b/projects/scion/workbench/_index.scss @@ -1,44 +1,152 @@ /** - * Public SASS Module Entry Point for @scion/workbench - * =================================================== + * This SCSS module provides a set of design tokens to enable consistent design of the workbench. * - * This module must be loaded by the application to style the workbench. Typically, this module is loaded from `styles.scss` file, as follows: + * An application can define a custom theme to change the default look of the SCION Workbench. Multiple themes are supported. + * A theme is a collection of design tokens, defining specific design aspects such as colors, spacings, etc. A design token can + * have a different value per theme. + * + * An application typically loads this module in the `styles.scss` file. * * ```scss * @use '@scion/workbench'; * ``` * - * ## Theming - * By passing a config when loading this module, theme and styles of the workbench can be customized. + * ## Themes + * SCION Workbench provides a light and a dark theme, `scion-light` and `scion-dark`. Custom themes can be passed to the module under the `$themes` map entry, + * replacing the built-in themes. A custom theme can define only a subset of the available design tokens, with unspecified tokens inherited from the + * built-in theme of the same color scheme. The color scheme of a theme is determined by the `color-scheme` token. + * + * ```scss + * @use '@scion/workbench' with ( + * $themes: ( + * dark: ( + * color-scheme: dark, + * --sci-color-gray-50: #1D1D1D, + * --sci-color-gray-75: #262626, + * --sci-color-gray-100: #323232, + * --sci-color-gray-200: #3F3F3F, + * --sci-color-gray-300: #545454, + * --sci-color-gray-400: #707070, + * --sci-color-gray-500: #909090, + * --sci-color-gray-600: #B2B2B2, + * --sci-color-gray-700: #D1D1D1, + * --sci-color-gray-800: #EBEBEB, + * --sci-color-gray-900: #FFFFFF, + * --sci-color-accent: blueviolet, + * --sci-workbench-tab-height: 3.5rem, + * --sci-workbench-view-background-color: var(--sci-color-gray-100), + * --sci-workbench-part-bar-background-color: var(--sci-color-gray-300), + * ), + * light: ( + * color-scheme: light, + * --sci-color-gray-50: #FFFFFF, + * --sci-color-gray-75: #FDFDFD, + * --sci-color-gray-100: #F8F8F8, + * --sci-color-gray-200: #E6E6E6, + * --sci-color-gray-300: #D5D5D5, + * --sci-color-gray-400: #B1B1B1, + * --sci-color-gray-500: #909090, + * --sci-color-gray-600: #6D6D6D, + * --sci-color-gray-700: #464646, + * --sci-color-gray-800: #222222, + * --sci-color-gray-900: #000000, + * --sci-color-accent: blueviolet, + * --sci-workbench-tab-height: 3.5rem, + * ), + * ) + * ); + * ``` + * + * ### Theme Selection + * A theme is selected based on the user's OS color scheme preference, or selected manually using the `WorkbenchService`. + * + * ```ts + * inject(WorkbenchService).switchTheme('dark'); + * ``` + * The selected theme is stored in workbench storage and will be selected when loading the application the next time. + * + * ### SCION Design Tokens + * SCION Workbench supports the following design tokens: + * + * #### Static Color Tokens + * Colors that have a fixed color value across all themes. + * + * [Static Color Tokens](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-toolkit/master/projects/scion/components/design/colors/_scion-static-colors.scss) + * + * #### Named Color Tokens + * Predefined set of named colors as palette of tints and shades. + * + * [Named Color Tokens (light theme)](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-toolkit/master/projects/scion/components/design/colors/_scion-light-colors.scss) + * [Named Color Tokens (dark theme)](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-toolkit/master/projects/scion/components/design/colors/_scion-dark-colors.scss) + * + * #### Semantic Tokens + * Tokens for a particular usage. + * + * [Semantic Tokens (light theme)](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-toolkit/master/projects/scion/components/design/themes/_scion-light-theme.scss) + * [Semantic Tokens (dark theme)](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-toolkit/master/projects/scion/components/design/themes/_scion-dark-theme.scss) + * + * #### Workbench-specific Tokens + * Tokens specific to the SCION Workbench. + * + * [Workbench-specific Tokens (light theme)](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-workbench/master/projects/scion/workbench/design/_workbench-light-theme-design-tokens.scss) + * [Workbench-specific Tokens (dark theme)](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-workbench/master/projects/scion/workbench/design/_workbench-dark-theme-design-tokens.scss) + * + * + * ## Workbench Icon Font + * SCION Workbench requires some icons from the `scion-workbench-icons` icon font. + * + * Icons can be replaced with the [IcoMoon](https://icomoon.io) web application. Import the icon font definition from [scion-workbench-icons.json](https://raw.githubusercontent.com/SchweizerischeBundesbahnen/scion-workbench/master/resources/scion-workbench-icons/scion-workbench-icons.json). * - * #### Configure path to the workbench icon font - * The workbench requires some icons to be loaded. Directory and filename can be customized, as follows: + * Directory and filename of the icon font can be changed as follows: * * ```scss * @use '@scion/workbench' with ( - * $theme: ( - * icon-font: ( - * directory: 'path/to/font', // defaults to 'assets/fonts' if omitted - * filename: 'icons' // defaults to 'scion-workbench-icons' if omitted - * ) + * icon-font: ( + * directory: 'path/to/font', // defaults to 'assets/fonts' if omitted + * filename: 'icons' // defaults to 'scion-workbench-icons' if omitted * ) * ); * ``` * - * #### Caching of the workbench icon font - * By default, when loading icon files, the workbench appends a version query parameter to the HTTP request, enabling browser cache invalidation when icons change. - * If using a project-specific workbench icon font, you can configure a custom version when importing the workbench sass module. Increment this version each time - * you change the icon font, as follows: + * When loading the default icon font, the workbench appends a version query parameter to the HTTP request, enabling browser cache invalidation when icons change. + * If using a custom icon font, we recommend incrementing the version when modifying the icon font, as follows: * * ```scss * @use '@scion/workbench' with ( - * $theme: ( - * icon-font: ( - * filename: 'custom-workbench-icons', - * version: '1.0.0' - * ) + * icon-font: ( + * filename: 'custom-workbench-icons', + * version: '1.0.0' * ) * ); * ``` */ -@forward './theme'; + +@use 'sass:map'; +@use '@scion/components/design' as sci-design; +@use '@angular/cdk'; +@use './design/workbench-light-theme-design-tokens'; +@use './design/workbench-dark-theme-design-tokens'; +@use './design/workbench-icon-font' as workbench-icons; +@use './design/workbench-popup-global-styles' as workbench-popup; +@use './design/workbench-view-drag-image-global-styles' as workbench-view-drag-image; + +// Install workbench theme +$-built-in-themes: ( + scion-light: map.set(workbench-light-theme-design-tokens.$tokens, color-scheme, light), + scion-dark: map.set(workbench-dark-theme-design-tokens.$tokens, color-scheme, dark), +); +$themes: $-built-in-themes !default; +$themes: sci-design.ɵthemes-augment($themes, $-built-in-themes); +@use '@scion/components' with ($themes: $themes); + +// Install workbench icon font +$icon-font: null !default; +@include workbench-icons.install-icon-font($icon-font); + +// Install global workbench styles +@include workbench-popup.install-global-styles(); +@include workbench-view-drag-image.install-global-styles(); + +// Install Angular CDK styles +@include cdk.a11y-visually-hidden(); +@include cdk.overlay(); diff --git a/projects/scion/workbench/design/_workbench-constants.scss b/projects/scion/workbench/design/_workbench-constants.scss new file mode 100644 index 000000000..c7fd9f7fe --- /dev/null +++ b/projects/scion/workbench/design/_workbench-constants.scss @@ -0,0 +1,8 @@ +/** + * SASS constants used by the SCION Workbench. + */ + +/** + * Breakpoint for displaying the contents of the view tab on multiple lines. + */ +$viewtab-multiline-breakpoint: 3.5rem; diff --git a/projects/scion/workbench/design/_workbench-dark-theme-design-tokens.scss b/projects/scion/workbench/design/_workbench-dark-theme-design-tokens.scss new file mode 100644 index 000000000..ba21fd9a6 --- /dev/null +++ b/projects/scion/workbench/design/_workbench-dark-theme-design-tokens.scss @@ -0,0 +1,41 @@ +/** + * Design tokens specific to the SCION Workbench. + */ +$tokens: ( + --sci-workbench-view-background-color: var(--sci-color-background-primary), + --sci-workbench-view-peripheral-background-color: var(--sci-color-gray-75), + --sci-workbench-part-bar-padding-top: .25rem, + --sci-workbench-part-bar-padding-inline: .1rem, + --sci-workbench-part-bar-background-color: var(--sci-color-background-secondary), + --sci-workbench-part-bar-divider-color: var(--sci-color-border), + --sci-workbench-part-bar-actions-padding-inline: .25rem, + --sci-workbench-part-divider-color: var(--sci-color-border), + --sci-workbench-part-divider-color-hover: var(--sci-workbench-sashbox-splitter-background-color), + --sci-workbench-part-divider-size: 1px, + --sci-workbench-part-divider-size-hover: 9px, + --sci-workbench-part-divider-touch-target-size: 5px, + --sci-workbench-part-divider-opacity-active: .25, + --sci-workbench-part-divider-opacity-hover: .125, + --sci-workbench-part-dropzone-background-color: color-mix(in srgb, var(--sci-color-gray-900) 10%, transparent), + --sci-workbench-part-dropzone-border-style: none, + --sci-workbench-part-dropzone-border-color: none, + --sci-workbench-part-dropzone-border-radius: none, + --sci-workbench-part-peripheral-bar-background-color: var(--sci-color-gray-100), + --sci-workbench-part-active-tab-active-text-color: var(--sci-color-accent), + --sci-workbench-part-hidden-tab-count-size: x-small, + --sci-workbench-tab-height: 2.5rem, + --sci-workbench-tab-max-width: 15rem, + --sci-workbench-tab-text-color: var(--sci-color-text), + --sci-workbench-tab-border-color: var(--sci-color-border), + --sci-workbench-tab-border-radius: var(--sci-corner-small), + --sci-workbench-tab-padding-inline: .725em, + --sci-workbench-tab-hover-background-color: none, + --sci-workbench-tab-drag-border-color: var(--sci-color-accent), + --sci-workbench-tab-active-text-color: var(--sci-color-text), + --sci-workbench-notification-width: 350px, + --sci-workbench-notification-severity-indicator-size: 6px, + --sci-workbench-messagebox-max-width: 400px, + --sci-workbench-messagebox-severity-indicator-size: 6px, + --sci-workbench-contextmenu-width: 18rem, + --sci-throbber-color: var(--sci-color-accent), +); diff --git a/projects/scion/workbench/theme/_icons.scss b/projects/scion/workbench/design/_workbench-icon-font.scss similarity index 79% rename from projects/scion/workbench/theme/_icons.scss rename to projects/scion/workbench/design/_workbench-icon-font.scss index ea84814f6..7c89ce40c 100644 --- a/projects/scion/workbench/theme/_icons.scss +++ b/projects/scion/workbench/design/_workbench-icon-font.scss @@ -1,11 +1,5 @@ /** - * Copyright (c) 2018-2023 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 + * Installs icons used by the SCION Workbench. */ @use 'sass:map'; @@ -15,13 +9,13 @@ * This version is appended to the HTTP request as query parameter when fetching the icon font to support cache busting. * Update this version when adding, removing or changing icons. */ -$version: 2; +$-version: 2; -$default-icon-font-config: ( +$-default-icon-font-config: ( // Path should be "root relative" to support HTML base href. directory: 'assets/fonts', filename: 'scion-workbench-icons', - version: $version, + version: $-version, ); /** @@ -34,10 +28,11 @@ $default-icon-font-config: ( * close * ``` */ -@mixin icon-font($theme) { - $effective-config: map.merge($default-icon-font-config, if(map.has-key($theme, icon-font), map.get($theme, icon-font), ())); - $path: map.get($effective-config, directory) + '/' + map.get($effective-config, filename); - $version: map.get($effective-config, version); +@mixin install-icon-font($config) { + $config: if($config, $config, ()); + $config: map.merge($-default-icon-font-config, $config); + $path: map.get($config, directory) + '/' + map.get($config, filename); + $version: map.get($config, version); @font-face { font-family: 'scion-workbench-icons'; diff --git a/projects/scion/workbench/design/_workbench-light-theme-design-tokens.scss b/projects/scion/workbench/design/_workbench-light-theme-design-tokens.scss new file mode 100644 index 000000000..5ebb37a90 --- /dev/null +++ b/projects/scion/workbench/design/_workbench-light-theme-design-tokens.scss @@ -0,0 +1,40 @@ +/** + * Design tokens specific to the SCION Workbench. + */ +$tokens: ( + --sci-workbench-view-background-color: var(--sci-color-background-primary), + --sci-workbench-view-peripheral-background-color: var(--sci-workbench-view-background-color), + --sci-workbench-part-bar-padding-top: .25rem, + --sci-workbench-part-bar-padding-inline: .1rem, + --sci-workbench-part-bar-background-color: var(--sci-color-background-secondary), + --sci-workbench-part-bar-divider-color: var(--sci-color-border), + --sci-workbench-part-bar-actions-padding-inline: .25rem, + --sci-workbench-part-divider-color: var(--sci-color-border), + --sci-workbench-part-divider-color-hover: var(--sci-workbench-sashbox-splitter-background-color), + --sci-workbench-part-divider-size: 1px, + --sci-workbench-part-divider-size-hover: 9px, + --sci-workbench-part-divider-touch-target-size: 5px, + --sci-workbench-part-divider-opacity-active: .25, + --sci-workbench-part-divider-opacity-hover: .125, + --sci-workbench-part-dropzone-background-color: color-mix(in srgb, var(--sci-color-gray-900) 10%, transparent), + --sci-workbench-part-dropzone-border-style: none, + --sci-workbench-part-dropzone-border-color: none, + --sci-workbench-part-dropzone-border-radius: none, + --sci-workbench-part-peripheral-bar-background-color: color-mix(in srgb, var(--sci-color-gray-200) 50%, transparent), + --sci-workbench-part-active-tab-active-text-color: var(--sci-color-accent), + --sci-workbench-part-hidden-tab-count-size: x-small, + --sci-workbench-tab-height: 2.5rem, + --sci-workbench-tab-max-width: 15rem, + --sci-workbench-tab-text-color: var(--sci-color-text), + --sci-workbench-tab-border-color: var(--sci-color-border), + --sci-workbench-tab-border-radius: var(--sci-corner-small), + --sci-workbench-tab-padding-inline: .725em, + --sci-workbench-tab-hover-background-color: none, + --sci-workbench-tab-drag-border-color: var(--sci-color-accent), + --sci-workbench-tab-active-text-color: var(--sci-color-text), + --sci-workbench-notification-width: 350px, + --sci-workbench-notification-severity-indicator-size: 6px, + --sci-workbench-messagebox-max-width: 400px, + --sci-workbench-messagebox-severity-indicator-size: 6px, + --sci-workbench-contextmenu-width: 18rem, +); diff --git a/projects/scion/workbench/theme/_popup-theme.scss b/projects/scion/workbench/design/_workbench-popup-global-styles.scss similarity index 72% rename from projects/scion/workbench/theme/_popup-theme.scss rename to projects/scion/workbench/design/_workbench-popup-global-styles.scss index e12c325af..df4a01af5 100644 --- a/projects/scion/workbench/theme/_popup-theme.scss +++ b/projects/scion/workbench/design/_workbench-popup-global-styles.scss @@ -1,17 +1,19 @@ +/** + * Styles for rendering the overlay of the Workbench popup. + */ + $diamond-height: 8; -$border-color: rgb(174, 181, 189); -$background-color: rgb(255, 255, 255); -$popup-box-shadow: 3px 3px 20px -5px rgba(0, 0, 0, 0.5); /** * Provides SCION workbench popup styles. */ -@mixin popup($theme) { +@mixin install-global-styles() { .wb-popup { - background-color: $background-color; - border-radius: 2px; - box-shadow: $popup-box-shadow, 0 0 1px $border-color; - border: 1px solid $border-color; + background-color: var(--sci-color-background-elevation); + color: var(--sci-color-text); + border-radius: var(--sci-corner); + box-shadow: var(--sci-elevation) var(--sci-static-color-black); + border: 1px solid var(--sci-color-border); display: grid; //::before is used as diamond-border @@ -31,14 +33,14 @@ $popup-box-shadow: 3px 3px 20px -5px rgba(0, 0, 0, 0.5); bottom: -#{$diamond-height}px; left: calc(50% - #{$diamond-height}px); border-bottom-width: 0; - border-top-color: $border-color; + border-top-color: var(--sci-color-border); } &::after { bottom: -#{$diamond-height - 1}px; left: calc(50% - #{$diamond-height}px); border-bottom-width: 0; - border-top-color: $background-color; + border-top-color: var(--sci-color-background-elevation); } } @@ -49,14 +51,14 @@ $popup-box-shadow: 3px 3px 20px -5px rgba(0, 0, 0, 0.5); top: -#{$diamond-height}px; left: calc(50% - #{$diamond-height}px); border-top-width: 0; - border-bottom-color: $border-color; + border-bottom-color: var(--sci-color-border); } &::after { top: -#{$diamond-height - 1}px; left: calc(50% - #{$diamond-height}px); border-top-width: 0; - border-bottom-color: $background-color; + border-bottom-color: var(--sci-color-background-elevation); } } @@ -67,14 +69,14 @@ $popup-box-shadow: 3px 3px 20px -5px rgba(0, 0, 0, 0.5); left: -#{$diamond-height}px; top: calc(50% - #{$diamond-height}px); border-left-width: 0; - border-right-color: $border-color; + border-right-color: var(--sci-color-border); } &::after { left: -#{$diamond-height - 1}px; top: calc(50% - #{$diamond-height}px); border-left-width: 0; - border-right-color: $background-color; + border-right-color: var(--sci-color-background-elevation); } } @@ -85,14 +87,14 @@ $popup-box-shadow: 3px 3px 20px -5px rgba(0, 0, 0, 0.5); right: -#{$diamond-height}px; top: calc(50% - #{$diamond-height}px); border-right-width: 0; - border-left-color: $border-color; + border-left-color: var(--sci-color-border); } &::after { right: -#{$diamond-height - 1}px; top: calc(50% - #{$diamond-height}px); border-right-width: 0; - border-left-color: $background-color; + border-left-color: var(--sci-color-background-elevation); } } diff --git a/projects/scion/workbench/design/_workbench-view-drag-image-global-styles.scss b/projects/scion/workbench/design/_workbench-view-drag-image-global-styles.scss new file mode 100644 index 000000000..4106c079f --- /dev/null +++ b/projects/scion/workbench/design/_workbench-view-drag-image-global-styles.scss @@ -0,0 +1,14 @@ +/** + * Provides styles for view drag and drop. + */ +@mixin install-global-styles() { + .wb-view-tab-drag-image { + display: grid; + position: fixed; + user-select: none; + pointer-events: none; + container-type: size; + container-name: viewtab; + } +} + diff --git a/projects/scion/workbench/package.json b/projects/scion/workbench/package.json index a309f4adf..f2dc0a2b7 100644 --- a/projects/scion/workbench/package.json +++ b/projects/scion/workbench/package.json @@ -29,7 +29,7 @@ "@angular/animations": "^16.0.0", "@angular/forms": "^16.0.0", "@angular/router": "^16.0.0", - "@scion/components": "^16.0.0", + "@scion/components": "^16.2.0", "@scion/toolkit": "^1.4.0", "@scion/microfrontend-platform": "^1.0.0", "@scion/workbench-client": "^1.0.0-beta.18", diff --git a/projects/scion/workbench/src/lib/filter-field/filter-field.component.scss b/projects/scion/workbench/src/lib/filter-field/filter-field.component.scss index e4fb0a53d..3185be3a2 100644 --- a/projects/scion/workbench/src/lib/filter-field/filter-field.component.scss +++ b/projects/scion/workbench/src/lib/filter-field/filter-field.component.scss @@ -9,16 +9,12 @@ align-self: center; user-select: none; font-size: 1.5em; - opacity: .75; } > input { + all: unset; flex: auto; - border: none; - box-shadow: none; - outline: none; - padding: 0; - width: 0; // allows the input to shrink past UA minimal width + min-width: 0; // allows the input to shrink past UA minimal width } > button.clear { @@ -33,15 +29,13 @@ } } - &:focus-within:not(.disabled) { - > label.filter-icon { - opacity: 1; - } - } - - &.disabled, &.empty { + &:not(:focus-within):not(:hover), &:has(> input:disabled), &.empty { > button.clear { visibility: hidden; } } + + &:has(> input:disabled) > label.filter-icon { + color: var(--sci-color-text-subtlest); + } } diff --git a/projects/scion/workbench/src/lib/filter-field/filter-field.component.ts b/projects/scion/workbench/src/lib/filter-field/filter-field.component.ts index dfc66da0e..71d4d2983 100644 --- a/projects/scion/workbench/src/lib/filter-field/filter-field.component.ts +++ b/projects/scion/workbench/src/lib/filter-field/filter-field.component.ts @@ -50,7 +50,6 @@ export class FilterFieldComponent implements ControlValueAccessor, OnDestroy { @Input() public placeholder?: string | undefined; - @HostBinding('class.disabled') @Input() public set disabled(disabled: boolean | string | undefined | null) { coerceBooleanProperty(disabled) ? this.formControl.disable() : this.formControl.enable(); diff --git a/projects/scion/workbench/src/lib/layout/grid-element/grid-element.component.scss b/projects/scion/workbench/src/lib/layout/grid-element/grid-element.component.scss index d6bf2abb9..2a66b6ec1 100644 --- a/projects/scion/workbench/src/lib/layout/grid-element/grid-element.component.scss +++ b/projects/scion/workbench/src/lib/layout/grid-element/grid-element.component.scss @@ -1,5 +1,3 @@ -@use '../../../../theme/colors'; - :host { display: grid; @@ -19,11 +17,13 @@ z-index: auto; --sci-sashbox-gap: 0; - --sci-sashbox-splitter-touch-target-size: 5px; - --sci-sashbox-splitter-size_hover: 9px; - --sci-sashbox-splitter-size: 1px; - --sci-sashbox-splitter-bgcolor: #{colors.$part_sash-bgcolor}; - --sci-sashbox-splitter-opacity_active: .25; - --sci-sashbox-splitter-opacity_hover: .125; + --sci-sashbox-splitter-background-color: var(--sci-workbench-part-divider-color); + --sci-sashbox-splitter-background-color-hover: var(--sci-workbench-part-divider-color-hover); + --sci-sashbox-splitter-size: var(--sci-workbench-part-divider-size); + --sci-sashbox-splitter-size-hover: var(--sci-workbench-part-divider-size-hover); + --sci-sashbox-splitter-touch-target-size: var(--sci-workbench-part-divider-touch-target-size); + --sci-sashbox-splitter-border-radius: 0; + --sci-sashbox-splitter-opacity-active: var(--sci-workbench-part-divider-opacity-active); + --sci-sashbox-splitter-opacity-hover: var(--sci-workbench-part-divider-opacity-hover); } } diff --git a/projects/scion/workbench/src/lib/message-box/message-box.component.scss b/projects/scion/workbench/src/lib/message-box/message-box.component.scss index 51c3f7dc3..350f33ef7 100644 --- a/projects/scion/workbench/src/lib/message-box/message-box.component.scss +++ b/projects/scion/workbench/src/lib/message-box/message-box.component.scss @@ -1,53 +1,54 @@ -@use '../../../theme/colors'; -@use '../../../theme/styles'; +:host { + --ɵmessage-box-transform-translate-x: 0; + --ɵmessage-box-transform-translate-y: 0; + --ɵmessage-box-severity-color: 0; -$severity-border-size: 6px; -$border-radius: 4px; + display: block; + font-size: 1rem; + border-radius: var(--sci-corner); + color: var(--sci-color-text); + box-shadow: var(--sci-elevation) var(--sci-static-color-black); + background-color: var(--sci-color-background-elevation); + outline: none; + border-top: var(--sci-workbench-messagebox-severity-indicator-size) solid var(--ɵmessage-box-severity-color); + transform: translateX(calc(1px * var(--ɵmessage-box-transform-translate-x))) translateY(calc(1px * var(--ɵmessage-box-transform-translate-y))); -@mixin message-box-theme($color, $opacity: 1) { - border-top: $severity-border-size solid $color; + &.info { + --ɵmessage-box-severity-color: var(--sci-color-accent); + } - div.outline > div.button-bar > div.button-outline > button.action { - &:focus, &:active { - color: $color; - border-color: rgba($color, .8); - box-shadow: 0 0 8px 0 rgba($color, .25); - } + &.warn { + --ɵmessage-box-severity-color: var(--sci-color-notice); } -} -:host { - display: block; - font-size: 1rem; - border-radius: $border-radius; - color: colors.$messagebox-fgcolor; - box-shadow: styles.$popup-box-shadow; - background-color: colors.$messagebox-bgcolor; - outline: none; + &.error { + --ɵmessage-box-severity-color: var(--sci-color-negative); + } > header.move-handle { - $size: $severity-border-size + 5px; + $size: calc(var(--sci-workbench-messagebox-severity-indicator-size) + 5px); position: absolute; - top: -($size); - height: 2*$size; + top: calc(-1 * $size); + height: calc(2 * $size); width: 100%; } > div.outline { display: flex; flex-direction: column; - border: 1px solid colors.$messagebox-border-color; + border: 1px solid var(--sci-color-border); border-top: none; - border-bottom-left-radius: $border-radius; - border-bottom-right-radius: $border-radius; + border-bottom-left-radius: var(--sci-corner); + border-bottom-right-radius: var(--sci-corner); overflow: hidden; > div.body { flex: auto; padding: 1.5em; - max-width: styles.$messagebox-max-width; + max-width: var(--sci-workbench-messagebox-max-width); user-select: none; white-space: pre-line; + overflow-wrap: break-word; > header { font-weight: bold; @@ -60,60 +61,50 @@ $border-radius: 4px; height: 3em; display: flex; margin-top: 1em; + background-color: var(--sci-color-background-secondary); + color: var(--sci-color-text); + border-top: 1px solid var(--sci-color-border); > div.button-outline { flex: 1 1 0; display: grid; - background-color: rgb(250, 250, 250); - border-top: 1px solid colors.$messagebox-border-color; min-width: 7.5em; &:not(:first-child) { - border-left: 1px solid colors.$messagebox-border-color; + border-left: 1px solid var(--sci-color-border); } > button.action { - font-family: inherit; - color: rgba(51, 51, 51, .8); - background-color: transparent; + all: unset; margin: 2px; border: 1px solid transparent; - border-radius: 2px; - transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + border-radius: var(--sci-corner-small); + transition: border-color ease-in-out .15s; cursor: pointer; user-select: none; + text-align: center; &:focus, &:active { outline: none; + color: var(--ɵmessage-box-severity-color); + border-color: var(--ɵmessage-box-severity-color); } } } } } - &.info { - @include message-box-theme(colors.$info-color); - } - - &.warn { - @include message-box-theme(colors.$warn-color); - } - - &.error { - @include message-box-theme(colors.$error-color); - } - &.blinking { - animation-duration: .125s; + animation-duration: 50ms; animation-iteration-count: infinite; animation-name: blink-animation; @keyframes blink-animation { from { - box-shadow: styles.$popup-box-shadow; + transform: translateX(calc(calc(1px * var(--ɵmessage-box-transform-translate-x)) - 2px)) translateY(calc(calc(1px * var(--ɵmessage-box-transform-translate-y)) - 1px)); } to { - box-shadow: none; + transform: translateX(calc(calc(1px * var(--ɵmessage-box-transform-translate-x)) + 2px)) translateY(calc(calc(1px * var(--ɵmessage-box-transform-translate-y)) + 1px)); } } } diff --git a/projects/scion/workbench/src/lib/message-box/message-box.component.ts b/projects/scion/workbench/src/lib/message-box/message-box.component.ts index b9d37436a..8e9743fbd 100644 --- a/projects/scion/workbench/src/lib/message-box/message-box.component.ts +++ b/projects/scion/workbench/src/lib/message-box/message-box.component.ts @@ -44,16 +44,17 @@ import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; }) export class MessageBoxComponent implements OnInit { - private _accDeltaX = 0; - private _accDeltaY = 0; + @HostBinding('style.--ɵmessage-box-transform-translate-x') + protected _transformTranslateX = 0; + + @HostBinding('style.--ɵmessage-box-transform-translate-y') + protected _transformTranslateY = 0; + private _cancelBlinkTimer$ = new Subject(); private _activeActionButton: HTMLElement | undefined; public portal: ComponentPortal | undefined; - @HostBinding('style.transform') - public transform: string | undefined; - @HostBinding('class.blinking') public blinking = false; @@ -110,9 +111,8 @@ export class MessageBoxComponent implements OnInit { } public onMove(delta: MoveDelta): void { - this._accDeltaX += delta.deltaX; - this._accDeltaY += delta.deltaY; - this.transform = `translate(${this._accDeltaX}px, ${this._accDeltaY}px)`; + this._transformTranslateX += delta.deltaX; + this._transformTranslateY += delta.deltaY; } public onMoveEnd(): void { @@ -141,7 +141,7 @@ export class MessageBoxComponent implements OnInit { this.blinking = true; this._cd.markForCheck(); - timer(500) + timer(300) .pipe( takeUntil(this._cancelBlinkTimer$), takeUntilDestroyed(this._destroyRef), diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-popup/microfrontend-popup.component.scss b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-popup/microfrontend-popup.component.scss index ee47e34e8..de41ff53c 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-popup/microfrontend-popup.component.scss +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-popup/microfrontend-popup.component.scss @@ -1,3 +1,9 @@ :host { display: grid; + + // Prevent the microfrontend (iframe) from consuming pointer events during a workbench drag operation. Otherwise, dragging a view tab, + // moving a part splitter, or moving a message box over an iframe would corrupt the drag operation because `dragover` events are not reported. + &.workbench-drag > sci-router-outlet { + pointer-events: none; + } } diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-popup/microfrontend-popup.component.ts b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-popup/microfrontend-popup.component.ts index 95cce7fc7..7ac7ac2c4 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-popup/microfrontend-popup.component.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-popup/microfrontend-popup.component.ts @@ -8,13 +8,15 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {Component, CUSTOM_ELEMENTS_SCHEMA, DestroyRef, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {Component, CUSTOM_ELEMENTS_SCHEMA, DestroyRef, ElementRef, HostBinding, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {Application, ManifestService, MessageClient, OutletRouter, SciRouterOutletElement} from '@scion/microfrontend-platform'; import {Logger, LoggerNames} from '../../logging'; -import {WorkbenchPopupCapability, ɵPOPUP_CONTEXT, ɵPopupContext, ɵWorkbenchCommands, ɵWorkbenchPopupMessageHeaders} from '@scion/workbench-client'; +import {WorkbenchPopupCapability, ɵPOPUP_CONTEXT, ɵPopupContext, ɵTHEME_CONTEXT_KEY, ɵWorkbenchCommands, ɵWorkbenchPopupMessageHeaders} from '@scion/workbench-client'; import {Popup} from '../../popup/popup.config'; import {NgClass} from '@angular/common'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {WorkbenchLayoutService} from '../../layout/workbench-layout.service'; +import {WorkbenchService} from '../../workbench.service'; /** * Displays the microfrontend of a popup capability inside a workbench popup. @@ -33,6 +35,12 @@ export class MicrofrontendPopupComponent implements OnInit, OnDestroy { public popupCapability: WorkbenchPopupCapability; + /** + * Indicates whether a workbench drag operation is in progress, such as when dragging a view or moving a sash. + */ + @HostBinding('class.workbench-drag') + public isWorkbenchDrag = false; + @ViewChild('router_outlet', {static: true}) public routerOutletElement!: ElementRef; @@ -42,9 +50,12 @@ export class MicrofrontendPopupComponent implements OnInit, OnDestroy { private _manifestService: ManifestService, private _messageClient: MessageClient, private _destroyRef: DestroyRef, + private _workbenchLayoutService: WorkbenchLayoutService, + private _workbenchService: WorkbenchService, private _logger: Logger) { this._popupContext = this.popup.input!; this.popupCapability = this._popupContext.capability; + this.installWorkbenchDragDetector(); this._logger.debug(() => 'Constructing MicrofrontendPopupComponent.', LoggerNames.MICROFRONTEND); } @@ -79,6 +90,8 @@ export class MicrofrontendPopupComponent implements OnInit, OnDestroy { params: this._popupContext.params, pushStateToSessionHistoryStack: false, }).then(); + + this.installThemePropagator(); } public onFocusWithin(event: Event): void { @@ -108,6 +121,34 @@ export class MicrofrontendPopupComponent implements OnInit, OnDestroy { return this._popupContext.popupId; } + /** + * Sets the {@link isWorkbenchDrag} property when a workbench drag operation is detected, + * such as when dragging a view or moving a sash. + */ + private installWorkbenchDragDetector(): void { + this._workbenchLayoutService.dragging$ + .pipe(takeUntilDestroyed()) + .subscribe(event => { + this.isWorkbenchDrag = (event === 'start'); + }); + } + + /** + * Propagates the current workbench theme to the popup microfrontend via router outlet context. + */ + private installThemePropagator(): void { + this._workbenchService.theme$ + .pipe(takeUntilDestroyed(this._destroyRef)) + .subscribe(theme => { + if (theme) { + this.routerOutletElement.nativeElement.setContextValue(ɵTHEME_CONTEXT_KEY, theme); + } + else { + this.routerOutletElement.nativeElement.removeContextValue(ɵTHEME_CONTEXT_KEY); + } + }); + } + public ngOnDestroy(): void { this._outletRouter.navigate(null, {outlet: this.popupId}); } diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.html b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.html index d06203b88..6277303c9 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.html +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.html @@ -4,6 +4,7 @@ [attr.data-capabilityid]="viewCapability && viewCapability.metadata!.id" [attr.data-app]="viewCapability && viewCapability.metadata!.appSymbolicName" [ngClass]="viewCssClasses" class="e2e-view" + [class.workbench-drag]="isWorkbenchDrag" (focuswithin)="onFocusWithin($event)" [keystrokes]="keystrokesToBubble$ | async"> diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.scss b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.scss index ee47e34e8..966f233bf 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.scss +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.scss @@ -1,3 +1,9 @@ :host { display: grid; } + +// Prevent the microfrontend (iframe) from consuming pointer events during a workbench drag operation. Otherwise, dragging a view tab, +// moving a part splitter, or moving a message box over an iframe would corrupt the drag operation because `dragover` events are not reported. +sci-router-outlet.workbench-drag { + pointer-events: none; +} diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.ts b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.ts index bf655ee40..368a44203 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.ts @@ -13,7 +13,7 @@ import {ActivatedRoute, ActivatedRouteSnapshot, Params} from '@angular/router'; import {asapScheduler, combineLatest, EMPTY, firstValueFrom, Observable, of, OperatorFunction, Subject} from 'rxjs'; import {catchError, debounceTime, first, map, pairwise, startWith, switchMap, takeUntil} from 'rxjs/operators'; import {Application, ManifestService, mapToBody, MessageClient, MessageHeaders, OutletRouter, ResponseStatusCodes, SciRouterOutletElement, TopicMessage} from '@scion/microfrontend-platform'; -import {WorkbenchViewCapability, ɵMicrofrontendRouteParams, ɵVIEW_ID_CONTEXT_KEY, ɵViewParamsUpdateCommand, ɵWorkbenchCommands} from '@scion/workbench-client'; +import {WorkbenchViewCapability, ɵMicrofrontendRouteParams, ɵTHEME_CONTEXT_KEY, ɵVIEW_ID_CONTEXT_KEY, ɵViewParamsUpdateCommand, ɵWorkbenchCommands} from '@scion/workbench-client'; import {Arrays, Dictionaries, Maps} from '@scion/toolkit/util'; import {Logger, LoggerNames} from '../../logging'; import {WorkbenchViewPreDestroy} from '../../workbench.model'; @@ -32,6 +32,8 @@ import {Beans} from '@scion/toolkit/bean-manager'; import {AsyncPipe, NgClass} from '@angular/common'; import {ContentAsOverlayComponent} from '../../content-projection/content-as-overlay.component'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {WorkbenchLayoutService} from '../../layout/workbench-layout.service'; +import {WorkbenchService} from '../../workbench.service'; /** * Embeds the microfrontend of a view capability. @@ -65,6 +67,11 @@ export class MicrofrontendViewComponent implements OnInit, OnDestroy, WorkbenchV */ public keystrokesToBubble$: Observable; + /** + * Indicates whether a workbench drag operation is in progress, such as when dragging a view or moving a sash. + */ + public isWorkbenchDrag = false; + constructor(private _host: ElementRef, private _route: ActivatedRoute, private _view: ɵWorkbenchView, @@ -74,11 +81,14 @@ export class MicrofrontendViewComponent implements OnInit, OnDestroy, WorkbenchV private _destroyRef: DestroyRef, private _logger: Logger, private _viewContextMenuService: ViewMenuService, + private _workbenchLayoutService: WorkbenchLayoutService, private _workbenchRouter: WorkbenchRouter, + private _workbenchService: WorkbenchService, @Inject(IFRAME_HOST) protected iframeHostRef: ViewContainerReference) { this._logger.debug(() => `Constructing MicrofrontendViewComponent. [viewId=${this._view.id}]`, LoggerNames.MICROFRONTEND_ROUTING); this.keystrokesToBubble$ = combineLatest([this.viewContextMenuKeystrokes$(), of(this._universalKeystrokes)]) .pipe(map(keystrokes => new Array().concat(...keystrokes))); + this.installWorkbenchDragDetector(); } public ngOnInit(): void { @@ -107,6 +117,8 @@ export class MicrofrontendViewComponent implements OnInit, OnDestroy, WorkbenchV takeUntilDestroyed(this._destroyRef), ) .subscribe(); + + this.installThemePropagator(); } private async onNavigate(prevRouteSnapshot: ActivatedMicrofrontendRouteSnapshot | undefined, currRouteSnapshot: ActivatedMicrofrontendRouteSnapshot): Promise { @@ -295,6 +307,34 @@ export class MicrofrontendViewComponent implements OnInit, OnDestroy, WorkbenchV ); } + /** + * Sets the {@link isWorkbenchDrag} property when a workbench drag operation is detected, + * such as when dragging a view or moving a sash. + */ + private installWorkbenchDragDetector(): void { + this._workbenchLayoutService.dragging$ + .pipe(takeUntilDestroyed()) + .subscribe(event => { + this.isWorkbenchDrag = (event === 'start'); + }); + } + + /** + * Propagates the current workbench theme to the view microfrontend via router outlet context. + */ + private installThemePropagator(): void { + this._workbenchService.theme$ + .pipe(takeUntilDestroyed(this._destroyRef)) + .subscribe(theme => { + if (theme) { + this.routerOutletElement.nativeElement.setContextValue(ɵTHEME_CONTEXT_KEY, theme); + } + else { + this.routerOutletElement.nativeElement.removeContextValue(ɵTHEME_CONTEXT_KEY); + } + }); + } + public ngOnDestroy(): void { // Instruct the message broker to delete retained messages to free resources. this._messageClient.publish(ɵWorkbenchCommands.viewActiveTopic(this.viewId), undefined, {retain: true}).then(); diff --git a/projects/scion/workbench/src/lib/notification/notification-list.component.scss b/projects/scion/workbench/src/lib/notification/notification-list.component.scss index 23ce0f9c6..38b145da5 100644 --- a/projects/scion/workbench/src/lib/notification/notification-list.component.scss +++ b/projects/scion/workbench/src/lib/notification/notification-list.component.scss @@ -1,16 +1,17 @@ -@use '../../../theme/styles'; - :host { + $padding: .5em; display: flex; flex-flow: column wrap-reverse; // to wrap to previous column (right alignment) + gap: .5em; max-height: 100%; // constrain max-height to wrap to the previous column align-items: flex-start; // right aligned align-content: flex-start; pointer-events: none; + padding: $padding; > wb-notification { position: relative; // for slide-in notification pointer-events: auto; - margin: styles.$notification-margin; + max-width: calc(100% - #{2*$padding}); // fit notification to maximal viewport width } } diff --git a/projects/scion/workbench/src/lib/notification/notification-list.component.ts b/projects/scion/workbench/src/lib/notification/notification-list.component.ts index f98f681a9..e861180f7 100644 --- a/projects/scion/workbench/src/lib/notification/notification-list.component.ts +++ b/projects/scion/workbench/src/lib/notification/notification-list.component.ts @@ -61,9 +61,6 @@ export class NotificationListComponent { style({opacity: 0, left: '100%'}), animate('.3s ease-out', style({opacity: 1, left: 0})), ]), - transition(':leave', [ - animate('.3s ease-out', style({opacity: 0})), - ]), ]; } } diff --git a/projects/scion/workbench/src/lib/notification/notification.component.html b/projects/scion/workbench/src/lib/notification/notification.component.html index 078eb17a7..8205834f6 100644 --- a/projects/scion/workbench/src/lib/notification/notification.component.html +++ b/projects/scion/workbench/src/lib/notification/notification.component.html @@ -2,4 +2,4 @@ {{title}} -close +close diff --git a/projects/scion/workbench/src/lib/notification/notification.component.scss b/projects/scion/workbench/src/lib/notification/notification.component.scss index 02539937b..675f0b0d6 100644 --- a/projects/scion/workbench/src/lib/notification/notification.component.scss +++ b/projects/scion/workbench/src/lib/notification/notification.component.scss @@ -1,41 +1,36 @@ -@use '../../../theme/colors'; -@use '../../../theme/styles'; - -$severity-border-size: 6px; -$border-radius: 4px; - :host { display: grid; - background-color: colors.$notification-bgcolor; - color: colors.$notification-fgcolor; + background-color: var(--sci-color-background-elevation); + color: var(--sci-color-text); font-size: 1rem; - border-radius: $border-radius; - box-shadow: styles.$popup-box-shadow; - width: styles.$notification-width; - max-width: calc(100vw - #{2*styles.$notification-margin}); - border-left: $severity-border-size solid transparent; + border-radius: var(--sci-corner); + box-shadow: var(--sci-elevation) var(--sci-static-color-black); + width: var(--sci-workbench-notification-width); + border-left: var(--sci-workbench-notification-severity-indicator-size) solid transparent; position: relative; // positioning context &.info { - border-left-color: colors.$info-color; + border-left-color: var(--sci-color-accent); } &.warn { - border-left-color: colors.$warn-color; + border-left-color: var(--sci-color-notice); } &.error { - border-left-color: colors.$error-color; + border-left-color: var(--sci-color-negative); } > div.outline { - border: 1px solid colors.$notification-border-color; + border: 1px solid var(--sci-color-border); border-left: none; padding: 1em 1.5em; - border-top-right-radius: $border-radius; - border-bottom-right-radius: $border-radius; + border-top-right-radius: var(--sci-corner); + border-bottom-right-radius: var(--sci-corner); font-size: .9em; white-space: pre-line; + overflow-wrap: break-word; + overflow: hidden; > header { font-weight: bold; diff --git a/projects/scion/workbench/src/lib/notification/notification.component.ts b/projects/scion/workbench/src/lib/notification/notification.component.ts index 45b42d7fb..87f660a58 100644 --- a/projects/scion/workbench/src/lib/notification/notification.component.ts +++ b/projects/scion/workbench/src/lib/notification/notification.component.ts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, EventEmitter, Injector, Input, OnChanges, Output, SimpleChanges} from '@angular/core'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, EventEmitter, HostListener, Injector, Input, OnChanges, Output, SimpleChanges} from '@angular/core'; import {asapScheduler, EMPTY, Subject, timer} from 'rxjs'; import {switchMap, takeUntil} from 'rxjs/operators'; import {ɵNotification} from './ɵnotification'; @@ -62,6 +62,13 @@ export class NotificationComponent implements OnChanges { }); } + @HostListener('mousedown', ['$event']) + public onMousedown(event: MouseEvent): void { + if (event.buttons === AUXILARY_MOUSE_BUTTON) { + this.closeNotification.emit(); + } + } + public onClose(): void { this.closeNotification.emit(); } @@ -109,5 +116,9 @@ export class NotificationComponent implements OnChanges { this.closeNotification.emit(); }); } - } + +/** + * Indicates that the auxilary mouse button is pressed (usually the mouse wheel button or middle button). + */ +const AUXILARY_MOUSE_BUTTON = 4; diff --git a/projects/scion/workbench/src/lib/part/part-action-bar/part-action-bar.component.scss b/projects/scion/workbench/src/lib/part/part-action-bar/part-action-bar.component.scss index b5a949f8e..00f482aa8 100644 --- a/projects/scion/workbench/src/lib/part/part-action-bar/part-action-bar.component.scss +++ b/projects/scion/workbench/src/lib/part/part-action-bar/part-action-bar.component.scss @@ -11,12 +11,12 @@ align-items: center; &.start { - padding-left: .75em; + padding-left: var(--sci-workbench-part-bar-actions-padding-inline); place-content: flex-start; } &.end { - padding-right: .75em; + padding-right: var(--sci-workbench-part-bar-actions-padding-inline); place-content: flex-end; } diff --git a/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.html b/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.html index 003721779..dd4f6d7ae 100644 --- a/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.html +++ b/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.html @@ -1,19 +1,24 @@ - - + + + + - - - + + - + + + + + diff --git a/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.scss b/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.scss index 709b19431..6b478638a 100644 --- a/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.scss +++ b/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.scss @@ -1,45 +1,126 @@ -@use '../../../../theme/colors'; - :host { + --ɵpart-bar-indent-left: 0; + --ɵpart-bar-indent-right: 0; + --ɵpart-bar-drag-source-width: 0; + --ɵpart-bar-drag-image-placeholder-width: 0; + display: flex; - background-color: colors.$part_tabbar-bgcolor; - color: colors.$part_tab-fgcolor; - --drag-source-width: 0; - --drag-source-placeholder-width: 0; + color: var(--sci-workbench-tab-text-color); + background-color: var(--sci-workbench-part-bar-background-color); + // Set border-top instead of padding-top because the viewtab containment context would otherwise displace the tab content. + border-top: var(--sci-workbench-part-bar-padding-top) solid transparent; + background-clip: border-box; + height: var(--sci-workbench-tab-height); + box-sizing: content-box; + // Positioning context for the bottom border pseudo element + position: relative; + // Containment context for view tabs to query their height, enabling view tabs to apply styles based on their height. + container-name: viewtab; + container-type: size; + + wb-workbench:has(wb-main-area-layout) wb-part:not(.main-area) & { + background-color: var(--sci-workbench-part-peripheral-bar-background-color); + } > sci-viewport.tabbar { // Use 'flex: initial' instead of 'flex: auto' to not grow to absorb any extra free space in the partbar, // so that left-aligned actions will directly follow the viewtabs. flex: initial; - > div.viewport-client { + &::part(content) { display: flex; - flex-flow: row nowrap; + padding-left: var(--ɵpart-bar-indent-left); + padding-right: var(--ɵpart-bar-indent-right); + } - > wb-view-tab { - flex: none; + // Indent right is not set if the tabbar contains no "visible" tab. + &:not(.dragover):not(:has(wb-view-tab:not(.drag-source)))::part(content) { + padding-right: 0; + } + + > wb-view-tab { + flex: none; - &.drag-source { - display: none; - } + &.drag-source { + display: none; } + } + + > div.divider { + flex: none; + display: flex; + position: relative; // positioning context for the :after pseudo element, which actually renders the divider as an overlay + width: 0; // divider must not occupy space - > div.drag-image-placeholder { + &:after { flex: none; - width: var(--drag-source-placeholder-width); + align-self: center; + content: ''; + position: absolute; + width: 1px; + left: -1px; + height: 65%; + background-color: var(--sci-workbench-part-bar-divider-color); } + } - // Shift drop target and subsequent tabs to the right. - > wb-view-tab.drop-target, wb-view-tab.drop-target ~ :is(wb-view-tab, div.drag-image-placeholder) { - transform: translateX(var(--drag-source-width)); - } + > div.drag-image-placeholder { + flex: none; + width: var(--ɵpart-bar-drag-image-placeholder-width); + } + + // Shift drop target and subsequent tabs and dividers to the right. + > wb-view-tab.drop-target, wb-view-tab.drop-target ~ :is(wb-view-tab, div.divider), > div.divider:has(+ wb-view-tab.drop-target) { + transform: translateX(var(--ɵpart-bar-drag-source-width)); + } + + // Do not render divider following the active tab or drag source. + > wb-view-tab:is(.active, .drag-source) + div.divider { + display: none; + } + + // Do not render divider preceding the active tab, but only if not the drag source. + > div.divider:has(+ wb-view-tab.active:not(.drag-source)) { + display: none; } } + > wb-part-action-bar { + flex: auto; + } + + > wb-view-list-button { + flex: none; + } + + // Pseudo element for rendering the bottom border of the tabbar. + // Must precede the viewport for the active tab to overlay it. + &:before { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background-color: var(--sci-workbench-tab-border-color); + } + + > div.tab-corner-radius { + position: absolute; // out of document flow + visibility: hidden; // must not be `display:none` to have an effective size + width: var(--sci-workbench-tab-border-radius); + } + + > div.padding-inline { + position: absolute; // out of document flow + visibility: hidden; // must not be `display:none` to have an effective size + width: var(--sci-workbench-part-bar-padding-inline); + } + &:not(.calculating-max-viewport-width) { // Animate shifting tabs during drag and drop. &.drag-enter, &.drag-over, &.drag-leave { - wb-view-tab { + wb-view-tab, div.divider { transition: transform 175ms cubic-bezier(0, 0, 0.2, 1); } } @@ -51,12 +132,4 @@ } } } - - > wb-part-action-bar { - flex: auto; - } - - > wb-view-list-button { - flex: none; - } } diff --git a/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.ts b/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.ts index 3779bb245..6882bf120 100644 --- a/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.ts +++ b/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.ts @@ -17,15 +17,16 @@ import {ConstrainFn, ViewDragImageRect, ViewTabDragImageRenderer} from '../../vi import {ViewDragData, ViewDragService} from '../../view-dnd/view-drag.service'; import {getCssTranslation, setCssClass, setCssVariable, unsetCssClass, unsetCssVariable} from '../../common/dom.util'; import {ɵWorkbenchPart} from '../ɵworkbench-part.model'; -import {observeInside, subscribeInside} from '@scion/toolkit/operators'; +import {filterArray, mapArray, observeInside, subscribeInside} from '@scion/toolkit/operators'; import {SciViewportComponent} from '@scion/components/viewport'; import {WorkbenchRouter} from '../../routing/workbench-router.service'; import {ɵWorkbenchService} from '../../ɵworkbench.service'; import {SciDimensionModule} from '@scion/components/dimension'; -import {AsyncPipe, NgFor} from '@angular/common'; +import {AsyncPipe, NgFor, NgIf} from '@angular/common'; import {PartActionBarComponent} from '../part-action-bar/part-action-bar.component'; import {ViewListButtonComponent} from '../view-list-button/view-list-button.component'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {fromDimension$} from '@scion/toolkit/observable'; /** * Renders view tabs and actions of a {@link WorkbenchPart}. @@ -63,6 +64,7 @@ import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; ViewTabComponent, PartActionBarComponent, ViewListButtonComponent, + NgIf, ], }) export class PartBarComponent implements OnInit { @@ -73,9 +75,19 @@ export class PartBarComponent implements OnInit { private readonly _dragenter$ = new Subject(); private readonly _dragleave$ = new Subject(); private readonly _dragend$ = new Subject(); + public readonly dragover$: Observable; + + @ViewChild(SciViewportComponent, {static: true, read: ElementRef}) + private _viewportElement!: ElementRef; @ViewChild(SciViewportComponent, {static: true}) - private _viewport!: SciViewportComponent; + private _viewportComponent!: SciViewportComponent; + + @ViewChild('tab_corner_radius', {static: true}) + private _tabCornerRadiusElement!: ElementRef; + + @ViewChild('padding_inline', {static: true}) + private _paddingInlineElement!: ElementRef; @ViewChildren(ViewTabComponent) public set injectViewTabs(queryList: QueryList) { @@ -105,6 +117,14 @@ export class PartBarComponent implements OnInit { */ private _constrainFn: ConstrainFn | null = null; + /** + * Tabbar indents where to display the rounded bottom corners of the first and last view tab. + */ + private _tabbarIndent = { + left: 0, + right: 0, + }; + /** * Notifies when the drag animation has finished, i.e., tabs have transitioned to their final position. */ @@ -126,12 +146,15 @@ export class PartBarComponent implements OnInit { private _destroyRef: DestroyRef, private _zone: NgZone) { this._host = host.nativeElement; + this.dragover$ = this._viewDragService.tabbarDragOver$.pipe(map(partId => partId === this._part.id)); } public ngOnInit(): void { this.installActiveViewScroller(); this.installScrolledIntoViewUpdater(); this.installViewDragListener(); + this.installTabbarIndentSizeDetector(); + this.installViewportClientSizeDetector(); } @HostListener('dblclick', ['$event']) @@ -150,10 +173,6 @@ export class PartBarComponent implements OnInit { this._viewportChange$.next(); } - public onTabbarViewportClientDimensionChange(): void { - this._viewportChange$.next(); - } - public onScroll(): void { this._viewportChange$.next(); } @@ -172,14 +191,19 @@ export class PartBarComponent implements OnInit { const dragSourceViewTab = this._viewTabs[dragSourceIndex]; const dropTargetViewTab = this._viewTabs[dragSourceIndex + 1] ?? 'end'; + // Constrain function for snapping the drag image to the tabbar when dragging near it. + this._constrainFn = this.createDragImageConstrainFn(); + this._viewTabDragImageRenderer.setConstrainDragImageRectFn(this._constrainFn); + this._viewDragService.setTabbarDragover(this._part.id); + // When start dragging a view tab, we remove it from the tabbar by setting its `display` to `none` and render its drag image in `ViewTabDragImageRenderer` instead. // However, do not unset the `display` of the drag source during `dragstart`. Otherwise, the native drag operation would be corrupted in Chrome and Edge (but not in Firefox). animationFrameScheduler.schedule(() => { this.dragSourceViewTab = dragSourceViewTab; this.dropTargetViewTab = dropTargetViewTab; // Set drop target to avoid tab animation. setCssVariable(this._host, { - '--drag-source-width': `${dragData.viewTabWidth}px`, - '--drag-source-placeholder-width': `${dragData.viewTabWidth}px`, + '--ɵpart-bar-drag-source-width': `${dragData.viewTabWidth}px`, + '--ɵpart-bar-drag-image-placeholder-width': `${dragData.viewTabWidth}px`, }); }); @@ -212,12 +236,14 @@ export class PartBarComponent implements OnInit { */ private onTabbarDragEnter(event: DragEvent): void { this._dragenter$.next(); - this._viewDragService.notifyDragOverTabbar(true); + this._viewDragService.setTabbarDragover(this._part.id); this._dragData = this._viewDragService.viewDragData!; - // Inject constrain function into the drag image renderer to snap the drag image to the tabbar when dragging near it. - this._constrainFn = this.createDragImageConstrainFn(); - this._viewTabDragImageRenderer.setConstrainDragImageRectFn(this._constrainFn); + // Constrain function for snapping the drag image to the tabbar when dragging near it. + if (!this._constrainFn) { + this._constrainFn = this.createDragImageConstrainFn(); + this._viewTabDragImageRenderer.setConstrainDragImageRectFn(this._constrainFn); + } // Locate drop target to have a nice animation for the drag source placeholder. this.dropTargetViewTab = this.computeDropTarget(event); @@ -229,8 +255,8 @@ export class PartBarComponent implements OnInit { .subscribe({complete: () => unsetCssClass(this._host, 'drag-enter')}); setCssVariable(this._host, { - '--drag-source-width': `${this._dragData.viewTabWidth}px`, - '--drag-source-placeholder-width': `${this._dragData.viewTabWidth}px`, + '--ɵpart-bar-drag-source-width': `${this._dragData.viewTabWidth}px`, + '--ɵpart-bar-drag-image-placeholder-width': `${this._dragData.viewTabWidth}px`, }); } @@ -239,7 +265,6 @@ export class PartBarComponent implements OnInit { */ private onTabbarDragLeave(): void { this._dragleave$.next(); - this._viewDragService.notifyDragOverTabbar(false); // Set the CSS class 'drag-leave' to indicate leaving the tabbar. setCssClass(this._host, 'drag-leave'); @@ -265,7 +290,7 @@ export class PartBarComponent implements OnInit { if (this.isDragAnimationStable()) { this.dropTargetViewTab = this.computeDropTarget(event); // Synchronize the width of the drag source placeholder with the drag pointer position to move actions along with the pointer. - setCssVariable(this._host, {'--drag-source-placeholder-width': `${this.calculateDragSourcePlaceholderWidth(event)}px`}); + setCssVariable(this._host, {'--ɵpart-bar-drag-image-placeholder-width': `${this.calculateDragImagePlaceholderWidth(event)}px`}); } } @@ -274,7 +299,6 @@ export class PartBarComponent implements OnInit { */ private onTabbarDrop(): void { const dropIndex = this.dropTargetViewTab === 'end' ? undefined : this._viewTabs.indexOf(this.dropTargetViewTab!); - this._viewDragService.notifyDragOverTabbar(false); this._viewDragService.dispatchViewMoveEvent({ source: { appInstanceId: this._dragData!.appInstanceId, @@ -321,11 +345,12 @@ export class PartBarComponent implements OnInit { const hostLeft = this._host.getBoundingClientRect().left; const maxViewportWidth = this.calculateMaxViewportWidth(); return (rect: ViewDragImageRect): ViewDragImageRect => { + const viewportBoundingBox = this._viewportElement.nativeElement.getBoundingClientRect(); return new ViewDragImageRect({ - x: Math.min(Math.max(hostLeft, rect.x), hostLeft + maxViewportWidth - rect.width), - y: this._host.getBoundingClientRect().top, + x: Math.min(Math.max(hostLeft + this._tabbarIndent.left, rect.x), hostLeft + maxViewportWidth - rect.width - this._tabbarIndent.right), + y: viewportBoundingBox.top, width: rect.width, - height: this._host.offsetHeight, + height: viewportBoundingBox.height, // fit into tabbar, e.g., when dragging a tab into a tabbar of a different height }); }; } @@ -337,32 +362,32 @@ export class PartBarComponent implements OnInit { * from `dragover` and disable animations. */ private calculateMaxViewportWidth(): number { - const currentWidth = this._host.style.getPropertyValue('--drag-source-placeholder-width'); + const currentWidth = this._host.style.getPropertyValue('--ɵpart-bar-drag-image-placeholder-width'); // Disable animations during calculation and force the viewport to overflow. setCssClass(this._host, 'calculating-max-viewport-width'); - setCssVariable(this._host, {'--drag-source-placeholder-width': `${this._host.clientWidth}px`}); + setCssVariable(this._host, {'--ɵpart-bar-drag-image-placeholder-width': `${this._host.clientWidth}px`}); try { - return this._viewport.viewportElement.clientWidth; + return this._viewportElement.nativeElement.getBoundingClientRect().width; } finally { - setCssVariable(this._host, {'--drag-source-placeholder-width': currentWidth || null}); + setCssVariable(this._host, {'--ɵpart-bar-drag-image-placeholder-width': currentWidth || null}); unsetCssClass(this._host, 'calculating-max-viewport-width'); } } /** - * Calculates the width for the drag source placeholder. + * Calculates the width for the drag image placeholder. * * - When dragging over tabs, the width is equal to the width of the drag source. * - When dragging to the right of the last tab, the width of the placeholder is calculated as the distance between the * last tab and the position of the dragover event. Applying this width will resize the viewport, moving part actions along * with the dragover event. */ - private calculateDragSourcePlaceholderWidth(event: DragEvent): number { + private calculateDragImagePlaceholderWidth(event: DragEvent): number { const viewDragImageRect = this._viewTabDragImageRenderer.calculateDragImageRect(this._dragData!, event); const lastViewTab = this._viewTabs.filter(viewTab => viewTab !== this.dragSourceViewTab).at(-1); - const lastViewTabRight = lastViewTab?.host.getBoundingClientRect().right ?? this._host.getBoundingClientRect().left; + const lastViewTabRight = lastViewTab?.host.getBoundingClientRect().right ?? this._host.getBoundingClientRect().left + this._tabbarIndent.left; return Math.max(lastViewTabRight + viewDragImageRect.width, viewDragImageRect.right) - lastViewTabRight; } @@ -377,7 +402,7 @@ export class PartBarComponent implements OnInit { const viewTabs = this._viewTabs.filter(viewTab => viewTab !== this.dragSourceViewTab); const dropTargetIndex = this.dropTargetViewTab === 'end' ? viewTabs.length : viewTabs.indexOf(this.dropTargetViewTab!); - const dragSourceWidth = `${this._dragData!.viewTabWidth}`; + const dragSourceWidth = this._dragData!.viewTabWidth; return viewTabs.every((viewTab, i) => { const viewTabTranslateX = getCssTranslation(viewTab.host).translateX; @@ -386,7 +411,7 @@ export class PartBarComponent implements OnInit { return false; } // Expect drop target and tabs following the drop target to be shifted to the right. - if (i >= dropTargetIndex && viewTabTranslateX !== dragSourceWidth) { + if (i >= dropTargetIndex && Math.floor(Number(viewTabTranslateX)) !== Math.floor(dragSourceWidth)) { // compare floored numbers because `translateX` is not precise return false; } return true; @@ -404,7 +429,8 @@ export class PartBarComponent implements OnInit { this._constrainFn = null; unsetCssClass(this._host, 'drag-over'); - unsetCssVariable(this._host, '--drag-source-width', '--drag-source-placeholder-width'); + unsetCssVariable(this._host, '--ɵpart-bar-drag-source-width', '--ɵpart-bar-drag-image-placeholder-width'); + this._viewDragService.unsetTabbarDragover(this._part.id); } private get _viewTabs(): ViewTabComponent[] { @@ -422,7 +448,10 @@ export class PartBarComponent implements OnInit { ) .subscribe(() => { // There may be no active view in the tabbar, e.g., when dragging the last view out of the tabbar. - this._viewTabs.find(viewTab => viewTab.active)?.scrollIntoView(); + const activeViewTab = this._viewTabs.find(viewTab => viewTab.active); + if (activeViewTab && !this._viewportComponent.isElementInView(activeViewTab.host, 'full')) { + this._viewportComponent.scrollIntoView(activeViewTab.host); + } }); } @@ -432,11 +461,13 @@ export class PartBarComponent implements OnInit { private installScrolledIntoViewUpdater(): void { this._viewportChange$ .pipe( - mergeMap(() => from(this._viewTabs)), + map(() => this._viewTabs), + filterArray(viewTab => viewTab !== this.dragSourceViewTab), // skip drag source as always scrolled out of view + mergeMap(viewTabs => from(viewTabs)), takeUntilDestroyed(this._destroyRef), ) .subscribe(viewTab => { - viewTab.view.scrolledIntoView = (viewTab.isScrolledIntoView() || viewTab.isDragSource()); + viewTab.view.scrolledIntoView = this._viewportComponent.isElementInView(viewTab.host, 'full'); }); } @@ -460,4 +491,27 @@ export class PartBarComponent implements OnInit { } }); } + + private installViewportClientSizeDetector(): void { + fromDimension$(this._viewportComponent.viewportClientElement) + .pipe( + subscribeInside(fn => this._zone.run(fn)), + takeUntilDestroyed(this._destroyRef), + ) + .subscribe(() => this._viewportChange$.next()); + } + + private installTabbarIndentSizeDetector(): void { + combineLatest([fromDimension$(this._tabCornerRadiusElement.nativeElement), fromDimension$(this._paddingInlineElement.nativeElement)]) + .pipe( + subscribeInside(fn => this._zone.runOutsideAngular(fn)), + mapArray(dimension => dimension.clientWidth), + takeUntilDestroyed(this._destroyRef), + ) + .subscribe(([tabCornerRadius, paddingInline]) => { + this._tabbarIndent = {left: tabCornerRadius + paddingInline, right: tabCornerRadius + paddingInline}; + setCssVariable(this._host, {'--ɵpart-bar-indent-left': `${this._tabbarIndent.left}px`}); + setCssVariable(this._host, {'--ɵpart-bar-indent-right': `${this._tabbarIndent.right}px`}); + }); + } } diff --git a/projects/scion/workbench/src/lib/part/part.component.scss b/projects/scion/workbench/src/lib/part/part.component.scss index f124e53cb..f0777ecc4 100644 --- a/projects/scion/workbench/src/lib/part/part.component.scss +++ b/projects/scion/workbench/src/lib/part/part.component.scss @@ -1,5 +1,3 @@ -@use '../../../theme/styles'; - :host { display: flex; flex-direction: column; @@ -7,7 +5,6 @@ > wb-part-bar { flex: none; - height: styles.$viewtab-height; } > div.active-view { diff --git a/projects/scion/workbench/src/lib/part/part.component.ts b/projects/scion/workbench/src/lib/part/part.component.ts index 910065b06..0ac04c234 100644 --- a/projects/scion/workbench/src/lib/part/part.component.ts +++ b/projects/scion/workbench/src/lib/part/part.component.ts @@ -54,6 +54,11 @@ export class PartComponent implements OnInit, OnDestroy { return this.part.isInMainArea; } + @HostBinding('class.main-area') + public get isMainArea(): boolean { + return this.part.isInMainArea; + } + @HostBinding('class.active') public get isActive(): boolean { return this.part.active; diff --git a/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.component.html b/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.component.html index b8f6990b6..a2f5f19ff 100644 --- a/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.component.html +++ b/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.component.html @@ -1,4 +1,4 @@ - + {{menuItem.accelerator | wbFormatAccelerator}} - + + diff --git a/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.component.scss b/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.component.scss index f1eb69eeb..b98bbfe37 100644 --- a/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.component.scss +++ b/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.component.scss @@ -1,5 +1,3 @@ -@use '../../../../theme/colors'; - @mixin show-ellipsis-on-overflow { text-overflow: ellipsis; overflow: hidden; @@ -9,45 +7,55 @@ :host { display: flex; flex-direction: column; - width: 250px; - border: 1px solid colors.$viewlistitem-border-color; - background-color: colors.$part_tab-active-bgcolor; - box-shadow: 8px 8px 9px -9px rgba(0, 0, 0, 0.2); + gap: 1px; // space for top/bottom borders when hovering the menu item + width: var(--sci-workbench-contextmenu-width); + border: 1px solid var(--sci-color-border); + color: var(--sci-color-text); + background-color: var(--sci-color-background-elevation); + border-radius: var(--sci-corner); + overflow: hidden; - > div.menu-item-group { + > button.menu-item { + all: unset; display: flex; - flex-direction: column; + flex-flow: row nowrap; + padding: .6em 1.5em; + font-size: smaller; + position: relative; // positioning context for separator pseudo element + + > div.text { + flex: auto; + @include show-ellipsis-on-overflow; + } + + > div.accelerator { + flex: none; + margin-left: 1em; + } - &:not(:last-child) { - border-bottom: 1px solid colors.$viewlistitem-border-color; + &:hover { + background-color: var(--sci-color-background-elevation-hover); + box-shadow: 0 0 0 1px var(--sci-color-border); } - > button.menu-item { - all: unset; - display: flex; - flex-flow: row nowrap; - padding: .6em 1.5em; - font-size: smaller; - - > div.text { - flex: auto; - @include show-ellipsis-on-overflow; - } - - > div.accelerator { - flex: none; - margin-left: 1em; - } - - &:hover { - background-color: rgba(239, 239, 239, .5); - box-shadow: 0 0 0 1px colors.$viewlistitem-border-color; - } - - &:disabled { - opacity: .5; - pointer-events: none; - } + &:disabled { + opacity: .5; + pointer-events: none; } + + // separator + &:has(+ hr):after { + position: absolute; + left: 0; + right: 0; + bottom: -1px; + height: 1px; + background-color: var(--sci-color-border); + content: ''; + } + } + + > hr { + display: none; // remove from flex container to not render the gap between flex items } } diff --git a/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.component.ts b/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.component.ts index 810a14502..f000ea2ed 100644 --- a/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.component.ts +++ b/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.component.ts @@ -14,12 +14,12 @@ import {Observable, OperatorFunction} from 'rxjs'; import {map} from 'rxjs/operators'; import {WorkbenchMenuItem} from '../../workbench.model'; import {ɵWorkbenchView} from '../../view/ɵworkbench-view.model'; -import {AsyncPipe, KeyValuePipe, NgClass, NgFor} from '@angular/common'; +import {AsyncPipe, KeyValuePipe, NgClass, NgFor, NgIf} from '@angular/common'; import {PortalModule} from '@angular/cdk/portal'; import {WbFormatAcceleratorPipe} from './accelerator-format.pipe'; import {MapCoercePipe} from '../../common/map-coerce.pipe'; -declare type MenuItemGroups = Map; +type MenuItemGroups = Map; /** * Renders the menu items of a {@link WorkbenchView} grouped by their menu item group. @@ -30,6 +30,7 @@ declare type MenuItemGroups = Map; styleUrls: ['./view-menu.component.scss'], standalone: true, imports: [ + NgIf, NgFor, NgClass, AsyncPipe, diff --git a/projects/scion/workbench/src/lib/part/view-list-button/view-list-button.component.scss b/projects/scion/workbench/src/lib/part/view-list-button/view-list-button.component.scss index f94204a5b..c3e0ba498 100644 --- a/projects/scion/workbench/src/lib/part/view-list-button/view-list-button.component.scss +++ b/projects/scion/workbench/src/lib/part/view-list-button/view-list-button.component.scss @@ -12,7 +12,14 @@ } > span.count { + // @formatter:off - Disable formatting because IntelliJ does not support style container queries. + // Display tab count only if font size is not 0 (style container query) + @container style(--sci-workbench-part-hidden-tab-count-size: 0) { + display: none; + } + // @formatter:on + flex: none; - font-size: smaller; + font-size: var(--sci-workbench-part-hidden-tab-count-size); } } diff --git a/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.html b/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.html new file mode 100644 index 000000000..2cd41c221 --- /dev/null +++ b/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.html @@ -0,0 +1,9 @@ + + + + + + close + diff --git a/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.scss b/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.scss new file mode 100644 index 000000000..55b9b25da --- /dev/null +++ b/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.scss @@ -0,0 +1,48 @@ +:host { + display: flex; + align-items: center; + padding: .6em 1em; + position: relative; // positioning context for the active indicator + user-select: none; + cursor: pointer; + gap: 1em; + + &.active { + color: var(--sci-color-accent); + + &:before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background-color: var(--sci-color-accent); + } + } + + > div.content { + flex: auto; + display: grid; + position: relative; // positioning context for the portal + } + + > button.close { + all: unset; + flex: none; + cursor: pointer; + visibility: hidden; + padding: .125em; + border-radius: var(--sci-corner-small); + opacity: .75; + + &:hover { + opacity: 1; + background-color: var(--sci-color-background-elevation-hover); + } + } + + &:hover > button.close { + visibility: visible; + } +} diff --git a/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.ts b/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.ts new file mode 100644 index 000000000..594243a7f --- /dev/null +++ b/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2018-2023 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {ChangeDetectionStrategy, Component, HostBinding, Injector, Input} from '@angular/core'; +import {ComponentPortal, PortalModule} from '@angular/cdk/portal'; +import {ɵWorkbenchView} from '../../view/ɵworkbench-view.model'; +import {WorkbenchViewRegistry} from '../../view/workbench-view.registry'; +import {ViewTabContentComponent} from '../view-tab-content/view-tab-content.component'; +import {NgIf} from '@angular/common'; +import {WorkbenchModuleConfig} from '../../workbench-module-config'; +import {WorkbenchView} from '../../view/workbench-view.model'; +import {VIEW_TAB_RENDERING_CONTEXT, ViewTabRenderingContext} from '../../workbench.constants'; + +@Component({ + selector: 'wb-view-list-item', + templateUrl: './view-list-item.component.html', + styleUrls: ['./view-list-item.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + PortalModule, + NgIf, + ], +}) +export class ViewListItemComponent { + + public view!: ɵWorkbenchView; + public viewTabContentPortal!: ComponentPortal; + + @Input({required: true}) + public set viewId(viewId: string) { + this.view = this._viewRegistry.get(viewId); + this.viewTabContentPortal = this.createViewTabContentPortal(); + } + + constructor(private _viewRegistry: WorkbenchViewRegistry, + private _workbenchModuleConfig: WorkbenchModuleConfig, + private _injector: Injector) { + } + + @HostBinding('class.active') + public get active(): boolean { + return this.view.active; + } + + public onClose(): void { + this.view.close().then(); + } + + private createViewTabContentPortal(): ComponentPortal { + const componentType = this._workbenchModuleConfig.viewTabComponent || ViewTabContentComponent; + return new ComponentPortal(componentType, null, Injector.create({ + parent: this._injector, + providers: [ + {provide: WorkbenchView, useValue: this.view}, + {provide: VIEW_TAB_RENDERING_CONTEXT, useValue: 'list-item' satisfies ViewTabRenderingContext}, + ], + })); + } +} diff --git a/projects/scion/workbench/src/lib/part/view-list/view-list.component.html b/projects/scion/workbench/src/lib/part/view-list/view-list.component.html index f76a3080c..936b898dd 100644 --- a/projects/scion/workbench/src/lib/part/view-list/view-list.component.html +++ b/projects/scion/workbench/src/lib/part/view-list/view-list.component.html @@ -8,14 +8,14 @@ wbFilterByText$:filterFormControl:viewTextFn | async } as views"> - - - - - - - - - + + + + + diff --git a/projects/scion/workbench/src/lib/part/view-list/view-list.component.scss b/projects/scion/workbench/src/lib/part/view-list/view-list.component.scss index fe1d63fa9..76791c4d6 100644 --- a/projects/scion/workbench/src/lib/part/view-list/view-list.component.scss +++ b/projects/scion/workbench/src/lib/part/view-list/view-list.component.scss @@ -1,13 +1,13 @@ -@use '../../../../theme/colors'; -@use '../../../../theme/styles'; - :host { display: flex; flex-direction: column; gap: .75em; - width: styles.$viewtab-max-width + 75px; - border: 1px solid colors.$viewlistitem-border-color; - background-color: colors.$part_tab-active-bgcolor; + width: var(--sci-workbench-contextmenu-width); + border: 1px solid var(--sci-color-border); + color: var(--sci-color-text); + background-color: var(--sci-color-background-elevation); + border-radius: var(--sci-corner); + overflow: hidden; &.south { border-top: none; @@ -22,20 +22,35 @@ flex: auto; max-height: 350px; - > ul { - all: unset; - list-style: none; + &::part(content) { display: flex; flex-direction: column; - padding-top: 1px; + gap: 1px; // space for top/bottom borders when hovering the menu item + padding-top: 1px; // to visualize top border on hover + } + + > wb-view-list-item { + position: relative; // positioning context for the separator - > li { - all: unset; + &:hover { + background-color: var(--sci-color-background-elevation-hover); + box-shadow: 0 0 0 1px var(--sci-color-border); + } - &.separator { - border-top: 1px solid colors.$viewlistitem-border-color; - } + // separator + &:has(+ hr):after { + position: absolute; + left: 0; + right: 0; + bottom: -1px; + height: 1px; + background-color: var(--sci-color-border); + content: ''; } } + + > hr { + display: none; // remove from flex container to not render the gap between flex items + } } } diff --git a/projects/scion/workbench/src/lib/part/view-list/view-list.component.ts b/projects/scion/workbench/src/lib/part/view-list/view-list.component.ts index 775f6f10a..53e960894 100644 --- a/projects/scion/workbench/src/lib/part/view-list/view-list.component.ts +++ b/projects/scion/workbench/src/lib/part/view-list/view-list.component.ts @@ -22,7 +22,7 @@ import {AsyncPipe, NgFor, NgIf} from '@angular/common'; import {FilterByPredicatePipe} from '../../common/filter-by-predicate.pipe'; import {FilterByTextPipe} from '../../common/filter-by-text.pipe'; import {SciViewportComponent} from '@scion/components/viewport'; -import {ViewTabComponent} from '../view-tab/view-tab.component'; +import {ViewListItemComponent} from '../view-list-item/view-list-item.component'; /** * Reference to inputs of {@link ViewListComponent}. @@ -45,7 +45,7 @@ export const ViewListComponentInputs = { FilterFieldComponent, FilterByPredicatePipe, FilterByTextPipe, - ViewTabComponent, + ViewListItemComponent, SciViewportComponent, ], }) @@ -84,8 +84,8 @@ export class ViewListComponent implements OnInit { this._filterFieldComponent.focus(); } - public onActivateView(): void { - // The view is activated in 'wb-view-tab' component. + public onActivateView(view: WorkbenchView): void { + view.activate().then(); this._overlayRef.dispose(); } diff --git a/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.html b/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.html index 044b33577..99561ec7a 100644 --- a/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.html +++ b/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.html @@ -1,15 +1,8 @@ - + dirty {{view.title}} - + - + {{view.heading}} - - - - close - + diff --git a/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.scss b/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.scss index ed1ff8fd9..b39bcbf50 100644 --- a/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.scss +++ b/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.scss @@ -1,3 +1,5 @@ +@use '../../../../design/workbench-constants'; + @mixin show-ellipsis-on-overflow { text-overflow: ellipsis; overflow: hidden; @@ -5,16 +7,12 @@ } :host { - display: grid; - grid-template-columns: 1fr; - grid-template-rows: 1.5em auto; - padding: .5em 1em; - - &:not(.has-heading) { - grid-template-rows: 1fr; - } + display: flex; + flex-direction: column; + gap: .25em; + overflow: hidden; // to have ellipsis on overflow - > div.title { + > span.title { @include show-ellipsis-on-overflow; font-weight: 400; font-size: 1em; @@ -24,94 +22,23 @@ } } - > div.heading { + > span.heading { @include show-ellipsis-on-overflow; font-size: .9em; font-weight: 300; } - > button.close { - all: unset; - cursor: pointer; - visibility: hidden; - opacity: .75; - - &:hover { - opacity: 1; - } - } - - &.active:not(.blocked):not([context="tabbar-dropdown"]), &:hover:not(.blocked) { - > button.close { - visibility: visible; - } - } - - &.active[context="tabbar-dropdown"] { - > div.title, div.heading { - font-weight: bold; - } - } - - &:not(.scrolled-into-view)[context="tabbar-dropdown"] { - > div.title, div.heading { - opacity: .85; - font-style: italic; + &[context="tab"], &[context="drag-image"] { + span.title { + margin-right: 1em; } } - &[context="tabbar"], &[context="drag-image"] { - // TODO [Angular 17] Add direct child selector when resolved issue https://github.com/angular/angular/issues/49100 - //> div.title { - div.title { - grid-column: 1/2; - grid-row: 1/2; - } - - // TODO [Angular 17] Add direct child selector when resolved issue https://github.com/angular/angular/issues/49100 - //> div.heading { - div.heading { - grid-column: 1/3; - grid-row: 2/3; - } - - // TODO [Angular 17] Add direct child selector when resolved issue https://github.com/angular/angular/issues/49100 - //> button.close { - button.close { - grid-column: 2/3; - grid-row: 1/-1; - justify-self: end; - align-self: start; - position: relative; - top: -.25em; - right: -.75em; - } - } - - &[context="tabbar-dropdown"] { - column-gap: 1.5em; - - // TODO [Angular 17] Add direct child selector when resolved issue https://github.com/angular/angular/issues/49100 - //> div.title { - div.title { - grid-column: 1/2; - grid-row: 1/2; - } - - // TODO [Angular 17] Add direct child selector when resolved issue https://github.com/angular/angular/issues/49100 - //> div.heading { - div.heading { - grid-column: 1/2; - grid-row: 2/3; - } - - // TODO [Angular 17] Add direct child selector when resolved issue https://github.com/angular/angular/issues/49100 - //> button.close { - button.close { - grid-column: 2/3; - grid-row: 1/-1; - justify-self: end; - align-self: center; + // @formatter:off - Disable formatting because IntelliJ adds a space between number and unit, making the condition invalid. + @container viewtab (height < #{workbench-constants.$viewtab-multiline-breakpoint}) { + > span.heading { + display: none; } } + // @formatter:on } diff --git a/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.ts b/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.ts index 1f7d2ad72..24b31da72 100644 --- a/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.ts +++ b/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.ts @@ -8,10 +8,10 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {Component, HostBinding, Inject} from '@angular/core'; -import {VIEW_TAB_CONTEXT, ViewTabContext} from '../../workbench.constants'; +import {Component, HostBinding, inject} from '@angular/core'; import {WorkbenchView} from '../../view/workbench-view.model'; import {NgIf} from '@angular/common'; +import {VIEW_TAB_RENDERING_CONTEXT, ViewTabRenderingContext} from '../../workbench.constants'; @Component({ selector: 'wb-view-tab-content', @@ -23,34 +23,8 @@ import {NgIf} from '@angular/common'; export class ViewTabContentComponent { @HostBinding('attr.context') - public context: ViewTabContext; + public readonly context = inject(VIEW_TAB_RENDERING_CONTEXT); - @HostBinding('class.has-heading') - public get hasHeading(): boolean { - return !!this.view.heading; - } - - @HostBinding('class.active') - public get active(): boolean { - return this.view.active; - } - - @HostBinding('class.blocked') - public get blocked(): boolean { - return this.view.blocked; - } - - @HostBinding('class.scrolled-into-view') - public get isScrolledIntoView(): boolean { - return this.view.scrolledIntoView; - } - - constructor(public view: WorkbenchView, @Inject(VIEW_TAB_CONTEXT) context: ViewTabContext) { - this.context = context; - } - - public onClose(event: Event): void { - event.stopPropagation(); // prevent the view from being activated - this.view.close().then(); + constructor(public view: WorkbenchView) { } } diff --git a/projects/scion/workbench/src/lib/part/view-tab-drag-image/view-tab-drag-image.component.ts b/projects/scion/workbench/src/lib/part/view-tab-drag-image/view-tab-drag-image.component.ts new file mode 100644 index 000000000..9dbba6fdd --- /dev/null +++ b/projects/scion/workbench/src/lib/part/view-tab-drag-image/view-tab-drag-image.component.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2018-2022 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Component, HostBinding, inject, Injector} from '@angular/core'; +import {ViewDragService} from '../../view-dnd/view-drag.service'; +import {ComponentPortal, PortalModule} from '@angular/cdk/portal'; +import {WorkbenchModuleConfig} from '../../workbench-module-config'; +import {ViewTabContentComponent} from '../view-tab-content/view-tab-content.component'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {DOCUMENT, NgIf} from '@angular/common'; +import {WorkbenchView} from '../../view/workbench-view.model'; + +/** + * @see ViewTabComponent + */ +@Component({ + selector: 'wb-view-tab-drag-image', + templateUrl: '../view-tab/view-tab.component.html', + styleUrls: [ + '../view-tab/view-tab.component.scss', + './view-tab-drag-image.partial.component.scss', + ], + standalone: true, + imports: [ + NgIf, + PortalModule, + ], +}) +export class ViewTabDragImageComponent { + + public viewTabContentPortal!: ComponentPortal; + + @HostBinding('class.active') + public active = true; + + @HostBinding('class.part-active') + public partActive = true; + + /** + * Indicates if dragging this view tab over a tabbar. + */ + @HostBinding('class.drag-over-tabbar') + public isDragOverTabbar = false; + + /** + * Indicates if dragging this view tab over a tabbar located in the peripheral area. + */ + @HostBinding('class.drag-over-peripheral-tabbar') + public isDragOverPeripheralTabbar = false; + + constructor(public view: WorkbenchView, + private _workbenchModuleConfig: WorkbenchModuleConfig, + private _viewDragService: ViewDragService, + private _injector: Injector) { + this.installDragOverTabbarDetector(); + this.viewTabContentPortal = this.createViewTabContentPortal(); + } + + public onClose(..._: unknown[]): void { + throw Error('[UnsupportedOperationError]'); + } + + private createViewTabContentPortal(): ComponentPortal { + const componentType = this._workbenchModuleConfig.viewTabComponent || ViewTabContentComponent; + return new ComponentPortal(componentType, null, this._injector); + } + + private installDragOverTabbarDetector(): void { + const documentRoot = inject(DOCUMENT).documentElement; + this._viewDragService.tabbarDragOver$ + .pipe(takeUntilDestroyed()) + .subscribe(partId => { + // Compute if dragging this view tab over a tabbar. + this.isDragOverTabbar = partId !== null; + // Compute if dragging this view tab over a tabbar located in the peripheral area. + this.isDragOverPeripheralTabbar = partId !== null && documentRoot.querySelector(`wb-workbench:has(wb-main-area-layout) wb-part[data-partid="${partId}"]:not(.main-area)`) !== null; + }); + } +} diff --git a/projects/scion/workbench/src/lib/part/view-tab-drag-image/view-tab-drag-image.partial.component.scss b/projects/scion/workbench/src/lib/part/view-tab-drag-image/view-tab-drag-image.partial.component.scss new file mode 100644 index 000000000..5245bd504 --- /dev/null +++ b/projects/scion/workbench/src/lib/part/view-tab-drag-image/view-tab-drag-image.partial.component.scss @@ -0,0 +1,14 @@ +:host { + &.drag-over-peripheral-tabbar { + background-color: var(--sci-workbench-view-peripheral-background-color); + } + + &:not(.drag-over-tabbar) { + border-radius: var(--sci-workbench-tab-border-radius); + border: 1px solid var(--sci-workbench-tab-drag-border-color); + + > div.corner-radius { + display: none; + } + } +} diff --git a/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.html b/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.html index 664ec3a23..8e79b0ade 100644 --- a/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.html +++ b/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.html @@ -1 +1,19 @@ - + + + + + + + + + + + + + + + + close + diff --git a/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.scss b/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.scss index 49c0bc977..3c0018b30 100644 --- a/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.scss +++ b/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.scss @@ -1,50 +1,127 @@ -@use '../../../../theme/colors'; -@use '../../../../theme/styles'; +@use '../../../../design/workbench-constants'; + +/** + * IMPORTANT: THIS CSS FILE IS ALSO USED BY `ViewTabDragImageComponent` + */ :host { - display: grid; // stretches the portal to be full width and full height - position: relative; // positioning context for the portal - overflow: hidden; + display: grid; + align-items: center; + padding-left: var(--sci-workbench-tab-padding-inline); + padding-right: var(--sci-workbench-tab-padding-inline); + position: relative; // positioning context user-select: none; cursor: pointer; - border: none; box-sizing: border-box; outline: none; + border-top: 1px solid transparent; + border-left: 1px solid transparent; + border-right: 1px solid transparent; + border-top-left-radius: var(--sci-workbench-tab-border-radius); + border-top-right-radius: var(--sci-workbench-tab-border-radius); &.active { cursor: default; - color: colors.$part_tab-active-fgcolor; - } + color: var(--sci-workbench-tab-active-text-color); + border-left-color: var(--sci-workbench-tab-border-color); + border-right-color: var(--sci-workbench-tab-border-color); + border-top-color: var(--sci-workbench-tab-border-color); + background-color: var(--sci-workbench-view-background-color); - &[context="tabbar"] { - border-right: 1px solid colors.$part_tabbar_separator-color; - min-width: styles.$viewtab-min-width; - max-width: styles.$viewtab-max-width; - height: styles.$viewtab-height; + wb-workbench:has(wb-main-area-layout) wb-part:not(.main-area) & { + background-color: var(--sci-workbench-view-peripheral-background-color); + } - &.active { - background-color: colors.$part_tab-active-bgcolor; - box-shadow: -7px 0 10px 0 rgba(0, 0, 0, 0.4); + &.part-active > div.content { + color: var(--sci-workbench-part-active-tab-active-text-color); } } - &[context="tabbar-dropdown"] { - padding: .25em 0; - border-left: 3px solid transparent; + > div.content { + max-width: var(--sci-workbench-tab-max-width); + isolation: isolate; // stacking context to overlay the positioned 'hover' overlay + } + + > button.close { + all: unset; + position: absolute; + cursor: pointer; + visibility: hidden; + padding: .125em; + border-radius: var(--sci-corner-small); + right: .2em; - &:hover { - background-color: rgba(239, 239, 239, .5); - box-shadow: 0 0 0 1px colors.$viewlistitem-border-color; + &:not(:hover) { + opacity: .75; } + } - &.active { - border-left-color: black; + &.active, &:hover:not(.drag-over-tabbar) { + > button.close { + visibility: visible; } } - // disable pointer events on direct children if the view is blocked (not on the host element to allow dragging the viewtab) - &.blocked > ::ng-deep * { + &.active > button.close:hover { + background-color: var(--sci-workbench-tab-hover-background-color); + } + + // Indicator when hovering the tab + &:hover:not(.active):not(.drag-over-tabbar):before { + content: ''; + background-color: var(--sci-workbench-tab-hover-background-color); + position: absolute; + inset: 0 0 .125em .125em; + border-radius: var(--sci-workbench-tab-border-radius); pointer-events: none; } -} + > div.corner-radius { + height: var(--sci-workbench-tab-border-radius); + width: var(--sci-workbench-tab-border-radius); + overflow: hidden; + position: absolute; + bottom: 0; + + > div.circle { + position: absolute; + top: calc(-2 * var(--sci-workbench-tab-border-radius)); + width: calc(2 * var(--sci-workbench-tab-border-radius)); + height: calc(2 * var(--sci-workbench-tab-border-radius)); + border: var(--sci-workbench-tab-border-radius) solid var(--sci-workbench-view-background-color); + border-radius: 50%; + box-shadow: inset 0 0 0 1px var(--sci-workbench-tab-border-color); + box-sizing: content-box; + + wb-workbench:has(wb-main-area-layout) wb-part:not(.main-area) & { + border-color: var(--sci-workbench-view-peripheral-background-color); + } + } + + &.start { + left: calc(-1 * var(--sci-workbench-tab-border-radius)); + + > div.circle { + left: calc(-2 * var(--sci-workbench-tab-border-radius)); + } + } + + &.end { + right: calc(-1 * var(--sci-workbench-tab-border-radius)); + + > div.circle { + right: calc(-2 * var(--sci-workbench-tab-border-radius)); + } + } + } + + // @formatter:off - Disable formatting because IntelliJ adds a space between number and unit, making the condition invalid. + @container viewtab (height >= #{workbench-constants.$viewtab-multiline-breakpoint}) { + > button.close { + align-self: start; + top: .125em; + right: .125em; + } + } + // @formatter:on +} diff --git a/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.ts b/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.ts index abc0d1ad7..84497af35 100644 --- a/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.ts +++ b/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.ts @@ -8,15 +8,14 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {Attribute, Component, ElementRef, HostBinding, HostListener, Injector, Input, IterableChanges, IterableDiffers, NgZone} from '@angular/core'; -import {SciViewportComponent} from '@scion/components/viewport'; +import {Component, ElementRef, HostBinding, HostListener, inject, Injector, Input, IterableChanges, IterableDiffers, NgZone, OnChanges, SimpleChanges} from '@angular/core'; import {fromEvent, merge, Subject, withLatestFrom} from 'rxjs'; import {WorkbenchViewRegistry} from '../../view/workbench-view.registry'; import {filter, map, switchMap} from 'rxjs/operators'; import {VIEW_DRAG_TRANSFER_TYPE, ViewDragService} from '../../view-dnd/view-drag.service'; import {createElement} from '../../common/dom.util'; import {ComponentPortal, PortalModule} from '@angular/cdk/portal'; -import {VIEW_TAB_CONTEXT} from '../../workbench.constants'; +import {VIEW_TAB_RENDERING_CONTEXT, ViewTabRenderingContext} from '../../workbench.constants'; import {WorkbenchModuleConfig} from '../../workbench-module-config'; import {ViewTabContentComponent} from '../view-tab-content/view-tab-content.component'; import {ViewMenuService} from '../view-context-menu/view-menu.service'; @@ -26,74 +25,83 @@ import {WorkbenchRouter} from '../../routing/workbench-router.service'; import {subscribeInside} from '@scion/toolkit/operators'; import {ɵWorkbenchService} from '../../ɵworkbench.service'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {NgIf} from '@angular/common'; /** - * Indicates that the auxilary mouse button is pressed (usually the mouse wheel button or middle button). + * IMPORTANT: HTML and CSS also used by {@link ViewTabDragImageComponent}. + * + * @see ViewTabDragImageComponent */ -const AUXILARY_MOUSE_BUTTON = 4; - @Component({ selector: 'wb-view-tab', templateUrl: './view-tab.component.html', styleUrls: ['./view-tab.component.scss'], standalone: true, - imports: [PortalModule], + imports: [ + NgIf, + PortalModule, + ], }) -export class ViewTabComponent { +export class ViewTabComponent implements OnChanges { - private _viewIdChange$ = new Subject(); - private _context: 'tabbar' | 'tabbar-dropdown'; + private _ngOnChanges$ = new Subject(); public host: HTMLElement; public view!: ɵWorkbenchView; - public viewTabContentPortal!: ComponentPortal; + public viewTabContentPortal!: ComponentPortal; @Input({required: true}) @HostBinding('attr.data-viewid') - public set viewId(viewId: string) { - this.view = this._viewRegistry.get(viewId); - this.viewTabContentPortal = this.createViewTabContentPortal(); - this._viewIdChange$.next(); - } + public viewId!: string; - public get viewId(): string { - return this.view.id; + @HostBinding('attr.draggable') + public draggable = true; + + @HostBinding('attr.tabindex') + public tabindex = -1; // make the view focusable to install view menu accelerators + + /** + * Indicates if dragging a view tab over this view tab's tabbar. + */ + @HostBinding('class.drag-over-tabbar') + public get isDragOverTabbar(): boolean { + return this._viewDragService.isDragOverTabbar === this.view.part.id; } constructor(host: ElementRef, - // The context must be available during construction to create the portal for the view tab content. - // The param is weak typed as a string (instead as a string literal) due to Angular restrictions when building prod. - @Attribute('context') context: string, private _workbenchService: ɵWorkbenchService, private _workbenchModuleConfig: WorkbenchModuleConfig, private _viewRegistry: WorkbenchViewRegistry, private _router: WorkbenchRouter, - private _viewport: SciViewportComponent, private _viewDragService: ViewDragService, private _differs: IterableDiffers, private _viewContextMenuService: ViewMenuService, - private _injector: Injector, - zone: NgZone) { - this._context = context as 'tabbar' | 'tabbar-dropdown'; + private _injector: Injector) { this.host = host.nativeElement; - this.installMaximizeListener(zone); + this.installMaximizeListener(); this.installViewCssClassListener(); this.installViewMenuItemAccelerators(); } + public ngOnChanges(changes: SimpleChanges): void { + this.view = this._viewRegistry.get(this.viewId); + this.viewTabContentPortal = this.createViewTabContentPortal(); + this._ngOnChanges$.next(); + } + @HostBinding('class.active') public get active(): boolean { return this.view.active; } - @HostBinding('class.dirty') - public get dirty(): boolean { - return this.view.dirty; + @HostBinding('class.part-active') + public get partActive(): boolean { + return this.view.part.active; } - @HostBinding('class.blocked') - public get blocked(): boolean { - return this.view.blocked; + @HostBinding('class.e2e-dirty') + public get dirty(): boolean { + return this.view.dirty; } @HostListener('click') @@ -101,6 +109,11 @@ export class ViewTabComponent { this.view.activate().then(); } + public onClose(event: Event): void { + event.stopPropagation(); // prevent the view from being activated + this.view.close().then(); + } + @HostListener('mousedown', ['$event']) public onMousedown(event: MouseEvent): void { if (event.buttons === AUXILARY_MOUSE_BUTTON) { @@ -112,21 +125,11 @@ export class ViewTabComponent { @HostListener('contextmenu', ['$event']) public onContextmenu(event: MouseEvent): void { - this._viewContextMenuService.showMenu({x: event.clientX, y: event.clientY}, this.viewId).then(); + this._viewContextMenuService.showMenu({x: event.clientX, y: event.clientY}, this.view.id).then(); event.stopPropagation(); event.preventDefault(); } - @HostBinding('attr.tabindex') - public get tabindex(): number { - return -1; // make the view focusable to install view menu accelerators - } - - @HostBinding('attr.draggable') - public get draggable(): boolean { - return this._context === 'tabbar'; - } - @HostListener('dragstart', ['$event']) public onDragStart(event: DragEvent): void { this.view.activate().then(() => { @@ -149,8 +152,8 @@ export class ViewTabComponent { partId: this.view.part.id, viewTabPointerOffsetX: event.offsetX, viewTabPointerOffsetY: event.offsetY, - viewTabWidth: (event.target as HTMLElement).offsetWidth, - viewTabHeight: (event.target as HTMLElement).offsetHeight, + viewTabWidth: this.host.getBoundingClientRect().width, + viewTabHeight: this.host.getBoundingClientRect().height, appInstanceId: this._workbenchService.appInstanceId, }); }); @@ -165,36 +168,14 @@ export class ViewTabComponent { this._viewDragService.unsetViewDragData(); } - /** - * Indicates whether this view tab is the drag source of a current view drag operation. - */ - public isDragSource(): boolean { - return this._viewDragService.viewDragData?.viewId === this.viewId; - } - - /** - * Returns whether this tab is fully scrolled into view. - */ - public isScrolledIntoView(): boolean { - return this._viewport.isElementInView(this.host, 'full'); - } - - /** - * Scrolls this tab into view. - */ - public scrollIntoView(): void { - if (!this.isScrolledIntoView()) { - this._viewport.scrollIntoView(this.host); - } - } - /** * Listens for 'dblclick' events to maximize or minimize the main area. * * Note that the listener is not activated until the mouse is moved. Otherwise, closing successive * views (if they have different tab widths) could result in unintended maximization or minimization. */ - private installMaximizeListener(zone: NgZone): void { + private installMaximizeListener(): void { + const zone = inject(NgZone); const enabled$ = merge(fromEvent(this.host, 'mouseenter'), fromEvent(this.host, 'mousemove'), fromEvent(this.host, 'mouseleave')) .pipe( map(event => event.type === 'mousemove'), // the 'mousemove' event arms the listener @@ -215,12 +196,12 @@ export class ViewTabComponent { } /** - * Adds view specific CSS classes to the . + * Adds view specific CSS classes to the view-tab. */ private installViewCssClassListener(): void { const differ = this._differs.find([]).create(); - this._viewIdChange$ + this._ngOnChanges$ .pipe( switchMap(() => this.view.cssClasses$), map(cssClasses => differ.diff(cssClasses)), @@ -234,7 +215,7 @@ export class ViewTabComponent { } private installViewMenuItemAccelerators(): void { - this._viewIdChange$ + this._ngOnChanges$ .pipe( switchMap(() => this._viewContextMenuService.installMenuItemAccelerators$(this.host, this.view)), takeUntilDestroyed(), @@ -242,14 +223,19 @@ export class ViewTabComponent { .subscribe(); } - private createViewTabContentPortal(): ComponentPortal { - const injector = Injector.create({ + private createViewTabContentPortal(): ComponentPortal { + const componentType = this._workbenchModuleConfig.viewTabComponent || ViewTabContentComponent; + return new ComponentPortal(componentType, null, Injector.create({ parent: this._injector, providers: [ {provide: WorkbenchView, useValue: this.view}, - {provide: VIEW_TAB_CONTEXT, useValue: this._context}, + {provide: VIEW_TAB_RENDERING_CONTEXT, useValue: 'tab' satisfies ViewTabRenderingContext}, ], - }); - return new ComponentPortal(this._workbenchModuleConfig.viewTabComponent || ViewTabContentComponent, null, injector); + })); } } + +/** + * Indicates that the auxilary mouse button is pressed (usually the mouse wheel button or middle button). + */ +const AUXILARY_MOUSE_BUTTON = 4; diff --git a/projects/scion/workbench/src/lib/perspective/workbench-perspective-storage.service.ts b/projects/scion/workbench/src/lib/perspective/workbench-perspective-storage.service.ts index fa757ffa1..f99defff5 100644 --- a/projects/scion/workbench/src/lib/perspective/workbench-perspective-storage.service.ts +++ b/projects/scion/workbench/src/lib/perspective/workbench-perspective-storage.service.ts @@ -64,7 +64,7 @@ export class WorkbenchPerspectiveStorageService { */ const storageKeys = { perspectiveData: (perspectiveId: string): string => `scion.workbench.perspectives.${perspectiveId}`, - activePerspectiveId: 'scion.workbench.activePerspective', + activePerspectiveId: 'scion.workbench.perspective', }; /** diff --git a/projects/scion/workbench/src/lib/popup/popup.component.scss b/projects/scion/workbench/src/lib/popup/popup.component.scss index e2fda6c3f..87bb10f90 100644 --- a/projects/scion/workbench/src/lib/popup/popup.component.scss +++ b/projects/scion/workbench/src/lib/popup/popup.component.scss @@ -3,4 +3,6 @@ grid-template-columns: 100%; grid-template-rows: 100%; outline: none; + border-radius: var(--sci-corner); + overflow: hidden; } diff --git a/projects/scion/workbench/src/lib/public_api.ts b/projects/scion/workbench/src/lib/public_api.ts index a6d5d30b2..d1204a1b1 100644 --- a/projects/scion/workbench/src/lib/public_api.ts +++ b/projects/scion/workbench/src/lib/public_api.ts @@ -13,7 +13,7 @@ export {WorkbenchModule} from './workbench.module'; export {WorkbenchService} from './workbench.service'; export {WorkbenchViewPreDestroy, WorkbenchPartAction} from './workbench.model'; export {WorkbenchComponent} from './workbench.component'; -export {VIEW_TAB_CONTEXT, ViewTabContext} from './workbench.constants'; +export {VIEW_TAB_RENDERING_CONTEXT, ViewTabRenderingContext} from './workbench.constants'; export * from './layout/public_api'; export * from './perspective/public_api'; diff --git a/projects/scion/workbench/src/lib/theme/workbench-theme-switcher.service.ts b/projects/scion/workbench/src/lib/theme/workbench-theme-switcher.service.ts new file mode 100644 index 000000000..f9c175d5a --- /dev/null +++ b/projects/scion/workbench/src/lib/theme/workbench-theme-switcher.service.ts @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2018-2023 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Observable, ReplaySubject, share} from 'rxjs'; +import {inject, Injectable} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; +import {fromMutation$} from '@scion/toolkit/observable'; +import {distinctUntilChanged, map, startWith} from 'rxjs/operators'; +import {WorkbenchStorage} from '../storage/workbench-storage'; + +/** + * Represents the key to associate the activated theme in the storage. + */ +const THEME_STORAGE_KEY = 'scion.workbench.theme'; + +/** + * Enables switching between workbench themes. + */ +@Injectable({providedIn: 'root'}) +export class WorkbenchThemeSwitcher { + + private readonly _documentRoot = inject(DOCUMENT).documentElement; + + /** + * Emits the name of the current workbench theme. + * + * Upon subscription, emits the name of the current theme, and then continuously emits when switching the theme. It never completes. + */ + public readonly theme$: Observable; + + constructor(private _workbenchStorage: WorkbenchStorage) { + this.theme$ = this.detectTheme$(); + this.activateThemeFromStorage().then(); + } + + /** + * Switches the theme of the workbench. + * + * @param theme - The name of the theme to switch to. + */ + public async switchTheme(theme: string): Promise { + this._documentRoot.setAttribute('sci-theme', theme); + await this._workbenchStorage.store(THEME_STORAGE_KEY, theme); + } + + /** + * Detects the current workbench theme from the HTML root element. + */ + private detectTheme$(): Observable { + return new Observable(observer => { + const subscription = fromMutation$(this._documentRoot, {attributeFilter: ['sci-theme']}) + .pipe( + startWith(undefined as void), + map(() => getComputedStyle(this._documentRoot).getPropertyValue('--sci-theme') || null), + distinctUntilChanged(), + share({connector: () => new ReplaySubject(1), resetOnRefCountZero: false}), + ) + .subscribe(observer); + + return () => subscription.unsubscribe(); + }); + } + + /** + * Activates the theme from storage, if any. + */ + private async activateThemeFromStorage(): Promise { + const theme = await this._workbenchStorage.load(THEME_STORAGE_KEY); + if (theme) { + await this.switchTheme(theme); + } + } +} diff --git a/projects/scion/workbench/src/lib/view-dnd/view-drag.service.ts b/projects/scion/workbench/src/lib/view-dnd/view-drag.service.ts index 136413af2..a0b564df5 100644 --- a/projects/scion/workbench/src/lib/view-dnd/view-drag.service.ts +++ b/projects/scion/workbench/src/lib/view-dnd/view-drag.service.ts @@ -39,7 +39,7 @@ export class ViewDragService implements OnDestroy { private _viewDragStartBroadcastChannel = new WorkbenchBroadcastChannel('workbench/view/dragstart'); private _viewDragEndBroadcastChannel = new WorkbenchBroadcastChannel('workbench/view/dragend'); private _viewMoveBroadcastChannel = new WorkbenchBroadcastChannel('workbench/view/move'); - private _tabbarDragOver$ = new BehaviorSubject(false); + private _tabbarDragOver$ = new BehaviorSubject(null); /** * Emits when the user starts dragging a viewtab. The event is received across app instances of the same origin. @@ -57,11 +57,12 @@ export class ViewDragService implements OnDestroy { public readonly viewMove$: Observable = this._viewMoveBroadcastChannel.observe$; /** - * Emits when the user is dragging a view over the tabbar of the current document. + * Emits the identity of the part when the user is dragging a view over its tabbar, or `null` if not dragging over a tabbar. + * The event is NOT received across app instances. * * Upon subscription, emits the current state, and then each time the state changes. The observable never completes. */ - public readonly tabbarDragOver$: Observable = this._tabbarDragOver$; + public readonly tabbarDragOver$: Observable = this._tabbarDragOver$; constructor(private _zone: NgZone) { this.viewDragStart$ @@ -77,14 +78,30 @@ export class ViewDragService implements OnDestroy { } /** - * Invoke to inform when the user is dragging a view over the tabbar. + * Set when dragging a view over specified tabbar. */ - public notifyDragOverTabbar(dragOverTabbar: boolean): void { - if (this._tabbarDragOver$.value !== dragOverTabbar) { - this._tabbarDragOver$.next(dragOverTabbar); + public setTabbarDragover(partId: string): void { + this._tabbarDragOver$.next(partId); + } + + /** + * Unset when not dragging a view over specified tabbar anymore. + */ + public unsetTabbarDragover(partId: string): void { + if (this._tabbarDragOver$.value === partId) { + this._tabbarDragOver$.next(null); } } + /** + * Indicates if dragging a view tab over a tabbar. + * + * Returns the identity of the part if the user is dragging a view over its tabbar, or `null` if not dragging over a tabbar. + */ + public get isDragOverTabbar(): string | null { + return this._tabbarDragOver$.value; + } + /** * Checks if the given event is a view drag event with the same origin. */ diff --git a/projects/scion/workbench/src/lib/view-dnd/view-drop-placeholder-renderer.service.ts b/projects/scion/workbench/src/lib/view-dnd/view-drop-placeholder-renderer.service.ts index 436292239..35e212045 100644 --- a/projects/scion/workbench/src/lib/view-dnd/view-drop-placeholder-renderer.service.ts +++ b/projects/scion/workbench/src/lib/view-dnd/view-drop-placeholder-renderer.service.ts @@ -56,8 +56,9 @@ export class ViewDropPlaceholderRenderer { parent: this._dropPlaceholderHost.ref().element.nativeElement, style: { 'position': 'fixed', - 'background': '#000000', - 'opacity': .1, + 'background-color': 'var(--sci-workbench-part-dropzone-background-color)', + 'border': '1px var(--sci-workbench-part-dropzone-border-style) var(--sci-workbench-part-dropzone-border-color)', + 'border-radius': 'var(--sci-workbench-part-dropzone-border-radius)', 'pointer-events': 'none', 'transition-duration': '125ms', 'transition-property': 'top,left,width,height', diff --git a/projects/scion/workbench/src/lib/view-dnd/view-tab-drag-image-renderer.service.ts b/projects/scion/workbench/src/lib/view-dnd/view-tab-drag-image-renderer.service.ts index 7b1fbc228..0fb4b0bb9 100644 --- a/projects/scion/workbench/src/lib/view-dnd/view-tab-drag-image-renderer.service.ts +++ b/projects/scion/workbench/src/lib/view-dnd/view-tab-drag-image-renderer.service.ts @@ -9,21 +9,20 @@ */ import {ApplicationRef, ComponentFactoryResolver, Injectable, Injector, NgZone} from '@angular/core'; -import {of} from 'rxjs'; import {take} from 'rxjs/operators'; import {createElement, setStyle} from '../common/dom.util'; import {ViewDragData, ViewDragService} from './view-drag.service'; import {ComponentPortal, DomPortalOutlet} from '@angular/cdk/portal'; -import {ViewTabContentComponent} from '../part/view-tab-content/view-tab-content.component'; -import {WorkbenchMenuItem} from '../workbench.model'; -import {WorkbenchModuleConfig} from '../workbench-module-config'; -import {VIEW_TAB_CONTEXT} from '../workbench.constants'; +import {subscribeInside} from '@scion/toolkit/operators'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {ViewTabDragImageComponent} from '../part/view-tab-drag-image/view-tab-drag-image.component'; +import {of} from 'rxjs'; import {UrlSegment} from '@angular/router'; -import {Disposable} from '../common/disposable'; +import {WorkbenchMenuItem} from '../workbench.model'; import {WorkbenchView} from '../view/workbench-view.model'; +import {VIEW_TAB_RENDERING_CONTEXT, ViewTabRenderingContext} from '../workbench.constants'; import {WorkbenchPart} from '../part/workbench-part.model'; -import {subscribeInside} from '@scion/toolkit/operators'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {Disposable} from '../common/disposable'; export type ConstrainFn = (rect: ViewDragImageRect) => ViewDragImageRect; @@ -40,7 +39,6 @@ export class ViewTabDragImageRenderer { private _constrainDragImageRectFn: ((rect: ViewDragImageRect) => ViewDragImageRect) | null = null; constructor(private _viewDragService: ViewDragService, - private _workbenchModuleConfig: WorkbenchModuleConfig, // TODO [Angular 17][https://github.com/angular/components/issues/24334] Alternative constructor (ComponentFactoryResolver is deprecated) private _componentFactoryResolver: ComponentFactoryResolver, private _applicationRef: ApplicationRef, @@ -89,6 +87,7 @@ export class ViewTabDragImageRenderer { setStyle(this._viewDragImagePortalOutlet!.outletElement as HTMLElement, { left: `${dragPosition.x}px`, top: `${dragPosition.y}px`, + height: `${dragPosition.height}px`, }); } @@ -133,8 +132,13 @@ export class ViewTabDragImageRenderer { }, }); this._viewDragImagePortalOutlet = new DomPortalOutlet(outletElement, this._componentFactoryResolver, this._applicationRef, this._injector); - const componentRef = this._viewDragImagePortalOutlet.attachComponentPortal(this.createViewTabContentPortal(dragData)); - componentRef.changeDetectorRef.detectChanges(); + this._viewDragImagePortalOutlet.attachComponentPortal(new ComponentPortal(ViewTabDragImageComponent, null, Injector.create({ + parent: this._injector, + providers: [ + {provide: WorkbenchView, useValue: new DragImageWorkbenchView(dragData)}, + {provide: VIEW_TAB_RENDERING_CONTEXT, useValue: 'drag-image' satisfies ViewTabRenderingContext}, + ], + }))); } private disposeDragImage(): void { @@ -181,18 +185,6 @@ export class ViewTabDragImageRenderer { } }); } - - private createViewTabContentPortal(viewDragData: ViewDragData): ComponentPortal { - const injector = Injector.create({ - parent: this._injector, - providers: [ - {provide: WorkbenchView, useValue: new DragImageWorkbenchView(viewDragData)}, - {provide: VIEW_TAB_CONTEXT, useValue: 'drag-image'}, - ], - }); - - return new ComponentPortal(this._workbenchModuleConfig.viewTabComponent || ViewTabContentComponent, null, injector); - } } /** @@ -257,6 +249,10 @@ class DragImageWorkbenchView implements WorkbenchView { this.urlSegments = dragData.viewUrlSegments; } + public get part(): WorkbenchPart { + throw Error('[UnsupportedOperationError]'); + } + public close(): Promise { throw Error('[UnsupportedOperationError]'); } @@ -273,10 +269,6 @@ class DragImageWorkbenchView implements WorkbenchView { throw Error('[UnsupportedOperationError]'); } - public get part(): WorkbenchPart { - throw Error('[UnsupportedOperationError]'); - } - public activate(): Promise { throw Error('[UnsupportedOperationError]'); } diff --git a/projects/scion/workbench/src/lib/view/view.component.scss b/projects/scion/workbench/src/lib/view/view.component.scss index 15ebfaf9c..337e97f56 100644 --- a/projects/scion/workbench/src/lib/view/view.component.scss +++ b/projects/scion/workbench/src/lib/view/view.component.scss @@ -1,6 +1,12 @@ :host { display: flex; flex-direction: column; + background-color: var(--sci-workbench-view-background-color); + color: var(--sci-color-text); + + wb-workbench:has(wb-main-area-layout) wb-part:not(.main-area) & { + background-color: var(--sci-workbench-view-peripheral-background-color); + } &.blocked, &.view-drag { pointer-events: none; diff --git a/projects/scion/workbench/src/lib/view/view.component.spec.ts b/projects/scion/workbench/src/lib/view/view.component.spec.ts index 237cffff6..a4f7519d8 100644 --- a/projects/scion/workbench/src/lib/view/view.component.spec.ts +++ b/projects/scion/workbench/src/lib/view/view.component.spec.ts @@ -55,16 +55,16 @@ describe('ViewComponent', () => { const viewDebugElement = getViewDebugElement('view.1'); viewDebugElement.view.dirty = true; advance(fixture); - expect(fixture.debugElement.query(By.css('wb-view-tab')).classes).withContext('(A)').toEqual(jasmine.objectContaining({'dirty': true})); + expect(fixture.debugElement.query(By.css('wb-view-tab')).classes).withContext('(A)').toEqual(jasmine.objectContaining({'e2e-dirty': true})); // Clear dirty flag viewDebugElement.view.dirty = false; advance(fixture); - expect(fixture.debugElement.query(By.css('wb-view-tab')).classes).not.withContext('(B)').toEqual(jasmine.objectContaining({'dirty': true})); + expect(fixture.debugElement.query(By.css('wb-view-tab')).classes).not.withContext('(B)').toEqual(jasmine.objectContaining({'e2e-dirty': true})); viewDebugElement.view.dirty = true; advance(fixture); - expect(fixture.debugElement.query(By.css('wb-view-tab')).classes).withContext('(C)').toEqual(jasmine.objectContaining({'dirty': true})); + expect(fixture.debugElement.query(By.css('wb-view-tab')).classes).withContext('(C)').toEqual(jasmine.objectContaining({'e2e-dirty': true})); discardPeriodicTasks(); })); @@ -89,7 +89,29 @@ describe('ViewComponent', () => { discardPeriodicTasks(); })); - it('should render heading', fakeAsync(() => { + it('should not render heading (by default)', fakeAsync(() => { + TestBed.inject(WorkbenchRouter).navigate(['view', {heading: 'HEADING'}]).then(); + advance(fixture); + const headingElement = fixture.debugElement.query(By.css('wb-view-tab .heading')).nativeElement; + expect(getComputedStyle(headingElement)).toEqual(jasmine.objectContaining({display: 'none'})); + + discardPeriodicTasks(); + })); + + it('should not render heading if tab height < 3.5rem', fakeAsync(() => { + setDesignToken('--sci-workbench-tab-height', '3.4rem'); + + TestBed.inject(WorkbenchRouter).navigate(['view', {heading: 'HEADING'}]).then(); + advance(fixture); + const headingElement = fixture.debugElement.query(By.css('wb-view-tab .heading')).nativeElement; + expect(getComputedStyle(headingElement)).toEqual(jasmine.objectContaining({display: 'none'})); + + discardPeriodicTasks(); + })); + + it('should render heading if tab height >= 3.5rem', fakeAsync(() => { + setDesignToken('--sci-workbench-tab-height', '3.5rem'); + // Add View TestBed.inject(WorkbenchRouter).navigate(['view', {heading: 'HEADING'}]).then(); advance(fixture); @@ -130,6 +152,8 @@ describe('ViewComponent', () => { })); it('should render heading as configured on route', fakeAsync(() => { + setDesignToken('--sci-workbench-tab-height', '3.5rem'); + // Add View TestBed.inject(WorkbenchRouter).navigate(['view-with-heading']).then(); advance(fixture); @@ -170,6 +194,8 @@ describe('ViewComponent', () => { })); it('should take heading from view over heading configured on route', fakeAsync(() => { + setDesignToken('--sci-workbench-tab-height', '3.5rem'); + // Add View TestBed.inject(WorkbenchRouter).navigate(['view-with-heading', {heading: 'HEADING'}]).then(); advance(fixture); @@ -203,6 +229,8 @@ describe('ViewComponent', () => { })); it('should unset heading when navigating to a different route', fakeAsync(() => { + setDesignToken('--sci-workbench-tab-height', '3.5rem'); + // Add View TestBed.inject(WorkbenchRouter).navigate(['view-1', {heading: 'HEADING'}]).then(); advance(fixture); @@ -230,6 +258,8 @@ describe('ViewComponent', () => { })); it('should replace heading when navigating to a different route', fakeAsync(() => { + setDesignToken('--sci-workbench-tab-height', '3.5rem'); + // Add View TestBed.inject(WorkbenchRouter).navigate(['view-1', {heading: 'HEADING 1'}]).then(); advance(fixture); @@ -481,6 +511,11 @@ describe('ViewComponent', () => { return {view, viewComponent, component}; } + function setDesignToken(name: string, value: string): void { + const workbenchElement = (fixture.debugElement.nativeElement as HTMLElement); + workbenchElement.style.setProperty(name, value); + } + interface ViewDebugElement { view: WorkbenchView; viewComponent: ViewComponent; diff --git a/projects/scion/workbench/src/lib/workbench.component.scss b/projects/scion/workbench/src/lib/workbench.component.scss index f2614a8d0..f5ebea0e9 100644 --- a/projects/scion/workbench/src/lib/workbench.component.scss +++ b/projects/scion/workbench/src/lib/workbench.component.scss @@ -1,21 +1,19 @@ -@use '../../theme/colors'; - :host { display: grid; position: relative; // positioning context overflow: hidden; - background-color: colors.$background-color; + color: var(--sci-color-text); + background-color: var(--sci-color-background-primary); > div.stacking-context-barrier { display: flex; - // Form a stacking context barrier (z-index=0) to clear nested stacking contexts so that subsequent DOM elements are rendered above elements contained in the barrier element, - // regardless of whether they form a stacking context. In other words, that elements overlay elements according to their DOM element ordering. + // Clear nested stacking contexts so that DOM elements following the workbench component are + // rendered on top of elements forming a stacking context. // - // The following elements form a stacking context: + // The following elements form a new stacking context: // - splitter of `` - // - `wb-workbench-layout` during drag & drop - z-index: 0; + isolation: isolate; > wb-workbench-layout { flex: auto; @@ -31,33 +29,11 @@ > wb-message-box-stack { position: absolute; - inset: 0 0 0 0; + inset: 0; } > wb-notification-list { position: absolute; - inset: 0 0 0 0; - } - - // For iframe-based microfrontend integration, `` forms a new stacking context during a workbench drag & drop operation such - // as dragging a view tab, moving a part splitter, or moving a message box. The stacking context, however, is local to the workbench component. - // - // Rationale: - // Since we cannot add iframes directly to the view component (an iframe would reload its content if it is reparented in the DOM), we - // add iframes after the `` element and project each iframe to its view boundaries. Alternatively, if we were to add - // iframes before the workbench layout, we would have to disable pointer events on the layout in order to interact with iframe content. - // - // As a consequence, the iframes now cover the view drop zones, stopping us from arranging views via drag & drop. For that reason, during drag & drop, - // we form a new stacking context on the `` element by setting its `z-index` to `1`. In turn, during drag & drop, the layout - // will overlap the iframes, making drop zones functional and preventing iframes from consuming pointer events. However, the new stacking context - // needs to be local to the workbench component, as `` would otherwise overlap subsequent DOM elements, causing, for example, - // drag images or message boxes to be rendered under the layout. Therefore, we add it to an element that acts as a stacking context barrier, i.e., - // an element with a z-index of `0`. A `z-index` of `0` is similar to the value `auto`, which means that the element is rendered in the same layer as - // elements without an explicit z-index, while, in contrast to the value `auto`, still forming a stacking context. - // - // For more information about stacking contexts, refer to - // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context. - &.dragging > div.stacking-context-barrier > wb-workbench-layout { - z-index: 1; + inset: 0; } } diff --git a/projects/scion/workbench/src/lib/workbench.component.ts b/projects/scion/workbench/src/lib/workbench.component.ts index e68d39533..d847d30f2 100644 --- a/projects/scion/workbench/src/lib/workbench.component.ts +++ b/projects/scion/workbench/src/lib/workbench.component.ts @@ -8,8 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {Component, HostBinding, inject, OnDestroy, ViewChild, ViewContainerRef} from '@angular/core'; -import {WorkbenchLayoutService} from './layout/workbench-layout.service'; +import {Component, inject, OnDestroy, ViewChild, ViewContainerRef} from '@angular/core'; import {IFRAME_HOST, VIEW_DROP_PLACEHOLDER_HOST, VIEW_MODAL_MESSAGE_BOX_HOST} from './content-projection/view-container.reference'; import {WorkbenchLauncher, WorkbenchStartup} from './startup/workbench-launcher.service'; import {WorkbenchModuleConfig} from './workbench-module-config'; @@ -20,7 +19,6 @@ import {AsyncPipe, NgComponentOutlet, NgIf} from '@angular/common'; import {WorkbenchLayoutComponent} from './layout/workbench-layout.component'; import {NotificationListComponent} from './notification/notification-list.component'; import {MessageBoxStackComponent} from './message-box/message-box-stack.component'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {combineLatest, lastValueFrom} from 'rxjs'; import {first, map} from 'rxjs/operators'; @@ -66,9 +64,6 @@ export class WorkbenchComponent implements OnDestroy { */ protected whenViewContainersInjected: Promise; - @HostBinding('class.dragging') - protected dragging = false; - @ViewChild('iframe_host', {read: ViewContainerRef}) protected set injectIframeHost(vcr: ViewContainerRef) { vcr && this.viewContainerReferences.iframeHost.set(vcr); @@ -85,7 +80,6 @@ export class WorkbenchComponent implements OnDestroy { } constructor(workbenchModuleConfig: WorkbenchModuleConfig, - private _workbenchLayoutService: WorkbenchLayoutService, private _workbenchLauncher: WorkbenchLauncher, private _logger: Logger, protected workbenchStartup: WorkbenchStartup) { @@ -93,7 +87,6 @@ export class WorkbenchComponent implements OnDestroy { this.splash = workbenchModuleConfig?.startup?.splash || SplashComponent; this.whenViewContainersInjected = this.createHostViewContainersInjectedPromise(); this.startWorkbench(); - this.installViewDragDetector(); } /** @@ -105,17 +98,6 @@ export class WorkbenchComponent implements OnDestroy { } } - /** - * Updates {@link dragging} property when start or end dragging a view. - */ - private installViewDragDetector(): void { - this._workbenchLayoutService.dragging$ - .pipe(takeUntilDestroyed()) - .subscribe(event => { - this.dragging = (event === 'start'); - }); - } - /** * Creates a Promise that resolves once all the required view containers for the workbench to attach elements have been injected from the template. */ diff --git a/projects/scion/workbench/src/lib/workbench.constants.ts b/projects/scion/workbench/src/lib/workbench.constants.ts index 1d2ac94ef..8b2e4ac29 100644 --- a/projects/scion/workbench/src/lib/workbench.constants.ts +++ b/projects/scion/workbench/src/lib/workbench.constants.ts @@ -36,18 +36,19 @@ export const POPUP_ID_PREFIX = 'popup.'; export const MAIN_AREA_LAYOUT_QUERY_PARAM = 'main_area'; /** - * Defines the contexts in which a viewtab is rendered. + * Defines the context in which a viewtab is rendered. * - * - tabbar: as tab in the tabbar - * - tabbar-dropdown: as item in the dropdown of not visible views - * - drag-image: as drag image during a view drag operation + * - `tab`: if rendered as view tab in the tabbar + * - `list-item`: if rendered as list item in the view list menu + * - `drag-image`: if rendered as drag image while dragging a view tab */ -export declare type ViewTabContext = 'tabbar' | 'tabbar-dropdown' | 'drag-image'; +export type ViewTabRenderingContext = 'tab' | 'list-item' | 'drag-image'; /** - * DI token to inject the context in which the viewtab is rendered. + * DI token to inject the context in which the view tab is rendered. */ -export const VIEW_TAB_CONTEXT = new InjectionToken('VIEW_TAB_CONTEXT'); +export const VIEW_TAB_RENDERING_CONTEXT = new InjectionToken('VIEW_TAB_RENDERING_CONTEXT'); + /** * DI token representing the configured workbench layout. diff --git a/projects/scion/workbench/src/lib/workbench.module.ts b/projects/scion/workbench/src/lib/workbench.module.ts index 25c731950..2edc70c52 100644 --- a/projects/scion/workbench/src/lib/workbench.module.ts +++ b/projects/scion/workbench/src/lib/workbench.module.ts @@ -22,12 +22,13 @@ import {ViewMoveHandler} from './view/view-move-handler.service'; import {provideWorkbenchMicrofrontendSupport} from './microfrontend-platform/workbench-microfrontend-support'; import {provideWorkbenchLauncher} from './startup/workbench-launcher.service'; import {provideLogging} from './logging'; -import {WORKBENCH_POST_STARTUP, WORKBENCH_STARTUP} from './startup/workbench-initializer'; +import {WORKBENCH_POST_STARTUP, WORKBENCH_PRE_STARTUP, WORKBENCH_STARTUP} from './startup/workbench-initializer'; import {WorkbenchPerspectiveService} from './perspective/workbench-perspective.service'; import {DefaultWorkbenchStorage, WorkbenchStorage} from './storage/workbench-storage'; import {provideLocationPatch} from './routing/ɵlocation'; import {WorkbenchLayoutFactory} from './layout/workbench-layout.factory'; import {MAIN_AREA} from './layout/workbench-layout'; +import {WorkbenchThemeSwitcher} from './theme/workbench-theme-switcher.service'; /** * Module of the SCION Workbench. @@ -102,6 +103,11 @@ export class WorkbenchModule { useFactory: provideForRootGuard, deps: [[WorkbenchService, new Optional(), new SkipSelf()]], }, + { + provide: WORKBENCH_PRE_STARTUP, + useExisting: WorkbenchThemeSwitcher, + multi: true, + }, { provide: WORKBENCH_STARTUP, multi: true, diff --git a/projects/scion/workbench/src/lib/workbench.service.ts b/projects/scion/workbench/src/lib/workbench.service.ts index 9ef0867b2..cb684672e 100644 --- a/projects/scion/workbench/src/lib/workbench.service.ts +++ b/projects/scion/workbench/src/lib/workbench.service.ts @@ -133,4 +133,23 @@ export abstract class WorkbenchService { * @return handle to unregister the menu item. */ public abstract registerViewMenuItem(factoryFn: WorkbenchMenuItemFactoryFn): Disposable; + + /** + * Switches the theme of the workbench. + * + * Themes can be registered when loading the `@scion/workbench` SCSS module in the application's `styles.scss` file. + * By default, SCION provides a light and a dark theme, `scion-light` and `scion-dark`. + * + * See the documentation of `@scion/workbench` SCSS module for more information. + * + * @param theme - The name of the theme to switch to. + */ + public abstract switchTheme(theme: string): Promise; + + /** + * Emits the name of the current workbench theme. + * + * Upon subscription, emits the name of the current theme, and then continuously emits when switching the theme. It never completes. + */ + public abstract readonly theme$: Observable; } diff --git "a/projects/scion/workbench/src/lib/\311\265workbench.service.ts" "b/projects/scion/workbench/src/lib/\311\265workbench.service.ts" index d65ca070f..ca49a487e 100644 --- "a/projects/scion/workbench/src/lib/\311\265workbench.service.ts" +++ "b/projects/scion/workbench/src/lib/\311\265workbench.service.ts" @@ -24,6 +24,7 @@ import {ɵWorkbenchPart} from './part/ɵworkbench-part.model'; import {ɵWorkbenchPerspective} from './perspective/ɵworkbench-perspective.model'; import {WorkbenchPerspectiveRegistry} from './perspective/workbench-perspective.registry'; import {WorkbenchPartActionRegistry} from './part/workbench-part-action.registry'; +import {WorkbenchThemeSwitcher} from './theme/workbench-theme-switcher.service'; @Injectable({providedIn: 'root'}) export class ɵWorkbenchService implements WorkbenchService { @@ -36,6 +37,7 @@ export class ɵWorkbenchService implements WorkbenchService { public readonly perspectives$: Observable; public readonly parts$: Observable; public readonly views$: Observable; + public readonly theme$: Observable; public readonly viewMenuItemProviders$ = new BehaviorSubject([]); @@ -44,10 +46,12 @@ export class ɵWorkbenchService implements WorkbenchService { private _partRegistry: WorkbenchPartRegistry, private _partActionRegistry: WorkbenchPartActionRegistry, private _viewRegistry: WorkbenchViewRegistry, - private _perspectiveService: WorkbenchPerspectiveService) { + private _perspectiveService: WorkbenchPerspectiveService, + private _workbenchThemeSwitcher: WorkbenchThemeSwitcher) { this.perspectives$ = this._perspectiveRegistry.perspectives$; this.parts$ = this._partRegistry.parts$; this.views$ = this._viewRegistry.views$; + this.theme$ = this._workbenchThemeSwitcher.theme$; } /** @inheritDoc */ @@ -129,4 +133,9 @@ export class ɵWorkbenchService implements WorkbenchService { }, }; } + + /** @inheritDoc */ + public switchTheme(theme: string): Promise { + return this._workbenchThemeSwitcher.switchTheme(theme); + } } diff --git a/projects/scion/workbench/theme/_colors.scss b/projects/scion/workbench/theme/_colors.scss deleted file mode 100644 index 864052835..000000000 --- a/projects/scion/workbench/theme/_colors.scss +++ /dev/null @@ -1,31 +0,0 @@ -$background-color: #FEFEFE; - -$viewlistitem-border-color: rgba(102, 102, 102, .3); - -$error-color: rgba(255, 55, 55, 1); -$info-color: rgba(19, 138, 203, 1); -$warn-color: rgba(240, 173, 78, 1); - -/* - * Part - */ -$part_tabbar-bgcolor: #333333; -$part_tabbar_separator-color: #666666; -$part_tab-fgcolor: #EFEFEF; -$part_tab-active-fgcolor: #252526; -$part_tab-active-bgcolor: $background-color; -$part_sash-bgcolor: #333333; - -/* - * Notification - */ -$notification-fgcolor: #333333; -$notification-bgcolor: $background-color; -$notification-border-color: rgb(204, 204, 204); - -/* - * Message Box - */ -$messagebox-fgcolor: #333333; -$messagebox-bgcolor: $background-color; -$messagebox-border-color: rgb(204, 204, 204); diff --git a/projects/scion/workbench/theme/_index.scss b/projects/scion/workbench/theme/_index.scss deleted file mode 100644 index e68406996..000000000 --- a/projects/scion/workbench/theme/_index.scss +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2018-2022 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -@forward './theme'; diff --git a/projects/scion/workbench/theme/_styles.scss b/projects/scion/workbench/theme/_styles.scss deleted file mode 100644 index caed0ea1a..000000000 --- a/projects/scion/workbench/theme/_styles.scss +++ /dev/null @@ -1,8 +0,0 @@ -$topbar-height: 3.5rem; -$viewtab-height: $topbar-height; -$viewtab-min-width: 100px; -$viewtab-max-width: 200px; -$notification-width: 350px; -$notification-margin: .5em; -$messagebox-max-width: 400px; -$popup-box-shadow: 3px 10px 20px -5px rgba(0, 0, 0, 0.5); diff --git a/projects/scion/workbench/theme/_theme.scss b/projects/scion/workbench/theme/_theme.scss deleted file mode 100644 index bb997d3da..000000000 --- a/projects/scion/workbench/theme/_theme.scss +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2018-2022 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -/** - * Import from `index.scss' to style the SCION workbench. - */ - -@use '@angular/cdk'; -@use 'icons' as wb-icons; -@use 'popup-theme' as wb-popup-theme; -@use 'view-drag-theme' as wb-drag-theme; - -$theme: () !default; // use default theme if not provided a theme - -@include cdk.a11y-visually-hidden(); -@include cdk.overlay(); -@include wb-icons.icon-font($theme); -@include wb-popup-theme.popup($theme); -@include wb-drag-theme.view-tab-drag-image($theme); diff --git a/projects/scion/workbench/theme/_view-drag-theme.scss b/projects/scion/workbench/theme/_view-drag-theme.scss deleted file mode 100644 index 4e44f8555..000000000 --- a/projects/scion/workbench/theme/_view-drag-theme.scss +++ /dev/null @@ -1,21 +0,0 @@ -@use './colors' as colors; - -/** - * Provides styles for view drag and drop. - */ -@mixin view-tab-drag-image($theme) { - .wb-view-tab-drag-image { - display: grid; - position: fixed; - overflow: hidden; - user-select: none; - box-sizing: border-box; - color: colors.$part_tab-active-fgcolor; - background-color: colors.$part_tab-active-bgcolor; - pointer-events: none; - box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), - 0 8px 10px 1px rgba(0, 0, 0, 0.14), - 0 3px 14px 2px rgba(0, 0, 0, 0.12); - } -} - diff --git a/tsconfig.json b/tsconfig.json index 95fe7edf1..754191b75 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -131,6 +131,12 @@ // // "@scion/components.internal/tabbar": [ // // "../scion-toolkit/projects/scion/components.internal/tabbar/src/public_api" // // ], + // // "@scion/components.internal/toggle-button": [ + // // "../scion-toolkit/projects/scion/components.internal/toggle-button/src/public_api" + // // ], + // // "@scion/components.internal/material-icon": [ + // // "../scion-toolkit/projects/scion/components.internal/material-icon/src/public_api" + // // ], // "@angular/*": [ // "./node_modules/@angular/*" // ],