fix: optimize shell accessibility and assets
This commit is contained in:
@@ -45,3 +45,30 @@ The Browser plugin is not available in this session. The repository has no insta
|
|||||||
|
|
||||||
- The world route placeholder contains no scene, nodes, API loading, or travel panel; Task 9 owns those surfaces.
|
- The world route placeholder contains no scene, nodes, API loading, or travel panel; Task 9 owns those surfaces.
|
||||||
- The context panel is only a reusable framed shell until Task 9 supplies world data.
|
- The context panel is only a reusable framed shell until Task 9 supplies world data.
|
||||||
|
|
||||||
|
## Review fix round 1
|
||||||
|
|
||||||
|
### Accessibility and responsive behavior
|
||||||
|
|
||||||
|
- The active Karte button now has the explicit accessible name `Karte`, so its label remains available when the compact viewport hides visible navigation text.
|
||||||
|
- At widths below 900px, the context panel now reflows into a full-width row below main content instead of being hidden. At widths below 620px it remains in DOM order after main content.
|
||||||
|
|
||||||
|
### Runtime HUD derivatives
|
||||||
|
|
||||||
|
The five original 1254 x 1254 user assets remain unmodified. The shell now loads deterministic 128 x 128 PNG derivatives from `images/hud/runtime/`; each was resized locally with high-quality bicubic interpolation and no creative image change.
|
||||||
|
|
||||||
|
| Asset | Original bytes | Runtime derivative bytes |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| CharacterIcon | 2,283,892 | 26,670 |
|
||||||
|
| MapsIcon | 2,144,772 | 27,774 |
|
||||||
|
| HuntIcon | 2,313,949 | 29,486 |
|
||||||
|
| QuestsIcon | 2,235,955 | 29,554 |
|
||||||
|
| InventoryIcon | 2,060,169 | 26,142 |
|
||||||
|
|
||||||
|
The combined persistent-icon payload is reduced from 11,038,737 bytes to 139,626 bytes (about 98.7%). The resized Maps icon was visually inspected after generation.
|
||||||
|
|
||||||
|
### Regression evidence
|
||||||
|
|
||||||
|
RED: the added explicit accessible-name assertion failed before the fix because the active Karte control had no `aria-label`.
|
||||||
|
|
||||||
|
GREEN: `npm test --workspace=@ashen-realms/web -- --watch=false` passed with 3 test files and 14 tests. The added coverage verifies the explicit Karte name, non-null WorldStore character rendering, and root/wildcard `/world` route redirects.
|
||||||
|
|||||||
BIN
apps/web/public/images/hud/runtime/CharacterIcon-128.png
Normal file
BIN
apps/web/public/images/hud/runtime/CharacterIcon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
apps/web/public/images/hud/runtime/HuntIcon-128.png
Normal file
BIN
apps/web/public/images/hud/runtime/HuntIcon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
apps/web/public/images/hud/runtime/InventoryIcon-128.png
Normal file
BIN
apps/web/public/images/hud/runtime/InventoryIcon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
apps/web/public/images/hud/runtime/MapsIcon-128.png
Normal file
BIN
apps/web/public/images/hud/runtime/MapsIcon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
apps/web/public/images/hud/runtime/QuestsIcon-128.png
Normal file
BIN
apps/web/public/images/hud/runtime/QuestsIcon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -1,18 +1,24 @@
|
|||||||
import { signal } from '@angular/core';
|
import { signal, WritableSignal } from '@angular/core';
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { CharacterResponse } from './core/api/game-api.models';
|
||||||
import { WorldStore } from './features/world/world.store';
|
import { WorldStore } from './features/world/world.store';
|
||||||
import { AppShellComponent } from './layout/app-shell/app-shell.component';
|
import { AppShellComponent } from './layout/app-shell/app-shell.component';
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
|
let character: WritableSignal<CharacterResponse | null>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
character = signal<CharacterResponse | null>(null);
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [AppShellComponent],
|
imports: [AppShellComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{
|
{
|
||||||
provide: WorldStore,
|
provide: WorldStore,
|
||||||
useValue: { character: signal(null) },
|
useValue: { character },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -32,6 +38,7 @@ describe('App', () => {
|
|||||||
expect(mapButton).not.toBeNull();
|
expect(mapButton).not.toBeNull();
|
||||||
expect(mapButton?.disabled).toBe(false);
|
expect(mapButton?.disabled).toBe(false);
|
||||||
expect(mapButton?.getAttribute('aria-current')).toBe('page');
|
expect(mapButton?.getAttribute('aria-current')).toBe('page');
|
||||||
|
expect(mapButton?.getAttribute('aria-label')).toBe('Karte');
|
||||||
|
|
||||||
for (const destination of ['hunt', 'quests', 'inventory', 'character']) {
|
for (const destination of ['hunt', 'quests', 'inventory', 'character']) {
|
||||||
expect(
|
expect(
|
||||||
@@ -41,4 +48,33 @@ describe('App', () => {
|
|||||||
|
|
||||||
expect(element.textContent).not.toContain('Shop');
|
expect(element.textContent).not.toContain('Shop');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders loaded character values supplied by the WorldStore', () => {
|
||||||
|
character.set({
|
||||||
|
id: 'character-id',
|
||||||
|
name: 'Mara Ashfall',
|
||||||
|
level: 7,
|
||||||
|
experience: 320,
|
||||||
|
currentHp: 52,
|
||||||
|
maxHp: 80,
|
||||||
|
attack: 12,
|
||||||
|
currentLocation: { id: 'location-id', key: 'south-gate', name: 'Südtor von Graufurt' },
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(AppShellComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(fixture.nativeElement.querySelector('app-top-bar')?.textContent).toContain(
|
||||||
|
'Mara Ashfall',
|
||||||
|
);
|
||||||
|
expect(fixture.nativeElement.querySelector('app-top-bar')?.textContent).toContain('Stufe 7');
|
||||||
|
expect(fixture.nativeElement.querySelector('app-top-bar')?.textContent).toContain('52 / 80');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects root and unknown routes to the world shell', () => {
|
||||||
|
expect(routes.find((route) => route.path === '')).toMatchObject({
|
||||||
|
pathMatch: 'full',
|
||||||
|
redirectTo: 'world',
|
||||||
|
});
|
||||||
|
expect(routes.find((route) => route.path === '**')).toMatchObject({ redirectTo: 'world' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,11 +27,22 @@
|
|||||||
|
|
||||||
@media (width < 900px) {
|
@media (width < 900px) {
|
||||||
.app-shell__content {
|
.app-shell__content {
|
||||||
grid-template-columns: minmax(8.5rem, 10rem) minmax(0, 1fr);
|
grid-template:
|
||||||
|
'navigation main' minmax(0, 1fr)
|
||||||
|
'context context' auto / minmax(8.5rem, 10rem) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
app-side-navigation {
|
||||||
|
grid-area: navigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__main {
|
||||||
|
grid-area: main;
|
||||||
}
|
}
|
||||||
|
|
||||||
app-context-panel {
|
app-context-panel {
|
||||||
display: none;
|
display: block;
|
||||||
|
grid-area: context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
routerLink="/world"
|
routerLink="/world"
|
||||||
data-navigation="world"
|
data-navigation="world"
|
||||||
aria-current="page"
|
aria-current="page"
|
||||||
|
aria-label="Karte"
|
||||||
>
|
>
|
||||||
<img src="/images/hud/MapsIcon.png" alt="" />
|
<img src="/images/hud/runtime/MapsIcon-128.png" alt="" />
|
||||||
<span>Karte</span>
|
<span>Karte</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
disabled
|
disabled
|
||||||
aria-label="Jagd ist noch nicht verfügbar"
|
aria-label="Jagd ist noch nicht verfügbar"
|
||||||
>
|
>
|
||||||
<img src="/images/hud/HuntIcon.png" alt="" />
|
<img src="/images/hud/runtime/HuntIcon-128.png" alt="" />
|
||||||
<span>Jagd</span>
|
<span>Jagd</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
disabled
|
disabled
|
||||||
aria-label="Quests sind noch nicht verfügbar"
|
aria-label="Quests sind noch nicht verfügbar"
|
||||||
>
|
>
|
||||||
<img src="/images/hud/QuestsIcon.png" alt="" />
|
<img src="/images/hud/runtime/QuestsIcon-128.png" alt="" />
|
||||||
<span>Quests</span>
|
<span>Quests</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
disabled
|
disabled
|
||||||
aria-label="Inventar ist noch nicht verfügbar"
|
aria-label="Inventar ist noch nicht verfügbar"
|
||||||
>
|
>
|
||||||
<img src="/images/hud/InventoryIcon.png" alt="" />
|
<img src="/images/hud/runtime/InventoryIcon-128.png" alt="" />
|
||||||
<span>Inventar</span>
|
<span>Inventar</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
disabled
|
disabled
|
||||||
aria-label="Charakter ist noch nicht verfügbar"
|
aria-label="Charakter ist noch nicht verfügbar"
|
||||||
>
|
>
|
||||||
<img src="/images/hud/CharacterIcon.png" alt="" />
|
<img src="/images/hud/runtime/CharacterIcon-128.png" alt="" />
|
||||||
<span>Charakter</span>
|
<span>Charakter</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<header class="top-bar">
|
<header class="top-bar">
|
||||||
<div class="top-bar__character">
|
<div class="top-bar__character">
|
||||||
<img class="top-bar__portrait" src="/images/hud/CharacterIcon.png" alt="" />
|
<img class="top-bar__portrait" src="/images/hud/runtime/CharacterIcon-128.png" alt="" />
|
||||||
@if (character(); as character) {
|
@if (character(); as character) {
|
||||||
<div class="top-bar__identity">
|
<div class="top-bar__identity">
|
||||||
<span class="top-bar__name">{{ character.name }}</span>
|
<span class="top-bar__name">{{ character.name }}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user