Files
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

189 lines
5.2 KiB
TypeScript

'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>
)
}