fix(desktop): global box-sizing + per-pane error boundary + Conversation defensives

Two bugs reported after v2.2.0:

1. Input fields and textareas overflowed their container — typing in
   Settings / SendModal / NewContactModal would push the border past
   the card edge because the renderer's default box-sizing was
   content-box and `width: 100%` + padding pushed widths past parents.
   Added `*, *::before, *::after { box-sizing: border-box; }` to
   index.html. Removes the need for per-element `boxSizing: 'border-box'`
   (the existing sprinkles stay for clarity but are now redundant).

2. App went blank when opening a chat — any throw inside Conversation
   propagated up through Shell and wiped the whole window, with no way
   to navigate out. Added PaneBoundary, a React error boundary scoped
   to one Shell pane, keyed on `${section}-(list|detail)` so it resets
   when the user switches section. Now a crash shows an inline error
   card with message + stack + Retry, while NavBar + StatusBar stay
   usable.

   Also hardened Conversation against edge cases that were candidates
   for the original crash:
     * `name` always falls back to shortAddr(address) if all other
       branches produce an empty string.
     * first letter used for the avatar is computed once, guarded
       against empty input with a `?` fallback.
     * Header name + short-address line get whiteSpace/overflow/ellipsis
       so very long contacts no longer escape the 32px wide sub-column
       the way they did for the reporter.

Fonts normalised in the global CSS too — inputs/textareas/buttons now
inherit `font-family` instead of the browser default, which was
breaking the visual rhythm in the Settings cards.
This commit is contained in:
vsecoder
2026-04-22 18:53:37 +03:00
parent 6b7cb1c5a9
commit 481d4d2fa8
4 changed files with 94 additions and 8 deletions

View File

@@ -9,6 +9,13 @@
dev vs. production rules cleanly. -->
<title>DChain</title>
<style>
/* Global box-sizing — every padding+border counts toward the declared
width, not on top of it. Without this, `<input style="width:100%">`
inside a padded flex container visibly overflows its parent on
every modal / Settings card. Applied universally because almost
every per-element override was forgetting it anyway. */
*, *::before, *::after { box-sizing: border-box; }
html, body, #root { margin: 0; padding: 0; height: 100%; background: #000; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
@@ -23,6 +30,11 @@
user-select: text;
-webkit-user-select: text;
}
/* Form elements should never paint their own background/border over
our dark theme. Each component still sets its own explicit colours. */
input, textarea, button {
font-family: inherit;
}
</style>
</head>
<body>

View File

@@ -98,12 +98,13 @@ export function Conversation({ address }: { address: string }): React.ReactEleme
}
};
const name = contact?.username ? `@${contact.username}`
const name = (contact?.username ? `@${contact.username}`
: contact?.alias
? contact.alias
: isSelf
? 'Saved Messages'
: shortAddr(address, 8);
: shortAddr(address || '', 8)) || shortAddr(address || '', 8);
const firstLetter = (name || '?').replace(/^@/, '').charAt(0).toUpperCase() || '?';
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
@@ -118,12 +119,18 @@ export function Conversation({ address }: { address: string }): React.ReactEleme
color: '#fff', fontWeight: 700, fontSize: 14,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{isSelf ? '★' : name.replace(/^@/, '').charAt(0).toUpperCase()}
{isSelf ? '★' : firstLetter}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: '#fff', fontSize: 14, fontWeight: 700 }}>{name}</div>
<div style={{ color: '#6a6a6a', fontSize: 11, fontFamily: 'monospace' }}>
{shortAddr(address, 6)}
<div style={{
color: '#fff', fontSize: 14, fontWeight: 700,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
}}>{name}</div>
<div style={{
color: '#6a6a6a', fontSize: 11, fontFamily: 'monospace',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
}}>
{shortAddr(address || '', 6)}
</div>
</div>
</div>

View File

@@ -0,0 +1,62 @@
// PaneBoundary — ErrorBoundary scoped to one Shell pane. A crash in
// the Conversation component shouldn't black-out the whole window; it
// should leave NavBar + List + StatusBar usable so the operator can
// switch sections and report the bug. Resets when the keyed section
// changes.
import React from 'react';
interface Props {
/** Used as React key at the callsite; also shown in the panic copy. */
sectionName: string;
children: React.ReactNode;
}
interface State {
error: Error | null;
}
export class PaneBoundary extends React.Component<Props, State> {
state: State = { error: null };
static getDerivedStateFromError(error: Error): State {
return { error };
}
componentDidCatch(error: Error, info: React.ErrorInfo): void {
console.error(`[PaneBoundary:${this.props.sectionName}]`, error, info);
}
render(): React.ReactNode {
if (!this.state.error) return this.props.children;
return (
<div style={{
padding: 20, height: '100%', overflow: 'auto',
background: '#000', color: '#fff', fontFamily: 'monospace',
}}>
<div style={{
color: '#ff6b6b', fontSize: 14, fontWeight: 700, marginBottom: 8,
}}>
{this.props.sectionName} crashed
</div>
<div style={{ color: '#fff', fontSize: 13, marginBottom: 8 }}>
{this.state.error.message}
</div>
<pre style={{
color: '#8b8b8b', fontSize: 11, lineHeight: 1.4,
whiteSpace: 'pre-wrap', wordBreak: 'break-word',
}}>
{this.state.error.stack}
</pre>
<button
onClick={() => this.setState({ error: null })}
style={{
marginTop: 10, padding: '6px 12px', borderRadius: 999,
border: '1px solid #1f1f1f', background: '#111',
color: '#fff', fontSize: 12, cursor: 'pointer',
}}
>Retry</button>
</div>
);
}
}

View File

@@ -24,6 +24,7 @@ import { TitleBar } from './TitleBar';
import { NavBar } from './NavBar';
import { StatusBar } from './StatusBar';
import { UpdateBanner } from './UpdateBanner';
import { PaneBoundary } from './PaneBoundary';
import { MessagesList, MessagesDetail } from '@/sections/messages';
import { FeedList, FeedDetail } from '@/sections/feed';
import { WalletList, WalletDetail } from '@/sections/wallet';
@@ -51,10 +52,14 @@ export function Shell(): React.ReactElement {
borderRight: '1px solid #1f1f1f',
overflowY: 'auto',
}}>
<PaneBoundary key={`${section}-list`} sectionName={`${section} / list`}>
<List />
</PaneBoundary>
</div>
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
<PaneBoundary key={`${section}-detail`} sectionName={`${section} / detail`}>
<Detail />
</PaneBoundary>
</div>
</div>
<UpdateBanner />