Compare commits

..

13 Commits

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

Revert with: git revert <this-sha>

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

Revert with: git revert <this-sha>

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

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

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

Revert: git revert <THIS_COMMIT_SHA>

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

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

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

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

To revert: git revert <this-commit>

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:23:28 -07:00
michael
1be0070abf chore: version packages (web 0.4.1, react 2.0.1, react-native 0.4.1)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:26:57 -07:00
18 changed files with 131 additions and 38 deletions

View File

@@ -1,7 +1,6 @@
{
"name": "dt-design-system",
"version": "0.0.0",
"private": true,
"description": "Dangerous Things design system — shared tokens, web CSS, and React Native components",
"repository": {
"type": "git",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,11 @@
# @dangerousthings/react-native
## 0.4.1
### Patch Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer.
## 0.4.0
### Minor Changes

View File

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

View File

@@ -1,5 +1,13 @@
# @dangerousthings/react
## 2.0.1
### Patch Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer.
- Updated dependencies
- @dangerousthings/web@0.4.1
## 2.0.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@dangerousthings/react",
"version": "2.0.0",
"version": "2.1.0",
"description": "React web components for the Dangerous Things design system",
"license": "MIT",
"author": {

View File

@@ -41,6 +41,9 @@ export type DTGalleryItem =
interface DTGalleryProps {
items: DTGalleryItem[];
/** Externally-controlled active index. When this value changes, the gallery
* jumps to the corresponding item. Thumbnail clicks still work normally. */
activeIndex?: number;
/** Color variant @default 'normal' */
variant?: DTVariant;
/** Aspect ratio (width / height) for the display area @default 1 */
@@ -85,12 +88,20 @@ const fillStyle: CSSProperties = {
export function DTGallery({
items,
activeIndex: activeIndexProp,
variant = 'normal',
aspectRatio = 1,
className,
style,
}: DTGalleryProps) {
const [activeIndex, setActiveIndex] = useState(0);
const [activeIndex, setActiveIndex] = useState(activeIndexProp ?? 0);
// Sync external activeIndex prop to internal state when it changes
useEffect(() => {
if (activeIndexProp != null && activeIndexProp >= 0 && activeIndexProp < items.length) {
setActiveIndex(activeIndexProp);
}
}, [activeIndexProp, items.length]);
const hasModel = items.some((i) => i.type === 'model3d');
useModelViewerScript(hasModel);

View File

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

View File

@@ -5,7 +5,7 @@
* .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 type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
@@ -40,6 +40,7 @@ export function DTMobileFilterOverlay({
}: DTMobileFilterOverlayProps) {
const panelRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLElement | null>(null);
const headingId = useId();
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
@@ -105,8 +106,18 @@ export function DTMobileFilterOverlay({
return createPortal(
<div className={cx('dt-filter-overlay', getVariantClass(variant), className)} style={style}>
<div className="dt-filter-overlay-backdrop" onClick={onDismiss} />
<div ref={panelRef} className="dt-filter-overlay-content">
<div
className="dt-filter-overlay-backdrop"
onClick={onDismiss}
aria-hidden="true"
/>
<div
ref={panelRef}
className="dt-filter-overlay-content"
role="dialog"
aria-modal="true"
aria-labelledby={headingId}
>
{/* Header */}
<div
style={{
@@ -116,7 +127,7 @@ export function DTMobileFilterOverlay({
padding: '16px',
borderBottom: '1px solid rgba(var(--color-primary-rgb), 0.2)',
}}>
<span style={{ fontWeight: 700, fontSize: '1.125rem', letterSpacing: '0.05em' }}>
<span id={headingId} style={{ fontWeight: 700, fontSize: '1.125rem', letterSpacing: '0.05em' }}>
{heading}
{activeFilterCount !== undefined && activeFilterCount > 0 && (
<span
@@ -144,6 +155,7 @@ export function DTMobileFilterOverlay({
<button
onClick={onDismiss}
type="button"
aria-label={`Close ${heading}`}
style={{
background: 'none',
border: 'none',

View File

@@ -1,7 +1,6 @@
{
"name": "@dangerousthings/showcase-desktop",
"version": "0.0.0",
"private": true,
"license": "MIT",
"description": "Desktop showcase for the Dangerous Things design system",
"author": {

View File

@@ -1,7 +1,6 @@
{
"name": "@dangerousthings/showcase-mobile",
"version": "0.0.0",
"private": true,
"license": "MIT",
"main": "./index.js",
"scripts": {

View File

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

View File

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

View File

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

View File

@@ -226,6 +226,9 @@
[data-brand="dt"] .badge {
--badge-border-color: var(--color-border);
--badge-fill: var(--color-surface);
/* inline-block so a wrapping badge stays one box — clip-path and the
::before fill fragment across line boxes on a plain inline span */
display: inline-block;
position: relative;
isolation: isolate;
background: var(--badge-border-color);
@@ -404,9 +407,10 @@
/* Media frame placeholder — shows when no image/video is present.
Surface-colored background is already provided by ::before.
This adds centered "MISSING MEDIA" text via ::after. */
This adds centered "LOADING MEDIA" text via ::after. Mostly seen while
lazy media loads, so it must read as a loading state, not an error. */
.dt-bevel-media::after {
content: 'MISSING MEDIA';
content: 'LOADING MEDIA';
position: absolute;
inset: 0;
display: flex;
@@ -424,7 +428,7 @@
Uses surface background and centered placeholder text. */
.dt-badge-parent:empty::after,
.card-body-flush:empty::after {
content: 'MISSING MEDIA';
content: 'LOADING MEDIA';
display: flex;
align-items: center;
justify-content: center;