WIP: React Native modal fixes #1

Draft
MikeFaith wants to merge 2 commits from MikeFaith/dt-design-system:main into main
3 changed files with 112 additions and 213 deletions
+19
View File
@@ -1,5 +1,24 @@
# @dangerousthings/react-native
## 0.5.1
### Patch Changes
- 28937ea: DTCard: draw the frame with Views instead of full-height SVGs so tall cards render correctly.
react-native-svg draws a tall `Svg` progressively lower the taller it is (about 3px at 1200dp
and 10px at 3800dp on a Pixel 7a), while a plain View lands exactly on its layout box. The
card frame, drawn as three full-height SVG overlays, therefore slid down relative to the
header on tall cards and opened a black notch in the top-left corner where the border and
progress strip should meet the header.
Every straight edge and the progress strip are now absolutely positioned Views driven by
layout, with no measurement dependency, and SVG is used only for the two beveled bottom
corners (1632dp, which cannot drift measurably). The bottom-right corner is split into a
background layer under the content and an accent band over it, so content reaching into
the bevel is no longer painted over. Adjacent pieces overlap by 1dp under the layers drawn
on top to avoid sub-pixel seams. No API change; the classic (non-bevel) branch is untouched.
## 0.5.0
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@dangerousthings/react-native",
"version": "0.5.0",
"version": "0.5.1",
"description": "React Native themed components for the Dangerous Things design system",
"license": "MIT",
"author": {
+92 -212
View File
@@ -20,7 +20,6 @@ import {Text} from 'react-native-paper';
import Svg, {Path} from 'react-native-svg';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
import {buildCardBevelPath} from '../utils/bevelPaths';
import {useComponentLayout} from '../utils/useComponentLayout';
interface DTCardProps {
@@ -96,116 +95,69 @@ interface DTCardProps {
onPress?: () => void;
}
/**
* Build inner card path with left edge at bevelSizeSmall (for progress bar zone).
* 5-point polygon — no bottom-left bevel, straight vertical left edge.
* Why the frame is built from Views, not one tall SVG:
*
* react-native-svg draws a tall `Svg` progressively lower the taller it is (≈3px at
* 1200dp, ≈10px at 3800dp on a Pixel 7a) while a plain View lands exactly on its layout
* box. A card frame drawn as full-height SVG paths therefore slides down relative to the
* header on tall cards and opens a notch in the top-left corner. So every straight edge
* and the progress strip are absolutely positioned Views driven by layout (no measuring,
* no first-frame gap), and SVG is used only for the two beveled bottom corners, which are
* 1632dp tall and cannot drift measurably.
*/
function buildInnerCardPath(
w: number,
h: number,
bevelBR: number,
bw: number,
bevelSizeSmall: number,
): string {
const right = w - bw;
const top = bw;
const bottom = h - bw;
const br = Math.min(
bevelBR - bw,
(right - bevelSizeSmall) / 3,
(bottom - top) / 3,
/**
* Bottom-right corner, in two layers so content is never painted over: the card background
* inside the bevel goes UNDER the content, and only the accent border band goes over it.
* Outside the outer diagonal nothing is painted, as before.
*/
function CornerBottomRightBg({size, bw, bg}: {size: number; bw: number; bg: string}) {
const s = size;
const i = s - bw;
return (
<Svg pointerEvents="none" style={[styles.abs, {right: 0, bottom: 0}]} width={s} height={s} viewBox={`0 0 ${s} ${s}`}>
<Path d={`M 0 0 L ${i} 0 L 0 ${i} Z`} fill={bg} />
</Svg>
);
}
function CornerBottomRightBorder({size, bw, accent}: {size: number; bw: number; accent: string}) {
const s = size;
const i = s - bw;
return (
<Svg pointerEvents="none" style={[styles.abs, {right: 0, bottom: 0, zIndex: 2}]} width={s} height={s} viewBox={`0 0 ${s} ${s}`}>
<Path d={`M ${i} 0 L ${s} 0 L 0 ${s} L 0 ${i} Z`} fill={accent} />
</Svg>
);
if (right <= bevelSizeSmall || bottom <= top) return '';
return [
`M ${bevelSizeSmall} ${top}`,
`L ${right} ${top}`,
`L ${right} ${bottom - br}`,
`L ${right - br} ${bottom}`,
`L ${bevelSizeSmall} ${bottom}`,
'Z',
].join(' ');
}
/**
* Build the full progress bar fill area path (inset 3px from frame on all sides).
* This is the region where the surface/accent gradient fills.
* Bottom-left corner: the outer bevel diagonal, the border band, and the beveled bottom of
* the progress strip. The strip's bottom triangle is olive (accent under 60% background)
* when empty and solid accent once any progress fills it, matching the strip above.
*/
function buildProgressAreaPath(
cardH: number,
bw: number,
bevelSizeSmall: number,
): string {
const left = bw;
const right = bevelSizeSmall - bw;
const top = bw;
const bevelStartY = cardH - bevelSizeSmall;
const bottomRight = cardH - bw * 2; // y at bottom-right of fill area (on bevel diagonal)
if (right <= left || bevelStartY <= top) return '';
return [
`M ${left} ${top}`,
`L ${right} ${top}`,
`L ${right} ${bottomRight}`,
`L ${left} ${bevelStartY}`,
'Z',
].join(' ');
function CornerBottomLeft({size, bw, accent, bg, filled}: {size: number; bw: number; accent: string; bg: string; filled: boolean}) {
const s = size;
// strip triangle: (bw,0) → (s-bw,0) → (s-bw, s-2bw)
const tri = `M ${bw} 0 L ${s - bw} 0 L ${s - bw} ${s - 2 * bw} Z`;
return (
<Svg
pointerEvents="none"
style={[styles.abs, {left: 0, bottom: 0, zIndex: 2}]}
width={s}
height={s}
viewBox={`0 0 ${s} ${s}`}>
{/* everything above the outer diagonal is card (accent border band by default) */}
<Path d={`M 0 0 L ${s} 0 L ${s} ${s} Z`} fill={accent} />
<Path d={tri} fill={accent} />
{!filled && <Path d={tri} fill={bg} opacity={0.6} />}
</Svg>
);
}
/**
* Build the accent-colored portion of the progress bar (fills from bottom).
* Returns empty string when progress is 0.
*/
function buildProgressFillPath(
cardH: number,
bw: number,
bevelSizeSmall: number,
progress: number,
): string {
const p = Math.max(0, Math.min(1, progress));
if (p === 0) return '';
const left = bw;
const right = bevelSizeSmall - bw;
const top = bw;
const bevelStartY = cardH - bevelSizeSmall;
const bottomRight = cardH - bw * 2;
if (right <= left || bevelStartY <= top) return '';
// Total fill area height (along the left edge, from top to bevelStartY)
const areaH = bevelStartY - top;
// How much of the area is filled from the bottom
const fillTop = top + areaH * (1 - p);
if (fillTop >= bevelStartY) {
// Fill is entirely within the bevel zone
// The bevel diagonal goes from (left, bevelStartY) to (right, bottomRight)
// At y=fillTop, x on the diagonal: x = left + (fillTop - bevelStartY) * (right - left) / (bottomRight - bevelStartY)
const bevelH = bottomRight - bevelStartY;
if (bevelH <= 0) return '';
const xAtFillTop = left + ((fillTop - bevelStartY) / bevelH) * (right - left);
if (xAtFillTop >= right) return '';
return [
`M ${xAtFillTop} ${fillTop}`,
`L ${right} ${fillTop}`,
`L ${right} ${bottomRight}`,
'Z',
].join(' ');
}
// Fill extends above the bevel zone — rectangle + bevel triangle
return [
`M ${left} ${fillTop}`,
`L ${right} ${fillTop}`,
`L ${right} ${bottomRight}`,
`L ${left} ${bevelStartY}`,
'Z',
].join(' ');
}
/**
* DT-styled Card component with SVG beveled corners
* DT-styled Card component with beveled corners
*
* @example
* <DTCard title="Detected Chip" mode="normal">
@@ -240,30 +192,15 @@ export function DTCard({
const accentColor = getVariantColor(theme, mode, borderColor);
const shouldShowHeader = showHeader ?? !!title;
const bgColor = backgroundColor ?? theme.colors.background;
const {width, height} = dimensions;
const useBevels = theme.custom.bevelMd > 0;
const bw = borderWidth;
const p = Math.max(0, Math.min(1, progress));
// Outer bevel path (full card shape)
const outerPath = useBevels && hasDimensions
? buildCardBevelPath(width, height, bevelSize, bevelSizeSmall, 0)
: '';
// Inner bevel path — left edge at bevelSizeSmall (progress bar zone)
const innerPath = useBevels && hasDimensions
? buildInnerCardPath(width, height, bevelSize, borderWidth, bevelSizeSmall)
: '';
// Progress bar paths — computed once, reused in progress SVG and frame overlay
const progressAreaPath = useBevels && hasDimensions
? buildProgressAreaPath(height, borderWidth, bevelSizeSmall)
: '';
const progressFillPath = useBevels && hasDimensions && progress > 0
? buildProgressFillPath(height, borderWidth, bevelSizeSmall, progress)
: '';
// Frame overlay: outer + inner + progressArea hole (evenodd punches window for progress bar)
const framePath = useBevels && hasDimensions
? outerPath + ' ' + innerPath + (progressAreaPath ? ' ' + progressAreaPath : '')
: '';
// Bevels shrink on tiny cards, as the old path builder did (min of a third of each side).
const {width, height} = dimensions;
const clamp = (v: number) => (hasDimensions ? Math.min(v, width / 3, height / 3) : v);
const bev = clamp(bevelSize);
const bss = clamp(bevelSizeSmall);
const content = (
<View
@@ -279,63 +216,21 @@ export function DTCard({
style,
]}
onLayout={onLayout}>
{/* Background fill (behind content) — beveled mode only */}
{useBevels && hasDimensions && (
<Svg
style={StyleSheet.absoluteFill}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}>
<Path d={innerPath} fill={bgColor} />
</Svg>
)}
{/* Progress bar fill area — beveled mode */}
{useBevels && hasDimensions && (
<Svg
style={[StyleSheet.absoluteFill, {zIndex: 1}]}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
pointerEvents="none">
{/* Accent base — visible through the semi-transparent surface above.
Matches web where ::after sits over the accent-colored card bg. */}
<Path d={progressAreaPath} fill={accentColor} />
{/* Surface fill over accent base — 0.6 opacity lets accent bleed through.
Matches web --dt-progress-empty-opacity default. */}
<Path
d={progressAreaPath}
fill={bgColor}
opacity={0.6}
/>
{/* Accent fill from bottom (progressed portion) */}
{progressFillPath !== '' && (
<Path
d={progressFillPath}
fill={accentColor}
/>
)}
</Svg>
)}
{/* Progress bar — non-beveled (classic) mode */}
{!useBevels && (
<View style={[styles.progressBar, {
width: bevelSizeSmall,
left: borderWidth,
top: borderWidth,
bottom: borderWidth,
borderBottomLeftRadius: theme.custom.radius > 0 ? Math.max(0, theme.custom.radius - borderWidth) : 0,
}]}>
<View
style={[
styles.progressFill,
{
backgroundColor: accentColor,
height: `${Math.round(Math.max(0, Math.min(1, progress)) * 100)}%`,
},
]}
/>
</View>
{useBevels && (
<>
{/* Body background: the inner rectangle minus the bottom-right corner square. */}
<View pointerEvents="none" style={[styles.abs, {left: bss - 1, top: bw - 1, right: bw - 1, bottom: bev - 1, backgroundColor: bgColor}]} />
<View pointerEvents="none" style={[styles.abs, {left: bss - 1, right: bev - 1, bottom: bw - 1, height: bev, backgroundColor: bgColor}]} />
<CornerBottomRightBg size={bev} bw={bw} bg={bgColor} />
{/* Progress strip: accent under 60% background, filled from the bottom by `progress`. */}
<View pointerEvents="none" style={[styles.abs, {left: bw - 1, width: bss - 2 * bw + 2, top: bw - 1, bottom: bss - 1, backgroundColor: accentColor, zIndex: 1, overflow: 'hidden'}]}>
<View style={[StyleSheet.absoluteFill, {backgroundColor: bgColor, opacity: 0.6}]} />
{p > 0 && <View style={[styles.abs, {left: 0, right: 0, bottom: 0, height: `${Math.round(p * 100)}%`, backgroundColor: accentColor}]} />}
</View>
</>
)}
<View style={[styles.innerContainer, useBevels && {paddingLeft: bevelSizeSmall - borderWidth}]}>
{shouldShowHeader && (
<View style={[styles.header, {backgroundColor: accentColor}, growArea === 'title' && {flexGrow: 1}, !!(title && headerRight) && styles.headerRow]}>
@@ -351,23 +246,18 @@ export function DTCard({
)}
<View style={[styles.content, {padding}, growArea === 'body' && {flexGrow: 1}, contentStyle]}>{children}</View>
</View>
{/* Frame overlay (above content) — beveled mode only.
Uses evenodd with 3 sub-paths: outer + inner + progressArea.
The progress area path punches a hole in the frame so the
progress bar SVG underneath (zIndex:1) shows through. */}
{useBevels && hasDimensions && (
<Svg
style={[StyleSheet.absoluteFill, styles.frameOverlay]}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
pointerEvents="none">
<Path
d={framePath}
fillRule="evenodd"
fill={accentColor}
/>
</Svg>
{useBevels && (
<>
{/* Border edges as Views: top, left outer, left inner (strip/body divider), right, bottom. */}
<View pointerEvents="none" style={[styles.abs, styles.edge, {left: 0, right: 0, top: 0, height: bw, backgroundColor: accentColor}]} />
<View pointerEvents="none" style={[styles.abs, styles.edge, {left: 0, width: bw, top: 0, bottom: bss - 1, backgroundColor: accentColor}]} />
<View pointerEvents="none" style={[styles.abs, styles.edge, {left: bss - bw, width: bw, top: 0, bottom: bss - 1, backgroundColor: accentColor}]} />
<View pointerEvents="none" style={[styles.abs, styles.edge, {right: 0, width: bw, top: 0, bottom: bev - 1, backgroundColor: accentColor}]} />
<View pointerEvents="none" style={[styles.abs, styles.edge, {left: bss - 1, right: bev - 1, bottom: 0, height: bw, backgroundColor: accentColor}]} />
<CornerBottomRightBorder size={bev} bw={bw} accent={accentColor} />
<CornerBottomLeft size={bss} bw={bw} accent={accentColor} bg={bgColor} filled={p > 0} />
</>
)}
</View>
);
@@ -408,6 +298,12 @@ const styles = StyleSheet.create({
container: {
position: 'relative',
},
abs: {
position: 'absolute',
},
edge: {
zIndex: 2,
},
innerContainer: {
position: 'relative',
zIndex: 1,
@@ -434,20 +330,4 @@ const styles = StyleSheet.create({
letterSpacing: 0.5,
},
content: {},
frameOverlay: {
zIndex: 2,
},
progressBar: {
position: 'absolute' as const,
left: 0,
top: 0,
bottom: 0,
backgroundColor: 'transparent',
zIndex: 3,
justifyContent: 'flex-end' as const,
overflow: 'hidden' as const,
},
progressFill: {
width: '100%' as const,
},
});