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. -->
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
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 { 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 />
|
||||
|
||||
Reference in New Issue
Block a user