@brutalcomponent/react
Version:
Brutalist React components
300 lines (296 loc) • 8.36 kB
JavaScript
;
var react = require('react');
/**
* @brutalcomponent/react
* (c) David Heffler (https://dvh.sh)
* Licensed under MIT
*/
function useClickOutside(handler, enabled = true) {
const ref = react.useRef(null);
react.useEffect(() => {
if (!enabled) return;
const handleClick = (event) => {
const el = ref.current;
if (!el || el.contains(event.target)) {
return;
}
handler();
};
document.addEventListener("mousedown", handleClick);
document.addEventListener("touchstart", handleClick);
return () => {
document.removeEventListener("mousedown", handleClick);
document.removeEventListener("touchstart", handleClick);
};
}, [handler, enabled]);
return ref;
}
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = react.useState(value);
react.useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
function useLocalStorage(key, initialValue) {
const readValue = react.useCallback(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
}, [key, initialValue]);
const [storedValue, setStoredValue] = react.useState(readValue);
const setValue = react.useCallback(
(value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
},
[key, storedValue]
);
const remove = react.useCallback(() => {
try {
if (typeof window !== "undefined") {
window.localStorage.removeItem(key);
}
setStoredValue(initialValue);
} catch (error) {
console.error(`Error removing localStorage key "${key}":`, error);
}
}, [key, initialValue]);
react.useEffect(() => {
const handleStorageChange = (e) => {
if (e.key === key && e.newValue !== null) {
try {
setStoredValue(JSON.parse(e.newValue));
} catch {
}
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, [key]);
return [storedValue, setValue, remove];
}
function useMediaQuery(query) {
const [matches, setMatches] = react.useState(false);
react.useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
const listener = (event) => {
setMatches(event.matches);
};
if (media.addEventListener) {
media.addEventListener("change", listener);
} else {
media.addListener(listener);
}
return () => {
if (media.removeEventListener) {
media.removeEventListener("change", listener);
} else {
media.removeListener(listener);
}
};
}, [query]);
return matches;
}
function useBreakpoint() {
const isMobile = useMediaQuery("(max-width: 640px)");
const isTablet = useMediaQuery("(min-width: 641px) and (max-width: 1024px)");
const isDesktop = useMediaQuery("(min-width: 1025px)");
const isMd = useMediaQuery("(min-width: 768px)");
const isLg = useMediaQuery("(min-width: 1024px)");
const isXl = useMediaQuery("(min-width: 1280px)");
return {
isMobile,
isTablet,
isDesktop,
isMd,
isLg,
isXl
};
}
function useKeyPress(keys, handler, options = {}) {
const {
element = null,
preventDefault = true,
stopPropagation = false,
enabled = true
} = options;
const savedHandler = react.useRef(handler);
react.useEffect(() => {
savedHandler.current = handler;
}, [handler]);
const eventHandler = react.useCallback(
(event) => {
const keysArray = Array.isArray(keys) ? keys : [keys];
if (keysArray.some((key) => {
if (key.includes("+")) {
const parts = key.split("+").map((p) => p.trim().toLowerCase());
const modifiers = parts.slice(0, -1);
const mainKey = parts[parts.length - 1];
const modifierChecks = {
ctrl: event.ctrlKey,
cmd: event.metaKey,
alt: event.altKey,
shift: event.shiftKey
};
const modifiersMatch = modifiers.every(
(mod) => modifierChecks[mod]
);
return modifiersMatch && event.key.toLowerCase() === mainKey;
}
return event.key === key;
})) {
if (preventDefault) event.preventDefault();
if (stopPropagation) event.stopPropagation();
savedHandler.current(event);
}
},
[keys, preventDefault, stopPropagation]
);
react.useEffect(() => {
if (!enabled) return;
const targetElement = element || document;
targetElement.addEventListener("keydown", eventHandler);
return () => {
targetElement.removeEventListener("keydown", eventHandler);
};
}, [element, enabled, eventHandler]);
}
function useFocusTrap(enabled = true) {
const ref = react.useRef(null);
react.useEffect(() => {
if (!enabled || !ref.current) return;
const element = ref.current;
const focusableElements = element.querySelectorAll(
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
const handleKeyDown = (e) => {
if (e.key !== "Tab") return;
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable?.focus();
}
} else {
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable?.focus();
}
}
};
element.addEventListener("keydown", handleKeyDown);
firstFocusable?.focus();
return () => {
element.removeEventListener("keydown", handleKeyDown);
};
}, [enabled]);
return ref;
}
/**
* @file src/hooks/useClickOutside.ts
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 11 2025
* @updated Fri Sep 12 2025
*
* @description
* Hook for detecting clicks outside an element
*/
/**
* @file src/hooks/useDebounce.ts
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 11 2025
* @updated Fri Sep 12 2025
*
* @description
* Hook for debouncing values
*/
/**
* @file src/hooks/useLocalStorage.ts
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 11 2025
* @updated Fri Sep 12 2025
*
* @description
* Hook for persisting state in localStorage
*/
/**
* @file src/hooks/useMediaQuery.ts
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 11 2025
* @updated Fri Sep 12 2025
*
* @description
* Hook for responsive media queries
*/
/**
* @file src/hooks/useKeyPress.ts
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 11 2025
* @updated Fri Sep 12 2025
*
* @description
* Hook for handling keyboard shortcuts
*/
/**
* @file src/hooks/useFocusTrap.ts
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 11 2025
* @updated Fri Sep 12 2025
*
* @description
* Hook for trapping focus within an element (useful for modals)
*/
/**
* @file src/hooks/index.ts
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 11 2025
* @updated Fri Sep 12 2025
*
* @description
* Hooks barrel export
*/
exports.useBreakpoint = useBreakpoint;
exports.useClickOutside = useClickOutside;
exports.useDebounce = useDebounce;
exports.useFocusTrap = useFocusTrap;
exports.useKeyPress = useKeyPress;
exports.useLocalStorage = useLocalStorage;
exports.useMediaQuery = useMediaQuery;
//# sourceMappingURL=chunk-Q6XW3RCO.js.map
//# sourceMappingURL=chunk-Q6XW3RCO.js.map