@birdapi/velinstyle 0.7.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 -224
- package/README.md +134 -223
- package/cli/blueprint.js +8 -0
- package/cli/blueprints/bottom-nav-mobile.html +17 -0
- package/cli/blueprints/cookie-consent.html +9 -0
- package/cli/blueprints/empty-state.html +5 -0
- package/cli/blueprints/filter-bar.html +15 -0
- package/cli/blueprints/notification-center.html +13 -0
- package/cli/blueprints/onboarding.html +23 -0
- package/cli/blueprints/pricing-table.html +20 -0
- package/cli/blueprints/settings-panel.html +20 -0
- 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 +236 -5
- package/cli/layout-audit.js +325 -0
- package/cli/meta.js +97 -0
- package/cli/perf-audit.js +174 -0
- package/cli/pii-scanner.js +134 -0
- package/cli/scaffold-recipes.json +70 -0
- package/cli/scaffold.js +155 -0
- package/cli/scanner-rules-data.js +62 -0
- package/cli/scanner.js +136 -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 +40 -11
- 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 -28
- package/components/shadow-a11y-styles.js +18 -0
- package/components/velin-accordion.js +12 -0
- package/components/velin-announcer.js +35 -0
- package/components/velin-bottom-nav.js +108 -0
- package/components/velin-carousel.js +2 -2
- package/components/velin-code-block.js +142 -0
- package/components/velin-combobox.js +149 -0
- package/components/velin-command.js +142 -0
- package/components/velin-copy.js +18 -5
- package/components/velin-counter.js +155 -0
- package/components/velin-dialog.js +9 -1
- package/components/velin-email.js +94 -0
- package/components/velin-flip.js +220 -0
- package/components/velin-icon.js +99 -22
- package/components/velin-lightbox.js +8 -3
- package/components/velin-live-dot.js +90 -0
- package/components/velin-menubar.js +83 -0
- package/components/velin-modal.js +6 -3
- package/components/velin-popover.js +34 -2
- package/components/velin-rating.js +91 -0
- package/components/velin-reveal.js +22 -0
- 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-segmented-control.js +108 -0
- package/components/velin-sheet.js +107 -0
- package/components/velin-sparkline.js +227 -0
- package/components/{velin-stepper-wc.js → velin-stepper.js} +37 -8
- package/components/velin-theme-toggle.js +284 -60
- 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 +7959 -2199
- package/dist/velinstyle-components.js +7982 -2174
- package/dist/velinstyle-components.min.js +521 -90
- package/dist/velinstyle.css +1290 -282
- 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 +57 -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/security.css +18 -0
- package/src/a11y/skip-link.css +3 -1
- package/src/a11y/sr-only.css +1 -1
- package/src/base/reset.css +0 -25
- 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 +151 -151
- 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/motion.css +7 -0
- package/src/tokens/typography.css +4 -5
- package/src/utilities/animation.css +99 -3
- package/src/utilities/chart-animation.css +101 -0
- package/src/utilities/display.css +3 -3
- package/src/utilities/divide.css +2 -6
- package/src/utilities/filter-effects.css +103 -0
- package/src/utilities/responsive.css +4 -4
- package/src/utilities/safe-area.css +39 -0
- 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 +12 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanner rule metadata for documentation and tooling.
|
|
3
|
+
* Scan logic lives in scanner.js / pii-scanner.js / perf-audit.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @typedef {{ id: string, category: string, severity: string, message: string, fixHint?: string }} ScannerRuleMeta */
|
|
7
|
+
|
|
8
|
+
/** @type {ScannerRuleMeta[]} */
|
|
9
|
+
export const SCANNER_RULES = [
|
|
10
|
+
// Security — HTML
|
|
11
|
+
{ id: 'security/no-inline-handler', category: 'security', severity: 'warning', message: 'Inline event handler found. Use addEventListener() instead.', fixHint: 'Move handler to JS module.' },
|
|
12
|
+
{ id: 'security/no-javascript-url', category: 'security', severity: 'error', message: 'javascript: URL detected. XSS vector.', fixHint: 'Use button + script or href to safe URL.' },
|
|
13
|
+
{ id: 'security/safe-external-link', category: 'security', severity: 'warning', message: 'target="_blank" without rel="noopener noreferrer".', fixHint: 'Add rel="noopener noreferrer" — auto-fix with --fix.' },
|
|
14
|
+
{ id: 'security/no-meta-refresh', category: 'security', severity: 'error', message: '<meta http-equiv="refresh"> can redirect without consent.', fixHint: 'Use server redirect or JS with user action.' },
|
|
15
|
+
{ id: 'security/no-inline-style', category: 'security', severity: 'warning', message: 'Inline style attribute increases XSS surface.', fixHint: 'Use VelinStyle utility classes.' },
|
|
16
|
+
{ id: 'security/no-data-html-uri', category: 'security', severity: 'error', message: 'data:text/html URI can execute script.', fixHint: 'Avoid data: HTML URIs in href/src.' },
|
|
17
|
+
{ id: 'security/dangerous-target', category: 'security', severity: 'warning', message: '<form target="_blank"> is unusual.', fixHint: 'Use same-tab navigation.' },
|
|
18
|
+
{ id: 'security/integrity-missing', category: 'security', severity: 'info', message: 'External <script> without integrity (SRI).', fixHint: 'Add integrity + crossorigin for CDN scripts.' },
|
|
19
|
+
{ id: 'security/csp-meta', category: 'security', severity: 'info', message: 'No CSP meta tag found.', fixHint: 'Add Content-Security-Policy header or meta.' },
|
|
20
|
+
// Security — JS
|
|
21
|
+
{ id: 'security/no-raw-innerhtml', category: 'security', severity: 'warning', message: 'Direct innerHTML without sanitization.', fixHint: 'Use escapeHTML() from @birdapi/velinstyle/sanitize.' },
|
|
22
|
+
{ id: 'security/no-document-write', category: 'security', severity: 'error', message: 'document.write() is dangerous.', fixHint: 'Use DOM APIs or template elements.' },
|
|
23
|
+
{ id: 'security/no-eval', category: 'security', severity: 'error', message: 'eval() is a critical risk.', fixHint: 'Never use eval in production code.' },
|
|
24
|
+
{ id: 'security/no-function-constructor', category: 'security', severity: 'error', message: 'new Function() equals eval().', fixHint: 'Refactor to static functions.' },
|
|
25
|
+
{ id: 'security/postmessage-wildcard', category: 'security', severity: 'warning', message: 'postMessage with targetOrigin "*".', fixHint: 'Specify exact target origin.' },
|
|
26
|
+
// PII
|
|
27
|
+
{ id: 'pii/hardcoded-email', category: 'pii', severity: 'warning', message: 'Hardcoded email in source.', fixHint: 'Use env/config or <velin-email>. --fix masks to placeholder.' },
|
|
28
|
+
{ id: 'pii/mailto-in-source', category: 'pii', severity: 'info', message: 'mailto: exposes email in HTML.', fixHint: 'Consider <velin-email> obfuscation.' },
|
|
29
|
+
{ id: 'pii/hardcoded-secret', category: 'pii', severity: 'error', message: 'Possible API key or secret in source.', fixHint: 'Use environment variables or secret manager.' },
|
|
30
|
+
{ id: 'pii/localstorage-pii', category: 'pii', severity: 'warning', message: 'Email-like data in Web Storage.', fixHint: 'Avoid storing PII in localStorage.' },
|
|
31
|
+
// A11y
|
|
32
|
+
{ id: 'a11y/html-lang', category: 'a11y', severity: 'error', message: '<html> missing lang attribute.', fixHint: 'Add lang="de" or appropriate locale — --fix.' },
|
|
33
|
+
{ id: 'a11y/img-alt', category: 'a11y', severity: 'error', message: '<img> without alt.', fixHint: 'Add alt text or alt="" for decorative images.' },
|
|
34
|
+
{ id: 'a11y/img-decorative', category: 'a11y', severity: 'warning', message: 'Decorative <img alt=""> without aria-hidden.', fixHint: 'Add aria-hidden="true" when visible text carries the meaning.' },
|
|
35
|
+
{ id: 'a11y/velin-icon-label', category: 'a11y', severity: 'warning', message: 'velin-icon in icon-only button without label.', fixHint: 'Add label="…" on <velin-icon>.' },
|
|
36
|
+
{ id: 'a11y/sparkline-label', category: 'a11y', severity: 'warning', message: 'velin-sparkline without accessible name.', fixHint: 'Add label attribute or wrap in <figure><figcaption>.' },
|
|
37
|
+
{ id: 'a11y/skeleton-text', category: 'a11y', severity: 'warning', message: 'velin-skeleton on non-empty element.', fixHint: 'Use skeleton only on empty placeholders.' },
|
|
38
|
+
{ id: 'a11y/button-label', category: 'a11y', severity: 'warning', message: '<button> without visible text or aria-label.', fixHint: 'Add text or aria-label.' },
|
|
39
|
+
{ id: 'a11y/input-label', category: 'a11y', severity: 'warning', message: '<input> without label association.', fixHint: 'Use <label for> or aria-label.' },
|
|
40
|
+
{ id: 'a11y/skip-link', category: 'a11y', severity: 'warning', message: 'No skip link for keyboard users.', fixHint: 'Add .velin-skip-link to #main — --fix when id="main" exists.' },
|
|
41
|
+
{ id: 'a11y/landmark-main', category: 'a11y', severity: 'warning', message: 'No <main> landmark.', fixHint: 'Wrap primary content in <main>.' },
|
|
42
|
+
{ id: 'a11y/heading-order', category: 'a11y', severity: 'warning', message: 'Heading levels skip (e.g. h2 → h4).', fixHint: 'Use sequential heading levels.' },
|
|
43
|
+
{ id: 'a11y/interactive-aria-hidden', category: 'a11y', severity: 'error', message: 'Interactive element with aria-hidden="true".', fixHint: 'Remove aria-hidden or use inert.' },
|
|
44
|
+
{ id: 'a11y/iframe-title', category: 'a11y', severity: 'error', message: '<iframe> without title.', fixHint: 'Add descriptive title attribute.' },
|
|
45
|
+
{ id: 'a11y/autocomplete-auth', category: 'a11y', severity: 'warning', message: 'Auth field missing autocomplete (WCAG 2.2).', fixHint: 'Add autocomplete="username" or "current-password".' },
|
|
46
|
+
{ id: 'a11y/invalid-describedby', category: 'a11y', severity: 'warning', message: 'aria-invalid without aria-describedby.', fixHint: 'Link to error message element id.' },
|
|
47
|
+
// CSS
|
|
48
|
+
{ id: 'css/var-fallback', category: 'css', severity: 'info', message: 'CSS variable without fallback in var().', fixHint: 'Use var(--velin-*, fallback).' },
|
|
49
|
+
{ id: 'css/z-index-token', category: 'css', severity: 'warning', message: 'Raw z-index instead of --velin-z-* token.', fixHint: 'Use tokens from z-index.css — --fix suggests token.' },
|
|
50
|
+
{ id: 'css/no-important', category: 'css', severity: 'info', message: '!important usage.', fixHint: 'Prefer @layer and specificity.' },
|
|
51
|
+
{ id: 'css/vendor-prefix', category: 'css', severity: 'info', message: 'Unnecessary vendor prefix.', fixHint: 'Lightning CSS handles autoprefixing.' },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/** @type {ScannerRuleMeta[]} */
|
|
55
|
+
export const PERF_RULES = [
|
|
56
|
+
{ id: 'perf/img-missing-dimensions', category: 'perf', severity: 'warning', message: '<img> without width/height causes CLS.', fixHint: 'Add width and height attributes — perf fix.' },
|
|
57
|
+
{ id: 'perf/img-no-lazy', category: 'perf', severity: 'info', message: 'Consider loading="lazy" for below-fold images.', fixHint: 'Add loading="lazy".' },
|
|
58
|
+
{ id: 'perf/script-no-defer', category: 'perf', severity: 'warning', message: 'External script without defer/async.', fixHint: 'Add defer or type="module".' },
|
|
59
|
+
{ id: 'perf/large-inline-style', category: 'perf', severity: 'info', message: 'Large inline style block.', fixHint: 'Move to external CSS.' },
|
|
60
|
+
{ id: 'perf/font-display-swap', category: 'perf', severity: 'info', message: '@font-face without font-display: swap.', fixHint: 'Add font-display: swap.' },
|
|
61
|
+
{ id: 'perf/unused-velin-import', category: 'perf', severity: 'info', message: 'Full component bundle loaded.', fixHint: 'Use @birdapi/velinstyle/runtime for tree-shaking.' },
|
|
62
|
+
];
|
package/cli/scanner.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join, relative, extname } from 'path';
|
|
3
3
|
import { applyFixes, fixSafeExternalLinkLine, fixZIndexLine } from './apply-fixes.js';
|
|
4
|
+
import { scanPIIHTML, scanPIIJS } from './pii-scanner.js';
|
|
5
|
+
|
|
6
|
+
export { SCANNER_RULES, PERF_RULES } from './scanner-rules-data.js';
|
|
4
7
|
|
|
5
8
|
const SEVERITY = { error: 0, warning: 1, info: 2 };
|
|
6
9
|
const SEVERITY_LABEL = { 0: 'ERROR', 1: 'WARNING', 2: 'INFO' };
|
|
@@ -80,6 +83,51 @@ function scanSecurityHTML(content, file) {
|
|
|
80
83
|
fix: (currentLine) => fixSafeExternalLinkLine(currentLine),
|
|
81
84
|
});
|
|
82
85
|
}
|
|
86
|
+
|
|
87
|
+
if (/<meta\s[^>]*http-equiv\s*=\s*["']refresh["']/i.test(line)) {
|
|
88
|
+
issues.push({
|
|
89
|
+
file, line: ln, severity: 0,
|
|
90
|
+
rule: 'security/no-meta-refresh',
|
|
91
|
+
message: '<meta http-equiv="refresh"> can redirect users without consent.',
|
|
92
|
+
fixable: false,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (/\sstyle\s*=\s*["'][^"']+["']/i.test(line) && !/velin-user-content/i.test(line)) {
|
|
97
|
+
issues.push({
|
|
98
|
+
file, line: ln, severity: 1,
|
|
99
|
+
rule: 'security/no-inline-style',
|
|
100
|
+
message: 'Inline style attribute. Prefer CSS classes to reduce XSS surface.',
|
|
101
|
+
fixable: false,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (/(?:href|src)\s*=\s*["']data:text\/html/i.test(line)) {
|
|
106
|
+
issues.push({
|
|
107
|
+
file, line: ln, severity: 0,
|
|
108
|
+
rule: 'security/no-data-html-uri',
|
|
109
|
+
message: 'data:text/html URI can execute script when mishandled.',
|
|
110
|
+
fixable: false,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (/<form\b[^>]*\btarget\s*=\s*["']_blank["']/i.test(line)) {
|
|
115
|
+
issues.push({
|
|
116
|
+
file, line: ln, severity: 1,
|
|
117
|
+
rule: 'security/dangerous-target',
|
|
118
|
+
message: '<form target="_blank"> is unusual and can be abused. Prefer same-tab navigation.',
|
|
119
|
+
fixable: false,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (/<script\b[^>]*\bsrc\s*=\s*["']https?:\/\//i.test(line) && !/\bintegrity\s*=/i.test(line)) {
|
|
124
|
+
issues.push({
|
|
125
|
+
file, line: ln, severity: 2,
|
|
126
|
+
rule: 'security/integrity-missing',
|
|
127
|
+
message: 'External <script> without integrity attribute. Use SRI for CDN scripts.',
|
|
128
|
+
fixable: false,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
83
131
|
});
|
|
84
132
|
|
|
85
133
|
if (!/<meta\s[^>]*http-equiv\s*=\s*["']Content-Security-Policy["']/i.test(content)) {
|
|
@@ -136,11 +184,29 @@ function scanSecurityJS(content, file) {
|
|
|
136
184
|
fixable: false,
|
|
137
185
|
});
|
|
138
186
|
}
|
|
187
|
+
|
|
188
|
+
if (/\.postMessage\s*\([^)]*,\s*['"]\*['"]\s*\)/.test(line)) {
|
|
189
|
+
issues.push({
|
|
190
|
+
file, line: ln, severity: 1,
|
|
191
|
+
rule: 'security/postmessage-wildcard',
|
|
192
|
+
message: 'postMessage with targetOrigin "*" accepts any origin.',
|
|
193
|
+
fixable: false,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
139
196
|
});
|
|
140
197
|
|
|
141
198
|
return issues;
|
|
142
199
|
}
|
|
143
200
|
|
|
201
|
+
function issueCategory(rule) {
|
|
202
|
+
if (rule.startsWith('security/')) return 'security';
|
|
203
|
+
if (rule.startsWith('pii/')) return 'pii';
|
|
204
|
+
if (rule.startsWith('a11y/')) return 'a11y';
|
|
205
|
+
if (rule.startsWith('css/')) return 'css';
|
|
206
|
+
if (rule.startsWith('perf/')) return 'perf';
|
|
207
|
+
return 'other';
|
|
208
|
+
}
|
|
209
|
+
|
|
144
210
|
// ── Accessibility Scanner ────────────────────────────────────────────────────
|
|
145
211
|
|
|
146
212
|
function scanA11yHTML(content, file) {
|
|
@@ -161,13 +227,25 @@ function scanA11yHTML(content, file) {
|
|
|
161
227
|
|
|
162
228
|
const imgMatches = line.matchAll(/<img\b[^>]*>/gi);
|
|
163
229
|
for (const m of imgMatches) {
|
|
164
|
-
|
|
230
|
+
const tag = m[0];
|
|
231
|
+
if (!/\balt\s*=/i.test(tag)) {
|
|
165
232
|
issues.push({
|
|
166
233
|
file, line: ln, severity: 0,
|
|
167
234
|
rule: 'a11y/img-alt',
|
|
168
235
|
message: '<img> without alt attribute. Add alt="" for decorative or descriptive text.',
|
|
169
236
|
fixable: false,
|
|
170
237
|
});
|
|
238
|
+
} else if (
|
|
239
|
+
/\balt\s*=\s*["']\s*["']/i.test(tag)
|
|
240
|
+
&& !/\baria-hidden\s*=\s*["']true["']/i.test(tag)
|
|
241
|
+
&& !/\brole\s*=\s*["']presentation["']/i.test(tag)
|
|
242
|
+
) {
|
|
243
|
+
issues.push({
|
|
244
|
+
file, line: ln, severity: 1,
|
|
245
|
+
rule: 'a11y/img-decorative',
|
|
246
|
+
message: 'Decorative <img alt=""> should include aria-hidden="true" so assistive tech ignores it consistently.',
|
|
247
|
+
fixable: false,
|
|
248
|
+
});
|
|
171
249
|
}
|
|
172
250
|
}
|
|
173
251
|
|
|
@@ -183,6 +261,31 @@ function scanA11yHTML(content, file) {
|
|
|
183
261
|
}
|
|
184
262
|
}
|
|
185
263
|
|
|
264
|
+
if (/<button\b[^>]*>[\s\S]*<velin-icon\b/i.test(line) && /<velin-icon\b(?![^>]*\blabel\s*=)/i.test(line)) {
|
|
265
|
+
issues.push({
|
|
266
|
+
file, line: ln, severity: 1,
|
|
267
|
+
rule: 'a11y/velin-icon-label',
|
|
268
|
+
message: '<velin-icon> inside icon-only <button> should have a label attribute.',
|
|
269
|
+
fixable: false,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (/<velin-sparkline\b/i.test(line) && !/<velin-sparkline\b[^>]*\blabel\s*=/i.test(line) && !/<figcaption/i.test(line)) {
|
|
273
|
+
issues.push({
|
|
274
|
+
file, line: ln, severity: 1,
|
|
275
|
+
rule: 'a11y/sparkline-label',
|
|
276
|
+
message: '<velin-sparkline> needs label or a parent <figure> with <figcaption>.',
|
|
277
|
+
fixable: false,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (/\bvelin-skeleton\b/i.test(line) && />\s*[^<\s][^<]+</i.test(line)) {
|
|
281
|
+
issues.push({
|
|
282
|
+
file, line: ln, severity: 1,
|
|
283
|
+
rule: 'a11y/skeleton-text',
|
|
284
|
+
message: 'velin-skeleton on elements with visible text may hide content from assistive tech.',
|
|
285
|
+
fixable: false,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
186
289
|
const inputMatches = line.matchAll(/<input\b[^>]*>/gi);
|
|
187
290
|
for (const m of inputMatches) {
|
|
188
291
|
const tag = m[0];
|
|
@@ -251,6 +354,27 @@ function scanA11yHTML(content, file) {
|
|
|
251
354
|
});
|
|
252
355
|
}
|
|
253
356
|
}
|
|
357
|
+
|
|
358
|
+
if (/<form\b/i.test(line) && /type\s*=\s*["'](?:password|email)["']/i.test(line)) {
|
|
359
|
+
const formCtx = content.slice(Math.max(0, content.indexOf(line) - 800), content.indexOf(line) + 200);
|
|
360
|
+
if (/type\s*=\s*["']password["']/i.test(formCtx) && !/autocomplete\s*=/i.test(line) && !/autocomplete\s*=/i.test(formCtx)) {
|
|
361
|
+
issues.push({
|
|
362
|
+
file, line: ln, severity: 1,
|
|
363
|
+
rule: 'a11y/autocomplete-auth',
|
|
364
|
+
message: 'Auth field missing autocomplete (WCAG 2.2 / 1.3.5). Add autocomplete="username" or "current-password".',
|
|
365
|
+
fixable: false,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (/aria-invalid\s*=\s*["']true["']/i.test(line) && !/aria-describedby/i.test(line)) {
|
|
371
|
+
issues.push({
|
|
372
|
+
file, line: ln, severity: 1,
|
|
373
|
+
rule: 'a11y/invalid-describedby',
|
|
374
|
+
message: 'aria-invalid without aria-describedby linking to error help text.',
|
|
375
|
+
fixable: false,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
254
378
|
});
|
|
255
379
|
|
|
256
380
|
return issues;
|
|
@@ -325,6 +449,9 @@ export function scan(targetPath, options = {}) {
|
|
|
325
449
|
const writeFixes = doFix && !fixDryRun;
|
|
326
450
|
const runFixPipeline = doFix || fixDryRun;
|
|
327
451
|
const ignore = options.ignore || DEFAULT_IGNORE;
|
|
452
|
+
const onlyCategories = options.only
|
|
453
|
+
? options.only.split(',').map((s) => s.trim().toLowerCase())
|
|
454
|
+
: null;
|
|
328
455
|
|
|
329
456
|
const htmlFiles = walkFiles(targetPath, ['.html', '.htm'], ignore);
|
|
330
457
|
const cssFiles = walkFiles(targetPath, ['.css'], ignore);
|
|
@@ -332,10 +459,13 @@ export function scan(targetPath, options = {}) {
|
|
|
332
459
|
|
|
333
460
|
let allIssues = [];
|
|
334
461
|
|
|
462
|
+
const fixEmailPlaceholder = options.fixEmailPlaceholder || 'user@example.com';
|
|
463
|
+
|
|
335
464
|
for (const file of htmlFiles) {
|
|
336
465
|
const content = readFileSync(file, 'utf-8');
|
|
337
466
|
allIssues.push(...scanSecurityHTML(content, file));
|
|
338
467
|
allIssues.push(...scanA11yHTML(content, file));
|
|
468
|
+
allIssues.push(...scanPIIHTML(content, file, { fixEmailPlaceholder }));
|
|
339
469
|
}
|
|
340
470
|
|
|
341
471
|
for (const file of cssFiles) {
|
|
@@ -346,9 +476,13 @@ export function scan(targetPath, options = {}) {
|
|
|
346
476
|
for (const file of jsFiles) {
|
|
347
477
|
const content = readFileSync(file, 'utf-8');
|
|
348
478
|
allIssues.push(...scanSecurityJS(content, file));
|
|
479
|
+
allIssues.push(...scanPIIJS(content, file));
|
|
349
480
|
}
|
|
350
481
|
|
|
351
482
|
allIssues = allIssues.filter(i => i.severity <= minSeverity);
|
|
483
|
+
if (onlyCategories?.length) {
|
|
484
|
+
allIssues = allIssues.filter((i) => onlyCategories.includes(issueCategory(i.rule)));
|
|
485
|
+
}
|
|
352
486
|
allIssues.sort((a, b) => a.severity - b.severity || a.file.localeCompare(b.file) || a.line - b.line);
|
|
353
487
|
|
|
354
488
|
let fixSummary = null;
|
|
@@ -368,6 +502,7 @@ export function scan(targetPath, options = {}) {
|
|
|
368
502
|
file: relative(targetPath, i.file),
|
|
369
503
|
line: i.line,
|
|
370
504
|
severity: SEVERITY_LABEL[i.severity],
|
|
505
|
+
category: issueCategory(i.rule),
|
|
371
506
|
rule: i.rule,
|
|
372
507
|
message: i.message,
|
|
373
508
|
fixable: !!i.fixable,
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync } from 'fs';
|
|
2
|
+
import { join, dirname, relative, basename } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const PKG_ROOT = join(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
const TITLE_RE = /^#\s+<([^>]+)>|^#\s+(.+)$/m;
|
|
9
|
+
const H1_RE = /^#\s+(.+)$/m;
|
|
10
|
+
|
|
11
|
+
function excerptFromMd(text, max = 160) {
|
|
12
|
+
const lines = text.split('\n');
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
if (line.startsWith('#') || !line.trim() || line.startsWith('|') || line.startsWith('<!--')) continue;
|
|
15
|
+
return line.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1').replace(/`/g, '').trim().slice(0, max);
|
|
16
|
+
}
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function slugId(category, name) {
|
|
21
|
+
return `${category}:${name}`.replace(/\s+/g, '-').toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function walkFiles(dir, cb) {
|
|
25
|
+
if (!existsSync(dir)) return;
|
|
26
|
+
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
|
27
|
+
const full = join(dir, ent.name);
|
|
28
|
+
if (ent.isDirectory()) walkFiles(full, cb);
|
|
29
|
+
else cb(full);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectMarkdown(dir, category, urlPrefix, weight = 1) {
|
|
34
|
+
const entries = [];
|
|
35
|
+
if (!existsSync(dir)) return entries;
|
|
36
|
+
walkFiles(dir, (full) => {
|
|
37
|
+
const name = basename(full);
|
|
38
|
+
if (!name.endsWith('.md') || name === 'README.md') return;
|
|
39
|
+
const text = readFileSync(full, 'utf-8');
|
|
40
|
+
const tagMatch = text.match(TITLE_RE);
|
|
41
|
+
const h1Match = text.match(H1_RE);
|
|
42
|
+
const title = tagMatch
|
|
43
|
+
? (tagMatch[1] || tagMatch[2] || basename(name, '.md')).trim()
|
|
44
|
+
: (h1Match ? h1Match[1] : basename(name, '.md')).replace(/`/g, '').trim();
|
|
45
|
+
const rel = relative(dir, full).replace(/\\/g, '/');
|
|
46
|
+
const base = basename(name, '.md');
|
|
47
|
+
entries.push({
|
|
48
|
+
id: slugId(category, base),
|
|
49
|
+
title,
|
|
50
|
+
excerpt: excerptFromMd(text),
|
|
51
|
+
url: `${urlPrefix}${rel.replace(/\.md$/, '.html')}`,
|
|
52
|
+
category,
|
|
53
|
+
keywords: [base, category, title],
|
|
54
|
+
weight,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
return entries;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function collectHtmlSamples(dirs) {
|
|
61
|
+
const entries = [];
|
|
62
|
+
for (const dir of dirs) {
|
|
63
|
+
if (!existsSync(dir)) continue;
|
|
64
|
+
const walk = (folder) => {
|
|
65
|
+
for (const ent of readdirSync(folder, { withFileTypes: true })) {
|
|
66
|
+
const full = join(folder, ent.name);
|
|
67
|
+
if (ent.isDirectory()) walk(full);
|
|
68
|
+
else if (ent.name.endsWith('.html')) {
|
|
69
|
+
const text = readFileSync(full, 'utf-8');
|
|
70
|
+
const titleM = text.match(/<title>([^<]+)<\/title>/i);
|
|
71
|
+
const h1M = text.match(/<h1[^>]*>([^<]+)<\/h1>/i);
|
|
72
|
+
const title = (h1M?.[1] || titleM?.[1] || ent.name).replace(/<[^>]+>/g, '').trim();
|
|
73
|
+
const rel = relative(PKG_ROOT, full).replace(/\\/g, '/');
|
|
74
|
+
entries.push({
|
|
75
|
+
id: slugId('examples', rel),
|
|
76
|
+
title,
|
|
77
|
+
excerpt: rel,
|
|
78
|
+
url: rel,
|
|
79
|
+
category: 'examples',
|
|
80
|
+
keywords: [ent.name, 'sample', 'example'],
|
|
81
|
+
weight: 0.85,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
walk(dir);
|
|
87
|
+
}
|
|
88
|
+
return entries;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function collectCliApi(manifestPath) {
|
|
92
|
+
const entries = [];
|
|
93
|
+
if (!existsSync(manifestPath)) return entries;
|
|
94
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
95
|
+
for (const cmd of manifest.commands || []) {
|
|
96
|
+
entries.push({
|
|
97
|
+
id: slugId('api', cmd.name || cmd.id),
|
|
98
|
+
title: `CLI: ${cmd.name || cmd.id}`,
|
|
99
|
+
excerpt: cmd.summary || cmd.description || '',
|
|
100
|
+
url: `docs/generated/cli/commands.html#${cmd.name || cmd.id}`,
|
|
101
|
+
category: 'api',
|
|
102
|
+
keywords: ['cli', cmd.name, ...(cmd.aliases || [])],
|
|
103
|
+
weight: 1.1,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {object} [options]
|
|
111
|
+
* @param {string} [options.outFile]
|
|
112
|
+
* @param {string} [options.generatedDir]
|
|
113
|
+
* @param {string[]} [options.extraHtmlDirs]
|
|
114
|
+
*/
|
|
115
|
+
export function buildSearchIndex(options = {}) {
|
|
116
|
+
const generatedDir = options.generatedDir || join(PKG_ROOT, 'docs', 'generated');
|
|
117
|
+
const outFile = options.outFile || join(PKG_ROOT, 'dist', 'search-index.json');
|
|
118
|
+
|
|
119
|
+
const entries = [
|
|
120
|
+
...collectMarkdown(join(generatedDir, 'components'), 'components', 'docs/generated/components/', 1.3),
|
|
121
|
+
...collectMarkdown(join(generatedDir, 'tokens'), 'docs', 'docs/generated/tokens/', 1),
|
|
122
|
+
...collectMarkdown(join(generatedDir, 'utilities'), 'docs', 'docs/generated/utilities/', 1),
|
|
123
|
+
...collectMarkdown(join(generatedDir, 'cli'), 'api', 'docs/generated/cli/', 1.15),
|
|
124
|
+
...collectMarkdown(join(generatedDir, 'rules'), 'docs', 'docs/generated/rules/', 0.95),
|
|
125
|
+
...collectMarkdown(join(generatedDir, 'a11y'), 'docs', 'docs/generated/a11y/', 1),
|
|
126
|
+
...collectMarkdown(join(generatedDir, 'attributes'), 'docs', 'docs/generated/attributes/', 1.2),
|
|
127
|
+
...collectCliApi(join(PKG_ROOT, 'cli', 'cli-manifest.json')),
|
|
128
|
+
...collectHtmlSamples([
|
|
129
|
+
join(PKG_ROOT, 'samples'),
|
|
130
|
+
join(PKG_ROOT, 'examples'),
|
|
131
|
+
...(options.extraHtmlDirs || []),
|
|
132
|
+
]),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
const unique = [];
|
|
137
|
+
for (const e of entries) {
|
|
138
|
+
if (seen.has(e.id)) continue;
|
|
139
|
+
seen.add(e.id);
|
|
140
|
+
unique.push(e);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
unique.sort((a, b) => a.title.localeCompare(b.title));
|
|
144
|
+
|
|
145
|
+
const payload = {
|
|
146
|
+
version: 1,
|
|
147
|
+
generatedAt: new Date().toISOString(),
|
|
148
|
+
entries: unique,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
152
|
+
writeFileSync(outFile, JSON.stringify(payload, null, 2), 'utf-8');
|
|
153
|
+
return { ok: true, count: unique.length, outFile };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function searchIndexMain(argv = process.argv.slice(2)) {
|
|
157
|
+
const outIdx = argv.indexOf('--out');
|
|
158
|
+
const extraIdx = argv.indexOf('--extra-html');
|
|
159
|
+
const outFile = outIdx !== -1 && argv[outIdx + 1] ? argv[outIdx + 1] : join(PKG_ROOT, 'dist', 'search-index.json');
|
|
160
|
+
const extra = extraIdx !== -1 && argv[extraIdx + 1] ? argv[extraIdx + 1].split(',') : [];
|
|
161
|
+
const result = buildSearchIndex({ outFile, extraHtmlDirs: extra });
|
|
162
|
+
console.log(`Wrote ${result.count} search entries → ${result.outFile}`);
|
|
163
|
+
}
|
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
|
+
}
|