Compare commits

...

14 Commits

Author SHA1 Message Date
michael
1be0070abf chore: version packages (web 0.4.1, react 2.0.1, react-native 0.4.1)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:26:57 -07:00
michael
c8c92d187d chore: version packages (web 0.4.0, react 2.0.0, react-native 0.4.0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:23:14 -07:00
michael
195c47602d 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>
2026-03-24 11:18:49 -07:00
michael
d1e2bf1964 fix: tune card badge position to 25% of bevel-md for responsive corner clipping 2026-03-13 05:03:46 -07:00
michael
926a282535 refactor: update showcase to use DTCard badge prop 2026-03-13 04:44:24 -07:00
michael
2dcf86c346 feat: add badge prop to DTCard for bevel-clipped badge slot 2026-03-12 19:21:11 -07:00
michael
8a91b21a11 feat: add .dt-card-badge positioning rule for card-level badge slot 2026-03-12 17:22:38 -07:00
michael
766266b799 Apply inner bevel clip-path to model-viewer and iframe elements
Extend the .dt-bevel-media inner clip-path selector to cover
model-viewer and iframe children, matching the existing treatment
for img and video. Without this, 3D viewers and embedded video
iframes filled edge-to-edge and covered the neon bevel border.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 13:26:48 -07:00
michael
36420868f5 Fix card-body-flush child bevel clip-path alignment
The child clip-path used bevel-md - 3px from child corner, but should
use bevel-md to align with the ::before inner surface diagonal. This
caused the bottom-right bevel border to appear too thin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:51:56 -07:00
michael
c8afd90527 Refactor DTStaggerContainer: wrapper-div approach with delay/interval props
Replace CSS nth-child stagger rules with JS-driven wrapper divs that set
animationDelay inline. Adds delay (base offset) and interval (ms between
children) props. Removes --dt-stagger-interval CSS variable and 24
nth-child rules in favor of unlimited children support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:18:12 -07:00
michael
34be1d0b9e DTCard: make progress optional, fill top-to-bottom
Progress bar now fills top-down (was bottom-up). When progress prop is
omitted the bar stays empty and the card renders at full size. Only cards
that explicitly pass progress={0–100} show the animated bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:04:15 -07:00
michael
6816ef4825 Add documentLevel mode to DTWebThemeProvider
When documentLevel is true, sets data-brand and data-theme on
document.documentElement instead of rendering a wrapper div.
This is needed for apps where CSS ancestor selectors must match
from the document root (e.g. [data-brand="dt"] .card).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:39:25 -07:00
michael
b319f15092 Fix feature legend data accuracy from storefront models
- NExT v2: fix access control (Legacy, not DESFire/iCLASS), digital
  security (unsupported), magic (supported via T5577), illumination
  (supported HF LED), data sharing (1 kB), payment (unsupported)
- Replace Titan with m0422a magnet: RP 22,860 G/g sensing, ~20 g
  lifting, Push/Pull haptics, Monopole polarity detection
- xLED HF: correct illumination colors and Low prominence
- All data sourced from dt-shopify-storefront model definitions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:12:03 -07:00
michael
68234e66f4 Add repository field to showcase desktop package.json
electron-builder requires a repository field to detect the
GitHub repo for publishing artifacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 12:41:03 -07:00
37 changed files with 615 additions and 317 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

@@ -1,5 +1,17 @@
# @dangerousthings/react-native # @dangerousthings/react-native
## 0.4.1
### Patch Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer.
## 0.4.0
### Minor Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer in react-native, updated DTGallery/DTModal/DTMobileFilterOverlay components.
## 0.3.0 ## 0.3.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@dangerousthings/react-native", "name": "@dangerousthings/react-native",
"version": "0.3.0", "version": "0.4.1",
"description": "React Native themed components for the Dangerous Things design system", "description": "React Native themed components for the Dangerous Things design system",
"license": "MIT", "license": "MIT",
"author": { "author": {

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

@@ -1,5 +1,24 @@
# @dangerousthings/react # @dangerousthings/react
## 2.0.1
### Patch Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer.
- Updated dependencies
- @dangerousthings/web@0.4.1
## 2.0.0
### Minor Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer in react-native, updated DTGallery/DTModal/DTMobileFilterOverlay components.
### Patch Changes
- Updated dependencies
- @dangerousthings/web@0.4.0
## 1.0.0 ## 1.0.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@dangerousthings/react", "name": "@dangerousthings/react",
"version": "1.0.0", "version": "2.0.1",
"description": "React web components for the Dangerous Things design system", "description": "React web components for the Dangerous Things design system",
"license": "MIT", "license": "MIT",
"author": { "author": {

View File

@@ -6,7 +6,8 @@
* *
* The progress bar is a structural element on the left edge (0 to bevel-sm). * The progress bar is a structural element on the left edge (0 to bevel-sm).
* It is always present. At 0 progress it shows surface color (empty bar). * It is always present. At 0 progress it shows surface color (empty bar).
* As progress increases, accent color fills from the bottom up. * As progress increases, accent color fills from top to bottom.
* When progress is omitted, the bar stays empty (full card, no fill).
*/ */
import type { ReactNode, CSSProperties } from 'react'; import type { ReactNode, CSSProperties } from 'react';
@@ -22,10 +23,18 @@ interface DTCardProps {
title?: string; title?: string;
/** Whether to show the accent header bar @default true when title is provided */ /** Whether to show the accent header bar @default true when title is provided */
showHeader?: boolean; showHeader?: boolean;
/** Progress value (0100) for left-edge vertical progress bar @default 0 */ /** Dismiss callback — renders a close button in the header bar */
onDismiss?: () => void;
/** Progress value (0100) for left-edge vertical progress bar. Omit for empty bar. */
progress?: number; progress?: number;
/** Grow to fill available space in flex/grid containers @default false */
grow?: boolean;
/** Which area expands when grow is true @default 'body' */
growArea?: 'body' | 'title';
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
/** Badge content rendered at bottom-right, clipped by card bevel */
badge?: ReactNode;
onClick?: () => void; onClick?: () => void;
} }
@@ -34,12 +43,16 @@ export function DTCard({
variant = 'normal', variant = 'normal',
title, title,
showHeader, showHeader,
progress = 0, onDismiss,
progress,
grow = false,
growArea = 'body',
className, className,
style, style,
onClick, onClick,
badge,
}: DTCardProps) { }: DTCardProps) {
const shouldShowHeader = showHeader ?? !!title; const shouldShowHeader = showHeader ?? !!(title || onDismiss);
const Tag = onClick ? 'button' : 'div'; const Tag = onClick ? 'button' : 'div';
return ( return (
@@ -47,20 +60,40 @@ export function DTCard({
className={cx( className={cx(
'card', 'card',
getVariantClass(variant), getVariantClass(variant),
grow && 'dt-card-grow',
grow && growArea === 'title' && 'dt-card-grow-title',
className, className,
)} )}
style={{ style={{
...style, ...style,
'--dt-card-progress': progress, ...(progress != null && { '--dt-card-progress': progress }),
} as CSSProperties} } as CSSProperties}
onClick={onClick} onClick={onClick}
type={onClick ? 'button' : undefined}> type={onClick ? 'button' : undefined}>
{shouldShowHeader && ( {shouldShowHeader && (
<div className="card-title"> <div className="card-title" style={onDismiss ? { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } : undefined}>
{title} <span>{title}</span>
{onDismiss && (
<button
type="button"
onClick={(e) => { e.stopPropagation(); onDismiss(); }}
aria-label="Close"
style={{
background: 'none',
border: 'none',
color: 'inherit',
cursor: 'pointer',
padding: 0,
fontSize: '1.25rem',
lineHeight: 1,
}}>
&#x2715;
</button>
)}
</div> </div>
)} )}
{children} {children}
{badge && <div className="dt-card-badge">{badge}</div>}
</Tag> </Tag>
); );
} }

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

@@ -2,18 +2,22 @@
* DTStaggerContainer — Staggered scale-in animation for children. * DTStaggerContainer — Staggered scale-in animation for children.
* *
* CSS reference: animations.css .dt-stagger-container * CSS reference: animations.css .dt-stagger-container
* CSS handles all stagger timing via nth-child — no JS animation needed. * Each child is wrapped in a div that carries the animationDelay.
* Wrapper divs inherit display:contents would break animation, so
* the CSS rule targets .dt-stagger-container > * directly.
*/ */
import type { ReactNode, CSSProperties } from 'react'; import { Children, type ReactNode, type CSSProperties } from 'react';
import { cx } from '../utils/cx'; import { cx } from '../utils/cx';
interface DTStaggerContainerProps { interface DTStaggerContainerProps {
children: ReactNode; children: ReactNode;
/** Duration per child animation @default '0.33s' */ /** Duration per child animation @default '0.33s' */
duration?: string; duration?: string;
/** Delay between each child @default '75ms' */ /** Delay between each child in ms @default 150 */
interval?: string; interval?: number;
/** Base delay before the first child animates in ms @default 0 */
delay?: number;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
} }
@@ -21,19 +25,23 @@ interface DTStaggerContainerProps {
export function DTStaggerContainer({ export function DTStaggerContainer({
children, children,
duration, duration,
interval, interval = 150,
delay = 0,
className, className,
style, style,
}: DTStaggerContainerProps) { }: DTStaggerContainerProps) {
const cssVars: Record<string, string> = {}; const cssVars: Record<string, string> = {};
if (duration) cssVars['--dt-stagger-duration'] = duration; if (duration) cssVars['--dt-stagger-duration'] = duration;
if (interval) cssVars['--dt-stagger-interval'] = interval;
return ( return (
<div <div
className={cx('dt-stagger-container', className)} className={cx('dt-stagger-container', className)}
style={{ ...cssVars, ...style } as CSSProperties}> style={{ ...cssVars, ...style } as CSSProperties}>
{children} {Children.map(children, (child, i) => (
<div style={{ animationDelay: `${delay + i * interval}ms` }}>
{child}
</div>
))}
</div> </div>
); );
} }

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,11 +1,12 @@
/** /**
* DTWebThemeProvider * DTWebThemeProvider
* *
* Sets data-brand and data-theme attributes on a wrapper div. * Sets data-brand and data-theme attributes on a wrapper div (default)
* or on document.documentElement (documentLevel mode).
* Provides React context for child components to read the active brand/theme. * Provides React context for child components to read the active brand/theme.
*/ */
import { createContext, type ReactNode } from 'react'; import { createContext, useEffect, type ReactNode } from 'react';
import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens'; import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens';
import { cx } from '../utils/cx'; import { cx } from '../utils/cx';
@@ -21,7 +22,9 @@ interface DTWebThemeProviderProps {
brand?: ThemeBrand; brand?: ThemeBrand;
/** Theme mode @default 'dark' */ /** Theme mode @default 'dark' */
theme?: ThemeMode; theme?: ThemeMode;
/** Additional className for the wrapper div */ /** When true, sets data-brand/data-theme on document.documentElement instead of a wrapper div */
documentLevel?: boolean;
/** Additional className for the wrapper div (ignored when documentLevel is true) */
className?: string; className?: string;
children: ReactNode; children: ReactNode;
} }
@@ -29,14 +32,26 @@ interface DTWebThemeProviderProps {
export function DTWebThemeProvider({ export function DTWebThemeProvider({
brand = 'dt', brand = 'dt',
theme = 'dark', theme = 'dark',
documentLevel,
className, className,
children, children,
}: DTWebThemeProviderProps) { }: DTWebThemeProviderProps) {
useEffect(() => {
if (documentLevel) {
document.documentElement.dataset.brand = brand;
document.documentElement.dataset.theme = theme;
}
}, [documentLevel, brand, theme]);
return ( return (
<DTWebThemeContext.Provider value={{ brand, theme }}> <DTWebThemeContext.Provider value={{ brand, theme }}>
{documentLevel ? (
children
) : (
<div data-brand={brand} data-theme={theme} className={cx('dt-theme-root', className)}> <div data-brand={brand} data-theme={theme} className={cx('dt-theme-root', className)}>
{children} {children}
</div> </div>
)}
</DTWebThemeContext.Provider> </DTWebThemeContext.Provider>
); );
} }

View File

@@ -44,7 +44,6 @@ export { DTSearchInput } from './components/DTSearchInput';
export { DTProgressBar } from './components/DTProgressBar'; export { DTProgressBar } from './components/DTProgressBar';
export { DTAccordion } from './components/DTAccordion'; export { DTAccordion } from './components/DTAccordion';
export type { DTAccordionSection } from './components/DTAccordion'; export type { DTAccordionSection } from './components/DTAccordion';
export { DTStaggerContainer } from './components/DTStaggerContainer';
export { DTBadgeOverlay } from './components/DTBadgeOverlay'; export { DTBadgeOverlay } from './components/DTBadgeOverlay';
// Components — filter & feature // Components — filter & feature
@@ -58,3 +57,6 @@ export { DTMobileFilterOverlay } from './components/DTMobileFilterOverlay';
export { DTGallery } from './components/DTGallery'; export { DTGallery } from './components/DTGallery';
export type { DTGalleryItem } from './components/DTGallery'; export type { DTGalleryItem } from './components/DTGallery';
export { DTHexagon } from './components/DTHexagon'; export { DTHexagon } from './components/DTHexagon';
// Components — animation
export { DTStaggerContainer } from './components/DTStaggerContainer';

View File

@@ -8,6 +8,11 @@
"name": "Dangerous Things", "name": "Dangerous Things",
"email": "info@dangerousthings.com" "email": "info@dangerousthings.com"
}, },
"repository": {
"type": "git",
"url": "https://github.com/dangerous-tac0s/dt-design-system.git",
"directory": "packages/showcase/desktop"
},
"homepage": "https://github.com/dangerous-tac0s/dt-design-system", "homepage": "https://github.com/dangerous-tac0s/dt-design-system",
"main": "dist/main/main.js", "main": "dist/main/main.js",
"scripts": { "scripts": {

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

@@ -1,7 +1,6 @@
import { createElement, useState, type CSSProperties } from 'react'; import { createElement, type CSSProperties } from 'react';
import { import {
DTCard, DTCard,
DTStaggerContainer,
DTFeatureLegend, DTFeatureLegend,
} from '@dangerousthings/react'; } from '@dangerousthings/react';
import type { DTFeatureItem } from '@dangerousthings/react'; import type { DTFeatureItem } from '@dangerousthings/react';
@@ -33,36 +32,35 @@ const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other']
const ico = (C: any) => createElement(C, { style: { fontSize: '2rem' } }); const ico = (C: any) => createElement(C, { style: { fontSize: '2rem' } });
// Full chip feature legend (9 features — from storefront UseCaseLegend) // Full chip feature legend (9 features — from storefront UseCaseLegend)
// detail text simulates what the storefront shows for an NExT implant // NExT v2: NTAGI2C (HF) + T5577 (LF) with power-harvesting LED
const chipFeatures: DTFeatureItem[] = [ const chipFeatures: DTFeatureItem[] = [
{ key: 'smartphone', name: 'Smartphone', icon: ico(MdOutlinePhonelinkRing), state: 'supported', detail: 'Full NFC smartphone support' }, { key: 'smartphone', name: 'Smartphone', icon: ico(MdOutlinePhonelinkRing), state: 'supported', detail: 'Full NFC smartphone support' },
{ key: 'access_control', name: 'Access Control', icon: ico(MdOutlineVpnKey), state: 'supported', detail: 'DESFire, MIFARE Classic, iCLASS' }, { key: 'access_control', name: 'Access Control', icon: ico(MdOutlineVpnKey), state: 'supported', detail: 'Legacy' },
{ key: 'digital_security', name: 'Digital Security', icon: ico(FaUserShield), state: 'supported', detail: 'FIDO2 / WebAuthn' }, { key: 'digital_security', name: 'Digital Security', icon: ico(FaUserShield), state: 'unsupported', detail: 'Not Supported' },
{ key: 'cryptography', name: 'Cryptography', icon: ico(LuBinary), state: 'unsupported', detail: 'Not Supported' }, { key: 'cryptography', name: 'Cryptography', icon: ico(LuBinary), state: 'unsupported', detail: 'Not Supported' },
{ key: 'data_sharing', name: 'Data Sharing', icon: ico(MdOutlineMobileScreenShare), state: 'supported', detail: 'NDEF records, vCard, URL' }, { key: 'data_sharing', name: 'Data Sharing', icon: ico(MdOutlineMobileScreenShare), state: 'supported', detail: '1 kB' },
{ key: 'payment', name: 'Payment', icon: ico(MdOutlineCreditCard), state: 'disabled', detail: 'Apex required' }, { key: 'payment', name: 'Payment', icon: ico(MdOutlineCreditCard), state: 'unsupported', detail: 'Not Supported' },
{ key: 'magic', name: 'Magic', icon: ico(MdOutlineCopyAll), state: 'supported', detail: 'Gen2 Magic UID' }, { key: 'magic', name: 'Magic', icon: ico(MdOutlineCopyAll), state: 'supported', detail: 'Yes' },
{ key: 'Illumination', name: 'Illumination', icon: ico(MdOutlineLightbulb), state: 'unsupported', detail: 'None' }, { key: 'Illumination', name: 'Illumination', icon: ico(MdOutlineLightbulb), state: 'supported', detail: 'HF: green, blue, white' },
{ key: 'temperature', name: 'Sensors', icon: ico(MdOutlineThermostat), state: 'unsupported', detail: 'None' }, { key: 'temperature', name: 'Sensors', icon: ico(MdOutlineThermostat), state: 'unsupported', detail: 'None' },
]; ];
// Full biomagnet feature legend (4 features — from storefront MagnetUseCaseLegend) // Full biomagnet feature legend (4 features — from storefront MagnetUseCaseLegend)
// m0422a: axial neodymium disc, bioresin coated
const magnetFeatures: DTFeatureItem[] = [ const magnetFeatures: DTFeatureItem[] = [
{ key: 'sensing', name: 'Sensing', icon: ico(MdOutlineSensors), state: 'supported', detail: 'Electromagnetic field detection' }, { key: 'sensing', name: 'Sensing', icon: ico(MdOutlineSensors), state: 'supported', detail: 'RP 22,860 G/g' },
{ key: 'lifting', name: 'Lifting', icon: ico(MdOutlineFitbit), state: 'supported', detail: '2.1 kg lifting force' }, { key: 'lifting', name: 'Lifting', icon: ico(MdOutlineFitbit), state: 'supported', detail: '~20 g' },
{ key: 'haptics', name: 'Haptics', icon: ico(MdOutlineVibration), state: 'supported', detail: 'Tactile vibration feedback' }, { key: 'haptics', name: 'Haptics', icon: ico(MdOutlineVibration), state: 'supported', detail: 'Push/Pull' },
{ key: 'polarity', name: 'Polarity Detection', icon: ico(MdOutlineExplore), state: 'unsupported', detail: 'Not Supported' }, { key: 'polarity', name: 'Polarity Detection', icon: ico(MdOutlineExplore), state: 'supported', detail: 'Monopole' },
]; ];
// Full aesthetic feature legend (2 features — from storefront AestheticUseCaseLegend) // Full aesthetic feature legend (2 features — from storefront AestheticUseCaseLegend)
const aestheticFeatures: DTFeatureItem[] = [ const aestheticFeatures: DTFeatureItem[] = [
{ key: 'illumination', name: 'Illumination', icon: ico(MdLightbulbOutline), state: 'supported', detail: 'LED: Red, Green, Blue, White' }, { key: 'illumination', name: 'Illumination', icon: ico(MdLightbulbOutline), state: 'supported', detail: 'HF: red, green, blue, white' },
{ key: 'prominence', name: 'Prominence', icon: ico(MdOutlineVisibility), state: 'supported', detail: 'High visibility under skin' }, { key: 'prominence', name: 'Prominence', icon: ico(MdOutlineVisibility), state: 'supported', detail: 'Low' },
]; ];
export function CardsAdvancedPage() { export function CardsAdvancedPage() {
const [staggerKey, setStaggerKey] = useState(0);
return ( return (
<> <>
<h1 className="page-title">Advanced Cards</h1> <h1 className="page-title">Advanced Cards</h1>
@@ -71,7 +69,7 @@ export function CardsAdvancedPage() {
<Section title="Card Color Modes" description="Per-card color via variant prop. Sets --dt-card-color, glow color, and accent."> <Section title="Card Color Modes" description="Per-card color via variant prop. Sets --dt-card-color, glow color, and accent.">
<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.8rem' }}>mode-{mode}</div> <div className="card-body" style={{ fontSize: '0.8rem' }}>mode-{mode}</div>
</DTCard> </DTCard>
))} ))}
@@ -80,6 +78,23 @@ export function CardsAdvancedPage() {
</Section> </Section>
<Section title="Card Grow" description="Cards stretch to equal height in a row via grow prop. Compare the varying content heights.">
<div style={{ display: 'flex', gap: 'var(--space-3)' }}>
<DTCard variant="normal" title="SHORT" grow>
<div className="card-body" style={{ fontSize: '0.8rem' }}>One line</div>
</DTCard>
<DTCard variant="emphasis" title="TALL" grow>
<div className="card-body" style={{ fontSize: '0.8rem' }}>
Multiple lines of content to demonstrate that the shorter card stretches to match this card's height.
</div>
</DTCard>
<DTCard variant="other" title="MEDIUM" grow>
<div className="card-body" style={{ fontSize: '0.8rem' }}>Two lines of content here.</div>
</DTCard>
</div>
<CodeLabel text="<DTCard grow /> — flex-grow: 1 + height: 100% to fill container" />
</Section>
<Section title="Card Progress Bar" description="Vertical left-edge bar driven by progress prop (0-100)."> <Section title="Card Progress Bar" description="Vertical left-edge bar driven by progress prop (0-100).">
<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">
{[0, 25, 50, 75, 100].map((val, i) => ( {[0, 25, 50, 75, 100].map((val, i) => (
@@ -98,15 +113,15 @@ export function CardsAdvancedPage() {
{ label: 'BUNDLE', mode: 'other' as DTVariant, img: 'https://images.unsplash.com/photo-1518770660439-4636190af475?w=400&h=400&fit=crop' }, { label: 'BUNDLE', mode: 'other' as DTVariant, img: 'https://images.unsplash.com/photo-1518770660439-4636190af475?w=400&h=400&fit=crop' },
{ label: 'NFC', mode: 'normal' as DTVariant, img: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=400&h=400&fit=crop' }, { label: 'NFC', mode: 'normal' as DTVariant, img: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=400&h=400&fit=crop' },
].map(badge => ( ].map(badge => (
<DTCard key={badge.label} variant={badge.mode} title={badge.label + ' PRODUCT'}> <DTCard key={badge.label} variant={badge.mode} title={badge.label + ' PRODUCT'}
<div className="card-body-flush dt-badge-parent" style={{ aspectRatio: '1' }}> badge={<span>{badge.label}</span>}>
<div className="card-body-flush" style={{ aspectRatio: '1' }}>
<img src={badge.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} /> <img src={badge.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
<span className="dt-card-badge">{badge.label}</span>
</div> </div>
</DTCard> </DTCard>
))} ))}
</div> </div>
<CodeLabel text=".dt-badge-parent > .dt-card-badge — badge positioned on card image container" /> <CodeLabel text="<DTCard badge={<span>LAB</span>}> — badge rendered and positioned by card" />
</Section> </Section>
<Section title="Buttons" description="Beveled buttons with active/selected states and mode colors. Nested levels use inline paddingLeft."> <Section title="Buttons" description="Beveled buttons with active/selected states and mode colors. Nested levels use inline paddingLeft.">
@@ -132,11 +147,11 @@ export function CardsAdvancedPage() {
</Section> </Section>
<Section title="Feature Legends" description="Interactive product feature grids from the storefront. Hover icons for details. Toggle labels with the ? button."> <Section title="Feature Legends" description="Interactive product feature grids from the storefront. Hover icons for details. Toggle labels with the ? button.">
<DTFeatureLegend features={chipFeatures} title="NExT Features" variant="normal" columns={5} /> <DTFeatureLegend features={chipFeatures} title="NExT v2 Features" variant="normal" columns={5} />
<div style={{ height: 'var(--space-6)' }} /> <div style={{ height: 'var(--space-6)' }} />
<DTFeatureLegend features={magnetFeatures} title="Titan Features" variant="emphasis" columns={4} /> <DTFeatureLegend features={magnetFeatures} title="m0422a Features" variant="emphasis" columns={4} />
<div style={{ height: 'var(--space-6)' }} /> <div style={{ height: 'var(--space-6)' }} />
<DTFeatureLegend features={aestheticFeatures} title="xLED Features" variant="other" columns={2} /> <DTFeatureLegend features={aestheticFeatures} title="xLED HF Features" variant="other" columns={2} />
<div style={{ height: 'var(--space-4)' }} /> <div style={{ height: 'var(--space-4)' }} />
<CodeLabel text="<DTFeatureLegend features={chipFeatures} title='NExT Features' variant='normal' />" /> <CodeLabel text="<DTFeatureLegend features={chipFeatures} title='NExT Features' variant='normal' />" />
<CodeLabel text="<DTFeatureLegend features={magnetFeatures} variant='emphasis' columns={4} />" /> <CodeLabel text="<DTFeatureLegend features={magnetFeatures} variant='emphasis' columns={4} />" />
@@ -148,27 +163,6 @@ export function CardsAdvancedPage() {
</div> </div>
</Section> </Section>
<Section title="Stagger + Modes" description="Mode-colored cards in a stagger container.">
<DTStaggerContainer
key={staggerKey}
className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3"
>
{Array.from({ length: 10 }, (_, i) => (
<DTCard key={i} variant={modes[i % modes.length]} title={modes[i % modes.length].toUpperCase()} style={{ textAlign: 'center' }}>
{null}
</DTCard>
))}
</DTStaggerContainer>
<button
className="btn-secondary"
style={{ marginTop: 'var(--space-4)' }}
onClick={() => setStaggerKey(k => k + 1)}
type="button"
>
REPLAY
</button>
<CodeLabel text="<DTStaggerContainer> with <DTCard variant='...' />" />
</Section>
</> </>
); );
} }

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

@@ -1,5 +1,17 @@
# @dangerousthings/web # @dangerousthings/web
## 0.4.1
### Patch Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer.
## 0.4.0
### Minor Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer in react-native, updated DTGallery/DTModal/DTMobileFilterOverlay components.
## 0.3.0 ## 0.3.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@dangerousthings/web", "name": "@dangerousthings/web",
"version": "0.3.0", "version": "0.4.1",
"description": "Web CSS theme for the Dangerous Things design system — bevels, glows, form styles", "description": "Web CSS theme for the Dangerous Things design system — bevels, glows, form styles",
"license": "MIT", "license": "MIT",
"author": { "author": {

View File

@@ -82,49 +82,16 @@
/* ============================================================================ /* ============================================================================
Stagger Container Stagger Container
Source: storefront framer-motion staggered card entrance
Usage: <div class="dt-stagger-container">...</div>
Customize: --dt-stagger-duration, --dt-stagger-interval
============================================================================ */ ============================================================================ */
.dt-stagger-container { .dt-stagger-container {
--dt-stagger-duration: 0.33s; --dt-stagger-duration: 0.33s;
--dt-stagger-interval: 75ms;
} }
.dt-stagger-container > * { .dt-stagger-container > * {
animation: dt-scale-in var(--dt-stagger-duration) ease-in-out both; animation: dt-scale-in var(--dt-stagger-duration) ease-in-out both;
} }
.dt-stagger-container > :nth-child(1) { animation-delay: calc(0 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(2) { animation-delay: calc(1 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(3) { animation-delay: calc(2 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(4) { animation-delay: calc(3 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(5) { animation-delay: calc(4 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(6) { animation-delay: calc(5 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(7) { animation-delay: calc(6 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(8) { animation-delay: calc(7 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(9) { animation-delay: calc(8 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(10) { animation-delay: calc(9 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(11) { animation-delay: calc(10 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(12) { animation-delay: calc(11 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(13) { animation-delay: calc(12 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(14) { animation-delay: calc(13 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(15) { animation-delay: calc(14 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(16) { animation-delay: calc(15 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(17) { animation-delay: calc(16 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(18) { animation-delay: calc(17 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(19) { animation-delay: calc(18 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(20) { animation-delay: calc(19 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(21) { animation-delay: calc(20 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(22) { animation-delay: calc(21 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(23) { animation-delay: calc(22 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(24) { animation-delay: calc(23 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(n+25) {
animation-delay: calc(24 * var(--dt-stagger-interval));
}
/* ============================================================================ /* ============================================================================
Transition Utilities Transition Utilities
============================================================================ */ ============================================================================ */

View File

@@ -19,6 +19,8 @@
[data-brand="dt"] .card { [data-brand="dt"] .card {
--dt-card-progress: 0; --dt-card-progress: 0;
--_card-pad: var(--space-6); --_card-pad: var(--space-6);
display: flex;
flex-direction: column;
position: relative; position: relative;
isolation: isolate; isolation: isolate;
background: var(--dt-card-color, var(--color-primary)); background: var(--dt-card-color, var(--color-primary));
@@ -44,17 +46,21 @@
z-index: -1; z-index: -1;
/* Inner clip-path: left edge at bevel-sm to create progress bar zone. /* Inner clip-path: left edge at bevel-sm to create progress bar zone.
The progress bar occupies x=0 to x=bevel-sm with its own borders. The progress bar occupies x=0 to x=bevel-sm with its own borders.
Inner surface starts at bevel-sm, so the left frame is wider than 3px. */ Inner surface starts at bevel-sm, so the left frame is wider than 3px.
Diagonal vertices use bevel-md + 3px (not bevel-md alone) so the
perpendicular border width on the 45-degree diagonal visually matches
the 3px straight edges despite sub-pixel antialiasing. */
clip-path: polygon(var(--bevel-sm) 3px, clip-path: polygon(var(--bevel-sm) 3px,
calc(100% - 3px) 3px, calc(100% - 3px) 3px,
calc(100% - 3px) calc(100% - var(--bevel-md)), calc(100% - 3px) calc(100% - var(--bevel-md) - 3px),
calc(100% - var(--bevel-md)) calc(100% - 3px), calc(100% - var(--bevel-md) - 3px) calc(100% - 3px),
var(--bevel-sm) calc(100% - 3px)); var(--bevel-sm) calc(100% - 3px));
} }
/* Card Progress Bar — structural fill area on the left edge /* Card Progress Bar — structural fill area on the left edge
Sits between the 3px frame borders, follows bottom-left bevel diagonal. Sits between the 3px frame borders, follows bottom-left bevel diagonal.
Gradient fills from bottom (accent) to top (surface) based on --dt-card-progress. Gradient fills top-to-bottom based on --dt-card-progress so the card
visually "opens" as the bar descends.
Filled portion: fully opaque accent color. Unfilled portion: semi-transparent surface. Filled portion: fully opaque accent color. Unfilled portion: semi-transparent surface.
At 0%: all semi-transparent surface. At 100%: all opaque accent. At 0%: all semi-transparent surface. At 100%: all opaque accent.
Override --dt-progress-empty-opacity to control unfilled portion transparency. */ Override --dt-progress-empty-opacity to control unfilled portion transparency. */
@@ -70,7 +76,7 @@
calc(var(--bevel-sm) - 3px) calc(100% - 6px), calc(var(--bevel-sm) - 3px) calc(100% - 6px),
3px calc(100% - var(--bevel-sm))); 3px calc(100% - var(--bevel-sm)));
background: linear-gradient(to top, background: linear-gradient(to bottom,
rgba(var(--dt-card-color-rgb, var(--color-primary-rgb)), 1) calc(var(--dt-card-progress, 0) * 1%), rgba(var(--dt-card-color-rgb, var(--color-primary-rgb)), 1) calc(var(--dt-card-progress, 0) * 1%),
rgba(var(--color-surface-rgb), rgba(var(--color-surface-rgb),
var(--dt-progress-empty-opacity, 0.6)) calc(var(--dt-card-progress, 0) * 1%)); var(--dt-progress-empty-opacity, 0.6)) calc(var(--dt-card-progress, 0) * 1%));
@@ -79,12 +85,32 @@
} }
/* Card children — ensure body content renders above the progress bar (::after z-index:1) */ /* Card children — ensure body content renders above the progress bar (::after z-index:1) */
[data-brand="dt"] .dt-bevel-card > *:not(.dt-card-badge), [data-brand="dt"] .dt-bevel-card > *:not(.dt-badge-bottom-right):not(.dt-card-badge),
[data-brand="dt"] .card > *:not(.dt-card-badge) { [data-brand="dt"] .card > *:not(.dt-badge-bottom-right):not(.dt-card-badge) {
position: relative; position: relative;
z-index: 2; z-index: 2;
} }
/* Card Grow — stretch to fill available space in flex/grid containers.
By default the body (content area) grows. Use .dt-card-grow-title to
grow the header instead. */
.dt-card-grow {
flex: 1 1 0%;
height: 100%;
}
.dt-card-grow > .card-title {
flex: 0 0 auto;
}
.dt-card-grow:not(.dt-card-grow-title) > :last-child {
flex: 1 1 auto;
}
.dt-card-grow.dt-card-grow-title > .card-title {
flex: 1 1 auto;
}
/* Card Header — storefront .card-header pattern /* Card Header — storefront .card-header pattern
Title starts at bevel-sm (past the progress bar zone) and extends to the right edge. Title starts at bevel-sm (past the progress bar zone) and extends to the right edge.
Below it, the ::before dark surface shows through for the body area. */ Below it, the ::before dark surface shows through for the body area. */
@@ -102,7 +128,6 @@
background: var(--dt-card-color, var(--color-primary)); background: var(--dt-card-color, var(--color-primary));
color: var(--color-bg); color: var(--color-bg);
font-weight: 900; font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
font-size: 1rem; font-size: 1rem;
position: relative; position: relative;
@@ -125,17 +150,16 @@
background: var(--dt-card-color, var(--color-primary)); background: var(--dt-card-color, var(--color-primary));
} }
/* Bottom-right bevel on card body content (img/video). Clip-path matches /* Bottom-right bevel on card body content. Clip-path matches
the card's inner surface bevel so the 3px structural border is visible the card's inner surface bevel so the 3px structural border is visible
at the diagonal corner. Badge is not clipped (it's a sibling, not img). */ at the diagonal corner. Badge overlays are excluded. */
[data-brand="dt"] .card-body-flush > img, [data-brand="dt"] .card-body-flush > *:not(.dt-badge-bottom-right):not(.dt-card-badge) {
[data-brand="dt"] .card-body-flush > video {
display: block; display: block;
clip-path: polygon( clip-path: polygon(
0% 0%, 0% 0%,
100% 0%, 100% 0%,
100% calc(100% - var(--bevel-md) + 3px), 100% calc(100% - var(--bevel-md)),
calc(100% - var(--bevel-md) + 3px) 100%, calc(100% - var(--bevel-md)) 100%,
0% 100%); 0% 100%);
} }
@@ -272,6 +296,37 @@
color: var(--color-bg); color: var(--color-bg);
} }
/* Mode-variant badges — bridge .mode-* vars to badge color vars */
[data-brand="dt"] .badge.mode-normal {
--badge-border-color: var(--mode-normal);
--badge-fill: var(--mode-normal);
color: var(--color-bg);
}
[data-brand="dt"] .badge.mode-emphasis {
--badge-border-color: var(--mode-emphasis);
--badge-fill: var(--mode-emphasis);
color: var(--color-bg);
}
[data-brand="dt"] .badge.mode-warning {
--badge-border-color: var(--mode-warning);
--badge-fill: var(--mode-warning);
color: var(--color-bg);
}
[data-brand="dt"] .badge.mode-success {
--badge-border-color: var(--mode-success);
--badge-fill: var(--mode-success);
color: var(--color-bg);
}
[data-brand="dt"] .badge.mode-other {
--badge-border-color: var(--mode-other);
--badge-fill: var(--mode-other);
color: var(--color-bg);
}
/* ============================================================================ /* ============================================================================
Opt-in Bevel Utility Classes Opt-in Bevel Utility Classes
Use these for custom elements that need bevel shapes Use these for custom elements that need bevel shapes
@@ -326,7 +381,9 @@
} }
.dt-bevel-media > img, .dt-bevel-media > img,
.dt-bevel-media > video { .dt-bevel-media > video,
.dt-bevel-media > model-viewer,
.dt-bevel-media > iframe {
display: block; display: block;
clip-path: polygon( clip-path: polygon(
calc(var(--bevel-md) + 1px) 3px, calc(var(--bevel-md) + 1px) 3px,
@@ -337,6 +394,14 @@
3px calc(var(--bevel-md) + 1px)); 3px calc(var(--bevel-md) + 1px));
} }
/* Video letterboxing — respect natural aspect ratio with black bars */
.dt-bevel-media > video {
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
/* Media frame placeholder — shows when no image/video is present. /* Media frame placeholder — shows when no image/video is present.
Surface-colored background is already provided by ::before. Surface-colored background is already provided by ::before.
This adds centered "MISSING MEDIA" text via ::after. */ This adds centered "MISSING MEDIA" text via ::after. */
@@ -352,7 +417,6 @@
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase;
pointer-events: none; pointer-events: none;
} }
@@ -369,7 +433,6 @@
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase;
background: var(--color-surface); background: var(--color-surface);
} }
@@ -492,15 +555,17 @@
/* ============================================================================ /* ============================================================================
Card Badge — bottom-right product type chip (LAB, BUNDLE, etc.) Card Badge — bottom-right product type chip (LAB, BUNDLE, etc.)
Source: dt-shopify-storefront CyberProductCard LabChip / BundleChip Source: dt-shopify-storefront CyberProductCard LabChip / BundleChip
Plain flat rectangle. Inherits card color via --dt-card-color. Positioned at card level so the card's clip-path bevels the badge corner.
Consumer provides badge content/styling; card handles placement.
============================================================================ */ ============================================================================ */
/* Badge parent — provides position context for absolutely-positioned badges.
Use on the image container inside a card, or on any element that wraps content + badge. */ /* Legacy: standalone badge parent for non-card contexts */
.dt-badge-parent { .dt-badge-parent {
position: relative; position: relative;
} }
.dt-card-badge { /* Legacy: standalone badge positioning for non-card contexts */
.dt-badge-bottom-right {
position: absolute; position: absolute;
bottom: 8px; bottom: 8px;
right: 8px; right: 8px;
@@ -508,19 +573,32 @@
padding: 2px calc(8px + 0.2rem) 2px 8px; padding: 2px calc(8px + 0.2rem) 2px 8px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
background-color: var(--dt-card-color, var(--color-primary)); background-color: var(--dt-card-color, var(--color-primary));
color: var(--color-bg); color: var(--color-bg);
} }
/* Inside card body-flush, offset badge position to compensate for the /* Card-level badge slot — positioned so the card's bevel clip-path
3px margin from the card edge. This makes the badge position relative clips the badge's bottom-right corner at 45 degrees.
to the card's bevel corner match the media frame's placement exactly Values tuned so the badge is mostly visible with just the corner clipped. */
(8px from card edge in both cases). */ .card > .dt-card-badge,
.card-body-flush > .dt-card-badge { .dt-bevel-card > .dt-card-badge {
bottom: 5px; position: absolute;
right: 5px; bottom: calc(var(--bevel-md) * 0.25);
right: calc(var(--bevel-md) * -0.15);
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;
} }
/* ============================================================================ /* ============================================================================
@@ -542,7 +620,6 @@
font-weight: 600; font-weight: 600;
font-size: 0.875rem; font-size: 0.875rem;
letter-spacing: 0.05em; letter-spacing: 0.05em;
text-transform: uppercase;
cursor: pointer; cursor: pointer;
transition: background-color var(--transition-fast), transition: background-color var(--transition-fast),
color var(--transition-fast), color var(--transition-fast),

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;
} }