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:
@@ -9,6 +9,13 @@
|
|||||||
dev vs. production rules cleanly. -->
|
dev vs. production rules cleanly. -->
|
||||||
<title>DChain</title>
|
<title>DChain</title>
|
||||||
<style>
|
<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; }
|
html, body, #root { margin: 0; padding: 0; height: 100%; background: #000; }
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
@@ -23,6 +30,11 @@
|
|||||||
user-select: text;
|
user-select: text;
|
||||||
-webkit-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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -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
|
||||||
? contact.alias
|
? contact.alias
|
||||||
: isSelf
|
: isSelf
|
||||||
? 'Saved Messages'
|
? 'Saved Messages'
|
||||||
: shortAddr(address, 8);
|
: shortAddr(address || '', 8)) || shortAddr(address || '', 8);
|
||||||
|
const firstLetter = (name || '?').replace(/^@/, '').charAt(0).toUpperCase() || '?';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
<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,
|
color: '#fff', fontWeight: 700, fontSize: 14,
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
}}>
|
}}>
|
||||||
{isSelf ? '★' : name.replace(/^@/, '').charAt(0).toUpperCase()}
|
{isSelf ? '★' : firstLetter}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div style={{ color: '#fff', fontSize: 14, fontWeight: 700 }}>{name}</div>
|
<div style={{
|
||||||
<div style={{ color: '#6a6a6a', fontSize: 11, fontFamily: 'monospace' }}>
|
color: '#fff', fontSize: 14, fontWeight: 700,
|
||||||
{shortAddr(address, 6)}
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
62
desktop/src/shell/PaneBoundary.tsx
Normal file
62
desktop/src/shell/PaneBoundary.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import { TitleBar } from './TitleBar';
|
|||||||
import { NavBar } from './NavBar';
|
import { NavBar } from './NavBar';
|
||||||
import { StatusBar } from './StatusBar';
|
import { StatusBar } from './StatusBar';
|
||||||
import { UpdateBanner } from './UpdateBanner';
|
import { UpdateBanner } from './UpdateBanner';
|
||||||
|
import { PaneBoundary } from './PaneBoundary';
|
||||||
import { MessagesList, MessagesDetail } from '@/sections/messages';
|
import { MessagesList, MessagesDetail } from '@/sections/messages';
|
||||||
import { FeedList, FeedDetail } from '@/sections/feed';
|
import { FeedList, FeedDetail } from '@/sections/feed';
|
||||||
import { WalletList, WalletDetail } from '@/sections/wallet';
|
import { WalletList, WalletDetail } from '@/sections/wallet';
|
||||||
@@ -51,10 +52,14 @@ export function Shell(): React.ReactElement {
|
|||||||
borderRight: '1px solid #1f1f1f',
|
borderRight: '1px solid #1f1f1f',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
}}>
|
}}>
|
||||||
<List />
|
<PaneBoundary key={`${section}-list`} sectionName={`${section} / list`}>
|
||||||
|
<List />
|
||||||
|
</PaneBoundary>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
|
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
|
||||||
<Detail />
|
<PaneBoundary key={`${section}-detail`} sectionName={`${section} / detail`}>
|
||||||
|
<Detail />
|
||||||
|
</PaneBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UpdateBanner />
|
<UpdateBanner />
|
||||||
|
|||||||
Reference in New Issue
Block a user