All files / web/src/components/create/abacus FabricationErrorBoundary.tsx

0% Statements 0/95
0% Branches 0/1
0% Functions 0/1
0% Lines 0/95

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96                                                                                                                                                                                               
'use client'

// A React error boundary scoped to ONE studio pane (Gitea epic #5).
//
// The studio's shared state lives in AbacusStudioProvider, which wraps the whole
// page — so a throw in a hook THERE unwinds past every pane to the route's
// error.tsx ("Something broke") and blanks the entire studio, design rail
// included. The structural fix is to keep the provider's derivations total (see
// hexRGB / catalogFromParams), so the print/filament math degrades instead of
// throwing. This boundary is the second layer: wrap each FDM output pane (the 3D
// viewer, the fabrication rail) so any fault that DOES originate inside one of
// them surfaces contextually — a compact notice in that pane, with a retry —
// while the rest of the studio (the design controls above all) keeps working.

import { Component, type ReactNode } from 'react'
import { button, notice, STUDIO } from '@/components/studio/theme'

interface Props {
  /** Named in the fallback + console so a caught error says which pane broke. */
  label: string
  children: ReactNode
}

interface State {
  error: Error | null
}

export class FabricationErrorBoundary extends Component<Props, State> {
  state: State = { error: null }

  static getDerivedStateFromError(error: Error): State {
    return { error }
  }

  componentDidCatch(error: Error, info: { componentStack?: string | null }) {
    console.error(`[FabricationErrorBoundary:${this.props.label}]`, error, info?.componentStack)
  }

  reset = () => this.setState({ error: null })

  render() {
    const { error } = this.state
    if (!error) return this.props.children
    return (
      <div
        data-component="fabrication-error-boundary"
        data-error-pane={this.props.label}
        style={{
          ...notice('danger'),
          display: 'flex',
          flexDirection: 'column',
          gap: 10,
          padding: 16,
          margin: 12,
          borderRadius: STUDIO.radius.panel,
        }}
      >
        <div style={{ fontWeight: 700, display: 'flex', alignItems: 'center', gap: 6 }}>
          <span aria-hidden="true">⛔</span> The {this.props.label} hit an error
        </div>
        <div style={{ opacity: 0.85 }}>
          The rest of the studio still works — your design and the other panels are unaffected.
          Retry this panel, or keep designing and try again.
        </div>
        <button
          type="button"
          data-action="retry-fabrication-pane"
          onClick={this.reset}
          style={{ ...button('fix'), alignSelf: 'flex-start' }}
        >
          Try again
        </button>
        <details style={{ opacity: 0.7 }}>
          <summary style={{ cursor: 'pointer', fontSize: 11 }}>Technical details</summary>
          <div style={{ marginTop: 6, fontWeight: 600 }}>{error.message}</div>
          {error.stack && (
            <pre
              style={{
                marginTop: 6,
                fontSize: 10,
                fontFamily: 'monospace',
                whiteSpace: 'pre-wrap',
                wordBreak: 'break-word',
                maxHeight: 160,
                overflow: 'auto',
              }}
            >
              {error.stack}
            </pre>
          )}
        </details>
      </div>
    )
  }
}