Compare commits
29 Commits
@dangerous
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3ab5d892c | ||
|
|
1951cf98da | ||
|
|
2d49471ebb | ||
|
|
e33bf70896 | ||
|
|
a459b643e8 | ||
|
|
1aeceb8f24 | ||
|
|
e664cdaf3d | ||
|
|
0cfc945960 | ||
|
|
77c6a5a3bf | ||
|
|
27824ae189 | ||
|
|
1f67ebf48a | ||
|
|
5112e5c057 | ||
|
|
1be0070abf | ||
|
|
c8c92d187d | ||
|
|
195c47602d | ||
|
|
d1e2bf1964 | ||
|
|
926a282535 | ||
|
|
2dcf86c346 | ||
|
|
8a91b21a11 | ||
|
|
766266b799 | ||
|
|
36420868f5 | ||
|
|
c8afd90527 | ||
|
|
34be1d0b9e | ||
|
|
6816ef4825 | ||
|
|
b319f15092 | ||
|
|
68234e66f4 | ||
|
|
2c9f7b8495 | ||
|
|
b137a99c78 | ||
|
|
4f477a50d8 |
108
.github/workflows/showcase-release.yml
vendored
108
.github/workflows/showcase-release.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Build design system packages
|
||||
run: npx turbo run build --filter='@dangerousthings/tokens' --filter='@dangerousthings/web'
|
||||
run: npx turbo run build
|
||||
|
||||
- name: Build desktop showcase
|
||||
working-directory: packages/showcase/desktop
|
||||
@@ -60,6 +60,81 @@ jobs:
|
||||
packages/showcase/desktop/release/*.deb
|
||||
retention-days: 30
|
||||
|
||||
# ─── Desktop Windows Build ──────────────────────────────────────
|
||||
desktop-windows:
|
||||
if: inputs.build_desktop
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build design system packages
|
||||
run: npx turbo run build
|
||||
|
||||
- name: Build desktop showcase
|
||||
working-directory: packages/showcase/desktop
|
||||
run: npm run build
|
||||
|
||||
- name: Package desktop (Windows)
|
||||
working-directory: packages/showcase/desktop
|
||||
run: npx electron-builder --win
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload desktop artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: desktop-windows
|
||||
path: |
|
||||
packages/showcase/desktop/release/*.exe
|
||||
retention-days: 30
|
||||
|
||||
# ─── Desktop macOS Build ────────────────────────────────────────
|
||||
desktop-macos:
|
||||
if: inputs.build_desktop
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build design system packages
|
||||
run: npx turbo run build
|
||||
|
||||
- name: Build desktop showcase
|
||||
working-directory: packages/showcase/desktop
|
||||
run: npm run build
|
||||
|
||||
- name: Package desktop (macOS)
|
||||
working-directory: packages/showcase/desktop
|
||||
run: npx electron-builder --mac --x64 --arm64
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# No CSC_LINK / CSC_KEY_PASSWORD = unsigned build
|
||||
# Users will need to right-click > Open to bypass Gatekeeper
|
||||
|
||||
- name: Upload desktop artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: desktop-macos
|
||||
path: |
|
||||
packages/showcase/desktop/release/*.dmg
|
||||
packages/showcase/desktop/release/*.zip
|
||||
retention-days: 30
|
||||
|
||||
# ─── Mobile Android Build ──────────────────────────────────────
|
||||
mobile-android:
|
||||
if: inputs.build_mobile
|
||||
@@ -84,7 +159,7 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Build design system packages
|
||||
run: npx turbo run build --filter='@dangerousthings/tokens'
|
||||
run: npx turbo run build
|
||||
|
||||
- name: Expo prebuild
|
||||
working-directory: packages/showcase/mobile
|
||||
@@ -112,8 +187,8 @@ jobs:
|
||||
|
||||
# ─── Create GitHub Release ─────────────────────────────────────
|
||||
create-release:
|
||||
needs: [desktop-linux, mobile-android]
|
||||
if: always() && (needs.desktop-linux.result == 'success' || needs.mobile-android.result == 'success')
|
||||
needs: [desktop-linux, desktop-windows, desktop-macos, mobile-android]
|
||||
if: always() && (needs.desktop-linux.result == 'success' || needs.desktop-windows.result == 'success' || needs.desktop-macos.result == 'success' || needs.mobile-android.result == 'success')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -131,7 +206,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p final-artifacts
|
||||
|
||||
# Desktop artifacts — replace 0.0.0 with release version
|
||||
# Desktop Linux artifacts
|
||||
if [ -d release-artifacts/desktop-linux ]; then
|
||||
for f in release-artifacts/desktop-linux/*; do
|
||||
filename=$(basename "$f")
|
||||
@@ -140,7 +215,25 @@ jobs:
|
||||
done
|
||||
fi
|
||||
|
||||
# Mobile APK — give it a descriptive name
|
||||
# Desktop Windows artifacts
|
||||
if [ -d release-artifacts/desktop-windows ]; then
|
||||
for f in release-artifacts/desktop-windows/*; do
|
||||
filename=$(basename "$f")
|
||||
newname=$(echo "$filename" | sed "s/0\.0\.0/${RELEASE_VERSION}/g")
|
||||
cp "$f" "final-artifacts/$newname"
|
||||
done
|
||||
fi
|
||||
|
||||
# Desktop macOS artifacts
|
||||
if [ -d release-artifacts/desktop-macos ]; then
|
||||
for f in release-artifacts/desktop-macos/*; do
|
||||
filename=$(basename "$f")
|
||||
newname=$(echo "$filename" | sed "s/0\.0\.0/${RELEASE_VERSION}/g")
|
||||
cp "$f" "final-artifacts/$newname"
|
||||
done
|
||||
fi
|
||||
|
||||
# Mobile APK
|
||||
if [ -f release-artifacts/mobile-android/app-release.apk ]; then
|
||||
cp release-artifacts/mobile-android/app-release.apk \
|
||||
"final-artifacts/dt-showcase-mobile-${RELEASE_VERSION}.apk"
|
||||
@@ -160,6 +253,9 @@ jobs:
|
||||
### Desktop (Electron)
|
||||
- **AppImage** (Linux, portable) — download and `chmod +x` to run
|
||||
- **deb** (Debian/Ubuntu) — install with `sudo dpkg -i <file>.deb`
|
||||
- **exe** (Windows) — NSIS installer + portable exe
|
||||
- **dmg** (macOS) — unsigned, right-click > Open to bypass Gatekeeper
|
||||
- **zip** (macOS) — portable archive
|
||||
|
||||
### Mobile (Android)
|
||||
- **APK** — sideload on any Android device (Settings > Install unknown apps)
|
||||
|
||||
@@ -17,7 +17,7 @@ Shared design tokens, web CSS, and React Native components for the Dangerous Thi
|
||||
|
||||
Two brand themes ship out of the box:
|
||||
|
||||
- **DT** — neon cyberpunk aesthetic with beveled corners and glow effects (Tektur font)
|
||||
- **DT** — neon cyberpunk aesthetic with beveled corners and baked in animations (Tektur font)
|
||||
- **Classic** — dark navy palette with magenta accents and standard border radius
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @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
|
||||
|
||||
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer in react-native, updated DTGallery/DTModal/DTMobileFilterOverlay components.
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dangerousthings/react-native",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.1",
|
||||
"description": "React Native themed components for the Dangerous Things design system",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
|
||||
@@ -251,7 +251,6 @@ const styles = StyleSheet.create({
|
||||
title: {
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 14,
|
||||
},
|
||||
contentWrapper: {},
|
||||
|
||||
@@ -243,6 +243,5 @@ const styles = StyleSheet.create({
|
||||
label: {
|
||||
fontWeight: '600',
|
||||
letterSpacing: 1,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -49,6 +49,11 @@ interface DTCardProps {
|
||||
* @default 0
|
||||
*/
|
||||
progress?: number;
|
||||
/**
|
||||
* Which area expands when the card grows to fill space
|
||||
* @default 'body'
|
||||
*/
|
||||
growArea?: 'body' | 'title';
|
||||
/**
|
||||
* Additional styles for the card container
|
||||
*/
|
||||
@@ -81,6 +86,10 @@ interface DTCardProps {
|
||||
* Background color (defaults to theme background)
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
/**
|
||||
* Element rendered at the right edge of the header
|
||||
*/
|
||||
headerRight?: ReactNode;
|
||||
/**
|
||||
* Press handler
|
||||
*/
|
||||
@@ -215,6 +224,7 @@ export function DTCard({
|
||||
title,
|
||||
showHeader,
|
||||
progress = 0,
|
||||
growArea = 'body',
|
||||
style,
|
||||
contentStyle,
|
||||
padding = 16,
|
||||
@@ -222,6 +232,7 @@ export function DTCard({
|
||||
bevelSize = 32,
|
||||
bevelSizeSmall = 16,
|
||||
backgroundColor,
|
||||
headerRight,
|
||||
onPress,
|
||||
}: DTCardProps) {
|
||||
const theme = useDTTheme();
|
||||
@@ -327,7 +338,7 @@ export function DTCard({
|
||||
)}
|
||||
<View style={[styles.innerContainer, useBevels && {paddingLeft: bevelSizeSmall - borderWidth}]}>
|
||||
{shouldShowHeader && (
|
||||
<View style={[styles.header, {backgroundColor: accentColor}]}>
|
||||
<View style={[styles.header, {backgroundColor: accentColor}, growArea === 'title' && {flex: 1}, !!(title && headerRight) && styles.headerRow]}>
|
||||
{title && (
|
||||
<Text
|
||||
variant="titleMedium"
|
||||
@@ -335,9 +346,10 @@ export function DTCard({
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{headerRight}
|
||||
</View>
|
||||
)}
|
||||
<View style={[styles.content, {padding}, contentStyle]}>{children}</View>
|
||||
<View style={[styles.content, {padding}, growArea === 'body' && {flex: 1}, contentStyle]}>{children}</View>
|
||||
</View>
|
||||
{/* Frame overlay (above content) — beveled mode only.
|
||||
Uses evenodd with 3 sub-paths: outer + inner + progressArea.
|
||||
@@ -400,12 +412,18 @@ const styles = StyleSheet.create({
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
overflow: 'hidden',
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
paddingLeft: 8,
|
||||
paddingRight: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerText: {
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.5,
|
||||
|
||||
@@ -109,7 +109,6 @@ const styles = StyleSheet.create({
|
||||
headerText: {
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
grid: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -79,6 +79,11 @@ interface DTGalleryProps {
|
||||
* @default 3
|
||||
*/
|
||||
borderWidth?: number;
|
||||
/**
|
||||
* Aspect ratio (width / height) for the display area
|
||||
* @default 1
|
||||
*/
|
||||
aspectRatio?: number;
|
||||
/**
|
||||
* Additional styles
|
||||
*/
|
||||
@@ -108,6 +113,7 @@ export function DTGallery({
|
||||
thumbnailSize = 64,
|
||||
bevelSize = 24,
|
||||
borderWidth = 3,
|
||||
aspectRatio = 1,
|
||||
style,
|
||||
}: DTGalleryProps) {
|
||||
const theme = useDTTheme();
|
||||
@@ -276,6 +282,7 @@ export function DTGallery({
|
||||
<View
|
||||
style={[
|
||||
styles.mainImageContainer,
|
||||
{ aspectRatio },
|
||||
!useBevels && {
|
||||
borderWidth,
|
||||
borderColor: accentColor,
|
||||
@@ -355,7 +362,6 @@ const styles = StyleSheet.create({
|
||||
gap: 12,
|
||||
},
|
||||
mainImageContainer: {
|
||||
aspectRatio: 1,
|
||||
position: 'relative',
|
||||
},
|
||||
mainImageContent: {
|
||||
|
||||
@@ -341,7 +341,6 @@ const styles = StyleSheet.create({
|
||||
menuItemTitle: {
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 14,
|
||||
},
|
||||
chevron: {
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
* - Content in beveled card container
|
||||
*/
|
||||
|
||||
import {StyleSheet, ViewStyle, StyleProp, KeyboardAvoidingView, Platform} from 'react-native';
|
||||
import {Portal, Modal} from 'react-native-paper';
|
||||
import {StyleSheet, ViewStyle, StyleProp, KeyboardAvoidingView, Platform, Pressable} from 'react-native';
|
||||
import {Portal, Modal, Text} from 'react-native-paper';
|
||||
import {useDTTheme} from '../theme/DTThemeProvider';
|
||||
import {type DTVariant} from '../utils/variantColors';
|
||||
import {DTCard} from './DTCard';
|
||||
@@ -97,6 +97,17 @@ export function DTModal({
|
||||
mode={variant}
|
||||
title={title}
|
||||
showHeader={!!title}
|
||||
headerRight={dismissable ? (
|
||||
<Pressable
|
||||
onPress={onDismiss}
|
||||
hitSlop={8}
|
||||
accessibilityLabel="Close"
|
||||
accessibilityRole="button">
|
||||
<Text style={{ color: theme.colors.onPrimary, fontSize: 18, lineHeight: 18 }}>
|
||||
{'\u2715'}
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : undefined}
|
||||
contentStyle={contentStyle}>
|
||||
{children}
|
||||
</DTCard>
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# @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
|
||||
|
||||
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer in react-native, updated DTGallery/DTModal/DTMobileFilterOverlay components.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @dangerousthings/web@0.4.0
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dangerousthings/react",
|
||||
"version": "1.0.0",
|
||||
"version": "2.1.0",
|
||||
"description": "React web components for the Dangerous Things design system",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
*
|
||||
* 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.
|
||||
* As progress increases, accent color fills from top to bottom.
|
||||
* When progress is omitted, the bar stays empty (full card, no fill).
|
||||
*/
|
||||
|
||||
import type { ReactNode, CSSProperties } from 'react';
|
||||
@@ -22,10 +23,18 @@ interface DTCardProps {
|
||||
title?: string;
|
||||
/** Whether to show the accent header bar @default true when title is provided */
|
||||
showHeader?: boolean;
|
||||
/** Progress value (0–100) for left-edge vertical progress bar @default 0 */
|
||||
/** Dismiss callback — renders a close button in the header bar */
|
||||
onDismiss?: () => void;
|
||||
/** Progress value (0–100) for left-edge vertical progress bar. Omit for empty bar. */
|
||||
progress?: number;
|
||||
/** Grow to fill available space in flex/grid containers @default false */
|
||||
grow?: boolean;
|
||||
/** Which area expands when grow is true @default 'body' */
|
||||
growArea?: 'body' | 'title';
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
/** Badge content rendered at bottom-right, clipped by card bevel */
|
||||
badge?: ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
@@ -34,12 +43,16 @@ export function DTCard({
|
||||
variant = 'normal',
|
||||
title,
|
||||
showHeader,
|
||||
progress = 0,
|
||||
onDismiss,
|
||||
progress,
|
||||
grow = false,
|
||||
growArea = 'body',
|
||||
className,
|
||||
style,
|
||||
onClick,
|
||||
badge,
|
||||
}: DTCardProps) {
|
||||
const shouldShowHeader = showHeader ?? !!title;
|
||||
const shouldShowHeader = showHeader ?? !!(title || onDismiss);
|
||||
const Tag = onClick ? 'button' : 'div';
|
||||
|
||||
return (
|
||||
@@ -47,20 +60,40 @@ export function DTCard({
|
||||
className={cx(
|
||||
'card',
|
||||
getVariantClass(variant),
|
||||
grow && 'dt-card-grow',
|
||||
grow && growArea === 'title' && 'dt-card-grow-title',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
...style,
|
||||
'--dt-card-progress': progress,
|
||||
...(progress != null && { '--dt-card-progress': progress }),
|
||||
} as CSSProperties}
|
||||
onClick={onClick}
|
||||
type={onClick ? 'button' : undefined}>
|
||||
{shouldShowHeader && (
|
||||
<div className="card-title">
|
||||
{title}
|
||||
<div className="card-title" style={onDismiss ? { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } : undefined}>
|
||||
<span>{title}</span>
|
||||
{onDismiss && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); onDismiss(); }}
|
||||
aria-label="Close"
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'inherit',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
fontSize: '1.25rem',
|
||||
lineHeight: 1,
|
||||
}}>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{badge && <div className="dt-card-badge">{badge}</div>}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,7 +160,6 @@ export function DTDrawer({
|
||||
fontWeight: 900,
|
||||
fontSize: '2em',
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase',
|
||||
borderBottom: '1px solid var(--color-bg)',
|
||||
}}>
|
||||
<span>{heading}</span>
|
||||
|
||||
@@ -1,56 +1,179 @@
|
||||
/**
|
||||
* DTGallery — Image gallery with beveled media frame and thumbnails.
|
||||
* DTGallery — Media gallery with beveled frame and thumbnails.
|
||||
* Supports images, 3D models (via <model-viewer>), videos, and external videos.
|
||||
*
|
||||
* CSS reference: bevels.css .dt-bevel-media
|
||||
*/
|
||||
|
||||
import { useState, type CSSProperties } from 'react';
|
||||
import { useState, useEffect, 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;
|
||||
/* ── TypeScript declaration for <model-viewer> web component ── */
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'model-viewer': React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLElement>,
|
||||
HTMLElement
|
||||
> & {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
thumbnail?: string;
|
||||
poster?: string;
|
||||
'camera-controls'?: boolean | string;
|
||||
'auto-rotate'?: boolean | string;
|
||||
'interaction-prompt'?: string;
|
||||
ar?: boolean | string;
|
||||
loading?: 'auto' | 'lazy' | 'eager';
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Gallery item types ── */
|
||||
|
||||
export type DTGalleryItem =
|
||||
| { type?: 'image'; key: string; src: string; alt?: string; thumbnail?: string }
|
||||
| { type: 'model3d'; key: string; src: string; alt?: string; thumbnail?: string; poster?: string }
|
||||
| { type: 'video'; key: string; src: string; alt?: string; thumbnail?: string; poster?: string }
|
||||
| { type: 'external-video'; key: string; src: string; alt?: string; thumbnail?: string };
|
||||
|
||||
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 */
|
||||
aspectRatio?: number;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
/* ── Model-viewer script loader ── */
|
||||
|
||||
const MODEL_VIEWER_SRC =
|
||||
'https://unpkg.com/@google/model-viewer@v1.12.1/dist/model-viewer.min.js';
|
||||
|
||||
let modelViewerLoaded = false;
|
||||
|
||||
function useModelViewerScript(needed: boolean) {
|
||||
useEffect(() => {
|
||||
if (!needed || modelViewerLoaded || typeof document === 'undefined') return;
|
||||
if (customElements.get('model-viewer')) {
|
||||
modelViewerLoaded = true;
|
||||
return;
|
||||
}
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.src = MODEL_VIEWER_SRC;
|
||||
document.head.appendChild(script);
|
||||
modelViewerLoaded = true;
|
||||
}, [needed]);
|
||||
}
|
||||
|
||||
/* ── Shared styles ── */
|
||||
|
||||
const fillStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'block',
|
||||
};
|
||||
|
||||
/* ── Component ── */
|
||||
|
||||
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);
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
const current = items[activeIndex];
|
||||
const currentType = current.type || 'image';
|
||||
|
||||
return (
|
||||
<div className={cx(getVariantClass(variant), className)} style={style}>
|
||||
{/* Main image */}
|
||||
<div className="dt-bevel-media" style={{ overflow: 'hidden' }}>
|
||||
{/* Main display */}
|
||||
<div
|
||||
className="dt-bevel-media"
|
||||
style={{ overflow: 'hidden', aspectRatio, position: 'relative' }}
|
||||
>
|
||||
{currentType === 'image' && (
|
||||
<img
|
||||
src={current.src}
|
||||
alt={current.alt || ''}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
...fillStyle,
|
||||
objectFit: 'contain',
|
||||
transition: 'opacity 300ms ease-in-out',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentType === 'model3d' && (
|
||||
<model-viewer
|
||||
src={current.src}
|
||||
alt={current.alt || ''}
|
||||
poster={'poster' in current ? current.poster : undefined}
|
||||
camera-controls=""
|
||||
auto-rotate=""
|
||||
interaction-prompt="auto"
|
||||
loading="lazy"
|
||||
style={{
|
||||
...fillStyle,
|
||||
backgroundColor: 'var(--color-bg, #000)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentType === 'video' && (
|
||||
<video
|
||||
src={current.src}
|
||||
poster={'poster' in current ? current.poster : undefined}
|
||||
controls
|
||||
playsInline
|
||||
preload="metadata"
|
||||
style={{
|
||||
...fillStyle,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentType === 'external-video' && (
|
||||
<iframe
|
||||
src={current.src}
|
||||
title={current.alt || 'Video'}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{
|
||||
...fillStyle,
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Thumbnails */}
|
||||
{items.length > 1 && (
|
||||
<div
|
||||
@@ -59,8 +182,14 @@ export function DTGallery({
|
||||
gap: '8px',
|
||||
marginTop: '8px',
|
||||
overflowX: 'auto',
|
||||
}}>
|
||||
{items.map((item, i) => (
|
||||
}}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
const itemType = item.type || 'image';
|
||||
const thumbSrc = item.thumbnail || ('poster' in item ? item.poster : undefined) || item.src;
|
||||
const isMedia = itemType === 'video' || itemType === 'external-video';
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
onClick={() => setActiveIndex(i)}
|
||||
@@ -70,20 +199,42 @@ export function DTGallery({
|
||||
width: '64px',
|
||||
height: '64px',
|
||||
padding: 0,
|
||||
border: i === activeIndex
|
||||
border:
|
||||
i === activeIndex
|
||||
? '2px solid var(--dt-card-color, var(--color-primary))'
|
||||
: '2px solid transparent',
|
||||
background: 'none',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={item.thumbnail || item.src}
|
||||
src={thumbSrc}
|
||||
alt={item.alt || ''}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
{/* Play icon overlay for video thumbnails */}
|
||||
{isMedia && (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="white"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
margin: 'auto',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.6))',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,25 +2,43 @@
|
||||
* DTMediaFrame — Diagonal beveled frame (top-left + bottom-right).
|
||||
*
|
||||
* CSS reference: bevels.css .dt-bevel-media
|
||||
*
|
||||
* The outer div provides the colored bevel border via clip-path.
|
||||
* A ::before pseudo-element fills the inner surface.
|
||||
* Direct img/video children are auto-clipped to match.
|
||||
* A ::after pseudo-element shows "MISSING MEDIA" when empty.
|
||||
*/
|
||||
|
||||
import type { ReactNode, CSSProperties } from 'react';
|
||||
import type { DTVariant } from '@dangerousthings/tokens';
|
||||
import { cx } from '../utils/cx';
|
||||
import { getVariantClass } from '../utils/variantClasses';
|
||||
|
||||
interface DTMediaFrameProps {
|
||||
children: ReactNode;
|
||||
/** Color variant for the bevel border @default 'normal' */
|
||||
variant?: DTVariant;
|
||||
/** Custom placeholder shown when children is nullish (overrides CSS ::after) */
|
||||
placeholder?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export function DTMediaFrame({
|
||||
children,
|
||||
variant = 'normal',
|
||||
placeholder,
|
||||
className,
|
||||
style,
|
||||
}: DTMediaFrameProps) {
|
||||
const isEmpty = children == null || children === false;
|
||||
|
||||
return (
|
||||
<div className={cx('dt-bevel-media', className)} style={style}>
|
||||
{children}
|
||||
<div
|
||||
className={cx('dt-bevel-media', getVariantClass(variant), className)}
|
||||
style={style}
|
||||
>
|
||||
{isEmpty && placeholder ? placeholder : children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* .dt-filter-overlay-content
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback, 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';
|
||||
@@ -38,6 +38,10 @@ export function DTMobileFilterOverlay({
|
||||
className,
|
||||
style,
|
||||
}: DTMobileFilterOverlayProps) {
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const triggerRef = useRef<HTMLElement | null>(null);
|
||||
const headingId = useId();
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onDismiss();
|
||||
@@ -45,6 +49,7 @@ export function DTMobileFilterOverlay({
|
||||
[onDismiss],
|
||||
);
|
||||
|
||||
// Escape key
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
@@ -52,12 +57,67 @@ export function DTMobileFilterOverlay({
|
||||
}
|
||||
}, [visible, handleKeyDown]);
|
||||
|
||||
// Body scroll lock
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => { document.body.style.overflow = ''; };
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
// Focus trap
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
triggerRef.current = document.activeElement as HTMLElement;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
const first = panelRef.current?.querySelector<HTMLElement>(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
||||
);
|
||||
first?.focus();
|
||||
}, 100);
|
||||
|
||||
const handleTab = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Tab' || !panelRef.current) return;
|
||||
const focusable = panelRef.current.querySelectorAll<HTMLElement>(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
||||
);
|
||||
if (focusable.length === 0) return;
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
if (e.shiftKey && document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleTab);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
document.removeEventListener('keydown', handleTab);
|
||||
triggerRef.current?.focus();
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
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">
|
||||
<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={{
|
||||
@@ -67,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', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||
<span id={headingId} style={{ fontWeight: 700, fontSize: '1.125rem', letterSpacing: '0.05em' }}>
|
||||
{heading}
|
||||
{activeFilterCount !== undefined && activeFilterCount > 0 && (
|
||||
<span
|
||||
@@ -88,7 +148,6 @@ export function DTMobileFilterOverlay({
|
||||
color: 'var(--color-primary)',
|
||||
fontSize: '0.875rem',
|
||||
cursor: 'pointer',
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
Clear All
|
||||
</button>
|
||||
@@ -96,6 +155,7 @@ export function DTMobileFilterOverlay({
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
type="button"
|
||||
aria-label={`Close ${heading}`}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
|
||||
@@ -82,13 +82,35 @@ export function DTModal({
|
||||
position: 'relative',
|
||||
maxWidth: '90vw',
|
||||
maxHeight: '85vh',
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
...style,
|
||||
}}
|
||||
role="dialog"
|
||||
aria-modal="true">
|
||||
{title && <div className="card-title">{title}</div>}
|
||||
<div style={{ padding: 'var(--space-6, 24px)' }}>{children}</div>
|
||||
{title && (
|
||||
<div className="card-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexShrink: 0 }}>
|
||||
<span>{title}</span>
|
||||
{dismissable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDismiss}
|
||||
aria-label="Close"
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'inherit',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
fontSize: '1.25rem',
|
||||
lineHeight: 1,
|
||||
}}>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ padding: 'var(--space-6, 24px)', overflow: 'auto', flex: '1 1 auto' }}>{children}</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
* 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.
|
||||
* Each child is wrapped in a div that carries the animationDelay.
|
||||
* Wrapper divs inherit display:contents would break animation, so
|
||||
* the CSS rule targets .dt-stagger-container > * directly.
|
||||
*/
|
||||
|
||||
import type { ReactNode, CSSProperties } from 'react';
|
||||
import { Children, type ReactNode, type 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;
|
||||
/** Delay between each child in ms @default 150 */
|
||||
interval?: number;
|
||||
/** Base delay before the first child animates in ms @default 0 */
|
||||
delay?: number;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
@@ -21,19 +25,23 @@ interface DTStaggerContainerProps {
|
||||
export function DTStaggerContainer({
|
||||
children,
|
||||
duration,
|
||||
interval,
|
||||
interval = 150,
|
||||
delay = 0,
|
||||
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}
|
||||
{Children.map(children, (child, i) => (
|
||||
<div style={{ animationDelay: `${delay + i * interval}ms` }}>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ export function DTTextInput({
|
||||
marginBottom: '4px',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
}}>
|
||||
{label}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* DTWebThemeProvider
|
||||
*
|
||||
* Sets data-brand and data-theme attributes on a wrapper div.
|
||||
* Sets data-brand and data-theme attributes on a wrapper div (default)
|
||||
* or on document.documentElement (documentLevel mode).
|
||||
* Provides React context for child components to read the active brand/theme.
|
||||
*/
|
||||
|
||||
import { createContext, type ReactNode } from 'react';
|
||||
import { createContext, useEffect, type ReactNode } from 'react';
|
||||
import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens';
|
||||
import { cx } from '../utils/cx';
|
||||
|
||||
@@ -21,7 +22,9 @@ interface DTWebThemeProviderProps {
|
||||
brand?: ThemeBrand;
|
||||
/** Theme mode @default 'dark' */
|
||||
theme?: ThemeMode;
|
||||
/** Additional className for the wrapper div */
|
||||
/** When true, sets data-brand/data-theme on document.documentElement instead of a wrapper div */
|
||||
documentLevel?: boolean;
|
||||
/** Additional className for the wrapper div (ignored when documentLevel is true) */
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
@@ -29,14 +32,26 @@ interface DTWebThemeProviderProps {
|
||||
export function DTWebThemeProvider({
|
||||
brand = 'dt',
|
||||
theme = 'dark',
|
||||
documentLevel,
|
||||
className,
|
||||
children,
|
||||
}: DTWebThemeProviderProps) {
|
||||
useEffect(() => {
|
||||
if (documentLevel) {
|
||||
document.documentElement.dataset.brand = brand;
|
||||
document.documentElement.dataset.theme = theme;
|
||||
}
|
||||
}, [documentLevel, brand, theme]);
|
||||
|
||||
return (
|
||||
<DTWebThemeContext.Provider value={{ brand, theme }}>
|
||||
{documentLevel ? (
|
||||
children
|
||||
) : (
|
||||
<div data-brand={brand} data-theme={theme} className={cx('dt-theme-root', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</DTWebThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ export { DTSearchInput } from './components/DTSearchInput';
|
||||
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
|
||||
@@ -58,3 +57,6 @@ export { DTMobileFilterOverlay } from './components/DTMobileFilterOverlay';
|
||||
export { DTGallery } from './components/DTGallery';
|
||||
export type { DTGalleryItem } from './components/DTGallery';
|
||||
export { DTHexagon } from './components/DTHexagon';
|
||||
|
||||
// Components — animation
|
||||
export { DTStaggerContainer } from './components/DTStaggerContainer';
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
{
|
||||
"name": "@dangerousthings/showcase-desktop",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"description": "Desktop showcase for the Dangerous Things design system",
|
||||
"author": {
|
||||
"name": "Dangerous Things",
|
||||
"email": "info@dangerousthings.com"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dangerous-tac0s/dt-design-system.git",
|
||||
"directory": "packages/showcase/desktop"
|
||||
},
|
||||
"homepage": "https://github.com/dangerous-tac0s/dt-design-system",
|
||||
"main": "dist/main/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { DTCard, DTStaggerContainer } from '@dangerousthings/react';
|
||||
import { DTCard } from '@dangerousthings/react';
|
||||
import { Section, Row, CodeLabel } from '../components/Section';
|
||||
|
||||
function AnimBox({ className, label }: { className: string; label: string }) {
|
||||
@@ -66,7 +66,7 @@ function ProgressBarDemo() {
|
||||
<DTCard
|
||||
key={v}
|
||||
variant={v}
|
||||
title={`${v.toUpperCase()} ${progress[i]}%`}
|
||||
title={`${v} ${progress[i]}%`}
|
||||
progress={progress[i]}
|
||||
style={{ width: 160, transition: 'all 0.1s ease-out' }}
|
||||
>
|
||||
@@ -90,13 +90,12 @@ function ProgressBarDemo() {
|
||||
|
||||
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>
|
||||
<p className="page-subtitle">Entrance animations, interactive effects, and transition utilities.</p>
|
||||
|
||||
<Section title="Entrance Animations" description="One-shot animations for elements entering the viewport.">
|
||||
<Row key={entranceKey}>
|
||||
@@ -119,28 +118,6 @@ export function AnimationsPage() {
|
||||
<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
|
||||
@@ -156,7 +133,6 @@ export function AnimationsPage() {
|
||||
cursor: 'pointer',
|
||||
color: 'var(--color-text)',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
aria-expanded={accordionExpanded}
|
||||
onClick={() => setAccordionExpanded(!accordionExpanded)}
|
||||
|
||||
@@ -53,12 +53,12 @@ export function BevelsPage() {
|
||||
<Row>
|
||||
{modes.map(mode => (
|
||||
<button key={mode} className={`dt-menu-item mode-${mode}`} type="button" onClick={() => setModalVariant(mode)}>
|
||||
{mode.toUpperCase()} MODAL
|
||||
{mode} Modal
|
||||
</button>
|
||||
))}
|
||||
</Row>
|
||||
<CodeLabel text="<DTModal variant='normal' visible onDismiss title='...'>content</DTModal>" />
|
||||
<DTModal visible={modalVariant !== null} onDismiss={() => setModalVariant(null)} variant={modalVariant ?? 'normal'} title={`${(modalVariant ?? 'normal').toUpperCase()} MODAL`}>
|
||||
<DTModal visible={modalVariant !== null} onDismiss={() => setModalVariant(null)} variant={modalVariant ?? 'normal'} title={`${modalVariant ?? 'normal'} Modal`}>
|
||||
<p style={{ marginBottom: 'var(--space-4)' }}>This is a <strong>{modalVariant}</strong> modal with beveled card shape, backdrop blur, and scale-in animation.</p>
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.85rem' }}>Click the backdrop or press Escape to dismiss.</p>
|
||||
</DTModal>
|
||||
@@ -74,7 +74,7 @@ export function BevelsPage() {
|
||||
</button>
|
||||
</Row>
|
||||
<CodeLabel text="<DTDrawer position='right' heading='...' visible onDismiss>content</DTDrawer>" />
|
||||
<DTDrawer visible={drawerSide !== null} onDismiss={() => setDrawerSide(null)} position={drawerSide ?? 'right'} heading={`${(drawerSide ?? 'right').toUpperCase()} DRAWER`} headingVariant={drawerSide === 'left' ? 'other' : 'emphasis'}>
|
||||
<DTDrawer visible={drawerSide !== null} onDismiss={() => setDrawerSide(null)} position={drawerSide ?? 'right'} heading={`${drawerSide ?? 'right'} Drawer`} headingVariant={drawerSide === 'left' ? 'other' : 'emphasis'}>
|
||||
<p style={{ marginBottom: 'var(--space-4)' }}>Sliding panel from the <strong>{drawerSide}</strong> edge with beveled corners and backdrop blur.</p>
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.85rem' }}>Click the backdrop, press Escape, or click ✕ to dismiss.</p>
|
||||
</DTDrawer>
|
||||
@@ -91,7 +91,7 @@ export function BevelsPage() {
|
||||
|
||||
<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>
|
||||
<span style={{ fontWeight: 600 }}>Thick Top Border Accent</span>
|
||||
</div>
|
||||
<CodeLabel text=".dt-accent-top" />
|
||||
</Section>
|
||||
@@ -99,7 +99,7 @@ export function BevelsPage() {
|
||||
<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()}>
|
||||
<DTCard key={mode} variant={mode} title={mode}>
|
||||
<div className="card-body" style={{ fontSize: '0.75rem' }}>mode-{mode}</div>
|
||||
</DTCard>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createElement, useState, type CSSProperties } from 'react';
|
||||
import { createElement, type CSSProperties } from 'react';
|
||||
import {
|
||||
DTCard,
|
||||
DTStaggerContainer,
|
||||
DTFeatureLegend,
|
||||
} from '@dangerousthings/react';
|
||||
import type { DTFeatureItem } from '@dangerousthings/react';
|
||||
@@ -33,36 +32,35 @@ const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other']
|
||||
const ico = (C: any) => createElement(C, { style: { fontSize: '2rem' } });
|
||||
|
||||
// Full chip feature legend (9 features — from storefront UseCaseLegend)
|
||||
// detail text simulates what the storefront shows for an NExT implant
|
||||
// NExT v2: NTAGI2C (HF) + T5577 (LF) with power-harvesting LED
|
||||
const chipFeatures: DTFeatureItem[] = [
|
||||
{ key: 'smartphone', name: 'Smartphone', icon: ico(MdOutlinePhonelinkRing), state: 'supported', detail: 'Full NFC smartphone support' },
|
||||
{ key: 'access_control', name: 'Access Control', icon: ico(MdOutlineVpnKey), state: 'supported', detail: 'DESFire, MIFARE Classic, iCLASS' },
|
||||
{ key: 'digital_security', name: 'Digital Security', icon: ico(FaUserShield), state: 'supported', detail: 'FIDO2 / WebAuthn' },
|
||||
{ key: 'access_control', name: 'Access Control', icon: ico(MdOutlineVpnKey), state: 'supported', detail: 'Legacy' },
|
||||
{ key: 'digital_security', name: 'Digital Security', icon: ico(FaUserShield), state: 'unsupported', detail: 'Not Supported' },
|
||||
{ key: 'cryptography', name: 'Cryptography', icon: ico(LuBinary), state: 'unsupported', detail: 'Not Supported' },
|
||||
{ key: 'data_sharing', name: 'Data Sharing', icon: ico(MdOutlineMobileScreenShare), state: 'supported', detail: 'NDEF records, vCard, URL' },
|
||||
{ key: 'payment', name: 'Payment', icon: ico(MdOutlineCreditCard), state: 'disabled', detail: 'Apex required' },
|
||||
{ key: 'magic', name: 'Magic', icon: ico(MdOutlineCopyAll), state: 'supported', detail: 'Gen2 Magic UID' },
|
||||
{ key: 'Illumination', name: 'Illumination', icon: ico(MdOutlineLightbulb), state: 'unsupported', detail: 'None' },
|
||||
{ key: 'data_sharing', name: 'Data Sharing', icon: ico(MdOutlineMobileScreenShare), state: 'supported', detail: '1 kB' },
|
||||
{ key: 'payment', name: 'Payment', icon: ico(MdOutlineCreditCard), state: 'unsupported', detail: 'Not Supported' },
|
||||
{ key: 'magic', name: 'Magic', icon: ico(MdOutlineCopyAll), state: 'supported', detail: 'Yes' },
|
||||
{ key: 'Illumination', name: 'Illumination', icon: ico(MdOutlineLightbulb), state: 'supported', detail: 'HF: green, blue, white' },
|
||||
{ key: 'temperature', name: 'Sensors', icon: ico(MdOutlineThermostat), state: 'unsupported', detail: 'None' },
|
||||
];
|
||||
|
||||
// Full biomagnet feature legend (4 features — from storefront MagnetUseCaseLegend)
|
||||
// m0422a: axial neodymium disc, bioresin coated
|
||||
const magnetFeatures: DTFeatureItem[] = [
|
||||
{ key: 'sensing', name: 'Sensing', icon: ico(MdOutlineSensors), state: 'supported', detail: 'Electromagnetic field detection' },
|
||||
{ key: 'lifting', name: 'Lifting', icon: ico(MdOutlineFitbit), state: 'supported', detail: '2.1 kg lifting force' },
|
||||
{ key: 'haptics', name: 'Haptics', icon: ico(MdOutlineVibration), state: 'supported', detail: 'Tactile vibration feedback' },
|
||||
{ key: 'polarity', name: 'Polarity Detection', icon: ico(MdOutlineExplore), state: 'unsupported', detail: 'Not Supported' },
|
||||
{ key: 'sensing', name: 'Sensing', icon: ico(MdOutlineSensors), state: 'supported', detail: 'RP 22,860 G/g' },
|
||||
{ key: 'lifting', name: 'Lifting', icon: ico(MdOutlineFitbit), state: 'supported', detail: '~20 g' },
|
||||
{ key: 'haptics', name: 'Haptics', icon: ico(MdOutlineVibration), state: 'supported', detail: 'Push/Pull' },
|
||||
{ key: 'polarity', name: 'Polarity Detection', icon: ico(MdOutlineExplore), state: 'supported', detail: 'Monopole' },
|
||||
];
|
||||
|
||||
// Full aesthetic feature legend (2 features — from storefront AestheticUseCaseLegend)
|
||||
const aestheticFeatures: DTFeatureItem[] = [
|
||||
{ key: 'illumination', name: 'Illumination', icon: ico(MdLightbulbOutline), state: 'supported', detail: 'LED: Red, Green, Blue, White' },
|
||||
{ key: 'prominence', name: 'Prominence', icon: ico(MdOutlineVisibility), state: 'supported', detail: 'High visibility under skin' },
|
||||
{ key: 'illumination', name: 'Illumination', icon: ico(MdLightbulbOutline), state: 'supported', detail: 'HF: red, green, blue, white' },
|
||||
{ key: 'prominence', name: 'Prominence', icon: ico(MdOutlineVisibility), state: 'supported', detail: 'Low' },
|
||||
];
|
||||
|
||||
export function CardsAdvancedPage() {
|
||||
const [staggerKey, setStaggerKey] = useState(0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="page-title">Advanced Cards</h1>
|
||||
@@ -71,7 +69,7 @@ export function CardsAdvancedPage() {
|
||||
<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()}>
|
||||
<DTCard key={mode} variant={mode} title={mode}>
|
||||
<div className="card-body" style={{ fontSize: '0.8rem' }}>mode-{mode}</div>
|
||||
</DTCard>
|
||||
))}
|
||||
@@ -80,6 +78,23 @@ export function CardsAdvancedPage() {
|
||||
</Section>
|
||||
|
||||
|
||||
<Section title="Card Grow" description="Cards stretch to equal height in a row via grow prop. Compare the varying content heights.">
|
||||
<div style={{ display: 'flex', gap: 'var(--space-3)' }}>
|
||||
<DTCard variant="normal" title="SHORT" grow>
|
||||
<div className="card-body" style={{ fontSize: '0.8rem' }}>One line</div>
|
||||
</DTCard>
|
||||
<DTCard variant="emphasis" title="TALL" grow>
|
||||
<div className="card-body" style={{ fontSize: '0.8rem' }}>
|
||||
Multiple lines of content to demonstrate that the shorter card stretches to match this card's height.
|
||||
</div>
|
||||
</DTCard>
|
||||
<DTCard variant="other" title="MEDIUM" grow>
|
||||
<div className="card-body" style={{ fontSize: '0.8rem' }}>Two lines of content here.</div>
|
||||
</DTCard>
|
||||
</div>
|
||||
<CodeLabel text="<DTCard grow /> — flex-grow: 1 + height: 100% to fill container" />
|
||||
</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) => (
|
||||
@@ -98,15 +113,15 @@ export function CardsAdvancedPage() {
|
||||
{ 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' }}>
|
||||
<DTCard key={badge.label} variant={badge.mode} title={badge.label + ' PRODUCT'}
|
||||
badge={<span>{badge.label}</span>}>
|
||||
<div className="card-body-flush" 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 card image container" />
|
||||
<CodeLabel text="<DTCard badge={<span>LAB</span>}> — badge rendered and positioned by card" />
|
||||
</Section>
|
||||
|
||||
<Section title="Buttons" description="Beveled buttons with active/selected states and mode colors. Nested levels use inline paddingLeft.">
|
||||
@@ -132,11 +147,11 @@ export function CardsAdvancedPage() {
|
||||
</Section>
|
||||
|
||||
<Section title="Feature Legends" description="Interactive product feature grids from the storefront. Hover icons for details. Toggle labels with the ? button.">
|
||||
<DTFeatureLegend features={chipFeatures} title="NExT Features" variant="normal" columns={5} />
|
||||
<DTFeatureLegend features={chipFeatures} title="NExT v2 Features" variant="normal" columns={5} />
|
||||
<div style={{ height: 'var(--space-6)' }} />
|
||||
<DTFeatureLegend features={magnetFeatures} title="Titan Features" variant="emphasis" columns={4} />
|
||||
<DTFeatureLegend features={magnetFeatures} title="m0422a Features" variant="emphasis" columns={4} />
|
||||
<div style={{ height: 'var(--space-6)' }} />
|
||||
<DTFeatureLegend features={aestheticFeatures} title="xLED Features" variant="other" columns={2} />
|
||||
<DTFeatureLegend features={aestheticFeatures} title="xLED HF Features" variant="other" columns={2} />
|
||||
<div style={{ height: 'var(--space-4)' }} />
|
||||
<CodeLabel text="<DTFeatureLegend features={chipFeatures} title='NExT Features' variant='normal' />" />
|
||||
<CodeLabel text="<DTFeatureLegend features={magnetFeatures} variant='emphasis' columns={4} />" />
|
||||
@@ -148,27 +163,6 @@ export function CardsAdvancedPage() {
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export function HexBackgroundPage({ hexProps, onHexPropsChange }: HexBackgroundP
|
||||
{sliders.map(({ key, label, min, max, step }) => (
|
||||
<div key={key} style={{ display: 'grid', gridTemplateColumns: '1fr auto', alignItems: 'center', gap: 'var(--space-4)' }}>
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<span style={{ fontSize: '0.8rem', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
||||
<span style={{ fontSize: '0.8rem', color: 'var(--color-text-muted)', letterSpacing: '0.1em' }}>
|
||||
{label}
|
||||
</span>
|
||||
<input
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { DTCard, DTStaggerContainer } from '@dangerousthings/react';
|
||||
import { DTCard } 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: '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: 'animations', title: 'Animations', desc: 'Entrance animations, 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 },
|
||||
];
|
||||
@@ -31,7 +31,6 @@ export function HomePage() {
|
||||
fontWeight: 900,
|
||||
letterSpacing: '0.15em',
|
||||
marginBottom: '0.25rem',
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
DANGEROUS THINGS
|
||||
</h1>
|
||||
@@ -40,7 +39,6 @@ export function HomePage() {
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.2em',
|
||||
textTransform: 'uppercase',
|
||||
marginTop: 0,
|
||||
}}>
|
||||
DESIGN SYSTEM
|
||||
@@ -71,16 +69,15 @@ export function HomePage() {
|
||||
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">
|
||||
<div 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()}>
|
||||
<DTCard variant={cat.mode} title={cat.title}>
|
||||
<div className="card-body">{cat.desc}</div>
|
||||
{cat.count > 0 && (
|
||||
<div style={{ color: 'var(--color-text-muted)', fontSize: '0.7rem', marginTop: 'var(--space-2)' }}>
|
||||
@@ -90,7 +87,7 @@ export function HomePage() {
|
||||
</DTCard>
|
||||
</a>
|
||||
))}
|
||||
</DTStaggerContainer>
|
||||
</div>
|
||||
|
||||
<Section title="Quick Start" description="Import the React components and CSS to get started.">
|
||||
<div className="terminal dt-accent-top">
|
||||
|
||||
@@ -43,7 +43,6 @@ body {
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--color-primary);
|
||||
text-transform: uppercase;
|
||||
padding-bottom: var(--space-4);
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
}
|
||||
@@ -65,7 +64,6 @@ body {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.05em;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
@@ -93,7 +91,6 @@ body {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
@@ -117,7 +114,6 @@ body {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
@@ -151,7 +147,6 @@ body {
|
||||
font-weight: 700;
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: var(--space-2);
|
||||
padding-bottom: var(--space-2);
|
||||
@@ -175,7 +170,6 @@ body {
|
||||
.demo-label {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.6875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: var(--space-2);
|
||||
margin-top: var(--space-4);
|
||||
@@ -200,7 +194,6 @@ body {
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
@@ -244,7 +237,6 @@ body {
|
||||
.card-title {
|
||||
font-weight: 900;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
@@ -260,7 +252,6 @@ body {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
@@ -300,7 +291,6 @@ input:focus {
|
||||
font-weight: 900;
|
||||
font-size: 1.75rem;
|
||||
color: var(--color-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "@dangerousthings/showcase-mobile",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"main": "./index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
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';
|
||||
@@ -20,44 +17,8 @@ export function AnimationsScreen() {
|
||||
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"
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
DTCard,
|
||||
DTChip,
|
||||
DTBadgeOverlay,
|
||||
DTStaggerContainer,
|
||||
useDTTheme,
|
||||
} from '@dangerousthings/react-native';
|
||||
import type { DTVariant } from '@dangerousthings/react-native';
|
||||
@@ -83,29 +82,6 @@ export function CardsAdvancedScreen() {
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,14 +58,14 @@ const categories: {
|
||||
},
|
||||
{
|
||||
title: 'ADVANCED CARDS',
|
||||
subtitle: 'Selected state, progress bar, badge overlays, stagger',
|
||||
subtitle: 'Selected state, progress bar, badge overlays',
|
||||
mode: 'emphasis',
|
||||
route: 'CardsAdvanced',
|
||||
count: 4,
|
||||
},
|
||||
{
|
||||
title: 'ANIMATIONS',
|
||||
subtitle: 'DTStaggerContainer, useScaleIn, usePulse',
|
||||
subtitle: 'useScaleIn, usePulse',
|
||||
mode: 'success',
|
||||
route: 'Animations',
|
||||
count: 3,
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @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
|
||||
|
||||
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer in react-native, updated DTGallery/DTModal/DTMobileFilterOverlay components.
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dangerousthings/web",
|
||||
"version": "0.3.0",
|
||||
"version": "0.5.1",
|
||||
"description": "Web CSS theme for the Dangerous Things design system — bevels, glows, form styles",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
|
||||
@@ -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 {
|
||||
@@ -82,49 +90,16 @@
|
||||
|
||||
/* ============================================================================
|
||||
Stagger Container
|
||||
Source: storefront framer-motion staggered card entrance
|
||||
Usage: <div class="dt-stagger-container">...</div>
|
||||
Customize: --dt-stagger-duration, --dt-stagger-interval
|
||||
============================================================================ */
|
||||
|
||||
.dt-stagger-container {
|
||||
--dt-stagger-duration: 0.33s;
|
||||
--dt-stagger-interval: 75ms;
|
||||
}
|
||||
|
||||
.dt-stagger-container > * {
|
||||
animation: dt-scale-in var(--dt-stagger-duration) ease-in-out both;
|
||||
}
|
||||
|
||||
.dt-stagger-container > :nth-child(1) { animation-delay: calc(0 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(2) { animation-delay: calc(1 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(3) { animation-delay: calc(2 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(4) { animation-delay: calc(3 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(5) { animation-delay: calc(4 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(6) { animation-delay: calc(5 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(7) { animation-delay: calc(6 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(8) { animation-delay: calc(7 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(9) { animation-delay: calc(8 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(10) { animation-delay: calc(9 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(11) { animation-delay: calc(10 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(12) { animation-delay: calc(11 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(13) { animation-delay: calc(12 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(14) { animation-delay: calc(13 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(15) { animation-delay: calc(14 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(16) { animation-delay: calc(15 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(17) { animation-delay: calc(16 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(18) { animation-delay: calc(17 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(19) { animation-delay: calc(18 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(20) { animation-delay: calc(19 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(21) { animation-delay: calc(20 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(22) { animation-delay: calc(21 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(23) { animation-delay: calc(22 * var(--dt-stagger-interval)); }
|
||||
.dt-stagger-container > :nth-child(24) { animation-delay: calc(23 * var(--dt-stagger-interval)); }
|
||||
|
||||
.dt-stagger-container > :nth-child(n+25) {
|
||||
animation-delay: calc(24 * var(--dt-stagger-interval));
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Transition Utilities
|
||||
============================================================================ */
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
[data-brand="dt"] .card {
|
||||
--dt-card-progress: 0;
|
||||
--_card-pad: var(--space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background: var(--dt-card-color, var(--color-primary));
|
||||
@@ -44,17 +46,21 @@
|
||||
z-index: -1;
|
||||
/* Inner clip-path: left edge at bevel-sm to create progress bar zone.
|
||||
The progress bar occupies x=0 to x=bevel-sm with its own borders.
|
||||
Inner surface starts at bevel-sm, so the left frame is wider than 3px. */
|
||||
Inner surface starts at bevel-sm, so the left frame is wider than 3px.
|
||||
Diagonal vertices use bevel-md + 3px (not bevel-md alone) so the
|
||||
perpendicular border width on the 45-degree diagonal visually matches
|
||||
the 3px straight edges despite sub-pixel antialiasing. */
|
||||
clip-path: polygon(var(--bevel-sm) 3px,
|
||||
calc(100% - 3px) 3px,
|
||||
calc(100% - 3px) calc(100% - var(--bevel-md)),
|
||||
calc(100% - var(--bevel-md)) calc(100% - 3px),
|
||||
calc(100% - 3px) calc(100% - var(--bevel-md) - 3px),
|
||||
calc(100% - var(--bevel-md) - 3px) calc(100% - 3px),
|
||||
var(--bevel-sm) calc(100% - 3px));
|
||||
}
|
||||
|
||||
/* Card Progress Bar — structural fill area on the left edge
|
||||
Sits between the 3px frame borders, follows bottom-left bevel diagonal.
|
||||
Gradient fills from bottom (accent) to top (surface) based on --dt-card-progress.
|
||||
Gradient fills top-to-bottom based on --dt-card-progress so the card
|
||||
visually "opens" as the bar descends.
|
||||
Filled portion: fully opaque accent color. Unfilled portion: semi-transparent surface.
|
||||
At 0%: all semi-transparent surface. At 100%: all opaque accent.
|
||||
Override --dt-progress-empty-opacity to control unfilled portion transparency. */
|
||||
@@ -70,7 +76,7 @@
|
||||
calc(var(--bevel-sm) - 3px) calc(100% - 6px),
|
||||
3px calc(100% - var(--bevel-sm)));
|
||||
|
||||
background: linear-gradient(to top,
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(var(--dt-card-color-rgb, var(--color-primary-rgb)), 1) calc(var(--dt-card-progress, 0) * 1%),
|
||||
rgba(var(--color-surface-rgb),
|
||||
var(--dt-progress-empty-opacity, 0.6)) calc(var(--dt-card-progress, 0) * 1%));
|
||||
@@ -79,12 +85,32 @@
|
||||
}
|
||||
|
||||
/* Card children — ensure body content renders above the progress bar (::after z-index:1) */
|
||||
[data-brand="dt"] .dt-bevel-card > *:not(.dt-card-badge),
|
||||
[data-brand="dt"] .card > *:not(.dt-card-badge) {
|
||||
[data-brand="dt"] .dt-bevel-card > *:not(.dt-badge-bottom-right):not(.dt-card-badge),
|
||||
[data-brand="dt"] .card > *:not(.dt-badge-bottom-right):not(.dt-card-badge) {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Card Grow — stretch to fill available space in flex/grid containers.
|
||||
By default the body (content area) grows. Use .dt-card-grow-title to
|
||||
grow the header instead. */
|
||||
.dt-card-grow {
|
||||
flex: 1 1 0%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dt-card-grow > .card-title {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.dt-card-grow:not(.dt-card-grow-title) > :last-child {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.dt-card-grow.dt-card-grow-title > .card-title {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/* Card Header — storefront .card-header pattern
|
||||
Title starts at bevel-sm (past the progress bar zone) and extends to the right edge.
|
||||
Below it, the ::before dark surface shows through for the body area. */
|
||||
@@ -102,7 +128,6 @@
|
||||
background: var(--dt-card-color, var(--color-primary));
|
||||
color: var(--color-bg);
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 1rem;
|
||||
position: relative;
|
||||
@@ -125,17 +150,16 @@
|
||||
background: var(--dt-card-color, var(--color-primary));
|
||||
}
|
||||
|
||||
/* Bottom-right bevel on card body content (img/video). Clip-path matches
|
||||
/* Bottom-right bevel on card body content. Clip-path matches
|
||||
the card's inner surface bevel so the 3px structural border is visible
|
||||
at the diagonal corner. Badge is not clipped (it's a sibling, not img). */
|
||||
[data-brand="dt"] .card-body-flush > img,
|
||||
[data-brand="dt"] .card-body-flush > video {
|
||||
at the diagonal corner. Badge overlays are excluded. */
|
||||
[data-brand="dt"] .card-body-flush > *:not(.dt-badge-bottom-right):not(.dt-card-badge) {
|
||||
display: block;
|
||||
clip-path: polygon(
|
||||
0% 0%,
|
||||
100% 0%,
|
||||
100% calc(100% - var(--bevel-md) + 3px),
|
||||
calc(100% - var(--bevel-md) + 3px) 100%,
|
||||
100% calc(100% - var(--bevel-md)),
|
||||
calc(100% - var(--bevel-md)) 100%,
|
||||
0% 100%);
|
||||
}
|
||||
|
||||
@@ -202,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);
|
||||
@@ -272,6 +299,37 @@
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
/* Mode-variant badges — bridge .mode-* vars to badge color vars */
|
||||
[data-brand="dt"] .badge.mode-normal {
|
||||
--badge-border-color: var(--mode-normal);
|
||||
--badge-fill: var(--mode-normal);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
[data-brand="dt"] .badge.mode-emphasis {
|
||||
--badge-border-color: var(--mode-emphasis);
|
||||
--badge-fill: var(--mode-emphasis);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
[data-brand="dt"] .badge.mode-warning {
|
||||
--badge-border-color: var(--mode-warning);
|
||||
--badge-fill: var(--mode-warning);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
[data-brand="dt"] .badge.mode-success {
|
||||
--badge-border-color: var(--mode-success);
|
||||
--badge-fill: var(--mode-success);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
[data-brand="dt"] .badge.mode-other {
|
||||
--badge-border-color: var(--mode-other);
|
||||
--badge-fill: var(--mode-other);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Opt-in Bevel Utility Classes
|
||||
Use these for custom elements that need bevel shapes
|
||||
@@ -326,7 +384,9 @@
|
||||
}
|
||||
|
||||
.dt-bevel-media > img,
|
||||
.dt-bevel-media > video {
|
||||
.dt-bevel-media > video,
|
||||
.dt-bevel-media > model-viewer,
|
||||
.dt-bevel-media > iframe {
|
||||
display: block;
|
||||
clip-path: polygon(
|
||||
calc(var(--bevel-md) + 1px) 3px,
|
||||
@@ -337,11 +397,20 @@
|
||||
3px calc(var(--bevel-md) + 1px));
|
||||
}
|
||||
|
||||
/* Video letterboxing — respect natural aspect ratio with black bars */
|
||||
.dt-bevel-media > video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
@@ -352,7 +421,6 @@
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -360,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;
|
||||
@@ -369,7 +437,6 @@
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
@@ -492,15 +559,17 @@
|
||||
/* ============================================================================
|
||||
Card Badge — bottom-right product type chip (LAB, BUNDLE, etc.)
|
||||
Source: dt-shopify-storefront CyberProductCard LabChip / BundleChip
|
||||
Plain flat rectangle. Inherits card color via --dt-card-color.
|
||||
Positioned at card level so the card's clip-path bevels the badge corner.
|
||||
Consumer provides badge content/styling; card handles placement.
|
||||
============================================================================ */
|
||||
/* Badge parent — provides position context for absolutely-positioned badges.
|
||||
Use on the image container inside a card, or on any element that wraps content + badge. */
|
||||
|
||||
/* Legacy: standalone badge parent for non-card contexts */
|
||||
.dt-badge-parent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dt-card-badge {
|
||||
/* Legacy: standalone badge positioning for non-card contexts */
|
||||
.dt-badge-bottom-right {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
@@ -508,19 +577,32 @@
|
||||
padding: 2px calc(8px + 0.2rem) 2px 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
background-color: var(--dt-card-color, var(--color-primary));
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
/* Inside card body-flush, offset badge position to compensate for the
|
||||
3px margin from the card edge. This makes the badge position relative
|
||||
to the card's bevel corner match the media frame's placement exactly
|
||||
(8px from card edge in both cases). */
|
||||
.card-body-flush > .dt-card-badge {
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
/* Card-level badge slot — positioned so the card's bevel clip-path
|
||||
clips the badge's bottom-right corner at 45 degrees.
|
||||
Values tuned so the badge is mostly visible with just the corner clipped. */
|
||||
.card > .dt-card-badge,
|
||||
.dt-bevel-card > .dt-card-badge {
|
||||
position: absolute;
|
||||
bottom: calc(var(--bevel-md) * 0.25);
|
||||
right: calc(var(--bevel-md) * -0.15);
|
||||
z-index: 4;
|
||||
background: var(--dt-card-color, var(--color-primary));
|
||||
color: var(--color-bg);
|
||||
padding: 0.35em 2em 0.35em 1em;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
[data-brand="classic"] .card > .dt-card-badge,
|
||||
[data-brand="classic"] .dt-bevel-card > .dt-card-badge {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
@@ -542,7 +624,6 @@
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-fast),
|
||||
color var(--transition-fast),
|
||||
|
||||
@@ -251,7 +251,6 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.05em;
|
||||
@@ -351,7 +350,6 @@
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
@@ -420,7 +418,6 @@
|
||||
border-top: 5px solid var(--color-primary);
|
||||
border-bottom: 1px solid rgba(var(--color-primary-rgb), 0.2);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user