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 shadcn@latest add https://uipkge.dev/r/lit/error-pages.json
Manifest:https://uipkge.dev/r/lit/error-pages.jsonInstalls to:src/uip/blocks/error-pages/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
variantErrorPagesVariant | 'all'alloptional
size'sm' | 'default'defaultoptional
headingstring—optional
descriptionstring—optional
codestring—optional
tagstring—optional
imagestring—optional
image-altstring—optional
primary-labelstring—optional
secondary-labelstring—optional
primary-hrefstring—optional
secondary-hrefstring—optional
show-primarybooleantrueoptional
show-secondarybooleantrueoptional
primary-disabledbooleanfalseoptional
secondary-disabledbooleanfalseoptional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-error-pages': UipErrorPages
  }
}

npm dependencies

Files installed (1)

  • src/uip/blocks/error-pages/error-pages.ts14.3 kB
    import { LitElement, css, html, nothing } from 'lit'
    import { Clock, RotateCcw } from 'lucide'
    import { cn } from '../../lib/utils'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    import { icon } from '../../lib/icon'
    // Side-effect import: registers <uip-button> for standalone use (consumers
    // who install only this block still get a working button).
    import '../../components/button/button'
    
    export type ErrorPagesVariant = '404' | '500' | '403' | 'maintenance'
    
    const falseConverter = { fromAttribute: (v: string | null) => v !== null && v !== 'false' }
    
    /**
     * <uip-error-pages> — the registry ErrorPages block as a web component.
     *
     * Class strings are React's ErrorPages verbatim. `heading` is React's `title`
     * (`title` is taken by HTMLElement). `onHome` / `onSearch` / `onRetry` /
     * `onStatus` / `onSwitchAccount` / `onRequestAccess` / `onSubscribe` become
     * `home` / `search` / `retry` / `status` / `switch-account` / `request-access`
     * / `subscribe` CustomEvents (bubbles, composed).
     *
     * Deviation: React's `image === undefined` HTTP-panel branches are
     * unreachable (every variant has a default image), so they are not ported:
     * omit `image` for the default art, pass `image=""` to hide it.
     */
    export class UipErrorPages extends LitElement {
      static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }
      // :host display can't be a utility class (the host has no template of its own).
      static styles = [tailwind, css`:host { display: block; }`]
    
      static properties = {
        variant: {},
        size: { reflect: true },
        heading: {},
        description: {},
        code: {},
        tag: {},
        image: {},
        imageAlt: { attribute: 'image-alt' },
        primaryLabel: { attribute: 'primary-label' },
        secondaryLabel: { attribute: 'secondary-label' },
        primaryHref: { attribute: 'primary-href' },
        secondaryHref: { attribute: 'secondary-href' },
        showPrimary: { type: Boolean, attribute: 'show-primary', converter: falseConverter },
        showSecondary: { type: Boolean, attribute: 'show-secondary', converter: falseConverter },
        primaryDisabled: { type: Boolean, attribute: 'primary-disabled' },
        secondaryDisabled: { type: Boolean, attribute: 'secondary-disabled' },
      }
    
      variant: ErrorPagesVariant | 'all' = 'all'
      size: 'sm' | 'default' = 'default'
      heading?: string
      description?: string
      code?: string
      tag?: string
      image?: string
      imageAlt = ''
      primaryLabel?: string
      secondaryLabel?: string
      primaryHref?: string
      secondaryHref?: string
      showPrimary = true
      showSecondary = true
      primaryDisabled = false
      secondaryDisabled = false
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'error-pages')
        this.setAttribute('data-size', this.size)
      }
    
      willUpdate() {
        this.setAttribute('data-size', this.size)
      }
    
      private emit(
        name: 'home' | 'search' | 'retry' | 'status' | 'switch-account' | 'request-access' | 'subscribe',
      ) {
        this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true }))
      }
    
      private action(opts: {
        label: string
        size: 'xs' | 'sm'
        variant?: string
        disabled: boolean
        href?: string
        event: 'home' | 'search' | 'retry' | 'status' | 'switch-account' | 'request-access' | 'subscribe'
        leadingIcon?: boolean
      }) {
        return html`<uip-button
          size=${opts.size}
          variant=${opts.variant ?? nothing}
          ?disabled=${opts.disabled}
          href=${opts.href ?? nothing}
          @click=${() => this.emit(opts.event)}
        >
          ${opts.leadingIcon ? icon(RotateCcw, 'rotate-ccw') : nothing}${opts.label}
        </uip-button>`
      }
    
      render() {
        const sm = this.size === 'sm'
        const single = (v: ErrorPagesVariant) => this.variant === v
        const pick = (v: ErrorPagesVariant, fallback: string, override?: string) =>
          single(v) ? (override ?? fallback) : fallback
        const frameClass = cn(
          'border-border bg-card overflow-hidden rounded-xl border text-left',
          sm && 'rounded-lg',
        )
        const imgClass = 'aspect-[16/10] w-full object-cover'
        const bodyClass = sm ? 'p-5' : 'p-6'
        const codeClass = cn('font-semibold tracking-tighter', sm ? 'text-2xl' : 'text-4xl')
        const titleClass = cn('mt-1.5 font-semibold tracking-tight', sm ? 'text-base' : 'text-xl')
        const descClass = 'text-muted-foreground mt-2 max-w-sm text-sm'
        const actionsClass = cn('flex flex-wrap items-center gap-2', sm ? 'mt-4' : 'mt-5')
        const actionSize = sm ? ('xs' as const) : ('sm' as const)
        const show = (v: ErrorPagesVariant) => this.variant === 'all' || single(v)
    
        const title404 = pick('404', 'Page not found', this.heading)
        const desc404 = pick(
          '404',
          "The page you're looking for doesn't exist or was moved. Check the URL or head back home.",
          this.description,
        )
        const code404 = pick('404', '404', this.code)
        const image404 = pick('404', '/illustrations/error-404.jpg', this.image)
        const primary404 = pick('404', 'Go home', this.primaryLabel)
        const secondary404 = pick('404', 'Search docs', this.secondaryLabel)
    
        const title500 = pick('500', 'Internal server error', this.heading)
        const desc500 = pick(
          '500',
          'Something went wrong on our end. The team has been notified — try again in a moment.',
          this.description,
        )
        const code500 = pick('500', '500', this.code)
        const image500 = pick('500', '/illustrations/error-500.jpg', this.image)
        const primary500 = pick('500', 'Retry', this.primaryLabel)
        const secondary500 = pick('500', 'Status page', this.secondaryLabel)
    
        const title403 = pick('403', 'Access denied', this.heading)
        const desc403 = pick(
          '403',
          "You don't have permission to view this page. Switch accounts or ask an admin to request access.",
          this.description,
        )
        const code403 = pick('403', '403', this.code)
        const image403 = pick('403', '/illustrations/error-403.jpg', this.image)
        const primary403 = pick('403', 'Request access', this.primaryLabel)
        const secondary403 = pick('403', 'Switch account', this.secondaryLabel)
    
        const title503 = pick('maintenance', 'Scheduled maintenance', this.heading)
        const desc503 = pick(
          'maintenance',
          "We're upgrading our systems. The service will be back online shortly.",
          this.description,
        )
        const code503 = pick('maintenance', '503', this.code)
        const image503 = pick('maintenance', '/illustrations/error-maintenance.jpg', this.image)
        const tag503 = pick('maintenance', 'Back online around 14:00 UTC', this.tag)
        const primary503 = pick('maintenance', 'Subscribe for updates', this.primaryLabel)
        const secondary503 = this.secondaryLabel ?? ''
    
        return html`
          <div data-slot="error-pages" data-size=${this.size} class=${cn(this.variant === 'all' && 'grid gap-4 sm:grid-cols-2')}>
            ${show('404')
              ? html`<article class=${frameClass} data-slot="error-page" data-variant="404" role="status">
                  ${image404 ? html`<img src=${image404} alt=${this.imageAlt} class=${imgClass} />` : nothing}
                  <div class=${bodyClass}>
                    ${code404 ? html`<p class=${codeClass}>${code404}</p>` : nothing}
                    <h3 class=${titleClass}>${title404}</h3>
                    <p class=${descClass}>${desc404}</p>
                    ${single('404') && this.tag
                      ? html`<p class="text-muted-foreground mt-3 inline-flex items-center rounded-full border px-3 py-1 text-xs">
                          ${this.tag}
                        </p>`
                      : nothing}
                    ${this.showPrimary || this.showSecondary
                      ? html`<div class=${actionsClass}>
                          ${this.showPrimary
                            ? this.action({
                                label: primary404,
                                size: actionSize,
                                disabled: this.primaryDisabled,
                                href: this.primaryHref,
                                event: 'home',
                              })
                            : nothing}
                          ${this.showSecondary
                            ? this.action({
                                label: secondary404,
                                size: actionSize,
                                variant: 'ghost',
                                disabled: this.secondaryDisabled,
                                href: this.secondaryHref,
                                event: 'search',
                              })
                            : nothing}
                        </div>`
                      : nothing}
                  </div>
                </article>`
              : nothing}
            ${show('500')
              ? html`<article class=${frameClass} data-slot="error-page" data-variant="500" role="alert">
                  ${image500 ? html`<img src=${image500} alt=${this.imageAlt} class=${imgClass} />` : nothing}
                  <div class=${bodyClass}>
                    ${code500 ? html`<p class=${codeClass}>${code500}</p>` : nothing}
                    <h3 class=${titleClass}>${title500}</h3>
                    <p class=${descClass}>${desc500}</p>
                    ${single('500') && this.tag
                      ? html`<p class="text-muted-foreground mt-3 inline-flex items-center rounded-full border px-3 py-1 text-xs">
                          ${this.tag}
                        </p>`
                      : nothing}
                    ${this.showPrimary || this.showSecondary
                      ? html`<div class=${actionsClass}>
                          ${this.showPrimary
                            ? this.action({
                                label: primary500,
                                size: actionSize,
                                disabled: this.primaryDisabled,
                                href: this.primaryHref,
                                event: 'retry',
                                leadingIcon: true,
                              })
                            : nothing}
                          ${this.showSecondary
                            ? this.action({
                                label: secondary500,
                                size: actionSize,
                                variant: 'outline',
                                disabled: this.secondaryDisabled,
                                href: this.secondaryHref,
                                event: 'status',
                              })
                            : nothing}
                        </div>`
                      : nothing}
                  </div>
                </article>`
              : nothing}
            ${show('403')
              ? html`<article class=${frameClass} data-slot="error-page" data-variant="403" role="alert">
                  ${image403 ? html`<img src=${image403} alt=${this.imageAlt} class=${imgClass} />` : nothing}
                  <div class=${bodyClass}>
                    ${code403 ? html`<p class=${codeClass}>${code403}</p>` : nothing}
                    <h3 class=${titleClass}>${title403}</h3>
                    <p class=${descClass}>${desc403}</p>
                    ${single('403') && this.tag
                      ? html`<p class="text-muted-foreground mt-3 inline-flex items-center rounded-full border px-3 py-1 text-xs">
                          ${this.tag}
                        </p>`
                      : nothing}
                    ${this.showPrimary || this.showSecondary
                      ? html`<div class=${actionsClass}>
                          ${this.showSecondary
                            ? this.action({
                                label: secondary403,
                                size: actionSize,
                                variant: 'outline',
                                disabled: this.secondaryDisabled,
                                href: this.secondaryHref,
                                event: 'switch-account',
                              })
                            : nothing}
                          ${this.showPrimary
                            ? this.action({
                                label: primary403,
                                size: actionSize,
                                disabled: this.primaryDisabled,
                                href: this.primaryHref,
                                event: 'request-access',
                              })
                            : nothing}
                        </div>`
                      : nothing}
                  </div>
                </article>`
              : nothing}
            ${show('maintenance')
              ? html`<article class=${frameClass} data-slot="error-page" data-variant="maintenance" role="status">
                  ${image503 ? html`<img src=${image503} alt=${this.imageAlt} class=${imgClass} />` : nothing}
                  <div class=${bodyClass}>
                    ${code503 ? html`<p class=${codeClass}>${code503}</p>` : nothing}
                    <h3 class=${titleClass}>${title503}</h3>
                    <p class=${descClass}>${desc503}</p>
                    ${tag503
                      ? html`<p class="text-muted-foreground mt-3 inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs">
                          ${icon(Clock, 'clock', 'size-3.5')} ${tag503}
                        </p>`
                      : nothing}
                    ${this.showPrimary || (this.showSecondary && secondary503)
                      ? html`<div class=${actionsClass}>
                          ${this.showPrimary
                            ? this.action({
                                label: primary503,
                                size: actionSize,
                                variant: 'ghost',
                                disabled: this.primaryDisabled,
                                href: this.primaryHref,
                                event: 'subscribe',
                              })
                            : nothing}
                          ${this.showSecondary && secondary503
                            ? this.action({
                                label: secondary503,
                                size: actionSize,
                                variant: 'outline',
                                disabled: this.secondaryDisabled,
                                href: this.secondaryHref,
                                event: 'status',
                              })
                            : nothing}
                        </div>`
                      : nothing}
                  </div>
                </article>`
              : nothing}
          </div>
        `
      }
    }
    
    customElements.get('uip-error-pages') || customElements.define('uip-error-pages', UipErrorPages)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-error-pages': UipErrorPages
      }
    }
    

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