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,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"]
}