pulse-js-framework 1.7.32 → 1.7.37
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/cli/index.js +0 -0
- package/cli/release.js +159 -22
- package/compiler/parser.js +96 -10
- package/compiler/transformer/constants.js +1 -1
- package/compiler/transformer/expressions.js +69 -9
- package/loader/README.md +509 -0
- package/loader/esbuild-plugin.js +251 -0
- package/loader/parcel-plugin.js +216 -0
- package/loader/rollup-plugin.js +259 -0
- package/loader/swc-plugin.js +286 -0
- package/loader/vite-plugin.js +1 -1
- package/loader/webpack-loader.js +228 -0
- package/package.json +15 -2
- package/runtime/security.js +21 -0
- package/runtime/ssr.js +17 -2
- package/types/a11y.d.ts +186 -0
- package/types/devtools.d.ts +418 -0
- package/types/dom-adapter.d.ts +643 -0
- package/types/dom.d.ts +63 -0
- package/types/errors.d.ts +618 -0
- package/types/http.d.ts +426 -0
- package/types/logger.d.ts +12 -0
- package/types/native.d.ts +282 -0
- package/types/pulse.d.ts +70 -1
- package/types/security.d.ts +286 -0
- package/types/ssr.d.ts +263 -0
- package/types/utils.d.ts +85 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Webpack Loader
|
|
3
|
+
*
|
|
4
|
+
* Enables .pulse file support in Webpack projects
|
|
5
|
+
* Extracts CSS and compiles to JavaScript with source maps
|
|
6
|
+
*
|
|
7
|
+
* CSS Preprocessor Support:
|
|
8
|
+
* - If `sass`, `less`, or `stylus` is installed in the user's project,
|
|
9
|
+
* preprocessor syntax in style blocks is automatically compiled
|
|
10
|
+
* - No configuration needed - just install the preprocessor package
|
|
11
|
+
*
|
|
12
|
+
* Usage in webpack.config.js:
|
|
13
|
+
* ```js
|
|
14
|
+
* module.exports = {
|
|
15
|
+
* module: {
|
|
16
|
+
* rules: [
|
|
17
|
+
* {
|
|
18
|
+
* test: /\.pulse$/,
|
|
19
|
+
* use: [
|
|
20
|
+
* 'style-loader', // or mini-css-extract-plugin
|
|
21
|
+
* 'css-loader',
|
|
22
|
+
* 'pulse-js-framework/loader/webpack-loader'
|
|
23
|
+
* ]
|
|
24
|
+
* }
|
|
25
|
+
* ]
|
|
26
|
+
* }
|
|
27
|
+
* };
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { compile } from '../compiler/index.js';
|
|
32
|
+
import {
|
|
33
|
+
preprocessStylesSync,
|
|
34
|
+
isSassAvailable,
|
|
35
|
+
isLessAvailable,
|
|
36
|
+
isStylusAvailable,
|
|
37
|
+
getSassVersion,
|
|
38
|
+
getLessVersion,
|
|
39
|
+
getStylusVersion,
|
|
40
|
+
detectPreprocessor
|
|
41
|
+
} from '../compiler/preprocessor.js';
|
|
42
|
+
import { dirname } from 'path';
|
|
43
|
+
|
|
44
|
+
// Cache for preprocessor availability checks
|
|
45
|
+
let preprocessorCache = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check available preprocessors once
|
|
49
|
+
*/
|
|
50
|
+
function checkPreprocessors() {
|
|
51
|
+
if (preprocessorCache) return preprocessorCache;
|
|
52
|
+
|
|
53
|
+
preprocessorCache = {
|
|
54
|
+
sass: isSassAvailable(),
|
|
55
|
+
less: isLessAvailable(),
|
|
56
|
+
stylus: isStylusAvailable()
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return preprocessorCache;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Webpack loader for .pulse files
|
|
64
|
+
* @this {import('webpack').LoaderContext}
|
|
65
|
+
*/
|
|
66
|
+
export default function pulseLoader(source) {
|
|
67
|
+
const callback = this.async();
|
|
68
|
+
const options = this.getOptions() || {};
|
|
69
|
+
|
|
70
|
+
const {
|
|
71
|
+
sourceMap = true,
|
|
72
|
+
sass: sassOptions = {},
|
|
73
|
+
less: lessOptions = {},
|
|
74
|
+
stylus: stylusOptions = {}
|
|
75
|
+
} = options;
|
|
76
|
+
|
|
77
|
+
// Mark as cacheable
|
|
78
|
+
this.cacheable?.();
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Compile .pulse to JavaScript
|
|
82
|
+
const result = compile(source, {
|
|
83
|
+
runtime: 'pulse-js-framework/runtime',
|
|
84
|
+
sourceMap,
|
|
85
|
+
filename: this.resourcePath
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
const errors = result.errors.map(e =>
|
|
90
|
+
`${e.message}${e.line ? ` at line ${e.line}` : ''}`
|
|
91
|
+
).join('\n');
|
|
92
|
+
|
|
93
|
+
callback(new Error(`Pulse compilation failed:\n${errors}`));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let outputCode = result.code;
|
|
98
|
+
let outputMap = result.map;
|
|
99
|
+
|
|
100
|
+
// Extract CSS from compiled output
|
|
101
|
+
const stylesMatch = outputCode.match(/const styles = `([\s\S]*?)`;/);
|
|
102
|
+
|
|
103
|
+
if (stylesMatch) {
|
|
104
|
+
let css = stylesMatch[1];
|
|
105
|
+
|
|
106
|
+
// Check available preprocessors
|
|
107
|
+
const available = checkPreprocessors();
|
|
108
|
+
const preprocessor = detectPreprocessor(css);
|
|
109
|
+
|
|
110
|
+
// Preprocess if preprocessor detected and available
|
|
111
|
+
if (preprocessor !== 'none' && available[preprocessor]) {
|
|
112
|
+
try {
|
|
113
|
+
const preprocessorOptions = {
|
|
114
|
+
sass: sassOptions,
|
|
115
|
+
less: lessOptions,
|
|
116
|
+
stylus: stylusOptions
|
|
117
|
+
}[preprocessor];
|
|
118
|
+
|
|
119
|
+
const preprocessed = preprocessStylesSync(css, {
|
|
120
|
+
filename: this.resourcePath,
|
|
121
|
+
loadPaths: [dirname(this.resourcePath), ...(preprocessorOptions.loadPaths || [])],
|
|
122
|
+
compressed: preprocessorOptions.compressed || false,
|
|
123
|
+
preprocessor // Force detected preprocessor
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
css = preprocessed.css;
|
|
127
|
+
|
|
128
|
+
// Log preprocessor usage in verbose mode
|
|
129
|
+
if (preprocessorOptions.verbose) {
|
|
130
|
+
console.log(`[Pulse] Compiled ${preprocessor.toUpperCase()} in ${this.resourcePath}`);
|
|
131
|
+
}
|
|
132
|
+
} catch (preprocessorError) {
|
|
133
|
+
// Emit warning but continue with original CSS
|
|
134
|
+
this.emitWarning(
|
|
135
|
+
new Error(`${preprocessor.toUpperCase()} compilation warning: ${preprocessorError.message}`)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Emit CSS as separate file or inline
|
|
141
|
+
if (options.extractCss !== false) {
|
|
142
|
+
// Default: emit CSS for css-loader to process
|
|
143
|
+
// This allows Webpack's CSS pipeline to handle it
|
|
144
|
+
this.emitFile?.(
|
|
145
|
+
this.resourcePath.replace(/\.pulse$/, '.pulse.css'),
|
|
146
|
+
css
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Replace inline CSS injection with import statement
|
|
150
|
+
// css-loader will process the CSS and style-loader will inject it
|
|
151
|
+
outputCode = outputCode.replace(
|
|
152
|
+
/\/\/ Styles\nconst styles = `[\s\S]*?`;\n\/\/ Inject styles\nconst styleEl = document\.createElement\("style"\);\nstyleEl\.textContent = styles;\ndocument\.head\.appendChild\(styleEl\);/,
|
|
153
|
+
`// Styles extracted - handled by css-loader\nimport "./${this.resourcePath.split('/').pop().replace(/\.pulse$/, '.pulse.css')}";`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
// else: keep inline CSS injection (useful for development)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Add HMR support if Webpack HMR is enabled
|
|
160
|
+
if (this.hot && options.hmr !== false) {
|
|
161
|
+
outputCode += `\n${generateHMRCode(this.resourcePath)}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Only pass source map if enabled in options
|
|
165
|
+
callback(null, outputCode, sourceMap ? outputMap : null);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
callback(new Error(`Pulse loader error: ${error.message}`));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Generate HMR (Hot Module Replacement) code for Webpack
|
|
173
|
+
*/
|
|
174
|
+
function generateHMRCode(resourcePath) {
|
|
175
|
+
return `
|
|
176
|
+
// Webpack HMR
|
|
177
|
+
if (module.hot) {
|
|
178
|
+
module.hot.accept();
|
|
179
|
+
|
|
180
|
+
// Cleanup on module replacement
|
|
181
|
+
module.hot.dispose((data) => {
|
|
182
|
+
// Store state for preservation
|
|
183
|
+
if (typeof __PULSE_HMR_STATE__ !== 'undefined') {
|
|
184
|
+
data.pulseState = __PULSE_HMR_STATE__;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Restore state after replacement
|
|
189
|
+
if (module.hot.data && module.hot.data.pulseState) {
|
|
190
|
+
if (typeof __PULSE_HMR_RESTORE__ !== 'undefined') {
|
|
191
|
+
__PULSE_HMR_RESTORE__(module.hot.data.pulseState);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Pitch loader - runs before other loaders
|
|
200
|
+
* Used to log preprocessor availability
|
|
201
|
+
*/
|
|
202
|
+
export function pitch() {
|
|
203
|
+
const available = checkPreprocessors();
|
|
204
|
+
const options = this.getOptions() || {};
|
|
205
|
+
|
|
206
|
+
// Log preprocessor availability once on first run
|
|
207
|
+
if (!pulseLoader._logged && options.verbose !== false) {
|
|
208
|
+
const preprocessors = [];
|
|
209
|
+
if (available.sass) {
|
|
210
|
+
preprocessors.push(`SASS ${getSassVersion() || 'unknown'}`);
|
|
211
|
+
}
|
|
212
|
+
if (available.less) {
|
|
213
|
+
preprocessors.push(`LESS ${getLessVersion() || 'unknown'}`);
|
|
214
|
+
}
|
|
215
|
+
if (available.stylus) {
|
|
216
|
+
preprocessors.push(`Stylus ${getStylusVersion() || 'unknown'}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (preprocessors.length > 0) {
|
|
220
|
+
console.log(`[Pulse Webpack] Preprocessor support: ${preprocessors.join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
pulseLoader._logged = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Export for CommonJS compatibility
|
|
228
|
+
export const raw = false; // Return code as string, not Buffer
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-js-framework",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.37",
|
|
4
4
|
"description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"main": "index.js",
|
|
7
8
|
"types": "types/index.d.ts",
|
|
8
9
|
"bin": {
|
|
@@ -94,6 +95,11 @@
|
|
|
94
95
|
"types": "./types/index.d.ts",
|
|
95
96
|
"default": "./loader/vite-plugin.js"
|
|
96
97
|
},
|
|
98
|
+
"./webpack": "./loader/webpack-loader.js",
|
|
99
|
+
"./rollup": "./loader/rollup-plugin.js",
|
|
100
|
+
"./esbuild": "./loader/esbuild-plugin.js",
|
|
101
|
+
"./parcel": "./loader/parcel-plugin.js",
|
|
102
|
+
"./swc": "./loader/swc-plugin.js",
|
|
97
103
|
"./mobile": "./mobile/bridge/pulse-native.js",
|
|
98
104
|
"./package.json": "./package.json"
|
|
99
105
|
},
|
|
@@ -110,7 +116,7 @@
|
|
|
110
116
|
"LICENSE"
|
|
111
117
|
],
|
|
112
118
|
"scripts": {
|
|
113
|
-
"test": "
|
|
119
|
+
"test": "node scripts/run-all-tests.js",
|
|
114
120
|
"test:compiler": "node test/compiler.test.js",
|
|
115
121
|
"test:sourcemap": "node test/sourcemap.test.js",
|
|
116
122
|
"test:css-parsing": "node test/css-parsing.test.js",
|
|
@@ -168,6 +174,13 @@
|
|
|
168
174
|
"test:websocket-stress": "node test/websocket-stress.test.js",
|
|
169
175
|
"test:ssr": "node test/ssr.test.js",
|
|
170
176
|
"test:ssr-hydrator": "node test/ssr-hydrator.test.js",
|
|
177
|
+
"test:webpack-loader": "node test/webpack-loader.test.js",
|
|
178
|
+
"test:rollup-plugin": "node test/rollup-plugin.test.js",
|
|
179
|
+
"test:esbuild-plugin": "node test/esbuild-plugin.test.js",
|
|
180
|
+
"test:parcel-plugin": "node test/parcel-plugin.test.js",
|
|
181
|
+
"test:swc-plugin": "node test/swc-plugin.test.js",
|
|
182
|
+
"test:dom-binding": "node test/dom-binding.test.js",
|
|
183
|
+
"test:interceptor-manager": "node test/interceptor-manager.test.js",
|
|
171
184
|
"build:netlify": "node scripts/build-netlify.js",
|
|
172
185
|
"version": "node scripts/sync-version.js",
|
|
173
186
|
"docs": "node cli/index.js dev docs"
|
package/runtime/security.js
CHANGED
|
@@ -321,6 +321,27 @@ export function sanitizeHtml(html, options = {}) {
|
|
|
321
321
|
attrValue = sanitized;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
// Sanitize style attribute to prevent CSS injection
|
|
325
|
+
if (attrName === 'style') {
|
|
326
|
+
const parts = attrValue.split(';').filter(Boolean);
|
|
327
|
+
const safeParts = [];
|
|
328
|
+
for (const part of parts) {
|
|
329
|
+
const colonIndex = part.indexOf(':');
|
|
330
|
+
if (colonIndex === -1) continue;
|
|
331
|
+
const cssProp = part.slice(0, colonIndex).trim();
|
|
332
|
+
const cssVal = part.slice(colonIndex + 1).trim();
|
|
333
|
+
if (/url\s*\(/i.test(cssVal) || /expression\s*\(/i.test(cssVal) ||
|
|
334
|
+
/javascript:/i.test(cssVal) || /behavior\s*:/i.test(cssVal) ||
|
|
335
|
+
/-moz-binding/i.test(cssVal)) {
|
|
336
|
+
log.warn(`Blocked dangerous CSS in style attribute: ${cssProp}`);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
safeParts.push(`${cssProp}: ${cssVal}`);
|
|
340
|
+
}
|
|
341
|
+
attrValue = safeParts.join('; ');
|
|
342
|
+
if (!attrValue) continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
324
345
|
result += ` ${attrName}="${escapeHtml(attrValue)}"`;
|
|
325
346
|
}
|
|
326
347
|
|
package/runtime/ssr.js
CHANGED
|
@@ -408,12 +408,16 @@ export function deserializeState(data) {
|
|
|
408
408
|
* // Default implementation just stores in global
|
|
409
409
|
* restoreState(window.__PULSE_STATE__);
|
|
410
410
|
*/
|
|
411
|
+
// Module-scoped state store (avoids globalThis collision between multiple SSR instances)
|
|
412
|
+
let _ssrState = null;
|
|
413
|
+
|
|
411
414
|
export function restoreState(state) {
|
|
412
415
|
const deserialized = typeof state === 'string'
|
|
413
416
|
? deserializeState(state)
|
|
414
417
|
: state;
|
|
415
418
|
|
|
416
|
-
// Store in global for
|
|
419
|
+
// Store in module scope and global for backward compatibility
|
|
420
|
+
_ssrState = deserialized;
|
|
417
421
|
if (typeof globalThis !== 'undefined') {
|
|
418
422
|
globalThis.__PULSE_SSR_STATE__ = deserialized;
|
|
419
423
|
}
|
|
@@ -430,10 +434,20 @@ export function restoreState(state) {
|
|
|
430
434
|
* const userData = getSSRState('user');
|
|
431
435
|
*/
|
|
432
436
|
export function getSSRState(key) {
|
|
433
|
-
const state = globalThis?.__PULSE_SSR_STATE__ || {};
|
|
437
|
+
const state = _ssrState || globalThis?.__PULSE_SSR_STATE__ || {};
|
|
434
438
|
return key ? state[key] : state;
|
|
435
439
|
}
|
|
436
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Clear the SSR state. Use in tests or when cleaning up SSR context.
|
|
443
|
+
*/
|
|
444
|
+
export function clearSSRState() {
|
|
445
|
+
_ssrState = null;
|
|
446
|
+
if (typeof globalThis !== 'undefined') {
|
|
447
|
+
delete globalThis.__PULSE_SSR_STATE__;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
437
451
|
// ============================================================================
|
|
438
452
|
// Re-exports for convenience
|
|
439
453
|
// ============================================================================
|
|
@@ -456,6 +470,7 @@ export default {
|
|
|
456
470
|
deserializeState,
|
|
457
471
|
restoreState,
|
|
458
472
|
getSSRState,
|
|
473
|
+
clearSSRState,
|
|
459
474
|
|
|
460
475
|
// Mode checks
|
|
461
476
|
isSSR,
|
package/types/a11y.d.ts
CHANGED
|
@@ -91,6 +91,24 @@ export function restoreFocus(): void;
|
|
|
91
91
|
*/
|
|
92
92
|
export function clearFocusStack(): void;
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Handle escape key press within a container
|
|
96
|
+
* @returns Cleanup function to remove listener
|
|
97
|
+
*/
|
|
98
|
+
export function onEscapeKey(
|
|
99
|
+
container: HTMLElement,
|
|
100
|
+
onEscape: () => void,
|
|
101
|
+
options?: { stopPropagation?: boolean }
|
|
102
|
+
): () => void;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Track keyboard vs mouse focus for :focus-visible styling
|
|
106
|
+
*/
|
|
107
|
+
export function createFocusVisibleTracker(): {
|
|
108
|
+
isKeyboardUser: Pulse<boolean>;
|
|
109
|
+
cleanup: () => void;
|
|
110
|
+
};
|
|
111
|
+
|
|
94
112
|
// =============================================================================
|
|
95
113
|
// SKIP LINKS
|
|
96
114
|
// =============================================================================
|
|
@@ -138,10 +156,28 @@ export function prefersColorScheme(): 'light' | 'dark' | 'no-preference';
|
|
|
138
156
|
*/
|
|
139
157
|
export function prefersHighContrast(): boolean;
|
|
140
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Check if user prefers reduced transparency
|
|
161
|
+
*/
|
|
162
|
+
export function prefersReducedTransparency(): boolean;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if forced colors mode is active (Windows High Contrast Mode)
|
|
166
|
+
*/
|
|
167
|
+
export function forcedColorsMode(): 'active' | 'none';
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check user's preferred contrast level
|
|
171
|
+
*/
|
|
172
|
+
export function prefersContrast(): 'no-preference' | 'more' | 'less' | 'custom';
|
|
173
|
+
|
|
141
174
|
export interface UserPreferences {
|
|
142
175
|
reducedMotion: Pulse<boolean>;
|
|
143
176
|
colorScheme: Pulse<'light' | 'dark' | 'no-preference'>;
|
|
144
177
|
highContrast: Pulse<boolean>;
|
|
178
|
+
reducedTransparency?: Pulse<boolean>;
|
|
179
|
+
forcedColors?: Pulse<'active' | 'none'>;
|
|
180
|
+
contrast?: Pulse<'no-preference' | 'more' | 'less' | 'custom'>;
|
|
145
181
|
}
|
|
146
182
|
|
|
147
183
|
/**
|
|
@@ -209,6 +245,96 @@ export function createTabs(
|
|
|
209
245
|
options?: TabsOptions
|
|
210
246
|
): TabsControl;
|
|
211
247
|
|
|
248
|
+
// =============================================================================
|
|
249
|
+
// ARIA WIDGETS
|
|
250
|
+
// =============================================================================
|
|
251
|
+
|
|
252
|
+
export interface ModalOptions {
|
|
253
|
+
labelledBy?: string;
|
|
254
|
+
describedBy?: string;
|
|
255
|
+
closeOnBackdropClick?: boolean;
|
|
256
|
+
onClose?: () => void;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface ModalControl {
|
|
260
|
+
open: () => void;
|
|
261
|
+
close: () => void;
|
|
262
|
+
isOpen: Pulse<boolean>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Create an ARIA-compliant modal dialog
|
|
267
|
+
*/
|
|
268
|
+
export function createModal(
|
|
269
|
+
dialog: HTMLElement,
|
|
270
|
+
options?: ModalOptions
|
|
271
|
+
): ModalControl;
|
|
272
|
+
|
|
273
|
+
export interface TooltipOptions {
|
|
274
|
+
showDelay?: number;
|
|
275
|
+
hideDelay?: number;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface TooltipControl {
|
|
279
|
+
isVisible: Pulse<boolean>;
|
|
280
|
+
cleanup: () => void;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Create an ARIA-compliant tooltip
|
|
285
|
+
*/
|
|
286
|
+
export function createTooltip(
|
|
287
|
+
trigger: HTMLElement,
|
|
288
|
+
tooltip: HTMLElement,
|
|
289
|
+
options?: TooltipOptions
|
|
290
|
+
): TooltipControl;
|
|
291
|
+
|
|
292
|
+
export interface AccordionOptions {
|
|
293
|
+
triggerSelector?: string;
|
|
294
|
+
panelSelector?: string;
|
|
295
|
+
allowMultiple?: boolean;
|
|
296
|
+
defaultOpen?: number;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface AccordionControl {
|
|
300
|
+
open: (index: number) => void;
|
|
301
|
+
close: (index: number) => void;
|
|
302
|
+
toggle: (index: number) => void;
|
|
303
|
+
closeAll: () => void;
|
|
304
|
+
openIndices: Pulse<number[]>;
|
|
305
|
+
cleanup: () => void;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Create an ARIA-compliant accordion
|
|
310
|
+
*/
|
|
311
|
+
export function createAccordion(
|
|
312
|
+
container: HTMLElement,
|
|
313
|
+
options?: AccordionOptions
|
|
314
|
+
): AccordionControl;
|
|
315
|
+
|
|
316
|
+
export interface MenuOptions {
|
|
317
|
+
itemSelector?: string;
|
|
318
|
+
closeOnSelect?: boolean;
|
|
319
|
+
onSelect?: (element: HTMLElement, index: number) => void;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface MenuControl {
|
|
323
|
+
open: () => void;
|
|
324
|
+
close: () => void;
|
|
325
|
+
toggle: () => void;
|
|
326
|
+
cleanup: () => void;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Create an ARIA-compliant dropdown menu
|
|
331
|
+
*/
|
|
332
|
+
export function createMenu(
|
|
333
|
+
button: HTMLElement,
|
|
334
|
+
menu: HTMLElement,
|
|
335
|
+
options?: MenuOptions
|
|
336
|
+
): MenuControl;
|
|
337
|
+
|
|
212
338
|
// =============================================================================
|
|
213
339
|
// KEYBOARD NAVIGATION
|
|
214
340
|
// =============================================================================
|
|
@@ -260,6 +386,61 @@ export function logA11yIssues(issues: A11yIssue[]): void;
|
|
|
260
386
|
*/
|
|
261
387
|
export function highlightA11yIssues(issues: A11yIssue[]): () => void;
|
|
262
388
|
|
|
389
|
+
// =============================================================================
|
|
390
|
+
// COLOR CONTRAST
|
|
391
|
+
// =============================================================================
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Calculate WCAG contrast ratio between two colors
|
|
395
|
+
*/
|
|
396
|
+
export function getContrastRatio(foreground: string, background: string): number;
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Check if a contrast ratio meets WCAG requirements
|
|
400
|
+
*/
|
|
401
|
+
export function meetsContrastRequirement(
|
|
402
|
+
ratio: number,
|
|
403
|
+
level?: 'AA' | 'AAA',
|
|
404
|
+
textSize?: 'normal' | 'large'
|
|
405
|
+
): boolean;
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Get the effective background color of an element (handles transparency)
|
|
409
|
+
*/
|
|
410
|
+
export function getEffectiveBackgroundColor(element: HTMLElement): string;
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Check contrast of a specific element
|
|
414
|
+
*/
|
|
415
|
+
export function checkElementContrast(
|
|
416
|
+
element: HTMLElement,
|
|
417
|
+
level?: 'AA' | 'AAA'
|
|
418
|
+
): {
|
|
419
|
+
ratio: number;
|
|
420
|
+
passes: boolean;
|
|
421
|
+
foreground: string;
|
|
422
|
+
background: string;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// =============================================================================
|
|
426
|
+
// ANNOUNCEMENT QUEUE
|
|
427
|
+
// =============================================================================
|
|
428
|
+
|
|
429
|
+
export interface AnnouncementQueueOptions {
|
|
430
|
+
minDelay?: number;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export interface AnnouncementQueue {
|
|
434
|
+
add: (message: string, options?: { priority?: 'polite' | 'assertive' }) => void;
|
|
435
|
+
clear: () => void;
|
|
436
|
+
queueLength: Pulse<number>;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Create a queue for managing multiple announcements
|
|
441
|
+
*/
|
|
442
|
+
export function createAnnouncementQueue(options?: AnnouncementQueueOptions): AnnouncementQueue;
|
|
443
|
+
|
|
263
444
|
// =============================================================================
|
|
264
445
|
// UTILITIES
|
|
265
446
|
// =============================================================================
|
|
@@ -285,6 +466,11 @@ export function makeInert(element: HTMLElement): () => void;
|
|
|
285
466
|
*/
|
|
286
467
|
export function srOnly(text: string): HTMLSpanElement;
|
|
287
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Compute the accessible name of an element (aria-label, text content, etc.)
|
|
471
|
+
*/
|
|
472
|
+
export function getAccessibleName(element: HTMLElement): string;
|
|
473
|
+
|
|
288
474
|
// =============================================================================
|
|
289
475
|
// DEFAULT EXPORT
|
|
290
476
|
// =============================================================================
|