Error Pages
blocklayoutPack of four status screens — 404, 500, 403, and 503 — each with a matching isometric illustration, code, copy, and actions. `variant` picks a single screen, `size` compacts it. Override title, description, image, code, tag, labels, hrefs, disabled flags, and callbacks. Pass `image=""` to hide the art.
Installation
$pnpm dlx uipkge-ng@latest add error-pages$npx uipkge-ng@latest add error-pages$yarn dlx uipkge-ng@latest add error-pages$bunx uipkge-ng@latest add error-pagesManifest:
https://uipkge.dev/r/angular/error-pages.jsonInstalls to:src/app/components/blocks/error-pages/Variants
Loading interactive previews…
Loading interactive previews…
npm dependencies
Includes
Files installed (2)
src/app/components/blocks/error-pages/error-pages.component.ts17.4 kB
// Deprecated four-variant status-screen gallery (use error-page instead). Byte-identical to // the error-page gallery: 404, 500, 403, and maintenance cards. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core' import { Clock, LucideAngularModule, RotateCcw } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiButtonComponent } from '@/ui/button/button.component' export type ErrorPagesVariant = '404' | '500' | '403' | 'maintenance' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-error-pages, [ui-error-pages]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [LucideAngularModule, UiButtonComponent], template: ` <div data-slot="error-pages" [attr.data-size]="size" [class]="rootClass"> @if (show('404')) { <article [class]="frameClass" data-slot="error-page" data-variant="404" role="status"> @if (image404) { <img [src]="image404" [alt]="imageAlt" [class]="imgClass" /> } @else if (image404 === undefined) { <div [class]="panelClass" aria-hidden="true"> <div class="border-border text-muted-foreground truncate border-b px-3 py-2 text-xs"> GET /pricing/teams </div> <div class="space-y-1 p-3 text-xs leading-relaxed"> <p><span class="text-muted-foreground">HTTP/1.1</span> {{ code404 || '404' }} Not Found</p> <p class="text-muted-foreground">x-request-id: req_9k2e18</p> </div> </div> } <div [class]="bodyClass"> @if (code404) { <p [class]="codeClass">{{ code404 }}</p> } <h3 [class]="titleClass">{{ title404 }}</h3> <p [class]="descClass">{{ desc404 }}</p> @if (variant === '404' && tag) { <p class="text-muted-foreground mt-3 inline-flex items-center rounded-full border px-3 py-1 text-xs"> {{ tag }} </p> } @if (showPrimary || showSecondary) { <div [class]="actionsClass"> @if (showPrimary) { @if (primaryHref) { <a ui-button [size]="actionSize" [href]="primaryHref" (click)="home.emit()">{{ primary404 }}</a> } @else { <button ui-button [size]="actionSize" [disabled]="primaryDisabled" (click)="home.emit()"> {{ primary404 }} </button> } } @if (showSecondary) { @if (secondaryHref) { <a ui-button [size]="actionSize" variant="ghost" [href]="secondaryHref" (click)="search.emit()"> {{ secondary404 }} </a> } @else { <button ui-button [size]="actionSize" variant="ghost" [disabled]="secondaryDisabled" (click)="search.emit()" > {{ secondary404 }} </button> } } </div> } </div> </article> } @if (show('500')) { <article [class]="frameClass" data-slot="error-page" data-variant="500" role="alert"> @if (image500) { <img [src]="image500" [alt]="imageAlt" [class]="imgClass" /> } @else if (image500 === undefined) { <div [class]="panelClass" aria-hidden="true"> <div class="border-border text-muted-foreground truncate border-b px-3 py-2 text-xs"> POST /api/v1/invoices </div> <div class="space-y-1 p-3 text-xs leading-relaxed"> <p> <span class="text-muted-foreground">HTTP/1.1</span>{{ ' ' }} <span class="text-destructive">{{ code500 || '500' }} Internal Server Error</span> </p> <p class="text-muted-foreground">error: ECONNRESET at Worker.run:142</p> </div> </div> } <div [class]="bodyClass"> @if (code500) { <p [class]="codeClass">{{ code500 }}</p> } <h3 [class]="titleClass">{{ title500 }}</h3> <p [class]="descClass">{{ desc500 }}</p> @if (variant === '500' && tag) { <p class="text-muted-foreground mt-3 inline-flex items-center rounded-full border px-3 py-1 text-xs"> {{ tag }} </p> } @if (showPrimary || showSecondary) { <div [class]="actionsClass"> @if (showPrimary) { @if (primaryHref) { <a ui-button [size]="actionSize" [href]="primaryHref" (click)="retry.emit()"> <lucide-icon [img]="RotateCcw" /> {{ primary500 }} </a> } @else { <button ui-button [size]="actionSize" [disabled]="primaryDisabled" (click)="retry.emit()"> <lucide-icon [img]="RotateCcw" /> {{ primary500 }} </button> } } @if (showSecondary) { @if (secondaryHref) { <a ui-button [size]="actionSize" variant="outline" [href]="secondaryHref" (click)="status.emit()"> {{ secondary500 }} </a> } @else { <button ui-button [size]="actionSize" variant="outline" [disabled]="secondaryDisabled" (click)="status.emit()" > {{ secondary500 }} </button> } } </div> } </div> </article> } @if (show('403')) { <article [class]="frameClass" data-slot="error-page" data-variant="403" role="alert"> @if (image403) { <img [src]="image403" [alt]="imageAlt" [class]="imgClass" /> } @else if (image403 === undefined) { <div [class]="panelClass" aria-hidden="true"> <div class="border-border text-muted-foreground truncate border-b px-3 py-2 text-xs"> GET /admin/billing </div> <div class="space-y-1 p-3 text-xs leading-relaxed"> <p> <span class="text-muted-foreground">HTTP/1.1</span>{{ ' ' }} <span class="text-warning">{{ code403 || '403' }} Forbidden</span> </p> <p class="text-muted-foreground">scope billing:read — denied for guest</p> </div> </div> } <div [class]="bodyClass"> @if (code403) { <p [class]="codeClass">{{ code403 }}</p> } <h3 [class]="titleClass">{{ title403 }}</h3> <p [class]="descClass">{{ desc403 }}</p> @if (variant === '403' && tag) { <p class="text-muted-foreground mt-3 inline-flex items-center rounded-full border px-3 py-1 text-xs"> {{ tag }} </p> } @if (showPrimary || showSecondary) { <div [class]="actionsClass"> @if (showSecondary) { @if (secondaryHref) { <a ui-button [size]="actionSize" variant="outline" [href]="secondaryHref" (click)="switchAccount.emit()" > {{ secondary403 }} </a> } @else { <button ui-button [size]="actionSize" variant="outline" [disabled]="secondaryDisabled" (click)="switchAccount.emit()" > {{ secondary403 }} </button> } } @if (showPrimary) { @if (primaryHref) { <a ui-button [size]="actionSize" [href]="primaryHref" (click)="requestAccess.emit()"> {{ primary403 }} </a> } @else { <button ui-button [size]="actionSize" [disabled]="primaryDisabled" (click)="requestAccess.emit()"> {{ primary403 }} </button> } } </div> } </div> </article> } @if (show('maintenance')) { <article [class]="frameClass" data-slot="error-page" data-variant="maintenance" role="status"> @if (image503) { <img [src]="image503" [alt]="imageAlt" [class]="imgClass" /> } @else if (image503 === undefined) { <div [class]="panelClass" aria-hidden="true"> <div class="border-border text-muted-foreground truncate border-b px-3 py-2 text-xs">GET /health</div> <div class="space-y-1 p-3 text-xs leading-relaxed"> <p> <span class="text-muted-foreground">HTTP/1.1</span>{{ ' ' }} <span class="text-warning">{{ code503 || '503' }} Service Unavailable</span> </p> <p class="text-muted-foreground">Retry-After: 3600</p> </div> </div> } <div [class]="bodyClass"> @if (code503) { <p [class]="codeClass">{{ code503 }}</p> } <h3 [class]="titleClass">{{ title503 }}</h3> <p [class]="descClass">{{ desc503 }}</p> @if (tag503) { <p class="text-muted-foreground mt-3 inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs" > <lucide-icon [img]="Clock" class="size-3.5" aria-hidden="true" /> {{ tag503 }} </p> } @if (showPrimary || (showSecondary && secondary503)) { <div [class]="actionsClass"> @if (showPrimary) { @if (primaryHref) { <a ui-button [size]="actionSize" variant="ghost" [href]="primaryHref" (click)="subscribe.emit()"> {{ primary503 }} </a> } @else { <button ui-button [size]="actionSize" variant="ghost" [disabled]="primaryDisabled" (click)="subscribe.emit()" > {{ primary503 }} </button> } } @if (showSecondary && secondary503) { @if (secondaryHref) { <a ui-button [size]="actionSize" variant="outline" [href]="secondaryHref" (click)="status.emit()"> {{ secondary503 }} </a> } @else { <button ui-button [size]="actionSize" variant="outline" [disabled]="secondaryDisabled" (click)="status.emit()" > {{ secondary503 }} </button> } } </div> } </div> </article> } </div> `, }) export class UiErrorPagesComponent { protected readonly Clock = Clock protected readonly RotateCcw = RotateCcw /** `'all'` renders the full gallery; a single variant renders just that screen. */ @Input() variant: ErrorPagesVariant | 'all' = 'all' @Input() size: 'sm' | 'default' = 'default' @Input('class') className?: string @Input() title?: string @Input() description?: string /** Status numeral. Pass `''` to hide. */ @Input() code?: string /** Eyebrow pill (maintenance ETA by default). Pass `''` to hide. */ @Input() tag?: string /** Replaces the HTTP response panel on a single variant. Pass `''` to hide art. */ @Input() image?: string @Input() imageAlt = '' @Input() primaryLabel?: string @Input() secondaryLabel?: string @Input() primaryHref?: string @Input() secondaryHref?: string @Input() showPrimary = true @Input() showSecondary = true @Input() primaryDisabled = false @Input() secondaryDisabled = false @Output() home = new EventEmitter<void>() @Output() search = new EventEmitter<void>() @Output() retry = new EventEmitter<void>() @Output() status = new EventEmitter<void>() @Output() switchAccount = new EventEmitter<void>() @Output() requestAccess = new EventEmitter<void>() @Output() subscribe = new EventEmitter<void>() show(v: ErrorPagesVariant): boolean { return this.variant === 'all' || this.variant === v } private copy(fallback: string, override?: string): string { return override === undefined ? fallback : override } private forVariant(v: ErrorPagesVariant, fallback: string, override?: string): string { return this.variant === v ? this.copy(fallback, override) : fallback } get rootClass(): string { return cn(this.variant === 'all' && 'grid gap-4 sm:grid-cols-2', this.className) } get frameClass(): string { return cn('border-border bg-card overflow-hidden rounded-xl border text-left', this.size === 'sm' && 'rounded-lg') } get imgClass(): string { return 'aspect-[16/10] w-full object-cover' } get panelClass(): string { return 'border-border bg-muted/40 border-b font-mono' } get bodyClass(): string { return this.size === 'sm' ? 'p-5' : 'p-6' } get codeClass(): string { return cn('font-semibold tracking-tighter', this.size === 'sm' ? 'text-2xl' : 'text-4xl') } get titleClass(): string { return cn('mt-1.5 font-semibold tracking-tight', this.size === 'sm' ? 'text-base' : 'text-xl') } get descClass(): string { return 'text-muted-foreground mt-2 max-w-sm text-sm' } get actionsClass(): string { return cn('flex flex-wrap items-center gap-2', this.size === 'sm' ? 'mt-4' : 'mt-5') } get actionSize(): 'xs' | 'sm' { return this.size === 'sm' ? 'xs' : 'sm' } get title404(): string { return this.forVariant('404', 'Page not found', this.title) } get desc404(): string { return this.forVariant( '404', "The page you're looking for doesn't exist or was moved. Check the URL or head back home.", this.description, ) } get code404(): string { return this.forVariant('404', '404', this.code) } get image404(): string | undefined { return this.forVariant('404', '/illustrations/error-404.jpg', this.image) } get primary404(): string { return this.forVariant('404', 'Go home', this.primaryLabel) } get secondary404(): string { return this.forVariant('404', 'Search docs', this.secondaryLabel) } get title500(): string { return this.forVariant('500', 'Internal server error', this.title) } get desc500(): string { return this.forVariant( '500', 'Something went wrong on our end. The team has been notified — try again in a moment.', this.description, ) } get code500(): string { return this.forVariant('500', '500', this.code) } get image500(): string | undefined { return this.forVariant('500', '/illustrations/error-500.jpg', this.image) } get primary500(): string { return this.forVariant('500', 'Retry', this.primaryLabel) } get secondary500(): string { return this.forVariant('500', 'Status page', this.secondaryLabel) } get title403(): string { return this.forVariant('403', 'Access denied', this.title) } get desc403(): string { return this.forVariant( '403', "You don't have permission to view this page. Switch accounts or ask an admin to request access.", this.description, ) } get code403(): string { return this.forVariant('403', '403', this.code) } get image403(): string | undefined { return this.forVariant('403', '/illustrations/error-403.jpg', this.image) } get primary403(): string { return this.forVariant('403', 'Request access', this.primaryLabel) } get secondary403(): string { return this.forVariant('403', 'Switch account', this.secondaryLabel) } get title503(): string { return this.forVariant('maintenance', 'Scheduled maintenance', this.title) } get desc503(): string { return this.forVariant( 'maintenance', "We're upgrading our systems. The service will be back online shortly.", this.description, ) } get code503(): string { return this.forVariant('maintenance', '503', this.code) } get image503(): string | undefined { return this.forVariant('maintenance', '/illustrations/error-maintenance.jpg', this.image) } get tag503(): string { return this.forVariant('maintenance', 'Back online around 14:00 UTC', this.tag) } get primary503(): string { return this.forVariant('maintenance', 'Subscribe for updates', this.primaryLabel) } get secondary503(): string { return this.variant === 'maintenance' ? (this.secondaryLabel ?? '') : (this.secondaryLabel ?? '') } }src/app/components/blocks/error-pages/index.ts0.1 kB
export { UiErrorPagesComponent, type ErrorPagesVariant } from './error-pages.component'
Raw manifest:https://uipkge.dev/r/angular/error-pages.json