@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.
Files changed (65) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +331 -77
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/react.d.ts +140 -0
  5. package/dist/adapters/react.esm.js +849 -0
  6. package/dist/adapters/react.esm.js.map +1 -0
  7. package/dist/adapters/svelte.d.ts +181 -0
  8. package/dist/adapters/svelte.esm.js +909 -0
  9. package/dist/adapters/svelte.esm.js.map +1 -0
  10. package/dist/adapters/vue.d.ts +205 -0
  11. package/dist/adapters/vue.esm.js +873 -0
  12. package/dist/adapters/vue.esm.js.map +1 -0
  13. package/dist/modules/a11y-audit.d.ts +31 -0
  14. package/dist/modules/a11y-audit.esm.js +64 -0
  15. package/dist/modules/a11y-audit.esm.js.map +1 -0
  16. package/dist/modules/a11y-primitives.d.ts +36 -0
  17. package/dist/modules/a11y-primitives.esm.js +114 -0
  18. package/dist/modules/a11y-primitives.esm.js.map +1 -0
  19. package/dist/modules/anchor.d.ts +30 -0
  20. package/dist/modules/anchor.esm.js +219 -0
  21. package/dist/modules/anchor.esm.js.map +1 -0
  22. package/dist/modules/container.d.ts +60 -0
  23. package/dist/modules/container.esm.js +194 -0
  24. package/dist/modules/container.esm.js.map +1 -0
  25. package/dist/modules/perf.d.ts +82 -0
  26. package/dist/modules/perf.esm.js +257 -0
  27. package/dist/modules/perf.esm.js.map +1 -0
  28. package/dist/modules/popover.d.ts +33 -0
  29. package/dist/modules/popover.esm.js +191 -0
  30. package/dist/modules/popover.esm.js.map +1 -0
  31. package/dist/modules/scroll.d.ts +43 -0
  32. package/dist/modules/scroll.esm.js +195 -0
  33. package/dist/modules/scroll.esm.js.map +1 -0
  34. package/dist/modules/transitions.d.ts +35 -0
  35. package/dist/modules/transitions.esm.js +120 -0
  36. package/dist/modules/transitions.esm.js.map +1 -0
  37. package/dist/modules/typography.d.ts +72 -0
  38. package/dist/modules/typography.esm.js +168 -0
  39. package/dist/modules/typography.esm.js.map +1 -0
  40. package/dist/proteus.cjs.js +1554 -12
  41. package/dist/proteus.cjs.js.map +1 -1
  42. package/dist/proteus.d.ts +516 -12
  43. package/dist/proteus.esm.js +1545 -12
  44. package/dist/proteus.esm.js.map +1 -1
  45. package/dist/proteus.esm.min.js +3 -3
  46. package/dist/proteus.esm.min.js.map +1 -1
  47. package/dist/proteus.js +1554 -12
  48. package/dist/proteus.js.map +1 -1
  49. package/dist/proteus.min.js +3 -3
  50. package/dist/proteus.min.js.map +1 -1
  51. package/package.json +69 -7
  52. package/src/adapters/react.ts +264 -0
  53. package/src/adapters/svelte.ts +321 -0
  54. package/src/adapters/vue.ts +268 -0
  55. package/src/index.ts +33 -6
  56. package/src/modules/a11y-audit/index.ts +84 -0
  57. package/src/modules/a11y-primitives/index.ts +152 -0
  58. package/src/modules/anchor/index.ts +259 -0
  59. package/src/modules/container/index.ts +230 -0
  60. package/src/modules/perf/index.ts +291 -0
  61. package/src/modules/popover/index.ts +238 -0
  62. package/src/modules/scroll/index.ts +251 -0
  63. package/src/modules/transitions/index.ts +145 -0
  64. package/src/modules/typography/index.ts +239 -0
  65. package/src/utils/version.ts +1 -1
@@ -0,0 +1,291 @@
1
+ /**
2
+ * @sc4rfurryx/proteusjs/perf
3
+ * Performance guardrails and CWV-friendly patterns
4
+ *
5
+ * @version 1.1.0
6
+ * @author sc4rfurry
7
+ * @license MIT
8
+ */
9
+
10
+ export interface SpeculationOptions {
11
+ prerender?: string[];
12
+ prefetch?: string[];
13
+ sameOriginOnly?: boolean;
14
+ }
15
+
16
+ export interface ContentVisibilityOptions {
17
+ containIntrinsicSize?: string;
18
+ }
19
+
20
+ /**
21
+ * Apply content-visibility for performance optimization
22
+ */
23
+ export function contentVisibility(
24
+ selector: string | Element,
25
+ mode: 'auto' | 'hidden' = 'auto',
26
+ opts: ContentVisibilityOptions = {}
27
+ ): void {
28
+ const elements = typeof selector === 'string'
29
+ ? document.querySelectorAll(selector)
30
+ : [selector];
31
+
32
+ const { containIntrinsicSize = '1000px 400px' } = opts;
33
+
34
+ elements.forEach(element => {
35
+ const el = element as HTMLElement;
36
+ el.style.contentVisibility = mode;
37
+
38
+ if (mode === 'auto') {
39
+ el.style.containIntrinsicSize = containIntrinsicSize;
40
+ }
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Set fetch priority for resources
46
+ */
47
+ export function fetchPriority(
48
+ selector: string | Element,
49
+ priority: 'high' | 'low' | 'auto'
50
+ ): void {
51
+ const elements = typeof selector === 'string'
52
+ ? document.querySelectorAll(selector)
53
+ : [selector];
54
+
55
+ elements.forEach(element => {
56
+ if (element instanceof HTMLImageElement ||
57
+ element instanceof HTMLLinkElement ||
58
+ element instanceof HTMLScriptElement) {
59
+ (element as HTMLImageElement | HTMLLinkElement | HTMLScriptElement & { fetchPriority?: string }).fetchPriority = priority;
60
+ }
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Set up speculation rules for prerendering and prefetching
66
+ */
67
+ export function speculate(opts: SpeculationOptions): void {
68
+ const { prerender = [], prefetch = [], sameOriginOnly = true } = opts;
69
+
70
+ // Check for Speculation Rules API support
71
+ if (!('supports' in HTMLScriptElement && HTMLScriptElement.supports('speculationrules'))) {
72
+ console.warn('Speculation Rules API not supported');
73
+ return;
74
+ }
75
+
76
+ const rules: any = {};
77
+
78
+ if (prerender.length > 0) {
79
+ rules.prerender = prerender.map(url => {
80
+ const rule: any = { where: { href_matches: url } };
81
+ if (sameOriginOnly) {
82
+ rule.where.href_matches = new URL(url, window.location.origin).href;
83
+ }
84
+ return rule;
85
+ });
86
+ }
87
+
88
+ if (prefetch.length > 0) {
89
+ rules.prefetch = prefetch.map(url => {
90
+ const rule: any = { where: { href_matches: url } };
91
+ if (sameOriginOnly) {
92
+ rule.where.href_matches = new URL(url, window.location.origin).href;
93
+ }
94
+ return rule;
95
+ });
96
+ }
97
+
98
+ if (Object.keys(rules).length === 0) return;
99
+
100
+ // Create and inject speculation rules script
101
+ const script = document.createElement('script');
102
+ script.type = 'speculationrules';
103
+ script.textContent = JSON.stringify(rules);
104
+ document.head.appendChild(script);
105
+ }
106
+
107
+ /**
108
+ * Yield to browser using scheduler.yield or postTask when available
109
+ */
110
+ export async function yieldToBrowser(): Promise<void> {
111
+ // Use scheduler.yield if available (Chrome 115+)
112
+ if ('scheduler' in window && 'yield' in (window as any).scheduler) {
113
+ return (window as any).scheduler.yield();
114
+ }
115
+
116
+ // Use scheduler.postTask if available
117
+ if ('scheduler' in window && 'postTask' in (window as any).scheduler) {
118
+ return new Promise(resolve => {
119
+ (window as any).scheduler.postTask(resolve, { priority: 'user-blocking' });
120
+ });
121
+ }
122
+
123
+ // Fallback to setTimeout
124
+ return new Promise(resolve => {
125
+ setTimeout(resolve, 0);
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Optimize images with loading and decoding hints
131
+ */
132
+ export function optimizeImages(selector: string | Element = 'img'): void {
133
+ const images = typeof selector === 'string'
134
+ ? document.querySelectorAll(selector)
135
+ : [selector];
136
+
137
+ images.forEach(img => {
138
+ if (!(img instanceof HTMLImageElement)) return;
139
+
140
+ // Set loading attribute if not already set
141
+ if (!img.hasAttribute('loading')) {
142
+ const rect = img.getBoundingClientRect();
143
+ const isAboveFold = rect.top < window.innerHeight;
144
+ img.loading = isAboveFold ? 'eager' : 'lazy';
145
+ }
146
+
147
+ // Set decoding hint
148
+ if (!img.hasAttribute('decoding')) {
149
+ img.decoding = 'async';
150
+ }
151
+
152
+ // Set fetch priority for above-fold images
153
+ if (!img.hasAttribute('fetchpriority')) {
154
+ const rect = img.getBoundingClientRect();
155
+ const isAboveFold = rect.top < window.innerHeight;
156
+ if (isAboveFold) {
157
+ (img as any).fetchPriority = 'high';
158
+ }
159
+ }
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Preload critical resources
165
+ */
166
+ export function preloadCritical(resources: Array<{ href: string; as: string; type?: string }>): void {
167
+ resources.forEach(({ href, as, type }) => {
168
+ // Check if already preloaded
169
+ const existing = document.querySelector(`link[rel="preload"][href="${href}"]`);
170
+ if (existing) return;
171
+
172
+ const link = document.createElement('link');
173
+ link.rel = 'preload';
174
+ link.href = href;
175
+ link.as = as;
176
+ if (type) {
177
+ link.type = type;
178
+ }
179
+ document.head.appendChild(link);
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Measure and report Core Web Vitals
185
+ */
186
+ export function measureCWV(): Promise<{ lcp?: number; fid?: number; cls?: number }> {
187
+ return new Promise(resolve => {
188
+ const metrics: { lcp?: number; fid?: number; cls?: number } = {};
189
+ let metricsCount = 0;
190
+ const totalMetrics = 3;
191
+
192
+ const checkComplete = () => {
193
+ metricsCount++;
194
+ if (metricsCount >= totalMetrics) {
195
+ resolve(metrics);
196
+ }
197
+ };
198
+
199
+ // LCP (Largest Contentful Paint)
200
+ if ('PerformanceObserver' in window) {
201
+ try {
202
+ const lcpObserver = new PerformanceObserver(list => {
203
+ const entries = list.getEntries();
204
+ const lastEntry = entries[entries.length - 1] as any;
205
+ metrics.lcp = lastEntry.startTime;
206
+ });
207
+ lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
208
+
209
+ // Stop observing after 10 seconds
210
+ setTimeout(() => {
211
+ lcpObserver.disconnect();
212
+ checkComplete();
213
+ }, 10000);
214
+ } catch {
215
+ checkComplete();
216
+ }
217
+
218
+ // FID (First Input Delay)
219
+ try {
220
+ const fidObserver = new PerformanceObserver(list => {
221
+ const entries = list.getEntries();
222
+ entries.forEach((entry: any) => {
223
+ metrics.fid = entry.processingStart - entry.startTime;
224
+ });
225
+ fidObserver.disconnect();
226
+ checkComplete();
227
+ });
228
+ fidObserver.observe({ entryTypes: ['first-input'] });
229
+
230
+ // If no input after 10 seconds, consider FID as 0
231
+ setTimeout(() => {
232
+ if (metrics.fid === undefined) {
233
+ metrics.fid = 0;
234
+ fidObserver.disconnect();
235
+ checkComplete();
236
+ }
237
+ }, 10000);
238
+ } catch {
239
+ checkComplete();
240
+ }
241
+
242
+ // CLS (Cumulative Layout Shift)
243
+ try {
244
+ let clsValue = 0;
245
+ const clsObserver = new PerformanceObserver(list => {
246
+ list.getEntries().forEach((entry: any) => {
247
+ if (!entry.hadRecentInput) {
248
+ clsValue += entry.value;
249
+ }
250
+ });
251
+ metrics.cls = clsValue;
252
+ });
253
+ clsObserver.observe({ entryTypes: ['layout-shift'] });
254
+
255
+ // Stop observing after 10 seconds
256
+ setTimeout(() => {
257
+ clsObserver.disconnect();
258
+ checkComplete();
259
+ }, 10000);
260
+ } catch {
261
+ checkComplete();
262
+ }
263
+ } else {
264
+ // Fallback if PerformanceObserver is not supported
265
+ setTimeout(() => resolve(metrics), 100);
266
+ }
267
+ });
268
+ }
269
+
270
+ // Export boost object to match usage examples in upgrade spec
271
+ export const boost = {
272
+ contentVisibility,
273
+ fetchPriority,
274
+ speculate,
275
+ yieldToBrowser,
276
+ optimizeImages,
277
+ preloadCritical,
278
+ measureCWV
279
+ };
280
+
281
+ // Export all functions as named exports and default object
282
+ export default {
283
+ contentVisibility,
284
+ fetchPriority,
285
+ speculate,
286
+ yieldToBrowser,
287
+ optimizeImages,
288
+ preloadCritical,
289
+ measureCWV,
290
+ boost
291
+ };
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @sc4rfurryx/proteusjs/popover
3
+ * HTML Popover API wrapper with robust focus/inert handling
4
+ *
5
+ * @version 1.1.0
6
+ * @author sc4rfurry
7
+ * @license MIT
8
+ */
9
+
10
+ export interface PopoverOptions {
11
+ type?: 'menu' | 'dialog' | 'tooltip';
12
+ trapFocus?: boolean;
13
+ restoreFocus?: boolean;
14
+ closeOnEscape?: boolean;
15
+ onOpen?: () => void;
16
+ onClose?: () => void;
17
+ }
18
+
19
+ export interface PopoverController {
20
+ open(): void;
21
+ close(): void;
22
+ toggle(): void;
23
+ destroy(): void;
24
+ }
25
+
26
+ /**
27
+ * Unified API for menus, tooltips, and dialogs using the native Popover API
28
+ * with robust focus/inert handling
29
+ */
30
+ export function attach(
31
+ trigger: Element | string,
32
+ panel: Element | string,
33
+ opts: PopoverOptions = {}
34
+ ): PopoverController {
35
+ const triggerEl = typeof trigger === 'string' ? document.querySelector(trigger) : trigger;
36
+ const panelEl = typeof panel === 'string' ? document.querySelector(panel) : panel;
37
+
38
+ if (!triggerEl || !panelEl) {
39
+ throw new Error('Both trigger and panel elements must exist');
40
+ }
41
+
42
+ const {
43
+ type = 'menu',
44
+ trapFocus = type === 'dialog',
45
+ restoreFocus = true,
46
+ closeOnEscape = true,
47
+ onOpen,
48
+ onClose
49
+ } = opts;
50
+
51
+ let isOpen = false;
52
+ let previousFocus: Element | null = null;
53
+ let focusTrap: FocusTrap | null = null;
54
+
55
+ // Check for native Popover API support
56
+ const hasPopoverAPI = 'popover' in HTMLElement.prototype;
57
+
58
+ // Set up ARIA attributes
59
+ const setupAria = () => {
60
+ const panelId = panelEl.id || `popover-${Math.random().toString(36).substr(2, 9)}`;
61
+ panelEl.id = panelId;
62
+
63
+ triggerEl.setAttribute('aria-expanded', 'false');
64
+ triggerEl.setAttribute('aria-controls', panelId);
65
+
66
+ if (type === 'menu') {
67
+ triggerEl.setAttribute('aria-haspopup', 'menu');
68
+ panelEl.setAttribute('role', 'menu');
69
+ } else if (type === 'dialog') {
70
+ triggerEl.setAttribute('aria-haspopup', 'dialog');
71
+ panelEl.setAttribute('role', 'dialog');
72
+ panelEl.setAttribute('aria-modal', 'true');
73
+ } else if (type === 'tooltip') {
74
+ triggerEl.setAttribute('aria-describedby', panelId);
75
+ panelEl.setAttribute('role', 'tooltip');
76
+ }
77
+ };
78
+
79
+ // Set up native popover if supported
80
+ const setupNativePopover = () => {
81
+ if (hasPopoverAPI) {
82
+ (panelEl as any).popover = type === 'dialog' ? 'manual' : 'auto';
83
+ triggerEl.setAttribute('popovertarget', panelEl.id);
84
+ }
85
+ };
86
+
87
+ // Focus trap implementation
88
+ class FocusTrap {
89
+ private focusableElements: Element[] = [];
90
+
91
+ constructor(private container: Element) {
92
+ this.updateFocusableElements();
93
+ }
94
+
95
+ private updateFocusableElements() {
96
+ const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
97
+ this.focusableElements = Array.from(this.container.querySelectorAll(selector));
98
+ }
99
+
100
+ activate() {
101
+ this.updateFocusableElements();
102
+ if (this.focusableElements.length > 0) {
103
+ (this.focusableElements[0] as HTMLElement).focus();
104
+ }
105
+ document.addEventListener('keydown', this.handleKeyDown);
106
+ }
107
+
108
+ deactivate() {
109
+ document.removeEventListener('keydown', this.handleKeyDown);
110
+ }
111
+
112
+ private handleKeyDown = (e: KeyboardEvent) => {
113
+ if (e.key !== 'Tab') return;
114
+
115
+ const firstElement = this.focusableElements[0] as HTMLElement;
116
+ const lastElement = this.focusableElements[this.focusableElements.length - 1] as HTMLElement;
117
+
118
+ if (e.shiftKey) {
119
+ if (document.activeElement === firstElement) {
120
+ e.preventDefault();
121
+ lastElement.focus();
122
+ }
123
+ } else {
124
+ if (document.activeElement === lastElement) {
125
+ e.preventDefault();
126
+ firstElement.focus();
127
+ }
128
+ }
129
+ };
130
+ }
131
+
132
+ const open = () => {
133
+ if (isOpen) return;
134
+
135
+ if (restoreFocus) {
136
+ previousFocus = document.activeElement;
137
+ }
138
+
139
+ if (hasPopoverAPI) {
140
+ (panelEl as any).showPopover();
141
+ } else {
142
+ (panelEl as HTMLElement).style.display = 'block';
143
+ panelEl.setAttribute('data-popover-open', 'true');
144
+ }
145
+
146
+ triggerEl.setAttribute('aria-expanded', 'true');
147
+ isOpen = true;
148
+
149
+ if (trapFocus) {
150
+ focusTrap = new FocusTrap(panelEl);
151
+ focusTrap.activate();
152
+ }
153
+
154
+ if (onOpen) {
155
+ onOpen();
156
+ }
157
+ };
158
+
159
+ const close = () => {
160
+ if (!isOpen) return;
161
+
162
+ if (hasPopoverAPI) {
163
+ (panelEl as any).hidePopover();
164
+ } else {
165
+ (panelEl as HTMLElement).style.display = 'none';
166
+ panelEl.removeAttribute('data-popover-open');
167
+ }
168
+
169
+ triggerEl.setAttribute('aria-expanded', 'false');
170
+ isOpen = false;
171
+
172
+ if (focusTrap) {
173
+ focusTrap.deactivate();
174
+ focusTrap = null;
175
+ }
176
+
177
+ if (restoreFocus && previousFocus) {
178
+ (previousFocus as HTMLElement).focus();
179
+ previousFocus = null;
180
+ }
181
+
182
+ if (onClose) {
183
+ onClose();
184
+ }
185
+ };
186
+
187
+ const toggle = () => {
188
+ if (isOpen) {
189
+ close();
190
+ } else {
191
+ open();
192
+ }
193
+ };
194
+
195
+ const handleKeyDown = (e: KeyboardEvent) => {
196
+ if (closeOnEscape && e.key === 'Escape' && isOpen) {
197
+ e.preventDefault();
198
+ close();
199
+ }
200
+ };
201
+
202
+ const handleClick = (e: Event) => {
203
+ e.preventDefault();
204
+ toggle();
205
+ };
206
+
207
+ const destroy = () => {
208
+ triggerEl.removeEventListener('click', handleClick);
209
+ document.removeEventListener('keydown', handleKeyDown);
210
+
211
+ if (focusTrap) {
212
+ focusTrap.deactivate();
213
+ }
214
+
215
+ if (isOpen) {
216
+ close();
217
+ }
218
+ };
219
+
220
+ // Initialize
221
+ setupAria();
222
+ setupNativePopover();
223
+
224
+ triggerEl.addEventListener('click', handleClick);
225
+ document.addEventListener('keydown', handleKeyDown);
226
+
227
+ return {
228
+ open,
229
+ close,
230
+ toggle,
231
+ destroy
232
+ };
233
+ }
234
+
235
+ // Export default object for convenience
236
+ export default {
237
+ attach
238
+ };