diff --git a/packages/react-native/CHANGELOG.md b/packages/react-native/CHANGELOG.md
index 69c036a..d065ba5 100644
--- a/packages/react-native/CHANGELOG.md
+++ b/packages/react-native/CHANGELOG.md
@@ -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 (16–32dp, 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
diff --git a/packages/react-native/package.json b/packages/react-native/package.json
index 9c8e411..24032e6 100644
--- a/packages/react-native/package.json
+++ b/packages/react-native/package.json
@@ -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": {
diff --git a/packages/react-native/src/components/DTCard.tsx b/packages/react-native/src/components/DTCard.tsx
index 10e92f9..45e0663 100644
--- a/packages/react-native/src/components/DTCard.tsx
+++ b/packages/react-native/src/components/DTCard.tsx
@@ -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
+ * 16–32dp 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 (
+
+ );
+}
+function CornerBottomRightBorder({size, bw, accent}: {size: number; bw: number; accent: string}) {
+ const s = size;
+ const i = s - bw;
+ return (
+
);
- 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 (
+
+ );
}
/**
- * 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
*
@@ -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 = (
- {/* Background fill (behind content) — beveled mode only */}
- {useBevels && hasDimensions && (
-
- )}
- {/* Progress bar fill area — beveled mode */}
- {useBevels && hasDimensions && (
-
- )}
- {/* Progress bar — non-beveled (classic) mode */}
- {!useBevels && (
- 0 ? Math.max(0, theme.custom.radius - borderWidth) : 0,
- }]}>
-
-
+ {useBevels && (
+ <>
+ {/* Body background: the inner rectangle minus the bottom-right corner square. */}
+
+
+
+
+ {/* Progress strip: accent under 60% background, filled from the bottom by `progress`. */}
+
+
+ {p > 0 && }
+
+ >
)}
+
{shouldShowHeader && (
@@ -351,23 +246,18 @@ export function DTCard({
)}
{children}
- {/* 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 && (
-
+
+ {useBevels && (
+ <>
+ {/* Border edges as Views: top, left outer, left inner (strip/body divider), right, bottom. */}
+
+
+
+
+
+
+ 0} />
+ >
)}
);
@@ -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,
- },
});