Menu

Change language

GitHub repo

Error Pages

blocklayout

Pack 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

$npx uipkge-ng@latest add error-pages
Manifest:https://uipkge.dev/r/angular/error-pages.jsonInstalls to:src/app/components/blocks/error-pages/

Variants

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

Raw manifest:https://uipkge.dev/r/angular/error-pages.json