feat: video letterboxing, card badge styling, component updates

- Add object-fit: contain + black background for video in DTMediaFrame
- Style .dt-card-badge with mode-colored background, padding, and
  bevel-aware positioning (negative right offset for clean diagonal clip)
- Restore DTStaggerContainer in react-native (was incorrectly deleted)
- Update DTGallery, DTModal, DTMobileFilterOverlay, DTMediaFrame components
- Refresh showcase pages for desktop and mobile

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-24 11:18:49 -07:00
parent d1e2bf1964
commit 195c47602d
24 changed files with 351 additions and 185 deletions

View File

@@ -17,7 +17,7 @@ Shared design tokens, web CSS, and React Native components for the Dangerous Thi
Two brand themes ship out of the box: Two brand themes ship out of the box:
- **DT** — neon cyberpunk aesthetic with beveled corners and glow effects (Tektur font) - **DT** — neon cyberpunk aesthetic with beveled corners and baked in animations (Tektur font)
- **Classic** — dark navy palette with magenta accents and standard border radius - **Classic** — dark navy palette with magenta accents and standard border radius
## Getting Started ## Getting Started

View File

@@ -251,7 +251,6 @@ const styles = StyleSheet.create({
title: { title: {
fontWeight: '600', fontWeight: '600',
letterSpacing: 0.5, letterSpacing: 0.5,
textTransform: 'uppercase',
fontSize: 14, fontSize: 14,
}, },
contentWrapper: {}, contentWrapper: {},

View File

@@ -243,6 +243,5 @@ const styles = StyleSheet.create({
label: { label: {
fontWeight: '600', fontWeight: '600',
letterSpacing: 1, letterSpacing: 1,
textTransform: 'uppercase',
}, },
}); });

View File

@@ -49,6 +49,11 @@ interface DTCardProps {
* @default 0 * @default 0
*/ */
progress?: number; progress?: number;
/**
* Which area expands when the card grows to fill space
* @default 'body'
*/
growArea?: 'body' | 'title';
/** /**
* Additional styles for the card container * Additional styles for the card container
*/ */
@@ -81,6 +86,10 @@ interface DTCardProps {
* Background color (defaults to theme background) * Background color (defaults to theme background)
*/ */
backgroundColor?: string; backgroundColor?: string;
/**
* Element rendered at the right edge of the header
*/
headerRight?: ReactNode;
/** /**
* Press handler * Press handler
*/ */
@@ -215,6 +224,7 @@ export function DTCard({
title, title,
showHeader, showHeader,
progress = 0, progress = 0,
growArea = 'body',
style, style,
contentStyle, contentStyle,
padding = 16, padding = 16,
@@ -222,6 +232,7 @@ export function DTCard({
bevelSize = 32, bevelSize = 32,
bevelSizeSmall = 16, bevelSizeSmall = 16,
backgroundColor, backgroundColor,
headerRight,
onPress, onPress,
}: DTCardProps) { }: DTCardProps) {
const theme = useDTTheme(); const theme = useDTTheme();
@@ -327,7 +338,7 @@ export function DTCard({
)} )}
<View style={[styles.innerContainer, useBevels && {paddingLeft: bevelSizeSmall - borderWidth}]}> <View style={[styles.innerContainer, useBevels && {paddingLeft: bevelSizeSmall - borderWidth}]}>
{shouldShowHeader && ( {shouldShowHeader && (
<View style={[styles.header, {backgroundColor: accentColor}]}> <View style={[styles.header, {backgroundColor: accentColor}, growArea === 'title' && {flex: 1}, !!(title && headerRight) && styles.headerRow]}>
{title && ( {title && (
<Text <Text
variant="titleMedium" variant="titleMedium"
@@ -335,9 +346,10 @@ export function DTCard({
{title} {title}
</Text> </Text>
)} )}
{headerRight}
</View> </View>
)} )}
<View style={[styles.content, {padding}, contentStyle]}>{children}</View> <View style={[styles.content, {padding}, growArea === 'body' && {flex: 1}, contentStyle]}>{children}</View>
</View> </View>
{/* Frame overlay (above content) — beveled mode only. {/* Frame overlay (above content) — beveled mode only.
Uses evenodd with 3 sub-paths: outer + inner + progressArea. Uses evenodd with 3 sub-paths: outer + inner + progressArea.
@@ -400,12 +412,18 @@ const styles = StyleSheet.create({
position: 'relative', position: 'relative',
zIndex: 1, zIndex: 1,
overflow: 'hidden', overflow: 'hidden',
flex: 1,
}, },
header: { header: {
paddingLeft: 8, paddingLeft: 8,
paddingRight: 16, paddingRight: 16,
paddingVertical: 12, paddingVertical: 12,
}, },
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
headerText: { headerText: {
fontWeight: '700', fontWeight: '700',
letterSpacing: 0.5, letterSpacing: 0.5,

View File

@@ -109,7 +109,6 @@ const styles = StyleSheet.create({
headerText: { headerText: {
fontWeight: '700', fontWeight: '700',
letterSpacing: 0.5, letterSpacing: 0.5,
textTransform: 'uppercase',
}, },
grid: { grid: {
flexDirection: 'row', flexDirection: 'row',

View File

@@ -79,6 +79,11 @@ interface DTGalleryProps {
* @default 3 * @default 3
*/ */
borderWidth?: number; borderWidth?: number;
/**
* Aspect ratio (width / height) for the display area
* @default 1
*/
aspectRatio?: number;
/** /**
* Additional styles * Additional styles
*/ */
@@ -108,6 +113,7 @@ export function DTGallery({
thumbnailSize = 64, thumbnailSize = 64,
bevelSize = 24, bevelSize = 24,
borderWidth = 3, borderWidth = 3,
aspectRatio = 1,
style, style,
}: DTGalleryProps) { }: DTGalleryProps) {
const theme = useDTTheme(); const theme = useDTTheme();
@@ -276,6 +282,7 @@ export function DTGallery({
<View <View
style={[ style={[
styles.mainImageContainer, styles.mainImageContainer,
{ aspectRatio },
!useBevels && { !useBevels && {
borderWidth, borderWidth,
borderColor: accentColor, borderColor: accentColor,
@@ -355,7 +362,6 @@ const styles = StyleSheet.create({
gap: 12, gap: 12,
}, },
mainImageContainer: { mainImageContainer: {
aspectRatio: 1,
position: 'relative', position: 'relative',
}, },
mainImageContent: { mainImageContent: {

View File

@@ -341,7 +341,6 @@ const styles = StyleSheet.create({
menuItemTitle: { menuItemTitle: {
fontWeight: '600', fontWeight: '600',
letterSpacing: 0.5, letterSpacing: 0.5,
textTransform: 'uppercase',
fontSize: 14, fontSize: 14,
}, },
chevron: { chevron: {

View File

@@ -9,8 +9,8 @@
* - Content in beveled card container * - Content in beveled card container
*/ */
import {StyleSheet, ViewStyle, StyleProp, KeyboardAvoidingView, Platform} from 'react-native'; import {StyleSheet, ViewStyle, StyleProp, KeyboardAvoidingView, Platform, Pressable} from 'react-native';
import {Portal, Modal} from 'react-native-paper'; import {Portal, Modal, Text} from 'react-native-paper';
import {useDTTheme} from '../theme/DTThemeProvider'; import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant} from '../utils/variantColors'; import {type DTVariant} from '../utils/variantColors';
import {DTCard} from './DTCard'; import {DTCard} from './DTCard';
@@ -97,6 +97,17 @@ export function DTModal({
mode={variant} mode={variant}
title={title} title={title}
showHeader={!!title} showHeader={!!title}
headerRight={dismissable ? (
<Pressable
onPress={onDismiss}
hitSlop={8}
accessibilityLabel="Close"
accessibilityRole="button">
<Text style={{ color: theme.colors.onPrimary, fontSize: 18, lineHeight: 18 }}>
{'\u2715'}
</Text>
</Pressable>
) : undefined}
contentStyle={contentStyle}> contentStyle={contentStyle}>
{children} {children}
</DTCard> </DTCard>

View File

@@ -160,7 +160,6 @@ export function DTDrawer({
fontWeight: 900, fontWeight: 900,
fontSize: '2em', fontSize: '2em',
letterSpacing: '0.05em', letterSpacing: '0.05em',
textTransform: 'uppercase',
borderBottom: '1px solid var(--color-bg)', borderBottom: '1px solid var(--color-bg)',
}}> }}>
<span>{heading}</span> <span>{heading}</span>

View File

@@ -1,56 +1,168 @@
/** /**
* DTGallery — Image gallery with beveled media frame and thumbnails. * DTGallery — Media gallery with beveled frame and thumbnails.
* Supports images, 3D models (via <model-viewer>), videos, and external videos.
* *
* CSS reference: bevels.css .dt-bevel-media * CSS reference: bevels.css .dt-bevel-media
*/ */
import { useState, type CSSProperties } from 'react'; import { useState, useEffect, type CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens'; import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx'; import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses'; import { getVariantClass } from '../utils/variantClasses';
export interface DTGalleryItem { /* ── TypeScript declaration for <model-viewer> web component ── */
key: string; declare global {
src: string; namespace JSX {
interface IntrinsicElements {
'model-viewer': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
> & {
src?: string;
alt?: string; alt?: string;
thumbnail?: string; poster?: string;
'camera-controls'?: boolean | string;
'auto-rotate'?: boolean | string;
'interaction-prompt'?: string;
ar?: boolean | string;
loading?: 'auto' | 'lazy' | 'eager';
};
}
}
} }
/* ── Gallery item types ── */
export type DTGalleryItem =
| { type?: 'image'; key: string; src: string; alt?: string; thumbnail?: string }
| { type: 'model3d'; key: string; src: string; alt?: string; thumbnail?: string; poster?: string }
| { type: 'video'; key: string; src: string; alt?: string; thumbnail?: string; poster?: string }
| { type: 'external-video'; key: string; src: string; alt?: string; thumbnail?: string };
interface DTGalleryProps { interface DTGalleryProps {
items: DTGalleryItem[]; items: DTGalleryItem[];
/** Color variant @default 'normal' */ /** Color variant @default 'normal' */
variant?: DTVariant; variant?: DTVariant;
/** Aspect ratio (width / height) for the display area @default 1 */
aspectRatio?: number;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
} }
/* ── Model-viewer script loader ── */
const MODEL_VIEWER_SRC =
'https://unpkg.com/@google/model-viewer@v1.12.1/dist/model-viewer.min.js';
let modelViewerLoaded = false;
function useModelViewerScript(needed: boolean) {
useEffect(() => {
if (!needed || modelViewerLoaded || typeof document === 'undefined') return;
if (customElements.get('model-viewer')) {
modelViewerLoaded = true;
return;
}
const script = document.createElement('script');
script.type = 'module';
script.src = MODEL_VIEWER_SRC;
document.head.appendChild(script);
modelViewerLoaded = true;
}, [needed]);
}
/* ── Shared styles ── */
const fillStyle: CSSProperties = {
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
display: 'block',
};
/* ── Component ── */
export function DTGallery({ export function DTGallery({
items, items,
variant = 'normal', variant = 'normal',
aspectRatio = 1,
className, className,
style, style,
}: DTGalleryProps) { }: DTGalleryProps) {
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);
const hasModel = items.some((i) => i.type === 'model3d');
useModelViewerScript(hasModel);
if (items.length === 0) return null; if (items.length === 0) return null;
const current = items[activeIndex]; const current = items[activeIndex];
const currentType = current.type || 'image';
return ( return (
<div className={cx(getVariantClass(variant), className)} style={style}> <div className={cx(getVariantClass(variant), className)} style={style}>
{/* Main image */} {/* Main display */}
<div className="dt-bevel-media" style={{ overflow: 'hidden' }}> <div
className="dt-bevel-media"
style={{ overflow: 'hidden', aspectRatio, position: 'relative' }}
>
{currentType === 'image' && (
<img <img
src={current.src} src={current.src}
alt={current.alt || ''} alt={current.alt || ''}
style={{ style={{
width: '100%', ...fillStyle,
height: 'auto', objectFit: 'contain',
display: 'block',
transition: 'opacity 300ms ease-in-out', transition: 'opacity 300ms ease-in-out',
}} }}
/> />
)}
{currentType === 'model3d' && (
<model-viewer
src={current.src}
alt={current.alt || ''}
poster={'poster' in current ? current.poster : undefined}
camera-controls=""
auto-rotate=""
interaction-prompt="auto"
loading="lazy"
style={{
...fillStyle,
backgroundColor: 'var(--color-bg, #000)',
}}
/>
)}
{currentType === 'video' && (
<video
src={current.src}
poster={'poster' in current ? current.poster : undefined}
controls
playsInline
preload="metadata"
style={{
...fillStyle,
objectFit: 'contain',
}}
/>
)}
{currentType === 'external-video' && (
<iframe
src={current.src}
title={current.alt || 'Video'}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
style={{
...fillStyle,
border: 'none',
}}
/>
)}
</div> </div>
{/* Thumbnails */} {/* Thumbnails */}
{items.length > 1 && ( {items.length > 1 && (
<div <div
@@ -59,8 +171,14 @@ export function DTGallery({
gap: '8px', gap: '8px',
marginTop: '8px', marginTop: '8px',
overflowX: 'auto', overflowX: 'auto',
}}> }}
{items.map((item, i) => ( >
{items.map((item, i) => {
const itemType = item.type || 'image';
const thumbSrc = item.thumbnail || ('poster' in item ? item.poster : undefined) || item.src;
const isMedia = itemType === 'video' || itemType === 'external-video';
return (
<button <button
key={item.key} key={item.key}
onClick={() => setActiveIndex(i)} onClick={() => setActiveIndex(i)}
@@ -70,20 +188,42 @@ export function DTGallery({
width: '64px', width: '64px',
height: '64px', height: '64px',
padding: 0, padding: 0,
border: i === activeIndex border:
i === activeIndex
? '2px solid var(--dt-card-color, var(--color-primary))' ? '2px solid var(--dt-card-color, var(--color-primary))'
: '2px solid transparent', : '2px solid transparent',
background: 'none', background: 'none',
cursor: 'pointer', cursor: 'pointer',
overflow: 'hidden', overflow: 'hidden',
}}> position: 'relative',
}}
>
<img <img
src={item.thumbnail || item.src} src={thumbSrc}
alt={item.alt || ''} alt={item.alt || ''}
style={{ width: '100%', height: '100%', objectFit: 'cover' }} style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/> />
{/* Play icon overlay for video thumbnails */}
{isMedia && (
<svg
viewBox="0 0 24 24"
fill="white"
style={{
position: 'absolute',
inset: 0,
margin: 'auto',
width: '24px',
height: '24px',
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.6))',
pointerEvents: 'none',
}}
>
<path d="M8 5v14l11-7z" />
</svg>
)}
</button> </button>
))} );
})}
</div> </div>
)} )}
</div> </div>

View File

@@ -2,25 +2,43 @@
* DTMediaFrame — Diagonal beveled frame (top-left + bottom-right). * DTMediaFrame — Diagonal beveled frame (top-left + bottom-right).
* *
* CSS reference: bevels.css .dt-bevel-media * CSS reference: bevels.css .dt-bevel-media
*
* The outer div provides the colored bevel border via clip-path.
* A ::before pseudo-element fills the inner surface.
* Direct img/video children are auto-clipped to match.
* A ::after pseudo-element shows "MISSING MEDIA" when empty.
*/ */
import type { ReactNode, CSSProperties } from 'react'; import type { ReactNode, CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx'; import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses';
interface DTMediaFrameProps { interface DTMediaFrameProps {
children: ReactNode; children: ReactNode;
/** Color variant for the bevel border @default 'normal' */
variant?: DTVariant;
/** Custom placeholder shown when children is nullish (overrides CSS ::after) */
placeholder?: ReactNode;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
} }
export function DTMediaFrame({ export function DTMediaFrame({
children, children,
variant = 'normal',
placeholder,
className, className,
style, style,
}: DTMediaFrameProps) { }: DTMediaFrameProps) {
const isEmpty = children == null || children === false;
return ( return (
<div className={cx('dt-bevel-media', className)} style={style}> <div
{children} className={cx('dt-bevel-media', getVariantClass(variant), className)}
style={style}
>
{isEmpty && placeholder ? placeholder : children}
</div> </div>
); );
} }

View File

@@ -5,7 +5,7 @@
* .dt-filter-overlay-content * .dt-filter-overlay-content
*/ */
import { useEffect, useCallback, type ReactNode, type CSSProperties } from 'react'; import { useEffect, useCallback, useRef, type ReactNode, type CSSProperties } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import type { DTVariant } from '@dangerousthings/tokens'; import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx'; import { cx } from '../utils/cx';
@@ -38,6 +38,9 @@ export function DTMobileFilterOverlay({
className, className,
style, style,
}: DTMobileFilterOverlayProps) { }: DTMobileFilterOverlayProps) {
const panelRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLElement | null>(null);
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
if (e.key === 'Escape') onDismiss(); if (e.key === 'Escape') onDismiss();
@@ -45,6 +48,7 @@ export function DTMobileFilterOverlay({
[onDismiss], [onDismiss],
); );
// Escape key
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
document.addEventListener('keydown', handleKeyDown); document.addEventListener('keydown', handleKeyDown);
@@ -52,12 +56,57 @@ export function DTMobileFilterOverlay({
} }
}, [visible, handleKeyDown]); }, [visible, handleKeyDown]);
// Body scroll lock
useEffect(() => {
if (visible) {
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}
}, [visible]);
// Focus trap
useEffect(() => {
if (!visible) return;
triggerRef.current = document.activeElement as HTMLElement;
const timer = setTimeout(() => {
const first = panelRef.current?.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
first?.focus();
}, 100);
const handleTab = (e: KeyboardEvent) => {
if (e.key !== 'Tab' || !panelRef.current) return;
const focusable = panelRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
};
document.addEventListener('keydown', handleTab);
return () => {
clearTimeout(timer);
document.removeEventListener('keydown', handleTab);
triggerRef.current?.focus();
};
}, [visible]);
if (!visible) return null; if (!visible) return null;
return createPortal( return createPortal(
<div className={cx('dt-filter-overlay', getVariantClass(variant), className)} style={style}> <div className={cx('dt-filter-overlay', getVariantClass(variant), className)} style={style}>
<div className="dt-filter-overlay-backdrop" onClick={onDismiss} /> <div className="dt-filter-overlay-backdrop" onClick={onDismiss} />
<div className="dt-filter-overlay-content"> <div ref={panelRef} className="dt-filter-overlay-content">
{/* Header */} {/* Header */}
<div <div
style={{ style={{
@@ -67,7 +116,7 @@ export function DTMobileFilterOverlay({
padding: '16px', padding: '16px',
borderBottom: '1px solid rgba(var(--color-primary-rgb), 0.2)', borderBottom: '1px solid rgba(var(--color-primary-rgb), 0.2)',
}}> }}>
<span style={{ fontWeight: 700, fontSize: '1.125rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}> <span style={{ fontWeight: 700, fontSize: '1.125rem', letterSpacing: '0.05em' }}>
{heading} {heading}
{activeFilterCount !== undefined && activeFilterCount > 0 && ( {activeFilterCount !== undefined && activeFilterCount > 0 && (
<span <span
@@ -88,7 +137,6 @@ export function DTMobileFilterOverlay({
color: 'var(--color-primary)', color: 'var(--color-primary)',
fontSize: '0.875rem', fontSize: '0.875rem',
cursor: 'pointer', cursor: 'pointer',
textTransform: 'uppercase',
}}> }}>
Clear All Clear All
</button> </button>

View File

@@ -82,13 +82,35 @@ export function DTModal({
position: 'relative', position: 'relative',
maxWidth: '90vw', maxWidth: '90vw',
maxHeight: '85vh', maxHeight: '85vh',
overflow: 'auto', display: 'flex',
flexDirection: 'column',
...style, ...style,
}} }}
role="dialog" role="dialog"
aria-modal="true"> aria-modal="true">
{title && <div className="card-title">{title}</div>} {title && (
<div style={{ padding: 'var(--space-6, 24px)' }}>{children}</div> <div className="card-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexShrink: 0 }}>
<span>{title}</span>
{dismissable && (
<button
type="button"
onClick={onDismiss}
aria-label="Close"
style={{
background: 'none',
border: 'none',
color: 'inherit',
cursor: 'pointer',
padding: 0,
fontSize: '1.25rem',
lineHeight: 1,
}}>
&#x2715;
</button>
)}
</div>
)}
<div style={{ padding: 'var(--space-6, 24px)', overflow: 'auto', flex: '1 1 auto' }}>{children}</div>
</div> </div>
</div>, </div>,
document.body, document.body,

View File

@@ -36,7 +36,6 @@ export function DTTextInput({
marginBottom: '4px', marginBottom: '4px',
fontSize: '0.875rem', fontSize: '0.875rem',
fontWeight: 600, fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.05em', letterSpacing: '0.05em',
}}> }}>
{label} {label}

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { DTCard, DTStaggerContainer } from '@dangerousthings/react'; import { DTCard } from '@dangerousthings/react';
import { Section, Row, CodeLabel } from '../components/Section'; import { Section, Row, CodeLabel } from '../components/Section';
function AnimBox({ className, label }: { className: string; label: string }) { function AnimBox({ className, label }: { className: string; label: string }) {
@@ -66,7 +66,7 @@ function ProgressBarDemo() {
<DTCard <DTCard
key={v} key={v}
variant={v} variant={v}
title={`${v.toUpperCase()} ${progress[i]}%`} title={`${v} ${progress[i]}%`}
progress={progress[i]} progress={progress[i]}
style={{ width: 160, transition: 'all 0.1s ease-out' }} style={{ width: 160, transition: 'all 0.1s ease-out' }}
> >
@@ -90,13 +90,12 @@ function ProgressBarDemo() {
export function AnimationsPage() { export function AnimationsPage() {
const [entranceKey, setEntranceKey] = useState(0); const [entranceKey, setEntranceKey] = useState(0);
const [staggerKey, setStaggerKey] = useState(0);
const [accordionExpanded, setAccordionExpanded] = useState(false); const [accordionExpanded, setAccordionExpanded] = useState(false);
return ( return (
<> <>
<h1 className="page-title">Animations</h1> <h1 className="page-title">Animations</h1>
<p className="page-subtitle">Entrance animations, interactive effects, stagger containers, and transition utilities.</p> <p className="page-subtitle">Entrance animations, interactive effects, and transition utilities.</p>
<Section title="Entrance Animations" description="One-shot animations for elements entering the viewport."> <Section title="Entrance Animations" description="One-shot animations for elements entering the viewport.">
<Row key={entranceKey}> <Row key={entranceKey}>
@@ -119,28 +118,6 @@ export function AnimationsPage() {
<CodeLabel text=".dt-animate-pulse | .dt-animate-ping | .dt-animate-spin" /> <CodeLabel text=".dt-animate-pulse | .dt-animate-ping | .dt-animate-spin" />
</Section> </Section>
<Section title="Stagger Container" description="Automatic staggered scale-in animation for child elements. Customizable via --dt-stagger-duration and --dt-stagger-interval.">
<DTStaggerContainer
key={staggerKey}
className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-dt-3"
>
{Array.from({ length: 12 }, (_, i) => (
<DTCard key={i} title={`CARD ${i + 1}`} style={{ textAlign: 'center' }}>
<div className="card-body" style={{ fontSize: '0.75rem' }}>Item #{i + 1}</div>
</DTCard>
))}
</DTStaggerContainer>
<button
className="btn-secondary"
style={{ marginTop: 'var(--space-4)' }}
onClick={() => setStaggerKey(k => k + 1)}
type="button"
>
REPLAY STAGGER
</button>
<CodeLabel text="<DTStaggerContainer> — nth-child delays up to 24 children" />
</Section>
<Section title="Transition Utilities" description="Accordion expand, chevron rotation, and progress bar transitions."> <Section title="Transition Utilities" description="Accordion expand, chevron rotation, and progress bar transitions.">
<div style={{ maxWidth: 400 }}> <div style={{ maxWidth: 400 }}>
<button <button
@@ -156,7 +133,6 @@ export function AnimationsPage() {
cursor: 'pointer', cursor: 'pointer',
color: 'var(--color-text)', color: 'var(--color-text)',
fontWeight: 600, fontWeight: 600,
textTransform: 'uppercase',
}} }}
aria-expanded={accordionExpanded} aria-expanded={accordionExpanded}
onClick={() => setAccordionExpanded(!accordionExpanded)} onClick={() => setAccordionExpanded(!accordionExpanded)}

View File

@@ -53,12 +53,12 @@ export function BevelsPage() {
<Row> <Row>
{modes.map(mode => ( {modes.map(mode => (
<button key={mode} className={`dt-menu-item mode-${mode}`} type="button" onClick={() => setModalVariant(mode)}> <button key={mode} className={`dt-menu-item mode-${mode}`} type="button" onClick={() => setModalVariant(mode)}>
{mode.toUpperCase()} MODAL {mode} Modal
</button> </button>
))} ))}
</Row> </Row>
<CodeLabel text="<DTModal variant='normal' visible onDismiss title='...'>content</DTModal>" /> <CodeLabel text="<DTModal variant='normal' visible onDismiss title='...'>content</DTModal>" />
<DTModal visible={modalVariant !== null} onDismiss={() => setModalVariant(null)} variant={modalVariant ?? 'normal'} title={`${(modalVariant ?? 'normal').toUpperCase()} MODAL`}> <DTModal visible={modalVariant !== null} onDismiss={() => setModalVariant(null)} variant={modalVariant ?? 'normal'} title={`${modalVariant ?? 'normal'} Modal`}>
<p style={{ marginBottom: 'var(--space-4)' }}>This is a <strong>{modalVariant}</strong> modal with beveled card shape, backdrop blur, and scale-in animation.</p> <p style={{ marginBottom: 'var(--space-4)' }}>This is a <strong>{modalVariant}</strong> modal with beveled card shape, backdrop blur, and scale-in animation.</p>
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.85rem' }}>Click the backdrop or press Escape to dismiss.</p> <p style={{ color: 'var(--color-text-muted)', fontSize: '0.85rem' }}>Click the backdrop or press Escape to dismiss.</p>
</DTModal> </DTModal>
@@ -74,7 +74,7 @@ export function BevelsPage() {
</button> </button>
</Row> </Row>
<CodeLabel text="<DTDrawer position='right' heading='...' visible onDismiss>content</DTDrawer>" /> <CodeLabel text="<DTDrawer position='right' heading='...' visible onDismiss>content</DTDrawer>" />
<DTDrawer visible={drawerSide !== null} onDismiss={() => setDrawerSide(null)} position={drawerSide ?? 'right'} heading={`${(drawerSide ?? 'right').toUpperCase()} DRAWER`} headingVariant={drawerSide === 'left' ? 'other' : 'emphasis'}> <DTDrawer visible={drawerSide !== null} onDismiss={() => setDrawerSide(null)} position={drawerSide ?? 'right'} heading={`${drawerSide ?? 'right'} Drawer`} headingVariant={drawerSide === 'left' ? 'other' : 'emphasis'}>
<p style={{ marginBottom: 'var(--space-4)' }}>Sliding panel from the <strong>{drawerSide}</strong> edge with beveled corners and backdrop blur.</p> <p style={{ marginBottom: 'var(--space-4)' }}>Sliding panel from the <strong>{drawerSide}</strong> edge with beveled corners and backdrop blur.</p>
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.85rem' }}>Click the backdrop, press Escape, or click to dismiss.</p> <p style={{ color: 'var(--color-text-muted)', fontSize: '0.85rem' }}>Click the backdrop, press Escape, or click to dismiss.</p>
</DTDrawer> </DTDrawer>
@@ -91,7 +91,7 @@ export function BevelsPage() {
<Section title="Accent Top" description="Used on accordion headers and menu items."> <Section title="Accent Top" description="Used on accordion headers and menu items.">
<div className="dt-accent-top" style={{ padding: 'var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}> <div className="dt-accent-top" style={{ padding: 'var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}>
<span style={{ fontWeight: 600, textTransform: 'uppercase' }}>Thick Top Border Accent</span> <span style={{ fontWeight: 600 }}>Thick Top Border Accent</span>
</div> </div>
<CodeLabel text=".dt-accent-top" /> <CodeLabel text=".dt-accent-top" />
</Section> </Section>
@@ -99,7 +99,7 @@ export function BevelsPage() {
<Section title="Card Color Modes" description="Per-card mode coloring — see Advanced Cards page for full demos."> <Section title="Card Color Modes" description="Per-card mode coloring — see Advanced Cards page for full demos.">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3">
{modes.map(mode => ( {modes.map(mode => (
<DTCard key={mode} variant={mode} title={mode.toUpperCase()}> <DTCard key={mode} variant={mode} title={mode}>
<div className="card-body" style={{ fontSize: '0.75rem' }}>mode-{mode}</div> <div className="card-body" style={{ fontSize: '0.75rem' }}>mode-{mode}</div>
</DTCard> </DTCard>
))} ))}

View File

@@ -58,7 +58,7 @@ export function HexBackgroundPage({ hexProps, onHexPropsChange }: HexBackgroundP
{sliders.map(({ key, label, min, max, step }) => ( {sliders.map(({ key, label, min, max, step }) => (
<div key={key} style={{ display: 'grid', gridTemplateColumns: '1fr auto', alignItems: 'center', gap: 'var(--space-4)' }}> <div key={key} style={{ display: 'grid', gridTemplateColumns: '1fr auto', alignItems: 'center', gap: 'var(--space-4)' }}>
<label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}> <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<span style={{ fontSize: '0.8rem', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: '0.1em' }}> <span style={{ fontSize: '0.8rem', color: 'var(--color-text-muted)', letterSpacing: '0.1em' }}>
{label} {label}
</span> </span>
<input <input

View File

@@ -1,11 +1,11 @@
import { DTCard, DTStaggerContainer } from '@dangerousthings/react'; import { DTCard } from '@dangerousthings/react';
import type { DTVariant } from '@dangerousthings/tokens'; import type { DTVariant } from '@dangerousthings/tokens';
import { Section, CodeLabel } from '../components/Section'; import { Section, CodeLabel } from '../components/Section';
const categories: { hash: string; title: string; desc: string; mode: DTVariant; count: number }[] = [ const categories: { hash: string; title: string; desc: string; mode: DTVariant; count: number }[] = [
{ hash: 'bevels', title: 'Bevels', desc: 'Angular clip-path patterns — cards, buttons, badges, media frames, modals, drawers', mode: 'normal', count: 8 }, { hash: 'bevels', title: 'Bevels', desc: 'Angular clip-path patterns — cards, buttons, badges, media frames, modals, drawers', mode: 'normal', count: 8 },
{ hash: 'forms', title: 'Forms', desc: 'Text inputs, checkboxes, switches, radios, progress bars, accordions, steppers', mode: 'success', count: 7 }, { hash: 'forms', title: 'Forms', desc: 'Text inputs, checkboxes, switches, radios, progress bars, accordions, steppers', mode: 'success', count: 7 },
{ hash: 'animations', title: 'Animations', desc: 'Entrance animations, stagger containers, transition utilities, scrollbar styling', mode: 'other', count: 5 }, { hash: 'animations', title: 'Animations', desc: 'Entrance animations, transition utilities, scrollbar styling', mode: 'other', count: 5 },
{ hash: 'cards-advanced', title: 'Advanced Cards', desc: 'Card color modes, progress bars, badge overlays, interactive bevel buttons, feature legend', mode: 'warning', count: 6 }, { hash: 'cards-advanced', title: 'Advanced Cards', desc: 'Card color modes, progress bars, badge overlays, interactive bevel buttons, feature legend', mode: 'warning', count: 6 },
{ hash: 'tokens', title: 'Tokens', desc: 'Color palette, typography, spacing, and shape values for the active brand', mode: 'normal', count: 3 }, { hash: 'tokens', title: 'Tokens', desc: 'Color palette, typography, spacing, and shape values for the active brand', mode: 'normal', count: 3 },
]; ];
@@ -31,7 +31,6 @@ export function HomePage() {
fontWeight: 900, fontWeight: 900,
letterSpacing: '0.15em', letterSpacing: '0.15em',
marginBottom: '0.25rem', marginBottom: '0.25rem',
textTransform: 'uppercase',
}}> }}>
DANGEROUS THINGS DANGEROUS THINGS
</h1> </h1>
@@ -40,7 +39,6 @@ export function HomePage() {
fontSize: '1.25rem', fontSize: '1.25rem',
fontWeight: 700, fontWeight: 700,
letterSpacing: '0.2em', letterSpacing: '0.2em',
textTransform: 'uppercase',
marginTop: 0, marginTop: 0,
}}> }}>
DESIGN SYSTEM DESIGN SYSTEM
@@ -71,16 +69,15 @@ export function HomePage() {
color: 'var(--color-text-muted)', color: 'var(--color-text-muted)',
fontSize: '0.7rem', fontSize: '0.7rem',
letterSpacing: '0.15em', letterSpacing: '0.15em',
textTransform: 'uppercase',
marginBottom: 'var(--space-4)', marginBottom: 'var(--space-4)',
}}> }}>
COMPONENT CATALOG COMPONENT CATALOG
</p> </p>
<DTStaggerContainer className="grid grid-cols-1 md:grid-cols-2 gap-dt-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-dt-4">
{categories.map(cat => ( {categories.map(cat => (
<a key={cat.hash} href={`#/${cat.hash}`} style={{ textDecoration: 'none', color: 'inherit' }}> <a key={cat.hash} href={`#/${cat.hash}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<DTCard variant={cat.mode} title={cat.title.toUpperCase()}> <DTCard variant={cat.mode} title={cat.title}>
<div className="card-body">{cat.desc}</div> <div className="card-body">{cat.desc}</div>
{cat.count > 0 && ( {cat.count > 0 && (
<div style={{ color: 'var(--color-text-muted)', fontSize: '0.7rem', marginTop: 'var(--space-2)' }}> <div style={{ color: 'var(--color-text-muted)', fontSize: '0.7rem', marginTop: 'var(--space-2)' }}>
@@ -90,7 +87,7 @@ export function HomePage() {
</DTCard> </DTCard>
</a> </a>
))} ))}
</DTStaggerContainer> </div>
<Section title="Quick Start" description="Import the React components and CSS to get started."> <Section title="Quick Start" description="Import the React components and CSS to get started.">
<div className="terminal dt-accent-top"> <div className="terminal dt-accent-top">

View File

@@ -43,7 +43,6 @@ body {
font-size: 0.875rem; font-size: 0.875rem;
letter-spacing: 0.1em; letter-spacing: 0.1em;
color: var(--color-primary); color: var(--color-primary);
text-transform: uppercase;
padding-bottom: var(--space-4); padding-bottom: var(--space-4);
border-bottom: 2px solid var(--color-primary); border-bottom: 2px solid var(--color-primary);
} }
@@ -65,7 +64,6 @@ body {
font-size: 0.75rem; font-size: 0.75rem;
letter-spacing: 0.05em; letter-spacing: 0.05em;
cursor: pointer; cursor: pointer;
text-transform: uppercase;
transition: all var(--transition-fast); transition: all var(--transition-fast);
} }
@@ -93,7 +91,6 @@ body {
font-size: 0.625rem; font-size: 0.625rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
text-transform: uppercase;
transition: all var(--transition-fast); transition: all var(--transition-fast);
} }
@@ -117,7 +114,6 @@ body {
font-weight: 600; font-weight: 600;
font-size: 0.875rem; font-size: 0.875rem;
letter-spacing: 0.02em; letter-spacing: 0.02em;
text-transform: uppercase;
border-left: 3px solid transparent; border-left: 3px solid transparent;
transition: all var(--transition-fast); transition: all var(--transition-fast);
} }
@@ -151,7 +147,6 @@ body {
font-weight: 700; font-weight: 700;
font-size: 1.125rem; font-size: 1.125rem;
color: var(--color-primary); color: var(--color-primary);
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
padding-bottom: var(--space-2); padding-bottom: var(--space-2);
@@ -175,7 +170,6 @@ body {
.demo-label { .demo-label {
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.6875rem; font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
margin-top: var(--space-4); margin-top: var(--space-4);
@@ -200,7 +194,6 @@ body {
padding: var(--space-3) var(--space-6); padding: var(--space-3) var(--space-6);
font-weight: 700; font-weight: 700;
font-size: 0.875rem; font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast); transition: all var(--transition-fast);
@@ -244,7 +237,6 @@ body {
.card-title { .card-title {
font-weight: 900; font-weight: 900;
font-size: 1rem; font-size: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
@@ -260,7 +252,6 @@ body {
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
font-weight: 600; font-weight: 600;
font-size: 0.75rem; font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
@@ -300,7 +291,6 @@ input:focus {
font-weight: 900; font-weight: 900;
font-size: 1.75rem; font-size: 1.75rem;
color: var(--color-primary); color: var(--color-primary);
text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
} }

View File

@@ -1,15 +1,12 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native';
import { Text } from 'react-native-paper'; import { Text } from 'react-native-paper';
import { import {
DTCard, DTCard,
DTLabel, DTLabel,
DTStaggerContainer,
useDTTheme, useDTTheme,
useScaleIn, useScaleIn,
usePulse, usePulse,
} from '@dangerousthings/react-native'; } from '@dangerousthings/react-native';
import type { DTVariant } from '@dangerousthings/react-native';
import { ScreenContainer } from '../components/ScreenContainer'; import { ScreenContainer } from '../components/ScreenContainer';
import { DemoSection } from '../components/DemoSection'; import { DemoSection } from '../components/DemoSection';
import { CodeLabel } from '../components/CodeLabel'; import { CodeLabel } from '../components/CodeLabel';
@@ -20,44 +17,8 @@ export function AnimationsScreen() {
const scaleAnim = useScaleIn({ duration: 600 }); const scaleAnim = useScaleIn({ duration: 600 });
const pulseAnim = usePulse(true); const pulseAnim = usePulse(true);
const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other'];
return ( return (
<ScreenContainer> <ScreenContainer>
{/* Stagger Container */}
<DemoSection
title="DTStaggerContainer"
variant="normal"
description="Staggered scale-in entrance animation for child elements."
>
<DTStaggerContainer duration={330} interval={75}>
{modes.map((mode) => (
<DTCard key={mode} mode={mode} title={mode.toUpperCase()} style={{ marginBottom: 12 }}>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
Staggered entrance with scale animation
</Text>
</DTCard>
))}
</DTStaggerContainer>
<CodeLabel text="<DTStaggerContainer duration={330} interval={75}>" />
</DemoSection>
{/* Stagger with Labels */}
<DemoSection
title="Staggered Labels"
variant="emphasis"
description="Labels with staggered entrance animation."
>
<DTStaggerContainer duration={400} interval={100}>
{modes.map((mode) => (
<View key={mode} style={{ marginBottom: 8 }}>
<DTLabel primaryText={mode.toUpperCase()} mode={mode} />
</View>
))}
</DTStaggerContainer>
<CodeLabel text="DTStaggerContainer > DTLabel — customizable timing" />
</DemoSection>
{/* Scale-In Hook */} {/* Scale-In Hook */}
<DemoSection <DemoSection
title="useScaleIn" title="useScaleIn"

View File

@@ -5,7 +5,6 @@ import {
DTCard, DTCard,
DTChip, DTChip,
DTBadgeOverlay, DTBadgeOverlay,
DTStaggerContainer,
useDTTheme, useDTTheme,
} from '@dangerousthings/react-native'; } from '@dangerousthings/react-native';
import type { DTVariant } from '@dangerousthings/react-native'; import type { DTVariant } from '@dangerousthings/react-native';
@@ -83,29 +82,6 @@ export function CardsAdvancedScreen() {
<CodeLabel text="<DTBadgeOverlay position='bottom-right'><DTChip variant='warning'>LAB</DTChip>" /> <CodeLabel text="<DTBadgeOverlay position='bottom-right'><DTChip variant='warning'>LAB</DTChip>" />
</DemoSection> </DemoSection>
{/* Stagger + Progress */}
<DemoSection
title="Staggered Cards with Progress"
variant="success"
description="Stagger container with progress bars across all modes."
>
<DTStaggerContainer>
{modes.map((mode) => (
<DTCard
key={mode}
mode={mode}
title={mode.toUpperCase()}
progress={Math.random()}
style={{ marginBottom: 12 }}
>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
Staggered + progress
</Text>
</DTCard>
))}
</DTStaggerContainer>
<CodeLabel text="DTStaggerContainer > DTCard progress" />
</DemoSection>
</ScreenContainer> </ScreenContainer>
); );
} }

View File

@@ -58,14 +58,14 @@ const categories: {
}, },
{ {
title: 'ADVANCED CARDS', title: 'ADVANCED CARDS',
subtitle: 'Selected state, progress bar, badge overlays, stagger', subtitle: 'Selected state, progress bar, badge overlays',
mode: 'emphasis', mode: 'emphasis',
route: 'CardsAdvanced', route: 'CardsAdvanced',
count: 4, count: 4,
}, },
{ {
title: 'ANIMATIONS', title: 'ANIMATIONS',
subtitle: 'DTStaggerContainer, useScaleIn, usePulse', subtitle: 'useScaleIn, usePulse',
mode: 'success', mode: 'success',
route: 'Animations', route: 'Animations',
count: 3, count: 3,

View File

@@ -585,8 +585,20 @@
.dt-bevel-card > .dt-card-badge { .dt-bevel-card > .dt-card-badge {
position: absolute; position: absolute;
bottom: calc(var(--bevel-md) * 0.25); bottom: calc(var(--bevel-md) * 0.25);
right: calc(var(--bevel-md) * 0.25); right: calc(var(--bevel-md) * -0.15);
z-index: 4; z-index: 4;
background: var(--dt-card-color, var(--color-primary));
color: var(--color-bg);
padding: 0.35em 2em 0.35em 1em;
font-size: 0.85rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
[data-brand="classic"] .card > .dt-card-badge,
[data-brand="classic"] .dt-bevel-card > .dt-card-badge {
border-radius: 0.375rem;
} }
/* ============================================================================ /* ============================================================================

View File

@@ -251,7 +251,6 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
text-transform: uppercase;
font-weight: 600; font-weight: 600;
font-size: 0.875rem; font-size: 0.875rem;
letter-spacing: 0.05em; letter-spacing: 0.05em;
@@ -351,7 +350,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: all var(--transition-fast); transition: all var(--transition-fast);
} }
@@ -420,7 +418,6 @@
border-top: 5px solid var(--color-primary); border-top: 5px solid var(--color-primary);
border-bottom: 1px solid rgba(var(--color-primary-rgb), 0.2); border-bottom: 1px solid rgba(var(--color-primary-rgb), 0.2);
font-weight: 600; font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
cursor: pointer; cursor: pointer;
} }