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:
@@ -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
|
||||||
|
|||||||
@@ -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: {},
|
||||||
|
|||||||
@@ -243,6 +243,5 @@ const styles = StyleSheet.create({
|
|||||||
label: {
|
label: {
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
textTransform: 'uppercase',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
alt?: string;
|
interface IntrinsicElements {
|
||||||
thumbnail?: string;
|
'model-viewer': React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLElement>,
|
||||||
|
HTMLElement
|
||||||
|
> & {
|
||||||
|
src?: string;
|
||||||
|
alt?: 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
|
||||||
<img
|
className="dt-bevel-media"
|
||||||
src={current.src}
|
style={{ overflow: 'hidden', aspectRatio, position: 'relative' }}
|
||||||
alt={current.alt || ''}
|
>
|
||||||
style={{
|
{currentType === 'image' && (
|
||||||
width: '100%',
|
<img
|
||||||
height: 'auto',
|
src={current.src}
|
||||||
display: 'block',
|
alt={current.alt || ''}
|
||||||
transition: 'opacity 300ms ease-in-out',
|
style={{
|
||||||
}}
|
...fillStyle,
|
||||||
/>
|
objectFit: 'contain',
|
||||||
|
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,31 +171,59 @@ export function DTGallery({
|
|||||||
gap: '8px',
|
gap: '8px',
|
||||||
marginTop: '8px',
|
marginTop: '8px',
|
||||||
overflowX: 'auto',
|
overflowX: 'auto',
|
||||||
}}>
|
}}
|
||||||
{items.map((item, i) => (
|
>
|
||||||
<button
|
{items.map((item, i) => {
|
||||||
key={item.key}
|
const itemType = item.type || 'image';
|
||||||
onClick={() => setActiveIndex(i)}
|
const thumbSrc = item.thumbnail || ('poster' in item ? item.poster : undefined) || item.src;
|
||||||
type="button"
|
const isMedia = itemType === 'video' || itemType === 'external-video';
|
||||||
style={{
|
|
||||||
flexShrink: 0,
|
return (
|
||||||
width: '64px',
|
<button
|
||||||
height: '64px',
|
key={item.key}
|
||||||
padding: 0,
|
onClick={() => setActiveIndex(i)}
|
||||||
border: i === activeIndex
|
type="button"
|
||||||
? '2px solid var(--dt-card-color, var(--color-primary))'
|
style={{
|
||||||
: '2px solid transparent',
|
flexShrink: 0,
|
||||||
background: 'none',
|
width: '64px',
|
||||||
cursor: 'pointer',
|
height: '64px',
|
||||||
overflow: 'hidden',
|
padding: 0,
|
||||||
}}>
|
border:
|
||||||
<img
|
i === activeIndex
|
||||||
src={item.thumbnail || item.src}
|
? '2px solid var(--dt-card-color, var(--color-primary))'
|
||||||
alt={item.alt || ''}
|
: '2px solid transparent',
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
background: 'none',
|
||||||
/>
|
cursor: 'pointer',
|
||||||
</button>
|
overflow: 'hidden',
|
||||||
))}
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={thumbSrc}
|
||||||
|
alt={item.alt || ''}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}}>
|
||||||
|
✕
|
||||||
|
</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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user