react-amiga-guru-meditation
Version:
A React Error Boundary styled like the Amiga Guru Meditation — accessible, themeable, zero runtime dependencies, ARIA-compliant, CSS variables theming.
1 lines • 16.5 kB
Source Map (JSON)
{"version":3,"sources":["../src/AmigaErrorBoundary.module.css","../src/AmigaErrorBoundary.tsx"],"names":["root","noBlink","portal","title","message","code","details","Component","jsx","jsxs","useState","useEffect","createPortal","useCallback"],"mappings":"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","file":"index.cjs","sourcesContent":["/* CSS variables (defaults inlined inside var() so any wrapping ancestor that\n * sets --ragm-* gets inherited — declaring them directly on .root would block\n * theming via CSS-only. */\n\n.root {\n position: relative;\n z-index: var(--ragm-z-index, 9999);\n box-sizing: border-box;\n border: var(--ragm-border-width, 6px) var(--ragm-border-style, solid) var(--ragm-fg, #ff2222);\n background-color: var(--ragm-bg, #000000);\n color: var(--ragm-fg, #ff2222);\n padding: var(--ragm-padding, 24px);\n text-align: center;\n font-family: var(--ragm-font-family, \"Times New Roman\", Times, serif);\n font-size: var(--ragm-font-size, 18px);\n font-weight: var(--ragm-font-weight, 400);\n line-height: var(--ragm-line-height, 1.4);\n\n animation-name: ragm-blink;\n animation-duration: var(--ragm-blink-duration, 1s);\n animation-timing-function: step-end;\n animation-iteration-count: infinite;\n animation-direction: alternate;\n}\n\n.root * {\n box-sizing: border-box;\n}\n\n.noBlink {\n animation: none;\n}\n\n.portal {\n position: fixed;\n inset: 0;\n z-index: var(--ragm-z-index, 9999);\n display: flex;\n align-items: center;\n justify-content: center;\n flex-direction: column;\n padding: var(--ragm-portal-padding, 0);\n background-color: var(--ragm-bg, #000000);\n color: var(--ragm-fg, #ff2222);\n border: var(--ragm-border-width, 6px) var(--ragm-border-style, solid) var(--ragm-fg, #ff2222);\n font-family: var(--ragm-font-family, \"Times New Roman\", Times, serif);\n font-size: var(--ragm-font-size, 18px);\n font-weight: var(--ragm-font-weight, 400);\n line-height: var(--ragm-line-height, 1.4);\n text-align: center;\n\n animation-name: ragm-blink;\n animation-duration: var(--ragm-blink-duration, 1s);\n animation-timing-function: step-end;\n animation-iteration-count: infinite;\n animation-direction: alternate;\n}\n\n.portal .root {\n border: 0;\n background: transparent;\n animation: none;\n padding: var(--ragm-padding, 24px);\n max-width: var(--ragm-max-width, 760px);\n width: 100%;\n}\n\n.title {\n margin: 0 0 12px;\n}\n\n.message {\n margin: 0 0 12px;\n}\n\n.code {\n font-family: var(--ragm-mono-family, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);\n word-break: break-word;\n}\n\n.details {\n text-align: left;\n margin: 16px auto 0;\n padding: 12px;\n border: 1px dashed var(--ragm-fg, #ff2222);\n font-size: 0.85em;\n max-width: var(--ragm-details-max-width, 100%);\n}\n\n.details summary {\n cursor: pointer;\n font-weight: bold;\n margin-bottom: 8px;\n user-select: none;\n}\n\n.details pre {\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n font-family: var(--ragm-mono-family, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);\n font-size: 12px;\n}\n\n@keyframes ragm-blink {\n 0% {\n border-color: var(--ragm-fg, #ff2222);\n }\n 50% {\n border-color: var(--ragm-bg, #000000);\n }\n 100% {\n border-color: var(--ragm-fg, #ff2222);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .root,\n .portal {\n animation: none;\n }\n}\n","import { Component, useCallback, useEffect, useState } from \"react\";\nimport type { CSSProperties, ErrorInfo, ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport styles from \"./AmigaErrorBoundary.module.css\";\n\nexport interface AmigaFallbackRenderProps {\n error: Error;\n errorInfo: ErrorInfo | null;\n resetErrorBoundary: () => void;\n}\n\nexport type AmigaPortalTarget = boolean | string | HTMLElement;\n\nexport interface AmigaErrorBoundaryProps {\n children?: ReactNode;\n\n /** Render-prop fallback. Takes priority over `fallback`. */\n fallbackRender?: (props: AmigaFallbackRenderProps) => ReactNode;\n\n /** Static fallback ReactNode rendered when an error is caught. */\n fallback?: ReactNode;\n\n /** Called when an error is caught. Useful for logging or error reporting. */\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n\n /** Called when the boundary is reset (imperatively or via resetKeys change). */\n onReset?: (details: { reason: \"imperative\" | \"keys\" }) => void;\n\n /** When any value in this array changes (shallow equality), the boundary auto-resets. */\n resetKeys?: ReadonlyArray<unknown>;\n\n /** Force the error UI to render even without a real error. Useful for showcase/preview. */\n show?: boolean;\n\n /** A pre-built error to display when `show` is true (or to override the caught one). */\n error?: Error;\n\n /**\n * Render the error UI through a React portal at the document level (Amiga-style takeover).\n * - `false` (default): render inline where the boundary is mounted.\n * - `true`: render in a portal under a div appended to `document.body`.\n * - `string`: id of the portal container element (created if missing).\n * - `HTMLElement`: explicit portal target.\n */\n portal?: AmigaPortalTarget;\n\n /** Title text. Default: \"Software Failure. Press left mouse button to continue.\" */\n title?: ReactNode;\n\n /** Override the message line. Receives the caught error. Default: \"Guru Meditation: {error.message}\". */\n formatGuru?: (error: Error) => ReactNode;\n\n /** Show collapsible details with the component stack. Default: true. */\n showDetails?: boolean;\n\n /** Disable the blink animation. Default: false. */\n noBlink?: boolean;\n\n /** Extra class name applied to the root element. */\n className?: string;\n\n /** Inline styles applied to the root element. Useful to set CSS variables. */\n style?: CSSProperties;\n}\n\ninterface State {\n error: Error | null;\n errorInfo: ErrorInfo | null;\n}\n\nconst DEFAULT_TITLE = \"Software Failure. Press left mouse button to continue.\";\n\nfunction shallowDiff(a: ReadonlyArray<unknown> | undefined, b: ReadonlyArray<unknown> | undefined): boolean {\n if (a === b) return false;\n if (!a || !b) return true;\n if (a.length !== b.length) return true;\n for (let i = 0; i < a.length; i += 1) {\n if (!Object.is(a[i], b[i])) return true;\n }\n return false;\n}\n\nexport class AmigaErrorBoundary extends Component<AmigaErrorBoundaryProps, State> {\n static displayName = \"AmigaErrorBoundary\";\n\n override state: State = { error: null, errorInfo: null };\n\n static getDerivedStateFromError(error: Error): Partial<State> {\n return { error };\n }\n\n override componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n this.setState({ errorInfo });\n this.props.onError?.(error, errorInfo);\n }\n\n override componentDidUpdate(prevProps: AmigaErrorBoundaryProps) {\n if (this.state.error !== null && shallowDiff(prevProps.resetKeys, this.props.resetKeys)) {\n this.resetWithReason(\"keys\");\n }\n }\n\n resetErrorBoundary = () => {\n this.resetWithReason(\"imperative\");\n };\n\n private resetWithReason(reason: \"imperative\" | \"keys\") {\n if (this.state.error === null && this.state.errorInfo === null) return;\n this.setState({ error: null, errorInfo: null });\n this.props.onReset?.({ reason });\n }\n\n override render() {\n const { error: caught, errorInfo } = this.state;\n const {\n show = false,\n error: forcedError,\n fallback,\n fallbackRender,\n children,\n portal,\n title,\n formatGuru,\n showDetails,\n noBlink,\n className,\n style,\n } = this.props;\n\n const errorToShow: Error | null =\n caught ?? (show ? (forcedError ?? new Error(\"Software Failure\")) : null);\n\n if (!errorToShow) {\n return children ?? null;\n }\n\n let node: ReactNode;\n\n if (fallbackRender) {\n node = fallbackRender({\n error: errorToShow,\n errorInfo,\n resetErrorBoundary: this.resetErrorBoundary,\n });\n } else if (fallback !== undefined) {\n node = fallback;\n } else {\n node = (\n <DefaultGuru\n error={errorToShow}\n errorInfo={errorInfo}\n title={title}\n formatGuru={formatGuru}\n showDetails={showDetails ?? true}\n noBlink={noBlink ?? false}\n className={className}\n style={style}\n portaled={Boolean(portal)}\n />\n );\n }\n\n if (portal) {\n return <PortalWrapper target={portal}>{node}</PortalWrapper>;\n }\n\n return node;\n }\n}\n\ninterface DefaultGuruProps {\n error: Error;\n errorInfo: ErrorInfo | null;\n title?: ReactNode;\n formatGuru?: (error: Error) => ReactNode;\n showDetails: boolean;\n noBlink: boolean;\n className?: string;\n style?: CSSProperties;\n portaled: boolean;\n}\n\nfunction DefaultGuru({\n error,\n errorInfo,\n title,\n formatGuru,\n showDetails,\n noBlink,\n className,\n style,\n portaled,\n}: DefaultGuruProps) {\n const message = formatGuru ? formatGuru(error) : error.message || error.toString();\n const classes = [styles.root, noBlink && !portaled ? styles.noBlink : null, className]\n .filter(Boolean)\n .join(\" \");\n return (\n <div role=\"alert\" aria-live=\"assertive\" className={classes} style={style}>\n <p className={styles.title}>{title ?? DEFAULT_TITLE}</p>\n <p className={styles.message}>\n Guru Meditation: <span className={styles.code}>{message}</span>\n </p>\n {showDetails && errorInfo?.componentStack ? (\n <details className={styles.details}>\n <summary>Component stack</summary>\n <pre>\n <code>{errorInfo.componentStack}</code>\n </pre>\n </details>\n ) : null}\n </div>\n );\n}\n\ninterface PortalWrapperProps {\n target: AmigaPortalTarget;\n children: ReactNode;\n}\n\nfunction PortalWrapper({ target, children }: PortalWrapperProps) {\n const [container, setContainer] = useState<HTMLElement | null>(null);\n\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n\n if (target instanceof HTMLElement) {\n setContainer(target);\n return;\n }\n\n const id = typeof target === \"string\" ? target : \"amiga-guru\";\n let el = document.getElementById(id);\n let created = false;\n\n if (!el) {\n el = document.createElement(\"div\");\n el.id = id;\n document.body.appendChild(el);\n created = true;\n }\n\n setContainer(el);\n\n return () => {\n if (created && el?.parentNode) {\n el.parentNode.removeChild(el);\n }\n };\n }, [target]);\n\n if (!container) return null;\n\n return createPortal(<div className={styles.portal}>{children}</div>, container);\n}\n\n/**\n * Imperatively trigger an Amiga Guru Meditation from a function component.\n * Returns a callback that re-throws the given error during render so the\n * nearest <AmigaErrorBoundary> can catch it.\n */\nexport function useAmigaGuru() {\n const [, setState] = useState<unknown>();\n return useCallback((error: unknown) => {\n setState(() => {\n throw error instanceof Error ? error : new Error(String(error));\n });\n }, []);\n}\n"]}