fix: Restore all files lost during destructive rebase

A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,188 @@
'use client'
import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react'
import { MOTION, LAYERS } from './depth-system'
import { usePerformance } from './PerformanceContext'
/**
* Focus Context - Manages focus mode for the UI
*
* When an element enters "focus mode", the rest of the UI dims and blurs,
* creating a spotlight effect that helps users concentrate on the task at hand.
*
* This is particularly useful for:
* - Replying to messages
* - Editing content
* - Modal-like interactions without actual modals
*/
interface FocusContextType {
/** Whether focus mode is active */
isFocused: boolean
/** The ID of the focused element (if any) */
focusedElementId: string | null
/** Enter focus mode */
enterFocus: (elementId: string) => void
/** Exit focus mode */
exitFocus: () => void
/** Toggle focus mode */
toggleFocus: (elementId: string) => void
}
const FocusContext = createContext<FocusContextType | null>(null)
interface FocusProviderProps {
children: React.ReactNode
/** Duration of the focus transition in ms */
transitionDuration?: number
/** Blur amount for unfocused elements (px) */
blurAmount?: number
/** Dim amount for unfocused elements (0-1) */
dimAmount?: number
}
export function FocusProvider({
children,
transitionDuration = 300,
blurAmount = 4,
dimAmount = 0.6,
}: FocusProviderProps) {
const [isFocused, setIsFocused] = useState(false)
const [focusedElementId, setFocusedElementId] = useState<string | null>(null)
const { settings } = usePerformance()
const enterFocus = useCallback((elementId: string) => {
setFocusedElementId(elementId)
setIsFocused(true)
}, [])
const exitFocus = useCallback(() => {
setIsFocused(false)
// Delay clearing the ID to allow exit animation
setTimeout(() => {
setFocusedElementId(null)
}, transitionDuration)
}, [transitionDuration])
const toggleFocus = useCallback(
(elementId: string) => {
if (isFocused && focusedElementId === elementId) {
exitFocus()
} else {
enterFocus(elementId)
}
},
[isFocused, focusedElementId, enterFocus, exitFocus]
)
// Keyboard handler for Escape
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isFocused) {
exitFocus()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isFocused, exitFocus])
// Adaptive values based on performance
const adaptiveBlur = settings.enableBlur ? blurAmount * settings.blurIntensity : 0
const adaptiveDuration = Math.round(transitionDuration * settings.animationSpeed)
return (
<FocusContext.Provider
value={{
isFocused,
focusedElementId,
enterFocus,
exitFocus,
toggleFocus,
}}
>
{/* Focus backdrop - dims and blurs unfocused content */}
<div
style={{
position: 'fixed',
inset: 0,
zIndex: LAYERS.floating.zIndex,
pointerEvents: isFocused ? 'auto' : 'none',
opacity: isFocused ? 1 : 0,
backgroundColor: `rgba(0, 0, 0, ${isFocused ? dimAmount : 0})`,
backdropFilter: isFocused && adaptiveBlur > 0 ? `blur(${adaptiveBlur}px)` : 'none',
WebkitBackdropFilter: isFocused && adaptiveBlur > 0 ? `blur(${adaptiveBlur}px)` : 'none',
transition: `
opacity ${adaptiveDuration}ms ${MOTION.standard.easing},
backdrop-filter ${adaptiveDuration}ms ${MOTION.standard.easing}
`,
}}
onClick={exitFocus}
aria-hidden={!isFocused}
/>
{children}
</FocusContext.Provider>
)
}
export function useFocus() {
const context = useContext(FocusContext)
if (!context) {
return {
isFocused: false,
focusedElementId: null,
enterFocus: () => {},
exitFocus: () => {},
toggleFocus: () => {},
}
}
return context
}
/**
* Hook to check if a specific element is focused
*/
export function useIsFocused(elementId: string): boolean {
const { isFocused, focusedElementId } = useFocus()
return isFocused && focusedElementId === elementId
}
/**
* FocusTarget - Wrapper that makes children focusable
*/
interface FocusTargetProps {
children: React.ReactNode
/** Unique ID for this focus target */
id: string
/** Additional class names */
className?: string
/** Style overrides */
style?: React.CSSProperties
}
export function FocusTarget({ children, id, className = '', style }: FocusTargetProps) {
const { isFocused, focusedElementId } = useFocus()
const { settings } = usePerformance()
const isThisElement = focusedElementId === id
const shouldElevate = isFocused && isThisElement
const duration = Math.round(MOTION.emphasis.duration * settings.animationSpeed)
return (
<div
className={className}
style={{
...style,
position: 'relative',
zIndex: shouldElevate ? LAYERS.overlay.zIndex + 1 : 'auto',
transition: `z-index ${duration}ms ${MOTION.standard.easing}`,
}}
data-focus-target={id}
data-focused={shouldElevate}
>
{children}
</div>
)
}

View File

@@ -0,0 +1,374 @@
'use client'
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'
/**
* Performance Context - Adaptive Quality System
*
* Monitors device capabilities and runtime performance to automatically
* adjust visual quality. This ensures smooth 60fps (or 120fps on capable devices)
* by degrading effects when needed.
*/
// =============================================================================
// TYPES
// =============================================================================
export type QualityLevel = 'high' | 'medium' | 'low' | 'minimal'
export interface PerformanceMetrics {
/** Frames per second (rolling average) */
fps: number
/** Dropped frames in last second */
droppedFrames: number
/** Device memory in GB (if available) */
deviceMemory: number | null
/** Number of logical CPU cores */
hardwareConcurrency: number
/** Whether device prefers reduced motion */
prefersReducedMotion: boolean
/** Whether device is on battery/power-save */
isLowPowerMode: boolean
/** Current quality level */
qualityLevel: QualityLevel
}
export interface QualitySettings {
/** Enable backdrop blur effects */
enableBlur: boolean
/** Blur intensity multiplier (0-1) */
blurIntensity: number
/** Enable shadow effects */
enableShadows: boolean
/** Shadow complexity (0-1) */
shadowComplexity: number
/** Enable parallax effects */
enableParallax: boolean
/** Parallax intensity multiplier (0-1) */
parallaxIntensity: number
/** Enable spring animations */
enableSpringAnimations: boolean
/** Animation duration multiplier (0.5-1.5) */
animationSpeed: number
/** Enable typewriter effects */
enableTypewriter: boolean
/** Max concurrent animations */
maxConcurrentAnimations: number
}
interface PerformanceContextType {
metrics: PerformanceMetrics
settings: QualitySettings
/** Force a specific quality level (null = auto) */
forceQuality: (level: QualityLevel | null) => void
/** Report that an animation started */
reportAnimationStart: () => void
/** Report that an animation ended */
reportAnimationEnd: () => void
/** Check if we can start another animation */
canStartAnimation: () => boolean
}
// =============================================================================
// DEFAULT VALUES
// =============================================================================
const DEFAULT_METRICS: PerformanceMetrics = {
fps: 60,
droppedFrames: 0,
deviceMemory: null,
hardwareConcurrency: 4,
prefersReducedMotion: false,
isLowPowerMode: false,
qualityLevel: 'high',
}
const QUALITY_PRESETS: Record<QualityLevel, QualitySettings> = {
high: {
enableBlur: true,
blurIntensity: 1,
enableShadows: true,
shadowComplexity: 1,
enableParallax: true,
parallaxIntensity: 1,
enableSpringAnimations: true,
animationSpeed: 1,
enableTypewriter: true,
maxConcurrentAnimations: 10,
},
medium: {
enableBlur: true,
blurIntensity: 0.7,
enableShadows: true,
shadowComplexity: 0.7,
enableParallax: true,
parallaxIntensity: 0.5,
enableSpringAnimations: true,
animationSpeed: 0.9,
enableTypewriter: true,
maxConcurrentAnimations: 6,
},
low: {
enableBlur: false,
blurIntensity: 0,
enableShadows: true,
shadowComplexity: 0.4,
enableParallax: false,
parallaxIntensity: 0,
enableSpringAnimations: false,
animationSpeed: 0.7,
enableTypewriter: true,
maxConcurrentAnimations: 3,
},
minimal: {
enableBlur: false,
blurIntensity: 0,
enableShadows: false,
shadowComplexity: 0,
enableParallax: false,
parallaxIntensity: 0,
enableSpringAnimations: false,
animationSpeed: 0.5,
enableTypewriter: false,
maxConcurrentAnimations: 1,
},
}
// =============================================================================
// CONTEXT
// =============================================================================
const PerformanceContext = createContext<PerformanceContextType | null>(null)
// =============================================================================
// PROVIDER
// =============================================================================
export function PerformanceProvider({ children }: { children: React.ReactNode }) {
const [metrics, setMetrics] = useState<PerformanceMetrics>(DEFAULT_METRICS)
const [forcedQuality, setForcedQuality] = useState<QualityLevel | null>(null)
const [activeAnimations, setActiveAnimations] = useState(0)
const frameTimesRef = useRef<number[]>([])
const lastFrameTimeRef = useRef<number>(performance.now())
const rafIdRef = useRef<number | null>(null)
// Detect device capabilities on mount
useEffect(() => {
if (typeof window === 'undefined') return
const detectCapabilities = () => {
// Hardware concurrency
const cores = navigator.hardwareConcurrency || 4
// Device memory (Chrome only)
const memory = (navigator as any).deviceMemory || null
// Reduced motion preference
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
// Battery/power save mode (approximation)
let lowPower = false
if ('getBattery' in navigator) {
(navigator as any).getBattery?.().then((battery: any) => {
lowPower = battery.level < 0.2 && !battery.charging
setMetrics(prev => ({ ...prev, isLowPowerMode: lowPower }))
})
}
// Determine initial quality level
let initialQuality: QualityLevel = 'high'
if (reducedMotion) {
initialQuality = 'minimal'
} else if (cores <= 2 || (memory && memory <= 2)) {
initialQuality = 'low'
} else if (cores <= 4 || (memory && memory <= 4)) {
initialQuality = 'medium'
}
setMetrics(prev => ({
...prev,
hardwareConcurrency: cores,
deviceMemory: memory,
prefersReducedMotion: reducedMotion,
qualityLevel: initialQuality,
}))
}
detectCapabilities()
// Listen for reduced motion changes
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
const handleChange = (e: MediaQueryListEvent) => {
setMetrics(prev => ({
...prev,
prefersReducedMotion: e.matches,
qualityLevel: e.matches ? 'minimal' : prev.qualityLevel,
}))
}
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}, [])
// FPS monitoring loop
useEffect(() => {
if (typeof window === 'undefined') return
let frameCount = 0
let lastFpsUpdate = performance.now()
const measureFrame = (timestamp: number) => {
const delta = timestamp - lastFrameTimeRef.current
lastFrameTimeRef.current = timestamp
// Track frame times (keep last 60)
frameTimesRef.current.push(delta)
if (frameTimesRef.current.length > 60) {
frameTimesRef.current.shift()
}
frameCount++
// Update FPS every second
if (timestamp - lastFpsUpdate >= 1000) {
const avgFrameTime =
frameTimesRef.current.reduce((a, b) => a + b, 0) / frameTimesRef.current.length
const fps = Math.round(1000 / avgFrameTime)
// Count dropped frames (frames > 20ms = dropped at 60fps)
const dropped = frameTimesRef.current.filter(t => t > 20).length
setMetrics(prev => {
// Auto-adjust quality based on performance
let newQuality = prev.qualityLevel
if (!forcedQuality) {
if (dropped > 10 && prev.qualityLevel !== 'minimal') {
// Downgrade
const levels: QualityLevel[] = ['high', 'medium', 'low', 'minimal']
const currentIndex = levels.indexOf(prev.qualityLevel)
newQuality = levels[Math.min(currentIndex + 1, levels.length - 1)]
} else if (dropped === 0 && fps >= 58 && prev.qualityLevel !== 'high') {
// Consider upgrade (only if stable for a while)
// This is conservative - we don't want to oscillate
}
}
return {
...prev,
fps,
droppedFrames: dropped,
qualityLevel: forcedQuality || newQuality,
}
})
frameCount = 0
lastFpsUpdate = timestamp
}
rafIdRef.current = requestAnimationFrame(measureFrame)
}
rafIdRef.current = requestAnimationFrame(measureFrame)
return () => {
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current)
}
}
}, [forcedQuality])
// Get current settings based on quality level
const settings = QUALITY_PRESETS[forcedQuality || metrics.qualityLevel]
// Force quality level
const forceQuality = useCallback((level: QualityLevel | null) => {
setForcedQuality(level)
if (level) {
setMetrics(prev => ({ ...prev, qualityLevel: level }))
}
}, [])
// Animation tracking
const reportAnimationStart = useCallback(() => {
setActiveAnimations(prev => prev + 1)
}, [])
const reportAnimationEnd = useCallback(() => {
setActiveAnimations(prev => Math.max(0, prev - 1))
}, [])
const canStartAnimation = useCallback(() => {
return activeAnimations < settings.maxConcurrentAnimations
}, [activeAnimations, settings.maxConcurrentAnimations])
return (
<PerformanceContext.Provider
value={{
metrics,
settings,
forceQuality,
reportAnimationStart,
reportAnimationEnd,
canStartAnimation,
}}
>
{children}
</PerformanceContext.Provider>
)
}
// =============================================================================
// HOOKS
// =============================================================================
export function usePerformance() {
const context = useContext(PerformanceContext)
if (!context) {
// Return defaults if used outside provider
return {
metrics: DEFAULT_METRICS,
settings: QUALITY_PRESETS.high,
forceQuality: () => {},
reportAnimationStart: () => {},
reportAnimationEnd: () => {},
canStartAnimation: () => true,
}
}
return context
}
/**
* Hook to get adaptive blur value
*/
export function useAdaptiveBlur(baseBlur: number): number {
const { settings } = usePerformance()
if (!settings.enableBlur) return 0
return Math.round(baseBlur * settings.blurIntensity)
}
/**
* Hook to get adaptive shadow
*/
export function useAdaptiveShadow(baseShadow: string, fallbackShadow: string = 'none'): string {
const { settings } = usePerformance()
if (!settings.enableShadows) return fallbackShadow
// Could also reduce shadow complexity here
return baseShadow
}
/**
* Hook for reduced motion check
*/
export function usePrefersReducedMotion(): boolean {
const { metrics } = usePerformance()
return metrics.prefersReducedMotion
}
/**
* Hook to get animation duration with performance adjustment
*/
export function useAdaptiveAnimationDuration(baseDuration: number): number {
const { settings } = usePerformance()
return Math.round(baseDuration * settings.animationSpeed)
}

View File

@@ -0,0 +1,335 @@
/**
* Spatial UI - Depth System
*
* Design tokens for creating consistent depth and layering across the UI.
* Based on the "Fake-3D without 3D" principle - 2.5D composition using
* z-hierarchy, shadows, blur, and subtle motion.
*/
// =============================================================================
// LAYER DEFINITIONS
// =============================================================================
export const LAYERS = {
/** Base content layer - main page content */
base: {
name: 'base',
zIndex: 0,
elevation: 0,
},
/** Content cards, lists, primary UI elements */
content: {
name: 'content',
zIndex: 10,
elevation: 1,
},
/** Floating elements, dropdowns, popovers */
floating: {
name: 'floating',
zIndex: 20,
elevation: 2,
},
/** Sticky headers, navigation */
sticky: {
name: 'sticky',
zIndex: 30,
elevation: 2,
},
/** Overlays, notifications, chat bubbles */
overlay: {
name: 'overlay',
zIndex: 40,
elevation: 3,
},
/** Modal dialogs */
modal: {
name: 'modal',
zIndex: 50,
elevation: 4,
},
/** Tooltips, cursor assists */
tooltip: {
name: 'tooltip',
zIndex: 60,
elevation: 5,
},
} as const
export type LayerName = keyof typeof LAYERS
// =============================================================================
// SHADOW DEFINITIONS (Dynamic based on elevation)
// =============================================================================
export const SHADOWS = {
/** No shadow - flat on surface */
none: 'none',
/** Subtle lift - barely visible */
xs: '0 1px 2px rgba(0,0,0,0.05)',
/** Small lift - cards at rest */
sm: '0 2px 4px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
/** Medium lift - cards on hover */
md: '0 4px 8px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.04)',
/** Large lift - active/dragging */
lg: '0 8px 16px rgba(0,0,0,0.10), 0 4px 8px rgba(0,0,0,0.06)',
/** Extra large - floating overlays */
xl: '0 12px 24px rgba(0,0,0,0.12), 0 6px 12px rgba(0,0,0,0.08)',
/** 2XL - modals, maximum elevation */
'2xl': '0 20px 40px rgba(0,0,0,0.15), 0 10px 20px rgba(0,0,0,0.10)',
/** Glow effect for focus states */
glow: (color: string, intensity: number = 0.3) =>
`0 0 20px rgba(${color}, ${intensity}), 0 4px 12px rgba(0,0,0,0.1)`,
} as const
// Dark mode shadows (slightly more pronounced)
export const SHADOWS_DARK = {
none: 'none',
xs: '0 1px 3px rgba(0,0,0,0.2)',
sm: '0 2px 6px rgba(0,0,0,0.25), 0 1px 3px rgba(0,0,0,0.15)',
md: '0 4px 12px rgba(0,0,0,0.3), 0 2px 6px rgba(0,0,0,0.2)',
lg: '0 8px 20px rgba(0,0,0,0.35), 0 4px 10px rgba(0,0,0,0.25)',
xl: '0 12px 32px rgba(0,0,0,0.4), 0 6px 16px rgba(0,0,0,0.3)',
'2xl': '0 20px 50px rgba(0,0,0,0.5), 0 10px 25px rgba(0,0,0,0.35)',
glow: (color: string, intensity: number = 0.4) =>
`0 0 30px rgba(${color}, ${intensity}), 0 4px 16px rgba(0,0,0,0.3)`,
} as const
// =============================================================================
// BLUR DEFINITIONS (Material simulation)
// =============================================================================
export const BLUR = {
/** No blur */
none: 0,
/** Subtle - barely perceptible */
xs: 4,
/** Small - light frosted glass */
sm: 8,
/** Medium - standard glass effect */
md: 12,
/** Large - heavy frosted glass */
lg: 16,
/** Extra large - maximum blur */
xl: 24,
/** 2XL - extreme blur for backgrounds */
'2xl': 32,
} as const
// =============================================================================
// MOTION DEFINITIONS (Physics-based transitions)
// =============================================================================
export const MOTION = {
/** Micro-interactions (hover, focus) */
micro: {
duration: 150,
easing: 'cubic-bezier(0.25, 0.1, 0.25, 1)',
},
/** Standard transitions */
standard: {
duration: 220,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
},
/** Emphasis (entering elements) */
emphasis: {
duration: 300,
easing: 'cubic-bezier(0.0, 0, 0.2, 1)',
},
/** Spring-like bounce */
spring: {
duration: 400,
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
},
/** Deceleration (entering from off-screen) */
decelerate: {
duration: 350,
easing: 'cubic-bezier(0, 0, 0.2, 1)',
},
/** Acceleration (exiting to off-screen) */
accelerate: {
duration: 250,
easing: 'cubic-bezier(0.4, 0, 1, 1)',
},
} as const
// =============================================================================
// PARALLAX DEFINITIONS (Subtle depth cues)
// =============================================================================
export const PARALLAX = {
/** No parallax */
none: 0,
/** Minimal - barely perceptible (1-2px) */
subtle: 0.02,
/** Light - noticeable but not distracting (2-4px) */
light: 0.04,
/** Medium - clear depth separation (4-6px) */
medium: 0.06,
/** Strong - pronounced effect (use sparingly) */
strong: 0.1,
} as const
// =============================================================================
// MATERIAL DEFINITIONS (Surface types)
// =============================================================================
export interface Material {
background: string
backgroundDark: string
blur: number
opacity: number
border: string
borderDark: string
}
export const MATERIALS: Record<string, Material> = {
/** Solid surface - no transparency */
solid: {
background: 'rgb(255, 255, 255)',
backgroundDark: 'rgb(15, 23, 42)',
blur: 0,
opacity: 1,
border: 'rgba(0, 0, 0, 0.1)',
borderDark: 'rgba(255, 255, 255, 0.1)',
},
/** Frosted glass - standard glassmorphism */
glass: {
background: 'rgba(255, 255, 255, 0.7)',
backgroundDark: 'rgba(255, 255, 255, 0.1)',
blur: 12,
opacity: 0.7,
border: 'rgba(0, 0, 0, 0.1)',
borderDark: 'rgba(255, 255, 255, 0.2)',
},
/** Thin glass - more transparent */
thinGlass: {
background: 'rgba(255, 255, 255, 0.5)',
backgroundDark: 'rgba(255, 255, 255, 0.05)',
blur: 8,
opacity: 0.5,
border: 'rgba(0, 0, 0, 0.05)',
borderDark: 'rgba(255, 255, 255, 0.1)',
},
/** Thick glass - more opaque */
thickGlass: {
background: 'rgba(255, 255, 255, 0.85)',
backgroundDark: 'rgba(255, 255, 255, 0.15)',
blur: 16,
opacity: 0.85,
border: 'rgba(0, 0, 0, 0.15)',
borderDark: 'rgba(255, 255, 255, 0.25)',
},
/** Acrylic - Windows 11 style */
acrylic: {
background: 'rgba(255, 255, 255, 0.6)',
backgroundDark: 'rgba(30, 30, 30, 0.6)',
blur: 20,
opacity: 0.6,
border: 'rgba(255, 255, 255, 0.3)',
borderDark: 'rgba(255, 255, 255, 0.15)',
},
}
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
/**
* Get shadow based on elevation level and theme
*/
export function getShadow(
elevation: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl',
isDark: boolean = false
): string {
return isDark ? SHADOWS_DARK[elevation] : SHADOWS[elevation]
}
/**
* Get CSS transition string for depth changes
*/
export function getDepthTransition(motion: keyof typeof MOTION = 'standard'): string {
const m = MOTION[motion]
return `box-shadow ${m.duration}ms ${m.easing}, transform ${m.duration}ms ${m.easing}`
}
/**
* Get transform for elevation effect (slight scale + Y offset)
*/
export function getElevationTransform(
elevation: 'rest' | 'hover' | 'active' | 'dragging'
): string {
switch (elevation) {
case 'rest':
return 'translateY(0) scale(1)'
case 'hover':
return 'translateY(-2px) scale(1.01)'
case 'active':
return 'translateY(0) scale(0.99)'
case 'dragging':
return 'translateY(-4px) scale(1.02)'
default:
return 'translateY(0) scale(1)'
}
}
/**
* Calculate parallax offset based on cursor position
*/
export function calculateParallax(
cursorX: number,
cursorY: number,
elementRect: DOMRect,
intensity: number = PARALLAX.subtle
): { x: number; y: number } {
const centerX = elementRect.left + elementRect.width / 2
const centerY = elementRect.top + elementRect.height / 2
const deltaX = (cursorX - centerX) * intensity
const deltaY = (cursorY - centerY) * intensity
// Clamp to reasonable values
const maxOffset = 6
return {
x: Math.max(-maxOffset, Math.min(maxOffset, deltaX)),
y: Math.max(-maxOffset, Math.min(maxOffset, deltaY)),
}
}
/**
* Get CSS variables for a material
*/
export function getMaterialCSS(material: keyof typeof MATERIALS, isDark: boolean): string {
const m = MATERIALS[material]
return `
background: ${isDark ? m.backgroundDark : m.background};
backdrop-filter: blur(${m.blur}px);
-webkit-backdrop-filter: blur(${m.blur}px);
border-color: ${isDark ? m.borderDark : m.border};
`
}
// =============================================================================
// CSS CLASS GENERATORS (for Tailwind-like usage)
// =============================================================================
export const depthClasses = {
// Layer z-index
'layer-base': 'z-0',
'layer-content': 'z-10',
'layer-floating': 'z-20',
'layer-sticky': 'z-30',
'layer-overlay': 'z-40',
'layer-modal': 'z-50',
'layer-tooltip': 'z-60',
// Transitions
'depth-transition': 'transition-all duration-200 ease-out',
'depth-transition-spring': 'transition-all duration-400',
} as const

View File

@@ -0,0 +1,16 @@
/**
* Spatial UI System
*
* A design system for creating cinematic, depth-aware user interfaces.
* Implements "Fake-3D without 3D" - 2.5D composition using layering,
* shadows, blur, and physics-based motion.
*/
// Core systems
export * from './depth-system'
export * from './PerformanceContext'
export * from './FocusContext'
// Re-export common types
export type { QualityLevel, QualitySettings, PerformanceMetrics } from './PerformanceContext'
export type { LayerName, Material } from './depth-system'