diff --git a/web/projects/ui/src/app/app.config.ts b/web/projects/ui/src/app/app.config.ts index 8b4436073..3bd32bd93 100644 --- a/web/projects/ui/src/app/app.config.ts +++ b/web/projects/ui/src/app/app.config.ts @@ -37,7 +37,7 @@ import { VERSION, WorkspaceConfig, } from '@start9labs/shared' -import { tuiObfuscateOptionsProvider } from '@taiga-ui/cdk' +import { TUI_WINDOW_SIZE, tuiObfuscateOptionsProvider } from '@taiga-ui/cdk' import { provideTaiga, TUI_DIALOGS_CLOSE, @@ -67,11 +67,12 @@ import { PATCH_CACHE, PatchDbSource, } from 'src/app/services/patch-db/patch-db-source' +import { PluginsService } from 'src/app/services/plugins.service' import { StateService } from 'src/app/services/state.service' import { StorageService } from 'src/app/services/storage.service' import { - DateTransformer, DatetimeTransformer, + DateTransformer, } from 'src/app/utils/value-transformers' import { environment } from 'src/environments/environment' @@ -185,5 +186,9 @@ export const APP_CONFIG: ApplicationConfig = { desktopLarge: Infinity, }, }, + { + provide: TUI_WINDOW_SIZE, + useExisting: PluginsService, + }, ], } diff --git a/web/projects/ui/src/app/routes/portal/components/header/header.component.ts b/web/projects/ui/src/app/routes/portal/components/header/header.component.ts index 28ef803f7..d24076f51 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/header.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/header.component.ts @@ -16,6 +16,7 @@ import { HeaderStatusComponent } from './status.component' template: `
+
diff --git a/web/projects/ui/src/app/routes/portal/components/plugins.component.ts b/web/projects/ui/src/app/routes/portal/components/plugins.component.ts new file mode 100644 index 000000000..c14e6e2ba --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/plugins.component.ts @@ -0,0 +1,100 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { WA_IS_MOBILE } from '@ng-web-apis/platform' +import { TuiAnimated } from '@taiga-ui/cdk' +import { TuiButton, TuiPopup } from '@taiga-ui/core' +import { ResizerComponent } from 'src/app/routes/portal/components/resizer.component' +import { PluginsService } from 'src/app/services/plugins.service' + +@Component({ + selector: 'app-plugins', + template: ` + + + + `, + styles: ` + :host { + float: inline-end; + margin-inline-end: 0.5rem; + } + + [tuiIconButton] { + background: transparent; + } + + aside { + position: fixed; + inset: 0 0 0 calc(320px + (100% - 640px) * var(--plugins)); + display: flex; + place-content: center; + place-items: center; + margin: var(--bumper) var(--bumper) var(--bumper) 0; + background: color-mix(in hsl, var(--start9-base-2) 75%, transparent); + background-image: linear-gradient( + transparent, + var(--tui-background-base) + ); + backdrop-filter: blur(1rem); + border-radius: var(--bumper); + + --tui-from: translateX(100%); + + &._mobile { + inset-inline-start: 20%; + box-shadow: + inset 0 1px rgba(255, 255, 255, 0.25), + 0 0 0 100vh rgb(0 0 0 / 50%); + + &::before { + content: ''; + position: fixed; + inset: -100vh; + } + + &.tui-enter, + &.tui-leave { + animation-name: tuiSlide, tuiFade; + } + } + + &.tui-enter, + &.tui-leave { + animation-name: tuiSlide; + } + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [FormsModule, TuiButton, ResizerComponent, TuiPopup, TuiAnimated], +}) +export class PluginsComponent { + protected readonly mobile = inject(WA_IS_MOBILE) + protected readonly service = inject(PluginsService) + + protected onClick(layerX: number) { + if (layerX < 0 && this.mobile) { + this.service.enabled.set(false) + } + } +} diff --git a/web/projects/ui/src/app/routes/portal/components/resizer.component.ts b/web/projects/ui/src/app/routes/portal/components/resizer.component.ts new file mode 100644 index 000000000..e02e0f89d --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/resizer.component.ts @@ -0,0 +1,56 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' + +@Component({ + selector: 'input[type="range"][appResizer]', + template: '', + styles: ` + @use '@taiga-ui/styles/utils' as taiga; + + :host { + @include taiga.transition(color); + + position: fixed; + inset: 0 calc(320px - var(--bumper)) 0 calc(320px - 2 * var(--bumper)); + appearance: none; + pointer-events: none; + background: none; + color: transparent; + outline: none; + cursor: ew-resize; + + &:hover { + color: var(--tui-background-neutral-1-hover); + } + + &::-webkit-slider-runnable-track { + block-size: 100%; + } + + &::-webkit-slider-thumb { + block-size: 100%; + inline-size: calc(var(--bumper) * 3); + padding: var(--bumper); + appearance: none; + background: currentColor; + background-clip: content-box; + border: none; + pointer-events: auto; + border-radius: var(--tui-radius-l); + } + + &::-moz-range-thumb { + block-size: 100%; + inline-size: calc(var(--bumper) * 3); + padding: var(--bumper); + appearance: none; + background: currentColor; + background-clip: content-box; + border: none; + pointer-events: auto; + border-radius: var(--tui-radius-l); + } + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ResizerComponent {} diff --git a/web/projects/ui/src/app/routes/portal/portal.component.ts b/web/projects/ui/src/app/routes/portal/portal.component.ts index 91b861124..073904924 100644 --- a/web/projects/ui/src/app/routes/portal/portal.component.ts +++ b/web/projects/ui/src/app/routes/portal/portal.component.ts @@ -6,6 +6,7 @@ import { } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' import { RouterOutlet } from '@angular/router' +import { WA_IS_MOBILE } from '@ng-web-apis/platform' import { ErrorService } from '@start9labs/shared' import { TuiButton, @@ -21,15 +22,17 @@ import { TuiProgress, } from '@taiga-ui/kit' import { PatchDB } from 'patch-db-client' +import { PluginsComponent } from 'src/app/routes/portal/components/plugins.component' import { TabsComponent } from 'src/app/routes/portal/components/tabs.component' import { ApiService } from 'src/app/services/api/embassy-api.service' import { OSService } from 'src/app/services/os.service' import { DataModel } from 'src/app/services/patch-db/data-model' +import { PluginsService } from 'src/app/services/plugins.service' import { HeaderComponent } from './components/header/header.component' @Component({ template: ` -
{{ name() }}
+
@@ -67,19 +70,47 @@ import { HeaderComponent } from './components/header/header.component' styles: ` @use '@taiga-ui/styles/utils' as taiga; + @keyframes open { + from { + inline-size: 100%; + } + + to { + inline-size: calc(320px + (100% - 640px) * var(--plugins)); + } + } + :host { - height: 100%; + @include taiga.transition(inline-size); + + block-size: 100%; + inline-size: 100%; display: flex; flex-direction: column; - // @TODO Theme - background: url(/assets/img/background_dark.jpeg) fixed center/cover; - &::before { + &._plugins { + inline-size: calc(320px + (100% - 640px) * var(--plugins)); + animation: open var(--tui-duration) ease-in-out; + transition: none; + + app-tabs { + inline-size: calc(100% - var(--bumper)); + } + } + + &::before, + &::after { content: ''; position: fixed; inset: 0; backdrop-filter: blur(0.5rem); } + + &::after { + z-index: -1; + // @TODO Theme + background: url(/assets/img/background_dark.jpeg) fixed center/cover; + } } main { @@ -101,6 +132,10 @@ import { HeaderComponent } from './components/header/header.component' text-wrap: balance; } `, + host: { + '[class._plugins]': '!mobile && plugins.enabled()', + '[style.--plugins]': 'plugins.size() / 100', + }, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ RouterOutlet, @@ -114,6 +149,7 @@ import { HeaderComponent } from './components/header/header.component' TuiButton, TuiPopup, TuiCell, + PluginsComponent, ], }) export class PortalComponent { @@ -122,6 +158,8 @@ export class PortalComponent { private readonly patch = inject>(PatchDB) private readonly api = inject(ApiService) + readonly mobile = inject(WA_IS_MOBILE) + readonly plugins = inject(PluginsService) readonly name = toSignal(this.patch.watch$('serverInfo', 'name')) readonly update = toSignal(inject(OSService).updating$) readonly bar = signal(true) diff --git a/web/projects/ui/src/app/services/plugins.service.ts b/web/projects/ui/src/app/services/plugins.service.ts new file mode 100644 index 000000000..e1a6f58ea --- /dev/null +++ b/web/projects/ui/src/app/services/plugins.service.ts @@ -0,0 +1,37 @@ +import { inject, Injectable, INJECTOR, Injector, signal } from '@angular/core' +import { toObservable } from '@angular/core/rxjs-interop' +import { WA_IS_MOBILE } from '@ng-web-apis/platform' +import { TUI_WINDOW_SIZE } from '@taiga-ui/cdk' +import { TUI_MEDIA } from '@taiga-ui/core' +import { combineLatest, map, Observable } from 'rxjs' + +@Injectable({ providedIn: 'root' }) +export class PluginsService extends Observable { + public readonly enabled = signal(false) + public readonly size = signal(100) + + // @ts-expect-error triggering TUI_WINDOW_SIZE default factory + private readonly window = Injector.create({ + parent: inject(INJECTOR), + providers: [{ provide: TUI_WINDOW_SIZE }], + }).get(TUI_WINDOW_SIZE) + + private readonly media = inject(TUI_MEDIA) + private readonly stream = inject(WA_IS_MOBILE) + ? this.window + : combineLatest([ + this.window, + toObservable(this.size), + toObservable(this.enabled), + ]).pipe( + map(([window, size, enabled]) => + window.width < this.media.mobile || !enabled + ? window + : { ...window, width: 320 + (window.width - 640) * (size / 100) }, + ), + ) + + constructor() { + super(subscriber => this.stream.subscribe(subscriber)) + } +}