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 shadcn@latest add https://uipkge.dev/r/lit/error-pages.json$npx shadcn@latest add https://uipkge.dev/r/lit/error-pages.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/error-pages.json$bunx shadcn@latest add https://uipkge.dev/r/lit/error-pages.jsonManifest:
https://uipkge.dev/r/lit/error-pages.jsonInstalls to:src/uip/blocks/error-pages/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | ErrorPagesVariant | 'all' | all | optional |
size | 'sm' | 'default' | default | optional |
heading | string | — | optional |
description | string | — | optional |
code | string | — | optional |
tag | string | — | optional |
image | string | — | optional |
image-alt | string | — | optional |
primary-label | string | — | optional |
secondary-label | string | — | optional |
primary-href | string | — | optional |
secondary-href | string | — | optional |
show-primary | boolean | true | optional |
show-secondary | boolean | true | optional |
primary-disabled | boolean | false | optional |
secondary-disabled | boolean | false | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-error-pages': UipErrorPages
}
}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