tekivex-ui 2.0.0
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 +21 -0
- package/README.md +201 -0
- package/dist/index.cjs +70 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11002 -0
- package/dist/src/a11y/index.d.ts +24 -0
- package/dist/src/a11y/index.d.ts.map +1 -0
- package/dist/src/components/TkxAccordion.d.ts +32 -0
- package/dist/src/components/TkxAccordion.d.ts.map +1 -0
- package/dist/src/components/TkxAlert.d.ts +14 -0
- package/dist/src/components/TkxAlert.d.ts.map +1 -0
- package/dist/src/components/TkxAvatar.d.ts +14 -0
- package/dist/src/components/TkxAvatar.d.ts.map +1 -0
- package/dist/src/components/TkxBadge.d.ts +12 -0
- package/dist/src/components/TkxBadge.d.ts.map +1 -0
- package/dist/src/components/TkxButton.d.ts +17 -0
- package/dist/src/components/TkxButton.d.ts.map +1 -0
- package/dist/src/components/TkxCard.d.ts +20 -0
- package/dist/src/components/TkxCard.d.ts.map +1 -0
- package/dist/src/components/TkxCarousel.d.ts +32 -0
- package/dist/src/components/TkxCarousel.d.ts.map +1 -0
- package/dist/src/components/TkxChat.d.ts +32 -0
- package/dist/src/components/TkxChat.d.ts.map +1 -0
- package/dist/src/components/TkxCheckbox.d.ts +13 -0
- package/dist/src/components/TkxCheckbox.d.ts.map +1 -0
- package/dist/src/components/TkxClock.d.ts +23 -0
- package/dist/src/components/TkxClock.d.ts.map +1 -0
- package/dist/src/components/TkxColorPicker.d.ts +18 -0
- package/dist/src/components/TkxColorPicker.d.ts.map +1 -0
- package/dist/src/components/TkxCommand.d.ts +34 -0
- package/dist/src/components/TkxCommand.d.ts.map +1 -0
- package/dist/src/components/TkxDatePicker.d.ts +47 -0
- package/dist/src/components/TkxDatePicker.d.ts.map +1 -0
- package/dist/src/components/TkxDivider.d.ts +8 -0
- package/dist/src/components/TkxDivider.d.ts.map +1 -0
- package/dist/src/components/TkxDrawer.d.ts +16 -0
- package/dist/src/components/TkxDrawer.d.ts.map +1 -0
- package/dist/src/components/TkxFileUpload.d.ts +16 -0
- package/dist/src/components/TkxFileUpload.d.ts.map +1 -0
- package/dist/src/components/TkxIcon.d.ts +16 -0
- package/dist/src/components/TkxIcon.d.ts.map +1 -0
- package/dist/src/components/TkxImage.d.ts +16 -0
- package/dist/src/components/TkxImage.d.ts.map +1 -0
- package/dist/src/components/TkxInput.d.ts +13 -0
- package/dist/src/components/TkxInput.d.ts.map +1 -0
- package/dist/src/components/TkxLogo.d.ts +21 -0
- package/dist/src/components/TkxLogo.d.ts.map +1 -0
- package/dist/src/components/TkxMenu.d.ts +64 -0
- package/dist/src/components/TkxMenu.d.ts.map +1 -0
- package/dist/src/components/TkxModal.d.ts +14 -0
- package/dist/src/components/TkxModal.d.ts.map +1 -0
- package/dist/src/components/TkxNumberInput.d.ts +32 -0
- package/dist/src/components/TkxNumberInput.d.ts.map +1 -0
- package/dist/src/components/TkxOTP.d.ts +24 -0
- package/dist/src/components/TkxOTP.d.ts.map +1 -0
- package/dist/src/components/TkxPagination.d.ts +19 -0
- package/dist/src/components/TkxPagination.d.ts.map +1 -0
- package/dist/src/components/TkxProgress.d.ts +13 -0
- package/dist/src/components/TkxProgress.d.ts.map +1 -0
- package/dist/src/components/TkxRadio.d.ts +22 -0
- package/dist/src/components/TkxRadio.d.ts.map +1 -0
- package/dist/src/components/TkxRating.d.ts +18 -0
- package/dist/src/components/TkxRating.d.ts.map +1 -0
- package/dist/src/components/TkxSelect.d.ts +37 -0
- package/dist/src/components/TkxSelect.d.ts.map +1 -0
- package/dist/src/components/TkxSkeleton.d.ts +12 -0
- package/dist/src/components/TkxSkeleton.d.ts.map +1 -0
- package/dist/src/components/TkxSlider.d.ts +33 -0
- package/dist/src/components/TkxSlider.d.ts.map +1 -0
- package/dist/src/components/TkxStepper.d.ts +31 -0
- package/dist/src/components/TkxStepper.d.ts.map +1 -0
- package/dist/src/components/TkxTable.d.ts +23 -0
- package/dist/src/components/TkxTable.d.ts.map +1 -0
- package/dist/src/components/TkxTabs.d.ts +32 -0
- package/dist/src/components/TkxTabs.d.ts.map +1 -0
- package/dist/src/components/TkxTag.d.ts +30 -0
- package/dist/src/components/TkxTag.d.ts.map +1 -0
- package/dist/src/components/TkxTimeline.d.ts +20 -0
- package/dist/src/components/TkxTimeline.d.ts.map +1 -0
- package/dist/src/components/TkxToast.d.ts +23 -0
- package/dist/src/components/TkxToast.d.ts.map +1 -0
- package/dist/src/components/TkxToggle.d.ts +11 -0
- package/dist/src/components/TkxToggle.d.ts.map +1 -0
- package/dist/src/components/TkxTooltip.d.ts +10 -0
- package/dist/src/components/TkxTooltip.d.ts.map +1 -0
- package/dist/src/components/TkxVideoPlayer.d.ts +43 -0
- package/dist/src/components/TkxVideoPlayer.d.ts.map +1 -0
- package/dist/src/components/index.d.ts +41 -0
- package/dist/src/components/index.d.ts.map +1 -0
- package/dist/src/engine/css.d.ts +24 -0
- package/dist/src/engine/css.d.ts.map +1 -0
- package/dist/src/engine/index.d.ts +10 -0
- package/dist/src/engine/index.d.ts.map +1 -0
- package/dist/src/engine/quantum.d.ts +47 -0
- package/dist/src/engine/quantum.d.ts.map +1 -0
- package/dist/src/engine/security.d.ts +54 -0
- package/dist/src/engine/security.d.ts.map +1 -0
- package/dist/src/engine/tkx.d.ts +29 -0
- package/dist/src/engine/tkx.d.ts.map +1 -0
- package/dist/src/engine/wcag.d.ts +34 -0
- package/dist/src/engine/wcag.d.ts.map +1 -0
- package/dist/src/hooks/index.d.ts +8 -0
- package/dist/src/hooks/index.d.ts.map +1 -0
- package/dist/src/themes/index.d.ts +26 -0
- package/dist/src/themes/index.d.ts.map +1 -0
- package/dist/style.css +1 -0
- package/dist/tests/TkxButton.test.d.ts +2 -0
- package/dist/tests/TkxButton.test.d.ts.map +1 -0
- package/dist/tests/a11y/components.test.d.ts +2 -0
- package/dist/tests/a11y/components.test.d.ts.map +1 -0
- package/dist/tests/quantum.test.d.ts +2 -0
- package/dist/tests/quantum.test.d.ts.map +1 -0
- package/dist/tests/security.test.d.ts +2 -0
- package/dist/tests/security.test.d.ts.map +1 -0
- package/dist/tests/setup.d.ts +2 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/tkx.test.d.ts +2 -0
- package/dist/tests/tkx.test.d.ts.map +1 -0
- package/dist/tests/wcag.test.d.ts +2 -0
- package/dist/tests/wcag.test.d.ts.map +1 -0
- package/package.json +78 -0
- package/src/a11y/index.tsx +87 -0
- package/src/components/TkxAccordion.tsx +588 -0
- package/src/components/TkxAlert.tsx +91 -0
- package/src/components/TkxAvatar.tsx +101 -0
- package/src/components/TkxBadge.tsx +73 -0
- package/src/components/TkxButton.tsx +153 -0
- package/src/components/TkxCard.tsx +92 -0
- package/src/components/TkxCarousel.tsx +552 -0
- package/src/components/TkxChat.tsx +354 -0
- package/src/components/TkxCheckbox.tsx +218 -0
- package/src/components/TkxClock.tsx +601 -0
- package/src/components/TkxColorPicker.tsx +633 -0
- package/src/components/TkxCommand.tsx +644 -0
- package/src/components/TkxDatePicker.tsx +1567 -0
- package/src/components/TkxDivider.tsx +49 -0
- package/src/components/TkxDrawer.tsx +234 -0
- package/src/components/TkxFileUpload.tsx +371 -0
- package/src/components/TkxIcon.tsx +683 -0
- package/src/components/TkxImage.tsx +277 -0
- package/src/components/TkxInput.tsx +101 -0
- package/src/components/TkxLogo.tsx +297 -0
- package/src/components/TkxMenu.tsx +1042 -0
- package/src/components/TkxModal.tsx +119 -0
- package/src/components/TkxNumberInput.tsx +408 -0
- package/src/components/TkxOTP.tsx +295 -0
- package/src/components/TkxPagination.tsx +388 -0
- package/src/components/TkxProgress.tsx +114 -0
- package/src/components/TkxRadio.tsx +284 -0
- package/src/components/TkxRating.tsx +286 -0
- package/src/components/TkxSelect.tsx +923 -0
- package/src/components/TkxSkeleton.tsx +105 -0
- package/src/components/TkxSlider.tsx +492 -0
- package/src/components/TkxStepper.tsx +485 -0
- package/src/components/TkxTable.tsx +148 -0
- package/src/components/TkxTabs.tsx +124 -0
- package/src/components/TkxTag.tsx +319 -0
- package/src/components/TkxTimeline.tsx +303 -0
- package/src/components/TkxToast.tsx +284 -0
- package/src/components/TkxToggle.tsx +93 -0
- package/src/components/TkxTooltip.tsx +69 -0
- package/src/components/TkxVideoPlayer.tsx +1013 -0
- package/src/components/index.ts +40 -0
- package/src/engine/css.ts +126 -0
- package/src/engine/index.ts +10 -0
- package/src/engine/quantum.ts +207 -0
- package/src/engine/security.ts +217 -0
- package/src/engine/tkx.ts +874 -0
- package/src/engine/wcag.ts +275 -0
- package/src/hooks/index.ts +120 -0
- package/src/styles/global.css +167 -0
- package/src/themes/index.ts +111 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export * from './TkxButton';
|
|
2
|
+
export * from './TkxCard';
|
|
3
|
+
export * from './TkxInput';
|
|
4
|
+
export * from './TkxBadge';
|
|
5
|
+
export * from './TkxProgress';
|
|
6
|
+
export * from './TkxToggle';
|
|
7
|
+
export * from './TkxAlert';
|
|
8
|
+
export * from './TkxModal';
|
|
9
|
+
export * from './TkxTabs';
|
|
10
|
+
export * from './TkxTooltip';
|
|
11
|
+
export * from './TkxSkeleton';
|
|
12
|
+
export * from './TkxAvatar';
|
|
13
|
+
export * from './TkxTable';
|
|
14
|
+
export * from './TkxDivider';
|
|
15
|
+
export * from './TkxIcon';
|
|
16
|
+
export * from './TkxLogo';
|
|
17
|
+
export * from './TkxSelect';
|
|
18
|
+
export * from './TkxCheckbox';
|
|
19
|
+
export * from './TkxRadio';
|
|
20
|
+
export * from './TkxToast';
|
|
21
|
+
export * from './TkxAccordion';
|
|
22
|
+
export * from './TkxDrawer';
|
|
23
|
+
export * from './TkxChat';
|
|
24
|
+
export * from './TkxTag';
|
|
25
|
+
export * from './TkxTimeline';
|
|
26
|
+
export * from './TkxDatePicker';
|
|
27
|
+
export * from './TkxSlider';
|
|
28
|
+
export * from './TkxPagination';
|
|
29
|
+
export * from './TkxImage';
|
|
30
|
+
export * from './TkxFileUpload';
|
|
31
|
+
export * from './TkxRating';
|
|
32
|
+
export * from './TkxMenu';
|
|
33
|
+
export * from './TkxClock';
|
|
34
|
+
export * from './TkxVideoPlayer';
|
|
35
|
+
export * from './TkxStepper';
|
|
36
|
+
export * from './TkxColorPicker';
|
|
37
|
+
export * from './TkxNumberInput';
|
|
38
|
+
export * from './TkxOTP';
|
|
39
|
+
export * from './TkxCommand';
|
|
40
|
+
export * from './TkxCarousel';
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ── TKX-CSS Engine ───────────────────────────────────────────────────────────
|
|
2
|
+
// Content-hashed class names, responsive helpers, keyframes, CSS variables, SSR
|
|
3
|
+
|
|
4
|
+
import { fnv1aHash } from './quantum';
|
|
5
|
+
|
|
6
|
+
// ── Internal style sheet registry ────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const styleSheet = new Map<string, string>();
|
|
9
|
+
|
|
10
|
+
function kebabCase(str: string): string {
|
|
11
|
+
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function generateClassName(styles: string): string {
|
|
15
|
+
return 'tkx-' + fnv1aHash(styles);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function register(styles: string): string {
|
|
19
|
+
const className = generateClassName(styles);
|
|
20
|
+
if (!styleSheet.has(className)) {
|
|
21
|
+
styleSheet.set(className, styles);
|
|
22
|
+
}
|
|
23
|
+
return className;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Template Literal CSS ─────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface ProcessedCSS {
|
|
29
|
+
className: string;
|
|
30
|
+
styles: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function css(strings: TemplateStringsArray, ...values: unknown[]): ProcessedCSS {
|
|
34
|
+
const styles = strings.reduce<string>((acc, str, i) => {
|
|
35
|
+
return acc + str + (values[i] !== undefined ? String(values[i]) : '');
|
|
36
|
+
}, '');
|
|
37
|
+
const className = register(styles.trim());
|
|
38
|
+
return { className, styles: styles.trim() };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── From Style Object ────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export function fromObject(styleObject: Record<string, unknown>): ProcessedCSS {
|
|
44
|
+
const styles = Object.entries(styleObject)
|
|
45
|
+
.map(([key, value]) => `${kebabCase(key)}: ${value};`)
|
|
46
|
+
.join(' ');
|
|
47
|
+
const className = register(styles);
|
|
48
|
+
return { className, styles };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Responsive Breakpoints ───────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
54
|
+
|
|
55
|
+
const BREAKPOINTS: Record<Breakpoint, string> = {
|
|
56
|
+
xs: '480px',
|
|
57
|
+
sm: '640px',
|
|
58
|
+
md: '768px',
|
|
59
|
+
lg: '1024px',
|
|
60
|
+
xl: '1280px',
|
|
61
|
+
'2xl': '1536px',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function responsive(breakpoint: Breakpoint, styles: string): string {
|
|
65
|
+
return `@media (min-width: ${BREAKPOINTS[breakpoint]}) { ${styles} }`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Keyframes ────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export function keyframes(
|
|
71
|
+
name: string,
|
|
72
|
+
frames: Record<string, Record<string, unknown>>,
|
|
73
|
+
): string {
|
|
74
|
+
const stops = Object.entries(frames)
|
|
75
|
+
.map(([stop, styles]) => {
|
|
76
|
+
const declarations = Object.entries(styles)
|
|
77
|
+
.map(([k, v]) => `${kebabCase(k)}: ${v};`)
|
|
78
|
+
.join(' ');
|
|
79
|
+
return `${stop} { ${declarations} }`;
|
|
80
|
+
})
|
|
81
|
+
.join(' ');
|
|
82
|
+
return `@keyframes ${name} { ${stops} }`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── CSS Variables ────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export function cssVar(name: string, value?: string): string {
|
|
88
|
+
if (value !== undefined) {
|
|
89
|
+
return `--tkx-${name}: ${value};`;
|
|
90
|
+
}
|
|
91
|
+
return `var(--tkx-${name})`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── SSR Extraction and Client Injection ──────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
export function extractCSS(): string {
|
|
97
|
+
return Array.from(styleSheet.entries())
|
|
98
|
+
.map(([className, styles]) => `.${className} { ${styles} }`)
|
|
99
|
+
.join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function resetStyles(): void {
|
|
103
|
+
styleSheet.clear();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function injectStyles(): void {
|
|
107
|
+
if (typeof document === 'undefined') return;
|
|
108
|
+
let styleEl = document.getElementById('tkx-styles') as HTMLStyleElement | null;
|
|
109
|
+
if (!styleEl) {
|
|
110
|
+
styleEl = document.createElement('style');
|
|
111
|
+
styleEl.id = 'tkx-styles';
|
|
112
|
+
document.head.appendChild(styleEl);
|
|
113
|
+
}
|
|
114
|
+
styleEl.textContent = extractCSS();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const TKX = {
|
|
118
|
+
css,
|
|
119
|
+
fromObject,
|
|
120
|
+
responsive,
|
|
121
|
+
keyframes,
|
|
122
|
+
cssVar,
|
|
123
|
+
extractCSS,
|
|
124
|
+
injectStyles,
|
|
125
|
+
resetStyles,
|
|
126
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './quantum';
|
|
2
|
+
export * from './security';
|
|
3
|
+
export * from './wcag';
|
|
4
|
+
export * from './css';
|
|
5
|
+
export * from './tkx';
|
|
6
|
+
|
|
7
|
+
export { Quantum } from './quantum';
|
|
8
|
+
export { Shield } from './security';
|
|
9
|
+
export { WCAGEngine } from './wcag';
|
|
10
|
+
export { TKX } from './css';
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// ── Quantum Render Engine ────────────────────────────────────────────────────
|
|
2
|
+
// FNV-1a hashing, LRU memoization, priority render queues, microtask batching
|
|
3
|
+
|
|
4
|
+
// ── FNV-1a Hash ─────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const FNV_OFFSET = 2166136261;
|
|
7
|
+
const FNV_PRIME = 16777619;
|
|
8
|
+
|
|
9
|
+
export function fnv1aHash(input: string): string {
|
|
10
|
+
let hash = FNV_OFFSET;
|
|
11
|
+
for (let i = 0; i < input.length; i++) {
|
|
12
|
+
hash ^= input.charCodeAt(i);
|
|
13
|
+
hash = (Math.imul(hash, FNV_PRIME)) >>> 0;
|
|
14
|
+
}
|
|
15
|
+
return hash.toString(16).padStart(8, '0');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ── LRU Cache ────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export class LRUCache<K, V> {
|
|
21
|
+
private cache = new Map<K, V>();
|
|
22
|
+
private hits = 0;
|
|
23
|
+
private misses = 0;
|
|
24
|
+
readonly ceiling: number;
|
|
25
|
+
|
|
26
|
+
constructor(ceiling = 512) {
|
|
27
|
+
this.ceiling = Math.min(ceiling, 512);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get(key: K): V | undefined {
|
|
31
|
+
if (!this.cache.has(key)) {
|
|
32
|
+
this.misses++;
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
this.hits++;
|
|
36
|
+
const value = this.cache.get(key)!;
|
|
37
|
+
// Move to end (most-recently-used)
|
|
38
|
+
this.cache.delete(key);
|
|
39
|
+
this.cache.set(key, value);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
set(key: K, value: V): void {
|
|
44
|
+
if (this.cache.has(key)) {
|
|
45
|
+
this.cache.delete(key);
|
|
46
|
+
} else if (this.cache.size >= this.ceiling) {
|
|
47
|
+
// Evict least-recently-used (first in insertion order)
|
|
48
|
+
this.cache.delete(this.cache.keys().next().value!);
|
|
49
|
+
}
|
|
50
|
+
this.cache.set(key, value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
has(key: K): boolean {
|
|
54
|
+
return this.cache.has(key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
invalidate(key?: K): void {
|
|
58
|
+
if (key !== undefined) {
|
|
59
|
+
this.cache.delete(key);
|
|
60
|
+
} else {
|
|
61
|
+
this.cache.clear();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get size(): number {
|
|
66
|
+
return this.cache.size;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get cacheHitRate(): number {
|
|
70
|
+
const total = this.hits + this.misses;
|
|
71
|
+
return total === 0 ? 0 : (this.hits / total) * 100;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Memoization via global LRU cache ────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const globalCache = new LRUCache<string, unknown>(512);
|
|
78
|
+
|
|
79
|
+
export function memoize<T>(key: string, fn: () => T): T {
|
|
80
|
+
const cached = globalCache.get(key);
|
|
81
|
+
if (cached !== undefined) return cached as T;
|
|
82
|
+
const result = fn();
|
|
83
|
+
globalCache.set(key, result);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Priority Render Queue ────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
export type Priority = 'critical' | 'normal' | 'low';
|
|
90
|
+
|
|
91
|
+
const PRIORITY_WEIGHTS: Record<Priority, number> = {
|
|
92
|
+
critical: 3,
|
|
93
|
+
normal: 2,
|
|
94
|
+
low: 1,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
interface QueueEntry {
|
|
98
|
+
id: string;
|
|
99
|
+
priority: Priority;
|
|
100
|
+
task: () => void;
|
|
101
|
+
weight: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class RenderQueue {
|
|
105
|
+
private queue: QueueEntry[] = [];
|
|
106
|
+
|
|
107
|
+
enqueue(id: string, task: () => void, priority: Priority = 'normal'): void {
|
|
108
|
+
this.queue.push({ id, task, priority, weight: PRIORITY_WEIGHTS[priority] });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
flush(): void {
|
|
112
|
+
this.queue.sort((a, b) => b.weight - a.weight);
|
|
113
|
+
for (const entry of this.queue) {
|
|
114
|
+
try {
|
|
115
|
+
entry.task();
|
|
116
|
+
} catch {
|
|
117
|
+
// Swallow per-task errors so the queue continues
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.queue = [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get depth(): number {
|
|
124
|
+
return this.queue.length;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Microtask Batch Updates ──────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
interface RenderMetric {
|
|
131
|
+
duration: number;
|
|
132
|
+
timestamp: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pendingBatch: Array<() => void> = [];
|
|
136
|
+
let scheduledFlush = false;
|
|
137
|
+
const renderMetrics: RenderMetric[] = [];
|
|
138
|
+
let totalRenderTime = 0;
|
|
139
|
+
let renderCount = 0;
|
|
140
|
+
|
|
141
|
+
function flushBatch(): void {
|
|
142
|
+
const tasks = pendingBatch.splice(0);
|
|
143
|
+
const start = performance.now();
|
|
144
|
+
for (const task of tasks) {
|
|
145
|
+
try {
|
|
146
|
+
task();
|
|
147
|
+
} catch {
|
|
148
|
+
// Continue queue on error
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const duration = performance.now() - start;
|
|
152
|
+
totalRenderTime += duration;
|
|
153
|
+
renderCount++;
|
|
154
|
+
renderMetrics.push({ duration, timestamp: Date.now() });
|
|
155
|
+
if (renderMetrics.length > 100) renderMetrics.shift();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function batchUpdate(fn: () => void): void {
|
|
159
|
+
pendingBatch.push(fn);
|
|
160
|
+
if (!scheduledFlush) {
|
|
161
|
+
scheduledFlush = true;
|
|
162
|
+
queueMicrotask(() => {
|
|
163
|
+
flushBatch();
|
|
164
|
+
scheduledFlush = false;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Profiling API ────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export interface Metrics {
|
|
172
|
+
avgRenderTime: number;
|
|
173
|
+
cacheHitRate: number;
|
|
174
|
+
renderCount: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getMetrics(n?: number): Metrics & { recent: RenderMetric[] } {
|
|
178
|
+
const recent = n !== undefined ? renderMetrics.slice(-n) : renderMetrics.slice();
|
|
179
|
+
return {
|
|
180
|
+
avgRenderTime: renderCount === 0 ? 0 : totalRenderTime / renderCount,
|
|
181
|
+
cacheHitRate: globalCache.cacheHitRate,
|
|
182
|
+
renderCount,
|
|
183
|
+
recent,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function invalidateCache(key?: string): void {
|
|
188
|
+
if (key !== undefined) {
|
|
189
|
+
globalCache.invalidate(key);
|
|
190
|
+
} else {
|
|
191
|
+
globalCache.invalidate();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const Quantum = {
|
|
196
|
+
hash: fnv1aHash,
|
|
197
|
+
memoize,
|
|
198
|
+
batchUpdate,
|
|
199
|
+
invalidate: invalidateCache,
|
|
200
|
+
get avgRenderTime() {
|
|
201
|
+
return renderCount === 0 ? 0 : totalRenderTime / renderCount;
|
|
202
|
+
},
|
|
203
|
+
get cacheHitRate() {
|
|
204
|
+
return globalCache.cacheHitRate;
|
|
205
|
+
},
|
|
206
|
+
getMetrics,
|
|
207
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// ── Security Shield ──────────────────────────────────────────────────────────
|
|
2
|
+
// XSS prevention, CSP, prop validation, immutable audit trail
|
|
3
|
+
|
|
4
|
+
import { fnv1aHash } from './quantum';
|
|
5
|
+
|
|
6
|
+
// ── XSS Sanitization ─────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const HTML_ENTITIES: Record<string, string> = {
|
|
9
|
+
'<': '<',
|
|
10
|
+
'>': '>',
|
|
11
|
+
'&': '&',
|
|
12
|
+
"'": ''',
|
|
13
|
+
'"': '"',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function sanitizeString(input: unknown): string {
|
|
17
|
+
return String(input).replace(/[<>&'"]/g, (char) => HTML_ENTITIES[char] ?? char);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sanitizeProps<T extends Record<string, unknown>>(props: T): T {
|
|
21
|
+
const result: Record<string, unknown> = {};
|
|
22
|
+
for (const key of Object.keys(props)) {
|
|
23
|
+
result[key] = sanitizeValue(props[key]);
|
|
24
|
+
}
|
|
25
|
+
return result as T;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sanitizeValue(value: unknown): unknown {
|
|
29
|
+
if (typeof value === 'string') return sanitizeString(value);
|
|
30
|
+
if (Array.isArray(value)) return value.map(sanitizeValue);
|
|
31
|
+
if (value !== null && typeof value === 'object' && !isReactElement(value)) {
|
|
32
|
+
return sanitizeProps(value as Record<string, unknown>);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isReactElement(value: unknown): boolean {
|
|
38
|
+
return typeof value === 'object' && value !== null && '$$typeof' in value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Runtime Prop Validation ──────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export interface PropSchema {
|
|
44
|
+
type: 'string' | 'number' | 'boolean' | 'function' | 'object';
|
|
45
|
+
required?: boolean;
|
|
46
|
+
min?: number;
|
|
47
|
+
max?: number;
|
|
48
|
+
pattern?: RegExp;
|
|
49
|
+
enum?: unknown[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ValidationResult {
|
|
53
|
+
valid: boolean;
|
|
54
|
+
errors: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function validateProps(
|
|
58
|
+
props: Record<string, unknown>,
|
|
59
|
+
schema: Record<string, PropSchema>,
|
|
60
|
+
): ValidationResult {
|
|
61
|
+
const errors: string[] = [];
|
|
62
|
+
|
|
63
|
+
for (const [key, rule] of Object.entries(schema)) {
|
|
64
|
+
const value = props[key];
|
|
65
|
+
const missing = value === undefined || value === null;
|
|
66
|
+
|
|
67
|
+
if (rule.required && missing) {
|
|
68
|
+
errors.push(`prop '${key}' is required`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (missing) continue;
|
|
73
|
+
|
|
74
|
+
if (typeof value !== rule.type) {
|
|
75
|
+
errors.push(`prop '${key}' must be of type ${rule.type}, got ${typeof value}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (rule.type === 'number' && typeof value === 'number') {
|
|
80
|
+
if (rule.min !== undefined && value < rule.min) {
|
|
81
|
+
errors.push(`prop '${key}' must be >= ${rule.min}`);
|
|
82
|
+
}
|
|
83
|
+
if (rule.max !== undefined && value > rule.max) {
|
|
84
|
+
errors.push(`prop '${key}' must be <= ${rule.max}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (rule.type === 'string' && typeof value === 'string') {
|
|
89
|
+
if (rule.pattern && !rule.pattern.test(value)) {
|
|
90
|
+
errors.push(`prop '${key}' does not match pattern ${rule.pattern}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (rule.enum && !rule.enum.includes(value)) {
|
|
95
|
+
errors.push(`prop '${key}' must be one of: ${rule.enum.join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { valid: errors.length === 0, errors };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── CSP Permission Model ─────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
export interface ComponentPermissions {
|
|
105
|
+
allowInlineStyles?: boolean;
|
|
106
|
+
allowDataUrls?: boolean;
|
|
107
|
+
allowExternalSrc?: boolean;
|
|
108
|
+
allowScripts?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface CSPDirectives {
|
|
112
|
+
componentId: string;
|
|
113
|
+
permissions: ComponentPermissions;
|
|
114
|
+
directives: Record<string, string[]>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cspRegistry = new Map<string, CSPDirectives>();
|
|
118
|
+
|
|
119
|
+
export function createCSP(componentId: string, permissions: ComponentPermissions): CSPDirectives {
|
|
120
|
+
const directives: Record<string, string[]> = {
|
|
121
|
+
'default-src': ["'self'"],
|
|
122
|
+
'style-src': permissions.allowInlineStyles ? ["'self'", "'unsafe-inline'"] : ["'self'"],
|
|
123
|
+
'img-src': permissions.allowDataUrls
|
|
124
|
+
? ["'self'", 'data:']
|
|
125
|
+
: permissions.allowExternalSrc
|
|
126
|
+
? ["'self'", '*']
|
|
127
|
+
: ["'self'"],
|
|
128
|
+
'script-src': permissions.allowScripts ? ["'self'"] : ["'none'"],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const csp: CSPDirectives = { componentId, permissions, directives };
|
|
132
|
+
cspRegistry.set(componentId, csp);
|
|
133
|
+
return csp;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function hasPermission(componentId: string, permission: keyof ComponentPermissions): boolean {
|
|
137
|
+
const csp = cspRegistry.get(componentId);
|
|
138
|
+
return csp?.permissions[permission] === true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Immutable Audit Trail ────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
export interface AuditEntry {
|
|
144
|
+
readonly timestamp: number;
|
|
145
|
+
readonly component: string;
|
|
146
|
+
readonly action: string;
|
|
147
|
+
readonly propsHash: string;
|
|
148
|
+
readonly chainHash: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface AuditFilter {
|
|
152
|
+
component?: string;
|
|
153
|
+
action?: string;
|
|
154
|
+
limit?: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let auditTrail: readonly AuditEntry[] = [];
|
|
158
|
+
|
|
159
|
+
export function audit(
|
|
160
|
+
action: string,
|
|
161
|
+
component: string,
|
|
162
|
+
meta?: Record<string, unknown>,
|
|
163
|
+
): AuditEntry {
|
|
164
|
+
const propsHash = fnv1aHash(JSON.stringify(meta ?? {}));
|
|
165
|
+
const prevChainHash = auditTrail.length > 0 ? auditTrail[auditTrail.length - 1].chainHash : '00000000';
|
|
166
|
+
const chainHash = fnv1aHash(prevChainHash + propsHash + component + action);
|
|
167
|
+
|
|
168
|
+
const entry: AuditEntry = Object.freeze({
|
|
169
|
+
timestamp: Date.now(),
|
|
170
|
+
component,
|
|
171
|
+
action,
|
|
172
|
+
propsHash,
|
|
173
|
+
chainHash,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
auditTrail = Object.freeze([...auditTrail, entry]);
|
|
177
|
+
return entry;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function getAuditLog(filter?: AuditFilter): readonly AuditEntry[] {
|
|
181
|
+
let result = auditTrail;
|
|
182
|
+
|
|
183
|
+
if (filter?.component) {
|
|
184
|
+
result = result.filter((e) => e.component === filter.component);
|
|
185
|
+
}
|
|
186
|
+
if (filter?.action) {
|
|
187
|
+
result = result.filter((e) => e.action === filter.action);
|
|
188
|
+
}
|
|
189
|
+
if (filter?.limit) {
|
|
190
|
+
result = result.slice(-filter.limit);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function verifyAuditIntegrity(): boolean {
|
|
197
|
+
let prevChainHash = '00000000';
|
|
198
|
+
|
|
199
|
+
for (const entry of auditTrail) {
|
|
200
|
+
const expectedChain = fnv1aHash(prevChainHash + entry.propsHash + entry.component + entry.action);
|
|
201
|
+
if (expectedChain !== entry.chainHash) return false;
|
|
202
|
+
prevChainHash = entry.chainHash;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const Shield = {
|
|
209
|
+
sanitize: sanitizeString,
|
|
210
|
+
sanitizeProps,
|
|
211
|
+
validateProps,
|
|
212
|
+
createCSP,
|
|
213
|
+
hasPermission,
|
|
214
|
+
audit,
|
|
215
|
+
getAuditLog,
|
|
216
|
+
verifyAuditIntegrity,
|
|
217
|
+
};
|