@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.
322 lines (277 loc) • 7.95 kB
text/typescript
/**
* @sc4rfurryx/proteusjs/adapters/svelte
* Svelte actions and stores for ProteusJS
*
* @version 2.0.0
* @author sc4rfurry
* @license MIT
*/
import { writable } from 'svelte/store';
import { transition, TransitionOptions } from '../modules/transitions';
import { scrollAnimate, ScrollAnimateOptions } from '../modules/scroll';
import { attach as attachPopover, PopoverOptions, PopoverController } from '../modules/popover';
import { tether, TetherOptions, TetherController } from '../modules/anchor';
import { defineContainer, ContainerOptions } from '../modules/container';
/**
* Svelte action for scroll animations
*/
export function proteusScroll(node: HTMLElement, options: ScrollAnimateOptions) {
scrollAnimate(node, options);
return {
update(newOptions: ScrollAnimateOptions) {
// Re-apply with new options
scrollAnimate(node, newOptions);
},
destroy() {
// Cleanup handled by scroll module
}
};
}
/**
* Svelte action for container queries
*/
export function proteusContainer(
node: HTMLElement,
options: { name?: string; containerOptions?: ContainerOptions } = {}
) {
const { name, containerOptions } = options;
defineContainer(node, name, containerOptions);
return {
update(newOptions: { name?: string; containerOptions?: ContainerOptions }) {
const { name: newName, containerOptions: newContainerOptions } = newOptions;
defineContainer(node, newName, newContainerOptions);
}
};
}
/**
* Svelte action for popover functionality
*/
export function proteusPopover(
node: HTMLElement,
options: { panel: HTMLElement | string; popoverOptions?: PopoverOptions }
) {
let controller: PopoverController | null = null;
const { panel, popoverOptions } = options;
controller = attachPopover(node, panel, popoverOptions);
return {
update(newOptions: { panel: HTMLElement | string; popoverOptions?: PopoverOptions }) {
if (controller) {
controller.destroy();
}
controller = attachPopover(node, newOptions.panel, newOptions.popoverOptions);
},
destroy() {
if (controller) {
controller.destroy();
}
}
};
}
/**
* Svelte action for anchor positioning
*/
export function proteusAnchor(
node: HTMLElement,
options: TetherOptions
) {
let controller: TetherController | null = null;
controller = tether(node, options);
return {
update(newOptions: TetherOptions) {
if (controller) {
controller.destroy();
}
controller = tether(node, newOptions);
},
destroy() {
if (controller) {
controller.destroy();
}
}
};
}
/**
* Svelte action for performance optimizations
*/
export function proteusPerf(node: HTMLElement) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
node.style.contentVisibility = 'visible';
} else {
node.style.contentVisibility = 'auto';
}
});
},
{ rootMargin: '50px' }
);
observer.observe(node);
return {
destroy() {
observer.disconnect();
}
};
}
/**
* Svelte action for accessibility enhancements
*/
export function proteusA11y(
node: HTMLElement,
options: { announceChanges?: boolean } = {}
) {
const { announceChanges = false } = options;
// Enhance focus indicators
const focusableElements = node.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const focusHandlers = new Map();
focusableElements.forEach(element => {
const htmlEl = element as HTMLElement;
const focusHandler = () => {
htmlEl.style.outline = '2px solid #0066cc';
htmlEl.style.outlineOffset = '2px';
};
const blurHandler = () => {
htmlEl.style.outline = 'none';
};
htmlEl.addEventListener('focus', focusHandler);
htmlEl.addEventListener('blur', blurHandler);
focusHandlers.set(htmlEl, { focusHandler, blurHandler });
});
let mutationObserver: MutationObserver | null = null;
if (announceChanges) {
mutationObserver = new MutationObserver(() => {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'polite');
announcement.style.position = 'absolute';
announcement.style.left = '-10000px';
announcement.textContent = 'Content updated';
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
});
mutationObserver.observe(node, { childList: true, subtree: true });
}
return {
destroy() {
// Clean up focus handlers
focusHandlers.forEach(({ focusHandler, blurHandler }, element) => {
element.removeEventListener('focus', focusHandler);
element.removeEventListener('blur', blurHandler);
});
if (mutationObserver) {
mutationObserver.disconnect();
}
}
};
}
/**
* Store for managing popover state
*/
export function createPopover(
triggerSelector: string,
panelSelector: string,
options?: PopoverOptions
) {
const isOpen = writable(false);
let controller: PopoverController | null = null;
const initialize = () => {
const trigger = document.querySelector(triggerSelector);
const panel = document.querySelector(panelSelector);
if (trigger && panel) {
controller = attachPopover(trigger, panel, {
...options,
onOpen: () => {
isOpen.set(true);
options?.onOpen?.();
},
onClose: () => {
isOpen.set(false);
options?.onClose?.();
}
});
}
};
const open = () => controller?.open();
const close = () => controller?.close();
const toggle = () => controller?.toggle();
const destroy = () => {
if (controller) {
controller.destroy();
controller = null;
}
};
return {
isOpen,
initialize,
open,
close,
toggle,
destroy
};
}
/**
* Store for managing transition state
*/
export function createTransition() {
const isTransitioning = writable(false);
const runTransition = async (
run: () => Promise<any> | any,
opts?: TransitionOptions
) => {
isTransitioning.set(true);
try {
await transition(run, opts);
} finally {
isTransitioning.set(false);
}
};
return {
isTransitioning,
runTransition
};
}
/**
* Utility function to create reactive container size store
*/
export function createContainerSize(element: HTMLElement) {
const size = writable({ width: 0, height: 0 });
if ('ResizeObserver' in window) {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
size.set({ width, height });
}
});
resizeObserver.observe(element);
return {
size,
destroy: () => resizeObserver.disconnect()
};
}
// Fallback for browsers without ResizeObserver
const updateSize = () => {
const rect = element.getBoundingClientRect();
size.set({ width: rect.width, height: rect.height });
};
updateSize();
(window as any).addEventListener('resize', updateSize);
return {
size,
destroy: () => (window as any).removeEventListener('resize', updateSize)
};
}
// Export all actions and utilities
export default {
proteusScroll,
proteusContainer,
proteusPopover,
proteusAnchor,
proteusPerf,
proteusA11y,
createPopover,
createTransition,
createContainerSize
};