@sc4rfurryx/proteusjs
Version:
The Modern Web Development Framework for Accessible, Responsive, and High-Performance Applications. Intelligent container queries, fluid typography, WCAG compliance, and performance optimization.
231 lines (202 loc) • 6.47 kB
text/typescript
/**
* @sc4rfurryx/proteusjs/container
* Container/Style Query helpers with visualization devtools
*
* @version 2.0.0
* @author sc4rfurry
* @license MIT
*/
export interface ContainerOptions {
type?: 'size' | 'style';
inlineSize?: boolean;
}
/**
* Sugar on native container queries with dev visualization
*/
export function defineContainer(
target: Element | string,
name?: string,
opts: ContainerOptions = {}
): void {
const targetEl = typeof target === 'string' ? document.querySelector(target) : target;
if (!targetEl) {
throw new Error('Target element not found');
}
const {
type = 'size',
inlineSize: _inlineSize = true
} = opts;
const containerName = name || `container-${Math.random().toString(36).substring(2, 11)}`;
// Apply container properties
const element = targetEl as HTMLElement;
element.style.containerName = containerName;
element.style.containerType = type;
// Warn if containment settings are missing
const computedStyle = getComputedStyle(element);
if (!computedStyle.contain || computedStyle.contain === 'none') {
console.warn(`Container "${containerName}" may need explicit containment settings for optimal performance`);
}
// Dev overlay (only in development)
if (process.env['NODE_ENV'] === 'development' || (window as unknown as { __PROTEUS_DEV__?: boolean }).__PROTEUS_DEV__) {
createDevOverlay(element, containerName);
}
}
/**
* Create development overlay showing container bounds and breakpoints
*/
function createDevOverlay(element: HTMLElement, name: string): void {
const overlay = document.createElement('div');
overlay.className = 'proteus-container-overlay';
overlay.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
border: 2px dashed rgba(255, 0, 255, 0.5);
background: rgba(255, 0, 255, 0.05);
z-index: 9999;
font-family: monospace;
font-size: 12px;
color: #ff00ff;
`;
const label = document.createElement('div');
label.style.cssText = `
position: absolute;
top: -20px;
left: 0;
background: rgba(255, 0, 255, 0.9);
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
white-space: nowrap;
`;
label.textContent = `Container: ${name}`;
const sizeInfo = document.createElement('div');
sizeInfo.style.cssText = `
position: absolute;
bottom: 2px;
right: 2px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 2px 4px;
border-radius: 2px;
font-size: 10px;
`;
overlay.appendChild(label);
overlay.appendChild(sizeInfo);
// Position overlay relative to container
if (getComputedStyle(element).position === 'static') {
element.style.position = 'relative';
}
element.appendChild(overlay);
// Update size info
const updateSizeInfo = () => {
const rect = element.getBoundingClientRect();
sizeInfo.textContent = `${Math.round(rect.width)}×${Math.round(rect.height)}`;
};
updateSizeInfo();
// Update on resize
if ('ResizeObserver' in window) {
const resizeObserver = new ResizeObserver(updateSizeInfo);
resizeObserver.observe(element);
}
// Store cleanup function
(element as HTMLElement & { _proteusContainerCleanup?: () => void })._proteusContainerCleanup = () => {
overlay.remove();
};
}
/**
* Helper to create container query CSS rules
*/
export function createContainerQuery(
containerName: string,
condition: string,
styles: Record<string, string>
): string {
const cssRules = Object.entries(styles)
.map(([property, value]) => ` ${property}: ${value};`)
.join('\n');
return `@container ${containerName} (${condition}) {\n${cssRules}\n}`;
}
/**
* Apply container query styles dynamically
*/
export function applyContainerQuery(
containerName: string,
condition: string,
styles: Record<string, string>
): void {
const css = createContainerQuery(containerName, condition, styles);
const styleElement = document.createElement('style');
styleElement.textContent = css;
styleElement.setAttribute('data-proteus-container', containerName);
document.head.appendChild(styleElement);
}
/**
* Remove container query styles
*/
export function removeContainerQuery(containerName: string): void {
const styleElements = document.querySelectorAll(`style[data-proteus-container="${containerName}"]`);
styleElements.forEach(element => element.remove());
}
/**
* Get container size information
*/
export function getContainerSize(target: Element | string): { width: number; height: number } {
const targetEl = typeof target === 'string' ? document.querySelector(target) : target;
if (!targetEl) {
throw new Error('Target element not found');
}
const rect = targetEl.getBoundingClientRect();
return {
width: rect.width,
height: rect.height
};
}
/**
* Check if container queries are supported
*/
export function isSupported(): boolean {
return CSS.supports('container-type', 'size');
}
/**
* Cleanup container overlays and observers
*/
export function cleanup(target: Element | string): void {
const targetEl = typeof target === 'string' ? document.querySelector(target) : target;
if (!targetEl) return;
// Call stored cleanup function if it exists
const elementWithCleanup = targetEl as HTMLElement & { _proteusContainerCleanup?: () => void };
if (elementWithCleanup._proteusContainerCleanup) {
elementWithCleanup._proteusContainerCleanup();
delete elementWithCleanup._proteusContainerCleanup;
}
}
/**
* Toggle dev overlay visibility
*/
export function toggleDevOverlay(visible?: boolean): void {
const overlays = document.querySelectorAll('.proteus-container-overlay');
overlays.forEach(overlay => {
const element = overlay as HTMLElement;
if (visible !== undefined) {
element.style.display = visible ? 'block' : 'none';
} else {
element.style.display = element.style.display === 'none' ? 'block' : 'none';
}
});
}
// Export default object for convenience
export default {
defineContainer,
createContainerQuery,
applyContainerQuery,
removeContainerQuery,
getContainerSize,
isSupported,
cleanup,
toggleDevOverlay
};