Initial commit
This commit is contained in:
176
landing/src/app/ui/site-header.ts
Normal file
176
landing/src/app/ui/site-header.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
PiActionButtonComponent,
|
||||
PiIconButtonComponent,
|
||||
PiSegmentedControlComponent,
|
||||
type SegmentedControlOption,
|
||||
} from '@pi/ui';
|
||||
|
||||
import { AppearanceService } from '../core/appearance';
|
||||
|
||||
import type { Copy } from '../core/copy';
|
||||
import type { Locale, SitePayload } from '../core/api';
|
||||
|
||||
export interface NavLink {
|
||||
anchor: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-site-header',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
PiActionButtonComponent,
|
||||
PiIconButtonComponent,
|
||||
PiSegmentedControlComponent,
|
||||
],
|
||||
styleUrl: './site-header.scss',
|
||||
template: `
|
||||
<header class="landing-header">
|
||||
<div class="landing-shell landing-header__inner">
|
||||
<a class="landing-brand" href="#top">
|
||||
<img
|
||||
class="landing-brand__mark"
|
||||
src="/logo.png"
|
||||
alt=""
|
||||
width="256"
|
||||
height="256"
|
||||
decoding="async"
|
||||
/>
|
||||
<span>Pireon</span>
|
||||
</a>
|
||||
|
||||
<nav class="landing-header__nav" aria-label="Principal">
|
||||
@for (link of links(); track link.anchor) {
|
||||
<a [href]="'#' + link.anchor">{{ link.label }}</a>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<div class="landing-header__actions">
|
||||
@if (localeOptions().length > 1) {
|
||||
<pi-segmented-control
|
||||
class="landing-header__locales"
|
||||
ariaLabel="Idioma"
|
||||
[options]="localeOptions()"
|
||||
[value]="locale()"
|
||||
(valueChange)="switchLocale($event)"
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
<pi-icon-button
|
||||
class="landing-header__appearance"
|
||||
iconType="custom"
|
||||
color="grey"
|
||||
[icon]="appearance.dark() ? 'sun' : 'moon'"
|
||||
[attr.aria-label]="
|
||||
appearance.dark() ? copy().nav.lightMode : copy().nav.darkMode
|
||||
"
|
||||
[attr.aria-pressed]="appearance.dark()"
|
||||
(execute)="appearance.toggle()"
|
||||
/>
|
||||
|
||||
<pi-action-button
|
||||
class="landing-header__cta"
|
||||
color="primary"
|
||||
[text]="copy().nav.contact"
|
||||
(execute)="navigate.emit('#contacto')"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="landing-header__toggle"
|
||||
aria-label="Menú"
|
||||
[attr.aria-expanded]="open()"
|
||||
aria-controls="mobile-nav"
|
||||
(click)="open.set(!open())"
|
||||
>
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (open()) {
|
||||
<nav id="mobile-nav" class="landing-header__mobile">
|
||||
<div class="landing-shell">
|
||||
@for (link of links(); track link.anchor) {
|
||||
<a [href]="'#' + link.anchor" (click)="open.set(false)">{{
|
||||
link.label
|
||||
}}</a>
|
||||
}
|
||||
|
||||
<!--
|
||||
The segmented control is hidden on this width, so the languages
|
||||
come back here as plain links rather than being unreachable on a
|
||||
phone.
|
||||
-->
|
||||
<button
|
||||
type="button"
|
||||
class="landing-header__mobile-appearance"
|
||||
[attr.aria-pressed]="appearance.dark()"
|
||||
(click)="appearance.toggle()"
|
||||
>
|
||||
{{ appearance.dark() ? copy().nav.lightMode : copy().nav.darkMode }}
|
||||
</button>
|
||||
|
||||
@if (locales().length > 1) {
|
||||
<div class="landing-header__mobile-locales">
|
||||
@for (option of locales(); track option.code) {
|
||||
<a
|
||||
[href]="'/?lang=' + option.code"
|
||||
[attr.hreflang]="option.code"
|
||||
[attr.aria-current]="option.code === locale() ? 'true' : null"
|
||||
[class.is-active]="option.code === locale()"
|
||||
>{{ option.code.toUpperCase() }}</a
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
</header>
|
||||
`,
|
||||
})
|
||||
export class SiteHeader {
|
||||
readonly copy = input.required<Copy>();
|
||||
readonly locale = input.required<Locale>();
|
||||
readonly links = input.required<NavLink[]>();
|
||||
readonly locales = input.required<SitePayload['locales']>();
|
||||
|
||||
readonly navigate = output<string>();
|
||||
|
||||
protected readonly appearance = inject(AppearanceService);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
|
||||
protected readonly open = signal(false);
|
||||
|
||||
protected readonly localeOptions = computed<SegmentedControlOption<Locale>[]>(
|
||||
() =>
|
||||
this.locales().map((option) => ({
|
||||
value: option.code as Locale,
|
||||
label: option.code.toUpperCase(),
|
||||
}))
|
||||
);
|
||||
|
||||
/**
|
||||
* A full navigation rather than a client-side one: the page's content is
|
||||
* resolved from `?lang=` on the server, so the server has to render it again.
|
||||
*/
|
||||
protected switchLocale(next: Locale | undefined): void {
|
||||
if (!next || next === this.locale()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.document.defaultView?.location.assign(`/?lang=${next}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user