DTCard: make progress optional, fill top-to-bottom
Progress bar now fills top-down (was bottom-up). When progress prop is
omitted the bar stays empty and the card renders at full size. Only cards
that explicitly pass progress={0–100} show the animated bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,8 @@
|
|||||||
*
|
*
|
||||||
* The progress bar is a structural element on the left edge (0 to bevel-sm).
|
* The progress bar is a structural element on the left edge (0 to bevel-sm).
|
||||||
* It is always present. At 0 progress it shows surface color (empty bar).
|
* It is always present. At 0 progress it shows surface color (empty bar).
|
||||||
* As progress increases, accent color fills from the bottom up.
|
* As progress increases, accent color fills from top to bottom.
|
||||||
|
* When progress is omitted, the bar stays empty (full card, no fill).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ReactNode, CSSProperties } from 'react';
|
import type { ReactNode, CSSProperties } from 'react';
|
||||||
@@ -22,8 +23,14 @@ interface DTCardProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
/** Whether to show the accent header bar @default true when title is provided */
|
/** Whether to show the accent header bar @default true when title is provided */
|
||||||
showHeader?: boolean;
|
showHeader?: boolean;
|
||||||
/** Progress value (0–100) for left-edge vertical progress bar @default 0 */
|
/** Dismiss callback — renders a close button in the header bar */
|
||||||
|
onDismiss?: () => void;
|
||||||
|
/** Progress value (0–100) for left-edge vertical progress bar. Omit for empty bar. */
|
||||||
progress?: number;
|
progress?: number;
|
||||||
|
/** Grow to fill available space in flex/grid containers @default false */
|
||||||
|
grow?: boolean;
|
||||||
|
/** Which area expands when grow is true @default 'body' */
|
||||||
|
growArea?: 'body' | 'title';
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
@@ -34,12 +41,15 @@ export function DTCard({
|
|||||||
variant = 'normal',
|
variant = 'normal',
|
||||||
title,
|
title,
|
||||||
showHeader,
|
showHeader,
|
||||||
progress = 0,
|
onDismiss,
|
||||||
|
progress,
|
||||||
|
grow = false,
|
||||||
|
growArea = 'body',
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
onClick,
|
onClick,
|
||||||
}: DTCardProps) {
|
}: DTCardProps) {
|
||||||
const shouldShowHeader = showHeader ?? !!title;
|
const shouldShowHeader = showHeader ?? !!(title || onDismiss);
|
||||||
const Tag = onClick ? 'button' : 'div';
|
const Tag = onClick ? 'button' : 'div';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,17 +57,36 @@ export function DTCard({
|
|||||||
className={cx(
|
className={cx(
|
||||||
'card',
|
'card',
|
||||||
getVariantClass(variant),
|
getVariantClass(variant),
|
||||||
|
grow && 'dt-card-grow',
|
||||||
|
grow && growArea === 'title' && 'dt-card-grow-title',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
||||||
'--dt-card-progress': progress,
|
...(progress != null && { '--dt-card-progress': progress }),
|
||||||
} as CSSProperties}
|
} as CSSProperties}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
type={onClick ? 'button' : undefined}>
|
type={onClick ? 'button' : undefined}>
|
||||||
{shouldShowHeader && (
|
{shouldShowHeader && (
|
||||||
<div className="card-title">
|
<div className="card-title" style={onDismiss ? { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } : undefined}>
|
||||||
{title}
|
<span>{title}</span>
|
||||||
|
{onDismiss && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onDismiss(); }}
|
||||||
|
aria-label="Close"
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: 0,
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
lineHeight: 1,
|
||||||
|
}}>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
[data-brand="dt"] .card {
|
[data-brand="dt"] .card {
|
||||||
--dt-card-progress: 0;
|
--dt-card-progress: 0;
|
||||||
--_card-pad: var(--space-6);
|
--_card-pad: var(--space-6);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
background: var(--dt-card-color, var(--color-primary));
|
background: var(--dt-card-color, var(--color-primary));
|
||||||
@@ -54,7 +56,8 @@
|
|||||||
|
|
||||||
/* Card Progress Bar — structural fill area on the left edge
|
/* Card Progress Bar — structural fill area on the left edge
|
||||||
Sits between the 3px frame borders, follows bottom-left bevel diagonal.
|
Sits between the 3px frame borders, follows bottom-left bevel diagonal.
|
||||||
Gradient fills from bottom (accent) to top (surface) based on --dt-card-progress.
|
Gradient fills top-to-bottom based on --dt-card-progress so the card
|
||||||
|
visually "opens" as the bar descends.
|
||||||
Filled portion: fully opaque accent color. Unfilled portion: semi-transparent surface.
|
Filled portion: fully opaque accent color. Unfilled portion: semi-transparent surface.
|
||||||
At 0%: all semi-transparent surface. At 100%: all opaque accent.
|
At 0%: all semi-transparent surface. At 100%: all opaque accent.
|
||||||
Override --dt-progress-empty-opacity to control unfilled portion transparency. */
|
Override --dt-progress-empty-opacity to control unfilled portion transparency. */
|
||||||
@@ -70,7 +73,7 @@
|
|||||||
calc(var(--bevel-sm) - 3px) calc(100% - 6px),
|
calc(var(--bevel-sm) - 3px) calc(100% - 6px),
|
||||||
3px calc(100% - var(--bevel-sm)));
|
3px calc(100% - var(--bevel-sm)));
|
||||||
|
|
||||||
background: linear-gradient(to top,
|
background: linear-gradient(to bottom,
|
||||||
rgba(var(--dt-card-color-rgb, var(--color-primary-rgb)), 1) calc(var(--dt-card-progress, 0) * 1%),
|
rgba(var(--dt-card-color-rgb, var(--color-primary-rgb)), 1) calc(var(--dt-card-progress, 0) * 1%),
|
||||||
rgba(var(--color-surface-rgb),
|
rgba(var(--color-surface-rgb),
|
||||||
var(--dt-progress-empty-opacity, 0.6)) calc(var(--dt-card-progress, 0) * 1%));
|
var(--dt-progress-empty-opacity, 0.6)) calc(var(--dt-card-progress, 0) * 1%));
|
||||||
@@ -85,6 +88,26 @@
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Card Grow — stretch to fill available space in flex/grid containers.
|
||||||
|
By default the body (content area) grows. Use .dt-card-grow-title to
|
||||||
|
grow the header instead. */
|
||||||
|
.dt-card-grow {
|
||||||
|
flex: 1 1 0%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dt-card-grow > .card-title {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dt-card-grow:not(.dt-card-grow-title) > :last-child {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dt-card-grow.dt-card-grow-title > .card-title {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* Card Header — storefront .card-header pattern
|
/* Card Header — storefront .card-header pattern
|
||||||
Title starts at bevel-sm (past the progress bar zone) and extends to the right edge.
|
Title starts at bevel-sm (past the progress bar zone) and extends to the right edge.
|
||||||
Below it, the ::before dark surface shows through for the body area. */
|
Below it, the ::before dark surface shows through for the body area. */
|
||||||
@@ -102,7 +125,6 @@
|
|||||||
background: var(--dt-card-color, var(--color-primary));
|
background: var(--dt-card-color, var(--color-primary));
|
||||||
color: var(--color-bg);
|
color: var(--color-bg);
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -125,11 +147,10 @@
|
|||||||
background: var(--dt-card-color, var(--color-primary));
|
background: var(--dt-card-color, var(--color-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bottom-right bevel on card body content (img/video). Clip-path matches
|
/* Bottom-right bevel on card body content. Clip-path matches
|
||||||
the card's inner surface bevel so the 3px structural border is visible
|
the card's inner surface bevel so the 3px structural border is visible
|
||||||
at the diagonal corner. Badge is not clipped (it's a sibling, not img). */
|
at the diagonal corner. Badge overlays are excluded. */
|
||||||
[data-brand="dt"] .card-body-flush > img,
|
[data-brand="dt"] .card-body-flush > *:not(.dt-card-badge) {
|
||||||
[data-brand="dt"] .card-body-flush > video {
|
|
||||||
display: block;
|
display: block;
|
||||||
clip-path: polygon(
|
clip-path: polygon(
|
||||||
0% 0%,
|
0% 0%,
|
||||||
@@ -272,6 +293,37 @@
|
|||||||
color: var(--color-bg);
|
color: var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mode-variant badges — bridge .mode-* vars to badge color vars */
|
||||||
|
[data-brand="dt"] .badge.mode-normal {
|
||||||
|
--badge-border-color: var(--mode-normal);
|
||||||
|
--badge-fill: var(--mode-normal);
|
||||||
|
color: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-brand="dt"] .badge.mode-emphasis {
|
||||||
|
--badge-border-color: var(--mode-emphasis);
|
||||||
|
--badge-fill: var(--mode-emphasis);
|
||||||
|
color: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-brand="dt"] .badge.mode-warning {
|
||||||
|
--badge-border-color: var(--mode-warning);
|
||||||
|
--badge-fill: var(--mode-warning);
|
||||||
|
color: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-brand="dt"] .badge.mode-success {
|
||||||
|
--badge-border-color: var(--mode-success);
|
||||||
|
--badge-fill: var(--mode-success);
|
||||||
|
color: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-brand="dt"] .badge.mode-other {
|
||||||
|
--badge-border-color: var(--mode-other);
|
||||||
|
--badge-fill: var(--mode-other);
|
||||||
|
color: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
Opt-in Bevel Utility Classes
|
Opt-in Bevel Utility Classes
|
||||||
Use these for custom elements that need bevel shapes
|
Use these for custom elements that need bevel shapes
|
||||||
@@ -352,7 +404,6 @@
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
text-transform: uppercase;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +420,6 @@
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
text-transform: uppercase;
|
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,7 +558,6 @@
|
|||||||
padding: 2px calc(8px + 0.2rem) 2px 8px;
|
padding: 2px calc(8px + 0.2rem) 2px 8px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
background-color: var(--dt-card-color, var(--color-primary));
|
background-color: var(--dt-card-color, var(--color-primary));
|
||||||
color: var(--color-bg);
|
color: var(--color-bg);
|
||||||
@@ -542,7 +591,6 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
text-transform: uppercase;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color var(--transition-fast),
|
transition: background-color var(--transition-fast),
|
||||||
color var(--transition-fast),
|
color var(--transition-fast),
|
||||||
|
|||||||
Reference in New Issue
Block a user