@sc4rfurryx/proteusjs 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +331 -77
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/react.d.ts +140 -0
- package/dist/adapters/react.esm.js +849 -0
- package/dist/adapters/react.esm.js.map +1 -0
- package/dist/adapters/svelte.d.ts +181 -0
- package/dist/adapters/svelte.esm.js +909 -0
- package/dist/adapters/svelte.esm.js.map +1 -0
- package/dist/adapters/vue.d.ts +205 -0
- package/dist/adapters/vue.esm.js +873 -0
- package/dist/adapters/vue.esm.js.map +1 -0
- package/dist/modules/a11y-audit.d.ts +31 -0
- package/dist/modules/a11y-audit.esm.js +64 -0
- package/dist/modules/a11y-audit.esm.js.map +1 -0
- package/dist/modules/a11y-primitives.d.ts +36 -0
- package/dist/modules/a11y-primitives.esm.js +114 -0
- package/dist/modules/a11y-primitives.esm.js.map +1 -0
- package/dist/modules/anchor.d.ts +30 -0
- package/dist/modules/anchor.esm.js +219 -0
- package/dist/modules/anchor.esm.js.map +1 -0
- package/dist/modules/container.d.ts +60 -0
- package/dist/modules/container.esm.js +194 -0
- package/dist/modules/container.esm.js.map +1 -0
- package/dist/modules/perf.d.ts +82 -0
- package/dist/modules/perf.esm.js +257 -0
- package/dist/modules/perf.esm.js.map +1 -0
- package/dist/modules/popover.d.ts +33 -0
- package/dist/modules/popover.esm.js +191 -0
- package/dist/modules/popover.esm.js.map +1 -0
- package/dist/modules/scroll.d.ts +43 -0
- package/dist/modules/scroll.esm.js +195 -0
- package/dist/modules/scroll.esm.js.map +1 -0
- package/dist/modules/transitions.d.ts +35 -0
- package/dist/modules/transitions.esm.js +120 -0
- package/dist/modules/transitions.esm.js.map +1 -0
- package/dist/modules/typography.d.ts +72 -0
- package/dist/modules/typography.esm.js +168 -0
- package/dist/modules/typography.esm.js.map +1 -0
- package/dist/proteus.cjs.js +1554 -12
- package/dist/proteus.cjs.js.map +1 -1
- package/dist/proteus.d.ts +516 -12
- package/dist/proteus.esm.js +1545 -12
- package/dist/proteus.esm.js.map +1 -1
- package/dist/proteus.esm.min.js +3 -3
- package/dist/proteus.esm.min.js.map +1 -1
- package/dist/proteus.js +1554 -12
- package/dist/proteus.js.map +1 -1
- package/dist/proteus.min.js +3 -3
- package/dist/proteus.min.js.map +1 -1
- package/package.json +69 -7
- package/src/adapters/react.ts +264 -0
- package/src/adapters/svelte.ts +321 -0
- package/src/adapters/vue.ts +268 -0
- package/src/index.ts +33 -6
- package/src/modules/a11y-audit/index.ts +84 -0
- package/src/modules/a11y-primitives/index.ts +152 -0
- package/src/modules/anchor/index.ts +259 -0
- package/src/modules/container/index.ts +230 -0
- package/src/modules/perf/index.ts +291 -0
- package/src/modules/popover/index.ts +238 -0
- package/src/modules/scroll/index.ts +251 -0
- package/src/modules/transitions/index.ts +145 -0
- package/src/modules/typography/index.ts +239 -0
- package/src/utils/version.ts +1 -1
@@ -0,0 +1,909 @@
|
|
1
|
+
/*!
|
2
|
+
* ProteusJS v1.1.1
|
3
|
+
* Shape-shifting responsive design that adapts like the sea god himself
|
4
|
+
* (c) 2025 sc4rfurry
|
5
|
+
* Released under the MIT License
|
6
|
+
*/
|
7
|
+
import { writable } from 'svelte/store';
|
8
|
+
|
9
|
+
/**
|
10
|
+
* @sc4rfurryx/proteusjs/transitions
|
11
|
+
* View Transitions API wrapper with safe fallbacks
|
12
|
+
*
|
13
|
+
* @version 1.1.0
|
14
|
+
* @author sc4rfurry
|
15
|
+
* @license MIT
|
16
|
+
*/
|
17
|
+
/**
|
18
|
+
* One API for animating DOM state changes and cross-document navigations
|
19
|
+
* using the View Transitions API with safe fallbacks.
|
20
|
+
*/
|
21
|
+
async function transition(run, opts = {}) {
|
22
|
+
const { name, duration = 300, onBefore, onAfter, allowInterrupt = true } = opts;
|
23
|
+
// Check for View Transitions API support
|
24
|
+
const hasViewTransitions = 'startViewTransition' in document;
|
25
|
+
if (onBefore) {
|
26
|
+
onBefore();
|
27
|
+
}
|
28
|
+
if (!hasViewTransitions) {
|
29
|
+
// Fallback: run immediately without transitions
|
30
|
+
try {
|
31
|
+
await run();
|
32
|
+
}
|
33
|
+
finally {
|
34
|
+
if (onAfter) {
|
35
|
+
onAfter();
|
36
|
+
}
|
37
|
+
}
|
38
|
+
return;
|
39
|
+
}
|
40
|
+
// Use native View Transitions API
|
41
|
+
try {
|
42
|
+
const viewTransition = document.startViewTransition(async () => {
|
43
|
+
await run();
|
44
|
+
});
|
45
|
+
// Add CSS view-transition-name if name provided
|
46
|
+
if (name) {
|
47
|
+
const style = document.createElement('style');
|
48
|
+
style.textContent = `
|
49
|
+
::view-transition-old(${name}),
|
50
|
+
::view-transition-new(${name}) {
|
51
|
+
animation-duration: ${duration}ms;
|
52
|
+
}
|
53
|
+
`;
|
54
|
+
document.head.appendChild(style);
|
55
|
+
// Clean up style after transition
|
56
|
+
viewTransition.finished.finally(() => {
|
57
|
+
style.remove();
|
58
|
+
});
|
59
|
+
}
|
60
|
+
await viewTransition.finished;
|
61
|
+
}
|
62
|
+
catch (error) {
|
63
|
+
console.warn('View transition failed, falling back to immediate execution:', error);
|
64
|
+
await run();
|
65
|
+
}
|
66
|
+
finally {
|
67
|
+
if (onAfter) {
|
68
|
+
onAfter();
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
/**
|
74
|
+
* @sc4rfurryx/proteusjs/scroll
|
75
|
+
* Scroll-driven animations with CSS Scroll-Linked Animations
|
76
|
+
*
|
77
|
+
* @version 1.1.0
|
78
|
+
* @author sc4rfurry
|
79
|
+
* @license MIT
|
80
|
+
*/
|
81
|
+
/**
|
82
|
+
* Zero-boilerplate setup for CSS Scroll-Linked Animations with fallbacks
|
83
|
+
*/
|
84
|
+
function scrollAnimate(target, opts) {
|
85
|
+
const targetEl = typeof target === 'string' ? document.querySelector(target) : target;
|
86
|
+
if (!targetEl) {
|
87
|
+
throw new Error('Target element not found');
|
88
|
+
}
|
89
|
+
const { keyframes, range = ['0%', '100%'], timeline = {}, fallback = 'io' } = opts;
|
90
|
+
const { axis = 'block', start = '0%', end = '100%' } = timeline;
|
91
|
+
// Check for CSS Scroll-Linked Animations support
|
92
|
+
const hasScrollTimeline = 'CSS' in window && CSS.supports('animation-timeline', 'scroll()');
|
93
|
+
// Check for reduced motion preference
|
94
|
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
95
|
+
if (prefersReducedMotion) {
|
96
|
+
// Respect user preference - either disable or reduce animation
|
97
|
+
if (fallback === false)
|
98
|
+
return;
|
99
|
+
// Apply only the end state for reduced motion
|
100
|
+
const endKeyframe = keyframes[keyframes.length - 1];
|
101
|
+
Object.assign(targetEl.style, endKeyframe);
|
102
|
+
return;
|
103
|
+
}
|
104
|
+
if (hasScrollTimeline) {
|
105
|
+
// Use native CSS Scroll-Linked Animations
|
106
|
+
const timelineName = `scroll-timeline-${Math.random().toString(36).substr(2, 9)}`;
|
107
|
+
// Create scroll timeline
|
108
|
+
const style = document.createElement('style');
|
109
|
+
style.textContent = `
|
110
|
+
@scroll-timeline ${timelineName} {
|
111
|
+
source: nearest;
|
112
|
+
orientation: ${axis};
|
113
|
+
scroll-offsets: ${start}, ${end};
|
114
|
+
}
|
115
|
+
|
116
|
+
.scroll-animate-${timelineName} {
|
117
|
+
animation-timeline: ${timelineName};
|
118
|
+
animation-duration: 1ms; /* Required but ignored */
|
119
|
+
animation-fill-mode: both;
|
120
|
+
}
|
121
|
+
`;
|
122
|
+
document.head.appendChild(style);
|
123
|
+
// Apply animation class
|
124
|
+
targetEl.classList.add(`scroll-animate-${timelineName}`);
|
125
|
+
// Create Web Animations API animation
|
126
|
+
const animation = targetEl.animate(keyframes, {
|
127
|
+
duration: 1, // Required but ignored with scroll timeline
|
128
|
+
fill: 'both'
|
129
|
+
});
|
130
|
+
// Set scroll timeline (when supported)
|
131
|
+
if ('timeline' in animation) {
|
132
|
+
animation.timeline = new window.ScrollTimeline({
|
133
|
+
source: document.scrollingElement,
|
134
|
+
orientation: axis,
|
135
|
+
scrollOffsets: [
|
136
|
+
{ target: targetEl, edge: 'start', threshold: parseFloat(start) / 100 },
|
137
|
+
{ target: targetEl, edge: 'end', threshold: parseFloat(end) / 100 }
|
138
|
+
]
|
139
|
+
});
|
140
|
+
}
|
141
|
+
}
|
142
|
+
else if (fallback === 'io') {
|
143
|
+
// Fallback using Intersection Observer
|
144
|
+
let animation = null;
|
145
|
+
const observer = new IntersectionObserver((entries) => {
|
146
|
+
entries.forEach(entry => {
|
147
|
+
const progress = Math.max(0, Math.min(1, entry.intersectionRatio));
|
148
|
+
if (!animation) {
|
149
|
+
animation = targetEl.animate(keyframes, {
|
150
|
+
duration: 1000,
|
151
|
+
fill: 'both'
|
152
|
+
});
|
153
|
+
animation.pause();
|
154
|
+
}
|
155
|
+
// Update animation progress based on intersection
|
156
|
+
animation.currentTime = progress * 1000;
|
157
|
+
});
|
158
|
+
}, {
|
159
|
+
threshold: Array.from({ length: 101 }, (_, i) => i / 100) // 0 to 1 in 0.01 steps
|
160
|
+
});
|
161
|
+
observer.observe(targetEl);
|
162
|
+
// Store cleanup function
|
163
|
+
targetEl._scrollAnimateCleanup = () => {
|
164
|
+
observer.disconnect();
|
165
|
+
if (animation) {
|
166
|
+
animation.cancel();
|
167
|
+
}
|
168
|
+
};
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
172
|
+
/**
|
173
|
+
* @sc4rfurryx/proteusjs/popover
|
174
|
+
* HTML Popover API wrapper with robust focus/inert handling
|
175
|
+
*
|
176
|
+
* @version 1.1.0
|
177
|
+
* @author sc4rfurry
|
178
|
+
* @license MIT
|
179
|
+
*/
|
180
|
+
/**
|
181
|
+
* Unified API for menus, tooltips, and dialogs using the native Popover API
|
182
|
+
* with robust focus/inert handling
|
183
|
+
*/
|
184
|
+
function attach(trigger, panel, opts = {}) {
|
185
|
+
const triggerEl = typeof trigger === 'string' ? document.querySelector(trigger) : trigger;
|
186
|
+
const panelEl = typeof panel === 'string' ? document.querySelector(panel) : panel;
|
187
|
+
if (!triggerEl || !panelEl) {
|
188
|
+
throw new Error('Both trigger and panel elements must exist');
|
189
|
+
}
|
190
|
+
const { type = 'menu', trapFocus = type === 'dialog', restoreFocus = true, closeOnEscape = true, onOpen, onClose } = opts;
|
191
|
+
let isOpen = false;
|
192
|
+
let previousFocus = null;
|
193
|
+
let focusTrap = null;
|
194
|
+
// Check for native Popover API support
|
195
|
+
const hasPopoverAPI = 'popover' in HTMLElement.prototype;
|
196
|
+
// Set up ARIA attributes
|
197
|
+
const setupAria = () => {
|
198
|
+
const panelId = panelEl.id || `popover-${Math.random().toString(36).substr(2, 9)}`;
|
199
|
+
panelEl.id = panelId;
|
200
|
+
triggerEl.setAttribute('aria-expanded', 'false');
|
201
|
+
triggerEl.setAttribute('aria-controls', panelId);
|
202
|
+
if (type === 'menu') {
|
203
|
+
triggerEl.setAttribute('aria-haspopup', 'menu');
|
204
|
+
panelEl.setAttribute('role', 'menu');
|
205
|
+
}
|
206
|
+
else if (type === 'dialog') {
|
207
|
+
triggerEl.setAttribute('aria-haspopup', 'dialog');
|
208
|
+
panelEl.setAttribute('role', 'dialog');
|
209
|
+
panelEl.setAttribute('aria-modal', 'true');
|
210
|
+
}
|
211
|
+
else if (type === 'tooltip') {
|
212
|
+
triggerEl.setAttribute('aria-describedby', panelId);
|
213
|
+
panelEl.setAttribute('role', 'tooltip');
|
214
|
+
}
|
215
|
+
};
|
216
|
+
// Set up native popover if supported
|
217
|
+
const setupNativePopover = () => {
|
218
|
+
if (hasPopoverAPI) {
|
219
|
+
panelEl.popover = type === 'dialog' ? 'manual' : 'auto';
|
220
|
+
triggerEl.setAttribute('popovertarget', panelEl.id);
|
221
|
+
}
|
222
|
+
};
|
223
|
+
// Focus trap implementation
|
224
|
+
class FocusTrap {
|
225
|
+
constructor(container) {
|
226
|
+
this.container = container;
|
227
|
+
this.focusableElements = [];
|
228
|
+
this.handleKeyDown = (e) => {
|
229
|
+
if (e.key !== 'Tab')
|
230
|
+
return;
|
231
|
+
const firstElement = this.focusableElements[0];
|
232
|
+
const lastElement = this.focusableElements[this.focusableElements.length - 1];
|
233
|
+
if (e.shiftKey) {
|
234
|
+
if (document.activeElement === firstElement) {
|
235
|
+
e.preventDefault();
|
236
|
+
lastElement.focus();
|
237
|
+
}
|
238
|
+
}
|
239
|
+
else {
|
240
|
+
if (document.activeElement === lastElement) {
|
241
|
+
e.preventDefault();
|
242
|
+
firstElement.focus();
|
243
|
+
}
|
244
|
+
}
|
245
|
+
};
|
246
|
+
this.updateFocusableElements();
|
247
|
+
}
|
248
|
+
updateFocusableElements() {
|
249
|
+
const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
250
|
+
this.focusableElements = Array.from(this.container.querySelectorAll(selector));
|
251
|
+
}
|
252
|
+
activate() {
|
253
|
+
this.updateFocusableElements();
|
254
|
+
if (this.focusableElements.length > 0) {
|
255
|
+
this.focusableElements[0].focus();
|
256
|
+
}
|
257
|
+
document.addEventListener('keydown', this.handleKeyDown);
|
258
|
+
}
|
259
|
+
deactivate() {
|
260
|
+
document.removeEventListener('keydown', this.handleKeyDown);
|
261
|
+
}
|
262
|
+
}
|
263
|
+
const open = () => {
|
264
|
+
if (isOpen)
|
265
|
+
return;
|
266
|
+
if (restoreFocus) {
|
267
|
+
previousFocus = document.activeElement;
|
268
|
+
}
|
269
|
+
if (hasPopoverAPI) {
|
270
|
+
panelEl.showPopover();
|
271
|
+
}
|
272
|
+
else {
|
273
|
+
panelEl.style.display = 'block';
|
274
|
+
panelEl.setAttribute('data-popover-open', 'true');
|
275
|
+
}
|
276
|
+
triggerEl.setAttribute('aria-expanded', 'true');
|
277
|
+
isOpen = true;
|
278
|
+
if (trapFocus) {
|
279
|
+
focusTrap = new FocusTrap(panelEl);
|
280
|
+
focusTrap.activate();
|
281
|
+
}
|
282
|
+
if (onOpen) {
|
283
|
+
onOpen();
|
284
|
+
}
|
285
|
+
};
|
286
|
+
const close = () => {
|
287
|
+
if (!isOpen)
|
288
|
+
return;
|
289
|
+
if (hasPopoverAPI) {
|
290
|
+
panelEl.hidePopover();
|
291
|
+
}
|
292
|
+
else {
|
293
|
+
panelEl.style.display = 'none';
|
294
|
+
panelEl.removeAttribute('data-popover-open');
|
295
|
+
}
|
296
|
+
triggerEl.setAttribute('aria-expanded', 'false');
|
297
|
+
isOpen = false;
|
298
|
+
if (focusTrap) {
|
299
|
+
focusTrap.deactivate();
|
300
|
+
focusTrap = null;
|
301
|
+
}
|
302
|
+
if (restoreFocus && previousFocus) {
|
303
|
+
previousFocus.focus();
|
304
|
+
previousFocus = null;
|
305
|
+
}
|
306
|
+
if (onClose) {
|
307
|
+
onClose();
|
308
|
+
}
|
309
|
+
};
|
310
|
+
const toggle = () => {
|
311
|
+
if (isOpen) {
|
312
|
+
close();
|
313
|
+
}
|
314
|
+
else {
|
315
|
+
open();
|
316
|
+
}
|
317
|
+
};
|
318
|
+
const handleKeyDown = (e) => {
|
319
|
+
if (closeOnEscape && e.key === 'Escape' && isOpen) {
|
320
|
+
e.preventDefault();
|
321
|
+
close();
|
322
|
+
}
|
323
|
+
};
|
324
|
+
const handleClick = (e) => {
|
325
|
+
e.preventDefault();
|
326
|
+
toggle();
|
327
|
+
};
|
328
|
+
const destroy = () => {
|
329
|
+
triggerEl.removeEventListener('click', handleClick);
|
330
|
+
document.removeEventListener('keydown', handleKeyDown);
|
331
|
+
if (focusTrap) {
|
332
|
+
focusTrap.deactivate();
|
333
|
+
}
|
334
|
+
if (isOpen) {
|
335
|
+
close();
|
336
|
+
}
|
337
|
+
};
|
338
|
+
// Initialize
|
339
|
+
setupAria();
|
340
|
+
setupNativePopover();
|
341
|
+
triggerEl.addEventListener('click', handleClick);
|
342
|
+
document.addEventListener('keydown', handleKeyDown);
|
343
|
+
return {
|
344
|
+
open,
|
345
|
+
close,
|
346
|
+
toggle,
|
347
|
+
destroy
|
348
|
+
};
|
349
|
+
}
|
350
|
+
|
351
|
+
/**
|
352
|
+
* @sc4rfurryx/proteusjs/anchor
|
353
|
+
* CSS Anchor Positioning utilities with robust JS fallback
|
354
|
+
*
|
355
|
+
* @version 1.1.0
|
356
|
+
* @author sc4rfurry
|
357
|
+
* @license MIT
|
358
|
+
*/
|
359
|
+
/**
|
360
|
+
* Declarative tethers (tooltips, callouts) via CSS Anchor Positioning when available;
|
361
|
+
* robust JS fallback with flip/collision detection
|
362
|
+
*/
|
363
|
+
function tether(floating, opts) {
|
364
|
+
const floatingEl = typeof floating === 'string' ? document.querySelector(floating) : floating;
|
365
|
+
const anchorEl = typeof opts.anchor === 'string' ? document.querySelector(opts.anchor) : opts.anchor;
|
366
|
+
if (!floatingEl || !anchorEl) {
|
367
|
+
throw new Error('Both floating and anchor elements must exist');
|
368
|
+
}
|
369
|
+
const { placement = 'bottom', align = 'center', offset = 8, strategy = 'absolute' } = opts;
|
370
|
+
// Check for CSS Anchor Positioning support
|
371
|
+
const hasAnchorPositioning = CSS.supports('anchor-name', 'test');
|
372
|
+
let isDestroyed = false;
|
373
|
+
let resizeObserver = null;
|
374
|
+
const setupCSSAnchorPositioning = () => {
|
375
|
+
if (!hasAnchorPositioning)
|
376
|
+
return false;
|
377
|
+
// Generate unique anchor name
|
378
|
+
const anchorName = `anchor-${Math.random().toString(36).substring(2, 11)}`;
|
379
|
+
// Set anchor name on anchor element
|
380
|
+
anchorEl.style.setProperty('anchor-name', anchorName);
|
381
|
+
// Position floating element using CSS anchor positioning
|
382
|
+
const floatingStyle = floatingEl;
|
383
|
+
floatingStyle.style.position = strategy;
|
384
|
+
floatingStyle.style.setProperty('position-anchor', anchorName);
|
385
|
+
// Set position based on placement
|
386
|
+
switch (placement) {
|
387
|
+
case 'top':
|
388
|
+
floatingStyle.style.bottom = `anchor(bottom, ${offset}px)`;
|
389
|
+
break;
|
390
|
+
case 'bottom':
|
391
|
+
floatingStyle.style.top = `anchor(bottom, ${offset}px)`;
|
392
|
+
break;
|
393
|
+
case 'left':
|
394
|
+
floatingStyle.style.right = `anchor(left, ${offset}px)`;
|
395
|
+
break;
|
396
|
+
case 'right':
|
397
|
+
floatingStyle.style.left = `anchor(right, ${offset}px)`;
|
398
|
+
break;
|
399
|
+
}
|
400
|
+
// Set alignment
|
401
|
+
if (placement === 'top' || placement === 'bottom') {
|
402
|
+
switch (align) {
|
403
|
+
case 'start':
|
404
|
+
floatingStyle.style.left = 'anchor(left)';
|
405
|
+
break;
|
406
|
+
case 'center':
|
407
|
+
floatingStyle.style.left = 'anchor(center)';
|
408
|
+
floatingStyle.style.transform = 'translateX(-50%)';
|
409
|
+
break;
|
410
|
+
case 'end':
|
411
|
+
floatingStyle.style.right = 'anchor(right)';
|
412
|
+
break;
|
413
|
+
}
|
414
|
+
}
|
415
|
+
else {
|
416
|
+
switch (align) {
|
417
|
+
case 'start':
|
418
|
+
floatingStyle.style.top = 'anchor(top)';
|
419
|
+
break;
|
420
|
+
case 'center':
|
421
|
+
floatingStyle.style.top = 'anchor(center)';
|
422
|
+
floatingStyle.style.transform = 'translateY(-50%)';
|
423
|
+
break;
|
424
|
+
case 'end':
|
425
|
+
floatingStyle.style.bottom = 'anchor(bottom)';
|
426
|
+
break;
|
427
|
+
}
|
428
|
+
}
|
429
|
+
return true;
|
430
|
+
};
|
431
|
+
const calculatePosition = () => {
|
432
|
+
const anchorRect = anchorEl.getBoundingClientRect();
|
433
|
+
const floatingRect = floatingEl.getBoundingClientRect();
|
434
|
+
const viewport = {
|
435
|
+
width: window.innerWidth,
|
436
|
+
height: window.innerHeight
|
437
|
+
};
|
438
|
+
let finalPlacement = placement;
|
439
|
+
let x = 0;
|
440
|
+
let y = 0;
|
441
|
+
// Calculate base position
|
442
|
+
switch (finalPlacement) {
|
443
|
+
case 'top':
|
444
|
+
x = anchorRect.left;
|
445
|
+
y = anchorRect.top - floatingRect.height - offset;
|
446
|
+
break;
|
447
|
+
case 'bottom':
|
448
|
+
x = anchorRect.left;
|
449
|
+
y = anchorRect.bottom + offset;
|
450
|
+
break;
|
451
|
+
case 'left':
|
452
|
+
x = anchorRect.left - floatingRect.width - offset;
|
453
|
+
y = anchorRect.top;
|
454
|
+
break;
|
455
|
+
case 'right':
|
456
|
+
x = anchorRect.right + offset;
|
457
|
+
y = anchorRect.top;
|
458
|
+
break;
|
459
|
+
case 'auto': {
|
460
|
+
// Choose best placement based on available space
|
461
|
+
const spaces = {
|
462
|
+
top: anchorRect.top,
|
463
|
+
bottom: viewport.height - anchorRect.bottom,
|
464
|
+
left: anchorRect.left,
|
465
|
+
right: viewport.width - anchorRect.right
|
466
|
+
};
|
467
|
+
const bestPlacement = Object.entries(spaces).reduce((a, b) => spaces[a[0]] > spaces[b[0]] ? a : b)[0];
|
468
|
+
finalPlacement = bestPlacement;
|
469
|
+
return calculatePosition(); // Recursive call with determined placement
|
470
|
+
}
|
471
|
+
}
|
472
|
+
// Apply alignment
|
473
|
+
if (finalPlacement === 'top' || finalPlacement === 'bottom') {
|
474
|
+
switch (align) {
|
475
|
+
case 'start':
|
476
|
+
// x already set correctly
|
477
|
+
break;
|
478
|
+
case 'center':
|
479
|
+
x = anchorRect.left + (anchorRect.width - floatingRect.width) / 2;
|
480
|
+
break;
|
481
|
+
case 'end':
|
482
|
+
x = anchorRect.right - floatingRect.width;
|
483
|
+
break;
|
484
|
+
}
|
485
|
+
}
|
486
|
+
else {
|
487
|
+
switch (align) {
|
488
|
+
case 'start':
|
489
|
+
// y already set correctly
|
490
|
+
break;
|
491
|
+
case 'center':
|
492
|
+
y = anchorRect.top + (anchorRect.height - floatingRect.height) / 2;
|
493
|
+
break;
|
494
|
+
case 'end':
|
495
|
+
y = anchorRect.bottom - floatingRect.height;
|
496
|
+
break;
|
497
|
+
}
|
498
|
+
}
|
499
|
+
// Collision detection and adjustment
|
500
|
+
if (x < 0)
|
501
|
+
x = 8;
|
502
|
+
if (y < 0)
|
503
|
+
y = 8;
|
504
|
+
if (x + floatingRect.width > viewport.width) {
|
505
|
+
x = viewport.width - floatingRect.width - 8;
|
506
|
+
}
|
507
|
+
if (y + floatingRect.height > viewport.height) {
|
508
|
+
y = viewport.height - floatingRect.height - 8;
|
509
|
+
}
|
510
|
+
return { x, y };
|
511
|
+
};
|
512
|
+
const updatePosition = () => {
|
513
|
+
if (isDestroyed)
|
514
|
+
return;
|
515
|
+
if (!hasAnchorPositioning) {
|
516
|
+
const { x, y } = calculatePosition();
|
517
|
+
const floatingStyle = floatingEl;
|
518
|
+
floatingStyle.style.position = strategy;
|
519
|
+
floatingStyle.style.left = `${x}px`;
|
520
|
+
floatingStyle.style.top = `${y}px`;
|
521
|
+
}
|
522
|
+
};
|
523
|
+
const setupJSFallback = () => {
|
524
|
+
updatePosition();
|
525
|
+
// Set up observers for position updates
|
526
|
+
resizeObserver = new ResizeObserver(updatePosition);
|
527
|
+
resizeObserver.observe(anchorEl);
|
528
|
+
resizeObserver.observe(floatingEl);
|
529
|
+
window.addEventListener('scroll', updatePosition, { passive: true });
|
530
|
+
window.addEventListener('resize', updatePosition, { passive: true });
|
531
|
+
};
|
532
|
+
const destroy = () => {
|
533
|
+
isDestroyed = true;
|
534
|
+
if (resizeObserver) {
|
535
|
+
resizeObserver.disconnect();
|
536
|
+
resizeObserver = null;
|
537
|
+
}
|
538
|
+
window.removeEventListener('scroll', updatePosition);
|
539
|
+
window.removeEventListener('resize', updatePosition);
|
540
|
+
// Clean up CSS anchor positioning
|
541
|
+
if (hasAnchorPositioning) {
|
542
|
+
anchorEl.style.removeProperty('anchor-name');
|
543
|
+
const floatingStyle = floatingEl;
|
544
|
+
floatingStyle.style.removeProperty('position-anchor');
|
545
|
+
floatingStyle.style.position = '';
|
546
|
+
}
|
547
|
+
};
|
548
|
+
// Initialize
|
549
|
+
if (!setupCSSAnchorPositioning()) {
|
550
|
+
setupJSFallback();
|
551
|
+
}
|
552
|
+
return {
|
553
|
+
update: updatePosition,
|
554
|
+
destroy
|
555
|
+
};
|
556
|
+
}
|
557
|
+
|
558
|
+
/**
|
559
|
+
* @sc4rfurryx/proteusjs/container
|
560
|
+
* Container/Style Query helpers with visualization devtools
|
561
|
+
*
|
562
|
+
* @version 1.1.0
|
563
|
+
* @author sc4rfurry
|
564
|
+
* @license MIT
|
565
|
+
*/
|
566
|
+
/**
|
567
|
+
* Sugar on native container queries with dev visualization
|
568
|
+
*/
|
569
|
+
function defineContainer(target, name, opts = {}) {
|
570
|
+
const targetEl = typeof target === 'string' ? document.querySelector(target) : target;
|
571
|
+
if (!targetEl) {
|
572
|
+
throw new Error('Target element not found');
|
573
|
+
}
|
574
|
+
const { type = 'size', inlineSize: _inlineSize = true } = opts;
|
575
|
+
const containerName = name || `container-${Math.random().toString(36).substring(2, 11)}`;
|
576
|
+
// Apply container properties
|
577
|
+
const element = targetEl;
|
578
|
+
element.style.containerName = containerName;
|
579
|
+
element.style.containerType = type;
|
580
|
+
// Warn if containment settings are missing
|
581
|
+
const computedStyle = getComputedStyle(element);
|
582
|
+
if (!computedStyle.contain || computedStyle.contain === 'none') {
|
583
|
+
console.warn(`Container "${containerName}" may need explicit containment settings for optimal performance`);
|
584
|
+
}
|
585
|
+
// Dev overlay (only in development)
|
586
|
+
if (process.env['NODE_ENV'] === 'development' || window.__PROTEUS_DEV__) {
|
587
|
+
createDevOverlay(element, containerName);
|
588
|
+
}
|
589
|
+
}
|
590
|
+
/**
|
591
|
+
* Create development overlay showing container bounds and breakpoints
|
592
|
+
*/
|
593
|
+
function createDevOverlay(element, name) {
|
594
|
+
const overlay = document.createElement('div');
|
595
|
+
overlay.className = 'proteus-container-overlay';
|
596
|
+
overlay.style.cssText = `
|
597
|
+
position: absolute;
|
598
|
+
top: 0;
|
599
|
+
left: 0;
|
600
|
+
right: 0;
|
601
|
+
bottom: 0;
|
602
|
+
pointer-events: none;
|
603
|
+
border: 2px dashed rgba(255, 0, 255, 0.5);
|
604
|
+
background: rgba(255, 0, 255, 0.05);
|
605
|
+
z-index: 9999;
|
606
|
+
font-family: monospace;
|
607
|
+
font-size: 12px;
|
608
|
+
color: #ff00ff;
|
609
|
+
`;
|
610
|
+
const label = document.createElement('div');
|
611
|
+
label.style.cssText = `
|
612
|
+
position: absolute;
|
613
|
+
top: -20px;
|
614
|
+
left: 0;
|
615
|
+
background: rgba(255, 0, 255, 0.9);
|
616
|
+
color: white;
|
617
|
+
padding: 2px 6px;
|
618
|
+
border-radius: 3px;
|
619
|
+
font-size: 10px;
|
620
|
+
white-space: nowrap;
|
621
|
+
`;
|
622
|
+
label.textContent = `Container: ${name}`;
|
623
|
+
const sizeInfo = document.createElement('div');
|
624
|
+
sizeInfo.style.cssText = `
|
625
|
+
position: absolute;
|
626
|
+
bottom: 2px;
|
627
|
+
right: 2px;
|
628
|
+
background: rgba(0, 0, 0, 0.7);
|
629
|
+
color: white;
|
630
|
+
padding: 2px 4px;
|
631
|
+
border-radius: 2px;
|
632
|
+
font-size: 10px;
|
633
|
+
`;
|
634
|
+
overlay.appendChild(label);
|
635
|
+
overlay.appendChild(sizeInfo);
|
636
|
+
// Position overlay relative to container
|
637
|
+
if (getComputedStyle(element).position === 'static') {
|
638
|
+
element.style.position = 'relative';
|
639
|
+
}
|
640
|
+
element.appendChild(overlay);
|
641
|
+
// Update size info
|
642
|
+
const updateSizeInfo = () => {
|
643
|
+
const rect = element.getBoundingClientRect();
|
644
|
+
sizeInfo.textContent = `${Math.round(rect.width)}×${Math.round(rect.height)}`;
|
645
|
+
};
|
646
|
+
updateSizeInfo();
|
647
|
+
// Update on resize
|
648
|
+
if ('ResizeObserver' in window) {
|
649
|
+
const resizeObserver = new ResizeObserver(updateSizeInfo);
|
650
|
+
resizeObserver.observe(element);
|
651
|
+
}
|
652
|
+
// Store cleanup function
|
653
|
+
element._proteusContainerCleanup = () => {
|
654
|
+
overlay.remove();
|
655
|
+
};
|
656
|
+
}
|
657
|
+
|
658
|
+
/**
|
659
|
+
* @sc4rfurryx/proteusjs/adapters/svelte
|
660
|
+
* Svelte actions and stores for ProteusJS
|
661
|
+
*
|
662
|
+
* @version 1.1.0
|
663
|
+
* @author sc4rfurry
|
664
|
+
* @license MIT
|
665
|
+
*/
|
666
|
+
/**
|
667
|
+
* Svelte action for scroll animations
|
668
|
+
*/
|
669
|
+
function proteusScroll(node, options) {
|
670
|
+
scrollAnimate(node, options);
|
671
|
+
return {
|
672
|
+
update(newOptions) {
|
673
|
+
// Re-apply with new options
|
674
|
+
scrollAnimate(node, newOptions);
|
675
|
+
},
|
676
|
+
destroy() {
|
677
|
+
// Cleanup handled by scroll module
|
678
|
+
}
|
679
|
+
};
|
680
|
+
}
|
681
|
+
/**
|
682
|
+
* Svelte action for container queries
|
683
|
+
*/
|
684
|
+
function proteusContainer(node, options = {}) {
|
685
|
+
const { name, containerOptions } = options;
|
686
|
+
defineContainer(node, name, containerOptions);
|
687
|
+
return {
|
688
|
+
update(newOptions) {
|
689
|
+
const { name: newName, containerOptions: newContainerOptions } = newOptions;
|
690
|
+
defineContainer(node, newName, newContainerOptions);
|
691
|
+
}
|
692
|
+
};
|
693
|
+
}
|
694
|
+
/**
|
695
|
+
* Svelte action for popover functionality
|
696
|
+
*/
|
697
|
+
function proteusPopover(node, options) {
|
698
|
+
let controller = null;
|
699
|
+
const { panel, popoverOptions } = options;
|
700
|
+
controller = attach(node, panel, popoverOptions);
|
701
|
+
return {
|
702
|
+
update(newOptions) {
|
703
|
+
if (controller) {
|
704
|
+
controller.destroy();
|
705
|
+
}
|
706
|
+
controller = attach(node, newOptions.panel, newOptions.popoverOptions);
|
707
|
+
},
|
708
|
+
destroy() {
|
709
|
+
if (controller) {
|
710
|
+
controller.destroy();
|
711
|
+
}
|
712
|
+
}
|
713
|
+
};
|
714
|
+
}
|
715
|
+
/**
|
716
|
+
* Svelte action for anchor positioning
|
717
|
+
*/
|
718
|
+
function proteusAnchor(node, options) {
|
719
|
+
let controller = null;
|
720
|
+
controller = tether(node, options);
|
721
|
+
return {
|
722
|
+
update(newOptions) {
|
723
|
+
if (controller) {
|
724
|
+
controller.destroy();
|
725
|
+
}
|
726
|
+
controller = tether(node, newOptions);
|
727
|
+
},
|
728
|
+
destroy() {
|
729
|
+
if (controller) {
|
730
|
+
controller.destroy();
|
731
|
+
}
|
732
|
+
}
|
733
|
+
};
|
734
|
+
}
|
735
|
+
/**
|
736
|
+
* Svelte action for performance optimizations
|
737
|
+
*/
|
738
|
+
function proteusPerf(node) {
|
739
|
+
const observer = new IntersectionObserver((entries) => {
|
740
|
+
entries.forEach(entry => {
|
741
|
+
if (entry.isIntersecting) {
|
742
|
+
node.style.contentVisibility = 'visible';
|
743
|
+
}
|
744
|
+
else {
|
745
|
+
node.style.contentVisibility = 'auto';
|
746
|
+
}
|
747
|
+
});
|
748
|
+
}, { rootMargin: '50px' });
|
749
|
+
observer.observe(node);
|
750
|
+
return {
|
751
|
+
destroy() {
|
752
|
+
observer.disconnect();
|
753
|
+
}
|
754
|
+
};
|
755
|
+
}
|
756
|
+
/**
|
757
|
+
* Svelte action for accessibility enhancements
|
758
|
+
*/
|
759
|
+
function proteusA11y(node, options = {}) {
|
760
|
+
const { announceChanges = false } = options;
|
761
|
+
// Enhance focus indicators
|
762
|
+
const focusableElements = node.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
763
|
+
const focusHandlers = new Map();
|
764
|
+
focusableElements.forEach(element => {
|
765
|
+
const htmlEl = element;
|
766
|
+
const focusHandler = () => {
|
767
|
+
htmlEl.style.outline = '2px solid #0066cc';
|
768
|
+
htmlEl.style.outlineOffset = '2px';
|
769
|
+
};
|
770
|
+
const blurHandler = () => {
|
771
|
+
htmlEl.style.outline = 'none';
|
772
|
+
};
|
773
|
+
htmlEl.addEventListener('focus', focusHandler);
|
774
|
+
htmlEl.addEventListener('blur', blurHandler);
|
775
|
+
focusHandlers.set(htmlEl, { focusHandler, blurHandler });
|
776
|
+
});
|
777
|
+
let mutationObserver = null;
|
778
|
+
if (announceChanges) {
|
779
|
+
mutationObserver = new MutationObserver(() => {
|
780
|
+
const announcement = document.createElement('div');
|
781
|
+
announcement.setAttribute('aria-live', 'polite');
|
782
|
+
announcement.style.position = 'absolute';
|
783
|
+
announcement.style.left = '-10000px';
|
784
|
+
announcement.textContent = 'Content updated';
|
785
|
+
document.body.appendChild(announcement);
|
786
|
+
setTimeout(() => {
|
787
|
+
document.body.removeChild(announcement);
|
788
|
+
}, 1000);
|
789
|
+
});
|
790
|
+
mutationObserver.observe(node, { childList: true, subtree: true });
|
791
|
+
}
|
792
|
+
return {
|
793
|
+
destroy() {
|
794
|
+
// Clean up focus handlers
|
795
|
+
focusHandlers.forEach(({ focusHandler, blurHandler }, element) => {
|
796
|
+
element.removeEventListener('focus', focusHandler);
|
797
|
+
element.removeEventListener('blur', blurHandler);
|
798
|
+
});
|
799
|
+
if (mutationObserver) {
|
800
|
+
mutationObserver.disconnect();
|
801
|
+
}
|
802
|
+
}
|
803
|
+
};
|
804
|
+
}
|
805
|
+
/**
|
806
|
+
* Store for managing popover state
|
807
|
+
*/
|
808
|
+
function createPopover(triggerSelector, panelSelector, options) {
|
809
|
+
const isOpen = writable(false);
|
810
|
+
let controller = null;
|
811
|
+
const initialize = () => {
|
812
|
+
const trigger = document.querySelector(triggerSelector);
|
813
|
+
const panel = document.querySelector(panelSelector);
|
814
|
+
if (trigger && panel) {
|
815
|
+
controller = attach(trigger, panel, {
|
816
|
+
...options,
|
817
|
+
onOpen: () => {
|
818
|
+
isOpen.set(true);
|
819
|
+
options?.onOpen?.();
|
820
|
+
},
|
821
|
+
onClose: () => {
|
822
|
+
isOpen.set(false);
|
823
|
+
options?.onClose?.();
|
824
|
+
}
|
825
|
+
});
|
826
|
+
}
|
827
|
+
};
|
828
|
+
const open = () => controller?.open();
|
829
|
+
const close = () => controller?.close();
|
830
|
+
const toggle = () => controller?.toggle();
|
831
|
+
const destroy = () => {
|
832
|
+
if (controller) {
|
833
|
+
controller.destroy();
|
834
|
+
controller = null;
|
835
|
+
}
|
836
|
+
};
|
837
|
+
return {
|
838
|
+
isOpen,
|
839
|
+
initialize,
|
840
|
+
open,
|
841
|
+
close,
|
842
|
+
toggle,
|
843
|
+
destroy
|
844
|
+
};
|
845
|
+
}
|
846
|
+
/**
|
847
|
+
* Store for managing transition state
|
848
|
+
*/
|
849
|
+
function createTransition() {
|
850
|
+
const isTransitioning = writable(false);
|
851
|
+
const runTransition = async (run, opts) => {
|
852
|
+
isTransitioning.set(true);
|
853
|
+
try {
|
854
|
+
await transition(run, opts);
|
855
|
+
}
|
856
|
+
finally {
|
857
|
+
isTransitioning.set(false);
|
858
|
+
}
|
859
|
+
};
|
860
|
+
return {
|
861
|
+
isTransitioning,
|
862
|
+
runTransition
|
863
|
+
};
|
864
|
+
}
|
865
|
+
/**
|
866
|
+
* Utility function to create reactive container size store
|
867
|
+
*/
|
868
|
+
function createContainerSize(element) {
|
869
|
+
const size = writable({ width: 0, height: 0 });
|
870
|
+
if ('ResizeObserver' in window) {
|
871
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
872
|
+
for (const entry of entries) {
|
873
|
+
const { width, height } = entry.contentRect;
|
874
|
+
size.set({ width, height });
|
875
|
+
}
|
876
|
+
});
|
877
|
+
resizeObserver.observe(element);
|
878
|
+
return {
|
879
|
+
size,
|
880
|
+
destroy: () => resizeObserver.disconnect()
|
881
|
+
};
|
882
|
+
}
|
883
|
+
// Fallback for browsers without ResizeObserver
|
884
|
+
const updateSize = () => {
|
885
|
+
const rect = element.getBoundingClientRect();
|
886
|
+
size.set({ width: rect.width, height: rect.height });
|
887
|
+
};
|
888
|
+
updateSize();
|
889
|
+
window.addEventListener('resize', updateSize);
|
890
|
+
return {
|
891
|
+
size,
|
892
|
+
destroy: () => window.removeEventListener('resize', updateSize)
|
893
|
+
};
|
894
|
+
}
|
895
|
+
// Export all actions and utilities
|
896
|
+
var svelte = {
|
897
|
+
proteusScroll,
|
898
|
+
proteusContainer,
|
899
|
+
proteusPopover,
|
900
|
+
proteusAnchor,
|
901
|
+
proteusPerf,
|
902
|
+
proteusA11y,
|
903
|
+
createPopover,
|
904
|
+
createTransition,
|
905
|
+
createContainerSize
|
906
|
+
};
|
907
|
+
|
908
|
+
export { createContainerSize, createPopover, createTransition, svelte as default, proteusA11y, proteusAnchor, proteusContainer, proteusPerf, proteusPopover, proteusScroll };
|
909
|
+
//# sourceMappingURL=svelte.esm.js.map
|