@@ -20,7 +20,6 @@ import {Text} from 'react-native-paper';
import Svg , { Path } from 'react-native-svg' ;
import Svg , { Path } from 'react-native-svg' ;
import { useDTTheme } from '../theme/DTThemeProvider' ;
import { useDTTheme } from '../theme/DTThemeProvider' ;
import { type DTVariant , getVariantColor } from '../utils/variantColors' ;
import { type DTVariant , getVariantColor } from '../utils/variantColors' ;
import { buildCardBevelPath } from '../utils/bevelPaths' ;
import { useComponentLayout } from '../utils/useComponentLayout' ;
import { useComponentLayout } from '../utils/useComponentLayout' ;
interface DTCardProps {
interface DTCardProps {
@@ -96,116 +95,69 @@ interface DTCardProps {
onPress ? : ( ) = > void ;
onPress ? : ( ) = > void ;
}
}
/**
/**
* Build inner card path with left edge at bevelSizeSmall (for progress bar zone).
* Why the frame is built from Views, not one tall SVG:
* 5-point polygon — no bottom-left bevel, straight vertical left edge.
*
* 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 ,
* Bottom-right corner, in two layers so content is never painted over: the card background
bevelBR : number ,
* inside the bevel goes UNDER the content, and only the accent border band goes over it.
bw : number ,
* Outside the outer diagonal nothing is painted, as before.
bevelSizeSmall : number ,
*/
) : string {
function CornerBottomRightBg ( { size , bw , bg } : { size : number ; bw : number ; bg : string } ) {
const right = w - bw ;
const s = size ;
const top = bw ;
const i = s - bw ;
const bottom = h - bw ;
return (
const br = Math . min (
< Svg pointerEvents = "none" style = { [ styles . abs , { right : 0 , bottom : 0 } ] } width = { s } height = { s } viewBox = { ` 0 0 ${ s } ${ s } ` } >
bevelBR - bw ,
< Path d = { ` M 0 0 L ${ i } 0 L 0 ${ i } Z ` } fill = { bg } / >
( right - bevelSizeSmall ) / 3 ,
< / Svg >
( bottom - top ) / 3 ,
) ;
}
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).
* Bottom-left corner: the outer bevel diagonal, the border band, and the beveled bottom of
* This is the region where the surface/accent gradient fills.
* 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 (
function CornerBottomLeft ( { size , bw , accent , bg , filled } : { size : number ; bw : number ; accent : string ; bg : string ; filled : boolean } ) {
cardH : number ,
const s = size ;
bw : number ,
// strip triangle: (bw,0) → (s-bw,0) → (s-bw, s-2bw)
bevelSizeSmall : number ,
const tri = ` M ${ bw } 0 L ${ s - bw } 0 L ${ s - bw } ${ s - 2 * bw } Z ` ;
) : string {
return (
const left = bw ;
< Svg
const right = bevelSizeSmall - bw ;
pointerEvents = "none"
const top = bw ;
style = { [ styles . abs , { left : 0 , bottom : 0 , zIndex : 2 } ] }
const bevelStartY = cardH - bevelSizeSmall ;
width = { s }
const bottomRight = cardH - bw * 2 ; // y at bottom-right of fill area (on bevel diagonal)
height = { s }
viewBox = { ` 0 0 ${ s } ${ s } ` } >
if ( right <= left || bevelStartY <= top ) return '' ;
{ /* 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 } / >
return [
< Path d = { tri } fill = { accent } / >
` M ${ left } ${ top } ` ,
{ ! filled && < Path d = { tri } fill = { bg } opacity = { 0.6 } / > }
` L ${ right } ${ top } ` ,
< / Svg >
` L ${ right } ${ bottomRight } ` ,
) ;
` L ${ left } ${ bevelStartY } ` ,
'Z' ,
] . join ( ' ' ) ;
}
}
/**
/**
* Build the accent-colored portion of the progress bar (fills from bottom).
* DT-styled Card component with beveled corners
* 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
*
*
* @example
* @example
* <DTCard title="Detected Chip" mode="normal">
* <DTCard title="Detected Chip" mode="normal">
@@ -240,30 +192,15 @@ export function DTCard({
const accentColor = getVariantColor ( theme , mode , borderColor ) ;
const accentColor = getVariantColor ( theme , mode , borderColor ) ;
const shouldShowHeader = showHeader ? ? ! ! title ;
const shouldShowHeader = showHeader ? ? ! ! title ;
const bgColor = backgroundColor ? ? theme . colors . background ;
const bgColor = backgroundColor ? ? theme . colors . background ;
const { width , height } = dimensions ;
const useBevels = theme . custom . bevelMd > 0 ;
const useBevels = theme . custom . bevelMd > 0 ;
const bw = borderWidth ;
const p = Math . max ( 0 , Math . min ( 1 , progress ) ) ;
// Outer bevel path (full card shap e)
// Bevels shrink on tiny cards, as the old path builder did (min of a third of each sid e).
const outerPath = useBevels && hasDimensions
const { width , height } = dimensions ;
? buildCardBevelPath ( width , height , bevelSize , bevelSizeSmall , 0 )
const clamp = ( v : number ) = > ( hasDimensions ? Math . min ( v , width / 3 , height / 3 ) : v ) ;
: '' ;
const bev = clamp ( bevelSize ) ;
// Inner bevel path — left edge at bevelSizeSmall (progress bar zone)
const bss = clamp ( bevelSizeSmall ) ;
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 : '' )
: '' ;
const content = (
const content = (
< View
< View
@@ -279,63 +216,21 @@ export function DTCard({
style ,
style ,
] }
] }
onLayout = { onLayout } >
onLayout = { onLayout } >
{ /* Background fill (behind content) — beveled mode only */ }
{ useBevels && (
{ useBevels && hasDimensions && (
< >
< Svg
{ /* Body background: the inner rectangle minus the bottom-right corner square. */ }
style = { StyleSheet . absoluteFill }
< View pointerEvents = "none" style = { [ styles . abs , { left : bss - 1 , top : bw - 1 , right : bw - 1 , bottom : bev - 1 , backgroundColor : bgColor } ] } / >
width = { width }
< View pointerEvents = "none" style = { [ styles . abs , { left : bss - 1 , right : bev - 1 , bottom : bw - 1 , height : bev , backgroundColor : bgColor } ] } / >
height = { height }
< CornerBottomRightBg size = { bev } bw = { bw } bg = { bgColor } / >
viewBox = { ` 0 0 ${ width } ${ height } ` } >
< Path d = { innerPath } fill = { bgColor } / >
{ /* Progress strip: accent under 60% background, filled from the bottom by `progress`. */ }
< / Svg >
< 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 } ] } / >
{ /* Progress bar fill area — beveled mode */ }
{ p > 0 && < View style = { [ styles . abs , { left : 0 , right : 0 , bottom : 0 , height : ` ${ Math . round ( p * 100 ) } % ` , backgroundColor : accentColor } ] } / > }
{ useBevels && hasDimensions && (
< / View >
< 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 >
) }
) }
< View style = { [ styles . innerContainer , useBevels && { paddingLeft : bevelSizeSmall - borderWidth } ] } >
< View style = { [ styles . innerContainer , useBevels && { paddingLeft : bevelSizeSmall - borderWidth } ] } >
{ shouldShowHeader && (
{ shouldShowHeader && (
< View style = { [ styles . header , { backgroundColor : accentColor } , growArea === 'title' && { flexGrow : 1 } , ! ! ( title && headerRight ) && styles . headerRow ] } >
< 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 style = { [ styles . content , { padding } , growArea === 'body' && { flexGrow : 1 } , contentStyle ] } > { children } < / View >
< / View >
< / View >
{ /* Frame overlay (above content) — beveled mode only.
Uses evenodd with 3 sub-paths: outer + inner + progressArea.
{ useBevels && (
The progress area path punches a hole in the frame so the
< >
progress bar SVG underneath (zIndex:1) shows through . */ }
{ /* Border edges as Views: top, left outer, left inner (strip/body divider), right, bottom . */}
{ useBevels && hasDimensions && (
< View pointerEvents = "none" style = { [ styles . abs , styles . edge , { left : 0 , right : 0 , top : 0 , height : bw , backgroundColor : accentColor } ] } / >
< Svg
< View pointerEvents = "none" style = { [ styles . abs , styles . edge , { left : 0 , width : bw , top : 0 , bottom : bss - 1 , backgroundColor : accentColor } ] } / >
style = { [ StyleSheet . absoluteFill , styles . frameOverlay ] }
< View pointerEvents = "none" style = { [ styles . abs , styles . edge , { left : bss - bw , width : bw , top : 0 , bottom : bss - 1 , backgroundColor : accentColor } ] } / >
width = { width }
< View pointerEvents = "none" style = { [ styles . abs , styles . edge , { right : 0 , width : bw , top : 0 , bottom : bev - 1 , backgroundColor : accentColor } ] } / >
height = { height }
< View pointerEvents = "none" style = { [ styles . abs , styles . edge , { left : bss - 1 , right : bev - 1 , bottom : 0 , height : bw , backgroundColor : accentColor } ] } / >
viewBox = { ` 0 0 ${ width } ${ height } ` }
< CornerBottomRightBorder size = { bev } bw = { bw } accent = { accentColor } / >
pointerEvents = "none" >
< CornerBottomLeft size = { bss } bw = { bw } accent = { accentColor } bg = { bgColor } filled = { p > 0 } / >
< Path
< / >
d = { framePath }
fillRule = "evenodd"
fill = { accentColor }
/ >
< / Svg >
) }
) }
< / View >
< / View >
) ;
) ;
@@ -408,6 +298,12 @@ const styles = StyleSheet.create({
container : {
container : {
position : 'relative' ,
position : 'relative' ,
} ,
} ,
abs : {
position : 'absolute' ,
} ,
edge : {
zIndex : 2 ,
} ,
innerContainer : {
innerContainer : {
position : 'relative' ,
position : 'relative' ,
zIndex : 1 ,
zIndex : 1 ,
@@ -434,20 +330,4 @@ const styles = StyleSheet.create({
letterSpacing : 0.5 ,
letterSpacing : 0.5 ,
} ,
} ,
content : { } ,
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 ,
} ,
} ) ;
} ) ;