From 28937ea6732f38d61c37d6dcbde3211aaec2e798 Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 13 Sep 2026 08:51:00 -0700 Subject: [PATCH 1/2] fix(react-native): DTCard frame from Views, SVG only for bevel corners, so tall cards render correctly 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, so the three full-height SVG overlays slid down relative to the header on tall cards and opened a black notch in the top-left corner. Straight edges and the progress strip are now layout-driven Views (no measuring, no first-frame gap); only the two beveled bottom corners stay SVG. The bottom-right corner is split into a background layer under the content and an accent band over it so content is never painted over; pieces overlap 1dp to avoid seams. No API change; classic (non-bevel) branch untouched. Verified on device at 60/1200/4000dp. Co-Authored-By: Claude Fable 5.1 --- .changeset/dtcard-tall-frame.md | 18 ++ .../react-native/src/components/DTCard.tsx | 304 ++++++------------ 2 files changed, 110 insertions(+), 212 deletions(-) create mode 100644 .changeset/dtcard-tall-frame.md diff --git a/.changeset/dtcard-tall-frame.md b/.changeset/dtcard-tall-frame.md new file mode 100644 index 0000000..cf11cf9 --- /dev/null +++ b/.changeset/dtcard-tall-frame.md @@ -0,0 +1,18 @@ +--- +"@dangerousthings/react-native": patch +--- + +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. 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 ( + + {/* everything above the outer diagonal is card (accent border band by default) */} + + + {!filled && } + + ); } /** - * 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 && ( - - {/* Accent base — visible through the semi-transparent surface above. - Matches web where ::after sits over the accent-colored card bg. */} - - {/* Surface fill over accent base — 0.6 opacity lets accent bleed through. - Matches web --dt-progress-empty-opacity default. */} - - {/* Accent fill from bottom (progressed portion) */} - {progressFillPath !== '' && ( - - )} - - )} - {/* 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, - }, }); -- 2.54.0 From 77664498a4838aa37a6e5843018dc92817fcbf7e Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 13 Sep 2026 08:54:27 -0700 Subject: [PATCH 2/2] chore(react-native): version 0.5.1 for the tall-card frame fix Co-Authored-By: Claude Fable 5.1 --- .changeset/dtcard-tall-frame.md | 18 ------------------ packages/react-native/CHANGELOG.md | 19 +++++++++++++++++++ packages/react-native/package.json | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) delete mode 100644 .changeset/dtcard-tall-frame.md diff --git a/.changeset/dtcard-tall-frame.md b/.changeset/dtcard-tall-frame.md deleted file mode 100644 index cf11cf9..0000000 --- a/.changeset/dtcard-tall-frame.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@dangerousthings/react-native": patch ---- - -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. 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": { -- 2.54.0