feat: build dark fantasy application shell

This commit is contained in:
Bastian Wagner
2026-08-18 22:36:21 +02:00
parent 6304703d67
commit b9e5c66c8e
28 changed files with 633 additions and 362 deletions

View 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>

View 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;
}
}

View 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);
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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 {}

View 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>

View 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;
}
}

View 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);
}