Compare commits

..

26 Commits

Author SHA1 Message Date
michael
a3ab5d892c pre-gitea addition 2026-07-13 21:40:03 -07:00
michael
1951cf98da fix(web): media placeholder reads LOADING MEDIA, not MISSING MEDIA
The ::after placeholder text on .dt-bevel-media and empty card bodies is
mostly seen while lazy media loads, where MISSING MEDIA reads as 'the
store is broken'. Neutral loading wording instead. Bump to 0.5.1.

Revert with: git revert <this-sha>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:55:18 -07:00
michael
2d49471ebb fix(web): .badge displays as inline-block so wrapped labels keep their shape
As a plain inline span, a badge that wraps to two lines fragments into
per-line boxes: the clip-path and ::before fill cover the wrong region,
so overflow text renders bare outside the beveled shape (storefront
DTLabel ribbons at <=375px viewports). inline-block keeps the badge one
box; the bevel and fill wrap all lines.

Revert with: git revert <this-sha>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 16:03:29 -07:00
michael
e33bf70896 fix(react): a11y attrs on DTMobileFilterOverlay
Add the screen-reader semantics that were missing:
- role="dialog" + aria-modal="true" on the content panel
- aria-labelledby pointing at the heading span (id via useId)
- aria-label="Close {heading}" on the ✕ button (was just text)
- aria-hidden="true" on the click-target backdrop

Without these, screen readers can't identify the mobile filter panel
as a dialog, can't announce its label, and the close button reads as
just "X". useId so multiple overlay instances on a page don't collide
on heading IDs.

Surfaced during dt-shopify pre-launch §10 verification — two
MobileFilterMenu.test.tsx assertions were dropped to make the suite
green pending this fix; storefront can put them back now.

Revert: git revert <THIS_COMMIT_SHA>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 16:38:38 -07:00
michael
a459b643e8 feat(web): dt-scale-in invisible-via-opacity during delay so children can self-measure
Prior keyframes held descendants at transform: scale(0) during the
animation-delay phase (with fill-mode: both). Any DTStaggerContainer
child performing getBoundingClientRect()-based self-measurement on
mount — most notably R3F's <Canvas>, but also chart libs, video
players, and virtualized lists — read a near-zero rect at the
transform-multiplied size and never recovered, because transform
changes don't fire ResizeObserver when they end.

New keyframes:
  0%     opacity 0, transform none    (during delay: invisible but
                                       full layout size, no transform —
                                       children measure correctly)
  0.001% opacity 0, transform scale(0) (snap to scaled-down at start)
  100%   opacity 1, transform scale(1) (animate scale + fade in)

Behavior change for consumers: entrance gains a subtle fade-in
alongside the scale-in. Functionally equivalent for the existing
DTStaggerContainer use cases. Fixes the SuggestedPlacements 3D
viewer "blank teal block until scroll" bug on the Hydrogen
storefront product pages.

Bumps @dangerousthings/web to 0.5.0 (MINOR — visual behavior change).

To revert: git revert <this-commit>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:38:42 -07:00
michael
1aeceb8f24 Main package.json fix 2026-05-05 14:20:26 -07:00
michael
e664cdaf3d Showcase fix 2026-05-05 14:18:53 -07:00
michael
0cfc945960 Fixed type in label 2026-05-05 14:16:32 -07:00
michael
77c6a5a3bf feat(DTGallery): add optional activeIndex prop for external control
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:42:10 -07:00
michael
27824ae189 chore(hex-background): bump 0.2.1, changelog for animation fix 2026-04-13 16:28:59 -07:00
michael
1f67ebf48a feat(hex-background): pass animationDuration through HexGridBackground 2026-04-13 16:26:17 -07:00
michael
5112e5c057 fix(hex-background): replace lerp with duration-based easing
Hexagons now interpolate via smoothstep over a fixed duration instead of
frame-rate-dependent lerp. Prevents height convergence to mean over time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:23:28 -07:00
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
44 changed files with 720 additions and 349 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:
- **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
## Getting Started

View File

@@ -1,7 +1,6 @@
{
"name": "dt-design-system",
"version": "0.0.0",
"private": true,
"description": "Dangerous Things design system — shared tokens, web CSS, and React Native components",
"repository": {
"type": "git",
@@ -36,4 +35,4 @@
"node": ">=18.0.0"
},
"packageManager": "npm@10.0.0"
}
}

View File

@@ -1,5 +1,13 @@
# @dangerousthings/hex-background
## 0.2.1
### Fixed
- Animation no longer degrades to noise over time — replaced frame-rate-dependent lerp with duration-based smoothstep easing
### Added
- `animationDuration` prop (default 1200ms) controls how long each hexagon takes to reach its target height
## 0.2.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@dangerousthings/hex-background",
"version": "0.2.0",
"version": "0.2.1",
"description": "3D hexagon grid background for the Dangerous Things design system",
"license": "MIT",
"author": {

View File

@@ -13,7 +13,6 @@ import {
Shape,
ExtrudeGeometry,
MeshStandardMaterial,
MathUtils,
} from 'three';
export interface HexGridProps {
@@ -29,6 +28,13 @@ export interface HexGridProps {
maxHeight?: number;
/** Interval (ms) between height target changes @default 1500 */
animationInterval?: number;
/** Duration (ms) for each hex to reach its target @default 1200 */
animationDuration?: number;
}
/** Hermite smoothstep: 3t² - 2t³ */
function smoothstep(t: number): number {
return t * t * (3 - 2 * t);
}
function computeHexPositions(
@@ -62,6 +68,7 @@ export function HexGrid({
margin = 0.05,
maxHeight = 3,
animationInterval = 1500,
animationDuration = 1200,
}: HexGridProps) {
const meshRef = useRef<InstancedMesh>(null);
@@ -70,11 +77,11 @@ export function HexGrid({
[rows, cols, hexRadius, extrudeHeight, margin],
);
const targetHeightsRef = useRef(
positions.map(() => 1 + Math.random() * maxHeight),
);
const currentHeightsRef = useRef(
positions.map(() => 1 + Math.random() * maxHeight),
const animStateRef = useRef(
positions.map(() => {
const h = 1 + Math.random() * maxHeight;
return {startHeight: h, targetHeight: h, startTime: 0};
}),
);
// Reuse dummy object for matrix updates — 15-20% FPS improvement
@@ -101,25 +108,41 @@ export function HexGrid({
// Regenerate target heights at interval
useEffect(() => {
const interval = setInterval(() => {
targetHeightsRef.current = positions.map(
() => 0.5 + Math.random() * maxHeight,
);
const now = performance.now();
animStateRef.current = animStateRef.current.map((state) => {
// Compute where the hex currently is (same easing as useFrame)
const elapsed = Math.min(
(now - state.startTime) / animationDuration,
1,
);
const t = smoothstep(elapsed);
const currentHeight =
state.startHeight + (state.targetHeight - state.startHeight) * t;
return {
startHeight: currentHeight,
targetHeight: 0.5 + Math.random() * maxHeight,
startTime: now,
};
});
}, animationInterval);
return () => clearInterval(interval);
}, [positions, maxHeight, animationInterval]);
}, [positions.length, maxHeight, animationInterval, animationDuration]);
// Animate per frame: lerp current heights toward targets
useFrame((_state, delta) => {
// Animate per frame: time-based smoothstep easing
useFrame(() => {
if (!meshRef.current) return;
const dummy = dummyRef.current;
const now = performance.now();
positions.forEach(({x, z}, i) => {
currentHeightsRef.current[i] = MathUtils.lerp(
currentHeightsRef.current[i],
targetHeightsRef.current[i],
delta * 0.5,
const state = animStateRef.current[i];
const elapsed = Math.min(
(now - state.startTime) / animationDuration,
1,
);
const height = currentHeightsRef.current[i];
const t = smoothstep(elapsed);
const height =
state.startHeight + (state.targetHeight - state.startHeight) * t;
dummy.position.set(x, height / 2, z);
dummy.scale.set(1, height, 1);

View File

@@ -22,6 +22,8 @@ export interface HexGridBackgroundProps {
maxHeight?: number;
/** Height target refresh interval in ms @default 1500 */
animationInterval?: number;
/** Duration (ms) for each hex to ease to target @default 1200 */
animationDuration?: number;
/** Camera orbital speed @default 0.02 */
cameraSpeed?: number;
/** Camera orbital radius @default 8 */
@@ -62,6 +64,7 @@ export function HexGridBackground({
margin = 0.05,
maxHeight = 3,
animationInterval = 1500,
animationDuration = 1200,
cameraSpeed = 0.02,
cameraRadius = 8,
fov = 40,
@@ -134,6 +137,7 @@ export function HexGridBackground({
margin={margin}
maxHeight={maxHeight}
animationInterval={animationInterval}
animationDuration={animationDuration}
/>
</Suspense>
</Canvas>

View File

@@ -1,5 +1,17 @@
# @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
### Minor Changes

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,8 @@
* - Content in beveled card container
*/
import {StyleSheet, ViewStyle, StyleProp, KeyboardAvoidingView, Platform} from 'react-native';
import {Portal, Modal} from 'react-native-paper';
import {StyleSheet, ViewStyle, StyleProp, KeyboardAvoidingView, Platform, Pressable} from 'react-native';
import {Portal, Modal, Text} from 'react-native-paper';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant} from '../utils/variantColors';
import {DTCard} from './DTCard';
@@ -97,6 +97,17 @@ export function DTModal({
mode={variant}
title={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}>
{children}
</DTCard>

View File

@@ -1,5 +1,24 @@
# @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
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@dangerousthings/react",
"version": "1.0.0",
"version": "2.1.0",
"description": "React web components for the Dangerous Things design system",
"license": "MIT",
"author": {
@@ -59,4 +59,4 @@
"react-dom": "^18.0.0",
"typescript": "^5.8.0"
}
}
}

View File

@@ -6,7 +6,8 @@
*
* 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).
* 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';
@@ -22,10 +23,18 @@ interface DTCardProps {
title?: string;
/** Whether to show the accent header bar @default true when title is provided */
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;
/** 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;
style?: CSSProperties;
/** Badge content rendered at bottom-right, clipped by card bevel */
badge?: ReactNode;
onClick?: () => void;
}
@@ -34,12 +43,16 @@ export function DTCard({
variant = 'normal',
title,
showHeader,
progress = 0,
onDismiss,
progress,
grow = false,
growArea = 'body',
className,
style,
onClick,
badge,
}: DTCardProps) {
const shouldShowHeader = showHeader ?? !!title;
const shouldShowHeader = showHeader ?? !!(title || onDismiss);
const Tag = onClick ? 'button' : 'div';
return (
@@ -47,20 +60,40 @@ export function DTCard({
className={cx(
'card',
getVariantClass(variant),
grow && 'dt-card-grow',
grow && growArea === 'title' && 'dt-card-grow-title',
className,
)}
style={{
...style,
'--dt-card-progress': progress,
...(progress != null && { '--dt-card-progress': progress }),
} as CSSProperties}
onClick={onClick}
type={onClick ? 'button' : undefined}>
{shouldShowHeader && (
<div className="card-title">
{title}
<div className="card-title" style={onDismiss ? { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } : undefined}>
<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>
)}
{children}
{badge && <div className="dt-card-badge">{badge}</div>}
</Tag>
);
}

View File

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

View File

@@ -1,56 +1,179 @@
/**
* 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
*/
import { useState, type CSSProperties } from 'react';
import { useState, useEffect, type CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses';
export interface DTGalleryItem {
key: string;
src: string;
alt?: string;
thumbnail?: string;
/* ── TypeScript declaration for <model-viewer> web component ── */
declare global {
namespace JSX {
interface IntrinsicElements {
'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 {
items: DTGalleryItem[];
/** Externally-controlled active index. When this value changes, the gallery
* jumps to the corresponding item. Thumbnail clicks still work normally. */
activeIndex?: number;
/** Color variant @default 'normal' */
variant?: DTVariant;
/** Aspect ratio (width / height) for the display area @default 1 */
aspectRatio?: number;
className?: string;
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({
items,
activeIndex: activeIndexProp,
variant = 'normal',
aspectRatio = 1,
className,
style,
}: DTGalleryProps) {
const [activeIndex, setActiveIndex] = useState(0);
const [activeIndex, setActiveIndex] = useState(activeIndexProp ?? 0);
// Sync external activeIndex prop to internal state when it changes
useEffect(() => {
if (activeIndexProp != null && activeIndexProp >= 0 && activeIndexProp < items.length) {
setActiveIndex(activeIndexProp);
}
}, [activeIndexProp, items.length]);
const hasModel = items.some((i) => i.type === 'model3d');
useModelViewerScript(hasModel);
if (items.length === 0) return null;
const current = items[activeIndex];
const currentType = current.type || 'image';
return (
<div className={cx(getVariantClass(variant), className)} style={style}>
{/* Main image */}
<div className="dt-bevel-media" style={{ overflow: 'hidden' }}>
<img
src={current.src}
alt={current.alt || ''}
style={{
width: '100%',
height: 'auto',
display: 'block',
transition: 'opacity 300ms ease-in-out',
}}
/>
{/* Main display */}
<div
className="dt-bevel-media"
style={{ overflow: 'hidden', aspectRatio, position: 'relative' }}
>
{currentType === 'image' && (
<img
src={current.src}
alt={current.alt || ''}
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>
{/* Thumbnails */}
{items.length > 1 && (
<div
@@ -59,31 +182,59 @@ export function DTGallery({
gap: '8px',
marginTop: '8px',
overflowX: 'auto',
}}>
{items.map((item, i) => (
<button
key={item.key}
onClick={() => setActiveIndex(i)}
type="button"
style={{
flexShrink: 0,
width: '64px',
height: '64px',
padding: 0,
border: i === activeIndex
? '2px solid var(--dt-card-color, var(--color-primary))'
: '2px solid transparent',
background: 'none',
cursor: 'pointer',
overflow: 'hidden',
}}>
<img
src={item.thumbnail || item.src}
alt={item.alt || ''}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</button>
))}
}}
>
{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
key={item.key}
onClick={() => setActiveIndex(i)}
type="button"
style={{
flexShrink: 0,
width: '64px',
height: '64px',
padding: 0,
border:
i === activeIndex
? '2px solid var(--dt-card-color, var(--color-primary))'
: '2px solid transparent',
background: 'none',
cursor: 'pointer',
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>

View File

@@ -25,7 +25,7 @@ export function DTLabel({
}: DTLabelProps) {
return (
<span
className={cx('badge', 'badge-mode', getVariantClass(variant), className)}
className={cx('badge', 'p-3', 'badge-mode', getVariantClass(variant), className)}
style={style}>
{children}
</span>

View File

@@ -2,25 +2,43 @@
* DTMediaFrame — Diagonal beveled frame (top-left + bottom-right).
*
* 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 { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses';
interface DTMediaFrameProps {
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;
style?: CSSProperties;
}
export function DTMediaFrame({
children,
variant = 'normal',
placeholder,
className,
style,
}: DTMediaFrameProps) {
const isEmpty = children == null || children === false;
return (
<div className={cx('dt-bevel-media', className)} style={style}>
{children}
<div
className={cx('dt-bevel-media', getVariantClass(variant), className)}
style={style}
>
{isEmpty && placeholder ? placeholder : children}
</div>
);
}

View File

@@ -5,7 +5,7 @@
* .dt-filter-overlay-content
*/
import { useEffect, useCallback, type ReactNode, type CSSProperties } from 'react';
import { useEffect, useCallback, useRef, useId, type ReactNode, type CSSProperties } from 'react';
import { createPortal } from 'react-dom';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
@@ -38,6 +38,10 @@ export function DTMobileFilterOverlay({
className,
style,
}: DTMobileFilterOverlayProps) {
const panelRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLElement | null>(null);
const headingId = useId();
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') onDismiss();
@@ -45,6 +49,7 @@ export function DTMobileFilterOverlay({
[onDismiss],
);
// Escape key
useEffect(() => {
if (visible) {
document.addEventListener('keydown', handleKeyDown);
@@ -52,12 +57,67 @@ export function DTMobileFilterOverlay({
}
}, [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;
return createPortal(
<div className={cx('dt-filter-overlay', getVariantClass(variant), className)} style={style}>
<div className="dt-filter-overlay-backdrop" onClick={onDismiss} />
<div className="dt-filter-overlay-content">
<div
className="dt-filter-overlay-backdrop"
onClick={onDismiss}
aria-hidden="true"
/>
<div
ref={panelRef}
className="dt-filter-overlay-content"
role="dialog"
aria-modal="true"
aria-labelledby={headingId}
>
{/* Header */}
<div
style={{
@@ -67,7 +127,7 @@ export function DTMobileFilterOverlay({
padding: '16px',
borderBottom: '1px solid rgba(var(--color-primary-rgb), 0.2)',
}}>
<span style={{ fontWeight: 700, fontSize: '1.125rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
<span id={headingId} style={{ fontWeight: 700, fontSize: '1.125rem', letterSpacing: '0.05em' }}>
{heading}
{activeFilterCount !== undefined && activeFilterCount > 0 && (
<span
@@ -88,7 +148,6 @@ export function DTMobileFilterOverlay({
color: 'var(--color-primary)',
fontSize: '0.875rem',
cursor: 'pointer',
textTransform: 'uppercase',
}}>
Clear All
</button>
@@ -96,6 +155,7 @@ export function DTMobileFilterOverlay({
<button
onClick={onDismiss}
type="button"
aria-label={`Close ${heading}`}
style={{
background: 'none',
border: 'none',

View File

@@ -82,13 +82,35 @@ export function DTModal({
position: 'relative',
maxWidth: '90vw',
maxHeight: '85vh',
overflow: 'auto',
display: 'flex',
flexDirection: 'column',
...style,
}}
role="dialog"
aria-modal="true">
{title && <div className="card-title">{title}</div>}
<div style={{ padding: 'var(--space-6, 24px)' }}>{children}</div>
{title && (
<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>,
document.body,

View File

@@ -2,18 +2,22 @@
* DTStaggerContainer — Staggered scale-in animation for children.
*
* 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';
interface DTStaggerContainerProps {
children: ReactNode;
/** Duration per child animation @default '0.33s' */
duration?: string;
/** Delay between each child @default '75ms' */
interval?: string;
/** Delay between each child in ms @default 150 */
interval?: number;
/** Base delay before the first child animates in ms @default 0 */
delay?: number;
className?: string;
style?: CSSProperties;
}
@@ -21,19 +25,23 @@ interface DTStaggerContainerProps {
export function DTStaggerContainer({
children,
duration,
interval,
interval = 150,
delay = 0,
className,
style,
}: DTStaggerContainerProps) {
const cssVars: Record<string, string> = {};
if (duration) cssVars['--dt-stagger-duration'] = duration;
if (interval) cssVars['--dt-stagger-interval'] = interval;
return (
<div
className={cx('dt-stagger-container', className)}
style={{ ...cssVars, ...style } as CSSProperties}>
{children}
{Children.map(children, (child, i) => (
<div style={{ animationDelay: `${delay + i * interval}ms` }}>
{child}
</div>
))}
</div>
);
}

View File

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

View File

@@ -1,11 +1,12 @@
/**
* 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.
*/
import { createContext, type ReactNode } from 'react';
import { createContext, useEffect, type ReactNode } from 'react';
import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
@@ -21,7 +22,9 @@ interface DTWebThemeProviderProps {
brand?: ThemeBrand;
/** Theme mode @default 'dark' */
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;
children: ReactNode;
}
@@ -29,14 +32,26 @@ interface DTWebThemeProviderProps {
export function DTWebThemeProvider({
brand = 'dt',
theme = 'dark',
documentLevel,
className,
children,
}: DTWebThemeProviderProps) {
useEffect(() => {
if (documentLevel) {
document.documentElement.dataset.brand = brand;
document.documentElement.dataset.theme = theme;
}
}, [documentLevel, brand, theme]);
return (
<DTWebThemeContext.Provider value={{ brand, theme }}>
<div data-brand={brand} data-theme={theme} className={cx('dt-theme-root', className)}>
{children}
</div>
{documentLevel ? (
children
) : (
<div data-brand={brand} data-theme={theme} className={cx('dt-theme-root', className)}>
{children}
</div>
)}
</DTWebThemeContext.Provider>
);
}

View File

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

View File

@@ -1,13 +1,17 @@
{
"name": "@dangerousthings/showcase-desktop",
"version": "0.0.0",
"private": true,
"license": "MIT",
"description": "Desktop showcase for the Dangerous Things design system",
"author": {
"name": "Dangerous Things",
"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",
"main": "dist/main/main.js",
"scripts": {

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import { DTCard, DTStaggerContainer } from '@dangerousthings/react';
import { DTCard } from '@dangerousthings/react';
import { Section, Row, CodeLabel } from '../components/Section';
function AnimBox({ className, label }: { className: string; label: string }) {
@@ -66,7 +66,7 @@ function ProgressBarDemo() {
<DTCard
key={v}
variant={v}
title={`${v.toUpperCase()} ${progress[i]}%`}
title={`${v} ${progress[i]}%`}
progress={progress[i]}
style={{ width: 160, transition: 'all 0.1s ease-out' }}
>
@@ -90,13 +90,12 @@ function ProgressBarDemo() {
export function AnimationsPage() {
const [entranceKey, setEntranceKey] = useState(0);
const [staggerKey, setStaggerKey] = useState(0);
const [accordionExpanded, setAccordionExpanded] = useState(false);
return (
<>
<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.">
<Row key={entranceKey}>
@@ -119,28 +118,6 @@ export function AnimationsPage() {
<CodeLabel text=".dt-animate-pulse | .dt-animate-ping | .dt-animate-spin" />
</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.">
<div style={{ maxWidth: 400 }}>
<button
@@ -156,7 +133,6 @@ export function AnimationsPage() {
cursor: 'pointer',
color: 'var(--color-text)',
fontWeight: 600,
textTransform: 'uppercase',
}}
aria-expanded={accordionExpanded}
onClick={() => setAccordionExpanded(!accordionExpanded)}

View File

@@ -53,12 +53,12 @@ export function BevelsPage() {
<Row>
{modes.map(mode => (
<button key={mode} className={`dt-menu-item mode-${mode}`} type="button" onClick={() => setModalVariant(mode)}>
{mode.toUpperCase()} MODAL
{mode} Modal
</button>
))}
</Row>
<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={{ color: 'var(--color-text-muted)', fontSize: '0.85rem' }}>Click the backdrop or press Escape to dismiss.</p>
</DTModal>
@@ -74,7 +74,7 @@ export function BevelsPage() {
</button>
</Row>
<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={{ color: 'var(--color-text-muted)', fontSize: '0.85rem' }}>Click the backdrop, press Escape, or click to dismiss.</p>
</DTDrawer>
@@ -91,7 +91,7 @@ export function BevelsPage() {
<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)' }}>
<span style={{ fontWeight: 600, textTransform: 'uppercase' }}>Thick Top Border Accent</span>
<span style={{ fontWeight: 600 }}>Thick Top Border Accent</span>
</div>
<CodeLabel text=".dt-accent-top" />
</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.">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3">
{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>
</DTCard>
))}

View File

@@ -1,7 +1,6 @@
import { createElement, useState, type CSSProperties } from 'react';
import { createElement, type CSSProperties } from 'react';
import {
DTCard,
DTStaggerContainer,
DTFeatureLegend,
} 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' } });
// 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[] = [
{ 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: 'digital_security', name: 'Digital Security', icon: ico(FaUserShield), state: 'supported', detail: 'FIDO2 / WebAuthn' },
{ key: 'access_control', name: 'Access Control', icon: ico(MdOutlineVpnKey), state: 'supported', detail: 'Legacy' },
{ 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: 'data_sharing', name: 'Data Sharing', icon: ico(MdOutlineMobileScreenShare), state: 'supported', detail: 'NDEF records, vCard, URL' },
{ key: 'payment', name: 'Payment', icon: ico(MdOutlineCreditCard), state: 'disabled', detail: 'Apex required' },
{ key: 'magic', name: 'Magic', icon: ico(MdOutlineCopyAll), state: 'supported', detail: 'Gen2 Magic UID' },
{ key: 'Illumination', name: 'Illumination', icon: ico(MdOutlineLightbulb), state: 'unsupported', detail: 'None' },
{ key: 'data_sharing', name: 'Data Sharing', icon: ico(MdOutlineMobileScreenShare), state: 'supported', detail: '1 kB' },
{ key: 'payment', name: 'Payment', icon: ico(MdOutlineCreditCard), state: 'unsupported', detail: 'Not Supported' },
{ key: 'magic', name: 'Magic', icon: ico(MdOutlineCopyAll), state: 'supported', detail: 'Yes' },
{ 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' },
];
// Full biomagnet feature legend (4 features — from storefront MagnetUseCaseLegend)
// m0422a: axial neodymium disc, bioresin coated
const magnetFeatures: DTFeatureItem[] = [
{ key: 'sensing', name: 'Sensing', icon: ico(MdOutlineSensors), state: 'supported', detail: 'Electromagnetic field detection' },
{ key: 'lifting', name: 'Lifting', icon: ico(MdOutlineFitbit), state: 'supported', detail: '2.1 kg lifting force' },
{ key: 'haptics', name: 'Haptics', icon: ico(MdOutlineVibration), state: 'supported', detail: 'Tactile vibration feedback' },
{ key: 'polarity', name: 'Polarity Detection', icon: ico(MdOutlineExplore), state: 'unsupported', detail: 'Not Supported' },
{ 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: '~20 g' },
{ key: 'haptics', name: 'Haptics', icon: ico(MdOutlineVibration), state: 'supported', detail: 'Push/Pull' },
{ key: 'polarity', name: 'Polarity Detection', icon: ico(MdOutlineExplore), state: 'supported', detail: 'Monopole' },
];
// Full aesthetic feature legend (2 features — from storefront AestheticUseCaseLegend)
const aestheticFeatures: DTFeatureItem[] = [
{ key: 'illumination', name: 'Illumination', icon: ico(MdLightbulbOutline), state: 'supported', detail: 'LED: Red, Green, Blue, White' },
{ key: 'prominence', name: 'Prominence', icon: ico(MdOutlineVisibility), state: 'supported', detail: 'High visibility under skin' },
{ 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: 'Low' },
];
export function CardsAdvancedPage() {
const [staggerKey, setStaggerKey] = useState(0);
return (
<>
<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.">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3">
{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>
</DTCard>
))}
@@ -80,6 +78,23 @@ export function CardsAdvancedPage() {
</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).">
<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) => (
@@ -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: 'NFC', mode: 'normal' as DTVariant, img: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=400&h=400&fit=crop' },
].map(badge => (
<DTCard key={badge.label} variant={badge.mode} title={badge.label + ' PRODUCT'}>
<div className="card-body-flush dt-badge-parent" style={{ aspectRatio: '1' }}>
<DTCard key={badge.label} variant={badge.mode} title={badge.label + ' PRODUCT'}
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' }} />
<span className="dt-card-badge">{badge.label}</span>
</div>
</DTCard>
))}
</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 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 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)' }} />
<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)' }} />
<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)' }} />
<CodeLabel text="<DTFeatureLegend features={chipFeatures} title='NExT Features' variant='normal' />" />
<CodeLabel text="<DTFeatureLegend features={magnetFeatures} variant='emphasis' columns={4} />" />
@@ -148,27 +163,6 @@ export function CardsAdvancedPage() {
</div>
</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 }) => (
<div key={key} style={{ display: 'grid', gridTemplateColumns: '1fr auto', alignItems: 'center', gap: 'var(--space-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}
</span>
<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 { Section, CodeLabel } from '../components/Section';
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: '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: '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,
letterSpacing: '0.15em',
marginBottom: '0.25rem',
textTransform: 'uppercase',
}}>
DANGEROUS THINGS
</h1>
@@ -40,7 +39,6 @@ export function HomePage() {
fontSize: '1.25rem',
fontWeight: 700,
letterSpacing: '0.2em',
textTransform: 'uppercase',
marginTop: 0,
}}>
DESIGN SYSTEM
@@ -71,16 +69,15 @@ export function HomePage() {
color: 'var(--color-text-muted)',
fontSize: '0.7rem',
letterSpacing: '0.15em',
textTransform: 'uppercase',
marginBottom: 'var(--space-4)',
}}>
COMPONENT CATALOG
</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 => (
<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>
{cat.count > 0 && (
<div style={{ color: 'var(--color-text-muted)', fontSize: '0.7rem', marginTop: 'var(--space-2)' }}>
@@ -90,7 +87,7 @@ export function HomePage() {
</DTCard>
</a>
))}
</DTStaggerContainer>
</div>
<Section title="Quick Start" description="Import the React components and CSS to get started.">
<div className="terminal dt-accent-top">

View File

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

View File

@@ -1,7 +1,6 @@
{
"name": "@dangerousthings/showcase-mobile",
"version": "0.0.0",
"private": true,
"license": "MIT",
"main": "./index.js",
"scripts": {
@@ -39,4 +38,4 @@
"@types/react": "~19.1.0",
"typescript": "^5.8.3"
}
}
}

View File

@@ -1,15 +1,12 @@
import React from 'react';
import { View } from 'react-native';
import { Text } from 'react-native-paper';
import {
DTCard,
DTLabel,
DTStaggerContainer,
useDTTheme,
useScaleIn,
usePulse,
} from '@dangerousthings/react-native';
import type { DTVariant } from '@dangerousthings/react-native';
import { ScreenContainer } from '../components/ScreenContainer';
import { DemoSection } from '../components/DemoSection';
import { CodeLabel } from '../components/CodeLabel';
@@ -20,44 +17,8 @@ export function AnimationsScreen() {
const scaleAnim = useScaleIn({ duration: 600 });
const pulseAnim = usePulse(true);
const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other'];
return (
<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 */}
<DemoSection
title="useScaleIn"

View File

@@ -5,7 +5,6 @@ import {
DTCard,
DTChip,
DTBadgeOverlay,
DTStaggerContainer,
useDTTheme,
} 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>" />
</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>
);
}

View File

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

View File

@@ -1,5 +1,23 @@
# @dangerousthings/web
## 0.5.0
### Minor Changes
- `dt-scale-in` keyframes now hold `transform: none, opacity: 0` during `animation-delay` (the period when `animation-fill-mode: both` shows the first keyframe), then snap to `scale(0)` at animation start before scaling/fading to 1. Visual difference vs prior is a subtle fade-in alongside the scale-in. **Why:** prior keyframes held descendants at `transform: scale(0)` during the delay, which caused any child performing `getBoundingClientRect()`-based self-measurement on mount (R3F `<Canvas>`, ResizeObserver-driven charts, virtualized lists, etc.) to read a near-zero rect and never recover — `transform` changes don't fire `ResizeObserver` when they end. Fixes the SuggestedPlacements 3D viewer "blank teal block until scroll" bug on the Hydrogen storefront product pages.
## 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
### Minor Changes

View File

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

View File

@@ -7,8 +7,16 @@
============================================================================ */
@keyframes dt-scale-in {
from { transform: scale(0); }
to { transform: scale(1); }
/* During animation-delay (with fill-mode: both), this 0% keyframe holds.
transform: none lets descendants self-measure correctly on mount —
critical for ResizeObserver-based components (R3F Canvas, charts,
virtualized lists) whose initial getBoundingClientRect() would
otherwise be transform-multiplied to ~0 inside a scale(0) ancestor. */
0% { opacity: 0; transform: none; }
/* Snap to scale(0) at animation start so the entrance is still a
visible scale-in (with a brief fade-in alongside). */
0.001% { opacity: 0; transform: scale(0); }
100% { opacity: 1; transform: scale(1); }
}
@keyframes dt-fade-in {
@@ -82,49 +90,16 @@
/* ============================================================================
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-duration: 0.33s;
--dt-stagger-interval: 75ms;
}
.dt-stagger-container > * {
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
============================================================================ */

View File

@@ -19,6 +19,8 @@
[data-brand="dt"] .card {
--dt-card-progress: 0;
--_card-pad: var(--space-6);
display: flex;
flex-direction: column;
position: relative;
isolation: isolate;
background: var(--dt-card-color, var(--color-primary));
@@ -44,17 +46,21 @@
z-index: -1;
/* Inner clip-path: left edge at bevel-sm to create progress bar zone.
The progress bar occupies x=0 to x=bevel-sm with its own borders.
Inner surface starts at bevel-sm, so the left frame is wider than 3px. */
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,
calc(100% - 3px) 3px,
calc(100% - 3px) calc(100% - var(--bevel-md)),
calc(100% - var(--bevel-md)) calc(100% - 3px),
calc(100% - 3px) calc(100% - var(--bevel-md) - 3px),
calc(100% - var(--bevel-md) - 3px) calc(100% - 3px),
var(--bevel-sm) calc(100% - 3px));
}
/* Card Progress Bar — structural fill area on the left edge
Sits between the 3px frame borders, follows bottom-left bevel diagonal.
Gradient fills 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.
At 0%: all semi-transparent surface. At 100%: all opaque accent.
Override --dt-progress-empty-opacity to control unfilled portion transparency. */
@@ -70,7 +76,7 @@
calc(var(--bevel-sm) - 3px) calc(100% - 6px),
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(--color-surface-rgb),
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) */
[data-brand="dt"] .dt-bevel-card > *:not(.dt-card-badge),
[data-brand="dt"] .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-badge-bottom-right):not(.dt-card-badge) {
position: relative;
z-index: 2;
}
/* Card Grow — stretch to fill available space in flex/grid containers.
By default the body (content area) grows. Use .dt-card-grow-title to
grow the header instead. */
.dt-card-grow {
flex: 1 1 0%;
height: 100%;
}
.dt-card-grow > .card-title {
flex: 0 0 auto;
}
.dt-card-grow:not(.dt-card-grow-title) > :last-child {
flex: 1 1 auto;
}
.dt-card-grow.dt-card-grow-title > .card-title {
flex: 1 1 auto;
}
/* Card Header — storefront .card-header pattern
Title starts at bevel-sm (past the progress bar zone) and extends to the right edge.
Below it, the ::before dark surface shows through for the body area. */
@@ -102,7 +128,6 @@
background: var(--dt-card-color, var(--color-primary));
color: var(--color-bg);
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 1rem;
position: relative;
@@ -125,17 +150,16 @@
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
at the diagonal corner. Badge is not clipped (it's a sibling, not img). */
[data-brand="dt"] .card-body-flush > img,
[data-brand="dt"] .card-body-flush > video {
at the diagonal corner. Badge overlays are excluded. */
[data-brand="dt"] .card-body-flush > *:not(.dt-badge-bottom-right):not(.dt-card-badge) {
display: block;
clip-path: polygon(
0% 0%,
100% 0%,
100% calc(100% - var(--bevel-md) + 3px),
calc(100% - var(--bevel-md) + 3px) 100%,
100% calc(100% - var(--bevel-md)),
calc(100% - var(--bevel-md)) 100%,
0% 100%);
}
@@ -202,6 +226,9 @@
[data-brand="dt"] .badge {
--badge-border-color: var(--color-border);
--badge-fill: var(--color-surface);
/* inline-block so a wrapping badge stays one box — clip-path and the
::before fill fragment across line boxes on a plain inline span */
display: inline-block;
position: relative;
isolation: isolate;
background: var(--badge-border-color);
@@ -272,6 +299,37 @@
color: var(--color-bg);
}
/* Mode-variant badges — bridge .mode-* vars to badge color vars */
[data-brand="dt"] .badge.mode-normal {
--badge-border-color: var(--mode-normal);
--badge-fill: var(--mode-normal);
color: var(--color-bg);
}
[data-brand="dt"] .badge.mode-emphasis {
--badge-border-color: var(--mode-emphasis);
--badge-fill: var(--mode-emphasis);
color: var(--color-bg);
}
[data-brand="dt"] .badge.mode-warning {
--badge-border-color: var(--mode-warning);
--badge-fill: var(--mode-warning);
color: var(--color-bg);
}
[data-brand="dt"] .badge.mode-success {
--badge-border-color: var(--mode-success);
--badge-fill: var(--mode-success);
color: var(--color-bg);
}
[data-brand="dt"] .badge.mode-other {
--badge-border-color: var(--mode-other);
--badge-fill: var(--mode-other);
color: var(--color-bg);
}
/* ============================================================================
Opt-in Bevel Utility Classes
Use these for custom elements that need bevel shapes
@@ -326,7 +384,9 @@
}
.dt-bevel-media > img,
.dt-bevel-media > video {
.dt-bevel-media > video,
.dt-bevel-media > model-viewer,
.dt-bevel-media > iframe {
display: block;
clip-path: polygon(
calc(var(--bevel-md) + 1px) 3px,
@@ -337,11 +397,20 @@
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.
Surface-colored background is already provided by ::before.
This adds centered "MISSING MEDIA" text via ::after. */
This adds centered "LOADING MEDIA" text via ::after. Mostly seen while
lazy media loads, so it must read as a loading state, not an error. */
.dt-bevel-media::after {
content: 'MISSING MEDIA';
content: 'LOADING MEDIA';
position: absolute;
inset: 0;
display: flex;
@@ -352,7 +421,6 @@
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
pointer-events: none;
}
@@ -360,7 +428,7 @@
Uses surface background and centered placeholder text. */
.dt-badge-parent:empty::after,
.card-body-flush:empty::after {
content: 'MISSING MEDIA';
content: 'LOADING MEDIA';
display: flex;
align-items: center;
justify-content: center;
@@ -369,7 +437,6 @@
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
background: var(--color-surface);
}
@@ -492,15 +559,17 @@
/* ============================================================================
Card Badge — bottom-right product type chip (LAB, BUNDLE, etc.)
Source: dt-shopify-storefront CyberProductCard LabChip / BundleChip
Plain flat rectangle. Inherits card color via --dt-card-color.
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 {
position: relative;
}
.dt-card-badge {
/* Legacy: standalone badge positioning for non-card contexts */
.dt-badge-bottom-right {
position: absolute;
bottom: 8px;
right: 8px;
@@ -508,19 +577,32 @@
padding: 2px calc(8px + 0.2rem) 2px 8px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
background-color: var(--dt-card-color, var(--color-primary));
color: var(--color-bg);
}
/* Inside card body-flush, offset badge position to compensate for the
3px margin from the card edge. This makes the badge position relative
to the card's bevel corner match the media frame's placement exactly
(8px from card edge in both cases). */
.card-body-flush > .dt-card-badge {
bottom: 5px;
right: 5px;
/* Card-level badge slot — positioned so the card's bevel clip-path
clips the badge's bottom-right corner at 45 degrees.
Values tuned so the badge is mostly visible with just the corner clipped. */
.card > .dt-card-badge,
.dt-bevel-card > .dt-card-badge {
position: absolute;
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 +624,6 @@
font-weight: 600;
font-size: 0.875rem;
letter-spacing: 0.05em;
text-transform: uppercase;
cursor: pointer;
transition: background-color var(--transition-fast),
color var(--transition-fast),

View File

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