feat(portal): basis for drawer and cards (#2370)

This commit is contained in:
Alex Inkin
2023-07-21 08:17:32 +08:00
committed by GitHub
parent 9c0c6c1bd6
commit 4204b4af90
31 changed files with 474 additions and 62 deletions

View File

@@ -59,3 +59,32 @@ tui-hint[data-appearance='onDark'] {
color: var(--tui-link-hover) !important;
}
}
[tuiWrapper][data-appearance='drawer'] {
// TODO: Theme
background: rgb(81 80 83 / 86%);
border-radius: 10rem;
&._focused::after {
color: var(--tui-primary);
}
}
tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
border: 0;
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
// TODO: Replace --tui-elevation-02 when Taiga UI is updated
background: rgb(63 63 63 / 95%);
tui-opt-group {
&::before {
background: var(--tui-clear);
box-shadow: 1rem 0 var(--tui-clear), -1rem 0 var(--tui-clear);
padding-top: 0.375rem;
}
&::after {
display: none;
}
}
}

View File

@@ -11,6 +11,7 @@
(ionSplitPaneVisible)="splitPaneVisible($event)"
>
<ion-menu
*ngIf="navigation$ | async"
contentId="main-content"
type="overlay"
side="start"

View File

@@ -0,0 +1,39 @@
<span class="link">
<img alt="" class="icon" [src]="appCard.icon" />
<label ticker class="title">{{ appCard.title }}</label>
</span>
<span class="side">
<tui-hosted-dropdown [content]="content" (click.stop.prevent)="(0)">
<button
tuiIconButton
appearance="outline"
shape="rounded"
size="xs"
icon="tuiIconMoreHorizontal"
>
Actions
</button>
<ng-template #content>
<!-- TODO: Move menu to a separate component -->
<tui-data-list>
<h3 class="menu-title">{{ appCard.title }}</h3>
<tui-opt-group label="LAUNCH">
<button tuiOption class="menu-item">
<tui-svg src="tuiIconLogOut" class="menu-icon"></tui-svg>
Tor
</button>
</tui-opt-group>
<tui-opt-group label="MANAGE">
<button tuiOption class="menu-item">
<tui-svg src="tuiIconSliders" class="menu-icon"></tui-svg>
Console
</button>
<button tuiOption class="menu-item">
<tui-svg src="tuiIconX" class="menu-icon"></tui-svg>
Remove from desktop
</button>
</tui-opt-group>
</tui-data-list>
</ng-template>
</tui-hosted-dropdown>
</span>

View File

@@ -0,0 +1,62 @@
:host {
display: flex;
height: 5.5rem;
width: 12.5rem;
border-radius: var(--tui-radius-l);
overflow: hidden;
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
// TODO: Theme
background: rgb(111 109 109 / 90%);
}
.link {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
gap: 0.25rem;
padding: 0 0.5rem;
font: var(--tui-font-text-m);
white-space: nowrap;
overflow: hidden;
}
.icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 100%;
box-shadow: 0.25rem 0.25rem 0.25rem rgb(0 0 0 / 25%);
}
.title {
max-width: 100%;
}
.side {
width: 3rem;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
// TODO: Theme
background: #4b4a4a;
}
.menu-title {
margin: 0;
padding: 0 0.5rem 0.25rem;
white-space: nowrap;
font: var(--tui-font-text-l);
font-weight: bold;
}
.menu-item {
justify-content: flex-start;
gap: 0.75rem;
}
.menu-icon {
opacity: var(--tui-disabled-opacity);
}

View File

@@ -0,0 +1,46 @@
import {
ChangeDetectionStrategy,
Component,
HostListener,
inject,
Input,
} from '@angular/core'
import { RouterLink } from '@angular/router'
import { TickerModule } from '@start9labs/shared'
import {
TuiButtonModule,
TuiDataListModule,
TuiHostedDropdownModule,
TuiSvgModule,
} from '@taiga-ui/core'
import {
NavigationItem,
NavigationService,
} from '../navigation/navigation.service'
@Component({
selector: '[appCard]',
templateUrl: 'card.component.html',
styleUrls: ['card.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
RouterLink,
TuiButtonModule,
TuiHostedDropdownModule,
TuiDataListModule,
TuiSvgModule,
TickerModule,
],
})
export class CardComponent {
private readonly navigation = inject(NavigationService)
@Input({ required: true })
appCard!: NavigationItem
@HostListener('click')
onClick() {
this.navigation.addTab(this.appCard)
}
}

View File

@@ -0,0 +1,38 @@
<div class="content" (tuiActiveZoneChange)="open = $event">
<button class="toggle" (click)="open = !open" (mousedown.prevent)="(0)">
<tui-svg src="tuiIconArrowUpCircleLarge" class="icon"></tui-svg>
Toggle drawer
</button>
<tui-input
class="search"
tuiTextfieldAppearance="drawer"
tuiTextfieldSize="m"
tuiTextfieldIconLeft="tuiIconSearchLarge"
[tuiTextfieldLabelOutside]="true"
[(ngModel)]="search"
>
Enter service name
</tui-input>
<h2 class="title">System Utilities</h2>
<div class="items">
<a
*ngFor="let item of system | tuiFilter : bySearch : search; empty: empty"
[appCard]="item"
[routerLink]="item.routerLink"
(click)="open = false"
></a>
</div>
<h2 class="title">Installed services</h2>
<div class="items">
<a
*ngFor="
let item of (services$ | async) || [] | tuiFilter : bySearch : search;
empty: empty
"
[appCard]="item"
[routerLink]="item.routerLink"
(click)="open = false"
></a>
</div>
<ng-template #empty>Nothing found</ng-template>
</div>

View File

@@ -0,0 +1,70 @@
@import '@taiga-ui/core/styles/taiga-ui-local';
:host {
@include transition(top);
position: absolute;
top: 100%;
left: 0;
width: 100%;
min-height: calc(100% - 10.25rem);
display: flex;
flex-direction: column;
// TODO: Theme
background: #2d2d2d;
color: #fff;
&._open {
top: 10.25rem;
}
}
.content {
flex: 1;
background: inherit;
}
.toggle {
position: absolute;
top: -2.5rem;
height: 2.5rem;
width: 25rem;
max-width: 100vw;
left: 50%;
background: inherit;
color: inherit;
text-align: center;
font-size: 0;
transform: translateX(-50%);
border-top-left-radius: var(--tui-radius-xl);
border-top-right-radius: var(--tui-radius-xl);
}
.icon {
@include transition(transform);
:host._open & {
transform: rotate(180deg);
}
}
.search {
max-width: 41rem;
margin: 6rem auto 0;
}
.title {
margin: 5rem 0 1.25rem;
text-align: center;
text-transform: uppercase;
font: var(--tui-font-text-xl);
}
.items {
display: flex;
gap: 2rem;
padding: 2rem;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}

View File

@@ -0,0 +1,57 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
HostBinding,
inject,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { RouterLink } from '@angular/router'
import {
TUI_DEFAULT_MATCHER,
TuiActiveZoneModule,
TuiFilterPipeModule,
TuiForModule,
} from '@taiga-ui/cdk'
import { TuiSvgModule, TuiTextfieldControllerModule } from '@taiga-ui/core'
import { TuiInputModule } from '@taiga-ui/kit'
import { map } from 'rxjs'
import { CardComponent } from '../card/card.component'
import { NavigationItem } from '../navigation/navigation.service'
import { ServicesService } from '../../services/services.service'
import { SYSTEM_UTILITIES } from './drawer.const'
import { toNavigationItem } from '../../utils/to-navigation-item'
@Component({
selector: 'app-drawer',
templateUrl: 'drawer.component.html',
styleUrls: ['drawer.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
FormsModule,
TuiSvgModule,
TuiActiveZoneModule,
TuiInputModule,
TuiTextfieldControllerModule,
TuiForModule,
TuiFilterPipeModule,
CardComponent,
RouterLink,
],
})
export class DrawerComponent {
@HostBinding('class._open')
open = false
search = ''
readonly system = SYSTEM_UTILITIES
readonly services$ = inject(ServicesService).pipe(
map(services => services.map(toNavigationItem)),
)
readonly bySearch = (item: NavigationItem, search: string): boolean =>
search.length < 2 || TUI_DEFAULT_MATCHER(item.title, search)
}

View File

@@ -0,0 +1,24 @@
import { NavigationItem } from '../navigation/navigation.service'
export const SYSTEM_UTILITIES: readonly NavigationItem[] = [
{
title: 'Devices',
routerLink: 'devices',
icon: 'assets/img/icon_transparent.png',
},
{
title: 'Metrics',
routerLink: 'metrics',
icon: 'assets/img/icon_transparent.png',
},
{
title: 'User manual',
routerLink: 'manual',
icon: 'assets/img/icon_transparent.png',
},
{
title: 'Snek',
routerLink: 'snek',
icon: 'assets/img/icon_transparent.png',
},
]

View File

@@ -2,9 +2,10 @@
display: flex;
align-items: center;
height: 4.5rem;
background: rgb(51 51 51 / 74%);
padding: 0 1rem 0 2rem;
font-size: 1.5rem;
// TODO: Theme
background: rgb(51 51 51 / 74%);
}
.toolbar {

View File

@@ -1,6 +1,6 @@
<a
class="tab"
routerLink="services"
routerLink="desktop"
routerLinkActive="tab_active"
[routerLinkActiveOptions]="{ exact: true }"
>

View File

@@ -1,6 +1,7 @@
:host {
height: 3rem;
display: flex;
// TODO: Theme
background: rgb(97 95 95 / 75%);
}
@@ -12,6 +13,7 @@
width: 7.5rem;
&_active {
// TODO: Theme
background: #373a3f;
}
}

View File

@@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { NavigationItem } from '../components/navigation/navigation.service'
import { toNavigationItem } from '../utils/to-navigation-item'
@Pipe({
name: 'toNavigationItem',
standalone: true,
})
export class ToNavigationItemPipe implements PipeTransform {
transform(service: PackageDataEntry): NavigationItem {
return toNavigationItem(service)
}
}

View File

@@ -3,3 +3,4 @@
<main>
<router-outlet></router-outlet>
</main>
<app-drawer></app-drawer>

View File

@@ -1,4 +1,5 @@
:host {
// TODO: Theme
background: url(/assets/img/background_dark.jpeg);
background-size: cover;
}

View File

@@ -1,8 +1,15 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { tuiDropdownOptionsProvider } from '@taiga-ui/core'
@Component({
templateUrl: 'portal.component.html',
styleUrls: ['portal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
// TODO: Move to global
tuiDropdownOptionsProvider({
appearance: 'start-os',
}),
],
})
export class PortalComponent {}

View File

@@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router'
import { HeaderComponent } from './components/header/header.component'
import { PortalComponent } from './portal.component'
import { NavigationComponent } from './components/navigation/navigation.component'
import { DrawerComponent } from './components/drawer/drawer.component'
const ROUTES: Routes = [
{
@@ -10,10 +11,15 @@ const ROUTES: Routes = [
component: PortalComponent,
children: [
{
redirectTo: 'services',
redirectTo: 'desktop',
pathMatch: 'full',
path: '',
},
{
path: 'desktop',
loadChildren: () =>
import('./routes/desktop/desktop.module').then(m => m.DesktopModule),
},
{
path: 'services',
loadChildren: () =>
@@ -30,6 +36,7 @@ const ROUTES: Routes = [
RouterModule.forChild(ROUTES),
HeaderComponent,
NavigationComponent,
DrawerComponent,
],
declarations: [PortalComponent],
exports: [PortalComponent],

View File

@@ -0,0 +1,5 @@
<a
*ngFor="let service of services$ | async"
[appCard]="service | toNavigationItem"
[routerLink]="(service | toNavigationItem).routerLink"
></a>

View File

@@ -0,0 +1,11 @@
:host {
display: flex;
align-items: center;
align-content: center;
justify-content: center;
flex-wrap: wrap;
height: 100%;
max-width: 56rem;
margin: 0 auto;
gap: 2rem;
}

View File

@@ -0,0 +1,12 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ServicesService } from '../../services/services.service'
@Component({
templateUrl: 'desktop.component.html',
styleUrls: ['desktop.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DesktopComponent {
// TODO: Only show services added to desktop
readonly services$ = inject(ServicesService)
}

View File

@@ -0,0 +1,25 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { DesktopComponent } from './desktop.component'
import { CardComponent } from '../../components/card/card.component'
import { ToNavigationItemPipe } from '../../pipes/to-navigation-item'
const ROUTES: Routes = [
{
path: '',
component: DesktopComponent,
},
]
@NgModule({
imports: [
CommonModule,
CardComponent,
ToNavigationItemPipe,
RouterModule.forChild(ROUTES),
],
declarations: [DesktopComponent],
exports: [DesktopComponent],
})
export class DesktopModule {}

View File

@@ -4,7 +4,7 @@ import { getPkgId } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import { tap } from 'rxjs'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { NavigationService } from '../../../components/navigation/navigation.service'
import { NavigationService } from '../../components/navigation/navigation.service'
@Component({
templateUrl: 'service.component.html',
@@ -27,7 +27,7 @@ export class ServiceComponent {
} else {
this.navigation.addTab({
title: pkg.manifest.title,
routerLink: ['services', pkg.manifest.id].join('/'),
routerLink: `/portal/services/${pkg.manifest.id}`,
icon: pkg.icon,
})
}

View File

@@ -1,18 +0,0 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { ServiceComponent } from './service.component'
const ROUTES: Routes = [
{
path: '',
component: ServiceComponent,
},
]
@NgModule({
imports: [CommonModule, RouterModule.forChild(ROUTES)],
declarations: [ServiceComponent],
exports: [ServiceComponent],
})
export class ServiceModule {}

View File

@@ -1,7 +0,0 @@
<a
*ngFor="let service of services$ | async"
[routerLink]="service.manifest.id"
(click)="onClick(service)"
>
{{ service.manifest.title }}
</a>

View File

@@ -1,23 +0,0 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { NavigationService } from '../../components/navigation/navigation.service'
import { ServicesService } from './services.service'
@Component({
templateUrl: 'services.component.html',
styleUrls: ['services.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ServicesComponent {
private readonly navigation = inject(NavigationService)
readonly services$ = inject(ServicesService)
onClick({ manifest, icon }: PackageDataEntry) {
this.navigation.addTab({
title: manifest.title,
routerLink: ['services', manifest.id].join('/'),
icon,
})
}
}

View File

@@ -1,23 +1,18 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { ServicesComponent } from './services.component'
import { ServiceComponent } from './service.component'
const ROUTES: Routes = [
{
path: '',
component: ServicesComponent,
},
{
path: ':pkgId',
loadChildren: () =>
import('./service/service.module').then(m => m.ServiceModule),
component: ServiceComponent,
},
]
@NgModule({
imports: [CommonModule, RouterModule.forChild(ROUTES)],
declarations: [ServicesComponent],
exports: [ServicesComponent],
declarations: [ServiceComponent],
exports: [ServiceComponent],
})
export class ServicesModule {}

View File

@@ -0,0 +1,13 @@
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { NavigationItem } from '../components/navigation/navigation.service'
export function toNavigationItem({
manifest,
icon,
}: PackageDataEntry): NavigationItem {
return {
title: manifest.title,
routerLink: `/portal/services/${manifest.id}`,
icon,
}
}