Files
dt-design-system/packages/web/src/components/bevels.css
michael 34be1d0b9e 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>
2026-03-09 20:04:15 -07:00

644 lines
21 KiB
CSS

/* dt-web-theme: Bevel Utilities */
/* Angular/beveled clip-path patterns from the DT design system */
/* Source: dt-shopify-storefront clip-path patterns + react-native-dt-theme bevelPaths.ts */
/* */
/* clip-path clips CSS borders and box-shadow. This file uses the storefront's */
/* dual-element technique: the element's own background serves as the "border" color, */
/* and a ::before pseudo-element provides the inner surface fill. The gap between */
/* them follows the clip-path shape perfectly. */
/* */
/* For glow effects on clipped elements, use filter: drop-shadow() (not box-shadow). */
/* ============================================================================
Card Bevels — dual bottom bevels (bottom-right large + bottom-left small)
Source: storefront .card-container + .clip-corners-inner pattern
Outer bg = border color, ::before = surface fill
Border width: ~3px uniform (storefront --card-border-width: 0.2em)
============================================================================ */
[data-brand="dt"] .dt-bevel-card,
[data-brand="dt"] .card {
--dt-card-progress: 0;
--_card-pad: var(--space-6);
display: flex;
flex-direction: column;
position: relative;
isolation: isolate;
background: var(--dt-card-color, var(--color-primary));
border: none;
border-radius: 0;
padding: var(--_card-pad);
padding-left: calc(var(--bevel-sm) + var(--_card-pad));
overflow: clip;
clip-path: polygon(0% 0%,
100% 0%,
100% calc(100% - var(--bevel-md)),
calc(100% - var(--bevel-md)) 100%,
var(--bevel-sm) 100%,
0% calc(100% - var(--bevel-sm)));
}
[data-brand="dt"] .dt-bevel-card::before,
[data-brand="dt"] .card::before {
content: '';
position: absolute;
inset: 0;
background: var(--color-surface);
z-index: -1;
/* Inner clip-path: left edge at bevel-sm to create progress bar zone.
The progress bar occupies x=0 to x=bevel-sm with its own borders.
Inner surface starts at bevel-sm, so the left frame is wider than 3px. */
clip-path: polygon(var(--bevel-sm) 3px,
calc(100% - 3px) 3px,
calc(100% - 3px) calc(100% - var(--bevel-md)),
calc(100% - var(--bevel-md)) calc(100% - 3px),
var(--bevel-sm) calc(100% - 3px));
}
/* Card Progress Bar — structural fill area on the left edge
Sits between the 3px frame borders, follows bottom-left bevel diagonal.
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.
At 0%: all semi-transparent surface. At 100%: all opaque accent.
Override --dt-progress-empty-opacity to control unfilled portion transparency. */
[data-brand="dt"] .dt-bevel-card::after,
[data-brand="dt"] .card::after {
content: '';
position: absolute;
inset: 0;
z-index: 1;
clip-path: polygon(3px 3px,
calc(var(--bevel-sm) - 3px) 3px,
calc(var(--bevel-sm) - 3px) calc(100% - 6px),
3px calc(100% - var(--bevel-sm)));
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(--color-surface-rgb),
var(--dt-progress-empty-opacity, 0.6)) calc(var(--dt-card-progress, 0) * 1%));
pointer-events: none;
}
/* Card children — ensure body content renders above the progress bar (::after z-index:1) */
[data-brand="dt"] .dt-bevel-card > *:not(.dt-card-badge),
[data-brand="dt"] .card > *:not(.dt-card-badge) {
position: relative;
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
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. */
[data-brand="dt"] .card>.card-title,
[data-brand="dt"] .dt-bevel-card>.card-title {
/* Pull title toward card edges to sit flush inside the 3px frame border.
Uses 2px inset (1px overlap into the frame border) on top/right/left
to prevent sub-pixel anti-aliasing gaps. The overlap is invisible
because the header and frame border share the same accent color.
Left edge lands at bevel-sm - 1px, keeping the progress bar column
visible alongside the header (the progress bar spans full card height). */
margin: calc(2px - var(--_card-pad)) calc(2px - var(--_card-pad)) var(--space-4) calc(-1px - var(--_card-pad));
padding: 1rem calc(var(--_card-pad) + 1px) 1rem calc(var(--_card-pad) + 1px);
/* Sits above ::before so its own bg covers the dark surface */
background: var(--dt-card-color, var(--color-primary));
color: var(--color-bg);
font-weight: 900;
letter-spacing: 0.05em;
font-size: 1rem;
position: relative;
z-index: 2;
}
/* Force icon spans to inherit header color (overrides inline styles) */
[data-brand="dt"] .card>.card-title>* {
color: inherit !important;
}
/* Full-bleed card body — pulls content flush against the inner frame border.
Use for images/media that should fill the card body edge-to-edge.
Margins align with the card's inner surface edges so the card's structural
3px border is the only visible border (no double-border). */
[data-brand="dt"] .card>.card-body-flush,
[data-brand="dt"] .dt-bevel-card>.card-body-flush {
margin: 0 calc(3px - var(--_card-pad)) calc(3px - var(--_card-pad)) calc(-1 * var(--_card-pad));
overflow: hidden;
background: var(--dt-card-color, var(--color-primary));
}
/* Bottom-right bevel on card body content. Clip-path matches
the card's inner surface bevel so the 3px structural border is visible
at the diagonal corner. Badge overlays are excluded. */
[data-brand="dt"] .card-body-flush > *:not(.dt-card-badge) {
display: block;
clip-path: polygon(
0% 0%,
100% 0%,
100% calc(100% - var(--bevel-md) + 3px),
calc(100% - var(--bevel-md) + 3px) 100%,
0% 100%);
}
/* Remove gap between card title and full-bleed body */
[data-brand="dt"] .card>.card-title + .card-body-flush,
[data-brand="dt"] .dt-bevel-card>.card-title + .card-body-flush {
margin-top: calc(-1 * var(--space-4));
}
/* ============================================================================
Button Bevels — top-right corner cut
Source: storefront .label-clipped pattern (solid fill + clip-path)
Filled buttons (primary/danger): direct clip-path, solid bg
Outline button (secondary): dual-element technique via ::before
============================================================================ */
[data-brand="dt"] .btn-primary,
[data-brand="dt"] .btn-danger {
border: none;
clip-path: polygon(0% 0%,
calc(100% - var(--bevel-sm)) 0%,
100% var(--bevel-sm),
100% 100%,
0% 100%);
}
[data-brand="dt"] .btn-secondary {
position: relative;
isolation: isolate;
background: var(--color-primary);
border: none;
clip-path: polygon(0% 0%,
calc(100% - var(--bevel-sm)) 0%,
100% var(--bevel-sm),
100% 100%,
0% 100%);
}
[data-brand="dt"] .btn-secondary::before {
content: '';
position: absolute;
inset: 0;
background: var(--color-bg);
z-index: -1;
clip-path: polygon(2px 2px,
calc(100% - var(--bevel-sm)) 2px,
calc(100% - 2px) var(--bevel-sm),
calc(100% - 2px) calc(100% - 2px),
2px calc(100% - 2px));
}
[data-brand="dt"] .btn-secondary:hover {
background: var(--color-accent);
}
[data-brand="dt"] .btn-secondary:hover::before {
background: var(--color-surface-hover);
}
/* ============================================================================
Badge/Label Bevels — top-right bevel
Source: storefront .label-clipped pattern
Dual-element technique: badge bg = border color, ::before = inner fill
============================================================================ */
[data-brand="dt"] .badge {
--badge-border-color: var(--color-border);
--badge-fill: var(--color-surface);
position: relative;
isolation: isolate;
background: var(--badge-border-color);
color: var(--color-text-primary);
clip-path: polygon(0% 0%,
calc(100% - var(--bevel-sm)) 0%,
100% var(--bevel-sm),
100% 100%,
0% 100%);
border: none;
border-radius: 0;
}
[data-brand="dt"] .badge::before {
content: '';
position: absolute;
inset: 0;
background: var(--badge-fill);
z-index: -1;
clip-path: polygon(2px 2px,
calc(100% - var(--bevel-sm)) 2px,
calc(100% - 2px) var(--bevel-sm),
calc(100% - 2px) calc(100% - 2px),
2px calc(100% - 2px));
}
/* Colored badge variants — solid color fill with contrasting text.
Dark mode: black text on bright neon fill.
Light mode: white text on bright fill (via --color-bg which flips per theme). */
[data-brand="dt"] .badge-success {
--badge-border-color: var(--color-success);
--badge-fill: var(--color-success);
color: var(--color-bg);
}
[data-brand="dt"] .badge-error {
--badge-border-color: var(--color-error);
--badge-fill: var(--color-error);
color: var(--color-bg);
}
[data-brand="dt"] .badge-warning {
--badge-border-color: var(--color-warning);
--badge-fill: var(--color-warning);
color: var(--color-bg);
}
[data-brand="dt"] .badge-info {
--badge-border-color: var(--color-info);
--badge-fill: var(--color-info);
color: var(--color-bg);
}
/* Classic brand colored badges — same solid fill approach */
[data-brand="classic"] .badge-success {
color: var(--color-bg);
}
[data-brand="classic"] .badge-error {
color: var(--color-bg);
}
[data-brand="classic"] .badge-warning {
color: var(--color-bg);
}
[data-brand="classic"] .badge-info {
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
Use these for custom elements that need bevel shapes
============================================================================ */
[data-brand="dt"] .dt-bevel-btn {
clip-path: polygon(0% 0%,
calc(100% - var(--bevel-sm)) 0%,
100% var(--bevel-sm),
100% 100%,
0% 100%);
}
[data-brand="dt"] .dt-bevel-label {
clip-path: polygon(0% 0%,
calc(100% - var(--bevel-sm)) 0%,
100% var(--bevel-sm),
100% 100%,
0% 100%);
border-radius: 0;
}
/* ============================================================================
Media Frame Bevels — diagonal opposite corners (top-left + bottom-right)
Source: storefront .media-frame-clipped pattern
============================================================================ */
.dt-bevel-media {
position: relative;
isolation: isolate;
background: var(--dt-card-color, var(--color-primary));
clip-path: polygon(var(--bevel-md) 0%,
100% 0%,
100% calc(100% - var(--bevel-md)),
calc(100% - var(--bevel-md)) 100%,
0% 100%,
0% var(--bevel-md));
}
/* Inner surface fill — shows as background when no image is loaded */
.dt-bevel-media::before {
content: '';
position: absolute;
inset: 0;
background: var(--color-surface);
z-index: -1;
clip-path: polygon(
calc(var(--bevel-md) + 1px) 3px,
calc(100% - 3px) 3px,
calc(100% - 3px) calc(100% - var(--bevel-md) - 1px),
calc(100% - var(--bevel-md) - 1px) calc(100% - 3px),
3px calc(100% - 3px),
3px calc(var(--bevel-md) + 1px));
}
.dt-bevel-media > img,
.dt-bevel-media > video {
display: block;
clip-path: polygon(
calc(var(--bevel-md) + 1px) 3px,
calc(100% - 3px) 3px,
calc(100% - 3px) calc(100% - var(--bevel-md) - 1px),
calc(100% - var(--bevel-md) - 1px) calc(100% - 3px),
3px calc(100% - 3px),
3px calc(var(--bevel-md) + 1px));
}
/* Media frame placeholder — shows when no image/video is present.
Surface-colored background is already provided by ::before.
This adds centered "MISSING MEDIA" text via ::after. */
.dt-bevel-media::after {
content: 'MISSING MEDIA';
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: -1;
color: var(--color-text-muted, #888);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
pointer-events: none;
}
/* Card body placeholder — shows when image container is empty or image fails.
Uses surface background and centered placeholder text. */
.dt-badge-parent:empty::after,
.card-body-flush:empty::after {
content: 'MISSING MEDIA';
display: flex;
align-items: center;
justify-content: center;
min-height: 120px;
color: var(--color-text-muted, #888);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
background: var(--color-surface);
}
/* ============================================================================
Modal/Drawer Bevels — dual bottom bevels (bottom-right + bottom-left)
============================================================================ */
.dt-bevel-modal {
clip-path: polygon(0% 0%,
100% 0%,
100% calc(100% - var(--bevel-lg)),
calc(100% - var(--bevel-lg)) 100%,
var(--bevel-lg) 100%,
0% calc(100% - var(--bevel-lg)));
}
/* ============================================================================
Drawer Bevels — exposed-edge bevels
Source: storefront aside clip-path pattern
============================================================================ */
.dt-bevel-drawer-right {
clip-path: polygon(0% var(--bevel-lg),
var(--bevel-lg) 0%,
100% 0%,
100% 100%,
var(--bevel-lg) 100%,
0% calc(100% - var(--bevel-lg)));
}
/* Inner surface clip — inset from outer to create colored border frame */
.dt-bevel-drawer-right-inner {
clip-path: polygon(3px calc(var(--bevel-lg) + 1px),
calc(var(--bevel-lg) + 1px) 3px,
100% 3px,
100% calc(100% - 3px),
calc(var(--bevel-lg) + 1px) calc(100% - 3px),
3px calc(100% - var(--bevel-lg) - 1px));
}
.dt-bevel-drawer-left {
clip-path: polygon(0% 0%,
calc(100% - var(--bevel-lg)) 0%,
100% var(--bevel-lg),
100% calc(100% - var(--bevel-lg)),
calc(100% - var(--bevel-lg)) 100%,
0% 100%);
}
/* Inner surface clip — inset from outer to create colored border frame */
.dt-bevel-drawer-left-inner {
clip-path: polygon(0% 3px,
calc(100% - var(--bevel-lg) - 1px) 3px,
calc(100% - 3px) calc(var(--bevel-lg) + 1px),
calc(100% - 3px) calc(100% - var(--bevel-lg) - 1px),
calc(100% - var(--bevel-lg) - 1px) calc(100% - 3px),
0% calc(100% - 3px));
}
/* ============================================================================
Small Bevels — for compact elements (arrows, stepper buttons)
============================================================================ */
.dt-bevel-sm {
clip-path: polygon(var(--bevel-sm) 0%,
100% 0%,
100% calc(100% - var(--bevel-sm)),
calc(100% - var(--bevel-sm)) 100%,
0% 100%,
0% var(--bevel-sm));
}
/* ============================================================================
Accordion/Menu — thick top border accent (storefront menu-item-clipped)
============================================================================ */
[data-brand="dt"] .dt-accent-top,
[data-brand="dt"] .terminal {
border-top: 3px solid var(--color-primary);
}
/* ============================================================================
Card Color Modes — per-instance color overrides
Sets --dt-card-color, --dt-card-color-selected, --dt-card-color-rgb,
and --accent-mode for use by all card sub-components.
Source: dt-shopify-storefront .mode-* classes
============================================================================ */
[data-brand="dt"] .mode-normal {
--dt-card-color: var(--mode-normal);
--dt-card-color-rgb: var(--mode-normal-rgb);
--dt-card-color-selected: var(--mode-normal-selected);
--accent-mode: var(--mode-emphasis);
}
[data-brand="dt"] .mode-emphasis {
--dt-card-color: var(--mode-emphasis);
--dt-card-color-rgb: var(--mode-emphasis-rgb);
--dt-card-color-selected: var(--mode-emphasis-selected);
--accent-mode: var(--mode-normal);
}
[data-brand="dt"] .mode-warning {
--dt-card-color: var(--mode-warning);
--dt-card-color-rgb: var(--mode-warning-rgb);
--dt-card-color-selected: var(--mode-warning-selected);
--accent-mode: var(--mode-emphasis);
}
[data-brand="dt"] .mode-success {
--dt-card-color: var(--mode-success);
--dt-card-color-rgb: var(--mode-success-rgb);
--dt-card-color-selected: var(--mode-success-selected);
--accent-mode: var(--mode-emphasis);
}
[data-brand="dt"] .mode-other {
--dt-card-color: var(--mode-other);
--dt-card-color-rgb: var(--mode-other-rgb);
--dt-card-color-selected: var(--mode-other-selected);
--accent-mode: var(--mode-normal);
}
/* ============================================================================
Card Badge — bottom-right product type chip (LAB, BUNDLE, etc.)
Source: dt-shopify-storefront CyberProductCard LabChip / BundleChip
Plain flat rectangle. Inherits card color via --dt-card-color.
============================================================================ */
/* Badge parent — provides position context for absolutely-positioned badges.
Use on the image container inside a card, or on any element that wraps content + badge. */
.dt-badge-parent {
position: relative;
}
.dt-card-badge {
position: absolute;
bottom: 8px;
right: 8px;
z-index: 4;
padding: 2px calc(8px + 0.2rem) 2px 8px;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
background-color: var(--dt-card-color, var(--color-primary));
color: var(--color-bg);
}
/* Inside card body-flush, offset badge position to compensate for the
3px margin from the card edge. This makes the badge position relative
to the card's bevel corner match the media frame's placement exactly
(8px from card edge in both cases). */
.card-body-flush > .dt-card-badge {
bottom: 5px;
right: 5px;
}
/* ============================================================================
Interactive Bevel Button — storefront .menu-item-clipped pattern
Default: outlined rectangle, no clip-path, 5px top border
Hover: bottom-right bevel appears, fills with mode color, pulse animation
Selected: bevel persists, fills with 70% opacity mode color
============================================================================ */
[data-brand="dt"] .dt-btn {
color: var(--accent-mode, var(--color-primary));
background-color: var(--color-bg);
border: 1px solid var(--dt-card-color, var(--color-primary));
border-top: 5px solid var(--dt-card-color, var(--color-primary));
text-decoration: none;
padding: 0.5rem 0.75rem;
position: relative;
display: inline-block;
overflow: visible;
font-weight: 600;
font-size: 0.875rem;
letter-spacing: 0.05em;
cursor: pointer;
transition: background-color var(--transition-fast),
color var(--transition-fast),
clip-path var(--transition-fast);
}
[data-brand="dt"] .dt-btn:hover {
clip-path: polygon(0% 0%,
100% 0%,
100% calc(100% - var(--menu-item-corner)),
calc(100% - var(--menu-item-corner)) 100%,
0% 100%);
color: var(--color-bg);
background-color: var(--dt-card-color, var(--color-primary));
animation: dt-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
[data-brand="dt"] .dt-btn.selected {
clip-path: polygon(0% 0%,
100% 0%,
100% calc(100% - var(--menu-item-corner)),
calc(100% - var(--menu-item-corner)) 100%,
0% 100%);
background-color: var(--dt-card-color-selected, rgba(var(--color-primary-rgb), 0.7));
color: var(--color-bg);
animation: none;
}
[data-brand="dt"] .dt-btn.selected:hover {
animation: dt-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Classic brand: no clip-path, uses border-radius instead */
[data-brand="classic"] .dt-btn {
border-radius: var(--radius);
border-top-width: 2px;
}
[data-brand="classic"] .dt-btn:hover {
clip-path: none;
background-color: var(--dt-card-color, var(--color-primary));
color: var(--color-bg);
border-radius: var(--radius);
}
[data-brand="classic"] .dt-btn.selected {
clip-path: none;
background-color: var(--dt-card-color-selected, rgba(var(--color-primary-rgb), 0.7));
color: var(--color-bg);
border-radius: var(--radius);
}