mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
Directory Restructuring: - qml/qml-components/qml-components/* → qml/components/ (flattens nesting) - All 104 QML files moved with git history preserved - Eliminates redundant qml-components nesting Documentation Updates: - ARCHITECTURE.md: Updated qml/components references (2 locations) - GETTING_STARTED.md: Updated qml/components path (1 location, end of file) - README.md: Updated qml/components references (3 locations) - CODE_REVIEW.md: Updated qml/components file paths (4 locations) - docs/ARCHITECTURE.md: Complete refactor with qml/components paths Verification: - ✅ No remaining qml-components/ references in documentation - ✅ All 104 QML files present in flattened structure - ✅ Directory structure verified (12 component categories) - ✅ First-class directory naming convention Structure Post-Refactor: qml/ ├── components/ │ ├── atoms/ (16 files) │ ├── core/ (11 files) │ ├── data-display/ (10 files) │ ├── feedback/ (11 files) │ ├── form/ (19 files) │ ├── lab/ (11 files) │ ├── layout/ (12 files) │ ├── navigation/ (12 files) │ ├── surfaces/ (7 files) │ ├── theming/ (4 files) │ └── utils/ (13 files) ├── hybrid/ └── widgets/ Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
172 lines
4.6 KiB
TypeScript
172 lines
4.6 KiB
TypeScript
/**
|
|
* useMediaQuery Hook
|
|
* Tracks CSS media query state for responsive design and feature detection
|
|
*
|
|
* Features:
|
|
* - Real-time media query matching
|
|
* - Support for single or multiple queries
|
|
* - Automatic listener cleanup
|
|
* - SSR-safe with initial state
|
|
* - Useful for responsive layouts and feature detection
|
|
*
|
|
* @example
|
|
* // Single query
|
|
* const isMobile = useMediaQuery('(max-width: 767px)')
|
|
* const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)')
|
|
* const isLandscape = useMediaQuery('(orientation: landscape)')
|
|
*
|
|
* return (
|
|
* <div>
|
|
* {isMobile && <MobileMenu />}
|
|
* {!isMobile && <DesktopMenu />}
|
|
* </div>
|
|
* )
|
|
*
|
|
* @example
|
|
* // Multiple queries
|
|
* const { mobile, tablet, desktop } = useMediaQuery({
|
|
* mobile: '(max-width: 639px)',
|
|
* tablet: '(min-width: 640px) and (max-width: 1023px)',
|
|
* desktop: '(min-width: 1024px)',
|
|
* })
|
|
*
|
|
* if (mobile) return <MobileLayout />
|
|
* if (tablet) return <TabletLayout />
|
|
* return <DesktopLayout />
|
|
*
|
|
* @example
|
|
* // Accessibility features
|
|
* const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)')
|
|
* const prefersLightMode = useMediaQuery('(prefers-color-scheme: light)')
|
|
*
|
|
* if (prefersReducedMotion) {
|
|
* // Disable animations
|
|
* }
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
|
|
export type UseMediaQueryInput = string | Record<string, string>
|
|
|
|
export type UseMediaQueryReturn<T extends UseMediaQueryInput> = T extends string ? boolean : Record<string, boolean>
|
|
|
|
export function useMediaQuery<T extends UseMediaQueryInput>(query: T): UseMediaQueryReturn<T> {
|
|
// Handle single string query
|
|
if (typeof query === 'string') {
|
|
const [matches, setMatches] = useState<boolean>(() => {
|
|
// SSR-safe check
|
|
if (typeof window === 'undefined') {
|
|
return false
|
|
}
|
|
try {
|
|
return window.matchMedia(query).matches
|
|
} catch {
|
|
return false
|
|
}
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') return
|
|
|
|
try {
|
|
const mediaQueryList = window.matchMedia(query)
|
|
|
|
// Update initial state
|
|
setMatches(mediaQueryList.matches)
|
|
|
|
// Create listener - supports both new and old API
|
|
const handleChange = (e: MediaQueryListEvent | Event) => {
|
|
if (e instanceof MediaQueryListEvent) {
|
|
setMatches(e.matches)
|
|
} else if ('matches' in e.target!) {
|
|
setMatches((e.target as MediaQueryList).matches)
|
|
}
|
|
}
|
|
|
|
// Use addEventListener (modern API)
|
|
mediaQueryList.addEventListener('change', handleChange)
|
|
|
|
return () => {
|
|
mediaQueryList.removeEventListener('change', handleChange)
|
|
}
|
|
} catch {
|
|
// Handle invalid queries
|
|
return undefined
|
|
}
|
|
}, [query])
|
|
|
|
return matches as UseMediaQueryReturn<T>
|
|
}
|
|
|
|
// Handle multiple queries as object
|
|
const [queryMatches, setQueryMatches] = useState<Record<string, boolean>>(() => {
|
|
const initial: Record<string, boolean> = {}
|
|
if (typeof window === 'undefined') {
|
|
Object.keys(query).forEach((key) => {
|
|
initial[key] = false
|
|
})
|
|
return initial
|
|
}
|
|
|
|
try {
|
|
Object.entries(query).forEach(([key, mediaQuery]) => {
|
|
initial[key] = window.matchMedia(mediaQuery).matches
|
|
})
|
|
} catch {
|
|
Object.keys(query).forEach((key) => {
|
|
initial[key] = false
|
|
})
|
|
}
|
|
|
|
return initial
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') return
|
|
|
|
const mediaQueryLists: Array<{ key: string; mql: MediaQueryList }> = []
|
|
|
|
try {
|
|
Object.entries(query).forEach(([key, mediaQuery]) => {
|
|
const mql = window.matchMedia(mediaQuery)
|
|
mediaQueryLists.push({ key, mql })
|
|
|
|
// Update initial state
|
|
setQueryMatches((prev) => ({
|
|
...prev,
|
|
[key]: mql.matches,
|
|
}))
|
|
})
|
|
} catch {
|
|
return undefined
|
|
}
|
|
|
|
const handleChange = (key: string) => (e: MediaQueryListEvent | Event) => {
|
|
if (e instanceof MediaQueryListEvent) {
|
|
setQueryMatches((prev) => ({
|
|
...prev,
|
|
[key]: e.matches,
|
|
}))
|
|
} else if ('matches' in e.target!) {
|
|
setQueryMatches((prev) => ({
|
|
...prev,
|
|
[key]: (e.target as MediaQueryList).matches,
|
|
}))
|
|
}
|
|
}
|
|
|
|
mediaQueryLists.forEach(({ key, mql }) => {
|
|
const handler = handleChange(key)
|
|
mql.addEventListener('change', handler)
|
|
})
|
|
|
|
return () => {
|
|
mediaQueryLists.forEach(({ mql, key }) => {
|
|
mql.removeEventListener('change', handleChange(key))
|
|
})
|
|
}
|
|
}, [query])
|
|
|
|
return queryMatches as UseMediaQueryReturn<T>
|
|
}
|