@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.
269 lines (238 loc) • 6.56 kB
text/typescript
/**
* @sc4rfurryx/proteusjs/adapters/vue
* Vue composables and directives for ProteusJS
*
* @version 2.0.0
* @author sc4rfurry
* @license MIT
*/
import { ref, onMounted, onUnmounted, Ref } from 'vue';
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';
/**
* Composable for view transitions
*/
export function useTransition() {
return {
transition: async (run: () => Promise<any> | any, opts?: TransitionOptions) => {
return transition(run, opts);
}
};
}
/**
* Composable for scroll animations
*/
export function useScrollAnimate(
elementRef: Ref<HTMLElement | undefined>,
opts: ScrollAnimateOptions
) {
onMounted(() => {
if (elementRef.value) {
scrollAnimate(elementRef.value, opts);
}
});
return {
elementRef
};
}
/**
* Composable for popover functionality
*/
export function usePopover(
triggerRef: Ref<HTMLElement | undefined>,
panelRef: Ref<HTMLElement | undefined>,
opts?: PopoverOptions
) {
const controller = ref<PopoverController | null>(null);
const isOpen = ref(false);
onMounted(() => {
if (triggerRef.value && panelRef.value) {
controller.value = attachPopover(triggerRef.value, panelRef.value, {
...opts,
onOpen: () => {
isOpen.value = true;
opts?.onOpen?.();
},
onClose: () => {
isOpen.value = false;
opts?.onClose?.();
}
});
}
});
onUnmounted(() => {
if (controller.value) {
controller.value.destroy();
}
});
const open = () => controller.value?.open();
const close = () => controller.value?.close();
const toggle = () => controller.value?.toggle();
return {
isOpen,
open,
close,
toggle
};
}
/**
* Composable for anchor positioning
*/
export function useAnchor(
floatingRef: Ref<HTMLElement | undefined>,
anchorRef: Ref<HTMLElement | undefined>,
opts?: Omit<TetherOptions, 'anchor'>
) {
const controller = ref<TetherController | null>(null);
onMounted(() => {
if (floatingRef.value && anchorRef.value) {
controller.value = tether(floatingRef.value, {
anchor: anchorRef.value,
...opts
});
}
});
onUnmounted(() => {
if (controller.value) {
controller.value.destroy();
}
});
return {
controller
};
}
/**
* Composable for container queries
*/
export function useContainer(
elementRef: Ref<HTMLElement | undefined>,
name?: string,
opts?: ContainerOptions
) {
onMounted(() => {
if (elementRef.value) {
defineContainer(elementRef.value, name, opts);
}
});
return {
elementRef
};
}
/**
* Vue directive for scroll animations
*/
export const vProteusScroll = {
mounted(el: HTMLElement, binding: { value: ScrollAnimateOptions }) {
scrollAnimate(el, binding.value);
}
};
/**
* Vue directive for container queries
*/
export const vProteusContainer = {
mounted(el: HTMLElement, binding: { value?: { name?: string; options?: ContainerOptions } }) {
const { name, options } = binding.value || {};
defineContainer(el, name, options);
}
};
/**
* Vue directive for performance optimizations
*/
export const vProteusPerf = {
mounted(el: HTMLElement) {
// Apply content visibility optimization
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
el.style.contentVisibility = 'visible';
} else {
el.style.contentVisibility = 'auto';
}
});
},
{ rootMargin: '50px' }
);
observer.observe(el);
// Store cleanup function
(el as any)._proteusCleanup = () => {
observer.disconnect();
};
},
unmounted(el: HTMLElement) {
if ((el as any)._proteusCleanup) {
(el as any)._proteusCleanup();
}
}
};
/**
* Vue directive for accessibility enhancements
*/
export const vProteusA11y = {
mounted(el: HTMLElement, binding: { value?: { announceChanges?: boolean } }) {
const { announceChanges = false } = binding.value || {};
// Enhance focus indicators
const focusableElements = el.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
focusableElements.forEach(element => {
const htmlEl = element as HTMLElement;
htmlEl.addEventListener('focus', () => {
htmlEl.style.outline = '2px solid #0066cc';
htmlEl.style.outlineOffset = '2px';
});
htmlEl.addEventListener('blur', () => {
htmlEl.style.outline = 'none';
});
});
if (announceChanges) {
const observer = 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);
});
observer.observe(el, { childList: true, subtree: true });
(el as any)._proteusA11yCleanup = () => {
observer.disconnect();
};
}
},
unmounted(el: HTMLElement) {
if ((el as any)._proteusA11yCleanup) {
(el as any)._proteusA11yCleanup();
}
}
};
/**
* Plugin for Vue 3
*/
export const ProteusPlugin = {
install(app: any) {
app.directive('proteus-scroll', vProteusScroll);
app.directive('proteus-container', vProteusContainer);
app.directive('proteus-perf', vProteusPerf);
app.directive('proteus-a11y', vProteusA11y);
}
};
// Export all composables and directives
export default {
useTransition,
useScrollAnimate,
usePopover,
useAnchor,
useContainer,
vProteusScroll,
vProteusContainer,
vProteusPerf,
vProteusA11y,
ProteusPlugin
};