Compare commits
12 Commits
@dangerous
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3ab5d892c | ||
|
|
1951cf98da | ||
|
|
2d49471ebb | ||
|
|
e33bf70896 | ||
|
|
a459b643e8 | ||
|
|
1aeceb8f24 | ||
|
|
e664cdaf3d | ||
|
|
0cfc945960 | ||
|
|
77c6a5a3bf | ||
|
|
27824ae189 | ||
|
|
1f67ebf48a | ||
|
|
5112e5c057 |
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dt-design-system",
|
"name": "dt-design-system",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
|
||||||
"description": "Dangerous Things design system — shared tokens, web CSS, and React Native components",
|
"description": "Dangerous Things design system — shared tokens, web CSS, and React Native components",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -36,4 +35,4 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"packageManager": "npm@10.0.0"
|
"packageManager": "npm@10.0.0"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
# @dangerousthings/hex-background
|
# @dangerousthings/hex-background
|
||||||
|
|
||||||
|
## 0.2.1
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Animation no longer degrades to noise over time — replaced frame-rate-dependent lerp with duration-based smoothstep easing
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `animationDuration` prop (default 1200ms) controls how long each hexagon takes to reach its target height
|
||||||
|
|
||||||
## 0.2.0
|
## 0.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dangerousthings/hex-background",
|
"name": "@dangerousthings/hex-background",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"description": "3D hexagon grid background for the Dangerous Things design system",
|
"description": "3D hexagon grid background for the Dangerous Things design system",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
Shape,
|
Shape,
|
||||||
ExtrudeGeometry,
|
ExtrudeGeometry,
|
||||||
MeshStandardMaterial,
|
MeshStandardMaterial,
|
||||||
MathUtils,
|
|
||||||
} from 'three';
|
} from 'three';
|
||||||
|
|
||||||
export interface HexGridProps {
|
export interface HexGridProps {
|
||||||
@@ -29,6 +28,13 @@ export interface HexGridProps {
|
|||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
/** Interval (ms) between height target changes @default 1500 */
|
/** Interval (ms) between height target changes @default 1500 */
|
||||||
animationInterval?: number;
|
animationInterval?: number;
|
||||||
|
/** Duration (ms) for each hex to reach its target @default 1200 */
|
||||||
|
animationDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hermite smoothstep: 3t² - 2t³ */
|
||||||
|
function smoothstep(t: number): number {
|
||||||
|
return t * t * (3 - 2 * t);
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeHexPositions(
|
function computeHexPositions(
|
||||||
@@ -62,6 +68,7 @@ export function HexGrid({
|
|||||||
margin = 0.05,
|
margin = 0.05,
|
||||||
maxHeight = 3,
|
maxHeight = 3,
|
||||||
animationInterval = 1500,
|
animationInterval = 1500,
|
||||||
|
animationDuration = 1200,
|
||||||
}: HexGridProps) {
|
}: HexGridProps) {
|
||||||
const meshRef = useRef<InstancedMesh>(null);
|
const meshRef = useRef<InstancedMesh>(null);
|
||||||
|
|
||||||
@@ -70,11 +77,11 @@ export function HexGrid({
|
|||||||
[rows, cols, hexRadius, extrudeHeight, margin],
|
[rows, cols, hexRadius, extrudeHeight, margin],
|
||||||
);
|
);
|
||||||
|
|
||||||
const targetHeightsRef = useRef(
|
const animStateRef = useRef(
|
||||||
positions.map(() => 1 + Math.random() * maxHeight),
|
positions.map(() => {
|
||||||
);
|
const h = 1 + Math.random() * maxHeight;
|
||||||
const currentHeightsRef = useRef(
|
return {startHeight: h, targetHeight: h, startTime: 0};
|
||||||
positions.map(() => 1 + Math.random() * maxHeight),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reuse dummy object for matrix updates — 15-20% FPS improvement
|
// Reuse dummy object for matrix updates — 15-20% FPS improvement
|
||||||
@@ -101,25 +108,41 @@ export function HexGrid({
|
|||||||
// Regenerate target heights at interval
|
// Regenerate target heights at interval
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
targetHeightsRef.current = positions.map(
|
const now = performance.now();
|
||||||
() => 0.5 + Math.random() * maxHeight,
|
animStateRef.current = animStateRef.current.map((state) => {
|
||||||
);
|
// Compute where the hex currently is (same easing as useFrame)
|
||||||
|
const elapsed = Math.min(
|
||||||
|
(now - state.startTime) / animationDuration,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
const t = smoothstep(elapsed);
|
||||||
|
const currentHeight =
|
||||||
|
state.startHeight + (state.targetHeight - state.startHeight) * t;
|
||||||
|
return {
|
||||||
|
startHeight: currentHeight,
|
||||||
|
targetHeight: 0.5 + Math.random() * maxHeight,
|
||||||
|
startTime: now,
|
||||||
|
};
|
||||||
|
});
|
||||||
}, animationInterval);
|
}, animationInterval);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [positions, maxHeight, animationInterval]);
|
}, [positions.length, maxHeight, animationInterval, animationDuration]);
|
||||||
|
|
||||||
// Animate per frame: lerp current heights toward targets
|
// Animate per frame: time-based smoothstep easing
|
||||||
useFrame((_state, delta) => {
|
useFrame(() => {
|
||||||
if (!meshRef.current) return;
|
if (!meshRef.current) return;
|
||||||
const dummy = dummyRef.current;
|
const dummy = dummyRef.current;
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
positions.forEach(({x, z}, i) => {
|
positions.forEach(({x, z}, i) => {
|
||||||
currentHeightsRef.current[i] = MathUtils.lerp(
|
const state = animStateRef.current[i];
|
||||||
currentHeightsRef.current[i],
|
const elapsed = Math.min(
|
||||||
targetHeightsRef.current[i],
|
(now - state.startTime) / animationDuration,
|
||||||
delta * 0.5,
|
1,
|
||||||
);
|
);
|
||||||
const height = currentHeightsRef.current[i];
|
const t = smoothstep(elapsed);
|
||||||
|
const height =
|
||||||
|
state.startHeight + (state.targetHeight - state.startHeight) * t;
|
||||||
|
|
||||||
dummy.position.set(x, height / 2, z);
|
dummy.position.set(x, height / 2, z);
|
||||||
dummy.scale.set(1, height, 1);
|
dummy.scale.set(1, height, 1);
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export interface HexGridBackgroundProps {
|
|||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
/** Height target refresh interval in ms @default 1500 */
|
/** Height target refresh interval in ms @default 1500 */
|
||||||
animationInterval?: number;
|
animationInterval?: number;
|
||||||
|
/** Duration (ms) for each hex to ease to target @default 1200 */
|
||||||
|
animationDuration?: number;
|
||||||
/** Camera orbital speed @default 0.02 */
|
/** Camera orbital speed @default 0.02 */
|
||||||
cameraSpeed?: number;
|
cameraSpeed?: number;
|
||||||
/** Camera orbital radius @default 8 */
|
/** Camera orbital radius @default 8 */
|
||||||
@@ -62,6 +64,7 @@ export function HexGridBackground({
|
|||||||
margin = 0.05,
|
margin = 0.05,
|
||||||
maxHeight = 3,
|
maxHeight = 3,
|
||||||
animationInterval = 1500,
|
animationInterval = 1500,
|
||||||
|
animationDuration = 1200,
|
||||||
cameraSpeed = 0.02,
|
cameraSpeed = 0.02,
|
||||||
cameraRadius = 8,
|
cameraRadius = 8,
|
||||||
fov = 40,
|
fov = 40,
|
||||||
@@ -134,6 +137,7 @@ export function HexGridBackground({
|
|||||||
margin={margin}
|
margin={margin}
|
||||||
maxHeight={maxHeight}
|
maxHeight={maxHeight}
|
||||||
animationInterval={animationInterval}
|
animationInterval={animationInterval}
|
||||||
|
animationDuration={animationDuration}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dangerousthings/react",
|
"name": "@dangerousthings/react",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"description": "React web components for the Dangerous Things design system",
|
"description": "React web components for the Dangerous Things design system",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -59,4 +59,4 @@
|
|||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"typescript": "^5.8.0"
|
"typescript": "^5.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,9 @@ export type DTGalleryItem =
|
|||||||
|
|
||||||
interface DTGalleryProps {
|
interface DTGalleryProps {
|
||||||
items: DTGalleryItem[];
|
items: DTGalleryItem[];
|
||||||
|
/** Externally-controlled active index. When this value changes, the gallery
|
||||||
|
* jumps to the corresponding item. Thumbnail clicks still work normally. */
|
||||||
|
activeIndex?: number;
|
||||||
/** Color variant @default 'normal' */
|
/** Color variant @default 'normal' */
|
||||||
variant?: DTVariant;
|
variant?: DTVariant;
|
||||||
/** Aspect ratio (width / height) for the display area @default 1 */
|
/** Aspect ratio (width / height) for the display area @default 1 */
|
||||||
@@ -85,12 +88,20 @@ const fillStyle: CSSProperties = {
|
|||||||
|
|
||||||
export function DTGallery({
|
export function DTGallery({
|
||||||
items,
|
items,
|
||||||
|
activeIndex: activeIndexProp,
|
||||||
variant = 'normal',
|
variant = 'normal',
|
||||||
aspectRatio = 1,
|
aspectRatio = 1,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
}: DTGalleryProps) {
|
}: DTGalleryProps) {
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(activeIndexProp ?? 0);
|
||||||
|
|
||||||
|
// Sync external activeIndex prop to internal state when it changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeIndexProp != null && activeIndexProp >= 0 && activeIndexProp < items.length) {
|
||||||
|
setActiveIndex(activeIndexProp);
|
||||||
|
}
|
||||||
|
}, [activeIndexProp, items.length]);
|
||||||
|
|
||||||
const hasModel = items.some((i) => i.type === 'model3d');
|
const hasModel = items.some((i) => i.type === 'model3d');
|
||||||
useModelViewerScript(hasModel);
|
useModelViewerScript(hasModel);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function DTLabel({
|
|||||||
}: DTLabelProps) {
|
}: DTLabelProps) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cx('badge', 'badge-mode', getVariantClass(variant), className)}
|
className={cx('badge', 'p-3', 'badge-mode', getVariantClass(variant), className)}
|
||||||
style={style}>
|
style={style}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* .dt-filter-overlay-content
|
* .dt-filter-overlay-content
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useCallback, useRef, type ReactNode, type CSSProperties } from 'react';
|
import { useEffect, useCallback, useRef, useId, type ReactNode, type CSSProperties } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import type { DTVariant } from '@dangerousthings/tokens';
|
import type { DTVariant } from '@dangerousthings/tokens';
|
||||||
import { cx } from '../utils/cx';
|
import { cx } from '../utils/cx';
|
||||||
@@ -40,6 +40,7 @@ export function DTMobileFilterOverlay({
|
|||||||
}: DTMobileFilterOverlayProps) {
|
}: DTMobileFilterOverlayProps) {
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
const triggerRef = useRef<HTMLElement | null>(null);
|
const triggerRef = useRef<HTMLElement | null>(null);
|
||||||
|
const headingId = useId();
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
@@ -105,8 +106,18 @@ export function DTMobileFilterOverlay({
|
|||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className={cx('dt-filter-overlay', getVariantClass(variant), className)} style={style}>
|
<div className={cx('dt-filter-overlay', getVariantClass(variant), className)} style={style}>
|
||||||
<div className="dt-filter-overlay-backdrop" onClick={onDismiss} />
|
<div
|
||||||
<div ref={panelRef} className="dt-filter-overlay-content">
|
className="dt-filter-overlay-backdrop"
|
||||||
|
onClick={onDismiss}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
ref={panelRef}
|
||||||
|
className="dt-filter-overlay-content"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby={headingId}
|
||||||
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -116,7 +127,7 @@ export function DTMobileFilterOverlay({
|
|||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderBottom: '1px solid rgba(var(--color-primary-rgb), 0.2)',
|
borderBottom: '1px solid rgba(var(--color-primary-rgb), 0.2)',
|
||||||
}}>
|
}}>
|
||||||
<span style={{ fontWeight: 700, fontSize: '1.125rem', letterSpacing: '0.05em' }}>
|
<span id={headingId} style={{ fontWeight: 700, fontSize: '1.125rem', letterSpacing: '0.05em' }}>
|
||||||
{heading}
|
{heading}
|
||||||
{activeFilterCount !== undefined && activeFilterCount > 0 && (
|
{activeFilterCount !== undefined && activeFilterCount > 0 && (
|
||||||
<span
|
<span
|
||||||
@@ -144,6 +155,7 @@ export function DTMobileFilterOverlay({
|
|||||||
<button
|
<button
|
||||||
onClick={onDismiss}
|
onClick={onDismiss}
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-label={`Close ${heading}`}
|
||||||
style={{
|
style={{
|
||||||
background: 'none',
|
background: 'none',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dangerousthings/showcase-desktop",
|
"name": "@dangerousthings/showcase-desktop",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Desktop showcase for the Dangerous Things design system",
|
"description": "Desktop showcase for the Dangerous Things design system",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dangerousthings/showcase-mobile",
|
"name": "@dangerousthings/showcase-mobile",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./index.js",
|
"main": "./index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -39,4 +38,4 @@
|
|||||||
"@types/react": "~19.1.0",
|
"@types/react": "~19.1.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
# @dangerousthings/web
|
# @dangerousthings/web
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- `dt-scale-in` keyframes now hold `transform: none, opacity: 0` during `animation-delay` (the period when `animation-fill-mode: both` shows the first keyframe), then snap to `scale(0)` at animation start before scaling/fading to 1. Visual difference vs prior is a subtle fade-in alongside the scale-in. **Why:** prior keyframes held descendants at `transform: scale(0)` during the delay, which caused any child performing `getBoundingClientRect()`-based self-measurement on mount (R3F `<Canvas>`, ResizeObserver-driven charts, virtualized lists, etc.) to read a near-zero rect and never recover — `transform` changes don't fire `ResizeObserver` when they end. Fixes the SuggestedPlacements 3D viewer "blank teal block until scroll" bug on the Hydrogen storefront product pages.
|
||||||
|
|
||||||
## 0.4.1
|
## 0.4.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dangerousthings/web",
|
"name": "@dangerousthings/web",
|
||||||
"version": "0.4.1",
|
"version": "0.5.1",
|
||||||
"description": "Web CSS theme for the Dangerous Things design system — bevels, glows, form styles",
|
"description": "Web CSS theme for the Dangerous Things design system — bevels, glows, form styles",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -7,8 +7,16 @@
|
|||||||
============================================================================ */
|
============================================================================ */
|
||||||
|
|
||||||
@keyframes dt-scale-in {
|
@keyframes dt-scale-in {
|
||||||
from { transform: scale(0); }
|
/* During animation-delay (with fill-mode: both), this 0% keyframe holds.
|
||||||
to { transform: scale(1); }
|
transform: none lets descendants self-measure correctly on mount —
|
||||||
|
critical for ResizeObserver-based components (R3F Canvas, charts,
|
||||||
|
virtualized lists) whose initial getBoundingClientRect() would
|
||||||
|
otherwise be transform-multiplied to ~0 inside a scale(0) ancestor. */
|
||||||
|
0% { opacity: 0; transform: none; }
|
||||||
|
/* Snap to scale(0) at animation start so the entrance is still a
|
||||||
|
visible scale-in (with a brief fade-in alongside). */
|
||||||
|
0.001% { opacity: 0; transform: scale(0); }
|
||||||
|
100% { opacity: 1; transform: scale(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes dt-fade-in {
|
@keyframes dt-fade-in {
|
||||||
|
|||||||
@@ -226,6 +226,9 @@
|
|||||||
[data-brand="dt"] .badge {
|
[data-brand="dt"] .badge {
|
||||||
--badge-border-color: var(--color-border);
|
--badge-border-color: var(--color-border);
|
||||||
--badge-fill: var(--color-surface);
|
--badge-fill: var(--color-surface);
|
||||||
|
/* inline-block so a wrapping badge stays one box — clip-path and the
|
||||||
|
::before fill fragment across line boxes on a plain inline span */
|
||||||
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
background: var(--badge-border-color);
|
background: var(--badge-border-color);
|
||||||
@@ -404,9 +407,10 @@
|
|||||||
|
|
||||||
/* Media frame placeholder — shows when no image/video is present.
|
/* Media frame placeholder — shows when no image/video is present.
|
||||||
Surface-colored background is already provided by ::before.
|
Surface-colored background is already provided by ::before.
|
||||||
This adds centered "MISSING MEDIA" text via ::after. */
|
This adds centered "LOADING MEDIA" text via ::after. Mostly seen while
|
||||||
|
lazy media loads, so it must read as a loading state, not an error. */
|
||||||
.dt-bevel-media::after {
|
.dt-bevel-media::after {
|
||||||
content: 'MISSING MEDIA';
|
content: 'LOADING MEDIA';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -424,7 +428,7 @@
|
|||||||
Uses surface background and centered placeholder text. */
|
Uses surface background and centered placeholder text. */
|
||||||
.dt-badge-parent:empty::after,
|
.dt-badge-parent:empty::after,
|
||||||
.card-body-flush:empty::after {
|
.card-body-flush:empty::after {
|
||||||
content: 'MISSING MEDIA';
|
content: 'LOADING MEDIA';
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user