feat: build dark fantasy application shell
This commit is contained in:
13
apps/web/src/app/layout/app-shell/app-shell.component.html
Normal file
13
apps/web/src/app/layout/app-shell/app-shell.component.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="app-shell">
|
||||
<app-top-bar [character]="worldStore.character()" />
|
||||
|
||||
<div class="app-shell__content">
|
||||
<app-side-navigation />
|
||||
<main class="app-shell__main" aria-label="Spielinhalt">
|
||||
<router-outlet />
|
||||
</main>
|
||||
<app-context-panel />
|
||||
</div>
|
||||
|
||||
<app-game-footer />
|
||||
</div>
|
||||
47
apps/web/src/app/layout/app-shell/app-shell.component.scss
Normal file
47
apps/web/src/app/layout/app-shell/app-shell.component.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
:host {
|
||||
display: block;
|
||||
min-block-size: 100dvh;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
min-block-size: 100dvh;
|
||||
background: var(--ar-bg);
|
||||
}
|
||||
|
||||
.app-shell__content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(11rem, 13rem) minmax(0, 1fr) minmax(15rem, 20rem);
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.app-shell__main {
|
||||
min-inline-size: 0;
|
||||
min-block-size: 0;
|
||||
padding: var(--ar-space-5);
|
||||
border-inline: 1px solid var(--ar-border);
|
||||
background:
|
||||
linear-gradient(115deg, rgb(255 255 255 / 0.018), transparent 28%), var(--ar-panel-muted);
|
||||
}
|
||||
|
||||
@media (width < 900px) {
|
||||
.app-shell__content {
|
||||
grid-template-columns: minmax(8.5rem, 10rem) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
app-context-panel {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 620px) {
|
||||
.app-shell__content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.app-shell__main {
|
||||
min-block-size: 20rem;
|
||||
border-inline: 0;
|
||||
}
|
||||
}
|
||||
23
apps/web/src/app/layout/app-shell/app-shell.component.ts
Normal file
23
apps/web/src/app/layout/app-shell/app-shell.component.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { WorldStore } from '../../features/world/world.store';
|
||||
import { ContextPanelComponent } from '../context-panel/context-panel.component';
|
||||
import { GameFooterComponent } from '../game-footer/game-footer.component';
|
||||
import { SideNavigationComponent } from '../side-navigation/side-navigation.component';
|
||||
import { TopBarComponent } from '../top-bar/top-bar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-app-shell',
|
||||
imports: [
|
||||
RouterOutlet,
|
||||
TopBarComponent,
|
||||
SideNavigationComponent,
|
||||
GameFooterComponent,
|
||||
ContextPanelComponent,
|
||||
],
|
||||
templateUrl: './app-shell.component.html',
|
||||
styleUrl: './app-shell.component.scss',
|
||||
})
|
||||
export class AppShellComponent {
|
||||
protected readonly worldStore = inject(WorldStore);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<aside class="context-panel" aria-label="Gebietsinformationen">
|
||||
<span class="context-panel__eyebrow">Gebiet</span>
|
||||
<p class="context-panel__hint">Informationen erscheinen mit der Weltansicht.</p>
|
||||
</aside>
|
||||
@@ -0,0 +1,26 @@
|
||||
:host {
|
||||
display: block;
|
||||
background: var(--ar-panel);
|
||||
}
|
||||
|
||||
.context-panel {
|
||||
min-block-size: 100%;
|
||||
padding: var(--ar-space-5);
|
||||
border-inline-start: 1px solid var(--ar-border);
|
||||
background: linear-gradient(180deg, rgb(255 255 255 / 0.025), transparent 16%), var(--ar-panel);
|
||||
}
|
||||
|
||||
.context-panel__eyebrow {
|
||||
display: block;
|
||||
padding-block-end: var(--ar-space-3);
|
||||
border-block-end: 1px solid var(--ar-border);
|
||||
color: var(--ar-gold);
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.context-panel__hint {
|
||||
color: var(--ar-text-muted);
|
||||
font-size: var(--ar-font-sm);
|
||||
line-height: 1.55;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-context-panel',
|
||||
templateUrl: './context-panel.component.html',
|
||||
styleUrl: './context-panel.component.scss',
|
||||
})
|
||||
export class ContextPanelComponent {}
|
||||
@@ -0,0 +1,5 @@
|
||||
<footer class="game-footer">
|
||||
<span class="game-footer__status"><span aria-hidden="true">●</span> Verbindung bereit</span>
|
||||
<span class="game-footer__ornament" aria-hidden="true"></span>
|
||||
<span>Aschenfelder</span>
|
||||
</footer>
|
||||
@@ -0,0 +1,30 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.game-footer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
min-block-size: 3.3rem;
|
||||
padding-inline: var(--ar-space-5);
|
||||
border-block-start: 1px solid var(--ar-border-highlight);
|
||||
color: var(--ar-text-muted);
|
||||
background: var(--ar-panel);
|
||||
font-size: var(--ar-font-sm);
|
||||
}
|
||||
|
||||
.game-footer > :last-child {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.game-footer__status span {
|
||||
color: var(--ar-success);
|
||||
}
|
||||
|
||||
.game-footer__ornament {
|
||||
inline-size: 0.8rem;
|
||||
block-size: 0.8rem;
|
||||
transform: rotate(45deg);
|
||||
border: 1px solid var(--ar-gold);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-game-footer',
|
||||
templateUrl: './game-footer.component.html',
|
||||
styleUrl: './game-footer.component.scss',
|
||||
})
|
||||
export class GameFooterComponent {}
|
||||
@@ -0,0 +1,53 @@
|
||||
<nav class="side-navigation" aria-label="Spielnavigation">
|
||||
<button
|
||||
class="side-navigation__item side-navigation__item--active"
|
||||
type="button"
|
||||
routerLink="/world"
|
||||
data-navigation="world"
|
||||
aria-current="page"
|
||||
>
|
||||
<img src="/images/hud/MapsIcon.png" alt="" />
|
||||
<span>Karte</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="side-navigation__item"
|
||||
type="button"
|
||||
data-navigation="hunt"
|
||||
disabled
|
||||
aria-label="Jagd ist noch nicht verfügbar"
|
||||
>
|
||||
<img src="/images/hud/HuntIcon.png" alt="" />
|
||||
<span>Jagd</span>
|
||||
</button>
|
||||
<button
|
||||
class="side-navigation__item"
|
||||
type="button"
|
||||
data-navigation="quests"
|
||||
disabled
|
||||
aria-label="Quests sind noch nicht verfügbar"
|
||||
>
|
||||
<img src="/images/hud/QuestsIcon.png" alt="" />
|
||||
<span>Quests</span>
|
||||
</button>
|
||||
<button
|
||||
class="side-navigation__item"
|
||||
type="button"
|
||||
data-navigation="inventory"
|
||||
disabled
|
||||
aria-label="Inventar ist noch nicht verfügbar"
|
||||
>
|
||||
<img src="/images/hud/InventoryIcon.png" alt="" />
|
||||
<span>Inventar</span>
|
||||
</button>
|
||||
<button
|
||||
class="side-navigation__item"
|
||||
type="button"
|
||||
data-navigation="character"
|
||||
disabled
|
||||
aria-label="Charakter ist noch nicht verfügbar"
|
||||
>
|
||||
<img src="/images/hud/CharacterIcon.png" alt="" />
|
||||
<span>Charakter</span>
|
||||
</button>
|
||||
</nav>
|
||||
@@ -0,0 +1,73 @@
|
||||
:host {
|
||||
display: block;
|
||||
background: var(--ar-panel);
|
||||
}
|
||||
|
||||
.side-navigation {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
padding-block: var(--ar-space-3);
|
||||
}
|
||||
|
||||
.side-navigation__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ar-space-3);
|
||||
inline-size: 100%;
|
||||
min-block-size: 4.3rem;
|
||||
padding: var(--ar-space-3) var(--ar-space-4);
|
||||
border: 0;
|
||||
border-block-end: 1px solid color-mix(in srgb, var(--ar-border) 65%, transparent);
|
||||
border-inline-start: 3px solid transparent;
|
||||
color: var(--ar-text-muted);
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
text-align: start;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
color var(--ar-motion-fast),
|
||||
background var(--ar-motion-fast),
|
||||
border-color var(--ar-motion-fast);
|
||||
}
|
||||
|
||||
.side-navigation__item img {
|
||||
inline-size: 2.25rem;
|
||||
block-size: 2.25rem;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.side-navigation__item--active {
|
||||
border-inline-start-color: var(--ar-blue);
|
||||
color: var(--ar-text);
|
||||
background: linear-gradient(90deg, rgb(92 169 216 / 0.2), transparent);
|
||||
box-shadow: inset 0 0 1.25rem rgb(92 169 216 / 0.08);
|
||||
}
|
||||
|
||||
.side-navigation__item:not(:disabled):hover,
|
||||
.side-navigation__item:not(:disabled):focus-visible {
|
||||
color: var(--ar-text);
|
||||
background: rgb(255 255 255 / 0.04);
|
||||
}
|
||||
|
||||
.side-navigation__item:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.52;
|
||||
}
|
||||
|
||||
@media (width < 620px) {
|
||||
.side-navigation {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.side-navigation__item {
|
||||
justify-content: center;
|
||||
min-block-size: 3.5rem;
|
||||
padding: var(--ar-space-2);
|
||||
}
|
||||
|
||||
.side-navigation__item span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-side-navigation',
|
||||
imports: [RouterLink],
|
||||
templateUrl: './side-navigation.component.html',
|
||||
styleUrl: './side-navigation.component.scss',
|
||||
})
|
||||
export class SideNavigationComponent {}
|
||||
25
apps/web/src/app/layout/top-bar/top-bar.component.html
Normal file
25
apps/web/src/app/layout/top-bar/top-bar.component.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<header class="top-bar">
|
||||
<div class="top-bar__character">
|
||||
<img class="top-bar__portrait" src="/images/hud/CharacterIcon.png" alt="" />
|
||||
@if (character(); as character) {
|
||||
<div class="top-bar__identity">
|
||||
<span class="top-bar__name">{{ character.name }}</span>
|
||||
<span class="top-bar__level">Stufe {{ character.level }}</span>
|
||||
</div>
|
||||
<div class="top-bar__health" aria-label="Lebenspunkte">
|
||||
<span>{{ character.currentHp }} / {{ character.maxHp }}</span>
|
||||
<span class="top-bar__health-track" aria-hidden="true">
|
||||
<span
|
||||
class="top-bar__health-value"
|
||||
[style.inline-size.%]="(character.currentHp / character.maxHp) * 100"
|
||||
></span>
|
||||
</span>
|
||||
</div>
|
||||
} @else {
|
||||
<span class="top-bar__loading">Charakterdaten werden geladen</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="top-bar__ornament" aria-hidden="true"></div>
|
||||
<span class="top-bar__world-name">Ashen Realms</span>
|
||||
</header>
|
||||
101
apps/web/src/app/layout/top-bar/top-bar.component.scss
Normal file
101
apps/web/src/app/layout/top-bar/top-bar.component.scss
Normal file
@@ -0,0 +1,101 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(18rem, 1fr) auto minmax(18rem, 1fr);
|
||||
align-items: center;
|
||||
min-block-size: 5.6rem;
|
||||
padding: var(--ar-space-2) var(--ar-space-5);
|
||||
border-block: 1px solid var(--ar-border-highlight);
|
||||
background: linear-gradient(180deg, rgb(255 255 255 / 0.035), transparent 62%), var(--ar-panel);
|
||||
box-shadow: var(--ar-shadow-raised);
|
||||
}
|
||||
|
||||
.top-bar__character {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ar-space-3);
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.top-bar__portrait {
|
||||
inline-size: 4.25rem;
|
||||
block-size: 4.25rem;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--ar-border-highlight);
|
||||
border-radius: 50%;
|
||||
background: #080909;
|
||||
}
|
||||
|
||||
.top-bar__identity,
|
||||
.top-bar__health {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.top-bar__name,
|
||||
.top-bar__world-name {
|
||||
color: var(--ar-text);
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.top-bar__level,
|
||||
.top-bar__loading {
|
||||
color: var(--ar-text-muted);
|
||||
font-size: var(--ar-font-sm);
|
||||
}
|
||||
|
||||
.top-bar__health {
|
||||
min-inline-size: 8rem;
|
||||
color: var(--ar-text);
|
||||
font-size: var(--ar-font-sm);
|
||||
}
|
||||
|
||||
.top-bar__health-track {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
block-size: 0.45rem;
|
||||
border: 1px solid #6d2f2a;
|
||||
background: #23100f;
|
||||
}
|
||||
|
||||
.top-bar__health-value {
|
||||
display: block;
|
||||
block-size: 100%;
|
||||
background: linear-gradient(90deg, #8e231f, #c74b42);
|
||||
transition: inline-size var(--ar-motion-base);
|
||||
}
|
||||
|
||||
.top-bar__ornament {
|
||||
inline-size: 1.5rem;
|
||||
block-size: 1.5rem;
|
||||
transform: rotate(45deg);
|
||||
border: 1px solid var(--ar-border-highlight);
|
||||
box-shadow: 0 0 0 0.25rem var(--ar-panel);
|
||||
}
|
||||
|
||||
.top-bar__world-name {
|
||||
justify-self: end;
|
||||
color: var(--ar-gold);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (width < 620px) {
|
||||
.top-bar {
|
||||
grid-template-columns: 1fr auto;
|
||||
padding-inline: var(--ar-space-3);
|
||||
}
|
||||
|
||||
.top-bar__ornament,
|
||||
.top-bar__world-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-bar__health {
|
||||
min-inline-size: 5rem;
|
||||
}
|
||||
}
|
||||
11
apps/web/src/app/layout/top-bar/top-bar.component.ts
Normal file
11
apps/web/src/app/layout/top-bar/top-bar.component.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
import { CharacterResponse } from '../../core/api/game-api.models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-top-bar',
|
||||
templateUrl: './top-bar.component.html',
|
||||
styleUrl: './top-bar.component.scss',
|
||||
})
|
||||
export class TopBarComponent {
|
||||
readonly character = input<CharacterResponse | null>(null);
|
||||
}
|
||||
Reference in New Issue
Block a user