Two failure modes of a sidebar render crash, fixed together: 1. Layout-push leak (root cause): the layout-push effect wrote --dsh-sidebar-width/--dsh-sidebar-height on document.documentElement without a cleanup, so ANY unmount of the Sidebar (error-boundary swap, plugin disable, HMR) left the variables behind and layout.css kept squeezing #root with a stale margin — "the sidebar cannot be hidden" until a full reload. The effect now removes both properties on cleanup (the CSS fallback 0px restores the layout). 2. Crash containment (UX): the root error boundary previously replaced the WHOLE sidebar — toggle cluster included — with an error strip on any tab crash. A new shared RenderBoundary (src/client/RenderBoundary.tsx) now scopes containment per tab: a crashing viewer/editor shows a dismissible strip inside its own pane while the toggle cluster, the other tabs, and the panel stay alive. The descriptor is rendered as a real element (createElement) so even top-level component throws land in the boundary, not in TabContent's own render. The root boundary remains as the last resort for Workbench/shell errors. Tests (tests/sidebar-crash.spec.tsx, jsdom + real Sidebar/store harness): - unmount clears both CSS variables (red before the fix: 307px leaked) - a crashing tab is contained: strip in-pane, collapse button alive, layout push still live - the retry button recovers a tab whose crash has since been fixed Full suite: 425 tests pass; tsc --noEmit clean.
50 lines
1.8 KiB
TypeScript
50 lines
1.8 KiB
TypeScript
/**
|
|
* The generic render error boundary for the sidebar tree: a render error in
|
|
* the wrapped subtree shows a dismissible error strip (retry re-renders the
|
|
* children) instead of blanking the shell. Used at two scopes:
|
|
*
|
|
* - ROOT (index.tsx, `css.boundaryError`): last-resort containment for
|
|
* errors in the sidebar shell itself (Workbench, drag layout, …) — a full
|
|
* swap keeps the page alive.
|
|
* - PER-TAB (Sidebar.tsx TabContent, `css.tabBoundaryError`): a crashing
|
|
* viewer/editor shows a strip inside ITS OWN pane; the toggle cluster, the
|
|
* other tabs, and the panel itself stay alive (issue #31 — a tab crash
|
|
* must never take down the whole sidebar).
|
|
*
|
|
* The className prop selects the strip's geometry: the root's full-height
|
|
* fixed rail vs. the tab's pane-filling block.
|
|
*/
|
|
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
|
import { t } from './locales.ts'
|
|
import css from './sidebar.module.css'
|
|
|
|
export class RenderBoundary extends Component<{ children?: ReactNode; className?: string }, { error: string | null }> {
|
|
state = { error: null as string | null }
|
|
|
|
static getDerivedStateFromError(error: unknown): { error: string } {
|
|
return { error: error instanceof Error ? error.message : String(error) }
|
|
}
|
|
|
|
componentDidCatch(error: Error, info: ErrorInfo): void {
|
|
console.error('[dsh-better-sidebar] render error:', error, info.componentStack)
|
|
}
|
|
|
|
render(): ReactNode {
|
|
if (this.state.error !== null) {
|
|
return (
|
|
<div className={this.props.className}>
|
|
<span>dsh-better-sidebar: {this.state.error}</span>
|
|
<button
|
|
type="button"
|
|
className={css.terminalRetry}
|
|
onClick={() => { this.setState({ error: null }) }}
|
|
>
|
|
{t('terminalRetry')}
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
return this.props.children
|
|
}
|
|
}
|