Add storefront component migration: cards, badges, progress bars, animations, and React wrapper package

Major feature migration from dt-shopify-storefront into the design system:

- Card system: per-card color modes (normal/emphasis/warning/success/other),
  progress bars, full-bleed card body (card-body-flush), image bevel clip-paths,
  and visible structural borders at bevel corners
- Badge system: dt-card-badge with position-aware offsets for cards vs media frames
- Media frames: dt-bevel-media with inner surface fill, image clipping, and
  "MISSING MEDIA" placeholder for empty containers
- Interactive bevel buttons: dt-btn with hover/selected states and pulse animation
- Menu items: dt-menu-item with active states and level indentation
- Animations: scale-in, fade-in, slide-up, pulse, ping, spin keyframes
- Feature legend: CSS grid for product feature icons with state colors
- Scrollbar styling for DT brand
- New @dangerousthings/react package wrapping web CSS components
- New @dangerousthings/tailwind-preset package
- New @dangerousthings/hex-background package
- Desktop showcase rewritten in React with Tailwind CSS
- Mobile showcase updated with new screens (animations, filters, advanced cards)
- Tokens: added mode color tokens, RGB variants, and selected-state tokens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-05 13:26:03 -08:00
parent 47e8085581
commit e74c285b70
109 changed files with 7196 additions and 1026 deletions

View File

@@ -0,0 +1,5 @@
---
"@dangerousthings/hex-background": minor
---
New package: 3D hexagon grid background using Three.js + React Three Fiber. Includes HexGridBackground (full-viewport web component), HexGrid (instanced mesh with animated heights), and HexCamera (orbiting camera). Supports both web and React Native via separate entry points.

View File

@@ -0,0 +1,6 @@
---
"@dangerousthings/react": minor
"@dangerousthings/tokens": patch
---
New package: @dangerousthings/react — React web components wrapping @dangerousthings/web CSS. 22 components matching the React Native package API (DTCard, DTButton, DTLabel, DTChip, DTModal, DTDrawer, DTAccordion, DTMenu, DTStaggerContainer, DTFeatureLegend, DTMobileFilterOverlay, DTGallery, DTHexagon, form primitives). Includes DTWebThemeProvider, animation hooks (useScaleIn, usePulse), and variant utility functions. DTVariant type moved to @dangerousthings/tokens as shared canonical source.

View File

@@ -0,0 +1,5 @@
---
"@dangerousthings/tokens": minor
---
Add mode CSS custom properties (--mode-normal, --mode-emphasis, --mode-warning, --mode-success, --mode-other) with RGB and selected variants for per-component color overrides

View File

@@ -0,0 +1,5 @@
---
"@dangerousthings/react-native": minor
---
Add storefront component features: DTCard selected state and progress bar, DTBadgeOverlay positioning wrapper, DTStaggerContainer with staggered scale-in animation, DTFeatureLegend product feature grid, DTMobileFilterOverlay slide-up filter panel, useScaleIn and usePulse animation hooks. Rewrite DTButton with interactive bevel pattern (outlined rectangle, bevels on hover/select).

View File

@@ -0,0 +1,5 @@
---
"@dangerousthings/web": minor
---
Add storefront component features: animations library (entrance, interactive, stagger container), scrollbar styling, card color modes with selected/hover states, card progress bar, badge positioning, interactive bevel buttons, menu items, filter headers/overlays, feature legend grid. Fix link hover glow to respect per-element --dt-glow-color.

View File

@@ -5,18 +5,18 @@ Shared design tokens, web CSS, and React Native components for the Dangerous Thi
## Packages
| Package | Description |
|---------|-------------|
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| [`@dangerousthings/tokens`](packages/tokens) | Canonical design tokens (colors, typography, shape) defined in TypeScript and exported as CSS custom properties |
| [`@dangerousthings/web`](packages/web) | Web CSS themes — bevels, glows, elevation, and form styles |
| [`@dangerousthings/web`](packages/web) | Web CSS themes — bevels, glows, animations, forms, and filter styles |
| [`@dangerousthings/react-native`](packages/react-native) | React Native components built on React Native Paper |
| [`@dangerousthings/hex-background`](packages/hex-background) | 3D hexagon grid background using Three.js + React Three Fiber (web and React Native) |
## Brand Themes
Three 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)
- **Classic** — dark navy palette with magenta accents and standard border radius
- **Supra** — Material Design 3inspired with blue accent and rounded corners
## Getting Started
@@ -81,7 +81,7 @@ console.log(dt.color.brand.primary); // "#00ffff"
### Scripts
| Command | Description |
|---------|-------------|
| ------------------- | ------------------------------ |
| `npm run build` | Build all packages (Turbo) |
| `npm run clean` | Remove all `dist/` directories |
| `npm run typecheck` | Run TypeScript type checking |
@@ -99,14 +99,18 @@ npm run release # build + publish to npm
## React Native Components
DTAccordion, DTButton, DTCard, DTCheckbox, DTChip, DTDrawer, DTGallery, DTHexagon, DTLabel, DTMediaFrame, DTMenu, DTModal, DTProgressBar, DTQuantityStepper, DTRadioGroup, DTSearchInput, DTSwitch, DTTextInput
DTAccordion, DTBadgeOverlay, DTButton, DTCard, DTCheckbox, DTChip, DTDrawer, DTFeatureLegend, DTGallery, DTHexagon, DTLabel, DTMediaFrame, DTMenu, DTMobileFilterOverlay, DTModal, DTProgressBar, DTQuantityStepper, DTRadioGroup, DTSearchInput, DTStaggerContainer, DTSwitch, DTTextInput
**Animation hooks:** `useScaleIn`, `usePulse`
## Web CSS Modules
- `bevels.css` — angular clip-path patterns for the DT brand
- `glows.css` — neon drop-shadow effects for clipped elements
- `forms-dt.css` — text inputs, checkboxes, radio buttons, switches
- `elevation.css`shadow elevation for Supra / MD3 brand
- `bevels.css` — angular clip-path bevels, card modes, selected states, progress bars, badge overlays, interactive bevel buttons
- `glows.css` — neon drop-shadow and text-shadow effects (mode-aware)
- `forms-dt.css` — text inputs, checkboxes, radio buttons, switches, menu items, filter headers
- `animations.css`entrance animations, stagger containers, transition utilities
- `scrollbar.css` — thin neon scrollbar styling
- `feature-legend.css` — product feature grid with rotated labels
## Project Structure
@@ -116,7 +120,8 @@ dt-design-system/
│ ├── tokens/ # Design token definitions + CSS generation
│ ├── web/ # CSS themes and components
│ ├── react-native/ # React Native Paper components
── showcase/ # Demo app (placeholder)
── hex-background/ # 3D hexagon grid (Three.js)
│ └── showcase/ # Demo apps (desktop + mobile)
├── turbo.json # Turbo pipeline config
└── package.json # Workspace root
```

1591
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@
},
"devDependencies": {
"@changesets/cli": "^2.30.0",
"puppeteer": "^24.38.0",
"turbo": "^2.4.0",
"typescript": "^5.7.0"
},

View File

@@ -0,0 +1,51 @@
{
"name": "@dangerousthings/hex-background",
"version": "0.1.0",
"description": "3D hexagon grid background for the Dangerous Things design system",
"license": "MIT",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./native": {
"types": "./dist/native.d.ts",
"import": "./dist/native.js",
"default": "./dist/native.js"
}
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"react-native": "dist/native.js",
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": ">=18",
"@react-three/fiber": ">=8",
"three": ">=0.150"
},
"peerDependenciesMeta": {
"expo-gl": {
"optional": true
}
},
"devDependencies": {
"@react-three/fiber": "^8.18.0",
"@types/react": "^18.2.0",
"@types/three": "^0.160.0",
"react": "^18.0.0",
"three": "^0.160.0",
"typescript": "^5.7.0"
}
}

View File

@@ -0,0 +1,36 @@
/**
* HexCamera — Orbiting camera for hexagon grid
*
* Continuously rotates around the grid center at a fixed radius and height.
*/
import {useRef} from 'react';
import {useFrame, useThree} from '@react-three/fiber';
export interface HexCameraProps {
/** Orbital rotation speed in radians per second @default 0.02 */
speed?: number;
/** Orbital distance from center @default 8 */
radius?: number;
/** Camera Y height @default 5 */
height?: number;
}
export function HexCamera({
speed = 0.02,
radius = 8,
height = 5,
}: HexCameraProps) {
const angleRef = useRef(0);
const {camera} = useThree();
useFrame((_state, delta) => {
angleRef.current = (angleRef.current + speed * delta) % (Math.PI * 2);
const x = Math.cos(angleRef.current) * radius;
const z = Math.sin(angleRef.current) * radius;
camera.position.set(x, height, z);
camera.lookAt(0, 0, 0);
});
return null;
}

View File

@@ -0,0 +1,148 @@
/**
* HexGrid — Core 3D hexagon grid renderer
*
* Renders an instanced mesh of animated hexagonal columns using Three.js.
* Source: dt-shopify-storefront HexagonGrid.tsx
*/
import {useRef, useMemo, useEffect} from 'react';
import {useFrame} from '@react-three/fiber';
import {
InstancedMesh,
Object3D,
Shape,
ExtrudeGeometry,
MeshStandardMaterial,
MathUtils,
} from 'three';
export interface HexGridProps {
rows: number;
cols: number;
/** Hexagon radius in world units @default 0.5 */
hexRadius?: number;
/** Extrude depth of hex geometry @default 1 */
extrudeHeight?: number;
/** Spacing between hexagons @default 0.05 */
margin?: number;
/** Maximum random height for animation @default 3 */
maxHeight?: number;
/** Interval (ms) between height target changes @default 1500 */
animationInterval?: number;
}
function computeHexPositions(
rows: number,
cols: number,
hexWidth: number,
hexHeight: number,
margin: number,
) {
const positions: {x: number; z: number}[] = [];
const horizontalStep = hexWidth * 0.75 + margin;
const verticalStep = hexHeight * 0.866 + margin;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = col * horizontalStep;
const z = row * verticalStep + (col % 2 ? verticalStep / 2 : 0);
const centerX = ((cols - 1) * horizontalStep) / 2;
const centerZ = ((rows - 1) * verticalStep) / 2;
positions.push({x: x - centerX, z: z - centerZ});
}
}
return positions;
}
export function HexGrid({
rows,
cols,
hexRadius = 0.5,
extrudeHeight = 1,
margin = 0.05,
maxHeight = 3,
animationInterval = 1500,
}: HexGridProps) {
const meshRef = useRef<InstancedMesh>(null);
const positions = useMemo(
() => computeHexPositions(rows, cols, hexRadius * 2, extrudeHeight, margin),
[rows, cols, hexRadius, extrudeHeight, margin],
);
const targetHeightsRef = useRef(
positions.map(() => 1 + Math.random() * maxHeight),
);
const currentHeightsRef = useRef(
positions.map(() => 1 + Math.random() * maxHeight),
);
// Reuse dummy object for matrix updates — 15-20% FPS improvement
const dummyRef = useRef(new Object3D());
const hexGeometry = useMemo(() => {
const shape = new Shape();
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i;
const x = hexRadius * Math.cos(angle);
const z = hexRadius * Math.sin(angle);
i === 0 ? shape.moveTo(x, z) : shape.lineTo(x, z);
}
shape.closePath();
const geometry = new ExtrudeGeometry(shape, {
depth: 1,
bevelEnabled: false,
});
geometry.rotateX(Math.PI / 2);
geometry.computeVertexNormals();
return geometry;
}, [hexRadius]);
// Regenerate target heights at interval
useEffect(() => {
const interval = setInterval(() => {
targetHeightsRef.current = positions.map(
() => 0.5 + Math.random() * maxHeight,
);
}, animationInterval);
return () => clearInterval(interval);
}, [positions, maxHeight, animationInterval]);
// Animate per frame: lerp current heights toward targets
useFrame((_state, delta) => {
if (!meshRef.current) return;
const dummy = dummyRef.current;
positions.forEach(({x, z}, i) => {
currentHeightsRef.current[i] = MathUtils.lerp(
currentHeightsRef.current[i],
targetHeightsRef.current[i],
delta * 0.5,
);
const height = currentHeightsRef.current[i];
dummy.position.set(x, height / 2, z);
dummy.scale.set(1, height, 1);
dummy.rotation.set(0, 0, 0);
dummy.updateMatrix();
meshRef.current!.setMatrixAt(i, dummy.matrix);
});
meshRef.current.instanceMatrix.needsUpdate = true;
});
return (
<instancedMesh
ref={meshRef}
args={[
hexGeometry,
new MeshStandardMaterial({
color: 'black',
metalness: 0.3,
roughness: 0.7,
}),
positions.length,
]}
/>
);
}

View File

@@ -0,0 +1,141 @@
/**
* HexGridBackground — Full-viewport hex grid background
*
* Drop-in component that renders a fixed, semi-transparent 3D hexagon grid
* behind page content. Automatically sizes grid to viewport and detects
* automated browsers to skip rendering.
*/
import {useState, useEffect, useRef, Suspense} from 'react';
import {Canvas} from '@react-three/fiber';
import {HexGrid} from './HexGrid';
import {HexCamera} from './HexCamera';
export interface HexGridBackgroundProps {
/** Canvas opacity @default 0.5 */
opacity?: number;
/** Hexagon radius @default 0.5 */
hexRadius?: number;
/** Spacing between hexagons @default 0.05 */
margin?: number;
/** Maximum animation height @default 3 */
maxHeight?: number;
/** Height target refresh interval in ms @default 1500 */
animationInterval?: number;
/** Camera orbital speed @default 0.02 */
cameraSpeed?: number;
/** Camera orbital radius @default 8 */
cameraRadius?: number;
/** Camera field of view @default 40 */
fov?: number;
}
function calculateGridDimensions(
width: number,
height: number,
cameraDistance: number,
fov: number,
hexRadius: number,
margin: number,
): {rows: number; cols: number} {
const aspect = width / height;
const fovRadians = (fov * Math.PI) / 180;
const visibleHeight = 2 * cameraDistance * Math.tan(fovRadians / 2);
const visibleWidth = visibleHeight * aspect;
const hexWidth = hexRadius * 2;
const horizontalStep = hexWidth * 0.75 + margin;
const verticalStep = hexWidth * 0.866 + margin;
const cols = Math.ceil((visibleWidth / horizontalStep) * 1.5);
const rows = Math.ceil((visibleHeight / verticalStep) * 1.5);
return {
rows: Math.min(Math.max(rows, 15), 35),
cols: Math.min(Math.max(cols, 15), 35),
};
}
export function HexGridBackground({
opacity = 0.5,
hexRadius = 0.5,
margin = 0.05,
maxHeight = 3,
animationInterval = 1500,
cameraSpeed = 0.02,
cameraRadius = 8,
fov = 40,
}: HexGridBackgroundProps) {
const [mounted, setMounted] = useState(false);
const [isAutomated, setIsAutomated] = useState(false);
const [gridDimensions, setGridDimensions] = useState({rows: 20, cols: 20});
const resizeTimeout = useRef<ReturnType<typeof setTimeout>>();
useEffect(() => {
setMounted(true);
// Detect automated browsers (Selenium, Puppeteer, etc.)
if (typeof navigator !== 'undefined' && (navigator as any).webdriver) {
setIsAutomated(true);
return;
}
const updateDimensions = () => {
const dims = calculateGridDimensions(
window.innerWidth,
window.innerHeight,
cameraRadius,
fov,
hexRadius,
margin,
);
setGridDimensions(dims);
};
updateDimensions();
const handleResize = () => {
clearTimeout(resizeTimeout.current);
resizeTimeout.current = setTimeout(updateDimensions, 250);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
clearTimeout(resizeTimeout.current);
};
}, [cameraRadius, fov, hexRadius, margin]);
if (!mounted || isAutomated) return null;
return (
<Canvas
camera={{position: [5, 5, 5], fov, near: 0.5, far: 50}}
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: -1,
pointerEvents: 'none',
opacity,
margin: 0,
}}>
<HexCamera speed={cameraSpeed} radius={cameraRadius} />
<ambientLight intensity={20} />
<directionalLight position={[20, 40, 20]} intensity={1.5} />
<directionalLight position={[-10, 10, -10]} intensity={0.5} />
<Suspense fallback={null}>
<HexGrid
rows={gridDimensions.rows}
cols={gridDimensions.cols}
hexRadius={hexRadius}
margin={margin}
maxHeight={maxHeight}
animationInterval={animationInterval}
/>
</Suspense>
</Canvas>
);
}

View File

@@ -0,0 +1,23 @@
/**
* @dangerousthings/hex-background
*
* 3D hexagon grid background for the Dangerous Things design system.
* Uses Three.js + React Three Fiber for instanced WebGL rendering.
*
* Web usage:
* import { HexGridBackground } from '@dangerousthings/hex-background';
* <HexGridBackground />
*
* React Native usage:
* import { HexGridBackground } from '@dangerousthings/hex-background/native';
* <HexGridBackground />
*/
export { HexGridBackground } from './HexGridBackground';
export type { HexGridBackgroundProps } from './HexGridBackground';
export { HexGrid } from './HexGrid';
export type { HexGridProps } from './HexGrid';
export { HexCamera } from './HexCamera';
export type { HexCameraProps } from './HexCamera';

View File

@@ -0,0 +1,25 @@
/**
* React Native entry point for @dangerousthings/hex-background
*
* Uses @react-three/fiber/native renderer with expo-gl.
* Consumers must install: expo-gl, @react-three/fiber, three
*
* Note: The HexGridBackground component uses web-specific APIs (window, CSS units).
* For React Native, use HexGrid and HexCamera directly within a native Canvas:
*
* import { Canvas } from '@react-three/fiber/native';
* import { HexGrid, HexCamera } from '@dangerousthings/hex-background/native';
*
* <Canvas camera={{ position: [5, 5, 5], fov: 40 }}>
* <HexCamera />
* <ambientLight intensity={20} />
* <directionalLight position={[20, 40, 20]} intensity={1.5} />
* <HexGrid rows={20} cols={20} />
* </Canvas>
*/
export { HexGrid } from './HexGrid';
export type { HexGridProps } from './HexGrid';
export { HexCamera } from './HexCamera';
export type { HexCameraProps } from './HexCamera';

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"jsx": "react-jsx"
},
"include": ["src"]
}

View File

@@ -1,6 +1,6 @@
# @dangerousthings/react-native
React Native components for the Dangerous Things design system — 18 themed components built on React Native Paper with cyberpunk beveled aesthetics.
React Native components for the Dangerous Things design system — 22 themed components built on React Native Paper with cyberpunk beveled aesthetics.
## Install
@@ -74,6 +74,9 @@ Options:
| `DTMediaFrame` | Diagonal beveled frame for images |
| `DTAccordion` | Collapsible sections with accent border |
| `DTHexagon` | Decorative hexagon SVG shape |
| `DTBadgeOverlay` | Absolute-positioned badge wrapper (top-left, top-right, bottom-left, bottom-right) |
| `DTStaggerContainer` | Staggered scale-in entrance animation for child elements |
| `DTFeatureLegend` | Product feature grid with icons and rotated labels |
### Interactive
@@ -83,6 +86,14 @@ Options:
| `DTDrawer` | Side drawer with edge bevels |
| `DTGallery` | Image gallery |
| `DTMenu` | Dropdown menu with item variants |
| `DTMobileFilterOverlay` | Full-screen slide-up filter overlay with backdrop |
### Animation Hooks
| Hook | Description |
|------|-------------|
| `useScaleIn` | Scale 0→1 entrance animation |
| `usePulse` | Looping opacity pulse animation |
## Usage Examples
@@ -99,6 +110,11 @@ import { DTButton, DTCard, DTTextInput } from "@dangerousthings/react-native";
<Text>Chip detected</Text>
</DTCard>
// Card with selected state and progress bar
<DTCard title="PRODUCT" mode="emphasis" selected progress={0.6}>
<Text>Selected with 60% progress</Text>
</DTCard>
// Text input with error state
<DTTextInput
variant="normal"

View File

@@ -0,0 +1,45 @@
/**
* DT Badge Overlay
*
* Positioning wrapper for badges on cards and media frames.
* Places children absolutely at a specified corner with configurable offset.
*/
import {ReactNode} from 'react';
import {View, ViewStyle, StyleProp} from 'react-native';
type BadgePosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
interface DTBadgeOverlayProps {
children: ReactNode;
/**
* Corner position for the badge
* @default 'bottom-right'
*/
position?: BadgePosition;
/**
* Offset from the corner in pixels
* @default 8
*/
offset?: number;
/**
* Additional styles
*/
style?: StyleProp<ViewStyle>;
}
export function DTBadgeOverlay({
children,
position = 'bottom-right',
offset = 8,
style,
}: DTBadgeOverlayProps) {
const positionStyle: ViewStyle = {
position: 'absolute',
zIndex: 4,
...(position.includes('top') ? {top: offset} : {bottom: offset}),
...(position.includes('right') ? {right: offset} : {left: offset}),
};
return <View style={[positionStyle, style]}>{children}</View>;
}

View File

@@ -1,18 +1,20 @@
/**
* DT Button Component
*
* A themed button following the Dangerous Things design language
* with SVG-based beveled corners.
* Interactive bevel button matching the storefront .menu-item-clipped pattern:
* - Default: outlined rectangle, no bevel, colored border with thick top
* - Pressed/hover: bottom-right bevel appears, fills with mode color, text goes dark
* - Selected: bevel persists, fills with 70% opacity mode color
*/
import {StyleSheet, View, ViewStyle, StyleProp, Pressable} from 'react-native';
import {useRef, useEffect} from 'react';
import {StyleSheet, View, ViewStyle, StyleProp, Pressable, Animated} from 'react-native';
import {Text} from 'react-native-paper';
import Svg, {Path} from 'react-native-svg';
import Svg, {Path, Rect} from 'react-native-svg';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
import {buildButtonBevelPath} from '../utils/bevelPaths';
import {buildBeveledRectPath} from '../utils/bevelPaths';
import {useComponentLayout} from '../utils/useComponentLayout';
import {useState} from 'react';
interface DTButtonProps {
/**
@@ -26,11 +28,16 @@ interface DTButtonProps {
variant?: DTVariant;
/**
* Button display mode
* - 'outlined': Border with transparent background (default)
* - 'contained': Filled background
* - 'outlined': Border with transparent background, bevel on press (default)
* - 'contained': Filled background with static bevel
* @default 'outlined'
*/
mode?: 'outlined' | 'contained';
/**
* Whether the button is in a persistent selected state
* (bevel visible, filled with 70% opacity mode color)
*/
selected?: boolean;
/**
* Custom color (overrides variant)
*/
@@ -53,67 +60,134 @@ interface DTButtonProps {
*/
borderWidth?: number;
/**
* Bevel size in pixels
* @default 8
* Bevel size in pixels (bottom-right corner)
* @default 16
*/
bevelSize?: number;
}
/**
* DT-styled Button component with SVG beveled corners
*
* @example
* <DTButton variant="normal" onPress={handleScan}>
* Scan NFC Tag
* </DTButton>
*
* @example
* <DTButton variant="emphasis" mode="contained" onPress={handleAction}>
* View Results
* </DTButton>
*/
export function DTButton({
children,
variant = 'normal',
mode = 'outlined',
selected = false,
color,
onPress,
disabled = false,
style,
borderWidth = 2,
bevelSize = 8,
bevelSize = 16,
}: DTButtonProps) {
const theme = useDTTheme();
const {dimensions, onLayout, hasDimensions} = useComponentLayout();
const [pressed, setPressed] = useState(false);
const pulseAnim = useRef(new Animated.Value(1)).current;
const pressed = useRef(false);
const accentColor = getVariantColor(theme, variant, color);
const selectedColor = accentColor + 'B3'; // ~70% opacity
const isContained = mode === 'contained';
const bgColor = isContained ? accentColor : 'transparent';
const textColor = isContained ? theme.colors.onPrimary : accentColor;
const useBevels = theme.custom.bevelMd > 0;
const {width, height} = dimensions;
const opacity = disabled ? 0.5 : pressed ? 0.7 : 1;
const useBevels = theme.custom.bevelMd > 0;
// Build SVG paths: rectangle (default) and beveled (active)
const rectPath = hasDimensions
? buildBeveledRectPath(width, height, {corners: {}, strokeWidth: borderWidth})
: '';
const bevelPath = useBevels && hasDimensions
? buildBeveledRectPath(width, height, {
corners: {bottomRight: bevelSize},
strokeWidth: borderWidth,
})
: '';
// Pulse animation for pressed/selected hover
useEffect(() => {
if (selected) {
// No pulse when just selected (matches storefront: animation: none on .selected)
pulseAnim.setValue(1);
}
return () => {
pulseAnim.stopAnimation();
};
}, [selected, pulseAnim]);
const startPulse = () => {
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 0.5,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
]),
).start();
};
const stopPulse = () => {
pulseAnim.stopAnimation();
pulseAnim.setValue(1);
};
const handlePressIn = () => {
pressed.current = true;
startPulse();
};
const handlePressOut = () => {
pressed.current = false;
if (!selected) {
stopPulse();
}
};
// Determine visual state
const showBevel = isContained || selected;
const getBgColor = (isPressed: boolean) => {
if (isContained) return accentColor;
if (selected) return selectedColor;
if (isPressed) return accentColor;
return 'transparent';
};
const getTextColor = (isPressed: boolean) => {
if (isContained || selected || isPressed) return theme.colors.onPrimary;
return accentColor;
};
return (
<Pressable
onPress={onPress}
disabled={disabled}
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
style={[{opacity}, style]}>
onPressIn={handlePressIn}
onPressOut={handlePressOut}
style={[{opacity: disabled ? 0.5 : 1}, style]}>
{({pressed: isPressedState}) => (
<Animated.View
style={[styles.container, {opacity: pulseAnim}]}
onLayout={onLayout}>
{/* Non-beveled mode (classic brand) */}
{!useBevels && (
<View
style={[
styles.container,
!useBevels && {
StyleSheet.absoluteFill,
{
borderWidth,
borderColor: accentColor,
borderTopWidth: 5,
borderRadius: theme.custom.radiusSm,
backgroundColor: bgColor,
backgroundColor: getBgColor(isPressedState),
},
]}
onLayout={onLayout}>
/>
)}
{/* Beveled mode (DT brand) */}
{useBevels && hasDimensions && (
<Svg
style={StyleSheet.absoluteFill}
@@ -121,23 +195,34 @@ export function DTButton({
height={height}
viewBox={`0 0 ${width} ${height}`}>
<Path
d={buildButtonBevelPath(width, height, bevelSize, borderWidth)}
fill={bgColor}
d={(isPressedState || showBevel) ? bevelPath : rectPath}
fill={getBgColor(isPressedState)}
stroke={accentColor}
strokeWidth={borderWidth}
/>
{/* Thick top accent — rendered inside SVG for exact alignment */}
<Rect
x={borderWidth / 2}
y={0}
width={width - borderWidth}
height={5}
fill={accentColor}
/>
</Svg>
)}
<View style={styles.content}>
{typeof children === 'string' ? (
<Text variant="labelLarge" style={[styles.label, {color: textColor}]}>
<Text
variant="labelLarge"
style={[styles.label, {color: getTextColor(isPressedState)}]}>
{children}
</Text>
) : (
children
)}
</View>
</View>
</Animated.View>
)}
</Pressable>
);
}

View File

@@ -8,6 +8,10 @@
* - Bottom-right bevel: 2em (~32px)
* - Bottom-left bevel: 1em (~16px)
* - Border width: 0.2em (~3px)
*
* The progress bar is a structural element on the left edge (0 to bevelSizeSmall).
* It is always present. At 0 progress it shows surface color (empty bar).
* As progress increases, accent color fills from the bottom up.
*/
import {ReactNode} from 'react';
@@ -39,6 +43,12 @@ interface DTCardProps {
* @default true when title is provided
*/
showHeader?: boolean;
/**
* Progress value (01) for left-edge vertical progress bar.
* Always present. At 0 the bar shows surface color (empty).
* @default 0
*/
progress?: number;
/**
* Additional styles for the card container
*/
@@ -77,6 +87,114 @@ interface DTCardProps {
onPress?: () => void;
}
/**
* Build inner card path with left edge at bevelSizeSmall (for progress bar zone).
* 5-point polygon — no bottom-left bevel, straight vertical left edge.
*/
function buildInnerCardPath(
w: number,
h: number,
bevelBR: number,
bw: number,
bevelSizeSmall: number,
): string {
const right = w - bw;
const top = bw;
const bottom = h - bw;
const br = Math.min(
bevelBR - bw,
(right - bevelSizeSmall) / 3,
(bottom - top) / 3,
);
if (right <= bevelSizeSmall || bottom <= top) return '';
return [
`M ${bevelSizeSmall} ${top}`,
`L ${right} ${top}`,
`L ${right} ${bottom - br}`,
`L ${right - br} ${bottom}`,
`L ${bevelSizeSmall} ${bottom}`,
'Z',
].join(' ');
}
/**
* Build the full progress bar fill area path (inset 3px from frame on all sides).
* This is the region where the surface/accent gradient fills.
*/
function buildProgressAreaPath(
cardH: number,
bw: number,
bevelSizeSmall: number,
): string {
const left = bw;
const right = bevelSizeSmall - bw;
const top = bw;
const bevelStartY = cardH - bevelSizeSmall;
const bottomRight = cardH - bw * 2; // y at bottom-right of fill area (on bevel diagonal)
if (right <= left || bevelStartY <= top) return '';
return [
`M ${left} ${top}`,
`L ${right} ${top}`,
`L ${right} ${bottomRight}`,
`L ${left} ${bevelStartY}`,
'Z',
].join(' ');
}
/**
* Build the accent-colored portion of the progress bar (fills from bottom).
* Returns empty string when progress is 0.
*/
function buildProgressFillPath(
cardH: number,
bw: number,
bevelSizeSmall: number,
progress: number,
): string {
const p = Math.max(0, Math.min(1, progress));
if (p === 0) return '';
const left = bw;
const right = bevelSizeSmall - bw;
const top = bw;
const bevelStartY = cardH - bevelSizeSmall;
const bottomRight = cardH - bw * 2;
if (right <= left || bevelStartY <= top) return '';
// Total fill area height (along the left edge, from top to bevelStartY)
const areaH = bevelStartY - top;
// How much of the area is filled from the bottom
const fillTop = top + areaH * (1 - p);
if (fillTop >= bevelStartY) {
// Fill is entirely within the bevel zone
// The bevel diagonal goes from (left, bevelStartY) to (right, bottomRight)
// At y=fillTop, x on the diagonal: x = left + (fillTop - bevelStartY) * (right - left) / (bottomRight - bevelStartY)
const bevelH = bottomRight - bevelStartY;
if (bevelH <= 0) return '';
const xAtFillTop = left + ((fillTop - bevelStartY) / bevelH) * (right - left);
if (xAtFillTop >= right) return '';
return [
`M ${xAtFillTop} ${fillTop}`,
`L ${right} ${fillTop}`,
`L ${right} ${bottomRight}`,
'Z',
].join(' ');
}
// Fill extends above the bevel zone — rectangle + bevel triangle
return [
`M ${left} ${fillTop}`,
`L ${right} ${fillTop}`,
`L ${right} ${bottomRight}`,
`L ${left} ${bevelStartY}`,
'Z',
].join(' ');
}
/**
* DT-styled Card component with SVG beveled corners
*
@@ -96,6 +214,7 @@ export function DTCard({
borderColor,
title,
showHeader,
progress = 0,
style,
contentStyle,
padding = 16,
@@ -118,9 +237,21 @@ export function DTCard({
const outerPath = useBevels && hasDimensions
? buildCardBevelPath(width, height, bevelSize, bevelSizeSmall, 0)
: '';
// Inner bevel path at border inset (for background + frame cutout)
// Inner bevel path — left edge at bevelSizeSmall (progress bar zone)
const innerPath = useBevels && hasDimensions
? buildCardBevelPath(width, height, bevelSize - borderWidth, bevelSizeSmall - borderWidth, borderWidth)
? buildInnerCardPath(width, height, bevelSize, borderWidth, bevelSizeSmall)
: '';
// Progress bar paths — computed once, reused in progress SVG and frame overlay
const progressAreaPath = useBevels && hasDimensions
? buildProgressAreaPath(height, borderWidth, bevelSizeSmall)
: '';
const progressFillPath = useBevels && hasDimensions && progress > 0
? buildProgressFillPath(height, borderWidth, bevelSizeSmall, progress)
: '';
// Frame overlay: outer + inner + progressArea hole (evenodd punches window for progress bar)
const framePath = useBevels && hasDimensions
? outerPath + ' ' + innerPath + (progressAreaPath ? ' ' + progressAreaPath : '')
: '';
const content = (
@@ -147,7 +278,54 @@ export function DTCard({
<Path d={innerPath} fill={bgColor} />
</Svg>
)}
<View style={styles.innerContainer}>
{/* Progress bar fill area — beveled mode */}
{useBevels && hasDimensions && (
<Svg
style={[StyleSheet.absoluteFill, {zIndex: 1}]}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
pointerEvents="none">
{/* Accent base — visible through the semi-transparent surface above.
Matches web where ::after sits over the accent-colored card bg. */}
<Path d={progressAreaPath} fill={accentColor} />
{/* Surface fill over accent base — 0.6 opacity lets accent bleed through.
Matches web --dt-progress-empty-opacity default. */}
<Path
d={progressAreaPath}
fill={bgColor}
opacity={0.6}
/>
{/* Accent fill from bottom (progressed portion) */}
{progressFillPath !== '' && (
<Path
d={progressFillPath}
fill={accentColor}
/>
)}
</Svg>
)}
{/* Progress bar — non-beveled (classic) mode */}
{!useBevels && (
<View style={[styles.progressBar, {
width: bevelSizeSmall,
left: borderWidth,
top: borderWidth,
bottom: borderWidth,
borderBottomLeftRadius: theme.custom.radius > 0 ? Math.max(0, theme.custom.radius - borderWidth) : 0,
}]}>
<View
style={[
styles.progressFill,
{
backgroundColor: accentColor,
height: `${Math.round(Math.max(0, Math.min(1, progress)) * 100)}%`,
},
]}
/>
</View>
)}
<View style={[styles.innerContainer, useBevels && {paddingLeft: bevelSizeSmall - borderWidth}]}>
{shouldShowHeader && (
<View style={[styles.header, {backgroundColor: accentColor}]}>
{title && (
@@ -161,7 +339,10 @@ export function DTCard({
)}
<View style={[styles.content, {padding}, contentStyle]}>{children}</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.
The progress area path punches a hole in the frame so the
progress bar SVG underneath (zIndex:1) shows through. */}
{useBevels && hasDimensions && (
<Svg
style={[StyleSheet.absoluteFill, styles.frameOverlay]}
@@ -170,7 +351,7 @@ export function DTCard({
viewBox={`0 0 ${width} ${height}`}
pointerEvents="none">
<Path
d={outerPath + ' ' + innerPath}
d={framePath}
fillRule="evenodd"
fill={accentColor}
/>
@@ -221,7 +402,8 @@ const styles = StyleSheet.create({
overflow: 'hidden',
},
header: {
paddingHorizontal: 16,
paddingLeft: 8,
paddingRight: 16,
paddingVertical: 12,
},
headerText: {
@@ -232,4 +414,17 @@ const styles = StyleSheet.create({
frameOverlay: {
zIndex: 2,
},
progressBar: {
position: 'absolute' as const,
left: 0,
top: 0,
bottom: 0,
backgroundColor: 'transparent',
zIndex: 3,
justifyContent: 'flex-end' as const,
overflow: 'hidden' as const,
},
progressFill: {
width: '100%' as const,
},
});

View File

@@ -61,6 +61,7 @@ export function DTChip({
<Chip
{...props}
mode="outlined"
showSelectedCheck={false}
textStyle={[
styles.text,
{ color: selected ? theme.colors.onPrimary : color },

View File

@@ -0,0 +1,136 @@
/**
* DT Feature Legend
*
* Displays product features in a grid with icons and rotated labels.
* Source: dt-shopify-storefront UseCaseLegend component.
*/
import {ReactNode} from 'react';
import {StyleSheet, View, ViewStyle, StyleProp} from 'react-native';
import {Text} from 'react-native-paper';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
export interface DTFeatureItem {
key: string;
name: string;
icon: ReactNode;
state: 'supported' | 'disabled' | 'unsupported';
}
const stateToVariant: Record<DTFeatureItem['state'], DTVariant> = {
supported: 'normal',
disabled: 'emphasis',
unsupported: 'warning',
};
interface DTFeatureLegendProps {
features: DTFeatureItem[];
/**
* Header title text
*/
title?: string;
/**
* Color variant for the header bar
* @default 'normal'
*/
variant?: DTVariant;
/**
* Number of columns in the grid
* @default 5
*/
columns?: number;
/**
* Icon size in pixels
* @default 42
*/
iconSize?: number;
style?: StyleProp<ViewStyle>;
}
export function DTFeatureLegend({
features,
title,
variant = 'normal',
columns = 5,
iconSize = 42,
style,
}: DTFeatureLegendProps) {
const theme = useDTTheme();
const headerColor = getVariantColor(theme, variant);
const itemWidth = `${100 / columns}%` as const;
return (
<View style={[styles.container, style]}>
{title && (
<View style={[styles.header, {backgroundColor: headerColor}]}>
<Text
variant="labelLarge"
style={[styles.headerText, {color: theme.colors.onPrimary}]}>
{title}
</Text>
</View>
)}
<View style={styles.grid}>
{features.map(feature => {
const featureColor = getVariantColor(
theme,
stateToVariant[feature.state],
);
return (
<View
key={feature.key}
style={[styles.item, {width: itemWidth as unknown as number}]}>
<View style={[styles.iconContainer, {width: iconSize, height: iconSize}]}>
{feature.icon}
</View>
<View style={styles.labelContainer}>
<Text
style={[styles.label, {color: featureColor}]}>
{feature.name}
</Text>
</View>
</View>
);
})}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
overflow: 'hidden',
},
header: {
paddingHorizontal: 16,
paddingVertical: 8,
},
headerText: {
fontWeight: '700',
letterSpacing: 0.5,
textTransform: 'uppercase',
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
},
item: {
alignItems: 'center',
paddingVertical: 8,
paddingHorizontal: 4,
gap: 4,
},
iconContainer: {
alignItems: 'center',
justifyContent: 'center',
},
labelContainer: {
paddingTop: 4,
},
label: {
fontSize: 11,
fontWeight: '600',
textAlign: 'center',
},
});

View File

@@ -0,0 +1,230 @@
/**
* DT Mobile Filter Overlay
*
* Full-screen slide-up overlay for mobile filter menus.
* Source: dt-shopify-storefront MobileFilterMenu component.
*/
import {ReactNode, useRef, useEffect} from 'react';
import {
StyleSheet,
View,
ViewStyle,
StyleProp,
Pressable,
Animated,
Dimensions,
BackHandler,
} from 'react-native';
import {Text, Portal} from 'react-native-paper';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
interface DTMobileFilterOverlayProps {
visible: boolean;
onDismiss: () => void;
/**
* Header title
*/
heading?: string;
/**
* Number of active filters (shown as badge in header)
*/
activeFilterCount?: number;
/**
* Callback to clear all filters
*/
onClearAll?: () => void;
/**
* Color variant for the header
* @default 'normal'
*/
variant?: DTVariant;
children: ReactNode;
style?: StyleProp<ViewStyle>;
}
export function DTMobileFilterOverlay({
visible,
onDismiss,
heading = 'Filters',
activeFilterCount,
onClearAll,
variant = 'normal',
children,
style,
}: DTMobileFilterOverlayProps) {
const theme = useDTTheme();
const accentColor = getVariantColor(theme, variant);
const slideAnim = useRef(new Animated.Value(0)).current;
const backdropAnim = useRef(new Animated.Value(0)).current;
const screenHeight = Dimensions.get('window').height;
useEffect(() => {
if (visible) {
Animated.parallel([
Animated.timing(backdropAnim, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
]).start();
} else {
Animated.parallel([
Animated.timing(backdropAnim, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}),
]).start();
}
}, [visible, backdropAnim, slideAnim]);
// Android back button
useEffect(() => {
if (!visible) return;
const handler = BackHandler.addEventListener('hardwareBackPress', () => {
onDismiss();
return true;
});
return () => handler.remove();
}, [visible, onDismiss]);
if (!visible) return null;
const translateY = slideAnim.interpolate({
inputRange: [0, 1],
outputRange: [screenHeight, 0],
});
return (
<Portal>
<View style={styles.overlay}>
{/* Backdrop */}
<Animated.View style={[styles.backdrop, {opacity: backdropAnim}]}>
<Pressable style={StyleSheet.absoluteFill} onPress={onDismiss} />
</Animated.View>
{/* Content */}
<Animated.View
style={[
styles.content,
{
backgroundColor: theme.colors.background,
transform: [{translateY}],
maxHeight: screenHeight * 0.85,
},
style,
]}>
{/* Header */}
<View style={[styles.header, {backgroundColor: accentColor}]}>
<View style={styles.headerLeft}>
<Text
variant="titleMedium"
style={[styles.headerText, {color: theme.colors.onPrimary}]}>
{heading}
</Text>
{activeFilterCount !== undefined && activeFilterCount > 0 && (
<View style={[styles.countBadge, {backgroundColor: theme.colors.onPrimary}]}>
<Text style={[styles.countText, {color: accentColor}]}>
{activeFilterCount}
</Text>
</View>
)}
</View>
<Pressable onPress={onDismiss} hitSlop={8}>
<Text style={[styles.closeText, {color: theme.colors.onPrimary}]}>
CLOSE
</Text>
</Pressable>
</View>
{/* Clear all button */}
{onClearAll && activeFilterCount !== undefined && activeFilterCount > 0 && (
<Pressable
onPress={onClearAll}
style={[styles.clearAll, {borderBottomColor: accentColor}]}>
<Text style={[styles.clearAllText, {color: accentColor}]}>
Clear All Filters
</Text>
</Pressable>
)}
{/* Filter content */}
<View style={styles.body}>{children}</View>
</Animated.View>
</View>
</Portal>
);
}
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
zIndex: 1000,
justifyContent: 'flex-end',
},
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
content: {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
overflow: 'hidden',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
},
headerLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
headerText: {
fontWeight: '700',
letterSpacing: 0.5,
},
countBadge: {
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 10,
minWidth: 24,
alignItems: 'center',
},
countText: {
fontSize: 12,
fontWeight: '700',
},
closeText: {
fontSize: 14,
fontWeight: '600',
letterSpacing: 1,
},
clearAll: {
paddingHorizontal: 16,
paddingVertical: 10,
borderBottomWidth: 1,
},
clearAllText: {
fontSize: 14,
fontWeight: '600',
},
body: {
flexGrow: 1,
},
});

View File

@@ -0,0 +1,66 @@
/**
* DT Stagger Container
*
* Animates children with staggered scale-in entrances.
* Source: dt-shopify-storefront framer-motion staggered card entrance pattern.
*/
import {ReactNode, Children, useRef, useEffect} from 'react';
import {Animated, ViewStyle, StyleProp} from 'react-native';
interface DTStaggerContainerProps {
children: ReactNode;
/**
* Duration of each child's scale-in animation in ms
* @default 330
*/
duration?: number;
/**
* Delay between each child's animation start in ms
* @default 75
*/
interval?: number;
/**
* Additional styles for the container
*/
style?: StyleProp<ViewStyle>;
}
export function DTStaggerContainer({
children,
duration = 330,
interval = 75,
style,
}: DTStaggerContainerProps) {
const childArray = Children.toArray(children);
const animations = useRef(childArray.map(() => new Animated.Value(0))).current;
useEffect(() => {
// Reset animations if child count changes
while (animations.length < childArray.length) {
animations.push(new Animated.Value(0));
}
const staggered = childArray.map((_, i) =>
Animated.timing(animations[i], {
toValue: 1,
duration,
delay: i * interval,
useNativeDriver: true,
}),
);
Animated.parallel(staggered).start();
}, [childArray.length, duration, interval]);
return (
<Animated.View style={style}>
{childArray.map((child, i) => (
<Animated.View
key={i}
style={{transform: [{scale: animations[i] ?? new Animated.Value(1)}]}}>
{child}
</Animated.View>
))}
</Animated.View>
);
}

View File

@@ -30,6 +30,7 @@ export type { DTVariant } from './utils/variantColors';
export { getVariantColor } from './utils/variantColors';
export { buildBeveledRectPath } from './utils/bevelPaths';
export { useComponentLayout } from './utils/useComponentLayout';
export { useScaleIn, usePulse } from './utils/animations';
// Component exports — existing
export { DTCard, DTCardClipPath } from './components/DTCard';
@@ -50,6 +51,10 @@ export { DTMediaFrame } from './components/DTMediaFrame';
export { DTAccordion } from './components/DTAccordion';
export type { DTAccordionSection } from './components/DTAccordion';
// Component exports — positioning & animation containers
export { DTBadgeOverlay } from './components/DTBadgeOverlay';
export { DTStaggerContainer } from './components/DTStaggerContainer';
// Component exports — complex interactive
export { DTModal } from './components/DTModal';
export { DTDrawer } from './components/DTDrawer';
@@ -57,6 +62,11 @@ export { DTGallery } from './components/DTGallery';
export type { DTGalleryItem } from './components/DTGallery';
export { DTSearchInput } from './components/DTSearchInput';
// Component exports — filter & feature
export { DTFeatureLegend } from './components/DTFeatureLegend';
export type { DTFeatureItem } from './components/DTFeatureLegend';
export { DTMobileFilterOverlay } from './components/DTMobileFilterOverlay';
// Component exports — navigation & decorative
export { DTMenu, DTMenuDropdown } from './components/DTMenu';
export type { DTMenuItem } from './components/DTMenu';

View File

@@ -0,0 +1,73 @@
/**
* Reusable animation hooks for DT components
*
* Provides common animation patterns (scale-in, pulse) as hooks
* that consumers and new components can use consistently.
*/
import {useRef, useEffect} from 'react';
import {Animated} from 'react-native';
/**
* Scale-in entrance animation.
* Returns an Animated.Value (0→1) to use as a scale transform.
*/
export function useScaleIn(options?: {
enabled?: boolean;
duration?: number;
delay?: number;
}): Animated.Value {
const {enabled = true, duration = 330, delay = 0} = options ?? {};
const anim = useRef(new Animated.Value(enabled ? 0 : 1)).current;
useEffect(() => {
if (enabled) {
anim.setValue(0);
Animated.timing(anim, {
toValue: 1,
duration,
delay,
useNativeDriver: true,
}).start();
} else {
anim.setValue(1);
}
}, [enabled, duration, delay, anim]);
return anim;
}
/**
* Pulse (ping) opacity animation loop.
* Returns an Animated.Value that oscillates between 1 and 0.3.
*/
export function usePulse(enabled = true): Animated.Value {
const anim = useRef(new Animated.Value(1)).current;
useEffect(() => {
if (enabled) {
const loop = Animated.loop(
Animated.sequence([
Animated.timing(anim, {
toValue: 0.3,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(anim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
]),
);
loop.start();
return () => {
loop.stop();
anim.setValue(1);
};
}
anim.setValue(1);
}, [enabled, anim]);
return anim;
}

View File

@@ -6,8 +6,10 @@
*/
import type {DTExtendedTheme} from '../theme/paperTheme';
import type {DTVariant} from '@dangerousthings/tokens';
export type DTVariant = 'normal' | 'emphasis' | 'warning' | 'success' | 'other';
// Re-export DTVariant from the canonical source (tokens package)
export type {DTVariant} from '@dangerousthings/tokens';
/** String-typed keys in DTExtendedTheme['custom'] that map to mode colors */
type ModeColorKey = 'modeNormal' | 'modeEmphasis' | 'modeWarning' | 'modeSuccess' | 'modeOther';

View File

@@ -0,0 +1,42 @@
{
"name": "@dangerousthings/react",
"version": "0.1.0",
"description": "React web components for the Dangerous Things design system",
"license": "MIT",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"@dangerousthings/tokens": "*",
"@dangerousthings/web": "*"
},
"devDependencies": {
"@dangerousthings/tokens": "*",
"@dangerousthings/web": "*",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"typescript": "^5.8.0"
}
}

View File

@@ -0,0 +1,83 @@
/**
* DTAccordion — Expandable sections with animated height and chevron.
*
* CSS reference: forms-dt.css .dt-accordion, .dt-accordion-header,
* .dt-accordion-chevron, .dt-accordion-content
*/
import { useState, useCallback, type ReactNode, type CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
export interface DTAccordionSection {
key: string;
title: string;
children: ReactNode;
}
interface DTAccordionProps {
sections: DTAccordionSection[];
/** Header variant when closed @default 'normal' */
variant?: DTVariant;
/** Header variant when open @default 'emphasis' */
activeVariant?: DTVariant;
/** Allow multiple sections open @default false */
allowMultiple?: boolean;
initialOpenKeys?: string[];
onSectionToggle?: (key: string, isOpen: boolean) => void;
className?: string;
style?: CSSProperties;
}
export function DTAccordion({
sections,
allowMultiple = false,
initialOpenKeys,
onSectionToggle,
className,
style,
}: DTAccordionProps) {
const [openKeys, setOpenKeys] = useState<Set<string>>(
new Set(initialOpenKeys),
);
const toggle = useCallback(
(key: string) => {
setOpenKeys(prev => {
const next = new Set(allowMultiple ? prev : []);
const isOpening = !prev.has(key);
if (isOpening) next.add(key);
else next.delete(key);
onSectionToggle?.(key, isOpening);
return next;
});
},
[allowMultiple, onSectionToggle],
);
return (
<div className={cx('dt-accordion', className)} style={style}>
{sections.map(section => {
const isOpen = openKeys.has(section.key);
return (
<div key={section.key}>
<button
className="dt-accordion-header"
aria-expanded={isOpen}
onClick={() => toggle(section.key)}
type="button"
style={{ width: '100%' }}>
<span>{section.title}</span>
<span className="dt-accordion-chevron" />
</button>
<div
className="dt-accordion-content"
data-open={isOpen}>
{section.children}
</div>
</div>
);
})}
</div>
);
}

View File

@@ -0,0 +1,50 @@
/**
* DTBadgeOverlay — Absolute-positioned badge container.
*
* CSS reference: bevels.css .dt-badge-bottom-right, .dt-badge-top-right, etc.
*/
import type { ReactNode, CSSProperties } from 'react';
import { cx } from '../utils/cx';
type BadgePosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
interface DTBadgeOverlayProps {
children: ReactNode;
/** Badge position @default 'bottom-right' */
position?: BadgePosition;
/** Offset from corner in px */
offset?: number;
className?: string;
style?: CSSProperties;
}
const positionClassMap: Record<BadgePosition, string> = {
'top-left': 'dt-badge-top-left',
'top-right': 'dt-badge-top-right',
'bottom-left': 'dt-badge-overlay',
'bottom-right': 'dt-badge-bottom-right',
};
export function DTBadgeOverlay({
children,
position = 'bottom-right',
offset,
className,
style,
}: DTBadgeOverlayProps) {
const offsetStyle: CSSProperties = offset
? {
...(position.includes('top') ? { top: `${offset}px` } : { bottom: `${offset}px` }),
...(position.includes('left') ? { left: `${offset}px` } : { right: `${offset}px` }),
}
: {};
return (
<div
className={cx(positionClassMap[position], className)}
style={{ ...offsetStyle, ...style }}>
{children}
</div>
);
}

View File

@@ -0,0 +1,66 @@
/**
* DTButton — Interactive bevel button with mode colors.
*
* CSS reference: bevels.css .dt-btn, .btn-primary, .mode-*, .selected
*/
import type { ReactNode, CSSProperties, MouseEvent } from 'react';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses';
interface DTButtonProps {
children: ReactNode;
/** Color variant @default 'normal' */
variant?: DTVariant;
/** Display mode @default 'outlined' */
mode?: 'outlined' | 'contained';
/** Persistent selected state (bevel + 70% fill) */
selected?: boolean;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
className?: string;
style?: CSSProperties;
}
export function DTButton({
children,
variant = 'normal',
mode = 'outlined',
selected = false,
onClick,
disabled = false,
type = 'button',
className,
style,
}: DTButtonProps) {
if (mode === 'contained') {
return (
<button
className={cx('btn-primary', getVariantClass(variant), className)}
onClick={onClick}
disabled={disabled}
type={type}
style={style}>
{children}
</button>
);
}
return (
<button
className={cx(
'dt-btn',
getVariantClass(variant),
selected && 'selected',
className,
)}
onClick={onClick}
disabled={disabled}
type={type}
style={style}>
{children}
</button>
);
}

View File

@@ -0,0 +1,66 @@
/**
* DTCard — Beveled card with header, progress bar, and mode colors.
*
* CSS reference: bevels.css .card / .dt-bevel-card, .card-title,
* ::after progress bar, .mode-*
*
* 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.
*/
import type { ReactNode, CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses';
interface DTCardProps {
children: ReactNode;
/** Color variant @default 'normal' */
variant?: DTVariant;
/** Card title (displayed in header bar) */
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 */
progress?: number;
className?: string;
style?: CSSProperties;
onClick?: () => void;
}
export function DTCard({
children,
variant = 'normal',
title,
showHeader,
progress = 0,
className,
style,
onClick,
}: DTCardProps) {
const shouldShowHeader = showHeader ?? !!title;
const Tag = onClick ? 'button' : 'div';
return (
<Tag
className={cx(
'card',
getVariantClass(variant),
className,
)}
style={{
...style,
'--dt-card-progress': progress,
} as CSSProperties}
onClick={onClick}
type={onClick ? 'button' : undefined}>
{shouldShowHeader && (
<div className="card-title">
{title}
</div>
)}
{children}
</Tag>
);
}

View File

@@ -0,0 +1,46 @@
/**
* DTCheckbox — Beveled diamond checkbox.
*
* CSS reference: forms-dt.css input[type="checkbox"]
*/
import type { CSSProperties } from 'react';
import { cx } from '../utils/cx';
interface DTCheckboxProps {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
label?: string;
className?: string;
style?: CSSProperties;
}
export function DTCheckbox({
checked,
onChange,
disabled = false,
label,
className,
style,
}: DTCheckboxProps) {
return (
<label
className={cx('dt-checkbox-wrapper', className)}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '12px',
cursor: disabled ? 'not-allowed' : 'pointer',
...style,
}}>
<input
type="checkbox"
checked={checked}
onChange={e => onChange(e.target.checked)}
disabled={disabled}
/>
{label && <span>{label}</span>}
</label>
);
}

View File

@@ -0,0 +1,47 @@
/**
* DTChip — Small badge/chip with variant-based status colors.
*
* CSS reference: bevels.css .badge, .badge-success, .badge-error, etc.
*/
import type { ReactNode, CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
import { getVariantClass, variantToBadgeClass } from '../utils/variantClasses';
interface DTChipProps {
children: ReactNode;
/** Color variant @default 'normal' */
variant?: DTVariant;
/** Use mode-colored fill instead of status border color */
filled?: boolean;
onClick?: () => void;
className?: string;
style?: CSSProperties;
}
export function DTChip({
children,
variant = 'normal',
filled = false,
onClick,
className,
style,
}: DTChipProps) {
const Tag = onClick ? 'button' : 'span';
return (
<Tag
className={cx(
'badge',
filled ? 'badge-mode' : variantToBadgeClass(variant),
getVariantClass(variant),
className,
)}
onClick={onClick}
style={style}
type={onClick ? 'button' : undefined}>
{children}
</Tag>
);
}

View File

@@ -0,0 +1,144 @@
/**
* DTDrawer — Sliding side panel with beveled edges.
*
* CSS reference: bevels.css .dt-bevel-drawer-right, .dt-bevel-drawer-left
*/
import { useEffect, useCallback, type ReactNode, type CSSProperties } from 'react';
import { createPortal } from 'react-dom';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses';
interface DTDrawerProps {
visible: boolean;
onDismiss: () => void;
heading: string;
/** Color variant for heading bar @default 'emphasis' */
headingVariant?: DTVariant;
/** Slide direction @default 'right' */
position?: 'right' | 'left';
/** Panel width @default '400px' */
width?: string | number;
children: ReactNode;
className?: string;
style?: CSSProperties;
}
export function DTDrawer({
visible,
onDismiss,
heading,
headingVariant = 'emphasis',
position = 'right',
width = 400,
children,
className,
style,
}: DTDrawerProps) {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') onDismiss();
},
[onDismiss],
);
useEffect(() => {
if (visible) {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}
}, [visible, handleKeyDown]);
if (!visible) return null;
const bevelClass = position === 'right' ? 'dt-bevel-drawer-right' : 'dt-bevel-drawer-left';
const widthValue = typeof width === 'number' ? `${width}px` : width;
return createPortal(
<div
style={{
position: 'fixed',
inset: 0,
zIndex: 1000,
}}>
{/* Backdrop */}
<div
style={{
position: 'absolute',
inset: 0,
background: 'rgba(0, 0, 0, 0.5)',
backdropFilter: 'blur(4px)',
animation: 'dt-fade-in 0.2s ease-in-out both',
}}
onClick={onDismiss}
/>
{/* Panel */}
<div
className={cx(
bevelClass,
getVariantClass(headingVariant),
className,
)}
style={{
position: 'absolute',
top: 0,
bottom: 0,
[position]: 0,
width: widthValue,
maxWidth: '100vw',
background: 'var(--color-bg)',
display: 'flex',
flexDirection: 'column',
animation: `dt-slide-${position === 'right' ? 'up' : 'up'} 0.2s ease-in-out both`,
...style,
}}>
{/* Header */}
<div
style={{
padding: '16px',
background: `var(${getVariantCSSVar(headingVariant)}, var(--color-primary))`,
color: 'var(--color-bg)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontWeight: 700,
fontSize: '18px',
letterSpacing: '0.5px',
}}>
<span>{heading}</span>
<button
onClick={onDismiss}
style={{
background: 'none',
border: 'none',
color: 'inherit',
fontSize: '20px',
fontWeight: 700,
cursor: 'pointer',
padding: '0 8px',
}}
type="button">
</button>
</div>
{/* Content */}
<div style={{ flex: 1, overflow: 'auto', padding: '16px' }}>
{children}
</div>
</div>
</div>,
document.body,
);
}
function getVariantCSSVar(variant: DTVariant): string {
const map: Record<DTVariant, string> = {
normal: '--mode-normal',
emphasis: '--mode-emphasis',
warning: '--mode-warning',
success: '--mode-success',
other: '--mode-other',
};
return map[variant];
}

View File

@@ -0,0 +1,77 @@
/**
* DTFeatureLegend — Grid of product feature icons with rotated labels.
*
* CSS reference: feature-legend.css .dt-feature-legend, .dt-feature-legend-header,
* .dt-feature-legend-grid, .dt-feature-legend-item, .dt-feature-supported, etc.
*/
import type { ReactNode, CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
import { getVariantClass, featureStateToVariant } from '../utils/variantClasses';
export interface DTFeatureItem {
key: string;
name: string;
icon: ReactNode;
state: 'supported' | 'disabled' | 'unsupported';
}
const stateClassMap: Record<DTFeatureItem['state'], string> = {
supported: 'dt-feature-supported',
disabled: 'dt-feature-disabled',
unsupported: 'dt-feature-unsupported',
};
interface DTFeatureLegendProps {
features: DTFeatureItem[];
title?: string;
/** Header variant @default 'normal' */
variant?: DTVariant;
/** Grid columns @default 5 */
columns?: number;
/** Icon size in px @default 42 */
iconSize?: number;
className?: string;
style?: CSSProperties;
}
export function DTFeatureLegend({
features,
title,
variant = 'normal',
columns = 5,
iconSize = 42,
className,
style,
}: DTFeatureLegendProps) {
// Suppress unused import warning — featureStateToVariant is available for consumers
void featureStateToVariant;
return (
<div
className={cx('dt-feature-legend', getVariantClass(variant), className)}
style={style}>
{title && (
<div className="dt-feature-legend-header">{title}</div>
)}
<div
className="dt-feature-legend-grid"
style={{ '--dt-feature-columns': columns } as CSSProperties}>
{features.map(feature => (
<div
key={feature.key}
className={cx('dt-feature-legend-item', stateClassMap[feature.state])}
style={{ width: `${100 / columns}%` }}>
<div
className="dt-feature-legend-icon"
style={{ width: `${iconSize}px`, height: `${iconSize}px` }}>
{feature.icon}
</div>
<div className="dt-feature-legend-label">{feature.name}</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,91 @@
/**
* DTGallery — Image gallery with beveled media frame and thumbnails.
*
* CSS reference: bevels.css .dt-bevel-media
*/
import { useState, 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;
}
interface DTGalleryProps {
items: DTGalleryItem[];
/** Color variant @default 'normal' */
variant?: DTVariant;
className?: string;
style?: CSSProperties;
}
export function DTGallery({
items,
variant = 'normal',
className,
style,
}: DTGalleryProps) {
const [activeIndex, setActiveIndex] = useState(0);
if (items.length === 0) return null;
const current = items[activeIndex];
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',
}}
/>
</div>
{/* Thumbnails */}
{items.length > 1 && (
<div
style={{
display: 'flex',
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>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,109 @@
/**
* DTHexagon — Decorative SVG hexagon with optional animations.
*
* Uses inline SVG (not CSS classes) — same approach as the RN version.
* Animation classes from animations.css applied to wrapper div.
*/
import type { ReactNode, CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses';
interface DTHexagonProps {
/** Diameter in pixels */
size: number;
/** Color variant @default 'normal' */
variant?: DTVariant;
/** Filled or outline @default true */
filled?: boolean;
/** Enable animation @default false */
animated?: boolean;
/** Animation type @default 'rotate' */
animationType?: 'rotate' | 'pulse' | 'none';
/** Animation duration in ms @default 2000 */
animationDuration?: number;
/** Outline stroke width @default 2 */
borderWidth?: number;
/** Custom color override */
color?: string;
/** Opacity @default 1 */
opacity?: number;
/** Centered content inside hexagon */
children?: ReactNode;
className?: string;
style?: CSSProperties;
}
/** Flat-top hexagon points for given size */
function hexPoints(size: number): string {
const r = size / 2;
const cx = r;
const cy = r;
const points: [number, number][] = [];
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i - Math.PI / 6;
points.push([cx + r * Math.cos(angle), cy + r * Math.sin(angle)]);
}
return points.map(([x, y]) => `${x},${y}`).join(' ');
}
export function DTHexagon({
size,
variant = 'normal',
filled = true,
animated = false,
animationType = 'rotate',
animationDuration = 2000,
borderWidth = 2,
color,
opacity = 1,
children,
className,
style,
}: DTHexagonProps) {
const animClass = animated
? animationType === 'rotate'
? 'dt-animate-spin'
: animationType === 'pulse'
? 'dt-animate-pulse'
: undefined
: undefined;
return (
<div
className={cx(getVariantClass(variant), animClass, className)}
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: size,
height: size,
position: 'relative',
opacity,
...(animated && animationDuration !== 2000
? { animationDuration: `${animationDuration}ms` }
: {}),
...style,
}}>
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
style={{ position: 'absolute', top: 0, left: 0 }}>
<polygon
points={hexPoints(size - borderWidth)}
transform={`translate(${borderWidth / 2}, ${borderWidth / 2})`}
fill={filled ? (color || 'var(--dt-card-color, var(--color-primary))') : 'none'}
stroke={color || 'var(--dt-card-color, var(--color-primary))'}
strokeWidth={borderWidth}
/>
</svg>
{children && (
<div style={{ position: 'relative', zIndex: 1 }}>
{children}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,33 @@
/**
* DTLabel — Filled badge/label with top-right bevel and mode color.
*
* CSS reference: bevels.css .badge, .badge-mode, .mode-*
*/
import type { ReactNode, CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses';
interface DTLabelProps {
children: ReactNode;
/** Color variant @default 'normal' */
variant?: DTVariant;
className?: string;
style?: CSSProperties;
}
export function DTLabel({
children,
variant = 'normal',
className,
style,
}: DTLabelProps) {
return (
<span
className={cx('badge', 'badge-mode', getVariantClass(variant), className)}
style={style}>
{children}
</span>
);
}

View File

@@ -0,0 +1,26 @@
/**
* DTMediaFrame — Diagonal beveled frame (top-left + bottom-right).
*
* CSS reference: bevels.css .dt-bevel-media
*/
import type { ReactNode, CSSProperties } from 'react';
import { cx } from '../utils/cx';
interface DTMediaFrameProps {
children: ReactNode;
className?: string;
style?: CSSProperties;
}
export function DTMediaFrame({
children,
className,
style,
}: DTMediaFrameProps) {
return (
<div className={cx('dt-bevel-media', className)} style={style}>
{children}
</div>
);
}

View File

@@ -0,0 +1,108 @@
/**
* DTMenu — Hierarchical menu with beveled items.
*
* CSS reference: forms-dt.css .dt-menu-item, .active, .selected
*/
import { useState, useCallback, type ReactNode, type CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses';
export interface DTMenuItem {
id: string;
title: string;
icon?: ReactNode;
items?: DTMenuItem[];
isActive?: boolean;
isSelected?: boolean;
}
interface DTMenuProps {
items: DTMenuItem[];
/** Color variant @default 'normal' */
variant?: DTVariant;
onItemClick?: (id: string) => void;
className?: string;
style?: CSSProperties;
}
export function DTMenu({
items,
variant = 'normal',
onItemClick,
className,
style,
}: DTMenuProps) {
return (
<div
className={cx(getVariantClass(variant), className)}
style={{ display: 'flex', flexDirection: 'column', gap: '4px', ...style }}>
{items.map(item => (
<MenuItem
key={item.id}
item={item}
level={0}
onItemClick={onItemClick}
/>
))}
</div>
);
}
function MenuItem({
item,
level,
onItemClick,
}: {
item: DTMenuItem;
level: number;
onItemClick?: (id: string) => void;
}) {
const [expanded, setExpanded] = useState(false);
const hasChildren = item.items && item.items.length > 0;
const handleClick = useCallback(() => {
if (hasChildren) {
setExpanded(prev => !prev);
}
onItemClick?.(item.id);
}, [hasChildren, item.id, onItemClick]);
return (
<>
<button
className={cx(
'dt-menu-item',
item.isActive && 'active',
item.isSelected && 'selected',
)}
style={{ '--dt-menu-level': level } as CSSProperties}
onClick={handleClick}
aria-expanded={hasChildren ? expanded : undefined}
type="button">
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{item.icon}
{item.title}
</span>
{hasChildren && (
<span
className="dt-accordion-chevron"
style={{
transition: 'transform 250ms ease-in-out',
transform: expanded ? 'rotate(180deg)' : undefined,
}}
/>
)}
</button>
{hasChildren && expanded && item.items!.map(child => (
<MenuItem
key={child.id}
item={child}
level={level + 1}
onItemClick={onItemClick}
/>
))}
</>
);
}

View File

@@ -0,0 +1,120 @@
/**
* DTMobileFilterOverlay — Full-screen slide-up filter panel.
*
* CSS reference: forms-dt.css .dt-filter-overlay, .dt-filter-overlay-backdrop,
* .dt-filter-overlay-content
*/
import { useEffect, useCallback, type ReactNode, type CSSProperties } from 'react';
import { createPortal } from 'react-dom';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses';
interface DTMobileFilterOverlayProps {
visible: boolean;
onDismiss: () => void;
/** Heading text @default 'Filters' */
heading?: string;
/** Number of active filters (shown as badge) */
activeFilterCount?: number;
/** Called when "Clear All" is pressed */
onClearAll?: () => void;
/** Color variant @default 'normal' */
variant?: DTVariant;
children: ReactNode;
className?: string;
style?: CSSProperties;
}
export function DTMobileFilterOverlay({
visible,
onDismiss,
heading = 'Filters',
activeFilterCount,
onClearAll,
variant = 'normal',
children,
className,
style,
}: DTMobileFilterOverlayProps) {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') onDismiss();
},
[onDismiss],
);
useEffect(() => {
if (visible) {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}
}, [visible, handleKeyDown]);
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">
{/* Header */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px',
borderBottom: '1px solid rgba(var(--color-primary-rgb), 0.2)',
}}>
<span style={{ fontWeight: 700, fontSize: '1.125rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{heading}
{activeFilterCount !== undefined && activeFilterCount > 0 && (
<span
className="badge badge-mode"
style={{ marginLeft: '8px', fontSize: '0.75rem' }}>
{activeFilterCount}
</span>
)}
</span>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{onClearAll && activeFilterCount !== undefined && activeFilterCount > 0 && (
<button
onClick={onClearAll}
type="button"
style={{
background: 'none',
border: 'none',
color: 'var(--color-primary)',
fontSize: '0.875rem',
cursor: 'pointer',
textTransform: 'uppercase',
}}>
Clear All
</button>
)}
<button
onClick={onDismiss}
type="button"
style={{
background: 'none',
border: 'none',
color: 'var(--color-text-primary)',
fontSize: '20px',
fontWeight: 700,
cursor: 'pointer',
padding: '0 4px',
}}>
</button>
</div>
</div>
{/* Content */}
<div style={{ padding: '16px', overflow: 'auto', maxHeight: 'calc(85vh - 60px)' }}>
{children}
</div>
</div>
</div>,
document.body,
);
}

View File

@@ -0,0 +1,96 @@
/**
* DTModal — Portal-based modal with beveled card content.
*
* CSS reference: bevels.css .dt-bevel-modal, .card, .mode-*
*/
import { useEffect, useCallback, type ReactNode, type CSSProperties } from 'react';
import { createPortal } from 'react-dom';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses';
interface DTModalProps {
visible: boolean;
onDismiss: () => void;
title?: string;
/** Color variant @default 'normal' */
variant?: DTVariant;
children: ReactNode;
/** Whether clicking backdrop dismisses @default true */
dismissable?: boolean;
className?: string;
style?: CSSProperties;
}
export function DTModal({
visible,
onDismiss,
title,
variant = 'normal',
children,
dismissable = true,
className,
style,
}: DTModalProps) {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape' && dismissable) onDismiss();
},
[onDismiss, dismissable],
);
useEffect(() => {
if (visible) {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}
}, [visible, handleKeyDown]);
if (!visible) return null;
return createPortal(
<div
className="dt-modal-overlay"
style={{
position: 'fixed',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}>
<div
className="dt-modal-backdrop"
style={{
position: 'absolute',
inset: 0,
background: 'rgba(0, 0, 0, 0.5)',
backdropFilter: 'blur(4px)',
}}
onClick={dismissable ? onDismiss : undefined}
/>
<div
className={cx(
'card',
'dt-bevel-modal',
getVariantClass(variant),
'dt-animate-scale-in',
className,
)}
style={{
position: 'relative',
maxWidth: '90vw',
maxHeight: '85vh',
overflow: 'auto',
...style,
}}
role="dialog"
aria-modal="true">
{title && <div className="card-title">{title}</div>}
<div style={{ padding: 'var(--space-6, 24px)' }}>{children}</div>
</div>
</div>,
document.body,
);
}

View File

@@ -0,0 +1,45 @@
/**
* DTProgressBar — Angular progress bar (horizontal or vertical).
*
* CSS reference: forms-dt.css .dt-progress, .dt-progress-fill, .dt-progress-label
*/
import type { CSSProperties } from 'react';
import { cx } from '../utils/cx';
interface DTProgressBarProps {
/** Progress value (01) */
value: number;
/** Text label below/beside the bar */
label?: string;
/** Layout direction @default 'horizontal' */
direction?: 'horizontal' | 'vertical';
className?: string;
style?: CSSProperties;
}
export function DTProgressBar({
value,
label,
direction = 'horizontal',
className,
style,
}: DTProgressBarProps) {
const pct = Math.round(Math.max(0, Math.min(1, value)) * 100);
return (
<div className={className} style={style}>
<div className={cx('dt-progress', direction === 'vertical' && 'vertical')}>
<div
className="dt-progress-fill"
style={
direction === 'vertical'
? { height: `${pct}%` }
: { width: `${pct}%` }
}
/>
</div>
{label && <span className="dt-progress-label">{label}</span>}
</div>
);
}

View File

@@ -0,0 +1,50 @@
/**
* DTQuantityStepper — Beveled +/- stepper.
*
* CSS reference: forms-dt.css .dt-stepper, .dt-stepper-btn, .dt-stepper-value
*/
import type { CSSProperties } from 'react';
import { cx } from '../utils/cx';
interface DTQuantityStepperProps {
value: number;
onChange: (value: number) => void;
min?: number;
max?: number;
disabled?: boolean;
className?: string;
style?: CSSProperties;
}
export function DTQuantityStepper({
value,
onChange,
min = 0,
max = 99,
disabled = false,
className,
style,
}: DTQuantityStepperProps) {
return (
<div className={cx('dt-stepper', className)} style={style}>
<button
className="dt-stepper-btn"
onClick={() => onChange(Math.max(min, value - 1))}
disabled={disabled || value <= min}
type="button"
aria-label="Decrease">
</button>
<span className="dt-stepper-value">{value}</span>
<button
className="dt-stepper-btn"
onClick={() => onChange(Math.min(max, value + 1))}
disabled={disabled || value >= max}
type="button"
aria-label="Increase">
+
</button>
</div>
);
}

View File

@@ -0,0 +1,58 @@
/**
* DTRadioGroup — Hexagonal radio buttons.
*
* CSS reference: forms-dt.css input[type="radio"], .dt-radio-option
*/
import { useId, type CSSProperties } from 'react';
import { cx } from '../utils/cx';
interface DTRadioOption {
value: string;
label: string;
}
interface DTRadioGroupProps {
options: DTRadioOption[];
value: string;
onChange: (value: string) => void;
name?: string;
className?: string;
style?: CSSProperties;
}
export { type DTRadioOption };
export function DTRadioGroup({
options,
value,
onChange,
name,
className,
style,
}: DTRadioGroupProps) {
const groupId = useId();
const groupName = name || `dt-radio-${groupId}`;
return (
<div
className={cx('dt-radio-group', className)}
style={{ display: 'flex', flexDirection: 'column', gap: '4px', ...style }}
role="radiogroup">
{options.map(option => (
<label
key={option.value}
className={cx('dt-radio-option', value === option.value && 'selected')}>
<input
type="radio"
name={groupName}
value={option.value}
checked={value === option.value}
onChange={() => onChange(option.value)}
/>
<span>{option.label}</span>
</label>
))}
</div>
);
}

View File

@@ -0,0 +1,70 @@
/**
* DTSearchInput — Search field with icon.
*
* CSS reference: forms-dt.css .input
*/
import { useCallback, type CSSProperties, type KeyboardEvent } from 'react';
import { cx } from '../utils/cx';
interface DTSearchInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
onSearch?: (value: string) => void;
className?: string;
style?: CSSProperties;
}
export function DTSearchInput({
value,
onChange,
placeholder = 'Search...',
onSearch,
className,
style,
}: DTSearchInputProps) {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Enter' && onSearch) {
onSearch(value);
}
},
[onSearch, value],
);
return (
<div
className={cx('dt-search-input-wrapper', className)}
style={{ position: 'relative', ...style }}>
<input
type="search"
className="input"
value={value}
onChange={e => onChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
style={{ width: '100%', padding: '8px 12px 8px 36px' }}
/>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
style={{
position: 'absolute',
left: '12px',
top: '50%',
transform: 'translateY(-50%)',
opacity: 0.6,
pointerEvents: 'none',
}}>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
</div>
);
}

View File

@@ -0,0 +1,39 @@
/**
* 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.
*/
import type { ReactNode, 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;
className?: string;
style?: CSSProperties;
}
export function DTStaggerContainer({
children,
duration,
interval,
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}
</div>
);
}

View File

@@ -0,0 +1,44 @@
/**
* DTSwitch — Angular toggle switch.
*
* CSS reference: forms-dt.css .dt-switch, .dt-switch-track, .dt-switch-thumb
*/
import type { CSSProperties } from 'react';
import { cx } from '../utils/cx';
interface DTSwitchProps {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
label?: string;
className?: string;
style?: CSSProperties;
}
export function DTSwitch({
checked,
onChange,
disabled = false,
label,
className,
style,
}: DTSwitchProps) {
return (
<div
className={cx('dt-switch-wrapper', className)}
style={{ display: 'inline-flex', alignItems: 'center', gap: '12px', ...style }}>
<label className="dt-switch">
<input
type="checkbox"
checked={checked}
onChange={e => onChange(e.target.checked)}
disabled={disabled}
/>
<span className="dt-switch-track" />
<span className="dt-switch-thumb" />
</label>
{label && <span>{label}</span>}
</div>
);
}

View File

@@ -0,0 +1,53 @@
/**
* DTTextInput — Styled text input with focus glow.
*
* CSS reference: forms-dt.css input[type="text"], .input, .error
*/
import type { CSSProperties, InputHTMLAttributes } from 'react';
import { cx } from '../utils/cx';
interface DTTextInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'style'> {
/** Show error state */
error?: boolean;
/** Label text above input */
label?: string;
className?: string;
style?: CSSProperties;
}
export function DTTextInput({
error = false,
label,
className,
style,
id,
...inputProps
}: DTTextInputProps) {
const inputId = id || (label ? `dt-input-${label.replace(/\s+/g, '-').toLowerCase()}` : undefined);
return (
<div className={cx('dt-text-input-wrapper', className)} style={style}>
{label && (
<label
htmlFor={inputId}
style={{
display: 'block',
marginBottom: '4px',
fontSize: '0.875rem',
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}>
{label}
</label>
)}
<input
{...inputProps}
id={inputId}
className={cx('input', error && 'error')}
style={{ width: '100%', padding: '8px 12px' }}
/>
</div>
);
}

View File

@@ -0,0 +1,42 @@
/**
* DTWebThemeProvider
*
* Sets data-brand and data-theme attributes on a wrapper div.
* Provides React context for child components to read the active brand/theme.
*/
import { createContext, type ReactNode } from 'react';
import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
interface DTWebThemeContextValue {
brand: ThemeBrand;
theme: ThemeMode;
}
export const DTWebThemeContext = createContext<DTWebThemeContextValue | null>(null);
interface DTWebThemeProviderProps {
/** Brand to apply @default 'dt' */
brand?: ThemeBrand;
/** Theme mode @default 'dark' */
theme?: ThemeMode;
/** Additional className for the wrapper div */
className?: string;
children: ReactNode;
}
export function DTWebThemeProvider({
brand = 'dt',
theme = 'dark',
className,
children,
}: DTWebThemeProviderProps) {
return (
<DTWebThemeContext.Provider value={{ brand, theme }}>
<div data-brand={brand} data-theme={theme} className={cx('dt-theme-root', className)}>
{children}
</div>
</DTWebThemeContext.Provider>
);
}

View File

@@ -0,0 +1,14 @@
/**
* Hook to access the DTWebThemeProvider context.
*/
import { useContext } from 'react';
import { DTWebThemeContext } from '../components/DTWebThemeProvider';
import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens';
export function useDTWebTheme(): { brand: ThemeBrand; theme: ThemeMode } {
const ctx = useContext(DTWebThemeContext);
if (!ctx) {
return { brand: 'dt', theme: 'dark' };
}
return ctx;
}

View File

@@ -0,0 +1,24 @@
/**
* Hook: toggles dt-animate-pulse CSS class based on enabled flag.
*/
import { useEffect } from 'react';
export function usePulse(
ref: React.RefObject<HTMLElement | null>,
enabled = false,
): void {
useEffect(() => {
const el = ref.current;
if (!el) return;
if (enabled) {
el.classList.add('dt-animate-pulse');
} else {
el.classList.remove('dt-animate-pulse');
}
return () => {
el.classList.remove('dt-animate-pulse');
};
}, [ref, enabled]);
}

View File

@@ -0,0 +1,29 @@
/**
* Hook: applies dt-animate-scale-in CSS class on mount.
*/
import { useEffect } from 'react';
export function useScaleIn(
ref: React.RefObject<HTMLElement | null>,
options?: { duration?: number; delay?: number },
): void {
useEffect(() => {
const el = ref.current;
if (!el) return;
if (options?.duration) {
el.style.animationDuration = `${options.duration}ms`;
}
if (options?.delay) {
el.style.animationDelay = `${options.delay}ms`;
}
el.classList.add('dt-animate-scale-in');
return () => {
el.classList.remove('dt-animate-scale-in');
el.style.animationDuration = '';
el.style.animationDelay = '';
};
}, [ref, options?.duration, options?.delay]);
}

View File

@@ -0,0 +1,60 @@
/**
* @dangerousthings/react
*
* React web components for the Dangerous Things design system.
* Wraps @dangerousthings/web CSS classes with React component APIs
* matching @dangerousthings/react-native for cross-platform parity.
*/
// Theme
export { DTWebThemeProvider } from './components/DTWebThemeProvider';
export { useDTWebTheme } from './hooks/useDTWebTheme';
// Types (re-exported from tokens for convenience)
export type { DTVariant, ThemeBrand, ThemeMode } from '@dangerousthings/tokens';
export { variantToClassName, variantToCSSProperty } from '@dangerousthings/tokens';
// Utilities
export { cx } from './utils/cx';
export { getVariantClass, featureStateToVariant, variantToBadgeClass } from './utils/variantClasses';
// Hooks
export { useScaleIn } from './hooks/useScaleIn';
export { usePulse } from './hooks/usePulse';
// Components — bevel & layout
export { DTCard } from './components/DTCard';
export { DTButton } from './components/DTButton';
export { DTLabel } from './components/DTLabel';
export { DTChip } from './components/DTChip';
export { DTMediaFrame } from './components/DTMediaFrame';
export { DTModal } from './components/DTModal';
export { DTDrawer } from './components/DTDrawer';
// Components — forms
export { DTTextInput } from './components/DTTextInput';
export { DTCheckbox } from './components/DTCheckbox';
export { DTSwitch } from './components/DTSwitch';
export { DTRadioGroup } from './components/DTRadioGroup';
export type { DTRadioOption } from './components/DTRadioGroup';
export { DTQuantityStepper } from './components/DTQuantityStepper';
export { DTSearchInput } from './components/DTSearchInput';
// Components — layout & animation
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
export { DTMenu } from './components/DTMenu';
export type { DTMenuItem } from './components/DTMenu';
export { DTFeatureLegend } from './components/DTFeatureLegend';
export type { DTFeatureItem } from './components/DTFeatureLegend';
export { DTMobileFilterOverlay } from './components/DTMobileFilterOverlay';
// Components — media & decorative
export { DTGallery } from './components/DTGallery';
export type { DTGalleryItem } from './components/DTGallery';
export { DTHexagon } from './components/DTHexagon';

View File

@@ -0,0 +1,7 @@
/**
* Lightweight className composition utility.
* Filters out falsy values and joins with spaces.
*/
export function cx(...args: (string | false | null | undefined)[]): string {
return args.filter(Boolean).join(' ');
}

View File

@@ -0,0 +1,31 @@
/**
* DTVariant → CSS class name mapping
*/
import { variantToClassName } from '@dangerousthings/tokens';
import type { DTVariant } from '@dangerousthings/tokens';
export function getVariantClass(variant: DTVariant): string {
return variantToClassName[variant];
}
/** Map feature state to variant */
export function featureStateToVariant(state: 'supported' | 'disabled' | 'unsupported'): DTVariant {
const map: Record<typeof state, DTVariant> = {
supported: 'normal',
disabled: 'emphasis',
unsupported: 'warning',
};
return map[state];
}
/** Map variant to badge CSS class (for status badge colors) */
export function variantToBadgeClass(variant: DTVariant): string {
const map: Record<DTVariant, string> = {
normal: 'badge-info',
emphasis: 'badge-warning',
warning: 'badge-error',
success: 'badge-success',
other: 'badge-info',
};
return map[variant];
}

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"jsx": "react-jsx"
},
"include": ["src"]
}

View File

@@ -24,13 +24,24 @@
"clean": "rm -rf dist release"
},
"dependencies": {
"@dangerousthings/react": "*",
"@dangerousthings/tokens": "*",
"@dangerousthings/web": "*"
"@dangerousthings/web": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-icons": "^5.6.0"
},
"devDependencies": {
"@dangerousthings/tailwind-preset": "*",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.0",
"concurrently": "^9.0.0",
"electron": "33.4.11",
"electron-builder": "^25.0.0",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.8.3",
"vite": "^6.0.0",
"wait-on": "^8.0.0"

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -0,0 +1,100 @@
import { useState, useEffect } from 'react';
import { themes } from '@dangerousthings/tokens';
import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens';
import { HomePage } from './pages/HomePage';
import { BevelsPage } from './pages/BevelsPage';
import { GlowsPage } from './pages/GlowsPage';
import { FormsPage } from './pages/FormsPage';
import { AnimationsPage } from './pages/AnimationsPage';
import { CardsAdvancedPage } from './pages/CardsAdvancedPage';
import { TokensPage } from './pages/TokensPage';
const navPages = [
{ hash: '', label: 'Home' },
{ hash: 'bevels', label: 'Bevels' },
{ hash: 'glows', label: 'Glows' },
{ hash: 'forms', label: 'Forms' },
{ hash: 'animations', label: 'Animations' },
{ hash: 'cards-advanced', label: 'Advanced Cards' },
{ hash: 'tokens', label: 'Tokens' },
];
export function App() {
const [brand, setBrand] = useState<ThemeBrand>('dt');
const [theme, setTheme] = useState<ThemeMode>('dark');
const [currentHash, setCurrentHash] = useState('');
// Sync brand/theme to <html> so CSS custom properties cascade everywhere
useEffect(() => {
document.documentElement.setAttribute('data-brand', brand);
document.documentElement.setAttribute('data-theme', theme);
}, [brand, theme]);
// Hash-based routing
useEffect(() => {
function onHashChange() {
setCurrentHash(window.location.hash.replace('#/', '').replace('#', ''));
}
window.addEventListener('hashchange', onHashChange);
onHashChange();
return () => window.removeEventListener('hashchange', onHashChange);
}, []);
function renderPage() {
switch (currentHash) {
case 'bevels': return <BevelsPage />;
case 'glows': return <GlowsPage />;
case 'forms': return <FormsPage />;
case 'animations': return <AnimationsPage />;
case 'cards-advanced': return <CardsAdvancedPage />;
case 'tokens': return <TokensPage brand={brand} />;
default: return <HomePage />;
}
}
return (
<>
<nav id="sidebar">
<div className="nav-title">DT DESIGN SYSTEM</div>
<div className="brand-switcher-container">
{themes.map(t => (
<button
key={t.id}
className={`brand-btn${t.id === brand ? ' active' : ''}`}
onClick={() => setBrand(t.id)}
type="button"
>
{t.name}
</button>
))}
<div className="mode-switcher">
{(['dark', 'light', 'auto'] as ThemeMode[]).map(m => (
<button
key={m}
className={`mode-btn${m === theme ? ' active' : ''}`}
onClick={() => setTheme(m)}
type="button"
>
{m}
</button>
))}
</div>
</div>
<div className="nav-links">
{navPages.map(p => (
<a
key={p.hash}
href={`#/${p.hash}`}
className={`nav-link${p.hash === currentHash ? ' active' : ''}`}
>
{p.label}
</a>
))}
</div>
</nav>
<main id="content">
{renderPage()}
</main>
</>
);
}

View File

@@ -1,77 +0,0 @@
import { themes } from '@dangerousthings/tokens';
import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens';
let currentBrand: ThemeBrand = 'dt';
let currentMode: ThemeMode = 'dark';
export function initBrandSwitcher(container: HTMLElement) {
// Brand buttons
for (const theme of themes) {
const btn = document.createElement('button');
btn.textContent = theme.name;
btn.className = 'brand-btn';
btn.dataset.brand = theme.id;
if (theme.id === currentBrand) btn.classList.add('active');
btn.addEventListener('click', () => switchBrand(theme.id));
container.appendChild(btn);
}
// Mode switcher
const modeContainer = document.createElement('div');
modeContainer.className = 'mode-switcher';
const modes: ThemeMode[] = ['dark', 'light', 'auto'];
for (const mode of modes) {
const btn = document.createElement('button');
btn.textContent = mode;
btn.className = 'mode-btn';
btn.dataset.mode = mode;
if (mode === currentMode) btn.classList.add('active');
btn.addEventListener('click', () => switchMode(mode));
modeContainer.appendChild(btn);
}
container.appendChild(modeContainer);
// Set initial state
applyTheme();
}
function switchBrand(id: ThemeBrand) {
currentBrand = id;
applyTheme();
updateBrandButtons();
}
function switchMode(mode: ThemeMode) {
currentMode = mode;
applyTheme();
updateModeButtons();
}
function applyTheme() {
document.documentElement.setAttribute('data-brand', currentBrand);
document.documentElement.setAttribute('data-theme', currentMode);
}
function updateBrandButtons() {
document.querySelectorAll('.brand-btn').forEach((btn) => {
const el = btn as HTMLButtonElement;
el.classList.toggle('active', el.dataset.brand === currentBrand);
});
}
function updateModeButtons() {
document.querySelectorAll('.mode-btn').forEach((btn) => {
const el = btn as HTMLButtonElement;
el.classList.toggle('active', el.dataset.mode === currentMode);
});
}
export function getCurrentBrand(): ThemeBrand {
return currentBrand;
}
export function getCurrentMode(): ThemeMode {
return currentMode;
}

View File

@@ -0,0 +1,23 @@
import type { ReactNode, CSSProperties } from 'react';
export function Section({ title, description, children }: { title: string; description: string; children: ReactNode }) {
return (
<div className="demo-section">
<h2>{title}</h2>
<p className="demo-description">{description}</p>
{children}
</div>
);
}
export function Row({ children, style }: { children: ReactNode; style?: CSSProperties }) {
return <div className="demo-row" style={style}>{children}</div>;
}
export function CodeLabel({ text }: { text: string }) {
return <div className="code-label">{text}</div>;
}
export function DemoLabel({ text }: { text: string }) {
return <div className="demo-label">{text}</div>;
}

View File

@@ -6,10 +6,7 @@
<title>DT Design System — Desktop Showcase</title>
</head>
<body>
<div id="app">
<nav id="sidebar"></nav>
<main id="content"></main>
</div>
<script type="module" src="./main.ts"></script>
<div id="app"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View File

@@ -1,56 +0,0 @@
import '@dangerousthings/web/index.css';
import './style.css';
import { initRouter } from './utils/router';
import { initBrandSwitcher } from './brand-switcher';
import { renderHomePage } from './pages/home';
import { renderBevelsPage } from './pages/bevels';
import { renderGlowsPage } from './pages/glows';
import { renderFormsPage } from './pages/forms';
import { renderTokensPage } from './pages/tokens';
const routes: Record<string, (container: HTMLElement) => void> = {
'': renderHomePage,
'bevels': renderBevelsPage,
'glows': renderGlowsPage,
'forms': renderFormsPage,
'tokens': renderTokensPage,
};
const sidebar = document.getElementById('sidebar')!;
const content = document.getElementById('content')!;
// Build sidebar navigation
const navTitle = document.createElement('div');
navTitle.className = 'nav-title';
navTitle.textContent = 'DT DESIGN SYSTEM';
sidebar.appendChild(navTitle);
const brandContainer = document.createElement('div');
brandContainer.className = 'brand-switcher-container';
sidebar.appendChild(brandContainer);
initBrandSwitcher(brandContainer);
const navLinks = document.createElement('div');
navLinks.className = 'nav-links';
const pages = [
{ hash: '', label: 'Home' },
{ hash: 'bevels', label: 'Bevels' },
{ hash: 'glows', label: 'Glows' },
{ hash: 'forms', label: 'Forms' },
{ hash: 'tokens', label: 'Tokens' },
];
for (const page of pages) {
const link = document.createElement('a');
link.href = `#/${page.hash}`;
link.textContent = page.label;
link.className = 'nav-link';
navLinks.appendChild(link);
}
sidebar.appendChild(navLinks);
// Initialize router
initRouter(routes, content);

View File

@@ -0,0 +1,6 @@
import '@dangerousthings/web/index.css';
import './style.css';
import { createRoot } from 'react-dom/client';
import { App } from './App';
createRoot(document.getElementById('app')!).render(<App />);

View File

@@ -0,0 +1,150 @@
import { useState } from 'react';
import { DTCard, DTStaggerContainer } from '@dangerousthings/react';
import { Section, Row, CodeLabel } from '../components/Section';
function AnimBox({ className, label }: { className: string; label: string }) {
return (
<div style={{ textAlign: 'center' }}>
<div
className={className}
style={{
background: 'var(--color-primary)',
width: 80,
height: 80,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
color: 'var(--color-bg)',
fontSize: '0.7rem',
}}
>
{label}
</div>
</div>
);
}
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>
<Section title="Entrance Animations" description="One-shot animations for elements entering the viewport.">
<Row key={entranceKey}>
<AnimBox className="dt-animate-scale-in" label="SCALE IN" />
<AnimBox className="dt-animate-fade-in" label="FADE IN" />
<AnimBox className="dt-animate-slide-up" label="SLIDE UP" />
</Row>
<button className="btn-secondary" style={{ marginTop: 'var(--space-4)' }} onClick={() => setEntranceKey(k => k + 1)} type="button">
REPLAY
</button>
<CodeLabel text=".dt-animate-scale-in | .dt-animate-fade-in | .dt-animate-slide-up" />
</Section>
<Section title="Interactive Animations" description="Looping animations for active/loading states.">
<Row>
<AnimBox className="dt-animate-pulse" label="PULSE" />
<AnimBox className="dt-animate-ping" label="PING" />
<AnimBox className="dt-animate-spin" label="SPIN" />
</Row>
<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
className="dt-accent-top"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
padding: 'var(--space-3) var(--space-4)',
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
cursor: 'pointer',
color: 'var(--color-text)',
fontWeight: 600,
textTransform: 'uppercase',
}}
aria-expanded={accordionExpanded}
onClick={() => setAccordionExpanded(!accordionExpanded)}
type="button"
>
Click to expand
<span style={{
display: 'inline-block',
transition: 'transform 250ms ease-in-out',
transform: accordionExpanded ? 'rotate(180deg)' : undefined,
}}>
&#x25BC;
</span>
</button>
<div style={{
maxHeight: accordionExpanded ? 200 : 0,
overflow: 'hidden',
transition: 'max-height 250ms ease-in-out',
}}>
<div style={{
padding: 'var(--space-4)',
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
borderTop: 'none',
}}>
This content expands with a smooth max-height transition. The chevron rotates 180 degrees.
</div>
</div>
</div>
<CodeLabel text=".dt-transition-accordion | .dt-transition-chevron | .dt-transition-progress" />
</Section>
<Section title="Scrollbar Styling" description="Thin neon scrollbar scoped under [data-brand='dt'].">
<div
className="dt-scrollbar"
style={{
maxHeight: 120,
overflowY: 'auto',
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
padding: 'var(--space-3)',
}}
>
{Array.from({ length: 20 }, (_, i) => (
<div key={i} style={{ padding: '4px 0', color: 'var(--color-text)' }}>
Scrollbar line {i + 1} thin neon scrollbar styled with --color-primary
</div>
))}
</div>
<CodeLabel text=".dt-scrollbar — scrollbar-color: var(--color-primary)" />
</Section>
</>
);
}

View File

@@ -0,0 +1,103 @@
import { DTCard, DTMediaFrame } from '@dangerousthings/react';
import type { DTVariant } from '@dangerousthings/tokens';
import { Section, Row, CodeLabel } from '../components/Section';
const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other'];
export function BevelsPage() {
return (
<>
<h1 className="page-title">Bevels</h1>
<p className="page-subtitle">Angular clip-path patterns from the DT design system. Active on the DT brand.</p>
<Section title="Card Bevels" description="Dual bottom bevels using clip-path. Outer bg = border color, ::before = surface fill.">
<DTCard title="CARD TITLE">
<div className="card-body">Dual bottom bevels bottom-right (bevel-md) and bottom-left (bevel-sm). Uses the dual-element technique with ::before pseudo-element.</div>
</DTCard>
<CodeLabel text="<DTCard title='...'> or .card" />
<DTCard>
<div className="card-body">Card without title no header bar, just the beveled container.</div>
</DTCard>
<CodeLabel text="<DTCard> (no title prop)" />
</Section>
<Section title="Button Bevels" description="Top-right corner cut. Filled buttons use direct clip-path, outline uses dual-element.">
<Row>
<button className="btn-primary" type="button">PRIMARY</button>
<button className="btn-secondary" type="button">SECONDARY</button>
<button className="btn-danger" type="button">DANGER</button>
</Row>
<CodeLabel text=".btn-primary | .btn-secondary | .btn-danger" />
</Section>
<Section title="Badge Bevels" description="Top-right bevel with dual-element technique. Color variants via CSS custom properties.">
<Row>
<span className="badge">Default</span>
<span className="badge badge-success">Success</span>
<span className="badge badge-error">Error</span>
<span className="badge badge-warning">Warning</span>
<span className="badge badge-info">Info</span>
</Row>
<CodeLabel text=".badge | .badge-success | .badge-error | .badge-warning | .badge-info" />
</Section>
<Section title="Media Frame Bevels" description="Diagonal opposite corners (top-left + bottom-right).">
<DTMediaFrame>
<div style={{ background: 'var(--color-primary)', width: '100%', aspectRatio: '16/9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: 'var(--color-bg)', fontWeight: 700 }}>MEDIA FRAME</span>
</div>
</DTMediaFrame>
<CodeLabel text="<DTMediaFrame> or .dt-bevel-media" />
</Section>
<Section title="Modal Bevels" description="Dual bottom bevels at bevel-lg scale.">
<div className="dt-bevel-modal" style={{ background: 'var(--color-primary)', padding: 'var(--space-8)', textAlign: 'center' }}>
<div style={{ background: 'var(--color-surface)', padding: 'var(--space-6)' }}>
<div style={{ fontWeight: 700, textTransform: 'uppercase' }}>Modal Content</div>
<div style={{ color: 'var(--color-text-muted)', marginTop: 'var(--space-2)' }}>Large dual bottom bevels</div>
</div>
</div>
<CodeLabel text=".dt-bevel-modal" />
</Section>
<Section title="Drawer Bevels" description="Exposed-edge bevels for sliding panels.">
<Row>
<div className="dt-bevel-drawer-right" style={{ background: 'var(--color-primary)', padding: 'var(--space-6)', width: 200, height: 120, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: 'var(--color-bg)', fontWeight: 700 }}>RIGHT</span>
</div>
<div className="dt-bevel-drawer-left" style={{ background: 'var(--color-primary)', padding: 'var(--space-6)', width: 200, height: 120, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: 'var(--color-bg)', fontWeight: 700 }}>LEFT</span>
</div>
</Row>
<CodeLabel text=".dt-bevel-drawer-right | .dt-bevel-drawer-left" />
</Section>
<Section title="Small Bevel Utility" description="For compact elements — arrows, stepper buttons.">
<Row>
<div className="dt-bevel-sm" style={{ background: 'var(--color-primary)', width: 60, height: 60, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: 'var(--color-bg)', fontWeight: 700, fontSize: '0.75rem' }}>SM</span>
</div>
</Row>
<CodeLabel text=".dt-bevel-sm" />
</Section>
<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>
</div>
<CodeLabel text=".dt-accent-top" />
</Section>
<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()}>
<div className="card-body" style={{ fontSize: '0.75rem' }}>mode-{mode}</div>
</DTCard>
))}
</div>
<CodeLabel text="<DTCard variant='normal'> | 'emphasis' | 'warning' | 'success' | 'other'" />
</Section>
</>
);
}

View File

@@ -0,0 +1,210 @@
import { createElement, useState, type CSSProperties } from 'react';
import {
DTCard,
DTButton,
DTStaggerContainer,
DTFeatureLegend,
} from '@dangerousthings/react';
import type { DTFeatureItem } from '@dangerousthings/react';
import type { DTVariant } from '@dangerousthings/tokens';
import { Section, Row, CodeLabel } from '../components/Section';
// Feature icons from dt-shopify-storefront
import {
MdOutlinePhonelinkRing,
MdOutlineVpnKey,
MdOutlineMobileScreenShare,
MdOutlineCreditCard,
MdOutlineCopyAll,
MdOutlineLightbulb,
MdOutlineThermostat,
MdOutlineSensors,
MdOutlineFitbit,
MdOutlineVibration,
MdOutlineExplore,
MdLightbulbOutline,
MdOutlineVisibility,
} from 'react-icons/md';
import { FaUserShield } from 'react-icons/fa';
import { LuBinary } from 'react-icons/lu';
const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other'];
// Wrap react-icons to avoid IconType/ReactNode type mismatch
const ico = (C: any) => createElement(C, { style: { fontSize: '2rem' } });
// Full chip feature legend (9 features — from storefront UseCaseLegend)
const chipFeatures: DTFeatureItem[] = [
{ key: 'smartphone', name: 'Smartphone', icon: ico(MdOutlinePhonelinkRing), state: 'supported' },
{ key: 'access_control', name: 'Access Control', icon: ico(MdOutlineVpnKey), state: 'supported' },
{ key: 'digital_security', name: 'Digital Security', icon: ico(FaUserShield), state: 'supported' },
{ key: 'cryptography', name: 'Cryptography', icon: ico(LuBinary), state: 'unsupported' },
{ key: 'data_sharing', name: 'Data Sharing', icon: ico(MdOutlineMobileScreenShare), state: 'supported' },
{ key: 'payment', name: 'Payment', icon: ico(MdOutlineCreditCard), state: 'disabled' },
{ key: 'magic', name: 'UID Magic', icon: ico(MdOutlineCopyAll), state: 'supported' },
{ key: 'illumination', name: 'Illumination', icon: ico(MdOutlineLightbulb), state: 'unsupported' },
{ key: 'temperature', name: 'Temperature', icon: ico(MdOutlineThermostat), state: 'unsupported' },
];
// Full biomagnet feature legend (4 features — from storefront MagnetUseCaseLegend)
const magnetFeatures: DTFeatureItem[] = [
{ key: 'sensing', name: 'Sensing', icon: ico(MdOutlineSensors), state: 'supported' },
{ key: 'lifting', name: 'Lifting', icon: ico(MdOutlineFitbit), state: 'supported' },
{ key: 'haptics', name: 'Haptics', icon: ico(MdOutlineVibration), state: 'supported' },
{ key: 'polarity', name: 'Polarity Detection', icon: ico(MdOutlineExplore), state: 'unsupported' },
];
// Full aesthetic feature legend (2 features — from storefront AestheticUseCaseLegend)
const aestheticFeatures: DTFeatureItem[] = [
{ key: 'illumination', name: 'Illumination', icon: ico(MdLightbulbOutline), state: 'supported' },
{ key: 'prominence', name: 'Prominence', icon: ico(MdOutlineVisibility), state: 'supported' },
];
export function CardsAdvancedPage() {
const [staggerKey, setStaggerKey] = useState(0);
return (
<>
<h1 className="page-title">Advanced Cards</h1>
<p className="page-subtitle">Card color modes, progress bars, badge overlays, and interactive bevel buttons.</p>
<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()}>
<div className="card-body" style={{ fontSize: '0.8rem' }}>mode-{mode}</div>
</DTCard>
))}
</div>
<CodeLabel text="<DTCard variant='normal'> | 'emphasis' | 'warning' | 'success' | 'other'" />
</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) => (
<DTCard key={val} variant={modes[i % modes.length]} title={`${val}%`} progress={val}>
<div className="card-body" style={{ fontSize: '0.8rem' }}>Progress: {val}%</div>
</DTCard>
))}
</div>
<CodeLabel text="<DTCard progress={50} /> — height driven by --dt-card-progress" />
</Section>
<Section title="Card Badges" description="Bottom-right chip badge on product image area. Inherits card mode color.">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-dt-4">
{[
{ label: 'LAB', mode: 'warning' as DTVariant, img: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?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' },
].map(badge => (
<DTCard key={badge.label} variant={badge.mode} title={badge.label + ' PRODUCT'}>
<div className="card-body-flush dt-badge-parent" 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 image container" />
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem', margin: 'var(--space-4) 0 var(--space-2)' }}>
Also works on beveled media frames:
</p>
<div className="grid grid-cols-3 gap-dt-4">
{[
{ label: 'LAB', mode: 'warning' as DTVariant, img: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?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' },
].map(badge => (
<div key={badge.label} className={`dt-bevel-media mode-${badge.mode}`} style={{ aspectRatio: '1', overflow: 'hidden' }}>
<img src={badge.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
<span className="dt-card-badge">{badge.label}</span>
</div>
))}
</div>
<CodeLabel text=".dt-bevel-media.mode-warning > .dt-card-badge" />
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem', margin: 'var(--space-4) 0 var(--space-2)' }}>
Missing image placeholder (no img element):
</p>
<div className="grid grid-cols-3 gap-dt-4">
{(['warning', 'other', 'normal'] as DTVariant[]).map(mode => (
<div key={mode} className={`dt-bevel-media mode-${mode}`} style={{ aspectRatio: '1' }} />
))}
</div>
<CodeLabel text=".dt-bevel-media without child img — shows 'MISSING MEDIA' placeholder" />
</Section>
<Section title="Interactive Bevel Buttons" description="Outlined rectangle by default. Bottom-right bevel appears on hover/select, fills with mode color.">
<Row>
{modes.map(mode => (
<DTButton key={mode} variant={mode}>{mode.toUpperCase()}</DTButton>
))}
</Row>
<CodeLabel text="<DTButton variant='normal'> — hover for bevel + fill" />
<Row style={{ marginTop: 'var(--space-4)' }}>
{modes.map(mode => (
<DTButton key={mode} variant={mode} selected>{mode.toUpperCase()} SEL</DTButton>
))}
</Row>
<CodeLabel text="<DTButton variant='emphasis' selected /> — persistent bevel + fill" />
</Section>
<Section title="Menu Items" description="Filter-style beveled menu items with active/selected states and level indentation.">
<div style={{ maxWidth: 300 }}>
{['All Products', 'NFC Implants', 'RFID Tags', 'Accessories', 'Lab Products'].map((item, i) => (
<button
key={item}
className={`dt-menu-item${i === 0 ? ' active' : ''}${i === 4 ? ' mode-warning' : ''}`}
style={i > 1 && i < 4 ? { '--dt-menu-level': '1' } as CSSProperties : undefined}
type="button"
>
{item}
</button>
))}
</div>
<CodeLabel text=".dt-menu-item | .dt-menu-item.active | --dt-menu-level: 1" />
</Section>
<Section title="Feature Legends" description="Product feature grids from the storefront. Icons indicate feature capabilities; color indicates state.">
<DTFeatureLegend features={chipFeatures} title="NFC CHIP FEATURES" variant="normal" columns={5} />
<div style={{ height: 'var(--space-6)' }} />
<DTFeatureLegend features={magnetFeatures} title="BIOMAGNET FEATURES" variant="emphasis" columns={4} />
<div style={{ height: 'var(--space-6)' }} />
<DTFeatureLegend features={aestheticFeatures} title="AESTHETIC FEATURES" variant="other" columns={2} />
<div style={{ height: 'var(--space-4)' }} />
<CodeLabel text="<DTFeatureLegend features={chipFeatures} title='NFC CHIP FEATURES' variant='normal' />" />
<CodeLabel text="<DTFeatureLegend features={magnetFeatures} variant='emphasis' columns={4} />" />
<CodeLabel text="<DTFeatureLegend features={aestheticFeatures} variant='other' columns={2} />" />
<div style={{ marginTop: 'var(--space-3)', display: 'flex', gap: 'var(--space-4)', fontSize: '0.75rem' }}>
<span><span style={{ color: 'var(--mode-normal)' }}></span> Supported</span>
<span><span style={{ color: 'var(--mode-emphasis)' }}></span> Disabled</span>
<span><span style={{ color: 'var(--mode-warning)' }}></span> Unsupported</span>
</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

@@ -0,0 +1,122 @@
import { useState } from 'react';
import {
DTTextInput,
DTCheckbox,
DTSwitch,
DTRadioGroup,
DTProgressBar,
DTAccordion,
DTQuantityStepper,
} from '@dangerousthings/react';
import type { DTAccordionSection } from '@dangerousthings/react';
import { Section, Row, CodeLabel, DemoLabel } from '../components/Section';
export function FormsPage() {
const [checks, setChecks] = useState({ a: false, b: true, c: true, d: false });
const [switches, setSwitches] = useState({ a: false, b: true, c: true, d: false });
const [radioValue, setRadioValue] = useState('nfc');
const [stepperValue, setStepperValue] = useState(1);
const accordionSections: DTAccordionSection[] = [
{ key: 'size', title: 'Size', children: <div style={{ padding: 'var(--space-4)' }}>Small, Medium, Large options available.</div> },
{ key: 'chip', title: 'Chip Type', children: <div style={{ padding: 'var(--space-4)' }}>NTAG, DESFire, MIFARE Classic.</div> },
{ key: 'freq', title: 'Frequency', children: <div style={{ padding: 'var(--space-4)' }}>13.56 MHz (HF), 125 kHz (LF).</div> },
];
return (
<>
<h1 className="page-title">Forms</h1>
<p className="page-subtitle">DT-branded form components using @dangerousthings/react.</p>
<Section title="Text Input" description="Sharp corners + focus glow bar. 2px solid border, no border-radius.">
<DTTextInput placeholder="Normal input — click to focus" style={{ maxWidth: 400 }} />
<CodeLabel text="<DTTextInput /> — focus glow bar" />
<br />
<DTTextInput error placeholder="Error state input" style={{ maxWidth: 400 }} />
<CodeLabel text="<DTTextInput error /> — red focus bar" />
<br />
<textarea placeholder="Textarea element" style={{ maxWidth: 400, minHeight: 80 }} />
<CodeLabel text="textarea — same styling" />
</Section>
<Section title="Checkbox" description="Beveled hexagon shape via clip-path. 30% corner cuts.">
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<DTCheckbox checked={checks.a} onChange={v => setChecks(p => ({ ...p, a: v }))} label="Unchecked checkbox" />
<DTCheckbox checked={checks.b} onChange={v => setChecks(p => ({ ...p, b: v }))} label="Checked checkbox" />
<DTCheckbox checked={checks.c} onChange={() => {}} disabled label="Disabled checked" />
<DTCheckbox checked={checks.d} onChange={() => {}} disabled label="Disabled unchecked" />
</div>
<CodeLabel text="<DTCheckbox checked onChange label />" />
</Section>
<Section title="Switch / Toggle" description="Angular track + sliding square thumb. 48x26 track, 20x20 thumb.">
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<DTSwitch checked={switches.a} onChange={v => setSwitches(p => ({ ...p, a: v }))} label="Off" />
<DTSwitch checked={switches.b} onChange={v => setSwitches(p => ({ ...p, b: v }))} label="On" />
<DTSwitch checked={switches.c} onChange={() => {}} disabled label="Disabled (on)" />
<DTSwitch checked={switches.d} onChange={() => {}} disabled label="Disabled (off)" />
</div>
<CodeLabel text="<DTSwitch checked onChange label />" />
</Section>
<Section title="Radio Button" description="Hexagonal indicator via clip-path. Flat-top hexagon shape.">
<DTRadioGroup
options={[
{ value: 'nfc', label: 'NFC (Near Field Communication)' },
{ value: 'rfid', label: 'RFID (Radio Frequency ID)' },
{ value: 'ble', label: 'BLE (Bluetooth Low Energy)' },
]}
value={radioValue}
onChange={setRadioValue}
/>
<CodeLabel text="<DTRadioGroup options value onChange />" />
</Section>
<Section title="Progress Bar" description="Angular, no border-radius. 4px default height.">
<DemoLabel text="25%" />
<DTProgressBar value={0.25} />
<DemoLabel text="50% WITH LABEL" />
<DTProgressBar value={0.5} label="50%" />
<DemoLabel text="75%" />
<DTProgressBar value={0.75} />
<DemoLabel text="100%" />
<DTProgressBar value={1.0} label="100%" />
<CodeLabel text="<DTProgressBar value={0.5} label='50%' />" />
</Section>
<Section title="Accordion" description="Click headers to expand. 5px top border, chevron rotation.">
<DTAccordion sections={accordionSections} />
<CodeLabel text="<DTAccordion sections={[{ key, title, children }]} />" />
</Section>
<Section title="Quantity Stepper" description="Beveled +/- buttons with center display.">
<DTQuantityStepper value={stepperValue} onChange={setStepperValue} min={0} max={10} />
<CodeLabel text="<DTQuantityStepper value onChange min max />" />
</Section>
<Section title="Filter Header" description="Thick top border accent. Active state switches to secondary color.">
<div style={{ maxWidth: 400 }}>
<button className="dt-filter-header" type="button">FILTER CATEGORY</button>
<button className="dt-filter-header active" type="button">ACTIVE FILTER</button>
</div>
<CodeLabel text=".dt-filter-header | .dt-filter-header.active" />
</Section>
<Section title="Menu Items" description="Beveled filter menu items with active state and level indentation.">
<div style={{ maxWidth: 300 }}>
{['All Products', 'NFC Implants', 'RFID Tags', 'Accessories', 'Lab Products'].map((name, i) => (
<button
key={name}
className={`dt-menu-item${i === 0 ? ' active' : ''}${i === 4 ? ' mode-warning' : ''}`}
style={(i === 2 || i === 3) ? { '--dt-menu-level': '1' } as React.CSSProperties : undefined}
type="button"
>
{name}
</button>
))}
</div>
<CodeLabel text=".dt-menu-item | .active | --dt-menu-level: 1" />
</Section>
</>
);
}

View File

@@ -0,0 +1,81 @@
import { DTCard } from '@dangerousthings/react';
import { Section, Row, CodeLabel } from '../components/Section';
export function GlowsPage() {
return (
<>
<h1 className="page-title">Glows</h1>
<p className="page-subtitle">Neon drop-shadow and text-shadow effects. Active on the DT brand.</p>
<Section title="Button Glows" description="filter: drop-shadow() follows clip-path shape. Hover for enhanced glow.">
<Row>
<button className="btn-primary" type="button">PRIMARY GLOW</button>
<button className="btn-danger" type="button">DANGER GLOW</button>
</Row>
<CodeLabel text={'.btn-primary / .btn-danger — automatic on [data-brand="dt"]'} />
<br />
<Row>
<button className="btn-secondary" type="button">SECONDARY (HOVER)</button>
</Row>
<CodeLabel text=".btn-secondary — glow on hover" />
</Section>
<Section title="Link Glow" description="text-shadow on hover. Respects --dt-glow-color when set by a parent mode.">
<Row>
<a href="#" style={{ color: 'var(--color-primary)', fontWeight: 600, fontSize: '1.125rem' }}>
Hover this link for text glow
</a>
</Row>
<CodeLabel text="a:hover — uses var(--dt-glow-color, var(--color-primary))" />
</Section>
<Section title="Mode-Aware Link Glow" description="Links inside mode containers glow with that mode's color.">
<Row>
{(['normal', 'emphasis', 'warning', 'success', 'other'] as const).map(mode => (
<div key={mode} className={`mode-${mode}`} style={{ padding: 'var(--space-3)' }}>
<a href="#" style={{ fontWeight: 600, fontSize: '1rem' }}>{mode.toUpperCase()}</a>
</div>
))}
</Row>
<CodeLabel text=".mode-emphasis a:hover — glows yellow via --dt-glow-color" />
</Section>
<Section title="Terminal Inset Glow" description="Inset + outer box-shadow. No clip-path so box-shadow works directly.">
<div className="terminal dt-accent-top">
<code>{'$ dt-web-theme --install\n> Installing design tokens...\n> Applying cyberpunk aesthetic...\n> Done. Welcome to the future.'}</code>
</div>
<CodeLabel text={'.terminal — automatic on [data-brand="dt"]'} />
</Section>
<Section title="Card Hover Glow" description="filter: drop-shadow() respects the beveled clip-path.">
<DTCard title="HOVER ME" style={{ cursor: 'pointer' }}>
<div className="card-body">Cards get a drop-shadow glow on hover.</div>
</DTCard>
<CodeLabel text={'.card:hover — automatic on [data-brand="dt"]'} />
</Section>
<Section title="Input Focus Glow" description="box-shadow: 0 4px 0 1px — bright bar beneath the input.">
<input
type="text"
className="input"
placeholder="Click to focus — see the glow bar"
style={{ maxWidth: 400 }}
/>
<CodeLabel text={'.input:focus — automatic on [data-brand="dt"]'} />
</Section>
<Section title="Glow Utilities" description="Generic utility classes for applying glow effects to any element.">
<Row>
<div className="dt-glow" style={{ background: 'var(--color-primary)', padding: 'var(--space-4) var(--space-6)', display: 'inline-block', fontWeight: 700, color: 'var(--color-bg)' }}>.dt-glow</div>
<div className="dt-glow-strong" style={{ background: 'var(--color-primary)', padding: 'var(--space-4) var(--space-6)', display: 'inline-block', fontWeight: 700, color: 'var(--color-bg)' }}>.dt-glow-strong</div>
</Row>
<CodeLabel text=".dt-glow | .dt-glow-strong" />
<Row>
<div className="dt-glow-inset" style={{ padding: 'var(--space-4) var(--space-6)', display: 'inline-block', fontWeight: 700, border: '1px solid var(--color-border)' }}>.dt-glow-inset</div>
<div className="dt-text-glow" style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-primary)', display: 'inline-block' }}>.dt-text-glow</div>
</Row>
<CodeLabel text=".dt-glow-inset | .dt-text-glow" />
</Section>
</>
);
}

View File

@@ -0,0 +1,103 @@
import { DTCard, DTStaggerContainer } 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: 'glows', title: 'Glows', desc: 'Neon drop-shadow and text-shadow effects for the DT brand', mode: 'emphasis', count: 6 },
{ 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: '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 },
];
function HexDecor({ color, size = 14, opacity = 0.5 }: { color: string; size?: number; opacity?: number }) {
return (
<svg width={size * 2} height={size * 2} viewBox="0 0 28 28" style={{ opacity }}>
<polygon
points="14,1 25.5,7.5 25.5,20.5 14,27 2.5,20.5 2.5,7.5"
fill={color}
/>
</svg>
);
}
export function HomePage() {
return (
<>
<div style={{ textAlign: 'center', marginBottom: 'var(--space-8)' }}>
<h1 style={{
color: 'var(--color-primary)',
fontSize: '2.5rem',
fontWeight: 900,
letterSpacing: '0.15em',
marginBottom: '0.25rem',
textTransform: 'uppercase',
}}>
DANGEROUS THINGS
</h1>
<p style={{
color: 'var(--mode-emphasis, var(--color-secondary))',
fontSize: '1.25rem',
fontWeight: 700,
letterSpacing: '0.2em',
textTransform: 'uppercase',
marginTop: 0,
}}>
DESIGN SYSTEM
</p>
<p style={{
color: 'var(--color-text-muted)',
fontSize: '0.8rem',
marginTop: 'var(--space-2)',
}}>
React component showcase switch brands and modes in the sidebar
</p>
<div style={{
display: 'flex',
justifyContent: 'center',
gap: 12,
marginTop: 'var(--space-6)',
}}>
<HexDecor color="var(--mode-normal, var(--color-primary))" opacity={0.4} />
<HexDecor color="var(--mode-emphasis, var(--color-secondary))" opacity={0.6} />
<HexDecor color="var(--mode-warning, var(--color-error))" opacity={0.4} />
<HexDecor color="var(--mode-success, var(--color-accent))" opacity={0.6} />
<HexDecor color="var(--mode-other, var(--color-other))" opacity={0.4} />
</div>
</div>
<p style={{
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">
{categories.map(cat => (
<a key={cat.hash} href={`#/${cat.hash}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<DTCard variant={cat.mode} title={cat.title.toUpperCase()}>
<div className="card-body">{cat.desc}</div>
{cat.count > 0 && (
<div style={{ color: 'var(--color-text-muted)', fontSize: '0.7rem', marginTop: 'var(--space-2)' }}>
{cat.count} components
</div>
)}
</DTCard>
</a>
))}
</DTStaggerContainer>
<Section title="Quick Start" description="Import the React components and CSS to get started.">
<div className="terminal dt-accent-top">
<code>{`import { DTCard, DTButton } from '@dangerousthings/react';\nimport '@dangerousthings/web/index.css';`}</code>
</div>
</Section>
</>
);
}

View File

@@ -0,0 +1,99 @@
import { brands } from '@dangerousthings/tokens';
import type { ThemeBrand } from '@dangerousthings/tokens';
import { Section, CodeLabel } from '../components/Section';
interface TokensPageProps {
brand: ThemeBrand;
}
export function TokensPage({ brand: brandId }: TokensPageProps) {
const brand = brands[brandId];
if (!brand) return null;
const darkColors = brand.dark;
const darkEntries = Object.entries(darkColors) as [string, string][];
const lightColors = brand.light;
const lightEntries = Object.entries(lightColors) as [string, string][];
return (
<>
<h1 className="page-title">Tokens</h1>
<p className="page-subtitle">Design token values for the active brand. Switch brands in the sidebar.</p>
<Section title={`Color Palette — ${brand.name} (Dark Mode)`} description={brand.description}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 'var(--space-3)' }}>
{darkEntries.map(([name, hex]) => (
<div key={name} style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
<div style={{ width: 32, height: 32, background: hex, border: '1px solid rgba(255,255,255,0.15)', flexShrink: 0 }} />
<div>
<div style={{ fontWeight: 600, fontSize: '0.8125rem' }}>{name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.6875rem', opacity: 0.5 }}>{hex}</div>
</div>
</div>
))}
</div>
</Section>
<Section title={`Color Palette — ${brand.name} (Light Mode)`} description="Light mode color tokens.">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 'var(--space-3)' }}>
{lightEntries.map(([name, hex]) => (
<div key={name} style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
<div style={{ width: 32, height: 32, background: hex, border: '1px solid rgba(255,255,255,0.15)', flexShrink: 0 }} />
<div>
<div style={{ fontWeight: 600, fontSize: '0.8125rem' }}>{name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.6875rem', opacity: 0.5 }}>{hex}</div>
</div>
</div>
))}
</div>
</Section>
<Section title="Typography" description="Font family tokens for headings, body text, and monospace.">
<div className="terminal" style={{ marginBottom: 'var(--space-4)' }}>
<code>{`heading: ${brand.typography.heading}\nbody: ${brand.typography.body}\nmono: ${brand.typography.mono}`}</code>
</div>
</Section>
<Section title="Typography Scale" description="Live rendering with the current brand's font families.">
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
<h1 style={{ fontFamily: 'var(--font-heading)', fontSize: '2rem', fontWeight: 900 }}>Heading 1</h1>
<h2 style={{ fontFamily: 'var(--font-heading)', fontSize: '1.5rem', fontWeight: 700 }}>Heading 2</h2>
<h3 style={{ fontFamily: 'var(--font-heading)', fontSize: '1.25rem', fontWeight: 600 }}>Heading 3</h3>
<p style={{ fontFamily: 'var(--font-body)', fontSize: '1rem' }}>Body text the quick brown fox jumps over the lazy dog</p>
<code style={{ fontFamily: 'var(--font-mono)', fontSize: '0.875rem' }}>{'const monospace = "code";'}</code>
</div>
</Section>
<Section title="Shape Tokens" description="Bevel and radius values determine the visual language — angular (DT) vs rounded (Classic).">
<div className="terminal" style={{ marginBottom: 'var(--space-4)' }}>
<code>{`bevelSm: ${brand.shape.bevelSm}\nbevelMd: ${brand.shape.bevelMd}\nbevelLg: ${brand.shape.bevelLg}\nradiusSm: ${brand.shape.radiusSm}\nradius: ${brand.shape.radius}\nradiusLg: ${brand.shape.radiusLg}`}</code>
</div>
</Section>
<Section title="Spacing Scale" description="Shared spacing tokens across all brands.">
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
{[
{ name: '--space-1', value: '0.25rem (4px)', width: '1.5rem' },
{ name: '--space-2', value: '0.5rem (8px)', width: '3rem' },
{ name: '--space-3', value: '0.75rem (12px)', width: '4.5rem' },
{ name: '--space-4', value: '1rem (16px)', width: '6rem' },
{ name: '--space-6', value: '1.5rem (24px)', width: '9rem' },
{ name: '--space-8', value: '2rem (32px)', width: '12rem' },
].map(sp => (
<div key={sp.name} style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
<div style={{ width: sp.width, height: 12, background: 'var(--color-primary)', opacity: 0.6 }} />
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', opacity: 0.6 }}>{sp.name}: {sp.value}</span>
</div>
))}
</div>
</Section>
<Section title="CSS Custom Properties" description="These are generated from TypeScript tokens and applied per-brand via data-brand attribute.">
<div className="terminal">
<code>{`/* Generated CSS custom properties */\n--color-bg: ${darkColors.bg};\n--color-primary: ${darkColors.primary};\n--color-secondary: ${darkColors.secondary};\n--color-error: ${darkColors.error};\n--color-success: ${darkColors.success};\n--bevel-sm: ${brand.shape.bevelSm};\n--bevel-md: ${brand.shape.bevelMd};\n--font-heading: ${brand.typography.heading};`}</code>
</div>
<CodeLabel text="See packages/tokens/src/scripts/generate-css.ts" />
</Section>
</>
);
}

View File

@@ -1,126 +0,0 @@
import { el, section, row, label, codeLabel } from '../utils/dom';
export function renderBevelsPage(container: HTMLElement) {
container.appendChild(el('h1', { className: 'page-title' }, 'Bevels'));
container.appendChild(el('p', { className: 'page-subtitle' }, 'Angular clip-path patterns from the DT design system. Active on the DT brand.'));
// Card Bevels
const card1 = el('div', { className: 'card', style: 'padding: var(--space-6);' });
card1.appendChild(el('div', { className: 'card-title' }, 'CARD TITLE'));
card1.appendChild(el('div', { className: 'card-body' }, 'Dual bottom bevels — bottom-right (bevel-md) and bottom-left (bevel-sm). Uses the dual-element technique with ::before pseudo-element.'));
const card2 = el('div', { className: 'card', style: 'padding: var(--space-6);' });
card2.appendChild(el('div', { className: 'card-body' }, 'Card without title — no header bar, just the beveled container.'));
container.appendChild(
section('Card Bevels', 'Dual bottom bevels using clip-path. Outer bg = border color, ::before = surface fill.',
card1,
codeLabel('.card or .dt-bevel-card'),
card2,
codeLabel('.card (no .card-title child)'),
),
);
// Button Bevels
container.appendChild(
section('Button Bevels', 'Top-right corner cut. Filled buttons use direct clip-path, outline uses dual-element.',
row(
el('button', { className: 'btn-primary' }, 'PRIMARY'),
el('button', { className: 'btn-secondary' }, 'SECONDARY'),
el('button', { className: 'btn-danger' }, 'DANGER'),
),
codeLabel('.btn-primary | .btn-secondary | .btn-danger'),
),
);
// Badge Bevels
container.appendChild(
section('Badge Bevels', 'Top-right bevel with dual-element technique. Color variants via CSS custom properties.',
row(
el('span', { className: 'badge' }, 'Default'),
el('span', { className: 'badge badge-success' }, 'Success'),
el('span', { className: 'badge badge-error' }, 'Error'),
el('span', { className: 'badge badge-warning' }, 'Warning'),
el('span', { className: 'badge badge-info' }, 'Info'),
),
codeLabel('.badge | .badge-success | .badge-error | .badge-warning | .badge-info'),
),
);
// Media Frame
const mediaFrame = el('div', {
className: 'dt-bevel-media',
style: 'background: var(--color-primary); width: 100%; aspect-ratio: 16/9; display: flex; align-items: center; justify-content: center;',
});
mediaFrame.appendChild(el('span', { style: 'color: var(--color-bg); font-weight: 700;' }, 'MEDIA FRAME'));
container.appendChild(
section('Media Frame Bevels', 'Diagonal opposite corners (top-left + bottom-right).',
mediaFrame,
codeLabel('.dt-bevel-media'),
),
);
// Modal Bevel
const modalDemo = el('div', {
className: 'dt-bevel-modal',
style: 'background: var(--color-primary); padding: var(--space-8); text-align: center;',
});
const modalInner = el('div', { style: 'background: var(--color-surface); padding: var(--space-6);' });
modalInner.appendChild(el('div', { style: 'font-weight: 700; text-transform: uppercase;' }, 'Modal Content'));
modalInner.appendChild(el('div', { style: 'color: var(--color-text-muted); margin-top: var(--space-2);' }, 'Large dual bottom bevels'));
modalDemo.appendChild(modalInner);
container.appendChild(
section('Modal Bevels', 'Dual bottom bevels at bevel-lg scale.',
modalDemo,
codeLabel('.dt-bevel-modal'),
),
);
// Drawer Bevels
const drawerRight = el('div', {
className: 'dt-bevel-drawer-right',
style: 'background: var(--color-primary); padding: var(--space-6); width: 200px; height: 120px; display: flex; align-items: center; justify-content: center;',
});
drawerRight.appendChild(el('span', { style: 'color: var(--color-bg); font-weight: 700;' }, 'RIGHT'));
const drawerLeft = el('div', {
className: 'dt-bevel-drawer-left',
style: 'background: var(--color-primary); padding: var(--space-6); width: 200px; height: 120px; display: flex; align-items: center; justify-content: center;',
});
drawerLeft.appendChild(el('span', { style: 'color: var(--color-bg); font-weight: 700;' }, 'LEFT'));
container.appendChild(
section('Drawer Bevels', 'Exposed-edge bevels for sliding panels.',
row(drawerRight, drawerLeft),
codeLabel('.dt-bevel-drawer-right | .dt-bevel-drawer-left'),
),
);
// Small Bevel Utility
const smallBevel = el('div', {
className: 'dt-bevel-sm',
style: 'background: var(--color-primary); width: 60px; height: 60px; display: flex; align-items: center; justify-content: center;',
});
smallBevel.appendChild(el('span', { style: 'color: var(--color-bg); font-weight: 700; font-size: 0.75rem;' }, 'SM'));
container.appendChild(
section('Small Bevel Utility', 'For compact elements — arrows, stepper buttons.',
row(smallBevel),
codeLabel('.dt-bevel-sm'),
),
);
// Accent Top
const accentDemo = el('div', {
className: 'dt-accent-top',
style: 'padding: var(--space-4); background: var(--color-surface); border: 1px solid var(--color-border);',
});
accentDemo.appendChild(el('span', { style: 'font-weight: 600; text-transform: uppercase;' }, 'Thick Top Border Accent'));
container.appendChild(
section('Accent Top', 'Used on accordion headers and menu items.',
accentDemo,
codeLabel('.dt-accent-top'),
),
);
}

View File

@@ -1,199 +0,0 @@
import { el, section, row, label, codeLabel } from '../utils/dom';
export function renderFormsPage(container: HTMLElement) {
container.appendChild(el('h1', { className: 'page-title' }, 'Forms'));
container.appendChild(el('p', { className: 'page-subtitle' }, 'DT-branded form components. Angular styling with focus effects.'));
// Text Inputs
const normalInput = el('input', { type: 'text', placeholder: 'Normal input — click to focus', style: 'max-width: 400px;' }) as HTMLInputElement;
const errorInput = el('input', { type: 'text', className: 'error', placeholder: 'Error state input', style: 'max-width: 400px;' }) as HTMLInputElement;
const textarea = el('textarea', { placeholder: 'Textarea element', style: 'max-width: 400px; min-height: 80px;' }) as HTMLTextAreaElement;
container.appendChild(
section('Text Input', 'Sharp corners + focus glow bar. 2px solid border, no border-radius.',
normalInput,
codeLabel('input[type="text"] — focus glow bar on [data-brand="dt"]'),
el('br'),
errorInput,
codeLabel('input.error — red focus bar on [data-brand="dt"]'),
el('br'),
textarea,
codeLabel('textarea — same styling'),
),
);
// Checkboxes
function checkbox(labelText: string, checked: boolean, disabled = false): HTMLElement {
const wrapper = el('label', { style: 'display: flex; align-items: center; gap: 12px; cursor: pointer; padding: 4px 0;' });
const input = document.createElement('input');
input.type = 'checkbox';
if (checked) input.checked = true;
if (disabled) input.disabled = true;
wrapper.appendChild(input);
wrapper.appendChild(el('span', {}, labelText));
return wrapper;
}
container.appendChild(
section('Checkbox', 'Beveled hexagon shape via clip-path. 30% corner cuts.',
checkbox('Unchecked checkbox', false),
checkbox('Checked checkbox', true),
checkbox('Disabled checked', true, true),
checkbox('Disabled unchecked', false, true),
codeLabel('input[type="checkbox"] — hex bevel on [data-brand="dt"]'),
),
);
// Switch / Toggle
function switchEl(labelText: string, checked: boolean, disabled = false): HTMLElement {
const wrapper = el('div', { style: 'display: flex; align-items: center; gap: 12px; margin-bottom: 8px;' });
const sw = el('label', { className: 'dt-switch' });
const input = document.createElement('input');
input.type = 'checkbox';
if (checked) input.checked = true;
if (disabled) input.disabled = true;
sw.appendChild(input);
sw.appendChild(el('div', { className: 'dt-switch-track' }));
sw.appendChild(el('div', { className: 'dt-switch-thumb' }));
wrapper.appendChild(sw);
wrapper.appendChild(el('span', {}, labelText));
return wrapper;
}
container.appendChild(
section('Switch / Toggle', 'Angular track + sliding square thumb. 48x26 track, 20x20 thumb.',
switchEl('Off', false),
switchEl('On', true),
switchEl('Disabled (on)', true, true),
switchEl('Disabled (off)', false, true),
codeLabel('.dt-switch > input + .dt-switch-track + .dt-switch-thumb'),
),
);
// Radio Buttons
function radioGroup(name: string, options: { value: string; label: string; checked?: boolean; disabled?: boolean }[]): HTMLElement {
const group = el('div', { style: 'display: flex; flex-direction: column; gap: 4px;' });
for (const opt of options) {
const wrapper = el('label', { className: `dt-radio-option${opt.checked ? ' selected' : ''}`, style: 'cursor: pointer;' });
const input = document.createElement('input');
input.type = 'radio';
input.name = name;
input.value = opt.value;
if (opt.checked) input.checked = true;
if (opt.disabled) input.disabled = true;
input.addEventListener('change', () => {
group.querySelectorAll('.dt-radio-option').forEach((o) => o.classList.remove('selected'));
if (input.checked) wrapper.classList.add('selected');
});
wrapper.appendChild(input);
wrapper.appendChild(el('span', {}, opt.label));
group.appendChild(wrapper);
}
return group;
}
container.appendChild(
section('Radio Button', 'Hexagonal indicator via clip-path. Flat-top hexagon shape.',
radioGroup('demo-radio', [
{ value: 'nfc', label: 'NFC (Near Field Communication)', checked: true },
{ value: 'rfid', label: 'RFID (Radio Frequency ID)' },
{ value: 'ble', label: 'BLE (Bluetooth Low Energy)' },
]),
codeLabel('input[type="radio"] — hex indicator on [data-brand="dt"]'),
),
);
// Progress Bar
function progressBar(value: number, showLabel = false): HTMLElement {
const wrapper = el('div');
const bar = el('div', { className: 'dt-progress' });
const fill = el('div', { className: 'dt-progress-fill', style: `width: ${value * 100}%;` });
bar.appendChild(fill);
wrapper.appendChild(bar);
if (showLabel) {
wrapper.appendChild(el('div', { className: 'dt-progress-label' }, `${Math.round(value * 100)}%`));
}
return wrapper;
}
container.appendChild(
section('Progress Bar', 'Angular, no border-radius. 4px default height.',
label('25%'),
progressBar(0.25),
label('50% WITH LABEL'),
progressBar(0.5, true),
label('75%'),
progressBar(0.75),
label('100%'),
progressBar(1.0, true),
codeLabel('.dt-progress > .dt-progress-fill'),
),
);
// Accordion
function accordion(items: { title: string; content: string }[]): HTMLElement {
const acc = el('div', { className: 'dt-accordion' });
for (const item of items) {
const header = el('button', { className: 'dt-accordion-header', 'aria-expanded': 'false' });
header.appendChild(document.createTextNode(item.title));
header.appendChild(el('span', { className: 'dt-accordion-chevron' }));
const content = el('div', { className: 'dt-accordion-content', 'data-open': 'false' });
content.appendChild(el('div', { style: 'padding: var(--space-4);' }, item.content));
header.addEventListener('click', () => {
const expanded = header.getAttribute('aria-expanded') === 'true';
header.setAttribute('aria-expanded', String(!expanded));
content.setAttribute('data-open', String(!expanded));
});
acc.appendChild(header);
acc.appendChild(content);
}
return acc;
}
container.appendChild(
section('Accordion', 'Click headers to expand. 5px top border, chevron rotation.',
accordion([
{ title: 'Size', content: 'Small, Medium, Large options available.' },
{ title: 'Chip Type', content: 'NTAG, DESFire, MIFARE Classic.' },
{ title: 'Frequency', content: '13.56 MHz (HF), 125 kHz (LF).' },
]),
codeLabel('.dt-accordion > .dt-accordion-header + .dt-accordion-content'),
),
);
// Quantity Stepper
function stepper(initialValue: number, min: number, max: number): HTMLElement {
let value = initialValue;
const wrapper = el('div', { className: 'dt-stepper' });
const minusBtn = el('button', { className: 'dt-stepper-btn' }, '\u2212') as HTMLButtonElement;
const valueEl = el('span', { className: 'dt-stepper-value' }, String(value));
const plusBtn = el('button', { className: 'dt-stepper-btn' }, '+') as HTMLButtonElement;
function update() {
valueEl.textContent = String(value);
minusBtn.disabled = value <= min;
plusBtn.disabled = value >= max;
}
minusBtn.addEventListener('click', () => { value = Math.max(min, value - 1); update(); });
plusBtn.addEventListener('click', () => { value = Math.min(max, value + 1); update(); });
wrapper.appendChild(minusBtn);
wrapper.appendChild(valueEl);
wrapper.appendChild(plusBtn);
update();
return wrapper;
}
container.appendChild(
section('Quantity Stepper', 'Beveled +/- buttons with center display.',
stepper(1, 0, 10),
codeLabel('.dt-stepper > .dt-stepper-btn + .dt-stepper-value + .dt-stepper-btn'),
),
);
}

View File

@@ -1,97 +0,0 @@
import { el, section, row, codeLabel } from '../utils/dom';
export function renderGlowsPage(container: HTMLElement) {
container.appendChild(el('h1', { className: 'page-title' }, 'Glows'));
container.appendChild(el('p', { className: 'page-subtitle' }, 'Neon drop-shadow and text-shadow effects. Active on the DT brand.'));
// Button Glows
container.appendChild(
section('Button Glows', 'filter: drop-shadow() follows clip-path shape. Hover for enhanced glow.',
row(
el('button', { className: 'btn-primary' }, 'PRIMARY GLOW'),
el('button', { className: 'btn-danger' }, 'DANGER GLOW'),
),
codeLabel('.btn-primary / .btn-danger — automatic on [data-brand="dt"]'),
el('br'),
row(
el('button', { className: 'btn-secondary' }, 'SECONDARY (HOVER)'),
),
codeLabel('.btn-secondary — glow on hover'),
),
);
// Link Glow
const link = el('a', { href: '#', style: 'color: var(--color-primary); font-weight: 600; font-size: 1.125rem;' }, 'Hover this link for text glow');
container.appendChild(
section('Link Glow', 'text-shadow on hover for anchor elements.',
row(link),
codeLabel('a:hover — automatic on [data-brand="dt"]'),
),
);
// Terminal Inset
const terminal = el('div', { className: 'terminal dt-accent-top' });
terminal.appendChild(el('code', {}, '$ dt-web-theme --install\n> Installing design tokens...\n> Applying cyberpunk aesthetic...\n> Done. Welcome to the future.'));
container.appendChild(
section('Terminal Inset Glow', 'Inset + outer box-shadow. No clip-path so box-shadow works directly.',
terminal,
codeLabel('.terminal — automatic on [data-brand="dt"]'),
),
);
// Card Hover Glow
const card = el('div', { className: 'card', style: 'padding: var(--space-6); cursor: pointer;' });
card.appendChild(el('div', { className: 'card-title' }, 'HOVER ME'));
card.appendChild(el('div', { className: 'card-body' }, 'Cards get a drop-shadow glow on hover.'));
container.appendChild(
section('Card Hover Glow', 'filter: drop-shadow() respects the beveled clip-path.',
card,
codeLabel('.card:hover — automatic on [data-brand="dt"]'),
),
);
// Input Focus Glow
const input = el('input', {
type: 'text',
className: 'input',
placeholder: 'Click to focus — see the glow bar',
style: 'max-width: 400px;',
}) as HTMLInputElement;
container.appendChild(
section('Input Focus Glow', 'box-shadow: 0 4px 0 1px — bright bar beneath the input.',
input,
codeLabel('.input:focus — automatic on [data-brand="dt"]'),
),
);
// Generic Glow Utilities
const glowBox = el('div', {
className: 'dt-glow',
style: 'background: var(--color-primary); padding: var(--space-4) var(--space-6); display: inline-block; font-weight: 700; color: var(--color-bg);',
}, '.dt-glow');
const glowStrong = el('div', {
className: 'dt-glow-strong',
style: 'background: var(--color-primary); padding: var(--space-4) var(--space-6); display: inline-block; font-weight: 700; color: var(--color-bg);',
}, '.dt-glow-strong');
const glowInset = el('div', {
className: 'dt-glow-inset',
style: 'padding: var(--space-4) var(--space-6); display: inline-block; font-weight: 700; border: 1px solid var(--color-border);',
}, '.dt-glow-inset');
const textGlow = el('div', {
className: 'dt-text-glow',
style: 'font-size: 1.25rem; font-weight: 700; color: var(--color-primary); display: inline-block;',
}, '.dt-text-glow');
container.appendChild(
section('Glow Utilities', 'Generic utility classes for applying glow effects to any element.',
row(glowBox, glowStrong),
codeLabel('.dt-glow | .dt-glow-strong'),
row(glowInset, textGlow),
codeLabel('.dt-glow-inset | .dt-text-glow'),
),
);
}

View File

@@ -1,33 +0,0 @@
import { el, section } from '../utils/dom';
export function renderHomePage(container: HTMLElement) {
container.appendChild(el('h1', { className: 'page-title' }, 'DT Design System'));
container.appendChild(el('p', { className: 'page-subtitle' }, 'Web CSS component showcase — switch brands and modes in the sidebar.'));
const categories = [
{ hash: 'bevels', title: 'Bevels', desc: 'Angular clip-path patterns — cards, buttons, badges, media frames, modals, drawers' },
{ hash: 'glows', title: 'Glows', desc: 'Neon drop-shadow and text-shadow effects for the DT brand' },
{ hash: 'forms', title: 'Forms', desc: 'Text inputs, checkboxes, switches, radios, progress bars, accordions, steppers' },
{ hash: 'tokens', title: 'Tokens', desc: 'Color palette, typography, spacing, and shape values for the active brand' },
];
for (const cat of categories) {
const link = el('a', { href: `#/${cat.hash}`, className: 'home-card-link' });
const card = el('div', { className: 'card' });
card.appendChild(el('div', { className: 'card-title' }, cat.title.toUpperCase()));
card.appendChild(el('div', { className: 'card-body' }, cat.desc));
link.appendChild(card);
container.appendChild(link);
}
// Quick overview terminal
container.appendChild(
section('Quick Start', 'Include the CSS and set data attributes on your HTML element.',
el('div', { className: 'terminal dt-accent-top' },
el('code', {},
'<link rel="stylesheet" href="@dangerousthings/web">\n<html data-brand="dt" data-theme="dark">'
),
),
),
);
}

View File

@@ -1,141 +0,0 @@
import { brands } from '@dangerousthings/tokens';
import type { ThemeBrand } from '@dangerousthings/tokens';
import { el, section, codeLabel } from '../utils/dom';
import { getCurrentBrand } from '../brand-switcher';
export function renderTokensPage(container: HTMLElement) {
container.appendChild(el('h1', { className: 'page-title' }, 'Tokens'));
container.appendChild(el('p', { className: 'page-subtitle' }, 'Design token values for the active brand. Switch brands in the sidebar.'));
const brandId = getCurrentBrand();
const brand = brands[brandId];
if (!brand) return;
// Color Palette — Dark Mode
const darkColors = brand.dark;
const colorEntries: [string, string][] = Object.entries(darkColors);
const colorsGrid = el('div', { style: 'display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--space-3);' });
for (const [name, hex] of colorEntries) {
const swatch = el('div', { style: 'display: flex; align-items: center; gap: var(--space-3);' });
const colorBox = el('div', { style: `width: 32px; height: 32px; background: ${hex}; border: 1px solid rgba(255,255,255,0.15); flex-shrink: 0;` });
const info = el('div');
info.appendChild(el('div', { style: 'font-weight: 600; font-size: 0.8125rem;' }, name));
info.appendChild(el('div', { style: 'font-family: var(--font-mono); font-size: 0.6875rem; opacity: 0.5;' }, hex));
swatch.appendChild(colorBox);
swatch.appendChild(info);
colorsGrid.appendChild(swatch);
}
container.appendChild(
section(`Color Palette — ${brand.name} (Dark Mode)`, brand.description,
colorsGrid,
),
);
// Light Mode
const lightColors = brand.light;
const lightEntries: [string, string][] = Object.entries(lightColors);
const lightGrid = el('div', { style: 'display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--space-3);' });
for (const [name, hex] of lightEntries) {
const swatch = el('div', { style: 'display: flex; align-items: center; gap: var(--space-3);' });
const colorBox = el('div', { style: `width: 32px; height: 32px; background: ${hex}; border: 1px solid rgba(255,255,255,0.15); flex-shrink: 0;` });
const info = el('div');
info.appendChild(el('div', { style: 'font-weight: 600; font-size: 0.8125rem;' }, name));
info.appendChild(el('div', { style: 'font-family: var(--font-mono); font-size: 0.6875rem; opacity: 0.5;' }, hex));
swatch.appendChild(colorBox);
swatch.appendChild(info);
lightGrid.appendChild(swatch);
}
container.appendChild(
section(`Color Palette — ${brand.name} (Light Mode)`, 'Light mode color tokens.',
lightGrid,
),
);
// Typography
const typo = el('div', { className: 'terminal', style: 'margin-bottom: var(--space-4);' });
typo.appendChild(el('code', {},
`heading: ${brand.typography.heading}\nbody: ${brand.typography.body}\nmono: ${brand.typography.mono}`
));
container.appendChild(
section('Typography', 'Font family tokens for headings, body text, and monospace.',
typo,
),
);
// Typography Scale Demo
const scaleItems = [
{ tag: 'h1', label: 'Heading 1', style: 'font-family: var(--font-heading); font-size: 2rem; font-weight: 900;' },
{ tag: 'h2', label: 'Heading 2', style: 'font-family: var(--font-heading); font-size: 1.5rem; font-weight: 700;' },
{ tag: 'h3', label: 'Heading 3', style: 'font-family: var(--font-heading); font-size: 1.25rem; font-weight: 600;' },
{ tag: 'p', label: 'Body text — the quick brown fox jumps over the lazy dog', style: 'font-family: var(--font-body); font-size: 1rem;' },
{ tag: 'code', label: 'const monospace = "code";', style: 'font-family: var(--font-mono); font-size: 0.875rem;' },
];
const scaleDemo = el('div', { style: 'display: flex; flex-direction: column; gap: var(--space-3);' });
for (const item of scaleItems) {
scaleDemo.appendChild(el(item.tag, { style: item.style }, item.label));
}
container.appendChild(
section('Typography Scale', 'Live rendering with the current brand\'s font families.',
scaleDemo,
),
);
// Shape Tokens
const shape = el('div', { className: 'terminal', style: 'margin-bottom: var(--space-4);' });
shape.appendChild(el('code', {},
`bevelSm: ${brand.shape.bevelSm}\nbevelMd: ${brand.shape.bevelMd}\nbevelLg: ${brand.shape.bevelLg}\nradiusSm: ${brand.shape.radiusSm}\nradius: ${brand.shape.radius}\nradiusLg: ${brand.shape.radiusLg}`
));
container.appendChild(
section('Shape Tokens', 'Bevel and radius values determine the visual language — angular (DT) vs rounded (Classic).',
shape,
),
);
// Spacing Scale
const spacings = [
{ name: '--space-1', value: '0.25rem (4px)' },
{ name: '--space-2', value: '0.5rem (8px)' },
{ name: '--space-3', value: '0.75rem (12px)' },
{ name: '--space-4', value: '1rem (16px)' },
{ name: '--space-6', value: '1.5rem (24px)' },
{ name: '--space-8', value: '2rem (32px)' },
];
const spacingDemo = el('div', { style: 'display: flex; flex-direction: column; gap: var(--space-2);' });
for (const sp of spacings) {
const row = el('div', { style: 'display: flex; align-items: center; gap: var(--space-3);' });
row.appendChild(el('div', { style: `width: ${sp.name.replace('--', 'var(--') + ')'}; width: calc(${sp.value.split(' ')[0]} * 6); height: 12px; background: var(--color-primary); opacity: 0.6;` }));
row.appendChild(el('span', { style: 'font-family: var(--font-mono); font-size: 0.75rem; opacity: 0.6;' }, `${sp.name}: ${sp.value}`));
spacingDemo.appendChild(row);
}
container.appendChild(
section('Spacing Scale', 'Shared spacing tokens across all brands.',
spacingDemo,
),
);
// CSS Custom Properties reference
const cssRef = el('div', { className: 'terminal' });
cssRef.appendChild(el('code', {},
`/* Generated CSS custom properties */\n--color-bg: ${darkColors.bg};\n--color-primary: ${darkColors.primary};\n--color-secondary: ${darkColors.secondary};\n--color-error: ${darkColors.error};\n--color-success: ${darkColors.success};\n--bevel-sm: ${brand.shape.bevelSm};\n--bevel-md: ${brand.shape.bevelMd};\n--font-heading: ${brand.typography.heading};`
));
container.appendChild(
section('CSS Custom Properties', 'These are generated from TypeScript tokens and applied per-brand via data-brand attribute.',
cssRef,
codeLabel('See packages/tokens/src/scripts/generate-css.ts'),
),
);
}

View File

@@ -1,3 +1,6 @@
@tailwind components;
@tailwind utilities;
/* Showcase App Layout */
* {
@@ -228,12 +231,16 @@ body {
opacity: 0.9;
}
/* Cards */
/* Cards — DT brand padding is set in bevels.css via --_card-pad */
.card {
padding: var(--space-6);
margin-bottom: var(--space-4);
}
/* Classic brand: cards need explicit padding (no bevel CSS provides it) */
[data-brand="classic"] .card {
padding: var(--space-6);
}
.card-title {
font-weight: 900;
font-size: 1rem;

View File

@@ -1,46 +0,0 @@
export function el(
tag: string,
attrs?: Record<string, string>,
...children: (string | HTMLElement)[]
): HTMLElement {
const element = document.createElement(tag);
if (attrs) {
for (const [key, value] of Object.entries(attrs)) {
if (key === 'className') {
element.className = value;
} else {
element.setAttribute(key, value);
}
}
}
for (const child of children) {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else {
element.appendChild(child);
}
}
return element;
}
export function section(title: string, description: string, ...children: HTMLElement[]): HTMLElement {
const container = el('div', { className: 'demo-section' });
container.appendChild(el('h2', {}, title));
container.appendChild(el('p', { className: 'demo-description' }, description));
for (const child of children) {
container.appendChild(child);
}
return container;
}
export function row(...children: HTMLElement[]): HTMLElement {
return el('div', { className: 'demo-row' }, ...children);
}
export function label(text: string): HTMLElement {
return el('div', { className: 'demo-label' }, text);
}
export function codeLabel(text: string): HTMLElement {
return el('div', { className: 'code-label' }, text);
}

View File

@@ -1,28 +0,0 @@
export function initRouter(
routes: Record<string, (container: HTMLElement) => void>,
container: HTMLElement,
) {
function navigate() {
const hash = window.location.hash.replace('#/', '').replace('#', '');
const render = routes[hash] || routes[''];
// Update active nav link
document.querySelectorAll('.nav-link').forEach((link) => {
const href = (link as HTMLAnchorElement).getAttribute('href') || '';
const linkHash = href.replace('#/', '').replace('#', '');
link.classList.toggle('active', linkHash === hash);
});
// Clear content safely
while (container.firstChild) {
container.removeChild(container.firstChild);
}
if (render) {
render(container);
}
}
window.addEventListener('hashchange', navigate);
navigate();
}

View File

@@ -0,0 +1,10 @@
import dtPreset from '@dangerousthings/tailwind-preset';
/** @type {import('tailwindcss').Config} */
export default {
presets: [dtPreset],
content: ['./src/renderer/**/*.{tsx,ts,html}'],
corePlugins: {
preflight: false,
},
};

View File

@@ -4,6 +4,7 @@
"outDir": "dist/renderer",
"rootDir": "src/renderer",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"noEmit": true
},
"include": ["src/renderer"]

View File

@@ -1,9 +1,11 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
root: 'src/renderer',
base: './',
plugins: [react()],
build: {
outDir: '../../dist/renderer',
emptyOutDir: true,

View File

@@ -0,0 +1,13 @@
> Why do I have a folder named ".expo" in my project?
The ".expo" folder is created when an Expo project is started using "expo start" command.
> What do the files contain?
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
- "settings.json": contains the server configuration that is used to serve the application manifest.
> Should I commit the ".expo" folder?
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.

View File

@@ -0,0 +1,3 @@
{
"devices": []
}

View File

@@ -46,3 +46,16 @@ export const dropdownItems = [
{ id: 'd3', title: 'Archive', onPress: () => {} },
{ id: 'd4', title: 'Delete', onPress: () => {} },
];
export const featureLegendItems = [
{ key: 'payment', name: 'Payment', icon: null, state: 'supported' as const },
{ key: 'access', name: 'Access', icon: null, state: 'supported' as const },
{ key: 'clone', name: 'Clone', icon: null, state: 'disabled' as const },
{ key: 'crypto', name: 'Crypto', icon: null, state: 'unsupported' as const },
{ key: 'sensing', name: 'Sensing', icon: null, state: 'supported' as const },
{ key: 'temp', name: 'Temp', icon: null, state: 'supported' as const },
{ key: 'fitness', name: 'Fitness', icon: null, state: 'disabled' as const },
{ key: 'explore', name: 'Explore', icon: null, state: 'supported' as const },
{ key: 'vibration', name: 'Vibration', icon: null, state: 'unsupported' as const },
{ key: 'sharing', name: 'Sharing', icon: null, state: 'supported' as const },
];

View File

@@ -10,6 +10,9 @@ import { FormsScreen } from '../screens/FormsScreen';
import { FeedbackScreen } from '../screens/FeedbackScreen';
import { OverlaysScreen } from '../screens/OverlaysScreen';
import { ThemeScreen } from '../screens/ThemeScreen';
import { AnimationsScreen } from '../screens/AnimationsScreen';
import { CardsAdvancedScreen } from '../screens/CardsAdvancedScreen';
import { FiltersScreen } from '../screens/FiltersScreen';
const Stack = createNativeStackNavigator<RootStackParamList>();
@@ -42,6 +45,11 @@ export function RootNavigator() {
component={CardsScreen}
options={{ title: 'CARDS & LABELS' }}
/>
<Stack.Screen
name="CardsAdvanced"
component={CardsAdvancedScreen}
options={{ title: 'ADVANCED CARDS' }}
/>
<Stack.Screen
name="Forms"
component={FormsScreen}
@@ -57,6 +65,16 @@ export function RootNavigator() {
component={OverlaysScreen}
options={{ title: 'OVERLAYS & NAV' }}
/>
<Stack.Screen
name="Animations"
component={AnimationsScreen}
options={{ title: 'ANIMATIONS' }}
/>
<Stack.Screen
name="Filters"
component={FiltersScreen}
options={{ title: 'FILTERS & FEATURES' }}
/>
<Stack.Screen
name="Theme"
component={ThemeScreen}

View File

@@ -2,8 +2,11 @@ export type RootStackParamList = {
Home: undefined;
Buttons: undefined;
Cards: undefined;
CardsAdvanced: undefined;
Forms: undefined;
Feedback: undefined;
Overlays: undefined;
Animations: undefined;
Filters: undefined;
Theme: undefined;
};

View File

@@ -0,0 +1,90 @@
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';
import { Animated } from 'react-native';
export function AnimationsScreen() {
const theme = useDTTheme();
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"
variant="success"
description="Scale 0→1 entrance animation hook for individual elements."
>
<Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
<DTCard mode="success" title="SCALE IN">
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
This card used useScaleIn(&#123; duration: 600 &#125;)
</Text>
</DTCard>
</Animated.View>
<CodeLabel text="const scale = useScaleIn({ duration: 600 })" />
</DemoSection>
{/* Pulse Hook */}
<DemoSection
title="usePulse"
variant="warning"
description="Looping opacity pulse animation for active/loading states."
>
<Animated.View style={{ opacity: pulseAnim }}>
<DTLabel primaryText="PULSING LABEL" mode="warning" />
</Animated.View>
<CodeLabel text="const opacity = usePulse(true)" />
</DemoSection>
</ScreenContainer>
);
}

View File

@@ -0,0 +1,111 @@
import React from 'react';
import { View } from 'react-native';
import { Text } from 'react-native-paper';
import {
DTCard,
DTChip,
DTBadgeOverlay,
DTStaggerContainer,
useDTTheme,
} 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';
export function CardsAdvancedScreen() {
const theme = useDTTheme();
const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other'];
return (
<ScreenContainer>
{/* Progress Bar */}
<DemoSection
title="Progress Bar"
variant="normal"
description="Vertical left-edge progress indicator (01)."
>
{[0, 0.25, 0.5, 0.75, 1].map((val, i) => (
<DTCard
key={val}
mode={modes[i]}
title={`${Math.round(val * 100)}% PROGRESS`}
progress={val}
style={{ marginBottom: 12 }}
>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
progress=&#123;{val}&#125;
</Text>
</DTCard>
))}
<CodeLabel text="<DTCard progress={0.5}> — width matches bevelSizeSmall" />
</DemoSection>
{/* Card Badges */}
<DemoSection
title="Card Badges"
variant="other"
description="Bottom-right chip badges. Color matches badge category, not card mode."
>
<DTCard mode="normal" title="PRODUCT CARD" style={{ marginBottom: 16 }}>
<View style={{ minHeight: 40 }}>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
Badge color independent of card mode
</Text>
</View>
<DTBadgeOverlay position="bottom-right">
<DTChip variant="warning">LAB</DTChip>
</DTBadgeOverlay>
</DTCard>
<DTCard mode="emphasis" title="PRODUCT CARD" style={{ marginBottom: 16 }}>
<View style={{ minHeight: 40 }}>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
Badge color independent of card mode
</Text>
</View>
<DTBadgeOverlay position="bottom-right">
<DTChip variant="other">BUNDLE</DTChip>
</DTBadgeOverlay>
</DTCard>
<DTCard mode="success" title="PRODUCT CARD" style={{ marginBottom: 16 }}>
<View style={{ minHeight: 40 }}>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
Badge color independent of card mode
</Text>
</View>
<DTBadgeOverlay position="bottom-right">
<DTChip variant="emphasis">NEW</DTChip>
</DTBadgeOverlay>
</DTCard>
<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

@@ -135,6 +135,20 @@ export function CardsScreen() {
</View>
</DemoSection>
{/* DTCard - Selected & Progress (quick preview) */}
<DemoSection
title="Progress Bar"
variant="warning"
description="Quick preview — see Advanced Cards screen for full demos."
>
<DTCard mode="normal" title="WITH PROGRESS" progress={0.6} style={{ marginBottom: 16 }}>
<Text variant="bodyMedium" style={{ color: theme.colors.onSurface }}>
progress=0.6 vertical left-edge bar
</Text>
</DTCard>
<CodeLabel text="See Advanced Cards for full progress/badge demos" />
</DemoSection>
{/* DTMediaFrame */}
<DemoSection
title="DTMediaFrame"

View File

@@ -0,0 +1,116 @@
import React, { useState } from 'react';
import { View } from 'react-native';
import { Text } from 'react-native-paper';
import {
DTFeatureLegend,
DTMobileFilterOverlay,
DTButton,
DTAccordion,
useDTTheme,
} from '@dangerousthings/react-native';
import type { DTFeatureItem } from '@dangerousthings/react-native';
import { ScreenContainer } from '../components/ScreenContainer';
import { DemoSection } from '../components/DemoSection';
import { CodeLabel } from '../components/CodeLabel';
const sampleFeatures: DTFeatureItem[] = [
{ key: 'payment', name: 'Payment', icon: null, state: 'supported' },
{ key: 'access', name: 'Access', icon: null, state: 'supported' },
{ key: 'clone', name: 'Clone', icon: null, state: 'disabled' },
{ key: 'crypto', name: 'Crypto', icon: null, state: 'unsupported' },
{ key: 'sensing', name: 'Sensing', icon: null, state: 'supported' },
{ key: 'temp', name: 'Temp', icon: null, state: 'supported' },
{ key: 'fitness', name: 'Fitness', icon: null, state: 'disabled' },
{ key: 'explore', name: 'Explore', icon: null, state: 'supported' },
{ key: 'vibration', name: 'Vibration', icon: null, state: 'unsupported' },
{ key: 'sharing', name: 'Sharing', icon: null, state: 'supported' },
];
export function FiltersScreen() {
const theme = useDTTheme();
const [overlayVisible, setOverlayVisible] = useState(false);
return (
<ScreenContainer>
{/* Feature Legend */}
<DemoSection
title="DTFeatureLegend"
variant="normal"
description="Product feature grid with icons and rotated labels. Color indicates feature state."
>
<DTFeatureLegend
features={sampleFeatures}
variant="normal"
title="NFC FEATURES"
columns={5}
/>
<CodeLabel text='<DTFeatureLegend features={...} variant="normal" columns={5}>' />
</DemoSection>
{/* Feature Legend — other variant */}
<DemoSection
title="Emphasis Variant"
variant="emphasis"
description="Feature legend with emphasis header color."
>
<DTFeatureLegend
features={sampleFeatures.slice(0, 5)}
variant="emphasis"
title="KEY FEATURES"
columns={5}
/>
<CodeLabel text='variant="emphasis" — header uses emphasis color' />
</DemoSection>
{/* Mobile Filter Overlay */}
<DemoSection
title="DTMobileFilterOverlay"
variant="warning"
description="Full-screen slide-up overlay for mobile filter menus."
>
<DTButton
variant="normal"
onPress={() => setOverlayVisible(true)}
>
OPEN FILTER OVERLAY
</DTButton>
<CodeLabel text="<DTMobileFilterOverlay visible onDismiss={...}>" />
<DTMobileFilterOverlay
visible={overlayVisible}
onDismiss={() => setOverlayVisible(false)}
heading="FILTERS"
activeFilterCount={3}
onClearAll={() => setOverlayVisible(false)}
variant="normal"
>
<DTAccordion
sections={[
{
key: 'chip-type',
title: 'CHIP TYPE',
children: (
<View style={{ gap: 8 }}>
<Text style={{ color: theme.colors.onSurface }}>NTAG215</Text>
<Text style={{ color: theme.colors.onSurface }}>NTAG216</Text>
<Text style={{ color: theme.colors.onSurface }}>DESFire EV2</Text>
</View>
),
},
{
key: 'frequency',
title: 'FREQUENCY',
children: (
<View style={{ gap: 8 }}>
<Text style={{ color: theme.colors.onSurface }}>13.56 MHz (HF)</Text>
<Text style={{ color: theme.colors.onSurface }}>125 kHz (LF)</Text>
</View>
),
},
]}
/>
</DTMobileFilterOverlay>
</DemoSection>
</ScreenContainer>
);
}

View File

@@ -56,6 +56,27 @@ const categories: {
route: 'Overlays',
count: 5,
},
{
title: 'ADVANCED CARDS',
subtitle: 'Selected state, progress bar, badge overlays, stagger',
mode: 'emphasis',
route: 'CardsAdvanced',
count: 4,
},
{
title: 'ANIMATIONS',
subtitle: 'DTStaggerContainer, useScaleIn, usePulse',
mode: 'success',
route: 'Animations',
count: 3,
},
{
title: 'FILTERS & FEATURES',
subtitle: 'DTFeatureLegend, DTMobileFilterOverlay',
mode: 'other',
route: 'Filters',
count: 2,
},
{
title: 'THEME REFERENCE',
subtitle: 'Colors, Typography, Spacing',

View File

@@ -0,0 +1,33 @@
{
"name": "@dangerousthings/tailwind-preset",
"version": "0.1.0",
"description": "Tailwind CSS v3 preset mapping DT design tokens to Tailwind theme",
"license": "MIT",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"tailwindcss": "^3.4.0"
},
"devDependencies": {
"tailwindcss": "^3.4.0",
"typescript": "^5.8.0"
}
}

View File

@@ -0,0 +1,76 @@
/**
* DT Design System — Tailwind CSS v3 Preset
*
* Maps design tokens (CSS custom properties) to Tailwind theme values.
* Uses var() references so the same utility classes work with both
* DT and Classic brands at runtime via [data-brand] switching.
*
* Usage in consumer's tailwind.config.js:
*
* import dtPreset from '@dangerousthings/tailwind-preset';
*
* export default {
* presets: [dtPreset],
* content: ['./src/**\/*.{tsx,ts,html}'],
* };
*
* Then use: bg-dt-primary, text-dt-text-primary, gap-dt-4, rounded-dt, etc.
* Opacity modifiers work for colors with -rgb variants: bg-dt-primary/50
*/
import type { Config } from 'tailwindcss';
const dtPreset: Partial<Config> = {
theme: {
extend: {
colors: {
dt: {
// Colors with -rgb variants support Tailwind opacity modifiers (bg-dt-primary/50)
bg: 'rgb(var(--color-bg-rgb) / <alpha-value>)',
surface: 'rgb(var(--color-surface-rgb) / <alpha-value>)',
primary: 'rgb(var(--color-primary-rgb) / <alpha-value>)',
secondary: 'rgb(var(--color-secondary-rgb) / <alpha-value>)',
accent: 'rgb(var(--color-accent-rgb) / <alpha-value>)',
other: 'rgb(var(--color-other-rgb) / <alpha-value>)',
error: 'rgb(var(--color-error-rgb) / <alpha-value>)',
warning: 'rgb(var(--color-warning-rgb) / <alpha-value>)',
success: 'rgb(var(--color-success-rgb) / <alpha-value>)',
info: 'rgb(var(--color-info-rgb) / <alpha-value>)',
// Colors without -rgb variants (plain var)
border: 'var(--color-border)',
'surface-hover': 'var(--color-surface-hover)',
'primary-dim': 'var(--color-primary-dim)',
'text-primary': 'var(--color-text-primary)',
'text-secondary': 'var(--color-text-secondary)',
'text-muted': 'var(--color-text-muted)',
},
mode: {
normal: 'var(--mode-normal)',
emphasis: 'var(--mode-emphasis)',
warning: 'var(--mode-warning)',
success: 'var(--mode-success)',
other: 'var(--mode-other)',
},
},
spacing: {
'dt-1': 'var(--space-1)',
'dt-2': 'var(--space-2)',
'dt-3': 'var(--space-3)',
'dt-4': 'var(--space-4)',
'dt-6': 'var(--space-6)',
'dt-8': 'var(--space-8)',
},
borderRadius: {
'dt-sm': 'var(--radius-sm)',
dt: 'var(--radius)',
'dt-lg': 'var(--radius-lg)',
},
fontFamily: {
'dt-heading': 'var(--font-heading)',
'dt-body': 'var(--font-body)',
'dt-mono': 'var(--font-mono)',
},
},
},
};
export default dtPreset;

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"]
}

View File

@@ -6,8 +6,11 @@ export type {
TypographyTokens,
ShapeTokens,
BrandTokens,
DTVariant,
} from "./types.js";
export { variantToCSSProperty, variantToClassName } from "./types.js";
export { dt } from "./brands/dt.js";
export { classic } from "./brands/classic.js";

View File

@@ -56,7 +56,24 @@ function colorVars(colors: ColorTokens): string {
--color-error: ${colors.error};
--color-error-rgb: ${hexToRgb(colors.error)};
--color-info: ${colors.info};
--color-info-rgb: ${hexToRgb(colors.info)};`;
--color-info-rgb: ${hexToRgb(colors.info)};
/* Mode Aliases — per-component color overrides (cards, badges, buttons) */
--mode-normal: var(--color-primary);
--mode-normal-rgb: var(--color-primary-rgb);
--mode-normal-selected: rgba(var(--color-primary-rgb), 0.7);
--mode-emphasis: var(--color-secondary);
--mode-emphasis-rgb: var(--color-secondary-rgb);
--mode-emphasis-selected: rgba(var(--color-secondary-rgb), 0.7);
--mode-warning: var(--color-error);
--mode-warning-rgb: var(--color-error-rgb);
--mode-warning-selected: rgba(var(--color-error-rgb), 0.7);
--mode-success: var(--color-accent);
--mode-success-rgb: var(--color-accent-rgb);
--mode-success-selected: rgba(var(--color-accent-rgb), 0.7);
--mode-other: var(--color-other);
--mode-other-rgb: var(--color-other-rgb);
--mode-other-selected: rgba(var(--color-other-rgb), 0.7);`;
}
/** Generate a full brand CSS file */

View File

@@ -53,6 +53,27 @@ export interface ShapeTokens {
radiusLg: string;
}
/** Color variant for component accent colors (shared across web + React Native) */
export type DTVariant = 'normal' | 'emphasis' | 'warning' | 'success' | 'other';
/** Maps DTVariant to CSS custom property names */
export const variantToCSSProperty: Record<DTVariant, string> = {
normal: '--mode-normal',
emphasis: '--mode-emphasis',
warning: '--mode-warning',
success: '--mode-success',
other: '--mode-other',
};
/** Maps DTVariant to CSS class names */
export const variantToClassName: Record<DTVariant, string> = {
normal: 'mode-normal',
emphasis: 'mode-emphasis',
warning: 'mode-warning',
success: 'mode-success',
other: 'mode-other',
};
/** Complete brand definition */
export interface BrandTokens {
id: ThemeBrand;

View File

@@ -52,16 +52,29 @@ import { themes, brands } from "@dangerousthings/web/theme-registry";
| File | Description |
|------|-------------|
| `bevels.css` | Angular clip-path bevels for cards, buttons, labels, modals, drawers |
| `glows.css` | Neon drop-shadow and text-shadow effects for clipped and standard elements |
| `forms-dt.css` | Text inputs, checkboxes, switches, radio buttons, progress bars, accordions, quantity steppers |
| `bevels.css` | Angular clip-path bevels for cards, buttons, labels, modals, drawers. Card color modes, selected states, progress bars, badge overlays, interactive bevel buttons |
| `glows.css` | Neon drop-shadow and text-shadow effects — mode-aware via `--dt-glow-color` |
| `forms-dt.css` | Text inputs, checkboxes, switches, radio buttons, progress bars, accordions, steppers, menu items, filter headers, filter overlays |
| `animations.css` | Entrance animations (scale-in, fade-in, slide-up), interactive animations (pulse, ping, spin), stagger container, transition utilities |
| `scrollbar.css` | Thin neon scrollbar styling scoped under `[data-brand="dt"]` |
| `feature-legend.css` | Product feature grid with icons and rotated labels, state-based coloring |
### Key Classes
**Bevels**`.dt-bevel-card`, `.dt-bevel-btn`, `.dt-bevel-label`, `.dt-bevel-modal`, `.dt-bevel-drawer-left`, `.dt-bevel-drawer-right`, `.dt-bevel-sm`
**Card Modes**`.mode-normal`, `.mode-emphasis`, `.mode-warning`, `.mode-success`, `.mode-other`, `.card.selected`, `.dt-card-progress`, `.dt-badge-overlay`
**Interactive Buttons**`.dt-btn` (outlined rectangle, bevels on hover/select)
**Glows**`.dt-glow`, `.dt-glow-strong`, `.dt-glow-inset`, `.dt-text-glow`
**Animations**`.dt-animate-scale-in`, `.dt-animate-fade-in`, `.dt-animate-slide-up`, `.dt-animate-pulse`, `.dt-animate-ping`, `.dt-animate-spin`, `.dt-stagger-container`
**Scrollbar**`.dt-scrollbar`, `.dt-scrollbar-mode`
**Filters**`.dt-menu-item`, `.dt-filter-header`, `.dt-filter-overlay`
## Exports
| Path | Description |

Some files were not shown because too many files have changed in this diff Show More