@birdapi/velinstyle 0.8.0 → 0.9.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/README.de.md +135 -246
- package/README.md +134 -245
- package/cli/blueprints/onboarding.html +2 -2
- package/cli/cli-manifest.json +97 -0
- package/cli/docgen/extract-a11y.js +89 -0
- package/cli/docgen/extract-attributes.js +95 -0
- package/cli/docgen/extract-cli.js +50 -0
- package/cli/docgen/extract-components.js +175 -0
- package/cli/docgen/extract-meta.js +32 -0
- package/cli/docgen/extract-rules.js +76 -0
- package/cli/docgen/extract-tokens.js +79 -0
- package/cli/docgen/extract-utilities.js +75 -0
- package/cli/docgen/markdown.js +43 -0
- package/cli/docs-generate.js +196 -0
- package/cli/index.js +121 -5
- package/cli/meta.js +97 -0
- package/cli/perf-audit.js +174 -0
- package/cli/pii-scanner.js +134 -0
- package/cli/scanner-rules-data.js +62 -0
- package/cli/scanner.js +68 -1
- package/cli/search-index.js +163 -0
- package/cli/tokens-build.js +21 -1
- package/cli/tokens-validate.js +69 -0
- package/components/a11y-entry.js +42 -0
- package/components/a11y-utils.js +71 -0
- package/components/focus-manager.js +45 -8
- package/components/index.js +22 -12
- package/components/runtime/component-loaders.js +43 -0
- package/components/runtime/index.js +77 -0
- package/components/runtime-entry.js +10 -0
- package/components/sanitize.js +135 -54
- package/components/velin-accordion.js +12 -0
- package/components/velin-bottom-nav.js +19 -0
- package/components/velin-carousel.js +2 -2
- package/components/velin-code-block.js +142 -0
- package/components/velin-command.js +15 -0
- package/components/velin-copy.js +18 -5
- package/components/velin-counter.js +3 -0
- package/components/velin-dialog.js +9 -1
- package/components/velin-email.js +94 -0
- package/components/velin-icon.js +65 -22
- package/components/velin-lightbox.js +8 -3
- package/components/velin-live-dot.js +5 -0
- package/components/velin-modal.js +6 -3
- package/components/velin-popover.js +34 -2
- package/components/velin-reveal.js +7 -65
- package/components/velin-scroll-top.js +1 -1
- package/components/velin-search.js +267 -0
- package/components/velin-secure-field.js +99 -0
- package/components/velin-sparkline.js +27 -7
- package/components/{velin-stepper-wc.js → velin-stepper.js} +37 -8
- package/components/velin-theme-toggle.js +12 -5
- package/components/velin-toast.js +4 -0
- package/components/{velin-tooltip-wc.js → velin-tooltip.js} +22 -3
- package/core/a11y/component-contracts.json +221 -0
- package/core/attributes/index.js +6 -0
- package/core/attributes/registry.js +268 -0
- package/core/highlight/highlight.js +114 -0
- package/core/highlight/index.js +29 -0
- package/core/highlight/languages/_utils.js +52 -0
- package/core/highlight/languages/blade.js +16 -0
- package/core/highlight/languages/css.js +26 -0
- package/core/highlight/languages/html.js +17 -0
- package/core/highlight/languages/js.js +58 -0
- package/core/highlight/languages/json.js +14 -0
- package/core/highlight/languages/markdown.js +16 -0
- package/core/highlight/languages/php.js +39 -0
- package/core/highlight/languages/plain.js +7 -0
- package/core/highlight/languages/shell.js +33 -0
- package/core/highlight/languages/sql.js +20 -0
- package/core/highlight/languages/typescript.js +28 -0
- package/core/highlight/observe.js +53 -0
- package/core/highlight/registry.js +107 -0
- package/core/highlight/render.js +35 -0
- package/core/highlight/types.js +28 -0
- package/core/meta/build.js +293 -0
- package/core/meta/index.js +17 -0
- package/core/meta/schema.js +49 -0
- package/core/motion/effects.js +54 -0
- package/core/motion/index.js +76 -0
- package/core/motion/scheduler.js +72 -0
- package/core/motion/scroll.js +32 -0
- package/core/motion/stagger.js +24 -0
- package/core/search/docs-url.js +96 -0
- package/core/search/engine.js +139 -0
- package/core/search/highlight.js +48 -0
- package/core/search/index.js +126 -0
- package/core/search/providers.js +36 -0
- package/core/search/types.js +61 -0
- package/core/search/worker-client.js +56 -0
- package/core/search/worker.js +20 -0
- package/dist/chunks/a11y-utils-IRC7LOPF.js +16 -0
- package/dist/chunks/attributes-3BZLI7XF.js +364 -0
- package/dist/chunks/attributes-4N5JP4CG.js +364 -0
- package/dist/chunks/attributes-5Q2WXGRC.js +374 -0
- package/dist/chunks/attributes-AIR7SVLK.js +364 -0
- package/dist/chunks/attributes-BGRN5XZO.js +364 -0
- package/dist/chunks/attributes-BVSQAYLR.js +364 -0
- package/dist/chunks/attributes-CDYFUY7Y.js +364 -0
- package/dist/chunks/attributes-JMYJZBPE.js +364 -0
- package/dist/chunks/attributes-R5JY3X4O.js +374 -0
- package/dist/chunks/attributes-STYCS3O5.js +391 -0
- package/dist/chunks/attributes-TNSPQTHX.js +374 -0
- package/dist/chunks/attributes-TPANLMLF.js +391 -0
- package/dist/chunks/blade-LIQLVKZQ.js +19 -0
- package/dist/chunks/browser-MRKYZPEH.js +1149 -0
- package/dist/chunks/chunk-2354NEBP.js +109 -0
- package/dist/chunks/chunk-2VNA7G7S.js +109 -0
- package/dist/chunks/chunk-42VVPW6N.js +132 -0
- package/dist/chunks/chunk-7FFAMRFV.js +44 -0
- package/dist/chunks/chunk-7MCGPHVC.js +69 -0
- package/dist/chunks/chunk-AQLRJSGG.js +109 -0
- package/dist/chunks/chunk-EWPSDMQT.js +109 -0
- package/dist/chunks/chunk-F7OLHTGX.js +22 -0
- package/dist/chunks/chunk-J472ERN2.js +242 -0
- package/dist/chunks/chunk-LA7264K4.js +85 -0
- package/dist/chunks/chunk-M5CHQVP6.js +61 -0
- package/dist/chunks/chunk-MKZP2TBH.js +89 -0
- package/dist/chunks/chunk-QTJJ3DPO.js +109 -0
- package/dist/chunks/chunk-RDE3QE7Z.js +89 -0
- package/dist/chunks/chunk-RSMRR27R.js +109 -0
- package/dist/chunks/chunk-RZQVWFHW.js +109 -0
- package/dist/chunks/chunk-SRL6EWHP.js +109 -0
- package/dist/chunks/chunk-U4XHMZ7D.js +109 -0
- package/dist/chunks/chunk-V5YIUA6G.js +109 -0
- package/dist/chunks/chunk-XIET2VSZ.js +109 -0
- package/dist/chunks/chunk-XYLJ2GCB.js +205 -0
- package/dist/chunks/chunk-Z4JYQ2XX.js +51 -0
- package/dist/chunks/chunk-ZPSPKUYD.js +109 -0
- package/dist/chunks/css-MQ5YU77P.js +28 -0
- package/dist/chunks/highlight-GJHHV4KM.js +37 -0
- package/dist/chunks/highlight-XTHRKBA3.js +37 -0
- package/dist/chunks/html-ZODC4DLQ.js +20 -0
- package/dist/chunks/js-MNSNYTJU.js +7 -0
- package/dist/chunks/json-VEM2HFH3.js +17 -0
- package/dist/chunks/markdown-E7OPGS3S.js +19 -0
- package/dist/chunks/php-MLL7T3LT.js +41 -0
- package/dist/chunks/plain-LJJDC5TN.js +8 -0
- package/dist/chunks/runtime-entry.js +14 -0
- package/dist/chunks/shell-QTRK4U4E.js +35 -0
- package/dist/chunks/sql-PPWLHGJ2.js +23 -0
- package/dist/chunks/typescript-JT3CCYWL.js +30 -0
- package/dist/chunks/velin-accordion-FBIPAEXG.js +105 -0
- package/dist/chunks/velin-accordion-QAQO7BI7.js +117 -0
- package/dist/chunks/velin-announcer-PVTXXCWR.js +33 -0
- package/dist/chunks/velin-bottom-nav-2N2TYWOF.js +86 -0
- package/dist/chunks/velin-bottom-nav-7GEMPIM6.js +86 -0
- package/dist/chunks/velin-bottom-nav-CTU6FFZV.js +101 -0
- package/dist/chunks/velin-bottom-nav-N3DFQR66.js +86 -0
- package/dist/chunks/velin-carousel-Y5VCOABZ.js +213 -0
- package/dist/chunks/velin-carousel-ZTBANFQQ.js +213 -0
- package/dist/chunks/velin-code-block-ASUBTAQV.js +119 -0
- package/dist/chunks/velin-code-block-CIAURDRJ.js +132 -0
- package/dist/chunks/velin-code-block-TSUKPY4C.js +123 -0
- package/dist/chunks/velin-code-block-Y6CM726Q.js +119 -0
- package/dist/chunks/velin-collapse-5XYBUIID.js +80 -0
- package/dist/chunks/velin-combobox-2KDTCQ7N.js +155 -0
- package/dist/chunks/velin-combobox-NXS4QB4R.js +155 -0
- package/dist/chunks/velin-combobox-UGA56HP3.js +155 -0
- package/dist/chunks/velin-command-CKDNO4BW.js +148 -0
- package/dist/chunks/velin-command-M7RNHUPR.js +136 -0
- package/dist/chunks/velin-command-ONKO2A3J.js +136 -0
- package/dist/chunks/velin-command-TMLSRGIU.js +136 -0
- package/dist/chunks/velin-copy-G2TZ5YBB.js +81 -0
- package/dist/chunks/velin-copy-NYDBOKXT.js +81 -0
- package/dist/chunks/velin-copy-T5KDTDXP.js +81 -0
- package/dist/chunks/velin-countdown-4ZVDGK4R.js +101 -0
- package/dist/chunks/velin-countdown-5XSTN3BH.js +101 -0
- package/dist/chunks/velin-countdown-7AIY66CU.js +101 -0
- package/dist/chunks/velin-counter-ISCTEAHI.js +118 -0
- package/dist/chunks/velin-counter-L5FYJF7A.js +121 -0
- package/dist/chunks/velin-dialog-D2GUHSCD.js +144 -0
- package/dist/chunks/velin-dialog-ELFOZIXZ.js +152 -0
- package/dist/chunks/velin-dialog-GTTZI564.js +147 -0
- package/dist/chunks/velin-dialog-IJLFDXAJ.js +144 -0
- package/dist/chunks/velin-dialog-TNS3WXMB.js +144 -0
- package/dist/chunks/velin-drawer-BE6TYN3H.js +130 -0
- package/dist/chunks/velin-drawer-DLL2GLT6.js +130 -0
- package/dist/chunks/velin-drawer-MJS6WCF7.js +130 -0
- package/dist/chunks/velin-dropdown-PLUIFA5M.js +172 -0
- package/dist/chunks/velin-email-N5B5ZYTE.js +90 -0
- package/dist/chunks/velin-email-NTM6DG3V.js +90 -0
- package/dist/chunks/velin-email-RBX6ZR4S.js +90 -0
- package/dist/chunks/velin-haptic-4QQTM3RK.js +50 -0
- package/dist/chunks/velin-icon-7M76DOPB.js +164 -0
- package/dist/chunks/velin-icon-KZO3IEHG.js +164 -0
- package/dist/chunks/velin-icon-WTFPG2DD.js +191 -0
- package/dist/chunks/velin-icon-ZTSKUOVN.js +193 -0
- package/dist/chunks/velin-lightbox-6E7CAATP.js +149 -0
- package/dist/chunks/velin-lightbox-I75U6JF4.js +140 -0
- package/dist/chunks/velin-lightbox-QOTB4PRL.js +140 -0
- package/dist/chunks/velin-lightbox-UCD6N7HF.js +140 -0
- package/dist/chunks/velin-live-dot-RJHX2DGY.js +74 -0
- package/dist/chunks/velin-live-dot-XHUZCIDZ.js +67 -0
- package/dist/chunks/velin-menubar-I426AED2.js +86 -0
- package/dist/chunks/velin-menubar-J66JXRWW.js +86 -0
- package/dist/chunks/velin-menubar-SFEXJE56.js +86 -0
- package/dist/chunks/velin-modal-2HOBXE6F.js +176 -0
- package/dist/chunks/velin-modal-5HYNNNUI.js +176 -0
- package/dist/chunks/velin-modal-AEOTZ67O.js +177 -0
- package/dist/chunks/velin-modal-GLI2XZZB.js +176 -0
- package/dist/chunks/velin-persist-DFPL3QP4.js +110 -0
- package/dist/chunks/velin-popover-EXINRIGJ.js +145 -0
- package/dist/chunks/velin-popover-HQW6Q2Y6.js +172 -0
- package/dist/chunks/velin-popover-OXX7MRZ6.js +145 -0
- package/dist/chunks/velin-popover-T72HK3DF.js +151 -0
- package/dist/chunks/velin-popover-YKCCROJG.js +145 -0
- package/dist/chunks/velin-progress-ring-E4YRLZHH.js +66 -0
- package/dist/chunks/velin-rating-6CDBI6DG.js +91 -0
- package/dist/chunks/velin-rating-N4DI2XNL.js +91 -0
- package/dist/chunks/velin-rating-XRQJV4LO.js +91 -0
- package/dist/chunks/velin-scroll-top-F2FCZNYD.js +58 -0
- package/dist/chunks/velin-scroll-top-OHYVVHHY.js +58 -0
- package/dist/chunks/velin-scrollspy-OKCT4FDV.js +50 -0
- package/dist/chunks/velin-search-2XRQFBCS.js +556 -0
- package/dist/chunks/velin-search-4YHJWYSS.js +556 -0
- package/dist/chunks/velin-search-7B4KJ6TX.js +557 -0
- package/dist/chunks/velin-search-LWHY23Q5.js +474 -0
- package/dist/chunks/velin-secure-field-6PV7RZDJ.js +95 -0
- package/dist/chunks/velin-secure-field-H7FSBUJW.js +95 -0
- package/dist/chunks/velin-secure-field-OFNAXCDP.js +95 -0
- package/dist/chunks/velin-segmented-control-EKL4E3BL.js +109 -0
- package/dist/chunks/velin-segmented-control-GYD5BIXQ.js +109 -0
- package/dist/chunks/velin-segmented-control-PQX6LKOJ.js +109 -0
- package/dist/chunks/velin-sheet-3OGI4HD7.js +119 -0
- package/dist/chunks/velin-sheet-OUK6572T.js +119 -0
- package/dist/chunks/velin-sheet-R2Y67X6H.js +119 -0
- package/dist/chunks/velin-sparkline-GVEANJVW.js +184 -0
- package/dist/chunks/velin-sparkline-KDA3GQAN.js +162 -0
- package/dist/chunks/velin-sparkline-XVTFC6GE.js +181 -0
- package/dist/chunks/velin-stepper-2ZIHLKGM.js +141 -0
- package/dist/chunks/velin-stepper-7ECRUYHD.js +148 -0
- package/dist/chunks/velin-stepper-FYEUEWE6.js +141 -0
- package/dist/chunks/velin-stepper-X54WGHDG.js +141 -0
- package/dist/chunks/velin-stepper-wc-QU3KRVQR.js +133 -0
- package/dist/chunks/velin-tabs-5UQ5ZVMV.js +138 -0
- package/dist/chunks/velin-theme-toggle-32J2CBNC.js +291 -0
- package/dist/chunks/velin-theme-toggle-BNNS7WLV.js +285 -0
- package/dist/chunks/velin-theme-toggle-NO7V4TWI.js +285 -0
- package/dist/chunks/velin-theme-toggle-QELCJM2Q.js +291 -0
- package/dist/chunks/velin-toast-OMMOA2DK.js +109 -0
- package/dist/chunks/velin-toast-RCZ4CSUC.js +109 -0
- package/dist/chunks/velin-toast-RSF2IHT3.js +113 -0
- package/dist/chunks/velin-toast-WEUBTJG6.js +109 -0
- package/dist/chunks/velin-tooltip-664IOHZA.js +112 -0
- package/dist/chunks/velin-tooltip-BDHGRTQB.js +112 -0
- package/dist/chunks/velin-tooltip-HVUOKNVX.js +112 -0
- package/dist/chunks/velin-tooltip-wc-LXDVT6DE.js +103 -0
- package/dist/chunks/worker-client-R6DBYPFT.js +46 -0
- package/dist/llms.test.txt +40 -0
- package/dist/llms.txt +40 -0
- package/dist/search-index.json +1773 -0
- package/dist/search-index.test.json +1773 -0
- package/dist/themes/brutalist.min.css +1 -1
- package/dist/themes/midnight.min.css +1 -1
- package/dist/themes/pastel.min.css +1 -1
- package/dist/themes/retro.min.css +1 -1
- package/dist/themes/sunset.min.css +1 -1
- package/dist/velin-agent.json +684 -0
- package/dist/velin-agent.test.json +684 -0
- package/dist/velinstyle-components.iife.js +7715 -3515
- package/dist/velinstyle-components.js +7087 -2859
- package/dist/velinstyle-components.min.js +196 -109
- package/dist/velinstyle.css +604 -65
- package/dist/velinstyle.d.ts +54 -0
- package/dist/velinstyle.min.css +1 -1
- package/examples/search-index.schema.json +33 -0
- package/package.json +56 -10
- package/src/a11y/authentication.css +37 -0
- package/src/a11y/consistent-help.css +22 -0
- package/src/a11y/dragging-alternatives.css +30 -0
- package/src/a11y/focus-appearance.css +20 -0
- package/src/a11y/high-contrast-aaa.css +16 -26
- package/src/a11y/skip-link.css +3 -1
- package/src/a11y/sr-only.css +1 -1
- package/src/base/reset.css +0 -36
- package/src/base/wc-placeholder.css +102 -0
- package/src/components/attributes.css +42 -0
- package/src/components/highlight.css +182 -0
- package/src/components/input-group.css +1 -2
- package/src/components/nav.css +2 -3
- package/src/components/search.css +95 -0
- package/src/components/stepper.css +2 -4
- package/src/components/timeline.css +1 -2
- package/src/components/tooltip.css +2 -4
- package/src/layout/container.css +5 -5
- package/src/layout/grid.css +8 -8
- package/src/layout/patterns.css +5 -5
- package/src/themes/brutalist.css +2 -11
- package/src/themes/corporate.css +0 -9
- package/src/themes/earth.css +0 -9
- package/src/themes/forest.css +0 -9
- package/src/themes/midnight.css +1 -10
- package/src/themes/neon.css +0 -9
- package/src/themes/nordic.css +0 -9
- package/src/themes/ocean.css +0 -9
- package/src/themes/pastel.css +1 -10
- package/src/themes/retro.css +2 -11
- package/src/themes/sharp.css +0 -7
- package/src/themes/soft.css +0 -7
- package/src/themes/sunset.css +1 -10
- package/src/tokens/color.css +45 -46
- package/src/tokens/fonts.css +10 -0
- package/src/tokens/typography.css +4 -5
- package/src/utilities/animation.css +2 -2
- package/src/utilities/display.css +3 -3
- package/src/utilities/divide.css +2 -6
- package/src/utilities/responsive.css +4 -4
- package/src/utilities/safe-area.css +1 -1
- package/src/utilities/scroll-animation.css +34 -0
- package/src/utilities/spacing.css +17 -8
- package/src/utilities/text.css +4 -1
- package/src/velinstyle.css +9 -0
package/cli/tokens-build.js
CHANGED
|
@@ -18,7 +18,9 @@ function varName(key) {
|
|
|
18
18
|
function emitBlock(indent, obj) {
|
|
19
19
|
if (!obj || typeof obj !== 'object') return [];
|
|
20
20
|
const lines = [];
|
|
21
|
-
|
|
21
|
+
const keys = Object.keys(obj).sort((a, b) => a.localeCompare(b));
|
|
22
|
+
for (const k of keys) {
|
|
23
|
+
const v = obj[k];
|
|
22
24
|
if (typeof v === 'string') {
|
|
23
25
|
lines.push(`${indent}${varName(k)}: ${v};`);
|
|
24
26
|
}
|
|
@@ -62,6 +64,24 @@ export function buildTokensFromJson(filePath) {
|
|
|
62
64
|
lines.push(' }');
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
if (data.fonts && typeof data.fonts === 'object') {
|
|
68
|
+
lines.push(' :root {');
|
|
69
|
+
lines.push(...emitBlock(' ', data.fonts));
|
|
70
|
+
lines.push(' }');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (data.motion && typeof data.motion === 'object') {
|
|
74
|
+
lines.push(' :root {');
|
|
75
|
+
lines.push(...emitBlock(' ', data.motion));
|
|
76
|
+
lines.push(' }');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (data.zIndex && typeof data.zIndex === 'object') {
|
|
80
|
+
lines.push(' :root {');
|
|
81
|
+
lines.push(...emitBlock(' ', data.zIndex));
|
|
82
|
+
lines.push(' }');
|
|
83
|
+
}
|
|
84
|
+
|
|
65
85
|
lines.push('}');
|
|
66
86
|
return `${lines.join('\n')}\n`;
|
|
67
87
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
const OKLCH_RE = /^oklch\(\s*[\d.]+%?\s+[\d.]+\s+[\d.]+(?:\s*\/\s*[\d.]+%?)?\s*\)$/i;
|
|
4
|
+
|
|
5
|
+
export function validateTokensJson(filePath) {
|
|
6
|
+
if (!existsSync(filePath)) {
|
|
7
|
+
return { ok: false, errors: [`File not found: ${filePath}`] };
|
|
8
|
+
}
|
|
9
|
+
let data;
|
|
10
|
+
try {
|
|
11
|
+
data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
12
|
+
} catch (e) {
|
|
13
|
+
return { ok: false, errors: [`Invalid JSON: ${e.message}`] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const errors = [];
|
|
17
|
+
const tokens = data.tokens ?? data;
|
|
18
|
+
|
|
19
|
+
if (typeof tokens !== 'object' || tokens === null) {
|
|
20
|
+
return { ok: false, errors: ['Root must be an object'] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const walk = (obj, path) => {
|
|
24
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
25
|
+
if (key === 'themes' || key === 'displayP3' || key === 'fonts' || key === 'motion' || key === 'zIndex') continue;
|
|
26
|
+
if (typeof val === 'object' && val !== null) {
|
|
27
|
+
walk(val, `${path}.${key}`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (typeof val !== 'string') {
|
|
31
|
+
errors.push(`${path}.${key}: must be a string`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (key.includes('color') && val.startsWith('oklch') && !OKLCH_RE.test(val.trim())) {
|
|
35
|
+
errors.push(`${path}.${key}: invalid OKLCH syntax`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
walk(tokens, 'tokens');
|
|
41
|
+
|
|
42
|
+
if (data.themes && typeof data.themes === 'object') {
|
|
43
|
+
for (const name of Object.keys(data.themes)) {
|
|
44
|
+
if (!/^[a-z0-9-]+$/i.test(name)) {
|
|
45
|
+
errors.push(`themes.${name}: invalid theme name`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (data.fonts && typeof data.fonts === 'object') {
|
|
51
|
+
for (const [k, v] of Object.entries(data.fonts)) {
|
|
52
|
+
if (typeof v !== 'string') errors.push(`fonts.${k}: must be a string`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (data.motion && typeof data.motion === 'object') {
|
|
57
|
+
for (const [k, v] of Object.entries(data.motion)) {
|
|
58
|
+
if (typeof v !== 'string') errors.push(`motion.${k}: must be a string`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (data.zIndex && typeof data.zIndex === 'object') {
|
|
63
|
+
for (const [k, v] of Object.entries(data.zIndex)) {
|
|
64
|
+
if (typeof v !== 'string') errors.push(`zIndex.${k}: must be a string`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { ok: errors.length === 0, errors, data };
|
|
69
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional accessibility bootstrap for VelinStyle apps.
|
|
3
|
+
* @example
|
|
4
|
+
* import { initA11y } from '@birdapi/velinstyle/a11y';
|
|
5
|
+
* initA11y({ announcer: true, scrollPadding: true });
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getAnnouncer } from './a11y-utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {{ announcer?: boolean, scrollPadding?: boolean, skipLink?: boolean }} [options]
|
|
12
|
+
*/
|
|
13
|
+
export function initA11y(options = {}) {
|
|
14
|
+
if (typeof document === 'undefined') return;
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
announcer = true,
|
|
18
|
+
scrollPadding = true,
|
|
19
|
+
skipLink = false,
|
|
20
|
+
} = options;
|
|
21
|
+
|
|
22
|
+
if (announcer) getAnnouncer();
|
|
23
|
+
|
|
24
|
+
if (scrollPadding) {
|
|
25
|
+
const nav = document.querySelector('.velin-nav, .velin-doc-header, .site-nav, [data-velin-fixed-nav]');
|
|
26
|
+
if (nav) {
|
|
27
|
+
const h = nav.getBoundingClientRect().height;
|
|
28
|
+
document.documentElement.style.setProperty('--velin-nav-height', `${Math.ceil(h)}px`);
|
|
29
|
+
document.documentElement.classList.add('velin-scroll-pt-nav');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (skipLink && !document.querySelector('.velin-skip-link') && document.getElementById('main')) {
|
|
34
|
+
const a = document.createElement('a');
|
|
35
|
+
a.href = '#main';
|
|
36
|
+
a.className = 'velin-skip-link';
|
|
37
|
+
a.textContent = 'Skip to main content';
|
|
38
|
+
document.body.prepend(a);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { announce, getAnnouncer } from './a11y-utils.js';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared accessibility helpers for VelinStyle web components.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
let _announcer;
|
|
6
|
+
|
|
7
|
+
export function getAnnouncer() {
|
|
8
|
+
if (typeof document === 'undefined') return null;
|
|
9
|
+
if (_announcer?.isConnected) return _announcer;
|
|
10
|
+
_announcer = document.querySelector('velin-announcer');
|
|
11
|
+
if (!_announcer) {
|
|
12
|
+
_announcer = document.createElement('velin-announcer');
|
|
13
|
+
document.body.appendChild(_announcer);
|
|
14
|
+
}
|
|
15
|
+
return _announcer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} message
|
|
20
|
+
* @param {'polite' | 'assertive'} [priority]
|
|
21
|
+
*/
|
|
22
|
+
export function announce(message, priority = 'polite') {
|
|
23
|
+
const el = getAnnouncer();
|
|
24
|
+
if (!el || typeof el.announce !== 'function') return;
|
|
25
|
+
el.announce(message, { assertive: priority === 'assertive' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {HTMLElement} trigger
|
|
30
|
+
* @param {HTMLElement | null} panel
|
|
31
|
+
*/
|
|
32
|
+
export function syncExpanded(trigger, panel) {
|
|
33
|
+
if (!trigger || !panel) return;
|
|
34
|
+
const open = trigger.getAttribute('aria-expanded') === 'true';
|
|
35
|
+
panel.hidden = !open;
|
|
36
|
+
if (panel.id) trigger.setAttribute('aria-controls', panel.id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {HTMLElement} el
|
|
41
|
+
* @param {string} label
|
|
42
|
+
*/
|
|
43
|
+
export function ensureAriaLabel(el, label) {
|
|
44
|
+
if (!el || !label) return;
|
|
45
|
+
if (!el.getAttribute('aria-label')?.trim()) el.setAttribute('aria-label', label);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {() => void} fn
|
|
50
|
+
* @param {() => void} [fallback]
|
|
51
|
+
*/
|
|
52
|
+
export function respectReducedMotion(fn, fallback) {
|
|
53
|
+
const reduced = typeof matchMedia === 'function'
|
|
54
|
+
&& matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
55
|
+
if (reduced && fallback) fallback();
|
|
56
|
+
else fn();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} [status] live|idle|error
|
|
61
|
+
*/
|
|
62
|
+
export function liveDotLabel(status) {
|
|
63
|
+
const map = {
|
|
64
|
+
live: 'Live',
|
|
65
|
+
idle: 'Idle',
|
|
66
|
+
error: 'Error',
|
|
67
|
+
offline: 'Offline',
|
|
68
|
+
connecting: 'Connecting',
|
|
69
|
+
};
|
|
70
|
+
return map[String(status || 'live').toLowerCase()] || status || 'Live';
|
|
71
|
+
}
|
|
@@ -85,22 +85,59 @@ export function restoreFocus(element) {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
/** @type {{ host: HTMLElement, siblings: Set<Element> }[]} */
|
|
89
|
+
const _inertStack = [];
|
|
90
|
+
let _overflowDepth = 0;
|
|
91
|
+
let _savedOverflow = '';
|
|
92
|
+
|
|
93
|
+
function activeInertElements() {
|
|
94
|
+
const out = new Set();
|
|
95
|
+
for (const layer of _inertStack) {
|
|
96
|
+
for (const el of layer.siblings) out.add(el);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
89
100
|
|
|
90
101
|
export function setBackgroundInert(except) {
|
|
91
|
-
|
|
102
|
+
const host = except instanceof HTMLElement ? except : null;
|
|
103
|
+
if (!host) return;
|
|
104
|
+
|
|
105
|
+
const siblings = new Set();
|
|
92
106
|
for (const child of document.body.children) {
|
|
93
|
-
if (child ===
|
|
107
|
+
if (child === host || child.contains(host)) continue;
|
|
94
108
|
if (!child.hasAttribute('inert')) {
|
|
95
109
|
child.setAttribute('inert', '');
|
|
96
|
-
|
|
110
|
+
siblings.add(child);
|
|
97
111
|
}
|
|
98
112
|
}
|
|
113
|
+
|
|
114
|
+
_inertStack.push({ host, siblings });
|
|
115
|
+
|
|
116
|
+
if (_overflowDepth === 0) {
|
|
117
|
+
_savedOverflow = document.body.style.overflow;
|
|
118
|
+
document.body.style.overflow = 'hidden';
|
|
119
|
+
}
|
|
120
|
+
_overflowDepth += 1;
|
|
99
121
|
}
|
|
100
122
|
|
|
101
|
-
export function clearBackgroundInert() {
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
export function clearBackgroundInert(except) {
|
|
124
|
+
let idx = -1;
|
|
125
|
+
if (except instanceof HTMLElement) {
|
|
126
|
+
idx = _inertStack.findLastIndex((layer) => layer.host === except);
|
|
127
|
+
} else if (_inertStack.length) {
|
|
128
|
+
idx = _inertStack.length - 1;
|
|
129
|
+
}
|
|
130
|
+
if (idx === -1) return;
|
|
131
|
+
|
|
132
|
+
const { siblings } = _inertStack.splice(idx, 1)[0];
|
|
133
|
+
const stillNeeded = activeInertElements();
|
|
134
|
+
for (const el of siblings) {
|
|
135
|
+
if (!stillNeeded.has(el)) el.removeAttribute('inert');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_overflowDepth = Math.max(0, _overflowDepth - 1);
|
|
139
|
+
if (_overflowDepth === 0) {
|
|
140
|
+
document.body.style.overflow = _savedOverflow;
|
|
141
|
+
_savedOverflow = '';
|
|
104
142
|
}
|
|
105
|
-
_inertSiblings = [];
|
|
106
143
|
}
|
package/components/index.js
CHANGED
|
@@ -12,9 +12,9 @@ export { default as VelinScrollTop } from './velin-scroll-top.js';
|
|
|
12
12
|
export { default as VelinCarousel } from './velin-carousel.js';
|
|
13
13
|
export { default as VelinCollapse } from './velin-collapse.js';
|
|
14
14
|
export { default as VelinScrollspy } from './velin-scrollspy.js';
|
|
15
|
-
export { default as VelinTooltipWC } from './velin-tooltip
|
|
15
|
+
export { default as VelinTooltip, VelinTooltipWC } from './velin-tooltip.js';
|
|
16
16
|
export { default as VelinLightbox } from './velin-lightbox.js';
|
|
17
|
-
export { default as VelinStepperWC } from './velin-stepper
|
|
17
|
+
export { default as VelinStepper, VelinStepperWC } from './velin-stepper.js';
|
|
18
18
|
export { default as VelinDialog } from './velin-dialog.js';
|
|
19
19
|
export { default as VelinCountdown } from './velin-countdown.js';
|
|
20
20
|
export { default as VelinProgressRing } from './velin-progress-ring.js';
|
|
@@ -27,10 +27,28 @@ export { default as VelinRating } from './velin-rating.js';
|
|
|
27
27
|
export { default as VelinMenubar } from './velin-menubar.js';
|
|
28
28
|
export { default as VelinCommand } from './velin-command.js';
|
|
29
29
|
export { default as VelinAnnouncer } from './velin-announcer.js';
|
|
30
|
+
export { default as VelinEmail } from './velin-email.js';
|
|
31
|
+
export { register, lazyDefine, whenDefined, bootFromDOM } from './runtime/index.js';
|
|
30
32
|
export { default as VelinSparkline } from './velin-sparkline.js';
|
|
31
33
|
export { default as VelinCounter } from './velin-counter.js';
|
|
32
34
|
export { default as VelinLiveDot } from './velin-live-dot.js';
|
|
33
|
-
export {
|
|
35
|
+
export { default as VelinCodeBlock } from './velin-code-block.js';
|
|
36
|
+
export { default as VelinSearch } from './velin-search.js';
|
|
37
|
+
export { bindDeclarativeSearch } from './velin-search.js';
|
|
38
|
+
export { initReveal, initMotion, velinMotion } from './velin-reveal.js';
|
|
39
|
+
export { initA11y, announce, getAnnouncer } from './a11y-entry.js';
|
|
40
|
+
export { velinSearch, createSearch, registerSearchProvider } from '../core/search/index.js';
|
|
41
|
+
export { bootAttributes, registerAttribute, listRegisteredAttributes } from '../core/attributes/index.js';
|
|
42
|
+
export {
|
|
43
|
+
velinSyntax,
|
|
44
|
+
highlightElement,
|
|
45
|
+
highlightAll,
|
|
46
|
+
initHighlight,
|
|
47
|
+
observeCodeBlocks,
|
|
48
|
+
registerLanguage,
|
|
49
|
+
lazyLoadLanguage,
|
|
50
|
+
renderTokens,
|
|
51
|
+
} from '../core/highlight/index.js';
|
|
34
52
|
export { flipReorder, filterList } from './velin-flip.js';
|
|
35
53
|
export { escapeHTML, escapeHTMLAttribute, sanitizeURL, stripControlChars, createSafeHTML, getTrustedPolicy } from './sanitize.js';
|
|
36
54
|
export { VelinHapticObserver, vibrate, applyHaptic, PATTERNS as HapticPatterns } from './velin-haptic.js';
|
|
@@ -41,12 +59,4 @@ import './velin-counter.js';
|
|
|
41
59
|
import './velin-live-dot.js';
|
|
42
60
|
import './velin-reveal.js';
|
|
43
61
|
import './velin-flip.js';
|
|
44
|
-
import
|
|
45
|
-
if (typeof document !== 'undefined') {
|
|
46
|
-
const _hapticInit = () => { new VelinHapticObserver().start(document.body); };
|
|
47
|
-
if (document.readyState === 'loading') {
|
|
48
|
-
document.addEventListener('DOMContentLoaded', _hapticInit);
|
|
49
|
-
} else {
|
|
50
|
-
_hapticInit();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
62
|
+
import './velin-email.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED — run: node scripts/generate-component-loaders.mjs
|
|
3
|
+
*/
|
|
4
|
+
export const COMPONENT_LOADERS = {
|
|
5
|
+
'velin-accordion': () => import('../velin-accordion.js'),
|
|
6
|
+
'velin-announcer': () => import('../velin-announcer.js'),
|
|
7
|
+
'velin-bottom-nav': () => import('../velin-bottom-nav.js'),
|
|
8
|
+
'velin-carousel': () => import('../velin-carousel.js'),
|
|
9
|
+
'velin-code-block': () => import('../velin-code-block.js'),
|
|
10
|
+
'velin-collapse': () => import('../velin-collapse.js'),
|
|
11
|
+
'velin-combobox': () => import('../velin-combobox.js'),
|
|
12
|
+
'velin-command': () => import('../velin-command.js'),
|
|
13
|
+
'velin-copy': () => import('../velin-copy.js'),
|
|
14
|
+
'velin-countdown': () => import('../velin-countdown.js'),
|
|
15
|
+
'velin-counter': () => import('../velin-counter.js'),
|
|
16
|
+
'velin-dialog': () => import('../velin-dialog.js'),
|
|
17
|
+
'velin-drawer': () => import('../velin-drawer.js'),
|
|
18
|
+
'velin-dropdown': () => import('../velin-dropdown.js'),
|
|
19
|
+
'velin-email': () => import('../velin-email.js'),
|
|
20
|
+
'velin-icon': () => import('../velin-icon.js'),
|
|
21
|
+
'velin-lightbox': () => import('../velin-lightbox.js'),
|
|
22
|
+
'velin-live-dot': () => import('../velin-live-dot.js'),
|
|
23
|
+
'velin-menubar': () => import('../velin-menubar.js'),
|
|
24
|
+
'velin-modal': () => import('../velin-modal.js'),
|
|
25
|
+
'velin-persist': () => import('../velin-persist.js'),
|
|
26
|
+
'velin-popover': () => import('../velin-popover.js'),
|
|
27
|
+
'velin-progress-ring': () => import('../velin-progress-ring.js'),
|
|
28
|
+
'velin-rating': () => import('../velin-rating.js'),
|
|
29
|
+
'velin-scroll-top': () => import('../velin-scroll-top.js'),
|
|
30
|
+
'velin-scrollspy': () => import('../velin-scrollspy.js'),
|
|
31
|
+
'velin-search': () => import('../velin-search.js'),
|
|
32
|
+
'velin-secure-field': () => import('../velin-secure-field.js'),
|
|
33
|
+
'velin-segmented-control': () => import('../velin-segmented-control.js'),
|
|
34
|
+
'velin-sheet': () => import('../velin-sheet.js'),
|
|
35
|
+
'velin-sparkline': () => import('../velin-sparkline.js'),
|
|
36
|
+
'velin-stepper': () => import('../velin-stepper.js'),
|
|
37
|
+
'velin-stepper-wc': () => import('../velin-stepper.js'),
|
|
38
|
+
'velin-tabs': () => import('../velin-tabs.js'),
|
|
39
|
+
'velin-theme-toggle': () => import('../velin-theme-toggle.js'),
|
|
40
|
+
'velin-toast': () => import('../velin-toast.js'),
|
|
41
|
+
'velin-tooltip': () => import('../velin-tooltip.js'),
|
|
42
|
+
'velin-tooltip-wc': () => import('../velin-tooltip.js'),
|
|
43
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VelinStyle runtime — register only the Web Components you need.
|
|
3
|
+
*/
|
|
4
|
+
import { COMPONENT_LOADERS } from './component-loaders.js';
|
|
5
|
+
|
|
6
|
+
const registry = new Map();
|
|
7
|
+
|
|
8
|
+
const VELIN_TAG_RE = /^velin-[a-z0-9-]+$/;
|
|
9
|
+
|
|
10
|
+
export function whenDefined(tagName) {
|
|
11
|
+
if (typeof customElements === 'undefined') {
|
|
12
|
+
return Promise.reject(new Error('customElements not available'));
|
|
13
|
+
}
|
|
14
|
+
return customElements.whenDefined(tagName);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function lazyDefine(tagName) {
|
|
18
|
+
const loader = COMPONENT_LOADERS[tagName];
|
|
19
|
+
if (!loader) {
|
|
20
|
+
throw new Error(`Unknown Velin component: ${tagName}`);
|
|
21
|
+
}
|
|
22
|
+
if (registry.has(tagName)) return registry.get(tagName);
|
|
23
|
+
const p = loader().then((mod) => {
|
|
24
|
+
const Cls = mod.default;
|
|
25
|
+
if (Cls && !customElements.get(tagName)) {
|
|
26
|
+
customElements.define(tagName, Cls);
|
|
27
|
+
}
|
|
28
|
+
return Cls;
|
|
29
|
+
});
|
|
30
|
+
registry.set(tagName, p);
|
|
31
|
+
return p;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function register(tagNames) {
|
|
35
|
+
return Promise.all(tagNames.map((t) => lazyDefine(t)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function collectTagsFromDOM(root) {
|
|
39
|
+
const tags = new Set();
|
|
40
|
+
root.querySelectorAll('[data-velin-component]').forEach((el) => {
|
|
41
|
+
const name = el.getAttribute('data-velin-component');
|
|
42
|
+
if (name) tags.add(name.startsWith('velin-') ? name : `velin-${name}`);
|
|
43
|
+
});
|
|
44
|
+
for (const el of root.querySelectorAll('*')) {
|
|
45
|
+
const tag = el.tagName?.toLowerCase();
|
|
46
|
+
if (tag && VELIN_TAG_RE.test(tag) && COMPONENT_LOADERS[tag]) {
|
|
47
|
+
tags.add(tag);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return tags;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {ParentNode} [root]
|
|
55
|
+
* @param {{ attributes?: boolean, highlight?: boolean, haptic?: boolean, tags?: string[] }} [options]
|
|
56
|
+
*/
|
|
57
|
+
export async function bootFromDOM(root = document, options = {}) {
|
|
58
|
+
const tags = options.tags?.length
|
|
59
|
+
? new Set(options.tags)
|
|
60
|
+
: collectTagsFromDOM(root);
|
|
61
|
+
const wcPromise = register([...tags]);
|
|
62
|
+
if (options.attributes) {
|
|
63
|
+
const { bootAttributes } = await import('../../core/attributes/index.js');
|
|
64
|
+
await bootAttributes(root);
|
|
65
|
+
}
|
|
66
|
+
if (options.highlight) {
|
|
67
|
+
const { initHighlight } = await import('../../core/highlight/index.js');
|
|
68
|
+
initHighlight(root);
|
|
69
|
+
}
|
|
70
|
+
if (options.haptic) {
|
|
71
|
+
const { VelinHapticObserver } = await import('../velin-haptic.js');
|
|
72
|
+
new VelinHapticObserver().start(root instanceof Document ? root.body : root);
|
|
73
|
+
}
|
|
74
|
+
return wcPromise;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { COMPONENT_LOADERS };
|
package/components/sanitize.js
CHANGED
|
@@ -1,54 +1,135 @@
|
|
|
1
|
-
const ESCAPE_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
2
|
-
const ESCAPE_RE = /[&<>"']/g;
|
|
3
|
-
const CONTROL_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
|
|
4
|
-
|
|
5
|
-
const ALLOWED_URL_PROTOCOLS = new Set(['http:', 'https:', 'data:', 'mailto:', 'tel:']);
|
|
6
|
-
const BLOCKED_DATA_MIME = /^data:text\/html/i;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
const ESCAPE_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
2
|
+
const ESCAPE_RE = /[&<>"']/g;
|
|
3
|
+
const CONTROL_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
|
|
4
|
+
|
|
5
|
+
const ALLOWED_URL_PROTOCOLS = new Set(['http:', 'https:', 'data:', 'mailto:', 'tel:']);
|
|
6
|
+
const BLOCKED_DATA_MIME = /^data:(?:text\/html|image\/svg\+xml|application\/xml)/i;
|
|
7
|
+
const ALLOWED_DATA_IMAGE = /^data:image\/(png|jpeg|jpg|gif|webp|avif);base64,/i;
|
|
8
|
+
|
|
9
|
+
const SECURE_INPUT_TYPES = new Set(['text', 'password', 'email', 'search', 'tel', 'url', 'number']);
|
|
10
|
+
|
|
11
|
+
export function escapeHTML(str) {
|
|
12
|
+
if (typeof str !== 'string') return '';
|
|
13
|
+
return str.replace(ESCAPE_RE, (ch) => ESCAPE_MAP[ch]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function stripControlChars(str) {
|
|
17
|
+
if (typeof str !== 'string') return '';
|
|
18
|
+
return str.replace(CONTROL_RE, '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function escapeHTMLAttribute(str) {
|
|
22
|
+
return escapeHTML(stripControlChars(str));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} url
|
|
27
|
+
* @param {string} [base]
|
|
28
|
+
*/
|
|
29
|
+
export function sanitizeURL(url, base) {
|
|
30
|
+
if (typeof url !== 'string') return '';
|
|
31
|
+
const trimmed = url.trim();
|
|
32
|
+
if (!trimmed) return '';
|
|
33
|
+
if (/^\s*javascript:/i.test(trimmed) || /^\s*vbscript:/i.test(trimmed)) return '';
|
|
34
|
+
if (BLOCKED_DATA_MIME.test(trimmed)) return '';
|
|
35
|
+
if (/^data:/i.test(trimmed) && !ALLOWED_DATA_IMAGE.test(trimmed)) return '';
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(trimmed, base || (typeof location !== 'undefined' ? location.href : 'https://example.invalid/'));
|
|
38
|
+
if (!ALLOWED_URL_PROTOCOLS.has(parsed.protocol)) return '';
|
|
39
|
+
if (parsed.protocol === 'data:' && !ALLOWED_DATA_IMAGE.test(parsed.href)) return '';
|
|
40
|
+
return parsed.href;
|
|
41
|
+
} catch {
|
|
42
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('/')) return trimmed;
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} url
|
|
49
|
+
* @param {string} [base]
|
|
50
|
+
*/
|
|
51
|
+
export function sanitizeSearchUrl(url, base) {
|
|
52
|
+
if (typeof url !== 'string') return '#';
|
|
53
|
+
const trimmed = url.trim();
|
|
54
|
+
if (!trimmed) return '#';
|
|
55
|
+
if (trimmed.startsWith('#')) return trimmed;
|
|
56
|
+
if (trimmed.startsWith('/') && !trimmed.startsWith('//')) return trimmed;
|
|
57
|
+
// Index paths like getting-started/foo.html — must stay relative (resolved at navigation).
|
|
58
|
+
if (!/^https?:\/\//i.test(trimmed)) {
|
|
59
|
+
if (/^\s*javascript:/i.test(trimmed) || /^\s*vbscript:/i.test(trimmed)) return '#';
|
|
60
|
+
return trimmed;
|
|
61
|
+
}
|
|
62
|
+
const safe = sanitizeURL(trimmed, base);
|
|
63
|
+
return safe || '#';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function sanitizeInputType(type) {
|
|
67
|
+
const t = String(type || 'text').toLowerCase();
|
|
68
|
+
return SECURE_INPUT_TYPES.has(t) ? t : 'text';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let _purify = null;
|
|
72
|
+
|
|
73
|
+
async function loadPurify() {
|
|
74
|
+
if (_purify) return _purify;
|
|
75
|
+
try {
|
|
76
|
+
const mod = await import('isomorphic-dompurify');
|
|
77
|
+
_purify = mod.default || mod;
|
|
78
|
+
} catch {
|
|
79
|
+
_purify = null;
|
|
80
|
+
}
|
|
81
|
+
return _purify;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const SVG_ALLOWED_TAGS = ['svg', 'g', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse', 'defs', 'use', 'symbol', 'title', 'desc', 'clipPath', 'mask', 'linearGradient', 'radialGradient', 'stop', 'pattern', 'text', 'tspan'];
|
|
85
|
+
const SVG_ALLOWED_ATTR = ['viewBox', 'xmlns', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'd', 'cx', 'cy', 'r', 'rx', 'ry', 'x', 'y', 'width', 'height', 'x1', 'y1', 'x2', 'y2', 'points', 'transform', 'opacity', 'class', 'id', 'href', 'xlink:href', 'aria-hidden', 'aria-label', 'aria-labelledby', 'role', 'focusable', 'clip-path', 'mask', 'offset', 'stop-color', 'stop-opacity', 'gradientUnits', 'gradientTransform', 'spreadMethod', 'fx', 'fy', 'patternUnits', 'patternContentUnits', 'preserveAspectRatio'];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} svgMarkup
|
|
89
|
+
*/
|
|
90
|
+
export async function sanitizeSVG(svgMarkup) {
|
|
91
|
+
if (typeof svgMarkup !== 'string') return '';
|
|
92
|
+
const DOMPurify = await loadPurify();
|
|
93
|
+
if (DOMPurify?.sanitize) {
|
|
94
|
+
return DOMPurify.sanitize(svgMarkup, {
|
|
95
|
+
USE_PROFILES: { svg: true, svgFilters: true },
|
|
96
|
+
ADD_TAGS: SVG_ALLOWED_TAGS,
|
|
97
|
+
ADD_ATTR: SVG_ALLOWED_ATTR,
|
|
98
|
+
FORBID_TAGS: ['script', 'foreignObject', 'iframe', 'object', 'embed'],
|
|
99
|
+
FORBID_ATTR: ['onload', 'onerror', 'onclick', 'onmouseover'],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return svgMarkup
|
|
103
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
104
|
+
.replace(/\s+on\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '')
|
|
105
|
+
.replace(/<foreignObject[\s\S]*?<\/foreignObject>/gi, '');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} html
|
|
110
|
+
*/
|
|
111
|
+
export async function sanitizeHTML(html) {
|
|
112
|
+
if (typeof html !== 'string') return '';
|
|
113
|
+
const DOMPurify = await loadPurify();
|
|
114
|
+
if (DOMPurify?.sanitize) return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
|
|
115
|
+
return escapeHTML(html);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let _policy = null;
|
|
119
|
+
|
|
120
|
+
export function getTrustedPolicy() {
|
|
121
|
+
if (_policy) return _policy;
|
|
122
|
+
if (typeof window !== 'undefined' && window.trustedTypes?.createPolicy) {
|
|
123
|
+
_policy = window.trustedTypes.createPolicy('velinstyle', {
|
|
124
|
+
createHTML: (input) => escapeHTML(input),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return _policy;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createSafeHTML(str) {
|
|
131
|
+
const policy = getTrustedPolicy();
|
|
132
|
+
const safe = escapeHTML(stripControlChars(str));
|
|
133
|
+
if (policy?.createHTML) return policy.createHTML(safe);
|
|
134
|
+
return safe;
|
|
135
|
+
}
|