diff --git a/README.md b/README.md index cfc18d9..9fedc7e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/packages/react-native/src/components/DTAccordion.tsx b/packages/react-native/src/components/DTAccordion.tsx index 375283c..e74cc80 100644 --- a/packages/react-native/src/components/DTAccordion.tsx +++ b/packages/react-native/src/components/DTAccordion.tsx @@ -251,7 +251,6 @@ const styles = StyleSheet.create({ title: { fontWeight: '600', letterSpacing: 0.5, - textTransform: 'uppercase', fontSize: 14, }, contentWrapper: {}, diff --git a/packages/react-native/src/components/DTButton.tsx b/packages/react-native/src/components/DTButton.tsx index ed9665f..9006d81 100644 --- a/packages/react-native/src/components/DTButton.tsx +++ b/packages/react-native/src/components/DTButton.tsx @@ -243,6 +243,5 @@ const styles = StyleSheet.create({ label: { fontWeight: '600', letterSpacing: 1, - textTransform: 'uppercase', }, }); diff --git a/packages/react-native/src/components/DTCard.tsx b/packages/react-native/src/components/DTCard.tsx index b189156..e9d4076 100644 --- a/packages/react-native/src/components/DTCard.tsx +++ b/packages/react-native/src/components/DTCard.tsx @@ -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({ )} {shouldShowHeader && ( - + {title && ( )} + {headerRight} )} - {children} + {children} {/* 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, diff --git a/packages/react-native/src/components/DTFeatureLegend.tsx b/packages/react-native/src/components/DTFeatureLegend.tsx index f7613ad..c6bad70 100644 --- a/packages/react-native/src/components/DTFeatureLegend.tsx +++ b/packages/react-native/src/components/DTFeatureLegend.tsx @@ -109,7 +109,6 @@ const styles = StyleSheet.create({ headerText: { fontWeight: '700', letterSpacing: 0.5, - textTransform: 'uppercase', }, grid: { flexDirection: 'row', diff --git a/packages/react-native/src/components/DTGallery.tsx b/packages/react-native/src/components/DTGallery.tsx index 8ddc48b..8eab2a6 100644 --- a/packages/react-native/src/components/DTGallery.tsx +++ b/packages/react-native/src/components/DTGallery.tsx @@ -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({ + + {'\u2715'} + + + ) : undefined} contentStyle={contentStyle}> {children} diff --git a/packages/react/src/components/DTDrawer.tsx b/packages/react/src/components/DTDrawer.tsx index 832d2ab..4c436ae 100644 --- a/packages/react/src/components/DTDrawer.tsx +++ b/packages/react/src/components/DTDrawer.tsx @@ -160,7 +160,6 @@ export function DTDrawer({ fontWeight: 900, fontSize: '2em', letterSpacing: '0.05em', - textTransform: 'uppercase', borderBottom: '1px solid var(--color-bg)', }}> {heading} diff --git a/packages/react/src/components/DTGallery.tsx b/packages/react/src/components/DTGallery.tsx index d2b9afa..c5d2d10 100644 --- a/packages/react/src/components/DTGallery.tsx +++ b/packages/react/src/components/DTGallery.tsx @@ -1,56 +1,168 @@ /** - * DTGallery — Image gallery with beveled media frame and thumbnails. + * DTGallery — Media gallery with beveled frame and thumbnails. + * Supports images, 3D models (via ), 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; - alt?: string; - thumbnail?: string; +/* ── TypeScript declaration for web component ── */ +declare global { + namespace JSX { + interface IntrinsicElements { + 'model-viewer': React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLElement + > & { + src?: string; + alt?: 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[]; /** 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, variant = 'normal', + aspectRatio = 1, className, style, }: DTGalleryProps) { const [activeIndex, setActiveIndex] = useState(0); + 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 (
- {/* Main image */} -
- {current.alt + {/* Main display */} +
+ {currentType === 'image' && ( + {current.alt + )} + + {currentType === 'model3d' && ( + + )} + + {currentType === 'video' && ( +