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:
51
packages/hex-background/package.json
Normal file
51
packages/hex-background/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
36
packages/hex-background/src/HexCamera.tsx
Normal file
36
packages/hex-background/src/HexCamera.tsx
Normal 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;
|
||||
}
|
||||
148
packages/hex-background/src/HexGrid.tsx
Normal file
148
packages/hex-background/src/HexGrid.tsx
Normal 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,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
141
packages/hex-background/src/HexGridBackground.tsx
Normal file
141
packages/hex-background/src/HexGridBackground.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
23
packages/hex-background/src/index.ts
Normal file
23
packages/hex-background/src/index.ts
Normal 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';
|
||||
25
packages/hex-background/src/native.ts
Normal file
25
packages/hex-background/src/native.ts
Normal 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';
|
||||
9
packages/hex-background/tsconfig.json
Normal file
9
packages/hex-background/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user