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:
188
studio-v2/lib/spatial-ui/FocusContext.tsx
Normal file
188
studio-v2/lib/spatial-ui/FocusContext.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
374
studio-v2/lib/spatial-ui/PerformanceContext.tsx
Normal file
374
studio-v2/lib/spatial-ui/PerformanceContext.tsx
Normal 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)
|
||||
}
|
||||
335
studio-v2/lib/spatial-ui/depth-system.ts
Normal file
335
studio-v2/lib/spatial-ui/depth-system.ts
Normal 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
|
||||
16
studio-v2/lib/spatial-ui/index.ts
Normal file
16
studio-v2/lib/spatial-ui/index.ts
Normal 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'
|
||||
Reference in New Issue
Block a user